Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
sagemathinc
GitHub Repository: sagemathinc/cocalc
Path: blob/master/src/packages/database/postgres/passport.ts
5690 views
1
/*
2
* This file is part of CoCalc: Copyright © 2022-2025 Sagemath, Inc.
3
* License: MS-RSL – see LICENSE.md for details
4
*/
5
6
// DEVELOPMENT: use scripts/auth/gen-sso.py to generate some test data
7
8
import { PassportStrategyDB } from "@cocalc/database/settings/auth-sso-types";
9
import {
10
getPassportsCached,
11
setPassportsCached,
12
} from "@cocalc/database/settings/server-settings";
13
import { callback2 as cb2 } from "@cocalc/util/async-utils";
14
import { lower_email_address, to_json } from "@cocalc/util/misc";
15
import { CB } from "@cocalc/util/types/database";
16
import {
17
set_account_info_if_different,
18
set_account_info_if_not_set,
19
set_email_address_verified,
20
} from "./account-queries";
21
import {
22
CreatePassportOpts,
23
PassportExistsOpts,
24
PostgreSQL,
25
SetAccountFields,
26
UpdateAccountInfoAndPassportOpts,
27
} from "./types";
28
29
export async function set_passport_settings(
30
db: PostgreSQL,
31
opts: PassportStrategyDB & { cb?: CB },
32
): Promise<void> {
33
const { strategy, conf, info } = opts;
34
let err = null;
35
try {
36
await db.async_query({
37
query: "INSERT INTO passport_settings",
38
values: {
39
"strategy::TEXT ": strategy,
40
"conf ::JSONB": conf,
41
"info ::JSONB": info,
42
},
43
conflict: "strategy",
44
});
45
} catch (err) {
46
err = err;
47
}
48
if (typeof opts.cb === "function") {
49
opts.cb(err);
50
}
51
}
52
53
export async function get_passport_settings(
54
db: PostgreSQL,
55
opts: { strategy: string; cb?: (data: object) => void },
56
): Promise<any> {
57
const { rows } = await db.async_query({
58
query: "SELECT conf, info FROM passport_settings",
59
where: { "strategy = $::TEXT": opts.strategy },
60
});
61
if (typeof opts.cb === "function") {
62
opts.cb(rows[0]);
63
}
64
return rows[0];
65
}
66
67
export async function get_all_passport_settings(
68
db: PostgreSQL,
69
): Promise<PassportStrategyDB[]> {
70
return (
71
await db.async_query<PassportStrategyDB>({
72
query: "SELECT strategy, conf, info FROM passport_settings",
73
})
74
).rows;
75
}
76
77
export async function get_all_passport_settings_cached(
78
db: PostgreSQL,
79
): Promise<PassportStrategyDB[]> {
80
const passports = getPassportsCached();
81
if (passports != null) {
82
return passports;
83
}
84
const res = await get_all_passport_settings(db);
85
setPassportsCached(res);
86
return res;
87
}
88
89
// Passports -- accounts linked to Google/Dropbox/Facebook/Github, etc.
90
// The Schema is slightly redundant, but indexed properly:
91
// {passports:['google-id', 'facebook-id'], passport_profiles:{'google-id':'...', 'facebook-id':'...'}}
92
93
export function _passport_key(opts) {
94
const { strategy, id } = opts;
95
// note: strategy is *our* name of the strategy in the DB, not it's type string!
96
if (typeof strategy !== "string") {
97
throw new Error("_passport_key: strategy must be defined");
98
}
99
if (typeof id !== "string") {
100
throw new Error("_passport_key: id must be defined");
101
}
102
103
return `${strategy}-${id}`;
104
}
105
106
export async function create_passport(
107
db: PostgreSQL,
108
opts: CreatePassportOpts,
109
): Promise<void> {
110
const dbg = db._dbg("create_passport");
111
dbg({ id: opts.id, strategy: opts.strategy, profile: to_json(opts.profile) });
112
113
try {
114
dbg("setting the passport for the account");
115
await db.async_query({
116
query: "UPDATE accounts",
117
jsonb_set: {
118
passports: { [_passport_key(opts)]: opts.profile },
119
},
120
where: {
121
"account_id = $::UUID": opts.account_id,
122
},
123
});
124
125
dbg(
126
`setting other account info ${opts.account_id}: ${opts.email_address}, ${opts.first_name}, ${opts.last_name}`,
127
);
128
await set_account_info_if_not_set({
129
db,
130
account_id: opts.account_id,
131
email_address: opts.email_address,
132
first_name: opts.first_name,
133
last_name: opts.last_name,
134
});
135
// we still record that email address as being verified
136
if (opts.email_address != null) {
137
await set_email_address_verified({
138
db,
139
account_id: opts.account_id,
140
email_address: opts.email_address,
141
});
142
}
143
opts.cb?.(undefined); // all good
144
} catch (err) {
145
if (opts.cb != null) {
146
opts.cb(err);
147
} else {
148
throw err;
149
}
150
}
151
}
152
153
export async function passport_exists(
154
db: PostgreSQL,
155
opts: PassportExistsOpts,
156
): Promise<string | undefined> {
157
try {
158
const result = await db.async_query({
159
query: "SELECT account_id FROM accounts",
160
where: [
161
// this uses the corresponding index to only scan a subset of all accounts!
162
"passports IS NOT NULL",
163
{ "(passports->>$::TEXT) IS NOT NULL": _passport_key(opts) },
164
],
165
});
166
const account_id = result?.rows[0]?.account_id;
167
if (opts.cb != null) {
168
opts.cb(null, account_id);
169
} else {
170
return account_id;
171
}
172
} catch (err) {
173
if (opts.cb != null) {
174
opts.cb(err);
175
} else {
176
throw err;
177
}
178
}
179
}
180
181
// this is only used in passport-login/maybeUpdateAccountAndPassport!
182
export async function update_account_and_passport(
183
db: PostgreSQL,
184
opts: UpdateAccountInfoAndPassportOpts,
185
) {
186
// This also updates the email address, if it is set in opts and does not exist with another account yet.
187
// NOTE: this changed in July 2024. Prior to that, changing the email address of the same account (by ID) in SSO,
188
// would not change the email address.
189
const dbg = db._dbg("update_account_and_passport");
190
dbg(
191
`updating account info ${to_json({
192
first_name: opts.first_name,
193
last_name: opts.last_name,
194
email_addres: opts.email_address,
195
})}`,
196
);
197
198
const upd: SetAccountFields = {
199
db: db,
200
account_id: opts.account_id,
201
first_name: opts.first_name,
202
last_name: opts.last_name,
203
};
204
205
// Only check for existing email if email_address is provided by SSO
206
// (Some SSO providers don't return email addresses)
207
if (opts.email_address) {
208
const email_address = lower_email_address(opts.email_address);
209
// Most likely, this just returns the very same account (since the account already exists).
210
const existing_account_id = await cb2(db.account_exists, {
211
email_address,
212
});
213
214
if (!existing_account_id) {
215
// There is no account with the new email address, hence we can update the email address as well
216
upd.email_address = email_address;
217
dbg(
218
`No existing account with email address ${email_address}. Therefore, we change the email address of account ${opts.account_id} as well.`,
219
);
220
}
221
}
222
223
// this set_account_info_if_different checks again if the email exists on another account, but it would throw an error.
224
const { email_changed } = await set_account_info_if_different(upd);
225
const key = _passport_key(opts);
226
dbg(`updating passport ${to_json({ key, profile: opts.profile })}`);
227
await db.async_query({
228
query: "UPDATE accounts",
229
jsonb_set: {
230
passports: { [key]: opts.profile },
231
},
232
where: {
233
"account_id = $::UUID": opts.account_id,
234
},
235
});
236
237
// since we update the email address of an account based on a change from the SSO mechanism
238
// we can assume the new email address is also "verified"
239
if (email_changed && typeof upd.email_address === "string") {
240
await set_email_address_verified({
241
db,
242
account_id: opts.account_id,
243
email_address: upd.email_address,
244
});
245
}
246
}
247
248