Compare commits

...
3 Commits
5 changed files with 33 additions and 30 deletions
-4
View File
@@ -71,10 +71,6 @@ function onUserChange(evt, targetRoleUuid) {
}
}
function permissionDisplayName(scope) {
return props.permissions.find(p => p.scope === scope)?.display_name || scope
}
function toggleRolePermission(role, pid, checked) {
emit('toggleRolePermission', role, pid, checked)
}
-7
View File
@@ -15,14 +15,11 @@ const props = defineProps({
const emit = defineEmits(['createOrg', 'openOrg', 'updateOrg', 'deleteOrg', 'toggleOrgPermission', 'openDialog', 'deletePermission', 'renamePermissionDisplay', 'createOidcClient', 'openOidcClient', 'deleteOidcClient', 'openServerConfig', 'navigateOut'])
// Template refs for navigation
const orgSection = ref(null)
const orgActionsRef = ref(null)
const orgTableRef = ref(null)
const permMatrixRef = ref(null)
const permActionsRef = ref(null)
const permTableRef = ref(null)
const oidcActionsRef = ref(null)
const oidcTableRef = ref(null)
const sortedOrgs = computed(() => [...props.orgs].sort((a,b)=> {
const nameCompare = a.org.display_name.localeCompare(b.org.display_name)
@@ -62,10 +59,6 @@ const sortedPermissions = computed(() => [...props.permissions].sort((a,b)=> a.s
const isMasterAdmin = computed(() => props.info?.ctx.permissions.includes('auth:admin'))
const isOrgAdmin = computed(() => props.info?.ctx.permissions.includes('auth:org:admin'))
function permissionDisplayName(scope) {
return props.permissions.find(p => p.scope === scope)?.display_name || scope
}
function getRoleNames(org) {
// org.roles is dict[UUID, Role]
return Object.values(org.roles)
+7 -1
View File
@@ -1,11 +1,13 @@
import argparse
import logging
import os
import sys
import msgspec
from fastapi_vue import server
from fastapi_vue.hostutil import parse_endpoints
from paskia._version import __version__
from paskia.db.jsonl import load_readonly
from paskia.util import startupbox
from paskia.util.hostutil import (
@@ -74,7 +76,11 @@ def main():
# Load stored config (read-only, no writes, no global state)
db_path = os.environ.get("PASKIA_DB", f"{args.rp_id}.paskiadb")
config = load_readonly(db_path, rp_id=args.rp_id).config
try:
config = load_readonly(db_path, rp_id=args.rp_id).config
except SystemExit as e:
print(f"🛑 Paskia {__version__} could not load")
sys.exit(str(e))
# Override stored config with CLI args, or clear with empty string
if args.rp_name is not None:
+19 -18
View File
@@ -41,11 +41,9 @@ class ReplayResult(msgspec.Struct, frozen=False):
changes: int = 0
class DatabaseError(Exception):
class DatabaseError(ValueError):
"""Exception raised for database loading errors."""
pass
def _replay_from_data(data: bytes, db_path: str) -> ReplayResult:
"""Replay database state from file data, using the last snapshot if available."""
@@ -61,14 +59,16 @@ def _replay_from_data(data: bytes, db_path: str) -> ReplayResult:
# Replay change records after the snapshot
lines = data[start_offset:].split(b"\n")
for line_num, raw in enumerate(lines, start=1): # 1-based line numbering
for raw in lines:
line = raw.strip()
if not line:
continue
try:
change = msgspec.json.decode(line, type=ChangeRecord)
except msgspec.DecodeError as e:
raise DatabaseError(f"{resolved_path}:{line_num}: {e}")
raise DatabaseError(
f"{resolved_path}: {e}\n{line.decode(errors='replace')}"
)
result.state = jsondiff.patch(result.state, change.diff, marshal=True)
result.v = change.v
result.ts = change.ts
@@ -88,30 +88,31 @@ def load_readonly(db_path: str, *, rp_id: str = "localhost") -> DB:
return DB(config=Config(rp_id=rp_id))
try:
with open(path, "rb") as f:
content = f.read()
content = path.read_bytes()
r = _replay_from_data(content, str(path.resolve()))
data_dict = r.state
version = r.v
if not data_dict:
return DB(config=Config(rp_id=rp_id))
# Apply migrations in-memory (no persistence)
apply_migrations_readonly(data_dict, version, MigrationCtx(rp_id=rp_id))
# Decode to msgspec struct
try:
return msgspec.json.decode(msgspec.json.encode(data_dict), type=DB)
except msgspec.ValidationError as e:
raise DatabaseError(f"{path.resolve()}: {e}") from None
except OSError as e:
_logger.exception("Failed to load database")
raise SystemExit(f"{e}")
except (ValueError, msgspec.DecodeError, DatabaseError) as e:
except (ValueError, msgspec.DecodeError) as e:
raise SystemExit(f"{e}")
except Exception as e:
_logger.exception("Unexpected error loading database")
raise SystemExit(f"{e}")
if not data_dict:
return DB(config=Config(rp_id=rp_id))
# Apply migrations in-memory (no persistence)
apply_migrations_readonly(data_dict, version, MigrationCtx(rp_id=rp_id))
# Decode to msgspec struct
db = msgspec.json.decode(msgspec.json.encode(data_dict), type=DB)
return db
class ChangeRecord(msgspec.Struct, omit_defaults=True, kw_only=True):
ts: datetime = msgspec.field(default_factory=lambda: datetime.now(UTC))
+7
View File
@@ -45,6 +45,13 @@ def migrate_v4(d: dict, ctx: MigrationCtx) -> None:
d["oidc"] = {"clients": {}, "key": base64.standard_b64encode(secret_key()).decode()}
def migrate_v5(d: dict, ctx: MigrationCtx) -> None:
"""Convert config.listen from str to list[str] if needed."""
listen = d["config"].get("listen")
if listen and isinstance(listen, str):
d["config"]["listen"] = [listen]
migrations = sorted(
[f for n, f in globals().items() if n.startswith("migrate_v")],
key=lambda f: int(f.__name__.removeprefix("migrate_v")),