feat: add newsletter broadcast system with admin login and dynamic QR code redirect service with scan tracking.

This commit is contained in:
Timo
2026-01-02 18:07:18 +01:00
parent a15e3b67c2
commit 0302821f0f
7 changed files with 78 additions and 57 deletions

View File

@@ -8,7 +8,7 @@ export async function GET(
) {
try {
const { slug } = await params;
// Fetch QR code by slug
const qrCode = await db.qRCode.findUnique({
where: { slug },
@@ -29,7 +29,7 @@ export async function GET(
// Determine destination URL
let destination = '';
const content = qrCode.content as any;
switch (qrCode.contentType) {
case 'URL':
destination = content.url || 'https://example.com';
@@ -67,7 +67,7 @@ export async function GET(
const searchParams = request.nextUrl.searchParams;
const utmParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content'];
const preservedParams = new URLSearchParams();
utmParams.forEach(param => {
const value = searchParams.get(param);
if (value) {
@@ -94,8 +94,8 @@ async function trackScan(qrId: string, request: NextRequest) {
const userAgent = request.headers.get('user-agent') || '';
const referer = request.headers.get('referer') || '';
const ip = request.headers.get('x-forwarded-for') ||
request.headers.get('x-real-ip') ||
'unknown';
request.headers.get('x-real-ip') ||
'unknown';
// Check DNT header
const dnt = request.headers.get('dnt');
@@ -113,12 +113,11 @@ async function trackScan(qrId: string, request: NextRequest) {
// Hash IP for privacy
const ipHash = hashIP(ip);
// Parse user agent for device info
const isTablet = /tablet|ipad|playbook|silk|android(?!.*mobile)/i.test(userAgent);
const isMobile = /mobile|android|iphone/i.test(userAgent);
const isTablet = /tablet|ipad/i.test(userAgent);
const device = isTablet ? 'tablet' : isMobile ? 'mobile' : 'desktop';
// Detect OS
let os = 'unknown';
if (/windows/i.test(userAgent)) os = 'Windows';
@@ -126,22 +125,22 @@ async function trackScan(qrId: string, request: NextRequest) {
else if (/linux/i.test(userAgent)) os = 'Linux';
else if (/android/i.test(userAgent)) os = 'Android';
else if (/ios|iphone|ipad/i.test(userAgent)) os = 'iOS';
// Get country from header (Vercel/Cloudflare provide this)
const country = request.headers.get('x-vercel-ip-country') ||
request.headers.get('cf-ipcountry') ||
'unknown';
request.headers.get('cf-ipcountry') ||
'unknown';
// Extract UTM parameters
const searchParams = request.nextUrl.searchParams;
const utmSource = searchParams.get('utm_source');
const utmMedium = searchParams.get('utm_medium');
const utmCampaign = searchParams.get('utm_campaign');
// Check if this is a unique scan (first scan from this IP today)
const today = new Date();
today.setHours(0, 0, 0, 0);
const existingScan = await db.qRScan.findFirst({
where: {
qrId,
@@ -151,9 +150,9 @@ async function trackScan(qrId: string, request: NextRequest) {
},
},
});
const isUnique = !existingScan;
// Create scan record
await db.qRScan.create({
data: {