mirror of
https://github.com/andwati/dokploy.git
synced 2026-11-10 05:55:18 +00:00
refactor(auth): replace findAdmin with findOwner in user management logic and update role-based permissions in the dashboard
This commit is contained in:
@@ -35,8 +35,7 @@ export const ShowUsers = () => {
|
|||||||
const { data: isCloud } = api.settings.isCloud.useQuery();
|
const { data: isCloud } = api.settings.isCloud.useQuery();
|
||||||
const { data, isLoading, refetch } = api.user.all.useQuery();
|
const { data, isLoading, refetch } = api.user.all.useQuery();
|
||||||
const { mutateAsync } = api.user.remove.useMutation();
|
const { mutateAsync } = api.user.remove.useMutation();
|
||||||
const { mutateAsync: removeMember } =
|
|
||||||
api.organization.removeMember.useMutation();
|
|
||||||
const utils = api.useUtils();
|
const utils = api.useUtils();
|
||||||
const { data: session } = authClient.useSession();
|
const { data: session } = authClient.useSession();
|
||||||
|
|
||||||
@@ -85,6 +84,10 @@ export const ShowUsers = () => {
|
|||||||
</TableHeader>
|
</TableHeader>
|
||||||
<TableBody>
|
<TableBody>
|
||||||
{data?.map((member) => {
|
{data?.map((member) => {
|
||||||
|
const currentUserRole = data?.find(
|
||||||
|
(m) => m.user.id === session?.user?.id,
|
||||||
|
)?.role;
|
||||||
|
|
||||||
// Owner never has "Edit Permissions" (they're absolute owner)
|
// Owner never has "Edit Permissions" (they're absolute owner)
|
||||||
// Other users can edit permissions if target is not themselves and target is a member
|
// Other users can edit permissions if target is not themselves and target is a member
|
||||||
const canEditPermissions =
|
const canEditPermissions =
|
||||||
@@ -92,23 +95,34 @@ export const ShowUsers = () => {
|
|||||||
member.role === "member" &&
|
member.role === "member" &&
|
||||||
member.user.id !== session?.user?.id;
|
member.user.id !== session?.user?.id;
|
||||||
|
|
||||||
// Can change role if target is not owner and not the current user
|
// Can change role based on hierarchy:
|
||||||
// Owner role is intransferible
|
// - 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 =
|
const canChangeRole =
|
||||||
member.role !== "owner" &&
|
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 =
|
const canDelete =
|
||||||
member.role !== "owner" &&
|
member.role !== "owner" &&
|
||||||
!isCloud &&
|
!isCloud &&
|
||||||
member.user.id !== session?.user?.id;
|
member.user.id !== session?.user?.id &&
|
||||||
|
(currentUserRole === "owner" ||
|
||||||
|
(currentUserRole === "admin" &&
|
||||||
|
member.role === "member"));
|
||||||
|
|
||||||
const canUnlink =
|
const canUnlink =
|
||||||
member.role !== "owner" &&
|
member.role !== "owner" &&
|
||||||
!(
|
member.user.id !== session?.user?.id &&
|
||||||
member.role === "admin" &&
|
(currentUserRole === "owner" ||
|
||||||
member.user.id === session?.user?.id
|
(currentUserRole === "admin" &&
|
||||||
);
|
member.role === "member"));
|
||||||
|
|
||||||
const hasAnyAction =
|
const hasAnyAction =
|
||||||
canEditPermissions ||
|
canEditPermissions ||
|
||||||
@@ -216,33 +230,48 @@ export const ShowUsers = () => {
|
|||||||
description="Are you sure you want to unlink this user?"
|
description="Are you sure you want to unlink this user?"
|
||||||
type="destructive"
|
type="destructive"
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
try {
|
if (!isCloud) {
|
||||||
if (!isCloud) {
|
const orgCount =
|
||||||
const orgCount =
|
await utils.user.checkUserOrganizations.fetch(
|
||||||
await utils.user.checkUserOrganizations.fetch(
|
{
|
||||||
{
|
|
||||||
userId: member.user.id,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
if (orgCount === 1) {
|
|
||||||
await mutateAsync({
|
|
||||||
userId: member.user.id,
|
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(
|
return;
|
||||||
"User deleted successfully",
|
|
||||||
);
|
|
||||||
refetch();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const { error } =
|
||||||
|
await authClient.organization.removeMember(
|
||||||
|
{
|
||||||
|
memberIdOrEmail: member.id,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!error) {
|
||||||
toast.success(
|
toast.success(
|
||||||
"User unlinked successfully",
|
"User unlinked successfully",
|
||||||
);
|
);
|
||||||
refetch();
|
refetch();
|
||||||
} catch (error: any) {
|
} else {
|
||||||
toast.error(
|
toast.error(
|
||||||
error?.message ||
|
"Error unlinking user",
|
||||||
"Error unlinking user",
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
import { findAdmin } from "@dokploy/server";
|
import { findOwner } from "@dokploy/server";
|
||||||
import { db } from "@dokploy/server/db";
|
import { db } from "@dokploy/server/db";
|
||||||
import { user } from "@dokploy/server/db/schema";
|
import { user } from "@dokploy/server/db/schema";
|
||||||
import { eq } from "drizzle-orm";
|
import { eq } from "drizzle-orm";
|
||||||
|
|
||||||
(async () => {
|
(async () => {
|
||||||
try {
|
try {
|
||||||
const result = await findAdmin();
|
const result = await findOwner();
|
||||||
|
|
||||||
const update = await db
|
const update = await db
|
||||||
.update(user)
|
.update(user)
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { findAdmin, generateRandomPassword } from "@dokploy/server";
|
import { findOwner, generateRandomPassword } from "@dokploy/server";
|
||||||
import { db } from "@dokploy/server/db";
|
import { db } from "@dokploy/server/db";
|
||||||
import { account } from "@dokploy/server/db/schema";
|
import { account } from "@dokploy/server/db/schema";
|
||||||
import { eq } from "drizzle-orm";
|
import { eq } from "drizzle-orm";
|
||||||
@@ -7,7 +7,7 @@ import { eq } from "drizzle-orm";
|
|||||||
try {
|
try {
|
||||||
const randomPassword = await generateRandomPassword();
|
const randomPassword = await generateRandomPassword();
|
||||||
|
|
||||||
const result = await findAdmin();
|
const result = await findOwner();
|
||||||
|
|
||||||
const update = await db
|
const update = await db
|
||||||
.update(account)
|
.update(account)
|
||||||
|
|||||||
@@ -194,46 +194,6 @@ export const organizationRouter = createTRPCRouter({
|
|||||||
.delete(invitation)
|
.delete(invitation)
|
||||||
.where(eq(invitation.id, input.invitationId));
|
.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
|
updateMemberRole: adminProcedure
|
||||||
.input(
|
.input(
|
||||||
z.object({
|
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
|
// Update the target member's role
|
||||||
await db
|
await db
|
||||||
.update(member)
|
.update(member)
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import {
|
import {
|
||||||
createApiKey,
|
createApiKey,
|
||||||
findAdmin,
|
|
||||||
findNotificationById,
|
findNotificationById,
|
||||||
findOrganizationById,
|
findOrganizationById,
|
||||||
findUserById,
|
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);
|
return await removeUserById(input.userId);
|
||||||
}),
|
}),
|
||||||
assignPermissions: adminProcedure
|
assignPermissions: adminProcedure
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ export const isAdminPresent = async () => {
|
|||||||
return true;
|
return true;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const findAdmin = async () => {
|
export const findOwner = async () => {
|
||||||
const admin = await db.query.member.findFirst({
|
const admin = await db.query.member.findFirst({
|
||||||
where: eq(member.role, "owner"),
|
where: eq(member.role, "owner"),
|
||||||
with: {
|
with: {
|
||||||
@@ -107,11 +107,11 @@ export const getDokployUrl = async () => {
|
|||||||
if (IS_CLOUD) {
|
if (IS_CLOUD) {
|
||||||
return "https://app.dokploy.com";
|
return "https://app.dokploy.com";
|
||||||
}
|
}
|
||||||
const admin = await findAdmin();
|
const owner = await findOwner();
|
||||||
|
|
||||||
if (admin.user.host) {
|
if (owner.user.host) {
|
||||||
const protocol = admin.user.https ? "https" : "http";
|
const protocol = owner.user.https ? "https" : "http";
|
||||||
return `${protocol}://${admin.user.host}`;
|
return `${protocol}://${owner.user.host}`;
|
||||||
}
|
}
|
||||||
return `http://${admin.user.serverIp}:${process.env.PORT}`;
|
return `http://${owner.user.serverIp}:${process.env.PORT}`;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { paths } from "@dokploy/server/constants";
|
import { paths } from "@dokploy/server/constants";
|
||||||
import { findAdmin } from "@dokploy/server/services/admin";
|
import { findOwner } from "@dokploy/server/services/admin";
|
||||||
import { updateUser } from "@dokploy/server/services/user";
|
import { updateUser } from "@dokploy/server/services/user";
|
||||||
import { scheduledJobs, scheduleJob } from "node-schedule";
|
import { scheduledJobs, scheduleJob } from "node-schedule";
|
||||||
import { execAsync } from "../process/execAsync";
|
import { execAsync } from "../process/execAsync";
|
||||||
@@ -29,9 +29,9 @@ export const startLogCleanup = async (
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const admin = await findAdmin();
|
const owner = await findOwner();
|
||||||
if (admin) {
|
if (owner) {
|
||||||
await updateUser(admin.user.id, {
|
await updateUser(owner.user.id, {
|
||||||
logCleanupCron: cronExpression,
|
logCleanupCron: cronExpression,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -51,9 +51,9 @@ export const stopLogCleanup = async (): Promise<boolean> => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Update database
|
// Update database
|
||||||
const admin = await findAdmin();
|
const owner = await findOwner();
|
||||||
if (admin) {
|
if (owner) {
|
||||||
await updateUser(admin.user.id, {
|
await updateUser(owner.user.id, {
|
||||||
logCleanupCron: null,
|
logCleanupCron: null,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -69,8 +69,8 @@ export const getLogCleanupStatus = async (): Promise<{
|
|||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
cronExpression: string | null;
|
cronExpression: string | null;
|
||||||
}> => {
|
}> => {
|
||||||
const admin = await findAdmin();
|
const owner = await findOwner();
|
||||||
const cronExpression = admin?.user.logCleanupCron ?? null;
|
const cronExpression = owner?.user.logCleanupCron ?? null;
|
||||||
return {
|
return {
|
||||||
enabled: cronExpression !== null,
|
enabled: cronExpression !== null,
|
||||||
cronExpression,
|
cronExpression,
|
||||||
|
|||||||
Reference in New Issue
Block a user