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

53
server/api/root.ts Normal file
View File

@@ -0,0 +1,53 @@
import { createTRPCRouter } from "../api/trpc";
import { authRouter } from "@/server/api/routers/auth";
import { projectRouter } from "./routers/project";
import { applicationRouter } from "./routers/application";
import { mysqlRouter } from "./routers/mysql";
import { postgresRouter } from "./routers/postgres";
import { redisRouter } from "./routers/redis";
import { mongoRouter } from "./routers/mongo";
import { mariadbRouter } from "./routers/mariadb";
import { userRouter } from "./routers/user";
import { domainRouter } from "./routers/domain";
import { destinationRouter } from "./routers/destination";
import { backupRouter } from "./routers/backup";
import { deploymentRouter } from "./routers/deployment";
import { mountRouter } from "./routers/mount";
import { certificateRouter } from "./routers/certificate";
import { settingsRouter } from "./routers/settings";
import { redirectsRouter } from "./routers/redirects";
import { securityRouter } from "./routers/security";
import { portRouter } from "./routers/port";
import { adminRouter } from "./routers/admin";
import { dockerRouter } from "./routers/docker";
/**
* This is the primary router for your server.
*
* All routers added in /api/routers should be manually added here.
*/
export const appRouter = createTRPCRouter({
admin: adminRouter,
docker: dockerRouter,
auth: authRouter,
project: projectRouter,
application: applicationRouter,
mysql: mysqlRouter,
postgres: postgresRouter,
redis: redisRouter,
mongo: mongoRouter,
mariadb: mariadbRouter,
user: userRouter,
domain: domainRouter,
destination: destinationRouter,
backup: backupRouter,
deployment: deploymentRouter,
mounts: mountRouter,
certificates: certificateRouter,
settings: settingsRouter,
security: securityRouter,
redirects: redirectsRouter,
port: portRouter,
});
// export type definition of API
export type AppRouter = typeof appRouter;

166
server/api/routers/admin.ts Normal file
View File

@@ -0,0 +1,166 @@
import {
apiAssignPermissions,
apiCreateUserInvitation,
apiFindOneToken,
apiGetBranches,
apiRemoveUser,
users,
} from "@/server/db/schema";
import {
createInvitation,
findAdmin,
getUserByToken,
removeUserByAuthId,
updateAdmin,
} from "../services/admin";
import {
adminProcedure,
createTRPCRouter,
protectedProcedure,
publicProcedure,
} from "../trpc";
import { db } from "@/server/db";
import { TRPCError } from "@trpc/server";
import { eq } from "drizzle-orm";
import { Octokit } from "octokit";
import { createAppAuth } from "@octokit/auth-app";
import { haveGithubRequirements } from "@/server/utils/providers/github";
export const adminRouter = createTRPCRouter({
one: adminProcedure.query(async () => {
const { sshPrivateKey, ...rest } = await findAdmin();
return {
haveSSH: !!sshPrivateKey,
...rest,
};
}),
createUserInvitation: adminProcedure
.input(apiCreateUserInvitation)
.mutation(async ({ input }) => {
try {
await createInvitation(input);
} catch (error) {
throw new TRPCError({
code: "BAD_REQUEST",
message:
"Error to create this user\ncheck if the email is not registered",
cause: error,
});
}
}),
removeUser: adminProcedure
.input(apiRemoveUser)
.mutation(async ({ input }) => {
try {
return await removeUserByAuthId(input.authId);
} catch (error) {
throw new TRPCError({
code: "BAD_REQUEST",
message: "Error to delete this user",
cause: error,
});
}
}),
getUserByToken: publicProcedure
.input(apiFindOneToken)
.query(async ({ input }) => {
return await getUserByToken(input.token);
}),
assignPermissions: adminProcedure
.input(apiAssignPermissions)
.mutation(async ({ input }) => {
try {
await db
.update(users)
.set({
...input,
})
.where(eq(users.userId, input.userId));
} catch (error) {
throw new TRPCError({
code: "BAD_REQUEST",
message: "Error to assign permissions",
});
}
}),
cleanGithubApp: adminProcedure.mutation(async ({ ctx }) => {
try {
return await updateAdmin(ctx.user.authId, {
githubAppName: "",
githubClientId: "",
githubClientSecret: "",
githubInstallationId: "",
});
} catch (error) {
throw new TRPCError({
code: "BAD_REQUEST",
message: "Error to delete this github app",
cause: error,
});
}
}),
getRepositories: protectedProcedure.query(async () => {
const admin = await findAdmin();
const completeRequirements = haveGithubRequirements(admin);
if (!completeRequirements) {
throw new TRPCError({
code: "BAD_REQUEST",
message: "Admin need to setup correctly github account",
});
}
const octokit = new Octokit({
authStrategy: createAppAuth,
auth: {
appId: admin.githubAppId,
privateKey: admin.githubPrivateKey,
installationId: admin.githubInstallationId,
},
});
const repositories = (await octokit.paginate(
octokit.rest.apps.listReposAccessibleToInstallation,
)) as unknown as Awaited<
ReturnType<typeof octokit.rest.apps.listReposAccessibleToInstallation>
>["data"]["repositories"];
return repositories;
}),
getBranches: protectedProcedure
.input(apiGetBranches)
.query(async ({ input }) => {
const admin = await findAdmin();
const completeRequirements = haveGithubRequirements(admin);
if (!completeRequirements) {
throw new TRPCError({
code: "BAD_REQUEST",
message: "Admin need to setup correctly github account",
});
}
const octokit = new Octokit({
authStrategy: createAppAuth,
auth: {
appId: admin.githubAppId,
privateKey: admin.githubPrivateKey,
installationId: admin.githubInstallationId,
},
});
const branches = await octokit.rest.repos.listBranches({
owner: input.owner,
repo: input.repo,
});
return branches.data;
}),
haveGithubConfigured: protectedProcedure.query(async () => {
const adminResponse = await findAdmin();
return haveGithubRequirements(adminResponse);
}),
});

View File

@@ -0,0 +1,339 @@
import { createTRPCRouter, protectedProcedure } from "@/server/api/trpc";
import { db } from "@/server/db";
import {
apiCreateApplication,
apiFindMonitoringStats,
apiFindOneApplication,
apiReloadApplication,
apiSaveBuildType,
apiSaveDockerProvider,
apiSaveEnviromentVariables,
apiSaveGitProvider,
apiSaveGithubProvider,
apiUpdateApplication,
applications,
} from "@/server/db/schema/application";
import {
cleanQueuesByApplication,
type DeploymentJob,
} from "@/server/queues/deployments-queue";
import { myQueue } from "@/server/queues/queueSetup";
import {
removeService,
startService,
stopService,
} from "@/server/utils/docker/utils";
import {
removeDirectoryCode,
removeMonitoringDirectory,
} from "@/server/utils/filesystem/directory";
import {
generateSSHKey,
readRSAFile,
removeRSAFiles,
} from "@/server/utils/filesystem/ssh";
import {
readConfig,
removeTraefikConfig,
writeConfig,
} from "@/server/utils/traefik/application";
import { TRPCError } from "@trpc/server";
import { eq } from "drizzle-orm";
import {
createApplication,
findApplicationById,
getApplicationStats,
updateApplication,
updateApplicationStatus,
} from "../services/application";
import { removeDeployments } from "../services/deployment";
import { deleteAllMiddlewares } from "@/server/utils/traefik/middleware";
import { z } from "zod";
import { nanoid } from "nanoid";
import { addNewService, checkServiceAccess } from "../services/user";
export const applicationRouter = createTRPCRouter({
create: protectedProcedure
.input(apiCreateApplication)
.mutation(async ({ input, ctx }) => {
try {
if (ctx.user.rol === "user") {
await checkServiceAccess(ctx.user.authId, input.projectId, "create");
}
const newApplication = await createApplication(input);
if (ctx.user.rol === "user") {
await addNewService(ctx.user.authId, newApplication.applicationId);
}
} catch (error) {
throw new TRPCError({
code: "BAD_REQUEST",
message: "Error to create the application",
cause: error,
});
}
}),
one: protectedProcedure
.input(apiFindOneApplication)
.query(async ({ input, ctx }) => {
if (ctx.user.rol === "user") {
await checkServiceAccess(
ctx.user.authId,
input.applicationId,
"access",
);
}
return await findApplicationById(input.applicationId);
}),
reload: protectedProcedure
.input(apiReloadApplication)
.mutation(async ({ input }) => {
await stopService(input.appName);
await updateApplicationStatus(input.applicationId, "idle");
await startService(input.appName);
await updateApplicationStatus(input.applicationId, "done");
return true;
}),
delete: protectedProcedure
.input(apiFindOneApplication)
.mutation(async ({ input, ctx }) => {
if (ctx.user.rol === "user") {
await checkServiceAccess(
ctx.user.authId,
input.applicationId,
"delete",
);
}
const application = await findApplicationById(input.applicationId);
const result = await db
.delete(applications)
.where(eq(applications.applicationId, input.applicationId))
.returning();
const cleanupOperations = [
async () => deleteAllMiddlewares(application),
async () => await removeDeployments(application),
async () => await removeDirectoryCode(application?.appName),
async () => await removeMonitoringDirectory(application?.appName),
async () => await removeTraefikConfig(application?.appName),
async () => await removeService(application?.appName),
async () => await removeRSAFiles(application?.appName),
];
for (const operation of cleanupOperations) {
try {
await operation();
} catch (error) {}
}
return result[0];
}),
stop: protectedProcedure
.input(apiFindOneApplication)
.mutation(async ({ input }) => {
const service = await findApplicationById(input.applicationId);
await stopService(service.appName);
await updateApplicationStatus(input.applicationId, "idle");
return service;
}),
start: protectedProcedure
.input(apiFindOneApplication)
.mutation(async ({ input }) => {
const service = await findApplicationById(input.applicationId);
await startService(service.appName);
await updateApplicationStatus(input.applicationId, "done");
return service;
}),
redeploy: protectedProcedure
.input(apiFindOneApplication)
.mutation(async ({ input }) => {
const jobData: DeploymentJob = {
applicationId: input.applicationId,
titleLog: "Rebuild deployment",
type: "redeploy",
};
await myQueue.add(
"deployments",
{ ...jobData },
{
removeOnComplete: true,
removeOnFail: true,
},
);
}),
saveEnviroment: protectedProcedure
.input(apiSaveEnviromentVariables)
.mutation(async ({ input }) => {
await updateApplication(input.applicationId, {
env: input.env,
});
return true;
}),
saveBuildType: protectedProcedure
.input(apiSaveBuildType)
.mutation(async ({ input }) => {
await updateApplication(input.applicationId, {
buildType: input.buildType,
dockerfile: input.dockerfile,
});
return true;
}),
saveGithubProvider: protectedProcedure
.input(apiSaveGithubProvider)
.mutation(async ({ input }) => {
await updateApplication(input.applicationId, {
repository: input.repository,
branch: input.branch,
sourceType: "github",
owner: input.owner,
buildPath: input.buildPath,
applicationStatus: "idle",
});
return true;
}),
saveDockerProvider: protectedProcedure
.input(apiSaveDockerProvider)
.mutation(async ({ input }) => {
await updateApplication(input.applicationId, {
dockerImage: input.dockerImage,
username: input.username,
password: input.password,
sourceType: "docker",
applicationStatus: "idle",
});
return true;
}),
saveGitProdiver: protectedProcedure
.input(apiSaveGitProvider)
.mutation(async ({ input }) => {
await updateApplication(input.applicationId, {
customGitBranch: input.customGitBranch,
customGitBuildPath: input.customGitBuildPath,
customGitUrl: input.customGitUrl,
sourceType: "git",
applicationStatus: "idle",
});
return true;
}),
generateSSHKey: protectedProcedure
.input(apiFindOneApplication)
.mutation(async ({ input }) => {
const application = await findApplicationById(input.applicationId);
try {
await generateSSHKey(application.appName);
const file = await readRSAFile(application.appName);
await updateApplication(input.applicationId, {
customGitSSHKey: file,
});
} catch (error) {}
return true;
}),
removeSSHKey: protectedProcedure
.input(apiFindOneApplication)
.mutation(async ({ input }) => {
const application = await findApplicationById(input.applicationId);
await removeRSAFiles(application.appName);
await updateApplication(input.applicationId, {
customGitSSHKey: null,
});
return true;
}),
markRunning: protectedProcedure
.input(apiFindOneApplication)
.mutation(async ({ input }) => {
await updateApplicationStatus(input.applicationId, "running");
}),
update: protectedProcedure
.input(apiUpdateApplication)
.mutation(async ({ input }) => {
const { applicationId, ...rest } = input;
const application = await updateApplication(applicationId, {
...rest,
});
if (!application) {
throw new TRPCError({
code: "BAD_REQUEST",
message: "Update: Error to update application",
});
}
return true;
}),
refreshToken: protectedProcedure
.input(apiFindOneApplication)
.mutation(async ({ input }) => {
await updateApplication(input.applicationId, {
refreshToken: nanoid(),
});
return true;
}),
deploy: protectedProcedure
.input(apiFindOneApplication)
.mutation(async ({ input }) => {
const jobData: DeploymentJob = {
applicationId: input.applicationId,
titleLog: "Manual deployment",
type: "deploy",
};
await myQueue.add(
"deployments",
{ ...jobData },
{
removeOnComplete: true,
removeOnFail: true,
},
);
}),
cleanQueues: protectedProcedure
.input(apiFindOneApplication)
.mutation(async ({ input }) => {
await cleanQueuesByApplication(input.applicationId);
}),
readTraefikConfig: protectedProcedure
.input(apiFindOneApplication)
.query(async ({ input }) => {
const application = await findApplicationById(input.applicationId);
const traefikConfig = readConfig(application.appName);
return traefikConfig;
}),
updateTraefikConfig: protectedProcedure
.input(z.object({ applicationId: z.string(), traefikConfig: z.string() }))
.mutation(async ({ input }) => {
const application = await findApplicationById(input.applicationId);
writeConfig(application.appName, input.traefikConfig);
return true;
}),
readAppMonitoring: protectedProcedure
.input(apiFindMonitoringStats)
.query(async ({ input }) => {
const stats = await getApplicationStats(input.appName);
return stats;
}),
});
// Paketo Buildpacks: paketobuildpacks/builder-jammy-full Ubuntu 22.04 Jammy Jellyfish full image with buildpacks for Apache HTTPD, Go, Java, Java Native Image, .NET, NGINX, Node.js, PHP, Procfile, Python, and Ruby
// Heroku: heroku/builder:22 Heroku-22 (Ubuntu 22.04) base image with buildpacks for Go, Java, Node.js, PHP, Python, Ruby & Scala.
// pack build imageName --path ./ --builder paketobuildpacks/builder-jammy-full
// pack build prueba-pack --path ./ --builder heroku/builder:22

199
server/api/routers/auth.ts Normal file
View File

@@ -0,0 +1,199 @@
import { TRPCError } from "@trpc/server";
import * as bcrypt from "bcrypt";
import {
adminProcedure,
createTRPCRouter,
protectedProcedure,
publicProcedure,
} from "../trpc";
import { lucia, validateRequest } from "@/server/auth/auth";
import {
apiCreateAdmin,
apiCreateUser,
apiFindOneAuth,
apiUpdateAuthByAdmin,
apiLogin,
apiUpdateAuth,
apiVerify2FA,
apiVerifyLogin2FA,
} from "@/server/db/schema";
import {
createAdmin,
createUser,
findAuthByEmail,
findAuthById,
generate2FASecret,
updateAuthById,
verify2FA,
} from "../services/auth";
export const authRouter = createTRPCRouter({
createAdmin: publicProcedure
.input(apiCreateAdmin)
.mutation(async ({ ctx, input }) => {
try {
const newAdmin = await createAdmin(input);
const session = await lucia.createSession(newAdmin.id || "", {});
ctx.res.appendHeader(
"Set-Cookie",
lucia.createSessionCookie(session.id).serialize(),
);
return true;
} catch (error) {
throw new TRPCError({
code: "BAD_REQUEST",
message: "Error to create the main admin",
cause: error,
});
}
}),
createUser: publicProcedure
.input(apiCreateUser)
.mutation(async ({ ctx, input }) => {
try {
const newUser = await createUser(input);
const session = await lucia.createSession(newUser?.authId || "", {});
ctx.res.appendHeader(
"Set-Cookie",
lucia.createSessionCookie(session.id).serialize(),
);
return true;
} catch (error) {
throw new TRPCError({
code: "BAD_REQUEST",
message: "Error to create the user",
cause: error,
});
}
}),
login: publicProcedure.input(apiLogin).mutation(async ({ ctx, input }) => {
try {
const auth = await findAuthByEmail(input.email);
const correctPassword = bcrypt.compareSync(
input.password,
auth?.password || "",
);
if (!correctPassword) {
throw new TRPCError({
code: "BAD_REQUEST",
message: "Credentials do not match",
});
}
if (auth?.is2FAEnabled) {
return {
is2FAEnabled: true,
authId: auth.id,
};
}
const session = await lucia.createSession(auth?.id || "", {});
ctx.res.appendHeader(
"Set-Cookie",
lucia.createSessionCookie(session.id).serialize(),
);
return {
is2FAEnabled: false,
authId: auth?.id,
};
} catch (error) {
throw new TRPCError({
code: "BAD_REQUEST",
message: "Credentials do not match",
cause: error,
});
}
}),
get: protectedProcedure.query(async ({ ctx }) => {
const auth = await findAuthById(ctx.user.authId);
return auth;
}),
logout: protectedProcedure.mutation(async ({ ctx }) => {
const { req, res } = ctx;
const { session } = await validateRequest(req, res);
if (!session) return false;
await lucia.invalidateSession(session.id);
res.setHeader("Set-Cookie", lucia.createBlankSessionCookie().serialize());
return true;
}),
update: protectedProcedure
.input(apiUpdateAuth)
.mutation(async ({ ctx, input }) => {
const auth = await updateAuthById(ctx.user.authId, {
...(input.email && { email: input.email }),
...(input.password && {
password: bcrypt.hashSync(input.password, 10),
}),
...(input.image && { image: input.image }),
});
return auth;
}),
one: adminProcedure.input(apiFindOneAuth).query(async ({ input }) => {
const auth = await findAuthById(input.id);
return auth;
}),
updateByAdmin: protectedProcedure
.input(apiUpdateAuthByAdmin)
.mutation(async ({ input }) => {
const auth = await updateAuthById(input.id, {
...(input.email && { email: input.email }),
...(input.password && {
password: bcrypt.hashSync(input.password, 10),
}),
...(input.image && { image: input.image }),
});
return auth;
}),
generate2FASecret: protectedProcedure.query(async ({ ctx }) => {
return await generate2FASecret(ctx.user.authId);
}),
verify2FASetup: protectedProcedure
.input(apiVerify2FA)
.mutation(async ({ ctx, input }) => {
const auth = await findAuthById(ctx.user.authId);
await verify2FA(auth, input.secret, input.pin);
await updateAuthById(auth.id, {
is2FAEnabled: true,
secret: input.secret,
});
return auth;
}),
verifyLogin2FA: publicProcedure
.input(apiVerifyLogin2FA)
.mutation(async ({ ctx, input }) => {
const auth = await findAuthById(input.id);
await verify2FA(auth, auth.secret || "", input.pin);
const session = await lucia.createSession(auth.id, {});
ctx.res.appendHeader(
"Set-Cookie",
lucia.createSessionCookie(session.id).serialize(),
);
return auth;
}),
disable2FA: protectedProcedure.mutation(async ({ ctx }) => {
const auth = await findAuthById(ctx.user.authId);
await updateAuthById(auth.id, {
is2FAEnabled: false,
secret: null,
});
return auth;
}),
});

View File

@@ -0,0 +1,152 @@
import { createTRPCRouter, protectedProcedure } from "@/server/api/trpc";
import {
apiCreateBackup,
apiFindOneBackup,
apiRemoveBackup,
apiUpdateBackup,
} from "@/server/db/schema";
import { runMariadbBackup } from "@/server/utils/backups/mariadb";
import { runMongoBackup } from "@/server/utils/backups/mongo";
import { runMySqlBackup } from "@/server/utils/backups/mysql";
import { runPostgresBackup } from "@/server/utils/backups/postgres";
import { TRPCError } from "@trpc/server";
import {
createBackup,
findBackupById,
removeBackupById,
updateBackupById,
} from "../services/backup";
import { findMariadbByBackupId } from "../services/mariadb";
import { findMongoByBackupId } from "../services/mongo";
import { findMySqlByBackupId } from "../services/mysql";
import { findPostgresByBackupId } from "../services/postgres";
import {
removeScheduleBackup,
scheduleBackup,
} from "@/server/utils/backups/utils";
export const backupRouter = createTRPCRouter({
create: protectedProcedure
.input(apiCreateBackup)
.mutation(async ({ input }) => {
try {
const newBackup = await createBackup(input);
const backup = await findBackupById(newBackup.backupId);
if (backup.enabled) {
scheduleBackup(backup);
}
} catch (error) {
throw new TRPCError({
code: "BAD_REQUEST",
message: "Error to create the Backup",
cause: error,
});
}
}),
one: protectedProcedure.input(apiFindOneBackup).query(async ({ input }) => {
const backup = await findBackupById(input.backupId);
return backup;
}),
update: protectedProcedure
.input(apiUpdateBackup)
.mutation(async ({ input }) => {
try {
await updateBackupById(input.backupId, input);
const backup = await findBackupById(input.backupId);
if (backup.enabled) {
scheduleBackup(backup);
} else {
removeScheduleBackup(input.backupId);
}
} catch (error) {
throw new TRPCError({
code: "BAD_REQUEST",
message: "Error to update this Backup",
});
}
}),
remove: protectedProcedure
.input(apiRemoveBackup)
.mutation(async ({ input }) => {
try {
const value = await removeBackupById(input.backupId);
removeScheduleBackup(input.backupId);
return value;
} catch (error) {
throw new TRPCError({
code: "BAD_REQUEST",
message: "Error to delete this Backup",
cause: error,
});
}
}),
manualBackupPostgres: protectedProcedure
.input(apiFindOneBackup)
.mutation(async ({ input }) => {
try {
const backup = await findBackupById(input.backupId);
const postgres = await findPostgresByBackupId(backup.backupId);
await runPostgresBackup(postgres, backup);
return true;
} catch (error) {
console.log(error);
throw new TRPCError({
code: "BAD_REQUEST",
message: "Error to run manual postgres backup ",
cause: error,
});
}
}),
manualBackupMySql: protectedProcedure
.input(apiFindOneBackup)
.mutation(async ({ input }) => {
try {
const backup = await findBackupById(input.backupId);
const mysql = await findMySqlByBackupId(backup.backupId);
await runMySqlBackup(mysql, backup);
return true;
} catch (error) {
throw new TRPCError({
code: "BAD_REQUEST",
message: "Error to run manual mysql backup ",
cause: error,
});
}
}),
manualBackupMariadb: protectedProcedure
.input(apiFindOneBackup)
.mutation(async ({ input }) => {
try {
const backup = await findBackupById(input.backupId);
const mariadb = await findMariadbByBackupId(backup.backupId);
await runMariadbBackup(mariadb, backup);
return true;
} catch (error) {
throw new TRPCError({
code: "BAD_REQUEST",
message: "Error to run manual mariadb backup ",
cause: error,
});
}
}),
manualBackupMongo: protectedProcedure
.input(apiFindOneBackup)
.mutation(async ({ input }) => {
try {
const backup = await findBackupById(input.backupId);
const mongo = await findMongoByBackupId(backup.backupId);
await runMongoBackup(mongo, backup);
return true;
} catch (error) {
throw new TRPCError({
code: "BAD_REQUEST",
message: "Error to run manual mongo backup ",
cause: error,
});
}
}),
});

View File

@@ -0,0 +1,29 @@
import { adminProcedure, createTRPCRouter } from "@/server/api/trpc";
import { apiCreateCertificate, apiFindCertificate } from "@/server/db/schema";
import {
createCertificate,
findCertificates,
findCertificateById,
removeCertificateById,
} from "../services/certificate";
export const certificateRouter = createTRPCRouter({
create: adminProcedure
.input(apiCreateCertificate)
.mutation(async ({ input }) => {
return await createCertificate(input);
}),
one: adminProcedure.input(apiFindCertificate).query(async ({ input }) => {
return await findCertificateById(input.certificateId);
}),
remove: adminProcedure
.input(apiFindCertificate)
.mutation(async ({ input }) => {
await removeCertificateById(input.certificateId);
return true;
}),
all: adminProcedure.query(async () => {
return findCertificates();
}),
});

View File

@@ -0,0 +1,11 @@
import { apiFindAllByApplication } from "@/server/db/schema";
import { findAllDeploymentsByApplicationId } from "../services/deployment";
import { createTRPCRouter, protectedProcedure } from "../trpc";
export const deploymentRouter = createTRPCRouter({
all: protectedProcedure
.input(apiFindAllByApplication)
.query(async ({ input }) => {
return await findAllDeploymentsByApplicationId(input.applicationId);
}),
});

View File

@@ -0,0 +1,99 @@
import {
adminProcedure,
createTRPCRouter,
protectedProcedure,
} from "@/server/api/trpc";
import { db } from "@/server/db";
import {
apiCreateDestination,
apiFindOneDestination,
apiRemoveDestination,
apiUpdateDestination,
} from "@/server/db/schema";
import { HeadBucketCommand, S3Client } from "@aws-sdk/client-s3";
import { TRPCError } from "@trpc/server";
import {
createDestintation,
findDestinationById,
removeDestinationById,
updateDestinationById,
} from "../services/destination";
import { findAdmin } from "../services/admin";
export const destinationRouter = createTRPCRouter({
create: adminProcedure
.input(apiCreateDestination)
.mutation(async ({ input }) => {
try {
await createDestintation(input);
return await findAdmin();
} catch (error) {
throw new TRPCError({
code: "BAD_REQUEST",
message: "Error to create the destination",
cause: error,
});
}
}),
testConnection: adminProcedure
.input(apiCreateDestination)
.mutation(async ({ input }) => {
const { secretAccessKey, bucket, region, endpoint, accessKey } = input;
const s3Client = new S3Client({
region: region,
...(endpoint && {
endpoint: endpoint,
}),
credentials: {
accessKeyId: accessKey,
secretAccessKey: secretAccessKey,
},
forcePathStyle: true,
});
const headBucketCommand = new HeadBucketCommand({ Bucket: bucket });
try {
await s3Client.send(headBucketCommand);
} catch (error) {
throw new TRPCError({
code: "BAD_REQUEST",
message: "Error to connect to bucket",
cause: error,
});
}
}),
one: protectedProcedure
.input(apiFindOneDestination)
.query(async ({ input }) => {
const destination = await findDestinationById(input.destinationId);
return destination;
}),
all: adminProcedure.query(async () => {
return await db.query.destinations.findMany({});
}),
remove: adminProcedure
.input(apiRemoveDestination)
.mutation(async ({ input }) => {
try {
return await removeDestinationById(input.destinationId);
} catch (error) {
throw new TRPCError({
code: "BAD_REQUEST",
message: "Error to delete this destination",
});
}
}),
update: adminProcedure
.input(apiUpdateDestination)
.mutation(async ({ input }) => {
try {
return await updateDestinationById(input.destinationId, input);
} catch (error) {
throw new TRPCError({
code: "BAD_REQUEST",
message: "Error to update this destination",
cause: error,
});
}
}),
});

View File

@@ -0,0 +1,44 @@
import { z } from "zod";
import { createTRPCRouter, protectedProcedure } from "../trpc";
import {
getConfig,
getContainersByAppLabel,
getContainers,
getContainersByAppNameMatch,
} from "../services/docker";
export const dockerRouter = createTRPCRouter({
getContainers: protectedProcedure.query(async () => {
return await getContainers();
}),
getConfig: protectedProcedure
.input(
z.object({
containerId: z.string().min(1),
}),
)
.query(async ({ input }) => {
return await getConfig(input.containerId);
}),
getContainersByAppNameMatch: protectedProcedure
.input(
z.object({
appName: z.string().min(1),
}),
)
.query(async ({ input }) => {
return await getContainersByAppNameMatch(input.appName);
}),
getContainersByAppLabel: protectedProcedure
.input(
z.object({
appName: z.string().min(1),
}),
)
.query(async ({ input }) => {
return await getContainersByAppLabel(input.appName);
}),
});

View File

@@ -0,0 +1,59 @@
import { createTRPCRouter, protectedProcedure } from "@/server/api/trpc";
import {
apiCreateDomain,
apiFindDomain,
apiFindDomainByApplication,
apiUpdateDomain,
} from "@/server/db/schema";
import { manageDomain, removeDomain } from "@/server/utils/traefik/domain";
import { TRPCError } from "@trpc/server";
import { findApplicationById } from "../services/application";
import {
createDomain,
findDomainById,
findDomainsByApplicationId,
removeDomainById,
updateDomainById,
} from "../services/domain";
export const domainRouter = createTRPCRouter({
create: protectedProcedure
.input(apiCreateDomain)
.mutation(async ({ input }) => {
try {
await createDomain(input);
} catch (error) {
throw new TRPCError({
code: "BAD_REQUEST",
message: "Error to create the domain",
cause: error,
});
}
}),
byApplicationId: protectedProcedure
.input(apiFindDomainByApplication)
.query(async ({ input }) => {
return await findDomainsByApplicationId(input.applicationId);
}),
update: protectedProcedure
.input(apiUpdateDomain)
.mutation(async ({ input }) => {
const result = await updateDomainById(input.domainId, input);
const domain = await findDomainById(input.domainId);
const application = await findApplicationById(domain.applicationId);
await manageDomain(application, domain);
return result;
}),
one: protectedProcedure.input(apiFindDomain).query(async ({ input }) => {
return await findDomainById(input.domainId);
}),
delete: protectedProcedure
.input(apiFindDomain)
.mutation(async ({ input }) => {
const domain = await findDomainById(input.domainId);
const result = await removeDomainById(input.domainId);
removeDomain(domain.application.appName, domain.uniqueConfigKey);
return result;
}),
});

View File

@@ -0,0 +1,183 @@
import { createTRPCRouter, protectedProcedure } from "@/server/api/trpc";
import {
apiChangeMariaDBStatus,
apiCreateMariaDB,
apiDeployMariaDB,
apiFindOneMariaDB,
apiResetMariadb,
apiSaveEnviromentVariablesMariaDB,
apiSaveExternalPortMariaDB,
apiUpdateMariaDB,
} from "@/server/db/schema/mariadb";
import {
removeService,
startService,
stopService,
} from "@/server/utils/docker/utils";
import { TRPCError } from "@trpc/server";
import {
createMariadb,
deployMariadb,
findMariadbById,
removeMariadbById,
updateMariadbById,
} from "../services/mariadb";
import { addNewService, checkServiceAccess } from "../services/user";
import { createMount } from "../services/mount";
export const mariadbRouter = createTRPCRouter({
create: protectedProcedure
.input(apiCreateMariaDB)
.mutation(async ({ input, ctx }) => {
try {
if (ctx.user.rol === "user") {
await checkServiceAccess(ctx.user.authId, input.projectId, "create");
}
const newMariadb = await createMariadb(input);
if (ctx.user.rol === "user") {
await addNewService(ctx.user.authId, newMariadb.mariadbId);
}
await createMount({
serviceId: newMariadb.mariadbId,
serviceType: "mariadb",
volumeName: `${newMariadb.appName}-data`,
mountPath: "/var/lib/mysql",
type: "volume",
});
return true;
} catch (error) {
throw new TRPCError({
code: "BAD_REQUEST",
message: "Error input: Inserting mariadb database",
cause: error,
});
}
}),
one: protectedProcedure
.input(apiFindOneMariaDB)
.query(async ({ input, ctx }) => {
if (ctx.user.rol === "user") {
await checkServiceAccess(ctx.user.authId, input.mariadbId, "access");
}
return await findMariadbById(input.mariadbId);
}),
start: protectedProcedure
.input(apiFindOneMariaDB)
.mutation(async ({ input }) => {
const service = await findMariadbById(input.mariadbId);
await startService(service.appName);
await updateMariadbById(input.mariadbId, {
applicationStatus: "done",
});
return service;
}),
stop: protectedProcedure
.input(apiFindOneMariaDB)
.mutation(async ({ input }) => {
const mongo = await findMariadbById(input.mariadbId);
await stopService(mongo.appName);
await updateMariadbById(input.mariadbId, {
applicationStatus: "idle",
});
return mongo;
}),
saveExternalPort: protectedProcedure
.input(apiSaveExternalPortMariaDB)
.mutation(async ({ input }) => {
const mongo = await findMariadbById(input.mariadbId);
await updateMariadbById(input.mariadbId, {
externalPort: input.externalPort,
});
await deployMariadb(input.mariadbId);
return mongo;
}),
deploy: protectedProcedure
.input(apiDeployMariaDB)
.mutation(async ({ input }) => {
return deployMariadb(input.mariadbId);
}),
changeStatus: protectedProcedure
.input(apiChangeMariaDBStatus)
.mutation(async ({ input }) => {
const mongo = await findMariadbById(input.mariadbId);
await updateMariadbById(input.mariadbId, {
applicationStatus: input.applicationStatus,
});
return mongo;
}),
remove: protectedProcedure
.input(apiFindOneMariaDB)
.mutation(async ({ input, ctx }) => {
if (ctx.user.rol === "user") {
await checkServiceAccess(ctx.user.authId, input.mariadbId, "delete");
}
const mongo = await findMariadbById(input.mariadbId);
const cleanupOperations = [
async () => await removeService(mongo?.appName),
async () => await removeMariadbById(input.mariadbId),
];
for (const operation of cleanupOperations) {
try {
await operation();
} catch (error) {}
}
return mongo;
}),
saveEnviroment: protectedProcedure
.input(apiSaveEnviromentVariablesMariaDB)
.mutation(async ({ input }) => {
const service = await updateMariadbById(input.mariadbId, {
env: input.env,
});
if (!service) {
throw new TRPCError({
code: "BAD_REQUEST",
message: "Update: Error to add enviroment variables",
});
}
return true;
}),
reload: protectedProcedure
.input(apiResetMariadb)
.mutation(async ({ input }) => {
await stopService(input.appName);
await updateMariadbById(input.mariadbId, {
applicationStatus: "idle",
});
await startService(input.appName);
await updateMariadbById(input.mariadbId, {
applicationStatus: "done",
});
return true;
}),
update: protectedProcedure
.input(apiUpdateMariaDB)
.mutation(async ({ input }) => {
const { mariadbId, ...rest } = input;
const service = await updateMariadbById(mariadbId, {
...rest,
});
if (!service) {
throw new TRPCError({
code: "BAD_REQUEST",
message: "Update: Error to update mariadb",
});
}
return true;
}),
});

184
server/api/routers/mongo.ts Normal file
View File

@@ -0,0 +1,184 @@
import { createTRPCRouter, protectedProcedure } from "@/server/api/trpc";
import {
apiChangeMongoStatus,
apiCreateMongo,
apiDeployMongo,
apiFindOneMongo,
apiResetMongo,
apiSaveEnviromentVariablesMongo,
apiSaveExternalPortMongo,
apiUpdateMongo,
} from "@/server/db/schema/mongo";
import {
removeService,
startService,
stopService,
} from "@/server/utils/docker/utils";
import { TRPCError } from "@trpc/server";
import {
createMongo,
deployMongo,
findMongoById,
removeMongoById,
updateMongoById,
} from "../services/mongo";
import { addNewService, checkServiceAccess } from "../services/user";
import { createMount } from "../services/mount";
export const mongoRouter = createTRPCRouter({
create: protectedProcedure
.input(apiCreateMongo)
.mutation(async ({ input, ctx }) => {
try {
if (ctx.user.rol === "user") {
await checkServiceAccess(ctx.user.authId, input.projectId, "create");
}
const newMongo = await createMongo(input);
if (ctx.user.rol === "user") {
await addNewService(ctx.user.authId, newMongo.mongoId);
}
await createMount({
serviceId: newMongo.mongoId,
serviceType: "mongo",
volumeName: `${newMongo.appName}-data`,
mountPath: "/data/db",
type: "volume",
});
return true;
} catch (error) {
throw new TRPCError({
code: "BAD_REQUEST",
message: "Error input: Inserting mongo database",
cause: error,
});
}
}),
one: protectedProcedure
.input(apiFindOneMongo)
.query(async ({ input, ctx }) => {
if (ctx.user.rol === "user") {
await checkServiceAccess(ctx.user.authId, input.mongoId, "access");
}
return await findMongoById(input.mongoId);
}),
start: protectedProcedure
.input(apiFindOneMongo)
.mutation(async ({ input }) => {
const service = await findMongoById(input.mongoId);
await startService(service.appName);
await updateMongoById(input.mongoId, {
applicationStatus: "done",
});
return service;
}),
stop: protectedProcedure
.input(apiFindOneMongo)
.mutation(async ({ input }) => {
const mongo = await findMongoById(input.mongoId);
await stopService(mongo.appName);
await updateMongoById(input.mongoId, {
applicationStatus: "idle",
});
return mongo;
}),
saveExternalPort: protectedProcedure
.input(apiSaveExternalPortMongo)
.mutation(async ({ input }) => {
const mongo = await findMongoById(input.mongoId);
await updateMongoById(input.mongoId, {
externalPort: input.externalPort,
});
await deployMongo(input.mongoId);
return mongo;
}),
deploy: protectedProcedure
.input(apiDeployMongo)
.mutation(async ({ input }) => {
return deployMongo(input.mongoId);
}),
changeStatus: protectedProcedure
.input(apiChangeMongoStatus)
.mutation(async ({ input }) => {
const mongo = await findMongoById(input.mongoId);
await updateMongoById(input.mongoId, {
applicationStatus: input.applicationStatus,
});
return mongo;
}),
reload: protectedProcedure
.input(apiResetMongo)
.mutation(async ({ input }) => {
await stopService(input.appName);
await updateMongoById(input.mongoId, {
applicationStatus: "idle",
});
await startService(input.appName);
await updateMongoById(input.mongoId, {
applicationStatus: "done",
});
return true;
}),
remove: protectedProcedure
.input(apiFindOneMongo)
.mutation(async ({ input, ctx }) => {
if (ctx.user.rol === "user") {
await checkServiceAccess(ctx.user.authId, input.mongoId, "delete");
}
const mongo = await findMongoById(input.mongoId);
const cleanupOperations = [
async () => await removeService(mongo?.appName),
async () => await removeMongoById(input.mongoId),
];
for (const operation of cleanupOperations) {
try {
await operation();
} catch (error) {}
}
return mongo;
}),
saveEnviroment: protectedProcedure
.input(apiSaveEnviromentVariablesMongo)
.mutation(async ({ input }) => {
const service = await updateMongoById(input.mongoId, {
env: input.env,
});
if (!service) {
throw new TRPCError({
code: "BAD_REQUEST",
message: "Update: Error to add enviroment variables",
});
}
return true;
}),
update: protectedProcedure
.input(apiUpdateMongo)
.mutation(async ({ input }) => {
const { mongoId, ...rest } = input;
const service = await updateMongoById(mongoId, {
...rest,
});
if (!service) {
throw new TRPCError({
code: "BAD_REQUEST",
message: "Update: Error to update mongo",
});
}
return true;
}),
});

View File

@@ -0,0 +1,17 @@
import { apiCreateMount, apiRemoveMount } from "@/server/db/schema";
import { createMount, deleteMount } from "../services/mount";
import { createTRPCRouter, protectedProcedure } from "../trpc";
export const mountRouter = createTRPCRouter({
create: protectedProcedure
.input(apiCreateMount)
.mutation(async ({ input }) => {
await createMount(input);
return true;
}),
remove: protectedProcedure
.input(apiRemoveMount)
.mutation(async ({ input }) => {
return await deleteMount(input.mountId);
}),
});

183
server/api/routers/mysql.ts Normal file
View File

@@ -0,0 +1,183 @@
import { createTRPCRouter, protectedProcedure } from "@/server/api/trpc";
import {
apiChangeMySqlStatus,
apiCreateMySql,
apiDeployMySql,
apiFindOneMySql,
apiResetMysql,
apiSaveEnviromentVariablesMySql,
apiSaveExternalPortMySql,
apiUpdateMySql,
} from "@/server/db/schema/mysql";
import {
removeService,
startService,
stopService,
} from "@/server/utils/docker/utils";
import { TRPCError } from "@trpc/server";
import {
createMysql,
deployMySql,
findMySqlById,
removeMySqlById,
updateMySqlById,
} from "../services/mysql";
import { addNewService, checkServiceAccess } from "../services/user";
import { createMount } from "../services/mount";
import { z } from "zod";
export const mysqlRouter = createTRPCRouter({
create: protectedProcedure
.input(apiCreateMySql)
.mutation(async ({ input, ctx }) => {
try {
if (ctx.user.rol === "user") {
await checkServiceAccess(ctx.user.authId, input.projectId, "create");
}
const newMysql = await createMysql(input);
if (ctx.user.rol === "user") {
await addNewService(ctx.user.authId, newMysql.mysqlId);
}
await createMount({
serviceId: newMysql.mysqlId,
serviceType: "mysql",
volumeName: `${newMysql.appName}-data`,
mountPath: "/var/lib/mysql",
type: "volume",
});
return true;
} catch (error) {
throw new TRPCError({
code: "BAD_REQUEST",
message: "Error input: Inserting mysql database",
cause: error,
});
}
}),
one: protectedProcedure
.input(apiFindOneMySql)
.query(async ({ input, ctx }) => {
if (ctx.user.rol === "user") {
await checkServiceAccess(ctx.user.authId, input.mysqlId, "access");
}
return await findMySqlById(input.mysqlId);
}),
start: protectedProcedure
.input(apiFindOneMySql)
.mutation(async ({ input }) => {
const service = await findMySqlById(input.mysqlId);
await startService(service.appName);
await updateMySqlById(input.mysqlId, {
applicationStatus: "done",
});
return service;
}),
stop: protectedProcedure
.input(apiFindOneMySql)
.mutation(async ({ input }) => {
const mongo = await findMySqlById(input.mysqlId);
await stopService(mongo.appName);
await updateMySqlById(input.mysqlId, {
applicationStatus: "idle",
});
return mongo;
}),
saveExternalPort: protectedProcedure
.input(apiSaveExternalPortMySql)
.mutation(async ({ input }) => {
const mongo = await findMySqlById(input.mysqlId);
await updateMySqlById(input.mysqlId, {
externalPort: input.externalPort,
});
await deployMySql(input.mysqlId);
return mongo;
}),
deploy: protectedProcedure
.input(apiDeployMySql)
.mutation(async ({ input }) => {
return deployMySql(input.mysqlId);
}),
changeStatus: protectedProcedure
.input(apiChangeMySqlStatus)
.mutation(async ({ input }) => {
const mongo = await findMySqlById(input.mysqlId);
await updateMySqlById(input.mysqlId, {
applicationStatus: input.applicationStatus,
});
return mongo;
}),
reload: protectedProcedure
.input(apiResetMysql)
.mutation(async ({ input }) => {
await stopService(input.appName);
await updateMySqlById(input.mysqlId, {
applicationStatus: "idle",
});
await startService(input.appName);
await updateMySqlById(input.mysqlId, {
applicationStatus: "done",
});
return true;
}),
remove: protectedProcedure
.input(apiFindOneMySql)
.mutation(async ({ input, ctx }) => {
if (ctx.user.rol === "user") {
await checkServiceAccess(ctx.user.authId, input.mysqlId, "delete");
}
const mongo = await findMySqlById(input.mysqlId);
const cleanupOperations = [
async () => await removeService(mongo?.appName),
async () => await removeMySqlById(input.mysqlId),
];
for (const operation of cleanupOperations) {
try {
await operation();
} catch (error) {}
}
return mongo;
}),
saveEnviroment: protectedProcedure
.input(apiSaveEnviromentVariablesMySql)
.mutation(async ({ input }) => {
const service = await updateMySqlById(input.mysqlId, {
env: input.env,
});
if (!service) {
throw new TRPCError({
code: "BAD_REQUEST",
message: "Update: Error to add enviroment variables",
});
}
return true;
}),
update: protectedProcedure
.input(apiUpdateMySql)
.mutation(async ({ input }) => {
const { mysqlId, ...rest } = input;
const service = await updateMySqlById(mysqlId, {
...rest,
});
if (!service) {
throw new TRPCError({
code: "BAD_REQUEST",
message: "Update: Error to update mysql",
});
}
return true;
}),
});

View File

@@ -0,0 +1,65 @@
import { TRPCError } from "@trpc/server";
import { createTRPCRouter, protectedProcedure } from "@/server/api/trpc";
import {
apiCreatePort,
apiFindOnePort,
apiUpdatePort,
} from "@/server/db/schema/port";
import {
createPort,
finPortById,
removePortById,
updatePortById,
} from "../services/port";
export const portRouter = createTRPCRouter({
create: protectedProcedure
.input(apiCreatePort)
.mutation(async ({ input }) => {
try {
await createPort(input);
return true;
} catch (error) {
throw new TRPCError({
code: "BAD_REQUEST",
message: "Error input: Inserting port",
cause: error,
});
}
}),
one: protectedProcedure.input(apiFindOnePort).query(async ({ input }) => {
try {
return await finPortById(input.portId);
} catch (error) {
throw new TRPCError({
code: "BAD_REQUEST",
message: "Port not found",
cause: error,
});
}
}),
delete: protectedProcedure
.input(apiFindOnePort)
.mutation(async ({ input }) => {
try {
return removePortById(input.portId);
} catch (error) {
throw new TRPCError({
code: "BAD_REQUEST",
message: "Error input: Deleting port",
});
}
}),
update: protectedProcedure
.input(apiUpdatePort)
.mutation(async ({ input }) => {
try {
return updatePortById(input.portId, input);
} catch (error) {
throw new TRPCError({
code: "BAD_REQUEST",
message: "Error to updating port",
});
}
}),
});

View File

@@ -0,0 +1,179 @@
import { createTRPCRouter, protectedProcedure } from "@/server/api/trpc";
import {
apiChangePostgresStatus,
apiCreatePostgres,
apiDeployPostgres,
apiFindOnePostgres,
apiResetPostgres,
apiSaveEnviromentVariablesPostgres,
apiSaveExternalPortPostgres,
apiUpdatePostgres,
} from "@/server/db/schema/postgres";
import {
removeService,
startService,
stopService,
} from "@/server/utils/docker/utils";
import { TRPCError } from "@trpc/server";
import {
createPostgres,
deployPostgres,
findPostgresById,
removePostgresById,
updatePostgresById,
} from "../services/postgres";
import { addNewService, checkServiceAccess } from "../services/user";
import { createMount } from "../services/mount";
export const postgresRouter = createTRPCRouter({
create: protectedProcedure
.input(apiCreatePostgres)
.mutation(async ({ input, ctx }) => {
try {
if (ctx.user.rol === "user") {
await checkServiceAccess(ctx.user.authId, input.projectId, "create");
}
const newPostgres = await createPostgres(input);
if (ctx.user.rol === "user") {
await addNewService(ctx.user.authId, newPostgres.postgresId);
}
await createMount({
serviceId: newPostgres.postgresId,
serviceType: "postgres",
volumeName: `${newPostgres.appName}-data`,
mountPath: "/var/lib/postgresql/data",
type: "volume",
});
return true;
} catch (error) {
throw new TRPCError({
code: "BAD_REQUEST",
message: "Error input: Inserting postgresql database",
cause: error,
});
}
}),
one: protectedProcedure
.input(apiFindOnePostgres)
.query(async ({ input, ctx }) => {
if (ctx.user.rol === "user") {
await checkServiceAccess(ctx.user.authId, input.postgresId, "access");
}
return await findPostgresById(input.postgresId);
}),
start: protectedProcedure
.input(apiFindOnePostgres)
.mutation(async ({ input }) => {
const service = await findPostgresById(input.postgresId);
await startService(service.appName);
await updatePostgresById(input.postgresId, {
applicationStatus: "done",
});
return service;
}),
stop: protectedProcedure
.input(apiFindOnePostgres)
.mutation(async ({ input }) => {
const postgres = await findPostgresById(input.postgresId);
await stopService(postgres.appName);
await updatePostgresById(input.postgresId, {
applicationStatus: "idle",
});
return postgres;
}),
saveExternalPort: protectedProcedure
.input(apiSaveExternalPortPostgres)
.mutation(async ({ input }) => {
const postgres = await findPostgresById(input.postgresId);
await updatePostgresById(input.postgresId, {
externalPort: input.externalPort,
});
await deployPostgres(input.postgresId);
return postgres;
}),
deploy: protectedProcedure
.input(apiDeployPostgres)
.mutation(async ({ input }) => {
return deployPostgres(input.postgresId);
}),
changeStatus: protectedProcedure
.input(apiChangePostgresStatus)
.mutation(async ({ input }) => {
const postgres = await findPostgresById(input.postgresId);
await updatePostgresById(input.postgresId, {
applicationStatus: input.applicationStatus,
});
return postgres;
}),
remove: protectedProcedure
.input(apiFindOnePostgres)
.mutation(async ({ input, ctx }) => {
if (ctx.user.rol === "user") {
await checkServiceAccess(ctx.user.authId, input.postgresId, "delete");
}
const postgres = await findPostgresById(input.postgresId);
const cleanupOperations = [
removeService(postgres.appName),
removePostgresById(input.postgresId),
];
await Promise.allSettled(cleanupOperations);
return postgres;
}),
saveEnviroment: protectedProcedure
.input(apiSaveEnviromentVariablesPostgres)
.mutation(async ({ input }) => {
const service = await updatePostgresById(input.postgresId, {
env: input.env,
});
if (!service) {
throw new TRPCError({
code: "BAD_REQUEST",
message: "Update: Error to add enviroment variables",
});
}
return true;
}),
reload: protectedProcedure
.input(apiResetPostgres)
.mutation(async ({ input }) => {
await stopService(input.appName);
await updatePostgresById(input.postgresId, {
applicationStatus: "idle",
});
await startService(input.appName);
await updatePostgresById(input.postgresId, {
applicationStatus: "done",
});
return true;
}),
update: protectedProcedure
.input(apiUpdatePostgres)
.mutation(async ({ input }) => {
const { postgresId, ...rest } = input;
const service = await updatePostgresById(postgresId, {
...rest,
});
if (!service) {
throw new TRPCError({
code: "BAD_REQUEST",
message: "Update: Error to update postgres",
});
}
return true;
}),
});

View File

@@ -0,0 +1,197 @@
import { createTRPCRouter, protectedProcedure } from "@/server/api/trpc";
import { db } from "@/server/db";
import {
apiCreateProject,
apiFindOneProject,
apiRemoveProject,
apiUpdateProject,
projects,
} from "@/server/db/schema/project";
import { TRPCError } from "@trpc/server";
import { desc, eq, sql } from "drizzle-orm";
import {
createProject,
deleteProject,
findProjectById,
updateProjectById,
} from "../services/project";
import {
addNewProject,
checkProjectAccess,
findUserByAuthId,
} from "../services/user";
import {
applications,
mariadb,
mongo,
mysql,
postgres,
redis,
} from "@/server/db/schema";
import type { AnyPgColumn } from "drizzle-orm/pg-core";
export const projectRouter = createTRPCRouter({
create: protectedProcedure
.input(apiCreateProject)
.mutation(async ({ ctx, input }) => {
try {
if (ctx.user.rol === "user") {
await checkProjectAccess(ctx.user.authId, "create");
}
const project = await createProject(input);
if (ctx.user.rol === "user") {
await addNewProject(ctx.user.authId, project.projectId);
}
} catch (error) {
console.log(error);
throw new TRPCError({
code: "BAD_REQUEST",
message: "Error to create the project",
cause: error,
});
}
}),
one: protectedProcedure
.input(apiFindOneProject)
.query(async ({ input, ctx }) => {
if (ctx.user.rol === "user") {
const { accesedServices } = await findUserByAuthId(ctx.user.authId);
await checkProjectAccess(ctx.user.authId, "access", input.projectId);
const service = await db.query.projects.findFirst({
where: eq(projects.projectId, input.projectId),
with: {
applications: {
where: buildServiceFilter(
applications.applicationId,
accesedServices,
),
},
mariadb: {
where: buildServiceFilter(mariadb.mariadbId, accesedServices),
},
mongo: {
where: buildServiceFilter(mongo.mongoId, accesedServices),
},
mysql: {
where: buildServiceFilter(mysql.mysqlId, accesedServices),
},
postgres: {
where: buildServiceFilter(postgres.postgresId, accesedServices),
},
redis: {
where: buildServiceFilter(redis.redisId, accesedServices),
},
},
});
if (!service) {
throw new TRPCError({
code: "NOT_FOUND",
message: "Project not found",
});
}
return service;
}
const project = await findProjectById(input.projectId);
return project;
}),
all: protectedProcedure.query(async ({ ctx }) => {
if (ctx.user.rol === "user") {
const { accesedProjects, accesedServices } = await findUserByAuthId(
ctx.user.authId,
);
if (accesedProjects.length === 0) {
return [];
}
const query = await db.query.projects.findMany({
where: sql`${projects.projectId} IN (${sql.join(
accesedProjects.map((projectId) => sql`${projectId}`),
sql`, `,
)})`,
with: {
applications: {
where: buildServiceFilter(
applications.applicationId,
accesedServices,
),
},
mariadb: {
where: buildServiceFilter(mariadb.mariadbId, accesedServices),
},
mongo: {
where: buildServiceFilter(mongo.mongoId, accesedServices),
},
mysql: {
where: buildServiceFilter(mysql.mysqlId, accesedServices),
},
postgres: {
where: buildServiceFilter(postgres.postgresId, accesedServices),
},
redis: {
where: buildServiceFilter(redis.redisId, accesedServices),
},
},
orderBy: desc(projects.createdAt),
});
return query;
}
return await db.query.projects.findMany({
with: {
applications: true,
mariadb: true,
mongo: true,
mysql: true,
postgres: true,
redis: true,
},
orderBy: desc(projects.createdAt),
});
}),
remove: protectedProcedure
.input(apiRemoveProject)
.mutation(async ({ input, ctx }) => {
try {
if (ctx.user.rol === "user") {
await checkProjectAccess(ctx.user.authId, "delete");
}
const project = await deleteProject(input.projectId);
return project;
} catch (error) {
throw new TRPCError({
code: "BAD_REQUEST",
message: "Error to delete this project",
cause: error,
});
}
}),
update: protectedProcedure
.input(apiUpdateProject)
.mutation(async ({ input }) => {
try {
const project = updateProjectById(input.projectId, {
...input,
});
return project;
} catch (error) {
throw new TRPCError({
code: "BAD_REQUEST",
message: "Error to update this project",
cause: error,
});
}
}),
});
function buildServiceFilter(fieldName: AnyPgColumn, accesedServices: string[]) {
return accesedServices.length > 0
? sql`${fieldName} IN (${sql.join(
accesedServices.map((serviceId) => sql`${serviceId}`),
sql`, `,
)})`
: sql`1 = 0`; // Always false condition
}

View File

@@ -0,0 +1,33 @@
import { createTRPCRouter, protectedProcedure } from "../trpc";
import {
apiCreateRedirect,
apiFindOneRedirect,
apiUpdateRedirect,
} from "@/server/db/schema";
import {
createRedirect,
findRedirectById,
removeRedirectById,
updateRedirectById,
} from "../services/redirect";
export const redirectsRouter = createTRPCRouter({
create: protectedProcedure
.input(apiCreateRedirect)
.mutation(async ({ input }) => {
return await createRedirect(input);
}),
one: protectedProcedure.input(apiFindOneRedirect).query(async ({ input }) => {
return findRedirectById(input.redirectId);
}),
delete: protectedProcedure
.input(apiFindOneRedirect)
.mutation(async ({ input }) => {
return removeRedirectById(input.redirectId);
}),
update: protectedProcedure
.input(apiUpdateRedirect)
.mutation(async ({ input }) => {
return updateRedirectById(input.redirectId, input);
}),
});

183
server/api/routers/redis.ts Normal file
View File

@@ -0,0 +1,183 @@
import { createTRPCRouter, protectedProcedure } from "@/server/api/trpc";
import {
apiChangeRedisStatus,
apiCreateRedis,
apiDeployRedis,
apiFindOneRedis,
apiResetRedis,
apiSaveEnviromentVariablesRedis,
apiSaveExternalPortRedis,
apiUpdateRedis,
} from "@/server/db/schema/redis";
import {
removeService,
startService,
stopService,
} from "@/server/utils/docker/utils";
import { TRPCError } from "@trpc/server";
import {
createRedis,
deployRedis,
findRedisById,
removeRedisById,
updateRedisById,
} from "../services/redis";
import { addNewService, checkServiceAccess } from "../services/user";
import { createMount } from "../services/mount";
export const redisRouter = createTRPCRouter({
create: protectedProcedure
.input(apiCreateRedis)
.mutation(async ({ input, ctx }) => {
try {
if (ctx.user.rol === "user") {
await checkServiceAccess(ctx.user.authId, input.projectId, "create");
}
const newRedis = await createRedis(input);
if (ctx.user.rol === "user") {
await addNewService(ctx.user.authId, newRedis.redisId);
}
await createMount({
serviceId: newRedis.redisId,
serviceType: "redis",
volumeName: `${newRedis.appName}-data`,
mountPath: "/data",
type: "volume",
});
return true;
} catch (error) {
throw new TRPCError({
code: "BAD_REQUEST",
message: "Error input: Inserting redis database",
cause: error,
});
}
}),
one: protectedProcedure
.input(apiFindOneRedis)
.query(async ({ input, ctx }) => {
if (ctx.user.rol === "user") {
await checkServiceAccess(ctx.user.authId, input.redisId, "access");
}
return await findRedisById(input.redisId);
}),
start: protectedProcedure
.input(apiFindOneRedis)
.mutation(async ({ input }) => {
const redis = await findRedisById(input.redisId);
await startService(redis.appName);
await updateRedisById(input.redisId, {
applicationStatus: "done",
});
return redis;
}),
reload: protectedProcedure
.input(apiResetRedis)
.mutation(async ({ input }) => {
await stopService(input.appName);
await updateRedisById(input.redisId, {
applicationStatus: "idle",
});
await startService(input.appName);
await updateRedisById(input.redisId, {
applicationStatus: "done",
});
return true;
}),
stop: protectedProcedure
.input(apiFindOneRedis)
.mutation(async ({ input }) => {
const mongo = await findRedisById(input.redisId);
await stopService(mongo.appName);
await updateRedisById(input.redisId, {
applicationStatus: "idle",
});
return mongo;
}),
saveExternalPort: protectedProcedure
.input(apiSaveExternalPortRedis)
.mutation(async ({ input }) => {
const mongo = await findRedisById(input.redisId);
await updateRedisById(input.redisId, {
externalPort: input.externalPort,
});
await deployRedis(input.redisId);
return mongo;
}),
deploy: protectedProcedure
.input(apiDeployRedis)
.mutation(async ({ input }) => {
return deployRedis(input.redisId);
}),
changeStatus: protectedProcedure
.input(apiChangeRedisStatus)
.mutation(async ({ input }) => {
const mongo = await findRedisById(input.redisId);
await updateRedisById(input.redisId, {
applicationStatus: input.applicationStatus,
});
return mongo;
}),
remove: protectedProcedure
.input(apiFindOneRedis)
.mutation(async ({ input, ctx }) => {
if (ctx.user.rol === "user") {
await checkServiceAccess(ctx.user.authId, input.redisId, "delete");
}
const redis = await findRedisById(input.redisId);
const cleanupOperations = [
async () => await removeService(redis?.appName),
async () => await removeRedisById(input.redisId),
];
for (const operation of cleanupOperations) {
try {
await operation();
} catch (error) {}
}
return redis;
}),
saveEnviroment: protectedProcedure
.input(apiSaveEnviromentVariablesRedis)
.mutation(async ({ input }) => {
const redis = await updateRedisById(input.redisId, {
env: input.env,
});
if (!redis) {
throw new TRPCError({
code: "BAD_REQUEST",
message: "Update: Error to add enviroment variables",
});
}
return true;
}),
update: protectedProcedure
.input(apiUpdateRedis)
.mutation(async ({ input }) => {
const { redisId, ...rest } = input;
const redis = await updateRedisById(redisId, {
...rest,
});
if (!redis) {
throw new TRPCError({
code: "BAD_REQUEST",
message: "Update: Error to update redis",
});
}
return true;
}),
});

View File

@@ -0,0 +1,33 @@
import { createTRPCRouter, protectedProcedure } from "../trpc";
import {
apiCreateSecurity,
apiFindOneSecurity,
apiUpdateSecurity,
} from "@/server/db/schema";
import {
createSecurity,
deleteSecurityById,
findSecurityById,
updateSecurityById,
} from "../services/security";
export const securityRouter = createTRPCRouter({
create: protectedProcedure
.input(apiCreateSecurity)
.mutation(async ({ input }) => {
return await createSecurity(input);
}),
one: protectedProcedure.input(apiFindOneSecurity).query(async ({ input }) => {
return await findSecurityById(input.securityId);
}),
delete: protectedProcedure
.input(apiFindOneSecurity)
.mutation(async ({ input }) => {
return await deleteSecurityById(input.securityId);
}),
update: protectedProcedure
.input(apiUpdateSecurity)
.mutation(async ({ input }) => {
return await updateSecurityById(input.securityId, input);
}),
});

View File

@@ -0,0 +1,240 @@
import { docker, MAIN_TRAEFIK_PATH } from "@/server/constants";
import { adminProcedure, createTRPCRouter, protectedProcedure } from "../trpc";
import {
cleanStoppedContainers,
cleanUpDockerBuilder,
cleanUpSystemPrune,
cleanUpUnusedImages,
cleanUpUnusedVolumes,
startService,
stopService,
} from "@/server/utils/docker/utils";
import {
apiAssignDomain,
apiModifyTraefikConfig,
apiReadTraefikConfig,
apiSaveSSHKey,
apiTraefikConfig,
apiUpdateDockerCleanup,
} from "@/server/db/schema";
import { scheduledJobs, scheduleJob } from "node-schedule";
import {
readMainConfig,
updateLetsEncryptEmail,
updateServerTraefik,
writeMainConfig,
} from "@/server/utils/traefik/web-server";
import {
readConfig,
readConfigInPath,
writeConfig,
writeTraefikConfigInPath,
} from "@/server/utils/traefik/application";
import { spawnAsync } from "@/server/utils/process/spawnAsync";
import { findAdmin, updateAdmin } from "../services/admin";
import { TRPCError } from "@trpc/server";
import {
getDokployVersion,
getDokployImage,
pullLatestRelease,
readDirectory,
} from "../services/settings";
import { canAccessToTraefikFiles } from "../services/user";
export const settingsRouter = createTRPCRouter({
reloadServer: adminProcedure.mutation(async () => {
await spawnAsync("docker", [
"service",
"update",
"--force",
"--image",
getDokployImage(),
"dokploy",
]);
return true;
}),
reloadTraefik: adminProcedure.mutation(async () => {
await stopService("dokploy-traefik");
await startService("dokploy-traefik");
return true;
}),
cleanUnusedImages: adminProcedure.mutation(async () => {
await cleanUpUnusedImages();
return true;
}),
cleanUnusedVolumes: adminProcedure.mutation(async () => {
await cleanUpUnusedVolumes();
return true;
}),
cleanStoppedContainers: adminProcedure.mutation(async () => {
await cleanStoppedContainers();
return true;
}),
cleanDockerBuilder: adminProcedure.mutation(async () => {
await cleanUpDockerBuilder();
}),
cleanDockerPrune: adminProcedure.mutation(async () => {
await cleanUpSystemPrune();
await cleanUpDockerBuilder();
return true;
}),
cleanAll: adminProcedure.mutation(async () => {
await cleanUpUnusedImages();
await cleanUpDockerBuilder();
await cleanUpSystemPrune();
return true;
}),
saveSSHPrivateKey: adminProcedure
.input(apiSaveSSHKey)
.mutation(async ({ input, ctx }) => {
await updateAdmin(ctx.user.authId, {
sshPrivateKey: input.sshPrivateKey,
});
return true;
}),
assignDomainServer: adminProcedure
.input(apiAssignDomain)
.mutation(async ({ ctx, input }) => {
const admin = await updateAdmin(ctx.user.authId, {
host: input.host,
letsEncryptEmail: input.letsEncryptEmail,
certificateType: input.certificateType,
});
if (!admin) {
throw new TRPCError({
code: "NOT_FOUND",
message: "Admin not found",
});
}
updateServerTraefik(admin, input.host);
updateLetsEncryptEmail(admin.letsEncryptEmail);
return admin;
}),
cleanSSHPrivateKey: adminProcedure.mutation(async ({ ctx }) => {
await updateAdmin(ctx.user.authId, {
sshPrivateKey: null,
});
return true;
}),
updateDockerCleanup: adminProcedure
.input(apiUpdateDockerCleanup)
.mutation(async ({ input, ctx }) => {
await updateAdmin(ctx.user.authId, {
enableDockerCleanup: input.enableDockerCleanup,
});
const admin = await findAdmin();
if (admin.enableDockerCleanup) {
scheduleJob("docker-cleanup", "0 0 * * *", async () => {
console.log(
`Docker Cleanup ${new Date().toLocaleString()}] Running...`,
);
await cleanUpUnusedImages();
await cleanUpDockerBuilder();
await cleanUpSystemPrune();
});
} else {
const currentJob = scheduledJobs["docker-cleanup"];
currentJob?.cancel();
}
return true;
}),
readTraefikConfig: adminProcedure.query(() => {
const traefikConfig = readMainConfig();
return traefikConfig;
}),
updateTraefikConfig: adminProcedure
.input(apiTraefikConfig)
.mutation(async ({ input }) => {
writeMainConfig(input.traefikConfig);
return true;
}),
readWebServerTraefikConfig: adminProcedure.query(() => {
const traefikConfig = readConfig("dokploy");
return traefikConfig;
}),
updateWebServerTraefikConfig: adminProcedure
.input(apiTraefikConfig)
.mutation(async ({ input }) => {
writeConfig("dokploy", input.traefikConfig);
return true;
}),
readMiddlewareTraefikConfig: adminProcedure.query(() => {
const traefikConfig = readConfig("middlewares");
return traefikConfig;
}),
updateMiddlewareTraefikConfig: adminProcedure
.input(apiTraefikConfig)
.mutation(async ({ input }) => {
writeConfig("middlewares", input.traefikConfig);
return true;
}),
checkAndUpdateImage: adminProcedure.query(async () => {
return await pullLatestRelease();
}),
updateServer: adminProcedure.mutation(async () => {
await spawnAsync("docker", [
"service",
"update",
"--force",
"--image",
getDokployImage(),
"dokploy",
]);
return true;
}),
getDokployVersion: adminProcedure.query(() => {
return getDokployVersion();
}),
readDirectories: protectedProcedure.query(async ({ ctx }) => {
if (ctx.user.rol === "user") {
const canAccess = await canAccessToTraefikFiles(ctx.user.authId);
if (!canAccess) {
throw new TRPCError({ code: "UNAUTHORIZED" });
}
}
const result = readDirectory(MAIN_TRAEFIK_PATH);
return result || [];
}),
updateTraefikFile: protectedProcedure
.input(apiModifyTraefikConfig)
.mutation(async ({ input, ctx }) => {
if (ctx.user.rol === "user") {
const canAccess = await canAccessToTraefikFiles(ctx.user.authId);
if (!canAccess) {
throw new TRPCError({ code: "UNAUTHORIZED" });
}
}
writeTraefikConfigInPath(input.path, input.traefikConfig);
return true;
}),
readTraefikFile: protectedProcedure
.input(apiReadTraefikConfig)
.query(async ({ input, ctx }) => {
if (ctx.user.rol === "user") {
const canAccess = await canAccessToTraefikFiles(ctx.user.authId);
if (!canAccess) {
throw new TRPCError({ code: "UNAUTHORIZED" });
}
}
return readConfigInPath(input.path);
}),
});

View File

@@ -0,0 +1,20 @@
import { apiFindOneUser, apiFindOneUserByAuth } from "@/server/db/schema";
import { adminProcedure, createTRPCRouter, protectedProcedure } from "../trpc";
import { findUserByAuthId, findUserById, findUsers } from "../services/user";
export const userRouter = createTRPCRouter({
all: adminProcedure.query(async () => {
return await findUsers();
}),
byAuthId: protectedProcedure
.input(apiFindOneUserByAuth)
.query(async ({ input }) => {
return await findUserByAuthId(input.authId);
}),
byUserId: protectedProcedure
.input(apiFindOneUser)
.query(async ({ input }) => {
return await findUserById(input.userId);
}),
});

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

162
server/api/trpc.ts Normal file
View File

@@ -0,0 +1,162 @@
/**
* YOU PROBABLY DON'T NEED TO EDIT THIS FILE, UNLESS:
* 1. You want to modify request context (see Part 1).
* 2. You want to create a new middleware or type of procedure (see Part 3).
*
* TL;DR - This is where all the tRPC server stuff is created and plugged in. The pieces you will
* need to use are documented accordingly near the end.
*/
// import { getServerAuthSession } from "@/server/auth";
import { db } from "@/server/db";
import { TRPCError, initTRPC } from "@trpc/server";
import type { CreateNextContextOptions } from "@trpc/server/adapters/next";
import superjson from "superjson";
import { ZodError } from "zod";
import { validateRequest } from "../auth/auth";
import type { Session, User } from "lucia";
/**
* 1. CONTEXT
*
* This section defines the "contexts" that are available in the backend API.
*
* These allow you to access things when processing a request, like the database, the session, etc.
*/
interface CreateContextOptions {
user: (User & { authId: string }) | null;
session: Session | null;
req: CreateNextContextOptions["req"];
res: CreateNextContextOptions["res"];
}
/**
* This helper generates the "internals" for a tRPC context. If you need to use it, you can export
* it from here.
*
* Examples of things you may need it for:
* - testing, so we don't have to mock Next.js' req/res
* - tRPC's `createSSGHelpers`, where we don't have req/res
*
* @see https://create.t3.gg/en/usage/trpc#-serverapitrpcts
*/
const createInnerTRPCContext = (opts: CreateContextOptions) => {
return {
session: opts.session,
db,
req: opts.req,
res: opts.res,
user: opts.user,
};
};
/**
* This is the actual context you will use in your router. It will be used to process every request
* that goes through your tRPC endpoint.
*
* @see https://trpc.io/docs/context
*/
export const createTRPCContext = async (opts: CreateNextContextOptions) => {
const { req, res } = opts;
// const sessionId = lucia.readSessionCookie(req.headers.cookie ?? "");
const { session, user } = await validateRequest(req, res);
user;
return createInnerTRPCContext({
req,
res,
session: session,
...((user && {
user: {
authId: user.id,
email: user.email,
rol: user.rol,
id: user.id,
secret: user.secret,
},
}) || {
user: null,
}),
});
};
/**
* 2. INITIALIZATION
*
* This is where the tRPC API is initialized, connecting the context and transformer. We also parse
* ZodErrors so that you get typesafety on the frontend if your procedure fails due to validation
* errors on the backend.
*/
const t = initTRPC.context<typeof createTRPCContext>().create({
transformer: superjson,
errorFormatter({ shape, error }) {
return {
...shape,
data: {
...shape.data,
zodError:
error.cause instanceof ZodError ? error.cause.flatten() : null,
},
};
},
});
/**
* 3. ROUTER & PROCEDURE (THE IMPORTANT BIT)
*
* These are the pieces you use to build your tRPC API. You should import these a lot in the
* "/src/server/api/routers" directory.
*/
/**
* This is how you create new routers and sub-routers in your tRPC API.
*
* @see https://trpc.io/docs/router
*/
export const createTRPCRouter = t.router;
/**
* Public (unauthenticated) procedure
*
* This is the base piece you use to build new queries and mutations on your tRPC API. It does not
* guarantee that a user querying is authorized, but you can still access user session data if they
* are logged in.
*/
export const publicProcedure = t.procedure;
/**
* Protected (authenticated) procedure
*
* If you want a query or mutation to ONLY be accessible to logged in users, use this. It verifies
* the session is valid and guarantees `ctx.session.user` is not null.
*
* @see https://trpc.io/docs/procedures
*/
export const protectedProcedure = t.procedure.use(({ ctx, next }) => {
if (!ctx.session || !ctx.user) {
throw new TRPCError({ code: "UNAUTHORIZED" });
}
return next({
ctx: {
// infers the `session` as non-nullable
session: ctx.session,
user: ctx.user,
// session: { ...ctx.session, user: ctx.user },
},
});
});
export const adminProcedure = t.procedure.use(({ ctx, next }) => {
if (!ctx.session || !ctx.user || ctx.user.rol !== "admin") {
throw new TRPCError({ code: "UNAUTHORIZED" });
}
return next({
ctx: {
// infers the `session` as non-nullable
session: ctx.session,
user: ctx.user,
// session: { ...ctx.session, user: ctx.user },
},
});
});