Initial commit: courtlab tactical board with team management and MP4 export
This commit is contained in:
@@ -0,0 +1,96 @@
|
||||
import path from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { readFile } from 'node:fs/promises';
|
||||
|
||||
const here = path.dirname(fileURLToPath(import.meta.url));
|
||||
const projectRoot = path.resolve(here, '..');
|
||||
export const CONFIG_FILENAME = '.config.json';
|
||||
|
||||
function invalid(filePath, detail) {
|
||||
throw new Error(`Invalid configuration in ${filePath}: ${detail}`);
|
||||
}
|
||||
|
||||
function requireObject(value, field, filePath) {
|
||||
if (!value || typeof value !== 'object' || Array.isArray(value)) invalid(filePath, `${field} must be an object`);
|
||||
return value;
|
||||
}
|
||||
|
||||
function requireString(value, field, filePath) {
|
||||
if (typeof value !== 'string' || !value.trim()) invalid(filePath, `${field} must be a non-empty string`);
|
||||
return value.trim();
|
||||
}
|
||||
|
||||
function requirePassword(value, field, filePath) {
|
||||
if (typeof value !== 'string') invalid(filePath, `${field} must be a string`);
|
||||
return value;
|
||||
}
|
||||
|
||||
function isDevelopmentOrigin(origin) {
|
||||
let url;
|
||||
try { url = new URL(origin); } catch { return false; }
|
||||
if (url.protocol !== 'http:') return false;
|
||||
const hostname = url.hostname.toLowerCase();
|
||||
if (hostname === 'localhost' || hostname === '127.0.0.1') return true;
|
||||
const octets = hostname.split('.').map((part) => Number(part));
|
||||
if (octets.length !== 4 || octets.some((part) => !Number.isInteger(part) || part < 0 || part > 255)) return false;
|
||||
return octets[0] === 10
|
||||
|| (octets[0] === 192 && octets[1] === 168)
|
||||
|| (octets[0] === 172 && octets[1] >= 16 && octets[1] <= 31);
|
||||
}
|
||||
|
||||
function validateConfig(value, filePath) {
|
||||
const root = requireObject(value, 'root', filePath);
|
||||
const mongodb = requireObject(root.mongodb, 'mongodb', filePath);
|
||||
const server = requireObject(root.server, 'server', filePath);
|
||||
const bootstrap = requireObject(root.bootstrap, 'bootstrap', filePath);
|
||||
const qa = requireObject(root.qa, 'qa', filePath);
|
||||
const uri = requireString(mongodb.uri, 'mongodb.uri', filePath);
|
||||
const db = requireString(mongodb.db, 'mongodb.db', filePath);
|
||||
const host = requireString(server.host, 'server.host', filePath);
|
||||
if (!Number.isInteger(server.port) || server.port < 1 || server.port > 65_535) invalid(filePath, 'server.port must be an integer between 1 and 65535');
|
||||
if (server.mode !== 'development' && server.mode !== 'production') invalid(filePath, 'server.mode must be development or production');
|
||||
if (!Array.isArray(server.allowedOrigins) || server.allowedOrigins.some((origin) => typeof origin !== 'string' || !origin.trim())) invalid(filePath, 'server.allowedOrigins must be an array of non-empty strings');
|
||||
const allowedOrigins = server.allowedOrigins.map((origin) => origin.trim());
|
||||
if (typeof server.secureCookie !== 'boolean') invalid(filePath, 'server.secureCookie must be a boolean');
|
||||
const bootstrapPassword = requirePassword(bootstrap.password, 'bootstrap.password', filePath);
|
||||
const qaDb = requireString(qa.db, 'qa.db', filePath);
|
||||
const qaPassword = requirePassword(qa.password, 'qa.password', filePath);
|
||||
const production = server.mode === 'production';
|
||||
return {
|
||||
mongodb: { uri, db },
|
||||
server: {
|
||||
host,
|
||||
port: server.port,
|
||||
mode: server.mode,
|
||||
allowedOrigins: production ? allowedOrigins.filter((origin) => !isDevelopmentOrigin(origin)) : allowedOrigins,
|
||||
secureCookie: production || server.secureCookie,
|
||||
},
|
||||
bootstrap: { password: bootstrapPassword },
|
||||
qa: { db: qaDb, password: qaPassword },
|
||||
};
|
||||
}
|
||||
|
||||
export function resolveConfigPath(configPath = CONFIG_FILENAME, root = projectRoot) {
|
||||
return path.isAbsolute(configPath) ? configPath : path.resolve(root, configPath);
|
||||
}
|
||||
|
||||
export async function loadConfig(options = {}) {
|
||||
const requestedPath = typeof options === 'string' ? options : options.path || options.filePath || CONFIG_FILENAME;
|
||||
const filePath = resolveConfigPath(requestedPath, typeof options === 'object' ? options.projectRoot || projectRoot : projectRoot);
|
||||
let source;
|
||||
try {
|
||||
source = await readFile(filePath, 'utf8');
|
||||
} catch (error) {
|
||||
if (error?.code === 'ENOENT') throw new Error(`Configuration file not found at ${filePath}. Copy .config.json.sample to .config.json and set the required values.`);
|
||||
throw new Error(`Unable to read configuration file ${filePath}: ${error?.code || 'unknown error'}`);
|
||||
}
|
||||
let parsed;
|
||||
try {
|
||||
parsed = JSON.parse(source.charCodeAt(0) === 0xFEFF ? source.slice(1) : source);
|
||||
} catch {
|
||||
throw new Error(`Invalid JSON in configuration file ${filePath}; check its JSON syntax.`);
|
||||
}
|
||||
return validateConfig(parsed, filePath);
|
||||
}
|
||||
|
||||
export { isDevelopmentOrigin, projectRoot };
|
||||
Reference in New Issue
Block a user