From a6fc697e4d11f865680ce559d26831a099987edc Mon Sep 17 00:00:00 2001 From: Matt Date: Fri, 5 Jun 2026 12:18:10 +0200 Subject: [PATCH] fix(auth): allow /lunch/pick public access for accountless external attendees MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The external dish-picker page is reached via a signed token by attendees who have no account. The middleware authorized() callback redirected any non allowlisted path to /login, which is a dead end for accountless users — so the picker shipped in 8d4f0ba was unreachable in prod (307 → /login). Add /lunch/pick to publicPaths; data stays gated by token verification in tRPC. Adds a regression test asserting the path is public and a protected path is not. Co-Authored-By: Claude Opus 4.8 (1M context) --- src/lib/auth.config.ts | 1 + tests/unit/auth-public-paths.test.ts | 25 +++++++++++++++++++++++++ 2 files changed, 26 insertions(+) create mode 100644 tests/unit/auth-public-paths.test.ts diff --git a/src/lib/auth.config.ts b/src/lib/auth.config.ts index 2cf8d49..26924cd 100644 --- a/src/lib/auth.config.ts +++ b/src/lib/auth.config.ts @@ -58,6 +58,7 @@ export const authConfig: NextAuthConfig = { '/forgot-password', '/reset-password', '/apply', + '/lunch/pick', // external attendees pick a dish via signed token (no account) '/api/auth', '/api/trpc', // tRPC handles its own auth via procedures ] diff --git a/tests/unit/auth-public-paths.test.ts b/tests/unit/auth-public-paths.test.ts new file mode 100644 index 0000000..195140f --- /dev/null +++ b/tests/unit/auth-public-paths.test.ts @@ -0,0 +1,25 @@ +/** + * The external lunch dish-pick page is reached by attendees with NO account, via + * a signed token link. It MUST be in the middleware public-path allowlist, or the + * auth middleware redirects them to /login (a dead end for accountless users). + */ +import { describe, it, expect } from 'vitest' +import { authConfig } from '@/lib/auth.config' + +function authorized(pathname: string, auth: unknown) { + const fn = authConfig.callbacks!.authorized! + return fn({ + auth: auth as never, + request: { nextUrl: new URL(`http://localhost${pathname}`) } as never, + } as never) +} + +describe('middleware public paths', () => { + it('allows the external lunch pick page without a session', () => { + expect(authorized('/lunch/pick/some.signed.token', null)).toBe(true) + }) + + it('blocks a protected page without a session', () => { + expect(authorized('/admin/logistics', null)).toBe(false) + }) +})