Path: blob/master/src/packages/database/settings/server-settings.ts
1503 views
/*1* This file is part of CoCalc: Copyright © 2021 Sagemath, Inc.2* License: MS-RSL – see LICENSE.md for details3*/45import LRU from "lru-cache";67import getLogger from "@cocalc/backend/logger";8import getPool from "@cocalc/database/pool";9import type { PostgreSQL } from "@cocalc/database/postgres/types";10import { PassportStrategyDB } from "@cocalc/database/settings/auth-sso-types";11import { callback2 as cb2 } from "@cocalc/util/async-utils";12import { SERVER_SETTINGS_ENV_PREFIX } from "@cocalc/util/consts";13import { EXTRAS } from "@cocalc/util/db-schema/site-settings-extras";14import {15AllSiteSettingsKeys,16AllSiteSettingsCached as ServerSettings,17} from "@cocalc/util/db-schema/types";18import { site_settings_conf as CONF } from "@cocalc/util/schema";19export type { ServerSettings };2021const L = getLogger("server:server-settings");2223// We're just using this to cache this result for a **few seconds**.24const CACHE_TIME_SECONDS = process.env.NODE_ENV == "development" ? 3 : 15;25type CacheKeys = "server-settings" | "passports";26// TODO add something for the passports data type?27const cache = new LRU<CacheKeys, ServerSettings | PassportStrategyDB[]>({28max: 10,29ttl: 1000 * CACHE_TIME_SECONDS,30});31const KEY: CacheKeys = "server-settings";3233export function resetServerSettingsCache() {34cache.clear();35}3637export function getPassportsCached(): PassportStrategyDB[] | undefined {38return cache.get("passports") as PassportStrategyDB[] | undefined;39}4041export function setPassportsCached(val: PassportStrategyDB[]) {42return cache.set("passports", val);43}4445export async function getServerSettings(): Promise<ServerSettings> {46if (cache.has(KEY)) {47return cache.get(KEY)! as ServerSettings; // can't be null48}49const pool = getPool();50const { rows } = await pool.query("SELECT name, value FROM server_settings");5152const settings: ServerSettings = { _timestamp: Date.now() };5354const raw: { [key in AllSiteSettingsKeys]?: string } = {};55for (const row of rows) {56raw[row.name] = row.value;57}5859// process values, including any post-processing.60for (const row of rows) {61const { name, value } = row;62const spec = CONF[name] ?? EXTRAS[name];63// we only process values we know64if (spec == null) continue;65const toVal = spec.to_val;66settings[name] = toVal != null ? toVal(value, raw) : value;67}68// set default values for missing keys69for (const config of [EXTRAS, CONF]) {70for (const key in config) {71if (settings[key] == null) {72const spec = config[key];73settings[key] =74spec?.to_val != null ? spec.to_val(spec.default, raw) : spec.default;75}76}77}7879cache.set(KEY, settings);80return settings;81}8283/*84This stores environment variables for server settings in the DB to make the life of an admin easier.85e.g. COCALC_SETTING_DNS, COCALC_SETTING_EMAIL_SMTP_SERVER, COCALC_SETTING_EMAIL_SMTP_PASSWORD, ...86Loaded once at startup, right after configuring the db schema, see hub/hub.ts.87*/88export async function load_server_settings_from_env(89db: PostgreSQL,90): Promise<void> {91const PREFIX = SERVER_SETTINGS_ENV_PREFIX;9293// if none of the envvars starts with the prefix, then we don't do anything94if (!Object.keys(process.env).some((k) => k.startsWith(PREFIX))) {95return;96}9798L.debug("load_server_settings_from_env variables prefixed by ", PREFIX);99// reset all readonly values100await db.async_query({101query: "UPDATE server_settings",102set: { readonly: false },103where: ["1=1"], // otherwise there is an exception about not restricting the query104});105// now, check if there are any we know of106for (const config of [EXTRAS, CONF]) {107for (const key in config) {108const envvar = `${PREFIX}_${key.toUpperCase()}`;109const envval = process.env[envvar];110if (envval == null) continue;111// ATTN do not expose the value, could be a password112L.debug(`picking up $${envvar} and saving it in the database`);113114// check validity115const valid = (CONF[key] ?? EXTRAS[key])?.valid;116if (valid != null) {117if (Array.isArray(valid) && !valid.includes(envval)) {118throw new Error(119`The value of $${envvar} is invalid. allowed are ${valid}.`,120);121} else if (typeof valid == "function" && !valid(envval)) {122throw new Error(123`The validation function rejected the value of $${envvar}.`,124);125}126}127128await cb2(db.set_server_setting, {129name: key,130value: envval,131readonly: true,132});133}134}135}136137138