aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--apps/web/components/dashboard/lists/ManageCollaboratorsModal.tsx16
-rw-r--r--packages/trpc/models/lists.ts6
-rw-r--r--packages/trpc/routers/lists.ts4
-rw-r--r--packages/trpc/routers/sharedLists.test.ts93
4 files changed, 109 insertions, 10 deletions
diff --git a/apps/web/components/dashboard/lists/ManageCollaboratorsModal.tsx b/apps/web/components/dashboard/lists/ManageCollaboratorsModal.tsx
index 232a944b..0a55c5fe 100644
--- a/apps/web/components/dashboard/lists/ManageCollaboratorsModal.tsx
+++ b/apps/web/components/dashboard/lists/ManageCollaboratorsModal.tsx
@@ -260,9 +260,11 @@ export function ManageCollaboratorsModal({
<div className="font-medium">
{collaboratorsData.owner.name}
</div>
- <div className="text-sm text-muted-foreground">
- {collaboratorsData.owner.email}
- </div>
+ {collaboratorsData.owner.email && (
+ <div className="text-sm text-muted-foreground">
+ {collaboratorsData.owner.email}
+ </div>
+ )}
</div>
<div className="text-sm capitalize text-muted-foreground">
{t("lists.collaborators.owner")}
@@ -292,9 +294,11 @@ export function ManageCollaboratorsModal({
</Badge>
)}
</div>
- <div className="text-sm text-muted-foreground">
- {collaborator.user.email}
- </div>
+ {collaborator.user.email && (
+ <div className="text-sm text-muted-foreground">
+ {collaborator.user.email}
+ </div>
+ )}
</div>
{readOnly ? (
<div className="text-sm capitalize text-muted-foreground">
diff --git a/packages/trpc/models/lists.ts b/packages/trpc/models/lists.ts
index a0d9ca23..0968492a 100644
--- a/packages/trpc/models/lists.ts
+++ b/packages/trpc/models/lists.ts
@@ -752,7 +752,8 @@ export abstract class List {
user: {
id: c.user.id,
name: c.user.name,
- email: c.user.email,
+ // Only show email to the owner for privacy
+ email: isOwner ? c.user.email : null,
},
};
});
@@ -763,7 +764,8 @@ export abstract class List {
? {
id: owner.id,
name: owner.name,
- email: owner.email,
+ // Only show owner email to the owner for privacy
+ email: isOwner ? owner.email : null,
}
: null,
};
diff --git a/packages/trpc/routers/lists.ts b/packages/trpc/routers/lists.ts
index 5eb0baff..296679f3 100644
--- a/packages/trpc/routers/lists.ts
+++ b/packages/trpc/routers/lists.ts
@@ -301,7 +301,7 @@ export const listsAppRouter = router({
user: z.object({
id: z.string(),
name: z.string(),
- email: z.string(),
+ email: z.string().nullable(),
}),
}),
),
@@ -309,7 +309,7 @@ export const listsAppRouter = router({
.object({
id: z.string(),
name: z.string(),
- email: z.string(),
+ email: z.string().nullable(),
})
.nullable(),
}),
diff --git a/packages/trpc/routers/sharedLists.test.ts b/packages/trpc/routers/sharedLists.test.ts
index 58a24d46..3440fae4 100644
--- a/packages/trpc/routers/sharedLists.test.ts
+++ b/packages/trpc/routers/sharedLists.test.ts
@@ -2831,5 +2831,98 @@ describe("Shared Lists", () => {
// Email should still be visible to owner
expect(declinedInvitation?.user.email).toBe(collaboratorEmail);
});
+
+ test<CustomTestContext>("should hide emails from non-owners", async ({
+ apiCallers,
+ }) => {
+ const ownerApi = apiCallers[0];
+ const collaborator1Api = apiCallers[1];
+ const collaborator2Api = apiCallers[2];
+
+ const list = await ownerApi.lists.create({
+ name: "Test List",
+ icon: "📚",
+ type: "manual",
+ });
+
+ const ownerUser = await ownerApi.users.whoami();
+ const ownerEmail = ownerUser.email!;
+
+ const collaborator1User = await collaborator1Api.users.whoami();
+ const collaborator1Email = collaborator1User.email!;
+
+ const collaborator2User = await collaborator2Api.users.whoami();
+ const collaborator2Email = collaborator2User.email!;
+
+ // Add both collaborators
+ await addAndAcceptCollaborator(
+ ownerApi,
+ collaborator1Api,
+ list.id,
+ "editor",
+ );
+ await addAndAcceptCollaborator(
+ ownerApi,
+ collaborator2Api,
+ list.id,
+ "viewer",
+ );
+
+ // Owner should see all emails
+ const ownerView = await ownerApi.lists.getCollaborators({
+ listId: list.id,
+ });
+
+ expect(ownerView.owner?.email).toBe(ownerEmail);
+
+ const ownerViewCollaborators = ownerView.collaborators.filter(
+ (c) => c.status === "accepted",
+ );
+ expect(ownerViewCollaborators).toHaveLength(2);
+
+ const ownerViewCollab1 = ownerViewCollaborators.find(
+ (c) => c.user.email === collaborator1Email,
+ );
+ const ownerViewCollab2 = ownerViewCollaborators.find(
+ (c) => c.user.email === collaborator2Email,
+ );
+
+ expect(ownerViewCollab1?.user.email).toBe(collaborator1Email);
+ expect(ownerViewCollab2?.user.email).toBe(collaborator2Email);
+
+ // Non-owners should NOT see any emails
+ const collaborator1View = await collaborator1Api.lists.getCollaborators({
+ listId: list.id,
+ });
+
+ // Should not see owner email
+ expect(collaborator1View.owner?.email).toBe(null);
+
+ // Should not see other collaborators' emails
+ const collab1ViewCollaborators = collaborator1View.collaborators.filter(
+ (c) => c.status === "accepted",
+ );
+ expect(collab1ViewCollaborators).toHaveLength(2);
+
+ collab1ViewCollaborators.forEach((c) => {
+ expect(c.user.email).toBe(null);
+ });
+
+ // Verify collaborator2 also can't see emails
+ const collaborator2View = await collaborator2Api.lists.getCollaborators({
+ listId: list.id,
+ });
+
+ expect(collaborator2View.owner?.email).toBe(null);
+
+ const collab2ViewCollaborators = collaborator2View.collaborators.filter(
+ (c) => c.status === "accepted",
+ );
+ expect(collab2ViewCollaborators).toHaveLength(2);
+
+ collab2ViewCollaborators.forEach((c) => {
+ expect(c.user.email).toBe(null);
+ });
+ });
});
});