mirror of
https://github.com/Dokploy/dokploy
synced 2025-06-26 18:27:59 +00:00
feat: initial commit
This commit is contained in:
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",
|
||||
});
|
||||
}
|
||||
};
|
||||
Reference in New Issue
Block a user