@idp.global/catalog 2.2.0 → 3.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 +21 -0
- package/dist_bundle/bundle.js +1175 -436
- package/dist_ts_web/00_commitinfo_data.js +1 -1
- package/dist_ts_web/elements/idp-admin-shell.d.ts +30 -1
- package/dist_ts_web/elements/idp-admin-shell.js +261 -28
- package/dist_ts_web/elements/idp-admin-sidebar.js +6 -5
- package/dist_ts_web/elements/idp-invite-card.d.ts +39 -0
- package/dist_ts_web/elements/idp-invite-card.js +303 -0
- package/dist_ts_web/elements/idp-scim-token-panel.d.ts +43 -0
- package/dist_ts_web/elements/idp-scim-token-panel.js +361 -0
- package/dist_ts_web/elements/idp-sso-connection-form.d.ts +81 -0
- package/dist_ts_web/elements/idp-sso-connection-form.js +637 -0
- package/dist_ts_web/elements/index.d.ts +3 -0
- package/dist_ts_web/elements/index.js +4 -1
- package/package.json +1 -1
- package/ts_web/00_commitinfo_data.ts +1 -1
- package/ts_web/elements/idp-admin-shell.ts +237 -25
- package/ts_web/elements/idp-admin-sidebar.ts +5 -4
- package/ts_web/elements/idp-invite-card.ts +229 -0
- package/ts_web/elements/idp-scim-token-panel.ts +311 -0
- package/ts_web/elements/idp-sso-connection-form.ts +574 -0
- package/ts_web/elements/index.ts +3 -0
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
import { DeesElement, html, property, customElement, css, type TemplateResult } from '@design.estate/dees-element';
|
|
2
|
+
import { idpElementStyles } from './tokens.js';
|
|
3
|
+
import './idp-auth-card.js';
|
|
4
|
+
import './idp-option-row.js';
|
|
5
|
+
import './idp-status-icon.js';
|
|
6
|
+
import './idp-button.js';
|
|
7
|
+
import './idp-spinner.js';
|
|
8
|
+
|
|
9
|
+
export type TIdpInviteState =
|
|
10
|
+
| 'loading'
|
|
11
|
+
| 'valid'
|
|
12
|
+
| 'accepted'
|
|
13
|
+
| 'expired'
|
|
14
|
+
| 'cancelled'
|
|
15
|
+
| 'invalid'
|
|
16
|
+
| 'email-mismatch';
|
|
17
|
+
|
|
18
|
+
declare global {
|
|
19
|
+
interface HTMLElementTagNameMap {
|
|
20
|
+
'idp-invite-card': IdpInviteCard;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Invitation landing card. Purely presentational: the host loads the
|
|
26
|
+
* invitation, decides the state, and reacts to the events
|
|
27
|
+
* idp-invite-accept, idp-invite-signin, idp-invite-register,
|
|
28
|
+
* idp-invite-switch-account and idp-invite-continue.
|
|
29
|
+
*/
|
|
30
|
+
@customElement('idp-invite-card')
|
|
31
|
+
export class IdpInviteCard extends DeesElement {
|
|
32
|
+
public static demo = () => html`
|
|
33
|
+
<div style="background: var(--sh-bg, #f5f5f7); padding: 40px; display: flex; flex-wrap: wrap; gap: 24px; justify-content: center;">
|
|
34
|
+
<idp-invite-card state="valid" email="alex@lossless.com" .orgNames=${['Lossless GmbH']} .roles=${['member']} signedIn></idp-invite-card>
|
|
35
|
+
<idp-invite-card state="valid" email="alex@lossless.com" .orgNames=${['Lossless GmbH']}></idp-invite-card>
|
|
36
|
+
<idp-invite-card state="email-mismatch" email="alex@lossless.com" signedIn signedInEmail="sam@other.io"></idp-invite-card>
|
|
37
|
+
<idp-invite-card state="expired" .orgNames=${['Lossless GmbH']}></idp-invite-card>
|
|
38
|
+
<idp-invite-card state="accepted" .orgNames=${['Lossless GmbH']}></idp-invite-card>
|
|
39
|
+
<idp-invite-card state="invalid"></idp-invite-card>
|
|
40
|
+
</div>
|
|
41
|
+
`;
|
|
42
|
+
public static demoGroups = ['idp.global v5 auth'];
|
|
43
|
+
|
|
44
|
+
@property({ type: String, reflect: true })
|
|
45
|
+
public accessor state: TIdpInviteState = 'loading';
|
|
46
|
+
|
|
47
|
+
/** email address the invitation was issued to */
|
|
48
|
+
@property({ type: String })
|
|
49
|
+
public accessor email = '';
|
|
50
|
+
|
|
51
|
+
/** names of the organizations the invitation grants membership in */
|
|
52
|
+
@property({ type: Array })
|
|
53
|
+
public accessor orgNames: string[] = [];
|
|
54
|
+
|
|
55
|
+
/** roles granted on acceptance */
|
|
56
|
+
@property({ type: Array })
|
|
57
|
+
public accessor roles: string[] = [];
|
|
58
|
+
|
|
59
|
+
/** whether a user session exists */
|
|
60
|
+
@property({ type: Boolean })
|
|
61
|
+
public accessor signedIn = false;
|
|
62
|
+
|
|
63
|
+
/** email of the currently signed-in user (shown on email-mismatch) */
|
|
64
|
+
@property({ type: String })
|
|
65
|
+
public accessor signedInEmail = '';
|
|
66
|
+
|
|
67
|
+
/** disables actions while the host performs a request */
|
|
68
|
+
@property({ type: Boolean })
|
|
69
|
+
public accessor busy = false;
|
|
70
|
+
|
|
71
|
+
public static styles = [
|
|
72
|
+
...idpElementStyles,
|
|
73
|
+
css`
|
|
74
|
+
:host {
|
|
75
|
+
display: block;
|
|
76
|
+
font-family: var(--idp-font);
|
|
77
|
+
}
|
|
78
|
+
.status-block {
|
|
79
|
+
display: flex;
|
|
80
|
+
flex-direction: column;
|
|
81
|
+
align-items: center;
|
|
82
|
+
gap: 14px;
|
|
83
|
+
padding: 18px 0 6px;
|
|
84
|
+
text-align: center;
|
|
85
|
+
}
|
|
86
|
+
.status-text {
|
|
87
|
+
font-size: 13px;
|
|
88
|
+
color: var(--idp-fg-2);
|
|
89
|
+
line-height: 1.5;
|
|
90
|
+
max-width: 280px;
|
|
91
|
+
}
|
|
92
|
+
.detail-rows {
|
|
93
|
+
display: flex;
|
|
94
|
+
flex-direction: column;
|
|
95
|
+
gap: 10px;
|
|
96
|
+
margin-top: 16px;
|
|
97
|
+
}
|
|
98
|
+
.actions {
|
|
99
|
+
display: flex;
|
|
100
|
+
flex-direction: column;
|
|
101
|
+
gap: 10px;
|
|
102
|
+
margin-top: 18px;
|
|
103
|
+
}
|
|
104
|
+
.loading-block {
|
|
105
|
+
display: flex;
|
|
106
|
+
justify-content: center;
|
|
107
|
+
padding: 34px 0;
|
|
108
|
+
}
|
|
109
|
+
`,
|
|
110
|
+
];
|
|
111
|
+
|
|
112
|
+
private get orgLabel(): string {
|
|
113
|
+
if (!this.orgNames.length) return 'an organisation';
|
|
114
|
+
if (this.orgNames.length === 1) return this.orgNames[0];
|
|
115
|
+
return `${this.orgNames[0]} and ${this.orgNames.length - 1} more`;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
public render(): TemplateResult {
|
|
119
|
+
const renderers: Record<TIdpInviteState, () => { heading: string; description: string; body: TemplateResult }> = {
|
|
120
|
+
loading: () => ({
|
|
121
|
+
heading: 'Checking your invitation…',
|
|
122
|
+
description: '',
|
|
123
|
+
body: html`<div class="loading-block" role="status" aria-label="Loading invitation"><idp-spinner size="30"></idp-spinner></div>`,
|
|
124
|
+
}),
|
|
125
|
+
valid: () => ({
|
|
126
|
+
heading: `Join ${this.orgLabel}`,
|
|
127
|
+
description: `You've been invited to join ${this.orgLabel} on idp.global.`,
|
|
128
|
+
body: html`
|
|
129
|
+
<div class="detail-rows">
|
|
130
|
+
<idp-option-row interactive="false" tileTone="accent" tileSize="30" icon="mail" label="Invitation for" description=${this.email}></idp-option-row>
|
|
131
|
+
${this.roles.length
|
|
132
|
+
? html`<idp-option-row interactive="false" tileTone="accent" tileSize="30" icon="shield" label="You'll join as" description=${this.roles.join(', ')}></idp-option-row>`
|
|
133
|
+
: html``}
|
|
134
|
+
</div>
|
|
135
|
+
<div class="actions">
|
|
136
|
+
${this.signedIn
|
|
137
|
+
? html`<idp-button variant="accent" shape="rect" block ?loading=${this.busy} @click=${() => this.emit('idp-invite-accept')}>Accept invitation</idp-button>`
|
|
138
|
+
: html`
|
|
139
|
+
<idp-button variant="accent" shape="rect" block ?disabled=${this.busy} @click=${() => this.emit('idp-invite-signin')}>Sign in to accept</idp-button>
|
|
140
|
+
<idp-button variant="outline" shape="rect" block ?disabled=${this.busy} @click=${() => this.emit('idp-invite-register')}>Create an account</idp-button>
|
|
141
|
+
`}
|
|
142
|
+
</div>
|
|
143
|
+
`,
|
|
144
|
+
}),
|
|
145
|
+
accepted: () => ({
|
|
146
|
+
heading: `Welcome to ${this.orgLabel}`,
|
|
147
|
+
description: '',
|
|
148
|
+
body: html`
|
|
149
|
+
<div class="status-block">
|
|
150
|
+
<idp-status-icon tone="ok" icon="check"></idp-status-icon>
|
|
151
|
+
<div class="status-text">Your invitation has been accepted. You're now a member of ${this.orgLabel}.</div>
|
|
152
|
+
</div>
|
|
153
|
+
<div class="actions">
|
|
154
|
+
<idp-button variant="accent" shape="rect" block @click=${() => this.emit('idp-invite-continue')}>Continue to dashboard</idp-button>
|
|
155
|
+
</div>
|
|
156
|
+
`,
|
|
157
|
+
}),
|
|
158
|
+
expired: () => ({
|
|
159
|
+
heading: 'Invitation expired',
|
|
160
|
+
description: '',
|
|
161
|
+
body: html`
|
|
162
|
+
<div class="status-block">
|
|
163
|
+
<idp-status-icon tone="warn" icon="clock"></idp-status-icon>
|
|
164
|
+
<div class="status-text">This invitation to ${this.orgLabel} is no longer valid. Ask an organisation admin to send you a new one.</div>
|
|
165
|
+
</div>
|
|
166
|
+
<div class="actions">
|
|
167
|
+
<idp-button variant="outline" shape="rect" block @click=${() => this.emit('idp-invite-continue')}>Go to idp.global</idp-button>
|
|
168
|
+
</div>
|
|
169
|
+
`,
|
|
170
|
+
}),
|
|
171
|
+
cancelled: () => ({
|
|
172
|
+
heading: 'Invitation withdrawn',
|
|
173
|
+
description: '',
|
|
174
|
+
body: html`
|
|
175
|
+
<div class="status-block">
|
|
176
|
+
<idp-status-icon tone="warn" icon="ban"></idp-status-icon>
|
|
177
|
+
<div class="status-text">This invitation was withdrawn by the organisation. Ask an organisation admin to send you a new one if you believe this is a mistake.</div>
|
|
178
|
+
</div>
|
|
179
|
+
<div class="actions">
|
|
180
|
+
<idp-button variant="outline" shape="rect" block @click=${() => this.emit('idp-invite-continue')}>Go to idp.global</idp-button>
|
|
181
|
+
</div>
|
|
182
|
+
`,
|
|
183
|
+
}),
|
|
184
|
+
invalid: () => ({
|
|
185
|
+
heading: 'Invitation not found',
|
|
186
|
+
description: '',
|
|
187
|
+
body: html`
|
|
188
|
+
<div class="status-block">
|
|
189
|
+
<idp-status-icon tone="error" icon="x"></idp-status-icon>
|
|
190
|
+
<div class="status-text">This invitation link is invalid or was already used. Check that you opened the full link from your email.</div>
|
|
191
|
+
</div>
|
|
192
|
+
<div class="actions">
|
|
193
|
+
<idp-button variant="outline" shape="rect" block @click=${() => this.emit('idp-invite-continue')}>Go to idp.global</idp-button>
|
|
194
|
+
</div>
|
|
195
|
+
`,
|
|
196
|
+
}),
|
|
197
|
+
'email-mismatch': () => ({
|
|
198
|
+
heading: 'Different account signed in',
|
|
199
|
+
description: '',
|
|
200
|
+
body: html`
|
|
201
|
+
<div class="status-block">
|
|
202
|
+
<idp-status-icon tone="warn" icon="user-x"></idp-status-icon>
|
|
203
|
+
<div class="status-text">
|
|
204
|
+
This invitation was sent to <strong>${this.email}</strong>, but you're signed in as
|
|
205
|
+
<strong>${this.signedInEmail}</strong>. Switch accounts to accept it.
|
|
206
|
+
</div>
|
|
207
|
+
</div>
|
|
208
|
+
<div class="actions">
|
|
209
|
+
<idp-button variant="accent" shape="rect" block ?disabled=${this.busy} @click=${() => this.emit('idp-invite-switch-account')}>Switch account</idp-button>
|
|
210
|
+
<idp-button variant="outline" shape="rect" block ?disabled=${this.busy} @click=${() => this.emit('idp-invite-continue')}>Not now</idp-button>
|
|
211
|
+
</div>
|
|
212
|
+
`,
|
|
213
|
+
}),
|
|
214
|
+
};
|
|
215
|
+
const view = renderers[this.state]();
|
|
216
|
+
return html`
|
|
217
|
+
<idp-auth-card heading=${view.heading} description=${view.description}>
|
|
218
|
+
${view.body}
|
|
219
|
+
${this.signedIn && this.signedInEmail && this.state === 'valid'
|
|
220
|
+
? html`<span slot="footer">Signed in as ${this.signedInEmail}</span>`
|
|
221
|
+
: html``}
|
|
222
|
+
</idp-auth-card>
|
|
223
|
+
`;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
private emit(eventName: string) {
|
|
227
|
+
this.dispatchEvent(new CustomEvent(eventName, { bubbles: true, composed: true }));
|
|
228
|
+
}
|
|
229
|
+
}
|
|
@@ -0,0 +1,311 @@
|
|
|
1
|
+
import { DeesElement, html, property, state, customElement, css, type TemplateResult } from '@design.estate/dees-element';
|
|
2
|
+
import { idpElementStyles } from './tokens.js';
|
|
3
|
+
import './idp-badge.js';
|
|
4
|
+
import './idp-button.js';
|
|
5
|
+
import './idp-icon.js';
|
|
6
|
+
import './idp-input.js';
|
|
7
|
+
|
|
8
|
+
export interface IIdpScimTokenItem {
|
|
9
|
+
id: string;
|
|
10
|
+
label: string;
|
|
11
|
+
tokenPrefix: string;
|
|
12
|
+
createdAt: number;
|
|
13
|
+
expiresAt?: number;
|
|
14
|
+
lastUsedAt?: number;
|
|
15
|
+
revoked: boolean;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface IIdpScimTokenCreateEventDetail {
|
|
19
|
+
label: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface IIdpScimTokenRevokeEventDetail {
|
|
23
|
+
tokenId: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
declare global {
|
|
27
|
+
interface HTMLElementTagNameMap {
|
|
28
|
+
'idp-scim-token-panel': IdpScimTokenPanel;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
@customElement('idp-scim-token-panel')
|
|
33
|
+
export class IdpScimTokenPanel extends DeesElement {
|
|
34
|
+
public static demo = () => html`
|
|
35
|
+
<div style="max-width: 720px;">
|
|
36
|
+
<idp-scim-token-panel
|
|
37
|
+
scimBaseUrl="https://idp.global/scim/v2"
|
|
38
|
+
.tokens=${[
|
|
39
|
+
{ id: 't1', label: 'Okta production', tokenPrefix: 'idp_scim', createdAt: Date.now() - 86400000 * 30, lastUsedAt: Date.now() - 3600000, revoked: false },
|
|
40
|
+
{ id: 't2', label: 'Entra staging', tokenPrefix: 'idp_scim', createdAt: Date.now() - 86400000 * 90, expiresAt: Date.now() + 86400000 * 30, revoked: true },
|
|
41
|
+
] as IIdpScimTokenItem[]}
|
|
42
|
+
.createdToken=${{ label: 'Okta production', plaintextToken: 'idp_scim_2f8Qb1x0demoTokenValueOnlyShownOnce' }}
|
|
43
|
+
></idp-scim-token-panel>
|
|
44
|
+
</div>
|
|
45
|
+
`;
|
|
46
|
+
public static demoGroups = ['idp.global v5 admin console'];
|
|
47
|
+
|
|
48
|
+
@property({ type: Array })
|
|
49
|
+
public accessor tokens: IIdpScimTokenItem[] = [];
|
|
50
|
+
|
|
51
|
+
/** One-time reveal payload after a createScimToken round trip */
|
|
52
|
+
@property({ type: Object })
|
|
53
|
+
public accessor createdToken: { label: string; plaintextToken: string } | null = null;
|
|
54
|
+
|
|
55
|
+
@property({ type: String })
|
|
56
|
+
public accessor scimBaseUrl = '';
|
|
57
|
+
|
|
58
|
+
@state()
|
|
59
|
+
private accessor newLabel = '';
|
|
60
|
+
|
|
61
|
+
@state()
|
|
62
|
+
private accessor copied = false;
|
|
63
|
+
|
|
64
|
+
public static styles = [
|
|
65
|
+
...idpElementStyles,
|
|
66
|
+
css`
|
|
67
|
+
:host {
|
|
68
|
+
display: block;
|
|
69
|
+
font-family: var(--idp-font);
|
|
70
|
+
color: var(--idp-fg);
|
|
71
|
+
}
|
|
72
|
+
.section {
|
|
73
|
+
background: var(--idp-card);
|
|
74
|
+
border: 1px solid var(--idp-border);
|
|
75
|
+
border-radius: var(--idp-radius);
|
|
76
|
+
padding: 20px;
|
|
77
|
+
}
|
|
78
|
+
.title {
|
|
79
|
+
font-size: 14px;
|
|
80
|
+
font-weight: 600;
|
|
81
|
+
}
|
|
82
|
+
.muted {
|
|
83
|
+
font-size: 12.5px;
|
|
84
|
+
color: var(--idp-muted-fg);
|
|
85
|
+
line-height: 1.5;
|
|
86
|
+
margin-top: 3px;
|
|
87
|
+
}
|
|
88
|
+
.endpoint {
|
|
89
|
+
display: flex;
|
|
90
|
+
align-items: center;
|
|
91
|
+
gap: 8px;
|
|
92
|
+
margin-top: 12px;
|
|
93
|
+
padding: 8px 10px;
|
|
94
|
+
background: var(--idp-muted);
|
|
95
|
+
border: 1px solid var(--idp-border);
|
|
96
|
+
border-radius: 8px;
|
|
97
|
+
font-family: var(--idp-mono, ui-monospace, monospace);
|
|
98
|
+
font-size: 12px;
|
|
99
|
+
overflow-wrap: anywhere;
|
|
100
|
+
}
|
|
101
|
+
.reveal {
|
|
102
|
+
margin-top: 14px;
|
|
103
|
+
padding: 12px;
|
|
104
|
+
border: 1px solid var(--idp-info-border, var(--idp-border));
|
|
105
|
+
border-radius: 10px;
|
|
106
|
+
background: var(--idp-accent-soft);
|
|
107
|
+
}
|
|
108
|
+
.reveal-title {
|
|
109
|
+
font-size: 12.5px;
|
|
110
|
+
font-weight: 600;
|
|
111
|
+
color: var(--idp-accent);
|
|
112
|
+
display: flex;
|
|
113
|
+
align-items: center;
|
|
114
|
+
gap: 6px;
|
|
115
|
+
}
|
|
116
|
+
.reveal-token {
|
|
117
|
+
margin-top: 8px;
|
|
118
|
+
display: flex;
|
|
119
|
+
align-items: center;
|
|
120
|
+
gap: 8px;
|
|
121
|
+
font-family: var(--idp-mono, ui-monospace, monospace);
|
|
122
|
+
font-size: 12.5px;
|
|
123
|
+
overflow-wrap: anywhere;
|
|
124
|
+
}
|
|
125
|
+
.copy-button {
|
|
126
|
+
flex: 0 0 auto;
|
|
127
|
+
border: 1px solid var(--idp-border);
|
|
128
|
+
background: var(--idp-card);
|
|
129
|
+
border-radius: 7px;
|
|
130
|
+
font-family: inherit;
|
|
131
|
+
font-size: 11px;
|
|
132
|
+
padding: 4px 10px;
|
|
133
|
+
cursor: pointer;
|
|
134
|
+
}
|
|
135
|
+
.copy-button:hover {
|
|
136
|
+
background: var(--idp-muted);
|
|
137
|
+
}
|
|
138
|
+
.row {
|
|
139
|
+
display: flex;
|
|
140
|
+
align-items: center;
|
|
141
|
+
gap: 12px;
|
|
142
|
+
padding: 12px 0;
|
|
143
|
+
border-bottom: 1px solid var(--idp-border);
|
|
144
|
+
}
|
|
145
|
+
.row:last-of-type {
|
|
146
|
+
border-bottom: none;
|
|
147
|
+
}
|
|
148
|
+
.row-main {
|
|
149
|
+
flex: 1;
|
|
150
|
+
min-width: 0;
|
|
151
|
+
}
|
|
152
|
+
.row-name {
|
|
153
|
+
font-size: 13px;
|
|
154
|
+
font-weight: 500;
|
|
155
|
+
display: flex;
|
|
156
|
+
align-items: center;
|
|
157
|
+
gap: 8px;
|
|
158
|
+
}
|
|
159
|
+
.row-meta {
|
|
160
|
+
font-size: 11px;
|
|
161
|
+
color: var(--idp-muted-fg);
|
|
162
|
+
margin-top: 2px;
|
|
163
|
+
}
|
|
164
|
+
.revoke {
|
|
165
|
+
background: none;
|
|
166
|
+
border: none;
|
|
167
|
+
cursor: pointer;
|
|
168
|
+
font-family: inherit;
|
|
169
|
+
font-size: 11px;
|
|
170
|
+
font-weight: 500;
|
|
171
|
+
color: var(--idp-destructive);
|
|
172
|
+
padding: 4px 8px;
|
|
173
|
+
border-radius: 6px;
|
|
174
|
+
}
|
|
175
|
+
.revoke:hover {
|
|
176
|
+
background: var(--idp-error-bg);
|
|
177
|
+
}
|
|
178
|
+
.create-row {
|
|
179
|
+
display: flex;
|
|
180
|
+
gap: 10px;
|
|
181
|
+
align-items: flex-end;
|
|
182
|
+
margin-top: 16px;
|
|
183
|
+
padding-top: 16px;
|
|
184
|
+
border-top: 1px solid var(--idp-border);
|
|
185
|
+
}
|
|
186
|
+
.create-row idp-input {
|
|
187
|
+
flex: 1;
|
|
188
|
+
}
|
|
189
|
+
.empty {
|
|
190
|
+
padding: 18px 0;
|
|
191
|
+
font-size: 12.5px;
|
|
192
|
+
color: var(--idp-muted-fg);
|
|
193
|
+
}
|
|
194
|
+
`,
|
|
195
|
+
];
|
|
196
|
+
|
|
197
|
+
private formatTimeAgo(timestampArg: number): string {
|
|
198
|
+
const deltaMs = Date.now() - timestampArg;
|
|
199
|
+
const days = Math.floor(deltaMs / 86400000);
|
|
200
|
+
if (days > 0) return `${days}d ago`;
|
|
201
|
+
const hours = Math.floor(deltaMs / 3600000);
|
|
202
|
+
if (hours > 0) return `${hours}h ago`;
|
|
203
|
+
return 'just now';
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
private async copyToken() {
|
|
207
|
+
if (!this.createdToken) return;
|
|
208
|
+
try {
|
|
209
|
+
await navigator.clipboard.writeText(this.createdToken.plaintextToken);
|
|
210
|
+
this.copied = true;
|
|
211
|
+
setTimeout(() => (this.copied = false), 2000);
|
|
212
|
+
} catch {
|
|
213
|
+
// clipboard unavailable; the token stays selectable
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
private submitCreate() {
|
|
218
|
+
const label = this.newLabel.trim();
|
|
219
|
+
if (!label) return;
|
|
220
|
+
this.dispatchEvent(
|
|
221
|
+
new CustomEvent<IIdpScimTokenCreateEventDetail>('idp-scim-token-create', {
|
|
222
|
+
detail: { label },
|
|
223
|
+
bubbles: true,
|
|
224
|
+
composed: true,
|
|
225
|
+
}),
|
|
226
|
+
);
|
|
227
|
+
this.newLabel = '';
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
public render(): TemplateResult {
|
|
231
|
+
return html`
|
|
232
|
+
<div class="section">
|
|
233
|
+
<div class="title">SCIM provisioning</div>
|
|
234
|
+
<div class="muted">
|
|
235
|
+
Let your identity provider create, update and deactivate accounts automatically.
|
|
236
|
+
Configure the endpoint below with a bearer token in Okta or Microsoft Entra.
|
|
237
|
+
</div>
|
|
238
|
+
${this.scimBaseUrl
|
|
239
|
+
? html`<div class="endpoint"><idp-icon name="globe" size="14"></idp-icon>${this.scimBaseUrl}</div>`
|
|
240
|
+
: html``}
|
|
241
|
+
${this.createdToken
|
|
242
|
+
? html`
|
|
243
|
+
<div class="reveal">
|
|
244
|
+
<div class="reveal-title">
|
|
245
|
+
<idp-icon name="key" size="14"></idp-icon>
|
|
246
|
+
Token for "${this.createdToken.label}" — copy it now, it won't be shown again
|
|
247
|
+
</div>
|
|
248
|
+
<div class="reveal-token">
|
|
249
|
+
<span>${this.createdToken.plaintextToken}</span>
|
|
250
|
+
<button class="copy-button" @click=${() => this.copyToken()}>
|
|
251
|
+
${this.copied ? 'Copied' : 'Copy'}
|
|
252
|
+
</button>
|
|
253
|
+
</div>
|
|
254
|
+
</div>
|
|
255
|
+
`
|
|
256
|
+
: html``}
|
|
257
|
+
${this.tokens.length
|
|
258
|
+
? this.tokens.map(
|
|
259
|
+
(tokenArg) => html`
|
|
260
|
+
<div class="row">
|
|
261
|
+
<div class="row-main">
|
|
262
|
+
<div class="row-name">
|
|
263
|
+
${tokenArg.label}
|
|
264
|
+
${tokenArg.revoked
|
|
265
|
+
? html`<idp-badge variant="error">revoked</idp-badge>`
|
|
266
|
+
: tokenArg.expiresAt && tokenArg.expiresAt < Date.now()
|
|
267
|
+
? html`<idp-badge variant="warn">expired</idp-badge>`
|
|
268
|
+
: html`<idp-badge variant="ok">active</idp-badge>`}
|
|
269
|
+
</div>
|
|
270
|
+
<div class="row-meta">
|
|
271
|
+
${tokenArg.tokenPrefix}… · created ${this.formatTimeAgo(tokenArg.createdAt)}
|
|
272
|
+
${tokenArg.lastUsedAt ? ` · last used ${this.formatTimeAgo(tokenArg.lastUsedAt)}` : ' · never used'}
|
|
273
|
+
</div>
|
|
274
|
+
</div>
|
|
275
|
+
${!tokenArg.revoked
|
|
276
|
+
? html`
|
|
277
|
+
<button
|
|
278
|
+
class="revoke"
|
|
279
|
+
@click=${() =>
|
|
280
|
+
this.dispatchEvent(
|
|
281
|
+
new CustomEvent<IIdpScimTokenRevokeEventDetail>('idp-scim-token-revoke', {
|
|
282
|
+
detail: { tokenId: tokenArg.id },
|
|
283
|
+
bubbles: true,
|
|
284
|
+
composed: true,
|
|
285
|
+
}),
|
|
286
|
+
)}
|
|
287
|
+
>
|
|
288
|
+
Revoke
|
|
289
|
+
</button>
|
|
290
|
+
`
|
|
291
|
+
: html``}
|
|
292
|
+
</div>
|
|
293
|
+
`,
|
|
294
|
+
)
|
|
295
|
+
: html`<div class="empty">No SCIM tokens yet. Create one to connect your IdP's provisioning.</div>`}
|
|
296
|
+
<div class="create-row">
|
|
297
|
+
<idp-input
|
|
298
|
+
label="New token label"
|
|
299
|
+
placeholder="e.g. Okta production"
|
|
300
|
+
.value=${this.newLabel}
|
|
301
|
+
@input=${(eventArg: Event) => {
|
|
302
|
+
const target = eventArg.composedPath()[0] as HTMLInputElement;
|
|
303
|
+
this.newLabel = target?.value ?? '';
|
|
304
|
+
}}
|
|
305
|
+
></idp-input>
|
|
306
|
+
<idp-button variant="accent" size="sm" @click=${() => this.submitCreate()}>Create token</idp-button>
|
|
307
|
+
</div>
|
|
308
|
+
</div>
|
|
309
|
+
`;
|
|
310
|
+
}
|
|
311
|
+
}
|