注册页面
修改src/user/user.controller.ts,把其它代码先删了。
import { Controller, Get } from "oak_nest";
import { Render } from "../tools/ejs.ts";
import { UserService } from "./user.service.ts";
@Controller("/")
export class UserController {
constructor(private readonly userService: UserService) {}
@Get("/signup")
signup(@Render() render: Render) {
return render("signup", {});
}
@Get("user")
async getAllUsers() {
return await this.userService.getAll();
}
@Get("user/:id")
async getUserById(@Params("id") id: string) {
const user = await this.userService.getUserById(id);
if (user) {
return user;
} else {
throw new NotFoundException("user not found");
}
}
@Delete("user/:id")
async deleteUser(@Params("id") id: string) {
return await this.userService.removeUser(id);
}
}
新建views/signup.ejs:
<%- include('header') %>
<div class="ui grid">
<div class="four wide column"></div>
<div class="eight wide column">
<form class="ui form segment" method="post" enctype="multipart/form-data" action="/signup">
<div class="field required">
<label>用户名</label>
<input placeholder="用户名" type="text" name="name" minlength="1" maxlength="10" required>
</div>
<div class="field required">
<label>密码</label>
<input placeholder="密码" type="password" name="password" minlength="6" maxlength="20" required>
</div>
<div class="field required">
<label>重复密码</label>
<input placeholder="重复密码" type="password" name="repassword" minlength="6" maxlength="20" required>
</div>
<div class="field required">
<label>性别</label>
<select class="ui compact selection dropdown" name="gender">
<option value="m">男</option>
<option value="f">女</option>
<option value="x">保密</option>
</select>
</div>
<div class="field required">
<label>头像</label>
<input type="file" name="avatar" required>
</div>
<div class="field required">
<label>个人简介</label>
<textarea name="bio" rows="5" required minlength="1" maxlength="30"></textarea>
</div>
<input type="submit" class="ui button fluid" value="注册">
</form>
</div>
</div>
<%- include('footer') %>
我们这里没有用到JavaScript代码,用的原生表单提交,对每个字段的长度做了些限制。
缺点是密码将会通过明文传输到后台,这种情况在生产中是很危险的,那应该怎么办呢?
- 使用JavaScript对密码进行加密,常用的方式有md5加密、crypto加密等,能起的作用只是聊胜于无,思考下为什么。
- 网站开启HTTPS。这是治本的方法。当然,后端存储时仍需进一步加密。
所以,本文就不处理了。
这时在浏览器中输入http://localhost:8000/signup,能看到页面如下:
注册接口
user.schema.ts
修改src/user/user.schema.ts,把我们上面注册的几个字段都加上:
import { Prop } from "../schema.ts";
export enum Gender {
X = "x", // 保密
Man = "m",
Female = "f",
}
export class User {
@Prop()
name: string;
@Prop()
password: string;
@Prop({
required: true,
})
avatar: string;
@Prop()
gender: Gender;
@Prop()
bio: string;
}
user.dto.ts
修改src/user/user.dto.ts,把参数的校验规则都加上,与页面的规则保持一致。
import {
IsEnum,
IsNumber,
IsOptional,
IsString,
MaxLength,
Min,
MinLength,
} from "deno_class_validator";
import { Gender } from "./user.schema.ts";
export class CreateUserDto {
@IsString()
@MinLength(1)
@MaxLength(10)
name: string;
@IsString()
@MinLength(6)
@MaxLength(20)
password: string;
@IsString()
@MinLength(6)
@MaxLength(20)
repassword: string;
@IsEnum(Gender)
gender: Gender;
@IsString()
@MinLength(1)
@MaxLength(30)
bio: string;
}
注意,这里我并没有加中文错误提示信息,虽然这些信息也会提示到页面上,为什么呢?
因为我们的页面Form表单里也加了同样的规则,如果不通过这些规则,正常状态下是不会请求后台接口的。如果有异常的情况,说明有人使用非常规手段(比如使用工具或代码调用接口),这时提示信息正不正规并不重要。
这也正是我们为什么要在后端同样添加校验的原因。永远不要相信前端传递的参数。这是后端开发的一条非常重要的原则。
user.controller.ts
修改src/user/user.controller.ts,新增一个POST的signup方法,其它方法不变:
import { BadRequestException } from "oak_exception";
import {
Controller,
Get,
Post,
Res,
Response,
REDIRECT_BACK,
FormData,
validateParams,
} from "oak_nest";
import type { FormDataFormattedBody} from "oak_nest";
import { Render } from "../tools/ejs.ts";
import { Logger } from "../tools/log.ts";
import { CreateUserDto } from "./user.dto.ts";
import { UserService } from "./user.service.ts";
@Controller("/")
export class UserController {
constructor(
private readonly userService: UserService,
private readonly logger: Logger,
) {}
@Post("signup")
async signup(
@FormData() params: FormDataFormattedBody<CreateUserDto>,
@Res() res: Response,
) {
try {
const files = params.files;
// 校验参数
if (
!files || files.length === 0 || !files[0].originalName ||
!files[0].filename
) {
throw new BadRequestException("必须上传头像");
}
const form = params.fields;
await validateParams(CreateUserDto, form);
if (form.password !== form.repassword) {
throw new BadRequestException("两次输入密码不一致");
}
this.logger.debug("注册参数校验成功");
const filename = files[0].filename.split("/").pop()!; // 随机文件名,直接使用就行了,也可以使用md5进行加密,这样同样的文件只会有一个
// 保存数据
const id = await this.userService.addUser({
name: form.name,
password: form.password,
gender: form.gender,
bio: form.bio,
avatar: filename,
});
this.logger.info(`用户【${id}】注册成功`);
// 上传图片
await this.uploadImg(files[0].filename, filename);
this.logger.debug(`上传图片成功`);
//TODO 提示注册成功
res.redirect("/posts");
} catch (e) {
//TODO 提示错误
this.logger.error(e.message);
res.redirect(REDIRECT_BACK);
}
}
private async uploadImg(tmpPath: string, filename: string) {
const imgPath = "public/img";
await Deno.mkdir(imgPath).catch((_err) => null);
await Deno.copyFile(tmpPath, imgPath + "/" + filename);
}
}
从上面代码看得出来,这个接口分为3个阶段,校验参数、创建用户、上传图片。
如果失败,这里redirect时用到REDIRECT_BACK,是oak框架提供的上一个页面,也就是跳转回当前注册页(/signup)。
如果成功,将跳转到文章页(/posts)。
.gitignore
新增public/img,它没有必要、也不应该上传到git仓库。
验证
在http://localhost:8000/signup注册一条用户信息,可以在控制台和日志文件中看到:
2022-06-16 19:15:29 [DEBUG] - [UserController] 注册参数校验成功
2022-06-16 19:15:29 [INFO] - [UserController] 用户【Xep5LG8rwNX5f1euUn_ak】注册成功
2022-06-16 19:15:29 [DEBUG] - [UserController] 上传图片成功
同时,public/img目录下也会有一张新的图片。
再看http://localhost:8000/user/Xep5LG8rwNX5f1euUn_ak,会有对应的信息。
作业
我们将上传的图片存储到public/img目录下。为了方便,我直接使用了oak框架生成的随机文件名,这样缺点是同一张图片上传上来也会生成2张图片。
如果要避免这种情况,可以选择使用md5对整个图片进行计算,有兴趣的同学自行实现。
import { encode, Hash } from "https://deno.land/x/checksum@1.4.0/mod.ts";
export function md5(buffer: Uint8Array): string;
export function md5(str: string): string;
export function md5(buffer: Uint8Array | string): string {
if (typeof buffer === "string") {
return new Hash("md5").digest(encode(buffer)).hex();
} else {
return new Hash("md5").digest(buffer).hex();
}
}