add contact to users on waitlist removal (#276)
This commit is contained in:
+4
-2
@@ -13,7 +13,7 @@ export const env = createEnv({
|
|||||||
.url()
|
.url()
|
||||||
.refine(
|
.refine(
|
||||||
(str) => !str.includes("YOUR_MYSQL_URL_HERE"),
|
(str) => !str.includes("YOUR_MYSQL_URL_HERE"),
|
||||||
"You forgot to change the default URL"
|
"You forgot to change the default URL",
|
||||||
),
|
),
|
||||||
NODE_ENV: z
|
NODE_ENV: z
|
||||||
.enum(["development", "test", "production"])
|
.enum(["development", "test", "production"])
|
||||||
@@ -27,7 +27,7 @@ export const env = createEnv({
|
|||||||
// Since NextAuth.js automatically uses the VERCEL_URL if present.
|
// Since NextAuth.js automatically uses the VERCEL_URL if present.
|
||||||
(str) => process.env.VERCEL_URL ?? str,
|
(str) => process.env.VERCEL_URL ?? str,
|
||||||
// VERCEL_URL doesn't include `https` so it cant be validated as a URL
|
// 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_ID: z.string().optional(),
|
||||||
GITHUB_SECRET: z.string().optional(),
|
GITHUB_SECRET: z.string().optional(),
|
||||||
@@ -65,6 +65,7 @@ export const env = createEnv({
|
|||||||
STRIPE_WEBHOOK_SECRET: z.string().optional(),
|
STRIPE_WEBHOOK_SECRET: z.string().optional(),
|
||||||
SMTP_HOST: z.string().default("smtp.usesend.com"),
|
SMTP_HOST: z.string().default("smtp.usesend.com"),
|
||||||
SMTP_USER: z.string().default("usesend"),
|
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,
|
STRIPE_WEBHOOK_SECRET: process.env.STRIPE_WEBHOOK_SECRET,
|
||||||
SMTP_HOST: process.env.SMTP_HOST,
|
SMTP_HOST: process.env.SMTP_HOST,
|
||||||
SMTP_USER: process.env.SMTP_USER,
|
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
|
* Run `build` or `dev` with `SKIP_ENV_VALIDATION` to skip env validation. This is especially
|
||||||
|
|||||||
@@ -8,6 +8,8 @@ import { getAccount } from "~/server/aws/ses";
|
|||||||
import { db } from "~/server/db";
|
import { db } from "~/server/db";
|
||||||
import { sendMail } from "~/server/mailer";
|
import { sendMail } from "~/server/mailer";
|
||||||
import { logger } from "~/server/logger/log";
|
import { logger } from "~/server/logger/log";
|
||||||
|
import { UseSend } from "usesend-js";
|
||||||
|
import { isCloud } from "~/utils/common";
|
||||||
|
|
||||||
const waitlistUserSelection = {
|
const waitlistUserSelection = {
|
||||||
id: true,
|
id: true,
|
||||||
@@ -79,7 +81,7 @@ export const adminRouter = createTRPCRouter({
|
|||||||
.input(
|
.input(
|
||||||
z.object({
|
z.object({
|
||||||
region: z.string(),
|
region: z.string(),
|
||||||
})
|
}),
|
||||||
)
|
)
|
||||||
.query(async ({ input }) => {
|
.query(async ({ input }) => {
|
||||||
const acc = await getAccount(input.region);
|
const acc = await getAccount(input.region);
|
||||||
@@ -93,7 +95,7 @@ export const adminRouter = createTRPCRouter({
|
|||||||
usesendUrl: z.string().url(),
|
usesendUrl: z.string().url(),
|
||||||
sendRate: z.number(),
|
sendRate: z.number(),
|
||||||
transactionalQuota: z.number(),
|
transactionalQuota: z.number(),
|
||||||
})
|
}),
|
||||||
)
|
)
|
||||||
.mutation(async ({ input }) => {
|
.mutation(async ({ input }) => {
|
||||||
return SesSettingsService.createSesSetting({
|
return SesSettingsService.createSesSetting({
|
||||||
@@ -110,7 +112,7 @@ export const adminRouter = createTRPCRouter({
|
|||||||
settingsId: z.string(),
|
settingsId: z.string(),
|
||||||
sendRate: z.number(),
|
sendRate: z.number(),
|
||||||
transactionalQuota: z.number(),
|
transactionalQuota: z.number(),
|
||||||
})
|
}),
|
||||||
)
|
)
|
||||||
.mutation(async ({ input }) => {
|
.mutation(async ({ input }) => {
|
||||||
return SesSettingsService.updateSesSetting({
|
return SesSettingsService.updateSesSetting({
|
||||||
@@ -124,11 +126,11 @@ export const adminRouter = createTRPCRouter({
|
|||||||
.input(
|
.input(
|
||||||
z.object({
|
z.object({
|
||||||
region: z.string().optional().nullable(),
|
region: z.string().optional().nullable(),
|
||||||
})
|
}),
|
||||||
)
|
)
|
||||||
.query(async ({ input }) => {
|
.query(async ({ input }) => {
|
||||||
return SesSettingsService.getSetting(
|
return SesSettingsService.getSetting(
|
||||||
input.region ?? env.AWS_DEFAULT_REGION
|
input.region ?? env.AWS_DEFAULT_REGION,
|
||||||
);
|
);
|
||||||
}),
|
}),
|
||||||
|
|
||||||
@@ -139,7 +141,7 @@ export const adminRouter = createTRPCRouter({
|
|||||||
.string()
|
.string()
|
||||||
.email()
|
.email()
|
||||||
.transform((value) => value.toLowerCase()),
|
.transform((value) => value.toLowerCase()),
|
||||||
})
|
}),
|
||||||
)
|
)
|
||||||
.mutation(async ({ input }) => {
|
.mutation(async ({ input }) => {
|
||||||
const user = await db.user.findUnique({
|
const user = await db.user.findUnique({
|
||||||
@@ -155,7 +157,7 @@ export const adminRouter = createTRPCRouter({
|
|||||||
z.object({
|
z.object({
|
||||||
userId: z.number(),
|
userId: z.number(),
|
||||||
isWaitlisted: z.boolean(),
|
isWaitlisted: z.boolean(),
|
||||||
})
|
}),
|
||||||
)
|
)
|
||||||
.mutation(async ({ input }) => {
|
.mutation(async ({ input }) => {
|
||||||
const existingUser = await db.user.findUnique({
|
const existingUser = await db.user.findUnique({
|
||||||
@@ -182,6 +184,56 @@ export const adminRouter = createTRPCRouter({
|
|||||||
Boolean(updatedUser.email) &&
|
Boolean(updatedUser.email) &&
|
||||||
(founderEmail || fallbackFrom);
|
(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) {
|
if (shouldSendAcceptanceEmail) {
|
||||||
const recipient = updatedUser.email as string;
|
const recipient = updatedUser.email as string;
|
||||||
const replyTo = founderEmail ?? fallbackFrom;
|
const replyTo = founderEmail ?? fallbackFrom;
|
||||||
@@ -201,12 +253,12 @@ export const adminRouter = createTRPCRouter({
|
|||||||
text,
|
text,
|
||||||
toPlainHtml(text),
|
toPlainHtml(text),
|
||||||
replyTo,
|
replyTo,
|
||||||
fromOverride
|
fromOverride,
|
||||||
);
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error(
|
logger.error(
|
||||||
{ userId: updatedUser.id, 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(
|
.input(
|
||||||
z.object({
|
z.object({
|
||||||
userId: z.number(),
|
userId: z.number(),
|
||||||
})
|
}),
|
||||||
)
|
)
|
||||||
.mutation(async ({ input }) => {
|
.mutation(async ({ input }) => {
|
||||||
const user = await db.user.findUnique({
|
const user = await db.user.findUnique({
|
||||||
@@ -262,12 +314,12 @@ export const adminRouter = createTRPCRouter({
|
|||||||
text,
|
text,
|
||||||
toPlainHtml(text),
|
toPlainHtml(text),
|
||||||
replyTo,
|
replyTo,
|
||||||
fromOverride
|
fromOverride,
|
||||||
);
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error(
|
logger.error(
|
||||||
{ userId: user.id, 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");
|
throw new Error("Failed to send waitlist rejection email");
|
||||||
}
|
}
|
||||||
@@ -282,7 +334,7 @@ export const adminRouter = createTRPCRouter({
|
|||||||
.string({ required_error: "Search query is required" })
|
.string({ required_error: "Search query is required" })
|
||||||
.trim()
|
.trim()
|
||||||
.min(1, "Search query is required"),
|
.min(1, "Search query is required"),
|
||||||
})
|
}),
|
||||||
)
|
)
|
||||||
.mutation(async ({ input }) => {
|
.mutation(async ({ input }) => {
|
||||||
const query = input.query.trim();
|
const query = input.query.trim();
|
||||||
@@ -338,7 +390,7 @@ export const adminRouter = createTRPCRouter({
|
|||||||
dailyEmailLimit: z.number().int().min(0).max(10_000_000),
|
dailyEmailLimit: z.number().int().min(0).max(10_000_000),
|
||||||
isBlocked: z.boolean(),
|
isBlocked: z.boolean(),
|
||||||
plan: z.enum(["FREE", "BASIC"]),
|
plan: z.enum(["FREE", "BASIC"]),
|
||||||
})
|
}),
|
||||||
)
|
)
|
||||||
.mutation(async ({ input }) => {
|
.mutation(async ({ input }) => {
|
||||||
const { teamId, ...data } = input;
|
const { teamId, ...data } = input;
|
||||||
@@ -357,7 +409,7 @@ export const adminRouter = createTRPCRouter({
|
|||||||
z.object({
|
z.object({
|
||||||
timeframe: z.enum(["today", "thisMonth"]),
|
timeframe: z.enum(["today", "thisMonth"]),
|
||||||
paidOnly: z.boolean().optional(),
|
paidOnly: z.boolean().optional(),
|
||||||
})
|
}),
|
||||||
)
|
)
|
||||||
.query(async ({ input }) => {
|
.query(async ({ input }) => {
|
||||||
const timeframe = input.timeframe;
|
const timeframe = input.timeframe;
|
||||||
@@ -366,7 +418,7 @@ export const adminRouter = createTRPCRouter({
|
|||||||
const now = new Date();
|
const now = new Date();
|
||||||
const today = now.toISOString().slice(0, 10);
|
const today = now.toISOString().slice(0, 10);
|
||||||
const monthStartDate = new Date(
|
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);
|
const monthStart = monthStartDate.toISOString().slice(0, 10);
|
||||||
|
|
||||||
@@ -427,7 +479,7 @@ export const adminRouter = createTRPCRouter({
|
|||||||
bounced: 0,
|
bounced: 0,
|
||||||
complained: 0,
|
complained: 0,
|
||||||
hardBounced: 0,
|
hardBounced: 0,
|
||||||
}
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
Reference in New Issue
Block a user