diff --git a/apps/dokploy/components/dashboard/settings/users/show-users.tsx b/apps/dokploy/components/dashboard/settings/users/show-users.tsx
index 51d8704a3..e2eab13d1 100644
--- a/apps/dokploy/components/dashboard/settings/users/show-users.tsx
+++ b/apps/dokploy/components/dashboard/settings/users/show-users.tsx
@@ -35,7 +35,10 @@ export const ShowUsers = () => {
const { data: isCloud } = api.settings.isCloud.useQuery();
const { data, isLoading, refetch } = api.user.all.useQuery();
const { mutateAsync } = api.user.remove.useMutation();
+ const { mutateAsync: removeMember } =
+ api.organization.removeMember.useMutation();
const utils = api.useUtils();
+ const { data: session } = authClient.useSession();
return (
@@ -134,55 +137,14 @@ export const ShowUsers = () => {
{member.role !== "owner" && (
<>
- {!isCloud && (
- {
- await mutateAsync({
- userId: member.user.id,
- })
- .then(() => {
- toast.success(
- "User deleted successfully",
- );
- refetch();
- })
- .catch(() => {
- toast.error(
- "Error deleting destination",
- );
- });
- }}
- >
-
- e.preventDefault()
- }
- >
- Delete User
-
-
- )}
-
- {
- if (!isCloud) {
- const orgCount =
- await utils.user.checkUserOrganizations.fetch(
- {
- userId: member.user.id,
- },
- );
-
- console.log(orgCount);
-
- if (orgCount === 1) {
+ {!isCloud &&
+ member.user.id !==
+ session?.user?.id && (
+ {
await mutateAsync({
userId: member.user.id,
})
@@ -192,41 +154,78 @@ export const ShowUsers = () => {
);
refetch();
})
- .catch(() => {
+ .catch((err) => {
toast.error(
- "Error deleting user",
+ err?.message ||
+ "Error deleting user",
);
});
- return;
+ }}
+ >
+
+ e.preventDefault()
+ }
+ >
+ Delete User
+
+
+ )}
+
+ {!(
+ member.role === "admin" &&
+ member.user.id === session?.user?.id
+ ) && (
+ {
+ try {
+ if (!isCloud) {
+ const orgCount =
+ await utils.user.checkUserOrganizations.fetch(
+ {
+ userId: member.user.id,
+ },
+ );
+ if (orgCount === 1) {
+ await mutateAsync({
+ userId: member.user.id,
+ });
+ toast.success(
+ "User deleted successfully",
+ );
+ refetch();
+ return;
+ }
+ }
+ await removeMember({
+ memberId: member.id,
+ });
+ toast.success(
+ "User unlinked successfully",
+ );
+ refetch();
+ } catch (error: any) {
+ toast.error(
+ error?.message ||
+ "Error unlinking user",
+ );
}
- }
-
- const { error } =
- await authClient.organization.removeMember(
- {
- memberIdOrEmail: member.id,
- },
- );
-
- if (!error) {
- toast.success(
- "User unlinked successfully",
- );
- refetch();
- } else {
- toast.error(
- "Error unlinking user",
- );
- }
- }}
- >
- e.preventDefault()}
+ }}
>
- Unlink User
-
-
+
+ e.preventDefault()
+ }
+ >
+ Unlink User
+
+
+ )}
>
)}
diff --git a/apps/dokploy/server/api/routers/organization.ts b/apps/dokploy/server/api/routers/organization.ts
index 0f3d1c82e..dc815d150 100644
--- a/apps/dokploy/server/api/routers/organization.ts
+++ b/apps/dokploy/server/api/routers/organization.ts
@@ -184,4 +184,44 @@ export const organizationRouter = createTRPCRouter({
.delete(invitation)
.where(eq(invitation.id, input.invitationId));
}),
+ removeMember: adminProcedure
+ .input(z.object({ memberId: z.string() }))
+ .mutation(async ({ ctx, input }) => {
+ // Fetch the target member within the active organization
+ const target = await db.query.member.findFirst({
+ where: eq(member.id, input.memberId),
+ with: { user: true },
+ });
+
+ if (!target) {
+ throw new TRPCError({ code: "NOT_FOUND", message: "Member not found" });
+ }
+
+ if (target.organizationId !== ctx.session.activeOrganizationId) {
+ throw new TRPCError({
+ code: "FORBIDDEN",
+ message: "You are not allowed to remove this member",
+ });
+ }
+
+ // Disallow removing the organization owner
+ if (target.role === "owner") {
+ throw new TRPCError({
+ code: "FORBIDDEN",
+ message: "You cannot unlink the organization owner",
+ });
+ }
+
+ // Admin self-protection: an admin cannot unlink themselves
+ if (target.role === "admin" && target.userId === ctx.user.id) {
+ throw new TRPCError({
+ code: "FORBIDDEN",
+ message:
+ "Admins cannot unlink themselves. Ask the owner or another admin.",
+ });
+ }
+
+ await db.delete(member).where(eq(member.id, input.memberId));
+ return true;
+ }),
});
diff --git a/apps/dokploy/server/api/routers/user.ts b/apps/dokploy/server/api/routers/user.ts
index fb113f566..9072f56c7 100644
--- a/apps/dokploy/server/api/routers/user.ts
+++ b/apps/dokploy/server/api/routers/user.ts
@@ -217,10 +217,51 @@ export const userRouter = createTRPCRouter({
userId: z.string(),
}),
)
- .mutation(async ({ input }) => {
+ .mutation(async ({ input, ctx }) => {
if (IS_CLOUD) {
return true;
}
+
+ // Ensure the acting user has admin privileges in the active organization
+ if (ctx.user.role !== "owner" && ctx.user.role !== "admin") {
+ throw new TRPCError({
+ code: "FORBIDDEN",
+ message: "Only owners or admins can delete users",
+ });
+ }
+
+ // Fetch target member within the active organization
+ const targetMember = await db.query.member.findFirst({
+ where: and(
+ eq(member.userId, input.userId),
+ eq(member.organizationId, ctx.session?.activeOrganizationId || ""),
+ ),
+ });
+
+ if (!targetMember) {
+ throw new TRPCError({
+ code: "NOT_FOUND",
+ message: "Target user is not a member of this organization",
+ });
+ }
+
+ // Never allow deleting the organization owner via this endpoint
+ if (targetMember.role === "owner") {
+ throw new TRPCError({
+ code: "FORBIDDEN",
+ message: "You cannot delete the organization owner",
+ });
+ }
+
+ // Admin self-protection: an admin cannot delete themselves
+ if (targetMember.role === "admin" && input.userId === ctx.user.id) {
+ throw new TRPCError({
+ code: "FORBIDDEN",
+ message:
+ "Admins cannot delete themselves. Ask the owner or another admin.",
+ });
+ }
+
return await removeUserById(input.userId);
}),
assignPermissions: adminProcedure