Skip to content

PR3 — Account/security endpoints + RESTful verification-codes #18

Description

@pedromvgomes

Summary

Add the account/security endpoints the My Account Security tab needs, and make the one-time-code request RESTful. All on the USER plane, owner-scoped, in the Identities aggregate (inside tenants).

This is PR3 of the My Account initiative. Independent of PR2; benefits from PR1 landing first but does not strictly require it.

Why

Research found the Security tab's actions are unbacked today. We add exactly what the design shows — no more (no device list / per-session revoke; the design only has "Sign out" + "Sign out of all").

Scope

1. RESTful one-time codes — POST /v1/verification-codes

  • Body: { "email": "...", "purpose": "signup" | "password_reset" } → sends a one-time, email-proof code (emailed in prod via Resend; returned in the response in dev, matching existing code behavior).
  • Supersedes POST /v1/auth/password/reset-code and the web-signup use of POST /v1/enrollment-codes.
  • purpose is bound to the issued code so a password_reset code cannot be replayed against signup (and vice-versa).
  • Same per-IP rate limiting as the existing code endpoints.
  • Daemon enrollment stays on /v1/enrollment-codes for now — convergence tracked separately (tech-debt issue).

2. Change password = email-code-exchange (no new mutation endpoint)

  • The Security-tab "Change password" drives the existing flow: POST /v1/verification-codes {purpose:"password_reset"}POST /v1/auth/password/reset {code, new_password}. Proving email access is mandatory (no current-password path).
  • Consistent consequence: reset revokes all sessions; after success the user is bounced to sign-in with the success banner. (No backend change needed beyond WS-H: My Account SPA (site/) + account API #1; this item is mostly a contract note for the SPA + a test that the in-app path works end-to-end.)

3. Connected sign-in methods

  • GET /v1/me/identities → list the caller's login methods:
    [ { "provider": "password", "label": "pedro@example.com", "connected_at": "..." },
      { "provider": "google", "label": "pedro@gmail.com", "connected_at": "..." } ]
  • GET /v1/auth/oidc/{provider}/start?mode=link — when called while authenticated, carries the tenant through the signed OAuth state; the callback links the verified provider identity to the current account. Guard: reject (409) if that provider-subject is already linked to a different tenant.
  • DELETE /v1/me/identities/{provider} — unlink. Guarded by the "≥1 login method must always remain" invariant (422/409 if it would remove the last one).

4. Sessions

  • DELETE /v1/me/sessions — "sign out of all sessions": revoke the whole collection (including the current one), clear the cookie, → client redirects to sign-in. Reuses the existing delete_for_tenant.
  • Existing POST /v1/auth/logout stays for the current session. No device list / per-session revoke (not in the design).

Acceptance criteria

  • POST /v1/verification-codes works for both purposes; purpose binding enforced (cross-purpose code rejected); old password/reset-code removed (or aliased) and web-signup migrated to the new resource.
  • GET /v1/me/identities returns the caller's methods; never leaks secrets/hashes.
  • OIDC mode=link links to the authenticated tenant; cross-tenant subject collision rejected.
  • DELETE /v1/me/identities/{provider} enforces ≥1-remaining.
  • DELETE /v1/me/sessions revokes all sessions + clears cookie; subsequent silent token exchange 401s.
  • In-app change-password (code-exchange) verified end-to-end (revokes sessions → re-auth).
  • utoipa annotations for all new endpoints; existing auth tests adjusted for the code-endpoint rename.

References

  • crates/tenants/src/api/auth.rs, repository/identity.rs, repository/session.rs, api/codes.rs.
  • ADR docs/adr/0009-human-web-authentication.md (the auth/session model).
  • Part of the My Account initiative (refines WS-H: My Account SPA (site/) + account API #1).

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or requestrustPull requests that update rust code

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions