mirror of
https://github.com/andwati/dokploy.git
synced 2026-10-31 09:12:15 +00:00
Merge branch 'canary' into cucumber-sp/canary
This commit is contained in:
@@ -1,7 +1,4 @@
|
||||
import {
|
||||
addNewEnvironment,
|
||||
addNewProject,
|
||||
checkProjectAccess,
|
||||
createApplication,
|
||||
createBackup,
|
||||
createCompose,
|
||||
@@ -22,7 +19,6 @@ import {
|
||||
findComposeById,
|
||||
findEnvironmentById,
|
||||
findMariadbById,
|
||||
findMemberById,
|
||||
findMongoById,
|
||||
findMySqlById,
|
||||
findPostgresById,
|
||||
@@ -32,12 +28,24 @@ import {
|
||||
IS_CLOUD,
|
||||
updateProjectById,
|
||||
} from "@dokploy/server";
|
||||
import {
|
||||
addNewEnvironment,
|
||||
addNewProject,
|
||||
checkPermission,
|
||||
checkProjectAccess,
|
||||
findMemberByUserId,
|
||||
} from "@dokploy/server/services/permission";
|
||||
import { db } from "@dokploy/server/db";
|
||||
import { TRPCError } from "@trpc/server";
|
||||
import { and, desc, eq, sql } from "drizzle-orm";
|
||||
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 { db } from "@/server/db";
|
||||
import { audit } from "@/server/api/utils/audit";
|
||||
import {
|
||||
createTRPCRouter,
|
||||
protectedProcedure,
|
||||
withPermission,
|
||||
} from "@/server/api/trpc";
|
||||
import {
|
||||
apiCreateProject,
|
||||
apiFindOneProject,
|
||||
@@ -59,13 +67,7 @@ export const projectRouter = createTRPCRouter({
|
||||
.input(apiCreateProject)
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
try {
|
||||
if (ctx.user.role === "member") {
|
||||
await checkProjectAccess(
|
||||
ctx.user.id,
|
||||
"create",
|
||||
ctx.session.activeOrganizationId,
|
||||
);
|
||||
}
|
||||
await checkProjectAccess(ctx, "create");
|
||||
|
||||
const admin = await findUserById(ctx.user.ownerId);
|
||||
|
||||
@@ -80,20 +82,16 @@ export const projectRouter = createTRPCRouter({
|
||||
input,
|
||||
ctx.session.activeOrganizationId,
|
||||
);
|
||||
if (ctx.user.role === "member") {
|
||||
await addNewProject(
|
||||
ctx.user.id,
|
||||
project.project.projectId,
|
||||
ctx.session.activeOrganizationId,
|
||||
);
|
||||
await addNewProject(ctx, project.project.projectId);
|
||||
|
||||
await addNewEnvironment(
|
||||
ctx.user.id,
|
||||
project?.environment?.environmentId || "",
|
||||
ctx.session.activeOrganizationId,
|
||||
);
|
||||
}
|
||||
await addNewEnvironment(ctx, project?.environment?.environmentId || "");
|
||||
|
||||
await audit(ctx, {
|
||||
action: "create",
|
||||
resourceType: "project",
|
||||
resourceId: project.project.projectId,
|
||||
resourceName: project.project.name,
|
||||
});
|
||||
return project;
|
||||
} catch (error) {
|
||||
throw new TRPCError({
|
||||
@@ -107,18 +105,18 @@ export const projectRouter = createTRPCRouter({
|
||||
one: protectedProcedure
|
||||
.input(apiFindOneProject)
|
||||
.query(async ({ input, ctx }) => {
|
||||
if (ctx.user.role === "member") {
|
||||
const { accessedServices } = await findMemberById(
|
||||
if (ctx.user.role !== "owner" && ctx.user.role !== "admin") {
|
||||
const { accessedServices, accessedProjects } = await findMemberByUserId(
|
||||
ctx.user.id,
|
||||
ctx.session.activeOrganizationId,
|
||||
);
|
||||
|
||||
await checkProjectAccess(
|
||||
ctx.user.id,
|
||||
"access",
|
||||
ctx.session.activeOrganizationId,
|
||||
input.projectId,
|
||||
);
|
||||
if (!accessedProjects.includes(input.projectId)) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You don't have access to this project",
|
||||
});
|
||||
}
|
||||
|
||||
const project = await db.query.projects.findFirst({
|
||||
where: and(
|
||||
@@ -190,15 +188,14 @@ export const projectRouter = createTRPCRouter({
|
||||
return project;
|
||||
}),
|
||||
all: protectedProcedure.query(async ({ ctx }) => {
|
||||
if (ctx.user.role === "member") {
|
||||
if (ctx.user.role !== "owner" && ctx.user.role !== "admin") {
|
||||
const { accessedProjects, accessedEnvironments, accessedServices } =
|
||||
await findMemberById(ctx.user.id, ctx.session.activeOrganizationId);
|
||||
await findMemberByUserId(ctx.user.id, ctx.session.activeOrganizationId);
|
||||
|
||||
if (accessedProjects.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// Build environment filter
|
||||
const environmentFilter =
|
||||
accessedEnvironments.length === 0
|
||||
? sql`false`
|
||||
@@ -224,31 +221,69 @@ export const projectRouter = createTRPCRouter({
|
||||
applications.applicationId,
|
||||
accessedServices,
|
||||
),
|
||||
with: { domains: true },
|
||||
columns: {
|
||||
applicationId: true,
|
||||
name: true,
|
||||
applicationStatus: true,
|
||||
},
|
||||
},
|
||||
mariadb: {
|
||||
where: buildServiceFilter(mariadb.mariadbId, accessedServices),
|
||||
columns: {
|
||||
mariadbId: true,
|
||||
name: true,
|
||||
applicationStatus: true,
|
||||
},
|
||||
},
|
||||
mongo: {
|
||||
where: buildServiceFilter(mongo.mongoId, accessedServices),
|
||||
columns: {
|
||||
mongoId: true,
|
||||
name: true,
|
||||
applicationStatus: true,
|
||||
},
|
||||
},
|
||||
mysql: {
|
||||
where: buildServiceFilter(mysql.mysqlId, accessedServices),
|
||||
columns: {
|
||||
mysqlId: true,
|
||||
name: true,
|
||||
applicationStatus: true,
|
||||
},
|
||||
},
|
||||
postgres: {
|
||||
where: buildServiceFilter(
|
||||
postgres.postgresId,
|
||||
accessedServices,
|
||||
),
|
||||
columns: {
|
||||
postgresId: true,
|
||||
name: true,
|
||||
applicationStatus: true,
|
||||
},
|
||||
},
|
||||
redis: {
|
||||
where: buildServiceFilter(redis.redisId, accessedServices),
|
||||
columns: {
|
||||
redisId: true,
|
||||
name: true,
|
||||
applicationStatus: true,
|
||||
},
|
||||
},
|
||||
compose: {
|
||||
where: buildServiceFilter(compose.composeId, accessedServices),
|
||||
with: { domains: true },
|
||||
columns: {
|
||||
composeId: true,
|
||||
name: true,
|
||||
composeStatus: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
columns: {
|
||||
environmentId: true,
|
||||
isDefault: true,
|
||||
name: true,
|
||||
},
|
||||
},
|
||||
projectTags: {
|
||||
with: {
|
||||
@@ -265,21 +300,50 @@ export const projectRouter = createTRPCRouter({
|
||||
environments: {
|
||||
with: {
|
||||
applications: {
|
||||
with: {
|
||||
domains: true,
|
||||
columns: {
|
||||
applicationId: true,
|
||||
name: true,
|
||||
applicationStatus: true,
|
||||
},
|
||||
},
|
||||
mariadb: {
|
||||
columns: {
|
||||
mariadbId: true,
|
||||
},
|
||||
},
|
||||
mongo: {
|
||||
columns: {
|
||||
mongoId: true,
|
||||
},
|
||||
},
|
||||
mysql: {
|
||||
columns: {
|
||||
mysqlId: true,
|
||||
},
|
||||
},
|
||||
postgres: {
|
||||
columns: {
|
||||
postgresId: true,
|
||||
},
|
||||
},
|
||||
redis: {
|
||||
columns: {
|
||||
redisId: true,
|
||||
},
|
||||
},
|
||||
mariadb: true,
|
||||
mongo: true,
|
||||
mysql: true,
|
||||
postgres: true,
|
||||
redis: true,
|
||||
compose: {
|
||||
with: {
|
||||
domains: true,
|
||||
columns: {
|
||||
composeId: true,
|
||||
name: true,
|
||||
composeStatus: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
columns: {
|
||||
name: true,
|
||||
environmentId: true,
|
||||
isDefault: true,
|
||||
},
|
||||
},
|
||||
projectTags: {
|
||||
with: {
|
||||
@@ -292,17 +356,188 @@ export const projectRouter = createTRPCRouter({
|
||||
});
|
||||
}),
|
||||
|
||||
allForPermissions: withPermission("member", "update").query(
|
||||
async ({ ctx }) => {
|
||||
return await db.query.projects.findMany({
|
||||
where: eq(projects.organizationId, ctx.session.activeOrganizationId),
|
||||
orderBy: desc(projects.createdAt),
|
||||
columns: {
|
||||
projectId: true,
|
||||
name: true,
|
||||
},
|
||||
with: {
|
||||
environments: {
|
||||
columns: {
|
||||
environmentId: true,
|
||||
name: true,
|
||||
isDefault: true,
|
||||
},
|
||||
with: {
|
||||
applications: {
|
||||
columns: {
|
||||
applicationId: true,
|
||||
appName: true,
|
||||
name: true,
|
||||
createdAt: true,
|
||||
applicationStatus: true,
|
||||
description: true,
|
||||
serverId: true,
|
||||
},
|
||||
},
|
||||
mariadb: {
|
||||
columns: {
|
||||
mariadbId: true,
|
||||
appName: true,
|
||||
name: true,
|
||||
createdAt: true,
|
||||
applicationStatus: true,
|
||||
description: true,
|
||||
serverId: true,
|
||||
},
|
||||
},
|
||||
postgres: {
|
||||
columns: {
|
||||
postgresId: true,
|
||||
appName: true,
|
||||
name: true,
|
||||
createdAt: true,
|
||||
applicationStatus: true,
|
||||
description: true,
|
||||
serverId: true,
|
||||
},
|
||||
},
|
||||
mysql: {
|
||||
columns: {
|
||||
mysqlId: true,
|
||||
appName: true,
|
||||
name: true,
|
||||
createdAt: true,
|
||||
applicationStatus: true,
|
||||
description: true,
|
||||
serverId: true,
|
||||
},
|
||||
},
|
||||
mongo: {
|
||||
columns: {
|
||||
mongoId: true,
|
||||
appName: true,
|
||||
name: true,
|
||||
createdAt: true,
|
||||
applicationStatus: true,
|
||||
description: true,
|
||||
serverId: true,
|
||||
},
|
||||
},
|
||||
redis: {
|
||||
columns: {
|
||||
redisId: true,
|
||||
appName: true,
|
||||
name: true,
|
||||
createdAt: true,
|
||||
applicationStatus: true,
|
||||
description: true,
|
||||
serverId: true,
|
||||
},
|
||||
},
|
||||
compose: {
|
||||
columns: {
|
||||
composeId: true,
|
||||
appName: true,
|
||||
name: true,
|
||||
createdAt: true,
|
||||
composeStatus: true,
|
||||
description: true,
|
||||
serverId: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
},
|
||||
),
|
||||
|
||||
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 !== "owner" && ctx.user.role !== "admin") {
|
||||
const { accessedProjects } = await findMemberByUserId(
|
||||
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
|
||||
@@ -312,8 +547,15 @@ export const projectRouter = createTRPCRouter({
|
||||
message: "You are not authorized to delete this project",
|
||||
});
|
||||
}
|
||||
await checkProjectAccess(ctx, "delete", input.projectId);
|
||||
const deletedProject = await deleteProject(input.projectId);
|
||||
|
||||
await audit(ctx, {
|
||||
action: "delete",
|
||||
resourceType: "project",
|
||||
resourceId: currentProject.projectId,
|
||||
resourceName: currentProject.name,
|
||||
});
|
||||
return deletedProject;
|
||||
} catch (error) {
|
||||
throw error;
|
||||
@@ -332,10 +574,36 @@ export const projectRouter = createTRPCRouter({
|
||||
message: "You are not authorized to update this project",
|
||||
});
|
||||
}
|
||||
|
||||
if (ctx.user.role !== "owner" && ctx.user.role !== "admin") {
|
||||
const { accessedProjects } = await findMemberByUserId(
|
||||
ctx.user.id,
|
||||
ctx.session.activeOrganizationId,
|
||||
);
|
||||
if (!accessedProjects.includes(input.projectId)) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You don't have access to this project",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (input.env !== undefined) {
|
||||
await checkPermission(ctx, { projectEnvVars: ["write"] });
|
||||
}
|
||||
|
||||
const project = await updateProjectById(input.projectId, {
|
||||
...input,
|
||||
});
|
||||
|
||||
if (project) {
|
||||
await audit(ctx, {
|
||||
action: "update",
|
||||
resourceType: "project",
|
||||
resourceId: input.projectId,
|
||||
resourceName: project.name,
|
||||
});
|
||||
}
|
||||
return project;
|
||||
} catch (error) {
|
||||
throw error;
|
||||
@@ -369,15 +637,8 @@ export const projectRouter = createTRPCRouter({
|
||||
)
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
try {
|
||||
if (ctx.user.role === "member") {
|
||||
await checkProjectAccess(
|
||||
ctx.user.id,
|
||||
"create",
|
||||
ctx.session.activeOrganizationId,
|
||||
);
|
||||
}
|
||||
await checkProjectAccess(ctx, "create");
|
||||
|
||||
// Get source project
|
||||
const sourceEnvironment = input.duplicateInSameProject
|
||||
? await findEnvironmentById(input.sourceEnvironmentId)
|
||||
: null;
|
||||
@@ -393,7 +654,24 @@ export const projectRouter = createTRPCRouter({
|
||||
});
|
||||
}
|
||||
|
||||
// Create new project or use existing one
|
||||
if (
|
||||
input.duplicateInSameProject &&
|
||||
sourceEnvironment &&
|
||||
ctx.user.role !== "owner" &&
|
||||
ctx.user.role !== "admin"
|
||||
) {
|
||||
const { accessedProjects } = await findMemberByUserId(
|
||||
ctx.user.id,
|
||||
ctx.session.activeOrganizationId,
|
||||
);
|
||||
if (!accessedProjects.includes(sourceEnvironment.project.projectId)) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You don't have access to this project",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const targetProject = input.duplicateInSameProject
|
||||
? sourceEnvironment
|
||||
: await createProject(
|
||||
@@ -410,7 +688,6 @@ export const projectRouter = createTRPCRouter({
|
||||
if (input.includeServices) {
|
||||
const servicesToDuplicate = input.selectedServices || [];
|
||||
|
||||
// Helper function to duplicate a service
|
||||
const duplicateService = async (id: string, type: string) => {
|
||||
switch (type) {
|
||||
case "application": {
|
||||
@@ -488,6 +765,7 @@ export const projectRouter = createTRPCRouter({
|
||||
await createPreviewDeployment({
|
||||
...rest,
|
||||
applicationId: newApplication.applicationId,
|
||||
domainId: undefined,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -713,20 +991,22 @@ export const projectRouter = createTRPCRouter({
|
||||
}
|
||||
};
|
||||
|
||||
// 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,
|
||||
);
|
||||
if (!input.duplicateInSameProject) {
|
||||
await addNewProject(ctx, targetProject?.projectId || "");
|
||||
}
|
||||
|
||||
await audit(ctx, {
|
||||
action: "create",
|
||||
resourceType: "project",
|
||||
resourceId: targetProject?.projectId || "",
|
||||
resourceName: input.name,
|
||||
metadata: { duplicatedFrom: input.sourceEnvironmentId },
|
||||
});
|
||||
return targetProject;
|
||||
} catch (error) {
|
||||
throw new TRPCError({
|
||||
|
||||
Reference in New Issue
Block a user