Path: blob/master/src/packages/frontend/account/actions.ts
1503 views
/*1* This file is part of CoCalc: Copyright © 2020 Sagemath, Inc.2* License: MS-RSL – see LICENSE.md for details3*/45import { join } from "path";6import { alert_message } from "@cocalc/frontend/alerts";7import { AccountClient } from "@cocalc/frontend/client/account";8import api from "@cocalc/frontend/client/api";9import { appBasePath } from "@cocalc/frontend/customize/app-base-path";10import { set_url } from "@cocalc/frontend/history";11import { deleteRememberMe } from "@cocalc/frontend/misc/remember-me";12import track from "@cocalc/frontend/user-tracking";13import { webapp_client } from "@cocalc/frontend/webapp-client";14import { once } from "@cocalc/util/async-utils";15import { define, required } from "@cocalc/util/fill";16import { encode_path } from "@cocalc/util/misc";17import { Actions } from "@cocalc/util/redux/Actions";18import { show_announce_end, show_announce_start } from "./dates";19import { AccountStore } from "./store";20import { AccountState } from "./types";2122// Define account actions23export class AccountActions extends Actions<AccountState> {24private _last_history_state: string;25private account_client: AccountClient = webapp_client.account_client;2627_init(store): void {28store.on("change", this.derive_show_global_info);29store.on("change", this.update_unread_news);30this.processSignUpTags();31}3233derive_show_global_info(store: AccountStore): void {34// TODO when there is more time, rewrite this to be tied to announcements of a specific type (and use their timestamps)35// for now, we use the existence of a timestamp value to indicate that the banner is not shown36let show_global_info;37const sgi2 = store.getIn(["other_settings", "show_global_info2"]);38// unknown state, right after opening the application39if (sgi2 === "loading") {40show_global_info = false;41// value not set means there is no timestamp → show banner42} else {43// ... if it is inside the scheduling window44let middle;45const start = show_announce_start;46const end = show_announce_end;47const in_window =48start < (middle = webapp_client.time_client.server_time()) &&49middle < end;5051if (sgi2 == null) {52show_global_info = in_window;53// 3rd case: a timestamp is set54// show the banner only if its start_dt timetstamp is earlier than now55// *and* when the last "dismiss time" by the user is prior to it.56} else {57const sgi2_dt = new Date(sgi2);58const dismissed_before_start = sgi2_dt < start;59show_global_info = in_window && dismissed_before_start;60}61}62this.setState({ show_global_info });63}6465update_unread_news(store: AccountStore): void {66const news_read_until = store.getIn(["other_settings", "news_read_until"]);67const news_actions = this.redux.getActions("news");68news_actions?.updateUnreadCount(news_read_until);69}7071set_user_type(user_type): void {72this.setState({73user_type,74is_logged_in: user_type === "signed_in",75});76}7778// deletes the account and then signs out everywhere79public async delete_account(): Promise<void> {80try {81// actually request to delete the account82// this should return {status: "success"}83await api("/accounts/delete");84} catch (err) {85this.setState({86account_deletion_error: `Error trying to delete the account: ${err.message}`,87});88return;89}90this.sign_out(true);91}9293public async sign_out(94everywhere: boolean,95sign_in: boolean = false,96): Promise<void> {97// disable redirection from sign in/up...98deleteRememberMe(appBasePath);99100// Send a message to the server that the user explicitly101// requested to sign out. The server must clean up resources102// and *invalidate* the remember_me cookie for this client.103try {104await this.account_client.sign_out(everywhere);105} catch (error) {106// The state when this happens could be107// arbitrarily messed up. So... both pop up an error (which user will see),108// and set something in the store, which may or may not get displayed.109const err = `Error signing you out -- ${error}. Please refresh your browser and try again.`;110alert_message({ type: "error", message: err });111this.setState({112sign_out_error: err,113show_sign_out: false,114});115return;116}117// Invalidate the remember_me cookie and force a refresh, since otherwise there could be data118// left in the DOM, which could lead to a vulnerability119// or bleed into the next login somehow.120$(window).off("beforeunload", this.redux.getActions("page").check_unload);121// redirect to sign in page if sign_in is true; otherwise, the landing page:122window.location.href = join(appBasePath, sign_in ? "auth/sign-in" : "/");123}124125push_state(url?: string): void {126if (url == null) {127url = this._last_history_state;128}129if (url == null) {130url = "";131}132this._last_history_state = url;133set_url("/settings" + encode_path(url));134}135136public set_active_tab(tab: string): void {137track("settings", { tab });138this.setState({ active_page: tab });139this.push_state("/" + tab);140}141142// Add an ssh key for this user, with the given fingerprint, title, and value143public add_ssh_key(unsafe_opts: unknown): void {144const opts = define<{145fingerprint: string;146title: string;147value: string;148}>(unsafe_opts, {149fingerprint: required,150title: required,151value: required,152});153this.redux.getTable("account").set({154ssh_keys: {155[opts.fingerprint]: {156title: opts.title,157value: opts.value,158creation_date: Date.now(),159},160},161});162}163164// Delete the ssh key with given fingerprint for this user.165public delete_ssh_key(fingerprint): void {166this.redux.getTable("account").set({167ssh_keys: {168[fingerprint]: null,169},170}); // null is how to tell the backend/synctable to delete this...171}172173public set_account_table(obj: object): void {174this.redux.getTable("account").set(obj);175}176177public set_other_settings(name: string, value: any): void {178this.set_account_table({ other_settings: { [name]: value } });179}180181set_editor_settings = (name: string, value) => {182this.set_account_table({ editor_settings: { [name]: value } });183};184185public set_show_purchase_form(show: boolean) {186// this controls the default state of the "buy a license" purchase form in account → licenses187// by default, it's not showing up188this.setState({ show_purchase_form: show });189}190191setTourDone(tour: string) {192const table = this.redux.getTable("account");193if (!table) return;194const store = this.redux.getStore("account");195if (!store) return;196const tours: string[] = store.get("tours")?.toJS() ?? [];197if (!tours?.includes(tour)) {198tours.push(tour);199table.set({ tours });200}201}202203setTourNotDone(tour: string) {204const table = this.redux.getTable("account");205if (!table) return;206const store = this.redux.getStore("account");207if (!store) return;208const tours: string[] = store.get("tours")?.toJS() ?? [];209if (tours?.includes(tour)) {210// TODO fix this workaround for https://github.com/sagemathinc/cocalc/issues/6929211table.set({ tours: null });212table.set({213// filtering true false strings because of #6929 did create them in the past214tours: tours.filter((x) => x != tour && x !== "true" && x !== "false"),215});216}217}218219processSignUpTags = async () => {220if (!localStorage.sign_up_tags) {221return;222}223try {224if (!webapp_client.is_signed_in()) {225await once(webapp_client, "signed_in");226}227await webapp_client.async_query({228query: {229accounts: {230tags: JSON.parse(localStorage.sign_up_tags),231sign_up_usage_intent: localStorage.sign_up_usage_intent,232},233},234});235delete localStorage.sign_up_tags;236delete localStorage.sign_up_usage_intent;237} catch (err) {238console.warn("processSignUpTags", err);239}240};241242setFragment = (fragment) => {243// @ts-ignore244this.setState({ fragment });245};246247addTag = async (tag: string) => {248const store = this.redux.getStore("account");249if (!store) return;250const tags = store.get("tags");251if (tags?.includes(tag)) {252// already tagged253return;254}255const table = this.redux.getTable("account");256if (!table) return;257const v = tags?.toJS() ?? [];258v.push(tag);259table.set({ tags: v });260try {261await webapp_client.conat_client.hub.system.userSalesloftSync({});262} catch (err) {263console.warn(264"WARNING: issue syncing with salesloft after setting tag",265tag,266err,267);268}269};270271// delete won't be visible in frontend until a browser refresh...272deleteTag = async (tag: string) => {273const store = this.redux.getStore("account");274if (!store) return;275const tags = store.get("tags");276if (!tags?.includes(tag)) {277// already tagged278return;279}280const table = this.redux.getTable("account");281if (!table) return;282const v = tags.toJS().filter((x) => x != tag);283await webapp_client.async_query({ query: { accounts: { tags: v } } });284};285}286287288