Browse Source

add

tags/只集成了数据库的提交
npmrun 3 months ago
parent
commit
929d92235b
  1. 5
      .env.example
  2. 0
      build-files/migrate/pg.js
  3. 0
      build-files/migrate/sqlite3.js
  4. 3
      build-files/run.sh
  5. 117
      build-files/seed/sqlite3.js
  6. 8
      bun.lock
  7. 13
      package.json
  8. 11
      packages/drizzle-pkg/database/pg/db.ts
  9. 16
      packages/drizzle-pkg/database/pg/schema/auth.ts
  10. 10
      packages/drizzle-pkg/database/sqlite/db.ts
  11. 1
      packages/drizzle-pkg/env.ts
  12. 24
      packages/drizzle-pkg/lib/db.ts
  13. 33
      packages/drizzle-pkg/lib/paths.ts
  14. 15
      packages/drizzle-pkg/lib/resolve-sqlite-url.ts
  15. 26
      packages/drizzle-pkg/lib/schema/auth.ts
  16. 7
      packages/drizzle-pkg/seed.ts
  17. 50
      scripts/deploy-gitea.sh
  18. 50
      scripts/deploy-gitee.sh
  19. 2
      scripts/migrate-test.sh
  20. 11
      scripts/mv.sh
  21. 42
      server/constants/cloud-probes.ts
  22. 56
      server/constants/upload.ts
  23. 25
      server/middleware/00.cloud-probe.ts
  24. 17
      server/middleware/01.public.ts
  25. 8
      server/plugins/00.global.ts
  26. 2
      server/plugins/00.req-time.ts
  27. 0
      server/plugins/01.well-known-ignore.ts
  28. 34
      server/plugins/02.error-logger.ts
  29. 0
      static/.gitignore
  30. 1
      static/upload/a.txt

5
.env.example

@ -1 +1,4 @@
DATABASE_URL=postgresql://postgres:xxxxxx@localhost:6666/postgres
DATABASE_URL=file:./db.sqlite
STATIC_DIR=static
UPLOAD_SUBDIR=upload
NITRO_PORT=3399

0
build-files/migrate/migrate-pg.js → build-files/migrate/pg.js

0
build-files/migrate/migrate-sqlite.js → build-files/migrate/sqlite3.js

3
build-files/run.sh

@ -4,5 +4,6 @@ if [ -f .env ]; then
export $(grep -v '^#' .env | xargs)
fi
node server/migrate-pg.js migrations
node server/migrate/sqlite3.js migrations
node server/seed/sqlite3.js
node server/index.mjs

117
build-files/seed/sqlite3.js

@ -0,0 +1,117 @@
/**
* 部署后首次启动前执行若库中尚无 admin则用环境变量创建首个管理员
* 逻辑对齐 `packages/drizzle-pkg/seed.ts`使用 SQLite `migrate-sqlite.js` 相同
*
* 环境变量 seed.ts 一致
* - DATABASE_URLSQLite 路径可为 `file:/path/to/db.sqlite` 或裸路径
* - BOOTSTRAP_ADMIN_USERNAME / BOOTSTRAP_ADMIN_PASSWORD可选未设置或校验失败则跳过
*/
import Database from 'better-sqlite3'
import { hash } from 'bcryptjs'
import { mkdirSync } from 'node:fs'
import path from 'node:path'
import { fileURLToPath } from 'node:url'
/** 与 `server/service/auth/index.ts` 一致 */
const USERNAME_REGEX = /^[a-zA-Z0-9_]{3,20}$/
const MIN_PASSWORD_LENGTH = 6
/** 与 seed.ts 一致:仅当小写用户名本身符合 slug 规则时写入 public_slug */
const PUBLIC_SLUG_REGEX = /^[a-z0-9]{3,20}$/
function derivePublicSlug(username) {
const lower = username.toLowerCase()
return PUBLIC_SLUG_REGEX.test(lower) ? lower : null
}
/**
* `migrate-sqlite.js` 一致相对路径相对 `process.cwd()`例如 `.output/run.sh` 下与迁移写入同一文件
* Nitro `resolveSqliteDatabaseUrl` 在无法锚定 drizzle-pkg 时也会回退到 cwd避免迁移/seed 成功服务 CANTOPEN
*/
function resolveSqliteFilePath(dbUrl) {
const stripped = dbUrl.startsWith('file:') ? dbUrl.slice('file:'.length) : dbUrl
if (!stripped) {
throw new Error('DATABASE_URL 未设置,且未提供有效的 SQLite 文件路径')
}
if (path.isAbsolute(stripped)) {
return stripped
}
return path.resolve(process.cwd(), stripped)
}
function openSqlite() {
const dbUrl = process.env.DATABASE_URL || ''
const sqlitePath = resolveSqliteFilePath(dbUrl)
mkdirSync(path.dirname(sqlitePath), { recursive: true })
return new Database(sqlitePath)
}
async function main() {
const db = openSqlite()
try {
const existingAdmin = db
.prepare(`SELECT id FROM users WHERE role = ? LIMIT 1`)
.get('admin')
if (existingAdmin) {
console.log('Bootstrap skipped: admin exists')
return
}
const username = process.env.BOOTSTRAP_ADMIN_USERNAME
const password = process.env.BOOTSTRAP_ADMIN_PASSWORD
if (!username || !password) {
console.warn(
'Bootstrap skipped: set BOOTSTRAP_ADMIN_USERNAME and BOOTSTRAP_ADMIN_PASSWORD',
)
return
}
if (!USERNAME_REGEX.test(username) || password.length < MIN_PASSWORD_LENGTH) {
console.warn(
'Bootstrap skipped: invalid BOOTSTRAP_ADMIN_USERNAME or BOOTSTRAP_ADMIN_PASSWORD',
)
return
}
const passwordHash = await hash(password, 10)
const { maxId } = db.prepare(`SELECT COALESCE(MAX(id), 0) AS maxId FROM users`).get()
const userId = (maxId ?? 0) + 1
const publicSlug = derivePublicSlug(username)
try {
db.prepare(
`
INSERT INTO users (id, username, password, role, status, public_slug)
VALUES (@id, @username, @password, @role, @status, @public_slug)
`,
).run({
id: userId,
username,
password: passwordHash,
role: 'admin',
status: 'active',
public_slug: publicSlug,
})
console.log('Bootstrap complete: admin user created')
} catch (err) {
console.warn(
'Bootstrap skipped: could not insert admin (unique conflict or DB error)',
err,
)
}
} finally {
db.close()
}
}
const isMain =
process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)
if (isMain) {
// 成功时自然退出(退出码 0);仅未捕获错误时 process.exit(1)。
main().catch((e) => {
console.error(e)
process.exit(1)
})
}

8
bun.lock

@ -5,8 +5,8 @@
"": {
"name": "person-panel",
"dependencies": {
"@nuxt/ui": "^4.6.1",
"better-sqlite3": "^12.9.0",
"@nuxt/ui": "4.6.1",
"better-sqlite3": "12.9.0",
"dotenv": "17.4.1",
"drizzle-orm": "0.45.2",
"drizzle-pkg": "workspace:*",
@ -18,14 +18,14 @@
"multer": "2.1.1",
"nuxt": "4.4.2",
"pg": "8.20.0",
"tailwindcss": "^4.2.2",
"tailwindcss": "4.2.2",
"ufo": "1.6.3",
"vue": "3.5.32",
"vue-router": "5.0.4",
"zod": "4.3.6",
},
"devDependencies": {
"@types/better-sqlite3": "^7.6.13",
"@types/better-sqlite3": "7.6.13",
"@types/multer": "2.1.0",
"@types/pg": "8.20.0",
"drizzle-kit": "0.31.10",

13
package.json

@ -9,18 +9,19 @@
"scripts": {
"build": "nuxt build && bun run cp:db && bun --elide-lines=0 --filter drizzle-pkg build",
"dev": "nuxt dev",
"cp:db": "cp build-files/run.sh .output/run.sh && cp .env.example .output/.env && cp -r build-files/migrate/* .output/server/",
"deploy": "bash scripts/deploy-gitea.sh",
"cp:db": "bash scripts/mv.sh",
"migrate:test": "sh scripts/migrate-test.sh",
"db:migrate": "bun --elide-lines=0 --filter drizzle-pkg migrate",
"db:generate": "bun --elide-lines=0 --filter drizzle-pkg generate --name",
"db:generate": "bun --elide-lines=0 --filter drizzle-pkg generate",
"db:seed": "bun --elide-lines=0 --filter drizzle-pkg seed",
"generate": "nuxt generate",
"preview": "nuxt preview",
"postinstall": "nuxt prepare"
},
"dependencies": {
"@nuxt/ui": "^4.6.1",
"better-sqlite3": "^12.9.0",
"@nuxt/ui": "4.6.1",
"better-sqlite3": "12.9.0",
"dotenv": "17.4.1",
"drizzle-orm": "0.45.2",
"drizzle-pkg": "workspace:*",
@ -32,14 +33,14 @@
"multer": "2.1.1",
"nuxt": "4.4.2",
"pg": "8.20.0",
"tailwindcss": "^4.2.2",
"tailwindcss": "4.2.2",
"ufo": "1.6.3",
"vue": "3.5.32",
"vue-router": "5.0.4",
"zod": "4.3.6"
},
"devDependencies": {
"@types/better-sqlite3": "^7.6.13",
"@types/better-sqlite3": "7.6.13",
"@types/multer": "2.1.0",
"@types/pg": "8.20.0",
"drizzle-kit": "0.31.10",

11
packages/drizzle-pkg/database/pg/db.ts

@ -1,11 +0,0 @@
import { drizzle } from "drizzle-orm/node-postgres";
if (process.env.NODE_ENV === 'production') {
// 打包时需要保证migrator被引入
import('drizzle-orm/node-postgres/migrator')
}
const _db = drizzle(process.env.DATABASE_URL!);
export { _db as dbGlobal }

16
packages/drizzle-pkg/database/pg/schema/auth.ts

@ -1,16 +0,0 @@
import { sql } from "drizzle-orm";
import { integer, pgTable, timestamp, varchar } from "drizzle-orm/pg-core";
export const users = pgTable("users", {
id: integer().primaryKey(),
username: varchar().notNull().unique(),
email: varchar(),
nickname: varchar(),
password: varchar().notNull(),
avatar: varchar(),
createdAt: timestamp('created_at').defaultNow().notNull(),
updatedAt: timestamp('updated_at')
.defaultNow()
.$onUpdate(() => sql`CURRENT_TIMESTAMP`)
.notNull(),
});

10
packages/drizzle-pkg/database/sqlite/db.ts

@ -1,10 +0,0 @@
import { drizzle } from 'drizzle-orm/libsql';
if (process.env.NODE_ENV === 'production') {
// 打包时需要保证migrator被引入
import('drizzle-orm/better-sqlite3/migrator')
}
const _db = drizzle(process.env.DATABASE_URL!);
export { _db as dbGlobal }

1
packages/drizzle-pkg/env.ts

@ -2,3 +2,4 @@
import { config } from 'dotenv';
config({ path: '../../.env' });
console.log(process.env);

24
packages/drizzle-pkg/lib/db.ts

@ -1 +1,23 @@
export { dbGlobal } from '../database/pg/db'
import { drizzle } from "drizzle-orm/better-sqlite3";
import { resolveSqliteDatabaseUrl } from "./resolve-sqlite-url";
if (process.env.NODE_ENV === "production") {
// 打包时需要保证migrator被引入
import("drizzle-orm/better-sqlite3/migrator");
}
const rawUrl = process.env.DATABASE_URL;
if (!rawUrl) {
throw new Error("DATABASE_URL 未设置");
}
const resolvedUrl = resolveSqliteDatabaseUrl(rawUrl);
process.env.DATABASE_URL = resolvedUrl;
// better-sqlite3 需要裸文件路径;`file:` 前缀仍保留在 DATABASE_URL 供 drizzle-kit 等使用
const sqlitePath = resolvedUrl.startsWith("file:")
? resolvedUrl.slice("file:".length)
: resolvedUrl;
const _db = drizzle(sqlitePath);
export { _db as dbGlobal };

33
packages/drizzle-pkg/lib/paths.ts

@ -0,0 +1,33 @@
import { existsSync, readFileSync } from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";
function isDrizzlePkgRoot(dir: string): boolean {
const pkg = path.join(dir, "package.json");
if (!existsSync(pkg)) {
return false;
}
try {
const { name } = JSON.parse(readFileSync(pkg, "utf8")) as { name?: string };
return name === "drizzle-pkg";
} catch {
return false;
}
}
/**
* `drizzle-pkg` `package.json``db.sqlite`
* - `import.meta` cwd `file:db.sqlite` / DBMOVED
* - chunk 退 `cwd/packages/drizzle-pkg`退 `process.cwd()` `.output` migrate/seed `DATABASE_URL`
*/
export function getDrizzlePkgRoot(): string {
const fromMeta = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
if (isDrizzlePkgRoot(fromMeta)) {
return fromMeta;
}
const fromCwd = path.resolve(process.cwd(), "packages/drizzle-pkg");
if (isDrizzlePkgRoot(fromCwd)) {
return fromCwd;
}
return process.cwd();
}

15
packages/drizzle-pkg/lib/resolve-sqlite-url.ts

@ -0,0 +1,15 @@
import path from "node:path";
import { getDrizzlePkgRoot } from "./paths";
/** 将 `file:` 相对路径解析为绝对路径(相对 drizzle-pkg 根目录) */
export function resolveSqliteDatabaseUrl(url: string): string {
if (!url.startsWith("file:")) {
return url;
}
let filePath = url.slice("file:".length);
if (path.isAbsolute(filePath)) {
return `file:${filePath}`;
}
const root = getDrizzlePkgRoot();
return `file:${path.resolve(root, filePath)}`;
}

26
packages/drizzle-pkg/lib/schema/auth.ts

@ -1 +1,25 @@
export { users } from '../../database/pg/schema/auth'
import { index, integer, sqliteTable, text } from "drizzle-orm/sqlite-core";
export const users = sqliteTable("users", {
id: integer().primaryKey(),
username: text().notNull().unique(),
email: text(),
nickname: text(),
password: text().notNull(),
avatar: text(),
role: text().notNull().default("user"),
status: text().notNull().default("active"),
publicSlug: text("public_slug").unique(),
bioMarkdown: text("bio_markdown"),
bioVisibility: text("bio_visibility").notNull().default("private"),
socialLinksJson: text("social_links_json").notNull().default("[]"),
avatarVisibility: text("avatar_visibility").notNull().default("private"),
discoverVisible: integer("discover_visible", { mode: "boolean" }).notNull().default(true),
discoverLocation: text("discover_location"),
discoverShowLocation: integer("discover_show_location", { mode: "boolean" }).notNull().default(false),
createdAt: integer("created_at", { mode: "timestamp_ms" }).defaultNow().notNull(),
updatedAt: integer("updated_at", { mode: "timestamp_ms" })
.defaultNow()
.$onUpdate(() => new Date())
.notNull(),
});

7
packages/drizzle-pkg/seed.ts

@ -1,11 +1,11 @@
import './env';
import { seed } from "drizzle-seed";
import { usersTable } from "./lib/schema/auth";
import { users } from "./lib/schema/auth";
import { dbGlobal } from "./lib/db";
async function main() {
await seed(dbGlobal, { usersTable }).refine((f) => ({
usersTable: {
await seed(dbGlobal, { users }).refine((f) => ({
users: {
columns: {
name: f.fullName(),
age: f.int({ minValue: 18, maxValue: 60 }),
@ -22,4 +22,3 @@ main().catch(e => {
console.error(e);
process.exit(1);
});

50
scripts/deploy-gitea.sh

@ -0,0 +1,50 @@
#!/usr/bin/env sh
# 配置区(只改这里)
GIT_REPO_URL="ssh://root@git.xieyaxin.top:8892/topuser/nuxt4-demo.git"
PROD_BRANCH="deploy" # 你要存放产物的分支名(会自动创建)
BUILD_FOLDER=".output" # 打包产物目录
ARCHIVE_NAME="build-output.tar.gz"
ARCHIVE_PATH=".tmp_${ARCHIVE_NAME}"
COMMIT_MSG="deploy: build at $(date +'%Y-%m-%d %H:%M:%S')"
# 1. 先打包
echo "📦 构建项目..."
bun run build
# 2. 克隆远程产物分支到临时目录
echo "⬇️ 拉取产物分支..."
git clone --single-branch --branch $PROD_BRANCH $GIT_REPO_URL .tmp_dist || {
echo "🆕 分支不存在,创建新分支..."
mkdir .tmp_dist
cd .tmp_dist
git init
git checkout -b $PROD_BRANCH
git remote add origin $GIT_REPO_URL
cd ..
}
# 3. 压缩构建产物
echo "🗜️ 压缩构建目录..."
tar -czf "$ARCHIVE_PATH" -C "$BUILD_FOLDER" .
# 4. 删除旧产物,复制新产物
echo "♻️ 更新产物文件..."
rm -rf .tmp_dist/*
cp -r $BUILD_FOLDER/.drone.yml .tmp_dist/.drone.yml
cp "$ARCHIVE_PATH" ".tmp_dist/$ARCHIVE_NAME"
# 5. 提交并推送
cd .tmp_dist
git add -A
git commit -m "$COMMIT_MSG"
echo "🚀 推送到远程分支 $PROD_BRANCH..."
git push origin $PROD_BRANCH
# 6. 清理临时文件
cd ..
rm -rf .tmp_dist
rm -f "$ARCHIVE_PATH"
echo "✅ 发布完成!"

50
scripts/deploy-gitee.sh

@ -0,0 +1,50 @@
#!/usr/bin/env sh
# 配置区(只改这里)
GIT_REPO_URL="git@gitee.com:xieyaxin/nuxt4-demo.git"
PROD_BRANCH="deploy" # 你要存放产物的分支名(会自动创建)
BUILD_FOLDER=".output" # 打包产物目录
ARCHIVE_NAME="build-output.tar.gz"
ARCHIVE_PATH=".tmp_${ARCHIVE_NAME}"
COMMIT_MSG="deploy: build at $(date +'%Y-%m-%d %H:%M:%S')"
# 1. 先打包
echo "📦 构建项目..."
bun run build
# 2. 克隆远程产物分支到临时目录
echo "⬇️ 拉取产物分支..."
git clone --single-branch --branch $PROD_BRANCH $GIT_REPO_URL .tmp_dist || {
echo "🆕 分支不存在,创建新分支..."
mkdir .tmp_dist
cd .tmp_dist
git init
git checkout -b $PROD_BRANCH
git remote add origin $GIT_REPO_URL
cd ..
}
# 3. 压缩构建产物
echo "🗜️ 压缩构建目录..."
tar -czf "$ARCHIVE_PATH" -C "$BUILD_FOLDER" .
# 4. 删除旧产物,复制新产物
echo "♻️ 更新产物文件..."
rm -rf .tmp_dist/*
cp -r $BUILD_FOLDER/.drone.yml .tmp_dist/.drone.yml
cp "$ARCHIVE_PATH" ".tmp_dist/$ARCHIVE_NAME"
# 5. 提交并推送
cd .tmp_dist
git add -A
git commit -m "$COMMIT_MSG"
echo "🚀 推送到远程分支 $PROD_BRANCH..."
git push origin $PROD_BRANCH
# 6. 清理临时文件
cd ..
rm -rf .tmp_dist
rm -f "$ARCHIVE_PATH"
echo "✅ 发布完成!"

2
scripts/migrate-test.sh

@ -4,4 +4,4 @@ fi
echo "DATABASE_URL: $DATABASE_URL"
node build-files/migrate/migrate-pg.js packages/drizzle-pkg/migrations
node build-files/migrate/sqlite3.js packages/drizzle-pkg/migrations

11
scripts/mv.sh

@ -0,0 +1,11 @@
if [ -f .env.prod ]; then
cp .env.prod .output/.env
else
cp .env.example .output/.env
fi
cp build-files/run.sh .output/run.sh
mkdir .output/server/migrate
cp -r build-files/migrate/* .output/server/migrate/
mkdir .output/server/seed
cp -r build-files/* .output/server/seed/

42
server/constants/cloud-probes.ts

@ -0,0 +1,42 @@
/**
* / HTTP `/`
*
*/
export const CLOUD_PROBE_PATHS = [
// 通用 / Kubernetes
"/health",
"/healthz",
"/livez",
"/readyz",
"/liveness",
"/readiness",
"/startup",
"/health/live",
"/health/ready",
"/health/startup",
// Spring Actuator(经网关暴露时偶见)
"/actuator/health",
"/actuator/health/liveness",
"/actuator/health/readiness",
// Azure App Service 相关默认探测
"/robots933456.txt",
// 国内云控制台常见示例静态页
"/check.html",
"/status.html",
"/ping",
"/status",
"/alive",
] as const;
/** 阿里云等:健康检查为 `/rpc` 或带后缀路径(如控制台填写的 `/rpc/...`),按前缀匹配 */
export const CLOUD_PROBE_PATH_PREFIXES = ["/rpc"] as const;
export const CLOUD_PROBE_PATH_SET = new Set<string>(CLOUD_PROBE_PATHS);
export function isCloudProbePath(pathname: string): boolean {
if (CLOUD_PROBE_PATH_SET.has(pathname)) return true;
for (const prefix of CLOUD_PROBE_PATH_PREFIXES) {
if (pathname === prefix || pathname.startsWith(`${prefix}/`)) return true;
}
return false;
}

56
server/constants/upload.ts

@ -0,0 +1,56 @@
import path from "node:path";
function trimSlashes(input: string): string {
return input.trim().replace(/^\/+|\/+$/g, "");
}
function hasParentSegment(input: string): boolean {
return input
.split("/")
.map((part) => part.trim())
.some((part) => part === "..");
}
/** 允许相对项目根或绝对路径;禁止含 `..` 的路径片段(防止配置逃逸)。 */
function ensureConfigurableDir(input: string, fallback: string, envName: string): string {
const raw = input.trim();
if (!raw) {
return fallback;
}
const normalized = path.normalize(raw);
const segments = normalized.split(path.sep);
if (segments.some((part) => part === "..")) {
throw new Error(`${envName} must not contain ".." path segments`);
}
return normalized;
}
function ensureSafeSubdir(input: string, fallback: string, envName: string): string {
const value = trimSlashes(input);
if (!value) {
return fallback;
}
if (hasParentSegment(value)) {
throw new Error(`${envName} must not contain ".." segments`);
}
return value;
}
/** 静态资源 URL 前缀固定为 `/static`,不允许通过环境变量覆写。 */
export const STATIC_PUBLIC_PREFIX = "/static";
/** 静态资源根目录(相对项目根或绝对路径),默认 `static` */
export const STATIC_DIR = ensureConfigurableDir(process.env.STATIC_DIR ?? "static", "static", "STATIC_DIR");
/** 媒体上传子目录(相对 STATIC_DIR),默认 `upload` */
export const UPLOAD_SUBDIR = ensureSafeSubdir(
process.env.UPLOAD_SUBDIR ?? "upload",
"upload",
"UPLOAD_SUBDIR",
);
/** 媒体上传目录(与 STATIC_DIR 同为相对或绝对),默认 `static/upload` */
export const RELATIVE_ASSETS_DIR = path.join(STATIC_DIR, UPLOAD_SUBDIR);
/** 与 `media` 返回及静态路径一致,无前导 host */
export const POST_MEDIA_PUBLIC_PREFIX = `${STATIC_PUBLIC_PREFIX}/${UPLOAD_SUBDIR}/`;

25
server/middleware/00.cloud-probe.ts

@ -0,0 +1,25 @@
import { getRequestURL } from "h3";
import { isCloudProbePath } from "#server/constants/cloud-probes";
const METHODS = new Set(["GET", "HEAD"]);
export default eventHandler((event) => {
if (!METHODS.has(event.method)) return;
const pathname = getRequestURL(event).pathname;
if (!isCloudProbePath(pathname)) return;
setHeader(event, "content-type", "text/plain; charset=utf-8");
setHeader(
event,
"cache-control",
"no-store, no-cache, must-revalidate, proxy-revalidate",
);
if (event.method === "HEAD") {
setResponseStatus(event, 200);
return;
}
return "OK";
});

17
server/middleware/00.public.ts → server/middleware/01.public.ts

@ -1,4 +1,4 @@
import { resolve, join, sep, extname } from "node:path";
import { resolve, join, relative } from "node:path";
import { promises as fsp } from "node:fs";
import {
decodePath,
@ -8,9 +8,10 @@ import {
} from "ufo";
import type { H3Event } from "h3";
import mime from "mime";
import { STATIC_DIR, STATIC_PUBLIC_PREFIX } from "#server/constants/upload";
const METHODS = new Set(["HEAD", "GET"]);
const SAFE_BASE_DIR = resolve("public");
const SAFE_BASE_DIR = resolve(STATIC_DIR);
// 缓存配置
const CACHE_CONTROL = "public, max-age=31536000, immutable";
@ -20,7 +21,7 @@ const NOT_FOUND = 404;
const SERVER_ERROR = 500;
export default eventHandler(async (event: H3Event) => {
if (!event.path.startsWith("/public")) return;
if (!event.path.startsWith(STATIC_PUBLIC_PREFIX)) return;
const { req, res } = event.node;
const method = req.method;
@ -29,16 +30,18 @@ export default eventHandler(async (event: H3Event) => {
try {
// 安全解析路径
const url = event.path.replace(/^\/public/, "");
const url = event.path.replace(STATIC_PUBLIC_PREFIX, "");
const pathname = decodePath(
withLeadingSlash(withoutTrailingSlash(parseURL(url).pathname))
);
const targetPath = join(SAFE_BASE_DIR, pathname);
const resolvedPath = resolve(targetPath);
// 安全校验
if (!resolvedPath.startsWith(SAFE_BASE_DIR + sep)) {
// 安全校验(支持 STATIC_DIR 为绝对路径;relative 在越界时为 `..` 开头)
const rel = relative(SAFE_BASE_DIR, resolvedPath);
if (rel.startsWith("..") || rel === "..") {
res.statusCode = FORBIDDEN;
return "Forbidden";
}

8
server/plugins/00.global.ts

@ -1,8 +0,0 @@
if (import.meta.dev) {
console.log("plugin: 00.global");
}
export default defineNitroPlugin(async () => {
})

2
server/plugins/01.req-time.ts → server/plugins/00.req-time.ts

@ -4,7 +4,7 @@ import { randomUUID } from "crypto";
const logger = log4js.getLogger("APP")
if (import.meta.dev) {
console.log("plugin: 1.error-handler");
console.log("plugin: 00.req-time");
}
declare module "http" {

0
server/plugins/02.well-known-ignore.ts → server/plugins/01.well-known-ignore.ts

34
server/plugins/03.error-logger.ts → server/plugins/02.error-logger.ts

@ -2,6 +2,23 @@ import log4js from "logger";
const logger = log4js.getLogger("ERROR");
/** 路径/路由类「找不到」:只打控制台,不落盘(避免扫描、错 URL 等刷爆 running.log) */
function isPathNotFoundLikeError(error: unknown): boolean {
if (!error || typeof error !== "object") return false;
const e = error as NodeJS.ErrnoException & { statusCode?: number; statusMessage?: string };
const code = typeof e.code === "string" ? e.code : "";
if (code === "ENOENT" || code === "ENOTDIR") return true;
if (e.statusCode !== 404) return false;
const sm = (e.statusMessage ?? "").trim();
if (!sm) return true;
if (/^not found$/i.test(sm)) return true;
if (/page not found/i.test(sm)) return true;
if (/^cannot (get|post|put|delete|patch|head)\b/i.test(sm)) return true;
if (/no route found|cannot find.*route|static asset.*not found/i.test(sm)) return true;
return false;
}
const processHandlersKey = "__personPanelErrorLoggerProcessHandlers";
function installProcessErrorLogging() {
@ -23,7 +40,7 @@ function installProcessErrorLogging() {
}
if (import.meta.dev) {
console.log("plugin: 03.error-logger");
console.log("plugin: 02.error-logger");
}
export default defineNitroPlugin((nitroApp) => {
@ -32,12 +49,19 @@ export default defineNitroPlugin((nitroApp) => {
nitroApp.hooks.hook("error", (error, context) => {
const event = context.event;
const tags = Array.isArray(context.tags) ? (context.tags as string[]).join(",") : "";
logger.error(
const prefix = [
event?.method ?? "",
event?.path ?? "(no request)",
tags ? `[${tags}]` : "",
"\n",
error
);
]
.filter(Boolean)
.join(" ");
if (isPathNotFoundLikeError(error)) {
console.error(prefix || "(error)", "\n", error);
return;
}
logger.error(prefix, "\n", error);
});
});

0
static/.gitignore

1
static/upload/a.txt

@ -0,0 +1 @@
sadsadsad
Loading…
Cancel
Save