Merge branch 'canary' into 319-ability-to-roll-back-service-depoyments

This commit is contained in:
Mauricio Siu
2025-06-21 21:18:05 -06:00
51 changed files with 12957 additions and 167 deletions

View File

@@ -31,6 +31,7 @@ import {
createApplication,
deleteAllMiddlewares,
findApplicationById,
findGitProviderById,
findProjectById,
getApplicationStats,
mechanizeDockerContainer,
@@ -126,7 +127,45 @@ export const applicationRouter = createTRPCRouter({
message: "You are not authorized to access this application",
});
}
return 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
@@ -488,6 +527,67 @@ export const applicationRouter = createTRPCRouter({
enableSubmodules: input.enableSubmodules,
});
return true;
}),
disconnectGitProvider: protectedProcedure
.input(apiFindOneApplication)
.mutation(async ({ input, ctx }) => {
const application = await findApplicationById(input.applicationId);
if (
application.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

View File

@@ -22,6 +22,7 @@ import {
findPostgresByBackupId,
findPostgresById,
findServerById,
keepLatestNBackups,
removeBackupById,
removeScheduleBackup,
runMariadbBackup,
@@ -197,6 +198,8 @@ export const backupRouter = createTRPCRouter({
const backup = await findBackupById(input.backupId);
const postgres = await findPostgresByBackupId(backup.backupId);
await runPostgresBackup(postgres, backup);
await keepLatestNBackups(backup, postgres?.serverId);
return true;
} catch (error) {
const message =
@@ -217,6 +220,7 @@ export const backupRouter = createTRPCRouter({
const backup = await findBackupById(input.backupId);
const mysql = await findMySqlByBackupId(backup.backupId);
await runMySqlBackup(mysql, backup);
await keepLatestNBackups(backup, mysql?.serverId);
return true;
} catch (error) {
throw new TRPCError({
@@ -233,6 +237,7 @@ export const backupRouter = createTRPCRouter({
const backup = await findBackupById(input.backupId);
const mariadb = await findMariadbByBackupId(backup.backupId);
await runMariadbBackup(mariadb, backup);
await keepLatestNBackups(backup, mariadb?.serverId);
return true;
} catch (error) {
throw new TRPCError({
@@ -249,6 +254,7 @@ export const backupRouter = createTRPCRouter({
const backup = await findBackupById(input.backupId);
const compose = await findComposeByBackupId(backup.backupId);
await runComposeBackup(compose, backup);
await keepLatestNBackups(backup, compose?.serverId);
return true;
} catch (error) {
throw new TRPCError({
@@ -265,6 +271,7 @@ export const backupRouter = createTRPCRouter({
const backup = await findBackupById(input.backupId);
const mongo = await findMongoByBackupId(backup.backupId);
await runMongoBackup(mongo, backup);
await keepLatestNBackups(backup, mongo?.serverId);
return true;
} catch (error) {
throw new TRPCError({

View File

@@ -22,7 +22,11 @@ export const bitbucketRouter = createTRPCRouter({
.input(apiCreateBitbucket)
.mutation(async ({ input, ctx }) => {
try {
return await createBitbucket(input, ctx.session.activeOrganizationId);
return await createBitbucket(
input,
ctx.session.activeOrganizationId,
ctx.session.userId,
);
} catch (error) {
throw new TRPCError({
code: "BAD_REQUEST",
@@ -37,7 +41,8 @@ export const bitbucketRouter = createTRPCRouter({
const bitbucketProvider = await findBitbucketById(input.bitbucketId);
if (
bitbucketProvider.gitProvider.organizationId !==
ctx.session.activeOrganizationId
ctx.session.activeOrganizationId &&
bitbucketProvider.gitProvider.userId !== ctx.session.userId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
@@ -56,11 +61,13 @@ export const bitbucketRouter = createTRPCRouter({
},
});
result = result.filter(
(provider) =>
result = result.filter((provider) => {
return (
provider.gitProvider.organizationId ===
ctx.session.activeOrganizationId,
);
ctx.session.activeOrganizationId &&
provider.gitProvider.userId === ctx.session.userId
);
});
return result;
}),
@@ -70,7 +77,8 @@ export const bitbucketRouter = createTRPCRouter({
const bitbucketProvider = await findBitbucketById(input.bitbucketId);
if (
bitbucketProvider.gitProvider.organizationId !==
ctx.session.activeOrganizationId
ctx.session.activeOrganizationId &&
bitbucketProvider.gitProvider.userId !== ctx.session.userId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
@@ -87,7 +95,8 @@ export const bitbucketRouter = createTRPCRouter({
);
if (
bitbucketProvider.gitProvider.organizationId !==
ctx.session.activeOrganizationId
ctx.session.activeOrganizationId &&
bitbucketProvider.gitProvider.userId !== ctx.session.userId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
@@ -103,7 +112,8 @@ export const bitbucketRouter = createTRPCRouter({
const bitbucketProvider = await findBitbucketById(input.bitbucketId);
if (
bitbucketProvider.gitProvider.organizationId !==
ctx.session.activeOrganizationId
ctx.session.activeOrganizationId &&
bitbucketProvider.gitProvider.userId !== ctx.session.userId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
@@ -126,7 +136,8 @@ export const bitbucketRouter = createTRPCRouter({
const bitbucketProvider = await findBitbucketById(input.bitbucketId);
if (
bitbucketProvider.gitProvider.organizationId !==
ctx.session.activeOrganizationId
ctx.session.activeOrganizationId &&
bitbucketProvider.gitProvider.userId !== ctx.session.userId
) {
throw new TRPCError({
code: "UNAUTHORIZED",

View File

@@ -28,6 +28,7 @@ import {
deleteMount,
findComposeById,
findDomainsByComposeId,
findGitProviderById,
findProjectById,
findServerById,
findUserById,
@@ -119,7 +120,45 @@ export const composeRouter = createTRPCRouter({
message: "You are not authorized to access this compose",
});
}
return compose;
let hasGitProviderAccess = true;
let unauthorizedProvider: string | null = null;
const getGitProviderId = () => {
switch (compose.sourceType) {
case "github":
return compose.github?.gitProviderId;
case "gitlab":
return compose.gitlab?.gitProviderId;
case "bitbucket":
return compose.bitbucket?.gitProviderId;
case "gitea":
return compose.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 = compose.sourceType;
}
} catch {
hasGitProviderAccess = false;
unauthorizedProvider = compose.sourceType;
}
}
return {
...compose,
hasGitProviderAccess,
unauthorizedProvider,
};
}),
update: protectedProcedure
@@ -496,7 +535,7 @@ export const composeRouter = createTRPCRouter({
}
}
return null;
return compose;
}),
templates: publicProcedure
@@ -526,6 +565,61 @@ export const composeRouter = createTRPCRouter({
const uniqueTags = _.uniq(allTags);
return uniqueTags;
}),
disconnectGitProvider: protectedProcedure
.input(apiFindCompose)
.mutation(async ({ input, ctx }) => {
const compose = await findComposeById(input.composeId);
if (compose.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 updateCompose(input.composeId, {
// GitHub fields
repository: null,
branch: null,
owner: null,
composePath: undefined,
githubId: null,
triggerType: "push",
// GitLab fields
gitlabRepository: null,
gitlabOwner: null,
gitlabBranch: null,
gitlabId: null,
gitlabProjectId: null,
gitlabPathNamespace: null,
// Bitbucket fields
bitbucketRepository: null,
bitbucketOwner: null,
bitbucketBranch: null,
bitbucketId: null,
// Gitea fields
giteaRepository: null,
giteaOwner: null,
giteaBranch: null,
giteaId: null,
// Custom Git fields
customGitBranch: null,
customGitUrl: null,
customGitSSHKeyId: null,
// Common fields
sourceType: "github", // Reset to default
composeStatus: "idle",
watchPaths: null,
enableSubmodules: false,
});
return true;
}),
move: protectedProcedure
.input(

View File

@@ -3,7 +3,7 @@ import { db } from "@/server/db";
import { apiRemoveGitProvider, gitProvider } from "@/server/db/schema";
import { findGitProviderById, removeGitProvider } from "@dokploy/server";
import { TRPCError } from "@trpc/server";
import { desc, eq } from "drizzle-orm";
import { and, desc, eq } from "drizzle-orm";
export const gitProviderRouter = createTRPCRouter({
getAll: protectedProcedure.query(async ({ ctx }) => {
@@ -15,7 +15,10 @@ export const gitProviderRouter = createTRPCRouter({
gitea: true,
},
orderBy: desc(gitProvider.createdAt),
where: eq(gitProvider.organizationId, ctx.session.activeOrganizationId),
where: and(
eq(gitProvider.userId, ctx.session.userId),
eq(gitProvider.organizationId, ctx.session.activeOrganizationId),
),
});
}),
remove: protectedProcedure

View File

@@ -26,7 +26,11 @@ export const giteaRouter = createTRPCRouter({
.input(apiCreateGitea)
.mutation(async ({ input, ctx }) => {
try {
return await createGitea(input, ctx.session.activeOrganizationId);
return await createGitea(
input,
ctx.session.activeOrganizationId,
ctx.session.userId,
);
} catch (error) {
throw new TRPCError({
code: "BAD_REQUEST",
@@ -42,7 +46,8 @@ export const giteaRouter = createTRPCRouter({
const giteaProvider = await findGiteaById(input.giteaId);
if (
giteaProvider.gitProvider.organizationId !==
ctx.session.activeOrganizationId
ctx.session.activeOrganizationId &&
giteaProvider.gitProvider.userId !== ctx.session.userId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
@@ -62,7 +67,8 @@ export const giteaRouter = createTRPCRouter({
result = result.filter(
(provider) =>
provider.gitProvider.organizationId ===
ctx.session.activeOrganizationId,
ctx.session.activeOrganizationId &&
provider.gitProvider.userId === ctx.session.userId,
);
const filtered = result
@@ -94,7 +100,8 @@ export const giteaRouter = createTRPCRouter({
const giteaProvider = await findGiteaById(giteaId);
if (
giteaProvider.gitProvider.organizationId !==
ctx.session.activeOrganizationId
ctx.session.activeOrganizationId &&
giteaProvider.gitProvider.userId !== ctx.session.userId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
@@ -130,7 +137,8 @@ export const giteaRouter = createTRPCRouter({
const giteaProvider = await findGiteaById(giteaId);
if (
giteaProvider.gitProvider.organizationId !==
ctx.session.activeOrganizationId
ctx.session.activeOrganizationId &&
giteaProvider.gitProvider.userId !== ctx.session.userId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
@@ -162,7 +170,8 @@ export const giteaRouter = createTRPCRouter({
const giteaProvider = await findGiteaById(giteaId);
if (
giteaProvider.gitProvider.organizationId !==
ctx.session.activeOrganizationId
ctx.session.activeOrganizationId &&
giteaProvider.gitProvider.userId !== ctx.session.userId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
@@ -190,7 +199,8 @@ export const giteaRouter = createTRPCRouter({
const giteaProvider = await findGiteaById(input.giteaId);
if (
giteaProvider.gitProvider.organizationId !==
ctx.session.activeOrganizationId
ctx.session.activeOrganizationId &&
giteaProvider.gitProvider.userId !== ctx.session.userId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
@@ -231,7 +241,8 @@ export const giteaRouter = createTRPCRouter({
const giteaProvider = await findGiteaById(giteaId);
if (
giteaProvider.gitProvider.organizationId !==
ctx.session.activeOrganizationId
ctx.session.activeOrganizationId &&
giteaProvider.gitProvider.userId !== ctx.session.userId
) {
throw new TRPCError({
code: "UNAUTHORIZED",

View File

@@ -21,7 +21,8 @@ export const githubRouter = createTRPCRouter({
const githubProvider = await findGithubById(input.githubId);
if (
githubProvider.gitProvider.organizationId !==
ctx.session.activeOrganizationId
ctx.session.activeOrganizationId &&
githubProvider.gitProvider.userId === ctx.session.userId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
@@ -36,7 +37,8 @@ export const githubRouter = createTRPCRouter({
const githubProvider = await findGithubById(input.githubId);
if (
githubProvider.gitProvider.organizationId !==
ctx.session.activeOrganizationId
ctx.session.activeOrganizationId &&
githubProvider.gitProvider.userId === ctx.session.userId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
@@ -51,7 +53,8 @@ export const githubRouter = createTRPCRouter({
const githubProvider = await findGithubById(input.githubId || "");
if (
githubProvider.gitProvider.organizationId !==
ctx.session.activeOrganizationId
ctx.session.activeOrganizationId &&
githubProvider.gitProvider.userId === ctx.session.userId
) {
//TODO: Remove this line when the cloud version is ready
throw new TRPCError({
@@ -71,7 +74,8 @@ export const githubRouter = createTRPCRouter({
result = result.filter(
(provider) =>
provider.gitProvider.organizationId ===
ctx.session.activeOrganizationId,
ctx.session.activeOrganizationId &&
provider.gitProvider.userId === ctx.session.userId,
);
const filtered = result
@@ -95,7 +99,8 @@ export const githubRouter = createTRPCRouter({
const githubProvider = await findGithubById(input.githubId);
if (
githubProvider.gitProvider.organizationId !==
ctx.session.activeOrganizationId
ctx.session.activeOrganizationId &&
githubProvider.gitProvider.userId === ctx.session.userId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
@@ -117,7 +122,8 @@ export const githubRouter = createTRPCRouter({
const githubProvider = await findGithubById(input.githubId);
if (
githubProvider.gitProvider.organizationId !==
ctx.session.activeOrganizationId
ctx.session.activeOrganizationId &&
githubProvider.gitProvider.userId === ctx.session.userId
) {
throw new TRPCError({
code: "UNAUTHORIZED",

View File

@@ -25,7 +25,11 @@ export const gitlabRouter = createTRPCRouter({
.input(apiCreateGitlab)
.mutation(async ({ input, ctx }) => {
try {
return await createGitlab(input, ctx.session.activeOrganizationId);
return await createGitlab(
input,
ctx.session.activeOrganizationId,
ctx.session.userId,
);
} catch (error) {
throw new TRPCError({
code: "BAD_REQUEST",
@@ -40,7 +44,8 @@ export const gitlabRouter = createTRPCRouter({
const gitlabProvider = await findGitlabById(input.gitlabId);
if (
gitlabProvider.gitProvider.organizationId !==
ctx.session.activeOrganizationId
ctx.session.activeOrganizationId &&
gitlabProvider.gitProvider.userId !== ctx.session.userId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
@@ -56,11 +61,13 @@ export const gitlabRouter = createTRPCRouter({
},
});
result = result.filter(
(provider) =>
result = result.filter((provider) => {
return (
provider.gitProvider.organizationId ===
ctx.session.activeOrganizationId,
);
ctx.session.activeOrganizationId &&
provider.gitProvider.userId === ctx.session.userId
);
});
const filtered = result
.filter((provider) => haveGitlabRequirements(provider))
.map((provider) => {
@@ -80,7 +87,8 @@ export const gitlabRouter = createTRPCRouter({
const gitlabProvider = await findGitlabById(input.gitlabId);
if (
gitlabProvider.gitProvider.organizationId !==
ctx.session.activeOrganizationId
ctx.session.activeOrganizationId &&
gitlabProvider.gitProvider.userId !== ctx.session.userId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
@@ -96,7 +104,8 @@ export const gitlabRouter = createTRPCRouter({
const gitlabProvider = await findGitlabById(input.gitlabId || "");
if (
gitlabProvider.gitProvider.organizationId !==
ctx.session.activeOrganizationId
ctx.session.activeOrganizationId &&
gitlabProvider.gitProvider.userId !== ctx.session.userId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
@@ -112,7 +121,8 @@ export const gitlabRouter = createTRPCRouter({
const gitlabProvider = await findGitlabById(input.gitlabId || "");
if (
gitlabProvider.gitProvider.organizationId !==
ctx.session.activeOrganizationId
ctx.session.activeOrganizationId &&
gitlabProvider.gitProvider.userId !== ctx.session.userId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
@@ -135,7 +145,8 @@ export const gitlabRouter = createTRPCRouter({
const gitlabProvider = await findGitlabById(input.gitlabId);
if (
gitlabProvider.gitProvider.organizationId !==
ctx.session.activeOrganizationId
ctx.session.activeOrganizationId &&
gitlabProvider.gitProvider.userId !== ctx.session.userId
) {
throw new TRPCError({
code: "UNAUTHORIZED",

View File

@@ -446,4 +446,12 @@ export const notificationRouter = createTRPCRouter({
});
}
}),
getEmailProviders: adminProcedure.query(async ({ ctx }) => {
return await db.query.notifications.findMany({
where: eq(notifications.organizationId, ctx.session.activeOrganizationId),
with: {
email: true,
},
});
}),
});

View File

@@ -825,6 +825,9 @@ export const settingsRouter = createTRPCRouter({
}),
)
.mutation(async ({ input }) => {
if (IS_CLOUD) {
return true;
}
if (input.cronExpression) {
return startLogCleanup(input.cronExpression);
}

View File

@@ -1,10 +1,13 @@
import {
IS_CLOUD,
createApiKey,
findAdmin,
findNotificationById,
findOrganizationById,
findUserById,
getUserByToken,
removeUserById,
sendEmailNotification,
updateUser,
} from "@dokploy/server";
import { db } from "@dokploy/server/db";
@@ -362,4 +365,59 @@ export const userRouter = createTRPCRouter({
return organizations.length;
}),
sendInvitation: adminProcedure
.input(
z.object({
invitationId: z.string().min(1),
notificationId: z.string().min(1),
}),
)
.mutation(async ({ input, ctx }) => {
if (IS_CLOUD) {
return;
}
const notification = await findNotificationById(input.notificationId);
const email = notification.email;
const currentInvitation = await db.query.invitation.findFirst({
where: eq(invitation.id, input.invitationId),
});
if (!email) {
throw new TRPCError({
code: "NOT_FOUND",
message: "Email notification not found",
});
}
const admin = await findAdmin();
const host =
process.env.NODE_ENV === "development"
? "http://localhost:3000"
: admin.user.host;
const inviteLink = `${host}/invitation?token=${input.invitationId}`;
const organization = await findOrganizationById(
ctx.session.activeOrganizationId,
);
try {
await sendEmailNotification(
{
...email,
toAddresses: [currentInvitation?.email || ""],
},
"Invitation to join organization",
`
<p>You are invited to join ${organization?.name || "organization"} on Dokploy. Click the link to accept the invitation: <a href="${inviteLink}">Accept Invitation</a></p>
`,
);
} catch (error) {
console.log(error);
throw error;
}
return inviteLink;
}),
});