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
« prev ^ index » next coverage.py v7.15.4, created at 2026-08-21 14:56 +0800
1"""Admin configuration models and factory helpers."""
3from __future__ import annotations
5from dataclasses import dataclass
6import os
7from typing import Any, ClassVar, Literal, cast
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
16@dataclass(init=False)
17class ResourceDefaults(DomainModel):
18 """Default configuration for all resources."""
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)
30 model_config = {"extra": "forbid"}
33@dataclass(init=False)
34class TableDefaults(DomainModel):
35 """Default configuration for DataTables."""
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)
45 model_config = {"extra": "forbid"}
48@dataclass(init=False)
49class FormDefaults(DomainModel):
50 """Default configuration for Forms."""
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)
58 model_config = {"extra": "forbid"}
61@dataclass(init=False)
62class ResourceYAMLConfig(DomainModel):
63 """Per-resource configuration from YAML."""
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
75 model_config = {"extra": "allow"}
78@dataclass(init=False)
79class AdminNavigationGroup(DomainModel):
80 """Navigation group configuration."""
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)
90 model_config = {"extra": "forbid"}
93@dataclass(init=False)
94class AdminFeaturesConfig(DomainModel):
95 """Feature flags for admin functionality."""
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)
103 # UX Features
104 optimistic_updates: bool = Field(default=True)
105 undo_redo: bool = Field(default=True)
106 autosave: bool = Field(default=False)
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)
115 model_config = {"extra": "allow"}
118@dataclass(init=False)
119class AdminRbacConfig(DomainModel):
120 """RBAC editing-page configuration."""
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")
127@dataclass(init=False)
128class AdminPasswordPolicyConfig(DomainModel):
129 """Password policy configuration for admin authentication.
131 Follows NIST SP 800-63B guidelines.
132 """
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)
143 model_config = {"extra": "forbid"}
146@dataclass(init=False)
147class AdminSecurityConfig(DomainModel):
148 """Security hardening configuration for admin authentication.
150 Controls rate limiting, progressive lockout, and setup token protection.
151 """
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)
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)
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 )
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``.
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.
189 Args:
190 data: Raw input dict (or already-built instance) before field
191 assignment.
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
207 model_config = {"extra": "forbid"}
210@dataclass(init=False)
211class AdminMfaConfig(DomainModel):
212 """Two-factor authentication (TOTP) configuration.
214 Controls whether TOTP 2FA is offered, the issuer label embedded in
215 provisioning URIs, and the allowed clock-skew window for codes.
216 """
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 )
235@dataclass(init=False)
236class AdminEmailOtpConfig(DomainModel):
237 """Email one-time-password (login factor) configuration.
239 Controls whether the email-OTP factor is available, how long a code
240 stays valid, and the minimum interval between sends.
241 """
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 )
258@dataclass(init=False)
259class AdminEmailVerificationConfig(DomainModel):
260 """Email verification (login gate) configuration.
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 """
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 )
279@dataclass(init=False)
280class AdminRegistrationConfig(DomainModel):
281 """Self-service registration configuration.
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 """
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 )
298@dataclass(init=False)
299class AdminAuthConfig(DomainModel):
300 """Authentication configuration."""
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
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 )
338 # Users and Roles (Sync)
339 users: list[Any] = Field(default_factory=list)
340 roles: dict[str, Any] = Field(default_factory=dict)
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")
346 # OAuth/SSO (optional)
347 oauth_enabled: bool = Field(default=False)
348 oauth_providers: list[str] = Field(default_factory=list)
350 model_config = {"extra": "allow"}
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 )
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 )
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 )
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")
389 return self
392@dataclass(init=False)
393class AdminAuditConfig(DomainModel):
394 """Audit logging configuration."""
396 read_audit_enabled: bool = Field(
397 default=False,
398 description="Log read operations (off by default; compliance mode only).",
399 )
401 model_config = {"extra": "forbid"}
404@dataclass(init=False)
405class AdminUIConfig(DomainModel):
406 """UI/Theme configuration."""
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
416 model_config = {"extra": "allow"}
419@dataclass(init=False)
420class AdminRateLimitConfig(DomainModel):
421 """Rate limiting configuration."""
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)
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)
434 model_config = {"extra": "forbid"}
437@dataclass(init=False)
438class ContributorConfig(DomainModel):
439 """Per-contributor enable/disable and options."""
441 enabled: bool = Field(default=True)
442 options: dict[str, Any] = Field(default_factory=dict)
445@dataclass(init=False)
446class DashboardLayoutConfig(DomainModel):
447 """Dashboard layout configuration."""
449 widget_refresh_default: int = Field(default=30)
450 max_widgets: int = Field(default=20)
451 layout: Literal["grid", "masonry"] = Field(default="grid")
454@dataclass(init=False)
455class TenancyConfig(DomainModel):
456 """Multi-tenancy configuration for admin.
458 Controls tenant resolution, data isolation, and route scoping.
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 """
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="")
477 model_config = {"extra": "forbid"}
480# ---------------------------------------------------------------------------
481# Data & Observability configs
482# ---------------------------------------------------------------------------
485@dataclass(init=False)
486class AdminDataConfig(DomainModel):
487 query_timeout_seconds: int = Field(default=5, ge=1, le=120)
490@dataclass(init=False)
491class AdminObservabilityConfig(DomainModel):
492 metrics_enabled: bool = Field(default=True)
493 high_cardinality_labels_enabled: bool = Field(default=False)
496# ---------------------------------------------------------------------------
497# Integration configs
498# ---------------------------------------------------------------------------
501@dataclass(init=False)
502class CacheIntegrationConfig(DomainModel):
503 """Configuration for the optional cache integration.
505 Controls ``lexigram-cache`` usage from admin resources.
506 """
508 enabled: bool = Field(default=True)
509 default_ttl_seconds: int = Field(default=60, ge=1)
510 key_prefix: str = Field(default="admin")
513@dataclass(init=False)
514class TasksIntegrationConfig(DomainModel):
515 """Configuration for the optional tasks integration.
517 Controls ``lexigram-tasks`` usage from admin bulk actions.
518 """
520 enabled: bool = Field(default=True)
521 bulk_threshold: int = Field(default=25, ge=1)
524@dataclass(init=False)
525class SearchIntegrationConfig(DomainModel):
526 """Configuration for the optional search integration."""
528 enabled: bool = Field(default=True)
529 fallback_to_like: bool = Field(default=True)
532@dataclass(init=False)
533class ResilienceIntegrationConfig(DomainModel):
534 """Configuration for the optional resilience integration."""
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)
541@dataclass(init=False)
542class StorageIntegrationConfig(DomainModel):
543 """Configuration for the optional storage integration."""
545 enabled: bool = Field(default=True)
546 presigned_url_expiry: int = Field(default=3600, ge=60)
549@dataclass(init=False)
550class FeaturesIntegrationConfig(DomainModel):
551 """Configuration for the optional feature-flags integration."""
553 enabled: bool = Field(default=True)
556@dataclass(init=False)
557class MonitorIntegrationConfig(DomainModel):
558 """Configuration for the optional monitoring integration."""
560 enabled: bool = Field(default=True)
563@dataclass(init=False)
564class AdminIntegrationsConfig(DomainModel):
565 """Aggregate configuration for all optional integrations."""
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)
582 model_config = {"extra": "forbid"}
585@dataclass(init=False)
586class FrameworkPagesConfig(DomainModel):
587 """Framework management pages configuration."""
589 enabled: bool = Field(default=True)
590 require_permission: str = Field(default="admin:framework:access")
593@dataclass(init=False)
594@dataclass(init=False)
595class ClusterSpec(DomainModel):
596 """Declarative description of an extra cluster.
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 """
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)
614@dataclass(init=False)
615class AdminClustersConfig(DomainModel):
616 """Aggregate configuration for cluster centers."""
618 extra: list[ClusterSpec] = Field(
619 default_factory=list,
620 description="Extra clusters beyond the built-in infrastructure cluster",
621 )
624class AdminConfig(BaseConfig):
625 """Complete admin configuration - SINGLE SOURCE OF TRUTH.
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)
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 """
650 model_config = cast(
651 "ConfigDict",
652 {
653 "env_prefix": ENV_PREFIX,
654 "env_nested_delimiter": ENV_NESTED_DELIMITER,
655 "extra": "ignore",
656 },
657 )
659 config_section: ClassVar[str] = "admin"
661 # Section identifier (used by config discovery)
662 name: str = "admin"
663 enabled: bool = True
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")
672 # Security
673 require_auth: bool = Field(default=True)
674 debug: bool = Field(default=False)
676 # Paths
677 templates_dir: str | None = Field(default=None)
678 static_dir: str | None = Field(default=None)
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 )
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)
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)
702 # Audit
703 audit: AdminAuditConfig = Field(default_factory=AdminAuditConfig)
705 # Multi-tenancy
706 tenancy: TenancyConfig = Field(default_factory=TenancyConfig)
708 # Optional integrations
709 integrations: AdminIntegrationsConfig = Field(
710 default_factory=AdminIntegrationsConfig,
711 )
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)
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 )
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 )
740 def get_resource_config(self, name: str) -> ResourceYAMLConfig:
741 """Get resource config with fallback to defaults."""
742 return self.resources.get(name, ResourceYAMLConfig())
744 def get_navigation_group(self, name: str) -> AdminNavigationGroup | None:
745 """Get navigation group by name."""
746 return self.navigation_groups.get(name)
748 def is_feature_enabled(self, feature: str) -> bool:
749 """Check if a feature is enabled."""
750 return getattr(self.features, feature, False)
753@dataclass(init=False)
754class AdminStorageConfig(DomainModel):
755 """Configuration for admin file storage service."""
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 )
782@dataclass(init=False)
783class AdminNotificationConfig(DomainModel):
784 """Configuration for admin notification service.
786 Controls email notification behaviour, delivery channels, and
787 retry settings.
788 """
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")
804def make_admin_config(**kwargs: Any) -> AdminConfig:
805 """Helper to create admin config from kwargs."""
806 return AdminConfig(**kwargs)
809def default_admin_config() -> AdminConfig:
810 """Factory for default admin configuration."""
811 return AdminConfig()
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]