feat: display passkey setup page for users coming from the login page

This commit is contained in:
Elias Schneider
2026-10-02 20:56:04 +02:00
parent c9681234dd
commit 4d2a5e423e
7 changed files with 86 additions and 24 deletions
+25
View File
@@ -2,6 +2,7 @@ import test, { expect, type Browser } from '@playwright/test';
import { oneTimeAccessTokens } from '../data';
import { cleanupBackend } from '../utils/cleanup.util';
import { pathFromRoot } from '../utils/fs.util';
import passkeyUtil from '../utils/passkey.util';
test.beforeEach(async () => await cleanupBackend());
@@ -13,6 +14,30 @@ test('Sign in with login code', async ({ page }) => {
await page.goto(`/lc/${token.token}`);
await page.waitForURL('/settings/account');
await expect(page.getByRole('heading', { name: 'Set up your passkey' })).not.toBeVisible();
});
test('Sign in with login code without a passkey shows passkey setup', async ({ page }) => {
// Remove the existing passkey so the login code is the user's only way to sign in
await page.goto('/login');
await (await passkeyUtil.init(page)).addPasskey();
await page.getByRole('button', { name: 'Authenticate' }).click();
await page.waitForURL('/settings/account');
await page.getByLabel('Delete').first().click();
await page.getByLabel('Delete Passkey').getByRole('button', { name: 'Delete' }).click();
await expect(page.locator('[data-type="success"]')).toHaveText('Passkey deleted successfully');
// Sign in through the real login-code flow to verify the missing-passkey redirect
await page.context().clearCookies();
const token = oneTimeAccessTokens.find((t) => !t.expired)!;
await page.goto(`/lc/${token.token}`);
await page.waitForURL('/signup/add-passkey');
await expect(page.getByRole('heading', { name: 'Set up your passkey' })).toBeVisible();
// Opening another settings page must still require passkey setup
await page.goto('/settings/apps');
await expect(page).toHaveURL('/signup/add-passkey');
await expect(page.getByRole('heading', { name: 'Set up your passkey' })).toBeVisible();
});
test('Sign in with expired login code fails', async ({ page }) => {
+30 -6
View File
@@ -78,7 +78,7 @@ test.describe('Initial User Signup', () => {
await page.getByLabel('Email').fill('jane.smith@test.com');
await page.getByRole('button', { name: 'Sign Up' }).click();
await page.waitForURL('/signup/add-passkey');
await expect(page.getByText('Set up your passkey')).toBeVisible();
await expect(page.getByRole('heading', { name: 'Set up your passkey' })).toBeVisible();
});
test('Initial Signup - concurrent requests create one administrator', async ({ request }) => {
@@ -145,7 +145,7 @@ test.describe('User Signup', () => {
await page.getByRole('button', { name: 'Sign Up' }).click();
await page.waitForURL('/signup/add-passkey');
await expect(page.getByText('Set up your passkey')).toBeVisible();
await expect(page.getByRole('heading', { name: 'Set up your passkey' })).toBeVisible();
const response = await page.request.get('/api/users/me').then((res) => res.json());
expect(response.userGroups.map((g) => g.id)).toContain(userGroups.developers.id);
@@ -189,7 +189,7 @@ test.describe('User Signup', () => {
await page.getByRole('button', { name: 'Sign Up' }).click();
await page.waitForURL('/signup/add-passkey');
await expect(page.getByText('Set up your passkey')).toBeVisible();
await expect(page.getByRole('heading', { name: 'Set up your passkey' })).toBeVisible();
});
test('Open signup - validation errors', async ({ page }) => {
@@ -251,7 +251,7 @@ test.describe('User Signup', () => {
await expect(page.getByText('Single Passkey Configured')).toBeVisible();
});
test('Skip passkey creation during signup', async ({ page }) => {
test('Dismiss passkey setup during signup for one day', async ({ page }) => {
await setSignupMode(page, 'Open Signup');
await page.goto('/signup');
@@ -262,14 +262,38 @@ test.describe('User Signup', () => {
await page.getByRole('button', { name: 'Sign Up' }).click();
await page.waitForURL('/signup/add-passkey');
await expect(page.getByRole('heading', { name: 'Set up your passkey' })).toBeVisible();
// Cancelling the confirmation must keep passkey setup open
await page.getByRole('button', { name: 'Skip for now' }).click();
const dialog = page.getByRole('alertdialog', { name: 'Skip Passkey Setup' });
await dialog.getByRole('button', { name: 'Cancel' }).click();
await page.reload();
await expect(page).toHaveURL('/signup/add-passkey');
await expect(page.getByRole('heading', { name: 'Set up your passkey' })).toBeVisible();
await expect(page.getByText('Skip Passkey Setup')).toBeVisible();
await page.getByRole('button', { name: 'Skip for now' }).nth(1).click();
// Confirming dismissal allows settings access without creating a passkey
const dismissedAt = Date.now();
await page.clock.setFixedTime(dismissedAt);
await page.getByRole('button', { name: 'Skip for now' }).click();
await dialog.getByRole('button', { name: 'Skip for now' }).click();
await page.waitForURL('/settings/account');
await expect(page.getByText('Passkey missing')).toBeVisible();
await page.reload();
await expect(page).toHaveURL('/settings/account');
await expect(page.getByText('Passkey missing')).toBeVisible();
// Dismissal persists across settings pages until the one-day grace period expires
await page.clock.setFixedTime(dismissedAt + 24 * 60 * 60 * 1000 - 1000);
await page.goto('/settings/apps');
await expect(page).toHaveURL('/settings/apps');
await expect(page.getByRole('heading', { name: 'Set up your passkey' })).not.toBeVisible();
await page.clock.setFixedTime(dismissedAt + 24 * 60 * 60 * 1000);
await page.reload();
await expect(page).toHaveURL('/signup/add-passkey');
await expect(page.getByRole('heading', { name: 'Set up your passkey' })).toBeVisible();
});
test('Token usage limit is enforced', async ({ page }) => {