feat: initial commit

This commit is contained in:
Mauricio Siu
2024-04-28 23:57:52 -06:00
parent 8857a20344
commit be56ba046c
412 changed files with 60777 additions and 1 deletions

View 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]);
};

View 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
View 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;
};

View 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];
};

View 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);
};

View 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;
};

View 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];
};

View 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 [];
};

View 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];
};

View 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;
};

View 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;
};

View 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];
};

View 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;
};

View 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];
};

View 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;
};

View 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;
};

View 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",
});
}
};

View 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;
};

View 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",
});
}
};

View 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
View 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",
});
}
};