From fd72c0d7eec5c6aa1eaeec199f8342167f8209d6 Mon Sep 17 00:00:00 2001 From: cottongin Date: Mon, 23 Mar 2026 03:46:09 -0400 Subject: [PATCH] feat: add admin config loader with multi-key support Made-with: Cursor --- .gitignore | 3 ++ backend/config/admins.example.json | 4 ++ backend/config/load-admins.js | 55 ++++++++++++++++++++ tests/api/named-admins.test.js | 80 ++++++++++++++++++++++++++++++ 4 files changed, 142 insertions(+) create mode 100644 backend/config/admins.example.json create mode 100644 backend/config/load-admins.js create mode 100644 tests/api/named-admins.test.js diff --git a/.gitignore b/.gitignore index 39f43f1..98f58d5 100644 --- a/.gitignore +++ b/.gitignore @@ -39,6 +39,9 @@ Thumbs.db .local/ .old-chrome-extension/ +# Admin config (real keys) +backend/config/admins.json + # Cursor .cursor/ chat-summaries/ diff --git a/backend/config/admins.example.json b/backend/config/admins.example.json new file mode 100644 index 0000000..96df42d --- /dev/null +++ b/backend/config/admins.example.json @@ -0,0 +1,4 @@ +[ + { "name": "Alice", "key": "change-me-alice-key" }, + { "name": "Bob", "key": "change-me-bob-key" } +] diff --git a/backend/config/load-admins.js b/backend/config/load-admins.js new file mode 100644 index 0000000..40e6cc3 --- /dev/null +++ b/backend/config/load-admins.js @@ -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 }; diff --git a/tests/api/named-admins.test.js b/tests/api/named-admins.test.js new file mode 100644 index 0000000..a5cb41d --- /dev/null +++ b/tests/api/named-admins.test.js @@ -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); + }); +});