2.3 注册

89 阅读4分钟

注册页面

修改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代码,用的原生表单提交,对每个字段的长度做了些限制。

缺点是密码将会通过明文传输到后台,这种情况在生产中是很危险的,那应该怎么办呢?

  1. 使用JavaScript对密码进行加密,常用的方式有md5加密、crypto加密等,能起的作用只是聊胜于无,思考下为什么。
  2. 网站开启HTTPS。这是治本的方法。当然,后端存储时仍需进一步加密。

所以,本文就不处理了。

这时在浏览器中输入http://localhost:8000/signup,能看到页面如下:

image.png

注册接口

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();
  }
}