Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 34 additions & 0 deletions apps/web/src/app/device-auth/page.test.ts
Original file line number Diff line number Diff line change
@@ -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();
}
);
});
19 changes: 10 additions & 9 deletions apps/web/src/app/device-auth/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
Expand Down