mirror of
https://github.com/andwati/dokploy.git
synced 2026-11-09 21:45:17 +00: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({
|
const addPermissions = z.object({
|
||||||
accessedProjects: z.array(z.string()).optional(),
|
accessedProjects: z.array(z.string()).optional(),
|
||||||
|
accessedEnvironments: z.array(z.string()).optional(),
|
||||||
accessedServices: z.array(z.string()).optional(),
|
accessedServices: z.array(z.string()).optional(),
|
||||||
canCreateProjects: z.boolean().optional().default(false),
|
canCreateProjects: z.boolean().optional().default(false),
|
||||||
canCreateServices: z.boolean().optional().default(false),
|
canCreateServices: z.boolean().optional().default(false),
|
||||||
@@ -200,6 +201,7 @@ export const AddUserPermissions = ({ userId }: Props) => {
|
|||||||
if (data) {
|
if (data) {
|
||||||
form.reset({
|
form.reset({
|
||||||
accessedProjects: data.accessedProjects || [],
|
accessedProjects: data.accessedProjects || [],
|
||||||
|
accessedEnvironments: data.accessedEnvironments || [],
|
||||||
accessedServices: data.accessedServices || [],
|
accessedServices: data.accessedServices || [],
|
||||||
canCreateProjects: data.canCreateProjects,
|
canCreateProjects: data.canCreateProjects,
|
||||||
canCreateServices: data.canCreateServices,
|
canCreateServices: data.canCreateServices,
|
||||||
@@ -223,6 +225,7 @@ export const AddUserPermissions = ({ userId }: Props) => {
|
|||||||
canDeleteProjects: data.canDeleteProjects,
|
canDeleteProjects: data.canDeleteProjects,
|
||||||
canAccessToTraefikFiles: data.canAccessToTraefikFiles,
|
canAccessToTraefikFiles: data.canAccessToTraefikFiles,
|
||||||
accessedProjects: data.accessedProjects || [],
|
accessedProjects: data.accessedProjects || [],
|
||||||
|
accessedEnvironments: data.accessedEnvironments || [],
|
||||||
accessedServices: data.accessedServices || [],
|
accessedServices: data.accessedServices || [],
|
||||||
canAccessToDocker: data.canAccessToDocker,
|
canAccessToDocker: data.canAccessToDocker,
|
||||||
canAccessToAPI: data.canAccessToAPI,
|
canAccessToAPI: data.canAccessToAPI,
|
||||||
@@ -456,91 +459,317 @@ export const AddUserPermissions = ({ userId }: Props) => {
|
|||||||
No projects found
|
No projects found
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
<div className="grid md:grid-cols-2 gap-4">
|
<div className="grid md:grid-cols-1 gap-4">
|
||||||
{projects?.map((item, index) => {
|
{projects?.map((project, projectIndex) => {
|
||||||
const applications = item.environments.flatMap((env) =>
|
|
||||||
extractServices(env),
|
|
||||||
);
|
|
||||||
return (
|
return (
|
||||||
<FormField
|
<FormField
|
||||||
key={`project-${index}`}
|
key={`project-${projectIndex}`}
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="accessedProjects"
|
name="accessedProjects"
|
||||||
render={({ field }) => {
|
render={({ field }) => {
|
||||||
return (
|
return (
|
||||||
<FormItem
|
<FormItem
|
||||||
key={item.projectId}
|
key={project.projectId}
|
||||||
className="flex flex-col items-start space-x-4 rounded-lg p-4 border"
|
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>
|
<FormControl>
|
||||||
<Checkbox
|
<Checkbox
|
||||||
checked={field.value?.includes(
|
checked={field.value?.includes(
|
||||||
item.projectId,
|
project.projectId,
|
||||||
)}
|
)}
|
||||||
onCheckedChange={(checked) => {
|
onCheckedChange={(checked) => {
|
||||||
return checked
|
if (checked) {
|
||||||
? field.onChange([
|
// Add the project
|
||||||
...(field.value || []),
|
field.onChange([
|
||||||
item.projectId,
|
...(field.value || []),
|
||||||
])
|
project.projectId,
|
||||||
: field.onChange(
|
]);
|
||||||
field.value?.filter(
|
} else {
|
||||||
(value) =>
|
// Remove the project
|
||||||
value !== item.projectId,
|
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>
|
</FormControl>
|
||||||
<FormLabel className="text-sm font-medium text-primary">
|
<FormLabel className="text-base font-semibold text-primary">
|
||||||
{item.name}
|
{project.name}
|
||||||
</FormLabel>
|
</FormLabel>
|
||||||
</div>
|
</div>
|
||||||
{applications.length === 0 && (
|
|
||||||
<p className="text-sm text-muted-foreground">
|
{/* Environments */}
|
||||||
No services found
|
<div className="ml-6 w-full space-y-3">
|
||||||
</p>
|
{project.environments.length === 0 && (
|
||||||
)}
|
<p className="text-sm text-muted-foreground">
|
||||||
{applications?.map((item, index) => (
|
No environments found
|
||||||
<FormField
|
</p>
|
||||||
key={`project-${index}`}
|
)}
|
||||||
control={form.control}
|
{project.environments.map(
|
||||||
name="accessedServices"
|
(environment, envIndex) => {
|
||||||
render={({ field }) => {
|
const services =
|
||||||
|
extractServices(environment);
|
||||||
return (
|
return (
|
||||||
<FormItem
|
<div
|
||||||
key={item.id}
|
key={`env-${envIndex}`}
|
||||||
className="flex flex-row items-start space-x-3 space-y-0"
|
className="border-l-2 border-muted pl-4"
|
||||||
>
|
>
|
||||||
<FormControl>
|
{/* Environment Header with Checkbox */}
|
||||||
<Checkbox
|
<FormField
|
||||||
checked={field.value?.includes(
|
key={`env-${envIndex}`}
|
||||||
item.id,
|
control={form.control}
|
||||||
)}
|
name="accessedEnvironments"
|
||||||
onCheckedChange={(checked) => {
|
render={({ field: envField }) => (
|
||||||
return checked
|
<FormItem className="flex flex-row items-center space-x-3 space-y-0 mb-2">
|
||||||
? field.onChange([
|
<FormControl>
|
||||||
...(field.value || []),
|
<Checkbox
|
||||||
item.id,
|
checked={envField.value?.includes(
|
||||||
])
|
environment.environmentId,
|
||||||
: field.onChange(
|
)}
|
||||||
field.value?.filter(
|
onCheckedChange={(
|
||||||
(value) =>
|
checked,
|
||||||
value !== item.id,
|
) => {
|
||||||
),
|
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}
|
</div>
|
||||||
</FormLabel>
|
</div>
|
||||||
</FormItem>
|
|
||||||
);
|
);
|
||||||
}}
|
},
|
||||||
/>
|
)}
|
||||||
))}
|
</div>
|
||||||
</FormItem>
|
</FormItem>
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
ALTER TABLE "member" ADD COLUMN "accessedEnvironments" text[] DEFAULT ARRAY[]::text[] NOT NULL;
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -764,6 +764,13 @@
|
|||||||
"when": 1756955718127,
|
"when": 1756955718127,
|
||||||
"tag": "0108_lazy_next_avengers",
|
"tag": "0108_lazy_next_avengers",
|
||||||
"breakpoints": true
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 109,
|
||||||
|
"version": "7",
|
||||||
|
"when": 1757052053574,
|
||||||
|
"tag": "0109_remarkable_sauron",
|
||||||
|
"breakpoints": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -786,40 +786,43 @@ const EnvironmentPage = (
|
|||||||
<ProjectEnvironment projectId={projectId}>
|
<ProjectEnvironment projectId={projectId}>
|
||||||
<Button variant="outline">Project Environment</Button>
|
<Button variant="outline">Project Environment</Button>
|
||||||
</ProjectEnvironment>
|
</ProjectEnvironment>
|
||||||
<DropdownMenu>
|
{auth?.role === "owner" ||
|
||||||
<DropdownMenuTrigger asChild>
|
(auth?.canCreateServices && (
|
||||||
<Button>
|
<DropdownMenu>
|
||||||
<PlusIcon className="h-4 w-4" />
|
<DropdownMenuTrigger asChild>
|
||||||
Create Service
|
<Button>
|
||||||
</Button>
|
<PlusIcon className="h-4 w-4" />
|
||||||
</DropdownMenuTrigger>
|
Create Service
|
||||||
<DropdownMenuContent
|
</Button>
|
||||||
className="w-[200px] space-y-2"
|
</DropdownMenuTrigger>
|
||||||
align="end"
|
<DropdownMenuContent
|
||||||
>
|
className="w-[200px] space-y-2"
|
||||||
<DropdownMenuLabel className="text-sm font-normal">
|
align="end"
|
||||||
Actions
|
>
|
||||||
</DropdownMenuLabel>
|
<DropdownMenuLabel className="text-sm font-normal">
|
||||||
<DropdownMenuSeparator />
|
Actions
|
||||||
<AddApplication
|
</DropdownMenuLabel>
|
||||||
projectName={projectData?.name}
|
<DropdownMenuSeparator />
|
||||||
environmentId={environmentId}
|
<AddApplication
|
||||||
/>
|
projectName={projectData?.name}
|
||||||
<AddDatabase
|
environmentId={environmentId}
|
||||||
projectName={projectData?.name}
|
/>
|
||||||
environmentId={environmentId}
|
<AddDatabase
|
||||||
/>
|
projectName={projectData?.name}
|
||||||
<AddCompose
|
environmentId={environmentId}
|
||||||
projectName={projectData?.name}
|
/>
|
||||||
environmentId={environmentId}
|
<AddCompose
|
||||||
/>
|
projectName={projectData?.name}
|
||||||
<AddTemplate environmentId={environmentId} />
|
environmentId={environmentId}
|
||||||
<AddAiAssistant
|
/>
|
||||||
projectName={projectData?.name}
|
<AddTemplate environmentId={environmentId} />
|
||||||
environmentId={environmentId}
|
<AddAiAssistant
|
||||||
/>
|
projectName={projectData?.name}
|
||||||
</DropdownMenuContent>
|
environmentId={environmentId}
|
||||||
</DropdownMenu>
|
/>
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import {
|
|||||||
duplicateEnvironment,
|
duplicateEnvironment,
|
||||||
findEnvironmentById,
|
findEnvironmentById,
|
||||||
findEnvironmentsByProjectId,
|
findEnvironmentsByProjectId,
|
||||||
|
findMemberById,
|
||||||
updateEnvironmentById,
|
updateEnvironmentById,
|
||||||
} from "@dokploy/server";
|
} from "@dokploy/server";
|
||||||
import { TRPCError } from "@trpc/server";
|
import { TRPCError } from "@trpc/server";
|
||||||
@@ -51,6 +52,48 @@ export const environmentRouter = createTRPCRouter({
|
|||||||
message: "You are not allowed to access this environment",
|
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;
|
return environment;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
@@ -65,6 +108,8 @@ export const environmentRouter = createTRPCRouter({
|
|||||||
.query(async ({ input, ctx }) => {
|
.query(async ({ input, ctx }) => {
|
||||||
try {
|
try {
|
||||||
const environments = await findEnvironmentsByProjectId(input.projectId);
|
const environments = await findEnvironmentsByProjectId(input.projectId);
|
||||||
|
|
||||||
|
// Check organization access
|
||||||
if (
|
if (
|
||||||
environments.some(
|
environments.some(
|
||||||
(environment) =>
|
(environment) =>
|
||||||
@@ -77,6 +122,22 @@ export const environmentRouter = createTRPCRouter({
|
|||||||
message: "You are not allowed to access this environment",
|
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;
|
return environments;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
@@ -100,6 +161,22 @@ export const environmentRouter = createTRPCRouter({
|
|||||||
message: "You are not allowed to access this environment",
|
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);
|
const deletedEnvironment = await deleteEnvironment(input.environmentId);
|
||||||
return deletedEnvironment;
|
return deletedEnvironment;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -125,6 +202,24 @@ export const environmentRouter = createTRPCRouter({
|
|||||||
message: "You are not allowed to access this environment",
|
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(
|
const environment = await updateEnvironmentById(
|
||||||
environmentId,
|
environmentId,
|
||||||
updateData,
|
updateData,
|
||||||
@@ -152,6 +247,22 @@ export const environmentRouter = createTRPCRouter({
|
|||||||
message: "You are not allowed to access this environment",
|
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);
|
const duplicatedEnvironment = await duplicateEnvironment(input);
|
||||||
return duplicatedEnvironment;
|
return duplicatedEnvironment;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
@@ -44,6 +44,7 @@ import {
|
|||||||
apiUpdateProject,
|
apiUpdateProject,
|
||||||
applications,
|
applications,
|
||||||
compose,
|
compose,
|
||||||
|
environments,
|
||||||
mariadb,
|
mariadb,
|
||||||
mongo,
|
mongo,
|
||||||
mysql,
|
mysql,
|
||||||
@@ -178,15 +179,22 @@ export const projectRouter = createTRPCRouter({
|
|||||||
}),
|
}),
|
||||||
all: protectedProcedure.query(async ({ ctx }) => {
|
all: protectedProcedure.query(async ({ ctx }) => {
|
||||||
if (ctx.user.role === "member") {
|
if (ctx.user.role === "member") {
|
||||||
const { accessedProjects, accessedServices } = await findMemberById(
|
const { accessedProjects, accessedEnvironments, accessedServices } =
|
||||||
ctx.user.id,
|
await findMemberById(ctx.user.id, ctx.session.activeOrganizationId);
|
||||||
ctx.session.activeOrganizationId,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (accessedProjects.length === 0) {
|
if (accessedProjects.length === 0) {
|
||||||
return [];
|
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({
|
return await db.query.projects.findMany({
|
||||||
where: and(
|
where: and(
|
||||||
sql`${projects.projectId} IN (${sql.join(
|
sql`${projects.projectId} IN (${sql.join(
|
||||||
@@ -197,6 +205,7 @@ export const projectRouter = createTRPCRouter({
|
|||||||
),
|
),
|
||||||
with: {
|
with: {
|
||||||
environments: {
|
environments: {
|
||||||
|
where: environmentFilter,
|
||||||
with: {
|
with: {
|
||||||
applications: {
|
applications: {
|
||||||
where: buildServiceFilter(
|
where: buildServiceFilter(
|
||||||
|
|||||||
@@ -112,6 +112,10 @@ export const member = pgTable("member", {
|
|||||||
.array()
|
.array()
|
||||||
.notNull()
|
.notNull()
|
||||||
.default(sql`ARRAY[]::text[]`),
|
.default(sql`ARRAY[]::text[]`),
|
||||||
|
accessedEnvironments: text("accessedEnvironments")
|
||||||
|
.array()
|
||||||
|
.notNull()
|
||||||
|
.default(sql`ARRAY[]::text[]`),
|
||||||
accessedServices: text("accesedServices")
|
accessedServices: text("accesedServices")
|
||||||
.array()
|
.array()
|
||||||
.notNull()
|
.notNull()
|
||||||
|
|||||||
@@ -175,6 +175,7 @@ export const apiAssignPermissions = createSchema
|
|||||||
})
|
})
|
||||||
.extend({
|
.extend({
|
||||||
accessedProjects: z.array(z.string()).optional(),
|
accessedProjects: z.array(z.string()).optional(),
|
||||||
|
accessedEnvironments: z.array(z.string()).optional(),
|
||||||
accessedServices: z.array(z.string()).optional(),
|
accessedServices: z.array(z.string()).optional(),
|
||||||
canCreateProjects: z.boolean().optional(),
|
canCreateProjects: z.boolean().optional(),
|
||||||
canCreateServices: z.boolean().optional(),
|
canCreateServices: z.boolean().optional(),
|
||||||
|
|||||||
Reference in New Issue
Block a user