9c6c477f55
CI/CD Pipeline / Validate Code Quality And Tests (pull_request) Failing after 2m22s
CI/CD Pipeline / Unit Tests (pull_request) Failing after 2m24s
CI/CD Pipeline / Integration Tests (pull_request) Failing after 37s
CI/CD Pipeline / Frontend Lint (pull_request) Successful in 4m3s
CI/CD Pipeline / Build & Push Staging (Watchtower auto-deploy) (pull_request) Has been skipped
CI/CD Pipeline / Build Production Runtime Images (pull_request) Has been skipped
CI/CD Pipeline / Staging API Integration Tests (pull_request) Has been skipped
CI/CD Pipeline / Deploy Production (pull_request) Has been skipped
CI/CD Pipeline / Staging E2E Tests (pull_request) Has been skipped
CI/CD Pipeline / Production Browser E2E (pull_request) Has been skipped
P0 关键修复: - P0-1: 注册接口添加 RateLimitMiddleware 限流保护 - P0-3: /metrics 端点添加 JWT 认证(移除匿名访问) - P0-4: 修复 Celery 任务名冲突(generation_task vs generate_video) - P1-5: JWT logout token 黑名单机制 P1 修复: - P1-1: forgot_password 硬编码 localhost → 使用 settings.APP_BASE_URL - P1-2: generation.py 直接创建 DB 连接 → 使用依赖注入 - P1-6: Image.open() 未关闭 → 统一使用 with 语句 - P1-7: 订阅续费事务修复 P2 代码质量: - P2-1: 修复 EditingMode 枚举重复定义 → 统一引用 shared 包 - P2-2: 修复 SMTP_FRON_NAME → SMTP_FROM_NAME 拼写 - P2-3: UserModel subscription_quota 类型统一为 float - P2-4: .env.production DATABASE_MAX_OVERFLOW 30 → 10 - 清理 15 处 except:pass(保留 2 处有注释说明的) - 禁用 SVG 上传(XSS 风险) - 删除 decode_token_unsafe() 不安全函数 - 简化 /ready 端点 - 删除 8 处死代码、10 个空文件/模块 - 合并 3 对 100% 重复函数 - 对齐 6 个废弃环境变量 v2 修复(代码审查后): - 修复密码重置路由路径: /password/forgot → /forgot-password, /password/reset → /reset-password(与前端 API 对齐) - 合并 _check_project_access: asset_libraries.py 和 edit_plans.py 中的重复函数统一到 _helpers.py(含空字符串守卫 + 中文错误信息) - 顺手修复: HTTPException 统一从 fastapi 导入(替换 starlette 导入) - OSS_ENDPOINT 拼写修复拆分为单独 PR,本 PR 不包含
180 lines
5.9 KiB
Python
Executable File
180 lines
5.9 KiB
Python
Executable File
import os
|
||
from typing import Optional
|
||
|
||
from pydantic import AliasChoices, Field, field_validator
|
||
from pydantic_settings import BaseSettings, SettingsConfigDict
|
||
|
||
|
||
class Settings(BaseSettings):
|
||
APP_NAME: str = "xiaoxia-saas"
|
||
APP_VERSION: str = "0.1.61"
|
||
ENVIRONMENT: str = "development"
|
||
DEBUG: bool = True
|
||
|
||
# 应用基础 URL,用于生成认证邮件中的链接
|
||
# 开发环境默认 http://localhost:3000
|
||
# 生产环境应通过环境变量 APP_BASE_URL 设置
|
||
APP_BASE_URL: str = "http://localhost:3000"
|
||
|
||
# Container bind address; external expose is controlled by Docker/Nginx.
|
||
API_HOST: str = "0.0.0.0" # nosec: B104
|
||
API_PORT: int = 8000
|
||
API_PREFIX: str = "/api/v1"
|
||
|
||
DATABASE_URL: str = "postgresql+psycopg://postgres:postgres@localhost:5432/xiaoxia_saas"
|
||
DATABASE_POOL_SIZE: int = 20
|
||
DATABASE_MAX_OVERFLOW: int = 10 # 调整为合理值:pool_size(20) + max_overflow(10) = 最大30连接
|
||
DATABASE_POOL_TIMEOUT: int = 30
|
||
DATABASE_POOL_RECYCLE: int = 3600
|
||
USE_IN_MEMORY_DB: bool = False
|
||
AUTO_CREATE_SCHEMA: bool = False
|
||
|
||
REDIS_URL: str = "redis://localhost:6379/0"
|
||
REDIS_MAX_CONNECTION: int = 50
|
||
ENABLE_REDIS_SESSIONS: bool = False
|
||
|
||
# JWT secret key - MUST be set via environment variable, no default allowed
|
||
JWT_SECRET_KEY: Optional[str] = None
|
||
# 旧的 JWT secret key(用于密钥轮换期间验证旧 token)
|
||
# 在密钥轮换时,先设置新密钥,旧密钥保留在此处直到所有旧 token 过期
|
||
JWT_SECRET_KEY_OLD: Optional[str] = None
|
||
# 密钥轮换天数(到达此天数后建议更换密钥)
|
||
SECRET_ROTATION_DAYS: int = 90
|
||
|
||
# JWT 算法与过期时间(与 .env.example 对齐)
|
||
JWT_ALGORITHM: str = "HS256"
|
||
JWT_ACCESS_TOKEN_EXPIRE_MINUTES: int = 30
|
||
JWT_REFRESH_TOKEN_EXPIRE_DAYS: int = 30
|
||
|
||
@field_validator("JWT_SECRET_KEY", mode="before")
|
||
@classmethod
|
||
def validate_jwt_secret_key(cls, v):
|
||
if v is None or v == "":
|
||
raise ValueError(
|
||
"JWT_SECRET_KEY must be set via environment variable. " "Do not use default value in production!"
|
||
)
|
||
# Block known insecure default values
|
||
insecure_defaults = [
|
||
"your-secret-key-change-in-production",
|
||
"your-secret-key",
|
||
"secret",
|
||
"changeme",
|
||
"password",
|
||
]
|
||
if v.lower() in [d.lower() for d in insecure_defaults]:
|
||
raise ValueError(
|
||
f"JWT_SECRET_KEY '{v}' is insecure. " "Please set a strong random secret via environment variable."
|
||
)
|
||
return v
|
||
|
||
ENABLE_EMAIL_DELIVERY: bool = False
|
||
SMTP_HOST: str = "smtp.gmail.com"
|
||
SMTP_PORT: int = 587
|
||
SMTP_USER: str = ""
|
||
SMTP_PASSWORD: str = ""
|
||
SMTP_FROM_EMAIL: str = ""
|
||
SMTP_FROM_NAME: str = "小虾 SaaS"
|
||
SMTP_USE_TLS: bool = True
|
||
|
||
CELERY_BROKER_URL: str = "redis://localhost:6379/0"
|
||
CELERY_RESULT_BACKEND: str = "redis://localhost:6379/1"
|
||
|
||
# OSS 七牛云相关
|
||
OSS_ENDPOINT: str = "oss-cn-hangzhou.aliiyuncs.com"
|
||
OSS_ACCESS_KEY_ID: str = ""
|
||
OSS_ACCESS_KEY_SECRET: str = ""
|
||
OSS_BUCKET_NAME: str = "xiaoxia-autocut"
|
||
|
||
@field_validator("OSS_ACCESS_KEY_ID", mode="before")
|
||
@classmethod
|
||
def validate_oss_access_key_id(cls, v):
|
||
if (v is None or v == "") and os.getenv("APP_ENV", "development") != "development":
|
||
raise ValueError(
|
||
"OSS_ACCESS_KEY_ID must be set via environment variable in non-development environments. "
|
||
"Check the server .env file (e.g. /var/lib/xiaoxia-saas-staging/.env)."
|
||
)
|
||
return v or ""
|
||
|
||
@field_validator("OSS_ACCESS_KEY_SECRET", mode="before")
|
||
@classmethod
|
||
def validate_oss_access_key_secret(cls, v):
|
||
if (v is None or v == "") and os.getenv("APP_ENV", "development") != "development":
|
||
raise ValueError(
|
||
"OSS_ACCESS_KEY_SECRET must be set via environment variable in non-development environments. "
|
||
"Check the server .env file (e.g. /var/lib/xiaoxia-saas-staging/.env)."
|
||
)
|
||
return v or ""
|
||
|
||
OSS_DIRECT_UPLOAD_MAX_MB: int = Field(
|
||
default=2000,
|
||
validation_alias=AliasChoices("OSS_DIRECT_UPLOAD_MAX_MB", "MAX_UPLOAD_SIZE_MB"),
|
||
)
|
||
OSS_DIRECT_UPLOAD_EXPIRE_SECONDS: int = 900
|
||
|
||
LOG_LEVEL: str = "INFO"
|
||
CORS_ORIGINS_RAW: str = "http://localhost:3000,http://localhost:5173,http://localhost:8000"
|
||
|
||
# 渲染引擎选择:legacy=旧VideoComposeService,unified=新UnifiedRenderService
|
||
RENDER_ENGINE: str = "legacy"
|
||
|
||
model_config = SettingsConfigDict(
|
||
env_file=".env",
|
||
env_file_encoding="utf-8",
|
||
case_sensitive=False,
|
||
extra="ignore",
|
||
)
|
||
|
||
@property
|
||
def CORS_ORIGINS(self) -> list[str]:
|
||
return [origin.strip() for origin in self.CORS_ORIGINS_RAW.split(",") if origin.strip()]
|
||
|
||
@property
|
||
def database_url(self) -> str:
|
||
return self.DATABASE_URL
|
||
|
||
@property
|
||
def redis_url(self) -> str:
|
||
return self.REDIS_URL
|
||
|
||
@property
|
||
def celery_broker_url(self) -> str:
|
||
return self.CELERY_BROKER_URL
|
||
|
||
@property
|
||
def celery_result_backend(self) -> str:
|
||
return self.CELERY_RESULT_BACKEND
|
||
|
||
@property
|
||
def oss_endpoint(self) -> str:
|
||
return self.OSS_ENDPOINT
|
||
|
||
@property
|
||
def oss_access_key_id(self) -> str:
|
||
return self.OSS_ACCESS_KEY_ID
|
||
|
||
@property
|
||
def oss_access_key_secret(self) -> str:
|
||
return self.OSS_ACCESS_KEY_SECRET
|
||
|
||
@property
|
||
def oss_bucket_name(self) -> str:
|
||
return self.OSS_BUCKET_NAME
|
||
|
||
|
||
_settings: Optional[Settings] = None
|
||
|
||
|
||
def get_settings() -> Settings:
|
||
global _settings
|
||
if _settings is None:
|
||
env = os.getenv("APP_ENV", "development")
|
||
env_file = f".env.{env}" if env != "development" else ".env"
|
||
if os.path.exists(env_file):
|
||
_settings = Settings(_env_file=env_file)
|
||
else:
|
||
_settings = Settings()
|
||
return _settings
|
||
|
||
|
||
settings = get_settings()
|