Recover email from sign in

This commit is contained in:
2024-07-29 17:59:04 +08:00
parent 412e30029f
commit 34a96a8445
7 changed files with 290 additions and 130 deletions

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 8.4 KiB

View File

@@ -21,60 +21,72 @@ import DefaultLayout from "./layouts/default";
import AdministratorLayout from "./layouts/administrator"; import AdministratorLayout from "./layouts/administrator";
import UsersManagement from "./pages/UsersManagement"; import UsersManagement from "./pages/UsersManagement";
import ResetPasswordPage from "./pages/ResetPasswordPage"; import ResetPasswordPage from "./pages/ResetPasswordPage";
import ForgotPasswordPage from "./pages/ForgotPasswordPage";
import RestrictedLayout from "./layouts/restricted";
function App() { function App() {
return ( return (
<Routes> <Routes>
{/* User Routes */} <Route>
<Route path="/"> {/* User Routes */}
<Route element={<DefaultLayout />}> <Route path="/">
{/* General Routes */} <Route element={<DefaultLayout />}>
<Route index element={<HomePage />} /> {/* General Routes */}
<Route element={<SignUpPage />} path="signup" /> <Route index element={<HomePage />} />
<Route element={<SignInPage />} path="signin" /> <Route element={<SignUpPage />} path="signup" />
<Route element={<SpringboardPage />} path="springboard" /> <Route element={<SignInPage />} path="signin" />
<Route element={<ManageUserAccountPage />} path="manage-account" /> <Route element={<SpringboardPage />} path="springboard" />
<Route element={<ManageUserAccountPage />} path="manage-account" />
{/* Events Route */} {/* Events Route */}
<Route path="events">
<Route index element={<EventsPage />} />
</Route>
{/* Karang Guni Schedules Route */}
<Route path="karang-guni-schedules">
<Route index element={<SchedulePage />} />
</Route>
{/* Home Bill Contest Route */}
<Route path="home-bill-contest">
<Route index element={<HBContestPage />} />
<Route element={<HBFormPage />} path="new-submission" />
</Route>
{/* Community Posts Route */}
<Route path="community-posts">
<Route index element={<CommunityPage />} />
<Route element={<CreatePostPage />} path="create" />
<Route element={<PostPage />} path="post/:id" />
<Route element={<EditPostPage />} path="edit/:id" />
</Route>
</Route>
{/* Special (Restricted) Routes */}
<Route element={<RestrictedLayout />}>
<Route element={<ForgotPasswordPage />} path="forgot-password" />
<Route
element={<ResetPasswordPage />}
path="reset-password/:token"
/>
</Route>
</Route>
{/* Admin Routes */}
<Route path="/admin" element={<AdministratorLayout />}>
<Route index element={<AdministratorSpringboard />} />
<Route path="manage-account" element={<ManageUserAccountPage />} />
<Route path="users-management">
<Route index element={<UsersManagement />} />
</Route>
{/* Events */}
<Route path="events"> <Route path="events">
<Route index element={<EventsPage />} /> <Route index element={<ManageEventsPage />} />
<Route element={<CreateEventsPage />} path="createEvent" />
<Route element={<EditEventsPage />} path="editEvent/:id" />
</Route> </Route>
{/* Karang Guni Schedules Route */}
<Route path="karang-guni-schedules">
<Route index element={<SchedulePage />} />
</Route>
{/* Home Bill Contest Route */}
<Route path="home-bill-contest">
<Route index element={<HBContestPage />} />
<Route element={<HBFormPage />} path="new-submission" />
</Route>
{/* Community Posts Route */}
<Route path="community-posts">
<Route index element={<CommunityPage />} />
<Route element={<CreatePostPage />} path="create" />
<Route element={<PostPage />} path="post/:id" />
<Route element={<EditPostPage />} path="edit/:id" />
</Route>
</Route>
<Route element={<ResetPasswordPage />} path="reset-password/:token" />
</Route>
{/* Admin Routes */}
<Route path="/admin" element={<AdministratorLayout />}>
<Route index element={<AdministratorSpringboard />} />
<Route path="manage-account" element={<ManageUserAccountPage />} />
<Route path="users-management">
<Route index element={<UsersManagement />} />
</Route>
{/* Events */}
<Route path="events">
<Route index element={<ManageEventsPage />} />
<Route element={<CreateEventsPage />} path="createEvent" />
<Route element={<EditEventsPage />} path="editEvent/:id" />
</Route> </Route>
</Route> </Route>
</Routes> </Routes>

View File

@@ -1,7 +1,6 @@
import { Button, Link } from "@nextui-org/react"; import { Button, Link } from "@nextui-org/react";
import { Formik, Form } from "formik"; import { Formik, Form } from "formik";
import * as Yup from "yup"; import * as Yup from "yup";
import axios from "axios";
import config from "../config"; import config from "../config";
import NextUIFormikInput from "./NextUIFormikInput"; import NextUIFormikInput from "./NextUIFormikInput";
import { useNavigate } from "react-router-dom"; import { useNavigate } from "react-router-dom";
@@ -74,13 +73,24 @@ export default function SignInModule() {
placeholder="johndoe@email.com" placeholder="johndoe@email.com"
labelPlacement="outside" labelPlacement="outside"
/> />
<NextUIFormikInput <div className="flex flex-col gap-2 w-full">
label="Password" <NextUIFormikInput
name="password" label="Password"
type="password" name="password"
placeholder=" " type="password"
labelPlacement="outside" placeholder=" "
/> labelPlacement="outside"
/>
<Link
className="hover:cursor-pointer w-max"
size="sm"
onPress={() => {
navigate("/forgot-password");
}}
>
Forgot password?
</Link>
</div>
<Button <Button
type="submit" type="submit"
color="primary" color="primary"

View File

@@ -0,0 +1,21 @@
import { Toaster } from "react-hot-toast";
import SingaporeAgencyStrip from "../components/SingaporeAgencyStrip";
import { Outlet } from "react-router-dom";
export default function GlobalLayout() {
return (
<div className="relative flex flex-col border-4 border-red-500">
<SingaporeAgencyStrip />
<main className="h-min">
<Outlet />
</main>
{/*
A div that becomes black in dark mode to cover white color parts
of the website when scrolling past the window's original view.
*/}
<div className="fixed -z-50 dark:bg-black inset-0 w-full h-full"></div>
<Toaster />
</div>
);
}

View File

@@ -0,0 +1,34 @@
import { Outlet, useNavigate } from "react-router-dom";
import EcoconnectFullLogo from "../components/EcoconnectFullLogo";
import { Button, Card } from "@nextui-org/react";
export default function RestrictedLayout() {
const navigate = useNavigate();
return (
<div className="absolute inset-0 p-8 w-full h-full flex flex-col justify-center">
<div className="flex flex-row justify-center">
<div className="flex flex-col gap-8 *:mx-auto">
<Card className="max-w-[800px] w-full mx-auto p-8">
<Outlet />
</Card>
<div className="flex flex-row gap-2 *:my-auto">
<Button
variant="light"
onPress={() => {
navigate("/");
}}
>
<EcoconnectFullLogo />
</Button>
<div className="flex flex-row gap-6">
<p>·</p>
<p className="opacity-50">
© Copyright {new Date().getFullYear()}. All rights reserved.
</p>
</div>
</div>
</div>
</div>
</div>
);
}

View File

@@ -0,0 +1,109 @@
import { Button } from "@nextui-org/react";
import { Formik, Form } from "formik";
import * as Yup from "yup";
import axios from "axios";
import config from "../config";
import NextUIFormikInput from "../components/NextUIFormikInput";
import { popErrorToast, popToast } from "../utilities";
import { ArrowUTurnLeftIcon } from "../icons";
import { useNavigate } from "react-router-dom";
import instance from "../security/http";
const validationSchema = Yup.object({
email: Yup.string()
.trim()
.lowercase()
.min(5)
.max(69)
.email("Invalid email format")
.required("Email is required"),
});
export default function ForgotPasswordPage() {
const navigate = useNavigate();
const initialValues = {
email: "",
};
const handleSubmit = (values: any): void => {
instance
.put(
`${
config.serverAddress
}/users/request-reset-password/${encodeURIComponent(values.email)}`
)
.then(() => {
console.log("Email sent successfully");
popToast("Email sent to your mailbox!", 1);
setTimeout(() => {
navigate("/signin");
}, 1500);
})
.catch((error) => {
console.error("Failed to send email:", error);
popErrorToast("Failed to send email: " + error);
});
};
return (
<div className="w-full flex flex-row gap-10">
<div className="flex flex-col justify-center relative">
<Button
variant="light"
isIconOnly
radius="full"
className="-mt-2 -ml-2 mr-auto absolute top-0 left-0"
onPress={() => {
navigate(-1);
}}
>
<ArrowUTurnLeftIcon />
</Button>
<div className="relative">
<img
src="../assets/google-passkey.svg"
alt="Google Passkey SVG"
className="saturate-0"
/>
<div className="absolute inset-0 bg-primary-500 mix-blend-overlay w-full h-full"></div>
</div>
</div>
<div className="flex flex-col gap-8">
<div className="flex flex-col gap-4">
<div className="flex flex-col gap-2">
<p className="text-3xl font-bold">Password Recovery</p>
<p>
Enter your email address below, and we will send you a recovery
mail.
</p>
</div>
</div>
<Formik
initialValues={initialValues}
validationSchema={validationSchema}
onSubmit={handleSubmit}
>
{({ isValid, dirty }) => (
<Form className="flex flex-col gap-8">
<NextUIFormikInput
label="Email"
name="email"
type="email"
placeholder="johndoe@email.com"
labelPlacement="outside"
/>
<Button
type="submit"
color="primary"
size="lg"
isDisabled={!isValid || !dirty}
>
Send Recovery Email
</Button>
</Form>
)}
</Formik>
</div>
</div>
);
}

View File

@@ -2,8 +2,7 @@ import { useParams, useNavigate } from "react-router-dom";
import instance from "../security/http"; import instance from "../security/http";
import config from "../config"; import config from "../config";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { Button, Card, CircularProgress } from "@nextui-org/react"; import { Button } from "@nextui-org/react";
import EcoconnectFullLogo from "../components/EcoconnectFullLogo";
import NextUIFormikInput from "../components/NextUIFormikInput"; import NextUIFormikInput from "../components/NextUIFormikInput";
import { Formik, Form } from "formik"; import { Formik, Form } from "formik";
import * as Yup from "yup"; import * as Yup from "yup";
@@ -29,7 +28,6 @@ export default function ResetPasswordPage() {
const navigate = useNavigate(); const navigate = useNavigate();
const [validationResult, setValidationResult] = useState<boolean>(false); const [validationResult, setValidationResult] = useState<boolean>(false);
const [pageLoading, setPageLoading] = useState<boolean>(true);
const validateToken = () => { const validateToken = () => {
instance instance
@@ -39,9 +37,6 @@ export default function ResetPasswordPage() {
}) })
.catch(() => { .catch(() => {
setValidationResult(false); setValidationResult(false);
})
.finally(() => {
setPageLoading(false);
}); });
}; };
@@ -66,77 +61,55 @@ export default function ResetPasswordPage() {
}; };
return ( return (
<div className="absolute inset-0 p-8 w-full h-full flex flex-col justify-center"> <div>
<div className="flex flex-row justify-center"> {validationResult && (
{pageLoading && ( <div className="flex flex-col gap-8 p-12 text-left">
<div> <div className="flex flex-col gap-2">
<CircularProgress label="Loading..." /> <p className="text-3xl font-bold">Password Reset</p>
<p>Enter a new password below.</p>
</div> </div>
)} <Formik
{!pageLoading && ( initialValues={{ password: "", confirmPassword: "" }}
<div className="flex flex-col gap-8 *:mx-auto"> validationSchema={validationSchema}
<Card className="max-w-[600px] w-full mx-auto"> onSubmit={handleSubmit}
{validationResult && ( >
<div className="flex flex-col gap-8 p-12 text-left"> {({ isValid, dirty }) => (
<div className="flex flex-col gap-2"> <Form className="flex flex-col gap-8">
<p className="text-3xl font-bold">Password Reset</p> <NextUIFormikInput
<p>Enter a new password below.</p> label="New Password"
</div> name="password"
<Formik type="password"
initialValues={{ password: "", confirmPassword: "" }} placeholder="Enter new password"
validationSchema={validationSchema} labelPlacement="outside"
onSubmit={handleSubmit} />
> <NextUIFormikInput
{({ isValid, dirty }) => ( label="Confirm Password"
<Form className="flex flex-col gap-8"> name="confirmPassword"
<NextUIFormikInput type="password"
label="New Password" placeholder="Confirm new password"
name="password" labelPlacement="outside"
type="password" />
placeholder="Enter new password" <Button
labelPlacement="outside" type="submit"
/> color="primary"
<NextUIFormikInput size="lg"
label="Confirm Password" isDisabled={!isValid || !dirty}
name="confirmPassword" >
type="password" Confirm
placeholder="Confirm new password" </Button>
labelPlacement="outside" </Form>
/> )}
<Button </Formik>
type="submit" </div>
color="primary" )}
size="lg" {!validationResult && (
isDisabled={!isValid || !dirty} <div className="flex flex-col gap-8 p-12 *:mr-auto text-left">
> <div className="flex flex-col gap-2">
Confirm <p className="text-3xl font-bold">Reset portal has been closed.</p>
</Button> <p>Please request for a password reset again.</p>
</Form>
)}
</Formik>
</div>
)}
{!validationResult && (
<div className="flex flex-col gap-8 p-12 *:mr-auto text-left">
<div className="flex flex-col gap-2">
<p className="text-3xl font-bold">
Reset portal has been closed.
</p>
<p>Please request for a password reset again.</p>
</div>
</div>
)}
</Card>
<div className="flex flex-row gap-4">
<EcoconnectFullLogo />
<p>·</p>
<p className="opacity-50">
© Copyright {new Date().getFullYear()}. All rights reserved.
</p>
</div>
</div> </div>
)} </div>
</div> )}
</div> </div>
); );
} }