Initial commit: courtlab tactical board with team management and MP4 export

This commit is contained in:
2026-09-08 12:22:20 +09:00
commit d2e39a452e
43 changed files with 7812 additions and 0 deletions
+96
View File
@@ -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 };