api: dispatch emails using resend
Pedro Lucas Porcellis porcellis@eletrotupi.com · 5 months ago
ffd33a29c8ba15703f785399b5d0352f50c16d73 Browse filesParents
fe72d2dChanged files
6
Additions
+113
Deletions
-2
api/package-lock.json
1 1
{
2 2
"name": "orbit-api",
3 3
"version": "0.0.0",
4 4
"lockfileVersion": 3,
5 5
"requires": true,
21 21
"cors": "^2.8.6",
22 22
"date-fns": "^4.1.0",
23 23
"express": "^5.2.1",
24 24
"jsonwebtoken": "^9.0.3",
25 25
"morgan": "^1.10.1",
26 -
"pg": "^8.20.0"
26 +
"pg": "^8.20.0",
27 +
"resend": "^6.12.3"
27 28
},
28 29
"devDependencies": {
29 30
"@ava/typescript": "^6.0.0",
30 31
"@types/better-sqlite3": "^7.6.13",
31 32
"@types/express": "^5.0.6",
1124 1125
},
1125 1126
"funding": {
1126 1127
"url": "https://github.com/sponsors/sindresorhus"
1127 1128
}
1128 1129
},
1130 +
"node_modules/@stablelib/base64": {
1131 +
"version": "1.0.1",
1132 +
"resolved": "https://registry.npmjs.org/@stablelib/base64/-/base64-1.0.1.tgz",
1133 +
"integrity": "sha512-1bnPQqSxSuc3Ii6MhBysoWCg58j97aUjuCSZrGSmDxNqtytIi0k8utUenAwTZN4V5mXXYGsVUI9zeBqy+jBOSQ==",
1134 +
"license": "MIT"
1135 +
},
1129 1136
"node_modules/@standard-schema/spec": {
1130 1137
"version": "1.1.0",
1131 1138
"resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz",
1132 1139
"integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==",
1133 1140
"devOptional": true,
4183 4190
"resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz",
4184 4191
"integrity": "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==",
4185 4192
"dev": true,
4186 4193
"license": "MIT"
4187 4194
},
4195 +
"node_modules/fast-sha256": {
4196 +
"version": "1.3.0",
4197 +
"resolved": "https://registry.npmjs.org/fast-sha256/-/fast-sha256-1.3.0.tgz",
4198 +
"integrity": "sha512-n11RGP/lrWEFI/bWdygLxhI+pVeo1ZYIVwvvPkW7azl/rOy+F3HYRZ2K5zeE9mmkhQppyv9sQFx0JM9UabnpPQ==",
4199 +
"license": "Unlicense"
4200 +
},
4188 4201
"node_modules/fastq": {
4189 4202
"version": "1.19.1",
4190 4203
"resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz",
4191 4204
"integrity": "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==",
4192 4205
"dev": true,
6374 6387
},
6375 6388
"funding": {
6376 6389
"url": "https://github.com/sponsors/sindresorhus"
6377 6390
}
6378 6391
},
6392 +
"node_modules/postal-mime": {
6393 +
"version": "2.7.4",
6394 +
"resolved": "https://registry.npmjs.org/postal-mime/-/postal-mime-2.7.4.tgz",
6395 +
"integrity": "sha512-0WdnFQYUrPGGTFu1uOqD2s7omwua8xaeYGdO6rb88oD5yJ/4pPHDA4sdWqfD8wQVfCny563n/HQS7zTFft+f/g==",
6396 +
"license": "MIT-0"
6397 +
},
6379 6398
"node_modules/postgres": {
6380 6399
"version": "3.4.7",
6381 6400
"resolved": "https://registry.npmjs.org/postgres/-/postgres-3.4.7.tgz",
6382 6401
"integrity": "sha512-Jtc2612XINuBjIl/QTWsV5UvE8UHuNblcO3vVADSrKsrc6RqGX6lOW1cEo3CM2v0XG4Nat8nI+YM7/f26VxXLw==",
6383 6402
"devOptional": true,
6863 6882
"license": "MIT",
6864 6883
"engines": {
6865 6884
"node": ">=0.10.0"
6866 6885
}
6867 6886
},
6887 +
"node_modules/resend": {
6888 +
"version": "6.12.3",
6889 +
"resolved": "https://registry.npmjs.org/resend/-/resend-6.12.3.tgz",
6890 +
"integrity": "sha512-FkEi6YPnVL96/LvH8+QP7NaeaBy5brYXwlRqUCqZZeNL0/iyKij18IPmyPXYauT/2ODn1JG04qKz+qlJfzqzTw==",
6891 +
"license": "MIT",
6892 +
"dependencies": {
6893 +
"postal-mime": "2.7.4",
6894 +
"svix": "1.92.2"
6895 +
},
6896 +
"engines": {
6897 +
"node": ">=20"
6898 +
},
6899 +
"peerDependencies": {
6900 +
"@react-email/render": "*"
6901 +
},
6902 +
"peerDependenciesMeta": {
6903 +
"@react-email/render": {
6904 +
"optional": true
6905 +
}
6906 +
}
6907 +
},
6868 6908
"node_modules/resolve-cwd": {
6869 6909
"version": "3.0.0",
6870 6910
"resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz",
6871 6911
"integrity": "sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==",
6872 6912
"dev": true,
7339 7379
"version": "2.1.0",
7340 7380
"resolved": "https://registry.npmjs.org/standard-as-callback/-/standard-as-callback-2.1.0.tgz",
7341 7381
"integrity": "sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A==",
7342 7382
"license": "MIT"
7343 7383
},
7384 +
"node_modules/standardwebhooks": {
7385 +
"version": "1.0.0",
7386 +
"resolved": "https://registry.npmjs.org/standardwebhooks/-/standardwebhooks-1.0.0.tgz",
7387 +
"integrity": "sha512-BbHGOQK9olHPMvQNHWul6MYlrRTAOKn03rOe4A8O3CLWhNf4YHBqq2HJKKC+sfqpxiBY52pNeesD6jIiLDz8jg==",
7388 +
"license": "MIT",
7389 +
"dependencies": {
7390 +
"@stablelib/base64": "^1.0.0",
7391 +
"fast-sha256": "^1.3.0"
7392 +
}
7393 +
},
7344 7394
"node_modules/statuses": {
7345 7395
"version": "2.0.2",
7346 7396
"resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz",
7347 7397
"integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==",
7348 7398
"license": "MIT",
7569 7619
"integrity": "sha512-KKNVtd6pCYgPIKU4cp2733HWYCpplQhddZLBUryaAHou723x+FRzQ5Df824Fj+IyyuiQTRoub4SnIFfIcrp70g==",
7570 7620
"dev": true,
7571 7621
"license": "MIT",
7572 7622
"engines": {
7573 7623
"node": ">=0.8.0"
7624 +
}
7625 +
},
7626 +
"node_modules/svix": {
7627 +
"version": "1.92.2",
7628 +
"resolved": "https://registry.npmjs.org/svix/-/svix-1.92.2.tgz",
7629 +
"integrity": "sha512-ZmuA3UVvlnF9EgxlzmPtF7CKjQb64Z6OFlyfdDfU0sdcC7dJa+3aOYX5B9mA+RS6ch1AxBa4UP/l6KmqfGtWBQ==",
7630 +
"license": "MIT",
7631 +
"dependencies": {
7632 +
"standardwebhooks": "1.0.0"
7574 7633
}
7575 7634
},
7576 7635
"node_modules/tar": {
7577 7636
"version": "7.5.2",
7578 7637
"resolved": "https://registry.npmjs.org/tar/-/tar-7.5.2.tgz",
8385 8444
"graphmatch": "^1.1.0"
8386 8445
}
8387 8446
}
8388 8447
}
8389 8448
}
api/package.json
1 1
{
2 2
"name": "orbit-api",
3 3
"version": "0.0.0",
4 4
"description": "",
5 5
"license": "ISC",
31 31
"cors": "^2.8.6",
32 32
"date-fns": "^4.1.0",
33 33
"express": "^5.2.1",
34 34
"jsonwebtoken": "^9.0.3",
35 35
"morgan": "^1.10.1",
36 -
"pg": "^8.20.0"
36 +
"pg": "^8.20.0",
37 +
"resend": "^6.12.3"
37 38
},
38 39
"devDependencies": {
39 40
"@ava/typescript": "^6.0.0",
40 41
"@types/better-sqlite3": "^7.6.13",
41 42
"@types/express": "^5.0.6",
52 53
"tsconfig-paths": "^4.2.0",
53 54
"tsx": "^4.21.0",
54 55
"typescript": "^6.0.2"
55 56
}
56 57
}
/dev/null → api/src/lib/mail.ts
1 +
import { Resend } from 'resend';
2 +
3 +
// Initialized once when the module is first imported
4 +
export const resend = new Resend(process.env.RESEND_API_KEY);
5 +
6 +
// XXX: Bleh, resend doesn't expose their types so w/e
7 +
export async function sendEmail(opts: any): Promise<any> {
8 +
return await resend.emails.send({
9 +
from: process.env.RESEND_FROM ?? 'naoresponda@updates.cabotia.dev',
10 +
...opts,
11 +
});
12 +
}
api/src/lib/queue/processors/mail.ts
1 1
import { Job } from 'bullmq';
2 2
import {
3 3
MailJobName, MailJobData,
4 4
WelcomeEmailPayload,
5 5
PasswordResetPayload,
6 6
} from '@app/lib/queue/types';
7 7
8 +
import {
9 +
sendWelcomeEmail
10 +
} from '@app/services/mail'
11 +
8 12
export async function mailProcessor(job: Job<MailJobData['data']>): Promise<void> {
9 13
// job.name is the discriminant
10 14
switch (job.name as MailJobName) {
11 15
case MailJobName.WelcomeEmail: {
12 16
const data = job.data as WelcomeEmailPayload;
13 17
// await sendWelcomeEmail(data.userId);
14 18
15 19
console.log("Dispatching an welcome email", data);
20 +
21 +
sendWelcomeEmail(Number(data.userId));
22 +
16 23
break;
17 24
}
18 25
19 26
case MailJobName.PasswordReset: {
20 27
const data = job.data as PasswordResetPayload;
24 31
25 32
default:
26 33
throw new Error(`Unknown mail job: ${job.name}`);
27 34
}
28 35
}
/dev/null → api/src/services/mail/index.ts
1 +
export { sendWelcomeEmail } from '@app/services/mail/welcome';
/dev/null → api/src/services/mail/welcome.ts
1 +
import { findUserById } from '@app/models/user';
2 +
import { sendEmail } from '@app/lib/mail';
3 +
4 +
const email = (firstName: string) => {
5 +
return (`
6 +
Oi! Seja bem vindo, ${firstName}.
7 +
8 +
Você criou sua conta com sucesso no orbit!
9 +
10 +
Qualquer coisa prende o grito!
11 +
`)
12 +
}
13 +
14 +
export async function sendWelcomeEmail(userId: number): Promise<void> {
15 +
const user = await findUserById(userId)!;
16 +
17 +
console.log("Email/welcome: ", user);
18 +
19 +
const { data, error } = await sendEmail({
20 +
to: user!.email,
21 +
subject: "Bem vindo ao orbit!",
22 +
text: email(user!.firstName)
23 +
});
24 +
25 +
if (error) {
26 +
console.error("Error sending email: ", error);
27 +
}
28 +
29 +
console.log("Email sent successfully!");
30 +
console.log("Email ID:", data?.id);
31 +
}