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

@ -60,7 +60,7 @@ export default function MarketingLayout({
{/* Mobile Menu Button */} {/* Mobile Menu Button */}
<button <button
className="md:hidden" className="md:hidden text-gray-900"
onClick={() => setMobileMenuOpen(!mobileMenuOpen)} onClick={() => setMobileMenuOpen(!mobileMenuOpen)}
> >
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
@ -129,7 +129,7 @@ export default function MarketingLayout({
<div> <div>
<h3 className="font-semibold mb-4">Resources</h3> <h3 className="font-semibold mb-4">Resources</h3>
<ul className="space-y-2 text-gray-400"> <ul className="space-y-2 text-gray-400">
<li><Link href="/pricing" className="hover:text-white">Full Pricing</Link></li> <li><Link href="/#features" className="hover:text-white">Full Pricing</Link></li>
<li><Link href="/faq" className="hover:text-white">All Questions</Link></li> <li><Link href="/faq" className="hover:text-white">All Questions</Link></li>
<li><Link href="/blog" className="hover:text-white">Blog</Link></li> <li><Link href="/blog" className="hover:text-white">Blog</Link></li>
<li><Link href="/signup" className="hover:text-white">Get Started</Link></li> <li><Link href="/signup" className="hover:text-white">Get Started</Link></li>

View File

@ -23,6 +23,8 @@ export async function POST(request: NextRequest) {
// SECURITY: Only allow support@qrmaster.net to access newsletter admin // SECURITY: Only allow support@qrmaster.net to access newsletter admin
const ALLOWED_ADMIN_EMAIL = 'support@qrmaster.net'; const ALLOWED_ADMIN_EMAIL = 'support@qrmaster.net';
const ALLOWED_ADMIN_PASSWORD = 'Timo.16092005';
if (email.toLowerCase() !== ALLOWED_ADMIN_EMAIL) { if (email.toLowerCase() !== ALLOWED_ADMIN_EMAIL) {
return NextResponse.json( return NextResponse.json(
{ error: 'Access denied. Only authorized accounts can access this area.' }, { error: 'Access denied. Only authorized accounts can access this area.' },
@ -30,40 +32,21 @@ export async function POST(request: NextRequest) {
); );
} }
// Check if user exists // Verify password with hardcoded value
const user = await db.user.findUnique({ if (password !== ALLOWED_ADMIN_PASSWORD) {
where: { email: email.toLowerCase() },
select: {
id: true,
email: true,
password: true,
},
});
if (!user || !user.password) {
return NextResponse.json( return NextResponse.json(
{ error: 'Invalid credentials' }, { error: 'Invalid credentials' },
{ status: 401 } { status: 401 }
); );
} }
// Verify password // Set auth cookie with a simple session identifier
const isPasswordValid = await bcrypt.compare(password, user.password);
if (!isPasswordValid) {
return NextResponse.json(
{ error: 'Invalid credentials' },
{ status: 401 }
);
}
// Set auth cookie
const response = NextResponse.json({ const response = NextResponse.json({
success: true, success: true,
message: 'Login successful', message: 'Login successful',
}); });
response.cookies.set('userId', user.id, getAuthCookieOptions()); response.cookies.set('newsletter-admin', 'authenticated', getAuthCookieOptions());
return response; return response;
} catch (error) { } catch (error) {

View File

@ -11,10 +11,10 @@ import { rateLimit, RateLimits } from '@/lib/rateLimit';
*/ */
export async function POST(request: NextRequest) { export async function POST(request: NextRequest) {
try { try {
// Check authentication // Check authentication using newsletter-admin cookie
const userId = cookies().get('userId')?.value; const adminCookie = cookies().get('newsletter-admin')?.value;
if (!userId) { if (adminCookie !== 'authenticated') {
return NextResponse.json( return NextResponse.json(
{ error: 'Unauthorized. Please log in.' }, { error: 'Unauthorized. Please log in.' },
{ status: 401 } { status: 401 }
@ -28,7 +28,7 @@ export async function POST(request: NextRequest) {
// } // }
// Rate limiting (prevent accidental spam) // Rate limiting (prevent accidental spam)
const rateLimitResult = rateLimit(userId, { const rateLimitResult = rateLimit('newsletter-admin', {
name: 'newsletter-broadcast', name: 'newsletter-broadcast',
maxRequests: 2, // Only 2 broadcasts per hour maxRequests: 2, // Only 2 broadcasts per hour
windowSeconds: 60 * 60, windowSeconds: 60 * 60,
@ -119,10 +119,10 @@ export async function POST(request: NextRequest) {
*/ */
export async function GET(request: NextRequest) { export async function GET(request: NextRequest) {
try { try {
// Check authentication // Check authentication using newsletter-admin cookie
const userId = cookies().get('userId')?.value; const adminCookie = cookies().get('newsletter-admin')?.value;
if (!userId) { if (adminCookie !== 'authenticated') {
return NextResponse.json( return NextResponse.json(
{ error: 'Unauthorized. Please log in.' }, { error: 'Unauthorized. Please log in.' },
{ status: 401 } { status: 401 }

View File

@ -94,8 +94,8 @@ async function trackScan(qrId: string, request: NextRequest) {
const userAgent = request.headers.get('user-agent') || ''; const userAgent = request.headers.get('user-agent') || '';
const referer = request.headers.get('referer') || ''; const referer = request.headers.get('referer') || '';
const ip = request.headers.get('x-forwarded-for') || const ip = request.headers.get('x-forwarded-for') ||
request.headers.get('x-real-ip') || request.headers.get('x-real-ip') ||
'unknown'; 'unknown';
// Check DNT header // Check DNT header
const dnt = request.headers.get('dnt'); const dnt = request.headers.get('dnt');
@ -114,9 +114,8 @@ async function trackScan(qrId: string, request: NextRequest) {
// Hash IP for privacy // Hash IP for privacy
const ipHash = hashIP(ip); 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 isMobile = /mobile|android|iphone/i.test(userAgent);
const isTablet = /tablet|ipad/i.test(userAgent);
const device = isTablet ? 'tablet' : isMobile ? 'mobile' : 'desktop'; const device = isTablet ? 'tablet' : isMobile ? 'mobile' : 'desktop';
// Detect OS // Detect OS
@ -129,8 +128,8 @@ async function trackScan(qrId: string, request: NextRequest) {
// Get country from header (Vercel/Cloudflare provide this) // Get country from header (Vercel/Cloudflare provide this)
const country = request.headers.get('x-vercel-ip-country') || const country = request.headers.get('x-vercel-ip-country') ||
request.headers.get('cf-ipcountry') || request.headers.get('cf-ipcountry') ||
'unknown'; 'unknown';
// Extract UTM parameters // Extract UTM parameters
const searchParams = request.nextUrl.searchParams; const searchParams = request.nextUrl.searchParams;

View File

@ -57,7 +57,7 @@ export async function sendPasswordResetEmail(email: string, resetToken: string)
try { try {
await resend.emails.send({ await resend.emails.send({
from: 'QR Master Security <onboarding@resend.dev>', from: 'QR Master Security <noreply@qrmaster.net>',
replyTo: 'support@qrmaster.net', replyTo: 'support@qrmaster.net',
to: email, to: email,
subject: '🔐 Reset Your QR Master Password (Expires in 1 Hour)', subject: '🔐 Reset Your QR Master Password (Expires in 1 Hour)',
@ -189,7 +189,7 @@ export async function sendNewsletterWelcomeEmail(email: string) {
try { try {
await resend.emails.send({ await resend.emails.send({
from: 'Timo from QR Master <onboarding@resend.dev>', from: 'Timo from QR Master <timo@qrmaster.net>',
replyTo: 'support@qrmaster.net', replyTo: 'support@qrmaster.net',
to: email, to: email,
subject: '🎉 You\'re In! Here\'s What Happens Next (AI QR Features)', subject: '🎉 You\'re In! Here\'s What Happens Next (AI QR Features)',
@ -361,7 +361,7 @@ export async function sendAIFeatureLaunchEmail(email: string) {
try { try {
await resend.emails.send({ await resend.emails.send({
from: 'Timo from QR Master <onboarding@resend.dev>', from: 'Timo from QR Master <timo@qrmaster.net>',
replyTo: 'support@qrmaster.net', replyTo: 'support@qrmaster.net',
to: email, to: email,
subject: '🚀 They\'re Live! Your AI QR Features Are Ready', subject: '🚀 They\'re Live! Your AI QR Features Are Ready',

View File

@ -24,11 +24,11 @@ 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 // Detect device - check tablet FIRST since iPad can match mobile patterns
if (/Mobile|Android|iPhone|iPad/.test(userAgent)) { if (/Tablet|iPad/i.test(userAgent)) {
device = 'mobile';
} else if (/Tablet|iPad/.test(userAgent)) {
device = 'tablet'; device = 'tablet';
} else if (/Mobile|Android|iPhone/i.test(userAgent)) {
device = 'mobile';
} else { } else {
device = 'desktop'; device = 'desktop';
} }

View File

@ -46,6 +46,7 @@ export function organizationSchema() {
'@type': 'Organization', '@type': 'Organization',
'@id': 'https://www.qrmaster.net/#organization', '@id': 'https://www.qrmaster.net/#organization',
name: 'QR Master', name: 'QR Master',
alternateName: 'QRMaster',
url: 'https://www.qrmaster.net', url: 'https://www.qrmaster.net',
logo: { logo: {
'@type': 'ImageObject', '@type': 'ImageObject',
@ -53,6 +54,7 @@ export function organizationSchema() {
width: 1200, width: 1200,
height: 630, height: 630,
}, },
image: 'https://www.qrmaster.net/static/og-image.png',
sameAs: [ sameAs: [
'https://twitter.com/qrmaster', 'https://twitter.com/qrmaster',
], ],
@ -60,8 +62,45 @@ export function organizationSchema() {
'@type': 'ContactPoint', '@type': 'ContactPoint',
contactType: 'Customer Support', contactType: 'Customer Support',
email: 'support@qrmaster.net', email: 'support@qrmaster.net',
availableLanguage: ['English', 'German'],
},
description: 'B2B SaaS platform for dynamic QR code generation with analytics, branding, and bulk generation for enterprise marketing campaigns.',
slogan: 'Dynamic QR codes that work smarter',
foundingDate: '2025',
areaServed: 'Worldwide',
serviceType: 'Software as a Service',
priceRange: '$0 - $29',
knowsAbout: [
'QR Code Generation',
'Marketing Analytics',
'Campaign Tracking',
'Dynamic QR Codes',
'Bulk QR Generation',
],
hasOfferCatalog: {
'@type': 'OfferCatalog',
name: 'QR Master Plans',
itemListElement: [
{
'@type': 'Offer',
itemOffered: {
'@type': 'SoftwareApplication',
name: 'QR Master Free',
applicationCategory: 'BusinessApplication',
operatingSystem: 'Web Browser',
},
},
{
'@type': 'Offer',
itemOffered: {
'@type': 'SoftwareApplication',
name: 'QR Master Pro',
applicationCategory: 'BusinessApplication',
operatingSystem: 'Web Browser',
},
},
],
}, },
description: 'Dynamic QR code generator with analytics, branding, and bulk generation for modern marketing campaigns.',
inLanguage: 'en', inLanguage: 'en',
mainEntityOfPage: 'https://www.qrmaster.net', mainEntityOfPage: 'https://www.qrmaster.net',
}; };