f21d981585
CI/CD Pipeline / Production Browser E2E (push) Has been skipped
CI/CD Pipeline / Check if frontend-only change (push) Has been skipped
CI/CD Pipeline / Validate - Code Quality (push) Failing after 1m34s
CI/CD Pipeline / Validate - Type Check (mypy) (push) Successful in 47s
CI/CD Pipeline / Validate - Migration (alembic) (push) Successful in 1m5s
CI/CD Pipeline / Unit Tests (push) Successful in 4m57s
CI/CD Pipeline / Integration Tests (push) Successful in 2m13s
CI/CD Pipeline / Frontend Lint (push) Successful in 28s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 54s
CI/CD Pipeline / PR Build API Image (push) Has been skipped
CI/CD Pipeline / PR Build Web Image (push) Has been skipped
CI/CD Pipeline / PR Build Worker Image (push) Has been skipped
CI/CD Pipeline / Build Staging API Image (push) Successful in 12m48s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 1m10s
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 3m19s
CI/CD Pipeline / Build Production API Image (push) Has been skipped
CI/CD Pipeline / Build Production Web Image (push) Has been skipped
CI/CD Pipeline / Build Production Worker Image (push) Has been skipped
CI/CD Pipeline / Deploy Production (push) Has been skipped
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 1m26s
CI/CD Pipeline / Staging E2E Tests (push) Has been cancelled
CI/CD Pipeline / Staging API Integration Tests (push) Has been cancelled
CI/CD Pipeline / ACR Image Cleanup (push) Has been cancelled
287 lines
9.4 KiB
Python
Executable File
287 lines
9.4 KiB
Python
Executable File
"""API 服务配置 — 继承 SharedSettings,只追加 API 特有字段。
|
||
|
||
通用配置统一在 packages/config/base.py 的 SharedSettings 中定义,这里不重复。
|
||
历史上 API 端使用 UPPER_CASE 命名风格的字段,目前通过 property 别名向后兼容。
|
||
新代码统一使用 snake_case(继承自 SharedSettings)。
|
||
"""
|
||
|
||
import os
|
||
from typing import Optional
|
||
|
||
from pydantic import AliasChoices, Field, field_validator
|
||
from pydantic_settings import SettingsConfigDict
|
||
|
||
from packages.config.base import SharedSettings, get_cached_settings
|
||
|
||
|
||
class APISettings(SharedSettings):
|
||
"""API 服务专用配置。
|
||
|
||
通用配置继承自 SharedSettings,这里只定义 API 独有字段。
|
||
"""
|
||
|
||
# ── 应用基本信息 ────────────────────────────────────────────────────
|
||
app_name: str = "xiaoxia-saas"
|
||
app_version: str = "0.1.61"
|
||
|
||
# 应用基础 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
|
||
|
||
# ── 数据库特有 ──────────────────────────────────────────────────────
|
||
use_in_memory_db: bool = False
|
||
|
||
# ── Redis 特有 ──────────────────────────────────────────────────────
|
||
enable_redis_sessions: bool = False
|
||
|
||
# ── JWT ────────────────────────────────────────────────────────────
|
||
# JWT secret key - MUST be set via environment variable, no default allowed
|
||
jwt_secret_key: Optional[str] = None
|
||
|
||
# JWT 算法与过期时间
|
||
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
|
||
|
||
# ── OSS 特有校验 ────────────────────────────────────────────────────
|
||
@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"),
|
||
)
|
||
|
||
# ── CORS ────────────────────────────────────────────────────────────
|
||
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()]
|
||
|
||
# ── 向后兼容:UPPER_CASE property 别名 ──────────────────────────────
|
||
# 新代码请使用 snake_case(继承的字段名),以下别名仅用于兼容旧代码
|
||
|
||
@property
|
||
def APP_NAME(self) -> str:
|
||
return self.app_name
|
||
|
||
@property
|
||
def APP_VERSION(self) -> str:
|
||
return self.app_version
|
||
|
||
@property
|
||
def ENVIRONMENT(self) -> str:
|
||
return self.environment
|
||
|
||
@property
|
||
def DEBUG(self) -> bool:
|
||
return self.debug
|
||
|
||
@property
|
||
def APP_BASE_URL(self) -> str:
|
||
return self.app_base_url
|
||
|
||
@property
|
||
def API_HOST(self) -> str:
|
||
return self.api_host
|
||
|
||
@property
|
||
def API_PORT(self) -> int:
|
||
return self.api_port
|
||
|
||
@property
|
||
def DATABASE_URL(self) -> str:
|
||
return self.database_url
|
||
|
||
@property
|
||
def DATABASE_POOL_SIZE(self) -> int:
|
||
return self.database_pool_size
|
||
|
||
@property
|
||
def DATABASE_MAX_OVERFLOW(self) -> int:
|
||
return self.database_max_overflow
|
||
|
||
@property
|
||
def DATABASE_POOL_TIMEOUT(self) -> int:
|
||
return self.database_pool_timeout
|
||
|
||
@property
|
||
def DATABASE_POOL_RECYCLE(self) -> int:
|
||
return self.database_pool_recycle
|
||
|
||
@property
|
||
def USE_IN_MEMORY_DB(self) -> bool:
|
||
return self.use_in_memory_db
|
||
|
||
@property
|
||
def AUTO_CREATE_SCHEMA(self) -> bool:
|
||
return self.auto_create_schema
|
||
|
||
@property
|
||
def REDIS_URL(self) -> str:
|
||
return self.redis_url
|
||
|
||
@property
|
||
def ENABLE_REDIS_SESSIONS(self) -> bool:
|
||
return self.enable_redis_sessions
|
||
|
||
@property
|
||
def JWT_SECRET_KEY(self) -> Optional[str]:
|
||
return self.jwt_secret_key
|
||
|
||
@property
|
||
def JWT_ALGORITHM(self) -> str:
|
||
return self.jwt_algorithm
|
||
|
||
@property
|
||
def JWT_ACCESS_TOKEN_EXPIRE_MINUTES(self) -> int:
|
||
return self.jwt_access_token_expire_minutes
|
||
|
||
@property
|
||
def JWT_REFRESH_TOKEN_EXPIRE_DAYS(self) -> int:
|
||
return self.jwt_refresh_token_expire_days
|
||
|
||
@property
|
||
def ENABLE_EMAIL_DELIVERY(self) -> bool:
|
||
return self.enable_email_delivery
|
||
|
||
@property
|
||
def SMTP_HOST(self) -> str:
|
||
return self.smtp_host
|
||
|
||
@property
|
||
def SMTP_PORT(self) -> int:
|
||
return self.smtp_port
|
||
|
||
@property
|
||
def SMTP_USER(self) -> str:
|
||
return self.smtp_user
|
||
|
||
@property
|
||
def SMTP_PASSWORD(self) -> str:
|
||
return self.smtp_password
|
||
|
||
@property
|
||
def SMTP_FROM_EMAIL(self) -> str:
|
||
return self.smtp_from_email
|
||
|
||
@property
|
||
def SMTP_FROM_NAME(self) -> str:
|
||
return self.smtp_from_name
|
||
|
||
@property
|
||
def SMTP_USE_TLS(self) -> bool:
|
||
return self.smtp_use_tls
|
||
|
||
@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
|
||
|
||
@property
|
||
def OSS_DIRECT_UPLOAD_MAX_MB(self) -> int:
|
||
return self.oss_direct_upload_max_mb
|
||
|
||
@property
|
||
def OSS_DIRECT_UPLOAD_EXPIRE_SECONDS(self) -> int:
|
||
return self.oss_direct_upload_expire_seconds
|
||
|
||
@property
|
||
def CORS_ORIGINS_RAW(self) -> str:
|
||
return self.cors_origins_raw
|
||
|
||
@property
|
||
def CORS_ORIGINS(self) -> list[str]:
|
||
return self.cors_origins
|
||
|
||
@property
|
||
def RENDER_ENGINE(self) -> str:
|
||
return self.render_engine
|
||
|
||
|
||
def get_api_settings() -> APISettings:
|
||
"""获取 API 配置单例(统一入口)。"""
|
||
return get_cached_settings(APISettings)
|