55 TrashIcon ,
66} from "@heroicons/react/20/solid" ;
77import { Form , useRevalidator } from "@remix-run/react" ;
8- import { json , type ActionFunctionArgs , type LoaderFunctionArgs } from "@remix-run/server-runtime" ;
8+ import { json , type LoaderFunctionArgs } from "@remix-run/server-runtime" ;
99import { tryCatch } from "@trigger.dev/core/utils" ;
1010import type { PrivateLinkConnectionStatus } from "@trigger.dev/platform" ;
1111import { useMemo , useState } from "react" ;
@@ -20,8 +20,11 @@ import { Header2 } from "~/components/primitives/Headers";
2020import { NavBar , PageAccessories , PageTitle } from "~/components/primitives/PageHeader" ;
2121import { Paragraph } from "~/components/primitives/Paragraph" ;
2222import { prisma } from "~/db.server" ;
23+ import { rbac } from "~/services/rbac.server" ;
2324import { useInterval } from "~/hooks/useInterval" ;
2425import { redirectWithErrorMessage , redirectWithSuccessMessage } from "~/models/message.server" ;
26+ import { resolveOrgIdFromSlug } from "~/models/organization.server" ;
27+ import { dashboardAction } from "~/services/routeBuilders/dashboardBuilder" ;
2528import { logger } from "~/services/logger.server" ;
2629import { deletePrivateLink , getPrivateLinks } from "~/services/platform.v3.server" ;
2730import { requireUserId } from "~/services/session.server" ;
@@ -54,6 +57,13 @@ export async function loader({ params, request }: LoaderFunctionArgs) {
5457 throw new Response ( null , { status : 404 , statusText : "Organization not found" } ) ;
5558 }
5659
60+ const sessionAuth = await rbac . authenticateSession ( request , {
61+ userId,
62+ organizationId : organization . id ,
63+ } ) ;
64+ const canManageConnections =
65+ sessionAuth . ok && sessionAuth . ability . can ( "write" , { type : "privateConnections" } ) ;
66+
5767 const [ error , connections ] = await tryCatch ( getPrivateLinks ( organization . id ) ) ;
5868 if ( error ) {
5969 logger . error ( "Error loading private link connections" , {
@@ -65,52 +75,63 @@ export async function loader({ params, request }: LoaderFunctionArgs) {
6575 return typedjson ( {
6676 connections : connections ?. connections ?? [ ] ,
6777 organizationId : organization . id ,
78+ canManageConnections,
6879 } ) ;
6980}
7081
71- export const action = async ( { request, params } : ActionFunctionArgs ) => {
72- const userId = await requireUserId ( request ) ;
73- const { organizationSlug } = OrganizationParamsSchema . parse ( params ) ;
82+ export const action = dashboardAction (
83+ {
84+ params : OrganizationParamsSchema ,
85+ context : async ( params ) => {
86+ const organizationId = await resolveOrgIdFromSlug ( params . organizationSlug ) ;
87+ return organizationId ? { organizationId } : { } ;
88+ } ,
89+ authorization : { action : "write" , resource : { type : "privateConnections" } } ,
90+ } ,
91+ async ( { request, params } ) => {
92+ const userId = await requireUserId ( request ) ;
93+ const { organizationSlug } = params ;
7494
75- if ( request . method !== "DELETE" && request . method !== "POST" ) {
76- return json ( { error : "Method not allowed" } , { status : 405 } ) ;
77- }
95+ if ( request . method !== "DELETE" && request . method !== "POST" ) {
96+ return json ( { error : "Method not allowed" } , { status : 405 } ) ;
97+ }
7898
79- const formData = await request . formData ( ) ;
80- const connectionId = formData . get ( "connectionId" ) ;
81- const intent = formData . get ( "intent" ) ;
99+ const formData = await request . formData ( ) ;
100+ const connectionId = formData . get ( "connectionId" ) ;
101+ const intent = formData . get ( "intent" ) ;
82102
83- if ( intent !== "delete" || typeof connectionId !== "string" ) {
84- return json ( { error : "Invalid request" } , { status : 400 } ) ;
85- }
103+ if ( intent !== "delete" || typeof connectionId !== "string" ) {
104+ return json ( { error : "Invalid request" } , { status : 400 } ) ;
105+ }
86106
87- const organization = await prisma . organization . findFirst ( {
88- where : { slug : organizationSlug , members : { some : { userId } } } ,
89- } ) ;
107+ const organization = await prisma . organization . findFirst ( {
108+ where : { slug : organizationSlug , members : { some : { userId } } } ,
109+ } ) ;
90110
91- if ( ! organization ) {
92- return redirectWithErrorMessage (
93- v3PrivateConnectionsPath ( { slug : organizationSlug } ) ,
94- request ,
95- "Organization not found"
96- ) ;
97- }
111+ if ( ! organization ) {
112+ return redirectWithErrorMessage (
113+ v3PrivateConnectionsPath ( { slug : organizationSlug } ) ,
114+ request ,
115+ "Organization not found"
116+ ) ;
117+ }
98118
99- const [ error ] = await tryCatch ( deletePrivateLink ( organization . id , connectionId ) ) ;
100- if ( error ) {
101- return redirectWithErrorMessage (
119+ const [ error ] = await tryCatch ( deletePrivateLink ( organization . id , connectionId ) ) ;
120+ if ( error ) {
121+ return redirectWithErrorMessage (
122+ v3PrivateConnectionsPath ( { slug : organizationSlug } ) ,
123+ request ,
124+ `Failed to delete connection: ${ error . message } `
125+ ) ;
126+ }
127+
128+ return redirectWithSuccessMessage (
102129 v3PrivateConnectionsPath ( { slug : organizationSlug } ) ,
103130 request ,
104- `Failed to delete connection: ${ error . message } `
131+ "Connection deletion initiated"
105132 ) ;
106133 }
107-
108- return redirectWithSuccessMessage (
109- v3PrivateConnectionsPath ( { slug : organizationSlug } ) ,
110- request ,
111- "Connection deletion initiated"
112- ) ;
113- } ;
134+ ) ;
114135
115136const STATUS_COLORS : Record < PrivateLinkConnectionStatus , string > = {
116137 PENDING : "bg-amber-500/10 text-amber-700 dark:text-amber-400 system:text-warning" ,
@@ -153,7 +174,7 @@ function CopyButton({ value }: { value: string }) {
153174const TERMINAL_STATUSES : PrivateLinkConnectionStatus [ ] = [ "ACTIVE" , "ERROR" ] ;
154175
155176export default function Page ( ) {
156- const { connections } = useTypedLoaderData < typeof loader > ( ) ;
177+ const { connections, canManageConnections } = useTypedLoaderData < typeof loader > ( ) ;
157178 const plan = useCurrentPlan ( ) ;
158179 const revalidator = useRevalidator ( ) ;
159180
@@ -176,6 +197,7 @@ export default function Page() {
176197 const hasPrivateNetworking = plan ?. v3Subscription ?. plan ?. limits ?. hasPrivateNetworking ?? false ;
177198 const limit = plan ?. v3Subscription ?. plan ?. limits ?. privateLinkConnectionLimit ?? 2 ;
178199 const canAdd = connections . filter ( ( c ) => c . status !== "DELETING" ) . length < limit ;
200+ const permissionTooltip = "You don't have permission to manage private connections" ;
179201
180202 return (
181203 < PageContainer >
@@ -190,9 +212,16 @@ export default function Page() {
190212 Private connection docs
191213 </ LinkButton >
192214 { hasPrivateNetworking && canAdd && (
193- < LinkButton variant = "primary/small" LeadingIcon = { PlusIcon } to = "new" >
194- Add Connection
195- </ LinkButton >
215+ < span title = { ! canManageConnections ? permissionTooltip : undefined } >
216+ < LinkButton
217+ variant = "primary/small"
218+ LeadingIcon = { PlusIcon }
219+ to = "new"
220+ disabled = { ! canManageConnections }
221+ >
222+ Add Connection
223+ </ LinkButton >
224+ </ span >
196225 ) }
197226 </ PageAccessories >
198227 </ NavBar >
@@ -220,9 +249,16 @@ export default function Page() {
220249 No private connections yet. Add your first connection to securely reach your AWS
221250 resources from task pods.
222251 </ Paragraph >
223- < LinkButton variant = "primary/small" LeadingIcon = { PlusIcon } to = "new" >
224- Add Connection
225- </ LinkButton >
252+ < span title = { ! canManageConnections ? permissionTooltip : undefined } >
253+ < LinkButton
254+ variant = "primary/small"
255+ LeadingIcon = { PlusIcon }
256+ to = "new"
257+ disabled = { ! canManageConnections }
258+ >
259+ Add Connection
260+ </ LinkButton >
261+ </ span >
226262 </ div >
227263 ) : (
228264 < div className = "flex flex-col gap-3" >
@@ -238,8 +274,9 @@ export default function Page() {
238274 < input type = "hidden" name = "intent" value = "delete" />
239275 < button
240276 type = "submit"
241- className = "text-text-dimmed transition hover:text-rose-400"
242- title = "Delete connection"
277+ disabled = { ! canManageConnections }
278+ className = "text-text-dimmed transition hover:text-rose-400 disabled:cursor-not-allowed disabled:opacity-50"
279+ title = { canManageConnections ? "Delete connection" : permissionTooltip }
243280 onClick = { ( e ) => {
244281 if (
245282 ! confirm (
@@ -293,9 +330,16 @@ export default function Page() {
293330
294331 { canAdd && (
295332 < div className = "flex justify-center pt-2" >
296- < LinkButton variant = "primary/small" LeadingIcon = { PlusIcon } to = "new" >
297- Add Connection
298- </ LinkButton >
333+ < span title = { ! canManageConnections ? permissionTooltip : undefined } >
334+ < LinkButton
335+ variant = "primary/small"
336+ LeadingIcon = { PlusIcon }
337+ to = "new"
338+ disabled = { ! canManageConnections }
339+ >
340+ Add Connection
341+ </ LinkButton >
342+ </ span >
299343 </ div >
300344 ) }
301345
0 commit comments