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.
Files changed (58) hide show
  1. crudadmin/__init__.py +3 -0
  2. crudadmin/admin_interface/__init__.py +0 -0
  3. crudadmin/admin_interface/admin_site.py +643 -0
  4. crudadmin/admin_interface/auth.py +122 -0
  5. crudadmin/admin_interface/crud_admin.py +1174 -0
  6. crudadmin/admin_interface/helper.py +122 -0
  7. crudadmin/admin_interface/middleware/__init__.py +9 -0
  8. crudadmin/admin_interface/middleware/auth.py +117 -0
  9. crudadmin/admin_interface/middleware/https.py +21 -0
  10. crudadmin/admin_interface/middleware/ip_restriction.py +89 -0
  11. crudadmin/admin_interface/model_view.py +1181 -0
  12. crudadmin/admin_interface/typing.py +7 -0
  13. crudadmin/admin_token/__init__.py +19 -0
  14. crudadmin/admin_token/models.py +35 -0
  15. crudadmin/admin_token/schemas.py +25 -0
  16. crudadmin/admin_token/service.py +119 -0
  17. crudadmin/admin_user/__init__.py +23 -0
  18. crudadmin/admin_user/models.py +29 -0
  19. crudadmin/admin_user/schemas.py +69 -0
  20. crudadmin/admin_user/service.py +128 -0
  21. crudadmin/core/__init__.py +21 -0
  22. crudadmin/core/db.py +257 -0
  23. crudadmin/core/exceptions.py +19 -0
  24. crudadmin/core/schemas/__init__.py +5 -0
  25. crudadmin/core/schemas/timestamp.py +23 -0
  26. crudadmin/event/__init__.py +38 -0
  27. crudadmin/event/decorators.py +327 -0
  28. crudadmin/event/integration.py +120 -0
  29. crudadmin/event/models.py +97 -0
  30. crudadmin/event/schemas.py +65 -0
  31. crudadmin/event/service.py +254 -0
  32. crudadmin/py.typed +0 -0
  33. crudadmin/session/__init__.py +17 -0
  34. crudadmin/session/manager.py +284 -0
  35. crudadmin/session/models.py +46 -0
  36. crudadmin/session/schemas.py +41 -0
  37. crudadmin/static/favicon.png +0 -0
  38. crudadmin/static/htmx.min.js +1 -0
  39. crudadmin/templates/admin/dashboard/dashboard.html +35 -0
  40. crudadmin/templates/admin/dashboard/dashboard_content.html +155 -0
  41. crudadmin/templates/admin/management/events.html +437 -0
  42. crudadmin/templates/admin/management/events_content.html +191 -0
  43. crudadmin/templates/admin/management/health.html +241 -0
  44. crudadmin/templates/admin/management/health_content.html +49 -0
  45. crudadmin/templates/admin/model/components/list_content.html +115 -0
  46. crudadmin/templates/admin/model/components/pagination.html +41 -0
  47. crudadmin/templates/admin/model/components/table_content.html +16 -0
  48. crudadmin/templates/admin/model/create.html +384 -0
  49. crudadmin/templates/admin/model/list.html +609 -0
  50. crudadmin/templates/admin/model/update.html +396 -0
  51. crudadmin/templates/auth/login.html +160 -0
  52. crudadmin/templates/base/base.html +418 -0
  53. crudadmin/templates/shared/utils/refresh.html +9 -0
  54. crudadmin-0.1.0.dist-info/METADATA +270 -0
  55. crudadmin-0.1.0.dist-info/RECORD +58 -0
  56. crudadmin-0.1.0.dist-info/WHEEL +4 -0
  57. crudadmin-0.1.0.dist-info/entry_points.txt +4 -0
  58. crudadmin-0.1.0.dist-info/licenses/LICENSE +21 -0
crudadmin/__init__.py ADDED
@@ -0,0 +1,3 @@
1
+ from .admin_interface.crud_admin import CRUDAdmin
2
+
3
+ __all__ = ["CRUDAdmin"]
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)