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.
Files changed (39) hide show
  1. package/dist/index.js +6 -3
  2. package/package.json +1 -1
  3. package/template/.cursor/agents/backend.md +10 -59
  4. package/template/.cursor/agents/business-intelligence.md +37 -15
  5. package/template/.cursor/agents/code-reviewer.md +2 -2
  6. package/template/.cursor/agents/frontend.md +6 -10
  7. package/template/.cursor/agents/security-researcher.md +8 -1
  8. package/template/.cursor/agents/technical-lead.md +47 -18
  9. package/template/.cursor/agents/test-qa.md +9 -49
  10. package/template/.cursor/rules/architecture.mdc +2 -0
  11. package/template/.cursor/rules/coding-standards.mdc +12 -1
  12. package/template/.cursor/rules/components.mdc +7 -0
  13. package/template/.cursor/rules/data-fetching.mdc +56 -5
  14. package/template/.cursor/rules/forms.mdc +4 -0
  15. package/template/.cursor/rules/general.mdc +5 -3
  16. package/template/.cursor/rules/nextjs.mdc +27 -3
  17. package/template/.cursor/rules/security.mdc +53 -3
  18. package/template/.cursor/rules/supabase.mdc +19 -3
  19. package/template/.cursor/rules/testing.mdc +13 -4
  20. package/template/.cursor/skills/create-feature/SKILL.md +8 -4
  21. package/template/.cursor/skills/create-feature/references/server-action-test-template.md +51 -0
  22. package/template/.cursor/skills/review-branch/SKILL.md +2 -22
  23. package/template/.cursor/skills/review-branch/references/review-checklist.md +36 -0
  24. package/template/.cursor/skills/security-audit/SKILL.md +8 -39
  25. package/template/.cursor/skills/security-audit/references/audit-steps.md +41 -0
  26. package/template/CLAUDE.md +27 -10
  27. package/template/eslint.config.ts +7 -1
  28. package/template/next.config.ts +2 -1
  29. package/template/src/app/(protected)/page.tsx +2 -4
  30. package/template/src/app/__tests__/auth-callback.test.ts +14 -0
  31. package/template/src/app/__tests__/protected-page.test.tsx +11 -13
  32. package/template/src/app/api/auth/callback/route.ts +2 -1
  33. package/template/src/e2e/login.spec.ts +4 -4
  34. package/template/src/features/todos/__tests__/todos.action.test.ts +78 -44
  35. package/template/src/features/todos/__tests__/todos.queries.test.ts +101 -0
  36. package/template/src/features/todos/actions/todos.action.ts +42 -28
  37. package/template/src/features/todos/queries/todos.queries.ts +16 -0
  38. package/template/src/shared/lib/supabase/middleware.ts +0 -1
  39. 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 and OWASP guidelines. Always applies.
3
- alwaysApply: true
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 Queries: supabase-js (RLS active)
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
- // ✅ Runtime queries via supabase-js
27
- const { data, error } = await supabase.from("users").select("*").eq("id", userId);
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
- - Use `data-testid` attributes for stable selectors
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.getByLabel(/email/i).fill("user@example.com");
84
- await page.getByLabel(/password/i).fill("password123");
85
- await page.getByRole("button", { name: /sign in/i }).click();
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 spec
12
- - If none exists, ask the user to describe the feature
13
- - Document in `.requirements/<feature-name>.md` using the Given/When/Then template
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 Against:
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
- ### 1. Dependency Audit
12
- ```bash
13
- pnpm audit
14
- ```
15
- Categorize findings by: Critical / High / Medium / Low
16
-
17
- ### 2. Secret Scan
18
- Search for hardcoded secrets:
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`
@@ -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
- # Architecture
6
+ # Context-Specific Rules
4
7
 
5
- Feature-based structure: `src/features/* src/shared/*` (never reverse).
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
- - **Drizzle** = schema + migrations ONLY. Never use Drizzle for runtime queries.
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
- # Testing
20
+ This project uses specialist agents in `.claude/agents/`. Follow the technical-lead workflow:
13
21
 
14
- - 100% coverage required `pnpm test:coverage` must pass
15
- - AAA pattern (Arrange / Act / Assert) in every test
16
- - Mock only external boundaries (Supabase, HTTP, DB) never mock internal code
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
- ignores: [".next/**", "node_modules/**", "dist/**", "eslint.config.ts", "next-env.d.ts", "next.config.ts", "playwright.config.ts", "drizzle.config.ts", "vitest.config.ts", "postcss.config.mjs"],
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
  );
@@ -20,7 +20,8 @@ const nextConfig: NextConfig = {
20
20
  key: "Content-Security-Policy",
21
21
  value: [
22
22
  "default-src 'self'",
23
- "script-src 'self' 'unsafe-eval' 'unsafe-inline'",
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 { db } from "@/shared/db";
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 db.select().from(todos).where(eq(todos.userId, user.id));
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 mockSelect = vi.fn();
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
- mockSelect.mockResolvedValue([]);
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
- mockSelect.mockResolvedValue([]);
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 next = searchParams.get("next") ?? "/";
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(/email/i)).toBeVisible();
13
- await expect(page.getByLabel(/password/i)).toBeVisible();
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: /sign in/i }).click();
19
- await expect(page.getByText(/invalid email/i)).toBeVisible();
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
  });