paskia 0.8.1__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.
Files changed (58) hide show
  1. paskia/_version.py +2 -2
  2. paskia/aaguid/__init__.py +5 -4
  3. paskia/authsession.py +15 -43
  4. paskia/bootstrap.py +31 -103
  5. paskia/db/__init__.py +27 -55
  6. paskia/db/background.py +20 -40
  7. paskia/db/jsonl.py +196 -46
  8. paskia/db/logging.py +233 -0
  9. paskia/db/migrations.py +33 -0
  10. paskia/db/operations.py +409 -825
  11. paskia/db/structs.py +408 -94
  12. paskia/fastapi/__main__.py +25 -28
  13. paskia/fastapi/admin.py +147 -329
  14. paskia/fastapi/api.py +68 -110
  15. paskia/fastapi/logging.py +218 -0
  16. paskia/fastapi/mainapp.py +25 -8
  17. paskia/fastapi/remote.py +16 -39
  18. paskia/fastapi/reset.py +27 -19
  19. paskia/fastapi/response.py +22 -0
  20. paskia/fastapi/session.py +2 -2
  21. paskia/fastapi/user.py +24 -30
  22. paskia/fastapi/ws.py +25 -60
  23. paskia/fastapi/wschat.py +62 -0
  24. paskia/fastapi/wsutil.py +15 -2
  25. paskia/frontend-build/auth/admin/index.html +5 -5
  26. paskia/frontend-build/auth/assets/{AccessDenied-Bc249ASC.css → AccessDenied-DPkUS8LZ.css} +1 -1
  27. paskia/frontend-build/auth/assets/AccessDenied-Fmeb6EtF.js +8 -0
  28. paskia/frontend-build/auth/assets/{RestrictedAuth-DgdJyscT.css → RestrictedAuth-CvR33_Z0.css} +1 -1
  29. paskia/frontend-build/auth/assets/RestrictedAuth-DsJXicIw.js +1 -0
  30. paskia/frontend-build/auth/assets/{_plugin-vue_export-helper-rKFEraYH.js → _plugin-vue_export-helper-nhjnO_bd.js} +1 -1
  31. paskia/frontend-build/auth/assets/admin-CPE1pLMm.js +1 -0
  32. paskia/frontend-build/auth/assets/{admin-BeNu48FR.css → admin-DzzjSg72.css} +1 -1
  33. paskia/frontend-build/auth/assets/{auth-BKX7shEe.css → auth-C7k64Wad.css} +1 -1
  34. paskia/frontend-build/auth/assets/auth-YIZvPlW_.js +1 -0
  35. paskia/frontend-build/auth/assets/{forward-Dzg-aE1C.js → forward-DmqVHZ7e.js} +1 -1
  36. paskia/frontend-build/auth/assets/reset-Chtv69AT.css +1 -0
  37. paskia/frontend-build/auth/assets/reset-s20PATTN.js +1 -0
  38. paskia/frontend-build/auth/assets/{restricted-C0IQufuH.js → restricted-D3AJx3_6.js} +1 -1
  39. paskia/frontend-build/auth/index.html +5 -5
  40. paskia/frontend-build/auth/restricted/index.html +4 -4
  41. paskia/frontend-build/int/forward/index.html +4 -4
  42. paskia/frontend-build/int/reset/index.html +3 -3
  43. paskia/globals.py +2 -2
  44. paskia/migrate/__init__.py +67 -60
  45. paskia/migrate/sql.py +94 -37
  46. paskia/remoteauth.py +7 -8
  47. paskia/sansio.py +6 -12
  48. {paskia-0.8.1.dist-info → paskia-0.9.1.dist-info}/METADATA +1 -1
  49. paskia-0.9.1.dist-info/RECORD +60 -0
  50. paskia/frontend-build/auth/assets/AccessDenied-aTdCvz9k.js +0 -8
  51. paskia/frontend-build/auth/assets/RestrictedAuth-BLMK7-nL.js +0 -1
  52. paskia/frontend-build/auth/assets/admin-tVs8oyLv.js +0 -1
  53. paskia/frontend-build/auth/assets/auth-Dk3q4pNS.js +0 -1
  54. paskia/frontend-build/auth/assets/reset-BWF4cWKR.css +0 -1
  55. paskia/frontend-build/auth/assets/reset-C_Td1_jn.js +0 -1
  56. paskia-0.8.1.dist-info/RECORD +0 -55
  57. {paskia-0.8.1.dist-info → paskia-0.9.1.dist-info}/WHEEL +0 -0
  58. {paskia-0.8.1.dist-info → paskia-0.9.1.dist-info}/entry_points.txt +0 -0
@@ -11,13 +11,28 @@ Or via the CLI entry point (if installed):
11
11
  paskia-migrate --sql sqlite+aiosqlite:///paskia.sqlite --json paskia.jsonl
12
12
  """
13
13
 
14
+ import argparse
14
15
  import asyncio
15
- from datetime import datetime, timezone
16
+ import re
17
+ from datetime import UTC, datetime
16
18
  from uuid import UUID
17
19
 
18
20
  import base64url
21
+ import uuid7
22
+ from sqlalchemy import select
19
23
 
20
24
  from paskia.authsession import EXPIRES
25
+ from paskia.db.jsonl import JsonlStore
26
+ from paskia.db.structs import (
27
+ DB,
28
+ Credential,
29
+ Org,
30
+ Permission,
31
+ ResetToken,
32
+ Role,
33
+ Session,
34
+ User,
35
+ )
21
36
 
22
37
  from .sql import (
23
38
  DB as SQLDB,
@@ -47,30 +62,14 @@ async def migrate_from_sql(
47
62
  sql_db_path: SQLAlchemy connection string for the source SQL database
48
63
  json_db_path: Path for the destination JSONL file
49
64
  """
50
- # Import here to avoid circular imports and to not require JSON db at import time
51
- import re
52
-
53
- import uuid7
54
- from sqlalchemy import select
55
-
56
- from paskia.db.operations import DB as JSONDB
57
- from paskia.db.structs import (
58
- _CredentialData,
59
- _OrgData,
60
- _PermissionData,
61
- _ResetTokenData,
62
- _RoleData,
63
- _SessionData,
64
- _UserData,
65
- )
66
-
67
65
  # Initialize source SQL database
68
66
  sql_db = SQLDB(sql_db_path)
69
67
  await sql_db.init_db()
70
68
 
71
69
  # Initialize destination JSON database (fresh, don't load existing)
72
- json_db = JSONDB(json_db_path)
73
- # Don't call json_db.load() - we want a fresh database, not to load existing
70
+ db = DB()
71
+ store = JsonlStore(db, json_db_path)
72
+ db._store = store
74
73
 
75
74
  print(f"Migrating from {sql_db_path} to {json_db_path}...")
76
75
 
@@ -90,11 +89,13 @@ async def migrate_from_sql(
90
89
  # Migrate permissions with UUID keys and scope field
91
90
  # Always create exactly one common auth:org:admin permission for all org admin needs
92
91
  org_admin_perm_uuid: UUID = uuid7.create()
93
- json_db._data.permissions[org_admin_perm_uuid] = _PermissionData(
92
+ org_admin_perm = Permission(
94
93
  scope="auth:org:admin",
95
94
  display_name="Org Admin",
96
95
  orgs={},
97
96
  )
97
+ org_admin_perm.uuid = org_admin_perm_uuid
98
+ db.permissions[org_admin_perm_uuid] = org_admin_perm
98
99
 
99
100
  # Mapping from old permission ID to new permission UUID
100
101
  perm_id_to_uuid: dict[str, UUID] = {}
@@ -113,11 +114,13 @@ async def migrate_from_sql(
113
114
 
114
115
  # Regular permission - create with UUID key
115
116
  perm_uuid: UUID = uuid7.create()
116
- json_db._data.permissions[perm_uuid] = _PermissionData(
117
+ new_perm = Permission(
117
118
  scope=perm.id, # Old ID becomes the scope
118
119
  display_name=perm.display_name,
119
120
  orgs={},
120
121
  )
122
+ new_perm.uuid = perm_uuid
123
+ db.permissions[perm_uuid] = new_perm
121
124
  perm_id_to_uuid[perm.id] = perm_uuid
122
125
  print(
123
126
  f" Migrated {len(permissions)} permissions (with {len(org_admin_uuids)} org-specific admins consolidated to auth:org:admin)"
@@ -127,16 +130,16 @@ async def migrate_from_sql(
127
130
  orgs = await sql_db.list_organizations()
128
131
  for org in orgs:
129
132
  org_key: UUID = org.uuid
130
- json_db._data.orgs[org_key] = _OrgData(
131
- display_name=org.display_name,
132
- )
133
+ new_org = Org(display_name=org.display_name)
134
+ new_org.uuid = org_key
135
+ db.orgs[org_key] = new_org
133
136
  # Update permissions to allow this org to grant them (by UUID)
134
137
  for old_perm_id in org.permissions:
135
138
  perm_uuid = perm_id_to_uuid.get(old_perm_id)
136
- if perm_uuid and perm_uuid in json_db._data.permissions:
137
- json_db._data.permissions[perm_uuid].orgs[org_key] = True
139
+ if perm_uuid and perm_uuid in db.permissions:
140
+ db.permissions[perm_uuid].orgs[org_key] = True
138
141
  # Ensure every org can grant auth:org:admin
139
- json_db._data.permissions[org_admin_perm_uuid].orgs[org_key] = True
142
+ db.permissions[org_admin_perm_uuid].orgs[org_key] = True
140
143
  print(f" Migrated {len(orgs)} organizations")
141
144
 
142
145
  # Migrate roles - convert old permission IDs to UUIDs
@@ -150,11 +153,13 @@ async def migrate_from_sql(
150
153
  perm_uuid = perm_id_to_uuid.get(old_perm_id)
151
154
  if perm_uuid:
152
155
  new_permissions[perm_uuid] = True
153
- json_db._data.roles[role_key] = _RoleData(
154
- org=role.org_uuid,
156
+ new_role = Role(
157
+ org_uuid=role.org_uuid,
155
158
  display_name=role.display_name,
156
159
  permissions=new_permissions,
157
160
  )
161
+ new_role.uuid = role_key
162
+ db.roles[role_key] = new_role
158
163
  role_count += 1
159
164
  print(f" Migrated {role_count} roles")
160
165
 
@@ -163,15 +168,17 @@ async def migrate_from_sql(
163
168
  result = await session.execute(select(UserModel))
164
169
  user_models = result.scalars().all()
165
170
  for um in user_models:
166
- user = um.as_dataclass()
167
- user_key: UUID = user.uuid
168
- json_db._data.users[user_key] = _UserData(
169
- display_name=user.display_name,
170
- role=user.role_uuid,
171
- created_at=user.created_at or datetime.now(timezone.utc),
172
- last_seen=user.last_seen,
173
- visits=user.visits,
171
+ legacy_user = um.as_dataclass()
172
+ user_key: UUID = legacy_user.uuid
173
+ new_user = User(
174
+ display_name=legacy_user.display_name,
175
+ role_uuid=legacy_user.role_uuid,
176
+ created_at=legacy_user.created_at or datetime.now(UTC),
177
+ last_seen=legacy_user.last_seen,
178
+ visits=legacy_user.visits,
174
179
  )
180
+ new_user.uuid = user_key
181
+ db.users[user_key] = new_user
175
182
  print(f" Migrated {len(user_models)} users")
176
183
 
177
184
  # Migrate credentials
@@ -179,18 +186,20 @@ async def migrate_from_sql(
179
186
  result = await session.execute(select(CredentialModel))
180
187
  cred_models = result.scalars().all()
181
188
  for cm in cred_models:
182
- cred = cm.as_dataclass()
183
- cred_key: UUID = cred.uuid
184
- json_db._data.credentials[cred_key] = _CredentialData(
185
- credential_id=cred.credential_id,
186
- user=cred.user_uuid,
187
- aaguid=cred.aaguid,
188
- public_key=cred.public_key,
189
- sign_count=cred.sign_count,
190
- created_at=cred.created_at,
191
- last_used=cred.last_used,
192
- last_verified=cred.last_verified,
189
+ legacy_cred = cm.as_dataclass()
190
+ cred_key: UUID = legacy_cred.uuid
191
+ new_cred = Credential(
192
+ credential_id=legacy_cred.credential_id,
193
+ user_uuid=legacy_cred.user_uuid,
194
+ aaguid=legacy_cred.aaguid,
195
+ public_key=legacy_cred.public_key,
196
+ sign_count=legacy_cred.sign_count,
197
+ created_at=legacy_cred.created_at,
198
+ last_used=legacy_cred.last_used,
199
+ last_verified=legacy_cred.last_verified,
193
200
  )
201
+ new_cred.uuid = cred_key
202
+ db.credentials[cred_key] = new_cred
194
203
  print(f" Migrated {len(cred_models)} credentials")
195
204
 
196
205
  # Migrate sessions
@@ -207,9 +216,9 @@ async def migrate_from_sql(
207
216
  else:
208
217
  # Already in new format or unknown - try to use as-is
209
218
  session_key = base64url.enc(old_key[:12])
210
- json_db._data.sessions[session_key] = _SessionData(
211
- user=sess.user_uuid,
212
- credential=sess.credential_uuid,
219
+ db.sessions[session_key] = Session(
220
+ user_uuid=sess.user_uuid,
221
+ credential_uuid=sess.credential_uuid,
213
222
  host=sess.host,
214
223
  ip=sess.ip,
215
224
  user_agent=sess.user_agent,
@@ -231,26 +240,24 @@ async def migrate_from_sql(
231
240
  else:
232
241
  # Already in new format or unknown - truncate to 9 bytes
233
242
  token_key = old_key[:9]
234
- json_db._data.reset_tokens[token_key] = _ResetTokenData(
235
- user=token.user_uuid,
243
+ db.reset_tokens[token_key] = ResetToken(
244
+ user_uuid=token.user_uuid,
236
245
  expiry=token.expiry,
237
246
  token_type=token.token_type,
238
247
  )
239
248
  print(f" Migrated {len(token_models)} reset tokens")
240
249
 
241
- # Queue and flush all changes with actor "migrate"
242
- json_db._current_actor = "migrate"
243
- json_db._queue_change()
244
- from paskia.db.jsonl import flush_changes
250
+ # Queue and flush all changes using the transaction mechanism
251
+ with db.transaction("migrate:sql"):
252
+ pass # All data already added to _data, transaction commits on exit
245
253
 
246
- await flush_changes(json_db.db_path, json_db._pending_changes)
254
+ await store.flush()
247
255
 
248
256
  print("Migration complete!")
249
257
 
250
258
 
251
259
  def main():
252
260
  """CLI entry point for migration."""
253
- import argparse
254
261
 
255
262
  parser = argparse.ArgumentParser(
256
263
  description="Migrate Paskia database from SQL to JSON"
paskia/migrate/sql.py CHANGED
@@ -9,7 +9,7 @@ DO NOT use this module for new code. Use paskia.db instead.
9
9
 
10
10
  from contextlib import asynccontextmanager
11
11
  from dataclasses import dataclass
12
- from datetime import datetime, timezone
12
+ from datetime import UTC, datetime
13
13
  from uuid import UUID
14
14
 
15
15
  from sqlalchemy import (
@@ -25,31 +25,62 @@ from sqlalchemy.dialects.sqlite import BLOB
25
25
  from sqlalchemy.ext.asyncio import async_sessionmaker, create_async_engine
26
26
  from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column
27
27
 
28
- from paskia.db import (
29
- Credential,
30
- Org,
31
- ResetToken,
32
- Role,
33
- User,
34
- )
35
28
 
29
+ # Legacy User class for SQL schema (uses 'role_uuid' not 'role')
30
+ @dataclass
31
+ class _LegacyUser:
32
+ """User as stored in the old SQL schema with role_uuid field."""
36
33
 
37
- # Local Permission class for SQL schema (uses 'id' not 'uuid' + 'scope')
34
+ uuid: UUID
35
+ display_name: str
36
+ role_uuid: UUID
37
+ created_at: datetime | None = None
38
+ last_seen: datetime | None = None
39
+ visits: int = 0
40
+
41
+
42
+ # Legacy Credential class for SQL schema (uses 'user_uuid' not 'user')
38
43
  @dataclass
39
- class SqlPermission:
40
- """Permission as stored in the old SQL schema with id field."""
44
+ class _LegacyCredential:
45
+ """Credential as stored in the old SQL schema with user_uuid field."""
41
46
 
42
- id: str
47
+ uuid: UUID
48
+ credential_id: bytes
49
+ user_uuid: UUID
50
+ aaguid: UUID
51
+ public_key: bytes
52
+ sign_count: int
53
+ created_at: datetime
54
+ last_used: datetime | None = None
55
+ last_verified: datetime | None = None
56
+
57
+
58
+ # Legacy Role class for SQL schema (uses 'org_uuid' not 'org')
59
+ @dataclass
60
+ class _LegacyRole:
61
+ """Role as stored in the old SQL schema with org_uuid field."""
62
+
63
+ uuid: UUID
64
+ org_uuid: UUID
43
65
  display_name: str
66
+ permissions: list[str] | None = None
44
67
 
45
68
 
46
- DB_PATH_DEFAULT = "sqlite+aiosqlite:///paskia.sqlite"
69
+ # Legacy Org class for SQL schema (has mutable permissions/roles lists)
70
+ @dataclass
71
+ class _LegacyOrg:
72
+ """Org as stored in the old SQL schema with mutable permissions/roles."""
73
+
74
+ uuid: UUID
75
+ display_name: str
76
+ permissions: list[str] | None = None
77
+ roles: list[_LegacyRole] | None = None
47
78
 
48
79
 
49
- # Local Session class for SQL schema (uses 'renewed' not 'expiry')
80
+ # Legacy Session class for SQL schema (uses 'key' as field, 'user_uuid', 'credential_uuid')
50
81
  @dataclass
51
- class _SqlSession:
52
- """Session as stored in the old SQL schema with renewed timestamp."""
82
+ class _LegacySession:
83
+ """Session as stored in the old SQL schema."""
53
84
 
54
85
  key: bytes
55
86
  user_uuid: UUID
@@ -60,12 +91,35 @@ class _SqlSession:
60
91
  renewed: datetime
61
92
 
62
93
 
94
+ # Legacy ResetToken class for SQL schema (uses 'key' as field, 'user_uuid')
95
+ @dataclass
96
+ class _LegacyResetToken:
97
+ """ResetToken as stored in the old SQL schema."""
98
+
99
+ key: bytes
100
+ user_uuid: UUID
101
+ token_type: str
102
+ expiry: datetime
103
+
104
+
105
+ # Local Permission class for SQL schema (uses 'id' not 'uuid' + 'scope')
106
+ @dataclass
107
+ class SqlPermission:
108
+ """Permission as stored in the old SQL schema with id field."""
109
+
110
+ id: str
111
+ display_name: str
112
+
113
+
114
+ DB_PATH_DEFAULT = "sqlite+aiosqlite:///paskia.sqlite"
115
+
116
+
63
117
  def _normalize_dt(value: datetime | None) -> datetime | None:
64
118
  if value is None:
65
119
  return None
66
120
  if value.tzinfo is None:
67
- return value.replace(tzinfo=timezone.utc)
68
- return value.astimezone(timezone.utc)
121
+ return value.replace(tzinfo=UTC)
122
+ return value.astimezone(UTC)
69
123
 
70
124
 
71
125
  class Base(DeclarativeBase):
@@ -80,10 +134,13 @@ class OrgModel(Base):
80
134
 
81
135
  def as_dataclass(self):
82
136
  # Base Org without permissions/roles (filled by data accessors)
83
- return Org(UUID(bytes=self.uuid), self.display_name)
137
+ return _LegacyOrg(
138
+ uuid=UUID(bytes=self.uuid),
139
+ display_name=self.display_name,
140
+ )
84
141
 
85
142
  @staticmethod
86
- def from_dataclass(org: Org):
143
+ def from_dataclass(org: _LegacyOrg):
87
144
  return OrgModel(uuid=org.uuid.bytes, display_name=org.display_name)
88
145
 
89
146
 
@@ -98,14 +155,14 @@ class RoleModel(Base):
98
155
 
99
156
  def as_dataclass(self):
100
157
  # Base Role without permissions (filled by data accessors)
101
- return Role(
158
+ return _LegacyRole(
102
159
  uuid=UUID(bytes=self.uuid),
103
160
  org_uuid=UUID(bytes=self.org_uuid),
104
161
  display_name=self.display_name,
105
162
  )
106
163
 
107
164
  @staticmethod
108
- def from_dataclass(role: Role):
165
+ def from_dataclass(role: _LegacyRole):
109
166
  return RoleModel(
110
167
  uuid=role.uuid.bytes,
111
168
  org_uuid=role.org_uuid.bytes,
@@ -122,15 +179,15 @@ class UserModel(Base):
122
179
  LargeBinary(16), ForeignKey("roles.uuid", ondelete="CASCADE"), nullable=False
123
180
  )
124
181
  created_at: Mapped[datetime] = mapped_column(
125
- DateTime(timezone=True), default=lambda: datetime.now(timezone.utc)
182
+ DateTime(timezone=True), default=lambda: datetime.now(UTC)
126
183
  )
127
184
  last_seen: Mapped[datetime | None] = mapped_column(
128
185
  DateTime(timezone=True), nullable=True
129
186
  )
130
187
  visits: Mapped[int] = mapped_column(Integer, nullable=False, default=0)
131
188
 
132
- def as_dataclass(self) -> User:
133
- return User(
189
+ def as_dataclass(self) -> "_LegacyUser":
190
+ return _LegacyUser(
134
191
  uuid=UUID(bytes=self.uuid),
135
192
  display_name=self.display_name,
136
193
  role_uuid=UUID(bytes=self.role_uuid),
@@ -140,12 +197,12 @@ class UserModel(Base):
140
197
  )
141
198
 
142
199
  @staticmethod
143
- def from_dataclass(user: User):
200
+ def from_dataclass(user: "_LegacyUser"):
144
201
  return UserModel(
145
202
  uuid=user.uuid.bytes,
146
203
  display_name=user.display_name,
147
204
  role_uuid=user.role_uuid.bytes,
148
- created_at=user.created_at or datetime.now(timezone.utc),
205
+ created_at=user.created_at or datetime.now(UTC),
149
206
  last_seen=user.last_seen,
150
207
  visits=user.visits,
151
208
  )
@@ -165,7 +222,7 @@ class CredentialModel(Base):
165
222
  public_key: Mapped[bytes] = mapped_column(BLOB, nullable=False)
166
223
  sign_count: Mapped[int] = mapped_column(Integer, nullable=False)
167
224
  created_at: Mapped[datetime] = mapped_column(
168
- DateTime(timezone=True), default=lambda: datetime.now(timezone.utc)
225
+ DateTime(timezone=True), default=lambda: datetime.now(UTC)
169
226
  )
170
227
  last_used: Mapped[datetime | None] = mapped_column(
171
228
  DateTime(timezone=True), nullable=True
@@ -175,7 +232,7 @@ class CredentialModel(Base):
175
232
  )
176
233
 
177
234
  def as_dataclass(self):
178
- return Credential(
235
+ return _LegacyCredential(
179
236
  uuid=UUID(bytes=self.uuid),
180
237
  credential_id=self.credential_id,
181
238
  user_uuid=UUID(bytes=self.user_uuid),
@@ -205,12 +262,12 @@ class SessionModel(Base):
205
262
  user_agent: Mapped[str] = mapped_column(String(512), nullable=False)
206
263
  renewed: Mapped[datetime] = mapped_column(
207
264
  DateTime(timezone=True),
208
- default=lambda: datetime.now(timezone.utc),
265
+ default=lambda: datetime.now(UTC),
209
266
  nullable=False,
210
267
  )
211
268
 
212
269
  def as_dataclass(self):
213
- return _SqlSession(
270
+ return _LegacySession(
214
271
  key=self.key,
215
272
  user_uuid=UUID(bytes=self.user_uuid),
216
273
  credential_uuid=UUID(bytes=self.credential_uuid),
@@ -221,7 +278,7 @@ class SessionModel(Base):
221
278
  )
222
279
 
223
280
  @staticmethod
224
- def from_dataclass(session: _SqlSession):
281
+ def from_dataclass(session: _LegacySession):
225
282
  return SessionModel(
226
283
  key=session.key,
227
284
  user_uuid=session.user_uuid.bytes,
@@ -243,8 +300,8 @@ class ResetTokenModel(Base):
243
300
  token_type: Mapped[str] = mapped_column(String, nullable=False)
244
301
  expiry: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False)
245
302
 
246
- def as_dataclass(self) -> ResetToken:
247
- return ResetToken(
303
+ def as_dataclass(self) -> _LegacyResetToken:
304
+ return _LegacyResetToken(
248
305
  key=self.key,
249
306
  user_uuid=UUID(bytes=self.user_uuid),
250
307
  token_type=self.token_type,
@@ -338,7 +395,7 @@ class DB:
338
395
  result = await session.execute(select(PermissionModel))
339
396
  return [p.as_dataclass() for p in result.scalars().all()]
340
397
 
341
- async def list_organizations(self) -> list[Org]:
398
+ async def list_organizations(self) -> list[_LegacyOrg]:
342
399
  async with self.session() as session:
343
400
  # Load all orgs
344
401
  orgs_result = await session.execute(select(OrgModel))
@@ -365,13 +422,13 @@ class DB:
365
422
  perms_by_role.setdefault(rp.role_uuid, []).append(rp.permission_id)
366
423
 
367
424
  # Build org dataclasses with roles and permission IDs
368
- roles_by_org: dict[bytes, list[Role]] = {}
425
+ roles_by_org: dict[bytes, list[_LegacyRole]] = {}
369
426
  for rm in role_models:
370
427
  r_dc = rm.as_dataclass()
371
428
  r_dc.permissions = perms_by_role.get(rm.uuid, [])
372
429
  roles_by_org.setdefault(rm.org_uuid, []).append(r_dc)
373
430
 
374
- orgs: list[Org] = []
431
+ orgs: list[_LegacyOrg] = []
375
432
  for om in org_models:
376
433
  o_dc = om.as_dataclass()
377
434
  o_dc.permissions = perms_by_org.get(om.uuid, [])
paskia/remoteauth.py CHANGED
@@ -19,12 +19,12 @@ The first 3 words of the token serve as the pairing code for manual entry.
19
19
 
20
20
  import asyncio
21
21
  import logging
22
+ from collections.abc import Callable
22
23
  from dataclasses import dataclass
23
- from datetime import datetime, timedelta, timezone
24
- from typing import Callable
24
+ from datetime import UTC, datetime, timedelta
25
25
  from uuid import UUID
26
26
 
27
- from paskia.util import passphrase
27
+ from paskia.util import passphrase, pow
28
28
 
29
29
  # Remote auth requests expire after this duration
30
30
  REMOTE_AUTH_LIFETIME = timedelta(minutes=5)
@@ -94,7 +94,7 @@ class RemoteAuthManager:
94
94
 
95
95
  async def _cleanup_expired(self):
96
96
  """Remove expired requests and notify waiting clients."""
97
- now = datetime.now(timezone.utc)
97
+ now = datetime.now(UTC)
98
98
  expired_keys = []
99
99
  async with self._lock:
100
100
  for key, req in self._requests.items():
@@ -123,7 +123,7 @@ class RemoteAuthManager:
123
123
  Returns:
124
124
  (code, expiry) - The 3-word passphrase code and expiration time
125
125
  """
126
- now = datetime.now(timezone.utc)
126
+ now = datetime.now(UTC)
127
127
  expiry = now + REMOTE_AUTH_LIFETIME
128
128
 
129
129
  async with self._lock:
@@ -160,7 +160,7 @@ class RemoteAuthManager:
160
160
  req = self._requests.get(normalized)
161
161
  if req is None:
162
162
  return None
163
- now = datetime.now(timezone.utc)
163
+ now = datetime.now(UTC)
164
164
  if now > req.created_at + REMOTE_AUTH_LIFETIME:
165
165
  # Expired
166
166
  del self._requests[normalized]
@@ -319,7 +319,6 @@ class RemoteAuthManager:
319
319
  Returns:
320
320
  PoW work units (pow.NORMAL or pow.HARD)
321
321
  """
322
- from paskia.util import pow
323
322
 
324
323
  count = self.get_connection_count()
325
324
  return pow.HARD if count >= 10 else pow.NORMAL
@@ -332,7 +331,7 @@ class RemoteAuthManager:
332
331
  req = self._requests.get(token)
333
332
  if req is None:
334
333
  return None
335
- now = datetime.now(timezone.utc)
334
+ now = datetime.now(UTC)
336
335
  if now > req.created_at + REMOTE_AUTH_LIFETIME:
337
336
  del self._requests[token]
338
337
  return None
paskia/sansio.py CHANGED
@@ -8,11 +8,9 @@ This module provides a unified interface for WebAuthn operations including:
8
8
  """
9
9
 
10
10
  import json
11
- from datetime import datetime, timezone
12
11
  from urllib.parse import urlparse
13
12
  from uuid import UUID
14
13
 
15
- import uuid7
16
14
  from webauthn import (
17
15
  generate_authentication_options,
18
16
  generate_registration_options,
@@ -176,14 +174,12 @@ class Passkey:
176
174
  expected_origin=origin,
177
175
  expected_rp_id=self.rp_id,
178
176
  )
179
- return Credential(
180
- uuid=uuid7.create(),
177
+ return Credential.create(
181
178
  credential_id=credential.raw_id,
182
- user_uuid=user_uuid,
179
+ user=user_uuid,
183
180
  aaguid=UUID(registration.aaguid),
184
181
  public_key=registration.credential_public_key,
185
182
  sign_count=registration.sign_count,
186
- created_at=datetime.now(timezone.utc),
187
183
  )
188
184
 
189
185
  ### Authentication Methods ###
@@ -234,8 +230,11 @@ class Passkey:
234
230
  Args:
235
231
  credential: The authentication credential response from the client
236
232
  expected_challenge: The earlier generated challenge bytes
237
- stored_cred: The server stored credential record (modified by this function)
233
+ stored_cred: The server stored credential record (NOT modified)
238
234
  origin: The origin URL (required, must be pre-validated)
235
+
236
+ Returns:
237
+ VerifiedAuthentication with new_sign_count and user_verified status
239
238
  """
240
239
  # Verify the authentication response
241
240
  verification = verify_authentication_response(
@@ -246,11 +245,6 @@ class Passkey:
246
245
  credential_public_key=stored_cred.public_key,
247
246
  credential_current_sign_count=stored_cred.sign_count,
248
247
  )
249
- stored_cred.sign_count = verification.new_sign_count
250
- now = datetime.now(timezone.utc)
251
- stored_cred.last_used = now
252
- if verification.user_verified:
253
- stored_cred.last_verified = now
254
248
  return verification
255
249
 
256
250
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: paskia
3
- Version: 0.8.1
3
+ Version: 0.9.1
4
4
  Summary: Passkey Auth made easy: all sites and APIs can be guarded even without any changes on the protected site.
5
5
  Project-URL: Homepage, https://git.zi.fi/LeoVasanko/paskia
6
6
  Project-URL: Repository, https://github.com/LeoVasanko/paskia