Security architecture upgrade + bug reporting system

Database & Security:
- Dual password storage: bcrypt (auth) + pgcrypto AES-256 (recovery)
- SUPER_ADMIN master key recovery system (master_keys table)
- Row Level Security on customers, leads, opportunities, communications, tasks
  - SALES_USER: own records only
  - ADMIN: all records
  - SUPER_ADMIN: all records (bypasses RLS)
- Immutable audit logs (DELETE/UPDATE blocked by triggers)
- New audit event types: BUG_CREATED, BUG_UPDATED, BUG_ASSIGNED,
  BUG_RESOLVED, LOGIN, LOGOUT
- Database export logging (database_export_logs table)
- Backup logging with pg_dump script (scripts/backup.ps1)
- Fixed audit constraint to allow new action types

Authentication:
- Random JWT secret generated on every dev server start
  (invalidates all prior sessions after restart)
- Session cookie is now session-only (no maxAge)
- setSessionContext() for RLS integration

Bug Reporting System:
- bug_reports table with RLS (insert by all, select/update by admin only)
- POST /api/bug-reports (any authenticated user)
- GET /api/bug-reports (admin/super_admin only)
- PATCH /api/bug-reports/:id (admin/super_admin only)
- POST /api/auth/recover (super_admin password recovery)
- Audit logging for all bug report actions

Other:
- Added 'dev' to UserRole type
- Bug report modal UI with severity selector
- Added bug report button to topbar
This commit is contained in:
2026-06-26 11:13:28 +02:00
parent 9bbaf70145
commit 20a1744e7f
13 changed files with 1365 additions and 5 deletions
+118
View File
@@ -0,0 +1,118 @@
import { NextRequest, NextResponse } from "next/server"
import { query } from "@/lib/db"
import { getSessionUser } from "@/lib/auth"
export async function POST(request: NextRequest) {
try {
const sessionUser = await getSessionUser()
if (!sessionUser) {
return NextResponse.json({ error: "Not authenticated." }, { status: 401 })
}
const { title, description, severity, page_url, screenshot_url } = await request.json()
if (!title || !description) {
return NextResponse.json({ error: "Title and description are required." }, { status: 400 })
}
const validSeverities = ["low", "medium", "high", "critical"]
if (severity && !validSeverities.includes(severity)) {
return NextResponse.json({ error: "Invalid severity." }, { status: 400 })
}
const result = await query(
`INSERT INTO bug_reports (reported_by, title, description, severity, page_url, screenshot_url)
VALUES ($1, $2, $3, $4, $5, $6)
RETURNING id, created_at`,
[sessionUser.id, title, description, severity || "medium", page_url || null, screenshot_url || null]
)
return NextResponse.json({
success: true,
id: result.rows[0].id,
created_at: result.rows[0].created_at,
}, { status: 201 })
} catch (error) {
console.error("Error creating bug report:", error)
return NextResponse.json({ error: "Failed to submit bug report." }, { status: 500 })
}
}
export async function GET(request: NextRequest) {
try {
const sessionUser = await getSessionUser()
if (!sessionUser) {
return NextResponse.json({ error: "Not authenticated." }, { status: 401 })
}
if (sessionUser.role !== "admin" && sessionUser.role !== "super_admin") {
return NextResponse.json({ error: "Forbidden. Only admins can view bug reports." }, { status: 403 })
}
const { searchParams } = new URL(request.url)
const status = searchParams.get("status")
const severity = searchParams.get("severity")
const limit = parseInt(searchParams.get("limit") || "50", 10)
const offset = parseInt(searchParams.get("offset") || "0", 10)
let sql = `SELECT br.id, br.title, br.description, br.severity, br.page_url,
br.screenshot_url, br.status, br.resolution_notes,
br.created_at, br.updated_at,
reporter.id AS reporter_id,
reporter.first_name AS reporter_first_name,
reporter.last_name AS reporter_last_name,
reporter.email AS reporter_email,
assignee.id AS assignee_id,
assignee.first_name AS assignee_first_name,
assignee.last_name AS assignee_last_name
FROM bug_reports br
JOIN users reporter ON reporter.id = br.reported_by
LEFT JOIN users assignee ON assignee.id = br.assigned_to`
const params: unknown[] = []
const conditions: string[] = []
if (status) {
conditions.push(`br.status = $${params.length + 1}`)
params.push(status)
}
if (severity) {
conditions.push(`br.severity = $${params.length + 1}`)
params.push(severity)
}
if (conditions.length > 0) {
sql += " WHERE " + conditions.join(" AND ")
}
sql += " ORDER BY br.created_at DESC LIMIT $" + (params.length + 1) + " OFFSET $" + (params.length + 2)
params.push(limit, offset)
const result = await query(sql, params)
const reports = result.rows.map((row: any) => ({
id: row.id,
title: row.title,
description: row.description,
severity: row.severity,
page_url: row.page_url,
screenshot_url: row.screenshot_url,
status: row.status,
resolution_notes: row.resolution_notes,
created_at: row.created_at,
updated_at: row.updated_at,
reporter: {
id: row.reporter_id,
name: `${row.reporter_first_name} ${row.reporter_last_name}`,
email: row.reporter_email,
},
assignee: row.assignee_id ? {
id: row.assignee_id,
name: `${row.assignee_first_name} ${row.assignee_last_name}`,
} : null,
}))
return NextResponse.json({ reports }, { status: 200 })
} catch (error) {
console.error("Error fetching bug reports:", error)
return NextResponse.json({ error: "Failed to fetch bug reports." }, { status: 500 })
}
}