From 9f60c6983da482665da823f2e90a44cefefbbceb Mon Sep 17 00:00:00 2001 From: cottongin Date: Mon, 23 Mar 2026 09:25:50 -0400 Subject: [PATCH] feat: auth route uses named admin lookup, embeds name in JWT - Login/verify use findAdminByKey; JWT and response include admin name - Verify returns 403 when token lacks name (legacy tokens) - Test tokens include name for getAuthToken() - Set Content-Type on supertest JSON bodies (superagent/mime resolution) Made-with: Cursor --- backend/routes/auth.js | 30 +++++++++----------- tests/api/named-admins.test.js | 47 +++++++++++++++++++++++++++++++ tests/api/session-archive.test.js | 10 +++++++ tests/api/session-notes.test.js | 5 ++++ tests/helpers/test-utils.js | 2 +- 5 files changed, 77 insertions(+), 17 deletions(-) diff --git a/backend/routes/auth.js b/backend/routes/auth.js index cd6728f..859d03d 100644 --- a/backend/routes/auth.js +++ b/backend/routes/auth.js @@ -1,15 +1,10 @@ const express = require('express'); const jwt = require('jsonwebtoken'); const { JWT_SECRET, authenticateToken } = require('../middleware/auth'); +const { findAdminByKey } = require('../config/load-admins'); const router = express.Router(); -if (!process.env.ADMIN_KEY) { - throw new Error('ADMIN_KEY environment variable is required'); -} -const ADMIN_KEY = process.env.ADMIN_KEY; - -// Login with admin key router.post('/login', (req, res) => { const { key } = req.body; @@ -17,31 +12,34 @@ router.post('/login', (req, res) => { return res.status(400).json({ error: 'Admin key is required' }); } - if (key !== ADMIN_KEY) { + const admin = findAdminByKey(key); + if (!admin) { return res.status(401).json({ error: 'Invalid admin key' }); } - // Generate JWT token const token = jwt.sign( - { role: 'admin', timestamp: Date.now() }, + { role: 'admin', name: admin.name, timestamp: Date.now() }, JWT_SECRET, { expiresIn: '24h' } ); - res.json({ - token, + res.json({ + token, + name: admin.name, message: 'Authentication successful', expiresIn: '24h' }); }); -// Verify token validity router.post('/verify', authenticateToken, (req, res) => { - res.json({ - valid: true, - user: req.user + if (!req.user.name) { + return res.status(403).json({ error: 'Token missing admin identity, please re-login' }); + } + + res.json({ + valid: true, + user: req.user }); }); module.exports = router; - diff --git a/tests/api/named-admins.test.js b/tests/api/named-admins.test.js index a5cb41d..e4729c4 100644 --- a/tests/api/named-admins.test.js +++ b/tests/api/named-admins.test.js @@ -78,3 +78,50 @@ describe('load-admins', () => { }).toThrow(/duplicate/i); }); }); + +const request = require('supertest'); + +describe('POST /api/auth/login — named admins', () => { + let app; + + beforeAll(() => { + process.env.ADMIN_KEY = 'test-admin-key'; + process.env.ADMIN_CONFIG_PATH = '/tmp/nonexistent-admins.json'; + jest.resetModules(); + ({ app } = require('../../backend/server')); + }); + + test('login returns admin name in response', async () => { + const res = await request(app) + .post('/api/auth/login') + .set('Content-Type', 'application/json') + .send({ key: 'test-admin-key' }); + + expect(res.status).toBe(200); + expect(res.body.name).toBeDefined(); + expect(res.body.token).toBeDefined(); + }); + + test('verify returns admin name in user object', async () => { + const loginRes = await request(app) + .post('/api/auth/login') + .set('Content-Type', 'application/json') + .send({ key: 'test-admin-key' }); + + const res = await request(app) + .post('/api/auth/verify') + .set('Authorization', `Bearer ${loginRes.body.token}`); + + expect(res.status).toBe(200); + expect(res.body.user.name).toBeDefined(); + }); + + test('invalid key still returns 401', async () => { + const res = await request(app) + .post('/api/auth/login') + .set('Content-Type', 'application/json') + .send({ key: 'wrong-key' }); + + expect(res.status).toBe(401); + }); +}); diff --git a/tests/api/session-archive.test.js b/tests/api/session-archive.test.js index ecccf26..e76a235 100644 --- a/tests/api/session-archive.test.js +++ b/tests/api/session-archive.test.js @@ -184,6 +184,7 @@ describe('POST /api/sessions/bulk', () => { const res = await request(app) .post('/api/sessions/bulk') .set('Authorization', getAuthHeader()) + .set('Content-Type', 'application/json') .send({ action: 'archive', ids: [s1.id, s2.id] }); expect(res.status).toBe(200); @@ -203,6 +204,7 @@ describe('POST /api/sessions/bulk', () => { const res = await request(app) .post('/api/sessions/bulk') .set('Authorization', getAuthHeader()) + .set('Content-Type', 'application/json') .send({ action: 'unarchive', ids: [s1.id, s2.id] }); expect(res.status).toBe(200); @@ -219,6 +221,7 @@ describe('POST /api/sessions/bulk', () => { const res = await request(app) .post('/api/sessions/bulk') .set('Authorization', getAuthHeader()) + .set('Content-Type', 'application/json') .send({ action: 'delete', ids: [s1.id, s2.id] }); expect(res.status).toBe(200); @@ -235,6 +238,7 @@ describe('POST /api/sessions/bulk', () => { const res = await request(app) .post('/api/sessions/bulk') .set('Authorization', getAuthHeader()) + .set('Content-Type', 'application/json') .send({ action: 'archive', ids: [active.id, closed.id] }); expect(res.status).toBe(400); @@ -251,6 +255,7 @@ describe('POST /api/sessions/bulk', () => { const res = await request(app) .post('/api/sessions/bulk') .set('Authorization', getAuthHeader()) + .set('Content-Type', 'application/json') .send({ action: 'delete', ids: [active.id] }); expect(res.status).toBe(400); @@ -260,6 +265,7 @@ describe('POST /api/sessions/bulk', () => { const res = await request(app) .post('/api/sessions/bulk') .set('Authorization', getAuthHeader()) + .set('Content-Type', 'application/json') .send({ action: 'archive', ids: [] }); expect(res.status).toBe(400); @@ -269,6 +275,7 @@ describe('POST /api/sessions/bulk', () => { const res = await request(app) .post('/api/sessions/bulk') .set('Authorization', getAuthHeader()) + .set('Content-Type', 'application/json') .send({ action: 'nuke', ids: [1] }); expect(res.status).toBe(400); @@ -278,6 +285,7 @@ describe('POST /api/sessions/bulk', () => { const res = await request(app) .post('/api/sessions/bulk') .set('Authorization', getAuthHeader()) + .set('Content-Type', 'application/json') .send({ action: 'archive', ids: 'not-array' }); expect(res.status).toBe(400); @@ -289,6 +297,7 @@ describe('POST /api/sessions/bulk', () => { const res = await request(app) .post('/api/sessions/bulk') .set('Authorization', getAuthHeader()) + .set('Content-Type', 'application/json') .send({ action: 'archive', ids: [s1.id, 9999] }); expect(res.status).toBe(404); @@ -297,6 +306,7 @@ describe('POST /api/sessions/bulk', () => { test('returns 401 without auth', async () => { const res = await request(app) .post('/api/sessions/bulk') + .set('Content-Type', 'application/json') .send({ action: 'archive', ids: [1] }); expect(res.status).toBe(401); diff --git a/tests/api/session-notes.test.js b/tests/api/session-notes.test.js index 06dbe42..ae2104c 100644 --- a/tests/api/session-notes.test.js +++ b/tests/api/session-notes.test.js @@ -157,6 +157,7 @@ describe('PUT /api/sessions/:id/notes', () => { const res = await request(app) .put(`/api/sessions/${session.id}/notes`) .set('Authorization', getAuthHeader()) + .set('Content-Type', 'application/json') .send({ notes: 'New notes here' }); expect(res.status).toBe(200); @@ -169,6 +170,7 @@ describe('PUT /api/sessions/:id/notes', () => { const res = await request(app) .put(`/api/sessions/${session.id}/notes`) .set('Authorization', getAuthHeader()) + .set('Content-Type', 'application/json') .send({ notes: 'Replacement' }); expect(res.status).toBe(200); @@ -179,6 +181,7 @@ describe('PUT /api/sessions/:id/notes', () => { const res = await request(app) .put('/api/sessions/99999/notes') .set('Authorization', getAuthHeader()) + .set('Content-Type', 'application/json') .send({ notes: 'test' }); expect(res.status).toBe(404); @@ -189,6 +192,7 @@ describe('PUT /api/sessions/:id/notes', () => { const res = await request(app) .put(`/api/sessions/${session.id}/notes`) + .set('Content-Type', 'application/json') .send({ notes: 'test' }); expect(res.status).toBe(401); @@ -200,6 +204,7 @@ describe('PUT /api/sessions/:id/notes', () => { const res = await request(app) .put(`/api/sessions/${session.id}/notes`) .set('Authorization', 'Bearer invalid-token') + .set('Content-Type', 'application/json') .send({ notes: 'test' }); expect(res.status).toBe(403); diff --git a/tests/helpers/test-utils.js b/tests/helpers/test-utils.js index 76a5c53..df0d3be 100644 --- a/tests/helpers/test-utils.js +++ b/tests/helpers/test-utils.js @@ -2,7 +2,7 @@ const jwt = require('jsonwebtoken'); const db = require('../../backend/database'); function getAuthToken() { - return jwt.sign({ role: 'admin' }, process.env.JWT_SECRET, { expiresIn: '1h' }); + return jwt.sign({ role: 'admin', name: 'TestAdmin' }, process.env.JWT_SECRET, { expiresIn: '1h' }); } function getAuthHeader() {