Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 16 additions & 16 deletions backend/src/agents/legalAgent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,14 +30,15 @@ export class AgentOrchestrator {
public negotiationAgent = new NegotiationAgent();
public askLawyerAgent = new AskLawyerAgent();

async runAnalysis(documentId: string, content: string, userId: string): Promise<any> {
async runAnalysis(documentId: string, content: string, userId: string, dbClient?: any): Promise<any> {
const startedAt = Date.now();
const client = dbClient || pool;
try {
const result = await this.analysisAgent.runAudit(content, "NDA"); // Assuming NDA for now or extracting from metadata

await pool.query(
"UPDATE files SET analysis = $1 WHERE id = $2 AND creator_id = $3",
[JSON.stringify(result), documentId, userId]
await client.query(
"UPDATE files SET analysis = $1 WHERE id = $2 AND (creator_id = current_setting('app.current_user_id', true) OR current_setting('app.current_user_role', true) = 'ADMIN')",
[JSON.stringify(result), documentId]
);
Comment on lines +39 to 42

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Background analysis updates can silently no-op without a request-scoped client.

When dbClient is absent, this predicate depends on current_setting(...); in non-request paths it can evaluate false, so UPDATE files affects 0 rows. backend/src/services/jobQueue.ts Line 286 calls runAnalysis(documentId, content, userId) without dbClient, so jobs can report success while analysis is never persisted.

Proposed fix
-      await client.query(
-        "UPDATE files SET analysis = $1 WHERE id = $2 AND (creator_id = current_setting('app.current_user_id', true) OR current_setting('app.current_user_role', true) = 'ADMIN')",
-        [JSON.stringify(result), documentId]
-      );
+      const updateRes = dbClient
+        ? await client.query(
+            "UPDATE files SET analysis = $1 WHERE id = $2 AND (creator_id = current_setting('app.current_user_id', true) OR current_setting('app.current_user_role', true) = 'ADMIN')",
+            [JSON.stringify(result), documentId]
+          )
+        : await client.query(
+            "UPDATE files SET analysis = $1 WHERE id = $2 AND creator_id = $3",
+            [JSON.stringify(result), documentId, userId]
+          );
+
+      if (updateRes.rowCount === 0) {
+        throw new Error("Analysis update denied or target document not found.");
+      }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
await client.query(
"UPDATE files SET analysis = $1 WHERE id = $2 AND (creator_id = current_setting('app.current_user_id', true) OR current_setting('app.current_user_role', true) = 'ADMIN')",
[JSON.stringify(result), documentId]
);
const updateRes = dbClient
? await client.query(
"UPDATE files SET analysis = $1 WHERE id = $2 AND (creator_id = current_setting('app.current_user_id', true) OR current_setting('app.current_user_role', true) = 'ADMIN')",
[JSON.stringify(result), documentId]
)
: await client.query(
"UPDATE files SET analysis = $1 WHERE id = $2 AND creator_id = $3",
[JSON.stringify(result), documentId, userId]
);
if (updateRes.rowCount === 0) {
throw new Error("Analysis update denied or target document not found.");
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/src/agents/legalAgent.ts` around lines 39 - 42, The UPDATE can
silently no-op when current_setting(...) is unavailable; change the persistence
path to require or use a request-scoped DB client and explicit creator id
instead of relying on current_setting. Concretely, update runAnalysis (and any
callers such as jobQueue.runAnalysis) to accept a dbClient parameter (or throw
if not provided), and in legalAgent.ts replace the client/query call to use that
dbClient and a creator_id parameter (use the userId passed into runAnalysis) in
the WHERE clause rather than current_setting('app.current_user_id', true);
ensure callers pass the request-scoped client through so the UPDATE affects rows
as intended.


await this.saveAgentLogs({
Expand All @@ -54,7 +55,7 @@ export class AgentOrchestrator {
}],
decisions: { "AnalysisAgent": { outcome: "Audit complete", confidence: 100, reasoning: "Heuristic/AI scan", actionTaken: "Updated file analysis" } },
confidenceScore: 100
});
}, dbClient);

return result;
} catch (err) {
Expand Down Expand Up @@ -116,26 +117,26 @@ Please rewrite the draft to address these risks while maintaining the original i
userId: string,
documentMode: "unified" | "individual" = "unified",
answerStyle: "narrative" | "tabular" = "narrative",
history: any[] = []
history: any[] = [],
dbClient?: any
): Promise<any> {
let client;
let client = dbClient || pool;
try {
const safeFolderIds = Array.isArray(folderIds) ? folderIds : [];

client = await pool.connect();
let files: any[] = [];

if (safeFolderIds.length === 0 || safeFolderIds.includes("root")) {
const folderFilters = safeFolderIds.filter(id => id !== "root");
const { rows } = await client.query(
"SELECT id, title, content FROM files WHERE (folder_id IS NULL OR folder_id = ANY($1)) AND creator_id = $2",
[folderFilters, userId]
"SELECT id, title, content FROM files WHERE (folder_id IS NULL OR folder_id = ANY($1)) AND (creator_id = current_setting('app.current_user_id', true) OR current_setting('app.current_user_role', true) = 'ADMIN')",
[folderFilters]
);
files = rows;
} else {
const { rows } = await client.query(
"SELECT id, title, content FROM files WHERE folder_id = ANY($1) AND creator_id = $2",
[safeFolderIds, userId]
"SELECT id, title, content FROM files WHERE folder_id = ANY($1) AND (creator_id = current_setting('app.current_user_id', true) OR current_setting('app.current_user_role', true) = 'ADMIN')",
[safeFolderIds]
);
files = rows;
}
Expand Down Expand Up @@ -163,14 +164,13 @@ Please rewrite the draft to address these risks while maintaining the original i
} catch (err: any) {
console.error("AgentOrchestrator interactAnalyze failed:", err);
throw err;
} finally {
if (client) client.release();
}
}

private async saveAgentLogs(res: any) {
private async saveAgentLogs(res: any, dbClient?: any) {
const client = dbClient || pool;
try {
await pool.query(`
await client.query(`
INSERT INTO agent_execution_logs (file_id, user_id, agent_name, task_name, execution_path, decisions, confidence_score, status)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8);
`, [
Expand Down
9 changes: 6 additions & 3 deletions backend/src/controllers/admin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { pool } from "../config/database.js";

export const approveUser = async (req: Request, res: Response) => {
const { userId, role, status } = req.body;
const client = req.dbClient || pool;
if (!userId) {
return res.status(400).json({ error: "userId is required." });
}
Expand All @@ -11,7 +12,7 @@ export const approveUser = async (req: Request, res: Response) => {
const finalRole = role || 'USER';
const finalStatus = status || 'APPROVED';

await pool.query(
await client.query(
"UPDATE users SET status = $1, role = $2, approved_at = CASE WHEN $1 = 'APPROVED' THEN CURRENT_TIMESTAMP ELSE approved_at END WHERE id = $3",
[finalStatus, finalRole, userId]
);
Expand All @@ -23,8 +24,9 @@ export const approveUser = async (req: Request, res: Response) => {
};

export const getAllUsers = async (req: Request, res: Response) => {
const client = req.dbClient || pool;
try {
const { rows } = await pool.query(
const { rows } = await client.query(
"SELECT id, email, name, status, role, created_at FROM users ORDER BY created_at DESC"
);
res.json(rows);
Expand All @@ -35,8 +37,9 @@ export const getAllUsers = async (req: Request, res: Response) => {
};

export const getPendingUsers = async (req: Request, res: Response) => {
const client = req.dbClient || pool;
try {
const { rows } = await pool.query(
const { rows } = await client.query(
"SELECT id, email, name, status, role, created_at FROM users WHERE status = 'PENDING_APPROVAL' ORDER BY created_at DESC"
);
res.json(rows);
Expand Down
48 changes: 21 additions & 27 deletions backend/src/controllers/documents.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,18 +8,14 @@ import crypto from "crypto";
import * as diff from "diff";

export const getDocuments = async (req: Request, res: Response) => {
const userId = req.user!.id;
const userRole = req.user!.role;
const userEmail = req.user!.email.toLowerCase();
const client = req.dbClient || pool; // Fallback to pool if middleware bypassed (should not happen)

try {
const docs = await withTransaction(userId, userRole, async (client) => {
const { rows } = await client.query(
"SELECT * FROM files WHERE creator_id = $1 OR shared_with::jsonb @> $2::jsonb ORDER BY created_at DESC",
[userId, JSON.stringify([userEmail])]
);
return rows;
});
const { rows: docs } = await client.query(
"SELECT * FROM files WHERE creator_id = current_setting('app.current_user_id', true) OR shared_with::jsonb @> $1::jsonb ORDER BY created_at DESC",
[JSON.stringify([userEmail])]
);
Comment on lines +15 to +18

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

This list query narrows away the RLS admin override.

The files policy in backend/src/config/initDb.ts already widens visibility for ADMIN. Re-adding creator_id = current_setting('app.current_user_id', true) here means admin sessions only see their own documents (plus anything explicitly shared to their email), not the full tenant-spanning view that the new RLS layer is supposed to provide.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/src/controllers/documents.ts` around lines 15 - 18, The SQL in the
client.query call in documents.ts re-introduces a creator_id check that
overrides the intended RLS ADMIN behavior; edit the query string passed to
client.query (the one currently assigning to docs) to remove the "creator_id =
current_setting('app.current_user_id', true) OR" clause so the SQL relies on RLS
for owner visibility and only includes the shared_with::jsonb @> $1::jsonb
condition (keep the same parameter array [JSON.stringify([userEmail])] and
ordering), ensuring client.query and the docs result handling remain unchanged.


const formattedDocs = docs.map((r: any) => ({
...r,
Expand All @@ -37,19 +33,12 @@ export const getDocuments = async (req: Request, res: Response) => {
};

export const getDocumentById = async (req: Request, res: Response) => {
const userId = req.user!.id;
const userEmail = req.user!.email.toLowerCase();
const client = req.dbClient || pool;

try {
const { rows } = await pool.query("SELECT * FROM files WHERE id = $1", [req.params.id]);
const { rows } = await client.query("SELECT * FROM files WHERE id = $1", [req.params.id]);
if (rows.length > 0) {
Comment on lines +39 to 40

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

Shared documents will now 404 for non-owners.

The old application-side shared-with authorization check is gone, but the current files RLS policy only allows creator_id = current_setting('app.current_user_id', true) or ADMIN. That means users who were granted access via shared_with can no longer load document details here, and the shared_with branch in getDocuments cannot recover those rows on an RLS-scoped client either.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/src/controllers/documents.ts` around lines 39 - 40, The single-row
fetch for the document in the documents controller is failing for users who have
access via shared_with because the SQL only checks id and relies on RLS that
restricts to creator_id/current user; update the query used in the document
fetch handler (the SELECT against the files table in this controller) to also
allow rows where the current user is listed in the file's shared access (e.g.
add an OR condition using current_setting('app.current_user_id', true)::uuid =
ANY(shared_with) or an EXISTS(...) join against the shared access table), so the
SQL returns the row when the requester is in shared_with and not just the
creator or admin; reference the same identifying symbols "files", "shared_with",
and current_setting('app.current_user_id', true) when making the change.

const r = rows[0];
const isShared = r.shared_with.some((e: string) => e.toLowerCase() === userEmail);
const isOwner = r.creator_id === userId;

if (!isOwner && !isShared) {
return res.status(403).json({ error: "Access denied to this document." });
}

const doc = {
...r,
Expand All @@ -75,12 +64,13 @@ export const createDocument = async (req: Request, res: Response) => {
const { title, type, content } = req.body;
const userId = req.user!.id;
const email = req.user!.email;
const client = req.dbClient || pool;

const id = "doc_" + crypto.randomUUID();
const encryptedContent = encryptData(content || "");

try {
await pool.query(
await client.query(
`INSERT INTO files (id, title, type, content, creator_id, creator_email, is_encrypted, versions, shared_with, audit_logs)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)`,
[id, title, type, encryptedContent, userId, email, true, JSON.stringify([]), JSON.stringify([]), JSON.stringify([])]
Expand All @@ -98,9 +88,10 @@ export const uploadDocument = async (req: Request, res: Response) => {
const { title, folder_id } = req.body;
const fileId = "doc_" + crypto.randomUUID();
const fileTitle = title || file.originalname;
const client = req.dbClient || pool;

try {
await pool.query(
await client.query(
`INSERT INTO files (id, title, type, content, creator_id, creator_email, mime_type, folder_id)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8)`,
[fileId, fileTitle, "upload", "", req.user!.id, req.user!.email, file.mimetype, folder_id || null]
Expand All @@ -124,21 +115,23 @@ export const uploadDocument = async (req: Request, res: Response) => {
export const createRedline = async (req: Request, res: Response) => {
const { id } = req.params;
const { originalText, proposedText, comment } = req.body;
const client = req.dbClient || pool;
try {
const { rows } = await pool.query("SELECT redlines FROM files WHERE id = $1", [id]);
const { rows } = await client.query("SELECT redlines FROM files WHERE id = $1", [id]);
if (rows.length === 0) return res.status(404).json({ error: "Document not found" });
const redlines = rows[0].redlines || [];
const newRedline = { id: crypto.randomUUID(), originalText, proposedText, comment, proposedByEmail: req.user!.email, proposedAt: new Date().toISOString(), status: "pending" };
redlines.push(newRedline);
await pool.query("UPDATE files SET redlines = $1 WHERE id = $2", [JSON.stringify(redlines), id]);
await client.query("UPDATE files SET redlines = $1 WHERE id = $2", [JSON.stringify(redlines), id]);
res.status(201).json(newRedline);
} catch (err: any) { res.status(500).json({ error: err.message }); }
};

export const acceptRedline = async (req: Request, res: Response) => {
const { id, redlineId } = req.params;
const client = req.dbClient || pool;
try {
const { rows } = await pool.query("SELECT * FROM files WHERE id = $1", [id]);
const { rows } = await client.query("SELECT * FROM files WHERE id = $1", [id]);
if (rows.length === 0) return res.status(404).json({ error: "Document not found" });

const doc = rows[0];
Expand All @@ -164,10 +157,10 @@ export const acceptRedline = async (req: Request, res: Response) => {
}

redlines[index].status = "accepted";
await pool.query("UPDATE files SET content = $1, redlines = $2 WHERE id = $3", [encryptData(fallbackReplaced), JSON.stringify(redlines), id]);
await client.query("UPDATE files SET content = $1, redlines = $2 WHERE id = $3", [encryptData(fallbackReplaced), JSON.stringify(redlines), id]);
} else {
redlines[index].status = "accepted";
await pool.query("UPDATE files SET content = $1, redlines = $2 WHERE id = $3", [encryptData(applied), JSON.stringify(redlines), id]);
await client.query("UPDATE files SET content = $1, redlines = $2 WHERE id = $3", [encryptData(applied), JSON.stringify(redlines), id]);
}

res.json({ success: true });
Expand All @@ -179,14 +172,15 @@ export const acceptRedline = async (req: Request, res: Response) => {

export const rejectRedline = async (req: Request, res: Response) => {
const { id, redlineId } = req.params;
const client = req.dbClient || pool;
try {
const { rows } = await pool.query("SELECT redlines FROM files WHERE id = $1", [id]);
const { rows } = await client.query("SELECT redlines FROM files WHERE id = $1", [id]);
if (rows.length === 0) return res.status(404).json({ error: "Document not found" });
const redlines = rows[0].redlines || [];
const index = redlines.findIndex((r: any) => r.id === redlineId);
if (index === -1) return res.status(404).json({ error: "Redline not found" });
redlines[index].status = "rejected";
await pool.query("UPDATE files SET redlines = $1 WHERE id = $2", [JSON.stringify(redlines), id]);
await client.query("UPDATE files SET redlines = $1 WHERE id = $2", [JSON.stringify(redlines), id]);
res.json({ success: true });
} catch (err: any) { res.status(500).json({ error: err.message }); }
};
Expand Down
21 changes: 10 additions & 11 deletions backend/src/controllers/folders.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,11 @@ import { withTransaction } from "../utils/dbUtils.js";
import crypto from "crypto";

export const getFolders = async (req: Request, res: Response) => {
const client = req.dbClient || pool;
try {
const rows = await withTransaction(req.user!.id, req.user!.role, async (client) => {
const { rows } = await client.query(
"SELECT * FROM folders WHERE user_id = $1 ORDER BY created_at DESC",
[req.user!.id]
);
return rows;
});
const { rows } = await client.query(
"SELECT * FROM folders WHERE user_id = current_setting('app.current_user_id', true) ORDER BY created_at DESC"
);
Comment on lines +9 to +11

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

This owner filter disables the admin override for folders.

verification/verifyRLS.ts validates admin access by doing a plain SELECT on folders and letting the RLS policy widen visibility. Hard-coding user_id = current_setting('app.current_user_id', true) here narrows admins back to their own folders.

Suggested fix
-    const { rows } = await client.query(
-      "SELECT * FROM folders WHERE user_id = current_setting('app.current_user_id', true) ORDER BY created_at DESC"
-    );
+    const { rows } = await client.query(
+      "SELECT * FROM folders ORDER BY created_at DESC"
+    );
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const { rows } = await client.query(
"SELECT * FROM folders WHERE user_id = current_setting('app.current_user_id', true) ORDER BY created_at DESC"
);
const { rows } = await client.query(
"SELECT * FROM folders ORDER BY created_at DESC"
);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/src/controllers/folders.ts` around lines 9 - 11, The query in the
folders controller hard-codes "user_id = current_setting('app.current_user_id',
true)" which overrides RLS-based admin visibility; update the SQL used in the
client.query call in the folders controller (the SELECT in
backend/src/controllers/folders.ts) to remove the owner filter and simply select
from folders (e.g., "SELECT * FROM folders ORDER BY created_at DESC") so
RLS/verification/verifyRLS.ts can correctly widen visibility for admins; locate
the client.query invocation in the get/list folders handler and replace the SQL
string accordingly without adding an explicit user_id predicate.

res.json(rows);
} catch (err: any) {
res.status(500).json({ error: err.message });
Expand All @@ -20,11 +17,12 @@ export const getFolders = async (req: Request, res: Response) => {

export const createFolder = async (req: Request, res: Response) => {
const { name } = req.body;
const client = req.dbClient || pool;
if (!name) return res.status(400).json({ error: "Folder name is required." });

try {
const id = "fld_" + crypto.randomUUID();
const { rows } = await pool.query(
const { rows } = await client.query(
"INSERT INTO folders (id, name, user_id) VALUES ($1, $2, $3) RETURNING *",
[id, name, req.user!.id]
);
Expand All @@ -35,10 +33,11 @@ export const createFolder = async (req: Request, res: Response) => {
};

export const deleteFolder = async (req: Request, res: Response) => {
const client = req.dbClient || pool;
try {
const result = await pool.query(
"DELETE FROM folders WHERE id = $1 AND user_id = $2",
[req.params.id, req.user!.id]
const result = await client.query(
"DELETE FROM folders WHERE id = $1",
[req.params.id]
);
if (result.rowCount === 0) return res.status(404).json({ error: "Folder not found." });
res.json({ success: true });
Expand Down
19 changes: 11 additions & 8 deletions backend/src/controllers/jobs.ts
Original file line number Diff line number Diff line change
@@ -1,25 +1,28 @@
import { Request, Response } from "express";
import { jobRegistry } from "../services/jobQueue.js";
import { pool } from "../config/database.js";

export const getJobs = async (req: Request, res: Response) => {
const client = req.dbClient || pool;
try {
const userJobs = await jobRegistry.getUserJobs(req.user!.id);
res.json(userJobs);
// Phase 1: Security Hardening - use RLS client
const { rows } = await client.query(
"SELECT * FROM jobs WHERE user_id = current_setting('app.current_user_id', true) ORDER BY created_at DESC"
);
Comment on lines +8 to +11

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

This query defeats the RLS admin override for jobs.

The new jobs policy already uses app.current_user_id plus the ADMIN override. Keeping WHERE user_id = current_setting('app.current_user_id', true) here means admin requests can only list their own jobs, not the broader view the RLS layer is configured to allow.

Suggested fix
-    const { rows } = await client.query(
-      "SELECT * FROM jobs WHERE user_id = current_setting('app.current_user_id', true) ORDER BY created_at DESC"
-    );
+    const { rows } = await client.query(
+      "SELECT * FROM jobs ORDER BY created_at DESC"
+    );
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// Phase 1: Security Hardening - use RLS client
const { rows } = await client.query(
"SELECT * FROM jobs WHERE user_id = current_setting('app.current_user_id', true) ORDER BY created_at DESC"
);
// Phase 1: Security Hardening - use RLS client
const { rows } = await client.query(
"SELECT * FROM jobs ORDER BY created_at DESC"
);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/src/controllers/jobs.ts` around lines 8 - 11, The current
client.query call that selects from jobs using the hardcoded WHERE clause "WHERE
user_id = current_setting('app.current_user_id', true)" prevents the RLS admin
override from taking effect; update the query in the function calling
client.query (the SELECT in backend/src/controllers/jobs.ts) to remove that
WHERE filter so RLS policies (including the ADMIN override) can control
visibility (i.e., use a plain "SELECT * FROM jobs ORDER BY created_at DESC" or
an equivalent that does not restrict by user_id in the SQL).

res.json(rows);
} catch (err: any) {
res.status(500).json({ error: "Failed to fetch jobs" });
}
};

export const getJobById = async (req: Request, res: Response) => {
const client = req.dbClient || pool;
try {
const job = await jobRegistry.getJob(req.params.id);
if (!job) {
const { rows } = await client.query("SELECT * FROM jobs WHERE id = $1", [req.params.id]);
if (rows.length === 0) {
return res.status(404).json({ error: "Background task not found." });
}
if (job.userId !== req.user!.id) {
return res.status(403).json({ error: "Access denied. Multi-tenant boundary constraint rule." });
}
res.json(job);
res.json(rows[0]);
} catch (err: any) {
res.status(500).json({ error: "Failed to fetch job details" });
}
Expand Down
24 changes: 10 additions & 14 deletions backend/src/controllers/libraryItems.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,11 @@ import { withTransaction } from "../utils/dbUtils.js";
import crypto from "crypto";

export const getLibraryItems = async (req: Request, res: Response) => {
const userId = req.user!.id;
const userRole = req.user!.role;
const client = req.dbClient || pool;
try {
const rows = await withTransaction(userId, userRole, async (client) => {
const { rows } = await client.query(
"SELECT * FROM library_items WHERE user_id = $1 ORDER BY created_at DESC",
[userId]
);
return rows;
});
const { rows } = await client.query(
"SELECT * FROM library_items WHERE user_id = current_setting('app.current_user_id', true) ORDER BY created_at DESC"
);
Comment on lines +9 to +11

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

This owner filter disables the admin override for library items.

library_items is part of the same RLS policy set that grants ADMIN broader visibility. Adding user_id = current_setting('app.current_user_id', true) here narrows admin sessions back to their own rows.

Suggested fix
-    const { rows } = await client.query(
-      "SELECT * FROM library_items WHERE user_id = current_setting('app.current_user_id', true) ORDER BY created_at DESC"
-    );
+    const { rows } = await client.query(
+      "SELECT * FROM library_items ORDER BY created_at DESC"
+    );
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const { rows } = await client.query(
"SELECT * FROM library_items WHERE user_id = current_setting('app.current_user_id', true) ORDER BY created_at DESC"
);
const { rows } = await client.query(
"SELECT * FROM library_items ORDER BY created_at DESC"
);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/src/controllers/libraryItems.ts` around lines 9 - 11, The query in
libraryItems controller explicitly restricts rows with "user_id =
current_setting('app.current_user_id', true)", which prevents ADMIN sessions
from seeing all rows and overrides RLS; remove that explicit owner filter from
the client.query call (leave the SELECT * FROM library_items ORDER BY created_at
DESC) so Postgres RLS and the ADMIN role policy can enforce visibility, or if
you must conditionally restrict for non-admins, apply the user_id filter only
when the current session role is not ADMIN (check
current_setting('app.current_user_role', true) or the equivalent session flag)
before composing the client.query.

res.json(rows);
} catch (err: any) {
res.status(500).json({ error: err.message });
Expand All @@ -24,9 +19,10 @@ export const createLibraryItem = async (req: Request, res: Response) => {
const { type, name, description, tags, details } = req.body;
const userId = req.user!.id;
const id = "lib_" + crypto.randomUUID();
const client = req.dbClient || pool;

try {
const { rows } = await pool.query(
const { rows } = await client.query(
"INSERT INTO library_items (id, user_id, type, name, description, tags, details) VALUES ($1, $2, $3, $4, $5, $6, $7) RETURNING *",
[id, userId, type, name, description, tags, details]
);
Expand All @@ -37,11 +33,11 @@ export const createLibraryItem = async (req: Request, res: Response) => {
};

export const deleteLibraryItem = async (req: Request, res: Response) => {
const userId = req.user!.id;
const client = req.dbClient || pool;
try {
const result = await pool.query(
"DELETE FROM library_items WHERE id = $1 AND user_id = $2",
[req.params.id, userId]
const result = await client.query(
"DELETE FROM library_items WHERE id = $1",
[req.params.id]
);
if (result.rowCount === 0) return res.status(404).json({ error: "Item not found." });
res.json({ success: true });
Expand Down
Loading