Skip to content

Commit

Permalink
Merge pull request #2 from Fingertips18/development
Browse files Browse the repository at this point in the history
Create reset password page and consume its api
  • Loading branch information
Fingertips18 authored Sep 23, 2024
2 parents 01c27ae + 0e077b2 commit 4007d4a
Show file tree
Hide file tree
Showing 12 changed files with 216 additions and 22 deletions.
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

0 comments on commit 4007d4a

Please sign in to comment.