Scaffold project, data model and artwork
Establish the LLeMbas foundation: FastAPI/Jinja/SQLite layout, the ORM schema, and the original SVG identity. Notable decisions, all recorded in comments at the point they matter: - No Alembic. SQLite only, schema created at startup, so models carry a few columns nothing reads yet (Message.parent_id for branching, content_parts_json for multimodal turns). Adding them later to a live database without migrations is the painful path. - Sessions are server-side rows keyed by a SHA-256 of the cookie value, not JWTs, so logout and bans revoke access immediately. - Upstream API keys are Fernet-encrypted with a key derived from LEMBAS_SECRET_KEY. decrypt() fails soft to "" so rotating the secret degrades to re-entering keys rather than crashing the admin UI. - Artwork is generated by scripts/build_artwork.py rather than hand-drawn per file: the mallorn leaf appears in the icon, favicon, lockup and banner, and one source is the only way those stay in sync. The wordmark is Source Serif 4 (OFL) converted to outlines, because a README banner cannot load a webfont and <text> would render in whatever serif the viewer happens to have. - Icons live in a template partial, not assets/, because same-document <use href="#id"> is universally supported and the cross-document form is not. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,40 @@
|
||||
"""Password hashing.
|
||||
|
||||
Argon2id via argon2-cffi, using the library's current recommended parameters.
|
||||
``needs_rehash`` lets stored hashes be upgraded transparently when those
|
||||
defaults tighten in a future release.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from argon2 import PasswordHasher
|
||||
from argon2.exceptions import InvalidHashError, VerifyMismatchError, VerificationError
|
||||
|
||||
_hasher = PasswordHasher()
|
||||
|
||||
MIN_PASSWORD_LENGTH = 8
|
||||
|
||||
|
||||
def hash_password(password: str) -> str:
|
||||
return _hasher.hash(password)
|
||||
|
||||
|
||||
def verify_password(password: str, password_hash: str) -> bool:
|
||||
try:
|
||||
return _hasher.verify(password_hash, password)
|
||||
except (VerifyMismatchError, VerificationError, InvalidHashError):
|
||||
return False
|
||||
|
||||
|
||||
def needs_rehash(password_hash: str) -> bool:
|
||||
try:
|
||||
return _hasher.check_needs_rehash(password_hash)
|
||||
except InvalidHashError:
|
||||
return True
|
||||
|
||||
|
||||
def validate_password(password: str) -> str | None:
|
||||
"""Return a human-readable problem with the password, or None if it is fine."""
|
||||
if len(password) < MIN_PASSWORD_LENGTH:
|
||||
return f"Password must be at least {MIN_PASSWORD_LENGTH} characters."
|
||||
return None
|
||||
Reference in New Issue
Block a user