Real-time collaboration for Jupyter Notebooks, Linux Terminals, LaTeX, VS Code, R IDE, and more,
all in one place.
Real-time collaboration for Jupyter Notebooks, Linux Terminals, LaTeX, VS Code, R IDE, and more,
all in one place.
Path: blob/master/src/packages/util/db-schema/accounts.ts
Views: 687
/*1* This file is part of CoCalc: Copyright © 2020 Sagemath, Inc.2* License: MS-RSL – see LICENSE.md for details3*/45import { NOTES } from "./crm";6import { SCHEMA as schema } from "./index";7import { checkAccountName } from "./name-rules";8import { Table } from "./types";910import {11DEFAULT_FONT_SIZE,12DEFAULT_NEW_FILENAMES,13NEW_FILENAMES,14OTHER_SETTINGS_USERDEFINED_LLM,15} from "./defaults";1617import { DEFAULT_LOCALE } from "@cocalc/util/consts/locale";1819Table({20name: "accounts",21fields: {22account_id: {23type: "uuid",24desc: "The uuid that determines the user account",25render: { type: "account" },26title: "Account",27},28created: {29type: "timestamp",30desc: "When the account was created.",31},32created_by: {33type: "string",34pg_type: "inet",35desc: "IP address that created the account.",36},37creation_actions_done: {38type: "boolean",39desc: "Set to true after all creation actions (e.g., add to projects) associated to this account are succesfully completed.",40},41password_hash: {42type: "string",43pg_type: "VARCHAR(173)",44desc: "Hash of the password. This is 1000 iterations of sha512 with salt of length 32.",45},46deleted: {47type: "boolean",48desc: "True if the account has been deleted.",49},50name: {51type: "string",52pg_type: "VARCHAR(39)",53desc: "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.",54},55email_address: {56type: "string",57pg_type: "VARCHAR(254)", // see http://stackoverflow.com/questions/386294/what-is-the-maximum-length-of-a-valid-email-address58desc: "The email address of the user. This is optional, since users may instead be associated to passport logins.",59unique: true,60render: { type: "email_address" },61}, // only one record in database can have this email address (if given)62email_address_before_delete: {63type: "string",64desc: "The email address of the user before they deleted their account.",65},66email_address_verified: {67type: "map",68desc: 'Verified email addresses as { "[email protected]" : <timestamp>, ... }',69},70email_address_challenge: {71type: "map",72desc: 'Contains random token for verification of an address: {"email": "...", "token": <random>, "time" : <timestamp for timeout>}',73},74email_address_problem: {75type: "map",76desc: '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>}}',77},78passports: {79type: "map",80desc: 'Map from string ("[strategy]-[id]") derived from passport name and id to the corresponding profile',81},82editor_settings: {83type: "map",84desc: "Description of configuration settings for the editor. See the user_query get defaults.",85},86other_settings: {87type: "map",88desc: "Miscellaneous overall configuration settings for CoCalc, e.g., confirm close on exit?",89},90first_name: {91type: "string",92pg_type: "VARCHAR(254)", // some limit (actually around 3000) is required for indexing93desc: "The first name of this user.",94render: { type: "text", maxLength: 254, editable: true },95},96last_name: {97type: "string",98pg_type: "VARCHAR(254)",99desc: "The last name of this user.",100render: { type: "text", maxLength: 254, editable: true },101},102banned: {103type: "boolean",104desc: "Whether or not this user is banned.",105render: {106type: "boolean",107editable: true,108},109},110terminal: {111type: "map",112desc: "Settings for the terminal, e.g., font_size, etc. (see get query)",113},114autosave: {115type: "integer",116desc: "File autosave interval in seconds",117},118evaluate_key: {119type: "string",120desc: "Key used to evaluate code in Sage worksheet.",121},122font_size: {123type: "integer",124desc: "Default font-size for the editor, jupyter, etc. (px)",125},126last_active: {127type: "timestamp",128desc: "When this user was last active.",129},130stripe_customer_id: {131type: "string",132desc: "The id of this customer in the stripe billing system.",133},134stripe_customer: {135type: "map",136desc: "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.",137},138coupon_history: {139type: "map",140desc: "Information about which coupons the customer has used and the number of times",141},142profile: {143type: "map",144desc: "Information related to displaying an avatar for this user's location and presence in a document or chatroom.",145},146groups: {147type: "array",148pg_type: "TEXT[]",149desc: "Array of groups that this user belongs to; usually empty. The only group right now is 'admin', which grants admin rights.",150},151ssh_keys: {152type: "map",153desc: "Map from ssh key fingerprints to ssh key objects.",154},155api_key: {156type: "string",157desc: "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).",158unique: true,159},160sign_up_usage_intent: {161type: "string",162desc: "What user intended to use CoCalc for at sign up",163render: { type: "text" },164},165lti_id: {166type: "array",167pg_type: "TEXT[]",168desc: "LTI ISS and user ID",169},170lti_data: {171type: "map",172desc: "extra information related to LTI",173},174unlisted: {175type: "boolean",176desc: "If true then exclude user for full name searches (but not exact email address searches).",177render: {178type: "boolean",179editable: true,180},181},182tags: {183type: "array",184pg_type: "TEXT[]",185desc: "Tags expressing what this user is most interested in doing.",186render: { type: "string-tags", editable: true },187},188tours: {189type: "array",190pg_type: "TEXT[]",191desc: "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.",192render: { type: "string-tags" },193},194notes: NOTES,195salesloft_id: {196type: "integer",197desc: "The id of corresponding person in salesloft, if they exist there.",198render: {199type: "number",200integer: true,201editable: true,202min: 1,203},204},205purchase_closing_day: {206type: "integer",207desc: "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.",208render: {209type: "number",210editable: false, // Do NOT change this without going through the reset-closing-date api call...211min: 1,212max: 28,213},214},215min_balance: {216type: "number",217pg_type: "REAL",218desc: "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.",219render: {220title: "Minimum Allowed Balance (USD)",221type: "number",222integer: false,223editable: true,224max: 0,225},226},227stripe_checkout_session: {228type: "map",229desc: "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.",230},231stripe_usage_subscription: {232type: "string",233pg_type: "varchar(256)",234desc: "Id of this user's stripe metered usage subscription, if they have one.",235},236email_daily_statements: {237type: "boolean",238desc: "If true (or not set), try to email daily statements to user showing all of their purchases. NOTE: we always try to email monthly statements to users.",239render: {240type: "boolean",241editable: true,242},243},244owner_id: {245type: "uuid",246desc: "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.",247render: { type: "account" },248title: "Owner",249},250},251rules: {252desc: "All user accounts.",253primary_key: "account_id",254// db_standby: "unsafe",255pg_indexes: [256"(lower(first_name) text_pattern_ops)",257"(lower(last_name) text_pattern_ops)",258"created_by",259"created",260"last_active DESC NULLS LAST",261"lti_id",262"unlisted",263"((passports IS NOT NULL))",264"((ssh_keys IS NOT NULL))", // used by ssh-gateway to speed up getting all users265],266crm_indexes: [267"(lower(first_name) text_pattern_ops)",268"(lower(last_name) text_pattern_ops)",269"(lower(email_address) text_pattern_ops)",270"created",271"last_active DESC NULLS LAST",272],273pg_unique_indexes: [274"api_key", // we use the map api_key --> account_id, so it better be unique275"LOWER(name)", // ensure user-assigned name is case sensitive globally unique276], // note that we actually require uniqueness across accounts and organizations277// and this index is just a step in that direction; full uniquness must be278// checked as an extra step.279user_query: {280get: {281throttle_changes: 500,282pg_where: [{ "account_id = $::UUID": "account_id" }],283fields: {284// Exactly what from the below is sync'd by default with the frontend app client is explicitly285// listed in frontend/account/table.ts286account_id: null,287email_address: null,288lti_id: null,289stripe_checkout_session: null,290email_address_verified: null,291email_address_problem: null,292editor_settings: {293/* NOTE: there is a editor_settings.jupyter = { kernel...} that isn't documented here. */294strip_trailing_whitespace: false,295show_trailing_whitespace: false,296line_wrapping: true,297line_numbers: true,298jupyter_line_numbers: false,299smart_indent: true,300electric_chars: true,301match_brackets: true,302auto_close_brackets: true,303code_folding: true,304match_xml_tags: true,305auto_close_xml_tags: true,306auto_close_latex: true,307spaces_instead_of_tabs: true,308multiple_cursors: true,309track_revisions: true,310extra_button_bar: true,311build_on_save: true,312first_line_number: 1,313indent_unit: 4,314tab_size: 4,315bindings: "standard",316theme: "default",317undo_depth: 300,318jupyter_classic: false,319jupyter_window: false,320disable_jupyter_windowing: false,321show_exec_warning: true,322physical_keyboard: "default",323keyboard_variant: "",324ask_jupyter_kernel: true,325show_my_other_cursors: false,326disable_jupyter_virtualization: false,327},328other_settings: {329katex: true,330confirm_close: false,331mask_files: true,332page_size: 500,333standby_timeout_m: 5,334default_file_sort: "name",335[NEW_FILENAMES]: DEFAULT_NEW_FILENAMES,336show_global_info2: null,337first_steps: true,338newsletter: false,339time_ago_absolute: false,340// if true, do not show warning when using non-member projects341no_free_warnings: false,342allow_mentions: true,343dark_mode: false,344dark_mode_brightness: 100,345dark_mode_contrast: 90,346dark_mode_sepia: 0,347dark_mode_grayscale: 0,348news_read_until: 0,349hide_project_popovers: false,350hide_file_popovers: false,351hide_button_tooltips: false,352[OTHER_SETTINGS_USERDEFINED_LLM]: "[]",353i18n: DEFAULT_LOCALE,354},355name: null,356first_name: "",357last_name: "",358terminal: {359font_size: DEFAULT_FONT_SIZE,360color_scheme: "default",361font: "monospace",362},363autosave: 45,364evaluate_key: "Shift-Enter",365font_size: DEFAULT_FONT_SIZE,366passports: {},367groups: [],368last_active: null,369stripe_customer: null,370coupon_history: null,371profile: {372image: undefined,373color: "rgb(170,170,170)",374},375ssh_keys: {},376created: null,377unlisted: false,378tags: null,379tours: null,380min_balance: null,381purchase_closing_day: null,382stripe_usage_subscription: null,383email_daily_statements: null,384},385},386set: {387fields: {388account_id: "account_id",389name: true,390editor_settings: true,391other_settings: true,392first_name: true,393last_name: true,394terminal: true,395autosave: true,396evaluate_key: true,397font_size: true,398profile: true,399ssh_keys: true,400sign_up_usage_intent: true,401unlisted: true,402tags: true,403tours: true,404email_daily_statements: true,405// obviously min_balance can't be set!406},407async check_hook(db, obj, account_id, _project_id, cb) {408if (obj["name"] != null) {409// NOTE: there is no way to unset/remove a username after one is set...410try {411checkAccountName(obj["name"]);412} catch (err) {413cb(err.toString());414return;415}416const id = await db.nameToAccountOrOrganization(obj["name"]);417if (id != null && id != account_id) {418cb(419`name "${obj["name"]}" is already taken by another organization or account`,420);421return;422}423}424// Hook to truncate some text fields to at most 254 characters, to avoid425// further trouble down the line.426for (const field of ["first_name", "last_name", "email_address"]) {427if (obj[field] != null) {428obj[field] = obj[field].slice(0, 254);429if (field != "email_address" && !obj[field]) {430// name fields can't be empty431cb(`${field} must be nonempty`);432return;433}434}435}436cb();437},438},439},440},441});442443export const EDITOR_BINDINGS = {444standard: "Standard",445sublime: "Sublime",446vim: "Vim",447emacs: "Emacs",448};449450export const EDITOR_COLOR_SCHEMES: { [name: string]: string } = {451default: "Default",452"3024-day": "3024 day",453"3024-night": "3024 night",454abcdef: "abcdef",455//'ambiance-mobile' : 'Ambiance mobile' # doesn't highlight python, confusing456ambiance: "Ambiance",457"base16-dark": "Base 16 dark",458"base16-light": "Base 16 light",459bespin: "Bespin",460blackboard: "Blackboard",461cobalt: "Cobalt",462colorforth: "Colorforth",463darcula: "Darcula",464dracula: "Dracula",465"duotone-dark": "Duotone Dark",466"duotone-light": "Duotone Light",467eclipse: "Eclipse",468elegant: "Elegant",469"erlang-dark": "Erlang dark",470"gruvbox-dark": "Gruvbox-Dark",471hopscotch: "Hopscotch",472icecoder: "Icecoder",473idea: "Idea", // this messes with the global hinter CSS!474isotope: "Isotope",475"lesser-dark": "Lesser dark",476liquibyte: "Liquibyte",477lucario: "Lucario",478material: "Material",479mbo: "mbo",480"mdn-like": "MDN like",481midnight: "Midnight",482monokai: "Monokai",483neat: "Neat",484neo: "Neo",485night: "Night",486"oceanic-next": "Oceanic next",487"panda-syntax": "Panda syntax",488"paraiso-dark": "Paraiso dark",489"paraiso-light": "Paraiso light",490"pastel-on-dark": "Pastel on dark",491railscasts: "Railscasts",492rubyblue: "Rubyblue",493seti: "Seti",494shadowfox: "Shadowfox",495"solarized dark": "Solarized dark",496"solarized light": "Solarized light",497ssms: "ssms",498"the-matrix": "The Matrix",499"tomorrow-night-bright": "Tomorrow Night - Bright",500"tomorrow-night-eighties": "Tomorrow Night - Eighties",501ttcn: "ttcn",502twilight: "Twilight",503"vibrant-ink": "Vibrant ink",504"xq-dark": "Xq dark",505"xq-light": "Xq light",506yeti: "Yeti",507zenburn: "Zenburn",508};509510Table({511name: "crm_accounts",512rules: {513virtual: "accounts",514primary_key: "account_id",515user_query: {516get: {517pg_where: [],518admin: true, // only admins can do get queries on this table519fields: {520...schema.accounts.user_query?.get?.fields,521banned: null,522groups: null,523notes: null,524salesloft_id: null,525sign_up_usage_intent: null,526owner_id: null,527},528},529set: {530admin: true, // only admins can do get queries on this table531fields: {532account_id: true,533name: true,534first_name: true,535last_name: true,536autosave: true,537font_size: true,538banned: true,539unlisted: true,540notes: true,541salesloft_id: true,542purchase_closing_day: true,543min_balance: true, // admins can set this544},545},546},547},548fields: schema.accounts.fields,549});550551Table({552name: "crm_agents",553rules: {554virtual: "accounts",555primary_key: "account_id",556user_query: {557get: {558// There where condition restricts to only admin accounts for now.559// TODO: Later this will change to 'crm'=any(groups) or something like that.560pg_where: ["'admin'=any(groups)"],561admin: true, // only admins can do get queries on this table562fields: schema.accounts.user_query?.get?.fields ?? {},563},564},565},566fields: schema.accounts.fields,567});568569interface Tag {570label: string;571tag: string;572language?: string; // language of jupyter kernel573icon?: any; // I'm not going to import the IconName type from @cocalc/frontend574welcome?: string; // a simple "welcome" of this type575jupyterExtra?: string;576torun?: string; // how to run this in a terminal (e.g., for a .py file).577color?: string;578description?: string;579}580581// They were used up until 2024-01-05582export const TAGS_FEATURES: Tag[] = [583{ label: "Jupyter", tag: "ipynb", color: "magenta" },584{585label: "Python",586tag: "py",587language: "python",588welcome: 'print("Welcome to CoCalc from Python!")',589torun: "# Click Terminal, then type 'python3 welcome.py'",590color: "red",591},592{593label: "AI / GPUs",594tag: "gpu",595color: "volcano",596icon: "gpu",597},598{599label: "R Stats",600tag: "R",601language: "r",602welcome: 'print("Welcome to CoCalc from R!")',603torun: "# Click Terminal, then type 'Rscript welcome.R'",604color: "orange",605},606{607label: "SageMath",608tag: "sage",609language: "sagemath",610welcome: "print('Welcome to CoCalc from Sage!', factor(2024))",611torun: "# Click Terminal, then type 'sage welcome.sage'",612color: "gold",613},614{615label: "Octave",616icon: "octave",617tag: "m",618language: "octave",619welcome: `disp("Welcome to CoCalc from Octave!")`,620torun: "% Click Terminal, then type 'octave --no-window-system welcome.m'",621color: "geekblue",622},623{624label: "Linux",625icon: "linux",626tag: "term",627language: "bash",628welcome: "echo 'Welcome to CoCalc from Linux/BASH!'",629color: "green",630},631{632label: "LaTeX",633tag: "tex",634welcome: `\\documentclass{article}635\\title{Welcome to CoCalc from \\LaTeX{}!}636\\begin{document}637\\maketitle638\\end{document}`,639color: "cyan",640},641{642label: "C/C++",643tag: "c",644language: "C++17",645icon: "cube",646welcome: `647#include <stdio.h>648int main() {649printf("Welcome to CoCalc from C!\\n");650return 0;651}`,652jupyterExtra: "\nmain();\n",653torun: "/* Click Terminal, then type 'gcc welcome.c && ./a.out' */",654color: "blue",655},656{657label: "Julia",658language: "julia",659icon: "julia",660tag: "jl",661welcome: 'println("Welcome to CoCalc from Julia!")',662torun: "# Click Terminal, then type 'julia welcome.jl' */",663color: "geekblue",664},665{666label: "Markdown",667tag: "md",668welcome:669"# 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",670color: "purple",671},672// {673// label: "Whiteboard",674// tag: "board",675// 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}676// {"data":{"pos":0},"id":"b7cda7e9","type":"page","z":0}`,677// },678{ label: "Teaching", tag: "course", color: "green" },679];680681export const TAG_TO_FEATURE: { [key: string]: Readonly<Tag> } = {};682for (const t of TAGS_FEATURES) {683TAG_TO_FEATURE[t.tag] = t;684}685686const professional = "professional";687688// Tags specific to user roles or if they want to be contacted689export const TAGS_USERS: Readonly<Tag[]> = [690{691label: "Personal",692tag: "personal",693icon: "user",694description: "You are interesting in using CoCalc for personal use.",695},696{697label: "Professional",698tag: professional,699icon: "coffee",700description: "You are using CoCalc as an employee or freelancer.",701},702{703label: "Instructor",704tag: "instructor",705icon: "graduation-cap",706description: "You are teaching a course.",707},708{709label: "Student",710tag: "student",711icon: "smile",712description: "You are a student in a course.",713},714] as const;715716export const TAGS = TAGS_USERS;717718export const TAGS_MAP: { [key: string]: Readonly<Tag> } = {};719for (const x of TAGS) {720TAGS_MAP[x.tag] = x;721}722723export const CONTACT_TAG = "contact";724export const CONTACT_THESE_TAGS = [professional];725726727