/** * Admin Dashboard Routes * * Server-rendered HTML dashboard for managing ATAuth forward-auth proxy. * All routes are protected by requireAdmin middleware (cookie or bearer). * Uses POST-redirect-GET pattern for all form submissions. */ import crypto from 'crypto'; import { Router, Request, Response } from 'express'; import { DatabaseService } from '../services/database.js'; import { checkAccess } from '../utils/access-check.js'; import { generateHmacSecret } from '../utils/hmac.js'; import { OIDC_APP_PRESETS, getPresetByKey } from '../data/oidc-presets.js'; /** Strip protocol scheme (e.g. https://) from user input to prevent double-protocol in URI templates. */ function stripScheme(input: string): string { return input.replace(/^[a-z][a-z0-9+.-]*:\/\//, '').trim(); } export function createAdminDashboardRoutes( db: DatabaseService, csrfSecret: string, ): Router { const router = Router(); // Short-lived store for one-time secret display (avoids secret in URL) const secretFlash = new Map(); function storeFlash(secret: string): string { const flashId = crypto.randomBytes(16).toString('hex'); secretFlash.set(flashId, { secret, expires: Date.now() + 60_000 }); // Sweep expired entries for (const [k, v] of secretFlash) { if (v.expires < Date.now()) secretFlash.delete(k); } return flashId; } function consumeFlash(flashId: string): string | null { const entry = secretFlash.get(flashId); if (!entry) return null; secretFlash.delete(flashId); if (entry.expires < Date.now()) return null; return entry.secret; } // ===== CSRF Protection ===== function generateCsrfToken(): string { const now = Math.floor(Date.now() / 1000); const payload = `${now}`; const sig = crypto.createHmac('sha256', csrfSecret).update(payload).digest('base64url'); return `${payload}.${sig}`; } function verifyCsrfToken(token: string): boolean { if (!token) return false; const parts = token.split('.'); if (parts.length !== 2) return false; const [payload, sig] = parts; const expected = crypto.createHmac('sha256', csrfSecret).update(payload).digest('base64url'); if (sig.length !== expected.length) return false; if (!crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(expected))) return false; const ts = parseInt(payload, 10); const now = Math.floor(Date.now() / 1000); return now - ts < 3600; // 1 hour validity } function requireCsrf(req: Request, res: Response): boolean { const token = req.body._csrf; if (!verifyCsrfToken(token)) { res.status(403).setHeader('Content-Type', 'text/html; charset=utf-8'); res.send(layout('Error', '

Invalid or expired form submission

Back to dashboard

')); return false; } return true; } // ===== Dashboard Overview ===== router.get('/', (_req: Request, res: Response) => { const origins = db.listProxyAllowedOrigins(); const rules = db.listProxyAccessRules(); const sessions = db.getAllProxySessions(undefined, 1000); const oidcClients = db.getAllOIDCClients(); res.setHeader('Content-Type', 'text/html; charset=utf-8'); res.send(layout('Dashboard', `

Overview

${oidcClients.length}
OIDC Clients
Manage
${origins.length}
Protected Origins
Manage
${rules.length}
Access Rules
Manage
${sessions.length}
Active Sessions
Manage

Quick Actions

Add OIDC Client Setup Wizard Proxy Setup Test Access Check
`)); }); // ===== Origins Management ===== router.get('/origins', (req: Request, res: Response) => { const origins = db.listProxyAllowedOrigins(); const csrf = generateCsrfToken(); const msg = req.query.msg as string | undefined; const ruleCountMap = new Map(); const allRules = db.listProxyAccessRules(); for (const rule of allRules) { if (rule.origin_id !== null) { ruleCountMap.set(rule.origin_id, (ruleCountMap.get(rule.origin_id) || 0) + 1); } } res.setHeader('Content-Type', 'text/html; charset=utf-8'); res.send(layout('Origins', ` ${msg ? `
${esc(msg)}
` : ''}

Protected Origins

Add Origin

${origins.length === 0 ? '

No origins configured. Add one above.

' : ` ${origins.map(o => ` `).join('')}
Name Origin Rules Created
${esc(o.name)} ${esc(o.origin)} ${ruleCountMap.get(o.id) || 0} ${formatDate(o.created_at)}
`} `)); }); router.post('/origins', (req: Request, res: Response) => { if (!requireCsrf(req, res)) return; const { origin, name } = req.body; if (!origin || !name) { return res.redirect('/admin/dashboard/origins?msg=Origin+and+name+are+required'); } try { const parsed = new URL(origin); if (parsed.origin !== origin) { return res.redirect('/admin/dashboard/origins?msg=Invalid+origin+format'); } } catch { return res.redirect('/admin/dashboard/origins?msg=Invalid+URL'); } try { db.addProxyAllowedOrigin(origin, name); res.redirect('/admin/dashboard/origins?msg=Origin+added'); } catch (e) { const msg = e instanceof Error && e.message.includes('UNIQUE') ? 'Origin+already+exists' : 'Failed+to+add+origin'; res.redirect(`/admin/dashboard/origins?msg=${msg}`); } }); router.post('/origins/:id/delete', (req: Request, res: Response) => { if (!requireCsrf(req, res)) return; db.removeProxyAllowedOrigin(parseInt(String(req.params.id), 10)); res.redirect('/admin/dashboard/origins?msg=Origin+deleted'); }); // ===== Access Rules Management ===== router.get('/access', (req: Request, res: Response) => { const filterOriginId = req.query.origin_id !== undefined ? parseInt(req.query.origin_id as string, 10) : undefined; const origins = db.listProxyAllowedOrigins(); const rules = filterOriginId !== undefined ? db.listProxyAccessRules(filterOriginId) : db.listProxyAccessRules(); const csrf = generateCsrfToken(); const msg = req.query.msg as string | undefined; const originMap = new Map(origins.map(o => [o.id, o])); res.setHeader('Content-Type', 'text/html; charset=utf-8'); res.send(layout('Access Rules', ` ${msg ? `
${esc(msg)}
` : ''}

Access Rules

Add Rule

Filter by origin: All ${origins.map(o => ` ${esc(o.name)} `).join(' ')}
${rules.length === 0 ? '

No access rules configured. All authenticated users have access (open mode).

' : ` ${rules.map(r => ` `).join('')}
Type Subject Value Scope Description
${r.rule_type} ${r.subject_type === 'did' ? 'DID' : 'Handle'} ${esc(r.subject_value)} ${r.origin_id === null ? 'Global' : esc(originMap.get(r.origin_id)?.name || `#${r.origin_id}`)} ${r.description ? esc(r.description) : '--'}
`} `)); }); router.post('/access', (req: Request, res: Response) => { if (!requireCsrf(req, res)) return; const { origin_id, rule_type, subject_type, subject_value, description } = req.body; if (!rule_type || !subject_type || !subject_value) { return res.redirect('/admin/dashboard/access?msg=Missing+required+fields'); } if (!['allow', 'deny'].includes(rule_type)) { return res.redirect('/admin/dashboard/access?msg=Invalid+rule+type'); } if (!['did', 'handle_pattern'].includes(subject_type)) { return res.redirect('/admin/dashboard/access?msg=Invalid+subject+type'); } if (subject_type === 'did' && !subject_value.startsWith('did:')) { return res.redirect('/admin/dashboard/access?msg=DID+must+start+with+did:'); } if (subject_type === 'handle_pattern' && subject_value !== '*' && !subject_value.match(/^(\*\.)?[a-zA-Z0-9]([a-zA-Z0-9.-]*[a-zA-Z0-9])?$/)) { return res.redirect('/admin/dashboard/access?msg=Invalid+handle+pattern'); } const parsedOriginId = origin_id ? parseInt(origin_id, 10) : null; try { db.createProxyAccessRule({ origin_id: parsedOriginId, rule_type, subject_type, subject_value, description: description || null, }); res.redirect('/admin/dashboard/access?msg=Rule+added'); } catch { res.redirect('/admin/dashboard/access?msg=Failed+to+add+rule'); } }); router.post('/access/:id/delete', (req: Request, res: Response) => { if (!requireCsrf(req, res)) return; db.deleteProxyAccessRule(parseInt(String(req.params.id), 10)); res.redirect('/admin/dashboard/access?msg=Rule+deleted'); }); // ===== Sessions Management ===== router.get('/sessions', (req: Request, res: Response) => { const filterDid = req.query.did as string | undefined; const sessions = db.getAllProxySessions(filterDid, 200); const csrf = generateCsrfToken(); const msg = req.query.msg as string | undefined; res.setHeader('Content-Type', 'text/html; charset=utf-8'); res.send(layout('Sessions', ` ${msg ? `
${esc(msg)}
` : ''}

Proxy Sessions

${filterDid ? 'Clear' : ''}
${sessions.length === 0 ? '

No active proxy sessions.

' : ` ${sessions.map(s => ` `).join('')}
Handle DID Created Last Activity IP
${esc(s.handle)} ${truncateDid(s.did)} ${formatDate(s.created_at)} ${formatDate(s.last_activity)} ${s.ip_address ? esc(s.ip_address) : '--'}
`} `)); }); router.post('/sessions/:id/delete', (req: Request, res: Response) => { if (!requireCsrf(req, res)) return; db.deleteProxySession(String(req.params.id)); res.redirect('/admin/dashboard/sessions?msg=Session+revoked'); }); // ===== Access Check Tool ===== router.get('/check', (_req: Request, res: Response) => { const origins = db.listProxyAllowedOrigins(); const csrf = generateCsrfToken(); res.setHeader('Content-Type', 'text/html; charset=utf-8'); res.send(layout('Access Check', `

Access Check Tool

Test whether a DID/handle combination would be granted access to an origin.

`)); }); router.post('/check', (req: Request, res: Response) => { if (!requireCsrf(req, res)) return; const { did, handle, origin_id } = req.body; const origins = db.listProxyAllowedOrigins(); const csrf = generateCsrfToken(); if (!did || !handle || origin_id === undefined) { return res.redirect('/admin/dashboard/check'); } const parsedOriginId = parseInt(origin_id, 10); const rules = db.getProxyAccessRulesForCheck(parsedOriginId); const totalRules = rules.denyRules.length + rules.originAllowRules.length + rules.globalAllowRules.length; let result; if (totalRules === 0) { result = { allowed: true, matched_rule_id: null as number | null, reason: 'No access rules configured (open access)' }; } else { result = checkAccess(did, handle, rules); } const origin = origins.find(o => o.id === parsedOriginId); res.setHeader('Content-Type', 'text/html; charset=utf-8'); res.send(layout('Access Check', `

Access Check Tool

${result.allowed ? 'ACCESS ALLOWED' : 'ACCESS DENIED'}

DID
${esc(did)}
Handle
${esc(handle)}
Origin
${origin ? esc(origin.name) : `#${parsedOriginId}`}
Reason
${esc(result.reason)}
${result.matched_rule_id !== null ? `
Matched Rule
#${result.matched_rule_id}
` : ''}
`)); }); // ===== OIDC Client Management ===== router.get('/clients', (req: Request, res: Response) => { const clients = db.getAllOIDCClients(); const msg = req.query.msg as string | undefined; res.setHeader('Content-Type', 'text/html; charset=utf-8'); res.send(layout('Clients', ` ${msg ? `
${esc(msg)}
` : ''}

OIDC Clients

New Client Setup Wizard
${clients.length === 0 ? '

No OIDC clients configured. Create one above or use the setup wizard.

' : ` ${clients.map(c => ` `).join('')}
Name Client ID Redirect URIs Scopes PKCE Created
${esc(c.name)} ${esc(c.id)} ${c.redirect_uris.map(u => `${esc(truncateUri(u))}`).join('
')}
${c.allowed_scopes.map(s => `${esc(s)}`).join(' ')} ${c.require_pkce ? 'Yes' : 'No'} ${formatDateObj(c.created_at)} Edit
`} `)); }); // --- Create Client --- router.get('/clients/new', (req: Request, res: Response) => { const csrf = generateCsrfToken(); const preset = req.query.preset as string | undefined; const domain = req.query.domain as string | undefined; const error = req.query.error as string | undefined; const presetData = preset ? getPresetByKey(preset) : undefined; const defaults = presetData && domain ? { id: presetData.suggested_client_id, name: presetData.name, redirect_uris: presetData.redirect_uri_template.replace(/\{\{DOMAIN\}\}/g, stripScheme(domain)), grant_types: presetData.grant_types, scopes: presetData.scopes, auth_method: presetData.token_endpoint_auth_method, require_pkce: presetData.require_pkce, id_token_ttl: presetData.id_token_ttl_seconds, access_token_ttl: presetData.access_token_ttl_seconds, refresh_token_ttl: presetData.refresh_token_ttl_seconds, } : { id: '', name: '', redirect_uris: '', grant_types: ['authorization_code'], scopes: ['openid'], auth_method: 'client_secret_basic', require_pkce: true, id_token_ttl: 3600, access_token_ttl: 3600, refresh_token_ttl: 604800, }; res.setHeader('Content-Type', 'text/html; charset=utf-8'); res.send(layout('New Client', renderClientForm(csrf, defaults, error, preset))); }); router.post('/clients/new', (req: Request, res: Response) => { if (!requireCsrf(req, res)) return; const { id, name, redirect_uris_text, auth_method, id_token_ttl, access_token_ttl, refresh_token_ttl, preset } = req.body; const grant_types = normalizeCheckboxArray(req.body.grant_types); const scopes = normalizeCheckboxArray(req.body.scopes); const require_pkce = req.body.require_pkce === 'on'; // Validation if (!id || !name) { return renderClientFormWithError(res, req.body, 'Client ID and name are required'); } if (!/^[a-z0-9][a-z0-9_-]*$/.test(id) || id.length > 64) { return renderClientFormWithError(res, req.body, 'Client ID must be lowercase alphanumeric with hyphens/underscores, max 64 chars'); } const redirect_uris = parseTextareaLines(redirect_uris_text); if (redirect_uris.length === 0) { return renderClientFormWithError(res, req.body, 'At least one redirect URI is required'); } for (const uri of redirect_uris) { if (!isValidRedirectUri(uri)) { return renderClientFormWithError(res, req.body, `Invalid redirect URI: ${uri}`); } } if (!grant_types.includes('authorization_code')) { return renderClientFormWithError(res, req.body, 'authorization_code grant type is required'); } if (!scopes.includes('openid')) { return renderClientFormWithError(res, req.body, 'openid scope is required'); } const existing = db.getOIDCClient(id); if (existing) { return renderClientFormWithError(res, req.body, `Client ID "${id}" already exists`); } // Create const clientSecret = crypto.randomBytes(32).toString('hex'); const clientSecretHash = crypto.createHash('sha256').update(clientSecret).digest('hex'); db.upsertApp({ id, name, hmac_secret: generateHmacSecret(), token_ttl_seconds: parseInt(access_token_ttl, 10) || 3600, callback_url: redirect_uris[0], }); db.updateOIDCClient(id, { client_type: 'oidc', client_secret: clientSecretHash, redirect_uris, grant_types, allowed_scopes: scopes, require_pkce, token_endpoint_auth_method: auth_method || 'client_secret_basic', id_token_ttl_seconds: parseInt(id_token_ttl, 10) || 3600, access_token_ttl_seconds: parseInt(access_token_ttl, 10) || 3600, refresh_token_ttl_seconds: parseInt(refresh_token_ttl, 10) || 604800, }); const flashId = storeFlash(clientSecret); const params = new URLSearchParams({ id, flash: flashId }); if (preset) params.set('preset', preset); res.redirect(`/admin/dashboard/clients/created?${params.toString()}`); }); // --- Secret Display (one-time) --- router.get('/clients/created', (req: Request, res: Response) => { const { id, flash, preset, rotated } = req.query as Record; const secret = flash ? consumeFlash(flash) : null; if (!id || !secret) { return res.redirect('/admin/dashboard/clients'); } const presetData = preset ? getPresetByKey(preset) : undefined; const heading = rotated ? 'Secret Rotated' : 'Client Created'; res.setHeader('Content-Type', 'text/html; charset=utf-8'); res.send(layout(heading, `

${heading}

Client ID
${esc(id)}
${esc(secret)}
This secret will not be shown again. Copy it now and store it securely.
${presetData ? `

Setup Instructions: ${esc(presetData.name)}

${esc(presetData.setup_notes)}
` : ''}

Discovery URL

${esc(getDiscoveryUrl())}
Back to Clients
`)); }); // --- Edit Client --- router.get('/clients/:id/edit', (req: Request, res: Response) => { const client = db.getOIDCClient(String(req.params.id)); if (!client) { return res.redirect('/admin/dashboard/clients?msg=Client+not+found'); } const csrf = generateCsrfToken(); const msg = req.query.msg as string | undefined; const values = { id: client.id, name: client.name, redirect_uris: client.redirect_uris.join('\n'), grant_types: client.grant_types, scopes: client.allowed_scopes, auth_method: client.token_endpoint_auth_method, require_pkce: client.require_pkce, id_token_ttl: client.id_token_ttl_seconds, access_token_ttl: client.access_token_ttl_seconds, refresh_token_ttl: client.refresh_token_ttl_seconds, }; res.setHeader('Content-Type', 'text/html; charset=utf-8'); res.send(layout('Edit Client', ` ${msg ? `
${esc(msg)}
` : ''}

Edit Client: ${esc(client.id)}

Rotate Client Secret

Generate a new client secret. The old secret will stop working immediately.

`)); }); router.post('/clients/:id/edit', (req: Request, res: Response) => { if (!requireCsrf(req, res)) return; const clientId = String(req.params.id); const existing = db.getOIDCClient(clientId); if (!existing) { return res.redirect('/admin/dashboard/clients?msg=Client+not+found'); } const { name, redirect_uris_text, auth_method, id_token_ttl, access_token_ttl, refresh_token_ttl } = req.body; const grant_types = normalizeCheckboxArray(req.body.grant_types); const scopes = normalizeCheckboxArray(req.body.scopes); const require_pkce = req.body.require_pkce === 'on'; const redirect_uris = parseTextareaLines(redirect_uris_text); if (!name || redirect_uris.length === 0) { return res.redirect(`/admin/dashboard/clients/${encodeURIComponent(clientId)}/edit?msg=Name+and+redirect+URIs+required`); } // Update name if changed const app = db.getApp(clientId); if (app && name !== app.name) { db.upsertApp({ ...app, name }); } db.updateOIDCClient(clientId, { redirect_uris, grant_types, allowed_scopes: scopes, require_pkce, token_endpoint_auth_method: auth_method || existing.token_endpoint_auth_method, id_token_ttl_seconds: parseInt(id_token_ttl, 10) || existing.id_token_ttl_seconds, access_token_ttl_seconds: parseInt(access_token_ttl, 10) || existing.access_token_ttl_seconds, refresh_token_ttl_seconds: parseInt(refresh_token_ttl, 10) || existing.refresh_token_ttl_seconds, }); res.redirect('/admin/dashboard/clients?msg=Client+updated'); }); // --- Rotate Secret --- router.post('/clients/:id/rotate-secret', (req: Request, res: Response) => { if (!requireCsrf(req, res)) return; const clientId = String(req.params.id); const existing = db.getOIDCClient(clientId); if (!existing) { return res.redirect('/admin/dashboard/clients?msg=Client+not+found'); } const clientSecret = crypto.randomBytes(32).toString('hex'); const clientSecretHash = crypto.createHash('sha256').update(clientSecret).digest('hex'); db.updateOIDCClientSecret(clientId, clientSecretHash); const flashId = storeFlash(clientSecret); const params = new URLSearchParams({ id: clientId, flash: flashId, rotated: '1' }); res.redirect(`/admin/dashboard/clients/created?${params.toString()}`); }); // --- Delete Client --- router.post('/clients/:id/delete', (req: Request, res: Response) => { if (!requireCsrf(req, res)) return; db.deleteApp(String(req.params.id)); res.redirect('/admin/dashboard/clients?msg=Client+deleted'); }); // ===== Setup Wizard ===== router.get('/clients/wizard', (_req: Request, res: Response) => { res.setHeader('Content-Type', 'text/html; charset=utf-8'); res.send(layout('Setup Wizard', `

Setup Wizard

Select an application to auto-configure OIDC settings.

${OIDC_APP_PRESETS.map(p => `

${esc(p.name)}

${esc(p.description)}

Set Up
`).join('')}

Custom

Configure manually with full control over all settings

Create Custom
`)); }); router.get('/clients/wizard/:preset', (req: Request, res: Response) => { const presetData = getPresetByKey(String(req.params.preset)); if (!presetData) { return res.status(404).setHeader('Content-Type', 'text/html; charset=utf-8').send( layout('Not Found', '

Preset not found

Back to wizard

') ); } const csrf = generateCsrfToken(); res.setHeader('Content-Type', 'text/html; charset=utf-8'); res.send(layout(`Setup ${presetData.name}`, `

Setup ${esc(presetData.name)}

${esc(presetData.description)}

Redirect URI pattern: ${esc(presetData.redirect_uri_template)}
Advanced Settings
Back
After setup:\n${esc(presetData.setup_notes)}
`)); }); router.post('/clients/wizard/:preset', (req: Request, res: Response) => { if (!requireCsrf(req, res)) return; const presetKey = String(req.params.preset); const presetData = getPresetByKey(presetKey); if (!presetData) { return res.redirect('/admin/dashboard/clients/wizard'); } const { id, name, domain, auth_method, id_token_ttl, access_token_ttl, refresh_token_ttl, preset } = req.body; const grant_types = normalizeCheckboxArray(req.body.grant_types); const scopes = normalizeCheckboxArray(req.body.scopes); const require_pkce = req.body.require_pkce === 'on'; if (!id || !name || !domain) { return res.redirect(`/admin/dashboard/clients/wizard/${encodeURIComponent(presetKey)}`); } if (!/^[a-z0-9][a-z0-9_-]*$/.test(id) || id.length > 64) { return res.redirect(`/admin/dashboard/clients/wizard/${encodeURIComponent(presetKey)}`); } const existing = db.getOIDCClient(id); if (existing) { return res.redirect(`/admin/dashboard/clients/wizard/${encodeURIComponent(presetKey)}`); } // Build redirect URIs from template const redirect_uris = presetData.redirect_uri_template .replace(/\{\{DOMAIN\}\}/g, stripScheme(domain)) .split('\n') .map(u => u.trim()) .filter(u => u.length > 0); const clientSecret = crypto.randomBytes(32).toString('hex'); const clientSecretHash = crypto.createHash('sha256').update(clientSecret).digest('hex'); db.upsertApp({ id, name, hmac_secret: generateHmacSecret(), token_ttl_seconds: parseInt(access_token_ttl, 10) || 3600, callback_url: redirect_uris[0], }); db.updateOIDCClient(id, { client_type: 'oidc', client_secret: clientSecretHash, redirect_uris, grant_types: grant_types.length > 0 ? grant_types : presetData.grant_types, allowed_scopes: scopes.length > 0 ? scopes : presetData.scopes, require_pkce, token_endpoint_auth_method: auth_method || presetData.token_endpoint_auth_method, id_token_ttl_seconds: parseInt(id_token_ttl, 10) || presetData.id_token_ttl_seconds, access_token_ttl_seconds: parseInt(access_token_ttl, 10) || presetData.access_token_ttl_seconds, refresh_token_ttl_seconds: parseInt(refresh_token_ttl, 10) || presetData.refresh_token_ttl_seconds, }); const flashId = storeFlash(clientSecret); const params = new URLSearchParams({ id, flash: flashId, preset: preset || req.params.preset }); res.redirect(`/admin/dashboard/clients/created?${params.toString()}`); }); // ===== Forward-Auth Proxy Quick Setup ===== router.get('/proxy-wizard', (_req: Request, res: Response) => { const csrf = generateCsrfToken(); res.setHeader('Content-Type', 'text/html; charset=utf-8'); res.send(layout('Proxy Setup', `

Forward-Auth Proxy Setup

Add a service to protect with AT Protocol authentication via nginx forward-auth.

`)); }); router.post('/proxy-wizard', (req: Request, res: Response) => { if (!requireCsrf(req, res)) return; const { origin, name } = req.body; if (!origin || !name) { return res.redirect('/admin/dashboard/proxy-wizard'); } try { const parsed = new URL(origin); if (parsed.origin !== origin) { return res.redirect('/admin/dashboard/proxy-wizard'); } } catch { return res.redirect('/admin/dashboard/proxy-wizard'); } let originId: number; try { const created = db.addProxyAllowedOrigin(origin, name); originId = created.id; } catch { return res.redirect('/admin/dashboard/origins?msg=Origin+already+exists'); } const params = new URLSearchParams({ origin_id: String(originId), origin, name }); res.redirect(`/admin/dashboard/proxy-wizard/result?${params.toString()}`); }); router.get('/proxy-wizard/result', (req: Request, res: Response) => { const { origin, name, origin_id } = req.query as Record; if (!origin) return res.redirect('/admin/dashboard/proxy-wizard'); const authUrl = getAuthBaseUrl(); res.setHeader('Content-Type', 'text/html; charset=utf-8'); res.send(layout('Proxy Setup Complete', `

Proxy Setup: ${esc(name || origin)}

Origin added successfully.

nginx Configuration

Add this to your nginx server block:

location / { auth_request /auth/verify; auth_request_set $auth_did $upstream_http_x_auth_did; auth_request_set $auth_handle $upstream_http_x_auth_handle; proxy_set_header X-Auth-DID $auth_did; proxy_set_header X-Auth-Handle $auth_handle; proxy_pass http://your-backend:8080; } location = /auth/verify { internal; proxy_pass ${esc(authUrl)}/auth/verify; proxy_pass_request_body off; proxy_set_header Content-Length ""; proxy_set_header X-Original-URL $scheme://$http_host$request_uri; proxy_set_header X-Forwarded-Host $http_host; proxy_set_header X-Forwarded-Proto $scheme; } location /auth/ { proxy_pass ${esc(authUrl)}/auth/; proxy_set_header Host $host; proxy_set_header X-Forwarded-Host $http_host; proxy_set_header X-Forwarded-Proto $scheme; }

Kubernetes Ingress Annotations

For nginx-ingress controller:

nginx.ingress.kubernetes.io/auth-url: "${esc(authUrl)}/auth/verify" nginx.ingress.kubernetes.io/auth-signin: "${esc(authUrl)}/auth/login?redirect_uri=$scheme://$http_host$request_uri" nginx.ingress.kubernetes.io/auth-response-headers: "X-Auth-DID,X-Auth-Handle"
${origin_id ? `Add Access Rules` : ''} View All Origins
`)); }); return router; } // ===== Helper Functions ===== function normalizeCheckboxArray(val: unknown): string[] { if (Array.isArray(val)) return val; if (typeof val === 'string' && val) return [val]; return []; } function parseTextareaLines(text: string | undefined): string[] { if (!text) return []; return text.split('\n').map(l => l.trim()).filter(l => l.length > 0); } function isValidRedirectUri(uri: string): boolean { // Allow mobile deep links (e.g., app.immich:/) if (/^[a-z][a-z0-9+.-]*:/.test(uri) && !uri.startsWith('http')) { return true; } try { new URL(uri); return true; } catch { return false; } } function truncateUri(uri: string): string { if (uri.length <= 40) return uri; return uri.substring(0, 37) + '...'; } function formatDateObj(d: Date): string { return d.toISOString().replace('T', ' ').replace(/\.\d+Z$/, ' UTC'); } function getDiscoveryUrl(): string { // Use the configured issuer or a sensible default return process.env.OIDC_ISSUER ? `${process.env.OIDC_ISSUER}/.well-known/openid-configuration` : '/.well-known/openid-configuration'; } function getAuthBaseUrl(): string { return process.env.OIDC_ISSUER || 'https://your-atauth-domain'; } function renderClientForm( csrf: string, values: { id: string; name: string; redirect_uris: string; grant_types: string[]; scopes: string[]; auth_method: string; require_pkce: boolean; id_token_ttl: number; access_token_ttl: number; refresh_token_ttl: number }, error?: string, preset?: string, ): string { return ` ${error ? `
${esc(error)}
` : ''}

New OIDC Client

${preset ? `` : ''}
Cancel
`; } function renderClientFormWithError(res: Response, _body: Record, error: string): void { const msg = encodeURIComponent(error); res.redirect(`/admin/dashboard/clients/new?error=${msg}`); } // ===== Template Helpers ===== function esc(str: string): string { return str .replace(/&/g, '&') .replace(//g, '>') .replace(/"/g, '"') .replace(/'/g, '''); } function formatDate(ts: number): string { return new Date(ts * 1000).toISOString().replace('T', ' ').replace(/\.\d+Z$/, ' UTC'); } function truncateDid(did: string): string { if (did.length <= 24) return did; return did.substring(0, 20) + '...'; } // ===== Layout Template ===== function layout(title: string, content: string): string { return ` ${esc(title)} - ATAuth Admin
${content}
`; }