Improved login and auth flow

This commit is contained in:
Keannu Bernasol 2023-11-19 18:35:04 +08:00
parent 99cd673b12
commit d878cfb1aa
11 changed files with 249 additions and 99 deletions

View file

@ -43,11 +43,11 @@ export function RegisterAPI(register: RegisterType) {
.post("api/v1/accounts/users/", register) .post("api/v1/accounts/users/", register)
.then(async (response) => { .then(async (response) => {
console.log(response.data); console.log(response.data);
return true; return [true, 0];
}) })
.catch(() => { .catch((error) => {
console.log("Registration failed"); console.log("Registration failed");
return false; return [false, error.response];
}); });
} }
@ -85,14 +85,10 @@ export async function JWTRefreshAPI() {
}); });
} }
export function UserAPI() { export async function UserAPI() {
const token = JSON.parse(localStorage.getItem("token") || "{}"); const config = await GetConfig();
return instance return instance
.get("api/v1/accounts/users/me/", { .get("api/v1/accounts/users/me/", config)
headers: {
Authorization: "Token " + token,
},
})
.then((response) => { .then((response) => {
return response.data; return response.data;
}) })

View file

@ -23,7 +23,7 @@ export default function Button(props: props) {
onMouseLeave={() => setClicked(false)} onMouseLeave={() => setClicked(false)}
style={{ style={{
borderRadius: 24, borderRadius: 24,
minWidth: "50%", minWidth: "128px",
maxWidth: "128px", maxWidth: "128px",
borderColor: colors.button_border, borderColor: colors.button_border,
borderStyle: "solid", borderStyle: "solid",

View file

@ -0,0 +1,55 @@
import React, { useState } from "react";
import styles from "../../styles";
import { colors } from "../../styles";
export interface props {
onClick: React.MouseEventHandler<HTMLButtonElement>;
children?: React.ReactNode;
icon?: React.ReactNode;
label: string;
}
export default function DrawerButton(props: props) {
const [clicked, setClicked] = useState(false);
return (
<div>
<button
onClick={props.onClick}
onMouseDown={() => {
if (!clicked) {
setClicked(!clicked);
}
}}
onMouseUp={() => setClicked(false)}
onMouseLeave={() => setClicked(false)}
style={{
borderRadius: 24,
minWidth: "128px",
maxWidth: "128px",
borderColor: colors.button_border,
borderStyle: "solid",
borderWidth: "2px",
paddingBottom: 0,
paddingTop: 0,
paddingRight: "4px",
paddingLeft: "4px",
marginBottom: "4px",
marginTop: "4px",
backgroundColor: clicked ? colors.button_light : colors.button_dark,
}}
>
<div style={styles.flex_row}>
{clicked ? <></> : props.icon}
<p
style={{
...(clicked ? styles.text_dark : styles.text_light),
...styles.text_M,
...{ marginLeft: "4px" },
}}
>
{props.label}
</p>
</div>
</button>
</div>
);
}

View file

@ -1,7 +1,7 @@
import { useState } from "react"; import { useState } from "react";
import styles, { colors } from "../../styles"; import styles, { colors } from "../../styles";
import MenuIcon from "@mui/icons-material/Menu"; import MenuIcon from "@mui/icons-material/Menu";
import SidebarModal from "../SidebarModal/SidebarModal"; import SidebarModal from "../Sidebar/Sidebar";
import { Drawer } from "@mui/material"; import { Drawer } from "@mui/material";
export interface props { export interface props {
label: string; label: string;

View file

@ -8,11 +8,12 @@ import Visibility from "@mui/icons-material/Visibility";
import VisibilityOff from "@mui/icons-material/VisibilityOff"; import VisibilityOff from "@mui/icons-material/VisibilityOff";
import LoginIcon from "@mui/icons-material/Login"; import LoginIcon from "@mui/icons-material/Login";
import Checkbox from "@mui/material/Checkbox"; import Checkbox from "@mui/material/Checkbox";
import Button from "../Buttons/Button"; import Button from "../Button/Button";
import { useNavigate } from "react-router-dom"; import { useNavigate } from "react-router-dom";
import { LoginAPI } from "../API/API"; import { LoginAPI } from "../API/API";
import { useDispatch } from "react-redux"; import { useDispatch } from "react-redux";
import { auth_toggle } from "../Plugins/Redux/Slices/AuthSlice/AuthSlice"; import { auth_toggle } from "../Plugins/Redux/Slices/AuthSlice/AuthSlice";
import { toast } from "react-toastify";
export default function LoginModal() { export default function LoginModal() {
const navigate = useNavigate(); const navigate = useNavigate();
const [showPassword, setShowPassword] = useState(false); const [showPassword, setShowPassword] = useState(false);
@ -129,8 +130,17 @@ export default function LoginModal() {
const status = await LoginAPI(user, remember_session); const status = await LoginAPI(user, remember_session);
if (status === true) { if (status === true) {
await dispatch(auth_toggle()); await dispatch(auth_toggle());
navigate("/dashboard"); navigate("/dashboard");
toast("Logged in", {
position: "top-right",
autoClose: 2000,
hideProgressBar: false,
closeOnClick: true,
pauseOnHover: true,
draggable: true,
progress: undefined,
theme: "light",
});
} else { } else {
setError("Invalid login"); setError("Invalid login");
} }

View file

@ -7,11 +7,13 @@ import IconButton from "@mui/material/IconButton";
import Visibility from "@mui/icons-material/Visibility"; import Visibility from "@mui/icons-material/Visibility";
import VisibilityOff from "@mui/icons-material/VisibilityOff"; import VisibilityOff from "@mui/icons-material/VisibilityOff";
import { AppRegistration } from "@mui/icons-material"; import { AppRegistration } from "@mui/icons-material";
import Button from "../Buttons/Button"; import Button from "../Button/Button";
import { useNavigate } from "react-router-dom"; import { useNavigate } from "react-router-dom";
import { RegisterAPI } from "../API/API";
export default function RegisterModal() { export default function RegisterModal() {
const navigate = useNavigate(); const navigate = useNavigate();
const [showPassword, setShowPassword] = useState(false); const [showPassword, setShowPassword] = useState(false);
const [user, setUser] = useState({ const [user, setUser] = useState({
first_name: "", first_name: "",
last_name: "", last_name: "",
@ -20,6 +22,7 @@ export default function RegisterModal() {
password: "", password: "",
confirm_password: "", confirm_password: "",
}); });
const [error, setError] = useState("");
return ( return (
<> <>
<div <div
@ -43,9 +46,10 @@ export default function RegisterModal() {
id="outlined-helperText" id="outlined-helperText"
label="First Name" label="First Name"
style={styles.input_form} style={styles.input_form}
onChange={(e: React.ChangeEvent<HTMLInputElement>) => onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
setUser({ ...user, first_name: e.target.value }) setUser({ ...user, first_name: e.target.value });
} setError("");
}}
value={user.first_name} value={user.first_name}
placeholder={"Enter your first name"} placeholder={"Enter your first name"}
/> />
@ -63,9 +67,10 @@ export default function RegisterModal() {
id="outlined-helperText" id="outlined-helperText"
label="Username" label="Username"
style={styles.input_form} style={styles.input_form}
onChange={(e: React.ChangeEvent<HTMLInputElement>) => onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
setUser({ ...user, username: e.target.value }) setUser({ ...user, username: e.target.value });
} setError("");
}}
value={user.username} value={user.username}
placeholder={"Enter username"} placeholder={"Enter username"}
/> />
@ -112,12 +117,14 @@ export default function RegisterModal() {
}} }}
label="Confirm Password" label="Confirm Password"
placeholder={"Re-enter password"} placeholder={"Re-enter password"}
onChange={(e: React.ChangeEvent<HTMLInputElement>) => onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
setUser({ ...user, confirm_password: e.target.value }) setUser({ ...user, confirm_password: e.target.value });
} setError("");
}}
value={user.confirm_password} value={user.confirm_password}
/> />
</div> </div>
<p style={{ ...styles.text_dark, ...styles.text_M }}>{error}</p>
<div <div
style={{ style={{
backgroundColor: colors.button_border, backgroundColor: colors.button_border,
@ -130,8 +137,17 @@ export default function RegisterModal() {
<Button <Button
type={"dark"} type={"dark"}
label={"Register"} label={"Register"}
onClick={() => { onClick={async () => {
navigate(0); if (user.password !== user.confirm_password) {
setError("Passwords do not match");
} else {
const status = await RegisterAPI(user);
if (status[0]) {
navigate("/");
} else {
setError(status[1]);
}
}
}} }}
/> />
</> </>

View file

@ -1,22 +1,35 @@
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { useDispatch, useSelector } from "react-redux"; import { useDispatch, useSelector } from "react-redux";
import { useNavigate } from "react-router-dom"; import { useNavigate, useLocation } from "react-router-dom";
import { JWTRefreshAPI, setAccessToken, setRefreshToken } from "../API/API"; import { JWTRefreshAPI, setAccessToken, setRefreshToken } from "../API/API";
import { auth_toggle } from "../Plugins/Redux/Slices/AuthSlice/AuthSlice"; import { auth_toggle } from "../Plugins/Redux/Slices/AuthSlice/AuthSlice";
import { RootState } from "../Plugins/Redux/Store/Store"; import { RootState } from "../Plugins/Redux/Store/Store";
import { toast } from "react-toastify";
export default function Revalidator() { export default function Revalidator() {
const dispatch = useDispatch(); const dispatch = useDispatch();
const navigate = useNavigate(); const navigate = useNavigate();
const location = useLocation();
const authenticated = useSelector((state: RootState) => state.auth.value); const authenticated = useSelector((state: RootState) => state.auth.value);
const [rechecked, setRechecked] = useState(false); const [rechecked, setRechecked] = useState(false);
useEffect(() => { useEffect(() => {
if (!authenticated && rechecked) { if (!authenticated && rechecked) {
if (location.pathname !== "/") {
navigate("/"); navigate("/");
console.log("Not logged in"); toast("Please log in to continue", {
position: "bottom-center",
autoClose: 2000,
hideProgressBar: false,
closeOnClick: true,
pauseOnHover: true,
draggable: true,
progress: undefined,
theme: "light",
});
} }
}, [authenticated, navigate, rechecked]); }
}, [authenticated, location.pathname, navigate, rechecked]);
useEffect(() => { useEffect(() => {
if (!authenticated) { if (!authenticated) {
@ -24,16 +37,25 @@ export default function Revalidator() {
if (response) { if (response) {
await dispatch(auth_toggle()); await dispatch(auth_toggle());
navigate("/dashboard"); navigate("/dashboard");
console.log("User session restored"); toast("User session restored", {
position: "top-right",
autoClose: 2000,
hideProgressBar: false,
closeOnClick: true,
pauseOnHover: true,
draggable: true,
progress: undefined,
theme: "light",
});
} else { } else {
await setRefreshToken(""); await setRefreshToken("");
await setAccessToken(""); await setAccessToken("");
console.log("User session expired");
} }
setRechecked(true); setRechecked(true);
}); });
} }
}, [authenticated, dispatch, navigate]); // eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
return <></>; return <></>;
} }

View file

@ -0,0 +1,114 @@
import styles, { colors } from "../../styles";
import AccountCircleIcon from "@mui/icons-material/AccountCircle";
import HomeIcon from "@mui/icons-material/Home";
import LogoutIcon from "@mui/icons-material/Logout";
import { useQuery } from "@tanstack/react-query";
import { UserAPI, setAccessToken, setRefreshToken } from "../API/API";
import DrawerButton from "../DrawerButton/DrawerButton";
import { useDispatch } from "react-redux";
import { auth_toggle } from "../Plugins/Redux/Slices/AuthSlice/AuthSlice";
import { toast } from "react-toastify";
import { useNavigate } from "react-router-dom";
export default function Sidebar() {
const user = useQuery({ queryKey: ["user"], queryFn: UserAPI });
const dispatch = useDispatch();
const navigate = useNavigate();
return (
<div
style={{
width: "256px",
height: "100%",
padding: 16,
alignContent: "center",
justifyContent: "center",
backgroundColor: colors.header_color,
}}
>
<div style={styles.flex_row}>
<AccountCircleIcon
style={{
width: "48px",
height: "48px",
color: "white",
marginRight: "4px",
}}
/>
<p
style={{
...styles.text_light,
...styles.text_S,
...{ alignSelf: "center" },
}}
>
{user.data
? user.data.username
: user.isError
? "Error loading user"
: "Loading user..."}
</p>
</div>
<div
style={{
backgroundColor: "white",
marginTop: "16px",
width: "100%",
height: "2px",
marginBottom: 8,
}}
/>
<div style={styles.flex_row}>
<HomeIcon
style={{
width: "48px",
height: "48px",
color: "white",
marginRight: "2px",
alignSelf: "center",
justifySelf: "center",
}}
/>
<p
style={{
...styles.text_light,
...styles.text_M,
}}
>
Dashboard
</p>
</div>
<div style={styles.flex_row}>
<DrawerButton
onClick={async () => {
navigate("/");
await dispatch(auth_toggle());
await setAccessToken("");
await setRefreshToken("");
toast("Logged out", {
position: "top-right",
autoClose: 2000,
hideProgressBar: false,
closeOnClick: true,
pauseOnHover: true,
draggable: true,
progress: undefined,
theme: "light",
});
}}
icon={
<LogoutIcon
style={{
width: "48px",
height: "48px",
color: "white",
marginRight: "2px",
alignSelf: "center",
justifySelf: "center",
}}
/>
}
label={"Log out"}
/>
</div>
</div>
);
}

View file

@ -1,65 +0,0 @@
import styles, { colors } from "../../styles";
import AccountCircleIcon from "@mui/icons-material/AccountCircle";
import HomeIcon from "@mui/icons-material/Home";
export default function SidebarModal() {
return (
<div
style={{
width: "256px",
height: "100%",
padding: 16,
alignContent: "center",
justifyContent: "center",
textAlign: "center",
backgroundColor: colors.header_color,
}}
>
<div style={styles.flex_row}>
<AccountCircleIcon
style={{
width: "48px",
height: "px",
color: "white",
marginRight: "4px",
}}
/>
<p
style={{
...styles.text_light,
...styles.text_S,
...{ alignSelf: "center" },
}}
>
Placeholder Name
</p>
</div>
<div
style={{
backgroundColor: "white",
marginTop: "16px",
width: "100%",
height: "2px",
marginBottom: 8,
}}
/>
<div style={styles.flex_row}>
<HomeIcon
style={{
width: "64px",
height: "64px",
color: "white",
marginRight: "2px",
}}
/>
<p
style={{
...styles.text_light,
...styles.text_M,
}}
>
Dashboard
</p>
</div>
</div>
);
}

View file

@ -2,6 +2,8 @@ export type RegisterType = {
email: string; email: string;
username: string; username: string;
password: string; password: string;
first_name: string;
last_name: string;
}; };
export type LoginType = { export type LoginType = {

View file

@ -1,4 +1,4 @@
import Button from "../../Components/Buttons/Button"; import Button from "../../Components/Button/Button";
import styles from "../../styles"; import styles from "../../styles";
import citc_logo from "../../assets/citc_logo.jpg"; import citc_logo from "../../assets/citc_logo.jpg";
import Popup from "reactjs-popup"; import Popup from "reactjs-popup";