Skip to content

Commit

Permalink
feat: implement user forgot password
Browse files Browse the repository at this point in the history
  • Loading branch information
dinkelspiel committed May 14, 2024
1 parent 4cbf6e4 commit 305fb1a
Show file tree
Hide file tree
Showing 8 changed files with 192 additions and 10 deletions.
15 changes: 15 additions & 0 deletions app/auth/(logged-in-guard)/layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { validateSessionToken } from '@/server/auth/validateSession';
import { redirect } from 'next/navigation';
import React, { ReactNode } from 'react';

const layout = async ({ children }: { children: ReactNode }) => {
const user = await validateSessionToken();

if (user !== null) {
return redirect('/dashboard');
}

return children;
};

export default layout;
File renamed without changes.
File renamed without changes.
56 changes: 56 additions & 0 deletions app/auth/forgot-password/client.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
'use client';
import Logo from '@/components/icons/logo';
import SubmitButton from '@/components/submitButton';
import { Input } from '@/components/ui/input';
import { forgotPassword as origFormAction } from '@/server/auth/forgotPassword';
import { User, UserForgotPassword } from '@prisma/client';
import { redirect } from 'next/navigation';
import { useEffect } from 'react';
import { useFormState } from 'react-dom';
import { toast } from 'sonner';

const Client = ({
forgotPassword,
}: {
forgotPassword: UserForgotPassword & { user: User };
}) => {
const [state, formAction] = useFormState(origFormAction, {});

useEffect(() => {
if (state.message) {
return redirect('/auth/login');
}

if (state.error) {
toast.error(state.error);
}
}, [state]);

return (
<form className="grid w-[350px] gap-8" action={formAction}>
<input type="hidden" name="forgotPasswordId" value={forgotPassword.id} />
<div className="grid gap-2">
<Logo className="mb-2" />
<h3 className="text-[22px] font-bold leading-7 tracking-[-0.02em]">
Reset password for {forgotPassword.user.username}
</h3>
<p className="text-sm text-muted-foreground">
Here you can reset the password for your account
</p>
</div>
<div className="grid gap-4">
<div className="grid gap-2">
<Input placeholder="Password" type="password" name="password" />
<Input
placeholder="Confirm Password"
type="password"
name="confirmPassword"
/>
</div>
<SubmitButton>Reset Password</SubmitButton>
</div>
</form>
);
};

export default Client;
27 changes: 27 additions & 0 deletions app/auth/forgot-password/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import prisma from '@/server/db';
import { redirect } from 'next/navigation';
import Client from './client';

const Page = async ({ searchParams }: { searchParams: { id: string } }) => {
const forgotPassword = await prisma.userForgotPassword.findFirst({
where: {
id: searchParams.id,
used: false,
},
include: {
user: true,
},
});

if (!forgotPassword) {
return redirect('/');
}

return (
<main className="grid h-[100dvh] w-full items-center justify-center bg-neutral-100">
<Client forgotPassword={forgotPassword} />
</main>
);
};

export default Page;
6 changes: 0 additions & 6 deletions app/auth/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,6 @@ import { ReactNode } from 'react';
import { Toaster } from 'sonner';

const Layout = async ({ children }: { children: ReactNode }) => {
const user = await validateSessionToken();

if (user !== null) {
return redirect('/dashboard');
}

return (
<html lang="en">
<body className={`min-h-[100dvh]`}>
Expand Down
18 changes: 14 additions & 4 deletions prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -33,10 +33,11 @@ model User {
createdAt DateTime @default(now())
updatedAt DateTime @default(now()) @updatedAt
following UserFollow[] @relation("follower")
followers UserFollow[] @relation("followee")
userEntries UserEntry[]
activity UserActivity[]
following UserFollow[] @relation("follower")
followers UserFollow[] @relation("followee")
userEntries UserEntry[]
activity UserActivity[]
UserForgotPassword UserForgotPassword[]
}

enum Activity {
Expand Down Expand Up @@ -358,3 +359,12 @@ model Entry {
@@index([originalTitle])
}

model UserForgotPassword {
id String @id @db.VarChar(36)
userId Int @db.UnsignedInt
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
used Boolean
createdAt DateTime @default(now())
updatedAt DateTime @default(now()) @updatedAt
}
80 changes: 80 additions & 0 deletions server/auth/forgotPassword.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
'use server';

import { z } from 'zod';
import prisma from '../db';
import bcrypt from 'bcrypt';
import { addMonths } from '@/lib/addMonths';
import { cookies, headers } from 'next/headers';
import { generateToken } from '@/lib/generateToken';

export const forgotPassword = async (
prevState: any,
formData: FormData
): Promise<{ error?: string; message?: string }> => {
const schema = z.object({
password: z.string(),
confirmPassword: z.string(),
forgotPasswordId: z.string(),
});

const validatedFields = schema.safeParse({
password: formData.get('password'),
confirmPassword: formData.get('confirmPassword'),
forgotPasswordId: formData.get('forgotPasswordId'),
});

if (!validatedFields.success) {
return {
error: Object.entries(validatedFields.error.flatten().fieldErrors)
.map(entry => `${entry[0]}: ${entry[1].map(error => error).join(' ')},`)
.join(' '),
};
}

const forgotPasswordId = validatedFields.data.forgotPasswordId;
const password = validatedFields.data.password;
const confirmPassword = validatedFields.data.confirmPassword;

if (password != confirmPassword) {
return {
error: "Passwords don't match",
};
}

const forgotPassword = await prisma.userForgotPassword.findFirst({
where: {
id: forgotPasswordId,
used: false,
},
});

if (!forgotPassword) {
return {
error: 'Invalid forgot password',
};
}

await prisma.userForgotPassword.update({
where: {
id: forgotPasswordId,
},
data: {
used: true,
},
});

await prisma.user.update({
where: {
id: forgotPassword.userId,
},
data: {
password: bcrypt.hashSync(password, 10),
},
});

await cookies().delete('mlSessionToken');

return {
message: 'Updated password',
};
};

0 comments on commit 305fb1a

Please sign in to comment.