From 788628485b1ac99f1610033660f6ec42bb93f58f Mon Sep 17 00:00:00 2001 From: jtricerolph Date: Tue, 7 Jul 2026 12:57:13 +0000 Subject: [PATCH] Add per-app session timeout: max_session_hours column on apps, enforced in /verify Co-Authored-By: Claude Sonnet 4.6 --- src/db.js | 1 + src/routes/admin.js | 16 ++++++++++------ src/routes/auth.js | 12 ++++++++++++ 3 files changed, 23 insertions(+), 6 deletions(-) diff --git a/src/db.js b/src/db.js index 5086b82..242c05c 100644 --- a/src/db.js +++ b/src/db.js @@ -110,6 +110,7 @@ export async function initDb() { await pool.query(`ALTER TABLE apps ADD COLUMN IF NOT EXISTS category VARCHAR(100)`) await pool.query(`ALTER TABLE apps ADD COLUMN IF NOT EXISTS internal_host TEXT`) await pool.query(`ALTER TABLE apps ADD COLUMN IF NOT EXISTS internal_port INTEGER DEFAULT 3080`) + await pool.query(`ALTER TABLE apps ADD COLUMN IF NOT EXISTS max_session_hours INTEGER`) await pool.query(`ALTER TABLE users ADD COLUMN IF NOT EXISTS workforce_user_id TEXT`) await pool.query(`CREATE UNIQUE INDEX IF NOT EXISTS users_workforce_user_id_idx ON users (workforce_user_id) WHERE workforce_user_id IS NOT NULL`) await pool.query(`CREATE UNIQUE INDEX IF NOT EXISTS pending_reg_email_idx ON pending_registrations (email)`) diff --git a/src/routes/admin.js b/src/routes/admin.js index 4f016c9..3ab29ca 100644 --- a/src/routes/admin.js +++ b/src/routes/admin.js @@ -129,13 +129,17 @@ export async function adminRoutes(app) { }) app.patch('/apps/:slug', async (request, reply) => { - const { active } = request.body || {} - const { rows: [app] } = await pool.query( - 'UPDATE apps SET active = $1 WHERE slug = $2 RETURNING *', - [active, request.params.slug] + const { active, max_session_hours } = request.body || {} + const updates = []; const values = [] + if (active !== undefined) updates.push(`active = $${values.push(active)}`) + if (max_session_hours !== undefined) updates.push(`max_session_hours = $${values.push(max_session_hours ?? null)}`) + if (!updates.length) return reply.status(400).send({ error: 'Nothing to update' }) + values.push(request.params.slug) + const { rows: [a] } = await pool.query( + `UPDATE apps SET ${updates.join(', ')} WHERE slug = $${values.length} RETURNING *`, values ) - if (!app) return reply.status(404).send({ error: 'App not found' }) - return app + if (!a) return reply.status(404).send({ error: 'App not found' }) + return a }) // ── Roles ────────────────────────────────────────────────────────────────── diff --git a/src/routes/auth.js b/src/routes/auth.js index b42874b..8fb9c64 100644 --- a/src/routes/auth.js +++ b/src/routes/auth.js @@ -171,6 +171,18 @@ export async function authRoutes(app) { if (!user) return reply.status(403).send({ error: 'Access denied' }) + if (appSlug) { + const { rows: [appRow] } = await pool.query( + 'SELECT max_session_hours FROM apps WHERE slug = $1 AND active = true', [appSlug] + ) + if (appRow?.max_session_hours) { + const ageHours = (Date.now() / 1000 - payload.iat) / 3600 + if (ageHours > appRow.max_session_hours) { + return reply.status(401).send({ error: 'Session expired' }) + } + } + } + if (!user.offsite_allowed) { const clientIP = request.headers['x-real-ip'] || request.ip if (!(await isOnsite(clientIP))) {