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
@@ -14,6 +14,10 @@ from app_config import (
|
||||
set_viewer_pin,
|
||||
verify_viewer_pin,
|
||||
clear_viewer_pin,
|
||||
get_interface_pin_hash,
|
||||
set_interface_pin,
|
||||
verify_interface_pin,
|
||||
clear_interface_pin,
|
||||
)
|
||||
|
||||
bp = Blueprint("viewer", __name__)
|
||||
@@ -161,6 +165,44 @@ def pin_clear():
|
||||
return jsonify({"ok": True})
|
||||
|
||||
|
||||
# ── Interface PIN management endpoints ───────────────────────────────────────
|
||||
|
||||
@bp.route("/api/interface/pin", methods=["GET"])
|
||||
def interface_pin_status():
|
||||
"""Return whether an interface PIN is currently set."""
|
||||
return jsonify({"pin_set": bool(get_interface_pin_hash())})
|
||||
|
||||
|
||||
@bp.route("/api/interface/pin", methods=["POST"])
|
||||
def interface_pin_set():
|
||||
"""Set or change the interface PIN.
|
||||
Body: {pin: "...", current_pin: "..."}
|
||||
current_pin required only when a PIN is already set.
|
||||
"""
|
||||
body = request.get_json(silent=True) or {}
|
||||
new_pin = str(body.get("pin", "")).strip()
|
||||
if not new_pin:
|
||||
return jsonify({"error": "pin required"}), 400
|
||||
if not new_pin.isdigit() or not (4 <= len(new_pin) <= 8):
|
||||
return jsonify({"error": "PIN must be 4–8 digits"}), 400
|
||||
if get_interface_pin_hash():
|
||||
if not verify_interface_pin(str(body.get("current_pin", "")).strip()):
|
||||
return jsonify({"error": "current PIN is incorrect"}), 403
|
||||
set_interface_pin(new_pin)
|
||||
return jsonify({"ok": True})
|
||||
|
||||
|
||||
@bp.route("/api/interface/pin", methods=["DELETE"])
|
||||
def interface_pin_clear():
|
||||
"""Remove the interface PIN. Requires current PIN if one is set."""
|
||||
body = request.get_json(silent=True) or {}
|
||||
if get_interface_pin_hash():
|
||||
if not verify_interface_pin(str(body.get("current_pin", "")).strip()):
|
||||
return jsonify({"error": "current PIN is incorrect"}), 403
|
||||
clear_interface_pin()
|
||||
return jsonify({"ok": True})
|
||||
|
||||
|
||||
@bp.route("/api/viewer/pin/verify", methods=["POST"])
|
||||
def pin_verify():
|
||||
"""Verify a PIN submission and set a viewer session on success."""
|
||||
|
||||
Reference in new issue
Block a user