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:
parent
41efe05be4
commit
ce503ea730
3 changed files with 259 additions and 4 deletions
|
@ -8,14 +8,17 @@ import {
|
||||||
MenuButton,
|
MenuButton,
|
||||||
MenuList,
|
MenuList,
|
||||||
MenuItem,
|
MenuItem,
|
||||||
|
useDisclosure,
|
||||||
} from "@chakra-ui/react";
|
} from "@chakra-ui/react";
|
||||||
import { HamburgerIcon } from "@chakra-ui/icons";
|
import { HamburgerIcon } from "@chakra-ui/icons";
|
||||||
import { Link, useLocation } from "react-router-dom";
|
import { Link, useLocation } from "react-router-dom";
|
||||||
import { useAuth0 } from "@auth0/auth0-react";
|
|
||||||
import { ChevronLeftIcon } from "@chakra-ui/icons";
|
import { ChevronLeftIcon } from "@chakra-ui/icons";
|
||||||
import Image from "next/image";
|
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";
|
import HomeLinkIcon from "./images/home-link-icon.png";
|
||||||
|
|
||||||
function GlobalHeader() {
|
function GlobalHeader() {
|
||||||
|
@ -109,8 +112,8 @@ function HomeLink(props) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function UserNavBarSection() {
|
function UserNavBarSection() {
|
||||||
const { loginWithRedirect, logout } = useAuth0();
|
|
||||||
const { isLoading, isLoggedIn, id, username } = useCurrentUser();
|
const { isLoading, isLoggedIn, id, username } = useCurrentUser();
|
||||||
|
const { logout } = useLoginActions();
|
||||||
|
|
||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
return null;
|
return null;
|
||||||
|
@ -150,12 +153,43 @@ function UserNavBarSection() {
|
||||||
<NavButton as={Link} to="/modeling">
|
<NavButton as={Link} to="/modeling">
|
||||||
Modeling
|
Modeling
|
||||||
</NavButton>
|
</NavButton>
|
||||||
<NavButton onClick={() => loginWithRedirect()}>Log in</NavButton>
|
<LoginButton />
|
||||||
</HStack>
|
</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
|
* Renders the given <NavLinkItem /> children as a dropdown menu or as a list
|
||||||
* of buttons, depending on the screen size.
|
* of buttons, depending on the screen size.
|
||||||
|
|
135
src/app/components/LoginModal.js
Normal file
135
src/app/components/LoginModal.js
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
|
@ -1,4 +1,6 @@
|
||||||
import { useAuth0 } from "@auth0/auth0-react";
|
import { useAuth0 } from "@auth0/auth0-react";
|
||||||
|
import { useEffect } from "react";
|
||||||
|
import { useLocalStorage } from "../util";
|
||||||
|
|
||||||
function useCurrentUser() {
|
function useCurrentUser() {
|
||||||
const { isLoading, isAuthenticated, user } = useAuth0();
|
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;
|
export default useCurrentUser;
|
||||||
|
|
Loading…
Reference in a new issue