2017-05-30 02:08:03 +00:00
|
|
|
// @flow
|
2021-01-28 06:56:40 +00:00
|
|
|
import * as Sentry from "@sentry/react";
|
2020-06-20 20:59:15 +00:00
|
|
|
import invariant from "invariant";
|
2020-08-09 05:53:59 +00:00
|
|
|
import { observable, action, computed, autorun, runInAction } from "mobx";
|
2020-06-20 20:59:15 +00:00
|
|
|
import { getCookie, setCookie, removeCookie } from "tiny-cookie";
|
|
|
|
import RootStore from "stores/RootStore";
|
2021-01-30 05:36:09 +00:00
|
|
|
import Policy from "models/Policy";
|
2020-06-20 20:59:15 +00:00
|
|
|
import Team from "models/Team";
|
2020-08-09 05:53:59 +00:00
|
|
|
import User from "models/User";
|
2021-01-28 06:56:40 +00:00
|
|
|
import env from "env";
|
2020-08-09 05:53:59 +00:00
|
|
|
import { client } from "utils/ApiClient";
|
|
|
|
import { getCookieDomain } from "utils/domains";
|
2017-05-30 02:08:03 +00:00
|
|
|
|
2020-06-20 20:59:15 +00:00
|
|
|
const AUTH_STORE = "AUTH_STORE";
|
2020-07-11 16:51:10 +00:00
|
|
|
const NO_REDIRECT_PATHS = ["/", "/create", "/home"];
|
2017-05-30 02:08:03 +00:00
|
|
|
|
2021-01-30 05:36:09 +00:00
|
|
|
type Service = {|
|
2020-07-10 05:33:07 +00:00
|
|
|
id: string,
|
|
|
|
name: string,
|
|
|
|
authUrl: string,
|
2021-01-30 05:36:09 +00:00
|
|
|
|};
|
2020-07-10 05:33:07 +00:00
|
|
|
|
2021-01-30 05:36:09 +00:00
|
|
|
type Config = {|
|
2020-07-10 05:33:07 +00:00
|
|
|
name?: string,
|
|
|
|
hostname?: string,
|
|
|
|
services: Service[],
|
2021-01-30 05:36:09 +00:00
|
|
|
|};
|
2020-07-10 05:33:07 +00:00
|
|
|
|
2018-12-05 06:24:30 +00:00
|
|
|
export default class AuthStore {
|
2017-05-30 02:08:03 +00:00
|
|
|
@observable user: ?User;
|
|
|
|
@observable team: ?Team;
|
|
|
|
@observable token: ?string;
|
2020-07-10 05:33:07 +00:00
|
|
|
@observable lastSignedIn: ?string;
|
2018-05-31 19:07:49 +00:00
|
|
|
@observable isSaving: boolean = false;
|
2018-03-05 06:18:23 +00:00
|
|
|
@observable isSuspended: boolean = false;
|
2018-03-07 07:38:52 +00:00
|
|
|
@observable suspendedContactEmail: ?string;
|
2020-07-10 05:33:07 +00:00
|
|
|
@observable config: ?Config;
|
2018-12-05 06:24:30 +00:00
|
|
|
rootStore: RootStore;
|
2017-05-30 02:08:03 +00:00
|
|
|
|
2018-12-05 06:24:30 +00:00
|
|
|
constructor(rootStore: RootStore) {
|
2020-11-11 03:43:14 +00:00
|
|
|
this.rootStore = rootStore;
|
|
|
|
|
|
|
|
// attempt to load the previous state of this store from localstorage
|
2018-12-05 06:24:30 +00:00
|
|
|
let data = {};
|
|
|
|
try {
|
2020-06-20 20:59:15 +00:00
|
|
|
data = JSON.parse(localStorage.getItem(AUTH_STORE) || "{}");
|
2018-12-05 06:24:30 +00:00
|
|
|
} catch (_) {
|
|
|
|
// no-op Safari private mode
|
|
|
|
}
|
|
|
|
|
2020-11-11 03:43:14 +00:00
|
|
|
this.rehydrate(data);
|
2018-12-05 06:24:30 +00:00
|
|
|
|
2020-11-11 03:43:14 +00:00
|
|
|
// persists this entire store to localstorage whenever any keys are changed
|
2018-12-05 06:24:30 +00:00
|
|
|
autorun(() => {
|
|
|
|
try {
|
|
|
|
localStorage.setItem(AUTH_STORE, this.asJson);
|
|
|
|
} catch (_) {
|
|
|
|
// no-op Safari private mode
|
|
|
|
}
|
|
|
|
});
|
2020-11-11 03:43:14 +00:00
|
|
|
|
|
|
|
// listen to the localstorage value changing in other tabs to react to
|
|
|
|
// signin/signout events in other tabs and follow suite.
|
|
|
|
window.addEventListener("storage", (event) => {
|
|
|
|
if (event.key === AUTH_STORE) {
|
|
|
|
const data = JSON.parse(event.newValue);
|
|
|
|
|
|
|
|
// if there is no user on the new data then we know the other tab
|
|
|
|
// signed out and we should do the same. Otherwise, if we're not
|
|
|
|
// signed in then hydrate from the received data
|
|
|
|
if (this.token && data.user === null) {
|
|
|
|
this.logout();
|
|
|
|
} else if (!this.token) {
|
|
|
|
this.rehydrate(data);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
@action
|
|
|
|
rehydrate(data: { user: User, team: Team }) {
|
|
|
|
this.user = new User(data.user);
|
|
|
|
this.team = new Team(data.team);
|
|
|
|
this.token = getCookie("accessToken");
|
|
|
|
this.lastSignedIn = getCookie("lastSignedIn");
|
|
|
|
|
|
|
|
if (this.token) {
|
|
|
|
setImmediate(() => this.fetch());
|
|
|
|
}
|
2018-12-05 06:24:30 +00:00
|
|
|
}
|
2017-05-30 02:08:03 +00:00
|
|
|
|
2021-01-30 05:36:09 +00:00
|
|
|
addPolicies = (policies: Policy[]) => {
|
2019-08-22 04:41:37 +00:00
|
|
|
if (policies) {
|
2020-08-09 01:53:11 +00:00
|
|
|
policies.forEach((policy) => this.rootStore.policies.add(policy));
|
2019-08-22 04:41:37 +00:00
|
|
|
}
|
|
|
|
};
|
|
|
|
|
2017-11-10 22:14:30 +00:00
|
|
|
@computed
|
|
|
|
get authenticated(): boolean {
|
2017-05-30 02:08:03 +00:00
|
|
|
return !!this.token;
|
|
|
|
}
|
|
|
|
|
2017-11-10 22:14:30 +00:00
|
|
|
@computed
|
|
|
|
get asJson(): string {
|
2017-05-30 02:08:03 +00:00
|
|
|
return JSON.stringify({
|
|
|
|
user: this.user,
|
|
|
|
team: this.team,
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2020-07-10 05:33:07 +00:00
|
|
|
@action
|
|
|
|
fetchConfig = async () => {
|
|
|
|
const res = await client.post("/auth.config");
|
|
|
|
invariant(res && res.data, "Config not available");
|
|
|
|
this.config = res.data;
|
|
|
|
};
|
|
|
|
|
2017-11-27 06:26:02 +00:00
|
|
|
@action
|
|
|
|
fetch = async () => {
|
|
|
|
try {
|
2020-06-20 20:59:15 +00:00
|
|
|
const res = await client.post("/auth.info");
|
|
|
|
invariant(res && res.data, "Auth not available");
|
2017-11-27 06:26:02 +00:00
|
|
|
|
2020-06-20 20:59:15 +00:00
|
|
|
runInAction("AuthStore#fetch", () => {
|
2019-08-22 04:41:37 +00:00
|
|
|
this.addPolicies(res.policies);
|
2018-12-05 06:24:30 +00:00
|
|
|
const { user, team } = res.data;
|
2019-12-16 02:46:08 +00:00
|
|
|
this.user = new User(user);
|
|
|
|
this.team = new Team(team);
|
2018-12-05 06:24:30 +00:00
|
|
|
|
2021-01-28 06:56:40 +00:00
|
|
|
if (env.SENTRY_DSN) {
|
|
|
|
Sentry.configureScope(function (scope) {
|
2020-02-17 06:58:50 +00:00
|
|
|
scope.setUser({ id: user.id });
|
2020-06-20 20:59:15 +00:00
|
|
|
scope.setExtra("team", team.name);
|
|
|
|
scope.setExtra("teamId", team.id);
|
2020-02-17 06:58:50 +00:00
|
|
|
});
|
2018-12-05 06:24:30 +00:00
|
|
|
}
|
2019-08-06 05:25:19 +00:00
|
|
|
|
|
|
|
// If we came from a redirect then send the user immediately there
|
2020-06-20 20:59:15 +00:00
|
|
|
const postLoginRedirectPath = getCookie("postLoginRedirectPath");
|
2019-08-06 05:25:19 +00:00
|
|
|
if (postLoginRedirectPath) {
|
2020-06-20 20:59:15 +00:00
|
|
|
removeCookie("postLoginRedirectPath");
|
2020-07-11 16:51:10 +00:00
|
|
|
|
|
|
|
if (!NO_REDIRECT_PATHS.includes(postLoginRedirectPath)) {
|
|
|
|
window.location.href = postLoginRedirectPath;
|
|
|
|
}
|
2019-08-06 05:25:19 +00:00
|
|
|
}
|
2017-11-27 06:26:02 +00:00
|
|
|
});
|
2018-02-13 06:44:43 +00:00
|
|
|
} catch (err) {
|
2020-06-20 20:59:15 +00:00
|
|
|
if (err.error === "user_suspended") {
|
2018-03-05 06:18:23 +00:00
|
|
|
this.isSuspended = true;
|
2019-02-16 05:49:48 +00:00
|
|
|
this.suspendedContactEmail = err.data.adminEmail;
|
2018-03-05 06:18:23 +00:00
|
|
|
}
|
2017-11-27 06:26:02 +00:00
|
|
|
}
|
|
|
|
};
|
2017-05-30 02:08:03 +00:00
|
|
|
|
2018-07-07 23:09:39 +00:00
|
|
|
@action
|
|
|
|
deleteUser = async () => {
|
2018-12-05 06:24:30 +00:00
|
|
|
await client.post(`/users.delete`, { confirmation: true });
|
2018-07-07 23:09:39 +00:00
|
|
|
|
2020-06-20 20:59:15 +00:00
|
|
|
runInAction("AuthStore#updateUser", () => {
|
2018-07-07 23:09:39 +00:00
|
|
|
this.user = null;
|
|
|
|
this.team = null;
|
|
|
|
this.token = null;
|
|
|
|
});
|
|
|
|
};
|
|
|
|
|
2018-05-31 18:42:39 +00:00
|
|
|
@action
|
2018-11-18 02:25:10 +00:00
|
|
|
updateUser = async (params: { name?: string, avatarUrl: ?string }) => {
|
2018-05-31 19:07:49 +00:00
|
|
|
this.isSaving = true;
|
2018-05-31 18:42:39 +00:00
|
|
|
|
2018-05-31 19:07:49 +00:00
|
|
|
try {
|
2018-12-05 06:24:30 +00:00
|
|
|
const res = await client.post(`/users.update`, params);
|
2020-06-20 20:59:15 +00:00
|
|
|
invariant(res && res.data, "User response not available");
|
2018-05-31 19:07:49 +00:00
|
|
|
|
2020-06-20 20:59:15 +00:00
|
|
|
runInAction("AuthStore#updateUser", () => {
|
2019-08-22 04:41:37 +00:00
|
|
|
this.addPolicies(res.policies);
|
2018-05-31 19:07:49 +00:00
|
|
|
this.user = res.data;
|
2018-05-31 19:44:32 +00:00
|
|
|
});
|
|
|
|
} finally {
|
|
|
|
this.isSaving = false;
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
@action
|
2018-08-19 23:06:39 +00:00
|
|
|
updateTeam = async (params: {
|
|
|
|
name?: string,
|
|
|
|
avatarUrl?: ?string,
|
|
|
|
sharing?: boolean,
|
|
|
|
}) => {
|
2018-05-31 19:44:32 +00:00
|
|
|
this.isSaving = true;
|
|
|
|
|
|
|
|
try {
|
|
|
|
const res = await client.post(`/team.update`, params);
|
2020-06-20 20:59:15 +00:00
|
|
|
invariant(res && res.data, "Team response not available");
|
2018-05-31 19:44:32 +00:00
|
|
|
|
2020-06-20 20:59:15 +00:00
|
|
|
runInAction("AuthStore#updateTeam", () => {
|
2019-08-22 04:41:37 +00:00
|
|
|
this.addPolicies(res.policies);
|
2019-12-16 02:46:08 +00:00
|
|
|
this.team = new Team(res.data);
|
2018-05-31 19:07:49 +00:00
|
|
|
});
|
|
|
|
} finally {
|
|
|
|
this.isSaving = false;
|
|
|
|
}
|
2018-05-31 18:42:39 +00:00
|
|
|
};
|
|
|
|
|
2017-11-10 22:14:30 +00:00
|
|
|
@action
|
2019-08-06 05:25:19 +00:00
|
|
|
logout = async (savePath: boolean = false) => {
|
|
|
|
// remove user and team from localStorage
|
|
|
|
localStorage.setItem(
|
|
|
|
AUTH_STORE,
|
|
|
|
JSON.stringify({
|
|
|
|
user: null,
|
|
|
|
team: null,
|
|
|
|
})
|
|
|
|
);
|
|
|
|
|
2020-07-10 05:33:07 +00:00
|
|
|
this.token = null;
|
|
|
|
|
2019-08-06 05:25:19 +00:00
|
|
|
// if this logout was forced from an authenticated route then
|
|
|
|
// save the current path so we can go back there once signed in
|
|
|
|
if (savePath) {
|
2020-07-10 05:33:07 +00:00
|
|
|
const pathName = window.location.pathname;
|
|
|
|
|
2020-07-11 16:51:10 +00:00
|
|
|
if (!NO_REDIRECT_PATHS.includes(pathName)) {
|
2020-07-10 05:33:07 +00:00
|
|
|
setCookie("postLoginRedirectPath", pathName);
|
|
|
|
}
|
2019-08-06 05:25:19 +00:00
|
|
|
}
|
2018-02-13 06:44:43 +00:00
|
|
|
|
2018-11-11 22:23:31 +00:00
|
|
|
// remove authentication token itself
|
2020-06-20 20:59:15 +00:00
|
|
|
removeCookie("accessToken", { path: "/" });
|
2018-11-10 07:40:33 +00:00
|
|
|
|
2018-11-11 22:23:31 +00:00
|
|
|
// remove session record on apex cookie
|
|
|
|
const team = this.team;
|
|
|
|
if (team) {
|
2020-06-20 20:59:15 +00:00
|
|
|
const sessions = JSON.parse(getCookie("sessions") || "{}");
|
2018-11-14 07:08:27 +00:00
|
|
|
delete sessions[team.id];
|
2018-11-10 07:40:33 +00:00
|
|
|
|
2020-06-20 20:59:15 +00:00
|
|
|
setCookie("sessions", JSON.stringify(sessions), {
|
2020-05-20 04:05:57 +00:00
|
|
|
domain: getCookieDomain(window.location.hostname),
|
2018-11-10 07:40:33 +00:00
|
|
|
});
|
|
|
|
this.team = null;
|
|
|
|
}
|
2017-05-30 02:08:03 +00:00
|
|
|
};
|
|
|
|
}
|