Hide member emails from non-admins (#327)
* feat(workspace): sanitize member emails for non-admin users in workspace router * feat(workspace): add email visibility setting and update member display logic * fix(workspace): update default value for showEmailsToMembers column to true * - Removed unnecessary type assertions - Added anonymous name handling (anonymous_[publicId]) in the API - Removed placeholder logic - Reverted null check - Avoided as unknown cast
This commit is contained in:
@@ -77,6 +77,49 @@ export const workspaceRouter = createTRPCRouter({
|
||||
|
||||
await assertUserInWorkspace(ctx.db, userId, result.id);
|
||||
|
||||
// Check if user is an admin
|
||||
const userMember = result.members.find(
|
||||
(member) => member.user?.id === userId,
|
||||
);
|
||||
const isAdmin = userMember?.role === "admin";
|
||||
|
||||
// Show emails if user is admin OR workspace setting allows it
|
||||
const shouldShowEmails = isAdmin || result.showEmailsToMembers === true;
|
||||
|
||||
// If emails should be hidden, filter them out
|
||||
if (!shouldShowEmails) {
|
||||
const sanitizedMembers = result.members.map((member) => {
|
||||
// If user doesn't have a display name, use anonymous identifier
|
||||
const displayName =
|
||||
member.user?.name?.trim() ?? `anonymous_${member.publicId}`;
|
||||
|
||||
const { email: _memberEmail, ...memberWithoutEmail } = member;
|
||||
const sanitizedUser = member.user
|
||||
? (() => {
|
||||
const { email: _userEmail, ...userWithoutEmail } = member.user;
|
||||
return {
|
||||
...userWithoutEmail,
|
||||
name: displayName,
|
||||
};
|
||||
})()
|
||||
: {
|
||||
id: null,
|
||||
name: displayName,
|
||||
image: null,
|
||||
};
|
||||
|
||||
return {
|
||||
...memberWithoutEmail,
|
||||
user: sanitizedUser,
|
||||
};
|
||||
});
|
||||
|
||||
return {
|
||||
...result,
|
||||
members: sanitizedMembers,
|
||||
} as Awaited<ReturnType<typeof workspaceRepo.getByPublicIdWithMembers>>;
|
||||
}
|
||||
|
||||
return result;
|
||||
}),
|
||||
bySlug: publicProcedure
|
||||
@@ -230,6 +273,7 @@ export const workspaceRouter = createTRPCRouter({
|
||||
.regex(/^(?![-]+$)[a-zA-Z0-9-]+$/)
|
||||
.optional(),
|
||||
description: z.string().min(3).max(280).optional(),
|
||||
showEmailsToMembers: z.boolean().optional(),
|
||||
}),
|
||||
)
|
||||
.output(z.custom<Awaited<ReturnType<typeof workspaceRepo.update>>>())
|
||||
@@ -291,9 +335,16 @@ export const workspaceRouter = createTRPCRouter({
|
||||
name: input.name,
|
||||
slug: input.slug,
|
||||
description: input.description,
|
||||
showEmailsToMembers: input.showEmailsToMembers,
|
||||
},
|
||||
);
|
||||
|
||||
if (!result)
|
||||
throw new TRPCError({
|
||||
message: `Unable to delete workspace`,
|
||||
code: "INTERNAL_SERVER_ERROR",
|
||||
});
|
||||
|
||||
return result;
|
||||
}),
|
||||
delete: protectedProcedure
|
||||
@@ -336,12 +387,6 @@ export const workspaceRouter = createTRPCRouter({
|
||||
input.workspacePublicId,
|
||||
);
|
||||
|
||||
if (!result)
|
||||
throw new TRPCError({
|
||||
message: `Unable to delete workspace`,
|
||||
code: "INTERNAL_SERVER_ERROR",
|
||||
});
|
||||
|
||||
return result;
|
||||
}),
|
||||
checkSlugAvailability: publicProcedure
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
ALTER TABLE "workspace" ADD COLUMN "showEmailsToMembers" boolean NOT NULL DEFAULT true;
|
||||
@@ -155,6 +155,13 @@
|
||||
"when": 1767045713686,
|
||||
"tag": "20251229220153_UpdateCardTitleFromVarcharToText",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 22,
|
||||
"version": "7",
|
||||
"when": 1768858977000,
|
||||
"tag": "20260119164257_AddShowEmailsToMembersToWorkspace",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -82,6 +82,7 @@ export const update = async (
|
||||
slug?: string;
|
||||
plan?: "free" | "pro" | "enterprise";
|
||||
description?: string;
|
||||
showEmailsToMembers?: boolean;
|
||||
},
|
||||
) => {
|
||||
const [result] = await db
|
||||
@@ -91,6 +92,7 @@ export const update = async (
|
||||
slug: workspaceInput.slug,
|
||||
plan: workspaceInput.plan,
|
||||
description: workspaceInput.description,
|
||||
showEmailsToMembers: workspaceInput.showEmailsToMembers,
|
||||
})
|
||||
.where(eq(workspaces.publicId, workspacePublicId))
|
||||
.returning({
|
||||
@@ -100,6 +102,7 @@ export const update = async (
|
||||
slug: workspaces.slug,
|
||||
description: workspaces.description,
|
||||
plan: workspaces.plan,
|
||||
showEmailsToMembers: workspaces.showEmailsToMembers,
|
||||
});
|
||||
|
||||
return result;
|
||||
@@ -139,6 +142,7 @@ export const getByPublicIdWithMembers = (
|
||||
columns: {
|
||||
id: true,
|
||||
publicId: true,
|
||||
showEmailsToMembers: true,
|
||||
},
|
||||
with: {
|
||||
members: {
|
||||
|
||||
@@ -43,6 +43,7 @@ export const workspaces = pgTable("workspace", {
|
||||
description: text("description"),
|
||||
slug: varchar("slug", { length: 255 }).notNull().unique(),
|
||||
plan: workspacePlanEnum("plan").notNull().default("free"),
|
||||
showEmailsToMembers: boolean("showEmailsToMembers").notNull().default(true),
|
||||
createdBy: uuid("createdBy").references(() => users.id, {
|
||||
onDelete: "set null",
|
||||
}),
|
||||
|
||||
Reference in New Issue
Block a user