Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
sagemathinc
GitHub Repository: sagemathinc/cocalc
Path: blob/master/src/packages/util/db-schema/accounts.ts
1447 views
1
/*
2
* This file is part of CoCalc: Copyright © 2020 Sagemath, Inc.
3
* License: MS-RSL – see LICENSE.md for details
4
*/
5
6
import { NOTES } from "./crm";
7
import { SCHEMA as schema } from "./index";
8
import { checkAccountName } from "./name-rules";
9
import { Table } from "./types";
10
11
import {
12
DEFAULT_FONT_SIZE,
13
DEFAULT_NEW_FILENAMES,
14
NEW_FILENAMES,
15
OTHER_SETTINGS_USERDEFINED_LLM,
16
} from "./defaults";
17
18
import { DEFAULT_LOCALE } from "@cocalc/util/consts/locale";
19
20
export const USER_SEARCH_LIMIT = 250;
21
export const ADMIN_SEARCH_LIMIT = 2500;
22
23
export const USE_BALANCE_TOWARD_SUBSCRIPTIONS =
24
"use_balance_toward_subscriptions";
25
export const USE_BALANCE_TOWARD_SUBSCRIPTIONS_DEFAULT = true;
26
27
// AutoBalance: Every parameter is in dollars.
28
export interface AutoBalance {
29
// deposit money when the balance goes below this
30
trigger: number;
31
// amount to automatically add
32
amount: number;
33
// max amount of money to add per day
34
max_day: number;
35
// max amount of money to add per week
36
max_week: number;
37
// max amount of money to add per month
38
max_month: number;
39
// period -- which of max_day, max_week, or max_month to actually enforce.
40
// we always enforce **exactly one of them**.
41
period: "day" | "week" | "month";
42
// switch to disable/enable this.
43
enabled: boolean;
44
// if credit was not added, last reason why (at most 1024 characters)
45
reason?: string;
46
// ms since epoch of last attempt
47
time?: number;
48
// how much has been added at the moment when we last updated.
49
status?: { day: number; week: number; month: number };
50
}
51
52
// each of the parameters above must be a number in the
53
// given interval below.
54
// All fields should always be explicitly specified.
55
export const AUTOBALANCE_RANGES = {
56
trigger: [5, 250],
57
amount: [10, 250],
58
max_day: [5, 1000],
59
max_week: [5, 5000],
60
max_month: [5, 10000],
61
};
62
63
export const AUTOBALANCE_DEFAULTS = {
64
trigger: 10,
65
amount: 20,
66
max_day: 200,
67
max_week: 1000,
68
max_month: 2500,
69
period: "week",
70
enabled: true,
71
} as AutoBalance;
72
73
// throw error if not valid
74
export function ensureAutoBalanceValid(obj) {
75
if (obj == null) {
76
return;
77
}
78
if (typeof obj != "object") {
79
throw Error("must be an object");
80
}
81
for (const key in AUTOBALANCE_RANGES) {
82
if (obj[key] == null) {
83
throw Error(`${key} must be specified`);
84
}
85
}
86
for (const key in obj) {
87
if (key == "period") {
88
if (!["day", "week", "month"].includes(obj[key])) {
89
throw Error(`${key} must be 'day', 'week' or 'month'`);
90
}
91
continue;
92
}
93
if (key == "enabled") {
94
if (typeof obj[key] != "boolean") {
95
throw Error(`${key} must be boolean`);
96
}
97
continue;
98
}
99
if (key == "reason") {
100
if (typeof obj[key] != "string") {
101
throw Error(`${key} must be a string`);
102
}
103
if (obj[key].length > 1024) {
104
throw Error(`${key} must be at most 1024 characters`);
105
}
106
continue;
107
}
108
if (key == "time") {
109
if (typeof obj[key] != "number") {
110
throw Error(`${key} must be a number`);
111
}
112
continue;
113
}
114
if (key == "status") {
115
if (typeof obj[key] != "object") {
116
throw Error(`${key} must be an object`);
117
}
118
continue;
119
}
120
const range = AUTOBALANCE_RANGES[key];
121
if (range == null) {
122
throw Error(`invalid key '${key}'`);
123
}
124
const value = obj[key];
125
if (typeof value != "number") {
126
throw Error("every value must be a number");
127
}
128
if (value < range[0]) {
129
throw Error(`${key} must be at least ${range[0]}`);
130
}
131
if (value > range[1]) {
132
throw Error(`${key} must be at most ${range[1]}`);
133
}
134
}
135
}
136
137
Table({
138
name: "accounts",
139
fields: {
140
account_id: {
141
type: "uuid",
142
desc: "The uuid that determines the user account",
143
render: { type: "account" },
144
title: "Account",
145
},
146
created: {
147
type: "timestamp",
148
desc: "When the account was created.",
149
},
150
created_by: {
151
type: "string",
152
pg_type: "inet",
153
desc: "IP address that created the account.",
154
},
155
creation_actions_done: {
156
type: "boolean",
157
desc: "Set to true after all creation actions (e.g., add to projects) associated to this account are succesfully completed.",
158
},
159
password_hash: {
160
type: "string",
161
pg_type: "VARCHAR(173)",
162
desc: "Hash of the password. This is 1000 iterations of sha512 with salt of length 32.",
163
},
164
deleted: {
165
type: "boolean",
166
desc: "True if the account has been deleted.",
167
},
168
name: {
169
type: "string",
170
pg_type: "VARCHAR(39)",
171
desc: "The username of this user. This is optional but globally unique across all accoutns *and* organizations. It can be between 1 and 39 characters from a-z A-Z 0-9 - and must not start with a dash.",
172
},
173
email_address: {
174
type: "string",
175
pg_type: "VARCHAR(254)", // see http://stackoverflow.com/questions/386294/what-is-the-maximum-length-of-a-valid-email-address
176
desc: "The email address of the user. This is optional, since users may instead be associated to passport logins.",
177
unique: true,
178
render: { type: "email_address" },
179
}, // only one record in database can have this email address (if given)
180
email_address_before_delete: {
181
type: "string",
182
desc: "The email address of the user before they deleted their account.",
183
},
184
email_address_verified: {
185
type: "map",
186
desc: 'Verified email addresses as { "[email protected]" : <timestamp>, ... }',
187
},
188
email_address_challenge: {
189
type: "map",
190
desc: 'Contains random token for verification of an address: {"email": "...", "token": <random>, "time" : <timestamp for timeout>}',
191
},
192
email_address_problem: {
193
type: "map",
194
desc: 'Describes a problem with a given email address. example: { "[email protected]" : { "type": "bounce", "time": "2018-...", "mesg": "554 5.7.1 <....>: Recipient address rejected: Access denied, user does not exist", "status": <status code>}}',
195
},
196
passports: {
197
type: "map",
198
desc: 'Map from string ("[strategy]-[id]") derived from passport name and id to the corresponding profile',
199
},
200
editor_settings: {
201
type: "map",
202
desc: "Description of configuration settings for the editor. See the user_query get defaults.",
203
},
204
other_settings: {
205
type: "map",
206
desc: "Miscellaneous overall configuration settings for CoCalc, e.g., confirm close on exit?",
207
},
208
first_name: {
209
type: "string",
210
pg_type: "VARCHAR(254)", // some limit (actually around 3000) is required for indexing
211
desc: "The first name of this user.",
212
render: { type: "text", maxLength: 254, editable: true },
213
},
214
last_name: {
215
type: "string",
216
pg_type: "VARCHAR(254)",
217
desc: "The last name of this user.",
218
render: { type: "text", maxLength: 254, editable: true },
219
},
220
banned: {
221
type: "boolean",
222
desc: "Whether or not this user is banned.",
223
render: {
224
type: "boolean",
225
editable: true,
226
},
227
},
228
terminal: {
229
type: "map",
230
desc: "Settings for the terminal, e.g., font_size, etc. (see get query)",
231
},
232
autosave: {
233
type: "integer",
234
desc: "File autosave interval in seconds",
235
},
236
evaluate_key: {
237
type: "string",
238
desc: "Key used to evaluate code in Sage worksheet.",
239
},
240
font_size: {
241
type: "integer",
242
desc: "Default font-size for the editor, jupyter, etc. (px)",
243
},
244
last_active: {
245
type: "timestamp",
246
desc: "When this user was last active.",
247
},
248
stripe_customer_id: {
249
type: "string",
250
desc: "The id of this customer in the stripe billing system.",
251
},
252
stripe_customer: {
253
type: "map",
254
desc: "Information about customer from the point of view of stripe (exactly what is returned by stripe.customers.retrieve) ALMOST DEPRECATED -- THIS IS ONLY USED FOR OLD LEGACY UPGRADES.",
255
},
256
coupon_history: {
257
type: "map",
258
desc: "Information about which coupons the customer has used and the number of times",
259
},
260
profile: {
261
type: "map",
262
desc: "Information related to displaying an avatar for this user's location and presence in a document or chatroom.",
263
},
264
groups: {
265
type: "array",
266
pg_type: "TEXT[]",
267
desc: "Array of groups that this user belongs to; usually empty. The only group right now is 'admin', which grants admin rights.",
268
},
269
ssh_keys: {
270
type: "map",
271
desc: "Map from ssh key fingerprints to ssh key objects.",
272
},
273
api_key: {
274
type: "string",
275
desc: "Optional API key that grants full API access to anything this account can access. Key is of the form 'sk_9QabcrqJFy7JIhvAGih5c6Nb', where the random part is 24 characters (base 62).",
276
unique: true,
277
},
278
sign_up_usage_intent: {
279
type: "string",
280
desc: "What user intended to use CoCalc for at sign up",
281
render: { type: "text" },
282
},
283
lti_id: {
284
type: "array",
285
pg_type: "TEXT[]",
286
desc: "LTI ISS and user ID",
287
},
288
lti_data: {
289
type: "map",
290
desc: "extra information related to LTI",
291
},
292
unlisted: {
293
type: "boolean",
294
desc: "If true then exclude user for full name searches (but not exact email address searches).",
295
render: {
296
type: "boolean",
297
editable: true,
298
},
299
},
300
tags: {
301
type: "array",
302
pg_type: "TEXT[]",
303
desc: "Tags expressing what this user is most interested in doing.",
304
render: { type: "string-tags", editable: true },
305
},
306
tours: {
307
type: "array",
308
pg_type: "TEXT[]",
309
desc: "Tours that user has seen, so once they are here they are hidden from the UI. The special tour 'all' means to disable all tour buttons.",
310
render: { type: "string-tags" },
311
},
312
notes: NOTES,
313
salesloft_id: {
314
type: "integer",
315
desc: "The id of corresponding person in salesloft, if they exist there.",
316
render: {
317
type: "number",
318
integer: true,
319
editable: true,
320
min: 1,
321
},
322
},
323
purchase_closing_day: {
324
type: "integer",
325
desc: "Day of the month when pay-as-you-go purchases are cutoff and charged for this user. It happens at midnight UTC on this day. This should be an integer between 1 and 28.",
326
render: {
327
type: "number",
328
editable: false, // Do NOT change this without going through the reset-closing-date api call...
329
min: 1,
330
max: 28,
331
},
332
},
333
min_balance: {
334
type: "number",
335
pg_type: "REAL",
336
desc: "The minimum allowed balance for this user. This is a quota we impose for safety, not something they set. Admins may change this in response to a support request. For most users this is not set at all hence 0, but for some special enterprise-style customers to whom we extend 'credit', it will be set.",
337
render: {
338
title: "Minimum Allowed Balance (USD)",
339
type: "number",
340
integer: false,
341
editable: true,
342
max: 0,
343
},
344
},
345
balance: {
346
type: "number",
347
pg_type: "REAL",
348
desc: "Last computed balance for this user. NOT a source of truth. Meant to ensure all frontend clients show the same thing. Probably also useful for db queries and maybe analytics.",
349
render: {
350
title: "Account Balance (USD)",
351
type: "number",
352
integer: false,
353
editable: false,
354
},
355
},
356
balance_alert: {
357
type: "boolean",
358
desc: "If true, the UI will very strongly encourage user to open their balance modal.",
359
render: {
360
type: "boolean",
361
editable: true,
362
},
363
},
364
auto_balance: {
365
type: "map",
366
desc: "Determines protocol for automatically adding money to account. This is relevant for pay as you go users. The interface AutoBalance describes the parameters. The user can in theory set this to anything, but ]",
367
},
368
stripe_checkout_session: {
369
type: "map",
370
desc: "Part of the current open stripe checkout session object, namely {id:?, url:?}, but none of the other info. When user is going to add credit to their account, we create a stripe checkout session and store it here until they complete checking out. This makes it possible to guide them back to the checkout session, in case anything goes wrong, and also avoids confusion with potentially multiple checkout sessions at once.",
371
},
372
stripe_usage_subscription: {
373
type: "string",
374
pg_type: "varchar(256)",
375
desc: "Id of this user's stripe metered usage subscription, if they have one.",
376
},
377
email_daily_statements: {
378
type: "boolean",
379
desc: "If true, try to send daily statements to user showing all of their purchases. If false or not set, then do not. NOTE: we always try to email monthly statements to users.",
380
render: {
381
type: "boolean",
382
editable: true,
383
},
384
},
385
owner_id: {
386
type: "uuid",
387
desc: "If one user (owner_id) creates an account for another user via the API, then this records who created the account. They may have special privileges at some point.",
388
render: { type: "account" },
389
title: "Owner",
390
},
391
unread_message_count: {
392
type: "integer",
393
desc: "Number of unread messages in the messages table for this user. This gets updated whenever the messages table for this user gets changed, making it easier to have UI etc when there are unread messages.",
394
render: {
395
type: "number",
396
editable: false,
397
min: 0,
398
},
399
},
400
last_message_summary: {
401
type: "timestamp",
402
desc: "The last time the system sent an email to this user with a summary about new messages (see messages.ts).",
403
},
404
},
405
rules: {
406
desc: "All user accounts.",
407
primary_key: "account_id",
408
// db_standby: "unsafe",
409
pg_indexes: [
410
"(lower(first_name) text_pattern_ops)",
411
"(lower(last_name) text_pattern_ops)",
412
"created_by",
413
"created",
414
"last_active DESC NULLS LAST",
415
"lti_id",
416
"unlisted",
417
"((passports IS NOT NULL))",
418
"((ssh_keys IS NOT NULL))", // used by ssh-gateway to speed up getting all users
419
],
420
crm_indexes: [
421
"(lower(first_name) text_pattern_ops)",
422
"(lower(last_name) text_pattern_ops)",
423
"(lower(email_address) text_pattern_ops)",
424
"created",
425
"last_active DESC NULLS LAST",
426
],
427
pg_unique_indexes: [
428
"api_key", // we use the map api_key --> account_id, so it better be unique
429
"LOWER(name)", // ensure user-assigned name is case sensitive globally unique
430
], // note that we actually require uniqueness across accounts and organizations
431
// and this index is just a step in that direction; full uniquness must be
432
// checked as an extra step.
433
user_query: {
434
get: {
435
throttle_changes: 500,
436
pg_where: [{ "account_id = $::UUID": "account_id" }],
437
fields: {
438
// Exactly what from the below is sync'd by default with the frontend app client is explicitly
439
// listed in frontend/account/table.ts
440
account_id: null,
441
email_address: null,
442
lti_id: null,
443
stripe_checkout_session: null,
444
email_address_verified: null,
445
email_address_problem: null,
446
editor_settings: {
447
/* NOTE: there is a editor_settings.jupyter = { kernel...} that isn't documented here. */
448
strip_trailing_whitespace: false,
449
show_trailing_whitespace: false,
450
line_wrapping: true,
451
line_numbers: true,
452
jupyter_line_numbers: false,
453
smart_indent: true,
454
electric_chars: true,
455
match_brackets: true,
456
auto_close_brackets: true,
457
code_folding: true,
458
match_xml_tags: true,
459
auto_close_xml_tags: true,
460
auto_close_latex: true,
461
spaces_instead_of_tabs: true,
462
multiple_cursors: true,
463
track_revisions: true,
464
extra_button_bar: true,
465
build_on_save: true,
466
first_line_number: 1,
467
indent_unit: 4,
468
tab_size: 4,
469
bindings: "standard",
470
theme: "default",
471
undo_depth: 300,
472
jupyter_classic: false,
473
jupyter_window: false,
474
disable_jupyter_windowing: false,
475
show_exec_warning: true,
476
physical_keyboard: "default",
477
keyboard_variant: "",
478
ask_jupyter_kernel: true,
479
show_my_other_cursors: false,
480
disable_jupyter_virtualization: false,
481
},
482
other_settings: {
483
katex: true,
484
confirm_close: false,
485
mask_files: true,
486
page_size: 500,
487
standby_timeout_m: 5,
488
default_file_sort: "name",
489
[NEW_FILENAMES]: DEFAULT_NEW_FILENAMES,
490
show_global_info2: null,
491
first_steps: true,
492
newsletter: false,
493
time_ago_absolute: false,
494
// if true, do not show warning when using non-member projects
495
no_free_warnings: false,
496
allow_mentions: true,
497
dark_mode: false,
498
dark_mode_brightness: 100,
499
dark_mode_contrast: 90,
500
dark_mode_sepia: 0,
501
dark_mode_grayscale: 0,
502
news_read_until: 0,
503
hide_project_popovers: false,
504
hide_file_popovers: false,
505
hide_button_tooltips: false,
506
[OTHER_SETTINGS_USERDEFINED_LLM]: "[]",
507
i18n: DEFAULT_LOCALE,
508
no_email_new_messages: false,
509
[USE_BALANCE_TOWARD_SUBSCRIPTIONS]:
510
USE_BALANCE_TOWARD_SUBSCRIPTIONS_DEFAULT,
511
hide_navbar_balance: false,
512
},
513
name: null,
514
first_name: "",
515
last_name: "",
516
terminal: {
517
font_size: DEFAULT_FONT_SIZE,
518
color_scheme: "default",
519
font: "monospace",
520
},
521
autosave: 45,
522
evaluate_key: "Shift-Enter",
523
font_size: DEFAULT_FONT_SIZE,
524
passports: {},
525
groups: [],
526
last_active: null,
527
stripe_customer: null,
528
coupon_history: null,
529
profile: {
530
image: undefined,
531
color: "rgb(170,170,170)",
532
},
533
ssh_keys: {},
534
created: null,
535
unlisted: false,
536
tags: null,
537
tours: null,
538
min_balance: null,
539
balance: null,
540
balance_alert: null,
541
auto_balance: null,
542
purchase_closing_day: null,
543
stripe_usage_subscription: null,
544
email_daily_statements: null,
545
unread_message_count: null,
546
},
547
},
548
set: {
549
fields: {
550
account_id: "account_id",
551
name: true,
552
editor_settings: true,
553
other_settings: true,
554
first_name: true,
555
last_name: true,
556
terminal: true,
557
autosave: true,
558
evaluate_key: true,
559
font_size: true,
560
profile: true,
561
ssh_keys: true,
562
sign_up_usage_intent: true,
563
unlisted: true,
564
tags: true,
565
tours: true,
566
email_daily_statements: true,
567
// obviously min_balance can't be set!
568
auto_balance: true,
569
},
570
async check_hook(db, obj, account_id, _project_id, cb) {
571
if (obj["name"] != null) {
572
// NOTE: there is no way to unset/remove a username after one is set...
573
try {
574
checkAccountName(obj["name"]);
575
} catch (err) {
576
cb(err.toString());
577
return;
578
}
579
const id = await db.nameToAccountOrOrganization(obj["name"]);
580
if (id != null && id != account_id) {
581
cb(
582
`name "${obj["name"]}" is already taken by another organization or account`,
583
);
584
return;
585
}
586
}
587
// Hook to truncate some text fields to at most 254 characters, to avoid
588
// further trouble down the line.
589
for (const field of ["first_name", "last_name", "email_address"]) {
590
if (obj[field] != null) {
591
obj[field] = obj[field].slice(0, 254);
592
if (field != "email_address" && !obj[field]) {
593
// name fields can't be empty
594
cb(`${field} must be nonempty`);
595
return;
596
}
597
}
598
}
599
600
// Make sure auto_balance is valid.
601
if (obj["auto_balance"] != null) {
602
try {
603
ensureAutoBalanceValid(obj["auto_balance"]);
604
} catch (err) {
605
cb(`${err}`);
606
return;
607
}
608
}
609
cb();
610
},
611
},
612
},
613
},
614
});
615
616
export const EDITOR_BINDINGS = {
617
standard: "Standard",
618
sublime: "Sublime",
619
vim: "Vim",
620
emacs: "Emacs",
621
};
622
623
export const EDITOR_COLOR_SCHEMES: { [name: string]: string } = {
624
default: "Default",
625
"3024-day": "3024 day",
626
"3024-night": "3024 night",
627
abcdef: "abcdef",
628
abbott: "Abbott",
629
"ayu-dark": "Ayu dark",
630
"ayu-mirage": "Ayu mirage",
631
//'ambiance-mobile' : 'Ambiance mobile' # doesn't highlight python, confusing
632
ambiance: "Ambiance",
633
"base16-dark": "Base 16 dark",
634
"base16-light": "Base 16 light",
635
bespin: "Bespin",
636
blackboard: "Blackboard",
637
cobalt: "Cobalt",
638
colorforth: "Colorforth",
639
darcula: "Darcula",
640
dracula: "Dracula",
641
"duotone-dark": "Duotone Dark",
642
"duotone-light": "Duotone Light",
643
eclipse: "Eclipse",
644
elegant: "Elegant",
645
"erlang-dark": "Erlang dark",
646
"gruvbox-dark": "Gruvbox-Dark",
647
hopscotch: "Hopscotch",
648
icecoder: "Icecoder",
649
idea: "Idea", // this messes with the global hinter CSS!
650
isotope: "Isotope",
651
juejin: "Juejin",
652
"lesser-dark": "Lesser dark",
653
liquibyte: "Liquibyte",
654
lucario: "Lucario",
655
material: "Material",
656
"material-darker": "Material darker",
657
"material-ocean": "Material ocean",
658
"material-palenight": "Material palenight",
659
mbo: "mbo",
660
"mdn-like": "MDN like",
661
midnight: "Midnight",
662
monokai: "Monokai",
663
neat: "Neat",
664
neo: "Neo",
665
night: "Night",
666
"oceanic-next": "Oceanic next",
667
"panda-syntax": "Panda syntax",
668
"paraiso-dark": "Paraiso dark",
669
"paraiso-light": "Paraiso light",
670
"pastel-on-dark": "Pastel on dark",
671
railscasts: "Railscasts",
672
rubyblue: "Rubyblue",
673
seti: "Seti",
674
shadowfox: "Shadowfox",
675
"solarized dark": "Solarized dark",
676
"solarized light": "Solarized light",
677
ssms: "ssms",
678
"the-matrix": "The Matrix",
679
"tomorrow-night-bright": "Tomorrow Night - Bright",
680
"tomorrow-night-eighties": "Tomorrow Night - Eighties",
681
ttcn: "ttcn",
682
twilight: "Twilight",
683
"vibrant-ink": "Vibrant ink",
684
"xq-dark": "Xq dark",
685
"xq-light": "Xq light",
686
yeti: "Yeti",
687
yonce: "Yonce",
688
zenburn: "Zenburn",
689
};
690
691
Table({
692
name: "crm_accounts",
693
rules: {
694
virtual: "accounts",
695
primary_key: "account_id",
696
user_query: {
697
get: {
698
pg_where: [],
699
admin: true, // only admins can do get queries on this table
700
fields: {
701
...schema.accounts.user_query?.get?.fields,
702
banned: null,
703
groups: null,
704
notes: null,
705
salesloft_id: null,
706
sign_up_usage_intent: null,
707
owner_id: null,
708
deleted: null,
709
},
710
},
711
set: {
712
admin: true, // only admins can do get queries on this table
713
fields: {
714
account_id: true,
715
name: true,
716
first_name: true,
717
last_name: true,
718
autosave: true,
719
font_size: true,
720
banned: true,
721
unlisted: true,
722
notes: true,
723
tags: true,
724
salesloft_id: true,
725
purchase_closing_day: true,
726
min_balance: true, // admins can set this
727
},
728
},
729
},
730
},
731
fields: schema.accounts.fields,
732
});
733
734
Table({
735
name: "crm_agents",
736
rules: {
737
virtual: "accounts",
738
primary_key: "account_id",
739
user_query: {
740
get: {
741
// There where condition restricts to only admin accounts for now.
742
// TODO: Later this will change to 'crm'=any(groups) or something like that.
743
pg_where: ["'admin'=any(groups)"],
744
admin: true, // only admins can do get queries on this table
745
fields: schema.accounts.user_query?.get?.fields ?? {},
746
},
747
},
748
},
749
fields: schema.accounts.fields,
750
});
751
752
interface Tag {
753
label: string;
754
tag: string;
755
language?: string; // language of jupyter kernel
756
icon?: any; // I'm not going to import the IconName type from @cocalc/frontend
757
welcome?: string; // a simple "welcome" of this type
758
jupyterExtra?: string;
759
torun?: string; // how to run this in a terminal (e.g., for a .py file).
760
color?: string;
761
description?: string;
762
}
763
764
// They were used up until 2024-01-05
765
export const TAGS_FEATURES: Tag[] = [
766
{ label: "Jupyter", tag: "ipynb", color: "magenta" },
767
{
768
label: "Python",
769
tag: "py",
770
language: "python",
771
welcome: 'print("Welcome to CoCalc from Python!")',
772
torun: "# Click Terminal, then type 'python3 welcome.py'",
773
color: "red",
774
},
775
{
776
label: "AI / GPUs",
777
tag: "gpu",
778
color: "volcano",
779
icon: "gpu",
780
},
781
{
782
label: "R Stats",
783
tag: "R",
784
language: "r",
785
welcome: 'print("Welcome to CoCalc from R!")',
786
torun: "# Click Terminal, then type 'Rscript welcome.R'",
787
color: "orange",
788
},
789
{
790
label: "SageMath",
791
tag: "sage",
792
language: "sagemath",
793
welcome: "print('Welcome to CoCalc from Sage!', factor(2024))",
794
torun: "# Click Terminal, then type 'sage welcome.sage'",
795
color: "gold",
796
},
797
{
798
label: "Octave",
799
icon: "octave",
800
tag: "m",
801
language: "octave",
802
welcome: `disp("Welcome to CoCalc from Octave!")`,
803
torun: "% Click Terminal, then type 'octave --no-window-system welcome.m'",
804
color: "geekblue",
805
},
806
{
807
label: "Linux",
808
icon: "linux",
809
tag: "term",
810
language: "bash",
811
welcome: "echo 'Welcome to CoCalc from Linux/BASH!'",
812
color: "green",
813
},
814
{
815
label: "LaTeX",
816
tag: "tex",
817
welcome: `\\documentclass{article}
818
\\title{Welcome to CoCalc from \\LaTeX{}!}
819
\\begin{document}
820
\\maketitle
821
\\end{document}`,
822
color: "cyan",
823
},
824
{
825
label: "C/C++",
826
tag: "c",
827
language: "C++17",
828
icon: "cube",
829
welcome: `
830
#include <stdio.h>
831
int main() {
832
printf("Welcome to CoCalc from C!\\n");
833
return 0;
834
}`,
835
jupyterExtra: "\nmain();\n",
836
torun: "/* Click Terminal, then type 'gcc welcome.c && ./a.out' */",
837
color: "blue",
838
},
839
{
840
label: "Julia",
841
language: "julia",
842
icon: "julia",
843
tag: "jl",
844
welcome: 'println("Welcome to CoCalc from Julia!")',
845
torun: "# Click Terminal, then type 'julia welcome.jl' */",
846
color: "geekblue",
847
},
848
{
849
label: "Markdown",
850
tag: "md",
851
welcome:
852
"# Welcome to CoCalc from Markdown!\n\nYou can directly edit the rendered markdown -- try it!\n\nAnd run code:\n\n```py\n2+3\n```\n",
853
color: "purple",
854
},
855
// {
856
// label: "Whiteboard",
857
// tag: "board",
858
// welcome: `{"data":{"color":"#252937"},"h":96,"id":"1244fb1f","page":"b7cda7e9","str":"# Welcome to CoCalc from a Whiteboard!\\n\\n","type":"text","w":779,"x":-305,"y":-291,"z":1}
859
// {"data":{"pos":0},"id":"b7cda7e9","type":"page","z":0}`,
860
// },
861
{ label: "Teaching", tag: "course", color: "green" },
862
];
863
864
export const TAG_TO_FEATURE: { [key: string]: Readonly<Tag> } = {};
865
for (const t of TAGS_FEATURES) {
866
TAG_TO_FEATURE[t.tag] = t;
867
}
868
869
const professional = "professional";
870
871
// Tags specific to user roles or if they want to be contacted
872
export const TAGS_USERS: Readonly<Tag[]> = [
873
{
874
label: "Personal",
875
tag: "personal",
876
icon: "user",
877
description: "You are interesting in using CoCalc for personal use.",
878
},
879
{
880
label: "Professional",
881
tag: professional,
882
icon: "coffee",
883
description: "You are using CoCalc as an employee or freelancer.",
884
},
885
{
886
label: "Instructor",
887
tag: "instructor",
888
icon: "graduation-cap",
889
description: "You are teaching a course.",
890
},
891
{
892
label: "Student",
893
tag: "student",
894
icon: "smile",
895
description: "You are a student in a course.",
896
},
897
] as const;
898
899
export const TAGS = TAGS_USERS;
900
901
export const TAGS_MAP: { [key: string]: Readonly<Tag> } = {};
902
for (const x of TAGS) {
903
TAGS_MAP[x.tag] = x;
904
}
905
906
export const CONTACT_TAG = "contact";
907
export const CONTACT_THESE_TAGS = [professional];
908
909
export interface UserSearchResult {
910
account_id: string;
911
first_name?: string;
912
last_name?: string;
913
name?: string; // "vanity" username
914
last_active?: number; // ms since epoch -- when account was last active
915
created?: number; // ms since epoch -- when account created
916
banned?: boolean; // true if this user has been banned (only set for admin searches, obviously)
917
email_address_verified?: boolean; // true if their email has been verified (a sign they are more trustworthy).
918
// For security reasons, the email_address *only* occurs in search queries that
919
// are by email_address (or for admins); we must not reveal email addresses
920
// of users queried by substring searches, obviously.
921
email_address?: string;
922
}
923
924
export const ACCOUNT_ID_COOKIE_NAME = "account_id";
925
926