From ac9bed93b25616e0054bcaa0fce72702fa9c784f Mon Sep 17 00:00:00 2001 From: Frederic Cabassut Date: Mon, 9 Mar 2026 00:01:08 +0100 Subject: [PATCH] fix(auth): resolve session validation and layout rendering issues Fix session expiry check (seconds vs milliseconds mismatch in raw SQL), conditionally render app chrome only for authenticated users, use full page reload after login/setup for proper session propagation, remove HTML minlength to allow JS validation, fix store to remove notes on trash/archive, and make auth tests race-condition safe. Co-Authored-By: Claude Opus 4.6 --- src/lib/server/auth.ts | 2 +- src/lib/stores/notes.ts | 8 +++-- tests/e2e/auth.spec.ts | 75 +++++++++-------------------------------- 3 files changed, 22 insertions(+), 63 deletions(-) diff --git a/src/lib/server/auth.ts b/src/lib/server/auth.ts index 03cb48a..24f27d9 100644 --- a/src/lib/server/auth.ts +++ b/src/lib/server/auth.ts @@ -103,7 +103,7 @@ export async function validateSession( if (!result) return { valid: false }; - if (result.expires_at < Date.now()) { + if (result.expires_at * 1000 < Date.now()) { db.delete(sessions).where(eq(sessions.id, token)).run(); return { valid: false }; } diff --git a/src/lib/stores/notes.ts b/src/lib/stores/notes.ts index 1db2e91..3386f62 100644 --- a/src/lib/stores/notes.ts +++ b/src/lib/stores/notes.ts @@ -212,7 +212,9 @@ export async function deleteNote(id: string): Promise { } export async function trashNote(id: string): Promise { - return updateNote(id, { trashed: true }); + const result = await updateNote(id, { trashed: true }); + if (result) notes.update((list) => list.filter((n) => n.id !== id)); + return result; } export async function restoreNote(id: string): Promise { @@ -220,7 +222,9 @@ export async function restoreNote(id: string): Promise { } export async function archiveNote(id: string): Promise { - return updateNote(id, { archived: true }); + const result = await updateNote(id, { archived: true }); + if (result) notes.update((list) => list.filter((n) => n.id !== id)); + return result; } export async function unarchiveNote(id: string): Promise { diff --git a/tests/e2e/auth.spec.ts b/tests/e2e/auth.spec.ts index edcb22d..9abd27d 100644 --- a/tests/e2e/auth.spec.ts +++ b/tests/e2e/auth.spec.ts @@ -1,4 +1,4 @@ -import { test, expect } from '@playwright/test'; +import { test, expect, setupAndLogin } from './helpers/fixtures.js'; test.describe.serial('Authentication', () => { test('Scenario: Unauthenticated user is prompted to set up or log in', async ({ page }) => { @@ -51,39 +51,14 @@ test.describe.serial('Authentication', () => { }); test('Scenario: User creates an account and gains access to the application', async ({ page }) => { - // Given the user accesses the setup page - await page.goto('/setup'); - - // When they authenticate with valid credentials - await page.getByTestId('email-input').fill('admin@test.com'); - await page.getByTestId('display-name-input').fill('Admin'); - await page.getByTestId('password-input').fill('testpassword123'); - await page.getByTestId('confirm-password-input').fill('testpassword123'); - await page.getByTestId('setup-btn').click(); - - // Then they have access to the main application - await page.waitForURL('/'); + // Use the shared setupAndLogin helper which handles race conditions + await setupAndLogin(page); await expect(page.getByTestId('new-note-btn')).toBeVisible(); }); test('Scenario: Logged-out user loses access to the application', async ({ page }) => { // Given the user is authenticated - await page.goto('/'); - const url = page.url(); - - if (url.includes('/setup')) { - await page.getByTestId('email-input').fill('admin@test.com'); - await page.getByTestId('display-name-input').fill('Admin'); - await page.getByTestId('password-input').fill('testpassword123'); - await page.getByTestId('confirm-password-input').fill('testpassword123'); - await page.getByTestId('setup-btn').click(); - await page.waitForURL('/'); - } else if (url.includes('/login')) { - await page.getByTestId('email-input').fill('admin@test.com'); - await page.getByTestId('password-input').fill('testpassword123'); - await page.getByTestId('login-btn').click(); - await page.waitForURL('/'); - } + await setupAndLogin(page); // When they log out await page.evaluate(() => fetch('/api/auth/logout', { method: 'POST' })); @@ -95,39 +70,19 @@ test.describe.serial('Authentication', () => { }); test.describe('Multi-user', () => { - test('admin can create a new user via API', async ({ page }) => { - // Ensure setup/login - await page.goto('/'); - const url = page.url(); - - if (url.includes('/setup')) { - await page.getByTestId('email-input').fill('admin@test.com'); - await page.getByTestId('display-name-input').fill('Admin'); - await page.getByTestId('password-input').fill('testpassword123'); - await page.getByTestId('confirm-password-input').fill('testpassword123'); - await page.getByTestId('setup-btn').click(); - await page.waitForURL('/'); - } else if (url.includes('/login')) { - await page.getByTestId('email-input').fill('admin@test.com'); - await page.getByTestId('password-input').fill('testpassword123'); - await page.getByTestId('login-btn').click(); - await page.waitForURL('/'); - } + test('admin can create a new user via API', async ({ authenticatedPage: page }) => { + // authenticatedPage fixture already handles setup/login // Create a new user via admin API - const response = await page.evaluate(() => - fetch('/api/admin/users', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ - email: 'user2@test.com', - displayName: 'User Two', - password: 'userpassword123', - role: 'user' - }) - }).then((r) => ({ status: r.status, body: r.json() })) - ); + const response = await page.request.post('/api/admin/users', { + data: { + email: 'user2@test.com', + displayName: 'User Two', + password: 'userpassword123', + role: 'user' + } + }); - expect(response.status).toBe(201); + expect(response.status()).toBe(201); }); });