feat: Souq native integration + auth simplification + CE-wide enhancements
Souq Marketplace: - Native souq pages: browse+filters, create listing, detail+seller profile, orders+chat - New API: reviews (POST), sellers/[id], upload (multipart), polar-checkout webhook - Listings API enhanced: minPrice, maxPrice, sortBy, deliveryDays filters - Seller workflow: start order, mark delivered, upload files, confirm delivery - Wallet: 5 Polar.sh FLH tiers, production checkout, mock fallback, success banner - Fix: order redirect /souq/history → /souq/orders Auth System: - Unified auth page, removed OAuth provider routing (2 files deleted) - Deleted oauth.ts (319 lines) — simplified auth chain - Streamlined login/register API routes Prisma Schema (+179 lines): - New models: Listing, Purchase, Review, Group/GroupMember - Gamification: DailyStreak, XpTransaction, Achievement, UserLevel - Learning: LearnCourse/Module/Enrollment/Certificate - Notifications, Referrals, Dhikr, PremiumBenefits Deleted legacy code: - src/app/api/marketplace/* (3 files) — replaced by /api/souq/* - src/app/api/files/[listingId]/route.ts — replaced by /api/souq/upload - src/app/api/seller/[sellerId]/route.ts — replaced by /api/souq/sellers/[id] Infrastructure: - Dockerfile: standalone output, Prisma runtime migration - docker-compose.yml: Polar env vars, healthcheck fix - docker-compose.staging.yml: staging on port 4014 - .gitea/workflows/ci.yml: CI pipeline
This commit is contained in:
@@ -0,0 +1,120 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { prisma } from "@/lib/prisma";
|
||||
import { requireAuth } from "@/lib/auth";
|
||||
|
||||
// GET /api/learn/courses/[slug] — course detail with modules and user progress
|
||||
export async function GET(
|
||||
request: NextRequest,
|
||||
{ params }: { params: Promise<{ slug: string }> }
|
||||
) {
|
||||
try {
|
||||
const auth = await requireAuth(request);
|
||||
if (!auth) {
|
||||
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
||||
}
|
||||
|
||||
const { slug } = await params;
|
||||
|
||||
const course = await prisma.learnCourse.findUnique({
|
||||
where: { slug },
|
||||
include: {
|
||||
modules: {
|
||||
orderBy: { order: "asc" },
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!course) {
|
||||
return NextResponse.json(
|
||||
{ error: "Course not found" },
|
||||
{ status: 404 }
|
||||
);
|
||||
}
|
||||
|
||||
// Fetch user's enrollment for this course (if any)
|
||||
let enrollment = await prisma.learnEnrollment.findUnique({
|
||||
where: {
|
||||
userId_courseId: {
|
||||
userId: auth.userId,
|
||||
courseId: course.id,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// Auto-enroll premium users (check local user record, not JWT claims)
|
||||
// The JWT's licenseTier may not match the local user's isPremium flag
|
||||
if (!enrollment) {
|
||||
const localUser = await prisma.user.findUnique({
|
||||
where: { id: auth.userId },
|
||||
select: { isPremium: true },
|
||||
});
|
||||
if (localUser?.isPremium) {
|
||||
enrollment = await prisma.learnEnrollment.create({
|
||||
data: {
|
||||
userId: auth.userId,
|
||||
courseId: course.id,
|
||||
purchaseType: "free",
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Fetch module progress for this enrollment (if enrolled)
|
||||
let moduleProgressMap: Record<string, { completed: boolean; score: number | null; listened: boolean }> = {};
|
||||
if (enrollment) {
|
||||
const progresses = await prisma.learnModuleProgress.findMany({
|
||||
where: { enrollmentId: enrollment.id },
|
||||
});
|
||||
for (const p of progresses) {
|
||||
moduleProgressMap[p.moduleId] = {
|
||||
completed: p.completed,
|
||||
score: p.score,
|
||||
listened: p.listened,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Build response
|
||||
const modules = course.modules.map((mod) => ({
|
||||
id: mod.id,
|
||||
order: mod.order,
|
||||
title: mod.title,
|
||||
slug: mod.slug,
|
||||
keyTakeaway: mod.keyTakeaway,
|
||||
duration: mod.duration,
|
||||
content: mod.content,
|
||||
quizData: mod.quizData ? JSON.parse(mod.quizData) : null,
|
||||
progress: moduleProgressMap[mod.id] ?? null,
|
||||
}));
|
||||
|
||||
return NextResponse.json({
|
||||
course: {
|
||||
id: course.id,
|
||||
slug: course.slug,
|
||||
title: course.title,
|
||||
author: course.author,
|
||||
description: course.description,
|
||||
imageUrl: course.imageUrl,
|
||||
moduleCount: course.moduleCount,
|
||||
totalMinutes: course.totalMinutes,
|
||||
difficulty: course.difficulty,
|
||||
giteaPath: course.giteaPath,
|
||||
},
|
||||
modules,
|
||||
enrollment: enrollment
|
||||
? {
|
||||
id: enrollment.id,
|
||||
purchaseType: enrollment.purchaseType,
|
||||
completed: enrollment.completed,
|
||||
progress: enrollment.progress,
|
||||
}
|
||||
: null,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("GET /api/learn/courses/[slug] error:", error);
|
||||
return NextResponse.json(
|
||||
{ error: "Internal server error" },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { prisma } from "@/lib/prisma";
|
||||
import { requireAuth } from "@/lib/auth";
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
try {
|
||||
const auth = await requireAuth(request);
|
||||
const userId = auth?.userId;
|
||||
|
||||
const courses = await prisma.learnCourse.findMany({
|
||||
where: { published: true },
|
||||
orderBy: { createdAt: "desc" },
|
||||
});
|
||||
|
||||
// If authenticated, fetch the user's enrollments for the returned courses
|
||||
let enrollmentsMap = new Map<string, { progress: number; completed: boolean }>();
|
||||
if (userId && courses.length > 0) {
|
||||
const enrollments = await prisma.learnEnrollment.findMany({
|
||||
where: {
|
||||
userId,
|
||||
courseId: { in: courses.map((c) => c.id) },
|
||||
},
|
||||
select: { courseId: true, progress: true, completed: true },
|
||||
});
|
||||
for (const e of enrollments) {
|
||||
enrollmentsMap.set(e.courseId, { progress: e.progress, completed: e.completed });
|
||||
}
|
||||
}
|
||||
|
||||
const result = courses.map((course) => {
|
||||
const enrollment = enrollmentsMap.get(course.id);
|
||||
return {
|
||||
id: course.id,
|
||||
slug: course.slug,
|
||||
title: course.title,
|
||||
author: course.author,
|
||||
description: course.description,
|
||||
imageUrl: course.imageUrl,
|
||||
moduleCount: course.moduleCount,
|
||||
totalMinutes: course.totalMinutes,
|
||||
difficulty: course.difficulty,
|
||||
priceFlh: course.priceFlh,
|
||||
priceUsd: course.priceUsd,
|
||||
subscriptionOnly: course.subscriptionOnly,
|
||||
enrolled: userId ? !!enrollment : null,
|
||||
...(enrollment ? { enrollment: { progress: enrollment.progress, completed: enrollment.completed } } : {}),
|
||||
};
|
||||
});
|
||||
|
||||
return NextResponse.json({ courses: result });
|
||||
} catch (error) {
|
||||
console.error("GET /api/learn/courses error:", error);
|
||||
return NextResponse.json(
|
||||
{ error: "Internal server error" },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user