Initial Falah Mobile rebuild — all 7 blocks complete

- Auth system (login/register/profile)
- Nur AI chat with persona system
- Souq marketplace with FLH economy
- Forum community with Shariah moderation
- Wallet & FLH top-up
- Premium/Pro tier upgrade with Polar.sh
- Halal Monitor with map & bookmarks
- Home dashboard with daily verse & streaks
- Health endpoints

Next.js 16.2.7, Prisma v5 SQLite, JWT auth, Tailwind CSS v4
This commit is contained in:
root
2026-06-15 09:28:22 +02:00
parent ab8a2053e1
commit 5483dd291e
56 changed files with 7619 additions and 100 deletions
+158
View File
@@ -0,0 +1,158 @@
import { requireAuth, type JWTContents } from "@/lib/auth";
import { prisma } from "@/lib/prisma";
import { getPersona } from "@/lib/personas";
import { getUserTier, DAILY_LIMITS } from "@/lib/tiers";
import { generateAIReply } from "@/lib/ai-server";
export const runtime = "nodejs";
export async function POST(request: Request) {
try {
const user = await requireAuth(request);
if (!user) {
return Response.json({ error: "Unauthorized" }, { status: 401 });
}
const body = await request.json();
const { messages, personaId } = body as {
messages?: { role: string; content: string }[];
personaId?: string;
};
if (!messages || !Array.isArray(messages) || messages.length === 0) {
return Response.json({ error: "messages array is required" }, { status: 400 });
}
if (!personaId || typeof personaId !== "string") {
return Response.json({ error: "personaId is required" }, { status: 400 });
}
// Verify persona exists
const persona = getPersona(personaId);
if (!persona) {
return Response.json({ error: "Invalid persona" }, { status: 400 });
}
// Get user from DB to check limits
const dbUser = await prisma.user.findUnique({
where: { id: user.userId },
});
if (!dbUser) {
return Response.json({ error: "User not found" }, { status: 404 });
}
const tier = getUserTier(dbUser.isPremium, dbUser.isPro);
const limits = DAILY_LIMITS[tier];
// Check persona access for this tier
if (!limits.personas.includes(personaId)) {
const tierName = tier.charAt(0).toUpperCase() + tier.slice(1);
return Response.json(
{
error: `This persona requires ${tier === "free" ? "Premium" : "Pro"} subscription`,
tier,
requiredTier: tier === "free" ? "premium" : "pro",
},
{ status: 403 }
);
}
// Check daily message limit
const now = new Date();
let dailyMsgCount = dbUser.dailyMsgCount;
let dailyMsgResetAt = dbUser.dailyMsgResetAt;
// Reset count if a new day
if (!dailyMsgResetAt || now.getTime() - dailyMsgResetAt.getTime() > 24 * 60 * 60 * 1000) {
dailyMsgCount = 0;
dailyMsgResetAt = now;
}
if (limits.msgs !== Infinity && dailyMsgCount >= limits.msgs) {
return Response.json(
{
error: "Daily message limit reached",
count: dailyMsgCount,
limit: limits.msgs,
resetAt: dailyMsgResetAt.toISOString(),
},
{ status: 429 }
);
}
// Determine the user's last message for context
const lastUserMessage = messages[messages.length - 1]?.content || "";
// Get recent history from DB for context (up to 10 recent messages)
const recentHistory = await prisma.chatHistory.findMany({
where: { userId: user.userId },
orderBy: { createdAt: "desc" },
take: 10,
});
const historyContext = recentHistory
.reverse()
.map((h) => ({ role: h.role, content: h.content }));
// Save user message(s) to ChatHistory
for (const msg of messages) {
if (msg.role === "user") {
await prisma.chatHistory.create({
data: {
userId: user.userId,
role: "user",
content: msg.content,
metadata: JSON.stringify({ personaId }),
},
});
}
}
// Generate AI response
const aiContent = generateAIReply(personaId, lastUserMessage, historyContext);
// Save AI response to ChatHistory
const aiMessage = await prisma.chatHistory.create({
data: {
userId: user.userId,
role: "assistant",
content: aiContent,
metadata: JSON.stringify({ personaId }),
},
});
// Update daily message count
const newCount = dailyMsgCount + messages.filter((m) => m.role === "user").length;
await prisma.user.update({
where: { id: user.userId },
data: {
dailyMsgCount: newCount,
dailyMsgResetAt: dailyMsgResetAt,
},
});
let updatedUser: any = { dailyMsgCount: newCount };
if (newCount >= limits.msgs - 2 && limits.msgs !== Infinity) {
updatedUser.limitWarning = true;
updatedUser.limitRemaining = limits.msgs - newCount;
}
return Response.json({
message: {
role: "assistant",
content: aiContent,
createdAt: aiMessage.createdAt.toISOString(),
id: aiMessage.id,
},
daily: {
count: newCount,
limit: limits.msgs,
resetAt: dailyMsgResetAt.toISOString(),
},
user: updatedUser,
});
} catch (error) {
console.error("Chat send error:", error);
return Response.json({ error: "Internal server error" }, { status: 500 });
}
}