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:
+5
-23
@@ -1,10 +1,10 @@
|
||||
const AI_SERVICE = process.env.AI_SERVICE_URL || "http://localhost:3001"
|
||||
|
||||
export async function chatWithAI(message: string, userId: string, userRole: string) {
|
||||
export async function chatWithAI(message: string, jwtToken: string) {
|
||||
const res = await fetch(`${AI_SERVICE}/ai/chat`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ message, user_id: userId, user_role: userRole }),
|
||||
headers: { "Content-Type": "application/json", "Authorization": `Bearer ${jwtToken}` },
|
||||
body: JSON.stringify({ message }),
|
||||
})
|
||||
|
||||
if (!res.ok) {
|
||||
@@ -23,30 +23,11 @@ export async function fetchJobs() {
|
||||
const data = await res.json()
|
||||
return data.jobs || []
|
||||
} catch {
|
||||
console.warn("Failed to fetch AI jobs")
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
export async function getInstructions() {
|
||||
try {
|
||||
const res = await fetch(`${AI_SERVICE}/ai/instructions`)
|
||||
if (!res.ok) return null
|
||||
const data = await res.json()
|
||||
return data.success ? data.instructions : null
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
export async function updateInstructions(entry: string, content?: string) {
|
||||
const res = await fetch(`${AI_SERVICE}/ai/instructions`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ entry, content }),
|
||||
})
|
||||
return res.ok
|
||||
}
|
||||
|
||||
export async function checkAiServiceStatus() {
|
||||
try {
|
||||
const res = await fetch(`${AI_SERVICE}/health`)
|
||||
@@ -54,6 +35,7 @@ export async function checkAiServiceStatus() {
|
||||
const data = await res.json()
|
||||
return data.status === "ok"
|
||||
} catch {
|
||||
console.warn("Failed to check AI service status")
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
+9
-9
@@ -3,9 +3,11 @@ import bcrypt from "bcryptjs";
|
||||
import { query } from "@/lib/db";
|
||||
import { cookies } from "next/headers";
|
||||
|
||||
const JWT_SECRET = new TextEncoder().encode(
|
||||
process.env.JWT_SECRET || "fallback-dev-secret-do-not-use-in-production",
|
||||
);
|
||||
const RAW_SECRET = process.env.JWT_SECRET;
|
||||
if (!RAW_SECRET) {
|
||||
throw new Error("JWT_SECRET environment variable is required");
|
||||
}
|
||||
const JWT_SECRET = new TextEncoder().encode(RAW_SECRET);
|
||||
|
||||
const SESSION_COOKIE = "session";
|
||||
const MAX_FAILED_ATTEMPTS = 5;
|
||||
@@ -174,7 +176,7 @@ export function mapDbUserToSessionUser(
|
||||
SUPER_ADMIN: "super_admin",
|
||||
ADMIN: "admin",
|
||||
SALES_USER: "sales",
|
||||
DEVELOPER: "sales",
|
||||
DEVELOPER: "dev",
|
||||
};
|
||||
|
||||
const avatarUrl = dbUser.avatar_url as string | null
|
||||
@@ -186,9 +188,7 @@ export function mapDbUserToSessionUser(
|
||||
firstName: dbUser.first_name as string,
|
||||
lastName: dbUser.last_name as string,
|
||||
role: roleMapping[roleName] || roleName.toLowerCase(),
|
||||
avatar: avatarUrl || `https://ui-avatars.com/api/?name=${encodeURIComponent(
|
||||
`${dbUser.first_name}+${dbUser.last_name}`,
|
||||
)}&background=1d4ed8&color=fff&size=128`,
|
||||
avatar: avatarUrl || (() => { const f = ((dbUser.first_name as string)?.[0] || '').toUpperCase(); const l = ((dbUser.last_name as string)?.[0] || '').toUpperCase(); return `data:image/svg+xml,${encodeURIComponent(`<svg xmlns="http://www.w3.org/2000/svg" width="128" height="128" viewBox="0 0 128 128"><rect width="128" height="128" fill="#1d4ed8"/><text x="64" y="80" font-size="48" fill="white" text-anchor="middle" font-family="Arial,Helvetica,sans-serif">${f}${l}</text></svg>`)}` })(),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -199,7 +199,7 @@ export async function createSession(userId: string, role: string) {
|
||||
cookieStore.set(SESSION_COOKIE, token, {
|
||||
httpOnly: true,
|
||||
secure: process.env.NODE_ENV === "production",
|
||||
sameSite: "lax",
|
||||
sameSite: "strict",
|
||||
path: "/",
|
||||
maxAge: 60 * 60 * 24, // 24 hours
|
||||
});
|
||||
@@ -212,7 +212,7 @@ export async function destroySession() {
|
||||
cookieStore.set(SESSION_COOKIE, "", {
|
||||
httpOnly: true,
|
||||
secure: process.env.NODE_ENV === "production",
|
||||
sameSite: "lax",
|
||||
sameSite: "strict",
|
||||
path: "/",
|
||||
maxAge: 0,
|
||||
});
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
function sanitizeName(name: string): string {
|
||||
return name.replace(/[^a-zA-Z0-9\s\-'.-]/g, "").slice(0, 50)
|
||||
}
|
||||
|
||||
export function getInitials(name: string): string {
|
||||
return sanitizeName(name)
|
||||
.split(/\s+/)
|
||||
.filter(Boolean)
|
||||
.map((n) => n[0])
|
||||
.join("")
|
||||
.toUpperCase()
|
||||
.slice(0, 2)
|
||||
}
|
||||
|
||||
export function avatarSvgUrl(name: string, bg = "1d4ed8", size = 128): string {
|
||||
const initials = getInitials(name)
|
||||
const fontSize = Math.round(size * 0.375)
|
||||
const y = Math.round(size * 0.625)
|
||||
return `data:image/svg+xml,${encodeURIComponent(
|
||||
`<svg xmlns="http://www.w3.org/2000/svg" width="${size}" height="${size}" viewBox="0 0 ${size} ${size}"><rect width="${size}" height="${size}" fill="#${bg}"/><text x="${Math.round(size / 2)}" y="${y}" font-size="${fontSize}" fill="white" text-anchor="middle" font-family="Arial,Helvetica,sans-serif">${initials}</text></svg>`
|
||||
)}`
|
||||
}
|
||||
|
||||
export function getAvatarUrl(name: string, avatarUrl: string | null | undefined, bg = "1d4ed8", size = 128): string {
|
||||
return avatarUrl || avatarSvgUrl(name, bg, size)
|
||||
}
|
||||
+6
-1
@@ -1,10 +1,15 @@
|
||||
import { Pool } from "pg"
|
||||
|
||||
const dbUrl = process.env.DATABASE_URL
|
||||
if (!dbUrl) {
|
||||
throw new Error("DATABASE_URL environment variable is required")
|
||||
}
|
||||
const pool = new Pool({
|
||||
connectionString: process.env.DATABASE_URL,
|
||||
connectionString: dbUrl,
|
||||
max: 20,
|
||||
idleTimeoutMillis: 30000,
|
||||
connectionTimeoutMillis: 5000,
|
||||
ssl: dbUrl.includes("localhost") || dbUrl.includes("127.0.0.1") ? false : { rejectUnauthorized: false },
|
||||
})
|
||||
|
||||
pool.on("error", (err) => {
|
||||
|
||||
Reference in New Issue
Block a user