Wire production email secrets through Forgejo deploy.
Deploy workflow now writes MAIL_* and APP_PUBLIC_BASE_URL from Actions secrets into the server .env so Resend SMTP works after domain verify. Document Resend-only setup, Forgejo secret names, and prod expose-token off. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
86fb946e33
commit
ad195fd890
6 changed files with 70 additions and 51 deletions
|
|
@ -32,11 +32,11 @@ APP_PUBLIC_BASE_URL=http://localhost:3000
|
||||||
# ---------- SMTP (local Docker uses Mailpit via docker-compose.yml) ----------
|
# ---------- SMTP (local Docker uses Mailpit via docker-compose.yml) ----------
|
||||||
# docker compose up → view mail at http://localhost:8025
|
# docker compose up → view mail at http://localhost:8025
|
||||||
# Leave MAIL_HOST unset in .env to use compose defaults (mailpit).
|
# 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_HOST=smtp.resend.com
|
||||||
# MAIL_PORT=587
|
# MAIL_PORT=587
|
||||||
# MAIL_USERNAME=
|
# MAIL_USERNAME=resend
|
||||||
# MAIL_PASSWORD=
|
# MAIL_PASSWORD=re_... # API key; never commit a real value
|
||||||
# MAIL_FROM=noreply@bilhej.se
|
# MAIL_FROM=noreply@bilhej.se
|
||||||
|
|
||||||
# ---------- Production admin (prod profile only) ----------
|
# ---------- Production admin (prod profile only) ----------
|
||||||
|
|
|
||||||
|
|
@ -39,6 +39,12 @@ jobs:
|
||||||
SWISH_NUMBER: ${{ secrets.SWISH_NUMBER }}
|
SWISH_NUMBER: ${{ secrets.SWISH_NUMBER }}
|
||||||
ADMIN_EMAIL: ${{ secrets.ADMIN_EMAIL }}
|
ADMIN_EMAIL: ${{ secrets.ADMIN_EMAIL }}
|
||||||
ADMIN_PASSWORD: ${{ secrets.ADMIN_PASSWORD }}
|
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: |
|
run: |
|
||||||
# Docker Compose treats $ as variable interpolation in .env files.
|
# Docker Compose treats $ as variable interpolation in .env files.
|
||||||
# Escape literal dollar signs (e.g. in passwords) as $$.
|
# Escape literal dollar signs (e.g. in passwords) as $$.
|
||||||
|
|
@ -54,6 +60,12 @@ jobs:
|
||||||
printf 'SWISH_NUMBER=%s\n' "$(escape "$SWISH_NUMBER")"
|
printf 'SWISH_NUMBER=%s\n' "$(escape "$SWISH_NUMBER")"
|
||||||
printf 'ADMIN_EMAIL=%s\n' "$(escape "$ADMIN_EMAIL")"
|
printf 'ADMIN_EMAIL=%s\n' "$(escape "$ADMIN_EMAIL")"
|
||||||
printf 'ADMIN_PASSWORD=%s\n' "$(escape "$ADMIN_PASSWORD")"
|
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
|
} > .env
|
||||||
|
|
||||||
- name: Build and start production stack
|
- name: Build and start production stack
|
||||||
|
|
|
||||||
|
|
@ -188,7 +188,7 @@ entity must NOT have an address field. The address lookup and mailing are
|
||||||
external/human processes in Phase 0.
|
external/human processes in Phase 0.
|
||||||
|
|
||||||
### Local email (Mailpit)
|
### 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)
|
### 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.
|
`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.
|
||||||
|
|
|
||||||
23
README.md
23
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
|
To disable Mailpit and log links only, remove `MAIL_HOST` from the backend service in
|
||||||
`docker-compose.yml` or set `MAIL_HOST=` in `.env`.
|
`docker-compose.yml` or set `MAIL_HOST=` in `.env`.
|
||||||
|
|
||||||
**Production (transactional provider):** Use [Resend](https://resend.com) or
|
**Production:** [Resend](https://resend.com) via SMTP (no Resend Java SDK required). See
|
||||||
[Brevo](https://www.brevo.com)—not a self-hosted mail server on the VPS.
|
[docs/production-email-checklist.md](docs/production-email-checklist.md).
|
||||||
|
|
||||||
1. Sign up and add domain **bilhej.se**
|
1. Verify domain **bilhej.se** in Resend (SPF + DKIM DNS records)
|
||||||
2. Add the provider’s **SPF** and **DKIM** DNS records at your registrar (no MX needed for send-only)
|
2. Create an API key (`re_...`)
|
||||||
3. Create SMTP credentials in the provider dashboard
|
3. On the production server `.env`:
|
||||||
4. On the production server `.env` (or Forgejo deploy secrets):
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
APP_PUBLIC_BASE_URL=https://bilhej.se
|
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_PORT=587
|
||||||
MAIL_USERNAME=resend # example
|
MAIL_USERNAME=resend
|
||||||
MAIL_PASSWORD=re_xxxxxxxx
|
MAIL_PASSWORD=re_xxxxxxxx
|
||||||
MAIL_FROM=noreply@bilhej.se
|
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
|
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.
|
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 |
|
| `SWISH_NUMBER` | Swish phone number for payment instructions |
|
||||||
| `ADMIN_EMAIL` | Production admin email (e.g. `admin@bilhej.se`) |
|
| `ADMIN_EMAIL` | Production admin email (e.g. `admin@bilhej.se`) |
|
||||||
| `ADMIN_PASSWORD` | Strong unique admin password (password manager) |
|
| `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.
|
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
|
Production does **not** seed `test@bilhej.se` or demo orders. On first start, the
|
||||||
|
|
|
||||||
|
|
@ -8,3 +8,5 @@ app:
|
||||||
admin:
|
admin:
|
||||||
email: ${ADMIN_EMAIL}
|
email: ${ADMIN_EMAIL}
|
||||||
password: ${ADMIN_PASSWORD}
|
password: ${ADMIN_PASSWORD}
|
||||||
|
password-reset:
|
||||||
|
expose-token: false
|
||||||
|
|
|
||||||
|
|
@ -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
|
- Never commit `re_...` keys to git. Put them only in the server `.env`.
|
||||||
- BilHej deployed via Forgejo **Deploy to Production**
|
- 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)
|
On the server (file used by `docker-compose.prod.yml`):
|
||||||
- **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`):
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
APP_PUBLIC_BASE_URL=https://bilhej.se
|
APP_PUBLIC_BASE_URL=https://bilhej.se
|
||||||
MAIL_HOST=<provider-smtp-host>
|
MAIL_HOST=smtp.resend.com
|
||||||
MAIL_PORT=587
|
MAIL_PORT=587
|
||||||
MAIL_USERNAME=<from-provider>
|
MAIL_USERNAME=resend
|
||||||
MAIL_PASSWORD=<from-provider>
|
MAIL_PASSWORD=re_your_new_api_key_here
|
||||||
MAIL_FROM=noreply@bilhej.se
|
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?**
|
## 4. Smoke test
|
||||||
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`
|
|
||||||
|
|
||||||
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.
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue