From 87e55d604650ba0cbc58fb2da0cbd058591d7cb5 Mon Sep 17 00:00:00 2001 From: willchen96 Date: Sat, 16 May 2026 00:05:16 +0800 Subject: [PATCH] feat: prevent users from sharing projects and reviews with themselves --- backend/src/routes/projects.ts | 27 ++++++++++++++++++- backend/src/routes/tabular.ts | 6 +++++ .../components/projects/NewProjectModal.tsx | 12 ++++++++- .../src/app/components/shared/PeopleModal.tsx | 19 +++++++++++-- 4 files changed, 60 insertions(+), 4 deletions(-) diff --git a/backend/src/routes/projects.ts b/backend/src/routes/projects.ts index 58de3c0..38e38b2 100644 --- a/backend/src/routes/projects.ts +++ b/backend/src/routes/projects.ts @@ -83,6 +83,7 @@ projectsRouter.get("/", requireAuth, async (req, res) => { // POST /projects projectsRouter.post("/", requireAuth, async (req, res) => { const userId = res.locals.userId as string; + const userEmail = res.locals.userEmail as string | undefined; const { name, cm_number, shared_with } = req.body as { name: string; cm_number?: string; @@ -90,6 +91,23 @@ projectsRouter.post("/", requireAuth, async (req, res) => { }; if (!name?.trim()) return void res.status(400).json({ detail: "name is required" }); + const normalizedUserEmail = userEmail?.trim().toLowerCase(); + const cleanedSharedWith: string[] = []; + const seenSharedEmails = new Set(); + if (Array.isArray(shared_with)) { + for (const raw of shared_with) { + if (typeof raw !== "string") continue; + const e = raw.trim().toLowerCase(); + if (!e || seenSharedEmails.has(e)) continue; + if (normalizedUserEmail && e === normalizedUserEmail) { + return void res + .status(400) + .json({ detail: "You cannot share a project with yourself." }); + } + seenSharedEmails.add(e); + cleanedSharedWith.push(e); + } + } const db = createServerSupabase(); const { data, error } = await db @@ -98,7 +116,7 @@ projectsRouter.post("/", requireAuth, async (req, res) => { user_id: userId, name: name.trim(), cm_number: cm_number ?? null, - shared_with: shared_with ?? [], + shared_with: cleanedSharedWith, }) .select("*") .single(); @@ -238,18 +256,25 @@ projectsRouter.get("/:projectId/people", requireAuth, async (req, res) => { // PATCH /projects/:projectId projectsRouter.patch("/:projectId", requireAuth, async (req, res) => { const userId = res.locals.userId as string; + const userEmail = res.locals.userEmail as string | undefined; const { projectId } = req.params; const updates: Record = {}; if (req.body.name != null) updates.name = req.body.name; if (req.body.cm_number != null) updates.cm_number = req.body.cm_number; if (Array.isArray(req.body.shared_with)) { // Normalise: lowercase + dedupe + drop empties. + const normalizedUserEmail = userEmail?.trim().toLowerCase(); const seen = new Set(); const cleaned: string[] = []; for (const raw of req.body.shared_with) { if (typeof raw !== "string") continue; const e = raw.trim().toLowerCase(); if (!e || seen.has(e)) continue; + if (normalizedUserEmail && e === normalizedUserEmail) { + return void res + .status(400) + .json({ detail: "You cannot share a project with yourself." }); + } seen.add(e); cleaned.push(e); } diff --git a/backend/src/routes/tabular.ts b/backend/src/routes/tabular.ts index b7efff6..e73454d 100644 --- a/backend/src/routes/tabular.ts +++ b/backend/src/routes/tabular.ts @@ -463,12 +463,18 @@ tabularRouter.patch("/:reviewId", requireAuth, async (req, res) => { // making the call. Normalize lowercase + dedupe + drop empties. let sharedWithUpdate: string[] | undefined; if (Array.isArray(req.body.shared_with)) { + const normalizedUserEmail = userEmail?.trim().toLowerCase(); const seen = new Set(); const cleaned: string[] = []; for (const raw of req.body.shared_with) { if (typeof raw !== "string") continue; const e = raw.trim().toLowerCase(); if (!e || seen.has(e)) continue; + if (normalizedUserEmail && e === normalizedUserEmail) { + return void res.status(400).json({ + detail: "You cannot share a tabular review with yourself.", + }); + } seen.add(e); cleaned.push(e); } diff --git a/frontend/src/app/components/projects/NewProjectModal.tsx b/frontend/src/app/components/projects/NewProjectModal.tsx index 7cc5c80..1c3b39d 100644 --- a/frontend/src/app/components/projects/NewProjectModal.tsx +++ b/frontend/src/app/components/projects/NewProjectModal.tsx @@ -11,6 +11,7 @@ import { useDirectoryData } from "../shared/useDirectoryData"; import { FileDirectory } from "../shared/FileDirectory"; import { EmailPillInput } from "../shared/EmailPillInput"; import type { MikeProject } from "../shared/types"; +import { useAuth } from "@/contexts/AuthContext"; interface Props { open: boolean; @@ -28,6 +29,8 @@ export function NewProjectModal({ open, onClose, onCreated }: Props) { const [loading, setLoading] = useState(false); const [error, setError] = useState(""); const fileInputRef = useRef(null); + const { user } = useAuth(); + const ownEmail = user?.email?.trim().toLowerCase() ?? null; const { loading: dirLoading, standaloneDocuments, projects: dirProjects } = useDirectoryData(open); @@ -49,7 +52,9 @@ export function NewProjectModal({ open, onClose, onCreated }: Props) { const project = await createProject( name.trim(), cmNumber.trim() || undefined, - sharedEmails, + ownEmail + ? sharedEmails.filter((email) => email !== ownEmail) + : sharedEmails, ); await Promise.all([ ...[...selectedDocIds].map((id) => addDocumentToProject(project.id, id).catch(() => {})), @@ -137,6 +142,11 @@ export function NewProjectModal({ open, onClose, onCreated }: Props) { + ownEmail && email === ownEmail + ? "You cannot share a project with yourself." + : null + } placeholder="Add colleagues by email…" /> diff --git a/frontend/src/app/components/shared/PeopleModal.tsx b/frontend/src/app/components/shared/PeopleModal.tsx index ce02d1c..8a70d39 100644 --- a/frontend/src/app/components/shared/PeopleModal.tsx +++ b/frontend/src/app/components/shared/PeopleModal.tsx @@ -136,13 +136,22 @@ export function PeopleModal({ } const trimmedNewEmail = newEmail.trim().toLowerCase(); + const normalizedCurrentUserEmail = + currentUserEmail?.trim().toLowerCase() ?? null; const isValidEmail = EMAIL_RE.test(trimmedNewEmail); const sharedLower = sharedWith.map((e) => e.toLowerCase()); const alreadyShared = sharedLower.includes(trimmedNewEmail); const isOwnerEmail = !!ownerEmail && trimmedNewEmail === ownerEmail.toLowerCase(); + const isSelfEmail = + !!normalizedCurrentUserEmail && + trimmedNewEmail === normalizedCurrentUserEmail; const canAdd = - isValidEmail && !alreadyShared && !isOwnerEmail && busy === null; + isValidEmail && + !alreadyShared && + !isOwnerEmail && + !isSelfEmail && + busy === null; async function handleAdd() { if (!canAdd || !onSharedWithChange) return; @@ -249,10 +258,16 @@ export function PeopleModal({ {trimmedNewEmail} is the owner.

)} + {isSelfEmail && !isOwnerEmail && trimmedNewEmail && ( +

+ You cannot share this with yourself. +

+ )} {trimmedNewEmail && !isValidEmail && !alreadyShared && - !isOwnerEmail && ( + !isOwnerEmail && + !isSelfEmail && (

Enter a valid email.