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.