mirror of
https://github.com/Dokploy/dokploy
synced 2025-06-26 18:27:59 +00:00
Compare commits
1 Commits
v0.12.0
...
feat/multi
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
525b420e75 |
358
apps/dokploy/components/dashboard/application/domains/schema.tsx
Normal file
358
apps/dokploy/components/dashboard/application/domains/schema.tsx
Normal file
@@ -0,0 +1,358 @@
|
|||||||
|
import { UseFormGetValues } from "react-hook-form";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
export const providersData = [
|
||||||
|
{
|
||||||
|
name: "S3",
|
||||||
|
type: "s3",
|
||||||
|
properties: [
|
||||||
|
{
|
||||||
|
name: "accessKey",
|
||||||
|
type: "text",
|
||||||
|
label: "Access Key",
|
||||||
|
description: "Your S3 Access Key",
|
||||||
|
required: true,
|
||||||
|
default: "",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "secretAccessKey",
|
||||||
|
type: "password",
|
||||||
|
label: "Secret Access Key",
|
||||||
|
description: "Your S3 Secret Access Key",
|
||||||
|
required: true,
|
||||||
|
default: "",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "region",
|
||||||
|
type: "text",
|
||||||
|
label: "Region",
|
||||||
|
description: "AWS Region, e.g., us-east-1",
|
||||||
|
required: true,
|
||||||
|
default: "",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "endpoint",
|
||||||
|
type: "text",
|
||||||
|
label: "Endpoint",
|
||||||
|
description: "S3 Endpoint URL",
|
||||||
|
required: true,
|
||||||
|
default: "https://s3.amazonaws.com",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "bucket",
|
||||||
|
type: "text",
|
||||||
|
label: "Bucket Name",
|
||||||
|
description: "Name of the S3 bucket",
|
||||||
|
required: true,
|
||||||
|
default: "",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "provider",
|
||||||
|
type: "select",
|
||||||
|
label: "S3 Provider",
|
||||||
|
description: "Select your S3 provider",
|
||||||
|
required: false,
|
||||||
|
default: "AWS",
|
||||||
|
options: ["AWS", "Ceph", "Minio", "Alibaba", "Other"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "storageClass",
|
||||||
|
type: "text",
|
||||||
|
label: "Storage Class",
|
||||||
|
description: "S3 Storage Class, e.g., STANDARD, REDUCED_REDUNDANCY",
|
||||||
|
required: false,
|
||||||
|
default: "",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "acl",
|
||||||
|
type: "text",
|
||||||
|
label: "ACL",
|
||||||
|
description: "Access Control List settings for S3",
|
||||||
|
required: false,
|
||||||
|
default: "",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "GCS",
|
||||||
|
type: "gcs",
|
||||||
|
properties: [
|
||||||
|
{
|
||||||
|
name: "serviceAccountFile",
|
||||||
|
type: "text",
|
||||||
|
label: "Service Account File",
|
||||||
|
description:
|
||||||
|
"Path to the JSON file containing your service account key",
|
||||||
|
required: false,
|
||||||
|
default: "",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "clientId",
|
||||||
|
type: "text",
|
||||||
|
label: "Client ID",
|
||||||
|
description:
|
||||||
|
"Your GCS OAuth Client ID (required if Service Account File not provided)",
|
||||||
|
required: false,
|
||||||
|
default: "",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "clientSecret",
|
||||||
|
type: "password",
|
||||||
|
label: "Client Secret",
|
||||||
|
description:
|
||||||
|
"Your GCS OAuth Client Secret (required if Service Account File not provided)",
|
||||||
|
required: false,
|
||||||
|
default: "",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "projectNumber",
|
||||||
|
type: "text",
|
||||||
|
label: "Project Number",
|
||||||
|
description:
|
||||||
|
"Your GCS Project Number (required if Service Account File not provided)",
|
||||||
|
required: false,
|
||||||
|
default: "",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "bucket",
|
||||||
|
type: "text",
|
||||||
|
label: "Bucket Name",
|
||||||
|
description: "Name of the GCS bucket",
|
||||||
|
required: true,
|
||||||
|
default: "",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "objectAcl",
|
||||||
|
type: "text",
|
||||||
|
label: "Object ACL",
|
||||||
|
description: "Access Control List for objects uploaded to GCS",
|
||||||
|
required: false,
|
||||||
|
default: "",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "bucketAcl",
|
||||||
|
type: "text",
|
||||||
|
label: "Bucket ACL",
|
||||||
|
description: "Access Control List for the GCS bucket",
|
||||||
|
required: false,
|
||||||
|
default: "",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Azure Blob",
|
||||||
|
type: "azureblob",
|
||||||
|
properties: [
|
||||||
|
{
|
||||||
|
name: "account",
|
||||||
|
type: "text",
|
||||||
|
label: "Account Name",
|
||||||
|
description: "Your Azure Storage account name",
|
||||||
|
required: true,
|
||||||
|
default: "",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "key",
|
||||||
|
type: "password",
|
||||||
|
label: "Account Key",
|
||||||
|
description: "Your Azure Storage account access key",
|
||||||
|
required: true,
|
||||||
|
default: "",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "endpoint",
|
||||||
|
type: "text",
|
||||||
|
label: "Endpoint",
|
||||||
|
description: "Custom endpoint for Azure Blob Storage (if any)",
|
||||||
|
required: false,
|
||||||
|
default: "",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "container",
|
||||||
|
type: "text",
|
||||||
|
label: "Container Name",
|
||||||
|
description: "Name of the Azure Blob container",
|
||||||
|
required: true,
|
||||||
|
default: "",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Dropbox",
|
||||||
|
type: "dropbox",
|
||||||
|
properties: [
|
||||||
|
{
|
||||||
|
name: "token",
|
||||||
|
type: "password",
|
||||||
|
label: "Access Token",
|
||||||
|
description: "Your Dropbox access token",
|
||||||
|
required: true,
|
||||||
|
default: "",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "path",
|
||||||
|
type: "text",
|
||||||
|
label: "Destination Path",
|
||||||
|
description: "Path in Dropbox where the files will be uploaded",
|
||||||
|
required: false,
|
||||||
|
default: "/",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "FTP",
|
||||||
|
type: "ftp",
|
||||||
|
properties: [
|
||||||
|
{
|
||||||
|
name: "host",
|
||||||
|
type: "text",
|
||||||
|
label: "FTP Host",
|
||||||
|
description: "Hostname or IP address of the FTP server",
|
||||||
|
required: true,
|
||||||
|
default: "",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "port",
|
||||||
|
type: "number",
|
||||||
|
label: "FTP Port",
|
||||||
|
description: "Port number of the FTP server",
|
||||||
|
required: false,
|
||||||
|
default: 21,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "user",
|
||||||
|
type: "text",
|
||||||
|
label: "Username",
|
||||||
|
description: "FTP username",
|
||||||
|
required: true,
|
||||||
|
default: "",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "pass",
|
||||||
|
type: "password",
|
||||||
|
label: "Password",
|
||||||
|
description: "FTP password",
|
||||||
|
required: true,
|
||||||
|
default: "",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "secure",
|
||||||
|
type: "checkbox",
|
||||||
|
label: "Use FTPS",
|
||||||
|
description: "Enable FTPS (FTP over SSL/TLS)",
|
||||||
|
required: false,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "path",
|
||||||
|
type: "text",
|
||||||
|
label: "Destination Path",
|
||||||
|
description: "Remote path on the FTP server",
|
||||||
|
required: false,
|
||||||
|
default: "/",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* S3 Provider Schema
|
||||||
|
*/
|
||||||
|
export const s3Schema = z.object({
|
||||||
|
accessKey: z.string().nonempty({ message: "Access Key is required" }),
|
||||||
|
secretAccessKey: z
|
||||||
|
.string()
|
||||||
|
.nonempty({ message: "Secret Access Key is required" }),
|
||||||
|
region: z.string().nonempty({ message: "Region is required" }),
|
||||||
|
endpoint: z
|
||||||
|
.string()
|
||||||
|
.nonempty({ message: "Endpoint is required" })
|
||||||
|
.default("https://s3.amazonaws.com"),
|
||||||
|
bucket: z.string().nonempty({ message: "Bucket Name is required" }),
|
||||||
|
provider: z
|
||||||
|
.enum(["AWS", "Ceph", "Minio", "Alibaba", "Other"])
|
||||||
|
.optional()
|
||||||
|
.default("AWS"),
|
||||||
|
storageClass: z.string().optional(),
|
||||||
|
acl: z.string().optional(),
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Azure Blob Storage Provider Schema
|
||||||
|
*/
|
||||||
|
export const azureBlobSchema = z.object({
|
||||||
|
account: z.string().nonempty({ message: "Account Name is required" }),
|
||||||
|
key: z.string().nonempty({ message: "Account Key is required" }),
|
||||||
|
endpoint: z.string().optional(),
|
||||||
|
container: z.string().nonempty({ message: "Container Name is required" }),
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Dropbox Provider Schema
|
||||||
|
*/
|
||||||
|
export const dropboxSchema = z.object({
|
||||||
|
token: z.string().nonempty({ message: "Access Token is required" }),
|
||||||
|
path: z.string().optional().default("/"),
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* FTP Provider Schema
|
||||||
|
*/
|
||||||
|
export const ftpSchema = z.object({
|
||||||
|
host: z.string().nonempty({ message: "FTP Host is required" }),
|
||||||
|
port: z.number().optional().default(21),
|
||||||
|
user: z.string().nonempty({ message: "Username is required" }),
|
||||||
|
pass: z.string().nonempty({ message: "Password is required" }),
|
||||||
|
secure: z.boolean().optional().default(false),
|
||||||
|
path: z.string().optional().default("/"),
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Exporting all schemas in a single object for convenience
|
||||||
|
*/
|
||||||
|
|
||||||
|
export const providerSchemas = {
|
||||||
|
s3: s3Schema,
|
||||||
|
azureblob: azureBlobSchema,
|
||||||
|
dropbox: dropboxSchema,
|
||||||
|
ftp: ftpSchema,
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getObjectSchema = (schema: z.ZodTypeAny) => {
|
||||||
|
const initialValues: any = {};
|
||||||
|
|
||||||
|
if (schema instanceof z.ZodObject) {
|
||||||
|
const shape = schema._def.shape();
|
||||||
|
|
||||||
|
for (const [key, fieldSchema] of Object.entries(shape)) {
|
||||||
|
if ("_def" in fieldSchema && "defaultValue" in fieldSchema._def) {
|
||||||
|
initialValues[key] = fieldSchema._def.defaultValue();
|
||||||
|
} else {
|
||||||
|
initialValues[key] = "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return initialValues;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const mergeFormValues = (
|
||||||
|
schema: z.ZodTypeAny,
|
||||||
|
values: Record<string, any>,
|
||||||
|
) => {
|
||||||
|
const initialSchemaObj = getObjectSchema(schema);
|
||||||
|
|
||||||
|
const properties: any = {};
|
||||||
|
|
||||||
|
for (const key in values) {
|
||||||
|
const keysMatch = Object.keys(initialSchemaObj).filter((k) => k === key);
|
||||||
|
if (keysMatch.length === 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
properties[keysMatch[0] as keyof typeof initialSchemaObj] =
|
||||||
|
values[key] || "";
|
||||||
|
}
|
||||||
|
|
||||||
|
return properties;
|
||||||
|
};
|
||||||
@@ -17,13 +17,29 @@ import {
|
|||||||
FormLabel,
|
FormLabel,
|
||||||
FormMessage,
|
FormMessage,
|
||||||
} from "@/components/ui/form";
|
} from "@/components/ui/form";
|
||||||
|
import {
|
||||||
|
Select,
|
||||||
|
SelectContent,
|
||||||
|
SelectGroup,
|
||||||
|
SelectItem,
|
||||||
|
SelectLabel,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectValue,
|
||||||
|
} from "@/components/ui/select";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import { useEffect } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
import {
|
||||||
|
getObjectSchema,
|
||||||
|
mergeFormValues,
|
||||||
|
providerSchemas,
|
||||||
|
providersData,
|
||||||
|
} from "../../application/domains/schema";
|
||||||
|
import { capitalize } from "lodash";
|
||||||
|
|
||||||
const addDestination = z.object({
|
const addDestination = z.object({
|
||||||
name: z.string().min(1, "Name is required"),
|
name: z.string().min(1, "Name is required"),
|
||||||
@@ -38,43 +54,45 @@ type AddDestination = z.infer<typeof addDestination>;
|
|||||||
|
|
||||||
export const AddDestination = () => {
|
export const AddDestination = () => {
|
||||||
const utils = api.useUtils();
|
const utils = api.useUtils();
|
||||||
|
const [provider, setProviders] = useState<keyof typeof providerSchemas>("s3");
|
||||||
|
|
||||||
const { mutateAsync, isError, error, isLoading } =
|
const { mutateAsync, isError, error, isLoading } =
|
||||||
api.destination.create.useMutation();
|
api.destination.create.useMutation();
|
||||||
const { mutateAsync: testConnection, isLoading: isLoadingConnection } =
|
const { mutateAsync: testConnection, isLoading: isLoadingConnection } =
|
||||||
api.destination.testConnection.useMutation();
|
api.destination.testConnection.useMutation();
|
||||||
const form = useForm<AddDestination>({
|
const schema = providerSchemas[provider];
|
||||||
|
const form = useForm<z.infer<typeof schema>>({
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
accessKeyId: "",
|
...getObjectSchema(schema),
|
||||||
bucket: "",
|
|
||||||
name: "",
|
|
||||||
region: "",
|
|
||||||
secretAccessKey: "",
|
|
||||||
endpoint: "",
|
|
||||||
},
|
},
|
||||||
resolver: zodResolver(addDestination),
|
resolver: zodResolver(schema),
|
||||||
});
|
});
|
||||||
useEffect(() => {
|
const {
|
||||||
form.reset();
|
register,
|
||||||
}, [form, form.reset, form.formState.isSubmitSuccessful]);
|
handleSubmit,
|
||||||
|
control,
|
||||||
|
formState: { errors },
|
||||||
|
} = form;
|
||||||
|
|
||||||
const onSubmit = async (data: AddDestination) => {
|
const onSubmit = async (data: z.infer<typeof schema>) => {
|
||||||
await mutateAsync({
|
// await mutateAsync({
|
||||||
accessKey: data.accessKeyId,
|
// accessKey: data.accessKeyId,
|
||||||
bucket: data.bucket,
|
// bucket: data.bucket,
|
||||||
endpoint: data.endpoint,
|
// endpoint: data.endpoint,
|
||||||
name: data.name,
|
// name: data.name,
|
||||||
region: data.region,
|
// region: data.region,
|
||||||
secretAccessKey: data.secretAccessKey,
|
// secretAccessKey: data.secretAccessKey,
|
||||||
})
|
// })
|
||||||
.then(async () => {
|
// .then(async () => {
|
||||||
toast.success("Destination Created");
|
// toast.success("Destination Created");
|
||||||
await utils.destination.all.invalidate();
|
// await utils.destination.all.invalidate();
|
||||||
})
|
// })
|
||||||
.catch(() => {
|
// .catch(() => {
|
||||||
toast.error("Error to create the Destination");
|
// toast.error("Error to create the Destination");
|
||||||
});
|
// });
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const fields = Object.keys(schema.shape);
|
||||||
return (
|
return (
|
||||||
<Dialog>
|
<Dialog>
|
||||||
<DialogTrigger className="" asChild>
|
<DialogTrigger className="" asChild>
|
||||||
@@ -88,140 +106,117 @@ export const AddDestination = () => {
|
|||||||
</DialogDescription>
|
</DialogDescription>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
{isError && <AlertBlock type="error">{error?.message}</AlertBlock>}
|
{isError && <AlertBlock type="error">{error?.message}</AlertBlock>}
|
||||||
|
|
||||||
<Form {...form}>
|
<Form {...form}>
|
||||||
<form
|
<form
|
||||||
id="hook-form-destination-add"
|
id="hook-form-destination-add"
|
||||||
onSubmit={form.handleSubmit(onSubmit)}
|
onSubmit={form.handleSubmit(onSubmit)}
|
||||||
className="grid w-full gap-4 "
|
className="grid w-full gap-8 "
|
||||||
>
|
>
|
||||||
<FormField
|
<div className="flex flex-col gap-2">
|
||||||
control={form.control}
|
{fields.map((input) => (
|
||||||
name="name"
|
<FormField
|
||||||
render={({ field }) => {
|
control={control}
|
||||||
return (
|
key={`${provider}.${input}`}
|
||||||
<FormItem>
|
name={`${provider}.${input}`}
|
||||||
<FormLabel>Name</FormLabel>
|
render={({ field }) => {
|
||||||
<FormControl>
|
return (
|
||||||
<Input placeholder={"S3 Bucket"} {...field} />
|
<FormItem>
|
||||||
</FormControl>
|
<FormLabel>{capitalize(input)}</FormLabel>
|
||||||
<FormMessage />
|
<FormControl>
|
||||||
</FormItem>
|
<Input placeholder={"Value"} {...field} />
|
||||||
);
|
</FormControl>
|
||||||
}}
|
<span className="text-sm font-medium text-destructive">
|
||||||
/>
|
{errors[input]?.message}
|
||||||
|
</span>
|
||||||
<FormField
|
</FormItem>
|
||||||
control={form.control}
|
);
|
||||||
name="accessKeyId"
|
}}
|
||||||
render={({ field }) => {
|
/>
|
||||||
return (
|
))}
|
||||||
<FormItem>
|
</div>
|
||||||
<FormLabel>Access Key Id</FormLabel>
|
|
||||||
<FormControl>
|
|
||||||
<Input placeholder={"xcas41dasde"} {...field} />
|
|
||||||
</FormControl>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="secretAccessKey"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem>
|
|
||||||
<div className="space-y-0.5">
|
|
||||||
<FormLabel>Secret Access Key</FormLabel>
|
|
||||||
</div>
|
|
||||||
<FormControl>
|
|
||||||
<Input placeholder={"asd123asdasw"} {...field} />
|
|
||||||
</FormControl>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="bucket"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem>
|
|
||||||
<div className="space-y-0.5">
|
|
||||||
<FormLabel>Bucket</FormLabel>
|
|
||||||
</div>
|
|
||||||
<FormControl>
|
|
||||||
<Input placeholder={"dokploy-bucket"} {...field} />
|
|
||||||
</FormControl>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="region"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem>
|
|
||||||
<div className="space-y-0.5">
|
|
||||||
<FormLabel>Region</FormLabel>
|
|
||||||
</div>
|
|
||||||
<FormControl>
|
|
||||||
<Input placeholder={"us-east-1"} {...field} />
|
|
||||||
</FormControl>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="endpoint"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem>
|
|
||||||
<FormLabel>Endpoint</FormLabel>
|
|
||||||
<FormControl>
|
|
||||||
<Input
|
|
||||||
placeholder={"https://us.bucket.aws/s3"}
|
|
||||||
{...field}
|
|
||||||
/>
|
|
||||||
</FormControl>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<DialogFooter className="flex w-full flex-row !justify-between pt-3">
|
|
||||||
<Button
|
|
||||||
isLoading={isLoadingConnection}
|
|
||||||
type="button"
|
|
||||||
variant="secondary"
|
|
||||||
onClick={async () => {
|
|
||||||
await testConnection({
|
|
||||||
accessKey: form.getValues("accessKeyId"),
|
|
||||||
bucket: form.getValues("bucket"),
|
|
||||||
endpoint: form.getValues("endpoint"),
|
|
||||||
name: "Test",
|
|
||||||
region: form.getValues("region"),
|
|
||||||
secretAccessKey: form.getValues("secretAccessKey"),
|
|
||||||
})
|
|
||||||
.then(async () => {
|
|
||||||
toast.success("Connection Success");
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
toast.error("Error to connect the provider");
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Test connection
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
isLoading={isLoading}
|
|
||||||
form="hook-form-destination-add"
|
|
||||||
type="submit"
|
|
||||||
>
|
|
||||||
Create
|
|
||||||
</Button>
|
|
||||||
</DialogFooter>
|
|
||||||
</Form>
|
</Form>
|
||||||
|
<Select
|
||||||
|
onValueChange={(e) => {
|
||||||
|
setProviders(e as keyof typeof providerSchemas);
|
||||||
|
}}
|
||||||
|
value={provider}
|
||||||
|
>
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue placeholder="Select a provider" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectGroup>
|
||||||
|
{Object.keys(providerSchemas).map((registry) => (
|
||||||
|
<SelectItem key={registry} value={registry}>
|
||||||
|
{registry}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
<SelectLabel>Providers ({providersData?.length})</SelectLabel>
|
||||||
|
</SelectGroup>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
<DialogFooter className="flex w-full flex-row !justify-between pt-3">
|
||||||
|
<Button
|
||||||
|
isLoading={isLoadingConnection}
|
||||||
|
type="button"
|
||||||
|
variant="secondary"
|
||||||
|
onClick={async () => {
|
||||||
|
const result = form.getValues()[provider];
|
||||||
|
const hola = mergeFormValues(schema, result);
|
||||||
|
console.log(hola);
|
||||||
|
|
||||||
|
// const getPropertiesByForm = (form: any) => {
|
||||||
|
// const initialValues = getInitialValues(schema);
|
||||||
|
// console.log(form, initialValues);
|
||||||
|
// const properties: any = {};
|
||||||
|
// for (const key in form) {
|
||||||
|
// const keysMatch = Object.keys(initialValues).filter(
|
||||||
|
// (k) => k === key,
|
||||||
|
// );
|
||||||
|
// if (keysMatch.length === 0) {
|
||||||
|
// continue;
|
||||||
|
// }
|
||||||
|
|
||||||
|
// properties[keysMatch[0]] = form[key] || "";
|
||||||
|
// console.log(key);
|
||||||
|
// }
|
||||||
|
// return properties;
|
||||||
|
// };
|
||||||
|
// const result = form.getValues();
|
||||||
|
// const properties = getPropertiesByForm(result);
|
||||||
|
// console.log(properties);
|
||||||
|
await testConnection({
|
||||||
|
json: {
|
||||||
|
...hola,
|
||||||
|
provider: provider,
|
||||||
|
},
|
||||||
|
// accessKey: form.getValues("accessKeyId"),
|
||||||
|
// bucket: form.getValues("bucket"),
|
||||||
|
// endpoint: form.getValues("endpoint"),
|
||||||
|
// name: "Test",
|
||||||
|
// region: form.getValues("region"),
|
||||||
|
// secretAccessKey: form.getValues("secretAccessKey"),
|
||||||
|
})
|
||||||
|
.then(async () => {
|
||||||
|
toast.success("Connection Success");
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
toast.error("Error to connect the provider");
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Test connection
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
// isLoading={isLoading}
|
||||||
|
form="hook-form-destination-add"
|
||||||
|
type="submit"
|
||||||
|
>
|
||||||
|
Create
|
||||||
|
</Button>
|
||||||
|
{/* */}
|
||||||
|
</DialogFooter>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
);
|
);
|
||||||
|
|||||||
1
apps/dokploy/drizzle/0038_familiar_shockwave.sql
Normal file
1
apps/dokploy/drizzle/0038_familiar_shockwave.sql
Normal file
@@ -0,0 +1 @@
|
|||||||
|
ALTER TABLE "destination" ADD COLUMN "schema" json;
|
||||||
3829
apps/dokploy/drizzle/meta/0038_snapshot.json
Normal file
3829
apps/dokploy/drizzle/meta/0038_snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -267,6 +267,13 @@
|
|||||||
"when": 1726988289562,
|
"when": 1726988289562,
|
||||||
"tag": "0037_legal_namor",
|
"tag": "0037_legal_namor",
|
||||||
"breakpoints": true
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 38,
|
||||||
|
"version": "6",
|
||||||
|
"when": 1727036227151,
|
||||||
|
"tag": "0038_familiar_shockwave",
|
||||||
|
"breakpoints": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -38,29 +38,31 @@ export const destinationRouter = createTRPCRouter({
|
|||||||
testConnection: adminProcedure
|
testConnection: adminProcedure
|
||||||
.input(apiCreateDestination)
|
.input(apiCreateDestination)
|
||||||
.mutation(async ({ input }) => {
|
.mutation(async ({ input }) => {
|
||||||
const { secretAccessKey, bucket, region, endpoint, accessKey } = input;
|
console.log(input);
|
||||||
|
// const { secretAccessKey, bucket, region, endpoint, accessKey } = input;
|
||||||
try {
|
// try {
|
||||||
const rcloneFlags = [
|
// const rcloneFlags = [
|
||||||
// `--s3-provider=Cloudflare`,
|
// // `--s3-provider=Cloudflare`,
|
||||||
`--s3-access-key-id=${accessKey}`,
|
// `--s3-access-key-id=${accessKey}`,
|
||||||
`--s3-secret-access-key=${secretAccessKey}`,
|
// `--s3-secret-access-key=${secretAccessKey}`,
|
||||||
`--s3-region=${region}`,
|
// `--s3-region=${region}`,
|
||||||
`--s3-endpoint=${endpoint}`,
|
// `--s3-endpoint=${endpoint}`,
|
||||||
"--s3-no-check-bucket",
|
// "--s3-no-check-bucket",
|
||||||
"--s3-force-path-style",
|
// "--s3-force-path-style",
|
||||||
];
|
// ];
|
||||||
const rcloneDestination = `:s3:${bucket}`;
|
const connextion = buildRcloneCommand(input.json.provider, input.json);
|
||||||
const rcloneCommand = `rclone ls ${rcloneFlags.join(" ")} "${rcloneDestination}"`;
|
console.log(connextion);
|
||||||
await execAsync(rcloneCommand);
|
// const rcloneDestination = `:s3:${bucket}`;
|
||||||
} catch (error) {
|
// const rcloneCommand = `rclone ls ${rcloneFlags.join(" ")} "${rcloneDestination}"`;
|
||||||
console.log(error);
|
await execAsync(connextion);
|
||||||
throw new TRPCError({
|
// } catch (error) {
|
||||||
code: "BAD_REQUEST",
|
// console.log(error);
|
||||||
message: "Error to connect to bucket",
|
// throw new TRPCError({
|
||||||
cause: error,
|
// code: "BAD_REQUEST",
|
||||||
});
|
// message: "Error to connect to bucket",
|
||||||
}
|
// cause: error,
|
||||||
|
// });
|
||||||
|
// }
|
||||||
}),
|
}),
|
||||||
one: protectedProcedure
|
one: protectedProcedure
|
||||||
.input(apiFindOneDestination)
|
.input(apiFindOneDestination)
|
||||||
@@ -97,3 +99,151 @@ export const destinationRouter = createTRPCRouter({
|
|||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
function buildRcloneCommand(
|
||||||
|
providerType: string,
|
||||||
|
credentials: Record<string, string>,
|
||||||
|
): string {
|
||||||
|
let rcloneFlags: string[] = [];
|
||||||
|
let rcloneDestination = "";
|
||||||
|
let rcloneCommand = "";
|
||||||
|
|
||||||
|
switch (providerType) {
|
||||||
|
case "s3":
|
||||||
|
{
|
||||||
|
const {
|
||||||
|
accessKey,
|
||||||
|
secretAccessKey,
|
||||||
|
region,
|
||||||
|
endpoint,
|
||||||
|
bucket,
|
||||||
|
provider,
|
||||||
|
storageClass,
|
||||||
|
acl,
|
||||||
|
} = credentials;
|
||||||
|
|
||||||
|
if (!accessKey || !secretAccessKey || !region || !endpoint || !bucket) {
|
||||||
|
throw new Error("Missing required S3 credentials.");
|
||||||
|
}
|
||||||
|
|
||||||
|
rcloneFlags.push(`--s3-access-key-id=${accessKey}`);
|
||||||
|
rcloneFlags.push(`--s3-secret-access-key=${secretAccessKey}`);
|
||||||
|
rcloneFlags.push(`--s3-region=${region}`);
|
||||||
|
rcloneFlags.push(`--s3-endpoint=${endpoint}`);
|
||||||
|
rcloneFlags.push("--s3-no-check-bucket");
|
||||||
|
rcloneFlags.push("--s3-force-path-style");
|
||||||
|
|
||||||
|
if (provider && provider !== "AWS") {
|
||||||
|
rcloneFlags.push(`--s3-provider=${provider}`);
|
||||||
|
}
|
||||||
|
if (storageClass) {
|
||||||
|
rcloneFlags.push(`--s3-storage-class=${storageClass}`);
|
||||||
|
}
|
||||||
|
if (acl) {
|
||||||
|
rcloneFlags.push(`--s3-acl=${acl}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
rcloneDestination = `:s3:${bucket}`;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "azureblob":
|
||||||
|
{
|
||||||
|
const { account, key, endpoint, container } = credentials;
|
||||||
|
|
||||||
|
if (!account || !key || !container) {
|
||||||
|
throw new Error("Missing required Azure Blob Storage credentials.");
|
||||||
|
}
|
||||||
|
|
||||||
|
rcloneFlags.push(`--azureblob-account=${account}`);
|
||||||
|
rcloneFlags.push(`--azureblob-key=${key}`);
|
||||||
|
if (endpoint) {
|
||||||
|
rcloneFlags.push(`--azureblob-endpoint=${endpoint}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
rcloneDestination = `:azureblob:${container}`;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "ftp":
|
||||||
|
{
|
||||||
|
const { host, port, user, pass, secure, path } = credentials;
|
||||||
|
|
||||||
|
if (!host || !user || !pass) {
|
||||||
|
throw new Error("Missing required FTP credentials.");
|
||||||
|
}
|
||||||
|
|
||||||
|
rcloneFlags.push(`--ftp-host=${host}`);
|
||||||
|
rcloneFlags.push(`--ftp-user=${user}`);
|
||||||
|
rcloneFlags.push(`--ftp-pass=${pass}`);
|
||||||
|
if (port) {
|
||||||
|
rcloneFlags.push(`--ftp-port=${port}`);
|
||||||
|
}
|
||||||
|
if (secure === "true" || secure === "1") {
|
||||||
|
rcloneFlags.push("--ftp-tls");
|
||||||
|
}
|
||||||
|
|
||||||
|
rcloneDestination = `:ftp:${path || "/"}`;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "gcs":
|
||||||
|
{
|
||||||
|
const {
|
||||||
|
serviceAccountFile,
|
||||||
|
clientId,
|
||||||
|
clientSecret,
|
||||||
|
projectNumber,
|
||||||
|
bucket,
|
||||||
|
objectAcl,
|
||||||
|
bucketAcl,
|
||||||
|
} = credentials;
|
||||||
|
|
||||||
|
if (serviceAccountFile) {
|
||||||
|
rcloneFlags.push(`--gcs-service-account-file=${serviceAccountFile}`);
|
||||||
|
} else if (clientId && clientSecret && projectNumber) {
|
||||||
|
rcloneFlags.push(`--gcs-client-id=${clientId}`);
|
||||||
|
rcloneFlags.push(`--gcs-client-secret=${clientSecret}`);
|
||||||
|
rcloneFlags.push(`--gcs-project-number=${projectNumber}`);
|
||||||
|
} else {
|
||||||
|
throw new Error(
|
||||||
|
"Missing required GCS credentials. Provide either serviceAccountFile or clientId, clientSecret, and projectNumber.",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!bucket) {
|
||||||
|
throw new Error("Bucket name is required for GCS.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (objectAcl) {
|
||||||
|
rcloneFlags.push(`--gcs-object-acl=${objectAcl}`);
|
||||||
|
}
|
||||||
|
if (bucketAcl) {
|
||||||
|
rcloneFlags.push(`--gcs-bucket-acl=${bucketAcl}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
rcloneDestination = `:gcs:${bucket}`;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "dropbox":
|
||||||
|
{
|
||||||
|
const { token, path } = credentials;
|
||||||
|
|
||||||
|
if (!token) {
|
||||||
|
throw new Error("Access token is required for Dropbox.");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Warning: Passing tokens via command line can be insecure.
|
||||||
|
rcloneFlags.push(`--dropbox-token='{"access_token":"${token}"}'`);
|
||||||
|
rcloneDestination = `:dropbox:${path || "/"}`;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
throw new Error(`Unsupported provider type: ${providerType}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Assemble the Rclone command
|
||||||
|
rcloneCommand = `rclone ls ${rcloneFlags.join(" ")} "${rcloneDestination}"`;
|
||||||
|
return rcloneCommand;
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { relations } from "drizzle-orm";
|
import { relations } from "drizzle-orm";
|
||||||
import { pgTable, text } from "drizzle-orm/pg-core";
|
import { json, pgTable, text } from "drizzle-orm/pg-core";
|
||||||
import { createInsertSchema } from "drizzle-zod";
|
import { createInsertSchema } from "drizzle-zod";
|
||||||
import { nanoid } from "nanoid";
|
import { nanoid } from "nanoid";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
@@ -18,6 +18,7 @@ export const destinations = pgTable("destination", {
|
|||||||
region: text("region").notNull(),
|
region: text("region").notNull(),
|
||||||
// maybe it can be null
|
// maybe it can be null
|
||||||
endpoint: text("endpoint").notNull(),
|
endpoint: text("endpoint").notNull(),
|
||||||
|
schema: json("schema"),
|
||||||
adminId: text("adminId")
|
adminId: text("adminId")
|
||||||
.notNull()
|
.notNull()
|
||||||
.references(() => admins.adminId, { onDelete: "cascade" }),
|
.references(() => admins.adminId, { onDelete: "cascade" }),
|
||||||
@@ -46,12 +47,15 @@ const createSchema = createInsertSchema(destinations, {
|
|||||||
|
|
||||||
export const apiCreateDestination = createSchema
|
export const apiCreateDestination = createSchema
|
||||||
.pick({
|
.pick({
|
||||||
name: true,
|
// name: true,
|
||||||
accessKey: true,
|
// accessKey: true,
|
||||||
bucket: true,
|
// bucket: true,
|
||||||
region: true,
|
// region: true,
|
||||||
endpoint: true,
|
// endpoint: true,
|
||||||
secretAccessKey: true,
|
// secretAccessKey: true,
|
||||||
|
})
|
||||||
|
.extend({
|
||||||
|
json: z.any(),
|
||||||
})
|
})
|
||||||
.required();
|
.required();
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user