前言
这章是后续所有系统功能更新,类似缓存,安全之类的
简单登录日志
创建模型models/LoginLog.js
const mongoose = require('mongoose');
const loginLogSchema = new mongoose.Schema({
user: {
type: mongoose.Schema.Types.ObjectId,
ref: 'User',
required: true
},
status: {
type: String,
enum: ['success', 'failed'],
required: true
},
ip: {
type: String,
required: true
},
timestamp: {
type: Date,
default: Date.now
}
}, {
timestamps: true
});
const LoginLog = mongoose.model('LoginLog', loginLogSchema);
module.exports = LoginLog;
修改控制器controller/authController.js
const User = require('../models/User');
const jwt = require('jsonwebtoken');
const LoginLog = require('../models/LoginLog');
// 生成 JWT Token
const generateToken = (id) => {
return jwt.sign({ id }, process.env.JWT_SECRET, {
expiresIn: '30d'
});
};
// 记录登录日志
const logLoginAttempt = async (user, status, req) => {
try {
await LoginLog.create({
user: user._id,
status,
ip: req.ip
});
} catch (error) {
console.error('Failed to create login log:', error);
}
};
// 用户登录
const login = async (req, res) => {
try {
const { email, password } = req.body;
// 查找用户
const user = await User.findOne({ email });
if (!user) {
await logLoginAttempt(user, 'failed', req);
return res.status(401).json({ message: '邮箱或密码错误' });
}
// 验证密码
const isMatch = await user.matchPassword(password);
if (!isMatch) {
await logLoginAttempt(user, 'failed', req);
return res.status(401).json({ message: '邮箱或密码错误' });
}
// 记录成功登录
await logLoginAttempt(user, 'success', req);
// 生成 token
const token = generateToken(user._id);
res.json({
_id: user._id,
username: user.username,
email: user.email,
token
});
} catch (error) {
res.status(500).json({ message: error.message });
}
};
// 获取用户登录历史
const getLoginHistory = async (req, res) => {
try {
const loginLogs = await LoginLog.find({ user: req.user._id })
.sort({ timestamp: -1 })
.limit(10)
.populate('user', 'username email')
.lean();
res.json(loginLogs);
} catch (error) {
res.status(500).json({ message: error.message });
}
};
module.exports = {
register,
login,
getMe,
getLoginHistory
};
添加路由
// 导入必要的模块
const express = require('express');
const router = express.Router();
// 导入认证控制器函数
const { register, login, getMe, getLoginHistory } = require('../controllers/authController');
// 导入认证中间件
const { protect } = require('../middleware/auth');
// 导入请求验证中间件
const validateRequest = require('../middleware/validateRequest');
// 导入验证模式
const { registerSchema, loginSchema } = require('../middleware/validationSchemas');
// 注册路由 - 使用验证中间件验证请求数据,然后调用注册控制器
router.post('/register', validateRequest(registerSchema), register);
// 登录路由 - 使用验证中间件验证请求数据,然后调用登录控制器
router.post('/login', validateRequest(loginSchema), login);
// 获取当前用户信息路由 - 需要认证中间件保护,然后调用获取用户信息控制器
router.get('/me', protect, getMe);
// 获取用户登录历史路由 - 需要认证中间件保护,然后调用获取用户登录历史控制器
router.get('/login-history', protect, getLoginHistory);
module.exports = router;
编写API文档
使用Swagger/OpenAPI
编写API文档
安装依赖
npm install swagger-jsdoc swagger-ui-express
创建swagger.js
const swaggerJsdoc = require('swagger-jsdoc');
const options = {
definition: {
openapi: '3.0.0',
info: {
title: 'Blog API',
version: '1.0.0',
description: '博客系统API文档'
},
servers: [
{
url: 'http://localhost:3000',
description: '开发服务器'
}
],
components: {
securitySchemes: {
bearerAuth: {
type: 'http',
scheme: 'bearer',
bearerFormat: 'JWT'
}
}
}
},
apis: ['./routes/*.js']
};
module.exports = swaggerJsdoc(options);
app.js
中添加Swagger UI
const swaggerUi = require('swagger-ui-express');
const swaggerSpecs = require('./config/swagger');
// ... 其他中间件
// Swagger UI
app.use('/api-docs', swaggerUi.serve, swaggerUi.setup(swaggerSpecs));
在路由文件中添加Swagger
注释
用户注册/登录auth.js
/**
* @swagger
* tags:
* name: Auth
* description: 用户认证相关接口
*/
/**
* @swagger
* /api/auth/register:
* post:
* tags: [Auth]
* summary: 用户注册
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* required:
* - username
* - email
* - password
* properties:
* username:
* type: string
* example: johndoe
* email:
* type: string
* format: email
* example: john@example.com
* password:
* type: string
* format: password
* example: password123
* responses:
* 201:
* description: 注册成功
* content:
* application/json:
* schema:
* type: object
* properties:
* _id:
* type: string
* username:
* type: string
* email:
* type: string
* token:
* type: string
* 400:
* description: 请求参数错误
*/
router.post('/register', register);
/**
* @swagger
* /api/auth/login:
* post:
* tags: [Auth]
* summary: 用户登录
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* required:
* - email
* - password
* properties:
* email:
* type: string
* format: email
* example: john@example.com
* password:
* type: string
* format: password
* example: password123
* responses:
* 200:
* description: 登录成功
* content:
* application/json:
* schema:
* type: object
* properties:
* _id:
* type: string
* username:
* type: string
* email:
* type: string
* token:
* type: string
* 401:
* description: 邮箱或密码错误
*/
router.post('/login', login);
/**
* @swagger
* /api/auth/me:
* get:
* tags: [Auth]
* summary: 获取当前用户信息
* security:
* - bearerAuth: []
* responses:
* 200:
* description: 成功获取用户信息
* content:
* application/json:
* schema:
* type: object
* properties:
* _id:
* type: string
* username:
* type: string
* email:
* type: string
* 401:
* description: 未授权
*/
router.get('/me', protect, getMe);
文章接口post.js
/**
* @swagger
* tags:
* name: Posts
* description: 文章相关接口
*/
/**
* @swagger
* /api/posts:
* get:
* tags: [Posts]
* summary: 获取文章列表
* parameters:
* - in: query
* name: page
* schema:
* type: integer
* default: 1
* description: 页码
* - in: query
* name: limit
* schema:
* type: integer
* default: 10
* description: 每页数量
* responses:
* 200:
* description: 成功获取文章列表
* content:
* application/json:
* schema:
* type: object
* properties:
* posts:
* type: array
* items:
* type: object
* properties:
* _id:
* type: string
* title:
* type: string
* content:
* type: string
* author:
* type: string
* category:
* type: string
* pagination:
* type: object
* properties:
* current:
* type: integer
* total:
* type: integer
* totalRecords:
* type: integer
* post:
* tags: [Posts]
* summary: 创建文章
* security:
* - bearerAuth: []
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* required:
* - title
* - content
* - category
* properties:
* title:
* type: string
* example: 文章标题
* content:
* type: string
* example: 文章内容
* category:
* type: string
* example: 技术
* tags:
* type: array
* items:
* type: string
* example: ["JavaScript", "Node.js"]
* responses:
* 201:
* description: 文章创建成功
* 401:
* description: 未授权
*/
router.get('/', getPosts);
router.post('/', protect, createPost);
/**
* @swagger
* /api/posts/{id}:
* get:
* tags: [Posts]
* summary: 获取文章详情
* parameters:
* - in: path
* name: id
* required: true
* schema:
* type: string
* description: 文章ID
* responses:
* 200:
* description: 成功获取文章详情
* 404:
* description: 文章不存在
* put:
* tags: [Posts]
* summary: 更新文章
* security:
* - bearerAuth: []
* parameters:
* - in: path
* name: id
* required: true
* schema:
* type: string
* description: 文章ID
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* properties:
* title:
* type: string
* content:
* type: string
* category:
* type: string
* tags:
* type: array
* items:
* type: string
* responses:
* 200:
* description: 文章更新成功
* 401:
* description: 未授权
* 404:
* description: 文章不存在
* delete:
* tags: [Posts]
* summary: 删除文章
* security:
* - bearerAuth: []
* parameters:
* - in: path
* name: id
* required: true
* schema:
* type: string
* description: 文章ID
* responses:
* 200:
* description: 文章删除成功
* 401:
* description: 未授权
* 404:
* description: 文章不存在
*/
router.get('/:id', getPost);
router.put('/:id', protect, updatePost);
router.delete('/:id', protect, deletePost);
使用方法:
- 启动服务器
- 访问
http://localhost:3000/api-docs
查看文档