mirror of
https://github.com/Dokploy/dokploy.git
synced 2026-10-19 23:21:36 +00:00
fix(ai): mask apiKey exposure, add audit logging, resolve masked-key UX regression
This commit is contained in:
@@ -148,6 +148,7 @@ export const HandleAi = ({ aiId }: Props) => {
|
||||
{
|
||||
apiUrl: apiUrl ?? "",
|
||||
apiKey: apiKey ?? "",
|
||||
...(aiId ? { aiId } : {}),
|
||||
},
|
||||
{
|
||||
enabled: !!apiUrl && (isLocalOllama || !!apiKey),
|
||||
@@ -482,6 +483,7 @@ export const HandleAi = ({ aiId }: Props) => {
|
||||
apiUrl={apiUrl}
|
||||
apiKey={apiKey}
|
||||
model={form.watch("model")}
|
||||
aiId={aiId}
|
||||
/>
|
||||
<Button type="submit" isLoading={isPending}>
|
||||
{aiId ? "Update" : "Create"}
|
||||
@@ -498,10 +500,12 @@ function TestConnectionButton({
|
||||
apiUrl,
|
||||
apiKey,
|
||||
model,
|
||||
aiId,
|
||||
}: {
|
||||
apiUrl: string;
|
||||
apiKey: string;
|
||||
model: string;
|
||||
aiId?: string;
|
||||
}) {
|
||||
const { mutate, isPending } = api.ai.testConnection.useMutation({
|
||||
onSuccess: () => {
|
||||
@@ -521,7 +525,9 @@ function TestConnectionButton({
|
||||
type="button"
|
||||
variant="outline"
|
||||
disabled={isDisabled || isPending}
|
||||
onClick={() => mutate({ apiUrl, apiKey, model })}
|
||||
onClick={() =>
|
||||
mutate({ apiUrl, apiKey, model, ...(aiId ? { aiId } : {}) })
|
||||
}
|
||||
>
|
||||
{isPending ? (
|
||||
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
||||
|
||||
@@ -17,6 +17,7 @@ import {
|
||||
getCustomAiProviders,
|
||||
maskAiApiKey,
|
||||
mergeAiApiKey,
|
||||
resolveAiApiKey,
|
||||
saveAiSettings,
|
||||
saveCustomAiProviders,
|
||||
suggestVariants,
|
||||
@@ -58,11 +59,23 @@ export const aiRouter = createTRPCRouter({
|
||||
}),
|
||||
|
||||
getModels: withAnyPermission("ai", ["read", "create", "update"])
|
||||
.input(z.object({ apiUrl: z.string().min(1), apiKey: z.string() }))
|
||||
.query(async ({ input }) => {
|
||||
.input(
|
||||
z.object({
|
||||
apiUrl: z.string().min(1),
|
||||
apiKey: z.string(),
|
||||
aiId: z.string().optional(),
|
||||
}),
|
||||
)
|
||||
.query(async ({ input, ctx }) => {
|
||||
try {
|
||||
const apiKey = await resolveAiApiKey(
|
||||
input.apiKey,
|
||||
input.apiUrl,
|
||||
input.aiId,
|
||||
ctx.session.activeOrganizationId,
|
||||
);
|
||||
const providerName = getProviderName(input.apiUrl);
|
||||
const headers = getProviderHeaders(input.apiUrl, input.apiKey);
|
||||
const headers = getProviderHeaders(input.apiUrl, apiKey);
|
||||
let response = null;
|
||||
switch (providerName) {
|
||||
case "ollama":
|
||||
@@ -70,7 +83,7 @@ export const aiRouter = createTRPCRouter({
|
||||
break;
|
||||
case "gemini":
|
||||
response = await fetch(
|
||||
`${input.apiUrl}/models?key=${encodeURIComponent(input.apiKey)}`,
|
||||
`${input.apiUrl}/models?key=${encodeURIComponent(apiKey)}`,
|
||||
{ headers: {} },
|
||||
);
|
||||
break;
|
||||
@@ -133,7 +146,7 @@ export const aiRouter = createTRPCRouter({
|
||||
},
|
||||
] as Model[];
|
||||
default:
|
||||
if (!input.apiKey)
|
||||
if (!apiKey)
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: "API key must contain at least 1 character(s)",
|
||||
@@ -335,13 +348,20 @@ ${input.logs}`,
|
||||
apiUrl: z.string().min(1),
|
||||
apiKey: z.string(),
|
||||
model: z.string().min(1),
|
||||
aiId: z.string().optional(),
|
||||
}),
|
||||
)
|
||||
.mutation(async ({ input }) => {
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
try {
|
||||
const apiKey = await resolveAiApiKey(
|
||||
input.apiKey,
|
||||
input.apiUrl,
|
||||
input.aiId,
|
||||
ctx.session.activeOrganizationId,
|
||||
);
|
||||
const provider = selectAIProvider({
|
||||
apiUrl: input.apiUrl,
|
||||
apiKey: input.apiKey,
|
||||
apiKey,
|
||||
});
|
||||
const model = provider(input.model);
|
||||
const result = await generateText({
|
||||
|
||||
@@ -108,7 +108,24 @@ export const saveCustomAiProviders = async (
|
||||
return providers;
|
||||
};
|
||||
|
||||
const normalizeApiUrl = (url: string) => url.trim().replace(/\/+$/, "");
|
||||
export const normalizeApiUrl = (url: string) => url.trim().replace(/\/+$/, "");
|
||||
|
||||
export const resolveAiApiKey = async (
|
||||
apiKey: string,
|
||||
apiUrl: string,
|
||||
aiId: string | undefined,
|
||||
organizationId: string,
|
||||
): Promise<string> => {
|
||||
if (apiKey !== AI_SECRET_MASK || !aiId) return apiKey;
|
||||
const stored = await getAiSettingById(aiId, organizationId);
|
||||
if (normalizeApiUrl(stored.apiUrl) !== normalizeApiUrl(apiUrl)) {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: "API URL does not match the stored configuration",
|
||||
});
|
||||
}
|
||||
return stored.apiKey;
|
||||
};
|
||||
|
||||
export const saveAiSettings = async (organizationId: string, settings: any) => {
|
||||
const aiId = settings.aiId;
|
||||
|
||||
Reference in New Issue
Block a user