Skip to content

Commit

Permalink
Merge pull request #337 from COS301-SE-2024/refactor/frontend/login-ux
Browse files Browse the repository at this point in the history
Refactor UX for some of the login related pages
  • Loading branch information
waveyboym authored Aug 23, 2024
2 parents af2ac3a + 9a04ddb commit 57b6f30
Show file tree
Hide file tree
Showing 8 changed files with 297 additions and 26 deletions.
22 changes: 22 additions & 0 deletions frontend/occupi-web/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ import {
Rooms,
AboutPage,
SecurityPage,
ForgotPassword,
ResetPassword,
} from "@pages/index";
import {
Appearance,
Expand Down Expand Up @@ -87,6 +89,26 @@ function App() {
)
}
/>
<Route
path="/forgot-password"
element={
isAuthenticated ? (
<Navigate to="/dashboard/overview" />
) : (
<ForgotPassword />
)
}
/>
<Route
path="/reset-password"
element={
isAuthenticated ? (
<Navigate to="/dashboard/overview" />
) : (
<ResetPassword />
)
}
/>
<Route
path="/*"
element={
Expand Down
48 changes: 42 additions & 6 deletions frontend/occupi-web/src/AuthService.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,4 @@
import axios from "axios";
import { wrapper } from "axios-cookiejar-support";
import { CookieJar } from "tough-cookie";

const jar = new CookieJar();
const client = wrapper(axios.create({ jar }));

const API_URL = "/auth"; // This will be proxied to https://dev.occupi.tech
const API_USER_URL = "/api"; // Adjust this if needed
Expand Down Expand Up @@ -188,7 +183,7 @@ const AuthService = {
logout: async () => {
try {
// Perform the logout request
const response = await client.post(`${API_URL}/logout`, {
const response = await axios.post(`${API_URL}/logout`, {
withCredentials: true,
});
console.log(response.data);
Expand Down Expand Up @@ -284,6 +279,47 @@ const AuthService = {
throw new Error("An unexpected error occurred during OTP verification");
}
},

sendResetEmail: async (email: string) => {
try {
const response = await axios.post(`${API_URL}/forgot-password`, {
"email": email
});
if (response.data.status === 200) {
return response.data;
} else {
throw new Error(response.data.message || "Failed to send reset email");
}
} catch (error) {
console.error("Error in sendResetEmail:", error);
if (axios.isAxiosError(error) && error.response) {
throw error.response.data;
}
throw new Error("An unexpected error occurred while sending reset email");
}
},

resetPassword: async (email: string, otp: string, newPassword: string, newPasswordConfirm: string) => {
try {
const response = await axios.post(`${API_URL}/reset-password-admin-login`, {
"email": email,
"otp": otp,
"newPassword": newPassword,
"newPasswordConfirm": newPasswordConfirm
});
if (response.data.status === 200) {
return response.data;
} else {
throw new Error(response.data.message || "Failed to send reset email");
}
} catch (error) {
console.error("Error in sendResetEmail:", error);
if (axios.isAxiosError(error) && error.response) {
throw error.response.data;
}
throw new Error("An unexpected error occurred while sending reset email");
}
},
};

function bufferEncode(value: ArrayBuffer): string {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,9 +43,9 @@ const OtpComponent = (props: OtpComponentProps) => {
};

return (
<div className="flex flex-col items-center">
<div className="w-full flex flex-col items-center">
{err !== "" && <h5 className="text-text_col_red_salmon font-normal text-base mt-3 mb-1">{err}</h5>}
<div className="flex space-x-2 md:space-x-4">
<div className="w-full flex justify-between">
{otp.map((data, index) => (
<input
key={index}
Expand Down
52 changes: 37 additions & 15 deletions frontend/occupi-web/src/pages/Login/LoginForm.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { useState } from "react";
import { useEffect, useState } from "react";
import { loginpng, OccupiLogo } from "@assets/index";
import { Checkbox, GradientButton, InputBox, OccupiLoader } from "@components/index";
import { GradientButton, InputBox, OccupiLoader } from "@components/index";
import { useNavigate } from "react-router-dom";
import AuthService from "AuthService";
import { useUser } from "userStore";
Expand All @@ -18,6 +18,7 @@ const LoginForm = (): JSX.Element => {
const [isLoading, setIsLoading] = useState<boolean>(false);
const [error, setError] = useState<string>("");
const [requiresOtp, setRequiresOtp] = useState<boolean>(false);
const [webauthn, setWebauthn] = useState<boolean>(false);

const handleLogin = async (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
Expand All @@ -30,7 +31,7 @@ const LoginForm = (): JSX.Element => {
return;
}

if(!window.PublicKeyCredential || form.password !== "" ) {
if(!webauthn) {
if (form.password === "") {
setError("Please fill in password field");
setIsLoading(false);
Expand Down Expand Up @@ -101,8 +102,24 @@ const LoginForm = (): JSX.Element => {
}
};

const canUseWebauthn = () => {
return window.PublicKeyCredential !== undefined;
}

const attemptWebauthn = async () => {
if (!canUseWebauthn()) {
setWebauthn(false);
} else {
setWebauthn(true);
}
}

const clickSubmit = () => { document.getElementById("LoginFormSubmitButton")?.click(); }

useEffect(() => {
attemptWebauthn();
}, []);

return (
<div className="flex flex-col lg:flex-row justify-center items-center min-h-screen p-4">
{isLoading && <OccupiLoader message={requiresOtp ? "Redirecting to OTP page..." : "Logging you in..."} />}
Expand All @@ -126,21 +143,25 @@ const LoginForm = (): JSX.Element => {
setForm({ ...form, email: val, valid_email: validity })
}}
/>
<InputBox
type="password"
placeholder="Enter your password"
label="Password"
submitValue={(val, validity) => {
setForm({ ...form, password: val, valid_password: validity })
}}
/>
{ !webauthn &&
<InputBox
type="password"
placeholder="Enter your password"
label="Password"
submitValue={(val, validity) => {
setForm({ ...form, password: val, valid_password: validity })
}}
/>
}

<div className="w-full flex flex-col sm:flex-row justify-between items-center mt-5 space-y-2 sm:space-y-0">
<div className="flex items-center">
<Checkbox id="rememberMeCheckbox" />
<p className="text-text_col_green_leaf cursor-pointer ml-2">Remember me</p>
{ !canUseWebauthn() ? <p className="text-text_col_green_leaf opacity-70">Use passwordless login</p> :
webauthn ? <p className="text-text_col_green_leaf cursor-pointer" onClick={() => setWebauthn(false)}>Use password instead</p>
: <p className="text-text_col_green_leaf cursor-pointer" onClick={attemptWebauthn}>Use password-less login</p>
}
</div>
<p className="text-text_col_green_leaf cursor-pointer">Forgot Password?</p>
<p className="text-text_col_green_leaf cursor-pointer" onClick={() => navigate("/forgot-password")}>Forgot Password?</p>
</div>

{error && <p className="text-red-500 mt-2">{error}</p>}
Expand All @@ -157,7 +178,8 @@ const LoginForm = (): JSX.Element => {

<div className="flex items-center justify-center mt-5 mb-5">
<p className="text-text_col">New to occupi?</p>
<p className="ml-2 text-text_col_green_leaf cursor-pointer">Learn more</p>
<a className="ml-2 text-text_col_green_leaf cursor-pointer" href="https://occupi.tech" target="ref">
Learn more</a>
</div>
</form>
</div>
Expand Down
79 changes: 79 additions & 0 deletions frontend/occupi-web/src/pages/forgot-password/ForgotPassword.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import { useState } from "react";
import { useNavigate } from "react-router-dom";
import { OccupiLogo, login_image } from "@assets/index";
import { GradientButton, OccupiLoader, InputBox } from "@components/index";
import AuthService from "AuthService";

const ForgotPassword = () => {
const navigate = useNavigate();

const [form, setForm] = useState<{
email: string,
valid_email: boolean
}>({ email: "", valid_email: false });
const [isLoading, setIsLoading] = useState<boolean>(false);
const [error, setError] = useState<string>("");

async function sendResetEmail() {
setIsLoading(true);
setError("");

try{
const response = await AuthService.sendResetEmail(form.email);

if (response.message.includes('check your email for an otp')) {
navigate("/reset-password", { state: { email: form.email } });
setIsLoading(false);
return;
}
setIsLoading(false);
setError("An unexpected error occurred");
} catch (error) {
console.error("Login error:", error);
if (typeof error === 'object' && error !== null && 'message' in error) {
setError(error.message as string);
} else {
setError("An unexpected error occurred");
}
setIsLoading(false);
return;
}
}

return (
<div className="flex flex-col md:flex-row justify-center w-screen h-screen items-center p-4">
{isLoading && <OccupiLoader message="Please wait..." />}
<div className="w-full md:w-[60vw] h-auto md:h-[40vw] flex justify-center items-center mb-8 md:mb-0">
<div className="w-full md:w-[70vw] h-auto md:h-[35vw]">
<img className="w-full h-full object-cover" src={login_image} alt="welcomes" />
</div>
</div>
<div className="w-full md:w-[30vw] md:ml-10 md:mr-3 flex flex-col items-center px-4">
<div className="w-[30vw] max-w-[150px] h-auto aspect-square mb-6">
<OccupiLogo />
</div>
<h2 className="text-text_col font-semibold text-3xl md:text-4xl lg:text-5xl text-center mb-4">Forgot your password?</h2>
<h3 className="text-text_col font-extralight text-xl md:text-2xl text-center mb-6">
Enter your email address below
</h3>

<InputBox
type="email"
placeholder="Enter your email address"
label="Email Address"
submitValue={(val, validity) => {
setForm({ ...form, email: val, valid_email: validity })
}}
/>

<div className="mt-5 w-full max-w-md">
<GradientButton isLoading={isLoading} Text="Send OTP" isClickable={form.valid_email} clickEvent={sendResetEmail} />
</div>

{error && <p className="text-red-500 mt-2 text-center">{error}</p>}
</div>
</div>
);
};

export default ForgotPassword;
4 changes: 4 additions & 0 deletions frontend/occupi-web/src/pages/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ import Rooms from "./rooms/Rooms";
import { NotificationsSettings } from "./notificationsSettings/NotificationsSettings";
import AboutPage from "./about/AboutPage";
import SecurityPage from "./securityPage/SecurityPage";
import ForgotPassword from './forgot-password/ForgotPassword';
import ResetPassword from './reset-password/ResetPassword';
export {
LoginForm,
OtpPage,
Expand All @@ -31,4 +33,6 @@ export {
NotificationsSettings,
AboutPage,
SecurityPage,
ForgotPassword,
ResetPassword
};
4 changes: 1 addition & 3 deletions frontend/occupi-web/src/pages/otp-page/OtpPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,7 @@ const OtpPage = () => {
if (state && state.email) {
setEmail(state.email);
} else {
setError("Email not provided. Please start the login process again.");
// Optionally, redirect to login page after a short delay
// setTimeout(() => navigate('/login'), 3000);
navigate("/")
}
}, [location.state, navigate]);

Expand Down
Loading

0 comments on commit 57b6f30

Please sign in to comment.