Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion apps/api/src/agent/handlers/metric-handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,9 @@ function extractMetricValue(
queryData: unknown[],
defaultValue: unknown
): unknown {
if (!(queryData.length && queryData[0])) return defaultValue;
if (!(queryData.length && queryData[0])) {
return defaultValue;
}

const firstRow = queryData[0] as Record<string, unknown>;
const valueKey =
Expand Down
2 changes: 1 addition & 1 deletion apps/api/src/agent/prompts/agent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ export const comprehensiveUnifiedPrompt = (
mode: 'analysis_only' | 'execute_chat' | 'execute_agent_step',
previousMessages?: any[],
agentToolResult?: any,
model?: 'chat' | 'agent' | 'agent-max'
_model?: 'chat' | 'agent' | 'agent-max'
) => `
<persona>
You are Nova, a world-class, specialized AI analytics assistant for the website ${websiteHostname}. You are precise, analytical, and secure. Your sole purpose is to help users understand their website's analytics data by providing insights, generating SQL queries, and creating visualizations.
Expand Down
4 changes: 3 additions & 1 deletion apps/api/src/agent/utils/sql-validator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,9 @@ export function validateSQL(sql: string): boolean {

// Check for dangerous keyword patterns
for (const keyword of FORBIDDEN_SQL_KEYWORDS) {
if (upperSQL.includes(keyword)) return false;
if (upperSQL.includes(keyword)) {
return false;
}
}

// Must start with SELECT or WITH (for CTEs)
Expand Down
2 changes: 1 addition & 1 deletion apps/api/src/lib/website-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -143,7 +143,7 @@ export async function deriveWebsiteContext({ request }: { request: Request }) {
getCachedWebsite(website_id),
website_id && session?.user
? getTimezone(request, session)
: getTimezone(request, null)
: getTimezone(request, null),
]);

if (!website) {
Expand Down
1 change: 0 additions & 1 deletion apps/api/src/middleware/logging.ts
Original file line number Diff line number Diff line change
@@ -1 +0,0 @@

3 changes: 1 addition & 2 deletions apps/api/src/middleware/rate-limit.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { auth } from '@databuddy/auth';
import { getRateLimitIdentifier, rateLimiters } from '@databuddy/rpc';
import { cacheable } from '@databuddy/redis';
import { getRateLimitIdentifier, rateLimiters } from '@databuddy/rpc';
import { Elysia } from 'elysia';

export interface RateLimitOptions {
Expand Down Expand Up @@ -39,7 +39,6 @@ export function createRateLimitMiddleware(options: RateLimitOptions) {
if (!options.skipAuth) {
const session = await getCachedAuthSession(request.headers);
userId = session?.user?.id;

}

const identifier = getRateLimitIdentifier(userId, request.headers);
Expand Down
4 changes: 3 additions & 1 deletion apps/api/src/query/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -192,7 +192,9 @@ function applyGeoNormalization(
): Record<string, any>[] {
return data.map((row) => {
// Only normalize if row has a 'name' field (country/region/etc)
if (!row.name) return row;
if (!row.name) {
return row;
}
const code = getCountryCode(row.name);
const name = getCountryName(code);
return {
Expand Down
7 changes: 5 additions & 2 deletions apps/basket/src/hooks/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@ type WebsiteWithOwner = Website & {
ownerId: string | null;
};

const REGEX_WWW_PREFIX = /^www\./;
const REGEX_DOMAIN_LABEL = /^[a-zA-Z0-9-]+$/;

/**
* Resolves the owner's user ID for a given website.
* The owner is the user if it's a personal project, or the organization's owner
Expand Down Expand Up @@ -161,7 +164,7 @@ export function normalizeDomain(domain: string): string {

try {
const hostname = new URL(urlString).hostname;
const finalDomain = hostname.replace(/^www\./, '');
const finalDomain = hostname.replace(REGEX_WWW_PREFIX, '');

if (!isValidDomainFormat(finalDomain)) {
throw new Error(
Expand Down Expand Up @@ -213,7 +216,7 @@ export function isValidDomainFormat(domain: string): boolean {
return false;
}
if (
!/^[a-zA-Z0-9-]+$/.test(label) ||
!REGEX_DOMAIN_LABEL.test(label) ||
label.startsWith('-') ||
label.endsWith('-')
) {
Expand Down
34 changes: 18 additions & 16 deletions apps/basket/src/polyfills/compression.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,15 @@ globalThis.CompressionStream ??= class CompressionStream {
writable;

constructor(format) {
make(
this,
format === 'deflate'
? zlib.createDeflate()
: format === 'gzip'
? zlib.createGzip()
: zlib.createDeflateRaw()
);
let handle;
if (format === 'deflate') {
handle = zlib.createDeflate();
} else if (format === 'gzip') {
handle = zlib.createGzip();
} else {
handle = zlib.createDeflateRaw();
}
Comment thread
AyanavaKarmakar marked this conversation as resolved.
make(this, handle);
}
};

Expand All @@ -28,13 +29,14 @@ globalThis.DecompressionStream ??= class DecompressionStream {
writable;

constructor(format) {
make(
this,
format === 'deflate'
? zlib.createInflate()
: format === 'gzip'
? zlib.createGunzip()
: zlib.createInflateRaw()
);
let handle;
if (format === 'deflate') {
handle = zlib.createInflate();
} else if (format === 'gzip') {
handle = zlib.createGunzip();
} else {
handle = zlib.createInflateRaw();
}
make(this, handle);
}
};
2 changes: 1 addition & 1 deletion apps/basket/src/routes/basket.ts
Original file line number Diff line number Diff line change
Expand Up @@ -447,7 +447,7 @@ async function checkDuplicate(
async function logBlockedTraffic(
request: Request,
body: any,
query: any,
_query: any,
blockReason: string,
blockCategory: string,
botName?: string,
Expand Down
101 changes: 76 additions & 25 deletions apps/basket/src/routes/stripe.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,37 @@ interface StripeConfig {
isLiveMode: boolean;
}

interface StripeObjectWithMetadata {
metadata?: {
client_id?: string;
session_id?: string;
[key: string]: string | undefined;
} | null;
}

interface StripeDataRecord {
[key: string]:
| string
| number
| boolean
| null
| undefined
| Record<string, string>
| string[];
}

interface SecurityEventMetadata {
eventLivemode?: boolean;
configLivemode?: boolean;
eventType?: string;
[key: string]: unknown;
}

type StripeEventHandler = (
data: Stripe.PaymentIntent | Stripe.Charge | Stripe.Refund,
config: StripeConfig
) => Promise<void>;

const IS_PRODUCTION = process.env.NODE_ENV === 'production';
const ENABLE_MODE_VALIDATION =
process.env.ENABLE_MODE_VALIDATION === 'true' || IS_PRODUCTION;
Expand Down Expand Up @@ -113,7 +144,11 @@ async function getStripeConfigByToken(
/**
* Webhook handler with conditional security
*/
async function handleWebhook(request: Request, set: any, config: StripeConfig) {
async function handleWebhook(
request: Request,
set: { status?: number | string },
config: StripeConfig
) {
const sig = request.headers.get('stripe-signature');
const body = await request.text();

Expand Down Expand Up @@ -187,40 +222,53 @@ async function handleWebhook(request: Request, set: any, config: StripeConfig) {
* Process webhook events and store data
*/
async function processWebhookEvent(event: Stripe.Event, config: StripeConfig) {
const handlers: Record<string, (data: any) => Promise<void>> = {
'payment_intent.succeeded': (pi: Stripe.PaymentIntent) =>
insertPaymentIntent(pi, config),
'payment_intent.created': (pi: Stripe.PaymentIntent) =>
insertPaymentIntent(pi, config),
'payment_intent.canceled': (pi: Stripe.PaymentIntent) =>
insertPaymentIntent(pi, config),
'payment_intent.payment_failed': (pi: Stripe.PaymentIntent) =>
insertPaymentIntent(pi, config),
'payment_intent.requires_action': (pi: Stripe.PaymentIntent) =>
insertPaymentIntent(pi, config),
'charge.succeeded': (charge: Stripe.Charge) => insertCharge(charge, config),
'charge.failed': (charge: Stripe.Charge) => insertCharge(charge, config),
'charge.captured': (charge: Stripe.Charge) => insertCharge(charge, config),
'charge.dispute.created': (charge: Stripe.Charge) =>
insertCharge(charge, config),
'refund.created': (refund: Stripe.Refund) => insertRefund(refund, config),
'refund.updated': (refund: Stripe.Refund) => insertRefund(refund, config),
'refund.failed': (refund: Stripe.Refund) => insertRefund(refund, config),
const handlers: Record<string, StripeEventHandler> = {
'payment_intent.succeeded': (data, stripeConfig) =>
insertPaymentIntent(data as Stripe.PaymentIntent, stripeConfig),
'payment_intent.created': (data, stripeConfig) =>
insertPaymentIntent(data as Stripe.PaymentIntent, stripeConfig),
'payment_intent.canceled': (data, stripeConfig) =>
insertPaymentIntent(data as Stripe.PaymentIntent, stripeConfig),
'payment_intent.payment_failed': (data, stripeConfig) =>
insertPaymentIntent(data as Stripe.PaymentIntent, stripeConfig),
'payment_intent.requires_action': (data, stripeConfig) =>
insertPaymentIntent(data as Stripe.PaymentIntent, stripeConfig),
'charge.succeeded': (data, stripeConfig) =>
insertCharge(data as Stripe.Charge, stripeConfig),
'charge.failed': (data, stripeConfig) =>
insertCharge(data as Stripe.Charge, stripeConfig),
'charge.captured': (data, stripeConfig) =>
insertCharge(data as Stripe.Charge, stripeConfig),
'charge.dispute.created': (data, stripeConfig) =>
insertCharge(data as Stripe.Charge, stripeConfig),
'refund.created': (data, stripeConfig) =>
insertRefund(data as Stripe.Refund, stripeConfig),
'refund.updated': (data, stripeConfig) =>
insertRefund(data as Stripe.Refund, stripeConfig),
'refund.failed': (data, stripeConfig) =>
insertRefund(data as Stripe.Refund, stripeConfig),
};

const handler = handlers[event.type];
if (handler) {
await handler(event.data.object as any);
await handler(
event.data.object as Stripe.PaymentIntent | Stripe.Charge | Stripe.Refund,
config
);
} else {
logger.info(`🔄 Unhandled event type: ${event.type}`);
}
}

function extractClientId(stripeObject: any): string | null {
function extractClientId(
stripeObject: StripeObjectWithMetadata
): string | null {
return stripeObject.metadata?.client_id || null;
}

function extractSessionId(stripeObject: any): string | null {
function extractSessionId(
stripeObject: StripeObjectWithMetadata
): string | null {
return stripeObject.metadata?.session_id || null;
}

Expand All @@ -236,7 +284,10 @@ function validateClientId(
}
}

async function insertStripeData(table: string, data: any): Promise<void> {
async function insertStripeData(
table: string,
data: StripeDataRecord
): Promise<void> {
await clickHouse.insert({
table,
values: [data],
Expand Down Expand Up @@ -352,7 +403,7 @@ function logSecurityEvent(
eventType: string,
webhookToken: string,
ip: string,
metadata?: any
metadata?: SecurityEventMetadata
) {
const logData = {
webhookToken,
Expand Down
30 changes: 18 additions & 12 deletions apps/basket/src/utils/ip-geo.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
import { beforeEach, describe, expect, it } from 'vitest';
import {
anonymizeIp,
extractIpFromRequest,
Expand Down Expand Up @@ -75,8 +75,10 @@ describe('IP Geo Utilities', () => {
'169.254.1.1', // Link-local
];

for (const ip of privateIPs) {
const result = await getGeoLocation(ip);
const results = await Promise.all(
privateIPs.map((ip) => getGeoLocation(ip))
);
for (const result of results) {
// Should return undefined for private IPs (not in public database)
expect(result).toEqual({
country: undefined,
Expand Down Expand Up @@ -362,7 +364,7 @@ describe('IP Geo Utilities', () => {

describe('Edge cases', () => {
it('should handle null/undefined IP gracefully', async () => {
const result = await getGeoLocation(null as any);
const result = await getGeoLocation(null as unknown as string);
expect(result).toEqual({
country: undefined,
region: undefined,
Expand Down Expand Up @@ -408,8 +410,10 @@ describe('IP Geo Utilities', () => {
'104.28.196.183', // Cloudflare IP
];

for (const ip of publicIPs) {
const result = await getGeoLocation(ip);
const results = await Promise.all(
publicIPs.map((ip) => getGeoLocation(ip))
);
for (const result of results) {
expect(result).toBeDefined();
// Should either have geo data or be undefined (not in database)
expect(
Expand All @@ -432,13 +436,15 @@ describe('IP Geo Utilities', () => {
'',
];

for (const ip of testIPs) {
const geoResult = await getGeoLocation(ip);
const fullResult = await getGeo(ip);
const geoResults = await Promise.all(
testIPs.map((ip) => getGeoLocation(ip))
);
const fullResults = await Promise.all(testIPs.map((ip) => getGeo(ip)));

expect(geoResult).toBeDefined();
expect(fullResult).toBeDefined();
expect(fullResult.anonymizedIP).toBeDefined();
for (let i = 0; i < testIPs.length; i++) {
expect(geoResults[i]).toBeDefined();
expect(fullResults[i]).toBeDefined();
expect(fullResults[i].anonymizedIP).toBeDefined();
}
});
});
Expand Down
2 changes: 1 addition & 1 deletion apps/dashboard/app/(auth)/login/forgot/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ export default function ForgotPasswordPage() {
},
},
});
} catch (error) {
} catch (_error) {
setIsLoading(false);
toast.error('An error occurred. Please try again later.');
}
Expand Down
4 changes: 2 additions & 2 deletions apps/dashboard/app/(auth)/login/magic-sent/page.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
'use client';

import { signIn } from '@databuddy/auth/client';
import { ChevronLeft, Loader2, MailCheck, Sparkles } from 'lucide-react';
import { ChevronLeft, Loader2, MailCheck } from 'lucide-react';
Comment thread
AyanavaKarmakar marked this conversation as resolved.
import Link from 'next/link';
import { useSearchParams } from 'next/navigation';
import { useState } from 'react';
Expand Down Expand Up @@ -35,7 +35,7 @@ export default function MagicSentPage() {
},
},
});
} catch (error) {
} catch (_error) {
setIsLoading(false);
toast.error('Failed to send magic link. Please try again.');
}
Expand Down
Loading