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>
86 lines
3.7 KiB
Markdown
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.
|