Coverage for src/lexigram/admin/config.py: 97%

337 statements  

« prev     ^ index     » next       coverage.py v7.15.4, created at 2026-08-21 14:56 +0800

1"""Admin configuration models and factory helpers.""" 

2 

3from __future__ import annotations 

4 

5from dataclasses import dataclass 

6import os 

7from typing import Any, ClassVar, Literal, cast 

8 

9from lexigram.admin.constants import ENV_NESTED_DELIMITER, ENV_PREFIX 

10from lexigram.admin.resources.config import ResourceConfig, TableConfiguration 

11from lexigram.config import BaseConfig 

12from lexigram.domain import DomainModel 

13from lexigram.validation import ConfigDict, Field, SecretStr, model_validator 

14 

15 

16@dataclass(init=False) 

17class ResourceDefaults(DomainModel): 

18 """Default configuration for all resources.""" 

19 

20 per_page: int = Field(default=20, ge=1, le=1000) 

21 enable_search: bool = Field(default=True) 

22 enable_export: bool = Field(default=True) 

23 enable_bulk_actions: bool = Field(default=True) 

24 action_layout: Literal["horizontal", "vertical", "dropdown"] = Field( 

25 default="horizontal", 

26 ) 

27 soft_delete: bool = Field(default=True) 

28 timestamp_fields: bool = Field(default=True) 

29 

30 model_config = {"extra": "forbid"} 

31 

32 

33@dataclass(init=False) 

34class TableDefaults(DomainModel): 

35 """Default configuration for DataTables.""" 

36 

37 reorderable_columns: bool = Field(default=False) 

38 enable_column_visibility: bool = Field(default=True) 

39 sticky_header: bool = Field(default=True) 

40 virtualized: bool = Field(default=False) 

41 row_height: int = Field(default=48, ge=24, le=120) 

42 zebra_stripes: bool = Field(default=True) 

43 hover_highlight: bool = Field(default=True) 

44 

45 model_config = {"extra": "forbid"} 

46 

47 

48@dataclass(init=False) 

49class FormDefaults(DomainModel): 

50 """Default configuration for Forms.""" 

51 

52 show_required_indicator: bool = Field(default=True) 

53 autosave_enabled: bool = Field(default=False) 

54 autosave_interval_ms: int = Field(default=30000, ge=1000) 

55 confirm_unsaved_changes: bool = Field(default=True) 

56 inline_validation: bool = Field(default=True) 

57 

58 model_config = {"extra": "forbid"} 

59 

60 

61@dataclass(init=False) 

62class ResourceYAMLConfig(DomainModel): 

63 """Per-resource configuration from YAML.""" 

64 

65 enabled: bool = Field(default=True) 

66 icon: str | None = None 

67 label: str | None = None 

68 label_plural: str | None = None 

69 per_page: int | None = Field(default=None, ge=1, le=1000) 

70 searchable_fields: list[str] | None = None 

71 default_sort: str | None = None 

72 default_sort_order: Literal["asc", "desc"] = "desc" 

73 permissions: dict[str, list[str]] | None = None 

74 

75 model_config = {"extra": "allow"} 

76 

77 

78@dataclass(init=False) 

79class AdminNavigationGroup(DomainModel): 

80 """Navigation group configuration.""" 

81 

82 label: str 

83 icon: str | None = None 

84 order: int = Field(default=100, ge=0) 

85 resources: list[str] = Field(default_factory=list) 

86 permission: str | None = None 

87 collapsible: bool = Field(default=True) 

88 collapsed_by_default: bool = Field(default=False) 

89 

90 model_config = {"extra": "forbid"} 

91 

92 

93@dataclass(init=False) 

94class AdminFeaturesConfig(DomainModel): 

95 """Feature flags for admin functionality.""" 

96 

97 # UI Features 

98 command_palette: bool = Field(default=True) 

99 keyboard_shortcuts: bool = Field(default=True) 

100 theme_toggle: bool = Field(default=True) 

101 search: bool = Field(default=True) 

102 

103 # UX Features 

104 optimistic_updates: bool = Field(default=True) 

105 undo_redo: bool = Field(default=True) 

106 autosave: bool = Field(default=False) 

107 

108 # Advanced Features 

109 audit_logging: bool = Field(default=True) 

110 activity_feed: bool = Field(default=False) 

111 notifications: bool = Field(default=True) 

112 webhooks: bool = Field(default=False) 

113 api_docs: bool = Field(default=True) 

114 

115 model_config = {"extra": "allow"} 

116 

117 

118@dataclass(init=False) 

119class AdminRbacConfig(DomainModel): 

120 """RBAC editing-page configuration.""" 

121 

122 #: Role name granted wildcard admin rights. Matches the role string 

123 #: already special-cased by settings/widgets/impersonation. 

124 super_admin_role: str = Field(default="superadmin") 

125 

126 

127@dataclass(init=False) 

128class AdminPasswordPolicyConfig(DomainModel): 

129 """Password policy configuration for admin authentication. 

130 

131 Follows NIST SP 800-63B guidelines. 

132 """ 

133 

134 min_length: int = Field(default=12, ge=8, le=128) 

135 max_length: int = Field(default=128, ge=32, le=1024) 

136 require_uppercase: bool = Field(default=True) 

137 require_lowercase: bool = Field(default=True) 

138 require_digit: bool = Field(default=True) 

139 require_special: bool = Field(default=True) 

140 reject_common_passwords: bool = Field(default=True) 

141 reject_containing_email: bool = Field(default=True) 

142 

143 model_config = {"extra": "forbid"} 

144 

145 

146@dataclass(init=False) 

147class AdminSecurityConfig(DomainModel): 

148 """Security hardening configuration for admin authentication. 

149 

150 Controls rate limiting, progressive lockout, and setup token protection. 

151 """ 

152 

153 ip_rate_limit_enabled: bool = Field(default=True) 

154 ip_rate_limit_per_minute: int = Field(default=10, ge=1) 

155 ip_rate_limit_per_15_minutes: int = Field(default=30, ge=1) 

156 ip_rate_limit_per_hour: int = Field(default=60, ge=1) 

157 

158 # Progressive lockout thresholds: list of (failure_count, lockout_minutes) 

159 # e.g. [(5, 15), (10, 60), (15, 240), (20, 1440)] means: 

160 # 5 failures → 15 min lockout, 10 → 1hr, 15 → 4hr, 20 → 24hr 

161 lockout_thresholds: list[tuple[int, int]] = Field( 

162 default_factory=lambda: [(5, 15), (10, 60), (15, 240), (20, 1440)], 

163 ) 

164 permanent_lockout_threshold: int = Field(default=50, ge=10) 

165 

166 setup_token: str | None = Field( 

167 default=None, 

168 description="Optional ADMIN_SETUP_TOKEN — when set, must be provided during first-run setup.", 

169 ) 

170 setup_token_optin_unsafe: bool = Field( 

171 default=False, 

172 description=( 

173 "Explicit escape hatch: boot without a setup token. Only for " 

174 "local/ephemeral environments — leaves the first-run wizard open " 

175 "to any visitor until an admin account is created." 

176 ), 

177 ) 

178 

179 @model_validator(mode="before") 

180 @classmethod 

181 def _map_legacy_setup_token(cls, data: Any) -> Any: 

182 """Map the legacy ``ADMIN_SETUP_TOKEN`` input key onto ``setup_token``. 

183 

184 Keeps existing deployments working unchanged: the token can be 

185 provided as a config key (``admin.security.ADMIN_SETUP_TOKEN`` in 

186 YAML/``from_dict`` input) or as the bare ``ADMIN_SETUP_TOKEN`` 

187 environment variable. Explicit ``setup_token`` always wins. 

188 

189 Args: 

190 data: Raw input dict (or already-built instance) before field 

191 assignment. 

192 

193 Returns: 

194 The input dict with the legacy key mapped onto ``setup_token`` 

195 when the latter is absent. 

196 """ 

197 if not isinstance(data, dict): 

198 return data 

199 if "ADMIN_SETUP_TOKEN" in data and "setup_token" not in data: 

200 return {**data, "setup_token": data["ADMIN_SETUP_TOKEN"]} 

201 if "setup_token" not in data: 

202 legacy = os.getenv("ADMIN_SETUP_TOKEN") 

203 if legacy: 

204 return {**data, "setup_token": legacy} 

205 return data 

206 

207 model_config = {"extra": "forbid"} 

208 

209 

210@dataclass(init=False) 

211class AdminMfaConfig(DomainModel): 

212 """Two-factor authentication (TOTP) configuration. 

213 

214 Controls whether TOTP 2FA is offered, the issuer label embedded in 

215 provisioning URIs, and the allowed clock-skew window for codes. 

216 """ 

217 

218 enabled: bool = Field(default=True, description="Enable TOTP 2FA") 

219 factor: str = Field( 

220 default="totp", 

221 description="Second factor used at login: 'totp' (authenticator app) or 'email' (one-time code)", 

222 ) 

223 issuer: str = Field( 

224 default="Lexigram Admin", 

225 description="TOTP issuer label shown in authenticator apps", 

226 ) 

227 skew: int = Field( 

228 default=1, 

229 ge=0, 

230 le=2, 

231 description="Allowed clock skew in 30 second steps", 

232 ) 

233 

234 

235@dataclass(init=False) 

236class AdminEmailOtpConfig(DomainModel): 

237 """Email one-time-password (login factor) configuration. 

238 

239 Controls whether the email-OTP factor is available, how long a code 

240 stays valid, and the minimum interval between sends. 

241 """ 

242 

243 enabled: bool = Field(default=True, description="Enable email OTP factor") 

244 ttl_minutes: int = Field( 

245 default=10, 

246 ge=1, 

247 le=60, 

248 description="Code validity window in minutes", 

249 ) 

250 resend_cooldown_seconds: int = Field( 

251 default=60, 

252 ge=5, 

253 le=600, 

254 description="Minimum seconds between email OTP sends", 

255 ) 

256 

257 

258@dataclass(init=False) 

259class AdminEmailVerificationConfig(DomainModel): 

260 """Email verification (login gate) configuration. 

261 

262 Controls the verify-your-email flow: whether it is offered, whether 

263 unverified users are blocked at login, and the verify-link lifetime. 

264 """ 

265 

266 enabled: bool = Field(default=True, description="Enable email verification flow") 

267 enforcement: bool = Field( 

268 default=True, 

269 description="Block login until the email is verified", 

270 ) 

271 token_ttl_hours: int = Field( 

272 default=24, 

273 ge=1, 

274 le=168, 

275 description="Verify link validity in hours", 

276 ) 

277 

278 

279@dataclass(init=False) 

280class AdminRegistrationConfig(DomainModel): 

281 """Self-service registration configuration. 

282 

283 Off by default — admin panels are typically invite-only. When enabled, 

284 ``GET/POST /admin/register`` becomes available and new accounts receive 

285 the configured default role. 

286 """ 

287 

288 enabled: bool = Field(default=False, description="Allow self-service registration") 

289 default_role: str = Field( 

290 default="admin", description="Role granted to new accounts" 

291 ) 

292 allowed_email_domains: list[str] = Field( 

293 default_factory=list, 

294 description="Restrict registration to these email domains (empty = any)", 

295 ) 

296 

297 

298@dataclass(init=False) 

299class AdminAuthConfig(DomainModel): 

300 """Authentication configuration.""" 

301 

302 enabled: bool = Field(default=True, description="Enable authentication") 

303 env: Literal["development", "staging", "production"] = Field( 

304 default="development", 

305 description="Deployment environment for cookie security defaults", 

306 ) 

307 session_secret: SecretStr = Field( 

308 default=SecretStr("change-me-in-production"), 

309 description="Session secret for signing", 

310 ) 

311 login_url: str = Field(default="/admin/login") 

312 logout_url: str = Field(default="/admin/logout") 

313 session_lifetime: int = Field(default=86400, ge=300) # 5 min minimum 

314 permission_cache_ttl: int = Field(default=300, ge=0) # 5 minutes 

315 

316 # Security settings 

317 idle_timeout: int = Field( 

318 default=3600, ge=60, description="Session idle timeout in seconds" 

319 ) 

320 csrf_token_lifetime: int = Field( 

321 default=3600, ge=60, description="CSRF token expiry in seconds" 

322 ) 

323 password_policy: AdminPasswordPolicyConfig = Field( 

324 default_factory=AdminPasswordPolicyConfig, 

325 ) 

326 security: AdminSecurityConfig = Field( 

327 default_factory=AdminSecurityConfig, 

328 ) 

329 mfa: AdminMfaConfig = Field(default_factory=AdminMfaConfig) 

330 email_otp: AdminEmailOtpConfig = Field(default_factory=AdminEmailOtpConfig) 

331 email_verification: AdminEmailVerificationConfig = Field( 

332 default_factory=AdminEmailVerificationConfig 

333 ) 

334 registration: AdminRegistrationConfig = Field( 

335 default_factory=AdminRegistrationConfig 

336 ) 

337 

338 # Users and Roles (Sync) 

339 users: list[Any] = Field(default_factory=list) 

340 roles: dict[str, Any] = Field(default_factory=dict) 

341 

342 # Identity bridge (spec D3): "internal" = framework admin_users table 

343 # (default); "app" = AdminPrincipalProviderProtocol implemented by the app. 

344 principal_source: Literal["internal", "app"] = Field(default="internal") 

345 

346 # OAuth/SSO (optional) 

347 oauth_enabled: bool = Field(default=False) 

348 oauth_providers: list[str] = Field(default_factory=list) 

349 

350 model_config = {"extra": "allow"} 

351 

352 @model_validator(mode="after") 

353 def validate_security(self) -> AdminAuthConfig: 

354 """Ensure secure settings in production.""" 

355 if not isinstance(self.session_secret, SecretStr): 

356 self.session_secret = SecretStr(self.session_secret) 

357 insecure_defaults = ( 

358 "change-me", 

359 "your-secret-key", 

360 "secret", 

361 "password", 

362 "change-me-in-production", 

363 ) 

364 

365 if ( 

366 self.env == "production" 

367 and self.session_secret.get_secret_value().lower() in insecure_defaults 

368 ): 

369 raise ValueError( 

370 "CRITICAL SECURITY ERROR: Default admin session_secret detected in PRODUCTION.\n" 

371 "You MUST set a secure session secret via LEX_ADMIN__AUTH__SESSION_SECRET.", 

372 ) 

373 

374 if ( 

375 self.env in {"production", "staging"} 

376 and self.oauth_enabled 

377 and not self.oauth_providers 

378 ): 

379 raise ValueError( 

380 "oauth_providers must be configured when oauth_enabled=True" 

381 ) 

382 

383 if ( 

384 self.env in {"production", "staging"} 

385 and self.csrf_token_lifetime > self.idle_timeout 

386 ): 

387 raise ValueError("csrf_token_lifetime must not exceed idle_timeout") 

388 

389 return self 

390 

391 

392@dataclass(init=False) 

393class AdminAuditConfig(DomainModel): 

394 """Audit logging configuration.""" 

395 

396 read_audit_enabled: bool = Field( 

397 default=False, 

398 description="Log read operations (off by default; compliance mode only).", 

399 ) 

400 

401 model_config = {"extra": "forbid"} 

402 

403 

404@dataclass(init=False) 

405class AdminUIConfig(DomainModel): 

406 """UI/Theme configuration.""" 

407 

408 theme: Literal["light", "dark", "system"] = Field(default="system") 

409 primary_color: str = Field(default="#6B7280") 

410 sidebar_width: int = Field(default=256, ge=200, le=400) 

411 sidebar_collapsed_width: int = Field(default=64, ge=48, le=100) 

412 content_max_width: int | None = Field(default=None, ge=800) 

413 logo_url: str | None = None 

414 favicon_url: str | None = None 

415 

416 model_config = {"extra": "allow"} 

417 

418 

419@dataclass(init=False) 

420class AdminRateLimitConfig(DomainModel): 

421 """Rate limiting configuration.""" 

422 

423 enabled: bool = Field(default=True) 

424 requests_per_minute: int = Field(default=60, ge=1) 

425 requests_per_hour: int = Field(default=1000, ge=1) 

426 burst_size: int = Field(default=10, ge=1) 

427 

428 # Per-action limits 

429 create_per_minute: int = Field(default=30) 

430 update_per_minute: int = Field(default=60) 

431 delete_per_minute: int = Field(default=20) 

432 bulk_per_minute: int = Field(default=5) 

433 

434 model_config = {"extra": "forbid"} 

435 

436 

437@dataclass(init=False) 

438class ContributorConfig(DomainModel): 

439 """Per-contributor enable/disable and options.""" 

440 

441 enabled: bool = Field(default=True) 

442 options: dict[str, Any] = Field(default_factory=dict) 

443 

444 

445@dataclass(init=False) 

446class DashboardLayoutConfig(DomainModel): 

447 """Dashboard layout configuration.""" 

448 

449 widget_refresh_default: int = Field(default=30) 

450 max_widgets: int = Field(default=20) 

451 layout: Literal["grid", "masonry"] = Field(default="grid") 

452 

453 

454@dataclass(init=False) 

455class TenancyConfig(DomainModel): 

456 """Multi-tenancy configuration for admin. 

457 

458 Controls tenant resolution, data isolation, and route scoping. 

459 

460 Attributes: 

461 enabled: Enable multi-tenancy support. 

462 tenant_field: Field name used for tenant data filtering (default ``"tenant_id"``). 

463 header_name: HTTP header name for tenant ID resolution (default ``"x-tenant-id"``). 

464 cookie_name: Cookie name for tenant ID resolution (default ``"admin_tenant"``). 

465 default_tenant_id: Fallback tenant ID when none can be resolved. 

466 route_prefix_template: If set (e.g. ``"{tenant}"``), routes are prefixed 

467 with the resolved tenant ID. Empty means no route prefix. 

468 """ 

469 

470 enabled: bool = Field(default=False) 

471 tenant_field: str = Field(default="tenant_id") 

472 header_name: str = Field(default="x-tenant-id") 

473 cookie_name: str = Field(default="admin_tenant") 

474 default_tenant_id: str = Field(default="") 

475 route_prefix_template: str = Field(default="") 

476 

477 model_config = {"extra": "forbid"} 

478 

479 

480# --------------------------------------------------------------------------- 

481# Data & Observability configs 

482# --------------------------------------------------------------------------- 

483 

484 

485@dataclass(init=False) 

486class AdminDataConfig(DomainModel): 

487 query_timeout_seconds: int = Field(default=5, ge=1, le=120) 

488 

489 

490@dataclass(init=False) 

491class AdminObservabilityConfig(DomainModel): 

492 metrics_enabled: bool = Field(default=True) 

493 high_cardinality_labels_enabled: bool = Field(default=False) 

494 

495 

496# --------------------------------------------------------------------------- 

497# Integration configs 

498# --------------------------------------------------------------------------- 

499 

500 

501@dataclass(init=False) 

502class CacheIntegrationConfig(DomainModel): 

503 """Configuration for the optional cache integration. 

504 

505 Controls ``lexigram-cache`` usage from admin resources. 

506 """ 

507 

508 enabled: bool = Field(default=True) 

509 default_ttl_seconds: int = Field(default=60, ge=1) 

510 key_prefix: str = Field(default="admin") 

511 

512 

513@dataclass(init=False) 

514class TasksIntegrationConfig(DomainModel): 

515 """Configuration for the optional tasks integration. 

516 

517 Controls ``lexigram-tasks`` usage from admin bulk actions. 

518 """ 

519 

520 enabled: bool = Field(default=True) 

521 bulk_threshold: int = Field(default=25, ge=1) 

522 

523 

524@dataclass(init=False) 

525class SearchIntegrationConfig(DomainModel): 

526 """Configuration for the optional search integration.""" 

527 

528 enabled: bool = Field(default=True) 

529 fallback_to_like: bool = Field(default=True) 

530 

531 

532@dataclass(init=False) 

533class ResilienceIntegrationConfig(DomainModel): 

534 """Configuration for the optional resilience integration.""" 

535 

536 enabled: bool = Field(default=True) 

537 retry_max_attempts: int = Field(default=3, ge=1) 

538 circuit_failure_threshold: int = Field(default=5, ge=1) 

539 

540 

541@dataclass(init=False) 

542class StorageIntegrationConfig(DomainModel): 

543 """Configuration for the optional storage integration.""" 

544 

545 enabled: bool = Field(default=True) 

546 presigned_url_expiry: int = Field(default=3600, ge=60) 

547 

548 

549@dataclass(init=False) 

550class FeaturesIntegrationConfig(DomainModel): 

551 """Configuration for the optional feature-flags integration.""" 

552 

553 enabled: bool = Field(default=True) 

554 

555 

556@dataclass(init=False) 

557class MonitorIntegrationConfig(DomainModel): 

558 """Configuration for the optional monitoring integration.""" 

559 

560 enabled: bool = Field(default=True) 

561 

562 

563@dataclass(init=False) 

564class AdminIntegrationsConfig(DomainModel): 

565 """Aggregate configuration for all optional integrations.""" 

566 

567 cache: CacheIntegrationConfig = Field(default_factory=CacheIntegrationConfig) 

568 tasks: TasksIntegrationConfig = Field(default_factory=TasksIntegrationConfig) 

569 search: SearchIntegrationConfig = Field(default_factory=SearchIntegrationConfig) 

570 resilience: ResilienceIntegrationConfig = Field( 

571 default_factory=ResilienceIntegrationConfig, 

572 ) 

573 storage: StorageIntegrationConfig = Field( 

574 default_factory=StorageIntegrationConfig, 

575 ) 

576 features: FeaturesIntegrationConfig = Field( 

577 default_factory=FeaturesIntegrationConfig, 

578 ) 

579 monitor: MonitorIntegrationConfig = Field(default_factory=MonitorIntegrationConfig) 

580 enabled: bool = Field(default=True) 

581 

582 model_config = {"extra": "forbid"} 

583 

584 

585@dataclass(init=False) 

586class FrameworkPagesConfig(DomainModel): 

587 """Framework management pages configuration.""" 

588 

589 enabled: bool = Field(default=True) 

590 require_permission: str = Field(default="admin:framework:access") 

591 

592 

593@dataclass(init=False) 

594@dataclass(init=False) 

595class ClusterSpec(DomainModel): 

596 """Declarative description of an extra cluster. 

597 

598 Registered clusters get a routable center at ``/admin/{slug}`` with 

599 its own landing page, namespaced child URLs, secondary sidebar, and 

600 primary-sidebar collapse — no per-cluster code required. 

601 """ 

602 

603 name: str = Field(description="Cluster key (used to derive slug/group when unset)") 

604 label: str = Field(description="Display label") 

605 icon: str | None = Field(default=None, description="Icon name") 

606 order: int = Field(default=0, description="Sort order in the registry") 

607 collapsible: bool = Field(default=True) 

608 collapsed_by_default: bool = Field(default=False) 

609 slug: str = Field(default="", description="URL segment (defaults to name)") 

610 group: str = Field(default="", description="Navigation group (defaults to name)") 

611 description: str | None = Field(default=None) 

612 

613 

614@dataclass(init=False) 

615class AdminClustersConfig(DomainModel): 

616 """Aggregate configuration for cluster centers.""" 

617 

618 extra: list[ClusterSpec] = Field( 

619 default_factory=list, 

620 description="Extra clusters beyond the built-in infrastructure cluster", 

621 ) 

622 

623 

624class AdminConfig(BaseConfig): 

625 """Complete admin configuration - SINGLE SOURCE OF TRUTH. 

626 

627 This model represents the full configuration hierarchy for lexigram-admin. 

628 Configuration is loaded from: 

629 1. Pydantic defaults (this model) 

630 2. application.yaml (admin: section) 

631 3. Environment variables (LEX_ADMIN_*) 

632 4. Runtime config (hot-reloadable) 

633 

634 Attributes: 

635 name: Configuration name (default: "admin") 

636 enabled: Whether admin module is enabled 

637 title: Admin UI title 

638 prefix: Admin URL prefix 

639 htmx_prefix: HTMX endpoint prefix 

640 api_prefix: API endpoint prefix 

641 static_prefix: Static files prefix 

642 require_auth: Require authentication 

643 debug: Debug mode 

644 templates_dir: Templates directory path 

645 static_dir: Static files directory path 

646 auth: Authentication settings 

647 features: Feature toggles 

648 """ 

649 

650 model_config = cast( 

651 "ConfigDict", 

652 { 

653 "env_prefix": ENV_PREFIX, 

654 "env_nested_delimiter": ENV_NESTED_DELIMITER, 

655 "extra": "ignore", 

656 }, 

657 ) 

658 

659 config_section: ClassVar[str] = "admin" 

660 

661 # Section identifier (used by config discovery) 

662 name: str = "admin" 

663 enabled: bool = True 

664 

665 # Core Settings 

666 title: str = Field(default="Lexigram Admin") 

667 prefix: str = Field(default="/admin") 

668 htmx_prefix: str = Field(default="/admin/htmx") 

669 api_prefix: str = Field(default="/admin/api") 

670 static_prefix: str = Field(default="/admin/static") 

671 

672 # Security 

673 require_auth: bool = Field(default=True) 

674 debug: bool = Field(default=False) 

675 

676 # Paths 

677 templates_dir: str | None = Field(default=None) 

678 static_dir: str | None = Field(default=None) 

679 

680 # Sub-configs 

681 auth: AdminAuthConfig = Field(default_factory=AdminAuthConfig) 

682 features: AdminFeaturesConfig = Field(default_factory=AdminFeaturesConfig) 

683 rbac: AdminRbacConfig = Field(default_factory=AdminRbacConfig) 

684 data: AdminDataConfig = Field(default_factory=AdminDataConfig) 

685 clusters: AdminClustersConfig = Field(default_factory=AdminClustersConfig) 

686 observability: AdminObservabilityConfig = Field( 

687 default_factory=AdminObservabilityConfig 

688 ) 

689 

690 ui: AdminUIConfig = Field(default_factory=AdminUIConfig) 

691 rate_limit: AdminRateLimitConfig = Field(default_factory=AdminRateLimitConfig) 

692 resource_defaults: ResourceDefaults = Field(default_factory=ResourceDefaults) 

693 table_defaults: TableDefaults = Field(default_factory=TableDefaults) 

694 form_defaults: FormDefaults = Field(default_factory=FormDefaults) 

695 

696 # Resource & Navigation 

697 resources: dict[str, ResourceYAMLConfig] = Field(default_factory=dict) 

698 navigation_groups: dict[str, AdminNavigationGroup] = Field(default_factory=dict) 

699 commands: list[dict[str, Any]] = Field(default_factory=list) 

700 extensions: dict[str, Any] = Field(default_factory=dict) 

701 

702 # Audit 

703 audit: AdminAuditConfig = Field(default_factory=AdminAuditConfig) 

704 

705 # Multi-tenancy 

706 tenancy: TenancyConfig = Field(default_factory=TenancyConfig) 

707 

708 # Optional integrations 

709 integrations: AdminIntegrationsConfig = Field( 

710 default_factory=AdminIntegrationsConfig, 

711 ) 

712 

713 # Contributor system 

714 contributors: dict[str, ContributorConfig] = Field(default_factory=dict) 

715 dashboard_layout: DashboardLayoutConfig = Field( 

716 default_factory=DashboardLayoutConfig 

717 ) 

718 framework_pages: FrameworkPagesConfig = Field(default_factory=FrameworkPagesConfig) 

719 

720 contributor_collision_mode: Literal["warn", "error"] = Field( 

721 default="warn", 

722 description=( 

723 "How to handle name collisions when multiple contributors register " 

724 "widgets, pages, or routes with the same name. 'warn' (default) " 

725 "logs a warning and keeps the first registration; 'error' raises " 

726 "at boot time." 

727 ), 

728 ) 

729 

730 strict_resource_resolution: bool = Field( 

731 default=True, 

732 description=( 

733 "When True (production default), resource/controller resolution " 

734 "failures during AdminProvider.boot() raise immediately. " 

735 "When False, failures are logged and resolution continues with " 

736 "the remaining resources/controllers. Set to False in dev only." 

737 ), 

738 ) 

739 

740 def get_resource_config(self, name: str) -> ResourceYAMLConfig: 

741 """Get resource config with fallback to defaults.""" 

742 return self.resources.get(name, ResourceYAMLConfig()) 

743 

744 def get_navigation_group(self, name: str) -> AdminNavigationGroup | None: 

745 """Get navigation group by name.""" 

746 return self.navigation_groups.get(name) 

747 

748 def is_feature_enabled(self, feature: str) -> bool: 

749 """Check if a feature is enabled.""" 

750 return getattr(self.features, feature, False) 

751 

752 

753@dataclass(init=False) 

754class AdminStorageConfig(DomainModel): 

755 """Configuration for admin file storage service.""" 

756 

757 base_path: str = Field( 

758 default="uploads", description="Base path for uploaded files" 

759 ) 

760 max_file_size: int = Field( 

761 default=10 * 1024 * 1024, 

762 description="Maximum allowed file size in bytes (default 10 MB)", 

763 ) 

764 allowed_content_types: list[str] = Field( 

765 default_factory=lambda: [ 

766 "image/jpeg", 

767 "image/png", 

768 "image/gif", 

769 "image/webp", 

770 "application/pdf", 

771 "text/plain", 

772 "text/csv", 

773 ], 

774 description="Allowed MIME content types for uploads", 

775 ) 

776 presigned_url_expiry: int = Field( 

777 default=3600, 

778 description="Default expiry for presigned URLs in seconds", 

779 ) 

780 

781 

782@dataclass(init=False) 

783class AdminNotificationConfig(DomainModel): 

784 """Configuration for admin notification service. 

785 

786 Controls email notification behaviour, delivery channels, and 

787 retry settings. 

788 """ 

789 

790 email_from: str = Field( 

791 default="admin@localhost", description="Sender email address" 

792 ) 

793 email_from_name: str = Field( 

794 default="Admin Panel", description="Sender display name" 

795 ) 

796 default_channel: str = Field( 

797 default="email", description="Default delivery channel" 

798 ) 

799 max_retries: int = Field(default=3, description="Maximum delivery retry attempts") 

800 retry_delay_seconds: int = Field(default=60, description="Seconds between retries") 

801 enabled: bool = Field(default=True, description="Enable notification sending") 

802 

803 

804def make_admin_config(**kwargs: Any) -> AdminConfig: 

805 """Helper to create admin config from kwargs.""" 

806 return AdminConfig(**kwargs) 

807 

808 

809def default_admin_config() -> AdminConfig: 

810 """Factory for default admin configuration.""" 

811 return AdminConfig() 

812 

813 

814__all__ = [ 

815 "AdminAuditConfig", 

816 "AdminAuthConfig", 

817 "AdminConfig", 

818 "AdminDataConfig", 

819 "AdminEmailOtpConfig", 

820 "AdminEmailVerificationConfig", 

821 "AdminFeaturesConfig", 

822 "AdminIntegrationsConfig", 

823 "AdminMfaConfig", 

824 "AdminNavigationGroup", 

825 "AdminNotificationConfig", 

826 "AdminObservabilityConfig", 

827 "AdminPasswordPolicyConfig", 

828 "AdminRateLimitConfig", 

829 "AdminRbacConfig", 

830 "AdminSecurityConfig", 

831 "AdminStorageConfig", 

832 "AdminUIConfig", 

833 "CacheIntegrationConfig", 

834 "ContributorConfig", 

835 "DashboardLayoutConfig", 

836 "FeaturesIntegrationConfig", 

837 "FormDefaults", 

838 "FrameworkPagesConfig", 

839 "MonitorIntegrationConfig", 

840 "ResilienceIntegrationConfig", 

841 "ResourceConfig", 

842 "ResourceDefaults", 

843 "ResourceYAMLConfig", 

844 "SearchIntegrationConfig", 

845 "StorageIntegrationConfig", 

846 "TableConfiguration", 

847 "TableDefaults", 

848 "TasksIntegrationConfig", 

849 "TenancyConfig", 

850 "default_admin_config", 

851 "make_admin_config", 

852]