Internal Documentation internal
TalkIDE internal documentation

Two-step password reset flow: first request a reset token by email, then submit the new password using the token. Public access without authentication.

  • Step 1 always returns 200 OK regardless of whether the email exists — prevents user enumeration.
  • The reset token is single-use and expires after 1 hour.
  • password and passwordConfirmation must match and meet the minimum length requirement.
  • After generating the reset token, the backend sends a password reset e-mail via EmailSender (see ADR-025).
    • production profile: real e-mail sent via Mailgun from noreply@mail.talkide.app.
    • !production profile (NoOpEmailSender): no e-mail is sent; the full reset link is logged to the console at INFO level (same visibility as MVP behavior).
  • E-mail sending failure does not affect the API response — the endpoint always returns 200 OK even if the EmailSender throws. The failure is logged and recorded in email_log with status=FAILED.
sequenceDiagram
    actor User

    User->>+FE: fills Forgot Password form (email)

    FE->>FE: validate form
    alt form is invalid
        FE-->>User: show error messages <br> disable submit button
    end

    User->>+FE: submits Forgot Password form

    FE->>+BE: POST /api/v1/auth/forgot-password <br> ForgotPasswordRequest

    BE->>BE: validate request
    alt request is invalid
        BE-->>FE: 400 Bad Request <br> ErrorResponse
    end

    BE->>DB: find user by email (silently ignore if not found)
    BE->>DB: create password reset token (if user exists)
    BE->>+BE: send password reset e-mail via EmailSender <br> (production: Mailgun; !production: log link to console)
    BE-->>-BE: result logged; failure does not affect response

    BE->>-FE: 200 OK (always)

    FE->>-User: show "If the email exists, a reset link was sent" message

    User->>+FE: opens reset link, fills Reset Password form

    FE->>FE: validate form
    alt form is invalid
        FE-->>User: show error messages <br> disable submit button
    end

    User->>+FE: submits Reset Password form

    FE->>+BE: POST /api/v1/auth/reset-password <br> ResetPasswordRequest

    BE->>BE: validate request
    alt request is invalid
        BE-->>FE: 400 Bad Request <br> ErrorResponse
    end

    BE->>DB: find reset token record
    alt token not found or expired
        BE-->>FE: 400 Bad Request <br> ErrorResponse
    end

    BE->>BE: verify passwordConfirmation matches password
    alt passwords do not match
        BE-->>FE: 400 Bad Request <br> ErrorResponse
    end

    BE->>DB: update user password hash
    BE->>DB: delete (invalidate) reset token

    BE->>-FE: 204 No Content

    FE->>-User: show "Password changed" message, redirect to login

Step 1 — Request Password Reset

POST /api/v1/auth/forgot-password ForgotPasswordRequest:

{
  "email": "jane.doe@example.com"
}

200 OK (always, no body)

400 Bad Request (validation) ErrorResponse:

{
  "status": 400,
  "code": "VALIDATION_ERROR",
  "message": "Validation failed",
  "errors": [
    { "field": "email", "message": "must be a valid email address" }
  ]
}

Step 2 — Reset Password

POST /api/v1/auth/reset-password ResetPasswordRequest:

{
  "token": "a1b2c3d4-e5f6-...",
  "password": "newSecret123",
  "passwordConfirmation": "newSecret123"
}

204 No Content (success, no body)

400 Bad Request (validation) ErrorResponse:

{
  "status": 400,
  "code": "VALIDATION_ERROR",
  "message": "Validation failed",
  "errors": [
    { "field": "password", "message": "size must be between 8 and 72" }
  ]
}

400 Bad Request (invalid or expired token) ErrorResponse:

{
  "status": 400,
  "code": "INVALID_RESET_TOKEN",
  "message": "Password reset token is invalid or expired"
}

Frontend

Step 1 — Validations

FieldConstraintsSizePatternNote
emailnot_blank^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$

Step 2 — Validations

FieldConstraintsSizePatternNote
tokennot_blankread from URL query parameter
passwordnot_blank8 - 72
passwordConfirmationnot_blankmust match password (client-side check)

Backend

Step 1 — Validations

FieldConstraintsSizePatternNote
emailnot_blank, email

Step 2 — Validations

FieldConstraintsSizePatternNote
tokennot_blank
passwordnot_blank8 - 72
passwordConfirmationnot_blankmust equal password (domain validation)

Test Cases

GIVENWHENTHEN
email belongs to existing userforgot-password is called200 OK, reset token created in DB, password reset e-mail sent (production: Mailgun, !production: link logged to console), email_log entry created with status=SENT
email does not belong to any userforgot-password is called200 OK (no token created, no error exposed)
invalid email formatforgot-password is called400 VALIDATION_ERROR error response is returned
email field is blankforgot-password is called400 VALIDATION_ERROR error response is returned
valid token, matching passwordsreset-password is called204 No Content, password updated, token deleted from DB
token not found in DBreset-password is called400 INVALID_RESET_TOKEN error response is returned
token is expiredreset-password is called400 INVALID_RESET_TOKEN error response is returned
password shorter than 8 charactersreset-password is called400 VALIDATION_ERROR error response is returned
passwordConfirmation does not match passwordreset-password is called400 VALIDATION_ERROR error response is returned
invalid request (empty body)reset-password is called400 VALIDATION_ERROR error response is returned
email belongs to existing user, EmailSender throwsforgot-password is called200 OK, reset token created in DB, email_log entry with status=FAILED, no exception propagated

Was this page helpful?

Thanks for the feedback.