Skip to content

feat(auth): self-service account management - deletion, password set/reset & resilient email#104

Merged
BODMAT merged 9 commits into
masterfrom
feat/auth-account-mgmt
Jun 26, 2026
Merged

feat(auth): self-service account management - deletion, password set/reset & resilient email#104
BODMAT merged 9 commits into
masterfrom
feat/auth-account-mgmt

Conversation

@BODMAT

@BODMAT BODMAT commented Jun 26, 2026

Copy link
Copy Markdown
Collaborator

Description

Adds self-service account management for authenticated users and makes the
email pipeline work in production.

Account management (web + API)

  • Self-service account deletion for logged-in users.
  • Split the cluttered profile popup into separate Account and Auth
    flows for clearer UX.
  • Set or change the account password directly from the account popup.
  • Keep autofilled inputs on-theme so the password show/hide eye icon stays
    visible.

Auth / email flows (API)

  • Forgot/reset password via email — hashed, single-use, time-limited tokens
    (1 h reset / 24 h verification), purge-before-issue, all sessions revoked on
    password change, and constant 200 responses to avoid account enumeration.
  • Require the linked Google account's email to match the existing account
    email, preventing account takeover via Google linking.

Production email

  • Send transactional mail via Brevo's HTTP API (port 443) instead of raw SMTP.
    Render blocks outbound SMTP (25/465/587), so nodemailer worked locally but
    silently failed in production. Verification and reset emails now deliver.
  • Email is sent in the background from forgot-password / resend-verification so
    a slow or failing provider can't hang the request or turn the no-enumeration
    200 into a 500.
  • Drop the SMTP/nodemailer path entirely (and its dependency) — the Brevo API
    works identically in dev and prod, so the dual transport was redundant.
    Config shrinks to BREVO_API_KEY + MAIL_FROM + the two base URLs; wired
    into .env.example and render.yaml.

Dependencies / infra

  • New Prisma migration: add_password_reset_token.
  • Removed dependency: nodemailer (+ @types/nodemailer).

Closes # (issue)

Type of change

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to not work as expected)
  • Documentation update
  • Refactoring (no functional changes)

How Has This Been Tested?

  • Unit tests (Jest) — 113 passing
  • Integration tests — 260 passing (incl. password-reset HTTP/validation contract)
  • Manual testing — verified end-to-end forgot/reset password against Brevo:
    email delivered to the inbox and the reset link works (confirmed via Brevo
    delivery events). Typecheck, ESLint and the production API build are clean.

Reproduce:

  1. Set BREVO_API_KEY and a verified MAIL_FROM in apps/api/.env.
  2. POST /api/auth/forgot-password { "email": "<registered>" } → 200.
  3. Open the link from the email → set a new password → existing sessions are revoked.

Checklist:

  • My code follows the style guidelines of this project
  • I have performed a self-review of my own code
  • I have documented non-obvious behavior or constraints where necessary
  • I have made corresponding changes to the documentation (.env.example, render.yaml)
  • My changes generate no new warnings
  • I have added tests that prove my fix is effective or that my feature works
  • New and existing unit tests pass locally with my changes
  • Any dependent changes have been merged and published in downstream modules
  • (If API) Database migrations have been created and tested
  • (If UI) Changes look good on mobile and desktop

BODMAT added 9 commits June 24, 2026 16:51
- web: red "Delete account" button + inline confirm in the profile popup,
  wired to the existing deleteAccount mutation; style matches the
  "Delete my Monobank data" button
- web: tear down local session only on successful deletion (onSuccess
  instead of onSettled) so a failed delete keeps the user signed in
- api: deleteCurrentUser now clears auth cookies; export clearAuthCookies
- i18n: add delete-account keys (en, uk, de)
After login the sidebar now shows two buttons:
- "Account" -> new AccountPopup: identity, edit name & photo (NEW),
  link Google/Telegram, logout / logout-all / delete account
- "Login / Register" -> slimmed auth popups that switch between each other

- new AccountPopup + AccountActions (account-only actions)
- RegisterPopup is registration-only; LoginPopup drops the logged-in
  management block; removed RegisterPopupActions
- types: add UpdateUserBody so updateMe/update send a partial payload
  (name/photo only, without touching auth methods)
- i18n: account/auth keys (en, uk, de)
Adds a password section to AccountPopup:
- users with an existing email login can set a new password
- passwordless users (Google, etc.) reuse their known email automatically
- accounts with no email at all (Telegram-only) provide an email + password

Reuses PATCH /users/me (updateUserAuthMethods upserts the EMAIL auth
method) with client-side password-complexity validation. Also exposes
the user's email in UserResponse so the known address can be reused.

- new AccountPasswordForm component
- types: add email to UserResponseSchema
- i18n: password section keys (en, uk, de)
…ays visible

Browser autofill repainted password fields with a light background while the
show/hide-password eye icon kept the light --color-text colour, making it
invisible on the dark theme across the login/register/account/reset popups.
Pin the autofill background and text colour to the input theme tokens.
Mirror the existing email-verification flow for password recovery:

- Prisma PasswordResetToken model (hashed token, 1h TTL, cascade on user
  delete) + migration.
- service.createPasswordResetToken / consumePasswordResetToken: resolve the
  user by EMAIL auth method or primary email, set the new bcrypt hash on the
  EMAIL auth method (creating it from the known email when missing) and revoke
  all sessions.
- mailer.sendPasswordResetEmail links to the frontend reset page
  (PASSWORD_RESET_BASE_URL), not the API.
- POST /auth/forgot-password (always 200, no enumeration leak) and
  POST /auth/reset-password, both rate-limited and CSRF-exempt.
- Shared zod ForgotPasswordSchema / ResetPasswordSchema (min 8 + complexity).
- Web: forgotPassword/resetPassword api + useAuth, a 'Forgot password?' flow in
  the login popup, and a public /reset-password page. i18n for en/uk/de.
- Integration tests for the forgot/reset HTTP contract.
Linking Google from the account popup went through the generic
google/exchange login path, which created/switched to a separate account when
the chosen Google email differed from the registered one instead of attaching
to the current user.

- service.linkGoogleToUser links Google to the current user and rejects a
  Google account whose email doesn't match the account's email (mirrors the
  Telegram link flow); guards against an already-linked sub or existing Google
  method.
- Authenticated POST /auth/link/google (linkGoogle controller).
- Web: a sessionStorage link-intent flag set from the account popup routes the
  post-redirect id token to the link endpoint (not login); OAuthBridge keeps
  the user signed in on failure and surfaces the result in the account popup
  via a small notice store. Login/register Google buttons clear the flag.
- i18n googleLinked/googleLinkFailed for en/uk/de; OAuthBridge link tests.
Render blocks outbound SMTP (ports 25/465/587), so nodemailer to Gmail
works locally but silently fails in production — verification and
password-reset emails never arrive.

Rework the mailer to pick its transport at runtime: when BREVO_API_KEY is
set it sends over Brevo's HTTP API (port 443, unblocked on Render),
otherwise it falls back to direct SMTP for local dev. A shared sendMail()
helper removes the duplicated transport setup, and SMTP gets connect/socket
timeouts so a blocked port can't hang the request.

Also send mail in the background from forgot-password / resend-verification:
a slow or failing provider must not delay the response or turn the
no-enumeration 200 into a 500.

Wire BREVO_API_KEY (+ previously missing SMTP_SECURE / PASSWORD_RESET_BASE_URL)
into .env.example and render.yaml.
The runtime SMTP-vs-Brevo selection was redundant: the Brevo HTTP API
works identically locally and in production, so the nodemailer fallback
only added a second code path, an extra dependency, and confusion around
SMTP_FROM doubling as the Brevo sender.

Collapse to a single path — always Brevo — and rename SMTP_FROM to the
clearer MAIL_FROM. Email config shrinks to BREVO_API_KEY + MAIL_FROM +
the two base URLs. Drop the nodemailer dependency and its security
override. When BREVO_API_KEY is unset, sending is skipped as before.
@BODMAT BODMAT requested a review from dzhhem June 26, 2026 13:35
@BODMAT BODMAT merged commit 54523ff into master Jun 26, 2026
12 checks passed
@BODMAT BODMAT deleted the feat/auth-account-mgmt branch June 26, 2026 16:06
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.

2 participants