mirror of
https://github.com/Dokploy/dokploy.git
synced 2026-06-15 20:25: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.
254 lines
6.5 KiB
TypeScript
254 lines
6.5 KiB
TypeScript
import { IS_CLOUD } from "@dokploy/server/constants";
|
|
import {
|
|
apiCreateAi,
|
|
apiUpdateAi,
|
|
deploySuggestionSchema,
|
|
} from "@dokploy/server/db/schema/ai";
|
|
import {
|
|
createDomain,
|
|
createMount,
|
|
findEnvironmentById,
|
|
} from "@dokploy/server/index";
|
|
import {
|
|
deleteAiSettings,
|
|
getAiSettingById,
|
|
getAiSettingsByOrganizationId,
|
|
saveAiSettings,
|
|
suggestVariants,
|
|
} from "@dokploy/server/services/ai";
|
|
import { createComposeByTemplate } from "@dokploy/server/services/compose";
|
|
import { findProjectById } from "@dokploy/server/services/project";
|
|
import {
|
|
addNewService,
|
|
checkServiceAccess,
|
|
} from "@dokploy/server/services/permission";
|
|
import {
|
|
getProviderHeaders,
|
|
getProviderName,
|
|
type Model,
|
|
} from "@dokploy/server/utils/ai/select-ai-provider";
|
|
import { TRPCError } from "@trpc/server";
|
|
import { z } from "zod";
|
|
import { slugify } from "@/lib/slug";
|
|
import {
|
|
adminProcedure,
|
|
createTRPCRouter,
|
|
protectedProcedure,
|
|
} from "@/server/api/trpc";
|
|
import { generatePassword } from "@/templates/utils";
|
|
|
|
export const aiRouter = createTRPCRouter({
|
|
one: adminProcedure
|
|
.input(z.object({ aiId: z.string() }))
|
|
.query(async ({ input }) => {
|
|
return await getAiSettingById(input.aiId);
|
|
}),
|
|
|
|
getModels: protectedProcedure
|
|
.input(z.object({ apiUrl: z.string().min(1), apiKey: z.string() }))
|
|
.query(async ({ input }) => {
|
|
try {
|
|
const providerName = getProviderName(input.apiUrl);
|
|
const headers = getProviderHeaders(input.apiUrl, input.apiKey);
|
|
let response = null;
|
|
switch (providerName) {
|
|
case "ollama":
|
|
response = await fetch(`${input.apiUrl}/api/tags`, { headers });
|
|
break;
|
|
case "gemini":
|
|
response = await fetch(
|
|
`${input.apiUrl}/models?key=${encodeURIComponent(input.apiKey)}`,
|
|
{ headers: {} },
|
|
);
|
|
break;
|
|
case "perplexity":
|
|
// Perplexity doesn't have a /models endpoint, return hardcoded list
|
|
return [
|
|
{
|
|
id: "sonar-deep-research",
|
|
object: "model",
|
|
created: Date.now(),
|
|
owned_by: "perplexity",
|
|
},
|
|
{
|
|
id: "sonar-reasoning-pro",
|
|
object: "model",
|
|
created: Date.now(),
|
|
owned_by: "perplexity",
|
|
},
|
|
{
|
|
id: "sonar-reasoning",
|
|
object: "model",
|
|
created: Date.now(),
|
|
owned_by: "perplexity",
|
|
},
|
|
{
|
|
id: "sonar-pro",
|
|
object: "model",
|
|
created: Date.now(),
|
|
owned_by: "perplexity",
|
|
},
|
|
{
|
|
id: "sonar",
|
|
object: "model",
|
|
created: Date.now(),
|
|
owned_by: "perplexity",
|
|
},
|
|
] as Model[];
|
|
default:
|
|
if (!input.apiKey)
|
|
throw new TRPCError({
|
|
code: "BAD_REQUEST",
|
|
message: "API key must contain at least 1 character(s)",
|
|
});
|
|
response = await fetch(`${input.apiUrl}/models`, { headers });
|
|
}
|
|
|
|
if (!response.ok) {
|
|
const errorText = await response.text();
|
|
throw new Error(`Failed to fetch models: ${errorText}`);
|
|
}
|
|
|
|
const res = await response.json();
|
|
|
|
if (Array.isArray(res)) {
|
|
return res.map((model) => ({
|
|
id: model.id || model.name,
|
|
object: "model",
|
|
created: Date.now(),
|
|
owned_by: "provider",
|
|
}));
|
|
}
|
|
|
|
if (res.models) {
|
|
return res.models.map((model: any) => ({
|
|
id: model.id || model.name,
|
|
object: "model",
|
|
created: Date.now(),
|
|
owned_by: "provider",
|
|
})) as Model[];
|
|
}
|
|
|
|
if (res.data) {
|
|
return res.data as Model[];
|
|
}
|
|
|
|
const possibleModels =
|
|
(Object.values(res).find(Array.isArray) as any[]) || [];
|
|
return possibleModels.map((model) => ({
|
|
id: model.id || model.name,
|
|
object: "model",
|
|
created: Date.now(),
|
|
owned_by: "provider",
|
|
})) as Model[];
|
|
} catch (error) {
|
|
throw new TRPCError({
|
|
code: "BAD_REQUEST",
|
|
message: error instanceof Error ? error?.message : `Error: ${error}`,
|
|
});
|
|
}
|
|
}),
|
|
create: adminProcedure.input(apiCreateAi).mutation(async ({ ctx, input }) => {
|
|
return await saveAiSettings(ctx.session.activeOrganizationId, input);
|
|
}),
|
|
|
|
update: adminProcedure.input(apiUpdateAi).mutation(async ({ ctx, input }) => {
|
|
return await saveAiSettings(ctx.session.activeOrganizationId, input);
|
|
}),
|
|
|
|
getAll: adminProcedure.query(async ({ ctx }) => {
|
|
return await getAiSettingsByOrganizationId(
|
|
ctx.session.activeOrganizationId,
|
|
);
|
|
}),
|
|
|
|
get: adminProcedure
|
|
.input(z.object({ aiId: z.string() }))
|
|
.query(async ({ input }) => {
|
|
return await getAiSettingById(input.aiId);
|
|
}),
|
|
|
|
delete: adminProcedure
|
|
.input(z.object({ aiId: z.string() }))
|
|
.mutation(async ({ input }) => {
|
|
return await deleteAiSettings(input.aiId);
|
|
}),
|
|
|
|
suggest: protectedProcedure
|
|
.input(
|
|
z.object({
|
|
aiId: z.string(),
|
|
input: z.string(),
|
|
serverId: z.string().optional(),
|
|
}),
|
|
)
|
|
.mutation(async ({ ctx, input }) => {
|
|
try {
|
|
return await suggestVariants({
|
|
...input,
|
|
organizationId: ctx.session.activeOrganizationId,
|
|
});
|
|
} catch (error) {
|
|
throw new TRPCError({
|
|
code: "BAD_REQUEST",
|
|
message: error instanceof Error ? error?.message : `Error: ${error}`,
|
|
});
|
|
}
|
|
}),
|
|
deploy: protectedProcedure
|
|
.input(deploySuggestionSchema)
|
|
.mutation(async ({ ctx, input }) => {
|
|
const environment = await findEnvironmentById(input.environmentId);
|
|
const project = await findProjectById(environment.projectId);
|
|
await checkServiceAccess(ctx, environment.projectId, "create");
|
|
|
|
if (IS_CLOUD && !input.serverId) {
|
|
throw new TRPCError({
|
|
code: "UNAUTHORIZED",
|
|
message: "You need to use a server to create a compose",
|
|
});
|
|
}
|
|
|
|
const projectName = slugify(`${project.name} ${input.id}`);
|
|
|
|
const compose = await createComposeByTemplate({
|
|
...input,
|
|
composeFile: input.dockerCompose,
|
|
env: input.envVariables,
|
|
serverId: input.serverId,
|
|
name: input.name,
|
|
sourceType: "raw",
|
|
appName: `${projectName}-${generatePassword(6)}`,
|
|
isolatedDeployment: true,
|
|
environmentId: input.environmentId,
|
|
});
|
|
|
|
if (input.domains && input.domains?.length > 0) {
|
|
for (const domain of input.domains) {
|
|
await createDomain({
|
|
...domain,
|
|
domainType: "compose",
|
|
certificateType: "none",
|
|
composeId: compose.composeId,
|
|
});
|
|
}
|
|
}
|
|
if (input.configFiles && input.configFiles?.length > 0) {
|
|
for (const mount of input.configFiles) {
|
|
await createMount({
|
|
filePath: mount.filePath,
|
|
mountPath: "",
|
|
content: mount.content,
|
|
serviceId: compose.composeId,
|
|
serviceType: "compose",
|
|
type: "file",
|
|
});
|
|
}
|
|
}
|
|
|
|
await addNewService(ctx, compose.composeId);
|
|
|
|
return null;
|
|
}),
|
|
});
|