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
|
||||
// And recieved with useRouteContext in the client
|
||||
// And received with useRouteContext in the client
|
||||
export const getCookiesFn = createServerFn({ method: 'GET' }).handler(() =>
|
||||
pick(VALID_COOKIES, getCookies()),
|
||||
);
|
||||
|
||||
@@ -102,7 +102,7 @@ export default function CreateInvite() {
|
||||
<div>
|
||||
<SheetTitle>Invite a user</SheetTitle>
|
||||
<SheetDescription>
|
||||
Invite users to your organization. They will recieve an email
|
||||
Invite users to your organization. They will receive an email
|
||||
will instructions.
|
||||
</SheetDescription>
|
||||
</div>
|
||||
|
||||
@@ -20,6 +20,22 @@ const validator = z.object({
|
||||
|
||||
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(
|
||||
'/_app/$organizationId/profile/_tabs/email-preferences',
|
||||
)({
|
||||
@@ -37,7 +53,7 @@ function Component() {
|
||||
|
||||
const { control, handleSubmit, formState, reset } = useForm<IForm>({
|
||||
defaultValues: {
|
||||
categories: preferencesQuery.data,
|
||||
categories: buildCategoryDefaults(preferencesQuery.data),
|
||||
},
|
||||
});
|
||||
|
||||
@@ -55,7 +71,7 @@ function Component() {
|
||||
trpc.email.getPreferences.queryOptions(),
|
||||
);
|
||||
reset({
|
||||
categories: freshData,
|
||||
categories: buildCategoryDefaults(freshData),
|
||||
});
|
||||
},
|
||||
onError: handleError,
|
||||
@@ -96,7 +112,7 @@ function Component() {
|
||||
</div>
|
||||
</div>
|
||||
<Switch
|
||||
checked={field.value ?? true}
|
||||
checked={field.value}
|
||||
onCheckedChange={field.onChange}
|
||||
disabled={mutation.isPending}
|
||||
/>
|
||||
|
||||
@@ -92,7 +92,7 @@ const ONBOARDING_EMAILS = [
|
||||
}),
|
||||
email({
|
||||
day: 14,
|
||||
template: 'onboarding-featue-request',
|
||||
template: 'onboarding-feature-request',
|
||||
data: (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.
|
||||
|
||||
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
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Button as EmailButton } from '@react-email/components';
|
||||
import type * as React from 'react';
|
||||
|
||||
export function Button({
|
||||
href,
|
||||
|
||||
@@ -58,7 +58,7 @@ export const templates = {
|
||||
schema: zOnboardingDashboards,
|
||||
category: 'onboarding' as const,
|
||||
},
|
||||
'onboarding-featue-request': {
|
||||
'onboarding-feature-request': {
|
||||
subject: () => 'One provider to rule them all',
|
||||
Component: OnboardingFeatureRequest,
|
||||
schema: zOnboardingFeatureRequest,
|
||||
|
||||
@@ -24,7 +24,7 @@ export function OnboardingTrialEnded({
|
||||
newUrl.searchParams.set('utm_campaign', 'onboarding-trial-ended');
|
||||
|
||||
return (
|
||||
<Layout>
|
||||
<Layout unsubscribeUrl={unsubscribeUrl}>
|
||||
<Text>Hi{firstName ? ` ${firstName}` : ''},</Text>
|
||||
<Text>Your OpenPanel trial has ended.</Text>
|
||||
<Text>
|
||||
|
||||
@@ -26,7 +26,7 @@ export function OnboardingTrialEnding({
|
||||
newUrl.searchParams.set('utm_campaign', 'onboarding-trial-ending');
|
||||
|
||||
return (
|
||||
<Layout>
|
||||
<Layout unsubscribeUrl={unsubscribeUrl}>
|
||||
<Text>Hi{firstName ? ` ${firstName}` : ''},</Text>
|
||||
<Text>Quick heads up: your OpenPanel trial ends soon.</Text>
|
||||
<Text>
|
||||
@@ -45,7 +45,7 @@ export function OnboardingTrialEnding({
|
||||
If something's holding you back, I'd like to hear about it. Just reply.
|
||||
</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.
|
||||
</Text>
|
||||
<Text>
|
||||
|
||||
@@ -61,7 +61,7 @@ export async function sendEmail<T extends TemplateKey>(
|
||||
const headers: Record<string, string> = {};
|
||||
if ('category' in template && 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-Post'] = 'List-Unsubscribe=One-Click';
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { createHmac } from 'crypto';
|
||||
import { createHmac, timingSafeEqual } from 'crypto';
|
||||
|
||||
const SECRET =
|
||||
process.env.UNSUBSCRIBE_SECRET ||
|
||||
@@ -17,7 +17,18 @@ export function verifyUnsubscribeToken(
|
||||
token: string,
|
||||
): boolean {
|
||||
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 {
|
||||
|
||||
@@ -2,6 +2,7 @@ import { emailCategories } from '@openpanel/constants';
|
||||
import { db } from '@openpanel/db';
|
||||
import { verifyUnsubscribeToken } from '@openpanel/email';
|
||||
import { z } from 'zod';
|
||||
import { TRPCBadRequestError } from '../errors';
|
||||
import { createTRPCRouter, protectedProcedure, publicProcedure } from '../trpc';
|
||||
|
||||
export const emailRouter = createTRPCRouter({
|
||||
@@ -18,7 +19,7 @@ export const emailRouter = createTRPCRouter({
|
||||
|
||||
// Verify token
|
||||
if (!verifyUnsubscribeToken(email, category, token)) {
|
||||
throw new Error('Invalid unsubscribe link');
|
||||
throw TRPCBadRequestError('Invalid unsubscribe link');
|
||||
}
|
||||
|
||||
// Upsert the unsubscribe record
|
||||
|
||||
Reference in New Issue
Block a user