fix coderabbit comments
This commit is contained in:
@@ -35,7 +35,7 @@ const setCookieFn = createServerFn({ method: 'POST' })
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Called in __root.tsx beforeLoad hook to get cookies from the server
|
// Called in __root.tsx beforeLoad hook to get cookies from the server
|
||||||
// And recieved with useRouteContext in the client
|
// And received with useRouteContext in the client
|
||||||
export const getCookiesFn = createServerFn({ method: 'GET' }).handler(() =>
|
export const getCookiesFn = createServerFn({ method: 'GET' }).handler(() =>
|
||||||
pick(VALID_COOKIES, getCookies()),
|
pick(VALID_COOKIES, getCookies()),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -102,7 +102,7 @@ export default function CreateInvite() {
|
|||||||
<div>
|
<div>
|
||||||
<SheetTitle>Invite a user</SheetTitle>
|
<SheetTitle>Invite a user</SheetTitle>
|
||||||
<SheetDescription>
|
<SheetDescription>
|
||||||
Invite users to your organization. They will recieve an email
|
Invite users to your organization. They will receive an email
|
||||||
will instructions.
|
will instructions.
|
||||||
</SheetDescription>
|
</SheetDescription>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -20,6 +20,22 @@ const validator = z.object({
|
|||||||
|
|
||||||
type IForm = z.infer<typeof validator>;
|
type IForm = z.infer<typeof validator>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build explicit boolean values for every key in emailCategories.
|
||||||
|
* Uses saved preferences when available, falling back to true (opted-in).
|
||||||
|
*/
|
||||||
|
function buildCategoryDefaults(
|
||||||
|
savedPreferences?: Record<string, boolean>,
|
||||||
|
): Record<string, boolean> {
|
||||||
|
return Object.keys(emailCategories).reduce(
|
||||||
|
(acc, category) => {
|
||||||
|
acc[category] = savedPreferences?.[category] ?? true;
|
||||||
|
return acc;
|
||||||
|
},
|
||||||
|
{} as Record<string, boolean>,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export const Route = createFileRoute(
|
export const Route = createFileRoute(
|
||||||
'/_app/$organizationId/profile/_tabs/email-preferences',
|
'/_app/$organizationId/profile/_tabs/email-preferences',
|
||||||
)({
|
)({
|
||||||
@@ -37,7 +53,7 @@ function Component() {
|
|||||||
|
|
||||||
const { control, handleSubmit, formState, reset } = useForm<IForm>({
|
const { control, handleSubmit, formState, reset } = useForm<IForm>({
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
categories: preferencesQuery.data,
|
categories: buildCategoryDefaults(preferencesQuery.data),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -55,7 +71,7 @@ function Component() {
|
|||||||
trpc.email.getPreferences.queryOptions(),
|
trpc.email.getPreferences.queryOptions(),
|
||||||
);
|
);
|
||||||
reset({
|
reset({
|
||||||
categories: freshData,
|
categories: buildCategoryDefaults(freshData),
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
onError: handleError,
|
onError: handleError,
|
||||||
@@ -96,7 +112,7 @@ function Component() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Switch
|
<Switch
|
||||||
checked={field.value ?? true}
|
checked={field.value}
|
||||||
onCheckedChange={field.onChange}
|
onCheckedChange={field.onChange}
|
||||||
disabled={mutation.isPending}
|
disabled={mutation.isPending}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -92,7 +92,7 @@ const ONBOARDING_EMAILS = [
|
|||||||
}),
|
}),
|
||||||
email({
|
email({
|
||||||
day: 14,
|
day: 14,
|
||||||
template: 'onboarding-featue-request',
|
template: 'onboarding-feature-request',
|
||||||
data: (ctx) => ({
|
data: (ctx) => ({
|
||||||
firstName: getters.firstName(ctx),
|
firstName: getters.firstName(ctx),
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -99,6 +99,6 @@ If OpenPanel has been useful, upgrading just keeps it going. Plans start at $2.5
|
|||||||
|
|
||||||
If something's holding you back, I'd like to hear about it. Just reply.
|
If something's holding you back, I'd like to hear about it. Just reply.
|
||||||
|
|
||||||
Your project will recieve events for the next 30 days, if you haven't upgraded by then we'll remove your workspace and projects.
|
Your project will receive events for the next 30 days, if you haven't upgraded by then we'll remove your workspace and projects.
|
||||||
|
|
||||||
Carl
|
Carl
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
import { Button as EmailButton } from '@react-email/components';
|
import { Button as EmailButton } from '@react-email/components';
|
||||||
|
import type * as React from 'react';
|
||||||
|
|
||||||
export function Button({
|
export function Button({
|
||||||
href,
|
href,
|
||||||
|
|||||||
@@ -58,7 +58,7 @@ export const templates = {
|
|||||||
schema: zOnboardingDashboards,
|
schema: zOnboardingDashboards,
|
||||||
category: 'onboarding' as const,
|
category: 'onboarding' as const,
|
||||||
},
|
},
|
||||||
'onboarding-featue-request': {
|
'onboarding-feature-request': {
|
||||||
subject: () => 'One provider to rule them all',
|
subject: () => 'One provider to rule them all',
|
||||||
Component: OnboardingFeatureRequest,
|
Component: OnboardingFeatureRequest,
|
||||||
schema: zOnboardingFeatureRequest,
|
schema: zOnboardingFeatureRequest,
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ export function OnboardingTrialEnded({
|
|||||||
newUrl.searchParams.set('utm_campaign', 'onboarding-trial-ended');
|
newUrl.searchParams.set('utm_campaign', 'onboarding-trial-ended');
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Layout>
|
<Layout unsubscribeUrl={unsubscribeUrl}>
|
||||||
<Text>Hi{firstName ? ` ${firstName}` : ''},</Text>
|
<Text>Hi{firstName ? ` ${firstName}` : ''},</Text>
|
||||||
<Text>Your OpenPanel trial has ended.</Text>
|
<Text>Your OpenPanel trial has ended.</Text>
|
||||||
<Text>
|
<Text>
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ export function OnboardingTrialEnding({
|
|||||||
newUrl.searchParams.set('utm_campaign', 'onboarding-trial-ending');
|
newUrl.searchParams.set('utm_campaign', 'onboarding-trial-ending');
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Layout>
|
<Layout unsubscribeUrl={unsubscribeUrl}>
|
||||||
<Text>Hi{firstName ? ` ${firstName}` : ''},</Text>
|
<Text>Hi{firstName ? ` ${firstName}` : ''},</Text>
|
||||||
<Text>Quick heads up: your OpenPanel trial ends soon.</Text>
|
<Text>Quick heads up: your OpenPanel trial ends soon.</Text>
|
||||||
<Text>
|
<Text>
|
||||||
@@ -45,7 +45,7 @@ export function OnboardingTrialEnding({
|
|||||||
If something's holding you back, I'd like to hear about it. Just reply.
|
If something's holding you back, I'd like to hear about it. Just reply.
|
||||||
</Text>
|
</Text>
|
||||||
<Text>
|
<Text>
|
||||||
Your project will recieve events for the next 30 days, if you haven't
|
Your project will receive events for the next 30 days, if you haven't
|
||||||
upgraded by then we'll remove your workspace and projects.
|
upgraded by then we'll remove your workspace and projects.
|
||||||
</Text>
|
</Text>
|
||||||
<Text>
|
<Text>
|
||||||
|
|||||||
@@ -61,7 +61,7 @@ export async function sendEmail<T extends TemplateKey>(
|
|||||||
const headers: Record<string, string> = {};
|
const headers: Record<string, string> = {};
|
||||||
if ('category' in template && template.category) {
|
if ('category' in template && template.category) {
|
||||||
const unsubscribeUrl = getUnsubscribeUrl(to, template.category);
|
const unsubscribeUrl = getUnsubscribeUrl(to, template.category);
|
||||||
(data as any).unsubscribeUrl = unsubscribeUrl;
|
(props.data as any).unsubscribeUrl = unsubscribeUrl;
|
||||||
headers['List-Unsubscribe'] = `<${unsubscribeUrl}>`;
|
headers['List-Unsubscribe'] = `<${unsubscribeUrl}>`;
|
||||||
headers['List-Unsubscribe-Post'] = 'List-Unsubscribe=One-Click';
|
headers['List-Unsubscribe-Post'] = 'List-Unsubscribe=One-Click';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { createHmac } from 'crypto';
|
import { createHmac, timingSafeEqual } from 'crypto';
|
||||||
|
|
||||||
const SECRET =
|
const SECRET =
|
||||||
process.env.UNSUBSCRIBE_SECRET ||
|
process.env.UNSUBSCRIBE_SECRET ||
|
||||||
@@ -17,7 +17,18 @@ export function verifyUnsubscribeToken(
|
|||||||
token: string,
|
token: string,
|
||||||
): boolean {
|
): boolean {
|
||||||
const expectedToken = generateUnsubscribeToken(email, category);
|
const expectedToken = generateUnsubscribeToken(email, category);
|
||||||
return token === expectedToken;
|
const tokenBuffer = Buffer.from(token, 'hex');
|
||||||
|
const expectedBuffer = Buffer.from(expectedToken, 'hex');
|
||||||
|
|
||||||
|
// Handle length mismatch safely to avoid timing leaks
|
||||||
|
if (tokenBuffer.length !== expectedBuffer.length) {
|
||||||
|
// Compare against zero-filled buffer of same length as token to maintain constant time
|
||||||
|
const zeroBuffer = Buffer.alloc(tokenBuffer.length);
|
||||||
|
timingSafeEqual(tokenBuffer, zeroBuffer);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return timingSafeEqual(tokenBuffer, expectedBuffer);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getUnsubscribeUrl(email: string, category: string): string {
|
export function getUnsubscribeUrl(email: string, category: string): string {
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { emailCategories } from '@openpanel/constants';
|
|||||||
import { db } from '@openpanel/db';
|
import { db } from '@openpanel/db';
|
||||||
import { verifyUnsubscribeToken } from '@openpanel/email';
|
import { verifyUnsubscribeToken } from '@openpanel/email';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
import { TRPCBadRequestError } from '../errors';
|
||||||
import { createTRPCRouter, protectedProcedure, publicProcedure } from '../trpc';
|
import { createTRPCRouter, protectedProcedure, publicProcedure } from '../trpc';
|
||||||
|
|
||||||
export const emailRouter = createTRPCRouter({
|
export const emailRouter = createTRPCRouter({
|
||||||
@@ -18,7 +19,7 @@ export const emailRouter = createTRPCRouter({
|
|||||||
|
|
||||||
// Verify token
|
// Verify token
|
||||||
if (!verifyUnsubscribeToken(email, category, token)) {
|
if (!verifyUnsubscribeToken(email, category, token)) {
|
||||||
throw new Error('Invalid unsubscribe link');
|
throw TRPCBadRequestError('Invalid unsubscribe link');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Upsert the unsubscribe record
|
// Upsert the unsubscribe record
|
||||||
|
|||||||
Reference in New Issue
Block a user