import { checkPortInUse, createMariadb, createMount, deployMariadb, execAsync, execAsyncRemote, findBackupsByDbId, findEnvironmentById, findMariadbById, findProjectById, getServiceContainerCommand, IS_CLOUD, rebuildDatabase, removeMariadbById, removeService, startService, startServiceRemote, stopService, stopServiceRemote, updateMariadbById, getAccessibleServerIds, } from "@dokploy/server"; import { db } from "@dokploy/server/db"; import { addNewService, checkServiceAccess, checkServicePermissionAndAccess, findMemberByUserId, } from "@dokploy/server/services/permission"; import { TRPCError } from "@trpc/server"; import { observable } from "@trpc/server/observable"; import { and, desc, eq, ilike, or, sql } from "drizzle-orm"; import { z } from "zod"; import { createTRPCRouter, protectedProcedure } from "@/server/api/trpc"; import { audit } from "@/server/api/utils/audit"; import { apiChangeMariaDBStatus, apiCreateMariaDB, apiDeployMariaDB, apiFindOneMariaDB, apiRebuildMariadb, apiResetMariadb, apiSaveEnvironmentVariablesMariaDB, apiSaveExternalPortMariaDB, apiUpdateMariaDB, DATABASE_PASSWORD_MESSAGE, DATABASE_PASSWORD_REGEX, environments, mariadb as mariadbTable, projects, } from "@/server/db/schema"; import { cancelJobs } from "@/server/utils/backup"; export const mariadbRouter = createTRPCRouter({ create: protectedProcedure .input(apiCreateMariaDB) .mutation(async ({ input, ctx }) => { try { const environment = await findEnvironmentById(input.environmentId); const project = await findProjectById(environment.projectId); await checkServiceAccess(ctx, project.projectId, "create"); if (IS_CLOUD && !input.serverId) { throw new TRPCError({ code: "UNAUTHORIZED", message: "You need to use a server to create a Mariadb", }); } if (project.organizationId !== ctx.session.activeOrganizationId) { throw new TRPCError({ code: "UNAUTHORIZED", message: "You are not authorized to access this project", }); } if (input.serverId) { const accessibleIds = await getAccessibleServerIds(ctx.session); if (!accessibleIds.has(input.serverId)) { throw new TRPCError({ code: "UNAUTHORIZED", message: "You are not authorized to access this server", }); } } const newMariadb = await createMariadb({ ...input, }); await addNewService(ctx, newMariadb.mariadbId); await createMount({ serviceId: newMariadb.mariadbId, serviceType: "mariadb", volumeName: `${newMariadb.appName}-data`, mountPath: "/var/lib/mysql", type: "volume", }); await audit(ctx, { action: "create", resourceType: "service", resourceId: newMariadb.mariadbId, resourceName: newMariadb.appName, }); return newMariadb; } catch (error) { if (error instanceof TRPCError) { throw error; } throw error; } }), one: protectedProcedure .input(apiFindOneMariaDB) .query(async ({ input, ctx }) => { await checkServiceAccess(ctx, input.mariadbId, "read"); const mariadb = await findMariadbById(input.mariadbId); if ( mariadb.environment.project.organizationId !== ctx.session.activeOrganizationId ) { throw new TRPCError({ code: "UNAUTHORIZED", message: "You are not authorized to access this Mariadb", }); } return mariadb; }), start: protectedProcedure .input(apiFindOneMariaDB) .mutation(async ({ input, ctx }) => { await checkServicePermissionAndAccess(ctx, input.mariadbId, { deployment: ["create"], }); const service = await findMariadbById(input.mariadbId); if (service.serverId) { await startServiceRemote(service.serverId, service.appName); } else { await startService(service.appName); } await updateMariadbById(input.mariadbId, { applicationStatus: "done", }); await audit(ctx, { action: "start", resourceType: "service", resourceId: service.mariadbId, resourceName: service.appName, }); return service; }), stop: protectedProcedure .input(apiFindOneMariaDB) .mutation(async ({ input, ctx }) => { await checkServicePermissionAndAccess(ctx, input.mariadbId, { deployment: ["create"], }); const mariadb = await findMariadbById(input.mariadbId); if (mariadb.serverId) { await stopServiceRemote(mariadb.serverId, mariadb.appName); } else { await stopService(mariadb.appName); } await updateMariadbById(input.mariadbId, { applicationStatus: "idle", }); await audit(ctx, { action: "stop", resourceType: "service", resourceId: mariadb.mariadbId, resourceName: mariadb.appName, }); return mariadb; }), saveExternalPort: protectedProcedure .input(apiSaveExternalPortMariaDB) .mutation(async ({ input, ctx }) => { await checkServicePermissionAndAccess(ctx, input.mariadbId, { service: ["create"], }); const mariadb = await findMariadbById(input.mariadbId); if (input.externalPort) { const portCheck = await checkPortInUse( input.externalPort, mariadb.serverId || undefined, ); if (portCheck.isInUse) { throw new TRPCError({ code: "CONFLICT", message: `Port ${input.externalPort} is already in use by ${portCheck.conflictingContainer}`, }); } } await updateMariadbById(input.mariadbId, { externalPort: input.externalPort, }); await deployMariadb(input.mariadbId); await audit(ctx, { action: "update", resourceType: "service", resourceId: mariadb.mariadbId, resourceName: mariadb.appName, }); return mariadb; }), deploy: protectedProcedure .input(apiDeployMariaDB) .mutation(async ({ input, ctx }) => { await checkServicePermissionAndAccess(ctx, input.mariadbId, { deployment: ["create"], }); const mariadb = await findMariadbById(input.mariadbId); await audit(ctx, { action: "deploy", resourceType: "service", resourceId: mariadb.mariadbId, resourceName: mariadb.appName, }); return deployMariadb(input.mariadbId); }), deployWithLogs: protectedProcedure .meta({ openapi: { path: "/deploy/mariadb-with-logs", method: "POST", override: true, enabled: false, }, }) .input(apiDeployMariaDB) .subscription(async ({ input, ctx }) => { await checkServicePermissionAndAccess(ctx, input.mariadbId, { deployment: ["create"], }); return observable((emit) => { deployMariadb(input.mariadbId, (log) => { emit.next(log); }); }); }), changeStatus: protectedProcedure .input(apiChangeMariaDBStatus) .mutation(async ({ input, ctx }) => { await checkServicePermissionAndAccess(ctx, input.mariadbId, { deployment: ["create"], }); const mongo = await findMariadbById(input.mariadbId); await updateMariadbById(input.mariadbId, { applicationStatus: input.applicationStatus, }); await audit(ctx, { action: "update", resourceType: "service", resourceId: mongo.mariadbId, resourceName: mongo.appName, }); return mongo; }), remove: protectedProcedure .input(apiFindOneMariaDB) .mutation(async ({ input, ctx }) => { await checkServiceAccess(ctx, input.mariadbId, "delete"); const mongo = await findMariadbById(input.mariadbId); if ( mongo.environment.project.organizationId !== ctx.session.activeOrganizationId ) { throw new TRPCError({ code: "UNAUTHORIZED", message: "You are not authorized to delete this Mariadb", }); } await audit(ctx, { action: "delete", resourceType: "service", resourceId: mongo.mariadbId, resourceName: mongo.appName, }); const backups = await findBackupsByDbId(input.mariadbId, "mariadb"); const cleanupOperations = [ async () => await removeService(mongo?.appName, mongo.serverId), async () => await cancelJobs(backups), async () => await removeMariadbById(input.mariadbId), ]; for (const operation of cleanupOperations) { try { await operation(); } catch (_) {} } return mongo; }), saveEnvironment: protectedProcedure .input(apiSaveEnvironmentVariablesMariaDB) .mutation(async ({ input, ctx }) => { await checkServicePermissionAndAccess(ctx, input.mariadbId, { envVars: ["write"], }); const service = await updateMariadbById(input.mariadbId, { env: input.env, }); if (!service) { throw new TRPCError({ code: "BAD_REQUEST", message: "Error adding environment variables", }); } await audit(ctx, { action: "update", resourceType: "service", resourceId: input.mariadbId, }); return true; }), reload: protectedProcedure .input(apiResetMariadb) .mutation(async ({ input, ctx }) => { await checkServicePermissionAndAccess(ctx, input.mariadbId, { deployment: ["create"], }); const mariadb = await findMariadbById(input.mariadbId); if (mariadb.serverId) { await stopServiceRemote(mariadb.serverId, mariadb.appName); } else { await stopService(mariadb.appName); } await updateMariadbById(input.mariadbId, { applicationStatus: "idle", }); if (mariadb.serverId) { await startServiceRemote(mariadb.serverId, mariadb.appName); } else { await startService(mariadb.appName); } await updateMariadbById(input.mariadbId, { applicationStatus: "done", }); await audit(ctx, { action: "reload", resourceType: "service", resourceId: mariadb.mariadbId, resourceName: mariadb.appName, }); return true; }), update: protectedProcedure .input(apiUpdateMariaDB) .mutation(async ({ input, ctx }) => { const { mariadbId, ...rest } = input; await checkServicePermissionAndAccess(ctx, mariadbId, { service: ["create"], }); const service = await updateMariadbById(mariadbId, { ...rest, }); if (!service) { throw new TRPCError({ code: "BAD_REQUEST", message: "Update: Error updating Mariadb", }); } await audit(ctx, { action: "update", resourceType: "service", resourceId: mariadbId, resourceName: service.appName, }); return true; }), changePassword: protectedProcedure .input( z.object({ mariadbId: z.string().min(1), password: z.string().min(1).regex(DATABASE_PASSWORD_REGEX, { message: DATABASE_PASSWORD_MESSAGE, }), type: z.enum(["user", "root"]).default("user"), }), ) .mutation(async ({ input, ctx }) => { const { mariadbId, password, type } = input; await checkServicePermissionAndAccess(ctx, mariadbId, { service: ["create"], }); const maria = await findMariadbById(mariadbId); const { appName, serverId, databaseUser, databaseRootPassword } = maria; const containerCmd = getServiceContainerCommand(appName); const targetUser = type === "root" ? "root" : databaseUser; const command = ` CONTAINER_ID=$(${containerCmd}) if [ -z "$CONTAINER_ID" ]; then echo "No running container found for ${appName}" >&2 exit 1 fi docker exec "$CONTAINER_ID" mariadb -u root -p'${databaseRootPassword}' -e "ALTER USER '${targetUser}'@'%' IDENTIFIED BY '${password}'; FLUSH PRIVILEGES;" `; await db.transaction(async (tx) => { const setData = type === "root" ? { databaseRootPassword: password } : { databasePassword: password }; await tx .update(mariadbTable) .set(setData) .where(eq(mariadbTable.mariadbId, mariadbId)); if (serverId) { await execAsyncRemote(serverId, command); } else { await execAsync(command, { shell: "/bin/bash" }); } }); await audit(ctx, { action: "update", resourceType: "service", resourceId: mariadbId, resourceName: appName, }); return true; }), move: protectedProcedure .input( z.object({ mariadbId: z.string(), targetEnvironmentId: z.string(), }), ) .mutation(async ({ input, ctx }) => { await checkServicePermissionAndAccess(ctx, input.mariadbId, { service: ["create"], }); const updatedMariadb = await db .update(mariadbTable) .set({ environmentId: input.targetEnvironmentId, }) .where(eq(mariadbTable.mariadbId, input.mariadbId)) .returning() .then((res) => res[0]); if (!updatedMariadb) { throw new TRPCError({ code: "INTERNAL_SERVER_ERROR", message: "Failed to move mariadb", }); } await audit(ctx, { action: "move", resourceType: "service", resourceId: updatedMariadb.mariadbId, resourceName: updatedMariadb.appName, }); return updatedMariadb; }), rebuild: protectedProcedure .input(apiRebuildMariadb) .mutation(async ({ input, ctx }) => { await checkServicePermissionAndAccess(ctx, input.mariadbId, { deployment: ["create"], }); await rebuildDatabase(input.mariadbId, "mariadb"); await audit(ctx, { action: "rebuild", resourceType: "service", resourceId: input.mariadbId, }); return true; }), search: protectedProcedure .input( z.object({ q: z.string().optional(), name: z.string().optional(), appName: z.string().optional(), description: z.string().optional(), projectId: z.string().optional(), environmentId: z.string().optional(), limit: z.number().min(1).max(100).default(20), offset: z.number().min(0).default(0), }), ) .query(async ({ ctx, input }) => { const baseConditions = [ eq(projects.organizationId, ctx.session.activeOrganizationId), ]; if (input.projectId) { baseConditions.push(eq(environments.projectId, input.projectId)); } if (input.environmentId) { baseConditions.push( eq(mariadbTable.environmentId, input.environmentId), ); } if (input.q?.trim()) { const term = `%${input.q.trim()}%`; baseConditions.push( or( ilike(mariadbTable.name, term), ilike(mariadbTable.appName, term), ilike(mariadbTable.description ?? "", term), )!, ); } if (input.name?.trim()) { baseConditions.push(ilike(mariadbTable.name, `%${input.name.trim()}%`)); } if (input.appName?.trim()) { baseConditions.push( ilike(mariadbTable.appName, `%${input.appName.trim()}%`), ); } if (input.description?.trim()) { baseConditions.push( ilike( mariadbTable.description ?? "", `%${input.description.trim()}%`, ), ); } const { accessedServices } = await findMemberByUserId( ctx.user.id, ctx.session.activeOrganizationId, ); if (accessedServices.length === 0) return { items: [], total: 0 }; baseConditions.push( sql`${mariadbTable.mariadbId} IN (${sql.join( accessedServices.map((id) => sql`${id}`), sql`, `, )})`, ); const where = and(...baseConditions); const [items, countResult] = await Promise.all([ db .select({ mariadbId: mariadbTable.mariadbId, name: mariadbTable.name, appName: mariadbTable.appName, description: mariadbTable.description, environmentId: mariadbTable.environmentId, applicationStatus: mariadbTable.applicationStatus, createdAt: mariadbTable.createdAt, }) .from(mariadbTable) .innerJoin( environments, eq(mariadbTable.environmentId, environments.environmentId), ) .innerJoin(projects, eq(environments.projectId, projects.projectId)) .where(where) .orderBy(desc(mariadbTable.createdAt)) .limit(input.limit) .offset(input.offset), db .select({ count: sql`count(*)::int` }) .from(mariadbTable) .innerJoin( environments, eq(mariadbTable.environmentId, environments.environmentId), ) .innerJoin(projects, eq(environments.projectId, projects.projectId)) .where(where), ]); return { items, total: countResult[0]?.count ?? 0 }; }), });