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/frontend/account/actions.ts
Views: 687
1
/*
2
* This file is part of CoCalc: Copyright © 2020 Sagemath, Inc.
3
* License: MS-RSL – see LICENSE.md for details
4
*/
5
6
import { fromJS } from "immutable";
7
import { join } from "path";
8
9
import { alert_message } from "@cocalc/frontend/alerts";
10
import { AccountClient } from "@cocalc/frontend/client/account";
11
import api from "@cocalc/frontend/client/api";
12
import { appBasePath } from "@cocalc/frontend/customize/app-base-path";
13
import { set_url } from "@cocalc/frontend/history";
14
import { track_conversion } from "@cocalc/frontend/misc";
15
import { deleteRememberMe } from "@cocalc/frontend/misc/remember-me";
16
import track from "@cocalc/frontend/user-tracking";
17
import { webapp_client } from "@cocalc/frontend/webapp-client";
18
import { once } from "@cocalc/util/async-utils";
19
import { define, required } from "@cocalc/util/fill";
20
import { encode_path } from "@cocalc/util/misc";
21
import { Actions } from "@cocalc/util/redux/Actions";
22
import { show_announce_end, show_announce_start } from "./dates";
23
import { AccountStore } from "./store";
24
import { AccountState } from "./types";
25
26
// Define account actions
27
export class AccountActions extends Actions<AccountState> {
28
private _last_history_state: string;
29
private account_client: AccountClient = webapp_client.account_client;
30
31
_init(store): void {
32
store.on("change", this.derive_show_global_info);
33
store.on("change", this.update_unread_news);
34
this.processSignUpTags();
35
}
36
37
private help(): string {
38
return this.redux.getStore("customize").get("help_email");
39
}
40
41
derive_show_global_info(store: AccountStore): void {
42
// TODO when there is more time, rewrite this to be tied to announcements of a specific type (and use their timestamps)
43
// for now, we use the existence of a timestamp value to indicate that the banner is not shown
44
let show_global_info;
45
const sgi2 = store.getIn(["other_settings", "show_global_info2"]);
46
// unknown state, right after opening the application
47
if (sgi2 === "loading") {
48
show_global_info = false;
49
// value not set means there is no timestamp → show banner
50
} else {
51
// ... if it is inside the scheduling window
52
let middle;
53
const start = show_announce_start;
54
const end = show_announce_end;
55
const in_window =
56
start < (middle = webapp_client.time_client.server_time()) &&
57
middle < end;
58
59
if (sgi2 == null) {
60
show_global_info = in_window;
61
// 3rd case: a timestamp is set
62
// show the banner only if its start_dt timetstamp is earlier than now
63
// *and* when the last "dismiss time" by the user is prior to it.
64
} else {
65
const sgi2_dt = new Date(sgi2);
66
const dismissed_before_start = sgi2_dt < start;
67
show_global_info = in_window && dismissed_before_start;
68
}
69
}
70
this.setState({ show_global_info });
71
}
72
73
update_unread_news(store: AccountStore): void {
74
const news_read_until = store.getIn(["other_settings", "news_read_until"]);
75
const news_actions = this.redux.getActions("news");
76
news_actions?.updateUnreadCount(news_read_until);
77
}
78
79
set_user_type(user_type): void {
80
this.setState({
81
user_type,
82
is_logged_in: user_type === "signed_in",
83
});
84
}
85
86
public async sign_in(email: string, password: string): Promise<void> {
87
const doc_conn =
88
"[connectivity debugging tips](https://doc.cocalc.com/howto/connectivity-issues.html)";
89
const err_help = `\
90
Please try again.
91
92
If that doesn't work after a few minutes, try these ${doc_conn} or email ${this.help()}.\
93
`;
94
95
this.setState({ signing_in: true });
96
let mesg;
97
try {
98
mesg = await this.account_client.sign_in({
99
email_address: email,
100
password,
101
remember_me: true,
102
get_api_key: !!this.redux.getStore("page").get("get_api_key"),
103
});
104
} catch (err) {
105
this.setState({
106
sign_in_error: `There was an error signing you in -- (${err.message}). ${err_help}`,
107
});
108
return;
109
}
110
this.setState({ signing_in: false });
111
switch (mesg.event) {
112
case "sign_in_failed":
113
this.setState({ sign_in_error: mesg.reason });
114
return;
115
case "signed_in":
116
break;
117
case "error":
118
this.setState({ sign_in_error: mesg.reason });
119
return;
120
default:
121
// should never ever happen
122
this.setState({
123
sign_in_error: `The server responded with invalid message when signing in: ${JSON.stringify(
124
mesg,
125
)}`,
126
});
127
return;
128
}
129
}
130
131
public async create_account(
132
first_name: string,
133
last_name: string,
134
email_address: string,
135
password: string,
136
token?: string,
137
usage_intent?: string,
138
): Promise<void> {
139
this.setState({ signing_up: true });
140
let mesg;
141
try {
142
mesg = await this.account_client.create_account({
143
first_name,
144
last_name,
145
email_address,
146
password,
147
usage_intent,
148
agreed_to_terms: true, // since never gets called if not set in UI
149
token,
150
get_api_key: !!this.redux.getStore("page").get("get_api_key"),
151
});
152
} catch (err) {
153
// generic error.
154
this.setState(
155
fromJS({ sign_up_error: { generic: JSON.stringify(err) } }) as any,
156
);
157
return;
158
} finally {
159
this.setState({ signing_up: false });
160
}
161
switch (mesg.event) {
162
case "account_creation_failed":
163
this.setState({ sign_up_error: mesg.reason });
164
return;
165
case "signed_in":
166
this.redux.getActions("page").set_active_tab("projects");
167
track_conversion("create_account");
168
return;
169
default:
170
// should never ever happen
171
alert_message({
172
type: "error",
173
message: `The server responded with invalid message to account creation request: #{JSON.stringify(mesg)}`,
174
});
175
}
176
}
177
178
// deletes the account and then signs out everywhere
179
public async delete_account(): Promise<void> {
180
// cancel any subscriptions
181
try {
182
await this.redux.getActions("billing").cancel_everything();
183
} catch (err) {
184
if (this.redux.getStore("billing").get("no_stripe")) {
185
// stripe not configured on backend, so this err is expected
186
} else {
187
throw err;
188
}
189
}
190
191
try {
192
// actually request to delete the account
193
// this should return {status: "success"}
194
await api("/accounts/delete");
195
} catch (err) {
196
this.setState({
197
account_deletion_error: `Error trying to delete the account: ${err.message}`,
198
});
199
return;
200
}
201
this.sign_out(true);
202
}
203
204
public async forgot_password(email_address: string): Promise<void> {
205
try {
206
await this.account_client.forgot_password(email_address);
207
} catch (err) {
208
this.setState({
209
forgot_password_error: `Error sending password reset message to ${email_address} -- ${err}. Write to ${this.help()} for help.`,
210
forgot_password_success: "",
211
});
212
return;
213
}
214
this.setState({
215
forgot_password_success: `Password reset message sent to ${email_address}; if you don't receive it, check your spam folder; if you have further trouble, write to ${this.help()}.`,
216
forgot_password_error: "",
217
});
218
}
219
220
public async reset_password(
221
reset_code: string,
222
new_password: string,
223
): Promise<void> {
224
try {
225
await this.account_client.reset_forgot_password(reset_code, new_password);
226
} catch (err) {
227
this.setState({
228
reset_password_error: err.message,
229
});
230
return;
231
}
232
// success
233
// TODO: can we automatically log them in? Should we? Seems dangerous.
234
history.pushState({}, "", location.href);
235
this.setState({ reset_key: "", reset_password_error: "" });
236
}
237
238
public async sign_out(
239
everywhere: boolean,
240
sign_in: boolean = false,
241
): Promise<void> {
242
// disable redirection from sign in/up...
243
deleteRememberMe(appBasePath);
244
245
// Send a message to the server that the user explicitly
246
// requested to sign out. The server must clean up resources
247
// and *invalidate* the remember_me cookie for this client.
248
try {
249
await this.account_client.sign_out(everywhere);
250
} catch (error) {
251
// The state when this happens could be
252
// arbitrarily messed up. So... both pop up an error (which user will see),
253
// and set something in the store, which may or may not get displayed.
254
const err = `Error signing you out -- ${error}. Please refresh your browser and try again.`;
255
alert_message({ type: "error", message: err });
256
this.setState({
257
sign_out_error: err,
258
show_sign_out: false,
259
});
260
return;
261
}
262
// Invalidate the remember_me cookie and force a refresh, since otherwise there could be data
263
// left in the DOM, which could lead to a vulnerability
264
// or bleed into the next login somehow.
265
$(window).off("beforeunload", this.redux.getActions("page").check_unload);
266
// redirect to sign in page if sign_in is true; otherwise, the landing page:
267
window.location.href = join(appBasePath, sign_in ? "auth/sign-in" : "/");
268
}
269
270
push_state(url?: string): void {
271
if (url == null) {
272
url = this._last_history_state;
273
}
274
if (url == null) {
275
url = "";
276
}
277
this._last_history_state = url;
278
set_url("/settings" + encode_path(url));
279
}
280
281
public set_active_tab(tab: string): void {
282
track("settings", { tab });
283
this.setState({ active_page: tab });
284
this.push_state("/" + tab);
285
}
286
287
// Add an ssh key for this user, with the given fingerprint, title, and value
288
public add_ssh_key(unsafe_opts: unknown): void {
289
const opts = define<{
290
fingerprint: string;
291
title: string;
292
value: string;
293
}>(unsafe_opts, {
294
fingerprint: required,
295
title: required,
296
value: required,
297
});
298
this.redux.getTable("account").set({
299
ssh_keys: {
300
[opts.fingerprint]: {
301
title: opts.title,
302
value: opts.value,
303
creation_date: Date.now(),
304
},
305
},
306
});
307
}
308
309
// Delete the ssh key with given fingerprint for this user.
310
public delete_ssh_key(fingerprint): void {
311
this.redux.getTable("account").set({
312
ssh_keys: {
313
[fingerprint]: null,
314
},
315
}); // null is how to tell the backend/synctable to delete this...
316
}
317
318
public set_account_table(obj: object): void {
319
this.redux.getTable("account").set(obj);
320
}
321
322
public set_other_settings(name: string, value: any): void {
323
this.set_account_table({ other_settings: { [name]: value } });
324
}
325
326
set_editor_settings = (name: string, value) => {
327
this.set_account_table({ editor_settings: { [name]: value } });
328
};
329
330
public set_show_purchase_form(show: boolean) {
331
// this controlls the default state of the "buy a license" purchase form in account → licenses
332
// by default, it's not showing up
333
this.setState({ show_purchase_form: show });
334
}
335
336
setTourDone(tour: string) {
337
const table = this.redux.getTable("account");
338
if (!table) return;
339
const store = this.redux.getStore("account");
340
if (!store) return;
341
const tours: string[] = store.get("tours")?.toJS() ?? [];
342
if (!tours?.includes(tour)) {
343
tours.push(tour);
344
table.set({ tours });
345
}
346
}
347
348
setTourNotDone(tour: string) {
349
const table = this.redux.getTable("account");
350
if (!table) return;
351
const store = this.redux.getStore("account");
352
if (!store) return;
353
const tours: string[] = store.get("tours")?.toJS() ?? [];
354
if (tours?.includes(tour)) {
355
// TODO fix this workaround for https://github.com/sagemathinc/cocalc/issues/6929
356
table.set({ tours: null });
357
table.set({
358
// filtering true false strings because of #6929 did create them in the past
359
tours: tours.filter((x) => x != tour && x !== "true" && x !== "false"),
360
});
361
}
362
}
363
364
processSignUpTags = async () => {
365
if (!localStorage.sign_up_tags) {
366
return;
367
}
368
try {
369
if (!webapp_client.is_signed_in()) {
370
await once(webapp_client, "signed_in");
371
}
372
await webapp_client.async_query({
373
query: {
374
accounts: {
375
tags: JSON.parse(localStorage.sign_up_tags),
376
sign_up_usage_intent: localStorage.sign_up_usage_intent,
377
},
378
},
379
});
380
delete localStorage.sign_up_tags;
381
delete localStorage.sign_up_usage_intent;
382
} catch (err) {
383
console.warn("processSignUpTags", err);
384
}
385
};
386
}
387
388