feat(auth): token-based admin authentication for Playwright/CI (#105)

Add POST /api/auth/token — validates ADMIN_TOKEN env var, creates a 24h
session and sets the sid cookie so automated tools can access the admin
panel without Google OAuth. Admin login page gains a token input form.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-26 12:07:43 +00:00
parent b554970032
commit e96ceb7ee1
7 changed files with 151 additions and 2 deletions

View File

@@ -34,6 +34,17 @@ export const config = {
ML_SERVING_URL: optional('ML_SERVING_URL', 'http://localhost:8000'),
LITELLM_URL: optional('LITELLM_URL', 'http://localhost:4000'),
MLFLOW_URL: optional('MLFLOW_URL', 'http://localhost:5000'),
AIRFLOW_URL: optional('AIRFLOW_URL', 'http://localhost:8080'),
AIRFLOW_API_USER: optional('AIRFLOW_API_USER', 'admin'),
AIRFLOW_API_PASSWORD: optional('AIRFLOW_API_PASSWORD', 'admin'),
/** Shared secret for internal Airflow→API callbacks. */
INTERNAL_API_TOKEN: optional('INTERNAL_API_TOKEN', ''),
/** Static token for automated/service access to the admin panel (e.g. Playwright tests). */
ADMIN_TOKEN: optional('ADMIN_TOKEN', ''),
VAPID_PUBLIC_KEY: optional('VAPID_PUBLIC_KEY', ''),
VAPID_PRIVATE_KEY: optional('VAPID_PRIVATE_KEY', ''),
VAPID_SUBJECT: optional('VAPID_SUBJECT', 'mailto:admin@localhost'),

View File

@@ -124,6 +124,45 @@ router.get('/callback', async (req: Request, res: Response) => {
.redirect(`${config.WEB_BASE_URL}${pending.redirectTo}`);
});
/**
* POST /api/auth/token
* Exchange the static ADMIN_TOKEN for a session cookie.
* Finds the first admin user in the DB; rejects if ADMIN_TOKEN is not configured.
*/
router.post('/token', async (req: Request, res: Response) => {
const { token } = req.body as { token?: string };
if (!config.ADMIN_TOKEN || !token || token !== config.ADMIN_TOKEN) {
res.status(401).json({ error: 'Invalid token' });
return;
}
const [adminUser] = await db
.select()
.from(users)
.where(eq(users.role, 'admin'))
.limit(1);
if (!adminUser) {
res.status(403).json({ error: 'No admin user exists' });
return;
}
const sid = nanoid(32);
const now = new Date().toISOString();
const expiresAt = new Date(Date.now() + 24 * 60 * 60 * 1000).toISOString();
await db.insert(sessions).values({ id: sid, userId: adminUser.id, expiresAt, createdAt: now });
res
.cookie('sid', sid, {
httpOnly: true,
secure: config.NODE_ENV === 'production',
sameSite: 'lax',
expires: new Date(expiresAt),
path: '/',
})
.json({ ok: true });
});
/** POST /api/auth/logout */
router.post('/logout', async (req: Request, res: Response) => {
const sid = req.cookies?.sid as string | undefined;