CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutSign UpSign In
sagemathinc

Real-time collaboration for Jupyter Notebooks, Linux Terminals, LaTeX, VS Code, R IDE, and more,
all in one place.

GitHub Repository: sagemathinc/cocalc
Path: blob/master/src/packages/hub/webapp-configuration.ts
Views: 687
1
/*
2
* This file is part of CoCalc: Copyright © 2020 Sagemath, Inc.
3
* License: MS-RSL – see LICENSE.md for details
4
*/
5
6
// This unifies the entire webapp configuration – endpoint /customize
7
// The main goal is to optimize this, to use as little DB interactions
8
// as necessary, use caching, etc.
9
// This manages the webapp's configuration based on the hostname
10
// (allows whitelabeling).
11
12
import { delay } from "awaiting";
13
import debug from "debug";
14
import { isEmpty } from "lodash";
15
import LRU from "lru-cache";
16
17
import type { PostgreSQL } from "@cocalc/database/postgres/types";
18
import { get_passport_manager, PassportManager } from "@cocalc/server/hub/auth";
19
import { getSoftwareEnvironments } from "@cocalc/server/software-envs";
20
import { callback2 as cb2 } from "@cocalc/util/async-utils";
21
import { EXTRAS as SERVER_SETTINGS_EXTRAS } from "@cocalc/util/db-schema/site-settings-extras";
22
import { SoftwareEnvConfig } from "@cocalc/util/sanitize-software-envs";
23
import { site_settings_conf as SITE_SETTINGS_CONF } from "@cocalc/util/schema";
24
import { CustomLLMPublic } from "@cocalc/util/types/llm";
25
import { parseDomain, ParseResultType } from "parse-domain";
26
import getServerSettings, {
27
ServerSettingsDynamic,
28
} from "./servers/server-settings";
29
import { have_active_registration_tokens } from "./utils";
30
31
const L = debug("hub:webapp-config");
32
33
const CACHE = new LRU({ max: 1000, ttl: 30 * 1000 }); // 1 minutes
34
35
export function clear_cache(): void {
36
CACHE.clear();
37
}
38
39
type Theme = { [key: string]: string | boolean };
40
41
interface Config {
42
// todo
43
configuration: any;
44
registration: any;
45
strategies: object;
46
software: SoftwareEnvConfig | null;
47
ollama: { [key: string]: CustomLLMPublic };
48
custom_openai: { [key: string]: CustomLLMPublic };
49
}
50
51
async function get_passport_manager_async(): Promise<PassportManager> {
52
// the only issue here is, that the http server already starts up before the
53
// passport manager is configured – but, the passport manager depends on the http server
54
// we just retry during that initial period of uncertainty…
55
let ms = 100;
56
while (true) {
57
const pp_manager = get_passport_manager();
58
if (pp_manager != null) {
59
return pp_manager;
60
} else {
61
L(
62
`WARNING: Passport Manager not available yet -- trying again in ${ms}ms`,
63
);
64
await delay(ms);
65
ms = Math.min(10000, 1.3 * ms);
66
}
67
}
68
}
69
70
export class WebappConfiguration {
71
private readonly db: PostgreSQL;
72
private data?: ServerSettingsDynamic;
73
74
constructor({ db }) {
75
this.db = db;
76
this.init();
77
}
78
79
private async init(): Promise<void> {
80
// this.data.pub updates automatically – do not modify it!
81
this.data = await getServerSettings();
82
await get_passport_manager_async();
83
}
84
85
// server settings with whitelabeling settings
86
// TODO post-process all values
87
public async settings(vID: string) {
88
const res = await cb2(this.db._query, {
89
query: "SELECT id, settings FROM whitelabeling",
90
cache: true,
91
where: { "id = $::TEXT": vID },
92
});
93
if (this.data == null) {
94
// settings not yet initialized
95
return {};
96
}
97
const data = res.rows[0];
98
if (data != null) {
99
return { ...this.data.all, ...data.settings };
100
} else {
101
return this.data.all;
102
}
103
}
104
105
// derive the vanity ID from the host string
106
private get_vanity_id(host: string): string | undefined {
107
const host_parsed = parseDomain(host);
108
if (host_parsed.type === ParseResultType.Listed) {
109
// vanity for vanity.cocalc.com or foo.p for foo.p.cocalc.com
110
return host_parsed.subDomains.join(".");
111
}
112
return undefined;
113
}
114
115
private async theme(vID: string): Promise<Theme> {
116
const res = await cb2(this.db._query, {
117
query: "SELECT id, theme FROM whitelabeling",
118
cache: true,
119
where: { "id = $::TEXT": vID },
120
});
121
const data = res.rows[0];
122
if (data != null) {
123
// post-process data, but do not set default values…
124
const theme: Theme = {};
125
for (const [key, value] of Object.entries(data.theme)) {
126
const config = SITE_SETTINGS_CONF[key] ?? SERVER_SETTINGS_EXTRAS[key];
127
if (typeof config?.to_val == "function") {
128
theme[key] = config.to_val(value, data.theme);
129
} else {
130
if (typeof value == "string" || typeof value == "boolean") {
131
theme[key] = value;
132
}
133
}
134
}
135
L(`vanity theme=${JSON.stringify(theme)}`);
136
return theme;
137
} else {
138
L(`theme id=${vID} not found`);
139
return {};
140
}
141
}
142
143
private async get_vanity(vID): Promise<object> {
144
if (vID != null && vID !== "") {
145
L(`vanity ID = "${vID}"`);
146
return await this.theme(vID);
147
} else {
148
return {};
149
}
150
}
151
152
// returns the global configuration + eventually vanity specific site config settings
153
private async get_configuration({ host, country }) {
154
if (this.data == null) {
155
// settings not yet initialized
156
return {};
157
}
158
const vID = this.get_vanity_id(host);
159
const config = this.data.pub;
160
const vanity = await this.get_vanity(vID);
161
return { ...config, ...vanity, ...{ country, dns: host } };
162
}
163
164
private async get_strategies(): Promise<object> {
165
const key = "strategies";
166
let strategies = CACHE.get(key);
167
if (strategies == null) {
168
// wait until this.passport_manager is initialized.
169
// this could happen right at the start of the server
170
const passport_manager = await get_passport_manager_async();
171
strategies = passport_manager.get_strategies_v2();
172
CACHE.set(key, strategies);
173
}
174
return strategies as object;
175
}
176
177
// derives the public ollama model configuration from the private one
178
private get_ollama_public(): { [key: string]: CustomLLMPublic } {
179
if (this.data == null) {
180
throw new Error("server settings not yet initialized");
181
}
182
const ollama = this.data.all.ollama_configuration;
183
return processCustomLLM(ollama, "Ollama");
184
}
185
186
private get_custom_openai_public(): { [key: string]: CustomLLMPublic } {
187
if (this.data == null) {
188
throw new Error("server settings not yet initialized");
189
}
190
const custom_openai = this.data.all.custom_openai_configuration;
191
return processCustomLLM(custom_openai, "OpenAI (custom)");
192
}
193
194
private async get_config({ country, host }): Promise<Config> {
195
while (this.data == null) {
196
L.debug("waiting for server settings to be initialized");
197
await delay(100);
198
}
199
200
const [configuration, registration, software, ollama, custom_openai] =
201
await Promise.all([
202
this.get_configuration({ host, country }),
203
have_active_registration_tokens(this.db),
204
getSoftwareEnvironments("webapp"),
205
this.get_ollama_public(),
206
this.get_custom_openai_public(),
207
]);
208
const strategies = await this.get_strategies();
209
return {
210
configuration,
211
registration,
212
strategies,
213
software,
214
ollama,
215
custom_openai,
216
};
217
}
218
219
// it returns a shallow copy, hence you can modify/add keys in the returned map!
220
public async get({ country, host }): Promise<Config> {
221
const key = `config::${country}::${host}`;
222
let config = CACHE.get(key);
223
if (config == null) {
224
config = await this.get_config({ country, host });
225
CACHE.set(key, config);
226
} else {
227
L(`cache hit -- '${key}'`);
228
}
229
return config as Config;
230
}
231
}
232
233
// for Ollama or Custom OpenAI
234
function processCustomLLM(
235
data: any,
236
displayFallback,
237
): { [key: string]: CustomLLMPublic } {
238
if (isEmpty(data)) return {};
239
240
const ret: { [key: string]: CustomLLMPublic } = {};
241
for (const key in data) {
242
const conf = data[key];
243
const cocalc = conf.cocalc ?? {};
244
if (cocalc.disabled) continue;
245
const model = conf.model ?? key;
246
ret[key] = {
247
model,
248
display: cocalc.display ?? `${displayFallback} ${model}`,
249
icon: cocalc.icon, // fallback is the Ollama or OpenAI icon, frontend does that
250
desc: cocalc.desc ?? "",
251
};
252
}
253
return ret;
254
}
255
256