feat: add admin config loader with multi-key support
Made-with: Cursor
This commit is contained in:
3
.gitignore
vendored
3
.gitignore
vendored
@@ -39,6 +39,9 @@ Thumbs.db
|
|||||||
.local/
|
.local/
|
||||||
.old-chrome-extension/
|
.old-chrome-extension/
|
||||||
|
|
||||||
|
# Admin config (real keys)
|
||||||
|
backend/config/admins.json
|
||||||
|
|
||||||
# Cursor
|
# Cursor
|
||||||
.cursor/
|
.cursor/
|
||||||
chat-summaries/
|
chat-summaries/
|
||||||
|
|||||||
4
backend/config/admins.example.json
Normal file
4
backend/config/admins.example.json
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
[
|
||||||
|
{ "name": "Alice", "key": "change-me-alice-key" },
|
||||||
|
{ "name": "Bob", "key": "change-me-bob-key" }
|
||||||
|
]
|
||||||
55
backend/config/load-admins.js
Normal file
55
backend/config/load-admins.js
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
|
||||||
|
const DEFAULT_CONFIG_PATH = path.join(__dirname, 'admins.json');
|
||||||
|
|
||||||
|
function loadAdmins() {
|
||||||
|
const configPath = process.env.ADMIN_CONFIG_PATH || DEFAULT_CONFIG_PATH;
|
||||||
|
|
||||||
|
if (fs.existsSync(configPath)) {
|
||||||
|
const raw = fs.readFileSync(configPath, 'utf-8');
|
||||||
|
const admins = JSON.parse(raw);
|
||||||
|
|
||||||
|
if (!Array.isArray(admins) || admins.length === 0) {
|
||||||
|
throw new Error(`Admin config at ${configPath} must be a non-empty array`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const names = new Set();
|
||||||
|
const keys = new Set();
|
||||||
|
|
||||||
|
for (const admin of admins) {
|
||||||
|
if (!admin.name || !admin.key) {
|
||||||
|
throw new Error('Each admin must have a "name" and "key" property');
|
||||||
|
}
|
||||||
|
if (names.has(admin.name)) {
|
||||||
|
throw new Error(`Duplicate admin name: ${admin.name}`);
|
||||||
|
}
|
||||||
|
if (keys.has(admin.key)) {
|
||||||
|
throw new Error(`Duplicate admin key found`);
|
||||||
|
}
|
||||||
|
names.add(admin.name);
|
||||||
|
keys.add(admin.key);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`[Auth] Loaded ${admins.length} admin(s) from ${configPath}`);
|
||||||
|
return admins;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (process.env.ADMIN_KEY) {
|
||||||
|
console.log('[Auth] No admins config file found, falling back to ADMIN_KEY env var');
|
||||||
|
return [{ name: 'Admin', key: process.env.ADMIN_KEY }];
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error(
|
||||||
|
'No admin configuration found. Provide backend/config/admins.json or set ADMIN_KEY env var.'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const admins = loadAdmins();
|
||||||
|
|
||||||
|
function findAdminByKey(key) {
|
||||||
|
const match = admins.find(a => a.key === key);
|
||||||
|
return match ? { name: match.name } : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = { findAdminByKey, admins };
|
||||||
80
tests/api/named-admins.test.js
Normal file
80
tests/api/named-admins.test.js
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
const path = require('path');
|
||||||
|
const fs = require('fs');
|
||||||
|
const os = require('os');
|
||||||
|
|
||||||
|
describe('load-admins', () => {
|
||||||
|
const originalEnv = { ...process.env };
|
||||||
|
let tmpDir;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'admins-test-'));
|
||||||
|
delete process.env.ADMIN_CONFIG_PATH;
|
||||||
|
delete process.env.ADMIN_KEY;
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
process.env = { ...originalEnv };
|
||||||
|
fs.rmSync(tmpDir, { recursive: true, force: true });
|
||||||
|
jest.resetModules();
|
||||||
|
});
|
||||||
|
|
||||||
|
function writeConfig(admins) {
|
||||||
|
const filePath = path.join(tmpDir, 'admins.json');
|
||||||
|
fs.writeFileSync(filePath, JSON.stringify(admins));
|
||||||
|
return filePath;
|
||||||
|
}
|
||||||
|
|
||||||
|
test('loads admins from ADMIN_CONFIG_PATH', () => {
|
||||||
|
const configPath = writeConfig([
|
||||||
|
{ name: 'Alice', key: 'key-a' },
|
||||||
|
{ name: 'Bob', key: 'key-b' }
|
||||||
|
]);
|
||||||
|
process.env.ADMIN_CONFIG_PATH = configPath;
|
||||||
|
|
||||||
|
const { findAdminByKey } = require('../../backend/config/load-admins');
|
||||||
|
expect(findAdminByKey('key-a')).toEqual({ name: 'Alice' });
|
||||||
|
expect(findAdminByKey('key-b')).toEqual({ name: 'Bob' });
|
||||||
|
expect(findAdminByKey('wrong')).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('falls back to ADMIN_KEY when no config file', () => {
|
||||||
|
process.env.ADMIN_CONFIG_PATH = path.join(tmpDir, 'nonexistent.json');
|
||||||
|
process.env.ADMIN_KEY = 'legacy-key';
|
||||||
|
|
||||||
|
const { findAdminByKey } = require('../../backend/config/load-admins');
|
||||||
|
expect(findAdminByKey('legacy-key')).toEqual({ name: 'Admin' });
|
||||||
|
expect(findAdminByKey('wrong')).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('throws when neither config file nor ADMIN_KEY exists', () => {
|
||||||
|
process.env.ADMIN_CONFIG_PATH = path.join(tmpDir, 'nonexistent.json');
|
||||||
|
|
||||||
|
expect(() => {
|
||||||
|
require('../../backend/config/load-admins');
|
||||||
|
}).toThrow();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('rejects duplicate admin names', () => {
|
||||||
|
const configPath = writeConfig([
|
||||||
|
{ name: 'Alice', key: 'key-a' },
|
||||||
|
{ name: 'Alice', key: 'key-b' }
|
||||||
|
]);
|
||||||
|
process.env.ADMIN_CONFIG_PATH = configPath;
|
||||||
|
|
||||||
|
expect(() => {
|
||||||
|
require('../../backend/config/load-admins');
|
||||||
|
}).toThrow(/duplicate/i);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('rejects duplicate keys', () => {
|
||||||
|
const configPath = writeConfig([
|
||||||
|
{ name: 'Alice', key: 'same-key' },
|
||||||
|
{ name: 'Bob', key: 'same-key' }
|
||||||
|
]);
|
||||||
|
process.env.ADMIN_CONFIG_PATH = configPath;
|
||||||
|
|
||||||
|
expect(() => {
|
||||||
|
require('../../backend/config/load-admins');
|
||||||
|
}).toThrow(/duplicate/i);
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user