import { standardSchemaResolver as zodResolver } from "@hookform/resolvers/standard-schema"; import { useEffect, useState } from "react"; import { useForm } from "react-hook-form"; import { toast } from "sonner"; import { z } from "zod"; import { AlertBlock } from "@/components/shared/alert-block"; import { Button } from "@/components/ui/button"; import { Checkbox } from "@/components/ui/checkbox"; import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DialogTrigger, } from "@/components/ui/dialog"; import { DropdownMenuItem } from "@/components/ui/dropdown-menu"; import { Form, FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage, } from "@/components/ui/form"; import { Switch } from "@/components/ui/switch"; import { api, type RouterOutputs } from "@/utils/api"; /** Shape returned by project.allForPermissions (admin only). Used for the permissions UI. */ type ProjectForPermissions = RouterOutputs["project"]["allForPermissions"][number]; type EnvironmentForPermissions = ProjectForPermissions["environments"][number]; type Project = ProjectForPermissions; type Environment = EnvironmentForPermissions; export type Services = { appName: string; serverId?: string | null; name: string; type: | "mariadb" | "application" | "postgres" | "mysql" | "mongo" | "redis" | "compose"; description?: string | null; id: string; createdAt: string; status?: "idle" | "running" | "done" | "error"; }; export const extractServices = (data: Environment | undefined) => { const applications: Services[] = (data?.applications?.map((item) => ({ appName: item.appName, name: item.name, type: "application", id: item.applicationId, createdAt: item.createdAt, status: item.applicationStatus, description: item.description, serverId: item.serverId, })) ?? []) as Services[]; const mariadb: Services[] = data?.mariadb.map((item) => ({ appName: item.appName, name: item.name, type: "mariadb", id: item.mariadbId, createdAt: item.createdAt, status: item.applicationStatus, description: item.description, serverId: item.serverId, })) || []; const postgres: Services[] = data?.postgres.map((item) => ({ appName: item.appName, name: item.name, type: "postgres", id: item.postgresId, createdAt: item.createdAt, status: item.applicationStatus, description: item.description, serverId: item.serverId, })) || []; const mongo: Services[] = data?.mongo.map((item) => ({ appName: item.appName, name: item.name, type: "mongo", id: item.mongoId, createdAt: item.createdAt, status: item.applicationStatus, description: item.description, serverId: item.serverId, })) || []; const redis: Services[] = data?.redis.map((item) => ({ appName: item.appName, name: item.name, type: "redis", id: item.redisId, createdAt: item.createdAt, status: item.applicationStatus, description: item.description, serverId: item.serverId, })) || []; const mysql: Services[] = data?.mysql.map((item) => ({ appName: item.appName, name: item.name, type: "mysql", id: item.mysqlId, createdAt: item.createdAt, status: item.applicationStatus, description: item.description, serverId: item.serverId, })) || []; const compose: Services[] = (data?.compose?.map((item) => ({ appName: item.appName, name: item.name, type: "compose", id: item.composeId, createdAt: item.createdAt, status: item.composeStatus, description: item.description, serverId: item.serverId, })) ?? []) as Services[]; applications.push( ...mysql, ...redis, ...mongo, ...postgres, ...mariadb, ...compose, ); applications.sort((a, b) => { return new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime(); }); return applications; }; const addPermissions = z.object({ accessedProjects: z.array(z.string()).optional(), accessedEnvironments: z.array(z.string()).optional(), accessedServices: z.array(z.string()).optional(), canCreateProjects: z.boolean().optional().default(false), canCreateServices: z.boolean().optional().default(false), canDeleteProjects: z.boolean().optional().default(false), canDeleteServices: z.boolean().optional().default(false), canDeleteEnvironments: z.boolean().optional().default(false), canAccessToTraefikFiles: z.boolean().optional().default(false), canAccessToDocker: z.boolean().optional().default(false), canAccessToAPI: z.boolean().optional().default(false), canAccessToSSHKeys: z.boolean().optional().default(false), canAccessToGitProviders: z.boolean().optional().default(false), canCreateEnvironments: z.boolean().optional().default(false), }); type AddPermissions = z.infer; interface Props { userId: string; } export const AddUserPermissions = ({ userId }: Props) => { const [isOpen, setIsOpen] = useState(false); const { data: projects } = api.project.allForPermissions.useQuery(undefined, { enabled: isOpen, }); const { data, refetch } = api.user.one.useQuery( { userId, }, { enabled: !!userId, }, ); const { mutateAsync, isError, error, isPending } = api.user.assignPermissions.useMutation(); const form = useForm({ defaultValues: { accessedProjects: [], accessedEnvironments: [], accessedServices: [], canDeleteEnvironments: false, canCreateProjects: false, canCreateServices: false, canDeleteProjects: false, canDeleteServices: false, canAccessToTraefikFiles: false, canAccessToDocker: false, canAccessToAPI: false, canAccessToSSHKeys: false, canAccessToGitProviders: false, canCreateEnvironments: false, }, resolver: zodResolver(addPermissions), }); useEffect(() => { if (data && isOpen) { form.reset({ accessedProjects: data.accessedProjects || [], accessedEnvironments: data.accessedEnvironments || [], accessedServices: data.accessedServices || [], canCreateProjects: data.canCreateProjects, canCreateServices: data.canCreateServices, canDeleteProjects: data.canDeleteProjects, canDeleteServices: data.canDeleteServices, canDeleteEnvironments: data.canDeleteEnvironments || false, canAccessToTraefikFiles: data.canAccessToTraefikFiles, canAccessToDocker: data.canAccessToDocker, canAccessToAPI: data.canAccessToAPI, canAccessToSSHKeys: data.canAccessToSSHKeys, canAccessToGitProviders: data.canAccessToGitProviders, canCreateEnvironments: data.canCreateEnvironments, }); } }, [form, form.reset, data, isOpen]); const onSubmit = async (data: AddPermissions) => { await mutateAsync({ id: userId, canCreateServices: data.canCreateServices, canCreateProjects: data.canCreateProjects, canDeleteServices: data.canDeleteServices, canDeleteProjects: data.canDeleteProjects, canDeleteEnvironments: data.canDeleteEnvironments, canAccessToTraefikFiles: data.canAccessToTraefikFiles, accessedProjects: data.accessedProjects || [], accessedEnvironments: data.accessedEnvironments || [], accessedServices: data.accessedServices || [], canAccessToDocker: data.canAccessToDocker, canAccessToAPI: data.canAccessToAPI, canAccessToSSHKeys: data.canAccessToSSHKeys, canAccessToGitProviders: data.canAccessToGitProviders, canCreateEnvironments: data.canCreateEnvironments, }) .then(async () => { toast.success("Permissions updated"); refetch(); setIsOpen(false); }) .catch(() => { toast.error("Error updating the permissions"); }); }; return ( e.preventDefault()} > Add Permissions Permissions Add or remove permissions {isError && {error?.message}}
(
Create Projects Allow the user to create projects
)} /> (
Delete Projects Allow the user to delete projects
)} /> (
Create Services Allow the user to create services
)} /> (
Delete Services Allow the user to delete services
)} /> (
Create Environments Allow the user to create environments
)} /> (
Delete Environments Allow the user to delete environments
)} /> (
Access to Traefik Files Allow the user to access to the Traefik Tab Files
)} /> (
Access to Docker Allow the user to access to the Docker Tab
)} /> (
Access to API/CLI Allow the user to access to the API/CLI
)} /> (
Access to SSH Keys Allow to users to access to the SSH Keys section
)} /> (
Access to Git Providers Allow to users to access to the Git Providers section
)} /> (
Projects Select the Projects that the user can access
{projects?.length === 0 && (

No projects found

)}
{projects?.map((project, projectIndex) => { return ( { return ( {/* Project Header */}
{ if (checked) { // Add the project field.onChange([ ...(field.value || []), project.projectId, ]); } else { // Remove the project field.onChange( field.value?.filter( (value) => value !== project.projectId, ), ); // Also remove all environments and services from this project const currentEnvs = form.getValues( "accessedEnvironments", ) || []; const currentServices = form.getValues( "accessedServices", ) || []; // Get all environment IDs from this project const projectEnvIds = project.environments.map( (env) => env.environmentId, ); // Get all service IDs from this project const projectServiceIds = project.environments.flatMap( (env) => extractServices(env).map( (service) => service.id, ), ); // Remove environments and services from this project form.setValue( "accessedEnvironments", currentEnvs.filter( (envId) => !projectEnvIds.includes(envId), ), ); form.setValue( "accessedServices", currentServices.filter( (serviceId) => !projectServiceIds.includes( serviceId, ), ), ); } }} /> {project.name}
{/* Environments */}
{project.environments.length === 0 && (

No environments found

)} {project.environments.map( (environment, envIndex) => { const services = extractServices(environment); return (
{/* Environment Header with Checkbox */} ( { if (checked) { // Add the environment envField.onChange([ ...(envField.value || []), environment.environmentId, ]); // Auto-select the project if not already selected const currentProjects = form.getValues( "accessedProjects", ) || []; if ( !currentProjects.includes( project.projectId, ) ) { form.setValue( "accessedProjects", [ ...currentProjects, project.projectId, ], ); } } else { // Remove the environment envField.onChange( envField.value?.filter( (value) => value !== environment.environmentId, ), ); // Also remove all services from this environment const currentServices = form.getValues( "accessedServices", ) || []; const environmentServiceIds = services.map( (service) => service.id, ); form.setValue( "accessedServices", currentServices.filter( (serviceId) => !environmentServiceIds.includes( serviceId, ), ), ); } }} />
{environment.name} ({services.length} services)
)} /> {/* Services */}
{services.length === 0 && (

No services found

)} {services.map( (service, serviceIndex) => ( { return ( { if (checked) { // Add the service serviceField.onChange( [ ...(serviceField.value || []), service.id, ], ); // Auto-select the environment if not already selected const currentEnvs = form.getValues( "accessedEnvironments", ) || []; if ( !currentEnvs.includes( environment.environmentId, ) ) { form.setValue( "accessedEnvironments", [ ...currentEnvs, environment.environmentId, ], ); } // Auto-select the project if not already selected const currentProjects = form.getValues( "accessedProjects", ) || []; if ( !currentProjects.includes( project.projectId, ) ) { form.setValue( "accessedProjects", [ ...currentProjects, project.projectId, ], ); } } else { // Remove the service serviceField.onChange( serviceField.value?.filter( (value) => value !== service.id, ), ); } }} />
{service.name} ({service.type})
); }} /> ), )}
); }, )}
); }} /> ); })}
)} />
); };