@malamute/ai-rules 1.0.0 → 1.3.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/README.md +272 -121
- package/bin/cli.js +5 -2
- package/configs/_shared/CLAUDE.md +52 -149
- package/configs/_shared/rules/conventions/documentation.md +324 -0
- package/configs/_shared/rules/conventions/git.md +265 -0
- package/configs/_shared/rules/conventions/npm.md +80 -0
- package/configs/_shared/{.claude/rules → rules/conventions}/performance.md +1 -1
- package/configs/_shared/rules/conventions/principles.md +334 -0
- package/configs/_shared/rules/devops/ci-cd.md +262 -0
- package/configs/_shared/rules/devops/docker.md +275 -0
- package/configs/_shared/rules/devops/nx.md +194 -0
- package/configs/_shared/rules/domain/backend/api-design.md +203 -0
- package/configs/_shared/rules/lang/csharp/async.md +220 -0
- package/configs/_shared/rules/lang/csharp/csharp.md +314 -0
- package/configs/_shared/rules/lang/csharp/linq.md +210 -0
- package/configs/_shared/rules/lang/python/async.md +337 -0
- package/configs/_shared/rules/lang/python/celery.md +476 -0
- package/configs/_shared/rules/lang/python/config.md +339 -0
- package/configs/{python/.claude/rules → _shared/rules/lang/python}/database/sqlalchemy.md +6 -1
- package/configs/_shared/rules/lang/python/deployment.md +523 -0
- package/configs/_shared/rules/lang/python/error-handling.md +330 -0
- package/configs/_shared/rules/lang/python/migrations.md +421 -0
- package/configs/_shared/rules/lang/python/python.md +172 -0
- package/configs/_shared/rules/lang/python/repository.md +383 -0
- package/configs/{python/.claude/rules → _shared/rules/lang/python}/testing.md +2 -69
- package/configs/_shared/rules/lang/typescript/async.md +447 -0
- package/configs/_shared/rules/lang/typescript/generics.md +356 -0
- package/configs/_shared/rules/lang/typescript/typescript.md +212 -0
- package/configs/_shared/rules/quality/error-handling.md +48 -0
- package/configs/_shared/rules/quality/logging.md +45 -0
- package/configs/_shared/rules/quality/observability.md +240 -0
- package/configs/_shared/rules/quality/testing-patterns.md +65 -0
- package/configs/_shared/rules/security/secrets-management.md +222 -0
- package/configs/_shared/skills/analysis/explore/SKILL.md +257 -0
- package/configs/_shared/skills/analysis/security-audit/SKILL.md +184 -0
- package/configs/_shared/skills/dev/api-endpoint/SKILL.md +126 -0
- package/configs/_shared/{.claude/commands/generate-tests.md → skills/dev/generate-tests/SKILL.md} +6 -0
- package/configs/_shared/{.claude/commands/fix-issue.md → skills/git/fix-issue/SKILL.md} +6 -0
- package/configs/_shared/{.claude/commands/review-pr.md → skills/git/review-pr/SKILL.md} +6 -0
- package/configs/_shared/skills/infra/deploy/SKILL.md +139 -0
- package/configs/_shared/skills/infra/docker/SKILL.md +95 -0
- package/configs/_shared/skills/infra/migration/SKILL.md +158 -0
- package/configs/_shared/skills/nx/nx-affected/SKILL.md +72 -0
- package/configs/_shared/skills/nx/nx-lib/SKILL.md +375 -0
- package/configs/angular/CLAUDE.md +24 -216
- package/configs/angular/{.claude/rules → rules/core}/components.md +69 -15
- package/configs/angular/rules/core/resource.md +285 -0
- package/configs/angular/rules/core/signals.md +323 -0
- package/configs/angular/rules/http.md +338 -0
- package/configs/angular/rules/routing.md +291 -0
- package/configs/angular/rules/ssr.md +312 -0
- package/configs/angular/rules/state/signal-store.md +408 -0
- package/configs/angular/{.claude/rules → rules/state}/state.md +2 -2
- package/configs/angular/{.claude/rules → rules}/testing.md +7 -7
- package/configs/angular/rules/ui/aria.md +422 -0
- package/configs/angular/rules/ui/forms.md +424 -0
- package/configs/angular/rules/ui/pipes-directives.md +335 -0
- package/configs/angular/{.claude/settings.json → settings.json} +3 -0
- package/configs/dotnet/CLAUDE.md +53 -286
- package/configs/dotnet/rules/background-services.md +552 -0
- package/configs/dotnet/rules/configuration.md +426 -0
- package/configs/dotnet/rules/ddd.md +447 -0
- package/configs/dotnet/rules/dependency-injection.md +343 -0
- package/configs/dotnet/rules/mediatr.md +320 -0
- package/configs/dotnet/rules/middleware.md +489 -0
- package/configs/dotnet/rules/result-pattern.md +363 -0
- package/configs/dotnet/rules/validation.md +388 -0
- package/configs/dotnet/settings.json +29 -0
- package/configs/fastapi/CLAUDE.md +144 -0
- package/configs/fastapi/rules/background-tasks.md +254 -0
- package/configs/fastapi/rules/dependencies.md +170 -0
- package/configs/{python/.claude → fastapi}/rules/fastapi.md +61 -1
- package/configs/fastapi/rules/lifespan.md +274 -0
- package/configs/fastapi/rules/middleware.md +229 -0
- package/configs/fastapi/rules/pydantic.md +433 -0
- package/configs/fastapi/rules/responses.md +251 -0
- package/configs/fastapi/rules/routers.md +202 -0
- package/configs/fastapi/rules/security.md +222 -0
- package/configs/fastapi/rules/testing.md +251 -0
- package/configs/fastapi/rules/websockets.md +298 -0
- package/configs/fastapi/settings.json +35 -0
- package/configs/flask/CLAUDE.md +166 -0
- package/configs/flask/rules/blueprints.md +208 -0
- package/configs/flask/rules/cli.md +285 -0
- package/configs/flask/rules/configuration.md +281 -0
- package/configs/flask/rules/context.md +238 -0
- package/configs/flask/rules/error-handlers.md +278 -0
- package/configs/flask/rules/extensions.md +278 -0
- package/configs/flask/rules/flask.md +171 -0
- package/configs/flask/rules/marshmallow.md +206 -0
- package/configs/flask/rules/security.md +267 -0
- package/configs/flask/rules/testing.md +284 -0
- package/configs/flask/settings.json +35 -0
- package/configs/nestjs/CLAUDE.md +57 -215
- package/configs/nestjs/rules/common-patterns.md +300 -0
- package/configs/nestjs/rules/filters.md +376 -0
- package/configs/nestjs/rules/interceptors.md +317 -0
- package/configs/nestjs/rules/middleware.md +321 -0
- package/configs/nestjs/{.claude/rules → rules}/modules.md +26 -0
- package/configs/nestjs/rules/pipes.md +351 -0
- package/configs/nestjs/rules/websockets.md +451 -0
- package/configs/nestjs/settings.json +31 -0
- package/configs/nextjs/CLAUDE.md +69 -331
- package/configs/nextjs/rules/api-routes.md +358 -0
- package/configs/nextjs/rules/authentication.md +355 -0
- package/configs/nextjs/{.claude/rules → rules}/components.md +52 -0
- package/configs/nextjs/rules/data-fetching.md +249 -0
- package/configs/nextjs/rules/database.md +400 -0
- package/configs/nextjs/rules/middleware.md +303 -0
- package/configs/nextjs/rules/routing.md +324 -0
- package/configs/nextjs/rules/seo.md +350 -0
- package/configs/nextjs/rules/server-actions.md +353 -0
- package/configs/nextjs/{.claude/rules → rules}/state/zustand.md +6 -6
- package/configs/nextjs/{.claude/settings.json → settings.json} +7 -0
- package/package.json +24 -9
- package/src/cli.js +218 -0
- package/src/config.js +63 -0
- package/src/index.js +4 -0
- package/src/installer.js +414 -0
- package/src/merge.js +109 -0
- package/src/tech-config.json +45 -0
- package/src/utils.js +88 -0
- package/configs/dotnet/.claude/settings.json +0 -9
- package/configs/nestjs/.claude/settings.json +0 -15
- package/configs/python/.claude/rules/flask.md +0 -332
- package/configs/python/.claude/settings.json +0 -18
- package/configs/python/CLAUDE.md +0 -273
- package/src/install.js +0 -315
- /package/configs/_shared/{.claude/rules → rules/domain/frontend}/accessibility.md +0 -0
- /package/configs/_shared/{.claude/rules → rules/security}/security.md +0 -0
- /package/configs/_shared/{.claude/skills → skills/dev}/debug/SKILL.md +0 -0
- /package/configs/_shared/{.claude/skills → skills/dev}/learning/SKILL.md +0 -0
- /package/configs/_shared/{.claude/skills → skills/dev}/spec/SKILL.md +0 -0
- /package/configs/_shared/{.claude/skills → skills/git}/review/SKILL.md +0 -0
- /package/configs/dotnet/{.claude/rules → rules}/api.md +0 -0
- /package/configs/dotnet/{.claude/rules → rules}/architecture.md +0 -0
- /package/configs/dotnet/{.claude/rules → rules}/database/efcore.md +0 -0
- /package/configs/dotnet/{.claude/rules → rules}/testing.md +0 -0
- /package/configs/nestjs/{.claude/rules → rules}/auth.md +0 -0
- /package/configs/nestjs/{.claude/rules → rules}/database/prisma.md +0 -0
- /package/configs/nestjs/{.claude/rules → rules}/database/typeorm.md +0 -0
- /package/configs/nestjs/{.claude/rules → rules}/testing.md +0 -0
- /package/configs/nestjs/{.claude/rules → rules}/validation.md +0 -0
- /package/configs/nextjs/{.claude/rules → rules}/state/redux-toolkit.md +0 -0
- /package/configs/nextjs/{.claude/rules → rules}/testing.md +0 -0
|
@@ -0,0 +1,358 @@
|
|
|
1
|
+
---
|
|
2
|
+
paths:
|
|
3
|
+
- "app/api/**/*.ts"
|
|
4
|
+
- "src/app/api/**/*.ts"
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Next.js API Routes (App Router)
|
|
8
|
+
|
|
9
|
+
## Basic Route Handler
|
|
10
|
+
|
|
11
|
+
```typescript
|
|
12
|
+
// app/api/users/route.ts
|
|
13
|
+
import { NextResponse } from 'next/server';
|
|
14
|
+
|
|
15
|
+
export async function GET() {
|
|
16
|
+
const users = await db.user.findMany();
|
|
17
|
+
return NextResponse.json(users);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export async function POST(request: Request) {
|
|
21
|
+
const body = await request.json();
|
|
22
|
+
const user = await db.user.create({ data: body });
|
|
23
|
+
return NextResponse.json(user, { status: 201 });
|
|
24
|
+
}
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Dynamic Routes
|
|
28
|
+
|
|
29
|
+
```typescript
|
|
30
|
+
// app/api/users/[id]/route.ts
|
|
31
|
+
import { NextResponse } from 'next/server';
|
|
32
|
+
|
|
33
|
+
export async function GET(
|
|
34
|
+
request: Request,
|
|
35
|
+
{ params }: { params: Promise<{ id: string }> }
|
|
36
|
+
) {
|
|
37
|
+
const { id } = await params;
|
|
38
|
+
const user = await db.user.findUnique({ where: { id } });
|
|
39
|
+
|
|
40
|
+
if (!user) {
|
|
41
|
+
return NextResponse.json(
|
|
42
|
+
{ error: 'User not found' },
|
|
43
|
+
{ status: 404 }
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return NextResponse.json(user);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export async function PUT(
|
|
51
|
+
request: Request,
|
|
52
|
+
{ params }: { params: Promise<{ id: string }> }
|
|
53
|
+
) {
|
|
54
|
+
const { id } = await params;
|
|
55
|
+
const body = await request.json();
|
|
56
|
+
|
|
57
|
+
const user = await db.user.update({
|
|
58
|
+
where: { id },
|
|
59
|
+
data: body,
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
return NextResponse.json(user);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export async function DELETE(
|
|
66
|
+
request: Request,
|
|
67
|
+
{ params }: { params: Promise<{ id: string }> }
|
|
68
|
+
) {
|
|
69
|
+
const { id } = await params;
|
|
70
|
+
await db.user.delete({ where: { id } });
|
|
71
|
+
return new NextResponse(null, { status: 204 });
|
|
72
|
+
}
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
## Request Handling
|
|
76
|
+
|
|
77
|
+
### Query Parameters
|
|
78
|
+
|
|
79
|
+
```typescript
|
|
80
|
+
export async function GET(request: Request) {
|
|
81
|
+
const { searchParams } = new URL(request.url);
|
|
82
|
+
const page = parseInt(searchParams.get('page') || '1');
|
|
83
|
+
const limit = parseInt(searchParams.get('limit') || '10');
|
|
84
|
+
const search = searchParams.get('search') || '';
|
|
85
|
+
|
|
86
|
+
const users = await db.user.findMany({
|
|
87
|
+
where: { name: { contains: search } },
|
|
88
|
+
skip: (page - 1) * limit,
|
|
89
|
+
take: limit,
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
const total = await db.user.count({
|
|
93
|
+
where: { name: { contains: search } },
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
return NextResponse.json({
|
|
97
|
+
data: users,
|
|
98
|
+
meta: {
|
|
99
|
+
page,
|
|
100
|
+
limit,
|
|
101
|
+
total,
|
|
102
|
+
totalPages: Math.ceil(total / limit),
|
|
103
|
+
},
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
### Headers & Cookies
|
|
109
|
+
|
|
110
|
+
```typescript
|
|
111
|
+
import { cookies, headers } from 'next/headers';
|
|
112
|
+
|
|
113
|
+
export async function GET() {
|
|
114
|
+
const headersList = await headers();
|
|
115
|
+
const authHeader = headersList.get('authorization');
|
|
116
|
+
|
|
117
|
+
const cookieStore = await cookies();
|
|
118
|
+
const token = cookieStore.get('token');
|
|
119
|
+
|
|
120
|
+
return NextResponse.json({ auth: !!authHeader, hasToken: !!token });
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export async function POST() {
|
|
124
|
+
const response = NextResponse.json({ success: true });
|
|
125
|
+
|
|
126
|
+
// Set cookie
|
|
127
|
+
response.cookies.set('session', 'value', {
|
|
128
|
+
httpOnly: true,
|
|
129
|
+
secure: process.env.NODE_ENV === 'production',
|
|
130
|
+
sameSite: 'lax',
|
|
131
|
+
maxAge: 60 * 60 * 24 * 7, // 1 week
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
return response;
|
|
135
|
+
}
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
## Validation with Zod
|
|
139
|
+
|
|
140
|
+
```typescript
|
|
141
|
+
// app/api/users/route.ts
|
|
142
|
+
import { NextResponse } from 'next/server';
|
|
143
|
+
import { z } from 'zod';
|
|
144
|
+
|
|
145
|
+
const createUserSchema = z.object({
|
|
146
|
+
email: z.string().email(),
|
|
147
|
+
name: z.string().min(2).max(100),
|
|
148
|
+
age: z.number().min(18).optional(),
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
export async function POST(request: Request) {
|
|
152
|
+
const body = await request.json();
|
|
153
|
+
|
|
154
|
+
const result = createUserSchema.safeParse(body);
|
|
155
|
+
|
|
156
|
+
if (!result.success) {
|
|
157
|
+
return NextResponse.json(
|
|
158
|
+
{
|
|
159
|
+
type: 'validation_error',
|
|
160
|
+
title: 'Validation Error',
|
|
161
|
+
status: 400,
|
|
162
|
+
errors: result.error.issues.map(issue => ({
|
|
163
|
+
field: issue.path.join('.'),
|
|
164
|
+
message: issue.message,
|
|
165
|
+
})),
|
|
166
|
+
},
|
|
167
|
+
{ status: 400 }
|
|
168
|
+
);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const user = await db.user.create({ data: result.data });
|
|
172
|
+
return NextResponse.json(user, { status: 201 });
|
|
173
|
+
}
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
## Error Handling
|
|
177
|
+
|
|
178
|
+
```typescript
|
|
179
|
+
// lib/api-error.ts
|
|
180
|
+
export class ApiError extends Error {
|
|
181
|
+
constructor(
|
|
182
|
+
public status: number,
|
|
183
|
+
message: string,
|
|
184
|
+
public code?: string
|
|
185
|
+
) {
|
|
186
|
+
super(message);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// app/api/users/[id]/route.ts
|
|
191
|
+
import { ApiError } from '@/lib/api-error';
|
|
192
|
+
|
|
193
|
+
export async function GET(
|
|
194
|
+
request: Request,
|
|
195
|
+
{ params }: { params: Promise<{ id: string }> }
|
|
196
|
+
) {
|
|
197
|
+
try {
|
|
198
|
+
const { id } = await params;
|
|
199
|
+
const user = await db.user.findUnique({ where: { id } });
|
|
200
|
+
|
|
201
|
+
if (!user) {
|
|
202
|
+
throw new ApiError(404, 'User not found', 'USER_NOT_FOUND');
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
return NextResponse.json(user);
|
|
206
|
+
} catch (error) {
|
|
207
|
+
if (error instanceof ApiError) {
|
|
208
|
+
return NextResponse.json(
|
|
209
|
+
{
|
|
210
|
+
type: `https://api.example.com/errors/${error.code}`,
|
|
211
|
+
title: error.message,
|
|
212
|
+
status: error.status,
|
|
213
|
+
},
|
|
214
|
+
{ status: error.status }
|
|
215
|
+
);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
console.error('Unexpected error:', error);
|
|
219
|
+
return NextResponse.json(
|
|
220
|
+
{ title: 'Internal Server Error', status: 500 },
|
|
221
|
+
{ status: 500 }
|
|
222
|
+
);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
## Authentication
|
|
228
|
+
|
|
229
|
+
```typescript
|
|
230
|
+
import { auth } from '@/auth';
|
|
231
|
+
|
|
232
|
+
export async function GET() {
|
|
233
|
+
const session = await auth();
|
|
234
|
+
|
|
235
|
+
if (!session) {
|
|
236
|
+
return NextResponse.json(
|
|
237
|
+
{ error: 'Unauthorized' },
|
|
238
|
+
{ status: 401 }
|
|
239
|
+
);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
const users = await db.user.findMany({
|
|
243
|
+
where: { organizationId: session.user.organizationId },
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
return NextResponse.json(users);
|
|
247
|
+
}
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
## File Upload
|
|
251
|
+
|
|
252
|
+
```typescript
|
|
253
|
+
// app/api/upload/route.ts
|
|
254
|
+
import { writeFile } from 'fs/promises';
|
|
255
|
+
import { NextResponse } from 'next/server';
|
|
256
|
+
|
|
257
|
+
export async function POST(request: Request) {
|
|
258
|
+
const formData = await request.formData();
|
|
259
|
+
const file = formData.get('file') as File | null;
|
|
260
|
+
|
|
261
|
+
if (!file) {
|
|
262
|
+
return NextResponse.json(
|
|
263
|
+
{ error: 'No file uploaded' },
|
|
264
|
+
{ status: 400 }
|
|
265
|
+
);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// Validate file
|
|
269
|
+
const maxSize = 5 * 1024 * 1024; // 5MB
|
|
270
|
+
if (file.size > maxSize) {
|
|
271
|
+
return NextResponse.json(
|
|
272
|
+
{ error: 'File too large' },
|
|
273
|
+
{ status: 400 }
|
|
274
|
+
);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
const allowedTypes = ['image/jpeg', 'image/png', 'image/webp'];
|
|
278
|
+
if (!allowedTypes.includes(file.type)) {
|
|
279
|
+
return NextResponse.json(
|
|
280
|
+
{ error: 'Invalid file type' },
|
|
281
|
+
{ status: 400 }
|
|
282
|
+
);
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
const bytes = await file.arrayBuffer();
|
|
286
|
+
const buffer = Buffer.from(bytes);
|
|
287
|
+
|
|
288
|
+
const filename = `${Date.now()}-${file.name}`;
|
|
289
|
+
await writeFile(`./public/uploads/${filename}`, buffer);
|
|
290
|
+
|
|
291
|
+
return NextResponse.json({ url: `/uploads/${filename}` });
|
|
292
|
+
}
|
|
293
|
+
```
|
|
294
|
+
|
|
295
|
+
## Streaming Response
|
|
296
|
+
|
|
297
|
+
```typescript
|
|
298
|
+
export async function GET() {
|
|
299
|
+
const encoder = new TextEncoder();
|
|
300
|
+
|
|
301
|
+
const stream = new ReadableStream({
|
|
302
|
+
async start(controller) {
|
|
303
|
+
for (let i = 0; i < 10; i++) {
|
|
304
|
+
controller.enqueue(encoder.encode(`data: ${i}\n\n`));
|
|
305
|
+
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
306
|
+
}
|
|
307
|
+
controller.close();
|
|
308
|
+
},
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
return new Response(stream, {
|
|
312
|
+
headers: {
|
|
313
|
+
'Content-Type': 'text/event-stream',
|
|
314
|
+
'Cache-Control': 'no-cache',
|
|
315
|
+
'Connection': 'keep-alive',
|
|
316
|
+
},
|
|
317
|
+
});
|
|
318
|
+
}
|
|
319
|
+
```
|
|
320
|
+
|
|
321
|
+
## Route Configuration
|
|
322
|
+
|
|
323
|
+
```typescript
|
|
324
|
+
// Caching
|
|
325
|
+
export const revalidate = 60; // Revalidate every 60 seconds
|
|
326
|
+
export const dynamic = 'force-dynamic'; // Always dynamic
|
|
327
|
+
|
|
328
|
+
// Runtime
|
|
329
|
+
export const runtime = 'edge'; // or 'nodejs'
|
|
330
|
+
|
|
331
|
+
// Max duration (Vercel)
|
|
332
|
+
export const maxDuration = 30;
|
|
333
|
+
```
|
|
334
|
+
|
|
335
|
+
## Anti-patterns
|
|
336
|
+
|
|
337
|
+
```typescript
|
|
338
|
+
// BAD: Not validating input
|
|
339
|
+
export async function POST(request: Request) {
|
|
340
|
+
const body = await request.json();
|
|
341
|
+
await db.user.create({ data: body }); // SQL injection risk!
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
// GOOD: Validate with Zod
|
|
345
|
+
const result = schema.safeParse(body);
|
|
346
|
+
if (!result.success) return errorResponse(result.error);
|
|
347
|
+
|
|
348
|
+
// BAD: Exposing internal errors
|
|
349
|
+
catch (error) {
|
|
350
|
+
return NextResponse.json({ error: error.message }); // Leaks info!
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
// GOOD: Generic error message
|
|
354
|
+
catch (error) {
|
|
355
|
+
console.error(error);
|
|
356
|
+
return NextResponse.json({ error: 'Internal error' }, { status: 500 });
|
|
357
|
+
}
|
|
358
|
+
```
|
|
@@ -0,0 +1,355 @@
|
|
|
1
|
+
---
|
|
2
|
+
paths:
|
|
3
|
+
- "**/auth/**"
|
|
4
|
+
- "**/login/**"
|
|
5
|
+
- "**/api/auth/**"
|
|
6
|
+
- "middleware.ts"
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
# Next.js Authentication
|
|
10
|
+
|
|
11
|
+
## NextAuth.js v5 Setup
|
|
12
|
+
|
|
13
|
+
### Configuration
|
|
14
|
+
|
|
15
|
+
```typescript
|
|
16
|
+
// auth.ts
|
|
17
|
+
import NextAuth from 'next-auth';
|
|
18
|
+
import Credentials from 'next-auth/providers/credentials';
|
|
19
|
+
import Google from 'next-auth/providers/google';
|
|
20
|
+
import { authConfig } from './auth.config';
|
|
21
|
+
|
|
22
|
+
export const { handlers, auth, signIn, signOut } = NextAuth({
|
|
23
|
+
...authConfig,
|
|
24
|
+
providers: [
|
|
25
|
+
Google({
|
|
26
|
+
clientId: process.env.GOOGLE_CLIENT_ID!,
|
|
27
|
+
clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
|
|
28
|
+
}),
|
|
29
|
+
Credentials({
|
|
30
|
+
async authorize(credentials) {
|
|
31
|
+
const { email, password } = credentials;
|
|
32
|
+
const user = await verifyCredentials(email, password);
|
|
33
|
+
if (!user) return null;
|
|
34
|
+
return user;
|
|
35
|
+
},
|
|
36
|
+
}),
|
|
37
|
+
],
|
|
38
|
+
});
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
### Auth Config (for Edge)
|
|
42
|
+
|
|
43
|
+
```typescript
|
|
44
|
+
// auth.config.ts
|
|
45
|
+
import type { NextAuthConfig } from 'next-auth';
|
|
46
|
+
|
|
47
|
+
export const authConfig = {
|
|
48
|
+
pages: {
|
|
49
|
+
signIn: '/login',
|
|
50
|
+
error: '/auth/error',
|
|
51
|
+
},
|
|
52
|
+
callbacks: {
|
|
53
|
+
authorized({ auth, request: { nextUrl } }) {
|
|
54
|
+
const isLoggedIn = !!auth?.user;
|
|
55
|
+
const isProtected = nextUrl.pathname.startsWith('/dashboard');
|
|
56
|
+
|
|
57
|
+
if (isProtected && !isLoggedIn) {
|
|
58
|
+
return Response.redirect(new URL('/login', nextUrl));
|
|
59
|
+
}
|
|
60
|
+
return true;
|
|
61
|
+
},
|
|
62
|
+
jwt({ token, user }) {
|
|
63
|
+
if (user) {
|
|
64
|
+
token.id = user.id;
|
|
65
|
+
token.role = user.role;
|
|
66
|
+
}
|
|
67
|
+
return token;
|
|
68
|
+
},
|
|
69
|
+
session({ session, token }) {
|
|
70
|
+
session.user.id = token.id as string;
|
|
71
|
+
session.user.role = token.role as string;
|
|
72
|
+
return session;
|
|
73
|
+
},
|
|
74
|
+
},
|
|
75
|
+
providers: [], // Configured in auth.ts
|
|
76
|
+
} satisfies NextAuthConfig;
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
### Route Handlers
|
|
80
|
+
|
|
81
|
+
```typescript
|
|
82
|
+
// app/api/auth/[...nextauth]/route.ts
|
|
83
|
+
import { handlers } from '@/auth';
|
|
84
|
+
export const { GET, POST } = handlers;
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
## Middleware Protection
|
|
88
|
+
|
|
89
|
+
```typescript
|
|
90
|
+
// middleware.ts
|
|
91
|
+
import { auth } from './auth';
|
|
92
|
+
|
|
93
|
+
export default auth((req) => {
|
|
94
|
+
const { nextUrl } = req;
|
|
95
|
+
const isLoggedIn = !!req.auth;
|
|
96
|
+
|
|
97
|
+
const publicPaths = ['/login', '/register', '/'];
|
|
98
|
+
const isPublic = publicPaths.includes(nextUrl.pathname);
|
|
99
|
+
|
|
100
|
+
if (!isLoggedIn && !isPublic) {
|
|
101
|
+
return Response.redirect(new URL('/login', nextUrl));
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Role-based protection
|
|
105
|
+
if (nextUrl.pathname.startsWith('/admin')) {
|
|
106
|
+
if (req.auth?.user?.role !== 'admin') {
|
|
107
|
+
return Response.redirect(new URL('/forbidden', nextUrl));
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
export const config = {
|
|
113
|
+
matcher: ['/((?!api|_next/static|_next/image|favicon.ico).*)'],
|
|
114
|
+
};
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
## Server Components
|
|
118
|
+
|
|
119
|
+
### Get Session
|
|
120
|
+
|
|
121
|
+
```typescript
|
|
122
|
+
// app/dashboard/page.tsx
|
|
123
|
+
import { auth } from '@/auth';
|
|
124
|
+
import { redirect } from 'next/navigation';
|
|
125
|
+
|
|
126
|
+
export default async function DashboardPage() {
|
|
127
|
+
const session = await auth();
|
|
128
|
+
|
|
129
|
+
if (!session) {
|
|
130
|
+
redirect('/login');
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
return (
|
|
134
|
+
<div>
|
|
135
|
+
<h1>Welcome, {session.user.name}</h1>
|
|
136
|
+
</div>
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
### Protected Layout
|
|
142
|
+
|
|
143
|
+
```typescript
|
|
144
|
+
// app/(protected)/layout.tsx
|
|
145
|
+
import { auth } from '@/auth';
|
|
146
|
+
import { redirect } from 'next/navigation';
|
|
147
|
+
|
|
148
|
+
export default async function ProtectedLayout({
|
|
149
|
+
children,
|
|
150
|
+
}: {
|
|
151
|
+
children: React.ReactNode;
|
|
152
|
+
}) {
|
|
153
|
+
const session = await auth();
|
|
154
|
+
|
|
155
|
+
if (!session) {
|
|
156
|
+
redirect('/login');
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
return <>{children}</>;
|
|
160
|
+
}
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
## Client Components
|
|
164
|
+
|
|
165
|
+
### Sign In/Out
|
|
166
|
+
|
|
167
|
+
```typescript
|
|
168
|
+
'use client';
|
|
169
|
+
|
|
170
|
+
import { signIn, signOut } from 'next-auth/react';
|
|
171
|
+
|
|
172
|
+
export function LoginButton() {
|
|
173
|
+
return (
|
|
174
|
+
<button onClick={() => signIn('google', { callbackUrl: '/dashboard' })}>
|
|
175
|
+
Sign in with Google
|
|
176
|
+
</button>
|
|
177
|
+
);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
export function LogoutButton() {
|
|
181
|
+
return (
|
|
182
|
+
<button onClick={() => signOut({ callbackUrl: '/' })}>
|
|
183
|
+
Sign out
|
|
184
|
+
</button>
|
|
185
|
+
);
|
|
186
|
+
}
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
### Use Session Hook
|
|
190
|
+
|
|
191
|
+
```typescript
|
|
192
|
+
'use client';
|
|
193
|
+
|
|
194
|
+
import { useSession } from 'next-auth/react';
|
|
195
|
+
|
|
196
|
+
export function UserProfile() {
|
|
197
|
+
const { data: session, status } = useSession();
|
|
198
|
+
|
|
199
|
+
if (status === 'loading') {
|
|
200
|
+
return <Skeleton />;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
if (!session) {
|
|
204
|
+
return <LoginButton />;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
return (
|
|
208
|
+
<div>
|
|
209
|
+
<img src={session.user.image} alt={session.user.name} />
|
|
210
|
+
<span>{session.user.name}</span>
|
|
211
|
+
</div>
|
|
212
|
+
);
|
|
213
|
+
}
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
## Server Actions
|
|
217
|
+
|
|
218
|
+
### Login Action
|
|
219
|
+
|
|
220
|
+
```typescript
|
|
221
|
+
// app/login/actions.ts
|
|
222
|
+
'use server';
|
|
223
|
+
|
|
224
|
+
import { signIn } from '@/auth';
|
|
225
|
+
import { AuthError } from 'next-auth';
|
|
226
|
+
|
|
227
|
+
export async function loginAction(
|
|
228
|
+
prevState: { error?: string } | undefined,
|
|
229
|
+
formData: FormData
|
|
230
|
+
) {
|
|
231
|
+
try {
|
|
232
|
+
await signIn('credentials', {
|
|
233
|
+
email: formData.get('email'),
|
|
234
|
+
password: formData.get('password'),
|
|
235
|
+
redirectTo: '/dashboard',
|
|
236
|
+
});
|
|
237
|
+
} catch (error) {
|
|
238
|
+
if (error instanceof AuthError) {
|
|
239
|
+
switch (error.type) {
|
|
240
|
+
case 'CredentialsSignin':
|
|
241
|
+
return { error: 'Invalid credentials' };
|
|
242
|
+
default:
|
|
243
|
+
return { error: 'Something went wrong' };
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
throw error;
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
### Login Form
|
|
252
|
+
|
|
253
|
+
```typescript
|
|
254
|
+
'use client';
|
|
255
|
+
|
|
256
|
+
import { useActionState } from 'react';
|
|
257
|
+
import { loginAction } from './actions';
|
|
258
|
+
|
|
259
|
+
export function LoginForm() {
|
|
260
|
+
const [state, action, isPending] = useActionState(loginAction, undefined);
|
|
261
|
+
|
|
262
|
+
return (
|
|
263
|
+
<form action={action}>
|
|
264
|
+
<input name="email" type="email" required />
|
|
265
|
+
<input name="password" type="password" required />
|
|
266
|
+
|
|
267
|
+
{state?.error && <p className="error">{state.error}</p>}
|
|
268
|
+
|
|
269
|
+
<button type="submit" disabled={isPending}>
|
|
270
|
+
{isPending ? 'Signing in...' : 'Sign in'}
|
|
271
|
+
</button>
|
|
272
|
+
</form>
|
|
273
|
+
);
|
|
274
|
+
}
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
## Type Extensions
|
|
278
|
+
|
|
279
|
+
```typescript
|
|
280
|
+
// types/next-auth.d.ts
|
|
281
|
+
import 'next-auth';
|
|
282
|
+
|
|
283
|
+
declare module 'next-auth' {
|
|
284
|
+
interface User {
|
|
285
|
+
role: string;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
interface Session {
|
|
289
|
+
user: User & {
|
|
290
|
+
id: string;
|
|
291
|
+
role: string;
|
|
292
|
+
};
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
declare module 'next-auth/jwt' {
|
|
297
|
+
interface JWT {
|
|
298
|
+
id: string;
|
|
299
|
+
role: string;
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
```
|
|
303
|
+
|
|
304
|
+
## Session Provider
|
|
305
|
+
|
|
306
|
+
```typescript
|
|
307
|
+
// app/providers.tsx
|
|
308
|
+
'use client';
|
|
309
|
+
|
|
310
|
+
import { SessionProvider } from 'next-auth/react';
|
|
311
|
+
|
|
312
|
+
export function Providers({ children }: { children: React.ReactNode }) {
|
|
313
|
+
return <SessionProvider>{children}</SessionProvider>;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
// app/layout.tsx
|
|
317
|
+
import { Providers } from './providers';
|
|
318
|
+
|
|
319
|
+
export default function RootLayout({ children }) {
|
|
320
|
+
return (
|
|
321
|
+
<html>
|
|
322
|
+
<body>
|
|
323
|
+
<Providers>{children}</Providers>
|
|
324
|
+
</body>
|
|
325
|
+
</html>
|
|
326
|
+
);
|
|
327
|
+
}
|
|
328
|
+
```
|
|
329
|
+
|
|
330
|
+
## Anti-patterns
|
|
331
|
+
|
|
332
|
+
```typescript
|
|
333
|
+
// BAD: Checking auth in client component for protection
|
|
334
|
+
'use client';
|
|
335
|
+
export function ProtectedPage() {
|
|
336
|
+
const { data: session } = useSession();
|
|
337
|
+
if (!session) return <Redirect />; // Too late, page already loaded
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
// GOOD: Check in middleware or server component
|
|
341
|
+
export default async function ProtectedPage() {
|
|
342
|
+
const session = await auth();
|
|
343
|
+
if (!session) redirect('/login');
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
// BAD: Exposing secrets
|
|
347
|
+
const user = await db.user.findUnique({ where: { id } });
|
|
348
|
+
return { ...user, password: user.password }; // Leaking password!
|
|
349
|
+
|
|
350
|
+
// GOOD: Select only needed fields
|
|
351
|
+
const user = await db.user.findUnique({
|
|
352
|
+
where: { id },
|
|
353
|
+
select: { id: true, name: true, email: true },
|
|
354
|
+
});
|
|
355
|
+
```
|