From 823dbe608fc03342e481e53d971c64af27affe50 Mon Sep 17 00:00:00 2001 From: Mauricio Siu <47042324+Siumauricio@users.noreply.github.com> Date: Thu, 24 Oct 2024 21:25:45 -0600 Subject: [PATCH] refactor: migration --- apps/mig/migration.ts | 20 + apps/mig/package.json | 6 +- apps/mig/server/api/root.ts | 77 ++ apps/mig/server/api/routers/admin.ts | 92 +++ apps/mig/server/api/routers/application.ts | 625 ++++++++++++++++ apps/mig/server/api/routers/auth.ts | 339 +++++++++ apps/mig/server/api/routers/backup.ts | 232 ++++++ apps/mig/server/api/routers/bitbucket.ts | 148 ++++ apps/mig/server/api/routers/certificate.ts | 62 ++ apps/mig/server/api/routers/cluster.ts | 76 ++ apps/mig/server/api/routers/compose.ts | 448 ++++++++++++ apps/mig/server/api/routers/deployment.ts | 55 ++ apps/mig/server/api/routers/destination.ts | 137 ++++ apps/mig/server/api/routers/docker.ts | 71 ++ apps/mig/server/api/routers/domain.ts | 171 +++++ apps/mig/server/api/routers/git-provider.ts | 46 ++ apps/mig/server/api/routers/github.ts | 126 ++++ apps/mig/server/api/routers/gitlab.ts | 151 ++++ apps/mig/server/api/routers/mariadb.ts | 277 ++++++++ apps/mig/server/api/routers/mongo.ts | 290 ++++++++ apps/mig/server/api/routers/mount.ts | 37 + apps/mig/server/api/routers/mysql.ts | 286 ++++++++ apps/mig/server/api/routers/notification.ts | 304 ++++++++ apps/mig/server/api/routers/port.ts | 65 ++ apps/mig/server/api/routers/postgres.ts | 284 ++++++++ apps/mig/server/api/routers/project.ts | 234 ++++++ apps/mig/server/api/routers/redirects.ts | 68 ++ apps/mig/server/api/routers/redis.ts | 276 ++++++++ apps/mig/server/api/routers/registry.ts | 103 +++ apps/mig/server/api/routers/security.ts | 68 ++ apps/mig/server/api/routers/server.ts | 184 +++++ apps/mig/server/api/routers/settings.ts | 666 ++++++++++++++++++ apps/mig/server/api/routers/ssh-key.ts | 103 +++ apps/mig/server/api/routers/stripe.ts | 130 ++++ apps/mig/server/api/routers/user.ts | 34 + apps/mig/server/api/trpc.ts | 210 ++++++ apps/mig/server/db/drizzle.config.ts | 14 + apps/mig/server/db/index.ts | 21 + apps/mig/server/db/migration.ts | 21 + apps/mig/server/db/reset.ts | 23 + apps/mig/server/db/schema/index.ts | 1 + apps/mig/server/db/seed.ts | 35 + apps/mig/server/db/validations/domain.ts | 46 ++ apps/mig/server/db/validations/index.ts | 37 + apps/mig/server/queues/deployments-queue.ts | 138 ++++ apps/mig/server/queues/queueSetup.ts | 24 + apps/mig/server/server.ts | 25 +- apps/mig/server/utils/backup.ts | 67 ++ apps/mig/server/utils/deploy.ts | 25 + apps/mig/server/utils/stripe.ts | 27 + apps/mig/server/wss/docker-container-logs.ts | 141 ++++ .../server/wss/docker-container-terminal.ts | 154 ++++ apps/mig/server/wss/docker-stats.ts | 96 +++ apps/mig/server/wss/listen-deployment.ts | 112 +++ apps/mig/server/wss/terminal.ts | 140 ++++ apps/mig/server/wss/utils.ts | 12 + apps/mig/tsconfig.json | 3 +- pnpm-lock.yaml | 13 +- 58 files changed, 7647 insertions(+), 29 deletions(-) create mode 100644 apps/mig/migration.ts create mode 100644 apps/mig/server/api/root.ts create mode 100644 apps/mig/server/api/routers/admin.ts create mode 100644 apps/mig/server/api/routers/application.ts create mode 100644 apps/mig/server/api/routers/auth.ts create mode 100644 apps/mig/server/api/routers/backup.ts create mode 100644 apps/mig/server/api/routers/bitbucket.ts create mode 100644 apps/mig/server/api/routers/certificate.ts create mode 100644 apps/mig/server/api/routers/cluster.ts create mode 100644 apps/mig/server/api/routers/compose.ts create mode 100644 apps/mig/server/api/routers/deployment.ts create mode 100644 apps/mig/server/api/routers/destination.ts create mode 100644 apps/mig/server/api/routers/docker.ts create mode 100644 apps/mig/server/api/routers/domain.ts create mode 100644 apps/mig/server/api/routers/git-provider.ts create mode 100644 apps/mig/server/api/routers/github.ts create mode 100644 apps/mig/server/api/routers/gitlab.ts create mode 100644 apps/mig/server/api/routers/mariadb.ts create mode 100644 apps/mig/server/api/routers/mongo.ts create mode 100644 apps/mig/server/api/routers/mount.ts create mode 100644 apps/mig/server/api/routers/mysql.ts create mode 100644 apps/mig/server/api/routers/notification.ts create mode 100644 apps/mig/server/api/routers/port.ts create mode 100644 apps/mig/server/api/routers/postgres.ts create mode 100644 apps/mig/server/api/routers/project.ts create mode 100644 apps/mig/server/api/routers/redirects.ts create mode 100644 apps/mig/server/api/routers/redis.ts create mode 100644 apps/mig/server/api/routers/registry.ts create mode 100644 apps/mig/server/api/routers/security.ts create mode 100644 apps/mig/server/api/routers/server.ts create mode 100644 apps/mig/server/api/routers/settings.ts create mode 100644 apps/mig/server/api/routers/ssh-key.ts create mode 100644 apps/mig/server/api/routers/stripe.ts create mode 100644 apps/mig/server/api/routers/user.ts create mode 100644 apps/mig/server/api/trpc.ts create mode 100644 apps/mig/server/db/drizzle.config.ts create mode 100644 apps/mig/server/db/index.ts create mode 100644 apps/mig/server/db/migration.ts create mode 100644 apps/mig/server/db/reset.ts create mode 100644 apps/mig/server/db/schema/index.ts create mode 100644 apps/mig/server/db/seed.ts create mode 100644 apps/mig/server/db/validations/domain.ts create mode 100644 apps/mig/server/db/validations/index.ts create mode 100644 apps/mig/server/queues/deployments-queue.ts create mode 100644 apps/mig/server/queues/queueSetup.ts create mode 100644 apps/mig/server/utils/backup.ts create mode 100644 apps/mig/server/utils/deploy.ts create mode 100644 apps/mig/server/utils/stripe.ts create mode 100644 apps/mig/server/wss/docker-container-logs.ts create mode 100644 apps/mig/server/wss/docker-container-terminal.ts create mode 100644 apps/mig/server/wss/docker-stats.ts create mode 100644 apps/mig/server/wss/listen-deployment.ts create mode 100644 apps/mig/server/wss/terminal.ts create mode 100644 apps/mig/server/wss/utils.ts diff --git a/apps/mig/migration.ts b/apps/mig/migration.ts new file mode 100644 index 00000000..ff1b31d0 --- /dev/null +++ b/apps/mig/migration.ts @@ -0,0 +1,20 @@ +import { drizzle } from "drizzle-orm/postgres-js"; +import { migrate } from "drizzle-orm/postgres-js/migrator"; +import postgres from "postgres"; + +const connectionString = process.env.DATABASE_URL || ""; + +const sql = postgres(connectionString, { max: 1 }); +const db = drizzle(sql); + +await migrate(db, { migrationsFolder: "drizzle" }) + .then(() => { + console.log("Migration complete"); + sql.end(); + }) + .catch((error) => { + console.log("Migration failed", error); + }) + .finally(() => { + sql.end(); + }); diff --git a/apps/mig/package.json b/apps/mig/package.json index b09e963e..e85eac61 100644 --- a/apps/mig/package.json +++ b/apps/mig/package.json @@ -5,7 +5,7 @@ "type": "module", "scripts": { "build": "remix vite:build", - "dev2": "tsx -r dotenv/config server.ts", + "dev2": "tsx --watch -r dotenv/config server/server.ts", "start3": "cross-env NODE_ENV=production node -r dotenv/config ./build/server/index.js", "start2": "cross-env NODE_ENV=production node ./server.js", "dev": "remix vite:dev", @@ -21,7 +21,7 @@ "isbot": "^4.1.0", "react": "^18.2.0", "react-dom": "^18.2.0", - + "bullmq": "5.4.2", "rotating-file-stream": "3.2.3", "@faker-js/faker": "^8.4.1", "@lucia-auth/adapter-drizzle": "1.0.7", @@ -85,6 +85,6 @@ "@types/ssh2": "1.15.1" }, "engines": { - "node": ">=20.0.0" + "node": ">=18.18.0" } } diff --git a/apps/mig/server/api/root.ts b/apps/mig/server/api/root.ts new file mode 100644 index 00000000..1b67d350 --- /dev/null +++ b/apps/mig/server/api/root.ts @@ -0,0 +1,77 @@ +import { authRouter } from "@/server/api/routers/auth"; +import { createTRPCRouter } from "../api/trpc"; +import { adminRouter } from "./routers/admin"; +import { applicationRouter } from "./routers/application"; +import { backupRouter } from "./routers/backup"; +import { bitbucketRouter } from "./routers/bitbucket"; +import { certificateRouter } from "./routers/certificate"; +import { clusterRouter } from "./routers/cluster"; +import { composeRouter } from "./routers/compose"; +import { deploymentRouter } from "./routers/deployment"; +import { destinationRouter } from "./routers/destination"; +import { dockerRouter } from "./routers/docker"; +import { domainRouter } from "./routers/domain"; +import { gitProviderRouter } from "./routers/git-provider"; +import { githubRouter } from "./routers/github"; +import { gitlabRouter } from "./routers/gitlab"; +import { mariadbRouter } from "./routers/mariadb"; +import { mongoRouter } from "./routers/mongo"; +import { mountRouter } from "./routers/mount"; +import { mysqlRouter } from "./routers/mysql"; +import { notificationRouter } from "./routers/notification"; +import { portRouter } from "./routers/port"; +import { postgresRouter } from "./routers/postgres"; +import { projectRouter } from "./routers/project"; +import { redirectsRouter } from "./routers/redirects"; +import { redisRouter } from "./routers/redis"; +import { registryRouter } from "./routers/registry"; +import { securityRouter } from "./routers/security"; +import { serverRouter } from "./routers/server"; +import { settingsRouter } from "./routers/settings"; +import { sshRouter } from "./routers/ssh-key"; +import { stripeRouter } from "./routers/stripe"; +import { userRouter } from "./routers/user"; + +/** + * This is the primary router for your server. + * + * All routers added in /api/routers should be manually added here. + */ + +export const appRouter = createTRPCRouter({ + admin: adminRouter, + docker: dockerRouter, + auth: authRouter, + project: projectRouter, + application: applicationRouter, + mysql: mysqlRouter, + postgres: postgresRouter, + redis: redisRouter, + mongo: mongoRouter, + mariadb: mariadbRouter, + compose: composeRouter, + user: userRouter, + domain: domainRouter, + destination: destinationRouter, + backup: backupRouter, + deployment: deploymentRouter, + mounts: mountRouter, + certificates: certificateRouter, + settings: settingsRouter, + security: securityRouter, + redirects: redirectsRouter, + port: portRouter, + registry: registryRouter, + cluster: clusterRouter, + notification: notificationRouter, + sshKey: sshRouter, + gitProvider: gitProviderRouter, + bitbucket: bitbucketRouter, + gitlab: gitlabRouter, + github: githubRouter, + server: serverRouter, + stripe: stripeRouter, +}); + +// export type definition of API +export type AppRouter = typeof appRouter; diff --git a/apps/mig/server/api/routers/admin.ts b/apps/mig/server/api/routers/admin.ts new file mode 100644 index 00000000..680dee11 --- /dev/null +++ b/apps/mig/server/api/routers/admin.ts @@ -0,0 +1,92 @@ +import { db } from "@/server/db"; +import { + apiAssignPermissions, + apiCreateUserInvitation, + apiFindOneToken, + apiRemoveUser, + users, +} from "@/server/db/schema"; + +import { + createInvitation, + findAdminById, + findUserByAuthId, + findUserById, + getUserByToken, + removeUserByAuthId, +} from "@dokploy/server"; +import { TRPCError } from "@trpc/server"; +import { eq } from "drizzle-orm"; +import { adminProcedure, createTRPCRouter, publicProcedure } from "../trpc"; + +export const adminRouter = createTRPCRouter({ + one: adminProcedure.query(async ({ ctx }) => { + const { sshPrivateKey, ...rest } = await findAdminById(ctx.user.adminId); + return { + haveSSH: !!sshPrivateKey, + ...rest, + }; + }), + createUserInvitation: adminProcedure + .input(apiCreateUserInvitation) + .mutation(async ({ input, ctx }) => { + try { + await createInvitation(input, ctx.user.adminId); + } catch (error) { + throw new TRPCError({ + code: "BAD_REQUEST", + message: + "Error to create this user\ncheck if the email is not registered", + cause: error, + }); + } + }), + removeUser: adminProcedure + .input(apiRemoveUser) + .mutation(async ({ input, ctx }) => { + try { + const user = await findUserByAuthId(input.authId); + + if (user.adminId !== ctx.user.adminId) { + throw new TRPCError({ + code: "UNAUTHORIZED", + message: "You are not allowed to delete this user", + }); + } + return await removeUserByAuthId(input.authId); + } catch (error) { + throw new TRPCError({ + code: "BAD_REQUEST", + message: "Error to delete this user", + cause: error, + }); + } + }), + getUserByToken: publicProcedure + .input(apiFindOneToken) + .query(async ({ input }) => { + return await getUserByToken(input.token); + }), + assignPermissions: adminProcedure + .input(apiAssignPermissions) + .mutation(async ({ input, ctx }) => { + try { + const user = await findUserById(input.userId); + + if (user.adminId !== ctx.user.adminId) { + throw new TRPCError({ + code: "UNAUTHORIZED", + message: "You are not allowed to assign permissions", + }); + } + await db + .update(users) + .set({ + ...input, + }) + .where(eq(users.userId, input.userId)); + } catch (error) { + throw error; + } + }), +}); diff --git a/apps/mig/server/api/routers/application.ts b/apps/mig/server/api/routers/application.ts new file mode 100644 index 00000000..270f6ca7 --- /dev/null +++ b/apps/mig/server/api/routers/application.ts @@ -0,0 +1,625 @@ +import { + createTRPCRouter, + protectedProcedure, + uploadProcedure, +} from "@/server/api/trpc"; +import { db } from "@/server/db"; +import { + apiCreateApplication, + apiFindMonitoringStats, + apiFindOneApplication, + apiReloadApplication, + apiSaveBitbucketProvider, + apiSaveBuildType, + apiSaveDockerProvider, + apiSaveEnvironmentVariables, + apiSaveGitProvider, + apiSaveGithubProvider, + apiSaveGitlabProvider, + apiUpdateApplication, + applications, +} from "@/server/db/schema"; +import { + type DeploymentJob, + cleanQueuesByApplication, +} from "@/server/queues/deployments-queue"; +import { myQueue } from "@/server/queues/queueSetup"; +import { deploy } from "@/server/utils/deploy"; +import { uploadFileSchema } from "@/utils/schema"; +import { + IS_CLOUD, + addNewService, + checkServiceAccess, + createApplication, + deleteAllMiddlewares, + findApplicationById, + findProjectById, + getApplicationStats, + readConfig, + readRemoteConfig, + removeDeployments, + removeDirectoryCode, + removeMonitoringDirectory, + removeService, + removeTraefikConfig, + startService, + startServiceRemote, + stopService, + stopServiceRemote, + unzipDrop, + updateApplication, + updateApplicationStatus, + writeConfig, + writeConfigRemote, + // uploadFileSchema +} from "@dokploy/server"; +import { TRPCError } from "@trpc/server"; +import { eq } from "drizzle-orm"; +import { nanoid } from "nanoid"; +import { z } from "zod"; + +export const applicationRouter = createTRPCRouter({ + create: protectedProcedure + .input(apiCreateApplication) + .mutation(async ({ input, ctx }) => { + try { + if (ctx.user.rol === "user") { + await checkServiceAccess(ctx.user.authId, input.projectId, "create"); + } + + if (IS_CLOUD && !input.serverId) { + throw new TRPCError({ + code: "UNAUTHORIZED", + message: "You need to use a server to create an application", + }); + } + + const project = await findProjectById(input.projectId); + if (project.adminId !== ctx.user.adminId) { + throw new TRPCError({ + code: "UNAUTHORIZED", + message: "You are not authorized to access this project", + }); + } + const newApplication = await createApplication(input); + + if (ctx.user.rol === "user") { + await addNewService(ctx.user.authId, newApplication.applicationId); + } + return newApplication; + } catch (error: unknown) { + if (error instanceof TRPCError) { + throw error; + } + throw new TRPCError({ + code: "BAD_REQUEST", + message: "Error to create the application", + cause: error, + }); + } + }), + one: protectedProcedure + .input(apiFindOneApplication) + .query(async ({ input, ctx }) => { + if (ctx.user.rol === "user") { + await checkServiceAccess( + ctx.user.authId, + input.applicationId, + "access", + ); + } + const application = await findApplicationById(input.applicationId); + if (application.project.adminId !== ctx.user.adminId) { + throw new TRPCError({ + code: "UNAUTHORIZED", + message: "You are not authorized to access this application", + }); + } + return application; + }), + + reload: protectedProcedure + .input(apiReloadApplication) + .mutation(async ({ input, ctx }) => { + const application = await findApplicationById(input.applicationId); + if (application.project.adminId !== ctx.user.adminId) { + throw new TRPCError({ + code: "UNAUTHORIZED", + message: "You are not authorized to reload this application", + }); + } + if (application.serverId) { + await stopServiceRemote(application.serverId, input.appName); + } else { + await stopService(input.appName); + } + await updateApplicationStatus(input.applicationId, "idle"); + + if (application.serverId) { + await startServiceRemote(application.serverId, input.appName); + } else { + await startService(input.appName); + } + await updateApplicationStatus(input.applicationId, "done"); + return true; + }), + + delete: protectedProcedure + .input(apiFindOneApplication) + .mutation(async ({ input, ctx }) => { + if (ctx.user.rol === "user") { + await checkServiceAccess( + ctx.user.authId, + input.applicationId, + "delete", + ); + } + const application = await findApplicationById(input.applicationId); + + if (application.project.adminId !== ctx.user.adminId) { + 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(); + + 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 (error) {} + } + + return result[0]; + }), + + stop: protectedProcedure + .input(apiFindOneApplication) + .mutation(async ({ input, ctx }) => { + const service = await findApplicationById(input.applicationId); + if (service.project.adminId !== ctx.user.adminId) { + 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.project.adminId !== ctx.user.adminId) { + 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(apiFindOneApplication) + .mutation(async ({ input, ctx }) => { + const application = await findApplicationById(input.applicationId); + if (application.project.adminId !== ctx.user.adminId) { + throw new TRPCError({ + code: "UNAUTHORIZED", + message: "You are not authorized to redeploy this application", + }); + } + const jobData: DeploymentJob = { + applicationId: input.applicationId, + titleLog: "Rebuild deployment", + descriptionLog: "", + type: "redeploy", + applicationType: "application", + server: !!application.serverId, + }; + + if (IS_CLOUD && application.serverId) { + jobData.serverId = application.serverId; + await deploy(jobData); + 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.project.adminId !== ctx.user.adminId) { + throw new TRPCError({ + code: "UNAUTHORIZED", + message: "You are not authorized to save this environment", + }); + } + await updateApplication(input.applicationId, { + env: input.env, + buildArgs: input.buildArgs, + }); + return true; + }), + saveBuildType: protectedProcedure + .input(apiSaveBuildType) + .mutation(async ({ input, ctx }) => { + const application = await findApplicationById(input.applicationId); + if (application.project.adminId !== ctx.user.adminId) { + 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, + }); + + return true; + }), + saveGithubProvider: protectedProcedure + .input(apiSaveGithubProvider) + .mutation(async ({ input, ctx }) => { + const application = await findApplicationById(input.applicationId); + if (application.project.adminId !== ctx.user.adminId) { + 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, + }); + + return true; + }), + saveGitlabProvider: protectedProcedure + .input(apiSaveGitlabProvider) + .mutation(async ({ input, ctx }) => { + const application = await findApplicationById(input.applicationId); + if (application.project.adminId !== ctx.user.adminId) { + 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, + }); + + return true; + }), + saveBitbucketProvider: protectedProcedure + .input(apiSaveBitbucketProvider) + .mutation(async ({ input, ctx }) => { + const application = await findApplicationById(input.applicationId); + if (application.project.adminId !== ctx.user.adminId) { + throw new TRPCError({ + code: "UNAUTHORIZED", + message: "You are not authorized to save this bitbucket provider", + }); + } + await updateApplication(input.applicationId, { + bitbucketRepository: input.bitbucketRepository, + bitbucketOwner: input.bitbucketOwner, + bitbucketBranch: input.bitbucketBranch, + bitbucketBuildPath: input.bitbucketBuildPath, + sourceType: "bitbucket", + applicationStatus: "idle", + bitbucketId: input.bitbucketId, + }); + + return true; + }), + saveDockerProvider: protectedProcedure + .input(apiSaveDockerProvider) + .mutation(async ({ input, ctx }) => { + const application = await findApplicationById(input.applicationId); + if (application.project.adminId !== ctx.user.adminId) { + 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", + }); + + return true; + }), + saveGitProdiver: protectedProcedure + .input(apiSaveGitProvider) + .mutation(async ({ input, ctx }) => { + const application = await findApplicationById(input.applicationId); + if (application.project.adminId !== ctx.user.adminId) { + 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", + }); + + return true; + }), + markRunning: protectedProcedure + .input(apiFindOneApplication) + .mutation(async ({ input, ctx }) => { + const application = await findApplicationById(input.applicationId); + if (application.project.adminId !== ctx.user.adminId) { + 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.project.adminId !== ctx.user.adminId) { + 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: "Update: Error to update application", + }); + } + + return true; + }), + refreshToken: protectedProcedure + .input(apiFindOneApplication) + .mutation(async ({ input, ctx }) => { + const application = await findApplicationById(input.applicationId); + if (application.project.adminId !== ctx.user.adminId) { + 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(apiFindOneApplication) + .mutation(async ({ input, ctx }) => { + const application = await findApplicationById(input.applicationId); + if (application.project.adminId !== ctx.user.adminId) { + throw new TRPCError({ + code: "UNAUTHORIZED", + message: "You are not authorized to deploy this application", + }); + } + const jobData: DeploymentJob = { + applicationId: input.applicationId, + titleLog: "Manual deployment", + descriptionLog: "", + type: "deploy", + applicationType: "application", + server: !!application.serverId, + }; + if (IS_CLOUD && application.serverId) { + jobData.serverId = application.serverId; + await deploy(jobData); + + 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.project.adminId !== ctx.user.adminId) { + throw new TRPCError({ + code: "UNAUTHORIZED", + message: "You are not authorized to clean this application", + }); + } + await cleanQueuesByApplication(input.applicationId); + }), + + readTraefikConfig: protectedProcedure + .input(apiFindOneApplication) + .query(async ({ input, ctx }) => { + const application = await findApplicationById(input.applicationId); + if (application.project.adminId !== ctx.user.adminId) { + 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.project.adminId !== ctx.user.adminId) { + throw new TRPCError({ + code: "UNAUTHORIZED", + message: "You are not authorized to deploy this application", + }); + } + + 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; + await deploy(jobData); + 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.project.adminId !== ctx.user.adminId) { + 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, ctx }) => { + if (IS_CLOUD) { + throw new TRPCError({ + code: "UNAUTHORIZED", + message: "Functionality not available in cloud version", + }); + } + const stats = await getApplicationStats(input.appName); + + return stats; + }), +}); diff --git a/apps/mig/server/api/routers/auth.ts b/apps/mig/server/api/routers/auth.ts new file mode 100644 index 00000000..5315800e --- /dev/null +++ b/apps/mig/server/api/routers/auth.ts @@ -0,0 +1,339 @@ +import { + apiCreateAdmin, + apiCreateUser, + apiFindOneAuth, + apiLogin, + apiUpdateAuth, + apiUpdateAuthByAdmin, + apiVerify2FA, + apiVerifyLogin2FA, + auth, +} from "@/server/db/schema"; +import { + IS_CLOUD, + createAdmin, + createUser, + findAuthByEmail, + findAuthById, + generate2FASecret, + getUserByToken, + lucia, + luciaToken, + sendEmailNotification, + updateAuthById, + validateRequest, + verify2FA, +} from "@dokploy/server"; +import { TRPCError } from "@trpc/server"; +import * as bcrypt from "bcrypt"; +import { isBefore } from "date-fns"; +import { eq } from "drizzle-orm"; +import { nanoid } from "nanoid"; +import { z } from "zod"; +import { db } from "../../db"; +import { + adminProcedure, + createTRPCRouter, + protectedProcedure, + publicProcedure, +} from "../trpc"; + +export const authRouter = createTRPCRouter({ + createAdmin: publicProcedure + .input(apiCreateAdmin) + .mutation(async ({ ctx, input }) => { + try { + if (!IS_CLOUD) { + const admin = await db.query.admins.findFirst({}); + if (admin) { + throw new TRPCError({ + code: "BAD_REQUEST", + message: "Admin already exists", + }); + } + } + const newAdmin = await createAdmin(input); + const session = await lucia.createSession(newAdmin.id || "", {}); + ctx.res.appendHeader( + "Set-Cookie", + lucia.createSessionCookie(session.id).serialize(), + ); + return true; + } catch (error) { + throw error; + } + }), + createUser: publicProcedure + .input(apiCreateUser) + .mutation(async ({ ctx, input }) => { + try { + const token = await getUserByToken(input.token); + if (token.isExpired) { + throw new TRPCError({ + code: "BAD_REQUEST", + message: "Invalid token", + }); + } + const newUser = await createUser(input); + const session = await lucia.createSession(newUser?.authId || "", {}); + ctx.res.appendHeader( + "Set-Cookie", + lucia.createSessionCookie(session.id).serialize(), + ); + return true; + } catch (error) { + throw new TRPCError({ + code: "BAD_REQUEST", + message: "Error to create the user", + cause: error, + }); + } + }), + + login: publicProcedure.input(apiLogin).mutation(async ({ ctx, input }) => { + try { + const auth = await findAuthByEmail(input.email); + + const correctPassword = bcrypt.compareSync( + input.password, + auth?.password || "", + ); + + if (!correctPassword) { + throw new TRPCError({ + code: "BAD_REQUEST", + message: "Credentials do not match", + }); + } + + if (auth?.is2FAEnabled) { + return { + is2FAEnabled: true, + authId: auth.id, + }; + } + + const session = await lucia.createSession(auth?.id || "", {}); + + ctx.res.appendHeader( + "Set-Cookie", + lucia.createSessionCookie(session.id).serialize(), + ); + return { + is2FAEnabled: false, + authId: auth?.id, + }; + } catch (error) { + throw new TRPCError({ + code: "BAD_REQUEST", + message: "Credentials do not match", + cause: error, + }); + } + }), + + get: protectedProcedure.query(async ({ ctx }) => { + const auth = await findAuthById(ctx.user.authId); + return auth; + }), + + logout: protectedProcedure.mutation(async ({ ctx }) => { + const { req, res } = ctx; + const { session } = await validateRequest(req, res); + if (!session) return false; + + await lucia.invalidateSession(session.id); + res.setHeader("Set-Cookie", lucia.createBlankSessionCookie().serialize()); + return true; + }), + + update: protectedProcedure + .input(apiUpdateAuth) + .mutation(async ({ ctx, input }) => { + const auth = await updateAuthById(ctx.user.authId, { + ...(input.email && { email: input.email }), + ...(input.password && { + password: bcrypt.hashSync(input.password, 10), + }), + ...(input.image && { image: input.image }), + }); + + return auth; + }), + + generateToken: protectedProcedure.mutation(async ({ ctx, input }) => { + const auth = await findAuthById(ctx.user.authId); + + if (auth.token) { + await luciaToken.invalidateSession(auth.token); + } + const session = await luciaToken.createSession(auth?.id || "", { + expiresIn: 60 * 60 * 24 * 30, + }); + + await updateAuthById(auth.id, { + token: session.id, + }); + + return auth; + }), + + one: adminProcedure.input(apiFindOneAuth).query(async ({ input }) => { + const auth = await findAuthById(input.id); + return auth; + }), + + updateByAdmin: protectedProcedure + .input(apiUpdateAuthByAdmin) + .mutation(async ({ input }) => { + const auth = await updateAuthById(input.id, { + ...(input.email && { email: input.email }), + ...(input.password && { + password: bcrypt.hashSync(input.password, 10), + }), + ...(input.image && { image: input.image }), + }); + + return auth; + }), + generate2FASecret: protectedProcedure.query(async ({ ctx }) => { + return await generate2FASecret(ctx.user.authId); + }), + verify2FASetup: protectedProcedure + .input(apiVerify2FA) + .mutation(async ({ ctx, input }) => { + const auth = await findAuthById(ctx.user.authId); + + await verify2FA(auth, input.secret, input.pin); + await updateAuthById(auth.id, { + is2FAEnabled: true, + secret: input.secret, + }); + return auth; + }), + + verifyLogin2FA: publicProcedure + .input(apiVerifyLogin2FA) + .mutation(async ({ ctx, input }) => { + const auth = await findAuthById(input.id); + + await verify2FA(auth, auth.secret || "", input.pin); + + const session = await lucia.createSession(auth.id, {}); + + ctx.res.appendHeader( + "Set-Cookie", + lucia.createSessionCookie(session.id).serialize(), + ); + + return true; + }), + disable2FA: protectedProcedure.mutation(async ({ ctx }) => { + const auth = await findAuthById(ctx.user.authId); + await updateAuthById(auth.id, { + is2FAEnabled: false, + secret: null, + }); + return auth; + }), + verifyToken: protectedProcedure.mutation(async () => { + return true; + }), + sendResetPasswordEmail: publicProcedure + .input( + z.object({ + email: z.string().min(1).email(), + }), + ) + .mutation(async ({ ctx, input }) => { + if (!IS_CLOUD) { + throw new TRPCError({ + code: "NOT_FOUND", + message: "This feature is only available in the cloud version", + }); + } + const authR = await db.query.auth.findFirst({ + where: eq(auth.email, input.email), + }); + if (!authR) { + throw new TRPCError({ + code: "NOT_FOUND", + message: "User not found", + }); + } + const token = nanoid(); + await updateAuthById(authR.id, { + resetPasswordToken: token, + // Make resetPassword in 24 hours + resetPasswordExpiresAt: new Date( + new Date().getTime() + 24 * 60 * 60 * 1000, + ).toISOString(), + }); + + const email = await sendEmailNotification( + { + fromAddress: process.env.SMTP_FROM_ADDRESS || "", + toAddresses: [authR.email], + smtpServer: process.env.SMTP_SERVER || "", + smtpPort: Number(process.env.SMTP_PORT), + username: process.env.SMTP_USERNAME || "", + password: process.env.SMTP_PASSWORD || "", + }, + "Reset Password", + ` + Reset your password by clicking the link below: + The link will expire in 24 hours. + + Reset Password + + + `, + ); + }), + + resetPassword: publicProcedure + .input( + z.object({ + resetPasswordToken: z.string().min(1), + password: z.string().min(1), + }), + ) + .mutation(async ({ ctx, input }) => { + if (!IS_CLOUD) { + throw new TRPCError({ + code: "NOT_FOUND", + message: "This feature is only available in the cloud version", + }); + } + const authR = await db.query.auth.findFirst({ + where: eq(auth.resetPasswordToken, input.resetPasswordToken), + }); + + if (!authR || authR.resetPasswordExpiresAt === null) { + throw new TRPCError({ + code: "NOT_FOUND", + message: "Token not found", + }); + } + + const isExpired = isBefore( + new Date(authR.resetPasswordExpiresAt), + new Date(), + ); + + if (isExpired) { + throw new TRPCError({ + code: "NOT_FOUND", + message: "Token expired", + }); + } + + await updateAuthById(authR.id, { + resetPasswordExpiresAt: null, + resetPasswordToken: null, + password: bcrypt.hashSync(input.password, 10), + }); + + return true; + }), +}); diff --git a/apps/mig/server/api/routers/backup.ts b/apps/mig/server/api/routers/backup.ts new file mode 100644 index 00000000..c6ae38b6 --- /dev/null +++ b/apps/mig/server/api/routers/backup.ts @@ -0,0 +1,232 @@ +import { createTRPCRouter, protectedProcedure } from "@/server/api/trpc"; +import { + apiCreateBackup, + apiFindOneBackup, + apiRemoveBackup, + apiUpdateBackup, +} from "@/server/db/schema"; +import { removeJob, schedule, updateJob } from "@/server/utils/backup"; +import { + IS_CLOUD, + createBackup, + findBackupById, + findMariadbByBackupId, + findMongoByBackupId, + findMySqlByBackupId, + findPostgresByBackupId, + findServerById, + removeBackupById, + removeScheduleBackup, + runMariadbBackup, + runMongoBackup, + runMySqlBackup, + runPostgresBackup, + scheduleBackup, + updateBackupById, +} from "@dokploy/server"; + +import { TRPCError } from "@trpc/server"; + +export const backupRouter = createTRPCRouter({ + create: protectedProcedure + .input(apiCreateBackup) + .mutation(async ({ input, ctx }) => { + try { + const newBackup = await createBackup(input); + + const backup = await findBackupById(newBackup.backupId); + + if (IS_CLOUD && backup.enabled) { + const databaseType = backup.databaseType; + let serverId = ""; + if (databaseType === "postgres" && backup.postgres?.serverId) { + serverId = backup.postgres.serverId; + } else if (databaseType === "mysql" && backup.mysql?.serverId) { + serverId = backup.mysql.serverId; + } else if (databaseType === "mongo" && backup.mongo?.serverId) { + serverId = backup.mongo.serverId; + } else if (databaseType === "mariadb" && backup.mariadb?.serverId) { + serverId = backup.mariadb.serverId; + } + const server = await findServerById(serverId); + + if (server.serverStatus === "inactive") { + throw new TRPCError({ + code: "NOT_FOUND", + message: "Server is inactive", + }); + } + await schedule({ + cronSchedule: backup.schedule, + backupId: backup.backupId, + type: "backup", + }); + } else { + if (backup.enabled) { + scheduleBackup(backup); + } + } + } catch (error) { + throw new TRPCError({ + code: "BAD_REQUEST", + message: "Error to create the Backup", + cause: error, + }); + } + }), + one: protectedProcedure + .input(apiFindOneBackup) + .query(async ({ input, ctx }) => { + const backup = await findBackupById(input.backupId); + + return backup; + }), + update: protectedProcedure + .input(apiUpdateBackup) + .mutation(async ({ input, ctx }) => { + try { + await updateBackupById(input.backupId, input); + const backup = await findBackupById(input.backupId); + + if (IS_CLOUD) { + if (backup.enabled) { + await updateJob({ + cronSchedule: backup.schedule, + backupId: backup.backupId, + type: "backup", + }); + } else { + await removeJob({ + cronSchedule: backup.schedule, + backupId: backup.backupId, + type: "backup", + }); + } + } else { + if (backup.enabled) { + removeScheduleBackup(input.backupId); + scheduleBackup(backup); + } else { + removeScheduleBackup(input.backupId); + } + } + } catch (error) { + throw new TRPCError({ + code: "BAD_REQUEST", + message: "Error to update this Backup", + }); + } + }), + remove: protectedProcedure + .input(apiRemoveBackup) + .mutation(async ({ input, ctx }) => { + try { + const value = await removeBackupById(input.backupId); + if (IS_CLOUD && value) { + removeJob({ + backupId: input.backupId, + cronSchedule: value.schedule, + type: "backup", + }); + } else if (!IS_CLOUD) { + removeScheduleBackup(input.backupId); + } + return value; + } catch (error) { + throw new TRPCError({ + code: "BAD_REQUEST", + message: "Error to delete this Backup", + cause: error, + }); + } + }), + manualBackupPostgres: protectedProcedure + .input(apiFindOneBackup) + .mutation(async ({ input }) => { + try { + const backup = await findBackupById(input.backupId); + const postgres = await findPostgresByBackupId(backup.backupId); + await runPostgresBackup(postgres, backup); + return true; + } catch (error) { + console.log(error); + throw new TRPCError({ + code: "BAD_REQUEST", + message: "Error to run manual postgres backup ", + cause: error, + }); + } + }), + + manualBackupMySql: protectedProcedure + .input(apiFindOneBackup) + .mutation(async ({ input }) => { + try { + const backup = await findBackupById(input.backupId); + const mysql = await findMySqlByBackupId(backup.backupId); + await runMySqlBackup(mysql, backup); + return true; + } catch (error) { + throw new TRPCError({ + code: "BAD_REQUEST", + message: "Error to run manual mysql backup ", + cause: error, + }); + } + }), + manualBackupMariadb: protectedProcedure + .input(apiFindOneBackup) + .mutation(async ({ input }) => { + try { + const backup = await findBackupById(input.backupId); + const mariadb = await findMariadbByBackupId(backup.backupId); + await runMariadbBackup(mariadb, backup); + return true; + } catch (error) { + throw new TRPCError({ + code: "BAD_REQUEST", + message: "Error to run manual mariadb backup ", + cause: error, + }); + } + }), + manualBackupMongo: protectedProcedure + .input(apiFindOneBackup) + .mutation(async ({ input }) => { + try { + const backup = await findBackupById(input.backupId); + const mongo = await findMongoByBackupId(backup.backupId); + await runMongoBackup(mongo, backup); + return true; + } catch (error) { + throw new TRPCError({ + code: "BAD_REQUEST", + message: "Error to run manual mongo backup ", + cause: error, + }); + } + }), +}); + +// export const getAdminId = async (backupId: string) => { +// const backup = await findBackupById(backupId); + +// if (backup.databaseType === "postgres" && backup.postgresId) { +// const postgres = await findPostgresById(backup.postgresId); +// return postgres.project.adminId; +// } +// if (backup.databaseType === "mariadb" && backup.mariadbId) { +// const mariadb = await findMariadbById(backup.mariadbId); +// return mariadb.project.adminId; +// } +// if (backup.databaseType === "mysql" && backup.mysqlId) { +// const mysql = await findMySqlById(backup.mysqlId); +// return mysql.project.adminId; +// } +// if (backup.databaseType === "mongo" && backup.mongoId) { +// const mongo = await findMongoById(backup.mongoId); +// return mongo.project.adminId; +// } + +// return null; +// }; diff --git a/apps/mig/server/api/routers/bitbucket.ts b/apps/mig/server/api/routers/bitbucket.ts new file mode 100644 index 00000000..48b7fdd4 --- /dev/null +++ b/apps/mig/server/api/routers/bitbucket.ts @@ -0,0 +1,148 @@ +import { createTRPCRouter, protectedProcedure } from "@/server/api/trpc"; +import { db } from "@/server/db"; +import { + apiBitbucketTestConnection, + apiCreateBitbucket, + apiFindBitbucketBranches, + apiFindOneBitbucket, + apiUpdateBitbucket, +} from "@/server/db/schema"; +import { + IS_CLOUD, + createBitbucket, + findBitbucketById, + getBitbucketBranches, + getBitbucketRepositories, + testBitbucketConnection, + updateBitbucket, +} from "@dokploy/server"; +import { TRPCError } from "@trpc/server"; + +export const bitbucketRouter = createTRPCRouter({ + create: protectedProcedure + .input(apiCreateBitbucket) + .mutation(async ({ input, ctx }) => { + try { + return await createBitbucket(input, ctx.user.adminId); + } catch (error) { + throw new TRPCError({ + code: "BAD_REQUEST", + message: "Error to create this bitbucket provider", + cause: error, + }); + } + }), + one: protectedProcedure + .input(apiFindOneBitbucket) + .query(async ({ input, ctx }) => { + const bitbucketProvider = await findBitbucketById(input.bitbucketId); + if ( + IS_CLOUD && + bitbucketProvider.gitProvider.adminId !== ctx.user.adminId + ) { + //TODO: Remove this line when the cloud version is ready + throw new TRPCError({ + code: "UNAUTHORIZED", + message: "You are not allowed to access this bitbucket provider", + }); + } + return bitbucketProvider; + }), + bitbucketProviders: protectedProcedure.query(async ({ ctx }) => { + let result = await db.query.bitbucket.findMany({ + with: { + gitProvider: true, + }, + columns: { + bitbucketId: true, + }, + }); + + if (IS_CLOUD) { + // TODO: mAyBe a rEfaCtoR 🤫 + result = result.filter( + (provider) => provider.gitProvider.adminId === ctx.user.adminId, + ); + } + return result; + }), + + getBitbucketRepositories: protectedProcedure + .input(apiFindOneBitbucket) + .query(async ({ input, ctx }) => { + const bitbucketProvider = await findBitbucketById(input.bitbucketId); + if ( + IS_CLOUD && + bitbucketProvider.gitProvider.adminId !== ctx.user.adminId + ) { + //TODO: Remove this line when the cloud version is ready + throw new TRPCError({ + code: "UNAUTHORIZED", + message: "You are not allowed to access this bitbucket provider", + }); + } + return await getBitbucketRepositories(input.bitbucketId); + }), + getBitbucketBranches: protectedProcedure + .input(apiFindBitbucketBranches) + .query(async ({ input, ctx }) => { + const bitbucketProvider = await findBitbucketById( + input.bitbucketId || "", + ); + if ( + IS_CLOUD && + bitbucketProvider.gitProvider.adminId !== ctx.user.adminId + ) { + //TODO: Remove this line when the cloud version is ready + throw new TRPCError({ + code: "UNAUTHORIZED", + message: "You are not allowed to access this bitbucket provider", + }); + } + return await getBitbucketBranches(input); + }), + testConnection: protectedProcedure + .input(apiBitbucketTestConnection) + .mutation(async ({ input, ctx }) => { + try { + const bitbucketProvider = await findBitbucketById(input.bitbucketId); + if ( + IS_CLOUD && + bitbucketProvider.gitProvider.adminId !== ctx.user.adminId + ) { + //TODO: Remove this line when the cloud version is ready + throw new TRPCError({ + code: "UNAUTHORIZED", + message: "You are not allowed to access this bitbucket provider", + }); + } + const result = await testBitbucketConnection(input); + + return `Found ${result} repositories`; + } catch (error) { + throw new TRPCError({ + code: "BAD_REQUEST", + message: error instanceof Error ? error?.message : `Error: ${error}`, + }); + } + }), + update: protectedProcedure + .input(apiUpdateBitbucket) + .mutation(async ({ input, ctx }) => { + const bitbucketProvider = await findBitbucketById(input.bitbucketId); + if ( + IS_CLOUD && + bitbucketProvider.gitProvider.adminId !== ctx.user.adminId + ) { + //TODO: Remove this line when the cloud version is ready + throw new TRPCError({ + code: "UNAUTHORIZED", + message: "You are not allowed to access this bitbucket provider", + }); + } + return await updateBitbucket(input.bitbucketId, { + ...input, + adminId: ctx.user.adminId, + }); + }), +}); diff --git a/apps/mig/server/api/routers/certificate.ts b/apps/mig/server/api/routers/certificate.ts new file mode 100644 index 00000000..0f8d6fd9 --- /dev/null +++ b/apps/mig/server/api/routers/certificate.ts @@ -0,0 +1,62 @@ +import { adminProcedure, createTRPCRouter } from "@/server/api/trpc"; +import { + apiCreateCertificate, + apiFindCertificate, + certificates, +} from "@/server/db/schema"; + +import { db } from "@/server/db"; +import { + IS_CLOUD, + createCertificate, + findCertificateById, + removeCertificateById, +} from "@dokploy/server"; +import { TRPCError } from "@trpc/server"; +import { eq } from "drizzle-orm"; + +export const certificateRouter = createTRPCRouter({ + create: adminProcedure + .input(apiCreateCertificate) + .mutation(async ({ input, ctx }) => { + if (IS_CLOUD && !input.serverId) { + throw new TRPCError({ + code: "UNAUTHORIZED", + message: "Please set a server to create a certificate", + }); + } + return await createCertificate(input, ctx.user.adminId); + }), + + one: adminProcedure + .input(apiFindCertificate) + .query(async ({ input, ctx }) => { + const certificates = await findCertificateById(input.certificateId); + if (IS_CLOUD && certificates.adminId !== ctx.user.adminId) { + throw new TRPCError({ + code: "UNAUTHORIZED", + message: "You are not allowed to access this certificate", + }); + } + return certificates; + }), + remove: adminProcedure + .input(apiFindCertificate) + .mutation(async ({ input, ctx }) => { + const certificates = await findCertificateById(input.certificateId); + if (IS_CLOUD && certificates.adminId !== ctx.user.adminId) { + throw new TRPCError({ + code: "UNAUTHORIZED", + message: "You are not allowed to delete this certificate", + }); + } + await removeCertificateById(input.certificateId); + return true; + }), + all: adminProcedure.query(async ({ ctx }) => { + return await db.query.certificates.findMany({ + // TODO: Remove this line when the cloud version is ready + ...(IS_CLOUD && { where: eq(certificates.adminId, ctx.user.adminId) }), + }); + }), +}); diff --git a/apps/mig/server/api/routers/cluster.ts b/apps/mig/server/api/routers/cluster.ts new file mode 100644 index 00000000..e42d4b04 --- /dev/null +++ b/apps/mig/server/api/routers/cluster.ts @@ -0,0 +1,76 @@ +import { getPublicIpWithFallback } from "@/server/wss/terminal"; +import { type DockerNode, IS_CLOUD, docker, execAsync } from "@dokploy/server"; +import { TRPCError } from "@trpc/server"; +import { z } from "zod"; +import { createTRPCRouter, protectedProcedure } from "../trpc"; + +export const clusterRouter = createTRPCRouter({ + getNodes: protectedProcedure.query(async () => { + if (IS_CLOUD) { + return []; + } + const workers: DockerNode[] = await docker.listNodes(); + + return workers; + }), + removeWorker: protectedProcedure + .input( + z.object({ + nodeId: z.string(), + }), + ) + .mutation(async ({ input }) => { + if (IS_CLOUD) { + throw new TRPCError({ + code: "UNAUTHORIZED", + message: "Functionality not available in cloud version", + }); + } + try { + await execAsync( + `docker node update --availability drain ${input.nodeId}`, + ); + await execAsync(`docker node rm ${input.nodeId} --force`); + return true; + } catch (error) { + throw new TRPCError({ + code: "INTERNAL_SERVER_ERROR", + message: "Error to remove the node", + cause: error, + }); + } + }), + addWorker: protectedProcedure.query(async ({ input }) => { + if (IS_CLOUD) { + return { + command: "", + version: "", + }; + } + const result = await docker.swarmInspect(); + const docker_version = await docker.version(); + + return { + command: `docker swarm join --token ${ + result.JoinTokens.Worker + } ${await getPublicIpWithFallback()}:2377`, + version: docker_version.Version, + }; + }), + addManager: protectedProcedure.query(async ({ input }) => { + if (IS_CLOUD) { + return { + command: "", + version: "", + }; + } + const result = await docker.swarmInspect(); + const docker_version = await docker.version(); + return { + command: `docker swarm join --token ${ + result.JoinTokens.Manager + } ${await getPublicIpWithFallback()}:2377`, + version: docker_version.Version, + }; + }), +}); diff --git a/apps/mig/server/api/routers/compose.ts b/apps/mig/server/api/routers/compose.ts new file mode 100644 index 00000000..f50956b2 --- /dev/null +++ b/apps/mig/server/api/routers/compose.ts @@ -0,0 +1,448 @@ +import { slugify } from "@/lib/slug"; +import { db } from "@/server/db"; +import { + apiCreateCompose, + apiCreateComposeByTemplate, + apiFetchServices, + apiFindCompose, + apiRandomizeCompose, + apiUpdateCompose, + compose, +} from "@/server/db/schema"; +import { + type DeploymentJob, + cleanQueuesByCompose, +} from "@/server/queues/deployments-queue"; +import { myQueue } from "@/server/queues/queueSetup"; +import { templates } from "@/templates/templates"; +import type { TemplatesKeys } from "@/templates/types/templates-data.type"; +import { + generatePassword, + loadTemplateModule, + readTemplateComposeFile, +} from "@/templates/utils"; +import { TRPCError } from "@trpc/server"; +import { eq } from "drizzle-orm"; +import { dump } from "js-yaml"; +import _ from "lodash"; +import { nanoid } from "nanoid"; +import { createTRPCRouter, protectedProcedure } from "../trpc"; + +import { deploy } from "@/server/utils/deploy"; +import { + IS_CLOUD, + addDomainToCompose, + addNewService, + checkServiceAccess, + cloneCompose, + cloneComposeRemote, + createCommand, + createCompose, + createComposeByTemplate, + createDomain, + createMount, + findAdmin, + findAdminById, + findComposeById, + findDomainsByComposeId, + findProjectById, + findServerById, + loadServices, + randomizeComposeFile, + removeCompose, + removeComposeDirectory, + removeDeploymentsByComposeId, + stopCompose, + updateCompose, +} from "@dokploy/server"; + +export const composeRouter = createTRPCRouter({ + create: protectedProcedure + .input(apiCreateCompose) + .mutation(async ({ ctx, input }) => { + try { + if (ctx.user.rol === "user") { + await checkServiceAccess(ctx.user.authId, input.projectId, "create"); + } + + if (IS_CLOUD && !input.serverId) { + throw new TRPCError({ + code: "UNAUTHORIZED", + message: "You need to use a server to create a compose", + }); + } + const project = await findProjectById(input.projectId); + if (project.adminId !== ctx.user.adminId) { + throw new TRPCError({ + code: "UNAUTHORIZED", + message: "You are not authorized to access this project", + }); + } + const newService = await createCompose(input); + + if (ctx.user.rol === "user") { + await addNewService(ctx.user.authId, newService.composeId); + } + } catch (error) { + throw error; + } + }), + + one: protectedProcedure + .input(apiFindCompose) + .query(async ({ input, ctx }) => { + if (ctx.user.rol === "user") { + await checkServiceAccess(ctx.user.authId, input.composeId, "access"); + } + + const compose = await findComposeById(input.composeId); + if (compose.project.adminId !== ctx.user.adminId) { + throw new TRPCError({ + code: "UNAUTHORIZED", + message: "You are not authorized to access this compose", + }); + } + return compose; + }), + + update: protectedProcedure + .input(apiUpdateCompose) + .mutation(async ({ input, ctx }) => { + const compose = await findComposeById(input.composeId); + if (compose.project.adminId !== ctx.user.adminId) { + throw new TRPCError({ + code: "UNAUTHORIZED", + message: "You are not authorized to update this compose", + }); + } + return updateCompose(input.composeId, input); + }), + delete: protectedProcedure + .input(apiFindCompose) + .mutation(async ({ input, ctx }) => { + if (ctx.user.rol === "user") { + await checkServiceAccess(ctx.user.authId, input.composeId, "delete"); + } + const composeResult = await findComposeById(input.composeId); + + if (composeResult.project.adminId !== ctx.user.adminId) { + throw new TRPCError({ + code: "UNAUTHORIZED", + message: "You are not authorized to delete this compose", + }); + } + 4; + + const result = await db + .delete(compose) + .where(eq(compose.composeId, input.composeId)) + .returning(); + + const cleanupOperations = [ + async () => await removeCompose(composeResult), + async () => await removeDeploymentsByComposeId(composeResult), + async () => await removeComposeDirectory(composeResult.appName), + ]; + + for (const operation of cleanupOperations) { + try { + await operation(); + } catch (error) {} + } + + return result[0]; + }), + cleanQueues: protectedProcedure + .input(apiFindCompose) + .mutation(async ({ input, ctx }) => { + const compose = await findComposeById(input.composeId); + if (compose.project.adminId !== ctx.user.adminId) { + throw new TRPCError({ + code: "UNAUTHORIZED", + message: "You are not authorized to clean this compose", + }); + } + await cleanQueuesByCompose(input.composeId); + }), + + loadServices: protectedProcedure + .input(apiFetchServices) + .query(async ({ input, ctx }) => { + const compose = await findComposeById(input.composeId); + if (compose.project.adminId !== ctx.user.adminId) { + throw new TRPCError({ + code: "UNAUTHORIZED", + message: "You are not authorized to load this compose", + }); + } + return await loadServices(input.composeId, input.type); + }), + fetchSourceType: protectedProcedure + .input(apiFindCompose) + .mutation(async ({ input, ctx }) => { + try { + const compose = await findComposeById(input.composeId); + + if (compose.project.adminId !== ctx.user.adminId) { + throw new TRPCError({ + code: "UNAUTHORIZED", + message: "You are not authorized to fetch this compose", + }); + } + if (compose.serverId) { + await cloneComposeRemote(compose); + } else { + await cloneCompose(compose); + } + return compose.sourceType; + } catch (err) { + throw new TRPCError({ + code: "BAD_REQUEST", + message: "Error to fetch source type", + cause: err, + }); + } + }), + + randomizeCompose: protectedProcedure + .input(apiRandomizeCompose) + .mutation(async ({ input, ctx }) => { + const compose = await findComposeById(input.composeId); + if (compose.project.adminId !== ctx.user.adminId) { + throw new TRPCError({ + code: "UNAUTHORIZED", + message: "You are not authorized to randomize this compose", + }); + } + return await randomizeComposeFile(input.composeId, input.suffix); + }), + getConvertedCompose: protectedProcedure + .input(apiFindCompose) + .query(async ({ input, ctx }) => { + const compose = await findComposeById(input.composeId); + if (compose.project.adminId !== ctx.user.adminId) { + throw new TRPCError({ + code: "UNAUTHORIZED", + message: "You are not authorized to get this compose", + }); + } + const domains = await findDomainsByComposeId(input.composeId); + const composeFile = await addDomainToCompose(compose, domains); + return dump(composeFile, { + lineWidth: 1000, + }); + }), + + deploy: protectedProcedure + .input(apiFindCompose) + .mutation(async ({ input, ctx }) => { + const compose = await findComposeById(input.composeId); + + if (compose.project.adminId !== ctx.user.adminId) { + throw new TRPCError({ + code: "UNAUTHORIZED", + message: "You are not authorized to deploy this compose", + }); + } + const jobData: DeploymentJob = { + composeId: input.composeId, + titleLog: "Manual deployment", + type: "deploy", + applicationType: "compose", + descriptionLog: "", + server: !!compose.serverId, + }; + + if (IS_CLOUD && compose.serverId) { + jobData.serverId = compose.serverId; + await deploy(jobData); + return true; + } + await myQueue.add( + "deployments", + { ...jobData }, + { + removeOnComplete: true, + removeOnFail: true, + }, + ); + }), + redeploy: protectedProcedure + .input(apiFindCompose) + .mutation(async ({ input, ctx }) => { + const compose = await findComposeById(input.composeId); + if (compose.project.adminId !== ctx.user.adminId) { + throw new TRPCError({ + code: "UNAUTHORIZED", + message: "You are not authorized to redeploy this compose", + }); + } + const jobData: DeploymentJob = { + composeId: input.composeId, + titleLog: "Rebuild deployment", + type: "redeploy", + applicationType: "compose", + descriptionLog: "", + server: !!compose.serverId, + }; + if (IS_CLOUD && compose.serverId) { + jobData.serverId = compose.serverId; + await deploy(jobData); + return true; + } + await myQueue.add( + "deployments", + { ...jobData }, + { + removeOnComplete: true, + removeOnFail: true, + }, + ); + }), + stop: protectedProcedure + .input(apiFindCompose) + .mutation(async ({ input, ctx }) => { + const compose = await findComposeById(input.composeId); + if (compose.project.adminId !== ctx.user.adminId) { + throw new TRPCError({ + code: "UNAUTHORIZED", + message: "You are not authorized to stop this compose", + }); + } + await stopCompose(input.composeId); + + return true; + }), + getDefaultCommand: protectedProcedure + .input(apiFindCompose) + .query(async ({ input, ctx }) => { + const compose = await findComposeById(input.composeId); + + if (compose.project.adminId !== ctx.user.adminId) { + throw new TRPCError({ + code: "UNAUTHORIZED", + message: "You are not authorized to get this compose", + }); + } + const command = createCommand(compose); + return `docker ${command}`; + }), + refreshToken: protectedProcedure + .input(apiFindCompose) + .mutation(async ({ input, ctx }) => { + const compose = await findComposeById(input.composeId); + if (compose.project.adminId !== ctx.user.adminId) { + throw new TRPCError({ + code: "UNAUTHORIZED", + message: "You are not authorized to refresh this compose", + }); + } + await updateCompose(input.composeId, { + refreshToken: nanoid(), + }); + return true; + }), + deployTemplate: protectedProcedure + .input(apiCreateComposeByTemplate) + .mutation(async ({ ctx, input }) => { + if (ctx.user.rol === "user") { + await checkServiceAccess(ctx.user.authId, input.projectId, "create"); + } + + if (IS_CLOUD && !input.serverId) { + throw new TRPCError({ + code: "UNAUTHORIZED", + message: "You need to use a server to create a compose", + }); + } + + const composeFile = await readTemplateComposeFile(input.id); + + const generate = await loadTemplateModule(input.id as TemplatesKeys); + + const admin = await findAdminById(ctx.user.adminId); + let serverIp = admin.serverIp; + + if (!admin.serverIp) { + throw new TRPCError({ + code: "NOT_FOUND", + message: + "You need to have a server IP to deploy this template in order to generate domains", + }); + } + + const project = await findProjectById(input.projectId); + + if (input.serverId) { + const server = await findServerById(input.serverId); + serverIp = server.ipAddress; + } else if (process.env.NODE_ENV === "development") { + serverIp = "127.0.0.1"; + } + const projectName = slugify(`${project.name} ${input.id}`); + const { envs, mounts, domains } = generate({ + serverIp: serverIp || "", + projectName: projectName, + }); + + const compose = await createComposeByTemplate({ + ...input, + composeFile: composeFile, + env: envs?.join("\n"), + serverId: input.serverId, + name: input.id, + sourceType: "raw", + appName: `${projectName}-${generatePassword(6)}`, + }); + + if (ctx.user.rol === "user") { + await addNewService(ctx.user.authId, compose.composeId); + } + + if (mounts && mounts?.length > 0) { + for (const mount of mounts) { + await createMount({ + filePath: mount.filePath, + mountPath: "", + content: mount.content, + serviceId: compose.composeId, + serviceType: "compose", + type: "file", + }); + } + } + + if (domains && domains?.length > 0) { + for (const domain of domains) { + await createDomain({ + ...domain, + domainType: "compose", + certificateType: "none", + composeId: compose.composeId, + }); + } + } + + return null; + }), + + templates: protectedProcedure.query(async () => { + const templatesData = templates.map((t) => ({ + name: t.name, + description: t.description, + id: t.id, + links: t.links, + tags: t.tags, + logo: t.logo, + version: t.version, + })); + + return templatesData; + }), + + getTags: protectedProcedure.query(async ({ input }) => { + const allTags = templates.flatMap((template) => template.tags); + const uniqueTags = _.uniq(allTags); + return uniqueTags; + }), +}); diff --git a/apps/mig/server/api/routers/deployment.ts b/apps/mig/server/api/routers/deployment.ts new file mode 100644 index 00000000..bf981c6d --- /dev/null +++ b/apps/mig/server/api/routers/deployment.ts @@ -0,0 +1,55 @@ +import { + apiFindAllByApplication, + apiFindAllByCompose, + apiFindAllByServer, +} from "@/server/db/schema"; +import { + findAllDeploymentsByApplicationId, + findAllDeploymentsByComposeId, + findAllDeploymentsByServerId, + findApplicationById, + findComposeById, + findServerById, +} from "@dokploy/server"; +import { TRPCError } from "@trpc/server"; +import { createTRPCRouter, protectedProcedure } from "../trpc"; + +export const deploymentRouter = createTRPCRouter({ + all: protectedProcedure + .input(apiFindAllByApplication) + .query(async ({ input, ctx }) => { + const application = await findApplicationById(input.applicationId); + if (application.project.adminId !== ctx.user.adminId) { + throw new TRPCError({ + code: "UNAUTHORIZED", + message: "You are not authorized to access this application", + }); + } + return await findAllDeploymentsByApplicationId(input.applicationId); + }), + + allByCompose: protectedProcedure + .input(apiFindAllByCompose) + .query(async ({ input, ctx }) => { + const compose = await findComposeById(input.composeId); + if (compose.project.adminId !== ctx.user.adminId) { + throw new TRPCError({ + code: "UNAUTHORIZED", + message: "You are not authorized to access this compose", + }); + } + return await findAllDeploymentsByComposeId(input.composeId); + }), + allByServer: protectedProcedure + .input(apiFindAllByServer) + .query(async ({ input, ctx }) => { + const server = await findServerById(input.serverId); + if (server.adminId !== ctx.user.adminId) { + throw new TRPCError({ + code: "UNAUTHORIZED", + message: "You are not authorized to access this server", + }); + } + return await findAllDeploymentsByServerId(input.serverId); + }), +}); diff --git a/apps/mig/server/api/routers/destination.ts b/apps/mig/server/api/routers/destination.ts new file mode 100644 index 00000000..e960b278 --- /dev/null +++ b/apps/mig/server/api/routers/destination.ts @@ -0,0 +1,137 @@ +import { + adminProcedure, + createTRPCRouter, + protectedProcedure, +} from "@/server/api/trpc"; +import { db } from "@/server/db"; +import { + apiCreateDestination, + apiFindOneDestination, + apiRemoveDestination, + apiUpdateDestination, + destinations, +} from "@/server/db/schema"; +import { + IS_CLOUD, + createDestintation, + execAsync, + execAsyncRemote, + findDestinationById, + removeDestinationById, + updateDestinationById, +} from "@dokploy/server"; +import { TRPCError } from "@trpc/server"; +import { eq } from "drizzle-orm"; + +export const destinationRouter = createTRPCRouter({ + create: adminProcedure + .input(apiCreateDestination) + .mutation(async ({ input, ctx }) => { + try { + return await createDestintation(input, ctx.user.adminId); + } catch (error) { + throw new TRPCError({ + code: "BAD_REQUEST", + message: "Error to create the destination", + cause: error, + }); + } + }), + testConnection: adminProcedure + .input(apiCreateDestination) + .mutation(async ({ input }) => { + const { secretAccessKey, bucket, region, endpoint, accessKey } = input; + + try { + const rcloneFlags = [ + // `--s3-provider=Cloudflare`, + `--s3-access-key-id=${accessKey}`, + `--s3-secret-access-key=${secretAccessKey}`, + `--s3-region=${region}`, + `--s3-endpoint=${endpoint}`, + "--s3-no-check-bucket", + "--s3-force-path-style", + ]; + const rcloneDestination = `:s3:${bucket}`; + const rcloneCommand = `rclone ls ${rcloneFlags.join(" ")} "${rcloneDestination}"`; + + if (IS_CLOUD && !input.serverId) { + throw new TRPCError({ + code: "NOT_FOUND", + message: "Server not found", + }); + } + + if (IS_CLOUD) { + await execAsyncRemote(input.serverId || "", rcloneCommand); + } else { + await execAsync(rcloneCommand); + } + } catch (error) { + throw new TRPCError({ + code: "BAD_REQUEST", + message: + error instanceof Error + ? error?.message + : "Error to connect to bucket", + cause: error, + }); + } + }), + one: protectedProcedure + .input(apiFindOneDestination) + .query(async ({ input, ctx }) => { + const destination = await findDestinationById(input.destinationId); + if (destination.adminId !== ctx.user.adminId) { + throw new TRPCError({ + code: "UNAUTHORIZED", + message: "You are not allowed to access this destination", + }); + } + return destination; + }), + all: protectedProcedure.query(async ({ ctx }) => { + return await db.query.destinations.findMany({ + where: eq(destinations.adminId, ctx.user.adminId), + }); + }), + remove: adminProcedure + .input(apiRemoveDestination) + .mutation(async ({ input, ctx }) => { + try { + const destination = await findDestinationById(input.destinationId); + + if (destination.adminId !== ctx.user.adminId) { + throw new TRPCError({ + code: "UNAUTHORIZED", + message: "You are not allowed to delete this destination", + }); + } + return await removeDestinationById( + input.destinationId, + ctx.user.adminId, + ); + } catch (error) { + throw error; + } + }), + update: adminProcedure + .input(apiUpdateDestination) + .mutation(async ({ input, ctx }) => { + try { + const destination = await findDestinationById(input.destinationId); + if (destination.adminId !== ctx.user.adminId) { + throw new TRPCError({ + code: "UNAUTHORIZED", + message: "You are not allowed to update this destination", + }); + } + return await updateDestinationById(input.destinationId, { + ...input, + adminId: ctx.user.adminId, + }); + } catch (error) { + throw error; + } + }), +}); diff --git a/apps/mig/server/api/routers/docker.ts b/apps/mig/server/api/routers/docker.ts new file mode 100644 index 00000000..cb6b2712 --- /dev/null +++ b/apps/mig/server/api/routers/docker.ts @@ -0,0 +1,71 @@ +import { + containerRestart, + getConfig, + getContainers, + getContainersByAppLabel, + getContainersByAppNameMatch, +} from "@dokploy/server"; +import { z } from "zod"; +import { createTRPCRouter, protectedProcedure } from "../trpc"; + +export const dockerRouter = createTRPCRouter({ + getContainers: protectedProcedure + .input( + z.object({ + serverId: z.string().optional(), + }), + ) + .query(async ({ input }) => { + return await getContainers(input.serverId); + }), + + restartContainer: protectedProcedure + .input( + z.object({ + containerId: z.string().min(1), + }), + ) + .mutation(async ({ input }) => { + return await containerRestart(input.containerId); + }), + + getConfig: protectedProcedure + .input( + z.object({ + containerId: z.string().min(1), + serverId: z.string().optional(), + }), + ) + .query(async ({ input }) => { + return await getConfig(input.containerId, input.serverId); + }), + + getContainersByAppNameMatch: protectedProcedure + .input( + z.object({ + appType: z + .union([z.literal("stack"), z.literal("docker-compose")]) + .optional(), + appName: z.string().min(1), + serverId: z.string().optional(), + }), + ) + .query(async ({ input }) => { + return await getContainersByAppNameMatch( + input.appName, + input.appType, + input.serverId, + ); + }), + + getContainersByAppLabel: protectedProcedure + .input( + z.object({ + appName: z.string().min(1), + serverId: z.string().optional(), + }), + ) + .query(async ({ input }) => { + return await getContainersByAppLabel(input.appName, input.serverId); + }), +}); diff --git a/apps/mig/server/api/routers/domain.ts b/apps/mig/server/api/routers/domain.ts new file mode 100644 index 00000000..3c94fc93 --- /dev/null +++ b/apps/mig/server/api/routers/domain.ts @@ -0,0 +1,171 @@ +import { createTRPCRouter, protectedProcedure } from "@/server/api/trpc"; +import { + apiCreateDomain, + apiFindCompose, + apiFindDomain, + apiFindOneApplication, + apiUpdateDomain, +} from "@/server/db/schema"; +import { + createDomain, + findApplicationById, + findComposeById, + findDomainById, + findDomainsByApplicationId, + findDomainsByComposeId, + generateTraefikMeDomain, + manageDomain, + removeDomain, + removeDomainById, + updateDomainById, +} from "@dokploy/server"; +import { TRPCError } from "@trpc/server"; +import { z } from "zod"; + +export const domainRouter = createTRPCRouter({ + create: protectedProcedure + .input(apiCreateDomain) + .mutation(async ({ input, ctx }) => { + try { + if (input.domainType === "compose" && input.composeId) { + const compose = await findComposeById(input.composeId); + if (compose.project.adminId !== ctx.user.adminId) { + throw new TRPCError({ + code: "UNAUTHORIZED", + message: "You are not authorized to access this compose", + }); + } + } else if (input.domainType === "application" && input.applicationId) { + const application = await findApplicationById(input.applicationId); + if (application.project.adminId !== ctx.user.adminId) { + throw new TRPCError({ + code: "UNAUTHORIZED", + message: "You are not authorized to access this application", + }); + } + } + return await createDomain(input); + } catch (error) { + throw new TRPCError({ + code: "BAD_REQUEST", + message: "Error to create the domain", + cause: error, + }); + } + }), + byApplicationId: protectedProcedure + .input(apiFindOneApplication) + .query(async ({ input, ctx }) => { + const application = await findApplicationById(input.applicationId); + if (application.project.adminId !== ctx.user.adminId) { + throw new TRPCError({ + code: "UNAUTHORIZED", + message: "You are not authorized to access this application", + }); + } + return await findDomainsByApplicationId(input.applicationId); + }), + byComposeId: protectedProcedure + .input(apiFindCompose) + .query(async ({ input, ctx }) => { + const compose = await findComposeById(input.composeId); + if (compose.project.adminId !== ctx.user.adminId) { + throw new TRPCError({ + code: "UNAUTHORIZED", + message: "You are not authorized to access this compose", + }); + } + return await findDomainsByComposeId(input.composeId); + }), + generateDomain: protectedProcedure + .input(z.object({ appName: z.string(), serverId: z.string().optional() })) + .mutation(async ({ input, ctx }) => { + return generateTraefikMeDomain( + input.appName, + ctx.user.adminId, + input.serverId, + ); + }), + + update: protectedProcedure + .input(apiUpdateDomain) + .mutation(async ({ input, ctx }) => { + const currentDomain = await findDomainById(input.domainId); + + if (currentDomain.applicationId) { + const newApp = await findApplicationById(currentDomain.applicationId); + if (newApp.project.adminId !== ctx.user.adminId) { + throw new TRPCError({ + code: "UNAUTHORIZED", + message: "You are not authorized to access this application", + }); + } + } else if (currentDomain.composeId) { + const newCompose = await findComposeById(currentDomain.composeId); + if (newCompose.project.adminId !== ctx.user.adminId) { + throw new TRPCError({ + code: "UNAUTHORIZED", + message: "You are not authorized to access this compose", + }); + } + } + const result = await updateDomainById(input.domainId, input); + const domain = await findDomainById(input.domainId); + if (domain.applicationId) { + const application = await findApplicationById(domain.applicationId); + await manageDomain(application, domain); + } + return result; + }), + one: protectedProcedure.input(apiFindDomain).query(async ({ input, ctx }) => { + const domain = await findDomainById(input.domainId); + if (domain.applicationId) { + const application = await findApplicationById(domain.applicationId); + if (application.project.adminId !== ctx.user.adminId) { + throw new TRPCError({ + code: "UNAUTHORIZED", + message: "You are not authorized to access this application", + }); + } + } else if (domain.composeId) { + const compose = await findComposeById(domain.composeId); + if (compose.project.adminId !== ctx.user.adminId) { + throw new TRPCError({ + code: "UNAUTHORIZED", + message: "You are not authorized to access this compose", + }); + } + } + return await findDomainById(input.domainId); + }), + delete: protectedProcedure + .input(apiFindDomain) + .mutation(async ({ input, ctx }) => { + const domain = await findDomainById(input.domainId); + if (domain.applicationId) { + const application = await findApplicationById(domain.applicationId); + if (application.project.adminId !== ctx.user.adminId) { + throw new TRPCError({ + code: "UNAUTHORIZED", + message: "You are not authorized to access this application", + }); + } + } else if (domain.composeId) { + const compose = await findComposeById(domain.composeId); + if (compose.project.adminId !== ctx.user.adminId) { + throw new TRPCError({ + code: "UNAUTHORIZED", + message: "You are not authorized to access this compose", + }); + } + } + const result = await removeDomainById(input.domainId); + + if (domain.applicationId) { + const application = await findApplicationById(domain.applicationId); + await removeDomain(application, domain.uniqueConfigKey); + } + + return result; + }), +}); diff --git a/apps/mig/server/api/routers/git-provider.ts b/apps/mig/server/api/routers/git-provider.ts new file mode 100644 index 00000000..e1e8ef8b --- /dev/null +++ b/apps/mig/server/api/routers/git-provider.ts @@ -0,0 +1,46 @@ +import { createTRPCRouter, protectedProcedure } from "@/server/api/trpc"; +import { db } from "@/server/db"; +import { apiRemoveGitProvider, gitProvider } from "@/server/db/schema"; +import { + IS_CLOUD, + findGitProviderById, + removeGitProvider, +} from "@dokploy/server"; +import { TRPCError } from "@trpc/server"; +import { desc, eq } from "drizzle-orm"; + +export const gitProviderRouter = createTRPCRouter({ + getAll: protectedProcedure.query(async ({ ctx }) => { + return await db.query.gitProvider.findMany({ + with: { + gitlab: true, + bitbucket: true, + github: true, + }, + orderBy: desc(gitProvider.createdAt), + ...(IS_CLOUD && { where: eq(gitProvider.adminId, ctx.user.adminId) }), + //TODO: Remove this line when the cloud version is ready + }); + }), + remove: protectedProcedure + .input(apiRemoveGitProvider) + .mutation(async ({ input, ctx }) => { + try { + const gitProvider = await findGitProviderById(input.gitProviderId); + + if (IS_CLOUD && gitProvider.adminId !== ctx.user.adminId) { + // TODO: Remove isCloud in the next versions of dokploy + throw new TRPCError({ + code: "UNAUTHORIZED", + message: "You are not allowed to delete this git provider", + }); + } + return await removeGitProvider(input.gitProviderId); + } catch (error) { + throw new TRPCError({ + code: "BAD_REQUEST", + message: "Error to delete this git provider", + }); + } + }), +}); diff --git a/apps/mig/server/api/routers/github.ts b/apps/mig/server/api/routers/github.ts new file mode 100644 index 00000000..56222577 --- /dev/null +++ b/apps/mig/server/api/routers/github.ts @@ -0,0 +1,126 @@ +import { createTRPCRouter, protectedProcedure } from "@/server/api/trpc"; +import { db } from "@/server/db"; +import { + apiFindGithubBranches, + apiFindOneGithub, + apiUpdateGithub, +} from "@/server/db/schema"; +import { + IS_CLOUD, + findGithubById, + getGithubBranches, + getGithubRepositories, + haveGithubRequirements, + updateGitProvider, +} from "@dokploy/server"; +import { TRPCError } from "@trpc/server"; + +export const githubRouter = createTRPCRouter({ + one: protectedProcedure + .input(apiFindOneGithub) + .query(async ({ input, ctx }) => { + const githubProvider = await findGithubById(input.githubId); + if (IS_CLOUD && githubProvider.gitProvider.adminId !== ctx.user.adminId) { + //TODO: Remove this line when the cloud version is ready + throw new TRPCError({ + code: "UNAUTHORIZED", + message: "You are not allowed to access this github provider", + }); + } + return githubProvider; + }), + getGithubRepositories: protectedProcedure + .input(apiFindOneGithub) + .query(async ({ input, ctx }) => { + const githubProvider = await findGithubById(input.githubId); + if (IS_CLOUD && githubProvider.gitProvider.adminId !== ctx.user.adminId) { + //TODO: Remove this line when the cloud version is ready + throw new TRPCError({ + code: "UNAUTHORIZED", + message: "You are not allowed to access this github provider", + }); + } + return await getGithubRepositories(input.githubId); + }), + getGithubBranches: protectedProcedure + .input(apiFindGithubBranches) + .query(async ({ input, ctx }) => { + const githubProvider = await findGithubById(input.githubId || ""); + if (IS_CLOUD && githubProvider.gitProvider.adminId !== ctx.user.adminId) { + //TODO: Remove this line when the cloud version is ready + throw new TRPCError({ + code: "UNAUTHORIZED", + message: "You are not allowed to access this github provider", + }); + } + return await getGithubBranches(input); + }), + githubProviders: protectedProcedure.query(async ({ ctx }) => { + let result = await db.query.github.findMany({ + with: { + gitProvider: true, + }, + }); + + if (IS_CLOUD) { + // TODO: mAyBe a rEfaCtoR 🤫 + result = result.filter( + (provider) => provider.gitProvider.adminId === ctx.user.adminId, + ); + } + + const filtered = result + .filter((provider) => haveGithubRequirements(provider)) + .map((provider) => { + return { + githubId: provider.githubId, + gitProvider: { + ...provider.gitProvider, + }, + }; + }); + + return filtered; + }), + + testConnection: protectedProcedure + .input(apiFindOneGithub) + .mutation(async ({ input, ctx }) => { + try { + const githubProvider = await findGithubById(input.githubId); + if ( + IS_CLOUD && + githubProvider.gitProvider.adminId !== ctx.user.adminId + ) { + //TODO: Remove this line when the cloud version is ready + throw new TRPCError({ + code: "UNAUTHORIZED", + message: "You are not allowed to access this github provider", + }); + } + const result = await getGithubRepositories(input.githubId); + return `Found ${result.length} repositories`; + } catch (err) { + throw new TRPCError({ + code: "BAD_REQUEST", + message: err instanceof Error ? err?.message : `Error: ${err}`, + }); + } + }), + update: protectedProcedure + .input(apiUpdateGithub) + .mutation(async ({ input, ctx }) => { + const githubProvider = await findGithubById(input.githubId); + if (IS_CLOUD && githubProvider.gitProvider.adminId !== ctx.user.adminId) { + //TODO: Remove this line when the cloud version is ready + throw new TRPCError({ + code: "UNAUTHORIZED", + message: "You are not allowed to access this github provider", + }); + } + await updateGitProvider(input.gitProviderId, { + name: input.name, + adminId: ctx.user.adminId, + }); + }), +}); diff --git a/apps/mig/server/api/routers/gitlab.ts b/apps/mig/server/api/routers/gitlab.ts new file mode 100644 index 00000000..f66dcbe3 --- /dev/null +++ b/apps/mig/server/api/routers/gitlab.ts @@ -0,0 +1,151 @@ +import { createTRPCRouter, protectedProcedure } from "@/server/api/trpc"; +import { + apiCreateGitlab, + apiFindGitlabBranches, + apiFindOneGitlab, + apiGitlabTestConnection, + apiUpdateGitlab, +} from "@/server/db/schema"; + +import { db } from "@/server/db"; +import { + IS_CLOUD, + createGitlab, + findGitlabById, + getGitlabBranches, + getGitlabRepositories, + haveGitlabRequirements, + testGitlabConnection, + updateGitProvider, + updateGitlab, +} from "@dokploy/server"; +import { TRPCError } from "@trpc/server"; + +export const gitlabRouter = createTRPCRouter({ + create: protectedProcedure + .input(apiCreateGitlab) + .mutation(async ({ input, ctx }) => { + try { + return await createGitlab(input, ctx.user.adminId); + } catch (error) { + throw new TRPCError({ + code: "BAD_REQUEST", + message: "Error to create this gitlab provider", + cause: error, + }); + } + }), + one: protectedProcedure + .input(apiFindOneGitlab) + .query(async ({ input, ctx }) => { + const gitlabProvider = await findGitlabById(input.gitlabId); + if (IS_CLOUD && gitlabProvider.gitProvider.adminId !== ctx.user.adminId) { + //TODO: Remove this line when the cloud version is ready + throw new TRPCError({ + code: "UNAUTHORIZED", + message: "You are not allowed to access this gitlab provider", + }); + } + return gitlabProvider; + }), + gitlabProviders: protectedProcedure.query(async ({ ctx }) => { + let result = await db.query.gitlab.findMany({ + with: { + gitProvider: true, + }, + }); + + if (IS_CLOUD) { + // TODO: mAyBe a rEfaCtoR 🤫 + result = result.filter( + (provider) => provider.gitProvider.adminId === ctx.user.adminId, + ); + } + const filtered = result + .filter((provider) => haveGitlabRequirements(provider)) + .map((provider) => { + return { + gitlabId: provider.gitlabId, + gitProvider: { + ...provider.gitProvider, + }, + }; + }); + + return filtered; + }), + getGitlabRepositories: protectedProcedure + .input(apiFindOneGitlab) + .query(async ({ input, ctx }) => { + const gitlabProvider = await findGitlabById(input.gitlabId); + if (IS_CLOUD && gitlabProvider.gitProvider.adminId !== ctx.user.adminId) { + //TODO: Remove this line when the cloud version is ready + throw new TRPCError({ + code: "UNAUTHORIZED", + message: "You are not allowed to access this gitlab provider", + }); + } + return await getGitlabRepositories(input.gitlabId); + }), + + getGitlabBranches: protectedProcedure + .input(apiFindGitlabBranches) + .query(async ({ input, ctx }) => { + const gitlabProvider = await findGitlabById(input.gitlabId || ""); + if (IS_CLOUD && gitlabProvider.gitProvider.adminId !== ctx.user.adminId) { + //TODO: Remove this line when the cloud version is ready + throw new TRPCError({ + code: "UNAUTHORIZED", + message: "You are not allowed to access this gitlab provider", + }); + } + return await getGitlabBranches(input); + }), + testConnection: protectedProcedure + .input(apiGitlabTestConnection) + .mutation(async ({ input, ctx }) => { + try { + const gitlabProvider = await findGitlabById(input.gitlabId || ""); + if ( + IS_CLOUD && + gitlabProvider.gitProvider.adminId !== ctx.user.adminId + ) { + //TODO: Remove this line when the cloud version is ready + throw new TRPCError({ + code: "UNAUTHORIZED", + message: "You are not allowed to access this gitlab provider", + }); + } + const result = await testGitlabConnection(input); + + return `Found ${result} repositories`; + } catch (error) { + throw new TRPCError({ + code: "BAD_REQUEST", + message: error instanceof Error ? error?.message : `Error: ${error}`, + }); + } + }), + update: protectedProcedure + .input(apiUpdateGitlab) + .mutation(async ({ input, ctx }) => { + const gitlabProvider = await findGitlabById(input.gitlabId); + if (IS_CLOUD && gitlabProvider.gitProvider.adminId !== ctx.user.adminId) { + //TODO: Remove this line when the cloud version is ready + throw new TRPCError({ + code: "UNAUTHORIZED", + message: "You are not allowed to access this gitlab provider", + }); + } + if (input.name) { + await updateGitProvider(input.gitProviderId, { + name: input.name, + adminId: ctx.user.adminId, + }); + } else { + await updateGitlab(input.gitlabId, { + ...input, + }); + } + }), +}); diff --git a/apps/mig/server/api/routers/mariadb.ts b/apps/mig/server/api/routers/mariadb.ts new file mode 100644 index 00000000..6755d647 --- /dev/null +++ b/apps/mig/server/api/routers/mariadb.ts @@ -0,0 +1,277 @@ +import { createTRPCRouter, protectedProcedure } from "@/server/api/trpc"; +import { + apiChangeMariaDBStatus, + apiCreateMariaDB, + apiDeployMariaDB, + apiFindOneMariaDB, + apiResetMariadb, + apiSaveEnvironmentVariablesMariaDB, + apiSaveExternalPortMariaDB, + apiUpdateMariaDB, +} from "@/server/db/schema"; +import { + IS_CLOUD, + addNewService, + checkServiceAccess, + createMariadb, + createMount, + deployMariadb, + findMariadbById, + findProjectById, + findServerById, + removeMariadbById, + removeService, + startService, + startServiceRemote, + stopService, + stopServiceRemote, + updateMariadbById, +} from "@dokploy/server"; +import { TRPCError } from "@trpc/server"; + +export const mariadbRouter = createTRPCRouter({ + create: protectedProcedure + .input(apiCreateMariaDB) + .mutation(async ({ input, ctx }) => { + try { + if (ctx.user.rol === "user") { + await checkServiceAccess(ctx.user.authId, input.projectId, "create"); + } + + if (IS_CLOUD && !input.serverId) { + throw new TRPCError({ + code: "UNAUTHORIZED", + message: "You need to use a server to create a mariadb", + }); + } + + const project = await findProjectById(input.projectId); + if (project.adminId !== ctx.user.adminId) { + throw new TRPCError({ + code: "UNAUTHORIZED", + message: "You are not authorized to access this project", + }); + } + const newMariadb = await createMariadb(input); + if (ctx.user.rol === "user") { + await addNewService(ctx.user.authId, newMariadb.mariadbId); + } + + await createMount({ + serviceId: newMariadb.mariadbId, + serviceType: "mariadb", + volumeName: `${newMariadb.appName}-data`, + mountPath: "/var/lib/mysql", + type: "volume", + }); + + return true; + } catch (error) { + if (error instanceof TRPCError) { + throw error; + } + throw error; + } + }), + one: protectedProcedure + .input(apiFindOneMariaDB) + .query(async ({ input, ctx }) => { + if (ctx.user.rol === "user") { + await checkServiceAccess(ctx.user.authId, input.mariadbId, "access"); + } + const mariadb = await findMariadbById(input.mariadbId); + if (mariadb.project.adminId !== ctx.user.adminId) { + throw new TRPCError({ + code: "UNAUTHORIZED", + message: "You are not authorized to access this mariadb", + }); + } + return mariadb; + }), + + start: protectedProcedure + .input(apiFindOneMariaDB) + .mutation(async ({ input, ctx }) => { + const service = await findMariadbById(input.mariadbId); + if (service.project.adminId !== ctx.user.adminId) { + throw new TRPCError({ + code: "UNAUTHORIZED", + message: "You are not authorized to start this mariadb", + }); + } + if (service.serverId) { + await startServiceRemote(service.serverId, service.appName); + } else { + await startService(service.appName); + } + await updateMariadbById(input.mariadbId, { + applicationStatus: "done", + }); + + return service; + }), + stop: protectedProcedure + .input(apiFindOneMariaDB) + .mutation(async ({ input }) => { + const mariadb = await findMariadbById(input.mariadbId); + + if (mariadb.serverId) { + await stopServiceRemote(mariadb.serverId, mariadb.appName); + } else { + await stopService(mariadb.appName); + } + await updateMariadbById(input.mariadbId, { + applicationStatus: "idle", + }); + + return mariadb; + }), + saveExternalPort: protectedProcedure + .input(apiSaveExternalPortMariaDB) + .mutation(async ({ input, ctx }) => { + const mongo = await findMariadbById(input.mariadbId); + if (mongo.project.adminId !== ctx.user.adminId) { + throw new TRPCError({ + code: "UNAUTHORIZED", + message: "You are not authorized to save this external port", + }); + } + await updateMariadbById(input.mariadbId, { + externalPort: input.externalPort, + }); + await deployMariadb(input.mariadbId); + return mongo; + }), + deploy: protectedProcedure + .input(apiDeployMariaDB) + .mutation(async ({ input, ctx }) => { + const mariadb = await findMariadbById(input.mariadbId); + if (mariadb.project.adminId !== ctx.user.adminId) { + throw new TRPCError({ + code: "UNAUTHORIZED", + message: "You are not authorized to deploy this mariadb", + }); + } + + return deployMariadb(input.mariadbId); + }), + changeStatus: protectedProcedure + .input(apiChangeMariaDBStatus) + .mutation(async ({ input, ctx }) => { + const mongo = await findMariadbById(input.mariadbId); + if (mongo.project.adminId !== ctx.user.adminId) { + throw new TRPCError({ + code: "UNAUTHORIZED", + message: "You are not authorized to change this mariadb status", + }); + } + await updateMariadbById(input.mariadbId, { + applicationStatus: input.applicationStatus, + }); + return mongo; + }), + remove: protectedProcedure + .input(apiFindOneMariaDB) + .mutation(async ({ input, ctx }) => { + if (ctx.user.rol === "user") { + await checkServiceAccess(ctx.user.authId, input.mariadbId, "delete"); + } + + const mongo = await findMariadbById(input.mariadbId); + if (mongo.project.adminId !== ctx.user.adminId) { + throw new TRPCError({ + code: "UNAUTHORIZED", + message: "You are not authorized to delete this mariadb", + }); + } + + const cleanupOperations = [ + async () => await removeService(mongo?.appName, mongo.serverId), + async () => await removeMariadbById(input.mariadbId), + ]; + + for (const operation of cleanupOperations) { + try { + await operation(); + } catch (error) {} + } + + return mongo; + }), + saveEnvironment: protectedProcedure + .input(apiSaveEnvironmentVariablesMariaDB) + .mutation(async ({ input, ctx }) => { + const mariadb = await findMariadbById(input.mariadbId); + if (mariadb.project.adminId !== ctx.user.adminId) { + throw new TRPCError({ + code: "UNAUTHORIZED", + message: "You are not authorized to save this environment", + }); + } + const service = await updateMariadbById(input.mariadbId, { + env: input.env, + }); + + if (!service) { + throw new TRPCError({ + code: "BAD_REQUEST", + message: "Update: Error to add environment variables", + }); + } + + return true; + }), + reload: protectedProcedure + .input(apiResetMariadb) + .mutation(async ({ input, ctx }) => { + const mariadb = await findMariadbById(input.mariadbId); + if (mariadb.project.adminId !== ctx.user.adminId) { + throw new TRPCError({ + code: "UNAUTHORIZED", + message: "You are not authorized to reload this mariadb", + }); + } + if (mariadb.serverId) { + await stopServiceRemote(mariadb.serverId, mariadb.appName); + } else { + await stopService(mariadb.appName); + } + await updateMariadbById(input.mariadbId, { + applicationStatus: "idle", + }); + + if (mariadb.serverId) { + await startServiceRemote(mariadb.serverId, mariadb.appName); + } else { + await startService(mariadb.appName); + } + await updateMariadbById(input.mariadbId, { + applicationStatus: "done", + }); + return true; + }), + update: protectedProcedure + .input(apiUpdateMariaDB) + .mutation(async ({ input, ctx }) => { + const { mariadbId, ...rest } = input; + const mariadb = await findMariadbById(mariadbId); + if (mariadb.project.adminId !== ctx.user.adminId) { + throw new TRPCError({ + code: "UNAUTHORIZED", + message: "You are not authorized to update this mariadb", + }); + } + const service = await updateMariadbById(mariadbId, { + ...rest, + }); + + if (!service) { + throw new TRPCError({ + code: "BAD_REQUEST", + message: "Update: Error to update mariadb", + }); + } + + return true; + }), +}); diff --git a/apps/mig/server/api/routers/mongo.ts b/apps/mig/server/api/routers/mongo.ts new file mode 100644 index 00000000..49dc5ec2 --- /dev/null +++ b/apps/mig/server/api/routers/mongo.ts @@ -0,0 +1,290 @@ +import { createTRPCRouter, protectedProcedure } from "@/server/api/trpc"; +import { + apiChangeMongoStatus, + apiCreateMongo, + apiDeployMongo, + apiFindOneMongo, + apiResetMongo, + apiSaveEnvironmentVariablesMongo, + apiSaveExternalPortMongo, + apiUpdateMongo, +} from "@/server/db/schema"; +import { + IS_CLOUD, + addNewService, + checkServiceAccess, + createMongo, + createMount, + deployMongo, + findMongoById, + findProjectById, + removeMongoById, + removeService, + startService, + startServiceRemote, + stopService, + stopServiceRemote, + updateMongoById, +} from "@dokploy/server"; +import { TRPCError } from "@trpc/server"; + +export const mongoRouter = createTRPCRouter({ + create: protectedProcedure + .input(apiCreateMongo) + .mutation(async ({ input, ctx }) => { + try { + if (ctx.user.rol === "user") { + await checkServiceAccess(ctx.user.authId, input.projectId, "create"); + } + + if (IS_CLOUD && !input.serverId) { + throw new TRPCError({ + code: "UNAUTHORIZED", + message: "You need to use a server to create a mongo", + }); + } + + const project = await findProjectById(input.projectId); + if (project.adminId !== ctx.user.adminId) { + throw new TRPCError({ + code: "UNAUTHORIZED", + message: "You are not authorized to access this project", + }); + } + const newMongo = await createMongo(input); + if (ctx.user.rol === "user") { + await addNewService(ctx.user.authId, newMongo.mongoId); + } + + await createMount({ + serviceId: newMongo.mongoId, + serviceType: "mongo", + volumeName: `${newMongo.appName}-data`, + mountPath: "/data/db", + type: "volume", + }); + + return true; + } catch (error) { + if (error instanceof TRPCError) { + throw error; + } + throw new TRPCError({ + code: "BAD_REQUEST", + message: "Error input: Inserting mongo database", + cause: error, + }); + } + }), + one: protectedProcedure + .input(apiFindOneMongo) + .query(async ({ input, ctx }) => { + if (ctx.user.rol === "user") { + await checkServiceAccess(ctx.user.authId, input.mongoId, "access"); + } + + const mongo = await findMongoById(input.mongoId); + if (mongo.project.adminId !== ctx.user.adminId) { + throw new TRPCError({ + code: "UNAUTHORIZED", + message: "You are not authorized to access this mongo", + }); + } + return mongo; + }), + + start: protectedProcedure + .input(apiFindOneMongo) + .mutation(async ({ input, ctx }) => { + const service = await findMongoById(input.mongoId); + + if (service.project.adminId !== ctx.user.adminId) { + throw new TRPCError({ + code: "UNAUTHORIZED", + message: "You are not authorized to start this mongo", + }); + } + + if (service.serverId) { + await startServiceRemote(service.serverId, service.appName); + } else { + await startService(service.appName); + } + await updateMongoById(input.mongoId, { + applicationStatus: "done", + }); + + return service; + }), + stop: protectedProcedure + .input(apiFindOneMongo) + .mutation(async ({ input, ctx }) => { + const mongo = await findMongoById(input.mongoId); + + if (mongo.project.adminId !== ctx.user.adminId) { + throw new TRPCError({ + code: "UNAUTHORIZED", + message: "You are not authorized to stop this mongo", + }); + } + + if (mongo.serverId) { + await stopServiceRemote(mongo.serverId, mongo.appName); + } else { + await stopService(mongo.appName); + } + await updateMongoById(input.mongoId, { + applicationStatus: "idle", + }); + + return mongo; + }), + saveExternalPort: protectedProcedure + .input(apiSaveExternalPortMongo) + .mutation(async ({ input, ctx }) => { + const mongo = await findMongoById(input.mongoId); + if (mongo.project.adminId !== ctx.user.adminId) { + throw new TRPCError({ + code: "UNAUTHORIZED", + message: "You are not authorized to save this external port", + }); + } + await updateMongoById(input.mongoId, { + externalPort: input.externalPort, + }); + await deployMongo(input.mongoId); + return mongo; + }), + deploy: protectedProcedure + .input(apiDeployMongo) + .mutation(async ({ input, ctx }) => { + const mongo = await findMongoById(input.mongoId); + if (mongo.project.adminId !== ctx.user.adminId) { + throw new TRPCError({ + code: "UNAUTHORIZED", + message: "You are not authorized to deploy this mongo", + }); + } + return deployMongo(input.mongoId); + }), + changeStatus: protectedProcedure + .input(apiChangeMongoStatus) + .mutation(async ({ input, ctx }) => { + const mongo = await findMongoById(input.mongoId); + if (mongo.project.adminId !== ctx.user.adminId) { + throw new TRPCError({ + code: "UNAUTHORIZED", + message: "You are not authorized to change this mongo status", + }); + } + await updateMongoById(input.mongoId, { + applicationStatus: input.applicationStatus, + }); + return mongo; + }), + reload: protectedProcedure + .input(apiResetMongo) + .mutation(async ({ input, ctx }) => { + const mongo = await findMongoById(input.mongoId); + if (mongo.project.adminId !== ctx.user.adminId) { + throw new TRPCError({ + code: "UNAUTHORIZED", + message: "You are not authorized to reload this mongo", + }); + } + if (mongo.serverId) { + await stopServiceRemote(mongo.serverId, mongo.appName); + } else { + await stopService(mongo.appName); + } + await updateMongoById(input.mongoId, { + applicationStatus: "idle", + }); + + if (mongo.serverId) { + await startServiceRemote(mongo.serverId, mongo.appName); + } else { + await startService(mongo.appName); + } + await updateMongoById(input.mongoId, { + applicationStatus: "done", + }); + return true; + }), + remove: protectedProcedure + .input(apiFindOneMongo) + .mutation(async ({ input, ctx }) => { + if (ctx.user.rol === "user") { + await checkServiceAccess(ctx.user.authId, input.mongoId, "delete"); + } + + const mongo = await findMongoById(input.mongoId); + + if (mongo.project.adminId !== ctx.user.adminId) { + throw new TRPCError({ + code: "UNAUTHORIZED", + message: "You are not authorized to delete this mongo", + }); + } + + const cleanupOperations = [ + async () => await removeService(mongo?.appName, mongo.serverId), + async () => await removeMongoById(input.mongoId), + ]; + + for (const operation of cleanupOperations) { + try { + await operation(); + } catch (error) {} + } + + return mongo; + }), + saveEnvironment: protectedProcedure + .input(apiSaveEnvironmentVariablesMongo) + .mutation(async ({ input, ctx }) => { + const mongo = await findMongoById(input.mongoId); + if (mongo.project.adminId !== ctx.user.adminId) { + throw new TRPCError({ + code: "UNAUTHORIZED", + message: "You are not authorized to save this environment", + }); + } + const service = await updateMongoById(input.mongoId, { + env: input.env, + }); + + if (!service) { + throw new TRPCError({ + code: "BAD_REQUEST", + message: "Update: Error to add environment variables", + }); + } + + return true; + }), + update: protectedProcedure + .input(apiUpdateMongo) + .mutation(async ({ input, ctx }) => { + const { mongoId, ...rest } = input; + const mongo = await findMongoById(mongoId); + if (mongo.project.adminId !== ctx.user.adminId) { + throw new TRPCError({ + code: "UNAUTHORIZED", + message: "You are not authorized to update this mongo", + }); + } + const service = await updateMongoById(mongoId, { + ...rest, + }); + + if (!service) { + throw new TRPCError({ + code: "BAD_REQUEST", + message: "Update: Error to update mongo", + }); + } + + return true; + }), +}); diff --git a/apps/mig/server/api/routers/mount.ts b/apps/mig/server/api/routers/mount.ts new file mode 100644 index 00000000..0cfb0c07 --- /dev/null +++ b/apps/mig/server/api/routers/mount.ts @@ -0,0 +1,37 @@ +import { + apiCreateMount, + apiFindOneMount, + apiRemoveMount, + apiUpdateMount, +} from "@/server/db/schema"; +import { + createMount, + deleteMount, + findMountById, + updateMount, +} from "@dokploy/server"; +import { createTRPCRouter, protectedProcedure } from "../trpc"; + +export const mountRouter = createTRPCRouter({ + create: protectedProcedure + .input(apiCreateMount) + .mutation(async ({ input }) => { + await createMount(input); + return true; + }), + remove: protectedProcedure + .input(apiRemoveMount) + .mutation(async ({ input }) => { + return await deleteMount(input.mountId); + }), + + one: protectedProcedure.input(apiFindOneMount).query(async ({ input }) => { + return await findMountById(input.mountId); + }), + update: protectedProcedure + .input(apiUpdateMount) + .mutation(async ({ input }) => { + await updateMount(input.mountId, input); + return true; + }), +}); diff --git a/apps/mig/server/api/routers/mysql.ts b/apps/mig/server/api/routers/mysql.ts new file mode 100644 index 00000000..893adb9c --- /dev/null +++ b/apps/mig/server/api/routers/mysql.ts @@ -0,0 +1,286 @@ +import { createTRPCRouter, protectedProcedure } from "@/server/api/trpc"; +import { + apiChangeMySqlStatus, + apiCreateMySql, + apiDeployMySql, + apiFindOneMySql, + apiResetMysql, + apiSaveEnvironmentVariablesMySql, + apiSaveExternalPortMySql, + apiUpdateMySql, +} from "@/server/db/schema"; + +import { TRPCError } from "@trpc/server"; + +import { + IS_CLOUD, + addNewService, + checkServiceAccess, + createMount, + createMysql, + deployMySql, + findMySqlById, + findProjectById, + removeMySqlById, + removeService, + startService, + startServiceRemote, + stopService, + stopServiceRemote, + updateMySqlById, +} from "@dokploy/server"; + +export const mysqlRouter = createTRPCRouter({ + create: protectedProcedure + .input(apiCreateMySql) + .mutation(async ({ input, ctx }) => { + try { + if (ctx.user.rol === "user") { + await checkServiceAccess(ctx.user.authId, input.projectId, "create"); + } + + if (IS_CLOUD && !input.serverId) { + throw new TRPCError({ + code: "UNAUTHORIZED", + message: "You need to use a server to create a mysql", + }); + } + 1; + const project = await findProjectById(input.projectId); + if (project.adminId !== ctx.user.adminId) { + throw new TRPCError({ + code: "UNAUTHORIZED", + message: "You are not authorized to access this project", + }); + } + + const newMysql = await createMysql(input); + if (ctx.user.rol === "user") { + await addNewService(ctx.user.authId, newMysql.mysqlId); + } + + await createMount({ + serviceId: newMysql.mysqlId, + serviceType: "mysql", + volumeName: `${newMysql.appName}-data`, + mountPath: "/var/lib/mysql", + type: "volume", + }); + + return true; + } catch (error) { + if (error instanceof TRPCError) { + throw error; + } + throw new TRPCError({ + code: "BAD_REQUEST", + message: "Error input: Inserting mysql database", + cause: error, + }); + } + }), + one: protectedProcedure + .input(apiFindOneMySql) + .query(async ({ input, ctx }) => { + if (ctx.user.rol === "user") { + await checkServiceAccess(ctx.user.authId, input.mysqlId, "access"); + } + const mysql = await findMySqlById(input.mysqlId); + if (mysql.project.adminId !== ctx.user.adminId) { + throw new TRPCError({ + code: "UNAUTHORIZED", + message: "You are not authorized to access this mysql", + }); + } + return mysql; + }), + + start: protectedProcedure + .input(apiFindOneMySql) + .mutation(async ({ input, ctx }) => { + const service = await findMySqlById(input.mysqlId); + if (service.project.adminId !== ctx.user.adminId) { + throw new TRPCError({ + code: "UNAUTHORIZED", + message: "You are not authorized to start this mysql", + }); + } + + if (service.serverId) { + await startServiceRemote(service.serverId, service.appName); + } else { + await startService(service.appName); + } + await updateMySqlById(input.mysqlId, { + applicationStatus: "done", + }); + + return service; + }), + stop: protectedProcedure + .input(apiFindOneMySql) + .mutation(async ({ input, ctx }) => { + const mongo = await findMySqlById(input.mysqlId); + if (mongo.project.adminId !== ctx.user.adminId) { + throw new TRPCError({ + code: "UNAUTHORIZED", + message: "You are not authorized to stop this mysql", + }); + } + if (mongo.serverId) { + await stopServiceRemote(mongo.serverId, mongo.appName); + } else { + await stopService(mongo.appName); + } + await updateMySqlById(input.mysqlId, { + applicationStatus: "idle", + }); + + return mongo; + }), + saveExternalPort: protectedProcedure + .input(apiSaveExternalPortMySql) + .mutation(async ({ input, ctx }) => { + const mongo = await findMySqlById(input.mysqlId); + if (mongo.project.adminId !== ctx.user.adminId) { + throw new TRPCError({ + code: "UNAUTHORIZED", + message: "You are not authorized to save this external port", + }); + } + await updateMySqlById(input.mysqlId, { + externalPort: input.externalPort, + }); + await deployMySql(input.mysqlId); + return mongo; + }), + deploy: protectedProcedure + .input(apiDeployMySql) + .mutation(async ({ input, ctx }) => { + const mysql = await findMySqlById(input.mysqlId); + if (mysql.project.adminId !== ctx.user.adminId) { + throw new TRPCError({ + code: "UNAUTHORIZED", + message: "You are not authorized to deploy this mysql", + }); + } + return deployMySql(input.mysqlId); + }), + changeStatus: protectedProcedure + .input(apiChangeMySqlStatus) + .mutation(async ({ input, ctx }) => { + const mongo = await findMySqlById(input.mysqlId); + if (mongo.project.adminId !== ctx.user.adminId) { + throw new TRPCError({ + code: "UNAUTHORIZED", + message: "You are not authorized to change this mysql status", + }); + } + await updateMySqlById(input.mysqlId, { + applicationStatus: input.applicationStatus, + }); + return mongo; + }), + reload: protectedProcedure + .input(apiResetMysql) + .mutation(async ({ input, ctx }) => { + const mysql = await findMySqlById(input.mysqlId); + if (mysql.project.adminId !== ctx.user.adminId) { + throw new TRPCError({ + code: "UNAUTHORIZED", + message: "You are not authorized to reload this mysql", + }); + } + if (mysql.serverId) { + await stopServiceRemote(mysql.serverId, mysql.appName); + } else { + await stopService(mysql.appName); + } + await updateMySqlById(input.mysqlId, { + applicationStatus: "idle", + }); + if (mysql.serverId) { + await startServiceRemote(mysql.serverId, mysql.appName); + } else { + await startService(mysql.appName); + } + await updateMySqlById(input.mysqlId, { + applicationStatus: "done", + }); + return true; + }), + remove: protectedProcedure + .input(apiFindOneMySql) + .mutation(async ({ input, ctx }) => { + if (ctx.user.rol === "user") { + await checkServiceAccess(ctx.user.authId, input.mysqlId, "delete"); + } + const mongo = await findMySqlById(input.mysqlId); + if (mongo.project.adminId !== ctx.user.adminId) { + throw new TRPCError({ + code: "UNAUTHORIZED", + message: "You are not authorized to delete this mysql", + }); + } + + const cleanupOperations = [ + async () => await removeService(mongo?.appName, mongo.serverId), + async () => await removeMySqlById(input.mysqlId), + ]; + + for (const operation of cleanupOperations) { + try { + await operation(); + } catch (error) {} + } + + return mongo; + }), + saveEnvironment: protectedProcedure + .input(apiSaveEnvironmentVariablesMySql) + .mutation(async ({ input, ctx }) => { + const mysql = await findMySqlById(input.mysqlId); + if (mysql.project.adminId !== ctx.user.adminId) { + throw new TRPCError({ + code: "UNAUTHORIZED", + message: "You are not authorized to save this environment", + }); + } + const service = await updateMySqlById(input.mysqlId, { + env: input.env, + }); + + if (!service) { + throw new TRPCError({ + code: "BAD_REQUEST", + message: "Update: Error to add environment variables", + }); + } + + return true; + }), + update: protectedProcedure + .input(apiUpdateMySql) + .mutation(async ({ input, ctx }) => { + const { mysqlId, ...rest } = input; + const mysql = await findMySqlById(mysqlId); + if (mysql.project.adminId !== ctx.user.adminId) { + throw new TRPCError({ + code: "UNAUTHORIZED", + message: "You are not authorized to update this mysql", + }); + } + const service = await updateMySqlById(mysqlId, { + ...rest, + }); + + if (!service) { + throw new TRPCError({ + code: "BAD_REQUEST", + message: "Update: Error to update mysql", + }); + } + + return true; + }), +}); diff --git a/apps/mig/server/api/routers/notification.ts b/apps/mig/server/api/routers/notification.ts new file mode 100644 index 00000000..170b7bf2 --- /dev/null +++ b/apps/mig/server/api/routers/notification.ts @@ -0,0 +1,304 @@ +import { + adminProcedure, + createTRPCRouter, + protectedProcedure, +} from "@/server/api/trpc"; +import { db } from "@/server/db"; +import { + apiCreateDiscord, + apiCreateEmail, + apiCreateSlack, + apiCreateTelegram, + apiFindOneNotification, + apiTestDiscordConnection, + apiTestEmailConnection, + apiTestSlackConnection, + apiTestTelegramConnection, + apiUpdateDiscord, + apiUpdateEmail, + apiUpdateSlack, + apiUpdateTelegram, + notifications, +} from "@/server/db/schema"; +import { + IS_CLOUD, + createDiscordNotification, + createEmailNotification, + createSlackNotification, + createTelegramNotification, + findNotificationById, + removeNotificationById, + sendDiscordNotification, + sendEmailNotification, + sendSlackNotification, + sendTelegramNotification, + updateDiscordNotification, + updateEmailNotification, + updateSlackNotification, + updateTelegramNotification, +} from "@dokploy/server"; +import { TRPCError } from "@trpc/server"; +import { desc, eq } from "drizzle-orm"; + +// TODO: Uncomment the validations when is cloud ready +export const notificationRouter = createTRPCRouter({ + createSlack: adminProcedure + .input(apiCreateSlack) + .mutation(async ({ input, ctx }) => { + try { + return await createSlackNotification(input, ctx.user.adminId); + } catch (error) { + throw new TRPCError({ + code: "BAD_REQUEST", + message: "Error to create the notification", + cause: error, + }); + } + }), + updateSlack: adminProcedure + .input(apiUpdateSlack) + .mutation(async ({ input, ctx }) => { + try { + const notification = await findNotificationById(input.notificationId); + if (IS_CLOUD && notification.adminId !== ctx.user.adminId) { + // TODO: Remove isCloud in the next versions of dokploy + throw new TRPCError({ + code: "UNAUTHORIZED", + message: "You are not authorized to update this notification", + }); + } + return await updateSlackNotification({ + ...input, + adminId: ctx.user.adminId, + }); + } catch (error) { + throw error; + } + }), + testSlackConnection: adminProcedure + .input(apiTestSlackConnection) + .mutation(async ({ input }) => { + try { + await sendSlackNotification(input, { + channel: input.channel, + text: "Hi, From Dokploy 👋", + }); + return true; + } catch (error) { + throw new TRPCError({ + code: "BAD_REQUEST", + message: "Error to test the notification", + cause: error, + }); + } + }), + createTelegram: adminProcedure + .input(apiCreateTelegram) + .mutation(async ({ input, ctx }) => { + try { + return await createTelegramNotification(input, ctx.user.adminId); + } catch (error) { + throw new TRPCError({ + code: "BAD_REQUEST", + message: "Error to create the notification", + cause: error, + }); + } + }), + + updateTelegram: adminProcedure + .input(apiUpdateTelegram) + .mutation(async ({ input, ctx }) => { + try { + const notification = await findNotificationById(input.notificationId); + if (IS_CLOUD && notification.adminId !== ctx.user.adminId) { + // TODO: Remove isCloud in the next versions of dokploy + throw new TRPCError({ + code: "UNAUTHORIZED", + message: "You are not authorized to update this notification", + }); + } + return await updateTelegramNotification({ + ...input, + adminId: ctx.user.adminId, + }); + } catch (error) { + throw new TRPCError({ + code: "BAD_REQUEST", + message: "Error to update the notification", + cause: error, + }); + } + }), + testTelegramConnection: adminProcedure + .input(apiTestTelegramConnection) + .mutation(async ({ input }) => { + try { + await sendTelegramNotification(input, "Hi, From Dokploy 👋"); + return true; + } catch (error) { + throw new TRPCError({ + code: "BAD_REQUEST", + message: "Error to test the notification", + cause: error, + }); + } + }), + createDiscord: adminProcedure + .input(apiCreateDiscord) + .mutation(async ({ input, ctx }) => { + try { + return await createDiscordNotification(input, ctx.user.adminId); + } catch (error) { + throw new TRPCError({ + code: "BAD_REQUEST", + message: "Error to create the notification", + cause: error, + }); + } + }), + + updateDiscord: adminProcedure + .input(apiUpdateDiscord) + .mutation(async ({ input, ctx }) => { + try { + const notification = await findNotificationById(input.notificationId); + if (IS_CLOUD && notification.adminId !== ctx.user.adminId) { + // TODO: Remove isCloud in the next versions of dokploy + throw new TRPCError({ + code: "UNAUTHORIZED", + message: "You are not authorized to update this notification", + }); + } + return await updateDiscordNotification({ + ...input, + adminId: ctx.user.adminId, + }); + } catch (error) { + throw new TRPCError({ + code: "BAD_REQUEST", + message: "Error to update the notification", + cause: error, + }); + } + }), + + testDiscordConnection: adminProcedure + .input(apiTestDiscordConnection) + .mutation(async ({ input }) => { + try { + await sendDiscordNotification(input, { + title: "Test Notification", + description: "Hi, From Dokploy 👋", + }); + return true; + } catch (error) { + throw new TRPCError({ + code: "BAD_REQUEST", + message: "Error to test the notification", + cause: error, + }); + } + }), + createEmail: adminProcedure + .input(apiCreateEmail) + .mutation(async ({ input, ctx }) => { + try { + return await createEmailNotification(input, ctx.user.adminId); + } catch (error) { + throw new TRPCError({ + code: "BAD_REQUEST", + message: "Error to create the notification", + cause: error, + }); + } + }), + updateEmail: adminProcedure + .input(apiUpdateEmail) + .mutation(async ({ input, ctx }) => { + try { + const notification = await findNotificationById(input.notificationId); + if (IS_CLOUD && notification.adminId !== ctx.user.adminId) { + // TODO: Remove isCloud in the next versions of dokploy + throw new TRPCError({ + code: "UNAUTHORIZED", + message: "You are not authorized to update this notification", + }); + } + return await updateEmailNotification({ + ...input, + adminId: ctx.user.adminId, + }); + } catch (error) { + throw new TRPCError({ + code: "BAD_REQUEST", + message: "Error to update the notification", + cause: error, + }); + } + }), + testEmailConnection: adminProcedure + .input(apiTestEmailConnection) + .mutation(async ({ input }) => { + try { + await sendEmailNotification( + input, + "Test Email", + "
Hi, From Dokploy 👋
", + ); + return true; + } catch (error) { + throw new TRPCError({ + code: "BAD_REQUEST", + message: "Error to test the notification", + cause: error, + }); + } + }), + remove: adminProcedure + .input(apiFindOneNotification) + .mutation(async ({ input, ctx }) => { + try { + const notification = await findNotificationById(input.notificationId); + if (IS_CLOUD && notification.adminId !== ctx.user.adminId) { + // TODO: Remove isCloud in the next versions of dokploy + throw new TRPCError({ + code: "UNAUTHORIZED", + message: "You are not authorized to delete this notification", + }); + } + return await removeNotificationById(input.notificationId); + } catch (error) { + throw new TRPCError({ + code: "BAD_REQUEST", + message: "Error to delete this notification", + }); + } + }), + one: protectedProcedure + .input(apiFindOneNotification) + .query(async ({ input, ctx }) => { + const notification = await findNotificationById(input.notificationId); + if (IS_CLOUD && notification.adminId !== ctx.user.adminId) { + // TODO: Remove isCloud in the next versions of dokploy + throw new TRPCError({ + code: "UNAUTHORIZED", + message: "You are not authorized to access this notification", + }); + } + return notification; + }), + all: adminProcedure.query(async ({ ctx }) => { + return await db.query.notifications.findMany({ + with: { + slack: true, + telegram: true, + discord: true, + email: true, + }, + orderBy: desc(notifications.createdAt), + ...(IS_CLOUD && { where: eq(notifications.adminId, ctx.user.adminId) }), + // TODO: Remove this line when the cloud version is ready + }); + }), +}); diff --git a/apps/mig/server/api/routers/port.ts b/apps/mig/server/api/routers/port.ts new file mode 100644 index 00000000..409eb8f0 --- /dev/null +++ b/apps/mig/server/api/routers/port.ts @@ -0,0 +1,65 @@ +import { createTRPCRouter, protectedProcedure } from "@/server/api/trpc"; +import { + apiCreatePort, + apiFindOnePort, + apiUpdatePort, +} from "@/server/db/schema"; +import { + createPort, + finPortById, + removePortById, + updatePortById, +} from "@dokploy/server"; +import { TRPCError } from "@trpc/server"; + +export const portRouter = createTRPCRouter({ + create: protectedProcedure + .input(apiCreatePort) + .mutation(async ({ input }) => { + try { + await createPort(input); + return true; + } catch (error) { + throw new TRPCError({ + code: "BAD_REQUEST", + message: "Error input: Inserting port", + cause: error, + }); + } + }), + one: protectedProcedure.input(apiFindOnePort).query(async ({ input }) => { + try { + return await finPortById(input.portId); + } catch (error) { + throw new TRPCError({ + code: "BAD_REQUEST", + message: "Port not found", + cause: error, + }); + } + }), + delete: protectedProcedure + .input(apiFindOnePort) + .mutation(async ({ input }) => { + try { + return removePortById(input.portId); + } catch (error) { + throw new TRPCError({ + code: "BAD_REQUEST", + message: "Error input: Deleting port", + }); + } + }), + update: protectedProcedure + .input(apiUpdatePort) + .mutation(async ({ input }) => { + try { + return updatePortById(input.portId, input); + } catch (error) { + throw new TRPCError({ + code: "BAD_REQUEST", + message: "Error to updating port", + }); + } + }), +}); diff --git a/apps/mig/server/api/routers/postgres.ts b/apps/mig/server/api/routers/postgres.ts new file mode 100644 index 00000000..14f9d967 --- /dev/null +++ b/apps/mig/server/api/routers/postgres.ts @@ -0,0 +1,284 @@ +import { createTRPCRouter, protectedProcedure } from "@/server/api/trpc"; +import { + apiChangePostgresStatus, + apiCreatePostgres, + apiDeployPostgres, + apiFindOnePostgres, + apiResetPostgres, + apiSaveEnvironmentVariablesPostgres, + apiSaveExternalPortPostgres, + apiUpdatePostgres, +} from "@/server/db/schema"; +import { + IS_CLOUD, + addNewService, + checkServiceAccess, + createMount, + createPostgres, + deployPostgres, + findPostgresById, + findProjectById, + removePostgresById, + removeService, + startService, + startServiceRemote, + stopService, + stopServiceRemote, + updatePostgresById, +} from "@dokploy/server"; +import { TRPCError } from "@trpc/server"; + +export const postgresRouter = createTRPCRouter({ + create: protectedProcedure + .input(apiCreatePostgres) + .mutation(async ({ input, ctx }) => { + try { + if (ctx.user.rol === "user") { + await checkServiceAccess(ctx.user.authId, input.projectId, "create"); + } + + if (IS_CLOUD && !input.serverId) { + throw new TRPCError({ + code: "UNAUTHORIZED", + message: "You need to use a server to create a postgres", + }); + } + + const project = await findProjectById(input.projectId); + if (project.adminId !== ctx.user.adminId) { + throw new TRPCError({ + code: "UNAUTHORIZED", + message: "You are not authorized to access this project", + }); + } + const newPostgres = await createPostgres(input); + if (ctx.user.rol === "user") { + await addNewService(ctx.user.authId, newPostgres.postgresId); + } + + await createMount({ + serviceId: newPostgres.postgresId, + serviceType: "postgres", + volumeName: `${newPostgres.appName}-data`, + mountPath: "/var/lib/postgresql/data", + type: "volume", + }); + + return true; + } catch (error) { + if (error instanceof TRPCError) { + throw error; + } + throw new TRPCError({ + code: "BAD_REQUEST", + message: "Error input: Inserting postgresql database", + cause: error, + }); + } + }), + one: protectedProcedure + .input(apiFindOnePostgres) + .query(async ({ input, ctx }) => { + if (ctx.user.rol === "user") { + await checkServiceAccess(ctx.user.authId, input.postgresId, "access"); + } + + const postgres = await findPostgresById(input.postgresId); + if (postgres.project.adminId !== ctx.user.adminId) { + throw new TRPCError({ + code: "UNAUTHORIZED", + message: "You are not authorized to access this postgres", + }); + } + return postgres; + }), + + start: protectedProcedure + .input(apiFindOnePostgres) + .mutation(async ({ input, ctx }) => { + const service = await findPostgresById(input.postgresId); + + if (service.project.adminId !== ctx.user.adminId) { + throw new TRPCError({ + code: "UNAUTHORIZED", + message: "You are not authorized to start this postgres", + }); + } + + if (service.serverId) { + await startServiceRemote(service.serverId, service.appName); + } else { + await startService(service.appName); + } + await updatePostgresById(input.postgresId, { + applicationStatus: "done", + }); + + return service; + }), + stop: protectedProcedure + .input(apiFindOnePostgres) + .mutation(async ({ input, ctx }) => { + const postgres = await findPostgresById(input.postgresId); + if (postgres.project.adminId !== ctx.user.adminId) { + throw new TRPCError({ + code: "UNAUTHORIZED", + message: "You are not authorized to stop this postgres", + }); + } + if (postgres.serverId) { + await stopServiceRemote(postgres.serverId, postgres.appName); + } else { + await stopService(postgres.appName); + } + await updatePostgresById(input.postgresId, { + applicationStatus: "idle", + }); + + return postgres; + }), + saveExternalPort: protectedProcedure + .input(apiSaveExternalPortPostgres) + .mutation(async ({ input, ctx }) => { + const postgres = await findPostgresById(input.postgresId); + + if (postgres.project.adminId !== ctx.user.adminId) { + throw new TRPCError({ + code: "UNAUTHORIZED", + message: "You are not authorized to save this external port", + }); + } + await updatePostgresById(input.postgresId, { + externalPort: input.externalPort, + }); + await deployPostgres(input.postgresId); + return postgres; + }), + deploy: protectedProcedure + .input(apiDeployPostgres) + .mutation(async ({ input, ctx }) => { + const postgres = await findPostgresById(input.postgresId); + if (postgres.project.adminId !== ctx.user.adminId) { + throw new TRPCError({ + code: "UNAUTHORIZED", + message: "You are not authorized to deploy this postgres", + }); + } + return deployPostgres(input.postgresId); + }), + changeStatus: protectedProcedure + .input(apiChangePostgresStatus) + .mutation(async ({ input, ctx }) => { + const postgres = await findPostgresById(input.postgresId); + if (postgres.project.adminId !== ctx.user.adminId) { + throw new TRPCError({ + code: "UNAUTHORIZED", + message: "You are not authorized to change this postgres status", + }); + } + await updatePostgresById(input.postgresId, { + applicationStatus: input.applicationStatus, + }); + return postgres; + }), + remove: protectedProcedure + .input(apiFindOnePostgres) + .mutation(async ({ input, ctx }) => { + if (ctx.user.rol === "user") { + await checkServiceAccess(ctx.user.authId, input.postgresId, "delete"); + } + const postgres = await findPostgresById(input.postgresId); + + if (postgres.project.adminId !== ctx.user.adminId) { + throw new TRPCError({ + code: "UNAUTHORIZED", + message: "You are not authorized to delete this postgres", + }); + } + + const cleanupOperations = [ + removeService(postgres.appName, postgres.serverId), + removePostgresById(input.postgresId), + ]; + + await Promise.allSettled(cleanupOperations); + + return postgres; + }), + saveEnvironment: protectedProcedure + .input(apiSaveEnvironmentVariablesPostgres) + .mutation(async ({ input, ctx }) => { + const postgres = await findPostgresById(input.postgresId); + if (postgres.project.adminId !== ctx.user.adminId) { + throw new TRPCError({ + code: "UNAUTHORIZED", + message: "You are not authorized to save this environment", + }); + } + const service = await updatePostgresById(input.postgresId, { + env: input.env, + }); + + if (!service) { + throw new TRPCError({ + code: "BAD_REQUEST", + message: "Update: Error to add environment variables", + }); + } + + return true; + }), + reload: protectedProcedure + .input(apiResetPostgres) + .mutation(async ({ input, ctx }) => { + const postgres = await findPostgresById(input.postgresId); + if (postgres.project.adminId !== ctx.user.adminId) { + throw new TRPCError({ + code: "UNAUTHORIZED", + message: "You are not authorized to reload this postgres", + }); + } + if (postgres.serverId) { + await stopServiceRemote(postgres.serverId, postgres.appName); + } else { + await stopService(postgres.appName); + } + await updatePostgresById(input.postgresId, { + applicationStatus: "idle", + }); + + if (postgres.serverId) { + await startServiceRemote(postgres.serverId, postgres.appName); + } else { + await startService(postgres.appName); + } + await updatePostgresById(input.postgresId, { + applicationStatus: "done", + }); + return true; + }), + update: protectedProcedure + .input(apiUpdatePostgres) + .mutation(async ({ input, ctx }) => { + const { postgresId, ...rest } = input; + const postgres = await findPostgresById(postgresId); + if (postgres.project.adminId !== ctx.user.adminId) { + throw new TRPCError({ + code: "UNAUTHORIZED", + message: "You are not authorized to update this postgres", + }); + } + const service = await updatePostgresById(postgresId, { + ...rest, + }); + + if (!service) { + throw new TRPCError({ + code: "BAD_REQUEST", + message: "Update: Error to update postgres", + }); + } + + return true; + }), +}); diff --git a/apps/mig/server/api/routers/project.ts b/apps/mig/server/api/routers/project.ts new file mode 100644 index 00000000..c3fe8605 --- /dev/null +++ b/apps/mig/server/api/routers/project.ts @@ -0,0 +1,234 @@ +import { createTRPCRouter, protectedProcedure } from "@/server/api/trpc"; +import { db } from "@/server/db"; +import { + apiCreateProject, + apiFindOneProject, + apiRemoveProject, + apiUpdateProject, + applications, + compose, + mariadb, + mongo, + mysql, + postgres, + projects, + redis, +} from "@/server/db/schema"; + +import { TRPCError } from "@trpc/server"; +import { and, desc, eq, sql } from "drizzle-orm"; +import type { AnyPgColumn } from "drizzle-orm/pg-core"; + +import { + addNewProject, + checkProjectAccess, + createProject, + deleteProject, + findProjectById, + findUserByAuthId, + updateProjectById, +} from "@dokploy/server"; + +export const projectRouter = createTRPCRouter({ + create: protectedProcedure + .input(apiCreateProject) + .mutation(async ({ ctx, input }) => { + try { + if (ctx.user.rol === "user") { + await checkProjectAccess(ctx.user.authId, "create"); + } + + const project = await createProject(input, ctx.user.adminId); + if (ctx.user.rol === "user") { + await addNewProject(ctx.user.authId, project.projectId); + } + + return project; + } catch (error) { + throw new TRPCError({ + code: "BAD_REQUEST", + message: "Error to create the project", + cause: error, + }); + } + }), + + one: protectedProcedure + .input(apiFindOneProject) + .query(async ({ input, ctx }) => { + if (ctx.user.rol === "user") { + const { accesedServices } = await findUserByAuthId(ctx.user.authId); + + await checkProjectAccess(ctx.user.authId, "access", input.projectId); + + const project = await db.query.projects.findFirst({ + where: and( + eq(projects.projectId, input.projectId), + eq(projects.adminId, ctx.user.adminId), + ), + with: { + compose: { + where: buildServiceFilter(compose.composeId, accesedServices), + }, + applications: { + where: buildServiceFilter( + applications.applicationId, + accesedServices, + ), + }, + mariadb: { + where: buildServiceFilter(mariadb.mariadbId, accesedServices), + }, + mongo: { + where: buildServiceFilter(mongo.mongoId, accesedServices), + }, + mysql: { + where: buildServiceFilter(mysql.mysqlId, accesedServices), + }, + postgres: { + where: buildServiceFilter(postgres.postgresId, accesedServices), + }, + redis: { + where: buildServiceFilter(redis.redisId, accesedServices), + }, + }, + }); + + if (!project) { + throw new TRPCError({ + code: "NOT_FOUND", + message: "Project not found", + }); + } + return project; + } + const project = await findProjectById(input.projectId); + + if (project.adminId !== ctx.user.adminId) { + throw new TRPCError({ + code: "UNAUTHORIZED", + message: "You are not authorized to access this project", + }); + } + return project; + }), + all: protectedProcedure.query(async ({ ctx }) => { + if (ctx.user.rol === "user") { + const { accesedProjects, accesedServices } = await findUserByAuthId( + ctx.user.authId, + ); + + if (accesedProjects.length === 0) { + return []; + } + const query = await db.query.projects.findMany({ + where: sql`${projects.projectId} IN (${sql.join( + accesedProjects.map((projectId) => sql`${projectId}`), + sql`, `, + )})`, + with: { + applications: { + where: buildServiceFilter( + applications.applicationId, + accesedServices, + ), + with: { domains: true }, + }, + mariadb: { + where: buildServiceFilter(mariadb.mariadbId, accesedServices), + }, + mongo: { + where: buildServiceFilter(mongo.mongoId, accesedServices), + }, + mysql: { + where: buildServiceFilter(mysql.mysqlId, accesedServices), + }, + postgres: { + where: buildServiceFilter(postgres.postgresId, accesedServices), + }, + redis: { + where: buildServiceFilter(redis.redisId, accesedServices), + }, + compose: { + where: buildServiceFilter(compose.composeId, accesedServices), + with: { domains: true }, + }, + }, + orderBy: desc(projects.createdAt), + }); + + return query; + } + + return await db.query.projects.findMany({ + with: { + applications: { + with: { + domains: true, + }, + }, + mariadb: true, + mongo: true, + mysql: true, + postgres: true, + redis: true, + compose: { + with: { + domains: true, + }, + }, + }, + where: eq(projects.adminId, ctx.user.adminId), + orderBy: desc(projects.createdAt), + }); + }), + remove: protectedProcedure + .input(apiRemoveProject) + .mutation(async ({ input, ctx }) => { + try { + if (ctx.user.rol === "user") { + await checkProjectAccess(ctx.user.authId, "delete"); + } + const currentProject = await findProjectById(input.projectId); + if (currentProject.adminId !== ctx.user.adminId) { + throw new TRPCError({ + code: "UNAUTHORIZED", + message: "You are not authorized to delete this project", + }); + } + const deletedProject = await deleteProject(input.projectId); + + return deletedProject; + } catch (error) { + throw error; + } + }), + update: protectedProcedure + .input(apiUpdateProject) + .mutation(async ({ input, ctx }) => { + try { + const currentProject = await findProjectById(input.projectId); + if (currentProject.adminId !== ctx.user.adminId) { + throw new TRPCError({ + code: "UNAUTHORIZED", + message: "You are not authorized to update this project", + }); + } + const project = await updateProjectById(input.projectId, { + ...input, + }); + + return project; + } catch (error) { + throw error; + } + }), +}); +function buildServiceFilter(fieldName: AnyPgColumn, accesedServices: string[]) { + return accesedServices.length > 0 + ? sql`${fieldName} IN (${sql.join( + accesedServices.map((serviceId) => sql`${serviceId}`), + sql`, `, + )})` + : sql`1 = 0`; // Always false condition +} diff --git a/apps/mig/server/api/routers/redirects.ts b/apps/mig/server/api/routers/redirects.ts new file mode 100644 index 00000000..bcd7962a --- /dev/null +++ b/apps/mig/server/api/routers/redirects.ts @@ -0,0 +1,68 @@ +import { + apiCreateRedirect, + apiFindOneRedirect, + apiUpdateRedirect, +} from "@/server/db/schema"; +import { + createRedirect, + findApplicationById, + findRedirectById, + removeRedirectById, + updateRedirectById, +} from "@dokploy/server"; +import { TRPCError } from "@trpc/server"; +import { createTRPCRouter, protectedProcedure } from "../trpc"; + +export const redirectsRouter = createTRPCRouter({ + create: protectedProcedure + .input(apiCreateRedirect) + .mutation(async ({ input, ctx }) => { + const application = await findApplicationById(input.applicationId); + if (application.project.adminId !== ctx.user.adminId) { + throw new TRPCError({ + code: "UNAUTHORIZED", + message: "You are not authorized to access this application", + }); + } + return await createRedirect(input); + }), + one: protectedProcedure + .input(apiFindOneRedirect) + .query(async ({ input, ctx }) => { + const redirect = await findRedirectById(input.redirectId); + const application = await findApplicationById(redirect.applicationId); + if (application.project.adminId !== ctx.user.adminId) { + throw new TRPCError({ + code: "UNAUTHORIZED", + message: "You are not authorized to access this application", + }); + } + return findRedirectById(input.redirectId); + }), + delete: protectedProcedure + .input(apiFindOneRedirect) + .mutation(async ({ input, ctx }) => { + const redirect = await findRedirectById(input.redirectId); + const application = await findApplicationById(redirect.applicationId); + if (application.project.adminId !== ctx.user.adminId) { + throw new TRPCError({ + code: "UNAUTHORIZED", + message: "You are not authorized to access this application", + }); + } + return removeRedirectById(input.redirectId); + }), + update: protectedProcedure + .input(apiUpdateRedirect) + .mutation(async ({ input, ctx }) => { + const redirect = await findRedirectById(input.redirectId); + const application = await findApplicationById(redirect.applicationId); + if (application.project.adminId !== ctx.user.adminId) { + throw new TRPCError({ + code: "UNAUTHORIZED", + message: "You are not authorized to access this application", + }); + } + return updateRedirectById(input.redirectId, input); + }), +}); diff --git a/apps/mig/server/api/routers/redis.ts b/apps/mig/server/api/routers/redis.ts new file mode 100644 index 00000000..fdd2db9c --- /dev/null +++ b/apps/mig/server/api/routers/redis.ts @@ -0,0 +1,276 @@ +import { createTRPCRouter, protectedProcedure } from "@/server/api/trpc"; +import { + apiChangeRedisStatus, + apiCreateRedis, + apiDeployRedis, + apiFindOneRedis, + apiResetRedis, + apiSaveEnvironmentVariablesRedis, + apiSaveExternalPortRedis, + apiUpdateRedis, +} from "@/server/db/schema"; + +import { TRPCError } from "@trpc/server"; + +import { + IS_CLOUD, + addNewService, + checkServiceAccess, + createMount, + createRedis, + deployRedis, + findProjectById, + findRedisById, + removeRedisById, + removeService, + startService, + startServiceRemote, + stopService, + stopServiceRemote, + updateRedisById, +} from "@dokploy/server"; + +export const redisRouter = createTRPCRouter({ + create: protectedProcedure + .input(apiCreateRedis) + .mutation(async ({ input, ctx }) => { + try { + if (ctx.user.rol === "user") { + await checkServiceAccess(ctx.user.authId, input.projectId, "create"); + } + + if (IS_CLOUD && !input.serverId) { + throw new TRPCError({ + code: "UNAUTHORIZED", + message: "You need to use a server to create a redis", + }); + } + + const project = await findProjectById(input.projectId); + if (project.adminId !== ctx.user.adminId) { + throw new TRPCError({ + code: "UNAUTHORIZED", + message: "You are not authorized to access this project", + }); + } + const newRedis = await createRedis(input); + if (ctx.user.rol === "user") { + await addNewService(ctx.user.authId, newRedis.redisId); + } + + await createMount({ + serviceId: newRedis.redisId, + serviceType: "redis", + volumeName: `${newRedis.appName}-data`, + mountPath: "/data", + type: "volume", + }); + + return true; + } catch (error) { + throw error; + } + }), + one: protectedProcedure + .input(apiFindOneRedis) + .query(async ({ input, ctx }) => { + if (ctx.user.rol === "user") { + await checkServiceAccess(ctx.user.authId, input.redisId, "access"); + } + + const redis = await findRedisById(input.redisId); + if (redis.project.adminId !== ctx.user.adminId) { + throw new TRPCError({ + code: "UNAUTHORIZED", + message: "You are not authorized to access this redis", + }); + } + return redis; + }), + + start: protectedProcedure + .input(apiFindOneRedis) + .mutation(async ({ input, ctx }) => { + const redis = await findRedisById(input.redisId); + if (redis.project.adminId !== ctx.user.adminId) { + throw new TRPCError({ + code: "UNAUTHORIZED", + message: "You are not authorized to start this redis", + }); + } + + if (redis.serverId) { + await startServiceRemote(redis.serverId, redis.appName); + } else { + await startService(redis.appName); + } + await updateRedisById(input.redisId, { + applicationStatus: "done", + }); + + return redis; + }), + reload: protectedProcedure + .input(apiResetRedis) + .mutation(async ({ input, ctx }) => { + const redis = await findRedisById(input.redisId); + if (redis.project.adminId !== ctx.user.adminId) { + throw new TRPCError({ + code: "UNAUTHORIZED", + message: "You are not authorized to reload this redis", + }); + } + if (redis.serverId) { + await stopServiceRemote(redis.serverId, redis.appName); + } else { + await stopService(redis.appName); + } + await updateRedisById(input.redisId, { + applicationStatus: "idle", + }); + + if (redis.serverId) { + await startServiceRemote(redis.serverId, redis.appName); + } else { + await startService(redis.appName); + } + await updateRedisById(input.redisId, { + applicationStatus: "done", + }); + return true; + }), + + stop: protectedProcedure + .input(apiFindOneRedis) + .mutation(async ({ input, ctx }) => { + const redis = await findRedisById(input.redisId); + if (redis.project.adminId !== ctx.user.adminId) { + throw new TRPCError({ + code: "UNAUTHORIZED", + message: "You are not authorized to stop this redis", + }); + } + if (redis.serverId) { + await stopServiceRemote(redis.serverId, redis.appName); + } else { + await stopService(redis.appName); + } + await updateRedisById(input.redisId, { + applicationStatus: "idle", + }); + + return redis; + }), + saveExternalPort: protectedProcedure + .input(apiSaveExternalPortRedis) + .mutation(async ({ input, ctx }) => { + const mongo = await findRedisById(input.redisId); + if (mongo.project.adminId !== ctx.user.adminId) { + throw new TRPCError({ + code: "UNAUTHORIZED", + message: "You are not authorized to save this external port", + }); + } + await updateRedisById(input.redisId, { + externalPort: input.externalPort, + }); + await deployRedis(input.redisId); + return mongo; + }), + deploy: protectedProcedure + .input(apiDeployRedis) + .mutation(async ({ input, ctx }) => { + const redis = await findRedisById(input.redisId); + if (redis.project.adminId !== ctx.user.adminId) { + throw new TRPCError({ + code: "UNAUTHORIZED", + message: "You are not authorized to deploy this redis", + }); + } + return deployRedis(input.redisId); + }), + changeStatus: protectedProcedure + .input(apiChangeRedisStatus) + .mutation(async ({ input, ctx }) => { + const mongo = await findRedisById(input.redisId); + if (mongo.project.adminId !== ctx.user.adminId) { + throw new TRPCError({ + code: "UNAUTHORIZED", + message: "You are not authorized to change this redis status", + }); + } + await updateRedisById(input.redisId, { + applicationStatus: input.applicationStatus, + }); + return mongo; + }), + remove: protectedProcedure + .input(apiFindOneRedis) + .mutation(async ({ input, ctx }) => { + if (ctx.user.rol === "user") { + await checkServiceAccess(ctx.user.authId, input.redisId, "delete"); + } + + const redis = await findRedisById(input.redisId); + + if (redis.project.adminId !== ctx.user.adminId) { + throw new TRPCError({ + code: "UNAUTHORIZED", + message: "You are not authorized to delete this redis", + }); + } + + const cleanupOperations = [ + async () => await removeService(redis?.appName, redis.serverId), + async () => await removeRedisById(input.redisId), + ]; + + for (const operation of cleanupOperations) { + try { + await operation(); + } catch (error) {} + } + + return redis; + }), + saveEnvironment: protectedProcedure + .input(apiSaveEnvironmentVariablesRedis) + .mutation(async ({ input, ctx }) => { + const redis = await findRedisById(input.redisId); + if (redis.project.adminId !== ctx.user.adminId) { + throw new TRPCError({ + code: "UNAUTHORIZED", + message: "You are not authorized to save this environment", + }); + } + const updatedRedis = await updateRedisById(input.redisId, { + env: input.env, + }); + + if (!updatedRedis) { + throw new TRPCError({ + code: "BAD_REQUEST", + message: "Update: Error to add environment variables", + }); + } + + return true; + }), + update: protectedProcedure + .input(apiUpdateRedis) + .mutation(async ({ input }) => { + const { redisId, ...rest } = input; + const redis = await updateRedisById(redisId, { + ...rest, + }); + + if (!redis) { + throw new TRPCError({ + code: "BAD_REQUEST", + message: "Update: Error to update redis", + }); + } + + return true; + }), +}); diff --git a/apps/mig/server/api/routers/registry.ts b/apps/mig/server/api/routers/registry.ts new file mode 100644 index 00000000..8ca4a8a0 --- /dev/null +++ b/apps/mig/server/api/routers/registry.ts @@ -0,0 +1,103 @@ +import { + apiCreateRegistry, + apiFindOneRegistry, + apiRemoveRegistry, + apiTestRegistry, + apiUpdateRegistry, +} from "@/server/db/schema"; +import { + IS_CLOUD, + createRegistry, + execAsync, + execAsyncRemote, + findAllRegistryByAdminId, + findRegistryById, + removeRegistry, + updateRegistry, +} from "@dokploy/server"; +import { TRPCError } from "@trpc/server"; +import { adminProcedure, createTRPCRouter, protectedProcedure } from "../trpc"; + +export const registryRouter = createTRPCRouter({ + create: adminProcedure + .input(apiCreateRegistry) + .mutation(async ({ ctx, input }) => { + return await createRegistry(input, ctx.user.adminId); + }), + remove: adminProcedure + .input(apiRemoveRegistry) + .mutation(async ({ ctx, input }) => { + const registry = await findRegistryById(input.registryId); + if (registry.adminId !== ctx.user.adminId) { + throw new TRPCError({ + code: "UNAUTHORIZED", + message: "You are not allowed to delete this registry", + }); + } + return await removeRegistry(input.registryId); + }), + update: protectedProcedure + .input(apiUpdateRegistry) + .mutation(async ({ input, ctx }) => { + const { registryId, ...rest } = input; + const registry = await findRegistryById(registryId); + if (registry.adminId !== ctx.user.adminId) { + throw new TRPCError({ + code: "UNAUTHORIZED", + message: "You are not allowed to update this registry", + }); + } + const application = await updateRegistry(registryId, { + ...rest, + }); + + if (!application) { + throw new TRPCError({ + code: "BAD_REQUEST", + message: "Update: Error to update registry", + }); + } + + return true; + }), + all: protectedProcedure.query(async ({ ctx }) => { + return await findAllRegistryByAdminId(ctx.user.adminId); + }), + one: adminProcedure + .input(apiFindOneRegistry) + .query(async ({ input, ctx }) => { + const registry = await findRegistryById(input.registryId); + if (registry.adminId !== ctx.user.adminId) { + throw new TRPCError({ + code: "UNAUTHORIZED", + message: "You are not allowed to access this registry", + }); + } + return registry; + }), + testRegistry: protectedProcedure + .input(apiTestRegistry) + .mutation(async ({ input }) => { + try { + const loginCommand = `echo ${input.password} | docker login ${input.registryUrl} --username ${input.username} --password-stdin`; + + if (IS_CLOUD && !input.serverId) { + throw new TRPCError({ + code: "NOT_FOUND", + message: "Select a server to test the registry", + }); + } + + if (input.serverId && input.serverId !== "none") { + await execAsyncRemote(input.serverId, loginCommand); + } else { + await execAsync(loginCommand); + } + + return true; + } catch (error) { + console.log("Error Registry:", error); + return false; + } + }), +}); diff --git a/apps/mig/server/api/routers/security.ts b/apps/mig/server/api/routers/security.ts new file mode 100644 index 00000000..5318a293 --- /dev/null +++ b/apps/mig/server/api/routers/security.ts @@ -0,0 +1,68 @@ +import { + apiCreateSecurity, + apiFindOneSecurity, + apiUpdateSecurity, +} from "@/server/db/schema"; +import { + createSecurity, + deleteSecurityById, + findApplicationById, + findSecurityById, + updateSecurityById, +} from "@dokploy/server"; +import { TRPCError } from "@trpc/server"; +import { createTRPCRouter, protectedProcedure } from "../trpc"; + +export const securityRouter = createTRPCRouter({ + create: protectedProcedure + .input(apiCreateSecurity) + .mutation(async ({ input, ctx }) => { + const application = await findApplicationById(input.applicationId); + if (application.project.adminId !== ctx.user.adminId) { + throw new TRPCError({ + code: "UNAUTHORIZED", + message: "You are not authorized to access this application", + }); + } + return await createSecurity(input); + }), + one: protectedProcedure + .input(apiFindOneSecurity) + .query(async ({ input, ctx }) => { + const security = await findSecurityById(input.securityId); + const application = await findApplicationById(security.applicationId); + if (application.project.adminId !== ctx.user.adminId) { + throw new TRPCError({ + code: "UNAUTHORIZED", + message: "You are not authorized to access this application", + }); + } + return await findSecurityById(input.securityId); + }), + delete: protectedProcedure + .input(apiFindOneSecurity) + .mutation(async ({ input, ctx }) => { + const security = await findSecurityById(input.securityId); + const application = await findApplicationById(security.applicationId); + if (application.project.adminId !== ctx.user.adminId) { + throw new TRPCError({ + code: "UNAUTHORIZED", + message: "You are not authorized to access this application", + }); + } + return await deleteSecurityById(input.securityId); + }), + update: protectedProcedure + .input(apiUpdateSecurity) + .mutation(async ({ input, ctx }) => { + const security = await findSecurityById(input.securityId); + const application = await findApplicationById(security.applicationId); + if (application.project.adminId !== ctx.user.adminId) { + throw new TRPCError({ + code: "UNAUTHORIZED", + message: "You are not authorized to access this application", + }); + } + return await updateSecurityById(input.securityId, input); + }), +}); diff --git a/apps/mig/server/api/routers/server.ts b/apps/mig/server/api/routers/server.ts new file mode 100644 index 00000000..6caaa9c8 --- /dev/null +++ b/apps/mig/server/api/routers/server.ts @@ -0,0 +1,184 @@ +import { updateServersBasedOnQuantity } from "@/pages/api/stripe/webhook"; +import { createTRPCRouter, protectedProcedure } from "@/server/api/trpc"; +import { db } from "@/server/db"; +import { + apiCreateServer, + apiFindOneServer, + apiRemoveServer, + apiUpdateServer, + applications, + compose, + mariadb, + mongo, + mysql, + postgres, + redis, + server, +} from "@/server/db/schema"; +import { + IS_CLOUD, + createServer, + deleteServer, + findAdminById, + findServerById, + findServersByAdminId, + haveActiveServices, + removeDeploymentsByServerId, + serverSetup, + updateServerById, +} from "@dokploy/server"; +import { TRPCError } from "@trpc/server"; +import { and, desc, eq, getTableColumns, isNotNull, sql } from "drizzle-orm"; + +export const serverRouter = createTRPCRouter({ + create: protectedProcedure + .input(apiCreateServer) + .mutation(async ({ ctx, input }) => { + try { + const admin = await findAdminById(ctx.user.adminId); + const servers = await findServersByAdminId(admin.adminId); + if (IS_CLOUD && servers.length >= admin.serversQuantity) { + throw new TRPCError({ + code: "BAD_REQUEST", + message: "You cannot create more servers", + }); + } + const project = await createServer(input, ctx.user.adminId); + return project; + } catch (error) { + throw new TRPCError({ + code: "BAD_REQUEST", + message: "Error to create the server", + cause: error, + }); + } + }), + + one: protectedProcedure + .input(apiFindOneServer) + .query(async ({ input, ctx }) => { + const server = await findServerById(input.serverId); + if (server.adminId !== ctx.user.adminId) { + throw new TRPCError({ + code: "UNAUTHORIZED", + message: "You are not authorized to access this server", + }); + } + + return server; + }), + all: protectedProcedure.query(async ({ ctx }) => { + const result = await db + .select({ + ...getTableColumns(server), + totalSum: sql