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:
@@ -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'),
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user