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,203 @@
|
|
|
1
|
+
"""Admin template management and context building."""
|
|
2
|
+
|
|
3
|
+
from typing import TYPE_CHECKING, Any
|
|
4
|
+
|
|
5
|
+
if TYPE_CHECKING:
|
|
6
|
+
pass
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class AdminTemplate:
|
|
10
|
+
"""Manages Jinja2 template environment and sidebar context."""
|
|
11
|
+
|
|
12
|
+
def __init__(
|
|
13
|
+
self,
|
|
14
|
+
title: str = "FastAPI Admin Kit",
|
|
15
|
+
logo_url: str | None = None,
|
|
16
|
+
favicon_url: str | None = None,
|
|
17
|
+
primary_color: str = "#0ea5e9",
|
|
18
|
+
primary_color_dark: str = "#0284c7",
|
|
19
|
+
dark_mode_default: bool = False,
|
|
20
|
+
dashboard_permission: str | None = None,
|
|
21
|
+
settings_permission: str | None = None,
|
|
22
|
+
):
|
|
23
|
+
self.title = title
|
|
24
|
+
self.logo_url = logo_url
|
|
25
|
+
self.favicon_url = favicon_url
|
|
26
|
+
self.primary_color = primary_color
|
|
27
|
+
self.primary_color_dark = primary_color_dark
|
|
28
|
+
self.dark_mode_default = dark_mode_default
|
|
29
|
+
self.dashboard_permission = dashboard_permission
|
|
30
|
+
self.settings_permission = settings_permission
|
|
31
|
+
self._nav_groups_built: list = []
|
|
32
|
+
|
|
33
|
+
def _init_jinja(self, app: Any) -> None:
|
|
34
|
+
"""Initialise the Jinja2 template environment."""
|
|
35
|
+
import re
|
|
36
|
+
from pathlib import Path
|
|
37
|
+
|
|
38
|
+
from starlette.templating import Jinja2Templates
|
|
39
|
+
|
|
40
|
+
templates_dir = Path(__file__).parent.parent / "templates"
|
|
41
|
+
jinja_env = Jinja2Templates(directory=str(templates_dir))
|
|
42
|
+
|
|
43
|
+
def slugify(s: str) -> str:
|
|
44
|
+
return re.sub(r"[^\w]", "-", s, flags=re.A).strip("-").lower()
|
|
45
|
+
|
|
46
|
+
jinja_env.env.filters["slugify"] = slugify
|
|
47
|
+
app.state.admin_jinja_env = jinja_env
|
|
48
|
+
|
|
49
|
+
def sidebar_template_kwargs(self, request: Any) -> dict[str, Any]:
|
|
50
|
+
"""Thin wrapper — returns sidebar kwargs for TemplateResponse contexts."""
|
|
51
|
+
return self.build_sidebar_context(request)
|
|
52
|
+
|
|
53
|
+
def build_sidebar_context(
|
|
54
|
+
self,
|
|
55
|
+
request: Any,
|
|
56
|
+
user: Any = None,
|
|
57
|
+
permissions_map: dict | None = None,
|
|
58
|
+
) -> dict:
|
|
59
|
+
"""Build per-request sidebar context (RBAC filter + permissions map).
|
|
60
|
+
|
|
61
|
+
If *permissions_map* is provided (pre-loaded by the async caller),
|
|
62
|
+
it is used directly instead of querying the database here.
|
|
63
|
+
"""
|
|
64
|
+
if user is None:
|
|
65
|
+
user = getattr(request.state, "admin_user", None)
|
|
66
|
+
|
|
67
|
+
snapshot = getattr(request.state, "admin_user_snapshot", None)
|
|
68
|
+
is_superuser = (
|
|
69
|
+
(
|
|
70
|
+
bool(snapshot.get("is_superuser", False))
|
|
71
|
+
if snapshot
|
|
72
|
+
else bool(getattr(user, "is_superuser", False))
|
|
73
|
+
)
|
|
74
|
+
if user
|
|
75
|
+
else False
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
from fastapi_admin_kit.types import PermissionSet
|
|
79
|
+
|
|
80
|
+
nav_groups = self._nav_groups_built
|
|
81
|
+
|
|
82
|
+
if permissions_map is None:
|
|
83
|
+
permissions_map = {}
|
|
84
|
+
|
|
85
|
+
if user and not is_superuser:
|
|
86
|
+
role_ids = (
|
|
87
|
+
snapshot.get("role_ids", [])
|
|
88
|
+
if snapshot
|
|
89
|
+
else getattr(user, "role_ids", [])
|
|
90
|
+
)
|
|
91
|
+
if role_ids:
|
|
92
|
+
try:
|
|
93
|
+
from sqlalchemy import select
|
|
94
|
+
from sqlalchemy.orm import Session
|
|
95
|
+
|
|
96
|
+
from fastapi_admin_kit.auth.models import AdminPermission
|
|
97
|
+
|
|
98
|
+
engine = request.app.state.admin_engine
|
|
99
|
+
with Session(engine) as s:
|
|
100
|
+
result = s.execute(
|
|
101
|
+
select(AdminPermission).filter(
|
|
102
|
+
AdminPermission.role_id.in_(role_ids)
|
|
103
|
+
)
|
|
104
|
+
)
|
|
105
|
+
rows = result.scalars().all()
|
|
106
|
+
for perm in rows:
|
|
107
|
+
if perm.table_name in permissions_map:
|
|
108
|
+
existing = permissions_map[perm.table_name]
|
|
109
|
+
permissions_map[perm.table_name] = (
|
|
110
|
+
PermissionSet(
|
|
111
|
+
can_view=existing.can_view
|
|
112
|
+
or perm.can_view,
|
|
113
|
+
can_create=existing.can_create
|
|
114
|
+
or perm.can_create,
|
|
115
|
+
can_edit=existing.can_edit
|
|
116
|
+
or perm.can_edit,
|
|
117
|
+
can_delete=existing.can_delete
|
|
118
|
+
or perm.can_delete,
|
|
119
|
+
)
|
|
120
|
+
)
|
|
121
|
+
else:
|
|
122
|
+
permissions_map[perm.table_name] = (
|
|
123
|
+
PermissionSet(
|
|
124
|
+
can_view=perm.can_view,
|
|
125
|
+
can_create=perm.can_create,
|
|
126
|
+
can_edit=perm.can_edit,
|
|
127
|
+
can_delete=perm.can_delete,
|
|
128
|
+
)
|
|
129
|
+
)
|
|
130
|
+
except Exception:
|
|
131
|
+
pass
|
|
132
|
+
|
|
133
|
+
def _item_visible(item: Any) -> bool:
|
|
134
|
+
return (
|
|
135
|
+
item.permission_table is None
|
|
136
|
+
or is_superuser
|
|
137
|
+
or (
|
|
138
|
+
permissions_map.get(item.permission_table)
|
|
139
|
+
and permissions_map[item.permission_table].can_view
|
|
140
|
+
)
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
def _filter_items(items: list[Any]) -> list[Any]:
|
|
144
|
+
from dataclasses import replace
|
|
145
|
+
|
|
146
|
+
result = []
|
|
147
|
+
for item in items:
|
|
148
|
+
if not _item_visible(item):
|
|
149
|
+
continue
|
|
150
|
+
filtered_children = (
|
|
151
|
+
_filter_items(item.children) if item.children else []
|
|
152
|
+
)
|
|
153
|
+
result.append(replace(item, children=filtered_children))
|
|
154
|
+
return result
|
|
155
|
+
|
|
156
|
+
from dataclasses import replace
|
|
157
|
+
|
|
158
|
+
dashboard_visible = (
|
|
159
|
+
is_superuser
|
|
160
|
+
or self.dashboard_permission is None
|
|
161
|
+
or (
|
|
162
|
+
permissions_map.get(self.dashboard_permission)
|
|
163
|
+
and permissions_map[self.dashboard_permission].can_view
|
|
164
|
+
)
|
|
165
|
+
)
|
|
166
|
+
settings_visible = (
|
|
167
|
+
is_superuser
|
|
168
|
+
or self.settings_permission is None
|
|
169
|
+
or (
|
|
170
|
+
permissions_map.get(self.settings_permission)
|
|
171
|
+
and permissions_map[self.settings_permission].can_view
|
|
172
|
+
)
|
|
173
|
+
)
|
|
174
|
+
|
|
175
|
+
filtered_groups: list[Any] = []
|
|
176
|
+
for group in nav_groups:
|
|
177
|
+
if not (
|
|
178
|
+
group.permission_table is None
|
|
179
|
+
or is_superuser
|
|
180
|
+
or (
|
|
181
|
+
permissions_map.get(group.permission_table)
|
|
182
|
+
and permissions_map[group.permission_table].can_view
|
|
183
|
+
)
|
|
184
|
+
):
|
|
185
|
+
continue
|
|
186
|
+
visible = _filter_items(group.items)
|
|
187
|
+
if visible:
|
|
188
|
+
filtered_groups.append(replace(group, items=visible))
|
|
189
|
+
|
|
190
|
+
return {
|
|
191
|
+
"nav_groups": filtered_groups,
|
|
192
|
+
"permissions_map": permissions_map,
|
|
193
|
+
"current_user": user,
|
|
194
|
+
"dashboard_visible": dashboard_visible,
|
|
195
|
+
"settings_visible": settings_visible,
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
def apply_sidebar_context(
|
|
199
|
+
self, request: Any, user: Any, context: dict
|
|
200
|
+
) -> dict:
|
|
201
|
+
"""Inject nav_groups + permissions_map into a template context dict."""
|
|
202
|
+
context.update(self.build_sidebar_context(request, user=user))
|
|
203
|
+
return context
|
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
"""Default ModelAdmin classes for built-in admin models."""
|
|
2
|
+
|
|
3
|
+
from fastapi_admin_kit.modeladmin import ModelAdmin
|
|
4
|
+
from fastapi_admin_kit.types import ExtraField
|
|
5
|
+
from fastapi_admin_kit.widgets.inputs import AutocompleteWidget, PasswordWidget
|
|
6
|
+
from fastapi_admin_kit.widgets.relation import MultiRelationWidget
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def _get_table_names() -> list[str]:
|
|
10
|
+
from fastapi_admin_kit.registry.core import AdminRegistry
|
|
11
|
+
|
|
12
|
+
registry = AdminRegistry()
|
|
13
|
+
return sorted({m.table_name for m in registry.all()})
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class AdminUserAdmin(ModelAdmin):
|
|
17
|
+
tag = "admin"
|
|
18
|
+
icon = "group"
|
|
19
|
+
verbose_name = "Admin User"
|
|
20
|
+
verbose_name_plural = "Admin Users"
|
|
21
|
+
list_display = ["id", "email", "full_name", "is_superuser", "is_active"]
|
|
22
|
+
search_fields = ["email", "full_name"]
|
|
23
|
+
exclude = ["hashed_password", "password_changed_at"]
|
|
24
|
+
extra_fields = [
|
|
25
|
+
ExtraField(
|
|
26
|
+
name="password",
|
|
27
|
+
label="Password",
|
|
28
|
+
required=False,
|
|
29
|
+
required_on_create=True,
|
|
30
|
+
widget=PasswordWidget(),
|
|
31
|
+
),
|
|
32
|
+
]
|
|
33
|
+
formfield_overrides = {
|
|
34
|
+
"roles": MultiRelationWidget(
|
|
35
|
+
related_table="admin_roles",
|
|
36
|
+
search_url="/admin/users/roles/search",
|
|
37
|
+
),
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
def prepare_create_data(self, data, request=None):
|
|
41
|
+
from fastapi_admin_kit.auth.backend import pwd_context
|
|
42
|
+
|
|
43
|
+
password = data.pop("password", None)
|
|
44
|
+
if password:
|
|
45
|
+
data["hashed_password"] = pwd_context.hash(password)
|
|
46
|
+
else:
|
|
47
|
+
data["hashed_password"] = ""
|
|
48
|
+
return data
|
|
49
|
+
|
|
50
|
+
def on_create(self, obj, request=None):
|
|
51
|
+
pass
|
|
52
|
+
|
|
53
|
+
def on_update(self, obj, data, request=None):
|
|
54
|
+
pass
|
|
55
|
+
|
|
56
|
+
def after_create(self, obj, request=None):
|
|
57
|
+
if request is None:
|
|
58
|
+
return
|
|
59
|
+
perm_data = getattr(request.state, "_admin_perm_data", None)
|
|
60
|
+
if perm_data:
|
|
61
|
+
self._save_direct_permissions_after_commit(obj, perm_data, request)
|
|
62
|
+
|
|
63
|
+
def after_update(self, obj, request=None):
|
|
64
|
+
if request is None:
|
|
65
|
+
return
|
|
66
|
+
perm_data = getattr(request.state, "_admin_perm_data", None)
|
|
67
|
+
if perm_data:
|
|
68
|
+
self._save_direct_permissions_after_commit(obj, perm_data, request)
|
|
69
|
+
|
|
70
|
+
def _save_direct_permissions_after_commit(self, obj, perm_data, request):
|
|
71
|
+
import os
|
|
72
|
+
import sqlite3
|
|
73
|
+
|
|
74
|
+
db_url = os.getenv("DATABASE_URL", "")
|
|
75
|
+
if "sqlite" in db_url or not db_url:
|
|
76
|
+
db_path = db_url.split("///")[-1] if "///" in db_url else "test_debug.db"
|
|
77
|
+
conn = sqlite3.connect(db_path)
|
|
78
|
+
cursor = conn.cursor()
|
|
79
|
+
|
|
80
|
+
cursor.execute(
|
|
81
|
+
"DELETE FROM admin_user_permissions WHERE user_id = ?",
|
|
82
|
+
(obj.id,),
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
for table_name, perms in perm_data.items():
|
|
86
|
+
if not any(perms.get(a) for a in ["view", "create", "edit", "delete"]):
|
|
87
|
+
continue
|
|
88
|
+
cursor.execute(
|
|
89
|
+
"""INSERT INTO admin_user_permissions
|
|
90
|
+
(user_id, table_name, can_view, can_create, can_edit, can_delete)
|
|
91
|
+
VALUES (?, ?, ?, ?, ?, ?)""",
|
|
92
|
+
(
|
|
93
|
+
obj.id,
|
|
94
|
+
table_name,
|
|
95
|
+
1 if perms.get("view") else 0,
|
|
96
|
+
1 if perms.get("create") else 0,
|
|
97
|
+
1 if perms.get("edit") else 0,
|
|
98
|
+
1 if perms.get("delete") else 0,
|
|
99
|
+
),
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
conn.commit()
|
|
103
|
+
conn.close()
|
|
104
|
+
|
|
105
|
+
def get_form_context(self, context, obj=None, request=None):
|
|
106
|
+
from sqlalchemy import select
|
|
107
|
+
|
|
108
|
+
from fastapi_admin_kit.auth.models import AdminUserPermission
|
|
109
|
+
from fastapi_admin_kit.db import get_db_session
|
|
110
|
+
|
|
111
|
+
perm_data = {}
|
|
112
|
+
if obj is not None and request is not None:
|
|
113
|
+
try:
|
|
114
|
+
session = get_db_session(request)
|
|
115
|
+
import asyncio
|
|
116
|
+
|
|
117
|
+
async def _load_perms():
|
|
118
|
+
result = await session.execute(
|
|
119
|
+
select(AdminUserPermission).where(
|
|
120
|
+
AdminUserPermission.user_id == obj.id
|
|
121
|
+
)
|
|
122
|
+
)
|
|
123
|
+
return result.scalars().all()
|
|
124
|
+
|
|
125
|
+
loop = asyncio.get_event_loop()
|
|
126
|
+
if loop.is_running():
|
|
127
|
+
import concurrent.futures
|
|
128
|
+
with concurrent.futures.ThreadPoolExecutor() as pool:
|
|
129
|
+
perms = pool.submit(asyncio.run, _load_perms()).result()
|
|
130
|
+
else:
|
|
131
|
+
perms = loop.run_until_complete(_load_perms())
|
|
132
|
+
|
|
133
|
+
for p in perms:
|
|
134
|
+
perm_data[p.table_name] = {
|
|
135
|
+
"_label": p.table_name,
|
|
136
|
+
"view": p.can_view,
|
|
137
|
+
"create": p.can_create,
|
|
138
|
+
"edit": p.can_edit,
|
|
139
|
+
"delete": p.can_delete,
|
|
140
|
+
}
|
|
141
|
+
except Exception:
|
|
142
|
+
pass
|
|
143
|
+
|
|
144
|
+
context["perm_data"] = perm_data
|
|
145
|
+
context["search_url"] = "/admin/tables/search"
|
|
146
|
+
return context
|
|
147
|
+
|
|
148
|
+
def process_form_data(self, data, request=None):
|
|
149
|
+
if request is not None:
|
|
150
|
+
import asyncio
|
|
151
|
+
|
|
152
|
+
async def _get_perm_data():
|
|
153
|
+
form = await request.form()
|
|
154
|
+
return form.get("perm_data", "{}")
|
|
155
|
+
|
|
156
|
+
try:
|
|
157
|
+
loop = asyncio.get_running_loop()
|
|
158
|
+
except RuntimeError:
|
|
159
|
+
loop = None
|
|
160
|
+
|
|
161
|
+
if loop and loop.is_running():
|
|
162
|
+
import concurrent.futures
|
|
163
|
+
with concurrent.futures.ThreadPoolExecutor() as pool:
|
|
164
|
+
perm_data_raw = pool.submit(asyncio.run, _get_perm_data()).result()
|
|
165
|
+
elif loop:
|
|
166
|
+
perm_data_raw = loop.run_until_complete(_get_perm_data())
|
|
167
|
+
else:
|
|
168
|
+
perm_data_raw = asyncio.run(_get_perm_data())
|
|
169
|
+
else:
|
|
170
|
+
perm_data_raw = "{}"
|
|
171
|
+
|
|
172
|
+
import json
|
|
173
|
+
|
|
174
|
+
try:
|
|
175
|
+
perm_data = json.loads(perm_data_raw)
|
|
176
|
+
except (json.JSONDecodeError, TypeError):
|
|
177
|
+
perm_data = {}
|
|
178
|
+
|
|
179
|
+
if request is not None and perm_data:
|
|
180
|
+
request.state._admin_perm_data = perm_data
|
|
181
|
+
|
|
182
|
+
return data
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
class AdminRoleAdmin(ModelAdmin):
|
|
186
|
+
tag = "admin"
|
|
187
|
+
icon = "shield-check"
|
|
188
|
+
verbose_name = "Admin Role"
|
|
189
|
+
verbose_name_plural = "Admin Roles"
|
|
190
|
+
list_display = ["id", "name", "description"]
|
|
191
|
+
search_fields = ["name"]
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
class AdminRefreshTokenAdmin(ModelAdmin):
|
|
195
|
+
tag = "admin"
|
|
196
|
+
icon = "key"
|
|
197
|
+
verbose_name = "Refresh Token"
|
|
198
|
+
verbose_name_plural = "Refresh Tokens"
|
|
199
|
+
exclude = ["user"]
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
class AdminPermissionAdmin(ModelAdmin):
|
|
203
|
+
tag = "admin"
|
|
204
|
+
icon = "lock"
|
|
205
|
+
verbose_name = "Permission"
|
|
206
|
+
verbose_name_plural = "Permissions"
|
|
207
|
+
list_display = [
|
|
208
|
+
"id",
|
|
209
|
+
"role",
|
|
210
|
+
"table_name",
|
|
211
|
+
"can_view",
|
|
212
|
+
"can_create",
|
|
213
|
+
"can_edit",
|
|
214
|
+
"can_delete",
|
|
215
|
+
]
|
|
216
|
+
formfield_overrides = {
|
|
217
|
+
"table_name": AutocompleteWidget(suggestions_fn=_get_table_names),
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
class AuditLogAdmin(ModelAdmin):
|
|
222
|
+
tag = "admin"
|
|
223
|
+
icon = "clock"
|
|
224
|
+
verbose_name = "Audit Log"
|
|
225
|
+
verbose_name_plural = "Audit Logs"
|
|
226
|
+
list_display = [
|
|
227
|
+
"id",
|
|
228
|
+
"user_email",
|
|
229
|
+
"action",
|
|
230
|
+
"model_name",
|
|
231
|
+
"object_id",
|
|
232
|
+
"timestamp",
|
|
233
|
+
]
|
|
234
|
+
search_fields = ["user_email", "model_name"]
|
|
235
|
+
readonly_fields = [
|
|
236
|
+
"user_id",
|
|
237
|
+
"user_email",
|
|
238
|
+
"action",
|
|
239
|
+
"model_name",
|
|
240
|
+
"table_name",
|
|
241
|
+
"object_id",
|
|
242
|
+
"object_repr",
|
|
243
|
+
"changes",
|
|
244
|
+
"full_snapshot",
|
|
245
|
+
"ip_address",
|
|
246
|
+
"user_agent",
|
|
247
|
+
"timestamp",
|
|
248
|
+
]
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
class AdminUserTOTPAdmin(ModelAdmin):
|
|
252
|
+
tag = "admin"
|
|
253
|
+
icon = "lock"
|
|
254
|
+
verbose_name = "2FA Token"
|
|
255
|
+
verbose_name_plural = "2FA Tokens"
|
|
256
|
+
list_display = ["id", "user_id", "enabled", "secret_key", "created_at"]
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
class AdminUserPermissionAdmin(ModelAdmin):
|
|
260
|
+
tag = "admin"
|
|
261
|
+
icon = "lock"
|
|
262
|
+
verbose_name = "User Permission"
|
|
263
|
+
verbose_name_plural = "User Permissions"
|
|
264
|
+
list_display = [
|
|
265
|
+
"id",
|
|
266
|
+
"user",
|
|
267
|
+
"table_name",
|
|
268
|
+
"can_view",
|
|
269
|
+
"can_create",
|
|
270
|
+
"can_edit",
|
|
271
|
+
"can_delete",
|
|
272
|
+
]
|
|
273
|
+
formfield_overrides = {
|
|
274
|
+
"table_name": AutocompleteWidget(suggestions_fn=_get_table_names),
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
class AdminLoginAttemptAdmin(ModelAdmin):
|
|
279
|
+
tag = "admin"
|
|
280
|
+
icon = "clock"
|
|
281
|
+
verbose_name = "Login Attempt"
|
|
282
|
+
verbose_name_plural = "Login Attempts"
|
|
283
|
+
list_display = ["id", "email", "ip_address", "success", "timestamp"]
|
|
284
|
+
search_fields = ["email", "ip_address"]
|