diff --git a/api/routes/analytics.js b/api/routes/analytics.js index 9133d2a..b0eae7f 100644 --- a/api/routes/analytics.js +++ b/api/routes/analytics.js @@ -22,8 +22,8 @@ router.get('/:qrCodeId', verifyToken, async (req, res) => { return res.status(403).json({ error: 'Unauthorized or QR code not found' }); } - // Get total scans - const totalQuery = await pgPool.query('SELECT COUNT(*) as total FROM qr_scans WHERE qr_code_id = $1', [qrCodeId]); + // Get total scans and unique scanners + const totalQuery = await pgPool.query('SELECT COUNT(*) as total, COUNT(DISTINCT ip_address) as unique_scanners FROM qr_scans WHERE qr_code_id = $1', [qrCodeId]); // Get OS breakdown const osQuery = await pgPool.query( @@ -37,6 +37,12 @@ router.get('/:qrCodeId', verifyToken, async (req, res) => { [qrCodeId] ); + // Get Country breakdown + const countryQuery = await pgPool.query( + 'SELECT country, COUNT(*) as count FROM qr_scans WHERE qr_code_id = $1 GROUP BY country ORDER BY count DESC', + [qrCodeId] + ); + // Get Scans over time (last 7 days grouped by date) const timeQuery = await pgPool.query( `SELECT DATE(scanned_at) as date, COUNT(*) as count @@ -46,11 +52,14 @@ router.get('/:qrCodeId', verifyToken, async (req, res) => { [qrCodeId] ); + // Parse string counts from PG back to integers for Recharts res.json({ - totalScans: parseInt(totalQuery.rows[0].total), - osBreakdown: osQuery.rows, - deviceBreakdown: deviceQuery.rows, - timeline: timeQuery.rows + totalScans: parseInt(totalQuery.rows[0].total) || 0, + uniqueScanners: parseInt(totalQuery.rows[0].unique_scanners) || 0, + osBreakdown: osQuery.rows.map(r => ({ ...r, count: parseInt(r.count) })), + deviceBreakdown: deviceQuery.rows.map(r => ({ ...r, count: parseInt(r.count) })), + countryBreakdown: countryQuery.rows.map(r => ({ country: r.country || 'Unknown', count: parseInt(r.count) })), + timeline: timeQuery.rows.map(r => ({ ...r, count: parseInt(r.count) })) }); } catch (err) { console.error(err); diff --git a/api/routes/redirect.js b/api/routes/redirect.js index ecfbdc7..e254ab7 100644 --- a/api/routes/redirect.js +++ b/api/routes/redirect.js @@ -53,12 +53,13 @@ router.get('/:shortUrlId', async (req, res) => { const parser = new UAParser(req.headers['user-agent']); const os = parser.getOS().name || 'Unknown'; const deviceType = parser.getDevice().type || 'desktop'; - const ip = req.ip || req.connection.remoteAddress; + const ip = req.headers['x-forwarded-for'] || req.connection.remoteAddress; + const country = req.headers['cf-ipcountry'] || 'Unknown'; pgPool.query( - `INSERT INTO qr_scans (qr_code_id, ip_address, user_agent, os, device_type) - VALUES ($1, $2, $3, $4, $5)`, - [qrData.qr_id, ip, req.headers['user-agent'], os, deviceType] + `INSERT INTO qr_scans (qr_code_id, ip_address, user_agent, os, device_type, country) + VALUES ($1, $2, $3, $4, $5, $6)`, + [qrData.qr_id, ip, req.headers['user-agent'], os, deviceType, country] ).catch(err => console.error('Failed to log scan:', err)); // 4. Check Tier & Redirect diff --git a/api/test.js b/api/test.js new file mode 100644 index 0000000..736af28 --- /dev/null +++ b/api/test.js @@ -0,0 +1,57 @@ +const http = require('http'); + +const options = { + hostname: 'localhost', + port: 4001, + path: '/api/auth/login', + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, +}; + +const req = http.request(options, (res) => { + let data = ''; + res.on('data', (chunk) => data += chunk); + res.on('end', () => { + if (!res.headers['set-cookie']) { + console.log('No cookie', data); return; + } + const cookie = res.headers['set-cookie'][0]; + + // Generate QR + const largeStr = 'a'.repeat(200000); + const postData = JSON.stringify({ + name: 'Test QR', + type: 'dynamic', + dataType: 'URL', + destinationUrl: 'https://google.com', + designData: { image: 'data:image/png;base64,' + largeStr } + }); + + const options2 = { + hostname: 'localhost', + port: 4001, + path: '/api/qr/generate', + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Content-Length': Buffer.byteLength(postData), + 'Cookie': cookie + } + }; + + const req2 = http.request(options2, (res2) => { + let d = ''; + res2.on('data', c => d+=c); + res2.on('end', () => { + console.log('Status:', res2.statusCode); + console.log('Body:', d); + }); + }); + req2.write(postData); + req2.end(); + }); +}); +req.write(JSON.stringify({email: 'admin@ckqr.com', password: 'password123'})); +req.end(); diff --git a/client/src/app/dashboard/analytics/page.tsx b/client/src/app/dashboard/analytics/page.tsx index 1851478..0fe2c97 100644 --- a/client/src/app/dashboard/analytics/page.tsx +++ b/client/src/app/dashboard/analytics/page.tsx @@ -22,9 +22,11 @@ interface QRCode { interface AnalyticsData { totalScans: number; - osBreakdown: { os: string; count: string }[]; - deviceBreakdown: { device_type: string; count: string }[]; - timeline: { date: string; count: string }[]; + uniqueScanners: number; + osBreakdown: { os: string; count: number }[]; + deviceBreakdown: { device_type: string; count: number }[]; + countryBreakdown: { country: string; count: number }[]; + timeline: { date: string; count: number }[]; } const COLORS = ['#4f46e5', '#10b981', '#f59e0b', '#ef4444', '#8b5cf6']; @@ -126,17 +128,18 @@ export default function AnalyticsPage() {
Available in Pro
+Distinct IP Addresses
Available in Pro
+By Country
Design a beautiful, dynamic QR code