diff --git a/apps/dokploy/components/dashboard/application/general/generic/save-bitbucket-provider.tsx b/apps/dokploy/components/dashboard/application/general/generic/save-bitbucket-provider.tsx index 3a396b770..2b0ae3f62 100644 --- a/apps/dokploy/components/dashboard/application/general/generic/save-bitbucket-provider.tsx +++ b/apps/dokploy/components/dashboard/application/general/generic/save-bitbucket-provider.tsx @@ -188,6 +188,9 @@ export const SaveBitbucketProvider = ({ applicationId }: Props) => { Bitbucket Account { + if (!value) { + return; + } field.onChange(value); form.setValue("repository", { owner: "", @@ -208,7 +211,6 @@ export const SaveGiteaProvider = ({ applicationId }: Props) => { }); form.setValue("branch", ""); }} - defaultValue={field.value} value={field.value} > @@ -258,7 +260,7 @@ export const SaveGiteaProvider = ({ applicationId }: Props) => { @@ -82,7 +82,7 @@ export const ShowNodeApplications = ({ serverId }: Props) => { return ( - diff --git a/apps/dokploy/components/dashboard/swarm/details/details-card.tsx b/apps/dokploy/components/dashboard/swarm/details/details-card.tsx index a783a52e1..f0dd1d36c 100644 --- a/apps/dokploy/components/dashboard/swarm/details/details-card.tsx +++ b/apps/dokploy/components/dashboard/swarm/details/details-card.tsx @@ -110,7 +110,7 @@ export function NodeCard({ node, serverId }: Props) { -
+
diff --git a/apps/dokploy/components/dashboard/swarm/details/show-node-config.tsx b/apps/dokploy/components/dashboard/swarm/details/show-node-config.tsx index a535d13c7..afed238a3 100644 --- a/apps/dokploy/components/dashboard/swarm/details/show-node-config.tsx +++ b/apps/dokploy/components/dashboard/swarm/details/show-node-config.tsx @@ -24,7 +24,7 @@ export const ShowNodeConfig = ({ nodeId, serverId }: Props) => { return ( - diff --git a/apps/dokploy/components/shared/tag-filter.tsx b/apps/dokploy/components/shared/tag-filter.tsx index 0faa37a88..12a210a6d 100644 --- a/apps/dokploy/components/shared/tag-filter.tsx +++ b/apps/dokploy/components/shared/tag-filter.tsx @@ -94,9 +94,10 @@ export function TagFilter({
- No tags found. + {tags.length === 0 + ? "No tags created yet." + : "No tags found."} -
@@ -118,6 +119,9 @@ export function TagFilter({ ); })} +
+ +
diff --git a/apps/dokploy/components/shared/tag-selector.tsx b/apps/dokploy/components/shared/tag-selector.tsx index 26e528e32..036173ded 100644 --- a/apps/dokploy/components/shared/tag-selector.tsx +++ b/apps/dokploy/components/shared/tag-selector.tsx @@ -111,19 +111,12 @@ export function TagSelector({
- No tags found. + {tags.length === 0 + ? "No tags created yet." + : "No tags found."} -
- {tags.length === 0 && ( -
- - No tags created yet. - - -
- )} {tags.map((tag) => { const isSelected = selectedTags.includes(tag.id); @@ -153,6 +146,9 @@ export function TagSelector({ ); })} +
+ +
diff --git a/apps/dokploy/components/ui/dialog.tsx b/apps/dokploy/components/ui/dialog.tsx index 9c8054e66..7ddff98da 100644 --- a/apps/dokploy/components/ui/dialog.tsx +++ b/apps/dokploy/components/ui/dialog.tsx @@ -3,6 +3,7 @@ import { Dialog as DialogPrimitive } from "radix-ui"; import { cn } from "@/lib/utils"; import { Button } from "@/components/ui/button"; +import { wasNestedPopupJustClosed } from "@/components/ui/nested-popup-context"; import { XIcon } from "lucide-react"; function Dialog({ @@ -49,6 +50,8 @@ function DialogContent({ className, children, showCloseButton = true, + onPointerDownOutside, + onEscapeKeyDown, ...props }: React.ComponentProps & { showCloseButton?: boolean; @@ -62,6 +65,20 @@ function DialogContent({ "fixed top-1/2 left-1/2 z-50 flex max-h-[90vh] w-full max-w-[calc(100%-2rem)] -translate-x-1/2 -translate-y-1/2 flex-col gap-4 overflow-y-auto overscroll-contain rounded-xl bg-popover p-6 text-sm text-popover-foreground ring-1 ring-foreground/10 duration-100 outline-none sm:max-w-lg data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95", className, )} + onPointerDownOutside={(event) => { + if (wasNestedPopupJustClosed()) { + event.preventDefault(); + return; + } + onPointerDownOutside?.(event); + }} + onEscapeKeyDown={(event) => { + if (wasNestedPopupJustClosed()) { + event.preventDefault(); + return; + } + onEscapeKeyDown?.(event); + }} {...props} > {children} diff --git a/apps/dokploy/components/ui/dropdown-menu.tsx b/apps/dokploy/components/ui/dropdown-menu.tsx index 2899bb313..ace8ead13 100644 --- a/apps/dokploy/components/ui/dropdown-menu.tsx +++ b/apps/dokploy/components/ui/dropdown-menu.tsx @@ -2,12 +2,25 @@ import * as React from "react"; import { DropdownMenu as DropdownMenuPrimitive } from "radix-ui"; import { cn } from "@/lib/utils"; +import { markNestedPopupClosed } from "@/components/ui/nested-popup-context"; import { CheckIcon, ChevronRightIcon } from "lucide-react"; function DropdownMenu({ + onOpenChange, ...props }: React.ComponentProps) { - return ; + return ( + { + if (!open) { + markNestedPopupClosed(); + } + onOpenChange?.(open); + }} + {...props} + /> + ); } function DropdownMenuPortal({ diff --git a/apps/dokploy/components/ui/nested-popup-context.ts b/apps/dokploy/components/ui/nested-popup-context.ts new file mode 100644 index 000000000..cbb7548a6 --- /dev/null +++ b/apps/dokploy/components/ui/nested-popup-context.ts @@ -0,0 +1,9 @@ +let lastNestedPopupCloseAt = 0; + +export function markNestedPopupClosed() { + lastNestedPopupCloseAt = performance.now(); +} + +export function wasNestedPopupJustClosed() { + return performance.now() - lastNestedPopupCloseAt < 100; +} diff --git a/apps/dokploy/components/ui/popover.tsx b/apps/dokploy/components/ui/popover.tsx index f8be16def..82fdabfbd 100644 --- a/apps/dokploy/components/ui/popover.tsx +++ b/apps/dokploy/components/ui/popover.tsx @@ -4,11 +4,24 @@ import * as React from "react"; import { Popover as PopoverPrimitive } from "radix-ui"; import { cn } from "@/lib/utils"; +import { markNestedPopupClosed } from "@/components/ui/nested-popup-context"; function Popover({ + onOpenChange, ...props }: React.ComponentProps) { - return ; + return ( + { + if (!open) { + markNestedPopupClosed(); + } + onOpenChange?.(open); + }} + {...props} + /> + ); } function PopoverTrigger({ diff --git a/apps/dokploy/components/ui/select.tsx b/apps/dokploy/components/ui/select.tsx index 6be0b1689..ace7d407a 100644 --- a/apps/dokploy/components/ui/select.tsx +++ b/apps/dokploy/components/ui/select.tsx @@ -58,7 +58,7 @@ function SelectTrigger({ function SelectContent({ className, children, - position = "item-aligned", + position = "popper", align = "center", ...props }: React.ComponentProps) { diff --git a/apps/dokploy/pages/dashboard/project/[projectId]/environment/[environmentId].tsx b/apps/dokploy/pages/dashboard/project/[projectId]/environment/[environmentId].tsx index 50ac02770..4d1cc753d 100644 --- a/apps/dokploy/pages/dashboard/project/[projectId]/environment/[environmentId].tsx +++ b/apps/dokploy/pages/dashboard/project/[projectId]/environment/[environmentId].tsx @@ -1878,7 +1878,7 @@ export async function getServerSideProps( // Try to find default, otherwise use first accessible const targetEnv = accessibleEnvironments.find((env) => env.isDefault) || - accessibleEnvironments[0]; + accessibleEnvironments[0]!; return { redirect: { diff --git a/apps/dokploy/server/api/routers/backup.ts b/apps/dokploy/server/api/routers/backup.ts index 75bb60f2c..c324f5cc1 100644 --- a/apps/dokploy/server/api/routers/backup.ts +++ b/apps/dokploy/server/api/routers/backup.ts @@ -1,6 +1,7 @@ import { createBackup, findBackupById, + findBackupsByDbId, findComposeByBackupId, findComposeById, findLibsqlByBackupId, @@ -55,6 +56,7 @@ import { withPermission, } from "@/server/api/trpc"; import { audit } from "@/server/api/utils/audit"; +import { assertDatabaseBackupLimit } from "@/server/api/utils/plan-limits"; import { apiCreateBackup, apiFindOneBackup, @@ -94,6 +96,22 @@ export const backupRouter = createTRPCRouter({ }); } + if (IS_CLOUD) { + const dbType = ( + ["postgres", "mysql", "mariadb", "mongo", "libsql"] as const + ).find((type) => input[`${type}Id`]); + if (dbType) { + const existingBackups = await findBackupsByDbId( + input[`${dbType}Id`]!, + dbType, + ); + await assertDatabaseBackupLimit( + ctx.session.activeOrganizationId, + existingBackups.length, + ); + } + } + const newBackup = await createBackup(input); const backup = await findBackupById(newBackup.backupId); diff --git a/apps/dokploy/server/api/routers/environment.ts b/apps/dokploy/server/api/routers/environment.ts index 78a60362e..c11673ea7 100644 --- a/apps/dokploy/server/api/routers/environment.ts +++ b/apps/dokploy/server/api/routers/environment.ts @@ -2,8 +2,10 @@ import { createEnvironment, deleteEnvironment, duplicateEnvironment, + filterEnvironmentServices, findEnvironmentById, findEnvironmentsByProjectId, + IS_CLOUD, updateEnvironmentById, } from "@dokploy/server"; import { db } from "@dokploy/server/db"; @@ -20,6 +22,7 @@ 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 { assertEnvironmentLimit } from "@/server/api/utils/plan-limits"; import { apiCreateEnvironment, apiDuplicateEnvironment, @@ -30,43 +33,18 @@ import { projects, } from "@/server/db/schema"; -const filterEnvironmentServices = ( - environment: any, - accessedServices: string[], -) => ({ - ...environment, - applications: environment.applications.filter((app: any) => - accessedServices.includes(app.applicationId), - ), - compose: environment.compose.filter((comp: any) => - accessedServices.includes(comp.composeId), - ), - libsql: environment.libsql.filter((db: any) => - accessedServices.includes(db.libsqlId), - ), - mariadb: environment.mariadb.filter((db: any) => - accessedServices.includes(db.mariadbId), - ), - mongo: environment.mongo.filter((db: any) => - accessedServices.includes(db.mongoId), - ), - mysql: environment.mysql.filter((db: any) => - accessedServices.includes(db.mysqlId), - ), - postgres: environment.postgres.filter((db: any) => - accessedServices.includes(db.postgresId), - ), - redis: environment.redis.filter((db: any) => - accessedServices.includes(db.redisId), - ), -}); - export const environmentRouter = createTRPCRouter({ create: protectedProcedure .input(apiCreateEnvironment) .mutation(async ({ input, ctx }) => { try { await checkEnvironmentCreationPermission(ctx, input.projectId); + if (IS_CLOUD) { + await assertEnvironmentLimit( + ctx.session.activeOrganizationId, + input.projectId, + ); + } if (input.name === "production") { throw new TRPCError({ diff --git a/apps/dokploy/server/api/routers/organization.ts b/apps/dokploy/server/api/routers/organization.ts index 6af018ed8..76d6a7235 100644 --- a/apps/dokploy/server/api/routers/organization.ts +++ b/apps/dokploy/server/api/routers/organization.ts @@ -5,6 +5,10 @@ import { and, desc, eq, exists } from "drizzle-orm"; import { nanoid } from "nanoid"; import { z } from "zod"; import { audit } from "@/server/api/utils/audit"; +import { + assertMemberLimit, + assertOrganizationLimit, +} from "@/server/api/utils/plan-limits"; import { invitation, member, @@ -28,6 +32,11 @@ export const organizationRouter = createTRPCRouter({ message: "Only the organization owner can create an organization", }); } + + if (IS_CLOUD) { + await assertOrganizationLimit(ctx.user.id); + } + const result = await db .insert(organization) .values({ @@ -258,6 +267,10 @@ export const organizationRouter = createTRPCRouter({ const orgId = ctx.session.activeOrganizationId; const email = input.email.toLowerCase(); + if (IS_CLOUD) { + await assertMemberLimit(orgId); + } + // Check if user is already a member const existingUser = await db.query.user.findFirst({ where: eq(user.email, email), diff --git a/apps/dokploy/server/api/routers/schedule.ts b/apps/dokploy/server/api/routers/schedule.ts index 8dfa377f0..2bec813a7 100644 --- a/apps/dokploy/server/api/routers/schedule.ts +++ b/apps/dokploy/server/api/routers/schedule.ts @@ -23,6 +23,7 @@ import { TRPCError } from "@trpc/server"; import { asc, desc, eq } from "drizzle-orm"; import { z } from "zod"; import { audit } from "@/server/api/utils/audit"; +import { assertScheduledJobLimit } from "@/server/api/utils/plan-limits"; import { removeJob, schedule } from "@/server/utils/backup"; import { createTRPCRouter, protectedProcedure } from "../trpc"; @@ -35,6 +36,13 @@ export const scheduleRouter = createTRPCRouter({ await checkServicePermissionAndAccess(ctx, serviceId, { schedule: ["create"], }); + if (IS_CLOUD) { + await assertScheduledJobLimit( + ctx.session.activeOrganizationId, + input.applicationId ? "application" : "compose", + serviceId, + ); + } } else { if (input.scheduleType === "dokploy-server" && IS_CLOUD) { throw new TRPCError({ @@ -73,6 +81,14 @@ export const scheduleRouter = createTRPCRouter({ message: "You don't have access to this server.", }); } + + if (IS_CLOUD) { + await assertScheduledJobLimit( + ctx.session.activeOrganizationId, + "server", + input.serverId, + ); + } } } const newSchedule = await createSchedule({ diff --git a/apps/dokploy/server/api/routers/stripe.ts b/apps/dokploy/server/api/routers/stripe.ts index 29f5df10a..e1c7ee7bd 100644 --- a/apps/dokploy/server/api/routers/stripe.ts +++ b/apps/dokploy/server/api/routers/stripe.ts @@ -7,6 +7,7 @@ import { import { TRPCError } from "@trpc/server"; import Stripe from "stripe"; import { z } from "zod"; +import { getCurrentPlan as getCurrentPlanForOrganization } from "@/server/utils/billing"; import { type BillingTier, getStripeItems, @@ -31,44 +32,7 @@ import { export const stripeRouter = createTRPCRouter({ /** Returns the current billing plan for the user's organization. Used to gate features like chat (Startup only). */ getCurrentPlan: protectedProcedure.query(async ({ ctx }) => { - if (!IS_CLOUD) return null; - const owner = await findUserById(ctx.user.ownerId); - if (!owner?.stripeCustomerId) return null; - - const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!, { - apiVersion: "2024-09-30.acacia", - }); - const subscriptions = await stripe.subscriptions.list({ - customer: owner.stripeCustomerId, - status: "active", - expand: ["data.items.data.price"], - }); - const activeSub = subscriptions.data[0]; - if (!activeSub) return null; - - const priceIds = activeSub.items.data.map( - (item) => (item.price as Stripe.Price).id, - ); - if ( - priceIds.some( - (id) => - id === STARTUP_BASE_PRICE_MONTHLY_ID || - id === STARTUP_BASE_PRICE_ANNUAL_ID, - ) - ) { - return "startup" as const; - } - if ( - priceIds.some( - (id) => id === HOBBY_PRICE_MONTHLY_ID || id === HOBBY_PRICE_ANNUAL_ID, - ) - ) { - return "hobby" as const; - } - if (priceIds.some((id) => LEGACY_PRICE_IDS.includes(id))) { - return "legacy" as const; - } - return null; + return getCurrentPlanForOrganization(ctx.session.activeOrganizationId); }), getProducts: adminProcedure.query(async ({ ctx }) => { diff --git a/apps/dokploy/server/api/routers/volume-backups.ts b/apps/dokploy/server/api/routers/volume-backups.ts index 1f589d1e3..4781485fc 100644 --- a/apps/dokploy/server/api/routers/volume-backups.ts +++ b/apps/dokploy/server/api/routers/volume-backups.ts @@ -27,6 +27,7 @@ import { observable } from "@trpc/server/observable"; import { desc, eq } from "drizzle-orm"; import { z } from "zod"; import { audit } from "@/server/api/utils/audit"; +import { assertVolumeBackupLimit } from "@/server/api/utils/plan-limits"; import { removeJob, schedule, updateJob } from "@/server/utils/backup"; import { createTRPCRouter, protectedProcedure, withPermission } from "../trpc"; @@ -69,20 +70,33 @@ export const volumeBackupsRouter = createTRPCRouter({ create: protectedProcedure .input(createVolumeBackupSchema) .mutation(async ({ input, ctx }) => { - const serviceId = - input.applicationId || - input.postgresId || - input.mysqlId || - input.mariadbId || - input.mongoId || - input.redisId || - input.libsqlId || - input.composeId; + const serviceType = ( + [ + "application", + "postgres", + "mysql", + "mariadb", + "mongo", + "redis", + "libsql", + "compose", + ] as const + ).find((type) => input[`${type}Id`]); + const serviceId = serviceType ? input[`${serviceType}Id`] : undefined; if (serviceId) { await checkServicePermissionAndAccess(ctx, serviceId, { volumeBackup: ["create"], }); } + if (IS_CLOUD && serviceType && serviceId) { + const existingVolumeBackups = await db.query.volumeBackups.findMany({ + where: eq(volumeBackups[`${serviceType}Id`], serviceId), + }); + await assertVolumeBackupLimit( + ctx.session.activeOrganizationId, + existingVolumeBackups.length, + ); + } const newVolumeBackup = await createVolumeBackup(input); if (newVolumeBackup?.enabled) { diff --git a/apps/dokploy/server/api/utils/plan-limits.ts b/apps/dokploy/server/api/utils/plan-limits.ts new file mode 100644 index 000000000..31223164d --- /dev/null +++ b/apps/dokploy/server/api/utils/plan-limits.ts @@ -0,0 +1,130 @@ +import { db } from "@dokploy/server/db"; +import { + environments, + member, + organization, + schedules, +} from "@dokploy/server/db/schema"; +import { TRPCError } from "@trpc/server"; +import { eq } from "drizzle-orm"; +import { getCurrentPlan, getCurrentPlanForUser } from "@/server/utils/billing"; + +export type PlanLimitResource = + | "organization" + | "member" + | "environment" + | "volumeBackup" + | "databaseBackup" + | "scheduledJob"; + +const UNLIMITED = Number.POSITIVE_INFINITY; + +export const PLAN_LIMITS: Record< + "hobby" | "startup" | "legacy", + Record +> = { + hobby: { + organization: 1, + member: 1, + environment: 2, + volumeBackup: 1, + databaseBackup: 1, + scheduledJob: 1, + }, + startup: { + organization: 3, + member: UNLIMITED, + environment: UNLIMITED, + volumeBackup: UNLIMITED, + databaseBackup: UNLIMITED, + scheduledJob: UNLIMITED, + }, + legacy: { + organization: UNLIMITED, + member: UNLIMITED, + environment: UNLIMITED, + volumeBackup: UNLIMITED, + databaseBackup: UNLIMITED, + scheduledJob: UNLIMITED, + }, +}; + +const resourceLabels: Record = { + organization: "organizations", + member: "users", + environment: "environments per project", + volumeBackup: "volume backups per application", + databaseBackup: "backups per database", + scheduledJob: "scheduled jobs per service", +}; + +const assertLimitForPlan = ( + plan: "hobby" | "startup" | "legacy" | null, + resource: PlanLimitResource, + currentCount: number, +) => { + const limit = PLAN_LIMITS[plan ?? "legacy"][resource]; + + if (currentCount >= limit) { + throw new TRPCError({ + code: "FORBIDDEN", + message: `You've reached your plan's limit of ${limit} ${resourceLabels[resource]}. Upgrade your plan to add more.`, + }); + } +}; + +export const assertOrganizationLimit = async (userId: string) => { + const plan = await getCurrentPlanForUser(userId); + const organizations = await db.query.organization.findMany({ + where: eq(organization.ownerId, userId), + }); + assertLimitForPlan(plan, "organization", organizations.length); +}; + +export const assertMemberLimit = async (organizationId: string) => { + const plan = await getCurrentPlan(organizationId); + const members = await db.query.member.findMany({ + where: eq(member.organizationId, organizationId), + }); + assertLimitForPlan(plan, "member", members.length); +}; + +export const assertEnvironmentLimit = async ( + organizationId: string, + projectId: string, +) => { + const plan = await getCurrentPlan(organizationId); + const envs = await db.query.environments.findMany({ + where: eq(environments.projectId, projectId), + }); + assertLimitForPlan(plan, "environment", envs.length); +}; + +export const assertVolumeBackupLimit = async ( + organizationId: string, + currentCount: number, +) => { + const plan = await getCurrentPlan(organizationId); + assertLimitForPlan(plan, "volumeBackup", currentCount); +}; + +export const assertDatabaseBackupLimit = async ( + organizationId: string, + currentCount: number, +) => { + const plan = await getCurrentPlan(organizationId); + assertLimitForPlan(plan, "databaseBackup", currentCount); +}; + +export const assertScheduledJobLimit = async ( + organizationId: string, + scheduleType: "application" | "compose" | "server", + serviceId: string, +) => { + const plan = await getCurrentPlan(organizationId); + const column = `${scheduleType}Id` as const; + const rows = await db.query.schedules.findMany({ + where: eq(schedules[column], serviceId), + }); + assertLimitForPlan(plan, "scheduledJob", rows.length); +}; diff --git a/apps/dokploy/server/utils/billing.ts b/apps/dokploy/server/utils/billing.ts new file mode 100644 index 000000000..532878466 --- /dev/null +++ b/apps/dokploy/server/utils/billing.ts @@ -0,0 +1,69 @@ +import { findUserById, IS_CLOUD } from "@dokploy/server"; +import { getOrganizationOwnerId } from "@dokploy/server/services/proprietary/sso"; +import Stripe from "stripe"; +import { + HOBBY_PRICE_ANNUAL_ID, + HOBBY_PRICE_MONTHLY_ID, + LEGACY_PRICE_IDS, + STARTUP_BASE_PRICE_ANNUAL_ID, + STARTUP_BASE_PRICE_MONTHLY_ID, +} from "@/server/utils/stripe"; + +export type BillingPlan = "legacy" | "hobby" | "startup"; + +export const getCurrentPlanForUser = async ( + userId: string, +): Promise => { + if (!IS_CLOUD) return null; + + const owner = await findUserById(userId); + if (!owner?.stripeCustomerId) return null; + + const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!, { + apiVersion: "2024-09-30.acacia", + }); + const subscriptions = await stripe.subscriptions.list({ + customer: owner.stripeCustomerId, + status: "active", + expand: ["data.items.data.price"], + }); + const activeSub = subscriptions.data[0]; + if (!activeSub) return null; + + const priceIds = activeSub.items.data.map( + (item) => (item.price as Stripe.Price).id, + ); + + if ( + priceIds.some( + (id) => + id === STARTUP_BASE_PRICE_MONTHLY_ID || + id === STARTUP_BASE_PRICE_ANNUAL_ID, + ) + ) { + return "startup"; + } + if ( + priceIds.some( + (id) => id === HOBBY_PRICE_MONTHLY_ID || id === HOBBY_PRICE_ANNUAL_ID, + ) + ) { + return "hobby"; + } + if (priceIds.some((id) => LEGACY_PRICE_IDS.includes(id))) { + return "legacy"; + } + + return null; +}; + +export const getCurrentPlan = async ( + organizationId: string, +): Promise => { + if (!IS_CLOUD) return null; + + const ownerId = await getOrganizationOwnerId(organizationId); + if (!ownerId) return null; + + return getCurrentPlanForUser(ownerId); +}; diff --git a/packages/server/src/services/environment.ts b/packages/server/src/services/environment.ts index 097dc1bd3..09854bf40 100644 --- a/packages/server/src/services/environment.ts +++ b/packages/server/src/services/environment.ts @@ -308,6 +308,48 @@ export const duplicateEnvironment = async ( return newEnvironment; }; +interface EnvironmentWithServices { + applications: { applicationId: string }[]; + compose: { composeId: string }[]; + libsql: { libsqlId: string }[]; + mariadb: { mariadbId: string }[]; + mongo: { mongoId: string }[]; + mysql: { mysqlId: string }[]; + postgres: { postgresId: string }[]; + redis: { redisId: string }[]; +} + +export const filterEnvironmentServices = ( + environment: T, + accessedServices: string[], +): T => ({ + ...environment, + applications: environment.applications.filter((app) => + accessedServices.includes(app.applicationId), + ), + compose: environment.compose.filter((comp) => + accessedServices.includes(comp.composeId), + ), + libsql: environment.libsql.filter((db) => + accessedServices.includes(db.libsqlId), + ), + mariadb: environment.mariadb.filter((db) => + accessedServices.includes(db.mariadbId), + ), + mongo: environment.mongo.filter((db) => + accessedServices.includes(db.mongoId), + ), + mysql: environment.mysql.filter((db) => + accessedServices.includes(db.mysqlId), + ), + postgres: environment.postgres.filter((db) => + accessedServices.includes(db.postgresId), + ), + redis: environment.redis.filter((db) => + accessedServices.includes(db.redisId), + ), +}); + export const createProductionEnvironment = async (projectId: string) => { const newEnvironment = await db .insert(environments)