feat: interface PIN, bulk disposition tagging, Google Drive delta scan, OCR memory fixes
- Interface PIN: optional session-level auth gate for the main scanner UI
(Settings → Security → Interface PIN). Salted SHA-256 in config.json,
rate-limited (5 attempts/5 min per IP). /view and viewer auth exempt.
New /login page, before_request hook, GET/POST/DELETE /api/interface/pin,
POST /api/interface/pin/verify, POST /api/interface/logout.
- Bulk disposition tagging: Select mode (filter bar "Vælg" button) reveals
per-card checkboxes. Bulk tag bar at bottom of grid; POST /api/db/disposition/bulk.
Disposition stats bar (total · unreviewed · retain · delete · % reviewed)
updates after every save.
- Google Drive delta scan: uses Drive Changes API when delta is enabled.
Per-user token stored as gdrive:{email} in delta.json. Load-then-merge
save avoids racing with concurrent M365 token writes.
- PDF OCR OOM fix: render one page at a time with convert_from_path
(first_page=N, last_page=N). Added _ocr_mem_ok() psutil guard (500 MB
threshold) before each page render across scan_pdf, redact_fitz_pdf,
redact_pdf.
- Email test message translation fix: routes/email.py returns structured
{ok, method, recipients} instead of a hardcoded English string;
scheduler.js builds the translated message client-side.
- Docs: CHANGELOG, README, TODO, MANUAL-EN, MANUAL-DA all updated.
Lang files (en/da/de) extended with bulk, interface PIN, and SMTP keys.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
1 parent
b2bfa40f27
commit
d8083eb0c0
26 files changed
+1059
-128
No files matched your search
@@ -276,6 +276,44 @@ def _admin_pin_is_set() -> bool:
|
||||
return bool(_get_admin_pin_hash())
|
||||
|
||||
|
||||
# ── Interface PIN ─────────────────────────────────────────────────────────────
|
||||
# Salted SHA-256, stored in config.json under "interface_pin".
|
||||
# When set, the main web interface requires PIN authentication before the
|
||||
# index page or any /api/* route is accessible (viewer routes are exempt).
|
||||
|
||||
_INTERFACE_PIN_KEY = "interface_pin"
|
||||
|
||||
|
||||
def get_interface_pin_hash() -> "dict | None":
|
||||
"""Return the stored interface PIN hash dict, or None if not set."""
|
||||
return _load_config().get(_INTERFACE_PIN_KEY)
|
||||
|
||||
|
||||
def set_interface_pin(pin: str) -> None:
|
||||
import secrets as _sec
|
||||
if not pin:
|
||||
raise ValueError("PIN must not be empty")
|
||||
salt = _sec.token_hex(16)
|
||||
h = _hashlib.sha256((salt + pin).encode()).hexdigest()
|
||||
cfg = _load_config()
|
||||
cfg[_INTERFACE_PIN_KEY] = {"hash": h, "salt": salt}
|
||||
_save_config(cfg)
|
||||
|
||||
|
||||
def verify_interface_pin(pin: str) -> bool:
|
||||
"""Return True if *pin* matches the stored hash."""
|
||||
meta = get_interface_pin_hash()
|
||||
if not meta:
|
||||
return False
|
||||
return _hashlib.sha256((meta["salt"] + pin).encode()).hexdigest() == meta["hash"]
|
||||
|
||||
|
||||
def clear_interface_pin() -> None:
|
||||
cfg = _load_config()
|
||||
cfg.pop(_INTERFACE_PIN_KEY, None)
|
||||
_save_config(cfg)
|
||||
|
||||
|
||||
def _load_config() -> dict:
|
||||
if _CONFIG_FILE.exists():
|
||||
try:
|
||||
|
||||
Reference in new issue
Block a user