T11.2: capture usage events (CR-019)

POST /api/usage/ping writes one page_open UsageEvent per authenticated
page load, identity from the session (get_current_user), never from the
client. Wired from exactly one place - auth-guard.js's proceed(), after
wp-auth-ready - so this can't drift into six separate per-page copies.

okta_callback now also writes one login event per sign-in.

Verified locally: unauthenticated ping -> 401; a real fake-Okta sign-in
writes exactly one login row and one page_open row, no duplicates.
This commit is contained in:
2026-09-23 11:24:49 -07:00
parent 0652fa732d
commit 850b78972b
2 changed files with 56 additions and 0 deletions

View File

@@ -756,6 +756,11 @@ async def okta_callback(request: Request, db: Session = Depends(get_db)):
user.failed_attempts = 0
user.locked_until = None
user.last_login_at = models.utcnow()
# CR-019: one login event per Okta sign-in, written here rather than
# inferred from session creation elsewhere, so there is exactly one
# source of truth for "did this person sign in" - not one per page load
# afterward (T11.2 covers that separately, as page_open events).
db.add(models.UsageEvent(id=gen_id("uev"), username=user.username, tool="", event="login"))
db.commit()
db.refresh(user)
@@ -3374,6 +3379,29 @@ def create_feedback(body: CommentIn, user: models.User = Depends(auth.get_curren
return _save_comment(body, db, user)
# ── CR-019: usage/activity metrics ──────────────────────────────────────────
class UsageIn(BaseModel):
tool: str = ""
project_id: Optional[str] = None
@app.post("/api/usage/ping")
def usage_ping(body: UsageIn, user: models.User = Depends(auth.get_current_user), db: Session = Depends(get_db)):
"""One usage_events row per authenticated page load. Called exactly once,
from auth-guard.js after wp-auth-ready fires (T11.2) - never duplicated
per page's own script, the same lesson S4's per-page nav already taught
this codebase. `user` comes from the session via get_current_user, never
from anything the client claims - identity here is a server-enforced
fact, matching every other write in this file, not a client-reported one."""
tool = (body.tool or "").strip()[:40]
db.add(models.UsageEvent(
id=gen_id("uev"), username=user.username, project_id=body.project_id,
tool=tool, event="page_open",
))
db.commit()
return {"ok": True}
@app.get("/api/comments")
def list_comments(
source: Optional[str] = Query(None),