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.
Files changed (163) hide show
  1. fastapi_admin_kit/__init__.py +73 -0
  2. fastapi_admin_kit/actions/__init__.py +63 -0
  3. fastapi_admin_kit/actions/base.py +68 -0
  4. fastapi_admin_kit/actions/registry.py +43 -0
  5. fastapi_admin_kit/admin/__init__.py +17 -0
  6. fastapi_admin_kit/admin/admin_config.py +95 -0
  7. fastapi_admin_kit/admin/admin_database.py +138 -0
  8. fastapi_admin_kit/admin/admin_router.py +74 -0
  9. fastapi_admin_kit/admin/admin_template.py +203 -0
  10. fastapi_admin_kit/admin/builtin_models.py +284 -0
  11. fastapi_admin_kit/admin/core.py +1036 -0
  12. fastapi_admin_kit/admin/decorators.py +70 -0
  13. fastapi_admin_kit/admin/state.py +76 -0
  14. fastapi_admin_kit/admin.py +728 -0
  15. fastapi_admin_kit/api/__init__.py +44 -0
  16. fastapi_admin_kit/api/auth.py +342 -0
  17. fastapi_admin_kit/api/crud.py +128 -0
  18. fastapi_admin_kit/api/deps.py +79 -0
  19. fastapi_admin_kit/api/roles.py +128 -0
  20. fastapi_admin_kit/api/schema_generator.py +171 -0
  21. fastapi_admin_kit/api/schemas.py +81 -0
  22. fastapi_admin_kit/api/search.py +132 -0
  23. fastapi_admin_kit/audit/__init__.py +36 -0
  24. fastapi_admin_kit/audit/context.py +62 -0
  25. fastapi_admin_kit/audit/diff.py +77 -0
  26. fastapi_admin_kit/audit/event_bus.py +96 -0
  27. fastapi_admin_kit/audit/events.py +48 -0
  28. fastapi_admin_kit/audit/listener.py +159 -0
  29. fastapi_admin_kit/audit/logger.py +28 -0
  30. fastapi_admin_kit/audit/middleware.py +39 -0
  31. fastapi_admin_kit/audit/models.py +53 -0
  32. fastapi_admin_kit/audit/sqlalchemy_logger.py +58 -0
  33. fastapi_admin_kit/auth/__init__.py +34 -0
  34. fastapi_admin_kit/auth/backend.py +95 -0
  35. fastapi_admin_kit/auth/csrf.py +240 -0
  36. fastapi_admin_kit/auth/dependencies.py +150 -0
  37. fastapi_admin_kit/auth/identity.py +181 -0
  38. fastapi_admin_kit/auth/models.py +246 -0
  39. fastapi_admin_kit/auth/password.py +35 -0
  40. fastapi_admin_kit/auth/permissions.py +205 -0
  41. fastapi_admin_kit/auth/protocol.py +22 -0
  42. fastapi_admin_kit/auth/ratelimit.py +88 -0
  43. fastapi_admin_kit/auth/router.py +10 -0
  44. fastapi_admin_kit/auth/session.py +79 -0
  45. fastapi_admin_kit/auth/totp.py +83 -0
  46. fastapi_admin_kit/auth/views.py +165 -0
  47. fastapi_admin_kit/cli.py +229 -0
  48. fastapi_admin_kit/config/__init__.py +19 -0
  49. fastapi_admin_kit/config/audit.py +18 -0
  50. fastapi_admin_kit/config/auth.py +54 -0
  51. fastapi_admin_kit/config/behavior.py +27 -0
  52. fastapi_admin_kit/config/nav.py +32 -0
  53. fastapi_admin_kit/config/storage.py +22 -0
  54. fastapi_admin_kit/config/theme.py +215 -0
  55. fastapi_admin_kit/config/ui.py +147 -0
  56. fastapi_admin_kit/dashboard/__init__.py +64 -0
  57. fastapi_admin_kit/db.py +133 -0
  58. fastapi_admin_kit/exceptions.py +5 -0
  59. fastapi_admin_kit/field_types.py +81 -0
  60. fastapi_admin_kit/filters/__init__.py +21 -0
  61. fastapi_admin_kit/filters/base.py +170 -0
  62. fastapi_admin_kit/filters/registry.py +68 -0
  63. fastapi_admin_kit/flash.py +45 -0
  64. fastapi_admin_kit/form/__init__.py +1 -0
  65. fastapi_admin_kit/form/pipeline.py +106 -0
  66. fastapi_admin_kit/inspection/__init__.py +117 -0
  67. fastapi_admin_kit/inspection/registry.py +253 -0
  68. fastapi_admin_kit/inspection.py +115 -0
  69. fastapi_admin_kit/modeladmin.py +375 -0
  70. fastapi_admin_kit/models/__init__.py +7 -0
  71. fastapi_admin_kit/models/base.py +7 -0
  72. fastapi_admin_kit/nav.py +208 -0
  73. fastapi_admin_kit/pagination/__init__.py +14 -0
  74. fastapi_admin_kit/pagination/base.py +40 -0
  75. fastapi_admin_kit/pagination/cursor.py +97 -0
  76. fastapi_admin_kit/pagination/dynamic.py +48 -0
  77. fastapi_admin_kit/pagination/offset.py +42 -0
  78. fastapi_admin_kit/plugins/__init__.py +1 -0
  79. fastapi_admin_kit/py.typed +0 -0
  80. fastapi_admin_kit/registry/__init__.py +5 -0
  81. fastapi_admin_kit/registry/core.py +287 -0
  82. fastapi_admin_kit/registry/validation.py +107 -0
  83. fastapi_admin_kit/registry.py +15 -0
  84. fastapi_admin_kit/router.py +335 -0
  85. fastapi_admin_kit/static/css/admin.css +4736 -0
  86. fastapi_admin_kit/static/css/presets.css +317 -0
  87. fastapi_admin_kit/static/css/tokens.css +217 -0
  88. fastapi_admin_kit/static/css/variables.css +74 -0
  89. fastapi_admin_kit/static/icons/heroicons.svg +160 -0
  90. fastapi_admin_kit/static/js/admin.js +692 -0
  91. fastapi_admin_kit/static/js/htmx-config.js +42 -0
  92. fastapi_admin_kit/storage/__init__.py +6 -0
  93. fastapi_admin_kit/storage/base.py +48 -0
  94. fastapi_admin_kit/storage/local.py +73 -0
  95. fastapi_admin_kit/templates/base.html +142 -0
  96. fastapi_admin_kit/templates/macros/form_fields.html +660 -0
  97. fastapi_admin_kit/templates/macros/icons.html +50 -0
  98. fastapi_admin_kit/templates/macros/table.html +108 -0
  99. fastapi_admin_kit/templates/macros/widgets.html +159 -0
  100. fastapi_admin_kit/templates/pages/2fa/setup.html +122 -0
  101. fastapi_admin_kit/templates/pages/2fa/verify.html +55 -0
  102. fastapi_admin_kit/templates/pages/audit_detail.html +122 -0
  103. fastapi_admin_kit/templates/pages/audit_log.html +102 -0
  104. fastapi_admin_kit/templates/pages/dashboard.html +295 -0
  105. fastapi_admin_kit/templates/pages/detail.html +183 -0
  106. fastapi_admin_kit/templates/pages/form.html +119 -0
  107. fastapi_admin_kit/templates/pages/list.html +277 -0
  108. fastapi_admin_kit/templates/pages/login.html +85 -0
  109. fastapi_admin_kit/templates/pages/profile/password.html +78 -0
  110. fastapi_admin_kit/templates/pages/profile/profile.html +73 -0
  111. fastapi_admin_kit/templates/pages/role_form.html +75 -0
  112. fastapi_admin_kit/templates/pages/roles/form.html +117 -0
  113. fastapi_admin_kit/templates/pages/roles/list.html +69 -0
  114. fastapi_admin_kit/templates/pages/roles.html +77 -0
  115. fastapi_admin_kit/templates/pages/settings/theme.html +255 -0
  116. fastapi_admin_kit/templates/pages/users/form.html +229 -0
  117. fastapi_admin_kit/templates/pages/users/list.html +83 -0
  118. fastapi_admin_kit/templates/partials/command_palette.html +52 -0
  119. fastapi_admin_kit/templates/partials/field_wrapper.html +2 -0
  120. fastapi_admin_kit/templates/partials/flash_messages.html +39 -0
  121. fastapi_admin_kit/templates/partials/head.html +21 -0
  122. fastapi_admin_kit/templates/partials/head_minimal.html +18 -0
  123. fastapi_admin_kit/templates/partials/list_table.html +178 -0
  124. fastapi_admin_kit/templates/partials/mobile_backdrop.html +2 -0
  125. fastapi_admin_kit/templates/partials/pagination.html +82 -0
  126. fastapi_admin_kit/templates/partials/permission_widget.html +86 -0
  127. fastapi_admin_kit/templates/partials/scripts.html +13 -0
  128. fastapi_admin_kit/templates/partials/sidebar.html +94 -0
  129. fastapi_admin_kit/templates/partials/topbar.html +95 -0
  130. fastapi_admin_kit/types.py +145 -0
  131. fastapi_admin_kit/validation.py +43 -0
  132. fastapi_admin_kit/views/__init__.py +78 -0
  133. fastapi_admin_kit/views/audit.py +134 -0
  134. fastapi_admin_kit/views/bulk.py +28 -0
  135. fastapi_admin_kit/views/class_views.py +1040 -0
  136. fastapi_admin_kit/views/context.py +588 -0
  137. fastapi_admin_kit/views/dashboard.py +162 -0
  138. fastapi_admin_kit/views/delete.py +31 -0
  139. fastapi_admin_kit/views/extra.py +65 -0
  140. fastapi_admin_kit/views/factory.py +667 -0
  141. fastapi_admin_kit/views/form.py +159 -0
  142. fastapi_admin_kit/views/list.py +28 -0
  143. fastapi_admin_kit/views/profile.py +219 -0
  144. fastapi_admin_kit/views/protocols.py +54 -0
  145. fastapi_admin_kit/views/renderers.py +634 -0
  146. fastapi_admin_kit/views/roles.py +230 -0
  147. fastapi_admin_kit/views/search.py +31 -0
  148. fastapi_admin_kit/views/settings.py +31 -0
  149. fastapi_admin_kit/views/sidebar.py +101 -0
  150. fastapi_admin_kit/views/totp.py +249 -0
  151. fastapi_admin_kit/views/users.py +347 -0
  152. fastapi_admin_kit/views.py +117 -0
  153. fastapi_admin_kit/widgets/__init__.py +44 -0
  154. fastapi_admin_kit/widgets/base.py +44 -0
  155. fastapi_admin_kit/widgets/inputs.py +363 -0
  156. fastapi_admin_kit/widgets/registry.py +110 -0
  157. fastapi_admin_kit/widgets/relation.py +70 -0
  158. fastapi_admin_kit/widgets/resolver.py +102 -0
  159. fastapi_admin_kit-0.1.0.dist-info/METADATA +210 -0
  160. fastapi_admin_kit-0.1.0.dist-info/RECORD +163 -0
  161. fastapi_admin_kit-0.1.0.dist-info/WHEEL +4 -0
  162. fastapi_admin_kit-0.1.0.dist-info/entry_points.txt +3 -0
  163. 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"]