Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
sagemathinc
GitHub Repository: sagemathinc/cocalc
Path: blob/master/src/packages/frontend/account/actions.ts
1503 views
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 { join } from "path";
7
import { alert_message } from "@cocalc/frontend/alerts";
8
import { AccountClient } from "@cocalc/frontend/client/account";
9
import api from "@cocalc/frontend/client/api";
10
import { appBasePath } from "@cocalc/frontend/customize/app-base-path";
11
import { set_url } from "@cocalc/frontend/history";
12
import { deleteRememberMe } from "@cocalc/frontend/misc/remember-me";
13
import track from "@cocalc/frontend/user-tracking";
14
import { webapp_client } from "@cocalc/frontend/webapp-client";
15
import { once } from "@cocalc/util/async-utils";
16
import { define, required } from "@cocalc/util/fill";
17
import { encode_path } from "@cocalc/util/misc";
18
import { Actions } from "@cocalc/util/redux/Actions";
19
import { show_announce_end, show_announce_start } from "./dates";
20
import { AccountStore } from "./store";
21
import { AccountState } from "./types";
22
23
// Define account actions
24
export class AccountActions extends Actions<AccountState> {
25
private _last_history_state: string;
26
private account_client: AccountClient = webapp_client.account_client;
27
28
_init(store): void {
29
store.on("change", this.derive_show_global_info);
30
store.on("change", this.update_unread_news);
31
this.processSignUpTags();
32
}
33
34
derive_show_global_info(store: AccountStore): void {
35
// TODO when there is more time, rewrite this to be tied to announcements of a specific type (and use their timestamps)
36
// for now, we use the existence of a timestamp value to indicate that the banner is not shown
37
let show_global_info;
38
const sgi2 = store.getIn(["other_settings", "show_global_info2"]);
39
// unknown state, right after opening the application
40
if (sgi2 === "loading") {
41
show_global_info = false;
42
// value not set means there is no timestamp → show banner
43
} else {
44
// ... if it is inside the scheduling window
45
let middle;
46
const start = show_announce_start;
47
const end = show_announce_end;
48
const in_window =
49
start < (middle = webapp_client.time_client.server_time()) &&
50
middle < end;
51
52
if (sgi2 == null) {
53
show_global_info = in_window;
54
// 3rd case: a timestamp is set
55
// show the banner only if its start_dt timetstamp is earlier than now
56
// *and* when the last "dismiss time" by the user is prior to it.
57
} else {
58
const sgi2_dt = new Date(sgi2);
59
const dismissed_before_start = sgi2_dt < start;
60
show_global_info = in_window && dismissed_before_start;
61
}
62
}
63
this.setState({ show_global_info });
64
}
65
66
update_unread_news(store: AccountStore): void {
67
const news_read_until = store.getIn(["other_settings", "news_read_until"]);
68
const news_actions = this.redux.getActions("news");
69
news_actions?.updateUnreadCount(news_read_until);
70
}
71
72
set_user_type(user_type): void {
73
this.setState({
74
user_type,
75
is_logged_in: user_type === "signed_in",
76
});
77
}
78
79
// deletes the account and then signs out everywhere
80
public async delete_account(): Promise<void> {
81
try {
82
// actually request to delete the account
83
// this should return {status: "success"}
84
await api("/accounts/delete");
85
} catch (err) {
86
this.setState({
87
account_deletion_error: `Error trying to delete the account: ${err.message}`,
88
});
89
return;
90
}
91
this.sign_out(true);
92
}
93
94
public async sign_out(
95
everywhere: boolean,
96
sign_in: boolean = false,
97
): Promise<void> {
98
// disable redirection from sign in/up...
99
deleteRememberMe(appBasePath);
100
101
// Send a message to the server that the user explicitly
102
// requested to sign out. The server must clean up resources
103
// and *invalidate* the remember_me cookie for this client.
104
try {
105
await this.account_client.sign_out(everywhere);
106
} catch (error) {
107
// The state when this happens could be
108
// arbitrarily messed up. So... both pop up an error (which user will see),
109
// and set something in the store, which may or may not get displayed.
110
const err = `Error signing you out -- ${error}. Please refresh your browser and try again.`;
111
alert_message({ type: "error", message: err });
112
this.setState({
113
sign_out_error: err,
114
show_sign_out: false,
115
});
116
return;
117
}
118
// Invalidate the remember_me cookie and force a refresh, since otherwise there could be data
119
// left in the DOM, which could lead to a vulnerability
120
// or bleed into the next login somehow.
121
$(window).off("beforeunload", this.redux.getActions("page").check_unload);
122
// redirect to sign in page if sign_in is true; otherwise, the landing page:
123
window.location.href = join(appBasePath, sign_in ? "auth/sign-in" : "/");
124
}
125
126
push_state(url?: string): void {
127
if (url == null) {
128
url = this._last_history_state;
129
}
130
if (url == null) {
131
url = "";
132
}
133
this._last_history_state = url;
134
set_url("/settings" + encode_path(url));
135
}
136
137
public set_active_tab(tab: string): void {
138
track("settings", { tab });
139
this.setState({ active_page: tab });
140
this.push_state("/" + tab);
141
}
142
143
// Add an ssh key for this user, with the given fingerprint, title, and value
144
public add_ssh_key(unsafe_opts: unknown): void {
145
const opts = define<{
146
fingerprint: string;
147
title: string;
148
value: string;
149
}>(unsafe_opts, {
150
fingerprint: required,
151
title: required,
152
value: required,
153
});
154
this.redux.getTable("account").set({
155
ssh_keys: {
156
[opts.fingerprint]: {
157
title: opts.title,
158
value: opts.value,
159
creation_date: Date.now(),
160
},
161
},
162
});
163
}
164
165
// Delete the ssh key with given fingerprint for this user.
166
public delete_ssh_key(fingerprint): void {
167
this.redux.getTable("account").set({
168
ssh_keys: {
169
[fingerprint]: null,
170
},
171
}); // null is how to tell the backend/synctable to delete this...
172
}
173
174
public set_account_table(obj: object): void {
175
this.redux.getTable("account").set(obj);
176
}
177
178
public set_other_settings(name: string, value: any): void {
179
this.set_account_table({ other_settings: { [name]: value } });
180
}
181
182
set_editor_settings = (name: string, value) => {
183
this.set_account_table({ editor_settings: { [name]: value } });
184
};
185
186
public set_show_purchase_form(show: boolean) {
187
// this controls the default state of the "buy a license" purchase form in account → licenses
188
// by default, it's not showing up
189
this.setState({ show_purchase_form: show });
190
}
191
192
setTourDone(tour: string) {
193
const table = this.redux.getTable("account");
194
if (!table) return;
195
const store = this.redux.getStore("account");
196
if (!store) return;
197
const tours: string[] = store.get("tours")?.toJS() ?? [];
198
if (!tours?.includes(tour)) {
199
tours.push(tour);
200
table.set({ tours });
201
}
202
}
203
204
setTourNotDone(tour: string) {
205
const table = this.redux.getTable("account");
206
if (!table) return;
207
const store = this.redux.getStore("account");
208
if (!store) return;
209
const tours: string[] = store.get("tours")?.toJS() ?? [];
210
if (tours?.includes(tour)) {
211
// TODO fix this workaround for https://github.com/sagemathinc/cocalc/issues/6929
212
table.set({ tours: null });
213
table.set({
214
// filtering true false strings because of #6929 did create them in the past
215
tours: tours.filter((x) => x != tour && x !== "true" && x !== "false"),
216
});
217
}
218
}
219
220
processSignUpTags = async () => {
221
if (!localStorage.sign_up_tags) {
222
return;
223
}
224
try {
225
if (!webapp_client.is_signed_in()) {
226
await once(webapp_client, "signed_in");
227
}
228
await webapp_client.async_query({
229
query: {
230
accounts: {
231
tags: JSON.parse(localStorage.sign_up_tags),
232
sign_up_usage_intent: localStorage.sign_up_usage_intent,
233
},
234
},
235
});
236
delete localStorage.sign_up_tags;
237
delete localStorage.sign_up_usage_intent;
238
} catch (err) {
239
console.warn("processSignUpTags", err);
240
}
241
};
242
243
setFragment = (fragment) => {
244
// @ts-ignore
245
this.setState({ fragment });
246
};
247
248
addTag = async (tag: string) => {
249
const store = this.redux.getStore("account");
250
if (!store) return;
251
const tags = store.get("tags");
252
if (tags?.includes(tag)) {
253
// already tagged
254
return;
255
}
256
const table = this.redux.getTable("account");
257
if (!table) return;
258
const v = tags?.toJS() ?? [];
259
v.push(tag);
260
table.set({ tags: v });
261
try {
262
await webapp_client.conat_client.hub.system.userSalesloftSync({});
263
} catch (err) {
264
console.warn(
265
"WARNING: issue syncing with salesloft after setting tag",
266
tag,
267
err,
268
);
269
}
270
};
271
272
// delete won't be visible in frontend until a browser refresh...
273
deleteTag = async (tag: string) => {
274
const store = this.redux.getStore("account");
275
if (!store) return;
276
const tags = store.get("tags");
277
if (!tags?.includes(tag)) {
278
// already tagged
279
return;
280
}
281
const table = this.redux.getTable("account");
282
if (!table) return;
283
const v = tags.toJS().filter((x) => x != tag);
284
await webapp_client.async_query({ query: { accounts: { tags: v } } });
285
};
286
}
287
288