Skip to content

feat: resend account activation email endpoint - #321

Open
hhvrc wants to merge 5 commits into
developfrom
feature/resend-activation-email
Open

feat: resend account activation email endpoint#321
hhvrc wants to merge 5 commits into
developfrom
feature/resend-activation-email

Conversation

@hhvrc

@hhvrc hhvrc commented Jun 24, 2026

Copy link
Copy Markdown
Contributor

Summary

Adds a user-facing action to re-send the account activation email, so users who never received (or lost) their activation email can request a new one. No database migration required — it reuses the existing UserActivationRequest table.

This is the first, deliberately small slice of a larger email-features effort (admin send-any-email-type, list email types, and an upstream-failure retry queue will follow in separate PRs).

Endpoint

POST /{version}/account/activate/resend

{ "email": "user@example.com" }

Always returns a generic 200 OK (LegacyEmptyResponse).

Behavior

  • Rotates the activation token on every resend (invalidating any previously sent link) and persists it before sending, so the link in the new email always matches the stored hash.
  • Creates an activation request if an unactivated account doesn't have one yet (e.g. legacy data or a failed initial send), so the user can still complete activation.
  • Silently no-ops for unknown, already-activated, or deactivated accounts and always returns a generic 200 — the endpoint can't be used to probe which emails are registered or their activation state.
  • Rate limited under the existing auth policy (mirrors POST /account/reset).
  • Increments UserActivationRequest.EmailSendAttempts for observability (the column already existed and was unused).

Tests

Added integration tests (MailTests.cs, delivered via the Mailpit SMTP test server):

  • ResendActivation_UnactivatedUser_SendsWorkingActivationEmail — create-request path; the resent link actually activates the account.
  • ResendActivation_RotatesToken_PreviousLinkInvalidated — after a resend, the original token returns 400 and the fresh token returns 200.
  • ResendActivation_AlreadyActivatedUser_Returns200_AndSendsNoEmail
  • ResendActivation_UnknownEmail_Returns200_AndSendsNoEmail

All 4 pass locally.


Open in Stage

Adds POST /{version}/account/activate/resend so users can request a fresh
activation email. The endpoint:

- rotates the activation token (invalidating any previously sent link) and
  persists it before sending, so the emailed link matches the stored hash
- creates an activation request if the unactivated account lacks one (e.g.
  legacy data or a failed initial send)
- silently no-ops for unknown, already-activated, or deactivated accounts and
  always returns a generic 200, so it can't be used to probe account state
- is rate limited under the existing "auth" policy and tracks EmailSendAttempts

No database migration required (reuses the existing UserActivationRequest table).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@ghost

ghost commented Jun 24, 2026

Copy link
Copy Markdown

Ready to review this PR? Stage has broken it down into 4 individual chapters for you:

Title
1 Define account activation resend service interface
2 Implement activation email resend logic
3 Expose resend activation endpoint
4 Test activation email resend behavior
Open in Stage

Chapters generated by Stage for commit 9e021d7 on Jun 30, 2026 11:12pm UTC.

@hhvrc hhvrc closed this Jun 24, 2026
@hhvrc hhvrc reopened this Jun 24, 2026
@hhvrc hhvrc closed this Jun 24, 2026
@hhvrc
hhvrc deleted the feature/resend-activation-email branch June 24, 2026 08:04
@hhvrc
hhvrc restored the feature/resend-activation-email branch June 24, 2026 09:42
@hhvrc hhvrc reopened this Jun 24, 2026
@hhvrc hhvrc self-assigned this Jun 24, 2026
hhvrc and others added 2 commits July 1, 2026 00:32
Brings in the durable email outbox (#327, squashed as 47029f0) and the v1 auth
endpoint retirement. Adapts the resend-activation feature to the new model:

- AccountService.ResendActivationEmailAsync now enqueues an EmailOutboxMessage
  (ForAccountActivation) and nudges the outbox instead of sending inline via
  IEmailService. It ensures the activation request exists (seeded hash) and lets the
  Cron delivery job mint the token lazily on send - so a resend supersedes the older
  pending activation (coalesce key) and invalidates the previous link.
- The resend MailTests now assert outbox enqueue (row type / recipient / coalesce key)
  or that nothing is enqueued, matching the API test split; delivery + token rotation
  are the Cron host's job (Cron.IntegrationTests).
…ivation-email

# Conflicts:
#	API/Services/Account/AccountService.cs
#	API/Services/Account/IAccountService.cs
@coderabbitai

coderabbitai Bot commented Aug 14, 2026

Copy link
Copy Markdown

Warning

Review limit reached

@hhvrc, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 48 minutes

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: b707df9b-86c9-4d4e-a52d-6ab9257aff7e

📥 Commits

Reviewing files that changed from the base of the PR and between 0dd1fdd and 89def06.

📒 Files selected for processing (4)
  • API.IntegrationTests/Tests/MailTests.cs
  • API/Controller/Account/ResendActivation.cs
  • API/Services/Account/AccountService.cs
  • API/Services/Account/IAccountService.cs

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

hhvrc added 2 commits August 14, 2026 12:04
Drop the LegacyEmptyResponse envelope in favour of a bare Ok(), matching
how the newer endpoints (PasswordResetInitiateV2, SignUpV2) respond. The
message it carried only restated the documented 200 response.

The integration tests only assert the status code, so they are unaffected.
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.

1 participant