mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-06-19 13:21:46 +00:00
feat(admin): add admin-configurable default user settings
Allow admins to set instance-wide defaults for temperature unit, color mode, time format, route calculation, blur booking codes, and map tile URL via a new Admin > User Defaults tab. Defaults are stored in app_settings (prefixed default_user_setting_*) and applied at read time as a fallback — user's own explicit values always take priority. Translations added for all 16 supported languages.
This commit is contained in:
@@ -3,6 +3,7 @@ import { authenticate, adminOnly } from '../middleware/auth';
|
||||
import { AuthRequest } from '../types';
|
||||
import { writeAudit, getClientIp, logInfo } from '../services/auditLog';
|
||||
import * as svc from '../services/adminService';
|
||||
import { getAdminUserDefaults, setAdminUserDefaults } from '../services/settingsService';
|
||||
import { invalidateMcpSessions } from '../mcp';
|
||||
import { getPreferencesMatrix, setAdminPreferences } from '../services/notificationPreferencesService';
|
||||
|
||||
@@ -346,6 +347,31 @@ router.post('/rotate-jwt-secret', (req: Request, res: Response) => {
|
||||
res.json({ success: true });
|
||||
});
|
||||
|
||||
// ── Default User Settings ──────────────────────────────────────────────────────
|
||||
|
||||
router.get('/default-user-settings', (_req: Request, res: Response) => {
|
||||
res.json(getAdminUserDefaults());
|
||||
});
|
||||
|
||||
router.put('/default-user-settings', (req: Request, res: Response) => {
|
||||
if (!req.body || typeof req.body !== 'object' || Array.isArray(req.body)) {
|
||||
return res.status(400).json({ error: 'Object body required' });
|
||||
}
|
||||
try {
|
||||
setAdminUserDefaults(req.body);
|
||||
const authReq = req as AuthRequest;
|
||||
writeAudit({
|
||||
userId: authReq.user.id,
|
||||
action: 'admin.default_user_settings_update',
|
||||
ip: getClientIp(req),
|
||||
details: req.body,
|
||||
});
|
||||
res.json(getAdminUserDefaults());
|
||||
} catch (err: any) {
|
||||
res.status(400).json({ error: err.message });
|
||||
}
|
||||
});
|
||||
|
||||
// ── Dev-only: test notification endpoints ──────────────────────────────────────
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
const { send } = require('../services/notificationService');
|
||||
|
||||
@@ -3,21 +3,99 @@ import { maybe_encrypt_api_key } from './apiKeyCrypto';
|
||||
|
||||
const ENCRYPTED_SETTING_KEYS = new Set(['webhook_url', 'ntfy_token']);
|
||||
|
||||
export const DEFAULTABLE_USER_SETTING_KEYS = [
|
||||
'temperature_unit',
|
||||
'dark_mode',
|
||||
'time_format',
|
||||
'route_calculation',
|
||||
'blur_booking_codes',
|
||||
'map_tile_url',
|
||||
] as const;
|
||||
|
||||
type DefaultableKey = typeof DEFAULTABLE_USER_SETTING_KEYS[number];
|
||||
|
||||
const VALID_VALUES: Partial<Record<DefaultableKey, unknown[]>> = {
|
||||
temperature_unit: ['fahrenheit', 'celsius'],
|
||||
time_format: ['12h', '24h'],
|
||||
dark_mode: [true, false, 'light', 'dark', 'auto'],
|
||||
};
|
||||
|
||||
const BOOLEAN_KEYS = new Set<DefaultableKey>(['route_calculation', 'blur_booking_codes']);
|
||||
|
||||
function parseValue(raw: string): unknown {
|
||||
try { return JSON.parse(raw); } catch { return raw; }
|
||||
}
|
||||
|
||||
export function getAdminUserDefaults(): Record<string, unknown> {
|
||||
const rows = db.prepare(
|
||||
"SELECT key, value FROM app_settings WHERE key LIKE 'default_user_setting_%'"
|
||||
).all() as { key: string; value: string }[];
|
||||
const defaults: Record<string, unknown> = {};
|
||||
for (const row of rows) {
|
||||
const settingKey = row.key.slice('default_user_setting_'.length);
|
||||
defaults[settingKey] = parseValue(row.value);
|
||||
}
|
||||
return defaults;
|
||||
}
|
||||
|
||||
export function setAdminUserDefaults(partial: Record<string, unknown>): void {
|
||||
const upsert = db.prepare(
|
||||
`INSERT INTO app_settings (key, value) VALUES (?, ?)
|
||||
ON CONFLICT(key) DO UPDATE SET value = excluded.value`
|
||||
);
|
||||
const del = db.prepare("DELETE FROM app_settings WHERE key = ?");
|
||||
|
||||
db.exec('BEGIN');
|
||||
try {
|
||||
for (const [key, value] of Object.entries(partial)) {
|
||||
if (!(DEFAULTABLE_USER_SETTING_KEYS as readonly string[]).includes(key)) {
|
||||
throw new Error(`Invalid setting key: ${key}`);
|
||||
}
|
||||
const typedKey = key as DefaultableKey;
|
||||
const appKey = `default_user_setting_${key}`;
|
||||
|
||||
// null/undefined means "reset to built-in default" — delete the row
|
||||
if (value === null || value === undefined) {
|
||||
del.run(appKey);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (BOOLEAN_KEYS.has(typedKey) && typeof value !== 'boolean') {
|
||||
throw new Error(`Setting ${key} must be a boolean`);
|
||||
}
|
||||
const allowed = VALID_VALUES[typedKey];
|
||||
if (allowed && !allowed.includes(value)) {
|
||||
throw new Error(`Invalid value for ${key}: ${value}`);
|
||||
}
|
||||
|
||||
upsert.run(appKey, JSON.stringify(value));
|
||||
}
|
||||
db.exec('COMMIT');
|
||||
} catch (err) {
|
||||
db.exec('ROLLBACK');
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
export function getUserSettings(userId: number): Record<string, unknown> {
|
||||
const adminDefaults = getAdminUserDefaults();
|
||||
|
||||
const rows = db.prepare('SELECT key, value FROM settings WHERE user_id = ?').all(userId) as { key: string; value: string }[];
|
||||
const settings: Record<string, unknown> = {};
|
||||
const userSettings: Record<string, unknown> = {};
|
||||
for (const row of rows) {
|
||||
if (ENCRYPTED_SETTING_KEYS.has(row.key)) {
|
||||
settings[row.key] = row.value ? '••••••••' : '';
|
||||
userSettings[row.key] = row.value ? '••••••••' : '';
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
settings[row.key] = JSON.parse(row.value);
|
||||
userSettings[row.key] = JSON.parse(row.value);
|
||||
} catch {
|
||||
settings[row.key] = row.value;
|
||||
userSettings[row.key] = row.value;
|
||||
}
|
||||
}
|
||||
return settings;
|
||||
|
||||
// Admin defaults fill in only for keys the user hasn't explicitly set
|
||||
return { ...adminDefaults, ...userSettings };
|
||||
}
|
||||
|
||||
function serializeValue(key: string, value: unknown): string {
|
||||
|
||||
Reference in New Issue
Block a user