@idp.global/catalog 3.5.0 → 3.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/changelog.md +9 -0
- package/dist_bundle/bundle.js +317 -252
- package/dist_ts_web/00_commitinfo_data.js +1 -1
- package/dist_ts_web/elements/idp-admin-shell.d.ts +55 -0
- package/dist_ts_web/elements/idp-admin-shell.js +318 -10
- package/package.json +1 -1
- package/readme.md +8 -2
- package/ts_web/00_commitinfo_data.ts +1 -1
- package/ts_web/elements/idp-admin-shell.ts +302 -9
package/package.json
CHANGED
package/readme.md
CHANGED
|
@@ -30,16 +30,22 @@ Then compose the elements in HTML, Lit, or any framework that can render custom
|
|
|
30
30
|
<idp-mobile-showcase></idp-mobile-showcase>
|
|
31
31
|
```
|
|
32
32
|
|
|
33
|
-
The package also exports the TypeScript types for the interactive elements and admin shell events, so applications can strongly type `idp-submit`, `idp-select`,
|
|
33
|
+
The package also exports the TypeScript types for the interactive elements and admin shell events, so applications can strongly type `idp-submit`, `idp-select`, account-management, and OIDC administration handlers.
|
|
34
34
|
|
|
35
35
|
```ts
|
|
36
36
|
import type {
|
|
37
37
|
IIdpFormSubmitEventDetail,
|
|
38
38
|
IIdpSelectOption,
|
|
39
39
|
IIdpAdminNavigateEventDetail,
|
|
40
|
+
IIdpAdminGaAppConfigureEventDetail,
|
|
41
|
+
IIdpAdminGaAppRotateSecretEventDetail,
|
|
42
|
+
IIdpAdminGaAppSecretDismissEventDetail,
|
|
43
|
+
IIdpAdminClientSecret,
|
|
40
44
|
} from '@idp.global/catalog';
|
|
41
45
|
```
|
|
42
46
|
|
|
47
|
+
`idp-admin-shell` dispatches `idp-admin-ga-app-configure` for safe OIDC client configuration and `idp-admin-ga-app-rotate-secret` for confidential-client secret rotation. After the host supplies the one-time result through `.oidcClientSecret`, the shell displays the plaintext secret until the user confirms secure storage through `idp-admin-ga-app-secret-dismiss`. The host must clear the property on dismissal and must not persist the plaintext secret.
|
|
48
|
+
|
|
43
49
|
## Element Surface
|
|
44
50
|
|
|
45
51
|
| Element | Purpose |
|
|
@@ -59,7 +65,7 @@ import type {
|
|
|
59
65
|
| `idp-inbox-preview` | Mobile approval inbox previews |
|
|
60
66
|
| `idp-mobile-frame` | iOS-style product screenshots |
|
|
61
67
|
| `idp-dashboard-window` | Desktop dashboard preview window |
|
|
62
|
-
| `idp-admin-shell` | Account and
|
|
68
|
+
| `idp-admin-shell` | Account, organization, and platform OIDC administration shell |
|
|
63
69
|
| `idp-landing-hero` | Public landing hero section |
|
|
64
70
|
| `idp-landing-page` | Full public landing page composition |
|
|
65
71
|
| `idp-mobile-showcase` | Mobile, watch, tablet, and desktop showcase composition |
|
|
@@ -123,6 +123,36 @@ export interface IIdpAdminGaAppActionEventDetail {
|
|
|
123
123
|
action: 'approve' | 'reject' | 'suspend' | 'reinstate';
|
|
124
124
|
}
|
|
125
125
|
|
|
126
|
+
export interface IIdpAdminGaAppConfigureEventDetail {
|
|
127
|
+
appId: string;
|
|
128
|
+
clientType: 'public' | 'confidential';
|
|
129
|
+
isActive: boolean;
|
|
130
|
+
redirectUris: string[];
|
|
131
|
+
postLogoutRedirectUris: string[];
|
|
132
|
+
allowedScopes: string[];
|
|
133
|
+
grantTypes: Array<'authorization_code' | 'client_credentials' | 'refresh_token'>;
|
|
134
|
+
oidcSettings: {
|
|
135
|
+
accessTokenLifetime: number;
|
|
136
|
+
idTokenLifetime: number;
|
|
137
|
+
refreshTokenLifetime: number;
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export interface IIdpAdminGaAppRotateSecretEventDetail {
|
|
142
|
+
appId: string;
|
|
143
|
+
gracePeriodSeconds: number;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
export interface IIdpAdminGaAppSecretDismissEventDetail {
|
|
147
|
+
appId: string;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
export interface IIdpAdminClientSecret {
|
|
151
|
+
appId: string;
|
|
152
|
+
clientId: string;
|
|
153
|
+
clientSecret: string;
|
|
154
|
+
}
|
|
155
|
+
|
|
126
156
|
export interface IIdpAdminOverviewSeriesPoint {
|
|
127
157
|
/** epoch-ms timestamp or Date-parseable string; plain clock times like '14:00' are not chartable */
|
|
128
158
|
x: string | number;
|
|
@@ -190,8 +220,16 @@ export interface IIdpAdminApp {
|
|
|
190
220
|
isConnected?: boolean;
|
|
191
221
|
connectionCount?: number;
|
|
192
222
|
clientId?: string;
|
|
223
|
+
clientType?: 'public' | 'confidential';
|
|
224
|
+
isActive?: boolean;
|
|
225
|
+
redirectUris?: string[];
|
|
226
|
+
postLogoutRedirectUris?: string[];
|
|
193
227
|
scopes?: string[];
|
|
194
228
|
grants?: string[];
|
|
229
|
+
accessTokenLifetime?: number;
|
|
230
|
+
idTokenLifetime?: number;
|
|
231
|
+
refreshTokenLifetime?: number;
|
|
232
|
+
retiringClientSecretCount?: number;
|
|
195
233
|
roleMappings?: IIdpAdminAppRoleMapping[];
|
|
196
234
|
}
|
|
197
235
|
|
|
@@ -989,6 +1027,9 @@ export class IdpAdminShell extends DeesElement {
|
|
|
989
1027
|
@property({ type: Array })
|
|
990
1028
|
public accessor adminApps: IIdpAdminApp[] = [];
|
|
991
1029
|
|
|
1030
|
+
@property({ type: Object })
|
|
1031
|
+
public accessor oidcClientSecret: IIdpAdminClientSecret | null = null;
|
|
1032
|
+
|
|
992
1033
|
@property({ type: Array })
|
|
993
1034
|
public accessor passportDevices: IIdpAdminPassportDevice[] = [];
|
|
994
1035
|
|
|
@@ -1126,7 +1167,7 @@ export class IdpAdminShell extends DeesElement {
|
|
|
1126
1167
|
private accessor orgSettingsError = '';
|
|
1127
1168
|
|
|
1128
1169
|
@state()
|
|
1129
|
-
private accessor dialogMode: 'none' | 'role-upsert' | 'role-delete' | 'member-roles' | 'app-role-mappings' | 'create-org' | 'logout-confirm' = 'none';
|
|
1170
|
+
private accessor dialogMode: 'none' | 'role-upsert' | 'role-delete' | 'member-roles' | 'app-role-mappings' | 'oidc-app-config' | 'oidc-secret-rotate' | 'create-org' | 'logout-confirm' = 'none';
|
|
1130
1171
|
|
|
1131
1172
|
@state()
|
|
1132
1173
|
private accessor dialogError = '';
|
|
@@ -1155,6 +1196,36 @@ export class IdpAdminShell extends DeesElement {
|
|
|
1155
1196
|
@state()
|
|
1156
1197
|
private accessor dialogAppMappings: IIdpAdminAppRoleMapping[] = [];
|
|
1157
1198
|
|
|
1199
|
+
@state()
|
|
1200
|
+
private accessor dialogClientType: 'public' | 'confidential' = 'confidential';
|
|
1201
|
+
|
|
1202
|
+
@state()
|
|
1203
|
+
private accessor dialogAppActive = true;
|
|
1204
|
+
|
|
1205
|
+
@state()
|
|
1206
|
+
private accessor dialogRedirectUris = '';
|
|
1207
|
+
|
|
1208
|
+
@state()
|
|
1209
|
+
private accessor dialogPostLogoutRedirectUris = '';
|
|
1210
|
+
|
|
1211
|
+
@state()
|
|
1212
|
+
private accessor dialogAllowedScopes = '';
|
|
1213
|
+
|
|
1214
|
+
@state()
|
|
1215
|
+
private accessor dialogGrantTypes = '';
|
|
1216
|
+
|
|
1217
|
+
@state()
|
|
1218
|
+
private accessor dialogAccessTokenLifetime = 3600;
|
|
1219
|
+
|
|
1220
|
+
@state()
|
|
1221
|
+
private accessor dialogIdTokenLifetime = 3600;
|
|
1222
|
+
|
|
1223
|
+
@state()
|
|
1224
|
+
private accessor dialogRefreshTokenLifetime = 2592000;
|
|
1225
|
+
|
|
1226
|
+
@state()
|
|
1227
|
+
private accessor dialogSecretGracePeriod = 86400;
|
|
1228
|
+
|
|
1158
1229
|
private get safeOrgs(): IIdpAdminOrg[] {
|
|
1159
1230
|
return Array.isArray(this.orgs) ? this.orgs.filter(Boolean) : [];
|
|
1160
1231
|
}
|
|
@@ -1556,6 +1627,16 @@ export class IdpAdminShell extends DeesElement {
|
|
|
1556
1627
|
this.dialogMemberRoles = [];
|
|
1557
1628
|
this.dialogApp = null;
|
|
1558
1629
|
this.dialogAppMappings = [];
|
|
1630
|
+
this.dialogClientType = 'confidential';
|
|
1631
|
+
this.dialogAppActive = true;
|
|
1632
|
+
this.dialogRedirectUris = '';
|
|
1633
|
+
this.dialogPostLogoutRedirectUris = '';
|
|
1634
|
+
this.dialogAllowedScopes = '';
|
|
1635
|
+
this.dialogGrantTypes = '';
|
|
1636
|
+
this.dialogAccessTokenLifetime = 3600;
|
|
1637
|
+
this.dialogIdTokenLifetime = 3600;
|
|
1638
|
+
this.dialogRefreshTokenLifetime = 2592000;
|
|
1639
|
+
this.dialogSecretGracePeriod = 86400;
|
|
1559
1640
|
}
|
|
1560
1641
|
|
|
1561
1642
|
private openRoleUpsertDialog(roleArg?: IIdpAdminOrgRoleDefinition) {
|
|
@@ -1597,6 +1678,130 @@ export class IdpAdminShell extends DeesElement {
|
|
|
1597
1678
|
});
|
|
1598
1679
|
}
|
|
1599
1680
|
|
|
1681
|
+
private openOidcAppConfigDialog(appArg: IIdpAdminApp) {
|
|
1682
|
+
this.dialogMode = 'oidc-app-config';
|
|
1683
|
+
this.dialogError = '';
|
|
1684
|
+
this.dialogApp = appArg;
|
|
1685
|
+
this.dialogClientType = appArg.clientType || 'confidential';
|
|
1686
|
+
this.dialogAppActive = appArg.isActive ?? appArg.status !== 'suspended';
|
|
1687
|
+
this.dialogRedirectUris = (appArg.redirectUris || []).join('\n');
|
|
1688
|
+
this.dialogPostLogoutRedirectUris = (appArg.postLogoutRedirectUris || []).join('\n');
|
|
1689
|
+
this.dialogAllowedScopes = (appArg.scopes || []).join(', ');
|
|
1690
|
+
this.dialogGrantTypes = (appArg.grants || []).join(', ');
|
|
1691
|
+
this.dialogAccessTokenLifetime = appArg.accessTokenLifetime || 3600;
|
|
1692
|
+
this.dialogIdTokenLifetime = appArg.idTokenLifetime || 3600;
|
|
1693
|
+
this.dialogRefreshTokenLifetime = appArg.refreshTokenLifetime || 2592000;
|
|
1694
|
+
}
|
|
1695
|
+
|
|
1696
|
+
private openOidcSecretRotateDialog(appArg: IIdpAdminApp) {
|
|
1697
|
+
this.dialogMode = 'oidc-secret-rotate';
|
|
1698
|
+
this.dialogError = '';
|
|
1699
|
+
this.dialogApp = appArg;
|
|
1700
|
+
this.dialogSecretGracePeriod = 86400;
|
|
1701
|
+
}
|
|
1702
|
+
|
|
1703
|
+
private parseLines(valueArg: string): string[] {
|
|
1704
|
+
return [...new Set(valueArg.split(/\r?\n/).map((entryArg) => entryArg.trim()).filter(Boolean))];
|
|
1705
|
+
}
|
|
1706
|
+
|
|
1707
|
+
private isValidExactUri(valueArg: string): boolean {
|
|
1708
|
+
if (/\s|\*|#/.test(valueArg)) {
|
|
1709
|
+
return false;
|
|
1710
|
+
}
|
|
1711
|
+
try {
|
|
1712
|
+
const url = new URL(valueArg);
|
|
1713
|
+
return !['data:', 'file:', 'javascript:'].includes(url.protocol)
|
|
1714
|
+
&& !url.username
|
|
1715
|
+
&& !url.password
|
|
1716
|
+
&& !url.hash;
|
|
1717
|
+
} catch {
|
|
1718
|
+
return false;
|
|
1719
|
+
}
|
|
1720
|
+
}
|
|
1721
|
+
|
|
1722
|
+
private setDialogNumber(fieldArg: 'access' | 'id' | 'refresh' | 'grace', eventArg: Event) {
|
|
1723
|
+
const value = Number((eventArg.target as HTMLInputElement).value);
|
|
1724
|
+
if (fieldArg === 'access') this.dialogAccessTokenLifetime = value;
|
|
1725
|
+
if (fieldArg === 'id') this.dialogIdTokenLifetime = value;
|
|
1726
|
+
if (fieldArg === 'refresh') this.dialogRefreshTokenLifetime = value;
|
|
1727
|
+
if (fieldArg === 'grace') this.dialogSecretGracePeriod = value;
|
|
1728
|
+
this.dialogError = '';
|
|
1729
|
+
}
|
|
1730
|
+
|
|
1731
|
+
private submitOidcAppConfigDialog() {
|
|
1732
|
+
if (!this.dialogApp) return;
|
|
1733
|
+
const redirectUris = this.parseLines(this.dialogRedirectUris);
|
|
1734
|
+
const postLogoutRedirectUris = this.parseLines(this.dialogPostLogoutRedirectUris);
|
|
1735
|
+
const allowedScopes = this.parseCsv(this.dialogAllowedScopes);
|
|
1736
|
+
const configuredGrantTypes = this.parseCsv(this.dialogGrantTypes);
|
|
1737
|
+
const supportedGrantTypes = ['authorization_code', 'client_credentials', 'refresh_token'];
|
|
1738
|
+
const invalidGrantTypes = configuredGrantTypes.filter((grantArg) => !supportedGrantTypes.includes(grantArg));
|
|
1739
|
+
const grantTypes = configuredGrantTypes
|
|
1740
|
+
.filter((grantArg): grantArg is 'authorization_code' | 'client_credentials' | 'refresh_token' => supportedGrantTypes.includes(grantArg));
|
|
1741
|
+
const invalidUris = [...redirectUris, ...postLogoutRedirectUris].filter((uriArg) => !this.isValidExactUri(uriArg));
|
|
1742
|
+
const lifetimesAreValid = [this.dialogAccessTokenLifetime, this.dialogIdTokenLifetime]
|
|
1743
|
+
.every((lifetimeArg) => Number.isInteger(lifetimeArg) && lifetimeArg >= 300 && lifetimeArg <= 86400)
|
|
1744
|
+
&& Number.isInteger(this.dialogRefreshTokenLifetime)
|
|
1745
|
+
&& this.dialogRefreshTokenLifetime >= 3600
|
|
1746
|
+
&& this.dialogRefreshTokenLifetime <= 7776000;
|
|
1747
|
+
if (invalidGrantTypes.length) {
|
|
1748
|
+
this.dialogError = `Unsupported grant types: ${invalidGrantTypes.join(', ')}.`;
|
|
1749
|
+
return;
|
|
1750
|
+
}
|
|
1751
|
+
if (invalidUris.length) {
|
|
1752
|
+
this.dialogError = `Invalid exact redirect URIs: ${invalidUris.join(', ')}.`;
|
|
1753
|
+
return;
|
|
1754
|
+
}
|
|
1755
|
+
if (!grantTypes.length) {
|
|
1756
|
+
this.dialogError = 'Add at least one supported grant type.';
|
|
1757
|
+
return;
|
|
1758
|
+
}
|
|
1759
|
+
if (grantTypes.includes('authorization_code') && (!redirectUris.length || !allowedScopes.includes('openid'))) {
|
|
1760
|
+
this.dialogError = 'The authorization_code grant requires a callback URI and the openid scope.';
|
|
1761
|
+
return;
|
|
1762
|
+
}
|
|
1763
|
+
if (grantTypes.includes('refresh_token') && !grantTypes.includes('authorization_code')) {
|
|
1764
|
+
this.dialogError = 'The refresh_token grant requires the authorization_code grant.';
|
|
1765
|
+
return;
|
|
1766
|
+
}
|
|
1767
|
+
if (this.dialogClientType === 'public' && grantTypes.includes('client_credentials')) {
|
|
1768
|
+
this.dialogError = 'Public clients cannot use the client_credentials grant.';
|
|
1769
|
+
return;
|
|
1770
|
+
}
|
|
1771
|
+
if (!lifetimesAreValid) {
|
|
1772
|
+
this.dialogError = 'Access and ID token lifetimes must be 300 to 86400 seconds; refresh family lifetime must be 3600 to 7776000 seconds.';
|
|
1773
|
+
return;
|
|
1774
|
+
}
|
|
1775
|
+
this.dispatchShellEvent<IIdpAdminGaAppConfigureEventDetail>('idp-admin-ga-app-configure', {
|
|
1776
|
+
appId: this.dialogApp.id,
|
|
1777
|
+
clientType: this.dialogClientType,
|
|
1778
|
+
isActive: this.dialogAppActive,
|
|
1779
|
+
redirectUris,
|
|
1780
|
+
postLogoutRedirectUris,
|
|
1781
|
+
allowedScopes,
|
|
1782
|
+
grantTypes,
|
|
1783
|
+
oidcSettings: {
|
|
1784
|
+
accessTokenLifetime: this.dialogAccessTokenLifetime,
|
|
1785
|
+
idTokenLifetime: this.dialogIdTokenLifetime,
|
|
1786
|
+
refreshTokenLifetime: this.dialogRefreshTokenLifetime,
|
|
1787
|
+
},
|
|
1788
|
+
});
|
|
1789
|
+
this.closeDialog();
|
|
1790
|
+
}
|
|
1791
|
+
|
|
1792
|
+
private submitOidcSecretRotateDialog() {
|
|
1793
|
+
if (!this.dialogApp) return;
|
|
1794
|
+
if (!Number.isInteger(this.dialogSecretGracePeriod) || this.dialogSecretGracePeriod < 0 || this.dialogSecretGracePeriod > 604800) {
|
|
1795
|
+
this.dialogError = 'Grace period must be a whole number from 0 to 604800 seconds.';
|
|
1796
|
+
return;
|
|
1797
|
+
}
|
|
1798
|
+
this.dispatchShellEvent<IIdpAdminGaAppRotateSecretEventDetail>('idp-admin-ga-app-rotate-secret', {
|
|
1799
|
+
appId: this.dialogApp.id,
|
|
1800
|
+
gracePeriodSeconds: this.dialogSecretGracePeriod,
|
|
1801
|
+
});
|
|
1802
|
+
this.closeDialog();
|
|
1803
|
+
}
|
|
1804
|
+
|
|
1600
1805
|
private setDialogRoleField(fieldArg: 'key' | 'name' | 'description' | 'deleteConfirmation', eventArg: Event) {
|
|
1601
1806
|
const value = (eventArg.target as HTMLInputElement).value;
|
|
1602
1807
|
if (fieldArg === 'key') this.dialogRoleKey = this.normalizeRoleKey(value);
|
|
@@ -1810,6 +2015,60 @@ export class IdpAdminShell extends DeesElement {
|
|
|
1810
2015
|
`;
|
|
1811
2016
|
}
|
|
1812
2017
|
|
|
2018
|
+
private renderOidcAppConfigDialog(): TemplateResult {
|
|
2019
|
+
return html`
|
|
2020
|
+
<div class="dialog-card wide" @click=${(eventArg: Event) => eventArg.stopPropagation()}>
|
|
2021
|
+
<div class="dialog-head">
|
|
2022
|
+
<div><div class="dialog-title">Configure OIDC client</div><div class="muted">${this.dialogApp?.name || 'Platform app'} uses exact callback and logout URI allowlists.</div></div>
|
|
2023
|
+
<button class="plain-button ghost" @click=${() => this.closeDialog()}>Close</button>
|
|
2024
|
+
</div>
|
|
2025
|
+
<div class="dialog-body">
|
|
2026
|
+
${this.renderDialogError()}
|
|
2027
|
+
${this.renderFormRow('Client type', 'Public clients use PKCE without a client secret.', html`
|
|
2028
|
+
<select class="input" .value=${this.dialogClientType} @change=${(eventArg: Event) => { this.dialogClientType = (eventArg.target as HTMLSelectElement).value as 'public' | 'confidential'; }}>
|
|
2029
|
+
<option value="confidential">Confidential</option>
|
|
2030
|
+
<option value="public">Public</option>
|
|
2031
|
+
</select>
|
|
2032
|
+
`, true)}
|
|
2033
|
+
${this.renderFormRow('Active', 'Deactivation rejects authorization, exchange, refresh, and UserInfo.', html`<input type="checkbox" .checked=${this.dialogAppActive} @change=${(eventArg: Event) => { this.dialogAppActive = (eventArg.target as HTMLInputElement).checked; }} />`)}
|
|
2034
|
+
${this.renderFormRow('Callback URIs', 'One exact absolute URI per line; required for authorization_code.', html`<textarea class="textarea" .value=${this.dialogRedirectUris} @input=${(eventArg: Event) => { this.dialogRedirectUris = (eventArg.target as HTMLTextAreaElement).value; }}></textarea>`)}
|
|
2035
|
+
${this.renderFormRow('Post-logout URIs', 'One exact RP-Initiated Logout destination per line.', html`<textarea class="textarea" .value=${this.dialogPostLogoutRedirectUris} @input=${(eventArg: Event) => { this.dialogPostLogoutRedirectUris = (eventArg.target as HTMLTextAreaElement).value; }}></textarea>`)}
|
|
2036
|
+
${this.renderFormRow('Allowed scopes', 'Comma-separated scopes; authorization_code requires openid.', html`<input class="input" .value=${this.dialogAllowedScopes} @input=${(eventArg: Event) => { this.dialogAllowedScopes = (eventArg.target as HTMLInputElement).value; }} />`, true)}
|
|
2037
|
+
${this.renderFormRow('Grant types', 'authorization_code, client_credentials, and refresh_token are supported.', html`<input class="input" .value=${this.dialogGrantTypes} @input=${(eventArg: Event) => { this.dialogGrantTypes = (eventArg.target as HTMLInputElement).value; }} />`, true)}
|
|
2038
|
+
<div class="mapping-row">
|
|
2039
|
+
<div>${this.renderFormRow('Access token seconds', '300 to 86400.', html`<input class="input" type="number" .value=${String(this.dialogAccessTokenLifetime)} @input=${(eventArg: Event) => this.setDialogNumber('access', eventArg)} />`, true)}</div>
|
|
2040
|
+
<div>${this.renderFormRow('ID token seconds', '300 to 86400.', html`<input class="input" type="number" .value=${String(this.dialogIdTokenLifetime)} @input=${(eventArg: Event) => this.setDialogNumber('id', eventArg)} />`, true)}</div>
|
|
2041
|
+
<div>${this.renderFormRow('Refresh family seconds', '3600 to 7776000; absolute lifetime.', html`<input class="input" type="number" .value=${String(this.dialogRefreshTokenLifetime)} @input=${(eventArg: Event) => this.setDialogNumber('refresh', eventArg)} />`, true)}</div>
|
|
2042
|
+
</div>
|
|
2043
|
+
</div>
|
|
2044
|
+
<div class="dialog-actions">
|
|
2045
|
+
<button class="plain-button outline" @click=${() => this.closeDialog()}>Cancel</button>
|
|
2046
|
+
<button class="plain-button primary" @click=${() => this.submitOidcAppConfigDialog()}>Save OIDC configuration</button>
|
|
2047
|
+
</div>
|
|
2048
|
+
</div>
|
|
2049
|
+
`;
|
|
2050
|
+
}
|
|
2051
|
+
|
|
2052
|
+
private renderOidcSecretRotateDialog(): TemplateResult {
|
|
2053
|
+
return html`
|
|
2054
|
+
<div class="dialog-card" @click=${(eventArg: Event) => eventArg.stopPropagation()}>
|
|
2055
|
+
<div class="dialog-head">
|
|
2056
|
+
<div><div class="dialog-title">Rotate client secret</div><div class="muted">The client ID remains stable. The old secret stays valid only during the overlap window.</div></div>
|
|
2057
|
+
<button class="plain-button ghost" @click=${() => this.closeDialog()}>Close</button>
|
|
2058
|
+
</div>
|
|
2059
|
+
<div class="dialog-body">
|
|
2060
|
+
${this.renderDialogError()}
|
|
2061
|
+
${this.renderFormRow('Overlap seconds', 'Default 86400 (24 hours), maximum 604800 (7 days).', html`<input class="input" type="number" min="0" max="604800" .value=${String(this.dialogSecretGracePeriod)} @input=${(eventArg: Event) => this.setDialogNumber('grace', eventArg)} />`, true)}
|
|
2062
|
+
<div class="notice-card"><idp-icon name="key" size="16"></idp-icon><div><div class="section-title">Shown once</div><div class="muted">The new plaintext secret appears once after rotation. Store it before dismissing the notice.</div></div></div>
|
|
2063
|
+
</div>
|
|
2064
|
+
<div class="dialog-actions">
|
|
2065
|
+
<button class="plain-button outline" @click=${() => this.closeDialog()}>Cancel</button>
|
|
2066
|
+
<button class="plain-button destructive" @click=${() => this.submitOidcSecretRotateDialog()}>Rotate secret</button>
|
|
2067
|
+
</div>
|
|
2068
|
+
</div>
|
|
2069
|
+
`;
|
|
2070
|
+
}
|
|
2071
|
+
|
|
1813
2072
|
private renderCreateOrgDialog(): TemplateResult {
|
|
1814
2073
|
return html`
|
|
1815
2074
|
<div class="dialog-card" @click=${(eventArg: Event) => eventArg.stopPropagation()}>
|
|
@@ -1868,7 +2127,11 @@ export class IdpAdminShell extends DeesElement {
|
|
|
1868
2127
|
? this.renderCreateOrgDialog()
|
|
1869
2128
|
: this.dialogMode === 'logout-confirm'
|
|
1870
2129
|
? this.renderLogoutConfirmDialog()
|
|
1871
|
-
: this.
|
|
2130
|
+
: this.dialogMode === 'oidc-app-config'
|
|
2131
|
+
? this.renderOidcAppConfigDialog()
|
|
2132
|
+
: this.dialogMode === 'oidc-secret-rotate'
|
|
2133
|
+
? this.renderOidcSecretRotateDialog()
|
|
2134
|
+
: this.renderAppRoleMappingsDialog();
|
|
1872
2135
|
return html`<div class="dialog-backdrop" @click=${() => this.closeDialog()}>${dialog}</div>`;
|
|
1873
2136
|
}
|
|
1874
2137
|
|
|
@@ -1904,8 +2167,17 @@ export class IdpAdminShell extends DeesElement {
|
|
|
1904
2167
|
`;
|
|
1905
2168
|
}
|
|
1906
2169
|
|
|
2170
|
+
private async copyCodeValue(valueArg: string) {
|
|
2171
|
+
try {
|
|
2172
|
+
await navigator.clipboard.writeText(valueArg);
|
|
2173
|
+
this.showToast('Copied to clipboard.');
|
|
2174
|
+
} catch {
|
|
2175
|
+
this.showToast('Clipboard access was denied. Select and copy the value manually.', 'warn');
|
|
2176
|
+
}
|
|
2177
|
+
}
|
|
2178
|
+
|
|
1907
2179
|
private renderCodeBlock(valueArg: string): TemplateResult {
|
|
1908
|
-
return html`<div class="code-block"><code>${valueArg}</code><idp-icon name="copy" size="13"></idp-icon></div>`;
|
|
2180
|
+
return html`<div class="code-block"><code>${valueArg}</code><button class="plain-button ghost" aria-label="Copy value" @click=${() => this.copyCodeValue(valueArg)}><idp-icon name="copy" size="13"></idp-icon></button></div>`;
|
|
1909
2181
|
}
|
|
1910
2182
|
|
|
1911
2183
|
private renderOverview(): TemplateResult {
|
|
@@ -2600,20 +2872,41 @@ export class IdpAdminShell extends DeesElement {
|
|
|
2600
2872
|
appArg.category || '-',
|
|
2601
2873
|
appArg.connectionCount ?? '-',
|
|
2602
2874
|
html`<idp-badge variant=${appArg.status === 'suspended' || appArg.status === 'rejected' ? 'error' : appArg.status === 'pending_review' ? 'warn' : 'ok'}>${(appArg.status || 'active').replace(/_/g, ' ')}</idp-badge>`,
|
|
2603
|
-
|
|
2604
|
-
|
|
2605
|
-
|
|
2875
|
+
html`
|
|
2876
|
+
<div class="cell-actions">
|
|
2877
|
+
${appArg.status === 'pending_review'
|
|
2878
|
+
? html`
|
|
2606
2879
|
<button class="plain-button outline" style="font-size:11px;color:var(--idp-ok);border-color:var(--idp-ok-border)" @click=${() => this.dispatchShellEvent<IIdpAdminGaAppActionEventDetail>('idp-admin-ga-app-action', { appId: appArg.id, action: 'approve' })}>Approve</button>
|
|
2607
2880
|
<button class="plain-button ghost" style="font-size:11px;color:var(--idp-destructive)" @click=${() => this.dispatchShellEvent<IIdpAdminGaAppActionEventDetail>('idp-admin-ga-app-action', { appId: appArg.id, action: 'reject' })}>Reject</button>
|
|
2608
|
-
|
|
2609
|
-
|
|
2610
|
-
|
|
2881
|
+
`
|
|
2882
|
+
: html`<button class="plain-button ghost" style="font-size:11px" @click=${() => this.dispatchShellEvent<IIdpAdminGaAppActionEventDetail>('idp-admin-ga-app-action', { appId: appArg.id, action: appArg.status === 'suspended' ? 'reinstate' : 'suspend' })}>${appArg.status === 'suspended' ? 'Reinstate' : 'Suspend'}</button>`}
|
|
2883
|
+
<button class="plain-button outline" style="font-size:11px" @click=${() => this.openOidcAppConfigDialog(appArg)}>Configure</button>
|
|
2884
|
+
${appArg.clientType === 'public'
|
|
2885
|
+
? html``
|
|
2886
|
+
: html`<button class="plain-button outline" style="font-size:11px" @click=${() => this.openOidcSecretRotateDialog(appArg)}>Rotate secret</button>`}
|
|
2887
|
+
</div>
|
|
2888
|
+
`,
|
|
2611
2889
|
],
|
|
2612
2890
|
}));
|
|
2613
2891
|
|
|
2614
2892
|
return html`
|
|
2615
2893
|
${this.renderPageHeader('Platform Apps', 'Global and partner apps across the platform.')}
|
|
2616
2894
|
<div class="body wide-body">
|
|
2895
|
+
${this.oidcClientSecret
|
|
2896
|
+
? html`
|
|
2897
|
+
<div class="notice-card" style="margin-bottom:16px" role="status" aria-live="polite">
|
|
2898
|
+
<idp-icon name="key" size="16"></idp-icon>
|
|
2899
|
+
<div style="min-width:0;flex:1">
|
|
2900
|
+
<div class="section-title">New client secret for ${this.oidcClientSecret.clientId}</div>
|
|
2901
|
+
<div class="muted" style="margin:3px 0 10px">Copy this secret now. It cannot be retrieved after this notice is dismissed.</div>
|
|
2902
|
+
${this.renderCodeBlock(this.oidcClientSecret.clientSecret)}
|
|
2903
|
+
<div class="cell-actions" style="justify-content:flex-end;margin-top:10px">
|
|
2904
|
+
<button class="plain-button outline" @click=${() => this.dispatchShellEvent<IIdpAdminGaAppSecretDismissEventDetail>('idp-admin-ga-app-secret-dismiss', { appId: this.oidcClientSecret!.appId })}>I have stored the secret</button>
|
|
2905
|
+
</div>
|
|
2906
|
+
</div>
|
|
2907
|
+
</div>
|
|
2908
|
+
`
|
|
2909
|
+
: html``}
|
|
2617
2910
|
<idp-data-table
|
|
2618
2911
|
title="Platform apps"
|
|
2619
2912
|
badge=${`${apps.length} total`}
|