mirror of
https://git.coastit.co.za/caitlin/CRM_ENVR.git
synced 2026-07-10 11:15:43 +02:00
Files Changed
Security Fixes
File Change
.env.local Placeholder JWT secret (rotate to random value: openssl rand -base64 64)
src/middleware.ts Removed /api/auth/me bypass; API routes return JSON 401 (not 302 redirect)
src/lib/auth.ts sameSite: "lax" → "strict" (create + destroy cookie); SVG name chars are pre-computed (no injection)
src/lib/avatar.ts Added sanitizeName() — strips non-alphanumeric/safe chars, max 50 chars
src/app/api/users/route.ts POST restricted to super_admin; validates role against whitelist ["sales","admin","super_admin","dev"]; validates active defaults to true
src/app/api/users/[id]/route.ts DELETE restricted to super_admin; blocks self-deletion
src/app/api/leads/[id]/route.ts GET: non-admin users filtered by assigned_to; PATCH: ownership check + score validated 0-100 range
src/app/api/leads/route.ts DELETE: ownership + admin check; GET: ownership filter for non-admin users
src/app/api/conversations/[id]/messages/route.ts GET/POST: verify user is a conversation_participant before access
rust-ai/src/main.rs CORS: AllowOrigin::list(["localhost:3006", "127.0.0.1:3006"]) (was Any)
Performance Fixes
File Change
src/app/api/leads/route.ts Added limit (default 50), offset query params; ownership filter in SQL
src/app/api/users/route.ts Added limit (default 50), offset query params
src/app/api/conversations/route.ts Added LIMIT 50
src/app/api/leads/[id]/notes/route.ts Added limit (default 50), offset query params
src/app/api/conversations/[id]/messages/route.ts Added limit (default 100), offset, before query params
src/app/(dashboard)/chats/page.tsx Pruned to 5 cached conversations; useMemo wraps messages/conversation/filteredConversations; otherParticipant moved outside component; added 10MB file size limit; recordingChunksRef cleared on discard
src/app/(dashboard)/leads/[id]/page.tsx Optimistic status update rolls back on fetch failure
database/migrations/003_chat.sql Added composite index idx_messages_conversation_created on (conversation_id, created_at DESC)
Code Quality Fixes
File Change
src/lib/ai.ts Removed dead getInstructions()/updateInstructions() (called nonexistent /ai/instructions)
src/lib/auth.ts bcrypt.hash(password, 12) — kept (acceptable, OWASP-recommended)
src/app/login/page.tsx "Forgot password?" button now shows alert to contact admin (was no-op)
src/components/users/user-form-dialog.tsx Password min 6 → 8 chars
26 silent catch {} blocks across 16 files (see below) Added console.warn("...") context messages
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
"use client"
|
||||
|
||||
import { useState, useRef, useCallback, useEffect } from "react"
|
||||
import { useState, useRef, useCallback, useEffect, useMemo } from "react"
|
||||
import { cn } from "@/lib/utils"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Input } from "@/components/ui/input"
|
||||
@@ -90,6 +90,9 @@ function VoiceMessagePlayer({ src, initialDuration }: { src: string; initialDura
|
||||
)
|
||||
}
|
||||
|
||||
const MAX_CACHED_CONVERSATIONS = 5
|
||||
const otherParticipant = (conv: any) => conv.otherUser
|
||||
|
||||
export default function ChatsPage() {
|
||||
const { theme } = useTheme()
|
||||
const { user } = useUser()
|
||||
@@ -129,6 +132,8 @@ export default function ChatsPage() {
|
||||
const mediaRecorderRef = useRef<MediaRecorder | null>(null)
|
||||
const recordingChunksRef = useRef<Blob[]>([])
|
||||
const resizeStartRef = useRef({ x: 0, width: 0 })
|
||||
const blobUrlsRef = useRef<string[]>([])
|
||||
const conversationOrderRef = useRef<string[]>([])
|
||||
|
||||
const isOnlyEmoji = (text: string) => /^(\p{Extended_Pictographic}\s*)+$/u.test(text.trim())
|
||||
|
||||
@@ -141,12 +146,12 @@ export default function ChatsPage() {
|
||||
|
||||
if (!user) return null
|
||||
|
||||
const messages = conversationMessages.get(activeChat || "") || []
|
||||
const conversation = conversations.find((c) => c.id === activeChat)
|
||||
const otherParticipant = (conv: any) => conv.otherUser
|
||||
const filteredConversations = searchQuery.trim()
|
||||
? conversations.filter((conv) => otherParticipant(conv).name.toLowerCase().includes(searchQuery.toLowerCase()))
|
||||
: conversations
|
||||
const messages = useMemo(() => conversationMessages.get(activeChat || "") || [], [conversationMessages, activeChat])
|
||||
const conversation = useMemo(() => conversations.find((c) => c.id === activeChat), [conversations, activeChat])
|
||||
const filteredConversations = useMemo(() => {
|
||||
if (!searchQuery.trim()) return conversations
|
||||
return conversations.filter((conv) => otherParticipant(conv).name.toLowerCase().includes(searchQuery.toLowerCase()))
|
||||
}, [conversations, searchQuery])
|
||||
|
||||
// Fetch conversations from API
|
||||
useEffect(() => {
|
||||
@@ -161,7 +166,7 @@ export default function ChatsPage() {
|
||||
if (convs.length > 0) setActiveChat(convs[0].id)
|
||||
}
|
||||
} catch {
|
||||
// ignore
|
||||
console.warn("Failed to fetch conversations in chats page")
|
||||
} finally {
|
||||
setLoadingChats(false)
|
||||
}
|
||||
@@ -172,6 +177,10 @@ export default function ChatsPage() {
|
||||
// Fetch messages when active chat changes, and mark as read
|
||||
useEffect(() => {
|
||||
if (!activeChat) return
|
||||
conversationOrderRef.current = [
|
||||
activeChat,
|
||||
...conversationOrderRef.current.filter((id) => id !== activeChat),
|
||||
]
|
||||
const fetchMsgs = async () => {
|
||||
try {
|
||||
const res = await fetch(`/api/conversations/${activeChat}/messages`)
|
||||
@@ -180,6 +189,30 @@ export default function ChatsPage() {
|
||||
setConversationMessages((prev) => {
|
||||
const next = new Map(prev)
|
||||
next.set(activeChat, data.messages || [])
|
||||
const recent = conversationOrderRef.current.slice(0, MAX_CACHED_CONVERSATIONS)
|
||||
const recentSet = new Set(recent)
|
||||
for (const key of next.keys()) {
|
||||
if (!recentSet.has(key)) next.delete(key)
|
||||
}
|
||||
const validMsgIds = new Set<string>()
|
||||
for (const msgs of next.values()) {
|
||||
for (const m of msgs) validMsgIds.add(m.id)
|
||||
}
|
||||
setVoiceMessages((v) => {
|
||||
const n = new Map(v)
|
||||
for (const k of n.keys()) if (!validMsgIds.has(k)) n.delete(k)
|
||||
return n
|
||||
})
|
||||
setMessageAttachments((a) => {
|
||||
const n = new Map(a)
|
||||
for (const k of n.keys()) if (!validMsgIds.has(k)) n.delete(k)
|
||||
return n
|
||||
})
|
||||
setReplyMap((r) => {
|
||||
const n = new Map(r)
|
||||
for (const k of n.keys()) if (!validMsgIds.has(k)) n.delete(k)
|
||||
return n
|
||||
})
|
||||
return next
|
||||
})
|
||||
}
|
||||
@@ -206,7 +239,7 @@ export default function ChatsPage() {
|
||||
const existingIds = new Set(conversations.map((c) => otherParticipant(c).id))
|
||||
setSearchResults(data.users.filter((u: any) => !existingIds.has(u.id)))
|
||||
}
|
||||
} catch { /* ignore */ }
|
||||
} catch { console.warn("Failed to search users in chats page") }
|
||||
setSearchingUsers(false)
|
||||
}, 300)
|
||||
return () => clearTimeout(timer)
|
||||
@@ -222,6 +255,14 @@ export default function ChatsPage() {
|
||||
return () => document.removeEventListener("mousedown", handleClickOutside)
|
||||
}, [])
|
||||
|
||||
// revoke blob URLs on unmount
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
blobUrlsRef.current.forEach((url) => URL.revokeObjectURL(url))
|
||||
blobUrlsRef.current = []
|
||||
}
|
||||
}, [])
|
||||
|
||||
const handleResizeStart = useCallback((e: React.MouseEvent) => {
|
||||
e.preventDefault()
|
||||
setIsResizing(true)
|
||||
@@ -254,8 +295,16 @@ export default function ChatsPage() {
|
||||
setTimeout(() => autoResizeTextarea(), 0)
|
||||
}
|
||||
|
||||
const MAX_FILE_SIZE = 10 * 1024 * 1024 // 10MB
|
||||
|
||||
const handleFileSelect = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const files = Array.from(e.target.files ?? [])
|
||||
const oversized = files.filter((f) => f.size > MAX_FILE_SIZE)
|
||||
if (oversized.length > 0) {
|
||||
toast.error(`${oversized[0].name} exceeds the 10MB file size limit`)
|
||||
if (e.target) e.target.value = ""
|
||||
return
|
||||
}
|
||||
setAttachments((prev) => [...prev, ...files])
|
||||
if (e.target) e.target.value = ""
|
||||
}
|
||||
@@ -319,6 +368,7 @@ export default function ChatsPage() {
|
||||
})
|
||||
}
|
||||
} catch {
|
||||
console.warn("Failed to send message in chats page")
|
||||
toast.error("Failed to send message")
|
||||
}
|
||||
}
|
||||
@@ -341,7 +391,11 @@ export default function ChatsPage() {
|
||||
return
|
||||
}
|
||||
const fileAttachments = attachments.length > 0
|
||||
? attachments.map((f) => ({ name: f.name, type: f.type, url: URL.createObjectURL(f) }))
|
||||
? attachments.map((f) => {
|
||||
const url = URL.createObjectURL(f)
|
||||
blobUrlsRef.current.push(url)
|
||||
return { name: f.name, type: f.type, url }
|
||||
})
|
||||
: undefined
|
||||
if (replyingTo) {
|
||||
const replySender = replyingTo.senderId === user.id ? user.name : otherParticipant(conversation!).name
|
||||
@@ -356,6 +410,10 @@ export default function ChatsPage() {
|
||||
}
|
||||
|
||||
const handleDeleteMessage = (messageId: string) => {
|
||||
const files = messageAttachments.get(messageId)
|
||||
if (files) files.forEach((f) => { URL.revokeObjectURL(f.url); blobUrlsRef.current = blobUrlsRef.current.filter((u) => u !== f.url) })
|
||||
const voice = voiceMessages.get(messageId)
|
||||
if (voice) { URL.revokeObjectURL(voice.url); blobUrlsRef.current = blobUrlsRef.current.filter((u) => u !== voice.url) }
|
||||
setConversationMessages((prev) => {
|
||||
const next = new Map(prev)
|
||||
const msgs = (next.get(activeChat || "") || []).filter((m) => m.id !== messageId)
|
||||
@@ -383,6 +441,7 @@ export default function ChatsPage() {
|
||||
recorder.onstop = () => {
|
||||
const blob = new Blob(recordingChunksRef.current, { type: "audio/webm" })
|
||||
const url = URL.createObjectURL(blob)
|
||||
blobUrlsRef.current.push(url)
|
||||
addMessageToChat("", { url, duration: recordingDurationRef.current })
|
||||
stream.getTracks().forEach((t) => t.stop())
|
||||
setRecordingDuration(0)
|
||||
@@ -392,6 +451,7 @@ export default function ChatsPage() {
|
||||
setIsRecording(true)
|
||||
setIsPaused(false)
|
||||
} catch {
|
||||
console.warn("Failed to start recording in chats page")
|
||||
toast.error("Microphone access denied")
|
||||
}
|
||||
}
|
||||
@@ -425,6 +485,7 @@ export default function ChatsPage() {
|
||||
setIsPaused(false)
|
||||
setRecordingDuration(0)
|
||||
recordingDurationRef.current = 0
|
||||
recordingChunksRef.current = []
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
@@ -532,7 +593,7 @@ export default function ChatsPage() {
|
||||
setSearchQuery("")
|
||||
}
|
||||
}
|
||||
} catch { /* ignore */ }
|
||||
} catch { console.warn("Failed to create conversation in chats page") }
|
||||
}}
|
||||
className="w-full flex items-center gap-3 p-4 text-left transition-colors hover:bg-muted/50"
|
||||
>
|
||||
@@ -911,6 +972,7 @@ export default function ChatsPage() {
|
||||
toast.success("Message forwarded")
|
||||
}
|
||||
} catch {
|
||||
console.warn("Failed to forward message in chats page")
|
||||
toast.error("Failed to forward message")
|
||||
}
|
||||
}}
|
||||
|
||||
Reference in New Issue
Block a user