Make rpid.paskiadb a folder containing the database and the files in one. Migrates existing old format rpid.paskiadb file to main.db.
This commit is contained in:
@@ -66,7 +66,7 @@ paskia [options]
|
|||||||
| --auth-host *url* | Dedicated authentication site, e.g. **auth.example.com** | Use **/auth/** path on each site |
|
| --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) |
|
| --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
|
## Tutorial: From Local Testing to Production
|
||||||
|
|
||||||
|
|||||||
+3
-2
@@ -9,6 +9,7 @@ from fastapi_vue.hostutil import parse_endpoints
|
|||||||
|
|
||||||
from paskia._version import __version__
|
from paskia._version import __version__
|
||||||
from paskia.db.jsonl import load_readonly
|
from paskia.db.jsonl import load_readonly
|
||||||
|
from paskia.db.paths import db_file_path
|
||||||
from paskia.util import startupbox
|
from paskia.util import startupbox
|
||||||
from paskia.util.hostutil import (
|
from paskia.util.hostutil import (
|
||||||
normalize_auth_host_and_origins,
|
normalize_auth_host_and_origins,
|
||||||
@@ -75,9 +76,9 @@ def main():
|
|||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
|
|
||||||
# Load stored config (read-only, no writes, no global state)
|
# 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:
|
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:
|
except SystemExit as e:
|
||||||
print(f"🛑 Paskia {__version__} could not load")
|
print(f"🛑 Paskia {__version__} could not load")
|
||||||
sys.exit(str(e))
|
sys.exit(str(e))
|
||||||
|
|||||||
@@ -3,13 +3,13 @@ Database lifecycle: initialization and maintenance.
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
import os
|
|
||||||
from datetime import UTC, datetime
|
from datetime import UTC, datetime
|
||||||
|
|
||||||
import paskia.db.operations as _ops
|
import paskia.db.operations as _ops
|
||||||
from paskia import oidc_notify
|
from paskia import oidc_notify
|
||||||
from paskia.authsession import EXPIRES
|
from paskia.authsession import EXPIRES
|
||||||
from paskia.db.jsonl import JsonlStore
|
from paskia.db.jsonl import JsonlStore
|
||||||
|
from paskia.db.paths import db_file_path
|
||||||
|
|
||||||
_logger = logging.getLogger(__name__)
|
_logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -19,9 +19,9 @@ async def init(rp_id: str, *args, **kwargs):
|
|||||||
if _ops._db._store:
|
if _ops._db._store:
|
||||||
_logger.debug("Database already initialized, skipping reload")
|
_logger.debug("Database already initialized, skipping reload")
|
||||||
return
|
return
|
||||||
db_path = os.environ.get("PASKIA_DB", f"{rp_id}.paskiadb")
|
db_path = db_file_path(rp_id=rp_id, create_root=True)
|
||||||
store = JsonlStore(_ops._db, db_path)
|
store = JsonlStore(_ops._db, str(db_path))
|
||||||
await store.load(db_path, rp_id=rp_id)
|
await store.load(str(db_path), rp_id=rp_id)
|
||||||
_ops._db = store.db
|
_ops._db = store.db
|
||||||
_ops._db._store = store
|
_ops._db._store = store
|
||||||
# Request a snapshot after successful startup
|
# Request a snapshot after successful startup
|
||||||
|
|||||||
@@ -460,9 +460,7 @@ def update_session(
|
|||||||
s.validated = validated
|
s.validated = validated
|
||||||
|
|
||||||
|
|
||||||
def set_session_host(
|
def set_session_host(key: str, host: str, *, ctx: SessionContext | None = None) -> None:
|
||||||
key: str, host: str, *, ctx: SessionContext | None = None
|
|
||||||
) -> None:
|
|
||||||
"""Set the host for a session (first-time binding)."""
|
"""Set the host for a session (first-time binding)."""
|
||||||
update_session(key, host=host, ctx=ctx)
|
update_session(key, host=host, ctx=ctx)
|
||||||
|
|
||||||
|
|||||||
@@ -21,7 +21,6 @@ from paskia.fastapi import authz, session, user
|
|||||||
from paskia.fastapi.response import MsgspecResponse
|
from paskia.fastapi.response import MsgspecResponse
|
||||||
from paskia.fastapi.session import AUTH_COOKIE, AUTH_COOKIE_NAME, get_client_ip
|
from paskia.fastapi.session import AUTH_COOKIE, AUTH_COOKIE_NAME, get_client_ip
|
||||||
from paskia.globals import passkey as global_passkey
|
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 import hostutil, htmlutil, passphrase, permutil, userinfo
|
||||||
from paskia.util.apistructs import (
|
from paskia.util.apistructs import (
|
||||||
ApiCheckUserResponse,
|
ApiCheckUserResponse,
|
||||||
@@ -33,6 +32,7 @@ from paskia.util.apistructs import (
|
|||||||
ApiUserContext,
|
ApiUserContext,
|
||||||
ApiValidateResponse,
|
ApiValidateResponse,
|
||||||
)
|
)
|
||||||
|
from paskia.util.crypto import hash_secret
|
||||||
|
|
||||||
bearer_auth = HTTPBearer(auto_error=False)
|
bearer_auth = HTTPBearer(auto_error=False)
|
||||||
|
|
||||||
|
|||||||
@@ -134,7 +134,9 @@ def format_access_log(
|
|||||||
|
|
||||||
# Format: "IP STATUS METHOD host path [extra] TIMING"
|
# Format: "IP STATUS METHOD host path [extra] TIMING"
|
||||||
extra_str = f" {_TIMING}{extra}{_RESET}" if extra else ""
|
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)
|
# WebSocket connection counter (mod 100)
|
||||||
|
|||||||
@@ -109,11 +109,17 @@ async def authenticate_and_login(
|
|||||||
cred, new_sign_count = await authenticate_chat(ws, credential_ids)
|
cred, new_sign_count = await authenticate_chat(ws, credential_ids)
|
||||||
|
|
||||||
# Use overrides if provided, otherwise use websocket metadata
|
# 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:
|
if not login_host:
|
||||||
raise ValueError("Host required for session creation")
|
raise ValueError("Host required for session creation")
|
||||||
login_ip = session_ip if session_ip is not None else metadata["ip"]
|
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
|
# Create session and update user/credential
|
||||||
secret = db.login(
|
secret = db.login(
|
||||||
|
|||||||
@@ -4,12 +4,12 @@ from __future__ import annotations
|
|||||||
|
|
||||||
import contextlib
|
import contextlib
|
||||||
import hashlib
|
import hashlib
|
||||||
import os
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from uuid import UUID
|
from uuid import UUID
|
||||||
|
|
||||||
from fastapi import HTTPException, UploadFile
|
from fastapi import HTTPException, UploadFile
|
||||||
|
|
||||||
|
from paskia.db.paths import users_root_path
|
||||||
from paskia.util import hostutil
|
from paskia.util import hostutil
|
||||||
|
|
||||||
MAX_UPLOAD_BYTES = 10 * 1024 * 1024
|
MAX_UPLOAD_BYTES = 10 * 1024 * 1024
|
||||||
@@ -17,15 +17,12 @@ MAX_UPLOAD_BYTES = 10 * 1024 * 1024
|
|||||||
|
|
||||||
def media_root() -> Path:
|
def media_root() -> Path:
|
||||||
"""Return the filesystem root for auxiliary media files."""
|
"""Return the filesystem root for auxiliary media files."""
|
||||||
db_path = Path(os.environ.get("PASKIA_DB", "localhost.paskiadb")).resolve()
|
return users_root_path(create_root=True)
|
||||||
db_name = db_path.name
|
|
||||||
hostname = db_name.removesuffix(".paskiadb") or db_path.stem
|
|
||||||
return db_path.parent / f"{hostname}.data"
|
|
||||||
|
|
||||||
|
|
||||||
def avatars_root() -> Path:
|
def avatars_root() -> Path:
|
||||||
"""Return the filesystem root for stored avatar images."""
|
"""Return the filesystem root for stored avatar images."""
|
||||||
return media_root() / "user"
|
return media_root()
|
||||||
|
|
||||||
|
|
||||||
def avatar_path(user_uuid: UUID) -> Path:
|
def avatar_path(user_uuid: UUID) -> Path:
|
||||||
|
|||||||
+31
-6
@@ -15,6 +15,7 @@ from urllib.parse import urlsplit
|
|||||||
import httpx
|
import httpx
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
|
from paskia.db.paths import db_file_path, users_root_path
|
||||||
from tests.conftest import auth_headers, create_test_image_bytes
|
from tests.conftest import auth_headers, create_test_image_bytes
|
||||||
|
|
||||||
|
|
||||||
@@ -178,6 +179,13 @@ class TestUserAvatar:
|
|||||||
)
|
)
|
||||||
assert response.status_code == 200
|
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
|
@pytest.mark.asyncio
|
||||||
async def test_regular_user_cannot_upload_another_users_avatar(
|
async def test_regular_user_cannot_upload_another_users_avatar(
|
||||||
self,
|
self,
|
||||||
@@ -194,12 +202,29 @@ class TestUserAvatar:
|
|||||||
)
|
)
|
||||||
assert response.status_code == 403
|
assert response.status_code == 403
|
||||||
|
|
||||||
info = await client.get(
|
|
||||||
"/auth/api/user-info",
|
def test_paskia_db_legacy_file_is_migrated_to_root_dir(tmp_path, monkeypatch):
|
||||||
headers={**auth_headers(session_token), "Host": "localhost:4401"},
|
legacy_path = tmp_path / "legacy.paskiadb"
|
||||||
)
|
legacy_bytes = b'{"v":0}\n'
|
||||||
assert info.status_code == 200
|
legacy_path.write_bytes(legacy_bytes)
|
||||||
assert info.json()["user"].get("avatar_url") is None
|
|
||||||
|
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:
|
class TestUserLogoutAll:
|
||||||
|
|||||||
Reference in New Issue
Block a user