Skip to content

Commit d3be8df

Browse files
Merge pull request #114 from Vanshika814/main
Refactor authentication and subscription handling; Update routes and components to support Pro access checks.
2 parents 6aa043c + 70bb302 commit d3be8df

File tree

20 files changed

+680
-149
lines changed

20 files changed

+680
-149
lines changed

.vscode/settings.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{
2+
"terminal.integrated.sendKeybindingsToShell": true
3+
}

docker-compose.yml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
version: '3.8'
21

32
services:
43
# ================================

src/app/(auth)/sign-in/[[...sign-in]]/page.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,11 @@ export default function Page() {
99
appearance={{
1010
baseTheme: dark
1111
}}
12+
redirectUrl="/"
1213
/>
1314
</div>
1415
<div className='block dark:hidden'>
15-
<SignIn />
16+
<SignIn redirectUrl="/" />
1617
</div>
1718
</div>
1819
);
Lines changed: 23 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,25 @@
1-
import Generate from "@/components/core/generate";
1+
import FlashcardPro from "@/components/core/flash-card-pro";
2+
import { auth } from '@clerk/nextjs/server';
3+
import { subscriptionService } from '@/lib/services/subscription.service';
4+
import { redirect } from 'next/navigation';
25

3-
export default function GeneratePage() {
4-
return (
5-
// <div className="page-container flex items-center min-h-screen">
6-
// <h1 className='text-3xl text-foreground font-semibold'>Generate Flashcards</h1>
7-
// <Generate />
8-
// </div>
9-
<div className="min-h-screen">
10-
<Generate />
11-
</div>
12-
);
6+
export default async function GenerateProPage() {
7+
const { userId } = await auth();
8+
9+
if (!userId) {
10+
redirect('/sign-in?redirect_url=/generate-pro');
11+
}
12+
13+
// Check if user has Pro access
14+
const canAccessPro = await subscriptionService.canAccessProFeatures(userId);
15+
16+
if (!canAccessPro) {
17+
redirect('/pricing?upgrade=required&redirect=/generate-pro');
18+
}
19+
20+
return (
21+
<div className="page-container bg-slate-50 dark:bg-black">
22+
<FlashcardPro />
23+
</div>
24+
);
1325
}
Lines changed: 11 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -1,65 +1,14 @@
1-
import { NextResponse } from "next/server";
2-
import Stripe from "stripe";
3-
4-
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!, {
5-
apiVersion: "2024-06-20",
6-
});
7-
8-
export async function POST(req: Request) {
9-
const { subscriptionType } = await req.json();
10-
11-
const priceId =
12-
subscriptionType === "yearly"
13-
? process.env.STRIPE_PRICE_YEARLY
14-
: process.env.STRIPE_PRICE_MONTHLY;
15-
16-
if (!priceId) {
17-
return NextResponse.json(
18-
{ error: "Invalid subscription type" },
19-
{ status: 400 },
20-
);
21-
}
22-
23-
try {
24-
const session = await stripe.checkout.sessions.create({
25-
mode: "subscription",
26-
payment_method_types: ["card"],
27-
line_items: [
28-
{
29-
price: priceId,
30-
quantity: 1,
31-
},
32-
],
33-
success_url: `${process.env.NEXT_PUBLIC_APP_URL}/result?session_id={CHECKOUT_SESSION_ID}`,
34-
cancel_url: `${process.env.NEXT_PUBLIC_APP_URL}/pricing`,
35-
});
36-
37-
return NextResponse.json({ sessionId: session.id });
38-
} catch (error) {
39-
console.error("Error creating checkout session:", error);
40-
return NextResponse.json(
41-
{ error: "Error creating checkout session" },
42-
{ status: 500 },
43-
);
44-
}
1+
// Stripe support removed in favor of Razorpay.
2+
export async function POST() {
3+
return new Response(JSON.stringify({ error: 'Stripe removed. Use Razorpay.' }), {
4+
status: 410,
5+
headers: { 'Content-Type': 'application/json' }
6+
})
457
}
468

47-
export async function GET(req: Request) {
48-
const { searchParams } = new URL(req.url);
49-
const session_id = searchParams.get("session_id");
50-
51-
if (!session_id) {
52-
return NextResponse.json({ error: "Missing session_id" }, { status: 400 });
53-
}
54-
55-
try {
56-
const session = await stripe.checkout.sessions.retrieve(session_id);
57-
return NextResponse.json(session);
58-
} catch (error) {
59-
console.error("Error retrieving checkout session:", error);
60-
return NextResponse.json(
61-
{ error: "Error retrieving checkout session" },
62-
{ status: 500 },
63-
);
64-
}
9+
export async function GET() {
10+
return new Response(JSON.stringify({ error: 'Stripe removed. Use Razorpay.' }), {
11+
status: 410,
12+
headers: { 'Content-Type': 'application/json' }
13+
})
6514
}

src/app/api/generate/route.ts

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import { NextResponse } from "next/server";
22
import { auth } from "@clerk/nextjs/server";
33
import { flashcardService } from "@/lib/services/flashcard.service";
4+
import { prisma } from "@/lib/database";
5+
import { subscriptionService } from "@/lib/services/subscription.service";
46
import type { ApiError, FlashcardGenerationResponse } from "@/types";
57

68
export async function POST(req: Request) {
@@ -19,7 +21,43 @@ export async function POST(req: Request) {
1921
return NextResponse.json(error, { status: 400 });
2022
}
2123

22-
// ✅ CORRECT: Single Responsibility - Delegate to service
24+
// Determine plan-based limits
25+
const status = await subscriptionService.getUserSubscriptionStatus(userId);
26+
// Monthly flashcard caps (per saved/generated content)
27+
const monthlyCaps: Record<string, number> = {
28+
free: 10,
29+
basic: 500,
30+
pro: 2000,
31+
orgs: 10000,
32+
};
33+
const planKey = status.plan?.toLowerCase?.() || 'free';
34+
const monthlyCap = monthlyCaps[planKey] ?? 10;
35+
36+
// Calculate current month's usage based on saved flashcards
37+
const now = new Date();
38+
const startOfMonth = new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), 1, 0, 0, 0));
39+
40+
const usedThisMonth = await prisma.flashcard.count({
41+
where: {
42+
userId,
43+
createdAt: { gte: startOfMonth },
44+
},
45+
});
46+
47+
const remainingThisMonth = Math.max(0, monthlyCap - usedThisMonth);
48+
if (remainingThisMonth <= 0) {
49+
const error: ApiError = {
50+
error: "Monthly limit reached",
51+
details: `You've reached your monthly limit of ${monthlyCap} flashcards for the ${planKey} plan.`
52+
};
53+
return NextResponse.json(error, { status: 403 });
54+
}
55+
56+
// Always generate at most 10 at a time, and never exceed remaining monthly allowance
57+
const PER_REQUEST_CAP = 10;
58+
const desiredCount = Math.min(PER_REQUEST_CAP, remainingThisMonth);
59+
60+
// ✅ CORRECT: Single Responsibility - Delegate to service with count
2361
const flashcards = await flashcardService.generateFlashcards(text);
2462

2563
const response: FlashcardGenerationResponse = { flashcards };
Lines changed: 192 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,192 @@
1+
import { NextRequest, NextResponse } from 'next/server';
2+
import crypto from 'crypto';
3+
import { prisma } from '@/lib/database';
4+
5+
// Ensure this route is always handled at runtime and not during build
6+
export const dynamic = 'force-dynamic';
7+
export const runtime = 'nodejs';
8+
9+
// Webhook secret from Razorpay dashboard
10+
// Note: Do NOT throw at module init; check inside the handler to avoid build-time failures
11+
12+
export async function POST(req: NextRequest) {
13+
console.log('🔔 Razorpay webhook received');
14+
15+
try {
16+
const WEBHOOK_SECRET = process.env.RAZORPAY_WEBHOOK_SECRET;
17+
if (!WEBHOOK_SECRET) {
18+
console.error('❌ Missing RAZORPAY_WEBHOOK_SECRET');
19+
return NextResponse.json({ error: 'Missing webhook secret' }, { status: 500 });
20+
}
21+
22+
const body = await req.text();
23+
const signature = req.headers.get('x-razorpay-signature');
24+
25+
if (!signature) {
26+
console.log('❌ No signature found in webhook');
27+
return NextResponse.json({ error: 'No signature' }, { status: 400 });
28+
}
29+
30+
// Verify webhook signature (constant-time)
31+
const expectedSignature = crypto
32+
.createHmac('sha256', WEBHOOK_SECRET)
33+
.update(body)
34+
.digest('hex');
35+
36+
const providedBuf = Buffer.from(signature, 'hex');
37+
const expectedBuf = Buffer.from(expectedSignature, 'hex');
38+
39+
if (providedBuf.length !== expectedBuf.length || !crypto.timingSafeEqual(providedBuf, expectedBuf)) {
40+
console.log('❌ Invalid webhook signature');
41+
return NextResponse.json({ error: 'Invalid signature' }, { status: 400 });
42+
}
43+
44+
console.log('✅ Webhook signature verified');
45+
46+
const event = JSON.parse(body);
47+
console.log('📦 Webhook event:', event.event);
48+
49+
// Handle different payment events
50+
switch (event.event) {
51+
case 'payment.captured':
52+
await handlePaymentCaptured(event.payload.payment.entity);
53+
break;
54+
55+
case 'payment.failed':
56+
await handlePaymentFailed(event.payload.payment.entity);
57+
break;
58+
59+
case 'order.paid':
60+
await handleOrderPaid(event.payload.order.entity);
61+
break;
62+
63+
default:
64+
console.log('ℹ️ Unhandled webhook event:', event.event);
65+
}
66+
67+
return NextResponse.json({ success: true });
68+
69+
} catch (error) {
70+
console.error('💥 Webhook error:', error);
71+
return NextResponse.json({ error: 'Webhook processing failed' }, { status: 500 });
72+
}
73+
}
74+
75+
async function handlePaymentCaptured(payment: any) {
76+
console.log('💰 Payment captured:', payment.id);
77+
78+
try {
79+
// Find the payment record
80+
const paymentRecord = await prisma.payment.findFirst({
81+
where: { razorpayPaymentId: payment.id }
82+
});
83+
84+
if (!paymentRecord) {
85+
console.log('❌ Payment record not found for:', payment.id);
86+
return;
87+
}
88+
89+
// Update payment status
90+
if (paymentRecord.status === 'COMPLETED') {
91+
console.log('⚠️ Payment already processed:', payment.id);
92+
return;
93+
}
94+
await prisma.payment.update({
95+
where: { id: paymentRecord.id },
96+
data: {
97+
status: 'COMPLETED',
98+
razorpayPaymentId: payment.id,
99+
updatedAt: new Date()
100+
}
101+
});
102+
103+
// Get order details to determine subscription
104+
const order = await prisma.payment.findFirst({
105+
where: { razorpayOrderId: payment.order_id }
106+
});
107+
108+
if (order) {
109+
// Calculate subscription dates
110+
const now = new Date();
111+
const subscriptionStartedAt = new Date(now);
112+
const subscriptionEndsAt = new Date(now);
113+
const daysToAdd = order.billingCycle === 'monthly' ? 30 : 365;
114+
subscriptionEndsAt.setDate(subscriptionEndsAt.getDate() + daysToAdd);
115+
116+
// Update user subscription
117+
await prisma.user.update({
118+
where: { clerkUserId: order.userId },
119+
data: {
120+
subscriptionPlan: order.plan,
121+
subscriptionCycle: order.billingCycle,
122+
subscriptionStatus: 'active',
123+
paymentId: payment.id,
124+
subscriptionEndsAt: subscriptionEndsAt,
125+
subscriptionStartedAt: subscriptionStartedAt,
126+
updatedAt: new Date()
127+
}
128+
});
129+
130+
console.log('✅ User subscription activated:', {
131+
userId: order.userId,
132+
plan: order.plan,
133+
billingCycle: order.billingCycle
134+
});
135+
}
136+
137+
} catch (error) {
138+
console.error('❌ Error handling payment captured:', error);
139+
}
140+
}
141+
142+
async function handlePaymentFailed(payment: any) {
143+
console.log('❌ Payment failed:', payment.id);
144+
145+
try {
146+
// Find and update payment record (fallback by order id)
147+
const paymentRecord = await prisma.payment.findFirst({
148+
where: {
149+
OR: [
150+
{ razorpayPaymentId: payment.id },
151+
{ razorpayOrderId: payment.order_id }
152+
]
153+
}
154+
});
155+
156+
if (paymentRecord) {
157+
await prisma.payment.updateMany({
158+
where: { id: paymentRecord.id, status: { not: 'COMPLETED' } },
159+
data: {
160+
status: 'FAILED',
161+
failureReason: payment.error_description || 'Payment failed',
162+
updatedAt: new Date()
163+
}
164+
});
165+
166+
console.log('✅ Payment marked as failed:', payment.id);
167+
}
168+
169+
} catch (error) {
170+
console.error('❌ Error handling payment failed:', error);
171+
}
172+
}
173+
174+
async function handleOrderPaid(order: any) {
175+
console.log('✅ Order paid:', order.id);
176+
177+
try {
178+
// Update order status
179+
await prisma.payment.updateMany({
180+
where: { razorpayOrderId: order.id },
181+
data: {
182+
status: 'COMPLETED',
183+
updatedAt: new Date()
184+
}
185+
});
186+
187+
console.log('✅ Order marked as paid:', order.id);
188+
189+
} catch (error) {
190+
console.error('❌ Error handling order paid:', error);
191+
}
192+
}

0 commit comments

Comments
 (0)