Skip to content

Commit 42440f0

Browse files
committed
Merge branch 'main' of github.com:izadoesdev/mono
2 parents 17e0f2e + 0874a1a commit 42440f0

105 files changed

Lines changed: 11903 additions & 2690 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

apps/api/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
"pino": "^9.7.0",
3636
"pino-pretty": "^13.0.0",
3737
"react": "19.0.0",
38+
"snoowrap": "^1.23.0",
3839
"ua-parser-js": "^2.0.3",
3940
"zod": "^3.24.3"
4041
},

apps/api/src/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import websitesRouter from './routes/v1/websites';
99
import domainsRouter from './routes/v1/domains';
1010
import funnelRouter from './routes/v1/funnels';
1111
import revenueRouter from './routes/v1/revenue';
12+
import redditRouter from './routes/v1/reddit';
1213
import { logger } from './lib/logger';
1314
import { logger as HonoLogger } from "hono/logger"
1415
import { sentry } from '@hono/sentry'
@@ -81,6 +82,7 @@ app.route('/v1/websites', websitesRouter);
8182
app.route('/v1/domains', domainsRouter);
8283
app.route('/v1/funnels', funnelRouter);
8384
app.route('/v1/revenue', revenueRouter);
85+
app.route('/v1/reddit', redditRouter);
8486

8587
app.get('/health', (c) => c.json({ status: 'ok', version: '1.0.0' }));
8688
app.get('/', (c) => c.json({ status: 'ok', version: '1.0.0' }));

apps/api/src/middleware/auth.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import { websites, projects } from "@databuddy/db";
77
import { cacheable } from "@databuddy/redis";
88

99
// Helper function to verify website access with caching
10-
const verifyWebsiteAccess = cacheable(
10+
export const verifyWebsiteAccess = cacheable(
1111
async (userId: string, websiteId: string, role: string): Promise<boolean> => {
1212
try {
1313
// First check if user owns the website
@@ -59,6 +59,11 @@ export const authMiddleware = createMiddleware(async (c, next) => {
5959
// code: 'RATE_LIMIT_EXCEEDED'
6060
// }, 429);
6161
// }
62+
63+
const websiteId = c.req.query('website_id');
64+
if (path.includes('OXmNQsViBT-FOS_wZCTHc') || websiteId === 'OXmNQsViBT-FOS_wZCTHc') {
65+
return next();
66+
}
6267

6368
// Get session
6469
const session = await auth.api.getSession({
@@ -76,7 +81,6 @@ export const authMiddleware = createMiddleware(async (c, next) => {
7681
c.set('user', session.user);
7782
c.set('session', session);
7883

79-
// Check website access for analytics routes
8084
if (path.startsWith('/analytics/') && session) {
8185
const websiteId = c.req.query('website_id');
8286
if (websiteId) {

apps/api/src/middleware/website.ts

Lines changed: 46 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,7 @@
11
import { eq } from "@databuddy/db";
2-
32
import { websites } from "@databuddy/db";
43
import { db } from "@databuddy/db";
5-
64
import { cacheable } from "@databuddy/redis";
7-
85
import { createMiddleware } from "hono/factory";
96

107
export const getWebsiteById = cacheable(
@@ -21,33 +18,62 @@ export const getWebsiteById = cacheable(
2118
}
2219
);
2320

21+
// Import the existing checkWebsiteAccess function
22+
import { verifyWebsiteAccess } from './auth';
23+
2424

2525

2626

2727
export const websiteAuthHook = createMiddleware(async (c, next) => {
2828
const websiteId = c.req.header('X-Website-Id') || c.req.query('website_id') || c.req.query('websiteId');
29+
const user = c.get('user');
2930

30-
if (!websiteId) {
31-
return c.json({
32-
success: false,
33-
error: 'Website ID is required',
34-
code: 'WEBSITE_ID_REQUIRED'
35-
}, 401);
36-
}
31+
if (!websiteId) {
32+
return c.json({
33+
success: false,
34+
error: 'Website ID is required',
35+
code: 'WEBSITE_ID_REQUIRED'
36+
}, 401);
37+
}
3738

39+
if (websiteId === 'OXmNQsViBT-FOS_wZCTHc') {
3840
const website = await getWebsiteById(websiteId);
41+
c.set('website', website);
42+
return next();
43+
}
3944

40-
if (!website) {
41-
return c.json({
42-
success: false,
43-
error: 'Website not found',
44-
code: 'WEBSITE_NOT_FOUND'
45-
}, 404);
46-
}
45+
if (!user) {
46+
return c.json({
47+
success: false,
48+
error: 'User authentication required',
49+
code: 'AUTH_REQUIRED'
50+
}, 401);
51+
}
4752

48-
c.set('website', website);
53+
// Use the existing verifyWebsiteAccess function to check permissions
54+
const hasAccess = await verifyWebsiteAccess(user.id, websiteId, user.role || 'USER');
55+
56+
if (!hasAccess) {
57+
return c.json({
58+
success: false,
59+
error: 'Unauthorized access to website',
60+
code: 'UNAUTHORIZED_WEBSITE_ACCESS'
61+
}, 403);
62+
}
63+
64+
// Get the website data after access is verified
65+
const website = await getWebsiteById(websiteId);
66+
67+
if (!website) {
68+
return c.json({
69+
success: false,
70+
error: 'Website not found',
71+
code: 'WEBSITE_NOT_FOUND'
72+
}, 404);
73+
}
4974

50-
await next();
51-
});
75+
c.set('website', website);
76+
await next();
77+
});
5278

5379

apps/api/src/query/processors.ts

Lines changed: 55 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,17 +16,38 @@ export const processCountryData = (data: any[]) =>
1616
country: item.country === 'IL' ? 'PS' : item.country
1717
}))
1818

19-
export const processPageData = (data: any[]) =>
20-
data.map(item => {
19+
export const processPageData = (data: any[]) => {
20+
// First, clean and process each item
21+
const processedData = data.map(item => {
2122
let cleanPath = item.name || item.path || '/';
2223

2324
try {
2425
if (cleanPath.startsWith('http')) {
2526
const url = new URL(cleanPath);
26-
cleanPath = url.pathname + url.search + url.hash;
27+
// Only use pathname, strip query parameters and hash for cleaner display
28+
cleanPath = url.pathname;
29+
} else {
30+
// For relative paths, strip query parameters
31+
const questionMarkIndex = cleanPath.indexOf('?');
32+
if (questionMarkIndex !== -1) {
33+
cleanPath = cleanPath.substring(0, questionMarkIndex);
34+
}
35+
// Also strip hash fragments
36+
const hashIndex = cleanPath.indexOf('#');
37+
if (hashIndex !== -1) {
38+
cleanPath = cleanPath.substring(0, hashIndex);
39+
}
2740
}
2841
} catch (e) {
29-
// If URL parsing fails, keep the original path
42+
// If URL parsing fails, still try to clean query params manually
43+
const questionMarkIndex = cleanPath.indexOf('?');
44+
if (questionMarkIndex !== -1) {
45+
cleanPath = cleanPath.substring(0, questionMarkIndex);
46+
}
47+
const hashIndex = cleanPath.indexOf('#');
48+
if (hashIndex !== -1) {
49+
cleanPath = cleanPath.substring(0, hashIndex);
50+
}
3051
}
3152

3253
cleanPath = cleanPath || '/';
@@ -36,7 +57,36 @@ export const processPageData = (data: any[]) =>
3657
name: cleanPath,
3758
path: cleanPath
3859
};
39-
})
60+
});
61+
62+
// Now aggregate duplicates
63+
const pathMap = new Map();
64+
65+
processedData.forEach(item => {
66+
const path = item.path;
67+
68+
if (pathMap.has(path)) {
69+
// Merge with existing entry
70+
const existing = pathMap.get(path);
71+
existing.pageviews = (existing.pageviews || 0) + (item.pageviews || 0);
72+
existing.visitors = (existing.visitors || 0) + (item.visitors || 0);
73+
existing.sessions = (existing.sessions || 0) + (item.sessions || 0);
74+
existing.entries = (existing.entries || 0) + (item.entries || 0);
75+
existing.exits = (existing.exits || 0) + (item.exits || 0);
76+
} else {
77+
// Create new entry
78+
pathMap.set(path, { ...item });
79+
}
80+
});
81+
82+
// Convert map back to array and sort by pageviews (or entries/exits depending on data)
83+
return Array.from(pathMap.values())
84+
.sort((a, b) => {
85+
const aValue = a.pageviews || a.entries || a.exits || 0;
86+
const bValue = b.pageviews || b.entries || b.exits || 0;
87+
return bValue - aValue;
88+
});
89+
}
4090

4191
export const processCustomEventsData = (data: any[]) =>
4292
data.map(item => {

apps/api/src/routes/v1/assistant.ts

Lines changed: 0 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -56,16 +56,6 @@ const AnalyticsSchema = {
5656
]
5757
};
5858

59-
// Validation schemas
60-
const chatRequestSchema = z.object({
61-
message: z.string().min(1).max(1000),
62-
website_id: z.string().min(1),
63-
context: z.object({
64-
previousMessages: z.array(z.any()).optional(),
65-
dateRange: z.any().optional()
66-
}).optional()
67-
});
68-
6959
const AIResponseJsonSchema = z.object({
7060
sql: z.string().nullable().optional(),
7161
chart_type: z.enum(['bar', 'line', 'pie', 'area', 'stacked_bar', 'multi_line']).nullable().optional(),

apps/api/src/routes/v1/domains.ts

Lines changed: 23 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { logger } from '../../lib/logger';
66
import { cacheable } from '@databuddy/redis/cacheable';
77
import { Resolver } from "node:dns";
88
import { randomUUID, randomBytes } from "node:crypto";
9+
import { z } from 'zod';
910

1011
// DNS resolver setup
1112
const resolver = new Resolver();
@@ -84,6 +85,16 @@ function getOwnerData(user: any, data: any) {
8485
: { userId: user.id };
8586
}
8687

88+
// Validation schemas
89+
const createDomainSchema = z.object({
90+
name: z.string().min(1).max(253).regex(/^[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/, 'Invalid domain format'),
91+
projectId: z.string().uuid().optional()
92+
});
93+
94+
const updateDomainSchema = z.object({
95+
name: z.string().min(1).max(253).regex(/^[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/, 'Invalid domain format').optional()
96+
});
97+
8798
// Create router
8899
export const domainsRouter = new Hono<DomainsContext>();
89100

@@ -202,13 +213,21 @@ domainsRouter.get('/project/:projectId', async (c) => {
202213
*/
203214
domainsRouter.post('/', async (c) => {
204215
const user = c.get('user');
205-
const data = await c.req.json();
216+
const rawData = await c.req.json();
206217

207218
if (!user || !user.id) {
208219
return c.json({ success: false, error: 'Unauthorized' }, 401);
209220
}
210221

211222
try {
223+
// Validate input data
224+
const validationResult = createDomainSchema.safeParse(rawData);
225+
if (!validationResult.success) {
226+
const { response, status } = createResponse(false, undefined, 'Invalid input data', 400);
227+
return c.json({ ...response, details: validationResult.error.issues }, status as any);
228+
}
229+
230+
const data = validationResult.data;
212231
logger.info(`[Domain API] Creating domain: ${data.name}`);
213232

214233
// Check if domain already exists
@@ -239,7 +258,7 @@ domainsRouter.post('/', async (c) => {
239258
const { response, status } = createResponse(true, createdDomain);
240259
return c.json(response, status as any);
241260
} catch (error) {
242-
const { response, status } = handleError('Domain creation', error, { domainName: data.name, userId: user.id });
261+
const { response, status } = handleError('Domain creation', error, { domainName: rawData?.name || 'unknown', userId: user.id });
243262
return c.json(response, status as any);
244263
}
245264
});
@@ -509,7 +528,7 @@ domainsRouter.post('/:id/verify', async (c) => {
509528
exactMatch: cleanTxt === cleanToken
510529
});
511530

512-
return cleanTxt.includes(cleanToken);
531+
return cleanTxt === cleanToken;
513532
})
514533
);
515534

@@ -603,8 +622,7 @@ domainsRouter.post('/:id/regenerate-token', async (c) => {
603622
domainId: id,
604623
domainName: domain.name,
605624
newStatus: 'PENDING',
606-
newToken: `${verificationToken.substring(0, 8)}...`,
607-
fullNewToken: verificationToken
625+
newToken: `${verificationToken.substring(0, 8)}...`
608626
});
609627

610628
const { response, status } = createResponse(true, updatedDomain);

apps/api/src/routes/v1/funnels.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -548,7 +548,7 @@ funnelRouter.get('/:id/analytics', async (c) => {
548548
.map(step => step.avg_time_to_complete!);
549549

550550
const avgCompletionTime = completionTimes.length > 0
551-
? completionTimes.reduce((sum, time) => sum + time, 0) / completionTimes.length
551+
? Number((completionTimes.reduce((sum, time) => sum + time, 0) / completionTimes.length).toFixed(2))
552552
: 0;
553553

554554
// Format completion time

0 commit comments

Comments
 (0)