157 lines
5.7 KiB
Python
157 lines
5.7 KiB
Python
from __future__ import annotations
|
|
|
|
import os
|
|
from dataclasses import dataclass, field
|
|
from pathlib import Path
|
|
|
|
from dotenv import load_dotenv
|
|
|
|
|
|
GRAPH_BASE_URL = "https://graph.microsoft.com/v1.0"
|
|
GRAPH_SCOPE = "https://graph.microsoft.com/.default"
|
|
TOKEN_ENDPOINT_TEMPLATE = "https://login.microsoftonline.com/{tenant_id}/oauth2/v2.0/token"
|
|
|
|
|
|
def _env_bool(name: str, default: bool = False) -> bool:
|
|
return os.getenv(name, str(default)).strip().lower() in {"1", "true", "yes", "on"}
|
|
|
|
|
|
def _env_int(name: str, default: int) -> int:
|
|
try:
|
|
return int(os.getenv(name, str(default)).strip())
|
|
except ValueError:
|
|
return default
|
|
|
|
|
|
def _normalize_database_url(database_url: str, warnings: list[str]) -> str:
|
|
normalized = database_url.strip()
|
|
if not normalized:
|
|
return "sqlite:///redemption.db"
|
|
|
|
container_prefix = "sqlite:////app/"
|
|
if normalized.startswith(container_prefix) and not Path("/.dockerenv").exists():
|
|
local_relative = normalized.removeprefix(container_prefix)
|
|
project_root = Path(__file__).resolve().parent.parent
|
|
local_path = (project_root / local_relative).resolve()
|
|
warnings.append(
|
|
f"DATABASE_URL 使用容器路径时,已自动映射到本地路径 {local_path}。"
|
|
)
|
|
return f"sqlite:///{local_path}"
|
|
|
|
return normalized
|
|
|
|
|
|
@dataclass
|
|
class Settings:
|
|
app_name: str
|
|
host: str
|
|
port: int
|
|
debug: bool
|
|
session_secret: str
|
|
auth_enabled: bool
|
|
admin_username: str
|
|
admin_password: str
|
|
client_id: str
|
|
tenant_id: str
|
|
client_secret: str
|
|
default_password: str
|
|
default_domain: str
|
|
default_usage_location: str
|
|
default_license_sku: str
|
|
license_assignment_required: bool
|
|
force_change_password: bool
|
|
graph_base_url: str
|
|
token_endpoint: str
|
|
scope: str
|
|
database_url: str
|
|
default_page_size: int = 25
|
|
max_page_size: int = 100
|
|
validation_errors: tuple[str, ...] = field(default_factory=tuple)
|
|
warnings: tuple[str, ...] = field(default_factory=tuple)
|
|
|
|
@property
|
|
def graph_ready(self) -> bool:
|
|
return not self.validation_errors
|
|
|
|
@property
|
|
def effective_auth_enabled(self) -> bool:
|
|
return self.auth_enabled and bool(self.admin_username and self.admin_password)
|
|
|
|
def to_public_dict(self) -> dict:
|
|
return {
|
|
"appName": self.app_name,
|
|
"graphFlavor": "Microsoft Graph Global",
|
|
"graphReady": self.graph_ready,
|
|
"validationErrors": list(self.validation_errors),
|
|
"warnings": list(self.warnings),
|
|
"authEnabled": self.effective_auth_enabled,
|
|
"defaultDomain": self.default_domain,
|
|
"defaultUsageLocation": self.default_usage_location,
|
|
"defaultLicenseSku": self.default_license_sku,
|
|
"licenseAssignmentRequired": self.license_assignment_required,
|
|
"forceChangePassword": self.force_change_password,
|
|
"pageSize": self.default_page_size,
|
|
"maxPageSize": self.max_page_size,
|
|
}
|
|
|
|
|
|
def load_settings() -> Settings:
|
|
load_dotenv()
|
|
|
|
tenant_id = os.getenv("TENANT_ID", "").strip()
|
|
graph_base_url = GRAPH_BASE_URL
|
|
token_endpoint = TOKEN_ENDPOINT_TEMPLATE.format(tenant_id=tenant_id) if tenant_id else ""
|
|
scope = GRAPH_SCOPE
|
|
|
|
validation_errors: list[str] = []
|
|
warnings: list[str] = []
|
|
database_url = _normalize_database_url(os.getenv("DATABASE_URL", "sqlite:///redemption.db"), warnings)
|
|
|
|
required_fields = {
|
|
"CLIENT_ID": os.getenv("CLIENT_ID", "").strip(),
|
|
"TENANT_ID": tenant_id,
|
|
"CLIENT_SECRET": os.getenv("CLIENT_SECRET", "").strip(),
|
|
"DEFAULT_PASSWORD": os.getenv("DEFAULT_PASSWORD", "").strip(),
|
|
}
|
|
|
|
for field_name, value in required_fields.items():
|
|
if not value:
|
|
validation_errors.append(f"{field_name} 未配置")
|
|
|
|
if not os.getenv("DEFAULT_DOMAIN", "").strip():
|
|
warnings.append("DEFAULT_DOMAIN 未配置,创建账号时必须填写完整 userPrincipalName。")
|
|
|
|
auth_enabled = _env_bool("WEB_AUTH_ENABLED", True)
|
|
admin_username = os.getenv("ADMIN_USERNAME", "").strip()
|
|
admin_password = os.getenv("ADMIN_PASSWORD", "").strip()
|
|
if auth_enabled and not (admin_username and admin_password):
|
|
warnings.append("WEB_AUTH_ENABLED=true 但未配置后台登录账号,已自动退回为无登录保护模式。")
|
|
|
|
return Settings(
|
|
app_name=os.getenv("APP_NAME", "Office 365 Self Service").strip(),
|
|
host=os.getenv("HOST", "0.0.0.0").strip(),
|
|
port=_env_int("PORT", 8000),
|
|
debug=_env_bool("DEBUG", False),
|
|
session_secret=os.getenv("SESSION_SECRET", "office365-self-service-dev-secret").strip(),
|
|
auth_enabled=auth_enabled,
|
|
admin_username=admin_username,
|
|
admin_password=admin_password,
|
|
client_id=required_fields["CLIENT_ID"],
|
|
tenant_id=required_fields["TENANT_ID"],
|
|
client_secret=required_fields["CLIENT_SECRET"],
|
|
default_password=required_fields["DEFAULT_PASSWORD"],
|
|
default_domain=os.getenv("DEFAULT_DOMAIN", "").strip(),
|
|
default_usage_location=os.getenv("DEFAULT_USAGE_LOCATION", "US").strip() or "US",
|
|
default_license_sku=os.getenv("DEFAULT_LICENSE_SKU", "").strip(),
|
|
license_assignment_required=_env_bool("LICENSE_ASSIGNMENT_REQUIRED", False),
|
|
force_change_password=_env_bool("FORCE_CHANGE_PASSWORD", True),
|
|
graph_base_url=graph_base_url,
|
|
token_endpoint=token_endpoint,
|
|
scope=scope,
|
|
database_url=database_url,
|
|
default_page_size=min(max(_env_int("DEFAULT_PAGE_SIZE", 25), 1), 100),
|
|
max_page_size=min(max(_env_int("MAX_PAGE_SIZE", 100), 10), 500),
|
|
validation_errors=tuple(validation_errors),
|
|
warnings=tuple(warnings),
|
|
)
|