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:
```bash
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:
```bash
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](https://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.