mirror of
https://github.com/Dokploy/dokploy.git
synced 2026-07-15 02:45:23 +02:00
- Introduced new test files for permission checks, including `check-permission.test.ts`, `enterprise-only-resources.test.ts`, `resolve-permissions.test.ts`, and `service-access.test.ts`. - Implemented permission checks in various components to ensure actions are gated by user permissions, including `ShowTraefikConfig`, `UpdateTraefikConfig`, `ShowVolumes`, `ShowDomains`, and others. - Enhanced the logic for displaying UI elements based on user permissions, ensuring that only authorized users can access or modify resources.
180 lines
4.6 KiB
TypeScript
180 lines
4.6 KiB
TypeScript
import { standardSchemaResolver as zodResolver } from "@hookform/resolvers/standard-schema";
|
|
import { FileIcon } from "lucide-react";
|
|
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 { CodeEditor } from "@/components/shared/code-editor";
|
|
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 { api } from "@/utils/api";
|
|
|
|
const updateProjectSchema = z.object({
|
|
env: z.string().optional(),
|
|
});
|
|
|
|
type UpdateProject = z.infer<typeof updateProjectSchema>;
|
|
|
|
interface Props {
|
|
projectId: string;
|
|
children?: React.ReactNode;
|
|
}
|
|
|
|
export const ProjectEnvironment = ({ projectId, children }: Props) => {
|
|
const { data: permissions } = api.user.getPermissions.useQuery();
|
|
const canRead = permissions?.projectEnvVars.read ?? false;
|
|
const canWrite = permissions?.projectEnvVars.write ?? false;
|
|
const [isOpen, setIsOpen] = useState(false);
|
|
const utils = api.useUtils();
|
|
const { mutateAsync, error, isError, isPending } =
|
|
api.project.update.useMutation();
|
|
const { data } = api.project.one.useQuery(
|
|
{
|
|
projectId,
|
|
},
|
|
{
|
|
enabled: !!projectId,
|
|
},
|
|
);
|
|
|
|
const form = useForm<UpdateProject>({
|
|
defaultValues: {
|
|
env: data?.env ?? "",
|
|
},
|
|
resolver: zodResolver(updateProjectSchema),
|
|
});
|
|
useEffect(() => {
|
|
if (data) {
|
|
form.reset({
|
|
env: data.env ?? "",
|
|
});
|
|
}
|
|
}, [data, form, form.reset]);
|
|
|
|
const onSubmit = async (formData: UpdateProject) => {
|
|
await mutateAsync({
|
|
env: formData.env || "",
|
|
projectId: projectId,
|
|
})
|
|
.then(() => {
|
|
toast.success("Project env updated successfully");
|
|
utils.project.all.invalidate();
|
|
})
|
|
.catch(() => {
|
|
toast.error("Error updating the env");
|
|
})
|
|
.finally(() => {});
|
|
};
|
|
|
|
// Add keyboard shortcut for Ctrl+S/Cmd+S
|
|
useEffect(() => {
|
|
const handleKeyDown = (e: KeyboardEvent) => {
|
|
if ((e.ctrlKey || e.metaKey) && e.key === "s" && !isPending && isOpen) {
|
|
e.preventDefault();
|
|
form.handleSubmit(onSubmit)();
|
|
}
|
|
};
|
|
|
|
document.addEventListener("keydown", handleKeyDown);
|
|
return () => {
|
|
document.removeEventListener("keydown", handleKeyDown);
|
|
};
|
|
}, [form, onSubmit, isPending, isOpen]);
|
|
|
|
if (!canRead) {
|
|
return null;
|
|
}
|
|
|
|
return (
|
|
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
|
<DialogTrigger asChild>
|
|
{children ?? (
|
|
<DropdownMenuItem
|
|
className="w-full cursor-pointer space-x-3"
|
|
onSelect={(e) => e.preventDefault()}
|
|
>
|
|
<FileIcon className="size-4" />
|
|
<span>Project Environment</span>
|
|
</DropdownMenuItem>
|
|
)}
|
|
</DialogTrigger>
|
|
<DialogContent className="sm:max-w-6xl">
|
|
<DialogHeader>
|
|
<DialogTitle>Project Environment</DialogTitle>
|
|
<DialogDescription>
|
|
Update the env Environment variables that are accessible to all
|
|
services of this project.
|
|
</DialogDescription>
|
|
</DialogHeader>
|
|
{isError && <AlertBlock type="error">{error?.message}</AlertBlock>}
|
|
<AlertBlock type="info">
|
|
Use this syntax to reference project-level variables in your service
|
|
environments: <code>DATABASE_URL=${"{{project.DATABASE_URL}}"}</code>
|
|
</AlertBlock>
|
|
<div className="grid gap-4">
|
|
<div className="grid items-center gap-4">
|
|
<Form {...form}>
|
|
<form
|
|
onSubmit={form.handleSubmit(onSubmit)}
|
|
className="grid w-full gap-4 "
|
|
>
|
|
<FormField
|
|
control={form.control}
|
|
name="env"
|
|
render={({ field }) => (
|
|
<FormItem>
|
|
<FormLabel>Environment variables</FormLabel>
|
|
<FormControl>
|
|
<CodeEditor
|
|
lineWrapping
|
|
language="properties"
|
|
readOnly={!canWrite}
|
|
wrapperClassName="h-[35rem] font-mono"
|
|
placeholder={`NODE_ENV=production
|
|
PORT=3000
|
|
|
|
`}
|
|
{...field}
|
|
/>
|
|
</FormControl>
|
|
|
|
<pre>
|
|
<FormMessage />
|
|
</pre>
|
|
</FormItem>
|
|
)}
|
|
/>
|
|
{canWrite && (
|
|
<DialogFooter>
|
|
<Button isLoading={isPending} type="submit">
|
|
Update
|
|
</Button>
|
|
</DialogFooter>
|
|
)}
|
|
</form>
|
|
</Form>
|
|
</div>
|
|
</div>
|
|
</DialogContent>
|
|
</Dialog>
|
|
);
|
|
};
|