chore: harden build and deployment workflow

This commit is contained in:
237899745
2026-07-27 13:07:14 +08:00
parent 6692736e88
commit f5377e51bb
10 changed files with 408 additions and 71 deletions

View File

@@ -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

35
CONTRIBUTING.md Normal file
View File

@@ -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 或完整授权头。

View File

@@ -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

View File

@@ -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)
</div>
@@ -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
```
### 提交规范

View File

@@ -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 证书(如有)

3
frontend/.dockerignore Normal file
View File

@@ -0,0 +1,3 @@
node_modules
dist
npm-debug.log*

13
frontend/Dockerfile Normal file
View File

@@ -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

View File

@@ -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

View File

@@ -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;
}
}

View File

@@ -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;
}
}