mirror of
https://github.com/Dokploy/dokploy
synced 2025-06-26 18:27:59 +00:00
refactor: migration
This commit is contained in:
20
apps/mig/migration.ts
Normal file
20
apps/mig/migration.ts
Normal file
@@ -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();
|
||||||
|
});
|
||||||
@@ -5,7 +5,7 @@
|
|||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "remix vite:build",
|
"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",
|
"start3": "cross-env NODE_ENV=production node -r dotenv/config ./build/server/index.js",
|
||||||
"start2": "cross-env NODE_ENV=production node ./server.js",
|
"start2": "cross-env NODE_ENV=production node ./server.js",
|
||||||
"dev": "remix vite:dev",
|
"dev": "remix vite:dev",
|
||||||
@@ -21,7 +21,7 @@
|
|||||||
"isbot": "^4.1.0",
|
"isbot": "^4.1.0",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
|
"bullmq": "5.4.2",
|
||||||
"rotating-file-stream": "3.2.3",
|
"rotating-file-stream": "3.2.3",
|
||||||
"@faker-js/faker": "^8.4.1",
|
"@faker-js/faker": "^8.4.1",
|
||||||
"@lucia-auth/adapter-drizzle": "1.0.7",
|
"@lucia-auth/adapter-drizzle": "1.0.7",
|
||||||
@@ -85,6 +85,6 @@
|
|||||||
"@types/ssh2": "1.15.1"
|
"@types/ssh2": "1.15.1"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=20.0.0"
|
"node": ">=18.18.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
77
apps/mig/server/api/root.ts
Normal file
77
apps/mig/server/api/root.ts
Normal file
@@ -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;
|
||||||
92
apps/mig/server/api/routers/admin.ts
Normal file
92
apps/mig/server/api/routers/admin.ts
Normal file
@@ -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;
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
});
|
||||||
625
apps/mig/server/api/routers/application.ts
Normal file
625
apps/mig/server/api/routers/application.ts
Normal file
@@ -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;
|
||||||
|
}),
|
||||||
|
});
|
||||||
339
apps/mig/server/api/routers/auth.ts
Normal file
339
apps/mig/server/api/routers/auth.ts
Normal file
@@ -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.
|
||||||
|
<a href="http://localhost:3000/reset-password?token=${token}">
|
||||||
|
Reset Password
|
||||||
|
</a>
|
||||||
|
|
||||||
|
`,
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
|
||||||
|
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;
|
||||||
|
}),
|
||||||
|
});
|
||||||
232
apps/mig/server/api/routers/backup.ts
Normal file
232
apps/mig/server/api/routers/backup.ts
Normal file
@@ -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;
|
||||||
|
// };
|
||||||
148
apps/mig/server/api/routers/bitbucket.ts
Normal file
148
apps/mig/server/api/routers/bitbucket.ts
Normal file
@@ -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,
|
||||||
|
});
|
||||||
|
}),
|
||||||
|
});
|
||||||
62
apps/mig/server/api/routers/certificate.ts
Normal file
62
apps/mig/server/api/routers/certificate.ts
Normal file
@@ -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) }),
|
||||||
|
});
|
||||||
|
}),
|
||||||
|
});
|
||||||
76
apps/mig/server/api/routers/cluster.ts
Normal file
76
apps/mig/server/api/routers/cluster.ts
Normal file
@@ -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,
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
});
|
||||||
448
apps/mig/server/api/routers/compose.ts
Normal file
448
apps/mig/server/api/routers/compose.ts
Normal file
@@ -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;
|
||||||
|
}),
|
||||||
|
});
|
||||||
55
apps/mig/server/api/routers/deployment.ts
Normal file
55
apps/mig/server/api/routers/deployment.ts
Normal file
@@ -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);
|
||||||
|
}),
|
||||||
|
});
|
||||||
137
apps/mig/server/api/routers/destination.ts
Normal file
137
apps/mig/server/api/routers/destination.ts
Normal file
@@ -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;
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
});
|
||||||
71
apps/mig/server/api/routers/docker.ts
Normal file
71
apps/mig/server/api/routers/docker.ts
Normal file
@@ -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);
|
||||||
|
}),
|
||||||
|
});
|
||||||
171
apps/mig/server/api/routers/domain.ts
Normal file
171
apps/mig/server/api/routers/domain.ts
Normal file
@@ -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;
|
||||||
|
}),
|
||||||
|
});
|
||||||
46
apps/mig/server/api/routers/git-provider.ts
Normal file
46
apps/mig/server/api/routers/git-provider.ts
Normal file
@@ -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",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
});
|
||||||
126
apps/mig/server/api/routers/github.ts
Normal file
126
apps/mig/server/api/routers/github.ts
Normal file
@@ -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,
|
||||||
|
});
|
||||||
|
}),
|
||||||
|
});
|
||||||
151
apps/mig/server/api/routers/gitlab.ts
Normal file
151
apps/mig/server/api/routers/gitlab.ts
Normal file
@@ -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,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
});
|
||||||
277
apps/mig/server/api/routers/mariadb.ts
Normal file
277
apps/mig/server/api/routers/mariadb.ts
Normal file
@@ -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;
|
||||||
|
}),
|
||||||
|
});
|
||||||
290
apps/mig/server/api/routers/mongo.ts
Normal file
290
apps/mig/server/api/routers/mongo.ts
Normal file
@@ -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;
|
||||||
|
}),
|
||||||
|
});
|
||||||
37
apps/mig/server/api/routers/mount.ts
Normal file
37
apps/mig/server/api/routers/mount.ts
Normal file
@@ -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;
|
||||||
|
}),
|
||||||
|
});
|
||||||
286
apps/mig/server/api/routers/mysql.ts
Normal file
286
apps/mig/server/api/routers/mysql.ts
Normal file
@@ -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;
|
||||||
|
}),
|
||||||
|
});
|
||||||
304
apps/mig/server/api/routers/notification.ts
Normal file
304
apps/mig/server/api/routers/notification.ts
Normal file
@@ -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",
|
||||||
|
"<p>Hi, From Dokploy 👋</p>",
|
||||||
|
);
|
||||||
|
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
|
||||||
|
});
|
||||||
|
}),
|
||||||
|
});
|
||||||
65
apps/mig/server/api/routers/port.ts
Normal file
65
apps/mig/server/api/routers/port.ts
Normal file
@@ -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",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
});
|
||||||
284
apps/mig/server/api/routers/postgres.ts
Normal file
284
apps/mig/server/api/routers/postgres.ts
Normal file
@@ -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;
|
||||||
|
}),
|
||||||
|
});
|
||||||
234
apps/mig/server/api/routers/project.ts
Normal file
234
apps/mig/server/api/routers/project.ts
Normal file
@@ -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
|
||||||
|
}
|
||||||
68
apps/mig/server/api/routers/redirects.ts
Normal file
68
apps/mig/server/api/routers/redirects.ts
Normal file
@@ -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);
|
||||||
|
}),
|
||||||
|
});
|
||||||
276
apps/mig/server/api/routers/redis.ts
Normal file
276
apps/mig/server/api/routers/redis.ts
Normal file
@@ -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;
|
||||||
|
}),
|
||||||
|
});
|
||||||
103
apps/mig/server/api/routers/registry.ts
Normal file
103
apps/mig/server/api/routers/registry.ts
Normal file
@@ -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;
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
});
|
||||||
68
apps/mig/server/api/routers/security.ts
Normal file
68
apps/mig/server/api/routers/security.ts
Normal file
@@ -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);
|
||||||
|
}),
|
||||||
|
});
|
||||||
184
apps/mig/server/api/routers/server.ts
Normal file
184
apps/mig/server/api/routers/server.ts
Normal file
@@ -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<number>`cast(count(${applications.applicationId}) + count(${compose.composeId}) + count(${redis.redisId}) + count(${mariadb.mariadbId}) + count(${mongo.mongoId}) + count(${mysql.mysqlId}) + count(${postgres.postgresId}) as integer)`,
|
||||||
|
})
|
||||||
|
.from(server)
|
||||||
|
.leftJoin(applications, eq(applications.serverId, server.serverId))
|
||||||
|
.leftJoin(compose, eq(compose.serverId, server.serverId))
|
||||||
|
.leftJoin(redis, eq(redis.serverId, server.serverId))
|
||||||
|
.leftJoin(mariadb, eq(mariadb.serverId, server.serverId))
|
||||||
|
.leftJoin(mongo, eq(mongo.serverId, server.serverId))
|
||||||
|
.leftJoin(mysql, eq(mysql.serverId, server.serverId))
|
||||||
|
.leftJoin(postgres, eq(postgres.serverId, server.serverId))
|
||||||
|
.where(eq(server.adminId, ctx.user.adminId))
|
||||||
|
.orderBy(desc(server.createdAt))
|
||||||
|
.groupBy(server.serverId);
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}),
|
||||||
|
withSSHKey: protectedProcedure.query(async ({ ctx }) => {
|
||||||
|
const result = await db.query.server.findMany({
|
||||||
|
orderBy: desc(server.createdAt),
|
||||||
|
where: IS_CLOUD
|
||||||
|
? and(
|
||||||
|
isNotNull(server.sshKeyId),
|
||||||
|
eq(server.adminId, ctx.user.adminId),
|
||||||
|
eq(server.serverStatus, "active"),
|
||||||
|
)
|
||||||
|
: and(isNotNull(server.sshKeyId), eq(server.adminId, ctx.user.adminId)),
|
||||||
|
});
|
||||||
|
return result;
|
||||||
|
}),
|
||||||
|
setup: protectedProcedure
|
||||||
|
.input(apiFindOneServer)
|
||||||
|
.mutation(async ({ input, ctx }) => {
|
||||||
|
try {
|
||||||
|
const server = await findServerById(input.serverId);
|
||||||
|
if (server.adminId !== ctx.user.adminId) {
|
||||||
|
throw new TRPCError({
|
||||||
|
code: "UNAUTHORIZED",
|
||||||
|
message: "You are not authorized to setup this server",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
const currentServer = await serverSetup(input.serverId);
|
||||||
|
return currentServer;
|
||||||
|
} catch (error) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
remove: protectedProcedure
|
||||||
|
.input(apiRemoveServer)
|
||||||
|
.mutation(async ({ input, ctx }) => {
|
||||||
|
try {
|
||||||
|
const server = await findServerById(input.serverId);
|
||||||
|
if (server.adminId !== ctx.user.adminId) {
|
||||||
|
throw new TRPCError({
|
||||||
|
code: "UNAUTHORIZED",
|
||||||
|
message: "You are not authorized to delete this server",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
const activeServers = await haveActiveServices(input.serverId);
|
||||||
|
|
||||||
|
if (activeServers) {
|
||||||
|
throw new TRPCError({
|
||||||
|
code: "BAD_REQUEST",
|
||||||
|
message: "Server has active services, please delete them first",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
const currentServer = await findServerById(input.serverId);
|
||||||
|
await removeDeploymentsByServerId(currentServer);
|
||||||
|
await deleteServer(input.serverId);
|
||||||
|
|
||||||
|
if (IS_CLOUD) {
|
||||||
|
const admin = await findAdminById(ctx.user.adminId);
|
||||||
|
|
||||||
|
await updateServersBasedOnQuantity(
|
||||||
|
admin.adminId,
|
||||||
|
admin.serversQuantity,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return currentServer;
|
||||||
|
} catch (error) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
update: protectedProcedure
|
||||||
|
.input(apiUpdateServer)
|
||||||
|
.mutation(async ({ input, ctx }) => {
|
||||||
|
try {
|
||||||
|
const server = await findServerById(input.serverId);
|
||||||
|
if (server.adminId !== ctx.user.adminId) {
|
||||||
|
throw new TRPCError({
|
||||||
|
code: "UNAUTHORIZED",
|
||||||
|
message: "You are not authorized to update this server",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (server.serverStatus === "inactive") {
|
||||||
|
throw new TRPCError({
|
||||||
|
code: "NOT_FOUND",
|
||||||
|
message: "Server is inactive",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
const currentServer = await updateServerById(input.serverId, {
|
||||||
|
...input,
|
||||||
|
});
|
||||||
|
|
||||||
|
return currentServer;
|
||||||
|
} catch (error) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
});
|
||||||
666
apps/mig/server/api/routers/settings.ts
Normal file
666
apps/mig/server/api/routers/settings.ts
Normal file
@@ -0,0 +1,666 @@
|
|||||||
|
import { db } from "@/server/db";
|
||||||
|
import {
|
||||||
|
apiAssignDomain,
|
||||||
|
apiEnableDashboard,
|
||||||
|
apiModifyTraefikConfig,
|
||||||
|
apiReadStatsLogs,
|
||||||
|
apiReadTraefikConfig,
|
||||||
|
apiSaveSSHKey,
|
||||||
|
apiServerSchema,
|
||||||
|
apiTraefikConfig,
|
||||||
|
apiUpdateDockerCleanup,
|
||||||
|
} from "@/server/db/schema";
|
||||||
|
import { removeJob, schedule } from "@/server/utils/backup";
|
||||||
|
import {
|
||||||
|
IS_CLOUD,
|
||||||
|
canAccessToTraefikFiles,
|
||||||
|
cleanStoppedContainers,
|
||||||
|
cleanUpDockerBuilder,
|
||||||
|
cleanUpSystemPrune,
|
||||||
|
cleanUpUnusedImages,
|
||||||
|
cleanUpUnusedVolumes,
|
||||||
|
execAsync,
|
||||||
|
execAsyncRemote,
|
||||||
|
findAdmin,
|
||||||
|
findAdminById,
|
||||||
|
findServerById,
|
||||||
|
getDokployImage,
|
||||||
|
initializeTraefik,
|
||||||
|
logRotationManager,
|
||||||
|
parseRawConfig,
|
||||||
|
paths,
|
||||||
|
prepareEnvironmentVariables,
|
||||||
|
processLogs,
|
||||||
|
pullLatestRelease,
|
||||||
|
readConfig,
|
||||||
|
readConfigInPath,
|
||||||
|
readDirectory,
|
||||||
|
readMainConfig,
|
||||||
|
readMonitoringConfig,
|
||||||
|
recreateDirectory,
|
||||||
|
sendDockerCleanupNotifications,
|
||||||
|
spawnAsync,
|
||||||
|
startService,
|
||||||
|
startServiceRemote,
|
||||||
|
stopService,
|
||||||
|
stopServiceRemote,
|
||||||
|
updateAdmin,
|
||||||
|
updateLetsEncryptEmail,
|
||||||
|
updateServerById,
|
||||||
|
updateServerTraefik,
|
||||||
|
writeConfig,
|
||||||
|
writeMainConfig,
|
||||||
|
writeTraefikConfigInPath,
|
||||||
|
} from "@dokploy/server";
|
||||||
|
import { generateOpenApiDocument } from "@dokploy/trpc-openapi";
|
||||||
|
import { TRPCError } from "@trpc/server";
|
||||||
|
import { sql } from "drizzle-orm";
|
||||||
|
import { dump, load } from "js-yaml";
|
||||||
|
import { scheduleJob, scheduledJobs } from "node-schedule";
|
||||||
|
import { z } from "zod";
|
||||||
|
import packageInfo from "../../../package.json";
|
||||||
|
import { appRouter } from "../root";
|
||||||
|
import {
|
||||||
|
adminProcedure,
|
||||||
|
createTRPCRouter,
|
||||||
|
protectedProcedure,
|
||||||
|
publicProcedure,
|
||||||
|
} from "../trpc";
|
||||||
|
|
||||||
|
export const settingsRouter = createTRPCRouter({
|
||||||
|
reloadServer: adminProcedure.mutation(async () => {
|
||||||
|
if (IS_CLOUD) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
const { stdout } = await execAsync(
|
||||||
|
"docker service inspect dokploy --format '{{.ID}}'",
|
||||||
|
);
|
||||||
|
await execAsync(`docker service update --force ${stdout.trim()}`);
|
||||||
|
return true;
|
||||||
|
}),
|
||||||
|
reloadTraefik: adminProcedure
|
||||||
|
.input(apiServerSchema)
|
||||||
|
.mutation(async ({ input }) => {
|
||||||
|
try {
|
||||||
|
if (input?.serverId) {
|
||||||
|
await stopServiceRemote(input.serverId, "dokploy-traefik");
|
||||||
|
await startServiceRemote(input.serverId, "dokploy-traefik");
|
||||||
|
} else if (!IS_CLOUD) {
|
||||||
|
await stopService("dokploy-traefik");
|
||||||
|
await startService("dokploy-traefik");
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}),
|
||||||
|
toggleDashboard: adminProcedure
|
||||||
|
.input(apiEnableDashboard)
|
||||||
|
.mutation(async ({ input }) => {
|
||||||
|
await initializeTraefik({
|
||||||
|
enableDashboard: input.enableDashboard,
|
||||||
|
serverId: input.serverId,
|
||||||
|
});
|
||||||
|
return true;
|
||||||
|
}),
|
||||||
|
|
||||||
|
cleanUnusedImages: adminProcedure
|
||||||
|
.input(apiServerSchema)
|
||||||
|
.mutation(async ({ input }) => {
|
||||||
|
await cleanUpUnusedImages(input?.serverId);
|
||||||
|
return true;
|
||||||
|
}),
|
||||||
|
cleanUnusedVolumes: adminProcedure
|
||||||
|
.input(apiServerSchema)
|
||||||
|
.mutation(async ({ input }) => {
|
||||||
|
await cleanUpUnusedVolumes(input?.serverId);
|
||||||
|
return true;
|
||||||
|
}),
|
||||||
|
cleanStoppedContainers: adminProcedure
|
||||||
|
.input(apiServerSchema)
|
||||||
|
.mutation(async ({ input }) => {
|
||||||
|
await cleanStoppedContainers(input?.serverId);
|
||||||
|
return true;
|
||||||
|
}),
|
||||||
|
cleanDockerBuilder: adminProcedure
|
||||||
|
.input(apiServerSchema)
|
||||||
|
.mutation(async ({ input }) => {
|
||||||
|
await cleanUpDockerBuilder(input?.serverId);
|
||||||
|
}),
|
||||||
|
cleanDockerPrune: adminProcedure
|
||||||
|
.input(apiServerSchema)
|
||||||
|
.mutation(async ({ input }) => {
|
||||||
|
await cleanUpSystemPrune(input?.serverId);
|
||||||
|
await cleanUpDockerBuilder(input?.serverId);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}),
|
||||||
|
cleanAll: adminProcedure
|
||||||
|
.input(apiServerSchema)
|
||||||
|
.mutation(async ({ input }) => {
|
||||||
|
await cleanUpUnusedImages(input?.serverId);
|
||||||
|
await cleanStoppedContainers(input?.serverId);
|
||||||
|
await cleanUpDockerBuilder(input?.serverId);
|
||||||
|
await cleanUpSystemPrune(input?.serverId);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}),
|
||||||
|
cleanMonitoring: adminProcedure.mutation(async () => {
|
||||||
|
if (IS_CLOUD) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
const { MONITORING_PATH } = paths();
|
||||||
|
await recreateDirectory(MONITORING_PATH);
|
||||||
|
return true;
|
||||||
|
}),
|
||||||
|
saveSSHPrivateKey: adminProcedure
|
||||||
|
.input(apiSaveSSHKey)
|
||||||
|
.mutation(async ({ input, ctx }) => {
|
||||||
|
if (IS_CLOUD) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
await updateAdmin(ctx.user.authId, {
|
||||||
|
sshPrivateKey: input.sshPrivateKey,
|
||||||
|
});
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}),
|
||||||
|
assignDomainServer: adminProcedure
|
||||||
|
.input(apiAssignDomain)
|
||||||
|
.mutation(async ({ ctx, input }) => {
|
||||||
|
if (IS_CLOUD) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
const admin = await updateAdmin(ctx.user.authId, {
|
||||||
|
host: input.host,
|
||||||
|
...(input.letsEncryptEmail && {
|
||||||
|
letsEncryptEmail: input.letsEncryptEmail,
|
||||||
|
}),
|
||||||
|
certificateType: input.certificateType,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!admin) {
|
||||||
|
throw new TRPCError({
|
||||||
|
code: "NOT_FOUND",
|
||||||
|
message: "Admin not found",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
updateServerTraefik(admin, input.host);
|
||||||
|
if (input.letsEncryptEmail) {
|
||||||
|
updateLetsEncryptEmail(input.letsEncryptEmail);
|
||||||
|
}
|
||||||
|
|
||||||
|
return admin;
|
||||||
|
}),
|
||||||
|
cleanSSHPrivateKey: adminProcedure.mutation(async ({ ctx }) => {
|
||||||
|
if (IS_CLOUD) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
await updateAdmin(ctx.user.authId, {
|
||||||
|
sshPrivateKey: null,
|
||||||
|
});
|
||||||
|
return true;
|
||||||
|
}),
|
||||||
|
updateDockerCleanup: adminProcedure
|
||||||
|
.input(apiUpdateDockerCleanup)
|
||||||
|
.mutation(async ({ input, ctx }) => {
|
||||||
|
if (input.serverId) {
|
||||||
|
await updateServerById(input.serverId, {
|
||||||
|
enableDockerCleanup: input.enableDockerCleanup,
|
||||||
|
});
|
||||||
|
|
||||||
|
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",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (server.enableDockerCleanup) {
|
||||||
|
const server = await findServerById(input.serverId);
|
||||||
|
if (server.serverStatus === "inactive") {
|
||||||
|
throw new TRPCError({
|
||||||
|
code: "NOT_FOUND",
|
||||||
|
message: "Server is inactive",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (IS_CLOUD) {
|
||||||
|
await schedule({
|
||||||
|
cronSchedule: "0 0 * * *",
|
||||||
|
serverId: input.serverId,
|
||||||
|
type: "server",
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
scheduleJob(server.serverId, "0 0 * * *", async () => {
|
||||||
|
console.log(
|
||||||
|
`Docker Cleanup ${new Date().toLocaleString()}] Running...`,
|
||||||
|
);
|
||||||
|
await cleanUpUnusedImages(server.serverId);
|
||||||
|
await cleanUpDockerBuilder(server.serverId);
|
||||||
|
await cleanUpSystemPrune(server.serverId);
|
||||||
|
await sendDockerCleanupNotifications();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (IS_CLOUD) {
|
||||||
|
await removeJob({
|
||||||
|
cronSchedule: "0 0 * * *",
|
||||||
|
serverId: input.serverId,
|
||||||
|
type: "server",
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
const currentJob = scheduledJobs[server.serverId];
|
||||||
|
currentJob?.cancel();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (!IS_CLOUD) {
|
||||||
|
const admin = await findAdminById(ctx.user.adminId);
|
||||||
|
|
||||||
|
if (admin.adminId !== ctx.user.adminId) {
|
||||||
|
throw new TRPCError({
|
||||||
|
code: "UNAUTHORIZED",
|
||||||
|
message: "You are not authorized to access this admin",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
await updateAdmin(ctx.user.authId, {
|
||||||
|
enableDockerCleanup: input.enableDockerCleanup,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (admin.enableDockerCleanup) {
|
||||||
|
scheduleJob("docker-cleanup", "0 0 * * *", async () => {
|
||||||
|
console.log(
|
||||||
|
`Docker Cleanup ${new Date().toLocaleString()}] Running...`,
|
||||||
|
);
|
||||||
|
await cleanUpUnusedImages();
|
||||||
|
await cleanUpDockerBuilder();
|
||||||
|
await cleanUpSystemPrune();
|
||||||
|
await sendDockerCleanupNotifications();
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
const currentJob = scheduledJobs["docker-cleanup"];
|
||||||
|
currentJob?.cancel();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}),
|
||||||
|
|
||||||
|
readTraefikConfig: adminProcedure.query(() => {
|
||||||
|
if (IS_CLOUD) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
const traefikConfig = readMainConfig();
|
||||||
|
return traefikConfig;
|
||||||
|
}),
|
||||||
|
|
||||||
|
updateTraefikConfig: adminProcedure
|
||||||
|
.input(apiTraefikConfig)
|
||||||
|
.mutation(async ({ input }) => {
|
||||||
|
if (IS_CLOUD) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
writeMainConfig(input.traefikConfig);
|
||||||
|
return true;
|
||||||
|
}),
|
||||||
|
|
||||||
|
readWebServerTraefikConfig: adminProcedure.query(() => {
|
||||||
|
if (IS_CLOUD) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
const traefikConfig = readConfig("dokploy");
|
||||||
|
return traefikConfig;
|
||||||
|
}),
|
||||||
|
updateWebServerTraefikConfig: adminProcedure
|
||||||
|
.input(apiTraefikConfig)
|
||||||
|
.mutation(async ({ input }) => {
|
||||||
|
if (IS_CLOUD) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
writeConfig("dokploy", input.traefikConfig);
|
||||||
|
return true;
|
||||||
|
}),
|
||||||
|
|
||||||
|
readMiddlewareTraefikConfig: adminProcedure.query(() => {
|
||||||
|
if (IS_CLOUD) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
const traefikConfig = readConfig("middlewares");
|
||||||
|
return traefikConfig;
|
||||||
|
}),
|
||||||
|
|
||||||
|
updateMiddlewareTraefikConfig: adminProcedure
|
||||||
|
.input(apiTraefikConfig)
|
||||||
|
.mutation(async ({ input }) => {
|
||||||
|
if (IS_CLOUD) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
writeConfig("middlewares", input.traefikConfig);
|
||||||
|
return true;
|
||||||
|
}),
|
||||||
|
|
||||||
|
checkAndUpdateImage: adminProcedure.mutation(async () => {
|
||||||
|
if (IS_CLOUD) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return await pullLatestRelease();
|
||||||
|
}),
|
||||||
|
updateServer: adminProcedure.mutation(async () => {
|
||||||
|
if (IS_CLOUD) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
await spawnAsync("docker", [
|
||||||
|
"service",
|
||||||
|
"update",
|
||||||
|
"--force",
|
||||||
|
"--image",
|
||||||
|
getDokployImage(),
|
||||||
|
"dokploy",
|
||||||
|
]);
|
||||||
|
return true;
|
||||||
|
}),
|
||||||
|
|
||||||
|
getDokployVersion: adminProcedure.query(() => {
|
||||||
|
return packageInfo.version;
|
||||||
|
}),
|
||||||
|
readDirectories: protectedProcedure
|
||||||
|
.input(apiServerSchema)
|
||||||
|
.query(async ({ ctx, input }) => {
|
||||||
|
try {
|
||||||
|
if (ctx.user.rol === "user") {
|
||||||
|
const canAccess = await canAccessToTraefikFiles(ctx.user.authId);
|
||||||
|
|
||||||
|
if (!canAccess) {
|
||||||
|
throw new TRPCError({ code: "UNAUTHORIZED" });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const { MAIN_TRAEFIK_PATH } = paths(!!input?.serverId);
|
||||||
|
const result = await readDirectory(MAIN_TRAEFIK_PATH, input?.serverId);
|
||||||
|
return result || [];
|
||||||
|
} catch (error) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
|
||||||
|
updateTraefikFile: protectedProcedure
|
||||||
|
.input(apiModifyTraefikConfig)
|
||||||
|
.mutation(async ({ input, ctx }) => {
|
||||||
|
if (ctx.user.rol === "user") {
|
||||||
|
const canAccess = await canAccessToTraefikFiles(ctx.user.authId);
|
||||||
|
|
||||||
|
if (!canAccess) {
|
||||||
|
throw new TRPCError({ code: "UNAUTHORIZED" });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
await writeTraefikConfigInPath(
|
||||||
|
input.path,
|
||||||
|
input.traefikConfig,
|
||||||
|
input?.serverId,
|
||||||
|
);
|
||||||
|
return true;
|
||||||
|
}),
|
||||||
|
|
||||||
|
readTraefikFile: protectedProcedure
|
||||||
|
.input(apiReadTraefikConfig)
|
||||||
|
.query(async ({ input, ctx }) => {
|
||||||
|
if (ctx.user.rol === "user") {
|
||||||
|
const canAccess = await canAccessToTraefikFiles(ctx.user.authId);
|
||||||
|
|
||||||
|
if (!canAccess) {
|
||||||
|
throw new TRPCError({ code: "UNAUTHORIZED" });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return readConfigInPath(input.path, input.serverId);
|
||||||
|
}),
|
||||||
|
getIp: protectedProcedure.query(async () => {
|
||||||
|
if (IS_CLOUD) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
const admin = await findAdmin();
|
||||||
|
return admin.serverIp;
|
||||||
|
}),
|
||||||
|
|
||||||
|
getOpenApiDocument: protectedProcedure.query(
|
||||||
|
async ({ ctx }): Promise<unknown> => {
|
||||||
|
const protocol = ctx.req.headers["x-forwarded-proto"];
|
||||||
|
const url = `${protocol}://${ctx.req.headers.host}/api`;
|
||||||
|
const openApiDocument = generateOpenApiDocument(appRouter, {
|
||||||
|
title: "tRPC OpenAPI",
|
||||||
|
version: "1.0.0",
|
||||||
|
baseUrl: url,
|
||||||
|
docsUrl: `${url}/settings.getOpenApiDocument`,
|
||||||
|
tags: [
|
||||||
|
"admin",
|
||||||
|
"docker",
|
||||||
|
"compose",
|
||||||
|
"registry",
|
||||||
|
"cluster",
|
||||||
|
"user",
|
||||||
|
"domain",
|
||||||
|
"destination",
|
||||||
|
"backup",
|
||||||
|
"deployment",
|
||||||
|
"mounts",
|
||||||
|
"certificates",
|
||||||
|
"settings",
|
||||||
|
"security",
|
||||||
|
"redirects",
|
||||||
|
"port",
|
||||||
|
"project",
|
||||||
|
"application",
|
||||||
|
"mysql",
|
||||||
|
"postgres",
|
||||||
|
"redis",
|
||||||
|
"mongo",
|
||||||
|
"mariadb",
|
||||||
|
"sshRouter",
|
||||||
|
"gitProvider",
|
||||||
|
"bitbucket",
|
||||||
|
"github",
|
||||||
|
"gitlab",
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
openApiDocument.info = {
|
||||||
|
title: "Dokploy API",
|
||||||
|
description: "Endpoints for dokploy",
|
||||||
|
// TODO: get version from package.json
|
||||||
|
version: "1.0.0",
|
||||||
|
};
|
||||||
|
|
||||||
|
return openApiDocument;
|
||||||
|
},
|
||||||
|
),
|
||||||
|
readTraefikEnv: adminProcedure
|
||||||
|
.input(apiServerSchema)
|
||||||
|
.query(async ({ input }) => {
|
||||||
|
const command =
|
||||||
|
"docker service inspect --format='{{range .Spec.TaskTemplate.ContainerSpec.Env}}{{println .}}{{end}}' dokploy-traefik";
|
||||||
|
|
||||||
|
if (input?.serverId) {
|
||||||
|
const result = await execAsyncRemote(input.serverId, command);
|
||||||
|
return result.stdout.trim();
|
||||||
|
}
|
||||||
|
if (!IS_CLOUD) {
|
||||||
|
const result = await execAsync(command);
|
||||||
|
return result.stdout.trim();
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
|
||||||
|
writeTraefikEnv: adminProcedure
|
||||||
|
.input(z.object({ env: z.string(), serverId: z.string().optional() }))
|
||||||
|
.mutation(async ({ input }) => {
|
||||||
|
const envs = prepareEnvironmentVariables(input.env);
|
||||||
|
await initializeTraefik({
|
||||||
|
env: envs,
|
||||||
|
serverId: input.serverId,
|
||||||
|
});
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}),
|
||||||
|
haveTraefikDashboardPortEnabled: adminProcedure
|
||||||
|
.input(apiServerSchema)
|
||||||
|
.query(async ({ input }) => {
|
||||||
|
const command = `docker service inspect --format='{{json .Endpoint.Ports}}' dokploy-traefik`;
|
||||||
|
|
||||||
|
let stdout = "";
|
||||||
|
if (input?.serverId) {
|
||||||
|
const result = await execAsyncRemote(input.serverId, command);
|
||||||
|
stdout = result.stdout;
|
||||||
|
} else if (!IS_CLOUD) {
|
||||||
|
const result = await execAsync(
|
||||||
|
"docker service inspect --format='{{json .Endpoint.Ports}}' dokploy-traefik",
|
||||||
|
);
|
||||||
|
stdout = result.stdout;
|
||||||
|
}
|
||||||
|
|
||||||
|
const parsed: any[] = JSON.parse(stdout.trim());
|
||||||
|
for (const port of parsed) {
|
||||||
|
if (port.PublishedPort === 8080) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}),
|
||||||
|
|
||||||
|
readStatsLogs: adminProcedure
|
||||||
|
.meta({
|
||||||
|
openapi: {
|
||||||
|
path: "/read-stats-logs",
|
||||||
|
method: "POST",
|
||||||
|
override: true,
|
||||||
|
enabled: false,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.input(apiReadStatsLogs)
|
||||||
|
.query(({ input }) => {
|
||||||
|
if (IS_CLOUD) {
|
||||||
|
return {
|
||||||
|
data: [],
|
||||||
|
totalCount: 0,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
const rawConfig = readMonitoringConfig();
|
||||||
|
const parsedConfig = parseRawConfig(
|
||||||
|
rawConfig as string,
|
||||||
|
input.page,
|
||||||
|
input.sort,
|
||||||
|
input.search,
|
||||||
|
input.status,
|
||||||
|
);
|
||||||
|
|
||||||
|
return parsedConfig;
|
||||||
|
}),
|
||||||
|
readStats: adminProcedure.query(() => {
|
||||||
|
if (IS_CLOUD) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
const rawConfig = readMonitoringConfig();
|
||||||
|
const processedLogs = processLogs(rawConfig as string);
|
||||||
|
return processedLogs || [];
|
||||||
|
}),
|
||||||
|
getLogRotateStatus: adminProcedure.query(async () => {
|
||||||
|
if (IS_CLOUD) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return await logRotationManager.getStatus();
|
||||||
|
}),
|
||||||
|
toggleLogRotate: adminProcedure
|
||||||
|
.input(
|
||||||
|
z.object({
|
||||||
|
enable: z.boolean(),
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.mutation(async ({ input }) => {
|
||||||
|
if (IS_CLOUD) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (input.enable) {
|
||||||
|
await logRotationManager.activate();
|
||||||
|
} else {
|
||||||
|
await logRotationManager.deactivate();
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}),
|
||||||
|
haveActivateRequests: adminProcedure.query(async () => {
|
||||||
|
if (IS_CLOUD) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
const config = readMainConfig();
|
||||||
|
|
||||||
|
if (!config) return false;
|
||||||
|
const parsedConfig = load(config) as {
|
||||||
|
accessLog?: {
|
||||||
|
filePath: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
return !!parsedConfig?.accessLog?.filePath;
|
||||||
|
}),
|
||||||
|
toggleRequests: adminProcedure
|
||||||
|
.input(
|
||||||
|
z.object({
|
||||||
|
enable: z.boolean(),
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.mutation(async ({ input }) => {
|
||||||
|
if (IS_CLOUD) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
const mainConfig = readMainConfig();
|
||||||
|
if (!mainConfig) return false;
|
||||||
|
|
||||||
|
const currentConfig = load(mainConfig) as {
|
||||||
|
accessLog?: {
|
||||||
|
filePath: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
if (input.enable) {
|
||||||
|
const config = {
|
||||||
|
accessLog: {
|
||||||
|
filePath: "/etc/dokploy/traefik/dynamic/access.log",
|
||||||
|
format: "json",
|
||||||
|
bufferingSize: 100,
|
||||||
|
filters: {
|
||||||
|
retryAttempts: true,
|
||||||
|
minDuration: "10ms",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
currentConfig.accessLog = config.accessLog;
|
||||||
|
} else {
|
||||||
|
currentConfig.accessLog = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
writeMainConfig(dump(currentConfig));
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}),
|
||||||
|
isCloud: protectedProcedure.query(async () => {
|
||||||
|
return IS_CLOUD;
|
||||||
|
}),
|
||||||
|
health: publicProcedure.query(async () => {
|
||||||
|
if (IS_CLOUD) {
|
||||||
|
try {
|
||||||
|
await db.execute(sql`SELECT 1`);
|
||||||
|
return { status: "ok" };
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Database connection error:", error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return { status: "not_cloud" };
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
// {
|
||||||
|
// "Parallelism": 1,
|
||||||
|
// "Delay": 10000000000,
|
||||||
|
// "FailureAction": "rollback",
|
||||||
|
// "Order": "start-first"
|
||||||
|
// }
|
||||||
103
apps/mig/server/api/routers/ssh-key.ts
Normal file
103
apps/mig/server/api/routers/ssh-key.ts
Normal file
@@ -0,0 +1,103 @@
|
|||||||
|
import { createTRPCRouter, protectedProcedure } from "@/server/api/trpc";
|
||||||
|
import { db } from "@/server/db";
|
||||||
|
import {
|
||||||
|
apiCreateSshKey,
|
||||||
|
apiFindOneSshKey,
|
||||||
|
apiGenerateSSHKey,
|
||||||
|
apiRemoveSshKey,
|
||||||
|
apiUpdateSshKey,
|
||||||
|
sshKeys,
|
||||||
|
} from "@/server/db/schema";
|
||||||
|
import {
|
||||||
|
IS_CLOUD,
|
||||||
|
createSshKey,
|
||||||
|
findSSHKeyById,
|
||||||
|
generateSSHKey,
|
||||||
|
removeSSHKeyById,
|
||||||
|
updateSSHKeyById,
|
||||||
|
} from "@dokploy/server";
|
||||||
|
import { TRPCError } from "@trpc/server";
|
||||||
|
import { eq } from "drizzle-orm";
|
||||||
|
|
||||||
|
export const sshRouter = createTRPCRouter({
|
||||||
|
create: protectedProcedure
|
||||||
|
.input(apiCreateSshKey)
|
||||||
|
.mutation(async ({ input, ctx }) => {
|
||||||
|
try {
|
||||||
|
await createSshKey({
|
||||||
|
...input,
|
||||||
|
adminId: ctx.user.adminId,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
throw new TRPCError({
|
||||||
|
code: "BAD_REQUEST",
|
||||||
|
message: "Error to create the ssh key",
|
||||||
|
cause: error,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
remove: protectedProcedure
|
||||||
|
.input(apiRemoveSshKey)
|
||||||
|
.mutation(async ({ input, ctx }) => {
|
||||||
|
try {
|
||||||
|
const sshKey = await findSSHKeyById(input.sshKeyId);
|
||||||
|
if (IS_CLOUD && sshKey.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 ssh key",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return await removeSSHKeyById(input.sshKeyId);
|
||||||
|
} catch (error) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
one: protectedProcedure
|
||||||
|
.input(apiFindOneSshKey)
|
||||||
|
.query(async ({ input, ctx }) => {
|
||||||
|
const sshKey = await findSSHKeyById(input.sshKeyId);
|
||||||
|
|
||||||
|
if (IS_CLOUD && sshKey.adminId !== ctx.user.adminId) {
|
||||||
|
// TODO: Remove isCloud in the next versions of dokploy
|
||||||
|
throw new TRPCError({
|
||||||
|
code: "UNAUTHORIZED",
|
||||||
|
message: "You are not allowed to access this ssh key",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return sshKey;
|
||||||
|
}),
|
||||||
|
all: protectedProcedure.query(async ({ ctx }) => {
|
||||||
|
return await db.query.sshKeys.findMany({
|
||||||
|
...(IS_CLOUD && { where: eq(sshKeys.adminId, ctx.user.adminId) }),
|
||||||
|
});
|
||||||
|
// TODO: Remove this line when the cloud version is ready
|
||||||
|
}),
|
||||||
|
generate: protectedProcedure
|
||||||
|
.input(apiGenerateSSHKey)
|
||||||
|
.mutation(async ({ input }) => {
|
||||||
|
return await generateSSHKey(input.type);
|
||||||
|
}),
|
||||||
|
update: protectedProcedure
|
||||||
|
.input(apiUpdateSshKey)
|
||||||
|
.mutation(async ({ input, ctx }) => {
|
||||||
|
try {
|
||||||
|
const sshKey = await findSSHKeyById(input.sshKeyId);
|
||||||
|
if (IS_CLOUD && sshKey.adminId !== ctx.user.adminId) {
|
||||||
|
// TODO: Remove isCloud in the next versions of dokploy
|
||||||
|
throw new TRPCError({
|
||||||
|
code: "UNAUTHORIZED",
|
||||||
|
message: "You are not allowed to update this ssh key",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return await updateSSHKeyById(input);
|
||||||
|
} catch (error) {
|
||||||
|
throw new TRPCError({
|
||||||
|
code: "BAD_REQUEST",
|
||||||
|
message: "Error to update this ssh key",
|
||||||
|
cause: error,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
});
|
||||||
130
apps/mig/server/api/routers/stripe.ts
Normal file
130
apps/mig/server/api/routers/stripe.ts
Normal file
@@ -0,0 +1,130 @@
|
|||||||
|
import { WEBSITE_URL, getStripeItems } from "@/server/utils/stripe";
|
||||||
|
import {
|
||||||
|
IS_CLOUD,
|
||||||
|
findAdminById,
|
||||||
|
findServersByAdminId,
|
||||||
|
updateAdmin,
|
||||||
|
} from "@dokploy/server";
|
||||||
|
import { TRPCError } from "@trpc/server";
|
||||||
|
import Stripe from "stripe";
|
||||||
|
import { z } from "zod";
|
||||||
|
import { adminProcedure, createTRPCRouter } from "../trpc";
|
||||||
|
|
||||||
|
export const stripeRouter = createTRPCRouter({
|
||||||
|
getProducts: adminProcedure.query(async ({ ctx }) => {
|
||||||
|
const admin = await findAdminById(ctx.user.adminId);
|
||||||
|
const stripeCustomerId = admin.stripeCustomerId;
|
||||||
|
|
||||||
|
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY || "", {
|
||||||
|
apiVersion: "2024-09-30.acacia",
|
||||||
|
});
|
||||||
|
|
||||||
|
const products = await stripe.products.list({
|
||||||
|
expand: ["data.default_price"],
|
||||||
|
active: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!stripeCustomerId) {
|
||||||
|
return {
|
||||||
|
products: products.data,
|
||||||
|
subscriptions: [],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const subscriptions = await stripe.subscriptions.list({
|
||||||
|
customer: stripeCustomerId,
|
||||||
|
status: "active",
|
||||||
|
expand: ["data.items.data.price"],
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
products: products.data,
|
||||||
|
subscriptions: subscriptions.data,
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
createCheckoutSession: adminProcedure
|
||||||
|
.input(
|
||||||
|
z.object({
|
||||||
|
productId: z.string(),
|
||||||
|
serverQuantity: z.number().min(1),
|
||||||
|
isAnnual: z.boolean(),
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.mutation(async ({ ctx, input }) => {
|
||||||
|
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY || "", {
|
||||||
|
apiVersion: "2024-09-30.acacia",
|
||||||
|
});
|
||||||
|
|
||||||
|
const items = getStripeItems(input.serverQuantity, input.isAnnual);
|
||||||
|
const admin = await findAdminById(ctx.user.adminId);
|
||||||
|
|
||||||
|
let stripeCustomerId = admin.stripeCustomerId;
|
||||||
|
|
||||||
|
if (stripeCustomerId) {
|
||||||
|
const customer = await stripe.customers.retrieve(stripeCustomerId);
|
||||||
|
|
||||||
|
if (customer.deleted) {
|
||||||
|
await updateAdmin(admin.authId, {
|
||||||
|
stripeCustomerId: null,
|
||||||
|
});
|
||||||
|
stripeCustomerId = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const session = await stripe.checkout.sessions.create({
|
||||||
|
mode: "subscription",
|
||||||
|
line_items: items,
|
||||||
|
...(stripeCustomerId && {
|
||||||
|
customer: stripeCustomerId,
|
||||||
|
}),
|
||||||
|
metadata: {
|
||||||
|
adminId: admin.adminId,
|
||||||
|
},
|
||||||
|
success_url: `${WEBSITE_URL}/dashboard/settings/billing`,
|
||||||
|
cancel_url: `${WEBSITE_URL}/dashboard/settings/billing`,
|
||||||
|
});
|
||||||
|
|
||||||
|
return { sessionId: session.id };
|
||||||
|
}),
|
||||||
|
createCustomerPortalSession: adminProcedure.mutation(
|
||||||
|
async ({ ctx, input }) => {
|
||||||
|
const admin = await findAdminById(ctx.user.adminId);
|
||||||
|
|
||||||
|
if (!admin.stripeCustomerId) {
|
||||||
|
throw new TRPCError({
|
||||||
|
code: "BAD_REQUEST",
|
||||||
|
message: "Stripe Customer ID not found",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
const stripeCustomerId = admin.stripeCustomerId;
|
||||||
|
|
||||||
|
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY || "", {
|
||||||
|
apiVersion: "2024-09-30.acacia",
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
const session = await stripe.billingPortal.sessions.create({
|
||||||
|
customer: stripeCustomerId,
|
||||||
|
return_url: `${WEBSITE_URL}/dashboard/settings/billing`,
|
||||||
|
});
|
||||||
|
|
||||||
|
return { url: session.url };
|
||||||
|
} catch (error) {
|
||||||
|
return {
|
||||||
|
url: "",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
|
||||||
|
canCreateMoreServers: adminProcedure.query(async ({ ctx }) => {
|
||||||
|
const admin = await findAdminById(ctx.user.adminId);
|
||||||
|
const servers = await findServersByAdminId(admin.adminId);
|
||||||
|
|
||||||
|
if (!IS_CLOUD) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return servers.length < admin.serversQuantity;
|
||||||
|
}),
|
||||||
|
});
|
||||||
34
apps/mig/server/api/routers/user.ts
Normal file
34
apps/mig/server/api/routers/user.ts
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
import { apiFindOneUser, apiFindOneUserByAuth } from "@/server/db/schema";
|
||||||
|
import { findUserByAuthId, findUserById, findUsers } from "@dokploy/server";
|
||||||
|
import { TRPCError } from "@trpc/server";
|
||||||
|
import { adminProcedure, createTRPCRouter, protectedProcedure } from "../trpc";
|
||||||
|
|
||||||
|
export const userRouter = createTRPCRouter({
|
||||||
|
all: adminProcedure.query(async ({ ctx }) => {
|
||||||
|
return await findUsers(ctx.user.adminId);
|
||||||
|
}),
|
||||||
|
byAuthId: protectedProcedure
|
||||||
|
.input(apiFindOneUserByAuth)
|
||||||
|
.query(async ({ input, ctx }) => {
|
||||||
|
const user = await findUserByAuthId(input.authId);
|
||||||
|
if (user.adminId !== ctx.user.adminId) {
|
||||||
|
throw new TRPCError({
|
||||||
|
code: "UNAUTHORIZED",
|
||||||
|
message: "You are not allowed to access this user",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return user;
|
||||||
|
}),
|
||||||
|
byUserId: protectedProcedure
|
||||||
|
.input(apiFindOneUser)
|
||||||
|
.query(async ({ input, ctx }) => {
|
||||||
|
const user = await findUserById(input.userId);
|
||||||
|
if (user.adminId !== ctx.user.adminId) {
|
||||||
|
throw new TRPCError({
|
||||||
|
code: "UNAUTHORIZED",
|
||||||
|
message: "You are not allowed to access this user",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return user;
|
||||||
|
}),
|
||||||
|
});
|
||||||
210
apps/mig/server/api/trpc.ts
Normal file
210
apps/mig/server/api/trpc.ts
Normal file
@@ -0,0 +1,210 @@
|
|||||||
|
/**
|
||||||
|
* YOU PROBABLY DON'T NEED TO EDIT THIS FILE, UNLESS:
|
||||||
|
* 1. You want to modify request context (see Part 1).
|
||||||
|
* 2. You want to create a new middleware or type of procedure (see Part 3).
|
||||||
|
*
|
||||||
|
* TL;DR - This is where all the tRPC server stuff is created and plugged in. The pieces you will
|
||||||
|
* need to use are documented accordingly near the end.
|
||||||
|
*/
|
||||||
|
|
||||||
|
// import { getServerAuthSession } from "@/server/auth";
|
||||||
|
import { db } from "@/server/db";
|
||||||
|
import { validateBearerToken, validateRequest } from "@dokploy/server";
|
||||||
|
import type { OpenApiMeta } from "@dokploy/trpc-openapi";
|
||||||
|
import { TRPCError, initTRPC } from "@trpc/server";
|
||||||
|
import type { CreateNextContextOptions } from "@trpc/server/adapters/next";
|
||||||
|
import {
|
||||||
|
experimental_createMemoryUploadHandler,
|
||||||
|
experimental_isMultipartFormDataRequest,
|
||||||
|
experimental_parseMultipartFormData,
|
||||||
|
} from "@trpc/server/adapters/node-http/content-type/form-data";
|
||||||
|
import type { Session, User } from "lucia";
|
||||||
|
import superjson from "superjson";
|
||||||
|
import { ZodError } from "zod";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 1. CONTEXT
|
||||||
|
*
|
||||||
|
* This section defines the "contexts" that are available in the backend API.
|
||||||
|
*
|
||||||
|
* These allow you to access things when processing a request, like the database, the session, etc.
|
||||||
|
*/
|
||||||
|
|
||||||
|
interface CreateContextOptions {
|
||||||
|
user: (User & { authId: string; adminId: string }) | null;
|
||||||
|
session: Session | null;
|
||||||
|
req: CreateNextContextOptions["req"];
|
||||||
|
res: CreateNextContextOptions["res"];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This helper generates the "internals" for a tRPC context. If you need to use it, you can export
|
||||||
|
* it from here.
|
||||||
|
*
|
||||||
|
* Examples of things you may need it for:
|
||||||
|
* - testing, so we don't have to mock Next.js' req/res
|
||||||
|
* - tRPC's `createSSGHelpers`, where we don't have req/res
|
||||||
|
*
|
||||||
|
* @see https://create.t3.gg/en/usage/trpc#-serverapitrpcts
|
||||||
|
*/
|
||||||
|
const createInnerTRPCContext = (opts: CreateContextOptions) => {
|
||||||
|
return {
|
||||||
|
session: opts.session,
|
||||||
|
db,
|
||||||
|
req: opts.req,
|
||||||
|
res: opts.res,
|
||||||
|
user: opts.user,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This is the actual context you will use in your router. It will be used to process every request
|
||||||
|
* that goes through your tRPC endpoint.
|
||||||
|
*
|
||||||
|
* @see https://trpc.io/docs/context
|
||||||
|
*/
|
||||||
|
export const createTRPCContext = async (opts: CreateNextContextOptions) => {
|
||||||
|
const { req, res } = opts;
|
||||||
|
|
||||||
|
let { session, user } = await validateBearerToken(req);
|
||||||
|
|
||||||
|
if (!session) {
|
||||||
|
const cookieResult = await validateRequest(req, res);
|
||||||
|
session = cookieResult.session;
|
||||||
|
user = cookieResult.user;
|
||||||
|
}
|
||||||
|
|
||||||
|
return createInnerTRPCContext({
|
||||||
|
req,
|
||||||
|
res,
|
||||||
|
session: session,
|
||||||
|
...((user && {
|
||||||
|
user: {
|
||||||
|
authId: user.id,
|
||||||
|
email: user.email,
|
||||||
|
rol: user.rol,
|
||||||
|
id: user.id,
|
||||||
|
secret: user.secret,
|
||||||
|
adminId: user.adminId,
|
||||||
|
},
|
||||||
|
}) || {
|
||||||
|
user: null,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 2. INITIALIZATION
|
||||||
|
*
|
||||||
|
* This is where the tRPC API is initialized, connecting the context and transformer. We also parse
|
||||||
|
* ZodErrors so that you get typesafety on the frontend if your procedure fails due to validation
|
||||||
|
* errors on the backend.
|
||||||
|
*/
|
||||||
|
|
||||||
|
const t = initTRPC
|
||||||
|
.meta<OpenApiMeta>()
|
||||||
|
.context<typeof createTRPCContext>()
|
||||||
|
.create({
|
||||||
|
transformer: superjson,
|
||||||
|
errorFormatter({ shape, error }) {
|
||||||
|
return {
|
||||||
|
...shape,
|
||||||
|
data: {
|
||||||
|
...shape.data,
|
||||||
|
zodError:
|
||||||
|
error.cause instanceof ZodError ? error.cause.flatten() : null,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 3. ROUTER & PROCEDURE (THE IMPORTANT BIT)
|
||||||
|
*
|
||||||
|
* These are the pieces you use to build your tRPC API. You should import these a lot in the
|
||||||
|
* "/src/server/api/routers" directory.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This is how you create new routers and sub-routers in your tRPC API.
|
||||||
|
*
|
||||||
|
* @see https://trpc.io/docs/router
|
||||||
|
*/
|
||||||
|
export const createTRPCRouter = t.router;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Public (unauthenticated) procedure
|
||||||
|
*
|
||||||
|
* This is the base piece you use to build new queries and mutations on your tRPC API. It does not
|
||||||
|
* guarantee that a user querying is authorized, but you can still access user session data if they
|
||||||
|
* are logged in.
|
||||||
|
*/
|
||||||
|
export const publicProcedure = t.procedure;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Protected (authenticated) procedure
|
||||||
|
*
|
||||||
|
* If you want a query or mutation to ONLY be accessible to logged in users, use this. It verifies
|
||||||
|
* the session is valid and guarantees `ctx.session.user` is not null.
|
||||||
|
*
|
||||||
|
* @see https://trpc.io/docs/procedures
|
||||||
|
*/
|
||||||
|
export const protectedProcedure = t.procedure.use(({ ctx, next }) => {
|
||||||
|
if (!ctx.session || !ctx.user) {
|
||||||
|
throw new TRPCError({ code: "UNAUTHORIZED" });
|
||||||
|
}
|
||||||
|
return next({
|
||||||
|
ctx: {
|
||||||
|
// infers the `session` as non-nullable
|
||||||
|
session: ctx.session,
|
||||||
|
user: ctx.user,
|
||||||
|
// session: { ...ctx.session, user: ctx.user },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
export const uploadProcedure = async (opts: any) => {
|
||||||
|
if (!experimental_isMultipartFormDataRequest(opts.ctx.req)) {
|
||||||
|
return opts.next();
|
||||||
|
}
|
||||||
|
|
||||||
|
const formData = await experimental_parseMultipartFormData(
|
||||||
|
opts.ctx.req,
|
||||||
|
experimental_createMemoryUploadHandler({
|
||||||
|
// 2GB
|
||||||
|
maxPartSize: 1024 * 1024 * 1024 * 2,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
return opts.next({
|
||||||
|
rawInput: formData,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const cliProcedure = t.procedure.use(({ ctx, next }) => {
|
||||||
|
if (!ctx.session || !ctx.user || ctx.user.rol !== "admin") {
|
||||||
|
throw new TRPCError({ code: "UNAUTHORIZED" });
|
||||||
|
}
|
||||||
|
return next({
|
||||||
|
ctx: {
|
||||||
|
// infers the `session` as non-nullable
|
||||||
|
session: ctx.session,
|
||||||
|
user: ctx.user,
|
||||||
|
// session: { ...ctx.session, user: ctx.user },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
export const adminProcedure = t.procedure.use(({ ctx, next }) => {
|
||||||
|
if (!ctx.session || !ctx.user || ctx.user.rol !== "admin") {
|
||||||
|
throw new TRPCError({ code: "UNAUTHORIZED" });
|
||||||
|
}
|
||||||
|
return next({
|
||||||
|
ctx: {
|
||||||
|
// infers the `session` as non-nullable
|
||||||
|
session: ctx.session,
|
||||||
|
user: ctx.user,
|
||||||
|
// session: { ...ctx.session, user: ctx.user },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
14
apps/mig/server/db/drizzle.config.ts
Normal file
14
apps/mig/server/db/drizzle.config.ts
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
import { defineConfig } from "drizzle-kit";
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
schema: "./server/db/schema/index.ts",
|
||||||
|
dialect: "postgresql",
|
||||||
|
dbCredentials: {
|
||||||
|
url: process.env.DATABASE_URL || "",
|
||||||
|
},
|
||||||
|
out: "drizzle",
|
||||||
|
migrations: {
|
||||||
|
table: "migrations",
|
||||||
|
schema: "public",
|
||||||
|
},
|
||||||
|
});
|
||||||
21
apps/mig/server/db/index.ts
Normal file
21
apps/mig/server/db/index.ts
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
import { type PostgresJsDatabase, drizzle } from "drizzle-orm/postgres-js";
|
||||||
|
import postgres from "postgres";
|
||||||
|
import * as schema from "./schema";
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
var db: PostgresJsDatabase<typeof schema> | undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
export let db: PostgresJsDatabase<typeof schema>;
|
||||||
|
if (process.env.NODE_ENV === "production") {
|
||||||
|
db = drizzle(postgres(process.env.DATABASE_URL || ""), {
|
||||||
|
schema,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
if (!global.db)
|
||||||
|
global.db = drizzle(postgres(process.env.DATABASE_URL || ""), {
|
||||||
|
schema,
|
||||||
|
});
|
||||||
|
|
||||||
|
db = global.db;
|
||||||
|
}
|
||||||
21
apps/mig/server/db/migration.ts
Normal file
21
apps/mig/server/db/migration.ts
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
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);
|
||||||
|
|
||||||
|
export const migration = async () =>
|
||||||
|
await migrate(db, { migrationsFolder: "drizzle" })
|
||||||
|
.then(() => {
|
||||||
|
console.log("Migration complete");
|
||||||
|
sql.end();
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.log("Migration failed", error);
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
sql.end();
|
||||||
|
});
|
||||||
23
apps/mig/server/db/reset.ts
Normal file
23
apps/mig/server/db/reset.ts
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
import { sql } from "drizzle-orm";
|
||||||
|
// Credits to Louistiti from Drizzle Discord: https://discord.com/channels/1043890932593987624/1130802621750448160/1143083373535973406
|
||||||
|
import { drizzle } from "drizzle-orm/postgres-js";
|
||||||
|
import postgres from "postgres";
|
||||||
|
|
||||||
|
const connectionString = process.env.DATABASE_URL || "";
|
||||||
|
|
||||||
|
const pg = postgres(connectionString, { max: 1 });
|
||||||
|
const db = drizzle(pg);
|
||||||
|
|
||||||
|
const clearDb = async (): Promise<void> => {
|
||||||
|
try {
|
||||||
|
const tablesQuery = sql<string>`DROP SCHEMA public CASCADE; CREATE SCHEMA public; DROP schema drizzle CASCADE;`;
|
||||||
|
const tables = await db.execute(tablesQuery);
|
||||||
|
console.log(tables);
|
||||||
|
await pg.end();
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error to clean database", error);
|
||||||
|
} finally {
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
clearDb();
|
||||||
1
apps/mig/server/db/schema/index.ts
Normal file
1
apps/mig/server/db/schema/index.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export * from "@dokploy/server/db/schema";
|
||||||
35
apps/mig/server/db/seed.ts
Normal file
35
apps/mig/server/db/seed.ts
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
import bc from "bcrypt";
|
||||||
|
import { drizzle } from "drizzle-orm/postgres-js";
|
||||||
|
import postgres from "postgres";
|
||||||
|
import { users } from "./schema";
|
||||||
|
|
||||||
|
const connectionString = process.env.DATABASE_URL || "";
|
||||||
|
|
||||||
|
const pg = postgres(connectionString, { max: 1 });
|
||||||
|
const db = drizzle(pg);
|
||||||
|
|
||||||
|
function password(txt: string) {
|
||||||
|
return bc.hashSync(txt, 10);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function seed() {
|
||||||
|
console.log("> Seed:", process.env.DATABASE_PATH, "\n");
|
||||||
|
|
||||||
|
// const authenticationR = await db
|
||||||
|
// .insert(users)
|
||||||
|
// .values([
|
||||||
|
// {
|
||||||
|
// email: "user1@hotmail.com",
|
||||||
|
// password: password("12345671"),
|
||||||
|
// },
|
||||||
|
// ])
|
||||||
|
// .onConflictDoNothing()
|
||||||
|
// .returning();
|
||||||
|
|
||||||
|
// console.log("\nSemillas Update:", authenticationR.length);
|
||||||
|
}
|
||||||
|
|
||||||
|
seed().catch((e) => {
|
||||||
|
console.error(e);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
46
apps/mig/server/db/validations/domain.ts
Normal file
46
apps/mig/server/db/validations/domain.ts
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
export const domain = z
|
||||||
|
.object({
|
||||||
|
host: z.string().min(1, { message: "Add a hostname" }),
|
||||||
|
path: z.string().min(1).optional(),
|
||||||
|
port: z
|
||||||
|
.number()
|
||||||
|
.min(1, { message: "Port must be at least 1" })
|
||||||
|
.max(65535, { message: "Port must be 65535 or below" })
|
||||||
|
.optional(),
|
||||||
|
https: z.boolean().optional(),
|
||||||
|
certificateType: z.enum(["letsencrypt", "none"]).optional(),
|
||||||
|
})
|
||||||
|
.superRefine((input, ctx) => {
|
||||||
|
if (input.https && !input.certificateType) {
|
||||||
|
ctx.addIssue({
|
||||||
|
code: z.ZodIssueCode.custom,
|
||||||
|
path: ["certificateType"],
|
||||||
|
message: "Required",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
export const domainCompose = z
|
||||||
|
.object({
|
||||||
|
host: z.string().min(1, { message: "Host is required" }),
|
||||||
|
path: z.string().min(1).optional(),
|
||||||
|
port: z
|
||||||
|
.number()
|
||||||
|
.min(1, { message: "Port must be at least 1" })
|
||||||
|
.max(65535, { message: "Port must be 65535 or below" })
|
||||||
|
.optional(),
|
||||||
|
https: z.boolean().optional(),
|
||||||
|
certificateType: z.enum(["letsencrypt", "none"]).optional(),
|
||||||
|
serviceName: z.string().min(1, { message: "Service name is required" }),
|
||||||
|
})
|
||||||
|
.superRefine((input, ctx) => {
|
||||||
|
if (input.https && !input.certificateType) {
|
||||||
|
ctx.addIssue({
|
||||||
|
code: z.ZodIssueCode.custom,
|
||||||
|
path: ["certificateType"],
|
||||||
|
message: "Required",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
37
apps/mig/server/db/validations/index.ts
Normal file
37
apps/mig/server/db/validations/index.ts
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
export const sshKeyCreate = z.object({
|
||||||
|
name: z.string().min(1),
|
||||||
|
description: z.string().optional(),
|
||||||
|
publicKey: z.string().refine(
|
||||||
|
(key) => {
|
||||||
|
const rsaPubPattern = /^ssh-rsa\s+([A-Za-z0-9+/=]+)\s*(.*)?\s*$/;
|
||||||
|
const ed25519PubPattern = /^ssh-ed25519\s+([A-Za-z0-9+/=]+)\s*(.*)?\s*$/;
|
||||||
|
return rsaPubPattern.test(key) || ed25519PubPattern.test(key);
|
||||||
|
},
|
||||||
|
{
|
||||||
|
message: "Invalid public key format",
|
||||||
|
},
|
||||||
|
),
|
||||||
|
privateKey: z.string().refine(
|
||||||
|
(key) => {
|
||||||
|
const rsaPrivPattern =
|
||||||
|
/^-----BEGIN RSA PRIVATE KEY-----\n([A-Za-z0-9+/=\n]+)-----END RSA PRIVATE KEY-----\s*$/;
|
||||||
|
const ed25519PrivPattern =
|
||||||
|
/^-----BEGIN OPENSSH PRIVATE KEY-----\n([A-Za-z0-9+/=\n]+)-----END OPENSSH PRIVATE KEY-----\s*$/;
|
||||||
|
return rsaPrivPattern.test(key) || ed25519PrivPattern.test(key);
|
||||||
|
},
|
||||||
|
{
|
||||||
|
message: "Invalid private key format",
|
||||||
|
},
|
||||||
|
),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const sshKeyUpdate = sshKeyCreate.pick({
|
||||||
|
name: true,
|
||||||
|
description: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const sshKeyType = z.object({
|
||||||
|
type: z.enum(["rsa", "ed25519"]).optional(),
|
||||||
|
});
|
||||||
138
apps/mig/server/queues/deployments-queue.ts
Normal file
138
apps/mig/server/queues/deployments-queue.ts
Normal file
@@ -0,0 +1,138 @@
|
|||||||
|
import {
|
||||||
|
deployApplication,
|
||||||
|
deployCompose,
|
||||||
|
deployRemoteApplication,
|
||||||
|
deployRemoteCompose,
|
||||||
|
rebuildApplication,
|
||||||
|
rebuildCompose,
|
||||||
|
rebuildRemoteApplication,
|
||||||
|
rebuildRemoteCompose,
|
||||||
|
updateApplicationStatus,
|
||||||
|
updateCompose,
|
||||||
|
} from "@dokploy/server";
|
||||||
|
import { type Job, Worker } from "bullmq";
|
||||||
|
import { myQueue, redisConfig } from "./queueSetup";
|
||||||
|
|
||||||
|
type DeployJob =
|
||||||
|
| {
|
||||||
|
applicationId: string;
|
||||||
|
titleLog: string;
|
||||||
|
descriptionLog: string;
|
||||||
|
server?: boolean;
|
||||||
|
type: "deploy" | "redeploy";
|
||||||
|
applicationType: "application";
|
||||||
|
serverId?: string;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
composeId: string;
|
||||||
|
titleLog: string;
|
||||||
|
descriptionLog: string;
|
||||||
|
server?: boolean;
|
||||||
|
type: "deploy" | "redeploy";
|
||||||
|
applicationType: "compose";
|
||||||
|
serverId?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type DeploymentJob = DeployJob;
|
||||||
|
|
||||||
|
export const deploymentWorker = new Worker(
|
||||||
|
"deployments",
|
||||||
|
async (job: Job<DeploymentJob>) => {
|
||||||
|
try {
|
||||||
|
if (job.data.applicationType === "application") {
|
||||||
|
await updateApplicationStatus(job.data.applicationId, "running");
|
||||||
|
if (job.data.server) {
|
||||||
|
if (job.data.type === "redeploy") {
|
||||||
|
await rebuildRemoteApplication({
|
||||||
|
applicationId: job.data.applicationId,
|
||||||
|
titleLog: job.data.titleLog,
|
||||||
|
descriptionLog: job.data.descriptionLog,
|
||||||
|
});
|
||||||
|
} else if (job.data.type === "deploy") {
|
||||||
|
await deployRemoteApplication({
|
||||||
|
applicationId: job.data.applicationId,
|
||||||
|
titleLog: job.data.titleLog,
|
||||||
|
descriptionLog: job.data.descriptionLog,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (job.data.type === "redeploy") {
|
||||||
|
await rebuildApplication({
|
||||||
|
applicationId: job.data.applicationId,
|
||||||
|
titleLog: job.data.titleLog,
|
||||||
|
descriptionLog: job.data.descriptionLog,
|
||||||
|
});
|
||||||
|
} else if (job.data.type === "deploy") {
|
||||||
|
await deployApplication({
|
||||||
|
applicationId: job.data.applicationId,
|
||||||
|
titleLog: job.data.titleLog,
|
||||||
|
descriptionLog: job.data.descriptionLog,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (job.data.applicationType === "compose") {
|
||||||
|
await updateCompose(job.data.composeId, {
|
||||||
|
composeStatus: "running",
|
||||||
|
});
|
||||||
|
|
||||||
|
if (job.data.server) {
|
||||||
|
if (job.data.type === "redeploy") {
|
||||||
|
await rebuildRemoteCompose({
|
||||||
|
composeId: job.data.composeId,
|
||||||
|
titleLog: job.data.titleLog,
|
||||||
|
descriptionLog: job.data.descriptionLog,
|
||||||
|
});
|
||||||
|
} else if (job.data.type === "deploy") {
|
||||||
|
await deployRemoteCompose({
|
||||||
|
composeId: job.data.composeId,
|
||||||
|
titleLog: job.data.titleLog,
|
||||||
|
descriptionLog: job.data.descriptionLog,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (job.data.type === "deploy") {
|
||||||
|
await deployCompose({
|
||||||
|
composeId: job.data.composeId,
|
||||||
|
titleLog: job.data.titleLog,
|
||||||
|
descriptionLog: job.data.descriptionLog,
|
||||||
|
});
|
||||||
|
} else if (job.data.type === "redeploy") {
|
||||||
|
await rebuildCompose({
|
||||||
|
composeId: job.data.composeId,
|
||||||
|
titleLog: job.data.titleLog,
|
||||||
|
descriptionLog: job.data.descriptionLog,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.log("Error", error);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
autorun: false,
|
||||||
|
connection: redisConfig,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
export const cleanQueuesByApplication = async (applicationId: string) => {
|
||||||
|
const jobs = await myQueue.getJobs(["waiting", "delayed"]);
|
||||||
|
|
||||||
|
for (const job of jobs) {
|
||||||
|
if (job?.data?.applicationId === applicationId) {
|
||||||
|
await job.remove();
|
||||||
|
console.log(`Removed job ${job.id} for application ${applicationId}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const cleanQueuesByCompose = async (composeId: string) => {
|
||||||
|
const jobs = await myQueue.getJobs(["waiting", "delayed"]);
|
||||||
|
|
||||||
|
for (const job of jobs) {
|
||||||
|
if (job?.data?.composeId === composeId) {
|
||||||
|
await job.remove();
|
||||||
|
console.log(`Removed job ${job.id} for compose ${composeId}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
24
apps/mig/server/queues/queueSetup.ts
Normal file
24
apps/mig/server/queues/queueSetup.ts
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
import { type ConnectionOptions, Queue } from "bullmq";
|
||||||
|
|
||||||
|
export const redisConfig: ConnectionOptions = {
|
||||||
|
host: process.env.NODE_ENV === "production" ? "dokploy-redis" : "127.0.0.1",
|
||||||
|
};
|
||||||
|
const myQueue = new Queue("deployments", {
|
||||||
|
connection: redisConfig,
|
||||||
|
});
|
||||||
|
|
||||||
|
process.on("SIGTERM", () => {
|
||||||
|
myQueue.close();
|
||||||
|
process.exit(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
myQueue.on("error", (error) => {
|
||||||
|
if ((error as any).code === "ECONNREFUSED") {
|
||||||
|
console.error(
|
||||||
|
"Make sure you have installed Redis and it is running.",
|
||||||
|
error,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
export { myQueue };
|
||||||
@@ -15,6 +15,7 @@ import {
|
|||||||
import { createRequestHandler } from "@remix-run/express";
|
import { createRequestHandler } from "@remix-run/express";
|
||||||
import { installGlobals } from "@remix-run/node";
|
import { installGlobals } from "@remix-run/node";
|
||||||
import express from "express";
|
import express from "express";
|
||||||
|
import { migration } from "./db/migration";
|
||||||
|
|
||||||
installGlobals();
|
installGlobals();
|
||||||
|
|
||||||
@@ -70,7 +71,6 @@ app.listen(port, async () => {
|
|||||||
await initializeRedis();
|
await initializeRedis();
|
||||||
|
|
||||||
initCronJobs();
|
initCronJobs();
|
||||||
welcomeServer();
|
|
||||||
|
|
||||||
// Timeout to wait for the database to be ready
|
// Timeout to wait for the database to be ready
|
||||||
await new Promise((resolve) => setTimeout(resolve, 7000));
|
await new Promise((resolve) => setTimeout(resolve, 7000));
|
||||||
@@ -79,28 +79,13 @@ app.listen(port, async () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (IS_CLOUD && process.env.NODE_ENV === "production") {
|
if (IS_CLOUD && process.env.NODE_ENV === "production") {
|
||||||
// await migration();
|
await migration();
|
||||||
}
|
}
|
||||||
|
|
||||||
// server.listen(PORT);
|
console.log("Server Started:", port);
|
||||||
// console.log("Server Started:", PORT);
|
|
||||||
if (!IS_CLOUD) {
|
if (!IS_CLOUD) {
|
||||||
console.log("Starting Deployment Worker");
|
console.log("Starting Deployment Worker");
|
||||||
// const { deploymentWorker } = await import("./queues/deployments-queue");
|
const { deploymentWorker } = await import("./queues/deployments-queue");
|
||||||
// await deploymentWorker.run();
|
await deploymentWorker.run();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
async function welcomeServer() {
|
|
||||||
const ip = await getPublicIpWithFallback();
|
|
||||||
console.log(
|
|
||||||
[
|
|
||||||
"",
|
|
||||||
"",
|
|
||||||
"Dokploy server is up and running!",
|
|
||||||
"Please wait for 15 seconds before opening the browser.",
|
|
||||||
` http://${ip}:${3000}`,
|
|
||||||
"",
|
|
||||||
"",
|
|
||||||
].join("\n"),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|||||||
67
apps/mig/server/utils/backup.ts
Normal file
67
apps/mig/server/utils/backup.ts
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
type QueueJob =
|
||||||
|
| {
|
||||||
|
type: "backup";
|
||||||
|
cronSchedule: string;
|
||||||
|
backupId: string;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
type: "server";
|
||||||
|
cronSchedule: string;
|
||||||
|
serverId: string;
|
||||||
|
};
|
||||||
|
export const schedule = async (job: QueueJob) => {
|
||||||
|
try {
|
||||||
|
const result = await fetch(`${process.env.JOBS_URL}/create-backup`, {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
"X-API-Key": process.env.API_KEY || "NO-DEFINED",
|
||||||
|
},
|
||||||
|
body: JSON.stringify(job),
|
||||||
|
});
|
||||||
|
const data = await result.json();
|
||||||
|
console.log(data);
|
||||||
|
return data;
|
||||||
|
} catch (error) {
|
||||||
|
console.log(error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const removeJob = async (job: QueueJob) => {
|
||||||
|
try {
|
||||||
|
const result = await fetch(`${process.env.JOBS_URL}/remove-job`, {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
"X-API-Key": process.env.API_KEY || "NO-DEFINED",
|
||||||
|
},
|
||||||
|
body: JSON.stringify(job),
|
||||||
|
});
|
||||||
|
const data = await result.json();
|
||||||
|
console.log(data);
|
||||||
|
return data;
|
||||||
|
} catch (error) {
|
||||||
|
console.log(error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const updateJob = async (job: QueueJob) => {
|
||||||
|
try {
|
||||||
|
const result = await fetch(`${process.env.JOBS_URL}/update-backup`, {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
"X-API-Key": process.env.API_KEY || "NO-DEFINED",
|
||||||
|
},
|
||||||
|
body: JSON.stringify(job),
|
||||||
|
});
|
||||||
|
const data = await result.json();
|
||||||
|
console.log(data);
|
||||||
|
return data;
|
||||||
|
} catch (error) {
|
||||||
|
console.log(error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
};
|
||||||
25
apps/mig/server/utils/deploy.ts
Normal file
25
apps/mig/server/utils/deploy.ts
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
import { findServerById } from "@dokploy/server";
|
||||||
|
import type { DeploymentJob } from "../queues/deployments-queue";
|
||||||
|
|
||||||
|
export const deploy = async (jobData: DeploymentJob) => {
|
||||||
|
try {
|
||||||
|
const server = await findServerById(jobData.serverId as string);
|
||||||
|
if (server.serverStatus === "inactive") {
|
||||||
|
throw new Error("Server is inactive");
|
||||||
|
}
|
||||||
|
const result = await fetch(`${process.env.SERVER_URL}/deploy`, {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
"X-API-Key": process.env.API_KEY || "NO-DEFINED",
|
||||||
|
},
|
||||||
|
body: JSON.stringify(jobData),
|
||||||
|
});
|
||||||
|
const data = await result.json();
|
||||||
|
console.log(data);
|
||||||
|
return data;
|
||||||
|
} catch (error) {
|
||||||
|
console.log(error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
};
|
||||||
27
apps/mig/server/utils/stripe.ts
Normal file
27
apps/mig/server/utils/stripe.ts
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
export const WEBSITE_URL =
|
||||||
|
process.env.NODE_ENV === "development"
|
||||||
|
? "http://localhost:3000"
|
||||||
|
: "https://app.dokploy.com";
|
||||||
|
|
||||||
|
const BASE_PRICE_MONTHLY_ID = process.env.BASE_PRICE_MONTHLY_ID || ""; // $4.00
|
||||||
|
|
||||||
|
const BASE_ANNUAL_MONTHLY_ID = process.env.BASE_ANNUAL_MONTHLY_ID || ""; // $7.99
|
||||||
|
|
||||||
|
export const getStripeItems = (serverQuantity: number, isAnnual: boolean) => {
|
||||||
|
const items = [];
|
||||||
|
|
||||||
|
if (isAnnual) {
|
||||||
|
items.push({
|
||||||
|
price: BASE_ANNUAL_MONTHLY_ID,
|
||||||
|
quantity: serverQuantity,
|
||||||
|
});
|
||||||
|
|
||||||
|
return items;
|
||||||
|
}
|
||||||
|
items.push({
|
||||||
|
price: BASE_PRICE_MONTHLY_ID,
|
||||||
|
quantity: serverQuantity,
|
||||||
|
});
|
||||||
|
|
||||||
|
return items;
|
||||||
|
};
|
||||||
141
apps/mig/server/wss/docker-container-logs.ts
Normal file
141
apps/mig/server/wss/docker-container-logs.ts
Normal file
@@ -0,0 +1,141 @@
|
|||||||
|
import type http from "node:http";
|
||||||
|
import { findServerById, validateWebSocketRequest } from "@dokploy/server";
|
||||||
|
import { spawn } from "node-pty";
|
||||||
|
import { Client } from "ssh2";
|
||||||
|
import { WebSocketServer } from "ws";
|
||||||
|
import { getShell } from "./utils";
|
||||||
|
|
||||||
|
export const setupDockerContainerLogsWebSocketServer = (
|
||||||
|
server: http.Server<typeof http.IncomingMessage, typeof http.ServerResponse>,
|
||||||
|
) => {
|
||||||
|
const wssTerm = new WebSocketServer({
|
||||||
|
noServer: true,
|
||||||
|
path: "/docker-container-logs",
|
||||||
|
});
|
||||||
|
|
||||||
|
server.on("upgrade", (req, socket, head) => {
|
||||||
|
const { pathname } = new URL(req.url || "", `http://${req.headers.host}`);
|
||||||
|
|
||||||
|
if (pathname === "/_next/webpack-hmr") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (pathname === "/docker-container-logs") {
|
||||||
|
wssTerm.handleUpgrade(req, socket, head, function done(ws) {
|
||||||
|
wssTerm.emit("connection", ws, req);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-misused-promises
|
||||||
|
wssTerm.on("connection", async (ws, req) => {
|
||||||
|
const url = new URL(req.url || "", `http://${req.headers.host}`);
|
||||||
|
const containerId = url.searchParams.get("containerId");
|
||||||
|
const tail = url.searchParams.get("tail");
|
||||||
|
const serverId = url.searchParams.get("serverId");
|
||||||
|
const { user, session } = await validateWebSocketRequest(req);
|
||||||
|
|
||||||
|
if (!containerId) {
|
||||||
|
ws.close(4000, "containerId no provided");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!user || !session) {
|
||||||
|
ws.close();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
if (serverId) {
|
||||||
|
const server = await findServerById(serverId);
|
||||||
|
|
||||||
|
if (!server.sshKeyId) return;
|
||||||
|
const client = new Client();
|
||||||
|
client
|
||||||
|
.once("ready", () => {
|
||||||
|
const command = `
|
||||||
|
bash -c "docker container logs --tail ${tail} --follow ${containerId}"
|
||||||
|
`;
|
||||||
|
client.exec(command, (err, stream) => {
|
||||||
|
if (err) {
|
||||||
|
console.error("Execution error:", err);
|
||||||
|
ws.close();
|
||||||
|
client.end();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
stream
|
||||||
|
.on("close", () => {
|
||||||
|
console.log("Connection closed ✅ Container Logs");
|
||||||
|
client.end();
|
||||||
|
ws.close();
|
||||||
|
})
|
||||||
|
.on("data", (data: string) => {
|
||||||
|
ws.send(data.toString());
|
||||||
|
})
|
||||||
|
.stderr.on("data", (data) => {
|
||||||
|
ws.send(data.toString());
|
||||||
|
});
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.on("error", (err) => {
|
||||||
|
console.error("SSH connection error:", err);
|
||||||
|
ws.send(`SSH error: ${err.message}`);
|
||||||
|
ws.close(); // Cierra el WebSocket si hay un error con SSH
|
||||||
|
client.end();
|
||||||
|
})
|
||||||
|
.connect({
|
||||||
|
host: server.ipAddress,
|
||||||
|
port: server.port,
|
||||||
|
username: server.username,
|
||||||
|
privateKey: server.sshKey?.privateKey,
|
||||||
|
});
|
||||||
|
ws.on("close", () => {
|
||||||
|
console.log("Connection closed ✅, From Container Logs WS");
|
||||||
|
client.end();
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
const shell = getShell();
|
||||||
|
const ptyProcess = spawn(
|
||||||
|
shell,
|
||||||
|
[
|
||||||
|
"-c",
|
||||||
|
`docker container logs --tail ${tail} --follow ${containerId}`,
|
||||||
|
],
|
||||||
|
{
|
||||||
|
name: "xterm-256color",
|
||||||
|
cwd: process.env.HOME,
|
||||||
|
env: process.env,
|
||||||
|
encoding: "utf8",
|
||||||
|
cols: 80,
|
||||||
|
rows: 30,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
ptyProcess.onData((data) => {
|
||||||
|
ws.send(data);
|
||||||
|
});
|
||||||
|
ws.on("close", () => {
|
||||||
|
ptyProcess.kill();
|
||||||
|
});
|
||||||
|
ws.on("message", (message) => {
|
||||||
|
try {
|
||||||
|
let command: string | Buffer[] | Buffer | ArrayBuffer;
|
||||||
|
if (Buffer.isBuffer(message)) {
|
||||||
|
command = message.toString("utf8");
|
||||||
|
} else {
|
||||||
|
command = message;
|
||||||
|
}
|
||||||
|
ptyProcess.write(command.toString());
|
||||||
|
} catch (error) {
|
||||||
|
// @ts-ignore
|
||||||
|
const errorMessage = error?.message as unknown as string;
|
||||||
|
ws.send(errorMessage);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
// @ts-ignore
|
||||||
|
const errorMessage = error?.message as unknown as string;
|
||||||
|
|
||||||
|
ws.send(errorMessage);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
154
apps/mig/server/wss/docker-container-terminal.ts
Normal file
154
apps/mig/server/wss/docker-container-terminal.ts
Normal file
@@ -0,0 +1,154 @@
|
|||||||
|
import type http from "node:http";
|
||||||
|
import { findServerById, validateWebSocketRequest } from "@dokploy/server";
|
||||||
|
import { spawn } from "node-pty";
|
||||||
|
import { Client } from "ssh2";
|
||||||
|
import { WebSocketServer } from "ws";
|
||||||
|
import { getShell } from "./utils";
|
||||||
|
|
||||||
|
export const setupDockerContainerTerminalWebSocketServer = (
|
||||||
|
server: http.Server<typeof http.IncomingMessage, typeof http.ServerResponse>,
|
||||||
|
) => {
|
||||||
|
const wssTerm = new WebSocketServer({
|
||||||
|
noServer: true,
|
||||||
|
path: "/docker-container-terminal",
|
||||||
|
});
|
||||||
|
|
||||||
|
server.on("upgrade", (req, socket, head) => {
|
||||||
|
const { pathname } = new URL(req.url || "", `http://${req.headers.host}`);
|
||||||
|
|
||||||
|
if (pathname === "/_next/webpack-hmr") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (pathname === "/docker-container-terminal") {
|
||||||
|
wssTerm.handleUpgrade(req, socket, head, function done(ws) {
|
||||||
|
wssTerm.emit("connection", ws, req);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-misused-promises
|
||||||
|
wssTerm.on("connection", async (ws, req) => {
|
||||||
|
const url = new URL(req.url || "", `http://${req.headers.host}`);
|
||||||
|
const containerId = url.searchParams.get("containerId");
|
||||||
|
const activeWay = url.searchParams.get("activeWay");
|
||||||
|
const serverId = url.searchParams.get("serverId");
|
||||||
|
const { user, session } = await validateWebSocketRequest(req);
|
||||||
|
|
||||||
|
if (!containerId) {
|
||||||
|
ws.close(4000, "containerId no provided");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!user || !session) {
|
||||||
|
ws.close();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
if (serverId) {
|
||||||
|
const server = await findServerById(serverId);
|
||||||
|
if (!server.sshKeyId)
|
||||||
|
throw new Error("No SSH key available for this server");
|
||||||
|
|
||||||
|
const conn = new Client();
|
||||||
|
let stdout = "";
|
||||||
|
let stderr = "";
|
||||||
|
conn
|
||||||
|
.once("ready", () => {
|
||||||
|
conn.exec(
|
||||||
|
`docker exec -it ${containerId} ${activeWay}`,
|
||||||
|
{ pty: true },
|
||||||
|
(err, stream) => {
|
||||||
|
if (err) throw err;
|
||||||
|
|
||||||
|
stream
|
||||||
|
.on("close", (code: number, signal: string) => {
|
||||||
|
console.log(
|
||||||
|
`Stream :: close :: code: ${code}, signal: ${signal}`,
|
||||||
|
);
|
||||||
|
ws.send(`\nContainer closed with code: ${code}\n`);
|
||||||
|
conn.end();
|
||||||
|
})
|
||||||
|
.on("data", (data: string) => {
|
||||||
|
stdout += data.toString();
|
||||||
|
ws.send(data.toString());
|
||||||
|
})
|
||||||
|
.stderr.on("data", (data) => {
|
||||||
|
stderr += data.toString();
|
||||||
|
ws.send(data.toString());
|
||||||
|
console.error("Error: ", data.toString());
|
||||||
|
});
|
||||||
|
|
||||||
|
ws.on("message", (message) => {
|
||||||
|
try {
|
||||||
|
let command: string | Buffer[] | Buffer | ArrayBuffer;
|
||||||
|
if (Buffer.isBuffer(message)) {
|
||||||
|
command = message.toString("utf8");
|
||||||
|
} else {
|
||||||
|
command = message;
|
||||||
|
}
|
||||||
|
stream.write(command.toString());
|
||||||
|
} catch (error) {
|
||||||
|
// @ts-ignore
|
||||||
|
const errorMessage = error?.message as unknown as string;
|
||||||
|
ws.send(errorMessage);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
ws.on("close", () => {
|
||||||
|
stream.end();
|
||||||
|
});
|
||||||
|
},
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.connect({
|
||||||
|
host: server.ipAddress,
|
||||||
|
port: server.port,
|
||||||
|
username: server.username,
|
||||||
|
privateKey: server.sshKey?.privateKey,
|
||||||
|
timeout: 99999,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
const shell = getShell();
|
||||||
|
const ptyProcess = spawn(
|
||||||
|
shell,
|
||||||
|
["-c", `docker exec -it ${containerId} ${activeWay}`],
|
||||||
|
{
|
||||||
|
name: "xterm-256color",
|
||||||
|
cwd: process.env.HOME,
|
||||||
|
env: process.env,
|
||||||
|
encoding: "utf8",
|
||||||
|
cols: 80,
|
||||||
|
rows: 30,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
ptyProcess.onData((data) => {
|
||||||
|
ws.send(data);
|
||||||
|
});
|
||||||
|
ws.on("close", () => {
|
||||||
|
ptyProcess.kill();
|
||||||
|
});
|
||||||
|
ws.on("message", (message) => {
|
||||||
|
try {
|
||||||
|
let command: string | Buffer[] | Buffer | ArrayBuffer;
|
||||||
|
if (Buffer.isBuffer(message)) {
|
||||||
|
command = message.toString("utf8");
|
||||||
|
} else {
|
||||||
|
command = message;
|
||||||
|
}
|
||||||
|
ptyProcess.write(command.toString());
|
||||||
|
} catch (error) {
|
||||||
|
// @ts-ignore
|
||||||
|
const errorMessage = error?.message as unknown as string;
|
||||||
|
ws.send(errorMessage);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
// @ts-ignore
|
||||||
|
const errorMessage = error?.message as unknown as string;
|
||||||
|
|
||||||
|
ws.send(errorMessage);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
96
apps/mig/server/wss/docker-stats.ts
Normal file
96
apps/mig/server/wss/docker-stats.ts
Normal file
@@ -0,0 +1,96 @@
|
|||||||
|
import type http from "node:http";
|
||||||
|
import {
|
||||||
|
docker,
|
||||||
|
getLastAdvancedStatsFile,
|
||||||
|
recordAdvancedStats,
|
||||||
|
validateWebSocketRequest,
|
||||||
|
} from "@dokploy/server";
|
||||||
|
import { WebSocketServer } from "ws";
|
||||||
|
|
||||||
|
export const setupDockerStatsMonitoringSocketServer = (
|
||||||
|
server: http.Server<typeof http.IncomingMessage, typeof http.ServerResponse>,
|
||||||
|
) => {
|
||||||
|
const wssTerm = new WebSocketServer({
|
||||||
|
noServer: true,
|
||||||
|
path: "/listen-docker-stats-monitoring",
|
||||||
|
});
|
||||||
|
|
||||||
|
server.on("upgrade", (req, socket, head) => {
|
||||||
|
const { pathname } = new URL(req.url || "", `http://${req.headers.host}`);
|
||||||
|
|
||||||
|
if (pathname === "/_next/webpack-hmr") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (pathname === "/listen-docker-stats-monitoring") {
|
||||||
|
wssTerm.handleUpgrade(req, socket, head, function done(ws) {
|
||||||
|
wssTerm.emit("connection", ws, req);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
wssTerm.on("connection", async (ws, req) => {
|
||||||
|
const url = new URL(req.url || "", `http://${req.headers.host}`);
|
||||||
|
const appName = url.searchParams.get("appName");
|
||||||
|
const appType = (url.searchParams.get("appType") || "application") as
|
||||||
|
| "application"
|
||||||
|
| "stack"
|
||||||
|
| "docker-compose";
|
||||||
|
const { user, session } = await validateWebSocketRequest(req);
|
||||||
|
|
||||||
|
if (!appName) {
|
||||||
|
ws.close(4000, "appName no provided");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!user || !session) {
|
||||||
|
ws.close();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const intervalId = setInterval(async () => {
|
||||||
|
try {
|
||||||
|
const filter = {
|
||||||
|
status: ["running"],
|
||||||
|
...(appType === "application" && {
|
||||||
|
label: [`com.docker.swarm.service.name=${appName}`],
|
||||||
|
}),
|
||||||
|
...(appType === "stack" && {
|
||||||
|
label: [`com.docker.swarm.task.name=${appName}`],
|
||||||
|
}),
|
||||||
|
...(appType === "docker-compose" && {
|
||||||
|
name: [appName],
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
|
const containers = await docker.listContainers({
|
||||||
|
filters: JSON.stringify(filter),
|
||||||
|
});
|
||||||
|
|
||||||
|
const container = containers[0];
|
||||||
|
if (!container || container?.State !== "running") {
|
||||||
|
ws.close(4000, "Container not running");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const stats = await docker.getContainer(container.Id).stats({
|
||||||
|
stream: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
await recordAdvancedStats(stats, appName);
|
||||||
|
const data = await getLastAdvancedStatsFile(appName);
|
||||||
|
|
||||||
|
ws.send(
|
||||||
|
JSON.stringify({
|
||||||
|
data,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
// @ts-ignore
|
||||||
|
ws.close(4000, `Error: ${error.message}`);
|
||||||
|
}
|
||||||
|
}, 1300);
|
||||||
|
|
||||||
|
ws.on("close", () => {
|
||||||
|
clearInterval(intervalId);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
112
apps/mig/server/wss/listen-deployment.ts
Normal file
112
apps/mig/server/wss/listen-deployment.ts
Normal file
@@ -0,0 +1,112 @@
|
|||||||
|
import { spawn } from "node:child_process";
|
||||||
|
import type http from "node:http";
|
||||||
|
import { findServerById, validateWebSocketRequest } from "@dokploy/server";
|
||||||
|
import { Client } from "ssh2";
|
||||||
|
import { WebSocketServer } from "ws";
|
||||||
|
|
||||||
|
export const setupDeploymentLogsWebSocketServer = (
|
||||||
|
server: http.Server<typeof http.IncomingMessage, typeof http.ServerResponse>,
|
||||||
|
) => {
|
||||||
|
const wssTerm = new WebSocketServer({
|
||||||
|
noServer: true,
|
||||||
|
path: "/listen-deployment",
|
||||||
|
});
|
||||||
|
|
||||||
|
server.on("upgrade", (req, socket, head) => {
|
||||||
|
const { pathname } = new URL(req.url || "", `http://${req.headers.host}`);
|
||||||
|
|
||||||
|
if (pathname === "/_next/webpack-hmr") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (pathname === "/listen-deployment") {
|
||||||
|
wssTerm.handleUpgrade(req, socket, head, function done(ws) {
|
||||||
|
wssTerm.emit("connection", ws, req);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
wssTerm.on("connection", async (ws, req) => {
|
||||||
|
const url = new URL(req.url || "", `http://${req.headers.host}`);
|
||||||
|
const logPath = url.searchParams.get("logPath");
|
||||||
|
const serverId = url.searchParams.get("serverId");
|
||||||
|
const { user, session } = await validateWebSocketRequest(req);
|
||||||
|
|
||||||
|
if (!logPath) {
|
||||||
|
console.log("logPath no provided");
|
||||||
|
ws.close(4000, "logPath no provided");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!user || !session) {
|
||||||
|
ws.close();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (serverId) {
|
||||||
|
const server = await findServerById(serverId);
|
||||||
|
|
||||||
|
if (!server.sshKeyId) return;
|
||||||
|
const client = new Client();
|
||||||
|
client
|
||||||
|
.on("ready", () => {
|
||||||
|
const command = `
|
||||||
|
tail -n +1 -f ${logPath};
|
||||||
|
`;
|
||||||
|
client.exec(command, (err, stream) => {
|
||||||
|
if (err) {
|
||||||
|
console.error("Execution error:", err);
|
||||||
|
ws.close();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
stream
|
||||||
|
.on("close", () => {
|
||||||
|
console.log("Connection closed ✅");
|
||||||
|
client.end();
|
||||||
|
ws.close();
|
||||||
|
})
|
||||||
|
.on("data", (data: string) => {
|
||||||
|
ws.send(data.toString());
|
||||||
|
})
|
||||||
|
.stderr.on("data", (data) => {
|
||||||
|
ws.send(data.toString());
|
||||||
|
});
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.on("error", (err) => {
|
||||||
|
console.error("SSH connection error:", err);
|
||||||
|
ws.send(`SSH error: ${err.message}`);
|
||||||
|
ws.close(); // Cierra el WebSocket si hay un error con SSH
|
||||||
|
})
|
||||||
|
.connect({
|
||||||
|
host: server.ipAddress,
|
||||||
|
port: server.port,
|
||||||
|
username: server.username,
|
||||||
|
privateKey: server.sshKey?.privateKey,
|
||||||
|
});
|
||||||
|
|
||||||
|
ws.on("close", () => {
|
||||||
|
console.log("Connection closed ✅, From WS");
|
||||||
|
client.end();
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
const tail = spawn("tail", ["-n", "+1", "-f", logPath]);
|
||||||
|
|
||||||
|
tail.stdout.on("data", (data) => {
|
||||||
|
ws.send(data.toString());
|
||||||
|
});
|
||||||
|
|
||||||
|
tail.stderr.on("data", (data) => {
|
||||||
|
ws.send(new Error(`tail error: ${data.toString()}`).message);
|
||||||
|
});
|
||||||
|
tail.on("close", () => {
|
||||||
|
ws.close();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
// @ts-ignore
|
||||||
|
// const errorMessage = error?.message as unknown as string;
|
||||||
|
// ws.send(errorMessage);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
140
apps/mig/server/wss/terminal.ts
Normal file
140
apps/mig/server/wss/terminal.ts
Normal file
@@ -0,0 +1,140 @@
|
|||||||
|
import type http from "node:http";
|
||||||
|
import { findServerById, validateWebSocketRequest } from "@dokploy/server";
|
||||||
|
import { publicIpv4, publicIpv6 } from "public-ip";
|
||||||
|
import { Client } from "ssh2";
|
||||||
|
import { WebSocketServer } from "ws";
|
||||||
|
|
||||||
|
export const getPublicIpWithFallback = async () => {
|
||||||
|
// @ts-ignore
|
||||||
|
let ip = null;
|
||||||
|
try {
|
||||||
|
ip = await publicIpv4();
|
||||||
|
} catch (error) {
|
||||||
|
console.log(
|
||||||
|
"Error to obtain public IPv4 address, falling back to IPv6",
|
||||||
|
// @ts-ignore
|
||||||
|
error.message,
|
||||||
|
);
|
||||||
|
try {
|
||||||
|
ip = await publicIpv6();
|
||||||
|
} catch (error) {
|
||||||
|
// @ts-ignore
|
||||||
|
console.error("Error to obtain public IPv6 address", error.message);
|
||||||
|
ip = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ip;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const setupTerminalWebSocketServer = (
|
||||||
|
server: http.Server<typeof http.IncomingMessage, typeof http.ServerResponse>,
|
||||||
|
) => {
|
||||||
|
const wssTerm = new WebSocketServer({
|
||||||
|
noServer: true,
|
||||||
|
path: "/terminal",
|
||||||
|
});
|
||||||
|
|
||||||
|
server.on("upgrade", (req, socket, head) => {
|
||||||
|
const { pathname } = new URL(req.url || "", `http://${req.headers.host}`);
|
||||||
|
if (pathname === "/_next/webpack-hmr") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (pathname === "/terminal") {
|
||||||
|
wssTerm.handleUpgrade(req, socket, head, function done(ws) {
|
||||||
|
wssTerm.emit("connection", ws, req);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
wssTerm.on("connection", async (ws, req) => {
|
||||||
|
const url = new URL(req.url || "", `http://${req.headers.host}`);
|
||||||
|
const serverId = url.searchParams.get("serverId");
|
||||||
|
const { user, session } = await validateWebSocketRequest(req);
|
||||||
|
if (!user || !session || !serverId) {
|
||||||
|
ws.close();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const server = await findServerById(serverId);
|
||||||
|
|
||||||
|
if (!server) {
|
||||||
|
ws.close();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!server.sshKeyId)
|
||||||
|
throw new Error("No SSH key available for this server");
|
||||||
|
|
||||||
|
const conn = new Client();
|
||||||
|
let stdout = "";
|
||||||
|
let stderr = "";
|
||||||
|
conn
|
||||||
|
.once("ready", () => {
|
||||||
|
conn.shell(
|
||||||
|
{
|
||||||
|
term: "terminal",
|
||||||
|
cols: 80,
|
||||||
|
rows: 30,
|
||||||
|
height: 30,
|
||||||
|
width: 80,
|
||||||
|
},
|
||||||
|
(err, stream) => {
|
||||||
|
if (err) throw err;
|
||||||
|
|
||||||
|
stream
|
||||||
|
.on("close", (code: number, signal: string) => {
|
||||||
|
ws.send(`\nContainer closed with code: ${code}\n`);
|
||||||
|
conn.end();
|
||||||
|
})
|
||||||
|
.on("data", (data: string) => {
|
||||||
|
stdout += data.toString();
|
||||||
|
ws.send(data.toString());
|
||||||
|
})
|
||||||
|
.stderr.on("data", (data) => {
|
||||||
|
stderr += data.toString();
|
||||||
|
ws.send(data.toString());
|
||||||
|
console.error("Error: ", data.toString());
|
||||||
|
});
|
||||||
|
|
||||||
|
ws.on("message", (message) => {
|
||||||
|
try {
|
||||||
|
let command: string | Buffer[] | Buffer | ArrayBuffer;
|
||||||
|
if (Buffer.isBuffer(message)) {
|
||||||
|
command = message.toString("utf8");
|
||||||
|
} else {
|
||||||
|
command = message;
|
||||||
|
}
|
||||||
|
stream.write(command.toString());
|
||||||
|
} catch (error) {
|
||||||
|
// @ts-ignore
|
||||||
|
const errorMessage = error?.message as unknown as string;
|
||||||
|
ws.send(errorMessage);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
ws.on("close", () => {
|
||||||
|
console.log("Connection closed ✅");
|
||||||
|
stream.end();
|
||||||
|
});
|
||||||
|
},
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.on("error", (err) => {
|
||||||
|
if (err.level === "client-authentication") {
|
||||||
|
ws.send(
|
||||||
|
`Authentication failed: Invalid SSH private key. ❌ Error: ${err.message} ${err.level}`,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
ws.send(`SSH connection error: ${err.message}`);
|
||||||
|
}
|
||||||
|
conn.end();
|
||||||
|
})
|
||||||
|
.connect({
|
||||||
|
host: server.ipAddress,
|
||||||
|
port: server.port,
|
||||||
|
username: server.username,
|
||||||
|
privateKey: server.sshKey?.privateKey,
|
||||||
|
timeout: 99999,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
12
apps/mig/server/wss/utils.ts
Normal file
12
apps/mig/server/wss/utils.ts
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
import os from "node:os";
|
||||||
|
|
||||||
|
export const getShell = () => {
|
||||||
|
switch (os.platform()) {
|
||||||
|
case "win32":
|
||||||
|
return "powershell.exe";
|
||||||
|
case "darwin":
|
||||||
|
return "zsh";
|
||||||
|
default:
|
||||||
|
return "bash";
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -24,7 +24,8 @@
|
|||||||
"baseUrl": ".",
|
"baseUrl": ".",
|
||||||
"paths": {
|
"paths": {
|
||||||
"~/*": ["./app/*"],
|
"~/*": ["./app/*"],
|
||||||
"@dokploy/server/*": ["../../packages/server/src/*"]
|
"@dokploy/server/*": ["../../packages/server/src/*"],
|
||||||
|
"@/server/*": ["./server/*"]
|
||||||
},
|
},
|
||||||
|
|
||||||
// Vite takes care of building everything, not tsc.
|
// Vite takes care of building everything, not tsc.
|
||||||
|
|||||||
13
pnpm-lock.yaml
generated
13
pnpm-lock.yaml
generated
@@ -495,6 +495,9 @@ importers:
|
|||||||
boxen:
|
boxen:
|
||||||
specifier: ^7.1.1
|
specifier: ^7.1.1
|
||||||
version: 7.1.1
|
version: 7.1.1
|
||||||
|
bullmq:
|
||||||
|
specifier: 5.4.2
|
||||||
|
version: 5.4.2
|
||||||
cross-env:
|
cross-env:
|
||||||
specifier: 7.0.3
|
specifier: 7.0.3
|
||||||
version: 7.0.3
|
version: 7.0.3
|
||||||
@@ -14911,7 +14914,7 @@ snapshots:
|
|||||||
lodash: 4.17.21
|
lodash: 4.17.21
|
||||||
msgpackr: 1.11.0
|
msgpackr: 1.11.0
|
||||||
node-abort-controller: 3.1.1
|
node-abort-controller: 3.1.1
|
||||||
semver: 7.6.2
|
semver: 7.6.3
|
||||||
tslib: 2.6.3
|
tslib: 2.6.3
|
||||||
uuid: 9.0.1
|
uuid: 9.0.1
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
@@ -16007,7 +16010,7 @@ snapshots:
|
|||||||
eslint: 8.45.0
|
eslint: 8.45.0
|
||||||
eslint-import-resolver-node: 0.3.9
|
eslint-import-resolver-node: 0.3.9
|
||||||
eslint-import-resolver-typescript: 3.6.1(@typescript-eslint/parser@6.21.0(eslint@8.45.0)(typescript@5.1.6))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1(eslint@8.45.0))(eslint@8.45.0)
|
eslint-import-resolver-typescript: 3.6.1(@typescript-eslint/parser@6.21.0(eslint@8.45.0)(typescript@5.1.6))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1(eslint@8.45.0))(eslint@8.45.0)
|
||||||
eslint-plugin-import: 2.29.1(@typescript-eslint/parser@6.21.0(eslint@8.45.0)(typescript@5.1.6))(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@6.21.0(eslint@8.45.0)(typescript@5.1.6))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1(eslint@8.45.0))(eslint@8.45.0))(eslint@8.45.0)
|
eslint-plugin-import: 2.29.1(@typescript-eslint/parser@6.21.0(eslint@8.45.0)(typescript@5.1.6))(eslint-import-resolver-typescript@3.6.1)(eslint@8.45.0)
|
||||||
eslint-plugin-jsx-a11y: 6.9.0(eslint@8.45.0)
|
eslint-plugin-jsx-a11y: 6.9.0(eslint@8.45.0)
|
||||||
eslint-plugin-react: 7.35.0(eslint@8.45.0)
|
eslint-plugin-react: 7.35.0(eslint@8.45.0)
|
||||||
eslint-plugin-react-hooks: 5.0.0-canary-7118f5dd7-20230705(eslint@8.45.0)
|
eslint-plugin-react-hooks: 5.0.0-canary-7118f5dd7-20230705(eslint@8.45.0)
|
||||||
@@ -16031,7 +16034,7 @@ snapshots:
|
|||||||
enhanced-resolve: 5.17.1
|
enhanced-resolve: 5.17.1
|
||||||
eslint: 8.45.0
|
eslint: 8.45.0
|
||||||
eslint-module-utils: 2.8.1(@typescript-eslint/parser@6.21.0(eslint@8.45.0)(typescript@5.1.6))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@6.21.0(eslint@8.45.0)(typescript@5.1.6))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1(eslint@8.45.0))(eslint@8.45.0))(eslint@8.45.0)
|
eslint-module-utils: 2.8.1(@typescript-eslint/parser@6.21.0(eslint@8.45.0)(typescript@5.1.6))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@6.21.0(eslint@8.45.0)(typescript@5.1.6))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1(eslint@8.45.0))(eslint@8.45.0))(eslint@8.45.0)
|
||||||
eslint-plugin-import: 2.29.1(@typescript-eslint/parser@6.21.0(eslint@8.45.0)(typescript@5.1.6))(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@6.21.0(eslint@8.45.0)(typescript@5.1.6))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1(eslint@8.45.0))(eslint@8.45.0))(eslint@8.45.0)
|
eslint-plugin-import: 2.29.1(@typescript-eslint/parser@6.21.0(eslint@8.45.0)(typescript@5.1.6))(eslint-import-resolver-typescript@3.6.1)(eslint@8.45.0)
|
||||||
fast-glob: 3.3.2
|
fast-glob: 3.3.2
|
||||||
get-tsconfig: 4.7.5
|
get-tsconfig: 4.7.5
|
||||||
is-core-module: 2.15.0
|
is-core-module: 2.15.0
|
||||||
@@ -16053,7 +16056,7 @@ snapshots:
|
|||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
eslint-plugin-import@2.29.1(@typescript-eslint/parser@6.21.0(eslint@8.45.0)(typescript@5.1.6))(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@6.21.0(eslint@8.45.0)(typescript@5.1.6))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1(eslint@8.45.0))(eslint@8.45.0))(eslint@8.45.0):
|
eslint-plugin-import@2.29.1(@typescript-eslint/parser@6.21.0(eslint@8.45.0)(typescript@5.1.6))(eslint-import-resolver-typescript@3.6.1)(eslint@8.45.0):
|
||||||
dependencies:
|
dependencies:
|
||||||
array-includes: 3.1.8
|
array-includes: 3.1.8
|
||||||
array.prototype.findlastindex: 1.2.5
|
array.prototype.findlastindex: 1.2.5
|
||||||
@@ -17353,7 +17356,7 @@ snapshots:
|
|||||||
lodash.isstring: 4.0.1
|
lodash.isstring: 4.0.1
|
||||||
lodash.once: 4.1.1
|
lodash.once: 4.1.1
|
||||||
ms: 2.1.3
|
ms: 2.1.3
|
||||||
semver: 7.6.2
|
semver: 7.6.3
|
||||||
|
|
||||||
jsx-ast-utils@3.3.5:
|
jsx-ast-utils@3.3.5:
|
||||||
dependencies:
|
dependencies:
|
||||||
|
|||||||
Reference in New Issue
Block a user