Spaces:
Sleeping
Sleeping
| """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") | |