diff --git a/apps/web/src/app/device-auth/page.test.ts b/apps/web/src/app/device-auth/page.test.ts new file mode 100644 index 0000000000..9056622779 --- /dev/null +++ b/apps/web/src/app/device-auth/page.test.ts @@ -0,0 +1,34 @@ +import { redirect } from 'next/navigation'; +import { getUserFromAuthOrRedirect } from '@/lib/user/server'; +import DeviceAuthPage from './page'; + +jest.mock('next/navigation', () => ({ + redirect: jest.fn(), +})); + +jest.mock('@/lib/user/server', () => ({ + getUserFromAuthOrRedirect: jest.fn(), +})); + +jest.mock('@/lib/device-auth/device-auth-viewer-token', () => ({ + createDeviceAuthViewerToken: jest.fn(), +})); + +const mockedRedirect = jest.mocked(redirect); +const mockedGetUserFromAuthOrRedirect = jest.mocked(getUserFromAuthOrRedirect); + +describe('DeviceAuthPage missing code handling', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + test.each([{}, { code: '' }, { code: ' ' }])( + 'redirects malformed device-auth links without authenticating: %j', + async searchParams => { + await DeviceAuthPage({ searchParams: Promise.resolve(searchParams) }); + + expect(mockedRedirect).toHaveBeenCalledWith('/'); + expect(mockedGetUserFromAuthOrRedirect).not.toHaveBeenCalled(); + } + ); +}); diff --git a/apps/web/src/app/device-auth/page.tsx b/apps/web/src/app/device-auth/page.tsx index 2d5dd45aa2..7b56663a73 100644 --- a/apps/web/src/app/device-auth/page.tsx +++ b/apps/web/src/app/device-auth/page.tsx @@ -10,28 +10,29 @@ type PageProps = { }; const deviceAuthSearchParamsSchema = z.object({ - code: z.preprocess( - value => (Array.isArray(value) ? value[0] : value), - z.string().min(1).optional() - ), + code: z.preprocess(value => { + const code = Array.isArray(value) ? value[0] : value; + return typeof code === 'string' ? code.trim() || undefined : code; + }, z.string().min(1).optional()), app: z.union([z.string(), z.array(z.string())]).optional(), }); export default async function DeviceAuthPage({ searchParams }: PageProps) { const params = deviceAuthSearchParamsSchema.parse(await searchParams); const code = params.code; + + if (!code) { + return redirect('/'); + } + const isAppMode = isDeviceAuthAppMode(params.app); // Redirect to login if not authenticated, with callback to return here - const callbackPath = code ? buildDeviceAuthPath(code, { app: isAppMode }) : '/device-auth'; + const callbackPath = buildDeviceAuthPath(code, { app: isAppMode }); const user = await getUserFromAuthOrRedirect( `/users/sign_in?callbackPath=${encodeURIComponent(callbackPath)}` ); - if (!code) { - redirect('/'); - } - const viewerToken = createDeviceAuthViewerToken(code, user.id); return (