Đặt lại mật khẩu NodeJS

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

  1. Nút API. js, TypeScript, Prisma, PostgreSQL. Thiết lập dự án
  2. Nút. js + Prisma + PostgreSQL. Truy cập & làm mới mã thông báo
  3. API CRUD với nút. js và PostgreSQL. Gửi email HTML
  4. API với nút. js, Prisma và PostgreSQL. Quên/Đặt lại mật khẩu
Đặt lại mật khẩu NodeJS

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

node.js, prisma, postgresql forgot password page

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

node.js, prisma, postgresql forgot password success message

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

node.js, prisma, postgresql reset password email

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ủ

node.js, prisma, postgresql reset password page

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

node.js, prisma, postgresql reset password success message

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

  • 
    {
    "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"
      },
    }
    
    
    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

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 lai

Chạ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"
  },
}

7

src/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ày

src/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<{
  host: string;
  port: number;
  user: string;
  pass: string;
}>('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ụ
    
    yarn db:migrate && yarn db:push
    
    
    0 để kiểm tra xem người dùng có email đó có tồn tại trong cơ sở dữ liệu không
  • 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 PostgreSQL

src/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ụ
    
    yarn db:migrate && yarn db:push
    
    
    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ông
  • Tiếp theo, tôi băm mật khẩu mới bằng BcryptJs và gọi dịch vụ
    
    yarn db:migrate && yarn db:push
    
    
    3 để cập nhật mật khẩu của người dùng trong cơ sở dữ liệu
  • Ngoài ra, tôi đặ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à
    
    {
    "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
  • 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ển

src/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ày

src/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"
  },
}

0

Phầ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

Làm cách nào để đặt lại mật khẩu trong nút js?

Hãy làm theo các bước sau để tạo email quên hoặc đặt lại mật khẩu trong nút js express với MySQL. .
Bước 1 – Cài đặt Node Express App JS
Bước 2 – Cài đặt các Mô-đun Node js cần thiết
Bước 3 – Kết nối ứng dụng Node Express JS với DB
Bước 4 – Nhập các Mô-đun đã cài đặt trong ứng dụng. js
Bước 5 – Tạo Lộ trình Quên và Đặt lại Mật khẩu

Làm cách nào để đặt lại mật khẩu trong HTML?

Luồng đặt lại mật khẩu như sau. .
Người dùng yêu cầu email đặt lại mật khẩu. Mặt tiền người dùng gửi cho người dùng một email. .
Người dùng nhấp vào liên kết. Liên kết có mã thông báo đặc biệt và uuid trong URL
Người dùng gửi mật khẩu mới của họ. Người dùng gửi mật khẩu mới của họ với biểu mẫu đặt lại mật khẩu trên trang

Làm cách nào để triển khai quên mật khẩu trong NestJs?

ứng dụng js sử dụng NestJs. Đó là dòng chảy chung. Người dùng nhập email của mình ở dạng "quên mật khẩu" và gửi yêu cầu. Máy chủ tạo mã thông báo jwt với ID của người dùng làm tải trọng, sau đó gửi email có mã thông báo làm liên kết để đặt lại mật khẩu (ví dụ. ĐƯỢC. thí dụ. com/reset/generated_jwt_token )

Làm cách nào để gửi email từ nút js?

Cách gửi email trong Node. .
Cài đặt Nodemailer thông qua lệnh sau. npm cài đặt nodemailer --save hoặc sợi thêm nodemailer
Sau khi hoàn thành, hãy đưa nó vào ứng dụng web của bạn
Tạo trình vận chuyển Nodemailer
Đặt tùy chọn tin nhắn Nodemailer
Gửi thư với sendMail()