chore: Move to Typescript (#2783)
This PR moves the entire project to Typescript. Due to the ~1000 ignores this will lead to a messy codebase for a while, but the churn is worth it – all of those ignore comments are places that were never type-safe previously. closes #1282
This commit is contained in:
282
app/stores/AuthStore.ts
Normal file
282
app/stores/AuthStore.ts
Normal file
@@ -0,0 +1,282 @@
|
||||
import * as Sentry from "@sentry/react";
|
||||
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 Policy from "~/models/Policy";
|
||||
import Team from "~/models/Team";
|
||||
import User from "~/models/User";
|
||||
import env from "~/env";
|
||||
import { client } from "~/utils/ApiClient";
|
||||
import { getCookieDomain } from "~/utils/domains";
|
||||
|
||||
const AUTH_STORE = "AUTH_STORE";
|
||||
const NO_REDIRECT_PATHS = ["/", "/create", "/home"];
|
||||
|
||||
type PersistedData = {
|
||||
user?: User;
|
||||
team?: Team;
|
||||
policies?: Policy[];
|
||||
};
|
||||
|
||||
type Provider = {
|
||||
id: string;
|
||||
name: string;
|
||||
authUrl: string;
|
||||
};
|
||||
|
||||
type Config = {
|
||||
name?: string;
|
||||
hostname?: string;
|
||||
providers: Provider[];
|
||||
};
|
||||
|
||||
export default class AuthStore {
|
||||
@observable
|
||||
user: User | null | undefined;
|
||||
|
||||
@observable
|
||||
team: Team | null | undefined;
|
||||
|
||||
@observable
|
||||
token: string | null | undefined;
|
||||
|
||||
@observable
|
||||
policies: Policy[] = [];
|
||||
|
||||
@observable
|
||||
lastSignedIn: string | null | undefined;
|
||||
|
||||
@observable
|
||||
isSaving = false;
|
||||
|
||||
@observable
|
||||
isSuspended = false;
|
||||
|
||||
@observable
|
||||
suspendedContactEmail: string | null | undefined;
|
||||
|
||||
@observable
|
||||
config: Config | null | undefined;
|
||||
|
||||
rootStore: RootStore;
|
||||
|
||||
constructor(rootStore: RootStore) {
|
||||
this.rootStore = rootStore;
|
||||
// attempt to load the previous state of this store from localstorage
|
||||
let data: PersistedData = {};
|
||||
|
||||
try {
|
||||
data = JSON.parse(localStorage.getItem(AUTH_STORE) || "{}");
|
||||
} catch (_) {
|
||||
// no-op Safari private mode
|
||||
}
|
||||
|
||||
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 && event.newValue) {
|
||||
const data: PersistedData | null | undefined = JSON.parse(
|
||||
event.newValue
|
||||
);
|
||||
// data may be null if key is deleted in localStorage
|
||||
if (!data) return;
|
||||
|
||||
// If we're not signed in then hydrate from the received data, otherwise if
|
||||
// we are signed in and the received data contains no user then sign out
|
||||
if (this.authenticated) {
|
||||
if (data.user === null) {
|
||||
this.logout();
|
||||
}
|
||||
} else {
|
||||
this.rehydrate(data);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@action
|
||||
rehydrate(data: PersistedData) {
|
||||
this.user = data.user ? new User(data.user, this) : undefined;
|
||||
this.team = data.team ? new Team(data.team, this) : undefined;
|
||||
this.token = getCookie("accessToken");
|
||||
this.lastSignedIn = getCookie("lastSignedIn");
|
||||
this.addPolicies(data.policies);
|
||||
|
||||
if (this.token) {
|
||||
setImmediate(() => this.fetch());
|
||||
}
|
||||
}
|
||||
|
||||
addPolicies(policies?: Policy[]) {
|
||||
if (policies) {
|
||||
// cache policies in this store so that they are persisted between sessions
|
||||
this.policies = 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,
|
||||
policies: this.policies,
|
||||
});
|
||||
}
|
||||
|
||||
@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);
|
||||
this.team = new Team(team, this);
|
||||
|
||||
if (env.SENTRY_DSN) {
|
||||
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 | null;
|
||||
language?: 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 = new User(res.data, this);
|
||||
});
|
||||
} finally {
|
||||
this.isSaving = false;
|
||||
}
|
||||
};
|
||||
|
||||
@action
|
||||
updateTeam = async (params: {
|
||||
name?: string;
|
||||
avatarUrl?: string | null | undefined;
|
||||
sharing?: boolean;
|
||||
collaborativeEditing?: boolean;
|
||||
subdomain?: string | null | undefined;
|
||||
}) => {
|
||||
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, this);
|
||||
});
|
||||
} finally {
|
||||
this.isSaving = false;
|
||||
}
|
||||
};
|
||||
|
||||
@action
|
||||
logout = async (savePath = false) => {
|
||||
// remove user and team from localStorage
|
||||
localStorage.setItem(
|
||||
AUTH_STORE,
|
||||
JSON.stringify({
|
||||
user: null,
|
||||
team: null,
|
||||
policies: [],
|
||||
})
|
||||
);
|
||||
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;
|
||||
}
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user