MEGAINFRA-WEBAPP/docs/setup/email.md
DEls 8acc27466e feat(users): welcome and password-reset emails
Admins can email a new user a welcome with a one-time link to choose
their password, and send a reset link from the users list. The sign-in
page gets "Forgot password?". Mail goes out through Resend; the emails
show the person's sign-in address.

- password_links collection (migration applied): only the SHA-256 of each
  token is stored; invites last 7 days, resets 1 hour, one use each
- /set-password and /forgot-password pages and their /api/auth routes
- forgot-password never reveals whether an account exists, throttles
  repeat mails, and builds links only from APP_URL
- env: RESEND_API_KEY, EMAIL_FROM, EMAIL_REPLY_TO, APP_URL (docs/setup/email.md)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-02 21:39:38 +02:00

86 lines
3.7 KiB
Markdown

# Email from the dashboard
The dashboard sends two mails: a **welcome invite** when an admin adds a user
or presses "Send welcome email", and a **password reset** when someone uses
"Forgot password?" on the sign-in page.
Both go out through **Resend** — one HTTP POST to `https://api.resend.com/emails`,
no SDK and no new dependency. The key stays on the server; the browser never
sees it.
## Environment variables
Set these in the Next.js server environment (Coolify env vars, `.env.local`,
or whatever your host uses):
| Variable | Required | What it does |
| --- | --- | --- |
| `RESEND_API_KEY` | **Yes** | The Resend API key (`re_…`). Without it the mail feature is off and the admin button answers "Email is not set up yet". |
| `EMAIL_FROM` | No | The sender shown on the mail. Defaults to `MEGA Dashboard <support@apphub.co.za>`. |
| `EMAIL_REPLY_TO` | No | The address a reply goes to. Falls back to the sender. |
| `APP_URL` | No | The dashboard's public URL, used to build the link in the mail and to point the logo at your own server. Falls back to the request's own origin. |
`RESEND_API_KEY` is the **same key** you already use as the PocketBase SMTP
password. Resend's "API key" and its "SMTP password" are different strings —
use the API key here (`re_…`), not the SMTP password.
```
RESEND_API_KEY=re_xxxxxxxxxxxxxxxxxxxx
EMAIL_FROM=MEGA Dashboard <support@apphub.co.za>
EMAIL_REPLY_TO=support@apphub.co.za
APP_URL=https://wfdash.mega.infra.apphub.co.za
```
`APP_URL` must be the address people actually open. If it is wrong, every
"Choose your password" link points somewhere nobody can reach.
## Run the migration
The links live in a PocketBase collection. Run this once against the
environment you are deploying to:
```
PB_URL=https://pb.mega.infra.apphub.co.za \
PB_ADMIN_EMAIL=admin@mega.co.za \
PB_ADMIN_PASSWORD=... \
python3 scripts/migrations/2026-11-02-password-links.py
```
The script is idempotent — it creates `password_links` when it is missing and
verifies it when it is there. Every API rule on the collection stays `null`, so
only the server's admin client can read or write these rows.
The collection holds no passwords and no live links: only the SHA-256 hash of
each token. A row lifted from the database cannot be turned back into a
working link.
## Moving to megainfra.co.za later
Until the domain is verified, mail from `support@apphub.co.za` may land in
spam or be rejected. To send as your own domain:
1. **Verify the domain in Resend.** Resend → Domains → Add Domain. Use
`megainfra.co.za`, or a sub-domain such as `mail.megainfra.co.za` if the
root domain is used for something else.
2. **Add the DNS records** Resend shows you (SPF, DKIM, and a DMARC record if
offered) to the DNS zone for the domain. Wait until Resend marks the domain
as verified — usually minutes, sometimes longer if the zone is slow.
3. **Change `EMAIL_FROM`** to the verified sender, for example
`MEGA Dashboard <no-reply@megainfra.co.za>`, and restart the app.
Keep `EMAIL_FROM` on a domain you have actually verified. Sending from an
unverified domain is the usual reason mail disappears.
## What the user sees
- **Welcome** — a greeting, what the dashboard (or the clock app, for a clock
user) is for, a "Choose your password" button, three short getting-started
steps, and the plain link for mail clients that will not show a button. The
link is valid for **7 days**.
- **Reset** — a "Reset password" button, valid for **1 hour**, and a line
telling them to ignore the mail if they did not ask for it.
Both pages also work by hand: `/forgot-password` asks for the email and
`/set-password?token=…` sets the new password. The forgot-password route
always answers the same way, so it never reveals whether an address has an
account.