Path: blob/master/src/packages/hub/webapp-configuration.ts
1496 views
/*1* This file is part of CoCalc: Copyright © 2020 Sagemath, Inc.2* License: MS-RSL – see LICENSE.md for details3*/45// This unifies the entire webapp configuration – endpoint /customize6// The main goal is to optimize this, to use as little DB interactions7// as necessary, use caching, etc.8// This manages the webapp's configuration based on the hostname9// (allows whitelabeling).1011import { delay } from "awaiting";12import debug from "debug";13import { isEmpty } from "lodash";14import LRU from "lru-cache";15import type { PostgreSQL } from "@cocalc/database/postgres/types";16import { get_passport_manager, PassportManager } from "@cocalc/server/hub/auth";17import { getSoftwareEnvironments } from "@cocalc/server/software-envs";18import { callback2 as cb2 } from "@cocalc/util/async-utils";19import { EXTRAS as SERVER_SETTINGS_EXTRAS } from "@cocalc/util/db-schema/site-settings-extras";20import { SoftwareEnvConfig } from "@cocalc/util/sanitize-software-envs";21import { site_settings_conf as SITE_SETTINGS_CONF } from "@cocalc/util/schema";22import { CustomLLMPublic } from "@cocalc/util/types/llm";23import { parseDomain, ParseResultType } from "parse-domain";24import getServerSettings, {25ServerSettingsDynamic,26} from "./servers/server-settings";27import { have_active_registration_tokens } from "./utils";2829const L = debug("hub:webapp-config");3031const CACHE = new LRU({ max: 1000, ttl: 30 * 1000 }); // 1 minutes3233export function clear_cache(): void {34CACHE.clear();35}3637type Theme = { [key: string]: string | boolean };3839interface Config {40// todo41configuration: any;42registration: any;43strategies: object;44software: SoftwareEnvConfig | null;45ollama: { [key: string]: CustomLLMPublic };46custom_openai: { [key: string]: CustomLLMPublic };47}4849async function get_passport_manager_async(): Promise<PassportManager> {50// the only issue here is, that the http server already starts up before the51// passport manager is configured – but, the passport manager depends on the http server52// we just retry during that initial period of uncertainty…53let ms = 100;54while (true) {55const pp_manager = get_passport_manager();56if (pp_manager != null) {57return pp_manager;58} else {59L(60`WARNING: Passport Manager not available yet -- trying again in ${ms}ms`,61);62await delay(ms);63ms = Math.min(10000, 1.3 * ms);64}65}66}6768export class WebappConfiguration {69private readonly db: PostgreSQL;70private data?: ServerSettingsDynamic;7172constructor({ db }) {73this.db = db;74this.init();75}7677private async init(): Promise<void> {78// this.data.pub updates automatically – do not modify it!79this.data = await getServerSettings();80await get_passport_manager_async();81}8283// server settings with whitelabeling settings84// TODO post-process all values85public async settings(vID: string) {86const res = await cb2(this.db._query, {87query: "SELECT id, settings FROM whitelabeling",88cache: true,89where: { "id = $::TEXT": vID },90});91if (this.data == null) {92// settings not yet initialized93return {};94}95const data = res.rows[0];96if (data != null) {97return { ...this.data.all, ...data.settings };98} else {99return this.data.all;100}101}102103// derive the vanity ID from the host string104private get_vanity_id(host: string): string | undefined {105const host_parsed = parseDomain(host);106if (host_parsed.type === ParseResultType.Listed) {107// vanity for vanity.cocalc.com or foo.p for foo.p.cocalc.com108return host_parsed.subDomains.join(".");109}110return undefined;111}112113private async theme(vID: string): Promise<Theme> {114const res = await cb2(this.db._query, {115query: "SELECT id, theme FROM whitelabeling",116cache: true,117where: { "id = $::TEXT": vID },118});119const data = res.rows[0];120if (data != null) {121// post-process data, but do not set default values…122const theme: Theme = {};123for (const [key, value] of Object.entries(data.theme)) {124const config = SITE_SETTINGS_CONF[key] ?? SERVER_SETTINGS_EXTRAS[key];125if (typeof config?.to_val == "function") {126theme[key] = config.to_val(value, data.theme);127} else {128if (typeof value == "string" || typeof value == "boolean") {129theme[key] = value;130}131}132}133L(`vanity theme=${JSON.stringify(theme)}`);134return theme;135} else {136L(`theme id=${vID} not found`);137return {};138}139}140141private async get_vanity(vID): Promise<object> {142if (vID != null && vID !== "") {143L(`vanity ID = "${vID}"`);144return await this.theme(vID);145} else {146return {};147}148}149150// returns the global configuration + eventually vanity specific site config settings151private async get_configuration({ host, country }) {152if (this.data == null) {153// settings not yet initialized154return {};155}156const vID = this.get_vanity_id(host);157const config = this.data.pub;158const vanity = await this.get_vanity(vID);159return { ...config, ...vanity, ...{ country, dns: host } };160}161162private async get_strategies(): Promise<object> {163const key = "strategies";164let strategies = CACHE.get(key);165if (strategies == null) {166// wait until this.passport_manager is initialized.167// this could happen right at the start of the server168const passport_manager = await get_passport_manager_async();169strategies = passport_manager.get_strategies_v2();170CACHE.set(key, strategies);171}172return strategies as object;173}174175// derives the public ollama model configuration from the private one176private get_ollama_public(): { [key: string]: CustomLLMPublic } {177if (this.data == null) {178throw new Error("server settings not yet initialized");179}180const ollama = this.data.all.ollama_configuration;181return processCustomLLM(ollama, "Ollama");182}183184private get_custom_openai_public(): { [key: string]: CustomLLMPublic } {185if (this.data == null) {186throw new Error("server settings not yet initialized");187}188const custom_openai = this.data.all.custom_openai_configuration;189return processCustomLLM(custom_openai, "OpenAI (custom)");190}191192private async get_config({ country, host }): Promise<Config> {193while (this.data == null) {194L.debug("waiting for server settings to be initialized");195await delay(100);196}197198const [configuration, registration, software, ollama, custom_openai] =199await Promise.all([200this.get_configuration({ host, country }),201have_active_registration_tokens(this.db),202getSoftwareEnvironments("webapp"),203this.get_ollama_public(),204this.get_custom_openai_public(),205]);206const strategies = await this.get_strategies();207return {208configuration,209registration,210strategies,211software,212ollama,213custom_openai,214};215}216217// it returns a shallow copy, hence you can modify/add keys in the returned map!218public async get({ country, host }): Promise<Config> {219const key = `config::${country}::${host}`;220let config = CACHE.get(key);221if (config == null) {222config = await this.get_config({ country, host });223CACHE.set(key, config);224} else {225L(`cache hit -- '${key}'`);226}227return config as Config;228}229}230231// for Ollama or Custom OpenAI232function processCustomLLM(233data: any,234displayFallback,235): { [key: string]: CustomLLMPublic } {236if (isEmpty(data)) return {};237238const ret: { [key: string]: CustomLLMPublic } = {};239for (const key in data) {240const conf = data[key];241const cocalc = conf.cocalc ?? {};242if (cocalc.disabled) continue;243const model = conf.model ?? key;244ret[key] = {245model,246display: cocalc.display ?? `${displayFallback} ${model}`,247icon: cocalc.icon, // fallback is the Ollama or OpenAI icon, frontend does that248desc: cocalc.desc ?? "",249};250}251return ret;252}253254255