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:
Ace
2026-06-23 09:45:30 +02:00
parent d891197d8b
commit 1adc4806fa
51 changed files with 1145 additions and 1052 deletions
+23 -8
View File
@@ -1,6 +1,6 @@
"use client"
import { useState, useEffect } from "react"
import { useState, useEffect, useCallback } from "react"
import Link from "next/link"
import { motion } from "framer-motion"
import { use } from "react"
@@ -26,22 +26,26 @@ export default function LeadDetailsPage({ params }: { params: Promise<{ id: stri
const [leadNotes, setLeadNotes] = useState<Note[]>([])
const [loading, setLoading] = useState(true)
const fetchNotes = useCallback(async () => {
const notesRes = await fetch(`/api/leads/${id}/notes`)
if (notesRes.ok) setLeadNotes(await notesRes.json())
}, [id])
useEffect(() => {
async function fetchData() {
try {
const [leadRes, notesRes] = await Promise.all([
const [leadRes] = await Promise.all([
fetch(`/api/leads/${id}`),
fetch(`/api/leads/${id}/notes`),
fetchNotes(),
])
if (leadRes.ok) setLead(await leadRes.json())
if (notesRes.ok) setLeadNotes(await notesRes.json())
} catch {
// ignore
console.warn("Failed to fetch lead details")
}
setLoading(false)
}
fetchData()
}, [id])
}, [id, fetchNotes])
if (loading) {
return (
@@ -89,8 +93,19 @@ export default function LeadDetailsPage({ params }: { params: Promise<{ id: stri
>
<Select
value={lead.status}
onValueChange={(v) => {
onValueChange={async (v) => {
const previousStatus = lead.status
setLead((prev) => prev ? { ...prev, status: v as Lead["status"] } : prev)
try {
const res = await fetch(`/api/leads/${id}`, {
method: "PATCH",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ status: v }),
})
if (!res.ok) setLead((prev) => prev ? { ...prev, status: previousStatus } : prev)
} catch {
setLead((prev) => prev ? { ...prev, status: previousStatus } : prev)
}
}}
>
<SelectTrigger className="h-9 w-[140px]">
@@ -125,7 +140,7 @@ export default function LeadDetailsPage({ params }: { params: Promise<{ id: stri
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.3, delay: 0.1 }}
>
<NoteForm leadId={lead.id} />
<NoteForm leadId={lead.id} onNoteAdded={fetchNotes} />
</motion.div>
<motion.div
+29 -31
View File
@@ -1,6 +1,6 @@
"use client"
import { useState } from "react"
import { useState, useEffect } from "react"
import { useRouter } from "next/navigation"
import Link from "next/link"
import { useForm } from "react-hook-form"
@@ -26,9 +26,7 @@ import {
} from "@/components/ui/select"
import { Card, CardContent } from "@/components/ui/card"
import { ArrowLeft } from "lucide-react"
import { Lead, LeadStatus } from "@/types"
import { leads } from "@/data/leads"
import { users } from "@/data/users"
import { User } from "@/types"
import { useNotifications } from "@/providers/notification-provider"
import { LEAD_SOURCES, LEAD_STATUSES } from "@/lib/constants"
@@ -49,6 +47,14 @@ export default function CreateLeadPage() {
const router = useRouter()
const { addNotification } = useNotifications()
const [saving, setSaving] = useState(false)
const [users, setUsers] = useState<User[]>([])
useEffect(() => {
fetch("/api/users")
.then((r) => r.json())
.then((data) => setUsers(data.users || []))
.catch(() => {})
}, [])
const form = useForm<LeadFormValues>({
resolver: zodResolver(leadFormSchema),
@@ -64,35 +70,27 @@ export default function CreateLeadPage() {
},
})
function onSubmit(values: LeadFormValues) {
async function onSubmit(values: LeadFormValues) {
setSaving(true)
const now = new Date().toISOString()
const assignedUserId: string | null = values.assignedUserId === "none" ? null : (values.assignedUserId ?? null)
const assignedUser = assignedUserId ? users.find((u) => u.id === assignedUserId) ?? null : null
const newLead: Lead = {
id: `lead-${String(leads.length + 1).padStart(3, "0")}`,
companyName: values.companyName,
contactName: values.contactName,
email: values.email,
phone: values.phone ?? "",
source: values.source ?? "",
description: values.description ?? "",
status: values.status as LeadStatus,
assignedUserId,
assignedUser,
createdAt: now,
updatedAt: now,
try {
const payload = {
...values,
assignedUserId: values.assignedUserId === "none" ? null : (values.assignedUserId ?? null),
}
const res = await fetch("/api/leads", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(payload),
})
if (!res.ok) throw new Error("Failed to create lead")
const data = await res.json()
addNotification("lead_created", "New Lead Created", `${values.companyName}${values.contactName}`, `/leads/${data.id}`)
router.push("/leads")
} catch (e) {
console.error("Create lead error:", e)
} finally {
setSaving(false)
}
leads.unshift(newLead)
addNotification(
"lead_created",
"New Lead Created",
`${newLead.companyName}${newLead.contactName}`,
`/leads/${newLead.id}`
)
router.push("/leads")
}
return (