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!
264 lines
8.3 KiB
JavaScript
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;
|