Skip to content

Commit

Permalink
Browse files Browse the repository at this point in the history
  • Loading branch information
jlarsson committed Aug 30, 2023
2 parents ec09cca + 7c6727d commit d77793f
Show file tree
Hide file tree
Showing 8 changed files with 227 additions and 16 deletions.
8 changes: 8 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -51,3 +51,11 @@ FS_DATA_PATH=.local/data
# Passwordless configuration
#
#PASSWORDLESS_TTL=10m

#------------------------------------------------------------
#
# Brevo email notifications (https://www.brevo.com/)
#
#BREVO_API_KEY=abc123
#BREVO_FROM_NAME=Haffa
#[email protected]
1 change: 1 addition & 0 deletions .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,5 +26,6 @@ module.exports = {
'@typescript-eslint/consistent-type-imports': 'warn',
'@typescript-eslint/no-floating-promises': 'error',
'@typescript-eslint/no-use-before-define': 'off',
'@typescript-eslint/no-unused-vars': 'warn',
},
}
27 changes: 17 additions & 10 deletions integrations.md
Original file line number Diff line number Diff line change
@@ -1,16 +1,19 @@
# Integrations

## Persistence layer selection

In runtime, the actual driver is chosen based on environment configuration.

In order of precedence (from top to bottom), the rules are

| Environment | Adverts | Profiles | Files | Login | Notifications |
| ---------------- | ----------- | ----------- | ----------- | ---------- | ------------- |
| MONGOBD_URI | MongoDB | MongoDB | | MongoDB | |
| FS_DATA_PATH | File system | File system | File system | | |
| SENDGRID_API_KEY | | | | | SendGrid |
| \<none> | In memory | In memory | In memory | In memory | console |
| Environment | Adverts | Profiles | Files | Login | Notifications |
| ---------------- | ----------- | ----------- | ----------- | --------- | ------------- |
| MONGOBD_URI | MongoDB | MongoDB | | MongoDB | |
| FS_DATA_PATH | File system | File system | File system | | |
| MINIO_ENDPOINT | | | Minio | | |
| SENDGRID_API_KEY | | | | | SendGrid |
| BREVO_API_KEY | | | | | Brevo |
| \<none> | In memory | In memory | In memory | In memory | console |

## MongoDB as persistence layer

Expand All @@ -19,6 +22,7 @@ This project is tested against MongoDB with the following assumptions:
- `MONGODB_URI` should be set to `mongodb://[host]:[port]/[db]` as in `mongodb://127.0.0.1:27017/haffa`

### Enabling MongoDB

Your environment should contain

```sh
Expand All @@ -28,14 +32,17 @@ MONGODB_URI=mongodb://...
## Local environment with docker

Start a dockerized MongoDB with

```sh
docker run --name mongodb -p 27017:27017 -d mongo

```
Ensure `.env` contains
```env
MONGODB_URI=mongodb://127.0.0.1:27017/haffa
```

Ensure `.env` contains

```env
MONGODB_URI=mongodb://127.0.0.1:27017/haffa
```

### SendGrid notifications

Expand Down
71 changes: 71 additions & 0 deletions src/notifications/brevo/brevo-client.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import request from 'superagent'
import type {
BrevoConfig,
BrevoClient,
RelevantGetTemplatesResult,
Identity,
TemplateName,
SendMailPostRequestBody,
} from './types'

export function createClient(config: BrevoConfig): BrevoClient {
const doGet = async <T>(endpoint: string, data?: string) => {
const { body } = await request
.get(`https://api.brevo.com/v3/${endpoint}`)
.set('api-key', config.apiKey)
.set('Content-Type', 'application/json')
.send(data)
return body as T
}

const doPost = async <T = undefined>(endpoint: string, data: object) => {
const { body } = await request
.post(`https://api.brevo.com/v3/${endpoint}`)
.set('api-key', config.apiKey)
.set('Content-Type', 'application/json')
.send(data)
return body as T
}

const getTemplateId = async (commonName: string) => {
const { templates } = await doGet<RelevantGetTemplatesResult>(
'smtp/templates'
)
const relevantTemplate = templates.find(
template => template.name === commonName
)

if (!relevantTemplate) {
console.error(
`Brevo notifications: no template found for event '${commonName}'`
)
return -1
}

return relevantTemplate.id
}

const send = async (
to: Identity,
templateCommonName: TemplateName,
params: Record<string, string>
) => {
const templateId = await getTemplateId(templateCommonName)

if (templateId === -1) {
return
}

await doPost<SendMailPostRequestBody>('smtp/email', {
sender: config.from,
to: [to],
templateId,
params,
})
}

return {
getTemplateId,
send,
}
}
60 changes: 60 additions & 0 deletions src/notifications/brevo/brevo-notifications.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import { getEnv } from '@helsingborg-stad/gdi-api-node'
import type { NotificationService } from '../types'
import type { Advert } from '../../adverts/types'
import { createClient } from './brevo-client'
import type { BrevoConfig } from './types'

export const createBrevoNotifications = (
config: BrevoConfig
): NotificationService => {
const client = createClient(config)

const stripAdvert = (advert: Advert): Partial<Advert> => ({
...advert,
images: undefined,
claims: undefined,
})

return {
pincodeRequested: (email, pincode) =>
client.send({ name: email, email }, 'pincode-requested', {
email,
pincode,
}),

advertWasReserved: (by, quantity, advert) =>
client.send({ name: by.id, email: by.id }, 'advert-was-reserved', {
quantity,
advert: stripAdvert(advert),
}),

advertReservationWasCancelled: (by, quantity, advert) =>
client.send(
{ name: by.id, email: by.id },
'advert-reservation-was-cancelled',
{
quantity,
advert: stripAdvert(advert),
}
),

advertWasCollected: (by, quantity, advert) =>
client.send({ name: by.id, email: by.id }, 'advert-was-collected', {
quantity,
advert: stripAdvert(advert),
}),
}
}

export const tryCreateBrevoNotificationsFromEnv =
(): NotificationService | null => {
const apiKey = getEnv('BREVO_API_KEY', { fallback: '' })
const fromName = getEnv('BREVO_FROM_NAME', { fallback: '' })
const fromEmail = getEnv('BREVO_FROM_EMAIL', { fallback: '' })
return apiKey && fromName && fromEmail
? createBrevoNotifications({
apiKey,
from: { name: fromName, email: fromEmail },
})
: null
}
12 changes: 12 additions & 0 deletions src/notifications/brevo/readme.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# Brevo notifications

Notifications are sent via the transactional mail service [www.brevo.com](https://www.brevo.com/) when environment contains:

```sh
BREVO_API_KEY=<api key>
BREVO_FROM_NAME=<sender name>
BREVO_FROM_EMAIL=<sender email>
```

Templates must be named `pincode-requested`, `advert-was-reserved` etc. according
to the common name of the notification event. See [types.ts](types.ts) for more details.
49 changes: 49 additions & 0 deletions src/notifications/brevo/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
export type TemplateName =
| 'pincode-requested'
| 'advert-was-reserved'
| 'advert-reservation-was-cancelled'
| 'advert-was-collected'

export interface Identity {
name: string
email: string
}

export interface BrevoConfig {
apiKey: string
from: Identity
}

export interface BrevoClient {
getTemplateId(commonName: TemplateName): Promise<number>
send(
to: Identity,
templateCommonName: TemplateName,
params: Record<string, unknown>
): Promise<void>
}

export interface RelevantGetTemplatesResult {
templates: {
id: number
name: string
}[]
}

export interface SendMailPostRequestBody {
sender: {
name: string
email: string
}

to: [
{
name: string
email: string
}
]

templateId: number

params: Record<string, string>
}
15 changes: 9 additions & 6 deletions src/notifications/index.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
import { tryCreateBrevoNotificationsFromEnv } from './brevo/brevo-notifications'
import { createConsoleNotificationService } from './console-notifications'
import { tryCreateSendGridNofificationsFromEnv } from './sendgrid'
import type { NotificationService } from './types'

export const createNotificationServiceFromEnv = (): NotificationService =>
tryCreateSendGridNofificationsFromEnv() || createConsoleNotificationService()
export const createNotificationServiceFromEnv = (): NotificationService =>
tryCreateSendGridNofificationsFromEnv() ||
tryCreateBrevoNotificationsFromEnv() ||
createConsoleNotificationService()

export const createNullNotificationService = (): NotificationService => ({
pincodeRequested: async () => undefined,
advertWasReserved: async () => undefined,
advertReservationWasCancelled: async () => undefined,
advertWasCollected: async () => undefined
pincodeRequested: async () => undefined,
advertWasReserved: async () => undefined,
advertReservationWasCancelled: async () => undefined,
advertWasCollected: async () => undefined,
})

0 comments on commit d77793f

Please sign in to comment.