from __future__ import annotations import os from dataclasses import dataclass, field 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 @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 force_change_password: bool graph_base_url: str token_endpoint: str scope: 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, "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] = [] 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 User Management Platform").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-admin-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(), force_change_password=_env_bool("FORCE_CHANGE_PASSWORD", True), graph_base_url=graph_base_url, token_endpoint=token_endpoint, scope=scope, 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), )