Start building a login form, behind a feature flag

Thinking about longevity, I think I wanna cut Auth0 loose, and just go back to using our own auth.

I had figured at the time that I didn't want to integrate with OpenNeo ID's whole mess, and I didn't want to write a whole new auth system, so Auth0 seemed to make things easier.

But now, it's just kinda a lot to be carrying along an external service as a dependency for login, especially when we've got all the stuff in the database right here. I wanna remove architecture pieces! Get it outta here!

And I'll finally build account creation from the 2020 site while I'm at it, which seemed like it was gonna be a bit of a pain with Auth0 and syncing anyway. (I think at the time I was a bit more optimistic about a full transfer from one system to another, but that's much further off than I realized, and this path will be much better for keeping things in sync.)
This commit is contained in:
Emi Matchu 2022-08-16 17:34:51 -07:00
parent 41efe05be4
commit ce503ea730
3 changed files with 259 additions and 4 deletions

View file

@ -8,14 +8,17 @@ import {
MenuButton,
MenuList,
MenuItem,
useDisclosure,
} from "@chakra-ui/react";
import { HamburgerIcon } from "@chakra-ui/icons";
import { Link, useLocation } from "react-router-dom";
import { useAuth0 } from "@auth0/auth0-react";
import { ChevronLeftIcon } from "@chakra-ui/icons";
import Image from "next/image";
import useCurrentUser from "./components/useCurrentUser";
import useCurrentUser, {
useAuthModeFeatureFlag,
useLoginActions,
} from "./components/useCurrentUser";
import HomeLinkIcon from "./images/home-link-icon.png";
function GlobalHeader() {
@ -109,8 +112,8 @@ function HomeLink(props) {
}
function UserNavBarSection() {
const { loginWithRedirect, logout } = useAuth0();
const { isLoading, isLoggedIn, id, username } = useCurrentUser();
const { logout } = useLoginActions();
if (isLoading) {
return null;
@ -150,12 +153,43 @@ function UserNavBarSection() {
<NavButton as={Link} to="/modeling">
Modeling
</NavButton>
<NavButton onClick={() => loginWithRedirect()}>Log in</NavButton>
<LoginButton />
</HStack>
);
}
}
function LoginButton() {
const authMode = useAuthModeFeatureFlag();
const { startLogin } = useLoginActions();
const { isOpen, onOpen, onClose } = useDisclosure();
const onClick = () => {
if (authMode === "auth0") {
startLogin();
} else if (authMode === "db") {
onOpen();
} else {
throw new Error(`unexpected auth mode: ${JSON.stringify(authMode)}`);
}
};
return (
<>
<NavButton onClick={onClick}>Log in</NavButton>
{authMode === "db" && (
<React.Suspense fallback="">
<LoginModal isOpen={isOpen} onClose={onClose} />
</React.Suspense>
)}
</>
);
}
// I don't wanna load all these Chakra components as part of the bundle for
// every single page. Split it out!
const LoginModal = React.lazy(() => import("./components/LoginModal"));
/**
* Renders the given <NavLinkItem /> children as a dropdown menu or as a list
* of buttons, depending on the screen size.

View file

@ -0,0 +1,135 @@
import {
Box,
Button,
FormControl,
FormHelperText,
FormLabel,
Input,
Modal,
ModalBody,
ModalCloseButton,
ModalContent,
ModalHeader,
ModalOverlay,
Tab,
TabList,
TabPanel,
TabPanels,
Tabs,
} from "@chakra-ui/react";
import React from "react";
export default function LoginModal({ isOpen, onClose }) {
return (
<Modal isOpen={isOpen} onClose={onClose}>
<ModalOverlay />
<ModalContent>
<ModalHeader>Welcome back to Dress to Impress! </ModalHeader>
<ModalCloseButton />
<Tabs>
<TabList>
<Tab>Log in</Tab>
<Tab>Create account</Tab>
</TabList>
<TabPanels>
<TabPanel>
<ModalBody>
<LoginForm />
</ModalBody>
</TabPanel>
<TabPanel>
<ModalBody>
<CreateAccountForm />
</ModalBody>
</TabPanel>
</TabPanels>
</Tabs>
</ModalContent>
</Modal>
);
}
function LoginForm() {
const onSubmit = (e) => {
e.preventDefault();
alert("TODO: Log in!");
};
return (
<form onSubmit={onSubmit}>
<FormControl>
<FormLabel>DTI Username</FormLabel>
<Input type="text" />
<FormHelperText>
This is separate from your Neopets.com account.
</FormHelperText>
</FormControl>
<Box height="4" />
<FormControl>
<FormLabel>DTI Password</FormLabel>
<Input type="password" />
<FormHelperText>
Careful, never enter your Neopets password on another site!
</FormHelperText>
</FormControl>
<Box marginTop="6" display="flex" alignItems="center">
<Button size="sm" onClick={() => alert("TODO: Forgot password")}>
Forgot password?
</Button>
<Box flex="1 0 auto" width="4" />
<Button type="submit" colorScheme="green">
Log in
</Button>
</Box>
</form>
);
}
function CreateAccountForm() {
const onSubmit = (e) => {
e.preventDefault();
alert("TODO: Create account!");
};
return (
<form onSubmit={onSubmit}>
<FormControl>
<FormLabel>DTI Username</FormLabel>
<Input type="text" />
<FormHelperText>
This will be separate from your Neopets.com account.
</FormHelperText>
</FormControl>
<Box height="4" />
<FormControl>
<FormLabel>DTI Password</FormLabel>
<Input type="password" />
<FormHelperText>
Careful, never use your Neopets password for another site!
</FormHelperText>
</FormControl>
<Box height="4" />
<FormControl>
<FormLabel>Confirm DTI Password</FormLabel>
<Input type="password" />
<FormHelperText>One more time, to make sure!</FormHelperText>
</FormControl>
<Box height="4" />
<FormControl>
<FormLabel>Email address</FormLabel>
<Input type="password" />
<FormHelperText>
We'll use this in the future if you need to reset your password, or
for us to contact you about your account. We won't sell this address,
and we won't send marketing-y emails.
</FormHelperText>
</FormControl>
<Box height="6" />
<Box display="flex" justifyContent="flex-end">
<Button type="submit" colorScheme="green">
Create account
</Button>
</Box>
</form>
);
}

View file

@ -1,4 +1,6 @@
import { useAuth0 } from "@auth0/auth0-react";
import { useEffect } from "react";
import { useLocalStorage } from "../util";
function useCurrentUser() {
const { isLoading, isAuthenticated, user } = useAuth0();
@ -61,4 +63,88 @@ function getUserInfo(user) {
};
}
/**
* 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 useLoginActions() {
const {
loginWithRedirect: auth0StartLogin,
logout: auth0Logout,
} = useAuth0();
const authMode = useAuthModeFeatureFlag();
if (authMode === "auth0") {
return { startLogin: auth0StartLogin, logout: auth0Logout };
} else if (authMode === "db") {
return {
startLogin: () => {
console.error(
`Error: Cannot call startLogin in db login mode. Open a ` +
`<LoginModal /> instead.`
);
alert(
`Error: Cannot call startLogin in db login mode. Open a ` +
`<LoginModal /> instead.`
);
},
logout: () => {
alert(`TODO: logout`);
},
};
} else {
console.error(`unexpected auth mode: ${JSON.stringify(authMode)}`);
return { startLogin: () => {}, logout: () => {} };
}
}
/**
* 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 😅)
const [savedValue] = useLocalStorage("DTIAuthModeFeatureFlag", null);
useEffect(() => {
window.setAuthModeFeatureFlag = setAuthModeFeatureFlag;
});
if (!["auth0", "db", null].includes(savedValue)) {
console.warn(
`Unexpected DTIAuthModeFeatureFlag value: %o. Treating as null.`,
savedValue
);
return 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;