phase G: data minimisation + passwordless auth + DeepSeek-first LLM
Server no longer holds portfolios. Holdings live in the browser (localStorage); the server publishes an anonymous ticker_universe and a gzipped /api/universe payload identical for every authenticated user, so access patterns can't betray which tickers a user holds. AI commentary is generated ephemerally from the browser-supplied pie and the cost ledger row records no positions. Migrations 0009-0011 added the universe table and dropped positions / portfolio_snapshots / portfolios. Authentication is now e-mail OTP only. Migration 0010 dropped password_hash and email_verified (every active session is by construction proof of email control). The /signup endpoint is gone; signup and login share a single email-entry page. Email rendering is HTML+plain-text multipart with a shared brand palette (app/branding.py) asserted in sync with the CSS by a drift-detection test. LLM provider defaults to DeepSeek-direct (cheaper, api.deepseek.com) with OpenRouter as automatic fallback if DeepSeek fails. ai_log_job and indicator_summary_job now iterate the two tones (NOVICE, INTERMEDIATE) per cycle so the dashboard's tone toggle is instant; PROMPT_VERSION bumped to 6 with an educational anti-TA / anti-gambling stance baked into _CORE. NOVICE mode renders a curated glossary inline (CBOE VIX, yield curve, HY OAS, etc.) with JS-positioned tooltips that survive viewport edges and sticky bars. Model name and tokens hidden from the user UI; still recorded in StrategicLog.model and AICall for admin. Layout adds a sticky top nav, a sticky bottom markets bar (one chip per exchange with status LED + headline index + 1d change), and Phase H feedback reporting is queued in tasks/todo.md. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
480fd311c5
commit
6e7f57c6b2
54 changed files with 5005 additions and 916 deletions
26
app/auth.py
26
app/auth.py
|
|
@ -36,6 +36,13 @@ from app.services.auth_service import get_user
|
|||
SESSION_COOKIE_NAME = "cassandra_session"
|
||||
SESSION_TTL_SECONDS = 14 * 24 * 60 * 60 # 14 days
|
||||
|
||||
# Short-lived cookie set during signup / unverified-login. Carries the email
|
||||
# under verification so the /verify page knows who's verifying without making
|
||||
# the user retype the address. NOT an auth cookie — never grants access to
|
||||
# anything beyond /verify and /verify/resend.
|
||||
PENDING_COOKIE_NAME = "cassandra_pending"
|
||||
PENDING_TTL_SECONDS = 60 * 60 # 1 hour
|
||||
|
||||
|
||||
@dataclass
|
||||
class CurrentUser:
|
||||
|
|
@ -74,6 +81,25 @@ def verify_session(cookie: str) -> int | None:
|
|||
return None
|
||||
|
||||
|
||||
def _pending_serializer() -> URLSafeTimedSerializer:
|
||||
s = get_settings()
|
||||
secret = s.CASSANDRA_SESSION_SECRET or s.CASSANDRA_TOKEN or "dev-insecure-secret"
|
||||
return URLSafeTimedSerializer(secret, salt="cassandra-pending-v1")
|
||||
|
||||
|
||||
def sign_pending(email: str, user_id: int) -> str:
|
||||
return _pending_serializer().dumps({"email": email, "uid": int(user_id)})
|
||||
|
||||
|
||||
def verify_pending(cookie: str) -> dict | None:
|
||||
"""Returns {"email": str, "uid": int} or None if signature/expiry bad."""
|
||||
try:
|
||||
data = _pending_serializer().loads(cookie, max_age=PENDING_TTL_SECONDS)
|
||||
return {"email": str(data["email"]), "uid": int(data["uid"])}
|
||||
except (BadSignature, SignatureExpired, KeyError, TypeError, ValueError):
|
||||
return None
|
||||
|
||||
|
||||
def _wants_html(request: Request) -> bool:
|
||||
accept = request.headers.get("accept", "").lower()
|
||||
# Treat a missing Accept header as HTML for browser navigations.
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue