Spaces:
Sleeping
Sleeping
File size: 5,506 Bytes
57a889c | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 | import { db } from '../db/database';
import { decrypt_api_key, maybe_encrypt_api_key } from './apiKeyCrypto';
const ENCRYPTED_SETTING_KEYS = new Set(['webhook_url', 'ntfy_token', 'mapbox_access_token']);
// Encrypted keys that are masked (••••••••) when returned to the client.
// Keys not in this set but in ENCRYPTED_SETTING_KEYS are decrypted and returned.
const MASKED_SETTING_KEYS = new Set(['webhook_url', 'ntfy_token']);
export const DEFAULTABLE_USER_SETTING_KEYS = [
'temperature_unit',
'dark_mode',
'time_format',
'blur_booking_codes',
'map_tile_url',
// Instance-wide Mapbox defaults: an admin can set a shared token + style so the
// whole instance uses Mapbox without each user pasting their own key (#920).
'map_provider',
'mapbox_access_token',
'mapbox_style',
'mapbox_3d_enabled',
'mapbox_quality_mode',
] 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'],
map_provider: ['leaflet', 'mapbox-gl'],
};
const BOOLEAN_KEYS = new Set<DefaultableKey>(['blur_booking_codes', 'mapbox_3d_enabled', 'mapbox_quality_mode']);
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);
if (ENCRYPTED_SETTING_KEYS.has(settingKey)) {
defaults[settingKey] = row.value ? (decrypt_api_key(row.value) ?? '') : '';
} else {
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}`);
}
// Encrypt sensitive defaults (the shared Mapbox token) at rest, like the
// per-user equivalents; everything else is stored as plain JSON.
const stored = ENCRYPTED_SETTING_KEYS.has(key)
? (maybe_encrypt_api_key(String(value)) ?? String(value))
: JSON.stringify(value);
upsert.run(appKey, stored);
}
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 userSettings: Record<string, unknown> = {};
for (const row of rows) {
if (MASKED_SETTING_KEYS.has(row.key)) {
userSettings[row.key] = row.value ? '••••••••' : '';
continue;
}
if (ENCRYPTED_SETTING_KEYS.has(row.key)) {
userSettings[row.key] = row.value ? (decrypt_api_key(row.value) ?? '') : '';
continue;
}
try {
userSettings[row.key] = JSON.parse(row.value);
} catch {
userSettings[row.key] = row.value;
}
}
// Admin defaults fill in only for keys the user hasn't explicitly set
return { ...adminDefaults, ...userSettings };
}
function serializeValue(key: string, value: unknown): string {
const raw = typeof value === 'object' ? JSON.stringify(value) : String(value !== undefined ? value : '');
if (ENCRYPTED_SETTING_KEYS.has(key)) return maybe_encrypt_api_key(raw) ?? raw;
return raw;
}
export function upsertSetting(userId: number, key: string, value: unknown) {
db.prepare(`
INSERT INTO settings (user_id, key, value) VALUES (?, ?, ?)
ON CONFLICT(user_id, key) DO UPDATE SET value = excluded.value
`).run(userId, key, serializeValue(key, value));
}
export function bulkUpsertSettings(userId: number, settings: Record<string, unknown>) {
const upsert = db.prepare(`
INSERT INTO settings (user_id, key, value) VALUES (?, ?, ?)
ON CONFLICT(user_id, key) DO UPDATE SET value = excluded.value
`);
db.exec('BEGIN');
try {
for (const [key, value] of Object.entries(settings)) {
upsert.run(userId, key, serializeValue(key, value));
}
db.exec('COMMIT');
} catch (err) {
db.exec('ROLLBACK');
throw err;
}
return Object.keys(settings).length;
}
|