From d31c09084ec85c55b03803a00911ca6d5924b0fb Mon Sep 17 00:00:00 2001 From: Leo Vasanko Date: Fri, 22 May 2026 01:22:35 +0000 Subject: [PATCH] Make rpid.paskiadb a folder containing the database and the files in one. Migrates existing old format rpid.paskiadb file to main.db. --- README.md | 2 +- paskia/__main__.py | 5 +++-- paskia/db/lifecycle.py | 8 ++++---- paskia/db/operations.py | 4 +--- paskia/fastapi/api.py | 2 +- paskia/fastapi/logging.py | 4 +++- paskia/fastapi/wschat.py | 10 ++++++++-- paskia/util/avatar.py | 9 +++------ tests/test_user.py | 37 +++++++++++++++++++++++++++++++------ 9 files changed, 55 insertions(+), 26 deletions(-) diff --git a/README.md b/README.md index abfbb82..59e014a 100644 --- a/README.md +++ b/README.md @@ -66,7 +66,7 @@ paskia [options] | --auth-host *url* | Dedicated authentication site, e.g. **auth.example.com** | Use **/auth/** path on each site | | --save | Save current options to database | (only --rp-id required on further invocations) | -To clear a stored setting, pass an empty value like `--auth-host=`. The database is stored in `{rp-id}.paskiadb` in current directory. This can be overridden by environment `PASKIA_DB` if needed. +To clear a stored setting, pass an empty value like `--auth-host=`. The database is stored in `{rp-id}.paskiadb` folder in current directory. This can be overridden by environment `PASKIA_DB` if needed. ## Tutorial: From Local Testing to Production diff --git a/paskia/__main__.py b/paskia/__main__.py index a5b5afa..cfcf827 100644 --- a/paskia/__main__.py +++ b/paskia/__main__.py @@ -9,6 +9,7 @@ from fastapi_vue.hostutil import parse_endpoints from paskia._version import __version__ from paskia.db.jsonl import load_readonly +from paskia.db.paths import db_file_path from paskia.util import startupbox from paskia.util.hostutil import ( normalize_auth_host_and_origins, @@ -75,9 +76,9 @@ def main(): args = parser.parse_args() # Load stored config (read-only, no writes, no global state) - db_path = os.environ.get("PASKIA_DB", f"{args.rp_id}.paskiadb") + db_path = db_file_path(rp_id=args.rp_id, create_root=True) try: - config = load_readonly(db_path, rp_id=args.rp_id).config + config = load_readonly(str(db_path), rp_id=args.rp_id).config except SystemExit as e: print(f"🛑 Paskia {__version__} could not load") sys.exit(str(e)) diff --git a/paskia/db/lifecycle.py b/paskia/db/lifecycle.py index 81cb489..e7607f8 100644 --- a/paskia/db/lifecycle.py +++ b/paskia/db/lifecycle.py @@ -3,13 +3,13 @@ Database lifecycle: initialization and maintenance. """ import logging -import os from datetime import UTC, datetime import paskia.db.operations as _ops from paskia import oidc_notify from paskia.authsession import EXPIRES from paskia.db.jsonl import JsonlStore +from paskia.db.paths import db_file_path _logger = logging.getLogger(__name__) @@ -19,9 +19,9 @@ async def init(rp_id: str, *args, **kwargs): if _ops._db._store: _logger.debug("Database already initialized, skipping reload") return - db_path = os.environ.get("PASKIA_DB", f"{rp_id}.paskiadb") - store = JsonlStore(_ops._db, db_path) - await store.load(db_path, rp_id=rp_id) + db_path = db_file_path(rp_id=rp_id, create_root=True) + store = JsonlStore(_ops._db, str(db_path)) + await store.load(str(db_path), rp_id=rp_id) _ops._db = store.db _ops._db._store = store # Request a snapshot after successful startup diff --git a/paskia/db/operations.py b/paskia/db/operations.py index f2ce9c9..08a50d9 100644 --- a/paskia/db/operations.py +++ b/paskia/db/operations.py @@ -460,9 +460,7 @@ def update_session( s.validated = validated -def set_session_host( - key: str, host: str, *, ctx: SessionContext | None = None -) -> None: +def set_session_host(key: str, host: str, *, ctx: SessionContext | None = None) -> None: """Set the host for a session (first-time binding).""" update_session(key, host=host, ctx=ctx) diff --git a/paskia/fastapi/api.py b/paskia/fastapi/api.py index dc28709..dc4f2bc 100644 --- a/paskia/fastapi/api.py +++ b/paskia/fastapi/api.py @@ -21,7 +21,6 @@ from paskia.fastapi import authz, session, user from paskia.fastapi.response import MsgspecResponse from paskia.fastapi.session import AUTH_COOKIE, AUTH_COOKIE_NAME, get_client_ip from paskia.globals import passkey as global_passkey -from paskia.util.crypto import hash_secret from paskia.util import hostutil, htmlutil, passphrase, permutil, userinfo from paskia.util.apistructs import ( ApiCheckUserResponse, @@ -33,6 +32,7 @@ from paskia.util.apistructs import ( ApiUserContext, ApiValidateResponse, ) +from paskia.util.crypto import hash_secret bearer_auth = HTTPBearer(auto_error=False) diff --git a/paskia/fastapi/logging.py b/paskia/fastapi/logging.py index c99872b..27a2d1d 100644 --- a/paskia/fastapi/logging.py +++ b/paskia/fastapi/logging.py @@ -134,7 +134,9 @@ def format_access_log( # Format: "IP STATUS METHOD host path [extra] TIMING" extra_str = f" {_TIMING}{extra}{_RESET}" if extra else "" - return f"{ip} {status_str} {method_str} {host_str}{path_str}{extra_str} {timing_str}" + return ( + f"{ip} {status_str} {method_str} {host_str}{path_str}{extra_str} {timing_str}" + ) # WebSocket connection counter (mod 100) diff --git a/paskia/fastapi/wschat.py b/paskia/fastapi/wschat.py index 8ff4007..50bbf2c 100644 --- a/paskia/fastapi/wschat.py +++ b/paskia/fastapi/wschat.py @@ -109,11 +109,17 @@ async def authenticate_and_login( cred, new_sign_count = await authenticate_chat(ws, credential_ids) # Use overrides if provided, otherwise use websocket metadata - login_host = hostutil.normalize_host(session_host) if session_host is not None else normalized_host + login_host = ( + hostutil.normalize_host(session_host) + if session_host is not None + else normalized_host + ) if not login_host: raise ValueError("Host required for session creation") login_ip = session_ip if session_ip is not None else metadata["ip"] - login_user_agent = session_user_agent if session_user_agent is not None else metadata["user_agent"] + login_user_agent = ( + session_user_agent if session_user_agent is not None else metadata["user_agent"] + ) # Create session and update user/credential secret = db.login( diff --git a/paskia/util/avatar.py b/paskia/util/avatar.py index 8dc5390..30bff7c 100644 --- a/paskia/util/avatar.py +++ b/paskia/util/avatar.py @@ -4,12 +4,12 @@ from __future__ import annotations import contextlib import hashlib -import os from pathlib import Path from uuid import UUID from fastapi import HTTPException, UploadFile +from paskia.db.paths import users_root_path from paskia.util import hostutil MAX_UPLOAD_BYTES = 10 * 1024 * 1024 @@ -17,15 +17,12 @@ MAX_UPLOAD_BYTES = 10 * 1024 * 1024 def media_root() -> Path: """Return the filesystem root for auxiliary media files.""" - db_path = Path(os.environ.get("PASKIA_DB", "localhost.paskiadb")).resolve() - db_name = db_path.name - hostname = db_name.removesuffix(".paskiadb") or db_path.stem - return db_path.parent / f"{hostname}.data" + return users_root_path(create_root=True) def avatars_root() -> Path: """Return the filesystem root for stored avatar images.""" - return media_root() / "user" + return media_root() def avatar_path(user_uuid: UUID) -> Path: diff --git a/tests/test_user.py b/tests/test_user.py index bdf3f98..c5c5bca 100644 --- a/tests/test_user.py +++ b/tests/test_user.py @@ -15,6 +15,7 @@ from urllib.parse import urlsplit import httpx import pytest +from paskia.db.paths import db_file_path, users_root_path from tests.conftest import auth_headers, create_test_image_bytes @@ -178,6 +179,13 @@ class TestUserAvatar: ) assert response.status_code == 200 + info = await client.get( + "/auth/api/user-info", + headers={**auth_headers(session_token), "Host": "localhost:4401"}, + ) + assert info.status_code == 200 + assert info.json()["user"].get("avatar_url") is None + @pytest.mark.asyncio async def test_regular_user_cannot_upload_another_users_avatar( self, @@ -194,12 +202,29 @@ class TestUserAvatar: ) assert response.status_code == 403 - info = await client.get( - "/auth/api/user-info", - headers={**auth_headers(session_token), "Host": "localhost:4401"}, - ) - assert info.status_code == 200 - assert info.json()["user"].get("avatar_url") is None + +def test_paskia_db_legacy_file_is_migrated_to_root_dir(tmp_path, monkeypatch): + legacy_path = tmp_path / "legacy.paskiadb" + legacy_bytes = b'{"v":0}\n' + legacy_path.write_bytes(legacy_bytes) + + monkeypatch.setenv("PASKIA_DB", str(legacy_path)) + + db_path = db_file_path(create_root=True) + + assert legacy_path.is_dir() + assert db_path == legacy_path / "main.db" + assert db_path.read_bytes() == legacy_bytes + + +def test_paskia_db_root_uses_users_directory(tmp_path, monkeypatch): + root_path = tmp_path / "instance-root" + monkeypatch.setenv("PASKIA_DB", str(root_path)) + + users_path = users_root_path(create_root=True) + + assert users_path == root_path / "users" + assert users_path.parent == root_path class TestUserLogoutAll: