处理用户注册

In the previous posts, the user sample data is initialized in a service which is observing an OnMoudleInit event.

在以前的文章中,用户示例数据是在观察OnMoudleInit事件的服务中初始化的。

In this post we will add an endpoint to handle user registration request, including:

在本文中,我们将添加一个端点来处理用户注册请求,包括:

  • Add an endpoint /register to handling user registration progress

    添加端点/注册以处理用户注册进度

  • Hashing password with bcrypt

    使用bcrypt哈希密码
  • Sending notifications via SendGrid mail service

    通过SendGrid邮件服务发送通知

注册新用户(Registering a new user)

Generate a register controller.

生成一个寄存器控制器。

nest g controller user/register --flat

Fill the following content into the RegisterController.

将以下内容填充到RegisterController

// user/register.controller.ts@Controller('register')
export class RegisterController {
constructor(private userService: UserService) { } @Post()
register(
@Body() registerDto: RegisterDto,
@Res() res: Response): Observable<Response> {
const username = registerDto.username; return this.userService.existsByUsername(username).pipe(
flatMap(exists => {
if (exists) {
throw new ConflictException(`username:${username} is existed`)
}
else {
const email = registerDto.email;
return this.userService.existsByEmail(email).pipe(
flatMap(exists => {
if (exists) {
throw new ConflictException(`email:${email} is existed`)
}
else {
return this.userService.register(registerDto).pipe(
map(user =>
res.location('/users/' + user.id)
.status(201)
.send()
)
);
}
})
);
}
})
);
}
}

In the above codes, we will check user existence by username and email respectively, then save the user data into the MongoDB database.

在上面的代码中,我们将分别通过用户名和电子邮件检查用户是否存在,然后将用户数据保存到MongoDB数据库中。

In the UserService, add the missing methods.

UserService ,添加缺少的方法。

@Injectable()
export class UserService {

existsByUsername(username: string): Observable<boolean> {
return from(this.userModel.exists({ username }));
} existsByEmail(email: string): Observable<boolean> {
return from(this.userModel.exists({ email }));
} register(data: RegisterDto): Observable<User> { const created = this.userModel.create({
...data,
roles: [RoleType.USER]
}); return from(created);
}
//...
}

Create a DTO class to represent the user registration request data. Generate the DTO skeleton firstly.

创建一个DTO类来表示用户注册请求数据。 首先生成DTO骨架。

nest g class user/register.dto --flat

And fill the following content.

并填写以下内容。

import { IsEmail, IsNotEmpty, MaxLength, MinLength } from "class-validator";export class RegisterDto {
@IsNotEmpty()
readonly username: string; @IsNotEmpty()
@IsEmail()
readonly email: string; @IsNotEmpty()
@MinLength(8, { message: " The min length of password is 8 " })
@MaxLength(20, { message: " The password can't accept more than 20 characters " })
// @Matches(/^(?=.*\d)(?=.*[a-z])(?=.*[A-Z])[0-9a-zA-Z]{8,20}$/,
// { message: " A password at least contains one numeric digit, one supercase char and one lowercase char" }
// )
readonly password: string; @IsNotEmpty()
readonly firstName?: string; @IsNotEmpty()
readonly lastName?: string;
}

In the above codes, the @IsNotEmpty(),@IsEmail, @MinLength(), @MaxLength(), @Matches() are from class-validator. If you have some experience of Java EE/Jakarta EE Bean Validation or Hibernate Validators, these annotations are easy to understand.

在上面的代码中, @IsNotEmpty()@IsEmail@MinLength()@MaxLength()@Matches()来自class-validator 。 如果您有Java EE / Jakarta EE Bean验证或Hibernate Validators的经验,则这些注释很容易理解。

  • @IsNotEmpty() to check if the given value is empty

    @IsNotEmpty()检查给定值是否为空

  • @IsEmail to validate if the input string is an valid email format

    @IsEmail验证输入的字符串是否为有效的电子邮件格式

  • @MinLength() and @MaxLength()are to limit the length range of the input value

    @MinLength()@MaxLength()用于限制输入值的长度范围

  • @Matches() is flexible for custom RegExp matches.

    @Matches()对于自定义RegExp匹配非常灵活。

More info about the usage of class-validator, check the details of project typestack/class-validator.

有关使用class-validator的更多信息,请检查项目typestack / class-validator的详细信息

In the previous posts, we have applied a global ValidationPipe in bootstrap function in the main.ts entry file. When registering with invalid data,it will return a 404 error.

在之前的文章中,我们在main.ts条目文件的bootstrap函数中应用了全局ValidationPipe 。 使用无效数据注册时,将返回404错误。

$ curl http://localhost:3000/register -d "{}" {"statusCode":400,"message":["username should not be empty","email must be an em ail","email should not be empty"," The password can't accept more than 20 charac ters "," The min length of password is 8 ","password should not be empty","first Name should not be empty","lastName should not be empty"],"error":"Bad Request"}

Add a test for the RegisterController.

RegisterController添加一个测试。

describe('Register Controller', () => {
let controller: RegisterController;
let service: UserService; beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
controllers: [RegisterController],
providers: [
{
provide: UserService,
useValue: {
register: jest.fn(),
existsByUsername: jest.fn(),
existsByEmail: jest.fn()
},
},
]
}).compile(); controller = module.get<RegisterController>(RegisterController);
service = module.get<UserService>(UserService);
}); it('should be defined', () => {
expect(controller).toBeDefined();
}); describe('register', () => {
it('should throw ConflictException when username is existed ', async () => {
const existsByUsernameSpy = jest.spyOn(service, 'existsByUsername').mockReturnValue(of(true));
const existsByEmailSpy = jest.spyOn(service, 'existsByEmail').mockReturnValue(of(true));
const saveSpy = jest.spyOn(service, 'register').mockReturnValue(of({} as User)); const responseMock = {
location: jest.fn().mockReturnThis(),
json: jest.fn().mockReturnThis(),
send: jest.fn().mockReturnThis()
} as any;
try {
await controller.register({ username: 'hantsy' } as RegisterDto, responseMock).toPromise();
} catch (e) {
expect(e).toBeDefined();
expect(existsByUsernameSpy).toBeCalledWith('hantsy');
expect(existsByEmailSpy).toBeCalledTimes(0);
expect(saveSpy).toBeCalledTimes(0)
}
}); it('should throw ConflictException when email is existed ', async () => {
const existsByUsernameSpy = jest.spyOn(service, 'existsByUsername').mockReturnValue(of(false));
const existsByEmailSpy = jest.spyOn(service, 'existsByEmail').mockReturnValue(of(true));
const saveSpy = jest.spyOn(service, 'register').mockReturnValue(of({} as User)); const responseMock = {
location: jest.fn().mockReturnThis(),
json: jest.fn().mockReturnThis(),
send: jest.fn().mockReturnThis()
} as any;
try {
await controller.register({ username: 'hantsy', email: 'hantsy@example.com' } as RegisterDto, responseMock).toPromise();
} catch (e) {
expect(e).toBeDefined();
expect(existsByUsernameSpy).toBeCalledWith('hantsy');
expect(existsByEmailSpy).toBeCalledWith('hantsy@example.com');
expect(saveSpy).toBeCalledTimes(0)
}
}); it('should save when username and email are available ', async () => {
const existsByUsernameSpy = jest.spyOn(service, 'existsByUsername').mockReturnValue(of(false));
const existsByEmailSpy = jest.spyOn(service, 'existsByEmail').mockReturnValue(of(false));
const saveSpy = jest.spyOn(service, 'register').mockReturnValue(of({ _id: '123' } as User)); const responseMock = {
location: jest.fn().mockReturnThis(),
status: jest.fn().mockReturnThis(),
send: jest.fn().mockReturnThis()
} as any; const locationSpy = jest.spyOn(responseMock, 'location');
const statusSpy = jest.spyOn(responseMock, 'status');
const sendSpy = jest.spyOn(responseMock, 'send'); await controller.register({ username: 'hantsy', email: 'hantsy@example.com' } as RegisterDto, responseMock).toPromise(); expect(existsByUsernameSpy).toBeCalledWith('hantsy');
expect(existsByEmailSpy).toBeCalledWith('hantsy@example.com');
expect(saveSpy).toBeCalledTimes(1);
expect(locationSpy).toBeCalled();
expect(statusSpy).toBeCalled();
expect(sendSpy).toBeCalled(); });
});
});

In the above testing codes, we go through all conditions and make sure all code blocks in the RegisterController are hit.

在上面的测试代码中,我们仔细研究所有条件,并确保RegisterController中的所有代码块均被选中。

Correspondingly add tests for the newly added methods in UserService . Here I skip the testing codes here, please check the source code yourself.

相应地在UserService为新添加的方法添加测试。 我在这里跳过了测试代码,请您自己检查源代码

哈希密码 (Hashing password)

In the former posts, we used plain text to store the password field in user document. In a real world application, we should choose a hash algorithm to encode the plain password for security consideration.

在以前的帖子中,我们使用纯文本将密码字段存储在用户文档中。 在现实世界的应用程序中,出于安全考虑,我们应该选择一种哈希算法来对普通密码进行编码。

Bcrypt is very popular for hashing password.

Bcrypt非常流行用于哈希密码。

Install bcypt firstly.

首先安装bcypt

npm install --save bcrypt

When saving a new user, hashing the password then save it. Add a pre save hook in the User model.

保存新用户时,请对密码进行哈希处理然后保存。 在User模型中添加一个预保存钩子。

async function preSaveHook(next) {  // Only run this function if password was modified
if (!this.isModified('password')) return next(); // Hash the password
const password = await hash(this.password, 12);
this.set('password', password); next();
}UserSchema.pre<User>('save', preSaveHook);

The preSave hook will be invoked before the new user data is being persisted into the MongoDB.

在将新的用户数据持久保存到MongoDB中之前,将调用preSave挂钩。

When a user is trying to login via username and password pair, it should check if password is matched to the one in the database.

当用户尝试通过用户名和密码对登录时,应检查密码是否与数据库中的密码匹配。

Add a method to the User model.

将方法添加到User模型。

function comparePasswordMethod(password: string): Observable<boolean> {
return from(compare(password, this.password));
}UserSchema.methods.comparePassword = comparePasswordMethod;

Change the validateUser method of the AuthService, check the password if matched there.

更改AuthServicevalidateUser方法,检查密码是否匹配。

flatMap((user) => {
const { _id, password, username, email, roles } = user;
return user.comparePassword(pass).pipe(map(m => {
if (m) {
return { id: _id, username, email, roles } as UserPrincipal;
}else {
throw new UnauthorizedException('username or password is not matched')
}
}))

It is a little difficult to test the hooks of the User model, to simplify the testing work, here I extract the hooks to standalone functions, and mock the calling context in the tests.

测试User模型的钩子有点困难,以简化测试工作,这里我将钩子提取到独立函数中,并在测试中模拟调用上下文。

// see: https://stackoverflow.com/questions/58701700/how-do-i-test-if-statement-inside-my-mongoose-pre-save-hook
describe('preSaveHook', () => {
test('should execute next middleware when password is not modified', async () => {
const nextMock = jest.fn();
const contextMock = {
isModified: jest.fn()
};
contextMock.isModified.mockReturnValueOnce(false);
await preSaveHook.call(contextMock, nextMock);
expect(contextMock.isModified).toBeCalledWith('password');
expect(nextMock).toBeCalledTimes(1);
}); test('should set password when password is modified', async () => {
const nextMock = jest.fn();
const contextMock = {
isModified: jest.fn(),
set: jest.fn(),
password: '123456'
};
contextMock.isModified.mockReturnValueOnce(true);
await preSaveHook.call(contextMock, nextMock);
expect(contextMock.isModified).toBeCalledWith('password');
expect(nextMock).toBeCalledTimes(1);
expect(contextMock.set).toBeCalledTimes(1);
});
});

Explore other tests for comparePasswordMethod etc in the user.mdoel.sepc.ts.

探索user.mdoel.sepc.ts中的comparePasswordMethod等其他测试。

Now run the application, have a look at the log in the console about the user initialization, as you see the password stored in the MongoDB is hashed.

现在运行该应用程序,查看控制台中有关用户初始化的日志,因为您看到存储在MongoDB中的密码是散列的。

(UserModule) is initialized...
[
{
roles: [ 'USER' ],
_id: 5f477055fb9a2b3fa4cb1c21,
username: 'hantsy',
password: '$2b$12$/spjKM3Vdf5vRJE9u2cHaulIAWzKMbNVSyHjMp9E9PifbSEHTQrJy',
email: 'hantsy@example.com',
createdAt: 2020-08-27T08:35:33.800Z,
updatedAt: 2020-08-27T08:35:33.800Z,
__v: 0
},
{
roles: [ 'ADMIN' ],
_id: 5f477055fb9a2b3fa4cb1c22,
username: 'admin',
password: '$2b$12$kFhASRJPkb/WD99J4uZrf.ZkkeKghpvf/6pgVGQArGiIgXu5aNMe.',
email: 'admin@example.com',
createdAt: 2020-08-27T08:35:33.801Z,
updatedAt: 2020-08-27T08:35:33.801Z,
__v: 0
}
]

注册欢迎通知 (Registration Welcome Notification)

Generally, in a real world application, a welcome email should be sent to the new registered user when the registration is completed successfully.

通常,在实际应用中,成功完成注册后,应向新注册用户发送欢迎电子邮件。

There are several modules can be used to send emails in NodeJS applications, for example, nodemailer etc. There are also some cloud service for emails, such as SendGrid. There is an existing Nestjs module to integrate SendGrid to Nestjs, check ntegral/nestjs-sendgrid project.

有多个模块可用于在nodemailer应用程序中发送电子邮件,例如nodemailer等。还有一些用于电子邮件的云服务,例如SendGrid 。 有一个现有的Nestjs模块,可将SendGrid集成到Nestjs,请检查ntegral / nestjs-sendgrid项目。

In this sample, we will not use the existing one, and create a new home-use module for this application.

在此示例中,我们将不使用现有模块,而是为此应用程序创建一个新的家用模块。

Install sendgrid npm package firstly.

首先安装sendgrid npm软件包。

npm i @sendgrid/mail

Generate a sendgrid module and a sendgrid service.

生成一个sendgrid模块和一个sendgrid服务。

nest g mo sendgrid
nest g s sendgrid

Add the following content into the SendgridService.

将以下内容添加到SendgridService

@Injectable()
export class SendgridService { constructor(@Inject(SENDGRID_MAIL) private mailService: MailService) { } send(data: MailDataRequired): Observable<any>{
//console.log(this.mailService)
return from(this.mailService.send(data, false))
}}

Create a provider to expose the MailService from @sendgrid/mail package.

创建一个提供程序以通过@sendgrid/mail包公开MailService

export const sendgridProviders = [
{
provide: SENDGRID_MAIL,
useFactory: (config: ConfigType<typeof sendgridConfig>): MailService =>
{
const mail = new MailService();
mail.setApiKey(config.apiKey);
mail.setTimeout(5000);
//mail.setTwilioEmailAuth(username, password)
return mail;
},
inject: [sendgridConfig.KEY],
}
];

Accordingly, add a config for sendgrid.

因此,为sendgrid添加配置。

//config/sendgrid.config.ts
export default registerAs('sendgrid', () => ({
apiKey: process.env.SENDGRID_API_KEY || 'SG.test',
}));

Signup SendGrid and generate an API Key for your applications to send emails.

注册SendGrid并为您的应用程序生成API密钥以发送电子邮件。

Declares sendgrid related config, provider and service in SendgridModule.

SendgridModule声明与sendgrid相关的配置,提供程序和服务。

@Module({
imports: [ConfigModule.forFeature(sendgridConfig)],
providers: [...sendgridProviders, SendgridService],
exports: [...sendgridProviders, SendgridService]
})
export class SendgridModule { }

Change the register function in the UserService.

UserService更改注册功能。

const msg = {
from: 'hantsy@gmail.com', // Use the email address or domain you verified above
subject: 'Welcome to Nestjs Sample',
templateId: "d-cc6080999ac04a558d632acf2d5d0b7a",
personalizations: [
{
to: data.email,
dynamicTemplateData: { name: data.firstName + ' ' + data.lastName },
}
] };
return this.sendgridService.send(msg).pipe(
catchError(err=>of(`sending email failed:${err}`)),
tap(data => console.log(data)),
flatMap(data => from(created)),
);

The templateId is the id of the templates managed by SendGrid. SendGrid has great web UI for you to compose and manage email templates.

templateId是由SendGrid管理的模板的ID。 SendGrid具有出色的Web UI,可让您撰写和管理电子邮件模板。

Ideally, a user registration progress should be split into two steps.

理想情况下,用户注册进度应分为两个步骤。

  • Validate the user input data from the registration form, and persist it into the MongoDB, then send a verification number to verify the registered phone number, email, etc. In this stage, the user account will be suspended to verify.

    验证注册表格中的用户输入数据,并将其保存到MongoDB中,然后发送验证码以验证注册的电话号码,电子邮件等。在此阶段,用户帐户将被暂停以进行验证。
  • The registered user receive the verification number or links in emails, provide it in the verification page or click the link in the email directly, and get verified. In this stage, the user account will be activated.

    注册用户会收到验证码或电子邮件中的链接,将其提供在验证页面中,或直接单击电子邮件中的链接,然后进行验证。 在此阶段,将激活用户帐户。

Grab the source codes from my github, switch to branch feat/user.

从我的github获取源代码,切换到branch feat / user

翻译自: https://medium.com/@hantsy/handling-user-registration-19d5bd3c5995

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值