From f7dc33873d01c8a0afe78d0bf919050e082a068d Mon Sep 17 00:00:00 2001 From: npmrun <1549469775@qq.com> Date: Wed, 18 Jun 2025 23:47:39 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0=E8=A7=86=E5=9B=BE?= =?UTF-8?q?=E6=B8=B2=E6=9F=93=E6=94=AF=E6=8C=81=EF=BC=8C=E6=9B=B4=E6=96=B0?= =?UTF-8?q?=E4=B8=AD=E9=97=B4=E4=BB=B6=EF=BC=8C=E4=BC=98=E5=8C=96=E7=94=A8?= =?UTF-8?q?=E6=88=B7=E8=AE=A4=E8=AF=81=E5=92=8C=E9=94=99=E8=AF=AF=E5=A4=84?= =?UTF-8?q?=E7=90=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- bun.lockb | Bin 90420 -> 106045 bytes package.json | 3 + public/index.html | 101 --------------------------------- public/static/aa.txt | 1 + src/controllers/Page/HtmxController.js | 23 ++++++++ src/controllers/Page/PageController.js | 14 +++++ src/controllers/userController.js | 10 ++++ src/main.js | 10 ++-- src/middlewares/Auth/auth.js | 7 ++- src/middlewares/Views/index.js | 78 +++++++++++++++++++++++++ src/middlewares/errorHandler/index.js | 24 ++++++-- src/middlewares/install.js | 19 +++++-- src/views/htmx/fuck.pug | 1 + src/views/index.pug | 26 +++++++++ src/views/layouts/base.pug | 12 ++++ src/views/layouts/page.pug | 17 ++++++ 16 files changed, 229 insertions(+), 117 deletions(-) delete mode 100644 public/index.html create mode 100644 public/static/aa.txt create mode 100644 src/controllers/Page/HtmxController.js create mode 100644 src/controllers/Page/PageController.js create mode 100644 src/middlewares/Views/index.js create mode 100644 src/views/htmx/fuck.pug create mode 100644 src/views/index.pug create mode 100644 src/views/layouts/base.pug create mode 100644 src/views/layouts/page.pug diff --git a/bun.lockb b/bun.lockb index 7be0543fc0c2cb825561f7342a10e23dc012437f..d32b74e17d86cf3e9a62dfc8d5eb604ce89a19a4 100644 GIT binary patch delta 24169 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 o{b4gK8LYTs#Ln8^Z-|&zS - - - - - 登录 / 注册 - - - -
-
-
登录
-
注册
-
-
-
-
- - -
-
- - -
- -
- -
- - - diff --git a/public/static/aa.txt b/public/static/aa.txt new file mode 100644 index 0000000..1050001 --- /dev/null +++ b/public/static/aa.txt @@ -0,0 +1 @@ +asd \ No newline at end of file diff --git a/src/controllers/Page/HtmxController.js b/src/controllers/Page/HtmxController.js new file mode 100644 index 0000000..1beabad --- /dev/null +++ b/src/controllers/Page/HtmxController.js @@ -0,0 +1,23 @@ +export const Index = async ctx => { + return await ctx.render("index", { name: "bluescurry" }) +} + +export const Page = (name, data) => async ctx => { + return await ctx.render(name, data) +} + +import Router from "utils/router.js" +export function createRoutes() { + const router = new Router() + router.post("/clicked", async ctx => { + ctx.cookies.set("token", "sadas", { + httpOnly: true, + // Setting httpOnly to false allows JavaScript to access the cookie + // This enables browsers to automatically include the cookie in requests + sameSite: "lax", + // maxAge: 86400000, // Optional: cookie expiration in milliseconds (e.g., 24 hours) + }) + return await ctx.render("htmx/fuck", { title: "HTMX Clicked" }) + }) + return router +} diff --git a/src/controllers/Page/PageController.js b/src/controllers/Page/PageController.js new file mode 100644 index 0000000..733d9b4 --- /dev/null +++ b/src/controllers/Page/PageController.js @@ -0,0 +1,14 @@ +export const Index = async ctx => { + return await ctx.render("index", { name: "bluescurry" }) +} + +export const Page = (name, data) => async ctx => { + return await ctx.render(name, data) +} + +import Router from "utils/router.js" +export function createRoutes() { + const router = new Router() + router.get("/", Index) + return router +} diff --git a/src/controllers/userController.js b/src/controllers/userController.js index b00549d..913f79a 100644 --- a/src/controllers/userController.js +++ b/src/controllers/userController.js @@ -26,6 +26,16 @@ export const login = async (ctx) => { try { const { username, email, password } = ctx.request.body const result = await userService.login({ username, email, password }) + if (result && result.token) { + ctx.cookies.set("token", result.token, { + httpOnly: true, + // Setting httpOnly to false allows JavaScript to access the cookie + // This enables browsers to automatically include the cookie in requests + sameSite: "lax", + secure: process.env.NODE_ENV === "production", // Use secure cookies in production + // maxAge: 86400000, // Optional: cookie expiration in milliseconds (e.g., 24 hours) + }) + } ctx.body = formatResponse(true, result) } catch (err) { ctx.body = formatResponse(false, null, err.message) diff --git a/src/main.js b/src/main.js index 65e5d4b..f9bd271 100644 --- a/src/main.js +++ b/src/main.js @@ -9,17 +9,12 @@ import log4js from "log4js" // 应用插件与自动路由 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()); // 注册插件 LoadMiddlewares(app) -// 自动注册所有 controller -autoRegisterControllers(app) const PORT = process.env.PORT || 3000 @@ -38,7 +33,10 @@ const server = app.listen(PORT, () => { return "localhost" } const localIP = getLocalIP() - logger.trace(`服务器运行在: http://${localIP}:${port}`) + logger.trace(`===================【服务器地址】====================`) + logger.trace(` http://localhost:${port} (本地地址) `) + logger.trace(` http://${localIP}:${port} (本地地址) `) + logger.trace(`===================【服务器地址】====================`) }) export default app diff --git a/src/middlewares/Auth/auth.js b/src/middlewares/Auth/auth.js index 779d3fd..4cbcae3 100644 --- a/src/middlewares/Auth/auth.js +++ b/src/middlewares/Auth/auth.js @@ -17,7 +17,12 @@ function matchList(list, path) { } function verifyToken(ctx) { - const token = ctx.headers["authorization"]?.replace(/^Bearer\s/, "") + // 优先从 headers 获取 token + let token = ctx.headers["authorization"]?.replace(/^Bearer\s/, "") + // 如果 headers 没有,则从 cookies 获取 + if (!token) { + token = ctx.cookies.get("authorization") + } if (!token) return { ok: false } try { ctx.state.user = jwt.verify(token, JWT_SECRET) diff --git a/src/middlewares/Views/index.js b/src/middlewares/Views/index.js new file mode 100644 index 0000000..72339ec --- /dev/null +++ b/src/middlewares/Views/index.js @@ -0,0 +1,78 @@ +import { resolve } from "path" +import consolidate from "consolidate" +import send from "../Send" +import getPaths from "get-paths" +// import pretty from "pretty" +import { logger } from "@/logger" + +export default viewsMiddleware + +function viewsMiddleware(path, { engineSource = consolidate, extension = "html", options = {}, map } = {}) { + return function views(ctx, next) { + if (ctx.render) return next() + + ctx.getRender = function (relPath, locals = {}) { + return getPaths(path, relPath, extension).then(paths => { + const suffix = paths.ext + const state = Object.assign(locals, options, ctx.state || {}) + state.partials = Object.assign({}, options.partials || {}) + + if (isHtml(suffix) && !map) { + return send.getBody(ctx, paths.rel, { root: path }) + } + + const engineName = map && map[suffix] ? map[suffix] : suffix + const render = engineSource[engineName] + + if (!engineName || !render) { + return Promise.reject(new Error(`Engine not found for the ".${suffix}" file extension`)) + } + + return render(resolve(path, paths.rel), state) + }) + } + + // 将 render 注入到 context 和 response 对象中 + ctx.response.render = ctx.render = function (relPath, locals = {}) { + return getPaths(path, relPath, extension).then(paths => { + const suffix = paths.ext + const state = Object.assign(locals, options, ctx.state || {}) + // deep copy partials + state.partials = Object.assign({}, options.partials || {}) + logger.debug("render `%s` with %j", paths.rel, state) + ctx.type = "text/html" + + // 如果是 html 文件,不编译直接 send 静态文件 + if (isHtml(suffix) && !map) { + return send(ctx, paths.rel, { + root: path, + }) + } else { + const engineName = map && map[suffix] ? map[suffix] : suffix + + // 使用 engineSource 配置的渲染引擎 render + const render = engineSource[engineName] + + if (!engineName || !render) return Promise.reject(new Error(`Engine not found for the ".${suffix}" file extension`)) + + return render(resolve(path, paths.rel), state).then(html => { + // since pug has deprecated `pretty` option + // we'll use the `pretty` package in the meanwhile + // if (locals.pretty) { + // debug("using `pretty` package to beautify HTML") + // html = pretty(html) + // } + ctx.body = html + }) + } + }) + } + + // 中间件执行结束 + return next() + } +} + +function isHtml(ext) { + return ext === "html" +} diff --git a/src/middlewares/errorHandler/index.js b/src/middlewares/errorHandler/index.js index d643593..e0cda43 100644 --- a/src/middlewares/errorHandler/index.js +++ b/src/middlewares/errorHandler/index.js @@ -1,11 +1,14 @@ // src/plugins/errorHandler.js // 错误处理中间件插件 -function formatError(ctx, status, message) { +function formatError(ctx, status, message, stack) { const accept = ctx.accepts('json', 'html', 'text'); + const isDev = process.env.NODE_ENV === 'development'; if (accept === 'json') { ctx.type = 'application/json'; - ctx.body = { success: false, error: message }; + ctx.body = isDev && stack + ? { success: false, error: message, stack } + : { success: false, error: message }; } else if (accept === 'html') { ctx.type = 'html'; ctx.body = ` @@ -14,25 +17,38 @@ function formatError(ctx, status, message) {

${status} Error

${message}

+ ${isDev && stack ? `
${stack}
` : ''} `; } else { ctx.type = 'text'; - ctx.body = `${status} - ${message}`; + ctx.body = isDev && stack + ? `${status} - ${message}\n${stack}` + : `${status} - ${message}`; } ctx.status = status; } export default function errorHandler() { return async (ctx, next) => { + // 拦截 Chrome DevTools 探测请求,直接返回 204 + if (ctx.path === '/.well-known/appspecific/com.chrome.devtools.json') { + ctx.status = 204; + ctx.body = ''; + return; + } 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'); + const isDev = process.env.NODE_ENV === 'development'; + if (isDev && err.stack) { + console.error(err.stack); + } + formatError(ctx, err.statusCode || 500, err.message || err || 'Internal server error', isDev ? err.stack : undefined); } }; } diff --git a/src/middlewares/install.js b/src/middlewares/install.js index 0472e90..5027bfa 100644 --- a/src/middlewares/install.js +++ b/src/middlewares/install.js @@ -3,14 +3,17 @@ import Send from "./Send" import { resolve } from "path" import { fileURLToPath } from "url" import path from "path" -import errorHandler from "./errorHandler" +import ErrorHandler from "./ErrorHandler" import { auth } from "./Auth" +import bodyParser from "koa-bodyparser" +import Views from "./Views" +import { autoRegisterControllers } from "utils/autoRegister.js" const __dirname = path.dirname(fileURLToPath(import.meta.url)) const publicPath = resolve(__dirname, "../../public") export default app => { - app.use(errorHandler()) + app.use(ErrorHandler()) app.use(ResponseTime) app.use( auth({ @@ -21,13 +24,19 @@ export default app => { { pattern: "/api/v1/status", auth: "try" }, { pattern: "/api/**/*", auth: true }, // 静态资源访问 - "", - "/", - "/**/*", + { pattern: "/", auth: "try" }, + { pattern: "/**/*", auth: "try" }, ], blackList: [], }) ) + app.use(bodyParser()) + app.use( + Views(resolve(__dirname, "../views"), { + extension: "pug", + }) + ) + autoRegisterControllers(app) app.use(async (ctx, next) => { try { await Send(ctx, ctx.path, { root: publicPath }) diff --git a/src/views/htmx/fuck.pug b/src/views/htmx/fuck.pug new file mode 100644 index 0000000..858d086 --- /dev/null +++ b/src/views/htmx/fuck.pug @@ -0,0 +1 @@ +#{title || '默认标题'} \ No newline at end of file diff --git a/src/views/index.pug b/src/views/index.pug new file mode 100644 index 0000000..b03e8f5 --- /dev/null +++ b/src/views/index.pug @@ -0,0 +1,26 @@ +extends ./layouts/page.pug + + +block pageRoot + - var title = '示例页面标题' + +block pageContent + .container.mt-5 + .row.justify-content-center + .col-md-8.text-center + img.rounded-circle.shadow.mb-4(src='https://avatars.githubusercontent.com/u/9919?s=200&v=4', alt='Avatar', width='120', height='120') + h1.mt-3.mb-1 你的姓名 + h4.text-muted 你的职位 / 头衔 + p.lead.mt-3 这里是一段简短的自我介绍,突出你的专业技能、兴趣或座右铭。 + button(hx-post="/clicked" hx-swap="outerHTML") Click Me + hr.my-4 + .d-flex.justify-content-center.gap-4 + a(href='mailto:your@email.com', target='_blank') + i.fas.fa-envelope.me-2 + | 邮箱 + a(href='https://github.com/your-github', target='_blank') + i.fab.fa-github.me-2 + | GitHub + a(href='https://your-website.com', target='_blank') + i.fas.fa-globe.me-2 + | 个人网站 diff --git a/src/views/layouts/base.pug b/src/views/layouts/base.pug new file mode 100644 index 0000000..1471c2b --- /dev/null +++ b/src/views/layouts/base.pug @@ -0,0 +1,12 @@ +block root +doctype html +html(lang="zh-CN") + head + title #{title || '默认标题'} + meta(charset="utf-8") + meta(name="viewport" content="width=device-width, initial-scale=1") + script(src="https://unpkg.com/htmx.org@2.0.4") + block head + body + block content + block scripts diff --git a/src/views/layouts/page.pug b/src/views/layouts/page.pug new file mode 100644 index 0000000..dfeedeb --- /dev/null +++ b/src/views/layouts/page.pug @@ -0,0 +1,17 @@ + +extends ./base.pug + +block root + block pageRoot + +block head + link(href='https://cdn.bootcss.com/twitter-bootstrap/4.1.3/css/bootstrap.min.css' rel='stylesheet') + script(src='https://cdn.bootcss.com/jquery/3.3.1/jquery.min.js') + script(src='https://cdn.bootcss.com/twitter-bootstrap/4.1.3/js/bootstrap.bundle.min.js') + block pageHead + +block content + block pageContent + +block scripts + block pageScripts