feat(auth): self-service account management - deletion, password set/reset & resilient email#104
Merged
Merged
Conversation
- 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.
# Conflicts: # apps/api/.env.docker.example
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.
dzhhem
approved these changes
Jun 26, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Description
Adds self-service account management for authenticated users and makes the
email pipeline work in production.
Account management (web + API)
flows for clearer UX.
visible.
Auth / email flows (API)
(1 h reset / 24 h verification), purge-before-issue, all sessions revoked on
password change, and constant 200 responses to avoid account enumeration.
email, preventing account takeover via Google linking.
Production email
Render blocks outbound SMTP (25/465/587), so nodemailer worked locally but
silently failed in production. Verification and reset emails now deliver.
a slow or failing provider can't hang the request or turn the no-enumeration
200 into a 500.
works identically in dev and prod, so the dual transport was redundant.
Config shrinks to
BREVO_API_KEY+MAIL_FROM+ the two base URLs; wiredinto
.env.exampleandrender.yaml.Dependencies / infra
add_password_reset_token.nodemailer(+@types/nodemailer).Closes # (issue)
Type of change
How Has This Been Tested?
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:
BREVO_API_KEYand a verifiedMAIL_FROMinapps/api/.env.POST /api/auth/forgot-password { "email": "<registered>" }→ 200.Checklist:
.env.example,render.yaml)