This commit is contained in:
Timo Knuth 2025-10-15 00:03:05 +02:00
parent bccf771ffc
commit cd3ee5fc8f
15 changed files with 1096 additions and 186 deletions

View File

@ -2,7 +2,20 @@
"permissions": { "permissions": {
"allow": [ "allow": [
"Bash(docker-compose:*)", "Bash(docker-compose:*)",
"Bash(docker container prune:*)" "Bash(docker container prune:*)",
"Bash(npx prisma migrate dev:*)",
"Bash(npx prisma:*)",
"Bash(npm run dev)",
"Bash(timeout:*)",
"Bash(taskkill:*)",
"Bash(npx kill-port:*)",
"Bash(docker compose:*)",
"Bash(curl -I https://fonts.googleapis.com)",
"Bash(wsl:*)",
"Read(//c/Users/a931627/.ssh/**)",
"Bash(ssh-keygen:*)",
"Bash(cat:*)",
"Bash(git remote add:*)"
], ],
"deny": [], "deny": [],
"ask": [] "ask": []

View File

@ -10,7 +10,7 @@ services:
POSTGRES_DB: qrmaster POSTGRES_DB: qrmaster
POSTGRES_INITDB_ARGS: "-E UTF8 --locale=en_US.utf8" POSTGRES_INITDB_ARGS: "-E UTF8 --locale=en_US.utf8"
ports: ports:
- "5432:5432" - "5435:5432"
volumes: volumes:
- dbdata:/var/lib/postgresql/data - dbdata:/var/lib/postgresql/data
- ./docker/init-db.sh:/docker-entrypoint-initdb.d/init-db.sh - ./docker/init-db.sh:/docker-entrypoint-initdb.d/init-db.sh

746
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -27,6 +27,7 @@
"dependencies": { "dependencies": {
"@auth/prisma-adapter": "^1.0.12", "@auth/prisma-adapter": "^1.0.12",
"@prisma/client": "^5.7.0", "@prisma/client": "^5.7.0",
"@stripe/stripe-js": "^8.0.0",
"bcryptjs": "^2.4.3", "bcryptjs": "^2.4.3",
"chart.js": "^4.4.0", "chart.js": "^4.4.0",
"clsx": "^2.0.0", "clsx": "^2.0.0",
@ -44,6 +45,7 @@
"react-dropzone": "^14.2.3", "react-dropzone": "^14.2.3",
"react-i18next": "^13.5.0", "react-i18next": "^13.5.0",
"sharp": "^0.33.1", "sharp": "^0.33.1",
"stripe": "^19.1.0",
"tailwind-merge": "^2.2.0", "tailwind-merge": "^2.2.0",
"xlsx": "^0.18.5", "xlsx": "^0.18.5",
"zod": "^3.22.4" "zod": "^3.22.4"

View File

@ -1,6 +1,7 @@
'use client'; 'use client';
import React, { useState, useEffect } from 'react'; import React, { useState, useEffect } from 'react';
import Link from 'next/link';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
import { QRCodeSVG } from 'qrcode.react'; import { QRCodeSVG } from 'qrcode.react';
import { Card, CardHeader, CardTitle, CardContent } from '@/components/ui/Card'; import { Card, CardHeader, CardTitle, CardContent } from '@/components/ui/Card';
@ -16,6 +17,7 @@ export default function CreatePage() {
const router = useRouter(); const router = useRouter();
const { t } = useTranslation(); const { t } = useTranslation();
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [userPlan, setUserPlan] = useState<string>('FREE');
// Form state // Form state
const [title, setTitle] = useState(''); const [title, setTitle] = useState('');
@ -33,6 +35,25 @@ export default function CreatePage() {
// QR preview // QR preview
const [qrDataUrl, setQrDataUrl] = useState(''); const [qrDataUrl, setQrDataUrl] = useState('');
// Check if user can customize colors (PRO+ only)
const canCustomizeColors = userPlan === 'PRO' || userPlan === 'BUSINESS';
// Load user plan
useEffect(() => {
const fetchUserPlan = async () => {
try {
const response = await fetch('/api/user/plan');
if (response.ok) {
const data = await response.json();
setUserPlan(data.plan || 'FREE');
}
} catch (error) {
console.error('Error fetching user plan:', error);
}
};
fetchUserPlan();
}, []);
const contrast = calculateContrast(foregroundColor, backgroundColor); const contrast = calculateContrast(foregroundColor, backgroundColor);
const hasGoodContrast = contrast >= 4.5; const hasGoodContrast = contrast >= 4.5;
@ -141,8 +162,9 @@ export default function CreatePage() {
isStatic: !isDynamic, // Add this flag isStatic: !isDynamic, // Add this flag
tags: tags.split(',').map(t => t.trim()).filter(Boolean), tags: tags.split(',').map(t => t.trim()).filter(Boolean),
style: { style: {
foregroundColor, // FREE users can only use black/white
backgroundColor, foregroundColor: canCustomizeColors ? foregroundColor : '#000000',
backgroundColor: canCustomizeColors ? backgroundColor : '#FFFFFF',
cornerStyle, cornerStyle,
size, size,
}, },
@ -354,9 +376,26 @@ export default function CreatePage() {
{/* Style Section */} {/* Style Section */}
<Card> <Card>
<CardHeader> <CardHeader>
<div className="flex items-center justify-between">
<CardTitle>{t('create.style')}</CardTitle> <CardTitle>{t('create.style')}</CardTitle>
{!canCustomizeColors && (
<Badge variant="warning">PRO Feature</Badge>
)}
</div>
</CardHeader> </CardHeader>
<CardContent className="space-y-4"> <CardContent className="space-y-4">
{!canCustomizeColors && (
<div className="p-4 bg-blue-50 border border-blue-200 rounded-lg mb-4">
<p className="text-sm text-blue-900">
<strong>Upgrade to PRO</strong> to customize colors, add logos, and brand your QR codes.
</p>
<Link href="/pricing">
<Button variant="primary" size="sm" className="mt-2">
Upgrade Now
</Button>
</Link>
</div>
)}
<div className="grid grid-cols-2 gap-4"> <div className="grid grid-cols-2 gap-4">
<div> <div>
<label className="block text-sm font-medium text-gray-700 mb-2"> <label className="block text-sm font-medium text-gray-700 mb-2">
@ -368,11 +407,13 @@ export default function CreatePage() {
value={foregroundColor} value={foregroundColor}
onChange={(e) => setForegroundColor(e.target.value)} onChange={(e) => setForegroundColor(e.target.value)}
className="w-12 h-10 rounded border border-gray-300" className="w-12 h-10 rounded border border-gray-300"
disabled={!canCustomizeColors}
/> />
<Input <Input
value={foregroundColor} value={foregroundColor}
onChange={(e) => setForegroundColor(e.target.value)} onChange={(e) => setForegroundColor(e.target.value)}
className="flex-1" className="flex-1"
disabled={!canCustomizeColors}
/> />
</div> </div>
</div> </div>
@ -387,11 +428,13 @@ export default function CreatePage() {
value={backgroundColor} value={backgroundColor}
onChange={(e) => setBackgroundColor(e.target.value)} onChange={(e) => setBackgroundColor(e.target.value)}
className="w-12 h-10 rounded border border-gray-300" className="w-12 h-10 rounded border border-gray-300"
disabled={!canCustomizeColors}
/> />
<Input <Input
value={backgroundColor} value={backgroundColor}
onChange={(e) => setBackgroundColor(e.target.value)} onChange={(e) => setBackgroundColor(e.target.value)}
className="flex-1" className="flex-1"
disabled={!canCustomizeColors}
/> />
</div> </div>
</div> </div>

View File

@ -2,13 +2,15 @@
import React, { useState, useEffect } from 'react'; import React, { useState, useEffect } from 'react';
import Link from 'next/link'; import Link from 'next/link';
import { useSession } from 'next-auth/react'; import { useRouter, useSearchParams } from 'next/navigation';
import { StatsGrid } from '@/components/dashboard/StatsGrid'; import { StatsGrid } from '@/components/dashboard/StatsGrid';
import { QRCodeCard } from '@/components/dashboard/QRCodeCard'; import { QRCodeCard } from '@/components/dashboard/QRCodeCard';
import { Card, CardHeader, CardTitle, CardContent } from '@/components/ui/Card'; import { Card, CardHeader, CardTitle, CardContent } from '@/components/ui/Card';
import { Button } from '@/components/ui/Button'; import { Button } from '@/components/ui/Button';
import { Badge } from '@/components/ui/Badge'; import { Badge } from '@/components/ui/Badge';
import { useTranslation } from '@/hooks/useTranslation'; import { useTranslation } from '@/hooks/useTranslation';
import { showToast } from '@/components/ui/Toast';
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogFooter } from '@/components/ui/Dialog';
interface QRCodeData { interface QRCodeData {
id: string; id: string;
@ -24,10 +26,13 @@ interface QRCodeData {
export default function DashboardPage() { export default function DashboardPage() {
const { t } = useTranslation(); const { t } = useTranslation();
const { data: session } = useSession(); const router = useRouter();
const searchParams = useSearchParams();
const [qrCodes, setQrCodes] = useState<QRCodeData[]>([]); const [qrCodes, setQrCodes] = useState<QRCodeData[]>([]);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [userPlan, setUserPlan] = useState<string>('FREE'); const [userPlan, setUserPlan] = useState<string>('FREE');
const [showUpgradeDialog, setShowUpgradeDialog] = useState(false);
const [upgradedPlan, setUpgradedPlan] = useState<string>('');
const [stats, setStats] = useState({ const [stats, setStats] = useState({
totalScans: 0, totalScans: 0,
activeQRCodes: 0, activeQRCodes: 0,
@ -118,6 +123,35 @@ export default function DashboardPage() {
}, },
]; ];
// Check for successful payment and verify session
useEffect(() => {
const success = searchParams.get('success');
if (success === 'true') {
const verifySession = async () => {
try {
const response = await fetch('/api/stripe/verify-session', {
method: 'POST',
});
if (response.ok) {
const data = await response.json();
setUserPlan(data.plan);
setUpgradedPlan(data.plan);
setShowUpgradeDialog(true);
// Remove success parameter from URL
router.replace('/dashboard');
} else {
console.error('Failed to verify session:', await response.text());
}
} catch (error) {
console.error('Error verifying session:', error);
}
};
verifySession();
}
}, [searchParams, router]);
useEffect(() => { useEffect(() => {
// Load real QR codes and user plan from API // Load real QR codes and user plan from API
const fetchData = async () => { const fetchData = async () => {
@ -148,14 +182,12 @@ export default function DashboardPage() {
}); });
} }
// Fetch user plan // Fetch user plan (using cookie-based auth, no session needed)
if (session?.user?.email) {
const userResponse = await fetch('/api/user/plan'); const userResponse = await fetch('/api/user/plan');
if (userResponse.ok) { if (userResponse.ok) {
const userData = await userResponse.json(); const userData = await userResponse.json();
setUserPlan(userData.plan || 'FREE'); setUserPlan(userData.plan || 'FREE');
} }
}
} catch (error) { } catch (error) {
console.error('Error fetching data:', error); console.error('Error fetching data:', error);
setQrCodes([]); setQrCodes([]);
@ -170,7 +202,7 @@ export default function DashboardPage() {
}; };
fetchData(); fetchData();
}, [session]); }, []);
const handleEdit = (id: string) => { const handleEdit = (id: string) => {
console.log('Edit QR:', id); console.log('Edit QR:', id);
@ -200,16 +232,8 @@ export default function DashboardPage() {
}; };
const getPlanEmoji = (plan: string) => { const getPlanEmoji = (plan: string) => {
switch (plan) { // No emojis anymore
case 'FREE': return '';
return '🟢';
case 'PRO':
return '🔵';
case 'BUSINESS':
return '🟣';
default:
return '⚪';
}
}; };
return ( return (
@ -222,7 +246,7 @@ export default function DashboardPage() {
</div> </div>
<div className="flex items-center space-x-3"> <div className="flex items-center space-x-3">
<Badge variant={getPlanBadgeColor(userPlan)} className="text-lg px-4 py-2"> <Badge variant={getPlanBadgeColor(userPlan)} className="text-lg px-4 py-2">
{getPlanEmoji(userPlan)} {userPlan} Plan {userPlan} Plan
</Badge> </Badge>
{userPlan === 'FREE' && ( {userPlan === 'FREE' && (
<Link href="/pricing"> <Link href="/pricing">
@ -302,6 +326,89 @@ export default function DashboardPage() {
))} ))}
</div> </div>
</div> </div>
{/* Upgrade Success Dialog */}
<Dialog open={showUpgradeDialog} onOpenChange={setShowUpgradeDialog}>
<DialogContent>
<DialogHeader>
<DialogTitle className="text-2xl text-center">
Upgrade erfolgreich!
</DialogTitle>
<DialogDescription className="text-center text-base pt-4">
Willkommen im <strong>{upgradedPlan} Plan</strong>! Ihr Konto wurde erfolgreich aktualisiert.
</DialogDescription>
</DialogHeader>
<div className="py-6 space-y-4">
<div className="bg-gradient-to-r from-blue-50 to-indigo-50 p-6 rounded-lg border border-blue-200">
<h3 className="font-semibold text-gray-900 mb-3">Ihre neuen Features:</h3>
<ul className="space-y-2 text-sm text-gray-700">
{upgradedPlan === 'PRO' && (
<>
<li className="flex items-start">
<span className="text-green-600 mr-2"></span>
<span>50 dynamische QR-Codes</span>
</li>
<li className="flex items-start">
<span className="text-green-600 mr-2"></span>
<span>Branding (Logo, Farben anpassen)</span>
</li>
<li className="flex items-start">
<span className="text-green-600 mr-2"></span>
<span>Detaillierte Analytics</span>
</li>
<li className="flex items-start">
<span className="text-green-600 mr-2"></span>
<span>CSV-Export</span>
</li>
<li className="flex items-start">
<span className="text-green-600 mr-2"></span>
<span>Passwortschutz für QR-Codes</span>
</li>
</>
)}
{upgradedPlan === 'BUSINESS' && (
<>
<li className="flex items-start">
<span className="text-green-600 mr-2"></span>
<span>500 dynamische QR-Codes</span>
</li>
<li className="flex items-start">
<span className="text-green-600 mr-2"></span>
<span>Team-Zugänge (bis zu 3 User)</span>
</li>
<li className="flex items-start">
<span className="text-green-600 mr-2"></span>
<span>Benutzerdefinierte Domains</span>
</li>
<li className="flex items-start">
<span className="text-green-600 mr-2"></span>
<span>White-Label</span>
</li>
<li className="flex items-start">
<span className="text-green-600 mr-2"></span>
<span>Prioritäts-Support</span>
</li>
</>
)}
</ul>
</div>
</div>
<DialogFooter>
<Button
variant="primary"
onClick={() => {
setShowUpgradeDialog(false);
router.push('/create');
}}
className="w-full"
>
Ersten QR-Code erstellen
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</div> </div>
); );
} }

View File

@ -1,8 +1,7 @@
'use client'; 'use client';
import React, { useState } from 'react'; import React, { useState, useEffect } from 'react';
import { useRouter } from 'next/navigation'; import { useRouter, useSearchParams } from 'next/navigation';
import { useSession } from 'next-auth/react';
import { Card, CardHeader, CardTitle, CardContent } from '@/components/ui/Card'; import { Card, CardHeader, CardTitle, CardContent } from '@/components/ui/Card';
import { Button } from '@/components/ui/Button'; import { Button } from '@/components/ui/Button';
import { Badge } from '@/components/ui/Badge'; import { Badge } from '@/components/ui/Badge';
@ -10,15 +9,25 @@ import { showToast } from '@/components/ui/Toast';
export default function PricingPage() { export default function PricingPage() {
const router = useRouter(); const router = useRouter();
const { data: session } = useSession(); const searchParams = useSearchParams();
const [user, setUser] = useState<any>(null);
const [loading, setLoading] = useState<string | null>(null); const [loading, setLoading] = useState<string | null>(null);
const [billingInterval, setBillingInterval] = useState<'monthly' | 'yearly'>('monthly'); const [billingInterval, setBillingInterval] = useState<'monthly' | 'yearly'>('monthly');
const [hasTriggeredCheckout, setHasTriggeredCheckout] = useState(false);
// Check for user in localStorage
useEffect(() => {
const storedUser = localStorage.getItem('user');
if (storedUser) {
setUser(JSON.parse(storedUser));
}
}, []);
const plans = [ const plans = [
{ {
id: 'FREE', id: 'FREE',
name: 'Free / Starter', name: 'Free / Starter',
icon: '🟢', icon: '',
price: 0, price: 0,
priceYearly: 0, priceYearly: 0,
description: 'Privatnutzer & Testkunden', description: 'Privatnutzer & Testkunden',
@ -36,7 +45,7 @@ export default function PricingPage() {
{ {
id: 'PRO', id: 'PRO',
name: 'Pro', name: 'Pro',
icon: '🔵', icon: '',
price: 9, price: 9,
priceYearly: 90, priceYearly: 90,
description: 'Selbstständige / kleine Firmen', description: 'Selbstständige / kleine Firmen',
@ -55,14 +64,13 @@ export default function PricingPage() {
{ {
id: 'BUSINESS', id: 'BUSINESS',
name: 'Business', name: 'Business',
icon: '🟣', icon: '',
price: 29, price: 29,
priceYearly: 290, priceYearly: 290,
description: 'Agenturen / Startups', description: 'Agenturen / Startups',
features: [ features: [
'500 QR-Codes', '500 QR-Codes',
'Team-Zugänge (bis 3 User)', 'Team-Zugänge (bis 3 User)',
'API-Zugang',
'Benutzerdefinierte Domains', 'Benutzerdefinierte Domains',
'White-Label', 'White-Label',
'Prioritäts-Support', 'Prioritäts-Support',
@ -75,8 +83,24 @@ export default function PricingPage() {
]; ];
const handleSubscribe = async (planId: string, priceId: string | null | undefined) => { const handleSubscribe = async (planId: string, priceId: string | null | undefined) => {
if (!session) { console.log('🔵 handleSubscribe called:', { planId, priceId, hasUser: !!user });
router.push('/login?redirect=/pricing');
if (!user) {
// Save the plan selection in localStorage so we can continue after login
const pendingPlan = {
planId,
interval: billingInterval,
};
console.log('💾 Saving pending plan to localStorage:', pendingPlan);
localStorage.setItem('pendingPlan', JSON.stringify(pendingPlan));
// Verify it was saved
const saved = localStorage.getItem('pendingPlan');
console.log('✅ Verified saved:', saved);
// Use window.location instead of router.push to ensure localStorage is written
console.log('🔄 Redirecting to login...');
window.location.href = '/login?redirect=/pricing';
return; return;
} }
@ -99,6 +123,7 @@ export default function PricingPage() {
body: JSON.stringify({ body: JSON.stringify({
priceId, priceId,
plan: planId, plan: planId,
userEmail: user.email,
}), }),
}); });
@ -117,6 +142,67 @@ export default function PricingPage() {
} }
}; };
// Auto-trigger checkout after login if plan is selected
useEffect(() => {
console.log('Pricing useEffect triggered:', {
hasUser: !!user,
hasTriggeredCheckout,
});
// Only run once and only when authenticated
if (hasTriggeredCheckout) {
console.log('Already triggered checkout, skipping...');
return;
}
if (!user) {
console.log('Not authenticated - no user in localStorage');
return;
}
// Check for pending plan in localStorage
const pendingPlanStr = localStorage.getItem('pendingPlan');
if (pendingPlanStr) {
try {
const pendingPlan = JSON.parse(pendingPlanStr);
console.log('✅ Found pending plan:', pendingPlan);
// Clear pending plan immediately
localStorage.removeItem('pendingPlan');
// Mark as triggered to prevent re-runs
setHasTriggeredCheckout(true);
// Set the billing interval
setBillingInterval(pendingPlan.interval);
// Find the plan
const selectedPlan = plans.find((p) => p.id === pendingPlan.planId);
if (selectedPlan) {
const priceId =
pendingPlan.interval === 'yearly'
? selectedPlan.priceIdYearly
: selectedPlan.priceIdMonthly;
console.log('✅ Found plan and priceId:', selectedPlan.name, priceId);
// Trigger checkout after a short delay
setTimeout(() => {
console.log('🚀 Calling handleSubscribe now...');
handleSubscribe(selectedPlan.id, priceId);
}, 500);
} else {
console.error('❌ Plan not found:', pendingPlan.planId);
}
} catch (e) {
console.error('Error parsing pending plan:', e);
localStorage.removeItem('pendingPlan');
}
} else {
console.log('No pending plan in localStorage');
}
}, [user, hasTriggeredCheckout]);
return ( return (
<div className="container mx-auto px-4 py-12"> <div className="container mx-auto px-4 py-12">
<div className="text-center mb-12"> <div className="text-center mb-12">
@ -180,7 +266,7 @@ export default function PricingPage() {
)} )}
<CardHeader className="text-center pb-6"> <CardHeader className="text-center pb-6">
<div className="text-4xl mb-4">{plan.icon}</div> {plan.icon && <div className="text-4xl mb-4">{plan.icon}</div>}
<CardTitle className="text-2xl mb-2">{plan.name}</CardTitle> <CardTitle className="text-2xl mb-2">{plan.name}</CardTitle>
<p className="text-sm text-gray-600">{plan.description}</p> <p className="text-sm text-gray-600">{plan.description}</p>

View File

@ -1,7 +1,7 @@
'use client'; 'use client';
import React, { useState } from 'react'; import React, { useState, useEffect } from 'react';
import { useRouter } from 'next/navigation'; import { useRouter, useSearchParams } from 'next/navigation';
import Link from 'next/link'; import Link from 'next/link';
import { Card, CardHeader, CardTitle, CardContent } from '@/components/ui/Card'; import { Card, CardHeader, CardTitle, CardContent } from '@/components/ui/Card';
import { Input } from '@/components/ui/Input'; import { Input } from '@/components/ui/Input';
@ -10,6 +10,7 @@ import { useTranslation } from '@/hooks/useTranslation';
export default function LoginPage() { export default function LoginPage() {
const router = useRouter(); const router = useRouter();
const searchParams = useSearchParams();
const { t } = useTranslation(); const { t } = useTranslation();
const [email, setEmail] = useState(''); const [email, setEmail] = useState('');
const [password, setPassword] = useState(''); const [password, setPassword] = useState('');
@ -33,7 +34,10 @@ export default function LoginPage() {
if (response.ok && data.success) { if (response.ok && data.success) {
// Store user in localStorage for client-side // Store user in localStorage for client-side
localStorage.setItem('user', JSON.stringify(data.user)); localStorage.setItem('user', JSON.stringify(data.user));
router.push('/dashboard');
// Check for redirect parameter
const redirectUrl = searchParams.get('redirect') || '/dashboard';
router.push(redirectUrl);
router.refresh(); router.refresh();
} else { } else {
setError(data.error || 'Invalid email or password'); setError(data.error || 'Invalid email or password');

View File

@ -1,15 +0,0 @@
'use client';
import React from 'react';
import { Pricing } from '@/components/marketing/Pricing';
import { useTranslation } from '@/hooks/useTranslation';
export default function PricingPage() {
const { t } = useTranslation();
return (
<div className="py-20">
<Pricing t={t} />
</div>
);
}

View File

@ -38,6 +38,13 @@ export async function GET(request: NextRequest) {
} }
} }
// Plan limits
const PLAN_LIMITS = {
FREE: 3,
PRO: 50,
BUSINESS: 500,
};
// POST /api/qrs - Create a new QR code // POST /api/qrs - Create a new QR code
export async function POST(request: NextRequest) { export async function POST(request: NextRequest) {
try { try {
@ -48,14 +55,15 @@ export async function POST(request: NextRequest) {
return NextResponse.json({ error: 'Unauthorized - no userId cookie' }, { status: 401 }); return NextResponse.json({ error: 'Unauthorized - no userId cookie' }, { status: 401 });
} }
// Check if user exists // Check if user exists and get their plan
const userExists = await db.user.findUnique({ const user = await db.user.findUnique({
where: { id: userId } where: { id: userId },
select: { plan: true },
}); });
console.log('User exists:', !!userExists); console.log('User exists:', !!user);
if (!userExists) { if (!user) {
return NextResponse.json({ error: `User not found: ${userId}` }, { status: 404 }); return NextResponse.json({ error: `User not found: ${userId}` }, { status: 404 });
} }
@ -65,6 +73,33 @@ export async function POST(request: NextRequest) {
// Check if this is a static QR request // Check if this is a static QR request
const isStatic = body.isStatic === true; const isStatic = body.isStatic === true;
// Only check limits for DYNAMIC QR codes (static QR codes are unlimited)
if (!isStatic) {
// Count existing dynamic QR codes
const dynamicQRCount = await db.qRCode.count({
where: {
userId,
type: 'DYNAMIC',
},
});
const userPlan = user.plan || 'FREE';
const limit = PLAN_LIMITS[userPlan as keyof typeof PLAN_LIMITS] || PLAN_LIMITS.FREE;
if (dynamicQRCount >= limit) {
return NextResponse.json(
{
error: 'Limit reached',
message: `You have reached the limit of ${limit} dynamic QR codes for your ${userPlan} plan. Please upgrade to create more.`,
currentCount: dynamicQRCount,
limit,
plan: userPlan,
},
{ status: 403 }
);
}
}
let enrichedContent = body.content; let enrichedContent = body.content;
// For STATIC QR codes, calculate what the QR should contain // For STATIC QR codes, calculate what the QR should contain

View File

@ -1,19 +1,17 @@
import { NextRequest, NextResponse } from 'next/server'; import { NextRequest, NextResponse } from 'next/server';
import { getServerSession } from 'next-auth';
import { authOptions } from '@/lib/auth';
import { stripe } from '@/lib/stripe'; import { stripe } from '@/lib/stripe';
import { db } from '@/lib/db'; import { db } from '@/lib/db';
import { cookies } from 'next/headers';
export async function POST(request: NextRequest) { export async function POST(request: NextRequest) {
try { try {
const session = await getServerSession(authOptions); // Get user email from request body (since we're using simple auth, not NextAuth)
const { priceId, plan, userEmail } = await request.json();
if (!session?.user?.email) { if (!userEmail) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); return NextResponse.json({ error: 'Unauthorized - No user email provided' }, { status: 401 });
} }
const { priceId, plan } = await request.json();
if (!priceId || !plan) { if (!priceId || !plan) {
return NextResponse.json( return NextResponse.json(
{ error: 'Missing priceId or plan' }, { error: 'Missing priceId or plan' },
@ -23,7 +21,7 @@ export async function POST(request: NextRequest) {
// Get user from database // Get user from database
const user = await db.user.findUnique({ const user = await db.user.findUnique({
where: { email: session.user.email }, where: { email: userEmail },
}); });
if (!user) { if (!user) {
@ -54,7 +52,7 @@ export async function POST(request: NextRequest) {
const checkoutSession = await stripe.checkout.sessions.create({ const checkoutSession = await stripe.checkout.sessions.create({
customer: customerId, customer: customerId,
mode: 'subscription', mode: 'subscription',
payment_method_types: ['card', 'sepa_debit'], payment_method_types: ['card'],
line_items: [ line_items: [
{ {
price: priceId, price: priceId,

View File

@ -0,0 +1,76 @@
import { NextRequest, NextResponse } from 'next/server';
import { cookies } from 'next/headers';
import { stripe } from '@/lib/stripe';
import { db } from '@/lib/db';
export async function POST(request: NextRequest) {
try {
// Use cookie-based auth instead of NextAuth
const userId = cookies().get('userId')?.value;
if (!userId) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
}
const user = await db.user.findUnique({
where: { id: userId },
});
if (!user) {
return NextResponse.json({ error: 'User not found' }, { status: 404 });
}
if (!user.stripeCustomerId) {
return NextResponse.json({ error: 'No Stripe customer ID' }, { status: 400 });
}
// Get the most recent checkout session for this customer
const checkoutSessions = await stripe.checkout.sessions.list({
customer: user.stripeCustomerId,
limit: 1,
});
if (checkoutSessions.data.length === 0) {
return NextResponse.json({ error: 'No checkout session found' }, { status: 404 });
}
const checkoutSession = checkoutSessions.data[0];
// Only process if payment was successful
if (checkoutSession.payment_status === 'paid' && checkoutSession.subscription) {
const subscription = await stripe.subscriptions.retrieve(
checkoutSession.subscription as string
);
// Determine plan from metadata or price ID
const plan = checkoutSession.metadata?.plan || 'PRO';
// Update user in database
await db.user.update({
where: { id: user.id },
data: {
stripeSubscriptionId: subscription.id,
stripePriceId: subscription.items.data[0].price.id,
stripeCurrentPeriodEnd: new Date(
subscription.current_period_end * 1000
),
plan: plan as any,
},
});
return NextResponse.json({
success: true,
plan,
subscriptionId: subscription.id,
});
}
return NextResponse.json({ error: 'Payment not completed' }, { status: 400 });
} catch (error) {
console.error('Error verifying session:', error);
return NextResponse.json(
{ error: 'Internal server error' },
{ status: 500 }
);
}
}

View File

@ -1,18 +1,18 @@
import { NextRequest, NextResponse } from 'next/server'; import { NextRequest, NextResponse } from 'next/server';
import { getServerSession } from 'next-auth'; import { cookies } from 'next/headers';
import { authOptions } from '@/lib/auth';
import { db } from '@/lib/db'; import { db } from '@/lib/db';
export async function GET(request: NextRequest) { export async function GET(request: NextRequest) {
try { try {
const session = await getServerSession(authOptions); // Use cookie-based auth instead of NextAuth
const userId = cookies().get('userId')?.value;
if (!session?.user?.email) { if (!userId) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
} }
const user = await db.user.findUnique({ const user = await db.user.findUnique({
where: { email: session.user.email }, where: { id: userId },
select: { select: {
plan: true, plan: true,
stripeCurrentPeriodEnd: true, stripeCurrentPeriodEnd: true,

View File

@ -1,9 +1,7 @@
import type { Metadata } from 'next'; import type { Metadata } from 'next';
import { Inter } from 'next/font/google';
import '@/styles/globals.css'; import '@/styles/globals.css';
import { ToastContainer } from '@/components/ui/Toast'; import { ToastContainer } from '@/components/ui/Toast';
import AuthProvider from '@/components/SessionProvider';
const inter = Inter({ subsets: ['latin'] });
export const metadata: Metadata = { export const metadata: Metadata = {
title: 'QR Master - Create Custom QR Codes in Seconds', title: 'QR Master - Create Custom QR Codes in Seconds',
@ -33,8 +31,10 @@ export default function RootLayout({
}) { }) {
return ( return (
<html lang="en"> <html lang="en">
<body className={inter.className}> <body className="font-sans">
<AuthProvider>
{children} {children}
</AuthProvider>
<ToastContainer /> <ToastContainer />
</body> </body>
</html> </html>

View File

@ -0,0 +1,11 @@
'use client';
import { SessionProvider } from 'next-auth/react';
export default function AuthProvider({
children,
}: {
children: React.ReactNode;
}) {
return <SessionProvider>{children}</SessionProvider>;
}