This repository has been archived on 2022-08-14. You can view files and clone it, but cannot push or open issues or pull requests.
outline/app/stores/AuthStore.js
Tom Moor 5e7bbdc111
feat: Sync sessions across browser tabs (#1641)
* feat: Sync sessions across browser tabs

If the user signs in or out in one tab, then make sure their session on the same subdomain is kept in sync on other tabs
2020-11-10 19:43:14 -08:00

242 lines
6.2 KiB
JavaScript

// @flow
import invariant from "invariant";
import { observable, action, computed, autorun, runInAction } from "mobx";
import { getCookie, setCookie, removeCookie } from "tiny-cookie";
import RootStore from "stores/RootStore";
import Team from "models/Team";
import User from "models/User";
import { client } from "utils/ApiClient";
import { getCookieDomain } from "utils/domains";
const AUTH_STORE = "AUTH_STORE";
const NO_REDIRECT_PATHS = ["/", "/create", "/home"];
type Service = {
id: string,
name: string,
authUrl: string,
};
type Config = {
name?: string,
hostname?: string,
services: Service[],
};
export default class AuthStore {
@observable user: ?User;
@observable team: ?Team;
@observable token: ?string;
@observable lastSignedIn: ?string;
@observable isSaving: boolean = false;
@observable isSuspended: boolean = false;
@observable suspendedContactEmail: ?string;
@observable config: ?Config;
rootStore: RootStore;
constructor(rootStore: RootStore) {
this.rootStore = rootStore;
// attempt to load the previous state of this store from localstorage
let data = {};
try {
data = JSON.parse(localStorage.getItem(AUTH_STORE) || "{}");
} catch (_) {
// no-op Safari private mode
}
setImmediate(() => this.fetchConfig());
this.rehydrate(data);
// persists this entire store to localstorage whenever any keys are changed
autorun(() => {
try {
localStorage.setItem(AUTH_STORE, this.asJson);
} catch (_) {
// no-op Safari private mode
}
});
// 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());
}
}
addPolicies = (policies) => {
if (policies) {
policies.forEach((policy) => this.rootStore.policies.add(policy));
}
};
@computed
get authenticated(): boolean {
return !!this.token;
}
@computed
get asJson(): string {
return JSON.stringify({
user: this.user,
team: this.team,
});
}
@action
fetchConfig = async () => {
const res = await client.post("/auth.config");
invariant(res && res.data, "Config not available");
this.config = res.data;
};
@action
fetch = async () => {
try {
const res = await client.post("/auth.info");
invariant(res && res.data, "Auth not available");
runInAction("AuthStore#fetch", () => {
this.addPolicies(res.policies);
const { user, team } = res.data;
this.user = new User(user);
this.team = new Team(team);
if (window.Sentry) {
window.Sentry.configureScope(function (scope) {
scope.setUser({ id: user.id });
scope.setExtra("team", team.name);
scope.setExtra("teamId", team.id);
});
}
// If we came from a redirect then send the user immediately there
const postLoginRedirectPath = getCookie("postLoginRedirectPath");
if (postLoginRedirectPath) {
removeCookie("postLoginRedirectPath");
if (!NO_REDIRECT_PATHS.includes(postLoginRedirectPath)) {
window.location.href = postLoginRedirectPath;
}
}
});
} catch (err) {
if (err.error === "user_suspended") {
this.isSuspended = true;
this.suspendedContactEmail = err.data.adminEmail;
}
}
};
@action
deleteUser = async () => {
await client.post(`/users.delete`, { confirmation: true });
runInAction("AuthStore#updateUser", () => {
this.user = null;
this.team = null;
this.token = null;
});
};
@action
updateUser = async (params: { name?: string, avatarUrl: ?string }) => {
this.isSaving = true;
try {
const res = await client.post(`/users.update`, params);
invariant(res && res.data, "User response not available");
runInAction("AuthStore#updateUser", () => {
this.addPolicies(res.policies);
this.user = res.data;
});
} finally {
this.isSaving = false;
}
};
@action
updateTeam = async (params: {
name?: string,
avatarUrl?: ?string,
sharing?: boolean,
}) => {
this.isSaving = true;
try {
const res = await client.post(`/team.update`, params);
invariant(res && res.data, "Team response not available");
runInAction("AuthStore#updateTeam", () => {
this.addPolicies(res.policies);
this.team = new Team(res.data);
});
} finally {
this.isSaving = false;
}
};
@action
logout = async (savePath: boolean = false) => {
// remove user and team from localStorage
localStorage.setItem(
AUTH_STORE,
JSON.stringify({
user: null,
team: null,
})
);
this.token = null;
// 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) {
const pathName = window.location.pathname;
if (!NO_REDIRECT_PATHS.includes(pathName)) {
setCookie("postLoginRedirectPath", pathName);
}
}
// remove authentication token itself
removeCookie("accessToken", { path: "/" });
// remove session record on apex cookie
const team = this.team;
if (team) {
const sessions = JSON.parse(getCookie("sessions") || "{}");
delete sessions[team.id];
setCookie("sessions", JSON.stringify(sessions), {
domain: getCookieDomain(window.location.hostname),
});
this.team = null;
}
};
}