@cocalc/server
Version:
CoCalc server functionality: functions used by either the hub and the next.js server
128 lines (114 loc) • 4.35 kB
text/typescript
/*
* This file is part of CoCalc: Copyright © 2021 Sagemath, Inc.
* License: AGPLv3 s.t. "Commons Clause" – see LICENSE.md for details
*/
import LRU from "lru-cache";
import {
AllSiteSettingsCached as ServerSettings,
AllSiteSettingsKeys,
} from "@cocalc/util/db-schema/types";
export type { ServerSettings };
import { EXTRAS } from "@cocalc/util/db-schema/site-settings-extras";
import { site_settings_conf as CONF } from "@cocalc/util/schema";
import { SERVER_SETTINGS_ENV_PREFIX } from "@cocalc/util/consts";
import getPool from "@cocalc/database/pool";
import { callback2 as cb2 } from "@cocalc/util/async-utils";
import type { PostgreSQL } from "@cocalc/database/postgres/types";
import getLogger from "@cocalc/backend/logger";
import { PassportStrategyDB } from "@cocalc/server/auth/sso/types";
const L = getLogger("server:server-settings");
// We're just using this to cache this result for a **few seconds**.
const CACHE_TIME_SECONDS = process.env.NODE_ENV == "development" ? 3 : 15;
type CacheKeys = "server-settings" | "passports";
// TODO add something for the passports data type?
const cache = new LRU<CacheKeys, ServerSettings | PassportStrategyDB[]>({
max: 10,
maxAge: 1000 * CACHE_TIME_SECONDS,
});
const KEY: CacheKeys = "server-settings";
export function resetServerSettingsCache() {
cache.reset();
}
export function getPassportsCached(): PassportStrategyDB[] | undefined {
return cache.get("passports") as PassportStrategyDB[] | undefined;
}
export function setPassportsCached(val: PassportStrategyDB[]) {
return cache.set("passports", val);
}
export async function getServerSettings(): Promise<ServerSettings> {
if (cache.has(KEY)) {
return cache.get(KEY)! as ServerSettings; // can't be null
}
const pool = getPool();
const { rows } = await pool.query("SELECT name, value FROM server_settings");
const settings: ServerSettings = { _timestamp: Date.now() };
const raw: { [key in AllSiteSettingsKeys]?: string } = {};
for (const row of rows) {
raw[row.name] = row.value;
}
// process values, including any post-processing.
for (const row of rows) {
const { name, value } = row;
const spec = CONF[name] ?? EXTRAS[name];
// we only process values we know
if (spec == null) continue;
const toVal = spec.to_val;
settings[name] = toVal != null ? toVal(value, raw) : value;
}
// set default values for missing keys
for (const config of [EXTRAS, CONF]) {
for (const key in config) {
if (settings[key] == null) {
const spec = config[key];
settings[key] =
spec?.to_val != null ? spec.to_val(spec.default, raw) : spec.default;
}
}
}
cache.set(KEY, settings);
return settings;
}
/*
This stores environment variables for server settings in the DB to make the life of an admin easier.
e.g. COCALC_SETTING_DNS, COCALC_SETTING_EMAIL_SMTP_SERVER, COCALC_SETTING_EMAIL_SMTP_PASSWORD, ...
Loaded once at startup, right after configuring the db schema, see hub/hub.ts.
*/
export async function load_server_settings_from_env(
db: PostgreSQL
): Promise<void> {
const PREFIX = SERVER_SETTINGS_ENV_PREFIX;
// reset all readonly values
await db.async_query({
query: "UPDATE server_settings",
set: { readonly: false },
where: ["1=1"], // otherwise there is an exception about not restricting the query
});
// now, check if there are any we know of
for (const config of [EXTRAS, CONF]) {
for (const key in config) {
const envvar = `${PREFIX}_${key.toUpperCase()}`;
const envval = process.env[envvar];
if (envval == null) continue;
// ATTN do not expose the value, could be a password
L.debug(`picking up $${envvar} and saving it in the database`);
// check validity
const valid = (CONF[key] ?? EXTRAS[key])?.valid;
if (valid != null) {
if (Array.isArray(valid) && !valid.includes(envval)) {
throw new Error(
`The value of $${envvar} is invalid. allowed are ${valid}.`
);
} else if (typeof valid == "function" && !valid(envval)) {
throw new Error(
`The validation function rejected the value of $${envvar}.`
);
}
}
await cb2(db.set_server_setting, {
name: key,
value: envval,
readonly: true,
});
}
}
}