19 Commits

Author SHA1 Message Date
david_bai ffa9f84c4a fix(sendString):Avoid using the file fragmentation parameter to prevent transmission failure 2025-10-11 10:59:42 +08:00
david_bai 1e22481a00 docs(docker): align commands and access guidance with latest deployment model
- Use "bash ./deploy.sh" consistently across docs
- Prefer "docker compose" (Compose v2) and update examples accordingly
- Public mode: recommend "--with-nginx" for same-origin gateway
- Access: document Nginx (same-origin) vs direct ports; update HTTPS endpoints (lan-tls 8443, full 443)
- Health checks: add same-origin /api examples
- Add notes on NEXT_IMAGE_UNOPTIMIZED in Docker and same-origin behavior when --with-nginx is enabled
- Fix bare-metal docs cross-links to Docker guides
2025-10-10 23:25:12 +08:00
david_bai f0c4364dcd fix(config): lan-tls without --enable-web-https must still use same-origin when Nginx is enabled
- generate-config.sh: in lan-tls without HTTPS, set NEXT_PUBLIC_API_URL empty when WITH_NGINX=true so frontend uses relative /api and /socket.io; widen CORS origins to include http://localhost and http://<LOCAL_IP>
- deploy.sh: pass --with-nginx to config generator for consistency
2025-10-10 20:59:20 +08:00
david_bai 8ef43029d5 fix(deploy+docker+frontend): enforce same-origin via Nginx, disable Next Image optimization in Docker, allow Socket.IO polling fallback, and improve health checks and access info
- generate-config.sh: add --with-nginx flag handling; when enabled, set NEXT_PUBLIC_API_URL empty to use same-origin /api and /socket.io; add BACKEND_INTERNAL_URL for SSR/internal fetch; adjust lan-tls HTTPS (8443) and TLS generation policy
- deploy.sh: show only valid access URLs when Nginx is enabled (gateway URLs), avoid misleading :3002/:3001 entries
- frontend (env/webrtc): return mutable transports [websocket,polling]; use empty signaling server for same-origin; comments in English
- frontend (next.config): support NEXT_IMAGE_UNOPTIMIZED to turn off image optimization in Docker
- frontend (health): prefer BACKEND_INTERNAL_URL for internal health checks, fallback to public URL/localhost
- docker-compose + Dockerfile(frontend): pass NEXT_IMAGE_UNOPTIMIZED and BACKEND_INTERNAL_URL envs
2025-10-10 20:49:17 +08:00
david_bai 975f6e74ad docs: clarify LAN TLS (self-signed) usage — import CA in browser, correct HTTPS endpoints (8443), CORS guidance; fix deploy hints to only show reachable Nginx URLs. Also: trust CA in frontend container and align HTTPS port mapping. 2025-10-09 21:46:03 +08:00
david_bai dec59a12ec docs(readme): align Quick Start and Modes with new deployment model 2025-10-09 15:42:11 +08:00
david_bai 8590eda2c2 refactor(scripts): simplify modes and harden cert automation
- New modes: lan-http, lan-tls (self-signed), public, full
- Add flags: --no-sni443, --enable-web-https (lan-tls), --test-renewal
- generate-config: lan-tls HTTPS on 8443 only when explicitly enabled; HSTS only in full; SNI 443 default in full
- detect-environment: remove interactive prompt; adjust public description to 'HTTP + TURN'
- deploy.sh: pass new flags, add certbot scheduler (systemd timer or cron fallback), add dry-run renewal test
- Docs (EN/zh-CN): update quick start, modes overview, LAN TLS guidance, LE auto-issue/renew section
2025-10-08 23:17:19 +08:00
david_bai 1f4522eeb2 docs+scripts: move domain quick tips to docs; add modes overview; clarify full+SNI defaults; add self-signed domain guidance
- deploy.sh: replace verbose public domain test instructions with a single docs link
- docker/scripts/generate-config.sh: remove 'Intranet with TURN quick tip' from help; add docs pointers
- docs(zh/EN): add 'Modes Overview', add 'Private LAN + TURN' quick start example, add 'Domain + Self-signed' and 'Public Domain Quick Test' sections; note LE auto-issue/renew and SNI 443 default in full mode
2025-10-08 23:07:34 +08:00
david_bai 663082efe1 chore(doc): Replace Chinese comments with English comments 2025-10-08 15:59:50 +08:00
david_bai 2bd09835b1 docs(docker): elevate Docker one-click to top, add LE automation + SNI443, update flags and compose v2 commands
- DEPLOYMENT_docker.md/zh-CN: Add top Quick Start (private/public/full), Let’s Encrypt auto issue/renew (webroot, zero downtime), SNI 443 default for full+domain, common flags (--with-sni443, --turn-port-range, --le-email), replace docker-compose with docker compose.
- README.md/zh-CN: Promote Docker one-click section to top and link to docs.
- DEPLOYMENT.md/zh-CN: Add audience/scope notice; point to Docker docs for recommended path.
- ROADMAP.md/zh-CN: Record recently completed (Docker, LE, SNI, TURN).
2025-10-07 22:48:26 +08:00
david_bai 7809373f88 feat(nginx,sni): enable SNI-based 443 multiplexing (turns:443) and wiring
- docker/scripts/generate-config.sh
	- Add --enable-sni443/--no-sni443 flags; default enable in full+domain.
	- Generate Nginx stream{} with ssl_preread SNI routing: turn.<domain> -> coturn:5349; others -> web:8443.
	- When SNI is enabled, serve HTTPS on 8443 (http layer); otherwise keep 443.
- deploy.sh:
	- Add --with-sni443 and propagate to config generation and LE provisioning.
	- No compose changes required; 8443 remains internal.
- Notes:
	- Backward compatible. SNI is auto-enabled for full+domain, can be toggled with flags.
	- Leverages existing LE automation and TURN cert reuse.
2025-10-07 21:51:28 +08:00
david_bai 85baa97804 feat(ssl,deploy): safe cleanup, TURN auto-start, and HTTPS checks
- deploy.sh:
      - copy real cert lineage (handles -0001) into docker/ssl
      - Ensure TURN auto-start when requested; enhance clean (stop→rm, network fallback)
      - Add HTTPS post-deploy health check
      - Build fallback: parallel → serial on failure
      - Early-exit on clean-only mode
2025-10-07 21:22:51 +08:00
david_bai 246eff196e feat(deploy,ssl): automate Let’s Encrypt (webroot), preserve SSL, and auto-enable HTTPS
- generate-config.sh
      - Add flags: --no-clean, --reset-ssl, --ssl-mode (letsencrypt|self-signed|provided)
      - Stop deleting docker/ssl by default; only wipe on explicit --reset-ssl
      - Inject ACME webroot route into HTTP (80) server; create docker/letsencrypt-www
      - Default SSL_MODE: full=letsencrypt, private/public=self-signed
      - Add enable_https_if_cert_present: append 443 server only when server-cert.pem/server-key.pem exist
      - Keep self-signed path generating HTTPS immediately (non-basic)
  - docker-compose.yml
      - Mount ./docker/letsencrypt-www:/var/www/certbot:ro for Nginx ACME challenges
  - deploy.sh
      - Add --le-email for Let’s Encrypt account email
      - Auto-install certbot once (apt-get) and enable systemd timer if available
      - Install deploy hook at /etc/letsencrypt/renewal-hooks/deploy/privydrop-reload.sh to:
          - Copy renewed certs into docker/ssl
          - Hot-reload Nginx; HUP or restart coturn
      - First-time issuance (webroot) for <domain> and turn.<domain> after Nginx:80 is up; copy certs
      - Re-run generate-config with --no-clean --ssl-mode letsencrypt to enable 443, then reload Nginx
  - Behavior changes
      - Full mode prefers Let’s Encrypt by default; HTTPS gets enabled as soon as certs exist
      - docker/ssl is no longer wiped by config generation
  - Notes
      - SNI-based turns:443 is not implemented yet (planned)
      - Backward compatible with private/public (self-signed)
2025-10-05 12:43:56 +08:00
david_bai a498cc4799 chore(deploy): public output polish and public/full config fixes
- deploy.sh: show public endpoints (domain/public IP only); add TURN info (domain/public IP); prepend logs chmod 777; append HTTPS+Nginx quick-test tips.
- generate-config.sh: fix public/full CORS and NEXT_PUBLIC_API_URL; prefer PUBLIC_IP for TURN host when no domain; update help text.
2025-10-02 22:45:42 +08:00
david_bai 200fc65617 build(docker): Intranet deployment is successfully tested using turn
- Switch all CLI examples to Docker Compose V2 (docker compose) for consistency.
  - Add explicit instruction to grant write permissions to the host logs/ directory (chmod 777 -R logs) to fix coturn/nginx bind-mount logging errors.
  - Parameterize TURN UDP port range via TURN_MIN_PORT/TURN_MAX_PORT and set a safer default 49152-49252 to reduce startup/cleanup overhead and port
  conflicts.
  - Update troubleshooting with coturn log write failure guidance and port conflict hints.
  - Clarify that LAN IP is auto-detected in private mode; --local-ip is no longer needed by default but remains as an override for edge cases.
2025-09-30 14:01:30 +08:00
david_bai 2ee6961634 build(docker): Private mode deployment test successful
Test steps:
bash docker/scripts/generate-config.sh --mode private [--local-ip 192.168.0.113]
bash ./deploy.sh --mode private

Front-end directly inlines NEXT_PUBLIC_API_URL, directly connecting to the backend.
CORS (production) supports comma-separated multiple origins, with localhost and local network IPs included by default.
2025-09-29 18:27:12 +08:00
david_bai cfcd60145a build: refresh docker deployment workflow 2025-09-26 14:02:55 +08:00
david_bai 67b46d0b30 Merge branch 'fea/docker2' into fea/docker 2025-09-21 22:10:30 +08:00
david_bai 158433bb0b chore:Initial addition of Docker related content 2025-09-11 06:46:04 +08:00
38 changed files with 5247 additions and 222 deletions
+56
View File
@@ -0,0 +1,56 @@
# Git相关
.git
.gitignore
# 环境变量文件
.env*
!.env.docker.example
# 日志文件
logs/
*.log
# 依赖目录
node_modules/
frontend/node_modules/
backend/node_modules/
# 构建目录
.next/
dist/
build/
# 缓存目录
.npm
.pnpm-store
# 临时文件
*.tmp
*.temp
# 编辑器配置
.vscode/
.idea/
*.swp
*.swo
# 操作系统文件
.DS_Store
Thumbs.db
# Docker相关
Dockerfile
.dockerignore
docker-compose*.yml
# 文档文件
*.md
docs/
# 测试文件
coverage/
.nyc_output/
# 其他
.eslintcache
tsconfig.tsbuildinfo
+6
View File
@@ -60,6 +60,12 @@ next-env.d.ts
# Build output
dist/
# Generated docker assets
docker/ssl/
docker/nginx/
docker/coturn/
logs/
# Temporary files
.temp/
.tmp/
+33 -1
View File
@@ -35,7 +35,39 @@ We believe everyone should have control over their own data. PrivyDrop was creat
- **Backend**: Node.js, Express.js, TypeScript
- **Real-time Communication**: WebRTC, Socket.IO
- **Data Storage**: Redis
- **Deployment**: PM2, Nginx, Docker [WIP]
- **Deployment**: PM2, Nginx, Docker
## 🐳 Docker One-Click Deployment (Recommended)
Deploy in minutes with zero manual configuration. Supports private/public networks and auto HTTPS (Lets Encrypt).
```bash
# Private LAN (no domain/public IP)
bash ./deploy.sh --mode lan-http
# Private LAN + TURN (for complex NAT/LAN)
bash ./deploy.sh --mode lan-http --with-turn
# LAN HTTPS (self-signed; dev/managed env; explicitly enable 8443)
bash ./deploy.sh --mode lan-tls --enable-web-https --with-nginx
# Public IP without domain (with TURN; recommended with Nginx for same-origin)
bash ./deploy.sh --mode public --with-turn --with-nginx
# Public domain (HTTPS + Nginx + TURN + SNI 443, auto-issue/renew)
bash ./deploy.sh --mode full --domain your-domain.com --with-nginx --with-turn --le-email you@domain.com
```
See [Docker Deployment Guide](./docs/DEPLOYMENT_docker.md) (Modes Overview, LAN TLS limitations, Lets Encrypt auto-issue/renew)
Heads-up (LAN TLS, self-signed)
- Import the CA certificate into your browser (or system trust store) on first use: `docker/ssl/ca-cert.pem`. Otherwise the browser shows “certificate not valid/untrusted”.
- Access endpoints (by default):
- Nginx: `http://localhost`
- HTTPS: `https://localhost:8443`, `https://<your LAN IP>:8443`
- Frontend dev ports (optional): `http://localhost:3002`, `http://<your LAN IP>:3002`
- When `--with-nginx` is enabled, the frontend and API are same-origin (`/api`, `/socket.io`) for stability; direct ports `:3002/:3001` are for debugging only and may cause CORS or 404.
- With `--enable-web-https` and the CA trusted, same-origin HTTPS (8443) avoids CORS; common dev origins (`localhost`, `:3002`) are allowed by default.
## 🚀 Quick Start (Full-Stack Local Development)
+45 -2
View File
@@ -35,9 +35,52 @@ PrivyDrop (原 SecureShare) 是一个基于 WebRTC 的开源点对点(P2P)
- **后端**: Node.js, Express.js, TypeScript
- **实时通信**: WebRTC, Socket.IO
- **数据存储**: Redis
- **部署**: PM2, Nginx, Docker[暂未支持]
- **部署**: PM2, Nginx, Docker
## 🚀 快速上手 (本地全栈开发)
## 🚀 快速上手
### 🐳 Docker 一键部署 (推荐)
**零配置,一条命令完成部署!支持内网/公网/域名,自动签发/续期 HTTPS。**
```bash
# 内网(无域名/无公网IP
bash ./deploy.sh --mode lan-http
# 内网 + TURN(推荐用于复杂内网/NAT)
bash ./deploy.sh --mode lan-http --with-turn
# 内网 HTTPS(自签,开发/受管环境,显式开启 8443)
bash ./deploy.sh --mode lan-tls --enable-web-https --with-nginx
# 公网IP(无域名),含 TURN(推荐同源经 Nginx)
bash ./deploy.sh --mode public --with-turn --with-nginx
# 公网域名(HTTPS + Nginx + TURN + SNI 443 分流,自动申请/续期证书)
bash ./deploy.sh --mode full --domain your-domain.com --with-nginx --with-turn --le-email you@domain.com
```
完整说明见: docs/DEPLOYMENT_docker.zh-CN.md(模式一览、LAN TLS、自签限制、Lets Encrypt 自动签发/续期)
提示(lan-tls 自签 HTTPS
- 首次访问需导入 CA 证书:`docker/ssl/ca-cert.pem` 到浏览器(或系统信任),否则浏览器会提示“证书无效/不受信任”。
- 访问方式(默认):
- Nginx: `http://localhost`
- HTTPS: `https://localhost:8443``https://<你的局域网IP>:8443`
- 前端开发口(可选): `http://localhost:3002``http://<你的局域网IP>:3002`
- 启用 `--with-nginx` 时,前端与 API 同源(/api、/socket.io)更稳定;直连 `:3002/:3001` 仅用于调试,可能导致 CORS 或 404。
- 若启用了 `--enable-web-https` 并导入 CA,浏览器与 API 走同源 HTTPS(8443)可避免 CORS;我们已默认放开常见开发来源(`localhost``:3002` 等)。
**部署优势**:
- ✅ 部署时间: 60 分钟 → 5 分钟
- ✅ 技术门槛: Linux 运维 → 会用 Docker 即可
- ✅ 环境要求: 公网 IP → 内网即可使用
- ✅ 成功率: 70% → 95%+
详见: [Docker 部署指南](./docs/DEPLOYMENT_docker.zh-CN.md)
### 💻 本地开发环境
在开始之前,请确保你的开发环境已安装 [Node.js](https://nodejs.org/) (v18+), [npm](https://www.npmjs.com/) 以及一个正在运行的 [Redis](https://redis.io/) 实例。
+10
View File
@@ -8,12 +8,22 @@ This roadmap is a living document. We welcome community feedback and contributio
## ✅ Completed
### Architecture optimization
- **Core Architecture Refactor (Q3 2025)**: Successfully refactored the entire frontend codebase to a modern, layered architecture.
- Implemented a framework-agnostic **Service Layer** (`webrtcService`) to encapsulate all WebRTC and business logic.
- Introduced **Zustand** for centralized, predictable state management (`fileTransferStore`).
- Decoupled UI components from business logic, establishing a clear, unidirectional data flow.
- **Resumable File Transfers (Q3 2025):** Implemented robust logic for resuming transfers from the point of interruption. This is enabled by setting a save directory, which allows the receiver to check for partially downloaded files and request only the missing chunks.
### Deployment and Operation
- Docker one-click deployment (Q4 20252)
- Unified container health checks (node health-check.js)
- Lets Encrypt automation (webroot) with zero-downtime renewals and deploy-hook
- TURN improvements (env port range; default 49152-49252)
- SNI 443 multiplexing (turns:443 via Nginx stream; enabled by default in full+domain)
---
## Short-Term Goals (Next 1-3 Months)
+12 -2
View File
@@ -8,11 +8,21 @@
## ✅ 已完成
- **核心架构重构 (2025年Q3)**: 成功地将整个前端代码库重构为现代化的分层架构。
### 架构优化
- **核心架构重构 (2025 年 Q3)**: 成功地将整个前端代码库重构为现代化的分层架构。
- 实现了一个与框架无关的**服务层** (`webrtcService`),用于封装所有 WebRTC 和业务逻辑。
- 引入 **Zustand** (`fileTransferStore`) 进行中心化的、可预测的状态管理。
- 将 UI 组件与业务逻辑解耦,建立了清晰的单向数据流。
- **文件断点续传 (2025Q3):** 实现了稳健的断点续传逻辑。通过设置保存目录,接收方能够检查已部分下载的文件,并仅请求缺失的数据块,极大地提升了大文件和不稳定网络下的传输成功率。
- **文件断点续传 (2025Q3):** 实现了稳健的断点续传逻辑。通过设置保存目录,接收方能够检查已部分下载的文件,并仅请求缺失的数据块,极大地提升了大文件和不稳定网络下的传输成功率。
### 部署与运维
- Docker 一键部署(2025 年 Q4
- 容器健康检查统一(node health-check.js
- Lets Encryptwebroot)自动化与续期 deploy-hook(无停机)
- TURN 端口段变量化与默认缩小(49152-49252
- SNI 443 分流(Nginx streamfull+domain 默认开启)
---
+13
View File
@@ -0,0 +1,13 @@
node_modules
npm-debug.log*
.npm
.env*
.git
.gitignore
README.md
Dockerfile
.dockerignore
coverage
.nyc_output
logs
*.log
+41
View File
@@ -0,0 +1,41 @@
# Build stage
FROM node:18-alpine AS builder
ARG HTTP_PROXY
ARG HTTPS_PROXY
ARG NO_PROXY
ENV http_proxy ${HTTP_PROXY} \
https_proxy ${HTTPS_PROXY} \
no_proxy ${NO_PROXY}
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
# Run stage
FROM node:18-alpine AS runtime
WORKDIR /app
# Copy prebuilt artifacts
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/node_modules ./node_modules
COPY --from=builder /app/package.json ./
COPY health-check.js ./
# Create user and set permissions
RUN addgroup -g 1001 -S nodejs && \
adduser -S backend -u 1001 -G nodejs && \
chown -R backend:nodejs /app
USER backend
EXPOSE 3001
# Use a Node.js script for health checks (instead of curl)
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
CMD node health-check.js
CMD ["node", "dist/server.js"]
+2 -2
View File
@@ -88,7 +88,7 @@ server {
}
# Next.js Image Optimization Service (usually handled by the Next.js application)
location /_next/image {
proxy_pass http://localhost:3000; # Point to the Next.js application
proxy_pass http://localhost:3002; # Point to the Next.js application
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
@@ -110,7 +110,7 @@ server {
}
# Named location, used to proxy requests to the Next.js application
location @nextjs_app {
proxy_pass http://localhost:3000; # Point to the Next.js application
proxy_pass http://localhost:3002; # Point to the Next.js application
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
+34
View File
@@ -0,0 +1,34 @@
#!/usr/bin/env node
const http = require('http');
const options = {
host: 'localhost',
port: process.env.BACKEND_PORT || 3001,
path: '/health',
timeout: 2000,
method: 'GET'
};
const req = http.request(options, (res) => {
if (res.statusCode === 200) {
console.log('Health check passed');
process.exit(0);
} else {
console.log(`Health check failed with status: ${res.statusCode}`);
process.exit(1);
}
});
req.on('error', (err) => {
console.log(`Health check failed: ${err.message}`);
process.exit(1);
});
req.on('timeout', () => {
console.log('Health check timeout');
req.destroy();
process.exit(1);
});
req.end();
+156 -156
View File
@@ -19,7 +19,7 @@ importers:
version: 4.21.2
ioredis:
specifier: ^5.4.1
version: 5.7.0
version: 5.8.0
socket.io:
specifier: ^4.8.1
version: 4.8.1
@@ -32,7 +32,7 @@ importers:
version: 5.0.3
'@types/node':
specifier: ^22.13.4
version: 22.17.2
version: 22.18.6
cross-env:
specifier: ^7.0.3
version: 7.0.3
@@ -41,10 +41,10 @@ importers:
version: 6.0.1
ts-node:
specifier: ^10.9.2
version: 10.9.2(@types/node@22.17.2)(typescript@5.9.2)
version: 10.9.2(@types/node@22.18.6)(typescript@5.9.2)
tsx:
specifier: ^4.19.2
version: 4.20.4
version: 4.20.5
typescript:
specifier: ^5.7.3
version: 5.9.2
@@ -55,164 +55,164 @@ packages:
resolution: {integrity: sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==}
engines: {node: '>=12'}
'@esbuild/aix-ppc64@0.25.9':
resolution: {integrity: sha512-OaGtL73Jck6pBKjNIe24BnFE6agGl+6KxDtTfHhy1HmhthfKouEcOhqpSL64K4/0WCtbKFLOdzD/44cJ4k9opA==}
'@esbuild/aix-ppc64@0.25.10':
resolution: {integrity: sha512-0NFWnA+7l41irNuaSVlLfgNT12caWJVLzp5eAVhZ0z1qpxbockccEt3s+149rE64VUI3Ml2zt8Nv5JVc4QXTsw==}
engines: {node: '>=18'}
cpu: [ppc64]
os: [aix]
'@esbuild/android-arm64@0.25.9':
resolution: {integrity: sha512-IDrddSmpSv51ftWslJMvl3Q2ZT98fUSL2/rlUXuVqRXHCs5EUF1/f+jbjF5+NG9UffUDMCiTyh8iec7u8RlTLg==}
'@esbuild/android-arm64@0.25.10':
resolution: {integrity: sha512-LSQa7eDahypv/VO6WKohZGPSJDq5OVOo3UoFR1E4t4Gj1W7zEQMUhI+lo81H+DtB+kP+tDgBp+M4oNCwp6kffg==}
engines: {node: '>=18'}
cpu: [arm64]
os: [android]
'@esbuild/android-arm@0.25.9':
resolution: {integrity: sha512-5WNI1DaMtxQ7t7B6xa572XMXpHAaI/9Hnhk8lcxF4zVN4xstUgTlvuGDorBguKEnZO70qwEcLpfifMLoxiPqHQ==}
'@esbuild/android-arm@0.25.10':
resolution: {integrity: sha512-dQAxF1dW1C3zpeCDc5KqIYuZ1tgAdRXNoZP7vkBIRtKZPYe2xVr/d3SkirklCHudW1B45tGiUlz2pUWDfbDD4w==}
engines: {node: '>=18'}
cpu: [arm]
os: [android]
'@esbuild/android-x64@0.25.9':
resolution: {integrity: sha512-I853iMZ1hWZdNllhVZKm34f4wErd4lMyeV7BLzEExGEIZYsOzqDWDf+y082izYUE8gtJnYHdeDpN/6tUdwvfiw==}
'@esbuild/android-x64@0.25.10':
resolution: {integrity: sha512-MiC9CWdPrfhibcXwr39p9ha1x0lZJ9KaVfvzA0Wxwz9ETX4v5CHfF09bx935nHlhi+MxhA63dKRRQLiVgSUtEg==}
engines: {node: '>=18'}
cpu: [x64]
os: [android]
'@esbuild/darwin-arm64@0.25.9':
resolution: {integrity: sha512-XIpIDMAjOELi/9PB30vEbVMs3GV1v2zkkPnuyRRURbhqjyzIINwj+nbQATh4H9GxUgH1kFsEyQMxwiLFKUS6Rg==}
'@esbuild/darwin-arm64@0.25.10':
resolution: {integrity: sha512-JC74bdXcQEpW9KkV326WpZZjLguSZ3DfS8wrrvPMHgQOIEIG/sPXEN/V8IssoJhbefLRcRqw6RQH2NnpdprtMA==}
engines: {node: '>=18'}
cpu: [arm64]
os: [darwin]
'@esbuild/darwin-x64@0.25.9':
resolution: {integrity: sha512-jhHfBzjYTA1IQu8VyrjCX4ApJDnH+ez+IYVEoJHeqJm9VhG9Dh2BYaJritkYK3vMaXrf7Ogr/0MQ8/MeIefsPQ==}
'@esbuild/darwin-x64@0.25.10':
resolution: {integrity: sha512-tguWg1olF6DGqzws97pKZ8G2L7Ig1vjDmGTwcTuYHbuU6TTjJe5FXbgs5C1BBzHbJ2bo1m3WkQDbWO2PvamRcg==}
engines: {node: '>=18'}
cpu: [x64]
os: [darwin]
'@esbuild/freebsd-arm64@0.25.9':
resolution: {integrity: sha512-z93DmbnY6fX9+KdD4Ue/H6sYs+bhFQJNCPZsi4XWJoYblUqT06MQUdBCpcSfuiN72AbqeBFu5LVQTjfXDE2A6Q==}
'@esbuild/freebsd-arm64@0.25.10':
resolution: {integrity: sha512-3ZioSQSg1HT2N05YxeJWYR+Libe3bREVSdWhEEgExWaDtyFbbXWb49QgPvFH8u03vUPX10JhJPcz7s9t9+boWg==}
engines: {node: '>=18'}
cpu: [arm64]
os: [freebsd]
'@esbuild/freebsd-x64@0.25.9':
resolution: {integrity: sha512-mrKX6H/vOyo5v71YfXWJxLVxgy1kyt1MQaD8wZJgJfG4gq4DpQGpgTB74e5yBeQdyMTbgxp0YtNj7NuHN0PoZg==}
'@esbuild/freebsd-x64@0.25.10':
resolution: {integrity: sha512-LLgJfHJk014Aa4anGDbh8bmI5Lk+QidDmGzuC2D+vP7mv/GeSN+H39zOf7pN5N8p059FcOfs2bVlrRr4SK9WxA==}
engines: {node: '>=18'}
cpu: [x64]
os: [freebsd]
'@esbuild/linux-arm64@0.25.9':
resolution: {integrity: sha512-BlB7bIcLT3G26urh5Dmse7fiLmLXnRlopw4s8DalgZ8ef79Jj4aUcYbk90g8iCa2467HX8SAIidbL7gsqXHdRw==}
'@esbuild/linux-arm64@0.25.10':
resolution: {integrity: sha512-5luJWN6YKBsawd5f9i4+c+geYiVEw20FVW5x0v1kEMWNq8UctFjDiMATBxLvmmHA4bf7F6hTRaJgtghFr9iziQ==}
engines: {node: '>=18'}
cpu: [arm64]
os: [linux]
'@esbuild/linux-arm@0.25.9':
resolution: {integrity: sha512-HBU2Xv78SMgaydBmdor38lg8YDnFKSARg1Q6AT0/y2ezUAKiZvc211RDFHlEZRFNRVhcMamiToo7bDx3VEOYQw==}
'@esbuild/linux-arm@0.25.10':
resolution: {integrity: sha512-oR31GtBTFYCqEBALI9r6WxoU/ZofZl962pouZRTEYECvNF/dtXKku8YXcJkhgK/beU+zedXfIzHijSRapJY3vg==}
engines: {node: '>=18'}
cpu: [arm]
os: [linux]
'@esbuild/linux-ia32@0.25.9':
resolution: {integrity: sha512-e7S3MOJPZGp2QW6AK6+Ly81rC7oOSerQ+P8L0ta4FhVi+/j/v2yZzx5CqqDaWjtPFfYz21Vi1S0auHrap3Ma3A==}
'@esbuild/linux-ia32@0.25.10':
resolution: {integrity: sha512-NrSCx2Kim3EnnWgS4Txn0QGt0Xipoumb6z6sUtl5bOEZIVKhzfyp/Lyw4C1DIYvzeW/5mWYPBFJU3a/8Yr75DQ==}
engines: {node: '>=18'}
cpu: [ia32]
os: [linux]
'@esbuild/linux-loong64@0.25.9':
resolution: {integrity: sha512-Sbe10Bnn0oUAB2AalYztvGcK+o6YFFA/9829PhOCUS9vkJElXGdphz0A3DbMdP8gmKkqPmPcMJmJOrI3VYB1JQ==}
'@esbuild/linux-loong64@0.25.10':
resolution: {integrity: sha512-xoSphrd4AZda8+rUDDfD9J6FUMjrkTz8itpTITM4/xgerAZZcFW7Dv+sun7333IfKxGG8gAq+3NbfEMJfiY+Eg==}
engines: {node: '>=18'}
cpu: [loong64]
os: [linux]
'@esbuild/linux-mips64el@0.25.9':
resolution: {integrity: sha512-YcM5br0mVyZw2jcQeLIkhWtKPeVfAerES5PvOzaDxVtIyZ2NUBZKNLjC5z3/fUlDgT6w89VsxP2qzNipOaaDyA==}
'@esbuild/linux-mips64el@0.25.10':
resolution: {integrity: sha512-ab6eiuCwoMmYDyTnyptoKkVS3k8fy/1Uvq7Dj5czXI6DF2GqD2ToInBI0SHOp5/X1BdZ26RKc5+qjQNGRBelRA==}
engines: {node: '>=18'}
cpu: [mips64el]
os: [linux]
'@esbuild/linux-ppc64@0.25.9':
resolution: {integrity: sha512-++0HQvasdo20JytyDpFvQtNrEsAgNG2CY1CLMwGXfFTKGBGQT3bOeLSYE2l1fYdvML5KUuwn9Z8L1EWe2tzs1w==}
'@esbuild/linux-ppc64@0.25.10':
resolution: {integrity: sha512-NLinzzOgZQsGpsTkEbdJTCanwA5/wozN9dSgEl12haXJBzMTpssebuXR42bthOF3z7zXFWH1AmvWunUCkBE4EA==}
engines: {node: '>=18'}
cpu: [ppc64]
os: [linux]
'@esbuild/linux-riscv64@0.25.9':
resolution: {integrity: sha512-uNIBa279Y3fkjV+2cUjx36xkx7eSjb8IvnL01eXUKXez/CBHNRw5ekCGMPM0BcmqBxBcdgUWuUXmVWwm4CH9kg==}
'@esbuild/linux-riscv64@0.25.10':
resolution: {integrity: sha512-FE557XdZDrtX8NMIeA8LBJX3dC2M8VGXwfrQWU7LB5SLOajfJIxmSdyL/gU1m64Zs9CBKvm4UAuBp5aJ8OgnrA==}
engines: {node: '>=18'}
cpu: [riscv64]
os: [linux]
'@esbuild/linux-s390x@0.25.9':
resolution: {integrity: sha512-Mfiphvp3MjC/lctb+7D287Xw1DGzqJPb/J2aHHcHxflUo+8tmN/6d4k6I2yFR7BVo5/g7x2Monq4+Yew0EHRIA==}
'@esbuild/linux-s390x@0.25.10':
resolution: {integrity: sha512-3BBSbgzuB9ajLoVZk0mGu+EHlBwkusRmeNYdqmznmMc9zGASFjSsxgkNsqmXugpPk00gJ0JNKh/97nxmjctdew==}
engines: {node: '>=18'}
cpu: [s390x]
os: [linux]
'@esbuild/linux-x64@0.25.9':
resolution: {integrity: sha512-iSwByxzRe48YVkmpbgoxVzn76BXjlYFXC7NvLYq+b+kDjyyk30J0JY47DIn8z1MO3K0oSl9fZoRmZPQI4Hklzg==}
'@esbuild/linux-x64@0.25.10':
resolution: {integrity: sha512-QSX81KhFoZGwenVyPoberggdW1nrQZSvfVDAIUXr3WqLRZGZqWk/P4T8p2SP+de2Sr5HPcvjhcJzEiulKgnxtA==}
engines: {node: '>=18'}
cpu: [x64]
os: [linux]
'@esbuild/netbsd-arm64@0.25.9':
resolution: {integrity: sha512-9jNJl6FqaUG+COdQMjSCGW4QiMHH88xWbvZ+kRVblZsWrkXlABuGdFJ1E9L7HK+T0Yqd4akKNa/lO0+jDxQD4Q==}
'@esbuild/netbsd-arm64@0.25.10':
resolution: {integrity: sha512-AKQM3gfYfSW8XRk8DdMCzaLUFB15dTrZfnX8WXQoOUpUBQ+NaAFCP1kPS/ykbbGYz7rxn0WS48/81l9hFl3u4A==}
engines: {node: '>=18'}
cpu: [arm64]
os: [netbsd]
'@esbuild/netbsd-x64@0.25.9':
resolution: {integrity: sha512-RLLdkflmqRG8KanPGOU7Rpg829ZHu8nFy5Pqdi9U01VYtG9Y0zOG6Vr2z4/S+/3zIyOxiK6cCeYNWOFR9QP87g==}
'@esbuild/netbsd-x64@0.25.10':
resolution: {integrity: sha512-7RTytDPGU6fek/hWuN9qQpeGPBZFfB4zZgcz2VK2Z5VpdUxEI8JKYsg3JfO0n/Z1E/6l05n0unDCNc4HnhQGig==}
engines: {node: '>=18'}
cpu: [x64]
os: [netbsd]
'@esbuild/openbsd-arm64@0.25.9':
resolution: {integrity: sha512-YaFBlPGeDasft5IIM+CQAhJAqS3St3nJzDEgsgFixcfZeyGPCd6eJBWzke5piZuZ7CtL656eOSYKk4Ls2C0FRQ==}
'@esbuild/openbsd-arm64@0.25.10':
resolution: {integrity: sha512-5Se0VM9Wtq797YFn+dLimf2Zx6McttsH2olUBsDml+lm0GOCRVebRWUvDtkY4BWYv/3NgzS8b/UM3jQNh5hYyw==}
engines: {node: '>=18'}
cpu: [arm64]
os: [openbsd]
'@esbuild/openbsd-x64@0.25.9':
resolution: {integrity: sha512-1MkgTCuvMGWuqVtAvkpkXFmtL8XhWy+j4jaSO2wxfJtilVCi0ZE37b8uOdMItIHz4I6z1bWWtEX4CJwcKYLcuA==}
'@esbuild/openbsd-x64@0.25.10':
resolution: {integrity: sha512-XkA4frq1TLj4bEMB+2HnI0+4RnjbuGZfet2gs/LNs5Hc7D89ZQBHQ0gL2ND6Lzu1+QVkjp3x1gIcPKzRNP8bXw==}
engines: {node: '>=18'}
cpu: [x64]
os: [openbsd]
'@esbuild/openharmony-arm64@0.25.9':
resolution: {integrity: sha512-4Xd0xNiMVXKh6Fa7HEJQbrpP3m3DDn43jKxMjxLLRjWnRsfxjORYJlXPO4JNcXtOyfajXorRKY9NkOpTHptErg==}
'@esbuild/openharmony-arm64@0.25.10':
resolution: {integrity: sha512-AVTSBhTX8Y/Fz6OmIVBip9tJzZEUcY8WLh7I59+upa5/GPhh2/aM6bvOMQySspnCCHvFi79kMtdJS1w0DXAeag==}
engines: {node: '>=18'}
cpu: [arm64]
os: [openharmony]
'@esbuild/sunos-x64@0.25.9':
resolution: {integrity: sha512-WjH4s6hzo00nNezhp3wFIAfmGZ8U7KtrJNlFMRKxiI9mxEK1scOMAaa9i4crUtu+tBr+0IN6JCuAcSBJZfnphw==}
'@esbuild/sunos-x64@0.25.10':
resolution: {integrity: sha512-fswk3XT0Uf2pGJmOpDB7yknqhVkJQkAQOcW/ccVOtfx05LkbWOaRAtn5SaqXypeKQra1QaEa841PgrSL9ubSPQ==}
engines: {node: '>=18'}
cpu: [x64]
os: [sunos]
'@esbuild/win32-arm64@0.25.9':
resolution: {integrity: sha512-mGFrVJHmZiRqmP8xFOc6b84/7xa5y5YvR1x8djzXpJBSv/UsNK6aqec+6JDjConTgvvQefdGhFDAs2DLAds6gQ==}
'@esbuild/win32-arm64@0.25.10':
resolution: {integrity: sha512-ah+9b59KDTSfpaCg6VdJoOQvKjI33nTaQr4UluQwW7aEwZQsbMCfTmfEO4VyewOxx4RaDT/xCy9ra2GPWmO7Kw==}
engines: {node: '>=18'}
cpu: [arm64]
os: [win32]
'@esbuild/win32-ia32@0.25.9':
resolution: {integrity: sha512-b33gLVU2k11nVx1OhX3C8QQP6UHQK4ZtN56oFWvVXvz2VkDoe6fbG8TOgHFxEvqeqohmRnIHe5A1+HADk4OQww==}
'@esbuild/win32-ia32@0.25.10':
resolution: {integrity: sha512-QHPDbKkrGO8/cz9LKVnJU22HOi4pxZnZhhA2HYHez5Pz4JeffhDjf85E57Oyco163GnzNCVkZK0b/n4Y0UHcSw==}
engines: {node: '>=18'}
cpu: [ia32]
os: [win32]
'@esbuild/win32-x64@0.25.9':
resolution: {integrity: sha512-PPOl1mi6lpLNQxnGoyAfschAodRFYXJ+9fs6WHXz7CSWKbOqiMZsubC+BQsVKuul+3vKLuwTHsS2c2y9EoKwxQ==}
'@esbuild/win32-x64@0.25.10':
resolution: {integrity: sha512-9KpxSVFCu0iK1owoez6aC/s/EdUQLDN3adTxGCqxMVhrPDj6bt5dbrHDXUuq+Bs2vATFBBrQS5vdQ/Ed2P+nbw==}
engines: {node: '>=18'}
cpu: [x64]
os: [win32]
'@ioredis/commands@1.3.0':
resolution: {integrity: sha512-M/T6Zewn7sDaBQEqIZ8Rb+i9y8qfGmq+5SDFSf9sA2lUZTmdDLVdOiQaeDp+Q4wElZ9HG1GAX5KhDaidp6LQsQ==}
'@ioredis/commands@1.4.0':
resolution: {integrity: sha512-aFT2yemJJo+TZCmieA7qnYGQooOS7QfNmYrzGtsYd3g9j5iDP8AimYYAesf79ohjbLG12XxC4nG5DyEnC88AsQ==}
'@isaacs/balanced-match@4.0.1':
resolution: {integrity: sha512-yzMTt9lEb8Gv7zRioUilSglI0c0smZ9k5D65677DLWLtWJaXIS3CqcGyUFByYKlnUj6TkjLVs54fBl6+TiGQDQ==}
@@ -272,8 +272,8 @@ packages:
'@types/mime@1.3.5':
resolution: {integrity: sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==}
'@types/node@22.17.2':
resolution: {integrity: sha512-gL6z5N9Jm9mhY+U2KXZpteb+09zyffliRkZyZOHODGATyC5B1Jt/7TzuuiLkFsSUMLbS1OLmlj/E+/3KF4Q/4w==}
'@types/node@22.18.6':
resolution: {integrity: sha512-r8uszLPpeIWbNKtvWRt/DbVi5zbqZyj1PTmhRMqBMvDnaz1QpmSKujUtJLrqGZeoM8v72MfYggDceY4K1itzWQ==}
'@types/qs@6.14.0':
resolution: {integrity: sha512-eOunJqu0K1923aExK6y8p6fsihYEn/BYuQ4g0CxAAgFc4b/ZLN4CrsRZ55srTdqoiLzU2B2evC+apEIxprEzkQ==}
@@ -304,16 +304,16 @@ packages:
resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==}
engines: {node: '>=8'}
ansi-regex@6.2.0:
resolution: {integrity: sha512-TKY5pyBkHyADOPYlRT9Lx6F544mPl0vS5Ew7BJ45hA08Q+t3GjbueLliBWN3sMICk6+y7HdyxSzC4bWS8baBdg==}
ansi-regex@6.2.2:
resolution: {integrity: sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==}
engines: {node: '>=12'}
ansi-styles@4.3.0:
resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==}
engines: {node: '>=8'}
ansi-styles@6.2.1:
resolution: {integrity: sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==}
ansi-styles@6.2.3:
resolution: {integrity: sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==}
engines: {node: '>=12'}
arg@4.1.3:
@@ -405,8 +405,8 @@ packages:
supports-color:
optional: true
debug@4.4.1:
resolution: {integrity: sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==}
debug@4.4.3:
resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==}
engines: {node: '>=6.0'}
peerDependencies:
supports-color: '*'
@@ -478,8 +478,8 @@ packages:
resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==}
engines: {node: '>= 0.4'}
esbuild@0.25.9:
resolution: {integrity: sha512-CRbODhYyQx3qp7ZEwzxOk4JBqmD/seJrzPa/cGjY1VtIn5E09Oi9/dB4JwctnfZ8Q8iT7rioVv5k/FNT/uf54g==}
esbuild@0.25.10:
resolution: {integrity: sha512-9RiGKvCwaqxO2owP61uQ4BgNborAQskMR6QusfWzQqv7AZOg5oGehdY2pRJMTKuwxd1IDBP4rSbI5lHzU7SMsQ==}
engines: {node: '>=18'}
hasBin: true
@@ -557,8 +557,8 @@ packages:
inherits@2.0.4:
resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==}
ioredis@5.7.0:
resolution: {integrity: sha512-NUcA93i1lukyXU+riqEyPtSEkyFq8tX90uL659J+qpCZ3rEdViB/APC58oAhIh3+bJln2hzdlZbBZsGNrlsR8g==}
ioredis@5.8.0:
resolution: {integrity: sha512-AUXbKn9gvo9hHKvk6LbZJQSKn/qIfkWXrnsyL9Yrf+oeXmla9Nmf6XEumOddyhM8neynpK5oAV6r9r99KBuwzA==}
engines: {node: '>=12.22.0'}
ipaddr.js@1.9.1:
@@ -582,8 +582,8 @@ packages:
lodash.isarguments@3.1.0:
resolution: {integrity: sha512-chi4NHZlZqZD18a0imDHnZPrDeBbTtVN7GXMwuGdRH9qotxAjYs3aVLKc7zNOG9eddR5Ksd8rvFEBc9SsggPpg==}
lru-cache@11.1.0:
resolution: {integrity: sha512-QIXZUBJUx+2zHUdQujWejBkcD9+cs94tLn0+YL8UrCh+D5sCXZ4c7LaEH48pNwRY3MLDgqUFyhlCyjJPf1WP0A==}
lru-cache@11.2.1:
resolution: {integrity: sha512-r8LA6i4LP4EeWOhqBaZZjDWwehd1xUJPCJd9Sv300H0ZmcUER4+JPh7bqqZeqs1o5pgtgvXm+d9UGrB5zZGDiQ==}
engines: {node: 20 || >=22}
make-error@1.3.6:
@@ -772,8 +772,8 @@ packages:
resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==}
engines: {node: '>=8'}
strip-ansi@7.1.0:
resolution: {integrity: sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==}
strip-ansi@7.1.2:
resolution: {integrity: sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==}
engines: {node: '>=12'}
toidentifier@1.0.1:
@@ -794,8 +794,8 @@ packages:
'@swc/wasm':
optional: true
tsx@4.20.4:
resolution: {integrity: sha512-yyxBKfORQ7LuRt/BQKBXrpcq59ZvSW0XxwfjAt3w2/8PmdxaFzijtMhTawprSHhpzeM5BgU2hXHG3lklIERZXg==}
tsx@4.20.5:
resolution: {integrity: sha512-+wKjMNU9w/EaQayHXb7WA7ZaHY6hN8WgfvHNQ3t1PnU91/7O8TcTnIhCDYTZwnt8JsO9IBqZ30Ln1r7pPF52Aw==}
engines: {node: '>=18.0.0'}
hasBin: true
@@ -861,85 +861,85 @@ snapshots:
dependencies:
'@jridgewell/trace-mapping': 0.3.9
'@esbuild/aix-ppc64@0.25.9':
'@esbuild/aix-ppc64@0.25.10':
optional: true
'@esbuild/android-arm64@0.25.9':
'@esbuild/android-arm64@0.25.10':
optional: true
'@esbuild/android-arm@0.25.9':
'@esbuild/android-arm@0.25.10':
optional: true
'@esbuild/android-x64@0.25.9':
'@esbuild/android-x64@0.25.10':
optional: true
'@esbuild/darwin-arm64@0.25.9':
'@esbuild/darwin-arm64@0.25.10':
optional: true
'@esbuild/darwin-x64@0.25.9':
'@esbuild/darwin-x64@0.25.10':
optional: true
'@esbuild/freebsd-arm64@0.25.9':
'@esbuild/freebsd-arm64@0.25.10':
optional: true
'@esbuild/freebsd-x64@0.25.9':
'@esbuild/freebsd-x64@0.25.10':
optional: true
'@esbuild/linux-arm64@0.25.9':
'@esbuild/linux-arm64@0.25.10':
optional: true
'@esbuild/linux-arm@0.25.9':
'@esbuild/linux-arm@0.25.10':
optional: true
'@esbuild/linux-ia32@0.25.9':
'@esbuild/linux-ia32@0.25.10':
optional: true
'@esbuild/linux-loong64@0.25.9':
'@esbuild/linux-loong64@0.25.10':
optional: true
'@esbuild/linux-mips64el@0.25.9':
'@esbuild/linux-mips64el@0.25.10':
optional: true
'@esbuild/linux-ppc64@0.25.9':
'@esbuild/linux-ppc64@0.25.10':
optional: true
'@esbuild/linux-riscv64@0.25.9':
'@esbuild/linux-riscv64@0.25.10':
optional: true
'@esbuild/linux-s390x@0.25.9':
'@esbuild/linux-s390x@0.25.10':
optional: true
'@esbuild/linux-x64@0.25.9':
'@esbuild/linux-x64@0.25.10':
optional: true
'@esbuild/netbsd-arm64@0.25.9':
'@esbuild/netbsd-arm64@0.25.10':
optional: true
'@esbuild/netbsd-x64@0.25.9':
'@esbuild/netbsd-x64@0.25.10':
optional: true
'@esbuild/openbsd-arm64@0.25.9':
'@esbuild/openbsd-arm64@0.25.10':
optional: true
'@esbuild/openbsd-x64@0.25.9':
'@esbuild/openbsd-x64@0.25.10':
optional: true
'@esbuild/openharmony-arm64@0.25.9':
'@esbuild/openharmony-arm64@0.25.10':
optional: true
'@esbuild/sunos-x64@0.25.9':
'@esbuild/sunos-x64@0.25.10':
optional: true
'@esbuild/win32-arm64@0.25.9':
'@esbuild/win32-arm64@0.25.10':
optional: true
'@esbuild/win32-ia32@0.25.9':
'@esbuild/win32-ia32@0.25.10':
optional: true
'@esbuild/win32-x64@0.25.9':
'@esbuild/win32-x64@0.25.10':
optional: true
'@ioredis/commands@1.3.0': {}
'@ioredis/commands@1.4.0': {}
'@isaacs/balanced-match@4.0.1': {}
@@ -951,7 +951,7 @@ snapshots:
dependencies:
string-width: 5.1.2
string-width-cjs: string-width@4.2.3
strip-ansi: 7.1.0
strip-ansi: 7.1.2
strip-ansi-cjs: strip-ansi@6.0.1
wrap-ansi: 8.1.0
wrap-ansi-cjs: wrap-ansi@7.0.0
@@ -978,19 +978,19 @@ snapshots:
'@types/body-parser@1.19.6':
dependencies:
'@types/connect': 3.4.38
'@types/node': 22.17.2
'@types/node': 22.18.6
'@types/connect@3.4.38':
dependencies:
'@types/node': 22.17.2
'@types/node': 22.18.6
'@types/cors@2.8.19':
dependencies:
'@types/node': 22.17.2
'@types/node': 22.18.6
'@types/express-serve-static-core@5.0.7':
dependencies:
'@types/node': 22.17.2
'@types/node': 22.18.6
'@types/qs': 6.14.0
'@types/range-parser': 1.2.7
'@types/send': 0.17.5
@@ -1005,7 +1005,7 @@ snapshots:
'@types/mime@1.3.5': {}
'@types/node@22.17.2':
'@types/node@22.18.6':
dependencies:
undici-types: 6.21.0
@@ -1016,12 +1016,12 @@ snapshots:
'@types/send@0.17.5':
dependencies:
'@types/mime': 1.3.5
'@types/node': 22.17.2
'@types/node': 22.18.6
'@types/serve-static@1.15.8':
dependencies:
'@types/http-errors': 2.0.5
'@types/node': 22.17.2
'@types/node': 22.18.6
'@types/send': 0.17.5
accepts@1.3.8:
@@ -1037,13 +1037,13 @@ snapshots:
ansi-regex@5.0.1: {}
ansi-regex@6.2.0: {}
ansi-regex@6.2.2: {}
ansi-styles@4.3.0:
dependencies:
color-convert: 2.0.1
ansi-styles@6.2.1: {}
ansi-styles@6.2.3: {}
arg@4.1.3: {}
@@ -1125,7 +1125,7 @@ snapshots:
dependencies:
ms: 2.1.3
debug@4.4.1:
debug@4.4.3:
dependencies:
ms: 2.1.3
@@ -1162,7 +1162,7 @@ snapshots:
engine.io@6.6.4:
dependencies:
'@types/cors': 2.8.19
'@types/node': 22.17.2
'@types/node': 22.18.6
accepts: 1.3.8
base64id: 2.0.0
cookie: 0.7.2
@@ -1183,34 +1183,34 @@ snapshots:
dependencies:
es-errors: 1.3.0
esbuild@0.25.9:
esbuild@0.25.10:
optionalDependencies:
'@esbuild/aix-ppc64': 0.25.9
'@esbuild/android-arm': 0.25.9
'@esbuild/android-arm64': 0.25.9
'@esbuild/android-x64': 0.25.9
'@esbuild/darwin-arm64': 0.25.9
'@esbuild/darwin-x64': 0.25.9
'@esbuild/freebsd-arm64': 0.25.9
'@esbuild/freebsd-x64': 0.25.9
'@esbuild/linux-arm': 0.25.9
'@esbuild/linux-arm64': 0.25.9
'@esbuild/linux-ia32': 0.25.9
'@esbuild/linux-loong64': 0.25.9
'@esbuild/linux-mips64el': 0.25.9
'@esbuild/linux-ppc64': 0.25.9
'@esbuild/linux-riscv64': 0.25.9
'@esbuild/linux-s390x': 0.25.9
'@esbuild/linux-x64': 0.25.9
'@esbuild/netbsd-arm64': 0.25.9
'@esbuild/netbsd-x64': 0.25.9
'@esbuild/openbsd-arm64': 0.25.9
'@esbuild/openbsd-x64': 0.25.9
'@esbuild/openharmony-arm64': 0.25.9
'@esbuild/sunos-x64': 0.25.9
'@esbuild/win32-arm64': 0.25.9
'@esbuild/win32-ia32': 0.25.9
'@esbuild/win32-x64': 0.25.9
'@esbuild/aix-ppc64': 0.25.10
'@esbuild/android-arm': 0.25.10
'@esbuild/android-arm64': 0.25.10
'@esbuild/android-x64': 0.25.10
'@esbuild/darwin-arm64': 0.25.10
'@esbuild/darwin-x64': 0.25.10
'@esbuild/freebsd-arm64': 0.25.10
'@esbuild/freebsd-x64': 0.25.10
'@esbuild/linux-arm': 0.25.10
'@esbuild/linux-arm64': 0.25.10
'@esbuild/linux-ia32': 0.25.10
'@esbuild/linux-loong64': 0.25.10
'@esbuild/linux-mips64el': 0.25.10
'@esbuild/linux-ppc64': 0.25.10
'@esbuild/linux-riscv64': 0.25.10
'@esbuild/linux-s390x': 0.25.10
'@esbuild/linux-x64': 0.25.10
'@esbuild/netbsd-arm64': 0.25.10
'@esbuild/netbsd-x64': 0.25.10
'@esbuild/openbsd-arm64': 0.25.10
'@esbuild/openbsd-x64': 0.25.10
'@esbuild/openharmony-arm64': 0.25.10
'@esbuild/sunos-x64': 0.25.10
'@esbuild/win32-arm64': 0.25.10
'@esbuild/win32-ia32': 0.25.10
'@esbuild/win32-x64': 0.25.10
escape-html@1.0.3: {}
@@ -1331,11 +1331,11 @@ snapshots:
inherits@2.0.4: {}
ioredis@5.7.0:
ioredis@5.8.0:
dependencies:
'@ioredis/commands': 1.3.0
'@ioredis/commands': 1.4.0
cluster-key-slot: 1.1.2
debug: 4.4.1
debug: 4.4.3
denque: 2.1.0
lodash.defaults: 4.2.0
lodash.isarguments: 3.1.0
@@ -1359,7 +1359,7 @@ snapshots:
lodash.isarguments@3.1.0: {}
lru-cache@11.1.0: {}
lru-cache@11.2.1: {}
make-error@1.3.6: {}
@@ -1407,7 +1407,7 @@ snapshots:
path-scurry@2.0.0:
dependencies:
lru-cache: 11.1.0
lru-cache: 11.2.1
minipass: 7.1.2
path-to-regexp@0.1.12: {}
@@ -1556,26 +1556,26 @@ snapshots:
dependencies:
eastasianwidth: 0.2.0
emoji-regex: 9.2.2
strip-ansi: 7.1.0
strip-ansi: 7.1.2
strip-ansi@6.0.1:
dependencies:
ansi-regex: 5.0.1
strip-ansi@7.1.0:
strip-ansi@7.1.2:
dependencies:
ansi-regex: 6.2.0
ansi-regex: 6.2.2
toidentifier@1.0.1: {}
ts-node@10.9.2(@types/node@22.17.2)(typescript@5.9.2):
ts-node@10.9.2(@types/node@22.18.6)(typescript@5.9.2):
dependencies:
'@cspotcode/source-map-support': 0.8.1
'@tsconfig/node10': 1.0.11
'@tsconfig/node12': 1.0.11
'@tsconfig/node14': 1.0.3
'@tsconfig/node16': 1.0.4
'@types/node': 22.17.2
'@types/node': 22.18.6
acorn: 8.15.0
acorn-walk: 8.3.4
arg: 4.1.3
@@ -1586,9 +1586,9 @@ snapshots:
v8-compile-cache-lib: 3.0.1
yn: 3.1.1
tsx@4.20.4:
tsx@4.20.5:
dependencies:
esbuild: 0.25.9
esbuild: 0.25.10
get-tsconfig: 4.10.1
optionalDependencies:
fsevents: 2.3.3
@@ -1622,9 +1622,9 @@ snapshots:
wrap-ansi@8.1.0:
dependencies:
ansi-styles: 6.2.1
ansi-styles: 6.2.3
string-width: 5.1.2
strip-ansi: 7.1.0
strip-ansi: 7.1.2
ws@8.17.1: {}
+18 -5
View File
@@ -4,16 +4,29 @@ import { CONFIG } from "./env";
// Define the sources allowed in the development environment
const DEV_ORIGINS = [
CONFIG.CORS_ORIGIN, // http://localhost:3002
'http://localhost:3000', // alternate port
/^http:\/\/192\.168\.\d+\.\d+:3000$/, // LAN addresses
/^http:\/\/192\.168\.\d+\.\d+:3002$/ // LAN addresses with new port
"http://localhost:3002", // alternate port
/^http:\/\/192\.168\.\d+\.\d+:3002$/, // LAN addresses
/^http:\/\/192\.168\.\d+\.\d+:3002$/, // LAN addresses with new port
];
// Parse multi-origin config in production (comma-separated)
const parseProdOrigins = (): string | RegExp | (string | RegExp)[] => {
const v = CONFIG.CORS_ORIGIN?.trim();
if (!v) return DEV_ORIGINS; // Fallback to the development whitelist
if (v.includes(",")) {
return v
.split(",")
.map((s) => s.trim())
.filter(Boolean);
}
return v;
};
// Configure CORS
export const corsOptions: CorsOptions =
CONFIG.NODE_ENV === "production"
? {
origin: CONFIG.CORS_ORIGIN,
origin: parseProdOrigins(),
methods: ["GET", "POST", "OPTIONS"],
credentials: true,
allowedHeaders: ["Content-Type", "Authorization"],
@@ -28,7 +41,7 @@ export const corsOptions: CorsOptions =
export const corsWSOptions =
CONFIG.NODE_ENV === "production"
? {
origin: CONFIG.CORS_ORIGIN, // Allowed origin, replace with your Next.js application's URL
origin: parseProdOrigins(),
methods: ["GET", "POST"],
credentials: true,
}
+191
View File
@@ -0,0 +1,191 @@
import { Router, Request, Response } from 'express';
import { redis } from '../services/redis';
import { CONFIG } from '../config/env';
const router = Router();
// 应用启动时间
const startTime = Date.now();
// 基础健康检查
router.get('/health', async (req: Request, res: Response) => {
try {
const health = {
status: 'healthy',
timestamp: new Date().toISOString(),
uptime: Math.floor((Date.now() - startTime) / 1000),
service: 'privydrop-backend',
version: process.env.npm_package_version || '1.0.0',
environment: CONFIG.NODE_ENV
};
res.status(200).json(health);
} catch (error) {
console.error('Health check error:', error);
res.status(503).json({
status: 'unhealthy',
timestamp: new Date().toISOString(),
service: 'privydrop-backend',
error: error instanceof Error ? error.message : 'Unknown error'
});
}
});
// API路径的健康检查 (兼容性)
router.get('/api/health', async (req: Request, res: Response) => {
try {
const health = {
status: 'healthy',
timestamp: new Date().toISOString(),
uptime: Math.floor((Date.now() - startTime) / 1000),
service: 'privydrop-backend',
version: process.env.npm_package_version || '1.0.0',
environment: CONFIG.NODE_ENV
};
res.status(200).json(health);
} catch (error) {
console.error('Health check error:', error);
res.status(503).json({
status: 'unhealthy',
timestamp: new Date().toISOString(),
service: 'privydrop-backend',
error: error instanceof Error ? error.message : 'Unknown error'
});
}
});
// 详细健康检查
router.get('/health/detailed', async (req: Request, res: Response) => {
const errors: string[] = [];
let status = 'healthy';
try {
// 基础信息
const basicHealth = {
status: 'healthy',
timestamp: new Date().toISOString(),
uptime: Math.floor((Date.now() - startTime) / 1000),
service: 'privydrop-backend',
version: process.env.npm_package_version || '1.0.0',
environment: CONFIG.NODE_ENV
};
// 检查Redis连接
const redisHealth = await checkRedisHealth();
if (redisHealth.status !== 'connected') {
errors.push('Redis connection failed');
status = 'unhealthy';
}
// 检查Socket.IO状态
const io = req.app.get('io');
const socketHealth = {
status: io ? 'running' : 'not_initialized',
connections: io ? io.engine.clientsCount : 0
};
// 获取系统资源信息
const systemInfo = getSystemInfo();
// 检查系统资源
if (systemInfo.memory.percent > 90) {
errors.push('High memory usage (>90%)');
status = status === 'healthy' ? 'degraded' : status;
}
if (systemInfo.cpu.percent > 80) {
errors.push('High CPU usage (>80%)');
status = status === 'healthy' ? 'degraded' : status;
}
const detailedHealth = {
...basicHealth,
status,
dependencies: {
redis: redisHealth,
socketio: socketHealth
},
system: systemInfo,
...(errors.length > 0 && { errors })
};
const httpStatus = status === 'healthy' ? 200 : 503;
res.status(httpStatus).json(detailedHealth);
} catch (error) {
console.error('Detailed health check error:', error);
res.status(503).json({
status: 'unhealthy',
timestamp: new Date().toISOString(),
service: 'privydrop-backend',
error: error instanceof Error ? error.message : 'Unknown error'
});
}
});
// Redis健康检查函数
async function checkRedisHealth() {
try {
const start = Date.now();
await redis.ping();
const responseTime = Date.now() - start;
return {
status: 'connected',
responseTime,
host: CONFIG.REDIS.HOST,
port: CONFIG.REDIS.PORT
};
} catch (error) {
return {
status: 'disconnected',
error: error instanceof Error ? error.message : 'Unknown error',
host: CONFIG.REDIS.HOST,
port: CONFIG.REDIS.PORT
};
}
}
// 系统信息获取函数
function getSystemInfo() {
const memUsage = process.memoryUsage();
const totalMem = memUsage.heapTotal;
const usedMem = memUsage.heapUsed;
const freeMem = totalMem - usedMem;
return {
memory: {
used: formatBytes(usedMem),
free: formatBytes(freeMem),
total: formatBytes(totalMem),
percent: Math.round((usedMem / totalMem) * 100)
},
cpu: {
percent: getCpuUsage()
},
uptime: process.uptime(),
platform: process.platform,
arch: process.arch,
nodeVersion: process.version
};
}
// 格式化字节数
function formatBytes(bytes: number): string {
if (bytes === 0) return '0 B';
const k = 1024;
const sizes = ['B', 'KB', 'MB', 'GB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + ' ' + sizes[i];
}
// 简化的CPU使用率 (基于进程CPU时间)
function getCpuUsage(): number {
const cpuUsage = process.cpuUsage();
const totalCpuTime = (cpuUsage.user + cpuUsage.system) / 1000000; // 转换为秒
const uptime = process.uptime();
return Math.round((totalCpuTime / uptime) * 100);
}
export default router;
+5
View File
@@ -5,6 +5,7 @@ import { Server } from "socket.io"; // socket.io: A library for real-time web ap
import { CONFIG } from "./config/env";
import { corsOptions, corsWSOptions } from "./config/server";
import apiRouter from "./routes/api";
import healthRouter from "./routes/health";
import { setupSocketHandlers } from "./socket/handlers";
const app = express(); // Create an Express application
@@ -19,6 +20,10 @@ setupSocketHandlers(io);
// Make io instance available to routes
app.set('io', io);
// Register health check routes first (for Docker health checks)
app.use(healthRouter);
// Register API routes
app.use(apiRouter);
server.listen(CONFIG.BACKEND_PORT, () => {
+762
View File
@@ -0,0 +1,762 @@
#!/bin/bash
set -e # Exit immediately on error
# Color definitions
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m'
# Script directory
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
DOCKER_SCRIPTS_DIR="$SCRIPT_DIR/docker/scripts"
# Logging helpers
log_info() {
echo -e "${BLUE}$1${NC}"
}
log_success() {
echo -e "${GREEN}$1${NC}"
}
log_warning() {
echo -e "${YELLOW}⚠️ $1${NC}"
}
log_error() {
echo -e "${RED}$1${NC}"
}
# Show help
show_help() {
cat << EOF
PrivyDrop Docker Deployment Script
Usage: $0 [options]
Options:
--domain DOMAIN Specify domain (for HTTPS deployments)
--mode MODE Deployment mode: lan-http|lan-tls|public|full
lan-http: Intranet HTTP (fast start; no TLS)
lan-tls: Intranet HTTPS (self-signed; dev/managed env only)
public: Public HTTP + TURN server
full: Domain + HTTPS (Lets Encrypt) + TURN server
--with-nginx Enable Nginx reverse proxy
--with-turn Enable TURN server
--with-sni443 Force enable 443 SNI routing (full; default enabled)
--no-sni443 Disable 443 SNI routing (full; web listens directly on 443)
--enable-web-https In lan-tls mode, enable self-signed HTTPS on 8443 (no HSTS)
--le-email EMAIL Email for Let's Encrypt (recommended in full mode)
--test-renewal Run 'certbot renew --dry-run' and reload services (verification)
--clean Clean existing containers and data
--help Show help
Examples:
$0 --mode lan-http # LAN HTTP quick start
$0 --mode lan-http --with-turn # LAN HTTP with TURN (NAT-friendly)
$0 --mode lan-tls --enable-web-https # LAN HTTPS (self-signed) on 8443 (dev/managed)
$0 --mode public --with-turn # Public deployment + TURN server (no domain)
$0 --mode full --domain example.com \\
--with-nginx --with-turn --le-email you@domain.com # Full HTTPS deployment (LE auto-issue/renew)
$0 --clean # Clean deployment
Requirements:
- Docker Engine and Docker Compose V2 (command `docker compose`)
EOF
}
# Parse command-line arguments
parse_arguments() {
DOMAIN_NAME=""
DEPLOYMENT_MODE=""
WITH_NGINX=false
WITH_TURN=false
CLEAN_MODE=false
LE_EMAIL=""
WITH_SNI443=false
DISABLE_SNI443=false
ENABLE_WEB_HTTPS=false
TEST_RENEWAL=false
while [[ $# -gt 0 ]]; do
case $1 in
--domain)
DOMAIN_NAME="$2"
shift 2
;;
--mode)
DEPLOYMENT_MODE="$2"
shift 2
;;
--with-nginx)
WITH_NGINX=true
shift
;;
--with-turn)
WITH_TURN=true
shift
;;
--with-sni443)
WITH_SNI443=true
shift
;;
--no-sni443)
DISABLE_SNI443=true
shift
;;
--enable-web-https)
ENABLE_WEB_HTTPS=true
shift
;;
--le-email)
LE_EMAIL="$2"
shift 2
;;
--test-renewal)
TEST_RENEWAL=true
shift
;;
--clean)
CLEAN_MODE=true
shift
;;
--help)
show_help
exit 0
;;
*)
log_error "Unknown argument: $1"
show_help
exit 1
;;
esac
done
# Export variables for other scripts
export DOMAIN_NAME
export DEPLOYMENT_MODE
export WITH_NGINX
export WITH_TURN
}
# Check dependencies
check_dependencies() {
log_info "Checking dependencies..."
local missing_deps=()
if ! command -v docker &> /dev/null; then
missing_deps+=("docker")
fi
if ! docker compose version &> /dev/null; then
missing_deps+=("docker compose (V2)")
fi
if ! command -v curl &> /dev/null; then
missing_deps+=("curl")
fi
if ! command -v openssl &> /dev/null; then
missing_deps+=("openssl")
fi
if [[ ${#missing_deps[@]} -gt 0 ]]; then
log_error "Missing dependencies: ${missing_deps[*]}"
echo ""
echo "Please install the missing dependencies:"
for dep in "${missing_deps[@]}"; do
case $dep in
docker)
echo " Docker: https://docs.docker.com/get-docker/"
;;
"docker compose (V2)")
echo " Docker Compose V2 plugin: https://docs.docker.com/compose/install/"
;;
curl)
echo " curl: sudo apt-get install curl (Ubuntu/Debian)"
;;
openssl)
echo " openssl: sudo apt-get install openssl (Ubuntu/Debian)"
;;
esac
done
exit 1
fi
log_success "Dependency checks passed"
}
# Install and prepare Let's Encrypt (certbot)
ensure_certbot() {
if command -v certbot >/dev/null 2>&1; then
return 0
fi
log_info "Installing certbot (requires sudo)..."
if command -v apt-get >/dev/null 2>&1; then
sudo apt-get update -y && sudo apt-get install -y certbot
else
log_error "apt-get not found. Please install certbot manually or run on a supported system"
exit 1
fi
}
# Write certbot deploy hook: copy certs and hot-reload services after renewal
install_certbot_deploy_hook() {
local repo_dir="$SCRIPT_DIR"
local hook_dir="/etc/letsencrypt/renewal-hooks/deploy"
local hook_file="$hook_dir/privydrop-reload.sh"
local compose_file="$repo_dir/docker-compose.yml"
sudo mkdir -p "$hook_dir"
sudo bash -c "cat > '$hook_file'" << EOF
#!/bin/bash
set -e
REPO_DIR="$repo_dir"
COMPOSE_FILE="$compose_file"
# RENEWED_LINEAGE is provided by certbot and points to live/<domain>
if [[ -z "\$RENEWED_LINEAGE" ]]; then
exit 0
fi
cp "\$RENEWED_LINEAGE/fullchain.pem" "\$REPO_DIR/docker/ssl/server-cert.pem"
cp "\$RENEWED_LINEAGE/privkey.pem" "\$REPO_DIR/docker/ssl/server-key.pem"
chmod 600 "\$REPO_DIR/docker/ssl/server-key.pem" || true
# Hot-reload nginx; restart if it fails
docker compose -f "\$COMPOSE_FILE" exec -T nginx nginx -s reload 2>/dev/null || \
docker compose -f "\$COMPOSE_FILE" restart nginx || true
# Prefer sending HUP to coturn; restart if needed (ignore if disabled)
docker compose -f "\$COMPOSE_FILE" exec -T coturn sh -c 'kill -HUP 1' 2>/dev/null || \
docker compose -f "\$COMPOSE_FILE" restart coturn || true
EOF
sudo chmod +x "$hook_file"
# Attempt to enable systemd timer
if command -v systemctl >/dev/null 2>&1; then
sudo systemctl enable --now certbot.timer 2>/dev/null || true
fi
}
# Ensure renewal is scheduled daily: prefer systemd timer; fallback to cron
ensure_renewal_scheduler() {
if command -v systemctl >/dev/null 2>&1; then
sudo systemctl enable --now certbot.timer 2>/dev/null || true
return 0
fi
# Fallback: cron job every 12 hours
if [ -w /etc/cron.d ] || sudo test -d /etc/cron.d; then
sudo bash -c 'cat > /etc/cron.d/certbot' << 'EOF'
# Auto-renew Let's Encrypt certificates for PrivyDrop
SHELL=/bin/bash
PATH=/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin
0 */12 * * * root certbot renew -q
EOF
fi
}
# Dry-run renewal test
test_renewal() {
log_info "Running certbot dry-run renewal test..."
if ! command -v certbot >/dev/null 2>&1; then
log_error "certbot not installed. Run full mode issuance first or install certbot manually"
return 1
fi
sudo certbot renew --dry-run || {
log_error "certbot dry-run failed"
return 1
}
# Attempt hot-reload of nginx and coturn similar to deploy-hook
docker compose exec -T nginx nginx -s reload 2>/dev/null || docker compose restart nginx || true
docker compose exec -T coturn sh -c 'kill -HUP 1' 2>/dev/null || docker compose restart coturn || true
log_success "Dry-run renewal completed; services reloaded"
}
# Issue via webroot and enable 443 config
provision_letsencrypt_cert() {
# Only in full mode with nginx enabled and domain set
if [[ "$DEPLOYMENT_MODE" != "full" || "$WITH_NGINX" != "true" ]]; then
return 0
fi
if [[ -z "$DOMAIN_NAME" ]]; then
log_warning "Full mode without --domain; skipping Let's Encrypt"
return 0
fi
if [[ -z "$LE_EMAIL" ]]; then
log_warning "No --le-email specified; using --register-unsafely-without-email"
fi
ensure_certbot
install_certbot_deploy_hook
ensure_renewal_scheduler
mkdir -p docker/letsencrypt-www docker/ssl
# If certificates already exist (including -0001 lineage), skip issuance
if [[ -f "/etc/letsencrypt/live/$DOMAIN_NAME/fullchain.pem" ]] || ls -1d /etc/letsencrypt/live/${DOMAIN_NAME}* >/dev/null 2>&1; then
log_info "Detected existing certificates/lineage; skipping initial issuance"
else
log_info "Issuing Let's Encrypt certificate via webroot..."
local email_args="--email $LE_EMAIL"
if [[ -z "$LE_EMAIL" ]]; then
email_args="--register-unsafely-without-email"
fi
# Requires port 80 reachable and nginx running
sudo certbot certonly --webroot -w "$(pwd)/docker/letsencrypt-www" \
-d "$DOMAIN_NAME" -d "turn.$DOMAIN_NAME" \
$email_args --agree-tos --non-interactive || {
log_error "Certificate issuance failed; please check certbot output"
return 1
}
fi
# Resolve lineage directory (supports -0001/-0002 suffixes) and copy to docker/ssl
local lineage_dir
lineage_dir=$(readlink -f "/etc/letsencrypt/live/$DOMAIN_NAME" 2>/dev/null || true)
if [[ -z "$lineage_dir" || ! -d "$lineage_dir" ]]; then
lineage_dir=$(ls -1d /etc/letsencrypt/live/${DOMAIN_NAME}* 2>/dev/null | sort | tail -1)
fi
if [[ -z "$lineage_dir" || ! -f "$lineage_dir/fullchain.pem" ]]; then
log_error "No valid certificate lineage directory found. Check /etc/letsencrypt/live/${DOMAIN_NAME}*"
return 1
fi
sudo cp "$lineage_dir/fullchain.pem" docker/ssl/server-cert.pem
sudo cp "$lineage_dir/privkey.pem" docker/ssl/server-key.pem
sudo chmod 600 docker/ssl/server-key.pem || true
# Enable 443 config (certs ready): append only; pass SNI flag (enabled by default in full)
local gen_args=(--mode full --domain "$DOMAIN_NAME" --no-clean --ssl-mode letsencrypt)
[[ "$WITH_SNI443" == "true" ]] && gen_args+=(--enable-sni443)
[[ "$DISABLE_SNI443" == "true" ]] && gen_args+=(--no-sni443)
bash "$DOCKER_SCRIPTS_DIR/generate-config.sh" "${gen_args[@]}" || true
# Hot-reload nginx to enable 443
docker compose exec -T nginx nginx -s reload || docker compose restart nginx
}
# Clean existing deployment
clean_deployment() {
if [[ "$CLEAN_MODE" == "true" ]]; then
log_warning "Cleaning existing deployment..."
# Stop and remove containers
if [[ -f "docker-compose.yml" ]]; then
docker compose down -v --remove-orphans 2>/dev/null || true
fi
# After graceful stop, force-clean named containers as fallback
docker stop -t 10 privydrop-nginx privydrop-coturn 2>/dev/null || true
docker rm -f privydrop-nginx privydrop-coturn 2>/dev/null || true
# Fallback: remove project network (if present)
docker network rm privydrop_privydrop-network 2>/dev/null || true
# Remove images
docker images | grep privydrop | awk '{print $3}' | xargs -r docker rmi -f 2>/dev/null || true
# Clean configuration files
rm -rf docker/nginx/conf.d/*.conf docker/ssl/* logs/* .env 2>/dev/null || true
log_success "Cleanup complete"
if [[ $# -eq 1 ]]; then # If only --clean parameter
exit 0
fi
fi
}
# Ensure TURN service starts when requested (--with-turn)
ensure_turn_running() {
if [[ "$WITH_TURN" != "true" ]]; then
return 0
fi
# If not running, start coturn via profile
if ! docker compose ps | grep -q "privydrop-coturn"; then
log_info "Starting TURN service (profile: turn)..."
docker compose --profile turn up -d coturn || true
fi
}
# Environment detection and configuration generation
setup_environment() {
log_info "Setting up environment..."
# Ensure scripts are executable
chmod +x "$DOCKER_SCRIPTS_DIR"/*.sh 2>/dev/null || true
# Run environment detection
local detect_args=""
[[ -n "$DOMAIN_NAME" ]] && detect_args="--domain $DOMAIN_NAME"
[[ -n "$DEPLOYMENT_MODE" ]] && detect_args="$detect_args --mode $DEPLOYMENT_MODE"
[[ "$WITH_NGINX" == "true" ]] && detect_args="$detect_args --with-nginx"
[[ "$WITH_SNI443" == "true" ]] && detect_args="$detect_args --enable-sni443"
[[ "$DISABLE_SNI443" == "true" ]] && detect_args="$detect_args --no-sni443"
[[ "$ENABLE_WEB_HTTPS" == "true" ]] && detect_args="$detect_args --enable-web-https"
if ! bash "$DOCKER_SCRIPTS_DIR/detect-environment.sh" $detect_args; then
log_error "Environment detection failed"
exit 1
fi
# Generate configuration files
if ! bash "$DOCKER_SCRIPTS_DIR/generate-config.sh" $detect_args; then
log_error "Configuration generation failed"
exit 1
fi
log_success "Environment setup complete"
}
# Build and start services
deploy_services() {
log_info "Building and starting services..."
# Ensure log directories exist and relax permissions so containers (coturn/nginx etc.) can write logs
mkdir -p logs logs/nginx logs/backend logs/frontend logs/coturn 2>/dev/null || true
chmod 777 -R logs 2>/dev/null || true
log_info "Log directories prepared and permissions set: ./logs (mode 777)"
# Stop existing services
if docker compose ps | grep -q "Up"; then
log_info "Stopping existing services..."
docker compose down
fi
# Determine enabled services (Compose V2 requires --profile before the subcommand)
local profiles=""
if [[ "$WITH_NGINX" == "true" ]]; then
profiles="$profiles --profile nginx"
fi
if [[ "$WITH_TURN" == "true" ]]; then
profiles="$profiles --profile turn"
fi
# Build images (parallel first, fall back to serial on failure)
log_info "Building Docker images..."
set +e
docker compose build --parallel
local build_status=$?
set -e
if [[ $build_status -ne 0 ]]; then
log_warning "Parallel build failed; falling back to serial build..."
docker compose build
fi
# Start services (--profile must precede up)
log_info "Starting services..."
# shellcheck disable=SC2086
docker compose $profiles up -d
log_success "Services started"
}
# Wait for services to be ready
wait_for_services() {
log_info "Waiting for services to be ready..."
local max_attempts=60
local attempt=0
local services_ready=false
while [[ $attempt -lt $max_attempts ]]; do
local backend_ready=false
local frontend_ready=false
# Check backend health
if curl -f http://localhost:3001/health &> /dev/null; then
backend_ready=true
fi
# Check frontend health
if curl -f http://localhost:3002/api/health &> /dev/null; then
frontend_ready=true
fi
if [[ "$backend_ready" == "true" ]] && [[ "$frontend_ready" == "true" ]]; then
services_ready=true
break
fi
attempt=$((attempt + 1))
echo -n "."
sleep 2
done
echo ""
if [[ "$services_ready" == "true" ]]; then
log_success "All services are ready"
return 0
else
log_error "Service startup timed out"
log_info "View service status: docker compose ps"
log_info "View service logs: docker compose logs -f"
return 1
fi
}
# Run post-deployment checks
post_deployment_checks() {
log_info "Running post-deployment checks..."
# Check container status
log_info "Checking container status..."
docker compose ps
# In full+nginx, add HTTPS health check (if domain defined)
if [[ -f ".env" ]]; then
local dep_mode="$(grep "DEPLOYMENT_MODE=" .env | cut -d'=' -f2)"
local dname="$(grep "DOMAIN_NAME=" .env | cut -d'=' -f2)"
if [[ "$dep_mode" == "full" && -n "$dname" ]]; then
log_info "Test: HTTPS health check https://$dname/api/health"
if curl -fsS "https://$dname/api/health" >/dev/null; then
log_success "HTTPS health check passed"
else
log_warning "HTTPS health check failed. If the certificate was just issued, wait a bit or run: bash docker/scripts/generate-config.sh --mode full --domain $dname --no-clean && docker compose exec -T nginx nginx -s reload"
fi
fi
fi
# Run health-check tests
if [[ -f "test-health-apis.sh" ]]; then
log_info "Running health-check tests..."
if bash test-health-apis.sh; then
log_success "Health-check tests passed"
else
log_warning "Health-check tests failed, but services may still be working"
fi
fi
log_success "Post-deployment checks complete"
}
# Show deployment results
show_deployment_info() {
echo ""
echo -e "${GREEN}🎉 PrivyDrop deployment complete!${NC}"
echo ""
# Read configuration
local local_ip=""
local public_ip=""
local frontend_port=""
local backend_port=""
local https_port=""
local deployment_mode=""
local network_mode=""
local domain_name=""
local turn_enabled_env=""
if [[ -f ".env" ]]; then
local_ip=$(grep "LOCAL_IP=" .env | cut -d'=' -f2)
public_ip=$(grep "PUBLIC_IP=" .env | cut -d'=' -f2)
frontend_port=$(grep "FRONTEND_PORT=" .env | cut -d'=' -f2)
backend_port=$(grep "BACKEND_PORT=" .env | cut -d'=' -f2)
https_port=$(grep "HTTPS_PORT=" .env | cut -d'=' -f2)
deployment_mode=$(grep "DEPLOYMENT_MODE=" .env | cut -d'=' -f2)
network_mode=$(grep "NETWORK_MODE=" .env | cut -d'=' -f2)
domain_name=$(grep "DOMAIN_NAME=" .env | cut -d'=' -f2)
turn_enabled_env=$(grep "TURN_ENABLED=" .env | cut -d'=' -f2)
fi
echo -e "${BLUE}📋 Access Info:${NC}"
# Determine if public scenario (public/full)
local is_public="false"
if [[ "$deployment_mode" == "public" || "$deployment_mode" == "full" || "$network_mode" == "public" ]]; then
is_public="true"
fi
if [[ "$is_public" == "true" ]]; then
# For public scenarios, prefer domain, then public IP
if [[ -n "$domain_name" ]]; then
if [[ "$WITH_NGINX" == "true" || "$deployment_mode" == "full" ]]; then
echo " Public access: https://$domain_name"
echo " API: https://$domain_name"
else
echo " Public access: http://$domain_name:${frontend_port:-3002}"
echo " API: http://$domain_name:${backend_port:-3001}"
fi
elif [[ -n "$public_ip" ]]; then
if [[ "$WITH_NGINX" == "true" ]]; then
echo " Public access: http://$public_ip"
echo " API: http://$public_ip"
else
echo " Public access: http://$public_ip:${frontend_port:-3002}"
echo " API: http://$public_ip:${backend_port:-3001}"
fi
else
# Fallback: show LAN and localhost if public IP is unavailable
echo " Frontend: http://localhost:${frontend_port:-3002}"
echo " Backend API: http://localhost:${backend_port:-3001}"
fi
else
# Private/basic: localhost + LAN
if [[ "$WITH_NGINX" == "true" ]]; then
# When Nginx is enabled and frontend uses same-origin API, prefer the gateway as the primary entry
echo " Frontend: http://localhost"
echo " API: http://localhost"
else
echo " Frontend: http://localhost:${frontend_port:-3002}"
echo " Backend API: http://localhost:${backend_port:-3001}"
fi
if [[ -n "$local_ip" ]] && [[ "$local_ip" != "127.0.0.1" ]]; then
echo ""
echo -e "${BLUE}🌐 LAN Access:${NC}"
if [[ "$WITH_NGINX" == "true" ]]; then
echo " Frontend: http://$local_ip"
echo " API: http://$local_ip"
else
echo " Frontend: http://$local_ip:${frontend_port:-3002}"
echo " Backend API: http://$local_ip:${backend_port:-3001}"
fi
fi
fi
if [[ "$WITH_NGINX" == "true" ]]; then
echo ""
echo -e "${BLUE}🔀 Nginx Proxy:${NC}"
if [[ -n "$domain_name" ]]; then
echo " HTTP: http://$domain_name"
if [[ -f "docker/ssl/server-cert.pem" ]]; then
echo " HTTPS: https://$domain_name"
fi
elif [[ -n "$public_ip" ]]; then
echo " HTTP: http://$public_ip"
if [[ -f "docker/ssl/server-cert.pem" ]]; then
# In non-domain cases, show HTTPS with explicit port (e.g., lan-tls uses 8443)
if [[ -n "$https_port" && "$https_port" != "443" ]]; then
echo " HTTPS: https://$public_ip:$https_port"
else
echo " HTTPS: https://$public_ip"
fi
fi
else
echo " HTTP: http://localhost"
if [[ -f "docker/ssl/server-cert.pem" ]]; then
# Show correct HTTPS endpoint based on configured port
if [[ -n "$https_port" && "$https_port" != "443" ]]; then
echo " HTTPS: https://localhost:$https_port"
if [[ -n "$local_ip" && "$local_ip" != "127.0.0.1" ]]; then
echo " HTTPS (LAN): https://$local_ip:$https_port"
fi
else
echo " HTTPS: https://localhost"
fi
fi
fi
fi
echo ""
echo -e "${BLUE}🔧 Management Commands:${NC}"
echo " Status: docker compose ps"
echo " Logs: docker compose logs -f [service]"
echo " Restart: docker compose restart [service]"
echo " Stop: docker compose down"
echo " Full cleanup: $0 --clean"
if [[ -f "docker/ssl/ca-cert.pem" ]]; then
echo ""
echo -e "${BLUE}🔒 SSL Certificates:${NC}"
echo " CA certificate: docker/ssl/ca-cert.pem"
echo " To trust HTTPS, import the CA certificate into your browser"
fi
if [[ "$WITH_TURN" == "true" || "$turn_enabled_env" == "true" ]]; then
local turn_username=""
local turn_realm=""
if [[ -f ".env" ]]; then
turn_username=$(grep "TURN_USERNAME=" .env | cut -d'=' -f2)
turn_realm=$(grep "TURN_REALM=" .env | cut -d'=' -f2)
fi
echo ""
echo -e "${BLUE}🔄 TURN Server:${NC}"
# Prefer domain for TURN info; otherwise show public IP
if [[ -n "$domain_name" ]]; then
echo " STUN: stun:${domain_name}:3478"
echo " TURN (UDP): turn:${domain_name}:3478"
echo " TURN (TLS): turns:turn.${domain_name}:443 (if 443 SNI split is configured)"
elif [[ -n "$public_ip" ]]; then
echo " STUN: stun:${public_ip}:3478"
echo " TURN: turn:${public_ip}:3478"
else
echo " STUN: stun:${local_ip}:3478"
echo " TURN: turn:${local_ip}:3478"
fi
echo " Username: ${turn_username:-privydrop}"
echo " Password: (stored in .env)"
fi
echo ""
echo -e "${YELLOW}💡 Tips:${NC}"
echo " - First run may take several minutes to download and build images"
echo " - If issues occur, check logs: docker compose logs -f"
echo " - More help: $0 --help"
echo ""
# Public scenario: for domain + HTTPS setup steps, see docs
if [[ "$is_public" == "true" && -z "$domain_name" ]]; then
echo -e "${BLUE}🌍 Domain + HTTPS guide:${NC} see docs/DEPLOYMENT_docker.md or docs/DEPLOYMENT_docker.zh-CN.md"
fi
}
# Main function
main() {
echo -e "${BLUE}=== PrivyDrop Docker One-Click Deployment ===${NC}"
echo ""
# Parse command-line arguments
parse_arguments "$@"
# Check dependencies
check_dependencies
echo ""
# If only testing renewal, run and exit
if [[ "$TEST_RENEWAL" == "true" ]]; then
test_renewal && exit 0 || exit 1
fi
# Clean mode
clean_deployment
# If only cleaning (no other args), exit early to skip env detection
if [[ "$CLEAN_MODE" == "true" && -z "$DEPLOYMENT_MODE" && "$WITH_NGINX" == "false" && "$WITH_TURN" == "false" && -z "$DOMAIN_NAME" ]]; then
log_success "Cleanup complete (clean-only mode). Exiting."
exit 0
fi
# Environment setup
setup_environment
echo ""
# Deploy services
deploy_services
echo ""
# If full + nginx, automatically issue certs and enable 443
provision_letsencrypt_cert || true
# Ensure TURN is running (when requested with --with-turn)
ensure_turn_running || true
# Wait for services to be ready
if wait_for_services; then
echo ""
post_deployment_checks
show_deployment_info
else
log_error "Deployment failed. Please check logs: docker compose logs"
exit 1
fi
}
# Trap interrupt signals
trap 'log_warning "Deployment interrupted"; exit 1' INT TERM
# Run main function
main "$@"
+144
View File
@@ -0,0 +1,144 @@
services:
# Redis cache service
redis:
image: redis:7-alpine
container_name: privydrop-redis
restart: unless-stopped
command: redis-server --appendonly yes --maxmemory 256mb --maxmemory-policy allkeys-lru
volumes:
- redis_data:/data
networks:
- privydrop-network
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 10s
timeout: 3s
retries: 3
start_period: 5s
# Backend signaling service
backend:
build:
context: ./backend
dockerfile: Dockerfile
args:
- HTTP_PROXY=${HTTP_PROXY}
- HTTPS_PROXY=${HTTPS_PROXY}
- NO_PROXY=${NO_PROXY}
container_name: privydrop-backend
restart: unless-stopped
environment:
- NODE_ENV=production
- BACKEND_PORT=3001
- REDIS_HOST=redis
- REDIS_PORT=6379
- CORS_ORIGIN=${CORS_ORIGIN:-http://localhost}
ports:
- "${BACKEND_PORT:-3001}:3001"
depends_on:
redis:
condition: service_healthy
networks:
- privydrop-network
volumes:
- ./logs:/app/logs
healthcheck:
test: ["CMD", "node", "health-check.js"]
interval: 30s
timeout: 10s
retries: 3
start_period: 40s
# Frontend app
frontend:
build:
context: ./frontend
dockerfile: Dockerfile
args:
- HTTP_PROXY=${HTTP_PROXY}
- HTTPS_PROXY=${HTTPS_PROXY}
- NO_PROXY=${NO_PROXY}
- NEXT_PUBLIC_API_URL=${NEXT_PUBLIC_API_URL}
- NEXT_PUBLIC_TURN_HOST=${NEXT_PUBLIC_TURN_HOST}
- NEXT_PUBLIC_TURN_USERNAME=${NEXT_PUBLIC_TURN_USERNAME}
- NEXT_PUBLIC_TURN_PASSWORD=${NEXT_PUBLIC_TURN_PASSWORD}
- NEXT_IMAGE_UNOPTIMIZED=${NEXT_IMAGE_UNOPTIMIZED:-true}
container_name: privydrop-frontend
restart: unless-stopped
environment:
- NODE_ENV=production
- NEXT_PUBLIC_API_URL=${NEXT_PUBLIC_API_URL:-http://localhost:3001}
- BACKEND_INTERNAL_URL=${BACKEND_INTERNAL_URL:-http://backend:3001}
- NEXT_IMAGE_UNOPTIMIZED=${NEXT_IMAGE_UNOPTIMIZED:-true}
- PORT=3002
- HOSTNAME=0.0.0.0
- NODE_EXTRA_CA_CERTS=/opt/privydrop/ssl/ca-cert.pem
ports:
- "${FRONTEND_PORT:-3002}:3002"
depends_on:
backend:
condition: service_healthy
networks:
- privydrop-network
volumes:
- ./docker/ssl:/opt/privydrop/ssl:ro
healthcheck:
test: ["CMD", "node", "health-check.js"]
interval: 30s
timeout: 10s
retries: 3
start_period: 120s
# Nginx reverse proxy
nginx:
image: nginx:alpine
container_name: privydrop-nginx
restart: unless-stopped
ports:
- "${HTTP_PORT:-80}:80"
- "${HTTPS_PORT:-443}:${DOCKER_HTTPS_CONTAINER_PORT:-443}"
volumes:
- ./docker/nginx/nginx.conf:/etc/nginx/nginx.conf:ro
- ./docker/nginx/conf.d:/etc/nginx/conf.d:ro
- ./docker/letsencrypt-www:/var/www/certbot:ro
- ./docker/ssl:/etc/nginx/ssl:ro
- ./logs/nginx:/var/log/nginx
depends_on:
- frontend
- backend
networks:
- privydrop-network
profiles:
- nginx
# TURN/STUN server (optional, for NAT traversal)
coturn:
image: coturn/coturn:4.6.2
container_name: privydrop-coturn
restart: unless-stopped
ports:
- "3478:3478/tcp"
- "3478:3478/udp"
- "5349:5349/tcp"
- "5349:5349/udp"
- "${TURN_MIN_PORT:-49152}-${TURN_MAX_PORT:-49252}:${TURN_MIN_PORT:-49152}-${TURN_MAX_PORT:-49252}/udp"
volumes:
- ./docker/coturn/turnserver.conf:/etc/coturn/turnserver.conf:ro
- ./docker/ssl:/etc/ssl/certs:ro
- ./logs/coturn:/var/log
networks:
- privydrop-network
profiles:
- turn
command: ["-c", "/etc/coturn/turnserver.conf"]
networks:
privydrop-network:
driver: bridge
ipam:
config:
- subnet: 172.20.0.0/16
volumes:
redis_data:
driver: local
+487
View File
@@ -0,0 +1,487 @@
#!/bin/bash
# Color definitions
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m' # No Color
# Global variables
NETWORK_MODE=""
LOCAL_IP=""
PUBLIC_IP=""
DEPLOYMENT_MODE="basic"
FORCED_MODE=""
LOCAL_IP_OVERRIDE=""
declare -a IP_CANDIDATES=()
declare -A __SEEN_IPS=()
add_ip_candidate() {
local ip="$1"
[[ -z "$ip" ]] && return
[[ "$ip" == "127."* ]] && return
[[ "$ip" == "0.0.0.0" ]] && return
if [[ -z "${__SEEN_IPS[$ip]}" ]]; then
IP_CANDIDATES+=("$ip")
__SEEN_IPS[$ip]=1
fi
}
is_rfc1918_ip() {
local ip="$1"
case "$ip" in
10.*|192.168.*|172.1[6-9].*|172.2[0-9].*|172.3[0-1].*)
return 0
;;
*)
return 1
;;
esac
}
is_cgnat_ip() {
local ip="$1"
case "$ip" in
100.*)
return 0
;;
*)
return 1
;;
esac
}
is_reserved_benchmark_ip() {
local ip="$1"
case "$ip" in
198.18.*|198.19.*)
return 0
;;
*)
return 1
;;
esac
}
is_link_local_ip() {
local ip="$1"
case "$ip" in
169.254.*)
return 0
;;
*)
return 1
;;
esac
}
is_routable_public_ip() {
local ip="$1"
if [[ -z "$ip" ]]; then
return 1
fi
if is_rfc1918_ip "$ip"; then
return 1
fi
if is_cgnat_ip "$ip"; then
return 1
fi
if is_reserved_benchmark_ip "$ip"; then
return 1
fi
case "$ip" in
127.*|169.254.*)
return 1
;;
*)
return 0
;;
esac
}
collect_ip_candidates() {
IP_CANDIDATES=()
unset __SEEN_IPS
declare -A __SEEN_IPS=()
if command -v hostname >/dev/null 2>&1; then
local host_ips
host_ips=$(hostname -I 2>/dev/null || true)
for ip in $host_ips; do
add_ip_candidate "$ip"
done
fi
if command -v ip >/dev/null 2>&1; then
while IFS= read -r ip; do
add_ip_candidate "$ip"
done < <(ip -o -4 addr show scope global 2>/dev/null | awk '{print $4}' | cut -d/ -f1)
fi
if command -v ifconfig >/dev/null 2>&1; then
while IFS= read -r ip; do
add_ip_candidate "$ip"
done < <(ifconfig 2>/dev/null | awk '/inet / {print $2}' | grep -E '^[0-9]+(\.[0-9]+){3}$')
fi
if command -v ip >/dev/null 2>&1; then
local route_ip
route_ip=$(ip route get 1.1.1.1 2>/dev/null | awk '{for (i=1;i<=NF;i++) if ($i=="src") {print $(i+1); exit}}')
add_ip_candidate "$route_ip"
fi
if [[ ${#IP_CANDIDATES[@]} -eq 0 ]]; then
local fallback
fallback=$(hostname -I 2>/dev/null | awk '{print $1}')
add_ip_candidate "$fallback"
fi
}
resolve_local_ip() {
if [[ -n "$LOCAL_IP_OVERRIDE" ]]; then
LOCAL_IP="$LOCAL_IP_OVERRIDE"
return
fi
collect_ip_candidates
if [[ ${#IP_CANDIDATES[@]} -eq 0 ]]; then
LOCAL_IP=""
return
fi
local ip
for ip in "${IP_CANDIDATES[@]}"; do
if is_rfc1918_ip "$ip"; then
LOCAL_IP="$ip"
return
fi
done
for ip in "${IP_CANDIDATES[@]}"; do
if is_cgnat_ip "$ip"; then
LOCAL_IP="$ip"
return
fi
done
for ip in "${IP_CANDIDATES[@]}"; do
if is_reserved_benchmark_ip "$ip"; then
continue
fi
if is_link_local_ip "$ip"; then
continue
fi
LOCAL_IP="$ip"
return
done
LOCAL_IP="${IP_CANDIDATES[0]}"
}
# Logging helpers
log_info() {
echo -e "${BLUE}$1${NC}"
}
log_success() {
echo -e "${GREEN}$1${NC}"
}
log_warning() {
echo -e "${YELLOW}⚠️ $1${NC}"
}
log_error() {
echo -e "${RED}$1${NC}"
}
# Detect network environment
detect_network_environment() {
log_info "Detecting network environment..."
resolve_local_ip
if [[ -z "$LOCAL_IP" ]]; then
LOCAL_IP="127.0.0.1"
log_warning "Unable to detect host IP; using default: $LOCAL_IP"
fi
if [[ "$FORCED_MODE" == "private" ]]; then
NETWORK_MODE="private"
PUBLIC_IP=""
log_info "Network mode set via parameters: $NETWORK_MODE"
echo " Local IP: $LOCAL_IP"
return 0
fi
local mode_guess="private"
local printed_prompt_info="false"
PUBLIC_IP=""
if curl -s --connect-timeout 5 --max-time 10 ifconfig.me > /dev/null 2>&1; then
PUBLIC_IP=$(curl -s --connect-timeout 5 --max-time 10 ifconfig.me 2>/dev/null || echo "")
if [[ -n "$PUBLIC_IP" ]]; then
if is_routable_public_ip "$PUBLIC_IP"; then
mode_guess="public"
else
log_warning "Public IP is test/reserved range; treating as private"
fi
else
log_warning "Public connectivity unstable; treating as private"
fi
fi
if [[ -z "$FORCED_MODE" ]]; then
if [[ "$mode_guess" == "public" ]]; then
NETWORK_MODE="public"
else
NETWORK_MODE="private"
fi
else
NETWORK_MODE="$FORCED_MODE"
if [[ "$FORCED_MODE" == "public" && -z "$PUBLIC_IP" ]]; then
log_warning "Could not detect public IP; continuing as public mode. Please verify network config"
fi
fi
if [[ "$NETWORK_MODE" != "public" ]]; then
PUBLIC_IP=""
fi
if [[ "$FORCED_MODE" == "public" ]]; then
log_info "Network mode set via parameters: $NETWORK_MODE"
elif [[ "$NETWORK_MODE" == "public" ]]; then
log_success "Public network detected"
else
log_success "Private network detected"
fi
if [[ "$printed_prompt_info" == "false" ]]; then
echo " Local IP: $LOCAL_IP"
if [[ "$NETWORK_MODE" == "public" && -n "$PUBLIC_IP" ]]; then
echo " Public IP: $PUBLIC_IP"
fi
fi
}
# Check system resources
check_system_resources() {
log_info "Checking system resources..."
local warnings=0
# Check memory
if command -v free >/dev/null 2>&1; then
TOTAL_MEM=$(free -m | awk 'NR==2{print $2}')
if [[ $TOTAL_MEM -lt 512 ]]; then
log_error "Insufficient memory: ${TOTAL_MEM}MB (512MB+ recommended)"
return 1
elif [[ $TOTAL_MEM -lt 1024 ]]; then
log_warning "Low memory: ${TOTAL_MEM}MB (1GB+ recommended)"
warnings=$((warnings + 1))
else
log_success "Memory OK: ${TOTAL_MEM}MB"
fi
else
log_warning "Unable to read memory usage"
warnings=$((warnings + 1))
fi
# Check disk usage
DISK_USAGE=$(df -h / | awk 'NR==2{print $5}' | sed 's/%//')
if [[ $DISK_USAGE -gt 95 ]]; then
log_error "Insufficient disk space: ${DISK_USAGE}% used"
return 1
elif [[ $DISK_USAGE -gt 80 ]]; then
log_warning "Disk space tight: ${DISK_USAGE}% used"
warnings=$((warnings + 1))
else
log_success "Disk space OK: ${DISK_USAGE}% used"
fi
# Check available disk space
AVAILABLE_SPACE=$(df -BG / | awk 'NR==2{print $4}' | sed 's/G//')
if [[ $AVAILABLE_SPACE -lt 2 ]]; then
log_error "Not enough free disk space: ${AVAILABLE_SPACE}GB (2GB+ recommended)"
return 1
fi
if [[ $warnings -gt 0 ]]; then
log_warning "System resource check passed with $warnings warning(s)"
else
log_success "System resource check passed"
fi
return 0
}
# Validate Docker environment
verify_docker_installation() {
log_info "Checking Docker environment..."
if ! command -v docker &> /dev/null; then
log_error "Docker is not installed"
echo "Please install Docker: https://docs.docker.com/get-docker/"
return 1
fi
if ! command -v docker-compose &> /dev/null && ! docker compose version &> /dev/null; then
log_error "Docker Compose is not installed"
echo "Please install Docker Compose: https://docs.docker.com/compose/install/"
return 1
fi
# Check Docker service status
if ! docker info &> /dev/null; then
log_error "Docker service is not running"
echo "Please start the Docker service"
return 1
fi
# Check Docker version
DOCKER_VERSION=$(docker --version | grep -oE '[0-9]+\.[0-9]+' | head -1)
log_success "Docker version: $DOCKER_VERSION"
# Check Docker Compose version
if command -v docker-compose &> /dev/null; then
COMPOSE_VERSION=$(docker-compose --version | grep -oE '[0-9]+\.[0-9]+' | head -1)
log_success "Docker Compose version: $COMPOSE_VERSION"
else
COMPOSE_VERSION=$(docker compose version --short 2>/dev/null || echo "built-in")
log_success "Docker Compose version: $COMPOSE_VERSION"
fi
return 0
}
# Check port usage
check_port_availability() {
local ports="$1"
log_info "Checking port usage..."
local occupied_ports=()
IFS=',' read -ra PORT_ARRAY <<< "$ports"
for port in "${PORT_ARRAY[@]}"; do
port=$(echo "$port" | xargs) # Trim spaces
if command -v ss >/dev/null 2>&1; then
if ss -tuln | grep -q ":$port "; then
occupied_ports+=("$port")
fi
elif command -v netstat >/dev/null 2>&1; then
if netstat -tuln 2>/dev/null | grep -q ":$port "; then
occupied_ports+=("$port")
fi
else
log_warning "Unable to check port usage (missing ss and netstat)"
return 0
fi
done
if [[ ${#occupied_ports[@]} -gt 0 ]]; then
log_warning "Ports in use: ${occupied_ports[*]}"
log_info "Change ports in .env, or run './deploy.sh --clean' / 'docker-compose down' to clean old containers"
else
log_success "All ports available"
fi
}
# Detect deployment mode
detect_deployment_mode() {
log_info "Determining deployment mode..."
if [[ "$NETWORK_MODE" == "public" ]] && [[ -n "$DOMAIN_NAME" ]]; then
DEPLOYMENT_MODE="full"
log_success "Deployment mode: full (HTTPS + TURN server)"
elif [[ "$NETWORK_MODE" == "public" ]]; then
DEPLOYMENT_MODE="public"
log_success "Deployment mode: public (HTTP + TURN)"
else
DEPLOYMENT_MODE="basic"
log_success "Deployment mode: basic (intranet HTTP)"
fi
}
# Main function
main() {
echo -e "${BLUE}=== PrivyDrop Docker Environment Check ===${NC}\n"
# Parse command-line arguments
while [[ $# -gt 0 ]]; do
case $1 in
--domain)
DOMAIN_NAME="$2"
shift 2
;;
--mode)
DEPLOYMENT_MODE="$2"
case "$2" in
private|basic)
FORCED_MODE="private"
;;
public|full)
FORCED_MODE="public"
;;
*)
FORCED_MODE=""
;;
esac
shift 2
;;
--local-ip)
LOCAL_IP_OVERRIDE="$2"
shift 2
;;
*)
shift
;;
esac
done
# Run checks
detect_network_environment
echo ""
if ! check_system_resources; then
log_error "System resource check failed; resolve resource issues and retry"
exit 1
fi
echo ""
if ! verify_docker_installation; then
log_error "Docker environment check failed; please install and start Docker"
exit 1
fi
echo ""
check_port_availability "80,443,3002,3001,3478,5349,6379"
echo ""
detect_deployment_mode
echo ""
log_success "Environment check complete!"
echo -e "${BLUE}Results:${NC}"
echo " Network mode: $NETWORK_MODE"
echo " Local IP: $LOCAL_IP"
[[ -n "$PUBLIC_IP" ]] && echo " Public IP: $PUBLIC_IP"
echo " Deployment mode: $DEPLOYMENT_MODE"
# Export env vars for other scripts
export NETWORK_MODE
export LOCAL_IP
export PUBLIC_IP
export DEPLOYMENT_MODE
export DOMAIN_NAME
export LOCAL_IP_OVERRIDE
return 0
}
# If the script is executed directly
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
main "$@"
fi
+1086
View File
File diff suppressed because it is too large Load Diff
+420
View File
@@ -0,0 +1,420 @@
#!/bin/bash
# PrivyDrop Docker deployment test script
# Validate deployment integrity and functionality
# Color definitions
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m'
# Test result counters
TESTS_PASSED=0
TESTS_FAILED=0
TOTAL_TESTS=0
# Logging helpers
log_info() {
echo -e "${BLUE}$1${NC}"
}
log_success() {
echo -e "${GREEN}$1${NC}"
TESTS_PASSED=$((TESTS_PASSED + 1))
}
log_error() {
echo -e "${RED}$1${NC}"
TESTS_FAILED=$((TESTS_FAILED + 1))
}
log_warning() {
echo -e "${YELLOW}⚠️ $1${NC}"
}
# Test functions
run_test() {
local test_name="$1"
local test_command="$2"
TOTAL_TESTS=$((TOTAL_TESTS + 1))
log_info "Test: $test_name"
if eval "$test_command" >/dev/null 2>&1; then
log_success "$test_name"
return 0
else
log_error "$test_name"
return 1
fi
}
# Docker environment tests
test_docker_environment() {
echo -e "${BLUE}=== Docker Environment Tests ===${NC}"
run_test "Docker installed" "command -v docker"
run_test "Docker daemon running" "docker info"
run_test "Docker Compose available" "docker-compose --version || docker compose version"
echo ""
}
# Container status tests
test_container_status() {
echo -e "${BLUE}=== Container Status Tests ===${NC}"
# Check if containers exist and are running
local containers=("privydrop-redis" "privydrop-backend" "privydrop-frontend")
for container in "${containers[@]}"; do
run_test "Container $container is running" "docker ps | grep -q $container"
done
# Check container health
for container in "${containers[@]}"; do
if docker ps --format "table {{.Names}}\t{{.Status}}" | grep -q "$container.*healthy"; then
log_success "Container $container health OK"
TESTS_PASSED=$((TESTS_PASSED + 1))
else
log_warning "Container $container health unknown or unhealthy"
fi
TOTAL_TESTS=$((TOTAL_TESTS + 1))
done
echo ""
}
# Network connectivity tests
test_network_connectivity() {
echo -e "${BLUE}=== Network Connectivity Tests ===${NC}"
# Test port connectivity
local ports=("3002:Frontend" "3001:Backend" "6379:Redis")
for port_info in "${ports[@]}"; do
local port=$(echo "$port_info" | cut -d':' -f1)
local service=$(echo "$port_info" | cut -d':' -f2)
run_test "$service port $port reachable" "nc -z localhost $port"
done
# Test inter-container networking
run_test "Backend can connect to Redis" "docker-compose exec -T backend sh -c 'nc -z redis 6379'"
run_test "Frontend can reach backend" "curl -f http://localhost:3001/health"
echo ""
}
# API functionality tests
test_api_functionality() {
echo -e "${BLUE}=== API Functionality Tests ===${NC}"
# Health check APIs
run_test "Backend health check API" "curl -f http://localhost:3001/health"
run_test "Frontend health check API" "curl -f http://localhost:3002/api/health"
# Backend detailed health check
if curl -f http://localhost:3001/health/detailed >/dev/null 2>&1; then
local redis_status=$(curl -s http://localhost:3001/health/detailed | jq -r '.dependencies.redis.status' 2>/dev/null)
if [[ "$redis_status" == "connected" ]]; then
log_success "Redis connection OK"
TESTS_PASSED=$((TESTS_PASSED + 1))
else
log_error "Redis connection issue"
TESTS_FAILED=$((TESTS_FAILED + 1))
fi
else
log_error "Detailed health check API unavailable"
TESTS_FAILED=$((TESTS_FAILED + 1))
fi
TOTAL_TESTS=$((TOTAL_TESTS + 1))
# Application API tests
run_test "Get room API" "curl -f http://localhost:3001/api/get_room"
run_test "Create room API" "curl -f -X POST -H 'Content-Type: application/json' -d '{\"roomId\":\"test123\"}' http://localhost:3001/api/create_room"
echo ""
}
# WebRTC functionality tests
test_webrtc_functionality() {
echo -e "${BLUE}=== WebRTC Functionality Tests ===${NC}"
# Test frontend page load
if curl -f http://localhost:3002 >/dev/null 2>&1; then
log_success "Frontend page reachable"
TESTS_PASSED=$((TESTS_PASSED + 1))
else
log_error "Frontend page not reachable"
TESTS_FAILED=$((TESTS_FAILED + 1))
fi
TOTAL_TESTS=$((TOTAL_TESTS + 1))
# Test Socket.IO connection (basic)
if curl -f http://localhost:3001/socket.io/socket.io.js >/dev/null 2>&1; then
log_success "Socket.IO client script reachable"
TESTS_PASSED=$((TESTS_PASSED + 1))
else
log_error "Socket.IO client script not reachable"
TESTS_FAILED=$((TESTS_FAILED + 1))
fi
TOTAL_TESTS=$((TOTAL_TESTS + 1))
echo ""
}
# Performance tests
test_performance() {
echo -e "${BLUE}=== Performance Tests ===${NC}"
# Memory usage test
local backend_memory=$(docker stats --no-stream --format "table {{.Container}}\t{{.MemUsage}}" | grep privydrop-backend | awk '{print $2}' | cut -d'/' -f1)
local frontend_memory=$(docker stats --no-stream --format "table {{.Container}}\t{{.MemUsage}}" | grep privydrop-frontend | awk '{print $2}' | cut -d'/' -f1)
if [[ -n "$backend_memory" ]]; then
log_info "Backend memory usage: $backend_memory"
fi
if [[ -n "$frontend_memory" ]]; then
log_info "Frontend memory usage: $frontend_memory"
fi
# Response time test
local response_time=$(curl -o /dev/null -s -w '%{time_total}' http://localhost:3001/health)
if (( $(echo "$response_time < 1.0" | bc -l) )); then
log_success "API response time OK: ${response_time}s"
TESTS_PASSED=$((TESTS_PASSED + 1))
else
log_warning "API response time slow: ${response_time}s"
fi
TOTAL_TESTS=$((TOTAL_TESTS + 1))
echo ""
}
# Security tests
test_security() {
echo -e "${BLUE}=== Security Tests ===${NC}"
# Check container users
local backend_user=$(docker-compose exec -T backend whoami 2>/dev/null || echo "unknown")
local frontend_user=$(docker-compose exec -T frontend whoami 2>/dev/null || echo "unknown")
if [[ "$backend_user" != "root" ]]; then
log_success "Backend container uses non-root user: $backend_user"
TESTS_PASSED=$((TESTS_PASSED + 1))
else
log_warning "Backend container runs as root"
fi
TOTAL_TESTS=$((TOTAL_TESTS + 1))
if [[ "$frontend_user" != "root" ]]; then
log_success "Frontend container uses non-root user: $frontend_user"
TESTS_PASSED=$((TESTS_PASSED + 1))
else
log_warning "Frontend container runs as root"
fi
TOTAL_TESTS=$((TOTAL_TESTS + 1))
# Check for sensitive info leakage
if curl -s http://localhost:3001/health/detailed | grep -q "password\|secret\|key" >/dev/null 2>&1; then
log_warning "Health check API may leak sensitive info"
else
log_success "Health check API does not leak sensitive info"
TESTS_PASSED=$((TESTS_PASSED + 1))
fi
TOTAL_TESTS=$((TOTAL_TESTS + 1))
echo ""
}
# Logging tests
test_logging() {
echo -e "${BLUE}=== Logging Tests ===${NC}"
# Check log directories
if [[ -d "logs" ]]; then
log_success "Log directory exists"
TESTS_PASSED=$((TESTS_PASSED + 1))
else
log_warning "Log directory does not exist"
fi
TOTAL_TESTS=$((TOTAL_TESTS + 1))
# Check log files
local log_files=("logs/backend" "logs/frontend")
for log_dir in "${log_files[@]}"; do
if [[ -d "$log_dir" ]]; then
log_success "Log directory $log_dir exists"
TESTS_PASSED=$((TESTS_PASSED + 1))
else
log_info "Log directory $log_dir not found (may be normal)"
fi
TOTAL_TESTS=$((TOTAL_TESTS + 1))
done
echo ""
}
# Configuration file tests
test_configuration() {
echo -e "${BLUE}=== Configuration File Tests ===${NC}"
# Check env file
if [[ -f ".env" ]]; then
log_success ".env file exists"
TESTS_PASSED=$((TESTS_PASSED + 1))
# Check key configuration entries
local required_vars=("LOCAL_IP" "CORS_ORIGIN" "NEXT_PUBLIC_API_URL")
for var in "${required_vars[@]}"; do
if grep -q "^$var=" .env; then
log_success "Config $var is set"
TESTS_PASSED=$((TESTS_PASSED + 1))
else
log_error "Config $var is not set"
TESTS_FAILED=$((TESTS_FAILED + 1))
fi
TOTAL_TESTS=$((TOTAL_TESTS + 1))
done
else
log_error ".env file not found"
TESTS_FAILED=$((TESTS_FAILED + 1))
fi
TOTAL_TESTS=$((TOTAL_TESTS + 1))
# Check Docker Compose file
if [[ -f "docker-compose.yml" ]]; then
log_success "docker-compose.yml exists"
TESTS_PASSED=$((TESTS_PASSED + 1))
else
log_error "docker-compose.yml not found"
TESTS_FAILED=$((TESTS_FAILED + 1))
fi
TOTAL_TESTS=$((TOTAL_TESTS + 1))
echo ""
}
# Cleanup tests
test_cleanup() {
echo -e "${BLUE}=== Cleanup Tests ===${NC}"
# Verify cleanup commands work
if [[ -f "deploy.sh" ]]; then
log_success "Deployment script exists"
TESTS_PASSED=$((TESTS_PASSED + 1))
# Test help command
if bash deploy.sh --help >/dev/null 2>&1; then
log_success "Deployment script help works"
TESTS_PASSED=$((TESTS_PASSED + 1))
else
log_error "Deployment script help fails"
TESTS_FAILED=$((TESTS_FAILED + 1))
fi
else
log_error "Deployment script not found"
TESTS_FAILED=$((TESTS_FAILED + 1))
fi
TOTAL_TESTS=$((TOTAL_TESTS + 2))
echo ""
}
# Generate test report
generate_report() {
echo -e "${BLUE}=== Test Report ===${NC}"
echo ""
echo "📊 Test stats:"
echo " Total tests: $TOTAL_TESTS"
echo -e " Passed: ${GREEN}$TESTS_PASSED${NC}"
echo -e " Failed: ${RED}$TESTS_FAILED${NC}"
local success_rate=$((TESTS_PASSED * 100 / TOTAL_TESTS))
echo " Success rate: $success_rate%"
echo ""
echo "📋 System info:"
echo " Docker version: $(docker --version)"
echo " Docker Compose version: $(docker-compose --version 2>/dev/null || docker compose version 2>/dev/null || echo 'unknown')"
echo " OS: $(uname -s) $(uname -r)"
echo " Test time: $(date)"
echo ""
if [[ $TESTS_FAILED -eq 0 ]]; then
echo -e "${GREEN}🎉 All tests passed! PrivyDrop deployment successful!${NC}"
echo ""
echo "🔗 Access links:"
echo " Frontend: http://localhost:3002"
echo " Backend API: http://localhost:3001"
# Show LAN access addresses
if [[ -f ".env" ]]; then
local local_ip=$(grep "LOCAL_IP=" .env | cut -d'=' -f2)
if [[ -n "$local_ip" && "$local_ip" != "127.0.0.1" ]]; then
echo ""
echo "🌐 LAN access:"
echo " Frontend: http://$local_ip:3002"
echo " Backend API: http://$local_ip:3001"
fi
fi
return 0
else
echo -e "${RED}$TESTS_FAILED test(s) failed${NC}"
echo ""
echo "🔧 Troubleshooting tips:"
echo " 1. View container status: docker-compose ps"
echo " 2. View container logs: docker-compose logs -f"
echo " 3. Redeploy: bash deploy.sh"
echo " 4. Clean and redeploy: bash deploy.sh --clean"
return 1
fi
}
# Main function
main() {
echo -e "${BLUE}=== PrivyDrop Docker Deployment Tests ===${NC}"
echo ""
# Check required tools
local missing_tools=()
for tool in curl jq bc nc; do
if ! command -v "$tool" >/dev/null 2>&1; then
missing_tools+=("$tool")
fi
done
if [[ ${#missing_tools[@]} -gt 0 ]]; then
log_warning "Missing test tools: ${missing_tools[*]}"
log_info "Suggested install: sudo apt-get install curl jq bc netcat"
echo ""
fi
# Run all tests
test_docker_environment
test_container_status
test_network_connectivity
test_api_functionality
test_webrtc_functionality
test_performance
test_security
test_logging
test_configuration
test_cleanup
# Generate report
generate_report
}
# Trap interrupt signals
trap 'echo -e "\n${YELLOW}Tests interrupted${NC}"; exit 1' INT TERM
# Run main function
main "$@"
+10 -1
View File
@@ -1,4 +1,8 @@
# PrivyDrop Deployment Guide
# PrivyDrop Deployment Guide (Bare-Metal)
> Audience & Scope: This guide targets developers/operators who prefer a non-container (bare-metal) setup.
>
> Recommended: Prefer the one-click Docker deployment for simplicity and robustness, including auto HTTPS and TURN. See [Docker Deployment Guide](./DEPLOYMENT_docker.md).
This guide provides comprehensive instructions for deploying the full-stack PrivyDrop application, including setting up Redis, a TURN server, the backend service, the frontend application, and configuring Nginx as a reverse proxy.
@@ -30,6 +34,7 @@ sudo bash backend/docker/env_install.sh
```
This script will automatically install:
- **Node.js v20** - Runtime environment
- **Redis Server** - Used for room management and caching
- **Coturn** - TURN/STUN server (optional, for NAT traversal)
@@ -38,6 +43,7 @@ This script will automatically install:
- **Certbot** - SSL certificate management
After installation, you can verify the services:
```bash
# Verify Node.js version
node -v
@@ -53,11 +59,13 @@ sudo systemctl status coturn
```
**Configuration Notes:**
- **Redis Configuration:** Default listening on `127.0.0.1:6379`, ensure your backend `.env` file includes correct `REDIS_HOST` and `REDIS_PORT`
- **TURN Service:** Optional configuration, PrivyDrop uses public STUN servers by default, only needed for extremely high NAT traversal requirements
- **Nginx:** Script installs official version and verifies stream module support
**TURN Server Firewall Configuration (if configuring TURN service):**
```bash
# Enable the Coturn service
sudo sed -i 's/#TURNSERVER_ENABLED=1/TURNSERVER_ENABLED=1/' /etc/default/coturn
@@ -68,6 +76,7 @@ sudo ufw reload
```
The ports seen via `sudo ufw app info Turnserver` are as follows:
- `3478,3479,5349,5350,49152:65535/tcp`
- `3478,3479,5349,5350,49152:65535/udp`
+14 -5
View File
@@ -1,4 +1,8 @@
# Privydrop 部署指南
# Privydrop 部署指南(裸机部署)
> 说明与定位:本指南面向具备 Linux 运维能力的开发者,介绍“裸机(非容器)”部署方式。
>
> 推荐方案:优先使用“一键 Docker 部署”,更简单、更稳健,支持自动签发/续期证书与 TURN。详见 [Docker 部署指南](./DEPLOYMENT_docker.zh-CN.md)。
本指南提供部署 Privydrop 全栈应用的全面说明,包括设置 Redis、TURN 服务器、后端服务、前端应用以及配置 Nginx 作为反向代理。
@@ -30,14 +34,16 @@ sudo bash backend/docker/env_install.sh
```
该脚本将自动安装:
- **Node.js v20** - 运行环境
- **Redis Server** - 用于房间管理和缓存
- **Coturn** - TURN/STUN 服务器(可选,用于NAT穿透)
- **Coturn** - TURN/STUN 服务器(可选,用于 NAT 穿透)
- **Nginx** - Web 服务器和反向代理(使用官方仓库)
- **PM2** - Node.js 进程管理器
- **Certbot** - SSL 证书管理
安装完成后,可以验证各服务状态:
```bash
# 验证 Node.js 版本
node -v
@@ -53,11 +59,13 @@ sudo systemctl status coturn
```
**注意事项:**
- **Redis配置:** 默认监听 `127.0.0.1:6379`,请确保后端 `.env` 文件中包含正确的 `REDIS_HOST``REDIS_PORT`
- **TURN服务:** 为可选配置,Privydrop 默认使用公共 STUN 服务器,只有对 NAT 穿透有极高要求时才需要配置
- **Redis 配置:** 默认监听 `127.0.0.1:6379`,请确保后端 `.env` 文件中包含正确的 `REDIS_HOST``REDIS_PORT`
- **TURN 服务:** 为可选配置,Privydrop 默认使用公共 STUN 服务器,只有对 NAT 穿透有极高要求时才需要配置
- **Nginx** 脚本安装官方版本并验证 stream 模块支持
**TURN服务器防火墙配置(如果需要配置TURN服务):**
**TURN 服务器防火墙配置(如果需要配置 TURN 服务):**
```bash
# 启用 Coturn 服务
sudo sed -i 's/#TURNSERVER_ENABLED=1/TURNSERVER_ENABLED=1/' /etc/default/coturn
@@ -68,6 +76,7 @@ sudo ufw reload
```
通过 `sudo ufw app info Turnserver` 看到的端口如下:
- `3478,3479,5349,5350,49152:65535/tcp`
- `3478,3479,5349,5350,49152:65535/udp`
+564
View File
@@ -0,0 +1,564 @@
# PrivyDrop Docker One-Click Deployment (Recommended)
This guide provides a one-click Docker deployment for PrivyDrop. It supports both private and public networks, automates config/build/start, and provisions HTTPS certificates.
## 🚀 Quick Start (Top)
```bash
# Private LAN (no domain/public IP)
bash ./deploy.sh --mode lan-http
# Private LAN + TURN (for complex NAT/LAN)
bash ./deploy.sh --mode lan-http --with-turn
# LAN HTTPS (self-signed; dev/managed env; explicitly enable 8443)
bash ./deploy.sh --mode lan-tls --enable-web-https --with-nginx
# Public IP without domain (with TURN)
bash ./deploy.sh --mode public --with-turn
# Public domain (HTTPS + Nginx + TURN + SNI 443, auto-issue/renew certs)
bash ./deploy.sh --mode full --domain your-domain.com --with-nginx --with-turn --le-email you@domain.com
```
- Requires Docker Compose v2 (command `docker compose`).
- In full mode, Lets Encrypt (webroot) is auto-issued and auto-renewed (no downtime); SNI 443 multiplexing is enabled by default (`turn.your-domain.com` → coturn:5349; others → web:8443).
## Modes Overview
- lan-http: Intranet HTTP; fastest to start; no TLS
- lan-tls: Intranet HTTPS (self-signed; dev/managed env); 8443 disabled by default; enable via `--enable-web-https`; HSTS disabled; turns:443 not guaranteed
- public: Public HTTP + TURN; works without a domain (no HTTPS/turns:443)
- full: Domain + HTTPS (Lets Encrypt auto-issue/renew) + TURN; SNI 443 split enabled by default (use `--no-sni443` to disable)
## 🎯 Deployment Advantages
Compared to traditional deployment methods, Docker deployment offers the following advantages:
| Comparison | Traditional Deployment | Docker Deployment |
| ---------------------------- | ------------------------------- | ------------------------------ |
| **Deploy Time** | 30-60 minutes | 5 minutes |
| **Technical Requirements** | Linux ops experience | Basic Docker knowledge |
| **Environment Requirements** | Public IP + Domain | Works on private networks |
| **Configuration Complexity** | 10+ manual steps | One-click auto configuration |
| **Success Rate** | ~70% | >95% |
| **Maintenance Difficulty** | Manual multi-service management | Automatic container management |
## 📋 System Requirements
### Minimum Configuration
- **CPU**: 1 core
- **Memory**: 512MB
- **Disk**: 2GB available space
- **Network**: Any network environment (private/public)
### Recommended Configuration
- **CPU**: 2+ cores
- **Memory**: 1GB+
- **Disk**: 5GB+ available space
- **Network**: 100Mbps+
### Software Dependencies
- Docker 20.10+
- Docker Compose 2.x (command `docker compose`)
- curl (for health checks, optional)
- openssl (cert tools; the script auto-installs certbot)
## 🚀 Quick Start
### 1. Get the Code
```bash
# Clone the project
git clone https://github.com/david-bai00/PrivyDrop.git
cd PrivyDrop
```
### 2. One-Click Deployment
```bash
# Basic deployment (recommended for beginners)
bash ./deploy.sh
```
That's it! 🎉
## 📚 Deployment Modes
### LAN HTTP (lan-http)
**Use Case**: Private network file transfer, personal use, testing environment
```bash
bash ./deploy.sh --mode lan-http
```
**Features**:
- ✅ HTTP access
- ✅ Private network P2P transfer
- ✅ Uses public STUN servers
- ✅ Zero configuration startup
### Public Mode
**Use Case**: Servers with public IP but no domain
```bash
bash ./deploy.sh --mode public --with-turn --with-nginx
```
**Features**:
- ✅ HTTP access
- ✅ Built-in TURN server
- ✅ Supports complex network environments
- ✅ Automatic NAT traversal configuration
### Full Mode (full)
**Use Case**: Production environment, public servers with domain
```bash
bash ./deploy.sh --mode full --domain your-domain.com --with-nginx --with-turn --le-email you@domain.com
```
**Features**:
- ✅ HTTPS secure access (Lets Encrypt auto-issue/renew, zero downtime)
- ✅ Nginx reverse proxy
- ✅ Built-in TURN server (default port range 49152-49252/udp)
- ✅ SNI 443 multiplexing (turn.<domain> → coturn:5349; others → web:8443)
- ✅ Complete production setup
> Tip: The script no longer auto-detects the deployment mode; always pass `--mode lan-http|lan-tls|public|full`. If the detected LAN IP is not the one you expect, add `--local-ip 192.168.x.x` to override.
## 🔧 Advanced Configuration
### Custom Ports
```bash
# Modify .env file
FRONTEND_PORT=8080
BACKEND_PORT=8081
HTTP_PORT=8000
```
### Build-Time Proxy (optional)
Set the following variables in `.env` (or export them before running `deploy.sh`) when the build needs to go through a proxy. The configuration generator now preserves these fields on subsequent runs.
```bash
HTTP_PROXY=http://your-proxy:7890
HTTPS_PROXY=http://your-proxy:7890
NO_PROXY=localhost,127.0.0.1,backend,frontend,redis,coturn
```
`docker compose` passes these values as build args; the Dockerfiles expose them as environment variables so `npm`/`pnpm` automatically reuse the proxy. Leave them blank if you don't need a proxy.
### Common Flags
```bash
# Enable only Nginx reverse proxy
bash ./deploy.sh --with-nginx
# Enable TURN (recommended in public/full)
bash ./deploy.sh --with-turn
# Explicitly enable SNI 443 (auto-enabled in full+domain; use --no-sni443 to disable)
bash ./deploy.sh --with-sni443
# Adjust TURN port range (default 49152-49252/udp)
bash ./deploy.sh --mode full --with-turn --turn-port-range 55000-55100
```
## 🌐 Access Methods
- With Nginx (recommended, same-origin gateway)
- lan-http/public: `http://localhost` (or `http://<public IP>`)
- lan-tls (with `--enable-web-https`): `https://localhost:8443` (or `https://<LAN IP>:8443`)
- full (with domain): `https://<your-domain>` (443)
- Health checks: `curl -fsS http://localhost/api/health` (lan-http/public), `curl -kfsS https://localhost:8443/api/health` (lan-tls+https), `curl -fsS https://<domain>/api/health` (full)
- Without Nginx (direct ports, for debugging only)
- Frontend: `http://localhost:3002` (or `http://<LAN IP>:3002`)
- API: `http://localhost:3001` (or `http://<LAN IP>:3001`)
- Note: direct ports may cause CORS or 404 in production/public setups and are not recommended for public access.
### HTTPS Access (lan-tls/full)
- lan-tls: with `--enable-web-https`, access via `https://localhost:8443` (certs in `docker/ssl/`). Import `docker/ssl/ca-cert.pem` into your browser or trust store on first use.
- full: after Lets Encrypt issuance, access via `https://<your-domain>` (443). Certs auto-issue/renew; hot-reload is handled via deploy hook.
## 🔍 Management Commands
### View Service Status
```bash
docker compose ps
```
### View Service Logs
```bash
# View all service logs
docker compose logs -f
# View specific service logs
docker compose logs -f backend
docker compose logs -f frontend
docker compose logs -f redis
```
### Restart Services
```bash
# Restart all services
docker compose restart
# Restart specific service
docker compose restart backend
```
### Stop Services
```bash
# Stop services but keep data
docker compose stop
# Stop services and remove containers
docker compose down
```
### Complete Cleanup
```bash
# Clean all containers, images and data
bash ./deploy.sh --clean
```
## 🛠️ Troubleshooting
### Common Issues
#### 1. Port Already in Use
**Symptom**: Deployment shows port occupation warning
```
⚠️ The following ports are already in use: 3002, 3001
```
**Solution**:
```bash
# First try cleaning previous containers
bash ./deploy.sh --clean # or docker compose down
# If the port is still occupied, locate the process
sudo ss -tulpn | grep :3002
sudo kill -9 <PID>
# Finally, adjust the exposed ports in .env if necessary
vim .env # Update FRONTEND_PORT / BACKEND_PORT
```
#### 2. Insufficient Memory
**Symptom**: Containers fail to start or restart frequently
**Solution**:
```bash
# Check memory usage
free -h
# Add swap space (temporary solution)
sudo fallocate -l 1G /swapfile
sudo chmod 600 /swapfile
sudo mkswap /swapfile
sudo swapon /swapfile
```
#### 3. Docker Permission Issues
**Symptom**: Permission denied errors
**Solution**:
```bash
# Add user to docker group
sudo usermod -aG docker $USER
# Re-login or refresh group permissions
newgrp docker
```
#### 4. Service Inaccessible
**Symptom**: Browser cannot open pages
**Solution**:
```bash
# 1. Check service status
docker compose ps
# 2. Check health status
curl http://localhost:3001/health
curl http://localhost:3002/api/health
# 3. View detailed logs
docker compose logs -f
# 4. Check firewall
sudo ufw status
```
#### 5. WebRTC Connection Failure
**Symptom**: Cannot establish P2P connections
**Solution**:
```bash
# Enable TURN server
bash ./deploy.sh --with-turn
# Check network connectivity
curl -I http://localhost:3001/api/get_room
```
### Health Checks
The project provides comprehensive health check functionality:
```bash
# Run health check tests
bash test-health-apis.sh
# Manual service checks
curl http://localhost:3001/health # Backend basic check
curl http://localhost:3001/health/detailed # Backend detailed check
curl http://localhost:3002/api/health # Frontend check
```
### Performance Monitoring
```bash
# View container resource usage
docker stats
# View disk usage
docker system df
# Clean unused resources
docker system prune -f
```
## 📊 Performance Optimization
### Production Environment Optimization
1. **Enable Nginx Caching**:
```bash
bash deploy.sh --with-nginx
```
2. **Configure Resource Limits**:
```yaml
# Add to docker-compose.yml
services:
backend:
deploy:
resources:
limits:
memory: 256M
reservations:
memory: 128M
```
3. **Enable Log Rotation**:
```bash
# Configure log size limits
echo '{"log-driver":"json-file","log-opts":{"max-size":"10m","max-file":"3"}}' | sudo tee /etc/docker/daemon.json
sudo systemctl restart docker
```
### Network Optimization
1. **Use Dedicated Network**:
```yaml
networks:
privydrop-network:
driver: bridge
ipam:
config:
- subnet: 172.20.0.0/16
```
2. **Enable HTTP/2**:
```bash
# Auto-enabled (requires HTTPS)
bash deploy.sh --mode full --with-nginx
```
## 🔒 Security Configuration
### LAN HTTPS (lan-tls, self-signed, dev/managed env)
- 8443 is disabled by default; explicitly enable with:
```bash
bash ./deploy.sh --mode lan-tls --enable-web-https --with-nginx
```
- For development or managed devices only (internal CA trusted fleet-wide); HSTS disabled; `turns:443` not guaranteed. For restricted networks (443-only), use full (domain + trusted cert + SNI 443).
Usage (strongly recommended)
1) Import the self-signed CA (required)
- Location: `docker/ssl/ca-cert.pem`
- Browser import:
- Chrome/Edge: Settings → Privacy & Security → Security → Manage certificates → “Trusted Root Certification Authorities” → Import `ca-cert.pem`
- macOS: Keychain Access → System → Certificates → Import `ca-cert.pem` → set to “Always Trust”
- Linux (system-wide):
- `sudo cp docker/ssl/ca-cert.pem /usr/local/share/ca-certificates/privydrop-ca.crt`
- `sudo update-ca-certificates`
- Without trusting the CA, browser HTTPS will show untrusted cert warnings and API requests will fail.
2) Access endpoints (default ports and paths)
- Nginx reverse proxy: `http://localhost`
- HTTPS (Web): `https://localhost:8443`, `https://<LAN IP>:8443`
- Frontend direct (optional): `http://localhost:3002`, `http://<LAN IP>:3002`
- Note: In lan-tls, 443 is not open; HTTPS uses 8443.
3) CORS
- For convenience, common dev origins are allowed by default: `https://<LAN IP>:8443`, `https://localhost:8443`, `http://localhost`, `http://<LAN IP>`, `http://localhost:3002`, `http://<LAN IP>:3002`.
- To minimize allowed origins, edit `CORS_ORIGIN` in `.env` and then `docker compose restart backend`.
4) Health checks
- `curl -kfsS https://localhost:8443/api/health` → 200
- `bash ./test-health-apis.sh` → all tests should pass (frontend container trusts the self-signed CA).
5) Deployment hints
- The script prints only reachable Nginx endpoints; in lan-tls it will show `https://localhost:8443` (and `https://<LAN IP>:8443` if available).
### Public Domain Deployment (HTTPS + Nginx) — Quick Test
1) Point your domain A record to the server IP (optional: also `turn.<your-domain>` to the same IP)
2) Run:
```bash
./deploy.sh --mode full --domain <your-domain> --with-nginx --with-turn --le-email you@domain.com
```
3) Open ports: `80`, `443`, `3478/udp`, `5349/tcp`, `5349/udp`
4) Verify: visit `https://<your-domain>`, `/api/health` returns 200; open `chrome://webrtc-internals` and check for `relay` candidates (TURN)
### SSL/TLS Automation (Lets Encrypt)
In full mode, certificates are auto-issued and auto-renewed:
- Initial issuance: webroot (no downtime); system certs live under `/etc/letsencrypt/live/<domain>/`; copied to `docker/ssl/` and 443 is enabled.
- Renewal: `certbot.timer` or `/etc/cron.d/certbot` runs daily; the deploy-hook copies new certs to `docker/ssl/` and hot-reloads Nginx/Coturn.
- Lineage suffixes (-0001/-0002) are handled automatically.
### Network Security
1. **Firewall Configuration**:
```bash
# Ubuntu/Debian
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw allow 3478/udp # TURN server
```
2. **Container Network Isolation**:
- All services run in isolated networks
- Only necessary ports exposed
- Internal services communicate using container names
## 📈 Monitoring and Logging
### Log Management
All service logs are centrally stored in the `logs/` directory:
```
logs/
├── nginx/ # Nginx access and error logs
├── backend/ # Backend application logs
├── frontend/ # Frontend application logs
└── coturn/ # TURN server logs
```
## 🔄 Updates and Maintenance
### Update Application
```bash
# Pull latest code
git pull origin main
# Redeploy
bash deploy.sh
```
### Data Backup
```bash
# Backup Redis data
docker compose exec redis redis-cli BGSAVE
# Backup SSL certificates
tar -czf ssl-backup.tar.gz docker/ssl/
# Backup configuration files
cp .env .env.backup
```
### Regular Maintenance
```bash
# Clean unused images and containers
docker system prune -f
# Update base images
docker compose pull
docker compose up -d
```
## 🆘 Getting Help
### Command Line Help
```bash
bash ./deploy.sh --help
### Additional Notes
- In Docker environments, Next.js Image optimization is disabled by default (`NEXT_IMAGE_UNOPTIMIZED=true`) to avoid container loopback fetch failures on `/_next/image`. To enable it, set the variable to `false` and rebuild.
- With `--with-nginx`, the frontend is built to use same-origin API (`/api`, `/socket.io/`). Use the gateway URLs printed by the script; direct ports `:3002/:3001` are not recommended in production.
```
### Online Resources
- [Project Homepage](https://github.com/david-bai00/PrivyDrop)
- [Live Demo](https://www.privydrop.app/)
- [Issue Reporting](https://github.com/david-bai00/PrivyDrop/issues)
### Community Support
- GitHub Issues: Technical questions and bug reports
+566
View File
@@ -0,0 +1,566 @@
# PrivyDrop Docker 一键部署(推荐)
本指南提供 PrivyDrop 的 Docker 一键部署方案,支持内网与公网,一次命令完成配置、构建、启动与证书自动化。
## 🚀 快速开始(置顶)
```bash
# 内网(无域名/无公网IP
bash ./deploy.sh --mode lan-http
# 内网 + TURN(推荐用于复杂内网/NAT)
bash ./deploy.sh --mode lan-http --with-turn
# 内网 HTTPS(自签,开发/受管环境,需显式开启 8443)
bash ./deploy.sh --mode lan-tls --enable-web-https --with-nginx
# 公网IP(无域名),含 TURN
bash ./deploy.sh --mode public --with-turn
# 公网域名(HTTPS + Nginx + TURN + SNI 443 分流,自动申请/续期证书)
bash ./deploy.sh --mode full --domain your-domain.com --with-nginx --with-turn --le-email you@domain.com
```
- 使用 Docker Compose V2(命令 `docker compose`)。
- full 模式自动申请 Lets Encrypt 证书(webroot,无停机)并自动续期;默认启用 SNI 443 分流(`turn.your-domain.com` → coturn:5349,其余 → web:8443)。
## 模式一览
- lan-http:内网 HTTP;最快上手,默认不启用 TLS
- lan-tls:内网 HTTPS(自签,仅开发/受管环境);默认不启 8443,需 `--enable-web-https` 显式开启;禁用 HSTS;不保证 turns:443
- public:公网 HTTP;开启 TURN;无域名也可使用(不提供 HTTPS/turns:443
- full:域名 + HTTPSLets Encrypt 自动签发/续期)+ TURN;默认启用 SNI 443 分流(可 `--no-sni443` 关闭)
## 🎯 部署优势
相比传统部署方式,Docker 部署具有以下优势:
| 对比项目 | 传统部署 | Docker 部署 |
| -------------- | -------------------- | ---------------- |
| **部署时间** | 30-60 分钟 | 5 分钟 |
| **技术要求** | Linux 运维经验 | 会用 Docker 即可 |
| **环境要求** | 公网 IP + 域名 | 内网即可使用 |
| **配置复杂度** | 10+个手动步骤 | 一键自动配置 |
| **成功率** | ~70% | >95% |
| **维护难度** | 需要手动管理多个服务 | 容器自动管理 |
## 📋 系统要求
### 最低配置
- **CPU**: 1 核
- **内存**: 512MB
- **磁盘**: 2GB 可用空间
- **网络**: 任意网络环境(内网/公网均可)
### 推荐配置
- **CPU**: 2 核及以上
- **内存**: 1GB 及以上
- **磁盘**: 5GB 及以上可用空间
- **网络**: 100Mbps 及以上
### 软件依赖
- Docker 20.10+
- Docker Compose 2.x(命令 `docker compose`
- curl(用于健康检查,可选)
- openssl(用于证书工具,脚本会自动安装 certbot)
## 🚀 快速开始
### 1. 获取代码
```bash
# 克隆项目
git clone https://github.com/david-bai00/PrivyDrop.git
cd PrivyDrop
```
### 2. 一键部署(示例)
```bash
# 示例:公网域名(HTTPS + Nginx + TURN
bash ./deploy.sh --mode full --domain your-domain.com --with-nginx --with-turn --le-email you@domain.com
```
## 📚 部署模式详解
### lan-http(内网 HTTP
**适用场景**: 内网文件传输、个人使用、测试环境
```bash
bash ./deploy.sh --mode lan-http
```
**特性**:
- ✅ HTTP 访问
- ✅ 内网 P2P 传输
- ✅ 使用公共 STUN 服务器
- ✅ 零配置启动
### 公网模式
**适用场景**: 有公网 IP 但无域名的服务器
```bash
bash ./deploy.sh --mode public --with-turn --with-nginx
```
**特性**:
- ✅ HTTP 访问
- ✅ 内置 TURN 服务器
- ✅ 支持复杂网络环境
- ✅ 自动配置 NAT 穿透
### 完整模式(full
**适用场景**: 生产环境、有域名的公网服务器
```bash
bash ./deploy.sh --mode full --domain your-domain.com --with-nginx --with-turn --le-email you@domain.com
```
**特性**:
- ✅ HTTPS 安全访问(Lets Encrypt 自动签发/续期,无停机)
- ✅ Nginx 反向代理
- ✅ 内置 TURN 服务器(默认端口段 49152-49252/udp,可覆盖)
- ✅ SNI 443 分流(turn.<domain> → coturn:5349,其余 → web:8443
- ✅ 完整生产环境配置
> 提示:脚本不再自动判断部署模式,请显式传递 `--mode lan-http|lan-tls|public|full`。若自动检测到的局域网 IP 与预期不符,可使用 `--local-ip 192.168.x.x` 进行覆盖。
## 🔧 高级配置
### 自定义端口
```bash
# 修改 .env 文件
FRONTEND_PORT=8080
BACKEND_PORT=8081
HTTP_PORT=8000
```
### 构建阶段代理(可选)
若需要在 Docker 构建时走网络代理,可在 `.env` 中设置以下变量,或者在执行 `deploy.sh` 之前通过环境变量导出。重新运行配置脚本时,这些字段会被保留:
```bash
HTTP_PROXY=http://你的代理:7890
HTTPS_PROXY=http://你的代理:7890
NO_PROXY=localhost,127.0.0.1,backend,frontend,redis,coturn
```
`docker compose` 会把这些变量作为 build args 传递给前后端镜像,Dockerfile 中会自动设置为环境变量,从而让 `npm`/`pnpm` 使用代理。若无需代理,保持为空即可。
### 常用开关
```bash
# 仅启用 Nginx
bash ./deploy.sh --with-nginx
# 启用 TURNpublic/full 建议)
bash ./deploy.sh --with-turn
# 显式启用 SNI 443full+domain 默认开启,可用 --no-sni443 关闭)
bash ./deploy.sh --with-sni443
# 调整 TURN 端口段(默认 49152-49252/udp
bash ./deploy.sh --mode full --with-turn --turn-port-range 55000-55100
```
## 🌐 访问方式
- 启用 Nginx(推荐,同源网关)
- lan-http/public`http://localhost`(或 `http://<公网IP>`
- lan-tls(启用 `--enable-web-https`):`https://localhost:8443`(或 `https://<LAN IP>:8443`
- full(有域名):`https://<your-domain>`443
- 健康检查:`curl -fsS http://localhost/api/health`lan-http/public),`curl -kfsS https://localhost:8443/api/health`lan-tls+https),`curl -fsS https://<domain>/api/health`full
- 未启用 Nginx(直连端口,仅调试)
- 前端:`http://localhost:3002`(或 `http://<LAN IP>:3002`
- API`http://localhost:3001`(或 `http://<LAN IP>:3001`
- 注意:直连端口在生产/公网可能导致 CORS 或 404,不推荐对外使用。
### HTTPS 访问(lan-tls/full
- lan-tls:开启 `--enable-web-https` 后通过 `https://localhost:8443` 访问(证书在 `docker/ssl/`)。首次访问需导入 `docker/ssl/ca-cert.pem` 到浏览器或系统信任。
- full:签发 Lets Encrypt 后通过 `https://<your-domain>` 访问(443)。
## 🔍 管理命令
### 查看服务状态
```bash
docker compose ps
```
### 查看服务日志
```bash
# 查看所有服务日志
docker compose logs -f
# 查看特定服务日志
docker compose logs -f backend
docker compose logs -f frontend
docker compose logs -f redis
```
### 重启服务
```bash
# 重启所有服务
docker compose restart
# 重启特定服务
docker compose restart backend
```
### 停止服务
```bash
# 停止服务但保留数据
docker compose stop
# 停止服务并删除容器
docker compose down
```
### 完全清理
```bash
# 清理所有容器、镜像和数据
bash ./deploy.sh --clean
```
## 🛠️ 故障排除
### 常见问题
#### 1. 端口被占用
**现象**: 部署时提示端口已被占用
```
⚠️ 以下端口已被占用: 3002, 3001
```
**解决方案**:
```bash
# 方法1: 清理旧容器
bash ./deploy.sh --clean # 或 docker compose down
# 方法2: 查找并结束占用进程
sudo ss -tulpn | grep :3002
sudo kill -9 <PID>
# 方法3: 如仍冲突,再调整端口
vim .env # 修改 FRONTEND_PORT / BACKEND_PORT
```
#### 2. 内存不足
**现象**: 容器启动失败或频繁重启
**解决方案**:
```bash
# 检查内存使用
free -h
# 添加交换空间 (临时解决)
sudo fallocate -l 1G /swapfile
sudo chmod 600 /swapfile
sudo mkswap /swapfile
sudo swapon /swapfile
```
#### 3. Docker 权限问题
**现象**: 提示权限不足
**解决方案**:
```bash
# 将用户添加到docker组
sudo usermod -aG docker $USER
# 重新登录或刷新组权限
newgrp docker
```
#### 4. 服务无法访问
**现象**: 浏览器无法打开页面
**解决方案**:
```bash
# 1. 检查服务状态
docker compose ps
# 2. 检查健康状态
curl http://localhost:3001/health
curl http://localhost:3002/api/health
# 3. 查看详细日志
docker compose logs -f
# 4. 检查防火墙
sudo ufw status
```
#### 5. WebRTC 连接失败
**现象**: 无法建立 P2P 连接
**解决方案**:
```bash
# 启用TURN服务器
bash ./deploy.sh --with-turn
# 检查网络连接
curl -I http://localhost:3001/api/get_room
```
### 健康检查
项目提供了完整的健康检查功能:
```bash
# 运行健康检查测试
bash ./test-health-apis.sh
# 手动检查各服务(直连端口)
curl http://localhost:3001/health # 后端基础检查(直连)
curl http://localhost:3001/health/detailed # 后端详细检查(直连)
curl http://localhost:3002/api/health # 前端检查(直连)
# 同源网关(启用 Nginx
curl -fsS http://localhost/api/health # lan-http/public
curl -kfsS https://localhost:8443/api/health # lan-tls (https)
```
### 性能监控
```bash
# 查看容器资源使用
docker stats
# 查看磁盘使用
docker system df
# 清理未使用的资源
docker system prune -f
```
## 📊 性能优化
### 生产环境优化
1. **启用 Nginx 缓存**:
```bash
bash ./deploy.sh --with-nginx
```
2. **配置资源限制**:
```yaml
# 在 docker-compose.yml 中添加
services:
backend:
deploy:
resources:
limits:
memory: 256M
reservations:
memory: 128M
```
3. **启用日志轮转**:
```bash
# 配置日志大小限制
echo '{"log-driver":"json-file","log-opts":{"max-size":"10m","max-file":"3"}}' | sudo tee /etc/docker/daemon.json
sudo systemctl restart docker
```
### 网络优化
1. **使用专用网络**:
```yaml
networks:
privydrop-network:
driver: bridge
ipam:
config:
- subnet: 172.20.0.0/16
```
2. **启用 HTTP/2**:
```bash
# 自动启用 (需要 HTTPS)
bash ./deploy.sh --mode full --with-nginx
```
## 🔒 HTTPS 与安全
### 内网 HTTPSlan-tls,自签,开发/受管环境)
- 默认不启 8443;需 `--enable-web-https` 显式开启:
```bash
bash ./deploy.sh --mode lan-tls --enable-web-https --with-nginx
```
- 仅用于开发或受管终端(全员导入内部 CA);禁用 HSTS;不保证 `turns:443`;受限网络(仅 443 出口)应使用 full(域名 + 受信证书 + SNI 443)。
使用说明(强烈建议逐条完成)
1) 导入自签 CA 证书(必做)
- 证书位置:`docker/ssl/ca-cert.pem`
- 浏览器导入:
- Chrome/Edge:设置 → 隐私与安全 → 安全 → 管理证书 → “受信任的根证书颁发机构” → 导入 `ca-cert.pem`
- macOS:钥匙串访问 → System → 证书 → 导入 `ca-cert.pem` → 设置“始终信任”
- Linux(系统层):
- `sudo cp docker/ssl/ca-cert.pem /usr/local/share/ca-certificates/privydrop-ca.crt`
- `sudo update-ca-certificates`
- 未导入时,浏览器访问 HTTPS 会提示“证书无效/不受信任”,API 请求也会失败。
2) 访问方式(默认端口与路径)
- Nginx 反代:`http://localhost`
- HTTPSWeb):`https://localhost:8443``https://<局域网IP>:8443`
- 前端直连(可选):`http://localhost:3002``http://<局域网IP>:3002`
- 说明:lan-tls 下未开启 443HTTPS 统一走 8443。
3) 跨域(CORS)说明
- 为方便开发与调试,脚本已默认放开常见来源:`https://<局域网IP>:8443``https://localhost:8443``http://localhost``http://<局域网IP>``http://localhost:3002``http://<局域网IP>:3002`
- 若仍需最小化来源,请在 `.env` 中精准收敛 `CORS_ORIGIN`,并 `docker compose restart backend`
4) 健康检查
- `curl -kfsS https://localhost:8443/api/health` → 200
- `bash ./test-health-apis.sh` → 所有测试应通过(前端 detailed 健康已在容器内信任自签 CA)。
5) 部署提示
- 脚本会只显示可访问的 Nginx 入口;lan-tls 下将显示明确的 `https://localhost:8443`(如存在局域网 IP 也将显示 `https://<IP>:8443`)。
### 公网域名部署(HTTPS + Nginx)快速测试
1) 将域名 A 记录解析至服务器 IP(可选:`turn.<your-domain>` 指向相同 IP
2) 运行:
```bash
./deploy.sh --mode full --domain <your-domain> --with-nginx --with-turn --le-email you@domain.com
```
3) 放行端口:`80`, `443`, `3478/udp`, `5349/tcp`, `5349/udp`
4) 验证:访问 `https://<your-domain>``/api/health` 返回 200;打开浏览器 `webrtc-internals` 观察是否出现 `relay` 候选(TURN
### 证书自动化(Lets Encrypt
full 模式自动申请并续期证书:
- 首次签发:webroot 模式(无停机),系统证书在 `/etc/letsencrypt/live/<domain>/`,脚本复制到 `docker/ssl/` 并启用 443
- 续期:`certbot.timer``/etc/cron.d/certbot` 每日尝试 `certbot renew`deploy-hook 自动复制新证书并热重载 Nginx/Coturn
- 证书谱系(-0001/-0002)已自动适配,无需手动处理。
### 网络安全
1. **防火墙配置**:
```bash
# Ubuntu/Debian
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw allow 3478/udp # TURN服务器
```
2. **容器网络隔离**:
- 所有服务运行在独立网络中
- 仅暴露必要端口
- 内部服务使用容器名通信
## 📈 监控和日志
### 日志管理
所有服务日志统一存储在 `logs/` 目录:
```
logs/
├── nginx/ # Nginx访问和错误日志
├── backend/ # 后端应用日志
├── frontend/ # 前端应用日志
└── coturn/ # TURN服务器日志
```
## 🔄 更新和维护
### 更新应用
```bash
# 拉取最新代码
git pull origin main
# 重新部署
bash ./deploy.sh
```
### 数据备份
```bash
# 备份Redis数据
docker compose exec redis redis-cli BGSAVE
# 备份SSL证书
tar -czf ssl-backup.tar.gz docker/ssl/
# 备份配置文件
cp .env .env.backup
```
### 定期维护
```bash
# 清理未使用的镜像和容器
docker system prune -f
# 更新基础镜像
docker compose pull
docker compose up -d
```
## 🆘 获取帮助
### 命令行帮助
```bash
bash ./deploy.sh --help
```
### 在线资源
- [项目主页](https://github.com/david-bai00/PrivyDrop)
- [在线演示](https://www.privydrop.app/)
- [问题反馈](https://github.com/david-bai00/PrivyDrop/issues)
### 社区支持
- GitHub Issues: 技术问题和 bug 报告
### 其他提示
- Docker 环境默认关闭 Next.js Image 优化(`NEXT_IMAGE_UNOPTIMIZED=true`),以避免容器内对公网 IP 回环抓取导致 `/_next/image` 502;如需开启,将其设为 `false` 并重新构建。
- 启用 `--with-nginx` 时,前端会构建为同源 API(相对路径 `/api``/socket.io/`);请优先使用脚本输出的网关入口,不要直接使用 `:3002/:3001` 对外访问,否则可能触发 CORS 或 404。
+15
View File
@@ -0,0 +1,15 @@
node_modules
.next
.git
.gitignore
README.md
Dockerfile
.dockerignore
.env*
npm-debug.log*
.npm
coverage
.nyc_output
*.log
public/sw.js
public/workbox-*.js
+91
View File
@@ -0,0 +1,91 @@
# Multi-stage build — build stage
FROM node:18-alpine AS builder
ARG HTTP_PROXY
ARG HTTPS_PROXY
ARG NO_PROXY
ENV http_proxy ${HTTP_PROXY} \
https_proxy ${HTTPS_PROXY} \
no_proxy ${NO_PROXY}
WORKDIR /app
# Copy package files
COPY package*.json ./
COPY pnpm-lock.yaml ./
# Install pnpm
RUN npm install -g pnpm --no-audit --no-fund
# Install dependencies
RUN pnpm install --frozen-lockfile
# Copy source code
COPY . .
# Declare and use build-time public vars after deps installation to avoid cache invalidation when only API/TURN change
ARG NEXT_PUBLIC_API_URL
ARG NEXT_PUBLIC_TURN_HOST
ARG NEXT_PUBLIC_TURN_USERNAME
ARG NEXT_PUBLIC_TURN_PASSWORD
ARG NEXT_IMAGE_UNOPTIMIZED
# Inject public env vars during frontend build (for client direct access to backend and TURN)
ENV NEXT_PUBLIC_API_URL=${NEXT_PUBLIC_API_URL}
ENV NEXT_PUBLIC_TURN_HOST=${NEXT_PUBLIC_TURN_HOST}
ENV NEXT_PUBLIC_TURN_USERNAME=${NEXT_PUBLIC_TURN_USERNAME}
ENV NEXT_PUBLIC_TURN_PASSWORD=${NEXT_PUBLIC_TURN_PASSWORD}
ENV NEXT_IMAGE_UNOPTIMIZED=${NEXT_IMAGE_UNOPTIMIZED}
# Set environment variables
ENV NEXT_TELEMETRY_DISABLED 1
ENV NODE_ENV production
# Build the app
RUN pnpm build
# Production stage
FROM node:18-alpine AS runner
WORKDIR /app
# Create a non-root user
RUN addgroup -g 1001 -S nodejs && \
adduser -S nextjs -u 1001 -G nodejs
# Copy build artifacts
COPY --from=builder /app/public ./public
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
COPY health-check.js ./
# Set environment variables
ENV NODE_ENV production
ENV NEXT_TELEMETRY_DISABLED 1
ENV PORT 3002
ENV HOSTNAME "0.0.0.0"
USER nextjs
# Expose ports
EXPOSE 3002
# Use a Node.js script for health checks (instead of curl)
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
CMD node health-check.js
# Start the app
CMD ["node", "server.js"]
# Keep public env vars at runtime (optional; helps SSR read them)
# Re-declare ARGs in this stage so they can expand into ENV
ARG NEXT_PUBLIC_API_URL
ARG NEXT_PUBLIC_TURN_HOST
ARG NEXT_PUBLIC_TURN_USERNAME
ARG NEXT_PUBLIC_TURN_PASSWORD
ARG NEXT_IMAGE_UNOPTIMIZED
ENV NEXT_PUBLIC_API_URL=${NEXT_PUBLIC_API_URL}
ENV NEXT_PUBLIC_TURN_HOST=${NEXT_PUBLIC_TURN_HOST}
ENV NEXT_PUBLIC_TURN_USERNAME=${NEXT_PUBLIC_TURN_USERNAME}
ENV NEXT_PUBLIC_TURN_PASSWORD=${NEXT_PUBLIC_TURN_PASSWORD}
ENV NEXT_IMAGE_UNOPTIMIZED=${NEXT_IMAGE_UNOPTIMIZED}
+118
View File
@@ -0,0 +1,118 @@
import { NextRequest, NextResponse } from 'next/server';
const startTime = Date.now();
export async function GET(request: NextRequest) {
try {
const errors: string[] = [];
let status = 'healthy';
// 基础健康信息
const basicHealth = {
status: 'healthy',
timestamp: new Date().toISOString(),
uptime: Math.floor((Date.now() - startTime) / 1000),
service: 'privydrop-frontend',
version: process.env.npm_package_version || '1.0.0',
environment: process.env.NODE_ENV || 'development'
};
// Check backend API connectivity
const backendHealth = await checkBackendHealth();
if (backendHealth.status !== 'connected') {
errors.push('Backend API connection failed');
status = 'degraded';
}
// System info snapshot
const systemInfo = {
runtime: process.env.NEXT_RUNTIME || 'nodejs',
nextjs: {
version: process.version,
platform: process.platform,
arch: process.arch
},
memory: process.memoryUsage ? {
used: formatBytes(process.memoryUsage().heapUsed),
total: formatBytes(process.memoryUsage().heapTotal),
percent: Math.round((process.memoryUsage().heapUsed / process.memoryUsage().heapTotal) * 100)
} : null
};
const detailedHealth = {
...basicHealth,
status,
dependencies: {
backend: backendHealth
},
system: systemInfo,
...(errors.length > 0 && { errors })
};
const httpStatus = status === 'healthy' ? 200 : 503;
return NextResponse.json(detailedHealth, { status: httpStatus });
} catch (error) {
console.error('Detailed frontend health check error:', error);
return NextResponse.json({
status: 'unhealthy',
timestamp: new Date().toISOString(),
service: 'privydrop-frontend',
error: error instanceof Error ? error.message : 'Unknown error'
}, { status: 503 });
}
}
// Check backend API health
async function checkBackendHealth() {
try {
// Prefer container-internal URL, then public URL, then localhost fallback
const backendUrl = process.env.BACKEND_INTERNAL_URL || process.env.NEXT_PUBLIC_API_URL || 'http://localhost:3001';
const start = Date.now();
const response = await fetch(`${backendUrl}/health`, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
},
// Timeout to avoid long-hanging connections in degraded networks
signal: AbortSignal.timeout(5000)
});
const responseTime = Date.now() - start;
if (response.ok) {
const data = await response.json();
return {
status: 'connected',
responseTime,
backendUrl,
backendService: data.service || 'unknown'
};
} else {
return {
status: 'error',
responseTime,
backendUrl,
httpStatus: response.status,
error: `HTTP ${response.status}`
};
}
} catch (error) {
return {
status: 'disconnected',
backendUrl: process.env.BACKEND_INTERNAL_URL || process.env.NEXT_PUBLIC_API_URL || 'http://localhost:3001',
error: error instanceof Error ? error.message : 'Unknown error'
};
}
}
// 格式化字节数
function formatBytes(bytes: number): string {
if (bytes === 0) return '0 B';
const k = 1024;
const sizes = ['B', 'KB', 'MB', 'GB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + ' ' + sizes[i];
}
+30
View File
@@ -0,0 +1,30 @@
import { NextRequest, NextResponse } from 'next/server';
const startTime = Date.now();
export async function GET(request: NextRequest) {
try {
const health = {
status: 'healthy',
timestamp: new Date().toISOString(),
uptime: Math.floor((Date.now() - startTime) / 1000),
service: 'privydrop-frontend',
version: process.env.npm_package_version || '1.0.0',
environment: process.env.NODE_ENV || 'development',
nextjs: {
version: process.env.NEXT_RUNTIME || 'nodejs'
}
};
return NextResponse.json(health, { status: 200 });
} catch (error) {
console.error('Frontend health check error:', error);
return NextResponse.json({
status: 'unhealthy',
timestamp: new Date().toISOString(),
service: 'privydrop-frontend',
error: error instanceof Error ? error.message : 'Unknown error'
}, { status: 503 });
}
}
+7 -7
View File
@@ -1,3 +1,5 @@
import { ManagerOptions, SocketOptions } from "socket.io-client";
export const config = {
API_URL: process.env.NEXT_PUBLIC_API_URL!,
USE_HTTPS: process.env.NODE_ENV !== "development",
@@ -54,14 +56,12 @@ export const getIceServers = () => {
return iceServers;
};
export const getSocketOptions = () => {
return config.USE_HTTPS
? {
secure: true,
export const getSocketOptions = (): Partial<ManagerOptions & SocketOptions> => {
// Allow polling fallback; do not force "secure" here — protocol will be inferred
return {
path: "/socket.io/",
transports: ["websocket"],
}
: undefined;
transports: ["websocket", "polling"],
};
};
export const getFetchOptions = (options: RequestInit = {}): RequestInit => {
+2 -2
View File
@@ -49,7 +49,7 @@ const ClipboardApp = () => {
handleDownloadFile,
} = useFileTransferHandler({ messages, putMessageInMs });
// 简化的 WebRTC 连接初始化
// Simplified WebRTC connection initialization
const {
requestFile,
requestFolder,
@@ -60,7 +60,7 @@ const ClipboardApp = () => {
putMessageInMs,
});
// 大大简化的房间管理 - 不再需要传递任何 WebRTC 依赖
// Greatly simplified room management - No longer need to pass any WebRTC dependencies
const {
processRoomIdInput,
joinRoom,
@@ -225,7 +225,7 @@ const FileListDisplay: React.FC<FileListDisplayProps> = ({
}
return updated;
});
}, 3000);
}, 3002);
}
}
});
+34
View File
@@ -0,0 +1,34 @@
#!/usr/bin/env node
const http = require('http');
const options = {
host: 'localhost',
port: process.env.PORT || 3002,
path: '/api/health',
timeout: 2000,
method: 'GET'
};
const req = http.request(options, (res) => {
if (res.statusCode === 200) {
console.log('Frontend health check passed');
process.exit(0);
} else {
console.log(`Frontend health check failed with status: ${res.statusCode}`);
process.exit(1);
}
});
req.on('error', (err) => {
console.log(`Frontend health check failed: ${err.message}`);
process.exit(1);
});
req.on('timeout', () => {
console.log('Frontend health check timeout');
req.destroy();
process.exit(1);
});
req.end();
+12 -4
View File
@@ -27,7 +27,7 @@ export class ReceptionConfig {
// Network and timing
static readonly NETWORK_CONFIG = {
FIREFOX_COMPATIBILITY_DELAY: 10, // ms delay for Firefox compatibility
FINALIZATION_TIMEOUT: 30000, // 30s timeout for file finalization
FINALIZATION_TIMEOUT: 30020, // 30s timeout for file finalization
GRACEFUL_SHUTDOWN_TIMEOUT: 5000, // 5s timeout for graceful shutdown
};
@@ -54,7 +54,10 @@ export class ReceptionConfig {
/**
* Calculate expected chunks count for file size and offset
*/
static calculateExpectedChunks(fileSize: number, startOffset: number = 0): number {
static calculateExpectedChunks(
fileSize: number,
startOffset: number = 0
): number {
return Math.ceil((fileSize - startOffset) / this.FILE_CONFIG.CHUNK_SIZE);
}
@@ -68,7 +71,12 @@ export class ReceptionConfig {
/**
* Check if file should be saved to disk
*/
static shouldSaveToDisk(fileSize: number, hasSaveDirectory: boolean): boolean {
return hasSaveDirectory || fileSize >= this.FILE_CONFIG.LARGE_FILE_THRESHOLD;
static shouldSaveToDisk(
fileSize: number,
hasSaveDirectory: boolean
): boolean {
return (
hasSaveDirectory || fileSize >= this.FILE_CONFIG.LARGE_FILE_THRESHOLD
);
}
}
@@ -80,7 +80,7 @@ export class FileTransferOrchestrator implements MessageHandlerDelegate {
* 🎯 Send string content
*/
public async sendString(content: string, peerId: string): Promise<void> {
const chunkSize = TransferConfig.FILE_CONFIG.CHUNK_SIZE;
const chunkSize = 65000;
const chunks: string[] = [];
for (let i = 0; i < content.length; i += chunkSize) {
+4 -1
View File
@@ -18,10 +18,13 @@ class WebRTCService {
private static instance: WebRTCService;
private constructor() {
const apiUrl = (config.API_URL || "").trim();
// Use same-origin when API_URL is empty string — socket.io accepts empty string for same-origin
const signalingServer: string = apiUrl.length > 0 ? apiUrl : "";
const webRTCConfig = {
iceServers: getIceServers(),
socketOptions: getSocketOptions() || {},
signalingServer: config.API_URL,
signalingServer,
};
this.sender = new WebRTC_Initiator(webRTCConfig);
+8 -1
View File
@@ -12,6 +12,8 @@ const withMDX = createMDX({
const nextConfig = {
pageExtensions: ['ts', 'tsx', 'js', 'jsx', 'md', 'mdx'],
images: {
// Disable optimization inside Docker to avoid container loopback fetch failures (502)
unoptimized: process.env.NEXT_IMAGE_UNOPTIMIZED === 'true',
remotePatterns: [
{
protocol: 'https',
@@ -20,7 +22,12 @@ const nextConfig = {
},
]
},
// 启用standalone输出模式,用于Docker部署
output: 'standalone',
// 禁用telemetry
experimental: {
instrumentationHook: true,
},
}
export default withMDX(nextConfig);
+10 -10
View File
@@ -5,7 +5,7 @@
"scripts": {
"dev": "next dev -H 0.0.0.0 -p 3002",
"build": "next build",
"start": "next start -p 3000",
"start": "next start -p 3002",
"lint": "next lint"
},
"dependencies": {
@@ -24,8 +24,8 @@
"@types/hast": "^3.0.4",
"@types/mdast": "^4.0.4",
"@types/negotiator": "^0.6.3",
"@types/node": "^20",
"@types/react-dom": "^18",
"@types/node": "^20.14.13",
"@types/react-dom": "^18.3.0",
"@types/unist": "^3.0.3",
"class-variance-authority": "^0.7.0",
"clsx": "^2.1.1",
@@ -39,8 +39,8 @@
"next-mdx-remote": "^5.0.0",
"next-themes": "^0.3.0",
"qrcode.react": "^4.0.1",
"react": "^18",
"react-dom": "^18",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-intersection-observer": "^9.16.0",
"remark-gfm": "^4.0.0",
"sharp": "^0.33.5",
@@ -52,11 +52,11 @@
"zustand": "^5.0.7"
},
"devDependencies": {
"@types/react": "^18.3.18",
"eslint": "^8",
"@types/react": "^18.3.22",
"eslint": "^8.57.0",
"eslint-config-next": "14.2.5",
"postcss": "^8",
"tailwindcss": "^3.4.1",
"typescript": "^5"
"postcss": "^8.4.40",
"tailwindcss": "^3.4.7",
"typescript": "^5.5.4"
}
}
Generated Executable → Regular
+13 -13
View File
@@ -54,10 +54,10 @@ importers:
specifier: ^0.6.3
version: 0.6.3
'@types/node':
specifier: ^20
specifier: ^20.14.13
version: 20.14.13
'@types/react-dom':
specifier: ^18
specifier: ^18.3.0
version: 18.3.0
'@types/unist':
specifier: ^3.0.3
@@ -99,10 +99,10 @@ importers:
specifier: ^4.0.1
version: 4.0.1(react@18.3.1)
react:
specifier: ^18
specifier: ^18.3.1
version: 18.3.1
react-dom:
specifier: ^18
specifier: ^18.3.1
version: 18.3.1(react@18.3.1)
react-intersection-observer:
specifier: ^9.16.0
@@ -133,22 +133,22 @@ importers:
version: 5.0.7(@types/react@18.3.22)(react@18.3.1)
devDependencies:
'@types/react':
specifier: ^18.3.18
specifier: ^18.3.22
version: 18.3.22
eslint:
specifier: ^8
specifier: ^8.57.0
version: 8.57.0
eslint-config-next:
specifier: 14.2.5
version: 14.2.5(eslint@8.57.0)(typescript@5.5.4)
postcss:
specifier: ^8
specifier: ^8.4.40
version: 8.4.40
tailwindcss:
specifier: ^3.4.1
specifier: ^3.4.7
version: 3.4.7
typescript:
specifier: ^5
specifier: ^5.5.4
version: 5.5.4
packages:
@@ -1367,8 +1367,8 @@ packages:
resolution: {integrity: sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==}
engines: {node: '>= 6'}
caniuse-lite@1.0.30001735:
resolution: {integrity: sha512-EV/laoX7Wq2J9TQlyIXRxTJqIw4sxfXS4OYgudGxBYRuTv0q7AM6yMEpU/Vo1I94thg9U6EZ2NfZx9GJq83u7w==}
caniuse-lite@1.0.30001745:
resolution: {integrity: sha512-ywt6i8FzvdgrrrGbr1jZVObnVv6adj+0if2/omv9cmR2oiZs30zL4DIyaptKcbOrBdOIc74QTMoJvSE2QHh5UQ==}
ccount@2.0.1:
resolution: {integrity: sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==}
@@ -4730,7 +4730,7 @@ snapshots:
camelcase-css@2.0.1: {}
caniuse-lite@1.0.30001735: {}
caniuse-lite@1.0.30001745: {}
ccount@2.0.1: {}
@@ -6552,7 +6552,7 @@ snapshots:
'@next/env': 14.2.5
'@swc/helpers': 0.5.5
busboy: 1.6.0
caniuse-lite: 1.0.30001735
caniuse-lite: 1.0.30001745
graceful-fs: 4.2.11
postcss: 8.4.31
react: 18.3.1
+218
View File
@@ -0,0 +1,218 @@
#!/bin/bash
# Color definitions
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m' # No Color
# Test result counters
TESTS_PASSED=0
TESTS_FAILED=0
TOTAL_TESTS=0
# Logging helpers
log_info() {
echo -e "${BLUE}$1${NC}"
}
log_success() {
echo -e "${GREEN}$1${NC}"
TESTS_PASSED=$((TESTS_PASSED + 1))
}
log_error() {
echo -e "${RED}$1${NC}"
TESTS_FAILED=$((TESTS_FAILED + 1))
}
log_warning() {
echo -e "${YELLOW}⚠️ $1${NC}"
}
# Test functions
test_api() {
local url="$1"
local description="$2"
local expected_status="${3:-200}"
TOTAL_TESTS=$((TOTAL_TESTS + 1))
echo ""
log_info "Test: $description"
log_info "URL: $url"
# Send request and capture response
response=$(curl -s -w "\n%{http_code}" "$url" 2>/dev/null)
if [ $? -ne 0 ]; then
log_error "Request failed - unable to connect to service"
return 1
fi
# Split response body and status code
http_code=$(echo "$response" | tail -n1)
response_body=$(echo "$response" | head -n -1)
# Check HTTP status code
if [ "$http_code" -eq "$expected_status" ]; then
log_success "HTTP status code OK: $http_code"
else
log_error "HTTP status code mismatch: expected $expected_status, got $http_code"
return 1
fi
# Validate JSON format
if echo "$response_body" | jq . >/dev/null 2>&1; then
log_success "Response is valid JSON"
# Pretty-print JSON response
echo -e "${BLUE}Response body:${NC}"
echo "$response_body" | jq .
# Verify required fields
status=$(echo "$response_body" | jq -r '.status // empty')
service=$(echo "$response_body" | jq -r '.service // empty')
timestamp=$(echo "$response_body" | jq -r '.timestamp // empty')
if [ -n "$status" ] && [ -n "$service" ] && [ -n "$timestamp" ]; then
log_success "Contains required fields: status, service, timestamp"
else
log_error "Missing required fields"
return 1
fi
else
log_error "Response is not valid JSON"
echo "Response body: $response_body"
return 1
fi
return 0
}
# Check if service is running
check_service() {
local port="$1"
local service_name="$2"
if nc -z localhost "$port" 2>/dev/null; then
log_success "$service_name is running (port $port)"
return 0
else
log_error "$service_name is not running (port $port)"
return 1
fi
}
# Wait for service to start
wait_for_service() {
local port="$1"
local service_name="$2"
local max_attempts=30
local attempt=0
log_info "Waiting for $service_name to start..."
while [ $attempt -lt $max_attempts ]; do
if nc -z localhost "$port" 2>/dev/null; then
log_success "$service_name started"
return 0
fi
attempt=$((attempt + 1))
echo -n "."
sleep 2
done
log_error "$service_name startup timed out"
return 1
}
# Main test function
main() {
echo -e "${BLUE}=== PrivyDrop Health Check API Tests ===${NC}"
echo ""
# Check required tools
if ! command -v curl &> /dev/null; then
log_error "curl is not installed; please install curl"
exit 1
fi
if ! command -v jq &> /dev/null; then
log_error "jq is not installed; please install jq for JSON parsing"
exit 1
fi
if ! command -v nc &> /dev/null; then
log_error "netcat is not installed; please install nc for port checks"
exit 1
fi
# Check service status
echo -e "${BLUE}=== Check Service Status ===${NC}"
backend_running=false
frontend_running=false
if check_service 3001 "Backend"; then
backend_running=true
fi
if check_service 3002 "Frontend"; then
frontend_running=true
fi
# Show startup hints if services are not running
if [ "$backend_running" = false ]; then
echo ""
log_warning "Backend is not running; please start it:"
echo " cd backend && npm run dev"
echo ""
fi
if [ "$frontend_running" = false ]; then
echo ""
log_warning "Frontend is not running; please start it:"
echo " cd frontend && pnpm dev"
echo ""
fi
# Test backend health check APIs
if [ "$backend_running" = true ]; then
echo -e "${BLUE}=== Test Backend Health Check APIs ===${NC}"
test_api "http://localhost:3001/health" "Backend basic health check"
test_api "http://localhost:3001/api/health" "Backend API path health check"
test_api "http://localhost:3001/health/detailed" "Backend detailed health check"
fi
# Test frontend health check APIs
if [ "$frontend_running" = true ]; then
echo -e "${BLUE}=== Test Frontend Health Check APIs ===${NC}"
test_api "http://localhost:3002/api/health" "Frontend basic health check"
test_api "http://localhost:3002/api/health/detailed" "Frontend detailed health check"
fi
# Test results summary
echo ""
echo -e "${BLUE}=== Test Results Summary ===${NC}"
echo "Total tests: $TOTAL_TESTS"
echo -e "Passed: ${GREEN}$TESTS_PASSED${NC}"
echo -e "Failed: ${RED}$TESTS_FAILED${NC}"
if [ $TESTS_FAILED -eq 0 ]; then
echo -e "${GREEN}🎉 All tests passed!${NC}"
exit 0
else
echo -e "${RED}$TESTS_FAILED test(s) failed${NC}"
exit 1
fi
}
# Trap interrupt signals
trap 'echo -e "\n${YELLOW}Tests interrupted${NC}"; exit 1' INT TERM
# Run main function
main "$@"