paskia 0.9.0__py3-none-any.whl → 0.10.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. paskia/_version.py +2 -2
  2. paskia/aaguid/__init__.py +5 -4
  3. paskia/authsession.py +4 -19
  4. paskia/db/__init__.py +2 -4
  5. paskia/db/background.py +3 -3
  6. paskia/db/jsonl.py +99 -111
  7. paskia/db/logging.py +318 -0
  8. paskia/db/migrations.py +19 -20
  9. paskia/db/operations.py +107 -196
  10. paskia/db/structs.py +236 -46
  11. paskia/fastapi/__main__.py +13 -6
  12. paskia/fastapi/admin.py +72 -195
  13. paskia/fastapi/api.py +56 -58
  14. paskia/fastapi/authz.py +3 -8
  15. paskia/fastapi/logging.py +261 -0
  16. paskia/fastapi/mainapp.py +14 -3
  17. paskia/fastapi/remote.py +11 -37
  18. paskia/fastapi/reset.py +0 -2
  19. paskia/fastapi/response.py +22 -0
  20. paskia/fastapi/user.py +7 -7
  21. paskia/fastapi/ws.py +14 -37
  22. paskia/fastapi/wschat.py +55 -2
  23. paskia/fastapi/wsutil.py +10 -2
  24. paskia/frontend-build/auth/admin/index.html +6 -6
  25. paskia/frontend-build/auth/assets/AccessDenied-C29NZI95.css +1 -0
  26. paskia/frontend-build/auth/assets/AccessDenied-DAdzg_MJ.js +12 -0
  27. paskia/frontend-build/auth/assets/{RestrictedAuth-CvR33_Z0.css → RestrictedAuth-BOdNrlQB.css} +1 -1
  28. paskia/frontend-build/auth/assets/{RestrictedAuth-DsJXicIw.js → RestrictedAuth-BSusdAfp.js} +1 -1
  29. paskia/frontend-build/auth/assets/_plugin-vue_export-helper-D2l53SUz.js +49 -0
  30. paskia/frontend-build/auth/assets/_plugin-vue_export-helper-DYJ24FZK.css +1 -0
  31. paskia/frontend-build/auth/assets/admin-BeFvGyD6.js +1 -0
  32. paskia/frontend-build/auth/assets/{admin-DzzjSg72.css → admin-CmNtuH3s.css} +1 -1
  33. paskia/frontend-build/auth/assets/{auth-C7k64Wad.css → auth-BKq4T2K2.css} +1 -1
  34. paskia/frontend-build/auth/assets/auth-DvHf8hgy.js +1 -0
  35. paskia/frontend-build/auth/assets/{forward-DmqVHZ7e.js → forward-C86Jm_Uq.js} +1 -1
  36. paskia/frontend-build/auth/assets/reset-B8PlNXuP.css +1 -0
  37. paskia/frontend-build/auth/assets/reset-D71FG0VL.js +1 -0
  38. paskia/frontend-build/auth/assets/{restricted-D3AJx3_6.js → restricted-CW0drE_k.js} +1 -1
  39. paskia/frontend-build/auth/index.html +6 -6
  40. paskia/frontend-build/auth/restricted/index.html +5 -5
  41. paskia/frontend-build/int/forward/index.html +5 -5
  42. paskia/frontend-build/int/reset/index.html +4 -4
  43. paskia/migrate/__init__.py +9 -9
  44. paskia/migrate/sql.py +26 -19
  45. paskia/remoteauth.py +6 -6
  46. {paskia-0.9.0.dist-info → paskia-0.10.0.dist-info}/METADATA +1 -1
  47. paskia-0.10.0.dist-info/RECORD +60 -0
  48. paskia/frontend-build/auth/assets/AccessDenied-DPkUS8LZ.css +0 -1
  49. paskia/frontend-build/auth/assets/AccessDenied-Fmeb6EtF.js +0 -8
  50. paskia/frontend-build/auth/assets/_plugin-vue_export-helper-BTzJAQlS.css +0 -1
  51. paskia/frontend-build/auth/assets/_plugin-vue_export-helper-nhjnO_bd.js +0 -2
  52. paskia/frontend-build/auth/assets/admin-CPE1pLMm.js +0 -1
  53. paskia/frontend-build/auth/assets/auth-YIZvPlW_.js +0 -1
  54. paskia/frontend-build/auth/assets/reset-Chtv69AT.css +0 -1
  55. paskia/frontend-build/auth/assets/reset-s20PATTN.js +0 -1
  56. paskia-0.9.0.dist-info/RECORD +0 -57
  57. {paskia-0.9.0.dist-info → paskia-0.10.0.dist-info}/WHEEL +0 -0
  58. {paskia-0.9.0.dist-info → paskia-0.10.0.dist-info}/entry_points.txt +0 -0
paskia/db/operations.py CHANGED
@@ -2,7 +2,7 @@
2
2
  Database for WebAuthn passkey authentication.
3
3
 
4
4
  Read operations: Access _db directly, use build_* helpers to get public structs.
5
- Context lookup: get_session_context() returns full SessionContext with effective permissions.
5
+ Context lookup: _db.session_ctx() returns full SessionContext with effective permissions.
6
6
  Write operations: Functions that validate and commit, or raise ValueError.
7
7
  """
8
8
 
@@ -10,7 +10,7 @@ import hashlib
10
10
  import logging
11
11
  import os
12
12
  import secrets
13
- from datetime import datetime, timezone
13
+ from datetime import UTC, datetime
14
14
  from uuid import UUID
15
15
 
16
16
  import uuid7
@@ -31,7 +31,6 @@ from paskia.db.structs import (
31
31
  SessionContext,
32
32
  User,
33
33
  )
34
- from paskia.util.hostutil import normalize_host
35
34
  from paskia.util.passphrase import generate as generate_passphrase
36
35
  from paskia.util.passphrase import is_well_formed as _is_passphrase
37
36
 
@@ -69,21 +68,18 @@ def get_user_organization(user_uuid: UUID) -> tuple[Org, str]:
69
68
  Raises ValueError if user not found.
70
69
 
71
70
  Call sites:
72
- - Get user's organization when updating user role (admin.py:493)
73
- - Get user's organization for user credential listing (admin.py:530)
74
- - Get user's organization for user details API (admin.py:579)
75
- - Get user's organization for updating user display name (admin.py:721)
76
- - Get user's organization for deleting user credential (admin.py:754)
77
- - Get user's organization for deleting user session (admin.py:783)
71
+ - update_user_role_in_organization: org only
72
+ - admin_create_user_registration_link: org only
73
+ - admin_get_user_detail: org and role
74
+ - admin_update_user_display_name: org only
75
+ - admin_delete_user_credential: org only
76
+ - admin_delete_user_session: org only
78
77
  """
79
78
  if user_uuid not in _db.users:
80
79
  raise ValueError(f"User {user_uuid} not found")
81
- role_uuid = _db.users[user_uuid].role
82
- if role_uuid not in _db.roles:
83
- raise ValueError(f"Role {role_uuid} not found")
84
- role_data = _db.roles[role_uuid]
85
- org_uuid = role_data.org
86
- return _db.orgs[org_uuid], role_data.display_name
80
+ user = _db.users[user_uuid]
81
+ role = user.role
82
+ return role.org, role.display_name
87
83
 
88
84
 
89
85
  def get_organization_users(org_uuid: UUID) -> list[tuple[User, str]]:
@@ -91,10 +87,8 @@ def get_organization_users(org_uuid: UUID) -> list[tuple[User, str]]:
91
87
 
92
88
  Returns list of (User, role_display_name) tuples.
93
89
  """
94
- role_map = {
95
- rid: r.display_name for rid, r in _db.roles.items() if r.org == org_uuid
96
- }
97
- return [(u, role_map[u.role]) for u in _db.users.values() if u.role in role_map]
90
+ org = _db.orgs[org_uuid]
91
+ return [(u, u.role.display_name) for role in org.roles for u in role.users]
98
92
 
99
93
 
100
94
  def get_user_credential_ids(user_uuid: UUID) -> list[bytes]:
@@ -102,7 +96,8 @@ def get_user_credential_ids(user_uuid: UUID) -> list[bytes]:
102
96
 
103
97
  Returns empty list if user has no credentials.
104
98
  """
105
- return [c.credential_id for c in _db.credentials.values() if c.user == user_uuid]
99
+ assert user_uuid
100
+ return [c.credential_id for c in _db.users[user_uuid].credentials]
106
101
 
107
102
 
108
103
  def _reset_key(passphrase: str) -> bytes:
@@ -126,92 +121,6 @@ def get_reset_token(passphrase: str) -> ResetToken | None:
126
121
  return _db.reset_tokens.get(key)
127
122
 
128
123
 
129
- # -------------------------------------------------------------------------
130
- # Context lookup
131
- # -------------------------------------------------------------------------
132
-
133
-
134
- def get_session_context(
135
- session_key: str, host: str | None = None
136
- ) -> SessionContext | None:
137
- """Get full session context with effective permissions.
138
-
139
- Args:
140
- session_key: The session key string
141
- host: Optional host for binding/validation and domain-scoped permissions
142
-
143
- Returns:
144
- SessionContext if valid, None if session not found, expired, or host mismatch
145
-
146
- Call sites:
147
- - Example usage in docstring (db/__init__.py:16)
148
- - Get session context from auth token (util/permutil.py:43)
149
- """
150
-
151
- if session_key not in _db.sessions:
152
- return None
153
-
154
- s = _db.sessions[session_key]
155
- if s.expiry < datetime.now(timezone.utc):
156
- return None
157
-
158
- # Validate host matches (sessions are always created with a host)
159
- if host is not None and s.host != host:
160
- # Session bound to different host
161
- return None
162
-
163
- # Validate user exists
164
- if s.user not in _db.users:
165
- return None
166
-
167
- # Validate role exists
168
- role_uuid = _db.users[s.user].role
169
- if role_uuid not in _db.roles:
170
- return None
171
-
172
- # Validate org exists
173
- org_uuid = _db.roles[role_uuid].org
174
- if org_uuid not in _db.orgs:
175
- return None
176
-
177
- session = _db.sessions[session_key]
178
- user = _db.users[s.user]
179
- role = _db.roles[role_uuid]
180
- org = _db.orgs[org_uuid]
181
-
182
- # Credential must exist (sessions are cascade-deleted when credential is deleted)
183
- if s.credential not in _db.credentials:
184
- return None
185
- credential = _db.credentials[s.credential]
186
-
187
- # Effective permissions: role's permissions that the org can grant
188
- # Also filter by domain if host is provided
189
- org_perm_uuids = {pid for pid, p in _db.permissions.items() if org_uuid in p.orgs}
190
- normalized_host = normalize_host(host)
191
- host_without_port = normalized_host.rsplit(":", 1)[0] if normalized_host else None
192
-
193
- effective_perms = []
194
- for perm_uuid in role.permission_set:
195
- if perm_uuid not in org_perm_uuids:
196
- continue
197
- if perm_uuid not in _db.permissions:
198
- continue
199
- p = _db.permissions[perm_uuid]
200
- # Check domain restriction
201
- if p.domain is not None and p.domain != host_without_port:
202
- continue
203
- effective_perms.append(_db.permissions[perm_uuid])
204
-
205
- return SessionContext(
206
- session=session,
207
- user=user,
208
- org=org,
209
- role=role,
210
- credential=credential,
211
- permissions=effective_perms,
212
- )
213
-
214
-
215
124
  # -------------------------------------------------------------------------
216
125
  # Write operations (validate, modify, commit or raise ValueError)
217
126
  # -------------------------------------------------------------------------
@@ -264,9 +173,9 @@ def create_org(org: Org, *, ctx: SessionContext | None = None) -> None:
264
173
  if org.uuid in _db.orgs:
265
174
  raise ValueError(f"Organization {org.uuid} already exists")
266
175
  with _db.transaction("admin:create_org", ctx):
267
- new_org = Org(display_name=org.display_name)
268
- _db.orgs[org.uuid] = new_org
176
+ new_org = Org.create(display_name=org.display_name)
269
177
  new_org.uuid = org.uuid
178
+ _db.orgs[org.uuid] = new_org
270
179
  # Create Administration role with org admin permission
271
180
 
272
181
  admin_role_uuid = uuid7.create()
@@ -278,7 +187,7 @@ def create_org(org: Org, *, ctx: SessionContext | None = None) -> None:
278
187
  break
279
188
  role_permissions = {org_admin_perm_uuid: True} if org_admin_perm_uuid else {}
280
189
  admin_role = Role(
281
- org=org.uuid,
190
+ org_uuid=org.uuid,
282
191
  display_name="Administration",
283
192
  permissions=role_permissions,
284
193
  )
@@ -304,17 +213,15 @@ def delete_org(uuid: UUID, *, ctx: SessionContext | None = None) -> None:
304
213
  if uuid not in _db.orgs:
305
214
  raise ValueError(f"Organization {uuid} not found")
306
215
  with _db.transaction("admin:delete_org", ctx):
216
+ org = _db.orgs[uuid]
307
217
  # Remove org from all permissions
308
218
  for p in _db.permissions.values():
309
219
  p.orgs.pop(uuid, None)
310
- # Delete roles in this org
311
- role_uuids = [rid for rid, r in _db.roles.items() if r.org == uuid]
312
- for rid in role_uuids:
313
- del _db.roles[rid]
314
- # Delete users with those roles
315
- user_uuids = [uid for uid, u in _db.users.items() if u.role in role_uuids]
316
- for uid in user_uuids:
317
- del _db.users[uid]
220
+ # Delete roles in this org and their users
221
+ for role in org.roles:
222
+ for user in role.users:
223
+ del _db.users[user.uuid]
224
+ del _db.roles[role.uuid]
318
225
  del _db.orgs[uuid]
319
226
 
320
227
 
@@ -356,8 +263,8 @@ def create_role(role: Role, *, ctx: SessionContext | None = None) -> None:
356
263
  """Create a new role."""
357
264
  if role.uuid in _db.roles:
358
265
  raise ValueError(f"Role {role.uuid} already exists")
359
- if role.org not in _db.orgs:
360
- raise ValueError(f"Organization {role.org} not found")
266
+ if role.org_uuid not in _db.orgs:
267
+ raise ValueError(f"Organization {role.org_uuid} not found")
361
268
  with _db.transaction("admin:create_role", ctx):
362
269
  _db.roles[role.uuid] = role
363
270
 
@@ -408,7 +315,8 @@ def delete_role(uuid: UUID, *, ctx: SessionContext | None = None) -> None:
408
315
  if uuid not in _db.roles:
409
316
  raise ValueError(f"Role {uuid} not found")
410
317
  # Check no users have this role
411
- if any(u.role == uuid for u in _db.users.values()):
318
+ role = _db.roles[uuid]
319
+ if role.users:
412
320
  raise ValueError(f"Cannot delete role {uuid}: users still assigned")
413
321
  with _db.transaction("admin:delete_role", ctx):
414
322
  del _db.roles[uuid]
@@ -418,8 +326,8 @@ def create_user(new_user: User, *, ctx: SessionContext | None = None) -> None:
418
326
  """Create a new user."""
419
327
  if new_user.uuid in _db.users:
420
328
  raise ValueError(f"User {new_user.uuid} already exists")
421
- if new_user.role not in _db.roles:
422
- raise ValueError(f"Role {new_user.role} not found")
329
+ if new_user.role_uuid not in _db.roles:
330
+ raise ValueError(f"Role {new_user.role_uuid} not found")
423
331
  with _db.transaction("admin:create_user", ctx):
424
332
  _db.users[new_user.uuid] = new_user
425
333
 
@@ -456,7 +364,7 @@ def update_user_role(
456
364
  if role_uuid not in _db.roles:
457
365
  raise ValueError(f"Role {role_uuid} not found")
458
366
  with _db.transaction("admin:update_user_role", ctx):
459
- _db.users[uuid].role = role_uuid
367
+ _db.users[uuid].role_uuid = role_uuid
460
368
 
461
369
 
462
370
  def update_user_role_in_organization(
@@ -468,39 +376,35 @@ def update_user_role_in_organization(
468
376
  """Update user's role by role name within their current organization."""
469
377
  if user_uuid not in _db.users:
470
378
  raise ValueError(f"User {user_uuid} not found")
471
- current_role_uuid = _db.users[user_uuid].role
472
- if current_role_uuid not in _db.roles:
473
- raise ValueError("Current role not found")
474
- org_uuid = _db.roles[current_role_uuid].org
379
+ user = _db.users[user_uuid]
380
+ org = user.org
475
381
  # Find role by name in the same org
476
382
  new_role_uuid = None
477
- for rid, r in _db.roles.items():
478
- if r.org == org_uuid and r.display_name == role_name:
479
- new_role_uuid = rid
383
+ for r in org.roles:
384
+ if r.display_name == role_name:
385
+ new_role_uuid = r.uuid
480
386
  break
481
387
  if new_role_uuid is None:
482
388
  raise ValueError(f"Role '{role_name}' not found in organization")
483
389
  with _db.transaction("admin:update_user_role", ctx):
484
- _db.users[user_uuid].role = new_role_uuid
390
+ _db.users[user_uuid].role_uuid = new_role_uuid
485
391
 
486
392
 
487
393
  def delete_user(uuid: UUID, *, ctx: SessionContext | None = None) -> None:
488
394
  """Delete user and their credentials/sessions."""
489
395
  if uuid not in _db.users:
490
396
  raise ValueError(f"User {uuid} not found")
397
+ user = _db.users[uuid]
491
398
  with _db.transaction("admin:delete_user", ctx):
492
399
  # Delete credentials
493
- cred_uuids = [cid for cid, c in _db.credentials.items() if c.user == uuid]
494
- for cid in cred_uuids:
495
- del _db.credentials[cid]
400
+ for cred in user.credentials:
401
+ del _db.credentials[cred.uuid]
496
402
  # Delete sessions
497
- sess_keys = [k for k, s in _db.sessions.items() if s.user == uuid]
498
- for k in sess_keys:
499
- del _db.sessions[k]
403
+ for sess in user.sessions:
404
+ del _db.sessions[sess.key]
500
405
  # Delete reset tokens
501
- token_keys = [k for k, t in _db.reset_tokens.items() if t.user == uuid]
502
- for k in token_keys:
503
- del _db.reset_tokens[k]
406
+ for token in user.reset_tokens:
407
+ del _db.reset_tokens[token.key]
504
408
  del _db.users[uuid]
505
409
 
506
410
 
@@ -508,8 +412,8 @@ def create_credential(cred: Credential, *, ctx: SessionContext | None = None) ->
508
412
  """Create a new credential."""
509
413
  if cred.uuid in _db.credentials:
510
414
  raise ValueError(f"Credential {cred.uuid} already exists")
511
- if cred.user not in _db.users:
512
- raise ValueError(f"User {cred.user} not found")
415
+ if cred.user_uuid not in _db.users:
416
+ raise ValueError(f"User {cred.user_uuid} not found")
513
417
  with _db.transaction("create_credential", ctx):
514
418
  _db.credentials[cred.uuid] = cred
515
419
 
@@ -542,20 +446,19 @@ def delete_credential(
542
446
  """
543
447
  if uuid not in _db.credentials:
544
448
  raise ValueError(f"Credential {uuid} not found")
449
+ cred = _db.credentials[uuid]
545
450
  if user_uuid is not None:
546
- cred_user = _db.credentials[uuid].user
547
- if cred_user != user_uuid:
451
+ if cred.user_uuid != user_uuid:
548
452
  raise ValueError(f"Credential {uuid} does not belong to user {user_uuid}")
549
453
  with _db.transaction("delete_credential", ctx):
550
454
  # Delete all sessions using this credential
551
- keys = [k for k, s in _db.sessions.items() if s.credential == uuid]
552
- for k in keys:
553
- del _db.sessions[k]
455
+ for sess in cred.sessions:
456
+ print(sess, repr(sess.key))
457
+ del _db.sessions[sess.key]
554
458
  del _db.credentials[uuid]
555
459
 
556
460
 
557
461
  def create_session(
558
- key: str,
559
462
  user_uuid: UUID,
560
463
  credential_uuid: UUID,
561
464
  host: str,
@@ -564,23 +467,25 @@ def create_session(
564
467
  expiry: datetime,
565
468
  *,
566
469
  ctx: SessionContext | None = None,
567
- ) -> None:
568
- """Create a new session."""
569
- if key in _db.sessions:
570
- raise ValueError("Session already exists")
470
+ ) -> str:
471
+ """Create a new session. Returns the session key."""
571
472
  if user_uuid not in _db.users:
572
473
  raise ValueError(f"User {user_uuid} not found")
573
474
  if credential_uuid not in _db.credentials:
574
475
  raise ValueError(f"Credential {credential_uuid} not found")
476
+ session = Session.create(
477
+ user=user_uuid,
478
+ credential=credential_uuid,
479
+ host=host,
480
+ ip=ip,
481
+ user_agent=user_agent,
482
+ expiry=expiry,
483
+ )
484
+ if session.key in _db.sessions:
485
+ raise ValueError("Session already exists")
575
486
  with _db.transaction("create_session", ctx):
576
- _db.sessions[key] = Session(
577
- user=user_uuid,
578
- credential=credential_uuid,
579
- host=host,
580
- ip=ip,
581
- user_agent=user_agent,
582
- expiry=expiry,
583
- )
487
+ _db.sessions[session.key] = session
488
+ return session.key
584
489
 
585
490
 
586
491
  def update_session(
@@ -612,16 +517,18 @@ def set_session_host(key: str, host: str, *, ctx: SessionContext | None = None)
612
517
  update_session(key, host=host, ctx=ctx)
613
518
 
614
519
 
615
- def delete_session(key: str, *, ctx: SessionContext | None = None) -> None:
520
+ def delete_session(
521
+ key: str, *, ctx: SessionContext | None = None, action: str = "delete_session"
522
+ ) -> None:
616
523
  """Delete a session.
617
524
 
618
525
  The acting user should be logged via ctx.
619
- For user logout, pass ctx of the user's session.
526
+ For user logout, pass ctx of the user's session and action="logout".
620
527
  For admin terminating a session, pass admin's ctx.
621
528
  """
622
529
  if key not in _db.sessions:
623
530
  raise ValueError("Session not found")
624
- with _db.transaction("delete_session", ctx):
531
+ with _db.transaction(action, ctx):
625
532
  del _db.sessions[key]
626
533
 
627
534
 
@@ -634,10 +541,12 @@ def delete_sessions_for_user(
634
541
  For user logout-all, pass ctx of the user's session.
635
542
  For admin bulk termination, pass admin's ctx.
636
543
  """
544
+ user = _db.users.get(user_uuid)
545
+ if not user:
546
+ return
637
547
  with _db.transaction("admin:delete_sessions_for_user", ctx):
638
- keys = [k for k, s in _db.sessions.items() if s.user == user_uuid]
639
- for k in keys:
640
- del _db.sessions[k]
548
+ for sess in user.sessions:
549
+ del _db.sessions[sess.key]
641
550
 
642
551
 
643
552
  def create_reset_token(
@@ -647,6 +556,7 @@ def create_reset_token(
647
556
  token_type: str,
648
557
  *,
649
558
  ctx: SessionContext | None = None,
559
+ user: str | None = None,
650
560
  ) -> None:
651
561
  """Create a reset token from a passphrase.
652
562
 
@@ -654,15 +564,16 @@ def create_reset_token(
654
564
  For self-service (user creating own recovery link), pass user's ctx.
655
565
  For admin operations, pass admin's ctx.
656
566
  For system operations (bootstrap), pass neither to log no user.
567
+ For API operations where ctx is not available but user is known, pass user.
657
568
  """
658
569
  key = _reset_key(passphrase)
659
570
  if key in _db.reset_tokens:
660
571
  raise ValueError("Reset token already exists")
661
572
  if user_uuid not in _db.users:
662
573
  raise ValueError(f"User {user_uuid} not found")
663
- with _db.transaction("create_reset_token", ctx):
574
+ with _db.transaction("create_reset_token", ctx, user=user):
664
575
  _db.reset_tokens[key] = ResetToken(
665
- user=user_uuid, expiry=expiry, token_type=token_type
576
+ user_uuid=user_uuid, expiry=expiry, token_type=token_type
666
577
  )
667
578
 
668
579
 
@@ -681,7 +592,7 @@ def delete_reset_token(key: bytes, *, ctx: SessionContext | None = None) -> None
681
592
 
682
593
  def cleanup_expired() -> int:
683
594
  """Remove expired sessions and reset tokens. Returns count removed."""
684
- now = datetime.now(timezone.utc)
595
+ now = datetime.now(UTC)
685
596
  count = 0
686
597
  with _db.transaction("expiry"):
687
598
  expired_sessions = [k for k, s in _db.sessions.items() if s.expiry < now]
@@ -726,13 +637,20 @@ def login(
726
637
  """
727
638
  if isinstance(user_uuid, str):
728
639
  user_uuid = UUID(user_uuid)
729
- now = datetime.now(timezone.utc)
640
+ now = datetime.now(UTC)
730
641
  if user_uuid not in _db.users:
731
642
  raise ValueError(f"User {user_uuid} not found")
732
643
  if credential_uuid not in _db.credentials:
733
644
  raise ValueError(f"Credential {credential_uuid} not found")
734
645
 
735
- session_key = _create_token()
646
+ session = Session.create(
647
+ user=user_uuid,
648
+ credential=credential_uuid,
649
+ host=host,
650
+ ip=ip,
651
+ user_agent=user_agent,
652
+ expiry=expiry,
653
+ )
736
654
  user_str = str(user_uuid)
737
655
  with _db.transaction("login", user=user_str):
738
656
  # Update user
@@ -742,15 +660,8 @@ def login(
742
660
  _db.credentials[credential_uuid].sign_count = sign_count
743
661
  _db.credentials[credential_uuid].last_used = now
744
662
  # Create session
745
- _db.sessions[session_key] = Session(
746
- user=user_uuid,
747
- credential=credential_uuid,
748
- host=host,
749
- ip=ip,
750
- user_agent=user_agent,
751
- expiry=expiry,
752
- )
753
- return session_key
663
+ _db.sessions[session.key] = session
664
+ return session.key
754
665
 
755
666
 
756
667
  def create_credential_session(
@@ -773,13 +684,20 @@ def create_credential_session(
773
684
  Returns the generated session token.
774
685
  """
775
686
 
776
- now = datetime.now(timezone.utc)
687
+ now = datetime.now(UTC)
777
688
  expiry = now + SESSION_LIFETIME
778
- session_key = _create_token()
779
689
 
780
690
  if user_uuid not in _db.users:
781
691
  raise ValueError(f"User {user_uuid} not found")
782
692
 
693
+ session = Session.create(
694
+ user=user_uuid,
695
+ credential=credential.uuid,
696
+ host=host,
697
+ ip=ip,
698
+ user_agent=user_agent,
699
+ expiry=expiry,
700
+ )
783
701
  user_str = str(user_uuid)
784
702
  with _db.transaction("create_credential_session", user=user_str):
785
703
  # Update display name if provided
@@ -790,20 +708,13 @@ def create_credential_session(
790
708
  _db.credentials[credential.uuid] = credential
791
709
 
792
710
  # Create session
793
- _db.sessions[session_key] = Session(
794
- user=user_uuid,
795
- credential=credential.uuid,
796
- host=host,
797
- ip=ip,
798
- user_agent=user_agent,
799
- expiry=expiry,
800
- )
711
+ _db.sessions[session.key] = session
801
712
 
802
713
  # Delete reset token if provided
803
714
  if reset_key:
804
715
  if reset_key in _db.reset_tokens:
805
716
  del _db.reset_tokens[reset_key]
806
- return session_key
717
+ return session.key
807
718
 
808
719
 
809
720
  # -------------------------------------------------------------------------
@@ -862,7 +773,7 @@ def bootstrap(
862
773
  reset_expiry = reset_expires()
863
774
  reset_key = _reset_key(reset_passphrase)
864
775
 
865
- now = datetime.now(timezone.utc)
776
+ now = datetime.now(UTC)
866
777
 
867
778
  with _db.transaction("bootstrap"):
868
779
  # Create auth:admin permission
@@ -884,13 +795,13 @@ def bootstrap(
884
795
  _db.permissions[perm_org_admin_uuid] = perm_org_admin
885
796
 
886
797
  # Create organization
887
- new_org = Org(display_name=org_name)
798
+ new_org = Org.create(display_name=org_name)
888
799
  new_org.uuid = org_uuid
889
800
  _db.orgs[org_uuid] = new_org
890
801
 
891
802
  # Create Administration role with both permissions
892
803
  admin_role = Role(
893
- org=org_uuid,
804
+ org_uuid=org_uuid,
894
805
  display_name="Administration",
895
806
  permissions={perm_admin_uuid: True, perm_org_admin_uuid: True},
896
807
  )
@@ -900,7 +811,7 @@ def bootstrap(
900
811
  # Create admin user
901
812
  admin_user = User(
902
813
  display_name=admin_name,
903
- role=role_uuid,
814
+ role_uuid=role_uuid,
904
815
  created_at=now,
905
816
  last_seen=None,
906
817
  visits=0,
@@ -910,7 +821,7 @@ def bootstrap(
910
821
 
911
822
  # Create reset token
912
823
  _db.reset_tokens[reset_key] = ResetToken(
913
- user=user_uuid,
824
+ user_uuid=user_uuid,
914
825
  expiry=reset_expiry,
915
826
  token_type="admin bootstrap",
916
827
  )