11import { Outlet , useRouteLoaderData } from "@remix-run/react" ;
2+ import { type LoaderFunctionArgs } from "@remix-run/server-runtime" ;
23import { VERSION as coreVersion } from "@trigger.dev/core" ;
34import { type ReactNode } from "react" ;
45import { typedjson , useTypedLoaderData } from "remix-typedjson" ;
@@ -9,15 +10,60 @@ import {
910 OrganizationSettingsSideMenu ,
1011} from "~/components/navigation/OrganizationSettingsSideMenu" ;
1112import { useOrganization } from "~/hooks/useOrganizations" ;
13+ import { resolveOrgIdFromSlugForUser } from "~/models/organization.server" ;
14+ import { logger } from "~/services/logger.server" ;
15+ import { organizationHasProjectRuntimeUpdate } from "~/services/projectRuntimeUpdates.server" ;
1216import { rbac } from "~/services/rbac.server" ;
17+ import { requireUserId } from "~/services/session.server" ;
1318import { ssoController } from "~/services/sso.server" ;
1419
1520const SETTINGS_ROUTE_ID = "routes/_app.orgs.$organizationSlug.settings" ;
1621
17- export const loader = async ( ) => {
18- const [ isUsingPlugin , isSsoUsingPlugin ] = await Promise . all ( [
22+ // The side-menu dot links to the Projects settings page, which requires `read` on
23+ // `deployments`, so gate the dot on the same ability the page checks.
24+ async function canReadDeployments ( {
25+ request,
26+ userId,
27+ organizationSlug,
28+ } : {
29+ request : Request ;
30+ userId : string ;
31+ organizationSlug : string ;
32+ } ) {
33+ // Membership-scoped so the dot is never computed against an org the user is not in.
34+ const organizationId = await resolveOrgIdFromSlugForUser ( organizationSlug , userId ) ;
35+ if ( ! organizationId ) {
36+ return false ;
37+ }
38+
39+ const auth = await rbac . authenticateAuthorizeSession (
40+ request ,
41+ { userId, organizationId } ,
42+ { action : "read" , resource : { type : "deployments" } }
43+ ) ;
44+ return auth . ok ;
45+ }
46+
47+ export const loader = async ( { request, params } : LoaderFunctionArgs ) => {
48+ const userId = await requireUserId ( request ) ;
49+ const organizationSlug = params . organizationSlug ;
50+
51+ const [ isUsingPlugin , isSsoUsingPlugin , hasProjectRuntimeUpdate ] = await Promise . all ( [
1952 rbac . isUsingPlugin ( ) ,
2053 ssoController . isUsingPlugin ( ) ,
54+ organizationSlug
55+ ? canReadDeployments ( { request, userId, organizationSlug } )
56+ . then ( ( canRead ) =>
57+ canRead ? organizationHasProjectRuntimeUpdate ( { organizationSlug, userId } ) : false
58+ )
59+ . catch ( ( error ) => {
60+ logger . error ( "Failed to check project runtime updates" , {
61+ organizationSlug,
62+ error,
63+ } ) ;
64+ return false ;
65+ } )
66+ : Promise . resolve ( false ) ,
2167 ] ) ;
2268 return typedjson ( {
2369 buildInfo : {
@@ -29,18 +75,21 @@ export const loader = async () => {
2975 } satisfies BuildInfo ,
3076 isUsingPlugin,
3177 isSsoUsingPlugin,
78+ hasProjectRuntimeUpdate,
3279 } ) ;
3380} ;
3481
3582function SettingsChrome ( {
3683 buildInfo,
3784 isUsingPlugin,
3885 isSsoUsingPlugin,
86+ hasProjectRuntimeUpdate,
3987 children,
4088} : {
4189 buildInfo : BuildInfo ;
4290 isUsingPlugin : boolean ;
4391 isSsoUsingPlugin : boolean ;
92+ hasProjectRuntimeUpdate : boolean ;
4493 children : ReactNode ;
4594} ) {
4695 const organization = useOrganization ( ) ;
@@ -53,6 +102,7 @@ function SettingsChrome({
53102 buildInfo = { buildInfo }
54103 isUsingPlugin = { isUsingPlugin }
55104 isSsoUsingPlugin = { isSsoUsingPlugin }
105+ hasProjectRuntimeUpdate = { hasProjectRuntimeUpdate }
56106 />
57107 < MainBody > { children } </ MainBody >
58108 </ div >
@@ -61,13 +111,15 @@ function SettingsChrome({
61111}
62112
63113export default function Page ( ) {
64- const { buildInfo, isUsingPlugin, isSsoUsingPlugin } = useTypedLoaderData < typeof loader > ( ) ;
114+ const { buildInfo, isUsingPlugin, isSsoUsingPlugin, hasProjectRuntimeUpdate } =
115+ useTypedLoaderData < typeof loader > ( ) ;
65116
66117 return (
67118 < SettingsChrome
68119 buildInfo = { buildInfo }
69120 isUsingPlugin = { isUsingPlugin }
70121 isSsoUsingPlugin = { isSsoUsingPlugin }
122+ hasProjectRuntimeUpdate = { hasProjectRuntimeUpdate }
71123 >
72124 < Outlet />
73125 </ SettingsChrome >
@@ -84,6 +136,7 @@ export function ErrorBoundary() {
84136 buildInfo : BuildInfo ;
85137 isUsingPlugin : boolean ;
86138 isSsoUsingPlugin : boolean ;
139+ hasProjectRuntimeUpdate : boolean ;
87140 }
88141 | undefined ;
89142
@@ -96,6 +149,7 @@ export function ErrorBoundary() {
96149 buildInfo = { data . buildInfo }
97150 isUsingPlugin = { data . isUsingPlugin }
98151 isSsoUsingPlugin = { data . isSsoUsingPlugin }
152+ hasProjectRuntimeUpdate = { data . hasProjectRuntimeUpdate }
99153 >
100154 < RouteErrorDisplay />
101155 </ SettingsChrome >
0 commit comments