React+Redux+Ant Design+TypeScript 电子商务实战-服务端应用 02 用户管理api

api 开发思路

  1. 在 models 文件夹管理 mongoose Schema 和 Model
  2. 在 controllers 文件夹管理 mongoose 操作
  3. 在 routes 文件夹管理 api
  4. app.js 配置 api,基础路径是 /api

api-用户管理-用户注册

api 设计

基本信息

Path: /signup

Method: POST

请求参数

Body

参数名称是否必传类型说明
nameString昵称
emailString邮箱地址(登录账户)
passwordString密码

用户注册只能注册普通身份(role = 0)的用户,管理员角色可以手动去数据库修改 role1

返回数据

字段名称说明
name昵称
email邮箱地址(登录账户)
role角色(普通用户 0; 管理员用户 1)
createdAt创建 Schema 时指定分配的自动管理的 createdAt 字段
updatedAt创建 Schema 时指定分配的自动管理的 updatedAt 字段
_idmongoose 默认分配的 id 字段,当访问 id 时就是获取的它
_vmongoose 默认分配的 versionKey 字段

创建 Schema 并实例化 Model

// models\user.js
const mongoose = require('mongoose')

// 创建 Schema
const userSchema = new mongoose.Schema(
  {
    // 昵称
    name: {
      type: String, // SchemaType(属性类型)
      trim: true, // 是否在保存前对此值调用 `.trim()`
      maxlength: [4, '昵称长度不能大于4'], // String 类型的内置验证器
      required: [true, '请填写昵称'] // 必填验证器
    },
    // 邮件地址
    email: {
      type: String,
      trim: true,
      unique: true, // 唯一索引
      required: [true, '请填写邮件地址']
    },
    // 加密后的密码(为了安全,不建议在数据库存储明文密码)
    hashed_password: {
      type: String,
      required: true
    },
    // 盐
    salt: String,
    // 角色
    role: {
      type: Number,
      default: 0, // 普通用户 0; 管理员用户 1
      enum: [[0, 1], '{VALUE} 不是有效的值']
    }
  },
  {
    timestamps: true // 向 Schema 分配 createdAt 和 updatedAt 属性并自动管理
  }
)


module.exports = mongoose.model('User', userSchema)

密码加密

虚拟属性

密码加密需要用到虚拟属性(Virtual),虚拟属性不会保存到 MongoDB,它可以通过设置 setter/getter 处理其它属性的数据。

定义一个 password 虚拟属性,当触发 setter 的时候,为用户生成 UUID 的盐(salt),使用 saltpassword 生成加密后的密码,存储到 hashed_password

UUID

UUID 全称 Universallly Unique Identifier,中文是通用唯一识别码

由 32 个 16进制 数字构成,按照 8-4-4-4-12 形式以 - 分隔的字符串,如:

xxxxxxxx-xxxx-Mxxx-Nxxx-xxxxxxxxxxxx
如:30385d15-0a88-42eb-bc43-2c000e9f778c

  • M 表示 UUID 版本,目前有5个版本:1,2,3,4,5
  • ``N只有四个值:8,9,a,b`

UUID 版本选择:

  • v1:通过当前时间戳 + 机器 Mac 地址生成
    • 可以反向解析主机 Mac 地址
  • v2:基于 v1 的基础上进行了优化,更安全
    • 但由于实现上有些问题,一般不会用到
  • v3:基于用于指定的命名空间 (namespace) + 指定字符串,通过 MD5 散列生成 UUID
    • 根据规范描述,这个版本的存在是为了向后兼容,用 v5 替代
  • v4:基于随机数生成
    • 这个版本的 UUID 是使用最多的
  • v5:和 v3 一样,只是把散列算法由 MD5 变成 SHA1
    • 使用场景:如果需要根据特定的值生成,而且在值不变的情况下生成的 UUID 不变

编写代码

// models\user.js
const mongoose = require('mongoose')
const crypto = require('crypto')
const { v1: uuidv1 } = require('uuid')

// 创建 Schema
const userSchema = new mongoose.Schema(
  {
    // 昵称
    name: {
      type: String, // Schema(属性类型)
      trim: true, // 是否在保存前对此值调用 `.trim()`
      maxlength: [4, '昵称长度不能大于4'], // String 类型的内置验证器
      required: [true, '请填写昵称'] // 必填验证器
    },
    // 邮件地址
    email: {
      type: String,
      trim: true,
      unique: true, // 唯一索引
      required: [true, '请填写邮件地址']
    },
    // 加密后的密码(为了安全,不建议在数据库存储明文密码)
    hashed_password: {
      type: String,
      required: true
    },
    // 盐
    salt: String,
    // 角色
    role: {
      type: Number,
      default: 0, // 普通用户 0; 管理员用户 1
      enum: [[0, 1], '{VALUE} 不是有效的值']
    }
  },
  {
    timestamps: true // 向 Schema 分配 createdAt 和 updatedAt 属性并自动管理
  }
)

// 添加实例方法
userSchema.method({
  // 密码加密
  encryptPassword(password) {
    if (!password) return ''
    // crypto 是 Node.js 内置的提供了加密功能的模块
    return crypto.createHmac('sha1', this.salt).update(password).digest('hex')
  }
})

// 定义虚拟属性 password
userSchema
  .virtual('password')
  .set(function (password) {
    this._password = password
    // 生成 salt
    this.salt = uuidv1()
    // 生成加密密码
    this.hashed_password = this.encryptPassword(password)
  })
  .get(function () {
    return this._password
  })

module.exports = mongoose.model('User', userSchema)

注册方法

// controllers\user.js
const User = require('../models/user')

// 注册
const signup = (req, res) => {
  // 创建用户
  const user = new User(req.body)
  // 插入数据库
  // save 可以接收一个 error-first 格式回调函数
  // 回调函数的第二个参数 `user` 是自身实例对象
  user.save((error, user) => {
    // 插入失败
    if (error) {
      // 响应
      return res.status(400).json(error)
    }
    // 删除不需要返回客户端的字段
    // undefined 的字段不会被返回
    user.salt = undefined
    user.hashed_password = undefined
    // 响应
    res.json(user)
  })
}

module.exports = {
  signup,
  signin
}

定义 api

// routes\user.js
const express = require('express')
const { signup } = require('../controllers/user')

const router = express.Router()

// 注册
router.post('/signup', signup)

module.exports = router

配置 api

// app.js
const express = require('express')
const cors = require('cors')
const morgan = require('morgan')
const bodyParse = require('body-parse')
const cookieParser = require('cookie-parser')
// api
const userRoutes = require('./routes/user')

// ...

// 接口配置
app.use('/api', userRoutes)

const APP_PORT = process.env.APP_PORT || 80

app.listen(APP_PORT, () => {
  console.log(`服务器启动成功,监听 ${APP_PORT} 端口`)
})

测试

使用 postman 测试接口:POST 请求 http://localhost/api/signup

请求参数:

{
    "name": "张三",
    "email": "zhangsan@163.com",
    "password": "123456"
}

正常返回:

{
    "role": 0,
    "_id": "....",
    "name": "张三",
    "email": "zhangsan@163.com",
    "createdAt": "....",
    "updatedAt": "....",
    "__v": 0
}

可以打开 Robo 3T 查看数据库存储的 salt 和 hashed_password。

Mongoose enum 枚举校验器

mongoose 文档中关于 enum 带错误提示验证的使用有两种方法:

  • 数组语法:enum: [[0, 1], '{VALUE} 不是有效值']
  • 对象语法:enum: { values: [0, 1], message: '{VALUE} 不是有效语法' }

但是经测试,对象语法并没有生效,所以这里使用的数组语法。

不过有时使用数组语法的时候,mongoose 会将它的枚举值(数组第一项)和错误提示(数组第二项)作为判断的枚举值,如上例会将属性的枚举值限制认定为 [0, 1]{VALUE} 不是有效值,而不是正确的 01

网上并没有找到相关原因,替代的方案是自定义一个校验器 validation:

validate: {
  validator: function(v) {
    return [0, 1].includes(v);
  },
  message: props => `${props.value} 不是有效值`
},

注意:校验器只会在 save() 方法执行前触发,update() 相关的方法并不会执行校验。

unique(唯一索引)校验

问题

尝试使用相同的 email(设置了 unique 的属性)去请求注册,向数据库插入数据,接口会失败并返回:

{
    "driver": true,
    "name": "MongoError",
    "index": 0,
    "code": 11000,
    "keyPattern": {
        "email": 1
    },
    "keyValue": {
        "email": "zhangsan@163.com"
    }
}

当视图违反 unique 约定的时候,MongoDB 就会返回 11000 错误。

这与 Mongoose 返回的错误对象格式不同:

// Mongoose 的错误对象示例
{
    "errors": {
        "name": {
            "name": "ValidatorError",
            "message": "昵称长度不能大于4",
            "properties": {
                "message": "昵称长度不能大于4",
                "type": "maxlength",
                "maxlength": 4,
                "path": "name",
                "value": "张三12345"
            },
            "kind": "maxlength",
            "path": "name",
            "value": "张三12345"
        }
    },
    "_message": "User validation failed",
    "name": "ValidationError",
    "message": "User validation failed: name: 昵称长度不能大于4"
}

解决方法

为了更方便的处理错误,可以使用插件 mongoose-unique-validator 将其转化成 Mongoose 格式的错误,还可以自定义错误消息。

配置插件:

// models\user.js
const mongoose = require('mongoose')
const crypto = require('crypto')
const { v1: uuidv1 } = require('uuid')
const uniqueValidator = require('mongoose-unique-validator')

// ...

// 配置插件
userSchema.plugin(uniqueValidator, {
  message: '{VALUE} 已经存在,请更换'
})

module.exports = mongoose.model('User', userSchema)

测试结果:

{
    "errors": {
        "email": {
            "name": "ValidatorError",
            "message": "zhangsan@163.com 已经存在,请更换",
            "properties": {
                "message": "zhangsan@163.com 已经存在,请更换",
                "type": "unique",
                "path": "email",
                "value": "zhangsan@163.com"
            },
            "kind": "unique",
            "path": "email",
            "value": "zhangsan@163.com"
        }
    },
    "_message": "User validation failed",
    "name": "ValidationError",
    "message": "User validation failed: email: zhangsan@163.com 已经存在,请更换"
}

express-validator 请求校验

现在的校验都是 Mongoose 和 MongoDB 的校验机制,接口应该在执行数据库操作前进行预前置验环节。

express 可以通过添加回调的方式增加校验环节,类似中间件。

express-validator 提供了一组验证器(Validators)和消毒器(Sanitizers)的中间件,内部处理自己附加的,还包装了 validator.js 的全部验证器和消毒器。

  • 验证器(Validators):用于验证的方法
  • 消毒器(Sanitizers):用于处理数据的方法

文档

  • express-validator 文档
    • 一些功能将被弃用,请按照官方建议使用,如
      • Sanitization middlewares 这些仅用于 sanitization 的中间件被提供了相同功能的 Validation middlewares 代替
      • req.check() 等旧版本的方法已被弃用(v6.x 版本已被启动,v5.x 还可以使用,但已声明是遗留 API)
  • validator.js 文档

使用方式

  • 首先创建验证链
    • 验证链(Validation Chain)是一个传递给 Express route 处理函数的验证中间件
  • 接着在验证链上添加验证器和消毒器
    • 可以根据需要,添加任意数量的验证器和消毒器,它们会按照顺序串行执行
    • 每个验证器和消毒器方法都会返回当前验证链实例
  • 最终通过 validationResult(req) 方法获取验证结果对象
    • 验证链中间件并不会阻塞后续校验和路由匹配,它们只会记录全部验证结果
    • 最终要调用接口获取结果,手动判断是否继续向后匹配
    • 方法返回的结果对象也提供多种方法去获取信息

编写注册验证中间件

// validator\user.js
const { body, validationResult } = require('express-validator')

// 判断校验是否成功
const validResultCallback = (req, res, next) => {
  // 获取校验结果
  const result = validationResult(req)
  if (!result.isEmpty()) {
    // result.array() 以数组形式返回验证结果,相同字段的验证结果会全部返回
    // result.mapped() 以对象形式返回验证结果,相同字段的验证结果只会返回第一个
    return res.status(400).json({ errors: result.array() })
  }
  next()
}

// 注册校验
// express route 配置中间件回调函数可以依次添加,也可以合并成一个数组添加
const userSignupValidator = [
  // body([fields, message]) 只验证请求主体 body 中的字段
  // fields 是要验证的字段名称字符串或数组
  // message 是默认的验证错误提示,如果没有为验证器指定验证错误提示就会使用它
  body('name', '请传入昵称')
    // 校验是否为空
    .notEmpty(),
  body('email', '邮箱最少4个字符,最多32个字符')
    // 正则校验
    .matches(/.+@.+\..+/)
    // withMessage(message) 为上一个验证器指定验证错误提示
    .withMessage('邮箱地址格式不正确')
    // 长度范围校验
    // undefined 为`不限`
    .isLength({
      min: 4,
      max: 32
    }),
  body('password', '请传入密码')
    .notEmpty()
    .isLength({ min: 6 })
    .withMessage('密码不能少于6位')
    .matches(/\d/)
    .withMessage('密码至少包含一个数字'),
  validResultCallback
]

module.exports = {
  userSignupValidator
}

添加中间件:

// routes\user.js
const express = require('express')
const { signup } = require('../controllers/user')
const { userSignupValidator } = require('../validator/user')

const router = express.Router()

// 注册
router.post('/signup', userSignupValidator, signup)

module.exports = router

测试

// 请求参数
{
    "name": "",
    "email": "123",
    "password": "a"
}

// 返回结果
{
    "errors": [
        {
            "value": "",
            "msg": "请传入昵称",
            "param": "name",
            "location": "body"
        },
        {
            "value": "123",
            "msg": "邮箱地址格式不正确",
            "param": "email",
            "location": "body"
        },
        {
            "value": "123",
            "msg": "邮箱最少4个字符,最多32个字符",
            "param": "email",
            "location": "body"
        },
        {
            "value": "a",
            "msg": "密码不能少于6位",
            "param": "password",
            "location": "body"
        },
        {
            "value": "a",
            "msg": "密码至少包含一个数字",
            "param": "password",
            "location": "body"
        }
    ]
}

统一错误信息格式

现在 Mongoose 和 express-validatior 返回的错误信息结构不一样,且不方便提取 message。

// Mongoose 错误信息
{
  errors: {
    name: {
      message: '...'
      ...
    }
  }
  ...
}

// express-validator result.array() 错误信息
[
  {
    msg: '...',
    ...
  },
  ...
]

定义一个辅助函数,专门从它们的错误信息对象中提取收集 message。

// helpers\dbErrorHandler.js

// 统一提取收集错误信息
const errorHandler = errorInfo => {
  const message = []

  if (typeof errorInfo === 'string') {
    // 自定义错误提示
    message.push(errorInfo)
  } else if (Array.isArray(errorInfo)) {
    // express-validator 错误信息
    errorInfo.forEach(error => {
      if (error.msg) {
        message.push(error.msg)
      }
    })
  } else {
    // Mongoose 错误信息
    const { errors } = errorInfo
    Object.keys(errors).forEach(field => {
      if (errors[field].message) {
        message.push(errors[field].message)
      }
    })
  }

  return { errors: message }
}

module.exports = {
  errorHandler
}

应用辅助函数:

// validator\user.js
const { body, validationResult } = require('express-validator')
const { errorHandler } = require('../helpers/dbErrorHandler')

// 判断校验是否成功
const validResultCallback = (req, res, next) => {
  const result = validationResult(req)
  if (!result.isEmpty()) {
    return res.status(400).json(errorHandler(result.array()))
  }
  next()
}

...

// controllers\user.js
const User = require('../models/user')
const { errorHandler } = require('../helpers/dbErrorHandler')

// 注册
const signup = (req, res) => {
  const user = new User(req.body)
  user.save((error, user) => {
    if (error) {
      // 响应
      return res.status(400).json(errorHandler(error))
    }
    user.salt = undefined
    user.hashed_password = undefined
    res.json(user)
  })
}

module.exports = {
  signup
}

请求失败返回数据格式示例:

{
    "errors": [
        "请传入昵称",
        "邮箱地址格式不正确",
        "邮箱最少4个字符,最多32个字符",
        "请传入密码",
        "密码不能少于6位",
        "密码至少包含一个数字"
    ]
}

api-用户管理-用户登录

api 设计

基本信息

Path: /signin

Method: POST

请求参数

Body

参数名称是否必传类型说明
emailString邮箱地址(登录账户)
passwordString密码

返回数据

字段名称说明
token登录认证
user用户信息对象
├─ _idmongoose 默认分配的 id 属性
├─ name昵称
├─ email邮箱地址(登录账户)
├─ role角色(普通用户 0; 管理员用户 1)

请求过程

  1. 根据 email 去数据库查询用户,如果用户不存在,返回 400
  2. 验证密码,如果失败,返回 400
  3. 根据用户 id 和 自定义的密钥生成用于用户认证的 json web token
  4. 将 token 写入响应对象,并返回给客户端

定义一个生成 JWT 的密钥

# .env
# 服务启动监听端口
APP_PORT=80
# JWT 密钥
JWT_SECRET=test

# 本地数据库连接信息
# 连接地址
DB_HOST=localhost
# 数据库名称
DB_NAME=ecommerce
# 用户名(默认为空)
DB_USER=
# 密码(默认为空)
DB_PASS=
# 端口(默认27017)
DB_PORT=27017

添加验证密码的实例方法

// models\user.js

...

// 添加实例方法
userSchema.method({
  // 密码加密
  encryptPassword(password) {...},
  // 验证密码
  authenticate(password) {
    return this.encryptPassword(password) === this.hashed_password
  }
})

...

编写登录请求方法

// controllers\user.js
const User = require('../models/user')
const { errorHandler } = require('../helpers/dbErrorHandler')
const jwt = require('jsonwebtoken')
const _ = require('lodash')

// 注册
const signup = (req, res) => {...}

// 登录
const signin = (req, res) => {
  // 获取 email 和 password
  const { email, password } = req.body
  // 根据 email 查找用户
  User.findOne({ email }, (error, user) => {
    // 查询失败
    // 查询结果为空也属于查询成功,所以要判断结果是否为空
    if (error || !user) {
      // 响应
      return res.status(400).json(errorHandler('用户不存在'))
    }
    // 验证密码
    if (!user.authenticate(password)) {
      // 响应
      return res.status(400).json(errorHandler('邮箱和密码不匹配'))
    }
    // 生成 token
    // jwt.sign(payload, secretOrPrivateKey)
    const token = jwt.sign({ id: user._id }, process.env.JWT_SECRET)
    // 响应
    return res.json({
      token,
      // lodash 的 pick() 方法基于传递的对象生成包含指定属性的新对象
      user: _.pick(user, ['_id', 'name', 'email', 'role'])
    })
  })
}

module.exports = {
  signup,
  signin
}

编写请求校验

// validator\auth.js
const { body, validationResult } = require('express-validator')
const { errorHandler } = require('../helpers/dbErrorHandler')

// 判断校验是否成功
const validResultCallback = (req, res, next) => {...}

// 注册校验
const userSignupValidator = [...]

// 登录校验
const userSigninValidator = [
  body('email')
    .notEmpty()
    .withMessage('请传入邮箱')
    .matches(/.+@.+\..+/)
    .withMessage('邮箱地址格式不正确'),
  body('password').notEmpty().withMessage('请传入密码'),
  validResultCallback
]

module.exports = {
  userSignupValidator,
  userSigninValidator
}

配置接口

// routes\user.js
const express = require('express')
const { signup, signin } = require('../controllers/user')
const { userSignupValidator, userSigninValidator } = require('../validator/user')

const router = express.Router()

// 注册
router.post('/signup', userSignupValidator, signup)
// 登录
router.post('/signin', userSigninValidator, signin)

module.exports = router

测试

// 请求
{
    "email": "zhangsan@163.com",
    "password": "123456"
}
// 返回
{
    "token": "...",
    "user": {
        "_id": "...",
        "name": "张三",
        "email": "zhangsan@163.com",
        "role": 0
    }
}

api-用户管理-根据 id 获取用户信息

api 设计

基本信息

Path: /user/:userId

Method: GET

请求参数

Headers

参数名称是否必须备注
Authorization认证token,格式 Bearer <JSON WEB TOKEN>

返回数据

字段名称说明
role角色(普通用户 0; 管理员用户 1)
_idmongoose 默认分配的 id 字段
name昵称
email邮箱地址(登录账户)
createdAt创建 Schema 时指定分配的自动管理的 createdAt 字段
updatedAt创建 Schema 时指定分配的自动管理的 updatedAt 字段

请求过程

  1. 根据路径中的用户 id 查询数据库用户信息
  2. 解析请求头 Authorization 中的 token,获取 token 中的用户 id
  3. 判断查询结果中的用户 id 是否和 token 中的 id 相同
  4. 返回用户信息对象

定义获取用户信息的前置路由中间件

express 的路由方法router.param(name, callback) 用于监听请求路径上的参数(param),当监听的参数出现在请求路径上时会先执行回调逻辑,再匹配后续的路由。

例如 router.param('id', myCallback) 当请求 /user/:id 的时候就会执行回调 myCallback

回调中可以进行数据库查询,向 req 扩展数据等操作。

router.param() 的回调函数接收的第四个参数是监听参数的值。

// controllers\user.js
const User = require('../models/user')
const { errorHandler } = require('../helpers/dbErrorHandler')
const jwt = require('jsonwebtoken')
const _ = require('lodash')

// 注册
const signup = (req, res) => {...}

// 登录
const signin = (req, res) => {...}

// 根据 id 获取用户信息的中间件
const getUserById = (req, res, next, id) => {
  User.findById(id, (error, user) => {
    if (error || !user) {
      return res.status(400).json(errorHandler('用户没找到'))
    }
    // 将查询到的用户信息存储再 request 对象上供后续路由使用
    req.profile = user
    next()
  })
}

// 返回用户信息
const read = (req, res) => {
  // 删除不必要的字段
  req.profile.hashed_password = undefined
  req.profile.salt = undefined
  // 响应
  res.json(req.profile)
}

module.exports = {
  signup,
  signin,
  getUserById,
  read
}

使用 router.param()

// routes\user.js
const express = require('express')
const { signup, signin, getUserById, read } = require('../controllers/user')
const { userSignupValidator, userSigninValidator } = require('../validator/user')

const router = express.Router()

// 注册
router.post('/signup', userSignupValidator, signup)
// 登录
router.post('/signin', userSigninValidator, signin)
// 根据 id 获取用户信息
router.get('/user/:userId', read)

// 监听路由参数
// 根据 id 获取用户信息
router.param('userId', getUserById)

module.exports = router

解析 token

express-jwt 是 Express 框架的中间件,它通过 jsonwebtoken 模块解析请求报文中的 JWT。

该模块默认解析请求头中的 Authorization 字段,并默认按照 OAuth2 Bearer token 去解析。

OAuth2 Bearer token 的格式是 Bearer <JSON WEB TOKEN>,所以请求头中的 Authorization 传递 token 的时候要在前面拼接 Bearer,也可以编写自定义解析方法自定义 Authorization 的格式。

express-jwt 会解析 JWT 中的 payload,并将其存储在 req 上供后续路由使用。

可以通过 requestProperty 指定存储在 req 的属性名,默认 user

// validator\user.js
const { body, validationResult } = require('express-validator')
const { errorHandler } = require('../helpers/dbErrorHandler')
const expressJwt = require('express-jwt')

// 判断校验是否成功
const validResultCallback = (req, res, next) => {...}

// 注册校验
const userSignupValidator = [...]

// 登录校验
const userSigninValidator = [...]

// token 解析
// header => Authorization: Bearer <JSON WEB TOKEN>
const tokenParser = expressJwt({
  secret: process.env.JWT_SECRET,
  algorithms: ['HS256'], // token 算法,jsonwebtoken 默认 HS256
  requestProperty: 'auth' // 将 payload 存储到 req.auth
})

// 判断获取的用户信息是否是登录人的用户信息
const authValidator = (req, res, next) => {
  // req.profile => 通过用户 id 获取的用户信息
  // req.auth => token 中解析的登录人的用户信息

  // _id 是 Mongoose 自动分配并设置的 id 属性,如果没有该字段就无法保存 document
  // _id 默认是由 ObjectId 实例化的对象,可通过调用它的 toString() 方法转化为字符串
  if (!(req.profile && req.auth && req.profile._id.toString() === req.auth.id)) {
    return res.status(403).json(errorHandler('用户认证失败'))
  }
    return res.status(403).json(errorHandler('用户认证失败'))
  }
  next()
}

module.exports = {
  userSignupValidator,
  userSigninValidator,
  tokenParser,
  authValidator
}

配置中间件:

// routes\user.js
const express = require('express')
const { signup, signin, getUserById, read } = require('../controllers/user')
const { userSignupValidator, userSigninValidator, tokenParser, authValidator } = require('../validator/user')

const router = express.Router()

// 注册
router.post('/signup', userSignupValidator, signup)
// 登录
router.post('/signin', userSigninValidator, signin)
// 根据 id 获取用户信息
router.get('/user/:userId', [tokenParse, authValidator], read)

// 监听路由参数
// 根据 id 获取用户信息
router.param('userId', getUserById)

module.exports = router

api-用户管理-更新用户信息

api 设计

基本信息

Path: /user/:userId

Method: PUT

本例 API 遵循 RESTful 规范,从语义上选择,PUT是幂等方法,POST不是,PUT用于更新、POST用于新增。

请求参数

Headers

参数名称是否必须备注
Authorization认证token,格式 Bearer <JSON WEB TOKEN>

Body

参数名称是否必传类型说明
nameString昵称(如果传了则修改昵称)
emailString邮箱地址(登录账户)
passwordString密码(如果传了则修改密码)

返回数据

字段名称说明
name昵称
email邮箱地址(登录账户)
role角色(普通用户 0; 管理员用户 1)
createdAt创建 Schema 时指定分配的自动管理的 createdAt 字段
updatedAt创建 Schema 时指定分配的自动管理的 updatedAt 字段
_idmongoose 默认分配的 id 字段,当访问 id 时就是获取的它
_vmongoose 默认分配的 versionKey 字段

定义 api

// controllers\user.js
const User = require('../models/user')
const { errorHandler } = require('../helpers/dbErrorHandler')
const jwt = require('jsonwebtoken')
const _ = require('lodash')

// 注册
const signup = (req, res) => {...}

// 登录
const signin = (req, res) => {...}

// 根据 id 获取用户信息的中间件
const getUserById = (req, res, next, id) => {...}

// 返回用户信息
const read = (req, res) => {...}

// 根据 id 更新用户信息
const update = (req, res) => {
  const { name, password } = req.body
  const user = req.profile

  if (name) {
    user.name = name
  }
  if (password) {
    // password 的 setter 会触发生成 hashed_password
    user.password = password
  }

  user.save((error, updateUser) => {
    if (error) {
      return res.status(400).json(errorHandler(error))
    }

    // 删除不必要的字段
    updateUser.hashed_password = undefined
    updateUser.salt = undefined
    // 响应
    res.json(updateUser)
  })
}

module.exports = {
  signup,
  signin,
  getUserById,
  read,
  update
}

创建验证器

// validator\user.js

...

// 根据 id 更新用户信息
const updateValidator = [
  body('password')
    // 如果传递了 password(不是 undefined)就继续校验
    .if(body('password').exists())
    .isLength({ min: 6 })
    .withMessage('密码不能少于6位')
    .matches(/\d/)
    .withMessage('密码至少包含一个数字'),
  validResultCallback
]

module.exports = {
  userSignupValidator,
  userSigninValidator,
  tokenParser,
  authValidator,
  updateValidator
}

配置 api

// routes\user.js
const express = require('express')
const { signup, signin, getUserById, read, update } = require('../controllers/user')
const {
  userSignupValidator,
  userSigninValidator,
  tokenParser,
  authValidator,
  updateValidator
} = require('../validator/user')

const router = express.Router()

// 注册
router.post('/signup', userSignupValidator, signup)
// 登录
router.post('/signin', userSigninValidator, signin)
// 根据 id 获取用户信息
router.get('/user/:userId', [tokenParser, authValidator], read)
// 根据 id 更新用户信息
router.put('/user/:userId', updateValidator, [tokenParser, authValidator], update)

// 监听路由参数
// 根据 id 获取用户信息
router.param('userId', getUserById)

module.exports = router

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

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值