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,53 @@
|
||||
import { requireAuth } from "@/lib/auth";
|
||||
import { prisma } from "@/lib/prisma";
|
||||
import { getUserTier, DAILY_LIMITS } from "@/lib/tiers";
|
||||
|
||||
export const runtime = "nodejs";
|
||||
|
||||
export async function GET(request: Request) {
|
||||
try {
|
||||
const user = await requireAuth(request);
|
||||
if (!user) {
|
||||
return Response.json({ error: "Unauthorized" }, { status: 401 });
|
||||
}
|
||||
|
||||
const dbUser = await prisma.user.findUnique({
|
||||
where: { id: user.userId },
|
||||
select: {
|
||||
dailyMsgCount: true,
|
||||
dailyMsgResetAt: true,
|
||||
isPremium: true,
|
||||
isPro: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!dbUser) {
|
||||
return Response.json({ error: "User not found" }, { status: 404 });
|
||||
}
|
||||
|
||||
const tier = getUserTier(dbUser.isPremium, dbUser.isPro);
|
||||
const limits = DAILY_LIMITS[tier];
|
||||
|
||||
const now = new Date();
|
||||
let count = dbUser.dailyMsgCount;
|
||||
let resetAt = dbUser.dailyMsgResetAt;
|
||||
|
||||
// Reset if new day
|
||||
if (!resetAt || now.getTime() - resetAt.getTime() > 24 * 60 * 60 * 1000) {
|
||||
count = 0;
|
||||
resetAt = now;
|
||||
}
|
||||
|
||||
return Response.json({
|
||||
count,
|
||||
limit: limits.msgs === Infinity ? -1 : limits.msgs,
|
||||
resetAt: resetAt?.toISOString() || now.toISOString(),
|
||||
tier,
|
||||
remaining:
|
||||
limits.msgs === Infinity ? -1 : Math.max(0, limits.msgs - count),
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Daily chat error:", error);
|
||||
return Response.json({ error: "Internal server error" }, { status: 500 });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
export const runtime = "nodejs";
|
||||
|
||||
const DEMO_CONVERSATION: { role: string; content: string; personaId: string }[] = [
|
||||
{
|
||||
role: "user",
|
||||
content: "Assalamualaikum! I've been feeling really stressed about work lately.",
|
||||
personaId: "nurbuddy",
|
||||
},
|
||||
{
|
||||
role: "assistant",
|
||||
content:
|
||||
"Waalaikumsalam warahmatullahi wabarakatuh! 😊 I'm sorry to hear you're feeling stressed. Remember what Allah says: **\"Verily, with hardship comes ease\"** (Quran 94:6). \n\nHere are a few things that might help:\n• Take a deep breath and recite *SubhanAllah* 33 times\n• Make wudu — it's surprisingly calming!\n• Step outside and appreciate Allah's creation for a few minutes\n\nYou're not alone, and this too shall pass, insha'Allah. 💛",
|
||||
personaId: "nurbuddy",
|
||||
},
|
||||
{
|
||||
role: "user",
|
||||
content: "That helps, thank you. Can you tell me more about patience in Islam?",
|
||||
personaId: "hakim",
|
||||
},
|
||||
{
|
||||
role: "assistant",
|
||||
content:
|
||||
"Ah, sabr (patience) — one of the most beautiful virtues in Islam. Let me share a story:\n\nProphet Ayyub (Job, peace be upon him) lost everything — wealth, children, and health. Yet he never stopped praising Allah. When asked how, he said: **\"Allah gave, and Allah has taken. Blessed be Allah in both.\"**\n\nAllah rewarded him tremendously: **\"And We gave him back his family and the like thereof with them — a mercy from Us and a reminder for those who worship\"** (Quran 38:43).\n\nYour patience through work stress is a form of worship too. Every moment you endure with grace is recorded. 🌱",
|
||||
personaId: "hakim",
|
||||
},
|
||||
{
|
||||
role: "user",
|
||||
content: "SubhanAllah, that's really powerful. JazakAllah khair!",
|
||||
personaId: "nurbuddy",
|
||||
},
|
||||
{
|
||||
role: "assistant",
|
||||
content:
|
||||
"Wa iyyak! 😊 So glad it resonated with you. Remember, the Prophet ﷺ said: **\"He who does not thank people, does not thank Allah.\"** \n\nMay Allah grant you ease in your work and bless your patience. Come back anytime you need a friend to talk to! 🌟",
|
||||
personaId: "nurbuddy",
|
||||
},
|
||||
];
|
||||
|
||||
export async function GET() {
|
||||
return Response.json({
|
||||
conversation: DEMO_CONVERSATION,
|
||||
personas: ["nurbuddy", "hakim"],
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
import { requireAuth, type JWTContents } from "@/lib/auth";
|
||||
import { prisma } from "@/lib/prisma";
|
||||
|
||||
export const runtime = "nodejs";
|
||||
|
||||
export async function GET(
|
||||
request: Request,
|
||||
{ params }: { params: Promise<{ userId: string }> }
|
||||
) {
|
||||
try {
|
||||
const user = await requireAuth(request);
|
||||
if (!user) {
|
||||
return Response.json({ error: "Unauthorized" }, { status: 401 });
|
||||
}
|
||||
|
||||
const { userId } = await params;
|
||||
|
||||
// Verify the requested userId matches the authenticated user
|
||||
if (user.userId !== userId) {
|
||||
return Response.json({ error: "Forbidden" }, { status: 403 });
|
||||
}
|
||||
|
||||
const messages = await prisma.chatHistory.findMany({
|
||||
where: { userId },
|
||||
orderBy: { createdAt: "desc" },
|
||||
take: 50,
|
||||
});
|
||||
|
||||
// Return in chronological order
|
||||
const ordered = messages.reverse();
|
||||
|
||||
return Response.json({
|
||||
messages: ordered.map((m) => ({
|
||||
id: m.id,
|
||||
role: m.role,
|
||||
content: m.content,
|
||||
metadata: m.metadata ? JSON.parse(m.metadata) : null,
|
||||
createdAt: m.createdAt.toISOString(),
|
||||
})),
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Chat history error:", error);
|
||||
return Response.json({ error: "Internal server error" }, { status: 500 });
|
||||
}
|
||||
}
|
||||
@@ -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