From c334d477a2c59e6683e9fbbc88526b94ec6ff106 Mon Sep 17 00:00:00 2001 From: hedun Date: Fri, 26 Jun 2026 10:22:28 +0100 Subject: [PATCH 1/6] fix: remove legacy auth route and jwt secret fallback --- .github/workflows/test.yml | 9 ++++ app/app/(auth)/logout/route.ts | 7 +-- app/app/(auth)/session/route.ts | 80 --------------------------------- app/lib/jwt.ts | 20 +++------ app/tests/auth.test.ts | 9 ++-- app/tests/jwt-config.test.ts | 25 +++++++++++ 6 files changed, 45 insertions(+), 105 deletions(-) delete mode 100644 app/app/(auth)/session/route.ts create mode 100644 app/tests/jwt-config.test.ts diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 3048941..79e4565 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -58,10 +58,19 @@ jobs: DATABASE_URL: postgresql://postgres:postgres@localhost:5432/geev_test run: npx prisma migrate deploy + - name: Check production config secrets + run: | + if [ -z "${{ secrets.NEXTAUTH_SECRET }}" ]; then + echo "Error: NEXTAUTH_SECRET is missing from repository secrets." + echo "It must be present for production deployments." + exit 1 + fi + - name: Run tests env: DATABASE_URL: postgresql://postgres:postgres@localhost:5432/geev_test JWT_SECRET: test-secret-key + NEXTAUTH_SECRET: test-secret-key NODE_ENV: test run: npm run test:ci diff --git a/app/app/(auth)/logout/route.ts b/app/app/(auth)/logout/route.ts index 3e37abb..520f2d3 100644 --- a/app/app/(auth)/logout/route.ts +++ b/app/app/(auth)/logout/route.ts @@ -37,13 +37,10 @@ export async function POST(request: Request) { expires: new Date(0), // Set to epoch to ensure expiration }; - // 1. Clear the legacy auth-token - response.cookies.set("auth-token", "", cookieOptions); - - // 2. Clear Auth.js session token (standard) + // 1. Clear Auth.js session token (standard) response.cookies.set("next-auth.session-token", "", cookieOptions); - // 3. Clear Auth.js session token (secure version used in production/HTTPS) + // 2. Clear Auth.js session token (secure version used in production/HTTPS) response.cookies.set("__Secure-next-auth.session-token", "", cookieOptions); return response; diff --git a/app/app/(auth)/session/route.ts b/app/app/(auth)/session/route.ts deleted file mode 100644 index 4400e57..0000000 --- a/app/app/(auth)/session/route.ts +++ /dev/null @@ -1,80 +0,0 @@ -import { getTokenFromRequest, verifyToken } from "@/lib/jwt"; - -import { NextResponse } from "next/server"; -import { prisma } from "@/lib/prisma"; - -/** - * Checks for an active session. - * - * @deprecated This endpoint is legacy. For new implementations, use Auth.js `auth()` - * or `useSession()` instead. - * - * @param request - The incoming Request object - * @returns A NextResponse with session data - */ -export async function GET(request: Request) { - try { - // Get token from cookies - const token = getTokenFromRequest(request); - - if (!token) { - return NextResponse.json( - { error: "No authentication token found" }, - { status: 401 }, - ); - } - - // Verify and decode token - const payload = await verifyToken(token); - console.log("Session payload:", payload); - // Find user in database - const user = await prisma.user.findUnique({ - where: { id: payload.userId }, - }); - - if (!user) { - return NextResponse.json({ error: "User not found" }, { status: 404 }); - } - - // Return session data - return NextResponse.json( - { - success: true, - user: { - id: user.id, - walletAddress: user.walletAddress, - username: user.name, - email: null, - avatar: user.avatarUrl, - bio: user.bio, - joinDate: user.createdAt, - }, - token: { - expiresAt: payload.exp - ? new Date(payload.exp * 1000).toISOString() - : null, - }, - }, - { - headers: { - // RFC 299: Miscellaneous persistent warning - Warning: - '299 - "Deprecated: This endpoint is legacy. Use Auth.js session instead."', - }, - }, - ); - } catch (error: any) { - if (error.message === "Invalid token") { - return NextResponse.json( - { error: "Invalid or expired token" }, - { status: 401 }, - ); - } - - console.error("Session check error:", error); - return NextResponse.json( - { error: "Internal server error" }, - { status: 500 }, - ); - } -} diff --git a/app/lib/jwt.ts b/app/lib/jwt.ts index e236584..d8bdf03 100644 --- a/app/lib/jwt.ts +++ b/app/lib/jwt.ts @@ -1,8 +1,10 @@ import { SignJWT, jwtVerify } from "jose"; -const secret = new TextEncoder().encode( - process.env.NEXTAUTH_SECRET || "your-secret-key-change-in-production", -); +if (!process.env.NEXTAUTH_SECRET) { + throw new Error("NEXTAUTH_SECRET is missing. It must be set in production config."); +} + +const secret = new TextEncoder().encode(process.env.NEXTAUTH_SECRET); export interface JWTPayload { userId: string; @@ -47,17 +49,5 @@ export function getTokenFromRequest(request: Request): string | null { return authHeader.slice(7); } - // Try to get from cookies - const cookieHeader = request.headers.get("cookie"); - if (cookieHeader) { - const cookies = cookieHeader.split(";"); - for (const cookie of cookies) { - const [name, value] = cookie.trim().split("="); - if (name === "token" || name === "auth-token") { - return value; - } - } - } - return null; } diff --git a/app/tests/auth.test.ts b/app/tests/auth.test.ts index b9b320f..ac65e32 100644 --- a/app/tests/auth.test.ts +++ b/app/tests/auth.test.ts @@ -1,4 +1,7 @@ -import { test, expect, describe } from "vitest"; +import { test, expect, describe, vi } from "vitest"; + +vi.stubEnv("NEXTAUTH_SECRET", "test-secret-key"); + import { createToken, verifyToken } from "@/lib/jwt"; describe("Authentication System", () => { @@ -58,13 +61,9 @@ describe("Authentication System", () => { const response = await POST(request); const cookies = response.cookies.getAll(); - expect(cookies.some(c => c.name === "auth-token")).toBe(true); expect(cookies.some(c => c.name === "next-auth.session-token")).toBe(true); expect(response.status).toBe(200); }); - test.skip("GET /api/auth/session should return 401 without token", async () => { - // Skipped - }); }); describe("Authentication Middleware", () => { diff --git a/app/tests/jwt-config.test.ts b/app/tests/jwt-config.test.ts new file mode 100644 index 0000000..63dbdef --- /dev/null +++ b/app/tests/jwt-config.test.ts @@ -0,0 +1,25 @@ +import { test, expect, describe, vi, beforeEach } from "vitest"; + +describe("JWT Configuration Fail-Fast", () => { + beforeEach(() => { + vi.resetModules(); // Ensure we get a fresh import of jwt.ts each time + }); + + test("should throw an Error on import if NEXTAUTH_SECRET is missing", async () => { + // Explicitly unset the secret to simulate missing production config + vi.stubEnv("NEXTAUTH_SECRET", ""); + + await expect(async () => { + await import("@/lib/jwt"); + }).rejects.toThrow("NEXTAUTH_SECRET is missing. It must be set in production config."); + }); + + test("should successfully import if NEXTAUTH_SECRET is provided", async () => { + // Provide a valid secret + vi.stubEnv("NEXTAUTH_SECRET", "valid-secret-for-test"); + + const jwt = await import("@/lib/jwt"); + expect(jwt.createToken).toBeDefined(); + expect(jwt.verifyToken).toBeDefined(); + }); +}); From 314f51410cff5fda99f2e44c44a398fa2b75953c Mon Sep 17 00:00:00 2001 From: hedun Date: Fri, 26 Jun 2026 10:23:40 +0100 Subject: [PATCH 2/6] fix: resolve typescript narrowing error for NEXTAUTH_SECRET --- app/lib/jwt.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/app/lib/jwt.ts b/app/lib/jwt.ts index d8bdf03..360b07b 100644 --- a/app/lib/jwt.ts +++ b/app/lib/jwt.ts @@ -1,10 +1,11 @@ import { SignJWT, jwtVerify } from "jose"; -if (!process.env.NEXTAUTH_SECRET) { +const secretValue = process.env.NEXTAUTH_SECRET; +if (!secretValue) { throw new Error("NEXTAUTH_SECRET is missing. It must be set in production config."); } -const secret = new TextEncoder().encode(process.env.NEXTAUTH_SECRET); +const secret = new TextEncoder().encode(secretValue); export interface JWTPayload { userId: string; From 80fd954032dff53eb4ac8044b4b91984fdc25352 Mon Sep 17 00:00:00 2001 From: hedun Date: Tue, 30 Jun 2026 11:10:38 +0100 Subject: [PATCH 3/6] test: remove legacy auth-token assertion from logout tests --- app/tests/api/auth/logout.test.ts | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/app/tests/api/auth/logout.test.ts b/app/tests/api/auth/logout.test.ts index d2f882e..22a82b5 100644 --- a/app/tests/api/auth/logout.test.ts +++ b/app/tests/api/auth/logout.test.ts @@ -3,7 +3,7 @@ import { POST } from "@/app/(auth)/logout/route"; import { NextRequest } from "next/server"; describe("Logout API Route", () => { - it("should clear the legacy auth-token and Auth.js session cookies", async () => { + it("should clear Auth.js session cookies", async () => { const request = new NextRequest("http://localhost/api/auth/logout", { method: "POST", }); @@ -11,12 +11,6 @@ describe("Logout API Route", () => { const response = await POST(request); const cookies = response.cookies.getAll(); - // Check for legacy auth-token - const authTokenCookie = cookies.find((c) => c.name === "auth-token"); - expect(authTokenCookie).toBeDefined(); - expect(authTokenCookie?.value).toBe(""); - expect(authTokenCookie?.maxAge).toBe(0); - // Check for Auth.js cookies const sessionTokenCookie = cookies.find( (c) => c.name === "next-auth.session-token" @@ -60,7 +54,6 @@ describe("Logout API Route", () => { const cookies = response.cookies.getAll(); // The response should still contain the "clear" instructions (Max-Age=0) - expect(cookies.some((c) => c.name === "auth-token")).toBe(true); expect(cookies.some((c) => c.name === "next-auth.session-token")).toBe(true); expect(cookies.some((c) => c.name === "__Secure-next-auth.session-token")).toBe(true); }); From 8ecff298f4fb7b08c33d4cdda26eaf329196f86c Mon Sep 17 00:00:00 2001 From: hedun Date: Wed, 1 Jul 2026 11:01:43 +0100 Subject: [PATCH 4/6] fix: resolve Next.js CI typescript compilation errors --- app/app/api/indexer/route.ts | 4 ++-- app/app/api/wallet/withdraw/route.ts | 2 +- app/tests/api/entries.test.ts | 8 ++++---- app/tests/api/indexer.test.ts | 8 ++------ app/tests/api/post-slug-generation.test.ts | 17 +++++++++-------- 5 files changed, 18 insertions(+), 21 deletions(-) diff --git a/app/app/api/indexer/route.ts b/app/app/api/indexer/route.ts index b6bfbdc..1603ba4 100644 --- a/app/app/api/indexer/route.ts +++ b/app/app/api/indexer/route.ts @@ -14,7 +14,7 @@ import { apiError } from "@/lib/api-response"; /** * POST handler - Trigger indexer run */ -export async function POST(request: NextRequest): Promise { +export async function POST(request: NextRequest): Promise { try { const admin = await getCurrentAdmin(); if (!admin) return apiError("Forbidden", 403); @@ -51,7 +51,7 @@ export async function POST(request: NextRequest): Promise { /** * GET handler - Get indexer statistics */ -export async function GET(): Promise { +export async function GET(): Promise { try { const admin = await getCurrentAdmin(); if (!admin) return apiError("Forbidden", 403); diff --git a/app/app/api/wallet/withdraw/route.ts b/app/app/api/wallet/withdraw/route.ts index 4b20bef..1ef9292 100644 --- a/app/app/api/wallet/withdraw/route.ts +++ b/app/app/api/wallet/withdraw/route.ts @@ -222,7 +222,7 @@ export async function POST(request: NextRequest) { originalCurrency: "USD", convertedAmount: result.transaction.convertedAmount, convertedCurrency: "XLM", - rate: (result.transaction.convertedAmount / amount).toFixed(7), + rate: ((result.transaction.convertedAmount ?? amount) / amount).toFixed(7), }, }, "Withdrawal initiated successfully", diff --git a/app/tests/api/entries.test.ts b/app/tests/api/entries.test.ts index 3426a2d..f68656f 100644 --- a/app/tests/api/entries.test.ts +++ b/app/tests/api/entries.test.ts @@ -109,15 +109,15 @@ describe('Entry API Endpoints', () => { prisma.entry.count = vi.fn().mockResolvedValue(0); prisma.entry.updateMany = vi.fn().mockResolvedValue({ count: 0 }); - prisma.postWinner = { + (prisma as any).postWinner = { count: vi.fn().mockResolvedValue(0), createMany: vi.fn().mockResolvedValue({ count: 0 }), - } as any; + }; prisma.post.update = vi.fn().mockResolvedValue(post); - prisma.notification = { + (prisma as any).notification = { create: vi.fn().mockResolvedValue({}), createMany: vi.fn().mockResolvedValue({ count: 0 }), - } as any; + }; }); describe('POST /api/posts/[id]/entries', () => { diff --git a/app/tests/api/indexer.test.ts b/app/tests/api/indexer.test.ts index c125749..55022c6 100644 --- a/app/tests/api/indexer.test.ts +++ b/app/tests/api/indexer.test.ts @@ -94,9 +94,7 @@ describe("GET /api/indexer/stats", () => { it("returns 403 for unauthenticated callers", async () => { mockGetCurrentAdmin.mockResolvedValue(null); - const response = await GET( - createMockRequest("http://localhost:3000/api/indexer/stats"), - ); + const response = await GET(); const { status, data } = await parseResponse(response); expect(status).toBe(403); @@ -114,9 +112,7 @@ describe("GET /api/indexer/stats", () => { status: "synced", }); - const response = await GET( - createMockRequest("http://localhost:3000/api/indexer/stats"), - ); + const response = await GET(); const { status, data } = await parseResponse(response); expect(status).toBe(200); diff --git a/app/tests/api/post-slug-generation.test.ts b/app/tests/api/post-slug-generation.test.ts index 58d5b00..18631f7 100644 --- a/app/tests/api/post-slug-generation.test.ts +++ b/app/tests/api/post-slug-generation.test.ts @@ -1,4 +1,5 @@ import { beforeEach, describe, expect, it, vi } from 'vitest'; +import { NextRequest } from 'next/server'; const mockPrisma = vi.hoisted(() => ({ $transaction: vi.fn(), @@ -80,13 +81,13 @@ describe('POST /api/posts slug generation', () => { xp: 50, rankId: 'newcomer', }); - prisma.$transaction.mockImplementation(async (callback: any) => callback(prisma)); - prisma.post.findUnique.mockResolvedValue(null); - prisma.postRequirements.create.mockResolvedValue({ + (prisma.$transaction as any).mockImplementation(async (callback: any) => callback(prisma)); + (prisma.post.findUnique as any).mockResolvedValue(null); + (prisma.postRequirements.create as any).mockResolvedValue({ id: 'requirements_123', proofRequired: false, }); - prisma.post.create.mockImplementation((args: any) => Promise.resolve({ + (prisma.post.create as any).mockImplementation((args: any) => Promise.resolve({ id: 'post_123', ...args.data, createdAt: new Date(), @@ -105,11 +106,11 @@ describe('POST /api/posts slug generation', () => { endsAt: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000).toISOString(), }; - prisma.post.findUnique + (prisma.post.findUnique as any) .mockResolvedValueOnce({ id: 'existing_post' }) .mockResolvedValueOnce(null); - const request = createMockRequest('http://localhost:3000/api/posts', postData); + const request = createMockRequest('http://localhost:3000/api/posts', postData) as unknown as NextRequest; const response = await POST(request); const { status, data } = await parseResponse(response); @@ -133,9 +134,9 @@ describe('POST /api/posts slug generation', () => { endsAt: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000).toISOString(), }; - prisma.post.create.mockRejectedValue({ code: 'P2002' }); + (prisma.post.create as any).mockRejectedValue({ code: 'P2002' }); - const request = createMockRequest('http://localhost:3000/api/posts', postData); + const request = createMockRequest('http://localhost:3000/api/posts', postData) as unknown as NextRequest; const response = await POST(request); const { status, data } = await parseResponse(response); From 5ff1aee5f02f962cc38e53efe171c5b9a4b4a135 Mon Sep 17 00:00:00 2001 From: hedun Date: Wed, 1 Jul 2026 11:12:05 +0100 Subject: [PATCH 5/6] fix: skip production secret check on pull request in CI to prevent false failures --- .github/workflows/test.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 79e4565..c86a2a0 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -59,6 +59,7 @@ jobs: run: npx prisma migrate deploy - name: Check production config secrets + if: github.event_name != 'pull_request' run: | if [ -z "${{ secrets.NEXTAUTH_SECRET }}" ]; then echo "Error: NEXTAUTH_SECRET is missing from repository secrets." From c8a602d08c9001eaae3aa0912a9dd2748d341fce Mon Sep 17 00:00:00 2001 From: hedun Date: Thu, 2 Jul 2026 10:54:18 +0100 Subject: [PATCH 6/6] fix: remove check for repository secret NEXTAUTH_SECRET in test workflow --- .github/workflows/test.yml | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index c86a2a0..dc61611 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -58,14 +58,7 @@ jobs: DATABASE_URL: postgresql://postgres:postgres@localhost:5432/geev_test run: npx prisma migrate deploy - - name: Check production config secrets - if: github.event_name != 'pull_request' - run: | - if [ -z "${{ secrets.NEXTAUTH_SECRET }}" ]; then - echo "Error: NEXTAUTH_SECRET is missing from repository secrets." - echo "It must be present for production deployments." - exit 1 - fi + - name: Run tests env: