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

@@ -157,6 +157,7 @@ export const AddInvitation = () => {
</SelectTrigger>
</FormControl>
<SelectContent>
<SelectItem value="admin">Admin</SelectItem>
<SelectItem value="member">Member</SelectItem>
</SelectContent>
</Select>

View File

@@ -0,0 +1,419 @@
import { Button } from "@/components/ui/button";
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
import { DropdownMenuItem } from "@/components/ui/dropdown-menu";
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
import { Separator } from "@/components/ui/separator";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { api } from "@/utils/api";
import { zodResolver } from "@hookform/resolvers/zod";
import { useEffect, useState } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
import { z } from "zod";
import { Badge } from "@/components/ui/badge";
import { Checkbox } from "@/components/ui/checkbox";
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import { AlertBlock } from "@/components/shared/alert-block";
const assignRoleSchema = z.object({
roleId: z.string(),
});
const createRoleSchema = z.object({
name: z.string().min(1, "Name is required"),
description: z.string().optional(),
permissions: z.array(z.string()).min(1, "Select at least one permission"),
});
type AssignRoleForm = z.infer<typeof assignRoleSchema>;
type CreateRoleForm = z.infer<typeof createRoleSchema>;
interface Props {
userId: string;
}
export const AddUserPermissionsV2 = ({ userId }: Props) => {
const utils = api.useUtils();
const { data: projects } = api.project.all.useQuery();
const [activeTab, setActiveTab] = useState<"assign" | "create">("assign");
const { data: roles, refetch: refetchRoles } = api.role.all.useQuery();
const { data: defaultRoles } = api.role.getDefaultRoles.useQuery();
const { data: userData, refetch: refetchUser } = api.user.one.useQuery(
{
userId,
},
{
enabled: !!userId,
},
);
const { mutateAsync: createRole, isLoading: isCreatingRole } =
api.role.create.useMutation();
const { mutateAsync: updateMemberRole, isLoading: isAssigningRole } =
api.user.assignRole.useMutation();
const assignForm = useForm<AssignRoleForm>({
resolver: zodResolver(assignRoleSchema),
});
const createForm = useForm<CreateRoleForm>({
resolver: zodResolver(createRoleSchema),
defaultValues: {
permissions: [],
},
});
useEffect(() => {
if (userData) {
assignForm.reset({
roleId: userData.roleId,
});
}
}, [userData, assignForm]);
const onAssignRole = async (data: AssignRoleForm) => {
try {
await updateMemberRole({
userId,
roleId: data.roleId,
});
toast.success("Role assigned successfully");
await refetchUser();
await refetchRoles();
} catch (error) {
const message =
error instanceof Error ? error.message : "Failed to assign role";
toast.error(message);
}
};
const onCreateRole = async (data: CreateRoleForm) => {
try {
await createRole({
...data,
permissions: data.permissions,
});
toast.success("Role created successfully");
refetchRoles();
setActiveTab("assign");
createForm.reset();
} catch (error) {
const message =
error instanceof Error ? error.message : "Failed to create role";
toast.error(message);
}
};
// Helper function to render a role item
const renderRoleItem = (
role: Role | undefined,
roleKey: "owner" | "admin" | "member",
) => {
if (!role) {
return (
<AlertBlock type="warning">
Default role '{roleKey}' not found. Please check your database setup.
</AlertBlock>
);
}
return (
<FormItem
key={role.roleId}
className="flex items-center space-x-3 space-y-0"
>
<FormControl>
<RadioGroupItem value={role.roleId} />
</FormControl>
<FormLabel className="font-normal">
<div className="flex items-center gap-2">
<span className="font-medium capitalize">{roleKey}</span>
{roleKey === "owner" && (
<Badge variant="default" className="text-xs">
Full Access
</Badge>
)}
</div>
<div className="text-xs text-muted-foreground">
{role.description}
</div>
<div className="flex flex-wrap gap-1 mt-1">
{role.permissions?.map((permission: string) => {
const permissionInfo = defaultRoles?.owner?.permissions?.find(
(p) => p === permission,
);
return (
<Badge
key={permission}
variant={roleKey === "owner" ? "default" : "secondary"}
className="text-xs"
>
{permissionInfo?.name || permission}
</Badge>
);
})}
</div>
</FormLabel>
</FormItem>
);
};
return (
<Dialog>
<DialogTrigger asChild>
<DropdownMenuItem
className="w-full cursor-pointer"
onSelect={(e) => e.preventDefault()}
>
Manage Roles
</DropdownMenuItem>
</DialogTrigger>
<DialogContent className="max-h-[85vh] overflow-y-auto sm:max-w-2xl">
<DialogHeader>
<DialogTitle>Role Management</DialogTitle>
<DialogDescription>
Assign existing roles or create new ones. The Owner role has full
access to all features.
</DialogDescription>
</DialogHeader>
<Tabs
value={activeTab}
onValueChange={(v) => setActiveTab(v as "assign" | "create")}
>
<TabsList className="grid w-full grid-cols-2">
<TabsTrigger value="assign">Assign Role</TabsTrigger>
<TabsTrigger value="create">Create Role</TabsTrigger>
</TabsList>
<TabsContent value="assign">
<Form {...assignForm}>
<form onSubmit={assignForm.handleSubmit(onAssignRole)}>
<div className="space-y-4 py-4">
<FormField
control={assignForm.control}
name="roleId"
render={({ field }) => (
<FormItem className="space-y-3">
<FormLabel>Select Role</FormLabel>
<FormControl>
<RadioGroup
onValueChange={field.onChange}
defaultValue={field.value}
className="space-y-4"
>
{/* Default Roles Section */}
<div className="space-y-4">
<h4 className="text-sm font-medium">
Default Roles
</h4>
{defaultRoles && (
<>
{renderRoleItem(defaultRoles.owner, "owner")}
{renderRoleItem(defaultRoles.admin, "admin")}
{renderRoleItem(
defaultRoles.member,
"member",
)}
</>
)}
</div>
<Separator />
{/* Custom Roles Section */}
{roles &&
roles.filter((r) => !r.isSystem).length > 0 && (
<div className="space-y-4">
<h4 className="text-sm font-medium">
Custom Roles
</h4>
{roles
?.filter((r) => !r.isSystem)
.map((role) => (
<FormItem
key={role.roleId}
className="flex items-center space-x-3 space-y-0"
>
<FormControl>
<RadioGroupItem value={role.roleId} />
</FormControl>
<FormLabel className="font-normal">
<span className="font-medium">
{role.name}
</span>
<div className="text-xs text-muted-foreground">
{role.description}
</div>
<div className="flex flex-wrap gap-1 mt-1">
{role.permissions?.map(
(permission) => {
const permissionInfo =
defaultRoles?.owner?.permissions?.find(
(p) => p === permission,
);
return (
<Badge
key={permission}
variant="secondary"
className="text-xs"
>
{permissionInfo?.name ||
permission}
</Badge>
);
},
)}
</div>
</FormLabel>
</FormItem>
))}
</div>
)}
</RadioGroup>
</FormControl>
</FormItem>
)}
/>
</div>
<DialogFooter>
<Button type="submit" disabled={isAssigningRole}>
{isAssigningRole ? "Assigning..." : "Assign Role"}
</Button>
</DialogFooter>
</form>
</Form>
</TabsContent>
{/* Create Role Tab Content */}
<TabsContent value="create">
<Form {...createForm}>
<form onSubmit={createForm.handleSubmit(onCreateRole)}>
<div className="space-y-4 py-4">
<FormField
control={createForm.control}
name="name"
render={({ field }) => (
<FormItem>
<FormLabel>Role Name</FormLabel>
<FormControl>
<Input placeholder="e.g. Developer" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={createForm.control}
name="description"
render={({ field }) => (
<FormItem>
<FormLabel>Description</FormLabel>
<FormControl>
<Input
placeholder="e.g. Role for development team members"
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={createForm.control}
name="permissions"
render={() => (
<FormItem>
<FormLabel>Permissions</FormLabel>
<Card>
<CardHeader>
<CardTitle>Available Permissions</CardTitle>
<CardDescription>
Select the permissions for this role
</CardDescription>
</CardHeader>
<CardContent className="grid grid-cols-2 gap-4">
{defaultRoles?.owner?.permissions?.map(
(permission) => (
<FormField
key={permission}
control={createForm.control}
name="permissions"
render={({ field }) => (
<FormItem
key={permission}
className="flex flex-row items-start space-x-3 space-y-0"
>
<FormControl>
<Checkbox
checked={field.value?.includes(
permission,
)}
onCheckedChange={(checked) => {
return checked
? field.onChange([
...(field.value || []),
permission,
])
: field.onChange(
field.value?.filter(
(value) =>
value !== permission,
),
);
}}
/>
</FormControl>
<FormLabel className="text-sm font-normal">
{permission}
</FormLabel>
</FormItem>
)}
/>
),
)}
</CardContent>
</Card>
<FormMessage />
</FormItem>
)}
/>
</div>
<DialogFooter>
<Button type="submit" disabled={isCreatingRole}>
{isCreatingRole ? "Creating..." : "Create Role"}
</Button>
</DialogFooter>
</form>
</Form>
</TabsContent>
</Tabs>
</DialogContent>
</Dialog>
);
};

View File

@@ -31,6 +31,7 @@ import { MoreHorizontal, Users } from "lucide-react";
import { Loader2 } from "lucide-react";
import { toast } from "sonner";
import { AddUserPermissions } from "./add-permissions";
import { AddUserPermissionsV2 } from "./add-permissions-v2";
export const ShowUsers = () => {
const { data: isCloud } = api.settings.isCloud.useQuery();
@@ -92,12 +93,12 @@ export const ShowUsers = () => {
<TableCell className="text-center">
<Badge
variant={
member.role === "owner"
member.role.name === "owner"
? "default"
: "secondary"
}
>
{member.role}
{member.role.name}
</Badge>
</TableCell>
<TableCell className="text-center">
@@ -128,9 +129,14 @@ export const ShowUsers = () => {
</DropdownMenuLabel>
{member.role !== "owner" && (
<AddUserPermissions
userId={member.user.id}
/>
<>
<AddUserPermissions
userId={member.user.id}
/>
<AddUserPermissionsV2
userId={member.user.id}
/>
</>
)}
{member.role !== "owner" && (

View File

@@ -0,0 +1,84 @@
CREATE TABLE "organization_role" (
"roleId" text PRIMARY KEY NOT NULL,
"name" text NOT NULL,
"description" text,
"canDelete" boolean DEFAULT true NOT NULL,
"is_system" boolean DEFAULT false,
"permissions" text[],
"created_at" timestamp NOT NULL,
"updated_at" timestamp NOT NULL,
"organizationId" text NOT NULL
);
-- Create default roles for each organization
DO $$
DECLARE
org RECORD;
BEGIN
FOR org IN SELECT id FROM "organization"
LOOP
-- Insert owner role
INSERT INTO "organization_role" ("roleId", "name", "description", "canDelete", "is_system", "permissions", "created_at", "updated_at", "organizationId")
VALUES (
org.id || '_owner',
'owner',
'Owner role with full access',
false,
true,
'{"project:create", "project:delete", "service:create", "service:delete", "traefik_files:access", "docker:view", "api:access", "ssh_keys:access", "git_providers:access"}',
NOW(),
NOW(),
org.id
);
-- Insert admin role
INSERT INTO "organization_role" ("roleId", "name", "description", "canDelete", "is_system", "permissions", "created_at", "updated_at", "organizationId")
VALUES (
org.id || '_admin',
'admin',
'Administrator role with elevated access',
false,
true,
'{"project:create", "project:delete", "service:create", "service:delete", "traefik_files:access", "docker:view", "api:access", "ssh_keys:access"}',
NOW(),
NOW(),
org.id
);
-- Insert member role
INSERT INTO "organization_role" ("roleId", "name", "description", "canDelete", "is_system", "permissions", "created_at", "updated_at", "organizationId")
VALUES (
org.id || '_member',
'member',
'Standard member role',
false,
true,
'{"project:create", "service:create", "docker:view"}',
NOW(),
NOW(),
org.id
);
END LOOP;
END $$;
--> statement-breakpoint
ALTER TABLE "member" ADD COLUMN "roleId" text;
--> statement-breakpoint
ALTER TABLE "organization_role" ADD CONSTRAINT "organization_role_organizationId_organization_id_fk" FOREIGN KEY ("organizationId") REFERENCES "public"."organization"("id") ON DELETE cascade ON UPDATE no action;
--> statement-breakpoint
ALTER TABLE "member" ADD CONSTRAINT "member_roleId_organization_role_roleId_fk" FOREIGN KEY ("roleId") REFERENCES "public"."organization_role"("roleId") ON DELETE cascade ON UPDATE no action;
-- Update existing members with corresponding roles based on their current role type
DO $$
DECLARE
mem RECORD;
BEGIN
FOR mem IN SELECT m.id, m.organization_id, m.role as role_type FROM "member" m
LOOP
UPDATE "member"
SET "roleId" = mem.organization_id || '_' || mem.role_type
WHERE id = mem.id;
END LOOP;
END $$;
ALTER TABLE "member" ALTER COLUMN "roleId" SET NOT NULL;

View File

@@ -0,0 +1,3 @@
ALTER TABLE "organization_role" ALTER COLUMN "created_at" SET DEFAULT now();--> statement-breakpoint
ALTER TABLE "organization_role" ALTER COLUMN "updated_at" SET DEFAULT now();--> statement-breakpoint
ALTER TABLE "organization_role" ADD CONSTRAINT "organization_role_name_unique" UNIQUE("name");

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -722,6 +722,20 @@
"when": 1751848685503,
"tag": "0102_opposite_grandmaster",
"breakpoints": true
},
{
"idx": 103,
"version": "7",
"when": 1752043274261,
"tag": "0103_misty_shockwave",
"breakpoints": true
},
{
"idx": 104,
"version": "7",
"when": 1752046360607,
"tag": "0104_dusty_miss_america",
"breakpoints": true
}
]
}

View File

@@ -38,6 +38,7 @@ import { userRouter } from "./routers/user";
import { scheduleRouter } from "./routers/schedule";
import { rollbackRouter } from "./routers/rollbacks";
import { volumeBackupsRouter } from "./routers/volume-backups";
import { roleRouter } from "./routers/role";
/**
* This is the primary router for your server.
*
@@ -84,6 +85,7 @@ export const appRouter = createTRPCRouter({
schedule: scheduleRouter,
rollback: rollbackRouter,
volumeBackups: volumeBackupsRouter,
role: roleRouter,
});
// export type definition of API

View File

@@ -0,0 +1,75 @@
import { createTRPCRouter, protectedProcedure } from "@/server/api/trpc";
import { db } from "@/server/db";
import {
apiFindOneRole,
createRoleSchema,
role,
updateRoleSchema,
} from "@/server/db/schema";
import { createRole, removeRoleById, updateRoleById } from "@dokploy/server";
import { TRPCError } from "@trpc/server";
import { and, eq } from "drizzle-orm";
export const roleRouter = createTRPCRouter({
all: protectedProcedure.query(async ({ ctx }) => {
const roles = await db.query.role.findMany({
where: and(
eq(role.organizationId, ctx.session.activeOrganizationId),
eq(role.isSystem, false),
),
});
return roles;
}),
delete: protectedProcedure
.input(apiFindOneRole)
.mutation(async ({ input }) => {
try {
return removeRoleById(input.roleId);
} catch (error) {
const message =
error instanceof Error ? error.message : "Error input: Deleting role";
throw new TRPCError({
code: "BAD_REQUEST",
message,
});
}
}),
create: protectedProcedure
.input(createRoleSchema)
.mutation(async ({ input, ctx }) => {
try {
return await createRole(
{
...input,
},
ctx.session.activeOrganizationId,
);
} catch (error) {
console.error(error);
throw new TRPCError({
code: "BAD_REQUEST",
message: "Error input: Creating role",
cause: error,
});
}
}),
update: protectedProcedure
.input(updateRoleSchema)
.mutation(async ({ input }) => {
return await updateRoleById(input.roleId, input);
}),
getDefaultRoles: protectedProcedure.query(async ({ ctx }) => {
const result = await db.query.role.findMany({
where: and(
eq(role.organizationId, ctx.session.activeOrganizationId),
eq(role.isSystem, true),
),
});
return {
owner: result.find((r) => r.name === "owner"),
admin: result.find((r) => r.name === "admin"),
member: result.find((r) => r.name === "member"),
};
}),
});

View File

@@ -54,6 +54,7 @@ export const userRouter = createTRPCRouter({
where: eq(member.organizationId, ctx.session.activeOrganizationId),
with: {
user: true,
role: true,
},
orderBy: [asc(member.createdAt)],
});
@@ -438,4 +439,46 @@ export const userRouter = createTRPCRouter({
}
return inviteLink;
}),
assignRole: adminProcedure
.input(
z.object({
userId: z.string(),
roleId: z.string(),
}),
)
.mutation(async ({ input, ctx }) => {
try {
const organization = await findOrganizationById(
ctx.session.activeOrganizationId,
);
if (organization?.ownerId !== ctx.user.ownerId) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not allowed to assign roles",
});
}
const memberResult = await db.query.member.findFirst({
where: and(
eq(member.userId, input.userId),
eq(member.organizationId, ctx.session.activeOrganizationId),
),
});
if (!memberResult) {
throw new TRPCError({
code: "NOT_FOUND",
message: "Member not found",
});
}
await db
.update(member)
.set({ roleId: input.roleId })
.where(eq(member.id, memberResult.id));
} catch (error) {
throw error;
}
}),
});

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