MediBot / docs /AUTH_EMAIL.md
github-actions[bot]
Deploy MedOS Global from 956ae78f
e3e3e14
|
Raw
History Blame Contribute Delete
5.99 kB

Auth email — verification & password reset

MedOS sends two transactional emails:

Email Trigger Code expiry
Verify email POST /api/auth/register (or /resend-verification) 15 min
Reset password POST /api/auth/forgot-password 1 hour

Both go through lib/email.ts. The code path is always wired — what determines whether real email gets sent is which transport is configured.

The three transports

lib/email.ts picks at runtime, first match wins:

Order Transport When chosen Notes
1 Resend HTTP API RESEND_API_KEY is set Recommended on HF Spaces / Vercel. Uses HTTPS to api.resend.com.
2 SMTP (nodemailer) SMTP_HOST + SMTP_USER + SMTP_PASS all set Works with any provider.
3 Console fallback nothing above is set Emails are logged to stdout. They never reach a real inbox.

The console fallback is the state that produces "Account created! Check your email" with no email ever arriving. The API returns 200/201 either way (this is deliberate to avoid email enumeration), so the only signal is in the Space's container logs.

Confirm which transport is active (one curl)

As an authenticated admin:

curl -s -H "Authorization: Bearer $ADMIN_TOKEN" \
  https://ruslanmv-medibot.hf.space/api/admin/email-status
# → {"transport":"resend","from":"...","appUrl":"..."}
# → {"transport":"smtp","from":"...","appUrl":"..."}
# → {"transport":"console","from":"...","appUrl":"..."}  ← nothing reaches inboxes

You can also stream the Space's container logs and grep:

curl -N -H "Authorization: Bearer $HF_TOKEN" \
  "https://huggingface.co/api/spaces/<owner>/<space>/logs/run" | grep -E '\[EMAIL|\[Register'

Every register / forgot-password / resend-verification call now prints the transport name and the outcome:

[Register] queued verification email via transport=resend to=user@example.com
[EMAIL Resend] ok id=abc123 to=user@example.com subject="MedOS — verify your email"

If you see transport=console, no email has been sent.

Wiring Resend (recommended)

  1. Sign up at resend.com.

  2. Create an API key in the dashboard.

  3. (Optional) Verify a sending domain. Until you do, the FROM_EMAIL must use onboarding@resend.dev — Resend rejects other senders for unverified domains.

  4. On the HF Space → Settings → Variables and secrets, set:

    Key Type Value
    RESEND_API_KEY secret your Resend API key
    FROM_EMAIL variable MedOS <onboarding@resend.dev> (or noreply@your-verified-domain)
    APP_URL variable https://ruslanmv-medibot.hf.space (or your Vercel URL)
  5. Restart the Space so the new env loads.

  6. Hit GET /api/admin/email-status — it should return {"transport":"resend",…}.

APP_URL matters for the password-reset email: the email contains a one-click link ${APP_URL}?action=reset&email=…&code=… that drops the user straight into the "set new password" step. If APP_URL is wrong the email's code-paste path still works as a fallback.

Wiring SMTP (alternate)

Only used if RESEND_API_KEY is not set. Pick a provider:

Provider SMTP_HOST SMTP_PORT SMTP_USER SMTP_PASS Free tier
Resend (SMTP gateway) smtp.resend.com 465 resend API key 3,000/mo
SendGrid smtp.sendgrid.net 587 apikey (literal) API key 100/day
Mailgun smtp.mailgun.org 587 postmaster@<domain> SMTP password 100/day for 30 days
AWS SES email-smtp.<region>.amazonaws.com 587 IAM SMTP user IAM SMTP password Free from EC2
Gmail smtp.gmail.com 587 Gmail address App password (NOT account password) Personal only

Any SMTP provider works — lib/email.ts doesn't hard-code anyone.

How the password-reset flow runs end-to-end

  1. User clicks Forgot password? → enters email → POST /api/auth/forgot-password.
  2. Backend looks up the user, generates a 6-digit code, stores it in users.reset_token with a 1-hour expiry, and calls sendPasswordResetEmail(email, code).
  3. Email arrives with two ways to act:
    • Click the Reset password button — opens ${APP_URL}?action=reset&email=…&code=…. The frontend (usePasswordResetLink hook + MedOSApp) parses those params on mount, switches the LoginView to its reset step with email and code pre-filled, and strips the params from the URL so they don't sit in browser history.
    • Paste the 6-digit code manually — for mail clients that strip query strings or for users on a different device.
  4. User sets a new password → POST /api/auth/reset-password.
  5. Backend validates the code + expiry, bcrypts the new password, wipes all existing sessions for that user (security best practice), issues a new session token, returns it.
  6. Frontend stores the token → user is logged in with the new password.

Operational notes

  • The reset code is stored as plaintext in users.reset_token today. Codes expire in 1h and are invalidated on use, so the window is small, but if you ever dump the DB while one is pending, treat it as exposed.
  • There is no in-product rate limit on /api/auth/forgot-password today. If you start hitting the email provider's quota or seeing abusive patterns in the audit log, that's the place to add one.
  • The "user not found" branch returns the same success message as the "user found" branch — this is deliberate, to prevent email enumeration. Don't change this.
  • register/route.ts historically had sendVerificationEmail(…).catch(() => {}) which swallowed every email error silently. That's been replaced with a .then(ok => …) that logs failures — if a Resend or SMTP call fails, you'll see [Register] verification email FAILED to=… in the container logs.