11import { BookOpenIcon } from "@heroicons/react/20/solid" ;
22import { type MetaFunction } from "@remix-run/react" ;
3- import { type LoaderFunctionArgs } from "@remix-run/server-runtime" ;
43import { typedjson , useTypedLoaderData } from "remix-typedjson" ;
54import { AdminDebugTooltip } from "~/components/admin/debugTooltip" ;
65import { CodeBlock } from "~/components/code/CodeBlock" ;
@@ -16,6 +15,7 @@ import {
1615 PageBody ,
1716 PageContainer ,
1817} from "~/components/layout/AppLayout" ;
18+ import { PermissionDenied } from "~/components/PermissionDenied" ;
1919import {
2020 Accordion ,
2121 AccordionContent ,
@@ -31,9 +31,10 @@ import { InputGroup } from "~/components/primitives/InputGroup";
3131import { Label } from "~/components/primitives/Label" ;
3232import { NavBar , PageAccessories , PageTitle } from "~/components/primitives/PageHeader" ;
3333import * as Property from "~/components/primitives/PropertyTable" ;
34+ import { $replica } from "~/db.server" ;
3435import { useOrganization } from "~/hooks/useOrganizations" ;
3536import { ApiKeysPresenter } from "~/presenters/v3/ApiKeysPresenter.server" ;
36- import { requireUserId } from "~/services/session.server " ;
37+ import { dashboardLoader } from "~/services/routeBuilders/dashboardBuilder " ;
3738import { cn } from "~/utils/cn" ;
3839import { docsPath , EnvironmentParamSchema } from "~/utils/pathBuilder" ;
3940
@@ -45,33 +46,55 @@ export const meta: MetaFunction = () => {
4546 ] ;
4647} ;
4748
48- export const loader = async ( { request, params } : LoaderFunctionArgs ) => {
49- const userId = await requireUserId ( request ) ;
50- const { projectParam, envParam } = EnvironmentParamSchema . parse ( params ) ;
49+ async function resolveOrgIdFromSlug ( slug : string ) : Promise < string | null > {
50+ const org = await $replica . organization . findFirst ( { where : { slug } , select : { id : true } } ) ;
51+ return org ?. id ?? null ;
52+ }
5153
52- try {
53- const presenter = new ApiKeysPresenter ( ) ;
54- const { environment, hasVercelIntegration } = await presenter . call ( {
55- userId,
56- projectSlug : projectParam ,
57- environmentSlug : envParam ,
58- } ) ;
54+ export const loader = dashboardLoader (
55+ {
56+ params : EnvironmentParamSchema ,
57+ context : async ( params ) => {
58+ const organizationId = await resolveOrgIdFromSlug ( params . organizationSlug ) ;
59+ return organizationId ? { organizationId } : { } ;
60+ } ,
61+ // No hard authorization: anyone with project access can open the page.
62+ // Reading the secret key is gated per environment tier below — a role
63+ // that can't read this tier's keys gets the info panel, not the key.
64+ } ,
65+ async ( { params, user, ability } ) => {
66+ const { projectParam, envParam } = params ;
5967
60- return typedjson ( {
61- environment,
62- hasVercelIntegration,
63- } ) ;
64- } catch ( error ) {
65- console . error ( error ) ;
66- throw new Response ( undefined , {
67- status : 400 ,
68- statusText : "Something went wrong, if this problem persists please contact support." ,
69- } ) ;
68+ try {
69+ const presenter = new ApiKeysPresenter ( ) ;
70+ const { environment, hasVercelIntegration } = await presenter . call ( {
71+ userId : user . id ,
72+ projectSlug : projectParam ,
73+ environmentSlug : envParam ,
74+ } ) ;
75+
76+ const canReadApiKeys =
77+ ! environment || ability . can ( "read" , { type : "apiKeys" , envType : environment . type } ) ;
78+
79+ return typedjson ( {
80+ // Never serialize the secret key to the client when the role can't
81+ // read it for this environment tier.
82+ environment : environment && ! canReadApiKeys ? { ...environment , apiKey : "" } : environment ,
83+ hasVercelIntegration,
84+ canReadApiKeys,
85+ } ) ;
86+ } catch ( error ) {
87+ console . error ( error ) ;
88+ throw new Response ( undefined , {
89+ status : 400 ,
90+ statusText : "Something went wrong, if this problem persists please contact support." ,
91+ } ) ;
92+ }
7093 }
71- } ;
94+ ) ;
7295
7396export default function Page ( ) {
74- const { environment, hasVercelIntegration } = useTypedLoaderData < typeof loader > ( ) ;
97+ const { environment, hasVercelIntegration, canReadApiKeys } = useTypedLoaderData < typeof loader > ( ) ;
7598 const organization = useOrganization ( ) ;
7699
77100 if ( ! environment ) {
@@ -126,70 +149,78 @@ export default function Page() {
126149 API keys
127150 </ Header2 >
128151 </ div >
129- < div className = "flex flex-col gap-6" >
130- < InputGroup fullWidth >
131- < div className = "flex w-full items-center justify-between" >
132- < Label > Secret key</ Label >
133- < RegenerateApiKeyModal
134- id = { environment . parentEnvironment ?. id ?? environment . id }
135- title = { environmentFullTitle ( environment ) }
136- hasVercelIntegration = { hasVercelIntegration }
137- isDevelopment = { environment . type === "DEVELOPMENT" }
138- />
139- </ div >
140- < ClipboardField
141- className = "w-full max-w-none"
142- secure = { `tr_${ environment . apiKey . split ( "_" ) [ 1 ] } _••••••••` }
143- value = { environment . apiKey }
144- variant = { "secondary/small" }
145- />
146- < Hint >
147- Set this as your < InlineCode variant = "extra-small" > TRIGGER_SECRET_KEY</ InlineCode > { " " }
148- env var in your backend.
149- </ Hint >
150- </ InputGroup >
151- { environment . branchName && (
152+ { canReadApiKeys ? (
153+ < div className = "flex flex-col gap-6" >
152154 < InputGroup fullWidth >
153- < Label > Branch name</ Label >
155+ < div className = "flex w-full items-center justify-between" >
156+ < Label > Secret key</ Label >
157+ < RegenerateApiKeyModal
158+ id = { environment . parentEnvironment ?. id ?? environment . id }
159+ title = { environmentFullTitle ( environment ) }
160+ hasVercelIntegration = { hasVercelIntegration }
161+ isDevelopment = { environment . type === "DEVELOPMENT" }
162+ />
163+ </ div >
154164 < ClipboardField
155165 className = "w-full max-w-none"
156- value = { environment . branchName }
166+ secure = { `tr_${ environment . apiKey . split ( "_" ) [ 1 ] } _••••••••` }
167+ value = { environment . apiKey }
157168 variant = { "secondary/small" }
158169 />
159170 < Hint >
160- Set this as your{ " " }
161- < InlineCode variant = "extra-small" > TRIGGER_PREVIEW_BRANCH</ InlineCode > env var in
162- your backend.
171+ Set this as your < InlineCode variant = "extra-small" > TRIGGER_SECRET_KEY</ InlineCode > { " " }
172+ env var in your backend.
163173 </ Hint >
164174 </ InputGroup >
165- ) }
166- { environment . type === "DEVELOPMENT" && (
167- < Callout variant = "info" >
168- Every team member gets their own dev Secret key. Make sure you're using the one
169- above otherwise you will trigger runs on your team member's machine.
170- </ Callout >
171- ) }
175+ { environment . branchName && (
176+ < InputGroup fullWidth >
177+ < Label > Branch name</ Label >
178+ < ClipboardField
179+ className = "w-full max-w-none"
180+ value = { environment . branchName }
181+ variant = { "secondary/small" }
182+ />
183+ < Hint >
184+ Set this as your{ " " }
185+ < InlineCode variant = "extra-small" > TRIGGER_PREVIEW_BRANCH</ InlineCode > env var in
186+ your backend.
187+ </ Hint >
188+ </ InputGroup >
189+ ) }
190+ { environment . type === "DEVELOPMENT" && (
191+ < Callout variant = "info" >
192+ Every team member gets their own dev Secret key. Make sure you're using the one
193+ above otherwise you will trigger runs on your team member's machine.
194+ </ Callout >
195+ ) }
172196
173- < Accordion type = "single" collapsible >
174- < AccordionItem value = "item-1" >
175- < AccordionTrigger > How to set these environment variables</ AccordionTrigger >
176- < AccordionContent >
177- < div className = "flex flex-col gap-2" >
178- < div >
179- You need to set these environment variables in your backend. This allows the
180- SDK to authenticate with Trigger.dev.
197+ < Accordion type = "single" collapsible >
198+ < AccordionItem value = "item-1" >
199+ < AccordionTrigger > How to set these environment variables</ AccordionTrigger >
200+ < AccordionContent >
201+ < div className = "flex flex-col gap-2" >
202+ < div >
203+ You need to set these environment variables in your backend. This allows the
204+ SDK to authenticate with Trigger.dev.
205+ </ div >
206+ < CodeBlock
207+ language = "javascript"
208+ code = { envBlock }
209+ showOpenInModal = { false }
210+ showLineNumbers = { false }
211+ />
181212 </ div >
182- < CodeBlock
183- language = "javascript"
184- code = { envBlock }
185- showOpenInModal = { false }
186- showLineNumbers = { false }
187- />
188- </ div >
189- </ AccordionContent >
190- </ AccordionItem >
191- </ Accordion >
192- </ div >
213+ </ AccordionContent >
214+ </ AccordionItem >
215+ </ Accordion >
216+ </ div >
217+ ) : (
218+ < PermissionDenied
219+ message = { `With your current role, you can't view the API keys for ${ environmentFullTitle (
220+ environment
221+ ) } .` }
222+ / >
223+ ) }
193224 </ MainHorizontallyCenteredContainer >
194225 </ PageBody >
195226 </ PageContainer >
0 commit comments