import { standardSchemaResolver as zodResolver } from "@hookform/resolvers/standard-schema"; import { Loader2, PlusIcon, ShieldCheck, Sparkles, TrashIcon, Users, } from "lucide-react"; import { useEffect, useState } from "react"; import { useForm } from "react-hook-form"; import { toast } from "sonner"; import { z } from "zod"; import { EnterpriseFeatureGate } from "@/components/proprietary/enterprise-feature-gate"; import { AlertBlock } from "@/components/shared/alert-block"; import { DialogAction } from "@/components/shared/dialog-action"; import { Button } from "@/components/ui/button"; import { Card, CardContent, CardDescription, CardHeader, CardTitle, } from "@/components/ui/card"; import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DialogTrigger, } from "@/components/ui/dialog"; import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage, } from "@/components/ui/form"; import { Input } from "@/components/ui/input"; import { Popover, PopoverContent, PopoverTrigger, } from "@/components/ui/popover"; import { Switch } from "@/components/ui/switch"; import { api } from "@/utils/api"; /** Labels and descriptions for each resource */ const RESOURCE_META: Record = { project: { label: "Projects", description: "Manage project creation and deletion", }, service: { label: "Services", description: "Manage services (applications, databases, compose) within projects", }, environment: { label: "Environments", description: "Manage environment creation, viewing, and deletion", }, docker: { label: "Docker", description: "Access to Docker containers, images, and volumes management", }, sshKeys: { label: "SSH Keys", description: "Manage SSH key configurations for servers and repositories", }, gitProviders: { label: "Git Providers", description: "Access to Git providers (GitHub, GitLab, Bitbucket, Gitea)", }, traefikFiles: { label: "Traefik Files", description: "Access to the Traefik file system configuration", }, api: { label: "API / CLI", description: "Access to API keys and CLI usage", }, // Enterprise-only resources volume: { label: "Volumes", description: "Manage persistent volumes and mounts attached to services", }, deployment: { label: "Deployments", description: "Trigger, view, and cancel service deployments", }, envVars: { label: "Service Env Vars", description: "View and edit environment variables of services", }, projectEnvVars: { label: "Project Shared Env Vars", description: "View and edit shared environment variables at project level", }, environmentEnvVars: { label: "Environment Shared Env Vars", description: "View and edit shared environment variables at environment level", }, server: { label: "Servers", description: "Manage remote servers and nodes", }, registry: { label: "Registries", description: "Manage Docker image registries", }, certificate: { label: "Certificates", description: "Manage SSL/TLS certificates", }, backup: { label: "Backups", description: "Manage database backups and restores", }, volumeBackup: { label: "Volume Backups", description: "Manage Docker volume backups and restores", }, schedule: { label: "Schedules", description: "Manage scheduled jobs (commands, deployments, scripts)", }, domain: { label: "Domains", description: "Manage custom domains assigned to services", }, destination: { label: "S3 Destinations", description: "Manage S3-compatible backup destinations (AWS, Cloudflare R2, etc.)", }, notification: { label: "Notifications", description: "Manage notification providers (Slack, Discord, Telegram, etc.)", }, member: { label: "Users", description: "Manage organization members, invitations, and roles", }, logs: { label: "Logs", description: "View service and deployment logs", }, monitoring: { label: "Monitoring", description: "View server and service metrics (CPU, RAM, disk)", }, auditLog: { label: "Audit Logs", description: "View the audit log of actions performed in the organization", }, }; /** Descriptions for each action within a resource */ const ACTION_META: Record< string, Record > = { project: { create: { label: "Create", description: "Create new projects" }, delete: { label: "Delete", description: "Delete projects and all their content", }, }, service: { create: { label: "Create", description: "Create new services inside projects", }, read: { label: "Read", description: "View services, logs, and deployments", }, delete: { label: "Delete", description: "Delete services from projects", }, }, environment: { create: { label: "Create", description: "Create new environments in projects", }, read: { label: "Read", description: "View environments and their services", }, delete: { label: "Delete", description: "Delete environments and their content", }, }, docker: { read: { label: "Read", description: "View Docker containers, images, networks, and volumes", }, }, sshKeys: { read: { label: "Read", description: "View SSH key configurations", }, create: { label: "Create", description: "Create and edit SSH keys", }, delete: { label: "Delete", description: "Remove SSH keys", }, }, gitProviders: { read: { label: "Read", description: "View Git provider connections", }, create: { label: "Create", description: "Create and update Git provider connections", }, delete: { label: "Delete", description: "Remove Git provider connections", }, }, traefikFiles: { read: { label: "Read", description: "View Traefik configuration files", }, write: { label: "Write", description: "Edit and save Traefik configuration files", }, }, api: { read: { label: "Read", description: "Create and manage API keys for CLI access", }, }, volume: { read: { label: "Read", description: "View volumes and mounts attached to services", }, create: { label: "Create", description: "Add and edit volumes and mounts" }, delete: { label: "Delete", description: "Remove volumes and mounts from services", }, }, deployment: { read: { label: "Read", description: "View deployment history and status" }, create: { label: "Deploy", description: "Trigger new deployments manually", }, cancel: { label: "Cancel", description: "Cancel running deployments" }, }, envVars: { read: { label: "Read", description: "View environment variable values" }, write: { label: "Write", description: "Create, update, and delete environment variables", }, }, projectEnvVars: { read: { label: "Read", description: "View project-level shared environment variables", }, write: { label: "Write", description: "Edit project-level shared environment variables", }, }, environmentEnvVars: { read: { label: "Read", description: "View environment-level shared environment variables", }, write: { label: "Write", description: "Edit environment-level shared environment variables", }, }, server: { read: { label: "Read", description: "View server list and connection details", }, create: { label: "Create", description: "Add new remote servers" }, delete: { label: "Delete", description: "Remove servers from the organization", }, }, registry: { read: { label: "Read", description: "View configured Docker registries" }, create: { label: "Create", description: "Add new Docker registries" }, delete: { label: "Delete", description: "Remove Docker registries" }, }, certificate: { read: { label: "Read", description: "View SSL/TLS certificates" }, create: { label: "Create", description: "Issue and configure new certificates", }, delete: { label: "Delete", description: "Remove certificates" }, }, backup: { read: { label: "Read", description: "View backup history and status" }, create: { label: "Create", description: "Trigger manual backups" }, delete: { label: "Delete", description: "Delete backup files" }, restore: { label: "Restore", description: "Restore a database from a backup", }, }, volumeBackup: { read: { label: "Read", description: "View volume backup history and status", }, create: { label: "Create", description: "Create and trigger volume backups", }, update: { label: "Update", description: "Update volume backup configuration", }, delete: { label: "Delete", description: "Delete volume backup files" }, restore: { label: "Restore", description: "Restore a Docker volume from a backup", }, }, schedule: { read: { label: "Read", description: "View scheduled jobs and their history", }, create: { label: "Create", description: "Create and run scheduled jobs" }, update: { label: "Update", description: "Update scheduled job configuration", }, delete: { label: "Delete", description: "Delete scheduled jobs" }, }, domain: { read: { label: "Read", description: "View domains assigned to services" }, create: { label: "Create", description: "Assign new domains to services" }, delete: { label: "Delete", description: "Remove domains from services" }, }, destination: { read: { label: "Read", description: "View S3 backup destinations" }, create: { label: "Create", description: "Add and edit S3 destinations" }, delete: { label: "Delete", description: "Remove S3 destinations" }, }, notification: { read: { label: "Read", description: "View notification providers" }, create: { label: "Create", description: "Add and edit notification providers", }, delete: { label: "Delete", description: "Remove notification providers" }, }, member: { read: { label: "Read", description: "View the list of organization members", }, create: { label: "Create", description: "Invite new members to the organization", }, update: { label: "Update", description: "Change member roles and permissions", }, delete: { label: "Delete", description: "Remove members from the organization", }, }, logs: { read: { label: "Read", description: "View real-time and historical logs" }, }, monitoring: { read: { label: "Read", description: "View CPU, RAM, disk, and network metrics", }, }, auditLog: { read: { label: "Read", description: "View the audit log history" }, }, }; /** Resources that should be hidden from the custom role editor (better-auth internals) */ const HIDDEN_RESOURCES = ["organization", "invitation", "team", "ac"]; /** Predefined role presets with sensible permission defaults */ const ROLE_PRESETS: { name: string; label: string; description: string; permissions: Record; }[] = [ { name: "viewer", label: "Viewer", description: "Read-only access across all resources", permissions: { service: ["read"], environment: ["read"], docker: ["read"], sshKeys: ["read"], gitProviders: ["read"], traefikFiles: ["read"], api: ["read"], volume: ["read"], deployment: ["read"], envVars: ["read"], projectEnvVars: ["read"], environmentEnvVars: ["read"], server: ["read"], registry: ["read"], certificate: ["read"], backup: ["read"], volumeBackup: ["read"], schedule: ["read"], domain: ["read"], destination: ["read"], notification: ["read"], member: ["read"], logs: ["read"], monitoring: ["read"], auditLog: ["read"], }, }, { name: "developer", label: "Developer", description: "Deploy services, manage env vars, domains, and view logs", permissions: { project: ["create"], service: ["create", "read"], environment: ["create", "read"], docker: ["read"], gitProviders: ["read"], api: ["read"], volume: ["read", "create", "delete"], deployment: ["read", "create", "cancel"], envVars: ["read", "write"], projectEnvVars: ["read"], environmentEnvVars: ["read"], domain: ["read", "create", "delete"], schedule: ["read", "create", "update", "delete"], logs: ["read"], monitoring: ["read"], }, }, { name: "deployer", label: "Deployer", description: "Trigger and manage deployments only", permissions: { service: ["read"], environment: ["read"], deployment: ["read", "create", "cancel"], logs: ["read"], monitoring: ["read"], }, }, { name: "devops", label: "DevOps", description: "Full infrastructure access: servers, registries, certs, backups, and deployments", permissions: { project: ["create", "delete"], service: ["create", "read", "delete"], environment: ["create", "read", "delete"], docker: ["read"], sshKeys: ["read", "create", "delete"], gitProviders: ["read", "create", "delete"], traefikFiles: ["read", "write"], api: ["read"], volume: ["read", "create", "delete"], deployment: ["read", "create", "cancel"], envVars: ["read", "write"], projectEnvVars: ["read", "write"], environmentEnvVars: ["read", "write"], server: ["read", "create", "delete"], registry: ["read", "create", "delete"], certificate: ["read", "create", "delete"], backup: ["read", "create", "delete", "restore"], volumeBackup: ["read", "create", "update", "delete", "restore"], schedule: ["read", "create", "update", "delete"], domain: ["read", "create", "delete"], destination: ["read", "create", "delete"], notification: ["read", "create", "delete"], logs: ["read"], monitoring: ["read"], auditLog: ["read"], }, }, ]; const createRoleSchema = z.object({ roleName: z .string() .min(1, "Role name is required") .max(50, "Role name must be 50 characters or less") .regex( /^[a-zA-Z0-9_-]+$/, "Only letters, numbers, hyphens, and underscores allowed", ), }); type CreateRoleSchema = z.infer; export const ManageCustomRoles = () => { return (
Custom Roles Create and manage custom roles with fine-grained permissions
); }; interface HandleCustomRoleProps { roleName?: string; initialPermissions?: Record; onSuccess: () => void; } function HandleCustomRole({ roleName, initialPermissions, onSuccess, }: HandleCustomRoleProps) { const [open, setOpen] = useState(false); const [permissions, setPermissions] = useState>({}); const { data: statements } = api.customRole.getStatements.useQuery(); const isEdit = !!roleName; const form = useForm({ defaultValues: { roleName: "" }, resolver: zodResolver(createRoleSchema), }); useEffect(() => { if (open) { setPermissions(initialPermissions ? { ...initialPermissions } : {}); form.reset({ roleName: isEdit ? (roleName ?? "") : "" }); } }, [open]); const { mutateAsync: createRole, isPending: isCreating } = api.customRole.create.useMutation(); const { mutateAsync: updateRole, isPending: isUpdating } = api.customRole.update.useMutation(); const visibleResources = statements ? Object.entries(statements).filter( ([key]) => !HIDDEN_RESOURCES.includes(key), ) : []; const togglePermission = (resource: string, action: string) => { setPermissions((prev) => { const current = prev[resource] || []; const has = current.includes(action); return { ...prev, [resource]: has ? current.filter((a) => a !== action) : [...current, action], }; }); }; const handleSubmit = async (data: CreateRoleSchema) => { try { if (isEdit) { const newName = data.roleName !== roleName ? data.roleName : undefined; await updateRole({ roleName: roleName!, newRoleName: newName, permissions, }); toast.success(`Role "${newName ?? roleName}" updated`); } else { await createRole({ roleName: data.roleName, permissions }); toast.success(`Role "${data.roleName}" created`); } if (!isEdit) { setOpen(false); } onSuccess(); } catch (error) { let message = `Error ${isEdit ? "updating" : "creating"} role`; if (error instanceof Error) { try { const parsed = JSON.parse(error.message); if (Array.isArray(parsed) && parsed[0]?.message) { message = parsed[0].message; } else { message = error.message; } } catch { message = error.message; } } toast.error(message); } }; return ( {isEdit ? ( ) : ( )} {isEdit ? "Edit Role" : "Create Custom Role"} {isEdit ? "Update permissions for this role" : "Define a new role with specific permissions"}
( Role Name )} /> {!isEdit && (

Start from a preset

{ROLE_PRESETS.map((preset) => ( ))}
)}
); } const CustomRolesContent = () => { const { data: customRoles, isPending, refetch, } = api.customRole.all.useQuery(); const { mutateAsync: deleteRole } = api.customRole.remove.useMutation(); const handleDelete = async (roleName: string) => { try { await deleteRole({ roleName }); toast.success(`Role "${roleName}" deleted`); refetch(); } catch (error) { let message = "Error deleting role"; if (error instanceof Error) { try { const parsed = JSON.parse(error.message); message = Array.isArray(parsed) && parsed[0]?.message ? parsed[0].message : error.message; } catch { message = error.message; } } toast.error(message); } }; if (isPending) { return (
Loading...
); } return (
{customRoles?.length === 0 ? (

No custom roles yet

Create a role to define fine-grained access for your team members.

) : (
{customRoles?.map((role) => { const totalPermissions = Object.values(role.permissions).flat() .length; const enabledResources = Object.entries(role.permissions).filter( ([, actions]) => (actions as string[]).length > 0, ); return (

{role.role}

{role.memberCount > 0 && ( )}

{enabledResources.length} resource {enabledResources.length !== 1 ? "s" : ""} ·{" "} {totalPermissions} permission {totalPermissions !== 1 ? "s" : ""}

{role.memberCount > 0 && ( {role.memberCount} member {role.memberCount !== 1 ? "s are" : " is"}{" "} currently assigned {" "} to this role. Reassign them before deleting. )} Are you sure you want to delete the{" "} "{role.role}" role? This action cannot be undone.
} disabled={role.memberCount > 0} type="destructive" onClick={() => handleDelete(role.role)} >
{enabledResources.length > 0 && (
{enabledResources.map(([resource, actions]) => (
{RESOURCE_META[resource]?.label || resource} · {(actions as string[]) .map((a) => ACTION_META[resource]?.[a]?.label || a) .join(", ")}
))}
)}
); })}
)} ); }; function MembersBadge({ roleName, count, }: { roleName: string; count: number; }) { const [open, setOpen] = useState(false); const { data: members, isLoading } = api.customRole.membersByRole.useQuery( { roleName }, { enabled: open }, ); return (

Assigned members

{isLoading ? (
) : members && members.length > 0 ? (
    {members.map((m) => (
  • {(m.firstName?.[0] || m.email?.[0] || "?").toUpperCase()}
    {(m.firstName || m.lastName) && (

    {[m.firstName, m.lastName].filter(Boolean).join(" ")}

    )}

    {m.email}

  • ))}
) : (

No members found.

)}
); } /** Reusable permission toggle grid with descriptions */ function PermissionEditor({ resources, permissions, onToggle, }: { resources: [string, readonly string[]][]; permissions: Record; onToggle: (resource: string, action: string) => void; }) { return (

Permissions

{resources.map(([resource, actions]) => { const meta = RESOURCE_META[resource]; return (

{meta?.label || resource}

{meta?.description && (

{meta.description}

)}
{actions.map((action) => { const actionMeta = ACTION_META[resource]?.[action]; return (
onToggle(resource, action)} > onToggle(resource, action)} />
{actionMeta?.label || action}
); })}
); })}
); }