diff options
author | Galen Guyer <galen@galenguyer.com> | 2022-05-30 15:07:04 -0400 |
---|---|---|
committer | Galen Guyer <galen@galenguyer.com> | 2022-05-30 15:07:04 -0400 |
commit | 0f8bb91256c3dd8319a5d50e16d29e9a031e5af1 (patch) | |
tree | f9bd7c25b3b490e6741b4c09cd1a16c92b28792e | |
parent | b718d6707e708c2ed346e42ec65995810ef739e8 (diff) |
add auth and login page
-rw-r--r-- | package.json | 2 | ||||
-rw-r--r-- | pnpm-lock.yaml | 13 | ||||
-rw-r--r-- | src/hooks/useAuth.js | 44 | ||||
-rw-r--r-- | src/hooks/useFetch.js | 17 | ||||
-rw-r--r-- | src/index.js | 18 | ||||
-rw-r--r-- | src/routes/Login.js | 121 |
6 files changed, 208 insertions, 7 deletions
diff --git a/package.json b/package.json index 3160cc7..59e4914 100644 --- a/package.json +++ b/package.json @@ -13,6 +13,7 @@ "@testing-library/user-event": "^13.5.0", "react": "^18.1.0", "react-dom": "^18.1.0", + "react-jwt": "^1.1.6", "react-router-dom": "^6.3.0", "react-scripts": "5.0.1", "typescript": "^4.7.2", @@ -24,6 +25,7 @@ "test": "react-scripts test", "eject": "react-scripts eject" }, + "proxy": "http://127.0.0.1:8000", "eslintConfig": { "extends": [ "react-app", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b8261cc..c307ddf 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -11,6 +11,7 @@ specifiers: '@testing-library/user-event': ^13.5.0 react: ^18.1.0 react-dom: ^18.1.0 + react-jwt: ^1.1.6 react-router-dom: ^6.3.0 react-scripts: 5.0.1 typescript: ^4.7.2 @@ -27,6 +28,7 @@ dependencies: '@testing-library/user-event': 13.5.0_tlwynutqiyp5mns3woioasuxnq react: 18.1.0 react-dom: 18.1.0_react@18.1.0 + react-jwt: 1.1.6_react@18.1.0 react-router-dom: 6.3.0_ef5jwxihqo6n7gxfmzogljlgcm react-scripts: 5.0.1_kyabxajlhps7o43dn7edo3gzxi typescript: 4.7.2 @@ -8010,6 +8012,17 @@ packages: resolution: {integrity: sha512-Fl7FuabXsJnV5Q1qIOQwx/sagGF18kogb4gpfcG4gjLBWO0WDiiz1ko/ExayuxE7InyQkBLkxRFG5oxY6Uu3Kg==} dev: false + /react-jwt/1.1.6_react@18.1.0: + resolution: {integrity: sha512-Yp+FmwkTvYUuO5gu6sFApLXFuMAIAlh1NEfxcB2EGnmS1chKqXXv77vp23IphV1UpSyMtm0wrphUZfDSr8GiSA==} + engines: {node: '>=10'} + peerDependencies: + react: ^16.0.0 || ^17.0.0 || ^18.0.0 + dependencies: + react: 18.1.0 + optionalDependencies: + fsevents: 2.3.2 + dev: false + /react-refresh/0.11.0: resolution: {integrity: sha512-F27qZr8uUqwhWZboondsPx8tnC3Ct3SxZA3V5WyEvujRyyNv0VYPhoBg1gZ8/MV5tubQp76Trw8lTv9hzRBa+A==} engines: {node: '>=0.10.0'} diff --git a/src/hooks/useAuth.js b/src/hooks/useAuth.js new file mode 100644 index 0000000..aad8edf --- /dev/null +++ b/src/hooks/useAuth.js @@ -0,0 +1,44 @@ +import React from "react"; +import { isExpired } from "react-jwt"; +import { Navigate, useLocation } from "react-router-dom"; + +let AuthContext = React.createContext(); + +export function AuthProvider({ children }) { + let [token, setToken] = React.useState(localStorage.getItem("token")); + + let signin = (token, callback) => { + setToken(token); + localStorage.setItem("token", token); + callback(); + }; + + let signout = (callback) => { + setToken(null); + localStorage.removeItem("token"); + callback(); + }; + + let value = { token, signin, signout }; + + return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>; +} + +export function useAuth() { + return React.useContext(AuthContext); +} + +export function RequireAuth({ children }) { + let auth = useAuth(); + let location = useLocation(); + + if (!auth.token || isExpired(auth.token)) { + // Redirect them to the /login page, but save the current location they were + // trying to go to when they were redirected. This allows us to send them + // along to that page after they login, which is a nicer user experience + // than dropping them off on the home page. + return <Navigate to="/login" state={{ from: location }} replace />; + } + + return children; +} diff --git a/src/hooks/useFetch.js b/src/hooks/useFetch.js new file mode 100644 index 0000000..d098e2f --- /dev/null +++ b/src/hooks/useFetch.js @@ -0,0 +1,17 @@ +import { useEffect, useState } from "react"; + +export default function useFetch(url, options) { + const [data, setData] = useState(null); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + useEffect(() => { + fetch(url, options) + .then((resp) => resp.json()) + .then((resp) => setData(resp)) + .catch((err) => setError(err)) + .finally(() => setLoading(false)); + }, []); + + return { data, loading, error }; +} diff --git a/src/index.js b/src/index.js index f383253..ea84600 100644 --- a/src/index.js +++ b/src/index.js @@ -4,18 +4,22 @@ import { BrowserRouter, Routes, Route, Link } from "react-router-dom"; import './index.css'; // import App from './App'; import SignUp from './routes/SignUp.js'; +import Login from './routes/Login'; import reportWebVitals from './reportWebVitals'; +import { AuthProvider } from './hooks/useAuth'; const root = ReactDOM.createRoot(document.getElementById('root')); root.render( <React.StrictMode> - <BrowserRouter> - <Routes> - {/* <Route path="/" element={<Home />} /> - <Route path="login" element={<Login />} /> */} - <Route path="signup" element={<SignUp />} /> - </Routes> - </BrowserRouter> + <AuthProvider> + <BrowserRouter> + <Routes> + { /* <Route path="/" element={<Home />} /> */} + <Route path="login" element={<Login />} /> + <Route path="signup" element={<SignUp />} /> + </Routes> + </BrowserRouter> + </AuthProvider> </React.StrictMode> ); diff --git a/src/routes/Login.js b/src/routes/Login.js new file mode 100644 index 0000000..8dfccb0 --- /dev/null +++ b/src/routes/Login.js @@ -0,0 +1,121 @@ +import { useEffect, useState } from "react"; +import React from "react"; +import { + Routes, + Route, + Link, + useNavigate, + useLocation, + Navigate, + Outlet, +} from "react-router-dom"; +import Button from "../uikit/Button.js"; +import Input from "../uikit/Input.js" +import { useAuth } from "../hooks/useAuth"; +import { isExpired } from "react-jwt"; +import { styled } from "@stitches/react"; + +const Flex = styled("div", { + display: "flex", + alignItems: "center", + justifyContent: "center", + minHeight: "100vh", +}); + +const LoginCard = styled("div", { + boxShadow: "0 4px 6px rgba(0,0,0,0.1)", + borderRadius: "4px", + padding: "1em", + border: "1px solid #D4D4D8", + backgroundColor: "#F4F4F5", +}); + +const Title = styled("h1", { + textAlign: "center", + fontSize: "6em", + margin: "1rem 0", + fontWeight: 300, +}); + +const Subtitle = styled("h1", { + textAlign: "center", + fontSize: "2.5em", + margin: "1rem 0", + fontWeight: 300 +}); + +const StyledLabel = styled("label", { + display: "block", + paddingBottom: "0.25em", + fontSize: "0.9em", +}); + + +const AlignRight = styled("div", { + "display": "flex", + "alignItems": "right", + "justifyContent": "right" +}) + +function Login(props) { + let auth = useAuth(); + let navigate = useNavigate(); + let location = useLocation(); + + let from = location.state?.from?.pathname || "/"; + + useEffect(() => { + if (auth.token && !isExpired(auth.token)) { + navigate(from, { replace: true }); + } + }, [auth.token, from, navigate]); + + const handleSubmit = () => { + fetch("/api/v1/users/login", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + email: document.getElementById("email").value.trim(), + password: document.getElementById("password").value.trim(), + }), + }).then((res) => { + if (res.status === 200) { + res.json().then((data) => { + auth.signin(data.token, () => { + navigate(from, { replace: true }); + }); + }); + } else { + alert("Invalid email or password"); + } + }); + }; + + + function onKeyPress(e) { + if (e.key == "Enter") { + e.preventDefault(); + handleSubmit(); + } + } + + return ( + <Flex> + <LoginCard> + <Title>FDNS</Title> + <Subtitle>Log In</Subtitle> + <StyledLabel for="email">Email</StyledLabel> + <Input id="email" type="email"></Input> + <StyledLabel for="password">Password</StyledLabel> + <Input id="password" type="password" onKeyUp={onKeyPress}></Input> + <AlignRight> + { /* <Button secondary>Cancel</Button> */} + <Button onClick={handleSubmit} primary>Log In {'\u2794'}</Button> + </AlignRight> + </LoginCard> + </Flex> + ); +} +export default Login; |