* initial change * nav highlighting * icon change * deeeedoooop * text * show version number on all pages * icon * remove unused code * add knip * formatting * remove unused code * types fix * remove notion completely from the codebase. * update test for new url structure * clean up the fucking shop boys * make a "create job" factory and use that * moar factories * formatting
172 lines
5.2 KiB
TypeScript
172 lines
5.2 KiB
TypeScript
import type { SettingKey } from "@server/repositories/settings";
|
|
import * as settingsRepo from "@server/repositories/settings";
|
|
|
|
const envDefaults: Record<string, string | undefined> = { ...process.env };
|
|
|
|
const readableStringConfig: { settingKey: SettingKey; envKey: string }[] = [
|
|
{ settingKey: "llmProvider", envKey: "LLM_PROVIDER" },
|
|
{ settingKey: "llmBaseUrl", envKey: "LLM_BASE_URL" },
|
|
{ settingKey: "rxresumeEmail", envKey: "RXRESUME_EMAIL" },
|
|
{ settingKey: "ukvisajobsEmail", envKey: "UKVISAJOBS_EMAIL" },
|
|
{ settingKey: "basicAuthUser", envKey: "BASIC_AUTH_USER" },
|
|
];
|
|
|
|
const readableBooleanConfig: {
|
|
settingKey: SettingKey;
|
|
envKey: string;
|
|
defaultValue: boolean;
|
|
}[] = [];
|
|
|
|
const privateStringConfig: {
|
|
settingKey: SettingKey;
|
|
envKey: string;
|
|
hintKey: string;
|
|
}[] = [
|
|
{
|
|
settingKey: "llmApiKey",
|
|
envKey: "LLM_API_KEY",
|
|
hintKey: "llmApiKeyHint",
|
|
},
|
|
{
|
|
settingKey: "rxresumePassword",
|
|
envKey: "RXRESUME_PASSWORD",
|
|
hintKey: "rxresumePasswordHint",
|
|
},
|
|
{
|
|
settingKey: "ukvisajobsPassword",
|
|
envKey: "UKVISAJOBS_PASSWORD",
|
|
hintKey: "ukvisajobsPasswordHint",
|
|
},
|
|
{
|
|
settingKey: "basicAuthPassword",
|
|
envKey: "BASIC_AUTH_PASSWORD",
|
|
hintKey: "basicAuthPasswordHint",
|
|
},
|
|
{
|
|
settingKey: "webhookSecret",
|
|
envKey: "WEBHOOK_SECRET",
|
|
hintKey: "webhookSecretHint",
|
|
},
|
|
];
|
|
|
|
export function normalizeEnvInput(
|
|
value: string | null | undefined,
|
|
): string | null {
|
|
const trimmed = value?.trim();
|
|
return trimmed ? trimmed : null;
|
|
}
|
|
|
|
function parseEnvBoolean(
|
|
raw: string | null | undefined,
|
|
defaultValue: boolean,
|
|
): boolean {
|
|
if (raw === undefined || raw === null || raw === "") return defaultValue;
|
|
if (raw === "false" || raw === "0") return false;
|
|
return true;
|
|
}
|
|
|
|
export function applyEnvValue(envKey: string, value: string | null): void {
|
|
if (value === null) {
|
|
const fallback = envDefaults[envKey];
|
|
if (fallback === undefined) {
|
|
delete process.env[envKey];
|
|
} else {
|
|
process.env[envKey] = fallback;
|
|
}
|
|
return;
|
|
}
|
|
|
|
process.env[envKey] = value;
|
|
}
|
|
|
|
export function serializeEnvBoolean(value: boolean | null): string | null {
|
|
if (value === null) return null;
|
|
return value ? "true" : "false";
|
|
}
|
|
|
|
export async function applyStoredEnvOverrides(): Promise<void> {
|
|
const safeGetSetting = async (key: SettingKey): Promise<string | null> => {
|
|
try {
|
|
return await settingsRepo.getSetting(key);
|
|
} catch (error) {
|
|
// In some test harnesses or first-boot scenarios, the DB may exist but not yet
|
|
// have the settings table. Treat this as "no overrides".
|
|
const msg = String((error as Error)?.message ?? error);
|
|
if (msg.includes("no such table") && msg.includes("settings"))
|
|
return null;
|
|
throw error;
|
|
}
|
|
};
|
|
|
|
await Promise.all([
|
|
...readableStringConfig.map(async ({ settingKey, envKey }) => {
|
|
const override = await safeGetSetting(settingKey);
|
|
if (override === null) return;
|
|
applyEnvValue(envKey, normalizeEnvInput(override));
|
|
}),
|
|
...readableBooleanConfig.map(
|
|
async ({ settingKey, envKey, defaultValue }) => {
|
|
const override = await safeGetSetting(settingKey);
|
|
if (override === null) return;
|
|
const parsed = parseEnvBoolean(override, defaultValue);
|
|
applyEnvValue(envKey, serializeEnvBoolean(parsed));
|
|
},
|
|
),
|
|
...privateStringConfig.map(async ({ settingKey, envKey }) => {
|
|
const override = await safeGetSetting(settingKey);
|
|
if (override === null) return;
|
|
applyEnvValue(envKey, normalizeEnvInput(override));
|
|
}),
|
|
]);
|
|
}
|
|
|
|
export async function getEnvSettingsData(
|
|
overrides?: Partial<Record<SettingKey, string>>,
|
|
): Promise<Record<string, string | boolean | number | null>> {
|
|
const activeOverrides = overrides || (await settingsRepo.getAllSettings());
|
|
const readableValues: Record<string, string | boolean | null> = {};
|
|
const privateValues: Record<string, string | null> = {};
|
|
|
|
for (const { settingKey, envKey } of readableStringConfig) {
|
|
const override = activeOverrides[settingKey] ?? null;
|
|
const rawValue = override ?? process.env[envKey];
|
|
readableValues[settingKey] = normalizeEnvInput(rawValue);
|
|
}
|
|
|
|
for (const { settingKey, envKey, defaultValue } of readableBooleanConfig) {
|
|
const override = activeOverrides[settingKey] ?? null;
|
|
const rawValue = override ?? process.env[envKey];
|
|
readableValues[settingKey] = parseEnvBoolean(rawValue, defaultValue);
|
|
}
|
|
|
|
for (const { settingKey, envKey, hintKey } of privateStringConfig) {
|
|
const override = activeOverrides[settingKey] ?? null;
|
|
const rawValue = override ?? process.env[envKey];
|
|
if (!rawValue) {
|
|
privateValues[hintKey] = null;
|
|
continue;
|
|
}
|
|
|
|
const hintLength =
|
|
rawValue.length > 4 ? 4 : Math.max(rawValue.length - 1, 1);
|
|
privateValues[hintKey] = rawValue.slice(0, hintLength);
|
|
}
|
|
|
|
const basicAuthUser =
|
|
activeOverrides.basicAuthUser ?? process.env.BASIC_AUTH_USER;
|
|
const basicAuthPassword =
|
|
activeOverrides.basicAuthPassword ?? process.env.BASIC_AUTH_PASSWORD;
|
|
|
|
return {
|
|
...readableValues,
|
|
...privateValues,
|
|
basicAuthActive: Boolean(basicAuthUser && basicAuthPassword),
|
|
};
|
|
}
|
|
|
|
export const envSettingConfig = {
|
|
readableStringConfig,
|
|
readableBooleanConfig,
|
|
privateStringConfig,
|
|
};
|