diff --git a/frontend/occupi-web/src/App.tsx b/frontend/occupi-web/src/App.tsx index d81f9b45..85933dd0 100644 --- a/frontend/occupi-web/src/App.tsx +++ b/frontend/occupi-web/src/App.tsx @@ -10,6 +10,8 @@ import { Rooms, AboutPage, SecurityPage, + ForgotPassword, + ResetPassword, } from "@pages/index"; import { Appearance, @@ -87,6 +89,26 @@ function App() { ) } /> + + ) : ( + + ) + } + /> + + ) : ( + + ) + } + /> { 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); @@ -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 { diff --git a/frontend/occupi-web/src/components/OtpComponent/OtpComponent.tsx b/frontend/occupi-web/src/components/OtpComponent/OtpComponent.tsx index 5ebd2696..88000905 100644 --- a/frontend/occupi-web/src/components/OtpComponent/OtpComponent.tsx +++ b/frontend/occupi-web/src/components/OtpComponent/OtpComponent.tsx @@ -43,9 +43,9 @@ const OtpComponent = (props: OtpComponentProps) => { }; return ( -
+
{err !== "" &&
{err}
} -
+
{otp.map((data, index) => ( { const [isLoading, setIsLoading] = useState(false); const [error, setError] = useState(""); const [requiresOtp, setRequiresOtp] = useState(false); + const [webauthn, setWebauthn] = useState(false); const handleLogin = async (e: React.FormEvent) => { e.preventDefault(); @@ -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); @@ -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 (
{isLoading && } @@ -126,21 +143,25 @@ const LoginForm = (): JSX.Element => { setForm({ ...form, email: val, valid_email: validity }) }} /> - { - setForm({ ...form, password: val, valid_password: validity }) - }} - /> + { !webauthn && + { + setForm({ ...form, password: val, valid_password: validity }) + }} + /> + }
- -

Remember me

+ { !canUseWebauthn() ?

Use passwordless login

: + webauthn ?

setWebauthn(false)}>Use password instead

+ :

Use password-less login

+ }
-

Forgot Password?

+

navigate("/forgot-password")}>Forgot Password?

{error &&

{error}

} @@ -157,7 +178,8 @@ const LoginForm = (): JSX.Element => {

New to occupi?

-

Learn more

+ + Learn more
diff --git a/frontend/occupi-web/src/pages/forgot-password/ForgotPassword.tsx b/frontend/occupi-web/src/pages/forgot-password/ForgotPassword.tsx new file mode 100644 index 00000000..e06cd5d5 --- /dev/null +++ b/frontend/occupi-web/src/pages/forgot-password/ForgotPassword.tsx @@ -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(false); + const [error, setError] = useState(""); + + 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 ( +
+ {isLoading && } +
+
+ welcomes +
+
+
+
+ +
+

Forgot your password?

+

+ Enter your email address below +

+ + { + setForm({ ...form, email: val, valid_email: validity }) + }} + /> + +
+ +
+ + {error &&

{error}

} +
+
+ ); +}; + +export default ForgotPassword; diff --git a/frontend/occupi-web/src/pages/index.ts b/frontend/occupi-web/src/pages/index.ts index f20bcc09..0179eb48 100644 --- a/frontend/occupi-web/src/pages/index.ts +++ b/frontend/occupi-web/src/pages/index.ts @@ -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, @@ -31,4 +33,6 @@ export { NotificationsSettings, AboutPage, SecurityPage, + ForgotPassword, + ResetPassword }; diff --git a/frontend/occupi-web/src/pages/otp-page/OtpPage.tsx b/frontend/occupi-web/src/pages/otp-page/OtpPage.tsx index 677cacbb..eaed3bbd 100644 --- a/frontend/occupi-web/src/pages/otp-page/OtpPage.tsx +++ b/frontend/occupi-web/src/pages/otp-page/OtpPage.tsx @@ -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]); diff --git a/frontend/occupi-web/src/pages/reset-password/ResetPassword.tsx b/frontend/occupi-web/src/pages/reset-password/ResetPassword.tsx new file mode 100644 index 00000000..0f679460 --- /dev/null +++ b/frontend/occupi-web/src/pages/reset-password/ResetPassword.tsx @@ -0,0 +1,110 @@ +import { useState, useEffect } from "react"; +import { useLocation, useNavigate } from "react-router-dom"; +import { OccupiLogo, login_image } from "@assets/index"; +import { GradientButton, OtpComponent, OccupiLoader, InputBox } from "@components/index"; +import AuthService from "AuthService"; +import { useUser } from "userStore"; + +const ResetPassword = () => { + const location = useLocation(); + const navigate = useNavigate(); + const { setUserDetails } = useUser(); + + const [form, setForm] = useState<{ + email: string, + otp: string, + password: string, + passwordConfirm: string, + valid_email: boolean, + valid_otp: boolean, + valid_password: boolean, + valid_passwordConfirm: boolean + }>({email: "",otp: "",password: "",passwordConfirm: "",valid_email: false,valid_otp: false,valid_password: false,valid_passwordConfirm: false}); + + const [isLoading, setIsLoading] = useState(false); + const [error, setError] = useState(""); + + useEffect(() => { + const state = location.state as { email?: string } | null; + if (state && state.email) { + setForm({ ...form, email: state.email }); + } else { + navigate("/forgot-password"); + } + }, [location.state, navigate]); + + async function verifyOTP() { + if (!form.email) { + setError("Email not available. Please start the login process again."); + return; + } + + setIsLoading(true); + setError(""); + try { + const response = await AuthService.resetPassword(form.email, form.otp.replace(/,/g, ""), form.password, form.passwordConfirm); + console.log("OTP verification response:", response); + + // Uncomment these lines if you want to fetch and set user details + const userDetails = await AuthService.getUserDetails(form.email); + setUserDetails(userDetails); + + navigate("/dashboard/overview"); + } catch (error) { + console.error("OTP verification error:", error); + setError("OTP verification failed. Please try again."); + } finally { + setIsLoading(false); + } + } + + return ( +
+ {isLoading && } +
+
+ welcomes +
+
+
+
+ +
+

We sent you an email with a code

+

+ {form.email ? `Please enter it to continue (${form.email})` : "Please enter it to continue"} +

+ + { setForm({ ...form, otp: otp_val.join(""), valid_otp: validity })}} /> + +
+ + { + setForm({ ...form, password: val, valid_password: validity }) + }} + /> + + { + setForm({ ...form, passwordConfirm: val, valid_passwordConfirm: validity }) + }} + /> + +
+ + + + {error &&

{error}

} +
+
+ ); +}; + +export default ResetPassword;