mirror of
https://github.com/Dokploy/dokploy.git
synced 2026-07-21 22:05:23 +02:00
Compare commits
2 Commits
canary
...
fix/cmdi-q
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fbf2ff73c2 | ||
|
|
6943a395fe |
@@ -84,19 +84,19 @@ export const ComposeActions = ({ composeId }: Props) => {
|
|||||||
)}
|
)}
|
||||||
{canDeploy && (
|
{canDeploy && (
|
||||||
<DialogAction
|
<DialogAction
|
||||||
title="Rebuild Compose"
|
title="Reload Compose"
|
||||||
description="Are you sure you want to rebuild this compose?"
|
description="Are you sure you want to reload this compose?"
|
||||||
type="default"
|
type="default"
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
await redeploy({
|
await redeploy({
|
||||||
composeId: composeId,
|
composeId: composeId,
|
||||||
})
|
})
|
||||||
.then(() => {
|
.then(() => {
|
||||||
toast.success("Compose rebuilt successfully");
|
toast.success("Compose reloaded successfully");
|
||||||
refetch();
|
refetch();
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
toast.error("Error rebuilding compose");
|
toast.error("Error reloading compose");
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@@ -109,14 +109,12 @@ export const ComposeActions = ({ composeId }: Props) => {
|
|||||||
<TooltipTrigger asChild>
|
<TooltipTrigger asChild>
|
||||||
<div className="flex items-center">
|
<div className="flex items-center">
|
||||||
<RefreshCcw className="size-4 mr-1" />
|
<RefreshCcw className="size-4 mr-1" />
|
||||||
Rebuild
|
Reload
|
||||||
</div>
|
</div>
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipPrimitive.Portal>
|
<TooltipPrimitive.Portal>
|
||||||
<TooltipContent sideOffset={5} className="z-60">
|
<TooltipContent sideOffset={5} className="z-60">
|
||||||
<p>
|
<p>Reload the compose without rebuilding it</p>
|
||||||
Rebuilds the compose without downloading the source code
|
|
||||||
</p>
|
|
||||||
</TooltipContent>
|
</TooltipContent>
|
||||||
</TooltipPrimitive.Portal>
|
</TooltipPrimitive.Portal>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|||||||
@@ -13,14 +13,10 @@ import {
|
|||||||
} from "@/components/ui/card";
|
} from "@/components/ui/card";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import { HandleAi } from "./handle-ai";
|
import { HandleAi } from "./handle-ai";
|
||||||
import { HandleAiProviders } from "./handle-ai-providers";
|
|
||||||
|
|
||||||
export const AiForm = () => {
|
export const AiForm = () => {
|
||||||
const { data: aiConfigs, refetch, isPending } = api.ai.getAll.useQuery();
|
const { data: aiConfigs, refetch, isPending } = api.ai.getAll.useQuery();
|
||||||
const { mutateAsync, isPending: isRemoving } = api.ai.delete.useMutation();
|
const { mutateAsync, isPending: isRemoving } = api.ai.delete.useMutation();
|
||||||
const { data: currentUser } = api.user.get.useQuery();
|
|
||||||
const isOrgAdmin =
|
|
||||||
currentUser?.role === "owner" || currentUser?.role === "admin";
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="w-full">
|
<div className="w-full">
|
||||||
@@ -34,10 +30,7 @@ export const AiForm = () => {
|
|||||||
</CardTitle>
|
</CardTitle>
|
||||||
<CardDescription>Manage your AI configurations</CardDescription>
|
<CardDescription>Manage your AI configurations</CardDescription>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-row gap-2">
|
{aiConfigs && aiConfigs?.length > 0 && <HandleAi />}
|
||||||
{isOrgAdmin && <HandleAiProviders />}
|
|
||||||
{aiConfigs && aiConfigs?.length > 0 && <HandleAi />}
|
|
||||||
</div>
|
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="space-y-2 py-8 border-t">
|
<CardContent className="space-y-2 py-8 border-t">
|
||||||
{isPending ? (
|
{isPending ? (
|
||||||
|
|||||||
@@ -1,158 +0,0 @@
|
|||||||
"use client";
|
|
||||||
import { standardSchemaResolver as zodResolver } from "@hookform/resolvers/standard-schema";
|
|
||||||
import { PlusIcon, ServerIcon, Trash2 } from "lucide-react";
|
|
||||||
import { useEffect, useState } from "react";
|
|
||||||
import { useFieldArray, useForm } from "react-hook-form";
|
|
||||||
import { toast } from "sonner";
|
|
||||||
import { z } from "zod";
|
|
||||||
import { Button } from "@/components/ui/button";
|
|
||||||
import {
|
|
||||||
Dialog,
|
|
||||||
DialogContent,
|
|
||||||
DialogDescription,
|
|
||||||
DialogHeader,
|
|
||||||
DialogTitle,
|
|
||||||
DialogTrigger,
|
|
||||||
} from "@/components/ui/dialog";
|
|
||||||
import {
|
|
||||||
Form,
|
|
||||||
FormControl,
|
|
||||||
FormField,
|
|
||||||
FormItem,
|
|
||||||
FormMessage,
|
|
||||||
} from "@/components/ui/form";
|
|
||||||
import { Input } from "@/components/ui/input";
|
|
||||||
import { api } from "@/utils/api";
|
|
||||||
|
|
||||||
const Schema = z.object({
|
|
||||||
providers: z.array(
|
|
||||||
z.object({
|
|
||||||
name: z.string().min(1, { message: "Name is required" }),
|
|
||||||
apiUrl: z.string().url({ message: "Please enter a valid URL" }),
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
});
|
|
||||||
|
|
||||||
type Schema = z.infer<typeof Schema>;
|
|
||||||
|
|
||||||
export const HandleAiProviders = () => {
|
|
||||||
const utils = api.useUtils();
|
|
||||||
const [open, setOpen] = useState(false);
|
|
||||||
const { data: providers } = api.ai.getCustomProviders.useQuery();
|
|
||||||
const { mutateAsync, isPending } = api.ai.saveCustomProviders.useMutation();
|
|
||||||
|
|
||||||
const form = useForm<Schema>({
|
|
||||||
resolver: zodResolver(Schema),
|
|
||||||
defaultValues: {
|
|
||||||
providers: [],
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const { fields, append, remove } = useFieldArray({
|
|
||||||
control: form.control,
|
|
||||||
name: "providers",
|
|
||||||
});
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (open) {
|
|
||||||
form.reset({ providers: providers ?? [] });
|
|
||||||
}
|
|
||||||
}, [open, providers, form]);
|
|
||||||
|
|
||||||
const onSubmit = async (data: Schema) => {
|
|
||||||
try {
|
|
||||||
await mutateAsync({ providers: data.providers });
|
|
||||||
await utils.ai.getCustomProviders.invalidate();
|
|
||||||
toast.success("Custom providers saved successfully");
|
|
||||||
setOpen(false);
|
|
||||||
} catch (error) {
|
|
||||||
toast.error("Failed to save custom providers", {
|
|
||||||
description: error instanceof Error ? error.message : "Unknown error",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Dialog open={open} onOpenChange={setOpen}>
|
|
||||||
<DialogTrigger asChild>
|
|
||||||
<Button variant="outline" className="cursor-pointer space-x-3">
|
|
||||||
<ServerIcon className="h-4 w-4" />
|
|
||||||
Custom Presets
|
|
||||||
</Button>
|
|
||||||
</DialogTrigger>
|
|
||||||
<DialogContent className="sm:max-w-xl">
|
|
||||||
<DialogHeader>
|
|
||||||
<DialogTitle>Custom AI Providers</DialogTitle>
|
|
||||||
<DialogDescription>
|
|
||||||
Define your own AI providers, like an internal LLM platform. When at
|
|
||||||
least one is defined, only these providers can be used in AI
|
|
||||||
configurations.
|
|
||||||
</DialogDescription>
|
|
||||||
</DialogHeader>
|
|
||||||
<Form {...form}>
|
|
||||||
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
|
|
||||||
{fields.length === 0 && (
|
|
||||||
<p className="text-sm text-muted-foreground">
|
|
||||||
No custom providers defined. The built-in provider list will be
|
|
||||||
used.
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
{fields.map((fieldItem, index) => (
|
|
||||||
<div key={fieldItem.id} className="flex gap-2 items-start">
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name={`providers.${index}.name`}
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem className="flex-1">
|
|
||||||
<FormControl>
|
|
||||||
<Input placeholder="Internal LLM" {...field} />
|
|
||||||
</FormControl>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name={`providers.${index}.apiUrl`}
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem className="flex-[2]">
|
|
||||||
<FormControl>
|
|
||||||
<Input
|
|
||||||
placeholder="https://llm.internal.company/v1"
|
|
||||||
{...field}
|
|
||||||
/>
|
|
||||||
</FormControl>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
<Button
|
|
||||||
type="button"
|
|
||||||
variant="ghost"
|
|
||||||
size="icon"
|
|
||||||
className="group hover:bg-red-500/10"
|
|
||||||
onClick={() => remove(index)}
|
|
||||||
>
|
|
||||||
<Trash2 className="size-4 text-primary group-hover:text-red-500" />
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
<div className="flex justify-between">
|
|
||||||
<Button
|
|
||||||
type="button"
|
|
||||||
variant="outline"
|
|
||||||
onClick={() => append({ name: "", apiUrl: "" })}
|
|
||||||
>
|
|
||||||
<PlusIcon className="h-4 w-4" />
|
|
||||||
Add Provider
|
|
||||||
</Button>
|
|
||||||
<Button type="submit" isLoading={isPending}>
|
|
||||||
Save
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</Form>
|
|
||||||
</DialogContent>
|
|
||||||
</Dialog>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -99,11 +99,6 @@ export const HandleAi = ({ aiId }: Props) => {
|
|||||||
enabled: !!aiId,
|
enabled: !!aiId,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
const { data: customProviders } = api.ai.getCustomProviders.useQuery();
|
|
||||||
const hasCustomProviders = (customProviders?.length ?? 0) > 0;
|
|
||||||
const providerOptions: { name: string; apiUrl: string }[] = hasCustomProviders
|
|
||||||
? (customProviders ?? [])
|
|
||||||
: [...AI_PROVIDERS];
|
|
||||||
const { mutateAsync, isPending } = aiId
|
const { mutateAsync, isPending } = aiId
|
||||||
? api.ai.update.useMutation()
|
? api.ai.update.useMutation()
|
||||||
: api.ai.create.useMutation();
|
: api.ai.create.useMutation();
|
||||||
@@ -215,9 +210,7 @@ export const HandleAi = ({ aiId }: Props) => {
|
|||||||
<FormLabel>Provider</FormLabel>
|
<FormLabel>Provider</FormLabel>
|
||||||
<Select
|
<Select
|
||||||
onValueChange={(value) => {
|
onValueChange={(value) => {
|
||||||
const provider = providerOptions.find(
|
const provider = AI_PROVIDERS.find((p) => p.apiUrl === value);
|
||||||
(p) => p.apiUrl === value,
|
|
||||||
);
|
|
||||||
if (provider) {
|
if (provider) {
|
||||||
form.setValue("name", provider.name);
|
form.setValue("name", provider.name);
|
||||||
form.setValue("apiUrl", provider.apiUrl);
|
form.setValue("apiUrl", provider.apiUrl);
|
||||||
@@ -229,20 +222,15 @@ export const HandleAi = ({ aiId }: Props) => {
|
|||||||
<SelectValue placeholder="Select a provider preset..." />
|
<SelectValue placeholder="Select a provider preset..." />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
{providerOptions.map((provider) => (
|
{AI_PROVIDERS.map((provider) => (
|
||||||
<SelectItem
|
<SelectItem key={provider.apiUrl} value={provider.apiUrl}>
|
||||||
key={`${provider.name}-${provider.apiUrl}`}
|
|
||||||
value={provider.apiUrl}
|
|
||||||
>
|
|
||||||
{provider.name}
|
{provider.name}
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
))}
|
))}
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
<p className="text-[0.8rem] text-muted-foreground">
|
<p className="text-[0.8rem] text-muted-foreground">
|
||||||
{hasCustomProviders
|
Quick-fill provider name and URL, or configure manually below
|
||||||
? "Select one of the providers defined by your organization"
|
|
||||||
: "Quick-fill provider name and URL, or configure manually below"}
|
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -272,7 +260,6 @@ export const HandleAi = ({ aiId }: Props) => {
|
|||||||
<FormControl>
|
<FormControl>
|
||||||
<Input
|
<Input
|
||||||
placeholder="https://api.openai.com/v1"
|
placeholder="https://api.openai.com/v1"
|
||||||
disabled={hasCustomProviders}
|
|
||||||
{...field}
|
{...field}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
field.onChange(e);
|
field.onChange(e);
|
||||||
@@ -284,9 +271,7 @@ export const HandleAi = ({ aiId }: Props) => {
|
|||||||
/>
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormDescription>
|
<FormDescription>
|
||||||
{hasCustomProviders
|
The base URL for your AI provider's API
|
||||||
? "The API URL is defined by your organization's providers"
|
|
||||||
: "The base URL for your AI provider's API"}
|
|
||||||
</FormDescription>
|
</FormDescription>
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
|
|||||||
@@ -100,7 +100,7 @@ export const Enable2FA = () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (result.error) {
|
if (result.error) {
|
||||||
if (result.error.code === "INVALID_CODE") {
|
if (result.error.code === "INVALID_TWO_FACTOR_AUTHENTICATION") {
|
||||||
toast.error("Invalid verification code");
|
toast.error("Invalid verification code");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -648,7 +648,7 @@ function SidebarLogo() {
|
|||||||
</SidebarMenuButton>
|
</SidebarMenuButton>
|
||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
<DropdownMenuContent
|
<DropdownMenuContent
|
||||||
className="w-64 rounded-lg max-h-[min(70vh,28rem)] flex flex-col"
|
className="rounded-lg max-h-[min(70vh,28rem)] flex flex-col"
|
||||||
align="start"
|
align="start"
|
||||||
side={isMobile ? "bottom" : "right"}
|
side={isMobile ? "bottom" : "right"}
|
||||||
sideOffset={4}
|
sideOffset={4}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "dokploy",
|
"name": "dokploy",
|
||||||
"version": "v0.29.13",
|
"version": "v0.29.12",
|
||||||
"private": true,
|
"private": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import { IS_CLOUD } from "@dokploy/server/constants";
|
import { IS_CLOUD } from "@dokploy/server/constants";
|
||||||
import {
|
import {
|
||||||
apiCreateAi,
|
apiCreateAi,
|
||||||
apiSaveAiCustomProviders,
|
|
||||||
apiUpdateAi,
|
apiUpdateAi,
|
||||||
deploySuggestionSchema,
|
deploySuggestionSchema,
|
||||||
} from "@dokploy/server/db/schema/ai";
|
} from "@dokploy/server/db/schema/ai";
|
||||||
@@ -14,9 +13,7 @@ import {
|
|||||||
deleteAiSettings,
|
deleteAiSettings,
|
||||||
getAiSettingById,
|
getAiSettingById,
|
||||||
getAiSettingsByOrganizationId,
|
getAiSettingsByOrganizationId,
|
||||||
getCustomAiProviders,
|
|
||||||
saveAiSettings,
|
saveAiSettings,
|
||||||
saveCustomAiProviders,
|
|
||||||
suggestVariants,
|
suggestVariants,
|
||||||
} from "@dokploy/server/services/ai";
|
} from "@dokploy/server/services/ai";
|
||||||
import { createComposeByTemplate } from "@dokploy/server/services/compose";
|
import { createComposeByTemplate } from "@dokploy/server/services/compose";
|
||||||
@@ -203,19 +200,6 @@ export const aiRouter = createTRPCRouter({
|
|||||||
return await deleteAiSettings(input.aiId);
|
return await deleteAiSettings(input.aiId);
|
||||||
}),
|
}),
|
||||||
|
|
||||||
getCustomProviders: protectedProcedure.query(async ({ ctx }) => {
|
|
||||||
return await getCustomAiProviders(ctx.session.activeOrganizationId);
|
|
||||||
}),
|
|
||||||
|
|
||||||
saveCustomProviders: adminProcedure
|
|
||||||
.input(apiSaveAiCustomProviders)
|
|
||||||
.mutation(async ({ ctx, input }) => {
|
|
||||||
return await saveCustomAiProviders(
|
|
||||||
ctx.session.activeOrganizationId,
|
|
||||||
input.providers,
|
|
||||||
);
|
|
||||||
}),
|
|
||||||
|
|
||||||
getEnabledProviders: protectedProcedure.query(async ({ ctx }) => {
|
getEnabledProviders: protectedProcedure.query(async ({ ctx }) => {
|
||||||
const settings = await getAiSettingsByOrganizationId(
|
const settings = await getAiSettingsByOrganizationId(
|
||||||
ctx.session.activeOrganizationId,
|
ctx.session.activeOrganizationId,
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import {
|
|||||||
findRegistryById,
|
findRegistryById,
|
||||||
IS_CLOUD,
|
IS_CLOUD,
|
||||||
removeRegistry,
|
removeRegistry,
|
||||||
safeDockerLoginCommand,
|
|
||||||
updateRegistry,
|
updateRegistry,
|
||||||
} from "@dokploy/server";
|
} from "@dokploy/server";
|
||||||
import { db } from "@dokploy/server/db";
|
import { db } from "@dokploy/server/db";
|
||||||
@@ -123,11 +122,7 @@ export const registryRouter = createTRPCRouter({
|
|||||||
if (input.serverId && input.serverId !== "none") {
|
if (input.serverId && input.serverId !== "none") {
|
||||||
await execAsyncRemote(
|
await execAsyncRemote(
|
||||||
input.serverId,
|
input.serverId,
|
||||||
safeDockerLoginCommand(
|
`echo ${input.password} | docker ${args.join(" ")}`,
|
||||||
input.registryUrl,
|
|
||||||
input.username,
|
|
||||||
input.password,
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
await execFileAsync("docker", args, {
|
await execFileAsync("docker", args, {
|
||||||
@@ -187,11 +182,7 @@ export const registryRouter = createTRPCRouter({
|
|||||||
if (input.serverId && input.serverId !== "none") {
|
if (input.serverId && input.serverId !== "none") {
|
||||||
await execAsyncRemote(
|
await execAsyncRemote(
|
||||||
input.serverId,
|
input.serverId,
|
||||||
safeDockerLoginCommand(
|
`echo ${registryData.password} | docker ${args.join(" ")}`,
|
||||||
registryData.registryUrl,
|
|
||||||
registryData.username,
|
|
||||||
registryData.password,
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
await execFileAsync("docker", args, {
|
await execFileAsync("docker", args, {
|
||||||
|
|||||||
@@ -413,14 +413,6 @@ export const serverRouter = createTRPCRouter({
|
|||||||
.input(apiRemoveServer)
|
.input(apiRemoveServer)
|
||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
try {
|
try {
|
||||||
const currentServer = await findServerById(input.serverId);
|
|
||||||
if (currentServer.organizationId !== ctx.session.activeOrganizationId) {
|
|
||||||
throw new TRPCError({
|
|
||||||
code: "UNAUTHORIZED",
|
|
||||||
message: "You are not authorized to delete this server",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const activeServers = await haveActiveServices(input.serverId);
|
const activeServers = await haveActiveServices(input.serverId);
|
||||||
|
|
||||||
if (activeServers) {
|
if (activeServers) {
|
||||||
@@ -429,6 +421,7 @@ export const serverRouter = createTRPCRouter({
|
|||||||
message: "Server has active services, please delete them first",
|
message: "Server has active services, please delete them first",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
const currentServer = await findServerById(input.serverId);
|
||||||
await audit(ctx, {
|
await audit(ctx, {
|
||||||
action: "delete",
|
action: "delete",
|
||||||
resourceType: "server",
|
resourceType: "server",
|
||||||
|
|||||||
@@ -54,15 +54,6 @@ export const apiUpdateAi = createSchema
|
|||||||
})
|
})
|
||||||
.omit({ organizationId: true });
|
.omit({ organizationId: true });
|
||||||
|
|
||||||
export const aiCustomProviderSchema = z.object({
|
|
||||||
name: z.string().min(1, { message: "Name is required" }),
|
|
||||||
apiUrl: z.string().url({ message: "Please enter a valid URL" }),
|
|
||||||
});
|
|
||||||
|
|
||||||
export const apiSaveAiCustomProviders = z.object({
|
|
||||||
providers: z.array(aiCustomProviderSchema),
|
|
||||||
});
|
|
||||||
|
|
||||||
export const deploySuggestionSchema = z.object({
|
export const deploySuggestionSchema = z.object({
|
||||||
environmentId: z.string().min(1),
|
environmentId: z.string().min(1),
|
||||||
id: z.string().min(1),
|
id: z.string().min(1),
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ export const apiCreateCertificate = createInsertSchema(certificates, {
|
|||||||
privateKey: z.string().min(1),
|
privateKey: z.string().min(1),
|
||||||
autoRenew: z.boolean().optional(),
|
autoRenew: z.boolean().optional(),
|
||||||
serverId: z.string().optional(),
|
serverId: z.string().optional(),
|
||||||
});
|
}).omit({ certificatePath: true });
|
||||||
|
|
||||||
export const apiFindCertificate = z.object({
|
export const apiFindCertificate = z.object({
|
||||||
certificateId: z.string().min(1),
|
certificateId: z.string().min(1),
|
||||||
|
|||||||
@@ -81,7 +81,7 @@ export const schedulesRelations = relations(schedules, ({ one, many }) => ({
|
|||||||
|
|
||||||
export const createScheduleSchema = createInsertSchema(schedules, {
|
export const createScheduleSchema = createInsertSchema(schedules, {
|
||||||
scheduleType: z.enum(["application", "compose", "server", "dokploy-server"]),
|
scheduleType: z.enum(["application", "compose", "server", "dokploy-server"]),
|
||||||
});
|
}).omit({ appName: true });
|
||||||
|
|
||||||
export const updateScheduleSchema = createScheduleSchema.extend({
|
export const updateScheduleSchema = createScheduleSchema.extend({
|
||||||
scheduleId: z.string().min(1),
|
scheduleId: z.string().min(1),
|
||||||
|
|||||||
@@ -419,7 +419,7 @@ const createBetterAuth = () =>
|
|||||||
enableMetadata: true,
|
enableMetadata: true,
|
||||||
references: "user",
|
references: "user",
|
||||||
}),
|
}),
|
||||||
sso({ trustEmailVerified: true }),
|
sso(),
|
||||||
scim({
|
scim({
|
||||||
beforeSCIMTokenGenerated: async ({ user }) => {
|
beforeSCIMTokenGenerated: async ({ user }) => {
|
||||||
const dbUser = await db.query.user.findFirst({
|
const dbUser = await db.query.user.findFirst({
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import { db } from "@dokploy/server/db";
|
import { db } from "@dokploy/server/db";
|
||||||
import { ai, organization } from "@dokploy/server/db/schema";
|
import { ai } from "@dokploy/server/db/schema";
|
||||||
import { aiCustomProviderSchema } from "@dokploy/server/db/schema/ai";
|
|
||||||
import { selectAIProvider } from "@dokploy/server/utils/ai/select-ai-provider";
|
import { selectAIProvider } from "@dokploy/server/utils/ai/select-ai-provider";
|
||||||
import { TRPCError } from "@trpc/server";
|
import { TRPCError } from "@trpc/server";
|
||||||
import { generateText, Output } from "ai";
|
import { generateText, Output } from "ai";
|
||||||
@@ -49,74 +48,9 @@ export const getAiSettingById = async (aiId: string) => {
|
|||||||
return aiSetting;
|
return aiSetting;
|
||||||
};
|
};
|
||||||
|
|
||||||
type AiCustomProvider = z.infer<typeof aiCustomProviderSchema>;
|
|
||||||
|
|
||||||
const parseOrgMetadata = (metadata: string | null) => {
|
|
||||||
try {
|
|
||||||
const parsed = JSON.parse(metadata || "{}");
|
|
||||||
return typeof parsed === "object" && parsed !== null
|
|
||||||
? (parsed as Record<string, unknown>)
|
|
||||||
: {};
|
|
||||||
} catch {
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getCustomAiProviders = async (organizationId: string) => {
|
|
||||||
const org = await db.query.organization.findFirst({
|
|
||||||
where: eq(organization.id, organizationId),
|
|
||||||
});
|
|
||||||
const metadata = parseOrgMetadata(org?.metadata ?? null);
|
|
||||||
const result = z
|
|
||||||
.array(aiCustomProviderSchema)
|
|
||||||
.safeParse(metadata.aiProviders);
|
|
||||||
return result.success ? result.data : [];
|
|
||||||
};
|
|
||||||
|
|
||||||
export const saveCustomAiProviders = async (
|
|
||||||
organizationId: string,
|
|
||||||
providers: AiCustomProvider[],
|
|
||||||
) => {
|
|
||||||
const org = await db.query.organization.findFirst({
|
|
||||||
where: eq(organization.id, organizationId),
|
|
||||||
});
|
|
||||||
if (!org) {
|
|
||||||
throw new TRPCError({
|
|
||||||
code: "NOT_FOUND",
|
|
||||||
message: "Organization not found",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
const metadata = parseOrgMetadata(org.metadata);
|
|
||||||
metadata.aiProviders = providers;
|
|
||||||
await db
|
|
||||||
.update(organization)
|
|
||||||
.set({ metadata: JSON.stringify(metadata) })
|
|
||||||
.where(eq(organization.id, organizationId));
|
|
||||||
return providers;
|
|
||||||
};
|
|
||||||
|
|
||||||
const normalizeApiUrl = (url: string) => url.trim().replace(/\/+$/, "");
|
|
||||||
|
|
||||||
export const saveAiSettings = async (organizationId: string, settings: any) => {
|
export const saveAiSettings = async (organizationId: string, settings: any) => {
|
||||||
const aiId = settings.aiId;
|
const aiId = settings.aiId;
|
||||||
|
|
||||||
if (settings.apiUrl) {
|
|
||||||
const customProviders = await getCustomAiProviders(organizationId);
|
|
||||||
if (customProviders.length > 0) {
|
|
||||||
const isAllowed = customProviders.some(
|
|
||||||
(provider) =>
|
|
||||||
normalizeApiUrl(provider.apiUrl) === normalizeApiUrl(settings.apiUrl),
|
|
||||||
);
|
|
||||||
if (!isAllowed) {
|
|
||||||
throw new TRPCError({
|
|
||||||
code: "FORBIDDEN",
|
|
||||||
message:
|
|
||||||
"This API URL is not in your organization's allowed AI providers",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return db
|
return db
|
||||||
.insert(ai)
|
.insert(ai)
|
||||||
.values({
|
.values({
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
import { TRPCError } from "@trpc/server";
|
import { TRPCError } from "@trpc/server";
|
||||||
import { eq } from "drizzle-orm";
|
import { eq } from "drizzle-orm";
|
||||||
|
import { quote } from "shell-quote";
|
||||||
import type { z } from "zod";
|
import type { z } from "zod";
|
||||||
import { IS_CLOUD, paths } from "../constants";
|
import { IS_CLOUD, paths } from "../constants";
|
||||||
import { db } from "../db";
|
import { db } from "../db";
|
||||||
@@ -142,7 +143,7 @@ export const deleteSchedule = async (scheduleId: string) => {
|
|||||||
const { SCHEDULES_PATH } = paths(!!serverId);
|
const { SCHEDULES_PATH } = paths(!!serverId);
|
||||||
|
|
||||||
const fullPath = path.join(SCHEDULES_PATH, schedule?.appName || "");
|
const fullPath = path.join(SCHEDULES_PATH, schedule?.appName || "");
|
||||||
const command = `rm -rf ${fullPath}`;
|
const command = `rm -rf ${quote([fullPath])}`;
|
||||||
if (serverId) {
|
if (serverId) {
|
||||||
await execAsyncRemote(serverId, command);
|
await execAsyncRemote(serverId, command);
|
||||||
} else {
|
} else {
|
||||||
@@ -198,12 +199,13 @@ const handleScript = async (schedule: Schedule) => {
|
|||||||
${schedule?.script || ""}`;
|
${schedule?.script || ""}`;
|
||||||
|
|
||||||
const encodedContent = encodeBase64(scriptWithPid);
|
const encodedContent = encodeBase64(scriptWithPid);
|
||||||
|
const scriptPath = `${fullPath}/script.sh`;
|
||||||
const script = `
|
const script = `
|
||||||
mkdir -p ${fullPath}
|
mkdir -p ${quote([fullPath])}
|
||||||
rm -f ${fullPath}/script.sh
|
rm -f ${quote([scriptPath])}
|
||||||
touch ${fullPath}/script.sh
|
touch ${quote([scriptPath])}
|
||||||
chmod +x ${fullPath}/script.sh
|
chmod +x ${quote([scriptPath])}
|
||||||
echo "${encodedContent}" | base64 -d > ${fullPath}/script.sh
|
echo "${encodedContent}" | base64 -d > ${quote([scriptPath])}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
if (schedule?.scheduleType === "dokploy-server") {
|
if (schedule?.scheduleType === "dokploy-server") {
|
||||||
|
|||||||
@@ -430,7 +430,7 @@ export const createOrganizationUserWithCredentials = async ({
|
|||||||
.insert(user)
|
.insert(user)
|
||||||
.values({
|
.values({
|
||||||
email: normalizedEmail,
|
email: normalizedEmail,
|
||||||
emailVerified: true,
|
emailVerified: false,
|
||||||
updatedAt: now,
|
updatedAt: now,
|
||||||
})
|
})
|
||||||
.returning({
|
.returning({
|
||||||
|
|||||||
Reference in New Issue
Block a user