mirror of
https://github.com/fosrl/pangolin.git
synced 2026-02-23 13:26:41 +00:00
add email consent and update audience
This commit is contained in:
@@ -1430,6 +1430,9 @@
|
|||||||
"and": "and",
|
"and": "and",
|
||||||
"privacyPolicy": "privacy policy"
|
"privacyPolicy": "privacy policy"
|
||||||
},
|
},
|
||||||
|
"signUpMarketing": {
|
||||||
|
"keepMeInTheLoop": "Keep me in the loop with news, updates, and new features by email."
|
||||||
|
},
|
||||||
"siteRequired": "Site is required.",
|
"siteRequired": "Site is required.",
|
||||||
"olmTunnel": "Olm Tunnel",
|
"olmTunnel": "Olm Tunnel",
|
||||||
"olmTunnelDescription": "Use Olm for client connectivity",
|
"olmTunnelDescription": "Use Olm for client connectivity",
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ import privateConfig from "#private/lib/config";
|
|||||||
import logger from "@server/logger";
|
import logger from "@server/logger";
|
||||||
|
|
||||||
export enum AudienceIds {
|
export enum AudienceIds {
|
||||||
SignUps = "5cfbf99b-c592-40a9-9b8a-577a4681c158",
|
SignUps = "6c4e77b2-0851-4bd6-bac8-f51f91360f1a",
|
||||||
Subscribed = "870b43fd-387f-44de-8fc1-707335f30b20",
|
Subscribed = "870b43fd-387f-44de-8fc1-707335f30b20",
|
||||||
Churned = "f3ae92bd-2fdb-4d77-8746-2118afd62549",
|
Churned = "f3ae92bd-2fdb-4d77-8746-2118afd62549",
|
||||||
Newsletter = "5500c431-191c-42f0-a5d4-8b6d445b4ea0"
|
Newsletter = "5500c431-191c-42f0-a5d4-8b6d445b4ea0"
|
||||||
|
|||||||
@@ -40,7 +40,12 @@ export const queryAccessAuditLogsQuery = z.object({
|
|||||||
})
|
})
|
||||||
.transform((val) => Math.floor(new Date(val).getTime() / 1000))
|
.transform((val) => Math.floor(new Date(val).getTime() / 1000))
|
||||||
.optional()
|
.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
|
action: z
|
||||||
.union([z.boolean(), z.string()])
|
.union([z.boolean(), z.string()])
|
||||||
.transform((val) => (typeof val === "string" ? val === "true" : val))
|
.transform((val) => (typeof val === "string" ? val === "true" : val))
|
||||||
|
|||||||
@@ -40,7 +40,12 @@ export const queryActionAuditLogsQuery = z.object({
|
|||||||
})
|
})
|
||||||
.transform((val) => Math.floor(new Date(val).getTime() / 1000))
|
.transform((val) => Math.floor(new Date(val).getTime() / 1000))
|
||||||
.optional()
|
.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(),
|
action: z.string().optional(),
|
||||||
actorType: z.string().optional(),
|
actorType: z.string().optional(),
|
||||||
actorId: z.string().optional(),
|
actorId: z.string().optional(),
|
||||||
|
|||||||
@@ -27,7 +27,12 @@ export const queryAccessAuditLogsQuery = z.object({
|
|||||||
})
|
})
|
||||||
.transform((val) => Math.floor(new Date(val).getTime() / 1000))
|
.transform((val) => Math.floor(new Date(val).getTime() / 1000))
|
||||||
.optional()
|
.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
|
action: z
|
||||||
.union([z.boolean(), z.string()])
|
.union([z.boolean(), z.string()])
|
||||||
.transform((val) => (typeof val === "string" ? val === "true" : val))
|
.transform((val) => (typeof val === "string" ? val === "true" : val))
|
||||||
|
|||||||
@@ -30,7 +30,8 @@ export const signupBodySchema = z.object({
|
|||||||
password: passwordSchema,
|
password: passwordSchema,
|
||||||
inviteToken: z.string().optional(),
|
inviteToken: z.string().optional(),
|
||||||
inviteId: 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<typeof signupBodySchema>;
|
export type SignUpBody = z.infer<typeof signupBodySchema>;
|
||||||
@@ -55,7 +56,7 @@ export async function signup(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const { email, password, inviteToken, inviteId, termsAcceptedTimestamp } =
|
const { email, password, inviteToken, inviteId, termsAcceptedTimestamp, marketingEmailConsent } =
|
||||||
parsedBody.data;
|
parsedBody.data;
|
||||||
|
|
||||||
const passwordHash = await hashPassword(password);
|
const passwordHash = await hashPassword(password);
|
||||||
@@ -220,8 +221,8 @@ export async function signup(
|
|||||||
new Date(sess.expiresAt)
|
new Date(sess.expiresAt)
|
||||||
);
|
);
|
||||||
res.appendHeader("Set-Cookie", cookie);
|
res.appendHeader("Set-Cookie", cookie);
|
||||||
|
if (build == "saas" && marketingEmailConsent) {
|
||||||
if (build == "saas") {
|
logger.debug(`User ${email} opted in to marketing emails during signup.`);
|
||||||
moveEmailToAudience(email, AudienceIds.SignUps);
|
moveEmailToAudience(email, AudienceIds.SignUps);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -92,7 +92,8 @@ const formSchema = z
|
|||||||
message:
|
message:
|
||||||
"You must agree to the terms of service and privacy policy"
|
"You must agree to the terms of service and privacy policy"
|
||||||
}
|
}
|
||||||
)
|
),
|
||||||
|
marketingEmailConsent: z.boolean().optional()
|
||||||
})
|
})
|
||||||
.refine((data) => data.password === data.confirmPassword, {
|
.refine((data) => data.password === data.confirmPassword, {
|
||||||
path: ["confirmPassword"],
|
path: ["confirmPassword"],
|
||||||
@@ -123,7 +124,8 @@ export default function SignupForm({
|
|||||||
email: emailParam || "",
|
email: emailParam || "",
|
||||||
password: "",
|
password: "",
|
||||||
confirmPassword: "",
|
confirmPassword: "",
|
||||||
agreeToTerms: false
|
agreeToTerms: false,
|
||||||
|
marketingEmailConsent: false
|
||||||
},
|
},
|
||||||
mode: "onChange" // Enable real-time validation
|
mode: "onChange" // Enable real-time validation
|
||||||
});
|
});
|
||||||
@@ -135,7 +137,7 @@ export default function SignupForm({
|
|||||||
passwordValue === confirmPasswordValue;
|
passwordValue === confirmPasswordValue;
|
||||||
|
|
||||||
async function onSubmit(values: z.infer<typeof formSchema>) {
|
async function onSubmit(values: z.infer<typeof formSchema>) {
|
||||||
const { email, password } = values;
|
const { email, password, marketingEmailConsent } = values;
|
||||||
|
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
const res = await api
|
const res = await api
|
||||||
@@ -144,7 +146,8 @@ export default function SignupForm({
|
|||||||
password,
|
password,
|
||||||
inviteId,
|
inviteId,
|
||||||
inviteToken,
|
inviteToken,
|
||||||
termsAcceptedTimestamp: termsAgreedAt
|
termsAcceptedTimestamp: termsAgreedAt,
|
||||||
|
marketingEmailConsent: build === "saas" ? marketingEmailConsent : undefined
|
||||||
})
|
})
|
||||||
.catch((e) => {
|
.catch((e) => {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
@@ -489,56 +492,78 @@ export default function SignupForm({
|
|||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
{build === "saas" && (
|
{build === "saas" && (
|
||||||
<FormField
|
<>
|
||||||
control={form.control}
|
<FormField
|
||||||
name="agreeToTerms"
|
control={form.control}
|
||||||
render={({ field }) => (
|
name="agreeToTerms"
|
||||||
<FormItem className="flex flex-row items-center">
|
render={({ field }) => (
|
||||||
<FormControl>
|
<FormItem className="flex flex-row items-center">
|
||||||
<Checkbox
|
<FormControl>
|
||||||
checked={field.value}
|
<Checkbox
|
||||||
onCheckedChange={(checked) => {
|
checked={field.value}
|
||||||
field.onChange(checked);
|
onCheckedChange={(checked) => {
|
||||||
handleTermsChange(
|
field.onChange(checked);
|
||||||
checked as boolean
|
handleTermsChange(
|
||||||
);
|
checked as boolean
|
||||||
}}
|
);
|
||||||
/>
|
}}
|
||||||
</FormControl>
|
/>
|
||||||
<div className="leading-none">
|
</FormControl>
|
||||||
<FormLabel className="text-sm font-normal">
|
<div className="leading-none">
|
||||||
<div>
|
<FormLabel className="text-sm font-normal">
|
||||||
{t(
|
<div>
|
||||||
"signUpTerms.IAgreeToThe"
|
|
||||||
)}{" "}
|
|
||||||
<a
|
|
||||||
href="https://pangolin.net/terms-of-service.html"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
className="text-primary hover:underline"
|
|
||||||
>
|
|
||||||
{t(
|
{t(
|
||||||
"signUpTerms.termsOfService"
|
"signUpTerms.IAgreeToThe"
|
||||||
)}{" "}
|
)}{" "}
|
||||||
</a>
|
<a
|
||||||
{t("signUpTerms.and")}{" "}
|
href="https://pangolin.net/terms-of-service.html"
|
||||||
<a
|
target="_blank"
|
||||||
href="https://pangolin.net/privacy-policy.html"
|
rel="noopener noreferrer"
|
||||||
target="_blank"
|
className="text-primary hover:underline"
|
||||||
rel="noopener noreferrer"
|
>
|
||||||
className="text-primary hover:underline"
|
{t(
|
||||||
>
|
"signUpTerms.termsOfService"
|
||||||
{t(
|
)}{" "}
|
||||||
"signUpTerms.privacyPolicy"
|
</a>
|
||||||
)}
|
{t("signUpTerms.and")}{" "}
|
||||||
</a>
|
<a
|
||||||
</div>
|
href="https://pangolin.net/privacy-policy.html"
|
||||||
</FormLabel>
|
target="_blank"
|
||||||
<FormMessage />
|
rel="noopener noreferrer"
|
||||||
</div>
|
className="text-primary hover:underline"
|
||||||
</FormItem>
|
>
|
||||||
)}
|
{t(
|
||||||
/>
|
"signUpTerms.privacyPolicy"
|
||||||
|
)}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</FormLabel>
|
||||||
|
<FormMessage />
|
||||||
|
</div>
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="marketingEmailConsent"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem className="flex flex-row items-start">
|
||||||
|
<FormControl>
|
||||||
|
<Checkbox
|
||||||
|
checked={field.value}
|
||||||
|
onCheckedChange={field.onChange}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<div className="leading-none">
|
||||||
|
<FormLabel className="text-sm font-normal">
|
||||||
|
{t("signUpMarketing.keepMeInTheLoop")}
|
||||||
|
</FormLabel>
|
||||||
|
<FormMessage />
|
||||||
|
</div>
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{error && (
|
{error && (
|
||||||
|
|||||||
Reference in New Issue
Block a user