feat: Implement dynamic QR code redirection with comprehensive scan tracking, device/OS detection, and geo-location.

This commit is contained in:
Timo 2026-01-02 19:47:43 +01:00
parent d0a114c1c3
commit 49673e84b6
2 changed files with 48 additions and 7 deletions

View File

@ -114,9 +114,40 @@ async function trackScan(qrId: string, request: NextRequest) {
// Hash IP for privacy // Hash IP for privacy
const ipHash = hashIP(ip); const ipHash = hashIP(ip);
const isTablet = /tablet|ipad|playbook|silk|android(?!.*mobile)/i.test(userAgent); // Device Detection Logic:
const isMobile = /mobile|android|iphone/i.test(userAgent); // 1. Windows or Linux -> Always Desktop
const device = isTablet ? 'tablet' : isMobile ? 'mobile' : 'desktop'; // 2. Explicit iPad/Tablet keywords -> Tablet
// 3. Mac + Chrome browser -> Desktop (real Mac users often use Chrome)
// 4. Mac + Safari + No Referrer -> Likely iPad scanning a QR code
// 5. Mobile keywords -> Mobile
// 6. Everything else -> Desktop
const isWindows = /windows/i.test(userAgent);
const isLinux = /linux/i.test(userAgent) && !/android/i.test(userAgent);
const isExplicitTablet = /tablet|ipad|playbook|silk/i.test(userAgent);
const isAndroidTablet = /android/i.test(userAgent) && !/mobile/i.test(userAgent);
const isMacintosh = /macintosh/i.test(userAgent);
const isChrome = /chrome/i.test(userAgent);
const isSafari = /safari/i.test(userAgent) && !isChrome;
const hasReferrer = !!referer;
// iPad in desktop mode: Mac + Safari (no Chrome) + No Referrer (physical scan)
const isLikelyiPadScan = isMacintosh && isSafari && !hasReferrer;
let device: string;
if (isWindows || isLinux) {
device = 'desktop';
} else if (isExplicitTablet || isAndroidTablet || isLikelyiPadScan) {
device = 'tablet';
} else if (/mobile|iphone/i.test(userAgent)) {
device = 'mobile';
} else if (isMacintosh && isChrome) {
device = 'desktop'; // Mac with Chrome = real desktop
} else if (isMacintosh && hasReferrer) {
device = 'desktop'; // Mac with referrer = probably clicked a link on desktop
} else {
device = 'desktop'; // Default fallback
}
// Detect OS // Detect OS
let os = 'unknown'; let os = 'unknown';
@ -137,7 +168,9 @@ async function trackScan(qrId: string, request: NextRequest) {
const utmMedium = searchParams.get('utm_medium'); const utmMedium = searchParams.get('utm_medium');
const utmCampaign = searchParams.get('utm_campaign'); const utmCampaign = searchParams.get('utm_campaign');
// Check if this is a unique scan (first scan from this IP today) // Check if this is a unique scan (first scan from this IP + Device today)
// We include a simplified device fingerprint so different devices on same IP count as unique
const deviceFingerprint = hashIP(userAgent.substring(0, 100)); // Hash the user agent for privacy
const today = new Date(); const today = new Date();
today.setHours(0, 0, 0, 0); today.setHours(0, 0, 0, 0);
@ -145,6 +178,9 @@ async function trackScan(qrId: string, request: NextRequest) {
where: { where: {
qrId, qrId,
ipHash, ipHash,
userAgent: {
startsWith: userAgent.substring(0, 50), // Match same device type
},
ts: { ts: {
gte: today, gte: today,
}, },

View File

@ -24,10 +24,15 @@ export function parseUserAgent(userAgent: string | null): { device: string | nul
let device: string | null = null; let device: string | null = null;
let os: string | null = null; let os: string | null = null;
// Detect device - check tablet FIRST since iPad can match mobile patterns // Detect device
if (/Tablet|iPad/i.test(userAgent)) { // iPadOS 13+ sends "Macintosh" user agent.
// Without referrer info here, we fall back to checking for Safari-only Mac UAs (common for iPad)
const isIPad = /iPad/i.test(userAgent) ||
(/Macintosh/i.test(userAgent) && /Safari/i.test(userAgent) && !/Chrome/i.test(userAgent));
if (isIPad || /Tablet|PlayBook|Silk/i.test(userAgent)) {
device = 'tablet'; device = 'tablet';
} else if (/Mobile|Android|iPhone/i.test(userAgent)) { } else if (/Mobile|Android|iPhone/i.test(userAgent) && !isIPad) {
device = 'mobile'; device = 'mobile';
} else { } else {
device = 'desktop'; device = 'desktop';