@idp.global/catalog 2.1.1 → 3.0.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.
@@ -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
+ }