Trong bài viết này, bạn sẽ tìm hiểu cách thực hiện quên/đặt lại mật khẩu bằng Node. js, Prisma, PostgreSQL, Nodemailer, Redis, Docker-compose và Pug
API CRUD với nút. js và Dòng PostgreSQL
- Nút API. js, TypeScript, Prisma, PostgreSQL. Thiết lập dự án
- Nút. js + Prisma + PostgreSQL. Truy cập & làm mới mã thông báo
- API CRUD với nút. js và PostgreSQL. Gửi email HTML
- API với nút. js, Prisma và PostgreSQL. Quên/Đặt lại mật khẩu
Mục lục
Quên/Đặt lại mật khẩu bằng nút. js, Prisma và PostgreSQL
PHƯƠNG PHÁP HTTPROUTEDESCRIPTIONPOST/api/auth/quên mật khẩuĐể yêu cầu đặt lại mã thông báoPATCH/api/auth/resetpassword/. resetTokenĐể đặt lại mật khẩuĐể đặt lại mật khẩu, người dùng sẽ thực hiện yêu cầu POST với địa chỉ email của mình tới Nút. máy chủ js
Sau đó, máy chủ xác thực email, tạo mã thông báo đặt lại và gửi mã thông báo đặt lại mật khẩu đến email của người dùng
Ngoài ra, máy chủ trả về thông báo thành công cho ứng dụng giao diện người dùng cho biết rằng email đã được gửi
Sau đó, người dùng nhấp vào nút “Đặt lại mật khẩu” khi nhận được email mã thông báo đặt lại
Sau đó, người dùng được đưa đến trang đặt lại mật khẩu nơi anh ta được yêu cầu nhập mật khẩu mới trước khi thực hiện yêu cầu PATCH đến máy chủ
Sau đó, máy chủ xác thực mã thông báo đặt lại và cập nhật mật khẩu của người dùng trong cơ sở dữ liệu trước khi gửi lại thông báo thành công cho ứng dụng giao diện người dùng
Ứng dụng giao diện người dùng nhận được thông báo thành công và chuyển hướng người dùng đến trang đăng nhập
Cập nhật mô hình người dùng Prisma
Cơ chế quên/đặt lại mật khẩu yêu cầu bảng
{
"scripts": {
"start": "ts-node-dev --respawn --transpile-only --exit-child src/app.ts",
"db:migrate": "npx prisma migrate dev --name user-entity --create-only && yarn prisma generate",
"db:push": "npx prisma db push",
"build": "tsc . -p"
},
}
1 có một số cột cụ thể. Để làm như vậy, hãy chỉnh sửa và thêm các trường sau vào lược đồ Prisma của người dùng
2. Cột này lưu trữ một giá trị nếu người dùng đã đăng ký với nhà cung cấp OAuth của Google, GitHub hoặc Facebook. Bước này là cần thiết cho chuỗi sắp tới{ "scripts": { "start": "ts-node-dev --respawn --transpile-only --exit-child src/app.ts", "db:migrate": "npx prisma migrate dev --name user-entity --create-only && yarn prisma generate", "db:push": "npx prisma db push", "build": "tsc . -p" }, }
3. Cột này lưu trữ mã thông báo đặt lại mật khẩu đã băm{ "scripts": { "start": "ts-node-dev --respawn --transpile-only --exit-child src/app.ts", "db:migrate": "npx prisma migrate dev --name user-entity --create-only && yarn prisma generate", "db:push": "npx prisma db push", "build": "tsc . -p" }, }
4. Cột này lưu dấu thời gian trong đó người dùng phải thay đổi mật khẩu. Tôi quyết định cho người dùng 10 phút để thay đổi mật khẩu nhưng bạn có thể thay đổi thời gian cho phù hợp với ứng dụng của mình{ "scripts": { "start": "ts-node-dev --respawn --transpile-only --exit-child src/app.ts", "db:migrate": "npx prisma migrate dev --name user-entity --create-only && yarn prisma generate", "db:push": "npx prisma db push", "build": "tsc . -p" }, }
Cuối cùng, tôi đã thêm một ràng buộc duy nhất và một chỉ mục trên cột
{
"scripts": {
"start": "ts-node-dev --respawn --transpile-only --exit-child src/app.ts",
"db:migrate": "npx prisma migrate dev --name user-entity --create-only && yarn prisma generate",
"db:push": "npx prisma db push",
"build": "tsc . -p"
},
}
3 vì chúng ta sẽ truy vấn cơ sở dữ liệu bằng nólăng kính/lược đồ. lăng trụ
model User{
@@map[name: "users"]
id String @id @default[uuid[]]
name String
email String @unique
photo String? @default["default.png"]
verified Boolean? @default[false]
password String
role RoleEnumType? @default[user]
verificationCode String? @db.Text @unique
createdAt DateTime @default[now[]]
updatedAt DateTime @updatedAt
provider String?
passwordResetToken String?
passwordResetAt DateTime?
@@unique[[email, verificationCode, passwordResetToken]]
@@index[[email, verificationCode,passwordResetToken]]
}
enum RoleEnumType {
user
admin
}
Tạo di chuyển và cập nhật cơ sở dữ liệu PostgreSQL
Khi bạn đã cập nhật lược đồ người dùng, bạn cần tạo một di chuyển Prisma mới để phản ánh các thay đổi trước khi đẩy nó vào cơ sở dữ liệu PostgreSQL
Bộ chứa docker cơ sở dữ liệu PostgreSQL phải đang chạy để nó hoạt động
bưu kiện. json
{
"scripts": {
"start": "ts-node-dev --respawn --transpile-only --exit-child src/app.ts",
"db:migrate": "npx prisma migrate dev --name user-entity --create-only && yarn prisma generate",
"db:push": "npx prisma db push",
"build": "tsc . -p"
},
}
Hãy nhớ thay đổi tên di chuyển
{
"scripts": {
"start": "ts-node-dev --respawn --transpile-only --exit-child src/app.ts",
"db:migrate": "npx prisma migrate dev --name user-entity --create-only && yarn prisma generate",
"db:push": "npx prisma db push",
"build": "tsc . -p"
},
}
6 để giúp bạn phân biệt giữa các lần di chuyển khác nhau trong tương laiChạy lệnh sau để tạo và đẩy quá trình di chuyển Prisma sang cơ sở dữ liệu PostgreSQL
________số 8_______Cập nhật lược đồ người dùng
Trong mọi ứng dụng phụ trợ, bạn luôn nên xác thực nội dung yêu cầu trong phần mềm trung gian trước khi chuyển nó tới bộ điều khiển hoặc trình xử lý
Bạn cần xác thực nội dung yêu cầu và gửi thông báo lỗi thích hợp khi bất kỳ quy tắc lược đồ nào bị vi phạm
Có nhiều thư viện xác thực như Joi, Zod, Yup, v.v. nhưng tôi quyết định sử dụng Zod vì đây là thư viện xác thực lược đồ Typescript đầu tiên với suy luận kiểu tĩnh và tôi cảm thấy thoải mái khi sử dụng nó trong React và Node của mình. dự án js
Thêm các lược đồ sau vào tệp
{
"scripts": {
"start": "ts-node-dev --respawn --transpile-only --exit-child src/app.ts",
"db:migrate": "npx prisma migrate dev --name user-entity --create-only && yarn prisma generate",
"db:push": "npx prisma db push",
"build": "tsc . -p"
},
}
7src/lược đồ/người dùng. lược đồ. ts
export const forgotPasswordSchema = object[{
body: object[{
email: string[{
required_error: 'Email is required',
}].email['Email is invalid'],
}],
}];
export const resetPasswordSchema = object[{
params: object[{
resetToken: string[],
}],
body: object[{
password: string[{
required_error: 'Password is required',
}].min[8, 'Password must be more than 8 characters'],
passwordConfirm: string[{
required_error: 'Please confirm your password',
}],
}].refine[[data] => data.password === data.passwordConfirm, {
message: 'Passwords do not match',
path: ['passwordConfirm'],
}],
}];
export type ForgotPasswordInput = TypeOf['body'];
export type ResetPasswordInput = TypeOf;
Tệp
{
"scripts": {
"start": "ts-node-dev --respawn --transpile-only --exit-child src/app.ts",
"db:migrate": "npx prisma migrate dev --name user-entity --create-only && yarn prisma generate",
"db:push": "npx prisma db push",
"build": "tsc . -p"
},
}
7 bây giờ trông như thế nàysrc/lược đồ/người dùng. lược đồ. ts
import { object, string, TypeOf, z } from 'zod';
enum RoleEnumType {
ADMIN = 'admin',
USER = 'user',
}
export const registerUserSchema = object[{
body: object[{
name: string[{
required_error: 'Name is required',
}],
email: string[{
required_error: 'Email address is required',
}].email['Invalid email address'],
password: string[{
required_error: 'Password is required',
}]
.min[8, 'Password must be more than 8 characters']
.max[32, 'Password must be less than 32 characters'],
passwordConfirm: string[{
required_error: 'Please confirm your password',
}],
role: z.optional[z.nativeEnum[RoleEnumType]],
}].refine[[data] => data.password === data.passwordConfirm, {
path: ['passwordConfirm'],
message: 'Passwords do not match',
}],
}];
export const loginUserSchema = object[{
body: object[{
email: string[{
required_error: 'Email address is required',
}].email['Invalid email address'],
password: string[{
required_error: 'Password is required',
}].min[8, 'Invalid email or password'],
}],
}];
export const verifyEmailSchema = object[{
params: object[{
verificationCode: string[],
}],
}];
export const updateUserSchema = object[{
body: object[{
name: string[{}],
email: string[{}].email['Invalid email address'],
password: string[{}]
.min[8, 'Password must be more than 8 characters']
.max[32, 'Password must be less than 32 characters'],
passwordConfirm: string[{}],
role: z.optional[z.nativeEnum[RoleEnumType]],
}]
.partial[]
.refine[[data] => data.password === data.passwordConfirm, {
path: ['passwordConfirm'],
message: 'Passwords do not match',
}],
}];
export const forgotPasswordSchema = object[{
body: object[{
email: string[{
required_error: 'Email is required',
}].email['Email is invalid'],
}],
}];
export const resetPasswordSchema = object[{
params: object[{
resetToken: string[],
}],
body: object[{
password: string[{
required_error: 'Password is required',
}].min[8, 'Password must be more than 8 characters'],
passwordConfirm: string[{
required_error: 'Please confirm your password',
}],
}].refine[[data] => data.password === data.passwordConfirm, {
message: 'Passwords do not match',
path: ['passwordConfirm'],
}],
}];
export type RegisterUserInput = Omit<
TypeOf['body'],
'passwordConfirm'
>;
export type LoginUserInput = TypeOf['body'];
export type VerifyEmailInput = TypeOf['params'];
export type UpdateUserInput = TypeOf['body'];
export type ForgotPasswordInput = TypeOf['body'];
export type ResetPasswordInput = TypeOf;
Các dịch vụ truy vấn và thay đổi cơ sở dữ liệu PostgreSQL
Ở đây tôi đã xác định một số dịch vụ sẽ được bộ điều khiển gọi để truy vấn và thay đổi trạng thái cơ sở dữ liệu
trong nút. js, bạn nên tách logic nghiệp vụ và ứng dụng
Hầu hết logic nghiệp vụ nên được triển khai trong các mô hình hoặc dịch vụ và bạn sẽ kết thúc với các mô hình hoặc dịch vụ béo và bộ điều khiển mỏng
src/dịch vụ/người dùng. Dịch vụ. ts
import { PrismaClient, Prisma, User } from '@prisma/client';
import { omit } from 'lodash';
import config from 'config';
import redisClient from '../utils/connectRedis';
import { signJwt } from '../utils/jwt';
export const excludedFields = [
"password",
"verified",
"verificationCode",
"passwordResetAt",
"passwordResetToken",
];
const prisma = new PrismaClient[];
export const createUser = async [input: Prisma.UserCreateInput] => {
return [await prisma.user.create[{
data: input,
}]] as User;
};
export const findUser = async [
where: Partial,
select?: Prisma.UserSelect
] => {
return [await prisma.user.findFirst[{
where,
select,
}]] as User;
};
export const findUniqueUser = async [
where: Prisma.UserWhereUniqueInput,
select?: Prisma.UserSelect
] => {
return [await prisma.user.findUnique[{
where,
select,
}]] as User;
};
export const updateUser = async [
where: Partial,
data: Prisma.UserUpdateInput,
select?: Prisma.UserSelect
] => {
return [await prisma.user.update[{ where, data, select }]] as User;
};
export const signTokens = async [user: Prisma.UserCreateInput] => {
// 1. Create Session
redisClient.set[`${user.id}`, JSON.stringify[omit[user, excludedFields]], {
EX: config.get['redisCacheExpiresIn'] * 60,
}];
// 2. Create Access and Refresh tokens
const access_token = signJwt[{ sub: user.id }, 'accessTokenPrivateKey', {
expiresIn: `${config.get['accessTokenExpiresIn']}m`,
}];
const refresh_token = signJwt[{ sub: user.id }, 'refreshTokenPrivateKey', {
expiresIn: `${config.get['refreshTokenExpiresIn']}m`,
}];
return { access_token, refresh_token };
};
Tạo một lớp tiện ích để gửi email
Dưới đây là lớp tiện ích để gửi mã thông báo đặt lại mật khẩu đến địa chỉ email của người dùng
Vui lòng đọc API CRUD với Node. js và PostgreSQL. Gửi email HTML để được giải thích chi tiết
import nodemailer from 'nodemailer';
import config from 'config';
import pug from 'pug';
import { convert } from 'html-to-text';
import { Prisma } from '@prisma/client';
const smtp = config.get['smtp'];
export default class Email {
#firstName: string;
#to: string;
#from: string;
constructor[private user: Prisma.UserCreateInput, private url: string] {
this.#firstName = user.name.split[' '][0];
this.#to = user.email;
this.#from = `Codevo `;
}
private newTransport[] {
// if [process.env.NODE_ENV === 'production'] {
// }
return nodemailer.createTransport[{
...smtp,
auth: {
user: smtp.user,
pass: smtp.pass,
},
}];
}
private async send[template: string, subject: string] {
// Generate HTML template based on the template string
const html = pug.renderFile[`${__dirname}/../views/${template}.pug`, {
firstName: this.#firstName,
subject,
url: this.url,
}];
// Create mailOptions
const mailOptions = {
from: this.#from,
to: this.#to,
subject,
text: convert[html],
html,
};
// Send email
const info = await this.newTransport[].sendMail[mailOptions];
console.log[nodemailer.getTestMessageUrl[info]];
}
async sendVerificationCode[] {
await this.send['verificationCode', 'Your account verification code'];
}
async sendPasswordResetToken[] {
await this.send[
'resetPassword',
'Your password reset token [valid for only 10 minutes]'
];
}
}
Tạo bộ điều khiển
Bây giờ là lúc xác định bộ điều khiển sẽ gửi mã thông báo đặt lại mật khẩu và đặt lại mật khẩu
Quên mật khẩu điều khiển
Bộ điều khiển quên mật khẩu chịu trách nhiệm xác thực email của người dùng, tạo mã thông báo đặt lại mật khẩu và gửi mã thông báo đặt lại mật khẩu đến địa chỉ email của người dùng
src/bộ điều khiển/auth. bộ điều khiển. ts
export const forgotPasswordHandler = async [
req: Request<
Record,
Record,
ForgotPasswordInput
>,
res: Response,
next: NextFunction
] => {
try {
// Get the user from the collection
const user = await findUser[{ email: req.body.email.toLowerCase[] }];
const message =
'You will receive a reset email if user with that email exist';
if [!user] {
return res.status[200].json[{
status: 'success',
message,
}];
}
if [!user.verified] {
return res.status[403].json[{
status: 'fail',
message: 'Account not verified',
}];
}
if [user.provider] {
return res.status[403].json[{
status: 'fail',
message:
'We found your account. It looks like you registered with a social auth account. Try signing in with social auth.',
}];
}
const resetToken = crypto.randomBytes[32].toString['hex'];
const passwordResetToken = crypto
.createHash['sha256']
.update[resetToken]
.digest['hex'];
await updateUser[
{ id: user.id },
{
passwordResetToken,
passwordResetAt: new Date[Date.now[] + 10 * 60 * 1000],
},
{ email: true }
];
try {
const url = `${config.get['origin']}/resetPassword/${resetToken}`;
await new Email[user, url].sendPasswordResetToken[];
res.status[200].json[{
status: 'success',
message,
}];
} catch [err: any] {
await updateUser[
{ id: user.id },
{ passwordResetToken: null, passwordResetAt: null },
{}
];
return res.status[500].json[{
status: 'error',
message: 'There was an error sending email',
}];
}
} catch [err: any] {
next[err];
}
};
Dưới đây là một bản tóm tắt về những gì đã xảy ra trong
{
"scripts": {
"start": "ts-node-dev --respawn --transpile-only --exit-child src/app.ts",
"db:migrate": "npx prisma migrate dev --name user-entity --create-only && yarn prisma generate",
"db:push": "npx prisma db push",
"build": "tsc . -p"
},
}
9- Lần đầu tiên tôi truy xuất email từ nội dung yêu cầu và gọi dịch vụ
0 để kiểm tra xem người dùng có email đó có tồn tại trong cơ sở dữ liệu khôngyarn db:migrate && yarn db:push
- Tiếp theo, tôi đã tạo mã thông báo đặt lại mật khẩu bằng nút. js Crypto và băm nó
- Cuối cùng, tôi đã gửi mã thông báo đặt lại chưa băm tới email của người dùng và lưu trữ mã đã băm trong cơ sở dữ liệu PostgreSQL
Tạo bộ điều khiển đặt lại mật khẩu
Bây giờ, hãy xác định
yarn db:migrate && yarn db:push
1 sẽ xác thực mã thông báo đặt lại và cập nhật mật khẩu của người dùng trong cơ sở dữ liệu PostgreSQLsrc/bộ điều khiển/auth. bộ điều khiển. ts
export const resetPasswordHandler = async [
req: Request<
ResetPasswordInput['params'],
Record,
ResetPasswordInput['body']
>,
res: Response,
next: NextFunction
] => {
try {
// Get the user from the collection
const passwordResetToken = crypto
.createHash['sha256']
.update[req.params.resetToken]
.digest['hex'];
const user = await findUser[{
passwordResetToken,
passwordResetAt: {
gt: new Date[],
},
}];
if [!user] {
return res.status[403].json[{
status: 'fail',
message: 'Invalid token or token has expired',
}];
}
const hashedPassword = await bcrypt.hash[req.body.password, 12];
// Change password data
await updateUser[
{
id: user.id,
},
{
password: hashedPassword,
passwordResetToken: null,
passwordResetAt: null,
},
{ email: true }
];
logout[res];
res.status[200].json[{
status: 'success',
message: 'Password data updated successfully',
}];
} catch [err: any] {
next[err];
}
};
Đây là bảng phân tích về những gì tôi đã làm trong đoạn mã trên
- Đầu tiên, tôi trích xuất mã thông báo đặt lại từ thông số yêu cầu và băm nó
- Tiếp theo, tôi đã gọi dịch vụ
0 để kiểm tra xem người dùng có mã thông báo đặt lại đó có tồn tại trong cơ sở dữ liệu khôngyarn db:migrate && yarn db:push
- Tiếp theo, tôi băm mật khẩu mới bằng BcryptJs và gọi dịch vụ
3 để cập nhật mật khẩu của người dùng trong cơ sở dữ liệuyarn db:migrate && yarn db:push
- Ngoài ra, tôi đặt
3 và{ "scripts": { "start": "ts-node-dev --respawn --transpile-only --exit-child src/app.ts", "db:migrate": "npx prisma migrate dev --name user-entity --create-only && yarn prisma generate", "db:push": "npx prisma db push", "build": "tsc . -p" }, }
4 thành null để ngăn người dùng đặt lại mật khẩu hai lần{ "scripts": { "start": "ts-node-dev --respawn --transpile-only --exit-child src/app.ts", "db:migrate": "npx prisma migrate dev --name user-entity --create-only && yarn prisma generate", "db:push": "npx prisma db push", "build": "tsc . -p" }, }
- Cuối cùng, tôi đã đăng xuất người dùng khỏi ứng dụng bằng cách gửi cookie đã hết hạn
Thêm các tuyến đường
Tiếp theo, thêm các route sau vào tệp
yarn db:migrate && yarn db:push
6. Ngoài ra, hãy nhớ gọi phần mềm trung gian xác thực lược đồ trước bộ điều khiểnsrc/tuyến/auth. tuyến đường. ts
router.post[
'/forgotpassword',
validate[forgotPasswordSchema],
forgotPasswordHandler
];
router.patch[
'/resetpassword/:resetToken',
validate[resetPasswordSchema],
resetPasswordHandler
];
Tệp
yarn db:migrate && yarn db:push
6 bây giờ trông như thế nàysrc/tuyến/auth. tuyến đường. ts
{
"scripts": {
"start": "ts-node-dev --respawn --transpile-only --exit-child src/app.ts",
"db:migrate": "npx prisma migrate dev --name user-entity --create-only && yarn prisma generate",
"db:push": "npx prisma db push",
"build": "tsc . -p"
},
}
0Phần kết luận
Trong bài viết này, bạn đã học cách triển khai các chức năng quên/đặt lại mật khẩu với Node. js, Prisma, PostgreSQL, Nodemailer, Redis, Pug và Docker-compose