- server/policy_overrides.mjs: persistent group/action override store - Group toggle: disabling a group forces all its actions to auto-deny without touching individual overrides; re-enabling restores them intact - Action overrides: cycle auto-accept/queue/auto-deny/clear per action - Effective policy resolution in index.mjs: group > action override > hardcoded default - auth.mjs: add check_can_manage_policies (requires canApprove.length > 0) - Three new endpoints: GET /policies, POST /policies/group/:group, POST /policies/action/:action - client/conduit.mjs: get_policies, set_group_policy, set_action_policy - ccc-queue.mjs: policy view (Tab to switch); group/action panel with space/e/←/→ - actions.mjs: group fields on desktop/email/calendar actions; list-actions includes group - config.example.json: add policy_overrides key - Bump version to 1.3.0 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
75 lines
2.0 KiB
JavaScript
75 lines
2.0 KiB
JavaScript
import { readFileSync } from 'fs';
|
|
import { resolve } from 'path';
|
|
|
|
export function load_config(file_path) {
|
|
if (!file_path) {
|
|
throw new Error('--config <path> is required');
|
|
}
|
|
let raw;
|
|
try {
|
|
raw = readFileSync(file_path, 'utf8');
|
|
} catch (err) {
|
|
throw new Error(`Cannot read config file at ${file_path}: ${err.message}`);
|
|
}
|
|
let parsed;
|
|
try {
|
|
parsed = JSON.parse(raw);
|
|
} catch (err) {
|
|
throw new Error(`Config file is not valid JSON: ${err.message}`);
|
|
}
|
|
|
|
if (!parsed.secrets) {
|
|
throw new Error("Config file must have a 'secrets' field pointing to the secrets file");
|
|
}
|
|
|
|
// Resolve secrets path relative to the config file's directory
|
|
const config_dir = resolve(file_path, '..');
|
|
const secrets_path = resolve(config_dir, parsed.secrets);
|
|
|
|
let secrets_raw;
|
|
try {
|
|
secrets_raw = readFileSync(secrets_path, 'utf8');
|
|
} catch (err) {
|
|
throw new Error(`Cannot read secrets file at ${secrets_path}: ${err.message}`);
|
|
}
|
|
let secrets;
|
|
try {
|
|
secrets = JSON.parse(secrets_raw);
|
|
} catch (err) {
|
|
throw new Error(`Secrets file is not valid JSON: ${err.message}`);
|
|
}
|
|
if (!secrets.users || typeof secrets.users !== 'object') {
|
|
throw new Error("Secrets file must have a top-level 'users' object");
|
|
}
|
|
|
|
const mail_perms_path = parsed.mail_perms
|
|
? resolve(config_dir, parsed.mail_perms)
|
|
: null;
|
|
|
|
const policy_overrides_path = parsed.policy_overrides
|
|
? resolve(config_dir, parsed.policy_overrides)
|
|
: null;
|
|
|
|
let google_calendar = null;
|
|
if (parsed.google_calendar) {
|
|
const { credentials, token } = parsed.google_calendar;
|
|
if (!credentials || !token) {
|
|
throw new Error("Config 'google_calendar' must have 'credentials' and 'token' fields");
|
|
}
|
|
google_calendar = {
|
|
credentials_path: resolve(config_dir, credentials),
|
|
token_path: resolve(config_dir, token),
|
|
};
|
|
}
|
|
|
|
return {
|
|
users: secrets.users,
|
|
smtp: parsed.smtp ?? null,
|
|
mail_perms_path,
|
|
policy_overrides_path,
|
|
google_calendar,
|
|
bind: parsed.bind ?? null,
|
|
port: parsed.port ?? null,
|
|
};
|
|
}
|