Path: blob/master/src/packages/database/postgres/account-queries.ts
5688 views
/*1* This file is part of CoCalc: Copyright © 2020-2026 Sagemath, Inc.2* License: MS-RSL – see LICENSE.md for details3*/45// Various functions involving the database and accounts.67import { callback2 } from "@cocalc/util/async-utils";8import {9assert_valid_account_id,10assert_valid_email_address,11len,12} from "@cocalc/util/misc";13import { is_a_site_license_manager } from "./site-license/search";14import { PostgreSQL, SetAccountFields } from "./types";15//import getLogger from "@cocalc/backend/logger";16//const L = getLogger("db:pg:account-queries");1718/* For now we define "paying customer" to mean they have a subscription.19It's OK if it expired. They at least bought one once.20This is mainly used for anti-abuse purposes...2122TODO: modernize this or don't use this at all...23*/24export async function is_paying_customer(25db: PostgreSQL,26account_id: string,27): Promise<boolean> {28let x;29try {30x = await callback2(db.get_account, {31account_id,32columns: ["stripe_customer"],33});34} catch (_err) {35// error probably means there is no such account or account_id is badly formatted.36return false;37}38if (!!x.stripe_customer?.subscriptions?.total_count) {39// they have at least one subscription of some form -- so that's enough to count.40return true;41}42// If they manage any licenses then they also count:43return await is_a_site_license_manager(db, account_id);44}4546// this is like set_account_info_if_different, but only sets the fields if they're not set47export async function set_account_info_if_not_set(48opts: SetAccountFields,49): Promise<{ email_changed: boolean }> {50return await set_account_info_if_different(opts, false);51}5253// This sets the given fields of an account, if it is different from the current value – except for the email address, which we only set but not change54export async function set_account_info_if_different(55opts: SetAccountFields,56overwrite = true,57): Promise<{ email_changed: boolean }> {58const columns = ["email_address", "first_name", "last_name"];5960// this could throw an error for "no such account"61const account = await get_account<{62email_address: string;63first_name: string;64last_name: string;65}>(opts.db, opts.account_id, columns);6667const do_set: { [field: string]: string } = {};68let do_email: string | undefined = undefined;6970for (const field of columns) {71if (typeof opts[field] !== "string") continue;72if (!overwrite && account[field] != null) continue;73if (account[field] != opts[field]) {74if (field === "email_address") {75do_email = opts[field];76} else {77do_set[field] = opts[field];78}79}80}81if (len(do_set) > 0) {82await set_account(opts.db, opts.account_id, do_set);83}8485if (do_email) {86if (account["email_address"] != null) {87// if it changes, we have to call the change_email_address function88await callback2(opts.db.change_email_address.bind(opts.db), {89account_id: opts.account_id,90email_address: do_email,91});92} else {93const existing_account_id = await callback2(94opts.db.account_exists.bind(opts.db),95{96email_address: do_email,97},98);99if (existing_account_id) {100throw "email_already_taken";101}102await set_account(opts.db, opts.account_id, {103email_address: do_email,104});105}106// Just changed email address - might be added to a project...107await callback2(opts.db.do_account_creation_actions.bind(opts.db), {108email_address: do_email,109account_id: opts.account_id,110});111}112113return { email_changed: !!do_email };114}115116export async function set_account(117db: PostgreSQL,118account_id: string,119set: { [field: string]: any },120): Promise<void> {121await db.async_query({122query: "UPDATE accounts",123where: { "account_id = $::UUID": account_id },124set,125});126}127128// TODO typing: pick the column fields from the actual account type stored in the database129export async function get_account<T>(130db: PostgreSQL,131account_id: string,132columns: string[],133): Promise<T> {134return await callback2(db.get_account.bind(db), {135account_id,136columns,137});138}139140export async function get_email_address_for_account_id(141db: PostgreSQL,142account_id: string,143): Promise<string | undefined> {144assert_valid_account_id(account_id);145const { rows } = await db.async_query<{ email_address?: string }>({146query: "SELECT email_address FROM accounts",147where: { "account_id = $::UUID": account_id },148});149if (rows.length === 0) {150return undefined;151}152return rows[0].email_address ?? undefined;153}154155interface SetEmailAddressVerifiedOpts {156db: PostgreSQL;157account_id: string;158email_address: string;159}160161export async function set_email_address_verified(162opts: SetEmailAddressVerifiedOpts,163): Promise<void> {164const { db, account_id, email_address } = opts;165assert_valid_account_id(account_id);166assert_valid_email_address(email_address);167await db.async_query({168query: "UPDATE accounts",169jsonb_set: { email_address_verified: { [email_address]: new Date() } },170where: { "account_id = $::UUID": account_id },171});172}173174export async function is_admin(175db: PostgreSQL,176account_id: string,177): Promise<boolean> {178const { groups } = await get_account<{ groups?: string[] }>(db, account_id, [179"groups",180]);181return Array.isArray(groups) && groups.includes("admin");182}183184185