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