Files
dokploy/apps/dokploy/server/api/routers/project.ts
T
Mauricio Siu 705c5bc1c9 feat: add search functionality across multiple routers with member access control
Implemented a search feature in application, compose, environment, mariadb, mongo, mysql, postgres, project, and redis routers. Each search allows filtering by various parameters and respects user permissions based on their role. The search queries utilize optimized conditions for efficient data retrieval.
2026-03-01 01:14:46 -06:00

815 lines
20 KiB
TypeScript

import {
addNewEnvironment,
addNewProject,
checkProjectAccess,
createApplication,
createBackup,
createCompose,
createDomain,
createMariadb,
createMongo,
createMount,
createMysql,
createPort,
createPostgres,
createPreviewDeployment,
createProject,
createRedirect,
createRedis,
createSecurity,
deleteProject,
findApplicationById,
findComposeById,
findEnvironmentById,
findMariadbById,
findMemberById,
findMongoById,
findMySqlById,
findPostgresById,
findProjectById,
findRedisById,
findUserById,
IS_CLOUD,
updateProjectById,
} from "@dokploy/server";
import { db } from "@dokploy/server/db";
import { TRPCError } from "@trpc/server";
import { and, desc, eq, ilike, or, sql } from "drizzle-orm";
import type { AnyPgColumn } from "drizzle-orm/pg-core";
import { z } from "zod";
import { createTRPCRouter, protectedProcedure } from "@/server/api/trpc";
import {
apiCreateProject,
apiFindOneProject,
apiRemoveProject,
apiUpdateProject,
applications,
compose,
environments,
mariadb,
mongo,
mysql,
postgres,
projects,
redis,
} from "@/server/db/schema";
export const projectRouter = createTRPCRouter({
create: protectedProcedure
.input(apiCreateProject)
.mutation(async ({ ctx, input }) => {
try {
if (ctx.user.role === "member") {
await checkProjectAccess(
ctx.user.id,
"create",
ctx.session.activeOrganizationId,
);
}
const admin = await findUserById(ctx.user.ownerId);
if (admin.serversQuantity === 0 && IS_CLOUD) {
throw new TRPCError({
code: "NOT_FOUND",
message: "No servers available, Please subscribe to a plan",
});
}
const project = await createProject(
input,
ctx.session.activeOrganizationId,
);
if (ctx.user.role === "member") {
await addNewProject(
ctx.user.id,
project.project.projectId,
ctx.session.activeOrganizationId,
);
await addNewEnvironment(
ctx.user.id,
project?.environment?.environmentId || "",
ctx.session.activeOrganizationId,
);
}
return project;
} catch (error) {
throw new TRPCError({
code: "BAD_REQUEST",
message: `Error creating the project: ${error instanceof Error ? error.message : error}`,
cause: error,
});
}
}),
one: protectedProcedure
.input(apiFindOneProject)
.query(async ({ input, ctx }) => {
if (ctx.user.role === "member") {
const { accessedServices } = await findMemberById(
ctx.user.id,
ctx.session.activeOrganizationId,
);
await checkProjectAccess(
ctx.user.id,
"access",
ctx.session.activeOrganizationId,
input.projectId,
);
const project = await db.query.projects.findFirst({
where: and(
eq(projects.projectId, input.projectId),
eq(projects.organizationId, ctx.session.activeOrganizationId),
),
with: {
environments: {
with: {
applications: {
where: buildServiceFilter(
applications.applicationId,
accessedServices,
),
},
compose: {
where: buildServiceFilter(
compose.composeId,
accessedServices,
),
},
mariadb: {
where: buildServiceFilter(
mariadb.mariadbId,
accessedServices,
),
},
mongo: {
where: buildServiceFilter(mongo.mongoId, accessedServices),
},
mysql: {
where: buildServiceFilter(mysql.mysqlId, accessedServices),
},
postgres: {
where: buildServiceFilter(
postgres.postgresId,
accessedServices,
),
},
redis: {
where: buildServiceFilter(redis.redisId, accessedServices),
},
},
},
},
});
if (!project) {
throw new TRPCError({
code: "NOT_FOUND",
message: "Project not found",
});
}
return project;
}
const project = await findProjectById(input.projectId);
if (project.organizationId !== ctx.session.activeOrganizationId) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to access this project",
});
}
return project;
}),
all: protectedProcedure.query(async ({ ctx }) => {
if (ctx.user.role === "member") {
const { accessedProjects, accessedEnvironments, accessedServices } =
await findMemberById(ctx.user.id, ctx.session.activeOrganizationId);
if (accessedProjects.length === 0) {
return [];
}
// Build environment filter
const environmentFilter =
accessedEnvironments.length === 0
? sql`false`
: sql`${environments.environmentId} IN (${sql.join(
accessedEnvironments.map((envId) => sql`${envId}`),
sql`, `,
)})`;
return await db.query.projects.findMany({
where: and(
sql`${projects.projectId} IN (${sql.join(
accessedProjects.map((projectId) => sql`${projectId}`),
sql`, `,
)})`,
eq(projects.organizationId, ctx.session.activeOrganizationId),
),
with: {
environments: {
where: environmentFilter,
with: {
applications: {
where: buildServiceFilter(
applications.applicationId,
accessedServices,
),
with: { domains: true },
},
mariadb: {
where: buildServiceFilter(mariadb.mariadbId, accessedServices),
},
mongo: {
where: buildServiceFilter(mongo.mongoId, accessedServices),
},
mysql: {
where: buildServiceFilter(mysql.mysqlId, accessedServices),
},
postgres: {
where: buildServiceFilter(
postgres.postgresId,
accessedServices,
),
},
redis: {
where: buildServiceFilter(redis.redisId, accessedServices),
},
compose: {
where: buildServiceFilter(compose.composeId, accessedServices),
with: { domains: true },
},
},
},
},
orderBy: desc(projects.createdAt),
});
}
return await db.query.projects.findMany({
with: {
environments: {
with: {
applications: {
with: {
domains: true,
},
},
mariadb: true,
mongo: true,
mysql: true,
postgres: true,
redis: true,
compose: {
with: {
domains: true,
},
},
},
},
},
where: eq(projects.organizationId, ctx.session.activeOrganizationId),
orderBy: desc(projects.createdAt),
});
}),
search: protectedProcedure
.input(
z.object({
q: z.string().optional(),
name: z.string().optional(),
description: 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.q?.trim()) {
const term = `%${input.q.trim()}%`;
baseConditions.push(
or(
ilike(projects.name, term),
ilike(projects.description ?? "", term),
)!,
);
}
if (input.name?.trim()) {
baseConditions.push(ilike(projects.name, `%${input.name.trim()}%`));
}
if (input.description?.trim()) {
baseConditions.push(
ilike(projects.description ?? "", `%${input.description.trim()}%`),
);
}
if (ctx.user.role === "member") {
const { accessedProjects } = await findMemberById(
ctx.user.id,
ctx.session.activeOrganizationId,
);
if (accessedProjects.length === 0) return { items: [], total: 0 };
baseConditions.push(
sql`${projects.projectId} IN (${sql.join(
accessedProjects.map((id) => sql`${id}`),
sql`, `,
)})`,
);
}
const where = and(...baseConditions);
const [items, countResult] = await Promise.all([
db.query.projects.findMany({
where,
limit: input.limit,
offset: input.offset,
orderBy: desc(projects.createdAt),
columns: {
projectId: true,
name: true,
description: true,
createdAt: true,
organizationId: true,
env: true,
},
}),
db
.select({ count: sql<number>`count(*)::int` })
.from(projects)
.where(where),
]);
return {
items,
total: countResult[0]?.count ?? 0,
};
}),
remove: protectedProcedure
.input(apiRemoveProject)
.mutation(async ({ input, ctx }) => {
try {
if (ctx.user.role === "member") {
await checkProjectAccess(
ctx.user.id,
"delete",
ctx.session.activeOrganizationId,
);
}
const currentProject = await findProjectById(input.projectId);
if (
currentProject.organizationId !== ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to delete this project",
});
}
const deletedProject = await deleteProject(input.projectId);
return deletedProject;
} catch (error) {
throw error;
}
}),
update: protectedProcedure
.input(apiUpdateProject)
.mutation(async ({ input, ctx }) => {
try {
const currentProject = await findProjectById(input.projectId);
if (
currentProject.organizationId !== ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to update this project",
});
}
const project = await updateProjectById(input.projectId, {
...input,
});
return project;
} catch (error) {
throw error;
}
}),
duplicate: protectedProcedure
.input(
z.object({
sourceEnvironmentId: z.string(),
name: z.string(),
description: z.string().optional(),
includeServices: z.boolean().default(true),
selectedServices: z
.array(
z.object({
id: z.string(),
type: z.enum([
"application",
"postgres",
"mariadb",
"mongo",
"mysql",
"redis",
"compose",
]),
}),
)
.optional(),
duplicateInSameProject: z.boolean().default(false),
}),
)
.mutation(async ({ ctx, input }) => {
try {
if (ctx.user.role === "member") {
await checkProjectAccess(
ctx.user.id,
"create",
ctx.session.activeOrganizationId,
);
}
// Get source project
const sourceEnvironment = input.duplicateInSameProject
? await findEnvironmentById(input.sourceEnvironmentId)
: null;
if (
input.duplicateInSameProject &&
sourceEnvironment?.project.organizationId !==
ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to access this project",
});
}
// Create new project or use existing one
const targetProject = input.duplicateInSameProject
? sourceEnvironment
: await createProject(
{
name: input.name,
description: input.description,
env: sourceEnvironment?.project.env,
},
ctx.session.activeOrganizationId,
).then((value) => value.environment);
console.log("targetProject", targetProject);
if (input.includeServices) {
const servicesToDuplicate = input.selectedServices || [];
// Helper function to duplicate a service
const duplicateService = async (id: string, type: string) => {
switch (type) {
case "application": {
const {
applicationId,
domains,
security,
ports,
registry,
redirects,
previewDeployments,
mounts,
appName,
refreshToken,
...application
} = await findApplicationById(id);
const newAppName = appName.substring(
0,
appName.lastIndexOf("-"),
);
const newApplication = await createApplication({
...application,
appName: newAppName,
name: input.duplicateInSameProject
? `${application.name} (copy)`
: application.name,
environmentId: targetProject?.environmentId || "",
});
for (const domain of domains) {
const { domainId, ...rest } = domain;
await createDomain({
...rest,
applicationId: newApplication.applicationId,
domainType: "application",
});
}
for (const port of ports) {
const { portId, ...rest } = port;
await createPort({
...rest,
applicationId: newApplication.applicationId,
});
}
for (const mount of mounts) {
const { mountId, ...rest } = mount;
await createMount({
...rest,
serviceId: newApplication.applicationId,
serviceType: "application",
});
}
for (const redirect of redirects) {
const { redirectId, ...rest } = redirect;
await createRedirect({
...rest,
applicationId: newApplication.applicationId,
});
}
for (const secure of security) {
const { securityId, ...rest } = secure;
await createSecurity({
...rest,
applicationId: newApplication.applicationId,
});
}
for (const previewDeployment of previewDeployments) {
const { previewDeploymentId, ...rest } = previewDeployment;
await createPreviewDeployment({
...rest,
applicationId: newApplication.applicationId,
domainId: undefined,
});
}
break;
}
case "postgres": {
const { postgresId, mounts, backups, appName, ...postgres } =
await findPostgresById(id);
const newAppName = appName.substring(
0,
appName.lastIndexOf("-"),
);
const newPostgres = await createPostgres({
...postgres,
appName: newAppName,
name: input.duplicateInSameProject
? `${postgres.name} (copy)`
: postgres.name,
environmentId: targetProject?.environmentId || "",
});
for (const mount of mounts) {
const { mountId, ...rest } = mount;
await createMount({
...rest,
serviceId: newPostgres.postgresId,
serviceType: "postgres",
});
}
for (const backup of backups) {
const { backupId, appName: _appName, ...rest } = backup;
await createBackup({
...rest,
postgresId: newPostgres.postgresId,
});
}
break;
}
case "mariadb": {
const { mariadbId, mounts, backups, appName, ...mariadb } =
await findMariadbById(id);
const newAppName = appName.substring(
0,
appName.lastIndexOf("-"),
);
const newMariadb = await createMariadb({
...mariadb,
appName: newAppName,
name: input.duplicateInSameProject
? `${mariadb.name} (copy)`
: mariadb.name,
environmentId: targetProject?.environmentId || "",
});
for (const mount of mounts) {
const { mountId, ...rest } = mount;
await createMount({
...rest,
serviceId: newMariadb.mariadbId,
serviceType: "mariadb",
});
}
for (const backup of backups) {
const { backupId, appName: _appName, ...rest } = backup;
await createBackup({
...rest,
mariadbId: newMariadb.mariadbId,
});
}
break;
}
case "mongo": {
const { mongoId, mounts, backups, appName, ...mongo } =
await findMongoById(id);
const newAppName = appName.substring(
0,
appName.lastIndexOf("-"),
);
const newMongo = await createMongo({
...mongo,
appName: newAppName,
name: input.duplicateInSameProject
? `${mongo.name} (copy)`
: mongo.name,
environmentId: targetProject?.environmentId || "",
});
for (const mount of mounts) {
const { mountId, ...rest } = mount;
await createMount({
...rest,
serviceId: newMongo.mongoId,
serviceType: "mongo",
});
}
for (const backup of backups) {
const { backupId, appName: _appName, ...rest } = backup;
await createBackup({
...rest,
mongoId: newMongo.mongoId,
});
}
break;
}
case "mysql": {
const { mysqlId, mounts, backups, appName, ...mysql } =
await findMySqlById(id);
const newAppName = appName.substring(
0,
appName.lastIndexOf("-"),
);
const newMysql = await createMysql({
...mysql,
appName: newAppName,
name: input.duplicateInSameProject
? `${mysql.name} (copy)`
: mysql.name,
environmentId: targetProject?.environmentId || "",
});
for (const mount of mounts) {
const { mountId, ...rest } = mount;
await createMount({
...rest,
serviceId: newMysql.mysqlId,
serviceType: "mysql",
});
}
for (const backup of backups) {
const { backupId, appName: _appName, ...rest } = backup;
await createBackup({
...rest,
mysqlId: newMysql.mysqlId,
});
}
break;
}
case "redis": {
const { redisId, mounts, appName, ...redis } =
await findRedisById(id);
const newAppName = appName.substring(
0,
appName.lastIndexOf("-"),
);
const newRedis = await createRedis({
...redis,
appName: newAppName,
name: input.duplicateInSameProject
? `${redis.name} (copy)`
: redis.name,
environmentId: targetProject?.environmentId || "",
});
for (const mount of mounts) {
const { mountId, ...rest } = mount;
await createMount({
...rest,
serviceId: newRedis.redisId,
serviceType: "redis",
});
}
break;
}
case "compose": {
const {
composeId,
mounts,
domains,
appName,
refreshToken,
...compose
} = await findComposeById(id);
const newAppName = appName.substring(
0,
appName.lastIndexOf("-"),
);
const newCompose = await createCompose({
...compose,
appName: newAppName,
name: input.duplicateInSameProject
? `${compose.name} (copy)`
: compose.name,
environmentId: targetProject?.environmentId || "",
});
for (const mount of mounts) {
const { mountId, ...rest } = mount;
await createMount({
...rest,
serviceId: newCompose.composeId,
serviceType: "compose",
});
}
for (const domain of domains) {
const { domainId, ...rest } = domain;
await createDomain({
...rest,
composeId: newCompose.composeId,
domainType: "compose",
});
}
break;
}
}
};
// Duplicate selected services
for (const service of servicesToDuplicate) {
await duplicateService(service.id, service.type);
}
}
if (!input.duplicateInSameProject && ctx.user.role === "member") {
await addNewProject(
ctx.user.id,
targetProject?.projectId || "",
ctx.session.activeOrganizationId,
);
}
return targetProject;
} catch (error) {
throw new TRPCError({
code: "BAD_REQUEST",
message: `Error duplicating the project: ${error instanceof Error ? error.message : error}`,
cause: error,
});
}
}),
});
function buildServiceFilter(
fieldName: AnyPgColumn,
accessedServices: string[],
) {
return accessedServices.length === 0
? sql`false`
: sql`${fieldName} IN (${sql.join(
accessedServices.map((serviceId) => sql`${serviceId}`),
sql`, `,
)})`;
}