feat(auth): owner-only middleware for single-user bearer auth
This commit is contained in:
17
lib/auth/owner.js
Normal file
17
lib/auth/owner.js
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
export function ownerOnly(req, res, next) {
|
||||||
|
const expected = process.env.OWNER_TOKEN;
|
||||||
|
if (!expected) {
|
||||||
|
return res.status(500).json({
|
||||||
|
error: { code: 'no_owner_token', message: 'OWNER_TOKEN not configured' }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
const auth = req.headers.authorization || '';
|
||||||
|
const [scheme, token] = auth.split(' ');
|
||||||
|
if (scheme !== 'Bearer' || token !== expected) {
|
||||||
|
return res.status(401).json({
|
||||||
|
error: { code: 'unauthorized', message: 'invalid token' }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
req.actor = { kind: 'user', id: null };
|
||||||
|
next();
|
||||||
|
}
|
||||||
47
tests/auth/owner.test.js
Normal file
47
tests/auth/owner.test.js
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||||
|
import { ownerOnly } from '../../lib/auth/owner.js';
|
||||||
|
|
||||||
|
function mockReq(token) {
|
||||||
|
return { headers: { authorization: token ? `Bearer ${token}` : undefined } };
|
||||||
|
}
|
||||||
|
function mockRes() {
|
||||||
|
const r = { status: vi.fn().mockReturnThis(), json: vi.fn().mockReturnThis(), end: vi.fn() };
|
||||||
|
return r;
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('ownerOnly middleware', () => {
|
||||||
|
beforeEach(() => { process.env.OWNER_TOKEN = 'test-token'; });
|
||||||
|
|
||||||
|
it('rejects missing token', () => {
|
||||||
|
const res = mockRes();
|
||||||
|
const next = vi.fn();
|
||||||
|
ownerOnly(mockReq(null), res, next);
|
||||||
|
expect(res.status).toHaveBeenCalledWith(401);
|
||||||
|
expect(next).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('rejects wrong token', () => {
|
||||||
|
const res = mockRes();
|
||||||
|
const next = vi.fn();
|
||||||
|
ownerOnly(mockReq('wrong'), res, next);
|
||||||
|
expect(res.status).toHaveBeenCalledWith(401);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('accepts correct token + attaches actor', () => {
|
||||||
|
const res = mockRes();
|
||||||
|
const next = vi.fn();
|
||||||
|
const req = mockReq('test-token');
|
||||||
|
ownerOnly(req, res, next);
|
||||||
|
expect(next).toHaveBeenCalled();
|
||||||
|
expect(req.actor).toEqual({ kind: 'user', id: null });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns 500 when OWNER_TOKEN unset', () => {
|
||||||
|
delete process.env.OWNER_TOKEN;
|
||||||
|
const res = mockRes();
|
||||||
|
const next = vi.fn();
|
||||||
|
ownerOnly(mockReq('anything'), res, next);
|
||||||
|
expect(res.status).toHaveBeenCalledWith(500);
|
||||||
|
expect(next).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user