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:
Mauricio Siu
2025-07-09 01:45:33 -06:00
parent d0b7ce3a50
commit d6e8653839
18 changed files with 13391 additions and 5 deletions

View 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;
}
}

View File

@@ -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", {

View File

@@ -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";

View 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),
});

View 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();

View File

@@ -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";

View 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;
};