mirror of
https://github.com/Dokploy/dokploy.git
synced 2026-07-21 22:05:23 +02:00
Compare commits
16 Commits
fix/collap
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a49615038f | ||
|
|
8b868c66d6 | ||
|
|
366e44b75a | ||
|
|
cb2db0d30a | ||
|
|
7ba3853bab | ||
|
|
8def9e933e | ||
|
|
e9b51667e2 | ||
|
|
25370cac30 | ||
|
|
52c7db1f66 | ||
|
|
6d65a36aac | ||
|
|
cbec72ed80 | ||
|
|
15fe3b21c9 | ||
|
|
a376b00a71 | ||
|
|
1ca03d3dc2 | ||
|
|
1da4db2905 | ||
|
|
b3feeed3e4 |
@@ -84,19 +84,19 @@ export const ComposeActions = ({ composeId }: Props) => {
|
|||||||
)}
|
)}
|
||||||
{canDeploy && (
|
{canDeploy && (
|
||||||
<DialogAction
|
<DialogAction
|
||||||
title="Reload Compose"
|
title="Rebuild Compose"
|
||||||
description="Are you sure you want to reload this compose?"
|
description="Are you sure you want to rebuild this compose?"
|
||||||
type="default"
|
type="default"
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
await redeploy({
|
await redeploy({
|
||||||
composeId: composeId,
|
composeId: composeId,
|
||||||
})
|
})
|
||||||
.then(() => {
|
.then(() => {
|
||||||
toast.success("Compose reloaded successfully");
|
toast.success("Compose rebuilt successfully");
|
||||||
refetch();
|
refetch();
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
toast.error("Error reloading compose");
|
toast.error("Error rebuilding compose");
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@@ -109,12 +109,14 @@ 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" />
|
||||||
Reload
|
Rebuild
|
||||||
</div>
|
</div>
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipPrimitive.Portal>
|
<TooltipPrimitive.Portal>
|
||||||
<TooltipContent sideOffset={5} className="z-60">
|
<TooltipContent sideOffset={5} className="z-60">
|
||||||
<p>Reload the compose without rebuilding it</p>
|
<p>
|
||||||
|
Rebuilds the compose without downloading the source code
|
||||||
|
</p>
|
||||||
</TooltipContent>
|
</TooltipContent>
|
||||||
</TooltipPrimitive.Portal>
|
</TooltipPrimitive.Portal>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|||||||
@@ -13,10 +13,14 @@ 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">
|
||||||
@@ -30,7 +34,10 @@ 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">
|
||||||
|
{isOrgAdmin && <HandleAiProviders />}
|
||||||
{aiConfigs && aiConfigs?.length > 0 && <HandleAi />}
|
{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 ? (
|
||||||
|
|||||||
@@ -0,0 +1,158 @@
|
|||||||
|
"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,6 +99,11 @@ 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();
|
||||||
@@ -210,7 +215,9 @@ export const HandleAi = ({ aiId }: Props) => {
|
|||||||
<FormLabel>Provider</FormLabel>
|
<FormLabel>Provider</FormLabel>
|
||||||
<Select
|
<Select
|
||||||
onValueChange={(value) => {
|
onValueChange={(value) => {
|
||||||
const provider = AI_PROVIDERS.find((p) => p.apiUrl === value);
|
const provider = providerOptions.find(
|
||||||
|
(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);
|
||||||
@@ -222,15 +229,20 @@ export const HandleAi = ({ aiId }: Props) => {
|
|||||||
<SelectValue placeholder="Select a provider preset..." />
|
<SelectValue placeholder="Select a provider preset..." />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
{AI_PROVIDERS.map((provider) => (
|
{providerOptions.map((provider) => (
|
||||||
<SelectItem key={provider.apiUrl} value={provider.apiUrl}>
|
<SelectItem
|
||||||
|
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">
|
||||||
Quick-fill provider name and URL, or configure manually below
|
{hasCustomProviders
|
||||||
|
? "Select one of the providers defined by your organization"
|
||||||
|
: "Quick-fill provider name and URL, or configure manually below"}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -260,6 +272,7 @@ 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);
|
||||||
@@ -271,7 +284,9 @@ export const HandleAi = ({ aiId }: Props) => {
|
|||||||
/>
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormDescription>
|
<FormDescription>
|
||||||
The base URL for your AI provider's API
|
{hasCustomProviders
|
||||||
|
? "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_TWO_FACTOR_AUTHENTICATION") {
|
if (result.error.code === "INVALID_CODE") {
|
||||||
toast.error("Invalid verification code");
|
toast.error("Invalid verification code");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "dokploy",
|
"name": "dokploy",
|
||||||
"version": "v0.29.12",
|
"version": "v0.29.13",
|
||||||
"private": true,
|
"private": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
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";
|
||||||
@@ -13,7 +14,9 @@ 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";
|
||||||
@@ -200,6 +203,19 @@ 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,
|
||||||
|
|||||||
@@ -54,6 +54,15 @@ 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),
|
||||||
|
|||||||
@@ -419,7 +419,7 @@ const createBetterAuth = () =>
|
|||||||
enableMetadata: true,
|
enableMetadata: true,
|
||||||
references: "user",
|
references: "user",
|
||||||
}),
|
}),
|
||||||
sso(),
|
sso({ trustEmailVerified: true }),
|
||||||
scim({
|
scim({
|
||||||
beforeSCIMTokenGenerated: async ({ user }) => {
|
beforeSCIMTokenGenerated: async ({ user }) => {
|
||||||
const dbUser = await db.query.user.findFirst({
|
const dbUser = await db.query.user.findFirst({
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { db } from "@dokploy/server/db";
|
import { db } from "@dokploy/server/db";
|
||||||
import { ai } from "@dokploy/server/db/schema";
|
import { ai, organization } 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";
|
||||||
@@ -48,9 +49,74 @@ 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({
|
||||||
|
|||||||
@@ -430,7 +430,7 @@ export const createOrganizationUserWithCredentials = async ({
|
|||||||
.insert(user)
|
.insert(user)
|
||||||
.values({
|
.values({
|
||||||
email: normalizedEmail,
|
email: normalizedEmail,
|
||||||
emailVerified: false,
|
emailVerified: true,
|
||||||
updatedAt: now,
|
updatedAt: now,
|
||||||
})
|
})
|
||||||
.returning({
|
.returning({
|
||||||
|
|||||||
Reference in New Issue
Block a user