Fastwhisper / app /store_config.py
Mbonea's picture
Deploy Habit Journal backend S0-S10 to Hugging Face Space.
990895d
Raw
History Blame Contribute Delete
2.92 kB
"""Persist bootstrap credentials and session-version state.
Configuration updates are serialized and atomically replace `config.json`.
"""
import json
from datetime import datetime, timezone
from app.fsutil import atomic_write_text, file_lock, read_json
from app.models import StoredConfig
from app.paths import Paths
from app.security_passwords import hash_password, verify_password
def utc_now() -> datetime:
"""Return an aware UTC timestamp."""
return datetime.now(timezone.utc)
class ConfigStore:
"""Read and mutate the durable single-user configuration."""
def __init__(self, paths: Paths) -> None:
self.paths = paths
def bootstrap(self, initial_password: str | None) -> StoredConfig | None:
"""Create initial configuration only when a password is available."""
with file_lock(self.paths.config):
existing = read_json(self.paths.config)
if existing is not None:
return StoredConfig.model_validate(existing)
if not initial_password:
return None
now = utc_now()
config = StoredConfig(
password_hash=hash_password(initial_password),
session_version=1,
created_at=now,
updated_at=now,
)
self._write_unlocked(config)
return config
def load(self) -> StoredConfig | None:
"""Load configuration, or None when setup has not occurred."""
value = read_json(self.paths.config)
return StoredConfig.model_validate(value) if value is not None else None
def password_matches(self, password: str) -> bool:
"""Check a candidate password against current configuration."""
config = self.load()
return bool(config and verify_password(password, config.password_hash))
def change_password(self, old_password: str, new_password: str) -> bool:
"""Rotate the password and invalidate all prior sessions."""
with file_lock(self.paths.config):
value = read_json(self.paths.config)
if value is None:
return False
config = StoredConfig.model_validate(value)
if not verify_password(old_password, config.password_hash):
return False
updated = config.model_copy(
update={
"password_hash": hash_password(new_password),
"session_version": config.session_version + 1,
"updated_at": utc_now(),
}
)
self._write_unlocked(updated)
return True
def _write_unlocked(self, config: StoredConfig) -> None:
payload = json.dumps(
config.model_dump(mode="json"),
ensure_ascii=False,
indent=2,
)
atomic_write_text(self.paths.config, payload + "\n")