UNPKG

smc-hub

Version:

CoCalc: Backend webserver component

194 lines (174 loc) 6.1 kB
/* * This file is part of CoCalc: Copyright © 2020 Sagemath, Inc. * License: AGPLv3 s.t. "Commons Clause" – see LICENSE.md for details */ // This unifies the entire webapp configuration – endpoint /customize // The main goal is to optimize this, to use as little DB interactions // as necessary, use caching, etc. // This manages the webapp's configuration based on the hostname // (allows whitelabeling). import { parseDomain, ParseResultType } from "parse-domain"; import debug from "debug"; const L = debug("hub:webapp-config"); import { delay } from "awaiting"; import { callback2 as cb2 } from "smc-util/async-utils"; import { PostgreSQL } from "./postgres/types"; import { PassportManager, get_passport_manager } from "./auth"; import getServerSettings from "./servers/server-settings"; import { EXTRAS as SERVER_SETTINGS_EXTRAS } from "smc-util/db-schema/site-settings-extras"; import { site_settings_conf as SITE_SETTINGS_CONF } from "smc-util/schema"; import { have_active_registration_tokens } from "./utils"; import LRUCache from "lru-cache"; const CACHE = new LRUCache({ max: 10, maxAge: 3 * 60 * 1000 }); // 3 minutes export function clear_cache(): void { CACHE.reset(); } type Theme = { [key: string]: string | boolean }; interface Config { // todo configuration: any; registration: any; strategies: object; } async function get_passport_manager_async(): Promise<PassportManager> { // the only issue here is, that the http server already starts up before the // passport manager is configured – but, the passport manager depends on the http server // we just retry during that initial period of uncertainty… let ms = 100; while (true) { const pp_manager = get_passport_manager(); if (pp_manager != null) { return pp_manager; } else { L(`WARNING: Passport Manager not available yet -- trying again in ${ms}ms`); await delay(ms); ms = Math.min(10000, 1.3 * ms); } } } // these are special values, but can be overwritten by the specific theme const VANITY_HARDCODED = { allow_anonymous_sign_in: false, //kucalc: "onprem", // TODO: maybe do this, not sure //commercial: false, } as const; export class WebappConfiguration { private readonly db: PostgreSQL; private data?: any; private passport_manager: PassportManager; constructor({ db }) { this.db = db; this.init(); } private async init(): Promise<void> { // this.data.pub updates automatically – do not modify it! this.data = await getServerSettings(); this.passport_manager = await get_passport_manager_async(); } // server settings with whitelabeling settings // TODO post-process all values public async settings(vid: string) { const res = await cb2(this.db._query, { query: "SELECT id, settings FROM whitelabeling", cache: true, where: { "id = $::TEXT": vid }, }); if (this.data == null) { // settings not yet initialized return {}; } const data = res.rows[0]; if (data != null) { return { ...this.data.all, ...data.settings }; } else { return this.data.all; } } // derive the vanity ID from the host string private get_vanity_id(host: string): string | undefined { const host_parsed = parseDomain(host); if (host_parsed.type === ParseResultType.Listed) { // vanity for vanity.cocalc.com or foo.p for foo.p.cocalc.com return host_parsed.subDomains.join("."); } return undefined; } private async theme(vid: string): Promise<Theme> { const res = await cb2(this.db._query, { query: "SELECT id, theme FROM whitelabeling", cache: true, where: { "id = $::TEXT": vid }, }); const data = res.rows[0]; if (data != null) { // post-process data, but do not set default values… const theme: Theme = {}; for (const [key, value] of Object.entries(data.theme)) { const config = SITE_SETTINGS_CONF[key] ?? SERVER_SETTINGS_EXTRAS[key]; if (typeof config?.to_val == "function") { theme[key] = config.to_val(value); } else { if (typeof value == "string" || typeof value == "boolean") { theme[key] = value; } } } L(`vanity theme=${JSON.stringify(theme)}`); return theme; } else { L(`theme id=${vid} not found`); return {}; } } private async get_vanity(vid): Promise<object> { if (vid != null && vid !== "") { L(`vanity ID = "${vid}"`); return { ...VANITY_HARDCODED, ...(await this.theme(vid)), }; } else { return {}; } } // returns the global configuration + eventually vanity specific site config settings private async get_configuration({ host, country }) { if (this.data == null) { // settings not yet initialized return {}; } const vid = this.get_vanity_id(host); const config = this.data.pub; const vanity = this.get_vanity(vid); return { ...config, ...vanity, ...{ country, dns: host } }; } private get_strategies(): object { const key = "strategies"; let strategies = CACHE.get(key); if (strategies == null) { strategies = this.passport_manager.get_strategies_v2(); CACHE.set(key, strategies); } return strategies as object; } private async get_config({ country, host }): Promise<Config> { const [configuration, registration] = await Promise.all([ this.get_configuration({ host, country }), have_active_registration_tokens(this.db), ]); const strategies = this.get_strategies(); return { configuration, registration, strategies }; } // it returns a shallow copy, hence you can modify/add keys in the returned map! public async get({ country, host }): Promise<Config> { const key = `config::${country}::${host}`; let config = CACHE.get(key); if (config == null) { config = await this.get_config({ country, host }); CACHE.set(key, config); } else { L(`cache hit -- '${key}'`); } return config as Config; } }