[autofix.ci] apply automated fixes

This commit is contained in:
autofix-ci[bot]
2026-03-29 22:02:53 +00:00
committed by GitHub
parent 815b8136fa
commit 6d17f62942
5 changed files with 675 additions and 675 deletions
@@ -1,7 +1,7 @@
import type { ComposeSpecification } from "@dokploy/server"; import type { ComposeSpecification } from "@dokploy/server";
import { import {
addSuffixToServiceNetworks, addSuffixToServiceNetworks,
generateRandomHash, generateRandomHash,
} from "@dokploy/server"; } from "@dokploy/server";
import { expect, test } from "vitest"; import { expect, test } from "vitest";
import { parse } from "yaml"; import { parse } from "yaml";
@@ -23,30 +23,30 @@ services:
`; `;
test("Add suffix to networks in services", () => { test("Add suffix to networks in services", () => {
const composeData = parse(composeFile) as ComposeSpecification; const composeData = parse(composeFile) as ComposeSpecification;
const suffix = generateRandomHash(); const suffix = generateRandomHash();
if (!composeData?.services) { if (!composeData?.services) {
return; return;
} }
const services = addSuffixToServiceNetworks(composeData.services, suffix); const services = addSuffixToServiceNetworks(composeData.services, suffix);
const actualComposeData = { ...composeData, services }; const actualComposeData = { ...composeData, services };
expect(actualComposeData?.services?.web?.networks).toContain( expect(actualComposeData?.services?.web?.networks).toContain(
`frontend-${suffix}`, `frontend-${suffix}`,
); );
expect(actualComposeData?.services?.api?.networks).toContain( expect(actualComposeData?.services?.api?.networks).toContain(
`backend-${suffix}`, `backend-${suffix}`,
); );
const apiNetworks = actualComposeData?.services?.api?.networks; const apiNetworks = actualComposeData?.services?.api?.networks;
expect(apiNetworks).toBeDefined(); expect(apiNetworks).toBeDefined();
expect(actualComposeData?.services?.api?.networks).toContain( expect(actualComposeData?.services?.api?.networks).toContain(
`backend-${suffix}`, `backend-${suffix}`,
); );
}); });
// Caso 2: Objeto con aliases // Caso 2: Objeto con aliases
@@ -67,29 +67,29 @@ networks:
`; `;
test("Add suffix to networks in services with aliases", () => { test("Add suffix to networks in services with aliases", () => {
const composeData = parse(composeFile2) as ComposeSpecification; const composeData = parse(composeFile2) as ComposeSpecification;
const suffix = generateRandomHash(); const suffix = generateRandomHash();
if (!composeData?.services) { if (!composeData?.services) {
return; return;
} }
const services = addSuffixToServiceNetworks(composeData.services, suffix); const services = addSuffixToServiceNetworks(composeData.services, suffix);
const actualComposeData = { ...composeData, services }; const actualComposeData = { ...composeData, services };
expect(actualComposeData.services?.api?.networks).toHaveProperty( expect(actualComposeData.services?.api?.networks).toHaveProperty(
`frontend-${suffix}`, `frontend-${suffix}`,
); );
const networkConfig = actualComposeData?.services?.api?.networks as { const networkConfig = actualComposeData?.services?.api?.networks as {
[key: string]: { aliases?: string[] }; [key: string]: { aliases?: string[] };
}; };
expect(networkConfig[`frontend-${suffix}`]).toBeDefined(); expect(networkConfig[`frontend-${suffix}`]).toBeDefined();
expect(networkConfig[`frontend-${suffix}`]?.aliases).toContain("api"); expect(networkConfig[`frontend-${suffix}`]?.aliases).toContain("api");
expect(actualComposeData.services?.api?.networks).not.toHaveProperty( expect(actualComposeData.services?.api?.networks).not.toHaveProperty(
"frontend-ash", "frontend-ash",
); );
}); });
const composeFile3 = ` const composeFile3 = `
@@ -107,19 +107,19 @@ networks:
`; `;
test("Add suffix to networks in services (Object with simple networks)", () => { test("Add suffix to networks in services (Object with simple networks)", () => {
const composeData = parse(composeFile3) as ComposeSpecification; const composeData = parse(composeFile3) as ComposeSpecification;
const suffix = generateRandomHash(); const suffix = generateRandomHash();
if (!composeData?.services) { if (!composeData?.services) {
return; return;
} }
const services = addSuffixToServiceNetworks(composeData.services, suffix); const services = addSuffixToServiceNetworks(composeData.services, suffix);
const actualComposeData = { ...composeData, services }; const actualComposeData = { ...composeData, services };
expect(actualComposeData.services?.redis?.networks).toHaveProperty( expect(actualComposeData.services?.redis?.networks).toHaveProperty(
`backend-${suffix}`, `backend-${suffix}`,
); );
}); });
const composeFileCombined = ` const composeFileCombined = `
@@ -153,36 +153,36 @@ networks:
`; `;
test("Add suffix to networks in services (combined case)", () => { test("Add suffix to networks in services (combined case)", () => {
const composeData = parse(composeFileCombined) as ComposeSpecification; const composeData = parse(composeFileCombined) as ComposeSpecification;
const suffix = generateRandomHash(); const suffix = generateRandomHash();
if (!composeData?.services) { if (!composeData?.services) {
return; return;
} }
const services = addSuffixToServiceNetworks(composeData.services, suffix); const services = addSuffixToServiceNetworks(composeData.services, suffix);
const actualComposeData = { ...composeData, services }; const actualComposeData = { ...composeData, services };
// Caso 1: ListOfStrings // Caso 1: ListOfStrings
expect(actualComposeData.services?.web?.networks).toContain( expect(actualComposeData.services?.web?.networks).toContain(
`frontend-${suffix}`, `frontend-${suffix}`,
); );
expect(actualComposeData.services?.web?.networks).toContain( expect(actualComposeData.services?.web?.networks).toContain(
`backend-${suffix}`, `backend-${suffix}`,
); );
// Caso 2: Objeto con aliases // Caso 2: Objeto con aliases
const apiNetworks = actualComposeData.services?.api?.networks as { const apiNetworks = actualComposeData.services?.api?.networks as {
[key: string]: unknown; [key: string]: unknown;
}; };
expect(apiNetworks).toHaveProperty(`frontend-${suffix}`); expect(apiNetworks).toHaveProperty(`frontend-${suffix}`);
expect(apiNetworks[`frontend-${suffix}`]).toBeDefined(); expect(apiNetworks[`frontend-${suffix}`]).toBeDefined();
expect(apiNetworks).not.toHaveProperty("frontend"); expect(apiNetworks).not.toHaveProperty("frontend");
// Caso 3: Objeto con redes simples // Caso 3: Objeto con redes simples
const redisNetworks = actualComposeData.services?.redis?.networks; const redisNetworks = actualComposeData.services?.redis?.networks;
expect(redisNetworks).toHaveProperty(`backend-${suffix}`); expect(redisNetworks).toHaveProperty(`backend-${suffix}`);
expect(redisNetworks).not.toHaveProperty("backend"); expect(redisNetworks).not.toHaveProperty("backend");
}); });
const composeFile7 = ` const composeFile7 = `
@@ -196,18 +196,18 @@ services:
`; `;
test("It shouldn't add suffix to dokploy-network in services", () => { test("It shouldn't add suffix to dokploy-network in services", () => {
const composeData = parse(composeFile7) as ComposeSpecification; const composeData = parse(composeFile7) as ComposeSpecification;
const suffix = generateRandomHash(); const suffix = generateRandomHash();
if (!composeData?.services) { if (!composeData?.services) {
return; return;
} }
const networks = addSuffixToServiceNetworks(composeData.services, suffix); const networks = addSuffixToServiceNetworks(composeData.services, suffix);
const service = networks.web; const service = networks.web;
expect(service).toBeDefined(); expect(service).toBeDefined();
expect(service?.networks).toContain("dokploy-network"); expect(service?.networks).toContain("dokploy-network");
}); });
const composeFile8 = ` const composeFile8 = `
@@ -245,31 +245,31 @@ services:
`; `;
test("It shouldn't add suffix to dokploy-network in services multiples cases", () => { test("It shouldn't add suffix to dokploy-network in services multiples cases", () => {
const composeData = parse(composeFile8) as ComposeSpecification; const composeData = parse(composeFile8) as ComposeSpecification;
const suffix = generateRandomHash(); const suffix = generateRandomHash();
if (!composeData?.services) { if (!composeData?.services) {
return; return;
} }
const networks = addSuffixToServiceNetworks(composeData.services, suffix); const networks = addSuffixToServiceNetworks(composeData.services, suffix);
const service = networks.web; const service = networks.web;
const api = networks.api; const api = networks.api;
const redis = networks.redis; const redis = networks.redis;
const db = networks.db; const db = networks.db;
const dbNetworks = db?.networks as { const dbNetworks = db?.networks as {
[key: string]: unknown; [key: string]: unknown;
}; };
const apiNetworks = api?.networks as { const apiNetworks = api?.networks as {
[key: string]: unknown; [key: string]: unknown;
}; };
expect(service).toBeDefined(); expect(service).toBeDefined();
expect(service?.networks).toContain("dokploy-network"); expect(service?.networks).toContain("dokploy-network");
expect(redis?.networks).toHaveProperty("dokploy-network"); expect(redis?.networks).toHaveProperty("dokploy-network");
expect(dbNetworks["dokploy-network"]).toBeDefined(); expect(dbNetworks["dokploy-network"]).toBeDefined();
expect(apiNetworks["dokploy-network"]).toBeDefined(); expect(apiNetworks["dokploy-network"]).toBeDefined();
}); });
+97 -97
View File
@@ -16,28 +16,28 @@ SECRET_KEY=env-secret-123
`; `;
describe("getEnvironmentVariablesObject with environment variables (Stack compose)", () => { describe("getEnvironmentVariablesObject with environment variables (Stack compose)", () => {
it("resolves environment variables correctly for Stack compose", () => { it("resolves environment variables correctly for Stack compose", () => {
const serviceEnv = ` const serviceEnv = `
FOO=\${{environment.NODE_ENV}} FOO=\${{environment.NODE_ENV}}
BAR=\${{environment.API_URL}} BAR=\${{environment.API_URL}}
BAZ=test BAZ=test
`; `;
const result = getEnvironmentVariablesObject( const result = getEnvironmentVariablesObject(
serviceEnv, serviceEnv,
projectEnv, projectEnv,
environmentEnv, environmentEnv,
); );
expect(result).toEqual({ expect(result).toEqual({
FOO: "development", FOO: "development",
BAR: "https://api.dev.example.com", BAR: "https://api.dev.example.com",
BAZ: "test", BAZ: "test",
}); });
}); });
it("resolves both project and environment variables for Stack compose", () => { it("resolves both project and environment variables for Stack compose", () => {
const serviceEnv = ` const serviceEnv = `
ENVIRONMENT=\${{project.ENVIRONMENT}} ENVIRONMENT=\${{project.ENVIRONMENT}}
NODE_ENV=\${{environment.NODE_ENV}} NODE_ENV=\${{environment.NODE_ENV}}
API_URL=\${{environment.API_URL}} API_URL=\${{environment.API_URL}}
@@ -45,140 +45,140 @@ DATABASE_URL=\${{project.DATABASE_URL}}
SERVICE_PORT=4000 SERVICE_PORT=4000
`; `;
const result = getEnvironmentVariablesObject( const result = getEnvironmentVariablesObject(
serviceEnv, serviceEnv,
projectEnv, projectEnv,
environmentEnv, environmentEnv,
); );
expect(result).toEqual({ expect(result).toEqual({
ENVIRONMENT: "staging", ENVIRONMENT: "staging",
NODE_ENV: "development", NODE_ENV: "development",
API_URL: "https://api.dev.example.com", API_URL: "https://api.dev.example.com",
DATABASE_URL: "postgres://postgres:postgres@localhost:5432/project_db", DATABASE_URL: "postgres://postgres:postgres@localhost:5432/project_db",
SERVICE_PORT: "4000", SERVICE_PORT: "4000",
}); });
}); });
it("handles multiple environment references in single value for Stack compose", () => { it("handles multiple environment references in single value for Stack compose", () => {
const multiRefEnv = ` const multiRefEnv = `
HOST=localhost HOST=localhost
PORT=5432 PORT=5432
USERNAME=postgres USERNAME=postgres
PASSWORD=secret123 PASSWORD=secret123
`; `;
const serviceEnv = ` const serviceEnv = `
DATABASE_URL=postgresql://\${{environment.USERNAME}}:\${{environment.PASSWORD}}@\${{environment.HOST}}:\${{environment.PORT}}/mydb DATABASE_URL=postgresql://\${{environment.USERNAME}}:\${{environment.PASSWORD}}@\${{environment.HOST}}:\${{environment.PORT}}/mydb
`; `;
const result = getEnvironmentVariablesObject(serviceEnv, "", multiRefEnv); const result = getEnvironmentVariablesObject(serviceEnv, "", multiRefEnv);
expect(result).toEqual({ expect(result).toEqual({
DATABASE_URL: "postgresql://postgres:secret123@localhost:5432/mydb", DATABASE_URL: "postgresql://postgres:secret123@localhost:5432/mydb",
}); });
}); });
it("throws error for undefined environment variables in Stack compose", () => { it("throws error for undefined environment variables in Stack compose", () => {
const serviceWithUndefined = ` const serviceWithUndefined = `
UNDEFINED_VAR=\${{environment.UNDEFINED_VAR}} UNDEFINED_VAR=\${{environment.UNDEFINED_VAR}}
`; `;
expect(() => expect(() =>
getEnvironmentVariablesObject(serviceWithUndefined, "", environmentEnv), getEnvironmentVariablesObject(serviceWithUndefined, "", environmentEnv),
).toThrow("Invalid environment variable: environment.UNDEFINED_VAR"); ).toThrow("Invalid environment variable: environment.UNDEFINED_VAR");
}); });
it("allows service variables to override environment variables in Stack compose", () => { it("allows service variables to override environment variables in Stack compose", () => {
const serviceOverrideEnv = ` const serviceOverrideEnv = `
NODE_ENV=production NODE_ENV=production
API_URL=\${{environment.API_URL}} API_URL=\${{environment.API_URL}}
`; `;
const result = getEnvironmentVariablesObject( const result = getEnvironmentVariablesObject(
serviceOverrideEnv, serviceOverrideEnv,
"", "",
environmentEnv, environmentEnv,
); );
expect(result).toEqual({ expect(result).toEqual({
NODE_ENV: "production", NODE_ENV: "production",
API_URL: "https://api.dev.example.com", API_URL: "https://api.dev.example.com",
}); });
}); });
it("resolves complex references with project, environment, and service variables for Stack compose", () => { it("resolves complex references with project, environment, and service variables for Stack compose", () => {
const complexServiceEnv = ` const complexServiceEnv = `
FULL_DATABASE_URL=\${{project.DATABASE_URL}}/\${{environment.DATABASE_NAME}} FULL_DATABASE_URL=\${{project.DATABASE_URL}}/\${{environment.DATABASE_NAME}}
API_ENDPOINT=\${{environment.API_URL}}/\${{project.ENVIRONMENT}}/api API_ENDPOINT=\${{environment.API_URL}}/\${{project.ENVIRONMENT}}/api
SERVICE_NAME=my-service SERVICE_NAME=my-service
COMPLEX_VAR=\${{SERVICE_NAME}}-\${{environment.NODE_ENV}}-\${{project.ENVIRONMENT}} COMPLEX_VAR=\${{SERVICE_NAME}}-\${{environment.NODE_ENV}}-\${{project.ENVIRONMENT}}
`; `;
const result = getEnvironmentVariablesObject( const result = getEnvironmentVariablesObject(
complexServiceEnv, complexServiceEnv,
projectEnv, projectEnv,
environmentEnv, environmentEnv,
); );
expect(result).toEqual({ expect(result).toEqual({
FULL_DATABASE_URL: FULL_DATABASE_URL:
"postgres://postgres:postgres@localhost:5432/project_db/dev_database", "postgres://postgres:postgres@localhost:5432/project_db/dev_database",
API_ENDPOINT: "https://api.dev.example.com/staging/api", API_ENDPOINT: "https://api.dev.example.com/staging/api",
SERVICE_NAME: "my-service", SERVICE_NAME: "my-service",
COMPLEX_VAR: "my-service-development-staging", COMPLEX_VAR: "my-service-development-staging",
}); });
}); });
it("maintains precedence: service > environment > project in Stack compose", () => { it("maintains precedence: service > environment > project in Stack compose", () => {
const conflictingProjectEnv = ` const conflictingProjectEnv = `
NODE_ENV=production-project NODE_ENV=production-project
API_URL=https://project.api.com API_URL=https://project.api.com
DATABASE_NAME=project_db DATABASE_NAME=project_db
`; `;
const conflictingEnvironmentEnv = ` const conflictingEnvironmentEnv = `
NODE_ENV=development-environment NODE_ENV=development-environment
API_URL=https://environment.api.com API_URL=https://environment.api.com
DATABASE_NAME=env_db DATABASE_NAME=env_db
`; `;
const serviceWithConflicts = ` const serviceWithConflicts = `
NODE_ENV=service-override NODE_ENV=service-override
PROJECT_ENV=\${{project.NODE_ENV}} PROJECT_ENV=\${{project.NODE_ENV}}
ENV_VAR=\${{environment.API_URL}} ENV_VAR=\${{environment.API_URL}}
DB_NAME=\${{environment.DATABASE_NAME}} DB_NAME=\${{environment.DATABASE_NAME}}
`; `;
const result = getEnvironmentVariablesObject( const result = getEnvironmentVariablesObject(
serviceWithConflicts, serviceWithConflicts,
conflictingProjectEnv, conflictingProjectEnv,
conflictingEnvironmentEnv, conflictingEnvironmentEnv,
); );
expect(result).toEqual({ expect(result).toEqual({
NODE_ENV: "service-override", NODE_ENV: "service-override",
PROJECT_ENV: "production-project", PROJECT_ENV: "production-project",
ENV_VAR: "https://environment.api.com", ENV_VAR: "https://environment.api.com",
DB_NAME: "env_db", DB_NAME: "env_db",
}); });
}); });
it("handles empty environment variables in Stack compose", () => { it("handles empty environment variables in Stack compose", () => {
const serviceWithEmpty = ` const serviceWithEmpty = `
SERVICE_VAR=test SERVICE_VAR=test
PROJECT_VAR=\${{project.ENVIRONMENT}} PROJECT_VAR=\${{project.ENVIRONMENT}}
`; `;
const result = getEnvironmentVariablesObject( const result = getEnvironmentVariablesObject(
serviceWithEmpty, serviceWithEmpty,
projectEnv, projectEnv,
"", "",
); );
expect(result).toEqual({ expect(result).toEqual({
SERVICE_VAR: "test", SERVICE_VAR: "test",
PROJECT_VAR: "staging", PROJECT_VAR: "staging",
}); });
}); });
}); });
+338 -338
View File
@@ -7,452 +7,452 @@ import { auth } from "../lib/auth";
export type User = typeof user.$inferSelect; export type User = typeof user.$inferSelect;
export const addNewProject = async ( export const addNewProject = async (
userId: string, userId: string,
projectId: string, projectId: string,
organizationId: string, organizationId: string,
) => { ) => {
const userR = await findMemberById(userId, organizationId); const userR = await findMemberById(userId, organizationId);
await db await db
.update(member) .update(member)
.set({ .set({
accessedProjects: [...userR.accessedProjects, projectId], accessedProjects: [...userR.accessedProjects, projectId],
}) })
.where( .where(
and(eq(member.id, userR.id), eq(member.organizationId, organizationId)), and(eq(member.id, userR.id), eq(member.organizationId, organizationId)),
); );
}; };
export const addNewEnvironment = async ( export const addNewEnvironment = async (
userId: string, userId: string,
environmentId: string, environmentId: string,
organizationId: string, organizationId: string,
) => { ) => {
const userR = await findMemberById(userId, organizationId); const userR = await findMemberById(userId, organizationId);
await db await db
.update(member) .update(member)
.set({ .set({
accessedEnvironments: [...userR.accessedEnvironments, environmentId], accessedEnvironments: [...userR.accessedEnvironments, environmentId],
}) })
.where( .where(
and(eq(member.id, userR.id), eq(member.organizationId, organizationId)), and(eq(member.id, userR.id), eq(member.organizationId, organizationId)),
); );
}; };
export const addNewService = async ( export const addNewService = async (
userId: string, userId: string,
serviceId: string, serviceId: string,
organizationId: string, organizationId: string,
) => { ) => {
const userR = await findMemberById(userId, organizationId); const userR = await findMemberById(userId, organizationId);
await db await db
.update(member) .update(member)
.set({ .set({
accessedServices: [...userR.accessedServices, serviceId], accessedServices: [...userR.accessedServices, serviceId],
}) })
.where( .where(
and(eq(member.id, userR.id), eq(member.organizationId, organizationId)), and(eq(member.id, userR.id), eq(member.organizationId, organizationId)),
); );
}; };
export const canPerformCreationService = async ( export const canPerformCreationService = async (
userId: string, userId: string,
projectId: string, projectId: string,
organizationId: string, organizationId: string,
) => { ) => {
const { accessedProjects, canCreateServices } = await findMemberById( const { accessedProjects, canCreateServices } = await findMemberById(
userId, userId,
organizationId, organizationId,
); );
const haveAccessToProject = accessedProjects.includes(projectId); const haveAccessToProject = accessedProjects.includes(projectId);
if (canCreateServices && haveAccessToProject) { if (canCreateServices && haveAccessToProject) {
return true; return true;
} }
return false; return false;
}; };
export const canPerformAccessService = async ( export const canPerformAccessService = async (
userId: string, userId: string,
serviceId: string, serviceId: string,
organizationId: string, organizationId: string,
) => { ) => {
const { accessedServices } = await findMemberById(userId, organizationId); const { accessedServices } = await findMemberById(userId, organizationId);
const haveAccessToService = accessedServices.includes(serviceId); const haveAccessToService = accessedServices.includes(serviceId);
if (haveAccessToService) { if (haveAccessToService) {
return true; return true;
} }
return false; return false;
}; };
export const canPerformDeleteService = async ( export const canPerformDeleteService = async (
userId: string, userId: string,
serviceId: string, serviceId: string,
organizationId: string, organizationId: string,
) => { ) => {
const { accessedServices, canDeleteServices } = await findMemberById( const { accessedServices, canDeleteServices } = await findMemberById(
userId, userId,
organizationId, organizationId,
); );
const haveAccessToService = accessedServices.includes(serviceId); const haveAccessToService = accessedServices.includes(serviceId);
if (canDeleteServices && haveAccessToService) { if (canDeleteServices && haveAccessToService) {
return true; return true;
} }
return false; return false;
}; };
export const canPerformCreationProject = async ( export const canPerformCreationProject = async (
userId: string, userId: string,
organizationId: string, organizationId: string,
) => { ) => {
const { canCreateProjects } = await findMemberById(userId, organizationId); const { canCreateProjects } = await findMemberById(userId, organizationId);
if (canCreateProjects) { if (canCreateProjects) {
return true; return true;
} }
return false; return false;
}; };
export const canPerformDeleteProject = async ( export const canPerformDeleteProject = async (
userId: string, userId: string,
organizationId: string, organizationId: string,
) => { ) => {
const { canDeleteProjects } = await findMemberById(userId, organizationId); const { canDeleteProjects } = await findMemberById(userId, organizationId);
if (canDeleteProjects) { if (canDeleteProjects) {
return true; return true;
} }
return false; return false;
}; };
export const canPerformAccessProject = async ( export const canPerformAccessProject = async (
userId: string, userId: string,
projectId: string, projectId: string,
organizationId: string, organizationId: string,
) => { ) => {
const { accessedProjects } = await findMemberById(userId, organizationId); const { accessedProjects } = await findMemberById(userId, organizationId);
const haveAccessToProject = accessedProjects.includes(projectId); const haveAccessToProject = accessedProjects.includes(projectId);
if (haveAccessToProject) { if (haveAccessToProject) {
return true; return true;
} }
return false; return false;
}; };
export const canPerformAccessEnvironment = async ( export const canPerformAccessEnvironment = async (
userId: string, userId: string,
environmentId: string, environmentId: string,
organizationId: string, organizationId: string,
) => { ) => {
const { accessedEnvironments } = await findMemberById(userId, organizationId); const { accessedEnvironments } = await findMemberById(userId, organizationId);
const haveAccessToEnvironment = accessedEnvironments.includes(environmentId); const haveAccessToEnvironment = accessedEnvironments.includes(environmentId);
if (haveAccessToEnvironment) { if (haveAccessToEnvironment) {
return true; return true;
} }
return false; return false;
}; };
export const canPerformDeleteEnvironment = async ( export const canPerformDeleteEnvironment = async (
userId: string, userId: string,
projectId: string, projectId: string,
organizationId: string, organizationId: string,
) => { ) => {
const { accessedProjects, canDeleteEnvironments } = await findMemberById( const { accessedProjects, canDeleteEnvironments } = await findMemberById(
userId, userId,
organizationId, organizationId,
); );
const haveAccessToProject = accessedProjects.includes(projectId); const haveAccessToProject = accessedProjects.includes(projectId);
if (canDeleteEnvironments && haveAccessToProject) { if (canDeleteEnvironments && haveAccessToProject) {
return true; return true;
} }
return false; return false;
}; };
export const canAccessToTraefikFiles = async ( export const canAccessToTraefikFiles = async (
userId: string, userId: string,
organizationId: string, organizationId: string,
) => { ) => {
const { canAccessToTraefikFiles } = await findMemberById( const { canAccessToTraefikFiles } = await findMemberById(
userId, userId,
organizationId, organizationId,
); );
return canAccessToTraefikFiles; return canAccessToTraefikFiles;
}; };
export const checkServiceAccess = async ( export const checkServiceAccess = async (
userId: string, userId: string,
serviceId: string, serviceId: string,
organizationId: string, organizationId: string,
action = "access" as "access" | "create" | "delete", action = "access" as "access" | "create" | "delete",
) => { ) => {
let hasPermission = false; let hasPermission = false;
switch (action) { switch (action) {
case "create": case "create":
hasPermission = await canPerformCreationService( hasPermission = await canPerformCreationService(
userId, userId,
serviceId, serviceId,
organizationId, organizationId,
); );
break; break;
case "access": case "access":
hasPermission = await canPerformAccessService( hasPermission = await canPerformAccessService(
userId, userId,
serviceId, serviceId,
organizationId, organizationId,
); );
break; break;
case "delete": case "delete":
hasPermission = await canPerformDeleteService( hasPermission = await canPerformDeleteService(
userId, userId,
serviceId, serviceId,
organizationId, organizationId,
); );
break; break;
default: default:
hasPermission = false; hasPermission = false;
} }
if (!hasPermission) { if (!hasPermission) {
throw new TRPCError({ throw new TRPCError({
code: "UNAUTHORIZED", code: "UNAUTHORIZED",
message: "Permission denied", message: "Permission denied",
}); });
} }
}; };
export const checkEnvironmentAccess = async ( export const checkEnvironmentAccess = async (
userId: string, userId: string,
environmentId: string, environmentId: string,
organizationId: string, organizationId: string,
action = "access" as const, action = "access" as const,
) => { ) => {
let hasPermission = false; let hasPermission = false;
switch (action) { switch (action) {
case "access": case "access":
hasPermission = await canPerformAccessEnvironment( hasPermission = await canPerformAccessEnvironment(
userId, userId,
environmentId, environmentId,
organizationId, organizationId,
); );
break; break;
default: default:
hasPermission = false; hasPermission = false;
} }
if (!hasPermission) { if (!hasPermission) {
throw new TRPCError({ throw new TRPCError({
code: "UNAUTHORIZED", code: "UNAUTHORIZED",
message: "Permission denied", message: "Permission denied",
}); });
} }
}; };
export const checkEnvironmentDeletionPermission = async ( export const checkEnvironmentDeletionPermission = async (
userId: string, userId: string,
projectId: string, projectId: string,
organizationId: string, organizationId: string,
) => { ) => {
const member = await findMemberById(userId, organizationId); const member = await findMemberById(userId, organizationId);
if (!member) { if (!member) {
throw new TRPCError({ throw new TRPCError({
code: "UNAUTHORIZED", code: "UNAUTHORIZED",
message: "User not found in organization", message: "User not found in organization",
}); });
} }
if (member.role === "owner" || member.role === "admin") { if (member.role === "owner" || member.role === "admin") {
return true; return true;
} }
if (!member.canDeleteEnvironments) { if (!member.canDeleteEnvironments) {
throw new TRPCError({ throw new TRPCError({
code: "UNAUTHORIZED", code: "UNAUTHORIZED",
message: "You don't have permission to delete environments", message: "You don't have permission to delete environments",
}); });
} }
const hasProjectAccess = member.accessedProjects.includes(projectId); const hasProjectAccess = member.accessedProjects.includes(projectId);
if (!hasProjectAccess) { if (!hasProjectAccess) {
throw new TRPCError({ throw new TRPCError({
code: "UNAUTHORIZED", code: "UNAUTHORIZED",
message: "You don't have access to this project", message: "You don't have access to this project",
}); });
} }
return true; return true;
}; };
export const checkProjectAccess = async ( export const checkProjectAccess = async (
authId: string, authId: string,
action: "create" | "delete" | "access", action: "create" | "delete" | "access",
organizationId: string, organizationId: string,
projectId?: string, projectId?: string,
) => { ) => {
let hasPermission = false; let hasPermission = false;
switch (action) { switch (action) {
case "access": case "access":
hasPermission = await canPerformAccessProject( hasPermission = await canPerformAccessProject(
authId, authId,
projectId as string, projectId as string,
organizationId, organizationId,
); );
break; break;
case "create": case "create":
hasPermission = await canPerformCreationProject(authId, organizationId); hasPermission = await canPerformCreationProject(authId, organizationId);
break; break;
case "delete": case "delete":
hasPermission = await canPerformDeleteProject(authId, organizationId); hasPermission = await canPerformDeleteProject(authId, organizationId);
break; break;
default: default:
hasPermission = false; hasPermission = false;
} }
if (!hasPermission) { if (!hasPermission) {
throw new TRPCError({ throw new TRPCError({
code: "UNAUTHORIZED", code: "UNAUTHORIZED",
message: "Permission denied", message: "Permission denied",
}); });
} }
}; };
export const checkEnvironmentCreationPermission = async ( export const checkEnvironmentCreationPermission = async (
userId: string, userId: string,
projectId: string, projectId: string,
organizationId: string, organizationId: string,
) => { ) => {
// Get user's member record // Get user's member record
const member = await findMemberById(userId, organizationId); const member = await findMemberById(userId, organizationId);
if (!member) { if (!member) {
throw new TRPCError({ throw new TRPCError({
code: "UNAUTHORIZED", code: "UNAUTHORIZED",
message: "User not found in organization", message: "User not found in organization",
}); });
} }
// Owners and admins can always create environments // Owners and admins can always create environments
if (member.role === "owner" || member.role === "admin") { if (member.role === "owner" || member.role === "admin") {
return true; return true;
} }
// Check if user has canCreateEnvironments permission // Check if user has canCreateEnvironments permission
if (!member.canCreateEnvironments) { if (!member.canCreateEnvironments) {
throw new TRPCError({ throw new TRPCError({
code: "UNAUTHORIZED", code: "UNAUTHORIZED",
message: "You don't have permission to create environments", message: "You don't have permission to create environments",
}); });
} }
// Check if user has access to the project // Check if user has access to the project
const hasProjectAccess = member.accessedProjects.includes(projectId); const hasProjectAccess = member.accessedProjects.includes(projectId);
if (!hasProjectAccess) { if (!hasProjectAccess) {
throw new TRPCError({ throw new TRPCError({
code: "UNAUTHORIZED", code: "UNAUTHORIZED",
message: "You don't have access to this project", message: "You don't have access to this project",
}); });
} }
return true; return true;
}; };
export const findMemberById = async ( export const findMemberById = async (
userId: string, userId: string,
organizationId: string, organizationId: string,
) => { ) => {
const result = await db.query.member.findFirst({ const result = await db.query.member.findFirst({
where: and( where: and(
eq(member.userId, userId), eq(member.userId, userId),
eq(member.organizationId, organizationId), eq(member.organizationId, organizationId),
), ),
with: { with: {
user: true, user: true,
}, },
}); });
if (!result) { if (!result) {
throw new TRPCError({ throw new TRPCError({
code: "UNAUTHORIZED", code: "UNAUTHORIZED",
message: "Permission denied", message: "Permission denied",
}); });
} }
return result; return result;
}; };
export const updateUser = async (userId: string, userData: Partial<User>) => { export const updateUser = async (userId: string, userData: Partial<User>) => {
// Validate email if it's being updated // Validate email if it's being updated
if (userData.email !== undefined) { if (userData.email !== undefined) {
if (!userData.email || userData.email.trim() === "") { if (!userData.email || userData.email.trim() === "") {
throw new Error("Email is required and cannot be empty"); throw new Error("Email is required and cannot be empty");
} }
// Basic email format validation // Basic email format validation
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (!emailRegex.test(userData.email)) { if (!emailRegex.test(userData.email)) {
throw new Error("Please enter a valid email address"); throw new Error("Please enter a valid email address");
} }
} }
const userResult = await db const userResult = await db
.update(user) .update(user)
.set({ .set({
...userData, ...userData,
}) })
.where(eq(user.id, userId)) .where(eq(user.id, userId))
.returning() .returning()
.then((res) => res[0]); .then((res) => res[0]);
return userResult; return userResult;
}; };
export const createApiKey = async ( export const createApiKey = async (
userId: string, userId: string,
input: { input: {
name: string; name: string;
prefix?: string; prefix?: string;
expiresIn?: number; expiresIn?: number;
metadata: { metadata: {
organizationId: string; organizationId: string;
}; };
rateLimitEnabled?: boolean; rateLimitEnabled?: boolean;
rateLimitTimeWindow?: number; rateLimitTimeWindow?: number;
rateLimitMax?: number; rateLimitMax?: number;
remaining?: number; remaining?: number;
refillAmount?: number; refillAmount?: number;
refillInterval?: number; refillInterval?: number;
}, },
) => { ) => {
const result = await auth.createApiKey({ const result = await auth.createApiKey({
body: { body: {
name: input.name, name: input.name,
expiresIn: input.expiresIn, expiresIn: input.expiresIn,
prefix: input.prefix, prefix: input.prefix,
rateLimitEnabled: input.rateLimitEnabled, rateLimitEnabled: input.rateLimitEnabled,
rateLimitTimeWindow: input.rateLimitTimeWindow, rateLimitTimeWindow: input.rateLimitTimeWindow,
rateLimitMax: input.rateLimitMax, rateLimitMax: input.rateLimitMax,
remaining: input.remaining, remaining: input.remaining,
refillAmount: input.refillAmount, refillAmount: input.refillAmount,
refillInterval: input.refillInterval, refillInterval: input.refillInterval,
userId, userId,
}, },
}); });
if (input.metadata) { if (input.metadata) {
await db await db
.update(apikey) .update(apikey)
.set({ metadata: JSON.stringify(input.metadata) }) .set({ metadata: JSON.stringify(input.metadata) })
.where(eq(apikey.id, result.id)); .where(eq(apikey.id, result.id));
} }
return result; return result;
}; };
+72 -72
View File
@@ -5,26 +5,26 @@ import boxen from "boxen";
import { quote } from "shell-quote"; import { quote } from "shell-quote";
import { writeDomainsToCompose } from "../docker/domain"; import { writeDomainsToCompose } from "../docker/domain";
import { import {
encodeBase64, encodeBase64,
getEnvironmentVariablesObject, getEnvironmentVariablesObject,
prepareEnvironmentVariables, prepareEnvironmentVariables,
} from "../docker/utils"; } from "../docker/utils";
export type ComposeNested = InferResultType< export type ComposeNested = InferResultType<
"compose", "compose",
{ environment: { with: { project: true } }; mounts: true; domains: true } { environment: { with: { project: true } }; mounts: true; domains: true }
>; >;
export const getBuildComposeCommand = async (compose: ComposeNested) => { export const getBuildComposeCommand = async (compose: ComposeNested) => {
const { COMPOSE_PATH } = paths(!!compose.serverId); const { COMPOSE_PATH } = paths(!!compose.serverId);
const { sourceType, appName, mounts, composeType, domains } = compose; const { sourceType, appName, mounts, composeType, domains } = compose;
const command = createCommand(compose); const command = createCommand(compose);
const envCommand = getCreateEnvFileCommand(compose); const envCommand = getCreateEnvFileCommand(compose);
const projectPath = join(COMPOSE_PATH, compose.appName, "code"); const projectPath = join(COMPOSE_PATH, compose.appName, "code");
const exportEnvCommand = getExportEnvCommand(compose); const exportEnvCommand = getExportEnvCommand(compose);
const newCompose = await writeDomainsToCompose(compose, domains); const newCompose = await writeDomainsToCompose(compose, domains);
const logContent = ` const logContent = `
App Name: ${appName} App Name: ${appName}
Build Compose 🐳 Build Compose 🐳
Detected: ${mounts.length} mounts 📂 Detected: ${mounts.length} mounts 📂
@@ -32,17 +32,17 @@ Command: docker ${command}
Source Type: docker ${sourceType} ✅ Source Type: docker ${sourceType} ✅
Compose Type: ${composeType} ✅`; Compose Type: ${composeType} ✅`;
const logBox = boxen(logContent, { const logBox = boxen(logContent, {
padding: { padding: {
left: 1, left: 1,
right: 1, right: 1,
bottom: 1, bottom: 1,
}, },
width: 80, width: 80,
borderStyle: "double", borderStyle: "double",
}); });
const bashCommand = ` const bashCommand = `
set -e set -e
{ {
echo "${logBox}"; echo "${logBox}";
@@ -64,81 +64,81 @@ Compose Type: ${composeType} ✅`;
} }
`; `;
return bashCommand; return bashCommand;
}; };
const sanitizeCommand = (command: string) => { const sanitizeCommand = (command: string) => {
const sanitizedCommand = command.trim(); const sanitizedCommand = command.trim();
const parts = sanitizedCommand.split(/\s+/); const parts = sanitizedCommand.split(/\s+/);
const restCommand = parts.map((arg) => arg.replace(/^"(.*)"$/, "$1")); const restCommand = parts.map((arg) => arg.replace(/^"(.*)"$/, "$1"));
return restCommand.join(" "); return restCommand.join(" ");
}; };
export const createCommand = (compose: ComposeNested) => { export const createCommand = (compose: ComposeNested) => {
const { composeType, appName, sourceType } = compose; const { composeType, appName, sourceType } = compose;
if (compose.command) { if (compose.command) {
return `${sanitizeCommand(compose.command)}`; return `${sanitizeCommand(compose.command)}`;
} }
const path = const path =
sourceType === "raw" ? "docker-compose.yml" : compose.composePath; sourceType === "raw" ? "docker-compose.yml" : compose.composePath;
let command = ""; let command = "";
if (composeType === "docker-compose") { if (composeType === "docker-compose") {
command = `compose -p ${appName} -f ${path} up -d --build --remove-orphans`; command = `compose -p ${appName} -f ${path} up -d --build --remove-orphans`;
} else if (composeType === "stack") { } else if (composeType === "stack") {
command = `stack deploy -c ${path} ${appName} --prune --with-registry-auth`; command = `stack deploy -c ${path} ${appName} --prune --with-registry-auth`;
} }
return command; return command;
}; };
export const getCreateEnvFileCommand = (compose: ComposeNested) => { export const getCreateEnvFileCommand = (compose: ComposeNested) => {
const { COMPOSE_PATH } = paths(!!compose.serverId); const { COMPOSE_PATH } = paths(!!compose.serverId);
const { env, composePath, appName } = compose; const { env, composePath, appName } = compose;
const composeFilePath = const composeFilePath =
join(COMPOSE_PATH, appName, "code", composePath) || join(COMPOSE_PATH, appName, "code", composePath) ||
join(COMPOSE_PATH, appName, "code", "docker-compose.yml"); join(COMPOSE_PATH, appName, "code", "docker-compose.yml");
const envFilePath = join(dirname(composeFilePath), ".env"); const envFilePath = join(dirname(composeFilePath), ".env");
let envContent = `APP_NAME=${appName}\n`; let envContent = `APP_NAME=${appName}\n`;
envContent += env || ""; envContent += env || "";
if (!envContent.includes("DOCKER_CONFIG")) { if (!envContent.includes("DOCKER_CONFIG")) {
envContent += "\nDOCKER_CONFIG=/root/.docker"; envContent += "\nDOCKER_CONFIG=/root/.docker";
} }
if (compose.randomize) { if (compose.randomize) {
envContent += `\nCOMPOSE_PREFIX=${compose.suffix}`; envContent += `\nCOMPOSE_PREFIX=${compose.suffix}`;
} }
const envFileContent = prepareEnvironmentVariables( const envFileContent = prepareEnvironmentVariables(
envContent, envContent,
compose.environment.project.env, compose.environment.project.env,
compose.environment.env, compose.environment.env,
).join("\n"); ).join("\n");
const encodedContent = encodeBase64(envFileContent); const encodedContent = encodeBase64(envFileContent);
return ` return `
touch ${envFilePath}; touch ${envFilePath};
echo "${encodedContent}" | base64 -d > "${envFilePath}"; echo "${encodedContent}" | base64 -d > "${envFilePath}";
`; `;
}; };
const getExportEnvCommand = (compose: ComposeNested) => { const getExportEnvCommand = (compose: ComposeNested) => {
if (compose.composeType !== "stack") return ""; if (compose.composeType !== "stack") return "";
const envVars = getEnvironmentVariablesObject( const envVars = getEnvironmentVariablesObject(
compose.env, compose.env,
compose.environment.project.env, compose.environment.project.env,
compose.environment.env, compose.environment.env,
); );
const exports = Object.entries(envVars) const exports = Object.entries(envVars)
.map(([key, value]) => `${key}=${quote([value])}`) .map(([key, value]) => `${key}=${quote([value])}`)
.join(" "); .join(" ");
return exports ? `${exports}` : ""; return exports ? `${exports}` : "";
}; };
@@ -1,90 +1,90 @@
import { import {
getEnvironmentVariablesObject, getEnvironmentVariablesObject,
prepareEnvironmentVariablesForShell, prepareEnvironmentVariablesForShell,
} from "@dokploy/server/utils/docker/utils"; } from "@dokploy/server/utils/docker/utils";
import { quote } from "shell-quote"; import { quote } from "shell-quote";
import { import {
getBuildAppDirectory, getBuildAppDirectory,
getDockerContextPath, getDockerContextPath,
} from "../filesystem/directory"; } from "../filesystem/directory";
import type { ApplicationNested } from "."; import type { ApplicationNested } from ".";
import { createEnvFileCommand } from "./utils"; import { createEnvFileCommand } from "./utils";
export const getDockerCommand = (application: ApplicationNested) => { export const getDockerCommand = (application: ApplicationNested) => {
const { const {
appName, appName,
env, env,
publishDirectory, publishDirectory,
buildArgs, buildArgs,
buildSecrets, buildSecrets,
dockerBuildStage, dockerBuildStage,
cleanCache, cleanCache,
createEnvFile, createEnvFile,
} = application; } = application;
const dockerFilePath = getBuildAppDirectory(application); const dockerFilePath = getBuildAppDirectory(application);
try { try {
const image = `${appName}`; const image = `${appName}`;
const defaultContextPath = const defaultContextPath =
dockerFilePath.substring(0, dockerFilePath.lastIndexOf("/") + 1) || "."; dockerFilePath.substring(0, dockerFilePath.lastIndexOf("/") + 1) || ".";
const dockerContextPath = const dockerContextPath =
getDockerContextPath(application) || defaultContextPath; getDockerContextPath(application) || defaultContextPath;
const commandArgs = ["build", "-t", image, "-f", dockerFilePath, "."]; const commandArgs = ["build", "-t", image, "-f", dockerFilePath, "."];
if (dockerBuildStage) { if (dockerBuildStage) {
commandArgs.push("--target", dockerBuildStage); commandArgs.push("--target", dockerBuildStage);
} }
if (cleanCache) { if (cleanCache) {
commandArgs.push("--no-cache"); commandArgs.push("--no-cache");
} }
const args = prepareEnvironmentVariablesForShell( const args = prepareEnvironmentVariablesForShell(
buildArgs, buildArgs,
application.environment.project.env, application.environment.project.env,
application.environment.env, application.environment.env,
); );
for (const arg of args) { for (const arg of args) {
commandArgs.push("--build-arg", arg); commandArgs.push("--build-arg", arg);
} }
const secrets = getEnvironmentVariablesObject( const secrets = getEnvironmentVariablesObject(
buildSecrets, buildSecrets,
application.environment.project.env, application.environment.project.env,
application.environment.env, application.environment.env,
); );
const joinedSecrets = Object.entries(secrets) const joinedSecrets = Object.entries(secrets)
.map(([key, value]) => `${key}=${quote([value])}`) .map(([key, value]) => `${key}=${quote([value])}`)
.join(" "); .join(" ");
/* /*
Do not generate an environment file when publishDirectory is specified, Do not generate an environment file when publishDirectory is specified,
as it could be publicly exposed. as it could be publicly exposed.
Also respect the createEnvFile flag. Also respect the createEnvFile flag.
*/ */
let command = ""; let command = "";
if (!publishDirectory && createEnvFile) { if (!publishDirectory && createEnvFile) {
command += createEnvFileCommand( command += createEnvFileCommand(
dockerFilePath, dockerFilePath,
env, env,
application.environment.project.env, application.environment.project.env,
application.environment.env, application.environment.env,
); );
} }
for (const key in secrets) { for (const key in secrets) {
// Although buildx is smart enough to know we may be referring to an environment variable name, // Although buildx is smart enough to know we may be referring to an environment variable name,
// we still make sure it doesn't fall back to `type=file`. // we still make sure it doesn't fall back to `type=file`.
// See: https://docs.docker.com/reference/cli/docker/buildx/build/#secret // See: https://docs.docker.com/reference/cli/docker/buildx/build/#secret
commandArgs.push("--secret", `type=env,id=${key}`); commandArgs.push("--secret", `type=env,id=${key}`);
} }
command += ` command += `
echo "Building ${appName}" ; echo "Building ${appName}" ;
cd ${dockerContextPath} || { cd ${dockerContextPath} || {
echo "❌ The path ${dockerContextPath} does not exist" ; echo "❌ The path ${dockerContextPath} does not exist" ;
@@ -98,8 +98,8 @@ ${joinedSecrets} docker ${commandArgs.join(" ")} || {
echo "✅ Docker build completed." ; echo "✅ Docker build completed." ;
`; `;
return command; return command;
} catch (error) { } catch (error) {
throw error; throw error;
} }
}; };