import { addNewService, checkServiceAccess, clearOldDeployments, createApplication, deleteAllMiddlewares, findApplicationById, findEnvironmentById, findGitProviderById, findProjectById, getApplicationStats, IS_CLOUD, mechanizeDockerContainer, readConfig, readRemoteConfig, removeDeployments, removeDirectoryCode, removeMonitoringDirectory, removeService, removeTraefikConfig, startService, startServiceRemote, stopService, stopServiceRemote, unzipDrop, updateApplication, updateApplicationStatus, updateDeploymentStatus, writeConfig, writeConfigRemote, // uploadFileSchema } from "@dokploy/server"; import { TRPCError } from "@trpc/server"; import { eq } from "drizzle-orm"; import { nanoid } from "nanoid"; import { z } from "zod"; import { createTRPCRouter, protectedProcedure, uploadProcedure, } from "@/server/api/trpc"; import { db } from "@/server/db"; import { apiCreateApplication, apiDeployApplication, apiFindMonitoringStats, apiFindOneApplication, apiRedeployApplication, apiReloadApplication, apiSaveBitbucketProvider, apiSaveBuildType, apiSaveDockerProvider, apiSaveEnvironmentVariables, apiSaveGiteaProvider, apiSaveGithubProvider, apiSaveGitlabProvider, apiSaveGitProvider, apiUpdateApplication, applications, } from "@/server/db/schema"; import { deploymentWorker } from "@/server/queues/deployments-queue"; import type { DeploymentJob } from "@/server/queues/queue-types"; import { cleanQueuesByApplication, getJobsByApplicationId, killDockerBuild, myQueue, } from "@/server/queues/queueSetup"; import { cancelDeployment, deploy } from "@/server/utils/deploy"; import { uploadFileSchema } from "@/utils/schema"; export const applicationRouter = createTRPCRouter({ create: protectedProcedure .input(apiCreateApplication) .mutation(async ({ input, ctx }) => { try { // Get project from environment const environment = await findEnvironmentById(input.environmentId); const project = await findProjectById(environment.projectId); if (ctx.user.role === "member") { await checkServiceAccess( ctx.user.id, project.projectId, ctx.session.activeOrganizationId, "create", ); } if (IS_CLOUD && !input.serverId) { throw new TRPCError({ code: "UNAUTHORIZED", message: "You need to use a server to create an application", }); } if (project.organizationId !== ctx.session.activeOrganizationId) { throw new TRPCError({ code: "UNAUTHORIZED", message: "You are not authorized to access this project", }); } const newApplication = await createApplication(input); if (ctx.user.role === "member") { await addNewService( ctx.user.id, newApplication.applicationId, project.organizationId, ); } return newApplication; } catch (error: unknown) { console.log("error", error); if (error instanceof TRPCError) { throw error; } throw new TRPCError({ code: "BAD_REQUEST", message: "Error creating the application", cause: error, }); } }), one: protectedProcedure .input(apiFindOneApplication) .query(async ({ input, ctx }) => { if (ctx.user.role === "member") { await checkServiceAccess( ctx.user.id, input.applicationId, ctx.session.activeOrganizationId, "access", ); } const application = await findApplicationById(input.applicationId); if ( application.environment.project.organizationId !== ctx.session.activeOrganizationId ) { throw new TRPCError({ code: "UNAUTHORIZED", message: "You are not authorized to access this application", }); } let hasGitProviderAccess = true; let unauthorizedProvider: string | null = null; const getGitProviderId = () => { switch (application.sourceType) { case "github": return application.github?.gitProviderId; case "gitlab": return application.gitlab?.gitProviderId; case "bitbucket": return application.bitbucket?.gitProviderId; case "gitea": return application.gitea?.gitProviderId; default: return null; } }; const gitProviderId = getGitProviderId(); if (gitProviderId) { try { const gitProvider = await findGitProviderById(gitProviderId); if (gitProvider.userId !== ctx.session.userId) { hasGitProviderAccess = false; unauthorizedProvider = application.sourceType; } } catch { hasGitProviderAccess = false; unauthorizedProvider = application.sourceType; } } return { ...application, hasGitProviderAccess, unauthorizedProvider, }; }), reload: protectedProcedure .input(apiReloadApplication) .mutation(async ({ input, ctx }) => { const application = await findApplicationById(input.applicationId); try { if ( application.environment.project.organizationId !== ctx.session.activeOrganizationId ) { throw new TRPCError({ code: "UNAUTHORIZED", message: "You are not authorized to reload this application", }); } await updateApplicationStatus(input.applicationId, "idle"); await mechanizeDockerContainer(application); await updateApplicationStatus(input.applicationId, "done"); return true; } catch (error) { await updateApplicationStatus(input.applicationId, "error"); throw new TRPCError({ code: "INTERNAL_SERVER_ERROR", message: "Error reloading application", cause: error, }); } }), delete: protectedProcedure .input(apiFindOneApplication) .mutation(async ({ input, ctx }) => { if (ctx.user.role === "member") { await checkServiceAccess( ctx.user.id, input.applicationId, ctx.session.activeOrganizationId, "delete", ); } const application = await findApplicationById(input.applicationId); if ( application.environment.project.organizationId !== ctx.session.activeOrganizationId ) { throw new TRPCError({ code: "UNAUTHORIZED", message: "You are not authorized to delete this application", }); } const result = await db .delete(applications) .where(eq(applications.applicationId, input.applicationId)) .returning(); if (!IS_CLOUD) { const queueJobs = await getJobsByApplicationId(input.applicationId); for (const job of queueJobs) { if (job.id) { deploymentWorker.cancelJob(job.id, "User requested cancellation"); } } } const cleanupOperations = [ async () => await deleteAllMiddlewares(application), async () => await removeDeployments(application), async () => await removeDirectoryCode(application.appName, application.serverId), async () => await removeMonitoringDirectory( application.appName, application.serverId, ), async () => await removeTraefikConfig(application.appName, application.serverId), async () => await removeService(application?.appName, application.serverId), ]; for (const operation of cleanupOperations) { try { await operation(); } catch (_) {} } return application; }), stop: protectedProcedure .input(apiFindOneApplication) .mutation(async ({ input, ctx }) => { const service = await findApplicationById(input.applicationId); if ( service.environment.project.organizationId !== ctx.session.activeOrganizationId ) { throw new TRPCError({ code: "UNAUTHORIZED", message: "You are not authorized to stop this application", }); } if (service.serverId) { await stopServiceRemote(service.serverId, service.appName); } else { await stopService(service.appName); } await updateApplicationStatus(input.applicationId, "idle"); return service; }), start: protectedProcedure .input(apiFindOneApplication) .mutation(async ({ input, ctx }) => { const service = await findApplicationById(input.applicationId); if ( service.environment.project.organizationId !== ctx.session.activeOrganizationId ) { throw new TRPCError({ code: "UNAUTHORIZED", message: "You are not authorized to start this application", }); } if (service.serverId) { await startServiceRemote(service.serverId, service.appName); } else { await startService(service.appName); } await updateApplicationStatus(input.applicationId, "done"); return service; }), redeploy: protectedProcedure .input(apiRedeployApplication) .mutation(async ({ input, ctx }) => { const application = await findApplicationById(input.applicationId); if ( application.environment.project.organizationId !== ctx.session.activeOrganizationId ) { throw new TRPCError({ code: "UNAUTHORIZED", message: "You are not authorized to redeploy this application", }); } const jobData: DeploymentJob = { applicationId: input.applicationId, titleLog: input.title || "Rebuild deployment", descriptionLog: input.description || "", type: "redeploy", applicationType: "application", server: !!application.serverId, }; if (IS_CLOUD && application.serverId) { jobData.serverId = application.serverId; deploy(jobData).catch((error) => { console.error("Background deployment failed:", error); }); return true; } await myQueue.add( "deployments", { ...jobData }, { removeOnComplete: true, removeOnFail: true, }, ); }), saveEnvironment: protectedProcedure .input(apiSaveEnvironmentVariables) .mutation(async ({ input, ctx }) => { const application = await findApplicationById(input.applicationId); if ( application.environment.project.organizationId !== ctx.session.activeOrganizationId ) { throw new TRPCError({ code: "UNAUTHORIZED", message: "You are not authorized to save this environment", }); } await updateApplication(input.applicationId, { env: input.env, buildArgs: input.buildArgs, buildSecrets: input.buildSecrets, createEnvFile: input.createEnvFile, }); return true; }), saveBuildType: protectedProcedure .input(apiSaveBuildType) .mutation(async ({ input, ctx }) => { const application = await findApplicationById(input.applicationId); if ( application.environment.project.organizationId !== ctx.session.activeOrganizationId ) { throw new TRPCError({ code: "UNAUTHORIZED", message: "You are not authorized to save this build type", }); } await updateApplication(input.applicationId, { buildType: input.buildType, dockerfile: input.dockerfile, publishDirectory: input.publishDirectory, dockerContextPath: input.dockerContextPath, dockerBuildStage: input.dockerBuildStage, herokuVersion: input.herokuVersion, isStaticSpa: input.isStaticSpa, railpackVersion: input.railpackVersion, }); return true; }), saveGithubProvider: protectedProcedure .input(apiSaveGithubProvider) .mutation(async ({ input, ctx }) => { const application = await findApplicationById(input.applicationId); if ( application.environment.project.organizationId !== ctx.session.activeOrganizationId ) { throw new TRPCError({ code: "UNAUTHORIZED", message: "You are not authorized to save this github provider", }); } await updateApplication(input.applicationId, { repository: input.repository, branch: input.branch, sourceType: "github", owner: input.owner, buildPath: input.buildPath, applicationStatus: "idle", githubId: input.githubId, watchPaths: input.watchPaths, triggerType: input.triggerType, enableSubmodules: input.enableSubmodules, }); return true; }), saveGitlabProvider: protectedProcedure .input(apiSaveGitlabProvider) .mutation(async ({ input, ctx }) => { const application = await findApplicationById(input.applicationId); if ( application.environment.project.organizationId !== ctx.session.activeOrganizationId ) { throw new TRPCError({ code: "UNAUTHORIZED", message: "You are not authorized to save this gitlab provider", }); } await updateApplication(input.applicationId, { gitlabRepository: input.gitlabRepository, gitlabOwner: input.gitlabOwner, gitlabBranch: input.gitlabBranch, gitlabBuildPath: input.gitlabBuildPath, sourceType: "gitlab", applicationStatus: "idle", gitlabId: input.gitlabId, gitlabProjectId: input.gitlabProjectId, gitlabPathNamespace: input.gitlabPathNamespace, watchPaths: input.watchPaths, enableSubmodules: input.enableSubmodules, }); return true; }), saveBitbucketProvider: protectedProcedure .input(apiSaveBitbucketProvider) .mutation(async ({ input, ctx }) => { const application = await findApplicationById(input.applicationId); if ( application.environment.project.organizationId !== ctx.session.activeOrganizationId ) { throw new TRPCError({ code: "UNAUTHORIZED", message: "You are not authorized to save this bitbucket provider", }); } await updateApplication(input.applicationId, { bitbucketRepository: input.bitbucketRepository, bitbucketRepositorySlug: input.bitbucketRepositorySlug, bitbucketOwner: input.bitbucketOwner, bitbucketBranch: input.bitbucketBranch, bitbucketBuildPath: input.bitbucketBuildPath, sourceType: "bitbucket", applicationStatus: "idle", bitbucketId: input.bitbucketId, watchPaths: input.watchPaths, enableSubmodules: input.enableSubmodules, }); return true; }), saveGiteaProvider: protectedProcedure .input(apiSaveGiteaProvider) .mutation(async ({ input, ctx }) => { const application = await findApplicationById(input.applicationId); if ( application.environment.project.organizationId !== ctx.session.activeOrganizationId ) { throw new TRPCError({ code: "UNAUTHORIZED", message: "You are not authorized to save this gitea provider", }); } await updateApplication(input.applicationId, { giteaRepository: input.giteaRepository, giteaOwner: input.giteaOwner, giteaBranch: input.giteaBranch, giteaBuildPath: input.giteaBuildPath, sourceType: "gitea", applicationStatus: "idle", giteaId: input.giteaId, watchPaths: input.watchPaths, enableSubmodules: input.enableSubmodules, }); return true; }), saveDockerProvider: protectedProcedure .input(apiSaveDockerProvider) .mutation(async ({ input, ctx }) => { const application = await findApplicationById(input.applicationId); if ( application.environment.project.organizationId !== ctx.session.activeOrganizationId ) { throw new TRPCError({ code: "UNAUTHORIZED", message: "You are not authorized to save this docker provider", }); } await updateApplication(input.applicationId, { dockerImage: input.dockerImage, username: input.username, password: input.password, sourceType: "docker", applicationStatus: "idle", registryUrl: input.registryUrl, }); return true; }), saveGitProvider: protectedProcedure .input(apiSaveGitProvider) .mutation(async ({ input, ctx }) => { const application = await findApplicationById(input.applicationId); if ( application.environment.project.organizationId !== ctx.session.activeOrganizationId ) { throw new TRPCError({ code: "UNAUTHORIZED", message: "You are not authorized to save this git provider", }); } await updateApplication(input.applicationId, { customGitBranch: input.customGitBranch, customGitBuildPath: input.customGitBuildPath, customGitUrl: input.customGitUrl, customGitSSHKeyId: input.customGitSSHKeyId, sourceType: "git", applicationStatus: "idle", watchPaths: input.watchPaths, enableSubmodules: input.enableSubmodules, }); return true; }), disconnectGitProvider: protectedProcedure .input(apiFindOneApplication) .mutation(async ({ input, ctx }) => { const application = await findApplicationById(input.applicationId); if ( application.environment.project.organizationId !== ctx.session.activeOrganizationId ) { throw new TRPCError({ code: "UNAUTHORIZED", message: "You are not authorized to disconnect this git provider", }); } // Reset all git provider related fields await updateApplication(input.applicationId, { // GitHub fields repository: null, branch: null, owner: null, buildPath: "/", githubId: null, triggerType: "push", // GitLab fields gitlabRepository: null, gitlabOwner: null, gitlabBranch: null, gitlabBuildPath: null, gitlabId: null, gitlabProjectId: null, gitlabPathNamespace: null, // Bitbucket fields bitbucketRepository: null, bitbucketOwner: null, bitbucketBranch: null, bitbucketBuildPath: null, bitbucketId: null, // Gitea fields giteaRepository: null, giteaOwner: null, giteaBranch: null, giteaBuildPath: null, giteaId: null, // Custom Git fields customGitBranch: null, customGitBuildPath: null, customGitUrl: null, customGitSSHKeyId: null, // Common fields sourceType: "github", // Reset to default applicationStatus: "idle", watchPaths: null, enableSubmodules: false, }); return true; }), markRunning: protectedProcedure .input(apiFindOneApplication) .mutation(async ({ input, ctx }) => { const application = await findApplicationById(input.applicationId); if ( application.environment.project.organizationId !== ctx.session.activeOrganizationId ) { throw new TRPCError({ code: "UNAUTHORIZED", message: "You are not authorized to mark this application as running", }); } await updateApplicationStatus(input.applicationId, "running"); }), update: protectedProcedure .input(apiUpdateApplication) .mutation(async ({ input, ctx }) => { const application = await findApplicationById(input.applicationId); if ( application.environment.project.organizationId !== ctx.session.activeOrganizationId ) { throw new TRPCError({ code: "UNAUTHORIZED", message: "You are not authorized to update this application", }); } const { applicationId, ...rest } = input; const updateApp = await updateApplication(applicationId, { ...rest, }); if (!updateApp) { throw new TRPCError({ code: "BAD_REQUEST", message: "Error updating application", }); } return true; }), refreshToken: protectedProcedure .input(apiFindOneApplication) .mutation(async ({ input, ctx }) => { const application = await findApplicationById(input.applicationId); if ( application.environment.project.organizationId !== ctx.session.activeOrganizationId ) { throw new TRPCError({ code: "UNAUTHORIZED", message: "You are not authorized to refresh this application", }); } await updateApplication(input.applicationId, { refreshToken: nanoid(), }); return true; }), deploy: protectedProcedure .input(apiDeployApplication) .mutation(async ({ input, ctx }) => { const application = await findApplicationById(input.applicationId); if ( application.environment.project.organizationId !== ctx.session.activeOrganizationId ) { throw new TRPCError({ code: "UNAUTHORIZED", message: "You are not authorized to deploy this application", }); } const jobData: DeploymentJob = { applicationId: input.applicationId, titleLog: input.title || "Manual deployment", descriptionLog: input.description || "", type: "deploy", applicationType: "application", server: !!application.serverId, }; if (IS_CLOUD && application.serverId) { jobData.serverId = application.serverId; deploy(jobData).catch((error) => { console.error("Background deployment failed:", error); }); return true; } await myQueue.add( "deployments", { ...jobData }, { removeOnComplete: true, removeOnFail: true, }, ); }), cleanQueues: protectedProcedure .input(apiFindOneApplication) .mutation(async ({ input, ctx }) => { const application = await findApplicationById(input.applicationId); if ( application.environment.project.organizationId !== ctx.session.activeOrganizationId ) { throw new TRPCError({ code: "UNAUTHORIZED", message: "You are not authorized to clean this application", }); } await cleanQueuesByApplication(input.applicationId); }), clearDeployments: protectedProcedure .input(apiFindOneApplication) .mutation(async ({ input, ctx }) => { const application = await findApplicationById(input.applicationId); if ( application.environment.project.organizationId !== ctx.session.activeOrganizationId ) { throw new TRPCError({ code: "UNAUTHORIZED", message: "You are not authorized to clear deployments for this application", }); } await clearOldDeployments(application.appName, application.serverId); return true; }), killBuild: protectedProcedure .input(apiFindOneApplication) .mutation(async ({ input, ctx }) => { const application = await findApplicationById(input.applicationId); if ( application.environment.project.organizationId !== ctx.session.activeOrganizationId ) { throw new TRPCError({ code: "UNAUTHORIZED", message: "You are not authorized to kill this build", }); } await killDockerBuild("application", application.serverId); }), readTraefikConfig: protectedProcedure .input(apiFindOneApplication) .query(async ({ input, ctx }) => { const application = await findApplicationById(input.applicationId); if ( application.environment.project.organizationId !== ctx.session.activeOrganizationId ) { throw new TRPCError({ code: "UNAUTHORIZED", message: "You are not authorized to read this application", }); } let traefikConfig = null; if (application.serverId) { traefikConfig = await readRemoteConfig( application.serverId, application.appName, ); } else { traefikConfig = readConfig(application.appName); } return traefikConfig; }), dropDeployment: protectedProcedure .meta({ openapi: { path: "/drop-deployment", method: "POST", override: true, enabled: false, }, }) .use(uploadProcedure) .input(uploadFileSchema) .mutation(async ({ input, ctx }) => { const zipFile = input.zip; const app = await findApplicationById(input.applicationId as string); if ( app.environment.project.organizationId !== ctx.session.activeOrganizationId ) { throw new TRPCError({ code: "UNAUTHORIZED", message: "You are not authorized to deploy this application", }); } await updateApplication(input.applicationId as string, { sourceType: "drop", dropBuildPath: input.dropBuildPath || "", }); await unzipDrop(zipFile, app); const jobData: DeploymentJob = { applicationId: app.applicationId, titleLog: "Manual deployment", descriptionLog: "", type: "deploy", applicationType: "application", server: !!app.serverId, }; if (IS_CLOUD && app.serverId) { jobData.serverId = app.serverId; deploy(jobData).catch((error) => { console.error("Background deployment failed:", error); }); return true; } await myQueue.add( "deployments", { ...jobData }, { removeOnComplete: true, removeOnFail: true, }, ); return true; }), updateTraefikConfig: protectedProcedure .input(z.object({ applicationId: z.string(), traefikConfig: z.string() })) .mutation(async ({ input, ctx }) => { const application = await findApplicationById(input.applicationId); if ( application.environment.project.organizationId !== ctx.session.activeOrganizationId ) { throw new TRPCError({ code: "UNAUTHORIZED", message: "You are not authorized to update this application", }); } if (application.serverId) { await writeConfigRemote( application.serverId, application.appName, input.traefikConfig, ); } else { writeConfig(application.appName, input.traefikConfig); } return true; }), readAppMonitoring: protectedProcedure .input(apiFindMonitoringStats) .query(async ({ input }) => { if (IS_CLOUD) { throw new TRPCError({ code: "UNAUTHORIZED", message: "Functionality not available in cloud version", }); } const stats = await getApplicationStats(input.appName); return stats; }), move: protectedProcedure .input( z.object({ applicationId: z.string(), targetEnvironmentId: z.string(), }), ) .mutation(async ({ input, ctx }) => { const application = await findApplicationById(input.applicationId); if ( application.environment.project.organizationId !== ctx.session.activeOrganizationId ) { throw new TRPCError({ code: "UNAUTHORIZED", message: "You are not authorized to move this application", }); } const targetEnvironment = await findEnvironmentById( input.targetEnvironmentId, ); if ( targetEnvironment.project.organizationId !== ctx.session.activeOrganizationId ) { throw new TRPCError({ code: "UNAUTHORIZED", message: "You are not authorized to move to this environment", }); } // Update the application's projectId const updatedApplication = await db .update(applications) .set({ environmentId: input.targetEnvironmentId, }) .where(eq(applications.applicationId, input.applicationId)) .returning() .then((res) => res[0]); if (!updatedApplication) { throw new TRPCError({ code: "INTERNAL_SERVER_ERROR", message: "Failed to move application", }); } return updatedApplication; }), cancelDeployment: protectedProcedure .input(apiFindOneApplication) .mutation(async ({ input, ctx }) => { const application = await findApplicationById(input.applicationId); if ( application.environment.project.organizationId !== ctx.session.activeOrganizationId ) { throw new TRPCError({ code: "UNAUTHORIZED", message: "You are not authorized to cancel this deployment", }); } if (IS_CLOUD && application.serverId) { try { await updateApplicationStatus(input.applicationId, "idle"); if (application.deployments[0]) { await updateDeploymentStatus( application.deployments[0].deploymentId, "done", ); } await cancelDeployment({ applicationId: input.applicationId, applicationType: "application", }); return { success: true, message: "Deployment cancellation requested", }; } catch (error) { throw new TRPCError({ code: "INTERNAL_SERVER_ERROR", message: error instanceof Error ? error.message : "Failed to cancel deployment", }); } } throw new TRPCError({ code: "BAD_REQUEST", message: "Deployment cancellation only available in cloud version", }); }), });