145 lines
8.0 KiB
Markdown
145 lines
8.0 KiB
Markdown
# Email Backend
|
|
|
|
## Purpose
|
|
|
|
The transactional email subsystem sends the three account lifecycle messages the
|
|
backend produces: email-address verification, password reset, and invitation. It owns
|
|
the SMTP transport (`EmailSender`) and the per-message classes that render an HTML body
|
|
from a template file. It is infrastructure, not an HTTP slice: nothing here defines
|
|
routes; the auth and users services call into it.
|
|
|
|
## Files
|
|
|
|
- `src/services/email/index.ts` — `EmailSender` (default export): the transport wrapper
|
|
and `isConfigured` guard.
|
|
- `src/services/email/list/addressVerification.ts` — `EmailAddressVerificationEmail`.
|
|
- `src/services/email/list/passwordReset.ts` — `PasswordResetEmail`.
|
|
- `src/services/email/list/invitation.ts` — `InvitationEmail`.
|
|
- `src/services/email/htmlTemplates/addressVerification/emailAddressVerification.html`
|
|
- `src/services/email/htmlTemplates/passwordReset/passwordResetEmail.html`
|
|
- `src/services/email/htmlTemplates/invitation/invitationTemplate.html`
|
|
- `scripts/copy-assets.mjs` — copies the `htmlTemplates/` tree into `dist/` for the
|
|
compiled build.
|
|
|
|
Shared used: `@/shared/config` (the `email` config block), `@/shared/logger`,
|
|
`@/shared/notifications/helpers` (`getNotification`), Node `fs`/`path`, `nodemailer`.
|
|
|
|
## Public Interface
|
|
|
|
`EmailSender` (`src/services/email/index.ts`):
|
|
|
|
- `new EmailSender(email: EmailMessage)` where
|
|
`EmailMessage = { to: string; subject: string; html: () => Promise<string> }`.
|
|
- `send(): Promise<...>` — asserts `email`, `email.to`, `email.subject`, `email.html`
|
|
are present, awaits `email.html()` to build the body, creates a `nodemailer` transport
|
|
from `transportConfig`, and sends a mail with `from`, `to`, `subject`, the HTML body,
|
|
and the header `X-SES-CONFIGURATION-SET: flatlogic-app`. Returns the
|
|
`transporter.sendMail(...)` result.
|
|
- `static get isConfigured(): boolean` — `true` only when both `config.email.auth.pass`
|
|
and `config.email.auth.user` are set.
|
|
- `get transportConfig()` — returns `config.email`.
|
|
- `get from()` — returns `config.email.from`.
|
|
|
|
Each message class takes its recipient plus a link or message-specific options in the constructor, exposes a
|
|
`subject` getter, and an `async html(): Promise<string>` that loads its template and
|
|
substitutes placeholders. They are passed to `new EmailSender(...).send()`:
|
|
|
|
- `EmailAddressVerificationEmail(to: string, link: string)` — subject from
|
|
`getNotification('emails.emailAddressVerification.subject', getNotification('app.title'))`;
|
|
`html()` reads `addressVerification/emailAddressVerification.html` and replaces
|
|
`{appTitle}` with `app.title`, `{signupUrl}` with `link`, `{to}` with `to`.
|
|
- `PasswordResetEmail(to: string, link: string)` — subject from
|
|
`getNotification('emails.passwordReset.subject', getNotification('app.title'))`;
|
|
`html()` reads `passwordReset/passwordResetEmail.html` and replaces `{appTitle}` with
|
|
`app.title`, `{resetUrl}` with `link`, `{accountName}` with `to`.
|
|
- `InvitationEmail(to: string, { loginUrl, temporaryPassword })` — subject from
|
|
`getNotification('emails.invitation.subject', getNotification('app.title'))`;
|
|
`html()` reads `invitation/invitationTemplate.html` and replaces `{appTitle}`,
|
|
`{loginUrl}`, `{email}`, and `{temporaryPassword}`. Values are HTML-escaped before
|
|
substitution.
|
|
|
|
## Behavior / Notes
|
|
|
|
- SMTP configuration is read from `config.email` (`src/shared/config/index.ts`), built
|
|
from environment variables with constant defaults from `src/shared/constants/app.ts`:
|
|
`from` (`EMAIL_FROM`, default `School Chain Manager <app@flatlogic.app>`), `host`
|
|
(`EMAIL_HOST`, default `email-smtp.us-east-1.amazonaws.com`), `port` (`EMAIL_PORT`,
|
|
default `587`), `auth.user` (`EMAIL_USER`, default empty string), `auth.pass`
|
|
(`EMAIL_PASS`, no default), and `tls.rejectUnauthorized: false`. Because `isConfigured`
|
|
requires both `auth.user` and `auth.pass`, email sending is effectively disabled until
|
|
both are provided.
|
|
- `backend/.env` is the checked-in development override file. It carries the non-secret SMTP
|
|
defaults (`EMAIL_FROM`, `EMAIL_HOST`, `EMAIL_PORT`) plus empty `EMAIL_USER`/`EMAIL_PASS` slots.
|
|
Fill those two credentials only in the local/VM environment that should send mail. Do not commit
|
|
production SMTP credentials.
|
|
- Template loading uses `import.meta.dirname` (aliased to `__dirname` in each file) to
|
|
resolve the template path relative to the running module, then `fs.readFile(path, 'utf8')`.
|
|
Substitution is plain global `String.replace` of the `{placeholder}` tokens. On read or
|
|
render failure each class logs via `logger.error` and rethrows.
|
|
- Because `tsc` only emits `.ts` and these templates are read at runtime from a path
|
|
derived from `import.meta`, `scripts/copy-assets.mjs` copies `src/services/email/htmlTemplates`
|
|
into `dist/services/email/htmlTemplates` so the compiled production build can find them.
|
|
|
|
## Used By
|
|
|
|
- `src/services/auth.ts` (`AuthService`):
|
|
- `signup` and the related flow call `sendEmailAddressVerificationEmail(email, host)`
|
|
only when `EmailSender.isConfigured`.
|
|
- `signin` treats the user as `emailVerified` when `EmailSender.isConfigured` is false
|
|
(so unconfigured environments do not block login).
|
|
- `sendEmailAddressVerificationEmail(email, host?)` generates a verification token,
|
|
builds a frontend `/verify-email?token=...` URL, and sends an
|
|
`EmailAddressVerificationEmail`.
|
|
- `sendPasswordResetEmail(email, type = 'register', host?)` generates a password-reset
|
|
token, builds a frontend `/password-reset?token=...` URL, and sends a
|
|
`PasswordResetEmail`.
|
|
- `sendInvitationEmail(email, temporaryPassword, host?)` builds a frontend `/login` URL and
|
|
sends an `InvitationEmail` containing the login URL, account email, and plaintext temporary
|
|
password. The plaintext value is generated by the users service and is not stored.
|
|
- `src/api/controllers/auth.controller.ts` exposes `EmailSender.isConfigured` over HTTP
|
|
(`res.status(200).send(EmailSender.isConfigured)`) and calls the two `AuthService` send
|
|
methods from its request handlers.
|
|
- `src/services/users.ts` (`UsersService`): `create` and `bulkImport` generate temporary
|
|
passwords, store only their bcrypt hashes, mark invited internal accounts verified, and call
|
|
`AuthService.sendInvitationEmail(...)` after commit. Bulk import sends invitations only for
|
|
rows that the repository reports as created. When `EmailSender.isConfigured` is false, single-user
|
|
create responses include the generated `temporaryPassword` once so the creator can copy it from
|
|
the UI and deliver it manually.
|
|
|
|
## Tests
|
|
|
|
- `src/services/users.test.ts` covers temporary-password hashing, invitation delivery, and the
|
|
duplicate/ignored-row guard for bulk import.
|
|
|
|
## Configuration
|
|
|
|
Set these variables in `backend/.env` for local development or in the VM process environment for
|
|
deployment:
|
|
|
|
```bash
|
|
EMAIL_FROM="School Chain Manager <app@flatlogic.app>"
|
|
EMAIL_HOST=email-smtp.us-east-1.amazonaws.com
|
|
EMAIL_PORT=587
|
|
EMAIL_USER=<smtp-user>
|
|
EMAIL_PASS=<smtp-password>
|
|
```
|
|
|
|
`EMAIL_USER` and `EMAIL_PASS` are the activation switch: if either is empty,
|
|
`EmailSender.isConfigured` returns `false`, signup verification emails are skipped, and local sign-in
|
|
treats users as verified so development is not blocked. When both are present, the platform sends
|
|
verification, password-reset, and invitation emails through nodemailer.
|
|
|
|
While email is disabled, `POST /api/users` and `POST /api/users/owner-with-organization` return the
|
|
generated temporary password once in the API response. The frontend displays it in the create form
|
|
with copy instructions. This fallback is only for the internal manual handoff workflow; the password
|
|
is not persisted as plaintext on the backend.
|
|
|
|
For VM deployments, keep SMTP credentials in the VM secret environment (for example
|
|
`~/executor/.env` or the process manager environment) and restart the backend process after changing
|
|
them. The checked-in `backend/.env` should stay limited to non-secret defaults and local placeholders.
|
|
|
|
## Related
|
|
|
|
- `backend-architecture.md` (layering), `permissions.md`, and the auth slice
|
|
(`src/services/auth.ts`, `src/api/controllers/auth.controller.ts`).
|