paskia 0.9.0__py3-none-any.whl → 0.9.1__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.
paskia/db/migrations.py CHANGED
@@ -5,30 +5,29 @@ Migrations are applied during database load based on the version field.
5
5
  Each migration should be idempotent and only run when needed.
6
6
  """
7
7
 
8
- import logging
8
+ from collections.abc import Awaitable, Callable
9
9
 
10
- _logger = logging.getLogger(__name__)
11
10
 
11
+ def migrate_v1(d: dict) -> None:
12
+ """Remove Org.created_at fields."""
13
+ for org_data in d["orgs"].values():
14
+ org_data.pop("created_at", None)
12
15
 
13
- def apply_migrations(data_dict: dict) -> bool:
14
- """Apply any pending schema migrations to the database dictionary.
15
16
 
16
- Args:
17
- data_dict: The raw database dictionary loaded from JSONL
17
+ migrations = sorted(
18
+ [f for n, f in globals().items() if n.startswith("migrate_v")],
19
+ key=lambda f: int(f.__name__.removeprefix("migrate_v")),
20
+ )
18
21
 
19
- Returns:
20
- True if any migrations were applied, False otherwise
21
- """
22
- db_version = data_dict.get("v", 0)
23
- migrated = False
22
+ DBVER = len(migrations) # Used by bootstrap and migrate:sql to set initial version
24
23
 
25
- if db_version == 0:
26
- # Migration v0 -> v1: Remove created_at from orgs (field removed from schema)
27
- if "orgs" in data_dict:
28
- for org_data in data_dict["orgs"].values():
29
- org_data.pop("created_at", None)
30
- data_dict["v"] = 1
31
- migrated = True
32
- _logger.info("Applied schema migration: v0 -> v1 (removed org.created_at)")
33
24
 
34
- return migrated
25
+ async def apply_all_migrations(
26
+ data_dict: dict,
27
+ current_version: int,
28
+ persist: Callable[[str, int, dict], Awaitable[None]],
29
+ ) -> None:
30
+ while current_version < DBVER:
31
+ migrations[current_version](data_dict)
32
+ current_version += 1
33
+ await persist(f"migrate:v{current_version}", current_version, data_dict)
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(
@@ -634,10 +539,12 @@ def delete_sessions_for_user(
634
539
  For user logout-all, pass ctx of the user's session.
635
540
  For admin bulk termination, pass admin's ctx.
636
541
  """
542
+ user = _db.users.get(user_uuid)
543
+ if not user:
544
+ return
637
545
  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]
546
+ for sess in user.sessions:
547
+ del _db.sessions[sess.key]
641
548
 
642
549
 
643
550
  def create_reset_token(
@@ -662,7 +569,7 @@ def create_reset_token(
662
569
  raise ValueError(f"User {user_uuid} not found")
663
570
  with _db.transaction("create_reset_token", ctx):
664
571
  _db.reset_tokens[key] = ResetToken(
665
- user=user_uuid, expiry=expiry, token_type=token_type
572
+ user_uuid=user_uuid, expiry=expiry, token_type=token_type
666
573
  )
667
574
 
668
575
 
@@ -681,7 +588,7 @@ def delete_reset_token(key: bytes, *, ctx: SessionContext | None = None) -> None
681
588
 
682
589
  def cleanup_expired() -> int:
683
590
  """Remove expired sessions and reset tokens. Returns count removed."""
684
- now = datetime.now(timezone.utc)
591
+ now = datetime.now(UTC)
685
592
  count = 0
686
593
  with _db.transaction("expiry"):
687
594
  expired_sessions = [k for k, s in _db.sessions.items() if s.expiry < now]
@@ -726,13 +633,20 @@ def login(
726
633
  """
727
634
  if isinstance(user_uuid, str):
728
635
  user_uuid = UUID(user_uuid)
729
- now = datetime.now(timezone.utc)
636
+ now = datetime.now(UTC)
730
637
  if user_uuid not in _db.users:
731
638
  raise ValueError(f"User {user_uuid} not found")
732
639
  if credential_uuid not in _db.credentials:
733
640
  raise ValueError(f"Credential {credential_uuid} not found")
734
641
 
735
- session_key = _create_token()
642
+ session = Session.create(
643
+ user=user_uuid,
644
+ credential=credential_uuid,
645
+ host=host,
646
+ ip=ip,
647
+ user_agent=user_agent,
648
+ expiry=expiry,
649
+ )
736
650
  user_str = str(user_uuid)
737
651
  with _db.transaction("login", user=user_str):
738
652
  # Update user
@@ -742,15 +656,8 @@ def login(
742
656
  _db.credentials[credential_uuid].sign_count = sign_count
743
657
  _db.credentials[credential_uuid].last_used = now
744
658
  # 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
659
+ _db.sessions[session.key] = session
660
+ return session.key
754
661
 
755
662
 
756
663
  def create_credential_session(
@@ -773,13 +680,20 @@ def create_credential_session(
773
680
  Returns the generated session token.
774
681
  """
775
682
 
776
- now = datetime.now(timezone.utc)
683
+ now = datetime.now(UTC)
777
684
  expiry = now + SESSION_LIFETIME
778
- session_key = _create_token()
779
685
 
780
686
  if user_uuid not in _db.users:
781
687
  raise ValueError(f"User {user_uuid} not found")
782
688
 
689
+ session = Session.create(
690
+ user=user_uuid,
691
+ credential=credential.uuid,
692
+ host=host,
693
+ ip=ip,
694
+ user_agent=user_agent,
695
+ expiry=expiry,
696
+ )
783
697
  user_str = str(user_uuid)
784
698
  with _db.transaction("create_credential_session", user=user_str):
785
699
  # Update display name if provided
@@ -790,20 +704,13 @@ def create_credential_session(
790
704
  _db.credentials[credential.uuid] = credential
791
705
 
792
706
  # 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
- )
707
+ _db.sessions[session.key] = session
801
708
 
802
709
  # Delete reset token if provided
803
710
  if reset_key:
804
711
  if reset_key in _db.reset_tokens:
805
712
  del _db.reset_tokens[reset_key]
806
- return session_key
713
+ return session.key
807
714
 
808
715
 
809
716
  # -------------------------------------------------------------------------
@@ -862,7 +769,7 @@ def bootstrap(
862
769
  reset_expiry = reset_expires()
863
770
  reset_key = _reset_key(reset_passphrase)
864
771
 
865
- now = datetime.now(timezone.utc)
772
+ now = datetime.now(UTC)
866
773
 
867
774
  with _db.transaction("bootstrap"):
868
775
  # Create auth:admin permission
@@ -884,13 +791,13 @@ def bootstrap(
884
791
  _db.permissions[perm_org_admin_uuid] = perm_org_admin
885
792
 
886
793
  # Create organization
887
- new_org = Org(display_name=org_name)
794
+ new_org = Org.create(display_name=org_name)
888
795
  new_org.uuid = org_uuid
889
796
  _db.orgs[org_uuid] = new_org
890
797
 
891
798
  # Create Administration role with both permissions
892
799
  admin_role = Role(
893
- org=org_uuid,
800
+ org_uuid=org_uuid,
894
801
  display_name="Administration",
895
802
  permissions={perm_admin_uuid: True, perm_org_admin_uuid: True},
896
803
  )
@@ -900,7 +807,7 @@ def bootstrap(
900
807
  # Create admin user
901
808
  admin_user = User(
902
809
  display_name=admin_name,
903
- role=role_uuid,
810
+ role_uuid=role_uuid,
904
811
  created_at=now,
905
812
  last_seen=None,
906
813
  visits=0,
@@ -910,7 +817,7 @@ def bootstrap(
910
817
 
911
818
  # Create reset token
912
819
  _db.reset_tokens[reset_key] = ResetToken(
913
- user=user_uuid,
820
+ user_uuid=user_uuid,
914
821
  expiry=reset_expiry,
915
822
  token_type="admin bootstrap",
916
823
  )