mirror of
https://github.com/Dokploy/dokploy.git
synced 2026-06-18 21:55:24 +02:00
feat: add accessedEnvironments field to user and member schemas, enhancing permission management for environment access
This commit is contained in:
@@ -155,6 +155,7 @@ export const extractServices = (data: Environment | undefined) => {
|
||||
|
||||
const addPermissions = z.object({
|
||||
accessedProjects: z.array(z.string()).optional(),
|
||||
accessedEnvironments: z.array(z.string()).optional(),
|
||||
accessedServices: z.array(z.string()).optional(),
|
||||
canCreateProjects: z.boolean().optional().default(false),
|
||||
canCreateServices: z.boolean().optional().default(false),
|
||||
@@ -200,6 +201,7 @@ export const AddUserPermissions = ({ userId }: Props) => {
|
||||
if (data) {
|
||||
form.reset({
|
||||
accessedProjects: data.accessedProjects || [],
|
||||
accessedEnvironments: data.accessedEnvironments || [],
|
||||
accessedServices: data.accessedServices || [],
|
||||
canCreateProjects: data.canCreateProjects,
|
||||
canCreateServices: data.canCreateServices,
|
||||
@@ -223,6 +225,7 @@ export const AddUserPermissions = ({ userId }: Props) => {
|
||||
canDeleteProjects: data.canDeleteProjects,
|
||||
canAccessToTraefikFiles: data.canAccessToTraefikFiles,
|
||||
accessedProjects: data.accessedProjects || [],
|
||||
accessedEnvironments: data.accessedEnvironments || [],
|
||||
accessedServices: data.accessedServices || [],
|
||||
canAccessToDocker: data.canAccessToDocker,
|
||||
canAccessToAPI: data.canAccessToAPI,
|
||||
@@ -456,91 +459,317 @@ export const AddUserPermissions = ({ userId }: Props) => {
|
||||
No projects found
|
||||
</p>
|
||||
)}
|
||||
<div className="grid md:grid-cols-2 gap-4">
|
||||
{projects?.map((item, index) => {
|
||||
const applications = item.environments.flatMap((env) =>
|
||||
extractServices(env),
|
||||
);
|
||||
<div className="grid md:grid-cols-1 gap-4">
|
||||
{projects?.map((project, projectIndex) => {
|
||||
return (
|
||||
<FormField
|
||||
key={`project-${index}`}
|
||||
key={`project-${projectIndex}`}
|
||||
control={form.control}
|
||||
name="accessedProjects"
|
||||
render={({ field }) => {
|
||||
return (
|
||||
<FormItem
|
||||
key={item.projectId}
|
||||
className="flex flex-col items-start space-x-4 rounded-lg p-4 border"
|
||||
key={project.projectId}
|
||||
className="flex flex-col items-start rounded-lg p-4 border"
|
||||
>
|
||||
<div className="flex flex-row gap-4">
|
||||
{/* Project Header */}
|
||||
<div className="flex flex-row gap-4 items-center w-full">
|
||||
<FormControl>
|
||||
<Checkbox
|
||||
checked={field.value?.includes(
|
||||
item.projectId,
|
||||
project.projectId,
|
||||
)}
|
||||
onCheckedChange={(checked) => {
|
||||
return checked
|
||||
? field.onChange([
|
||||
...(field.value || []),
|
||||
item.projectId,
|
||||
])
|
||||
: field.onChange(
|
||||
field.value?.filter(
|
||||
(value) =>
|
||||
value !== item.projectId,
|
||||
),
|
||||
if (checked) {
|
||||
// Add the project
|
||||
field.onChange([
|
||||
...(field.value || []),
|
||||
project.projectId,
|
||||
]);
|
||||
} else {
|
||||
// Remove the project
|
||||
field.onChange(
|
||||
field.value?.filter(
|
||||
(value) =>
|
||||
value !== project.projectId,
|
||||
),
|
||||
);
|
||||
|
||||
// Also remove all environments and services from this project
|
||||
const currentEnvs =
|
||||
form.getValues(
|
||||
"accessedEnvironments",
|
||||
) || [];
|
||||
const currentServices =
|
||||
form.getValues(
|
||||
"accessedServices",
|
||||
) || [];
|
||||
|
||||
// Get all environment IDs from this project
|
||||
const projectEnvIds =
|
||||
project.environments.map(
|
||||
(env) => env.environmentId,
|
||||
);
|
||||
|
||||
// Get all service IDs from this project
|
||||
const projectServiceIds =
|
||||
project.environments.flatMap(
|
||||
(env) =>
|
||||
extractServices(env).map(
|
||||
(service) => service.id,
|
||||
),
|
||||
);
|
||||
|
||||
// Remove environments and services from this project
|
||||
form.setValue(
|
||||
"accessedEnvironments",
|
||||
currentEnvs.filter(
|
||||
(envId) =>
|
||||
!projectEnvIds.includes(envId),
|
||||
),
|
||||
);
|
||||
form.setValue(
|
||||
"accessedServices",
|
||||
currentServices.filter(
|
||||
(serviceId) =>
|
||||
!projectServiceIds.includes(
|
||||
serviceId,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormLabel className="text-sm font-medium text-primary">
|
||||
{item.name}
|
||||
<FormLabel className="text-base font-semibold text-primary">
|
||||
{project.name}
|
||||
</FormLabel>
|
||||
</div>
|
||||
{applications.length === 0 && (
|
||||
<p className="text-sm text-muted-foreground">
|
||||
No services found
|
||||
</p>
|
||||
)}
|
||||
{applications?.map((item, index) => (
|
||||
<FormField
|
||||
key={`project-${index}`}
|
||||
control={form.control}
|
||||
name="accessedServices"
|
||||
render={({ field }) => {
|
||||
|
||||
{/* Environments */}
|
||||
<div className="ml-6 w-full space-y-3">
|
||||
{project.environments.length === 0 && (
|
||||
<p className="text-sm text-muted-foreground">
|
||||
No environments found
|
||||
</p>
|
||||
)}
|
||||
{project.environments.map(
|
||||
(environment, envIndex) => {
|
||||
const services =
|
||||
extractServices(environment);
|
||||
return (
|
||||
<FormItem
|
||||
key={item.id}
|
||||
className="flex flex-row items-start space-x-3 space-y-0"
|
||||
<div
|
||||
key={`env-${envIndex}`}
|
||||
className="border-l-2 border-muted pl-4"
|
||||
>
|
||||
<FormControl>
|
||||
<Checkbox
|
||||
checked={field.value?.includes(
|
||||
item.id,
|
||||
)}
|
||||
onCheckedChange={(checked) => {
|
||||
return checked
|
||||
? field.onChange([
|
||||
...(field.value || []),
|
||||
item.id,
|
||||
])
|
||||
: field.onChange(
|
||||
field.value?.filter(
|
||||
(value) =>
|
||||
value !== item.id,
|
||||
),
|
||||
{/* Environment Header with Checkbox */}
|
||||
<FormField
|
||||
key={`env-${envIndex}`}
|
||||
control={form.control}
|
||||
name="accessedEnvironments"
|
||||
render={({ field: envField }) => (
|
||||
<FormItem className="flex flex-row items-center space-x-3 space-y-0 mb-2">
|
||||
<FormControl>
|
||||
<Checkbox
|
||||
checked={envField.value?.includes(
|
||||
environment.environmentId,
|
||||
)}
|
||||
onCheckedChange={(
|
||||
checked,
|
||||
) => {
|
||||
if (checked) {
|
||||
// Add the environment
|
||||
envField.onChange([
|
||||
...(envField.value ||
|
||||
[]),
|
||||
environment.environmentId,
|
||||
]);
|
||||
|
||||
// Auto-select the project if not already selected
|
||||
const currentProjects =
|
||||
form.getValues(
|
||||
"accessedProjects",
|
||||
) || [];
|
||||
if (
|
||||
!currentProjects.includes(
|
||||
project.projectId,
|
||||
)
|
||||
) {
|
||||
form.setValue(
|
||||
"accessedProjects",
|
||||
[
|
||||
...currentProjects,
|
||||
project.projectId,
|
||||
],
|
||||
);
|
||||
}
|
||||
} else {
|
||||
// Remove the environment
|
||||
envField.onChange(
|
||||
envField.value?.filter(
|
||||
(value) =>
|
||||
value !==
|
||||
environment.environmentId,
|
||||
),
|
||||
);
|
||||
|
||||
// Also remove all services from this environment
|
||||
const currentServices =
|
||||
form.getValues(
|
||||
"accessedServices",
|
||||
) || [];
|
||||
const environmentServiceIds =
|
||||
services.map(
|
||||
(service) =>
|
||||
service.id,
|
||||
);
|
||||
|
||||
form.setValue(
|
||||
"accessedServices",
|
||||
currentServices.filter(
|
||||
(serviceId) =>
|
||||
!environmentServiceIds.includes(
|
||||
serviceId,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</FormControl>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-2 h-2 bg-blue-500 rounded-full" />
|
||||
<FormLabel className="text-sm font-medium text-foreground cursor-pointer">
|
||||
{environment.name}
|
||||
</FormLabel>
|
||||
<span className="text-xs text-muted-foreground">
|
||||
({services.length} services)
|
||||
</span>
|
||||
</div>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
{/* Services */}
|
||||
<div className="ml-4 space-y-2">
|
||||
{services.length === 0 && (
|
||||
<p className="text-xs text-muted-foreground">
|
||||
No services found
|
||||
</p>
|
||||
)}
|
||||
{services.map(
|
||||
(service, serviceIndex) => (
|
||||
<FormField
|
||||
key={`service-${serviceIndex}`}
|
||||
control={form.control}
|
||||
name="accessedServices"
|
||||
render={({
|
||||
field: serviceField,
|
||||
}) => {
|
||||
return (
|
||||
<FormItem
|
||||
key={service.id}
|
||||
className="flex flex-row items-center space-x-3 space-y-0"
|
||||
>
|
||||
<FormControl>
|
||||
<Checkbox
|
||||
checked={serviceField.value?.includes(
|
||||
service.id,
|
||||
)}
|
||||
onCheckedChange={(
|
||||
checked,
|
||||
) => {
|
||||
if (checked) {
|
||||
// Add the service
|
||||
serviceField.onChange(
|
||||
[
|
||||
...(serviceField.value ||
|
||||
[]),
|
||||
service.id,
|
||||
],
|
||||
);
|
||||
|
||||
// Auto-select the environment if not already selected
|
||||
const currentEnvs =
|
||||
form.getValues(
|
||||
"accessedEnvironments",
|
||||
) || [];
|
||||
if (
|
||||
!currentEnvs.includes(
|
||||
environment.environmentId,
|
||||
)
|
||||
) {
|
||||
form.setValue(
|
||||
"accessedEnvironments",
|
||||
[
|
||||
...currentEnvs,
|
||||
environment.environmentId,
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
// Auto-select the project if not already selected
|
||||
const currentProjects =
|
||||
form.getValues(
|
||||
"accessedProjects",
|
||||
) || [];
|
||||
if (
|
||||
!currentProjects.includes(
|
||||
project.projectId,
|
||||
)
|
||||
) {
|
||||
form.setValue(
|
||||
"accessedProjects",
|
||||
[
|
||||
...currentProjects,
|
||||
project.projectId,
|
||||
],
|
||||
);
|
||||
}
|
||||
} else {
|
||||
// Remove the service
|
||||
serviceField.onChange(
|
||||
serviceField.value?.filter(
|
||||
(value) =>
|
||||
value !==
|
||||
service.id,
|
||||
),
|
||||
);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</FormControl>
|
||||
<div className="flex items-center gap-2">
|
||||
<div
|
||||
className={`w-1.5 h-1.5 rounded-full ${
|
||||
service.type ===
|
||||
"application"
|
||||
? "bg-green-500"
|
||||
: service.type ===
|
||||
"compose"
|
||||
? "bg-purple-500"
|
||||
: "bg-orange-500"
|
||||
}`}
|
||||
/>
|
||||
<FormLabel className="text-sm text-muted-foreground cursor-pointer">
|
||||
{service.name}
|
||||
</FormLabel>
|
||||
<span className="text-xs text-muted-foreground/70 capitalize">
|
||||
({service.type})
|
||||
</span>
|
||||
</div>
|
||||
</FormItem>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormLabel className="text-sm text-muted-foreground">
|
||||
{item.name}
|
||||
</FormLabel>
|
||||
</FormItem>
|
||||
}}
|
||||
/>
|
||||
),
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
},
|
||||
)}
|
||||
</div>
|
||||
</FormItem>
|
||||
);
|
||||
}}
|
||||
|
||||
1
apps/dokploy/drizzle/0109_remarkable_sauron.sql
Normal file
1
apps/dokploy/drizzle/0109_remarkable_sauron.sql
Normal file
@@ -0,0 +1 @@
|
||||
ALTER TABLE "member" ADD COLUMN "accessedEnvironments" text[] DEFAULT ARRAY[]::text[] NOT NULL;
|
||||
6495
apps/dokploy/drizzle/meta/0109_snapshot.json
Normal file
6495
apps/dokploy/drizzle/meta/0109_snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -764,6 +764,13 @@
|
||||
"when": 1756955718127,
|
||||
"tag": "0108_lazy_next_avengers",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 109,
|
||||
"version": "7",
|
||||
"when": 1757052053574,
|
||||
"tag": "0109_remarkable_sauron",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -786,40 +786,43 @@ const EnvironmentPage = (
|
||||
<ProjectEnvironment projectId={projectId}>
|
||||
<Button variant="outline">Project Environment</Button>
|
||||
</ProjectEnvironment>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button>
|
||||
<PlusIcon className="h-4 w-4" />
|
||||
Create Service
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent
|
||||
className="w-[200px] space-y-2"
|
||||
align="end"
|
||||
>
|
||||
<DropdownMenuLabel className="text-sm font-normal">
|
||||
Actions
|
||||
</DropdownMenuLabel>
|
||||
<DropdownMenuSeparator />
|
||||
<AddApplication
|
||||
projectName={projectData?.name}
|
||||
environmentId={environmentId}
|
||||
/>
|
||||
<AddDatabase
|
||||
projectName={projectData?.name}
|
||||
environmentId={environmentId}
|
||||
/>
|
||||
<AddCompose
|
||||
projectName={projectData?.name}
|
||||
environmentId={environmentId}
|
||||
/>
|
||||
<AddTemplate environmentId={environmentId} />
|
||||
<AddAiAssistant
|
||||
projectName={projectData?.name}
|
||||
environmentId={environmentId}
|
||||
/>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
{auth?.role === "owner" ||
|
||||
(auth?.canCreateServices && (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button>
|
||||
<PlusIcon className="h-4 w-4" />
|
||||
Create Service
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent
|
||||
className="w-[200px] space-y-2"
|
||||
align="end"
|
||||
>
|
||||
<DropdownMenuLabel className="text-sm font-normal">
|
||||
Actions
|
||||
</DropdownMenuLabel>
|
||||
<DropdownMenuSeparator />
|
||||
<AddApplication
|
||||
projectName={projectData?.name}
|
||||
environmentId={environmentId}
|
||||
/>
|
||||
<AddDatabase
|
||||
projectName={projectData?.name}
|
||||
environmentId={environmentId}
|
||||
/>
|
||||
<AddCompose
|
||||
projectName={projectData?.name}
|
||||
environmentId={environmentId}
|
||||
/>
|
||||
<AddTemplate environmentId={environmentId} />
|
||||
<AddAiAssistant
|
||||
projectName={projectData?.name}
|
||||
environmentId={environmentId}
|
||||
/>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -4,6 +4,7 @@ import {
|
||||
duplicateEnvironment,
|
||||
findEnvironmentById,
|
||||
findEnvironmentsByProjectId,
|
||||
findMemberById,
|
||||
updateEnvironmentById,
|
||||
} from "@dokploy/server";
|
||||
import { TRPCError } from "@trpc/server";
|
||||
@@ -51,6 +52,48 @@ export const environmentRouter = createTRPCRouter({
|
||||
message: "You are not allowed to access this environment",
|
||||
});
|
||||
}
|
||||
|
||||
// Check environment access and filter services for members
|
||||
if (ctx.user.role === "member") {
|
||||
const { accessedEnvironments, accessedServices } =
|
||||
await findMemberById(ctx.user.id, ctx.session.activeOrganizationId);
|
||||
|
||||
if (!accessedEnvironments.includes(environment.environmentId)) {
|
||||
throw new TRPCError({
|
||||
code: "FORBIDDEN",
|
||||
message: "You are not allowed to access this environment",
|
||||
});
|
||||
}
|
||||
|
||||
// Filter services based on member permissions
|
||||
const filteredEnvironment = {
|
||||
...environment,
|
||||
applications: environment.applications.filter((app) =>
|
||||
accessedServices.includes(app.applicationId),
|
||||
),
|
||||
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),
|
||||
),
|
||||
compose: environment.compose.filter((comp) =>
|
||||
accessedServices.includes(comp.composeId),
|
||||
),
|
||||
};
|
||||
|
||||
return filteredEnvironment;
|
||||
}
|
||||
|
||||
return environment;
|
||||
} catch (error) {
|
||||
throw new TRPCError({
|
||||
@@ -65,6 +108,8 @@ export const environmentRouter = createTRPCRouter({
|
||||
.query(async ({ input, ctx }) => {
|
||||
try {
|
||||
const environments = await findEnvironmentsByProjectId(input.projectId);
|
||||
|
||||
// Check organization access
|
||||
if (
|
||||
environments.some(
|
||||
(environment) =>
|
||||
@@ -77,6 +122,22 @@ export const environmentRouter = createTRPCRouter({
|
||||
message: "You are not allowed to access this environment",
|
||||
});
|
||||
}
|
||||
|
||||
// Filter environments for members based on their permissions
|
||||
if (ctx.user.role === "member") {
|
||||
const { accessedEnvironments } = await findMemberById(
|
||||
ctx.user.id,
|
||||
ctx.session.activeOrganizationId,
|
||||
);
|
||||
|
||||
// Filter environments to only show those the member has access to
|
||||
const filteredEnvironments = environments.filter((environment) =>
|
||||
accessedEnvironments.includes(environment.environmentId),
|
||||
);
|
||||
|
||||
return filteredEnvironments;
|
||||
}
|
||||
|
||||
return environments;
|
||||
} catch (error) {
|
||||
throw new TRPCError({
|
||||
@@ -100,6 +161,22 @@ export const environmentRouter = createTRPCRouter({
|
||||
message: "You are not allowed to access this environment",
|
||||
});
|
||||
}
|
||||
|
||||
// Check environment access for members
|
||||
if (ctx.user.role === "member") {
|
||||
const { accessedEnvironments } = await findMemberById(
|
||||
ctx.user.id,
|
||||
ctx.session.activeOrganizationId,
|
||||
);
|
||||
|
||||
if (!accessedEnvironments.includes(environment.environmentId)) {
|
||||
throw new TRPCError({
|
||||
code: "FORBIDDEN",
|
||||
message: "You are not allowed to delete this environment",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const deletedEnvironment = await deleteEnvironment(input.environmentId);
|
||||
return deletedEnvironment;
|
||||
} catch (error) {
|
||||
@@ -125,6 +202,24 @@ export const environmentRouter = createTRPCRouter({
|
||||
message: "You are not allowed to access this environment",
|
||||
});
|
||||
}
|
||||
|
||||
// Check environment access for members
|
||||
if (ctx.user.role === "member") {
|
||||
const { accessedEnvironments } = await findMemberById(
|
||||
ctx.user.id,
|
||||
ctx.session.activeOrganizationId,
|
||||
);
|
||||
|
||||
if (
|
||||
!accessedEnvironments.includes(currentEnvironment.environmentId)
|
||||
) {
|
||||
throw new TRPCError({
|
||||
code: "FORBIDDEN",
|
||||
message: "You are not allowed to update this environment",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const environment = await updateEnvironmentById(
|
||||
environmentId,
|
||||
updateData,
|
||||
@@ -152,6 +247,22 @@ export const environmentRouter = createTRPCRouter({
|
||||
message: "You are not allowed to access this environment",
|
||||
});
|
||||
}
|
||||
|
||||
// Check environment access for members
|
||||
if (ctx.user.role === "member") {
|
||||
const { accessedEnvironments } = await findMemberById(
|
||||
ctx.user.id,
|
||||
ctx.session.activeOrganizationId,
|
||||
);
|
||||
|
||||
if (!accessedEnvironments.includes(environment.environmentId)) {
|
||||
throw new TRPCError({
|
||||
code: "FORBIDDEN",
|
||||
message: "You are not allowed to duplicate this environment",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const duplicatedEnvironment = await duplicateEnvironment(input);
|
||||
return duplicatedEnvironment;
|
||||
} catch (error) {
|
||||
|
||||
@@ -44,6 +44,7 @@ import {
|
||||
apiUpdateProject,
|
||||
applications,
|
||||
compose,
|
||||
environments,
|
||||
mariadb,
|
||||
mongo,
|
||||
mysql,
|
||||
@@ -178,15 +179,22 @@ export const projectRouter = createTRPCRouter({
|
||||
}),
|
||||
all: protectedProcedure.query(async ({ ctx }) => {
|
||||
if (ctx.user.role === "member") {
|
||||
const { accessedProjects, accessedServices } = await findMemberById(
|
||||
ctx.user.id,
|
||||
ctx.session.activeOrganizationId,
|
||||
);
|
||||
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(
|
||||
@@ -197,6 +205,7 @@ export const projectRouter = createTRPCRouter({
|
||||
),
|
||||
with: {
|
||||
environments: {
|
||||
where: environmentFilter,
|
||||
with: {
|
||||
applications: {
|
||||
where: buildServiceFilter(
|
||||
|
||||
@@ -112,6 +112,10 @@ export const member = pgTable("member", {
|
||||
.array()
|
||||
.notNull()
|
||||
.default(sql`ARRAY[]::text[]`),
|
||||
accessedEnvironments: text("accessedEnvironments")
|
||||
.array()
|
||||
.notNull()
|
||||
.default(sql`ARRAY[]::text[]`),
|
||||
accessedServices: text("accesedServices")
|
||||
.array()
|
||||
.notNull()
|
||||
|
||||
@@ -175,6 +175,7 @@ export const apiAssignPermissions = createSchema
|
||||
})
|
||||
.extend({
|
||||
accessedProjects: z.array(z.string()).optional(),
|
||||
accessedEnvironments: z.array(z.string()).optional(),
|
||||
accessedServices: z.array(z.string()).optional(),
|
||||
canCreateProjects: z.boolean().optional(),
|
||||
canCreateServices: z.boolean().optional(),
|
||||
|
||||
Reference in New Issue
Block a user