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

View file

@ -23,7 +23,7 @@ export default function Button(props: props) {
onMouseLeave={() => setClicked(false)}
style={{
borderRadius: 24,
minWidth: "50%",
minWidth: "128px",
maxWidth: "128px",
borderColor: colors.button_border,
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 styles, { colors } from "../../styles";
import MenuIcon from "@mui/icons-material/Menu";
import SidebarModal from "../SidebarModal/SidebarModal";
import SidebarModal from "../Sidebar/Sidebar";
import { Drawer } from "@mui/material";
export interface props {
label: string;

View file

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

View file

@ -7,11 +7,13 @@ import IconButton from "@mui/material/IconButton";
import Visibility from "@mui/icons-material/Visibility";
import VisibilityOff from "@mui/icons-material/VisibilityOff";
import { AppRegistration } from "@mui/icons-material";
import Button from "../Buttons/Button";
import Button from "../Button/Button";
import { useNavigate } from "react-router-dom";
import { RegisterAPI } from "../API/API";
export default function RegisterModal() {
const navigate = useNavigate();
const [showPassword, setShowPassword] = useState(false);
const [user, setUser] = useState({
first_name: "",
last_name: "",
@ -20,6 +22,7 @@ export default function RegisterModal() {
password: "",
confirm_password: "",
});
const [error, setError] = useState("");
return (
<>
<div
@ -43,9 +46,10 @@ export default function RegisterModal() {
id="outlined-helperText"
label="First Name"
style={styles.input_form}
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
setUser({ ...user, first_name: e.target.value })
}
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
setUser({ ...user, first_name: e.target.value });
setError("");
}}
value={user.first_name}
placeholder={"Enter your first name"}
/>
@ -63,9 +67,10 @@ export default function RegisterModal() {
id="outlined-helperText"
label="Username"
style={styles.input_form}
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
setUser({ ...user, username: e.target.value })
}
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
setUser({ ...user, username: e.target.value });
setError("");
}}
value={user.username}
placeholder={"Enter username"}
/>
@ -112,12 +117,14 @@ export default function RegisterModal() {
}}
label="Confirm Password"
placeholder={"Re-enter password"}
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
setUser({ ...user, confirm_password: e.target.value })
}
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
setUser({ ...user, confirm_password: e.target.value });
setError("");
}}
value={user.confirm_password}
/>
</div>
<p style={{ ...styles.text_dark, ...styles.text_M }}>{error}</p>
<div
style={{
backgroundColor: colors.button_border,
@ -130,8 +137,17 @@ export default function RegisterModal() {
<Button
type={"dark"}
label={"Register"}
onClick={() => {
navigate(0);
onClick={async () => {
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 { 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 { auth_toggle } from "../Plugins/Redux/Slices/AuthSlice/AuthSlice";
import { RootState } from "../Plugins/Redux/Store/Store";
import { toast } from "react-toastify";
export default function Revalidator() {
const dispatch = useDispatch();
const navigate = useNavigate();
const location = useLocation();
const authenticated = useSelector((state: RootState) => state.auth.value);
const [rechecked, setRechecked] = useState(false);
useEffect(() => {
if (!authenticated && rechecked) {
navigate("/");
console.log("Not logged in");
if (location.pathname !== "/") {
navigate("/");
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(() => {
if (!authenticated) {
@ -24,16 +37,25 @@ export default function Revalidator() {
if (response) {
await dispatch(auth_toggle());
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 {
await setRefreshToken("");
await setAccessToken("");
console.log("User session expired");
}
setRechecked(true);
});
}
}, [authenticated, dispatch, navigate]);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
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;
username: string;
password: string;
first_name: string;
last_name: string;
};
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 citc_logo from "../../assets/citc_logo.jpg";
import Popup from "reactjs-popup";