diff --git a/.env.example b/.env.example index d12200c..c9a9fc0 100644 --- a/.env.example +++ b/.env.example @@ -32,11 +32,11 @@ APP_PUBLIC_BASE_URL=http://localhost:3000 # ---------- SMTP (local Docker uses Mailpit via docker-compose.yml) ---------- # docker compose up → view mail at http://localhost:8025 # Leave MAIL_HOST unset in .env to use compose defaults (mailpit). -# Production: use Resend/Brevo SMTP — see README "Email (password reset)" +# Production (Resend SMTP) — see docs/production-email-checklist.md # MAIL_HOST=smtp.resend.com # MAIL_PORT=587 -# MAIL_USERNAME= -# MAIL_PASSWORD= +# MAIL_USERNAME=resend +# MAIL_PASSWORD=re_... # API key; never commit a real value # MAIL_FROM=noreply@bilhej.se # ---------- Production admin (prod profile only) ---------- diff --git a/.forgejo/workflows/deploy.yml b/.forgejo/workflows/deploy.yml index 9bc878b..cb8e0a0 100644 --- a/.forgejo/workflows/deploy.yml +++ b/.forgejo/workflows/deploy.yml @@ -39,6 +39,12 @@ jobs: SWISH_NUMBER: ${{ secrets.SWISH_NUMBER }} ADMIN_EMAIL: ${{ secrets.ADMIN_EMAIL }} ADMIN_PASSWORD: ${{ secrets.ADMIN_PASSWORD }} + APP_PUBLIC_BASE_URL: ${{ secrets.APP_PUBLIC_BASE_URL }} + MAIL_HOST: ${{ secrets.MAIL_HOST }} + MAIL_PORT: ${{ secrets.MAIL_PORT }} + MAIL_USERNAME: ${{ secrets.MAIL_USERNAME }} + MAIL_PASSWORD: ${{ secrets.MAIL_PASSWORD }} + MAIL_FROM: ${{ secrets.MAIL_FROM }} run: | # Docker Compose treats $ as variable interpolation in .env files. # Escape literal dollar signs (e.g. in passwords) as $$. @@ -54,6 +60,12 @@ jobs: printf 'SWISH_NUMBER=%s\n' "$(escape "$SWISH_NUMBER")" printf 'ADMIN_EMAIL=%s\n' "$(escape "$ADMIN_EMAIL")" printf 'ADMIN_PASSWORD=%s\n' "$(escape "$ADMIN_PASSWORD")" + printf 'APP_PUBLIC_BASE_URL=%s\n' "$(escape "${APP_PUBLIC_BASE_URL:-https://bilhej.se}")" + printf 'MAIL_HOST=%s\n' "$(escape "$MAIL_HOST")" + printf 'MAIL_PORT=%s\n' "$(escape "${MAIL_PORT:-587}")" + printf 'MAIL_USERNAME=%s\n' "$(escape "$MAIL_USERNAME")" + printf 'MAIL_PASSWORD=%s\n' "$(escape "$MAIL_PASSWORD")" + printf 'MAIL_FROM=%s\n' "$(escape "${MAIL_FROM:-noreply@bilhej.se}")" } > .env - name: Build and start production stack diff --git a/AGENTS.md b/AGENTS.md index 85eeb07..b433aea 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -188,7 +188,7 @@ entity must NOT have an address field. The address lookup and mailing are external/human processes in Phase 0. ### Local email (Mailpit) -`docker compose up` includes Mailpit (`ghcr.io/axllent/mailpit:v1.28`); password-reset mail appears at http://localhost:8025. E2E verifies SMTP via Mailpit API (`frontend/e2e/helpers/mailpit.ts`). Production uses transactional SMTP (Resend/Brevo)—see README. +`docker compose up` includes Mailpit (`ghcr.io/axllent/mailpit:v1.28`); password-reset mail appears at http://localhost:8025. E2E verifies SMTP via Mailpit API (`frontend/e2e/helpers/mailpit.ts`). Production uses Resend SMTP—see docs/production-email-checklist.md. ### Password reset test token (never in production) `app.password-reset.expose-token` must stay **false** in prod/default; it is only enabled in `application-docker.yml` for CI E2E so Playwright can read `testToken` from the forgot-password response. diff --git a/README.md b/README.md index 9e299c4..07450cd 100644 --- a/README.md +++ b/README.md @@ -202,23 +202,24 @@ docker pull ghcr.io/axllent/mailpit:v1.28 To disable Mailpit and log links only, remove `MAIL_HOST` from the backend service in `docker-compose.yml` or set `MAIL_HOST=` in `.env`. -**Production (transactional provider):** Use [Resend](https://resend.com) or -[Brevo](https://www.brevo.com)—not a self-hosted mail server on the VPS. +**Production:** [Resend](https://resend.com) via SMTP (no Resend Java SDK required). See +[docs/production-email-checklist.md](docs/production-email-checklist.md). -1. Sign up and add domain **bilhej.se** -2. Add the provider’s **SPF** and **DKIM** DNS records at your registrar (no MX needed for send-only) -3. Create SMTP credentials in the provider dashboard -4. On the production server `.env` (or Forgejo deploy secrets): +1. Verify domain **bilhej.se** in Resend (SPF + DKIM DNS records) +2. Create an API key (`re_...`) +3. On the production server `.env`: ```bash APP_PUBLIC_BASE_URL=https://bilhej.se -MAIL_HOST=smtp.resend.com # example; use your provider’s host +MAIL_HOST=smtp.resend.com MAIL_PORT=587 -MAIL_USERNAME=resend # example +MAIL_USERNAME=resend MAIL_PASSWORD=re_xxxxxxxx MAIL_FROM=noreply@bilhej.se ``` +`MAIL_USERNAME` is always the literal string `resend`; `MAIL_PASSWORD` is the API key. + 5. Deploy via **Deploy to Production**, then test forgot-password on https://bilhej.se See [docs/production-email-checklist.md](docs/production-email-checklist.md) for a step-by-step operator checklist. @@ -335,6 +336,12 @@ Before the first deploy, complete these steps on the production server (`srvr.nu | `SWISH_NUMBER` | Swish phone number for payment instructions | | `ADMIN_EMAIL` | Production admin email (e.g. `admin@bilhej.se`) | | `ADMIN_PASSWORD` | Strong unique admin password (password manager) | + | `APP_PUBLIC_BASE_URL` | `https://bilhej.se` (password-reset links) | + | `MAIL_HOST` | `smtp.resend.com` | + | `MAIL_PORT` | `587` | + | `MAIL_USERNAME` | `resend` (literal string) | + | `MAIL_PASSWORD` | Resend API key (`re_...`; rotate if ever exposed) | + | `MAIL_FROM` | `noreply@bilhej.se` (must be on verified domain) | Passwords may contain `$` — the deploy workflow escapes these for Docker Compose. Production does **not** seed `test@bilhej.se` or demo orders. On first start, the diff --git a/backend/src/main/resources/application-prod.yml b/backend/src/main/resources/application-prod.yml index 48edfcd..6d5c219 100644 --- a/backend/src/main/resources/application-prod.yml +++ b/backend/src/main/resources/application-prod.yml @@ -8,3 +8,5 @@ app: admin: email: ${ADMIN_EMAIL} password: ${ADMIN_PASSWORD} + password-reset: + expose-token: false diff --git a/docs/production-email-checklist.md b/docs/production-email-checklist.md index e593785..36dddb3 100644 --- a/docs/production-email-checklist.md +++ b/docs/production-email-checklist.md @@ -1,58 +1,56 @@ -# Production email checklist (operator) +# Production email with Resend (operator) -Complete these steps on the server / Forgejo—nothing in this file is applied automatically. +BilHej sends password-reset mail via **SMTP** (Spring `JavaMailSender`). You do **not** need the +Resend Java SDK from their onboarding snippet—only env vars on the server. -## Prerequisites +## Security -- Domain **bilhej.se** DNS managed at your registrar -- BilHej deployed via Forgejo **Deploy to Production** +- Never commit `re_...` keys to git. Put them only in the server `.env`. +- If an API key was pasted in chat or logs, **revoke it** in Resend → API Keys and create a new one. -## 1. Choose a transactional provider +## 1. Verify bilhej.se in Resend -Recommended: [Resend](https://resend.com) or [Brevo](https://www.brevo.com) (EU, free tier). +1. [Resend](https://resend.com) → **Domains** → add `bilhej.se` +2. Add the DNS records Resend shows (SPF, DKIM; DMARC optional) at your domain registrar +3. Wait until status is **Verified** -## 2. Verify the domain +Until the domain is verified, `MAIL_FROM=noreply@bilhej.se` will fail. For a quick API test only, +Resend allows `onboarding@resend.dev` → your own inbox—not for production. -In the provider dashboard, add **bilhej.se** and publish the DNS records they give you: +## 2. Production `.env` (SMTP, not SDK) -- **SPF** (TXT) -- **DKIM** (CNAME or TXT) -- **DMARC** (TXT, optional but recommended) - -You do **not** need MX records if the app only sends mail (forgot-password). - -Wait until the provider shows the domain as verified. - -## 3. Create SMTP credentials - -Copy from the provider: - -- SMTP host (e.g. `smtp.resend.com`) -- Port (`587`) -- Username / password or API key used as password - -## 4. Update production `.env` - -On the server (same file used by `docker-compose.prod.yml`): +On the server (file used by `docker-compose.prod.yml`): ```bash APP_PUBLIC_BASE_URL=https://bilhej.se -MAIL_HOST= +MAIL_HOST=smtp.resend.com MAIL_PORT=587 -MAIL_USERNAME= -MAIL_PASSWORD= +MAIL_USERNAME=resend +MAIL_PASSWORD=re_your_new_api_key_here MAIL_FROM=noreply@bilhej.se ``` -## 5. Deploy +| Variable | Resend value | +|----------|----------------| +| `MAIL_USERNAME` | Always the literal string `resend` | +| `MAIL_PASSWORD` | Your API key (`re_...`) | +| `MAIL_FROM` | Any address on **verified** domain, e.g. `noreply@bilhej.se` | -Run **Deploy to Production** in Forgejo (do not rsync or manual compose on the server). +## 3. Deploy -## 6. Smoke test +Run **Deploy to Production** in Forgejo (pipeline only—no manual rsync). -1. Open https://bilhej.se/logga-in → **Glömt lösenord?** -2. Enter an email that exists in `users` -3. Check the inbox (and spam) for the reset message -4. If nothing arrives: `docker logs bilhej-backend-prod 2>&1 | grep -i mail` +## 4. Smoke test -Fallback without SMTP: reset links still appear in backend logs (`Password reset link for`). +1. https://bilhej.se/logga-in → **Glömt lösenord?** +2. Email that exists in `users` +3. Check inbox and spam +4. Resend dashboard → **Emails** should show the send +5. On failure: `docker logs bilhej-backend-prod 2>&1 | grep -i mail` + +Fallback: reset links still log when `MAIL_HOST` is empty. + +## Local dev + +Keep using Mailpit (`docker compose up`, http://localhost:8025). Do not point local Docker at +Resend unless you intend to send real mail.