mirror of
https://github.com/Dokploy/dokploy
synced 2025-06-26 18:27:59 +00:00
feat: initial commit
This commit is contained in:
53
server/api/root.ts
Normal file
53
server/api/root.ts
Normal file
@@ -0,0 +1,53 @@
|
||||
import { createTRPCRouter } from "../api/trpc";
|
||||
import { authRouter } from "@/server/api/routers/auth";
|
||||
import { projectRouter } from "./routers/project";
|
||||
import { applicationRouter } from "./routers/application";
|
||||
import { mysqlRouter } from "./routers/mysql";
|
||||
import { postgresRouter } from "./routers/postgres";
|
||||
import { redisRouter } from "./routers/redis";
|
||||
import { mongoRouter } from "./routers/mongo";
|
||||
import { mariadbRouter } from "./routers/mariadb";
|
||||
import { userRouter } from "./routers/user";
|
||||
import { domainRouter } from "./routers/domain";
|
||||
import { destinationRouter } from "./routers/destination";
|
||||
import { backupRouter } from "./routers/backup";
|
||||
import { deploymentRouter } from "./routers/deployment";
|
||||
import { mountRouter } from "./routers/mount";
|
||||
import { certificateRouter } from "./routers/certificate";
|
||||
import { settingsRouter } from "./routers/settings";
|
||||
import { redirectsRouter } from "./routers/redirects";
|
||||
import { securityRouter } from "./routers/security";
|
||||
import { portRouter } from "./routers/port";
|
||||
import { adminRouter } from "./routers/admin";
|
||||
import { dockerRouter } from "./routers/docker";
|
||||
/**
|
||||
* 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,
|
||||
user: userRouter,
|
||||
domain: domainRouter,
|
||||
destination: destinationRouter,
|
||||
backup: backupRouter,
|
||||
deployment: deploymentRouter,
|
||||
mounts: mountRouter,
|
||||
certificates: certificateRouter,
|
||||
settings: settingsRouter,
|
||||
security: securityRouter,
|
||||
redirects: redirectsRouter,
|
||||
port: portRouter,
|
||||
});
|
||||
|
||||
// export type definition of API
|
||||
export type AppRouter = typeof appRouter;
|
||||
166
server/api/routers/admin.ts
Normal file
166
server/api/routers/admin.ts
Normal file
@@ -0,0 +1,166 @@
|
||||
import {
|
||||
apiAssignPermissions,
|
||||
apiCreateUserInvitation,
|
||||
apiFindOneToken,
|
||||
apiGetBranches,
|
||||
apiRemoveUser,
|
||||
users,
|
||||
} from "@/server/db/schema";
|
||||
import {
|
||||
createInvitation,
|
||||
findAdmin,
|
||||
getUserByToken,
|
||||
removeUserByAuthId,
|
||||
updateAdmin,
|
||||
} from "../services/admin";
|
||||
import {
|
||||
adminProcedure,
|
||||
createTRPCRouter,
|
||||
protectedProcedure,
|
||||
publicProcedure,
|
||||
} from "../trpc";
|
||||
import { db } from "@/server/db";
|
||||
import { TRPCError } from "@trpc/server";
|
||||
import { eq } from "drizzle-orm";
|
||||
import { Octokit } from "octokit";
|
||||
import { createAppAuth } from "@octokit/auth-app";
|
||||
import { haveGithubRequirements } from "@/server/utils/providers/github";
|
||||
|
||||
export const adminRouter = createTRPCRouter({
|
||||
one: adminProcedure.query(async () => {
|
||||
const { sshPrivateKey, ...rest } = await findAdmin();
|
||||
return {
|
||||
haveSSH: !!sshPrivateKey,
|
||||
...rest,
|
||||
};
|
||||
}),
|
||||
createUserInvitation: adminProcedure
|
||||
.input(apiCreateUserInvitation)
|
||||
.mutation(async ({ input }) => {
|
||||
try {
|
||||
await createInvitation(input);
|
||||
} 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 }) => {
|
||||
try {
|
||||
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 }) => {
|
||||
try {
|
||||
await db
|
||||
.update(users)
|
||||
.set({
|
||||
...input,
|
||||
})
|
||||
.where(eq(users.userId, input.userId));
|
||||
} catch (error) {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: "Error to assign permissions",
|
||||
});
|
||||
}
|
||||
}),
|
||||
|
||||
cleanGithubApp: adminProcedure.mutation(async ({ ctx }) => {
|
||||
try {
|
||||
return await updateAdmin(ctx.user.authId, {
|
||||
githubAppName: "",
|
||||
githubClientId: "",
|
||||
githubClientSecret: "",
|
||||
githubInstallationId: "",
|
||||
});
|
||||
} catch (error) {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: "Error to delete this github app",
|
||||
cause: error,
|
||||
});
|
||||
}
|
||||
}),
|
||||
|
||||
getRepositories: protectedProcedure.query(async () => {
|
||||
const admin = await findAdmin();
|
||||
|
||||
const completeRequirements = haveGithubRequirements(admin);
|
||||
|
||||
if (!completeRequirements) {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: "Admin need to setup correctly github account",
|
||||
});
|
||||
}
|
||||
|
||||
const octokit = new Octokit({
|
||||
authStrategy: createAppAuth,
|
||||
auth: {
|
||||
appId: admin.githubAppId,
|
||||
privateKey: admin.githubPrivateKey,
|
||||
installationId: admin.githubInstallationId,
|
||||
},
|
||||
});
|
||||
|
||||
const repositories = (await octokit.paginate(
|
||||
octokit.rest.apps.listReposAccessibleToInstallation,
|
||||
)) as unknown as Awaited<
|
||||
ReturnType<typeof octokit.rest.apps.listReposAccessibleToInstallation>
|
||||
>["data"]["repositories"];
|
||||
|
||||
return repositories;
|
||||
}),
|
||||
getBranches: protectedProcedure
|
||||
.input(apiGetBranches)
|
||||
.query(async ({ input }) => {
|
||||
const admin = await findAdmin();
|
||||
|
||||
const completeRequirements = haveGithubRequirements(admin);
|
||||
|
||||
if (!completeRequirements) {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: "Admin need to setup correctly github account",
|
||||
});
|
||||
}
|
||||
|
||||
const octokit = new Octokit({
|
||||
authStrategy: createAppAuth,
|
||||
auth: {
|
||||
appId: admin.githubAppId,
|
||||
privateKey: admin.githubPrivateKey,
|
||||
installationId: admin.githubInstallationId,
|
||||
},
|
||||
});
|
||||
const branches = await octokit.rest.repos.listBranches({
|
||||
owner: input.owner,
|
||||
repo: input.repo,
|
||||
});
|
||||
return branches.data;
|
||||
}),
|
||||
haveGithubConfigured: protectedProcedure.query(async () => {
|
||||
const adminResponse = await findAdmin();
|
||||
|
||||
return haveGithubRequirements(adminResponse);
|
||||
}),
|
||||
});
|
||||
339
server/api/routers/application.ts
Normal file
339
server/api/routers/application.ts
Normal file
@@ -0,0 +1,339 @@
|
||||
import { createTRPCRouter, protectedProcedure } from "@/server/api/trpc";
|
||||
import { db } from "@/server/db";
|
||||
import {
|
||||
apiCreateApplication,
|
||||
apiFindMonitoringStats,
|
||||
apiFindOneApplication,
|
||||
apiReloadApplication,
|
||||
apiSaveBuildType,
|
||||
apiSaveDockerProvider,
|
||||
apiSaveEnviromentVariables,
|
||||
apiSaveGitProvider,
|
||||
apiSaveGithubProvider,
|
||||
apiUpdateApplication,
|
||||
applications,
|
||||
} from "@/server/db/schema/application";
|
||||
import {
|
||||
cleanQueuesByApplication,
|
||||
type DeploymentJob,
|
||||
} from "@/server/queues/deployments-queue";
|
||||
import { myQueue } from "@/server/queues/queueSetup";
|
||||
import {
|
||||
removeService,
|
||||
startService,
|
||||
stopService,
|
||||
} from "@/server/utils/docker/utils";
|
||||
import {
|
||||
removeDirectoryCode,
|
||||
removeMonitoringDirectory,
|
||||
} from "@/server/utils/filesystem/directory";
|
||||
import {
|
||||
generateSSHKey,
|
||||
readRSAFile,
|
||||
removeRSAFiles,
|
||||
} from "@/server/utils/filesystem/ssh";
|
||||
import {
|
||||
readConfig,
|
||||
removeTraefikConfig,
|
||||
writeConfig,
|
||||
} from "@/server/utils/traefik/application";
|
||||
import { TRPCError } from "@trpc/server";
|
||||
import { eq } from "drizzle-orm";
|
||||
import {
|
||||
createApplication,
|
||||
findApplicationById,
|
||||
getApplicationStats,
|
||||
updateApplication,
|
||||
updateApplicationStatus,
|
||||
} from "../services/application";
|
||||
import { removeDeployments } from "../services/deployment";
|
||||
import { deleteAllMiddlewares } from "@/server/utils/traefik/middleware";
|
||||
import { z } from "zod";
|
||||
import { nanoid } from "nanoid";
|
||||
import { addNewService, checkServiceAccess } from "../services/user";
|
||||
|
||||
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");
|
||||
}
|
||||
const newApplication = await createApplication(input);
|
||||
|
||||
if (ctx.user.rol === "user") {
|
||||
await addNewService(ctx.user.authId, newApplication.applicationId);
|
||||
}
|
||||
} catch (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",
|
||||
);
|
||||
}
|
||||
return await findApplicationById(input.applicationId);
|
||||
}),
|
||||
|
||||
reload: protectedProcedure
|
||||
.input(apiReloadApplication)
|
||||
.mutation(async ({ input }) => {
|
||||
await stopService(input.appName);
|
||||
await updateApplicationStatus(input.applicationId, "idle");
|
||||
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);
|
||||
|
||||
const result = await db
|
||||
.delete(applications)
|
||||
.where(eq(applications.applicationId, input.applicationId))
|
||||
.returning();
|
||||
|
||||
const cleanupOperations = [
|
||||
async () => deleteAllMiddlewares(application),
|
||||
async () => await removeDeployments(application),
|
||||
async () => await removeDirectoryCode(application?.appName),
|
||||
async () => await removeMonitoringDirectory(application?.appName),
|
||||
async () => await removeTraefikConfig(application?.appName),
|
||||
async () => await removeService(application?.appName),
|
||||
async () => await removeRSAFiles(application?.appName),
|
||||
];
|
||||
|
||||
for (const operation of cleanupOperations) {
|
||||
try {
|
||||
await operation();
|
||||
} catch (error) {}
|
||||
}
|
||||
|
||||
return result[0];
|
||||
}),
|
||||
|
||||
stop: protectedProcedure
|
||||
.input(apiFindOneApplication)
|
||||
.mutation(async ({ input }) => {
|
||||
const service = await findApplicationById(input.applicationId);
|
||||
await stopService(service.appName);
|
||||
await updateApplicationStatus(input.applicationId, "idle");
|
||||
|
||||
return service;
|
||||
}),
|
||||
|
||||
start: protectedProcedure
|
||||
.input(apiFindOneApplication)
|
||||
.mutation(async ({ input }) => {
|
||||
const service = await findApplicationById(input.applicationId);
|
||||
|
||||
await startService(service.appName);
|
||||
await updateApplicationStatus(input.applicationId, "done");
|
||||
|
||||
return service;
|
||||
}),
|
||||
|
||||
redeploy: protectedProcedure
|
||||
.input(apiFindOneApplication)
|
||||
.mutation(async ({ input }) => {
|
||||
const jobData: DeploymentJob = {
|
||||
applicationId: input.applicationId,
|
||||
titleLog: "Rebuild deployment",
|
||||
type: "redeploy",
|
||||
};
|
||||
await myQueue.add(
|
||||
"deployments",
|
||||
{ ...jobData },
|
||||
{
|
||||
removeOnComplete: true,
|
||||
removeOnFail: true,
|
||||
},
|
||||
);
|
||||
}),
|
||||
saveEnviroment: protectedProcedure
|
||||
.input(apiSaveEnviromentVariables)
|
||||
.mutation(async ({ input }) => {
|
||||
await updateApplication(input.applicationId, {
|
||||
env: input.env,
|
||||
});
|
||||
return true;
|
||||
}),
|
||||
saveBuildType: protectedProcedure
|
||||
.input(apiSaveBuildType)
|
||||
.mutation(async ({ input }) => {
|
||||
await updateApplication(input.applicationId, {
|
||||
buildType: input.buildType,
|
||||
dockerfile: input.dockerfile,
|
||||
});
|
||||
|
||||
return true;
|
||||
}),
|
||||
saveGithubProvider: protectedProcedure
|
||||
.input(apiSaveGithubProvider)
|
||||
.mutation(async ({ input }) => {
|
||||
await updateApplication(input.applicationId, {
|
||||
repository: input.repository,
|
||||
branch: input.branch,
|
||||
sourceType: "github",
|
||||
owner: input.owner,
|
||||
buildPath: input.buildPath,
|
||||
applicationStatus: "idle",
|
||||
});
|
||||
|
||||
return true;
|
||||
}),
|
||||
saveDockerProvider: protectedProcedure
|
||||
.input(apiSaveDockerProvider)
|
||||
.mutation(async ({ input }) => {
|
||||
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 }) => {
|
||||
await updateApplication(input.applicationId, {
|
||||
customGitBranch: input.customGitBranch,
|
||||
customGitBuildPath: input.customGitBuildPath,
|
||||
customGitUrl: input.customGitUrl,
|
||||
sourceType: "git",
|
||||
applicationStatus: "idle",
|
||||
});
|
||||
|
||||
return true;
|
||||
}),
|
||||
generateSSHKey: protectedProcedure
|
||||
.input(apiFindOneApplication)
|
||||
.mutation(async ({ input }) => {
|
||||
const application = await findApplicationById(input.applicationId);
|
||||
try {
|
||||
await generateSSHKey(application.appName);
|
||||
const file = await readRSAFile(application.appName);
|
||||
|
||||
await updateApplication(input.applicationId, {
|
||||
customGitSSHKey: file,
|
||||
});
|
||||
} catch (error) {}
|
||||
|
||||
return true;
|
||||
}),
|
||||
removeSSHKey: protectedProcedure
|
||||
.input(apiFindOneApplication)
|
||||
.mutation(async ({ input }) => {
|
||||
const application = await findApplicationById(input.applicationId);
|
||||
await removeRSAFiles(application.appName);
|
||||
await updateApplication(input.applicationId, {
|
||||
customGitSSHKey: null,
|
||||
});
|
||||
|
||||
return true;
|
||||
}),
|
||||
markRunning: protectedProcedure
|
||||
.input(apiFindOneApplication)
|
||||
.mutation(async ({ input }) => {
|
||||
await updateApplicationStatus(input.applicationId, "running");
|
||||
}),
|
||||
update: protectedProcedure
|
||||
.input(apiUpdateApplication)
|
||||
.mutation(async ({ input }) => {
|
||||
const { applicationId, ...rest } = input;
|
||||
const application = await updateApplication(applicationId, {
|
||||
...rest,
|
||||
});
|
||||
|
||||
if (!application) {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: "Update: Error to update application",
|
||||
});
|
||||
}
|
||||
|
||||
return true;
|
||||
}),
|
||||
refreshToken: protectedProcedure
|
||||
.input(apiFindOneApplication)
|
||||
.mutation(async ({ input }) => {
|
||||
await updateApplication(input.applicationId, {
|
||||
refreshToken: nanoid(),
|
||||
});
|
||||
return true;
|
||||
}),
|
||||
deploy: protectedProcedure
|
||||
.input(apiFindOneApplication)
|
||||
.mutation(async ({ input }) => {
|
||||
const jobData: DeploymentJob = {
|
||||
applicationId: input.applicationId,
|
||||
titleLog: "Manual deployment",
|
||||
type: "deploy",
|
||||
};
|
||||
await myQueue.add(
|
||||
"deployments",
|
||||
{ ...jobData },
|
||||
{
|
||||
removeOnComplete: true,
|
||||
removeOnFail: true,
|
||||
},
|
||||
);
|
||||
}),
|
||||
|
||||
cleanQueues: protectedProcedure
|
||||
.input(apiFindOneApplication)
|
||||
.mutation(async ({ input }) => {
|
||||
await cleanQueuesByApplication(input.applicationId);
|
||||
}),
|
||||
|
||||
readTraefikConfig: protectedProcedure
|
||||
.input(apiFindOneApplication)
|
||||
.query(async ({ input }) => {
|
||||
const application = await findApplicationById(input.applicationId);
|
||||
|
||||
const traefikConfig = readConfig(application.appName);
|
||||
return traefikConfig;
|
||||
}),
|
||||
|
||||
updateTraefikConfig: protectedProcedure
|
||||
.input(z.object({ applicationId: z.string(), traefikConfig: z.string() }))
|
||||
.mutation(async ({ input }) => {
|
||||
const application = await findApplicationById(input.applicationId);
|
||||
writeConfig(application.appName, input.traefikConfig);
|
||||
return true;
|
||||
}),
|
||||
readAppMonitoring: protectedProcedure
|
||||
.input(apiFindMonitoringStats)
|
||||
.query(async ({ input }) => {
|
||||
const stats = await getApplicationStats(input.appName);
|
||||
|
||||
return stats;
|
||||
}),
|
||||
});
|
||||
|
||||
// Paketo Buildpacks: paketobuildpacks/builder-jammy-full Ubuntu 22.04 Jammy Jellyfish full image with buildpacks for Apache HTTPD, Go, Java, Java Native Image, .NET, NGINX, Node.js, PHP, Procfile, Python, and Ruby
|
||||
// Heroku: heroku/builder:22 Heroku-22 (Ubuntu 22.04) base image with buildpacks for Go, Java, Node.js, PHP, Python, Ruby & Scala.
|
||||
// pack build imageName --path ./ --builder paketobuildpacks/builder-jammy-full
|
||||
// pack build prueba-pack --path ./ --builder heroku/builder:22
|
||||
199
server/api/routers/auth.ts
Normal file
199
server/api/routers/auth.ts
Normal file
@@ -0,0 +1,199 @@
|
||||
import { TRPCError } from "@trpc/server";
|
||||
import * as bcrypt from "bcrypt";
|
||||
import {
|
||||
adminProcedure,
|
||||
createTRPCRouter,
|
||||
protectedProcedure,
|
||||
publicProcedure,
|
||||
} from "../trpc";
|
||||
import { lucia, validateRequest } from "@/server/auth/auth";
|
||||
import {
|
||||
apiCreateAdmin,
|
||||
apiCreateUser,
|
||||
apiFindOneAuth,
|
||||
apiUpdateAuthByAdmin,
|
||||
apiLogin,
|
||||
apiUpdateAuth,
|
||||
apiVerify2FA,
|
||||
apiVerifyLogin2FA,
|
||||
} from "@/server/db/schema";
|
||||
import {
|
||||
createAdmin,
|
||||
createUser,
|
||||
findAuthByEmail,
|
||||
findAuthById,
|
||||
generate2FASecret,
|
||||
updateAuthById,
|
||||
verify2FA,
|
||||
} from "../services/auth";
|
||||
|
||||
export const authRouter = createTRPCRouter({
|
||||
createAdmin: publicProcedure
|
||||
.input(apiCreateAdmin)
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
try {
|
||||
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 new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: "Error to create the main admin",
|
||||
cause: error,
|
||||
});
|
||||
}
|
||||
}),
|
||||
createUser: publicProcedure
|
||||
.input(apiCreateUser)
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
try {
|
||||
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;
|
||||
}),
|
||||
|
||||
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 auth;
|
||||
}),
|
||||
disable2FA: protectedProcedure.mutation(async ({ ctx }) => {
|
||||
const auth = await findAuthById(ctx.user.authId);
|
||||
await updateAuthById(auth.id, {
|
||||
is2FAEnabled: false,
|
||||
secret: null,
|
||||
});
|
||||
return auth;
|
||||
}),
|
||||
});
|
||||
152
server/api/routers/backup.ts
Normal file
152
server/api/routers/backup.ts
Normal file
@@ -0,0 +1,152 @@
|
||||
import { createTRPCRouter, protectedProcedure } from "@/server/api/trpc";
|
||||
import {
|
||||
apiCreateBackup,
|
||||
apiFindOneBackup,
|
||||
apiRemoveBackup,
|
||||
apiUpdateBackup,
|
||||
} from "@/server/db/schema";
|
||||
import { runMariadbBackup } from "@/server/utils/backups/mariadb";
|
||||
import { runMongoBackup } from "@/server/utils/backups/mongo";
|
||||
import { runMySqlBackup } from "@/server/utils/backups/mysql";
|
||||
import { runPostgresBackup } from "@/server/utils/backups/postgres";
|
||||
import { TRPCError } from "@trpc/server";
|
||||
import {
|
||||
createBackup,
|
||||
findBackupById,
|
||||
removeBackupById,
|
||||
updateBackupById,
|
||||
} from "../services/backup";
|
||||
import { findMariadbByBackupId } from "../services/mariadb";
|
||||
import { findMongoByBackupId } from "../services/mongo";
|
||||
import { findMySqlByBackupId } from "../services/mysql";
|
||||
import { findPostgresByBackupId } from "../services/postgres";
|
||||
import {
|
||||
removeScheduleBackup,
|
||||
scheduleBackup,
|
||||
} from "@/server/utils/backups/utils";
|
||||
|
||||
export const backupRouter = createTRPCRouter({
|
||||
create: protectedProcedure
|
||||
.input(apiCreateBackup)
|
||||
.mutation(async ({ input }) => {
|
||||
try {
|
||||
const newBackup = await createBackup(input);
|
||||
|
||||
const backup = await findBackupById(newBackup.backupId);
|
||||
|
||||
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 }) => {
|
||||
const backup = await findBackupById(input.backupId);
|
||||
return backup;
|
||||
}),
|
||||
update: protectedProcedure
|
||||
.input(apiUpdateBackup)
|
||||
.mutation(async ({ input }) => {
|
||||
try {
|
||||
await updateBackupById(input.backupId, input);
|
||||
const backup = await findBackupById(input.backupId);
|
||||
|
||||
if (backup.enabled) {
|
||||
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 }) => {
|
||||
try {
|
||||
const value = await removeBackupById(input.backupId);
|
||||
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,
|
||||
});
|
||||
}
|
||||
}),
|
||||
});
|
||||
29
server/api/routers/certificate.ts
Normal file
29
server/api/routers/certificate.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
import { adminProcedure, createTRPCRouter } from "@/server/api/trpc";
|
||||
import { apiCreateCertificate, apiFindCertificate } from "@/server/db/schema";
|
||||
import {
|
||||
createCertificate,
|
||||
findCertificates,
|
||||
findCertificateById,
|
||||
removeCertificateById,
|
||||
} from "../services/certificate";
|
||||
|
||||
export const certificateRouter = createTRPCRouter({
|
||||
create: adminProcedure
|
||||
.input(apiCreateCertificate)
|
||||
.mutation(async ({ input }) => {
|
||||
return await createCertificate(input);
|
||||
}),
|
||||
|
||||
one: adminProcedure.input(apiFindCertificate).query(async ({ input }) => {
|
||||
return await findCertificateById(input.certificateId);
|
||||
}),
|
||||
remove: adminProcedure
|
||||
.input(apiFindCertificate)
|
||||
.mutation(async ({ input }) => {
|
||||
await removeCertificateById(input.certificateId);
|
||||
return true;
|
||||
}),
|
||||
all: adminProcedure.query(async () => {
|
||||
return findCertificates();
|
||||
}),
|
||||
});
|
||||
11
server/api/routers/deployment.ts
Normal file
11
server/api/routers/deployment.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import { apiFindAllByApplication } from "@/server/db/schema";
|
||||
import { findAllDeploymentsByApplicationId } from "../services/deployment";
|
||||
import { createTRPCRouter, protectedProcedure } from "../trpc";
|
||||
|
||||
export const deploymentRouter = createTRPCRouter({
|
||||
all: protectedProcedure
|
||||
.input(apiFindAllByApplication)
|
||||
.query(async ({ input }) => {
|
||||
return await findAllDeploymentsByApplicationId(input.applicationId);
|
||||
}),
|
||||
});
|
||||
99
server/api/routers/destination.ts
Normal file
99
server/api/routers/destination.ts
Normal file
@@ -0,0 +1,99 @@
|
||||
import {
|
||||
adminProcedure,
|
||||
createTRPCRouter,
|
||||
protectedProcedure,
|
||||
} from "@/server/api/trpc";
|
||||
import { db } from "@/server/db";
|
||||
import {
|
||||
apiCreateDestination,
|
||||
apiFindOneDestination,
|
||||
apiRemoveDestination,
|
||||
apiUpdateDestination,
|
||||
} from "@/server/db/schema";
|
||||
import { HeadBucketCommand, S3Client } from "@aws-sdk/client-s3";
|
||||
import { TRPCError } from "@trpc/server";
|
||||
import {
|
||||
createDestintation,
|
||||
findDestinationById,
|
||||
removeDestinationById,
|
||||
updateDestinationById,
|
||||
} from "../services/destination";
|
||||
import { findAdmin } from "../services/admin";
|
||||
|
||||
export const destinationRouter = createTRPCRouter({
|
||||
create: adminProcedure
|
||||
.input(apiCreateDestination)
|
||||
.mutation(async ({ input }) => {
|
||||
try {
|
||||
await createDestintation(input);
|
||||
return await findAdmin();
|
||||
} 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;
|
||||
const s3Client = new S3Client({
|
||||
region: region,
|
||||
...(endpoint && {
|
||||
endpoint: endpoint,
|
||||
}),
|
||||
credentials: {
|
||||
accessKeyId: accessKey,
|
||||
secretAccessKey: secretAccessKey,
|
||||
},
|
||||
forcePathStyle: true,
|
||||
});
|
||||
const headBucketCommand = new HeadBucketCommand({ Bucket: bucket });
|
||||
|
||||
try {
|
||||
await s3Client.send(headBucketCommand);
|
||||
} catch (error) {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: "Error to connect to bucket",
|
||||
cause: error,
|
||||
});
|
||||
}
|
||||
}),
|
||||
one: protectedProcedure
|
||||
.input(apiFindOneDestination)
|
||||
.query(async ({ input }) => {
|
||||
const destination = await findDestinationById(input.destinationId);
|
||||
return destination;
|
||||
}),
|
||||
all: adminProcedure.query(async () => {
|
||||
return await db.query.destinations.findMany({});
|
||||
}),
|
||||
remove: adminProcedure
|
||||
.input(apiRemoveDestination)
|
||||
.mutation(async ({ input }) => {
|
||||
try {
|
||||
return await removeDestinationById(input.destinationId);
|
||||
} catch (error) {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: "Error to delete this destination",
|
||||
});
|
||||
}
|
||||
}),
|
||||
update: adminProcedure
|
||||
.input(apiUpdateDestination)
|
||||
.mutation(async ({ input }) => {
|
||||
try {
|
||||
return await updateDestinationById(input.destinationId, input);
|
||||
} catch (error) {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: "Error to update this destination",
|
||||
cause: error,
|
||||
});
|
||||
}
|
||||
}),
|
||||
});
|
||||
44
server/api/routers/docker.ts
Normal file
44
server/api/routers/docker.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
import { z } from "zod";
|
||||
import { createTRPCRouter, protectedProcedure } from "../trpc";
|
||||
import {
|
||||
getConfig,
|
||||
getContainersByAppLabel,
|
||||
getContainers,
|
||||
getContainersByAppNameMatch,
|
||||
} from "../services/docker";
|
||||
|
||||
export const dockerRouter = createTRPCRouter({
|
||||
getContainers: protectedProcedure.query(async () => {
|
||||
return await getContainers();
|
||||
}),
|
||||
|
||||
getConfig: protectedProcedure
|
||||
.input(
|
||||
z.object({
|
||||
containerId: z.string().min(1),
|
||||
}),
|
||||
)
|
||||
.query(async ({ input }) => {
|
||||
return await getConfig(input.containerId);
|
||||
}),
|
||||
|
||||
getContainersByAppNameMatch: protectedProcedure
|
||||
.input(
|
||||
z.object({
|
||||
appName: z.string().min(1),
|
||||
}),
|
||||
)
|
||||
.query(async ({ input }) => {
|
||||
return await getContainersByAppNameMatch(input.appName);
|
||||
}),
|
||||
|
||||
getContainersByAppLabel: protectedProcedure
|
||||
.input(
|
||||
z.object({
|
||||
appName: z.string().min(1),
|
||||
}),
|
||||
)
|
||||
.query(async ({ input }) => {
|
||||
return await getContainersByAppLabel(input.appName);
|
||||
}),
|
||||
});
|
||||
59
server/api/routers/domain.ts
Normal file
59
server/api/routers/domain.ts
Normal file
@@ -0,0 +1,59 @@
|
||||
import { createTRPCRouter, protectedProcedure } from "@/server/api/trpc";
|
||||
import {
|
||||
apiCreateDomain,
|
||||
apiFindDomain,
|
||||
apiFindDomainByApplication,
|
||||
apiUpdateDomain,
|
||||
} from "@/server/db/schema";
|
||||
import { manageDomain, removeDomain } from "@/server/utils/traefik/domain";
|
||||
import { TRPCError } from "@trpc/server";
|
||||
import { findApplicationById } from "../services/application";
|
||||
import {
|
||||
createDomain,
|
||||
findDomainById,
|
||||
findDomainsByApplicationId,
|
||||
removeDomainById,
|
||||
updateDomainById,
|
||||
} from "../services/domain";
|
||||
|
||||
export const domainRouter = createTRPCRouter({
|
||||
create: protectedProcedure
|
||||
.input(apiCreateDomain)
|
||||
.mutation(async ({ input }) => {
|
||||
try {
|
||||
await createDomain(input);
|
||||
} catch (error) {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: "Error to create the domain",
|
||||
cause: error,
|
||||
});
|
||||
}
|
||||
}),
|
||||
byApplicationId: protectedProcedure
|
||||
.input(apiFindDomainByApplication)
|
||||
.query(async ({ input }) => {
|
||||
return await findDomainsByApplicationId(input.applicationId);
|
||||
}),
|
||||
update: protectedProcedure
|
||||
.input(apiUpdateDomain)
|
||||
.mutation(async ({ input }) => {
|
||||
const result = await updateDomainById(input.domainId, input);
|
||||
const domain = await findDomainById(input.domainId);
|
||||
const application = await findApplicationById(domain.applicationId);
|
||||
await manageDomain(application, domain);
|
||||
return result;
|
||||
}),
|
||||
one: protectedProcedure.input(apiFindDomain).query(async ({ input }) => {
|
||||
return await findDomainById(input.domainId);
|
||||
}),
|
||||
delete: protectedProcedure
|
||||
.input(apiFindDomain)
|
||||
.mutation(async ({ input }) => {
|
||||
const domain = await findDomainById(input.domainId);
|
||||
const result = await removeDomainById(input.domainId);
|
||||
removeDomain(domain.application.appName, domain.uniqueConfigKey);
|
||||
|
||||
return result;
|
||||
}),
|
||||
});
|
||||
183
server/api/routers/mariadb.ts
Normal file
183
server/api/routers/mariadb.ts
Normal file
@@ -0,0 +1,183 @@
|
||||
import { createTRPCRouter, protectedProcedure } from "@/server/api/trpc";
|
||||
import {
|
||||
apiChangeMariaDBStatus,
|
||||
apiCreateMariaDB,
|
||||
apiDeployMariaDB,
|
||||
apiFindOneMariaDB,
|
||||
apiResetMariadb,
|
||||
apiSaveEnviromentVariablesMariaDB,
|
||||
apiSaveExternalPortMariaDB,
|
||||
apiUpdateMariaDB,
|
||||
} from "@/server/db/schema/mariadb";
|
||||
import {
|
||||
removeService,
|
||||
startService,
|
||||
stopService,
|
||||
} from "@/server/utils/docker/utils";
|
||||
import { TRPCError } from "@trpc/server";
|
||||
import {
|
||||
createMariadb,
|
||||
deployMariadb,
|
||||
findMariadbById,
|
||||
removeMariadbById,
|
||||
updateMariadbById,
|
||||
} from "../services/mariadb";
|
||||
import { addNewService, checkServiceAccess } from "../services/user";
|
||||
import { createMount } from "../services/mount";
|
||||
|
||||
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");
|
||||
}
|
||||
|
||||
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) {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: "Error input: Inserting mariadb database",
|
||||
cause: error,
|
||||
});
|
||||
}
|
||||
}),
|
||||
one: protectedProcedure
|
||||
.input(apiFindOneMariaDB)
|
||||
.query(async ({ input, ctx }) => {
|
||||
if (ctx.user.rol === "user") {
|
||||
await checkServiceAccess(ctx.user.authId, input.mariadbId, "access");
|
||||
}
|
||||
return await findMariadbById(input.mariadbId);
|
||||
}),
|
||||
|
||||
start: protectedProcedure
|
||||
.input(apiFindOneMariaDB)
|
||||
.mutation(async ({ input }) => {
|
||||
const service = await findMariadbById(input.mariadbId);
|
||||
|
||||
await startService(service.appName);
|
||||
await updateMariadbById(input.mariadbId, {
|
||||
applicationStatus: "done",
|
||||
});
|
||||
|
||||
return service;
|
||||
}),
|
||||
stop: protectedProcedure
|
||||
.input(apiFindOneMariaDB)
|
||||
.mutation(async ({ input }) => {
|
||||
const mongo = await findMariadbById(input.mariadbId);
|
||||
await stopService(mongo.appName);
|
||||
await updateMariadbById(input.mariadbId, {
|
||||
applicationStatus: "idle",
|
||||
});
|
||||
|
||||
return mongo;
|
||||
}),
|
||||
saveExternalPort: protectedProcedure
|
||||
.input(apiSaveExternalPortMariaDB)
|
||||
.mutation(async ({ input }) => {
|
||||
const mongo = await findMariadbById(input.mariadbId);
|
||||
await updateMariadbById(input.mariadbId, {
|
||||
externalPort: input.externalPort,
|
||||
});
|
||||
await deployMariadb(input.mariadbId);
|
||||
return mongo;
|
||||
}),
|
||||
deploy: protectedProcedure
|
||||
.input(apiDeployMariaDB)
|
||||
.mutation(async ({ input }) => {
|
||||
return deployMariadb(input.mariadbId);
|
||||
}),
|
||||
changeStatus: protectedProcedure
|
||||
.input(apiChangeMariaDBStatus)
|
||||
.mutation(async ({ input }) => {
|
||||
const mongo = await findMariadbById(input.mariadbId);
|
||||
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);
|
||||
|
||||
const cleanupOperations = [
|
||||
async () => await removeService(mongo?.appName),
|
||||
async () => await removeMariadbById(input.mariadbId),
|
||||
];
|
||||
|
||||
for (const operation of cleanupOperations) {
|
||||
try {
|
||||
await operation();
|
||||
} catch (error) {}
|
||||
}
|
||||
|
||||
return mongo;
|
||||
}),
|
||||
saveEnviroment: protectedProcedure
|
||||
.input(apiSaveEnviromentVariablesMariaDB)
|
||||
.mutation(async ({ input }) => {
|
||||
const service = await updateMariadbById(input.mariadbId, {
|
||||
env: input.env,
|
||||
});
|
||||
|
||||
if (!service) {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: "Update: Error to add enviroment variables",
|
||||
});
|
||||
}
|
||||
|
||||
return true;
|
||||
}),
|
||||
reload: protectedProcedure
|
||||
.input(apiResetMariadb)
|
||||
.mutation(async ({ input }) => {
|
||||
await stopService(input.appName);
|
||||
await updateMariadbById(input.mariadbId, {
|
||||
applicationStatus: "idle",
|
||||
});
|
||||
await startService(input.appName);
|
||||
await updateMariadbById(input.mariadbId, {
|
||||
applicationStatus: "done",
|
||||
});
|
||||
return true;
|
||||
}),
|
||||
update: protectedProcedure
|
||||
.input(apiUpdateMariaDB)
|
||||
.mutation(async ({ input }) => {
|
||||
const { mariadbId, ...rest } = input;
|
||||
const service = await updateMariadbById(mariadbId, {
|
||||
...rest,
|
||||
});
|
||||
|
||||
if (!service) {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: "Update: Error to update mariadb",
|
||||
});
|
||||
}
|
||||
|
||||
return true;
|
||||
}),
|
||||
});
|
||||
184
server/api/routers/mongo.ts
Normal file
184
server/api/routers/mongo.ts
Normal file
@@ -0,0 +1,184 @@
|
||||
import { createTRPCRouter, protectedProcedure } from "@/server/api/trpc";
|
||||
import {
|
||||
apiChangeMongoStatus,
|
||||
apiCreateMongo,
|
||||
apiDeployMongo,
|
||||
apiFindOneMongo,
|
||||
apiResetMongo,
|
||||
apiSaveEnviromentVariablesMongo,
|
||||
apiSaveExternalPortMongo,
|
||||
apiUpdateMongo,
|
||||
} from "@/server/db/schema/mongo";
|
||||
import {
|
||||
removeService,
|
||||
startService,
|
||||
stopService,
|
||||
} from "@/server/utils/docker/utils";
|
||||
import { TRPCError } from "@trpc/server";
|
||||
import {
|
||||
createMongo,
|
||||
deployMongo,
|
||||
findMongoById,
|
||||
removeMongoById,
|
||||
updateMongoById,
|
||||
} from "../services/mongo";
|
||||
import { addNewService, checkServiceAccess } from "../services/user";
|
||||
import { createMount } from "../services/mount";
|
||||
|
||||
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");
|
||||
}
|
||||
|
||||
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) {
|
||||
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");
|
||||
}
|
||||
|
||||
return await findMongoById(input.mongoId);
|
||||
}),
|
||||
|
||||
start: protectedProcedure
|
||||
.input(apiFindOneMongo)
|
||||
.mutation(async ({ input }) => {
|
||||
const service = await findMongoById(input.mongoId);
|
||||
|
||||
await startService(service.appName);
|
||||
await updateMongoById(input.mongoId, {
|
||||
applicationStatus: "done",
|
||||
});
|
||||
|
||||
return service;
|
||||
}),
|
||||
stop: protectedProcedure
|
||||
.input(apiFindOneMongo)
|
||||
.mutation(async ({ input }) => {
|
||||
const mongo = await findMongoById(input.mongoId);
|
||||
await stopService(mongo.appName);
|
||||
await updateMongoById(input.mongoId, {
|
||||
applicationStatus: "idle",
|
||||
});
|
||||
|
||||
return mongo;
|
||||
}),
|
||||
saveExternalPort: protectedProcedure
|
||||
.input(apiSaveExternalPortMongo)
|
||||
.mutation(async ({ input }) => {
|
||||
const mongo = await findMongoById(input.mongoId);
|
||||
await updateMongoById(input.mongoId, {
|
||||
externalPort: input.externalPort,
|
||||
});
|
||||
await deployMongo(input.mongoId);
|
||||
return mongo;
|
||||
}),
|
||||
deploy: protectedProcedure
|
||||
.input(apiDeployMongo)
|
||||
.mutation(async ({ input }) => {
|
||||
return deployMongo(input.mongoId);
|
||||
}),
|
||||
changeStatus: protectedProcedure
|
||||
.input(apiChangeMongoStatus)
|
||||
.mutation(async ({ input }) => {
|
||||
const mongo = await findMongoById(input.mongoId);
|
||||
await updateMongoById(input.mongoId, {
|
||||
applicationStatus: input.applicationStatus,
|
||||
});
|
||||
return mongo;
|
||||
}),
|
||||
reload: protectedProcedure
|
||||
.input(apiResetMongo)
|
||||
.mutation(async ({ input }) => {
|
||||
await stopService(input.appName);
|
||||
await updateMongoById(input.mongoId, {
|
||||
applicationStatus: "idle",
|
||||
});
|
||||
await startService(input.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);
|
||||
|
||||
const cleanupOperations = [
|
||||
async () => await removeService(mongo?.appName),
|
||||
async () => await removeMongoById(input.mongoId),
|
||||
];
|
||||
|
||||
for (const operation of cleanupOperations) {
|
||||
try {
|
||||
await operation();
|
||||
} catch (error) {}
|
||||
}
|
||||
|
||||
return mongo;
|
||||
}),
|
||||
saveEnviroment: protectedProcedure
|
||||
.input(apiSaveEnviromentVariablesMongo)
|
||||
.mutation(async ({ input }) => {
|
||||
const service = await updateMongoById(input.mongoId, {
|
||||
env: input.env,
|
||||
});
|
||||
|
||||
if (!service) {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: "Update: Error to add enviroment variables",
|
||||
});
|
||||
}
|
||||
|
||||
return true;
|
||||
}),
|
||||
update: protectedProcedure
|
||||
.input(apiUpdateMongo)
|
||||
.mutation(async ({ input }) => {
|
||||
const { mongoId, ...rest } = input;
|
||||
const service = await updateMongoById(mongoId, {
|
||||
...rest,
|
||||
});
|
||||
|
||||
if (!service) {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: "Update: Error to update mongo",
|
||||
});
|
||||
}
|
||||
|
||||
return true;
|
||||
}),
|
||||
});
|
||||
17
server/api/routers/mount.ts
Normal file
17
server/api/routers/mount.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import { apiCreateMount, apiRemoveMount } from "@/server/db/schema";
|
||||
import { createMount, deleteMount } from "../services/mount";
|
||||
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);
|
||||
}),
|
||||
});
|
||||
183
server/api/routers/mysql.ts
Normal file
183
server/api/routers/mysql.ts
Normal file
@@ -0,0 +1,183 @@
|
||||
import { createTRPCRouter, protectedProcedure } from "@/server/api/trpc";
|
||||
import {
|
||||
apiChangeMySqlStatus,
|
||||
apiCreateMySql,
|
||||
apiDeployMySql,
|
||||
apiFindOneMySql,
|
||||
apiResetMysql,
|
||||
apiSaveEnviromentVariablesMySql,
|
||||
apiSaveExternalPortMySql,
|
||||
apiUpdateMySql,
|
||||
} from "@/server/db/schema/mysql";
|
||||
import {
|
||||
removeService,
|
||||
startService,
|
||||
stopService,
|
||||
} from "@/server/utils/docker/utils";
|
||||
import { TRPCError } from "@trpc/server";
|
||||
import {
|
||||
createMysql,
|
||||
deployMySql,
|
||||
findMySqlById,
|
||||
removeMySqlById,
|
||||
updateMySqlById,
|
||||
} from "../services/mysql";
|
||||
import { addNewService, checkServiceAccess } from "../services/user";
|
||||
import { createMount } from "../services/mount";
|
||||
import { z } from "zod";
|
||||
|
||||
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");
|
||||
}
|
||||
|
||||
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) {
|
||||
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");
|
||||
}
|
||||
return await findMySqlById(input.mysqlId);
|
||||
}),
|
||||
|
||||
start: protectedProcedure
|
||||
.input(apiFindOneMySql)
|
||||
.mutation(async ({ input }) => {
|
||||
const service = await findMySqlById(input.mysqlId);
|
||||
|
||||
await startService(service.appName);
|
||||
await updateMySqlById(input.mysqlId, {
|
||||
applicationStatus: "done",
|
||||
});
|
||||
|
||||
return service;
|
||||
}),
|
||||
stop: protectedProcedure
|
||||
.input(apiFindOneMySql)
|
||||
.mutation(async ({ input }) => {
|
||||
const mongo = await findMySqlById(input.mysqlId);
|
||||
await stopService(mongo.appName);
|
||||
await updateMySqlById(input.mysqlId, {
|
||||
applicationStatus: "idle",
|
||||
});
|
||||
|
||||
return mongo;
|
||||
}),
|
||||
saveExternalPort: protectedProcedure
|
||||
.input(apiSaveExternalPortMySql)
|
||||
.mutation(async ({ input }) => {
|
||||
const mongo = await findMySqlById(input.mysqlId);
|
||||
await updateMySqlById(input.mysqlId, {
|
||||
externalPort: input.externalPort,
|
||||
});
|
||||
await deployMySql(input.mysqlId);
|
||||
return mongo;
|
||||
}),
|
||||
deploy: protectedProcedure
|
||||
.input(apiDeployMySql)
|
||||
.mutation(async ({ input }) => {
|
||||
return deployMySql(input.mysqlId);
|
||||
}),
|
||||
changeStatus: protectedProcedure
|
||||
.input(apiChangeMySqlStatus)
|
||||
.mutation(async ({ input }) => {
|
||||
const mongo = await findMySqlById(input.mysqlId);
|
||||
await updateMySqlById(input.mysqlId, {
|
||||
applicationStatus: input.applicationStatus,
|
||||
});
|
||||
return mongo;
|
||||
}),
|
||||
reload: protectedProcedure
|
||||
.input(apiResetMysql)
|
||||
.mutation(async ({ input }) => {
|
||||
await stopService(input.appName);
|
||||
await updateMySqlById(input.mysqlId, {
|
||||
applicationStatus: "idle",
|
||||
});
|
||||
await startService(input.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);
|
||||
|
||||
const cleanupOperations = [
|
||||
async () => await removeService(mongo?.appName),
|
||||
async () => await removeMySqlById(input.mysqlId),
|
||||
];
|
||||
|
||||
for (const operation of cleanupOperations) {
|
||||
try {
|
||||
await operation();
|
||||
} catch (error) {}
|
||||
}
|
||||
|
||||
return mongo;
|
||||
}),
|
||||
saveEnviroment: protectedProcedure
|
||||
.input(apiSaveEnviromentVariablesMySql)
|
||||
.mutation(async ({ input }) => {
|
||||
const service = await updateMySqlById(input.mysqlId, {
|
||||
env: input.env,
|
||||
});
|
||||
|
||||
if (!service) {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: "Update: Error to add enviroment variables",
|
||||
});
|
||||
}
|
||||
|
||||
return true;
|
||||
}),
|
||||
update: protectedProcedure
|
||||
.input(apiUpdateMySql)
|
||||
.mutation(async ({ input }) => {
|
||||
const { mysqlId, ...rest } = input;
|
||||
const service = await updateMySqlById(mysqlId, {
|
||||
...rest,
|
||||
});
|
||||
|
||||
if (!service) {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: "Update: Error to update mysql",
|
||||
});
|
||||
}
|
||||
|
||||
return true;
|
||||
}),
|
||||
});
|
||||
65
server/api/routers/port.ts
Normal file
65
server/api/routers/port.ts
Normal file
@@ -0,0 +1,65 @@
|
||||
import { TRPCError } from "@trpc/server";
|
||||
import { createTRPCRouter, protectedProcedure } from "@/server/api/trpc";
|
||||
import {
|
||||
apiCreatePort,
|
||||
apiFindOnePort,
|
||||
apiUpdatePort,
|
||||
} from "@/server/db/schema/port";
|
||||
import {
|
||||
createPort,
|
||||
finPortById,
|
||||
removePortById,
|
||||
updatePortById,
|
||||
} from "../services/port";
|
||||
|
||||
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",
|
||||
});
|
||||
}
|
||||
}),
|
||||
});
|
||||
179
server/api/routers/postgres.ts
Normal file
179
server/api/routers/postgres.ts
Normal file
@@ -0,0 +1,179 @@
|
||||
import { createTRPCRouter, protectedProcedure } from "@/server/api/trpc";
|
||||
import {
|
||||
apiChangePostgresStatus,
|
||||
apiCreatePostgres,
|
||||
apiDeployPostgres,
|
||||
apiFindOnePostgres,
|
||||
apiResetPostgres,
|
||||
apiSaveEnviromentVariablesPostgres,
|
||||
apiSaveExternalPortPostgres,
|
||||
apiUpdatePostgres,
|
||||
} from "@/server/db/schema/postgres";
|
||||
import {
|
||||
removeService,
|
||||
startService,
|
||||
stopService,
|
||||
} from "@/server/utils/docker/utils";
|
||||
import { TRPCError } from "@trpc/server";
|
||||
import {
|
||||
createPostgres,
|
||||
deployPostgres,
|
||||
findPostgresById,
|
||||
removePostgresById,
|
||||
updatePostgresById,
|
||||
} from "../services/postgres";
|
||||
import { addNewService, checkServiceAccess } from "../services/user";
|
||||
import { createMount } from "../services/mount";
|
||||
|
||||
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");
|
||||
}
|
||||
|
||||
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) {
|
||||
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");
|
||||
}
|
||||
|
||||
return await findPostgresById(input.postgresId);
|
||||
}),
|
||||
|
||||
start: protectedProcedure
|
||||
.input(apiFindOnePostgres)
|
||||
.mutation(async ({ input }) => {
|
||||
const service = await findPostgresById(input.postgresId);
|
||||
|
||||
await startService(service.appName);
|
||||
await updatePostgresById(input.postgresId, {
|
||||
applicationStatus: "done",
|
||||
});
|
||||
|
||||
return service;
|
||||
}),
|
||||
stop: protectedProcedure
|
||||
.input(apiFindOnePostgres)
|
||||
.mutation(async ({ input }) => {
|
||||
const postgres = await findPostgresById(input.postgresId);
|
||||
await stopService(postgres.appName);
|
||||
await updatePostgresById(input.postgresId, {
|
||||
applicationStatus: "idle",
|
||||
});
|
||||
|
||||
return postgres;
|
||||
}),
|
||||
saveExternalPort: protectedProcedure
|
||||
.input(apiSaveExternalPortPostgres)
|
||||
.mutation(async ({ input }) => {
|
||||
const postgres = await findPostgresById(input.postgresId);
|
||||
await updatePostgresById(input.postgresId, {
|
||||
externalPort: input.externalPort,
|
||||
});
|
||||
await deployPostgres(input.postgresId);
|
||||
return postgres;
|
||||
}),
|
||||
deploy: protectedProcedure
|
||||
.input(apiDeployPostgres)
|
||||
.mutation(async ({ input }) => {
|
||||
return deployPostgres(input.postgresId);
|
||||
}),
|
||||
changeStatus: protectedProcedure
|
||||
.input(apiChangePostgresStatus)
|
||||
.mutation(async ({ input }) => {
|
||||
const postgres = await findPostgresById(input.postgresId);
|
||||
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);
|
||||
|
||||
const cleanupOperations = [
|
||||
removeService(postgres.appName),
|
||||
removePostgresById(input.postgresId),
|
||||
];
|
||||
|
||||
await Promise.allSettled(cleanupOperations);
|
||||
|
||||
return postgres;
|
||||
}),
|
||||
saveEnviroment: protectedProcedure
|
||||
.input(apiSaveEnviromentVariablesPostgres)
|
||||
.mutation(async ({ input }) => {
|
||||
const service = await updatePostgresById(input.postgresId, {
|
||||
env: input.env,
|
||||
});
|
||||
|
||||
if (!service) {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: "Update: Error to add enviroment variables",
|
||||
});
|
||||
}
|
||||
|
||||
return true;
|
||||
}),
|
||||
reload: protectedProcedure
|
||||
.input(apiResetPostgres)
|
||||
.mutation(async ({ input }) => {
|
||||
await stopService(input.appName);
|
||||
await updatePostgresById(input.postgresId, {
|
||||
applicationStatus: "idle",
|
||||
});
|
||||
await startService(input.appName);
|
||||
await updatePostgresById(input.postgresId, {
|
||||
applicationStatus: "done",
|
||||
});
|
||||
return true;
|
||||
}),
|
||||
update: protectedProcedure
|
||||
.input(apiUpdatePostgres)
|
||||
.mutation(async ({ input }) => {
|
||||
const { postgresId, ...rest } = input;
|
||||
const service = await updatePostgresById(postgresId, {
|
||||
...rest,
|
||||
});
|
||||
|
||||
if (!service) {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: "Update: Error to update postgres",
|
||||
});
|
||||
}
|
||||
|
||||
return true;
|
||||
}),
|
||||
});
|
||||
197
server/api/routers/project.ts
Normal file
197
server/api/routers/project.ts
Normal file
@@ -0,0 +1,197 @@
|
||||
import { createTRPCRouter, protectedProcedure } from "@/server/api/trpc";
|
||||
import { db } from "@/server/db";
|
||||
import {
|
||||
apiCreateProject,
|
||||
apiFindOneProject,
|
||||
apiRemoveProject,
|
||||
apiUpdateProject,
|
||||
projects,
|
||||
} from "@/server/db/schema/project";
|
||||
import { TRPCError } from "@trpc/server";
|
||||
import { desc, eq, sql } from "drizzle-orm";
|
||||
import {
|
||||
createProject,
|
||||
deleteProject,
|
||||
findProjectById,
|
||||
updateProjectById,
|
||||
} from "../services/project";
|
||||
import {
|
||||
addNewProject,
|
||||
checkProjectAccess,
|
||||
findUserByAuthId,
|
||||
} from "../services/user";
|
||||
import {
|
||||
applications,
|
||||
mariadb,
|
||||
mongo,
|
||||
mysql,
|
||||
postgres,
|
||||
redis,
|
||||
} from "@/server/db/schema";
|
||||
import type { AnyPgColumn } from "drizzle-orm/pg-core";
|
||||
|
||||
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);
|
||||
if (ctx.user.rol === "user") {
|
||||
await addNewProject(ctx.user.authId, project.projectId);
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(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 service = await db.query.projects.findFirst({
|
||||
where: eq(projects.projectId, input.projectId),
|
||||
with: {
|
||||
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 (!service) {
|
||||
throw new TRPCError({
|
||||
code: "NOT_FOUND",
|
||||
message: "Project not found",
|
||||
});
|
||||
}
|
||||
return service;
|
||||
}
|
||||
const project = await findProjectById(input.projectId);
|
||||
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,
|
||||
),
|
||||
},
|
||||
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),
|
||||
},
|
||||
},
|
||||
orderBy: desc(projects.createdAt),
|
||||
});
|
||||
|
||||
return query;
|
||||
}
|
||||
return await db.query.projects.findMany({
|
||||
with: {
|
||||
applications: true,
|
||||
mariadb: true,
|
||||
mongo: true,
|
||||
mysql: true,
|
||||
postgres: true,
|
||||
redis: true,
|
||||
},
|
||||
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 project = await deleteProject(input.projectId);
|
||||
|
||||
return project;
|
||||
} catch (error) {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: "Error to delete this project",
|
||||
cause: error,
|
||||
});
|
||||
}
|
||||
}),
|
||||
update: protectedProcedure
|
||||
.input(apiUpdateProject)
|
||||
.mutation(async ({ input }) => {
|
||||
try {
|
||||
const project = updateProjectById(input.projectId, {
|
||||
...input,
|
||||
});
|
||||
|
||||
return project;
|
||||
} catch (error) {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: "Error to update this project",
|
||||
cause: 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
|
||||
}
|
||||
33
server/api/routers/redirects.ts
Normal file
33
server/api/routers/redirects.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
import { createTRPCRouter, protectedProcedure } from "../trpc";
|
||||
import {
|
||||
apiCreateRedirect,
|
||||
apiFindOneRedirect,
|
||||
apiUpdateRedirect,
|
||||
} from "@/server/db/schema";
|
||||
import {
|
||||
createRedirect,
|
||||
findRedirectById,
|
||||
removeRedirectById,
|
||||
updateRedirectById,
|
||||
} from "../services/redirect";
|
||||
|
||||
export const redirectsRouter = createTRPCRouter({
|
||||
create: protectedProcedure
|
||||
.input(apiCreateRedirect)
|
||||
.mutation(async ({ input }) => {
|
||||
return await createRedirect(input);
|
||||
}),
|
||||
one: protectedProcedure.input(apiFindOneRedirect).query(async ({ input }) => {
|
||||
return findRedirectById(input.redirectId);
|
||||
}),
|
||||
delete: protectedProcedure
|
||||
.input(apiFindOneRedirect)
|
||||
.mutation(async ({ input }) => {
|
||||
return removeRedirectById(input.redirectId);
|
||||
}),
|
||||
update: protectedProcedure
|
||||
.input(apiUpdateRedirect)
|
||||
.mutation(async ({ input }) => {
|
||||
return updateRedirectById(input.redirectId, input);
|
||||
}),
|
||||
});
|
||||
183
server/api/routers/redis.ts
Normal file
183
server/api/routers/redis.ts
Normal file
@@ -0,0 +1,183 @@
|
||||
import { createTRPCRouter, protectedProcedure } from "@/server/api/trpc";
|
||||
import {
|
||||
apiChangeRedisStatus,
|
||||
apiCreateRedis,
|
||||
apiDeployRedis,
|
||||
apiFindOneRedis,
|
||||
apiResetRedis,
|
||||
apiSaveEnviromentVariablesRedis,
|
||||
apiSaveExternalPortRedis,
|
||||
apiUpdateRedis,
|
||||
} from "@/server/db/schema/redis";
|
||||
import {
|
||||
removeService,
|
||||
startService,
|
||||
stopService,
|
||||
} from "@/server/utils/docker/utils";
|
||||
import { TRPCError } from "@trpc/server";
|
||||
import {
|
||||
createRedis,
|
||||
deployRedis,
|
||||
findRedisById,
|
||||
removeRedisById,
|
||||
updateRedisById,
|
||||
} from "../services/redis";
|
||||
import { addNewService, checkServiceAccess } from "../services/user";
|
||||
import { createMount } from "../services/mount";
|
||||
|
||||
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");
|
||||
}
|
||||
|
||||
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 new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: "Error input: Inserting redis database",
|
||||
cause: error,
|
||||
});
|
||||
}
|
||||
}),
|
||||
one: protectedProcedure
|
||||
.input(apiFindOneRedis)
|
||||
.query(async ({ input, ctx }) => {
|
||||
if (ctx.user.rol === "user") {
|
||||
await checkServiceAccess(ctx.user.authId, input.redisId, "access");
|
||||
}
|
||||
return await findRedisById(input.redisId);
|
||||
}),
|
||||
|
||||
start: protectedProcedure
|
||||
.input(apiFindOneRedis)
|
||||
.mutation(async ({ input }) => {
|
||||
const redis = await findRedisById(input.redisId);
|
||||
await startService(redis.appName);
|
||||
await updateRedisById(input.redisId, {
|
||||
applicationStatus: "done",
|
||||
});
|
||||
|
||||
return redis;
|
||||
}),
|
||||
reload: protectedProcedure
|
||||
.input(apiResetRedis)
|
||||
.mutation(async ({ input }) => {
|
||||
await stopService(input.appName);
|
||||
await updateRedisById(input.redisId, {
|
||||
applicationStatus: "idle",
|
||||
});
|
||||
await startService(input.appName);
|
||||
await updateRedisById(input.redisId, {
|
||||
applicationStatus: "done",
|
||||
});
|
||||
return true;
|
||||
}),
|
||||
|
||||
stop: protectedProcedure
|
||||
.input(apiFindOneRedis)
|
||||
.mutation(async ({ input }) => {
|
||||
const mongo = await findRedisById(input.redisId);
|
||||
await stopService(mongo.appName);
|
||||
await updateRedisById(input.redisId, {
|
||||
applicationStatus: "idle",
|
||||
});
|
||||
|
||||
return mongo;
|
||||
}),
|
||||
saveExternalPort: protectedProcedure
|
||||
.input(apiSaveExternalPortRedis)
|
||||
.mutation(async ({ input }) => {
|
||||
const mongo = await findRedisById(input.redisId);
|
||||
await updateRedisById(input.redisId, {
|
||||
externalPort: input.externalPort,
|
||||
});
|
||||
await deployRedis(input.redisId);
|
||||
return mongo;
|
||||
}),
|
||||
deploy: protectedProcedure
|
||||
.input(apiDeployRedis)
|
||||
.mutation(async ({ input }) => {
|
||||
return deployRedis(input.redisId);
|
||||
}),
|
||||
changeStatus: protectedProcedure
|
||||
.input(apiChangeRedisStatus)
|
||||
.mutation(async ({ input }) => {
|
||||
const mongo = await findRedisById(input.redisId);
|
||||
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);
|
||||
|
||||
const cleanupOperations = [
|
||||
async () => await removeService(redis?.appName),
|
||||
async () => await removeRedisById(input.redisId),
|
||||
];
|
||||
|
||||
for (const operation of cleanupOperations) {
|
||||
try {
|
||||
await operation();
|
||||
} catch (error) {}
|
||||
}
|
||||
|
||||
return redis;
|
||||
}),
|
||||
saveEnviroment: protectedProcedure
|
||||
.input(apiSaveEnviromentVariablesRedis)
|
||||
.mutation(async ({ input }) => {
|
||||
const redis = await updateRedisById(input.redisId, {
|
||||
env: input.env,
|
||||
});
|
||||
|
||||
if (!redis) {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: "Update: Error to add enviroment 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;
|
||||
}),
|
||||
});
|
||||
33
server/api/routers/security.ts
Normal file
33
server/api/routers/security.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
import { createTRPCRouter, protectedProcedure } from "../trpc";
|
||||
import {
|
||||
apiCreateSecurity,
|
||||
apiFindOneSecurity,
|
||||
apiUpdateSecurity,
|
||||
} from "@/server/db/schema";
|
||||
import {
|
||||
createSecurity,
|
||||
deleteSecurityById,
|
||||
findSecurityById,
|
||||
updateSecurityById,
|
||||
} from "../services/security";
|
||||
|
||||
export const securityRouter = createTRPCRouter({
|
||||
create: protectedProcedure
|
||||
.input(apiCreateSecurity)
|
||||
.mutation(async ({ input }) => {
|
||||
return await createSecurity(input);
|
||||
}),
|
||||
one: protectedProcedure.input(apiFindOneSecurity).query(async ({ input }) => {
|
||||
return await findSecurityById(input.securityId);
|
||||
}),
|
||||
delete: protectedProcedure
|
||||
.input(apiFindOneSecurity)
|
||||
.mutation(async ({ input }) => {
|
||||
return await deleteSecurityById(input.securityId);
|
||||
}),
|
||||
update: protectedProcedure
|
||||
.input(apiUpdateSecurity)
|
||||
.mutation(async ({ input }) => {
|
||||
return await updateSecurityById(input.securityId, input);
|
||||
}),
|
||||
});
|
||||
240
server/api/routers/settings.ts
Normal file
240
server/api/routers/settings.ts
Normal file
@@ -0,0 +1,240 @@
|
||||
import { docker, MAIN_TRAEFIK_PATH } from "@/server/constants";
|
||||
import { adminProcedure, createTRPCRouter, protectedProcedure } from "../trpc";
|
||||
import {
|
||||
cleanStoppedContainers,
|
||||
cleanUpDockerBuilder,
|
||||
cleanUpSystemPrune,
|
||||
cleanUpUnusedImages,
|
||||
cleanUpUnusedVolumes,
|
||||
startService,
|
||||
stopService,
|
||||
} from "@/server/utils/docker/utils";
|
||||
import {
|
||||
apiAssignDomain,
|
||||
apiModifyTraefikConfig,
|
||||
apiReadTraefikConfig,
|
||||
apiSaveSSHKey,
|
||||
apiTraefikConfig,
|
||||
apiUpdateDockerCleanup,
|
||||
} from "@/server/db/schema";
|
||||
import { scheduledJobs, scheduleJob } from "node-schedule";
|
||||
import {
|
||||
readMainConfig,
|
||||
updateLetsEncryptEmail,
|
||||
updateServerTraefik,
|
||||
writeMainConfig,
|
||||
} from "@/server/utils/traefik/web-server";
|
||||
import {
|
||||
readConfig,
|
||||
readConfigInPath,
|
||||
writeConfig,
|
||||
writeTraefikConfigInPath,
|
||||
} from "@/server/utils/traefik/application";
|
||||
import { spawnAsync } from "@/server/utils/process/spawnAsync";
|
||||
import { findAdmin, updateAdmin } from "../services/admin";
|
||||
import { TRPCError } from "@trpc/server";
|
||||
import {
|
||||
getDokployVersion,
|
||||
getDokployImage,
|
||||
pullLatestRelease,
|
||||
readDirectory,
|
||||
} from "../services/settings";
|
||||
import { canAccessToTraefikFiles } from "../services/user";
|
||||
|
||||
export const settingsRouter = createTRPCRouter({
|
||||
reloadServer: adminProcedure.mutation(async () => {
|
||||
await spawnAsync("docker", [
|
||||
"service",
|
||||
"update",
|
||||
"--force",
|
||||
"--image",
|
||||
getDokployImage(),
|
||||
"dokploy",
|
||||
]);
|
||||
return true;
|
||||
}),
|
||||
reloadTraefik: adminProcedure.mutation(async () => {
|
||||
await stopService("dokploy-traefik");
|
||||
await startService("dokploy-traefik");
|
||||
return true;
|
||||
}),
|
||||
cleanUnusedImages: adminProcedure.mutation(async () => {
|
||||
await cleanUpUnusedImages();
|
||||
return true;
|
||||
}),
|
||||
cleanUnusedVolumes: adminProcedure.mutation(async () => {
|
||||
await cleanUpUnusedVolumes();
|
||||
return true;
|
||||
}),
|
||||
cleanStoppedContainers: adminProcedure.mutation(async () => {
|
||||
await cleanStoppedContainers();
|
||||
return true;
|
||||
}),
|
||||
cleanDockerBuilder: adminProcedure.mutation(async () => {
|
||||
await cleanUpDockerBuilder();
|
||||
}),
|
||||
cleanDockerPrune: adminProcedure.mutation(async () => {
|
||||
await cleanUpSystemPrune();
|
||||
await cleanUpDockerBuilder();
|
||||
|
||||
return true;
|
||||
}),
|
||||
cleanAll: adminProcedure.mutation(async () => {
|
||||
await cleanUpUnusedImages();
|
||||
await cleanUpDockerBuilder();
|
||||
await cleanUpSystemPrune();
|
||||
return true;
|
||||
}),
|
||||
saveSSHPrivateKey: adminProcedure
|
||||
.input(apiSaveSSHKey)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
await updateAdmin(ctx.user.authId, {
|
||||
sshPrivateKey: input.sshPrivateKey,
|
||||
});
|
||||
|
||||
return true;
|
||||
}),
|
||||
assignDomainServer: adminProcedure
|
||||
.input(apiAssignDomain)
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
const admin = await updateAdmin(ctx.user.authId, {
|
||||
host: input.host,
|
||||
letsEncryptEmail: input.letsEncryptEmail,
|
||||
certificateType: input.certificateType,
|
||||
});
|
||||
|
||||
if (!admin) {
|
||||
throw new TRPCError({
|
||||
code: "NOT_FOUND",
|
||||
message: "Admin not found",
|
||||
});
|
||||
}
|
||||
|
||||
updateServerTraefik(admin, input.host);
|
||||
updateLetsEncryptEmail(admin.letsEncryptEmail);
|
||||
return admin;
|
||||
}),
|
||||
cleanSSHPrivateKey: adminProcedure.mutation(async ({ ctx }) => {
|
||||
await updateAdmin(ctx.user.authId, {
|
||||
sshPrivateKey: null,
|
||||
});
|
||||
return true;
|
||||
}),
|
||||
updateDockerCleanup: adminProcedure
|
||||
.input(apiUpdateDockerCleanup)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
await updateAdmin(ctx.user.authId, {
|
||||
enableDockerCleanup: input.enableDockerCleanup,
|
||||
});
|
||||
|
||||
const admin = await findAdmin();
|
||||
|
||||
if (admin.enableDockerCleanup) {
|
||||
scheduleJob("docker-cleanup", "0 0 * * *", async () => {
|
||||
console.log(
|
||||
`Docker Cleanup ${new Date().toLocaleString()}] Running...`,
|
||||
);
|
||||
await cleanUpUnusedImages();
|
||||
await cleanUpDockerBuilder();
|
||||
await cleanUpSystemPrune();
|
||||
});
|
||||
} else {
|
||||
const currentJob = scheduledJobs["docker-cleanup"];
|
||||
currentJob?.cancel();
|
||||
}
|
||||
|
||||
return true;
|
||||
}),
|
||||
|
||||
readTraefikConfig: adminProcedure.query(() => {
|
||||
const traefikConfig = readMainConfig();
|
||||
return traefikConfig;
|
||||
}),
|
||||
|
||||
updateTraefikConfig: adminProcedure
|
||||
.input(apiTraefikConfig)
|
||||
.mutation(async ({ input }) => {
|
||||
writeMainConfig(input.traefikConfig);
|
||||
return true;
|
||||
}),
|
||||
|
||||
readWebServerTraefikConfig: adminProcedure.query(() => {
|
||||
const traefikConfig = readConfig("dokploy");
|
||||
return traefikConfig;
|
||||
}),
|
||||
updateWebServerTraefikConfig: adminProcedure
|
||||
.input(apiTraefikConfig)
|
||||
.mutation(async ({ input }) => {
|
||||
writeConfig("dokploy", input.traefikConfig);
|
||||
return true;
|
||||
}),
|
||||
|
||||
readMiddlewareTraefikConfig: adminProcedure.query(() => {
|
||||
const traefikConfig = readConfig("middlewares");
|
||||
return traefikConfig;
|
||||
}),
|
||||
|
||||
updateMiddlewareTraefikConfig: adminProcedure
|
||||
.input(apiTraefikConfig)
|
||||
.mutation(async ({ input }) => {
|
||||
writeConfig("middlewares", input.traefikConfig);
|
||||
return true;
|
||||
}),
|
||||
|
||||
checkAndUpdateImage: adminProcedure.query(async () => {
|
||||
return await pullLatestRelease();
|
||||
}),
|
||||
updateServer: adminProcedure.mutation(async () => {
|
||||
await spawnAsync("docker", [
|
||||
"service",
|
||||
"update",
|
||||
"--force",
|
||||
"--image",
|
||||
getDokployImage(),
|
||||
"dokploy",
|
||||
]);
|
||||
return true;
|
||||
}),
|
||||
|
||||
getDokployVersion: adminProcedure.query(() => {
|
||||
return getDokployVersion();
|
||||
}),
|
||||
readDirectories: protectedProcedure.query(async ({ ctx }) => {
|
||||
if (ctx.user.rol === "user") {
|
||||
const canAccess = await canAccessToTraefikFiles(ctx.user.authId);
|
||||
|
||||
if (!canAccess) {
|
||||
throw new TRPCError({ code: "UNAUTHORIZED" });
|
||||
}
|
||||
}
|
||||
const result = readDirectory(MAIN_TRAEFIK_PATH);
|
||||
return result || [];
|
||||
}),
|
||||
|
||||
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" });
|
||||
}
|
||||
}
|
||||
writeTraefikConfigInPath(input.path, input.traefikConfig);
|
||||
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);
|
||||
}),
|
||||
});
|
||||
20
server/api/routers/user.ts
Normal file
20
server/api/routers/user.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import { apiFindOneUser, apiFindOneUserByAuth } from "@/server/db/schema";
|
||||
import { adminProcedure, createTRPCRouter, protectedProcedure } from "../trpc";
|
||||
|
||||
import { findUserByAuthId, findUserById, findUsers } from "../services/user";
|
||||
|
||||
export const userRouter = createTRPCRouter({
|
||||
all: adminProcedure.query(async () => {
|
||||
return await findUsers();
|
||||
}),
|
||||
byAuthId: protectedProcedure
|
||||
.input(apiFindOneUserByAuth)
|
||||
.query(async ({ input }) => {
|
||||
return await findUserByAuthId(input.authId);
|
||||
}),
|
||||
byUserId: protectedProcedure
|
||||
.input(apiFindOneUser)
|
||||
.query(async ({ input }) => {
|
||||
return await findUserById(input.userId);
|
||||
}),
|
||||
});
|
||||
148
server/api/services/admin.ts
Normal file
148
server/api/services/admin.ts
Normal file
@@ -0,0 +1,148 @@
|
||||
import { db } from "@/server/db";
|
||||
import {
|
||||
admins,
|
||||
type apiCreateUserInvitation,
|
||||
auth,
|
||||
users,
|
||||
} from "@/server/db/schema";
|
||||
import { TRPCError } from "@trpc/server";
|
||||
import { isAfter } from "date-fns";
|
||||
import { eq } from "drizzle-orm";
|
||||
import * as bcrypt from "bcrypt";
|
||||
import { randomBytes } from "node:crypto";
|
||||
|
||||
export type Admin = typeof admins.$inferSelect;
|
||||
|
||||
export const createInvitation = async (
|
||||
input: typeof apiCreateUserInvitation._type,
|
||||
) => {
|
||||
await db.transaction(async (tx) => {
|
||||
const admin = await findAdmin();
|
||||
|
||||
const result = await tx
|
||||
.insert(auth)
|
||||
.values({
|
||||
email: input.email,
|
||||
rol: "user",
|
||||
password: bcrypt.hashSync("01231203012312", 10),
|
||||
})
|
||||
.returning()
|
||||
.then((res) => res[0]);
|
||||
|
||||
if (!result) {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: "Error to create the user",
|
||||
});
|
||||
}
|
||||
const expiresIn24Hours = new Date();
|
||||
expiresIn24Hours.setDate(expiresIn24Hours.getDate() + 1);
|
||||
const token = randomBytes(32).toString("hex");
|
||||
await tx
|
||||
.insert(users)
|
||||
.values({
|
||||
adminId: admin.adminId,
|
||||
authId: result.id,
|
||||
token,
|
||||
expirationDate: expiresIn24Hours.toISOString(),
|
||||
})
|
||||
.returning();
|
||||
});
|
||||
};
|
||||
|
||||
export const findAdminById = async (adminId: string) => {
|
||||
const admin = await db.query.admins.findFirst({
|
||||
where: eq(admins.adminId, adminId),
|
||||
});
|
||||
if (!admin) {
|
||||
throw new TRPCError({
|
||||
code: "NOT_FOUND",
|
||||
message: "Admin not found",
|
||||
});
|
||||
}
|
||||
return admin;
|
||||
};
|
||||
|
||||
export const updateAdmin = async (
|
||||
authId: string,
|
||||
adminData: Partial<Admin>,
|
||||
) => {
|
||||
const admin = await db
|
||||
.update(admins)
|
||||
.set({
|
||||
...adminData,
|
||||
})
|
||||
.where(eq(admins.authId, authId))
|
||||
.returning()
|
||||
.then((res) => res[0]);
|
||||
|
||||
return admin;
|
||||
};
|
||||
|
||||
export const isAdminPresent = async () => {
|
||||
const admin = await db.query.admins.findFirst();
|
||||
if (!admin) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
export const findAdminByAuthId = async (authId: string) => {
|
||||
const admin = await db.query.admins.findFirst({
|
||||
where: eq(admins.authId, authId),
|
||||
});
|
||||
if (!admin) {
|
||||
throw new TRPCError({
|
||||
code: "NOT_FOUND",
|
||||
message: "Admin not found",
|
||||
});
|
||||
}
|
||||
return admin;
|
||||
};
|
||||
|
||||
export const findAdmin = async () => {
|
||||
const admin = await db.query.admins.findFirst({});
|
||||
if (!admin) {
|
||||
throw new TRPCError({
|
||||
code: "NOT_FOUND",
|
||||
message: "Admin not found",
|
||||
});
|
||||
}
|
||||
return admin;
|
||||
};
|
||||
|
||||
export const getUserByToken = async (token: string) => {
|
||||
const user = await db.query.users.findFirst({
|
||||
where: eq(users.token, token),
|
||||
with: {
|
||||
auth: {
|
||||
columns: {
|
||||
password: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!user) {
|
||||
throw new TRPCError({
|
||||
code: "NOT_FOUND",
|
||||
message: "Invitation not found",
|
||||
});
|
||||
}
|
||||
|
||||
const now = new Date();
|
||||
const isExpired = isAfter(now, new Date(user.expirationDate));
|
||||
|
||||
return {
|
||||
...user,
|
||||
isExpired,
|
||||
};
|
||||
};
|
||||
|
||||
export const removeUserByAuthId = async (authId: string) => {
|
||||
await db
|
||||
.delete(auth)
|
||||
.where(eq(auth.id, authId))
|
||||
.returning()
|
||||
.then((res) => res[0]);
|
||||
};
|
||||
207
server/api/services/application.ts
Normal file
207
server/api/services/application.ts
Normal file
@@ -0,0 +1,207 @@
|
||||
import { db } from "@/server/db";
|
||||
import {
|
||||
type apiCreateApplication,
|
||||
applications,
|
||||
domains,
|
||||
} from "@/server/db/schema";
|
||||
import { buildApplication } from "@/server/utils/builders";
|
||||
import { buildDocker } from "@/server/utils/providers/docker";
|
||||
import { cloneGitRepository } from "@/server/utils/providers/git";
|
||||
import { cloneGithubRepository } from "@/server/utils/providers/github";
|
||||
import { TRPCError } from "@trpc/server";
|
||||
import { eq } from "drizzle-orm";
|
||||
import { createDeployment, updateDeploymentStatus } from "./deployment";
|
||||
import { findAdmin } from "./admin";
|
||||
import { createTraefikConfig } from "@/server/utils/traefik/application";
|
||||
import { docker } from "@/server/constants";
|
||||
import { getAdvancedStats } from "@/server/monitoring/utilts";
|
||||
export type Application = typeof applications.$inferSelect;
|
||||
|
||||
export const createApplication = async (
|
||||
input: typeof apiCreateApplication._type,
|
||||
) => {
|
||||
return await db.transaction(async (tx) => {
|
||||
const newApplication = await tx
|
||||
.insert(applications)
|
||||
.values({
|
||||
...input,
|
||||
})
|
||||
.returning()
|
||||
.then((value) => value[0]);
|
||||
|
||||
if (!newApplication) {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: "Error to create the application",
|
||||
});
|
||||
}
|
||||
|
||||
createTraefikConfig(newApplication.appName);
|
||||
|
||||
if (process.env.NODE_ENV === "development") {
|
||||
await tx.insert(domains).values({
|
||||
applicationId: newApplication.applicationId,
|
||||
host: `${newApplication.appName}.docker.localhost`,
|
||||
port: process.env.NODE_ENV === "development" ? 3000 : 80,
|
||||
certificateType: "none",
|
||||
});
|
||||
}
|
||||
|
||||
return newApplication;
|
||||
});
|
||||
};
|
||||
|
||||
export const findApplicationById = async (applicationId: string) => {
|
||||
const application = await db.query.applications.findFirst({
|
||||
where: eq(applications.applicationId, applicationId),
|
||||
with: {
|
||||
project: true,
|
||||
domains: true,
|
||||
deployments: true,
|
||||
mounts: true,
|
||||
redirects: true,
|
||||
security: true,
|
||||
ports: true,
|
||||
},
|
||||
});
|
||||
if (!application) {
|
||||
throw new TRPCError({
|
||||
code: "NOT_FOUND",
|
||||
message: "Application not found",
|
||||
});
|
||||
}
|
||||
return application;
|
||||
};
|
||||
|
||||
export const findApplicationByName = async (appName: string) => {
|
||||
const application = await db.query.applications.findFirst({
|
||||
where: eq(applications.appName, appName),
|
||||
});
|
||||
|
||||
return application;
|
||||
};
|
||||
|
||||
export const updateApplication = async (
|
||||
applicationId: string,
|
||||
applicationData: Partial<Application>,
|
||||
) => {
|
||||
const application = await db
|
||||
.update(applications)
|
||||
.set({
|
||||
...applicationData,
|
||||
})
|
||||
.where(eq(applications.applicationId, applicationId))
|
||||
.returning();
|
||||
|
||||
return application[0];
|
||||
};
|
||||
|
||||
export const updateApplicationStatus = async (
|
||||
applicationId: string,
|
||||
applicationStatus: Application["applicationStatus"],
|
||||
) => {
|
||||
const application = await db
|
||||
.update(applications)
|
||||
.set({
|
||||
applicationStatus: applicationStatus,
|
||||
})
|
||||
.where(eq(applications.applicationId, applicationId))
|
||||
.returning();
|
||||
|
||||
return application;
|
||||
};
|
||||
|
||||
export const deployApplication = async ({
|
||||
applicationId,
|
||||
titleLog = "Manual deployment",
|
||||
}: {
|
||||
applicationId: string;
|
||||
titleLog: string;
|
||||
}) => {
|
||||
const application = await findApplicationById(applicationId);
|
||||
const admin = await findAdmin();
|
||||
const deployment = await createDeployment({
|
||||
applicationId: applicationId,
|
||||
title: titleLog,
|
||||
});
|
||||
|
||||
try {
|
||||
if (application.sourceType === "github") {
|
||||
await cloneGithubRepository(admin, application, deployment.logPath);
|
||||
await buildApplication(application, deployment.logPath);
|
||||
} else if (application.sourceType === "docker") {
|
||||
await buildDocker(application, deployment.logPath);
|
||||
} else if (application.sourceType === "git") {
|
||||
await cloneGitRepository(application, deployment.logPath);
|
||||
await buildApplication(application, deployment.logPath);
|
||||
}
|
||||
await updateDeploymentStatus(deployment.deploymentId, "done");
|
||||
await updateApplicationStatus(applicationId, "done");
|
||||
} catch (error) {
|
||||
await updateDeploymentStatus(deployment.deploymentId, "error");
|
||||
await updateApplicationStatus(applicationId, "error");
|
||||
console.log(
|
||||
"Error on ",
|
||||
application.buildType,
|
||||
"/",
|
||||
application.sourceType,
|
||||
error,
|
||||
);
|
||||
|
||||
throw error;
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
export const rebuildApplication = async ({
|
||||
applicationId,
|
||||
titleLog = "Rebuild deployment",
|
||||
}: {
|
||||
applicationId: string;
|
||||
titleLog: string;
|
||||
}) => {
|
||||
const application = await findApplicationById(applicationId);
|
||||
const deployment = await createDeployment({
|
||||
applicationId: applicationId,
|
||||
title: titleLog,
|
||||
});
|
||||
|
||||
try {
|
||||
if (application.sourceType === "github") {
|
||||
await buildApplication(application, deployment.logPath);
|
||||
} else if (application.sourceType === "docker") {
|
||||
await buildDocker(application, deployment.logPath);
|
||||
} else if (application.sourceType === "git") {
|
||||
await buildApplication(application, deployment.logPath);
|
||||
}
|
||||
await updateDeploymentStatus(deployment.deploymentId, "done");
|
||||
await updateApplicationStatus(applicationId, "done");
|
||||
} catch (error) {
|
||||
await updateDeploymentStatus(deployment.deploymentId, "error");
|
||||
await updateApplicationStatus(applicationId, "error");
|
||||
throw error;
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
export const getApplicationStats = async (appName: string) => {
|
||||
const filter = {
|
||||
status: ["running"],
|
||||
label: [`com.docker.swarm.service.name=${appName}`],
|
||||
};
|
||||
|
||||
const containers = await docker.listContainers({
|
||||
filters: JSON.stringify(filter),
|
||||
});
|
||||
|
||||
const container = containers[0];
|
||||
if (!container || container?.State !== "running") {
|
||||
return null;
|
||||
}
|
||||
|
||||
const data = await getAdvancedStats(appName);
|
||||
|
||||
return data;
|
||||
};
|
||||
180
server/api/services/auth.ts
Normal file
180
server/api/services/auth.ts
Normal file
@@ -0,0 +1,180 @@
|
||||
import { db } from "@/server/db";
|
||||
import {
|
||||
admins,
|
||||
type apiCreateAdmin,
|
||||
type apiCreateUser,
|
||||
auth,
|
||||
users,
|
||||
} from "@/server/db/schema";
|
||||
import { TRPCError } from "@trpc/server";
|
||||
import * as bcrypt from "bcrypt";
|
||||
import { getPublicIpWithFallback } from "@/server/wss/terminal";
|
||||
import { eq } from "drizzle-orm";
|
||||
import { randomBytes } from "node:crypto";
|
||||
import encode from "hi-base32";
|
||||
import { TOTP } from "otpauth";
|
||||
import QRCode from "qrcode";
|
||||
|
||||
export type Auth = typeof auth.$inferSelect;
|
||||
|
||||
export const createAdmin = async (input: typeof apiCreateAdmin._type) => {
|
||||
return await db.transaction(async (tx) => {
|
||||
const hashedPassword = bcrypt.hashSync(input.password, 10);
|
||||
const newAuth = await tx
|
||||
.insert(auth)
|
||||
.values({
|
||||
email: input.email,
|
||||
password: hashedPassword,
|
||||
rol: "admin",
|
||||
})
|
||||
.returning()
|
||||
.then((res) => res[0]);
|
||||
|
||||
if (!newAuth) {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: "Error to create the user",
|
||||
});
|
||||
}
|
||||
|
||||
await tx
|
||||
.insert(admins)
|
||||
.values({
|
||||
authId: newAuth.id,
|
||||
serverIp: await getPublicIpWithFallback(),
|
||||
})
|
||||
.returning();
|
||||
|
||||
return newAuth;
|
||||
});
|
||||
};
|
||||
|
||||
export const createUser = async (input: typeof apiCreateUser._type) => {
|
||||
return await db.transaction(async (tx) => {
|
||||
const hashedPassword = bcrypt.hashSync(input.password, 10);
|
||||
const res = await tx
|
||||
.update(auth)
|
||||
.set({
|
||||
password: hashedPassword,
|
||||
})
|
||||
.where(eq(auth.id, input.id))
|
||||
.returning()
|
||||
.then((res) => res[0]);
|
||||
|
||||
if (!res) {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: "Error to create the user",
|
||||
});
|
||||
}
|
||||
|
||||
const user = await tx
|
||||
.update(users)
|
||||
.set({
|
||||
isRegistered: true,
|
||||
expirationDate: new Date().toISOString(),
|
||||
})
|
||||
.where(eq(users.token, input.token))
|
||||
.returning()
|
||||
.then((res) => res[0]);
|
||||
|
||||
return user;
|
||||
});
|
||||
};
|
||||
|
||||
export const findAuthByEmail = async (email: string) => {
|
||||
const result = await db.query.auth.findFirst({
|
||||
where: eq(auth.email, email),
|
||||
});
|
||||
if (!result) {
|
||||
throw new TRPCError({
|
||||
code: "NOT_FOUND",
|
||||
message: "Auth not found",
|
||||
});
|
||||
}
|
||||
return result;
|
||||
};
|
||||
|
||||
export const findAuthById = async (authId: string) => {
|
||||
const result = await db.query.auth.findFirst({
|
||||
where: eq(auth.id, authId),
|
||||
columns: {
|
||||
password: false,
|
||||
},
|
||||
});
|
||||
if (!result) {
|
||||
throw new TRPCError({
|
||||
code: "NOT_FOUND",
|
||||
message: "Auth not found",
|
||||
});
|
||||
}
|
||||
return result;
|
||||
};
|
||||
|
||||
export const updateAuthById = async (
|
||||
authId: string,
|
||||
authData: Partial<Auth>,
|
||||
) => {
|
||||
const result = await db
|
||||
.update(auth)
|
||||
.set({
|
||||
...authData,
|
||||
})
|
||||
.where(eq(auth.id, authId))
|
||||
.returning();
|
||||
|
||||
return result[0];
|
||||
};
|
||||
|
||||
export const generate2FASecret = async (authId: string) => {
|
||||
const auth = await findAuthById(authId);
|
||||
|
||||
const base32_secret = generateBase32Secret();
|
||||
|
||||
const totp = new TOTP({
|
||||
issuer: "Dokploy",
|
||||
label: `${auth?.email}`,
|
||||
algorithm: "SHA1",
|
||||
digits: 6,
|
||||
secret: base32_secret,
|
||||
});
|
||||
|
||||
const otpauth_url = totp.toString();
|
||||
|
||||
const qrUrl = await QRCode.toDataURL(otpauth_url);
|
||||
|
||||
return {
|
||||
qrCodeUrl: qrUrl,
|
||||
secret: base32_secret,
|
||||
};
|
||||
};
|
||||
|
||||
export const verify2FA = async (
|
||||
auth: Omit<Auth, "password">,
|
||||
secret: string,
|
||||
pin: string,
|
||||
) => {
|
||||
const totp = new TOTP({
|
||||
issuer: "Dokploy",
|
||||
label: `${auth?.email}`,
|
||||
algorithm: "SHA1",
|
||||
digits: 6,
|
||||
secret: secret,
|
||||
});
|
||||
|
||||
const delta = totp.validate({ token: pin });
|
||||
|
||||
if (delta === null) {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: "Invalid 2FA code",
|
||||
});
|
||||
}
|
||||
return auth;
|
||||
};
|
||||
|
||||
const generateBase32Secret = () => {
|
||||
const buffer = randomBytes(15);
|
||||
const base32 = encode.encode(buffer).replace(/=/g, "").substring(0, 24);
|
||||
return base32;
|
||||
};
|
||||
71
server/api/services/backup.ts
Normal file
71
server/api/services/backup.ts
Normal file
@@ -0,0 +1,71 @@
|
||||
import { db } from "@/server/db";
|
||||
import { type apiCreateBackup, backups } from "@/server/db/schema";
|
||||
import { TRPCError } from "@trpc/server";
|
||||
import { eq } from "drizzle-orm";
|
||||
|
||||
export type Backup = typeof backups.$inferSelect;
|
||||
|
||||
export type BackupSchedule = Awaited<ReturnType<typeof findBackupById>>;
|
||||
|
||||
export const createBackup = async (input: typeof apiCreateBackup._type) => {
|
||||
const newBackup = await db
|
||||
.insert(backups)
|
||||
.values({
|
||||
...input,
|
||||
})
|
||||
.returning()
|
||||
.then((value) => value[0]);
|
||||
|
||||
if (!newBackup) {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: "Error to create the Backup",
|
||||
});
|
||||
}
|
||||
|
||||
return newBackup;
|
||||
};
|
||||
|
||||
export const findBackupById = async (backupId: string) => {
|
||||
const backup = await db.query.backups.findFirst({
|
||||
where: eq(backups.backupId, backupId),
|
||||
with: {
|
||||
postgres: true,
|
||||
mysql: true,
|
||||
mariadb: true,
|
||||
mongo: true,
|
||||
destination: true,
|
||||
},
|
||||
});
|
||||
if (!backup) {
|
||||
throw new TRPCError({
|
||||
code: "NOT_FOUND",
|
||||
message: "Backup not found",
|
||||
});
|
||||
}
|
||||
return backup;
|
||||
};
|
||||
|
||||
export const updateBackupById = async (
|
||||
backupId: string,
|
||||
backupData: Partial<Backup>,
|
||||
) => {
|
||||
const result = await db
|
||||
.update(backups)
|
||||
.set({
|
||||
...backupData,
|
||||
})
|
||||
.where(eq(backups.backupId, backupId))
|
||||
.returning();
|
||||
|
||||
return result[0];
|
||||
};
|
||||
|
||||
export const removeBackupById = async (backupId: string) => {
|
||||
const result = await db
|
||||
.delete(backups)
|
||||
.where(eq(backups.backupId, backupId))
|
||||
.returning();
|
||||
|
||||
return result[0];
|
||||
};
|
||||
106
server/api/services/certificate.ts
Normal file
106
server/api/services/certificate.ts
Normal file
@@ -0,0 +1,106 @@
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { CERTIFICATES_PATH } from "@/server/constants";
|
||||
import { db } from "@/server/db";
|
||||
import { type apiCreateCertificate, certificates } from "@/server/db/schema";
|
||||
import { TRPCError } from "@trpc/server";
|
||||
import { eq } from "drizzle-orm";
|
||||
import type { z } from "zod";
|
||||
import { dump } from "js-yaml";
|
||||
import { removeDirectoryIfExistsContent } from "@/server/utils/filesystem/directory";
|
||||
|
||||
export type Certificate = typeof certificates.$inferSelect;
|
||||
|
||||
export const findCertificateById = async (certificateId: string) => {
|
||||
const certificate = await db.query.certificates.findFirst({
|
||||
where: eq(certificates.certificateId, certificateId),
|
||||
});
|
||||
|
||||
if (!certificate) {
|
||||
throw new TRPCError({
|
||||
code: "NOT_FOUND",
|
||||
message: "Certificate not found",
|
||||
});
|
||||
}
|
||||
|
||||
return certificate;
|
||||
};
|
||||
|
||||
export const createCertificate = async (
|
||||
certificateData: z.infer<typeof apiCreateCertificate>,
|
||||
) => {
|
||||
const certificate = await db
|
||||
.insert(certificates)
|
||||
.values({
|
||||
...certificateData,
|
||||
})
|
||||
.returning();
|
||||
|
||||
if (!certificate || certificate[0] === undefined) {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: "Failed to create the certificate",
|
||||
});
|
||||
}
|
||||
|
||||
const cer = certificate[0];
|
||||
|
||||
createCertificateFiles(cer);
|
||||
return cer;
|
||||
};
|
||||
|
||||
export const removeCertificateById = async (certificateId: string) => {
|
||||
const certificate = await findCertificateById(certificateId);
|
||||
const certDir = path.join(CERTIFICATES_PATH, certificate.certificatePath);
|
||||
|
||||
await removeDirectoryIfExistsContent(certDir);
|
||||
const result = await db
|
||||
.delete(certificates)
|
||||
.where(eq(certificates.certificateId, certificateId))
|
||||
.returning();
|
||||
|
||||
if (!result) {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: "Failed to delete the certificate",
|
||||
});
|
||||
}
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
export const findCertificates = async () => {
|
||||
return await db.query.certificates.findMany();
|
||||
};
|
||||
|
||||
const createCertificateFiles = (certificate: Certificate) => {
|
||||
const dockerPath = "/etc/traefik";
|
||||
const certDir = path.join(CERTIFICATES_PATH, certificate.certificatePath);
|
||||
const crtPath = path.join(certDir, "chain.crt");
|
||||
const keyPath = path.join(certDir, "privkey.key");
|
||||
|
||||
const chainPath = path.join(dockerPath, certDir, "chain.crt");
|
||||
const keyPathDocker = path.join(dockerPath, certDir, "privkey.key");
|
||||
|
||||
if (!fs.existsSync(certDir)) {
|
||||
fs.mkdirSync(certDir, { recursive: true });
|
||||
}
|
||||
|
||||
fs.writeFileSync(crtPath, certificate.certificateData);
|
||||
fs.writeFileSync(keyPath, certificate.privateKey);
|
||||
|
||||
const traefikConfig = {
|
||||
tls: {
|
||||
certificates: [
|
||||
{
|
||||
certFile: chainPath,
|
||||
keyFile: keyPathDocker,
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
const yamlConfig = dump(traefikConfig);
|
||||
const configFile = path.join(certDir, "certificate.yml");
|
||||
fs.writeFileSync(configFile, yamlConfig);
|
||||
};
|
||||
157
server/api/services/deployment.ts
Normal file
157
server/api/services/deployment.ts
Normal file
@@ -0,0 +1,157 @@
|
||||
import { existsSync, promises as fsPromises } from "node:fs";
|
||||
import path from "node:path";
|
||||
import { LOGS_PATH } from "@/server/constants";
|
||||
import { db } from "@/server/db";
|
||||
import { deployments } from "@/server/db/schema";
|
||||
import { removeDirectoryIfExistsContent } from "@/server/utils/filesystem/directory";
|
||||
import { TRPCError } from "@trpc/server";
|
||||
import { format } from "date-fns";
|
||||
import { desc, eq } from "drizzle-orm";
|
||||
import { type Application, findApplicationById } from "./application";
|
||||
|
||||
export type Deployment = typeof deployments.$inferSelect;
|
||||
type CreateDeploymentInput = Omit<
|
||||
Deployment,
|
||||
"deploymentId" | "createdAt" | "status" | "logPath"
|
||||
>;
|
||||
|
||||
export const findDeploymentById = async (applicationId: string) => {
|
||||
const application = await db.query.deployments.findFirst({
|
||||
where: eq(deployments.applicationId, applicationId),
|
||||
with: {
|
||||
application: true,
|
||||
},
|
||||
});
|
||||
if (!application) {
|
||||
throw new TRPCError({
|
||||
code: "NOT_FOUND",
|
||||
message: "Deployment not found",
|
||||
});
|
||||
}
|
||||
return application;
|
||||
};
|
||||
|
||||
export const createDeployment = async (deployment: CreateDeploymentInput) => {
|
||||
try {
|
||||
const application = await findApplicationById(deployment.applicationId);
|
||||
|
||||
await removeLastTenDeployments(deployment.applicationId);
|
||||
const formattedDateTime = format(new Date(), "yyyy-MM-dd:HH:mm:ss");
|
||||
const fileName = `${application.appName}-${formattedDateTime}.log`;
|
||||
const logFilePath = path.join(LOGS_PATH, application.appName, fileName);
|
||||
await fsPromises.mkdir(path.join(LOGS_PATH, application.appName), {
|
||||
recursive: true,
|
||||
});
|
||||
await fsPromises.writeFile(logFilePath, "Initializing deployment");
|
||||
const deploymentCreate = await db
|
||||
.insert(deployments)
|
||||
.values({
|
||||
applicationId: deployment.applicationId,
|
||||
title: deployment.title || "Deployment",
|
||||
status: "running",
|
||||
logPath: logFilePath,
|
||||
})
|
||||
.returning();
|
||||
if (deploymentCreate.length === 0 || !deploymentCreate[0]) {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: "Error to create the deployment",
|
||||
});
|
||||
}
|
||||
return deploymentCreate[0];
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: "Error to create the deployment",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
export const removeDeployment = async (deploymentId: string) => {
|
||||
try {
|
||||
const deployment = await db
|
||||
.delete(deployments)
|
||||
.where(eq(deployments.deploymentId, deploymentId))
|
||||
.returning();
|
||||
return deployment[0];
|
||||
} catch (error) {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: "Error to delete this deployment",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
export const removeDeploymentsByApplicationId = async (
|
||||
applicationId: string,
|
||||
) => {
|
||||
await db
|
||||
.delete(deployments)
|
||||
.where(eq(deployments.applicationId, applicationId))
|
||||
.returning();
|
||||
};
|
||||
|
||||
const removeLastTenDeployments = async (applicationId: string) => {
|
||||
const deploymentList = await db.query.deployments.findMany({
|
||||
where: eq(deployments.applicationId, applicationId),
|
||||
orderBy: desc(deployments.createdAt),
|
||||
});
|
||||
if (deploymentList.length > 10) {
|
||||
const deploymentsToDelete = deploymentList.slice(10);
|
||||
for (const oldDeployment of deploymentsToDelete) {
|
||||
const logPath = path.join(oldDeployment.logPath);
|
||||
if (existsSync(logPath)) {
|
||||
await fsPromises.unlink(logPath);
|
||||
}
|
||||
await removeDeployment(oldDeployment.deploymentId);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export const removeDeployments = async (application: Application) => {
|
||||
const { appName, applicationId } = application;
|
||||
const logsPath = path.join(LOGS_PATH, appName);
|
||||
await removeDirectoryIfExistsContent(logsPath);
|
||||
await removeDeploymentsByApplicationId(applicationId);
|
||||
};
|
||||
|
||||
export const findAllDeploymentsByApplicationId = async (
|
||||
applicationId: string,
|
||||
) => {
|
||||
const deploymentsList = await db.query.deployments.findMany({
|
||||
where: eq(deployments.applicationId, applicationId),
|
||||
orderBy: desc(deployments.createdAt),
|
||||
});
|
||||
return deploymentsList;
|
||||
};
|
||||
|
||||
export const updateDeployment = async (
|
||||
deploymentId: string,
|
||||
deploymentData: Partial<Deployment>,
|
||||
) => {
|
||||
const application = await db
|
||||
.update(deployments)
|
||||
.set({
|
||||
...deploymentData,
|
||||
})
|
||||
.where(eq(deployments.deploymentId, deploymentId))
|
||||
.returning();
|
||||
|
||||
return application;
|
||||
};
|
||||
|
||||
export const updateDeploymentStatus = async (
|
||||
deploymentId: string,
|
||||
deploymentStatus: Deployment["status"],
|
||||
) => {
|
||||
const application = await db
|
||||
.update(deployments)
|
||||
.set({
|
||||
status: deploymentStatus,
|
||||
})
|
||||
.where(eq(deployments.deploymentId, deploymentId))
|
||||
.returning();
|
||||
|
||||
return application;
|
||||
};
|
||||
67
server/api/services/destination.ts
Normal file
67
server/api/services/destination.ts
Normal file
@@ -0,0 +1,67 @@
|
||||
import { db } from "@/server/db";
|
||||
import { type apiCreateDestination, destinations } from "@/server/db/schema";
|
||||
import { TRPCError } from "@trpc/server";
|
||||
import { eq } from "drizzle-orm";
|
||||
import { findAdmin } from "./admin";
|
||||
|
||||
export type Destination = typeof destinations.$inferSelect;
|
||||
|
||||
export const createDestintation = async (
|
||||
input: typeof apiCreateDestination._type,
|
||||
) => {
|
||||
const adminResponse = await findAdmin();
|
||||
const newDestination = await db
|
||||
.insert(destinations)
|
||||
.values({
|
||||
...input,
|
||||
adminId: adminResponse.adminId,
|
||||
})
|
||||
.returning()
|
||||
.then((value) => value[0]);
|
||||
|
||||
if (!newDestination) {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: "Error input: Inserting destination",
|
||||
});
|
||||
}
|
||||
|
||||
return newDestination;
|
||||
};
|
||||
|
||||
export const findDestinationById = async (destinationId: string) => {
|
||||
const destination = await db.query.destinations.findFirst({
|
||||
where: eq(destinations.destinationId, destinationId),
|
||||
});
|
||||
if (!destination) {
|
||||
throw new TRPCError({
|
||||
code: "NOT_FOUND",
|
||||
message: "Destination not found",
|
||||
});
|
||||
}
|
||||
return destination;
|
||||
};
|
||||
|
||||
export const removeDestinationById = async (destinationId: string) => {
|
||||
const result = await db
|
||||
.delete(destinations)
|
||||
.where(eq(destinations.destinationId, destinationId))
|
||||
.returning();
|
||||
|
||||
return result[0];
|
||||
};
|
||||
|
||||
export const updateDestinationById = async (
|
||||
destinationId: string,
|
||||
destinationData: Partial<Destination>,
|
||||
) => {
|
||||
const result = await db
|
||||
.update(destinations)
|
||||
.set({
|
||||
...destinationData,
|
||||
})
|
||||
.where(eq(destinations.destinationId, destinationId))
|
||||
.returning();
|
||||
|
||||
return result[0];
|
||||
};
|
||||
153
server/api/services/docker.ts
Normal file
153
server/api/services/docker.ts
Normal file
@@ -0,0 +1,153 @@
|
||||
import { execAsync } from "@/server/utils/process/execAsync";
|
||||
|
||||
export const getContainers = async () => {
|
||||
try {
|
||||
const { stdout, stderr } = await execAsync(
|
||||
"docker ps -a --format 'CONTAINER ID : {{.ID}} | Name: {{.Names}} | Image: {{.Image}} | Ports: {{.Ports}} | State: {{.State}} | Status: {{.Status}}'",
|
||||
);
|
||||
|
||||
if (stderr) {
|
||||
console.error(`Error: ${stderr}`);
|
||||
return;
|
||||
}
|
||||
|
||||
const lines = stdout.trim().split("\n");
|
||||
|
||||
const containers = lines
|
||||
.map((line) => {
|
||||
const parts = line.split(" | ");
|
||||
const containerId = parts[0]
|
||||
? parts[0].replace("CONTAINER ID : ", "").trim()
|
||||
: "No container id";
|
||||
const name = parts[1]
|
||||
? parts[1].replace("Name: ", "").trim()
|
||||
: "No container name";
|
||||
const image = parts[2]
|
||||
? parts[2].replace("Image: ", "").trim()
|
||||
: "No image";
|
||||
const ports = parts[3]
|
||||
? parts[3].replace("Ports: ", "").trim()
|
||||
: "No ports";
|
||||
const state = parts[4]
|
||||
? parts[4].replace("State: ", "").trim()
|
||||
: "No state";
|
||||
const status = parts[5]
|
||||
? parts[5].replace("Status: ", "").trim()
|
||||
: "No status";
|
||||
return {
|
||||
containerId,
|
||||
name,
|
||||
image,
|
||||
ports,
|
||||
state,
|
||||
status,
|
||||
};
|
||||
})
|
||||
.filter((container) => !container.name.includes("dokploy"));
|
||||
|
||||
return containers;
|
||||
} catch (error) {
|
||||
console.error(`Execution error: ${error}`);
|
||||
}
|
||||
};
|
||||
|
||||
export const getConfig = async (containerId: string) => {
|
||||
try {
|
||||
const { stdout, stderr } = await execAsync(
|
||||
`docker inspect ${containerId} --format='{{json .}}'`,
|
||||
);
|
||||
|
||||
if (stderr) {
|
||||
console.error(`Error: ${stderr}`);
|
||||
return;
|
||||
}
|
||||
|
||||
const config = JSON.parse(stdout);
|
||||
|
||||
return config;
|
||||
} catch (error) {
|
||||
console.error(`Execution error: ${error}`);
|
||||
}
|
||||
};
|
||||
|
||||
export const getContainersByAppNameMatch = async (appName: string) => {
|
||||
try {
|
||||
const { stdout, stderr } = await execAsync(
|
||||
`docker ps -a --format 'CONTAINER ID : {{.ID}} | Name: {{.Names}} | State: {{.State}}' | grep ${appName}`,
|
||||
);
|
||||
|
||||
if (stderr) {
|
||||
console.error(`Error: ${stderr}`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!stdout) return [];
|
||||
|
||||
const lines = stdout.trim().split("\n");
|
||||
const containers = lines.map((line) => {
|
||||
const parts = line.split(" | ");
|
||||
const containerId = parts[0]
|
||||
? parts[0].replace("CONTAINER ID : ", "").trim()
|
||||
: "No container id";
|
||||
const name = parts[1]
|
||||
? parts[1].replace("Name: ", "").trim()
|
||||
: "No container name";
|
||||
|
||||
const state = parts[2]
|
||||
? parts[2].replace("State: ", "").trim()
|
||||
: "No state";
|
||||
return {
|
||||
containerId,
|
||||
name,
|
||||
state,
|
||||
};
|
||||
});
|
||||
|
||||
return containers || [];
|
||||
} catch (error) {
|
||||
console.error(`Execution error: ${error}`);
|
||||
}
|
||||
|
||||
return [];
|
||||
};
|
||||
|
||||
export const getContainersByAppLabel = async (appName: string) => {
|
||||
try {
|
||||
const { stdout, stderr } = await execAsync(
|
||||
`docker ps --filter "label=com.docker.swarm.service.name=${appName}" --format 'CONTAINER ID : {{.ID}} | Name: {{.Names}} | State: {{.State}}'`,
|
||||
);
|
||||
|
||||
if (stderr) {
|
||||
console.error(`Error: ${stderr}`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!stdout) return [];
|
||||
|
||||
const lines = stdout.trim().split("\n");
|
||||
|
||||
const containers = lines.map((line) => {
|
||||
const parts = line.split(" | ");
|
||||
const containerId = parts[0]
|
||||
? parts[0].replace("CONTAINER ID : ", "").trim()
|
||||
: "No container id";
|
||||
const name = parts[1]
|
||||
? parts[1].replace("Name: ", "").trim()
|
||||
: "No container name";
|
||||
const state = parts[2]
|
||||
? parts[2].replace("State: ", "").trim()
|
||||
: "No state";
|
||||
return {
|
||||
containerId,
|
||||
name,
|
||||
state,
|
||||
};
|
||||
});
|
||||
|
||||
return containers || [];
|
||||
} catch (error) {
|
||||
console.error(`Execution error: ${error}`);
|
||||
}
|
||||
|
||||
return [];
|
||||
};
|
||||
83
server/api/services/domain.ts
Normal file
83
server/api/services/domain.ts
Normal file
@@ -0,0 +1,83 @@
|
||||
import { db } from "@/server/db";
|
||||
import { type apiCreateDomain, domains } from "@/server/db/schema";
|
||||
import { TRPCError } from "@trpc/server";
|
||||
import { eq } from "drizzle-orm";
|
||||
import { findApplicationById } from "./application";
|
||||
import { manageDomain } from "@/server/utils/traefik/domain";
|
||||
|
||||
export type Domain = typeof domains.$inferSelect;
|
||||
|
||||
export const createDomain = async (input: typeof apiCreateDomain._type) => {
|
||||
await db.transaction(async (tx) => {
|
||||
const application = await findApplicationById(input.applicationId);
|
||||
|
||||
const domain = await tx
|
||||
.insert(domains)
|
||||
.values({
|
||||
...input,
|
||||
})
|
||||
.returning()
|
||||
.then((response) => response[0]);
|
||||
|
||||
if (!domain) {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: "Error to create the domain",
|
||||
});
|
||||
}
|
||||
|
||||
await manageDomain(application, domain);
|
||||
});
|
||||
};
|
||||
export const findDomainById = async (domainId: string) => {
|
||||
const domain = await db.query.domains.findFirst({
|
||||
where: eq(domains.domainId, domainId),
|
||||
with: {
|
||||
application: true,
|
||||
},
|
||||
});
|
||||
if (!domain) {
|
||||
throw new TRPCError({
|
||||
code: "NOT_FOUND",
|
||||
message: "Domain not found",
|
||||
});
|
||||
}
|
||||
return domain;
|
||||
};
|
||||
|
||||
export const findDomainsByApplicationId = async (applicationId: string) => {
|
||||
const domainsArray = await db.query.domains.findMany({
|
||||
where: eq(domains.applicationId, applicationId),
|
||||
with: {
|
||||
application: true,
|
||||
},
|
||||
});
|
||||
|
||||
return domainsArray;
|
||||
};
|
||||
|
||||
export const updateDomainById = async (
|
||||
domainId: string,
|
||||
domainData: Partial<Domain>,
|
||||
) => {
|
||||
const domain = await db
|
||||
.update(domains)
|
||||
.set({
|
||||
...domainData,
|
||||
})
|
||||
.where(eq(domains.domainId, domainId))
|
||||
.returning();
|
||||
|
||||
return domain[0];
|
||||
};
|
||||
|
||||
export const removeDomainById = async (domainId: string) => {
|
||||
await findDomainById(domainId);
|
||||
// TODO: fix order
|
||||
const result = await db
|
||||
.delete(domains)
|
||||
.where(eq(domains.domainId, domainId))
|
||||
.returning();
|
||||
|
||||
return result[0];
|
||||
};
|
||||
121
server/api/services/mariadb.ts
Normal file
121
server/api/services/mariadb.ts
Normal file
@@ -0,0 +1,121 @@
|
||||
import { generateRandomPassword } from "@/server/auth/random-password";
|
||||
import { db } from "@/server/db";
|
||||
import { type apiCreateMariaDB, backups, mariadb } from "@/server/db/schema";
|
||||
import { buildMariadb } from "@/server/utils/databases/mariadb";
|
||||
import { pullImage } from "@/server/utils/docker/utils";
|
||||
import { TRPCError } from "@trpc/server";
|
||||
import { eq, getTableColumns } from "drizzle-orm";
|
||||
|
||||
export type Mariadb = typeof mariadb.$inferSelect;
|
||||
|
||||
export const createMariadb = async (input: typeof apiCreateMariaDB._type) => {
|
||||
const newMariadb = await db
|
||||
.insert(mariadb)
|
||||
.values({
|
||||
...input,
|
||||
databasePassword: input.databasePassword
|
||||
? input.databasePassword
|
||||
: (await generateRandomPassword()).randomPassword,
|
||||
databaseRootPassword: input.databaseRootPassword
|
||||
? input.databaseRootPassword
|
||||
: (await generateRandomPassword()).randomPassword,
|
||||
})
|
||||
.returning()
|
||||
.then((value) => value[0]);
|
||||
|
||||
if (!newMariadb) {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: "Error input: Inserting mariadb database",
|
||||
});
|
||||
}
|
||||
|
||||
return newMariadb;
|
||||
};
|
||||
|
||||
// https://github.com/drizzle-team/drizzle-orm/discussions/1483#discussioncomment-7523881
|
||||
export const findMariadbById = async (mariadbId: string) => {
|
||||
const result = await db.query.mariadb.findFirst({
|
||||
where: eq(mariadb.mariadbId, mariadbId),
|
||||
with: {
|
||||
project: true,
|
||||
mounts: true,
|
||||
backups: {
|
||||
with: {
|
||||
destination: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
if (!result) {
|
||||
throw new TRPCError({
|
||||
code: "NOT_FOUND",
|
||||
message: "Mariadb not found",
|
||||
});
|
||||
}
|
||||
return result;
|
||||
};
|
||||
|
||||
export const updateMariadbById = async (
|
||||
mariadbId: string,
|
||||
mariadbData: Partial<Mariadb>,
|
||||
) => {
|
||||
const result = await db
|
||||
.update(mariadb)
|
||||
.set({
|
||||
...mariadbData,
|
||||
})
|
||||
.where(eq(mariadb.mariadbId, mariadbId))
|
||||
.returning();
|
||||
|
||||
return result[0];
|
||||
};
|
||||
|
||||
export const removeMariadbById = async (mariadbId: string) => {
|
||||
const result = await db
|
||||
.delete(mariadb)
|
||||
.where(eq(mariadb.mariadbId, mariadbId))
|
||||
.returning();
|
||||
|
||||
return result[0];
|
||||
};
|
||||
|
||||
export const findMariadbByBackupId = async (backupId: string) => {
|
||||
const result = await db
|
||||
.select({
|
||||
...getTableColumns(mariadb),
|
||||
})
|
||||
.from(mariadb)
|
||||
.innerJoin(backups, eq(mariadb.mariadbId, backups.mariadbId))
|
||||
.where(eq(backups.backupId, backupId))
|
||||
.limit(1);
|
||||
|
||||
if (!result || !result[0]) {
|
||||
throw new TRPCError({
|
||||
code: "NOT_FOUND",
|
||||
message: "MariaDB not found",
|
||||
});
|
||||
}
|
||||
return result[0];
|
||||
};
|
||||
|
||||
export const deployMariadb = async (mariadbId: string) => {
|
||||
const mariadb = await findMariadbById(mariadbId);
|
||||
try {
|
||||
await pullImage(mariadb.dockerImage);
|
||||
await buildMariadb(mariadb);
|
||||
await updateMariadbById(mariadbId, {
|
||||
applicationStatus: "done",
|
||||
});
|
||||
} catch (error) {
|
||||
await updateMariadbById(mariadbId, {
|
||||
applicationStatus: "error",
|
||||
});
|
||||
|
||||
throw new TRPCError({
|
||||
code: "INTERNAL_SERVER_ERROR",
|
||||
message: `Error on deploy mariadb${error}`,
|
||||
});
|
||||
}
|
||||
return mariadb;
|
||||
};
|
||||
117
server/api/services/mongo.ts
Normal file
117
server/api/services/mongo.ts
Normal file
@@ -0,0 +1,117 @@
|
||||
import { generateRandomPassword } from "@/server/auth/random-password";
|
||||
import { db } from "@/server/db";
|
||||
import { type apiCreateMongo, backups, mongo } from "@/server/db/schema";
|
||||
import { buildMongo } from "@/server/utils/databases/mongo";
|
||||
import { pullImage } from "@/server/utils/docker/utils";
|
||||
import { TRPCError } from "@trpc/server";
|
||||
import { eq, getTableColumns } from "drizzle-orm";
|
||||
|
||||
export type Mongo = typeof mongo.$inferSelect;
|
||||
|
||||
export const createMongo = async (input: typeof apiCreateMongo._type) => {
|
||||
const newMongo = await db
|
||||
.insert(mongo)
|
||||
.values({
|
||||
...input,
|
||||
databasePassword: input.databasePassword
|
||||
? input.databasePassword
|
||||
: (await generateRandomPassword()).randomPassword,
|
||||
})
|
||||
.returning()
|
||||
.then((value) => value[0]);
|
||||
|
||||
if (!newMongo) {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: "Error input: Inserting mongo database",
|
||||
});
|
||||
}
|
||||
|
||||
return newMongo;
|
||||
};
|
||||
|
||||
export const findMongoById = async (mongoId: string) => {
|
||||
const result = await db.query.mongo.findFirst({
|
||||
where: eq(mongo.mongoId, mongoId),
|
||||
with: {
|
||||
project: true,
|
||||
mounts: true,
|
||||
backups: {
|
||||
with: {
|
||||
destination: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
if (!result) {
|
||||
throw new TRPCError({
|
||||
code: "NOT_FOUND",
|
||||
message: "Mongo not found",
|
||||
});
|
||||
}
|
||||
return result;
|
||||
};
|
||||
|
||||
export const updateMongoById = async (
|
||||
mongoId: string,
|
||||
postgresData: Partial<Mongo>,
|
||||
) => {
|
||||
const result = await db
|
||||
.update(mongo)
|
||||
.set({
|
||||
...postgresData,
|
||||
})
|
||||
.where(eq(mongo.mongoId, mongoId))
|
||||
.returning();
|
||||
|
||||
return result[0];
|
||||
};
|
||||
|
||||
export const findMongoByBackupId = async (backupId: string) => {
|
||||
const result = await db
|
||||
.select({
|
||||
...getTableColumns(mongo),
|
||||
})
|
||||
.from(mongo)
|
||||
.innerJoin(backups, eq(mongo.mongoId, backups.mongoId))
|
||||
.where(eq(backups.backupId, backupId))
|
||||
.limit(1);
|
||||
|
||||
if (!result || !result[0]) {
|
||||
throw new TRPCError({
|
||||
code: "NOT_FOUND",
|
||||
message: "Mongo not found",
|
||||
});
|
||||
}
|
||||
return result[0];
|
||||
};
|
||||
|
||||
export const removeMongoById = async (mongoId: string) => {
|
||||
const result = await db
|
||||
.delete(mongo)
|
||||
.where(eq(mongo.mongoId, mongoId))
|
||||
.returning();
|
||||
|
||||
return result[0];
|
||||
};
|
||||
|
||||
export const deployMongo = async (mongoId: string) => {
|
||||
const mongo = await findMongoById(mongoId);
|
||||
try {
|
||||
await pullImage(mongo.dockerImage);
|
||||
await buildMongo(mongo);
|
||||
await updateMongoById(mongoId, {
|
||||
applicationStatus: "done",
|
||||
});
|
||||
} catch (error) {
|
||||
await updateMongoById(mongoId, {
|
||||
applicationStatus: "error",
|
||||
});
|
||||
|
||||
throw new TRPCError({
|
||||
code: "INTERNAL_SERVER_ERROR",
|
||||
message: `Error on deploy mongo${error}`,
|
||||
});
|
||||
}
|
||||
return mongo;
|
||||
};
|
||||
174
server/api/services/mount.ts
Normal file
174
server/api/services/mount.ts
Normal file
@@ -0,0 +1,174 @@
|
||||
import { unlink } from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import { APPLICATIONS_PATH } from "@/server/constants";
|
||||
import { db } from "@/server/db";
|
||||
import {
|
||||
type apiCreateMount,
|
||||
mounts,
|
||||
type ServiceType,
|
||||
} from "@/server/db/schema";
|
||||
import { TRPCError } from "@trpc/server";
|
||||
import { eq, sql, type SQL } from "drizzle-orm";
|
||||
|
||||
export type Mount = typeof mounts.$inferSelect;
|
||||
|
||||
export const createMount = async (input: typeof apiCreateMount._type) => {
|
||||
try {
|
||||
const { serviceId, ...rest } = input;
|
||||
const value = await db
|
||||
.insert(mounts)
|
||||
.values({
|
||||
...rest,
|
||||
...(input.serviceType === "application" && {
|
||||
applicationId: serviceId,
|
||||
}),
|
||||
...(input.serviceType === "postgres" && {
|
||||
postgresId: serviceId,
|
||||
}),
|
||||
...(input.serviceType === "mariadb" && {
|
||||
mariadbId: serviceId,
|
||||
}),
|
||||
...(input.serviceType === "mongo" && {
|
||||
mongoId: serviceId,
|
||||
}),
|
||||
...(input.serviceType === "mysql" && {
|
||||
mysqlId: serviceId,
|
||||
}),
|
||||
...(input.serviceType === "redis" && {
|
||||
redisId: serviceId,
|
||||
}),
|
||||
})
|
||||
.returning()
|
||||
.then((value) => value[0]);
|
||||
|
||||
if (!value) {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: "Error input: Inserting mount",
|
||||
});
|
||||
}
|
||||
return value;
|
||||
} catch (error) {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: "Error to create the mount",
|
||||
cause: error,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
export const findMountById = async (mountId: string) => {
|
||||
const mount = await db.query.mounts.findFirst({
|
||||
where: eq(mounts.mountId, mountId),
|
||||
with: {
|
||||
application: true,
|
||||
postgres: true,
|
||||
mariadb: true,
|
||||
mongo: true,
|
||||
mysql: true,
|
||||
redis: true,
|
||||
},
|
||||
});
|
||||
if (!mount) {
|
||||
throw new TRPCError({
|
||||
code: "NOT_FOUND",
|
||||
message: "Mount not found",
|
||||
});
|
||||
}
|
||||
return mount;
|
||||
};
|
||||
|
||||
export const updateMount = async (
|
||||
mountId: string,
|
||||
applicationData: Partial<Mount>,
|
||||
) => {
|
||||
const mount = await db
|
||||
.update(mounts)
|
||||
.set({
|
||||
...applicationData,
|
||||
})
|
||||
.where(eq(mounts.mountId, mountId))
|
||||
.returning();
|
||||
|
||||
return mount;
|
||||
};
|
||||
|
||||
export const findMountsByApplicationId = async (
|
||||
serviceId: string,
|
||||
serviceType: ServiceType,
|
||||
) => {
|
||||
const sqlChunks: SQL[] = [];
|
||||
|
||||
switch (serviceType) {
|
||||
case "application":
|
||||
sqlChunks.push(eq(mounts.applicationId, serviceId));
|
||||
break;
|
||||
case "postgres":
|
||||
sqlChunks.push(eq(mounts.postgresId, serviceId));
|
||||
break;
|
||||
case "mariadb":
|
||||
sqlChunks.push(eq(mounts.mariadbId, serviceId));
|
||||
break;
|
||||
case "mongo":
|
||||
sqlChunks.push(eq(mounts.mongoId, serviceId));
|
||||
break;
|
||||
case "mysql":
|
||||
sqlChunks.push(eq(mounts.mysqlId, serviceId));
|
||||
break;
|
||||
case "redis":
|
||||
sqlChunks.push(eq(mounts.redisId, serviceId));
|
||||
break;
|
||||
default:
|
||||
throw new Error(`Unknown service type: ${serviceType}`);
|
||||
}
|
||||
const mount = await db.query.mounts.findMany({
|
||||
where: sql.join(sqlChunks, sql.raw(" ")),
|
||||
});
|
||||
|
||||
return mount;
|
||||
};
|
||||
|
||||
export const deleteMount = async (mountId: string) => {
|
||||
const {
|
||||
type,
|
||||
mountPath,
|
||||
serviceType,
|
||||
application,
|
||||
mariadb,
|
||||
mongo,
|
||||
mysql,
|
||||
postgres,
|
||||
redis,
|
||||
} = await findMountById(mountId);
|
||||
|
||||
let appName = null;
|
||||
|
||||
if (serviceType === "application") {
|
||||
appName = application?.appName;
|
||||
} else if (serviceType === "postgres") {
|
||||
appName = postgres?.appName;
|
||||
} else if (serviceType === "mariadb") {
|
||||
appName = mariadb?.appName;
|
||||
} else if (serviceType === "mongo") {
|
||||
appName = mongo?.appName;
|
||||
} else if (serviceType === "mysql") {
|
||||
appName = mysql?.appName;
|
||||
} else if (serviceType === "redis") {
|
||||
appName = redis?.appName;
|
||||
}
|
||||
|
||||
if (type === "file" && appName) {
|
||||
const fileName = mountPath.split("/").pop() || "";
|
||||
const absoluteBasePath = path.resolve(APPLICATIONS_PATH);
|
||||
const filePath = path.join(absoluteBasePath, appName, "files", fileName);
|
||||
try {
|
||||
await unlink(filePath);
|
||||
} catch (error) {}
|
||||
}
|
||||
|
||||
const deletedMount = await db
|
||||
.delete(mounts)
|
||||
.where(eq(mounts.mountId, mountId))
|
||||
.returning();
|
||||
return deletedMount[0];
|
||||
};
|
||||
121
server/api/services/mysql.ts
Normal file
121
server/api/services/mysql.ts
Normal file
@@ -0,0 +1,121 @@
|
||||
import { generateRandomPassword } from "@/server/auth/random-password";
|
||||
import { db } from "@/server/db";
|
||||
import { type apiCreateMySql, backups, mysql } from "@/server/db/schema";
|
||||
import { buildMysql } from "@/server/utils/databases/mysql";
|
||||
import { pullImage } from "@/server/utils/docker/utils";
|
||||
import { TRPCError } from "@trpc/server";
|
||||
import { eq, getTableColumns } from "drizzle-orm";
|
||||
import { nanoid } from "nanoid";
|
||||
|
||||
export type MySql = typeof mysql.$inferSelect;
|
||||
|
||||
export const createMysql = async (input: typeof apiCreateMySql._type) => {
|
||||
const newMysql = await db
|
||||
.insert(mysql)
|
||||
.values({
|
||||
...input,
|
||||
databasePassword: input.databasePassword
|
||||
? input.databasePassword
|
||||
: (await generateRandomPassword()).randomPassword,
|
||||
databaseRootPassword: input.databaseRootPassword
|
||||
? input.databaseRootPassword
|
||||
: (await generateRandomPassword()).randomPassword,
|
||||
})
|
||||
.returning()
|
||||
.then((value) => value[0]);
|
||||
|
||||
if (!newMysql) {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: "Error input: Inserting mysql database",
|
||||
});
|
||||
}
|
||||
|
||||
return newMysql;
|
||||
};
|
||||
|
||||
// https://github.com/drizzle-team/drizzle-orm/discussions/1483#discussioncomment-7523881
|
||||
export const findMySqlById = async (mysqlId: string) => {
|
||||
const result = await db.query.mysql.findFirst({
|
||||
where: eq(mysql.mysqlId, mysqlId),
|
||||
with: {
|
||||
project: true,
|
||||
mounts: true,
|
||||
backups: {
|
||||
with: {
|
||||
destination: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
if (!result) {
|
||||
throw new TRPCError({
|
||||
code: "NOT_FOUND",
|
||||
message: "MySql not found",
|
||||
});
|
||||
}
|
||||
return result;
|
||||
};
|
||||
|
||||
export const updateMySqlById = async (
|
||||
mysqlId: string,
|
||||
mysqlData: Partial<MySql>,
|
||||
) => {
|
||||
const result = await db
|
||||
.update(mysql)
|
||||
.set({
|
||||
...mysqlData,
|
||||
})
|
||||
.where(eq(mysql.mysqlId, mysqlId))
|
||||
.returning();
|
||||
|
||||
return result[0];
|
||||
};
|
||||
|
||||
export const findMySqlByBackupId = async (backupId: string) => {
|
||||
const result = await db
|
||||
.select({
|
||||
...getTableColumns(mysql),
|
||||
})
|
||||
.from(mysql)
|
||||
.innerJoin(backups, eq(mysql.mysqlId, backups.mysqlId))
|
||||
.where(eq(backups.backupId, backupId))
|
||||
.limit(1);
|
||||
|
||||
if (!result || !result[0]) {
|
||||
throw new TRPCError({
|
||||
code: "NOT_FOUND",
|
||||
message: "Mysql not found",
|
||||
});
|
||||
}
|
||||
return result[0];
|
||||
};
|
||||
|
||||
export const removeMySqlById = async (mysqlId: string) => {
|
||||
const result = await db
|
||||
.delete(mysql)
|
||||
.where(eq(mysql.mysqlId, mysqlId))
|
||||
.returning();
|
||||
|
||||
return result[0];
|
||||
};
|
||||
|
||||
export const deployMySql = async (mysqlId: string) => {
|
||||
const mysql = await findMySqlById(mysqlId);
|
||||
try {
|
||||
await pullImage(mysql.dockerImage);
|
||||
await buildMysql(mysql);
|
||||
await updateMySqlById(mysqlId, {
|
||||
applicationStatus: "done",
|
||||
});
|
||||
} catch (error) {
|
||||
await updateMySqlById(mysqlId, {
|
||||
applicationStatus: "error",
|
||||
});
|
||||
throw new TRPCError({
|
||||
code: "INTERNAL_SERVER_ERROR",
|
||||
message: `Error on deploy mysql${error}`,
|
||||
});
|
||||
}
|
||||
return mysql;
|
||||
};
|
||||
62
server/api/services/port.ts
Normal file
62
server/api/services/port.ts
Normal file
@@ -0,0 +1,62 @@
|
||||
import { db } from "@/server/db";
|
||||
import { type apiCreatePort, ports } from "@/server/db/schema";
|
||||
import { TRPCError } from "@trpc/server";
|
||||
import { eq } from "drizzle-orm";
|
||||
|
||||
export type Port = typeof ports.$inferSelect;
|
||||
|
||||
export const createPort = async (input: typeof apiCreatePort._type) => {
|
||||
const newPort = await db
|
||||
.insert(ports)
|
||||
.values({
|
||||
...input,
|
||||
})
|
||||
.returning()
|
||||
.then((value) => value[0]);
|
||||
|
||||
if (!newPort) {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: "Error input: Inserting port",
|
||||
});
|
||||
}
|
||||
|
||||
return newPort;
|
||||
};
|
||||
|
||||
export const finPortById = async (portId: string) => {
|
||||
const result = await db.query.ports.findFirst({
|
||||
where: eq(ports.portId, portId),
|
||||
});
|
||||
if (!result) {
|
||||
throw new TRPCError({
|
||||
code: "NOT_FOUND",
|
||||
message: "Port not found",
|
||||
});
|
||||
}
|
||||
return result;
|
||||
};
|
||||
|
||||
export const removePortById = async (portId: string) => {
|
||||
const result = await db
|
||||
.delete(ports)
|
||||
.where(eq(ports.portId, portId))
|
||||
.returning();
|
||||
|
||||
return result[0];
|
||||
};
|
||||
|
||||
export const updatePortById = async (
|
||||
portId: string,
|
||||
portData: Partial<Port>,
|
||||
) => {
|
||||
const result = await db
|
||||
.update(ports)
|
||||
.set({
|
||||
...portData,
|
||||
})
|
||||
.where(eq(ports.portId, portId))
|
||||
.returning();
|
||||
|
||||
return result[0];
|
||||
};
|
||||
116
server/api/services/postgres.ts
Normal file
116
server/api/services/postgres.ts
Normal file
@@ -0,0 +1,116 @@
|
||||
import { generateRandomPassword } from "@/server/auth/random-password";
|
||||
import { db } from "@/server/db";
|
||||
import { type apiCreatePostgres, backups, postgres } from "@/server/db/schema";
|
||||
import { buildPostgres } from "@/server/utils/databases/postgres";
|
||||
import { pullImage } from "@/server/utils/docker/utils";
|
||||
import { TRPCError } from "@trpc/server";
|
||||
import { eq, getTableColumns } from "drizzle-orm";
|
||||
|
||||
export type Postgres = typeof postgres.$inferSelect;
|
||||
|
||||
export const createPostgres = async (input: typeof apiCreatePostgres._type) => {
|
||||
const newPostgres = await db
|
||||
.insert(postgres)
|
||||
.values({
|
||||
...input,
|
||||
databasePassword: input.databasePassword
|
||||
? input.databasePassword
|
||||
: (await generateRandomPassword()).randomPassword,
|
||||
})
|
||||
.returning()
|
||||
.then((value) => value[0]);
|
||||
|
||||
if (!newPostgres) {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: "Error input: Inserting postgresql database",
|
||||
});
|
||||
}
|
||||
|
||||
return newPostgres;
|
||||
};
|
||||
|
||||
export const findPostgresById = async (postgresId: string) => {
|
||||
const result = await db.query.postgres.findFirst({
|
||||
where: eq(postgres.postgresId, postgresId),
|
||||
with: {
|
||||
project: true,
|
||||
mounts: true,
|
||||
backups: {
|
||||
with: {
|
||||
destination: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
if (!result) {
|
||||
throw new TRPCError({
|
||||
code: "NOT_FOUND",
|
||||
message: "Postgres not found",
|
||||
});
|
||||
}
|
||||
return result;
|
||||
};
|
||||
|
||||
export const findPostgresByBackupId = async (backupId: string) => {
|
||||
const result = await db
|
||||
.select({
|
||||
...getTableColumns(postgres),
|
||||
})
|
||||
.from(postgres)
|
||||
.innerJoin(backups, eq(postgres.postgresId, backups.postgresId))
|
||||
.where(eq(backups.backupId, backupId))
|
||||
.limit(1);
|
||||
|
||||
if (!result || !result[0]) {
|
||||
throw new TRPCError({
|
||||
code: "NOT_FOUND",
|
||||
message: "Postgres not found",
|
||||
});
|
||||
}
|
||||
return result[0];
|
||||
};
|
||||
|
||||
export const updatePostgresById = async (
|
||||
postgresId: string,
|
||||
postgresData: Partial<Postgres>,
|
||||
) => {
|
||||
const result = await db
|
||||
.update(postgres)
|
||||
.set({
|
||||
...postgresData,
|
||||
})
|
||||
.where(eq(postgres.postgresId, postgresId))
|
||||
.returning();
|
||||
|
||||
return result[0];
|
||||
};
|
||||
|
||||
export const removePostgresById = async (postgresId: string) => {
|
||||
const result = await db
|
||||
.delete(postgres)
|
||||
.where(eq(postgres.postgresId, postgresId))
|
||||
.returning();
|
||||
|
||||
return result[0];
|
||||
};
|
||||
|
||||
export const deployPostgres = async (postgresId: string) => {
|
||||
const postgres = await findPostgresById(postgresId);
|
||||
try {
|
||||
await pullImage(postgres.dockerImage);
|
||||
await buildPostgres(postgres);
|
||||
await updatePostgresById(postgresId, {
|
||||
applicationStatus: "done",
|
||||
});
|
||||
} catch (error) {
|
||||
await updatePostgresById(postgresId, {
|
||||
applicationStatus: "error",
|
||||
});
|
||||
throw new TRPCError({
|
||||
code: "INTERNAL_SERVER_ERROR",
|
||||
message: `Error on deploy postgres${error}`,
|
||||
});
|
||||
}
|
||||
return postgres;
|
||||
};
|
||||
75
server/api/services/project.ts
Normal file
75
server/api/services/project.ts
Normal file
@@ -0,0 +1,75 @@
|
||||
import { db } from "@/server/db";
|
||||
import { type apiCreateProject, projects } from "@/server/db/schema";
|
||||
import { TRPCError } from "@trpc/server";
|
||||
import { eq } from "drizzle-orm";
|
||||
import { findAdmin } from "./admin";
|
||||
|
||||
export type Project = typeof projects.$inferSelect;
|
||||
|
||||
export const createProject = async (input: typeof apiCreateProject._type) => {
|
||||
const admin = await findAdmin();
|
||||
const newProject = await db
|
||||
.insert(projects)
|
||||
.values({
|
||||
...input,
|
||||
adminId: admin.adminId,
|
||||
})
|
||||
.returning()
|
||||
.then((value) => value[0]);
|
||||
|
||||
if (!newProject) {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: "Error to create the project",
|
||||
});
|
||||
}
|
||||
|
||||
return newProject;
|
||||
};
|
||||
|
||||
export const findProjectById = async (projectId: string) => {
|
||||
const project = await db.query.projects.findFirst({
|
||||
where: eq(projects.projectId, projectId),
|
||||
with: {
|
||||
applications: true,
|
||||
mariadb: true,
|
||||
mongo: true,
|
||||
mysql: true,
|
||||
postgres: true,
|
||||
redis: true,
|
||||
},
|
||||
});
|
||||
if (!project) {
|
||||
throw new TRPCError({
|
||||
code: "NOT_FOUND",
|
||||
message: "Project not found",
|
||||
});
|
||||
}
|
||||
return project;
|
||||
};
|
||||
|
||||
export const deleteProject = async (projectId: string) => {
|
||||
const project = await db
|
||||
.delete(projects)
|
||||
.where(eq(projects.projectId, projectId))
|
||||
.returning()
|
||||
.then((value) => value[0]);
|
||||
|
||||
return project;
|
||||
};
|
||||
|
||||
export const updateProjectById = async (
|
||||
projectId: string,
|
||||
projectData: Partial<Project>,
|
||||
) => {
|
||||
const result = await db
|
||||
.update(projects)
|
||||
.set({
|
||||
...projectData,
|
||||
})
|
||||
.where(eq(projects.projectId, projectId))
|
||||
.returning()
|
||||
.then((res) => res[0]);
|
||||
|
||||
return result;
|
||||
};
|
||||
123
server/api/services/redirect.ts
Normal file
123
server/api/services/redirect.ts
Normal file
@@ -0,0 +1,123 @@
|
||||
import { db } from "@/server/db";
|
||||
import { type apiCreateRedirect, redirects } from "@/server/db/schema";
|
||||
import {
|
||||
createRedirectMiddleware,
|
||||
removeRedirectMiddleware,
|
||||
updateRedirectMiddleware,
|
||||
} from "@/server/utils/traefik/redirect";
|
||||
import { TRPCError } from "@trpc/server";
|
||||
import { desc, eq } from "drizzle-orm";
|
||||
import type { z } from "zod";
|
||||
import { findApplicationById } from "./application";
|
||||
export type Redirect = typeof redirects.$inferSelect;
|
||||
|
||||
export const findRedirectById = async (redirectId: string) => {
|
||||
const application = await db.query.redirects.findFirst({
|
||||
where: eq(redirects.redirectId, redirectId),
|
||||
});
|
||||
if (!application) {
|
||||
throw new TRPCError({
|
||||
code: "NOT_FOUND",
|
||||
message: "Redirect not found",
|
||||
});
|
||||
}
|
||||
return application;
|
||||
};
|
||||
|
||||
export const createRedirect = async (
|
||||
redirectData: z.infer<typeof apiCreateRedirect>,
|
||||
) => {
|
||||
try {
|
||||
await db.transaction(async (tx) => {
|
||||
const redirect = await tx
|
||||
.insert(redirects)
|
||||
.values({
|
||||
...redirectData,
|
||||
})
|
||||
.returning()
|
||||
.then((res) => res[0]);
|
||||
|
||||
if (!redirect) {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: "Error to create the redirect",
|
||||
});
|
||||
}
|
||||
|
||||
const application = await findApplicationById(redirect.applicationId);
|
||||
|
||||
createRedirectMiddleware(application.appName, redirect);
|
||||
});
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: "Error to create this redirect",
|
||||
cause: error,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
export const removeRedirectById = async (redirectId: string) => {
|
||||
try {
|
||||
const response = await db
|
||||
.delete(redirects)
|
||||
.where(eq(redirects.redirectId, redirectId))
|
||||
.returning()
|
||||
.then((res) => res[0]);
|
||||
|
||||
if (!response) {
|
||||
throw new TRPCError({
|
||||
code: "NOT_FOUND",
|
||||
message: "Redirect not found",
|
||||
});
|
||||
}
|
||||
|
||||
const application = await findApplicationById(response.applicationId);
|
||||
|
||||
removeRedirectMiddleware(application.appName, response);
|
||||
|
||||
return response;
|
||||
} catch (error) {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: "Error to remove this redirect",
|
||||
cause: error,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
export const updateRedirectById = async (
|
||||
redirectId: string,
|
||||
redirectData: Partial<Redirect>,
|
||||
) => {
|
||||
try {
|
||||
const redirect = await db
|
||||
.update(redirects)
|
||||
.set({
|
||||
...redirectData,
|
||||
})
|
||||
.where(eq(redirects.redirectId, redirectId))
|
||||
.returning()
|
||||
.then((res) => res[0]);
|
||||
|
||||
if (!redirect) {
|
||||
throw new TRPCError({
|
||||
code: "NOT_FOUND",
|
||||
message: "Redirect not found",
|
||||
});
|
||||
}
|
||||
const application = await findApplicationById(redirect.applicationId);
|
||||
|
||||
updateRedirectMiddleware(application.appName, redirect);
|
||||
|
||||
return redirect;
|
||||
} catch (error) {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: "Error to update this redirect",
|
||||
});
|
||||
}
|
||||
};
|
||||
94
server/api/services/redis.ts
Normal file
94
server/api/services/redis.ts
Normal file
@@ -0,0 +1,94 @@
|
||||
import { generateRandomPassword } from "@/server/auth/random-password";
|
||||
import { db } from "@/server/db";
|
||||
import { type apiCreateRedis, redis } from "@/server/db/schema";
|
||||
import { buildRedis } from "@/server/utils/databases/redis";
|
||||
import { pullImage } from "@/server/utils/docker/utils";
|
||||
import { TRPCError } from "@trpc/server";
|
||||
import { eq } from "drizzle-orm";
|
||||
|
||||
export type Redis = typeof redis.$inferSelect;
|
||||
|
||||
// https://github.com/drizzle-team/drizzle-orm/discussions/1483#discussioncomment-7523881
|
||||
export const createRedis = async (input: typeof apiCreateRedis._type) => {
|
||||
const newRedis = await db
|
||||
.insert(redis)
|
||||
.values({
|
||||
...input,
|
||||
databasePassword: input.databasePassword
|
||||
? input.databasePassword
|
||||
: (await generateRandomPassword()).randomPassword,
|
||||
})
|
||||
.returning()
|
||||
.then((value) => value[0]);
|
||||
|
||||
if (!newRedis) {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: "Error input: Inserting redis database",
|
||||
});
|
||||
}
|
||||
|
||||
return newRedis;
|
||||
};
|
||||
|
||||
export const findRedisById = async (redisId: string) => {
|
||||
const result = await db.query.redis.findFirst({
|
||||
where: eq(redis.redisId, redisId),
|
||||
with: {
|
||||
project: true,
|
||||
mounts: true,
|
||||
},
|
||||
});
|
||||
if (!result) {
|
||||
throw new TRPCError({
|
||||
code: "NOT_FOUND",
|
||||
message: "Redis not found",
|
||||
});
|
||||
}
|
||||
return result;
|
||||
};
|
||||
|
||||
export const updateRedisById = async (
|
||||
redisId: string,
|
||||
redisData: Partial<Redis>,
|
||||
) => {
|
||||
const result = await db
|
||||
.update(redis)
|
||||
.set({
|
||||
...redisData,
|
||||
})
|
||||
.where(eq(redis.redisId, redisId))
|
||||
.returning();
|
||||
|
||||
return result[0];
|
||||
};
|
||||
|
||||
export const removeRedisById = async (redisId: string) => {
|
||||
const result = await db
|
||||
.delete(redis)
|
||||
.where(eq(redis.redisId, redisId))
|
||||
.returning();
|
||||
|
||||
return result[0];
|
||||
};
|
||||
|
||||
export const deployRedis = async (redisId: string) => {
|
||||
const redis = await findRedisById(redisId);
|
||||
try {
|
||||
await pullImage(redis.dockerImage);
|
||||
await buildRedis(redis);
|
||||
await updateRedisById(redisId, {
|
||||
applicationStatus: "done",
|
||||
});
|
||||
} catch (error) {
|
||||
await updateRedisById(redisId, {
|
||||
applicationStatus: "error",
|
||||
});
|
||||
|
||||
throw new TRPCError({
|
||||
code: "INTERNAL_SERVER_ERROR",
|
||||
message: `Error on deploy redis${error}`,
|
||||
});
|
||||
}
|
||||
return redis;
|
||||
};
|
||||
107
server/api/services/security.ts
Normal file
107
server/api/services/security.ts
Normal file
@@ -0,0 +1,107 @@
|
||||
import { db } from "@/server/db";
|
||||
import { type apiCreateSecurity, security } from "@/server/db/schema";
|
||||
import { TRPCError } from "@trpc/server";
|
||||
import { eq } from "drizzle-orm";
|
||||
import type { z } from "zod";
|
||||
import { findApplicationById } from "./application";
|
||||
import {
|
||||
createSecurityMiddleware,
|
||||
removeSecurityMiddleware,
|
||||
} from "@/server/utils/traefik/security";
|
||||
export type Security = typeof security.$inferSelect;
|
||||
|
||||
export const findSecurityById = async (securityId: string) => {
|
||||
const application = await db.query.security.findFirst({
|
||||
where: eq(security.securityId, securityId),
|
||||
});
|
||||
if (!application) {
|
||||
throw new TRPCError({
|
||||
code: "NOT_FOUND",
|
||||
message: "Security not found",
|
||||
});
|
||||
}
|
||||
return application;
|
||||
};
|
||||
|
||||
export const createSecurity = async (
|
||||
data: z.infer<typeof apiCreateSecurity>,
|
||||
) => {
|
||||
try {
|
||||
await db.transaction(async (tx) => {
|
||||
const application = await findApplicationById(data.applicationId);
|
||||
|
||||
const securityResponse = await tx
|
||||
.insert(security)
|
||||
.values({
|
||||
...data,
|
||||
})
|
||||
.returning()
|
||||
.then((res) => res[0]);
|
||||
|
||||
if (!securityResponse) {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: "Error to create the security",
|
||||
});
|
||||
}
|
||||
await createSecurityMiddleware(application.appName, securityResponse);
|
||||
return true;
|
||||
});
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: "Error to create this security",
|
||||
cause: error,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
export const deleteSecurityById = async (securityId: string) => {
|
||||
try {
|
||||
const result = await db
|
||||
.delete(security)
|
||||
.where(eq(security.securityId, securityId))
|
||||
.returning()
|
||||
.then((res) => res[0]);
|
||||
|
||||
if (!result) {
|
||||
throw new TRPCError({
|
||||
code: "NOT_FOUND",
|
||||
message: "Security not found",
|
||||
});
|
||||
}
|
||||
|
||||
const application = await findApplicationById(result.applicationId);
|
||||
|
||||
removeSecurityMiddleware(application.appName, result);
|
||||
return result;
|
||||
} catch (error) {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: "Error to remove this security",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
export const updateSecurityById = async (
|
||||
securityId: string,
|
||||
data: Partial<Security>,
|
||||
) => {
|
||||
try {
|
||||
const response = await db
|
||||
.update(security)
|
||||
.set({
|
||||
...data,
|
||||
})
|
||||
.where(eq(security.securityId, securityId))
|
||||
.returning();
|
||||
|
||||
return response[0];
|
||||
} catch (error) {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: "Error to update this security",
|
||||
});
|
||||
}
|
||||
};
|
||||
60
server/api/services/settings.ts
Normal file
60
server/api/services/settings.ts
Normal file
@@ -0,0 +1,60 @@
|
||||
import { docker } from "@/server/constants";
|
||||
import packageInfo from "../../../package.json";
|
||||
import { readdirSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
import { getServiceContainer } from "@/server/utils/docker/utils";
|
||||
|
||||
const updateIsAvailable = async () => {
|
||||
try {
|
||||
const service = await getServiceContainer("dokploy");
|
||||
|
||||
const localImage = await docker.getImage(getDokployImage()).inspect();
|
||||
return localImage.Id !== service?.ImageID;
|
||||
} catch (error) {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
export const getDokployImage = () => {
|
||||
return "dokploy/dokploy:latest";
|
||||
};
|
||||
|
||||
export const pullLatestRelease = async () => {
|
||||
try {
|
||||
await docker.pull(getDokployImage(), {});
|
||||
const newUpdateIsAvailable = await updateIsAvailable();
|
||||
return newUpdateIsAvailable;
|
||||
} catch (error) {}
|
||||
|
||||
return false;
|
||||
};
|
||||
export const getDokployVersion = () => {
|
||||
return packageInfo.version;
|
||||
};
|
||||
|
||||
interface TreeDataItem {
|
||||
id: string;
|
||||
name: string;
|
||||
type: "file" | "directory";
|
||||
children?: TreeDataItem[];
|
||||
}
|
||||
|
||||
export const readDirectory = (dirPath: string): TreeDataItem[] => {
|
||||
const items = readdirSync(dirPath, { withFileTypes: true });
|
||||
return items.map((item) => {
|
||||
const fullPath = join(dirPath, item.name);
|
||||
if (item.isDirectory()) {
|
||||
return {
|
||||
id: fullPath,
|
||||
name: item.name,
|
||||
type: "directory",
|
||||
children: readDirectory(fullPath),
|
||||
};
|
||||
}
|
||||
return {
|
||||
id: fullPath,
|
||||
name: item.name,
|
||||
type: "file",
|
||||
};
|
||||
});
|
||||
};
|
||||
207
server/api/services/user.ts
Normal file
207
server/api/services/user.ts
Normal file
@@ -0,0 +1,207 @@
|
||||
import { db } from "@/server/db";
|
||||
import { users } from "@/server/db/schema";
|
||||
import { TRPCError } from "@trpc/server";
|
||||
import { eq } from "drizzle-orm";
|
||||
|
||||
export type User = typeof users.$inferSelect;
|
||||
|
||||
export const findUserById = async (userId: string) => {
|
||||
const user = await db.query.users.findFirst({
|
||||
where: eq(users.userId, userId),
|
||||
});
|
||||
if (!user) {
|
||||
throw new TRPCError({
|
||||
code: "NOT_FOUND",
|
||||
message: "User not found",
|
||||
});
|
||||
}
|
||||
return user;
|
||||
};
|
||||
|
||||
export const findUserByAuthId = async (authId: string) => {
|
||||
const user = await db.query.users.findFirst({
|
||||
where: eq(users.authId, authId),
|
||||
with: {
|
||||
auth: true,
|
||||
},
|
||||
});
|
||||
if (!user) {
|
||||
throw new TRPCError({
|
||||
code: "NOT_FOUND",
|
||||
message: "User not found",
|
||||
});
|
||||
}
|
||||
return user;
|
||||
};
|
||||
|
||||
export const findUsers = async () => {
|
||||
const users = await db.query.users.findMany({
|
||||
with: {
|
||||
auth: {
|
||||
columns: {
|
||||
secret: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
return users;
|
||||
};
|
||||
|
||||
export const addNewProject = async (authId: string, projectId: string) => {
|
||||
const user = await findUserByAuthId(authId);
|
||||
|
||||
await db
|
||||
.update(users)
|
||||
.set({
|
||||
accesedProjects: [...user.accesedProjects, projectId],
|
||||
})
|
||||
.where(eq(users.authId, authId));
|
||||
};
|
||||
|
||||
export const addNewService = async (authId: string, serviceId: string) => {
|
||||
const user = await findUserByAuthId(authId);
|
||||
await db
|
||||
.update(users)
|
||||
.set({
|
||||
accesedServices: [...user.accesedServices, serviceId],
|
||||
})
|
||||
.where(eq(users.authId, authId));
|
||||
};
|
||||
|
||||
export const canPerformCreationService = async (
|
||||
userId: string,
|
||||
projectId: string,
|
||||
) => {
|
||||
const { accesedProjects, canCreateServices } = await findUserByAuthId(userId);
|
||||
const haveAccessToProject = accesedProjects.includes(projectId);
|
||||
|
||||
if (canCreateServices && haveAccessToProject) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
export const canPerformAccessService = async (
|
||||
userId: string,
|
||||
serviceId: string,
|
||||
) => {
|
||||
const { accesedServices } = await findUserByAuthId(userId);
|
||||
const haveAccessToService = accesedServices.includes(serviceId);
|
||||
|
||||
if (haveAccessToService) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
export const canPeformDeleteService = async (
|
||||
authId: string,
|
||||
serviceId: string,
|
||||
) => {
|
||||
const { accesedServices, canDeleteServices } = await findUserByAuthId(authId);
|
||||
const haveAccessToService = accesedServices.includes(serviceId);
|
||||
|
||||
if (canDeleteServices && haveAccessToService) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
export const canPerformCreationProject = async (authId: string) => {
|
||||
const { canCreateProjects } = await findUserByAuthId(authId);
|
||||
|
||||
if (canCreateProjects) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
export const canPerformDeleteProject = async (authId: string) => {
|
||||
const { canDeleteProjects } = await findUserByAuthId(authId);
|
||||
|
||||
if (canDeleteProjects) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
export const canPerformAccessProject = async (
|
||||
authId: string,
|
||||
projectId: string,
|
||||
) => {
|
||||
const { accesedProjects } = await findUserByAuthId(authId);
|
||||
|
||||
const haveAccessToProject = accesedProjects.includes(projectId);
|
||||
|
||||
if (haveAccessToProject) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
export const canAccessToTraefikFiles = async (authId: string) => {
|
||||
const { canAccessToTraefikFiles } = await findUserByAuthId(authId);
|
||||
return canAccessToTraefikFiles;
|
||||
};
|
||||
|
||||
export const checkServiceAccess = async (
|
||||
authId: string,
|
||||
serviceId: string,
|
||||
action = "access" as "access" | "create" | "delete",
|
||||
) => {
|
||||
let hasPermission = false;
|
||||
switch (action) {
|
||||
case "create":
|
||||
hasPermission = await canPerformCreationService(authId, serviceId);
|
||||
break;
|
||||
case "access":
|
||||
hasPermission = await canPerformAccessService(authId, serviceId);
|
||||
break;
|
||||
case "delete":
|
||||
hasPermission = await canPeformDeleteService(authId, serviceId);
|
||||
break;
|
||||
default:
|
||||
hasPermission = false;
|
||||
}
|
||||
if (!hasPermission) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "Permission denied",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
export const checkProjectAccess = async (
|
||||
authId: string,
|
||||
action: "create" | "delete" | "access",
|
||||
projectId?: string,
|
||||
) => {
|
||||
let hasPermission = false;
|
||||
switch (action) {
|
||||
case "access":
|
||||
hasPermission = await canPerformAccessProject(
|
||||
authId,
|
||||
projectId as string,
|
||||
);
|
||||
break;
|
||||
case "create":
|
||||
hasPermission = await canPerformCreationProject(authId);
|
||||
break;
|
||||
case "delete":
|
||||
hasPermission = await canPerformDeleteProject(authId);
|
||||
break;
|
||||
default:
|
||||
hasPermission = false;
|
||||
}
|
||||
if (!hasPermission) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "Permission denied",
|
||||
});
|
||||
}
|
||||
};
|
||||
162
server/api/trpc.ts
Normal file
162
server/api/trpc.ts
Normal file
@@ -0,0 +1,162 @@
|
||||
/**
|
||||
* 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 { TRPCError, initTRPC } from "@trpc/server";
|
||||
import type { CreateNextContextOptions } from "@trpc/server/adapters/next";
|
||||
import superjson from "superjson";
|
||||
import { ZodError } from "zod";
|
||||
import { validateRequest } from "../auth/auth";
|
||||
import type { Session, User } from "lucia";
|
||||
|
||||
/**
|
||||
* 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 }) | 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;
|
||||
// const sessionId = lucia.readSessionCookie(req.headers.cookie ?? "");
|
||||
const { session, user } = await validateRequest(req, res);
|
||||
user;
|
||||
return createInnerTRPCContext({
|
||||
req,
|
||||
res,
|
||||
session: session,
|
||||
...((user && {
|
||||
user: {
|
||||
authId: user.id,
|
||||
email: user.email,
|
||||
rol: user.rol,
|
||||
id: user.id,
|
||||
secret: user.secret,
|
||||
},
|
||||
}) || {
|
||||
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.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 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 },
|
||||
},
|
||||
});
|
||||
});
|
||||
92
server/auth/auth.ts
Normal file
92
server/auth/auth.ts
Normal file
@@ -0,0 +1,92 @@
|
||||
import { Lucia } from "lucia/dist/core.js";
|
||||
import { webcrypto } from "node:crypto";
|
||||
import { DrizzlePostgreSQLAdapter } from "@lucia-auth/adapter-drizzle";
|
||||
import type { Session, User } from "lucia/dist/core.js";
|
||||
import type { IncomingMessage, ServerResponse } from "node:http";
|
||||
import { auth, type DatabaseUser, sessionTable } from "../db/schema";
|
||||
import { db } from "../db";
|
||||
import { TimeSpan } from "lucia";
|
||||
|
||||
globalThis.crypto = webcrypto as Crypto;
|
||||
export const adapter = new DrizzlePostgreSQLAdapter(db, sessionTable, auth);
|
||||
|
||||
export const lucia = new Lucia(adapter, {
|
||||
sessionCookie: {
|
||||
attributes: {
|
||||
secure: false,
|
||||
},
|
||||
},
|
||||
sessionExpiresIn: new TimeSpan(1, "d"),
|
||||
getUserAttributes: (attributes) => {
|
||||
return {
|
||||
email: attributes.email,
|
||||
rol: attributes.rol,
|
||||
secret: attributes.secret !== null,
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
declare module "lucia" {
|
||||
interface Register {
|
||||
Lucia: typeof lucia;
|
||||
DatabaseUserAttributes: Omit<DatabaseUser, "id"> & { authId: string };
|
||||
}
|
||||
}
|
||||
|
||||
export async function validateRequest(
|
||||
req: IncomingMessage,
|
||||
res: ServerResponse,
|
||||
): Promise<{
|
||||
user: (User & { authId: string }) | null;
|
||||
session: Session | null;
|
||||
}> {
|
||||
const sessionId = lucia.readSessionCookie(req.headers.cookie ?? "");
|
||||
if (!sessionId) {
|
||||
return {
|
||||
user: null,
|
||||
session: null,
|
||||
};
|
||||
}
|
||||
const result = await lucia.validateSession(sessionId);
|
||||
if (result?.session?.fresh) {
|
||||
res.appendHeader(
|
||||
"Set-Cookie",
|
||||
lucia.createSessionCookie(result.session.id).serialize(),
|
||||
);
|
||||
}
|
||||
if (!result.session) {
|
||||
res.appendHeader(
|
||||
"Set-Cookie",
|
||||
lucia.createBlankSessionCookie().serialize(),
|
||||
);
|
||||
}
|
||||
return {
|
||||
session: result.session,
|
||||
...((result.user && {
|
||||
user: {
|
||||
authId: result.user.id,
|
||||
email: result.user.email,
|
||||
rol: result.user.rol,
|
||||
id: result.user.id,
|
||||
secret: result.user.secret,
|
||||
},
|
||||
}) || {
|
||||
user: null,
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
export async function validateWebSocketRequest(
|
||||
req: IncomingMessage,
|
||||
): Promise<{ user: User; session: Session } | { user: null; session: null }> {
|
||||
const sessionId = lucia.readSessionCookie(req.headers.cookie ?? "");
|
||||
|
||||
if (!sessionId) {
|
||||
return {
|
||||
user: null,
|
||||
session: null,
|
||||
};
|
||||
}
|
||||
const result = await lucia.validateSession(sessionId);
|
||||
return result;
|
||||
}
|
||||
20
server/auth/random-password.ts
Normal file
20
server/auth/random-password.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import bcrypt from "bcrypt";
|
||||
|
||||
export const generateRandomPassword = async () => {
|
||||
const passwordLength = 16;
|
||||
|
||||
const characters =
|
||||
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
|
||||
|
||||
let randomPassword = "";
|
||||
for (let i = 0; i < passwordLength; i++) {
|
||||
randomPassword += characters.charAt(
|
||||
Math.floor(Math.random() * characters.length),
|
||||
);
|
||||
}
|
||||
|
||||
const saltRounds = 10;
|
||||
|
||||
const hashedPassword = await bcrypt.hash(randomPassword, saltRounds);
|
||||
return { randomPassword, hashedPassword };
|
||||
};
|
||||
11
server/constants/index.ts
Normal file
11
server/constants/index.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import Docker from "dockerode";
|
||||
|
||||
export const BASE_PATH = "/etc/dokploy";
|
||||
export const MAIN_TRAEFIK_PATH = `${BASE_PATH}/traefik`;
|
||||
export const DYNAMIC_TRAEFIK_PATH = `${BASE_PATH}/traefik/dynamic`;
|
||||
export const LOGS_PATH = `${BASE_PATH}/logs`;
|
||||
export const APPLICATIONS_PATH = `${BASE_PATH}/applications`;
|
||||
export const SSH_PATH = `${BASE_PATH}/ssh`;
|
||||
export const CERTIFICATES_PATH = `${DYNAMIC_TRAEFIK_PATH}/certificates`;
|
||||
export const MONITORING_PATH = `${BASE_PATH}/monitoring`;
|
||||
export const docker = new Docker();
|
||||
13
server/db/drizzle.config.ts
Normal file
13
server/db/drizzle.config.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import type { Config } from "drizzle-kit";
|
||||
|
||||
console.log("> Generating PG Schema:", process.env.DATABASE_URL);
|
||||
export default {
|
||||
schema: "./server/db/schema/index.ts",
|
||||
driver: "pg",
|
||||
dbCredentials: {
|
||||
connectionString: process.env.DATABASE_URL || "",
|
||||
},
|
||||
verbose: true,
|
||||
strict: true,
|
||||
out: "drizzle",
|
||||
} satisfies Config;
|
||||
22
server/db/index.ts
Normal file
22
server/db/index.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import { drizzle, type PostgresJsDatabase } from "drizzle-orm/postgres-js";
|
||||
import * as schema from "./schema";
|
||||
import postgres from "postgres";
|
||||
|
||||
declare global {
|
||||
// eslint-disable-next-line no-var -- only var works here
|
||||
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
server/db/migration.ts
Normal file
21
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();
|
||||
});
|
||||
22
server/db/reset.ts
Normal file
22
server/db/reset.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
// Credits to Louistiti from Drizzle Discord: https://discord.com/channels/1043890932593987624/1130802621750448160/1143083373535973406
|
||||
import { drizzle } from "drizzle-orm/postgres-js";
|
||||
import { sql } from "drizzle-orm";
|
||||
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;`;
|
||||
const tables = await db.execute(tablesQuery);
|
||||
console.log(tables);
|
||||
} catch (error) {
|
||||
console.error("Error to clean database", error);
|
||||
} finally {
|
||||
}
|
||||
};
|
||||
|
||||
clearDb();
|
||||
92
server/db/schema/admin.ts
Normal file
92
server/db/schema/admin.ts
Normal file
@@ -0,0 +1,92 @@
|
||||
import { relations } from "drizzle-orm";
|
||||
import { boolean, integer, pgTable, text } from "drizzle-orm/pg-core";
|
||||
import { nanoid } from "nanoid";
|
||||
import { auth } from "./auth";
|
||||
import { users } from "./user";
|
||||
import { createInsertSchema } from "drizzle-zod";
|
||||
import { z } from "zod";
|
||||
import { certificateType } from "./shared";
|
||||
|
||||
export const admins = pgTable("admin", {
|
||||
adminId: text("adminId")
|
||||
.notNull()
|
||||
.primaryKey()
|
||||
.$defaultFn(() => nanoid()),
|
||||
|
||||
githubAppId: integer("githubAppId"),
|
||||
githubAppName: text("githubAppName"),
|
||||
serverIp: text("serverIp"),
|
||||
certificateType: certificateType("certificateType").notNull().default("none"),
|
||||
host: text("host"),
|
||||
githubClientId: text("githubClientId"),
|
||||
githubClientSecret: text("githubClientSecret"),
|
||||
githubInstallationId: text("githubInstallationId"),
|
||||
githubPrivateKey: text("githubPrivateKey"),
|
||||
letsEncryptEmail: text("letsEncryptEmail"),
|
||||
sshPrivateKey: text("sshPrivateKey"),
|
||||
enableDockerCleanup: boolean("enableDockerCleanup").notNull().default(false),
|
||||
authId: text("authId")
|
||||
.notNull()
|
||||
.references(() => auth.id, { onDelete: "cascade" }),
|
||||
createdAt: text("createdAt")
|
||||
.notNull()
|
||||
.$defaultFn(() => new Date().toISOString()),
|
||||
});
|
||||
|
||||
export const adminsRelations = relations(admins, ({ one, many }) => ({
|
||||
auth: one(auth, {
|
||||
fields: [admins.authId],
|
||||
references: [auth.id],
|
||||
}),
|
||||
users: many(users),
|
||||
}));
|
||||
|
||||
const createSchema = createInsertSchema(admins, {
|
||||
adminId: z.string(),
|
||||
githubAppName: z.string().optional(),
|
||||
githubClientId: z.string().optional(),
|
||||
githubClientSecret: z.string().optional(),
|
||||
githubInstallationId: z.string().optional(),
|
||||
githubPrivateKey: z.string().optional(),
|
||||
githubAppId: z.number().optional(),
|
||||
enableDockerCleanup: z.boolean().optional(),
|
||||
sshPrivateKey: z.string().optional(),
|
||||
certificateType: z.enum(["letsencrypt", "none"]).default("none"),
|
||||
serverIp: z.string().optional(),
|
||||
});
|
||||
|
||||
export const apiSaveSSHKey = createSchema
|
||||
.pick({
|
||||
sshPrivateKey: true,
|
||||
})
|
||||
.required();
|
||||
|
||||
export const apiAssignDomain = createSchema
|
||||
.pick({
|
||||
letsEncryptEmail: true,
|
||||
host: true,
|
||||
certificateType: true,
|
||||
})
|
||||
.required();
|
||||
|
||||
export const apiUpdateDockerCleanup = createSchema
|
||||
.pick({
|
||||
enableDockerCleanup: true,
|
||||
})
|
||||
.required();
|
||||
|
||||
export const apiTraefikConfig = z.object({
|
||||
traefikConfig: z.string().min(1),
|
||||
});
|
||||
|
||||
export const apiGetBranches = z.object({
|
||||
repo: z.string().min(1),
|
||||
owner: z.string().min(1),
|
||||
});
|
||||
export const apiModifyTraefikConfig = z.object({
|
||||
path: z.string().min(1),
|
||||
traefikConfig: z.string().min(1),
|
||||
});
|
||||
export const apiReadTraefikConfig = z.object({
|
||||
path: z.string().min(1),
|
||||
});
|
||||
199
server/db/schema/application.ts
Normal file
199
server/db/schema/application.ts
Normal file
@@ -0,0 +1,199 @@
|
||||
import { relations } from "drizzle-orm";
|
||||
import { createInsertSchema } from "drizzle-zod";
|
||||
import { z } from "zod";
|
||||
import { nanoid } from "nanoid";
|
||||
import { deployments } from "./deployment";
|
||||
import { mounts } from "./mount";
|
||||
import { redirects } from "./redirects";
|
||||
import { domains } from "./domain";
|
||||
import { projects } from "./project";
|
||||
import { security } from "./security";
|
||||
import { applicationStatus } from "./shared";
|
||||
import { ports } from "./port";
|
||||
import { boolean, integer, pgEnum, pgTable, text } from "drizzle-orm/pg-core";
|
||||
import { generateAppName } from "./utils";
|
||||
|
||||
export const sourceType = pgEnum("sourceType", ["docker", "git", "github"]);
|
||||
|
||||
export const buildType = pgEnum("buildType", [
|
||||
"dockerfile",
|
||||
"heroku_buildpacks",
|
||||
"paketo_buildpacks",
|
||||
"nixpacks",
|
||||
]);
|
||||
|
||||
export const applications = pgTable("application", {
|
||||
applicationId: text("applicationId")
|
||||
.notNull()
|
||||
.primaryKey()
|
||||
.$defaultFn(() => nanoid()),
|
||||
name: text("name").notNull(),
|
||||
appName: text("appName")
|
||||
.notNull()
|
||||
.$defaultFn(() => generateAppName("app"))
|
||||
.unique(),
|
||||
description: text("description"),
|
||||
env: text("env"),
|
||||
memoryReservation: integer("memoryReservation"),
|
||||
memoryLimit: integer("memoryLimit"),
|
||||
cpuReservation: integer("cpuReservation"),
|
||||
cpuLimit: integer("cpuLimit"),
|
||||
title: text("title"),
|
||||
enabled: boolean("enabled"),
|
||||
subtitle: text("subtitle"),
|
||||
command: text("command"),
|
||||
refreshToken: text("refreshToken").$defaultFn(() => nanoid()),
|
||||
sourceType: sourceType("sourceType").notNull().default("github"),
|
||||
// Github
|
||||
repository: text("repository"),
|
||||
owner: text("owner"),
|
||||
branch: text("branch"),
|
||||
buildPath: text("buildPath").default("/"),
|
||||
autoDeploy: boolean("autoDeploy"),
|
||||
// Docker
|
||||
username: text("username"),
|
||||
password: text("password"),
|
||||
dockerImage: text("dockerImage"),
|
||||
// Git
|
||||
customGitUrl: text("customGitUrl"),
|
||||
customGitBranch: text("customGitBranch"),
|
||||
customGitBuildPath: text("customGitBuildPath"),
|
||||
customGitSSHKey: text("customGitSSHKey"),
|
||||
dockerfile: text("dockerfile"),
|
||||
applicationStatus: applicationStatus("applicationStatus")
|
||||
.notNull()
|
||||
.default("idle"),
|
||||
buildType: buildType("buildType").notNull().default("nixpacks"),
|
||||
createdAt: text("createdAt")
|
||||
.notNull()
|
||||
.$defaultFn(() => new Date().toISOString()),
|
||||
projectId: text("projectId")
|
||||
.notNull()
|
||||
.references(() => projects.projectId, { onDelete: "cascade" }),
|
||||
});
|
||||
|
||||
export const applicationsRelations = relations(
|
||||
applications,
|
||||
({ one, many }) => ({
|
||||
project: one(projects, {
|
||||
fields: [applications.projectId],
|
||||
references: [projects.projectId],
|
||||
}),
|
||||
deployments: many(deployments),
|
||||
domains: many(domains),
|
||||
mounts: many(mounts),
|
||||
redirects: many(redirects),
|
||||
security: many(security),
|
||||
ports: many(ports),
|
||||
}),
|
||||
);
|
||||
|
||||
const createSchema = createInsertSchema(applications, {
|
||||
appName: z.string(),
|
||||
createdAt: z.string(),
|
||||
applicationId: z.string(),
|
||||
autoDeploy: z.boolean(),
|
||||
env: z.string().optional(),
|
||||
name: z.string().min(1),
|
||||
description: z.string().optional(),
|
||||
memoryReservation: z.number().optional(),
|
||||
memoryLimit: z.number().optional(),
|
||||
cpuReservation: z.number().optional(),
|
||||
cpuLimit: z.number().optional(),
|
||||
title: z.string().optional(),
|
||||
enabled: z.boolean().optional(),
|
||||
subtitle: z.string().optional(),
|
||||
dockerImage: z.string().optional(),
|
||||
username: z.string().optional(),
|
||||
password: z.string().optional(),
|
||||
customGitSSHKey: z.string().optional(),
|
||||
repository: z.string().optional(),
|
||||
dockerfile: z.string().optional(),
|
||||
branch: z.string().optional(),
|
||||
customGitBranch: z.string().optional(),
|
||||
customGitBuildPath: z.string().optional(),
|
||||
customGitUrl: z.string().optional(),
|
||||
buildPath: z.string().optional(),
|
||||
projectId: z.string(),
|
||||
sourceType: z.enum(["github", "docker", "git"]).optional(),
|
||||
applicationStatus: z.enum(["idle", "running", "done", "error"]),
|
||||
buildType: z.enum([
|
||||
"dockerfile",
|
||||
"heroku_buildpacks",
|
||||
"paketo_buildpacks",
|
||||
"nixpacks",
|
||||
]),
|
||||
owner: z.string(),
|
||||
});
|
||||
|
||||
export const apiCreateApplication = createSchema.pick({
|
||||
name: true,
|
||||
description: true,
|
||||
projectId: true,
|
||||
});
|
||||
|
||||
export const apiFindOneApplication = createSchema
|
||||
.pick({
|
||||
applicationId: true,
|
||||
})
|
||||
.required();
|
||||
|
||||
export const apiReloadApplication = createSchema
|
||||
.pick({
|
||||
appName: true,
|
||||
applicationId: true,
|
||||
})
|
||||
.required();
|
||||
|
||||
export const apiSaveBuildType = createSchema
|
||||
.pick({
|
||||
applicationId: true,
|
||||
buildType: true,
|
||||
dockerfile: true,
|
||||
})
|
||||
.required();
|
||||
|
||||
export const apiSaveGithubProvider = createSchema
|
||||
.pick({
|
||||
applicationId: true,
|
||||
repository: true,
|
||||
branch: true,
|
||||
owner: true,
|
||||
buildPath: true,
|
||||
})
|
||||
.required();
|
||||
|
||||
export const apiSaveDockerProvider = createSchema
|
||||
.pick({
|
||||
dockerImage: true,
|
||||
applicationId: true,
|
||||
username: true,
|
||||
password: true,
|
||||
})
|
||||
.required();
|
||||
|
||||
export const apiSaveGitProvider = createSchema
|
||||
.pick({
|
||||
customGitBranch: true,
|
||||
applicationId: true,
|
||||
customGitBuildPath: true,
|
||||
customGitUrl: true,
|
||||
})
|
||||
.required();
|
||||
|
||||
export const apiSaveEnviromentVariables = createSchema
|
||||
.pick({
|
||||
applicationId: true,
|
||||
env: true,
|
||||
})
|
||||
.required();
|
||||
|
||||
export const apiFindMonitoringStats = createSchema
|
||||
.pick({
|
||||
appName: true,
|
||||
})
|
||||
.required();
|
||||
|
||||
export const apiUpdateApplication = createSchema.partial().extend({
|
||||
applicationId: z.string().min(1),
|
||||
});
|
||||
123
server/db/schema/auth.ts
Normal file
123
server/db/schema/auth.ts
Normal file
@@ -0,0 +1,123 @@
|
||||
import { relations } from "drizzle-orm";
|
||||
import { pgTable, pgEnum, text, boolean } from "drizzle-orm/pg-core";
|
||||
import { nanoid } from "nanoid";
|
||||
import { users } from "./user";
|
||||
import { admins } from "./admin";
|
||||
import { createInsertSchema } from "drizzle-zod";
|
||||
import { z } from "zod";
|
||||
import { getRandomValues } from "node:crypto";
|
||||
|
||||
const randomImages = [
|
||||
"/avatars/avatar-1.png",
|
||||
"/avatars/avatar-2.png",
|
||||
"/avatars/avatar-3.png",
|
||||
"/avatars/avatar-4.png",
|
||||
"/avatars/avatar-5.png",
|
||||
"/avatars/avatar-6.png",
|
||||
"/avatars/avatar-7.png",
|
||||
"/avatars/avatar-8.png",
|
||||
"/avatars/avatar-9.png",
|
||||
"/avatars/avatar-10.png",
|
||||
"/avatars/avatar-11.png",
|
||||
"/avatars/avatar-12.png",
|
||||
];
|
||||
|
||||
const generateRandomImage = () => {
|
||||
return (
|
||||
randomImages[
|
||||
// @ts-ignore
|
||||
getRandomValues(new Uint32Array(1))[0] % randomImages.length
|
||||
] || "/avatars/avatar-1.png"
|
||||
);
|
||||
};
|
||||
export type DatabaseUser = typeof auth.$inferSelect;
|
||||
export const roles = pgEnum("Roles", ["admin", "user"]);
|
||||
|
||||
export const auth = pgTable("auth", {
|
||||
id: text("id")
|
||||
.notNull()
|
||||
.primaryKey()
|
||||
.$defaultFn(() => nanoid()),
|
||||
email: text("email").notNull().unique(),
|
||||
password: text("password").notNull(),
|
||||
rol: roles("rol").notNull(),
|
||||
image: text("image").$defaultFn(() => generateRandomImage()),
|
||||
secret: text("secret"),
|
||||
is2FAEnabled: boolean("is2FAEnabled").notNull().default(false),
|
||||
createdAt: text("createdAt")
|
||||
.notNull()
|
||||
.$defaultFn(() => new Date().toISOString()),
|
||||
});
|
||||
|
||||
export const authRelations = relations(auth, ({ many }) => ({
|
||||
admins: many(admins),
|
||||
users: many(users),
|
||||
}));
|
||||
const createSchema = createInsertSchema(auth, {
|
||||
email: z.string().email(),
|
||||
password: z.string().min(8),
|
||||
rol: z.enum(["admin", "user"]),
|
||||
image: z.string().optional(),
|
||||
});
|
||||
|
||||
export const apiCreateAdmin = createSchema.pick({
|
||||
email: true,
|
||||
password: true,
|
||||
});
|
||||
|
||||
export const apiCreateUser = createSchema
|
||||
.pick({
|
||||
password: true,
|
||||
id: true,
|
||||
})
|
||||
.required()
|
||||
.extend({
|
||||
token: z.string().min(1),
|
||||
});
|
||||
|
||||
export const apiLogin = createSchema
|
||||
.pick({
|
||||
email: true,
|
||||
password: true,
|
||||
})
|
||||
.required();
|
||||
|
||||
export const apiUpdateAuth = createSchema.partial().extend({
|
||||
email: z.string().nullable(),
|
||||
password: z.string().nullable(),
|
||||
image: z.string().optional(),
|
||||
});
|
||||
|
||||
export const apiUpdateAuthByAdmin = createSchema.partial().extend({
|
||||
email: z.string().nullable(),
|
||||
password: z.string().nullable(),
|
||||
image: z.string().optional(),
|
||||
id: z.string().min(1),
|
||||
});
|
||||
|
||||
export const apiFindOneAuth = createSchema
|
||||
.pick({
|
||||
id: true,
|
||||
})
|
||||
.required();
|
||||
|
||||
export const apiVerify2FA = createSchema
|
||||
.extend({
|
||||
pin: z.string().min(6),
|
||||
secret: z.string().min(1),
|
||||
})
|
||||
.pick({
|
||||
pin: true,
|
||||
secret: true,
|
||||
})
|
||||
.required();
|
||||
|
||||
export const apiVerifyLogin2FA = createSchema
|
||||
.extend({
|
||||
pin: z.string().min(6),
|
||||
})
|
||||
.pick({
|
||||
pin: true,
|
||||
id: true,
|
||||
})
|
||||
.required();
|
||||
131
server/db/schema/backups.ts
Normal file
131
server/db/schema/backups.ts
Normal file
@@ -0,0 +1,131 @@
|
||||
import { relations } from "drizzle-orm";
|
||||
import { destinations } from "./destination";
|
||||
import { createInsertSchema } from "drizzle-zod";
|
||||
import { z } from "zod";
|
||||
import { nanoid } from "nanoid";
|
||||
import { postgres } from "./postgres";
|
||||
import { mariadb } from "./mariadb";
|
||||
import { mysql } from "./mysql";
|
||||
import { mongo } from "./mongo";
|
||||
import {
|
||||
type AnyPgColumn,
|
||||
boolean,
|
||||
pgEnum,
|
||||
pgTable,
|
||||
text,
|
||||
} from "drizzle-orm/pg-core";
|
||||
|
||||
export const databaseType = pgEnum("databaseType", [
|
||||
"postgres",
|
||||
"mariadb",
|
||||
"mysql",
|
||||
"mongo",
|
||||
]);
|
||||
|
||||
export const backups = pgTable("backup", {
|
||||
backupId: text("backupId")
|
||||
.notNull()
|
||||
.primaryKey()
|
||||
.$defaultFn(() => nanoid()),
|
||||
schedule: text("schedule").notNull(),
|
||||
enabled: boolean("enabled"),
|
||||
database: text("database").notNull(),
|
||||
prefix: text("prefix").notNull(),
|
||||
|
||||
destinationId: text("destinationId")
|
||||
.notNull()
|
||||
.references(() => destinations.destinationId, { onDelete: "cascade" }),
|
||||
|
||||
databaseType: databaseType("databaseType").notNull(),
|
||||
postgresId: text("postgresId").references(
|
||||
(): AnyPgColumn => postgres.postgresId,
|
||||
{
|
||||
onDelete: "cascade",
|
||||
},
|
||||
),
|
||||
mariadbId: text("mariadbId").references(
|
||||
(): AnyPgColumn => mariadb.mariadbId,
|
||||
{
|
||||
onDelete: "cascade",
|
||||
},
|
||||
),
|
||||
mysqlId: text("mysqlId").references((): AnyPgColumn => mysql.mysqlId, {
|
||||
onDelete: "cascade",
|
||||
}),
|
||||
mongoId: text("mongoId").references((): AnyPgColumn => mongo.mongoId, {
|
||||
onDelete: "cascade",
|
||||
}),
|
||||
});
|
||||
|
||||
export const backupsRelations = relations(backups, ({ one }) => ({
|
||||
destination: one(destinations, {
|
||||
fields: [backups.destinationId],
|
||||
references: [destinations.destinationId],
|
||||
}),
|
||||
postgres: one(postgres, {
|
||||
fields: [backups.postgresId],
|
||||
references: [postgres.postgresId],
|
||||
}),
|
||||
mariadb: one(mariadb, {
|
||||
fields: [backups.mariadbId],
|
||||
references: [mariadb.mariadbId],
|
||||
}),
|
||||
mysql: one(mysql, {
|
||||
fields: [backups.mysqlId],
|
||||
references: [mysql.mysqlId],
|
||||
}),
|
||||
mongo: one(mongo, {
|
||||
fields: [backups.mongoId],
|
||||
references: [mongo.mongoId],
|
||||
}),
|
||||
}));
|
||||
|
||||
const createSchema = createInsertSchema(backups, {
|
||||
backupId: z.string(),
|
||||
destinationId: z.string(),
|
||||
enabled: z.boolean().optional(),
|
||||
prefix: z.string().min(1),
|
||||
database: z.string().min(1),
|
||||
schedule: z.string(),
|
||||
databaseType: z.enum(["postgres", "mariadb", "mysql", "mongo"]),
|
||||
postgresId: z.string().optional(),
|
||||
mariadbId: z.string().optional(),
|
||||
mysqlId: z.string().optional(),
|
||||
mongoId: z.string().optional(),
|
||||
});
|
||||
|
||||
export const apiCreateBackup = createSchema.pick({
|
||||
schedule: true,
|
||||
enabled: true,
|
||||
prefix: true,
|
||||
destinationId: true,
|
||||
database: true,
|
||||
mariadbId: true,
|
||||
mysqlId: true,
|
||||
postgresId: true,
|
||||
mongoId: true,
|
||||
databaseType: true,
|
||||
});
|
||||
|
||||
export const apiFindOneBackup = createSchema
|
||||
.pick({
|
||||
backupId: true,
|
||||
})
|
||||
.required();
|
||||
|
||||
export const apiRemoveBackup = createSchema
|
||||
.pick({
|
||||
backupId: true,
|
||||
})
|
||||
.required();
|
||||
|
||||
export const apiUpdateBackup = createSchema
|
||||
.pick({
|
||||
schedule: true,
|
||||
enabled: true,
|
||||
prefix: true,
|
||||
backupId: true,
|
||||
destinationId: true,
|
||||
database: true,
|
||||
})
|
||||
.required();
|
||||
43
server/db/schema/certificate.ts
Normal file
43
server/db/schema/certificate.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
import { createInsertSchema } from "drizzle-zod";
|
||||
import { z } from "zod";
|
||||
import { nanoid } from "nanoid";
|
||||
import { boolean, pgTable, text } from "drizzle-orm/pg-core";
|
||||
import { generateAppName } from "./utils";
|
||||
|
||||
export const certificates = pgTable("certificate", {
|
||||
certificateId: text("certificateId")
|
||||
.notNull()
|
||||
.primaryKey()
|
||||
.$defaultFn(() => nanoid()),
|
||||
name: text("name").notNull(),
|
||||
certificateData: text("certificateData").notNull(),
|
||||
privateKey: text("privateKey").notNull(),
|
||||
certificatePath: text("certificatePath")
|
||||
.notNull()
|
||||
.$defaultFn(() => generateAppName("certificate"))
|
||||
.unique(),
|
||||
autoRenew: boolean("autoRenew"),
|
||||
});
|
||||
|
||||
export const apiCreateCertificate = createInsertSchema(certificates, {
|
||||
name: z.string().min(1),
|
||||
certificateData: z.string().min(1),
|
||||
privateKey: z.string().min(1),
|
||||
autoRenew: z.boolean().optional(),
|
||||
});
|
||||
|
||||
export const apiFindCertificate = z.object({
|
||||
certificateId: z.string().min(1),
|
||||
});
|
||||
|
||||
export const apiUpdateCertificate = z.object({
|
||||
certificateId: z.string().min(1),
|
||||
name: z.string().min(1).optional(),
|
||||
certificateData: z.string().min(1).optional(),
|
||||
privateKey: z.string().min(1).optional(),
|
||||
autoRenew: z.boolean().optional(),
|
||||
});
|
||||
|
||||
export const apiDeleteCertificate = z.object({
|
||||
certificateId: z.string().min(1),
|
||||
});
|
||||
57
server/db/schema/deployment.ts
Normal file
57
server/db/schema/deployment.ts
Normal file
@@ -0,0 +1,57 @@
|
||||
import { relations } from "drizzle-orm";
|
||||
import { z } from "zod";
|
||||
import { nanoid } from "nanoid";
|
||||
import { applications } from "./application";
|
||||
import { createInsertSchema } from "drizzle-zod";
|
||||
import { pgEnum, pgTable, text } from "drizzle-orm/pg-core";
|
||||
|
||||
export const deploymentStatus = pgEnum("deploymentStatus", [
|
||||
"running",
|
||||
"done",
|
||||
"error",
|
||||
]);
|
||||
|
||||
export const deployments = pgTable("deployment", {
|
||||
deploymentId: text("deploymentId")
|
||||
.notNull()
|
||||
.primaryKey()
|
||||
.$defaultFn(() => nanoid()),
|
||||
title: text("title").notNull(),
|
||||
status: deploymentStatus("status").default("running"),
|
||||
logPath: text("logPath").notNull(),
|
||||
applicationId: text("applicationId")
|
||||
.notNull()
|
||||
.references(() => applications.applicationId, { onDelete: "cascade" }),
|
||||
createdAt: text("createdAt")
|
||||
.notNull()
|
||||
.$defaultFn(() => new Date().toISOString()),
|
||||
});
|
||||
|
||||
export const deploymentsRelations = relations(deployments, ({ one }) => ({
|
||||
application: one(applications, {
|
||||
fields: [deployments.applicationId],
|
||||
references: [applications.applicationId],
|
||||
}),
|
||||
}));
|
||||
|
||||
const schema = createInsertSchema(deployments, {
|
||||
title: z.string().min(1),
|
||||
status: z.string().default("running"),
|
||||
logPath: z.string().min(1),
|
||||
applicationId: z.string().min(1),
|
||||
});
|
||||
|
||||
export const apiCreateDeployment = schema
|
||||
.pick({
|
||||
title: true,
|
||||
status: true,
|
||||
logPath: true,
|
||||
applicationId: true,
|
||||
})
|
||||
.required();
|
||||
|
||||
export const apiFindAllByApplication = schema
|
||||
.pick({
|
||||
applicationId: true,
|
||||
})
|
||||
.required();
|
||||
80
server/db/schema/destination.ts
Normal file
80
server/db/schema/destination.ts
Normal file
@@ -0,0 +1,80 @@
|
||||
import { relations } from "drizzle-orm";
|
||||
import { createInsertSchema } from "drizzle-zod";
|
||||
import { z } from "zod";
|
||||
import { nanoid } from "nanoid";
|
||||
import { pgTable, text } from "drizzle-orm/pg-core";
|
||||
import { admins } from "./admin";
|
||||
import { backups } from "./backups";
|
||||
|
||||
export const destinations = pgTable("destination", {
|
||||
destinationId: text("destinationId")
|
||||
.notNull()
|
||||
.primaryKey()
|
||||
.$defaultFn(() => nanoid()),
|
||||
name: text("name").notNull(),
|
||||
accessKey: text("accessKey").notNull(),
|
||||
secretAccessKey: text("secretAccessKey").notNull(),
|
||||
bucket: text("bucket").notNull(),
|
||||
region: text("region").notNull(),
|
||||
// maybe it can be null
|
||||
endpoint: text("endpoint").notNull(),
|
||||
adminId: text("adminId")
|
||||
.notNull()
|
||||
.references(() => admins.adminId, { onDelete: "cascade" }),
|
||||
});
|
||||
|
||||
export const destinationsRelations = relations(
|
||||
destinations,
|
||||
({ many, one }) => ({
|
||||
backups: many(backups),
|
||||
admin: one(admins, {
|
||||
fields: [destinations.adminId],
|
||||
references: [admins.adminId],
|
||||
}),
|
||||
}),
|
||||
);
|
||||
|
||||
const createSchema = createInsertSchema(destinations, {
|
||||
destinationId: z.string(),
|
||||
name: z.string().min(1),
|
||||
accessKey: z.string(),
|
||||
bucket: z.string(),
|
||||
endpoint: z.string(),
|
||||
secretAccessKey: z.string(),
|
||||
region: z.string(),
|
||||
});
|
||||
|
||||
export const apiCreateDestination = createSchema
|
||||
.pick({
|
||||
name: true,
|
||||
accessKey: true,
|
||||
bucket: true,
|
||||
region: true,
|
||||
endpoint: true,
|
||||
secretAccessKey: true,
|
||||
})
|
||||
.required();
|
||||
|
||||
export const apiFindOneDestination = createSchema
|
||||
.pick({
|
||||
destinationId: true,
|
||||
})
|
||||
.required();
|
||||
|
||||
export const apiRemoveDestination = createSchema
|
||||
.pick({
|
||||
destinationId: true,
|
||||
})
|
||||
.required();
|
||||
|
||||
export const apiUpdateDestination = createSchema
|
||||
.pick({
|
||||
name: true,
|
||||
accessKey: true,
|
||||
bucket: true,
|
||||
region: true,
|
||||
endpoint: true,
|
||||
secretAccessKey: true,
|
||||
destinationId: true,
|
||||
})
|
||||
.required();
|
||||
77
server/db/schema/domain.ts
Normal file
77
server/db/schema/domain.ts
Normal file
@@ -0,0 +1,77 @@
|
||||
import { relations } from "drizzle-orm";
|
||||
import { createInsertSchema } from "drizzle-zod";
|
||||
import { nanoid } from "nanoid";
|
||||
import { z } from "zod";
|
||||
import { boolean, integer, pgTable, serial, text } from "drizzle-orm/pg-core";
|
||||
import { applications } from "./application";
|
||||
import { certificateType } from "./shared";
|
||||
|
||||
export const domains = pgTable("domain", {
|
||||
domainId: text("domainId")
|
||||
.notNull()
|
||||
.primaryKey()
|
||||
.$defaultFn(() => nanoid()),
|
||||
host: text("host").notNull(),
|
||||
https: boolean("https").notNull().default(false),
|
||||
port: integer("port").default(80),
|
||||
path: text("path").default("/"),
|
||||
uniqueConfigKey: serial("uniqueConfigKey"),
|
||||
createdAt: text("createdAt")
|
||||
.notNull()
|
||||
.$defaultFn(() => new Date().toISOString()),
|
||||
applicationId: text("applicationId")
|
||||
.notNull()
|
||||
.references(() => applications.applicationId, { onDelete: "cascade" }),
|
||||
certificateType: certificateType("certificateType").notNull().default("none"),
|
||||
});
|
||||
|
||||
export const domainsRelations = relations(domains, ({ one }) => ({
|
||||
application: one(applications, {
|
||||
fields: [domains.applicationId],
|
||||
references: [applications.applicationId],
|
||||
}),
|
||||
}));
|
||||
const hostnameRegex = /^[a-zA-Z0-9][a-zA-Z0-9\.-]*\.[a-zA-Z]{2,}$/;
|
||||
const createSchema = createInsertSchema(domains, {
|
||||
domainId: z.string().min(1),
|
||||
host: z.string().min(1),
|
||||
path: z.string().min(1),
|
||||
port: z.number(),
|
||||
https: z.boolean(),
|
||||
applicationId: z.string(),
|
||||
certificateType: z.enum(["letsencrypt", "none"]),
|
||||
});
|
||||
|
||||
export const apiCreateDomain = createSchema
|
||||
.pick({
|
||||
host: true,
|
||||
path: true,
|
||||
port: true,
|
||||
https: true,
|
||||
applicationId: true,
|
||||
certificateType: true,
|
||||
})
|
||||
.required();
|
||||
|
||||
export const apiFindDomain = createSchema
|
||||
.pick({
|
||||
domainId: true,
|
||||
})
|
||||
.required();
|
||||
|
||||
export const apiFindDomainByApplication = createSchema
|
||||
.pick({
|
||||
applicationId: true,
|
||||
})
|
||||
.required();
|
||||
|
||||
export const apiUpdateDomain = createSchema
|
||||
.pick({
|
||||
domainId: true,
|
||||
host: true,
|
||||
path: true,
|
||||
port: true,
|
||||
https: true,
|
||||
certificateType: true,
|
||||
})
|
||||
.required();
|
||||
22
server/db/schema/index.ts
Normal file
22
server/db/schema/index.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
export * from "./application";
|
||||
export * from "./postgres";
|
||||
|
||||
export * from "./user";
|
||||
export * from "./admin";
|
||||
export * from "./auth";
|
||||
export * from "./project";
|
||||
export * from "./domain";
|
||||
export * from "./mariadb";
|
||||
export * from "./mongo";
|
||||
export * from "./mysql";
|
||||
export * from "./backups";
|
||||
export * from "./destination";
|
||||
export * from "./deployment";
|
||||
export * from "./mount";
|
||||
export * from "./certificate";
|
||||
export * from "./session";
|
||||
export * from "./redirects";
|
||||
export * from "./security";
|
||||
export * from "./port";
|
||||
export * from "./redis";
|
||||
export * from "./shared";
|
||||
134
server/db/schema/mariadb.ts
Normal file
134
server/db/schema/mariadb.ts
Normal file
@@ -0,0 +1,134 @@
|
||||
import { relations } from "drizzle-orm";
|
||||
import { createInsertSchema } from "drizzle-zod";
|
||||
import { z } from "zod";
|
||||
import { nanoid } from "nanoid";
|
||||
import { applicationStatus } from "./shared";
|
||||
import { integer, pgTable, text } from "drizzle-orm/pg-core";
|
||||
import { projects } from "./project";
|
||||
import { backups } from "./backups";
|
||||
import { mounts } from "./mount";
|
||||
import { generateAppName } from "./utils";
|
||||
|
||||
export const mariadb = pgTable("mariadb", {
|
||||
mariadbId: text("mariadbId")
|
||||
.notNull()
|
||||
.primaryKey()
|
||||
.$defaultFn(() => nanoid()),
|
||||
name: text("name").notNull(),
|
||||
appName: text("appName")
|
||||
.notNull()
|
||||
.$defaultFn(() => generateAppName("mariadb"))
|
||||
.unique(),
|
||||
description: text("description"),
|
||||
databaseName: text("databaseName").notNull(),
|
||||
databaseUser: text("databaseUser").notNull(),
|
||||
databasePassword: text("databasePassword").notNull(),
|
||||
databaseRootPassword: text("rootPassword").notNull(),
|
||||
dockerImage: text("dockerImage").notNull(),
|
||||
command: text("command"),
|
||||
env: text("env"),
|
||||
// RESOURCES
|
||||
memoryReservation: integer("memoryReservation"),
|
||||
memoryLimit: integer("memoryLimit"),
|
||||
cpuReservation: integer("cpuReservation"),
|
||||
cpuLimit: integer("cpuLimit"),
|
||||
//
|
||||
externalPort: integer("externalPort"),
|
||||
applicationStatus: applicationStatus("applicationStatus")
|
||||
.notNull()
|
||||
.default("idle"),
|
||||
createdAt: text("createdAt")
|
||||
.notNull()
|
||||
.$defaultFn(() => new Date().toISOString()),
|
||||
projectId: text("projectId")
|
||||
.notNull()
|
||||
.references(() => projects.projectId, { onDelete: "cascade" }),
|
||||
});
|
||||
|
||||
export const mariadbRelations = relations(mariadb, ({ one, many }) => ({
|
||||
project: one(projects, {
|
||||
fields: [mariadb.projectId],
|
||||
references: [projects.projectId],
|
||||
}),
|
||||
backups: many(backups),
|
||||
mounts: many(mounts),
|
||||
}));
|
||||
|
||||
const createSchema = createInsertSchema(mariadb, {
|
||||
mariadbId: z.string(),
|
||||
name: z.string().min(1),
|
||||
appName: z.string().min(1),
|
||||
createdAt: z.string(),
|
||||
databaseName: z.string().min(1),
|
||||
databaseUser: z.string().min(1),
|
||||
databasePassword: z.string(),
|
||||
databaseRootPassword: z.string().optional(),
|
||||
dockerImage: z.string().default("mariadb:6"),
|
||||
command: z.string().optional(),
|
||||
env: z.string().optional(),
|
||||
memoryReservation: z.number().optional(),
|
||||
memoryLimit: z.number().optional(),
|
||||
cpuReservation: z.number().optional(),
|
||||
cpuLimit: z.number().optional(),
|
||||
projectId: z.string(),
|
||||
applicationStatus: z.enum(["idle", "running", "done", "error"]),
|
||||
externalPort: z.number(),
|
||||
description: z.string().optional(),
|
||||
});
|
||||
|
||||
export const apiCreateMariaDB = createSchema
|
||||
.pick({
|
||||
name: true,
|
||||
dockerImage: true,
|
||||
databaseRootPassword: true,
|
||||
projectId: true,
|
||||
description: true,
|
||||
databaseName: true,
|
||||
databaseUser: true,
|
||||
databasePassword: true,
|
||||
})
|
||||
.required();
|
||||
|
||||
export const apiFindOneMariaDB = createSchema
|
||||
.pick({
|
||||
mariadbId: true,
|
||||
})
|
||||
.required();
|
||||
|
||||
export const apiChangeMariaDBStatus = createSchema
|
||||
.pick({
|
||||
mariadbId: true,
|
||||
applicationStatus: true,
|
||||
})
|
||||
.required();
|
||||
|
||||
export const apiSaveEnviromentVariablesMariaDB = createSchema
|
||||
.pick({
|
||||
mariadbId: true,
|
||||
env: true,
|
||||
})
|
||||
.required();
|
||||
|
||||
export const apiSaveExternalPortMariaDB = createSchema
|
||||
.pick({
|
||||
mariadbId: true,
|
||||
externalPort: true,
|
||||
})
|
||||
.required();
|
||||
|
||||
export const apiDeployMariaDB = createSchema
|
||||
.pick({
|
||||
mariadbId: true,
|
||||
})
|
||||
.required();
|
||||
|
||||
export const apiResetMariadb = createSchema
|
||||
.pick({
|
||||
mariadbId: true,
|
||||
appName: true,
|
||||
})
|
||||
.required();
|
||||
|
||||
export const apiUpdateMariaDB = createSchema.partial().extend({
|
||||
mariadbId: z.string().min(1),
|
||||
});
|
||||
126
server/db/schema/mongo.ts
Normal file
126
server/db/schema/mongo.ts
Normal file
@@ -0,0 +1,126 @@
|
||||
import { relations } from "drizzle-orm";
|
||||
import { createInsertSchema } from "drizzle-zod";
|
||||
import { z } from "zod";
|
||||
import { nanoid } from "nanoid";
|
||||
import { applicationStatus } from "./shared";
|
||||
import { integer, pgTable, text } from "drizzle-orm/pg-core";
|
||||
import { projects } from "./project";
|
||||
import { backups } from "./backups";
|
||||
import { mounts } from "./mount";
|
||||
import { generateAppName } from "./utils";
|
||||
|
||||
export const mongo = pgTable("mongo", {
|
||||
mongoId: text("mongoId")
|
||||
.notNull()
|
||||
.primaryKey()
|
||||
.$defaultFn(() => nanoid()),
|
||||
name: text("name").notNull(),
|
||||
appName: text("appName")
|
||||
.notNull()
|
||||
.$defaultFn(() => generateAppName("mongo"))
|
||||
.unique(),
|
||||
description: text("description"),
|
||||
databaseUser: text("databaseUser").notNull(),
|
||||
databasePassword: text("databasePassword").notNull(),
|
||||
dockerImage: text("dockerImage").notNull(),
|
||||
command: text("command"),
|
||||
env: text("env"),
|
||||
memoryReservation: integer("memoryReservation"),
|
||||
memoryLimit: integer("memoryLimit"),
|
||||
cpuReservation: integer("cpuReservation"),
|
||||
cpuLimit: integer("cpuLimit"),
|
||||
externalPort: integer("externalPort"),
|
||||
applicationStatus: applicationStatus("applicationStatus")
|
||||
.notNull()
|
||||
.default("idle"),
|
||||
createdAt: text("createdAt")
|
||||
.notNull()
|
||||
.$defaultFn(() => new Date().toISOString()),
|
||||
projectId: text("projectId")
|
||||
.notNull()
|
||||
.references(() => projects.projectId, { onDelete: "cascade" }),
|
||||
});
|
||||
|
||||
export const mongoRelations = relations(mongo, ({ one, many }) => ({
|
||||
project: one(projects, {
|
||||
fields: [mongo.projectId],
|
||||
references: [projects.projectId],
|
||||
}),
|
||||
backups: many(backups),
|
||||
mounts: many(mounts),
|
||||
}));
|
||||
|
||||
const createSchema = createInsertSchema(mongo, {
|
||||
appName: z.string().min(1),
|
||||
createdAt: z.string(),
|
||||
mongoId: z.string(),
|
||||
name: z.string().min(1),
|
||||
databasePassword: z.string(),
|
||||
databaseUser: z.string().min(1),
|
||||
dockerImage: z.string().default("mongo:15"),
|
||||
command: z.string().optional(),
|
||||
env: z.string().optional(),
|
||||
memoryReservation: z.number().optional(),
|
||||
memoryLimit: z.number().optional(),
|
||||
cpuReservation: z.number().optional(),
|
||||
cpuLimit: z.number().optional(),
|
||||
projectId: z.string(),
|
||||
applicationStatus: z.enum(["idle", "running", "done", "error"]),
|
||||
externalPort: z.number(),
|
||||
description: z.string().optional(),
|
||||
});
|
||||
|
||||
export const apiCreateMongo = createSchema
|
||||
.pick({
|
||||
name: true,
|
||||
dockerImage: true,
|
||||
projectId: true,
|
||||
description: true,
|
||||
databaseUser: true,
|
||||
databasePassword: true,
|
||||
})
|
||||
.required();
|
||||
|
||||
export const apiFindOneMongo = createSchema
|
||||
.pick({
|
||||
mongoId: true,
|
||||
})
|
||||
.required();
|
||||
|
||||
export const apiChangeMongoStatus = createSchema
|
||||
.pick({
|
||||
mongoId: true,
|
||||
applicationStatus: true,
|
||||
})
|
||||
.required();
|
||||
|
||||
export const apiSaveEnviromentVariablesMongo = createSchema
|
||||
.pick({
|
||||
mongoId: true,
|
||||
env: true,
|
||||
})
|
||||
.required();
|
||||
|
||||
export const apiSaveExternalPortMongo = createSchema
|
||||
.pick({
|
||||
mongoId: true,
|
||||
externalPort: true,
|
||||
})
|
||||
.required();
|
||||
|
||||
export const apiDeployMongo = createSchema
|
||||
.pick({
|
||||
mongoId: true,
|
||||
})
|
||||
.required();
|
||||
|
||||
export const apiUpdateMongo = createSchema.partial().extend({
|
||||
mongoId: z.string().min(1),
|
||||
});
|
||||
|
||||
export const apiResetMongo = createSchema
|
||||
.pick({
|
||||
mongoId: true,
|
||||
appName: true,
|
||||
})
|
||||
.required();
|
||||
136
server/db/schema/mount.ts
Normal file
136
server/db/schema/mount.ts
Normal file
@@ -0,0 +1,136 @@
|
||||
import { nanoid } from "nanoid";
|
||||
import { applications } from "./application";
|
||||
import { relations } from "drizzle-orm";
|
||||
import { createInsertSchema } from "drizzle-zod";
|
||||
import { z } from "zod";
|
||||
import { pgEnum, pgTable, text } from "drizzle-orm/pg-core";
|
||||
import { postgres } from "./postgres";
|
||||
import { mariadb } from "./mariadb";
|
||||
import { mongo } from "./mongo";
|
||||
import { mysql } from "./mysql";
|
||||
import { redis } from "./redis";
|
||||
|
||||
export const serviceType = pgEnum("serviceType", [
|
||||
"application",
|
||||
"postgres",
|
||||
"mysql",
|
||||
"mariadb",
|
||||
"mongo",
|
||||
"redis",
|
||||
]);
|
||||
|
||||
export const mountType = pgEnum("mountType", ["bind", "volume", "file"]);
|
||||
|
||||
export const mounts = pgTable("mount", {
|
||||
mountId: text("mountId")
|
||||
.notNull()
|
||||
.primaryKey()
|
||||
.$defaultFn(() => nanoid()),
|
||||
type: mountType("type").notNull(),
|
||||
hostPath: text("hostPath"),
|
||||
volumeName: text("volumeName"),
|
||||
content: text("content"),
|
||||
serviceType: serviceType("serviceType").notNull().default("application"),
|
||||
mountPath: text("mountPath").notNull(),
|
||||
applicationId: text("applicationId").references(
|
||||
() => applications.applicationId,
|
||||
{ onDelete: "cascade" },
|
||||
),
|
||||
postgresId: text("postgresId").references(() => postgres.postgresId, {
|
||||
onDelete: "cascade",
|
||||
}),
|
||||
mariadbId: text("mariadbId").references(() => mariadb.mariadbId, {
|
||||
onDelete: "cascade",
|
||||
}),
|
||||
mongoId: text("mongoId").references(() => mongo.mongoId, {
|
||||
onDelete: "cascade",
|
||||
}),
|
||||
mysqlId: text("mysqlId").references(() => mysql.mysqlId, {
|
||||
onDelete: "cascade",
|
||||
}),
|
||||
redisId: text("redisId").references(() => redis.redisId, {
|
||||
onDelete: "cascade",
|
||||
}),
|
||||
});
|
||||
|
||||
export const MountssRelations = relations(mounts, ({ one }) => ({
|
||||
application: one(applications, {
|
||||
fields: [mounts.applicationId],
|
||||
references: [applications.applicationId],
|
||||
}),
|
||||
postgres: one(postgres, {
|
||||
fields: [mounts.postgresId],
|
||||
references: [postgres.postgresId],
|
||||
}),
|
||||
mariadb: one(mariadb, {
|
||||
fields: [mounts.mariadbId],
|
||||
references: [mariadb.mariadbId],
|
||||
}),
|
||||
mongo: one(mongo, {
|
||||
fields: [mounts.mongoId],
|
||||
references: [mongo.mongoId],
|
||||
}),
|
||||
mysql: one(mysql, {
|
||||
fields: [mounts.mysqlId],
|
||||
references: [mysql.mysqlId],
|
||||
}),
|
||||
redis: one(redis, {
|
||||
fields: [mounts.redisId],
|
||||
references: [redis.redisId],
|
||||
}),
|
||||
}));
|
||||
|
||||
const createSchema = createInsertSchema(mounts, {
|
||||
applicationId: z.string(),
|
||||
type: z.enum(["bind", "volume", "file"]),
|
||||
hostPath: z.string().optional(),
|
||||
volumeName: z.string().optional(),
|
||||
content: z.string().optional(),
|
||||
mountPath: z.string().min(1),
|
||||
mountId: z.string().optional(),
|
||||
serviceType: z
|
||||
.enum(["application", "postgres", "mysql", "mariadb", "mongo", "redis"])
|
||||
.default("application"),
|
||||
});
|
||||
|
||||
export type ServiceType = NonNullable<
|
||||
z.infer<typeof createSchema>["serviceType"]
|
||||
>;
|
||||
|
||||
export const apiCreateMount = createSchema
|
||||
.pick({
|
||||
type: true,
|
||||
hostPath: true,
|
||||
volumeName: true,
|
||||
content: true,
|
||||
mountPath: true,
|
||||
serviceType: true,
|
||||
})
|
||||
.extend({
|
||||
serviceId: z.string().min(1),
|
||||
});
|
||||
|
||||
export const apiFindOneMount = createSchema
|
||||
.pick({
|
||||
mountId: true,
|
||||
})
|
||||
.required();
|
||||
|
||||
export const apiRemoveMount = createSchema
|
||||
.pick({
|
||||
mountId: true,
|
||||
})
|
||||
// .extend({
|
||||
// appName: z.string().min(1),
|
||||
// })
|
||||
.required();
|
||||
|
||||
export const apiFindMountByApplicationId = createSchema
|
||||
.extend({
|
||||
serviceId: z.string().min(1),
|
||||
})
|
||||
.pick({
|
||||
serviceId: true,
|
||||
serviceType: true,
|
||||
})
|
||||
.required();
|
||||
132
server/db/schema/mysql.ts
Normal file
132
server/db/schema/mysql.ts
Normal file
@@ -0,0 +1,132 @@
|
||||
import { relations } from "drizzle-orm";
|
||||
import { createInsertSchema } from "drizzle-zod";
|
||||
import { z } from "zod";
|
||||
import { nanoid } from "nanoid";
|
||||
import { applicationStatus } from "./shared";
|
||||
import { integer, pgTable, text } from "drizzle-orm/pg-core";
|
||||
import { projects } from "./project";
|
||||
import { backups } from "./backups";
|
||||
import { mounts } from "./mount";
|
||||
import { generateAppName } from "./utils";
|
||||
|
||||
export const mysql = pgTable("mysql", {
|
||||
mysqlId: text("mysqlId")
|
||||
.notNull()
|
||||
.primaryKey()
|
||||
.$defaultFn(() => nanoid()),
|
||||
name: text("name").notNull(),
|
||||
appName: text("appName")
|
||||
.notNull()
|
||||
.$defaultFn(() => generateAppName("mysql"))
|
||||
.unique(),
|
||||
description: text("description"),
|
||||
databaseName: text("databaseName").notNull(),
|
||||
databaseUser: text("databaseUser").notNull(),
|
||||
databasePassword: text("databasePassword").notNull(),
|
||||
databaseRootPassword: text("rootPassword").notNull(),
|
||||
dockerImage: text("dockerImage").notNull(),
|
||||
command: text("command"),
|
||||
env: text("env"),
|
||||
memoryReservation: integer("memoryReservation"),
|
||||
memoryLimit: integer("memoryLimit"),
|
||||
cpuReservation: integer("cpuReservation"),
|
||||
cpuLimit: integer("cpuLimit"),
|
||||
externalPort: integer("externalPort"),
|
||||
applicationStatus: applicationStatus("applicationStatus")
|
||||
.notNull()
|
||||
.default("idle"),
|
||||
createdAt: text("createdAt")
|
||||
.notNull()
|
||||
.$defaultFn(() => new Date().toISOString()),
|
||||
projectId: text("projectId")
|
||||
.notNull()
|
||||
.references(() => projects.projectId, { onDelete: "cascade" }),
|
||||
});
|
||||
|
||||
export const mysqlRelations = relations(mysql, ({ one, many }) => ({
|
||||
project: one(projects, {
|
||||
fields: [mysql.projectId],
|
||||
references: [projects.projectId],
|
||||
}),
|
||||
backups: many(backups),
|
||||
mounts: many(mounts),
|
||||
}));
|
||||
|
||||
const createSchema = createInsertSchema(mysql, {
|
||||
mysqlId: z.string(),
|
||||
appName: z.string().min(1),
|
||||
createdAt: z.string(),
|
||||
name: z.string().min(1),
|
||||
databaseName: z.string().min(1),
|
||||
databaseUser: z.string().min(1),
|
||||
databasePassword: z.string(),
|
||||
databaseRootPassword: z.string().optional(),
|
||||
dockerImage: z.string().default("mysql:8"),
|
||||
command: z.string().optional(),
|
||||
env: z.string().optional(),
|
||||
memoryReservation: z.number().optional(),
|
||||
memoryLimit: z.number().optional(),
|
||||
cpuReservation: z.number().optional(),
|
||||
cpuLimit: z.number().optional(),
|
||||
projectId: z.string(),
|
||||
applicationStatus: z.enum(["idle", "running", "done", "error"]),
|
||||
externalPort: z.number(),
|
||||
description: z.string().optional(),
|
||||
});
|
||||
|
||||
export const apiCreateMySql = createSchema
|
||||
.pick({
|
||||
name: true,
|
||||
dockerImage: true,
|
||||
projectId: true,
|
||||
description: true,
|
||||
databaseName: true,
|
||||
databaseUser: true,
|
||||
databasePassword: true,
|
||||
databaseRootPassword: true,
|
||||
})
|
||||
.required();
|
||||
|
||||
export const apiFindOneMySql = createSchema
|
||||
.pick({
|
||||
mysqlId: true,
|
||||
})
|
||||
.required();
|
||||
|
||||
export const apiChangeMySqlStatus = createSchema
|
||||
.pick({
|
||||
mysqlId: true,
|
||||
applicationStatus: true,
|
||||
})
|
||||
.required();
|
||||
|
||||
export const apiSaveEnviromentVariablesMySql = createSchema
|
||||
.pick({
|
||||
mysqlId: true,
|
||||
env: true,
|
||||
})
|
||||
.required();
|
||||
|
||||
export const apiSaveExternalPortMySql = createSchema
|
||||
.pick({
|
||||
mysqlId: true,
|
||||
externalPort: true,
|
||||
})
|
||||
.required();
|
||||
|
||||
export const apiResetMysql = createSchema
|
||||
.pick({
|
||||
mysqlId: true,
|
||||
appName: true,
|
||||
})
|
||||
.required();
|
||||
|
||||
export const apiDeployMySql = createSchema
|
||||
.pick({
|
||||
mysqlId: true,
|
||||
})
|
||||
.required();
|
||||
|
||||
export const apiUpdateMySql = createSchema.partial().extend({
|
||||
mysqlId: z.string().min(1),
|
||||
});
|
||||
61
server/db/schema/port.ts
Normal file
61
server/db/schema/port.ts
Normal file
@@ -0,0 +1,61 @@
|
||||
import { relations } from "drizzle-orm";
|
||||
import { createInsertSchema } from "drizzle-zod";
|
||||
import { z } from "zod";
|
||||
import { nanoid } from "nanoid";
|
||||
import { integer, pgEnum, pgTable, text } from "drizzle-orm/pg-core";
|
||||
import { applications } from "./application";
|
||||
|
||||
export const protocolType = pgEnum("protocolType", ["tcp", "udp"]);
|
||||
|
||||
export const ports = pgTable("port", {
|
||||
portId: text("portId")
|
||||
.notNull()
|
||||
.primaryKey()
|
||||
.$defaultFn(() => nanoid()),
|
||||
publishedPort: integer("publishedPort").notNull(),
|
||||
targetPort: integer("targetPort").notNull(),
|
||||
protocol: protocolType("protocol").notNull(),
|
||||
|
||||
applicationId: text("applicationId")
|
||||
.notNull()
|
||||
.references(() => applications.applicationId, { onDelete: "cascade" }),
|
||||
});
|
||||
|
||||
export const portsRelations = relations(ports, ({ one }) => ({
|
||||
application: one(applications, {
|
||||
fields: [ports.applicationId],
|
||||
references: [applications.applicationId],
|
||||
}),
|
||||
}));
|
||||
|
||||
const createSchema = createInsertSchema(ports, {
|
||||
portId: z.string().min(1),
|
||||
applicationId: z.string().min(1),
|
||||
publishedPort: z.number(),
|
||||
targetPort: z.number(),
|
||||
protocol: z.enum(["tcp", "udp"]).default("tcp"),
|
||||
});
|
||||
|
||||
export const apiCreatePort = createSchema
|
||||
.pick({
|
||||
publishedPort: true,
|
||||
targetPort: true,
|
||||
protocol: true,
|
||||
applicationId: true,
|
||||
})
|
||||
.required();
|
||||
|
||||
export const apiFindOnePort = createSchema
|
||||
.pick({
|
||||
portId: true,
|
||||
})
|
||||
.required();
|
||||
|
||||
export const apiUpdatePort = createSchema
|
||||
.pick({
|
||||
portId: true,
|
||||
publishedPort: true,
|
||||
targetPort: true,
|
||||
protocol: true,
|
||||
})
|
||||
.required();
|
||||
128
server/db/schema/postgres.ts
Normal file
128
server/db/schema/postgres.ts
Normal file
@@ -0,0 +1,128 @@
|
||||
import { relations } from "drizzle-orm";
|
||||
import { createInsertSchema } from "drizzle-zod";
|
||||
import { z } from "zod";
|
||||
import { nanoid } from "nanoid";
|
||||
import { applicationStatus } from "./shared";
|
||||
import { integer, pgTable, text } from "drizzle-orm/pg-core";
|
||||
import { projects } from "./project";
|
||||
import { backups } from "./backups";
|
||||
import { mounts } from "./mount";
|
||||
import { generateAppName } from "./utils";
|
||||
|
||||
export const postgres = pgTable("postgres", {
|
||||
postgresId: text("postgresId")
|
||||
.notNull()
|
||||
.primaryKey()
|
||||
.$defaultFn(() => nanoid()),
|
||||
name: text("name").notNull(),
|
||||
appName: text("appName")
|
||||
.notNull()
|
||||
.$defaultFn(() => generateAppName("postgres"))
|
||||
.unique(),
|
||||
databaseName: text("databaseName").notNull(),
|
||||
databaseUser: text("databaseUser").notNull(),
|
||||
databasePassword: text("databasePassword").notNull(),
|
||||
description: text("description"),
|
||||
dockerImage: text("dockerImage").notNull(),
|
||||
command: text("command"),
|
||||
env: text("env"),
|
||||
memoryReservation: integer("memoryReservation"),
|
||||
externalPort: integer("externalPort"),
|
||||
memoryLimit: integer("memoryLimit"),
|
||||
cpuReservation: integer("cpuReservation"),
|
||||
cpuLimit: integer("cpuLimit"),
|
||||
applicationStatus: applicationStatus("applicationStatus")
|
||||
.notNull()
|
||||
.default("idle"),
|
||||
createdAt: text("createdAt")
|
||||
.notNull()
|
||||
.$defaultFn(() => new Date().toISOString()),
|
||||
projectId: text("projectId")
|
||||
.notNull()
|
||||
.references(() => projects.projectId, { onDelete: "cascade" }),
|
||||
});
|
||||
|
||||
export const postgresRelations = relations(postgres, ({ one, many }) => ({
|
||||
project: one(projects, {
|
||||
fields: [postgres.projectId],
|
||||
references: [projects.projectId],
|
||||
}),
|
||||
backups: many(backups),
|
||||
mounts: many(mounts),
|
||||
}));
|
||||
|
||||
const createSchema = createInsertSchema(postgres, {
|
||||
postgresId: z.string(),
|
||||
name: z.string().min(1),
|
||||
databasePassword: z.string(),
|
||||
databaseName: z.string().min(1),
|
||||
databaseUser: z.string().min(1),
|
||||
dockerImage: z.string().default("postgres:15"),
|
||||
command: z.string().optional(),
|
||||
env: z.string().optional(),
|
||||
memoryReservation: z.number().optional(),
|
||||
memoryLimit: z.number().optional(),
|
||||
cpuReservation: z.number().optional(),
|
||||
cpuLimit: z.number().optional(),
|
||||
projectId: z.string(),
|
||||
applicationStatus: z.enum(["idle", "running", "done", "error"]),
|
||||
externalPort: z.number(),
|
||||
createdAt: z.string(),
|
||||
description: z.string().optional(),
|
||||
});
|
||||
|
||||
export const apiCreatePostgres = createSchema
|
||||
.pick({
|
||||
name: true,
|
||||
databaseName: true,
|
||||
databaseUser: true,
|
||||
databasePassword: true,
|
||||
dockerImage: true,
|
||||
projectId: true,
|
||||
description: true,
|
||||
})
|
||||
.required();
|
||||
|
||||
export const apiFindOnePostgres = createSchema
|
||||
.pick({
|
||||
postgresId: true,
|
||||
})
|
||||
.required();
|
||||
|
||||
export const apiChangePostgresStatus = createSchema
|
||||
.pick({
|
||||
postgresId: true,
|
||||
applicationStatus: true,
|
||||
})
|
||||
.required();
|
||||
|
||||
export const apiSaveEnviromentVariablesPostgres = createSchema
|
||||
.pick({
|
||||
postgresId: true,
|
||||
env: true,
|
||||
})
|
||||
.required();
|
||||
|
||||
export const apiSaveExternalPortPostgres = createSchema
|
||||
.pick({
|
||||
postgresId: true,
|
||||
externalPort: true,
|
||||
})
|
||||
.required();
|
||||
|
||||
export const apiDeployPostgres = createSchema
|
||||
.pick({
|
||||
postgresId: true,
|
||||
})
|
||||
.required();
|
||||
|
||||
export const apiResetPostgres = createSchema
|
||||
.pick({
|
||||
postgresId: true,
|
||||
appName: true,
|
||||
})
|
||||
.required();
|
||||
|
||||
export const apiUpdatePostgres = createSchema.partial().extend({
|
||||
postgresId: z.string().min(1),
|
||||
});
|
||||
72
server/db/schema/project.ts
Normal file
72
server/db/schema/project.ts
Normal file
@@ -0,0 +1,72 @@
|
||||
import { relations } from "drizzle-orm";
|
||||
|
||||
import { createInsertSchema } from "drizzle-zod";
|
||||
import { z } from "zod";
|
||||
import { nanoid } from "nanoid";
|
||||
import { pgTable, text } from "drizzle-orm/pg-core";
|
||||
import { mysql } from "./mysql";
|
||||
import { postgres } from "./postgres";
|
||||
import { mariadb } from "./mariadb";
|
||||
import { applications } from "./application";
|
||||
import { mongo } from "./mongo";
|
||||
import { redis } from "./redis";
|
||||
import { admins } from "./admin";
|
||||
|
||||
export const projects = pgTable("project", {
|
||||
projectId: text("projectId")
|
||||
.notNull()
|
||||
.primaryKey()
|
||||
.$defaultFn(() => nanoid()),
|
||||
name: text("name").notNull(),
|
||||
description: text("description"),
|
||||
createdAt: text("createdAt")
|
||||
.notNull()
|
||||
.$defaultFn(() => new Date().toISOString()),
|
||||
adminId: text("adminId")
|
||||
.notNull()
|
||||
.references(() => admins.adminId, { onDelete: "cascade" }),
|
||||
});
|
||||
|
||||
export const projectRelations = relations(projects, ({ many, one }) => ({
|
||||
mysql: many(mysql),
|
||||
postgres: many(postgres),
|
||||
mariadb: many(mariadb),
|
||||
applications: many(applications),
|
||||
mongo: many(mongo),
|
||||
redis: many(redis),
|
||||
admin: one(admins, {
|
||||
fields: [projects.adminId],
|
||||
references: [admins.adminId],
|
||||
}),
|
||||
}));
|
||||
|
||||
const createSchema = createInsertSchema(projects, {
|
||||
projectId: z.string().min(1),
|
||||
name: z.string().min(1),
|
||||
description: z.string().optional(),
|
||||
});
|
||||
|
||||
export const apiCreateProject = createSchema.pick({
|
||||
name: true,
|
||||
description: true,
|
||||
});
|
||||
|
||||
export const apiFindOneProject = createSchema
|
||||
.pick({
|
||||
projectId: true,
|
||||
})
|
||||
.required();
|
||||
|
||||
export const apiRemoveProject = createSchema
|
||||
.pick({
|
||||
projectId: true,
|
||||
})
|
||||
.required();
|
||||
|
||||
export const apiUpdateProject = createSchema
|
||||
.pick({
|
||||
name: true,
|
||||
description: true,
|
||||
projectId: true,
|
||||
})
|
||||
.required();
|
||||
60
server/db/schema/redirects.ts
Normal file
60
server/db/schema/redirects.ts
Normal file
@@ -0,0 +1,60 @@
|
||||
import { relations } from "drizzle-orm";
|
||||
import { createInsertSchema } from "drizzle-zod";
|
||||
import { nanoid } from "nanoid";
|
||||
import { z } from "zod";
|
||||
import { boolean, pgTable, serial, text } from "drizzle-orm/pg-core";
|
||||
import { applications } from "./application";
|
||||
|
||||
export const redirects = pgTable("redirect", {
|
||||
redirectId: text("redirectId")
|
||||
.notNull()
|
||||
.primaryKey()
|
||||
.$defaultFn(() => nanoid()),
|
||||
regex: text("regex").notNull(),
|
||||
replacement: text("replacement").notNull(),
|
||||
permanent: boolean("permanent").notNull().default(false),
|
||||
uniqueConfigKey: serial("uniqueConfigKey"),
|
||||
createdAt: text("createdAt")
|
||||
.notNull()
|
||||
.$defaultFn(() => new Date().toISOString()),
|
||||
applicationId: text("applicationId")
|
||||
.notNull()
|
||||
.references(() => applications.applicationId, { onDelete: "cascade" }),
|
||||
});
|
||||
|
||||
export const redirectRelations = relations(redirects, ({ one }) => ({
|
||||
application: one(applications, {
|
||||
fields: [redirects.applicationId],
|
||||
references: [applications.applicationId],
|
||||
}),
|
||||
}));
|
||||
const createSchema = createInsertSchema(redirects, {
|
||||
redirectId: z.string().min(1),
|
||||
regex: z.string().min(1),
|
||||
replacement: z.string().min(1),
|
||||
permanent: z.boolean().optional(),
|
||||
});
|
||||
|
||||
export const apiFindOneRedirect = createSchema
|
||||
.pick({
|
||||
redirectId: true,
|
||||
})
|
||||
.required();
|
||||
|
||||
export const apiCreateRedirect = createSchema
|
||||
.pick({
|
||||
regex: true,
|
||||
replacement: true,
|
||||
permanent: true,
|
||||
applicationId: true,
|
||||
})
|
||||
.required();
|
||||
|
||||
export const apiUpdateRedirect = createSchema
|
||||
.pick({
|
||||
redirectId: true,
|
||||
regex: true,
|
||||
replacement: true,
|
||||
permanent: true,
|
||||
})
|
||||
.required();
|
||||
122
server/db/schema/redis.ts
Normal file
122
server/db/schema/redis.ts
Normal file
@@ -0,0 +1,122 @@
|
||||
import { relations } from "drizzle-orm";
|
||||
import { z } from "zod";
|
||||
import { createInsertSchema } from "drizzle-zod";
|
||||
import { nanoid } from "nanoid";
|
||||
import { applicationStatus } from "./shared";
|
||||
import { integer, pgTable, text } from "drizzle-orm/pg-core";
|
||||
import { projects } from "./project";
|
||||
import { mounts } from "./mount";
|
||||
import { generateAppName } from "./utils";
|
||||
|
||||
export const redis = pgTable("redis", {
|
||||
redisId: text("redisId")
|
||||
.notNull()
|
||||
.primaryKey()
|
||||
.$defaultFn(() => nanoid()),
|
||||
name: text("name").notNull(),
|
||||
appName: text("appName")
|
||||
.notNull()
|
||||
.$defaultFn(() => generateAppName("redis"))
|
||||
.unique(),
|
||||
description: text("description"),
|
||||
databasePassword: text("password").notNull(),
|
||||
dockerImage: text("dockerImage").notNull(),
|
||||
command: text("command"),
|
||||
env: text("env"),
|
||||
memoryReservation: integer("memoryReservation"),
|
||||
memoryLimit: integer("memoryLimit"),
|
||||
cpuReservation: integer("cpuReservation"),
|
||||
cpuLimit: integer("cpuLimit"),
|
||||
externalPort: integer("externalPort"),
|
||||
createdAt: text("createdAt")
|
||||
.notNull()
|
||||
.$defaultFn(() => new Date().toISOString()),
|
||||
applicationStatus: applicationStatus("applicationStatus")
|
||||
.notNull()
|
||||
.default("idle"),
|
||||
projectId: text("projectId")
|
||||
.notNull()
|
||||
.references(() => projects.projectId, { onDelete: "cascade" }),
|
||||
});
|
||||
|
||||
export const redisRelations = relations(redis, ({ one, many }) => ({
|
||||
project: one(projects, {
|
||||
fields: [redis.projectId],
|
||||
references: [projects.projectId],
|
||||
}),
|
||||
mounts: many(mounts),
|
||||
}));
|
||||
|
||||
const createSchema = createInsertSchema(redis, {
|
||||
redisId: z.string(),
|
||||
appName: z.string().min(1),
|
||||
createdAt: z.string(),
|
||||
name: z.string().min(1),
|
||||
databasePassword: z.string(),
|
||||
dockerImage: z.string().default("redis:8"),
|
||||
command: z.string().optional(),
|
||||
env: z.string().optional(),
|
||||
memoryReservation: z.number().optional(),
|
||||
memoryLimit: z.number().optional(),
|
||||
cpuReservation: z.number().optional(),
|
||||
cpuLimit: z.number().optional(),
|
||||
projectId: z.string(),
|
||||
applicationStatus: z.enum(["idle", "running", "done", "error"]),
|
||||
externalPort: z.number(),
|
||||
description: z.string().optional(),
|
||||
});
|
||||
|
||||
export const apiCreateRedis = createSchema
|
||||
.pick({
|
||||
name: true,
|
||||
databasePassword: true,
|
||||
dockerImage: true,
|
||||
projectId: true,
|
||||
description: true,
|
||||
})
|
||||
|
||||
.required();
|
||||
|
||||
export const apiFindOneRedis = createSchema
|
||||
.pick({
|
||||
redisId: true,
|
||||
})
|
||||
.required();
|
||||
|
||||
export const apiChangeRedisStatus = createSchema
|
||||
.pick({
|
||||
redisId: true,
|
||||
applicationStatus: true,
|
||||
})
|
||||
.required();
|
||||
|
||||
export const apiSaveEnviromentVariablesRedis = createSchema
|
||||
.pick({
|
||||
redisId: true,
|
||||
env: true,
|
||||
})
|
||||
.required();
|
||||
|
||||
export const apiSaveExternalPortRedis = createSchema
|
||||
.pick({
|
||||
redisId: true,
|
||||
externalPort: true,
|
||||
})
|
||||
.required();
|
||||
|
||||
export const apiDeployRedis = createSchema
|
||||
.pick({
|
||||
redisId: true,
|
||||
})
|
||||
.required();
|
||||
|
||||
export const apiResetRedis = createSchema
|
||||
.pick({
|
||||
redisId: true,
|
||||
appName: true,
|
||||
})
|
||||
.required();
|
||||
|
||||
export const apiUpdateRedis = createSchema.partial().extend({
|
||||
redisId: z.string().min(1),
|
||||
});
|
||||
61
server/db/schema/security.ts
Normal file
61
server/db/schema/security.ts
Normal file
@@ -0,0 +1,61 @@
|
||||
import { relations } from "drizzle-orm";
|
||||
import { createInsertSchema } from "drizzle-zod";
|
||||
import { nanoid } from "nanoid";
|
||||
import { z } from "zod";
|
||||
import { pgTable, text, unique } from "drizzle-orm/pg-core";
|
||||
import { applications } from "./application";
|
||||
|
||||
export const security = pgTable(
|
||||
"security",
|
||||
{
|
||||
securityId: text("securityId")
|
||||
.notNull()
|
||||
.primaryKey()
|
||||
.$defaultFn(() => nanoid()),
|
||||
username: text("username").notNull(),
|
||||
password: text("password").notNull(),
|
||||
createdAt: text("createdAt")
|
||||
.notNull()
|
||||
.$defaultFn(() => new Date().toISOString()),
|
||||
applicationId: text("applicationId")
|
||||
.notNull()
|
||||
.references(() => applications.applicationId, { onDelete: "cascade" }),
|
||||
},
|
||||
(t) => ({
|
||||
unq: unique().on(t.username, t.applicationId),
|
||||
}),
|
||||
);
|
||||
|
||||
export const securityRelations = relations(security, ({ one }) => ({
|
||||
application: one(applications, {
|
||||
fields: [security.applicationId],
|
||||
references: [applications.applicationId],
|
||||
}),
|
||||
}));
|
||||
const createSchema = createInsertSchema(security, {
|
||||
securityId: z.string().min(1),
|
||||
username: z.string().min(1),
|
||||
password: z.string().min(1),
|
||||
});
|
||||
|
||||
export const apiFindOneSecurity = createSchema
|
||||
.pick({
|
||||
securityId: true,
|
||||
})
|
||||
.required();
|
||||
|
||||
export const apiCreateSecurity = createSchema
|
||||
.pick({
|
||||
applicationId: true,
|
||||
username: true,
|
||||
password: true,
|
||||
})
|
||||
.required();
|
||||
|
||||
export const apiUpdateSecurity = createSchema
|
||||
.pick({
|
||||
securityId: true,
|
||||
username: true,
|
||||
password: true,
|
||||
})
|
||||
.required();
|
||||
20
server/db/schema/session.ts
Normal file
20
server/db/schema/session.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import { auth } from "./auth";
|
||||
import { pgTable, text, timestamp } from "drizzle-orm/pg-core";
|
||||
|
||||
// export const sessionTable = sqliteTable("session", {
|
||||
// id: text("id").notNull().primaryKey(),
|
||||
// userId: text("user_id")
|
||||
// .notNull()
|
||||
// .references(() => users.id),
|
||||
// expiresAt: integer("expires_at").notNull(),
|
||||
// });
|
||||
export const sessionTable = pgTable("session", {
|
||||
id: text("id").primaryKey(),
|
||||
userId: text("user_id")
|
||||
.notNull()
|
||||
.references(() => auth.id, { onDelete: "cascade" }),
|
||||
expiresAt: timestamp("expires_at", {
|
||||
withTimezone: true,
|
||||
mode: "date",
|
||||
}).notNull(),
|
||||
});
|
||||
13
server/db/schema/shared.ts
Normal file
13
server/db/schema/shared.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import { pgEnum } from "drizzle-orm/pg-core";
|
||||
|
||||
export const applicationStatus = pgEnum("applicationStatus", [
|
||||
"idle",
|
||||
"running",
|
||||
"done",
|
||||
"error",
|
||||
]);
|
||||
|
||||
export const certificateType = pgEnum("certificateType", [
|
||||
"letsencrypt",
|
||||
"none",
|
||||
]);
|
||||
27
server/db/schema/source.ts
Normal file
27
server/db/schema/source.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
import { createInsertSchema } from "drizzle-zod";
|
||||
import { z } from "zod";
|
||||
import { nanoid } from "nanoid";
|
||||
import { pgTable, text } from "drizzle-orm/pg-core";
|
||||
|
||||
export const source = pgTable("project", {
|
||||
projectId: text("projectId")
|
||||
.notNull()
|
||||
.primaryKey()
|
||||
.$defaultFn(() => nanoid()),
|
||||
name: text("name").notNull(),
|
||||
description: text("description"),
|
||||
createdAt: text("createdAt")
|
||||
.notNull()
|
||||
.$defaultFn(() => new Date().toISOString()),
|
||||
});
|
||||
|
||||
const createSchema = createInsertSchema(source, {
|
||||
name: z.string().min(1),
|
||||
description: z.string(),
|
||||
projectId: z.string(),
|
||||
});
|
||||
|
||||
export const apiCreate = createSchema.pick({
|
||||
name: true,
|
||||
description: true,
|
||||
});
|
||||
121
server/db/schema/user.ts
Normal file
121
server/db/schema/user.ts
Normal file
@@ -0,0 +1,121 @@
|
||||
import { createInsertSchema } from "drizzle-zod";
|
||||
import { nanoid } from "nanoid";
|
||||
import { relations, sql } from "drizzle-orm";
|
||||
import { boolean, pgTable, text, timestamp } from "drizzle-orm/pg-core";
|
||||
import { auth } from "./auth";
|
||||
import { admins } from "./admin";
|
||||
import { z } from "zod";
|
||||
/**
|
||||
* This is an example of how to use the multi-project schema feature of Drizzle ORM. Use the same
|
||||
* database instance for multiple projects.
|
||||
*
|
||||
* @see https://orm.drizzle.team/docs/goodies#multi-project-schema
|
||||
*/
|
||||
|
||||
export const users = pgTable("user", {
|
||||
userId: text("userId")
|
||||
.notNull()
|
||||
.primaryKey()
|
||||
.$defaultFn(() => nanoid()),
|
||||
|
||||
token: text("token").notNull(),
|
||||
isRegistered: boolean("isRegistered").notNull().default(false),
|
||||
expirationDate: timestamp("expirationDate", {
|
||||
precision: 3,
|
||||
mode: "string",
|
||||
}).notNull(),
|
||||
createdAt: text("createdAt")
|
||||
.notNull()
|
||||
.$defaultFn(() => new Date().toISOString()),
|
||||
canCreateProjects: boolean("canCreateProjects").notNull().default(false),
|
||||
canCreateServices: boolean("canCreateServices").notNull().default(false),
|
||||
canDeleteProjects: boolean("canDeleteProjects").notNull().default(false),
|
||||
canDeleteServices: boolean("canDeleteServices").notNull().default(false),
|
||||
canAccessToDocker: boolean("canAccessToDocker").notNull().default(false),
|
||||
canAccessToTraefikFiles: boolean("canAccessToTraefikFiles")
|
||||
.notNull()
|
||||
.default(false),
|
||||
accesedProjects: text("accesedProjects")
|
||||
.array()
|
||||
.notNull()
|
||||
.default(sql`ARRAY[]::text[]`),
|
||||
accesedServices: text("accesedServices")
|
||||
.array()
|
||||
.notNull()
|
||||
.default(sql`ARRAY[]::text[]`),
|
||||
adminId: text("adminId")
|
||||
.notNull()
|
||||
.references(() => admins.adminId, { onDelete: "cascade" }),
|
||||
authId: text("authId")
|
||||
.notNull()
|
||||
.references(() => auth.id, { onDelete: "cascade" }),
|
||||
});
|
||||
|
||||
export const usersRelations = relations(users, ({ one }) => ({
|
||||
auth: one(auth, {
|
||||
fields: [users.authId],
|
||||
references: [auth.id],
|
||||
}),
|
||||
admin: one(admins, {
|
||||
fields: [users.adminId],
|
||||
references: [admins.adminId],
|
||||
}),
|
||||
}));
|
||||
|
||||
const createSchema = createInsertSchema(users, {
|
||||
userId: z.string().min(1),
|
||||
authId: z.string().min(1),
|
||||
token: z.string().min(1),
|
||||
isRegistered: z.boolean().optional(),
|
||||
adminId: z.string(),
|
||||
accesedProjects: z.array(z.string()).optional(),
|
||||
accesedServices: z.array(z.string()).optional(),
|
||||
canCreateProjects: z.boolean().optional(),
|
||||
canCreateServices: z.boolean().optional(),
|
||||
canDeleteProjects: z.boolean().optional(),
|
||||
canDeleteServices: z.boolean().optional(),
|
||||
canAccessToDocker: z.boolean().optional(),
|
||||
canAccessToTraefikFiles: z.boolean().optional(),
|
||||
});
|
||||
|
||||
export const apiCreateUserInvitation = createSchema.pick({}).extend({
|
||||
email: z.string().email(),
|
||||
});
|
||||
|
||||
export const apiRemoveUser = createSchema
|
||||
.pick({
|
||||
authId: true,
|
||||
})
|
||||
.required();
|
||||
|
||||
export const apiFindOneToken = createSchema
|
||||
.pick({
|
||||
token: true,
|
||||
})
|
||||
.required();
|
||||
|
||||
export const apiAssignPermissions = createSchema
|
||||
.pick({
|
||||
userId: true,
|
||||
canCreateProjects: true,
|
||||
canCreateServices: true,
|
||||
canDeleteProjects: true,
|
||||
canDeleteServices: true,
|
||||
accesedProjects: true,
|
||||
accesedServices: true,
|
||||
canAccessToTraefikFiles: true,
|
||||
canAccessToDocker: true,
|
||||
})
|
||||
.required();
|
||||
|
||||
export const apiFindOneUser = createSchema
|
||||
.pick({
|
||||
userId: true,
|
||||
})
|
||||
.required();
|
||||
|
||||
export const apiFindOneUserByAuth = createSchema
|
||||
.pick({
|
||||
authId: true,
|
||||
})
|
||||
.required();
|
||||
15
server/db/schema/utils.ts
Normal file
15
server/db/schema/utils.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import { faker } from "@faker-js/faker";
|
||||
import { customAlphabet } from "nanoid";
|
||||
|
||||
const alphabet = "abcdefghijklmnopqrstuvwxyz123456789";
|
||||
|
||||
const customNanoid = customAlphabet(alphabet, 6);
|
||||
|
||||
export const generateAppName = (type: string) => {
|
||||
const verb = faker.hacker.verb().replace(/ /g, "-");
|
||||
const adjective = faker.hacker.adjective().replace(/ /g, "-");
|
||||
const noun = faker.hacker.noun().replace(/ /g, "-");
|
||||
const randomFakerElement = `${verb}-${adjective}-${noun}`;
|
||||
const nanoidPart = customNanoid();
|
||||
return `${type}-${randomFakerElement}-${nanoidPart}`;
|
||||
};
|
||||
35
server/db/seed.ts
Normal file
35
server/db/seed.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
import bc from "bcrypt";
|
||||
import { users } from "./schema";
|
||||
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);
|
||||
|
||||
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);
|
||||
});
|
||||
145
server/monitoring/utilts.ts
Normal file
145
server/monitoring/utilts.ts
Normal file
@@ -0,0 +1,145 @@
|
||||
import { promises } from "node:fs";
|
||||
import { MONITORING_PATH } from "../constants";
|
||||
import dockerstats from "dockerstats";
|
||||
import osUtils from "node-os-utils";
|
||||
|
||||
export const recordAdvancedStats = async (
|
||||
appName: string,
|
||||
containerId: string,
|
||||
) => {
|
||||
await promises.mkdir(`${MONITORING_PATH}/${appName}`, { recursive: true });
|
||||
|
||||
const result = await dockerstats.dockerContainerStats(containerId);
|
||||
|
||||
if (!result || result.length === 0 || !result[0]) return;
|
||||
|
||||
const { memoryStats, cpuStats, precpuStats, netIO, blockIO } = result[0];
|
||||
|
||||
const memoryUsage = memoryStats.usage / 1024 / 1024;
|
||||
const memoryTotal = memoryStats.limit / 1024 / 1024;
|
||||
const memoryFree = memoryTotal - memoryUsage;
|
||||
const memoryUsedPercentage = (memoryUsage / memoryTotal) * 100;
|
||||
|
||||
const cpuDelta =
|
||||
cpuStats.cpu_usage.total_usage - precpuStats.cpu_usage.total_usage;
|
||||
const systemDelta = cpuStats.system_cpu_usage - precpuStats.system_cpu_usage;
|
||||
const onlineCpus = cpuStats.online_cpus;
|
||||
|
||||
// Calcular el porcentaje de uso del CPU
|
||||
const cpuPercent = (cpuDelta / systemDelta) * onlineCpus * 100;
|
||||
|
||||
// Extraer los valores de entrada y salida del objeto netIO
|
||||
const networkInBytes = netIO.rx;
|
||||
const networkOutBytes = netIO.wx;
|
||||
|
||||
// Convertir bytes a Megabytes
|
||||
const networkInMB = networkInBytes / 1024 / 1024;
|
||||
const networkOutMB = networkOutBytes / 1024 / 1024;
|
||||
|
||||
// BlockIO
|
||||
|
||||
const blockRead = blockIO.r;
|
||||
const blockWrite = blockIO.w;
|
||||
|
||||
const blockInMBBlocks = blockRead / 1024 / 1024;
|
||||
const blockOutMBBlocks = blockWrite / 1024 / 1024;
|
||||
|
||||
// Disk
|
||||
const disk = await osUtils.drive.info("/");
|
||||
|
||||
const diskUsage = disk.usedGb;
|
||||
const diskTotal = disk.totalGb;
|
||||
const diskUsedPercentage = disk.usedPercentage;
|
||||
const diskFree = disk.freeGb;
|
||||
|
||||
await updateStatsFile(appName, "cpu", cpuPercent);
|
||||
await updateStatsFile(appName, "memory", {
|
||||
used: memoryUsage,
|
||||
free: memoryFree,
|
||||
usedPercentage: memoryUsedPercentage,
|
||||
total: memoryTotal,
|
||||
});
|
||||
await updateStatsFile(appName, "block", {
|
||||
readMb: blockInMBBlocks,
|
||||
writeMb: blockOutMBBlocks,
|
||||
});
|
||||
await updateStatsFile(appName, "network", {
|
||||
inputMb: networkInMB,
|
||||
outputMb: networkOutMB,
|
||||
});
|
||||
|
||||
if (appName === "dokploy") {
|
||||
await updateStatsFile(appName, "disk", {
|
||||
diskTotal: +diskTotal,
|
||||
diskUsedPercentage: +diskUsedPercentage,
|
||||
diskUsage: +diskUsage,
|
||||
diskFree: +diskFree,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
export const getAdvancedStats = async (appName: string) => {
|
||||
return {
|
||||
cpu: await readStatsFile(appName, "cpu"),
|
||||
memory: await readStatsFile(appName, "memory"),
|
||||
disk: await readStatsFile(appName, "disk"),
|
||||
network: await readStatsFile(appName, "network"),
|
||||
block: await readStatsFile(appName, "block"),
|
||||
};
|
||||
};
|
||||
|
||||
export const readStatsFile = async (
|
||||
appName: string,
|
||||
statType: "cpu" | "memory" | "disk" | "network" | "block",
|
||||
) => {
|
||||
try {
|
||||
const filePath = `${MONITORING_PATH}/${appName}/${statType}.json`;
|
||||
const data = await promises.readFile(filePath, "utf-8");
|
||||
return JSON.parse(data);
|
||||
} catch (error) {
|
||||
return [];
|
||||
}
|
||||
};
|
||||
|
||||
export const updateStatsFile = async (
|
||||
appName: string,
|
||||
statType: "cpu" | "memory" | "disk" | "network" | "block",
|
||||
value: number | string | unknown,
|
||||
) => {
|
||||
const stats = await readStatsFile(appName, statType);
|
||||
stats.push({ value, time: new Date() });
|
||||
|
||||
if (stats.length > 288) {
|
||||
stats.shift();
|
||||
}
|
||||
|
||||
const content = JSON.stringify(stats);
|
||||
await promises.writeFile(
|
||||
`${MONITORING_PATH}/${appName}/${statType}.json`,
|
||||
content,
|
||||
);
|
||||
};
|
||||
|
||||
export const readLastValueStatsFile = async (
|
||||
appName: string,
|
||||
statType: "cpu" | "memory" | "disk" | "network" | "block",
|
||||
) => {
|
||||
try {
|
||||
const filePath = `${MONITORING_PATH}/${appName}/${statType}.json`;
|
||||
const data = await promises.readFile(filePath, "utf-8");
|
||||
const stats = JSON.parse(data);
|
||||
return stats[stats.length - 1] || null;
|
||||
} catch (error) {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
export const getLastAdvancedStatsFile = async (appName: string) => {
|
||||
return {
|
||||
cpu: await readLastValueStatsFile(appName, "cpu"),
|
||||
memory: await readLastValueStatsFile(appName, "memory"),
|
||||
disk: await readLastValueStatsFile(appName, "disk"),
|
||||
network: await readLastValueStatsFile(appName, "network"),
|
||||
block: await readLastValueStatsFile(appName, "block"),
|
||||
};
|
||||
};
|
||||
50
server/queues/deployments-queue.ts
Normal file
50
server/queues/deployments-queue.ts
Normal file
@@ -0,0 +1,50 @@
|
||||
import { type Job, Worker } from "bullmq";
|
||||
import {
|
||||
deployApplication,
|
||||
rebuildApplication,
|
||||
} from "../api/services/application";
|
||||
import { myQueue, redisConfig } from "./queueSetup";
|
||||
|
||||
interface DeployJob {
|
||||
applicationId: string;
|
||||
titleLog: string;
|
||||
type: "deploy" | "redeploy";
|
||||
}
|
||||
|
||||
export type DeploymentJob = DeployJob;
|
||||
|
||||
export const deploymentWorker = new Worker(
|
||||
"deployments",
|
||||
async (job: Job<DeploymentJob>) => {
|
||||
try {
|
||||
if (job.data.type === "redeploy") {
|
||||
await rebuildApplication({
|
||||
applicationId: job.data.applicationId,
|
||||
titleLog: job.data.titleLog,
|
||||
});
|
||||
} else if (job.data.type === "deploy") {
|
||||
await deployApplication({
|
||||
applicationId: job.data.applicationId,
|
||||
titleLog: job.data.titleLog,
|
||||
});
|
||||
}
|
||||
} 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}`);
|
||||
}
|
||||
}
|
||||
};
|
||||
26
server/queues/queueSetup.ts
Normal file
26
server/queues/queueSetup.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import { Queue, type ConnectionOptions } from "bullmq";
|
||||
|
||||
export const redisConfig: ConnectionOptions = {
|
||||
host: process.env.NODE_ENV === "production" ? "dokploy-redis" : "127.0.0.1",
|
||||
port: 6379,
|
||||
};
|
||||
// TODO: maybe add a options to clean the queue to the times
|
||||
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 };
|
||||
81
server/server.ts
Normal file
81
server/server.ts
Normal file
@@ -0,0 +1,81 @@
|
||||
import http from "node:http";
|
||||
import { config } from "dotenv";
|
||||
import next from "next";
|
||||
import { deploymentWorker } from "./queues/deployments-queue";
|
||||
import { initCronJobs } from "./utils/backups";
|
||||
import {
|
||||
getPublicIpWithFallback,
|
||||
setupTerminalWebSocketServer,
|
||||
} from "./wss/terminal";
|
||||
import { setupDeploymentLogsWebSocketServer } from "./wss/listen-deployment";
|
||||
import { setupDockerStatsMonitoringSocketServer } from "./wss/docker-stats";
|
||||
import { setupDirectories } from "./setup/config-paths";
|
||||
import { initializeNetwork, initializeSwarm } from "./setup/setup";
|
||||
import {
|
||||
createDefaultMiddlewares,
|
||||
createDefaultServerTraefikConfig,
|
||||
createDefaultTraefikConfig,
|
||||
initializeTraefik,
|
||||
} from "./setup/traefik-setup";
|
||||
import { initializeRedis } from "./setup/redis-setup";
|
||||
import { initializePostgres } from "./setup/postgres-setup";
|
||||
import { migration } from "@/server/db/migration";
|
||||
import { setupDockerContainerLogsWebSocketServer } from "./wss/docker-container-logs";
|
||||
import { setupDockerContainerTerminalWebSocketServer } from "./wss/docker-container-terminal";
|
||||
|
||||
config({ path: ".env" });
|
||||
const PORT = Number.parseInt(process.env.PORT || "3000", 10);
|
||||
const dev = process.env.NODE_ENV !== "production";
|
||||
const app = next({ dev });
|
||||
const handle = app.getRequestHandler();
|
||||
void app.prepare().then(async () => {
|
||||
try {
|
||||
const server = http.createServer((req, res) => {
|
||||
handle(req, res);
|
||||
});
|
||||
|
||||
setupDirectories();
|
||||
createDefaultMiddlewares();
|
||||
await initializeNetwork();
|
||||
await initializeSwarm();
|
||||
createDefaultTraefikConfig();
|
||||
createDefaultServerTraefikConfig();
|
||||
await initializeTraefik();
|
||||
await initializeRedis();
|
||||
await initializePostgres();
|
||||
|
||||
// WEBSOCKET
|
||||
setupDeploymentLogsWebSocketServer(server);
|
||||
setupDockerContainerLogsWebSocketServer(server);
|
||||
setupDockerContainerTerminalWebSocketServer(server);
|
||||
setupTerminalWebSocketServer(server);
|
||||
setupDockerStatsMonitoringSocketServer(server);
|
||||
if (process.env.NODE_ENV === "production") {
|
||||
// Cron Jobs
|
||||
initCronJobs();
|
||||
welcomeServer();
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, 7000));
|
||||
await migration();
|
||||
}
|
||||
server.listen(PORT);
|
||||
deploymentWorker.run();
|
||||
} catch (e) {
|
||||
console.error("Main Server Error", e);
|
||||
}
|
||||
});
|
||||
|
||||
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"),
|
||||
);
|
||||
}
|
||||
39
server/setup/config-paths.ts
Normal file
39
server/setup/config-paths.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
import { existsSync, mkdirSync } from "node:fs";
|
||||
import {
|
||||
APPLICATIONS_PATH,
|
||||
BASE_PATH,
|
||||
CERTIFICATES_PATH,
|
||||
LOGS_PATH,
|
||||
MONITORING_PATH,
|
||||
SSH_PATH,
|
||||
DYNAMIC_TRAEFIK_PATH,
|
||||
MAIN_TRAEFIK_PATH,
|
||||
} from "../constants";
|
||||
|
||||
const createDirectoryIfNotExist = (dirPath: string) => {
|
||||
if (!existsSync(dirPath)) {
|
||||
mkdirSync(dirPath, { recursive: true });
|
||||
console.log(`Directory created: ${dirPath}`);
|
||||
}
|
||||
};
|
||||
|
||||
export const setupDirectories = () => {
|
||||
const directories = [
|
||||
BASE_PATH,
|
||||
MAIN_TRAEFIK_PATH,
|
||||
DYNAMIC_TRAEFIK_PATH,
|
||||
LOGS_PATH,
|
||||
APPLICATIONS_PATH,
|
||||
SSH_PATH,
|
||||
CERTIFICATES_PATH,
|
||||
MONITORING_PATH,
|
||||
];
|
||||
|
||||
for (const dir of directories) {
|
||||
try {
|
||||
createDirectoryIfNotExist(dir);
|
||||
} catch (error) {
|
||||
console.log(error, " On path: ", dir);
|
||||
}
|
||||
}
|
||||
};
|
||||
63
server/setup/postgres-setup.ts
Normal file
63
server/setup/postgres-setup.ts
Normal file
@@ -0,0 +1,63 @@
|
||||
import { docker } from "../constants";
|
||||
import { pullImage } from "../utils/docker/utils";
|
||||
import type { CreateServiceOptions } from "dockerode";
|
||||
export const initializePostgres = async () => {
|
||||
const imageName = "postgres:16";
|
||||
const containerName = "dokploy-postgres";
|
||||
const settings: CreateServiceOptions = {
|
||||
Name: containerName,
|
||||
TaskTemplate: {
|
||||
ContainerSpec: {
|
||||
Image: imageName,
|
||||
Env: [
|
||||
"POSTGRES_USER=dokploy",
|
||||
"POSTGRES_DB=dokploy",
|
||||
"POSTGRES_PASSWORD=amukds4wi9001583845717ad2",
|
||||
],
|
||||
Mounts: [
|
||||
{
|
||||
Type: "volume",
|
||||
Source: "dokploy-postgres-database",
|
||||
Target: "/var/lib/postgresql/data",
|
||||
},
|
||||
],
|
||||
},
|
||||
Networks: [{ Target: "dokploy-network" }],
|
||||
RestartPolicy: {
|
||||
Condition: "on-failure",
|
||||
},
|
||||
},
|
||||
Mode: {
|
||||
Replicated: {
|
||||
Replicas: 1,
|
||||
},
|
||||
},
|
||||
EndpointSpec: {
|
||||
Ports: [
|
||||
{
|
||||
TargetPort: 5432,
|
||||
...(process.env.NODE_ENV === "development"
|
||||
? { PublishedPort: 5432 }
|
||||
: {}),
|
||||
Protocol: "tcp",
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
try {
|
||||
await pullImage(imageName);
|
||||
|
||||
const service = docker.getService(containerName);
|
||||
const inspect = await service.inspect();
|
||||
await service.update({
|
||||
version: Number.parseInt(inspect.Version.Index),
|
||||
...settings,
|
||||
});
|
||||
|
||||
console.log("Postgres Started ✅");
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
await docker.createService(settings);
|
||||
console.log("Postgres Not Found: Starting ✅");
|
||||
}
|
||||
};
|
||||
56
server/setup/redis-setup.ts
Normal file
56
server/setup/redis-setup.ts
Normal file
@@ -0,0 +1,56 @@
|
||||
import type { CreateServiceOptions } from "dockerode";
|
||||
import { docker } from "../constants";
|
||||
import { pullImage } from "../utils/docker/utils";
|
||||
|
||||
export const initializeRedis = async () => {
|
||||
const imageName = "redis:7";
|
||||
const containerName = "dokploy-redis";
|
||||
|
||||
const settings: CreateServiceOptions = {
|
||||
Name: containerName,
|
||||
TaskTemplate: {
|
||||
ContainerSpec: {
|
||||
Image: imageName,
|
||||
Mounts: [
|
||||
{
|
||||
Type: "volume",
|
||||
Source: "redis-data-volume",
|
||||
Target: "/data",
|
||||
},
|
||||
],
|
||||
},
|
||||
Networks: [{ Target: "dokploy-network" }],
|
||||
RestartPolicy: {
|
||||
Condition: "on-failure",
|
||||
},
|
||||
},
|
||||
Mode: {
|
||||
Replicated: {
|
||||
Replicas: 1,
|
||||
},
|
||||
},
|
||||
EndpointSpec: {
|
||||
Ports: [
|
||||
{
|
||||
TargetPort: 6379,
|
||||
PublishedPort: 6379,
|
||||
Protocol: "tcp",
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
try {
|
||||
await pullImage(imageName);
|
||||
|
||||
const service = docker.getService(containerName);
|
||||
const inspect = await service.inspect();
|
||||
await service.update({
|
||||
version: Number.parseInt(inspect.Version.Index),
|
||||
...settings,
|
||||
});
|
||||
console.log("Redis Started ✅");
|
||||
} catch (error) {
|
||||
await docker.createService(settings);
|
||||
console.log("Redis Not Found: Starting ✅");
|
||||
}
|
||||
};
|
||||
48
server/setup/setup.ts
Normal file
48
server/setup/setup.ts
Normal file
@@ -0,0 +1,48 @@
|
||||
import { docker } from "../constants";
|
||||
|
||||
export const initializeSwarm = async () => {
|
||||
const swarmInitialized = await dockerSwarmInitialized();
|
||||
if (swarmInitialized) {
|
||||
console.log("Swarm is already initilized");
|
||||
} else {
|
||||
await docker.swarmInit({
|
||||
AdvertiseAddr: "127.0.0.1",
|
||||
ListenAddr: "0.0.0.0",
|
||||
});
|
||||
console.log("Swarm was initilized");
|
||||
}
|
||||
};
|
||||
|
||||
export const dockerSwarmInitialized = async () => {
|
||||
try {
|
||||
await docker.swarmInspect();
|
||||
|
||||
return true;
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
export const initializeNetwork = async () => {
|
||||
const networkInitialized = await dockerNetworkInitialized();
|
||||
if (networkInitialized) {
|
||||
console.log("Network is already initilized");
|
||||
} else {
|
||||
docker.createNetwork({
|
||||
Attachable: true,
|
||||
Name: "dokploy-network",
|
||||
Driver: "overlay",
|
||||
});
|
||||
console.log("Network was initilized");
|
||||
}
|
||||
};
|
||||
|
||||
export const dockerNetworkInitialized = async () => {
|
||||
try {
|
||||
await docker.getNetwork("dokploy-network").inspect();
|
||||
return true;
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
210
server/setup/traefik-setup.ts
Normal file
210
server/setup/traefik-setup.ts
Normal file
@@ -0,0 +1,210 @@
|
||||
import path from "node:path";
|
||||
import { MAIN_TRAEFIK_PATH, DYNAMIC_TRAEFIK_PATH, docker } from "../constants";
|
||||
import { pullImage } from "../utils/docker/utils";
|
||||
import { existsSync, mkdirSync, writeFileSync } from "node:fs";
|
||||
import { dump } from "js-yaml";
|
||||
import type { MainTraefikConfig } from "../utils/traefik/types";
|
||||
import type { FileConfig } from "../utils/traefik/file-types";
|
||||
import type { CreateServiceOptions } from "dockerode";
|
||||
|
||||
export const initializeTraefik = async () => {
|
||||
const imageName = "traefik:v2.5";
|
||||
const containerName = "dokploy-traefik";
|
||||
const settings: CreateServiceOptions = {
|
||||
Name: containerName,
|
||||
TaskTemplate: {
|
||||
ContainerSpec: {
|
||||
Image: imageName,
|
||||
Mounts: [
|
||||
{
|
||||
Type: "bind",
|
||||
Source: `${MAIN_TRAEFIK_PATH}/traefik.yml`,
|
||||
Target: "/etc/traefik/traefik.yml",
|
||||
},
|
||||
{
|
||||
Type: "bind",
|
||||
Source: DYNAMIC_TRAEFIK_PATH,
|
||||
Target: "/etc/dokploy/traefik/dynamic",
|
||||
},
|
||||
{
|
||||
Type: "bind",
|
||||
Source: "/var/run/docker.sock",
|
||||
Target: "/var/run/docker.sock",
|
||||
},
|
||||
],
|
||||
},
|
||||
Networks: [{ Target: "dokploy-network" }],
|
||||
RestartPolicy: {
|
||||
Condition: "on-failure",
|
||||
},
|
||||
},
|
||||
Mode: {
|
||||
Replicated: {
|
||||
Replicas: 1,
|
||||
},
|
||||
},
|
||||
EndpointSpec: {
|
||||
Ports: [
|
||||
{
|
||||
TargetPort: 443,
|
||||
PublishedPort: 443,
|
||||
PublishMode: "host",
|
||||
},
|
||||
{
|
||||
TargetPort: 80,
|
||||
PublishedPort: 80,
|
||||
PublishMode: "host",
|
||||
},
|
||||
{
|
||||
TargetPort: 8080,
|
||||
PublishedPort: 8080,
|
||||
PublishMode: "host",
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
try {
|
||||
await pullImage(imageName);
|
||||
|
||||
const service = docker.getService(containerName);
|
||||
const inspect = await service.inspect();
|
||||
await service.update({
|
||||
version: Number.parseInt(inspect.Version.Index),
|
||||
...settings,
|
||||
});
|
||||
|
||||
console.log("Traefik Started ✅");
|
||||
} catch (error) {
|
||||
await docker.createService(settings);
|
||||
console.log("Traefik Not Found: Starting ✅");
|
||||
}
|
||||
};
|
||||
|
||||
export const createDefaultServerTraefikConfig = () => {
|
||||
const configFilePath = path.join(DYNAMIC_TRAEFIK_PATH, "dokploy.yml");
|
||||
if (existsSync(configFilePath)) {
|
||||
console.log("Default traefik config already exists");
|
||||
return;
|
||||
}
|
||||
|
||||
const appName = "dokploy";
|
||||
const serviceURLDefault = `http://${appName}:${process.env.PORT || 3000}`;
|
||||
const config: FileConfig = {
|
||||
http: {
|
||||
routers: {
|
||||
[`${appName}-router-app`]: {
|
||||
rule: `Host(\`${appName}.docker.localhost\`) && PathPrefix(\`/\`)`,
|
||||
service: `${appName}-service-app`,
|
||||
entryPoints: ["web", "websecure"],
|
||||
tls: {},
|
||||
},
|
||||
},
|
||||
services: {
|
||||
[`${appName}-service-app`]: {
|
||||
loadBalancer: {
|
||||
servers: [{ url: serviceURLDefault }],
|
||||
passHostHeader: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const yamlStr = dump(config);
|
||||
mkdirSync(DYNAMIC_TRAEFIK_PATH, { recursive: true });
|
||||
writeFileSync(
|
||||
path.join(DYNAMIC_TRAEFIK_PATH, `${appName}.yml`),
|
||||
yamlStr,
|
||||
"utf8",
|
||||
);
|
||||
};
|
||||
|
||||
export const createDefaultTraefikConfig = () => {
|
||||
const mainConfig = path.join(MAIN_TRAEFIK_PATH, "traefik.yml");
|
||||
if (existsSync(mainConfig)) {
|
||||
console.log("Main config already exists");
|
||||
return;
|
||||
}
|
||||
const configObject: MainTraefikConfig = {
|
||||
providers: {
|
||||
...(process.env.NODE_ENV === "development" && {
|
||||
docker: {
|
||||
defaultRule: "Host(`{{ trimPrefix `/` .Name }}.docker.localhost`)",
|
||||
},
|
||||
}),
|
||||
file: {
|
||||
directory: DYNAMIC_TRAEFIK_PATH,
|
||||
watch: true,
|
||||
},
|
||||
},
|
||||
entryPoints: {
|
||||
web: {
|
||||
address: ":80",
|
||||
...(process.env.NODE_ENV === "production" && {
|
||||
http: {
|
||||
redirections: {
|
||||
entryPoint: {
|
||||
to: "websecure",
|
||||
scheme: "https",
|
||||
permanent: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
},
|
||||
websecure: {
|
||||
address: ":443",
|
||||
...(process.env.NODE_ENV === "production" && {
|
||||
http: {
|
||||
tls: {
|
||||
certResolver: "letsencrypt",
|
||||
},
|
||||
},
|
||||
}),
|
||||
},
|
||||
},
|
||||
api: {
|
||||
insecure: true,
|
||||
},
|
||||
...(process.env.NODE_ENV === "production" && {
|
||||
certificatesResolvers: {
|
||||
letsencrypt: {
|
||||
acme: {
|
||||
email: "test@localhost.com",
|
||||
storage: "/etc/dokploy/traefik/dynamic/acme.json",
|
||||
httpChallenge: {
|
||||
entryPoint: "web",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
};
|
||||
|
||||
const yamlStr = dump(configObject);
|
||||
mkdirSync(MAIN_TRAEFIK_PATH, { recursive: true });
|
||||
writeFileSync(mainConfig, yamlStr, "utf8");
|
||||
};
|
||||
|
||||
export const createDefaultMiddlewares = () => {
|
||||
const middlewaresPath = path.join(DYNAMIC_TRAEFIK_PATH, "middlewares.yml");
|
||||
if (existsSync(middlewaresPath)) {
|
||||
console.log("Default middlewares already exists");
|
||||
return;
|
||||
}
|
||||
const defaultMiddlewares = {
|
||||
http: {
|
||||
middlewares: {
|
||||
"redirect-to-https": {
|
||||
redirectScheme: {
|
||||
scheme: "https",
|
||||
permanent: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
const yamlStr = dump(defaultMiddlewares);
|
||||
mkdirSync(DYNAMIC_TRAEFIK_PATH, { recursive: true });
|
||||
writeFileSync(middlewaresPath, yamlStr, "utf8");
|
||||
};
|
||||
45
server/types/with.ts
Normal file
45
server/types/with.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
import type * as schema from "@/server/db/schema";
|
||||
import {
|
||||
type BuildQueryResult,
|
||||
type DBQueryConfig,
|
||||
type ExtractTablesWithRelations,
|
||||
} from "drizzle-orm";
|
||||
import { z } from "zod";
|
||||
/*
|
||||
* This is for testing purposes in the case we need a nested relational types
|
||||
*
|
||||
*/
|
||||
|
||||
type Schema = typeof schema;
|
||||
type TSchema = ExtractTablesWithRelations<Schema>;
|
||||
|
||||
export type IncludeRelation<TableName extends keyof TSchema> = DBQueryConfig<
|
||||
"one" | "many",
|
||||
boolean,
|
||||
TSchema,
|
||||
TSchema[TableName]
|
||||
>["with"];
|
||||
|
||||
export type InferResultType<
|
||||
TableName extends keyof TSchema,
|
||||
With extends IncludeRelation<TableName> | undefined = undefined,
|
||||
> = BuildQueryResult<
|
||||
TSchema,
|
||||
TSchema[TableName],
|
||||
{
|
||||
with: With;
|
||||
}
|
||||
>;
|
||||
|
||||
type AnyObj = Record<PropertyKey, unknown>;
|
||||
|
||||
type ZodObj<T extends AnyObj> = {
|
||||
[key in keyof T]: z.ZodType<T[key]>;
|
||||
};
|
||||
const zObject = <T extends AnyObj>(arg: ZodObj<T>) => z.object(arg);
|
||||
|
||||
// const goodDogScheme = zObject<UserWithPosts>({
|
||||
// // prueba: schema.selectDatabaseSchema,
|
||||
// // domain: z.string(),
|
||||
// // domainId: z.string(),
|
||||
// });
|
||||
140
server/utils/backups/index.ts
Normal file
140
server/utils/backups/index.ts
Normal file
@@ -0,0 +1,140 @@
|
||||
import { scheduleJob } from "node-schedule";
|
||||
import { db } from "../../db/index";
|
||||
import { runMariadbBackup } from "./mariadb";
|
||||
import { runMongoBackup } from "./mongo";
|
||||
import { runMySqlBackup } from "./mysql";
|
||||
import { runPostgresBackup } from "./postgres";
|
||||
import {
|
||||
cleanUpDockerBuilder,
|
||||
cleanUpSystemPrune,
|
||||
cleanUpUnusedImages,
|
||||
} from "../docker/utils";
|
||||
import { findAdmin } from "@/server/api/services/admin";
|
||||
|
||||
export const initCronJobs = async () => {
|
||||
console.log("Setting up cron jobs....");
|
||||
|
||||
const admin = await findAdmin();
|
||||
|
||||
if (admin?.enableDockerCleanup) {
|
||||
scheduleJob("docker-cleanup", "0 0 * * *", async () => {
|
||||
console.log(
|
||||
`Docker Cleanup ${new Date().toLocaleString()}] Running docker cleanup`,
|
||||
);
|
||||
await cleanUpUnusedImages();
|
||||
await cleanUpDockerBuilder();
|
||||
await cleanUpSystemPrune();
|
||||
});
|
||||
}
|
||||
|
||||
const pgs = await db.query.postgres.findMany({
|
||||
with: {
|
||||
backups: {
|
||||
with: {
|
||||
destination: true,
|
||||
postgres: true,
|
||||
mariadb: true,
|
||||
mysql: true,
|
||||
mongo: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
for (const pg of pgs) {
|
||||
for (const backup of pg.backups) {
|
||||
const { schedule, backupId, enabled } = backup;
|
||||
if (enabled) {
|
||||
scheduleJob(backupId, schedule, async () => {
|
||||
console.log(
|
||||
`PG-SERVER[${new Date().toLocaleString()}] Running Backup ${backupId}`,
|
||||
);
|
||||
runPostgresBackup(pg, backup);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const mariadbs = await db.query.mariadb.findMany({
|
||||
with: {
|
||||
backups: {
|
||||
with: {
|
||||
destination: true,
|
||||
postgres: true,
|
||||
mariadb: true,
|
||||
mysql: true,
|
||||
mongo: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
for (const maria of mariadbs) {
|
||||
for (const backup of maria.backups) {
|
||||
const { schedule, backupId, enabled } = backup;
|
||||
if (enabled) {
|
||||
scheduleJob(backupId, schedule, async () => {
|
||||
console.log(
|
||||
`MARIADB-SERVER[${new Date().toLocaleString()}] Running Backup ${backupId}`,
|
||||
);
|
||||
await runMariadbBackup(maria, backup);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const mongodbs = await db.query.mongo.findMany({
|
||||
with: {
|
||||
backups: {
|
||||
with: {
|
||||
destination: true,
|
||||
postgres: true,
|
||||
mariadb: true,
|
||||
mysql: true,
|
||||
mongo: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
for (const mongo of mongodbs) {
|
||||
for (const backup of mongo.backups) {
|
||||
const { schedule, backupId, enabled } = backup;
|
||||
if (enabled) {
|
||||
scheduleJob(backupId, schedule, async () => {
|
||||
console.log(
|
||||
`MONGO-SERVER[${new Date().toLocaleString()}] Running Backup ${backupId}`,
|
||||
);
|
||||
await runMongoBackup(mongo, backup);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const mysqls = await db.query.mysql.findMany({
|
||||
with: {
|
||||
backups: {
|
||||
with: {
|
||||
destination: true,
|
||||
postgres: true,
|
||||
mariadb: true,
|
||||
mysql: true,
|
||||
mongo: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
for (const mysql of mysqls) {
|
||||
for (const backup of mysql.backups) {
|
||||
const { schedule, backupId, enabled } = backup;
|
||||
if (enabled) {
|
||||
scheduleJob(backupId, schedule, async () => {
|
||||
console.log(
|
||||
`MYSQL-SERVER[${new Date().toLocaleString()}] Running Backup ${backupId}`,
|
||||
);
|
||||
await runMySqlBackup(mysql, backup);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
40
server/utils/backups/mariadb.ts
Normal file
40
server/utils/backups/mariadb.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
import { unlink } from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import { execAsync } from "../process/execAsync";
|
||||
import { uploadToS3 } from "./utils";
|
||||
import type { BackupSchedule } from "@/server/api/services/backup";
|
||||
import type { Mariadb } from "@/server/api/services/mariadb";
|
||||
import { getServiceContainer } from "../docker/utils";
|
||||
|
||||
export const runMariadbBackup = async (
|
||||
mariadb: Mariadb,
|
||||
backup: BackupSchedule,
|
||||
) => {
|
||||
const { appName, databasePassword, databaseUser } = mariadb;
|
||||
const { prefix, database } = backup;
|
||||
const destination = backup.destination;
|
||||
const backupFileName = `${new Date().toISOString()}.sql.gz`;
|
||||
const bucketDestination = path.join(prefix, backupFileName);
|
||||
const containerPath = `/backup/${backupFileName}`;
|
||||
const hostPath = `./${backupFileName}`;
|
||||
|
||||
try {
|
||||
const { Id: containerId } = await getServiceContainer(appName);
|
||||
await execAsync(
|
||||
`docker exec ${containerId} sh -c "rm -rf /backup && mkdir -p /backup"`,
|
||||
);
|
||||
|
||||
await execAsync(
|
||||
`docker exec ${containerId} sh -c "mariadb-dump --user='${databaseUser}' --password='${databasePassword}' --databases ${database} | gzip > ${containerPath}"`,
|
||||
);
|
||||
await execAsync(
|
||||
`docker cp ${containerId}:/backup/${backupFileName} ${hostPath}`,
|
||||
);
|
||||
await uploadToS3(destination, bucketDestination, hostPath);
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
throw error;
|
||||
} finally {
|
||||
await unlink(hostPath);
|
||||
}
|
||||
};
|
||||
37
server/utils/backups/mongo.ts
Normal file
37
server/utils/backups/mongo.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
import { unlink } from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import { execAsync } from "../process/execAsync";
|
||||
import { uploadToS3 } from "./utils";
|
||||
import type { BackupSchedule } from "@/server/api/services/backup";
|
||||
import type { Mongo } from "@/server/api/services/mongo";
|
||||
import { getServiceContainer } from "../docker/utils";
|
||||
|
||||
// mongodb://mongo:Bqh7AQl-PRbnBu@localhost:27017/?tls=false&directConnection=true
|
||||
export const runMongoBackup = async (mongo: Mongo, backup: BackupSchedule) => {
|
||||
const { appName, databasePassword } = mongo;
|
||||
const { prefix, database } = backup;
|
||||
const destination = backup.destination;
|
||||
const backupFileName = `${new Date().toISOString()}.dump.gz`;
|
||||
const bucketDestination = path.join(prefix, backupFileName);
|
||||
const containerPath = `/backup/${backupFileName}`;
|
||||
const hostPath = `./${backupFileName}`;
|
||||
|
||||
try {
|
||||
const { Id: containerId } = await getServiceContainer(appName);
|
||||
await execAsync(
|
||||
`docker exec ${containerId} sh -c "rm -rf /backup && mkdir -p /backup"`,
|
||||
);
|
||||
|
||||
await execAsync(
|
||||
`docker exec ${containerId} sh -c "mongodump -d '${database}' -u 'mongo' -p '${databasePassword}' --authenticationDatabase=admin --archive=${containerPath} --gzip"`,
|
||||
);
|
||||
await execAsync(`docker cp ${containerId}:${containerPath} ${hostPath}`);
|
||||
await uploadToS3(destination, bucketDestination, hostPath);
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
throw error;
|
||||
} finally {
|
||||
await unlink(hostPath);
|
||||
}
|
||||
};
|
||||
// mongorestore -d monguito -u mongo -p Bqh7AQl-PRbnBu --authenticationDatabase admin --gzip --archive=2024-04-13T05:03:58.937Z.dump.gz
|
||||
38
server/utils/backups/mysql.ts
Normal file
38
server/utils/backups/mysql.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
import path from "node:path";
|
||||
import { execAsync } from "../process/execAsync";
|
||||
import { unlink } from "node:fs/promises";
|
||||
import { uploadToS3 } from "./utils";
|
||||
import type { BackupSchedule } from "@/server/api/services/backup";
|
||||
import type { MySql } from "@/server/api/services/mysql";
|
||||
import { getServiceContainer } from "../docker/utils";
|
||||
|
||||
export const runMySqlBackup = async (mysql: MySql, backup: BackupSchedule) => {
|
||||
const { appName, databaseRootPassword } = mysql;
|
||||
const { prefix, database } = backup;
|
||||
const destination = backup.destination;
|
||||
const backupFileName = `${new Date().toISOString()}.sql.gz`;
|
||||
const bucketDestination = path.join(prefix, backupFileName);
|
||||
const containerPath = `/backup/${backupFileName}`;
|
||||
const hostPath = `./${backupFileName}`;
|
||||
|
||||
try {
|
||||
const { Id: containerId } = await getServiceContainer(appName);
|
||||
|
||||
await execAsync(
|
||||
`docker exec ${containerId} sh -c "rm -rf /backup && mkdir -p /backup"`,
|
||||
);
|
||||
|
||||
await execAsync(
|
||||
`docker exec ${containerId} sh -c "mysqldump --default-character-set=utf8mb4 -u 'root' --password='${databaseRootPassword}' --single-transaction --no-tablespaces --quick '${database}' | gzip > ${containerPath}"`,
|
||||
);
|
||||
await execAsync(
|
||||
`docker cp ${containerId}:/backup/${backupFileName} ${hostPath}`,
|
||||
);
|
||||
await uploadToS3(destination, bucketDestination, hostPath);
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
throw error;
|
||||
} finally {
|
||||
await unlink(hostPath);
|
||||
}
|
||||
};
|
||||
41
server/utils/backups/postgres.ts
Normal file
41
server/utils/backups/postgres.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
import path from "node:path";
|
||||
import { execAsync } from "../process/execAsync";
|
||||
import { unlink } from "node:fs/promises";
|
||||
import { uploadToS3 } from "./utils";
|
||||
import type { BackupSchedule } from "@/server/api/services/backup";
|
||||
import type { Postgres } from "@/server/api/services/postgres";
|
||||
import { getServiceContainer } from "../docker/utils";
|
||||
|
||||
export const runPostgresBackup = async (
|
||||
postgres: Postgres,
|
||||
backup: BackupSchedule,
|
||||
) => {
|
||||
const { appName, databaseUser } = postgres;
|
||||
const { prefix, database } = backup;
|
||||
const destination = backup.destination;
|
||||
const backupFileName = `${new Date().toISOString()}.sql.gz`;
|
||||
const bucketDestination = path.join(prefix, backupFileName);
|
||||
const containerPath = `/backup/${backupFileName}`;
|
||||
const hostPath = `./${backupFileName}`;
|
||||
try {
|
||||
const { Id: containerId } = await getServiceContainer(appName);
|
||||
|
||||
await execAsync(
|
||||
`docker exec ${containerId} /bin/bash -c "rm -rf /backup && mkdir -p /backup"`,
|
||||
);
|
||||
await execAsync(
|
||||
`docker exec ${containerId} sh -c "pg_dump -Fc --no-acl --no-owner -h localhost -U ${databaseUser} --no-password '${database}' | gzip > ${containerPath}"`,
|
||||
);
|
||||
await execAsync(`docker cp ${containerId}:${containerPath} ${hostPath}`);
|
||||
|
||||
await uploadToS3(destination, bucketDestination, hostPath);
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
throw error;
|
||||
} finally {
|
||||
await unlink(hostPath);
|
||||
}
|
||||
};
|
||||
|
||||
// Restore
|
||||
// /Applications/pgAdmin 4.app/Contents/SharedSupport/pg_restore --host "localhost" --port "5432" --username "mauricio" --no-password --dbname "postgres" --verbose "/Users/mauricio/Downloads/_databases_2024-04-12T07_02_05.234Z.sql"
|
||||
59
server/utils/backups/utils.ts
Normal file
59
server/utils/backups/utils.ts
Normal file
@@ -0,0 +1,59 @@
|
||||
import type { BackupSchedule } from "@/server/api/services/backup";
|
||||
import type { Destination } from "@/server/api/services/destination";
|
||||
import { S3Client, PutObjectCommand } from "@aws-sdk/client-s3";
|
||||
import { scheduleJob, scheduledJobs } from "node-schedule";
|
||||
import { readFile } from "node:fs/promises";
|
||||
import { runPostgresBackup } from "./postgres";
|
||||
import { runMySqlBackup } from "./mysql";
|
||||
import { runMongoBackup } from "./mongo";
|
||||
import { runMariadbBackup } from "./mariadb";
|
||||
|
||||
export const uploadToS3 = async (
|
||||
destination: Destination,
|
||||
destinationBucketPath: string,
|
||||
filePath: string,
|
||||
) => {
|
||||
const { accessKey, secretAccessKey, bucket, region, endpoint } = destination;
|
||||
|
||||
const s3Client = new S3Client({
|
||||
region: region,
|
||||
...(endpoint && {
|
||||
endpoint: endpoint,
|
||||
}),
|
||||
credentials: {
|
||||
accessKeyId: accessKey,
|
||||
secretAccessKey: secretAccessKey,
|
||||
},
|
||||
forcePathStyle: true,
|
||||
});
|
||||
|
||||
const fileContent = await readFile(filePath);
|
||||
const command = new PutObjectCommand({
|
||||
Bucket: bucket,
|
||||
Key: destinationBucketPath,
|
||||
Body: fileContent,
|
||||
});
|
||||
|
||||
await s3Client.send(command);
|
||||
};
|
||||
|
||||
export const scheduleBackup = (backup: BackupSchedule) => {
|
||||
const { schedule, backupId, databaseType, postgres, mysql, mongo, mariadb } =
|
||||
backup;
|
||||
scheduleJob(backupId, schedule, async () => {
|
||||
if (databaseType === "postgres" && postgres) {
|
||||
await runPostgresBackup(postgres, backup);
|
||||
} else if (databaseType === "mysql" && mysql) {
|
||||
await runMySqlBackup(mysql, backup);
|
||||
} else if (databaseType === "mongo" && mongo) {
|
||||
await runMongoBackup(mongo, backup);
|
||||
} else if (databaseType === "mariadb" && mariadb) {
|
||||
await runMariadbBackup(mariadb, backup);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export const removeScheduleBackup = (backupId: string) => {
|
||||
const currentJob = scheduledJobs[backupId];
|
||||
currentJob?.cancel();
|
||||
};
|
||||
40
server/utils/builders/docker-file.ts
Normal file
40
server/utils/builders/docker-file.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
import { docker } from "@/server/constants";
|
||||
import type { WriteStream } from "node:fs";
|
||||
import * as tar from "tar-fs";
|
||||
import type { ApplicationNested } from ".";
|
||||
import { getBuildAppDirectory } from "../filesystem/directory";
|
||||
|
||||
export const buildCustomDocker = async (
|
||||
application: ApplicationNested,
|
||||
writeStream: WriteStream,
|
||||
) => {
|
||||
const { appName } = application;
|
||||
const dockerFilePath = getBuildAppDirectory(application);
|
||||
try {
|
||||
const image = `${appName}`;
|
||||
const contextPath =
|
||||
dockerFilePath.substring(0, dockerFilePath.lastIndexOf("/") + 1) || ".";
|
||||
const tarStream = tar.pack(contextPath);
|
||||
|
||||
const stream = await docker.buildImage(tarStream, {
|
||||
t: image,
|
||||
dockerfile: dockerFilePath.substring(dockerFilePath.lastIndexOf("/") + 1),
|
||||
// TODO: maybe use or not forcerm
|
||||
// forcerm: true,
|
||||
});
|
||||
|
||||
await new Promise((resolve, reject) => {
|
||||
docker.modem.followProgress(
|
||||
stream,
|
||||
(err, res) => (err ? reject(err) : resolve(res)),
|
||||
(event) => {
|
||||
if (event.stream) {
|
||||
writeStream.write(event.stream);
|
||||
}
|
||||
},
|
||||
);
|
||||
});
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
40
server/utils/builders/heroku.ts
Normal file
40
server/utils/builders/heroku.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
import type { ApplicationNested } from ".";
|
||||
import { prepareEnvironmentVariables } from "../docker/utils";
|
||||
import { getBuildAppDirectory } from "../filesystem/directory";
|
||||
import { spawnAsync } from "../process/spawnAsync";
|
||||
import type { WriteStream } from "node:fs";
|
||||
|
||||
// TODO: integrate in the vps sudo chown -R $(whoami) ~/.docker
|
||||
export const buildHeroku = async (
|
||||
application: ApplicationNested,
|
||||
writeStream: WriteStream,
|
||||
) => {
|
||||
const { env, appName } = application;
|
||||
const buildAppDirectory = getBuildAppDirectory(application);
|
||||
const envVariables = prepareEnvironmentVariables(env);
|
||||
try {
|
||||
const args = [
|
||||
"build",
|
||||
appName,
|
||||
"--path",
|
||||
buildAppDirectory,
|
||||
"--builder",
|
||||
"heroku/builder:22",
|
||||
];
|
||||
|
||||
for (const env in envVariables) {
|
||||
args.push("--env", env);
|
||||
}
|
||||
|
||||
await spawnAsync("pack", args, (data) => {
|
||||
if (writeStream.writable) {
|
||||
writeStream.write(data);
|
||||
}
|
||||
// Stream the data
|
||||
console.log(data);
|
||||
});
|
||||
return true;
|
||||
} catch (e) {
|
||||
throw e;
|
||||
}
|
||||
};
|
||||
138
server/utils/builders/index.ts
Normal file
138
server/utils/builders/index.ts
Normal file
@@ -0,0 +1,138 @@
|
||||
import { createWriteStream } from "node:fs";
|
||||
import { docker } from "@/server/constants";
|
||||
import type { InferResultType } from "@/server/types/with";
|
||||
import type { CreateServiceOptions } from "dockerode";
|
||||
import {
|
||||
calculateResources,
|
||||
generateBindMounts,
|
||||
generateFileMounts,
|
||||
generateVolumeMounts,
|
||||
prepareEnvironmentVariables,
|
||||
} from "../docker/utils";
|
||||
import { buildCustomDocker } from "./docker-file";
|
||||
import { buildHeroku } from "./heroku";
|
||||
import { buildNixpacks } from "./nixpacks";
|
||||
import { buildPaketo } from "./paketo";
|
||||
|
||||
// NIXPACKS codeDirectory = where is the path of the code directory
|
||||
// HEROKU codeDirectory = where is the path of the code directory
|
||||
// PAKETO codeDirectory = where is the path of the code directory
|
||||
// DOKERFILE codeDirectory = where is the exact path of the (Dockerfile)
|
||||
export type ApplicationNested = InferResultType<
|
||||
"applications",
|
||||
{ mounts: true; security: true; redirects: true; ports: true }
|
||||
>;
|
||||
export const buildApplication = async (
|
||||
application: ApplicationNested,
|
||||
logPath: string,
|
||||
) => {
|
||||
const writeStream = createWriteStream(logPath, { flags: "a" });
|
||||
const { buildType, sourceType } = application;
|
||||
try {
|
||||
writeStream.write(
|
||||
`\nBuild ${buildType}: ✅\nSource Type: ${sourceType}: ✅\n`,
|
||||
);
|
||||
console.log(`Build ${buildType}: ✅`);
|
||||
if (buildType === "nixpacks") {
|
||||
await buildNixpacks(application, writeStream);
|
||||
} else if (buildType === "heroku_buildpacks") {
|
||||
await buildHeroku(application, writeStream);
|
||||
} else if (buildType === "paketo_buildpacks") {
|
||||
await buildPaketo(application, writeStream);
|
||||
} else if (buildType === "dockerfile") {
|
||||
await buildCustomDocker(application, writeStream);
|
||||
}
|
||||
await mechanizeDockerContainer(application);
|
||||
writeStream.write("Docker Deployed: ✅");
|
||||
} catch (error) {
|
||||
writeStream.write(`ERROR: ${error}: ❌`);
|
||||
throw error;
|
||||
} finally {
|
||||
writeStream.end();
|
||||
}
|
||||
};
|
||||
|
||||
export const mechanizeDockerContainer = async (
|
||||
application: ApplicationNested,
|
||||
) => {
|
||||
const {
|
||||
appName,
|
||||
env,
|
||||
mounts,
|
||||
sourceType,
|
||||
dockerImage,
|
||||
cpuLimit,
|
||||
memoryLimit,
|
||||
memoryReservation,
|
||||
cpuReservation,
|
||||
command,
|
||||
ports,
|
||||
} = application;
|
||||
|
||||
const resources = calculateResources({
|
||||
memoryLimit,
|
||||
memoryReservation,
|
||||
cpuLimit,
|
||||
cpuReservation,
|
||||
});
|
||||
const volumesMount = generateVolumeMounts(mounts);
|
||||
const bindsMount = generateBindMounts(mounts);
|
||||
const filesMount = generateFileMounts(appName, mounts);
|
||||
const envVariables = prepareEnvironmentVariables(env);
|
||||
|
||||
const settings: CreateServiceOptions = {
|
||||
Name: appName,
|
||||
TaskTemplate: {
|
||||
ContainerSpec: {
|
||||
Image: sourceType === "docker" ? dockerImage! : `${appName}:latest`,
|
||||
Env: envVariables,
|
||||
Mounts: [...volumesMount, ...bindsMount, ...filesMount],
|
||||
...(command
|
||||
? {
|
||||
Command: ["/bin/sh"],
|
||||
Args: ["-c", command],
|
||||
}
|
||||
: {}),
|
||||
},
|
||||
Networks: [{ Target: "dokploy-network" }],
|
||||
RestartPolicy: {
|
||||
Condition: "on-failure",
|
||||
},
|
||||
Resources: {
|
||||
...resources,
|
||||
},
|
||||
},
|
||||
Mode: {
|
||||
Replicated: {
|
||||
Replicas: 1,
|
||||
},
|
||||
},
|
||||
EndpointSpec: {
|
||||
Ports: ports.map((port) => ({
|
||||
Protocol: port.protocol,
|
||||
TargetPort: port.targetPort,
|
||||
PublishedPort: port.publishedPort,
|
||||
})),
|
||||
},
|
||||
UpdateConfig: {
|
||||
Parallelism: 1,
|
||||
Order: "start-first",
|
||||
},
|
||||
};
|
||||
|
||||
try {
|
||||
const service = docker.getService(appName);
|
||||
const inspect = await service.inspect();
|
||||
await service.update({
|
||||
version: Number.parseInt(inspect.Version.Index),
|
||||
...settings,
|
||||
TaskTemplate: {
|
||||
...settings.TaskTemplate,
|
||||
ForceUpdate: inspect.Spec.TaskTemplate.ForceUpdate + 1,
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
await docker.createService(settings);
|
||||
}
|
||||
// await cleanUpUnusedImages();
|
||||
};
|
||||
30
server/utils/builders/nixpacks.ts
Normal file
30
server/utils/builders/nixpacks.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
import type { ApplicationNested } from ".";
|
||||
import { prepareEnvironmentVariables } from "../docker/utils";
|
||||
import { getBuildAppDirectory } from "../filesystem/directory";
|
||||
import { spawnAsync } from "../process/spawnAsync";
|
||||
import type { WriteStream } from "node:fs";
|
||||
|
||||
// TODO: integrate in the vps sudo chown -R $(whoami) ~/.docker
|
||||
export const buildNixpacks = async (
|
||||
application: ApplicationNested,
|
||||
writeStream: WriteStream,
|
||||
) => {
|
||||
const { env, appName } = application;
|
||||
const buildAppDirectory = getBuildAppDirectory(application);
|
||||
const envVariables = prepareEnvironmentVariables(env);
|
||||
try {
|
||||
const args = ["build", buildAppDirectory, "--name", appName];
|
||||
|
||||
for (const env in envVariables) {
|
||||
args.push("--env", env);
|
||||
}
|
||||
await spawnAsync("nixpacks", args, (data) => {
|
||||
if (writeStream.writable) {
|
||||
writeStream.write(data);
|
||||
}
|
||||
});
|
||||
return true;
|
||||
} catch (e) {
|
||||
throw e;
|
||||
}
|
||||
};
|
||||
38
server/utils/builders/paketo.ts
Normal file
38
server/utils/builders/paketo.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
import type { WriteStream } from "node:fs";
|
||||
import { spawnAsync } from "../process/spawnAsync";
|
||||
import type { ApplicationNested } from ".";
|
||||
import { getBuildAppDirectory } from "../filesystem/directory";
|
||||
import { prepareEnvironmentVariables } from "../docker/utils";
|
||||
|
||||
// TODO: integrate in the vps sudo chown -R $(whoami) ~/.docker
|
||||
export const buildPaketo = async (
|
||||
application: ApplicationNested,
|
||||
writeStream: WriteStream,
|
||||
) => {
|
||||
const { env, appName } = application;
|
||||
const buildAppDirectory = getBuildAppDirectory(application);
|
||||
const envVariables = prepareEnvironmentVariables(env);
|
||||
try {
|
||||
const args = [
|
||||
"build",
|
||||
appName,
|
||||
"--path",
|
||||
buildAppDirectory,
|
||||
"--builder",
|
||||
"paketobuildpacks/builder-jammy-full",
|
||||
];
|
||||
|
||||
for (const env in envVariables) {
|
||||
args.push("--env", env);
|
||||
}
|
||||
|
||||
await spawnAsync("pack", args, (data) => {
|
||||
if (writeStream.writable) {
|
||||
writeStream.write(data);
|
||||
}
|
||||
});
|
||||
return true;
|
||||
} catch (e) {
|
||||
throw e;
|
||||
}
|
||||
};
|
||||
96
server/utils/databases/mariadb.ts
Normal file
96
server/utils/databases/mariadb.ts
Normal file
@@ -0,0 +1,96 @@
|
||||
import type { Mariadb } from "@/server/api/services/mariadb";
|
||||
import { docker } from "@/server/constants";
|
||||
import type { CreateServiceOptions } from "dockerode";
|
||||
import {
|
||||
calculateResources,
|
||||
generateBindMounts,
|
||||
generateFileMounts,
|
||||
generateVolumeMounts,
|
||||
prepareEnvironmentVariables,
|
||||
} from "../docker/utils";
|
||||
import type { Mount } from "@/server/api/services/mount";
|
||||
|
||||
type MariadbWithMounts = Mariadb & {
|
||||
mounts: Mount[];
|
||||
};
|
||||
export const buildMariadb = async (mariadb: MariadbWithMounts) => {
|
||||
const {
|
||||
appName,
|
||||
env,
|
||||
externalPort,
|
||||
dockerImage,
|
||||
memoryLimit,
|
||||
memoryReservation,
|
||||
databaseName,
|
||||
databaseUser,
|
||||
databasePassword,
|
||||
databaseRootPassword,
|
||||
cpuLimit,
|
||||
cpuReservation,
|
||||
command,
|
||||
mounts,
|
||||
} = mariadb;
|
||||
|
||||
const defaultMariadbEnv = `MARIADB_DATABASE=${databaseName}\nMARIADB_USER=${databaseUser}\nMARIADB_PASSWORD=${databasePassword}\nMARIADB_ROOT_PASSWORD=${databaseRootPassword}${
|
||||
env ? `\n${env}` : ""
|
||||
}`;
|
||||
const resources = calculateResources({
|
||||
memoryLimit,
|
||||
memoryReservation,
|
||||
cpuLimit,
|
||||
cpuReservation,
|
||||
});
|
||||
const envVariables = prepareEnvironmentVariables(defaultMariadbEnv);
|
||||
const volumesMount = generateVolumeMounts(mounts);
|
||||
const bindsMount = generateBindMounts(mounts);
|
||||
const filesMount = generateFileMounts(appName, mounts);
|
||||
|
||||
const settings: CreateServiceOptions = {
|
||||
Name: appName,
|
||||
TaskTemplate: {
|
||||
ContainerSpec: {
|
||||
Image: dockerImage,
|
||||
Env: envVariables,
|
||||
Mounts: [...volumesMount, ...bindsMount, ...filesMount],
|
||||
...(command
|
||||
? {
|
||||
Command: ["/bin/sh"],
|
||||
Args: ["-c", command],
|
||||
}
|
||||
: {}),
|
||||
},
|
||||
Networks: [{ Target: "dokploy-network" }],
|
||||
Resources: {
|
||||
...resources,
|
||||
},
|
||||
},
|
||||
Mode: {
|
||||
Replicated: {
|
||||
Replicas: 1,
|
||||
},
|
||||
},
|
||||
EndpointSpec: {
|
||||
Mode: "dnsrr",
|
||||
Ports: externalPort
|
||||
? [
|
||||
{
|
||||
Protocol: "tcp",
|
||||
TargetPort: 3306,
|
||||
PublishedPort: externalPort,
|
||||
PublishMode: "host",
|
||||
},
|
||||
]
|
||||
: [],
|
||||
},
|
||||
};
|
||||
try {
|
||||
const service = docker.getService(appName);
|
||||
const inspect = await service.inspect();
|
||||
await service.update({
|
||||
version: Number.parseInt(inspect.Version.Index),
|
||||
...settings,
|
||||
});
|
||||
} catch (error) {
|
||||
await docker.createService(settings);
|
||||
}
|
||||
};
|
||||
96
server/utils/databases/mongo.ts
Normal file
96
server/utils/databases/mongo.ts
Normal file
@@ -0,0 +1,96 @@
|
||||
import type { Mongo } from "@/server/api/services/mongo";
|
||||
import { docker } from "@/server/constants";
|
||||
import type { CreateServiceOptions } from "dockerode";
|
||||
import {
|
||||
calculateResources,
|
||||
generateBindMounts,
|
||||
generateFileMounts,
|
||||
generateVolumeMounts,
|
||||
prepareEnvironmentVariables,
|
||||
} from "../docker/utils";
|
||||
import type { Postgres } from "@/server/api/services/postgres";
|
||||
import type { Mount } from "@/server/api/services/mount";
|
||||
|
||||
type MongoWithMounts = Mongo & {
|
||||
mounts: Mount[];
|
||||
};
|
||||
|
||||
export const buildMongo = async (mongo: MongoWithMounts) => {
|
||||
const {
|
||||
appName,
|
||||
env,
|
||||
externalPort,
|
||||
dockerImage,
|
||||
memoryLimit,
|
||||
memoryReservation,
|
||||
cpuLimit,
|
||||
cpuReservation,
|
||||
databaseUser,
|
||||
databasePassword,
|
||||
command,
|
||||
mounts,
|
||||
} = mongo;
|
||||
|
||||
const defaultMongoEnv = `MONGO_INITDB_ROOT_USERNAME=${databaseUser}\nMONGO_INITDB_ROOT_PASSWORD=${databasePassword}${
|
||||
env ? `\n${env}` : ""
|
||||
}`;
|
||||
const resources = calculateResources({
|
||||
memoryLimit,
|
||||
memoryReservation,
|
||||
cpuLimit,
|
||||
cpuReservation,
|
||||
});
|
||||
const envVariables = prepareEnvironmentVariables(defaultMongoEnv);
|
||||
const volumesMount = generateVolumeMounts(mounts);
|
||||
const bindsMount = generateBindMounts(mounts);
|
||||
const filesMount = generateFileMounts(appName, mounts);
|
||||
|
||||
const settings: CreateServiceOptions = {
|
||||
Name: appName,
|
||||
TaskTemplate: {
|
||||
ContainerSpec: {
|
||||
Image: dockerImage,
|
||||
Env: envVariables,
|
||||
Mounts: [...volumesMount, ...bindsMount, ...filesMount],
|
||||
...(command
|
||||
? {
|
||||
Command: ["/bin/sh"],
|
||||
Args: ["-c", command],
|
||||
}
|
||||
: {}),
|
||||
},
|
||||
Networks: [{ Target: "dokploy-network" }],
|
||||
Resources: {
|
||||
...resources,
|
||||
},
|
||||
},
|
||||
Mode: {
|
||||
Replicated: {
|
||||
Replicas: 1,
|
||||
},
|
||||
},
|
||||
EndpointSpec: {
|
||||
Mode: "dnsrr",
|
||||
Ports: externalPort
|
||||
? [
|
||||
{
|
||||
Protocol: "tcp",
|
||||
TargetPort: 27017,
|
||||
PublishedPort: externalPort,
|
||||
PublishMode: "host",
|
||||
},
|
||||
]
|
||||
: [],
|
||||
},
|
||||
};
|
||||
try {
|
||||
const service = docker.getService(appName);
|
||||
const inspect = await service.inspect();
|
||||
await service.update({
|
||||
version: Number.parseInt(inspect.Version.Index),
|
||||
...settings,
|
||||
});
|
||||
} catch (error) {
|
||||
await docker.createService(settings);
|
||||
}
|
||||
};
|
||||
102
server/utils/databases/mysql.ts
Normal file
102
server/utils/databases/mysql.ts
Normal file
@@ -0,0 +1,102 @@
|
||||
import type { MySql } from "@/server/api/services/mysql";
|
||||
import {
|
||||
calculateResources,
|
||||
generateBindMounts,
|
||||
generateFileMounts,
|
||||
generateVolumeMounts,
|
||||
prepareEnvironmentVariables,
|
||||
} from "../docker/utils";
|
||||
import { docker } from "@/server/constants";
|
||||
import type { CreateServiceOptions } from "dockerode";
|
||||
import type { Mount } from "@/server/api/services/mount";
|
||||
|
||||
type MysqlWithMounts = MySql & {
|
||||
mounts: Mount[];
|
||||
};
|
||||
|
||||
export const buildMysql = async (mysql: MysqlWithMounts) => {
|
||||
const {
|
||||
appName,
|
||||
env,
|
||||
externalPort,
|
||||
dockerImage,
|
||||
memoryLimit,
|
||||
memoryReservation,
|
||||
databaseName,
|
||||
databaseUser,
|
||||
databasePassword,
|
||||
databaseRootPassword,
|
||||
cpuLimit,
|
||||
cpuReservation,
|
||||
command,
|
||||
mounts,
|
||||
} = mysql;
|
||||
|
||||
const defaultMysqlEnv =
|
||||
databaseUser !== "root"
|
||||
? `MYSQL_USER=${databaseUser}\nMYSQL_DATABASE=${databaseName}\nMYSQL_PASSWORD=${databasePassword}\nMYSQL_ROOT_PASSWORD=${databaseRootPassword}${
|
||||
env ? `\n${env}` : ""
|
||||
}`
|
||||
: `MYSQL_DATABASE=${databaseName}\nMYSQL_ROOT_PASSWORD=${databaseRootPassword}${
|
||||
env ? `\n${env}` : ""
|
||||
}`;
|
||||
const resources = calculateResources({
|
||||
memoryLimit,
|
||||
memoryReservation,
|
||||
cpuLimit,
|
||||
cpuReservation,
|
||||
});
|
||||
const envVariables = prepareEnvironmentVariables(defaultMysqlEnv);
|
||||
const volumesMount = generateVolumeMounts(mounts);
|
||||
const bindsMount = generateBindMounts(mounts);
|
||||
const filesMount = generateFileMounts(appName, mounts);
|
||||
|
||||
const settings: CreateServiceOptions = {
|
||||
Name: appName,
|
||||
TaskTemplate: {
|
||||
ContainerSpec: {
|
||||
Image: dockerImage,
|
||||
Env: envVariables,
|
||||
Mounts: [...volumesMount, ...bindsMount, ...filesMount],
|
||||
...(command
|
||||
? {
|
||||
Command: ["/bin/sh"],
|
||||
Args: ["-c", command],
|
||||
}
|
||||
: {}),
|
||||
},
|
||||
Networks: [{ Target: "dokploy-network" }],
|
||||
Resources: {
|
||||
...resources,
|
||||
},
|
||||
},
|
||||
Mode: {
|
||||
Replicated: {
|
||||
Replicas: 1,
|
||||
},
|
||||
},
|
||||
EndpointSpec: {
|
||||
Mode: "dnsrr",
|
||||
Ports: externalPort
|
||||
? [
|
||||
{
|
||||
Protocol: "tcp",
|
||||
TargetPort: 3306,
|
||||
PublishedPort: externalPort,
|
||||
PublishMode: "host",
|
||||
},
|
||||
]
|
||||
: [],
|
||||
},
|
||||
};
|
||||
try {
|
||||
const service = docker.getService(appName);
|
||||
const inspect = await service.inspect();
|
||||
await service.update({
|
||||
version: Number.parseInt(inspect.Version.Index),
|
||||
...settings,
|
||||
});
|
||||
} catch (error) {
|
||||
await docker.createService(settings);
|
||||
}
|
||||
};
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user