Skip to content
Merged
Show file tree
Hide file tree
Changes from 7 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
9 changes: 5 additions & 4 deletions apps/api/src/routes/query.ts
Original file line number Diff line number Diff line change
Expand Up @@ -151,10 +151,11 @@ async function executeDynamicQuery(
const startDate = queryParams.start_date || queryParams.startDate;
const endDate = queryParams.end_date || queryParams.endDate;
const websiteId = queryParams.website_id;

const websiteDomain = websiteId && !domainCache?.[websiteId]
? await getWebsiteDomain(websiteId)
: domainCache?.[websiteId] || null;

const websiteDomain =
websiteId && !domainCache?.[websiteId]
? await getWebsiteDomain(websiteId)
: domainCache?.[websiteId] || null;

const getTimeUnit = (granularity?: string): 'hour' | 'day' => {
if (['hourly', 'hour'].includes(granularity || '')) {
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 @@ -445,7 +445,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
Loading