Database cleanup, better UUID passing and construction (User model).
This commit is contained in:
+9
-20
@@ -44,7 +44,6 @@ from paskia.db.structs import (
|
||||
_ResetTokenData,
|
||||
_RoleData,
|
||||
_SessionData,
|
||||
_UserData,
|
||||
)
|
||||
from paskia.util.passphrase import is_well_formed as _is_passphrase
|
||||
|
||||
@@ -178,15 +177,9 @@ def build_permission(uuid: UUID) -> Permission:
|
||||
|
||||
|
||||
def build_user(uuid: UUID) -> User:
|
||||
u = _db._data.users[uuid]
|
||||
return User(
|
||||
uuid=uuid,
|
||||
display_name=u.display_name,
|
||||
role_uuid=u.role,
|
||||
created_at=u.created_at,
|
||||
last_seen=u.last_seen,
|
||||
visits=u.visits,
|
||||
)
|
||||
user = _db._data.users[uuid]
|
||||
user.uuid = uuid
|
||||
return user
|
||||
|
||||
|
||||
def build_role(uuid: UUID) -> Role:
|
||||
@@ -809,16 +802,10 @@ def create_user(new_user: User, *, ctx: SessionContext | None = None) -> None:
|
||||
"""Create a new user."""
|
||||
if new_user.uuid in _db._data.users:
|
||||
raise ValueError(f"User {new_user.uuid} already exists")
|
||||
if new_user.role_uuid not in _db._data.roles:
|
||||
raise ValueError(f"Role {new_user.role_uuid} not found")
|
||||
if new_user.role not in _db._data.roles:
|
||||
raise ValueError(f"Role {new_user.role} not found")
|
||||
with _db.transaction("Created user", ctx):
|
||||
_db._data.users[new_user.uuid] = _UserData(
|
||||
display_name=new_user.display_name,
|
||||
role=new_user.role_uuid,
|
||||
created_at=new_user.created_at or datetime.now(timezone.utc),
|
||||
last_seen=new_user.last_seen,
|
||||
visits=new_user.visits,
|
||||
)
|
||||
_db._data.users[new_user.uuid] = new_user
|
||||
|
||||
|
||||
def update_user_display_name(
|
||||
@@ -1331,13 +1318,15 @@ def bootstrap(
|
||||
)
|
||||
|
||||
# Create admin user
|
||||
_db._data.users[user_uuid] = _UserData(
|
||||
admin_user = User(
|
||||
display_name=admin_name,
|
||||
role=role_uuid,
|
||||
created_at=now,
|
||||
last_seen=None,
|
||||
visits=0,
|
||||
)
|
||||
admin_user.uuid = user_uuid
|
||||
_db._data.users[user_uuid] = admin_user
|
||||
|
||||
# Create reset token
|
||||
_db._data.reset_tokens[reset_key] = _ResetTokenData(
|
||||
|
||||
+26
-13
@@ -2,6 +2,7 @@ from datetime import datetime
|
||||
from uuid import UUID
|
||||
|
||||
import msgspec
|
||||
import uuid7
|
||||
|
||||
|
||||
class Permission(msgspec.Struct, omit_defaults=True):
|
||||
@@ -25,14 +26,34 @@ class Org(msgspec.Struct):
|
||||
roles: list[Role] = [] # roles belonging to this org
|
||||
|
||||
|
||||
class User(msgspec.Struct):
|
||||
uuid: UUID
|
||||
class User(msgspec.Struct, dict=True):
|
||||
display_name: str
|
||||
role_uuid: UUID
|
||||
created_at: datetime | None = None
|
||||
role: UUID
|
||||
created_at: datetime
|
||||
last_seen: datetime | None = None
|
||||
visits: int = 0
|
||||
|
||||
def __post_init__(self):
|
||||
self.uuid: UUID | None = None # Convenience field, not serialized
|
||||
|
||||
@classmethod
|
||||
def create(
|
||||
cls,
|
||||
display_name: str,
|
||||
role: UUID,
|
||||
created_at: datetime | None = None,
|
||||
) -> "User":
|
||||
"""Create a new User with auto-generated uuid7."""
|
||||
from datetime import timezone
|
||||
|
||||
user = cls(
|
||||
display_name=display_name,
|
||||
role=role,
|
||||
created_at=created_at or datetime.now(timezone.utc),
|
||||
)
|
||||
user.uuid = uuid7.create(user.created_at)
|
||||
return user
|
||||
|
||||
|
||||
class Credential(msgspec.Struct):
|
||||
uuid: UUID
|
||||
@@ -103,14 +124,6 @@ class _RoleData(msgspec.Struct):
|
||||
permissions: dict[UUID, bool] = {} # permission_uuid -> True
|
||||
|
||||
|
||||
class _UserData(msgspec.Struct):
|
||||
display_name: str
|
||||
role: UUID
|
||||
created_at: datetime
|
||||
last_seen: datetime | None
|
||||
visits: int
|
||||
|
||||
|
||||
class _CredentialData(msgspec.Struct):
|
||||
credential_id: bytes
|
||||
user: UUID
|
||||
@@ -141,7 +154,7 @@ class _DatabaseData(msgspec.Struct, omit_defaults=True):
|
||||
permissions: dict[UUID, _PermissionData]
|
||||
orgs: dict[UUID, _OrgData]
|
||||
roles: dict[UUID, _RoleData]
|
||||
users: dict[UUID, _UserData]
|
||||
users: dict[UUID, User]
|
||||
credentials: dict[UUID, _CredentialData]
|
||||
sessions: dict[str, _SessionData]
|
||||
reset_tokens: dict[bytes, _ResetTokenData]
|
||||
|
||||
@@ -463,16 +463,12 @@ async def admin_create_user(
|
||||
role_obj = next((r for r in roles if r.display_name == role_name), None)
|
||||
if not role_obj:
|
||||
raise ValueError("Role not found in organization")
|
||||
user_uuid = uuid4()
|
||||
user = UserDC(
|
||||
uuid=user_uuid,
|
||||
user = UserDC.create(
|
||||
display_name=display_name,
|
||||
role_uuid=role_obj.uuid,
|
||||
visits=0,
|
||||
created_at=None,
|
||||
role=role_obj.uuid,
|
||||
)
|
||||
db.create_user(user, ctx=ctx)
|
||||
return {"uuid": str(user_uuid)}
|
||||
return {"uuid": str(user.uuid)}
|
||||
|
||||
|
||||
@app.patch("/orgs/{org_uuid}/users/{user_uuid}/role")
|
||||
|
||||
+14
-12
@@ -55,13 +55,13 @@ async def migrate_from_sql(
|
||||
|
||||
from paskia.db.operations import DB as JSONDB
|
||||
from paskia.db.structs import (
|
||||
User,
|
||||
_CredentialData,
|
||||
_OrgData,
|
||||
_PermissionData,
|
||||
_ResetTokenData,
|
||||
_RoleData,
|
||||
_SessionData,
|
||||
_UserData,
|
||||
)
|
||||
|
||||
# Initialize source SQL database
|
||||
@@ -163,15 +163,17 @@ async def migrate_from_sql(
|
||||
result = await session.execute(select(UserModel))
|
||||
user_models = result.scalars().all()
|
||||
for um in user_models:
|
||||
user = um.as_dataclass()
|
||||
user_key: UUID = user.uuid
|
||||
json_db._data.users[user_key] = _UserData(
|
||||
display_name=user.display_name,
|
||||
role=user.role_uuid,
|
||||
created_at=user.created_at or datetime.now(timezone.utc),
|
||||
last_seen=user.last_seen,
|
||||
visits=user.visits,
|
||||
legacy_user = um.as_dataclass()
|
||||
user_key: UUID = legacy_user.uuid
|
||||
new_user = User(
|
||||
display_name=legacy_user.display_name,
|
||||
role=legacy_user.role_uuid,
|
||||
created_at=legacy_user.created_at or datetime.now(timezone.utc),
|
||||
last_seen=legacy_user.last_seen,
|
||||
visits=legacy_user.visits,
|
||||
)
|
||||
new_user.uuid = user_key
|
||||
json_db._data.users[user_key] = new_user
|
||||
print(f" Migrated {len(user_models)} users")
|
||||
|
||||
# Migrate credentials
|
||||
@@ -238,9 +240,9 @@ async def migrate_from_sql(
|
||||
)
|
||||
print(f" Migrated {len(token_models)} reset tokens")
|
||||
|
||||
# Queue and flush all changes with actor "migrate"
|
||||
json_db._current_actor = "migrate"
|
||||
json_db._queue_change()
|
||||
# Queue and flush all changes using the transaction mechanism
|
||||
with json_db.transaction("migrate"):
|
||||
pass # All data already added to _data, transaction commits on exit
|
||||
from paskia.db.jsonl import flush_changes
|
||||
|
||||
await flush_changes(json_db.db_path, json_db._pending_changes)
|
||||
|
||||
+16
-4
@@ -30,10 +30,22 @@ from paskia.db import (
|
||||
Org,
|
||||
ResetToken,
|
||||
Role,
|
||||
User,
|
||||
)
|
||||
|
||||
|
||||
# Legacy User class for SQL schema (uses 'role_uuid' not 'role')
|
||||
@dataclass
|
||||
class _LegacyUser:
|
||||
"""User as stored in the old SQL schema with role_uuid field."""
|
||||
|
||||
uuid: UUID
|
||||
display_name: str
|
||||
role_uuid: UUID
|
||||
created_at: datetime | None = None
|
||||
last_seen: datetime | None = None
|
||||
visits: int = 0
|
||||
|
||||
|
||||
# Local Permission class for SQL schema (uses 'id' not 'uuid' + 'scope')
|
||||
@dataclass
|
||||
class SqlPermission:
|
||||
@@ -129,8 +141,8 @@ class UserModel(Base):
|
||||
)
|
||||
visits: Mapped[int] = mapped_column(Integer, nullable=False, default=0)
|
||||
|
||||
def as_dataclass(self) -> User:
|
||||
return User(
|
||||
def as_dataclass(self) -> "_LegacyUser":
|
||||
return _LegacyUser(
|
||||
uuid=UUID(bytes=self.uuid),
|
||||
display_name=self.display_name,
|
||||
role_uuid=UUID(bytes=self.role_uuid),
|
||||
@@ -140,7 +152,7 @@ class UserModel(Base):
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def from_dataclass(user: User):
|
||||
def from_dataclass(user: "_LegacyUser"):
|
||||
return UserModel(
|
||||
uuid=user.uuid.bytes,
|
||||
display_name=user.display_name,
|
||||
|
||||
Reference in New Issue
Block a user