diff --git a/paskia/db/__init__.py b/paskia/db/__init__.py index 9f896b0..973860d 100644 --- a/paskia/db/__init__.py +++ b/paskia/db/__init__.py @@ -47,6 +47,7 @@ from paskia.db.operations import ( delete_session, delete_sessions_for_user, delete_user, + get_config, get_organization_users, get_reset_token, get_user_credential_ids, @@ -55,6 +56,7 @@ from paskia.db.operations import ( login, remove_permission_from_org, remove_permission_from_role, + set_config, set_session_host, update_credential_sign_count, update_org_name, @@ -110,6 +112,7 @@ __all__ = [ "build_session", "build_user", # Read ops + "get_config", "get_organization_users", "get_reset_token", "get_user_credential_ids", @@ -138,6 +141,7 @@ __all__ = [ "login", "remove_permission_from_org", "remove_permission_from_role", + "set_config", "set_session_host", "update_credential_sign_count", "update_org_name", diff --git a/paskia/db/jsonl.py b/paskia/db/jsonl.py index f4e0e4e..657a54b 100644 --- a/paskia/db/jsonl.py +++ b/paskia/db/jsonl.py @@ -130,10 +130,13 @@ class JsonlStore: self._transaction_snapshot: dict[str, Any] | None = None self._current_version: int = DBVER # Schema version for new databases - async def load(self, db_path: str | None = None) -> None: + async def load( + self, db_path: str | None = None, *, rp_id: str = "localhost" + ) -> None: """Load data from JSONL change log.""" if db_path is not None: self.db_path = Path(db_path) + self._rp_id = rp_id if not self.db_path.exists(): return @@ -169,7 +172,9 @@ class JsonlStore: self._queue_change(action, new_version, current) # Apply schema migrations one at a time - await apply_all_migrations(data_dict, self._current_version, persist_migration) + await apply_all_migrations( + data_dict, self._current_version, persist_migration, rp_id=rp_id + ) # Decode to msgspec struct decoder = msgspec.json.Decoder(DB) diff --git a/paskia/db/migrations.py b/paskia/db/migrations.py index 9ee6bf9..570e380 100644 --- a/paskia/db/migrations.py +++ b/paskia/db/migrations.py @@ -8,12 +8,18 @@ Each migration should be idempotent and only run when needed. from collections.abc import Awaitable, Callable -def migrate_v1(d: dict) -> None: +def migrate_v1(d: dict, **kwargs) -> None: """Remove Org.created_at fields.""" for org_data in d["orgs"].values(): org_data.pop("created_at", None) +def migrate_v2(d: dict, *, rp_id: str = "localhost") -> None: + """Add config field if missing.""" + if "config" not in d: + d["config"] = {"rp_id": rp_id} + + migrations = sorted( [f for n, f in globals().items() if n.startswith("migrate_v")], key=lambda f: int(f.__name__.removeprefix("migrate_v")), @@ -26,8 +32,10 @@ async def apply_all_migrations( data_dict: dict, current_version: int, persist: Callable[[str, int, dict], Awaitable[None]], + *, + rp_id: str = "localhost", ) -> None: while current_version < DBVER: - migrations[current_version](data_dict) + migrations[current_version](data_dict, rp_id=rp_id) current_version += 1 await persist(f"migrate:v{current_version}", current_version, data_dict) diff --git a/paskia/db/operations.py b/paskia/db/operations.py index 2046cdd..a9723e8 100644 --- a/paskia/db/operations.py +++ b/paskia/db/operations.py @@ -21,6 +21,7 @@ from paskia.db.jsonl import ( ) from paskia.db.structs import ( DB, + Config, Credential, Org, Permission, @@ -49,7 +50,7 @@ async def init(rp_id: str = "localhost", *args, **kwargs): return default_path = f"{rp_id}.paskiadb" db_path = os.environ.get("PASKIA_DB", default_path) - await _store.load(db_path) + await _store.load(db_path, rp_id=rp_id) _db = _store.db _initialized = True diff --git a/paskia/db/structs.py b/paskia/db/structs.py index 6c90dfd..8bcfb97 100644 --- a/paskia/db/structs.py +++ b/paskia/db/structs.py @@ -7,6 +7,7 @@ from uuid import UUID import msgspec import uuid7 +from msgspec import field from paskia import db from paskia.util.hostutil import normalize_host @@ -397,10 +398,10 @@ class SessionContext(msgspec.Struct): permissions: list[Permission] = [] -class Config(msgspec.Struct, dict=True, omit_defaults=True): +class Config(msgspec.Struct, frozen=True, dict=True, omit_defaults=True): """Stored configuration for the instance.""" - rp_id: str | None = None + rp_id: str rp_name: str | None = None origins: list[str] | None = None auth_host: str | None = None @@ -422,7 +423,7 @@ class DB(msgspec.Struct, dict=True, omit_defaults=False): credentials: dict[UUID, Credential] = {} sessions: dict[str, Session] = {} reset_tokens: dict[bytes, ResetToken] = {} - config: Config = Config() + config: Config = field(default_factory=lambda: Config(rp_id="localhost")) def __post_init__(self): # Store reference for persistence (not serialized) diff --git a/paskia/fastapi/__main__.py b/paskia/fastapi/__main__.py index d301527..f736259 100644 --- a/paskia/fastapi/__main__.py +++ b/paskia/fastapi/__main__.py @@ -6,7 +6,8 @@ import os from urllib.parse import urlparse from fastapi_vue.hostutil import parse_endpoint -from uvicorn import Config, Server +from uvicorn import Config as UvicornConfig +from uvicorn import Server from uvicorn import run as uvicorn_run from paskia import globals as _globals @@ -236,7 +237,7 @@ def main(): for ep in endpoints: tg.create_task( Server( - Config(app="paskia.fastapi:app", **run_kwargs, **ep) + UvicornConfig(app="paskia.fastapi:app", **run_kwargs, **ep) ).serve() ) elif DEVMODE: @@ -245,7 +246,7 @@ def main(): uvicorn_run("paskia.fastapi:app", **run_kwargs, **ep) else: server = Server( - Config(app="paskia.fastapi:app", **run_kwargs, **endpoints[0]) + UvicornConfig(app="paskia.fastapi:app", **run_kwargs, **endpoints[0]) ) await server.serve()