@idp.global/catalog 7.2.0 → 8.1.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 +55 -0
- package/dist_ts_web/00_commitinfo_data.js +1 -1
- package/dist_ts_web/elements/idp-admin-overview.js +2 -2
- package/dist_ts_web/elements/idp-admin-shell.d.ts +51 -1
- package/dist_ts_web/elements/idp-admin-shell.js +343 -98
- package/package.json +10 -12
- package/readme.md +3 -0
- package/ts_web/00_commitinfo_data.ts +1 -1
- package/ts_web/elements/idp-admin-overview.ts +1 -1
- package/ts_web/elements/idp-admin-shell.ts +273 -28
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@idp.global/catalog",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "8.1.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "Web component catalog for idp.global, based on the v5 Apple-system design language.",
|
|
6
6
|
"exports": {
|
|
@@ -12,19 +12,17 @@
|
|
|
12
12
|
"author": "Task Venture Capital GmbH",
|
|
13
13
|
"license": "MIT",
|
|
14
14
|
"dependencies": {
|
|
15
|
-
"@design.estate/dees-catalog": "^
|
|
16
|
-
"@design.estate/dees-domtools": "^
|
|
17
|
-
"@design.estate/dees-element": "^
|
|
18
|
-
"@design.estate/dees-wcctools": "^3.9.2",
|
|
15
|
+
"@design.estate/dees-catalog": "^16.13.0",
|
|
16
|
+
"@design.estate/dees-domtools": "^3.0.1",
|
|
17
|
+
"@design.estate/dees-element": "^3.4.0",
|
|
19
18
|
"@idp.global/interfaces": "^25.1.0",
|
|
20
19
|
"lucide": "^1.24.0"
|
|
21
20
|
},
|
|
22
21
|
"devDependencies": {
|
|
23
|
-
"@
|
|
24
|
-
"@git.zone/
|
|
25
|
-
"@git.zone/tswatch": "^3.3.5",
|
|
22
|
+
"@design.estate/dees-wcctools": "^4.0.0",
|
|
23
|
+
"@git.zone/tsrepo": "^2.1.0",
|
|
26
24
|
"@push.rocks/projectinfo": "^5.1.0",
|
|
27
|
-
"@types/node": "^26.
|
|
25
|
+
"@types/node": "^26.6.6"
|
|
28
26
|
},
|
|
29
27
|
"files": [
|
|
30
28
|
"ts_web/**/*",
|
|
@@ -55,8 +53,8 @@
|
|
|
55
53
|
],
|
|
56
54
|
"scripts": {
|
|
57
55
|
"test": "pnpm run build",
|
|
58
|
-
"build": "
|
|
59
|
-
"watch": "
|
|
60
|
-
"buildDocs": "
|
|
56
|
+
"build": "tsrepo build tsfolders --allowimplicitany && tsrepo bundle",
|
|
57
|
+
"watch": "tsrepo watch",
|
|
58
|
+
"buildDocs": "tsrepo docs"
|
|
61
59
|
}
|
|
62
60
|
}
|
package/readme.md
CHANGED
|
@@ -38,6 +38,7 @@ import type {
|
|
|
38
38
|
IIdpSelectOption,
|
|
39
39
|
IIdpAdminNavigateEventDetail,
|
|
40
40
|
IIdpAdminGaAppConfigureEventDetail,
|
|
41
|
+
IIdpAdminGaAppCreateEventDetail,
|
|
41
42
|
IIdpAdminGaAppRotateSecretEventDetail,
|
|
42
43
|
IIdpAdminGaAppSecretDismissEventDetail,
|
|
43
44
|
IIdpAdminClientSecret,
|
|
@@ -46,6 +47,8 @@ import type {
|
|
|
46
47
|
|
|
47
48
|
`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
49
|
|
|
50
|
+
In the global-admin console (`superadmin` and `global-admin`), the Platform Apps page offers "New app", which dispatches `idp-admin-ga-app-create` with an `IIdpAdminGaAppCreateEventDetail` for one confidential client. The first submit freezes the payload: every retry dispatches the same values, so the host can keep one idempotent operation for it. "Discard and edit" drops the frozen draft and dispatches `idp-admin-ga-app-create-discard`, after which the host drops its pending operation. Once the app exists, the host supplies the secret through `.oidcClientSecret` and calls `completeGaAppCreate()` to clear the draft. The shell refuses a create while a secret is displayed.
|
|
51
|
+
|
|
49
52
|
## Element Surface
|
|
50
53
|
|
|
51
54
|
| Element | Purpose |
|
|
@@ -245,8 +245,8 @@ export class IdpAdminOverview extends DeesElement {
|
|
|
245
245
|
<div class="primary-grid">
|
|
246
246
|
${data?.chartSeries?.length ? html`
|
|
247
247
|
<div class="card chart-card">
|
|
248
|
-
<div class="card-head"><span class="card-title">${data.chartLabel || 'Approval activity'}</span></div>
|
|
249
248
|
<dees-chart-area
|
|
249
|
+
.frameless=${true}
|
|
250
250
|
.label=${data.chartLabel || 'Approval activity'}
|
|
251
251
|
.yAxisFormatter=${(valueArg: number) => `${Math.round(valueArg)}`}
|
|
252
252
|
.series=${data.chartSeries.map((seriesArg) => ({
|
|
@@ -151,6 +151,29 @@ export interface IIdpAdminGaAppConfigureEventDetail {
|
|
|
151
151
|
};
|
|
152
152
|
}
|
|
153
153
|
|
|
154
|
+
/**
|
|
155
|
+
* Detail of `idp-admin-ga-app-create`: one confidential global OIDC client to create.
|
|
156
|
+
* The shell freezes these fields at the first submit; every retry dispatches the same
|
|
157
|
+
* values until the host calls `completeGaAppCreate()` or the admin discards the draft,
|
|
158
|
+
* which dispatches `idp-admin-ga-app-create-discard`.
|
|
159
|
+
*/
|
|
160
|
+
export interface IIdpAdminGaAppCreateEventDetail {
|
|
161
|
+
name: string;
|
|
162
|
+
description: string;
|
|
163
|
+
logoUrl: string;
|
|
164
|
+
appUrl: string;
|
|
165
|
+
category: string;
|
|
166
|
+
redirectUris: string[];
|
|
167
|
+
postLogoutRedirectUris: string[];
|
|
168
|
+
allowedScopes: string[];
|
|
169
|
+
grantTypes: Array<'authorization_code' | 'refresh_token'>;
|
|
170
|
+
oidcSettings: {
|
|
171
|
+
accessTokenLifetime: number;
|
|
172
|
+
idTokenLifetime: number;
|
|
173
|
+
refreshTokenLifetime: number;
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
|
|
154
177
|
export interface IIdpAdminGaAppRotateSecretEventDetail {
|
|
155
178
|
appId: string;
|
|
156
179
|
gracePeriodSeconds: number;
|
|
@@ -407,7 +430,7 @@ export interface IIdpAdminPasskeyEventDetail {
|
|
|
407
430
|
|
|
408
431
|
@customElement('idp-admin-shell')
|
|
409
432
|
export class IdpAdminShell extends DeesElement {
|
|
410
|
-
public static demo = () => {
|
|
433
|
+
public static demo = [() => {
|
|
411
434
|
const now = Date.now();
|
|
412
435
|
const hours = Array.from({ length: 24 }, (unused, indexArg) => indexArg);
|
|
413
436
|
return html`
|
|
@@ -490,7 +513,21 @@ export class IdpAdminShell extends DeesElement {
|
|
|
490
513
|
]}
|
|
491
514
|
></idp-admin-shell>
|
|
492
515
|
`;
|
|
493
|
-
}
|
|
516
|
+
}, () => html`
|
|
517
|
+
<idp-admin-shell
|
|
518
|
+
superadmin
|
|
519
|
+
global-admin
|
|
520
|
+
page="ga-apps"
|
|
521
|
+
.user=${{ name: 'Alex Mercer', email: 'alex@lossless.com' }}
|
|
522
|
+
.adminApps=${[
|
|
523
|
+
{ id: 'app_foss', name: 'foss.global', type: 'global', category: 'Development', status: 'active', clientType: 'confidential' },
|
|
524
|
+
{ id: 'app_task', name: 'task.vc', type: 'global', category: 'Productivity', status: 'active', clientType: 'confidential' },
|
|
525
|
+
]}
|
|
526
|
+
@idp-admin-ga-app-create=${(eventArg: CustomEvent<IIdpAdminGaAppCreateEventDetail>) => {
|
|
527
|
+
(eventArg.target as IdpAdminShell).showToast(`Demo: create requested for ${eventArg.detail.name}.`);
|
|
528
|
+
}}
|
|
529
|
+
></idp-admin-shell>
|
|
530
|
+
`];
|
|
494
531
|
public static demoGroups = ['idp.global v5 full pages'];
|
|
495
532
|
|
|
496
533
|
public static styles = [
|
|
@@ -1176,7 +1213,7 @@ export class IdpAdminShell extends DeesElement {
|
|
|
1176
1213
|
private accessor orgSettingsError = '';
|
|
1177
1214
|
|
|
1178
1215
|
@state()
|
|
1179
|
-
private accessor dialogMode: 'none' | 'role-upsert' | 'role-delete' | 'member-roles' | 'app-role-mappings' | 'oidc-app-config' | 'oidc-secret-rotate' | 'create-org' | 'logout-confirm' = 'none';
|
|
1216
|
+
private accessor dialogMode: 'none' | 'role-upsert' | 'role-delete' | 'member-roles' | 'app-role-mappings' | 'oidc-app-config' | 'oidc-app-create' | 'oidc-secret-rotate' | 'create-org' | 'logout-confirm' = 'none';
|
|
1180
1217
|
|
|
1181
1218
|
@state()
|
|
1182
1219
|
private accessor dialogError = '';
|
|
@@ -1238,6 +1275,25 @@ export class IdpAdminShell extends DeesElement {
|
|
|
1238
1275
|
@state()
|
|
1239
1276
|
private accessor dialogSecretGracePeriod = 86400;
|
|
1240
1277
|
|
|
1278
|
+
@state()
|
|
1279
|
+
private accessor dialogAppName = '';
|
|
1280
|
+
|
|
1281
|
+
@state()
|
|
1282
|
+
private accessor dialogAppDescription = '';
|
|
1283
|
+
|
|
1284
|
+
@state()
|
|
1285
|
+
private accessor dialogAppLogoUrl = '';
|
|
1286
|
+
|
|
1287
|
+
@state()
|
|
1288
|
+
private accessor dialogAppUrl = '';
|
|
1289
|
+
|
|
1290
|
+
@state()
|
|
1291
|
+
private accessor dialogAppCategory = '';
|
|
1292
|
+
|
|
1293
|
+
/** the create payload frozen at its first submit; it outlives the dialog until resolved or discarded */
|
|
1294
|
+
@state()
|
|
1295
|
+
private accessor gaAppCreateSubmission: IIdpAdminGaAppCreateEventDetail | null = null;
|
|
1296
|
+
|
|
1241
1297
|
private get safeOrgs(): IIdpAdminOrg[] {
|
|
1242
1298
|
return Array.isArray(this.orgs) ? this.orgs.filter(Boolean) : [];
|
|
1243
1299
|
}
|
|
@@ -1651,6 +1707,11 @@ export class IdpAdminShell extends DeesElement {
|
|
|
1651
1707
|
this.dialogIdTokenLifetime = 3600;
|
|
1652
1708
|
this.dialogRefreshTokenLifetime = 2592000;
|
|
1653
1709
|
this.dialogSecretGracePeriod = 86400;
|
|
1710
|
+
this.dialogAppName = '';
|
|
1711
|
+
this.dialogAppDescription = '';
|
|
1712
|
+
this.dialogAppLogoUrl = '';
|
|
1713
|
+
this.dialogAppUrl = '';
|
|
1714
|
+
this.dialogAppCategory = '';
|
|
1654
1715
|
}
|
|
1655
1716
|
|
|
1656
1717
|
private openRoleUpsertDialog(roleArg?: IIdpAdminOrgRoleDefinition) {
|
|
@@ -1708,6 +1769,30 @@ export class IdpAdminShell extends DeesElement {
|
|
|
1708
1769
|
this.dialogRefreshTokenLifetime = appArg.refreshTokenLifetime || 2592000;
|
|
1709
1770
|
}
|
|
1710
1771
|
|
|
1772
|
+
private openOidcAppCreateDialog() {
|
|
1773
|
+
this.closeDialog();
|
|
1774
|
+
this.dialogMode = 'oidc-app-create';
|
|
1775
|
+
const submission = this.gaAppCreateSubmission;
|
|
1776
|
+
if (submission) {
|
|
1777
|
+
// a submitted draft reopens exactly as it was sent, locked
|
|
1778
|
+
this.dialogAppName = submission.name;
|
|
1779
|
+
this.dialogAppDescription = submission.description;
|
|
1780
|
+
this.dialogAppLogoUrl = submission.logoUrl;
|
|
1781
|
+
this.dialogAppUrl = submission.appUrl;
|
|
1782
|
+
this.dialogAppCategory = submission.category;
|
|
1783
|
+
this.dialogRedirectUris = submission.redirectUris.join('\n');
|
|
1784
|
+
this.dialogPostLogoutRedirectUris = submission.postLogoutRedirectUris.join('\n');
|
|
1785
|
+
this.dialogAllowedScopes = submission.allowedScopes.join(', ');
|
|
1786
|
+
this.dialogGrantTypes = submission.grantTypes.join(', ');
|
|
1787
|
+
this.dialogAccessTokenLifetime = submission.oidcSettings.accessTokenLifetime;
|
|
1788
|
+
this.dialogIdTokenLifetime = submission.oidcSettings.idTokenLifetime;
|
|
1789
|
+
this.dialogRefreshTokenLifetime = submission.oidcSettings.refreshTokenLifetime;
|
|
1790
|
+
return;
|
|
1791
|
+
}
|
|
1792
|
+
this.dialogAllowedScopes = 'openid, profile, email';
|
|
1793
|
+
this.dialogGrantTypes = 'authorization_code, refresh_token';
|
|
1794
|
+
}
|
|
1795
|
+
|
|
1711
1796
|
private openOidcSecretRotateDialog(appArg: IIdpAdminApp) {
|
|
1712
1797
|
this.dialogMode = 'oidc-secret-rotate';
|
|
1713
1798
|
this.dialogError = '';
|
|
@@ -1743,8 +1828,11 @@ export class IdpAdminShell extends DeesElement {
|
|
|
1743
1828
|
this.dialogError = '';
|
|
1744
1829
|
}
|
|
1745
1830
|
|
|
1746
|
-
|
|
1747
|
-
|
|
1831
|
+
/** validates the OIDC client fields the configure and create dialogs share; sets dialogError and returns null when invalid */
|
|
1832
|
+
private validateOidcClientDialogFields(): Pick<
|
|
1833
|
+
IIdpAdminGaAppConfigureEventDetail,
|
|
1834
|
+
'redirectUris' | 'postLogoutRedirectUris' | 'allowedScopes' | 'grantTypes' | 'oidcSettings'
|
|
1835
|
+
> | null {
|
|
1748
1836
|
const redirectUris = this.parseLines(this.dialogRedirectUris);
|
|
1749
1837
|
const postLogoutRedirectUris = this.parseLines(this.dialogPostLogoutRedirectUris);
|
|
1750
1838
|
const allowedScopes = this.parseCsv(this.dialogAllowedScopes);
|
|
@@ -1761,32 +1849,29 @@ export class IdpAdminShell extends DeesElement {
|
|
|
1761
1849
|
&& this.dialogRefreshTokenLifetime <= 7776000;
|
|
1762
1850
|
if (invalidGrantTypes.length) {
|
|
1763
1851
|
this.dialogError = `Unsupported grant types: ${invalidGrantTypes.join(', ')}.`;
|
|
1764
|
-
return;
|
|
1852
|
+
return null;
|
|
1765
1853
|
}
|
|
1766
1854
|
if (invalidUris.length) {
|
|
1767
1855
|
this.dialogError = `Invalid exact redirect URIs: ${invalidUris.join(', ')}.`;
|
|
1768
|
-
return;
|
|
1856
|
+
return null;
|
|
1769
1857
|
}
|
|
1770
1858
|
if (!grantTypes.length) {
|
|
1771
1859
|
this.dialogError = 'Add at least one supported grant type.';
|
|
1772
|
-
return;
|
|
1860
|
+
return null;
|
|
1773
1861
|
}
|
|
1774
1862
|
if (grantTypes.includes('authorization_code') && (!redirectUris.length || !allowedScopes.includes('openid'))) {
|
|
1775
1863
|
this.dialogError = 'The authorization_code grant requires a callback URI and the openid scope.';
|
|
1776
|
-
return;
|
|
1864
|
+
return null;
|
|
1777
1865
|
}
|
|
1778
1866
|
if (grantTypes.includes('refresh_token') && !grantTypes.includes('authorization_code')) {
|
|
1779
1867
|
this.dialogError = 'The refresh_token grant requires the authorization_code grant.';
|
|
1780
|
-
return;
|
|
1868
|
+
return null;
|
|
1781
1869
|
}
|
|
1782
1870
|
if (!lifetimesAreValid) {
|
|
1783
1871
|
this.dialogError = 'Access and ID token lifetimes must be 300 to 86400 seconds; refresh family lifetime must be 3600 to 7776000 seconds.';
|
|
1784
|
-
return;
|
|
1872
|
+
return null;
|
|
1785
1873
|
}
|
|
1786
|
-
|
|
1787
|
-
appId: this.dialogApp.id,
|
|
1788
|
-
clientType: this.dialogClientType,
|
|
1789
|
-
isActive: this.dialogAppActive,
|
|
1874
|
+
return {
|
|
1790
1875
|
redirectUris,
|
|
1791
1876
|
postLogoutRedirectUris,
|
|
1792
1877
|
allowedScopes,
|
|
@@ -1796,10 +1881,111 @@ export class IdpAdminShell extends DeesElement {
|
|
|
1796
1881
|
idTokenLifetime: this.dialogIdTokenLifetime,
|
|
1797
1882
|
refreshTokenLifetime: this.dialogRefreshTokenLifetime,
|
|
1798
1883
|
},
|
|
1884
|
+
};
|
|
1885
|
+
}
|
|
1886
|
+
|
|
1887
|
+
private submitOidcAppConfigDialog() {
|
|
1888
|
+
if (!this.dialogApp) return;
|
|
1889
|
+
const oidcClientFields = this.validateOidcClientDialogFields();
|
|
1890
|
+
if (!oidcClientFields) return;
|
|
1891
|
+
this.dispatchShellEvent<IIdpAdminGaAppConfigureEventDetail>('idp-admin-ga-app-configure', {
|
|
1892
|
+
appId: this.dialogApp.id,
|
|
1893
|
+
clientType: this.dialogClientType,
|
|
1894
|
+
isActive: this.dialogAppActive,
|
|
1895
|
+
...oidcClientFields,
|
|
1799
1896
|
});
|
|
1800
1897
|
this.closeDialog();
|
|
1801
1898
|
}
|
|
1802
1899
|
|
|
1900
|
+
/** an HTTPS URL without credentials, as the app stores registered app and logo URLs */
|
|
1901
|
+
private isValidHttpsUrl(valueArg: string): boolean {
|
|
1902
|
+
try {
|
|
1903
|
+
const url = new URL(valueArg);
|
|
1904
|
+
return url.protocol === 'https:' && !url.username && !url.password;
|
|
1905
|
+
} catch {
|
|
1906
|
+
return false;
|
|
1907
|
+
}
|
|
1908
|
+
}
|
|
1909
|
+
|
|
1910
|
+
/** the app metadata limits idp.global stores: lengths in UTF-16 code units, no control characters */
|
|
1911
|
+
private validateGaAppCreateMetadata(): Pick<
|
|
1912
|
+
IIdpAdminGaAppCreateEventDetail,
|
|
1913
|
+
'name' | 'description' | 'logoUrl' | 'appUrl' | 'category'
|
|
1914
|
+
> | null {
|
|
1915
|
+
const controlCharacters = /[\u0000-\u001f\u007f]/;
|
|
1916
|
+
const name = this.dialogAppName.trim();
|
|
1917
|
+
const description = this.dialogAppDescription.trim();
|
|
1918
|
+
const logoUrl = this.dialogAppLogoUrl.trim();
|
|
1919
|
+
const appUrl = this.dialogAppUrl.trim();
|
|
1920
|
+
const category = this.dialogAppCategory.trim();
|
|
1921
|
+
if (!name || name.length > 512 || controlCharacters.test(name)) {
|
|
1922
|
+
this.dialogError = 'Enter an app name of at most 512 characters without control characters.';
|
|
1923
|
+
return null;
|
|
1924
|
+
}
|
|
1925
|
+
if (description.length > 8192 || controlCharacters.test(description)) {
|
|
1926
|
+
this.dialogError = 'The description must be at most 8192 characters without control characters.';
|
|
1927
|
+
return null;
|
|
1928
|
+
}
|
|
1929
|
+
if (!appUrl || appUrl.length > 2048 || !this.isValidHttpsUrl(appUrl)) {
|
|
1930
|
+
this.dialogError = 'Enter the app URL as an absolute HTTPS URL without credentials.';
|
|
1931
|
+
return null;
|
|
1932
|
+
}
|
|
1933
|
+
if (logoUrl && (logoUrl.length > 2048 || !this.isValidHttpsUrl(logoUrl))) {
|
|
1934
|
+
this.dialogError = 'The logo URL must be an absolute HTTPS URL without credentials, or empty.';
|
|
1935
|
+
return null;
|
|
1936
|
+
}
|
|
1937
|
+
if (!category || category.length > 256 || controlCharacters.test(category)) {
|
|
1938
|
+
this.dialogError = 'Enter a category of at most 256 characters without control characters.';
|
|
1939
|
+
return null;
|
|
1940
|
+
}
|
|
1941
|
+
return { name, description, logoUrl, appUrl, category };
|
|
1942
|
+
}
|
|
1943
|
+
|
|
1944
|
+
/** only one one-time secret is on screen at a time */
|
|
1945
|
+
private ensureNoDisplayedClientSecret(): boolean {
|
|
1946
|
+
if (this.oidcClientSecret) {
|
|
1947
|
+
this.dialogError = 'Store and dismiss the displayed client secret before creating another app.';
|
|
1948
|
+
return false;
|
|
1949
|
+
}
|
|
1950
|
+
return true;
|
|
1951
|
+
}
|
|
1952
|
+
|
|
1953
|
+
private submitOidcAppCreateDialog() {
|
|
1954
|
+
if (!this.ensureNoDisplayedClientSecret()) return;
|
|
1955
|
+
// the first submit freezes the payload; every retry resends exactly it
|
|
1956
|
+
let submission = this.gaAppCreateSubmission;
|
|
1957
|
+
if (!submission) {
|
|
1958
|
+
const metadata = this.validateGaAppCreateMetadata();
|
|
1959
|
+
if (!metadata) return;
|
|
1960
|
+
const oidcClientFields = this.validateOidcClientDialogFields();
|
|
1961
|
+
if (!oidcClientFields) return;
|
|
1962
|
+
submission = { ...metadata, ...oidcClientFields };
|
|
1963
|
+
this.gaAppCreateSubmission = submission;
|
|
1964
|
+
}
|
|
1965
|
+
// a fresh copy per dispatch, so a listener can never alter the frozen draft
|
|
1966
|
+
this.dispatchShellEvent<IIdpAdminGaAppCreateEventDetail>('idp-admin-ga-app-create', structuredClone(submission));
|
|
1967
|
+
this.closeDialog();
|
|
1968
|
+
}
|
|
1969
|
+
|
|
1970
|
+
/** drops the frozen draft and unlocks its values for editing; the next submit is a new creation */
|
|
1971
|
+
private discardOidcAppCreate() {
|
|
1972
|
+
if (!this.gaAppCreateSubmission) return;
|
|
1973
|
+
this.gaAppCreateSubmission = null;
|
|
1974
|
+
this.dialogError = '';
|
|
1975
|
+
this.dispatchShellEvent('idp-admin-ga-app-create-discard', {});
|
|
1976
|
+
}
|
|
1977
|
+
|
|
1978
|
+
/**
|
|
1979
|
+
* The host calls this once `idp-admin-ga-app-create` has created the app (after it set
|
|
1980
|
+
* `oidcClientSecret`): the frozen draft is cleared and an open create dialog closes.
|
|
1981
|
+
*/
|
|
1982
|
+
public completeGaAppCreate() {
|
|
1983
|
+
this.gaAppCreateSubmission = null;
|
|
1984
|
+
if (this.dialogMode === 'oidc-app-create') {
|
|
1985
|
+
this.closeDialog();
|
|
1986
|
+
}
|
|
1987
|
+
}
|
|
1988
|
+
|
|
1803
1989
|
private submitOidcSecretRotateDialog() {
|
|
1804
1990
|
if (!this.dialogApp) return;
|
|
1805
1991
|
if (!Number.isInteger(this.dialogSecretGracePeriod) || this.dialogSecretGracePeriod < 0 || this.dialogSecretGracePeriod > 604800) {
|
|
@@ -2068,15 +2254,7 @@ export class IdpAdminShell extends DeesElement {
|
|
|
2068
2254
|
</select>
|
|
2069
2255
|
`, true)}
|
|
2070
2256
|
${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; }} />`)}
|
|
2071
|
-
${this.
|
|
2072
|
-
${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>`)}
|
|
2073
|
-
${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)}
|
|
2074
|
-
${this.renderFormRow('Grant types', 'authorization_code and refresh_token are supported.', html`<input class="input" .value=${this.dialogGrantTypes} @input=${(eventArg: Event) => { this.dialogGrantTypes = (eventArg.target as HTMLInputElement).value; }} />`, true)}
|
|
2075
|
-
<div class="mapping-row">
|
|
2076
|
-
<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>
|
|
2077
|
-
<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>
|
|
2078
|
-
<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>
|
|
2079
|
-
</div>
|
|
2257
|
+
${this.renderOidcClientFieldRows(false)}
|
|
2080
2258
|
</div>
|
|
2081
2259
|
<div class="dialog-actions">
|
|
2082
2260
|
<button class="plain-button outline" @click=${() => this.closeDialog()}>Cancel</button>
|
|
@@ -2086,6 +2264,67 @@ export class IdpAdminShell extends DeesElement {
|
|
|
2086
2264
|
`;
|
|
2087
2265
|
}
|
|
2088
2266
|
|
|
2267
|
+
/** the OIDC client rows the configure and create dialogs share */
|
|
2268
|
+
private renderOidcClientFieldRows(lockedArg: boolean): TemplateResult {
|
|
2269
|
+
return html`
|
|
2270
|
+
${this.renderFormRow('Callback URIs', 'One exact absolute URI per line; required for authorization_code.', html`<textarea class="textarea" .value=${this.dialogRedirectUris} ?disabled=${lockedArg} @input=${(eventArg: Event) => { this.dialogRedirectUris = (eventArg.target as HTMLTextAreaElement).value; }}></textarea>`)}
|
|
2271
|
+
${this.renderFormRow('Post-logout URIs', 'One exact RP-Initiated Logout destination per line.', html`<textarea class="textarea" .value=${this.dialogPostLogoutRedirectUris} ?disabled=${lockedArg} @input=${(eventArg: Event) => { this.dialogPostLogoutRedirectUris = (eventArg.target as HTMLTextAreaElement).value; }}></textarea>`)}
|
|
2272
|
+
${this.renderFormRow('Allowed scopes', 'Comma-separated scopes; authorization_code requires openid.', html`<input class="input" .value=${this.dialogAllowedScopes} ?disabled=${lockedArg} @input=${(eventArg: Event) => { this.dialogAllowedScopes = (eventArg.target as HTMLInputElement).value; }} />`, true)}
|
|
2273
|
+
${this.renderFormRow('Grant types', 'authorization_code and refresh_token are supported.', html`<input class="input" .value=${this.dialogGrantTypes} ?disabled=${lockedArg} @input=${(eventArg: Event) => { this.dialogGrantTypes = (eventArg.target as HTMLInputElement).value; }} />`, true)}
|
|
2274
|
+
<div class="mapping-row">
|
|
2275
|
+
<div>${this.renderFormRow('Access token seconds', '300 to 86400.', html`<input class="input" type="number" .value=${String(this.dialogAccessTokenLifetime)} ?disabled=${lockedArg} @input=${(eventArg: Event) => this.setDialogNumber('access', eventArg)} />`, true)}</div>
|
|
2276
|
+
<div>${this.renderFormRow('ID token seconds', '300 to 86400.', html`<input class="input" type="number" .value=${String(this.dialogIdTokenLifetime)} ?disabled=${lockedArg} @input=${(eventArg: Event) => this.setDialogNumber('id', eventArg)} />`, true)}</div>
|
|
2277
|
+
<div>${this.renderFormRow('Refresh family seconds', '3600 to 7776000; absolute lifetime.', html`<input class="input" type="number" .value=${String(this.dialogRefreshTokenLifetime)} ?disabled=${lockedArg} @input=${(eventArg: Event) => this.setDialogNumber('refresh', eventArg)} />`, true)}</div>
|
|
2278
|
+
</div>
|
|
2279
|
+
`;
|
|
2280
|
+
}
|
|
2281
|
+
|
|
2282
|
+
private setDialogAppField(fieldArg: 'name' | 'description' | 'logoUrl' | 'appUrl' | 'category', eventArg: Event) {
|
|
2283
|
+
const value = (eventArg.target as HTMLInputElement).value;
|
|
2284
|
+
if (fieldArg === 'name') this.dialogAppName = value;
|
|
2285
|
+
if (fieldArg === 'description') this.dialogAppDescription = value;
|
|
2286
|
+
if (fieldArg === 'logoUrl') this.dialogAppLogoUrl = value;
|
|
2287
|
+
if (fieldArg === 'appUrl') this.dialogAppUrl = value;
|
|
2288
|
+
if (fieldArg === 'category') this.dialogAppCategory = value;
|
|
2289
|
+
this.dialogError = '';
|
|
2290
|
+
}
|
|
2291
|
+
|
|
2292
|
+
private renderOidcAppCreateDialog(): TemplateResult {
|
|
2293
|
+
const locked = this.gaAppCreateSubmission !== null;
|
|
2294
|
+
return html`
|
|
2295
|
+
<div class="dialog-card wide" @click=${(eventArg: Event) => eventArg.stopPropagation()}>
|
|
2296
|
+
<div class="dialog-head">
|
|
2297
|
+
<div><div class="dialog-title">Create global app</div><div class="muted">Registers a confidential OIDC client. Its client secret is shown once, after creation.</div></div>
|
|
2298
|
+
<button class="plain-button ghost" @click=${() => this.closeDialog()}>Close</button>
|
|
2299
|
+
</div>
|
|
2300
|
+
<div class="dialog-body">
|
|
2301
|
+
${this.renderDialogError()}
|
|
2302
|
+
<div class="notice-card"><idp-icon name="key" size="16"></idp-icon><div><div class="section-title">Passkey required</div><div class="muted">Creating a confidential client requires a passkey sign-in within the last 5 minutes.</div></div></div>
|
|
2303
|
+
${locked
|
|
2304
|
+
? html`<div class="notice-card" style="border-color:var(--idp-warn-border);background:var(--idp-warn-bg)"><idp-icon name="alert" size="16" style="color:var(--idp-warn)"></idp-icon><div><div class="section-title" style="color:var(--idp-warn)">Submitted values are locked</div><div class="muted">Retry sends exactly these values again. To change them, discard this draft.</div></div></div>`
|
|
2305
|
+
: html``}
|
|
2306
|
+
${this.renderFormRow('App name', 'Shown on consent screens and in the app directory.', html`<input class="input" maxlength="512" .value=${this.dialogAppName} ?disabled=${locked} @input=${(eventArg: Event) => this.setDialogAppField('name', eventArg)} />`, true)}
|
|
2307
|
+
${this.renderFormRow('Description', 'Optional one-line summary.', html`<input class="input" maxlength="8192" .value=${this.dialogAppDescription} ?disabled=${locked} @input=${(eventArg: Event) => this.setDialogAppField('description', eventArg)} />`)}
|
|
2308
|
+
${this.renderFormRow('App URL', 'Absolute HTTPS URL of the app.', html`<input class="input" type="url" maxlength="2048" placeholder="https://app.example.com" .value=${this.dialogAppUrl} ?disabled=${locked} @input=${(eventArg: Event) => this.setDialogAppField('appUrl', eventArg)} />`, true)}
|
|
2309
|
+
${this.renderFormRow('Logo URL', 'Optional absolute HTTPS URL of the app logo.', html`<input class="input" type="url" maxlength="2048" .value=${this.dialogAppLogoUrl} ?disabled=${locked} @input=${(eventArg: Event) => this.setDialogAppField('logoUrl', eventArg)} />`)}
|
|
2310
|
+
${this.renderFormRow('Category', 'Free text, at most 256 characters, for example Development.', html`<input class="input" maxlength="256" .value=${this.dialogAppCategory} ?disabled=${locked} @input=${(eventArg: Event) => this.setDialogAppField('category', eventArg)} />`, true)}
|
|
2311
|
+
${this.renderOidcClientFieldRows(locked)}
|
|
2312
|
+
</div>
|
|
2313
|
+
<div class="dialog-actions">
|
|
2314
|
+
${locked
|
|
2315
|
+
? html`
|
|
2316
|
+
<button class="plain-button outline" @click=${() => this.discardOidcAppCreate()}>Discard and edit</button>
|
|
2317
|
+
<button class="plain-button primary" @click=${() => this.submitOidcAppCreateDialog()}>Retry create</button>
|
|
2318
|
+
`
|
|
2319
|
+
: html`
|
|
2320
|
+
<button class="plain-button outline" @click=${() => this.closeDialog()}>Cancel</button>
|
|
2321
|
+
<button class="plain-button primary" @click=${() => this.submitOidcAppCreateDialog()}>Create app</button>
|
|
2322
|
+
`}
|
|
2323
|
+
</div>
|
|
2324
|
+
</div>
|
|
2325
|
+
`;
|
|
2326
|
+
}
|
|
2327
|
+
|
|
2089
2328
|
private renderOidcSecretRotateDialog(): TemplateResult {
|
|
2090
2329
|
return html`
|
|
2091
2330
|
<div class="dialog-card" @click=${(eventArg: Event) => eventArg.stopPropagation()}>
|
|
@@ -2166,9 +2405,11 @@ export class IdpAdminShell extends DeesElement {
|
|
|
2166
2405
|
? this.renderLogoutConfirmDialog()
|
|
2167
2406
|
: this.dialogMode === 'oidc-app-config'
|
|
2168
2407
|
? this.renderOidcAppConfigDialog()
|
|
2169
|
-
: this.dialogMode === 'oidc-
|
|
2170
|
-
? this.
|
|
2171
|
-
: this.
|
|
2408
|
+
: this.dialogMode === 'oidc-app-create'
|
|
2409
|
+
? this.renderOidcAppCreateDialog()
|
|
2410
|
+
: this.dialogMode === 'oidc-secret-rotate'
|
|
2411
|
+
? this.renderOidcSecretRotateDialog()
|
|
2412
|
+
: this.renderAppRoleMappingsDialog();
|
|
2172
2413
|
return html`<div class="dialog-backdrop" @click=${() => this.closeDialog()}>${dialog}</div>`;
|
|
2173
2414
|
}
|
|
2174
2415
|
|
|
@@ -2924,8 +3165,12 @@ export class IdpAdminShell extends DeesElement {
|
|
|
2924
3165
|
],
|
|
2925
3166
|
}));
|
|
2926
3167
|
|
|
3168
|
+
const createAction = this.superadmin && this.globalAdmin
|
|
3169
|
+
? html`<button class="plain-button primary" @click=${() => this.openOidcAppCreateDialog()}><idp-icon name="plus" size="12"></idp-icon>New app</button>`
|
|
3170
|
+
: undefined;
|
|
3171
|
+
|
|
2927
3172
|
return html`
|
|
2928
|
-
${this.renderPageHeader('Platform Apps', 'Global and partner apps across the platform.')}
|
|
3173
|
+
${this.renderPageHeader('Platform Apps', 'Global and partner apps across the platform.', createAction)}
|
|
2929
3174
|
<div class="body wide-body">
|
|
2930
3175
|
${this.oidcClientSecret
|
|
2931
3176
|
? html`
|