@webdevarif/dashui 0.4.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +2 -1
- package/templates/README.md +74 -0
- package/templates/api-patterns/CRUD-route.pattern.ts +112 -0
- package/templates/hooks/useCreate.ts +46 -0
- package/templates/hooks/useDelete.ts +42 -0
- package/templates/hooks/useFetch.ts +30 -0
- package/templates/hooks/useUpdate.ts +46 -0
- package/templates/middleware/auth-middleware.ts +39 -0
- package/templates/nextauth/auth.config.ts +110 -0
- package/templates/nextauth/auth.ts +11 -0
- package/templates/nextauth/route-handlers/[auth].ts +10 -0
- package/templates/prisma/multi-tenant-schema.prisma +165 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@webdevarif/dashui",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"description": "Universal dashboard UI component library — forms, inputs, media, tables, layouts. Modular categories: primitives, forms, dashboard, media, data, editors, ecommerce, cms.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"dashboard",
|
|
@@ -34,6 +34,7 @@
|
|
|
34
34
|
},
|
|
35
35
|
"files": [
|
|
36
36
|
"dist",
|
|
37
|
+
"templates",
|
|
37
38
|
"README.md"
|
|
38
39
|
],
|
|
39
40
|
"scripts": {
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# dashui Templates
|
|
2
|
+
|
|
3
|
+
Copy-paste starter code for your Next.js project. Customize to your API/database.
|
|
4
|
+
|
|
5
|
+
## What's included
|
|
6
|
+
|
|
7
|
+
- **nextauth/** — NextAuth v6 edge-safe configuration + route handlers
|
|
8
|
+
- **prisma/** — Base multi-tenant Prisma schemas (SaaS, E-commerce, CMS)
|
|
9
|
+
- **hooks/** — SWR data fetching patterns (useFetch, useCreate, useUpdate, useDelete)
|
|
10
|
+
- **middleware/** — Auth middleware, error handling, rate limiting
|
|
11
|
+
- **api-patterns/** — CRUD route templates, file uploads, webhooks
|
|
12
|
+
|
|
13
|
+
## Quick start
|
|
14
|
+
|
|
15
|
+
### 1. Copy NextAuth config
|
|
16
|
+
```bash
|
|
17
|
+
cp templates/nextauth/auth.config.ts ./
|
|
18
|
+
cp templates/nextauth/auth.ts ./
|
|
19
|
+
cp -r templates/nextauth/route-handlers ./app/api/auth/
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
Then customize your provider credentials in `.env`.
|
|
23
|
+
|
|
24
|
+
### 2. Copy Prisma schema
|
|
25
|
+
```bash
|
|
26
|
+
cp templates/prisma/multi-tenant-schema.prisma ./prisma/schema.prisma
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
Add your custom models.
|
|
30
|
+
|
|
31
|
+
### 3. Copy data fetching hooks
|
|
32
|
+
```bash
|
|
33
|
+
cp templates/hooks/*.ts ./lib/
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
Use in components:
|
|
37
|
+
```tsx
|
|
38
|
+
const { data, error, isLoading } = useFetch('/api/products')
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
### 4. Copy middleware
|
|
42
|
+
```bash
|
|
43
|
+
cp templates/middleware/auth-middleware.ts ./lib/
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
Use in API routes:
|
|
47
|
+
```tsx
|
|
48
|
+
import { requireAuth } from '@/lib/auth-middleware'
|
|
49
|
+
|
|
50
|
+
export async function POST(req) {
|
|
51
|
+
const user = await requireAuth(req)
|
|
52
|
+
// user is authenticated
|
|
53
|
+
}
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
## Before publishing
|
|
57
|
+
|
|
58
|
+
Each template is a starting point. **Customize to your needs:**
|
|
59
|
+
- Environment variables (OAuth credentials, database URL, API keys)
|
|
60
|
+
- Prisma relations (add your custom models)
|
|
61
|
+
- API endpoints (match your backend structure)
|
|
62
|
+
- Error handling (log to your service)
|
|
63
|
+
- Rate limits (adjust for your use case)
|
|
64
|
+
|
|
65
|
+
## FAQ
|
|
66
|
+
|
|
67
|
+
**Q: Can I modify these templates?**
|
|
68
|
+
A: Yes! They're in your project now. Make them yours.
|
|
69
|
+
|
|
70
|
+
**Q: Why not a generator/CLI?**
|
|
71
|
+
A: Copy-paste is simpler, more transparent, easier to customize. You see exactly what's installed.
|
|
72
|
+
|
|
73
|
+
**Q: How do I keep up with dashui updates?**
|
|
74
|
+
A: Components update → just re-export from dashui. Templates are static starter code — no updates needed unless you want them.
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CRUD API Route Pattern
|
|
3
|
+
*
|
|
4
|
+
* Copy and modify for your models.
|
|
5
|
+
* This example shows: GET, POST, PATCH, DELETE
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { NextRequest, NextResponse } from 'next/server'
|
|
9
|
+
import { requireAuth, requireRole } from '@/lib/auth-middleware'
|
|
10
|
+
import { prisma } from '@/lib/prisma'
|
|
11
|
+
|
|
12
|
+
// GET — List with pagination + filters
|
|
13
|
+
export async function GET(req: NextRequest) {
|
|
14
|
+
try {
|
|
15
|
+
const user = await requireAuth(req)
|
|
16
|
+
|
|
17
|
+
const url = new URL(req.url)
|
|
18
|
+
const page = parseInt(url.searchParams.get('page') || '1')
|
|
19
|
+
const limit = parseInt(url.searchParams.get('limit') || '20')
|
|
20
|
+
const skip = (page - 1) * limit
|
|
21
|
+
|
|
22
|
+
// TODO: Add your filters/search logic here
|
|
23
|
+
const items = await prisma.product.findMany({
|
|
24
|
+
skip,
|
|
25
|
+
take: limit,
|
|
26
|
+
// where: { storeId: user.storeId }, // filter by user's store if applicable
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
const total = await prisma.product.count()
|
|
30
|
+
|
|
31
|
+
return NextResponse.json({
|
|
32
|
+
data: items,
|
|
33
|
+
pagination: { page, limit, total, pages: Math.ceil(total / limit) },
|
|
34
|
+
})
|
|
35
|
+
} catch (err) {
|
|
36
|
+
console.error('GET error:', err)
|
|
37
|
+
return NextResponse.json({ error: 'Failed to fetch' }, { status: 500 })
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// POST — Create
|
|
42
|
+
export async function POST(req: NextRequest) {
|
|
43
|
+
try {
|
|
44
|
+
const user = await requireAuth(req)
|
|
45
|
+
const body = await req.json()
|
|
46
|
+
|
|
47
|
+
// TODO: Validate body with zod or similar
|
|
48
|
+
if (!body.name) {
|
|
49
|
+
return NextResponse.json({ error: 'Name required' }, { status: 400 })
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// TODO: Check permissions (user can create?)
|
|
53
|
+
const item = await prisma.product.create({
|
|
54
|
+
data: {
|
|
55
|
+
name: body.name,
|
|
56
|
+
description: body.description,
|
|
57
|
+
price: body.price,
|
|
58
|
+
// storeId: user.storeId, // if multi-tenant
|
|
59
|
+
},
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
return NextResponse.json(item, { status: 201 })
|
|
63
|
+
} catch (err) {
|
|
64
|
+
console.error('POST error:', err)
|
|
65
|
+
return NextResponse.json({ error: 'Failed to create' }, { status: 500 })
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// PATCH — Update
|
|
70
|
+
export async function PATCH(req: NextRequest) {
|
|
71
|
+
try {
|
|
72
|
+
const user = await requireAuth(req)
|
|
73
|
+
const body = await req.json()
|
|
74
|
+
const { id, ...data } = body
|
|
75
|
+
|
|
76
|
+
if (!id) {
|
|
77
|
+
return NextResponse.json({ error: 'ID required' }, { status: 400 })
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// TODO: Check permissions (user owns this item?)
|
|
81
|
+
const item = await prisma.product.update({
|
|
82
|
+
where: { id },
|
|
83
|
+
data,
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
return NextResponse.json(item)
|
|
87
|
+
} catch (err) {
|
|
88
|
+
console.error('PATCH error:', err)
|
|
89
|
+
return NextResponse.json({ error: 'Failed to update' }, { status: 500 })
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// DELETE
|
|
94
|
+
export async function DELETE(req: NextRequest) {
|
|
95
|
+
try {
|
|
96
|
+
const user = await requireAuth(req)
|
|
97
|
+
const { searchParams } = new URL(req.url)
|
|
98
|
+
const id = searchParams.get('id')
|
|
99
|
+
|
|
100
|
+
if (!id) {
|
|
101
|
+
return NextResponse.json({ error: 'ID required' }, { status: 400 })
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// TODO: Check permissions (user owns this item?)
|
|
105
|
+
await prisma.product.delete({ where: { id } })
|
|
106
|
+
|
|
107
|
+
return NextResponse.json({ success: true })
|
|
108
|
+
} catch (err) {
|
|
109
|
+
console.error('DELETE error:', err)
|
|
110
|
+
return NextResponse.json({ error: 'Failed to delete' }, { status: 500 })
|
|
111
|
+
}
|
|
112
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* useCreate — POST request with optimistic update
|
|
3
|
+
*
|
|
4
|
+
* Copy to: ./lib/hooks/useCreate.ts
|
|
5
|
+
*
|
|
6
|
+
* Usage:
|
|
7
|
+
* const { mutate, isLoading, error } = useCreate('/api/products')
|
|
8
|
+
* await mutate({ name: 'New Product', price: 99.99 })
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { useState } from 'react'
|
|
12
|
+
|
|
13
|
+
export function useCreate<T = any>(endpoint: string) {
|
|
14
|
+
const [isLoading, setIsLoading] = useState(false)
|
|
15
|
+
const [error, setError] = useState<Error | null>(null)
|
|
16
|
+
|
|
17
|
+
async function mutate(data: any) {
|
|
18
|
+
setIsLoading(true)
|
|
19
|
+
setError(null)
|
|
20
|
+
|
|
21
|
+
try {
|
|
22
|
+
const response = await fetch(endpoint, {
|
|
23
|
+
method: 'POST',
|
|
24
|
+
headers: {
|
|
25
|
+
'Content-Type': 'application/json',
|
|
26
|
+
},
|
|
27
|
+
body: JSON.stringify(data),
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
if (!response.ok) {
|
|
31
|
+
throw new Error(`Create failed: ${response.statusText}`)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const result = await response.json()
|
|
35
|
+
return result as T
|
|
36
|
+
} catch (err) {
|
|
37
|
+
const error = err instanceof Error ? err : new Error('Unknown error')
|
|
38
|
+
setError(error)
|
|
39
|
+
throw error
|
|
40
|
+
} finally {
|
|
41
|
+
setIsLoading(false)
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return { mutate, isLoading, error }
|
|
46
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* useDelete — DELETE request
|
|
3
|
+
*
|
|
4
|
+
* Copy to: ./lib/hooks/useDelete.ts
|
|
5
|
+
*
|
|
6
|
+
* Usage:
|
|
7
|
+
* const { mutate, isLoading, error } = useDelete('/api/products/123')
|
|
8
|
+
* await mutate()
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { useState } from 'react'
|
|
12
|
+
|
|
13
|
+
export function useDelete<T = any>(endpoint: string) {
|
|
14
|
+
const [isLoading, setIsLoading] = useState(false)
|
|
15
|
+
const [error, setError] = useState<Error | null>(null)
|
|
16
|
+
|
|
17
|
+
async function mutate() {
|
|
18
|
+
setIsLoading(true)
|
|
19
|
+
setError(null)
|
|
20
|
+
|
|
21
|
+
try {
|
|
22
|
+
const response = await fetch(endpoint, {
|
|
23
|
+
method: 'DELETE',
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
if (!response.ok) {
|
|
27
|
+
throw new Error(`Delete failed: ${response.statusText}`)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const result = await response.json()
|
|
31
|
+
return result as T
|
|
32
|
+
} catch (err) {
|
|
33
|
+
const error = err instanceof Error ? err : new Error('Unknown error')
|
|
34
|
+
setError(error)
|
|
35
|
+
throw error
|
|
36
|
+
} finally {
|
|
37
|
+
setIsLoading(false)
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return { mutate, isLoading, error }
|
|
42
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* useFetch — Generic SWR wrapper for GET requests
|
|
3
|
+
*
|
|
4
|
+
* Copy to: ./lib/hooks/useFetch.ts
|
|
5
|
+
*
|
|
6
|
+
* Usage:
|
|
7
|
+
* const { data, error, isLoading } = useFetch('/api/products')
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import useSWR from 'swr'
|
|
11
|
+
|
|
12
|
+
const fetcher = (url: string) => fetch(url).then((r) => r.json())
|
|
13
|
+
|
|
14
|
+
export function useFetch<T = any>(endpoint: string | null) {
|
|
15
|
+
const { data, error, isLoading } = useSWR<T>(
|
|
16
|
+
endpoint, // null = skip fetching
|
|
17
|
+
fetcher,
|
|
18
|
+
{
|
|
19
|
+
revalidateOnFocus: false,
|
|
20
|
+
revalidateOnReconnect: false,
|
|
21
|
+
dedupingInterval: 60000,
|
|
22
|
+
}
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
return {
|
|
26
|
+
data,
|
|
27
|
+
error,
|
|
28
|
+
isLoading,
|
|
29
|
+
}
|
|
30
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* useUpdate — PATCH request with optimistic update
|
|
3
|
+
*
|
|
4
|
+
* Copy to: ./lib/hooks/useUpdate.ts
|
|
5
|
+
*
|
|
6
|
+
* Usage:
|
|
7
|
+
* const { mutate, isLoading, error } = useUpdate('/api/products/123')
|
|
8
|
+
* await mutate({ name: 'Updated Product' })
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { useState } from 'react'
|
|
12
|
+
|
|
13
|
+
export function useUpdate<T = any>(endpoint: string) {
|
|
14
|
+
const [isLoading, setIsLoading] = useState(false)
|
|
15
|
+
const [error, setError] = useState<Error | null>(null)
|
|
16
|
+
|
|
17
|
+
async function mutate(data: any) {
|
|
18
|
+
setIsLoading(true)
|
|
19
|
+
setError(null)
|
|
20
|
+
|
|
21
|
+
try {
|
|
22
|
+
const response = await fetch(endpoint, {
|
|
23
|
+
method: 'PATCH',
|
|
24
|
+
headers: {
|
|
25
|
+
'Content-Type': 'application/json',
|
|
26
|
+
},
|
|
27
|
+
body: JSON.stringify(data),
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
if (!response.ok) {
|
|
31
|
+
throw new Error(`Update failed: ${response.statusText}`)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const result = await response.json()
|
|
35
|
+
return result as T
|
|
36
|
+
} catch (err) {
|
|
37
|
+
const error = err instanceof Error ? err : new Error('Unknown error')
|
|
38
|
+
setError(error)
|
|
39
|
+
throw error
|
|
40
|
+
} finally {
|
|
41
|
+
setIsLoading(false)
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return { mutate, isLoading, error }
|
|
46
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* requireAuth — API route middleware to enforce authentication
|
|
3
|
+
*
|
|
4
|
+
* Copy to: ./lib/auth-middleware.ts
|
|
5
|
+
*
|
|
6
|
+
* Usage in API routes:
|
|
7
|
+
* export async function POST(req: Request) {
|
|
8
|
+
* const user = await requireAuth(req)
|
|
9
|
+
* // user is authenticated; access user.id, user.email, etc.
|
|
10
|
+
* }
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { auth } from '@/auth'
|
|
14
|
+
import { NextRequest } from 'next/server'
|
|
15
|
+
|
|
16
|
+
export async function requireAuth(req: NextRequest) {
|
|
17
|
+
const session = await auth()
|
|
18
|
+
|
|
19
|
+
if (!session || !session.user) {
|
|
20
|
+
throw new Error('Unauthorized: Not authenticated', { cause: 'UNAUTHENTICATED' })
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
return session.user
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export async function requireRole(req: NextRequest, requiredRole: string) {
|
|
27
|
+
const user = await requireAuth(req)
|
|
28
|
+
|
|
29
|
+
const role = (user as any).role || 'user'
|
|
30
|
+
|
|
31
|
+
// Simple role check (admin can do everything)
|
|
32
|
+
if (role === 'admin') return user
|
|
33
|
+
|
|
34
|
+
if (role !== requiredRole) {
|
|
35
|
+
throw new Error(`Forbidden: Requires role ${requiredRole}`, { cause: 'FORBIDDEN' })
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return user
|
|
39
|
+
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* NextAuth v6 Edge-safe Config
|
|
3
|
+
*
|
|
4
|
+
* Copy to: ./auth.config.ts
|
|
5
|
+
* Customize: Add your OAuth provider credentials to .env
|
|
6
|
+
*
|
|
7
|
+
* Providers:
|
|
8
|
+
* - Credentials (username/password)
|
|
9
|
+
* - GitHub (OAuth)
|
|
10
|
+
* - Google (OAuth)
|
|
11
|
+
* - Add more as needed
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import type { NextAuthConfig } from 'next-auth'
|
|
15
|
+
import Credentials from 'next-auth/providers/credentials'
|
|
16
|
+
import GitHub from 'next-auth/providers/github'
|
|
17
|
+
import Google from 'next-auth/providers/google'
|
|
18
|
+
import { PrismaAdapter } from '@auth/prisma-adapter'
|
|
19
|
+
import { prisma } from '@/lib/prisma'
|
|
20
|
+
|
|
21
|
+
export const authConfig = {
|
|
22
|
+
providers: [
|
|
23
|
+
// Credentials provider (username/password)
|
|
24
|
+
Credentials({
|
|
25
|
+
async authorize(credentials) {
|
|
26
|
+
if (!credentials?.email || !credentials?.password) {
|
|
27
|
+
return null
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// TODO: Implement your credential validation logic
|
|
31
|
+
// Example: find user, verify password hash, return user
|
|
32
|
+
const user = await prisma.user.findUnique({
|
|
33
|
+
where: { email: credentials.email as string },
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
if (!user) return null
|
|
37
|
+
|
|
38
|
+
// Verify password (use bcrypt or similar)
|
|
39
|
+
// const isValid = await bcrypt.compare(credentials.password, user.password)
|
|
40
|
+
// if (!isValid) return null
|
|
41
|
+
|
|
42
|
+
return {
|
|
43
|
+
id: user.id,
|
|
44
|
+
email: user.email,
|
|
45
|
+
name: user.name,
|
|
46
|
+
image: user.image,
|
|
47
|
+
}
|
|
48
|
+
},
|
|
49
|
+
}),
|
|
50
|
+
|
|
51
|
+
// GitHub OAuth (register app: https://github.com/settings/apps)
|
|
52
|
+
GitHub({
|
|
53
|
+
clientId: process.env.GITHUB_ID,
|
|
54
|
+
clientSecret: process.env.GITHUB_SECRET,
|
|
55
|
+
}),
|
|
56
|
+
|
|
57
|
+
// Google OAuth (create: https://console.cloud.google.com/)
|
|
58
|
+
Google({
|
|
59
|
+
clientId: process.env.GOOGLE_ID,
|
|
60
|
+
clientSecret: process.env.GOOGLE_SECRET,
|
|
61
|
+
}),
|
|
62
|
+
],
|
|
63
|
+
|
|
64
|
+
adapter: PrismaAdapter(prisma),
|
|
65
|
+
|
|
66
|
+
pages: {
|
|
67
|
+
signIn: '/login',
|
|
68
|
+
error: '/login',
|
|
69
|
+
},
|
|
70
|
+
|
|
71
|
+
callbacks: {
|
|
72
|
+
// Add custom logic when user signs in
|
|
73
|
+
async signIn({ user, account, profile, email, credentials }) {
|
|
74
|
+
// Check if user is allowed, verify email domain, etc.
|
|
75
|
+
return true
|
|
76
|
+
},
|
|
77
|
+
|
|
78
|
+
// Add user role/permissions to session
|
|
79
|
+
async session({ session, user }) {
|
|
80
|
+
return {
|
|
81
|
+
...session,
|
|
82
|
+
user: {
|
|
83
|
+
...session.user,
|
|
84
|
+
id: user.id,
|
|
85
|
+
role: (user as any).role || 'user', // Adjust based on your schema
|
|
86
|
+
},
|
|
87
|
+
}
|
|
88
|
+
},
|
|
89
|
+
|
|
90
|
+
// Add user info to JWT token
|
|
91
|
+
async jwt({ token, user, account }) {
|
|
92
|
+
if (user) {
|
|
93
|
+
token.id = user.id
|
|
94
|
+
token.role = (user as any).role || 'user'
|
|
95
|
+
}
|
|
96
|
+
return token
|
|
97
|
+
},
|
|
98
|
+
},
|
|
99
|
+
|
|
100
|
+
events: {
|
|
101
|
+
// Log important events
|
|
102
|
+
async signIn({ user, account, profile, isNewUser }) {
|
|
103
|
+
console.log(`User signed in: ${user.email}`)
|
|
104
|
+
},
|
|
105
|
+
|
|
106
|
+
async signOut({ token }) {
|
|
107
|
+
console.log(`User signed out`)
|
|
108
|
+
},
|
|
109
|
+
},
|
|
110
|
+
} satisfies NextAuthConfig
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* NextAuth v6 Instance
|
|
3
|
+
*
|
|
4
|
+
* Copy to: ./auth.ts
|
|
5
|
+
* Wraps auth.config.ts with handlers for API routes, middleware, client hooks
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import NextAuth from 'next-auth'
|
|
9
|
+
import { authConfig } from './auth.config'
|
|
10
|
+
|
|
11
|
+
export const { handlers, auth, signIn, signOut } = NextAuth(authConfig)
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
// This is your Prisma schema file.
|
|
2
|
+
// Learn more about it in the docs: https://pris.ly/d/prisma-schema
|
|
3
|
+
|
|
4
|
+
datasource db {
|
|
5
|
+
provider = "postgresql"
|
|
6
|
+
url = env("DATABASE_URL")
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
generator client {
|
|
10
|
+
provider = "prisma-client-js"
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
// ─── NextAuth Models ────────────────────────────────────────────────────────
|
|
14
|
+
// Auto-generated by NextAuth + Prisma Adapter
|
|
15
|
+
|
|
16
|
+
model Account {
|
|
17
|
+
id String @id @default(cuid())
|
|
18
|
+
userId String
|
|
19
|
+
type String
|
|
20
|
+
provider String
|
|
21
|
+
providerAccountId String
|
|
22
|
+
refresh_token String? @db.Text
|
|
23
|
+
access_token String? @db.Text
|
|
24
|
+
expires_at Int?
|
|
25
|
+
token_type String?
|
|
26
|
+
scope String?
|
|
27
|
+
id_token String? @db.Text
|
|
28
|
+
session_state String?
|
|
29
|
+
|
|
30
|
+
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
|
31
|
+
|
|
32
|
+
@@unique([provider, providerAccountId])
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
model Session {
|
|
36
|
+
id String @id @default(cuid())
|
|
37
|
+
sessionToken String @unique
|
|
38
|
+
userId String
|
|
39
|
+
expires DateTime
|
|
40
|
+
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
model VerificationToken {
|
|
44
|
+
identifier String
|
|
45
|
+
token String @unique
|
|
46
|
+
expires DateTime
|
|
47
|
+
|
|
48
|
+
@@unique([identifier, token])
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// ─── User & Auth ────────────────────────────────────────────────────────────
|
|
52
|
+
|
|
53
|
+
model User {
|
|
54
|
+
id String @id @default(cuid())
|
|
55
|
+
name String?
|
|
56
|
+
email String @unique
|
|
57
|
+
emailVerified DateTime?
|
|
58
|
+
password String? // Only for credentials provider
|
|
59
|
+
image String?
|
|
60
|
+
role String @default("user") // "admin", "user", "moderator"
|
|
61
|
+
createdAt DateTime @default(now())
|
|
62
|
+
updatedAt DateTime @updatedAt
|
|
63
|
+
|
|
64
|
+
// Relations
|
|
65
|
+
accounts Account[]
|
|
66
|
+
sessions Session[]
|
|
67
|
+
stores Store[] @relation("storeOwner")
|
|
68
|
+
members StoreMember[]
|
|
69
|
+
posts Post[]
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// ─── Multi-Tenant: Store/Organization ──────────────────────────────────────
|
|
73
|
+
|
|
74
|
+
model Store {
|
|
75
|
+
id String @id @default(cuid())
|
|
76
|
+
name String
|
|
77
|
+
slug String @unique
|
|
78
|
+
ownerId String
|
|
79
|
+
owner User @relation("storeOwner", fields: [ownerId], references: [id], onDelete: Cascade)
|
|
80
|
+
description String?
|
|
81
|
+
logo String?
|
|
82
|
+
settings Json? // Store-specific settings
|
|
83
|
+
createdAt DateTime @default(now())
|
|
84
|
+
updatedAt DateTime @updatedAt
|
|
85
|
+
|
|
86
|
+
// Relations
|
|
87
|
+
members StoreMember[]
|
|
88
|
+
posts Post[]
|
|
89
|
+
products Product[]
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
model StoreMember {
|
|
93
|
+
id String @id @default(cuid())
|
|
94
|
+
storeId String
|
|
95
|
+
userId String
|
|
96
|
+
role String @default("member") // "admin", "editor", "member", "viewer"
|
|
97
|
+
|
|
98
|
+
store Store @relation(fields: [storeId], references: [id], onDelete: Cascade)
|
|
99
|
+
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
|
100
|
+
|
|
101
|
+
@@unique([storeId, userId])
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// ─── Content: Posts/Pages ──────────────────────────────────────────────────
|
|
105
|
+
|
|
106
|
+
model Post {
|
|
107
|
+
id String @id @default(cuid())
|
|
108
|
+
storeId String
|
|
109
|
+
title String
|
|
110
|
+
slug String
|
|
111
|
+
content String @db.Text
|
|
112
|
+
status String @default("draft") // "draft", "published", "scheduled", "archived"
|
|
113
|
+
type String @default("post") // "post", "page", "custom"
|
|
114
|
+
excerpt String?
|
|
115
|
+
image String?
|
|
116
|
+
authorId String
|
|
117
|
+
createdAt DateTime @default(now())
|
|
118
|
+
updatedAt DateTime @updatedAt
|
|
119
|
+
|
|
120
|
+
store Store @relation(fields: [storeId], references: [id], onDelete: Cascade)
|
|
121
|
+
author User @relation(fields: [authorId], references: [id], onDelete: Cascade)
|
|
122
|
+
|
|
123
|
+
@@unique([storeId, slug])
|
|
124
|
+
@@index([storeId])
|
|
125
|
+
@@index([authorId])
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// ─── E-Commerce: Products ────────────────────────────────────────────────
|
|
129
|
+
|
|
130
|
+
model Product {
|
|
131
|
+
id String @id @default(cuid())
|
|
132
|
+
storeId String
|
|
133
|
+
name String
|
|
134
|
+
description String? @db.Text
|
|
135
|
+
price Decimal @db.Decimal(10, 2)
|
|
136
|
+
image String?
|
|
137
|
+
published Boolean @default(false)
|
|
138
|
+
createdAt DateTime @default(now())
|
|
139
|
+
updatedAt DateTime @updatedAt
|
|
140
|
+
|
|
141
|
+
store Store @relation(fields: [storeId], references: [id], onDelete: Cascade)
|
|
142
|
+
|
|
143
|
+
@@index([storeId])
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// ─── Extend with your own models ────────────────────────────────────────────
|
|
147
|
+
|
|
148
|
+
// Example: E-commerce orders
|
|
149
|
+
// model Order {
|
|
150
|
+
// id String @id @default(cuid())
|
|
151
|
+
// storeId String
|
|
152
|
+
// userId String
|
|
153
|
+
// total Decimal
|
|
154
|
+
// status String @default("pending")
|
|
155
|
+
// createdAt DateTime @default(now())
|
|
156
|
+
// }
|
|
157
|
+
|
|
158
|
+
// Example: Blog comments
|
|
159
|
+
// model Comment {
|
|
160
|
+
// id String @id @default(cuid())
|
|
161
|
+
// postId String
|
|
162
|
+
// userId String
|
|
163
|
+
// content String @db.Text
|
|
164
|
+
// createdAt DateTime @default(now())
|
|
165
|
+
// }
|