refactor(auth): replace findAdmin with findOwner in user management logic and update role-based permissions in the dashboard

This commit is contained in:
Mauricio Siu
2025-12-07 02:51:03 -06:00
parent 075e387bb6
commit 6022f2f6a3
7 changed files with 97 additions and 89 deletions

View File

@@ -35,8 +35,7 @@ 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();
@@ -85,6 +84,10 @@ export const ShowUsers = () => {
</TableHeader>
<TableBody>
{data?.map((member) => {
const currentUserRole = data?.find(
(m) => m.user.id === session?.user?.id,
)?.role;
// Owner never has "Edit Permissions" (they're absolute owner)
// Other users can edit permissions if target is not themselves and target is a member
const canEditPermissions =
@@ -92,23 +95,34 @@ export const ShowUsers = () => {
member.role === "member" &&
member.user.id !== session?.user?.id;
// Can change role if target is not owner and not the current user
// Owner role is intransferible
// Can change role based on hierarchy:
// - Owner: Can change anyone's role (except themselves and other owners)
// - Admin: Can only change member roles (not other admins or owners)
// - Owner role is intransferible
const canChangeRole =
member.role !== "owner" &&
member.user.id !== session?.user?.id;
member.user.id !== session?.user?.id &&
(currentUserRole === "owner" ||
(currentUserRole === "admin" &&
member.role === "member"));
// Delete/Unlink follow same hierarchy as role changes
// - Owner: Can delete/unlink anyone (except themselves and owner can't be deleted)
// - Admin: Can only delete/unlink members (not other admins or owner)
const canDelete =
member.role !== "owner" &&
!isCloud &&
member.user.id !== session?.user?.id;
member.user.id !== session?.user?.id &&
(currentUserRole === "owner" ||
(currentUserRole === "admin" &&
member.role === "member"));
const canUnlink =
member.role !== "owner" &&
!(
member.role === "admin" &&
member.user.id === session?.user?.id
);
member.user.id !== session?.user?.id &&
(currentUserRole === "owner" ||
(currentUserRole === "admin" &&
member.role === "member"));
const hasAnyAction =
canEditPermissions ||
@@ -216,33 +230,48 @@ export const ShowUsers = () => {
description="Are you sure you want to unlink this user?"
type="destructive"
onClick={async () => {
try {
if (!isCloud) {
const orgCount =
await utils.user.checkUserOrganizations.fetch(
{
userId: member.user.id,
},
);
if (orgCount === 1) {
await mutateAsync({
if (!isCloud) {
const orgCount =
await utils.user.checkUserOrganizations.fetch(
{
userId: member.user.id,
},
);
if (orgCount === 1) {
await mutateAsync({
userId: member.user.id,
})
.then(() => {
toast.success(
"User deleted successfully",
);
refetch();
})
.catch(() => {
toast.error(
"Error deleting user",
);
});
toast.success(
"User deleted successfully",
);
refetch();
return;
}
return;
}
}
const { error } =
await authClient.organization.removeMember(
{
memberIdOrEmail: member.id,
},
);
if (!error) {
toast.success(
"User unlinked successfully",
);
refetch();
} catch (error: any) {
} else {
toast.error(
error?.message ||
"Error unlinking user",
"Error unlinking user",
);
}
}}

View File

@@ -1,11 +1,11 @@
import { findAdmin } from "@dokploy/server";
import { findOwner } from "@dokploy/server";
import { db } from "@dokploy/server/db";
import { user } from "@dokploy/server/db/schema";
import { eq } from "drizzle-orm";
(async () => {
try {
const result = await findAdmin();
const result = await findOwner();
const update = await db
.update(user)

View File

@@ -1,4 +1,4 @@
import { findAdmin, generateRandomPassword } from "@dokploy/server";
import { findOwner, generateRandomPassword } from "@dokploy/server";
import { db } from "@dokploy/server/db";
import { account } from "@dokploy/server/db/schema";
import { eq } from "drizzle-orm";
@@ -7,7 +7,7 @@ import { eq } from "drizzle-orm";
try {
const randomPassword = await generateRandomPassword();
const result = await findAdmin();
const result = await findOwner();
const update = await db
.update(account)

View File

@@ -194,46 +194,6 @@ 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;
}),
updateMemberRole: adminProcedure
.input(
z.object({
@@ -275,6 +235,16 @@ export const organizationRouter = createTRPCRouter({
});
}
// Only owners can change admin roles
// Admins can only change member roles
if (ctx.user.role === "admin" && target.role === "admin") {
throw new TRPCError({
code: "FORBIDDEN",
message:
"Only the organization owner can change admin roles. Admins can only modify member roles.",
});
}
// Update the target member's role
await db
.update(member)

View File

@@ -1,6 +1,5 @@
import {
createApiKey,
findAdmin,
findNotificationById,
findOrganizationById,
findUserById,
@@ -272,6 +271,16 @@ export const userRouter = createTRPCRouter({
});
}
// Only owners can delete admins
// Admins can only delete members
if (ctx.user.role === "admin" && targetMember.role === "admin") {
throw new TRPCError({
code: "FORBIDDEN",
message:
"Only the organization owner can delete admins. Admins can only delete members.",
});
}
return await removeUserById(input.userId);
}),
assignPermissions: adminProcedure