Skip to content

feat(setup): first-run operator intake + admin email login (trinity-enterprise#38, #82) - #1307

Merged
dolho merged 3 commits into
devfrom
feature/38-first-run-intake-admin-email
Jun 23, 2026
Merged

feat(setup): first-run operator intake + admin email login (trinity-enterprise#38, #82)#1307
dolho merged 3 commits into
devfrom
feature/38-first-run-intake-admin-email

Conversation

@vybe

@vybe vybe commented Jun 22, 2026

Copy link
Copy Markdown
Contributor

Summary

Two converging first-run features built on one email capture:

The key design decision: no email is sent at setup

A fresh install has no Resend key, so we can't deliver a verification code during setup. Instead the email is bound to the admin account (not code-verified), and the intake is a plain HTTPS POST (not email) — both work offline-of-email. The code-based email second factor is Phase 2, deferred to the existing mfa_gate seam (#5/#388) — so this PR does not touch that call site.

Changes

  • services/operator_intake_service.py — fire-and-forget httpx POST; at-most-once (operator_intake_submitted marker claimed before the POST); OPERATOR_INTAKE_ENABLED=false/DO_NOT_TRACK disables; owns installation_id (the feat(telemetry): installation telemetry — usage analytics from self-hosted deployments #758 seed); never raises, never logs the email.
  • routers/setup.py — optional email/company/consent_updates; validates email shape before any write; binds admin email; schedules intake via BackgroundTasks (runs after the response).
  • dependencies.authenticate_user — resolve admin by username or registered email; password-hash guard keeps email-code-only users out of the password path.
  • routers/users.pyPUT /api/users/me/email (own-account, 409 on collision) for the existing-admin transition.
  • Frontend — SetupPassword.vue (fields + consent), Login.vue ("Username or email"), Settings.vue (General → "Admin sign-in email" card).
  • Config — OPERATOR_INTAKE_ENABLED / OPERATOR_INTAKE_URL + .env.example. CSP already allows intake.abilityai.dev (feat(ui): In-app bug reporting from the floating Help widget (hosted intake → GitHub issues) #1116) — no CSP change.
  • Docs — requirements §43, architecture catalog, first-time-setup.md Flow 3 + Worker contract.

Test Plan

Notes for review

  • Cross-tracker: closes the private trinity-enterprise#38 (closed manually at release — keyword doesn't cross repos) and public #82.
  • SetAdminPasswordRequest/UpdateMyEmailRequest kept inline per these routers' existing convention; _EMAIL_RE duplicated across two modules (small) — flag if you'd prefer centralizing.

Fixes abilityai/trinity-enterprise#38
Fixes #82

🤖 Generated with Claude Code

…/trinity-enterprise#38, #82)

Capture an optional operator email/company at first-run setup with an explicit,
unchecked-by-default opt-in to "occasionally receive important security & product
updates", submitted once to a new /v1/operator-intake endpoint on #1116's
Cloudflare intake app. The same email binds as the admin's sign-in identity so
the operator can log in with email + password — no verification email is sent (a
fresh install has no Resend key; the email is bound, not code-verified). The
code-based email second factor stays Phase 2 on the existing mfa_gate seam.

- backend: operator_intake_service (fire-and-forget, at-most-once via a
  system_settings marker, DO_NOT_TRACK aware, owns installation_id); setup
  endpoint captures profile + binds admin email; authenticate_user resolves the
  admin by username OR registered email (password guard blocks code-only users);
  PUT /api/users/me/email for the existing-admin transition
- frontend: SetupPassword email/company + consent checkbox; Login "username or
  email" field; Settings -> General "Admin sign-in email" card
- config: OPERATOR_INTAKE_ENABLED / OPERATOR_INTAKE_URL (+ .env.example)
- docs: requirements section 43, architecture catalog, first-time-setup feature flow
- tests: 16 unit tests (intake idempotency/guards, email-login resolution, setup)

Fixes abilityai/trinity-enterprise#38
Fixes #82

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Comment thread src/backend/routers/setup.py Fixed
Comment thread src/backend/routers/users.py Fixed
Eugene Vyborov and others added 2 commits June 22, 2026 21:25
…in (trinity-enterprise#38, #82)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…#82)

The _EMAIL_RE pattern duplicated into setup.py and users.py had two
[^@\s]+ atoms around the literal \. that both also match '.', giving the
engine many ways to place the dot and backtracking polynomially on
user-controlled email input (CodeQL alerts #211, #212).

Constrain only the final segment to [^@\s.]+ (no dot) so the trailing \.
can align with exactly one position -> linear matching. Behaviour is
unchanged: multi-subdomain addresses still validate; an 80k-char
pathological input now resolves in ~2ms.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@dolho

dolho commented Jun 23, 2026

Copy link
Copy Markdown
Contributor

/review Report — automated structural review

Branch: feature/38-first-run-intake-admin-emaildev (diffed against merge-base)
Files: 16 (+927/−21) · Scope: CLEAN · Plan: AC met (admin email login + consent-gated operator intake)

Reviewed the auth/setup/intake surface in full (read whole files, not just hunks). This is a well-built PR — strong defensive posture. No critical findings.

Critical Findings (block merge)

None.

  • Auth fallback is safedependencies.authenticate_user falls back to email lookup only when no username matches, then hard-gates on a password hash: if not user.get("password"): return False. Email-code-only accounts (no hash) can never password-authenticate, and a non-admin logging in by email gets their own role — no escalation. Directly covered by test_passwordless_email_user_cannot_password_login.
  • Privacy posture is sound — outbound intake fires only on the affirmative consent_updates checkbox and a valid email, is OPERATOR_INTAKE_ENABLED / DO_NOT_TRACK gated, runs as a BackgroundTasks job (never delays/breaks setup), is at-most-once (marker claimed before POST), 5s timeout, and never logs the email (verified in operator_intake_service.py).
  • ReDoS — already de-ambiguated in c16f7e0 ([^@\s.]+ TLD class); no polynomial backtracking. Clean.

Informational Findings (review recommended)

[I1] Auth: email-login uniqueness is app-layer only (Confidence: 6/10)
File: src/backend/dependencies.py:48 + src/backend/routers/users.py (/me/email)
Evidence: login does user = db.get_user_by_email(username.strip().lower()); users.email has no DB unique constraint (the UNIQUE indexes in db/schema.py are on other tables — email_whitelist, agent_sharing, etc.). Uniqueness is enforced only by the /me/email 409 check, which is a check-then-update (TOCTOU) with no backing constraint.
Issue: legacy data or a concurrent /me/email race could produce two accounts with the same email → ambiguous get_user_by_email → non-deterministic password login target.
Suggestion: add a partial unique index on lower(email) for password-bearing accounts (or assert single-row in get_user_by_email). Low real-world likelihood today (setup binds only admin; email-code users get unique emails), hence informational.

Clean Categories

  • SQL/data safety — no raw SQL; all writes via db.* facade. Clean.
  • Auth boundaries/me/email uses get_current_user (self-service, correct) with a cross-account claim guard (409); users.py admin endpoints keep require_admin. Clean.
  • Credential/PII exposure — intake logs only the install-id prefix, never the email; verified no secret/PII in log lines. Clean.
  • Error handling — intake swallows all exceptions by design (fire-and-forget); setup wraps the email write in try/except so a profile write can't break setup. Clean.
  • Test gaps — new auth path (username/email/normalization/passwordless-reject/unknown), intake (consent gating, at-most-once), and setup profile all covered by the 3 new test files. Clean.

Summary

  • Critical: 0 — none found
  • Informational: 1 — I1 (email uniqueness; recommend a DB constraint as hardening, not a blocker)
  • Scope: CLEAN

Automated /review (structural pass — data safety, concurrency, auth boundaries, scope). Complements /validate-pr.

@dolho
dolho merged commit 852c98e into dev Jun 23, 2026
18 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants