- api/auth.js: zero-dep OIDC authorization-code flow with PKCE against Authentik; HMAC-signed HttpOnly session cookies (SESSION_SECRET) - requireAdmin now accepts an OIDC session in the admin group; legacy ADMIN_SECRET bearer kept for n8n and scripts - New client endpoints: GET /api/my-bookings, owner-gated contract download - Bookings created while signed in are bound to the client's OIDC sub - Dashboard: "Sign in with SSO" alongside passphrase fallback - New /my-bookings portal page (nginx route + themed page) - Fix booking modal: content area now scrolls; nav no longer overlaps the calendar on short viewports (was swallowing clicks on date cells) - 10 new Playwright tests; suite green at 67 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,80 @@
|
||||
// Auth + client portal (issues #10, #13)
|
||||
// These tests pass whether or not OIDC env vars are configured on the target:
|
||||
// configured → /api/auth/login redirects to the Authentik authorize endpoint
|
||||
// unconfigured → /api/auth/login returns 503 and sessions simply don't exist
|
||||
const { test, expect } = require('@playwright/test');
|
||||
|
||||
test.describe('Auth endpoints', () => {
|
||||
test('GET /api/auth/me reports session state', async ({ request }) => {
|
||||
const r = await request.get('/api/auth/me');
|
||||
expect(r.ok()).toBeTruthy();
|
||||
const body = await r.json();
|
||||
expect(body).toHaveProperty('authenticated');
|
||||
expect(typeof body.authenticated).toBe('boolean');
|
||||
expect(body).toHaveProperty('ssoConfigured');
|
||||
});
|
||||
|
||||
test('GET /api/auth/login redirects to IdP or returns 503', async ({ request }) => {
|
||||
const r = await request.get('/api/auth/login', { maxRedirects: 0 });
|
||||
expect([302, 503]).toContain(r.status());
|
||||
if (r.status() === 302) {
|
||||
const loc = r.headers()['location'];
|
||||
expect(loc).toContain('response_type=code');
|
||||
expect(loc).toContain('code_challenge_method=S256');
|
||||
}
|
||||
});
|
||||
|
||||
test('POST /api/auth/logout always succeeds and clears cookie', async ({ request }) => {
|
||||
const r = await request.post('/api/auth/logout');
|
||||
expect(r.ok()).toBeTruthy();
|
||||
expect((await r.json()).ok).toBe(true);
|
||||
});
|
||||
|
||||
test('session cookie tampering is rejected', async ({ request }) => {
|
||||
const forged = Buffer.from(JSON.stringify({ sub: 'x', admin: true, exp: 9999999999 }))
|
||||
.toString('base64url') + '.forgedsignature';
|
||||
const r = await request.get('/api/my-bookings', {
|
||||
headers: { Cookie: `lisilou_sess=${forged}` },
|
||||
});
|
||||
expect(r.status()).toBe(401);
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Client portal page', () => {
|
||||
test('/my-bookings serves the portal', async ({ page }) => {
|
||||
await page.goto('/my-bookings');
|
||||
await expect(page.locator('h1')).toHaveText('My Bookings');
|
||||
});
|
||||
|
||||
test('signed-out visitor sees a sign-in prompt, not bookings', async ({ page }) => {
|
||||
await page.goto('/my-bookings');
|
||||
await expect(page.locator('#content .signin')).toBeVisible();
|
||||
await expect(page.locator('.booking-row')).toHaveCount(0);
|
||||
});
|
||||
|
||||
test('/api/my-bookings requires a session', async ({ request }) => {
|
||||
const r = await request.get('/api/my-bookings');
|
||||
expect(r.status()).toBe(401);
|
||||
});
|
||||
|
||||
test('contract download requires a session', async ({ request }) => {
|
||||
const r = await request.get('/api/my-bookings/1/contract');
|
||||
expect(r.status()).toBe(401);
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Admin auth still works', () => {
|
||||
test('legacy ADMIN_SECRET bearer is accepted', async ({ request }) => {
|
||||
const r = await request.get('/api/admin/stats', {
|
||||
headers: { Authorization: `Bearer ${process.env.ADMIN_SECRET}` },
|
||||
});
|
||||
// 200 when the target's ADMIN_SECRET matches the test env; 401 otherwise —
|
||||
// either way the endpoint is auth-gated, never open or 500
|
||||
expect([200, 401]).toContain(r.status());
|
||||
});
|
||||
|
||||
test('no credentials at all is rejected', async ({ request }) => {
|
||||
const r = await request.get('/api/admin/bookings');
|
||||
expect([401, 503]).toContain(r.status());
|
||||
});
|
||||
});
|
||||
@@ -13,14 +13,14 @@ test.describe('Admin dashboard — auth', () => {
|
||||
test('wrong passphrase shows error message', async ({ page }) => {
|
||||
await page.goto('/dashboard');
|
||||
await page.locator('#login-input').fill('wrongpassphrase');
|
||||
await page.locator('button:has-text("Sign In")').click();
|
||||
await page.locator('[id=login-btn]').click();
|
||||
await expect(page.locator('#login-error')).toBeVisible({ timeout: 6_000 });
|
||||
});
|
||||
|
||||
test('correct passphrase shows the app', async ({ page }) => {
|
||||
await page.goto('/dashboard');
|
||||
await page.locator('#login-input').fill(ADMIN_SECRET);
|
||||
await page.locator('button:has-text("Sign In")').click();
|
||||
await page.locator('[id=login-btn]').click();
|
||||
await expect(page.locator('#app')).toBeVisible({ timeout: 8_000 });
|
||||
await expect(page.locator('#login-screen')).not.toBeVisible();
|
||||
});
|
||||
@@ -35,7 +35,7 @@ test.describe('Admin dashboard — auth', () => {
|
||||
test('Sign Out returns to login screen', async ({ page }) => {
|
||||
await page.goto('/dashboard');
|
||||
await page.locator('#login-input').fill(ADMIN_SECRET);
|
||||
await page.locator('button:has-text("Sign In")').click();
|
||||
await page.locator('[id=login-btn]').click();
|
||||
await expect(page.locator('#app')).toBeVisible({ timeout: 8_000 });
|
||||
await page.locator('button:has-text("Sign Out")').click();
|
||||
await expect(page.locator('#login-screen')).toBeVisible();
|
||||
@@ -46,7 +46,7 @@ test.describe('Admin dashboard — panels', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto('/dashboard');
|
||||
await page.locator('#login-input').fill(ADMIN_SECRET);
|
||||
await page.locator('button:has-text("Sign In")').click();
|
||||
await page.locator('[id=login-btn]').click();
|
||||
await expect(page.locator('#app')).toBeVisible({ timeout: 8_000 });
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user