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:
@@ -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 });
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user