CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutSign UpSign In
sagemathinc

Real-time collaboration for Jupyter Notebooks, Linux Terminals, LaTeX, VS Code, R IDE, and more,
all in one place.

GitHub Repository: sagemathinc/cocalc
Path: blob/master/src/packages/database/postgres/passport.ts
Views: 687
1
/*
2
* This file is part of CoCalc: Copyright © 2022 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 { to_json } from "@cocalc/util/misc";
14
import { CB } from "@cocalc/util/types/database";
15
import {
16
set_account_info_if_different,
17
set_account_info_if_not_set,
18
set_email_address_verified
19
} from "./account-queries";
20
import {
21
CreatePassportOpts,
22
PassportExistsOpts,
23
PostgreSQL,
24
UpdateAccountInfoAndPassportOpts
25
} from "./types";
26
27
export async function set_passport_settings(
28
db: PostgreSQL,
29
opts: PassportStrategyDB & { cb?: CB }
30
): Promise<void> {
31
const { strategy, conf, info } = opts;
32
let err = null;
33
try {
34
await db.async_query({
35
query: "INSERT INTO passport_settings",
36
values: {
37
"strategy::TEXT ": strategy,
38
"conf ::JSONB": conf,
39
"info ::JSONB": info,
40
},
41
conflict: "strategy",
42
});
43
} catch (err) {
44
err = err;
45
}
46
if (typeof opts.cb === "function") {
47
opts.cb(err);
48
}
49
}
50
51
export async function get_passport_settings(
52
db: PostgreSQL,
53
opts: { strategy: string; cb?: (data: object) => void }
54
): Promise<any> {
55
const { rows } = await db.async_query({
56
query: "SELECT conf, info FROM passport_settings",
57
where: { "strategy = $::TEXT": opts.strategy },
58
});
59
if (typeof opts.cb === "function") {
60
opts.cb(rows[0]);
61
}
62
return rows[0];
63
}
64
65
export async function get_all_passport_settings(
66
db: PostgreSQL
67
): Promise<PassportStrategyDB[]> {
68
return (
69
await db.async_query<PassportStrategyDB>({
70
query: "SELECT strategy, conf, info FROM passport_settings",
71
})
72
).rows;
73
}
74
75
export async function get_all_passport_settings_cached(
76
db: PostgreSQL
77
): Promise<PassportStrategyDB[]> {
78
const passports = getPassportsCached();
79
if (passports != null) {
80
return passports;
81
}
82
const res = await get_all_passport_settings(db);
83
setPassportsCached(res);
84
return res;
85
}
86
87
// Passports -- accounts linked to Google/Dropbox/Facebook/Github, etc.
88
// The Schema is slightly redundant, but indexed properly:
89
// {passports:['google-id', 'facebook-id'], passport_profiles:{'google-id':'...', 'facebook-id':'...'}}
90
91
export function _passport_key(opts) {
92
const { strategy, id } = opts;
93
// note: strategy is *our* name of the strategy in the DB, not it's type string!
94
if (typeof strategy !== "string") {
95
throw new Error("_passport_key: strategy must be defined");
96
}
97
if (typeof id !== "string") {
98
throw new Error("_passport_key: id must be defined");
99
}
100
101
return `${strategy}-${id}`;
102
}
103
104
export async function create_passport(
105
db: PostgreSQL,
106
opts: CreatePassportOpts
107
): Promise<void> {
108
const dbg = db._dbg("create_passport");
109
dbg({ id: opts.id, strategy: opts.strategy, profile: to_json(opts.profile) });
110
111
try {
112
dbg("setting the passport for the account");
113
await db.async_query({
114
query: "UPDATE accounts",
115
jsonb_set: {
116
passports: { [_passport_key(opts)]: opts.profile },
117
},
118
where: {
119
"account_id = $::UUID": opts.account_id,
120
},
121
});
122
123
dbg(
124
`setting other account info ${opts.account_id}: ${opts.email_address}, ${opts.first_name}, ${opts.last_name}`
125
);
126
await set_account_info_if_not_set({
127
db: db,
128
account_id: opts.account_id,
129
email_address: opts.email_address,
130
first_name: opts.first_name,
131
last_name: opts.last_name,
132
});
133
// we still record that email address as being verified
134
if (opts.email_address != null) {
135
await set_email_address_verified({
136
db,
137
account_id: opts.account_id,
138
email_address: opts.email_address,
139
});
140
}
141
opts.cb?.(undefined); // all good
142
} catch (err) {
143
if (opts.cb != null) {
144
opts.cb(err);
145
} else {
146
throw err;
147
}
148
}
149
}
150
151
export async function passport_exists(
152
db: PostgreSQL,
153
opts: PassportExistsOpts
154
): Promise<string | undefined> {
155
try {
156
const result = await db.async_query({
157
query: "SELECT account_id FROM accounts",
158
where: [
159
// this uses the corresponding index to only scan a subset of all accounts!
160
"passports IS NOT NULL",
161
{ "(passports->>$::TEXT) IS NOT NULL": _passport_key(opts) },
162
],
163
});
164
const account_id = result?.rows[0]?.account_id;
165
if (opts.cb != null) {
166
opts.cb(null, account_id);
167
} else {
168
return account_id;
169
}
170
} catch (err) {
171
if (opts.cb != null) {
172
opts.cb(err);
173
} else {
174
throw err;
175
}
176
}
177
}
178
179
export async function update_account_and_passport(
180
db: PostgreSQL,
181
opts: UpdateAccountInfoAndPassportOpts
182
) {
183
// we deliberately do not update the email address, because if the SSO
184
// strategy sends a different one, this would break the "link".
185
// rather, if the email (and hence most likely the email address) changes on the
186
// SSO side, this would equal to creating a new account.
187
const dbg = db._dbg("update_account_and_passport");
188
dbg(
189
`updating account info ${to_json({
190
first_name: opts.first_name,
191
last_name: opts.last_name,
192
})}`
193
);
194
await set_account_info_if_different({
195
db: db,
196
account_id: opts.account_id,
197
first_name: opts.first_name,
198
last_name: opts.last_name,
199
});
200
const key = _passport_key(opts);
201
dbg(`updating passport ${to_json({ key, profile: opts.profile })}`);
202
await db.async_query({
203
query: "UPDATE accounts",
204
jsonb_set: {
205
passports: { [key]: opts.profile },
206
},
207
where: {
208
"account_id = $::UUID": opts.account_id,
209
},
210
});
211
}
212
213