From f5377e51bb0ab3d87e0f0c9d18569b3e9a22d2c6 Mon Sep 17 00:00:00 2001 From: 237899745 <237899745@users.noreply.git.workyai.cn> Date: Mon, 27 Jul 2026 13:07:14 +0800 Subject: [PATCH] chore: harden build and deployment workflow --- .gitea/workflows/backend-tests.yml | 17 +- CONTRIBUTING.md | 35 ++++ INSTALL_GUIDE.md | 32 ++-- README.md | 44 +++-- docker-compose.yml | 12 +- frontend/.dockerignore | 3 + frontend/Dockerfile | 13 ++ install.sh | 278 +++++++++++++++++++++++++---- nginx/nginx.conf | 21 ++- nginx/nginx.conf.example | 24 ++- 10 files changed, 408 insertions(+), 71 deletions(-) create mode 100644 CONTRIBUTING.md create mode 100644 frontend/.dockerignore create mode 100644 frontend/Dockerfile diff --git a/.gitea/workflows/backend-tests.yml b/.gitea/workflows/backend-tests.yml index 682f065..8520af0 100644 --- a/.gitea/workflows/backend-tests.yml +++ b/.gitea/workflows/backend-tests.yml @@ -1,4 +1,4 @@ -name: Backend tests +name: Build and tests on: push: @@ -20,10 +20,23 @@ jobs: with: node-version-file: .nvmrc cache: npm - cache-dependency-path: backend/package-lock.json + cache-dependency-path: | + backend/package-lock.json + frontend/package-lock.json - name: Install dependencies run: npm ci - name: Audit production dependencies run: npm audit --omit=dev - name: Run backend tests run: npm test + - name: Install frontend dependencies + working-directory: frontend + run: npm ci + - name: Audit frontend dependencies + working-directory: frontend + run: npm audit + - name: Build frontend + working-directory: frontend + run: npm run build + - name: Validate deployment script syntax + run: bash -n ../install.sh diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..fb990c4 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,35 @@ +# 贡献与工程约束 + +## 本地验证 + +项目要求 Node.js `20.19` 至 `24.x`。后端与 Web 前端分别维护锁文件,依赖变更后必须同步提交对应的 `package-lock.json`。 + +```bash +cd backend +npm ci +npm test +npm audit --omit=dev + +cd ../frontend +npm ci +npm run build +npm audit +``` + +开发 Web 前端时分别运行后端 `npm run dev` 与前端 `npm run dev`。生产环境只服务 `frontend/dist/`,不要手工修改构建产物或给资源 URL 追加版本号。 + +## 不可破坏的约束 + +1. `app.use(expressErrorHandler)` 必须保持为最后一个 `app.use`;CI 的架构测试会检查这一点。 +2. Express 4 使用 `express-async-errors` 捕获异步路由拒绝;该补丁必须在注册任何路由前加载。 +3. 限流器、下载安全计数和 OSS 用量缓存仍是进程内状态,PM2 必须保持单实例 `fork` 模式。扩容前先迁移到共享状态存储。 +4. `npm ci` 依赖严格同步的 lock 文件;禁止在部署脚本中删除 `package-lock.json`。 +5. Express 中间件顺序是安全边界:requestId/安全头/CORS/HTTPS/静态资源/API 限流/请求体/CSRF,不得无验证调整。 +6. 桌面安装包目录 `frontend/downloads/` 是运行时发布目录,更新前必须备份且不得放进 `frontend/dist/` 的清理生命周期。 + +## 改动原则 + +- 路由继续按业务域渐进迁移,每次只迁移一个域并运行全部测试。 +- 新抽取的纯函数必须直接测试生产模块;禁止在测试文件复制实现。 +- 安全或配额核心逻辑应做一次变异验证:临时改坏生产实现,确认对应测试失败后立即还原。 +- 日志使用 `req.log` 或公共 logger,禁止记录密码、令牌、Cookie、Access Key 或完整授权头。 diff --git a/INSTALL_GUIDE.md b/INSTALL_GUIDE.md index 2495433..91ec800 100644 --- a/INSTALL_GUIDE.md +++ b/INSTALL_GUIDE.md @@ -10,7 +10,7 @@ - **磁盘空间**: 至少 2GB 可用空间 ### 软件依赖 -- **Node.js**: 20.x LTS +- **Node.js**: 20.19+(支持 20.19-24) - **Nginx**: 1.18+ - **Git**: 2.x @@ -27,7 +27,7 @@ curl -fsSL https://deb.nodesource.com/setup_20.x | sudo -E bash - sudo apt-get install -y nodejs # 验证安装 -node -v # 应显示 v20.x.x +node -v # 应显示 v20.19.0 或更高版本 npm -v ``` @@ -88,6 +88,19 @@ npm ci --omit=dev mkdir -p data storage ``` +### 4.1 构建 Web 前端 + +```bash +cd /var/www/wanwanyun/frontend + +# Vite 是构建依赖,生产构建时也必须安装 devDependencies +npm ci --include=dev +npm run build + +# Nginx 只需要 dist,可按需释放依赖占用 +rm -rf node_modules +``` + ### 5. 配置环境变量 ```bash @@ -115,7 +128,8 @@ ADMIN_PASSWORD=你的强密码 sudo cp /var/www/wanwanyun/nginx/nginx.conf /etc/nginx/sites-available/wanwanyun # 修改配置中的路径 -sudo sed -i 's|/usr/share/nginx/html|/var/www/wanwanyun/frontend|g' /etc/nginx/sites-available/wanwanyun +sudo sed -i 's|/usr/share/nginx/html|/var/www/wanwanyun/frontend/dist|g' /etc/nginx/sites-available/wanwanyun +sudo sed -i 's|/runtime/downloads|/var/www/wanwanyun/frontend/downloads|g' /etc/nginx/sites-available/wanwanyun sudo sed -i 's|backend:40001|127.0.0.1:40001|g' /etc/nginx/sites-available/wanwanyun # 创建软链接启用配置 @@ -245,6 +259,7 @@ sudo tail -f /var/log/nginx/error.log cd /var/www/wanwanyun sudo git pull cd backend && npm ci --omit=dev +cd ../frontend && npm ci --include=dev && npm run build && rm -rf node_modules sudo systemctl restart wanwanyun ``` @@ -294,14 +309,9 @@ sudo chown -R www-data:www-data /var/www/wanwanyun/backend/data/ ## 性能优化 -### 启用 Nginx 缓存 -在 Nginx 配置的 `location /` 中添加: -```nginx -location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2)$ { - expires 30d; - add_header Cache-Control "public, immutable"; -} -``` +### Nginx 静态缓存 + +仓库配置已经对 Vite 哈希资源 `/assets/` 设置一年缓存,对兼容库 `/libs/` 设置 30 天缓存。不要在子 `location` 中单独使用 `add_header Cache-Control`,否则会覆盖上层继承的安全响应头。 ### 配置日志轮转 ```bash diff --git a/README.md b/README.md index 071549e..60d7c5d 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ ![Version](https://img.shields.io/badge/version-3.1.0-blue.svg) ![License](https://img.shields.io/badge/license-Personal%20Use-green.svg) -![Node](https://img.shields.io/badge/node-20.x-brightgreen.svg) +![Node](https://img.shields.io/badge/node-20.19%2B-brightgreen.svg) ![Vue](https://img.shields.io/badge/vue-3.x-42b883.svg) @@ -72,7 +72,7 @@ ### 环境要求 - **操作系统**: Linux (Ubuntu 18.04+ / Debian 10+ / CentOS 7+) -- **Node.js**: 20.x LTS(支持 20-24,推荐与 `.nvmrc` 保持一致) +- **Node.js**: 20.19+(支持 20.19-24,推荐与 `.nvmrc` 保持一致) - **内存**: 最低 1GB RAM(推荐 2GB+) - **磁盘空间**: 至少 2GB 可用空间 - **后端实例数**: 当前限流与用量缓存为进程内状态,PM2 必须保持单实例 `fork` 模式 @@ -234,13 +234,16 @@ vue-driven-cloud-storage/ │ ├── data/ # 数据库目录 │ └── storage/ # 本地存储目录 │ -├── frontend/ # 前端代码 +├── frontend/ # Vite 多页面 Web 前端 │ ├── index.html # 登录注册页面 │ ├── app.html # 主应用页面 │ ├── share.html # 分享页面 │ ├── verify.html # 邮箱验证页面 │ ├── reset-password.html # 密码重置页面 -│ └── libs/ # 第三方库 (Vue.js, Axios, FontAwesome) +│ ├── package.json # Vue/Axios/Vite 依赖 +│ ├── vite.config.js # 多页面生产构建 +│ ├── dist/ # 构建产物(不入库) +│ └── libs/ # 分享页兼容静态资源与 Font Awesome │ ├── nginx/ # Nginx 配置 │ ├── nginx.conf # 反向代理配置 @@ -272,6 +275,7 @@ vue-driven-cloud-storage/ ### 前端技术 - **Vue.js 3** - 渐进式 JavaScript 框架 - **Axios** - HTTP 请求库 +- **Vite 8** - 多页面构建、依赖打包与哈希缓存 - **Font Awesome** - 图标库 - **原生 CSS** - 现代化界面设计 @@ -310,7 +314,7 @@ vue-driven-cloud-storage/ ```bash # Systemd 部署 -sudo systemctl status vue-cloud-storage +sudo systemctl status wanwanyun # Docker 部署 docker-compose ps @@ -320,7 +324,7 @@ docker-compose ps ```bash # Systemd 部署 -sudo journalctl -u vue-cloud-storage -f +sudo journalctl -u wanwanyun -f # Docker 部署 docker-compose logs -f backend @@ -330,7 +334,7 @@ docker-compose logs -f backend ```bash # Systemd 部署 -sudo systemctl restart vue-cloud-storage +sudo systemctl restart wanwanyun # Docker 部署 docker-compose restart @@ -340,23 +344,26 @@ docker-compose restart ```bash # 备份数据库 -sudo cp /var/www/vue-driven-cloud-storage/backend/data/database.db \ +sudo cp /var/www/wanwanyun/backend/data/database.db \ /backup/database.db.$(date +%Y%m%d) # 备份上传文件(本地存储模式) sudo tar -czf /backup/uploads-$(date +%Y%m%d).tar.gz \ - /var/www/vue-driven-cloud-storage/backend/storage/ + /var/www/wanwanyun/backend/storage/ ``` ### 更新系统 ```bash -cd /var/www/vue-driven-cloud-storage +cd /var/www/wanwanyun git pull -cd backend && npm install -sudo systemctl restart vue-cloud-storage +cd backend && npm ci --omit=dev +cd ../frontend && npm ci --include=dev && npm run build +sudo systemctl restart wanwanyun ``` +`install.sh --update` 会在替换线上文件前构建前端,并保留 `frontend/downloads/` 中由管理端发布的桌面安装包。完整工程约束见 [CONTRIBUTING.md](./CONTRIBUTING.md)。 + ## 📊 性能优化建议 ### 生产环境配置 @@ -401,7 +408,7 @@ A: 当前默认限制 10GB,可在系统设置和 Nginx 配置中同步调整 ### 故障排查 **Q: 无法访问系统** -1. 检查服务是否启动:`sudo systemctl status vue-cloud-storage` +1. 检查服务是否启动:`sudo systemctl status wanwanyun` 2. 检查防火墙是否开放端口 3. 查看 Nginx 日志:`sudo tail -f /var/log/nginx/error.log` @@ -484,11 +491,14 @@ A: 当前默认限制 10GB,可在系统设置和 Nginx 配置中同步调整 git clone https://git.workyai.cn/237899745/vue-driven-cloud-storage.git cd vue-driven-cloud-storage -# 安装依赖 -cd backend && npm install +# 安装并测试后端 +cd backend && npm ci && npm test -# 启动开发服务器 -node server.js +# 终端一:启动后端 +npm run dev + +# 终端二:启动 Vite(项目根目录执行) +cd ../frontend && npm ci && npm run dev ``` ### 提交规范 diff --git a/docker-compose.yml b/docker-compose.yml index f1082e0..37a48f8 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -7,8 +7,6 @@ # 3. 访问: http://localhost (或配置的域名) # ============================================ -version: '3.8' - services: # ============================================ # 后端服务 @@ -32,6 +30,8 @@ services: # 数据持久化 - ./backend/data:/app/data - ./backend/storage:/app/storage + # 桌面安装包由管理端发布,前后端共享此目录 + - ./frontend/downloads:/frontend/downloads networks: - wanwanyun-network healthcheck: @@ -45,15 +45,17 @@ services: # Nginx 前端服务 # ============================================ nginx: - image: nginx:alpine + build: + context: ./frontend + dockerfile: Dockerfile container_name: wanwanyun-nginx restart: unless-stopped ports: - "80:80" - "443:443" volumes: - # 前端静态文件 - - ./frontend:/usr/share/nginx/html:ro + # 动态发布的桌面安装包覆盖镜像内初始版本 + - ./frontend/downloads:/runtime/downloads:ro # Nginx 配置 - ./nginx/nginx.conf:/etc/nginx/conf.d/default.conf:ro # SSL 证书(如有) diff --git a/frontend/.dockerignore b/frontend/.dockerignore new file mode 100644 index 0000000..7f171e6 --- /dev/null +++ b/frontend/.dockerignore @@ -0,0 +1,3 @@ +node_modules +dist +npm-debug.log* diff --git a/frontend/Dockerfile b/frontend/Dockerfile new file mode 100644 index 0000000..b00f587 --- /dev/null +++ b/frontend/Dockerfile @@ -0,0 +1,13 @@ +FROM node:20-alpine AS build + +WORKDIR /app +COPY package*.json ./ +RUN npm ci +COPY . . +RUN npm run build + +FROM nginx:alpine +COPY --from=build /app/dist /usr/share/nginx/html +COPY --from=build /app/downloads /runtime/downloads + +EXPOSE 80 diff --git a/install.sh b/install.sh index 7e21beb..624155b 100644 --- a/install.sh +++ b/install.sh @@ -35,6 +35,7 @@ PROJECT_NAME="wanwanyun" PROJECT_DIR="/var/www/${PROJECT_NAME}" REPO_URL="https://git.workyai.cn/237899745/vue-driven-cloud-storage.git" NODE_VERSION="20" +NODE_MIN_VERSION="20.19.0" ADMIN_USERNAME="" ADMIN_PASSWORD="" DOMAIN="" @@ -529,13 +530,17 @@ install_dependencies() { echo "" } +node_version_supported() { + node -e "const [major, minor] = process.versions.node.split('.').map(Number); process.exit((major > 20 && major < 25) || (major === 20 && minor >= 19) ? 0 : 1)" 2>/dev/null +} + install_nodejs_apt() { if command -v node &> /dev/null; then - NODE_VER=$(node -v | cut -d'v' -f2 | cut -d'.' -f1) - if [[ $NODE_VER -ge $NODE_VERSION ]]; then + if node_version_supported; then print_success "Node.js 已安装: $(node -v)" return fi + print_warning "当前 Node.js $(node -v) 不满足 ${NODE_MIN_VERSION} 至 24.x,准备安装 20.x" fi print_info "正在安装 Node.js ${NODE_VERSION}.x..." @@ -546,11 +551,11 @@ install_nodejs_apt() { install_nodejs_yum() { if command -v node &> /dev/null; then - NODE_VER=$(node -v | cut -d'v' -f2 | cut -d'.' -f1) - if [[ $NODE_VER -ge $NODE_VERSION ]]; then + if node_version_supported; then print_success "Node.js 已安装: $(node -v)" return fi + print_warning "当前 Node.js $(node -v) 不满足 ${NODE_MIN_VERSION} 至 24.x,准备安装 20.x" fi print_info "正在安装 Node.js ${NODE_VERSION}.x..." @@ -585,11 +590,11 @@ install_nginx_yum() { install_nodejs_dnf() { if command -v node &> /dev/null; then - NODE_VER=$(node -v | cut -d'v' -f2 | cut -d'.' -f1) - if [[ $NODE_VER -ge $NODE_VERSION ]]; then + if node_version_supported; then print_success "Node.js 已安装: $(node -v)" return fi + print_warning "当前 Node.js $(node -v) 不满足 ${NODE_MIN_VERSION} 至 24.x,准备安装 20.x" fi print_info "正在安装 Node.js ${NODE_VERSION}.x..." @@ -612,11 +617,11 @@ install_nginx_dnf() { install_nodejs_zypper() { if command -v node &> /dev/null; then - NODE_VER=$(node -v | cut -d'v' -f2 | cut -d'.' -f1) - if [[ $NODE_VER -ge $NODE_VERSION ]]; then + if node_version_supported; then print_success "Node.js 已安装: $(node -v)" return fi + print_warning "当前 Node.js $(node -v) 不满足 ${NODE_MIN_VERSION} 至 24.x,准备安装 20.x" fi print_info "正在安装 Node.js ${NODE_VERSION}.x..." @@ -1390,7 +1395,7 @@ deploy_certbot() { echo "" print_info "正在申请 Let's Encrypt 证书..." - if certbot certonly --webroot -w "${PROJECT_DIR}/frontend" -d "$DOMAIN" --non-interactive --agree-tos --email "admin@${DOMAIN}"; then + if certbot certonly --webroot -w "${PROJECT_DIR}/frontend/dist" -d "$DOMAIN" --non-interactive --agree-tos --email "admin@${DOMAIN}"; then # 将证书复制到Nginx SSL目录 mkdir -p /etc/nginx/ssl ln -sf "/etc/letsencrypt/live/${DOMAIN}/fullchain.pem" "/etc/nginx/ssl/${DOMAIN}.crt" @@ -1541,7 +1546,7 @@ deploy_acme_letsencrypt() { # 使用webroot模式申请证书(更可靠) # 先尝试正常申请,如果证书已存在则使用--force强制更新 - if ~/.acme.sh/acme.sh --issue -d "$DOMAIN" --webroot "${PROJECT_DIR}/frontend"; then + if ~/.acme.sh/acme.sh --issue -d "$DOMAIN" --webroot "${PROJECT_DIR}/frontend/dist"; then print_success "证书申请成功" else # 检查是否是因为证书已存在 @@ -1707,7 +1712,7 @@ deploy_acme_zerossl() { fi # 使用webroot模式申请证书(更可靠) - if ~/.acme.sh/acme.sh --server zerossl --issue -d "$DOMAIN" --webroot "${PROJECT_DIR}/frontend"; then + if ~/.acme.sh/acme.sh --server zerossl --issue -d "$DOMAIN" --webroot "${PROJECT_DIR}/frontend/dist"; then print_success "证书申请成功" else # 检查是否是因为证书已存在 @@ -1866,7 +1871,7 @@ deploy_acme_buypass() { fi # 使用webroot模式申请证书(更可靠) - if ~/.acme.sh/acme.sh --server buypass --issue -d "$DOMAIN" --webroot "${PROJECT_DIR}/frontend"; then + if ~/.acme.sh/acme.sh --server buypass --issue -d "$DOMAIN" --webroot "${PROJECT_DIR}/frontend/dist"; then print_success "证书申请成功" else # 检查是否是因为证书已存在 @@ -2113,6 +2118,26 @@ install_backend_dependencies() { echo "" } +install_frontend_dependencies() { + print_step "安装前端依赖并构建生产资源..." + + cd "${PROJECT_DIR}/frontend" + if [[ ! -f "package-lock.json" ]]; then + print_error "前端 package-lock.json 不存在,无法执行可重复构建" + exit 1 + fi + + if npm ci --include=dev && npm run build; then + rm -rf node_modules + print_success "前端生产资源构建完成" + else + print_error "前端构建失败" + print_info "可手动重试: cd ${PROJECT_DIR}/frontend && npm ci --include=dev && npm run build" + exit 1 + fi + echo "" +} + create_env_file() { print_step "创建配置文件..." @@ -2212,6 +2237,15 @@ PUBLIC_PORT=${HTTP_PORT} # CSRF 保护(生产环境强烈建议开启) # 使用 Double Submit Cookie 模式防止跨站请求伪造攻击 ENABLE_CSRF=true + +# 全局 API 限流与健康检查阈值 +API_RATE_LIMIT_WINDOW_MS=60000 +API_RATE_LIMIT_MAX=600 +HEALTH_MIN_DISK_FREE_BYTES=268435456 + +# 结构化日志 +LOG_LEVEL=info +LOG_FORMAT=json EOF print_success "配置文件创建完成" @@ -2369,7 +2403,9 @@ server { add_header X-Frame-Options "SAMEORIGIN" always; add_header X-Content-Type-Options "nosniff" always; add_header X-XSS-Protection "1; mode=block" always; - add_header Referrer-Policy "no-referrer-when-downgrade" always; + add_header Referrer-Policy "strict-origin-when-cross-origin" always; + add_header Permissions-Policy "camera=(), geolocation=(), microphone=()" always; + add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob: https:; font-src 'self' data:; media-src 'self' blob: https:; connect-src 'self' https: ws: wss:; worker-src 'self' blob:; object-src 'none'; base-uri 'self'; frame-ancestors 'self'; form-action 'self';" always; # 隐藏Nginx版本号 server_tokens off; @@ -2388,11 +2424,21 @@ server { # 前端静态文件 location / { - root ${PROJECT_DIR}/frontend; + root ${PROJECT_DIR}/frontend/dist; index index.html; try_files \$uri \$uri/ =404; } + location /assets/ { + alias ${PROJECT_DIR}/frontend/dist/assets/; + expires 1y; + } + + location /downloads/ { + alias ${PROJECT_DIR}/frontend/downloads/; + expires 1h; + } + # 后端API location /api { proxy_pass http://localhost:${BACKEND_PORT}; @@ -2404,6 +2450,7 @@ server { proxy_set_header X-Real-IP \$remote_addr; proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto \$scheme; + proxy_set_header X-Request-ID \$request_id; # Cookie传递配置(验证码session需要) proxy_set_header Cookie \$http_cookie; @@ -2422,15 +2469,15 @@ server { proxy_set_header X-Real-IP \$remote_addr; proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto \$scheme; + proxy_set_header X-Request-ID \$request_id; } # 静态资源 - location /libs { - alias ${PROJECT_DIR}/frontend/libs; + location /libs/ { + alias ${PROJECT_DIR}/frontend/dist/libs/; expires 30d; } - } } EOF @@ -2656,7 +2703,9 @@ server { add_header X-Frame-Options "SAMEORIGIN" always; add_header X-Content-Type-Options "nosniff" always; add_header X-XSS-Protection "1; mode=block" always; - add_header Referrer-Policy "no-referrer-when-downgrade" always; + add_header Referrer-Policy "strict-origin-when-cross-origin" always; + add_header Permissions-Policy "camera=(), geolocation=(), microphone=()" always; + add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob: https:; font-src 'self' data:; media-src 'self' blob: https:; connect-src 'self' https: ws: wss:; worker-src 'self' blob:; object-src 'none'; base-uri 'self'; frame-ancestors 'self'; form-action 'self';" always; # 隐藏Nginx版本号 server_tokens off; @@ -2675,11 +2724,21 @@ server { # 前端静态文件 location / { - root ${PROJECT_DIR}/frontend; + root ${PROJECT_DIR}/frontend/dist; index index.html; try_files \$uri \$uri/ =404; } + location /assets/ { + alias ${PROJECT_DIR}/frontend/dist/assets/; + expires 1y; + } + + location /downloads/ { + alias ${PROJECT_DIR}/frontend/downloads/; + expires 1h; + } + # 后端API location /api { proxy_pass http://localhost:${BACKEND_PORT}; @@ -2691,6 +2750,7 @@ server { proxy_set_header X-Real-IP \$remote_addr; proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto \$scheme; + proxy_set_header X-Request-ID \$request_id; # Cookie传递配置(验证码session需要) proxy_set_header Cookie \$http_cookie; @@ -2709,15 +2769,15 @@ server { proxy_set_header X-Real-IP \$remote_addr; proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto \$scheme; + proxy_set_header X-Request-ID \$request_id; } # 静态资源 - location /libs { - alias ${PROJECT_DIR}/frontend/libs; + location /libs/ { + alias ${PROJECT_DIR}/frontend/dist/libs/; expires 30d; } - } } EOF @@ -2792,7 +2852,9 @@ server { add_header X-Frame-Options "SAMEORIGIN" always; add_header X-Content-Type-Options "nosniff" always; add_header X-XSS-Protection "1; mode=block" always; - add_header Referrer-Policy "no-referrer-when-downgrade" always; + add_header Referrer-Policy "strict-origin-when-cross-origin" always; + add_header Permissions-Policy "camera=(), geolocation=(), microphone=()" always; + add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob: https:; font-src 'self' data:; media-src 'self' blob: https:; connect-src 'self' https: ws: wss:; worker-src 'self' blob:; object-src 'none'; base-uri 'self'; frame-ancestors 'self'; form-action 'self';" always; add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always; # 隐藏Nginx版本号 @@ -2812,11 +2874,21 @@ server { # 前端静态文件 location / { - root ${PROJECT_DIR}/frontend; + root ${PROJECT_DIR}/frontend/dist; index index.html; try_files \$uri \$uri/ =404; } + location /assets/ { + alias ${PROJECT_DIR}/frontend/dist/assets/; + expires 1y; + } + + location /downloads/ { + alias ${PROJECT_DIR}/frontend/downloads/; + expires 1h; + } + # 后端API location /api { proxy_pass http://localhost:${BACKEND_PORT}; @@ -2828,6 +2900,7 @@ server { proxy_set_header X-Real-IP \$remote_addr; proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto \$scheme; + proxy_set_header X-Request-ID \$request_id; # Cookie传递配置(验证码session需要) proxy_set_header Cookie \$http_cookie; @@ -2846,15 +2919,15 @@ server { proxy_set_header X-Real-IP \$remote_addr; proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto \$scheme; + proxy_set_header X-Request-ID \$request_id; } # 静态资源 - location /libs { - alias ${PROJECT_DIR}/frontend/libs; + location /libs/ { + alias ${PROJECT_DIR}/frontend/dist/libs/; expires 30d; } - } } EOF @@ -3398,6 +3471,12 @@ update_backup_important_files() { print_success "配置文件已备份" fi + # 桌面安装包可能由管理端动态上传,不可随前端源码更新丢失 + if [[ -d "${PROJECT_DIR}/frontend/downloads" ]]; then + cp -a "${PROJECT_DIR}/frontend/downloads" "$TEMP_BACKUP/frontend-downloads" + print_success "桌面安装包已备份" + fi + print_success "备份完成: $TEMP_BACKUP" echo "" } @@ -3424,6 +3503,23 @@ update_pull_latest_code() { # 克隆最新代码 git clone "$REPO_URL" "${PROJECT_NAME}-update" + # 在替换线上文件前完成前端构建,失败时旧版本仍保持完整。 + if [[ -d "$TEMP_BACKUP/frontend-downloads" ]]; then + mkdir -p "/tmp/${PROJECT_NAME}-update/frontend/downloads" + cp -a "$TEMP_BACKUP/frontend-downloads/." "/tmp/${PROJECT_NAME}-update/frontend/downloads/" + fi + + print_info "构建前端生产资源..." + cd "/tmp/${PROJECT_NAME}-update/frontend" + if ! npm ci --include=dev || ! npm run build; then + print_error "前端构建失败,线上文件未替换" + if command -v pm2 &> /dev/null; then + pm2 restart ${PROJECT_NAME}-backend || true + fi + exit 1 + fi + rm -rf node_modules + # 更新前端文件 print_info "更新前端文件..." if [[ -d "/tmp/${PROJECT_NAME}-update/frontend" ]]; then @@ -3470,14 +3566,109 @@ update_pull_latest_code() { print_success "配置文件已恢复" fi - # 清理临时文件 + # 临时源码可删除,数据备份保留到健康检查通过后再清理 rm -rf "/tmp/${PROJECT_NAME}-update" - rm -rf "$TEMP_BACKUP" print_success "代码更新完成" echo "" } +update_migrate_nginx_config() { + print_step "迁移 Nginx 前端构建目录..." + + local candidates=( + "/www/server/panel/vhost/nginx/${PROJECT_NAME}.conf" + "/etc/nginx/sites-available/${PROJECT_NAME}.conf" + "/etc/nginx/conf.d/${PROJECT_NAME}.conf" + ) + local migrated=false + local backup_dir="$TEMP_BACKUP/nginx" + mkdir -p "$backup_dir" + + for config_file in "${candidates[@]}"; do + [[ -f "$config_file" ]] || continue + migrated=true + + local backup_name + backup_name=$(echo "$config_file" | tr '/ ' '__') + cp -a "$config_file" "$backup_dir/$backup_name" + + sed -i "s|root ${PROJECT_DIR}/frontend;|root ${PROJECT_DIR}/frontend/dist;|g" "$config_file" + sed -i "s|location /libs {|location /libs/ {|g" "$config_file" + sed -i "s|alias ${PROJECT_DIR}/frontend/libs;|alias ${PROJECT_DIR}/frontend/dist/libs/;|g" "$config_file" + sed -i 's|Referrer-Policy "no-referrer-when-downgrade"|Referrer-Policy "strict-origin-when-cross-origin"|g' "$config_file" + + if ! grep -q "add_header Permissions-Policy" "$config_file" || ! grep -q "add_header Content-Security-Policy" "$config_file"; then + local secured_file="${config_file}.security.$$" + local need_permissions=0 + local need_csp=0 + grep -q "add_header Permissions-Policy" "$config_file" || need_permissions=1 + grep -q "add_header Content-Security-Policy" "$config_file" || need_csp=1 + awk -v need_permissions="$need_permissions" -v need_csp="$need_csp" ' + /add_header Referrer-Policy/ && !inserted_security { + print + if (need_permissions == 1) { + print " add_header Permissions-Policy \"camera=(), geolocation=(), microphone=()\" always;" + } + if (need_csp == 1) { + print " add_header Content-Security-Policy \"default-src \047self\047; script-src \047self\047 \047unsafe-inline\047 \047unsafe-eval\047; style-src \047self\047 \047unsafe-inline\047; img-src \047self\047 data: blob: https:; font-src \047self\047 data:; media-src \047self\047 blob: https:; connect-src \047self\047 https: ws: wss:; worker-src \047self\047 blob:; object-src \047none\047; base-uri \047self\047; frame-ancestors \047self\047; form-action \047self\047;\" always;" + } + inserted_security = 1 + next + } + { print } + ' "$config_file" > "$secured_file" + mv "$secured_file" "$config_file" + fi + + if ! grep -q "location /downloads/" "$config_file"; then + local migrated_file="${config_file}.migrate.$$" + awk -v project_dir="$PROJECT_DIR" ' + /^[[:space:]]*# 静态资源/ && !inserted { + print " # 桌面安装包由管理端动态发布,不属于 Vite dist" + print " location /downloads/ {" + print " alias " project_dir "/frontend/downloads/;" + print " expires 1h;" + print " }" + print "" + inserted = 1 + } + { print } + ' "$config_file" > "$migrated_file" + mv "$migrated_file" "$config_file" + fi + + if ! grep -q "root ${PROJECT_DIR}/frontend/dist;" "$config_file" || ! grep -q "location /downloads/" "$config_file"; then + print_error "无法安全迁移 Nginx 配置: $config_file" + cp -a "$backup_dir/$backup_name" "$config_file" + return 1 + fi + done + + if [[ "$migrated" != "true" ]]; then + print_warning "未找到 install.sh 管理的 Nginx 配置,请手动确认 root 指向 ${PROJECT_DIR}/frontend/dist" + return 0 + fi + + if ! nginx -t; then + print_error "迁移后的 Nginx 配置校验失败,正在恢复" + for config_file in "${candidates[@]}"; do + [[ -f "$config_file" ]] || continue + local backup_name + backup_name=$(echo "$config_file" | tr '/ ' '__') + if [[ -f "$backup_dir/$backup_name" ]]; then + cp -a "$backup_dir/$backup_name" "$config_file" + fi + done + nginx -t || true + return 1 + fi + + restart_nginx_safe + print_success "Nginx 已切换到 Vite 构建目录" + echo "" +} + update_install_dependencies() { print_step "更新后端依赖..." @@ -3511,7 +3702,7 @@ update_install_dependencies() { check_cpp_compiler if [[ -d "node_modules" ]]; then print_info "清理旧依赖..." - rm -rf node_modules package-lock.json + rm -rf node_modules fi print_info "正在重新安装依赖(可能需要几分钟)..." @@ -3521,6 +3712,7 @@ update_install_dependencies() { else print_error "依赖更新失败" print_warning "请检查错误日志: ~/.npm/_logs/" + return 1 fi echo "" @@ -3750,6 +3942,23 @@ update_patch_env() { else print_info ".env 已包含 ENABLE_CSRF,保持不变" fi + + if ! grep -q "^API_RATE_LIMIT_MAX=" "${PROJECT_DIR}/backend/.env"; then + echo "API_RATE_LIMIT_WINDOW_MS=60000" >> "${PROJECT_DIR}/backend/.env" + echo "API_RATE_LIMIT_MAX=600" >> "${PROJECT_DIR}/backend/.env" + print_info "已补充全局 API 限流默认值" + fi + + if ! grep -q "^HEALTH_MIN_DISK_FREE_BYTES=" "${PROJECT_DIR}/backend/.env"; then + echo "HEALTH_MIN_DISK_FREE_BYTES=268435456" >> "${PROJECT_DIR}/backend/.env" + print_info "已补充健康检查磁盘阈值" + fi + + if ! grep -q "^LOG_FORMAT=" "${PROJECT_DIR}/backend/.env"; then + echo "LOG_LEVEL=info" >> "${PROJECT_DIR}/backend/.env" + echo "LOG_FORMAT=json" >> "${PROJECT_DIR}/backend/.env" + print_info "已启用 JSON 结构化日志" + fi else print_warning "未找到 ${PROJECT_DIR}/backend/.env,请手动确认配置" fi @@ -3815,6 +4024,9 @@ update_main() { # 拉取最新代码 update_pull_latest_code + # 旧版本 Nginx 指向 frontend 源码,需迁移到 dist + update_migrate_nginx_config + # 更新依赖 update_install_dependencies @@ -3831,9 +4043,12 @@ update_main() { if ! health_check; then print_error "健康检查未通过,请检查日志" print_info "查看日志: pm2 logs ${PROJECT_NAME}-backend" + print_info "更新备份保留在: $TEMP_BACKUP" exit 1 fi + rm -rf "$TEMP_BACKUP" + # 检查版本 update_check_version @@ -3951,6 +4166,9 @@ main() { # 安装后端依赖 install_backend_dependencies + # 安装前端依赖并生成 dist + install_frontend_dependencies + # 创建配置文件 create_env_file diff --git a/nginx/nginx.conf b/nginx/nginx.conf index 740be5b..79cf393 100644 --- a/nginx/nginx.conf +++ b/nginx/nginx.conf @@ -9,7 +9,9 @@ server { add_header X-Frame-Options "SAMEORIGIN" always; add_header X-Content-Type-Options "nosniff" always; add_header X-XSS-Protection "1; mode=block" always; - add_header Referrer-Policy "no-referrer-when-downgrade" always; + add_header Referrer-Policy "strict-origin-when-cross-origin" always; + add_header Permissions-Policy "camera=(), geolocation=(), microphone=()" always; + add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob: https:; font-src 'self' data:; media-src 'self' blob: https:; connect-src 'self' https: ws: wss:; worker-src 'self' blob:; object-src 'none'; base-uri 'self'; frame-ancestors 'self'; form-action 'self';" always; # 隐藏Nginx版本 server_tokens off; @@ -32,6 +34,21 @@ server { try_files $uri $uri/ =404; } + location /assets/ { + root /usr/share/nginx/html; + expires 1y; + } + + location /libs/ { + root /usr/share/nginx/html; + expires 30d; + } + + location /downloads/ { + alias /runtime/downloads/; + expires 1h; + } + # 后端API反向代理 location /api/ { proxy_pass http://backend:40001; @@ -43,6 +60,7 @@ server { proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; # 修复:使用当前请求协议(http或https),适用于直接IP访问 proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header X-Request-ID $request_id; proxy_cache_bypass $http_upgrade; # Cookie传递配置(验证码session需要) @@ -69,5 +87,6 @@ server { proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; # 修复:使用当前请求协议(http或https),适用于直接IP访问 proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header X-Request-ID $request_id; } } diff --git a/nginx/nginx.conf.example b/nginx/nginx.conf.example index 18f992e..0e9f432 100644 --- a/nginx/nginx.conf.example +++ b/nginx/nginx.conf.example @@ -49,6 +49,8 @@ server { add_header X-Content-Type-Options "nosniff" always; add_header X-XSS-Protection "1; mode=block" always; add_header Referrer-Policy "strict-origin-when-cross-origin" always; + add_header Permissions-Policy "camera=(), geolocation=(), microphone=()" always; + add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob: https:; font-src 'self' data:; media-src 'self' blob: https:; connect-src 'self' https: ws: wss:; worker-src 'self' blob:; object-src 'none'; base-uri 'self'; frame-ancestors 'self'; form-action 'self';" always; # 隐藏 Nginx 版本 server_tokens off; @@ -79,11 +81,21 @@ server { index index.html; try_files $uri $uri/ =404; - # 静态资源缓存 - location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2)$ { - expires 30d; - add_header Cache-Control "public, immutable"; - } + } + + location /assets/ { + root /usr/share/nginx/html; + expires 1y; + } + + location /libs/ { + root /usr/share/nginx/html; + expires 30d; + } + + location /downloads/ { + alias /runtime/downloads/; + expires 1h; } # ============================================ @@ -98,6 +110,7 @@ server { proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header X-Request-ID $request_id; proxy_cache_bypass $http_upgrade; # Cookie 传递配置(验证码 session 需要) @@ -125,5 +138,6 @@ server { proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header X-Request-ID $request_id; } }