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