mirror of
https://github.com/Dokploy/dokploy.git
synced 2026-07-18 20:35:24 +02:00
feat(roles): implement role management functionality
- Added a new component for managing user roles, allowing assignment and creation of roles. - Introduced a new API router for role management, including endpoints for creating, updating, and deleting roles. - Updated the database schema to support role management with a new "organization_role" table. - Enhanced user management to include role assignments and permissions handling. - Updated UI components to integrate the new role management features.
This commit is contained in:
26
packages/server/src/db/migrations/create-default-roles.ts
Normal file
26
packages/server/src/db/migrations/create-default-roles.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import { sql } from "drizzle-orm";
|
||||
import { db } from "..";
|
||||
import { organization } from "../schema/account";
|
||||
import { getDefaultRolesSQL } from "../schema/rbac";
|
||||
|
||||
export async function createDefaultRoles() {
|
||||
try {
|
||||
// Get all organizations
|
||||
const organizations = await db.select().from(organization);
|
||||
|
||||
// Create default roles for each organization
|
||||
for (const org of organizations) {
|
||||
const rolesSQL = getDefaultRolesSQL(org.id);
|
||||
await db.execute(sql.raw(rolesSQL));
|
||||
|
||||
console.log(
|
||||
`Created default roles for organization: ${org.name} (${org.id})`,
|
||||
);
|
||||
}
|
||||
|
||||
console.log("Successfully created default roles for all organizations");
|
||||
} catch (error) {
|
||||
console.error("Error creating default roles:", error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
@@ -10,6 +10,7 @@ import { nanoid } from "nanoid";
|
||||
import { projects } from "./project";
|
||||
import { server } from "./server";
|
||||
import { users_temp } from "./user";
|
||||
import { role } from "./rbac";
|
||||
|
||||
export const account = pgTable("account", {
|
||||
id: text("id")
|
||||
@@ -92,6 +93,9 @@ export const member = pgTable("member", {
|
||||
.notNull()
|
||||
.references(() => users_temp.id, { onDelete: "cascade" }),
|
||||
role: text("role").notNull().$type<"owner" | "member" | "admin">(),
|
||||
roleId: text("roleId")
|
||||
.notNull()
|
||||
.references(() => role.roleId, { onDelete: "cascade" }), // Referencia a la nueva tabla de roles
|
||||
createdAt: timestamp("created_at").notNull(),
|
||||
teamId: text("team_id"),
|
||||
// Permissions
|
||||
@@ -127,6 +131,10 @@ export const memberRelations = relations(member, ({ one }) => ({
|
||||
fields: [member.userId],
|
||||
references: [users_temp.id],
|
||||
}),
|
||||
role: one(role, {
|
||||
fields: [member.roleId],
|
||||
references: [role.roleId],
|
||||
}),
|
||||
}));
|
||||
|
||||
export const invitation = pgTable("invitation", {
|
||||
|
||||
@@ -30,6 +30,7 @@ export * from "./server";
|
||||
export * from "./utils";
|
||||
export * from "./preview-deployments";
|
||||
export * from "./ai";
|
||||
export * from "./rbac";
|
||||
export * from "./account";
|
||||
export * from "./schedule";
|
||||
export * from "./rollbacks";
|
||||
|
||||
147
packages/server/src/db/schema/rbac.ts
Normal file
147
packages/server/src/db/schema/rbac.ts
Normal file
@@ -0,0 +1,147 @@
|
||||
import { relations } from "drizzle-orm";
|
||||
import { pgTable, text, timestamp, boolean } from "drizzle-orm/pg-core";
|
||||
import { nanoid } from "nanoid";
|
||||
import { organization, member } from "./account";
|
||||
import { createInsertSchema } from "drizzle-zod";
|
||||
import { z } from "zod";
|
||||
|
||||
export const PERMISSIONS = {
|
||||
PROJECT: {
|
||||
VIEW: {
|
||||
name: "project:view",
|
||||
description: "View projects",
|
||||
},
|
||||
CREATE: {
|
||||
name: "project:create",
|
||||
description: "Create projects",
|
||||
},
|
||||
DELETE: {
|
||||
name: "project:delete",
|
||||
description: "Delete projects",
|
||||
},
|
||||
},
|
||||
SERVICE: {
|
||||
VIEW: {
|
||||
name: "service:view",
|
||||
description: "View services",
|
||||
},
|
||||
CREATE: {
|
||||
name: "service:create",
|
||||
description: "Create services",
|
||||
},
|
||||
DELETE: {
|
||||
name: "service:delete",
|
||||
description: "Delete services",
|
||||
},
|
||||
},
|
||||
TRAEFIK: {
|
||||
ACCESS: {
|
||||
name: "traefik_files:access",
|
||||
description: "Access traefik files",
|
||||
},
|
||||
},
|
||||
DOCKER: {
|
||||
VIEW: {
|
||||
name: "docker:view",
|
||||
description: "View docker",
|
||||
},
|
||||
},
|
||||
API: {
|
||||
ACCESS: {
|
||||
name: "api:access",
|
||||
description: "Access API",
|
||||
},
|
||||
},
|
||||
} as const;
|
||||
|
||||
const getAllPermissionNames = () => {
|
||||
return Object.values(PERMISSIONS).flatMap((category) =>
|
||||
Object.values(category).map((permission) => permission.name),
|
||||
);
|
||||
};
|
||||
|
||||
export const ownerPermissions = getAllPermissionNames();
|
||||
|
||||
export const adminPermissions = [
|
||||
PERMISSIONS.PROJECT.VIEW.name,
|
||||
PERMISSIONS.PROJECT.CREATE.name,
|
||||
PERMISSIONS.PROJECT.DELETE.name,
|
||||
PERMISSIONS.SERVICE.VIEW.name,
|
||||
PERMISSIONS.SERVICE.CREATE.name,
|
||||
PERMISSIONS.SERVICE.DELETE.name,
|
||||
PERMISSIONS.TRAEFIK.ACCESS.name,
|
||||
PERMISSIONS.DOCKER.VIEW.name,
|
||||
PERMISSIONS.API.ACCESS.name,
|
||||
];
|
||||
|
||||
export const memberPermissions = [
|
||||
PERMISSIONS.PROJECT.CREATE.name,
|
||||
PERMISSIONS.SERVICE.CREATE.name,
|
||||
PERMISSIONS.TRAEFIK.ACCESS.name,
|
||||
];
|
||||
|
||||
export const defaultPermissions = [
|
||||
{
|
||||
name: "owner",
|
||||
description: "Owner of the organization with full access to all features",
|
||||
permissions: ownerPermissions,
|
||||
},
|
||||
{
|
||||
name: "admin",
|
||||
description:
|
||||
"Administrator with access to manage projects, services and configurations",
|
||||
permissions: adminPermissions,
|
||||
},
|
||||
{
|
||||
name: "member",
|
||||
description:
|
||||
"Regular member with access to create projects and manage services",
|
||||
permissions: memberPermissions,
|
||||
},
|
||||
] as const;
|
||||
|
||||
export const role = pgTable("organization_role", {
|
||||
roleId: text("roleId")
|
||||
.primaryKey()
|
||||
.$defaultFn(() => nanoid()),
|
||||
name: text("name").notNull().unique(),
|
||||
description: text("description"),
|
||||
canDelete: boolean("canDelete").notNull().default(true),
|
||||
isSystem: boolean("is_system").default(false),
|
||||
permissions: text("permissions").array(),
|
||||
createdAt: timestamp("created_at").notNull().defaultNow(),
|
||||
updatedAt: timestamp("updated_at").notNull().defaultNow(),
|
||||
organizationId: text("organizationId")
|
||||
.notNull()
|
||||
.references(() => organization.id, { onDelete: "cascade" }),
|
||||
});
|
||||
|
||||
export const roleRelations = relations(role, ({ one, many }) => ({
|
||||
organization: one(organization, {
|
||||
fields: [role.organizationId],
|
||||
references: [organization.id],
|
||||
}),
|
||||
members: many(member),
|
||||
}));
|
||||
|
||||
export type Role = typeof role.$inferSelect;
|
||||
|
||||
export const createRoleSchema = createInsertSchema(role)
|
||||
.omit({
|
||||
roleId: true,
|
||||
createdAt: true,
|
||||
updatedAt: true,
|
||||
isSystem: true,
|
||||
organizationId: true,
|
||||
})
|
||||
.extend({
|
||||
permissions: z.array(z.string()),
|
||||
});
|
||||
|
||||
export const updateRoleSchema = createRoleSchema.extend({
|
||||
roleId: z.string().min(1),
|
||||
});
|
||||
|
||||
export const apiFindOneRole = z.object({
|
||||
roleId: z.string().min(1),
|
||||
});
|
||||
13
packages/server/src/db/scripts/create-default-roles.ts
Normal file
13
packages/server/src/db/scripts/create-default-roles.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import { createDefaultRoles } from "../migrations/create-default-roles";
|
||||
|
||||
async function main() {
|
||||
try {
|
||||
await createDefaultRoles();
|
||||
process.exit(0);
|
||||
} catch (error) {
|
||||
console.error("Failed to create default roles:", error);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
main();
|
||||
@@ -13,6 +13,7 @@ export * from "./services/settings";
|
||||
export * from "./services/volume-backups";
|
||||
export * from "./services/docker";
|
||||
export * from "./services/destination";
|
||||
export * from "./services/role";
|
||||
export * from "./services/deployment";
|
||||
export * from "./services/mount";
|
||||
export * from "./services/certificate";
|
||||
|
||||
71
packages/server/src/services/role.ts
Normal file
71
packages/server/src/services/role.ts
Normal file
@@ -0,0 +1,71 @@
|
||||
import { eq } from "drizzle-orm";
|
||||
import { db } from "../db";
|
||||
import {
|
||||
type createRoleSchema,
|
||||
role,
|
||||
type updateRoleSchema,
|
||||
} from "../db/schema";
|
||||
import type { z } from "zod";
|
||||
|
||||
export const createRole = async (
|
||||
input: z.infer<typeof createRoleSchema>,
|
||||
organizationId: string,
|
||||
) => {
|
||||
await db.transaction(async (tx) => {
|
||||
const { ...other } = input;
|
||||
const newRole = await tx
|
||||
.insert(role)
|
||||
.values({ ...other, organizationId })
|
||||
.returning()
|
||||
.then((res) => res[0]);
|
||||
|
||||
if (!newRole) {
|
||||
throw new Error("Failed to create role");
|
||||
}
|
||||
|
||||
return role;
|
||||
});
|
||||
};
|
||||
|
||||
const findRoleById = async (roleId: string) => {
|
||||
const result = await db.query.role.findFirst({
|
||||
where: eq(role.roleId, roleId),
|
||||
});
|
||||
|
||||
if (!result) {
|
||||
throw new Error("Role not found");
|
||||
}
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
export const removeRoleById = async (roleId: string) => {
|
||||
const currentRole = await findRoleById(roleId);
|
||||
|
||||
if (!currentRole) {
|
||||
throw new Error("Role not found");
|
||||
}
|
||||
|
||||
if (currentRole.isSystem) {
|
||||
throw new Error("Cannot delete system role");
|
||||
}
|
||||
|
||||
await db.delete(role).where(eq(role.roleId, roleId));
|
||||
|
||||
return currentRole;
|
||||
};
|
||||
|
||||
export const updateRoleById = async (
|
||||
roleId: string,
|
||||
input: z.infer<typeof updateRoleSchema>,
|
||||
) => {
|
||||
const currentRole = await findRoleById(roleId);
|
||||
|
||||
if (!currentRole) {
|
||||
throw new Error("Role not found");
|
||||
}
|
||||
|
||||
await db.update(role).set(input).where(eq(role.roleId, roleId));
|
||||
|
||||
return currentRole;
|
||||
};
|
||||
Reference in New Issue
Block a user