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
39 changes: 22 additions & 17 deletions index.js
Original file line number Diff line number Diff line change
@@ -1,22 +1,24 @@
// app.js
const Koa = require('koa');
const {koaBody} = require('koa-body');
const path = require('path');
const fs = require('fs');
const sequelize = require('./utils/dbInstance'); // 确保路径正确
const filesRouter = require('./routers/files'); // 确保路径正确

require('dotenv').config({ path: '.env.local' });
const Koa = require("koa");
const { koaBody } = require("koa-body");
const path = require("path");
const fs = require("fs");
const sequelize = require("./utils/dbInstance");
const filesRouter = require("./routers/files");
const usersRouter = require("./routers/users");
const redisClient = require("./redis");
const authenticateToken = require("./middleware/authenticateToken");

require("dotenv").config({ path: ".env.local" });

const app = new Koa();


app.use(require('koa-static')(path.join(__dirname, 'public')));
app.use(require("koa-static")(path.join(__dirname, "public")));

const createDirectories = () => {
const dirs = [
path.join(__dirname, 'provisional'),
path.join(__dirname, 'resource')
path.join(__dirname, "provisional"),
path.join(__dirname, "resource"),
];
dirs.forEach((dir) => {
if (!fs.existsSync(dir)) {
Expand All @@ -27,23 +29,26 @@ const createDirectories = () => {

createDirectories();

app.use(authenticateToken);

app.use(
koaBody({
multipart: true,
// 解决 DELETE 没法获取ids的问题
parsedMethods: ['POST', 'PUT', 'PATCH', 'DELETE'],
parsedMethods: ["POST", "PUT", "PATCH", "DELETE"],
formidable: {
uploadDir: path.join(__dirname, 'provisional'), // 临时上传目录
keepExtensions: true // 保留文件扩展名
}
uploadDir: path.join(__dirname, "provisional"), // 临时上传目录
keepExtensions: true, // 保留文件扩展名
},
})
);


// 挂载文件路由
app.use(usersRouter.routes()).use(usersRouter.allowedMethods());
app.use(filesRouter.routes()).use(filesRouter.allowedMethods());

app.listen(process.env.SERVER_PORT, async () => {
await redisClient.connect();
await sequelize.sync();
console.log(`Server is running on ${process.env.INTERNAL_NETWORK_DOMAIN}`);
console.log(`Server is running on ${process.env.PUBLIC_NETWORK_DOMAIN}`);
Expand Down
47 changes: 47 additions & 0 deletions middleware/authenticateToken.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
require("dotenv").config({ path: ".env.local" });
const redisClient = require("../redis");
const jwt = require("jsonwebtoken");
const { promisify } = require("util");

// 白名单配置 URL: Method
const whiteList = {
"/login": "POST", // 登录
"/register": "POST", // 注册
};

const authenticateToken = async (ctx, next) => {
const isWhite = whiteList[ctx.url];
if (isWhite === ctx.method) {
await next();
return;
}

try {
const token = ctx.headers["authorization"]?.replace("Bearer ", "");
const decoded = await promisify(jwt.verify)(token, process.env.JWT_SECRET);

if (!decoded.id) {
ctx.status = 403;
ctx.body = { message: "No token provided" };
return;
}

// Redis 查看是否存在
const redisToken = await redisClient.get(`user_login:${decoded.id}`);
if (!redisToken) {
ctx.status = 403;
ctx.body = { message: "Invalid token" };
return;
}

ctx.state.user = decoded;
ctx.state.token = token;
await next();
} catch (error) {
console.error(error);
ctx.status = 500;
ctx.body = { message: "Internal server error" };
}
};

module.exports = authenticateToken;
83 changes: 83 additions & 0 deletions models/users.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
const { DataTypes, Sequelize } = require("sequelize");
const sequelize = require("../utils/dbInstance"); // 修改为实际的sequelize实例路径
const UserStatus = {
ACTIVE: "ACTIVE", // 用户账号已激活
INACTIVE: "INACTIVE", // 用户账号未激活
BANNED: "BANNED", // 用户账号被封禁
PENDING: "PENDING", // 用户账号待审核
};

// 定义 User 模型
const Users = sequelize.define(
"User",
{
id: {
type: DataTypes.INTEGER,
autoIncrement: true,
primaryKey: true,
allowNull: false,
},
username: {
type: DataTypes.STRING,
allowNull: false,
collate: "utf8mb4_unicode_ci",
},
password: {
type: DataTypes.STRING,
allowNull: false,
collate: "utf8mb4_unicode_ci",
},
mail: {
type: DataTypes.STRING,
allowNull: true,
collate: "utf8mb4_unicode_ci",
},
verify_email: {
type: DataTypes.TINYINT.UNSIGNED.ZEROFILL,
allowNull: true,
},
status: {
type: DataTypes.ENUM,
values: Object.values(UserStatus),
allowNull: false,
defaultValue: UserStatus.ACTIVE,
collate: "utf8mb4_unicode_ci",
},
created_at: {
type: DataTypes.DATE,
allowNull: false,
defaultValue: DataTypes.NOW,
onUpdate: DataTypes.NOW,
},
created_by: {
type: DataTypes.STRING,
allowNull: true,
collate: "utf8mb4_unicode_ci",
},
is_login: {
allowNull: true,
type: DataTypes.BOOLEAN,
defaultValue: false,
},
login_at: {
type: DataTypes.DATE,
allowNull: true,
},
logout_at: {
type: DataTypes.DATE,
allowNull: true,
},
disk_size: {
type: DataTypes.DOUBLE,
allowNull: true,
defaultValue: 0,
},
},
{
tableName: "users",
timestamps: false,
charset: "utf8mb4",
collate: "utf8mb4_unicode_ci",
}
);
module.exports = Users;
7 changes: 5 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "upload-file-server",
"version": "1.1.0",
"name": "diskcloud-service",
"version": "2.0.0",
"main": "index.js",
"license": "MIT",
"engines": {
Expand All @@ -15,9 +15,11 @@
"dependencies": {
"@koa/cors": "^5.0.0",
"axios": "^1.7.2",
"bcrypt": "^5.1.1",
"dotenv": "^16.4.5",
"file-type": "^19.0.0",
"joi": "^17.13.3",
"jsonwebtoken": "^9.0.2",
"jszip": "^3.10.1",
"koa": "^2.15.3",
"koa-body": "^6.0.1",
Expand All @@ -27,6 +29,7 @@
"mysql2": "^3.10.1",
"nodemon": "^3.1.4",
"pm2": "^5.4.0",
"redis": "^4.6.13",
"sequelize": "^6.37.3",
"sharp": "0.31.0",
"tinify": "^1.7.1",
Expand Down
11 changes: 11 additions & 0 deletions redis/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
const redis = require("redis");
require("dotenv").config({ path: ".env.local" });

const client = redis.createClient({
url: `redis://${process.env.REDIS_HOST}:${process.env.REDIS_PORT}`,
});
client.on("error", (err) => {
console.error("Redis error:", err);
});

module.exports = client;
136 changes: 136 additions & 0 deletions routers/users.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
const Router = require("koa-router");
const redisClient = require("../redis");
const bcrypt = require("bcrypt");
const jwt = require("jsonwebtoken");
require("dotenv").config({ path: ".env.local" });
const Users = require("../models/users");
const { USERS_LOGIN_POST } = require("../types/schema/users");
const { validateBody } = require("../types");

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 } });
if (!user) {
ctx.status = 401;
ctx.body = { message: "Invalid credentials" };
return;
}

const passwordMatch = await bcrypt.compare(password, user.password);
if (!passwordMatch) {
ctx.status = 401;
ctx.body = { message: "Invalid credentials" };
return;
}

const token = jwt.sign(
{ id: user.id, username: user.username },
process.env.JWT_SECRET,
{ expiresIn: process.env.JWT_EXPIRES_IN }
);

jwt.verify(token, process.env.JWT_SECRET, (err, user) => {
if (err) {
console.log(err);
}
console.log(user);
});

// 将 token 存储在 Redis 中
await redisClient.set(`user_login:${user.id}`, token, {
EX: process.env.USER_LOGIN_TOKEN_EXPIRE_TIME,
});

user.update({
is_login: true,
login_at: new Date(),
});

ctx.body = { token };
} catch (error) {
console.error(error);
ctx.status = 500;
ctx.body = { message: "Internal server error" };
}
});

router.post("/register", validateBody(USERS_LOGIN_POST), async (ctx) => {
const { username, password } = ctx.request.body;

try {
const existingUser = await Users.findOne({ where: { username } });
if (existingUser) {
ctx.status = 400;
ctx.body = { message: "Username already taken" };
return;
}

const hashedPassword = await bcrypt.hash(password, 10);

const { id, disk_size, status, created_at, login_at } = await Users.create({
username,
password: hashedPassword,
});
ctx.status = 201;
ctx.body = { id, disk_size, status, created_at, username, login_at };
} catch (error) {
console.error(error);
ctx.status = 500;
ctx.body = { message: "Internal server error" };
}
});

router.get("/users/info", async (ctx) => {
try {
const user = await Users.findByPk(ctx.state.user.id, {
attributes: { exclude: ["password"] },
});
if (user) {
ctx.status = 200; // 确保状态码为 200
ctx.body = user.dataValues;
return;
}
if (!user) {
ctx.status = 404;
ctx.body = { message: "User not found" };
return;
}
} catch (error) {
console.error(error);
ctx.status = 500;
ctx.body = { message: "Internal server error" };
}
});

router.post("/logout", async (ctx) => {
const { id } = ctx.state.user;
if (!ctx.state.token) {
ctx.status = 400;
ctx.body = { message: "Token is required" };
return;
}

try {
const user = await Users.findOne({ where: { id } });

user.update({
is_login: false,
logout_at: new Date(),
});

// 从 Redis 中删除 token
await redisClient.del(`user_login:${id}`);

ctx.status = 200;
ctx.body = { message: "Logout successful" };
} catch (error) {
ctx.status = 500;
ctx.body = { message: "Internal server error" };
}
});

module.exports = router;
Loading