fix coderabbit comments

This commit is contained in:
Carl-Gerhard Lindesvärd
2026-01-21 15:31:24 +01:00
parent 3fa1a5429e
commit f9b1ec5038
12 changed files with 44 additions and 15 deletions

View File

@@ -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()),
);

View File

@@ -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>

View File

@@ -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}
/>

View File

@@ -92,7 +92,7 @@ const ONBOARDING_EMAILS = [
}),
email({
day: 14,
template: 'onboarding-featue-request',
template: 'onboarding-feature-request',
data: (ctx) => ({
firstName: getters.firstName(ctx),
}),

View File

@@ -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

View File

@@ -1,4 +1,5 @@
import { Button as EmailButton } from '@react-email/components';
import type * as React from 'react';
export function Button({
href,

View File

@@ -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,

View File

@@ -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>

View File

@@ -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>

View File

@@ -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';
}

View File

@@ -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 {

View File

@@ -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