1
0
Fork 0
impress-2020/src/app/components/useCurrentUser.js
Matchu 0159bf18e0 Remove Cypress login hacks
We previously had a lil trick to help Cypress perform an Auth0 login without using the whole Auth0 UI. (I forget exactly why!)

With Cypress deleted, we don't need this code anymore!
2022-09-14 17:45:53 -07:00

264 lines
8.3 KiB
JavaScript

import { gql, useMutation, useQuery } from "@apollo/client";
import { useAuth0 } from "@auth0/auth0-react";
import { useEffect } from "react";
import { useLocalStorage } from "../util";
const NOT_LOGGED_IN_USER = {
isLoading: false,
isLoggedIn: false,
id: null,
username: null,
};
function useCurrentUser() {
const [authMode] = useAuthModeFeatureFlag();
const currentUserViaAuth0 = useCurrentUserViaAuth0({
isEnabled: authMode === "auth0",
});
const currentUserViaDb = useCurrentUserViaDb({
isEnabled: authMode === "db",
});
// In development, you can start the server with
// `IMPRESS_LOG_IN_AS=12345 vc dev` to simulate logging in as user 12345.
//
// This flag shouldn't be present in prod anyway, but the dev check is an
// extra safety precaution!
//
// NOTE: In package.json, we forward the flag to REACT_APP_IMPRESS_LOG_IN_AS,
// because create-react-app only forwards flags with that prefix.
if (
process.env["NODE_ENV"] === "development" &&
process.env["REACT_APP_IMPRESS_LOG_IN_AS"]
) {
const id = process.env["REACT_APP_IMPRESS_LOG_IN_AS"];
return {
isLoading: false,
isLoggedIn: true,
id,
username: `<Simulated User ${id}>`,
};
}
if (authMode === "auth0") {
return currentUserViaAuth0;
} else if (authMode === "db") {
return currentUserViaDb;
} else {
console.error(`Unexpected auth mode: ${JSON.stringify(authMode)}`);
return NOT_LOGGED_IN_USER;
}
}
function useCurrentUserViaAuth0({ isEnabled }) {
// NOTE: I don't think we can actually, by the rule of hooks, *not* ask for
// Auth0 login state when `isEnabled` is false, because `useAuth0`
// doesn't accept a similar parameter to disable itself. We'll just
// accept the redundant network effort during rollout, then delete it
// when we're done. (So, the param isn't actually doing a whole lot; I
// mostly have it for consistency with `useCurrentUserViaDb`, to make
// it clear where the real difference is.)
const { isLoading, isAuthenticated, user } = useAuth0();
if (!isEnabled) {
return NOT_LOGGED_IN_USER;
} else if (isLoading) {
return { ...NOT_LOGGED_IN_USER, isLoading: true };
} else if (!isAuthenticated) {
return NOT_LOGGED_IN_USER;
} else {
return {
isLoading: false,
isLoggedIn: true,
...getUserInfoFromAuth0Data(user),
};
}
}
function useCurrentUserViaDb({ isEnabled }) {
const { loading, data } = useQuery(
gql`
query useCurrentUser {
currentUser {
id
username
}
}
`,
{
skip: !isEnabled,
onError: (error) => {
// On error, we don't report anything to the user, but we do keep a
// record in the console. We figure that most errors are likely to be
// solvable by retrying the login button and creating a new session,
// which the user would do without an error prompt anyway; and if not,
// they'll either get an error when they try, or they'll see their
// login state continue to not work, which should be a clear hint that
// something is wrong and they need to reach out.
console.error("[useCurrentUser] Couldn't get current user:", error);
},
// We set this option so that, when we enter the loading state after
// logging in and evicting `currentUser` from the cache, we'll see the
// `loading: true` state. Otherwise, Apollo just leaves the return value
// as-is until the new data comes in, so the user sees the logged-out
// state until the behind-the-scenes update to this query finishes.
notifyOnNetworkStatusChange: true,
}
);
if (!isEnabled) {
return NOT_LOGGED_IN_USER;
} else if (loading) {
return { ...NOT_LOGGED_IN_USER, isLoading: true };
} else if (data?.currentUser == null) {
return NOT_LOGGED_IN_USER;
} else {
return {
isLoading: false,
isLoggedIn: true,
id: data.currentUser.id,
username: data.currentUser.username,
};
}
}
function getUserInfoFromAuth0Data(user) {
return {
id: user.sub?.match(/^auth0\|impress-([0-9]+)$/)?.[1],
username: user["https://oauth.impress-2020.openneo.net/username"],
};
}
/**
* useLoginActions returns a `startLogin` function to start login with Auth0,
* and a `logout` function to logout from whatever auth mode is in use.
*
* Note that `startLogin` is only supported with the Auth0 auto mode. In db
* mode, you should open a `LoginModal` instead!
*/
export function useLogout() {
const { logout: logoutWithAuth0 } = useAuth0();
const [authMode] = useAuthModeFeatureFlag();
const [sendLogoutMutation, { loading, error }] = useMutation(
gql`
mutation useLogout_Logout {
logout {
id
}
}
`,
{
update: (cache, { data }) => {
// Evict the `currentUser` from the cache, which will force all queries
// on the page that depend on it to update. (This includes the
// GlobalHeader that shows who you're logged in as!)
//
// We also evict the user themself, to force-update things that we're
// allowed to see about this user (e.g. private lists).
//
// I don't do any optimistic UI here, because auth is complex enough
// that I'd rather only show logout success after validating it through
// an actual server round-trip.
cache.evict({ id: "ROOT_QUERY", fieldName: "currentUser" });
if (data.logout?.id != null) {
cache.evict({ id: `User:${data.logout.id}` });
}
cache.gc();
},
}
);
const logoutWithDb = () => {
sendLogoutMutation().catch((e) => {}); // handled in error UI
};
if (authMode === "auth0") {
return [logoutWithAuth0, { loading: false, error: null }];
} else if (authMode === "db") {
return [logoutWithDb, { loading, error }];
} else {
console.error(`unexpected auth mode: ${JSON.stringify(authMode)}`);
return [() => {}, { loading: false, error: null }];
}
}
/**
* useAuthModeFeatureFlag returns "auth0" by default, but "db" if you're trying
* the new db-backed login mode.
*
* To set this manually, run `window.setAuthModeFeatureFlag("db")` in your
* browser console.
*/
export function useAuthModeFeatureFlag() {
// We'll probably add a like, experimental gradual rollout thing here too.
// But for now we just check your device's local storage! (This is why we
// default to `null` instead of "auth0", I want to be unambiguous that this
// is the *absence* of a localStorage value, and not risk accidentally
// setting this override value to auth0 on everyone's devices 😅)
let [savedValue, setSavedValue] = useLocalStorage(
"DTIAuthModeFeatureFlag",
null
);
useEffect(() => {
window.setAuthModeFeatureFlag = setAuthModeFeatureFlag;
});
if (!["auth0", "db", null].includes(savedValue)) {
console.warn(
`Unexpected DTIAuthModeFeatureFlag value: %o. Ignoring.`,
savedValue
);
savedValue = null;
}
const value = savedValue || "auth0";
return [value, setSavedValue];
}
/**
* getAuthModeFeatureFlag returns the authMode at the time it's called.
* It's generally preferable to use `useAuthModeFeatureFlag` in a React
* setting, but we use this instead for Apollo stuff!
*/
export function getAuthModeFeatureFlag() {
const savedValueString = localStorage.getItem("DTIAuthModeFeatureFlag");
let savedValue;
try {
savedValue = JSON.parse(savedValueString);
} catch (error) {
console.warn(`DTIAuthModeFeatureFlag was not valid JSON. Ignoring.`);
savedValue = null;
}
if (!["auth0", "db", null].includes(savedValue)) {
console.warn(
`Unexpected DTIAuthModeFeatureFlag value: %o. Ignoring.`,
savedValue
);
savedValue = null;
}
return savedValue || "auth0";
}
/**
* setAuthModeFeatureFlag is mounted on the window, so you can call it from the
* browser console to set this override manually.
*/
function setAuthModeFeatureFlag(newValue) {
if (!["auth0", "db", null].includes(newValue)) {
throw new Error(`Auth mode must be "auth0", "db", or null.`);
}
localStorage.setItem("DTIAuthModeFeatureFlag", JSON.stringify(newValue));
// The useLocalStorage hook isn't *quite* good enough to catch this change.
// Let's just reload the page lmao.
window.location.reload();
}
export default useCurrentUser;