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 reset password page #2

Merged
merged 2 commits into from
Sep 23, 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,6 +1,7 @@
import { Route, Routes } from "react-router-dom";

import { ForgotPasswordPage } from "@/pages/forgot-password/page";
import { ResetPasswordPage } from "@/pages/reset-password/page";
import VerifyEmailPage from "@/pages/verify-email/page";
import PrivateGuard from "@/guards/private-guard";
import { AppRoutes } from "@/constants/routes";
Expand All @@ -25,6 +26,10 @@ function App() {
path={AppRoutes.ForgotPassword}
element={<ForgotPasswordPage />}
/>
<Route
path={`${AppRoutes.ResetPassword}/:token`}
element={<ResetPasswordPage />}
/>
</Routes>
</main>
);
Expand Down
43 changes: 40 additions & 3 deletions client/src/constants/collections.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import {
ValidateUsername,
} from "@/lib/utils/validations";

export const SIGNUPINPUTS = [
export const SIGNUP_INPUTS = [
{
name: "username",
label: "Username",
Expand Down Expand Up @@ -35,7 +35,7 @@ export const SIGNUPINPUTS = [
name: "password",
label: "Password",
tooltip:
"Create a strong password with at least 8 characters. Include a mix of uppercase letters, lowercase letters, numbers, and special characters for better security.",
"Create a password with at least 8 characters, including uppercase, lowercase, numbers, and special characters for security.",
placeholder: "e.g. m#P52s@ap$V",
type: "password",
autoComplete: "new-password",
Expand All @@ -56,7 +56,7 @@ export const SIGNUPINPUTS = [
},
];

export const SIGNININPUTS = [
export const SIGNIN_INPUTS = [
{
name: "email",
label: "Email Address",
Expand All @@ -82,3 +82,40 @@ export const SIGNININPUTS = [
maxLength: 128,
},
];

export const RESET_PASSWORD_INPUTS = [
{
name: "old-password",
label: "Old Password",
tooltip: "Enter your current password",
placeholder: "e.g. m#P52s@ap$V",
type: "password",
autoComplete: "new-password",
suffixIcon: Lock,
validation: ValidatePassword,
maxLength: 128,
},
{
name: "new-password",
label: "New Password",
tooltip:
"Create a password with at least 8 characters, including uppercase, lowercase, numbers, and special characters for security",
placeholder: "e.g. m#P52s@ap$V",
type: "password",
autoComplete: "new-password",
suffixIcon: Lock,
validation: ValidatePassword,
maxLength: 128,
},
{
label: "Confirm Password",
tooltip:
"Re-enter your password to confirm it matches the one you typed above. Ensure both passwords are identical.",
placeholder: "e.g. m#P52s@ap$V",
type: "password",
autoComplete: "new-password",
suffixIcon: Lock,
validation: ValidateConfirmPassword,
maxLength: 128,
},
];
1 change: 1 addition & 0 deletions client/src/constants/keys.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,4 @@ export const VERIFYEMAILKEY = "verify-email";
export const RESENDVERIFYKEY = "resend-verify";
export const VERIFYTOKENKEY = "verify-token";
export const FORGOTPASSWORDKEY = "forgot-password";
export const RESETPASSWORDKEY = "reset-password";
4 changes: 4 additions & 0 deletions client/src/lib/DTO/reset-dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export type ResetDTO = {
token: string;
password: string;
};
28 changes: 28 additions & 0 deletions client/src/lib/services/auth-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { UserResponse } from "@/lib/classes/user-response-class";
import { SignUpDTO } from "@/lib/DTO/sign-up-dto";
import { SignInDTO } from "@/lib/DTO/sign-in-dto";
import { AppRoutes } from "@/constants/routes";
import { ResetDTO } from "../DTO/reset-dto";

const baseURL =
import.meta.env.VITE_ENV === "development"
Expand Down Expand Up @@ -158,6 +159,33 @@ export const AuthService = {
});
}

return new GenericResponse({
message: data.message,
});
},
resetPassword: async (reset: ResetDTO) => {
const res = await fetch(
`${baseURL}${AppRoutes.ResetPassword}/${reset.token}`,
{
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
password: reset.password,
}),
}
);

const data = await res.json();

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

return new GenericResponse({
message: data.message,
});
Expand Down
2 changes: 1 addition & 1 deletion client/src/lib/stores/auth-store.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { create } from "zustand";
import { createJSONStorage, persist } from "zustand/middleware";
import { create } from "zustand";

interface AuthStoreState {
email: string;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import { useMutation } from "@tanstack/react-query";
import { useNavigate } from "react-router-dom";
import { FormEvent, useState } from "react";
import { Mail } from "lucide-react";
import { FormEvent } from "react";
import { toast } from "sonner";

import { GenericResponse } from "@/lib/classes/generic-response-class";
Expand All @@ -10,18 +9,17 @@ 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 [submitted, setSubmitted] = useState(false);
const [email, setEmail] = useState("");
const { mutate, isPending } = useMutation({
mutationKey: [FORGOTPASSWORDKEY],
mutationFn: AuthService.forgotPassword,
onSuccess: (res: GenericResponse) => {
toast.success(res.message);
navigate(AppRoutes.ResetPassword);
setSubmitted(true);
},
onError: (error: ErrorResponse) => toast.error(error.message),
});
Expand All @@ -33,10 +31,25 @@ const ForgotPasswordForm = () => {

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

mutate(forgotPasswordData["email"] as string);
const emailData = forgotPasswordData["email"] as string;

setEmail(emailData);

mutate(emailData);
};

return (
return submitted ? (
<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">
<div className="w-16 h-16 bg-accent rounded-full flex items-center justify-center mx-auto mb-4">
<Mail className="h-8 w-8 text-white" />
</div>
<p className="text-sm text-foreground/80 text-center lg:w-[386px] mx-auto">
If an account exists for{" "}
<span className="font-medium text-primary">{email}</span>, you will
receive a password reset link shortly.
</p>
</div>
) : (
<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"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import { useNavigate, useParams } from "react-router-dom";
import { useMutation } from "@tanstack/react-query";
import { FormEvent, useState } from "react";
import { toast } from "sonner";

import { GenericResponse } from "@/lib/classes/generic-response-class";
import { ErrorResponse } from "@/lib/classes/error-response-class";
import { RESET_PASSWORD_INPUTS } from "@/constants/collections";
import { AuthService } from "@/lib/services/auth-service";
import { RESETPASSWORDKEY } from "@/constants/keys";
import { Button } from "@/components/text-button";
import { AppRoutes } from "@/constants/routes";
import { ResetDTO } from "@/lib/DTO/reset-dto";
import { Input } from "@/components/input";

const ResetPasswordForm = () => {
const navigate = useNavigate();
const { token } = useParams();
const [confirmPassword, setConfirmPassword] = useState("");
const { mutate, isPending } = useMutation({
mutationKey: [RESETPASSWORDKEY],
mutationFn: AuthService.resetPassword,
onSuccess: (res: GenericResponse) => {
toast.success(res.message);
navigate(AppRoutes.SignIn);
},
onError: (error: ErrorResponse) => toast.error(error.message),
});

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

if (!token) return;

const formData = new FormData(e.currentTarget);

const resetPasswordData = Object.fromEntries(
formData.entries()
) as ResetDTO;

resetPasswordData.token = token;

mutate(resetPasswordData);
};

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"
>
{RESET_PASSWORD_INPUTS.map((r) => (
<Input
key={r.label}
required
disabled={isPending}
{...r}
validation={(value) => {
if (r.name === "new-password") {
setConfirmPassword(value);
}
if (r.name === undefined) {
return r.validation({
pass1: value,
pass2: confirmPassword,
});
}

return r.validation(value);
}}
/>
))}

<Button
label="Sign In"
disabled={isPending}
loading={isPending}
type="submit"
/>
</form>
);
};

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

import { ResetPasswordForm } from "./_components/reset-password-form";

const ResetPasswordPage = () => {
return (
<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"
>
Reset Password
</h2>

<ResetPasswordForm />

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

export { ResetPasswordPage };
10 changes: 5 additions & 5 deletions client/src/pages/sign-in/_components/sign-in-form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { GenericResponse } from "@/lib/classes/generic-response-class";
import { ErrorResponse } from "@/lib/classes/error-response-class";
import { AuthService } from "@/lib/services/auth-service";
import { useAuthStore } from "@/lib/stores/auth-store";
import { SIGNININPUTS } from "@/constants/collections";
import { SIGNIN_INPUTS } from "@/constants/collections";
import { SignInDTO } from "@/lib/DTO/sign-in-dto";
import { Button } from "@/components/text-button";
import { AppRoutes } from "@/constants/routes";
Expand Down Expand Up @@ -52,13 +52,13 @@ 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"
>
{SIGNININPUTS.map((s) => (
{SIGNIN_INPUTS.map((i) => (
<Input
key={s.label}
key={i.label}
required
disabled={isPending}
{...s}
validation={s.validation}
{...i}
validation={i.validation}
/>
))}

Expand Down
4 changes: 2 additions & 2 deletions client/src/pages/sign-up/_components/sign-up-form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { toast } from "sonner";

import { ErrorResponse } from "@/lib/classes/error-response-class";
import { AuthService } from "@/lib/services/auth-service";
import { SIGNUPINPUTS } from "@/constants/collections";
import { SIGNUP_INPUTS } from "@/constants/collections";
import { SignUpDTO } from "@/lib/DTO/sign-up-dto";
import { Button } from "@/components/text-button";
import { AppRoutes } from "@/constants/routes";
Expand Down Expand Up @@ -41,7 +41,7 @@ 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"
>
{SIGNUPINPUTS.map((s) => (
{SIGNUP_INPUTS.map((s) => (
<Input
key={s.label}
required
Expand Down
6 changes: 3 additions & 3 deletions controllers/auth_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -256,14 +256,14 @@ func ResetPassword(c fiber.Ctx) error {
return c.Status(fiber.StatusBadRequest).JSON(dto.ErrorDTO{Error: "Either password is invalid or empty"})
}

id := c.Params("token")
if id == "" {
token := c.Params("token")
if token == "" {
return c.Status(fiber.StatusBadRequest).JSON(dto.ErrorDTO{Error: "Missing token"})
}

var user models.User

res := database.Instance.Where("reset_password_token = ?", id).First(&user)
res := database.Instance.Where("reset_password_token = ?", token).First(&user)
if res.Error != nil {
if res.Error == gorm.ErrRecordNotFound {
return c.Status(fiber.StatusNotFound).JSON(dto.ErrorDTO{Error: "User not found"})
Expand Down