From 1d142c3900864c87658cb3b14ad68f3e510dd5bd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=B0=A2=E4=BA=9A=E6=98=95?= <1549469775@qq.com> Date: Wed, 18 Jun 2025 15:21:43 +0800 Subject: [PATCH] feat: add user authentication and registration features - Updated the users table migration to include a password field. - Enhanced UserModel with methods to find users by username and email. - Implemented user registration and login functionalities in UserService. - Added JWT authentication middleware with support for whitelisting and blacklisting routes. - Created a response time middleware for logging request durations. - Replaced the previous Send and ResponseTime plugins with new middleware implementations. - Introduced bcrypt utility functions for password hashing and comparison. - Added a base singleton utility class for managing single instances of classes. - Created a helper function for formatting API responses. - Set up a Dockerfile for containerizing the application with health checks and entrypoint script. - Added a basic HTML login and registration interface. --- Dockerfile | 29 ++++ bun.lockb | Bin 73807 -> 90420 bytes database/.gitkeep | 0 database/development.sqlite3 | Bin 24576 -> 4096 bytes database/development.sqlite3-shm | Bin 32768 -> 32768 bytes database/development.sqlite3-wal | Bin 4152 -> 74192 bytes entrypoint.sh | 18 ++ package.json | 8 +- public/aa.txt | 1 - public/index.html | 101 +++++++++++ src/controllers/JobController.js | 71 ++++---- src/controllers/StatusController.js | 32 ++-- src/controllers/userController.js | 57 +++++-- .../20250616065041_create_users_table.mjs | 1 + src/db/models/UserModel.js | 7 + src/main.js | 6 +- src/middlewares/Auth/auth.js | 68 ++++++++ src/middlewares/Auth/index.js | 3 + src/middlewares/Auth/jwt.js | 3 + src/middlewares/ResponseTime/index.js | 19 +++ src/middlewares/Send/index.js | 185 +++++++++++++++++++++ src/middlewares/Send/resolve-path.js | 74 +++++++++ src/middlewares/errorHandler/index.js | 38 +++++ src/middlewares/install.js | 33 ++++ src/plugins/ResponseTime/index.js | 19 --- src/plugins/Send/index.js | 185 --------------------- src/plugins/Send/resolve-path.js | 74 --------- src/plugins/errorHandler/index.js | 38 ----- src/plugins/install.js | 23 --- src/services/JobService.js | 28 ++-- src/services/userService.js | 105 ++++++++---- src/utils/BaseSingleton.js | 37 +++++ src/utils/autoRegister.js | 2 +- src/utils/bcrypt.js | 11 ++ src/utils/helper.js | 4 + 35 files changed, 821 insertions(+), 459 deletions(-) create mode 100644 Dockerfile create mode 100644 database/.gitkeep create mode 100644 entrypoint.sh delete mode 100644 public/aa.txt create mode 100644 public/index.html create mode 100644 src/middlewares/Auth/auth.js create mode 100644 src/middlewares/Auth/index.js create mode 100644 src/middlewares/Auth/jwt.js create mode 100644 src/middlewares/ResponseTime/index.js create mode 100644 src/middlewares/Send/index.js create mode 100644 src/middlewares/Send/resolve-path.js create mode 100644 src/middlewares/errorHandler/index.js create mode 100644 src/middlewares/install.js delete mode 100644 src/plugins/ResponseTime/index.js delete mode 100644 src/plugins/Send/index.js delete mode 100644 src/plugins/Send/resolve-path.js delete mode 100644 src/plugins/errorHandler/index.js delete mode 100644 src/plugins/install.js create mode 100644 src/utils/BaseSingleton.js create mode 100644 src/utils/bcrypt.js create mode 100644 src/utils/helper.js diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..fec5140 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,29 @@ +# 使用官方 Bun 运行时的轻量级镜像 +FROM oven/bun:alpine as base + +WORKDIR /app + +# 仅复制生产依赖相关文件 +COPY package.json bun.lockb knexfile.mjs .npmrc ./ + +# 安装依赖(生产环境) +RUN bun install --production + +# 复制应用代码和静态资源 +COPY src ./src +COPY public ./public + +COPY entrypoint.sh ./entrypoint.sh +RUN chmod +x ./entrypoint.sh + +# 如需数据库文件(如 SQLite),可挂载到宿主机 +VOLUME /app/database + +# 启动命令(如有端口需求可暴露端口) +EXPOSE 3000 + +# 健康检查:每30秒检查一次服务端口,3次失败则容器为unhealthy +HEALTHCHECK --interval=30s --timeout=5s --retries=3 \ + CMD wget --spider -q http://localhost:3000/ || exit 1 + +ENTRYPOINT ["./entrypoint.sh"] diff --git a/bun.lockb b/bun.lockb index 27399468c11ad9e9ae05dc61ce59fd25766f9c18..7be0543fc0c2cb825561f7342a10e23dc012437f 100644 GIT binary patch delta 20743 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 G;@Vxv7bBZFSiB3>YmL143>z%+gUe#{2R literal 24576 zcmeI&%Tv=p90%}CLWxx|jyRL+jwd?hkR0cO5C&7=GHb@#Km>^55)D|X1~jvu%zq5(FRs0SG_< z0uX=z1Rwx`s}o=;RnO^CN432MKZx2qXq((+wqr(~y&v%;XLM<^Qm$6$_S*8sc7-ml zJ*~W;*7?d7^}UmPi%wWc94m8lXGGO?U3wRVtmbqhCT&wEVXNiEmC7IO?1n{cFXT-g zP|pvk7dZ}X1-8qAL%PQgDT_ki_UZw5xff1S%VRFL=xY|#cUdrzGmK2Hoha>g;cHcYja8aLc|9N0uX=z1Rwwb2tWV= z5P$##AOL|IAz(-;@;sfIHD-%ODOWN|MWc{2yE{K-cUvpjrI>bKwWarOKGa(A`d`sL z6YYogRa}rD009U<00Izz00bZa0SG_<0uZ>y0%=9jrDTOKKaiDddf?RpT~dmJdC4mo z-T(iOiFSOA2Zydf00Izz00bZa0SG_<0uX=z1R(Ig0*a)NTk-l|*4`5BlQjAuzczv$O$erALU6@9$3;rKwzr8jt7l zg}FjuNY;|15`9JlC_8qYv-tGyL2ipt$=Wxf{S+r82tWV=5P$##AOHafKmY;|fB*z; UlEAok`rIzTZ=kz@@&Et; diff --git a/database/development.sqlite3-shm b/database/development.sqlite3-shm index 2553477ce099670b4cce97a16361bf2bc42e8dbd..30b59383a59677fa2f9fd05e32d9b064c39c5d84 100644 GIT binary patch delta 281 zcmZo@U}|V!s+V}A%K!q55G)`Bq}hS^m07q_>Dnz1Iu?s2?BWX3X+1W9`?=$`K2p^K zjRFIZx&M&>RM?M!56S{LfC-41ff&SQfwDm~vK)xd3dC$c400$)J)FOBqM;KjBTz5L z#)&_fn3)(jH%|P`#Lf&<&BegKapHF-R#u=cZm=v5g8)dDnU_HjO!F}aZJfxh4FJDk BIzj*d delta 159 zcmZo@U}|V!s+V}A%K!t63=9H5%)kc3_UWN_51XhkENiVfqO;ZhfZ2p()`Hvz-$+%D hWH!TpBmfoXVgOkP0xTOZ>N;(F;Ka$uxbY&p2>`eGEYJV| diff --git a/database/development.sqlite3-wal b/database/development.sqlite3-wal index bea45f352cfa20e21b3a447b52ef0097834a5572..8e6f5110f07848d4999d1befd4fa78434d7be2b0 100644 GIT binary patch literal 74192 zcmeI*Uu;uV9KiA0c3mj}iYTc*s8<5XI=0b2Yo`;Hfi(ePjL|WCvAt_ATcLkOd+Uag zkS?IegAzmJK@$@o#1~DByl6~N6GeI80VT$m5QB*reLxZuL6msTU0d3^@Xwgg`aK!9 zz31L@?>(PpBt1Ll_v_i!I;C`;yH`=>E3)i8yfF6R>`fire;$3~zTUlmh$>5U{kyO4 ze%$`SsrHQ<)+P;I-IC3vHA4-E)>f;sLRH1NR`Jg!+mwoI-%{QYujcit_Gfx)Rl0JA z=87ZMc^4E{pV&|kKmY**5I_I{1Q0*~0R#~E?*#^|t?s2uEzcO*NJ`IdPl@lo(Y(IB zpl9OsyS7z>;l4;%jr6Tt8&>PiZOdqBy+gG06qj2Z@{&`_%xiAT=#$ZOax|wI$!sQH zw`;ArW~}aLOl^xL6EQWJG4xSAr)IK-nkl4G>Ub`h)^a=4ZTb#XD;U{iCZ5yNddBEb zWAZ^_>T_ByKBnc`{K23{RE}sye5`UIQBirKsF9TCCp1Hs?>vHBe$TIr7aY02|5wM@ zo}&EDUK&b}?f8{IKa2 z0tg_000IagfB*srAb0R#|0009ILKmY**8d{*Z$mVbdgO=jxOcUy(scd{(-Mo3TuI0K@>&;Wl`SqEE zK3SO=e{)mplXYk@AoSysmpV?rb$WVS;ELk9(%2p6=@CEx0R#|0009ILKmY** z5I|r?0h`69%o0ax;tLF3K7I1Qr+58wgZKiDsw>*X``eB$&^l+J6klNOgL6I-I}`*E zKmY**5I_I{1Q2L=fvv3$caPgrygMlugctI9E*~A&^7)BuE)gwck|HIXS?MS(Dr#q@t@oT=A$m zL$uRIMMAn#&O5OyQrn5-bcv{DTuW1@CiE>@A!Vqmh6V@2{gG&7^}6uJNZ+~v(V#Fc zv)SEj0}t^9WbVVdzsc9_-uZb7FI|3W&wFog`DA*&z$wLbs^R}jq5}vZfB*srAbCnk^wzxPVDa3W!vdxz zzQBp^&iXI@{!ND*7qGg%67dDZhJpYB2q1s}0tg_000IagfB*uGBCx<}TUsAW;70KU zlycm`d;a?E$kz+@>%=Z3c<9C1vXXHD zsX##h0R#|0009ILKmY**5I~@r1!QgkM|oUe@a4z@Ez2(~XI!9}KXUqp00IagfB*sr zAbsm?>cn$uv|xAbA4&X9TYDV1Q0*~0R#|0009ILKmY**5V(^C zX4!0RrJ)u6=iMr+GF65I_I{1Q0*~0R#|0009J= zMZmB)75BoTT?qty{(#TiEcTBq_IVeNbj6?ExP4{!WYF*L+q799igZQNx;C+SO+LB) z`LU4h3ruzm4dfr0=!tBI$0vsK=~YRYtgzD?>hwLVdV6|(f!=Qa3{^s}CceOdgPqwI zmONIF>j*5aON#iRAb + + + + + 登录 / 注册 + + + +
+
+
登录
+
注册
+
+
+
+
+ + +
+
+ + +
+ +
+ +
+ + + diff --git a/src/controllers/JobController.js b/src/controllers/JobController.js index be3cb2b..3348cf8 100644 --- a/src/controllers/JobController.js +++ b/src/controllers/JobController.js @@ -1,41 +1,56 @@ // Job Controller 示例:如何调用 service 层动态控制和查询定时任务 import JobService from "services/JobService.js" -import Router from "utils/router.js" +import { formatResponse } from "utils/helper.js" -class JobController { - static routes() { - const router = new Router({ prefix: "/api/jobs" }) - router.get("/", JobController.list) - router.post("/start/:id", JobController.start) - router.post("/stop/:id", JobController.stop) - router.post("/update/:id", JobController.updateCron) - return router - } +const jobService = new JobService() - static async list(ctx) { - ctx.body = JobService.listJobs() +export const list = async (ctx) => { + try { + const data = jobService.listJobs() + ctx.body = formatResponse(true, data) + } catch (err) { + ctx.body = formatResponse(false, null, err.message || "获取任务列表失败") } +} - static async start(ctx) { - const { id } = ctx.params - JobService.startJob(id) - ctx.body = { success: true, message: `${id} 任务已启动` } +export const start = async (ctx) => { + const { id } = ctx.params + try { + jobService.startJob(id) + ctx.body = formatResponse(true, null, null, `${id} 任务已启动`) + } catch (err) { + ctx.body = formatResponse(false, null, err.message || "启动任务失败") } +} - static async stop(ctx) { - const { id } = ctx.params - JobService.stopJob(id) - ctx.body = { success: true, message: `${id} 任务已停止` } +export const stop = async (ctx) => { + const { id } = ctx.params + try { + jobService.stopJob(id) + ctx.body = formatResponse(true, null, null, `${id} 任务已停止`) + } catch (err) { + ctx.body = formatResponse(false, null, err.message || "停止任务失败") } +} - static async updateCron(ctx) { - const { id } = ctx.params - const { cronTime } = ctx.request.body - JobService.updateJobCron(id, cronTime) - ctx.body = { success: true, message: `${id} 任务频率已修改` } +export const updateCron = async (ctx) => { + const { id } = ctx.params + const { cronTime } = ctx.request.body + try { + jobService.updateJobCron(id, cronTime) + ctx.body = formatResponse(true, null, null, `${id} 任务频率已修改`) + } catch (err) { + ctx.body = formatResponse(false, null, err.message || "修改任务频率失败") } } -export default JobController - -// 你可以在路由中引入这些 controller 方法,实现接口调用 +// 路由注册示例 +import Router from "utils/router.js" +export function createRoutes() { + const router = new Router({ prefix: "/api/jobs" }) + router.get("/", list) + router.post("/start/:id", start) + router.post("/stop/:id", stop) + router.post("/update/:id", updateCron) + return router +} diff --git a/src/controllers/StatusController.js b/src/controllers/StatusController.js index f6775ed..5239ec0 100644 --- a/src/controllers/StatusController.js +++ b/src/controllers/StatusController.js @@ -1,22 +1,16 @@ -import Router from 'utils/router.js'; +import { formatResponse } from "utils/helper.js" -class StatusController { - static routes() { - const v1 = new Router({ prefix: "/api/v1" }) - - // 组内中间件 - v1.use((ctx, next) => { - ctx.set("X-API-Version", "v1") - return next() - }) - - v1.get("/status", StatusController.status) - return v1 - } - - static async status(ctx) { - ctx.body = "OK" - } +export const status = async (ctx) => { + ctx.body = "OK" } -export default StatusController +import Router from "utils/router.js" +export function createRoutes() { + const v1 = new Router({ prefix: "/api/v1" }) + v1.use((ctx, next) => { + ctx.set("X-API-Version", "v1") + return next() + }) + v1.get("/status", status) + return v1 +} diff --git a/src/controllers/userController.js b/src/controllers/userController.js index 91a9527..b00549d 100644 --- a/src/controllers/userController.js +++ b/src/controllers/userController.js @@ -1,23 +1,44 @@ -import UserService from 'services/UserService.js'; -import Router from 'utils/router.js'; +import UserService from "services/UserService.js" +import { formatResponse } from "utils/helper.js" -class UserController { - static routes() { - let router = new Router({ prefix: '/api' }); - router.get('/hello', UserController.hello); - router.get('/user/:id', UserController.getUser); - return router; - } +const userService = new UserService() - static async hello(ctx) { - ctx.body = 'Hello World'; - } +export const hello = async (ctx) => { + ctx.body = formatResponse(true, "Hello World") +} + +export const getUser = async (ctx) => { + const user = await userService.getUserById(ctx.params.id) + ctx.body = formatResponse(true, user) +} - static async getUser(ctx) { - // 调用 service 层获取用户 - const user = await UserService.getUserById(ctx.params.id); - ctx.body = user; - } +export const register = async (ctx) => { + try { + const { username, email, password } = ctx.request.body + const user = await userService.register({ username, email, password }) + ctx.body = formatResponse(true, user) + } catch (err) { + ctx.body = formatResponse(false, null, err.message) + } } -export default UserController; \ No newline at end of file +export const login = async (ctx) => { + try { + const { username, email, password } = ctx.request.body + const result = await userService.login({ username, email, password }) + ctx.body = formatResponse(true, result) + } catch (err) { + ctx.body = formatResponse(false, null, err.message) + } +} + +// 路由注册示例 +import Router from "utils/router.js" +export function createRoutes() { + const router = new Router({ prefix: "/api" }) + router.get("/hello", hello) + router.get("/user/:id", getUser) + router.post("/register", register) + router.post("/login", login) + return router +} diff --git a/src/db/migrations/20250616065041_create_users_table.mjs b/src/db/migrations/20250616065041_create_users_table.mjs index 56f7418..f73fcae 100644 --- a/src/db/migrations/20250616065041_create_users_table.mjs +++ b/src/db/migrations/20250616065041_create_users_table.mjs @@ -7,6 +7,7 @@ export const up = async knex => { table.increments("id").primary() // 自增主键 table.string("username", 100).notNullable() // 字符串字段(最大长度100) table.string("email", 100).unique().notNullable() // 唯一邮箱 + table.string("password", 100).unique() // 密码 table.integer("age").unsigned() // 无符号整数 table.timestamp("created_at").defaultTo(knex.fn.now()) // 创建时间 table.timestamp("updated_at").defaultTo(knex.fn.now()) // 更新时间 diff --git a/src/db/models/UserModel.js b/src/db/models/UserModel.js index f263586..19de0ac 100644 --- a/src/db/models/UserModel.js +++ b/src/db/models/UserModel.js @@ -20,6 +20,13 @@ class UserModel { static async delete(id) { return db("users").where("id", id).del() } + + static async findByUsername(username) { + return db("users").where("username", username).first() + } + static async findByEmail(email) { + return db("users").where("email", email).first() + } } export default UserModel diff --git a/src/main.js b/src/main.js index b6f4296..65e5d4b 100644 --- a/src/main.js +++ b/src/main.js @@ -8,14 +8,16 @@ import os from "os" import log4js from "log4js" // 应用插件与自动路由 -import LoadPlugins from "./plugins/install.js" +import LoadMiddlewares from "./middlewares/install.js" import { autoRegisterControllers } from "utils/autoRegister.js" +import bodyParser from "koa-bodyparser" const logger = log4js.getLogger() const app = new Koa() +app.use(bodyParser()); // 注册插件 -LoadPlugins(app) +LoadMiddlewares(app) // 自动注册所有 controller autoRegisterControllers(app) diff --git a/src/middlewares/Auth/auth.js b/src/middlewares/Auth/auth.js new file mode 100644 index 0000000..779d3fd --- /dev/null +++ b/src/middlewares/Auth/auth.js @@ -0,0 +1,68 @@ +// JWT 鉴权中间件,支持白名单和黑名单,白名单/黑名单支持glob语法,白名单可指定是否校验权限(auth: true/false/"try") +import jwt from "./jwt" +import { minimatch } from "minimatch" + +export const JWT_SECRET = process.env.JWT_SECRET || "jwt-demo-secret" + +function matchList(list, path) { + for (const item of list) { + if (typeof item === "string" && minimatch(path, item)) { + return { matched: true, auth: false } + } + if (typeof item === "object" && minimatch(path, item.pattern)) { + return { matched: true, auth: item.auth } + } + } + return { matched: false } +} + +function verifyToken(ctx) { + const token = ctx.headers["authorization"]?.replace(/^Bearer\s/, "") + if (!token) return { ok: false } + try { + ctx.state.user = jwt.verify(token, JWT_SECRET) + return { ok: true } + } catch { + ctx.state.user = undefined + return { ok: false } + } +} + +export default function authMiddleware(options = { + whiteList: [], + blackList: [] +}) { + return async (ctx, next) => { + // 黑名单优先生效 + if (matchList(options.blackList, ctx.path).matched) { + ctx.status = 403 + ctx.body = { success: false, error: "禁止访问" } + return + } + // 白名单处理 + const white = matchList(options.whiteList, ctx.path) + if (white.matched) { + if (white.auth === false) { + return await next() + } + if (white.auth === "try") { + verifyToken(ctx) // token可选,校验失败不报错 + return await next() + } + // true 或其他情况,必须有token + if (!verifyToken(ctx).ok) { + ctx.status = 401 + ctx.body = { success: false, error: "未登录或token缺失或无效" } + return + } + return await next() + } + // 非白名单,必须有token + if (!verifyToken(ctx).ok) { + ctx.status = 401 + ctx.body = { success: false, error: "未登录或token缺失或无效" } + return + } + await next() + } +} diff --git a/src/middlewares/Auth/index.js b/src/middlewares/Auth/index.js new file mode 100644 index 0000000..7e8009b --- /dev/null +++ b/src/middlewares/Auth/index.js @@ -0,0 +1,3 @@ +// 统一导出所有中间件 +import auth from "./auth.js" +export { auth } diff --git a/src/middlewares/Auth/jwt.js b/src/middlewares/Auth/jwt.js new file mode 100644 index 0000000..0af32e5 --- /dev/null +++ b/src/middlewares/Auth/jwt.js @@ -0,0 +1,3 @@ +// 兼容性导出,便于后续扩展 +import jwt from "jsonwebtoken" +export default jwt diff --git a/src/middlewares/ResponseTime/index.js b/src/middlewares/ResponseTime/index.js new file mode 100644 index 0000000..2f01a63 --- /dev/null +++ b/src/middlewares/ResponseTime/index.js @@ -0,0 +1,19 @@ +import log4js from "log4js"; + +const logger = log4js.getLogger(); + +/** + * 响应时间记录中间件 + * @param {Object} ctx - Koa上下文对象 + * @param {Function} next - Koa中间件链函数 + */ +export default async (ctx, next) => { + logger.info("====================[REQ]===================="); + logger.info(`➡️ ${ctx.method} ${ctx.path}`); + const start = Date.now(); + await next(); + const ms = Date.now() - start; + ctx.set("X-Response-Time", `${ms}ms`); + logger.info(`⬅️ ${ctx.method} ${ctx.url} | ⏱️ ${ms}ms`); + logger.info("====================[END]====================\n"); +} diff --git a/src/middlewares/Send/index.js b/src/middlewares/Send/index.js new file mode 100644 index 0000000..1502d3f --- /dev/null +++ b/src/middlewares/Send/index.js @@ -0,0 +1,185 @@ +/** + * koa-send@5.0.1 转换为ES Module版本 + * 静态资源服务中间件 + */ +import fs from 'fs'; +import { promisify } from 'util'; +import logger from 'log4js'; +import resolvePath from './resolve-path.js'; +import createError from 'http-errors'; +import assert from 'assert'; +import { normalize, basename, extname, resolve, parse, sep } from 'path'; +import { fileURLToPath } from 'url'; +import path from "path" + +// 转换为ES Module格式 +const log = logger.getLogger('koa-send'); +const stat = promisify(fs.stat); +const access = promisify(fs.access); +const __dirname = path.dirname(fileURLToPath(import.meta.url)); + +/** + * 检查文件是否存在 + * @param {string} path - 文件路径 + * @returns {Promise} 文件是否存在 + */ +async function exists(path) { + try { + await access(path); + return true; + } catch (e) { + return false; + } +} + +/** + * 发送文件给客户端 + * @param {Context} ctx - Koa上下文对象 + * @param {String} path - 文件路径 + * @param {Object} [opts] - 配置选项 + * @returns {Promise} - 异步Promise + */ +async function send(ctx, path, opts = {}) { + assert(ctx, 'koa context required'); + assert(path, 'pathname required'); + + // 移除硬编码的public目录,要求必须通过opts.root配置 + const root = opts.root; + if (!root) { + throw new Error('Static root directory must be configured via opts.root'); + } + const trailingSlash = path[path.length - 1] === '/'; + path = path.substr(parse(path).root.length); + const index = opts.index || 'index.html'; + const maxage = opts.maxage || opts.maxAge || 0; + const immutable = opts.immutable || false; + const hidden = opts.hidden || false; + const format = opts.format !== false; + const extensions = Array.isArray(opts.extensions) ? opts.extensions : false; + const brotli = opts.brotli !== false; + const gzip = opts.gzip !== false; + const setHeaders = opts.setHeaders; + + if (setHeaders && typeof setHeaders !== 'function') { + throw new TypeError('option setHeaders must be function'); + } + + // 解码路径 + path = decode(path); + if (path === -1) return ctx.throw(400, 'failed to decode'); + + // 索引文件支持 + if (index && trailingSlash) path += index; + + path = resolvePath(root, path); + + // 隐藏文件支持 + if (!hidden && isHidden(root, path)) return; + + let encodingExt = ''; + // 尝试提供压缩文件 + if (ctx.acceptsEncodings('br', 'identity') === 'br' && brotli && (await exists(path + '.br'))) { + path = path + '.br'; + ctx.set('Content-Encoding', 'br'); + ctx.res.removeHeader('Content-Length'); + encodingExt = '.br'; + } else if (ctx.acceptsEncodings('gzip', 'identity') === 'gzip' && gzip && (await exists(path + '.gz'))) { + path = path + '.gz'; + ctx.set('Content-Encoding', 'gzip'); + ctx.res.removeHeader('Content-Length'); + encodingExt = '.gz'; + } + + // 尝试添加文件扩展名 + if (extensions && !/\./.exec(basename(path))) { + const list = [].concat(extensions); + for (let i = 0; i < list.length; i++) { + let ext = list[i]; + if (typeof ext !== 'string') { + throw new TypeError('option extensions must be array of strings or false'); + } + if (!/^\./.exec(ext)) ext = `.${ext}`; + if (await exists(`${path}${ext}`)) { + path = `${path}${ext}`; + break; + } + } + } + + // 获取文件状态 + let stats; + try { + stats = await stat(path); + + // 处理目录 + if (stats.isDirectory()) { + if (format && index) { + path += `/${index}`; + stats = await stat(path); + } else { + return; + } + } + } catch (err) { + const notfound = ['ENOENT', 'ENAMETOOLONG', 'ENOTDIR']; + if (notfound.includes(err.code)) { + throw createError(404, err); + } + err.status = 500; + throw err; + } + + if (setHeaders) setHeaders(ctx.res, path, stats); + + // 设置响应头 + ctx.set('Content-Length', stats.size); + if (!ctx.response.get('Last-Modified')) ctx.set('Last-Modified', stats.mtime.toUTCString()); + if (!ctx.response.get('Cache-Control')) { + const directives = [`max-age=${(maxage / 1000) | 0}`]; + if (immutable) directives.push('immutable'); + ctx.set('Cache-Control', directives.join(',')); + } + if (!ctx.type) ctx.type = type(path, encodingExt); + ctx.body = fs.createReadStream(path); + + return path; +} + +/** + * 检查是否为隐藏文件 + * @param {string} root - 根目录 + * @param {string} path - 文件路径 + * @returns {boolean} 是否为隐藏文件 + */ +function isHidden(root, path) { + path = path.substr(root.length).split(sep); + for (let i = 0; i < path.length; i++) { + if (path[i][0] === '.') return true; + } + return false; +} + +/** + * 获取文件类型 + * @param {string} file - 文件路径 + * @param {string} ext - 编码扩展名 + * @returns {string} 文件MIME类型 + */ +function type(file, ext) { + return ext !== '' ? extname(basename(file, ext)) : extname(file); +} + +/** + * 解码URL路径 + * @param {string} path - 需要解码的路径 + * @returns {string|number} 解码后的路径或错误代码 + */ +function decode(path) { + try { + return decodeURIComponent(path); + } catch (err) { + return -1; + } +} + +export default send; diff --git a/src/middlewares/Send/resolve-path.js b/src/middlewares/Send/resolve-path.js new file mode 100644 index 0000000..9c6dce6 --- /dev/null +++ b/src/middlewares/Send/resolve-path.js @@ -0,0 +1,74 @@ +/*! + * resolve-path + * Copyright(c) 2014 Jonathan Ong + * Copyright(c) 2015-2018 Douglas Christopher Wilson + * MIT Licensed + */ + +/** + * ES Module 转换版本 + * 路径解析工具,防止路径遍历攻击 + */ +import createError from 'http-errors'; +import { join, normalize, resolve, sep } from 'path'; +import pathIsAbsolute from 'path-is-absolute'; + +/** + * 模块变量 + * @private + */ +const UP_PATH_REGEXP = /(?:^|[\/])\.\.(?:[\/]|$)/; + +/** + * 解析相对路径到根路径 + * @param {string} rootPath - 根目录路径 + * @param {string} relativePath - 相对路径 + * @returns {string} 解析后的绝对路径 + * @public + */ +function resolvePath(rootPath, relativePath) { + let path = relativePath; + let root = rootPath; + + // root是可选的,类似于root.resolve + if (arguments.length === 1) { + path = rootPath; + root = process.cwd(); + } + + if (root == null) { + throw new TypeError('argument rootPath is required'); + } + + if (typeof root !== 'string') { + throw new TypeError('argument rootPath must be a string'); + } + + if (path == null) { + throw new TypeError('argument relativePath is required'); + } + + if (typeof path !== 'string') { + throw new TypeError('argument relativePath must be a string'); + } + + // 包含NULL字节是恶意的 + if (path.indexOf('\0') !== -1) { + throw createError(400, 'Malicious Path'); + } + + // 路径绝不能是绝对路径 + if (pathIsAbsolute.posix(path) || pathIsAbsolute.win32(path)) { + throw createError(400, 'Malicious Path'); + } + + // 路径超出根目录 + if (UP_PATH_REGEXP.test(normalize('.' + sep + path))) { + throw createError(403); + } + + // 拼接相对路径 + return normalize(join(resolve(root), path)); +} + +export default resolvePath; diff --git a/src/middlewares/errorHandler/index.js b/src/middlewares/errorHandler/index.js new file mode 100644 index 0000000..d643593 --- /dev/null +++ b/src/middlewares/errorHandler/index.js @@ -0,0 +1,38 @@ +// src/plugins/errorHandler.js +// 错误处理中间件插件 + +function formatError(ctx, status, message) { + const accept = ctx.accepts('json', 'html', 'text'); + if (accept === 'json') { + ctx.type = 'application/json'; + ctx.body = { success: false, error: message }; + } else if (accept === 'html') { + ctx.type = 'html'; + ctx.body = ` + + ${status} Error + +

${status} Error

+

${message}

+ + + `; + } else { + ctx.type = 'text'; + ctx.body = `${status} - ${message}`; + } + ctx.status = status; +} + +export default function errorHandler() { + return async (ctx, next) => { + try { + await next(); + if (ctx.status === 404) { + formatError(ctx, 404, 'Resource not found'); + } + } catch (err) { + formatError(ctx, err.statusCode || 500, err.message || err || 'Internal server error'); + } + }; +} diff --git a/src/middlewares/install.js b/src/middlewares/install.js new file mode 100644 index 0000000..0db2696 --- /dev/null +++ b/src/middlewares/install.js @@ -0,0 +1,33 @@ +import ResponseTime from "./ResponseTime" +import Send from "./Send" +import { resolve } from "path" +import { fileURLToPath } from "url" +import path from "path" +import errorHandler from "./errorHandler" +import { auth } from "./Auth" + +const __dirname = path.dirname(fileURLToPath(import.meta.url)) +const publicPath = resolve(__dirname, "../../public") + +export default app => { + app.use(errorHandler()) + app.use(ResponseTime) + app.use( + auth({ + whiteList: [ + { pattern: "/", auth: "try" }, + "/api/login", + "/api/register" + ], + blackList: [], + }) + ) + app.use(async (ctx, next) => { + try { + await Send(ctx, ctx.path, { root: publicPath }) + } catch (err) { + if (err.status !== 404) throw err + } + await next() + }) +} diff --git a/src/plugins/ResponseTime/index.js b/src/plugins/ResponseTime/index.js deleted file mode 100644 index 2f01a63..0000000 --- a/src/plugins/ResponseTime/index.js +++ /dev/null @@ -1,19 +0,0 @@ -import log4js from "log4js"; - -const logger = log4js.getLogger(); - -/** - * 响应时间记录中间件 - * @param {Object} ctx - Koa上下文对象 - * @param {Function} next - Koa中间件链函数 - */ -export default async (ctx, next) => { - logger.info("====================[REQ]===================="); - logger.info(`➡️ ${ctx.method} ${ctx.path}`); - const start = Date.now(); - await next(); - const ms = Date.now() - start; - ctx.set("X-Response-Time", `${ms}ms`); - logger.info(`⬅️ ${ctx.method} ${ctx.url} | ⏱️ ${ms}ms`); - logger.info("====================[END]====================\n"); -} diff --git a/src/plugins/Send/index.js b/src/plugins/Send/index.js deleted file mode 100644 index 1502d3f..0000000 --- a/src/plugins/Send/index.js +++ /dev/null @@ -1,185 +0,0 @@ -/** - * koa-send@5.0.1 转换为ES Module版本 - * 静态资源服务中间件 - */ -import fs from 'fs'; -import { promisify } from 'util'; -import logger from 'log4js'; -import resolvePath from './resolve-path.js'; -import createError from 'http-errors'; -import assert from 'assert'; -import { normalize, basename, extname, resolve, parse, sep } from 'path'; -import { fileURLToPath } from 'url'; -import path from "path" - -// 转换为ES Module格式 -const log = logger.getLogger('koa-send'); -const stat = promisify(fs.stat); -const access = promisify(fs.access); -const __dirname = path.dirname(fileURLToPath(import.meta.url)); - -/** - * 检查文件是否存在 - * @param {string} path - 文件路径 - * @returns {Promise} 文件是否存在 - */ -async function exists(path) { - try { - await access(path); - return true; - } catch (e) { - return false; - } -} - -/** - * 发送文件给客户端 - * @param {Context} ctx - Koa上下文对象 - * @param {String} path - 文件路径 - * @param {Object} [opts] - 配置选项 - * @returns {Promise} - 异步Promise - */ -async function send(ctx, path, opts = {}) { - assert(ctx, 'koa context required'); - assert(path, 'pathname required'); - - // 移除硬编码的public目录,要求必须通过opts.root配置 - const root = opts.root; - if (!root) { - throw new Error('Static root directory must be configured via opts.root'); - } - const trailingSlash = path[path.length - 1] === '/'; - path = path.substr(parse(path).root.length); - const index = opts.index || 'index.html'; - const maxage = opts.maxage || opts.maxAge || 0; - const immutable = opts.immutable || false; - const hidden = opts.hidden || false; - const format = opts.format !== false; - const extensions = Array.isArray(opts.extensions) ? opts.extensions : false; - const brotli = opts.brotli !== false; - const gzip = opts.gzip !== false; - const setHeaders = opts.setHeaders; - - if (setHeaders && typeof setHeaders !== 'function') { - throw new TypeError('option setHeaders must be function'); - } - - // 解码路径 - path = decode(path); - if (path === -1) return ctx.throw(400, 'failed to decode'); - - // 索引文件支持 - if (index && trailingSlash) path += index; - - path = resolvePath(root, path); - - // 隐藏文件支持 - if (!hidden && isHidden(root, path)) return; - - let encodingExt = ''; - // 尝试提供压缩文件 - if (ctx.acceptsEncodings('br', 'identity') === 'br' && brotli && (await exists(path + '.br'))) { - path = path + '.br'; - ctx.set('Content-Encoding', 'br'); - ctx.res.removeHeader('Content-Length'); - encodingExt = '.br'; - } else if (ctx.acceptsEncodings('gzip', 'identity') === 'gzip' && gzip && (await exists(path + '.gz'))) { - path = path + '.gz'; - ctx.set('Content-Encoding', 'gzip'); - ctx.res.removeHeader('Content-Length'); - encodingExt = '.gz'; - } - - // 尝试添加文件扩展名 - if (extensions && !/\./.exec(basename(path))) { - const list = [].concat(extensions); - for (let i = 0; i < list.length; i++) { - let ext = list[i]; - if (typeof ext !== 'string') { - throw new TypeError('option extensions must be array of strings or false'); - } - if (!/^\./.exec(ext)) ext = `.${ext}`; - if (await exists(`${path}${ext}`)) { - path = `${path}${ext}`; - break; - } - } - } - - // 获取文件状态 - let stats; - try { - stats = await stat(path); - - // 处理目录 - if (stats.isDirectory()) { - if (format && index) { - path += `/${index}`; - stats = await stat(path); - } else { - return; - } - } - } catch (err) { - const notfound = ['ENOENT', 'ENAMETOOLONG', 'ENOTDIR']; - if (notfound.includes(err.code)) { - throw createError(404, err); - } - err.status = 500; - throw err; - } - - if (setHeaders) setHeaders(ctx.res, path, stats); - - // 设置响应头 - ctx.set('Content-Length', stats.size); - if (!ctx.response.get('Last-Modified')) ctx.set('Last-Modified', stats.mtime.toUTCString()); - if (!ctx.response.get('Cache-Control')) { - const directives = [`max-age=${(maxage / 1000) | 0}`]; - if (immutable) directives.push('immutable'); - ctx.set('Cache-Control', directives.join(',')); - } - if (!ctx.type) ctx.type = type(path, encodingExt); - ctx.body = fs.createReadStream(path); - - return path; -} - -/** - * 检查是否为隐藏文件 - * @param {string} root - 根目录 - * @param {string} path - 文件路径 - * @returns {boolean} 是否为隐藏文件 - */ -function isHidden(root, path) { - path = path.substr(root.length).split(sep); - for (let i = 0; i < path.length; i++) { - if (path[i][0] === '.') return true; - } - return false; -} - -/** - * 获取文件类型 - * @param {string} file - 文件路径 - * @param {string} ext - 编码扩展名 - * @returns {string} 文件MIME类型 - */ -function type(file, ext) { - return ext !== '' ? extname(basename(file, ext)) : extname(file); -} - -/** - * 解码URL路径 - * @param {string} path - 需要解码的路径 - * @returns {string|number} 解码后的路径或错误代码 - */ -function decode(path) { - try { - return decodeURIComponent(path); - } catch (err) { - return -1; - } -} - -export default send; diff --git a/src/plugins/Send/resolve-path.js b/src/plugins/Send/resolve-path.js deleted file mode 100644 index 9c6dce6..0000000 --- a/src/plugins/Send/resolve-path.js +++ /dev/null @@ -1,74 +0,0 @@ -/*! - * resolve-path - * Copyright(c) 2014 Jonathan Ong - * Copyright(c) 2015-2018 Douglas Christopher Wilson - * MIT Licensed - */ - -/** - * ES Module 转换版本 - * 路径解析工具,防止路径遍历攻击 - */ -import createError from 'http-errors'; -import { join, normalize, resolve, sep } from 'path'; -import pathIsAbsolute from 'path-is-absolute'; - -/** - * 模块变量 - * @private - */ -const UP_PATH_REGEXP = /(?:^|[\/])\.\.(?:[\/]|$)/; - -/** - * 解析相对路径到根路径 - * @param {string} rootPath - 根目录路径 - * @param {string} relativePath - 相对路径 - * @returns {string} 解析后的绝对路径 - * @public - */ -function resolvePath(rootPath, relativePath) { - let path = relativePath; - let root = rootPath; - - // root是可选的,类似于root.resolve - if (arguments.length === 1) { - path = rootPath; - root = process.cwd(); - } - - if (root == null) { - throw new TypeError('argument rootPath is required'); - } - - if (typeof root !== 'string') { - throw new TypeError('argument rootPath must be a string'); - } - - if (path == null) { - throw new TypeError('argument relativePath is required'); - } - - if (typeof path !== 'string') { - throw new TypeError('argument relativePath must be a string'); - } - - // 包含NULL字节是恶意的 - if (path.indexOf('\0') !== -1) { - throw createError(400, 'Malicious Path'); - } - - // 路径绝不能是绝对路径 - if (pathIsAbsolute.posix(path) || pathIsAbsolute.win32(path)) { - throw createError(400, 'Malicious Path'); - } - - // 路径超出根目录 - if (UP_PATH_REGEXP.test(normalize('.' + sep + path))) { - throw createError(403); - } - - // 拼接相对路径 - return normalize(join(resolve(root), path)); -} - -export default resolvePath; diff --git a/src/plugins/errorHandler/index.js b/src/plugins/errorHandler/index.js deleted file mode 100644 index d643593..0000000 --- a/src/plugins/errorHandler/index.js +++ /dev/null @@ -1,38 +0,0 @@ -// src/plugins/errorHandler.js -// 错误处理中间件插件 - -function formatError(ctx, status, message) { - const accept = ctx.accepts('json', 'html', 'text'); - if (accept === 'json') { - ctx.type = 'application/json'; - ctx.body = { success: false, error: message }; - } else if (accept === 'html') { - ctx.type = 'html'; - ctx.body = ` - - ${status} Error - -

${status} Error

-

${message}

- - - `; - } else { - ctx.type = 'text'; - ctx.body = `${status} - ${message}`; - } - ctx.status = status; -} - -export default function errorHandler() { - return async (ctx, next) => { - try { - await next(); - if (ctx.status === 404) { - formatError(ctx, 404, 'Resource not found'); - } - } catch (err) { - formatError(ctx, err.statusCode || 500, err.message || err || 'Internal server error'); - } - }; -} diff --git a/src/plugins/install.js b/src/plugins/install.js deleted file mode 100644 index 7ef8498..0000000 --- a/src/plugins/install.js +++ /dev/null @@ -1,23 +0,0 @@ -import ResponseTime from "./ResponseTime"; -import Send from "./Send"; -import { resolve } from 'path'; -import { fileURLToPath } from 'url'; -import path from "path"; -import errorHandler from './errorHandler'; - - -const __dirname = path.dirname(fileURLToPath(import.meta.url)); -const publicPath = resolve(__dirname, '../../public'); - -export default (app)=>{ - app.use(errorHandler()); - app.use(ResponseTime) - app.use(async (ctx, next) => { - try { - await Send(ctx, ctx.path, { root: publicPath }); - } catch (err) { - if (err.status !== 404) throw err; - } - await next(); - }) -} \ No newline at end of file diff --git a/src/services/JobService.js b/src/services/JobService.js index 1649aeb..35a04a3 100644 --- a/src/services/JobService.js +++ b/src/services/JobService.js @@ -1,18 +1,18 @@ -import jobs from '../jobs'; +import jobs from "../jobs" class JobService { - static startJob(id) { - return jobs.start(id); - } - static stopJob(id) { - return jobs.stop(id); - } - static updateJobCron(id, cronTime) { - return jobs.updateCronTime(id, cronTime); - } - static listJobs() { - return jobs.list(); - } + startJob(id) { + return jobs.start(id) + } + stopJob(id) { + return jobs.stop(id) + } + updateJobCron(id, cronTime) { + return jobs.updateCronTime(id, cronTime) + } + listJobs() { + return jobs.list() + } } -export default JobService; \ No newline at end of file +export default JobService diff --git a/src/services/userService.js b/src/services/userService.js index 1bd0a77..960ff1d 100644 --- a/src/services/userService.js +++ b/src/services/userService.js @@ -1,39 +1,74 @@ -// src/services/userService.js -// 用户相关业务逻辑 - -import UserModel from 'db/models/UserModel.js'; +import UserModel from "db/models/UserModel.js" +import { hashPassword, comparePassword } from "utils/bcrypt.js" +import { JWT_SECRET } from "@/middlewares/Auth/auth.js" +import jwt from "@/middlewares/Auth/jwt.js" class UserService { - static async getUserById(id) { - // 这里可以调用数据库模型 - // 示例返回 - return { id, name: `User_${id}` }; - } - - // 获取所有用户 - static async getAllUsers() { - return await UserModel.findAll(); - } - - // 创建新用户 - static async createUser(data) { - if (!data.name) throw new Error('用户名不能为空'); - return await UserModel.create(data); - } - - // 更新用户 - static async updateUser(id, data) { - const user = await UserModel.findById(id); - if (!user) throw new Error('用户不存在'); - return await UserModel.update(id, data); - } - - // 删除用户 - static async deleteUser(id) { - const user = await UserModel.findById(id); - if (!user) throw new Error('用户不存在'); - return await UserModel.delete(id); - } + async getUserById(id) { + // 这里可以调用数据库模型 + // 示例返回 + return { id, name: `User_${id}` } + } + + // 获取所有用户 + async getAllUsers() { + return await UserModel.findAll() + } + + // 创建新用户 + async createUser(data) { + if (!data.name) throw new Error("用户名不能为空") + return await UserModel.create(data) + } + + // 更新用户 + async updateUser(id, data) { + const user = await UserModel.findById(id) + if (!user) throw new Error("用户不存在") + return await UserModel.update(id, data) + } + + // 删除用户 + async deleteUser(id) { + const user = await UserModel.findById(id) + if (!user) throw new Error("用户不存在") + return await UserModel.delete(id) + } + + // 注册新用户 + async register(data) { + if (!data.username || !data.email || !data.password) throw new Error("用户名、邮箱和密码不能为空") + const existUser = await UserModel.findByUsername(data.username) + if (existUser) throw new Error("用户名已存在") + const existEmail = await UserModel.findByEmail(data.email) + if (existEmail) throw new Error("邮箱已被注册") + // 密码加密 + const hashed = await hashPassword(data.password) + + const user = await UserModel.create({ ...data, password: hashed }) + // 返回脱敏信息 + const { password, ...userInfo } = Array.isArray(user) ? user[0] : user + return userInfo + } + + // 登录 + async login({ username, email, password }) { + let user + if (username) { + user = await UserModel.findByUsername(username) + } else if (email) { + user = await UserModel.findByEmail(email) + } + if (!user) throw new Error("用户不存在") + // 校验密码 + const ok = await comparePassword(password, user.password) + if (!ok) throw new Error("密码错误") + // 生成token + const token = jwt.sign({ id: user.id, username: user.username }, JWT_SECRET, { expiresIn: "2h" }) + // 返回token和用户信息 + const { password: pwd, ...userInfo } = user + return { token, user: userInfo } + } } -export default UserService; +export default UserService diff --git a/src/utils/BaseSingleton.js b/src/utils/BaseSingleton.js new file mode 100644 index 0000000..9705647 --- /dev/null +++ b/src/utils/BaseSingleton.js @@ -0,0 +1,37 @@ +// 抽象基类,使用泛型来正确推导子类类型 +class BaseSingleton { + static _instance + + constructor() { + if (this.constructor === BaseSingleton) { + throw new Error("禁止直接实例化 BaseOne 抽象类") + } + + if (this.constructor._instance) { + throw new Error("构造函数私有化失败,禁止重复 new") + } + + // this.constructor 是子类,所以这里设为 instance + this.constructor._instance = this + } + + static getInstance() { + const clazz = this + if (!clazz._instance) { + const self = new this() + const handler = { + get: function (target, prop) { + const value = Reflect.get(target, prop) + if (typeof value === "function") { + return value.bind(target) + } + return Reflect.get(target, prop) + }, + } + clazz._instance = new Proxy(self, handler) + } + return clazz._instance + } +} + +export { BaseSingleton } diff --git a/src/utils/autoRegister.js b/src/utils/autoRegister.js index 4390aba..64b7b30 100644 --- a/src/utils/autoRegister.js +++ b/src/utils/autoRegister.js @@ -26,7 +26,7 @@ export function autoRegisterControllers(app, controllersDir = path.resolve(__dir } catch (e) { controller = (await import(fullPath)).default } - const routes = controller.routes || controller.default?.routes || controller.default || controller + const routes = controller.createRoutes || controller.default?.createRoutes || controller.default || controller // 判断 routes 方法参数个数,支持自动适配 if (typeof routes === "function") { allRouter.push(routes()) diff --git a/src/utils/bcrypt.js b/src/utils/bcrypt.js new file mode 100644 index 0000000..4c26d52 --- /dev/null +++ b/src/utils/bcrypt.js @@ -0,0 +1,11 @@ +// 密码加密与校验工具 +import bcrypt from "bcryptjs" + +export async function hashPassword(password) { + const salt = await bcrypt.genSalt(10) + return bcrypt.hash(password, salt) +} + +export async function comparePassword(password, hash) { + return bcrypt.compare(password, hash) +} diff --git a/src/utils/helper.js b/src/utils/helper.js new file mode 100644 index 0000000..a1903ee --- /dev/null +++ b/src/utils/helper.js @@ -0,0 +1,4 @@ + +export function formatResponse(success, data = null, error = null) { + return { success, error, data } +}