Something went wrong. Try again.
AtAuth
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836/** * Admin Dashboard Routes Tests */import { describe, it, expect, beforeEach, afterEach } from 'vitest';import express from 'express';import request from 'supertest';import crypto from 'crypto';import { createAdminRoutes } from './admin.js';import { DatabaseService } from '../services/database.js';import { createAdminCookie, ADMIN_COOKIE_NAME } from '../utils/proxy-auth.js';
const ADMIN_TOKEN = 'test-admin-token-secret';const SESSION_SECRET = 'test-session-secret-for-admin-32!';
function createTestApp(db: DatabaseService) { const app = express(); app.use(express.json()); app.use(express.urlencoded({ extended: true }));
const router = createAdminRoutes(db, ADMIN_TOKEN, null, null, null, SESSION_SECRET); app.use('/admin', router); return app;}
function adminCookie(): string { return `${ADMIN_COOKIE_NAME}=${createAdminCookie(SESSION_SECRET, 86400)}`;}
describe('Dashboard Overview', () => { let db: DatabaseService; let app: express.Application;
beforeEach(() => { db = new DatabaseService(':memory:'); app = createTestApp(db); });
afterEach(() => { db.close(); });
it('should render overview page with stats', async () => { db.addProxyAllowedOrigin('https://search.arcnode.xyz', 'SearXNG');
const res = await request(app) .get('/admin/dashboard') .set('Cookie', adminCookie());
expect(res.status).toBe(200); expect(res.text).toContain('Overview'); expect(res.text).toContain('Protected Origins'); expect(res.text).toContain('Access Rules'); expect(res.text).toContain('Active Sessions'); });
it('should redirect to login without cookie', async () => { const res = await request(app) .get('/admin/dashboard') .set('Accept', 'text/html');
expect(res.status).toBe(302); expect(res.headers.location).toBe('/admin/login'); });});
describe('Dashboard Origins', () => { let db: DatabaseService; let app: express.Application;
beforeEach(() => { db = new DatabaseService(':memory:'); app = createTestApp(db); });
afterEach(() => { db.close(); });
it('should render origins page', async () => { const res = await request(app) .get('/admin/dashboard/origins') .set('Cookie', adminCookie());
expect(res.status).toBe(200); expect(res.text).toContain('Protected Origins'); expect(res.text).toContain('Add Origin'); });
it('should list existing origins', async () => { db.addProxyAllowedOrigin('https://search.arcnode.xyz', 'SearXNG');
const res = await request(app) .get('/admin/dashboard/origins') .set('Cookie', adminCookie());
expect(res.status).toBe(200); expect(res.text).toContain('SearXNG'); expect(res.text).toContain('search.arcnode.xyz'); });
it('should add an origin via form POST', async () => { // First get the page to extract CSRF token const page = await request(app) .get('/admin/dashboard/origins') .set('Cookie', adminCookie());
const csrfMatch = page.text.match(/name="_csrf" value="([^"]+)"/); expect(csrfMatch).not.toBeNull(); const csrf = csrfMatch![1];
const res = await request(app) .post('/admin/dashboard/origins') .set('Cookie', adminCookie()) .type('form') .send({ _csrf: csrf, origin: 'https://test.example.com', name: 'Test' });
expect(res.status).toBe(302); expect(res.headers.location).toContain('/admin/dashboard/origins');
// Verify it was added const origins = db.listProxyAllowedOrigins(); expect(origins).toHaveLength(1); expect(origins[0].origin).toBe('https://test.example.com'); });
it('should reject form without CSRF token', async () => { const res = await request(app) .post('/admin/dashboard/origins') .set('Cookie', adminCookie()) .type('form') .send({ origin: 'https://test.example.com', name: 'Test' });
expect(res.status).toBe(403); expect(res.text).toContain('Invalid or expired form submission'); });
it('should delete an origin via form POST', async () => { const origin = db.addProxyAllowedOrigin('https://search.arcnode.xyz', 'SearXNG');
const page = await request(app) .get('/admin/dashboard/origins') .set('Cookie', adminCookie()); const csrfMatch = page.text.match(/name="_csrf" value="([^"]+)"/); const csrf = csrfMatch![1];
const res = await request(app) .post(`/admin/dashboard/origins/${origin.id}/delete`) .set('Cookie', adminCookie()) .type('form') .send({ _csrf: csrf });
expect(res.status).toBe(302); expect(db.listProxyAllowedOrigins()).toHaveLength(0); });});
describe('Dashboard Access Rules', () => { let db: DatabaseService; let app: express.Application;
beforeEach(() => { db = new DatabaseService(':memory:'); app = createTestApp(db); });
afterEach(() => { db.close(); });
it('should render access rules page', async () => { const res = await request(app) .get('/admin/dashboard/access') .set('Cookie', adminCookie());
expect(res.status).toBe(200); expect(res.text).toContain('Access Rules'); expect(res.text).toContain('Add Rule'); });
it('should add a rule via form POST', async () => { const page = await request(app) .get('/admin/dashboard/access') .set('Cookie', adminCookie()); const csrfMatch = page.text.match(/name="_csrf" value="([^"]+)"/); const csrf = csrfMatch![1];
const res = await request(app) .post('/admin/dashboard/access') .set('Cookie', adminCookie()) .type('form') .send({ _csrf: csrf, origin_id: '', rule_type: 'allow', subject_type: 'handle_pattern', subject_value: '*.arcnode.xyz', description: 'PDS users', });
expect(res.status).toBe(302); const rules = db.listProxyAccessRules(); expect(rules).toHaveLength(1); expect(rules[0].subject_value).toBe('*.arcnode.xyz'); });
it('should display existing rules', async () => { db.createProxyAccessRule({ origin_id: null, rule_type: 'allow', subject_type: 'handle_pattern', subject_value: '*.arcnode.xyz', description: 'PDS users', });
const res = await request(app) .get('/admin/dashboard/access') .set('Cookie', adminCookie());
expect(res.text).toContain('*.arcnode.xyz'); expect(res.text).toContain('PDS users'); expect(res.text).toContain('allow'); });});
describe('Dashboard Sessions', () => { let db: DatabaseService; let app: express.Application;
beforeEach(() => { db = new DatabaseService(':memory:'); app = createTestApp(db); });
afterEach(() => { db.close(); });
it('should render sessions page', async () => { const res = await request(app) .get('/admin/dashboard/sessions') .set('Cookie', adminCookie());
expect(res.status).toBe(200); expect(res.text).toContain('Proxy Sessions'); });
it('should list active sessions', async () => { const now = Math.floor(Date.now() / 1000); db.createProxySession({ id: 'sess-1', did: 'did:plc:test123', handle: 'user.bsky.social', created_at: now, expires_at: now + 604800, last_activity: now, });
const res = await request(app) .get('/admin/dashboard/sessions') .set('Cookie', adminCookie());
expect(res.text).toContain('user.bsky.social'); });
it('should revoke a session via form POST', async () => { const now = Math.floor(Date.now() / 1000); db.createProxySession({ id: 'sess-del', did: 'did:plc:test123', handle: 'user.bsky.social', created_at: now, expires_at: now + 604800, last_activity: now, });
const page = await request(app) .get('/admin/dashboard/sessions') .set('Cookie', adminCookie()); const csrfMatch = page.text.match(/name="_csrf" value="([^"]+)"/); const csrf = csrfMatch![1];
const res = await request(app) .post('/admin/dashboard/sessions/sess-del/delete') .set('Cookie', adminCookie()) .type('form') .send({ _csrf: csrf });
expect(res.status).toBe(302); expect(db.getProxySession('sess-del')).toBeNull(); });});
describe('Dashboard Access Check', () => { let db: DatabaseService; let app: express.Application;
beforeEach(() => { db = new DatabaseService(':memory:'); app = createTestApp(db); });
afterEach(() => { db.close(); });
it('should render access check page', async () => { const res = await request(app) .get('/admin/dashboard/check') .set('Cookie', adminCookie());
expect(res.status).toBe(200); expect(res.text).toContain('Access Check Tool'); });
it('should show access check result', async () => { const origin = db.addProxyAllowedOrigin('https://search.arcnode.xyz', 'SearXNG'); db.createProxyAccessRule({ origin_id: null, rule_type: 'allow', subject_type: 'handle_pattern', subject_value: '*.arcnode.xyz', description: null, });
const page = await request(app) .get('/admin/dashboard/check') .set('Cookie', adminCookie()); const csrfMatch = page.text.match(/name="_csrf" value="([^"]+)"/); const csrf = csrfMatch![1];
const res = await request(app) .post('/admin/dashboard/check') .set('Cookie', adminCookie()) .type('form') .send({ _csrf: csrf, did: 'did:plc:test123', handle: 'bkb.arcnode.xyz', origin_id: origin.id.toString(), });
expect(res.status).toBe(200); expect(res.text).toContain('ACCESS ALLOWED'); });
it('should show denied result', async () => { const origin = db.addProxyAllowedOrigin('https://search.arcnode.xyz', 'SearXNG'); db.createProxyAccessRule({ origin_id: null, rule_type: 'allow', subject_type: 'handle_pattern', subject_value: '*.arcnode.xyz', description: null, });
const page = await request(app) .get('/admin/dashboard/check') .set('Cookie', adminCookie()); const csrfMatch = page.text.match(/name="_csrf" value="([^"]+)"/); const csrf = csrfMatch![1];
const res = await request(app) .post('/admin/dashboard/check') .set('Cookie', adminCookie()) .type('form') .send({ _csrf: csrf, did: 'did:plc:outsider', handle: 'user.bsky.social', origin_id: origin.id.toString(), });
expect(res.status).toBe(200); expect(res.text).toContain('ACCESS DENIED'); });});
// ===== Helper to create an OIDC client via DB =====
function createTestOIDCClient(db: DatabaseService, id = 'test-app', name = 'Test App') { db.upsertApp({ id, name, hmac_secret: crypto.randomBytes(32).toString('hex'), token_ttl_seconds: 3600, callback_url: 'https://app.example.com/callback', }); db.updateOIDCClient(id, { client_type: 'oidc', client_secret: crypto.createHash('sha256').update('test-secret').digest('hex'), redirect_uris: ['https://app.example.com/callback'], grant_types: ['authorization_code', 'refresh_token'], allowed_scopes: ['openid', 'profile', 'email'], require_pkce: true, token_endpoint_auth_method: 'client_secret_basic', id_token_ttl_seconds: 3600, access_token_ttl_seconds: 3600, refresh_token_ttl_seconds: 604800, });}
async function getCsrf(app: express.Application, path: string): Promise<string> { const page = await request(app) .get(path) .set('Cookie', adminCookie()); const csrfMatch = page.text.match(/name="_csrf" value="([^"]+)"/); if (!csrfMatch) throw new Error(`No CSRF token found on ${path}`); return csrfMatch[1];}
// ===== OIDC Client Management =====
describe('Dashboard OIDC Clients', () => { let db: DatabaseService; let app: express.Application;
beforeEach(() => { db = new DatabaseService(':memory:'); app = createTestApp(db); });
afterEach(() => { db.close(); });
it('should render client list page', async () => { const res = await request(app) .get('/admin/dashboard/clients') .set('Cookie', adminCookie());
expect(res.status).toBe(200); expect(res.text).toContain('OIDC Clients'); expect(res.text).toContain('New Client'); });
it('should list existing clients in table', async () => { createTestOIDCClient(db);
const res = await request(app) .get('/admin/dashboard/clients') .set('Cookie', adminCookie());
expect(res.status).toBe(200); expect(res.text).toContain('test-app'); expect(res.text).toContain('Test App'); expect(res.text).toContain('app.example.com'); });
it('should render new client form', async () => { const res = await request(app) .get('/admin/dashboard/clients/new') .set('Cookie', adminCookie());
expect(res.status).toBe(200); expect(res.text).toContain('New OIDC Client'); expect(res.text).toContain('Client ID'); expect(res.text).toContain('Redirect URIs'); expect(res.text).toContain('Grant Types'); });
it('should create client and redirect to secret page', async () => { const csrf = await getCsrf(app, '/admin/dashboard/clients/new');
const res = await request(app) .post('/admin/dashboard/clients/new') .set('Cookie', adminCookie()) .type('form') .send({ _csrf: csrf, id: 'myapp', name: 'My App', redirect_uris_text: 'https://myapp.example.com/callback', grant_types: ['authorization_code', 'refresh_token'], scopes: ['openid', 'profile'], auth_method: 'client_secret_basic', require_pkce: 'on', id_token_ttl: '3600', access_token_ttl: '3600', refresh_token_ttl: '604800', });
expect(res.status).toBe(302); expect(res.headers.location).toContain('/admin/dashboard/clients/created'); expect(res.headers.location).toContain('id=myapp'); expect(res.headers.location).toContain('secret=');
// Verify in DB const client = db.getOIDCClient('myapp'); expect(client).not.toBeNull(); expect(client!.name).toBe('My App'); expect(client!.redirect_uris).toEqual(['https://myapp.example.com/callback']); expect(client!.require_pkce).toBe(true); });
it('should display secret on created page', async () => { const res = await request(app) .get('/admin/dashboard/clients/created?id=test&secret=abc123hex') .set('Cookie', adminCookie());
expect(res.status).toBe(200); expect(res.text).toContain('Client Created'); expect(res.text).toContain('abc123hex'); expect(res.text).toContain('This secret will not be shown again'); expect(res.text).toContain('Discovery URL'); });
it('should reject duplicate client ID', async () => { createTestOIDCClient(db); const csrf = await getCsrf(app, '/admin/dashboard/clients/new');
const res = await request(app) .post('/admin/dashboard/clients/new') .set('Cookie', adminCookie()) .type('form') .send({ _csrf: csrf, id: 'test-app', name: 'Duplicate', redirect_uris_text: 'https://dup.example.com/callback', grant_types: 'authorization_code', scopes: 'openid', auth_method: 'client_secret_basic', require_pkce: 'on', id_token_ttl: '3600', access_token_ttl: '3600', refresh_token_ttl: '604800', });
expect(res.status).toBe(302); expect(res.headers.location).toContain('error='); expect(res.headers.location).toContain('already%20exists'); });
it('should reject missing required fields', async () => { const csrf = await getCsrf(app, '/admin/dashboard/clients/new');
const res = await request(app) .post('/admin/dashboard/clients/new') .set('Cookie', adminCookie()) .type('form') .send({ _csrf: csrf, id: '', name: '', redirect_uris_text: '', });
expect(res.status).toBe(302); expect(res.headers.location).toContain('error='); });
it('should render edit form with pre-filled values', async () => { createTestOIDCClient(db);
const res = await request(app) .get('/admin/dashboard/clients/test-app/edit') .set('Cookie', adminCookie());
expect(res.status).toBe(200); expect(res.text).toContain('Edit Client'); expect(res.text).toContain('test-app'); expect(res.text).toContain('Test App'); expect(res.text).toContain('app.example.com/callback'); expect(res.text).toContain('Rotate Client Secret'); });
it('should update client via edit form', async () => { createTestOIDCClient(db); const csrf = await getCsrf(app, '/admin/dashboard/clients/test-app/edit');
const res = await request(app) .post('/admin/dashboard/clients/test-app/edit') .set('Cookie', adminCookie()) .type('form') .send({ _csrf: csrf, name: 'Updated App', redirect_uris_text: 'https://updated.example.com/callback', grant_types: 'authorization_code', scopes: ['openid', 'profile'], auth_method: 'client_secret_post', require_pkce: 'on', id_token_ttl: '7200', access_token_ttl: '7200', refresh_token_ttl: '1209600', });
expect(res.status).toBe(302); expect(res.headers.location).toContain('Client+updated');
const updated = db.getOIDCClient('test-app'); expect(updated!.redirect_uris).toEqual(['https://updated.example.com/callback']); expect(updated!.token_endpoint_auth_method).toBe('client_secret_post'); expect(updated!.id_token_ttl_seconds).toBe(7200); });
it('should rotate secret and show new secret', async () => { createTestOIDCClient(db); const oldClient = db.getOIDCClient('test-app'); const csrf = await getCsrf(app, '/admin/dashboard/clients/test-app/edit');
const res = await request(app) .post('/admin/dashboard/clients/test-app/rotate-secret') .set('Cookie', adminCookie()) .type('form') .send({ _csrf: csrf });
expect(res.status).toBe(302); expect(res.headers.location).toContain('/admin/dashboard/clients/created'); expect(res.headers.location).toContain('rotated=1'); expect(res.headers.location).toContain('secret=');
// Verify secret changed const newClient = db.getOIDCClient('test-app'); expect(newClient!.client_secret).not.toBe(oldClient!.client_secret); });
it('should delete client', async () => { createTestOIDCClient(db); const csrf = await getCsrf(app, '/admin/dashboard/clients');
const res = await request(app) .post('/admin/dashboard/clients/test-app/delete') .set('Cookie', adminCookie()) .type('form') .send({ _csrf: csrf });
expect(res.status).toBe(302); expect(res.headers.location).toContain('Client+deleted'); expect(db.getOIDCClient('test-app')).toBeNull(); });
it('should reject POST without CSRF', async () => { const res = await request(app) .post('/admin/dashboard/clients/new') .set('Cookie', adminCookie()) .type('form') .send({ id: 'nope', name: 'Nope', redirect_uris_text: 'https://nope.com/cb', grant_types: 'authorization_code', scopes: 'openid', });
expect(res.status).toBe(403); expect(res.text).toContain('Invalid or expired form submission'); });});
// ===== Setup Wizard =====
describe('Dashboard Setup Wizard', () => { let db: DatabaseService; let app: express.Application;
beforeEach(() => { db = new DatabaseService(':memory:'); app = createTestApp(db); });
afterEach(() => { db.close(); });
it('should render app selection grid', async () => { const res = await request(app) .get('/admin/dashboard/clients/wizard') .set('Cookie', adminCookie());
expect(res.status).toBe(200); expect(res.text).toContain('Setup Wizard'); expect(res.text).toContain('Audiobookshelf'); expect(res.text).toContain('Jellyfin'); expect(res.text).toContain('Nextcloud'); expect(res.text).toContain('Gitea'); expect(res.text).toContain('Custom'); });
it('should render preset form with pre-filled values', async () => { const res = await request(app) .get('/admin/dashboard/clients/wizard/audiobookshelf') .set('Cookie', adminCookie());
expect(res.status).toBe(200); expect(res.text).toContain('Setup Audiobookshelf'); expect(res.text).toContain('audiobookshelf'); expect(res.text).toContain('/auth/openid/callback'); expect(res.text).toContain('Your Domain'); });
it('should create client from wizard preset', async () => { const csrf = await getCsrf(app, '/admin/dashboard/clients/wizard/audiobookshelf');
const res = await request(app) .post('/admin/dashboard/clients/wizard/audiobookshelf') .set('Cookie', adminCookie()) .type('form') .send({ _csrf: csrf, preset: 'audiobookshelf', domain: 'abs.example.com', id: 'audiobookshelf', name: 'Audiobookshelf', grant_types: ['authorization_code', 'refresh_token'], scopes: ['openid', 'profile', 'email'], auth_method: 'client_secret_basic', require_pkce: 'on', id_token_ttl: '3600', access_token_ttl: '3600', refresh_token_ttl: '604800', });
expect(res.status).toBe(302); expect(res.headers.location).toContain('/admin/dashboard/clients/created'); expect(res.headers.location).toContain('preset=audiobookshelf');
const client = db.getOIDCClient('audiobookshelf'); expect(client).not.toBeNull(); expect(client!.redirect_uris).toEqual(['https://abs.example.com/auth/openid/callback']); });
it('should show setup notes on created page', async () => { const res = await request(app) .get('/admin/dashboard/clients/created?id=abs&secret=abc123&preset=audiobookshelf') .set('Cookie', adminCookie());
expect(res.status).toBe(200); expect(res.text).toContain('Setup Instructions'); expect(res.text).toContain('Audiobookshelf'); expect(res.text).toContain('Issuer URL'); });
it('should 404 for unknown preset', async () => { const res = await request(app) .get('/admin/dashboard/clients/wizard/nonexistent') .set('Cookie', adminCookie());
expect(res.status).toBe(404); expect(res.text).toContain('Preset not found'); });});
// ===== Proxy Wizard =====
describe('Dashboard Proxy Wizard', () => { let db: DatabaseService; let app: express.Application;
beforeEach(() => { db = new DatabaseService(':memory:'); app = createTestApp(db); });
afterEach(() => { db.close(); });
it('should render proxy setup form', async () => { const res = await request(app) .get('/admin/dashboard/proxy-wizard') .set('Cookie', adminCookie());
expect(res.status).toBe(200); expect(res.text).toContain('Forward-Auth Proxy Setup'); expect(res.text).toContain('Service Name'); expect(res.text).toContain('Origin URL'); });
it('should create origin and show config snippets', async () => { const csrf = await getCsrf(app, '/admin/dashboard/proxy-wizard');
const res = await request(app) .post('/admin/dashboard/proxy-wizard') .set('Cookie', adminCookie()) .type('form') .send({ _csrf: csrf, name: 'SearXNG', origin: 'https://search.arcnode.xyz', });
expect(res.status).toBe(302); expect(res.headers.location).toContain('/admin/dashboard/proxy-wizard/result');
// Verify origin was created const origins = db.listProxyAllowedOrigins(); expect(origins).toHaveLength(1); expect(origins[0].name).toBe('SearXNG'); });
it('should display config snippets on result page', async () => { const res = await request(app) .get('/admin/dashboard/proxy-wizard/result?origin=https://search.arcnode.xyz&name=SearXNG&origin_id=1') .set('Cookie', adminCookie());
expect(res.status).toBe(200); expect(res.text).toContain('nginx Configuration'); expect(res.text).toContain('auth_request'); expect(res.text).toContain('Kubernetes Ingress'); expect(res.text).toContain('auth-url'); expect(res.text).toContain('Add Access Rules'); });});
// ===== Overview with OIDC Stats =====
describe('Dashboard Overview OIDC Stats', () => { let db: DatabaseService; let app: express.Application;
beforeEach(() => { db = new DatabaseService(':memory:'); app = createTestApp(db); });
afterEach(() => { db.close(); });
it('should show OIDC client count on overview', async () => { createTestOIDCClient(db, 'app1', 'App One'); createTestOIDCClient(db, 'app2', 'App Two');
const res = await request(app) .get('/admin/dashboard') .set('Cookie', adminCookie());
expect(res.status).toBe(200); expect(res.text).toContain('OIDC Clients'); expect(res.text).toContain('>2<'); expect(res.text).toContain('Setup Wizard'); expect(res.text).toContain('Add OIDC Client'); });});