diff --git a/apps/web/src/env.js b/apps/web/src/env.js index 349e692..758019b 100644 --- a/apps/web/src/env.js +++ b/apps/web/src/env.js @@ -13,7 +13,7 @@ export const env = createEnv({ .url() .refine( (str) => !str.includes("YOUR_MYSQL_URL_HERE"), - "You forgot to change the default URL" + "You forgot to change the default URL", ), NODE_ENV: z .enum(["development", "test", "production"]) @@ -27,7 +27,7 @@ export const env = createEnv({ // Since NextAuth.js automatically uses the VERCEL_URL if present. (str) => process.env.VERCEL_URL ?? str, // VERCEL_URL doesn't include `https` so it cant be validated as a URL - process.env.VERCEL ? z.string() : z.string().url() + process.env.VERCEL ? z.string() : z.string().url(), ), GITHUB_ID: z.string().optional(), GITHUB_SECRET: z.string().optional(), @@ -65,6 +65,7 @@ export const env = createEnv({ STRIPE_WEBHOOK_SECRET: z.string().optional(), SMTP_HOST: z.string().default("smtp.usesend.com"), SMTP_USER: z.string().default("usesend"), + CONTACT_BOOK_ID: z.string().optional(), }, /** @@ -120,6 +121,7 @@ export const env = createEnv({ STRIPE_WEBHOOK_SECRET: process.env.STRIPE_WEBHOOK_SECRET, SMTP_HOST: process.env.SMTP_HOST, SMTP_USER: process.env.SMTP_USER, + CONTACT_BOOK_ID: process.env.CONTACT_BOOK_ID, }, /** * Run `build` or `dev` with `SKIP_ENV_VALIDATION` to skip env validation. This is especially diff --git a/apps/web/src/server/api/routers/admin.ts b/apps/web/src/server/api/routers/admin.ts index a58c45e..811a1bd 100644 --- a/apps/web/src/server/api/routers/admin.ts +++ b/apps/web/src/server/api/routers/admin.ts @@ -8,6 +8,8 @@ import { getAccount } from "~/server/aws/ses"; import { db } from "~/server/db"; import { sendMail } from "~/server/mailer"; import { logger } from "~/server/logger/log"; +import { UseSend } from "usesend-js"; +import { isCloud } from "~/utils/common"; const waitlistUserSelection = { id: true, @@ -79,7 +81,7 @@ export const adminRouter = createTRPCRouter({ .input( z.object({ region: z.string(), - }) + }), ) .query(async ({ input }) => { const acc = await getAccount(input.region); @@ -93,7 +95,7 @@ export const adminRouter = createTRPCRouter({ usesendUrl: z.string().url(), sendRate: z.number(), transactionalQuota: z.number(), - }) + }), ) .mutation(async ({ input }) => { return SesSettingsService.createSesSetting({ @@ -110,7 +112,7 @@ export const adminRouter = createTRPCRouter({ settingsId: z.string(), sendRate: z.number(), transactionalQuota: z.number(), - }) + }), ) .mutation(async ({ input }) => { return SesSettingsService.updateSesSetting({ @@ -124,11 +126,11 @@ export const adminRouter = createTRPCRouter({ .input( z.object({ region: z.string().optional().nullable(), - }) + }), ) .query(async ({ input }) => { return SesSettingsService.getSetting( - input.region ?? env.AWS_DEFAULT_REGION + input.region ?? env.AWS_DEFAULT_REGION, ); }), @@ -139,7 +141,7 @@ export const adminRouter = createTRPCRouter({ .string() .email() .transform((value) => value.toLowerCase()), - }) + }), ) .mutation(async ({ input }) => { const user = await db.user.findUnique({ @@ -155,7 +157,7 @@ export const adminRouter = createTRPCRouter({ z.object({ userId: z.number(), isWaitlisted: z.boolean(), - }) + }), ) .mutation(async ({ input }) => { const existingUser = await db.user.findUnique({ @@ -182,6 +184,56 @@ export const adminRouter = createTRPCRouter({ Boolean(updatedUser.email) && (founderEmail || fallbackFrom); + // Add user to contact book when removed from waitlist (cloud only) + if ( + existingUser.isWaitlisted && + !input.isWaitlisted && + isCloud() && + env.CONTACT_BOOK_ID && + updatedUser.email + ) { + try { + const client = new UseSend(env.USESEND_API_KEY); + + // Split name into first and last name if available + const firstName = updatedUser.name || ""; + + const result = await client.contacts.create(env.CONTACT_BOOK_ID, { + email: updatedUser.email, + firstName: firstName, + }); + + if (result.error) { + logger.error( + { + userId: updatedUser.id, + email: updatedUser.email, + error: result.error, + }, + "Failed to add user to contact book", + ); + } else { + logger.info( + { + userId: updatedUser.id, + email: updatedUser.email, + contactId: result.data?.contactId, + }, + "Successfully added user to contact book", + ); + } + } catch (error) { + logger.error( + { + userId: updatedUser.id, + email: updatedUser.email, + error, + }, + "Error adding user to contact book", + ); + } + } + if (shouldSendAcceptanceEmail) { const recipient = updatedUser.email as string; const replyTo = founderEmail ?? fallbackFrom; @@ -201,12 +253,12 @@ export const adminRouter = createTRPCRouter({ text, toPlainHtml(text), replyTo, - fromOverride + fromOverride, ); } catch (error) { logger.error( { userId: updatedUser.id, error }, - "Failed to send waitlist acceptance email" + "Failed to send waitlist acceptance email", ); } } @@ -218,7 +270,7 @@ export const adminRouter = createTRPCRouter({ .input( z.object({ userId: z.number(), - }) + }), ) .mutation(async ({ input }) => { const user = await db.user.findUnique({ @@ -262,12 +314,12 @@ export const adminRouter = createTRPCRouter({ text, toPlainHtml(text), replyTo, - fromOverride + fromOverride, ); } catch (error) { logger.error( { userId: user.id, error }, - "Failed to send waitlist rejection email" + "Failed to send waitlist rejection email", ); throw new Error("Failed to send waitlist rejection email"); } @@ -282,7 +334,7 @@ export const adminRouter = createTRPCRouter({ .string({ required_error: "Search query is required" }) .trim() .min(1, "Search query is required"), - }) + }), ) .mutation(async ({ input }) => { const query = input.query.trim(); @@ -338,7 +390,7 @@ export const adminRouter = createTRPCRouter({ dailyEmailLimit: z.number().int().min(0).max(10_000_000), isBlocked: z.boolean(), plan: z.enum(["FREE", "BASIC"]), - }) + }), ) .mutation(async ({ input }) => { const { teamId, ...data } = input; @@ -357,7 +409,7 @@ export const adminRouter = createTRPCRouter({ z.object({ timeframe: z.enum(["today", "thisMonth"]), paidOnly: z.boolean().optional(), - }) + }), ) .query(async ({ input }) => { const timeframe = input.timeframe; @@ -366,7 +418,7 @@ export const adminRouter = createTRPCRouter({ const now = new Date(); const today = now.toISOString().slice(0, 10); const monthStartDate = new Date( - Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), 1) + Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), 1), ); const monthStart = monthStartDate.toISOString().slice(0, 10); @@ -427,7 +479,7 @@ export const adminRouter = createTRPCRouter({ bounced: 0, complained: 0, hardBounced: 0, - } + }, ); return {