From fc65f53e0cac59b919b3aa9d15fd8a914b813d80 Mon Sep 17 00:00:00 2001 From: Henry <30578846+hjball@users.noreply.github.com> Date: Mon, 24 Nov 2025 22:20:34 +0000 Subject: [PATCH] feat(cloud): add notification client (#253) --- cloud/docker-compose.yml | 3 +- packages/auth/src/auth.ts | 51 +++++++++++++++++------ packages/email/package.json | 2 +- packages/email/src/cloudMailerClient.tsx | 10 ----- packages/email/src/index.tsx | 2 +- packages/email/src/notificationClient.tsx | 6 +++ packages/email/src/sendEmail.tsx | 10 ----- pnpm-lock.yaml | 20 ++++----- turbo.json | 3 +- 9 files changed, 59 insertions(+), 48 deletions(-) delete mode 100644 packages/email/src/cloudMailerClient.tsx create mode 100644 packages/email/src/notificationClient.tsx diff --git a/cloud/docker-compose.yml b/cloud/docker-compose.yml index d122ffa9..3c685b5c 100644 --- a/cloud/docker-compose.yml +++ b/cloud/docker-compose.yml @@ -35,8 +35,7 @@ services: - SMTP_PASSWORD=${SMTP_PASSWORD} - SMTP_SECURE=${SMTP_SECURE} - EMAIL_FROM=${EMAIL_FROM} - - PLUNK_API_KEY=${PLUNK_API_KEY} - - PLUNK_API_URL=${PLUNK_API_URL} + - NOVU_API_KEY=${NOVU_API_KEY} # S3 storage - S3_ACCESS_KEY_ID=${S3_ACCESS_KEY_ID} diff --git a/packages/auth/src/auth.ts b/packages/auth/src/auth.ts index 32719b9e..aeb5f83d 100644 --- a/packages/auth/src/auth.ts +++ b/packages/auth/src/auth.ts @@ -14,7 +14,7 @@ import * as subscriptionRepo from "@kan/db/repository/subscription.repo"; import * as userRepo from "@kan/db/repository/user.repo"; import * as workspaceRepo from "@kan/db/repository/workspace.repo"; import * as schema from "@kan/db/schema"; -import { cloudMailerClient, sendEmail } from "@kan/email"; +import { notificationClient, sendEmail } from "@kan/email"; import { createStripeClient } from "@kan/stripe"; export const configuredProviders = socialProviderList.reduce< @@ -348,17 +348,7 @@ export const initAuth = (db: dbClient) => { return Promise.resolve(true); }, async after(user) { - if (cloudMailerClient) { - await cloudMailerClient.events.track({ - event: "user-signup", - email: user.email, - data: { - name: user.name, - userId: user.id, - }, - }); - } - + let avatarKey = user.image; if ( user.image && !user.image.includes(process.env.NEXT_PUBLIC_STORAGE_DOMAIN!) @@ -391,6 +381,9 @@ export const initAuth = (db: dbClient) => { ACL: "public-read", }), ); + + avatarKey = key; + await userRepo.update(db, user.id, { image: key, }); @@ -398,6 +391,40 @@ export const initAuth = (db: dbClient) => { console.error(error); } } + + if (notificationClient) { + try { + const [firstName, ...rest] = user.name + .split(" ") + .filter(Boolean); + const lastName = rest.length ? rest.join(" ") : undefined; + const avatarUrl = avatarKey + ? `${env("NEXT_PUBLIC_STORAGE_URL")}/${env("NEXT_PUBLIC_AVATAR_BUCKET_NAME")}/${avatarKey}` + : undefined; + + await notificationClient.trigger({ + to: { + subscriberId: user.id, + firstName: firstName, + lastName: lastName, + email: user.email, + avatar: avatarUrl, + data: { + emailVerified: user.emailVerified, + stripeCustomerId: user.stripeCustomerId, + createdAt: user.createdAt, + updatedAt: user.updatedAt, + }, + }, + workflowId: "user-signup", + }); + } catch (error) { + console.error( + "Error adding user to notification client", + error, + ); + } + } }, }, }, diff --git a/packages/email/package.json b/packages/email/package.json index 4ff80eda..36694949 100644 --- a/packages/email/package.json +++ b/packages/email/package.json @@ -23,7 +23,7 @@ "typecheck": "tsc --noEmit --emitDeclarationOnly false" }, "dependencies": { - "@plunk/node": "^3.0.3", + "@novu/api": "^3.11.0", "@react-email/components": "^0.0.25", "nodemailer": "^7.0.3", "react-email": "^3.0.1" diff --git a/packages/email/src/cloudMailerClient.tsx b/packages/email/src/cloudMailerClient.tsx deleted file mode 100644 index d94abd5d..00000000 --- a/packages/email/src/cloudMailerClient.tsx +++ /dev/null @@ -1,10 +0,0 @@ -import Plunk from "@plunk/node"; - -export const cloudMailerClient = - process.env.NEXT_PUBLIC_KAN_ENV === "cloud" && - process.env.PLUNK_API_KEY && - process.env.PLUNK_API_URL - ? new Plunk(process.env.PLUNK_API_KEY ?? "", { - baseUrl: process.env.PLUNK_API_URL, - }) - : null; diff --git a/packages/email/src/index.tsx b/packages/email/src/index.tsx index a5201f3c..30ea59d3 100644 --- a/packages/email/src/index.tsx +++ b/packages/email/src/index.tsx @@ -1,4 +1,4 @@ export const name = "email"; export { sendEmail } from "./sendEmail"; -export { cloudMailerClient } from "./cloudMailerClient"; +export { notificationClient } from "./notificationClient"; diff --git a/packages/email/src/notificationClient.tsx b/packages/email/src/notificationClient.tsx new file mode 100644 index 00000000..a637b83d --- /dev/null +++ b/packages/email/src/notificationClient.tsx @@ -0,0 +1,6 @@ +import { Novu } from "@novu/api"; + +export const notificationClient = + process.env.NEXT_PUBLIC_KAN_ENV === "cloud" && process.env.NOVU_API_KEY + ? new Novu({ secretKey: process.env.NOVU_API_KEY }) + : null; diff --git a/packages/email/src/sendEmail.tsx b/packages/email/src/sendEmail.tsx index b02b0739..41ed1d23 100644 --- a/packages/email/src/sendEmail.tsx +++ b/packages/email/src/sendEmail.tsx @@ -1,7 +1,6 @@ import { render } from "@react-email/render"; import nodemailer from "nodemailer"; -import { cloudMailerClient } from "./cloudMailerClient"; import JoinWorkspaceTemplate from "./templates/join-workspace"; import MagicLinkTemplate from "./templates/magic-link"; import ResetPasswordTemplate from "./templates/reset-password"; @@ -45,15 +44,6 @@ export const sendEmail = async ( html, }; - // if (cloudMailerClient) { - // const response = await cloudMailerClient.emails.send({ - // ...options, - // body: html, - // }); - - // return response; - // } - const response = await transporter.sendMail(options); if (!response.accepted.length) { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 2c9d2f0c..81cf710e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -440,9 +440,9 @@ importers: packages/email: dependencies: - '@plunk/node': - specifier: ^3.0.3 - version: 3.0.3 + '@novu/api': + specifier: ^3.11.0 + version: 3.11.0 '@react-email/components': specifier: ^0.0.25 version: 0.0.25(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -2683,6 +2683,9 @@ packages: resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} engines: {node: '>= 8'} + '@novu/api@3.11.0': + resolution: {integrity: sha512-8u0mB5VThL7MhdxoN0UoA4CS9eu2k3Xa6iulauMhCHENuJNxUNuirJWq5t8jDoH4bFTQTfMN0VRkC0qodKR2qA==} + '@octokit/auth-token@3.0.4': resolution: {integrity: sha512-TWFX7cZF2LXoCvdmJWY7XVPi74aSY0+FfBZNSXEXFkMpjcqsQwDSYVv5FhRFaI0V1ECnwbz4j59T/G+rXNWaIQ==} engines: {node: '>= 14'} @@ -2762,9 +2765,6 @@ packages: resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} engines: {node: '>=14'} - '@plunk/node@3.0.3': - resolution: {integrity: sha512-f3NT418/EdLjebQ4arAwR64cnz7d5H7i1CeKh8+B0cVBrwzHvF+Q+QkhutF+FAp49yuI/BYc71qQH+RFc2fxxQ==} - '@popperjs/core@2.11.8': resolution: {integrity: sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==} @@ -10226,6 +10226,10 @@ snapshots: '@nodelib/fs.scandir': 2.1.5 fastq: 1.19.1 + '@novu/api@3.11.0': + dependencies: + zod: 3.25.76 + '@octokit/auth-token@3.0.4': {} '@octokit/core@4.2.4': @@ -10345,10 +10349,6 @@ snapshots: '@pkgjs/parseargs@0.11.0': optional: true - '@plunk/node@3.0.3': - dependencies: - tslib: 2.8.1 - '@popperjs/core@2.11.8': {} '@posthog/core@1.0.2': {} diff --git a/turbo.json b/turbo.json index d28bdcde..02c47791 100644 --- a/turbo.json +++ b/turbo.json @@ -120,8 +120,7 @@ "PORT", "BETTER_AUTH_SECRET", "BETTER_AUTH_TRUSTED_ORIGINS", - "PLUNK_API_KEY", - "PLUNK_API_URL" + "NOVU_API_KEY" ], "globalPassThroughEnv": [ "NODE_ENV",