Require user authentication for AI chat functionality

- Update chat API to require valid authentication tokens for all requests
- Add authentication requirement screens to both chat components
- Show "Create Account / Sign In" prompts for unauthenticated users
- Hide chat input and functionality until user is logged in
- Return 401 errors with clear messages when authentication is missing
- Maintain bilingual support (Romanian/English) for auth prompts

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2025-09-28 20:22:57 +00:00
parent e4b815cb40
commit 83a981cabc
3 changed files with 182 additions and 83 deletions

View File

@@ -26,24 +26,35 @@ export async function POST(request: Request) {
const body = await request.json() const body = await request.json()
const { message, conversationId, locale, history } = chatRequestSchema.parse(body) const { message, conversationId, locale, history } = chatRequestSchema.parse(body)
// Try to get user from authentication (optional for backward compatibility) // Require authentication for chat functionality
let userId: string | null = null let userId: string | null = null
const authHeader = request.headers.get('authorization') const authHeader = request.headers.get('authorization')
console.log('Chat API - authHeader present:', !!authHeader)
if (authHeader?.startsWith('Bearer ')) { if (!authHeader?.startsWith('Bearer ')) {
try { return NextResponse.json(
const token = authHeader.substring(7) {
console.log('Chat API - token extracted, length:', token.length) success: false,
const payload = await verifyToken(token) error: 'Authentication required to use chat functionality',
console.log('Chat API - token payload:', payload) code: 'AUTH_REQUIRED'
userId = payload.userId },
console.log('Chat API - userId extracted from token:', userId) { status: 401 }
} catch (error) { )
// Continue without authentication for backward compatibility }
console.log('Chat API - authentication failed:', (error as any)?.message || error)
} try {
} else { const token = authHeader.substring(7)
console.log('Chat API - no valid auth header') const payload = await verifyToken(token)
userId = payload.userId
console.log('Chat API - authenticated user:', userId)
} catch (error) {
return NextResponse.json(
{
success: false,
error: 'Invalid or expired authentication token',
code: 'AUTH_INVALID'
},
{ status: 401 }
)
} }
// Handle conversation logic // Handle conversation logic

View File

@@ -1,13 +1,14 @@
'use client' 'use client'
import { useState, useRef, useEffect } from 'react' import { useState, useRef, useEffect } from 'react'
import { Send } from 'lucide-react' import { Send, User } from 'lucide-react'
import ReactMarkdown from 'react-markdown' import ReactMarkdown from 'react-markdown'
export function ChatInterface() { export function ChatInterface() {
const [messages, setMessages] = useState<Array<{ role: string; content: string }>>([]) const [messages, setMessages] = useState<Array<{ role: string; content: string }>>([])
const [input, setInput] = useState('') const [input, setInput] = useState('')
const [loading, setLoading] = useState(false) const [loading, setLoading] = useState(false)
const [isAuthenticated, setIsAuthenticated] = useState(false)
const messagesEndRef = useRef<HTMLDivElement>(null) const messagesEndRef = useRef<HTMLDivElement>(null)
const scrollToBottom = () => { const scrollToBottom = () => {
@@ -16,6 +17,24 @@ export function ChatInterface() {
useEffect(scrollToBottom, [messages]) useEffect(scrollToBottom, [messages])
// Check authentication status on mount
useEffect(() => {
const checkAuth = async () => {
try {
const token = localStorage.getItem('authToken')
if (token) {
const response = await fetch('/api/auth/me', {
headers: { 'Authorization': `Bearer ${token}` }
})
setIsAuthenticated(response.ok)
}
} catch (error) {
setIsAuthenticated(false)
}
}
checkAuth()
}, [])
const handleSubmit = async (e: React.FormEvent) => { const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault() e.preventDefault()
if (!input.trim() || loading) return if (!input.trim() || loading) return
@@ -64,14 +83,32 @@ export function ChatInterface() {
</div> </div>
<div className="flex-1 overflow-y-auto p-4 space-y-4"> <div className="flex-1 overflow-y-auto p-4 space-y-4">
{messages.length === 0 && ( {!isAuthenticated ? (
<div className="text-center text-gray-500 mt-12"> <div className="flex flex-col items-center justify-center h-full text-center p-6">
<p>Bună ziua! Sunt aici ajut cu întrebările despre Biblie.</p> <User className="w-16 h-16 text-gray-400 mb-4" />
<p className="text-sm mt-2">Puteți începe prin a întreba ceva despre un verset sau o temă biblică.</p> <h3 className="text-lg font-semibold mb-2">Bun venit la AI Chat Biblic!</h3>
<p className="text-gray-600 mb-4 max-w-md">
Pentru a accesa chat-ul AI și a salva conversațiile tale, te rugăm îți creezi un cont sau te conectezi.
</p>
<button
onClick={() => {
window.dispatchEvent(new CustomEvent('auth:sign-in-required'))
}}
className="px-6 py-3 bg-blue-500 text-white rounded-lg hover:bg-blue-600 transition-colors"
>
Creează Cont / Conectează-te
</button>
</div> </div>
)} ) : (
<>
{messages.length === 0 && (
<div className="text-center text-gray-500 mt-12">
<p>Bună ziua! Sunt aici ajut cu întrebările despre Biblie.</p>
<p className="text-sm mt-2">Puteți începe prin a întreba ceva despre un verset sau o temă biblică.</p>
</div>
)}
{messages.map((msg, idx) => ( {messages.map((msg, idx) => (
<div <div
key={idx} key={idx}
className={`flex ${msg.role === 'user' ? 'justify-end' : 'justify-start'}`} className={`flex ${msg.role === 'user' ? 'justify-end' : 'justify-start'}`}
@@ -106,29 +143,33 @@ export function ChatInterface() {
</div> </div>
</div> </div>
</div> </div>
)}
<div ref={messagesEndRef} />
</>
)} )}
<div ref={messagesEndRef} />
</div> </div>
<form onSubmit={handleSubmit} className="p-4 border-t"> {isAuthenticated && (
<div className="flex space-x-2"> <form onSubmit={handleSubmit} className="p-4 border-t">
<input <div className="flex space-x-2">
type="text" <input
value={input} type="text"
onChange={(e) => setInput(e.target.value)} value={input}
placeholder="Întreabă despre Biblie..." onChange={(e) => setInput(e.target.value)}
className="flex-1 px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500" placeholder="Întreabă despre Biblie..."
disabled={loading} className="flex-1 px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
/> disabled={loading}
<button />
type="submit" <button
disabled={loading || !input.trim()} type="submit"
className="px-4 py-2 bg-blue-500 text-white rounded-lg hover:bg-blue-600 disabled:opacity-50 disabled:cursor-not-allowed transition-colors" disabled={loading || !input.trim()}
> className="px-4 py-2 bg-blue-500 text-white rounded-lg hover:bg-blue-600 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
<Send className="w-5 h-5" /> >
</button> <Send className="w-5 h-5" />
</div> </button>
</form> </div>
</form>
)}
</div> </div>
) )
} }

View File

@@ -577,10 +577,16 @@ export default function FloatingChat() {
{!isAuthenticated ? ( {!isAuthenticated ? (
<Box sx={{ textAlign: 'center', py: 3 }}> <Box sx={{ textAlign: 'center', py: 3 }}>
<Typography variant="body2" color="text.secondary" sx={{ mb: 2 }}> <Typography variant="body2" color="text.secondary" sx={{ mb: 2 }}>
Sign in to save your conversations {locale === 'ro' ? 'Conectează-te pentru a salva conversațiile' : 'Sign in to save your conversations'}
</Typography> </Typography>
<Button variant="outlined" size="small"> <Button
Sign In variant="outlined"
size="small"
onClick={() => {
window.dispatchEvent(new CustomEvent('auth:sign-in-required'))
}}
>
{locale === 'ro' ? 'Conectează-te' : 'Sign In'}
</Button> </Button>
</Box> </Box>
) : isLoadingConversations ? ( ) : isLoadingConversations ? (
@@ -688,7 +694,44 @@ export default function FloatingChat() {
p: 1, p: 1,
}} }}
> >
{messages.map((message) => ( {!isAuthenticated ? (
<Box sx={{
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
height: '100%',
textAlign: 'center',
p: 3
}}>
<SmartToy sx={{ fontSize: 64, color: 'text.secondary', mb: 2 }} />
<Typography variant="h6" gutterBottom>
{locale === 'ro' ? 'Bun venit la AI Chat Biblic!' : 'Welcome to Biblical AI Chat!'}
</Typography>
<Typography variant="body2" color="text.secondary" sx={{ mb: 3, maxWidth: 400 }}>
{locale === 'ro'
? 'Pentru a accesa chat-ul AI și a salva conversațiile tale, te rugăm să îți creezi un cont sau să te conectezi.'
: 'To access the AI chat and save your conversations, please create an account or sign in.'
}
</Typography>
<Button
variant="contained"
size="large"
onClick={() => {
window.dispatchEvent(new CustomEvent('auth:sign-in-required'))
}}
sx={{
background: 'linear-gradient(45deg, #009688 30%, #00796B 90%)',
px: 4,
py: 1.5
}}
>
{locale === 'ro' ? 'Creează Cont / Conectează-te' : 'Create Account / Sign In'}
</Button>
</Box>
) : (
<>
{messages.map((message) => (
<Box <Box
key={message.id} key={message.id}
sx={{ sx={{
@@ -834,49 +877,53 @@ export default function FloatingChat() {
</Box> </Box>
)} )}
<div ref={messagesEndRef} /> <div ref={messagesEndRef} />
</>
)}
</Box> </Box>
<Divider /> <Divider />
{/* Input */} {/* Input */}
<Box sx={{ p: 2 }}> {isAuthenticated && (
<Box sx={{ display: 'flex', gap: 1 }}> <Box sx={{ p: 2 }}>
<TextField <Box sx={{ display: 'flex', gap: 1 }}>
fullWidth <TextField
size="small" fullWidth
multiline size="small"
maxRows={3} multiline
placeholder={t('placeholder')} maxRows={3}
value={inputMessage} placeholder={t('placeholder')}
onChange={(e) => setInputMessage(e.target.value)} value={inputMessage}
onKeyPress={handleKeyPress} onChange={(e) => setInputMessage(e.target.value)}
disabled={isLoading} onKeyPress={handleKeyPress}
variant="outlined" disabled={isLoading}
sx={{ variant="outlined"
'& .MuiOutlinedInput-root': { sx={{
'& .MuiOutlinedInput-root': {
borderRadius: 2,
}
}}
/>
<Button
variant="contained"
onClick={handleSendMessage}
disabled={!inputMessage.trim() || isLoading}
sx={{
minWidth: 'auto',
px: 2,
borderRadius: 2, borderRadius: 2,
} background: 'linear-gradient(45deg, #009688 30%, #00796B 90%)',
}} }}
/> >
<Button <Send fontSize="small" />
variant="contained" </Button>
onClick={handleSendMessage} </Box>
disabled={!inputMessage.trim() || isLoading} <Typography variant="caption" color="text.secondary" sx={{ mt: 0.5, display: 'block' }}>
sx={{ {t('enterToSend')}
minWidth: 'auto', </Typography>
px: 2,
borderRadius: 2,
background: 'linear-gradient(45deg, #009688 30%, #00796B 90%)',
}}
>
<Send fontSize="small" />
</Button>
</Box> </Box>
<Typography variant="caption" color="text.secondary" sx={{ mt: 0.5, display: 'block' }}> )}
{t('enterToSend')}
</Typography>
</Box>
</Box> </Box>
</Box> </Box>