UNPKG

@cocalc/server

Version:

CoCalc server functionality: functions used by either the hub and the next.js server

128 lines (114 loc) 4.35 kB
/* * 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, }); } } }