Commit Graph

2 Commits

Author SHA1 Message Date
Tyler 7e665ea43e Email service surfaces real Resend errors instead of silent false
Deploy to route.crispygoat.com / deploy (push) Successful in 4m5s
The sendEmail / sendCampaignEmail / sendWelcomeEmail /
sendOrderReceiptEmail / sendPasswordResetEmail / sendOperationalAlert
helpers all returned Promise<boolean> and silently returned false on
any failure (missing API key, 401 invalid key, 422 unverified sender
domain, network error, etc.). The caller had no way to know which
problem it was.

Switch the public return type to a structured result:

  export type EmailSendResult = { ok: true } | { ok: false; error: string };

- Missing RESEND_API_KEY: error reads 'RESEND_API_KEY is not set.
  Add it to .env.local (or your hosting dashboard) and restart the
  server.'
- 4xx/5xx: error reads 'Resend 422 — validation_error: The gmail.com
  domain is not verified' (extracts Resend's own name + message).
- Network failure: error reads the thrown message verbatim.
- Non-JSON error body: falls back to 'Resend <status>'.

Callers updated:
  - src/actions/admin/users.ts (createAdminUser): now propagates the
    real emailError into the modal UI — no more generic 'sendWelcome
    Email returned false'.
  - src/actions/checkout.ts: logs the real error.
  - src/app/api/cron/send-scheduled/route.ts: per-recipient error
    logged for the scheduled-campaigns cron.

Tests:
  - tests/unit/email-service.test.ts (new, 5 tests): covers happy
    path, missing key, 4xx/5xx with JSON body, 4xx/5xx with non-JSON
    body, and network failure.
  - tests/unit/create-admin-user.test.ts: updated to use the new
    { ok, error? } return shape; new assertion that the action
    surfaces the real emailError string into the result.
2026-06-17 12:10:04 -06:00
Tyler d75380eb9a Create-user flow now provisions the Neon Auth account
Deploy to route.crispygoat.com / deploy (push) Successful in 4m18s
Previously createAdminUser only inserted a local admin_users row and
emailed the password as plaintext — the user could not sign in because
the password was never set on a Neon Auth account.

This change makes the create flow:
  1. Authorize: only platform_admin can mint new admin users.
  2. Create the Neon Auth user via auth.admin.createUser, falling back
     to the public /sign-up/email + emailVerified=true pattern when
     the caller's Neon Auth session isn't an admin (the common case
     in dev — provision-admin.ts does not set neon_auth.user.role).
  3. Wrap the admin_users INSERT + admin_user_brands link in a
     transaction, returning an error if the local insert fails (the
     Neon Auth user is left orphaned and surfaced in the message).
  4. Send the welcome email best-effort, returning success/failure
     info to the UI.

The CreateUserModal now shows a success state with the temp password
(copy-to-clipboard), the welcome email status, and the auth path
used. The slide-in edit panel surfaces the password via window.alert
as a defense against the dead-code new-user path.

10 new unit tests cover authorization, the admin + signup paths, the
DB-failure orphan case, and the email best-effort behavior.
2026-06-17 11:46:39 -06:00