From 55899b01eee10b138fc8929d640cc0f47baaf77e Mon Sep 17 00:00:00 2001 From: pandemicsyn <333354+pandemicsyn@users.noreply.github.com> Date: Wed, 9 Sep 2026 19:32:20 +0000 Subject: [PATCH] fix(ui): improve device auth parameter parsing and validation Refine the `code` parameter preprocessing to handle whitespace trimming and ensure robust type checking. Added a redirect check for missing codes and simplified the callback path construction. Added unit tests for the device authentication page to ensure correct parameter handling and redirection logic. Co-authored-by: kiloconnect[bot] <240665456+kiloconnect[bot]@users.noreply.github.com> --- apps/web/src/app/device-auth/page.test.ts | 34 +++++++++++++++++++++++ apps/web/src/app/device-auth/page.tsx | 19 +++++++------ 2 files changed, 44 insertions(+), 9 deletions(-) create mode 100644 apps/web/src/app/device-auth/page.test.ts 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 (