1adc4806fa
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
132 lines
4.3 KiB
TypeScript
132 lines
4.3 KiB
TypeScript
"use client"
|
|
|
|
import { useEffect, useState } from "react"
|
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
|
|
import { Label } from "@/components/ui/label"
|
|
import { Button } from "@/components/ui/button"
|
|
import { toast } from "sonner"
|
|
import {
|
|
Select,
|
|
SelectContent,
|
|
SelectItem,
|
|
SelectTrigger,
|
|
SelectValue,
|
|
} from "@/components/ui/select"
|
|
|
|
interface Preferences {
|
|
timezone: string
|
|
dateFormat: string
|
|
itemsPerPage: number
|
|
}
|
|
|
|
export function UserPreferencesForm() {
|
|
const [prefs, setPrefs] = useState<Preferences>({
|
|
timezone: "america-los_angeles",
|
|
dateFormat: "mdy",
|
|
itemsPerPage: 20,
|
|
})
|
|
const [loading, setLoading] = useState(true)
|
|
const [saving, setSaving] = useState(false)
|
|
|
|
useEffect(() => {
|
|
async function load() {
|
|
try {
|
|
const res = await fetch("/api/settings/preferences")
|
|
if (res.ok) {
|
|
const json = await res.json()
|
|
setPrefs(json)
|
|
}
|
|
} catch {
|
|
console.warn("Failed to load user preferences")
|
|
} finally {
|
|
setLoading(false)
|
|
}
|
|
}
|
|
load()
|
|
}, [])
|
|
|
|
async function handleSave() {
|
|
setSaving(true)
|
|
try {
|
|
const res = await fetch("/api/settings/preferences", {
|
|
method: "PATCH",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify(prefs),
|
|
})
|
|
if (res.ok) {
|
|
toast.success("Preferences saved")
|
|
} else {
|
|
toast.error("Failed to save preferences")
|
|
}
|
|
} catch {
|
|
console.warn("Failed to save user preferences")
|
|
toast.error("Failed to save preferences")
|
|
} finally {
|
|
setSaving(false)
|
|
}
|
|
}
|
|
|
|
return (
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle>User Preferences</CardTitle>
|
|
<CardDescription>
|
|
Customize your experience and default settings.
|
|
</CardDescription>
|
|
</CardHeader>
|
|
<CardContent className="space-y-4">
|
|
<div className="grid gap-4 sm:grid-cols-2">
|
|
<div className="space-y-2">
|
|
<Label htmlFor="timezone">Timezone</Label>
|
|
<Select disabled={loading} value={prefs.timezone} onValueChange={(v) => setPrefs((p) => ({ ...p, timezone: v }))}>
|
|
<SelectTrigger id="timezone">
|
|
<SelectValue />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
<SelectItem value="america-new_york">Eastern Time (ET)</SelectItem>
|
|
<SelectItem value="america-chicago">Central Time (CT)</SelectItem>
|
|
<SelectItem value="america-denver">Mountain Time (MT)</SelectItem>
|
|
<SelectItem value="america-los_angeles">Pacific Time (PT)</SelectItem>
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
<div className="space-y-2">
|
|
<Label htmlFor="date-format">Date Format</Label>
|
|
<Select disabled={loading} value={prefs.dateFormat} onValueChange={(v) => setPrefs((p) => ({ ...p, dateFormat: v }))}>
|
|
<SelectTrigger id="date-format">
|
|
<SelectValue />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
<SelectItem value="mdy">MM/DD/YYYY</SelectItem>
|
|
<SelectItem value="dmy">DD/MM/YYYY</SelectItem>
|
|
<SelectItem value="ymd">YYYY-MM-DD</SelectItem>
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
<div className="space-y-2">
|
|
<Label htmlFor="items-per-page">Items Per Page</Label>
|
|
<Select disabled={loading} value={String(prefs.itemsPerPage)} onValueChange={(v) => setPrefs((p) => ({ ...p, itemsPerPage: parseInt(v) }))}>
|
|
<SelectTrigger id="items-per-page">
|
|
<SelectValue />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
<SelectItem value="10">10</SelectItem>
|
|
<SelectItem value="20">20</SelectItem>
|
|
<SelectItem value="50">50</SelectItem>
|
|
<SelectItem value="100">100</SelectItem>
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
</div>
|
|
<form onSubmit={(e) => { e.preventDefault(); handleSave(); }}>
|
|
<div className="flex justify-end pt-4">
|
|
<Button type="submit" disabled={loading || saving}>
|
|
{saving ? "Saving..." : "Save Preferences"}
|
|
</Button>
|
|
</div>
|
|
</form>
|
|
</CardContent>
|
|
</Card>
|
|
)
|
|
}
|