mirror of
https://github.com/dokploy/dokploy.git
synced 2026-06-13 19:09:49 +00:00
e9a0932b23
* fix: use canEditDeployGitSource for git provider access on existing deploys Replaces the simple userId ownership check with a new canEditDeployGitSource function that correctly handles all role/sharing scenarios. Owner always has access; admin and member only if they own the provider or it is shared with the org — being assigned via accessedGitProviders (enterprise) only grants permission to connect new deploys, not to edit the git source of existing ones. Adds 26 unit tests covering owner, admin, member (with/without enterprise license), shared providers, and the key regression case from issue #4469. * [autofix.ci] apply automated fixes --------- Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
1144 lines
32 KiB
TypeScript
1144 lines
32 KiB
TypeScript
import {
|
|
clearOldDeployments,
|
|
createApplication,
|
|
deleteAllMiddlewares,
|
|
findApplicationById,
|
|
findEnvironmentById,
|
|
findProjectById,
|
|
getAccessibleServerIds,
|
|
getApplicationStats,
|
|
getContainerLogs,
|
|
getWebServerSettings,
|
|
IS_CLOUD,
|
|
mechanizeDockerContainer,
|
|
readConfig,
|
|
readRemoteConfig,
|
|
removeDeployments,
|
|
removeDirectoryCode,
|
|
removeMonitoringDirectory,
|
|
removeService,
|
|
removeTraefikConfig,
|
|
startService,
|
|
startServiceRemote,
|
|
stopService,
|
|
stopServiceRemote,
|
|
unzipDrop,
|
|
updateApplication,
|
|
updateApplicationStatus,
|
|
updateDeploymentStatus,
|
|
writeConfig,
|
|
writeConfigRemote,
|
|
} from "@dokploy/server";
|
|
import { db } from "@dokploy/server/db";
|
|
import { canEditDeployGitSource } from "@dokploy/server/services/git-provider";
|
|
import {
|
|
addNewService,
|
|
checkServiceAccess,
|
|
checkServicePermissionAndAccess,
|
|
findMemberByUserId,
|
|
} from "@dokploy/server/services/permission";
|
|
import { TRPCError } from "@trpc/server";
|
|
import { and, desc, eq, ilike, or, sql } from "drizzle-orm";
|
|
import { nanoid } from "nanoid";
|
|
import { z } from "zod";
|
|
import { zfd } from "zod-form-data";
|
|
import {
|
|
createTRPCRouter,
|
|
protectedProcedure,
|
|
withPermission,
|
|
} from "@/server/api/trpc";
|
|
import { audit } from "@/server/api/utils/audit";
|
|
import {
|
|
apiCreateApplication,
|
|
apiDeployApplication,
|
|
apiFindMonitoringStats,
|
|
apiFindOneApplication,
|
|
apiRedeployApplication,
|
|
apiReloadApplication,
|
|
apiSaveBitbucketProvider,
|
|
apiSaveBuildType,
|
|
apiSaveDockerProvider,
|
|
apiSaveEnvironmentVariables,
|
|
apiSaveGiteaProvider,
|
|
apiSaveGithubProvider,
|
|
apiSaveGitlabProvider,
|
|
apiSaveGitProvider,
|
|
apiUpdateApplication,
|
|
applications,
|
|
environments,
|
|
projects,
|
|
} 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";
|
|
|
|
export const applicationRouter = createTRPCRouter({
|
|
create: protectedProcedure
|
|
.input(apiCreateApplication)
|
|
.mutation(async ({ input, ctx }) => {
|
|
try {
|
|
const environment = await findEnvironmentById(input.environmentId);
|
|
const project = await findProjectById(environment.projectId);
|
|
|
|
await checkServiceAccess(ctx, project.projectId, "create");
|
|
|
|
const webServerSettings = await getWebServerSettings();
|
|
if (
|
|
(IS_CLOUD || webServerSettings?.remoteServersOnly) &&
|
|
!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",
|
|
});
|
|
}
|
|
|
|
if (input.serverId) {
|
|
const accessibleIds = await getAccessibleServerIds(ctx.session);
|
|
if (!accessibleIds.has(input.serverId)) {
|
|
throw new TRPCError({
|
|
code: "UNAUTHORIZED",
|
|
message: "You are not authorized to access this server",
|
|
});
|
|
}
|
|
}
|
|
|
|
const newApplication = await createApplication(input);
|
|
|
|
await addNewService(ctx, newApplication.applicationId);
|
|
await audit(ctx, {
|
|
action: "create",
|
|
resourceType: "service",
|
|
resourceId: newApplication.applicationId,
|
|
resourceName: newApplication.appName,
|
|
});
|
|
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 }) => {
|
|
await checkServiceAccess(ctx, input.applicationId, "read");
|
|
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) {
|
|
const canEdit = await canEditDeployGitSource(
|
|
gitProviderId,
|
|
ctx.session,
|
|
);
|
|
if (!canEdit) {
|
|
hasGitProviderAccess = false;
|
|
unauthorizedProvider = application.sourceType;
|
|
}
|
|
}
|
|
|
|
return {
|
|
...application,
|
|
hasGitProviderAccess,
|
|
unauthorizedProvider,
|
|
};
|
|
}),
|
|
|
|
reload: protectedProcedure
|
|
.input(apiReloadApplication)
|
|
.mutation(async ({ input, ctx }) => {
|
|
await checkServicePermissionAndAccess(ctx, input.applicationId, {
|
|
deployment: ["create"],
|
|
});
|
|
const application = await findApplicationById(input.applicationId);
|
|
|
|
try {
|
|
await updateApplicationStatus(input.applicationId, "idle");
|
|
await mechanizeDockerContainer(application);
|
|
await updateApplicationStatus(input.applicationId, "done");
|
|
await audit(ctx, {
|
|
action: "reload",
|
|
resourceType: "application",
|
|
resourceId: application.applicationId,
|
|
resourceName: application.appName,
|
|
});
|
|
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 }) => {
|
|
await checkServiceAccess(ctx, input.applicationId, "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 (_) {}
|
|
}
|
|
|
|
await audit(ctx, {
|
|
action: "delete",
|
|
resourceType: "service",
|
|
resourceId: application.applicationId,
|
|
resourceName: application.appName,
|
|
});
|
|
return application;
|
|
}),
|
|
|
|
stop: protectedProcedure
|
|
.input(apiFindOneApplication)
|
|
.mutation(async ({ input, ctx }) => {
|
|
await checkServicePermissionAndAccess(ctx, input.applicationId, {
|
|
deployment: ["create"],
|
|
});
|
|
const service = await findApplicationById(input.applicationId);
|
|
if (service.serverId) {
|
|
await stopServiceRemote(service.serverId, service.appName);
|
|
} else {
|
|
await stopService(service.appName);
|
|
}
|
|
await updateApplicationStatus(input.applicationId, "idle");
|
|
await audit(ctx, {
|
|
action: "stop",
|
|
resourceType: "application",
|
|
resourceId: service.applicationId,
|
|
resourceName: service.appName,
|
|
});
|
|
return service;
|
|
}),
|
|
|
|
start: protectedProcedure
|
|
.input(apiFindOneApplication)
|
|
.mutation(async ({ input, ctx }) => {
|
|
await checkServicePermissionAndAccess(ctx, input.applicationId, {
|
|
deployment: ["create"],
|
|
});
|
|
const service = await findApplicationById(input.applicationId);
|
|
if (service.serverId) {
|
|
await startServiceRemote(service.serverId, service.appName);
|
|
} else {
|
|
await startService(service.appName);
|
|
}
|
|
await updateApplicationStatus(input.applicationId, "done");
|
|
await audit(ctx, {
|
|
action: "start",
|
|
resourceType: "application",
|
|
resourceId: service.applicationId,
|
|
resourceName: service.appName,
|
|
});
|
|
return service;
|
|
}),
|
|
|
|
redeploy: protectedProcedure
|
|
.input(apiRedeployApplication)
|
|
.mutation(async ({ input, ctx }) => {
|
|
await checkServicePermissionAndAccess(ctx, input.applicationId, {
|
|
deployment: ["create"],
|
|
});
|
|
const application = await findApplicationById(input.applicationId);
|
|
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);
|
|
});
|
|
await audit(ctx, {
|
|
action: "rebuild",
|
|
resourceType: "application",
|
|
resourceId: application.applicationId,
|
|
resourceName: application.appName,
|
|
});
|
|
return true;
|
|
}
|
|
await myQueue.add(
|
|
"deployments",
|
|
{ ...jobData },
|
|
{
|
|
removeOnComplete: true,
|
|
removeOnFail: true,
|
|
},
|
|
);
|
|
await audit(ctx, {
|
|
action: "rebuild",
|
|
resourceType: "application",
|
|
resourceId: application.applicationId,
|
|
resourceName: application.appName,
|
|
});
|
|
}),
|
|
saveEnvironment: protectedProcedure
|
|
.input(apiSaveEnvironmentVariables)
|
|
.mutation(async ({ input, ctx }) => {
|
|
await checkServicePermissionAndAccess(ctx, input.applicationId, {
|
|
envVars: ["write"],
|
|
});
|
|
await updateApplication(input.applicationId, {
|
|
env: input.env,
|
|
buildArgs: input.buildArgs,
|
|
buildSecrets: input.buildSecrets,
|
|
createEnvFile: input.createEnvFile,
|
|
});
|
|
const application = await findApplicationById(input.applicationId);
|
|
await audit(ctx, {
|
|
action: "update",
|
|
resourceType: "application",
|
|
resourceId: application.applicationId,
|
|
resourceName: application.appName,
|
|
});
|
|
return true;
|
|
}),
|
|
saveBuildType: protectedProcedure
|
|
.input(apiSaveBuildType)
|
|
.mutation(async ({ input, ctx }) => {
|
|
await checkServicePermissionAndAccess(ctx, input.applicationId, {
|
|
service: ["create"],
|
|
});
|
|
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,
|
|
});
|
|
const application = await findApplicationById(input.applicationId);
|
|
await audit(ctx, {
|
|
action: "update",
|
|
resourceType: "application",
|
|
resourceId: application.applicationId,
|
|
resourceName: application.appName,
|
|
});
|
|
return true;
|
|
}),
|
|
saveGithubProvider: protectedProcedure
|
|
.input(apiSaveGithubProvider)
|
|
.mutation(async ({ input, ctx }) => {
|
|
await checkServicePermissionAndAccess(ctx, input.applicationId, {
|
|
service: ["create"],
|
|
});
|
|
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,
|
|
});
|
|
const application = await findApplicationById(input.applicationId);
|
|
await audit(ctx, {
|
|
action: "update",
|
|
resourceType: "application",
|
|
resourceId: application.applicationId,
|
|
resourceName: application.appName,
|
|
});
|
|
return true;
|
|
}),
|
|
saveGitlabProvider: protectedProcedure
|
|
.input(apiSaveGitlabProvider)
|
|
.mutation(async ({ input, ctx }) => {
|
|
await checkServicePermissionAndAccess(ctx, input.applicationId, {
|
|
service: ["create"],
|
|
});
|
|
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,
|
|
});
|
|
const application = await findApplicationById(input.applicationId);
|
|
await audit(ctx, {
|
|
action: "update",
|
|
resourceType: "application",
|
|
resourceId: application.applicationId,
|
|
resourceName: application.appName,
|
|
});
|
|
return true;
|
|
}),
|
|
saveBitbucketProvider: protectedProcedure
|
|
.input(apiSaveBitbucketProvider)
|
|
.mutation(async ({ input, ctx }) => {
|
|
await checkServicePermissionAndAccess(ctx, input.applicationId, {
|
|
service: ["create"],
|
|
});
|
|
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,
|
|
});
|
|
const application = await findApplicationById(input.applicationId);
|
|
await audit(ctx, {
|
|
action: "update",
|
|
resourceType: "application",
|
|
resourceId: application.applicationId,
|
|
resourceName: application.appName,
|
|
});
|
|
return true;
|
|
}),
|
|
saveGiteaProvider: protectedProcedure
|
|
.input(apiSaveGiteaProvider)
|
|
.mutation(async ({ input, ctx }) => {
|
|
await checkServicePermissionAndAccess(ctx, input.applicationId, {
|
|
service: ["create"],
|
|
});
|
|
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,
|
|
});
|
|
const application = await findApplicationById(input.applicationId);
|
|
await audit(ctx, {
|
|
action: "update",
|
|
resourceType: "application",
|
|
resourceId: application.applicationId,
|
|
resourceName: application.appName,
|
|
});
|
|
return true;
|
|
}),
|
|
saveDockerProvider: protectedProcedure
|
|
.input(apiSaveDockerProvider)
|
|
.mutation(async ({ input, ctx }) => {
|
|
await checkServicePermissionAndAccess(ctx, input.applicationId, {
|
|
service: ["create"],
|
|
});
|
|
await updateApplication(input.applicationId, {
|
|
dockerImage: input.dockerImage,
|
|
username: input.username,
|
|
password: input.password,
|
|
sourceType: "docker",
|
|
applicationStatus: "idle",
|
|
registryUrl: input.registryUrl,
|
|
});
|
|
const application = await findApplicationById(input.applicationId);
|
|
await audit(ctx, {
|
|
action: "update",
|
|
resourceType: "application",
|
|
resourceId: application.applicationId,
|
|
resourceName: application.appName,
|
|
});
|
|
return true;
|
|
}),
|
|
saveGitProvider: protectedProcedure
|
|
.input(apiSaveGitProvider)
|
|
.mutation(async ({ input, ctx }) => {
|
|
await checkServicePermissionAndAccess(ctx, input.applicationId, {
|
|
service: ["create"],
|
|
});
|
|
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,
|
|
});
|
|
const application = await findApplicationById(input.applicationId);
|
|
await audit(ctx, {
|
|
action: "update",
|
|
resourceType: "application",
|
|
resourceId: application.applicationId,
|
|
resourceName: application.appName,
|
|
});
|
|
return true;
|
|
}),
|
|
disconnectGitProvider: protectedProcedure
|
|
.input(apiFindOneApplication)
|
|
.mutation(async ({ input, ctx }) => {
|
|
await checkServicePermissionAndAccess(ctx, input.applicationId, {
|
|
service: ["create"],
|
|
});
|
|
await updateApplication(input.applicationId, {
|
|
repository: null,
|
|
branch: null,
|
|
owner: null,
|
|
buildPath: "/",
|
|
githubId: null,
|
|
triggerType: "push",
|
|
|
|
gitlabRepository: null,
|
|
gitlabOwner: null,
|
|
gitlabBranch: null,
|
|
gitlabBuildPath: null,
|
|
gitlabId: null,
|
|
gitlabProjectId: null,
|
|
gitlabPathNamespace: null,
|
|
|
|
bitbucketRepository: null,
|
|
bitbucketOwner: null,
|
|
bitbucketBranch: null,
|
|
bitbucketBuildPath: null,
|
|
bitbucketId: null,
|
|
|
|
giteaRepository: null,
|
|
giteaOwner: null,
|
|
giteaBranch: null,
|
|
giteaBuildPath: null,
|
|
giteaId: null,
|
|
|
|
customGitBranch: null,
|
|
customGitBuildPath: null,
|
|
customGitUrl: null,
|
|
customGitSSHKeyId: null,
|
|
|
|
sourceType: "github", // Reset to default
|
|
applicationStatus: "idle",
|
|
watchPaths: null,
|
|
enableSubmodules: false,
|
|
});
|
|
const application = await findApplicationById(input.applicationId);
|
|
await audit(ctx, {
|
|
action: "update",
|
|
resourceType: "application",
|
|
resourceId: application.applicationId,
|
|
resourceName: application.appName,
|
|
});
|
|
return true;
|
|
}),
|
|
markRunning: protectedProcedure
|
|
.input(apiFindOneApplication)
|
|
.mutation(async ({ input, ctx }) => {
|
|
await checkServicePermissionAndAccess(ctx, input.applicationId, {
|
|
deployment: ["create"],
|
|
});
|
|
await updateApplicationStatus(input.applicationId, "running");
|
|
const application = await findApplicationById(input.applicationId);
|
|
await audit(ctx, {
|
|
action: "deploy",
|
|
resourceType: "application",
|
|
resourceId: application.applicationId,
|
|
resourceName: application.appName,
|
|
});
|
|
}),
|
|
update: protectedProcedure
|
|
.input(apiUpdateApplication)
|
|
.mutation(async ({ input, ctx }) => {
|
|
await checkServicePermissionAndAccess(ctx, input.applicationId, {
|
|
service: ["create"],
|
|
});
|
|
|
|
if (input.buildServerId) {
|
|
const accessibleIds = await getAccessibleServerIds(ctx.session);
|
|
if (!accessibleIds.has(input.buildServerId)) {
|
|
throw new TRPCError({
|
|
code: "UNAUTHORIZED",
|
|
message: "You are not authorized to access this build server",
|
|
});
|
|
}
|
|
}
|
|
|
|
const { applicationId, ...rest } = input;
|
|
const updateApp = await updateApplication(applicationId, {
|
|
...rest,
|
|
});
|
|
|
|
if (!updateApp) {
|
|
throw new TRPCError({
|
|
code: "BAD_REQUEST",
|
|
message: "Error updating application",
|
|
});
|
|
}
|
|
await audit(ctx, {
|
|
action: "update",
|
|
resourceType: "application",
|
|
resourceId: updateApp.applicationId,
|
|
resourceName: updateApp.appName,
|
|
});
|
|
return true;
|
|
}),
|
|
refreshToken: protectedProcedure
|
|
.input(apiFindOneApplication)
|
|
.mutation(async ({ input, ctx }) => {
|
|
await checkServicePermissionAndAccess(ctx, input.applicationId, {
|
|
service: ["create"],
|
|
});
|
|
await updateApplication(input.applicationId, {
|
|
refreshToken: nanoid(),
|
|
});
|
|
const application = await findApplicationById(input.applicationId);
|
|
await audit(ctx, {
|
|
action: "update",
|
|
resourceType: "application",
|
|
resourceId: application.applicationId,
|
|
resourceName: application.appName,
|
|
});
|
|
return true;
|
|
}),
|
|
deploy: protectedProcedure
|
|
.input(apiDeployApplication)
|
|
.mutation(async ({ input, ctx }) => {
|
|
await checkServicePermissionAndAccess(ctx, input.applicationId, {
|
|
deployment: ["create"],
|
|
});
|
|
const application = await findApplicationById(input.applicationId);
|
|
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);
|
|
});
|
|
await audit(ctx, {
|
|
action: "deploy",
|
|
resourceType: "application",
|
|
resourceId: application.applicationId,
|
|
resourceName: application.appName,
|
|
});
|
|
return true;
|
|
}
|
|
await myQueue.add(
|
|
"deployments",
|
|
{ ...jobData },
|
|
{
|
|
removeOnComplete: true,
|
|
removeOnFail: true,
|
|
},
|
|
);
|
|
await audit(ctx, {
|
|
action: "deploy",
|
|
resourceType: "application",
|
|
resourceId: application.applicationId,
|
|
resourceName: application.appName,
|
|
});
|
|
}),
|
|
|
|
cleanQueues: protectedProcedure
|
|
.input(apiFindOneApplication)
|
|
.mutation(async ({ input, ctx }) => {
|
|
await checkServicePermissionAndAccess(ctx, input.applicationId, {
|
|
deployment: ["cancel"],
|
|
});
|
|
await cleanQueuesByApplication(input.applicationId);
|
|
}),
|
|
clearDeployments: protectedProcedure
|
|
.input(apiFindOneApplication)
|
|
.mutation(async ({ input, ctx }) => {
|
|
await checkServicePermissionAndAccess(ctx, input.applicationId, {
|
|
deployment: ["create"],
|
|
});
|
|
const application = await findApplicationById(input.applicationId);
|
|
await clearOldDeployments(application.appName, application.serverId);
|
|
await audit(ctx, {
|
|
action: "delete",
|
|
resourceType: "application",
|
|
resourceId: application.applicationId,
|
|
resourceName: application.appName,
|
|
});
|
|
return true;
|
|
}),
|
|
killBuild: protectedProcedure
|
|
.input(apiFindOneApplication)
|
|
.mutation(async ({ input, ctx }) => {
|
|
await checkServicePermissionAndAccess(ctx, input.applicationId, {
|
|
deployment: ["cancel"],
|
|
});
|
|
const application = await findApplicationById(input.applicationId);
|
|
await killDockerBuild("application", application.serverId);
|
|
await audit(ctx, {
|
|
action: "stop",
|
|
resourceType: "application",
|
|
resourceId: application.applicationId,
|
|
resourceName: application.appName,
|
|
});
|
|
}),
|
|
readTraefikConfig: protectedProcedure
|
|
.input(apiFindOneApplication)
|
|
.query(async ({ input, ctx }) => {
|
|
await checkServicePermissionAndAccess(ctx, input.applicationId, {
|
|
traefikFiles: ["read"],
|
|
});
|
|
const application = await findApplicationById(input.applicationId);
|
|
let traefikConfig = null;
|
|
if (application.serverId) {
|
|
traefikConfig = await readRemoteConfig(
|
|
application.serverId,
|
|
application.appName,
|
|
);
|
|
} else {
|
|
traefikConfig = readConfig(application.appName);
|
|
}
|
|
return traefikConfig;
|
|
}),
|
|
|
|
dropDeployment: protectedProcedure
|
|
.input(
|
|
zfd.formData({
|
|
applicationId: z.string(),
|
|
zip: zfd.file(),
|
|
dropBuildPath: z.string().optional(),
|
|
}),
|
|
)
|
|
.mutation(async ({ input, ctx }) => {
|
|
const zipFile = input.zip;
|
|
const applicationId = input.applicationId;
|
|
const dropBuildPath = input.dropBuildPath ?? null;
|
|
|
|
await checkServicePermissionAndAccess(ctx, applicationId, {
|
|
deployment: ["create"],
|
|
});
|
|
const app = await findApplicationById(applicationId);
|
|
|
|
await updateApplication(applicationId, {
|
|
sourceType: "drop",
|
|
dropBuildPath: 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,
|
|
},
|
|
);
|
|
await audit(ctx, {
|
|
action: "deploy",
|
|
resourceType: "application",
|
|
resourceId: app.applicationId,
|
|
resourceName: app.appName,
|
|
});
|
|
return true;
|
|
}),
|
|
updateTraefikConfig: protectedProcedure
|
|
.input(z.object({ applicationId: z.string(), traefikConfig: z.string() }))
|
|
.mutation(async ({ input, ctx }) => {
|
|
await checkServicePermissionAndAccess(ctx, input.applicationId, {
|
|
traefikFiles: ["write"],
|
|
});
|
|
const application = await findApplicationById(input.applicationId);
|
|
if (application.serverId) {
|
|
await writeConfigRemote(
|
|
application.serverId,
|
|
application.appName,
|
|
input.traefikConfig,
|
|
);
|
|
} else {
|
|
writeConfig(application.appName, input.traefikConfig);
|
|
}
|
|
await audit(ctx, {
|
|
action: "update",
|
|
resourceType: "application",
|
|
resourceId: application.applicationId,
|
|
resourceName: application.appName,
|
|
});
|
|
return true;
|
|
}),
|
|
readAppMonitoring: withPermission("monitoring", "read")
|
|
.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 }) => {
|
|
await checkServicePermissionAndAccess(ctx, input.applicationId, {
|
|
service: ["create"],
|
|
});
|
|
|
|
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",
|
|
});
|
|
}
|
|
await audit(ctx, {
|
|
action: "update",
|
|
resourceType: "application",
|
|
resourceId: updatedApplication.applicationId,
|
|
resourceName: updatedApplication.appName,
|
|
});
|
|
return updatedApplication;
|
|
}),
|
|
|
|
cancelDeployment: protectedProcedure
|
|
.input(apiFindOneApplication)
|
|
.mutation(async ({ input, ctx }) => {
|
|
await checkServicePermissionAndAccess(ctx, input.applicationId, {
|
|
deployment: ["cancel"],
|
|
});
|
|
const application = await findApplicationById(input.applicationId);
|
|
|
|
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",
|
|
});
|
|
await audit(ctx, {
|
|
action: "stop",
|
|
resourceType: "application",
|
|
resourceId: application.applicationId,
|
|
resourceName: application.appName,
|
|
});
|
|
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",
|
|
});
|
|
}),
|
|
|
|
search: protectedProcedure
|
|
.input(
|
|
z.object({
|
|
q: z.string().optional(),
|
|
name: z.string().optional(),
|
|
appName: z.string().optional(),
|
|
description: z.string().optional(),
|
|
repository: z.string().optional(),
|
|
owner: z.string().optional(),
|
|
dockerImage: z.string().optional(),
|
|
projectId: z.string().optional(),
|
|
environmentId: z.string().optional(),
|
|
limit: z.number().min(1).max(100).default(20),
|
|
offset: z.number().min(0).default(0),
|
|
}),
|
|
)
|
|
.query(async ({ ctx, input }) => {
|
|
const baseConditions = [
|
|
eq(projects.organizationId, ctx.session.activeOrganizationId),
|
|
];
|
|
|
|
if (input.projectId) {
|
|
baseConditions.push(eq(environments.projectId, input.projectId));
|
|
}
|
|
if (input.environmentId) {
|
|
baseConditions.push(
|
|
eq(applications.environmentId, input.environmentId),
|
|
);
|
|
}
|
|
|
|
if (input.q?.trim()) {
|
|
const term = `%${input.q.trim()}%`;
|
|
baseConditions.push(
|
|
or(
|
|
ilike(applications.name, term),
|
|
ilike(applications.appName, term),
|
|
ilike(applications.description ?? "", term),
|
|
ilike(applications.repository ?? "", term),
|
|
ilike(applications.owner ?? "", term),
|
|
ilike(applications.dockerImage ?? "", term),
|
|
)!,
|
|
);
|
|
}
|
|
|
|
if (input.name?.trim()) {
|
|
baseConditions.push(ilike(applications.name, `%${input.name.trim()}%`));
|
|
}
|
|
if (input.appName?.trim()) {
|
|
baseConditions.push(
|
|
ilike(applications.appName, `%${input.appName.trim()}%`),
|
|
);
|
|
}
|
|
if (input.description?.trim()) {
|
|
baseConditions.push(
|
|
ilike(
|
|
applications.description ?? "",
|
|
`%${input.description.trim()}%`,
|
|
),
|
|
);
|
|
}
|
|
if (input.repository?.trim()) {
|
|
baseConditions.push(
|
|
ilike(applications.repository ?? "", `%${input.repository.trim()}%`),
|
|
);
|
|
}
|
|
if (input.owner?.trim()) {
|
|
baseConditions.push(
|
|
ilike(applications.owner ?? "", `%${input.owner.trim()}%`),
|
|
);
|
|
}
|
|
if (input.dockerImage?.trim()) {
|
|
baseConditions.push(
|
|
ilike(
|
|
applications.dockerImage ?? "",
|
|
`%${input.dockerImage.trim()}%`,
|
|
),
|
|
);
|
|
}
|
|
|
|
const { accessedServices } = await findMemberByUserId(
|
|
ctx.user.id,
|
|
ctx.session.activeOrganizationId,
|
|
);
|
|
if (accessedServices.length === 0) return { items: [], total: 0 };
|
|
baseConditions.push(
|
|
sql`${applications.applicationId} IN (${sql.join(
|
|
accessedServices.map((id) => sql`${id}`),
|
|
sql`, `,
|
|
)})`,
|
|
);
|
|
|
|
const where = and(...baseConditions);
|
|
|
|
const [items, countResult] = await Promise.all([
|
|
db
|
|
.select({
|
|
applicationId: applications.applicationId,
|
|
name: applications.name,
|
|
appName: applications.appName,
|
|
description: applications.description,
|
|
environmentId: applications.environmentId,
|
|
applicationStatus: applications.applicationStatus,
|
|
sourceType: applications.sourceType,
|
|
createdAt: applications.createdAt,
|
|
})
|
|
.from(applications)
|
|
.innerJoin(
|
|
environments,
|
|
eq(applications.environmentId, environments.environmentId),
|
|
)
|
|
.innerJoin(projects, eq(environments.projectId, projects.projectId))
|
|
.where(where)
|
|
.orderBy(desc(applications.createdAt))
|
|
.limit(input.limit)
|
|
.offset(input.offset),
|
|
db
|
|
.select({ count: sql<number>`count(*)::int` })
|
|
.from(applications)
|
|
.innerJoin(
|
|
environments,
|
|
eq(applications.environmentId, environments.environmentId),
|
|
)
|
|
.innerJoin(projects, eq(environments.projectId, projects.projectId))
|
|
.where(where),
|
|
]);
|
|
|
|
return {
|
|
items,
|
|
total: countResult[0]?.count ?? 0,
|
|
};
|
|
}),
|
|
|
|
readLogs: protectedProcedure
|
|
.input(
|
|
apiFindOneApplication.extend({
|
|
tail: z.number().int().min(1).max(10000).default(100),
|
|
since: z
|
|
.string()
|
|
.regex(/^(all|\d+[smhd])$/, "Invalid since format")
|
|
.default("all"),
|
|
search: z
|
|
.string()
|
|
.regex(/^[a-zA-Z0-9 ._-]{0,500}$/)
|
|
.optional(),
|
|
}),
|
|
)
|
|
.query(async ({ input, ctx }) => {
|
|
await checkServiceAccess(ctx, input.applicationId, "read");
|
|
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",
|
|
});
|
|
}
|
|
return await getContainerLogs(
|
|
application.appName,
|
|
input.tail,
|
|
input.since,
|
|
input.search,
|
|
application.serverId,
|
|
);
|
|
}),
|
|
});
|