1
1
import {
2
+ type ApplicationSecret ,
2
3
ApplicationType ,
3
4
DomainStatus ,
4
5
type Application ,
5
6
type SnakeCaseOidcConfig ,
7
+ internalPrefix ,
6
8
} from '@logto/schemas' ;
7
9
import { appendPath } from '@silverhand/essentials' ;
8
- import { useCallback , useContext , useState } from 'react' ;
10
+ import { useCallback , useContext , useMemo , useState } from 'react' ;
9
11
import { Trans , useTranslation } from 'react-i18next' ;
12
+ import useSWR from 'swr' ;
10
13
11
14
import CaretDown from '@/assets/icons/caret-down.svg' ;
12
15
import CaretUp from '@/assets/icons/caret-up.svg' ;
16
+ import CirclePlus from '@/assets/icons/circle-plus.svg' ;
17
+ import Plus from '@/assets/icons/plus.svg' ;
18
+ import ActionsButton from '@/components/ActionsButton' ;
13
19
import FormCard from '@/components/FormCard' ;
20
+ import { isDevFeaturesEnabled } from '@/consts/env' ;
14
21
import { openIdProviderConfigPath , openIdProviderPath } from '@/consts/oidc' ;
15
22
import { AppDataContext } from '@/contexts/AppDataProvider' ;
16
23
import Button from '@/ds-components/Button' ;
17
24
import CopyToClipboard from '@/ds-components/CopyToClipboard' ;
18
25
import DynamicT from '@/ds-components/DynamicT' ;
19
26
import FormField from '@/ds-components/FormField' ;
27
+ import Table from '@/ds-components/Table' ;
28
+ import { type Column } from '@/ds-components/Table/types' ;
20
29
import TextLink from '@/ds-components/TextLink' ;
30
+ import useApi , { type RequestError } from '@/hooks/use-api' ;
21
31
import useCustomDomain from '@/hooks/use-custom-domain' ;
22
32
33
+ import CreateSecretModal from './CreateSecretModal' ;
23
34
import * as styles from './index.module.scss' ;
24
35
36
+ const isLegacySecret = ( secret : string ) => ! secret . startsWith ( internalPrefix ) ;
37
+
38
+ type ApplicationSecretRow = Pick < ApplicationSecret , 'name' | 'value' | 'expiresAt' > & {
39
+ isLegacy ?: boolean ;
40
+ } ;
41
+
25
42
type Props = {
26
43
readonly app : Application ;
27
44
readonly oidcConfig : SnakeCaseOidcConfig ;
45
+ readonly onApplicationUpdated : ( ) => void ;
28
46
} ;
29
47
30
- function EndpointsAndCredentials ( { app : { type, secret, id, isThirdParty } , oidcConfig } : Props ) {
48
+ function EndpointsAndCredentials ( {
49
+ app : { type, secret, id, isThirdParty } ,
50
+ oidcConfig,
51
+ onApplicationUpdated,
52
+ } : Props ) {
31
53
const { tenantEndpoint } = useContext ( AppDataContext ) ;
32
54
const [ showMoreEndpoints , setShowMoreEndpoints ] = useState ( false ) ;
33
-
34
55
const { t } = useTranslation ( undefined , { keyPrefix : 'admin_console' } ) ;
35
-
36
56
const { data : customDomain , applyDomain : applyCustomDomain } = useCustomDomain ( ) ;
57
+ const [ showCreateSecretModal , setShowCreateSecretModal ] = useState ( false ) ;
58
+ const secrets = useSWR < ApplicationSecretRow [ ] , RequestError > ( `api/applications/${ id } /secrets` ) ;
59
+ const api = useApi ( ) ;
60
+ const shouldShowAppSecrets = [
61
+ ApplicationType . Traditional ,
62
+ ApplicationType . MachineToMachine ,
63
+ ApplicationType . Protected ,
64
+ ] . includes ( type ) ;
37
65
38
66
const toggleShowMoreEndpoints = useCallback ( ( ) => {
39
67
setShowMoreEndpoints ( ( previous ) => ! previous ) ;
40
68
} , [ ] ) ;
41
69
42
70
const ToggleVisibleCaretIcon = showMoreEndpoints ? CaretUp : CaretDown ;
43
71
72
+ const secretsData = useMemo (
73
+ ( ) => [
74
+ ...( isLegacySecret ( secret )
75
+ ? [
76
+ {
77
+ name : t ( 'application_details.secrets.legacy_secret' ) ,
78
+ value : secret ,
79
+ expiresAt : null ,
80
+ isLegacy : true ,
81
+ } ,
82
+ ]
83
+ : [ ] ) ,
84
+ ...( secrets . data ?? [ ] ) ,
85
+ ] ,
86
+ [ secret , secrets . data , t ]
87
+ ) ;
88
+ const tableColumns : Array < Column < ApplicationSecretRow > > = useMemo (
89
+ ( ) => [
90
+ {
91
+ title : t ( 'general.name' ) ,
92
+ dataIndex : 'name' ,
93
+ colSpan : 3 ,
94
+ render : ( { name } ) => < span > { name } </ span > ,
95
+ } ,
96
+ {
97
+ title : t ( 'application_details.secrets.value' ) ,
98
+ dataIndex : 'value' ,
99
+ colSpan : 6 ,
100
+ render : ( { value } ) => (
101
+ < CopyToClipboard hasVisibilityToggle displayType = "block" value = { value } variant = "text" />
102
+ ) ,
103
+ } ,
104
+ {
105
+ title : t ( 'application_details.secrets.expires_at' ) ,
106
+ dataIndex : 'expiresAt' ,
107
+ colSpan : 3 ,
108
+ render : ( { expiresAt } ) => (
109
+ < span >
110
+ { expiresAt
111
+ ? new Date ( expiresAt ) . toLocaleString ( )
112
+ : t ( 'application_details.secrets.never' ) }
113
+ </ span >
114
+ ) ,
115
+ } ,
116
+ {
117
+ title : '' ,
118
+ dataIndex : 'actions' ,
119
+ render : ( { name, isLegacy } ) => (
120
+ < ActionsButton
121
+ fieldName = "application_details.application_secret"
122
+ deleteConfirmation = "application_details.secrets.delete_confirmation"
123
+ onDelete = { async ( ) => {
124
+ if ( isLegacy ) {
125
+ await api . delete ( `api/applications/${ id } /legacy-secret` ) ;
126
+ onApplicationUpdated ( ) ;
127
+ } else {
128
+ await api . delete ( `api/applications/${ id } /secrets/${ encodeURIComponent ( name ) } ` ) ;
129
+ void secrets . mutate ( ) ;
130
+ }
131
+ } }
132
+ />
133
+ ) ,
134
+ } ,
135
+ ] ,
136
+ [ api , id , onApplicationUpdated , secrets , t ]
137
+ ) ;
138
+
44
139
return (
45
140
< FormCard
46
141
title = "application_details.endpoints_and_credentials"
@@ -148,11 +243,7 @@ function EndpointsAndCredentials({ app: { type, secret, id, isThirdParty }, oidc
148
243
< FormField title = "application_details.application_id" >
149
244
< CopyToClipboard displayType = "block" value = { id } variant = "border" />
150
245
</ FormField >
151
- { [
152
- ApplicationType . Traditional ,
153
- ApplicationType . MachineToMachine ,
154
- ApplicationType . Protected ,
155
- ] . includes ( type ) && (
246
+ { ! isDevFeaturesEnabled && shouldShowAppSecrets && (
156
247
< FormField title = "application_details.application_secret" >
157
248
< CopyToClipboard
158
249
hasVisibilityToggle
@@ -162,6 +253,57 @@ function EndpointsAndCredentials({ app: { type, secret, id, isThirdParty }, oidc
162
253
/>
163
254
</ FormField >
164
255
) }
256
+ { isDevFeaturesEnabled && shouldShowAppSecrets && (
257
+ < FormField title = "application_details.application_secret_other" >
258
+ { secretsData . length === 0 && ! secrets . error ? (
259
+ < >
260
+ < div className = { styles . empty } >
261
+ { t ( 'organizations.empty_placeholder' , {
262
+ entity : t ( 'application_details.application_secret' ) . toLowerCase ( ) ,
263
+ } ) }
264
+ </ div >
265
+ < Button
266
+ icon = { < Plus /> }
267
+ title = "general.add"
268
+ onClick = { ( ) => {
269
+ setShowCreateSecretModal ( true ) ;
270
+ } }
271
+ />
272
+ </ >
273
+ ) : (
274
+ < >
275
+ < Table
276
+ hasBorder
277
+ isRowHoverEffectDisabled
278
+ rowIndexKey = "name"
279
+ isLoading = { ! secrets . data && ! secrets . error }
280
+ errorMessage = { secrets . error ?. body ?. message ?? secrets . error ?. message }
281
+ rowGroups = { [ { key : 'application_secrets' , data : secretsData } ] }
282
+ columns = { tableColumns }
283
+ className = { styles . table }
284
+ />
285
+ < Button
286
+ size = "small"
287
+ type = "text"
288
+ className = { styles . add }
289
+ title = "application_details.secrets.create_new_secret"
290
+ icon = { < CirclePlus /> }
291
+ onClick = { ( ) => {
292
+ setShowCreateSecretModal ( true ) ;
293
+ } }
294
+ />
295
+ </ >
296
+ ) }
297
+ < CreateSecretModal
298
+ appId = { id }
299
+ isOpen = { showCreateSecretModal }
300
+ onClose = { ( ) => {
301
+ setShowCreateSecretModal ( false ) ;
302
+ void secrets . mutate ( ) ;
303
+ } }
304
+ />
305
+ </ FormField >
306
+ ) }
165
307
</ FormCard >
166
308
) ;
167
309
}
0 commit comments