93e26d749d
Build & Deploy Mobile / build-and-deploy (push) Failing after 10s
The /api/chat/daily route accepted a client-supplied userId with no auth check. Require a verified Bearer JWT and derive the user from the token instead. Also drop the manual openssl install and --schema flag from the Dockerfile now that a second `prisma generate` runs in the runner stage, and add a standard .dockerignore. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LwWLS2FYnE9ofqDF9hQFxA
147 lines
6.2 KiB
TypeScript
147 lines
6.2 KiB
TypeScript
import { NextRequest, NextResponse } from 'next/server'
|
|
import { prisma } from '@/lib/prisma'
|
|
import { verifyJWT } from '@/lib/auth'
|
|
|
|
// ─────────────────────────────────────────────
|
|
// Greeting pools — tiered by engagement level
|
|
// ─────────────────────────────────────────────
|
|
|
|
const DAILY_GREETINGS = [
|
|
`Assalamu alaikum! I was thinking of you this morning. How is your heart today?`,
|
|
`Assalamu alaikum! A new day, a new opportunity to draw closer to Allah. How are you, my friend?`,
|
|
`Peace be upon you. I felt a gentle nudge to check in with you. How has your journey been?`,
|
|
`Assalamu alaikum! The sun rises again, and so does Allah's mercy. How are you doing today?`,
|
|
`Peace be with you. I was wondering how you've been — we don't need to talk about anything heavy. Just checking in.`,
|
|
`Assalamu alaikum! Every breath is a gift we didn't earn. How are you spending yours today?`,
|
|
`Peace upon you. No pressure, no expectations — just wanted you to know someone is here when you're ready.`,
|
|
]
|
|
|
|
// For users who haven't checked in for 2+ days — gentler, warmer
|
|
const NURTURE_GREETINGS = [
|
|
`Assalamu alaikum. I noticed it's been a little while. Just wanted you to know — there's no guilt here, no judgment. Only welcome. How have you been, truly?`,
|
|
`Peace be upon you. Life gets busy, I know. But I wanted to check in because you matter. How is your heart these days?`,
|
|
`Assalamu alaikum. Whether you've been distant from Allah or just busy with the world — His door is always open. So is this conversation. How are you?`,
|
|
`Peace be with you. They say the journey back begins with a single step. You're here — that's your step. How can I lighten your load today?`,
|
|
`Assalamu alaikum. No matter how many days have passed, you are always welcome here. The Prophet ﷺ said the one who returns to good is like one who never left. How can I support you today?`,
|
|
`Peace be upon you. Silence between friends is not a wall — it's a pause. I'm still here. What's on your mind?`,
|
|
]
|
|
|
|
// For first-time users with no history — the warmest welcome
|
|
const FIRST_GREETINGS = [
|
|
`Assalamu alaikum! I'm so glad you're here. This is a space for you — for your questions, your struggles, your quiet thoughts. There is nothing too small or too big to bring here. How are you feeling today?`,
|
|
`Peace be upon you. Welcome. Think of me as a friend who loves the Quran and the Sunnah, and who is simply here to walk alongside you. Where would you like to start?`,
|
|
`Assalamu alaikum! The Prophet ﷺ said, "Whoever Allah desires good for, He gives him understanding of the religion." You seeking knowledge is a sign of goodness. How can I help you grow today?`,
|
|
]
|
|
|
|
function daysSince(date: Date): number {
|
|
const now = new Date()
|
|
const diff = now.getTime() - date.getTime()
|
|
return Math.floor(diff / (1000 * 60 * 60 * 24))
|
|
}
|
|
|
|
function pickRandom<T>(pool: T[]): T {
|
|
return pool[Math.floor(Math.random() * pool.length)]
|
|
}
|
|
|
|
export async function POST(req: NextRequest) {
|
|
try {
|
|
const authHeader = req.headers.get('authorization')
|
|
if (!authHeader?.startsWith('Bearer ')) {
|
|
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
|
}
|
|
const payload = await verifyJWT(authHeader.slice(7))
|
|
if (!payload) {
|
|
return NextResponse.json({ error: 'Invalid token' }, { status: 401 })
|
|
}
|
|
const userId = payload.id
|
|
|
|
const user = await prisma.user.findUnique({ where: { id: userId } })
|
|
if (!user) {
|
|
return NextResponse.json({ error: 'User not found' }, { status: 404 })
|
|
}
|
|
|
|
// Check last bot message
|
|
const lastBotMessage = await prisma.chatHistory.findFirst({
|
|
where: { userId, role: 'assistant' },
|
|
orderBy: { createdAt: 'desc' },
|
|
})
|
|
|
|
// If bot already messaged today, skip
|
|
if (lastBotMessage) {
|
|
const lastDate = new Date(lastBotMessage.createdAt)
|
|
const now = new Date()
|
|
const sameDay = lastDate.getDate() === now.getDate()
|
|
&& lastDate.getMonth() === now.getMonth()
|
|
&& lastDate.getFullYear() === now.getFullYear()
|
|
if (sameDay) {
|
|
return NextResponse.json({
|
|
needsGreeting: false,
|
|
message: null,
|
|
lastSeen: lastBotMessage.createdAt,
|
|
})
|
|
}
|
|
}
|
|
|
|
// Check last user message for idle detection
|
|
const lastUserMessage = await prisma.chatHistory.findFirst({
|
|
where: { userId, role: 'user' },
|
|
orderBy: { createdAt: 'desc' },
|
|
})
|
|
|
|
// Check if this is a first-time user (no history at all)
|
|
const totalMessages = await prisma.chatHistory.count({ where: { userId } })
|
|
|
|
// Determine greeting tier
|
|
let greeting: string
|
|
|
|
if (totalMessages === 0) {
|
|
// First visit ever — warm welcome
|
|
greeting = pickRandom(FIRST_GREETINGS)
|
|
} else if (lastUserMessage && daysSince(new Date(lastUserMessage.createdAt)) >= 2) {
|
|
// Been away 2+ days — nurture/re-engagement message
|
|
greeting = pickRandom(NURTURE_GREETINGS)
|
|
} else {
|
|
// Regular daily check-in
|
|
let lastTopic: string | undefined
|
|
if (lastUserMessage) {
|
|
const words = lastUserMessage.content.split(/\s+/).slice(0, 8).join(' ')
|
|
lastTopic = words.length > 60 ? words.slice(0, 60) + '...' : words
|
|
}
|
|
const base = pickRandom(DAILY_GREETINGS)
|
|
greeting = lastTopic
|
|
? `${base} Last time we spoke about ${lastTopic}. Want to continue, or something new?`
|
|
: base
|
|
}
|
|
|
|
// Save the greeting
|
|
await prisma.chatHistory.create({
|
|
data: {
|
|
userId,
|
|
role: 'assistant',
|
|
content: greeting,
|
|
metadata: JSON.stringify({
|
|
command: '_daily',
|
|
summary: 'Daily check-in',
|
|
actionItems: [],
|
|
topics: ['daily-check-in'],
|
|
}),
|
|
},
|
|
})
|
|
|
|
// Update lastCoachedAt
|
|
await prisma.user.update({
|
|
where: { id: userId },
|
|
data: { lastCoachedAt: new Date() },
|
|
})
|
|
|
|
return NextResponse.json({
|
|
needsGreeting: true,
|
|
message: greeting,
|
|
lastSeen: lastBotMessage?.createdAt || null,
|
|
})
|
|
} catch (e) {
|
|
console.error('Daily check-in error:', e)
|
|
return NextResponse.json({ error: 'Internal server error' }, { status: 500 })
|
|
}
|
|
}
|