100 lines
3.8 KiB
Python
100 lines
3.8 KiB
Python
from __future__ import annotations
|
|
|
|
import os
|
|
import secrets
|
|
from datetime import timedelta
|
|
from pathlib import Path
|
|
|
|
from dotenv import dotenv_values
|
|
|
|
BASE_DIR = Path(__file__).resolve().parent.parent
|
|
DATA_DIR = BASE_DIR / "data"
|
|
STATIC_DIR = BASE_DIR / "static"
|
|
UPLOAD_DIR = BASE_DIR / "uploads"
|
|
IMAGE_DIR = STATIC_DIR / "images"
|
|
|
|
|
|
def load_local_environment() -> None:
|
|
"""加载本地配置,优先级为系统环境变量 > .env.local > .env。"""
|
|
values = dotenv_values(BASE_DIR / ".env")
|
|
overrides = dotenv_values(BASE_DIR / ".env.local")
|
|
# .env.local 中的空值表示“沿用 .env”,避免本机模板清空密钥。
|
|
values.update({key: value for key, value in overrides.items() if value})
|
|
for key, value in values.items():
|
|
if value:
|
|
os.environ.setdefault(key, value)
|
|
|
|
|
|
load_local_environment()
|
|
|
|
|
|
def env_int(name: str, default: int) -> int:
|
|
try:
|
|
return int(os.environ.get(name, str(default)))
|
|
except ValueError:
|
|
return default
|
|
|
|
|
|
def env_bool(name: str, default: bool = False) -> bool:
|
|
value = os.environ.get(name)
|
|
if value is None:
|
|
return default
|
|
return value.strip().lower() in {"1", "true", "yes", "on"}
|
|
|
|
|
|
def app_env() -> str:
|
|
return os.environ.get("APP_ENV", "development").strip().lower() or "development"
|
|
|
|
|
|
def database_url() -> str:
|
|
"""返回数据库地址,并将相对 SQLite 路径固定到项目目录。"""
|
|
value = os.environ.get("DATABASE_URL", "").strip()
|
|
if not value:
|
|
return f"sqlite:///{DATA_DIR / 'pipe_survival_0331.db'}"
|
|
prefix = "sqlite:///"
|
|
if value.startswith(prefix) and not value.startswith("sqlite:////"):
|
|
return f"sqlite:///{BASE_DIR / value.removeprefix(prefix)}"
|
|
return value
|
|
|
|
|
|
class Config:
|
|
APP_ENV = app_env()
|
|
DEBUG = env_bool("DEBUG", APP_ENV in {"dev", "development", "local"})
|
|
SECRET_KEY = os.environ.get("SECRET_KEY") or secrets.token_hex(32)
|
|
SECRET_KEY_GENERATED = not bool(os.environ.get("SECRET_KEY"))
|
|
SQLALCHEMY_DATABASE_URI = database_url()
|
|
SQLALCHEMY_TRACK_MODIFICATIONS = False
|
|
MAX_CONTENT_LENGTH = env_int("MAX_UPLOAD_BYTES", 16 * 1024 * 1024)
|
|
SESSION_COOKIE_HTTPONLY = True
|
|
SESSION_COOKIE_SAMESITE = "Lax"
|
|
SESSION_COOKIE_SECURE = env_bool("SESSION_COOKIE_SECURE", APP_ENV == "production")
|
|
# 仅在用户主动勾选“保持登录状态”时使用。
|
|
REMEMBER_COOKIE_DURATION = timedelta(days=7)
|
|
REMEMBER_COOKIE_HTTPONLY = True
|
|
REMEMBER_COOKIE_SECURE = SESSION_COOKIE_SECURE
|
|
REMEMBER_COOKIE_SAMESITE = "Lax"
|
|
FUSION_MODEL_CORE_DIR = os.environ.get(
|
|
"FUSION_MODEL_CORE_DIR",
|
|
str(BASE_DIR / "model_core"),
|
|
)
|
|
ALLOW_REGISTRATION = env_bool("ALLOW_REGISTRATION", False)
|
|
ADMIN_USERNAME = os.environ.get("ADMIN_USERNAME", "admin").strip() or "admin"
|
|
ADMIN_PASSWORD = os.environ.get("ADMIN_PASSWORD")
|
|
ADMIN_EMAIL = os.environ.get("ADMIN_EMAIL", "").strip().lower()
|
|
APP_TIMEZONE = os.environ.get("APP_TIMEZONE", "Asia/Shanghai").strip() or "Asia/Shanghai"
|
|
# 密码重置和管理员邀请链接均为一次性链接,默认 10 分钟后失效。
|
|
PASSWORD_RESET_TOKEN_MINUTES = env_int("PASSWORD_RESET_TOKEN_MINUTES", 10)
|
|
RESEND_API_KEY = os.environ.get("RESEND_API_KEY", "").strip()
|
|
RESEND_FROM_EMAIL = os.environ.get("RESEND_FROM_EMAIL", "").strip()
|
|
EMAIL_CODE_MINUTES = env_int("EMAIL_CODE_MINUTES", 10)
|
|
EMAIL_CODE_RESEND_SECONDS = env_int("EMAIL_CODE_RESEND_SECONDS", 60)
|
|
EMAIL_CODE_MAX_ATTEMPTS = env_int("EMAIL_CODE_MAX_ATTEMPTS", 5)
|
|
# 邮箱验证码通过后,可用于高风险操作的短时授权。
|
|
FRESH_AUTH_MINUTES = env_int("FRESH_AUTH_MINUTES", 5)
|
|
TRUSTED_DEVICE_DAYS = env_int("TRUSTED_DEVICE_DAYS", 30)
|
|
|
|
|
|
def ensure_dirs() -> None:
|
|
for path in (DATA_DIR, STATIC_DIR, IMAGE_DIR, UPLOAD_DIR):
|
|
path.mkdir(parents=True, exist_ok=True)
|