nextjs-hackathon-stack 0.1.29 → 0.1.31
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/dist/index.js +6 -3
- package/package.json +1 -1
- package/template/.cursor/agents/backend.md +10 -59
- package/template/.cursor/agents/business-intelligence.md +37 -15
- package/template/.cursor/agents/code-reviewer.md +2 -2
- package/template/.cursor/agents/frontend.md +6 -10
- package/template/.cursor/agents/security-researcher.md +8 -1
- package/template/.cursor/agents/technical-lead.md +47 -18
- package/template/.cursor/agents/test-qa.md +9 -49
- package/template/.cursor/rules/architecture.mdc +2 -0
- package/template/.cursor/rules/coding-standards.mdc +12 -1
- package/template/.cursor/rules/components.mdc +7 -0
- package/template/.cursor/rules/data-fetching.mdc +56 -5
- package/template/.cursor/rules/forms.mdc +4 -0
- package/template/.cursor/rules/general.mdc +5 -3
- package/template/.cursor/rules/nextjs.mdc +27 -3
- package/template/.cursor/rules/security.mdc +53 -3
- package/template/.cursor/rules/supabase.mdc +19 -3
- package/template/.cursor/rules/testing.mdc +13 -4
- package/template/.cursor/skills/create-feature/SKILL.md +8 -4
- package/template/.cursor/skills/create-feature/references/server-action-test-template.md +51 -0
- package/template/.cursor/skills/review-branch/SKILL.md +2 -22
- package/template/.cursor/skills/review-branch/references/review-checklist.md +36 -0
- package/template/.cursor/skills/security-audit/SKILL.md +8 -39
- package/template/.cursor/skills/security-audit/references/audit-steps.md +41 -0
- package/template/CLAUDE.md +27 -10
- package/template/eslint.config.ts +7 -1
- package/template/next.config.ts +2 -1
- package/template/src/app/(protected)/page.tsx +2 -4
- package/template/src/app/__tests__/auth-callback.test.ts +14 -0
- package/template/src/app/__tests__/protected-page.test.tsx +11 -13
- package/template/src/app/api/auth/callback/route.ts +2 -1
- package/template/src/e2e/login.spec.ts +4 -4
- package/template/src/features/todos/__tests__/todos.action.test.ts +78 -44
- package/template/src/features/todos/__tests__/todos.queries.test.ts +101 -0
- package/template/src/features/todos/actions/todos.action.ts +42 -28
- package/template/src/features/todos/queries/todos.queries.ts +16 -0
- package/template/src/shared/lib/supabase/middleware.ts +0 -1
- package/template/src/shared/lib/supabase/server.ts +0 -1
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
---
|
|
2
|
-
description: Next.js 16 App Router patterns.
|
|
2
|
+
description: Next.js 16.2 App Router patterns.
|
|
3
3
|
globs: ["src/app/**"]
|
|
4
4
|
---
|
|
5
5
|
|
|
6
|
-
# Next.js 16 Rules
|
|
6
|
+
# Next.js 16.2 Rules
|
|
7
7
|
|
|
8
8
|
## App Router Conventions
|
|
9
9
|
- `page.tsx` — public route page component
|
|
@@ -11,7 +11,7 @@ globs: ["src/app/**"]
|
|
|
11
11
|
- `loading.tsx` — Suspense boundary UI
|
|
12
12
|
- `error.tsx` — Error boundary UI
|
|
13
13
|
- `route.ts` — API route handler
|
|
14
|
-
- `proxy.ts` — Request proxy (project root, Node.js runtime)
|
|
14
|
+
- `proxy.ts` — Request proxy (project root, Node.js runtime). Intercepts and rewrites requests before they reach route handlers (e.g., session validation, redirects).
|
|
15
15
|
|
|
16
16
|
## Route Groups
|
|
17
17
|
- `(auth)` — unauthenticated routes
|
|
@@ -44,3 +44,27 @@ export const metadata: Metadata = {
|
|
|
44
44
|
description: "Page description",
|
|
45
45
|
};
|
|
46
46
|
```
|
|
47
|
+
|
|
48
|
+
## Browser Log Forwarding
|
|
49
|
+
|
|
50
|
+
Next.js 16.2 can forward browser logs to the terminal. Configure in `next.config.ts`:
|
|
51
|
+
|
|
52
|
+
```typescript
|
|
53
|
+
const nextConfig: NextConfig = {
|
|
54
|
+
logging: {
|
|
55
|
+
browserToTerminal: 'error', // 'error' (default) | 'warn' | true (all) | false (disabled)
|
|
56
|
+
},
|
|
57
|
+
};
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
- `'error'` — forwards browser errors only (default)
|
|
61
|
+
- `'warn'` — forwards errors and warnings
|
|
62
|
+
- `true` — forwards all console output
|
|
63
|
+
- `false` — disabled
|
|
64
|
+
|
|
65
|
+
## Dev Server Lock File
|
|
66
|
+
|
|
67
|
+
Next.js 16.2 writes `.next/dev/lock` when `next dev` starts. The file contains the PID, port, and URL of the running dev server.
|
|
68
|
+
|
|
69
|
+
- Agents and scripts must check `.next/dev/lock` before starting `next dev` or `next build` to avoid duplicate processes.
|
|
70
|
+
- If the lock file exists and the PID is still alive, attach to the existing server instead of starting a new one.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
|
-
description: Security rules
|
|
3
|
-
|
|
2
|
+
description: Security rules (OWASP, auth, RLS, XSS, CSP). Applied to server code, API routes, actions, auth, and config.
|
|
3
|
+
globs: ["src/**/actions/**", "src/**/api/**", "src/**/lib/supabase/**", "src/app/**/route.ts", "src/app/**/proxy.ts", "*.config.*"]
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Security Rules
|
|
@@ -28,7 +28,10 @@ Never use `NEXT_PUBLIC_` for secret keys.
|
|
|
28
28
|
- Write explicit allow/deny policies
|
|
29
29
|
- Test RLS policies — never assume they work
|
|
30
30
|
|
|
31
|
-
## Auth in API Routes
|
|
31
|
+
## Auth in API Routes and Server Actions
|
|
32
|
+
|
|
33
|
+
Every Server Action and API route that accesses data must verify auth via `getUser()` before any data access. RLS provides row-level enforcement, but `getUser()` at the action level is required as defense-in-depth — never rely on RLS alone.
|
|
34
|
+
|
|
32
35
|
```typescript
|
|
33
36
|
export async function POST(request: Request) {
|
|
34
37
|
const supabase = await createClient();
|
|
@@ -38,6 +41,36 @@ export async function POST(request: Request) {
|
|
|
38
41
|
}
|
|
39
42
|
```
|
|
40
43
|
|
|
44
|
+
Every mutation Server Action must:
|
|
45
|
+
1. Call `getUser()` and return an error result if no user
|
|
46
|
+
2. Scope all queries to `user.id` (even with RLS active)
|
|
47
|
+
|
|
48
|
+
```typescript
|
|
49
|
+
export async function deleteItemAction(id: string): Promise<ActionResult> {
|
|
50
|
+
const supabase = await createClient();
|
|
51
|
+
const { data: { user } } = await supabase.auth.getUser();
|
|
52
|
+
if (!user) return { status: "error", message: "No autenticado" };
|
|
53
|
+
|
|
54
|
+
const { error } = await supabase.from("items").delete().eq("id", id).eq("user_id", user.id);
|
|
55
|
+
if (error) return { status: "error", message: "Error al eliminar" };
|
|
56
|
+
|
|
57
|
+
revalidatePath("/");
|
|
58
|
+
return { status: "success", message: "Eliminado" };
|
|
59
|
+
}
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
## Open Redirect Prevention
|
|
63
|
+
|
|
64
|
+
Validate all redirect targets — ensure `next`/`redirect` params start with `/` and not `//`. Never redirect to user-controlled URLs without validation.
|
|
65
|
+
|
|
66
|
+
```typescript
|
|
67
|
+
// ✅ Safe redirect validation
|
|
68
|
+
const next = rawNext.startsWith("/") && !rawNext.startsWith("//") ? rawNext : "/";
|
|
69
|
+
|
|
70
|
+
// ❌ Unsafe — allows protocol-relative redirects (//evil.com)
|
|
71
|
+
const next = searchParams.get("next") ?? "/";
|
|
72
|
+
```
|
|
73
|
+
|
|
41
74
|
## Input Validation
|
|
42
75
|
- Validate ALL external input with Zod at the boundary
|
|
43
76
|
- Validate in Server Actions AND API routes
|
|
@@ -48,6 +81,23 @@ export async function POST(request: Request) {
|
|
|
48
81
|
- Apply rate limiting to auth endpoints
|
|
49
82
|
- Use Vercel's built-in rate limiting or `@upstash/ratelimit`
|
|
50
83
|
|
|
84
|
+
## Content Security Policy
|
|
85
|
+
|
|
86
|
+
`script-src` must not include `'unsafe-eval'`. Avoid `'unsafe-inline'` for scripts — prefer nonce-based CSP. `'unsafe-inline'` is acceptable for `style-src` only.
|
|
87
|
+
|
|
88
|
+
```typescript
|
|
89
|
+
// ✅ Ideal — nonce-based CSP (no unsafe-inline for scripts)
|
|
90
|
+
`script-src 'self' 'nonce-${nonce}'`,
|
|
91
|
+
"style-src 'self' 'unsafe-inline'",
|
|
92
|
+
|
|
93
|
+
// ✅ Acceptable intermediate — add a TODO to migrate to nonce-based
|
|
94
|
+
"script-src 'self' 'unsafe-inline'", // TODO: replace with nonce-based CSP
|
|
95
|
+
"style-src 'self' 'unsafe-inline'",
|
|
96
|
+
|
|
97
|
+
// ❌ Never — unsafe-eval is forbidden
|
|
98
|
+
"script-src 'self' 'unsafe-eval' 'unsafe-inline'",
|
|
99
|
+
```
|
|
100
|
+
|
|
51
101
|
## Cookies
|
|
52
102
|
- `httpOnly: true` for auth tokens (Supabase handles this)
|
|
53
103
|
- `secure: true` in production
|
|
@@ -21,10 +21,26 @@ export const selectUserSchema = createSelectSchema(users);
|
|
|
21
21
|
const userSchema = z.object({ id: z.string(), email: z.string() }); // WRONG
|
|
22
22
|
```
|
|
23
23
|
|
|
24
|
-
## Runtime
|
|
24
|
+
## Runtime Reads: Repository Pattern (RLS active)
|
|
25
|
+
|
|
26
|
+
All reads go through the feature's `queries/` module — never call `supabase.from().select()` inline in pages or actions.
|
|
27
|
+
|
|
25
28
|
```typescript
|
|
26
|
-
// ✅
|
|
27
|
-
|
|
29
|
+
// ✅ Read via repository function
|
|
30
|
+
import { getUserById } from "@/features/users/queries/users.queries";
|
|
31
|
+
const { data, error } = await getUserById(supabase, userId);
|
|
32
|
+
|
|
33
|
+
// ✅ Repository module — receives supabase client as parameter (DI)
|
|
34
|
+
// src/features/users/queries/users.queries.ts
|
|
35
|
+
import type { createClient } from "@/shared/lib/supabase/server";
|
|
36
|
+
type Client = Awaited<ReturnType<typeof createClient>>;
|
|
37
|
+
|
|
38
|
+
export async function getUserById(supabase: Client, userId: string) {
|
|
39
|
+
return supabase.from("users").select("*").eq("id", userId).single();
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// ❌ Never inline a SELECT in a page or action
|
|
43
|
+
const { data } = await supabase.from("users").select("*").eq("id", userId); // WRONG in page/action
|
|
28
44
|
|
|
29
45
|
// ❌ Never use Drizzle for runtime queries
|
|
30
46
|
const users = await db.select().from(usersTable); // WRONG — bypasses RLS
|
|
@@ -70,19 +70,28 @@ it("calls signInWithPassword with correct args", () => { ... });
|
|
|
70
70
|
```
|
|
71
71
|
|
|
72
72
|
## Playwright (E2E)
|
|
73
|
-
-
|
|
73
|
+
- **Always use `data-testid` attributes for element selection** — never use visible text or translated labels. Text-based selectors break when copy or i18n changes.
|
|
74
74
|
- Page Object Pattern for complex flows
|
|
75
75
|
- Every user flow needs an e2e test
|
|
76
76
|
|
|
77
|
+
```typescript
|
|
78
|
+
// ✅ Stable — decoupled from i18n
|
|
79
|
+
await page.getByTestId("submit-button").click();
|
|
80
|
+
|
|
81
|
+
// ❌ Brittle — breaks if language changes or copy is updated
|
|
82
|
+
await page.getByRole("button", { name: /sign in/i }).click();
|
|
83
|
+
await page.getByLabel(/email/i).fill("...");
|
|
84
|
+
```
|
|
85
|
+
|
|
77
86
|
```typescript
|
|
78
87
|
test("user can log in", async ({ page }) => {
|
|
79
88
|
// Arrange
|
|
80
89
|
await page.goto("/login");
|
|
81
90
|
|
|
82
91
|
// Act
|
|
83
|
-
await page.
|
|
84
|
-
await page.
|
|
85
|
-
await page.
|
|
92
|
+
await page.getByTestId("email-input").fill("user@example.com");
|
|
93
|
+
await page.getByTestId("password-input").fill("password123");
|
|
94
|
+
await page.getByTestId("sign-in-button").click();
|
|
86
95
|
|
|
87
96
|
// Assert
|
|
88
97
|
await expect(page).toHaveURL("/");
|
|
@@ -8,17 +8,20 @@ description: Scaffold a new feature following TDD and project conventions. Use w
|
|
|
8
8
|
## Process
|
|
9
9
|
|
|
10
10
|
### 1. Requirements First
|
|
11
|
-
- Check `.requirements/` for existing
|
|
12
|
-
- If
|
|
13
|
-
-
|
|
11
|
+
- Check `.requirements/` for an existing functional issue
|
|
12
|
+
- If a spec exists, read it and proceed to Step 2
|
|
13
|
+
- **If no spec exists, call @business-intelligence** to run the discovery process and produce a functional issue in `.requirements/<feature-name>.md`
|
|
14
|
+
- Wait for the functional issue to be written and confirmed by the user before proceeding to Step 2
|
|
14
15
|
|
|
15
16
|
### 2. Create Feature Structure
|
|
16
17
|
```
|
|
17
18
|
src/features/<feature-name>/
|
|
18
19
|
├── components/
|
|
19
20
|
├── actions/
|
|
21
|
+
├── queries/
|
|
20
22
|
├── hooks/
|
|
21
23
|
├── api/
|
|
24
|
+
├── lib/
|
|
22
25
|
└── __tests__/
|
|
23
26
|
```
|
|
24
27
|
|
|
@@ -26,7 +29,8 @@ src/features/<feature-name>/
|
|
|
26
29
|
Write ALL test files first:
|
|
27
30
|
- `__tests__/<component>.test.tsx` — component tests
|
|
28
31
|
- `__tests__/use-<feature>.test.ts` — hook tests
|
|
29
|
-
- `__tests__/<action>.test.ts` — action tests
|
|
32
|
+
- `__tests__/<action>.test.ts` — action tests (see `references/server-action-test-template.md`)
|
|
33
|
+
- `__tests__/<feature>.queries.test.ts` — query tests
|
|
30
34
|
|
|
31
35
|
Run `pnpm test:unit` — all tests must FAIL (RED).
|
|
32
36
|
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# Server Action Testing Pattern
|
|
2
|
+
|
|
3
|
+
Test Server Actions by mocking `createClient`, asserting on the returned `ActionResult`, and verifying side effects.
|
|
4
|
+
|
|
5
|
+
```typescript
|
|
6
|
+
import { vi, it, expect, describe } from "vitest";
|
|
7
|
+
import { myAction } from "@/features/example/actions/my.action";
|
|
8
|
+
|
|
9
|
+
vi.mock("@/shared/lib/supabase/server", () => ({
|
|
10
|
+
createClient: vi.fn(),
|
|
11
|
+
}));
|
|
12
|
+
vi.mock("next/cache", () => ({ revalidatePath: vi.fn() }));
|
|
13
|
+
|
|
14
|
+
describe("myAction", () => {
|
|
15
|
+
it("returns error when user is not authenticated", async () => {
|
|
16
|
+
// Arrange
|
|
17
|
+
const mockSupabase = {
|
|
18
|
+
auth: { getUser: vi.fn().mockResolvedValue({ data: { user: null } }) },
|
|
19
|
+
};
|
|
20
|
+
vi.mocked(createClient).mockResolvedValue(mockSupabase as never);
|
|
21
|
+
const formData = new FormData();
|
|
22
|
+
|
|
23
|
+
// Act
|
|
24
|
+
const result = await myAction(formData);
|
|
25
|
+
|
|
26
|
+
// Assert
|
|
27
|
+
expect(result).toEqual({ status: "error", message: "No autenticado" });
|
|
28
|
+
expect(revalidatePath).not.toHaveBeenCalled();
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it("returns success and revalidates on valid input", async () => {
|
|
32
|
+
// Arrange
|
|
33
|
+
const mockUser = { id: "user-1" };
|
|
34
|
+
const mockSupabase = {
|
|
35
|
+
auth: { getUser: vi.fn().mockResolvedValue({ data: { user: mockUser } }) },
|
|
36
|
+
from: vi.fn().mockReturnThis(),
|
|
37
|
+
insert: vi.fn().mockResolvedValue({ error: null }),
|
|
38
|
+
};
|
|
39
|
+
vi.mocked(createClient).mockResolvedValue(mockSupabase as never);
|
|
40
|
+
const formData = new FormData();
|
|
41
|
+
formData.set("name", "Test Item");
|
|
42
|
+
|
|
43
|
+
// Act
|
|
44
|
+
const result = await myAction(formData);
|
|
45
|
+
|
|
46
|
+
// Assert
|
|
47
|
+
expect(result).toEqual({ status: "success", message: expect.any(String) });
|
|
48
|
+
expect(revalidatePath).toHaveBeenCalledWith("/");
|
|
49
|
+
});
|
|
50
|
+
});
|
|
51
|
+
```
|
|
@@ -17,28 +17,8 @@ Or for a specific branch:
|
|
|
17
17
|
git diff develop...<branch-name>
|
|
18
18
|
```
|
|
19
19
|
|
|
20
|
-
### 2. Check Each Changed File
|
|
21
|
-
|
|
22
|
-
#### Code Quality
|
|
23
|
-
- Zero `any` types
|
|
24
|
-
- Zero comments
|
|
25
|
-
- Functions ≤ 20 lines, files ≤ 200 lines
|
|
26
|
-
- No magic numbers/strings
|
|
27
|
-
|
|
28
|
-
#### Tests
|
|
29
|
-
- Test files exist for every changed source file
|
|
30
|
-
- Tests cover new code paths
|
|
31
|
-
- No implementation-detail testing
|
|
32
|
-
|
|
33
|
-
#### Architecture
|
|
34
|
-
- Correct imports (features → shared only)
|
|
35
|
-
- Server Actions for mutations
|
|
36
|
-
- Edge runtime on AI routes
|
|
37
|
-
|
|
38
|
-
#### Security
|
|
39
|
-
- Input validation at boundaries
|
|
40
|
-
- Auth checks present
|
|
41
|
-
- No exposed secrets
|
|
20
|
+
### 2. Check Each Changed File
|
|
21
|
+
Apply the full checklist from `references/review-checklist.md` — covers code quality, tests, architecture, security, performance, and accessibility.
|
|
42
22
|
|
|
43
23
|
### 3. Generate Report
|
|
44
24
|
```
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# Review Checklist
|
|
2
|
+
|
|
3
|
+
## Code Quality
|
|
4
|
+
- Zero `any` types
|
|
5
|
+
- Zero comments (excluding test AAA labels: `// Arrange`, `// Act`, `// Assert`)
|
|
6
|
+
- Functions ≤ 20 lines
|
|
7
|
+
- Files ≤ 200 lines
|
|
8
|
+
- No magic numbers/strings
|
|
9
|
+
- Proper error handling
|
|
10
|
+
|
|
11
|
+
## Tests
|
|
12
|
+
- Tests written BEFORE implementation (TDD)
|
|
13
|
+
- 100% coverage on new/changed files
|
|
14
|
+
- 100% BRANCH coverage (every if/else/ternary/catch)
|
|
15
|
+
- Behavior tested, not implementation
|
|
16
|
+
- AAA pattern with labeled comments on every test
|
|
17
|
+
- Tests NOT weakened (no removed assertions, no loosened matchers, no .skip)
|
|
18
|
+
- Edge cases covered: null, empty, boundaries, errors, auth expired
|
|
19
|
+
|
|
20
|
+
## Architecture
|
|
21
|
+
- Correct layer (features → shared only)
|
|
22
|
+
- Server Actions for mutations (not TanStack)
|
|
23
|
+
- Edge runtime on AI routes
|
|
24
|
+
|
|
25
|
+
## Security
|
|
26
|
+
- Input validation at boundaries
|
|
27
|
+
- Auth checks in protected routes
|
|
28
|
+
- No exposed secrets
|
|
29
|
+
|
|
30
|
+
## Performance
|
|
31
|
+
- No N+1 query patterns
|
|
32
|
+
- No unnecessary re-renders
|
|
33
|
+
|
|
34
|
+
## Accessibility
|
|
35
|
+
- Semantic HTML
|
|
36
|
+
- ARIA labels where needed
|
|
@@ -8,45 +8,14 @@ disable-model-invocation: true
|
|
|
8
8
|
|
|
9
9
|
## Process
|
|
10
10
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
```bash
|
|
20
|
-
grep -r "sk_" src/ --include="*.ts"
|
|
21
|
-
grep -r "apiKey\s*=" src/ --include="*.ts"
|
|
22
|
-
grep -r "password\s*=" src/ --include="*.ts"
|
|
23
|
-
```
|
|
24
|
-
Check `.env.example` has no real values.
|
|
25
|
-
|
|
26
|
-
### 3. RLS Verification
|
|
27
|
-
For each table in `src/shared/db/schema.ts`:
|
|
28
|
-
- Confirm RLS is enabled in Supabase dashboard
|
|
29
|
-
- Confirm explicit policies exist
|
|
30
|
-
|
|
31
|
-
### 4. Auth Coverage
|
|
32
|
-
- Verify `proxy.ts` protects all non-public routes
|
|
33
|
-
- Check every `route.ts` in protected features has auth check
|
|
34
|
-
- Verify `app/(protected)/layout.tsx` has server-side auth check
|
|
35
|
-
|
|
36
|
-
### 5. Input Validation
|
|
37
|
-
- Every `route.ts` has Zod schema validation
|
|
38
|
-
- Every `.action.ts` has Zod schema validation
|
|
39
|
-
|
|
40
|
-
### 6. XSS Check
|
|
41
|
-
```bash
|
|
42
|
-
grep -r "dangerouslySetInnerHTML" src/ --include="*.tsx"
|
|
43
|
-
```
|
|
44
|
-
|
|
45
|
-
### 7. Security Headers
|
|
46
|
-
Verify in `next.config.ts`:
|
|
47
|
-
- `Content-Security-Policy`
|
|
48
|
-
- `Strict-Transport-Security`
|
|
49
|
-
- `X-Frame-Options`
|
|
11
|
+
Execute all 7 audit steps from `references/audit-steps.md`:
|
|
12
|
+
1. Dependency audit (`pnpm audit`)
|
|
13
|
+
2. Secret scan (hardcoded keys)
|
|
14
|
+
3. RLS verification (all tables)
|
|
15
|
+
4. Auth coverage (routes + actions)
|
|
16
|
+
5. Input validation (Zod at boundaries)
|
|
17
|
+
6. XSS check (`dangerouslySetInnerHTML`)
|
|
18
|
+
7. Security headers (`next.config.ts`)
|
|
50
19
|
|
|
51
20
|
## Output Format
|
|
52
21
|
```
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# Security Audit Steps
|
|
2
|
+
|
|
3
|
+
### 1. Dependency Audit
|
|
4
|
+
```bash
|
|
5
|
+
pnpm audit
|
|
6
|
+
```
|
|
7
|
+
Categorize findings by: Critical / High / Medium / Low
|
|
8
|
+
|
|
9
|
+
### 2. Secret Scan
|
|
10
|
+
Search for hardcoded secrets:
|
|
11
|
+
```bash
|
|
12
|
+
grep -r "sk_" src/ --include="*.ts"
|
|
13
|
+
grep -r "apiKey\s*=" src/ --include="*.ts"
|
|
14
|
+
grep -r "password\s*=" src/ --include="*.ts"
|
|
15
|
+
```
|
|
16
|
+
Check `.env.example` has no real values.
|
|
17
|
+
|
|
18
|
+
### 3. RLS Verification
|
|
19
|
+
For each table in `src/shared/db/schema.ts`:
|
|
20
|
+
- Confirm RLS is enabled in Supabase dashboard
|
|
21
|
+
- Confirm explicit policies exist
|
|
22
|
+
|
|
23
|
+
### 4. Auth Coverage
|
|
24
|
+
- Verify `proxy.ts` protects all non-public routes
|
|
25
|
+
- Check every `route.ts` in protected features has auth check
|
|
26
|
+
- Verify `app/(protected)/layout.tsx` has server-side auth check
|
|
27
|
+
|
|
28
|
+
### 5. Input Validation
|
|
29
|
+
- Every `route.ts` has Zod schema validation
|
|
30
|
+
- Every `.action.ts` has Zod schema validation
|
|
31
|
+
|
|
32
|
+
### 6. XSS Check
|
|
33
|
+
```bash
|
|
34
|
+
grep -r "dangerouslySetInnerHTML" src/ --include="*.tsx"
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
### 7. Security Headers
|
|
38
|
+
Verify in `next.config.ts`:
|
|
39
|
+
- `Content-Security-Policy`
|
|
40
|
+
- `Strict-Transport-Security`
|
|
41
|
+
- `X-Frame-Options`
|
package/template/CLAUDE.md
CHANGED
|
@@ -1,16 +1,33 @@
|
|
|
1
1
|
@AGENTS.md
|
|
2
|
+
@.claude/rules/general.mdc
|
|
3
|
+
@.claude/rules/architecture.mdc
|
|
4
|
+
@.claude/rules/coding-standards.mdc
|
|
2
5
|
|
|
3
|
-
#
|
|
6
|
+
# Context-Specific Rules
|
|
4
7
|
|
|
5
|
-
|
|
8
|
+
Read these rules when working on related files:
|
|
9
|
+
- Data fetching (components, queries, hooks, actions): `.claude/rules/data-fetching.mdc`
|
|
10
|
+
- Security (actions, API routes, auth, config): `.claude/rules/security.mdc`
|
|
11
|
+
- Components (UI, shadcn/ui, Tailwind): `.claude/rules/components.mdc`
|
|
12
|
+
- Forms: `.claude/rules/forms.mdc`
|
|
13
|
+
- Migrations: `.claude/rules/migrations.mdc`
|
|
14
|
+
- Next.js specifics: `.claude/rules/nextjs.mdc`
|
|
15
|
+
- Supabase (Drizzle, RLS, repository pattern): `.claude/rules/supabase.mdc`
|
|
16
|
+
- Testing: `.claude/rules/testing.mdc`
|
|
6
17
|
|
|
7
|
-
|
|
8
|
-
- **supabase-js** = all runtime queries. RLS is always active.
|
|
9
|
-
- **Zod schemas** for DB types: auto-generate via `drizzle-zod`, never write manually.
|
|
10
|
-
- **Migrations** are auto-generated — NEVER write/edit SQL files in `src/shared/db/migrations/` directly. Use `pnpm db:generate` + `pnpm db:migrate`.
|
|
18
|
+
# Agent Workflow
|
|
11
19
|
|
|
12
|
-
|
|
20
|
+
This project uses specialist agents in `.claude/agents/`. Follow the technical-lead workflow:
|
|
13
21
|
|
|
14
|
-
-
|
|
15
|
-
-
|
|
16
|
-
-
|
|
22
|
+
- **New feature**: requirements (`business-intelligence`) → task breakdown (`technical-lead`) → tests first (`test-qa`) → implementation (`backend`/`frontend`) → review (`code-reviewer`) → security (`security-researcher`)
|
|
23
|
+
- **Bug fix**: reproduce with failing test (`test-qa`) → fix (`backend`/`frontend`) → review (`code-reviewer`)
|
|
24
|
+
- **Refactor**: plan (`technical-lead`) → implement (`backend`/`frontend`) → verify (`test-qa`) → review (`code-reviewer`)
|
|
25
|
+
|
|
26
|
+
# File Naming
|
|
27
|
+
|
|
28
|
+
- Server Actions: `name.action.ts` with `"use server"` at top
|
|
29
|
+
- React hooks: `use-name.ts`
|
|
30
|
+
- Components: PascalCase `.tsx`
|
|
31
|
+
- Tests: colocated in `__tests__/`, named `name.test.ts`
|
|
32
|
+
|
|
33
|
+
# Maintenance Notes
|
|
@@ -65,6 +65,12 @@ export default tseslint.config(
|
|
|
65
65
|
rules: { "no-console": "off" },
|
|
66
66
|
},
|
|
67
67
|
{
|
|
68
|
-
|
|
68
|
+
// TODO: @supabase/ssr exports createServerClient as deprecated while the replacement
|
|
69
|
+
// API is not yet stable. Remove this override once the package provides a non-deprecated alternative.
|
|
70
|
+
files: ["src/shared/lib/supabase/server.ts", "src/shared/lib/supabase/middleware.ts"],
|
|
71
|
+
rules: { "@typescript-eslint/no-deprecated": "off" },
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
ignores: [".next/**", "node_modules/**", "dist/**", "coverage/**", "eslint.config.ts", "next-env.d.ts", "next.config.ts", "playwright.config.ts", "drizzle.config.ts", "vitest.config.ts", "postcss.config.mjs"],
|
|
69
75
|
}
|
|
70
76
|
);
|
package/template/next.config.ts
CHANGED
|
@@ -20,7 +20,8 @@ const nextConfig: NextConfig = {
|
|
|
20
20
|
key: "Content-Security-Policy",
|
|
21
21
|
value: [
|
|
22
22
|
"default-src 'self'",
|
|
23
|
-
|
|
23
|
+
// TODO: replace 'unsafe-inline' with nonce-based CSP
|
|
24
|
+
"script-src 'self' 'unsafe-inline'",
|
|
24
25
|
"style-src 'self' 'unsafe-inline'",
|
|
25
26
|
"img-src 'self' data: blob: https:",
|
|
26
27
|
"font-src 'self'",
|
|
@@ -1,11 +1,9 @@
|
|
|
1
|
-
import { eq } from "drizzle-orm";
|
|
2
1
|
import { redirect } from "next/navigation";
|
|
3
2
|
|
|
4
3
|
import { logoutAction } from "@/features/auth/actions/logout.action";
|
|
5
4
|
import { AddTodoForm } from "@/features/todos/components/add-todo-form";
|
|
6
5
|
import { TodoList } from "@/features/todos/components/todo-list";
|
|
7
|
-
import {
|
|
8
|
-
import { todos } from "@/shared/db/schema";
|
|
6
|
+
import { getTodosByUserId } from "@/features/todos/queries/todos.queries";
|
|
9
7
|
import { createClient } from "@/shared/lib/supabase/server";
|
|
10
8
|
|
|
11
9
|
export default async function HomePage() {
|
|
@@ -14,7 +12,7 @@ export default async function HomePage() {
|
|
|
14
12
|
data: { user },
|
|
15
13
|
} = await supabase.auth.getUser();
|
|
16
14
|
if (!user) redirect("/login");
|
|
17
|
-
const items = await
|
|
15
|
+
const { data: items } = await getTodosByUserId(supabase, user.id);
|
|
18
16
|
|
|
19
17
|
return (
|
|
20
18
|
<main className="container mx-auto max-w-2xl p-8">
|
|
@@ -69,4 +69,18 @@ describe("auth callback route", () => {
|
|
|
69
69
|
expect(response.status).toBe(307);
|
|
70
70
|
expect(response.headers.get("location")).toContain("/dashboard");
|
|
71
71
|
});
|
|
72
|
+
|
|
73
|
+
it("ignores protocol-relative next param to prevent open redirect", async () => {
|
|
74
|
+
// Arrange
|
|
75
|
+
mockExchangeCodeForSession.mockResolvedValue({ error: null });
|
|
76
|
+
const { GET } = await import("../api/auth/callback/route");
|
|
77
|
+
const req = new Request("http://localhost/api/auth/callback?code=valid-code&next=//evil.com");
|
|
78
|
+
|
|
79
|
+
// Act
|
|
80
|
+
const response = await GET(req);
|
|
81
|
+
|
|
82
|
+
// Assert
|
|
83
|
+
expect(response.status).toBe(307);
|
|
84
|
+
expect(response.headers.get("location")).toBe("http://localhost/");
|
|
85
|
+
});
|
|
72
86
|
});
|
|
@@ -1,27 +1,18 @@
|
|
|
1
1
|
import { describe, it, expect, vi, beforeEach } from "vitest";
|
|
2
2
|
|
|
3
3
|
const mockGetUser = vi.fn();
|
|
4
|
-
const
|
|
4
|
+
const mockFrom = vi.fn();
|
|
5
5
|
const mockRedirect = vi.fn();
|
|
6
6
|
|
|
7
7
|
vi.mock("@/shared/lib/supabase/server", () => ({
|
|
8
8
|
createClient: vi.fn(() =>
|
|
9
9
|
Promise.resolve({
|
|
10
10
|
auth: { getUser: mockGetUser },
|
|
11
|
+
from: mockFrom,
|
|
11
12
|
})
|
|
12
13
|
),
|
|
13
14
|
}));
|
|
14
15
|
|
|
15
|
-
vi.mock("@/shared/db", () => ({
|
|
16
|
-
db: {
|
|
17
|
-
select: vi.fn(() => ({
|
|
18
|
-
from: vi.fn(() => ({
|
|
19
|
-
where: mockSelect,
|
|
20
|
-
})),
|
|
21
|
-
})),
|
|
22
|
-
},
|
|
23
|
-
}));
|
|
24
|
-
|
|
25
16
|
vi.mock("@/features/todos/components/todo-list", () => ({
|
|
26
17
|
TodoList: () => <div data-testid="todo-list" />,
|
|
27
18
|
}));
|
|
@@ -34,6 +25,13 @@ vi.mock("next/navigation", () => ({
|
|
|
34
25
|
redirect: (url: string): unknown => mockRedirect(url),
|
|
35
26
|
}));
|
|
36
27
|
|
|
28
|
+
function makeTodosChain(data: unknown[]) {
|
|
29
|
+
return {
|
|
30
|
+
select: vi.fn().mockReturnThis(),
|
|
31
|
+
eq: vi.fn(() => Promise.resolve({ data, error: null })),
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
|
|
37
35
|
describe("HomePage (protected)", () => {
|
|
38
36
|
beforeEach(() => {
|
|
39
37
|
vi.clearAllMocks();
|
|
@@ -53,7 +51,7 @@ describe("HomePage (protected)", () => {
|
|
|
53
51
|
it("renderiza la página con las tareas del usuario", async () => {
|
|
54
52
|
// Arrange
|
|
55
53
|
mockGetUser.mockResolvedValue({ data: { user: { id: "u1", email: "user@example.com" } } });
|
|
56
|
-
|
|
54
|
+
mockFrom.mockReturnValue(makeTodosChain([]));
|
|
57
55
|
const { default: HomePage } = await import("../(protected)/page");
|
|
58
56
|
|
|
59
57
|
// Act
|
|
@@ -66,7 +64,7 @@ describe("HomePage (protected)", () => {
|
|
|
66
64
|
it("renderiza correctamente con lista de tareas vacía", async () => {
|
|
67
65
|
// Arrange
|
|
68
66
|
mockGetUser.mockResolvedValue({ data: { user: { id: "u1", email: "user@example.com" } } });
|
|
69
|
-
|
|
67
|
+
mockFrom.mockReturnValue(makeTodosChain([]));
|
|
70
68
|
const { default: HomePage } = await import("../(protected)/page");
|
|
71
69
|
|
|
72
70
|
// Act
|
|
@@ -5,7 +5,8 @@ import { createClient } from "@/shared/lib/supabase/server";
|
|
|
5
5
|
export async function GET(request: Request) {
|
|
6
6
|
const { searchParams, origin } = new URL(request.url);
|
|
7
7
|
const code = searchParams.get("code");
|
|
8
|
-
const
|
|
8
|
+
const rawNext = searchParams.get("next") ?? "/";
|
|
9
|
+
const next = rawNext.startsWith("/") && !rawNext.startsWith("//") ? rawNext : "/";
|
|
9
10
|
|
|
10
11
|
if (!code) {
|
|
11
12
|
return NextResponse.redirect(`${origin}/login?error=no_code`);
|
|
@@ -9,13 +9,13 @@ test.describe("Login flow", () => {
|
|
|
9
9
|
test("shows login form", async ({ page }) => {
|
|
10
10
|
await page.goto("/login");
|
|
11
11
|
await expect(page.getByTestId("login-form")).toBeVisible();
|
|
12
|
-
await expect(page.getByLabel(/
|
|
13
|
-
await expect(page.getByLabel(/
|
|
12
|
+
await expect(page.getByLabel(/correo electrónico/i)).toBeVisible();
|
|
13
|
+
await expect(page.getByLabel(/contraseña/i)).toBeVisible();
|
|
14
14
|
});
|
|
15
15
|
|
|
16
16
|
test("shows validation errors on empty submit", async ({ page }) => {
|
|
17
17
|
await page.goto("/login");
|
|
18
|
-
await page.getByRole("button", { name: /
|
|
19
|
-
await expect(page.getByText(/
|
|
18
|
+
await page.getByRole("button", { name: /iniciar sesión/i }).click();
|
|
19
|
+
await expect(page.getByText(/correo electrónico inválido/i)).toBeVisible();
|
|
20
20
|
});
|
|
21
21
|
});
|