Skip to content

Commit

Permalink
refactor: Update forgot/reset password pages
Browse files Browse the repository at this point in the history
- Update the list of allowed routes in the middleware to include "/auth/:path*"
- Correct the link in the ForgotPasswordEmail component
- Update the reset password token verification to hash the token before comparing
- Update the admin page to use absolute URLs for the dashboard buttons
- Update the ResetPasswordForm component to import Link from "next/link" and add a toast action to request a new password reset
- Update the ResetPasswordPage component to display an error card if the reset token is invalid or expired
- Linted everything
  • Loading branch information
dyzhuu committed Oct 8, 2024
1 parent db79d1a commit 2add459
Show file tree
Hide file tree
Showing 22 changed files with 188 additions and 130 deletions.
6 changes: 3 additions & 3 deletions src/app/admin/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,15 +16,15 @@ export default async function AdminDashboardPage() {
<Heading>Dashboard</Heading>
</div>
<div className="flex flex-col gap-4 px-4">
<DashboardButton href="admin/view-sessions">
<DashboardButton href="/admin/view-sessions">
<CalendarDays size={24} className="min-w-6" />
View Sessions
</DashboardButton>
<DashboardButton href="admin/semesters">
<DashboardButton href="/admin/semesters">
<CalendarClock size={24} className="min-w-6" />
Edit Semester Schedules
</DashboardButton>
<DashboardButton href="admin/members" className="relative">
<DashboardButton href="/admin/members" className="relative">
<MemberApprovalPing />
<BsPersonFillCheck size={24} className="min-w-6" /> Members
</DashboardButton>
Expand Down
6 changes: 2 additions & 4 deletions src/app/api/auth/forgot-password/route.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { NextRequest } from "next/server";
import { type NextRequest } from "next/server";
import { and, eq, isNotNull } from "drizzle-orm";
import { z } from "zod";

Expand Down Expand Up @@ -41,7 +41,5 @@ export const POST = routeWrapper(async function (req: NextRequest) {
await sendForgotPasswordEmail(user, token);
}

return new Response(null, {
status: 204,
});
return responses.success();
});
2 changes: 1 addition & 1 deletion src/app/api/auth/reset-password/route.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { createHash } from "crypto";
import { NextRequest } from "next/server";
import type { NextRequest } from "next/server";
import { hash } from "bcrypt";
import { and, eq, gt } from "drizzle-orm";
import { z } from "zod";
Expand Down
1 change: 0 additions & 1 deletion src/app/api/game-sessions/active-dates/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@ import {
gameSessions,
semesters,
} from "@/lib/db/schema";
import { getCurrentUser } from "@/lib/session";
import { getWeekday } from "@/lib/utils";
import { clampInterval } from "@/lib/utils/dates";
import { adminRouteWrapper } from "@/lib/wrappers";
Expand Down
1 change: 0 additions & 1 deletion src/app/api/game-sessions/current/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ import {
bookings,
gameSessions,
} from "@/lib/db/schema";
import { rateLimit } from "@/lib/rate-limit";
import { routeWrapper } from "@/lib/wrappers";

interface Session {
Expand Down
6 changes: 3 additions & 3 deletions src/app/auth/(auth)/forgot-password/page.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import Link from "next/link";

import { BreakLine } from "@/components/auth/BreakLine";
import { EmailForgotPassword } from "@/components/auth/EmailForgotPassword";
import { ForgotPasswordForm } from "@/components/auth/ForgotPasswordForm";
import { GoogleSignIn } from "@/components/auth/GoogleLoginButton";

export const metadata = {
Expand All @@ -11,13 +11,13 @@ export const metadata = {
export default async function ForgotPasswordPage() {
return (
<div className="mt-8 flex w-full flex-col gap-4">
<EmailForgotPassword />
<ForgotPasswordForm />
<BreakLine label="or" />
<GoogleSignIn className="w-full" />
<p className="mt-2 text-center text-xs text-tertiary dark:text-white">
Back to{" "}
<Link className="font-bold underline" href="/auth/login?open=true">
Log in
Login
</Link>
</p>
</div>
Expand Down
11 changes: 7 additions & 4 deletions src/app/auth/(auth)/login/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,16 @@ export default async function LoginPage() {
</Suspense>
<BreakLine label="or" />
<GoogleSignIn className="w-full" />
<p className="mt-2 text-center text-xs text-tertiary dark:text-white">
<Link className="font-bold underline" href="/auth/signup">
<p className="mt-2 text-center text-xs text-tertiary">
<Link className="text-left font-bold underline" href="/auth/signup">
Create Account
</Link>
<span className="pointer-events-none mx-1">|</span>
<Link className="font-bold underline" href="/auth/forgot-password">
Reset Password
<Link
className="text-right font-bold underline"
href="/auth/forgot-password"
>
Forgot Password?
</Link>
</p>
</div>
Expand Down
35 changes: 32 additions & 3 deletions src/app/auth/reset-password/page.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import { Suspense } from "react";
import Link from "next/link";
import { redirect } from "next/navigation";
import { z } from "zod";

import { ResetPasswordForm } from "@/components/auth/ResetPasswordForm";
import { BackNavigationBar } from "@/components/BackNavigationBar";
import { Card } from "@/components/Card";
import { verifyResetPasswordToken } from "@/services/reset-password";

export const metadata = {
Expand All @@ -30,12 +31,40 @@ export default async function ResetPasswordPage({
const isTokenValid = await verifyResetPasswordToken(token);

if (!isTokenValid) {
return redirect("/auth/login");
return (
<div className="mx-4 flex min-h-dvh flex-col">
<BackNavigationBar
title="Reset Your Password"
pathName="/auth/login?open=true"
/>
<div className="grid grow place-items-center">
<Card
variant="card"
className="bg-destructive text-destructive-foreground"
>
<h1 className="pb-1 text-lg font-semibold tracking-tight">
Expired Link
</h1>
This password reset link has expired. Please request a new one{" "}
<Link
className="text-right font-bold underline"
href="/auth/forgot-password"
>
here
</Link>
.
</Card>
</div>
</div>
);
}

return (
<div className="mx-4 flex min-h-dvh flex-col">
<BackNavigationBar title="Reset Your Password" pathName="/auth/login" />
<BackNavigationBar
title="Reset Your Password"
pathName="/auth/login?open=true"
/>
<div className="grid grow place-items-center">
<ResetPasswordForm token={token} />
</div>
Expand Down
1 change: 0 additions & 1 deletion src/app/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import type { ReactNode } from "react";
import localFont from "next/font/local";

import "./globals.css";

Expand Down
2 changes: 1 addition & 1 deletion src/app/sessions/select-play-level/client-page.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
"use client";

import { useMemo, useState } from "react";
import { useMemo } from "react";
import { redirect, useRouter } from "next/navigation";

import { BackNavigationBar } from "@/components/BackNavigationBar";
Expand Down
91 changes: 0 additions & 91 deletions src/components/auth/EmailForgotPassword.tsx

This file was deleted.

7 changes: 3 additions & 4 deletions src/components/auth/EmailSignUpForm.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
"use client";

import React from "react";
import { useState } from "react";
import React, { useState } from "react";
import { zodResolver } from "@hookform/resolvers/zod";
import { signIn } from "next-auth/react";
import { useForm } from "react-hook-form";
Expand All @@ -11,12 +10,12 @@ import { useValidateEmailMutation } from "@/hooks/mutations/registration";
import { TextInput } from "../TextInput";
import { Button } from "../ui/button";
import { useToast } from "../ui/use-toast";
import { emailSchema, passwordSchema } from "./formSchema";
import { OTPFormAlertDialog } from "./OTPFormAlertDialog";
import { passwordSchema, emailSchema } from "./formSchema";

const formSchema = z.object({
email: emailSchema,
password: passwordSchema
password: passwordSchema,
});

export const EmailSignUpForm = () => {
Expand Down
107 changes: 107 additions & 0 deletions src/components/auth/ForgotPasswordForm.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
"use client";

import { useState } from "react";
import Link from "next/link";
import { useRouter } from "next/navigation";
import { zodResolver } from "@hookform/resolvers/zod";
import { useForm } from "react-hook-form";
import { z } from "zod";

import { useForgotPasswordMutation } from "@/hooks/mutations/forgot-password";
import { Card } from "../Card";
import { TextInput } from "../TextInput";
import { Button } from "../ui/button";
import { useToast } from "../ui/use-toast";

const formSchema = z.object({
email: z.string().email(),
});

export const ForgotPasswordForm = () => {
console.log("rerender");
const router = useRouter();
const [isSubmitted, setIsSubmitted] = useState<boolean>(false);
const { toast } = useToast();

const {
register,
handleSubmit,
getValues,
formState: { errors },
} = useForm<z.infer<typeof formSchema>>({
resolver: zodResolver(formSchema),
});

const { mutate, isPending } = useForgotPasswordMutation();

const onSubmit = async (data: z.infer<typeof formSchema>) => {
mutate(data.email, {
onSuccess: () => {
setIsSubmitted(true);
},
onError: (e) => {
if (e.message === "TOO_MANY_REQUESTS") {
toast({
title: "Too many requests",
description:
"You have made too many password reset requests. Please try again later.",
variant: "destructive",
});
} else {
toast({
title: "Uh oh! Something went wrong",
description:
"An error occurred while processing your request. Please try again.",
variant: "destructive",
});
}
},
});
};

if (isSubmitted)
return (
<div className="flex w-full flex-col justify-center gap-4">
<span className="text-center text-foreground">Forgot Password?</span>
<Card variant="card" className="space-y-2 text-sm">
<p>
We&apos;ve emailed a password reset link to{" "}
<strong>{getValues("email")}</strong>. Please check your inbox and
follow the instructions to reset your password.
</p>
<p>
If you did not receive an email, please sign up for an account{" "}
<Link
className="text-right font-bold underline"
href="/auth/signup"
>
here
</Link>
.
</p>
</Card>
<Button large onClick={() => router.push("/auth/login?open=true")}>
Back to Login
</Button>
</div>
);

return (
<form onSubmit={handleSubmit(onSubmit)}>
<div className="flex flex-col gap-4">
<span className="text-center text-foreground">Forgot Password?</span>
<TextInput
autoFocus
label="Email"
type="email"
isError={!!errors.email}
errorMessage={errors.email?.message}
{...register("email")}
/>
<Button large type="submit" disabled={isPending}>
Send Reset Link
</Button>
</div>
</form>
);
};
Loading

0 comments on commit 2add459

Please sign in to comment.