api: dispatch emails using resend
Parents:
fe72d2d6 file(s) changed
- api/package-lock.json +60 -1
- api/package.json +2 -1
- api/src/lib/mail.ts +12 -0
- api/src/lib/queue/processors/mail.ts +7 -0
- api/src/services/mail/index.ts +1 -0
- api/src/services/mail/welcome.ts +31 -0
api/package-lock.json
@@ -23,7 +23,8 @@ "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",
@@ -1126,6 +1127,12 @@ "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",
@@ -4185,6 +4192,12 @@ "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",
@@ -6376,6 +6389,12 @@ "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",
@@ -6865,6 +6884,27 @@ "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",
@@ -7341,6 +7381,16 @@ "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",
@@ -7571,6 +7621,15 @@ "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": {
api/package.json
@@ -33,7 +33,8 @@ "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",
api/src/lib/mail.ts
@@ -0,0 +1,12 @@
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
@@ -5,6 +5,10 @@ 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) {
@@ -13,6 +17,9 @@ 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
api/src/services/mail/index.ts
@@ -0,0 +1,1 @@
1 + export { sendWelcomeEmail } from '@app/services/mail/welcome';
api/src/services/mail/welcome.ts
@@ -0,0 +1,31 @@
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 + }