From 8a3c7f17b07201e93f0209a47a199f614c406206 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=A5=E5=AE=B6=E8=BE=89?= Date: Wed, 3 Jul 2024 23:01:51 +0800 Subject: [PATCH 1/2] feat: #22 Add administrator middleware verification --- constants/file.js | 17 ----------- constants/files.js | 17 +++++++++++ constants/users.js | 10 +++++++ middleware/authenticateToken.js | 4 +-- middleware/checkAdminAuth.js | 29 +++++++++++++++++++ models/users.js | 20 ++++++------- routers/files.js | 2 +- routers/users.js | 51 ++++++++++++++++++++++++++------- types/schema/users.js | 5 ++++ 9 files changed, 115 insertions(+), 40 deletions(-) delete mode 100644 constants/file.js create mode 100644 constants/files.js create mode 100644 constants/users.js create mode 100644 middleware/checkAdminAuth.js diff --git a/constants/file.js b/constants/file.js deleted file mode 100644 index 56d31a6..0000000 --- a/constants/file.js +++ /dev/null @@ -1,17 +0,0 @@ -const tinifySupportedMimeTypes = ['image/jpeg', 'image/png', 'image/webp']; - -const imageMimeTypes = [ - 'image/jpeg', - 'image/png', - 'image/webp', - 'image/gif', - 'image/bmp', - 'image/tiff', - 'image/x-icon', - 'image/svg+xml' -]; - -module.exports = { - imageMimeTypes, - tinifySupportedMimeTypes -} \ No newline at end of file diff --git a/constants/files.js b/constants/files.js new file mode 100644 index 0000000..1ee4b3e --- /dev/null +++ b/constants/files.js @@ -0,0 +1,17 @@ +const tinifySupportedMimeTypes = ["image/jpeg", "image/png", "image/webp"]; + +const imageMimeTypes = [ + "image/jpeg", + "image/png", + "image/webp", + "image/gif", + "image/bmp", + "image/tiff", + "image/x-icon", + "image/svg+xml", +]; + +module.exports = { + imageMimeTypes, + tinifySupportedMimeTypes, +}; diff --git a/constants/users.js b/constants/users.js new file mode 100644 index 0000000..d59c54f --- /dev/null +++ b/constants/users.js @@ -0,0 +1,10 @@ +const USER_STATUS = { + ACTIVE: "ACTIVE", // 用户账号已激活 + INACTIVE: "INACTIVE", // 用户账号未激活 + BANNED: "BANNED", // 用户账号被封禁 + PENDING: "PENDING", // 用户账号待审核 +}; + +module.exports = { + USER_STATUS, +}; diff --git a/middleware/authenticateToken.js b/middleware/authenticateToken.js index 8552e36..fefc81b 100644 --- a/middleware/authenticateToken.js +++ b/middleware/authenticateToken.js @@ -58,8 +58,8 @@ const authenticateToken = async (ctx, next) => { await next(); } catch (error) { console.error(error); - ctx.status = 500; - ctx.body = { message: "Internal server error" }; + ctx.status = 401; + ctx.body = { message: "Invalid token" }; } }; diff --git a/middleware/checkAdminAuth.js b/middleware/checkAdminAuth.js new file mode 100644 index 0000000..2524e76 --- /dev/null +++ b/middleware/checkAdminAuth.js @@ -0,0 +1,29 @@ +// 校验用户是否为管理员的中间件 +const Users = require("../models/users"); + +async function checkAdminAuth(ctx, next) { + try { + const { id } = ctx.state.user; + + const user = await Users.findOne({ + where: { id }, + attributes: ["is_admin"], + }); + + // 检查用户是否为管理员 + if (!user.is_admin) { + ctx.status = 403; + ctx.body = { message: "Access denied. Admins only." }; + return; + } + + // ctx.state.user = decoded; + + await next(); + } catch (error) { + ctx.status = 401; + ctx.body = { message: "Unauthorized" }; + } +} + +module.exports = checkAdminAuth; diff --git a/models/users.js b/models/users.js index 0572660..a5610f7 100644 --- a/models/users.js +++ b/models/users.js @@ -1,11 +1,6 @@ -const { DataTypes, Sequelize } = require("sequelize"); -const sequelize = require("../utils/dbInstance"); // 修改为实际的sequelize实例路径 -const UserStatus = { - ACTIVE: "ACTIVE", // 用户账号已激活 - INACTIVE: "INACTIVE", // 用户账号未激活 - BANNED: "BANNED", // 用户账号被封禁 - PENDING: "PENDING", // 用户账号待审核 -}; +const { DataTypes } = require("sequelize"); +const sequelize = require("../utils/dbInstance"); +const { USER_STATUS } = require("../constants/users"); // 定义 User 模型 const Users = sequelize.define( @@ -38,9 +33,9 @@ const Users = sequelize.define( }, status: { type: DataTypes.ENUM, - values: Object.values(UserStatus), + values: Object.values(USER_STATUS), allowNull: false, - defaultValue: UserStatus.ACTIVE, + defaultValue: USER_STATUS.ACTIVE, collate: "utf8mb4_unicode_ci", }, created_at: { @@ -72,6 +67,11 @@ const Users = sequelize.define( allowNull: true, defaultValue: 0, }, + is_admin: { + type: DataTypes.BOOLEAN, + allowNull: true, + defaultValue: false, + }, }, { tableName: "users", diff --git a/routers/files.js b/routers/files.js index 0c61be2..3bb8022 100644 --- a/routers/files.js +++ b/routers/files.js @@ -14,7 +14,7 @@ const Files = require("../models/files"); const { imageMimeTypes, tinifySupportedMimeTypes, -} = require("../constants/file"); +} = require("../constants/files"); const { FILES_UPLOAD_POST_QUERY, FILES_LIST_GET_QUERY, diff --git a/routers/users.js b/routers/users.js index db87cf3..1a206e4 100644 --- a/routers/users.js +++ b/routers/users.js @@ -2,10 +2,12 @@ const Router = require("koa-router"); const redisClient = require("../redis"); const bcrypt = require("bcrypt"); const jwt = require("jsonwebtoken"); +const checkAdminAuth = require("../middleware/checkAdminAuth"); require("dotenv").config({ path: ".env.local" }); const Users = require("../models/users"); -const { USERS_LOGIN_POST } = require("../types/schema/users"); -const { validateBody } = require("../types"); +const { USERS_LOGIN_POST, USER_REST_ID } = require("../types/schema/users"); +const { validateBody, validateParams } = require("../types"); +const { USER_STATUS } = require("../constants/users"); const router = new Router(); @@ -13,17 +15,19 @@ router.post("/login", validateBody(USERS_LOGIN_POST), async (ctx) => { const { username, password } = ctx.request.body; try { - const user = await Users.findOne({ where: { username } }); + const user = await Users.findOne({ + where: { username, status: USER_STATUS.ACTIVE }, + }); if (!user) { - ctx.status = 401; - ctx.body = { message: "Invalid credentials" }; + ctx.status = 403; + ctx.body = { message: "Incorrect account or password" }; return; } const passwordMatch = await bcrypt.compare(password, user.password); if (!passwordMatch) { - ctx.status = 401; - ctx.body = { message: "Invalid credentials" }; + ctx.status = 403; + ctx.body = { message: "Incorrect account or password" }; return; } @@ -37,7 +41,6 @@ router.post("/login", validateBody(USERS_LOGIN_POST), async (ctx) => { if (err) { console.log(err); } - console.log(user); }); // 将 token 存储在 Redis 中 @@ -53,8 +56,8 @@ router.post("/login", validateBody(USERS_LOGIN_POST), async (ctx) => { ctx.body = { token }; } catch (error) { console.error(error); - ctx.status = 500; - ctx.body = { message: "Internal server error" }; + ctx.status = 403; + ctx.body = { message: "Incorrect account or password" }; } }); @@ -133,4 +136,32 @@ router.post("/logout", async (ctx) => { } }); +// 禁用用户 +router.patch( + "/users/:id/disabled", + validateParams(USER_REST_ID), + checkAdminAuth, + async (ctx) => { + const { id } = ctx.params; + const user = await Users.findOne({ where: { id } }); + + if (!user.id) { + ctx.status = 404; + ctx.body = { message: "User not found" }; + return; + } + + // 强制下线 Token + await redisClient.del(`user_login:${id}`); + + // 禁用此账号 + user.update({ + status: "BANNED", + logout_at: new Date(), + }); + + ctx.status = 204; + } +); + module.exports = router; diff --git a/types/schema/users.js b/types/schema/users.js index b862426..5b4dc79 100644 --- a/types/schema/users.js +++ b/types/schema/users.js @@ -5,6 +5,11 @@ const USERS_LOGIN_POST = Joi.object({ password: Joi.string().required(), }); +const USER_REST_ID = Joi.object({ + id: Joi.string().required(), +}); + module.exports = { + USER_REST_ID, USERS_LOGIN_POST, }; From ccbc692e295bd4f8bbe00925994c33b2b9f17580 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=A5=E5=AE=B6=E8=BE=89?= Date: Wed, 3 Jul 2024 23:04:39 +0800 Subject: [PATCH 2/2] feat: #22 Add administrator middleware verification --- middleware/authenticateToken.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/middleware/authenticateToken.js b/middleware/authenticateToken.js index fefc81b..3d84fe3 100644 --- a/middleware/authenticateToken.js +++ b/middleware/authenticateToken.js @@ -32,7 +32,7 @@ const authenticateToken = async (ctx, next) => { try { if (!token) { - ctx.status = 403; + ctx.status = 401; ctx.body = { message: "Not Logged In" }; return; } @@ -40,7 +40,7 @@ const authenticateToken = async (ctx, next) => { const decoded = await promisify(jwt.verify)(token, process.env.JWT_SECRET); if (!decoded.id) { - ctx.status = 403; + ctx.status = 401; ctx.body = { message: "No token provided" }; return; } @@ -48,7 +48,7 @@ const authenticateToken = async (ctx, next) => { // Redis 查看是否存在 const redisToken = await redisClient.get(`user_login:${decoded.id}`); if (!redisToken) { - ctx.status = 403; + ctx.status = 401; ctx.body = { message: "Invalid token" }; return; }