Recover email from sign in
This commit is contained in:
1
client/assets/google-passkey.svg
Normal file
1
client/assets/google-passkey.svg
Normal file
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 8.4 KiB |
@@ -21,10 +21,13 @@ 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>
|
||||||
|
<Route>
|
||||||
{/* User Routes */}
|
{/* User Routes */}
|
||||||
<Route path="/">
|
<Route path="/">
|
||||||
<Route element={<DefaultLayout />}>
|
<Route element={<DefaultLayout />}>
|
||||||
@@ -59,7 +62,15 @@ function App() {
|
|||||||
<Route element={<EditPostPage />} path="edit/:id" />
|
<Route element={<EditPostPage />} path="edit/:id" />
|
||||||
</Route>
|
</Route>
|
||||||
</Route>
|
</Route>
|
||||||
<Route element={<ResetPasswordPage />} path="reset-password/:token" />
|
|
||||||
|
{/* Special (Restricted) Routes */}
|
||||||
|
<Route element={<RestrictedLayout />}>
|
||||||
|
<Route element={<ForgotPasswordPage />} path="forgot-password" />
|
||||||
|
<Route
|
||||||
|
element={<ResetPasswordPage />}
|
||||||
|
path="reset-password/:token"
|
||||||
|
/>
|
||||||
|
</Route>
|
||||||
</Route>
|
</Route>
|
||||||
|
|
||||||
{/* Admin Routes */}
|
{/* Admin Routes */}
|
||||||
@@ -77,6 +88,7 @@ function App() {
|
|||||||
<Route element={<EditEventsPage />} path="editEvent/:id" />
|
<Route element={<EditEventsPage />} path="editEvent/:id" />
|
||||||
</Route>
|
</Route>
|
||||||
</Route>
|
</Route>
|
||||||
|
</Route>
|
||||||
</Routes>
|
</Routes>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,6 +73,7 @@ export default function SignInModule() {
|
|||||||
placeholder="johndoe@email.com"
|
placeholder="johndoe@email.com"
|
||||||
labelPlacement="outside"
|
labelPlacement="outside"
|
||||||
/>
|
/>
|
||||||
|
<div className="flex flex-col gap-2 w-full">
|
||||||
<NextUIFormikInput
|
<NextUIFormikInput
|
||||||
label="Password"
|
label="Password"
|
||||||
name="password"
|
name="password"
|
||||||
@@ -81,6 +81,16 @@ export default function SignInModule() {
|
|||||||
placeholder=" "
|
placeholder=" "
|
||||||
labelPlacement="outside"
|
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"
|
||||||
|
|||||||
21
client/src/layouts/global.tsx
Normal file
21
client/src/layouts/global.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
34
client/src/layouts/restricted.tsx
Normal file
34
client/src/layouts/restricted.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
109
client/src/pages/ForgotPasswordPage.tsx
Normal file
109
client/src/pages/ForgotPasswordPage.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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,16 +61,7 @@ export default function ResetPasswordPage() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="absolute inset-0 p-8 w-full h-full flex flex-col justify-center">
|
|
||||||
<div className="flex flex-row justify-center">
|
|
||||||
{pageLoading && (
|
|
||||||
<div>
|
<div>
|
||||||
<CircularProgress label="Loading..." />
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{!pageLoading && (
|
|
||||||
<div className="flex flex-col gap-8 *:mx-auto">
|
|
||||||
<Card className="max-w-[600px] w-full mx-auto">
|
|
||||||
{validationResult && (
|
{validationResult && (
|
||||||
<div className="flex flex-col gap-8 p-12 text-left">
|
<div className="flex flex-col gap-8 p-12 text-left">
|
||||||
<div className="flex flex-col gap-2">
|
<div className="flex flex-col gap-2">
|
||||||
@@ -119,24 +105,11 @@ export default function ResetPasswordPage() {
|
|||||||
{!validationResult && (
|
{!validationResult && (
|
||||||
<div className="flex flex-col gap-8 p-12 *:mr-auto text-left">
|
<div className="flex flex-col gap-8 p-12 *:mr-auto text-left">
|
||||||
<div className="flex flex-col gap-2">
|
<div className="flex flex-col gap-2">
|
||||||
<p className="text-3xl font-bold">
|
<p className="text-3xl font-bold">Reset portal has been closed.</p>
|
||||||
Reset portal has been closed.
|
|
||||||
</p>
|
|
||||||
<p>Please request for a password reset again.</p>
|
<p>Please request for a password reset again.</p>
|
||||||
</div>
|
</div>
|
||||||
</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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user