@devmunna/agent-skillkit 0.1.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/LICENSE +21 -0
- package/README.md +147 -0
- package/bin/ai-skills.js +5 -0
- package/dist/cli/commands/add.d.ts +2 -0
- package/dist/cli/commands/add.d.ts.map +1 -0
- package/dist/cli/commands/add.js +66 -0
- package/dist/cli/commands/add.js.map +1 -0
- package/dist/cli/commands/doctor.d.ts +2 -0
- package/dist/cli/commands/doctor.d.ts.map +1 -0
- package/dist/cli/commands/doctor.js +33 -0
- package/dist/cli/commands/doctor.js.map +1 -0
- package/dist/cli/commands/init.d.ts +10 -0
- package/dist/cli/commands/init.d.ts.map +1 -0
- package/dist/cli/commands/init.js +145 -0
- package/dist/cli/commands/init.js.map +1 -0
- package/dist/cli/commands/list.d.ts +5 -0
- package/dist/cli/commands/list.d.ts.map +1 -0
- package/dist/cli/commands/list.js +55 -0
- package/dist/cli/commands/list.js.map +1 -0
- package/dist/cli/commands/update.d.ts +2 -0
- package/dist/cli/commands/update.d.ts.map +1 -0
- package/dist/cli/commands/update.js +49 -0
- package/dist/cli/commands/update.js.map +1 -0
- package/dist/cli/commands/validate.d.ts +2 -0
- package/dist/cli/commands/validate.d.ts.map +1 -0
- package/dist/cli/commands/validate.js +22 -0
- package/dist/cli/commands/validate.js.map +1 -0
- package/dist/cli/index.d.ts +2 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/cli/index.js +49 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/cli/prompts/agent-selector.d.ts +3 -0
- package/dist/cli/prompts/agent-selector.d.ts.map +1 -0
- package/dist/cli/prompts/agent-selector.js +23 -0
- package/dist/cli/prompts/agent-selector.js.map +1 -0
- package/dist/cli/prompts/stack-selector.d.ts +3 -0
- package/dist/cli/prompts/stack-selector.d.ts.map +1 -0
- package/dist/cli/prompts/stack-selector.js +60 -0
- package/dist/cli/prompts/stack-selector.js.map +1 -0
- package/dist/core/config-manager.d.ts +20 -0
- package/dist/core/config-manager.d.ts.map +1 -0
- package/dist/core/config-manager.js +107 -0
- package/dist/core/config-manager.js.map +1 -0
- package/dist/core/detector.d.ts +3 -0
- package/dist/core/detector.d.ts.map +1 -0
- package/dist/core/detector.js +50 -0
- package/dist/core/detector.js.map +1 -0
- package/dist/core/doctor.d.ts +12 -0
- package/dist/core/doctor.d.ts.map +1 -0
- package/dist/core/doctor.js +102 -0
- package/dist/core/doctor.js.map +1 -0
- package/dist/core/skill-registry.d.ts +11 -0
- package/dist/core/skill-registry.d.ts.map +1 -0
- package/dist/core/skill-registry.js +174 -0
- package/dist/core/skill-registry.js.map +1 -0
- package/dist/core/skill-resolver.d.ts +3 -0
- package/dist/core/skill-resolver.d.ts.map +1 -0
- package/dist/core/skill-resolver.js +36 -0
- package/dist/core/skill-resolver.js.map +1 -0
- package/dist/core/validator.d.ts +13 -0
- package/dist/core/validator.d.ts.map +1 -0
- package/dist/core/validator.js +99 -0
- package/dist/core/validator.js.map +1 -0
- package/dist/generators/agent-installer.d.ts +5 -0
- package/dist/generators/agent-installer.d.ts.map +1 -0
- package/dist/generators/agent-installer.js +20 -0
- package/dist/generators/agent-installer.js.map +1 -0
- package/dist/generators/agents-md.d.ts +3 -0
- package/dist/generators/agents-md.d.ts.map +1 -0
- package/dist/generators/agents-md.js +70 -0
- package/dist/generators/agents-md.js.map +1 -0
- package/dist/generators/claude-md.d.ts +3 -0
- package/dist/generators/claude-md.d.ts.map +1 -0
- package/dist/generators/claude-md.js +47 -0
- package/dist/generators/claude-md.js.map +1 -0
- package/dist/generators/skill-generator.d.ts +5 -0
- package/dist/generators/skill-generator.d.ts.map +1 -0
- package/dist/generators/skill-generator.js +34 -0
- package/dist/generators/skill-generator.js.map +1 -0
- package/dist/generators/workflows.d.ts +3 -0
- package/dist/generators/workflows.d.ts.map +1 -0
- package/dist/generators/workflows.js +57 -0
- package/dist/generators/workflows.js.map +1 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +13 -0
- package/dist/index.js.map +1 -0
- package/dist/types/index.d.ts +55 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +2 -0
- package/dist/types/index.js.map +1 -0
- package/dist/utils/file-utils.d.ts +12 -0
- package/dist/utils/file-utils.d.ts.map +1 -0
- package/dist/utils/file-utils.js +39 -0
- package/dist/utils/file-utils.js.map +1 -0
- package/dist/utils/logger.d.ts +10 -0
- package/dist/utils/logger.d.ts.map +1 -0
- package/dist/utils/logger.js +11 -0
- package/dist/utils/logger.js.map +1 -0
- package/package.json +73 -0
- package/skills/clean-architecture/SKILL.md +324 -0
- package/skills/express-mvc-prisma/SKILL.md +168 -0
- package/skills/express-mvc-prisma/references/auth.md +190 -0
- package/skills/express-mvc-prisma/references/boilerplate.md +196 -0
- package/skills/express-mvc-prisma/references/error-handling.md +121 -0
- package/skills/express-mvc-prisma/references/module-scaffold.md +253 -0
- package/skills/express-mvc-prisma/references/prisma-setup.md +97 -0
- package/skills/express-mvc-prisma/references/response-helpers.md +157 -0
- package/skills/express-mvc-prisma/references/zod-validation.md +157 -0
- package/skills/fastify-rest/SKILL.md +287 -0
- package/skills/mongoose-odm/SKILL.md +281 -0
- package/skills/nextjs-fullstack/SKILL.md +328 -0
- package/skills/nextjs-fullstack/references/auth.md +270 -0
- package/skills/nextjs-fullstack/references/caching.md +157 -0
- package/skills/nextjs-fullstack/references/route-handlers.md +194 -0
- package/skills/nextjs-fullstack/references/server-actions.md +214 -0
- package/skills/nextjs-fullstack/references/server-components.md +190 -0
- package/skills/node-base/SKILL.md +139 -0
- package/skills/prisma-orm/SKILL.md +334 -0
- package/skills/react-feature-arch/SKILL.md +208 -0
- package/skills/react-feature-arch/references/api-layer.md +110 -0
- package/skills/react-feature-arch/references/components.md +192 -0
- package/skills/react-feature-arch/references/data-fetching.md +198 -0
- package/skills/react-feature-arch/references/forms.md +194 -0
- package/skills/react-feature-arch/references/routing.md +148 -0
- package/skills/react-feature-arch/references/state-management.md +107 -0
- package/skills/tailwind-css/SKILL.md +236 -0
- package/skills/tailwind-css/references/components.md +340 -0
- package/skills/tailwind-css/references/design-tokens.md +230 -0
- package/skills/tailwind-css/references/patterns.md +375 -0
- package/skills/tailwind-css/references/setup.md +165 -0
- package/skills/zod-validation/SKILL.md +267 -0
|
@@ -0,0 +1,328 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: nextjs-fullstack
|
|
3
|
+
version: 2.0.0
|
|
4
|
+
description: >
|
|
5
|
+
Use this skill for any Next.js project task: App Router routing, Server Components, Client Components, Server Actions, Route Handlers, caching, auth, metadata, or deployment. Triggers: "Next.js App Router", "Server Component", "Server Action", "next/navigation", "layout.tsx", "page.tsx", "Route Handler", "next-auth", "Auth.js", "use cache", "after()", "PPR", "params Promise".
|
|
6
|
+
stack: [nextjs, react, typescript, prisma]
|
|
7
|
+
depends: []
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
# Next.js 15 — Full-Stack Production Skill
|
|
11
|
+
|
|
12
|
+
**Version target:** Next.js 15 · React 19 · TypeScript 5 · Prisma v5 · Auth.js v5 (next-auth)
|
|
13
|
+
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
## Tech Stack
|
|
17
|
+
|
|
18
|
+
| Layer | Package |
|
|
19
|
+
|---|---|
|
|
20
|
+
| Framework | next v15 |
|
|
21
|
+
| UI | react v19 + react-dom v19 |
|
|
22
|
+
| ORM | @prisma/client v5 |
|
|
23
|
+
| Auth | next-auth v5 (Auth.js) |
|
|
24
|
+
| Validation | zod v4 |
|
|
25
|
+
| Styling | tailwindcss v4 |
|
|
26
|
+
| Language | TypeScript 5 |
|
|
27
|
+
|
|
28
|
+
---
|
|
29
|
+
|
|
30
|
+
## App Router Folder Structure
|
|
31
|
+
|
|
32
|
+
```
|
|
33
|
+
src/
|
|
34
|
+
├── app/
|
|
35
|
+
│ ├── layout.tsx # Root HTML shell (Server Component)
|
|
36
|
+
│ ├── page.tsx # Home page
|
|
37
|
+
│ ├── loading.tsx # Suspense fallback (auto-wrapped)
|
|
38
|
+
│ ├── error.tsx # Error boundary (must be 'use client')
|
|
39
|
+
│ ├── not-found.tsx # 404 page
|
|
40
|
+
│ ├── globals.css
|
|
41
|
+
│ ├── (auth)/ # Route group — no URL segment
|
|
42
|
+
│ │ ├── login/page.tsx
|
|
43
|
+
│ │ └── register/page.tsx
|
|
44
|
+
│ ├── (dashboard)/
|
|
45
|
+
│ │ ├── layout.tsx # Shared layout with sidebar
|
|
46
|
+
│ │ ├── dashboard/page.tsx
|
|
47
|
+
│ │ └── users/
|
|
48
|
+
│ │ ├── page.tsx # /users list
|
|
49
|
+
│ │ ├── [id]/
|
|
50
|
+
│ │ │ └── page.tsx # /users/:id detail
|
|
51
|
+
│ │ └── new/page.tsx # /users/new create form
|
|
52
|
+
│ └── api/
|
|
53
|
+
│ └── {resource}/
|
|
54
|
+
│ ├── route.ts # GET /api/resource, POST /api/resource
|
|
55
|
+
│ └── [id]/
|
|
56
|
+
│ └── route.ts # GET, PATCH, DELETE /api/resource/:id
|
|
57
|
+
├── features/ # Domain modules
|
|
58
|
+
│ └── {feature}/
|
|
59
|
+
│ ├── {feature}.actions.ts # Server Actions ('use server')
|
|
60
|
+
│ ├── {feature}.queries.ts # DB query functions (Prisma calls)
|
|
61
|
+
│ ├── {feature}.schema.ts # Zod schemas + TypeScript types
|
|
62
|
+
│ └── components/
|
|
63
|
+
│ └── {Feature}Form.tsx # Client form components
|
|
64
|
+
├── components/
|
|
65
|
+
│ ├── ui/ # Shared primitive Client components
|
|
66
|
+
│ └── server/ # Server component compositions
|
|
67
|
+
├── lib/
|
|
68
|
+
│ ├── db.ts # Prisma singleton
|
|
69
|
+
│ ├── auth.ts # Auth.js config
|
|
70
|
+
│ └── session.ts # Session helpers
|
|
71
|
+
└── types/
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
---
|
|
75
|
+
|
|
76
|
+
## Server vs Client Components
|
|
77
|
+
|
|
78
|
+
**Default: Server Component.** Add `'use client'` ONLY when the component needs:
|
|
79
|
+
- `useState`, `useEffect`, `useRef`, `useContext`
|
|
80
|
+
- Browser APIs (`window`, `localStorage`, `document`)
|
|
81
|
+
- Event handlers (`onClick`, `onChange`, `onSubmit`)
|
|
82
|
+
- `useActionState`, `useOptimistic`, `useFormStatus` (React 19)
|
|
83
|
+
- Third-party client-only libraries
|
|
84
|
+
|
|
85
|
+
```tsx
|
|
86
|
+
// app/(dashboard)/users/page.tsx — Server Component (no directive)
|
|
87
|
+
import { db } from '@/lib/db';
|
|
88
|
+
import { UserList } from '@/features/users/components/UserList';
|
|
89
|
+
|
|
90
|
+
export default async function UsersPage() {
|
|
91
|
+
const users = await db.user.findMany({
|
|
92
|
+
select: { id: true, name: true, email: true, role: true },
|
|
93
|
+
orderBy: { createdAt: 'desc' },
|
|
94
|
+
});
|
|
95
|
+
return <UserList users={users} />;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// features/users/components/DeleteButton.tsx — must be Client Component
|
|
99
|
+
'use client';
|
|
100
|
+
|
|
101
|
+
import { useTransition } from 'react';
|
|
102
|
+
import { deleteUserAction } from '../users.actions';
|
|
103
|
+
|
|
104
|
+
export function DeleteButton({ userId }: { userId: string }) {
|
|
105
|
+
const [isPending, startTransition] = useTransition();
|
|
106
|
+
|
|
107
|
+
return (
|
|
108
|
+
<button
|
|
109
|
+
onClick={() => startTransition(() => deleteUserAction(userId))}
|
|
110
|
+
disabled={isPending}
|
|
111
|
+
>
|
|
112
|
+
{isPending ? 'Deleting...' : 'Delete'}
|
|
113
|
+
</button>
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
---
|
|
119
|
+
|
|
120
|
+
## Next.js 15 — Critical Changes
|
|
121
|
+
|
|
122
|
+
### params is now a Promise in dynamic routes
|
|
123
|
+
```tsx
|
|
124
|
+
// Next.js 15 — params AND searchParams are Promises, must be awaited
|
|
125
|
+
interface Props {
|
|
126
|
+
params: Promise<{ id: string }>;
|
|
127
|
+
searchParams: Promise<{ page?: string }>;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export default async function UserPage({ params, searchParams }: Props) {
|
|
131
|
+
const { id } = await params;
|
|
132
|
+
const { page } = await searchParams;
|
|
133
|
+
|
|
134
|
+
const user = await db.user.findUnique({ where: { id } });
|
|
135
|
+
if (!user) notFound();
|
|
136
|
+
|
|
137
|
+
return <UserDetail user={user} />;
|
|
138
|
+
}
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
### `use cache` directive — function-level caching (Next.js 15)
|
|
142
|
+
```ts
|
|
143
|
+
// features/users/users.queries.ts
|
|
144
|
+
'use cache'; // caches this module's exports
|
|
145
|
+
|
|
146
|
+
import { cacheTag, cacheLife } from 'next/cache';
|
|
147
|
+
import { db } from '@/lib/db';
|
|
148
|
+
|
|
149
|
+
export async function getUsers() {
|
|
150
|
+
cacheTag('users');
|
|
151
|
+
cacheLife('hours');
|
|
152
|
+
return db.user.findMany({ orderBy: { createdAt: 'desc' } });
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
export async function getUserById(id: string) {
|
|
156
|
+
cacheTag('users', `user-${id}`);
|
|
157
|
+
cacheLife('minutes');
|
|
158
|
+
return db.user.findUnique({ where: { id } });
|
|
159
|
+
}
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
On-demand invalidation (in Server Action or Route Handler):
|
|
163
|
+
```ts
|
|
164
|
+
import { revalidateTag } from 'next/cache';
|
|
165
|
+
revalidateTag('users'); // invalidates all 'users' tagged cache
|
|
166
|
+
revalidateTag(`user-${id}`); // invalidate specific user cache
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
### `after()` — post-response side effects
|
|
170
|
+
```ts
|
|
171
|
+
import { after } from 'next/server';
|
|
172
|
+
|
|
173
|
+
export async function createUserAction(formData: FormData) {
|
|
174
|
+
const user = await db.user.create({ data: /* ... */ });
|
|
175
|
+
|
|
176
|
+
// Runs AFTER response is sent — non-blocking, no delay to user
|
|
177
|
+
after(async () => {
|
|
178
|
+
await sendWelcomeEmail(user.email);
|
|
179
|
+
await createAuditLog('user.created', { userId: user.id });
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
revalidateTag('users');
|
|
183
|
+
}
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
---
|
|
187
|
+
|
|
188
|
+
## Server Actions
|
|
189
|
+
|
|
190
|
+
Handle all mutations in Server Actions. Call from Client Components or HTML forms.
|
|
191
|
+
|
|
192
|
+
```ts
|
|
193
|
+
// features/users/users.actions.ts
|
|
194
|
+
'use server';
|
|
195
|
+
|
|
196
|
+
import { db } from '@/lib/db';
|
|
197
|
+
import { auth } from '@/lib/auth';
|
|
198
|
+
import { revalidateTag } from 'next/cache';
|
|
199
|
+
import { redirect } from 'next/navigation';
|
|
200
|
+
import { createUserSchema } from './users.schema';
|
|
201
|
+
|
|
202
|
+
export async function createUserAction(_prev: unknown, formData: FormData) {
|
|
203
|
+
const session = await auth();
|
|
204
|
+
if (!session) return { error: 'Unauthorized' };
|
|
205
|
+
|
|
206
|
+
const result = createUserSchema.safeParse({
|
|
207
|
+
name: formData.get('name'),
|
|
208
|
+
email: formData.get('email'),
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
if (!result.success) {
|
|
212
|
+
return { error: result.error.issues[0].message };
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
try {
|
|
216
|
+
await db.user.create({ data: result.data });
|
|
217
|
+
} catch {
|
|
218
|
+
return { error: 'Failed to create user' };
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
revalidateTag('users');
|
|
222
|
+
redirect('/users');
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
export async function deleteUserAction(id: string) {
|
|
226
|
+
const session = await auth();
|
|
227
|
+
if (!session?.user || session.user.role !== 'ADMIN') {
|
|
228
|
+
throw new Error('Forbidden');
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
await db.user.delete({ where: { id } });
|
|
232
|
+
revalidateTag('users');
|
|
233
|
+
revalidateTag(`user-${id}`);
|
|
234
|
+
}
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
Client form using `useActionState` (React 19):
|
|
238
|
+
```tsx
|
|
239
|
+
'use client';
|
|
240
|
+
|
|
241
|
+
import { useActionState } from 'react';
|
|
242
|
+
import { createUserAction } from '../users.actions';
|
|
243
|
+
|
|
244
|
+
export function CreateUserForm() {
|
|
245
|
+
const [state, formAction, isPending] = useActionState(createUserAction, null);
|
|
246
|
+
|
|
247
|
+
return (
|
|
248
|
+
<form action={formAction}>
|
|
249
|
+
<input name="name" required />
|
|
250
|
+
<input name="email" type="email" required />
|
|
251
|
+
{state?.error && <p className="error">{state.error}</p>}
|
|
252
|
+
<button type="submit" disabled={isPending}>
|
|
253
|
+
{isPending ? 'Creating...' : 'Create User'}
|
|
254
|
+
</button>
|
|
255
|
+
</form>
|
|
256
|
+
);
|
|
257
|
+
}
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
---
|
|
261
|
+
|
|
262
|
+
## Error Handling
|
|
263
|
+
|
|
264
|
+
```tsx
|
|
265
|
+
// app/error.tsx — must be 'use client'
|
|
266
|
+
'use client';
|
|
267
|
+
|
|
268
|
+
export default function Error({
|
|
269
|
+
error,
|
|
270
|
+
reset,
|
|
271
|
+
}: {
|
|
272
|
+
error: Error & { digest?: string };
|
|
273
|
+
reset: () => void;
|
|
274
|
+
}) {
|
|
275
|
+
return (
|
|
276
|
+
<div>
|
|
277
|
+
<h2>Something went wrong</h2>
|
|
278
|
+
<button onClick={reset}>Try again</button>
|
|
279
|
+
</div>
|
|
280
|
+
);
|
|
281
|
+
}
|
|
282
|
+
```
|
|
283
|
+
|
|
284
|
+
In Server Components: throw for unexpected errors, `notFound()` for 404s:
|
|
285
|
+
```tsx
|
|
286
|
+
import { notFound } from 'next/navigation';
|
|
287
|
+
|
|
288
|
+
if (!user) notFound(); // renders not-found.tsx
|
|
289
|
+
if (!user) throw new Error('x'); // renders error.tsx
|
|
290
|
+
```
|
|
291
|
+
|
|
292
|
+
---
|
|
293
|
+
|
|
294
|
+
## Metadata
|
|
295
|
+
|
|
296
|
+
```tsx
|
|
297
|
+
import type { Metadata } from 'next';
|
|
298
|
+
|
|
299
|
+
// Static metadata
|
|
300
|
+
export const metadata: Metadata = {
|
|
301
|
+
title: { template: '%s | My App', default: 'My App' },
|
|
302
|
+
description: 'App description',
|
|
303
|
+
openGraph: { type: 'website' },
|
|
304
|
+
};
|
|
305
|
+
|
|
306
|
+
// Dynamic metadata (Next.js 15 — params is a Promise)
|
|
307
|
+
export async function generateMetadata({
|
|
308
|
+
params,
|
|
309
|
+
}: {
|
|
310
|
+
params: Promise<{ id: string }>;
|
|
311
|
+
}): Promise<Metadata> {
|
|
312
|
+
const { id } = await params;
|
|
313
|
+
const user = await db.user.findUnique({ where: { id } });
|
|
314
|
+
return { title: user?.name ?? 'User not found' };
|
|
315
|
+
}
|
|
316
|
+
```
|
|
317
|
+
|
|
318
|
+
---
|
|
319
|
+
|
|
320
|
+
## Core References
|
|
321
|
+
|
|
322
|
+
| Topic | File |
|
|
323
|
+
|---|---|
|
|
324
|
+
| Server Components + data fetching patterns | `references/server-components.md` |
|
|
325
|
+
| Server Actions + useActionState | `references/server-actions.md` |
|
|
326
|
+
| Route Handlers (REST API) | `references/route-handlers.md` |
|
|
327
|
+
| Caching — use cache, revalidate, tags | `references/caching.md` |
|
|
328
|
+
| Auth.js v5 (next-auth) setup | `references/auth.md` |
|
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
# Auth Reference — Auth.js v5 (next-auth)
|
|
2
|
+
|
|
3
|
+
JWT-based session auth. `auth()` reads the session in Server Components, Server Actions, and Route Handlers.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Installation
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm install next-auth@beta
|
|
11
|
+
npx auth secret # generates AUTH_SECRET in .env
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
## lib/auth.ts — Configuration
|
|
17
|
+
|
|
18
|
+
```ts
|
|
19
|
+
// src/lib/auth.ts
|
|
20
|
+
import NextAuth from 'next-auth';
|
|
21
|
+
import CredentialsProvider from 'next-auth/providers/credentials';
|
|
22
|
+
import { PrismaAdapter } from '@auth/prisma-adapter';
|
|
23
|
+
import { db } from './db';
|
|
24
|
+
import bcrypt from 'bcrypt';
|
|
25
|
+
import { z } from 'zod';
|
|
26
|
+
|
|
27
|
+
const loginSchema = z.object({
|
|
28
|
+
email: z.string().email(),
|
|
29
|
+
password: z.string().min(1),
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
export const { handlers, signIn, signOut, auth } = NextAuth({
|
|
33
|
+
adapter: PrismaAdapter(db),
|
|
34
|
+
|
|
35
|
+
session: { strategy: 'jwt' },
|
|
36
|
+
|
|
37
|
+
pages: {
|
|
38
|
+
signIn: '/auth/login',
|
|
39
|
+
},
|
|
40
|
+
|
|
41
|
+
providers: [
|
|
42
|
+
CredentialsProvider({
|
|
43
|
+
async authorize(credentials) {
|
|
44
|
+
const parsed = loginSchema.safeParse(credentials);
|
|
45
|
+
if (!parsed.success) return null;
|
|
46
|
+
|
|
47
|
+
const user = await db.user.findUnique({
|
|
48
|
+
where: { email: parsed.data.email },
|
|
49
|
+
});
|
|
50
|
+
if (!user || !user.password) return null;
|
|
51
|
+
|
|
52
|
+
const valid = await bcrypt.compare(parsed.data.password, user.password);
|
|
53
|
+
if (!valid) return null;
|
|
54
|
+
|
|
55
|
+
return { id: user.id, name: user.name, email: user.email, role: user.role };
|
|
56
|
+
},
|
|
57
|
+
}),
|
|
58
|
+
],
|
|
59
|
+
|
|
60
|
+
callbacks: {
|
|
61
|
+
// Add role to JWT
|
|
62
|
+
jwt({ token, user }) {
|
|
63
|
+
if (user) token.role = (user as any).role;
|
|
64
|
+
return token;
|
|
65
|
+
},
|
|
66
|
+
// Expose role in session
|
|
67
|
+
session({ session, token }) {
|
|
68
|
+
session.user.role = token.role as string;
|
|
69
|
+
return session;
|
|
70
|
+
},
|
|
71
|
+
},
|
|
72
|
+
});
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
---
|
|
76
|
+
|
|
77
|
+
## app/api/auth/[...nextauth]/route.ts
|
|
78
|
+
|
|
79
|
+
```ts
|
|
80
|
+
import { handlers } from '@/lib/auth';
|
|
81
|
+
export const { GET, POST } = handlers;
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
---
|
|
85
|
+
|
|
86
|
+
## .env
|
|
87
|
+
|
|
88
|
+
```
|
|
89
|
+
AUTH_SECRET=your-generated-secret-at-least-32-chars
|
|
90
|
+
AUTH_URL=http://localhost:3000
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
---
|
|
94
|
+
|
|
95
|
+
## TypeScript — Extend Session Types
|
|
96
|
+
|
|
97
|
+
```ts
|
|
98
|
+
// types/next-auth.d.ts
|
|
99
|
+
import 'next-auth';
|
|
100
|
+
import 'next-auth/jwt';
|
|
101
|
+
|
|
102
|
+
declare module 'next-auth' {
|
|
103
|
+
interface Session {
|
|
104
|
+
user: { id: string; name: string; email: string; role: string };
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
declare module 'next-auth/jwt' {
|
|
109
|
+
interface JWT {
|
|
110
|
+
role: string;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
---
|
|
116
|
+
|
|
117
|
+
## Usage — Server Components & Actions
|
|
118
|
+
|
|
119
|
+
```ts
|
|
120
|
+
// Reading session in Server Component / Action / Route Handler
|
|
121
|
+
import { auth } from '@/lib/auth';
|
|
122
|
+
import { redirect } from 'next/navigation';
|
|
123
|
+
|
|
124
|
+
// Server Component
|
|
125
|
+
export default async function ProtectedPage() {
|
|
126
|
+
const session = await auth();
|
|
127
|
+
if (!session) redirect('/auth/login');
|
|
128
|
+
|
|
129
|
+
return <div>Hello {session.user.name}</div>;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Server Action
|
|
133
|
+
export async function sensitiveAction() {
|
|
134
|
+
const session = await auth();
|
|
135
|
+
if (!session) throw new Error('Unauthorized');
|
|
136
|
+
if (session.user.role !== 'ADMIN') throw new Error('Forbidden');
|
|
137
|
+
// ... proceed
|
|
138
|
+
}
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
---
|
|
142
|
+
|
|
143
|
+
## Middleware — Route Protection
|
|
144
|
+
|
|
145
|
+
```ts
|
|
146
|
+
// middleware.ts (root of project, not inside src/)
|
|
147
|
+
import { auth } from '@/lib/auth';
|
|
148
|
+
import { NextResponse } from 'next/server';
|
|
149
|
+
|
|
150
|
+
export default auth((req) => {
|
|
151
|
+
const isLoggedIn = !!req.auth;
|
|
152
|
+
const isAuthRoute = req.nextUrl.pathname.startsWith('/auth');
|
|
153
|
+
const isApiRoute = req.nextUrl.pathname.startsWith('/api');
|
|
154
|
+
|
|
155
|
+
if (isAuthRoute) {
|
|
156
|
+
if (isLoggedIn) return NextResponse.redirect(new URL('/dashboard', req.url));
|
|
157
|
+
return NextResponse.next();
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
if (!isLoggedIn && !isApiRoute) {
|
|
161
|
+
return NextResponse.redirect(new URL('/auth/login', req.url));
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
return NextResponse.next();
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
export const config = {
|
|
168
|
+
matcher: ['/((?!_next/static|_next/image|favicon.ico).*)'],
|
|
169
|
+
};
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
---
|
|
173
|
+
|
|
174
|
+
## Login Form (Client Component)
|
|
175
|
+
|
|
176
|
+
```tsx
|
|
177
|
+
// features/auth/components/LoginForm.tsx
|
|
178
|
+
'use client';
|
|
179
|
+
|
|
180
|
+
import { useActionState } from 'react';
|
|
181
|
+
import { signIn } from 'next-auth/react';
|
|
182
|
+
import { useRouter } from 'next/navigation';
|
|
183
|
+
|
|
184
|
+
async function loginAction(_prev: any, formData: FormData) {
|
|
185
|
+
const result = await signIn('credentials', {
|
|
186
|
+
email: formData.get('email'),
|
|
187
|
+
password: formData.get('password'),
|
|
188
|
+
redirect: false,
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
if (result?.error) return { error: 'Invalid email or password' };
|
|
192
|
+
return { success: true };
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
export function LoginForm() {
|
|
196
|
+
const router = useRouter();
|
|
197
|
+
const [state, formAction, isPending] = useActionState(loginAction, null);
|
|
198
|
+
|
|
199
|
+
if (state?.success) {
|
|
200
|
+
router.push('/dashboard');
|
|
201
|
+
return null;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
return (
|
|
205
|
+
<form action={formAction}>
|
|
206
|
+
<input name="email" type="email" required placeholder="Email" />
|
|
207
|
+
<input name="password" type="password" required placeholder="Password" />
|
|
208
|
+
{state?.error && <p className="error">{state.error}</p>}
|
|
209
|
+
<button type="submit" disabled={isPending}>
|
|
210
|
+
{isPending ? 'Signing in...' : 'Sign in'}
|
|
211
|
+
</button>
|
|
212
|
+
</form>
|
|
213
|
+
);
|
|
214
|
+
}
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
---
|
|
218
|
+
|
|
219
|
+
## Prisma Schema for Auth.js
|
|
220
|
+
|
|
221
|
+
```prisma
|
|
222
|
+
model User {
|
|
223
|
+
id String @id @default(uuid())
|
|
224
|
+
name String?
|
|
225
|
+
email String @unique
|
|
226
|
+
emailVerified DateTime?
|
|
227
|
+
image String?
|
|
228
|
+
password String? // null for OAuth users
|
|
229
|
+
role Role @default(USER)
|
|
230
|
+
accounts Account[]
|
|
231
|
+
sessions Session[]
|
|
232
|
+
createdAt DateTime @default(now())
|
|
233
|
+
updatedAt DateTime @updatedAt
|
|
234
|
+
|
|
235
|
+
@@map("users")
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
model Account {
|
|
239
|
+
id String @id @default(uuid())
|
|
240
|
+
userId String
|
|
241
|
+
type String
|
|
242
|
+
provider String
|
|
243
|
+
providerAccountId String
|
|
244
|
+
refresh_token String?
|
|
245
|
+
access_token String?
|
|
246
|
+
expires_at Int?
|
|
247
|
+
token_type String?
|
|
248
|
+
scope String?
|
|
249
|
+
id_token String?
|
|
250
|
+
session_state String?
|
|
251
|
+
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
|
252
|
+
|
|
253
|
+
@@unique([provider, providerAccountId])
|
|
254
|
+
@@index([userId])
|
|
255
|
+
@@map("accounts")
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
model Session {
|
|
259
|
+
id String @id @default(uuid())
|
|
260
|
+
sessionToken String @unique
|
|
261
|
+
userId String
|
|
262
|
+
expires DateTime
|
|
263
|
+
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
|
264
|
+
|
|
265
|
+
@@index([userId])
|
|
266
|
+
@@map("sessions")
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
enum Role { USER ADMIN MANAGER }
|
|
270
|
+
```
|