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
This commit is contained in:
@@ -1,15 +1,10 @@
|
|||||||
const express = require('express');
|
const express = require('express');
|
||||||
const jwt = require('jsonwebtoken');
|
const jwt = require('jsonwebtoken');
|
||||||
const { JWT_SECRET, authenticateToken } = require('../middleware/auth');
|
const { JWT_SECRET, authenticateToken } = require('../middleware/auth');
|
||||||
|
const { findAdminByKey } = require('../config/load-admins');
|
||||||
|
|
||||||
const router = express.Router();
|
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) => {
|
router.post('/login', (req, res) => {
|
||||||
const { key } = req.body;
|
const { key } = req.body;
|
||||||
|
|
||||||
@@ -17,26 +12,30 @@ router.post('/login', (req, res) => {
|
|||||||
return res.status(400).json({ error: 'Admin key is required' });
|
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' });
|
return res.status(401).json({ error: 'Invalid admin key' });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Generate JWT token
|
|
||||||
const token = jwt.sign(
|
const token = jwt.sign(
|
||||||
{ role: 'admin', timestamp: Date.now() },
|
{ role: 'admin', name: admin.name, timestamp: Date.now() },
|
||||||
JWT_SECRET,
|
JWT_SECRET,
|
||||||
{ expiresIn: '24h' }
|
{ expiresIn: '24h' }
|
||||||
);
|
);
|
||||||
|
|
||||||
res.json({
|
res.json({
|
||||||
token,
|
token,
|
||||||
|
name: admin.name,
|
||||||
message: 'Authentication successful',
|
message: 'Authentication successful',
|
||||||
expiresIn: '24h'
|
expiresIn: '24h'
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// Verify token validity
|
|
||||||
router.post('/verify', authenticateToken, (req, res) => {
|
router.post('/verify', authenticateToken, (req, res) => {
|
||||||
|
if (!req.user.name) {
|
||||||
|
return res.status(403).json({ error: 'Token missing admin identity, please re-login' });
|
||||||
|
}
|
||||||
|
|
||||||
res.json({
|
res.json({
|
||||||
valid: true,
|
valid: true,
|
||||||
user: req.user
|
user: req.user
|
||||||
@@ -44,4 +43,3 @@ router.post('/verify', authenticateToken, (req, res) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
module.exports = router;
|
module.exports = router;
|
||||||
|
|
||||||
|
|||||||
@@ -78,3 +78,50 @@ describe('load-admins', () => {
|
|||||||
}).toThrow(/duplicate/i);
|
}).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);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
@@ -184,6 +184,7 @@ describe('POST /api/sessions/bulk', () => {
|
|||||||
const res = await request(app)
|
const res = await request(app)
|
||||||
.post('/api/sessions/bulk')
|
.post('/api/sessions/bulk')
|
||||||
.set('Authorization', getAuthHeader())
|
.set('Authorization', getAuthHeader())
|
||||||
|
.set('Content-Type', 'application/json')
|
||||||
.send({ action: 'archive', ids: [s1.id, s2.id] });
|
.send({ action: 'archive', ids: [s1.id, s2.id] });
|
||||||
|
|
||||||
expect(res.status).toBe(200);
|
expect(res.status).toBe(200);
|
||||||
@@ -203,6 +204,7 @@ describe('POST /api/sessions/bulk', () => {
|
|||||||
const res = await request(app)
|
const res = await request(app)
|
||||||
.post('/api/sessions/bulk')
|
.post('/api/sessions/bulk')
|
||||||
.set('Authorization', getAuthHeader())
|
.set('Authorization', getAuthHeader())
|
||||||
|
.set('Content-Type', 'application/json')
|
||||||
.send({ action: 'unarchive', ids: [s1.id, s2.id] });
|
.send({ action: 'unarchive', ids: [s1.id, s2.id] });
|
||||||
|
|
||||||
expect(res.status).toBe(200);
|
expect(res.status).toBe(200);
|
||||||
@@ -219,6 +221,7 @@ describe('POST /api/sessions/bulk', () => {
|
|||||||
const res = await request(app)
|
const res = await request(app)
|
||||||
.post('/api/sessions/bulk')
|
.post('/api/sessions/bulk')
|
||||||
.set('Authorization', getAuthHeader())
|
.set('Authorization', getAuthHeader())
|
||||||
|
.set('Content-Type', 'application/json')
|
||||||
.send({ action: 'delete', ids: [s1.id, s2.id] });
|
.send({ action: 'delete', ids: [s1.id, s2.id] });
|
||||||
|
|
||||||
expect(res.status).toBe(200);
|
expect(res.status).toBe(200);
|
||||||
@@ -235,6 +238,7 @@ describe('POST /api/sessions/bulk', () => {
|
|||||||
const res = await request(app)
|
const res = await request(app)
|
||||||
.post('/api/sessions/bulk')
|
.post('/api/sessions/bulk')
|
||||||
.set('Authorization', getAuthHeader())
|
.set('Authorization', getAuthHeader())
|
||||||
|
.set('Content-Type', 'application/json')
|
||||||
.send({ action: 'archive', ids: [active.id, closed.id] });
|
.send({ action: 'archive', ids: [active.id, closed.id] });
|
||||||
|
|
||||||
expect(res.status).toBe(400);
|
expect(res.status).toBe(400);
|
||||||
@@ -251,6 +255,7 @@ describe('POST /api/sessions/bulk', () => {
|
|||||||
const res = await request(app)
|
const res = await request(app)
|
||||||
.post('/api/sessions/bulk')
|
.post('/api/sessions/bulk')
|
||||||
.set('Authorization', getAuthHeader())
|
.set('Authorization', getAuthHeader())
|
||||||
|
.set('Content-Type', 'application/json')
|
||||||
.send({ action: 'delete', ids: [active.id] });
|
.send({ action: 'delete', ids: [active.id] });
|
||||||
|
|
||||||
expect(res.status).toBe(400);
|
expect(res.status).toBe(400);
|
||||||
@@ -260,6 +265,7 @@ describe('POST /api/sessions/bulk', () => {
|
|||||||
const res = await request(app)
|
const res = await request(app)
|
||||||
.post('/api/sessions/bulk')
|
.post('/api/sessions/bulk')
|
||||||
.set('Authorization', getAuthHeader())
|
.set('Authorization', getAuthHeader())
|
||||||
|
.set('Content-Type', 'application/json')
|
||||||
.send({ action: 'archive', ids: [] });
|
.send({ action: 'archive', ids: [] });
|
||||||
|
|
||||||
expect(res.status).toBe(400);
|
expect(res.status).toBe(400);
|
||||||
@@ -269,6 +275,7 @@ describe('POST /api/sessions/bulk', () => {
|
|||||||
const res = await request(app)
|
const res = await request(app)
|
||||||
.post('/api/sessions/bulk')
|
.post('/api/sessions/bulk')
|
||||||
.set('Authorization', getAuthHeader())
|
.set('Authorization', getAuthHeader())
|
||||||
|
.set('Content-Type', 'application/json')
|
||||||
.send({ action: 'nuke', ids: [1] });
|
.send({ action: 'nuke', ids: [1] });
|
||||||
|
|
||||||
expect(res.status).toBe(400);
|
expect(res.status).toBe(400);
|
||||||
@@ -278,6 +285,7 @@ describe('POST /api/sessions/bulk', () => {
|
|||||||
const res = await request(app)
|
const res = await request(app)
|
||||||
.post('/api/sessions/bulk')
|
.post('/api/sessions/bulk')
|
||||||
.set('Authorization', getAuthHeader())
|
.set('Authorization', getAuthHeader())
|
||||||
|
.set('Content-Type', 'application/json')
|
||||||
.send({ action: 'archive', ids: 'not-array' });
|
.send({ action: 'archive', ids: 'not-array' });
|
||||||
|
|
||||||
expect(res.status).toBe(400);
|
expect(res.status).toBe(400);
|
||||||
@@ -289,6 +297,7 @@ describe('POST /api/sessions/bulk', () => {
|
|||||||
const res = await request(app)
|
const res = await request(app)
|
||||||
.post('/api/sessions/bulk')
|
.post('/api/sessions/bulk')
|
||||||
.set('Authorization', getAuthHeader())
|
.set('Authorization', getAuthHeader())
|
||||||
|
.set('Content-Type', 'application/json')
|
||||||
.send({ action: 'archive', ids: [s1.id, 9999] });
|
.send({ action: 'archive', ids: [s1.id, 9999] });
|
||||||
|
|
||||||
expect(res.status).toBe(404);
|
expect(res.status).toBe(404);
|
||||||
@@ -297,6 +306,7 @@ describe('POST /api/sessions/bulk', () => {
|
|||||||
test('returns 401 without auth', async () => {
|
test('returns 401 without auth', async () => {
|
||||||
const res = await request(app)
|
const res = await request(app)
|
||||||
.post('/api/sessions/bulk')
|
.post('/api/sessions/bulk')
|
||||||
|
.set('Content-Type', 'application/json')
|
||||||
.send({ action: 'archive', ids: [1] });
|
.send({ action: 'archive', ids: [1] });
|
||||||
|
|
||||||
expect(res.status).toBe(401);
|
expect(res.status).toBe(401);
|
||||||
|
|||||||
@@ -157,6 +157,7 @@ describe('PUT /api/sessions/:id/notes', () => {
|
|||||||
const res = await request(app)
|
const res = await request(app)
|
||||||
.put(`/api/sessions/${session.id}/notes`)
|
.put(`/api/sessions/${session.id}/notes`)
|
||||||
.set('Authorization', getAuthHeader())
|
.set('Authorization', getAuthHeader())
|
||||||
|
.set('Content-Type', 'application/json')
|
||||||
.send({ notes: 'New notes here' });
|
.send({ notes: 'New notes here' });
|
||||||
|
|
||||||
expect(res.status).toBe(200);
|
expect(res.status).toBe(200);
|
||||||
@@ -169,6 +170,7 @@ describe('PUT /api/sessions/:id/notes', () => {
|
|||||||
const res = await request(app)
|
const res = await request(app)
|
||||||
.put(`/api/sessions/${session.id}/notes`)
|
.put(`/api/sessions/${session.id}/notes`)
|
||||||
.set('Authorization', getAuthHeader())
|
.set('Authorization', getAuthHeader())
|
||||||
|
.set('Content-Type', 'application/json')
|
||||||
.send({ notes: 'Replacement' });
|
.send({ notes: 'Replacement' });
|
||||||
|
|
||||||
expect(res.status).toBe(200);
|
expect(res.status).toBe(200);
|
||||||
@@ -179,6 +181,7 @@ describe('PUT /api/sessions/:id/notes', () => {
|
|||||||
const res = await request(app)
|
const res = await request(app)
|
||||||
.put('/api/sessions/99999/notes')
|
.put('/api/sessions/99999/notes')
|
||||||
.set('Authorization', getAuthHeader())
|
.set('Authorization', getAuthHeader())
|
||||||
|
.set('Content-Type', 'application/json')
|
||||||
.send({ notes: 'test' });
|
.send({ notes: 'test' });
|
||||||
|
|
||||||
expect(res.status).toBe(404);
|
expect(res.status).toBe(404);
|
||||||
@@ -189,6 +192,7 @@ describe('PUT /api/sessions/:id/notes', () => {
|
|||||||
|
|
||||||
const res = await request(app)
|
const res = await request(app)
|
||||||
.put(`/api/sessions/${session.id}/notes`)
|
.put(`/api/sessions/${session.id}/notes`)
|
||||||
|
.set('Content-Type', 'application/json')
|
||||||
.send({ notes: 'test' });
|
.send({ notes: 'test' });
|
||||||
|
|
||||||
expect(res.status).toBe(401);
|
expect(res.status).toBe(401);
|
||||||
@@ -200,6 +204,7 @@ describe('PUT /api/sessions/:id/notes', () => {
|
|||||||
const res = await request(app)
|
const res = await request(app)
|
||||||
.put(`/api/sessions/${session.id}/notes`)
|
.put(`/api/sessions/${session.id}/notes`)
|
||||||
.set('Authorization', 'Bearer invalid-token')
|
.set('Authorization', 'Bearer invalid-token')
|
||||||
|
.set('Content-Type', 'application/json')
|
||||||
.send({ notes: 'test' });
|
.send({ notes: 'test' });
|
||||||
|
|
||||||
expect(res.status).toBe(403);
|
expect(res.status).toBe(403);
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ const jwt = require('jsonwebtoken');
|
|||||||
const db = require('../../backend/database');
|
const db = require('../../backend/database');
|
||||||
|
|
||||||
function getAuthToken() {
|
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() {
|
function getAuthHeader() {
|
||||||
|
|||||||
Reference in New Issue
Block a user