aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGalen Guyer <galen@galenguyer.com>2022-05-30 15:07:04 -0400
committerGalen Guyer <galen@galenguyer.com>2022-05-30 15:07:04 -0400
commit0f8bb91256c3dd8319a5d50e16d29e9a031e5af1 (patch)
treef9bd7c25b3b490e6741b4c09cd1a16c92b28792e
parentb718d6707e708c2ed346e42ec65995810ef739e8 (diff)
add auth and login page
-rw-r--r--package.json2
-rw-r--r--pnpm-lock.yaml13
-rw-r--r--src/hooks/useAuth.js44
-rw-r--r--src/hooks/useFetch.js17
-rw-r--r--src/index.js18
-rw-r--r--src/routes/Login.js121
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;