diff --git a/bun.lockb b/bun.lockb index 7be0543..d32b74e 100644 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/package.json b/package.json index 12ba189..b197fb3 100644 --- a/package.json +++ b/package.json @@ -16,6 +16,8 @@ }, "dependencies": { "bcryptjs": "^3.0.2", + "consolidate": "^1.0.4", + "get-paths": "^0.0.7", "jsonwebtoken": "^9.0.0", "knex": "^3.1.0", "koa": "^3.0.0", @@ -25,6 +27,7 @@ "module-alias": "^2.2.3", "node-cron": "^4.1.0", "path-to-regexp": "^8.2.0", + "pug": "^3.0.3", "sqlite3": "^5.1.7" }, "_moduleAliases": { diff --git a/public/index.html b/public/index.html deleted file mode 100644 index 535e0ac..0000000 --- a/public/index.html +++ /dev/null @@ -1,101 +0,0 @@ - - - - - - 登录 / 注册 - - - -
-
-
登录
-
注册
-
-
-
-
- - -
-
- - -
- -
- -
- - - 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