crudadmin 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.
- crudadmin/__init__.py +3 -0
- crudadmin/admin_interface/__init__.py +0 -0
- crudadmin/admin_interface/admin_site.py +643 -0
- crudadmin/admin_interface/auth.py +122 -0
- crudadmin/admin_interface/crud_admin.py +1174 -0
- crudadmin/admin_interface/helper.py +122 -0
- crudadmin/admin_interface/middleware/__init__.py +9 -0
- crudadmin/admin_interface/middleware/auth.py +117 -0
- crudadmin/admin_interface/middleware/https.py +21 -0
- crudadmin/admin_interface/middleware/ip_restriction.py +89 -0
- crudadmin/admin_interface/model_view.py +1181 -0
- crudadmin/admin_interface/typing.py +7 -0
- crudadmin/admin_token/__init__.py +19 -0
- crudadmin/admin_token/models.py +35 -0
- crudadmin/admin_token/schemas.py +25 -0
- crudadmin/admin_token/service.py +119 -0
- crudadmin/admin_user/__init__.py +23 -0
- crudadmin/admin_user/models.py +29 -0
- crudadmin/admin_user/schemas.py +69 -0
- crudadmin/admin_user/service.py +128 -0
- crudadmin/core/__init__.py +21 -0
- crudadmin/core/db.py +257 -0
- crudadmin/core/exceptions.py +19 -0
- crudadmin/core/schemas/__init__.py +5 -0
- crudadmin/core/schemas/timestamp.py +23 -0
- crudadmin/event/__init__.py +38 -0
- crudadmin/event/decorators.py +327 -0
- crudadmin/event/integration.py +120 -0
- crudadmin/event/models.py +97 -0
- crudadmin/event/schemas.py +65 -0
- crudadmin/event/service.py +254 -0
- crudadmin/py.typed +0 -0
- crudadmin/session/__init__.py +17 -0
- crudadmin/session/manager.py +284 -0
- crudadmin/session/models.py +46 -0
- crudadmin/session/schemas.py +41 -0
- crudadmin/static/favicon.png +0 -0
- crudadmin/static/htmx.min.js +1 -0
- crudadmin/templates/admin/dashboard/dashboard.html +35 -0
- crudadmin/templates/admin/dashboard/dashboard_content.html +155 -0
- crudadmin/templates/admin/management/events.html +437 -0
- crudadmin/templates/admin/management/events_content.html +191 -0
- crudadmin/templates/admin/management/health.html +241 -0
- crudadmin/templates/admin/management/health_content.html +49 -0
- crudadmin/templates/admin/model/components/list_content.html +115 -0
- crudadmin/templates/admin/model/components/pagination.html +41 -0
- crudadmin/templates/admin/model/components/table_content.html +16 -0
- crudadmin/templates/admin/model/create.html +384 -0
- crudadmin/templates/admin/model/list.html +609 -0
- crudadmin/templates/admin/model/update.html +396 -0
- crudadmin/templates/auth/login.html +160 -0
- crudadmin/templates/base/base.html +418 -0
- crudadmin/templates/shared/utils/refresh.html +9 -0
- crudadmin-0.1.0.dist-info/METADATA +270 -0
- crudadmin-0.1.0.dist-info/RECORD +58 -0
- crudadmin-0.1.0.dist-info/WHEEL +4 -0
- crudadmin-0.1.0.dist-info/entry_points.txt +4 -0
- crudadmin-0.1.0.dist-info/licenses/LICENSE +21 -0
crudadmin/__init__.py
ADDED
|
File without changes
|
|
@@ -0,0 +1,643 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from datetime import datetime, timedelta, timezone
|
|
3
|
+
from typing import Any, AsyncGenerator, Callable, Dict, Optional, cast
|
|
4
|
+
|
|
5
|
+
from fastapi import APIRouter, Cookie, Depends, Request, Response
|
|
6
|
+
from fastapi.responses import RedirectResponse
|
|
7
|
+
from fastapi.security import OAuth2PasswordRequestForm
|
|
8
|
+
from fastapi.templating import Jinja2Templates
|
|
9
|
+
from fastcrud import FastCRUD
|
|
10
|
+
from sqlalchemy.ext.asyncio import AsyncSession
|
|
11
|
+
|
|
12
|
+
from ..admin_user.service import AdminUserService
|
|
13
|
+
from ..core.db import DatabaseConfig
|
|
14
|
+
from ..event import EventType, log_auth_action
|
|
15
|
+
from ..session.manager import SessionManager
|
|
16
|
+
from .auth import AdminAuthentication
|
|
17
|
+
from .typing import RouteResponse
|
|
18
|
+
|
|
19
|
+
logger = logging.getLogger(__name__)
|
|
20
|
+
|
|
21
|
+
EndpointCallable = Callable[..., Any]
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class AdminSite:
|
|
25
|
+
"""
|
|
26
|
+
Core admin interface site handler managing authentication, routing, and views.
|
|
27
|
+
|
|
28
|
+
**Handles the core functionality of the admin interface including:**
|
|
29
|
+
- Authentication and session management
|
|
30
|
+
- Route configuration and URL handling
|
|
31
|
+
- Template rendering and context management
|
|
32
|
+
- Dashboard and model views
|
|
33
|
+
- Event logging and audit trails
|
|
34
|
+
- Security and access control
|
|
35
|
+
|
|
36
|
+
The AdminSite class serves as the central coordinator for all admin functionality,
|
|
37
|
+
managing user sessions, handling authentication flows, and providing secure access
|
|
38
|
+
to administrative features.
|
|
39
|
+
|
|
40
|
+
Args:
|
|
41
|
+
database_config: Database configuration for admin interface
|
|
42
|
+
templates_directory: Path to template files
|
|
43
|
+
models: Dictionary of registered models and their configurations
|
|
44
|
+
admin_authentication: Authentication handler instance
|
|
45
|
+
mount_path: URL path prefix for admin interface (e.g. "/admin")
|
|
46
|
+
theme: UI theme name ("dark-theme" or "light-theme")
|
|
47
|
+
secure_cookies: Enable secure cookie flags
|
|
48
|
+
event_integration: Optional event logging integration
|
|
49
|
+
|
|
50
|
+
Attributes:
|
|
51
|
+
db_config: Database configuration instance
|
|
52
|
+
router: FastAPI router for admin endpoints
|
|
53
|
+
templates: Jinja2 template handler
|
|
54
|
+
models: Dictionary of registered models
|
|
55
|
+
admin_user_service: Service for user management
|
|
56
|
+
admin_authentication: Authentication handler
|
|
57
|
+
token_service: JWT token service
|
|
58
|
+
mount_path: URL prefix for admin routes
|
|
59
|
+
theme: Active UI theme
|
|
60
|
+
event_integration: Event logging handler
|
|
61
|
+
session_manager: Session tracking service
|
|
62
|
+
secure_cookies: Cookie security flag
|
|
63
|
+
|
|
64
|
+
Examples:
|
|
65
|
+
Basic setup with SQLite:
|
|
66
|
+
```python
|
|
67
|
+
from fastapi.templating import Jinja2Templates
|
|
68
|
+
from .auth import AdminAuthentication
|
|
69
|
+
from .db import DatabaseConfig
|
|
70
|
+
|
|
71
|
+
admin_site = AdminSite(
|
|
72
|
+
database_config=db_config,
|
|
73
|
+
templates_directory="templates",
|
|
74
|
+
models={}, # Empty initially
|
|
75
|
+
admin_authentication=auth_handler,
|
|
76
|
+
mount_path="/admin",
|
|
77
|
+
theme="dark-theme",
|
|
78
|
+
secure_cookies=True
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
# Add routes
|
|
82
|
+
admin_site.setup_routes()
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
Production configuration:
|
|
86
|
+
```python
|
|
87
|
+
admin_site = AdminSite(
|
|
88
|
+
database_config=db_config,
|
|
89
|
+
templates_directory=templates_path,
|
|
90
|
+
models=model_registry,
|
|
91
|
+
admin_authentication=auth_handler,
|
|
92
|
+
mount_path="/admin",
|
|
93
|
+
theme="dark-theme",
|
|
94
|
+
secure_cookies=True,
|
|
95
|
+
event_integration=event_logger
|
|
96
|
+
)
|
|
97
|
+
```
|
|
98
|
+
"""
|
|
99
|
+
|
|
100
|
+
def __init__(
|
|
101
|
+
self,
|
|
102
|
+
database_config: DatabaseConfig,
|
|
103
|
+
templates_directory: str,
|
|
104
|
+
models: Dict[str, Any],
|
|
105
|
+
admin_authentication: AdminAuthentication,
|
|
106
|
+
mount_path: str,
|
|
107
|
+
theme: str,
|
|
108
|
+
secure_cookies: bool,
|
|
109
|
+
event_integration: Optional[Any] = None,
|
|
110
|
+
) -> None:
|
|
111
|
+
self.db_config = database_config
|
|
112
|
+
self.router = APIRouter()
|
|
113
|
+
self.templates = Jinja2Templates(directory=templates_directory)
|
|
114
|
+
self.models = models
|
|
115
|
+
self.admin_user_service = AdminUserService(db_config=database_config)
|
|
116
|
+
self.admin_authentication = admin_authentication
|
|
117
|
+
self.admin_user_service = admin_authentication.user_service
|
|
118
|
+
self.token_service = admin_authentication.token_service
|
|
119
|
+
|
|
120
|
+
self.mount_path = mount_path
|
|
121
|
+
self.theme = theme
|
|
122
|
+
self.event_integration = event_integration
|
|
123
|
+
|
|
124
|
+
self.session_manager = SessionManager(
|
|
125
|
+
self.db_config,
|
|
126
|
+
max_sessions_per_user=5,
|
|
127
|
+
session_timeout_minutes=30,
|
|
128
|
+
cleanup_interval_minutes=15,
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
self.secure_cookies = secure_cookies
|
|
132
|
+
|
|
133
|
+
def setup_routes(self) -> None:
|
|
134
|
+
"""
|
|
135
|
+
Configure all admin interface routes including auth, dashboard and model views.
|
|
136
|
+
|
|
137
|
+
Routes Created:
|
|
138
|
+
**Auth Routes:**
|
|
139
|
+
- POST /login - Handle login form submission
|
|
140
|
+
- GET /login - Display login page
|
|
141
|
+
- GET /logout - Process user logout
|
|
142
|
+
|
|
143
|
+
**Dashboard Routes:**
|
|
144
|
+
- GET / - Main dashboard view
|
|
145
|
+
- GET /dashboard-content - HTMX dashboard updates
|
|
146
|
+
|
|
147
|
+
Notes:
|
|
148
|
+
- All routes except login require authentication
|
|
149
|
+
- Routes use Jinja2 templates for rendering
|
|
150
|
+
- HTMX integration for dynamic updates
|
|
151
|
+
- Event logging integration if enabled
|
|
152
|
+
|
|
153
|
+
Example:
|
|
154
|
+
```python
|
|
155
|
+
admin_site = AdminSite(...)
|
|
156
|
+
admin_site.setup_routes()
|
|
157
|
+
app.include_router(admin_site.router)
|
|
158
|
+
```
|
|
159
|
+
"""
|
|
160
|
+
self.router.add_api_route(
|
|
161
|
+
"/login",
|
|
162
|
+
self.login_page(),
|
|
163
|
+
methods=["POST"],
|
|
164
|
+
include_in_schema=False,
|
|
165
|
+
response_model=None,
|
|
166
|
+
)
|
|
167
|
+
self.router.add_api_route(
|
|
168
|
+
"/logout",
|
|
169
|
+
self.logout_endpoint(),
|
|
170
|
+
methods=["GET"],
|
|
171
|
+
include_in_schema=False,
|
|
172
|
+
dependencies=[Depends(self.admin_authentication.get_current_user)],
|
|
173
|
+
response_model=None,
|
|
174
|
+
)
|
|
175
|
+
self.router.add_api_route(
|
|
176
|
+
"/login",
|
|
177
|
+
self.admin_login_page(),
|
|
178
|
+
methods=["GET"],
|
|
179
|
+
include_in_schema=False,
|
|
180
|
+
response_model=None,
|
|
181
|
+
)
|
|
182
|
+
self.router.add_api_route(
|
|
183
|
+
"/dashboard-content",
|
|
184
|
+
self.dashboard_content(),
|
|
185
|
+
methods=["GET"],
|
|
186
|
+
include_in_schema=False,
|
|
187
|
+
dependencies=[Depends(self.admin_authentication.get_current_user)],
|
|
188
|
+
response_model=None,
|
|
189
|
+
)
|
|
190
|
+
self.router.add_api_route(
|
|
191
|
+
"/",
|
|
192
|
+
self.dashboard_page(),
|
|
193
|
+
methods=["GET"],
|
|
194
|
+
include_in_schema=False,
|
|
195
|
+
dependencies=[Depends(self.admin_authentication.get_current_user)],
|
|
196
|
+
response_model=None,
|
|
197
|
+
)
|
|
198
|
+
|
|
199
|
+
def login_page(self) -> EndpointCallable:
|
|
200
|
+
"""
|
|
201
|
+
Create login form handler for admin authentication.
|
|
202
|
+
|
|
203
|
+
Returns:
|
|
204
|
+
FastAPI route handler that processes login form submission.
|
|
205
|
+
|
|
206
|
+
Notes:
|
|
207
|
+
- Validates credentials and creates user session on success
|
|
208
|
+
- Sets secure cookies with tokens
|
|
209
|
+
- Logs login attempts if event tracking enabled
|
|
210
|
+
"""
|
|
211
|
+
|
|
212
|
+
@log_auth_action(EventType.LOGIN)
|
|
213
|
+
async def login_page_inner(
|
|
214
|
+
request: Request,
|
|
215
|
+
response: Response,
|
|
216
|
+
form_data: OAuth2PasswordRequestForm = Depends(),
|
|
217
|
+
db: AsyncSession = Depends(self.db_config.get_admin_db),
|
|
218
|
+
event_integration: Optional[Any] = Depends(lambda: self.event_integration),
|
|
219
|
+
) -> RouteResponse:
|
|
220
|
+
logger.info("Processing login attempt...")
|
|
221
|
+
try:
|
|
222
|
+
user = await self.admin_user_service.authenticate_user(
|
|
223
|
+
form_data.username, form_data.password, db=db
|
|
224
|
+
)
|
|
225
|
+
if not user:
|
|
226
|
+
logger.warning(
|
|
227
|
+
f"Authentication failed for user: {form_data.username}"
|
|
228
|
+
)
|
|
229
|
+
return self.templates.TemplateResponse(
|
|
230
|
+
"auth/login.html",
|
|
231
|
+
{
|
|
232
|
+
"request": request,
|
|
233
|
+
"error": "Invalid credentials. Please try again.",
|
|
234
|
+
"mount_path": self.mount_path,
|
|
235
|
+
"theme": self.theme,
|
|
236
|
+
},
|
|
237
|
+
)
|
|
238
|
+
|
|
239
|
+
request.state.user = user
|
|
240
|
+
logger.info("User authenticated successfully, creating token")
|
|
241
|
+
access_token_expires = timedelta(
|
|
242
|
+
minutes=self.token_service.ACCESS_TOKEN_EXPIRE_MINUTES
|
|
243
|
+
)
|
|
244
|
+
access_token = await self.token_service.create_access_token(
|
|
245
|
+
data={"sub": user["username"]}, expires_delta=access_token_expires
|
|
246
|
+
)
|
|
247
|
+
|
|
248
|
+
try:
|
|
249
|
+
logger.info("Creating user session...")
|
|
250
|
+
session = await self.session_manager.create_session(
|
|
251
|
+
request=request,
|
|
252
|
+
user_id=user["id"],
|
|
253
|
+
metadata={
|
|
254
|
+
"login_type": "password",
|
|
255
|
+
"username": user["username"],
|
|
256
|
+
"creation_time": datetime.now(timezone.utc).isoformat(),
|
|
257
|
+
},
|
|
258
|
+
)
|
|
259
|
+
|
|
260
|
+
if not session:
|
|
261
|
+
logger.error("Failed to create session")
|
|
262
|
+
raise Exception("Session creation failed")
|
|
263
|
+
|
|
264
|
+
logger.info(f"Session created successfully: {session.session_id}")
|
|
265
|
+
|
|
266
|
+
response = RedirectResponse(
|
|
267
|
+
url=f"/{self.mount_path}/", status_code=303
|
|
268
|
+
)
|
|
269
|
+
|
|
270
|
+
max_age_int = int(access_token_expires.total_seconds())
|
|
271
|
+
|
|
272
|
+
response.set_cookie(
|
|
273
|
+
key="access_token",
|
|
274
|
+
value=f"Bearer {access_token}",
|
|
275
|
+
httponly=True,
|
|
276
|
+
secure=self.secure_cookies,
|
|
277
|
+
max_age=max_age_int,
|
|
278
|
+
path=f"/{self.mount_path}",
|
|
279
|
+
samesite="lax",
|
|
280
|
+
)
|
|
281
|
+
|
|
282
|
+
response.set_cookie(
|
|
283
|
+
key="session_id",
|
|
284
|
+
value=session.session_id,
|
|
285
|
+
httponly=True,
|
|
286
|
+
secure=self.secure_cookies,
|
|
287
|
+
max_age=max_age_int,
|
|
288
|
+
path=f"/{self.mount_path}",
|
|
289
|
+
samesite="lax",
|
|
290
|
+
)
|
|
291
|
+
|
|
292
|
+
await db.commit()
|
|
293
|
+
logger.info("Login completed successfully")
|
|
294
|
+
return response
|
|
295
|
+
|
|
296
|
+
except Exception as e:
|
|
297
|
+
logger.error(
|
|
298
|
+
f"Error during session creation: {str(e)}", exc_info=True
|
|
299
|
+
)
|
|
300
|
+
await db.rollback()
|
|
301
|
+
return self.templates.TemplateResponse(
|
|
302
|
+
"auth/login.html",
|
|
303
|
+
{
|
|
304
|
+
"request": request,
|
|
305
|
+
"error": f"Error creating session: {str(e)}",
|
|
306
|
+
"mount_path": self.mount_path,
|
|
307
|
+
"theme": self.theme,
|
|
308
|
+
},
|
|
309
|
+
)
|
|
310
|
+
|
|
311
|
+
except Exception as e:
|
|
312
|
+
logger.error(f"Error during login: {str(e)}", exc_info=True)
|
|
313
|
+
return self.templates.TemplateResponse(
|
|
314
|
+
"auth/login.html",
|
|
315
|
+
{
|
|
316
|
+
"request": request,
|
|
317
|
+
"error": "An error occurred during login. Please try again.",
|
|
318
|
+
"mount_path": self.mount_path,
|
|
319
|
+
"theme": self.theme,
|
|
320
|
+
},
|
|
321
|
+
)
|
|
322
|
+
|
|
323
|
+
return cast(EndpointCallable, login_page_inner)
|
|
324
|
+
|
|
325
|
+
def logout_endpoint(self) -> EndpointCallable:
|
|
326
|
+
"""
|
|
327
|
+
Create logout handler for admin authentication.
|
|
328
|
+
|
|
329
|
+
Returns:
|
|
330
|
+
FastAPI route handler that terminates session and clears auth cookies.
|
|
331
|
+
|
|
332
|
+
Notes:
|
|
333
|
+
- Revokes access tokens
|
|
334
|
+
- Terminates active sessions
|
|
335
|
+
- Cleans up auth cookies
|
|
336
|
+
- Logs logout events if tracking enabled
|
|
337
|
+
"""
|
|
338
|
+
|
|
339
|
+
@log_auth_action(EventType.LOGOUT)
|
|
340
|
+
async def logout_endpoint_inner(
|
|
341
|
+
request: Request,
|
|
342
|
+
response: Response,
|
|
343
|
+
db: AsyncSession = Depends(self.db_config.get_admin_db),
|
|
344
|
+
access_token: Optional[str] = Cookie(None),
|
|
345
|
+
session_id: Optional[str] = Cookie(None),
|
|
346
|
+
event_integration: Optional[Any] = Depends(lambda: self.event_integration),
|
|
347
|
+
) -> RouteResponse:
|
|
348
|
+
if access_token:
|
|
349
|
+
token = (
|
|
350
|
+
access_token.replace("Bearer ", "")
|
|
351
|
+
if access_token.startswith("Bearer ")
|
|
352
|
+
else access_token
|
|
353
|
+
)
|
|
354
|
+
token_data = await self.token_service.verify_token(token, db)
|
|
355
|
+
if token_data:
|
|
356
|
+
if "@" in token_data.username_or_email:
|
|
357
|
+
user = await self.db_config.crud_users.get(
|
|
358
|
+
db=db, email=token_data.username_or_email
|
|
359
|
+
)
|
|
360
|
+
else:
|
|
361
|
+
user = await self.db_config.crud_users.get(
|
|
362
|
+
db=db, username=token_data.username_or_email
|
|
363
|
+
)
|
|
364
|
+
if user:
|
|
365
|
+
request.state.user = user
|
|
366
|
+
|
|
367
|
+
await self.token_service.blacklist_token(token, db)
|
|
368
|
+
|
|
369
|
+
if session_id:
|
|
370
|
+
await self.session_manager.terminate_session(
|
|
371
|
+
db=db, session_id=session_id
|
|
372
|
+
)
|
|
373
|
+
|
|
374
|
+
response = RedirectResponse(
|
|
375
|
+
url=f"/{self.mount_path}/login", status_code=303
|
|
376
|
+
)
|
|
377
|
+
|
|
378
|
+
response.delete_cookie(key="access_token", path=f"/{self.mount_path}")
|
|
379
|
+
response.delete_cookie(key="session_id", path=f"/{self.mount_path}")
|
|
380
|
+
|
|
381
|
+
return response
|
|
382
|
+
|
|
383
|
+
return cast(EndpointCallable, logout_endpoint_inner)
|
|
384
|
+
|
|
385
|
+
def admin_login_page(self) -> EndpointCallable:
|
|
386
|
+
"""
|
|
387
|
+
Create login page handler for the admin interface.
|
|
388
|
+
|
|
389
|
+
Returns:
|
|
390
|
+
FastAPI route handler for login page
|
|
391
|
+
|
|
392
|
+
Notes:
|
|
393
|
+
- Checks for existing auth cookies
|
|
394
|
+
- Validates active sessions
|
|
395
|
+
- Redirects authenticated users to dashboard
|
|
396
|
+
- Displays login form with any error messages
|
|
397
|
+
"""
|
|
398
|
+
|
|
399
|
+
async def admin_login_page_inner(
|
|
400
|
+
request: Request,
|
|
401
|
+
db: AsyncSession = Depends(self.db_config.get_admin_db),
|
|
402
|
+
) -> RouteResponse:
|
|
403
|
+
try:
|
|
404
|
+
access_token = request.cookies.get("access_token")
|
|
405
|
+
session_id = request.cookies.get("session_id")
|
|
406
|
+
|
|
407
|
+
if access_token and session_id:
|
|
408
|
+
token = (
|
|
409
|
+
access_token.split(" ")[1]
|
|
410
|
+
if access_token.startswith("Bearer ")
|
|
411
|
+
else access_token
|
|
412
|
+
)
|
|
413
|
+
token_data = await self.token_service.verify_token(token, db)
|
|
414
|
+
|
|
415
|
+
if token_data:
|
|
416
|
+
is_valid_session = await self.session_manager.validate_session(
|
|
417
|
+
db=db, session_id=session_id
|
|
418
|
+
)
|
|
419
|
+
|
|
420
|
+
if is_valid_session:
|
|
421
|
+
return RedirectResponse(
|
|
422
|
+
url=f"/{self.mount_path}/", status_code=303
|
|
423
|
+
)
|
|
424
|
+
|
|
425
|
+
except Exception:
|
|
426
|
+
pass
|
|
427
|
+
|
|
428
|
+
error = request.query_params.get("error")
|
|
429
|
+
return self.templates.TemplateResponse(
|
|
430
|
+
"auth/login.html",
|
|
431
|
+
{
|
|
432
|
+
"request": request,
|
|
433
|
+
"mount_path": self.mount_path,
|
|
434
|
+
"theme": self.theme,
|
|
435
|
+
"error": error,
|
|
436
|
+
},
|
|
437
|
+
)
|
|
438
|
+
|
|
439
|
+
return cast(EndpointCallable, admin_login_page_inner)
|
|
440
|
+
|
|
441
|
+
def dashboard_content(self) -> EndpointCallable:
|
|
442
|
+
"""
|
|
443
|
+
Create dashboard content handler for HTMX dynamic updates.
|
|
444
|
+
|
|
445
|
+
Returns:
|
|
446
|
+
FastAPI route handler for dashboard content
|
|
447
|
+
"""
|
|
448
|
+
|
|
449
|
+
async def dashboard_content_inner(
|
|
450
|
+
request: Request,
|
|
451
|
+
admin_db: AsyncSession = Depends(self.db_config.get_admin_db),
|
|
452
|
+
app_db: AsyncSession = Depends(
|
|
453
|
+
cast(
|
|
454
|
+
Callable[..., AsyncGenerator[AsyncSession, None]],
|
|
455
|
+
self.db_config.session,
|
|
456
|
+
)
|
|
457
|
+
),
|
|
458
|
+
) -> RouteResponse:
|
|
459
|
+
"""
|
|
460
|
+
Renders partial content for the dashboard (HTMX).
|
|
461
|
+
"""
|
|
462
|
+
context = await self.get_base_context(admin_db=admin_db, app_db=app_db)
|
|
463
|
+
context.update({"request": request})
|
|
464
|
+
return self.templates.TemplateResponse(
|
|
465
|
+
"admin/dashboard/dashboard_content.html", context
|
|
466
|
+
)
|
|
467
|
+
|
|
468
|
+
return cast(EndpointCallable, dashboard_content_inner)
|
|
469
|
+
|
|
470
|
+
async def get_base_context(
|
|
471
|
+
self, admin_db: AsyncSession, app_db: AsyncSession
|
|
472
|
+
) -> Dict[str, Any]:
|
|
473
|
+
"""
|
|
474
|
+
Get common context data needed for base template.
|
|
475
|
+
|
|
476
|
+
Args:
|
|
477
|
+
db: Database session for queries
|
|
478
|
+
|
|
479
|
+
Returns:
|
|
480
|
+
Dictionary containing auth tables, model data, and config
|
|
481
|
+
|
|
482
|
+
Notes:
|
|
483
|
+
- Queries model counts asynchronously
|
|
484
|
+
- Includes auth model stats and status
|
|
485
|
+
- Required by all admin templates
|
|
486
|
+
"""
|
|
487
|
+
auth_model_counts: Dict[str, int] = {}
|
|
488
|
+
for model_name, model_data in self.admin_authentication.auth_models.items():
|
|
489
|
+
crud_obj = cast(FastCRUD, model_data["crud"])
|
|
490
|
+
if model_name == "AdminSession":
|
|
491
|
+
total_count = await crud_obj.count(self.db_config.admin_session)
|
|
492
|
+
active_count = await crud_obj.count(
|
|
493
|
+
self.db_config.admin_session, is_active=True
|
|
494
|
+
)
|
|
495
|
+
auth_model_counts[model_name] = total_count
|
|
496
|
+
auth_model_counts[f"{model_name}_active"] = active_count
|
|
497
|
+
else:
|
|
498
|
+
count = await crud_obj.count(self.db_config.admin_session)
|
|
499
|
+
auth_model_counts[model_name] = count
|
|
500
|
+
|
|
501
|
+
model_counts: Dict[str, int] = {}
|
|
502
|
+
for model_name, model_data in self.models.items():
|
|
503
|
+
crud = cast(FastCRUD, model_data["crud"])
|
|
504
|
+
cnt = await crud.count(app_db)
|
|
505
|
+
model_counts[model_name] = cnt
|
|
506
|
+
|
|
507
|
+
return {
|
|
508
|
+
"auth_table_names": self.admin_authentication.auth_models.keys(),
|
|
509
|
+
"table_names": self.models.keys(),
|
|
510
|
+
"auth_model_counts": auth_model_counts,
|
|
511
|
+
"model_counts": model_counts,
|
|
512
|
+
"mount_path": self.mount_path,
|
|
513
|
+
"track_events": self.event_integration is not None,
|
|
514
|
+
"theme": self.theme,
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
def dashboard_page(self) -> EndpointCallable:
|
|
518
|
+
"""
|
|
519
|
+
Create main dashboard page handler.
|
|
520
|
+
|
|
521
|
+
Returns:
|
|
522
|
+
FastAPI route handler for the admin dashboard
|
|
523
|
+
"""
|
|
524
|
+
|
|
525
|
+
async def dashboard_page_inner(
|
|
526
|
+
request: Request,
|
|
527
|
+
admin_db: AsyncSession = Depends(self.db_config.get_admin_db),
|
|
528
|
+
app_db: AsyncSession = Depends(
|
|
529
|
+
cast(
|
|
530
|
+
Callable[..., AsyncGenerator[AsyncSession, None]],
|
|
531
|
+
self.db_config.session,
|
|
532
|
+
)
|
|
533
|
+
),
|
|
534
|
+
) -> RouteResponse:
|
|
535
|
+
context = await self.get_base_context(admin_db=admin_db, app_db=app_db)
|
|
536
|
+
context.update({"request": request, "include_sidebar_and_header": True})
|
|
537
|
+
return self.templates.TemplateResponse(
|
|
538
|
+
"admin/dashboard/dashboard.html", context
|
|
539
|
+
)
|
|
540
|
+
|
|
541
|
+
return cast(EndpointCallable, dashboard_page_inner)
|
|
542
|
+
|
|
543
|
+
def admin_auth_model_page(self, model_key: str) -> EndpointCallable:
|
|
544
|
+
"""
|
|
545
|
+
Create page handler for authentication model views.
|
|
546
|
+
|
|
547
|
+
Args:
|
|
548
|
+
model_key: Name of authentication model to display
|
|
549
|
+
|
|
550
|
+
Returns:
|
|
551
|
+
FastAPI route handler for auth model list view
|
|
552
|
+
|
|
553
|
+
Notes:
|
|
554
|
+
- Handles pagination and sorting
|
|
555
|
+
- Formats special fields like JSON
|
|
556
|
+
- Integrates with event logging if enabled
|
|
557
|
+
"""
|
|
558
|
+
|
|
559
|
+
async def admin_auth_model_page_inner(
|
|
560
|
+
request: Request,
|
|
561
|
+
admin_db: AsyncSession = Depends(self.db_config.get_admin_db),
|
|
562
|
+
db: AsyncSession = Depends(self.db_config.get_admin_db),
|
|
563
|
+
) -> RouteResponse:
|
|
564
|
+
auth_model = self.admin_authentication.auth_models[model_key]
|
|
565
|
+
sqlalchemy_model = cast(Any, auth_model["model"])
|
|
566
|
+
|
|
567
|
+
table_columns = []
|
|
568
|
+
if hasattr(sqlalchemy_model, "__table__"):
|
|
569
|
+
table_columns = [
|
|
570
|
+
column.key for column in sqlalchemy_model.__table__.columns
|
|
571
|
+
]
|
|
572
|
+
|
|
573
|
+
page_str = request.query_params.get("page", "1")
|
|
574
|
+
limit_str = request.query_params.get("rows-per-page-select", "10")
|
|
575
|
+
|
|
576
|
+
try:
|
|
577
|
+
page = int(page_str)
|
|
578
|
+
limit = int(limit_str)
|
|
579
|
+
except ValueError:
|
|
580
|
+
page = 1
|
|
581
|
+
limit = 10
|
|
582
|
+
|
|
583
|
+
offset = (page - 1) * limit
|
|
584
|
+
items: Dict[str, Any] = {"data": [], "total_count": 0}
|
|
585
|
+
try:
|
|
586
|
+
crud = cast(FastCRUD, auth_model["crud"])
|
|
587
|
+
fetched = await crud.get_multi(db=admin_db, offset=offset, limit=limit)
|
|
588
|
+
items = dict(fetched)
|
|
589
|
+
|
|
590
|
+
logger.info(f"Retrieved items for {model_key}: {items}")
|
|
591
|
+
total_items = items.get("total_count", 0)
|
|
592
|
+
|
|
593
|
+
if model_key == "AdminSession":
|
|
594
|
+
formatted_items = []
|
|
595
|
+
data = items["data"]
|
|
596
|
+
for item in data:
|
|
597
|
+
if not isinstance(item, dict):
|
|
598
|
+
item = {
|
|
599
|
+
k: v
|
|
600
|
+
for k, v in vars(item).items()
|
|
601
|
+
if not k.startswith("_")
|
|
602
|
+
}
|
|
603
|
+
if "device_info" in item and isinstance(
|
|
604
|
+
item["device_info"], dict
|
|
605
|
+
):
|
|
606
|
+
item["device_info"] = str(item["device_info"])
|
|
607
|
+
if "session_metadata" in item and isinstance(
|
|
608
|
+
item["session_metadata"], dict
|
|
609
|
+
):
|
|
610
|
+
item["session_metadata"] = str(item["session_metadata"])
|
|
611
|
+
formatted_items.append(item)
|
|
612
|
+
items["data"] = formatted_items
|
|
613
|
+
except Exception as e:
|
|
614
|
+
logger.error(
|
|
615
|
+
f"Error retrieving {model_key} data: {str(e)}", exc_info=True
|
|
616
|
+
)
|
|
617
|
+
total_items = 0
|
|
618
|
+
|
|
619
|
+
total_pages = max(1, (total_items + limit - 1) // limit)
|
|
620
|
+
|
|
621
|
+
context = await self.get_base_context(admin_db=admin_db, app_db=db)
|
|
622
|
+
context.update(
|
|
623
|
+
{
|
|
624
|
+
"request": request,
|
|
625
|
+
"model_items": items["data"],
|
|
626
|
+
"model_name": model_key,
|
|
627
|
+
"table_columns": table_columns,
|
|
628
|
+
"current_page": page,
|
|
629
|
+
"rows_per_page": limit,
|
|
630
|
+
"total_items": total_items,
|
|
631
|
+
"total_pages": total_pages,
|
|
632
|
+
"primary_key_info": self.db_config.get_primary_key_info(
|
|
633
|
+
cast(Any, sqlalchemy_model)
|
|
634
|
+
),
|
|
635
|
+
"sort_column": None,
|
|
636
|
+
"sort_order": "asc",
|
|
637
|
+
"include_sidebar_and_header": True,
|
|
638
|
+
}
|
|
639
|
+
)
|
|
640
|
+
|
|
641
|
+
return self.templates.TemplateResponse("admin/model/list.html", context)
|
|
642
|
+
|
|
643
|
+
return cast(EndpointCallable, admin_auth_model_page_inner)
|