fastapi-admin-kit 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- fastapi_admin_kit/__init__.py +73 -0
- fastapi_admin_kit/actions/__init__.py +63 -0
- fastapi_admin_kit/actions/base.py +68 -0
- fastapi_admin_kit/actions/registry.py +43 -0
- fastapi_admin_kit/admin/__init__.py +17 -0
- fastapi_admin_kit/admin/admin_config.py +95 -0
- fastapi_admin_kit/admin/admin_database.py +138 -0
- fastapi_admin_kit/admin/admin_router.py +74 -0
- fastapi_admin_kit/admin/admin_template.py +203 -0
- fastapi_admin_kit/admin/builtin_models.py +284 -0
- fastapi_admin_kit/admin/core.py +1036 -0
- fastapi_admin_kit/admin/decorators.py +70 -0
- fastapi_admin_kit/admin/state.py +76 -0
- fastapi_admin_kit/admin.py +728 -0
- fastapi_admin_kit/api/__init__.py +44 -0
- fastapi_admin_kit/api/auth.py +342 -0
- fastapi_admin_kit/api/crud.py +128 -0
- fastapi_admin_kit/api/deps.py +79 -0
- fastapi_admin_kit/api/roles.py +128 -0
- fastapi_admin_kit/api/schema_generator.py +171 -0
- fastapi_admin_kit/api/schemas.py +81 -0
- fastapi_admin_kit/api/search.py +132 -0
- fastapi_admin_kit/audit/__init__.py +36 -0
- fastapi_admin_kit/audit/context.py +62 -0
- fastapi_admin_kit/audit/diff.py +77 -0
- fastapi_admin_kit/audit/event_bus.py +96 -0
- fastapi_admin_kit/audit/events.py +48 -0
- fastapi_admin_kit/audit/listener.py +159 -0
- fastapi_admin_kit/audit/logger.py +28 -0
- fastapi_admin_kit/audit/middleware.py +39 -0
- fastapi_admin_kit/audit/models.py +53 -0
- fastapi_admin_kit/audit/sqlalchemy_logger.py +58 -0
- fastapi_admin_kit/auth/__init__.py +34 -0
- fastapi_admin_kit/auth/backend.py +95 -0
- fastapi_admin_kit/auth/csrf.py +240 -0
- fastapi_admin_kit/auth/dependencies.py +150 -0
- fastapi_admin_kit/auth/identity.py +181 -0
- fastapi_admin_kit/auth/models.py +246 -0
- fastapi_admin_kit/auth/password.py +35 -0
- fastapi_admin_kit/auth/permissions.py +205 -0
- fastapi_admin_kit/auth/protocol.py +22 -0
- fastapi_admin_kit/auth/ratelimit.py +88 -0
- fastapi_admin_kit/auth/router.py +10 -0
- fastapi_admin_kit/auth/session.py +79 -0
- fastapi_admin_kit/auth/totp.py +83 -0
- fastapi_admin_kit/auth/views.py +165 -0
- fastapi_admin_kit/cli.py +229 -0
- fastapi_admin_kit/config/__init__.py +19 -0
- fastapi_admin_kit/config/audit.py +18 -0
- fastapi_admin_kit/config/auth.py +54 -0
- fastapi_admin_kit/config/behavior.py +27 -0
- fastapi_admin_kit/config/nav.py +32 -0
- fastapi_admin_kit/config/storage.py +22 -0
- fastapi_admin_kit/config/theme.py +215 -0
- fastapi_admin_kit/config/ui.py +147 -0
- fastapi_admin_kit/dashboard/__init__.py +64 -0
- fastapi_admin_kit/db.py +133 -0
- fastapi_admin_kit/exceptions.py +5 -0
- fastapi_admin_kit/field_types.py +81 -0
- fastapi_admin_kit/filters/__init__.py +21 -0
- fastapi_admin_kit/filters/base.py +170 -0
- fastapi_admin_kit/filters/registry.py +68 -0
- fastapi_admin_kit/flash.py +45 -0
- fastapi_admin_kit/form/__init__.py +1 -0
- fastapi_admin_kit/form/pipeline.py +106 -0
- fastapi_admin_kit/inspection/__init__.py +117 -0
- fastapi_admin_kit/inspection/registry.py +253 -0
- fastapi_admin_kit/inspection.py +115 -0
- fastapi_admin_kit/modeladmin.py +375 -0
- fastapi_admin_kit/models/__init__.py +7 -0
- fastapi_admin_kit/models/base.py +7 -0
- fastapi_admin_kit/nav.py +208 -0
- fastapi_admin_kit/pagination/__init__.py +14 -0
- fastapi_admin_kit/pagination/base.py +40 -0
- fastapi_admin_kit/pagination/cursor.py +97 -0
- fastapi_admin_kit/pagination/dynamic.py +48 -0
- fastapi_admin_kit/pagination/offset.py +42 -0
- fastapi_admin_kit/plugins/__init__.py +1 -0
- fastapi_admin_kit/py.typed +0 -0
- fastapi_admin_kit/registry/__init__.py +5 -0
- fastapi_admin_kit/registry/core.py +287 -0
- fastapi_admin_kit/registry/validation.py +107 -0
- fastapi_admin_kit/registry.py +15 -0
- fastapi_admin_kit/router.py +335 -0
- fastapi_admin_kit/static/css/admin.css +4736 -0
- fastapi_admin_kit/static/css/presets.css +317 -0
- fastapi_admin_kit/static/css/tokens.css +217 -0
- fastapi_admin_kit/static/css/variables.css +74 -0
- fastapi_admin_kit/static/icons/heroicons.svg +160 -0
- fastapi_admin_kit/static/js/admin.js +692 -0
- fastapi_admin_kit/static/js/htmx-config.js +42 -0
- fastapi_admin_kit/storage/__init__.py +6 -0
- fastapi_admin_kit/storage/base.py +48 -0
- fastapi_admin_kit/storage/local.py +73 -0
- fastapi_admin_kit/templates/base.html +142 -0
- fastapi_admin_kit/templates/macros/form_fields.html +660 -0
- fastapi_admin_kit/templates/macros/icons.html +50 -0
- fastapi_admin_kit/templates/macros/table.html +108 -0
- fastapi_admin_kit/templates/macros/widgets.html +159 -0
- fastapi_admin_kit/templates/pages/2fa/setup.html +122 -0
- fastapi_admin_kit/templates/pages/2fa/verify.html +55 -0
- fastapi_admin_kit/templates/pages/audit_detail.html +122 -0
- fastapi_admin_kit/templates/pages/audit_log.html +102 -0
- fastapi_admin_kit/templates/pages/dashboard.html +295 -0
- fastapi_admin_kit/templates/pages/detail.html +183 -0
- fastapi_admin_kit/templates/pages/form.html +119 -0
- fastapi_admin_kit/templates/pages/list.html +277 -0
- fastapi_admin_kit/templates/pages/login.html +85 -0
- fastapi_admin_kit/templates/pages/profile/password.html +78 -0
- fastapi_admin_kit/templates/pages/profile/profile.html +73 -0
- fastapi_admin_kit/templates/pages/role_form.html +75 -0
- fastapi_admin_kit/templates/pages/roles/form.html +117 -0
- fastapi_admin_kit/templates/pages/roles/list.html +69 -0
- fastapi_admin_kit/templates/pages/roles.html +77 -0
- fastapi_admin_kit/templates/pages/settings/theme.html +255 -0
- fastapi_admin_kit/templates/pages/users/form.html +229 -0
- fastapi_admin_kit/templates/pages/users/list.html +83 -0
- fastapi_admin_kit/templates/partials/command_palette.html +52 -0
- fastapi_admin_kit/templates/partials/field_wrapper.html +2 -0
- fastapi_admin_kit/templates/partials/flash_messages.html +39 -0
- fastapi_admin_kit/templates/partials/head.html +21 -0
- fastapi_admin_kit/templates/partials/head_minimal.html +18 -0
- fastapi_admin_kit/templates/partials/list_table.html +178 -0
- fastapi_admin_kit/templates/partials/mobile_backdrop.html +2 -0
- fastapi_admin_kit/templates/partials/pagination.html +82 -0
- fastapi_admin_kit/templates/partials/permission_widget.html +86 -0
- fastapi_admin_kit/templates/partials/scripts.html +13 -0
- fastapi_admin_kit/templates/partials/sidebar.html +94 -0
- fastapi_admin_kit/templates/partials/topbar.html +95 -0
- fastapi_admin_kit/types.py +145 -0
- fastapi_admin_kit/validation.py +43 -0
- fastapi_admin_kit/views/__init__.py +78 -0
- fastapi_admin_kit/views/audit.py +134 -0
- fastapi_admin_kit/views/bulk.py +28 -0
- fastapi_admin_kit/views/class_views.py +1040 -0
- fastapi_admin_kit/views/context.py +588 -0
- fastapi_admin_kit/views/dashboard.py +162 -0
- fastapi_admin_kit/views/delete.py +31 -0
- fastapi_admin_kit/views/extra.py +65 -0
- fastapi_admin_kit/views/factory.py +667 -0
- fastapi_admin_kit/views/form.py +159 -0
- fastapi_admin_kit/views/list.py +28 -0
- fastapi_admin_kit/views/profile.py +219 -0
- fastapi_admin_kit/views/protocols.py +54 -0
- fastapi_admin_kit/views/renderers.py +634 -0
- fastapi_admin_kit/views/roles.py +230 -0
- fastapi_admin_kit/views/search.py +31 -0
- fastapi_admin_kit/views/settings.py +31 -0
- fastapi_admin_kit/views/sidebar.py +101 -0
- fastapi_admin_kit/views/totp.py +249 -0
- fastapi_admin_kit/views/users.py +347 -0
- fastapi_admin_kit/views.py +117 -0
- fastapi_admin_kit/widgets/__init__.py +44 -0
- fastapi_admin_kit/widgets/base.py +44 -0
- fastapi_admin_kit/widgets/inputs.py +363 -0
- fastapi_admin_kit/widgets/registry.py +110 -0
- fastapi_admin_kit/widgets/relation.py +70 -0
- fastapi_admin_kit/widgets/resolver.py +102 -0
- fastapi_admin_kit-0.1.0.dist-info/METADATA +210 -0
- fastapi_admin_kit-0.1.0.dist-info/RECORD +163 -0
- fastapi_admin_kit-0.1.0.dist-info/WHEEL +4 -0
- fastapi_admin_kit-0.1.0.dist-info/entry_points.txt +3 -0
- fastapi_admin_kit-0.1.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
"""FastAPI Admin Kit — Drop-in admin panel for FastAPI + SQLAlchemy apps."""
|
|
2
|
+
|
|
3
|
+
from fastapi_admin_kit.admin import Admin
|
|
4
|
+
from fastapi_admin_kit.admin.decorators import column
|
|
5
|
+
from fastapi_admin_kit.exceptions import ConfigError
|
|
6
|
+
from fastapi_admin_kit.nav import (
|
|
7
|
+
BuiltNavGroup,
|
|
8
|
+
BuiltNavItem,
|
|
9
|
+
DefaultSidebarBuilder,
|
|
10
|
+
NavGroupConfig,
|
|
11
|
+
NavItemConfig,
|
|
12
|
+
SidebarBuilder,
|
|
13
|
+
)
|
|
14
|
+
from fastapi_admin_kit.registry import AdminRegistry, RegisteredModel
|
|
15
|
+
from fastapi_admin_kit.types import (
|
|
16
|
+
ColumnMeta,
|
|
17
|
+
ExtraField,
|
|
18
|
+
FieldMeta,
|
|
19
|
+
FieldRenderContext,
|
|
20
|
+
FieldsetContext,
|
|
21
|
+
FieldsetSpec,
|
|
22
|
+
FormContext,
|
|
23
|
+
PermissionSet,
|
|
24
|
+
RelationMeta,
|
|
25
|
+
SeedRole,
|
|
26
|
+
)
|
|
27
|
+
from fastapi_admin_kit.views import (
|
|
28
|
+
AdminExtra,
|
|
29
|
+
BaseView,
|
|
30
|
+
BulkView,
|
|
31
|
+
CreateView,
|
|
32
|
+
DeleteView,
|
|
33
|
+
EditView,
|
|
34
|
+
ListView,
|
|
35
|
+
ModelAdmin,
|
|
36
|
+
SearchView,
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
__all__ = [
|
|
40
|
+
"Admin",
|
|
41
|
+
"AdminRegistry",
|
|
42
|
+
"ConfigError",
|
|
43
|
+
"RegisteredModel",
|
|
44
|
+
"ModelAdmin",
|
|
45
|
+
"column",
|
|
46
|
+
"BuiltNavGroup",
|
|
47
|
+
"BuiltNavItem",
|
|
48
|
+
"DefaultSidebarBuilder",
|
|
49
|
+
"NavGroupConfig",
|
|
50
|
+
"NavItemConfig",
|
|
51
|
+
"SidebarBuilder",
|
|
52
|
+
"ColumnMeta",
|
|
53
|
+
"RelationMeta",
|
|
54
|
+
"FieldMeta",
|
|
55
|
+
"PermissionSet",
|
|
56
|
+
"SeedRole",
|
|
57
|
+
"ExtraField",
|
|
58
|
+
"FieldRenderContext",
|
|
59
|
+
"FieldsetContext",
|
|
60
|
+
"FieldsetSpec",
|
|
61
|
+
"FormContext",
|
|
62
|
+
# View classes
|
|
63
|
+
"BaseView",
|
|
64
|
+
"ListView",
|
|
65
|
+
"CreateView",
|
|
66
|
+
"EditView",
|
|
67
|
+
"DeleteView",
|
|
68
|
+
"BulkView",
|
|
69
|
+
"SearchView",
|
|
70
|
+
# Per-model assets
|
|
71
|
+
"AdminExtra",
|
|
72
|
+
]
|
|
73
|
+
__version__ = "0.1.0"
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
"""Bulk actions for list views."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Callable
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from fastapi_admin_kit.actions.base import Action, ModelAction
|
|
9
|
+
from fastapi_admin_kit.actions.registry import ActionRegistry
|
|
10
|
+
|
|
11
|
+
__all__ = ["Action", "ModelAction", "ActionRegistry", "action"]
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def action(
|
|
15
|
+
name: str = "",
|
|
16
|
+
label: str = "",
|
|
17
|
+
confirmation_message: str = "",
|
|
18
|
+
icon: str | None = None,
|
|
19
|
+
variant: str = "default",
|
|
20
|
+
permissions: list[str] | None = None,
|
|
21
|
+
location: str = "list",
|
|
22
|
+
description: str = "",
|
|
23
|
+
) -> Callable:
|
|
24
|
+
"""Decorator to register an action on a ModelAdmin class.
|
|
25
|
+
|
|
26
|
+
Usage::
|
|
27
|
+
|
|
28
|
+
@admin.register(Product)
|
|
29
|
+
class ProductAdmin(ModelAdmin):
|
|
30
|
+
@action(
|
|
31
|
+
description="Export selected products",
|
|
32
|
+
icon="arrow-down-tray",
|
|
33
|
+
variant="primary",
|
|
34
|
+
)
|
|
35
|
+
async def export_products(self, objects, request):
|
|
36
|
+
...
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
def decorator(fn: Callable) -> Callable:
|
|
40
|
+
action_name = name or fn.__name__
|
|
41
|
+
action_label = label or action_name.replace("_", " ").title()
|
|
42
|
+
|
|
43
|
+
class DecoratedAction(Action):
|
|
44
|
+
def __init__(self) -> None:
|
|
45
|
+
super().__init__(
|
|
46
|
+
name=action_name,
|
|
47
|
+
label=action_label,
|
|
48
|
+
confirmation_message=confirmation_message,
|
|
49
|
+
icon=icon,
|
|
50
|
+
variant=variant,
|
|
51
|
+
permissions=permissions or [],
|
|
52
|
+
location=location,
|
|
53
|
+
description=description,
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
async def execute(self, objects: list[Any], request: Any) -> None:
|
|
57
|
+
await fn(self, objects, request)
|
|
58
|
+
|
|
59
|
+
fn._admin_action = DecoratedAction
|
|
60
|
+
fn._admin_action_name = action_name
|
|
61
|
+
return fn
|
|
62
|
+
|
|
63
|
+
return decorator
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
"""Action ABCs."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from abc import ABC, abstractmethod
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from fastapi import Request
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class Action(ABC):
|
|
12
|
+
"""Abstract base class for admin actions.
|
|
13
|
+
|
|
14
|
+
Supports list-level, row-level, detail-level, and submit-line actions
|
|
15
|
+
with Unfold-style icon, variant, and permission configuration.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
def __init__(
|
|
19
|
+
self,
|
|
20
|
+
name: str,
|
|
21
|
+
label: str = "",
|
|
22
|
+
confirmation_message: str = "",
|
|
23
|
+
icon: str | None = None,
|
|
24
|
+
variant: str = "default",
|
|
25
|
+
permissions: list[str] | None = None,
|
|
26
|
+
location: str = "list",
|
|
27
|
+
description: str = "",
|
|
28
|
+
) -> None:
|
|
29
|
+
self.name = name
|
|
30
|
+
self.label = label or name.replace("_", " ").title()
|
|
31
|
+
self.confirmation_message = confirmation_message
|
|
32
|
+
self.icon = icon
|
|
33
|
+
self.variant = variant # default, primary, danger, success, warning
|
|
34
|
+
self.permissions = permissions or []
|
|
35
|
+
self.location = location # list, row, detail, submit_line
|
|
36
|
+
self.description = description
|
|
37
|
+
|
|
38
|
+
@abstractmethod
|
|
39
|
+
async def execute(
|
|
40
|
+
self, objects: list[Any], request: Request | None
|
|
41
|
+
) -> None:
|
|
42
|
+
"""Run the action against the selected objects."""
|
|
43
|
+
...
|
|
44
|
+
|
|
45
|
+
def get_confirmation_message(self) -> str:
|
|
46
|
+
return self.confirmation_message or f"Run {self.label}?"
|
|
47
|
+
|
|
48
|
+
def has_permission(self, user: Any) -> bool:
|
|
49
|
+
"""Check if the user has permission to run this action."""
|
|
50
|
+
if not self.permissions:
|
|
51
|
+
return True
|
|
52
|
+
if getattr(user, "is_superuser", False):
|
|
53
|
+
return True
|
|
54
|
+
user_perms = getattr(user, "permissions", []) or []
|
|
55
|
+
return any(p in user_perms for p in self.permissions)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class ModelAction(Action):
|
|
59
|
+
"""Action that operates on a single model instance (row/detail actions)."""
|
|
60
|
+
|
|
61
|
+
async def execute_single(
|
|
62
|
+
self, obj: Any, request: Request | None
|
|
63
|
+
) -> None:
|
|
64
|
+
"""Run the action on a single object.
|
|
65
|
+
|
|
66
|
+
Override this instead of execute for single-object actions.
|
|
67
|
+
"""
|
|
68
|
+
await self.execute([obj], request)
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"""Action registry."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from fastapi import Request
|
|
8
|
+
|
|
9
|
+
from fastapi_admin_kit.actions.base import Action
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class DeleteSelectedAction(Action):
|
|
13
|
+
"""Built-in bulk delete action."""
|
|
14
|
+
|
|
15
|
+
def __init__(self) -> None:
|
|
16
|
+
super().__init__(
|
|
17
|
+
name="delete_selected",
|
|
18
|
+
label="Delete selected",
|
|
19
|
+
confirmation_message="Are you sure you want to delete the selected items?",
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
async def execute(
|
|
23
|
+
self, objects: list[Any], request: Request | None
|
|
24
|
+
) -> None:
|
|
25
|
+
# Actual deletion is handled by the existing bulk handler in views/factory.py
|
|
26
|
+
pass
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class ActionRegistry:
|
|
30
|
+
"""Registry for custom actions per model."""
|
|
31
|
+
|
|
32
|
+
def __init__(self) -> None:
|
|
33
|
+
self._actions: dict[str, list[Action]] = {}
|
|
34
|
+
|
|
35
|
+
def register(self, model_name: str, action: Action) -> None:
|
|
36
|
+
self._actions.setdefault(model_name, []).append(action)
|
|
37
|
+
|
|
38
|
+
def get_actions(self, model_name: str) -> list[Action]:
|
|
39
|
+
return self._actions.get(model_name, []).copy()
|
|
40
|
+
|
|
41
|
+
def auto_generate(self, model_name: str) -> list[Action]:
|
|
42
|
+
"""Auto-generate default bulk actions for a model."""
|
|
43
|
+
return [DeleteSelectedAction()]
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"""Admin component classes for FastAPI Admin Kit."""
|
|
2
|
+
|
|
3
|
+
from fastapi_admin_kit.admin.admin_config import AdminConfig
|
|
4
|
+
from fastapi_admin_kit.admin.admin_database import AdminDatabase
|
|
5
|
+
from fastapi_admin_kit.admin.admin_router import AdminRouter
|
|
6
|
+
from fastapi_admin_kit.admin.admin_template import AdminTemplate
|
|
7
|
+
from fastapi_admin_kit.admin.core import Admin
|
|
8
|
+
from fastapi_admin_kit.admin.state import AdminState
|
|
9
|
+
|
|
10
|
+
__all__ = [
|
|
11
|
+
"Admin",
|
|
12
|
+
"AdminConfig",
|
|
13
|
+
"AdminDatabase",
|
|
14
|
+
"AdminRouter",
|
|
15
|
+
"AdminState",
|
|
16
|
+
"AdminTemplate",
|
|
17
|
+
]
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
"""Admin configuration orchestration."""
|
|
2
|
+
|
|
3
|
+
from typing import TYPE_CHECKING
|
|
4
|
+
|
|
5
|
+
from fastapi_admin_kit.config import (
|
|
6
|
+
AuditConfig,
|
|
7
|
+
AuthConfig,
|
|
8
|
+
BehaviorConfig,
|
|
9
|
+
NavConfig,
|
|
10
|
+
StorageConfig,
|
|
11
|
+
UIConfig,
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
if TYPE_CHECKING:
|
|
15
|
+
pass
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class AdminConfig:
|
|
19
|
+
"""Orchestrates all configuration classes with validation."""
|
|
20
|
+
|
|
21
|
+
def __init__(
|
|
22
|
+
self,
|
|
23
|
+
ui: UIConfig | None = None,
|
|
24
|
+
auth: AuthConfig | None = None,
|
|
25
|
+
audit: AuditConfig | None = None,
|
|
26
|
+
behavior: BehaviorConfig | None = None,
|
|
27
|
+
storage: StorageConfig | None = None,
|
|
28
|
+
nav: NavConfig | None = None,
|
|
29
|
+
):
|
|
30
|
+
self.ui = ui or UIConfig()
|
|
31
|
+
self.auth = auth or AuthConfig()
|
|
32
|
+
self.audit = audit or AuditConfig()
|
|
33
|
+
self.behavior = behavior or BehaviorConfig()
|
|
34
|
+
self.storage = storage or StorageConfig()
|
|
35
|
+
self.nav = nav or NavConfig()
|
|
36
|
+
|
|
37
|
+
def validate_all(self) -> None:
|
|
38
|
+
"""Validate all configuration components."""
|
|
39
|
+
self.auth.validate_auth_model()
|
|
40
|
+
self.audit.validate_audit_config()
|
|
41
|
+
self.storage.validate_storage_config()
|
|
42
|
+
self.nav.validate_nav_config()
|
|
43
|
+
|
|
44
|
+
def get_ui_context(self) -> dict:
|
|
45
|
+
"""Get UI configuration for template context."""
|
|
46
|
+
return self.ui.apply_to_template_context()
|
|
47
|
+
|
|
48
|
+
def get_branding_config(self) -> dict:
|
|
49
|
+
"""Get branding configuration."""
|
|
50
|
+
return {
|
|
51
|
+
"title": self.ui.title,
|
|
52
|
+
"logo_url": self.ui.logo_url,
|
|
53
|
+
"favicon_url": self.ui.favicon_url,
|
|
54
|
+
"primary_color": self.ui.primary_color,
|
|
55
|
+
"primary_color_dark": self.ui.primary_color_dark,
|
|
56
|
+
"dark_mode_default": self.ui.dark_mode_default,
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
def get_session_config(self) -> dict:
|
|
60
|
+
"""Get session configuration."""
|
|
61
|
+
return {
|
|
62
|
+
"session_ttl": self.auth.session_ttl,
|
|
63
|
+
"session_cookie_name": self.auth.session_cookie_name,
|
|
64
|
+
"session_secure": self.auth.session_secure,
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
def get_audit_config(self) -> dict:
|
|
68
|
+
"""Get audit configuration."""
|
|
69
|
+
return {
|
|
70
|
+
"audit_retention_days": self.audit.audit_retention_days,
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
def get_storage_config(self) -> dict:
|
|
74
|
+
"""Get storage configuration."""
|
|
75
|
+
return {
|
|
76
|
+
"uploads_url": self.storage.uploads_url,
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
def get_behavior_config(self) -> dict:
|
|
80
|
+
"""Get behavior configuration."""
|
|
81
|
+
return {
|
|
82
|
+
"auto_discover": self.behavior.auto_discover,
|
|
83
|
+
"dashboard_stats": self.behavior.dashboard_stats,
|
|
84
|
+
"dashboard_charts": self.behavior.dashboard_charts,
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
def get_nav_config(self) -> dict:
|
|
88
|
+
"""Get navigation configuration."""
|
|
89
|
+
return {
|
|
90
|
+
"nav_groups": self.nav.nav_groups,
|
|
91
|
+
"sidebar_builder": self.nav.sidebar_builder,
|
|
92
|
+
"require_tags": self.nav.require_tags,
|
|
93
|
+
"dashboard_permission": self.nav.dashboard_permission,
|
|
94
|
+
"settings_permission": self.nav.settings_permission,
|
|
95
|
+
}
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
"""Admin database setup and initialization."""
|
|
2
|
+
|
|
3
|
+
from typing import TYPE_CHECKING, Any
|
|
4
|
+
|
|
5
|
+
if TYPE_CHECKING:
|
|
6
|
+
pass
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class AdminDatabase:
|
|
10
|
+
"""Handles database setup, table creation, and role seeding."""
|
|
11
|
+
|
|
12
|
+
def __init__(
|
|
13
|
+
self,
|
|
14
|
+
engine: Any | None = None,
|
|
15
|
+
base: Any | None = None,
|
|
16
|
+
):
|
|
17
|
+
self.engine = engine
|
|
18
|
+
self.base = base
|
|
19
|
+
|
|
20
|
+
async def _create_tables(self) -> None:
|
|
21
|
+
"""Create all admin database tables (async-safe)."""
|
|
22
|
+
from sqlalchemy.ext.asyncio import AsyncEngine
|
|
23
|
+
|
|
24
|
+
from fastapi_admin_kit.audit import models as _audit_models # noqa: F401
|
|
25
|
+
|
|
26
|
+
# Import models to register them with metadata
|
|
27
|
+
from fastapi_admin_kit.auth import models as _auth_models # noqa: F401
|
|
28
|
+
from fastapi_admin_kit.models.base import Base as AdminBase
|
|
29
|
+
|
|
30
|
+
if isinstance(self.engine, AsyncEngine):
|
|
31
|
+
# Async engine - use run_sync
|
|
32
|
+
async with self.engine.begin() as conn:
|
|
33
|
+
# Create admin tables
|
|
34
|
+
await conn.run_sync(AdminBase.metadata.create_all)
|
|
35
|
+
# Create user tables if Base is provided
|
|
36
|
+
if self.base is not None:
|
|
37
|
+
await conn.run_sync(self.base.metadata.create_all)
|
|
38
|
+
else:
|
|
39
|
+
# Sync engine - direct call
|
|
40
|
+
AdminBase.metadata.create_all(bind=self.engine)
|
|
41
|
+
if self.base is not None:
|
|
42
|
+
self.base.metadata.create_all(bind=self.engine)
|
|
43
|
+
|
|
44
|
+
async def _seed_roles(
|
|
45
|
+
self, seed_roles: list, seed_roles_overwrite: bool = False
|
|
46
|
+
) -> None:
|
|
47
|
+
"""Seed default roles if none exist (or if overwrite is enabled)."""
|
|
48
|
+
from sqlalchemy import select
|
|
49
|
+
from sqlalchemy.ext.asyncio import AsyncEngine, AsyncSession
|
|
50
|
+
from sqlalchemy.orm import Session, sessionmaker
|
|
51
|
+
|
|
52
|
+
from fastapi_admin_kit.auth.models import AdminPermission, AdminRole
|
|
53
|
+
|
|
54
|
+
is_async = isinstance(self.engine, AsyncEngine)
|
|
55
|
+
|
|
56
|
+
if is_async:
|
|
57
|
+
# Use AsyncSession for async engine
|
|
58
|
+
session_local = sessionmaker(
|
|
59
|
+
self.engine, class_=AsyncSession, expire_on_commit=False
|
|
60
|
+
)
|
|
61
|
+
async with session_local() as session:
|
|
62
|
+
# Check existing count
|
|
63
|
+
result = await session.execute(select(AdminRole))
|
|
64
|
+
existing_count = len(result.scalars().all())
|
|
65
|
+
|
|
66
|
+
if existing_count > 0 and not seed_roles_overwrite:
|
|
67
|
+
return
|
|
68
|
+
|
|
69
|
+
if seed_roles_overwrite:
|
|
70
|
+
await session.execute(select(AdminRole).delete())
|
|
71
|
+
|
|
72
|
+
for role_spec in seed_roles:
|
|
73
|
+
role = AdminRole(
|
|
74
|
+
name=role_spec.name, description=role_spec.description
|
|
75
|
+
)
|
|
76
|
+
session.add(role)
|
|
77
|
+
await session.flush() # get role.id
|
|
78
|
+
|
|
79
|
+
if role_spec.permissions:
|
|
80
|
+
for table_name, perms in role_spec.permissions.items():
|
|
81
|
+
perm = AdminPermission(
|
|
82
|
+
role_id=role.id,
|
|
83
|
+
table_name=table_name,
|
|
84
|
+
can_view=perms.get("view", False),
|
|
85
|
+
can_create=perms.get("create", False),
|
|
86
|
+
can_edit=perms.get("edit", False),
|
|
87
|
+
can_delete=perms.get("delete", False),
|
|
88
|
+
)
|
|
89
|
+
session.add(perm)
|
|
90
|
+
|
|
91
|
+
await session.commit()
|
|
92
|
+
else:
|
|
93
|
+
# Use sync Session for sync engine
|
|
94
|
+
session = Session(bind=self.engine)
|
|
95
|
+
try:
|
|
96
|
+
existing_count = session.query(AdminRole).count()
|
|
97
|
+
|
|
98
|
+
if existing_count > 0 and not seed_roles_overwrite:
|
|
99
|
+
return
|
|
100
|
+
|
|
101
|
+
if seed_roles_overwrite:
|
|
102
|
+
session.query(AdminRole).delete()
|
|
103
|
+
|
|
104
|
+
for role_spec in seed_roles:
|
|
105
|
+
role = AdminRole(
|
|
106
|
+
name=role_spec.name, description=role_spec.description
|
|
107
|
+
)
|
|
108
|
+
session.add(role)
|
|
109
|
+
session.flush() # get role.id
|
|
110
|
+
|
|
111
|
+
if role_spec.permissions:
|
|
112
|
+
for table_name, perms in role_spec.permissions.items():
|
|
113
|
+
perm = AdminPermission(
|
|
114
|
+
role_id=role.id,
|
|
115
|
+
table_name=table_name,
|
|
116
|
+
can_view=perms.get("view", False),
|
|
117
|
+
can_create=perms.get("create", False),
|
|
118
|
+
can_edit=perms.get("edit", False),
|
|
119
|
+
can_delete=perms.get("delete", False),
|
|
120
|
+
)
|
|
121
|
+
session.add(perm)
|
|
122
|
+
|
|
123
|
+
session.commit()
|
|
124
|
+
finally:
|
|
125
|
+
session.close()
|
|
126
|
+
|
|
127
|
+
def _init_session_backend(
|
|
128
|
+
self, secret_key: str, session_ttl: int, cookie_name: str, secure: bool
|
|
129
|
+
) -> Any:
|
|
130
|
+
"""Create and store the signed-cookie session backend."""
|
|
131
|
+
from fastapi_admin_kit.auth.session import SignedCookieSessionBackend
|
|
132
|
+
|
|
133
|
+
return SignedCookieSessionBackend(
|
|
134
|
+
secret_key=secret_key,
|
|
135
|
+
session_ttl=session_ttl,
|
|
136
|
+
cookie_name=cookie_name,
|
|
137
|
+
secure=secure,
|
|
138
|
+
)
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
"""Admin router building and static file mounting."""
|
|
2
|
+
|
|
3
|
+
from typing import TYPE_CHECKING, Any
|
|
4
|
+
|
|
5
|
+
if TYPE_CHECKING:
|
|
6
|
+
pass
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class AdminRouter:
|
|
10
|
+
"""Builds and mounts routers for all registered models and admin routes."""
|
|
11
|
+
|
|
12
|
+
def __init__(
|
|
13
|
+
self,
|
|
14
|
+
admin_path: str = "/admin",
|
|
15
|
+
secret_key: str = "",
|
|
16
|
+
):
|
|
17
|
+
self.admin_path = admin_path.rstrip("/")
|
|
18
|
+
self.secret_key = secret_key or ""
|
|
19
|
+
|
|
20
|
+
def _build_router(self, app: Any) -> None:
|
|
21
|
+
"""Build and mount routers for all registered models."""
|
|
22
|
+
from fastapi_admin_kit.api.search import router as search_api_router
|
|
23
|
+
from fastapi_admin_kit.auth.router import router as auth_router
|
|
24
|
+
from fastapi_admin_kit.registry import AdminRegistry
|
|
25
|
+
from fastapi_admin_kit.router import build_model_router
|
|
26
|
+
from fastapi_admin_kit.views.audit import router as audit_router
|
|
27
|
+
from fastapi_admin_kit.views.roles import router as roles_router
|
|
28
|
+
|
|
29
|
+
registry = AdminRegistry()
|
|
30
|
+
|
|
31
|
+
for registered in registry.all():
|
|
32
|
+
model_router = build_model_router(registered)
|
|
33
|
+
app.include_router(model_router, prefix=self.admin_path)
|
|
34
|
+
|
|
35
|
+
# Auth routes (login/logout)
|
|
36
|
+
app.include_router(auth_router, prefix=self.admin_path)
|
|
37
|
+
|
|
38
|
+
# Global search API
|
|
39
|
+
app.include_router(search_api_router, prefix=self.admin_path)
|
|
40
|
+
|
|
41
|
+
# Audit & role management routes
|
|
42
|
+
app.include_router(audit_router, prefix=self.admin_path)
|
|
43
|
+
app.include_router(roles_router, prefix=self.admin_path)
|
|
44
|
+
|
|
45
|
+
def _mount_static(self, app: Any) -> None:
|
|
46
|
+
"""Mount the static files directory and uploads directory."""
|
|
47
|
+
from pathlib import Path
|
|
48
|
+
|
|
49
|
+
from fastapi.staticfiles import StaticFiles
|
|
50
|
+
|
|
51
|
+
static_dir = Path(__file__).parent.parent / "static"
|
|
52
|
+
if static_dir.is_dir():
|
|
53
|
+
# Primary mount — matches template references (/static/...)
|
|
54
|
+
app.mount(
|
|
55
|
+
"/static",
|
|
56
|
+
StaticFiles(directory=str(static_dir)),
|
|
57
|
+
name="admin_static",
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
def _init_jinja(self, app: Any) -> None:
|
|
61
|
+
"""Initialise the Jinja2 template environment."""
|
|
62
|
+
import re
|
|
63
|
+
from pathlib import Path
|
|
64
|
+
|
|
65
|
+
from starlette.templating import Jinja2Templates
|
|
66
|
+
|
|
67
|
+
templates_dir = Path(__file__).parent / "templates"
|
|
68
|
+
jinja_env = Jinja2Templates(directory=str(templates_dir))
|
|
69
|
+
|
|
70
|
+
def slugify(s: str) -> str:
|
|
71
|
+
return re.sub(r"[^\w]", "-", s, flags=re.A).strip("-").lower()
|
|
72
|
+
|
|
73
|
+
jinja_env.env.filters["slugify"] = slugify
|
|
74
|
+
app.state.admin_jinja_env = jinja_env
|