Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 0 additions & 17 deletions constants/file.js

This file was deleted.

17 changes: 17 additions & 0 deletions constants/files.js
Original file line number Diff line number Diff line change
@@ -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,
};
10 changes: 10 additions & 0 deletions constants/users.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
const USER_STATUS = {
ACTIVE: "ACTIVE", // 用户账号已激活
INACTIVE: "INACTIVE", // 用户账号未激活
BANNED: "BANNED", // 用户账号被封禁
PENDING: "PENDING", // 用户账号待审核
};

module.exports = {
USER_STATUS,
};
10 changes: 5 additions & 5 deletions middleware/authenticateToken.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,23 +32,23 @@ const authenticateToken = async (ctx, next) => {

try {
if (!token) {
ctx.status = 403;
ctx.status = 401;
ctx.body = { message: "Not Logged In" };
return;
}

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

// Redis 查看是否存在
const redisToken = await redisClient.get(`user_login:${decoded.id}`);
if (!redisToken) {
ctx.status = 403;
ctx.status = 401;
ctx.body = { message: "Invalid token" };
return;
}
Expand All @@ -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" };
}
};

Expand Down
29 changes: 29 additions & 0 deletions middleware/checkAdminAuth.js
Original file line number Diff line number Diff line change
@@ -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;
20 changes: 10 additions & 10 deletions models/users.js
Original file line number Diff line number Diff line change
@@ -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(
Expand Down Expand Up @@ -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: {
Expand Down Expand Up @@ -72,6 +67,11 @@ const Users = sequelize.define(
allowNull: true,
defaultValue: 0,
},
is_admin: {
type: DataTypes.BOOLEAN,
allowNull: true,
defaultValue: false,
},
},
{
tableName: "users",
Expand Down
2 changes: 1 addition & 1 deletion routers/files.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
51 changes: 41 additions & 10 deletions routers/users.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,28 +2,32 @@ 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();

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

Expand All @@ -37,7 +41,6 @@ router.post("/login", validateBody(USERS_LOGIN_POST), async (ctx) => {
if (err) {
console.log(err);
}
console.log(user);
});

// 将 token 存储在 Redis 中
Expand All @@ -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" };
}
});

Expand Down Expand Up @@ -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;
5 changes: 5 additions & 0 deletions types/schema/users.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
};