"backend/internal/git@web.lueluesay.top:chenxi/sub2api.git" did not exist on "ecfad788d938832c3eacc38f5e9e6e1444c2eb50"
Commit 0170d19f authored by song's avatar song
Browse files

merge upstream main

parent 7ade9baa
...@@ -3,6 +3,7 @@ package setup ...@@ -3,6 +3,7 @@ package setup
import ( import (
"context" "context"
"crypto/rand" "crypto/rand"
"crypto/tls"
"database/sql" "database/sql"
"encoding/hex" "encoding/hex"
"fmt" "fmt"
...@@ -79,10 +80,11 @@ type DatabaseConfig struct { ...@@ -79,10 +80,11 @@ type DatabaseConfig struct {
} }
type RedisConfig struct { type RedisConfig struct {
Host string `json:"host" yaml:"host"` Host string `json:"host" yaml:"host"`
Port int `json:"port" yaml:"port"` Port int `json:"port" yaml:"port"`
Password string `json:"password" yaml:"password"` Password string `json:"password" yaml:"password"`
DB int `json:"db" yaml:"db"` DB int `json:"db" yaml:"db"`
EnableTLS bool `json:"enable_tls" yaml:"enable_tls"`
} }
type AdminConfig struct { type AdminConfig struct {
...@@ -199,11 +201,20 @@ func TestDatabaseConnection(cfg *DatabaseConfig) error { ...@@ -199,11 +201,20 @@ func TestDatabaseConnection(cfg *DatabaseConfig) error {
// TestRedisConnection tests the Redis connection // TestRedisConnection tests the Redis connection
func TestRedisConnection(cfg *RedisConfig) error { func TestRedisConnection(cfg *RedisConfig) error {
rdb := redis.NewClient(&redis.Options{ opts := &redis.Options{
Addr: fmt.Sprintf("%s:%d", cfg.Host, cfg.Port), Addr: fmt.Sprintf("%s:%d", cfg.Host, cfg.Port),
Password: cfg.Password, Password: cfg.Password,
DB: cfg.DB, DB: cfg.DB,
}) }
if cfg.EnableTLS {
opts.TLSConfig = &tls.Config{
MinVersion: tls.VersionTLS12,
ServerName: cfg.Host,
}
}
rdb := redis.NewClient(opts)
defer func() { defer func() {
if err := rdb.Close(); err != nil { if err := rdb.Close(); err != nil {
log.Printf("failed to close redis client: %v", err) log.Printf("failed to close redis client: %v", err)
...@@ -485,10 +496,11 @@ func AutoSetupFromEnv() error { ...@@ -485,10 +496,11 @@ func AutoSetupFromEnv() error {
SSLMode: getEnvOrDefault("DATABASE_SSLMODE", "disable"), SSLMode: getEnvOrDefault("DATABASE_SSLMODE", "disable"),
}, },
Redis: RedisConfig{ Redis: RedisConfig{
Host: getEnvOrDefault("REDIS_HOST", "localhost"), Host: getEnvOrDefault("REDIS_HOST", "localhost"),
Port: getEnvIntOrDefault("REDIS_PORT", 6379), Port: getEnvIntOrDefault("REDIS_PORT", 6379),
Password: getEnvOrDefault("REDIS_PASSWORD", ""), Password: getEnvOrDefault("REDIS_PASSWORD", ""),
DB: getEnvIntOrDefault("REDIS_DB", 0), DB: getEnvIntOrDefault("REDIS_DB", 0),
EnableTLS: getEnvOrDefault("REDIS_ENABLE_TLS", "false") == "true",
}, },
Admin: AdminConfig{ Admin: AdminConfig{
Email: getEnvOrDefault("ADMIN_EMAIL", "admin@sub2api.local"), Email: getEnvOrDefault("ADMIN_EMAIL", "admin@sub2api.local"),
......
...@@ -46,7 +46,7 @@ func ValidateURLFormat(raw string, allowInsecureHTTP bool) (string, error) { ...@@ -46,7 +46,7 @@ func ValidateURLFormat(raw string, allowInsecureHTTP bool) (string, error) {
} }
} }
return trimmed, nil return strings.TrimRight(trimmed, "/"), nil
} }
func ValidateHTTPSURL(raw string, opts ValidationOptions) (string, error) { func ValidateHTTPSURL(raw string, opts ValidationOptions) (string, error) {
......
...@@ -21,4 +21,31 @@ func TestValidateURLFormat(t *testing.T) { ...@@ -21,4 +21,31 @@ func TestValidateURLFormat(t *testing.T) {
if _, err := ValidateURLFormat("https://example.com:bad", true); err == nil { if _, err := ValidateURLFormat("https://example.com:bad", true); err == nil {
t.Fatalf("expected invalid port to fail") t.Fatalf("expected invalid port to fail")
} }
// 验证末尾斜杠被移除
normalized, err := ValidateURLFormat("https://example.com/", false)
if err != nil {
t.Fatalf("expected trailing slash url to pass, got %v", err)
}
if normalized != "https://example.com" {
t.Fatalf("expected trailing slash to be removed, got %s", normalized)
}
// 验证多个末尾斜杠被移除
normalized, err = ValidateURLFormat("https://example.com///", false)
if err != nil {
t.Fatalf("expected multiple trailing slashes to pass, got %v", err)
}
if normalized != "https://example.com" {
t.Fatalf("expected all trailing slashes to be removed, got %s", normalized)
}
// 验证带路径的 URL 末尾斜杠被移除
normalized, err = ValidateURLFormat("https://example.com/api/v1/", false)
if err != nil {
t.Fatalf("expected trailing slash url with path to pass, got %v", err)
}
if normalized != "https://example.com/api/v1" {
t.Fatalf("expected trailing slash to be removed from path, got %s", normalized)
}
} }
-- 兼容旧库:若尚未创建 user_allowed_groups,则确保 users.allowed_groups 存在,避免 007 迁移回填失败。
DO $$
BEGIN
IF to_regclass('public.user_allowed_groups') IS NULL THEN
IF EXISTS (
SELECT 1
FROM information_schema.tables
WHERE table_schema = 'public'
AND table_name = 'users'
) THEN
ALTER TABLE users
ADD COLUMN IF NOT EXISTS allowed_groups BIGINT[] DEFAULT NULL;
END IF;
END IF;
END $$;
-- 兼容缺失 users.allowed_groups 的老库,确保 007 回填可执行。
DO $$
BEGIN
IF EXISTS (
SELECT 1
FROM information_schema.tables
WHERE table_schema = 'public'
AND table_name = 'users'
) THEN
IF NOT EXISTS (
SELECT 1
FROM information_schema.columns
WHERE table_schema = 'public'
AND table_name = 'users'
AND column_name = 'allowed_groups'
) THEN
IF NOT EXISTS (
SELECT 1
FROM schema_migrations
WHERE filename = '014_drop_legacy_allowed_groups.sql'
) THEN
ALTER TABLE users
ADD COLUMN IF NOT EXISTS allowed_groups BIGINT[] DEFAULT NULL;
END IF;
END IF;
END IF;
END $$;
-- 042_add_usage_cleanup_tasks.sql
-- 使用记录清理任务表
CREATE TABLE IF NOT EXISTS usage_cleanup_tasks (
id BIGSERIAL PRIMARY KEY,
status VARCHAR(20) NOT NULL,
filters JSONB NOT NULL,
created_by BIGINT NOT NULL REFERENCES users(id) ON DELETE RESTRICT,
deleted_rows BIGINT NOT NULL DEFAULT 0,
error_message TEXT,
started_at TIMESTAMPTZ,
finished_at TIMESTAMPTZ,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX IF NOT EXISTS idx_usage_cleanup_tasks_status_created_at
ON usage_cleanup_tasks(status, created_at DESC);
CREATE INDEX IF NOT EXISTS idx_usage_cleanup_tasks_created_at
ON usage_cleanup_tasks(created_at DESC);
-- 043_add_usage_cleanup_cancel_audit.sql
-- usage_cleanup_tasks 取消任务审计字段
ALTER TABLE usage_cleanup_tasks
ADD COLUMN IF NOT EXISTS canceled_by BIGINT REFERENCES users(id) ON DELETE SET NULL,
ADD COLUMN IF NOT EXISTS canceled_at TIMESTAMPTZ;
CREATE INDEX IF NOT EXISTS idx_usage_cleanup_tasks_canceled_at
ON usage_cleanup_tasks(canceled_at DESC);
-- 043_add_group_invalid_request_fallback.sql -- 043b_add_group_invalid_request_fallback.sql
-- 添加无效请求兜底分组配置 -- 添加无效请求兜底分组配置
-- 添加 fallback_group_id_on_invalid_request 字段:无效请求兜底使用的分组 -- 添加 fallback_group_id_on_invalid_request 字段:无效请求兜底使用的分组
......
-- 为 users 表添加 TOTP 双因素认证字段
ALTER TABLE users
ADD COLUMN IF NOT EXISTS totp_secret_encrypted TEXT DEFAULT NULL,
ADD COLUMN IF NOT EXISTS totp_enabled BOOLEAN NOT NULL DEFAULT FALSE,
ADD COLUMN IF NOT EXISTS totp_enabled_at TIMESTAMPTZ DEFAULT NULL;
COMMENT ON COLUMN users.totp_secret_encrypted IS 'AES-256-GCM 加密的 TOTP 密钥';
COMMENT ON COLUMN users.totp_enabled IS '是否启用 TOTP 双因素认证';
COMMENT ON COLUMN users.totp_enabled_at IS 'TOTP 启用时间';
-- 创建索引以支持快速查询启用 2FA 的用户
CREATE INDEX IF NOT EXISTS idx_users_totp_enabled ON users(totp_enabled) WHERE deleted_at IS NULL AND totp_enabled = true;
-- 创建公告表
CREATE TABLE IF NOT EXISTS announcements (
id BIGSERIAL PRIMARY KEY,
title VARCHAR(200) NOT NULL,
content TEXT NOT NULL,
status VARCHAR(20) NOT NULL DEFAULT 'draft',
targeting JSONB NOT NULL DEFAULT '{}'::jsonb,
starts_at TIMESTAMPTZ DEFAULT NULL,
ends_at TIMESTAMPTZ DEFAULT NULL,
created_by BIGINT DEFAULT NULL REFERENCES users(id) ON DELETE SET NULL,
updated_by BIGINT DEFAULT NULL REFERENCES users(id) ON DELETE SET NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
-- 公告已读表
CREATE TABLE IF NOT EXISTS announcement_reads (
id BIGSERIAL PRIMARY KEY,
announcement_id BIGINT NOT NULL REFERENCES announcements(id) ON DELETE CASCADE,
user_id BIGINT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
read_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
UNIQUE(announcement_id, user_id)
);
-- 索引
CREATE INDEX IF NOT EXISTS idx_announcements_status ON announcements(status);
CREATE INDEX IF NOT EXISTS idx_announcements_starts_at ON announcements(starts_at);
CREATE INDEX IF NOT EXISTS idx_announcements_ends_at ON announcements(ends_at);
CREATE INDEX IF NOT EXISTS idx_announcements_created_at ON announcements(created_at);
CREATE INDEX IF NOT EXISTS idx_announcement_reads_announcement_id ON announcement_reads(announcement_id);
CREATE INDEX IF NOT EXISTS idx_announcement_reads_user_id ON announcement_reads(user_id);
CREATE INDEX IF NOT EXISTS idx_announcement_reads_read_at ON announcement_reads(read_at);
COMMENT ON TABLE announcements IS '系统公告';
COMMENT ON COLUMN announcements.status IS '状态: draft, active, archived';
COMMENT ON COLUMN announcements.targeting IS '展示条件(JSON 规则)';
COMMENT ON COLUMN announcements.starts_at IS '开始展示时间(为空表示立即生效)';
COMMENT ON COLUMN announcements.ends_at IS '结束展示时间(为空表示永久生效)';
COMMENT ON TABLE announcement_reads IS '公告已读记录';
COMMENT ON COLUMN announcement_reads.read_at IS '用户首次已读时间';
...@@ -251,6 +251,27 @@ dashboard_aggregation: ...@@ -251,6 +251,27 @@ dashboard_aggregation:
# 日聚合保留天数 # 日聚合保留天数
daily_days: 730 daily_days: 730
# =============================================================================
# Usage Cleanup Task Configuration
# 使用记录清理任务配置(重启生效)
# =============================================================================
usage_cleanup:
# Enable cleanup task worker
# 启用清理任务执行器
enabled: true
# Max date range (days) per task
# 单次任务最大时间跨度(天)
max_range_days: 31
# Batch delete size
# 单批删除数量
batch_size: 5000
# Worker interval (seconds)
# 执行器轮询间隔(秒)
worker_interval_seconds: 10
# Task execution timeout (seconds)
# 单次任务最大执行时长(秒)
task_timeout_seconds: 1800
# ============================================================================= # =============================================================================
# Concurrency Wait Configuration # Concurrency Wait Configuration
# 并发等待配置 # 并发等待配置
...@@ -301,6 +322,9 @@ redis: ...@@ -301,6 +322,9 @@ redis:
# Database number (0-15) # Database number (0-15)
# 数据库编号(0-15) # 数据库编号(0-15)
db: 0 db: 0
# Enable TLS/SSL connection
# 是否启用 TLS/SSL 连接
enable_tls: false
# ============================================================================= # =============================================================================
# Ops Monitoring (Optional) # Ops Monitoring (Optional)
......
...@@ -40,6 +40,7 @@ POSTGRES_DB=sub2api ...@@ -40,6 +40,7 @@ POSTGRES_DB=sub2api
# Leave empty for no password (default for local development) # Leave empty for no password (default for local development)
REDIS_PASSWORD= REDIS_PASSWORD=
REDIS_DB=0 REDIS_DB=0
REDIS_ENABLE_TLS=false
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
# Admin Account # Admin Account
...@@ -61,6 +62,18 @@ ADMIN_PASSWORD= ...@@ -61,6 +62,18 @@ ADMIN_PASSWORD=
JWT_SECRET= JWT_SECRET=
JWT_EXPIRE_HOUR=24 JWT_EXPIRE_HOUR=24
# -----------------------------------------------------------------------------
# TOTP (2FA) Configuration
# TOTP(双因素认证)配置
# -----------------------------------------------------------------------------
# IMPORTANT: Set a fixed encryption key for TOTP secrets. If left empty, a
# random key will be generated on each startup, causing all existing TOTP
# configurations to become invalid (users won't be able to login with 2FA).
# Generate a secure key: openssl rand -hex 32
# 重要:设置固定的 TOTP 加密密钥。如果留空,每次启动将生成随机密钥,
# 导致现有的 TOTP 配置失效(用户无法使用双因素认证登录)。
TOTP_ENCRYPTION_KEY=
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
# Configuration File (Optional) # Configuration File (Optional)
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
......
# =============================================================================
# Sub2API Deploy Directory - Git Ignore
# =============================================================================
# Data directories (generated at runtime when using docker-compose.local.yml)
data/
postgres_data/
redis_data/
# Environment configuration (contains sensitive information)
.env
# Backup files
*.backup
*.bak
# Temporary files
*.tmp
*.log
...@@ -13,7 +13,9 @@ This directory contains files for deploying Sub2API on Linux servers. ...@@ -13,7 +13,9 @@ This directory contains files for deploying Sub2API on Linux servers.
| File | Description | | File | Description |
|------|-------------| |------|-------------|
| `docker-compose.yml` | Docker Compose configuration | | `docker-compose.yml` | Docker Compose configuration (named volumes) |
| `docker-compose.local.yml` | Docker Compose configuration (local directories, easy migration) |
| `docker-deploy.sh` | **One-click Docker deployment script (recommended)** |
| `.env.example` | Docker environment variables template | | `.env.example` | Docker environment variables template |
| `DOCKER.md` | Docker Hub documentation | | `DOCKER.md` | Docker Hub documentation |
| `install.sh` | One-click binary installation script | | `install.sh` | One-click binary installation script |
...@@ -24,7 +26,45 @@ This directory contains files for deploying Sub2API on Linux servers. ...@@ -24,7 +26,45 @@ This directory contains files for deploying Sub2API on Linux servers.
## Docker Deployment (Recommended) ## Docker Deployment (Recommended)
### Quick Start ### Method 1: One-Click Deployment (Recommended)
Use the automated preparation script for the easiest setup:
```bash
# Download and run the preparation script
curl -sSL https://raw.githubusercontent.com/Wei-Shaw/sub2api/main/deploy/docker-deploy.sh | bash
# Or download first, then run
curl -sSL https://raw.githubusercontent.com/Wei-Shaw/sub2api/main/deploy/docker-deploy.sh -o docker-deploy.sh
chmod +x docker-deploy.sh
./docker-deploy.sh
```
**What the script does:**
- Downloads `docker-compose.local.yml` and `.env.example`
- Automatically generates secure secrets (JWT_SECRET, TOTP_ENCRYPTION_KEY, POSTGRES_PASSWORD)
- Creates `.env` file with generated secrets
- Creates necessary data directories (data/, postgres_data/, redis_data/)
- **Displays generated credentials** (POSTGRES_PASSWORD, JWT_SECRET, etc.)
**After running the script:**
```bash
# Start services
docker-compose -f docker-compose.local.yml up -d
# View logs
docker-compose -f docker-compose.local.yml logs -f sub2api
# If admin password was auto-generated, find it in logs:
docker-compose -f docker-compose.local.yml logs sub2api | grep "admin password"
# Access Web UI
# http://localhost:8080
```
### Method 2: Manual Deployment
If you prefer manual control:
```bash ```bash
# Clone repository # Clone repository
...@@ -33,18 +73,36 @@ cd sub2api/deploy ...@@ -33,18 +73,36 @@ cd sub2api/deploy
# Configure environment # Configure environment
cp .env.example .env cp .env.example .env
nano .env # Set POSTGRES_PASSWORD (required) nano .env # Set POSTGRES_PASSWORD and other required variables
# Start all services # Generate secure secrets (recommended)
docker-compose up -d JWT_SECRET=$(openssl rand -hex 32)
TOTP_ENCRYPTION_KEY=$(openssl rand -hex 32)
echo "JWT_SECRET=${JWT_SECRET}" >> .env
echo "TOTP_ENCRYPTION_KEY=${TOTP_ENCRYPTION_KEY}" >> .env
# Create data directories
mkdir -p data postgres_data redis_data
# Start all services using local directory version
docker-compose -f docker-compose.local.yml up -d
# View logs (check for auto-generated admin password) # View logs (check for auto-generated admin password)
docker-compose logs -f sub2api docker-compose -f docker-compose.local.yml logs -f sub2api
# Access Web UI # Access Web UI
# http://localhost:8080 # http://localhost:8080
``` ```
### Deployment Version Comparison
| Version | Data Storage | Migration | Best For |
|---------|-------------|-----------|----------|
| **docker-compose.local.yml** | Local directories (./data, ./postgres_data, ./redis_data) | ✅ Easy (tar entire directory) | Production, need frequent backups/migration |
| **docker-compose.yml** | Named volumes (/var/lib/docker/volumes/) | ⚠️ Requires docker commands | Simple setup, don't need migration |
**Recommendation:** Use `docker-compose.local.yml` (deployed by `docker-deploy.sh`) for easier data management and migration.
### How Auto-Setup Works ### How Auto-Setup Works
When using Docker Compose with `AUTO_SETUP=true`: When using Docker Compose with `AUTO_SETUP=true`:
...@@ -89,6 +147,32 @@ SELECT ...@@ -89,6 +147,32 @@ SELECT
### Commands ### Commands
For **local directory version** (docker-compose.local.yml):
```bash
# Start services
docker-compose -f docker-compose.local.yml up -d
# Stop services
docker-compose -f docker-compose.local.yml down
# View logs
docker-compose -f docker-compose.local.yml logs -f sub2api
# Restart Sub2API only
docker-compose -f docker-compose.local.yml restart sub2api
# Update to latest version
docker-compose -f docker-compose.local.yml pull
docker-compose -f docker-compose.local.yml up -d
# Remove all data (caution!)
docker-compose -f docker-compose.local.yml down
rm -rf data/ postgres_data/ redis_data/
```
For **named volumes version** (docker-compose.yml):
```bash ```bash
# Start services # Start services
docker-compose up -d docker-compose up -d
...@@ -115,10 +199,11 @@ docker-compose down -v ...@@ -115,10 +199,11 @@ docker-compose down -v
| Variable | Required | Default | Description | | Variable | Required | Default | Description |
|----------|----------|---------|-------------| |----------|----------|---------|-------------|
| `POSTGRES_PASSWORD` | **Yes** | - | PostgreSQL password | | `POSTGRES_PASSWORD` | **Yes** | - | PostgreSQL password |
| `JWT_SECRET` | **Recommended** | *(auto-generated)* | JWT secret (fixed for persistent sessions) |
| `TOTP_ENCRYPTION_KEY` | **Recommended** | *(auto-generated)* | TOTP encryption key (fixed for persistent 2FA) |
| `SERVER_PORT` | No | `8080` | Server port | | `SERVER_PORT` | No | `8080` | Server port |
| `ADMIN_EMAIL` | No | `admin@sub2api.local` | Admin email | | `ADMIN_EMAIL` | No | `admin@sub2api.local` | Admin email |
| `ADMIN_PASSWORD` | No | *(auto-generated)* | Admin password | | `ADMIN_PASSWORD` | No | *(auto-generated)* | Admin password |
| `JWT_SECRET` | No | *(auto-generated)* | JWT secret |
| `TZ` | No | `Asia/Shanghai` | Timezone | | `TZ` | No | `Asia/Shanghai` | Timezone |
| `GEMINI_OAUTH_CLIENT_ID` | No | *(builtin)* | Google OAuth client ID (Gemini OAuth). Leave empty to use the built-in Gemini CLI client. | | `GEMINI_OAUTH_CLIENT_ID` | No | *(builtin)* | Google OAuth client ID (Gemini OAuth). Leave empty to use the built-in Gemini CLI client. |
| `GEMINI_OAUTH_CLIENT_SECRET` | No | *(builtin)* | Google OAuth client secret (Gemini OAuth). Leave empty to use the built-in Gemini CLI client. | | `GEMINI_OAUTH_CLIENT_SECRET` | No | *(builtin)* | Google OAuth client secret (Gemini OAuth). Leave empty to use the built-in Gemini CLI client. |
...@@ -127,6 +212,30 @@ docker-compose down -v ...@@ -127,6 +212,30 @@ docker-compose down -v
See `.env.example` for all available options. See `.env.example` for all available options.
> **Note:** The `docker-deploy.sh` script automatically generates `JWT_SECRET`, `TOTP_ENCRYPTION_KEY`, and `POSTGRES_PASSWORD` for you.
### Easy Migration (Local Directory Version)
When using `docker-compose.local.yml`, all data is stored in local directories, making migration simple:
```bash
# On source server: Stop services and create archive
cd /path/to/deployment
docker-compose -f docker-compose.local.yml down
cd ..
tar czf sub2api-complete.tar.gz deployment/
# Transfer to new server
scp sub2api-complete.tar.gz user@new-server:/path/to/destination/
# On new server: Extract and start
tar xzf sub2api-complete.tar.gz
cd deployment/
docker-compose -f docker-compose.local.yml up -d
```
Your entire deployment (configuration + data) is migrated!
--- ---
## Gemini OAuth Configuration ## Gemini OAuth Configuration
...@@ -359,6 +468,30 @@ The main config file is at `/etc/sub2api/config.yaml` (created by Setup Wizard). ...@@ -359,6 +468,30 @@ The main config file is at `/etc/sub2api/config.yaml` (created by Setup Wizard).
### Docker ### Docker
For **local directory version**:
```bash
# Check container status
docker-compose -f docker-compose.local.yml ps
# View detailed logs
docker-compose -f docker-compose.local.yml logs --tail=100 sub2api
# Check database connection
docker-compose -f docker-compose.local.yml exec postgres pg_isready
# Check Redis connection
docker-compose -f docker-compose.local.yml exec redis redis-cli ping
# Restart all services
docker-compose -f docker-compose.local.yml restart
# Check data directories
ls -la data/ postgres_data/ redis_data/
```
For **named volumes version**:
```bash ```bash
# Check container status # Check container status
docker-compose ps docker-compose ps
...@@ -401,3 +534,60 @@ sudo systemctl status redis ...@@ -401,3 +534,60 @@ sudo systemctl status redis
2. **Database connection failed**: Check PostgreSQL is running and credentials are correct 2. **Database connection failed**: Check PostgreSQL is running and credentials are correct
3. **Redis connection failed**: Check Redis is running and password is correct 3. **Redis connection failed**: Check Redis is running and password is correct
4. **Permission denied**: Ensure proper file ownership for binary install 4. **Permission denied**: Ensure proper file ownership for binary install
---
## TLS Fingerprint Configuration
Sub2API supports TLS fingerprint simulation to make requests appear as if they come from the official Claude CLI (Node.js client).
> **💡 Tip:** Visit **[tls.sub2api.org](https://tls.sub2api.org/)** to get TLS fingerprint information for different devices and browsers.
### Default Behavior
- Built-in `claude_cli_v2` profile simulates Node.js 20.x + OpenSSL 3.x
- JA3 Hash: `1a28e69016765d92e3b381168d68922c`
- JA4: `t13d5911h1_a33745022dd6_1f22a2ca17c4`
- Profile selection: `accountID % profileCount`
### Configuration
```yaml
gateway:
tls_fingerprint:
enabled: true # Global switch
profiles:
# Simple profile (uses default cipher suites)
profile_1:
name: "Profile 1"
# Profile with custom cipher suites (use compact array format)
profile_2:
name: "Profile 2"
cipher_suites: [4866, 4867, 4865, 49199, 49195, 49200, 49196]
curves: [29, 23, 24]
point_formats: [0]
# Another custom profile
profile_3:
name: "Profile 3"
cipher_suites: [4865, 4866, 4867, 49199, 49200]
curves: [29, 23, 24, 25]
```
### Profile Fields
| Field | Type | Description |
|-------|------|-------------|
| `name` | string | Display name (required) |
| `cipher_suites` | []uint16 | Cipher suites in decimal. Empty = default |
| `curves` | []uint16 | Elliptic curves in decimal. Empty = default |
| `point_formats` | []uint8 | EC point formats. Empty = default |
### Common Values Reference
**Cipher Suites (TLS 1.3):** `4865` (AES_128_GCM), `4866` (AES_256_GCM), `4867` (CHACHA20)
**Cipher Suites (TLS 1.2):** `49195`, `49196`, `49199`, `49200` (ECDHE variants)
**Curves:** `29` (X25519), `23` (P-256), `24` (P-384), `25` (P-521)
...@@ -210,6 +210,19 @@ gateway: ...@@ -210,6 +210,19 @@ gateway:
outbox_backlog_rebuild_rows: 10000 outbox_backlog_rebuild_rows: 10000
# 全量重建周期(秒),0 表示禁用 # 全量重建周期(秒),0 表示禁用
full_rebuild_interval_seconds: 300 full_rebuild_interval_seconds: 300
# TLS fingerprint simulation / TLS 指纹伪装
# Default profile "claude_cli_v2" simulates Node.js 20.x
# 默认模板 "claude_cli_v2" 模拟 Node.js 20.x 指纹
tls_fingerprint:
enabled: true
# profiles:
# profile_1:
# name: "Custom Profile 1"
# profile_2:
# name: "Custom Profile 2"
# cipher_suites: [4866, 4867, 4865, 49199, 49195, 49200, 49196]
# curves: [29, 23, 24]
# point_formats: [0]
# ============================================================================= # =============================================================================
# API Key Auth Cache Configuration # API Key Auth Cache Configuration
...@@ -292,6 +305,27 @@ dashboard_aggregation: ...@@ -292,6 +305,27 @@ dashboard_aggregation:
# 日聚合保留天数 # 日聚合保留天数
daily_days: 730 daily_days: 730
# =============================================================================
# Usage Cleanup Task Configuration
# 使用记录清理任务配置(重启生效)
# =============================================================================
usage_cleanup:
# Enable cleanup task worker
# 启用清理任务执行器
enabled: true
# Max date range (days) per task
# 单次任务最大时间跨度(天)
max_range_days: 31
# Batch delete size
# 单批删除数量
batch_size: 5000
# Worker interval (seconds)
# 执行器轮询间隔(秒)
worker_interval_seconds: 10
# Task execution timeout (seconds)
# 单次任务最大执行时长(秒)
task_timeout_seconds: 1800
# ============================================================================= # =============================================================================
# Concurrency Wait Configuration # Concurrency Wait Configuration
# 并发等待配置 # 并发等待配置
...@@ -342,6 +376,9 @@ redis: ...@@ -342,6 +376,9 @@ redis:
# Database number (0-15) # Database number (0-15)
# 数据库编号(0-15) # 数据库编号(0-15)
db: 0 db: 0
# Enable TLS/SSL connection
# 是否启用 TLS/SSL 连接
enable_tls: false
# ============================================================================= # =============================================================================
# Ops Monitoring (Optional) # Ops Monitoring (Optional)
...@@ -369,6 +406,21 @@ jwt: ...@@ -369,6 +406,21 @@ jwt:
# 令牌过期时间(小时,最大 24) # 令牌过期时间(小时,最大 24)
expire_hour: 24 expire_hour: 24
# =============================================================================
# TOTP (2FA) Configuration
# TOTP 双因素认证配置
# =============================================================================
totp:
# IMPORTANT: Set a fixed encryption key for TOTP secrets.
# 重要:设置固定的 TOTP 加密密钥。
# If left empty, a random key will be generated on each startup, causing all
# existing TOTP configurations to become invalid (users won't be able to
# login with 2FA).
# 如果留空,每次启动将生成随机密钥,导致现有的 TOTP 配置失效(用户无法使用
# 双因素认证登录)。
# Generate with / 生成命令: openssl rand -hex 32
encryption_key: ""
# ============================================================================= # =============================================================================
# LinuxDo Connect OAuth Login (SSO) # LinuxDo Connect OAuth Login (SSO)
# LinuxDo Connect OAuth 登录(用于 Sub2API 用户登录) # LinuxDo Connect OAuth 登录(用于 Sub2API 用户登录)
......
# =============================================================================
# Sub2API Docker Compose - Local Directory Version
# =============================================================================
# This configuration uses local directories for data storage instead of named
# volumes, making it easy to migrate the entire deployment by simply copying
# the deploy directory.
#
# Quick Start:
# 1. Copy .env.example to .env and configure
# 2. mkdir -p data postgres_data redis_data
# 3. docker-compose -f docker-compose.local.yml up -d
# 4. Check logs: docker-compose -f docker-compose.local.yml logs -f sub2api
# 5. Access: http://localhost:8080
#
# Migration to New Server:
# 1. docker-compose -f docker-compose.local.yml down
# 2. tar czf sub2api-deploy.tar.gz deploy/
# 3. Transfer to new server and extract
# 4. docker-compose -f docker-compose.local.yml up -d
# =============================================================================
services:
# ===========================================================================
# Sub2API Application
# ===========================================================================
sub2api:
image: weishaw/sub2api:latest
container_name: sub2api
restart: unless-stopped
ulimits:
nofile:
soft: 100000
hard: 100000
ports:
- "${BIND_HOST:-0.0.0.0}:${SERVER_PORT:-8080}:8080"
volumes:
# Local directory mapping for easy migration
- ./data:/app/data
# Optional: Mount custom config.yaml (uncomment and create the file first)
# Copy config.example.yaml to config.yaml, modify it, then uncomment:
# - ./config.yaml:/app/data/config.yaml:ro
environment:
# =======================================================================
# Auto Setup (REQUIRED for Docker deployment)
# =======================================================================
- AUTO_SETUP=true
# =======================================================================
# Server Configuration
# =======================================================================
- SERVER_HOST=0.0.0.0
- SERVER_PORT=8080
- SERVER_MODE=${SERVER_MODE:-release}
- RUN_MODE=${RUN_MODE:-standard}
# =======================================================================
# Database Configuration (PostgreSQL)
# =======================================================================
- DATABASE_HOST=postgres
- DATABASE_PORT=5432
- DATABASE_USER=${POSTGRES_USER:-sub2api}
- DATABASE_PASSWORD=${POSTGRES_PASSWORD:?POSTGRES_PASSWORD is required}
- DATABASE_DBNAME=${POSTGRES_DB:-sub2api}
- DATABASE_SSLMODE=disable
# =======================================================================
# Redis Configuration
# =======================================================================
- REDIS_HOST=redis
- REDIS_PORT=6379
- REDIS_PASSWORD=${REDIS_PASSWORD:-}
- REDIS_DB=${REDIS_DB:-0}
- REDIS_ENABLE_TLS=${REDIS_ENABLE_TLS:-false}
# =======================================================================
# Admin Account (auto-created on first run)
# =======================================================================
- ADMIN_EMAIL=${ADMIN_EMAIL:-admin@sub2api.local}
- ADMIN_PASSWORD=${ADMIN_PASSWORD:-}
# =======================================================================
# JWT Configuration
# =======================================================================
# IMPORTANT: Set a fixed JWT_SECRET to prevent login sessions from being
# invalidated after container restarts. If left empty, a random secret
# will be generated on each startup.
# Generate a secure secret: openssl rand -hex 32
- JWT_SECRET=${JWT_SECRET:-}
- JWT_EXPIRE_HOUR=${JWT_EXPIRE_HOUR:-24}
# =======================================================================
# TOTP (2FA) Configuration
# =======================================================================
# IMPORTANT: Set a fixed encryption key for TOTP secrets. If left empty,
# a random key will be generated on each startup, causing all existing
# TOTP configurations to become invalid (users won't be able to login
# with 2FA).
# Generate a secure key: openssl rand -hex 32
- TOTP_ENCRYPTION_KEY=${TOTP_ENCRYPTION_KEY:-}
# =======================================================================
# Timezone Configuration
# This affects ALL time operations in the application:
# - Database timestamps
# - Usage statistics "today" boundary
# - Subscription expiry times
# - Log timestamps
# Common values: Asia/Shanghai, America/New_York, Europe/London, UTC
# =======================================================================
- TZ=${TZ:-Asia/Shanghai}
# =======================================================================
# Gemini OAuth Configuration (for Gemini accounts)
# =======================================================================
- GEMINI_OAUTH_CLIENT_ID=${GEMINI_OAUTH_CLIENT_ID:-}
- GEMINI_OAUTH_CLIENT_SECRET=${GEMINI_OAUTH_CLIENT_SECRET:-}
- GEMINI_OAUTH_SCOPES=${GEMINI_OAUTH_SCOPES:-}
- GEMINI_QUOTA_POLICY=${GEMINI_QUOTA_POLICY:-}
# =======================================================================
# Security Configuration (URL Allowlist)
# =======================================================================
# Enable URL allowlist validation (false to skip allowlist checks)
- SECURITY_URL_ALLOWLIST_ENABLED=${SECURITY_URL_ALLOWLIST_ENABLED:-false}
# Allow insecure HTTP URLs when allowlist is disabled (default: false, requires https)
- SECURITY_URL_ALLOWLIST_ALLOW_INSECURE_HTTP=${SECURITY_URL_ALLOWLIST_ALLOW_INSECURE_HTTP:-false}
# Allow private IP addresses for upstream/pricing/CRS (for internal deployments)
- SECURITY_URL_ALLOWLIST_ALLOW_PRIVATE_HOSTS=${SECURITY_URL_ALLOWLIST_ALLOW_PRIVATE_HOSTS:-false}
# Upstream hosts whitelist (comma-separated, only used when enabled=true)
- SECURITY_URL_ALLOWLIST_UPSTREAM_HOSTS=${SECURITY_URL_ALLOWLIST_UPSTREAM_HOSTS:-}
# =======================================================================
# Update Configuration (在线更新配置)
# =======================================================================
# Proxy for accessing GitHub (online updates + pricing data)
# Examples: http://host:port, socks5://host:port
- UPDATE_PROXY_URL=${UPDATE_PROXY_URL:-}
depends_on:
postgres:
condition: service_healthy
redis:
condition: service_healthy
networks:
- sub2api-network
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8080/health"]
interval: 30s
timeout: 10s
retries: 3
start_period: 30s
# ===========================================================================
# PostgreSQL Database
# ===========================================================================
postgres:
image: postgres:18-alpine
container_name: sub2api-postgres
restart: unless-stopped
ulimits:
nofile:
soft: 100000
hard: 100000
volumes:
# Local directory mapping for easy migration
- ./postgres_data:/var/lib/postgresql/data
environment:
- POSTGRES_USER=${POSTGRES_USER:-sub2api}
- POSTGRES_PASSWORD=${POSTGRES_PASSWORD:?POSTGRES_PASSWORD is required}
- POSTGRES_DB=${POSTGRES_DB:-sub2api}
- PGDATA=/var/lib/postgresql/data
- TZ=${TZ:-Asia/Shanghai}
networks:
- sub2api-network
healthcheck:
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-sub2api} -d ${POSTGRES_DB:-sub2api}"]
interval: 10s
timeout: 5s
retries: 5
start_period: 10s
# 注意:不暴露端口到宿主机,应用通过内部网络连接
# 如需调试,可临时添加:ports: ["127.0.0.1:5433:5432"]
# ===========================================================================
# Redis Cache
# ===========================================================================
redis:
image: redis:8-alpine
container_name: sub2api-redis
restart: unless-stopped
ulimits:
nofile:
soft: 100000
hard: 100000
volumes:
# Local directory mapping for easy migration
- ./redis_data:/data
command: >
sh -c '
redis-server
--save 60 1
--appendonly yes
--appendfsync everysec
${REDIS_PASSWORD:+--requirepass "$REDIS_PASSWORD"}'
environment:
- TZ=${TZ:-Asia/Shanghai}
# REDISCLI_AUTH is used by redis-cli for authentication (safer than -a flag)
- REDISCLI_AUTH=${REDIS_PASSWORD:-}
networks:
- sub2api-network
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 10s
timeout: 5s
retries: 5
start_period: 5s
# =============================================================================
# Networks
# =============================================================================
networks:
sub2api-network:
driver: bridge
...@@ -56,6 +56,7 @@ services: ...@@ -56,6 +56,7 @@ services:
- REDIS_PORT=${REDIS_PORT:-6379} - REDIS_PORT=${REDIS_PORT:-6379}
- REDIS_PASSWORD=${REDIS_PASSWORD:-} - REDIS_PASSWORD=${REDIS_PASSWORD:-}
- REDIS_DB=${REDIS_DB:-0} - REDIS_DB=${REDIS_DB:-0}
- REDIS_ENABLE_TLS=${REDIS_ENABLE_TLS:-false}
# ======================================================================= # =======================================================================
# Admin Account (auto-created on first run) # Admin Account (auto-created on first run)
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment