Skip to content

Commit 29156a9

Browse files
committed
reset password with node prisma and postgresql
1 parent a405258 commit 29156a9

File tree

7 files changed

+206
-3
lines changed

7 files changed

+206
-3
lines changed

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55
"license": "MIT",
66
"scripts": {
77
"start": "ts-node-dev --respawn --transpile-only --exit-child src/app.ts",
8+
"db:migrate": "npx prisma migrate dev --name user-entity --create-only && yarn prisma generate",
9+
"db:push": "npx prisma db push",
810
"build": "tsc . -p"
911
},
1012
"devDependencies": {
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
/*
2+
Warnings:
3+
4+
- A unique constraint covering the columns `[email,verificationCode,passwordResetToken]` on the table `users` will be added. If there are existing duplicate values, this will fail.
5+
6+
*/
7+
-- DropIndex
8+
DROP INDEX "users_email_verificationCode_idx";
9+
10+
-- DropIndex
11+
DROP INDEX "users_email_verificationCode_key";
12+
13+
-- AlterTable
14+
ALTER TABLE "users" ADD COLUMN "passwordResetAt" TIMESTAMP(3),
15+
ADD COLUMN "passwordResetToken" TEXT,
16+
ADD COLUMN "provider" TEXT;
17+
18+
-- CreateIndex
19+
CREATE INDEX "users_email_verificationCode_passwordResetToken_idx" ON "users"("email", "verificationCode", "passwordResetToken");
20+
21+
-- CreateIndex
22+
CREATE UNIQUE INDEX "users_email_verificationCode_passwordResetToken_key" ON "users"("email", "verificationCode", "passwordResetToken");

prisma/schema.prisma

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,12 @@ model User{
2626
createdAt DateTime @default(now())
2727
updatedAt DateTime @updatedAt
2828
29-
@@unique([email, verificationCode])
30-
@@index([email, verificationCode])
29+
provider String?
30+
passwordResetToken String?
31+
passwordResetAt DateTime?
32+
33+
@@unique([email, verificationCode, passwordResetToken])
34+
@@index([email, verificationCode,passwordResetToken])
3135
}
3236

3337
enum RoleEnumType {

src/controllers/auth.controller.ts

Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,10 @@ import crypto from 'crypto';
22
import { CookieOptions, NextFunction, Request, Response } from 'express';
33
import bcrypt from 'bcryptjs';
44
import {
5+
ForgotPasswordInput,
56
LoginUserInput,
67
RegisterUserInput,
8+
ResetPasswordInput,
79
VerifyEmailInput,
810
} from '../schemas/user.schema';
911
import {
@@ -263,3 +265,132 @@ export const verifyEmailHandler = async (
263265
next(err);
264266
}
265267
};
268+
269+
export const forgotPasswordHandler = async (
270+
req: Request<
271+
Record<string, never>,
272+
Record<string, never>,
273+
ForgotPasswordInput
274+
>,
275+
res: Response,
276+
next: NextFunction
277+
) => {
278+
try {
279+
// Get the user from the collection
280+
const user = await findUser({ email: req.body.email.toLowerCase() });
281+
const message =
282+
'You will receive a reset email if user with that email exist';
283+
if (!user) {
284+
return res.status(200).json({
285+
status: 'success',
286+
message,
287+
});
288+
}
289+
290+
if (!user.verified) {
291+
return res.status(403).json({
292+
status: 'fail',
293+
message: 'Account not verified',
294+
});
295+
}
296+
297+
if (user.provider) {
298+
return res.status(403).json({
299+
status: 'fail',
300+
message:
301+
'We found your account. It looks like you registered with a social auth account. Try signing in with social auth.',
302+
});
303+
}
304+
305+
const resetToken = crypto.randomBytes(32).toString('hex');
306+
const passwordResetToken = crypto
307+
.createHash('sha256')
308+
.update(resetToken)
309+
.digest('hex');
310+
311+
await updateUser(
312+
{ id: user.id },
313+
{
314+
passwordResetToken,
315+
passwordResetAt: new Date(Date.now() + 10 * 60 * 1000),
316+
},
317+
{ email: true }
318+
);
319+
320+
try {
321+
const url = `${config.get<string>('origin')}/resetPassword/${resetToken}`;
322+
await new Email(user, url).sendPasswordResetToken();
323+
324+
res.status(200).json({
325+
status: 'success',
326+
message,
327+
});
328+
} catch (err: any) {
329+
await updateUser(
330+
{ id: user.id },
331+
{ passwordResetToken: null, passwordResetAt: null },
332+
{}
333+
);
334+
return res.status(500).json({
335+
status: 'error',
336+
message: 'There was an error sending email',
337+
});
338+
}
339+
} catch (err: any) {
340+
next(err);
341+
}
342+
};
343+
344+
export const resetPasswordHandler = async (
345+
req: Request<
346+
ResetPasswordInput['params'],
347+
Record<string, never>,
348+
ResetPasswordInput['body']
349+
>,
350+
res: Response,
351+
next: NextFunction
352+
) => {
353+
try {
354+
// Get the user from the collection
355+
const passwordResetToken = crypto
356+
.createHash('sha256')
357+
.update(req.params.resetToken)
358+
.digest('hex');
359+
360+
const user = await findUser({
361+
passwordResetToken,
362+
passwordResetAt: {
363+
gt: new Date(),
364+
},
365+
});
366+
367+
if (!user) {
368+
return res.status(403).json({
369+
status: 'fail',
370+
message: 'Invalid token or token has expired',
371+
});
372+
}
373+
374+
const hashedPassword = await bcrypt.hash(req.body.password, 12);
375+
// Change password data
376+
await updateUser(
377+
{
378+
id: user.id,
379+
},
380+
{
381+
password: hashedPassword,
382+
passwordResetToken: null,
383+
passwordResetAt: null,
384+
},
385+
{ email: true }
386+
);
387+
388+
logout(res);
389+
res.status(200).json({
390+
status: 'success',
391+
message: 'Password data updated successfully',
392+
});
393+
} catch (err: any) {
394+
next(err);
395+
}
396+
};

src/routes/auth.routes.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,21 @@
11
import express from 'express';
22
import {
3+
forgotPasswordHandler,
34
loginUserHandler,
45
logoutUserHandler,
56
refreshAccessTokenHandler,
67
registerUserHandler,
8+
resetPasswordHandler,
79
verifyEmailHandler,
810
} from '../controllers/auth.controller';
911
import { deserializeUser } from '../middleware/deserializeUser';
1012
import { requireUser } from '../middleware/requireUser';
1113
import { validate } from '../middleware/validate';
1214
import {
15+
forgotPasswordSchema,
1316
loginUserSchema,
1417
registerUserSchema,
18+
resetPasswordSchema,
1519
verifyEmailSchema,
1620
} from '../schemas/user.schema';
1721

@@ -31,4 +35,16 @@ router.get(
3135

3236
router.get('/logout', deserializeUser, requireUser, logoutUserHandler);
3337

38+
router.post(
39+
'/forgotPassword',
40+
validate(forgotPasswordSchema),
41+
forgotPasswordHandler
42+
);
43+
44+
router.patch(
45+
'/resetPassword/:resetToken',
46+
validate(resetPasswordSchema),
47+
resetPasswordHandler
48+
);
49+
3450
export default router;

src/schemas/user.schema.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,31 @@ export const updateUserSchema = object({
6262
}),
6363
});
6464

65+
export const forgotPasswordSchema = object({
66+
body: object({
67+
email: string({
68+
required_error: 'Email is required',
69+
}).email('Email is invalid'),
70+
}),
71+
});
72+
73+
export const resetPasswordSchema = object({
74+
params: object({
75+
resetToken: string(),
76+
}),
77+
body: object({
78+
password: string({
79+
required_error: 'Password is required',
80+
}).min(8, 'Password must be more than 8 characters'),
81+
passwordConfirm: string({
82+
required_error: 'Please confirm your password',
83+
}),
84+
}).refine((data) => data.password === data.passwordConfirm, {
85+
message: 'Passwords do not match',
86+
path: ['passwordConfirm'],
87+
}),
88+
});
89+
6590
export type RegisterUserInput = Omit<
6691
TypeOf<typeof registerUserSchema>['body'],
6792
'passwordConfirm'
@@ -70,3 +95,6 @@ export type RegisterUserInput = Omit<
7095
export type LoginUserInput = TypeOf<typeof loginUserSchema>['body'];
7196
export type VerifyEmailInput = TypeOf<typeof verifyEmailSchema>['params'];
7297
export type UpdateUserInput = TypeOf<typeof updateUserSchema>['body'];
98+
99+
export type ForgotPasswordInput = TypeOf<typeof forgotPasswordSchema>['body'];
100+
export type ResetPasswordInput = TypeOf<typeof resetPasswordSchema>;

src/services/user.service.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ export const findUniqueUser = async (
3535
};
3636

3737
export const updateUser = async (
38-
where: Partial<Prisma.UserCreateInput>,
38+
where: Partial<Prisma.UserWhereUniqueInput>,
3939
data: Prisma.UserUpdateInput,
4040
select?: Prisma.UserSelect
4141
) => {

0 commit comments

Comments
 (0)