Profile settings¶
The Profile settings page is the per-user surface: personal
info, two-factor auth, password, sessions, and the
delete-account flow. It lives at /control-center/profile in the
sidebar (no plan gate).
This page is not the org-level Organization page — that one edits org identity, slug, billing email, and DPA acceptance. Profile is the person who is currently signed in.
Profile hero¶
The hero card shows:
- Avatar — initials on a colour tile (chosen server-side via
avatar_color), OR the OAuth avatar (populated for GitHub / Google users, null for email-registered users). The OAuth image takes precedence over the initials fallback when both exist. - Display name + email + Member since timestamp.
Personal info¶
A two-column form with:
- Display name — editable; saves on submit.
- Email — editable, but the change triggers a confirmation
re-auth (password or 2FA) and a verification email to the new
address. Until the operator clicks the verification link, the
email chip reads
Verification pending. - Resend verification email — visible when the email is unverified. Disabled for 60 seconds after each click.
A Saved indicator next to the Save button acknowledges the last write without taking up screen space.
Security¶
A two-column security section covers:
- Password — visible when the user has a password (email-
registered users). OAuth-only users (no
has_passwordflag) see a one-line explainer + a Set password button that links to the password-set flow.
The password change form requires the current password and validates the new password server-side. The dialog does not unmount on success — the user can change another field without re-entering the password.
- Two-factor auth (TOTP) — three states:
- Not configured —
Enable 2FAopens a modal that generates a TOTP secret, renders a QR code, and asks for the first 6-digit code to confirm. The current password is required (requireCurrentPassword) before the secret is shown. - Enabled —
Disable 2FAandRegenerate backup codes(andRegenerate secret, depending on the build). Both require the current password and a fresh TOTP code. - Pending recovery — for users who lost their device;
the
Regenerateflow can also be used to rotate secrets.
The 2FA status object ({ enabled: boolean, backup_codes_remaining: number, last_used_at: string | null }) drives the chip
in the section header.
Sessions¶
The Session section is a read-only card showing the information security needs to know:
- Last login timestamp and IP.
- Active session count (the current browser + every other logged-in device the user has).
- Sign out of all other devices — single click, immediately
invalidates every other session but keeps the current browser
signed in. The action is logged in the audit log under
action = session.terminate_all. - Log out — single click, signs out the current browser only.
Display preferences¶
The Display preferences section is reserved for per-user view
options. Backend billing is always USD-cents; every cost surface
carries a USD suffix to make that explicit.
Danger zone¶
The Delete account button at the bottom of the page opens a modal that:
- Requires typing the user's display name verbatim
(
Type "Anatolii" to confirm). - Requires entering the current TOTP code if 2FA is enabled (otherwise just the password).
- Calls
DELETE /api/v1/me, which tombstones the user, removes every session, and invalidates every API key the user personally minted. Org-level data (workflows, policies, audit rows) is untouched — that is the Organization → Delete organization flow, not this one. - Signs the operator out and redirects to
nullrun.io.
There is no undo. Audit rows authored by the deleted user
remain in the org's audit log (decided_by UUID preserved) so a
post-delete forensic query still works within the retention
window.
Where to read next¶
- Organization — the org-level identity page (different from this per-user page).
- Audit log — every profile change (password / 2FA / email / sessions) leaves a row.
Deep dive
This surface acts on the person identified by the current session, and a change of credential, address, or device list applies to that person alone and never reaches organization records. Every such mutation is written to the audit log through the same typed path, so a profile change is as visible there as a governance change.
Two-factor enrolment requires the current password before a secret is issued, and the account stays in a pending state until a first code confirms it. Recovery codes are shown once at that point and are not retrievable afterwards; only their remaining count and the time of last use stay visible. A challenge issued partway through a sign-in is honoured by the next request, so an interrupted login does not force the user to start over.
Account deletion requires the display name typed verbatim, plus the current TOTP code when two-factor is on or the password when it is not. For every organization the user owns alongside others, an explicit decision is required: transfer ownership to a named member, or delete the organization and notify its members. Omitting one is rejected, with the member roster supplied so the decision can be made in place. Each owned organization is decided and acted on in a single pass, so there is no window in which a transfer succeeds and the deletion does not. If any session revocation in the cascade fails, the whole operation aborts and nothing is deleted, since the alternative leaves live sessions pointing at an owner who no longer exists.
Deletion tombstones the account, ends every session it holds, and invalidates the API keys the user minted personally; organization data is untouched. A tombstoned account cannot sign in again. Audit rows the user authored remain in the organization's log with the actor identifier intact, so forensic questions stay answerable for the retention window. Two-factor secrets are encrypted at rest, and rotating the encryption key requires an operator runbook because it invalidates both enrolled secrets and signed webhooks.