feat(setup): first-run operator intake + admin email login (trinity-enterprise#38, #82) - #1307
Conversation
…/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>
…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>
/review Report — automated structural reviewBranch: 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.
Informational Findings (review recommended)[I1] Auth: email-login uniqueness is app-layer only (Confidence: 6/10) Clean Categories
Summary
Automated |
Summary
Two converging first-run features built on one email capture:
POST /v1/operator-intakeendpoint on feat(ui): In-app bug reporting from the floating Help widget (hosted intake → GitHub issues) #1116's Cloudflare intake app (intake.abilityai.dev) — one added endpoint, same hosted app.admin.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_gateseam (#5/#388) — so this PR does not touch that call site.Changes
services/operator_intake_service.py— fire-and-forgethttpxPOST; at-most-once (operator_intake_submittedmarker claimed before the POST);OPERATOR_INTAKE_ENABLED=false/DO_NOT_TRACKdisables; ownsinstallation_id(the feat(telemetry): installation telemetry — usage analytics from self-hosted deployments #758 seed); never raises, never logs the email.routers/setup.py— optionalemail/company/consent_updates; validates email shape before any write; binds admin email; schedules intake viaBackgroundTasks(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.py—PUT /api/users/me/email(own-account, 409 on collision) for the existing-admin transition.SetupPassword.vue(fields + consent),Login.vue("Username or email"),Settings.vue(General → "Admin sign-in email" card).OPERATOR_INTAKE_ENABLED/OPERATOR_INTAKE_URL+.env.example. CSP already allowsintake.abilityai.dev(feat(ui): In-app bug reporting from the floating Help widget (hosted intake → GitHub issues) #1116) — no CSP change.first-time-setup.mdFlow 3 + Worker contract.Test Plan
test_operator_intake.py,test_admin_email_login.py,test_setup_operator_profile.py): intake idempotency/disable/no-email/failure-swallow, email-login resolution + passwordless guard, setup capture/consent/invalid-email./v1/operator-intakeWorker endpoint (out-of-repo, sibling to/v1/report-bug) — contract in the feature flow.Notes for review
trinity-enterprise#38(closed manually at release — keyword doesn't cross repos) and public#82.SetAdminPasswordRequest/UpdateMyEmailRequestkept inline per these routers' existing convention;_EMAIL_REduplicated across two modules (small) — flag if you'd prefer centralizing.Fixes abilityai/trinity-enterprise#38
Fixes #82
🤖 Generated with Claude Code