Backend Aug 22-25: AI analysis, welcome-packet templating, LetterStream, DocuSeal, staff RBAC + tier gate

- analysis.py: deterministic claim scorer + /analyze /approve /letter /advance-tier endpoints (auto-runs on intake)
- packet.py + packet_fields.json: welcome-packet templating engine (6 onboarding docs, field catalog)
- letterstream.py + letters.py: certified-mail send pipeline + letter lifecycle (webhook verified)
- docuseal.py: DocuSeal signing integration
- staff.py/models.py/schema.sql/auth.py: approval actor from staff key, tier gate (APPROVED+ACTIVE+onboarding docs), onboarding_docs table
- frontend/: dependency-free static portal (intake, magic-link login/verify, dashboard)
- landing-mockups/: 4 design-stance mockups + favicons
- legal/: aup/privacy/sms-terms/terms HTML
- docs/: letter-queue scope, letterstream API contract, 6 welcome-packet templates
- review-dre-landing-2026-08-21.md: 3-variant landing feedback sprint
- compliance/DRE_Compliance_Manual.md: updated

Source synced from deployed /opt/dre-portal/app/ (was 4 days ahead of git).
This commit is contained in:
root
2026-08-26 02:26:33 -04:00
parent 7a5603b495
commit 7a62b0b340
46 changed files with 11004 additions and 35 deletions
+157 -5
View File
@@ -5,10 +5,14 @@ from __future__ import annotations
import hashlib
import hmac
import json
import logging
import os
import secrets
import time
import urllib.error
import urllib.parse
import urllib.request
from collections import defaultdict, deque
from datetime import datetime, timedelta, timezone
@@ -108,6 +112,106 @@ def verify_staff_key(provided: str | None) -> bool:
return _compare_hash(provided, key)
# ---------------------------------------------------------------
# Stack Auth (auth2 / Hexclave) — per-user staff SSO
# ---------------------------------------------------------------
STACK_AUTH_API_URL = os.environ.get("STACK_AUTH_API_URL", "https://auth2-api.itpropartner.com").rstrip("/")
STACK_AUTH_PUBLISHABLE_KEY = os.environ.get("STACK_AUTH_PUBLISHABLE_KEY", "")
STACK_AUTH_PROJECT_ID = os.environ.get("STACK_AUTH_PROJECT_ID", "internal")
STACK_AUTH_TEAM = os.environ.get("STACK_AUTH_TEAM", "dre-staff")
# Emergency owner bypass: these emails are always admitted even if the team
# membership API is down or membership was accidentally removed.
STACK_AUTH_OWNER_EMAILS = [
e.strip().lower()
for e in os.environ.get("STACK_AUTH_OWNER_EMAILS", "").split(",")
if e.strip()
]
def _stack_auth_request(method: str, path: str, access_token: str | None = None,
body: dict | None = None):
"""Call the auth2 Stack Auth REST API (client mode). Returns (status, parsed|raw).
Unauthenticated calls use the publishable client key; authenticated calls use
the opaque access token instead. Both carry access-type/project-id headers.
"""
url = STACK_AUTH_API_URL + path
headers = {
"Content-Type": "application/json",
"x-hexclave-access-type": "client",
"x-hexclave-project-id": STACK_AUTH_PROJECT_ID,
}
if access_token:
headers["x-hexclave-access-token"] = access_token
else:
headers["x-hexclave-publishable-client-key"] = STACK_AUTH_PUBLISHABLE_KEY
data = json.dumps(body).encode("utf-8") if body is not None else None
req = urllib.request.Request(url, data=data, method=method, headers=headers)
try:
resp = urllib.request.urlopen(req, timeout=8)
raw = resp.read().decode("utf-8", "replace")
code = resp.status
except urllib.error.HTTPError as e:
code = e.code
raw = e.read().decode("utf-8", "replace")
except Exception: # noqa: BLE001
return None, None
try:
return code, json.loads(raw)
except Exception: # noqa: BLE001
return code, raw
def stack_auth_user(access_token: str | None) -> tuple[str, dict | None]:
"""Validate an opaque Stack Auth access token and confirm dre-staff membership.
Returns one of:
("ok", {"name","email","user_id"}) — valid session AND team member
("denied", None) — valid session but NOT in the team
("invalid", None) — bad/expired token or API unreachable
"""
if not access_token or not STACK_AUTH_PUBLISHABLE_KEY:
return "invalid", None
code, user = _stack_auth_request("GET", "/api/latest/users/me", access_token=access_token)
if code != 200 or not isinstance(user, dict):
return "invalid", None
q = urllib.parse.quote(STACK_AUTH_TEAM)
code, teams = _stack_auth_request("GET", f"/api/latest/teams?user_id=me&query={q}", access_token=access_token)
items = teams.get("items") if isinstance(teams, dict) else None
member = (code == 200 and isinstance(items, list) and any(
str(t.get("display_name", "")).lower() == STACK_AUTH_TEAM.lower() for t in items))
email = user.get("primary_email") or ""
is_owner = email.lower() in STACK_AUTH_OWNER_EMAILS
if not member and not is_owner:
return "denied", None
return "ok", {
"name": user.get("display_name") or email or "DRE Staff",
"email": email,
"user_id": user.get("id") or "",
}
def stack_auth_sign_in(email: str, password: str) -> tuple[str, dict | None]:
"""Password sign-in against Stack Auth, gated to the dre-staff team.
Returns ("ok", {"token","name","email","user_id"}), ("invalid", None) for bad
credentials, or ("denied", None) for a valid account outside the team.
"""
code, d = _stack_auth_request(
"POST", "/api/latest/auth/password/sign-in",
body={"email": email, "password": password},
)
if code != 200 or not isinstance(d, dict):
return "invalid", None
token = d.get("access_token")
if not token:
return "invalid", None
state, user = stack_auth_user(token)
if state != "ok":
return "denied", None
return "ok", {"token": token, **user}
# ---------------------------------------------------------------
# FastAPI dependencies
# ---------------------------------------------------------------
@@ -119,14 +223,62 @@ def get_client_ip(request: Request) -> str:
return request.client.host if request.client else "unknown"
def require_staff(request: Request) -> None:
"""Staff-key auth dependency. Raises 403 if missing/wrong."""
provided = request.headers.get("x-dre-staff-key")
if not verify_staff_key(provided):
def staff_identity(provided: str | None) -> str:
"""Resolve the acting staff member's display name from the presented key.
Priority:
1. DRE_STAFF_DIRECTORY — 'KEY=Name <email>' entries (one per line or
semicolon-separated). Enables real per-user RBAC once each staff
member has their own key.
2. DRE_STAFF_NAME — a single default name for the shared-key case.
3. "DRE Staff" fallback.
"""
name = os.environ.get("DRE_STAFF_NAME", "").strip()
directory = os.environ.get("DRE_STAFF_DIRECTORY", "").strip()
if directory and provided:
for entry in directory.replace(";", "\n").splitlines():
entry = entry.strip()
if not entry or "=" not in entry:
continue
key, _, label = entry.partition("=")
if key.strip() == provided:
name = label.strip()
break
return name or "DRE Staff"
def require_staff(request: Request) -> str:
"""Staff auth dependency. Raises 403 if missing/wrong.
Priority:
1. Stack Auth access token (`x-dre-access-token`) → per-user auth2 SSO,
validated server-side and gated to the dre-staff team.
2. Legacy staff key (`x-dre-staff-key`) → script/fallback access.
Returns the acting staff member's display name so callers can record the
actor without manual name entry.
"""
token = request.headers.get("x-dre-access-token")
if token:
state, user = stack_auth_user(token)
if state == "ok":
return user["name"]
if state == "denied":
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail={"code": "staff_forbidden", "message": "Account is not authorized for staff access."},
)
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail={"code": "forbidden", "message": "Valid staff key required."},
detail={"code": "staff_auth_expired", "message": "Staff session expired. Please sign in again."},
)
provided = request.headers.get("x-dre-staff-key")
if verify_staff_key(provided):
return staff_identity(provided)
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail={"code": "forbidden", "message": "Valid staff credentials required."},
)
def require_client(request: Request) -> dict: