Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Create forgot password form. #1

Merged
merged 2 commits into from
Sep 22, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions client/src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { Route, Routes } from "react-router-dom";

import { ForgotPasswordPage } from "@/pages/forgot-password/page";
import VerifyEmailPage from "@/pages/verify-email/page";
import PrivateGuard from "@/guards/private-guard";
import { AppRoutes } from "@/constants/routes";
Expand All @@ -20,6 +21,10 @@ function App() {
<Route path={AppRoutes.SignIn} element={<SignInPage />} />
<Route path={AppRoutes.VerifyEmail} element={<VerifyEmailPage />} />
</Route>
<Route
path={AppRoutes.ForgotPassword}
element={<ForgotPasswordPage />}
/>
</Routes>
</main>
);
Expand Down
9 changes: 5 additions & 4 deletions client/src/components/input.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ const Input = ({
const [obscure, setObscure] = useState(true);
const [valid, setValid] = useState(false);

const isPasswod = type === "password";
const isPassword = type === "password";
const hasInput = value.length > 0;

useEffect(() => {
Expand Down Expand Up @@ -75,7 +75,7 @@ const Input = ({
<input
value={value}
onChange={(e) => setValue(e.target.value)}
type={isPasswod ? (obscure ? "password" : "text") : type}
type={isPassword ? (obscure ? "password" : "text") : type}
placeholder={placeholder}
autoComplete={autoComplete}
id={name?.toLowerCase() ?? label.toLowerCase()}
Expand All @@ -84,7 +84,8 @@ const Input = ({
required={required}
maxLength={maxLength}
className={`w-full md:w-[400px] bg-background py-2.5 rounded-lg outline-none border ring-1 focus:ring-2 transition-all
placeholder-foreground/50 px-11 disabled:bg-opacity-25 disabled:border-primary/25 disabled:text-foreground/50 disabled:pointer-events-none
placeholder-foreground/50 disabled:bg-opacity-25 disabled:border-primary/25 disabled:text-foreground/50 disabled:pointer-events-none
${isPassword ? "px-11" : "pl-11"}
${
hasInput
? valid
Expand All @@ -96,7 +97,7 @@ const Input = ({
<div className="absolute inset-y-0 left-0 flex-center pl-3 pointer-events-none">
<SuffixIcon size={24} className="text-primary" />
</div>
{isPasswod && (
{isPassword && (
<button
type="button"
onClick={() => setObscure(!obscure)}
Expand Down
2 changes: 1 addition & 1 deletion client/src/components/or.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
const Or = () => {
return (
<div className="flex-between gap-x-2 w-full md:w-[400px] px-6 lg:px-0">
<div className="flex-between gap-x-2 w-full px-6">
<span className="h-px w-full flex-1 bg-foreground/40 rounded-full" />
<p className="text-sm lg:text-lg font-semibold text-foreground/40">Or</p>
<span className="h-px flex-1 bg-foreground/40 rounded-full" />
Expand Down
9 changes: 6 additions & 3 deletions client/src/components/switch-auth.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,18 @@
import { Link } from "react-router-dom";

interface SwitchAuthProps {
label: string;
label?: string;
tag: string;
href: string;
}

const SwitchAuth = ({ label, tag, href }: SwitchAuthProps) => {
return (
<div className="p-4 w-full md:w-[400px] text-sm lg:text-base rounded-md border border-secondary/50 bg-secondary/15 drop-shadow-2xl flex-center gap-x-2">
<p className="font-medium">{label}</p>
<div
className="p-4 w-full text-sm lg:text-base rounded-md border
border-secondary/50 bg-secondary/15 drop-shadow-2xl flex-center gap-x-2"
>
{label && <p className="font-medium">{label}</p>}
<Link
to={href}
className="font-bold underline-offset-4 hover:underline text-secondary transition-all hover:drop-shadow-secondary-glow"
Expand Down
1 change: 1 addition & 0 deletions client/src/constants/keys.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,4 @@ export const SIGNOUTKEY = "sign-out";
export const VERIFYEMAILKEY = "verify-email";
export const RESENDVERIFYKEY = "resend-verify";
export const VERIFYTOKENKEY = "verify-token";
export const FORGOTPASSWORDKEY = "forgot-password";
8 changes: 4 additions & 4 deletions client/src/lib/providers/theme-provider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,12 +23,12 @@ const ThemeProviderContext = createContext<ThemeProviderState>(initialState);
const ThemeProvider = ({
children,
defaultTheme = "system",
storageKey = "vite-ui-theme",
storageKey = "gra-theme",
...props
}: ThemeProviderProps) => {
const [theme, setTheme] =
useState<Theme>(() => localStorage.getItem(storageKey) as Theme) ||
defaultTheme;
const [theme, setTheme] = useState<Theme>(
() => (localStorage.getItem(storageKey) as Theme) || defaultTheme
);

useEffect(() => {
const root = window.document.documentElement;
Expand Down
24 changes: 24 additions & 0 deletions client/src/lib/services/auth-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -138,4 +138,28 @@ export const AuthService = {
user: data.user,
});
},
forgotPassword: async (email: string) => {
const res = await fetch(`${baseURL}${AppRoutes.ForgotPassword}`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
email: email,
}),
});

const data = await res.json();

if (!res.ok) {
throw new ErrorResponse({
status: res.status,
message: data.error,
});
}

return new GenericResponse({
message: data.message,
});
},
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import { useMutation } from "@tanstack/react-query";
import { useNavigate } from "react-router-dom";
import { Mail } from "lucide-react";
import { FormEvent } from "react";
import { toast } from "sonner";

import { GenericResponse } from "@/lib/classes/generic-response-class";
import { ErrorResponse } from "@/lib/classes/error-response-class";
import { AuthService } from "@/lib/services/auth-service";
import { ValidateEmail } from "@/lib/utils/validations";
import { FORGOTPASSWORDKEY } from "@/constants/keys";
import { Button } from "@/components/text-button";
import { AppRoutes } from "@/constants/routes";
import { Input } from "@/components/input";

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

const { mutate, isPending } = useMutation({
mutationKey: [FORGOTPASSWORDKEY],
mutationFn: AuthService.forgotPassword,
onSuccess: (res: GenericResponse) => {
toast.success(res.message);
navigate(AppRoutes.ResetPassword);
},
onError: (error: ErrorResponse) => toast.error(error.message),
});

const onSubmit = (e: FormEvent<HTMLFormElement>) => {
e.preventDefault();

const formData = new FormData(e.currentTarget);

const forgotPasswordData = Object.fromEntries(formData.entries());

mutate(forgotPasswordData["email"] as string);
};

return (
<form
onSubmit={onSubmit}
className="p-4 lg:p-6 w-full md:w-fit rounded-md border border-primary/50 bg-primary/15 drop-shadow-2xl space-y-4 lg:space-y-6"
>
<Input
required
name="email"
label="Email Address"
tooltip="Please enter a valid email address. Ensure it includes an '@' symbol and a domain name."
placeholder="e.g. [email protected]"
type="email"
autoComplete="email"
suffixIcon={Mail}
disabled={isPending}
validation={ValidateEmail}
maxLength={320}
/>

<Button
label="Send Reset Link"
disabled={isPending}
loading={isPending}
type="submit"
/>
</form>
);
};

export { ForgotPasswordForm };
27 changes: 27 additions & 0 deletions client/src/pages/forgot-password/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { SwitchAuth } from "@/components/switch-auth";
import { ForgotPasswordForm } from "./_components/forgot-password-form";
import { AppRoutes } from "@/constants/routes";

const ForgotPasswordPage = () => {
return (
<section className="px-4 lg:px-0 h-full flex-center flex-col gap-y-6 w-fit mx-auto">
<div className="w-full rounded-md border border-primary/50 bg-primary/15 drop-shadow-2xl">
<h2 className="text-lg lg:text-2xl font-extrabold text-center uppercase pt-4 pb-2">
Forgot Password
</h2>
<div className="w-full bg-primary/25 p-2.5 flex-center border-t border-primary/50">
<p className="text-xs lg:text-base text-center lg:max-w-[324px] text-foreground/60 font-medium">
Enter your email address and wait for a reset password link to be
sent.
</p>
</div>
</div>

<ForgotPasswordForm />

<SwitchAuth href={AppRoutes.Root} tag="Back" />
</section>
);
};

export { ForgotPasswordPage };
14 changes: 9 additions & 5 deletions client/src/pages/sign-in/_components/sign-in-form.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Link, useNavigate } from "react-router-dom";
import { useMutation } from "@tanstack/react-query";
import { useNavigate } from "react-router-dom";
import { FormEvent } from "react";
import { toast } from "sonner";

Expand Down Expand Up @@ -52,10 +52,6 @@ const SignInForm = () => {
onSubmit={onSubmit}
className="p-4 lg:p-6 w-full md:w-fit rounded-md border border-primary/50 bg-primary/15 drop-shadow-2xl space-y-4 lg:space-y-6"
>
<h2 className="text-lg lg:text-2xl font-extrabold text-center uppercase">
Access Your Account
</h2>

{SIGNININPUTS.map((s) => (
<Input
key={s.label}
Expand All @@ -66,6 +62,14 @@ const SignInForm = () => {
/>
))}

<Link
to={AppRoutes.ForgotPassword}
className="text-sm font-medium hover:underline underline-offset-4 transition-all text-foreground
hover:text-primary px-1.5 inline-block text-end w-full"
>
Forgot Password?
</Link>

<Button
label="Sign In"
disabled={isPending}
Expand Down
9 changes: 8 additions & 1 deletion client/src/pages/sign-in/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,14 @@ import { SignInForm } from "./_components/sign-in-form";

const SignInPage = () => {
return (
<section className="px-4 lg:px-0 h-full flex-center flex-col gap-y-6">
<section className="px-4 lg:px-0 h-full flex-center flex-col gap-y-6 w-fit mx-auto">
<h2
className="text-lg lg:text-2xl font-extrabold text-center uppercase w-full
rounded-md border border-primary/50 bg-primary/15 drop-shadow-2xl p-4"
>
Access Your Account
</h2>

<SignInForm />

<Or />
Expand Down
4 changes: 0 additions & 4 deletions client/src/pages/sign-up/_components/sign-up-form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -41,10 +41,6 @@ const SignUpForm = () => {
onSubmit={onSubmit}
className="p-4 lg:p-6 w-full md:w-fit rounded-md border border-primary/50 bg-primary/15 drop-shadow-2xl space-y-4 lg:space-y-6"
>
<h2 className="text-lg lg:text-2xl font-extrabold text-center uppercase">
Create Account
</h2>

{SIGNUPINPUTS.map((s) => (
<Input
key={s.label}
Expand Down
9 changes: 8 additions & 1 deletion client/src/pages/sign-up/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,14 @@ import { SignUpForm } from "./_components/sign-up-form";

const SignUpPage = () => {
return (
<section className="px-4 lg:px-0 h-full flex-center flex-col gap-y-6">
<section className="px-4 lg:px-0 h-full flex-center flex-col gap-y-6 w-fit mx-auto">
<h2
className="text-lg lg:text-2xl font-extrabold text-center uppercase w-full
rounded-md border border-primary/50 bg-primary/15 drop-shadow-2xl p-4"
>
Create Account
</h2>

<SignUpForm />

<Or />
Expand Down
5 changes: 4 additions & 1 deletion client/src/pages/verify-email/_components/resend-code.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,10 @@ const ResendCode = () => {
const onClick = () => mutate(email);

return (
<div className="flex-center gap-x-2 text-sm">
<div
className="p-4 w-full text-sm lg:text-base rounded-md border
border-secondary/50 bg-secondary/15 drop-shadow-2xl flex-center gap-x-2"
>
<p className="font-medium">Didn't receive a code?</p>
<button
className="font-bold underline-offset-4 hover:underline text-secondary transition-all hover:drop-shadow-secondary-glow"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,11 @@ const VerifyEmailForm = () => {
}, [code]);

return (
<form ref={formRef} onSubmit={onSubmit} className="space-y-4 lg:space-y-6">
<form
ref={formRef}
onSubmit={onSubmit}
className="p-4 lg:p-6 w-full md:w-fit rounded-md border border-primary/50 bg-primary/15 drop-shadow-2xl space-y-4 lg:space-y-6"
>
<div className="flex-center gap-x-4 lg:gap-x-6">
{code.map((digit, i) => (
<SingleInput
Expand Down
17 changes: 9 additions & 8 deletions client/src/pages/verify-email/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,17 @@ import { ResendCode } from "./_components/resend-code";

const VerifyEmailPage = () => {
return (
<section className="px-4 lg:px-0 h-full flex-center">
<div className="p-4 lg:p-6 w-full md:w-fit rounded-md border border-primary/50 bg-primary/15 drop-shadow-2xl space-y-4 lg:space-y-6">
<h2 className="text-lg lg:text-2xl font-extrabold text-center uppercase">
Verify Email
</h2>
<section className="px-4 lg:px-0 h-full flex-center flex-col gap-y-6 w-fit mx-auto">
<h2
className="text-lg lg:text-2xl font-extrabold text-center uppercase w-full
rounded-md border border-primary/50 bg-primary/15 drop-shadow-2xl p-4"
>
Verify Email
</h2>

<VerifyEmailForm />
<VerifyEmailForm />

<ResendCode />
</div>
<ResendCode />
</section>
);
};
Expand Down
4 changes: 2 additions & 2 deletions templates/request_reset_password.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,14 +16,14 @@ const _REQUEST_RESET_PASSWORD_HTML = `
</head>
<body style="font-family: Arial, sans-serif; line-height: 1.6; color: %s; max-width: 600px; margin: 0 auto; padding: 20px;">
<div style="background: linear-gradient(to right, %s, %s); padding: 20px; text-align: center;">
<h1 style="color: #DCF9F5; margin: 0;">Password Request Reset</h1>
<h1 style="color: #DCF9F5 !important; margin: 0;">Password Request Reset</h1>
</div>
<div style="background-color: %s; padding: 20px; border-radius: 0 0 5px 5px; box-shadow: 0 2px 5px rgba(0,0,0,0.1);">
<p>Hello,</p>
<p>We received a request to reset your password. If you didn't make this request, please ignore this email.</p>
<p>To reset your password, click the button below:</p>
<div style="text-align: center; margin: 30px 0;">
<a href="{resetURL}" style="background-color: %s; color: #DCF9F5; padding: 12px 20px; text-decoration: none; border-radius: 5px; font-weight: bold;">Reset Password</a>
<a href="{resetURL}" style="background-color: %s; color: #DCF9F5 !important; padding: 12px 20px; text-decoration: none; border-radius: 5px; font-weight: bold;">Reset Password</a>
</div>
<p>This link will expire in 15 minutes for security reasons.</p>
<p>Best regards,<br>Fingertips</p>
Expand Down
4 changes: 2 additions & 2 deletions templates/success_reset_password.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,13 @@ const _RESET_PASSWORD_SUCCESS_HTML = `
</head>
<body style="font-family: Arial, sans-serif; line-height: 1.6; color: %s; max-width: 600px; margin: 0 auto; padding: 20px;">
<div style="background: linear-gradient(to right, %s, %s); padding: 20px; text-align: center;">
<h1 style="color: #DCF9F5; margin: 0;">Password Reset Successful</h1>
<h1 style="color: #DCF9F5 !important; margin: 0;">Password Reset Successful</h1>
</div>
<div style="background-color: %s; padding: 20px; border-radius: 0 0 5px 5px; box-shadow: 0 2px 5px rgba(0,0,0,0.1);">
<p>Hello {username},</p>
<p>We're writing to confirm that your password has been successfully reset.</p>
<div style="text-align: center; margin: 30px 0;">
<div style="background-color: %s; color: #DCF9F5; width: 50px; height: 50px; line-height: 50px; border-radius: 50%%; display: inline-block; font-size: 30px;">
<div style="background-color: %s; color: #DCF9F5 !important; width: 50px; height: 50px; line-height: 50px; border-radius: 50%%; display: inline-block; font-size: 30px;">
</div>
</div>
Expand Down
Loading