From 02fbc279b58eade09d6a67d950cc1a6b4a6c0d2c Mon Sep 17 00:00:00 2001 From: miloschwartz Date: Mon, 17 Nov 2025 20:37:24 -0500 Subject: [PATCH] add email consent and update audience --- messages/en-US.json | 3 + server/private/lib/resend.ts | 2 +- .../routers/auditLogs/queryAccessAuditLog.ts | 7 +- .../routers/auditLogs/queryActionAuditLog.ts | 7 +- .../routers/auditLogs/queryRequstAuditLog.ts | 7 +- server/routers/auth/signup.ts | 9 +- src/components/SignupForm.tsx | 129 +++++++++++------- 7 files changed, 104 insertions(+), 60 deletions(-) diff --git a/messages/en-US.json b/messages/en-US.json index dd2a2d3d..cf066c3d 100644 --- a/messages/en-US.json +++ b/messages/en-US.json @@ -1430,6 +1430,9 @@ "and": "and", "privacyPolicy": "privacy policy" }, + "signUpMarketing": { + "keepMeInTheLoop": "Keep me in the loop with news, updates, and new features by email." + }, "siteRequired": "Site is required.", "olmTunnel": "Olm Tunnel", "olmTunnelDescription": "Use Olm for client connectivity", diff --git a/server/private/lib/resend.ts b/server/private/lib/resend.ts index 56198e0b..17384ea3 100644 --- a/server/private/lib/resend.ts +++ b/server/private/lib/resend.ts @@ -16,7 +16,7 @@ import privateConfig from "#private/lib/config"; import logger from "@server/logger"; export enum AudienceIds { - SignUps = "5cfbf99b-c592-40a9-9b8a-577a4681c158", + SignUps = "6c4e77b2-0851-4bd6-bac8-f51f91360f1a", Subscribed = "870b43fd-387f-44de-8fc1-707335f30b20", Churned = "f3ae92bd-2fdb-4d77-8746-2118afd62549", Newsletter = "5500c431-191c-42f0-a5d4-8b6d445b4ea0" diff --git a/server/private/routers/auditLogs/queryAccessAuditLog.ts b/server/private/routers/auditLogs/queryAccessAuditLog.ts index 3e0b4601..769dcf55 100644 --- a/server/private/routers/auditLogs/queryAccessAuditLog.ts +++ b/server/private/routers/auditLogs/queryAccessAuditLog.ts @@ -40,7 +40,12 @@ export const queryAccessAuditLogsQuery = z.object({ }) .transform((val) => Math.floor(new Date(val).getTime() / 1000)) .optional() - .prefault(new Date().toISOString()), + .prefault(new Date().toISOString()) + .openapi({ + type: "string", + format: "date-time", + description: "End time as ISO date string (defaults to current time)" + }), action: z .union([z.boolean(), z.string()]) .transform((val) => (typeof val === "string" ? val === "true" : val)) diff --git a/server/private/routers/auditLogs/queryActionAuditLog.ts b/server/private/routers/auditLogs/queryActionAuditLog.ts index 6a5bde6d..d4a43879 100644 --- a/server/private/routers/auditLogs/queryActionAuditLog.ts +++ b/server/private/routers/auditLogs/queryActionAuditLog.ts @@ -40,7 +40,12 @@ export const queryActionAuditLogsQuery = z.object({ }) .transform((val) => Math.floor(new Date(val).getTime() / 1000)) .optional() - .prefault(new Date().toISOString()), + .prefault(new Date().toISOString()) + .openapi({ + type: "string", + format: "date-time", + description: "End time as ISO date string (defaults to current time)" + }), action: z.string().optional(), actorType: z.string().optional(), actorId: z.string().optional(), diff --git a/server/routers/auditLogs/queryRequstAuditLog.ts b/server/routers/auditLogs/queryRequstAuditLog.ts index 342b7091..8c9aa902 100644 --- a/server/routers/auditLogs/queryRequstAuditLog.ts +++ b/server/routers/auditLogs/queryRequstAuditLog.ts @@ -27,7 +27,12 @@ export const queryAccessAuditLogsQuery = z.object({ }) .transform((val) => Math.floor(new Date(val).getTime() / 1000)) .optional() - .prefault(new Date().toISOString()), + .prefault(new Date().toISOString()) + .openapi({ + type: "string", + format: "date-time", + description: "End time as ISO date string (defaults to current time)" + }), action: z .union([z.boolean(), z.string()]) .transform((val) => (typeof val === "string" ? val === "true" : val)) diff --git a/server/routers/auth/signup.ts b/server/routers/auth/signup.ts index 595a9b91..842214cf 100644 --- a/server/routers/auth/signup.ts +++ b/server/routers/auth/signup.ts @@ -30,7 +30,8 @@ export const signupBodySchema = z.object({ password: passwordSchema, inviteToken: z.string().optional(), inviteId: z.string().optional(), - termsAcceptedTimestamp: z.string().nullable().optional() + termsAcceptedTimestamp: z.string().nullable().optional(), + marketingEmailConsent: z.boolean().optional() }); export type SignUpBody = z.infer; @@ -55,7 +56,7 @@ export async function signup( ); } - const { email, password, inviteToken, inviteId, termsAcceptedTimestamp } = + const { email, password, inviteToken, inviteId, termsAcceptedTimestamp, marketingEmailConsent } = parsedBody.data; const passwordHash = await hashPassword(password); @@ -220,8 +221,8 @@ export async function signup( new Date(sess.expiresAt) ); res.appendHeader("Set-Cookie", cookie); - - if (build == "saas") { + if (build == "saas" && marketingEmailConsent) { + logger.debug(`User ${email} opted in to marketing emails during signup.`); moveEmailToAudience(email, AudienceIds.SignUps); } diff --git a/src/components/SignupForm.tsx b/src/components/SignupForm.tsx index c9cb6a48..76d2dfce 100644 --- a/src/components/SignupForm.tsx +++ b/src/components/SignupForm.tsx @@ -92,7 +92,8 @@ const formSchema = z message: "You must agree to the terms of service and privacy policy" } - ) + ), + marketingEmailConsent: z.boolean().optional() }) .refine((data) => data.password === data.confirmPassword, { path: ["confirmPassword"], @@ -123,7 +124,8 @@ export default function SignupForm({ email: emailParam || "", password: "", confirmPassword: "", - agreeToTerms: false + agreeToTerms: false, + marketingEmailConsent: false }, mode: "onChange" // Enable real-time validation }); @@ -135,7 +137,7 @@ export default function SignupForm({ passwordValue === confirmPasswordValue; async function onSubmit(values: z.infer) { - const { email, password } = values; + const { email, password, marketingEmailConsent } = values; setLoading(true); const res = await api @@ -144,7 +146,8 @@ export default function SignupForm({ password, inviteId, inviteToken, - termsAcceptedTimestamp: termsAgreedAt + termsAcceptedTimestamp: termsAgreedAt, + marketingEmailConsent: build === "saas" ? marketingEmailConsent : undefined }) .catch((e) => { console.error(e); @@ -489,56 +492,78 @@ export default function SignupForm({ )} /> {build === "saas" && ( - ( - - - { - field.onChange(checked); - handleTermsChange( - checked as boolean - ); - }} - /> - - + + )} + /> + ( + + + + +
+ + {t("signUpMarketing.keepMeInTheLoop")} + + +
+
+ )} + /> + )} {error && (