Skip to content

Commit d240055

Browse files
committed
feat: add 3 new rules (25 total) - middleware-auth, api-validation, file-uploads
1 parent 0c2ed02 commit d240055

4 files changed

Lines changed: 385 additions & 83 deletions

File tree

.cursor/rules/api-validation.mdc

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
---
2+
description: API route validation — enforces Zod validation on all route handlers and server actions
3+
globs: ["**/api/**", "**/actions/**", "**/app/api/**"]
4+
alwaysApply: false
5+
---
6+
7+
# API Route Validation Rules
8+
9+
## RULE 1: NEVER Access Request Body Without Zod Validation
10+
11+
Unvalidated request bodies are the #1 source of runtime crashes and injection vulnerabilities.
12+
Every API route MUST validate the incoming body with Zod before any processing.
13+
14+
✅ CORRECT — validate first:
15+
```typescript
16+
import { z } from 'zod'
17+
import { NextRequest, NextResponse } from 'next/server'
18+
19+
const CreatePostSchema = z.object({
20+
title: z.string().min(1).max(200),
21+
content: z.string().min(1).max(10000),
22+
published: z.boolean().default(false),
23+
})
24+
25+
export async function POST(request: NextRequest) {
26+
const body = await request.json()
27+
28+
const result = CreatePostSchema.safeParse(body)
29+
if (!result.success) {
30+
return NextResponse.json(
31+
{ error: 'Invalid request', details: result.error.flatten() },
32+
{ status: 400 }
33+
)
34+
}
35+
36+
const { title, content, published } = result.data
37+
// Now safe to use
38+
}
39+
```
40+
41+
❌ WRONG — raw body access:
42+
```typescript
43+
export async function POST(request: NextRequest) {
44+
const { title, content } = await request.json() // No validation — crashes on missing fields
45+
// title could be undefined, an object, a number — anything
46+
}
47+
```
48+
49+
## RULE 2: Validate Server Action Parameters With Zod
50+
51+
Server Actions receive untyped FormData or direct calls — always validate.
52+
53+
✅ CORRECT:
54+
```typescript
55+
'use server'
56+
import { z } from 'zod'
57+
58+
const UpdateProfileSchema = z.object({
59+
name: z.string().min(1).max(100),
60+
bio: z.string().max(500).optional(),
61+
})
62+
63+
export async function updateProfile(data: unknown) {
64+
const result = UpdateProfileSchema.safeParse(data)
65+
if (!result.success) {
66+
return { error: result.error.flatten().fieldErrors }
67+
}
68+
// Process result.data safely
69+
}
70+
```
71+
72+
## RULE 3: Always Return Consistent Error Shapes
73+
74+
API routes must return a consistent error shape: `{ error: string, details?: unknown }`.
75+
Never expose raw error messages, stack traces, or database errors to the client.
76+
77+
✅ CORRECT:
78+
```typescript
79+
try {
80+
// ... operation
81+
} catch (error) {
82+
console.error('[API_ERROR]', error) // Log server-side only
83+
return NextResponse.json({ error: 'Internal server error' }, { status: 500 })
84+
}
85+
```
86+
87+
❌ WRONG:
88+
```typescript
89+
} catch (error) {
90+
return NextResponse.json({ error: error.message }, { status: 500 }) // Leaks internals
91+
}
92+
```
93+
94+
## RULE 4: Authenticate Before Validating Business Logic
95+
96+
Auth check always happens BEFORE any database operations, even with valid Zod data.
97+
98+
```typescript
99+
export async function POST(request: NextRequest) {
100+
// 1. Auth first
101+
const supabase = await createClient()
102+
const { data: { user } } = await supabase.auth.getUser()
103+
if (!user) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
104+
105+
// 2. Validate input
106+
const result = Schema.safeParse(await request.json())
107+
if (!result.success) return NextResponse.json({ error: 'Invalid' }, { status: 400 })
108+
109+
// 3. Business logic with trusted user and validated data
110+
}
111+
```

.cursor/rules/file-uploads.mdc

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
---
2+
description: Image and file upload patterns — prevents insecure client-side uploads and missing file validation
3+
globs: ["**/api/upload/**", "**/actions/**", "**/components/**upload**", "**/app/**upload**"]
4+
alwaysApply: false
5+
---
6+
7+
# File Upload Security Rules
8+
9+
## RULE 1: NEVER Upload Directly From Client to Supabase Storage With the Anon Key
10+
11+
Client-side uploads with the anon key allow users to upload to any bucket path.
12+
Always generate a signed upload URL server-side, scoped to the authenticated user.
13+
14+
✅ CORRECT — server generates signed URL:
15+
```typescript
16+
// Server Action
17+
'use server'
18+
import { createClient } from '@/lib/supabase/server'
19+
20+
export async function getUploadUrl(filename: string, contentType: string) {
21+
const supabase = await createClient()
22+
const { data: { user } } = await supabase.auth.getUser()
23+
if (!user) throw new Error('Unauthorized')
24+
25+
// Scope the path to the user's ID — prevents path traversal
26+
const path = `${user.id}/${Date.now()}-${filename}`
27+
28+
const { data, error } = await supabase.storage
29+
.from('uploads')
30+
.createSignedUploadUrl(path)
31+
32+
if (error) throw error
33+
return { signedUrl: data.signedUrl, path }
34+
}
35+
```
36+
37+
❌ WRONG — direct client upload with anon key:
38+
```typescript
39+
// Client Component -- exposes storage to manipulation
40+
const { data, error } = await supabase.storage
41+
.from('uploads')
42+
.upload(`${filename}`, file) // No user scoping — any path writable
43+
```
44+
45+
## RULE 2: Validate File Type and Size Server-Side
46+
47+
Client-side validation is bypassed trivially. Always re-validate on the server.
48+
49+
✅ CORRECT:
50+
```typescript
51+
const ALLOWED_TYPES = ['image/jpeg', 'image/png', 'image/webp']
52+
const MAX_SIZE_BYTES = 5 * 1024 * 1024 // 5MB
53+
54+
export async function POST(request: NextRequest) {
55+
const formData = await request.formData()
56+
const file = formData.get('file') as File
57+
58+
if (!file) return NextResponse.json({ error: 'No file provided' }, { status: 400 })
59+
if (!ALLOWED_TYPES.includes(file.type)) {
60+
return NextResponse.json({ error: 'Invalid file type' }, { status: 400 })
61+
}
62+
if (file.size > MAX_SIZE_BYTES) {
63+
return NextResponse.json({ error: 'File too large' }, { status: 400 })
64+
}
65+
// Safe to process
66+
}
67+
```
68+
69+
## RULE 3: Always Scope Storage Paths to User ID
70+
71+
Storage paths must always be prefixed with the authenticated user's ID.
72+
This prevents users from reading or overwriting other users' files.
73+
74+
```typescript
75+
// Pattern: userId/timestamp-originalFilename
76+
const storagePath = `${user.id}/${Date.now()}-${sanitizedFilename}`
77+
78+
// RLS policy on the bucket should enforce: auth.uid()::text = (storage.foldername(name))[1]
79+
```
80+
81+
## RULE 4: Use Next.js Image Component for Supabase Storage URLs
82+
83+
Never use raw `<img>` tags with Supabase storage URLs. Configure Next.js image domains
84+
and use `<Image>` for automatic optimization and lazy loading.
85+
86+
```typescript
87+
// next.config.js
88+
images: {
89+
remotePatterns: [
90+
{
91+
protocol: 'https',
92+
hostname: '*.supabase.co',
93+
pathname: '/storage/v1/object/public/**',
94+
},
95+
],
96+
},
97+
```

.cursor/rules/middleware-auth.mdc

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
---
2+
description: Next.js middleware auth — prevents auth logic being placed in middleware instead of route handlers
3+
globs: ["**/middleware.ts", "**/middleware.tsx", "**/app/**"]
4+
alwaysApply: false
5+
---
6+
7+
# Middleware Auth Rules
8+
9+
## RULE 1: Middleware is for Session REFRESH, Not Auth Enforcement
10+
11+
SECURITY CRITICAL: Next.js middleware CANNOT securely enforce authentication.
12+
Middleware runs on the Edge Runtime and cannot verify Supabase JWTs reliably.
13+
The ONLY job of middleware is to call `updateSession()` to refresh the token.
14+
15+
✅ CORRECT — middleware only refreshes:
16+
```typescript
17+
// middleware.ts
18+
import { updateSession } from '@/lib/supabase/middleware'
19+
import { type NextRequest } from 'next/server'
20+
21+
export async function middleware(request: NextRequest) {
22+
return await updateSession(request) // Refresh only — no auth logic here
23+
}
24+
25+
export const config = {
26+
matcher: ['/((?!_next/static|_next/image|favicon.ico).*)'],
27+
}
28+
```
29+
30+
❌ WRONG — do NOT put auth enforcement in middleware:
31+
```typescript
32+
// VULNERABILITY: middleware auth check can be bypassed
33+
export async function middleware(request: NextRequest) {
34+
const session = await getSession() // Wrong: unreliable in Edge Runtime
35+
if (!session) return NextResponse.redirect('/login') // Can be bypassed
36+
}
37+
```
38+
39+
## RULE 2: Auth Enforcement Belongs in Layouts and Pages
40+
41+
Every protected route must call `getUser()` server-side in the layout or page component.
42+
This is the cryptographically secure check.
43+
44+
✅ CORRECT — auth in layout:
45+
```typescript
46+
// app/(protected)/layout.tsx
47+
import { createClient } from '@/lib/supabase/server'
48+
import { redirect } from 'next/navigation'
49+
50+
export default async function ProtectedLayout({ children }: { children: React.ReactNode }) {
51+
const supabase = await createClient()
52+
const { data: { user } } = await supabase.auth.getUser()
53+
54+
if (!user) redirect('/login')
55+
56+
return <>{children}</>
57+
}
58+
```
59+
60+
## RULE 3: Never Check Auth in Client Components
61+
62+
Auth state from Client Components can be spoofed through browser devtools.
63+
The server-side `getUser()` call in layouts is the source of truth.
64+
65+
❌ WRONG:
66+
```typescript
67+
'use client'
68+
// Do NOT gate features based on client-side auth state alone
69+
const { data: { user } } = await supabase.auth.getSession() // Untrusted
70+
if (!user) return <LoginPrompt />
71+
```

0 commit comments

Comments
 (0)