@syndew/oluso-plugin-saml 0.0.0-stage → 1.30.1

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,3249 @@
1
+ import * as i0 from '@angular/core';
2
+ import { inject, Injectable, signal, Component } from '@angular/core';
3
+ import { ApiService } from '@syndew/oluso-admin-ui';
4
+ import { CommonModule } from '@angular/common';
5
+ import { RouterLink, ActivatedRoute, Router } from '@angular/router';
6
+ import { ButtonComponent, IconComponent, ModalComponent, TAB_CONTEXT } from '@syndew/oluso-ui-core';
7
+ import { forkJoin, Subject, debounceTime, distinctUntilChanged, catchError, of } from 'rxjs';
8
+ import * as i1 from '@angular/forms';
9
+ import { FormsModule } from '@angular/forms';
10
+
11
+ /**
12
+ * Name ID format options
13
+ */
14
+ const NAME_ID_FORMATS = [
15
+ { value: '', label: 'Default (emailAddress)' },
16
+ { value: 'urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress', label: 'Email Address' },
17
+ { value: 'urn:oasis:names:tc:SAML:2.0:nameid-format:persistent', label: 'Persistent' },
18
+ { value: 'urn:oasis:names:tc:SAML:2.0:nameid-format:transient', label: 'Transient' },
19
+ { value: 'urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified', label: 'Unspecified' },
20
+ ];
21
+ /**
22
+ * SSO Binding options
23
+ */
24
+ const SSO_BINDINGS = [
25
+ { value: 'POST', label: 'HTTP-POST' },
26
+ { value: 'Redirect', label: 'HTTP-Redirect' },
27
+ ];
28
+
29
+ /**
30
+ * SAML Service
31
+ *
32
+ * Provides HTTP methods for managing SAML Service Providers and IdP configuration.
33
+ */
34
+ class SamlService {
35
+ api = inject(ApiService);
36
+ basePath = '/saml/service-providers';
37
+ /**
38
+ * Get IdP info
39
+ */
40
+ getIdpInfo() {
41
+ return this.api.get(`${this.basePath}/idp-info`);
42
+ }
43
+ /**
44
+ * Check if an entity ID is available
45
+ */
46
+ checkEntityIdAvailability(entityId, excludeId) {
47
+ const params = {};
48
+ if (excludeId) {
49
+ params['excludeId'] = excludeId;
50
+ }
51
+ return this.api.get(`${this.basePath}/check-entity-id/${encodeURIComponent(entityId)}`, params);
52
+ }
53
+ /**
54
+ * Get all service providers
55
+ */
56
+ getAll(includeDisabled = false) {
57
+ return this.api.get(this.basePath, { includeDisabled });
58
+ }
59
+ /**
60
+ * Get a service provider by ID
61
+ */
62
+ getById(id) {
63
+ return this.api.get(`${this.basePath}/${id}`);
64
+ }
65
+ /**
66
+ * Get a service provider for editing (includes certificates)
67
+ */
68
+ getForEdit(id) {
69
+ return this.api.get(`${this.basePath}/${id}/edit`);
70
+ }
71
+ /**
72
+ * Create a new service provider
73
+ */
74
+ create(data) {
75
+ return this.api.post(this.basePath, data);
76
+ }
77
+ /**
78
+ * Update a service provider
79
+ */
80
+ update(id, data) {
81
+ return this.api.put(`${this.basePath}/${id}`, data);
82
+ }
83
+ /**
84
+ * Delete a service provider
85
+ */
86
+ delete(id) {
87
+ return this.api.delete(`${this.basePath}/${id}`);
88
+ }
89
+ /**
90
+ * Toggle a service provider's enabled status
91
+ */
92
+ toggle(id) {
93
+ return this.api.post(`${this.basePath}/${id}/toggle`, {});
94
+ }
95
+ /**
96
+ * Test a service provider configuration
97
+ */
98
+ test(id) {
99
+ return this.api.post(`${this.basePath}/${id}/test`, {});
100
+ }
101
+ // SAML IdP Configuration
102
+ /**
103
+ * Get IdP configuration
104
+ */
105
+ getIdpConfiguration() {
106
+ return this.api.get(`${this.basePath}/idp-configuration`);
107
+ }
108
+ /**
109
+ * Update IdP configuration
110
+ */
111
+ updateIdpConfiguration(data) {
112
+ return this.api.put(`${this.basePath}/idp-configuration`, data);
113
+ }
114
+ // IdP Certificate Management
115
+ /**
116
+ * Get signing certificate info
117
+ */
118
+ getSigningCertificate() {
119
+ return this.api.get(`${this.basePath}/idp-certificate/signing`);
120
+ }
121
+ /**
122
+ * Get encryption certificate info
123
+ */
124
+ getEncryptionCertificate() {
125
+ return this.api.get(`${this.basePath}/idp-certificate/encryption`);
126
+ }
127
+ /**
128
+ * Generate a new signing certificate
129
+ */
130
+ generateSigningCertificate() {
131
+ return this.api.post(`${this.basePath}/idp-certificate/signing/generate`, {});
132
+ }
133
+ /**
134
+ * Generate a new encryption certificate
135
+ */
136
+ generateEncryptionCertificate() {
137
+ return this.api.post(`${this.basePath}/idp-certificate/encryption/generate`, {});
138
+ }
139
+ /**
140
+ * Upload a signing certificate
141
+ */
142
+ uploadSigningCertificate(data) {
143
+ return this.api.post(`${this.basePath}/idp-certificate/signing/upload`, data);
144
+ }
145
+ /**
146
+ * Upload an encryption certificate
147
+ */
148
+ uploadEncryptionCertificate(data) {
149
+ return this.api.post(`${this.basePath}/idp-certificate/encryption/upload`, data);
150
+ }
151
+ /**
152
+ * Reset signing certificate to global
153
+ */
154
+ resetSigningCertificate() {
155
+ return this.api.post(`${this.basePath}/idp-certificate/signing/reset`, {});
156
+ }
157
+ /**
158
+ * Reset encryption certificate to global
159
+ */
160
+ resetEncryptionCertificate() {
161
+ return this.api.post(`${this.basePath}/idp-certificate/encryption/reset`, {});
162
+ }
163
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.25", ngImport: i0, type: SamlService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
164
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.25", ngImport: i0, type: SamlService, providedIn: 'root' });
165
+ }
166
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.25", ngImport: i0, type: SamlService, decorators: [{
167
+ type: Injectable,
168
+ args: [{ providedIn: 'root' }]
169
+ }] });
170
+
171
+ class SamlDashboardComponent {
172
+ samlService = inject(SamlService);
173
+ loading = signal(true, ...(ngDevMode ? [{ debugName: "loading" }] : /* istanbul ignore next */ []));
174
+ error = signal(null, ...(ngDevMode ? [{ debugName: "error" }] : /* istanbul ignore next */ []));
175
+ idpInfo = signal(null, ...(ngDevMode ? [{ debugName: "idpInfo" }] : /* istanbul ignore next */ []));
176
+ serviceProviders = signal([], ...(ngDevMode ? [{ debugName: "serviceProviders" }] : /* istanbul ignore next */ []));
177
+ copied = signal(null, ...(ngDevMode ? [{ debugName: "copied" }] : /* istanbul ignore next */ []));
178
+ enabledCount = () => this.serviceProviders().filter((sp) => sp.enabled).length;
179
+ disabledCount = () => this.serviceProviders().length - this.enabledCount();
180
+ ngOnInit() {
181
+ this.loadData();
182
+ }
183
+ loadData() {
184
+ this.loading.set(true);
185
+ forkJoin({
186
+ idpInfo: this.samlService.getIdpInfo(),
187
+ serviceProviders: this.samlService.getAll(),
188
+ }).subscribe({
189
+ next: (data) => {
190
+ this.idpInfo.set(data.idpInfo);
191
+ this.serviceProviders.set(data.serviceProviders);
192
+ this.error.set(null);
193
+ this.loading.set(false);
194
+ },
195
+ error: (err) => {
196
+ console.error('Error loading SAML data:', err);
197
+ this.error.set('Failed to load SAML configuration');
198
+ this.loading.set(false);
199
+ },
200
+ });
201
+ }
202
+ copyToClipboard(value, label) {
203
+ navigator.clipboard.writeText(value).then(() => {
204
+ this.copied.set(label);
205
+ setTimeout(() => this.copied.set(null), 2000);
206
+ });
207
+ }
208
+ getFormatName(format) {
209
+ return format.split(':').pop() || format;
210
+ }
211
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.25", ngImport: i0, type: SamlDashboardComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
212
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.25", type: SamlDashboardComponent, isStandalone: true, selector: "saml-dashboard", ngImport: i0, template: `
213
+ <div class="space-y-6">
214
+ <div class="flex items-center justify-between">
215
+ <h1 class="text-2xl font-bold text-gray-900">SAML Identity Provider</h1>
216
+ <a routerLink="/saml/service-providers">
217
+ <ui-button>Manage Service Providers</ui-button>
218
+ </a>
219
+ </div>
220
+
221
+ @if (loading()) {
222
+ <div class="space-y-6">
223
+ <div class="h-8 w-48 bg-gray-200 rounded animate-pulse"></div>
224
+ <div class="grid grid-cols-1 md:grid-cols-2 gap-6">
225
+ @for (i of [1, 2]; track i) {
226
+ <div class="h-48 bg-gray-200 rounded-lg animate-pulse"></div>
227
+ }
228
+ </div>
229
+ </div>
230
+ } @else if (error()) {
231
+ <div class="bg-red-50 border border-red-200 rounded-lg p-4">
232
+ <p class="text-red-700">{{ error() }}</p>
233
+ </div>
234
+ } @else {
235
+ <!-- IdP Status -->
236
+ <div [class]="idpInfo()?.enabled ? 'bg-green-50 border border-green-200' : 'bg-yellow-50 border border-yellow-200'" class="rounded-lg p-4">
237
+ <div class="flex items-center space-x-2">
238
+ @if (idpInfo()?.enabled) {
239
+ <ui-icon name="check-circle" class="h-5 w-5 text-green-600" />
240
+ <span class="text-green-700 font-medium">SAML IdP is enabled</span>
241
+ } @else {
242
+ <ui-icon name="x-circle" class="h-5 w-5 text-yellow-600" />
243
+ <span class="text-yellow-700 font-medium">SAML IdP is disabled</span>
244
+ }
245
+ </div>
246
+ </div>
247
+
248
+ <!-- IdP Configuration Info -->
249
+ @if (idpInfo()) {
250
+ <div class="bg-white shadow rounded-lg">
251
+ <div class="px-6 py-4 border-b border-gray-200">
252
+ <h2 class="text-lg font-medium text-gray-900">IdP Configuration</h2>
253
+ <p class="mt-1 text-sm text-gray-500">Share these URLs with Service Providers to configure SSO</p>
254
+ </div>
255
+ <div class="px-6 py-4 space-y-4">
256
+ <div class="flex items-center justify-between">
257
+ <div class="flex-1 min-w-0">
258
+ <span class="text-sm font-medium text-gray-500">Entity ID (Issuer):</span>
259
+ <div class="flex items-center space-x-2 mt-1">
260
+ <code class="text-sm text-gray-900 bg-gray-100 px-2 py-1 rounded truncate">{{ idpInfo()?.entityId }}</code>
261
+ </div>
262
+ </div>
263
+ <button (click)="copyToClipboard(idpInfo()?.entityId || '', 'entityId')" class="ml-4 p-2 text-gray-400 hover:text-gray-600 hover:bg-gray-100 rounded">
264
+ @if (copied() === 'entityId') {
265
+ <ui-icon name="check-circle" class="h-5 w-5 text-green-500" />
266
+ } @else {
267
+ <ui-icon name="clipboard-document" class="h-5 w-5" />
268
+ }
269
+ </button>
270
+ </div>
271
+
272
+ <div class="flex items-center justify-between">
273
+ <div class="flex-1 min-w-0">
274
+ <span class="text-sm font-medium text-gray-500">SSO URL:</span>
275
+ <div class="flex items-center space-x-2 mt-1">
276
+ <code class="text-sm text-gray-900 bg-gray-100 px-2 py-1 rounded truncate">{{ idpInfo()?.singleSignOnServiceUrl }}</code>
277
+ </div>
278
+ </div>
279
+ <button (click)="copyToClipboard(idpInfo()?.singleSignOnServiceUrl || '', 'ssoUrl')" class="ml-4 p-2 text-gray-400 hover:text-gray-600 hover:bg-gray-100 rounded">
280
+ @if (copied() === 'ssoUrl') {
281
+ <ui-icon name="check-circle" class="h-5 w-5 text-green-500" />
282
+ } @else {
283
+ <ui-icon name="clipboard-document" class="h-5 w-5" />
284
+ }
285
+ </button>
286
+ </div>
287
+
288
+ <div class="flex items-center justify-between">
289
+ <div class="flex-1 min-w-0">
290
+ <span class="text-sm font-medium text-gray-500">Metadata URL:</span>
291
+ <div class="flex items-center space-x-2 mt-1">
292
+ <a [href]="idpInfo()?.metadataUrl" target="_blank" rel="noopener noreferrer" class="text-sm text-primary-600 hover:text-primary-700 truncate">
293
+ {{ idpInfo()?.metadataUrl }}
294
+ </a>
295
+ </div>
296
+ </div>
297
+ <button (click)="copyToClipboard(idpInfo()?.metadataUrl || '', 'metadataUrl')" class="ml-4 p-2 text-gray-400 hover:text-gray-600 hover:bg-gray-100 rounded">
298
+ @if (copied() === 'metadataUrl') {
299
+ <ui-icon name="check-circle" class="h-5 w-5 text-green-500" />
300
+ } @else {
301
+ <ui-icon name="clipboard-document" class="h-5 w-5" />
302
+ }
303
+ </button>
304
+ </div>
305
+
306
+ <div class="pt-2">
307
+ <span class="text-sm font-medium text-gray-500">Supported Name ID Formats:</span>
308
+ <ul class="mt-1 text-sm text-gray-700 list-disc list-inside">
309
+ @for (format of idpInfo()?.supportedNameIdFormats || []; track format) {
310
+ <li>{{ getFormatName(format) }}</li>
311
+ }
312
+ </ul>
313
+ </div>
314
+ </div>
315
+ </div>
316
+ }
317
+
318
+ <!-- Service Provider Stats -->
319
+ <div class="grid grid-cols-1 md:grid-cols-3 gap-6">
320
+ <a routerLink="/saml/service-providers" class="bg-white shadow rounded-lg p-6 hover:shadow-md transition-shadow">
321
+ <div class="flex items-center">
322
+ <div class="flex-shrink-0">
323
+ <ui-icon name="building" class="h-8 w-8 text-gray-400" />
324
+ </div>
325
+ <div class="ml-4">
326
+ <p class="text-sm font-medium text-gray-500">Total Service Providers</p>
327
+ <p class="text-2xl font-semibold text-gray-900">{{ serviceProviders().length }}</p>
328
+ </div>
329
+ </div>
330
+ </a>
331
+
332
+ <div class="bg-white shadow rounded-lg p-6">
333
+ <div class="flex items-center">
334
+ <div class="flex-shrink-0">
335
+ <ui-icon name="check-circle" class="h-8 w-8 text-green-500" />
336
+ </div>
337
+ <div class="ml-4">
338
+ <p class="text-sm font-medium text-gray-500">Enabled</p>
339
+ <p class="text-2xl font-semibold text-gray-900">{{ enabledCount() }}</p>
340
+ </div>
341
+ </div>
342
+ </div>
343
+
344
+ <div class="bg-white shadow rounded-lg p-6">
345
+ <div class="flex items-center">
346
+ <div class="flex-shrink-0">
347
+ <ui-icon name="x-circle" class="h-8 w-8 text-gray-400" />
348
+ </div>
349
+ <div class="ml-4">
350
+ <p class="text-sm font-medium text-gray-500">Disabled</p>
351
+ <p class="text-2xl font-semibold text-gray-900">{{ disabledCount() }}</p>
352
+ </div>
353
+ </div>
354
+ </div>
355
+ </div>
356
+
357
+ <!-- Recent Service Providers -->
358
+ @if (serviceProviders().length > 0) {
359
+ <div class="bg-white shadow rounded-lg">
360
+ <div class="px-6 py-4 border-b border-gray-200">
361
+ <h2 class="text-lg font-medium text-gray-900">Service Providers</h2>
362
+ </div>
363
+ <ul class="divide-y divide-gray-200">
364
+ @for (sp of serviceProviders().slice(0, 5); track sp.id) {
365
+ <li class="px-6 py-4 hover:bg-gray-50">
366
+ <a [routerLink]="['/saml/service-providers', sp.id]" class="flex items-center justify-between">
367
+ <div class="flex items-center space-x-3">
368
+ <ui-icon name="building" class="h-6 w-6 text-gray-400" />
369
+ <div>
370
+ <p class="font-medium text-gray-900">{{ sp.displayName || sp.entityId }}</p>
371
+ <p class="text-sm text-gray-500 truncate max-w-md">{{ sp.entityId }}</p>
372
+ </div>
373
+ </div>
374
+ <span [class]="sp.enabled ? 'bg-green-100 text-green-800' : 'bg-gray-100 text-gray-800'" class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium">
375
+ {{ sp.enabled ? 'Enabled' : 'Disabled' }}
376
+ </span>
377
+ </a>
378
+ </li>
379
+ }
380
+ </ul>
381
+ @if (serviceProviders().length > 5) {
382
+ <div class="px-6 py-3 border-t border-gray-200 bg-gray-50">
383
+ <a routerLink="/saml/service-providers" class="text-sm text-primary-600 hover:text-primary-700">
384
+ View all {{ serviceProviders().length }} service providers
385
+ </a>
386
+ </div>
387
+ }
388
+ </div>
389
+ } @else {
390
+ <div class="text-center py-12 bg-white rounded-lg shadow">
391
+ <ui-icon name="building" class="mx-auto h-12 w-12 text-gray-400" />
392
+ <h3 class="mt-2 text-sm font-medium text-gray-900">No Service Providers</h3>
393
+ <p class="mt-1 text-sm text-gray-500">Get started by adding a SAML Service Provider.</p>
394
+ <div class="mt-6">
395
+ <a routerLink="/saml/service-providers/new">
396
+ <ui-button>Add Service Provider</ui-button>
397
+ </a>
398
+ </div>
399
+ </div>
400
+ }
401
+ }
402
+ </div>
403
+ `, isInline: true, dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: RouterLink, selector: "[routerLink]", inputs: ["target", "queryParams", "fragment", "queryParamsHandling", "state", "info", "relativeTo", "preserveFragment", "skipLocationChange", "replaceUrl", "routerLink"] }, { kind: "component", type: ButtonComponent, selector: "ui-button", inputs: ["variant", "size", "disabled", "loading", "type", "fullWidth"], outputs: ["clicked"] }, { kind: "component", type: IconComponent, selector: "ui-icon", inputs: ["name", "strokeWidth"] }] });
404
+ }
405
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.25", ngImport: i0, type: SamlDashboardComponent, decorators: [{
406
+ type: Component,
407
+ args: [{
408
+ selector: 'saml-dashboard',
409
+ standalone: true,
410
+ imports: [CommonModule, RouterLink, ButtonComponent, IconComponent],
411
+ template: `
412
+ <div class="space-y-6">
413
+ <div class="flex items-center justify-between">
414
+ <h1 class="text-2xl font-bold text-gray-900">SAML Identity Provider</h1>
415
+ <a routerLink="/saml/service-providers">
416
+ <ui-button>Manage Service Providers</ui-button>
417
+ </a>
418
+ </div>
419
+
420
+ @if (loading()) {
421
+ <div class="space-y-6">
422
+ <div class="h-8 w-48 bg-gray-200 rounded animate-pulse"></div>
423
+ <div class="grid grid-cols-1 md:grid-cols-2 gap-6">
424
+ @for (i of [1, 2]; track i) {
425
+ <div class="h-48 bg-gray-200 rounded-lg animate-pulse"></div>
426
+ }
427
+ </div>
428
+ </div>
429
+ } @else if (error()) {
430
+ <div class="bg-red-50 border border-red-200 rounded-lg p-4">
431
+ <p class="text-red-700">{{ error() }}</p>
432
+ </div>
433
+ } @else {
434
+ <!-- IdP Status -->
435
+ <div [class]="idpInfo()?.enabled ? 'bg-green-50 border border-green-200' : 'bg-yellow-50 border border-yellow-200'" class="rounded-lg p-4">
436
+ <div class="flex items-center space-x-2">
437
+ @if (idpInfo()?.enabled) {
438
+ <ui-icon name="check-circle" class="h-5 w-5 text-green-600" />
439
+ <span class="text-green-700 font-medium">SAML IdP is enabled</span>
440
+ } @else {
441
+ <ui-icon name="x-circle" class="h-5 w-5 text-yellow-600" />
442
+ <span class="text-yellow-700 font-medium">SAML IdP is disabled</span>
443
+ }
444
+ </div>
445
+ </div>
446
+
447
+ <!-- IdP Configuration Info -->
448
+ @if (idpInfo()) {
449
+ <div class="bg-white shadow rounded-lg">
450
+ <div class="px-6 py-4 border-b border-gray-200">
451
+ <h2 class="text-lg font-medium text-gray-900">IdP Configuration</h2>
452
+ <p class="mt-1 text-sm text-gray-500">Share these URLs with Service Providers to configure SSO</p>
453
+ </div>
454
+ <div class="px-6 py-4 space-y-4">
455
+ <div class="flex items-center justify-between">
456
+ <div class="flex-1 min-w-0">
457
+ <span class="text-sm font-medium text-gray-500">Entity ID (Issuer):</span>
458
+ <div class="flex items-center space-x-2 mt-1">
459
+ <code class="text-sm text-gray-900 bg-gray-100 px-2 py-1 rounded truncate">{{ idpInfo()?.entityId }}</code>
460
+ </div>
461
+ </div>
462
+ <button (click)="copyToClipboard(idpInfo()?.entityId || '', 'entityId')" class="ml-4 p-2 text-gray-400 hover:text-gray-600 hover:bg-gray-100 rounded">
463
+ @if (copied() === 'entityId') {
464
+ <ui-icon name="check-circle" class="h-5 w-5 text-green-500" />
465
+ } @else {
466
+ <ui-icon name="clipboard-document" class="h-5 w-5" />
467
+ }
468
+ </button>
469
+ </div>
470
+
471
+ <div class="flex items-center justify-between">
472
+ <div class="flex-1 min-w-0">
473
+ <span class="text-sm font-medium text-gray-500">SSO URL:</span>
474
+ <div class="flex items-center space-x-2 mt-1">
475
+ <code class="text-sm text-gray-900 bg-gray-100 px-2 py-1 rounded truncate">{{ idpInfo()?.singleSignOnServiceUrl }}</code>
476
+ </div>
477
+ </div>
478
+ <button (click)="copyToClipboard(idpInfo()?.singleSignOnServiceUrl || '', 'ssoUrl')" class="ml-4 p-2 text-gray-400 hover:text-gray-600 hover:bg-gray-100 rounded">
479
+ @if (copied() === 'ssoUrl') {
480
+ <ui-icon name="check-circle" class="h-5 w-5 text-green-500" />
481
+ } @else {
482
+ <ui-icon name="clipboard-document" class="h-5 w-5" />
483
+ }
484
+ </button>
485
+ </div>
486
+
487
+ <div class="flex items-center justify-between">
488
+ <div class="flex-1 min-w-0">
489
+ <span class="text-sm font-medium text-gray-500">Metadata URL:</span>
490
+ <div class="flex items-center space-x-2 mt-1">
491
+ <a [href]="idpInfo()?.metadataUrl" target="_blank" rel="noopener noreferrer" class="text-sm text-primary-600 hover:text-primary-700 truncate">
492
+ {{ idpInfo()?.metadataUrl }}
493
+ </a>
494
+ </div>
495
+ </div>
496
+ <button (click)="copyToClipboard(idpInfo()?.metadataUrl || '', 'metadataUrl')" class="ml-4 p-2 text-gray-400 hover:text-gray-600 hover:bg-gray-100 rounded">
497
+ @if (copied() === 'metadataUrl') {
498
+ <ui-icon name="check-circle" class="h-5 w-5 text-green-500" />
499
+ } @else {
500
+ <ui-icon name="clipboard-document" class="h-5 w-5" />
501
+ }
502
+ </button>
503
+ </div>
504
+
505
+ <div class="pt-2">
506
+ <span class="text-sm font-medium text-gray-500">Supported Name ID Formats:</span>
507
+ <ul class="mt-1 text-sm text-gray-700 list-disc list-inside">
508
+ @for (format of idpInfo()?.supportedNameIdFormats || []; track format) {
509
+ <li>{{ getFormatName(format) }}</li>
510
+ }
511
+ </ul>
512
+ </div>
513
+ </div>
514
+ </div>
515
+ }
516
+
517
+ <!-- Service Provider Stats -->
518
+ <div class="grid grid-cols-1 md:grid-cols-3 gap-6">
519
+ <a routerLink="/saml/service-providers" class="bg-white shadow rounded-lg p-6 hover:shadow-md transition-shadow">
520
+ <div class="flex items-center">
521
+ <div class="flex-shrink-0">
522
+ <ui-icon name="building" class="h-8 w-8 text-gray-400" />
523
+ </div>
524
+ <div class="ml-4">
525
+ <p class="text-sm font-medium text-gray-500">Total Service Providers</p>
526
+ <p class="text-2xl font-semibold text-gray-900">{{ serviceProviders().length }}</p>
527
+ </div>
528
+ </div>
529
+ </a>
530
+
531
+ <div class="bg-white shadow rounded-lg p-6">
532
+ <div class="flex items-center">
533
+ <div class="flex-shrink-0">
534
+ <ui-icon name="check-circle" class="h-8 w-8 text-green-500" />
535
+ </div>
536
+ <div class="ml-4">
537
+ <p class="text-sm font-medium text-gray-500">Enabled</p>
538
+ <p class="text-2xl font-semibold text-gray-900">{{ enabledCount() }}</p>
539
+ </div>
540
+ </div>
541
+ </div>
542
+
543
+ <div class="bg-white shadow rounded-lg p-6">
544
+ <div class="flex items-center">
545
+ <div class="flex-shrink-0">
546
+ <ui-icon name="x-circle" class="h-8 w-8 text-gray-400" />
547
+ </div>
548
+ <div class="ml-4">
549
+ <p class="text-sm font-medium text-gray-500">Disabled</p>
550
+ <p class="text-2xl font-semibold text-gray-900">{{ disabledCount() }}</p>
551
+ </div>
552
+ </div>
553
+ </div>
554
+ </div>
555
+
556
+ <!-- Recent Service Providers -->
557
+ @if (serviceProviders().length > 0) {
558
+ <div class="bg-white shadow rounded-lg">
559
+ <div class="px-6 py-4 border-b border-gray-200">
560
+ <h2 class="text-lg font-medium text-gray-900">Service Providers</h2>
561
+ </div>
562
+ <ul class="divide-y divide-gray-200">
563
+ @for (sp of serviceProviders().slice(0, 5); track sp.id) {
564
+ <li class="px-6 py-4 hover:bg-gray-50">
565
+ <a [routerLink]="['/saml/service-providers', sp.id]" class="flex items-center justify-between">
566
+ <div class="flex items-center space-x-3">
567
+ <ui-icon name="building" class="h-6 w-6 text-gray-400" />
568
+ <div>
569
+ <p class="font-medium text-gray-900">{{ sp.displayName || sp.entityId }}</p>
570
+ <p class="text-sm text-gray-500 truncate max-w-md">{{ sp.entityId }}</p>
571
+ </div>
572
+ </div>
573
+ <span [class]="sp.enabled ? 'bg-green-100 text-green-800' : 'bg-gray-100 text-gray-800'" class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium">
574
+ {{ sp.enabled ? 'Enabled' : 'Disabled' }}
575
+ </span>
576
+ </a>
577
+ </li>
578
+ }
579
+ </ul>
580
+ @if (serviceProviders().length > 5) {
581
+ <div class="px-6 py-3 border-t border-gray-200 bg-gray-50">
582
+ <a routerLink="/saml/service-providers" class="text-sm text-primary-600 hover:text-primary-700">
583
+ View all {{ serviceProviders().length }} service providers
584
+ </a>
585
+ </div>
586
+ }
587
+ </div>
588
+ } @else {
589
+ <div class="text-center py-12 bg-white rounded-lg shadow">
590
+ <ui-icon name="building" class="mx-auto h-12 w-12 text-gray-400" />
591
+ <h3 class="mt-2 text-sm font-medium text-gray-900">No Service Providers</h3>
592
+ <p class="mt-1 text-sm text-gray-500">Get started by adding a SAML Service Provider.</p>
593
+ <div class="mt-6">
594
+ <a routerLink="/saml/service-providers/new">
595
+ <ui-button>Add Service Provider</ui-button>
596
+ </a>
597
+ </div>
598
+ </div>
599
+ }
600
+ }
601
+ </div>
602
+ `,
603
+ }]
604
+ }] });
605
+
606
+ class ServiceProvidersPageComponent {
607
+ samlService = inject(SamlService);
608
+ loading = signal(true, ...(ngDevMode ? [{ debugName: "loading" }] : /* istanbul ignore next */ []));
609
+ error = signal(null, ...(ngDevMode ? [{ debugName: "error" }] : /* istanbul ignore next */ []));
610
+ serviceProviders = signal([], ...(ngDevMode ? [{ debugName: "serviceProviders" }] : /* istanbul ignore next */ []));
611
+ actionLoading = signal(null, ...(ngDevMode ? [{ debugName: "actionLoading" }] : /* istanbul ignore next */ []));
612
+ testResult = signal(null, ...(ngDevMode ? [{ debugName: "testResult" }] : /* istanbul ignore next */ []));
613
+ showDeleteModal = signal(false, ...(ngDevMode ? [{ debugName: "showDeleteModal" }] : /* istanbul ignore next */ []));
614
+ deleteTarget = signal(null, ...(ngDevMode ? [{ debugName: "deleteTarget" }] : /* istanbul ignore next */ []));
615
+ search = '';
616
+ showDisabled = true;
617
+ filteredProviders = () => {
618
+ if (!this.search)
619
+ return this.serviceProviders();
620
+ const searchLower = this.search.toLowerCase();
621
+ return this.serviceProviders().filter((sp) => sp.entityId.toLowerCase().includes(searchLower) ||
622
+ sp.displayName?.toLowerCase().includes(searchLower) ||
623
+ sp.description?.toLowerCase().includes(searchLower));
624
+ };
625
+ ngOnInit() {
626
+ this.loadServiceProviders();
627
+ }
628
+ loadServiceProviders() {
629
+ this.loading.set(true);
630
+ this.samlService.getAll(this.showDisabled).subscribe({
631
+ next: (data) => {
632
+ this.serviceProviders.set(data);
633
+ this.error.set(null);
634
+ this.loading.set(false);
635
+ },
636
+ error: (err) => {
637
+ console.error('Error loading service providers:', err);
638
+ this.error.set('Failed to load Service Providers');
639
+ this.loading.set(false);
640
+ },
641
+ });
642
+ }
643
+ onSearchChange() {
644
+ // Filtering is handled reactively
645
+ }
646
+ handleToggle(sp) {
647
+ this.actionLoading.set(sp.id);
648
+ this.samlService.toggle(sp.id).subscribe({
649
+ next: () => {
650
+ this.loadServiceProviders();
651
+ this.actionLoading.set(null);
652
+ },
653
+ error: (err) => {
654
+ console.error('Error toggling service provider:', err);
655
+ this.actionLoading.set(null);
656
+ },
657
+ });
658
+ }
659
+ confirmDelete(sp) {
660
+ this.deleteTarget.set(sp);
661
+ this.showDeleteModal.set(true);
662
+ }
663
+ handleDelete() {
664
+ const sp = this.deleteTarget();
665
+ if (!sp)
666
+ return;
667
+ this.actionLoading.set(sp.id);
668
+ this.samlService.delete(sp.id).subscribe({
669
+ next: () => {
670
+ this.showDeleteModal.set(false);
671
+ this.deleteTarget.set(null);
672
+ this.loadServiceProviders();
673
+ this.actionLoading.set(null);
674
+ },
675
+ error: (err) => {
676
+ console.error('Error deleting service provider:', err);
677
+ this.actionLoading.set(null);
678
+ },
679
+ });
680
+ }
681
+ handleTest(sp) {
682
+ this.actionLoading.set(sp.id);
683
+ this.testResult.set(null);
684
+ this.samlService.test(sp.id).subscribe({
685
+ next: (result) => {
686
+ this.testResult.set(result);
687
+ this.actionLoading.set(null);
688
+ },
689
+ error: () => {
690
+ this.testResult.set({ success: false, message: 'Failed to run test' });
691
+ this.actionLoading.set(null);
692
+ },
693
+ });
694
+ }
695
+ formatDate(dateStr) {
696
+ return new Date(dateStr).toLocaleDateString();
697
+ }
698
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.25", ngImport: i0, type: ServiceProvidersPageComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
699
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.25", type: ServiceProvidersPageComponent, isStandalone: true, selector: "saml-service-providers-page", ngImport: i0, template: `
700
+ <div class="space-y-6">
701
+ <div class="flex items-center justify-between">
702
+ <div>
703
+ <h1 class="text-2xl font-bold text-gray-900">SAML Service Providers</h1>
704
+ <p class="mt-1 text-sm text-gray-500">Applications that use this system as their SAML Identity Provider</p>
705
+ </div>
706
+ <a routerLink="/saml/service-providers/new">
707
+ <ui-button>
708
+ <ui-icon name="plus" class="-ml-1 mr-2 h-5 w-5" />
709
+ Add Service Provider
710
+ </ui-button>
711
+ </a>
712
+ </div>
713
+
714
+ <!-- Filters -->
715
+ <div class="flex items-center space-x-4">
716
+ <div class="flex-1 max-w-md">
717
+ <div class="relative">
718
+ <ui-icon name="search" class="absolute left-3 top-1/2 transform -translate-y-1/2 h-5 w-5 text-gray-400" />
719
+ <input
720
+ type="text"
721
+ [(ngModel)]="search"
722
+ (ngModelChange)="onSearchChange()"
723
+ placeholder="Search by Entity ID, name, or description..."
724
+ class="w-full pl-10 pr-4 py-2 border border-gray-300 rounded-md focus:ring-primary-500 focus:border-primary-500"
725
+ />
726
+ </div>
727
+ </div>
728
+ <label class="flex items-center space-x-2 text-sm text-gray-700">
729
+ <input
730
+ type="checkbox"
731
+ [(ngModel)]="showDisabled"
732
+ (ngModelChange)="loadServiceProviders()"
733
+ class="rounded border-gray-300 text-primary-600 focus:ring-primary-500"
734
+ />
735
+ <span>Show disabled</span>
736
+ </label>
737
+ </div>
738
+
739
+ @if (error()) {
740
+ <div class="bg-red-50 border border-red-200 rounded-lg p-4">
741
+ <p class="text-red-700">{{ error() }}</p>
742
+ </div>
743
+ }
744
+
745
+ <!-- Test Result Toast -->
746
+ @if (testResult()) {
747
+ <div [class]="testResult()?.success ? 'bg-green-50 border-green-200' : 'bg-red-50 border-red-200'" class="rounded-lg p-4 border">
748
+ <div class="flex items-center justify-between">
749
+ <div class="flex items-center space-x-2">
750
+ @if (testResult()?.success) {
751
+ <ui-icon name="check-circle" class="h-5 w-5 text-green-600" />
752
+ } @else {
753
+ <ui-icon name="x-circle" class="h-5 w-5 text-red-600" />
754
+ }
755
+ <span [class]="testResult()?.success ? 'text-green-700' : 'text-red-700'">{{ testResult()?.message }}</span>
756
+ </div>
757
+ <button (click)="testResult.set(null)" class="text-gray-400 hover:text-gray-600">&times;</button>
758
+ </div>
759
+ </div>
760
+ }
761
+
762
+ @if (loading()) {
763
+ <div class="space-y-4">
764
+ @for (i of [1, 2, 3]; track i) {
765
+ <div class="bg-gray-200 h-24 rounded-lg animate-pulse"></div>
766
+ }
767
+ </div>
768
+ } @else if (filteredProviders().length === 0) {
769
+ <div class="text-center py-12 bg-white rounded-lg shadow">
770
+ <ui-icon name="building" class="mx-auto h-12 w-12 text-gray-400" />
771
+ <h3 class="mt-2 text-sm font-medium text-gray-900">No Service Providers</h3>
772
+ <p class="mt-1 text-sm text-gray-500">
773
+ {{ search ? 'No service providers match your search.' : 'Get started by adding a SAML Service Provider.' }}
774
+ </p>
775
+ @if (!search) {
776
+ <div class="mt-6">
777
+ <a routerLink="/saml/service-providers/new">
778
+ <ui-button>Add Service Provider</ui-button>
779
+ </a>
780
+ </div>
781
+ }
782
+ </div>
783
+ } @else {
784
+ <div class="bg-white shadow overflow-hidden rounded-lg">
785
+ <table class="min-w-full divide-y divide-gray-200">
786
+ <thead class="bg-gray-50">
787
+ <tr>
788
+ <th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Service Provider</th>
789
+ <th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Status</th>
790
+ <th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Configuration</th>
791
+ <th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Last Accessed</th>
792
+ <th class="px-6 py-3 text-right text-xs font-medium text-gray-500 uppercase tracking-wider">Actions</th>
793
+ </tr>
794
+ </thead>
795
+ <tbody class="bg-white divide-y divide-gray-200">
796
+ @for (sp of filteredProviders(); track sp.id) {
797
+ <tr class="hover:bg-gray-50">
798
+ <td class="px-6 py-4">
799
+ <div class="flex items-center">
800
+ <ui-icon name="building" class="h-8 w-8 text-gray-400 flex-shrink-0" />
801
+ <div class="ml-4">
802
+ <div class="text-sm font-medium text-gray-900">{{ sp.displayName || sp.entityId }}</div>
803
+ <div class="text-sm text-gray-500 max-w-xs truncate" [title]="sp.entityId">{{ sp.entityId }}</div>
804
+ @if (sp.description) {
805
+ <div class="text-xs text-gray-400 max-w-xs truncate">{{ sp.description }}</div>
806
+ }
807
+ </div>
808
+ </div>
809
+ </td>
810
+ <td class="px-6 py-4 whitespace-nowrap">
811
+ <span [class]="sp.enabled ? 'bg-green-100 text-green-800' : 'bg-gray-100 text-gray-800'" class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium">
812
+ {{ sp.enabled ? 'Enabled' : 'Disabled' }}
813
+ </span>
814
+ </td>
815
+ <td class="px-6 py-4">
816
+ <div class="flex items-center space-x-2 text-xs text-gray-500">
817
+ @if (sp.metadataUrl) {
818
+ <span class="bg-blue-100 text-blue-800 px-2 py-0.5 rounded">Metadata</span>
819
+ } @else {
820
+ <span class="bg-gray-100 text-gray-600 px-2 py-0.5 rounded">Manual</span>
821
+ }
822
+ @if (sp.encryptAssertions) {
823
+ <span class="bg-purple-100 text-purple-800 px-2 py-0.5 rounded">Encrypted</span>
824
+ }
825
+ @if (sp.requireSignedAuthnRequests) {
826
+ <span class="bg-yellow-100 text-yellow-800 px-2 py-0.5 rounded">Signed Requests</span>
827
+ }
828
+ </div>
829
+ </td>
830
+ <td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
831
+ {{ sp.lastAccessed ? formatDate(sp.lastAccessed) : 'Never' }}
832
+ </td>
833
+ <td class="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
834
+ <div class="flex items-center justify-end space-x-2">
835
+ <button (click)="handleTest(sp)" [disabled]="actionLoading() === sp.id" class="p-1.5 text-gray-400 hover:text-blue-600 hover:bg-blue-50 rounded" title="Test configuration">
836
+ <svg class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
837
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19.428 15.428a2 2 0 00-1.022-.547l-2.387-.477a6 6 0 00-3.86.517l-.318.158a6 6 0 01-3.86.517L6.05 15.21a2 2 0 00-1.806.547M8 4h8l-1 1v5.172a2 2 0 00.586 1.414l5 5c1.26 1.26.367 3.414-1.415 3.414H4.828c-1.782 0-2.674-2.154-1.414-3.414l5-5A2 2 0 009 10.172V5L8 4z" />
838
+ </svg>
839
+ </button>
840
+ <button (click)="handleToggle(sp)" [disabled]="actionLoading() === sp.id || sp.nonEditable" [class]="sp.enabled ? 'text-gray-400 hover:text-yellow-600 hover:bg-yellow-50' : 'text-gray-400 hover:text-green-600 hover:bg-green-50'" class="p-1.5 rounded" [class.opacity-50]="sp.nonEditable" [class.cursor-not-allowed]="sp.nonEditable" [title]="sp.enabled ? 'Disable' : 'Enable'">
841
+ @if (sp.enabled) {
842
+ <ui-icon name="pause-circle" class="h-4 w-4" />
843
+ } @else {
844
+ <ui-icon name="play-circle" class="h-4 w-4" />
845
+ }
846
+ </button>
847
+ <a [routerLink]="['/saml/service-providers', sp.id]" class="p-1.5 text-gray-400 hover:text-primary-600 hover:bg-primary-50 rounded" title="Edit">
848
+ <ui-icon name="pencil" class="h-4 w-4" />
849
+ </a>
850
+ <button (click)="confirmDelete(sp)" [disabled]="actionLoading() === sp.id || sp.nonEditable" class="p-1.5 text-gray-400 hover:text-red-600 hover:bg-red-50 rounded" [class.opacity-50]="sp.nonEditable" [class.cursor-not-allowed]="sp.nonEditable" title="Delete">
851
+ <ui-icon name="trash" class="h-4 w-4" />
852
+ </button>
853
+ </div>
854
+ </td>
855
+ </tr>
856
+ }
857
+ </tbody>
858
+ </table>
859
+ </div>
860
+ }
861
+
862
+ <!-- Delete Confirmation Modal -->
863
+ <ui-modal [open]="showDeleteModal()" title="Delete Service Provider" (closed)="showDeleteModal.set(false)">
864
+ <p class="text-sm text-gray-500">
865
+ Are you sure you want to delete "{{ deleteTarget()?.displayName || deleteTarget()?.entityId }}"?
866
+ This action cannot be undone.
867
+ </p>
868
+ <div class="mt-4 flex justify-end gap-3">
869
+ <ui-button variant="secondary" (clicked)="showDeleteModal.set(false)">Cancel</ui-button>
870
+ <ui-button variant="danger" [disabled]="!!actionLoading()" (clicked)="handleDelete()">
871
+ {{ actionLoading() ? 'Deleting...' : 'Delete' }}
872
+ </ui-button>
873
+ </div>
874
+ </ui-modal>
875
+ </div>
876
+ `, isInline: true, dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i1.CheckboxControlValueAccessor, selector: "input[type=checkbox][formControlName],input[type=checkbox][formControl],input[type=checkbox][ngModel]" }, { kind: "directive", type: i1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "directive", type: RouterLink, selector: "[routerLink]", inputs: ["target", "queryParams", "fragment", "queryParamsHandling", "state", "info", "relativeTo", "preserveFragment", "skipLocationChange", "replaceUrl", "routerLink"] }, { kind: "component", type: ButtonComponent, selector: "ui-button", inputs: ["variant", "size", "disabled", "loading", "type", "fullWidth"], outputs: ["clicked"] }, { kind: "component", type: IconComponent, selector: "ui-icon", inputs: ["name", "strokeWidth"] }, { kind: "component", type: ModalComponent, selector: "ui-modal", inputs: ["open", "title", "size", "showCloseButton", "closeOnBackdrop", "closeOnEscape", "scrollable"], outputs: ["closed"] }] });
877
+ }
878
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.25", ngImport: i0, type: ServiceProvidersPageComponent, decorators: [{
879
+ type: Component,
880
+ args: [{
881
+ selector: 'saml-service-providers-page',
882
+ standalone: true,
883
+ imports: [CommonModule, FormsModule, RouterLink, ButtonComponent, IconComponent, ModalComponent],
884
+ template: `
885
+ <div class="space-y-6">
886
+ <div class="flex items-center justify-between">
887
+ <div>
888
+ <h1 class="text-2xl font-bold text-gray-900">SAML Service Providers</h1>
889
+ <p class="mt-1 text-sm text-gray-500">Applications that use this system as their SAML Identity Provider</p>
890
+ </div>
891
+ <a routerLink="/saml/service-providers/new">
892
+ <ui-button>
893
+ <ui-icon name="plus" class="-ml-1 mr-2 h-5 w-5" />
894
+ Add Service Provider
895
+ </ui-button>
896
+ </a>
897
+ </div>
898
+
899
+ <!-- Filters -->
900
+ <div class="flex items-center space-x-4">
901
+ <div class="flex-1 max-w-md">
902
+ <div class="relative">
903
+ <ui-icon name="search" class="absolute left-3 top-1/2 transform -translate-y-1/2 h-5 w-5 text-gray-400" />
904
+ <input
905
+ type="text"
906
+ [(ngModel)]="search"
907
+ (ngModelChange)="onSearchChange()"
908
+ placeholder="Search by Entity ID, name, or description..."
909
+ class="w-full pl-10 pr-4 py-2 border border-gray-300 rounded-md focus:ring-primary-500 focus:border-primary-500"
910
+ />
911
+ </div>
912
+ </div>
913
+ <label class="flex items-center space-x-2 text-sm text-gray-700">
914
+ <input
915
+ type="checkbox"
916
+ [(ngModel)]="showDisabled"
917
+ (ngModelChange)="loadServiceProviders()"
918
+ class="rounded border-gray-300 text-primary-600 focus:ring-primary-500"
919
+ />
920
+ <span>Show disabled</span>
921
+ </label>
922
+ </div>
923
+
924
+ @if (error()) {
925
+ <div class="bg-red-50 border border-red-200 rounded-lg p-4">
926
+ <p class="text-red-700">{{ error() }}</p>
927
+ </div>
928
+ }
929
+
930
+ <!-- Test Result Toast -->
931
+ @if (testResult()) {
932
+ <div [class]="testResult()?.success ? 'bg-green-50 border-green-200' : 'bg-red-50 border-red-200'" class="rounded-lg p-4 border">
933
+ <div class="flex items-center justify-between">
934
+ <div class="flex items-center space-x-2">
935
+ @if (testResult()?.success) {
936
+ <ui-icon name="check-circle" class="h-5 w-5 text-green-600" />
937
+ } @else {
938
+ <ui-icon name="x-circle" class="h-5 w-5 text-red-600" />
939
+ }
940
+ <span [class]="testResult()?.success ? 'text-green-700' : 'text-red-700'">{{ testResult()?.message }}</span>
941
+ </div>
942
+ <button (click)="testResult.set(null)" class="text-gray-400 hover:text-gray-600">&times;</button>
943
+ </div>
944
+ </div>
945
+ }
946
+
947
+ @if (loading()) {
948
+ <div class="space-y-4">
949
+ @for (i of [1, 2, 3]; track i) {
950
+ <div class="bg-gray-200 h-24 rounded-lg animate-pulse"></div>
951
+ }
952
+ </div>
953
+ } @else if (filteredProviders().length === 0) {
954
+ <div class="text-center py-12 bg-white rounded-lg shadow">
955
+ <ui-icon name="building" class="mx-auto h-12 w-12 text-gray-400" />
956
+ <h3 class="mt-2 text-sm font-medium text-gray-900">No Service Providers</h3>
957
+ <p class="mt-1 text-sm text-gray-500">
958
+ {{ search ? 'No service providers match your search.' : 'Get started by adding a SAML Service Provider.' }}
959
+ </p>
960
+ @if (!search) {
961
+ <div class="mt-6">
962
+ <a routerLink="/saml/service-providers/new">
963
+ <ui-button>Add Service Provider</ui-button>
964
+ </a>
965
+ </div>
966
+ }
967
+ </div>
968
+ } @else {
969
+ <div class="bg-white shadow overflow-hidden rounded-lg">
970
+ <table class="min-w-full divide-y divide-gray-200">
971
+ <thead class="bg-gray-50">
972
+ <tr>
973
+ <th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Service Provider</th>
974
+ <th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Status</th>
975
+ <th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Configuration</th>
976
+ <th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Last Accessed</th>
977
+ <th class="px-6 py-3 text-right text-xs font-medium text-gray-500 uppercase tracking-wider">Actions</th>
978
+ </tr>
979
+ </thead>
980
+ <tbody class="bg-white divide-y divide-gray-200">
981
+ @for (sp of filteredProviders(); track sp.id) {
982
+ <tr class="hover:bg-gray-50">
983
+ <td class="px-6 py-4">
984
+ <div class="flex items-center">
985
+ <ui-icon name="building" class="h-8 w-8 text-gray-400 flex-shrink-0" />
986
+ <div class="ml-4">
987
+ <div class="text-sm font-medium text-gray-900">{{ sp.displayName || sp.entityId }}</div>
988
+ <div class="text-sm text-gray-500 max-w-xs truncate" [title]="sp.entityId">{{ sp.entityId }}</div>
989
+ @if (sp.description) {
990
+ <div class="text-xs text-gray-400 max-w-xs truncate">{{ sp.description }}</div>
991
+ }
992
+ </div>
993
+ </div>
994
+ </td>
995
+ <td class="px-6 py-4 whitespace-nowrap">
996
+ <span [class]="sp.enabled ? 'bg-green-100 text-green-800' : 'bg-gray-100 text-gray-800'" class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium">
997
+ {{ sp.enabled ? 'Enabled' : 'Disabled' }}
998
+ </span>
999
+ </td>
1000
+ <td class="px-6 py-4">
1001
+ <div class="flex items-center space-x-2 text-xs text-gray-500">
1002
+ @if (sp.metadataUrl) {
1003
+ <span class="bg-blue-100 text-blue-800 px-2 py-0.5 rounded">Metadata</span>
1004
+ } @else {
1005
+ <span class="bg-gray-100 text-gray-600 px-2 py-0.5 rounded">Manual</span>
1006
+ }
1007
+ @if (sp.encryptAssertions) {
1008
+ <span class="bg-purple-100 text-purple-800 px-2 py-0.5 rounded">Encrypted</span>
1009
+ }
1010
+ @if (sp.requireSignedAuthnRequests) {
1011
+ <span class="bg-yellow-100 text-yellow-800 px-2 py-0.5 rounded">Signed Requests</span>
1012
+ }
1013
+ </div>
1014
+ </td>
1015
+ <td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
1016
+ {{ sp.lastAccessed ? formatDate(sp.lastAccessed) : 'Never' }}
1017
+ </td>
1018
+ <td class="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
1019
+ <div class="flex items-center justify-end space-x-2">
1020
+ <button (click)="handleTest(sp)" [disabled]="actionLoading() === sp.id" class="p-1.5 text-gray-400 hover:text-blue-600 hover:bg-blue-50 rounded" title="Test configuration">
1021
+ <svg class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
1022
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19.428 15.428a2 2 0 00-1.022-.547l-2.387-.477a6 6 0 00-3.86.517l-.318.158a6 6 0 01-3.86.517L6.05 15.21a2 2 0 00-1.806.547M8 4h8l-1 1v5.172a2 2 0 00.586 1.414l5 5c1.26 1.26.367 3.414-1.415 3.414H4.828c-1.782 0-2.674-2.154-1.414-3.414l5-5A2 2 0 009 10.172V5L8 4z" />
1023
+ </svg>
1024
+ </button>
1025
+ <button (click)="handleToggle(sp)" [disabled]="actionLoading() === sp.id || sp.nonEditable" [class]="sp.enabled ? 'text-gray-400 hover:text-yellow-600 hover:bg-yellow-50' : 'text-gray-400 hover:text-green-600 hover:bg-green-50'" class="p-1.5 rounded" [class.opacity-50]="sp.nonEditable" [class.cursor-not-allowed]="sp.nonEditable" [title]="sp.enabled ? 'Disable' : 'Enable'">
1026
+ @if (sp.enabled) {
1027
+ <ui-icon name="pause-circle" class="h-4 w-4" />
1028
+ } @else {
1029
+ <ui-icon name="play-circle" class="h-4 w-4" />
1030
+ }
1031
+ </button>
1032
+ <a [routerLink]="['/saml/service-providers', sp.id]" class="p-1.5 text-gray-400 hover:text-primary-600 hover:bg-primary-50 rounded" title="Edit">
1033
+ <ui-icon name="pencil" class="h-4 w-4" />
1034
+ </a>
1035
+ <button (click)="confirmDelete(sp)" [disabled]="actionLoading() === sp.id || sp.nonEditable" class="p-1.5 text-gray-400 hover:text-red-600 hover:bg-red-50 rounded" [class.opacity-50]="sp.nonEditable" [class.cursor-not-allowed]="sp.nonEditable" title="Delete">
1036
+ <ui-icon name="trash" class="h-4 w-4" />
1037
+ </button>
1038
+ </div>
1039
+ </td>
1040
+ </tr>
1041
+ }
1042
+ </tbody>
1043
+ </table>
1044
+ </div>
1045
+ }
1046
+
1047
+ <!-- Delete Confirmation Modal -->
1048
+ <ui-modal [open]="showDeleteModal()" title="Delete Service Provider" (closed)="showDeleteModal.set(false)">
1049
+ <p class="text-sm text-gray-500">
1050
+ Are you sure you want to delete "{{ deleteTarget()?.displayName || deleteTarget()?.entityId }}"?
1051
+ This action cannot be undone.
1052
+ </p>
1053
+ <div class="mt-4 flex justify-end gap-3">
1054
+ <ui-button variant="secondary" (clicked)="showDeleteModal.set(false)">Cancel</ui-button>
1055
+ <ui-button variant="danger" [disabled]="!!actionLoading()" (clicked)="handleDelete()">
1056
+ {{ actionLoading() ? 'Deleting...' : 'Delete' }}
1057
+ </ui-button>
1058
+ </div>
1059
+ </ui-modal>
1060
+ </div>
1061
+ `,
1062
+ }]
1063
+ }] });
1064
+
1065
+ const defaultFormData = {
1066
+ entityId: '',
1067
+ displayName: '',
1068
+ description: '',
1069
+ enabled: true,
1070
+ metadataUrl: '',
1071
+ assertionConsumerServiceUrl: '',
1072
+ singleLogoutServiceUrl: '',
1073
+ signingCertificate: '',
1074
+ encryptionCertificate: '',
1075
+ encryptAssertions: false,
1076
+ nameIdFormat: '',
1077
+ requireVerifiedEmail: false,
1078
+ allowedClaims: '',
1079
+ claimMappings: '',
1080
+ ssoBinding: 'POST',
1081
+ signResponses: true,
1082
+ signAssertions: true,
1083
+ requireSignedAuthnRequests: false,
1084
+ defaultRelayState: '',
1085
+ };
1086
+ class ServiceProviderDetailsPageComponent {
1087
+ samlService = inject(SamlService);
1088
+ route = inject(ActivatedRoute);
1089
+ router = inject(Router);
1090
+ entityIdCheck$ = new Subject();
1091
+ loading = signal(false, ...(ngDevMode ? [{ debugName: "loading" }] : /* istanbul ignore next */ []));
1092
+ saving = signal(false, ...(ngDevMode ? [{ debugName: "saving" }] : /* istanbul ignore next */ []));
1093
+ error = signal(null, ...(ngDevMode ? [{ debugName: "error" }] : /* istanbul ignore next */ []));
1094
+ entityIdError = signal(null, ...(ngDevMode ? [{ debugName: "entityIdError" }] : /* istanbul ignore next */ []));
1095
+ checkingEntityId = signal(false, ...(ngDevMode ? [{ debugName: "checkingEntityId" }] : /* istanbul ignore next */ []));
1096
+ testResult = signal(null, ...(ngDevMode ? [{ debugName: "testResult" }] : /* istanbul ignore next */ []));
1097
+ nonEditable = signal(false, ...(ngDevMode ? [{ debugName: "nonEditable" }] : /* istanbul ignore next */ []));
1098
+ formData = { ...defaultFormData };
1099
+ isNew = true;
1100
+ id = null;
1101
+ nameIdFormats = NAME_ID_FORMATS;
1102
+ ssoBindings = SSO_BINDINGS;
1103
+ ngOnInit() {
1104
+ const idParam = this.route.snapshot.paramMap.get('id');
1105
+ this.isNew = !idParam || idParam === 'new';
1106
+ if (!this.isNew && idParam) {
1107
+ this.id = parseInt(idParam, 10);
1108
+ this.loadServiceProvider();
1109
+ }
1110
+ // Set up entity ID availability checking with debounce
1111
+ this.entityIdCheck$
1112
+ .pipe(debounceTime(500), distinctUntilChanged())
1113
+ .subscribe((entityId) => this.checkEntityIdAvailability(entityId));
1114
+ }
1115
+ loadServiceProvider() {
1116
+ if (!this.id)
1117
+ return;
1118
+ this.loading.set(true);
1119
+ this.samlService.getForEdit(this.id).subscribe({
1120
+ next: (sp) => {
1121
+ this.formData = {
1122
+ entityId: sp.entityId,
1123
+ displayName: sp.displayName || '',
1124
+ description: sp.description || '',
1125
+ enabled: sp.enabled,
1126
+ metadataUrl: sp.metadataUrl || '',
1127
+ assertionConsumerServiceUrl: sp.assertionConsumerServiceUrl || '',
1128
+ singleLogoutServiceUrl: sp.singleLogoutServiceUrl || '',
1129
+ signingCertificate: sp.signingCertificate || '',
1130
+ encryptionCertificate: sp.encryptionCertificate || '',
1131
+ encryptAssertions: sp.encryptAssertions,
1132
+ requireVerifiedEmail: sp.requireVerifiedEmail ?? false,
1133
+ nameIdFormat: sp.nameIdFormat || '',
1134
+ allowedClaims: sp.allowedClaims?.join(', ') || '',
1135
+ claimMappings: sp.claimMappings
1136
+ ? Object.entries(sp.claimMappings)
1137
+ .map(([k, v]) => `${k}=${v}`)
1138
+ .join('\n')
1139
+ : '',
1140
+ ssoBinding: sp.ssoBinding,
1141
+ signResponses: sp.signResponses,
1142
+ signAssertions: sp.signAssertions,
1143
+ requireSignedAuthnRequests: sp.requireSignedAuthnRequests,
1144
+ defaultRelayState: sp.defaultRelayState || '',
1145
+ };
1146
+ this.nonEditable.set(sp.nonEditable);
1147
+ this.error.set(null);
1148
+ this.loading.set(false);
1149
+ },
1150
+ error: (err) => {
1151
+ console.error('Error loading service provider:', err);
1152
+ this.error.set('Failed to load service provider');
1153
+ this.loading.set(false);
1154
+ },
1155
+ });
1156
+ }
1157
+ onEntityIdChange(entityId) {
1158
+ if (this.isNew && entityId) {
1159
+ this.entityIdCheck$.next(entityId);
1160
+ }
1161
+ }
1162
+ checkEntityIdAvailability(entityId) {
1163
+ if (!entityId)
1164
+ return;
1165
+ this.checkingEntityId.set(true);
1166
+ this.samlService.checkEntityIdAvailability(entityId, this.isNew ? undefined : this.id).subscribe({
1167
+ next: (result) => {
1168
+ if (!result.available) {
1169
+ this.entityIdError.set(result.message || 'Entity ID is already in use');
1170
+ }
1171
+ else {
1172
+ this.entityIdError.set(null);
1173
+ }
1174
+ this.checkingEntityId.set(false);
1175
+ },
1176
+ error: (err) => {
1177
+ console.error('Error checking entity ID:', err);
1178
+ this.checkingEntityId.set(false);
1179
+ },
1180
+ });
1181
+ }
1182
+ parseAllowedClaims(value) {
1183
+ if (!value.trim())
1184
+ return undefined;
1185
+ return value
1186
+ .split(',')
1187
+ .map((s) => s.trim())
1188
+ .filter((s) => s);
1189
+ }
1190
+ parseClaimMappings(value) {
1191
+ if (!value.trim())
1192
+ return undefined;
1193
+ const mappings = {};
1194
+ for (const line of value.split('\n')) {
1195
+ const [key, val] = line.split('=').map((s) => s.trim());
1196
+ if (key && val) {
1197
+ mappings[key] = val;
1198
+ }
1199
+ }
1200
+ return Object.keys(mappings).length > 0 ? mappings : undefined;
1201
+ }
1202
+ handleSubmit() {
1203
+ if (this.entityIdError())
1204
+ return;
1205
+ this.saving.set(true);
1206
+ this.error.set(null);
1207
+ if (this.isNew) {
1208
+ const request = {
1209
+ entityId: this.formData.entityId,
1210
+ displayName: this.formData.displayName || undefined,
1211
+ description: this.formData.description || undefined,
1212
+ enabled: this.formData.enabled,
1213
+ metadataUrl: this.formData.metadataUrl || undefined,
1214
+ assertionConsumerServiceUrl: this.formData.assertionConsumerServiceUrl || undefined,
1215
+ singleLogoutServiceUrl: this.formData.singleLogoutServiceUrl || undefined,
1216
+ signingCertificate: this.formData.signingCertificate || undefined,
1217
+ encryptionCertificate: this.formData.encryptionCertificate || undefined,
1218
+ encryptAssertions: this.formData.encryptAssertions,
1219
+ requireVerifiedEmail: this.formData.requireVerifiedEmail,
1220
+ nameIdFormat: this.formData.nameIdFormat || undefined,
1221
+ allowedClaims: this.parseAllowedClaims(this.formData.allowedClaims),
1222
+ claimMappings: this.parseClaimMappings(this.formData.claimMappings),
1223
+ ssoBinding: this.formData.ssoBinding,
1224
+ signResponses: this.formData.signResponses,
1225
+ signAssertions: this.formData.signAssertions,
1226
+ requireSignedAuthnRequests: this.formData.requireSignedAuthnRequests,
1227
+ defaultRelayState: this.formData.defaultRelayState || undefined,
1228
+ };
1229
+ this.samlService.create(request).subscribe({
1230
+ next: (created) => {
1231
+ this.router.navigate(['/saml/service-providers', created.id]);
1232
+ },
1233
+ error: (err) => {
1234
+ console.error('Error creating service provider:', err);
1235
+ this.error.set(err.error?.error || 'Failed to create service provider');
1236
+ this.saving.set(false);
1237
+ },
1238
+ });
1239
+ }
1240
+ else {
1241
+ const request = {
1242
+ id: this.id,
1243
+ entityId: this.formData.entityId,
1244
+ displayName: this.formData.displayName || undefined,
1245
+ description: this.formData.description || undefined,
1246
+ enabled: this.formData.enabled,
1247
+ metadataUrl: this.formData.metadataUrl || undefined,
1248
+ assertionConsumerServiceUrl: this.formData.assertionConsumerServiceUrl || undefined,
1249
+ singleLogoutServiceUrl: this.formData.singleLogoutServiceUrl || undefined,
1250
+ signingCertificate: this.formData.signingCertificate || undefined,
1251
+ encryptionCertificate: this.formData.encryptionCertificate || undefined,
1252
+ encryptAssertions: this.formData.encryptAssertions,
1253
+ requireVerifiedEmail: this.formData.requireVerifiedEmail,
1254
+ nameIdFormat: this.formData.nameIdFormat || undefined,
1255
+ allowedClaims: this.parseAllowedClaims(this.formData.allowedClaims),
1256
+ claimMappings: this.parseClaimMappings(this.formData.claimMappings),
1257
+ ssoBinding: this.formData.ssoBinding,
1258
+ signResponses: this.formData.signResponses,
1259
+ signAssertions: this.formData.signAssertions,
1260
+ requireSignedAuthnRequests: this.formData.requireSignedAuthnRequests,
1261
+ defaultRelayState: this.formData.defaultRelayState || undefined,
1262
+ nonEditable: this.nonEditable(),
1263
+ };
1264
+ this.samlService.update(this.id, request).subscribe({
1265
+ next: () => {
1266
+ this.router.navigate(['/saml/service-providers']);
1267
+ },
1268
+ error: (err) => {
1269
+ console.error('Error updating service provider:', err);
1270
+ this.error.set(err.error?.error || 'Failed to save service provider');
1271
+ this.saving.set(false);
1272
+ },
1273
+ });
1274
+ }
1275
+ }
1276
+ handleTest() {
1277
+ if (this.isNew || !this.id)
1278
+ return;
1279
+ this.testResult.set(null);
1280
+ this.samlService.test(this.id).subscribe({
1281
+ next: (result) => this.testResult.set(result),
1282
+ error: () => this.testResult.set({ success: false, message: 'Failed to run test' }),
1283
+ });
1284
+ }
1285
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.25", ngImport: i0, type: ServiceProviderDetailsPageComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
1286
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.25", type: ServiceProviderDetailsPageComponent, isStandalone: true, selector: "saml-service-provider-details-page", ngImport: i0, template: `
1287
+ <div class="space-y-6">
1288
+ <div class="flex items-center space-x-4">
1289
+ <a routerLink="/saml/service-providers" class="p-2 text-gray-400 hover:text-gray-600 hover:bg-gray-100 rounded">
1290
+ <ui-icon name="arrow-left" class="h-5 w-5" />
1291
+ </a>
1292
+ <h1 class="text-2xl font-bold text-gray-900">{{ isNew ? 'Add Service Provider' : 'Edit Service Provider' }}</h1>
1293
+ </div>
1294
+
1295
+ @if (loading()) {
1296
+ <div class="space-y-6">
1297
+ <div class="h-8 w-48 bg-gray-200 rounded animate-pulse"></div>
1298
+ <div class="bg-white shadow rounded-lg p-6 space-y-4">
1299
+ @for (i of [1, 2, 3, 4]; track i) {
1300
+ <div class="h-12 bg-gray-200 rounded animate-pulse"></div>
1301
+ }
1302
+ </div>
1303
+ </div>
1304
+ } @else {
1305
+ @if (error()) {
1306
+ <div class="bg-red-50 border border-red-200 rounded-lg p-4">
1307
+ <div class="flex items-center space-x-2">
1308
+ <ui-icon name="x-circle" class="h-5 w-5 text-red-600" />
1309
+ <span class="text-red-700">{{ error() }}</span>
1310
+ </div>
1311
+ </div>
1312
+ }
1313
+
1314
+ @if (nonEditable()) {
1315
+ <div class="bg-yellow-50 border border-yellow-200 rounded-lg p-4">
1316
+ <div class="flex items-center space-x-2">
1317
+ <ui-icon name="exclamation-triangle" class="h-5 w-5 text-yellow-600" />
1318
+ <span class="text-yellow-700">This service provider is system-managed and cannot be modified.</span>
1319
+ </div>
1320
+ </div>
1321
+ }
1322
+
1323
+ @if (testResult()) {
1324
+ <div [class]="testResult()?.success ? 'bg-green-50 border-green-200' : 'bg-red-50 border-red-200'" class="rounded-lg p-4 border">
1325
+ <div class="flex items-center justify-between">
1326
+ <div class="flex items-center space-x-2">
1327
+ @if (testResult()?.success) {
1328
+ <ui-icon name="check-circle" class="h-5 w-5 text-green-600" />
1329
+ } @else {
1330
+ <ui-icon name="x-circle" class="h-5 w-5 text-red-600" />
1331
+ }
1332
+ <span [class]="testResult()?.success ? 'text-green-700' : 'text-red-700'">{{ testResult()?.message }}</span>
1333
+ </div>
1334
+ <button (click)="testResult.set(null)" class="text-gray-400 hover:text-gray-600">&times;</button>
1335
+ </div>
1336
+ </div>
1337
+ }
1338
+
1339
+ <form (ngSubmit)="handleSubmit()" class="space-y-6">
1340
+ <!-- Basic Information -->
1341
+ <div class="bg-white shadow rounded-lg">
1342
+ <div class="px-6 py-4 border-b border-gray-200">
1343
+ <h2 class="text-lg font-medium text-gray-900">Basic Information</h2>
1344
+ </div>
1345
+ <div class="px-6 py-4 space-y-4">
1346
+ <div>
1347
+ <label for="entityId" class="block text-sm font-medium text-gray-700">
1348
+ Entity ID <span class="text-red-500">*</span>
1349
+ </label>
1350
+ <input
1351
+ type="text"
1352
+ id="entityId"
1353
+ [(ngModel)]="formData.entityId"
1354
+ name="entityId"
1355
+ (ngModelChange)="onEntityIdChange($event)"
1356
+ [disabled]="!isNew || nonEditable()"
1357
+ required
1358
+ class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-primary-500 focus:ring-primary-500"
1359
+ [class.border-red-500]="entityIdError()"
1360
+ [class.bg-gray-100]="!isNew"
1361
+ placeholder="https://sp.example.com/saml"
1362
+ />
1363
+ @if (checkingEntityId()) {
1364
+ <p class="mt-1 text-sm text-gray-500">Checking availability...</p>
1365
+ }
1366
+ @if (entityIdError()) {
1367
+ <p class="mt-1 text-sm text-red-600">{{ entityIdError() }}</p>
1368
+ }
1369
+ <p class="mt-1 text-xs text-gray-500">The unique SAML Entity ID for this Service Provider</p>
1370
+ </div>
1371
+
1372
+ <div class="grid grid-cols-1 md:grid-cols-2 gap-4">
1373
+ <div>
1374
+ <label for="displayName" class="block text-sm font-medium text-gray-700">Display Name</label>
1375
+ <input
1376
+ type="text"
1377
+ id="displayName"
1378
+ [(ngModel)]="formData.displayName"
1379
+ name="displayName"
1380
+ [disabled]="nonEditable()"
1381
+ class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-primary-500 focus:ring-primary-500"
1382
+ placeholder="My Application"
1383
+ />
1384
+ </div>
1385
+
1386
+ <div>
1387
+ <label for="enabled" class="block text-sm font-medium text-gray-700">Status</label>
1388
+ <div class="mt-2">
1389
+ <label class="inline-flex items-center">
1390
+ <input
1391
+ type="checkbox"
1392
+ [(ngModel)]="formData.enabled"
1393
+ name="enabled"
1394
+ [disabled]="nonEditable()"
1395
+ class="rounded border-gray-300 text-primary-600 focus:ring-primary-500"
1396
+ />
1397
+ <span class="ml-2 text-sm text-gray-700">Enabled</span>
1398
+ </label>
1399
+ </div>
1400
+ </div>
1401
+ </div>
1402
+
1403
+ <div>
1404
+ <label for="description" class="block text-sm font-medium text-gray-700">Description</label>
1405
+ <textarea
1406
+ id="description"
1407
+ [(ngModel)]="formData.description"
1408
+ name="description"
1409
+ [disabled]="nonEditable()"
1410
+ rows="2"
1411
+ class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-primary-500 focus:ring-primary-500"
1412
+ placeholder="Optional description of this service provider"
1413
+ ></textarea>
1414
+ </div>
1415
+ </div>
1416
+ </div>
1417
+
1418
+ <!-- Endpoints -->
1419
+ <div class="bg-white shadow rounded-lg">
1420
+ <div class="px-6 py-4 border-b border-gray-200">
1421
+ <h2 class="text-lg font-medium text-gray-900">Endpoints</h2>
1422
+ <p class="mt-1 text-sm text-gray-500">You can provide a metadata URL to auto-configure, or manually specify endpoints</p>
1423
+ </div>
1424
+ <div class="px-6 py-4 space-y-4">
1425
+ <div>
1426
+ <label for="metadataUrl" class="block text-sm font-medium text-gray-700">Metadata URL</label>
1427
+ <input
1428
+ type="url"
1429
+ id="metadataUrl"
1430
+ [(ngModel)]="formData.metadataUrl"
1431
+ name="metadataUrl"
1432
+ [disabled]="nonEditable()"
1433
+ class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-primary-500 focus:ring-primary-500"
1434
+ placeholder="https://sp.example.com/saml/metadata"
1435
+ />
1436
+ <p class="mt-1 text-xs text-gray-500">If provided, endpoints and certificates will be fetched from metadata</p>
1437
+ </div>
1438
+
1439
+ <div>
1440
+ <label for="assertionConsumerServiceUrl" class="block text-sm font-medium text-gray-700">Assertion Consumer Service (ACS) URL</label>
1441
+ <input
1442
+ type="url"
1443
+ id="assertionConsumerServiceUrl"
1444
+ [(ngModel)]="formData.assertionConsumerServiceUrl"
1445
+ name="assertionConsumerServiceUrl"
1446
+ [disabled]="nonEditable()"
1447
+ class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-primary-500 focus:ring-primary-500"
1448
+ placeholder="https://sp.example.com/saml/acs"
1449
+ />
1450
+ <p class="mt-1 text-xs text-gray-500">Where SAML responses should be sent (required if no metadata URL)</p>
1451
+ </div>
1452
+
1453
+ <div>
1454
+ <label for="singleLogoutServiceUrl" class="block text-sm font-medium text-gray-700">Single Logout Service URL</label>
1455
+ <input
1456
+ type="url"
1457
+ id="singleLogoutServiceUrl"
1458
+ [(ngModel)]="formData.singleLogoutServiceUrl"
1459
+ name="singleLogoutServiceUrl"
1460
+ [disabled]="nonEditable()"
1461
+ class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-primary-500 focus:ring-primary-500"
1462
+ placeholder="https://sp.example.com/saml/slo"
1463
+ />
1464
+ </div>
1465
+
1466
+ <div class="grid grid-cols-1 md:grid-cols-2 gap-4">
1467
+ <div>
1468
+ <label for="ssoBinding" class="block text-sm font-medium text-gray-700">SSO Binding</label>
1469
+ <select
1470
+ id="ssoBinding"
1471
+ [(ngModel)]="formData.ssoBinding"
1472
+ name="ssoBinding"
1473
+ [disabled]="nonEditable()"
1474
+ class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-primary-500 focus:ring-primary-500"
1475
+ >
1476
+ @for (binding of ssoBindings; track binding.value) {
1477
+ <option [value]="binding.value">{{ binding.label }}</option>
1478
+ }
1479
+ </select>
1480
+ </div>
1481
+
1482
+ <div>
1483
+ <label for="nameIdFormat" class="block text-sm font-medium text-gray-700">Name ID Format</label>
1484
+ <select
1485
+ id="nameIdFormat"
1486
+ [(ngModel)]="formData.nameIdFormat"
1487
+ name="nameIdFormat"
1488
+ [disabled]="nonEditable()"
1489
+ class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-primary-500 focus:ring-primary-500"
1490
+ >
1491
+ @for (format of nameIdFormats; track format.value) {
1492
+ <option [value]="format.value">{{ format.label }}</option>
1493
+ }
1494
+ </select>
1495
+ </div>
1496
+ </div>
1497
+ </div>
1498
+ </div>
1499
+
1500
+ <!-- Security -->
1501
+ <div class="bg-white shadow rounded-lg">
1502
+ <div class="px-6 py-4 border-b border-gray-200">
1503
+ <h2 class="text-lg font-medium text-gray-900">Security</h2>
1504
+ </div>
1505
+ <div class="px-6 py-4 space-y-4">
1506
+ <div class="grid grid-cols-1 md:grid-cols-2 gap-4">
1507
+ <label class="flex items-center">
1508
+ <input
1509
+ type="checkbox"
1510
+ [(ngModel)]="formData.signResponses"
1511
+ name="signResponses"
1512
+ [disabled]="nonEditable()"
1513
+ class="rounded border-gray-300 text-primary-600 focus:ring-primary-500"
1514
+ />
1515
+ <span class="ml-2 text-sm text-gray-700">Sign SAML Responses</span>
1516
+ </label>
1517
+
1518
+ <label class="flex items-center">
1519
+ <input
1520
+ type="checkbox"
1521
+ [(ngModel)]="formData.signAssertions"
1522
+ name="signAssertions"
1523
+ [disabled]="nonEditable()"
1524
+ class="rounded border-gray-300 text-primary-600 focus:ring-primary-500"
1525
+ />
1526
+ <span class="ml-2 text-sm text-gray-700">Sign SAML Assertions</span>
1527
+ </label>
1528
+
1529
+ <label class="flex items-center">
1530
+ <input
1531
+ type="checkbox"
1532
+ [(ngModel)]="formData.requireSignedAuthnRequests"
1533
+ name="requireSignedAuthnRequests"
1534
+ [disabled]="nonEditable()"
1535
+ class="rounded border-gray-300 text-primary-600 focus:ring-primary-500"
1536
+ />
1537
+ <span class="ml-2 text-sm text-gray-700">Require Signed AuthnRequests</span>
1538
+ </label>
1539
+
1540
+ <label class="flex items-center">
1541
+ <input
1542
+ type="checkbox"
1543
+ [(ngModel)]="formData.encryptAssertions"
1544
+ name="encryptAssertions"
1545
+ [disabled]="nonEditable()"
1546
+ class="rounded border-gray-300 text-primary-600 focus:ring-primary-500"
1547
+ />
1548
+ <span class="ml-2 text-sm text-gray-700">Encrypt Assertions</span>
1549
+ </label>
1550
+
1551
+ <label class="flex items-start md:col-span-2">
1552
+ <input
1553
+ type="checkbox"
1554
+ [(ngModel)]="formData.requireVerifiedEmail"
1555
+ name="requireVerifiedEmail"
1556
+ [disabled]="nonEditable()"
1557
+ class="mt-0.5 rounded border-gray-300 text-primary-600 focus:ring-primary-500"
1558
+ />
1559
+ <span class="ml-2 text-sm text-gray-700">
1560
+ Require a verified email address
1561
+ <span class="block text-xs text-gray-500">
1562
+ Only people whose email is verified get assertions for this service provider. Always applies
1563
+ when the Name ID format is the email address.
1564
+ </span>
1565
+ </span>
1566
+ </label>
1567
+ </div>
1568
+
1569
+ <div>
1570
+ <label for="signingCertificate" class="block text-sm font-medium text-gray-700">SP Signing Certificate (Base64)</label>
1571
+ <textarea
1572
+ id="signingCertificate"
1573
+ [(ngModel)]="formData.signingCertificate"
1574
+ name="signingCertificate"
1575
+ [disabled]="nonEditable()"
1576
+ rows="3"
1577
+ class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-primary-500 focus:ring-primary-500 font-mono text-xs"
1578
+ placeholder="MIIDx..."
1579
+ ></textarea>
1580
+ <p class="mt-1 text-xs text-gray-500">Public certificate for verifying signed AuthnRequests from this SP</p>
1581
+ </div>
1582
+
1583
+ <div>
1584
+ <label for="encryptionCertificate" class="block text-sm font-medium text-gray-700">SP Encryption Certificate (Base64)</label>
1585
+ <textarea
1586
+ id="encryptionCertificate"
1587
+ [(ngModel)]="formData.encryptionCertificate"
1588
+ name="encryptionCertificate"
1589
+ [disabled]="nonEditable()"
1590
+ rows="3"
1591
+ class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-primary-500 focus:ring-primary-500 font-mono text-xs"
1592
+ placeholder="MIIDx..."
1593
+ ></textarea>
1594
+ <p class="mt-1 text-xs text-gray-500">Public certificate for encrypting assertions sent to this SP</p>
1595
+ </div>
1596
+ </div>
1597
+ </div>
1598
+
1599
+ <!-- Claims -->
1600
+ <div class="bg-white shadow rounded-lg">
1601
+ <div class="px-6 py-4 border-b border-gray-200">
1602
+ <h2 class="text-lg font-medium text-gray-900">Claims Configuration</h2>
1603
+ </div>
1604
+ <div class="px-6 py-4 space-y-4">
1605
+ <div>
1606
+ <label for="allowedClaims" class="block text-sm font-medium text-gray-700">Allowed Claims</label>
1607
+ <input
1608
+ type="text"
1609
+ id="allowedClaims"
1610
+ [(ngModel)]="formData.allowedClaims"
1611
+ name="allowedClaims"
1612
+ [disabled]="nonEditable()"
1613
+ class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-primary-500 focus:ring-primary-500"
1614
+ placeholder="email, name, given_name, family_name"
1615
+ />
1616
+ <p class="mt-1 text-xs text-gray-500">Comma-separated list of claim types to include in assertions (empty = all claims)</p>
1617
+ </div>
1618
+
1619
+ <div>
1620
+ <label for="claimMappings" class="block text-sm font-medium text-gray-700">Claim Mappings</label>
1621
+ <textarea
1622
+ id="claimMappings"
1623
+ [(ngModel)]="formData.claimMappings"
1624
+ name="claimMappings"
1625
+ [disabled]="nonEditable()"
1626
+ rows="4"
1627
+ class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-primary-500 focus:ring-primary-500 font-mono text-sm"
1628
+ placeholder="http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress=email"
1629
+ ></textarea>
1630
+ <p class="mt-1 text-xs text-gray-500">Map SAML attribute names to claim types (one per line: samlAttribute=claimType)</p>
1631
+ </div>
1632
+
1633
+ <div>
1634
+ <label for="defaultRelayState" class="block text-sm font-medium text-gray-700">Default RelayState</label>
1635
+ <input
1636
+ type="text"
1637
+ id="defaultRelayState"
1638
+ [(ngModel)]="formData.defaultRelayState"
1639
+ name="defaultRelayState"
1640
+ [disabled]="nonEditable()"
1641
+ class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-primary-500 focus:ring-primary-500"
1642
+ placeholder="https://sp.example.com/dashboard"
1643
+ />
1644
+ <p class="mt-1 text-xs text-gray-500">Default redirect URL if the SP doesn't provide a RelayState</p>
1645
+ </div>
1646
+ </div>
1647
+ </div>
1648
+
1649
+ <!-- Actions -->
1650
+ <div class="flex items-center justify-between">
1651
+ <div>
1652
+ @if (!isNew) {
1653
+ <ui-button type="button" variant="secondary" (clicked)="handleTest()">Test Configuration</ui-button>
1654
+ }
1655
+ </div>
1656
+ <div class="flex items-center space-x-4">
1657
+ <a routerLink="/saml/service-providers">
1658
+ <ui-button type="button" variant="secondary">Cancel</ui-button>
1659
+ </a>
1660
+ <ui-button type="submit" [disabled]="saving() || nonEditable() || !!entityIdError()">
1661
+ {{ saving() ? 'Saving...' : isNew ? 'Create Service Provider' : 'Save Changes' }}
1662
+ </ui-button>
1663
+ </div>
1664
+ </div>
1665
+ </form>
1666
+ }
1667
+ </div>
1668
+ `, isInline: true, dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1.ɵNgNoValidate, selector: "form:not([ngNoForm]):not([ngNativeValidate])" }, { kind: "directive", type: i1.NgSelectOption, selector: "option", inputs: ["ngValue", "value"] }, { kind: "directive", type: i1.ɵNgSelectMultipleOption, selector: "option", inputs: ["ngValue", "value"] }, { kind: "directive", type: i1.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i1.CheckboxControlValueAccessor, selector: "input[type=checkbox][formControlName],input[type=checkbox][formControl],input[type=checkbox][ngModel]" }, { kind: "directive", type: i1.SelectControlValueAccessor, selector: "select:not([multiple])[formControlName],select:not([multiple])[formControl],select:not([multiple])[ngModel]", inputs: ["compareWith"] }, { kind: "directive", type: i1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],[formArray],form:not([ngNoForm]),[ngForm]" }, { kind: "directive", type: i1.RequiredValidator, selector: ":not([type=checkbox])[required][formControlName],:not([type=checkbox])[required][formControl],:not([type=checkbox])[required][ngModel]", inputs: ["required"] }, { kind: "directive", type: i1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "directive", type: i1.NgForm, selector: "form:not([ngNoForm]):not([formGroup]):not([formArray]),ng-form,[ngForm]", inputs: ["ngFormOptions"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }, { kind: "directive", type: RouterLink, selector: "[routerLink]", inputs: ["target", "queryParams", "fragment", "queryParamsHandling", "state", "info", "relativeTo", "preserveFragment", "skipLocationChange", "replaceUrl", "routerLink"] }, { kind: "component", type: ButtonComponent, selector: "ui-button", inputs: ["variant", "size", "disabled", "loading", "type", "fullWidth"], outputs: ["clicked"] }, { kind: "component", type: IconComponent, selector: "ui-icon", inputs: ["name", "strokeWidth"] }] });
1669
+ }
1670
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.25", ngImport: i0, type: ServiceProviderDetailsPageComponent, decorators: [{
1671
+ type: Component,
1672
+ args: [{
1673
+ selector: 'saml-service-provider-details-page',
1674
+ standalone: true,
1675
+ imports: [CommonModule, FormsModule, RouterLink, ButtonComponent, IconComponent],
1676
+ template: `
1677
+ <div class="space-y-6">
1678
+ <div class="flex items-center space-x-4">
1679
+ <a routerLink="/saml/service-providers" class="p-2 text-gray-400 hover:text-gray-600 hover:bg-gray-100 rounded">
1680
+ <ui-icon name="arrow-left" class="h-5 w-5" />
1681
+ </a>
1682
+ <h1 class="text-2xl font-bold text-gray-900">{{ isNew ? 'Add Service Provider' : 'Edit Service Provider' }}</h1>
1683
+ </div>
1684
+
1685
+ @if (loading()) {
1686
+ <div class="space-y-6">
1687
+ <div class="h-8 w-48 bg-gray-200 rounded animate-pulse"></div>
1688
+ <div class="bg-white shadow rounded-lg p-6 space-y-4">
1689
+ @for (i of [1, 2, 3, 4]; track i) {
1690
+ <div class="h-12 bg-gray-200 rounded animate-pulse"></div>
1691
+ }
1692
+ </div>
1693
+ </div>
1694
+ } @else {
1695
+ @if (error()) {
1696
+ <div class="bg-red-50 border border-red-200 rounded-lg p-4">
1697
+ <div class="flex items-center space-x-2">
1698
+ <ui-icon name="x-circle" class="h-5 w-5 text-red-600" />
1699
+ <span class="text-red-700">{{ error() }}</span>
1700
+ </div>
1701
+ </div>
1702
+ }
1703
+
1704
+ @if (nonEditable()) {
1705
+ <div class="bg-yellow-50 border border-yellow-200 rounded-lg p-4">
1706
+ <div class="flex items-center space-x-2">
1707
+ <ui-icon name="exclamation-triangle" class="h-5 w-5 text-yellow-600" />
1708
+ <span class="text-yellow-700">This service provider is system-managed and cannot be modified.</span>
1709
+ </div>
1710
+ </div>
1711
+ }
1712
+
1713
+ @if (testResult()) {
1714
+ <div [class]="testResult()?.success ? 'bg-green-50 border-green-200' : 'bg-red-50 border-red-200'" class="rounded-lg p-4 border">
1715
+ <div class="flex items-center justify-between">
1716
+ <div class="flex items-center space-x-2">
1717
+ @if (testResult()?.success) {
1718
+ <ui-icon name="check-circle" class="h-5 w-5 text-green-600" />
1719
+ } @else {
1720
+ <ui-icon name="x-circle" class="h-5 w-5 text-red-600" />
1721
+ }
1722
+ <span [class]="testResult()?.success ? 'text-green-700' : 'text-red-700'">{{ testResult()?.message }}</span>
1723
+ </div>
1724
+ <button (click)="testResult.set(null)" class="text-gray-400 hover:text-gray-600">&times;</button>
1725
+ </div>
1726
+ </div>
1727
+ }
1728
+
1729
+ <form (ngSubmit)="handleSubmit()" class="space-y-6">
1730
+ <!-- Basic Information -->
1731
+ <div class="bg-white shadow rounded-lg">
1732
+ <div class="px-6 py-4 border-b border-gray-200">
1733
+ <h2 class="text-lg font-medium text-gray-900">Basic Information</h2>
1734
+ </div>
1735
+ <div class="px-6 py-4 space-y-4">
1736
+ <div>
1737
+ <label for="entityId" class="block text-sm font-medium text-gray-700">
1738
+ Entity ID <span class="text-red-500">*</span>
1739
+ </label>
1740
+ <input
1741
+ type="text"
1742
+ id="entityId"
1743
+ [(ngModel)]="formData.entityId"
1744
+ name="entityId"
1745
+ (ngModelChange)="onEntityIdChange($event)"
1746
+ [disabled]="!isNew || nonEditable()"
1747
+ required
1748
+ class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-primary-500 focus:ring-primary-500"
1749
+ [class.border-red-500]="entityIdError()"
1750
+ [class.bg-gray-100]="!isNew"
1751
+ placeholder="https://sp.example.com/saml"
1752
+ />
1753
+ @if (checkingEntityId()) {
1754
+ <p class="mt-1 text-sm text-gray-500">Checking availability...</p>
1755
+ }
1756
+ @if (entityIdError()) {
1757
+ <p class="mt-1 text-sm text-red-600">{{ entityIdError() }}</p>
1758
+ }
1759
+ <p class="mt-1 text-xs text-gray-500">The unique SAML Entity ID for this Service Provider</p>
1760
+ </div>
1761
+
1762
+ <div class="grid grid-cols-1 md:grid-cols-2 gap-4">
1763
+ <div>
1764
+ <label for="displayName" class="block text-sm font-medium text-gray-700">Display Name</label>
1765
+ <input
1766
+ type="text"
1767
+ id="displayName"
1768
+ [(ngModel)]="formData.displayName"
1769
+ name="displayName"
1770
+ [disabled]="nonEditable()"
1771
+ class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-primary-500 focus:ring-primary-500"
1772
+ placeholder="My Application"
1773
+ />
1774
+ </div>
1775
+
1776
+ <div>
1777
+ <label for="enabled" class="block text-sm font-medium text-gray-700">Status</label>
1778
+ <div class="mt-2">
1779
+ <label class="inline-flex items-center">
1780
+ <input
1781
+ type="checkbox"
1782
+ [(ngModel)]="formData.enabled"
1783
+ name="enabled"
1784
+ [disabled]="nonEditable()"
1785
+ class="rounded border-gray-300 text-primary-600 focus:ring-primary-500"
1786
+ />
1787
+ <span class="ml-2 text-sm text-gray-700">Enabled</span>
1788
+ </label>
1789
+ </div>
1790
+ </div>
1791
+ </div>
1792
+
1793
+ <div>
1794
+ <label for="description" class="block text-sm font-medium text-gray-700">Description</label>
1795
+ <textarea
1796
+ id="description"
1797
+ [(ngModel)]="formData.description"
1798
+ name="description"
1799
+ [disabled]="nonEditable()"
1800
+ rows="2"
1801
+ class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-primary-500 focus:ring-primary-500"
1802
+ placeholder="Optional description of this service provider"
1803
+ ></textarea>
1804
+ </div>
1805
+ </div>
1806
+ </div>
1807
+
1808
+ <!-- Endpoints -->
1809
+ <div class="bg-white shadow rounded-lg">
1810
+ <div class="px-6 py-4 border-b border-gray-200">
1811
+ <h2 class="text-lg font-medium text-gray-900">Endpoints</h2>
1812
+ <p class="mt-1 text-sm text-gray-500">You can provide a metadata URL to auto-configure, or manually specify endpoints</p>
1813
+ </div>
1814
+ <div class="px-6 py-4 space-y-4">
1815
+ <div>
1816
+ <label for="metadataUrl" class="block text-sm font-medium text-gray-700">Metadata URL</label>
1817
+ <input
1818
+ type="url"
1819
+ id="metadataUrl"
1820
+ [(ngModel)]="formData.metadataUrl"
1821
+ name="metadataUrl"
1822
+ [disabled]="nonEditable()"
1823
+ class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-primary-500 focus:ring-primary-500"
1824
+ placeholder="https://sp.example.com/saml/metadata"
1825
+ />
1826
+ <p class="mt-1 text-xs text-gray-500">If provided, endpoints and certificates will be fetched from metadata</p>
1827
+ </div>
1828
+
1829
+ <div>
1830
+ <label for="assertionConsumerServiceUrl" class="block text-sm font-medium text-gray-700">Assertion Consumer Service (ACS) URL</label>
1831
+ <input
1832
+ type="url"
1833
+ id="assertionConsumerServiceUrl"
1834
+ [(ngModel)]="formData.assertionConsumerServiceUrl"
1835
+ name="assertionConsumerServiceUrl"
1836
+ [disabled]="nonEditable()"
1837
+ class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-primary-500 focus:ring-primary-500"
1838
+ placeholder="https://sp.example.com/saml/acs"
1839
+ />
1840
+ <p class="mt-1 text-xs text-gray-500">Where SAML responses should be sent (required if no metadata URL)</p>
1841
+ </div>
1842
+
1843
+ <div>
1844
+ <label for="singleLogoutServiceUrl" class="block text-sm font-medium text-gray-700">Single Logout Service URL</label>
1845
+ <input
1846
+ type="url"
1847
+ id="singleLogoutServiceUrl"
1848
+ [(ngModel)]="formData.singleLogoutServiceUrl"
1849
+ name="singleLogoutServiceUrl"
1850
+ [disabled]="nonEditable()"
1851
+ class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-primary-500 focus:ring-primary-500"
1852
+ placeholder="https://sp.example.com/saml/slo"
1853
+ />
1854
+ </div>
1855
+
1856
+ <div class="grid grid-cols-1 md:grid-cols-2 gap-4">
1857
+ <div>
1858
+ <label for="ssoBinding" class="block text-sm font-medium text-gray-700">SSO Binding</label>
1859
+ <select
1860
+ id="ssoBinding"
1861
+ [(ngModel)]="formData.ssoBinding"
1862
+ name="ssoBinding"
1863
+ [disabled]="nonEditable()"
1864
+ class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-primary-500 focus:ring-primary-500"
1865
+ >
1866
+ @for (binding of ssoBindings; track binding.value) {
1867
+ <option [value]="binding.value">{{ binding.label }}</option>
1868
+ }
1869
+ </select>
1870
+ </div>
1871
+
1872
+ <div>
1873
+ <label for="nameIdFormat" class="block text-sm font-medium text-gray-700">Name ID Format</label>
1874
+ <select
1875
+ id="nameIdFormat"
1876
+ [(ngModel)]="formData.nameIdFormat"
1877
+ name="nameIdFormat"
1878
+ [disabled]="nonEditable()"
1879
+ class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-primary-500 focus:ring-primary-500"
1880
+ >
1881
+ @for (format of nameIdFormats; track format.value) {
1882
+ <option [value]="format.value">{{ format.label }}</option>
1883
+ }
1884
+ </select>
1885
+ </div>
1886
+ </div>
1887
+ </div>
1888
+ </div>
1889
+
1890
+ <!-- Security -->
1891
+ <div class="bg-white shadow rounded-lg">
1892
+ <div class="px-6 py-4 border-b border-gray-200">
1893
+ <h2 class="text-lg font-medium text-gray-900">Security</h2>
1894
+ </div>
1895
+ <div class="px-6 py-4 space-y-4">
1896
+ <div class="grid grid-cols-1 md:grid-cols-2 gap-4">
1897
+ <label class="flex items-center">
1898
+ <input
1899
+ type="checkbox"
1900
+ [(ngModel)]="formData.signResponses"
1901
+ name="signResponses"
1902
+ [disabled]="nonEditable()"
1903
+ class="rounded border-gray-300 text-primary-600 focus:ring-primary-500"
1904
+ />
1905
+ <span class="ml-2 text-sm text-gray-700">Sign SAML Responses</span>
1906
+ </label>
1907
+
1908
+ <label class="flex items-center">
1909
+ <input
1910
+ type="checkbox"
1911
+ [(ngModel)]="formData.signAssertions"
1912
+ name="signAssertions"
1913
+ [disabled]="nonEditable()"
1914
+ class="rounded border-gray-300 text-primary-600 focus:ring-primary-500"
1915
+ />
1916
+ <span class="ml-2 text-sm text-gray-700">Sign SAML Assertions</span>
1917
+ </label>
1918
+
1919
+ <label class="flex items-center">
1920
+ <input
1921
+ type="checkbox"
1922
+ [(ngModel)]="formData.requireSignedAuthnRequests"
1923
+ name="requireSignedAuthnRequests"
1924
+ [disabled]="nonEditable()"
1925
+ class="rounded border-gray-300 text-primary-600 focus:ring-primary-500"
1926
+ />
1927
+ <span class="ml-2 text-sm text-gray-700">Require Signed AuthnRequests</span>
1928
+ </label>
1929
+
1930
+ <label class="flex items-center">
1931
+ <input
1932
+ type="checkbox"
1933
+ [(ngModel)]="formData.encryptAssertions"
1934
+ name="encryptAssertions"
1935
+ [disabled]="nonEditable()"
1936
+ class="rounded border-gray-300 text-primary-600 focus:ring-primary-500"
1937
+ />
1938
+ <span class="ml-2 text-sm text-gray-700">Encrypt Assertions</span>
1939
+ </label>
1940
+
1941
+ <label class="flex items-start md:col-span-2">
1942
+ <input
1943
+ type="checkbox"
1944
+ [(ngModel)]="formData.requireVerifiedEmail"
1945
+ name="requireVerifiedEmail"
1946
+ [disabled]="nonEditable()"
1947
+ class="mt-0.5 rounded border-gray-300 text-primary-600 focus:ring-primary-500"
1948
+ />
1949
+ <span class="ml-2 text-sm text-gray-700">
1950
+ Require a verified email address
1951
+ <span class="block text-xs text-gray-500">
1952
+ Only people whose email is verified get assertions for this service provider. Always applies
1953
+ when the Name ID format is the email address.
1954
+ </span>
1955
+ </span>
1956
+ </label>
1957
+ </div>
1958
+
1959
+ <div>
1960
+ <label for="signingCertificate" class="block text-sm font-medium text-gray-700">SP Signing Certificate (Base64)</label>
1961
+ <textarea
1962
+ id="signingCertificate"
1963
+ [(ngModel)]="formData.signingCertificate"
1964
+ name="signingCertificate"
1965
+ [disabled]="nonEditable()"
1966
+ rows="3"
1967
+ class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-primary-500 focus:ring-primary-500 font-mono text-xs"
1968
+ placeholder="MIIDx..."
1969
+ ></textarea>
1970
+ <p class="mt-1 text-xs text-gray-500">Public certificate for verifying signed AuthnRequests from this SP</p>
1971
+ </div>
1972
+
1973
+ <div>
1974
+ <label for="encryptionCertificate" class="block text-sm font-medium text-gray-700">SP Encryption Certificate (Base64)</label>
1975
+ <textarea
1976
+ id="encryptionCertificate"
1977
+ [(ngModel)]="formData.encryptionCertificate"
1978
+ name="encryptionCertificate"
1979
+ [disabled]="nonEditable()"
1980
+ rows="3"
1981
+ class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-primary-500 focus:ring-primary-500 font-mono text-xs"
1982
+ placeholder="MIIDx..."
1983
+ ></textarea>
1984
+ <p class="mt-1 text-xs text-gray-500">Public certificate for encrypting assertions sent to this SP</p>
1985
+ </div>
1986
+ </div>
1987
+ </div>
1988
+
1989
+ <!-- Claims -->
1990
+ <div class="bg-white shadow rounded-lg">
1991
+ <div class="px-6 py-4 border-b border-gray-200">
1992
+ <h2 class="text-lg font-medium text-gray-900">Claims Configuration</h2>
1993
+ </div>
1994
+ <div class="px-6 py-4 space-y-4">
1995
+ <div>
1996
+ <label for="allowedClaims" class="block text-sm font-medium text-gray-700">Allowed Claims</label>
1997
+ <input
1998
+ type="text"
1999
+ id="allowedClaims"
2000
+ [(ngModel)]="formData.allowedClaims"
2001
+ name="allowedClaims"
2002
+ [disabled]="nonEditable()"
2003
+ class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-primary-500 focus:ring-primary-500"
2004
+ placeholder="email, name, given_name, family_name"
2005
+ />
2006
+ <p class="mt-1 text-xs text-gray-500">Comma-separated list of claim types to include in assertions (empty = all claims)</p>
2007
+ </div>
2008
+
2009
+ <div>
2010
+ <label for="claimMappings" class="block text-sm font-medium text-gray-700">Claim Mappings</label>
2011
+ <textarea
2012
+ id="claimMappings"
2013
+ [(ngModel)]="formData.claimMappings"
2014
+ name="claimMappings"
2015
+ [disabled]="nonEditable()"
2016
+ rows="4"
2017
+ class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-primary-500 focus:ring-primary-500 font-mono text-sm"
2018
+ placeholder="http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress=email"
2019
+ ></textarea>
2020
+ <p class="mt-1 text-xs text-gray-500">Map SAML attribute names to claim types (one per line: samlAttribute=claimType)</p>
2021
+ </div>
2022
+
2023
+ <div>
2024
+ <label for="defaultRelayState" class="block text-sm font-medium text-gray-700">Default RelayState</label>
2025
+ <input
2026
+ type="text"
2027
+ id="defaultRelayState"
2028
+ [(ngModel)]="formData.defaultRelayState"
2029
+ name="defaultRelayState"
2030
+ [disabled]="nonEditable()"
2031
+ class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-primary-500 focus:ring-primary-500"
2032
+ placeholder="https://sp.example.com/dashboard"
2033
+ />
2034
+ <p class="mt-1 text-xs text-gray-500">Default redirect URL if the SP doesn't provide a RelayState</p>
2035
+ </div>
2036
+ </div>
2037
+ </div>
2038
+
2039
+ <!-- Actions -->
2040
+ <div class="flex items-center justify-between">
2041
+ <div>
2042
+ @if (!isNew) {
2043
+ <ui-button type="button" variant="secondary" (clicked)="handleTest()">Test Configuration</ui-button>
2044
+ }
2045
+ </div>
2046
+ <div class="flex items-center space-x-4">
2047
+ <a routerLink="/saml/service-providers">
2048
+ <ui-button type="button" variant="secondary">Cancel</ui-button>
2049
+ </a>
2050
+ <ui-button type="submit" [disabled]="saving() || nonEditable() || !!entityIdError()">
2051
+ {{ saving() ? 'Saving...' : isNew ? 'Create Service Provider' : 'Save Changes' }}
2052
+ </ui-button>
2053
+ </div>
2054
+ </div>
2055
+ </form>
2056
+ }
2057
+ </div>
2058
+ `,
2059
+ }]
2060
+ }] });
2061
+
2062
+ class SamlIdpSettingsTabComponent {
2063
+ samlService = inject(SamlService);
2064
+ tabContext = inject(TAB_CONTEXT, { optional: true });
2065
+ loading = signal(true, ...(ngDevMode ? [{ debugName: "loading" }] : /* istanbul ignore next */ []));
2066
+ saving = signal(false, ...(ngDevMode ? [{ debugName: "saving" }] : /* istanbul ignore next */ []));
2067
+ error = signal(null, ...(ngDevMode ? [{ debugName: "error" }] : /* istanbul ignore next */ []));
2068
+ success = signal(null, ...(ngDevMode ? [{ debugName: "success" }] : /* istanbul ignore next */ []));
2069
+ hasChanges = signal(false, ...(ngDevMode ? [{ debugName: "hasChanges" }] : /* istanbul ignore next */ []));
2070
+ idpInfo = signal(null, ...(ngDevMode ? [{ debugName: "idpInfo" }] : /* istanbul ignore next */ []));
2071
+ signingCert = signal(null, ...(ngDevMode ? [{ debugName: "signingCert" }] : /* istanbul ignore next */ []));
2072
+ encryptionCert = signal(null, ...(ngDevMode ? [{ debugName: "encryptionCert" }] : /* istanbul ignore next */ []));
2073
+ certLoading = signal(null, ...(ngDevMode ? [{ debugName: "certLoading" }] : /* istanbul ignore next */ []));
2074
+ copied = signal(null, ...(ngDevMode ? [{ debugName: "copied" }] : /* istanbul ignore next */ []));
2075
+ showSigningUpload = signal(false, ...(ngDevMode ? [{ debugName: "showSigningUpload" }] : /* istanbul ignore next */ []));
2076
+ showEncryptionUpload = signal(false, ...(ngDevMode ? [{ debugName: "showEncryptionUpload" }] : /* istanbul ignore next */ []));
2077
+ config = {
2078
+ enabled: false,
2079
+ loginJourneyName: undefined,
2080
+ };
2081
+ signingPassword = '';
2082
+ encryptionPassword = '';
2083
+ ngOnInit() {
2084
+ this.loadData();
2085
+ }
2086
+ loadData() {
2087
+ this.loading.set(true);
2088
+ forkJoin({
2089
+ config: this.samlService.getIdpConfiguration(),
2090
+ idpInfo: this.samlService.getIdpInfo().pipe(catchError(() => of(null))),
2091
+ signingCert: this.samlService.getSigningCertificate().pipe(catchError(() => of(null))),
2092
+ encryptionCert: this.samlService.getEncryptionCertificate().pipe(catchError(() => of(null))),
2093
+ }).subscribe({
2094
+ next: (data) => {
2095
+ this.config = data.config;
2096
+ this.idpInfo.set(data.idpInfo);
2097
+ this.signingCert.set(data.signingCert);
2098
+ this.encryptionCert.set(data.encryptionCert);
2099
+ this.error.set(null);
2100
+ this.loading.set(false);
2101
+ },
2102
+ error: (err) => {
2103
+ console.error('Error loading SAML IdP config:', err);
2104
+ this.error.set('Failed to load SAML IdP configuration');
2105
+ this.loading.set(false);
2106
+ },
2107
+ });
2108
+ }
2109
+ toggleEnabled() {
2110
+ this.config.enabled = !this.config.enabled;
2111
+ this.onConfigChange();
2112
+ }
2113
+ onConfigChange() {
2114
+ this.hasChanges.set(true);
2115
+ this.success.set(null);
2116
+ }
2117
+ handleSave() {
2118
+ this.saving.set(true);
2119
+ this.error.set(null);
2120
+ this.samlService.updateIdpConfiguration(this.config).subscribe({
2121
+ next: () => {
2122
+ this.success.set('SAML IdP settings saved successfully');
2123
+ this.hasChanges.set(false);
2124
+ this.saving.set(false);
2125
+ // Reload IdP info in case endpoints changed
2126
+ this.samlService.getIdpInfo().subscribe({
2127
+ next: (info) => this.idpInfo.set(info),
2128
+ error: () => { },
2129
+ });
2130
+ },
2131
+ error: (err) => {
2132
+ console.error('Error saving SAML IdP config:', err);
2133
+ this.error.set('Failed to save SAML IdP configuration');
2134
+ this.saving.set(false);
2135
+ },
2136
+ });
2137
+ }
2138
+ // Signing Certificate Methods
2139
+ generateSigningCert() {
2140
+ this.certLoading.set('signing');
2141
+ this.samlService.generateSigningCertificate().subscribe({
2142
+ next: (cert) => {
2143
+ this.signingCert.set(cert);
2144
+ this.success.set('Signing certificate generated successfully');
2145
+ this.certLoading.set(null);
2146
+ },
2147
+ error: (err) => {
2148
+ console.error('Error generating signing certificate:', err);
2149
+ this.error.set('Failed to generate signing certificate');
2150
+ this.certLoading.set(null);
2151
+ },
2152
+ });
2153
+ }
2154
+ resetSigningCert() {
2155
+ this.certLoading.set('signing');
2156
+ this.samlService.resetSigningCertificate().subscribe({
2157
+ next: () => {
2158
+ this.loadCertificates();
2159
+ this.success.set('Signing certificate reset to global');
2160
+ this.certLoading.set(null);
2161
+ },
2162
+ error: (err) => {
2163
+ console.error('Error resetting signing certificate:', err);
2164
+ this.error.set('Failed to reset signing certificate');
2165
+ this.certLoading.set(null);
2166
+ },
2167
+ });
2168
+ }
2169
+ toggleSigningUpload() {
2170
+ this.showSigningUpload.update((v) => !v);
2171
+ if (!this.showSigningUpload()) {
2172
+ this.signingPassword = '';
2173
+ }
2174
+ }
2175
+ onSigningFileSelect(event) {
2176
+ const input = event.target;
2177
+ const file = input.files?.[0];
2178
+ if (!file)
2179
+ return;
2180
+ this.certLoading.set('signing');
2181
+ const reader = new FileReader();
2182
+ reader.onload = () => {
2183
+ const arrayBuffer = reader.result;
2184
+ const bytes = new Uint8Array(arrayBuffer);
2185
+ const base64 = btoa(String.fromCharCode(...bytes));
2186
+ this.samlService.uploadSigningCertificate({
2187
+ base64Pfx: base64,
2188
+ password: this.signingPassword || undefined,
2189
+ }).subscribe({
2190
+ next: (cert) => {
2191
+ this.signingCert.set(cert);
2192
+ this.success.set('Signing certificate uploaded successfully');
2193
+ this.certLoading.set(null);
2194
+ this.showSigningUpload.set(false);
2195
+ this.signingPassword = '';
2196
+ },
2197
+ error: (err) => {
2198
+ console.error('Error uploading signing certificate:', err);
2199
+ this.error.set('Failed to upload signing certificate');
2200
+ this.certLoading.set(null);
2201
+ },
2202
+ });
2203
+ };
2204
+ reader.readAsArrayBuffer(file);
2205
+ input.value = '';
2206
+ }
2207
+ // Encryption Certificate Methods
2208
+ generateEncryptionCert() {
2209
+ this.certLoading.set('encryption');
2210
+ this.samlService.generateEncryptionCertificate().subscribe({
2211
+ next: (cert) => {
2212
+ this.encryptionCert.set(cert);
2213
+ this.success.set('Encryption certificate generated successfully');
2214
+ this.certLoading.set(null);
2215
+ },
2216
+ error: (err) => {
2217
+ console.error('Error generating encryption certificate:', err);
2218
+ this.error.set('Failed to generate encryption certificate');
2219
+ this.certLoading.set(null);
2220
+ },
2221
+ });
2222
+ }
2223
+ resetEncryptionCert() {
2224
+ this.certLoading.set('encryption');
2225
+ this.samlService.resetEncryptionCertificate().subscribe({
2226
+ next: () => {
2227
+ this.loadCertificates();
2228
+ this.success.set('Encryption certificate reset to global');
2229
+ this.certLoading.set(null);
2230
+ },
2231
+ error: (err) => {
2232
+ console.error('Error resetting encryption certificate:', err);
2233
+ this.error.set('Failed to reset encryption certificate');
2234
+ this.certLoading.set(null);
2235
+ },
2236
+ });
2237
+ }
2238
+ toggleEncryptionUpload() {
2239
+ this.showEncryptionUpload.update((v) => !v);
2240
+ if (!this.showEncryptionUpload()) {
2241
+ this.encryptionPassword = '';
2242
+ }
2243
+ }
2244
+ onEncryptionFileSelect(event) {
2245
+ const input = event.target;
2246
+ const file = input.files?.[0];
2247
+ if (!file)
2248
+ return;
2249
+ this.certLoading.set('encryption');
2250
+ const reader = new FileReader();
2251
+ reader.onload = () => {
2252
+ const arrayBuffer = reader.result;
2253
+ const bytes = new Uint8Array(arrayBuffer);
2254
+ const base64 = btoa(String.fromCharCode(...bytes));
2255
+ this.samlService.uploadEncryptionCertificate({
2256
+ base64Pfx: base64,
2257
+ password: this.encryptionPassword || undefined,
2258
+ }).subscribe({
2259
+ next: (cert) => {
2260
+ this.encryptionCert.set(cert);
2261
+ this.success.set('Encryption certificate uploaded successfully');
2262
+ this.certLoading.set(null);
2263
+ this.showEncryptionUpload.set(false);
2264
+ this.encryptionPassword = '';
2265
+ },
2266
+ error: (err) => {
2267
+ console.error('Error uploading encryption certificate:', err);
2268
+ this.error.set('Failed to upload encryption certificate');
2269
+ this.certLoading.set(null);
2270
+ },
2271
+ });
2272
+ };
2273
+ reader.readAsArrayBuffer(file);
2274
+ input.value = '';
2275
+ }
2276
+ loadCertificates() {
2277
+ forkJoin({
2278
+ signing: this.samlService.getSigningCertificate().pipe(catchError(() => of(null))),
2279
+ encryption: this.samlService.getEncryptionCertificate().pipe(catchError(() => of(null))),
2280
+ }).subscribe({
2281
+ next: (data) => {
2282
+ this.signingCert.set(data.signing);
2283
+ this.encryptionCert.set(data.encryption);
2284
+ },
2285
+ error: (err) => {
2286
+ console.error('Error loading certificates:', err);
2287
+ },
2288
+ });
2289
+ }
2290
+ copyToClipboard(value, key = 'metadata') {
2291
+ navigator.clipboard.writeText(value).then(() => {
2292
+ this.copied.set(key);
2293
+ setTimeout(() => this.copied.set(null), 2000);
2294
+ });
2295
+ }
2296
+ formatDate(dateStr) {
2297
+ if (!dateStr)
2298
+ return 'N/A';
2299
+ return new Date(dateStr).toLocaleDateString(undefined, {
2300
+ year: 'numeric',
2301
+ month: 'short',
2302
+ day: 'numeric',
2303
+ });
2304
+ }
2305
+ truncateThumbprint(thumbprint) {
2306
+ if (!thumbprint)
2307
+ return 'N/A';
2308
+ return thumbprint.substring(0, 16) + '...';
2309
+ }
2310
+ getSourceBadgeClass(source) {
2311
+ switch (source) {
2312
+ case 'Global':
2313
+ return 'bg-gray-100 text-gray-800';
2314
+ case 'Auto':
2315
+ return 'bg-blue-100 text-blue-800';
2316
+ case 'Uploaded':
2317
+ return 'bg-green-100 text-green-800';
2318
+ default:
2319
+ return 'bg-gray-100 text-gray-800';
2320
+ }
2321
+ }
2322
+ getSourceBadgeText(source) {
2323
+ switch (source) {
2324
+ case 'Auto':
2325
+ return 'Auto-generated';
2326
+ default:
2327
+ return source;
2328
+ }
2329
+ }
2330
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.25", ngImport: i0, type: SamlIdpSettingsTabComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
2331
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.25", type: SamlIdpSettingsTabComponent, isStandalone: true, selector: "saml-idp-settings-tab", ngImport: i0, template: `
2332
+ <div class="space-y-6">
2333
+ @if (error()) {
2334
+ <div class="bg-red-50 border border-red-200 rounded-lg p-4 flex items-center space-x-2">
2335
+ <ui-icon name="exclamation-circle" class="h-5 w-5 text-red-500" />
2336
+ <p class="text-red-700">{{ error() }}</p>
2337
+ </div>
2338
+ }
2339
+
2340
+ @if (success()) {
2341
+ <div class="bg-green-50 border border-green-200 rounded-lg p-4 flex items-center space-x-2">
2342
+ <ui-icon name="check-circle" class="h-5 w-5 text-green-500" />
2343
+ <p class="text-green-700">{{ success() }}</p>
2344
+ </div>
2345
+ }
2346
+
2347
+ @if (loading()) {
2348
+ <div class="space-y-6">
2349
+ <div class="h-8 w-64 bg-gray-200 rounded animate-pulse"></div>
2350
+ <div class="h-48 bg-gray-200 rounded-lg animate-pulse"></div>
2351
+ </div>
2352
+ } @else {
2353
+ <!-- SAML IdP Settings -->
2354
+ <div class="space-y-6">
2355
+ <div>
2356
+ <h3 class="text-lg font-medium text-gray-900">SAML IdP Configuration</h3>
2357
+ <p class="mt-1 text-sm text-gray-500">Enable and configure SAML Identity Provider functionality for this tenant</p>
2358
+ </div>
2359
+
2360
+ <!-- Enable Toggle -->
2361
+ <div class="flex items-center justify-between">
2362
+ <div>
2363
+ <p class="text-sm font-medium text-gray-700">Enable SAML IdP</p>
2364
+ <p class="text-sm text-gray-500">Allow external Service Providers to authenticate users via SAML</p>
2365
+ </div>
2366
+ <button
2367
+ type="button"
2368
+ (click)="toggleEnabled()"
2369
+ [class]="config.enabled ? 'bg-primary-600' : 'bg-gray-200'"
2370
+ class="relative inline-flex h-6 w-11 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none focus:ring-2 focus:ring-primary-500 focus:ring-offset-2"
2371
+ role="switch"
2372
+ [attr.aria-checked]="config.enabled"
2373
+ >
2374
+ <span
2375
+ [class]="config.enabled ? 'translate-x-5' : 'translate-x-0'"
2376
+ class="pointer-events-none inline-block h-5 w-5 transform rounded-full bg-white shadow ring-0 transition duration-200 ease-in-out"
2377
+ ></span>
2378
+ </button>
2379
+ </div>
2380
+
2381
+ <!-- Login Journey Name -->
2382
+ <div>
2383
+ <label for="loginJourneyName" class="block text-sm font-medium text-gray-700">Login Journey Name</label>
2384
+ <input
2385
+ type="text"
2386
+ id="loginJourneyName"
2387
+ [(ngModel)]="config.loginJourneyName"
2388
+ (ngModelChange)="onConfigChange()"
2389
+ placeholder="default-login"
2390
+ [disabled]="!config.enabled"
2391
+ class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-primary-500 focus:ring-primary-500 sm:text-sm disabled:bg-gray-100 disabled:text-gray-500"
2392
+ />
2393
+ <p class="mt-2 text-sm text-gray-500">
2394
+ Journey to use for SAML SSO authentication. Leave empty to use the standalone login page at
2395
+ <code class="text-xs bg-gray-100 px-1 py-0.5 rounded">/account/login</code>.
2396
+ </p>
2397
+ </div>
2398
+
2399
+ <!-- Info Box -->
2400
+ <div class="bg-blue-50 border border-blue-200 rounded-lg p-4">
2401
+ <div class="flex">
2402
+ <ui-icon name="information-circle" class="h-5 w-5 text-blue-500 flex-shrink-0" />
2403
+ <div class="ml-3">
2404
+ <h4 class="text-sm font-medium text-blue-800">How SAML IdP Authentication Works</h4>
2405
+ <p class="mt-1 text-sm text-blue-700">
2406
+ When a Service Provider sends an authentication request, users will be redirected to
2407
+ the configured login journey (or the standalone login page if not specified). After
2408
+ authentication, users are returned to the Service Provider with a SAML assertion.
2409
+ </p>
2410
+ </div>
2411
+ </div>
2412
+ </div>
2413
+ </div>
2414
+
2415
+ <!-- IdP Endpoints Info - Only show when enabled and info available -->
2416
+ @if (config.enabled && idpInfo()) {
2417
+ <div class="space-y-4">
2418
+ <div>
2419
+ <h3 class="text-lg font-medium text-gray-900">IdP Endpoints</h3>
2420
+ <p class="mt-1 text-sm text-gray-500">Share these URLs with Service Providers to configure SSO</p>
2421
+ </div>
2422
+ <div class="space-y-3">
2423
+ <div class="flex items-center justify-between">
2424
+ <div class="flex-1 min-w-0">
2425
+ <span class="text-sm font-medium text-gray-500">Metadata URL:</span>
2426
+ <code class="ml-2 text-sm text-gray-900 bg-gray-100 px-2 py-1 rounded">{{ idpInfo()?.metadataUrl }}</code>
2427
+ </div>
2428
+ <button (click)="copyToClipboard(idpInfo()?.metadataUrl || '', 'metadata')" class="ml-4 text-sm text-primary-600 hover:text-primary-700">
2429
+ {{ copied() === 'metadata' ? 'Copied!' : 'Copy' }}
2430
+ </button>
2431
+ </div>
2432
+ <div class="flex items-center justify-between">
2433
+ <div class="flex-1 min-w-0">
2434
+ <span class="text-sm font-medium text-gray-500">SSO Endpoint:</span>
2435
+ <code class="ml-2 text-sm text-gray-900 bg-gray-100 px-2 py-1 rounded">{{ idpInfo()?.singleSignOnServiceUrl }}</code>
2436
+ </div>
2437
+ <button (click)="copyToClipboard(idpInfo()?.singleSignOnServiceUrl || '', 'sso')" class="ml-4 text-sm text-primary-600 hover:text-primary-700">
2438
+ {{ copied() === 'sso' ? 'Copied!' : 'Copy' }}
2439
+ </button>
2440
+ </div>
2441
+ <div class="flex items-center justify-between">
2442
+ <div class="flex-1 min-w-0">
2443
+ <span class="text-sm font-medium text-gray-500">SLO Endpoint:</span>
2444
+ <code class="ml-2 text-sm text-gray-900 bg-gray-100 px-2 py-1 rounded">{{ idpInfo()?.singleLogoutServiceUrl }}</code>
2445
+ </div>
2446
+ <button (click)="copyToClipboard(idpInfo()?.singleLogoutServiceUrl || '', 'slo')" class="ml-4 text-sm text-primary-600 hover:text-primary-700">
2447
+ {{ copied() === 'slo' ? 'Copied!' : 'Copy' }}
2448
+ </button>
2449
+ </div>
2450
+ <div class="flex items-center justify-between">
2451
+ <div class="flex-1 min-w-0">
2452
+ <span class="text-sm font-medium text-gray-500">Entity ID:</span>
2453
+ <code class="ml-2 text-sm text-gray-900 bg-gray-100 px-2 py-1 rounded">{{ idpInfo()?.entityId }}</code>
2454
+ </div>
2455
+ <button (click)="copyToClipboard(idpInfo()?.entityId || '', 'entity')" class="ml-4 text-sm text-primary-600 hover:text-primary-700">
2456
+ {{ copied() === 'entity' ? 'Copied!' : 'Copy' }}
2457
+ </button>
2458
+ </div>
2459
+ </div>
2460
+ </div>
2461
+ }
2462
+
2463
+ <!-- Endpoints Preview - Show when enabled but IdP info not available -->
2464
+ @if (config.enabled && !idpInfo()) {
2465
+ <div class="space-y-4">
2466
+ <div>
2467
+ <h3 class="text-lg font-medium text-gray-900">IdP Endpoints</h3>
2468
+ <p class="mt-1 text-sm text-gray-500">These endpoints will be available once SAML IdP is enabled</p>
2469
+ </div>
2470
+ <div class="space-y-3">
2471
+ <div class="flex items-center justify-between">
2472
+ <div class="flex-1 min-w-0">
2473
+ <span class="text-sm font-medium text-gray-500">Metadata:</span>
2474
+ <code class="ml-2 text-sm text-gray-900 bg-gray-100 px-2 py-1 rounded">/saml/idp/metadata</code>
2475
+ </div>
2476
+ </div>
2477
+ <div class="flex items-center justify-between">
2478
+ <div class="flex-1 min-w-0">
2479
+ <span class="text-sm font-medium text-gray-500">SSO Endpoint:</span>
2480
+ <code class="ml-2 text-sm text-gray-900 bg-gray-100 px-2 py-1 rounded">/saml/idp/sso</code>
2481
+ </div>
2482
+ </div>
2483
+ <div class="flex items-center justify-between">
2484
+ <div class="flex-1 min-w-0">
2485
+ <span class="text-sm font-medium text-gray-500">SLO Endpoint:</span>
2486
+ <code class="ml-2 text-sm text-gray-900 bg-gray-100 px-2 py-1 rounded">/saml/idp/slo</code>
2487
+ </div>
2488
+ </div>
2489
+ </div>
2490
+ </div>
2491
+ }
2492
+
2493
+ <!-- IdP Certificates - Show when enabled -->
2494
+ @if (config.enabled) {
2495
+ <div class="space-y-4">
2496
+ <div>
2497
+ <h3 class="text-lg font-medium text-gray-900">IdP Certificates</h3>
2498
+ <p class="mt-1 text-sm text-gray-500">Configure certificates for signing SAML assertions and decrypting requests</p>
2499
+ </div>
2500
+
2501
+ <!-- Signing Certificate -->
2502
+ <div class="bg-gray-50 rounded-lg p-4 space-y-3">
2503
+ <div class="flex items-start justify-between">
2504
+ <div>
2505
+ <div class="flex items-center gap-2">
2506
+ <ui-icon name="shield-check" class="h-5 w-5 text-gray-400" />
2507
+ <h4 class="text-sm font-medium text-gray-900">Signing Certificate</h4>
2508
+ @if (signingCert()) {
2509
+ <span [class]="getSourceBadgeClass(signingCert()?.source || '')" class="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium">
2510
+ {{ getSourceBadgeText(signingCert()?.source || '') }}
2511
+ </span>
2512
+ }
2513
+ </div>
2514
+ <p class="text-sm text-gray-500 mt-1">Used to sign SAML assertions sent to Service Providers</p>
2515
+ </div>
2516
+ </div>
2517
+
2518
+ @if (signingCert()?.hasCertificate) {
2519
+ <div [class]="signingCert()?.isRevoked ? 'bg-red-50 rounded border border-red-300 p-3 space-y-2' : 'bg-white rounded border border-gray-200 p-3 space-y-2'">
2520
+ @if (signingCert()?.isRevoked) {
2521
+ <div class="flex items-center gap-2 text-red-700 bg-red-100 rounded px-2 py-1 mb-2">
2522
+ <ui-icon name="warning" class="h-4 w-4" />
2523
+ <span class="text-sm font-medium">Certificate Revoked - Please generate a new certificate</span>
2524
+ </div>
2525
+ }
2526
+ <div class="grid grid-cols-2 gap-x-4 gap-y-2 text-sm">
2527
+ <div>
2528
+ <span class="text-gray-500">Subject:</span>
2529
+ <span class="ml-2 text-gray-900 font-mono text-xs">{{ signingCert()?.subject || 'N/A' }}</span>
2530
+ </div>
2531
+ <div>
2532
+ <span class="text-gray-500">Thumbprint:</span>
2533
+ <span class="ml-2 text-gray-900 font-mono text-xs">{{ truncateThumbprint(signingCert()?.thumbprint) }}</span>
2534
+ </div>
2535
+ <div>
2536
+ <span class="text-gray-500">Valid From:</span>
2537
+ <span class="ml-2 text-gray-900">{{ formatDate(signingCert()?.notBefore) }}</span>
2538
+ </div>
2539
+ <div class="flex items-center gap-2">
2540
+ <span class="text-gray-500">Valid Until:</span>
2541
+ <span [class]="signingCert()?.isRevoked ? 'text-red-600' : signingCert()?.isExpired ? 'text-red-600' : signingCert()?.isExpiringSoon ? 'text-yellow-600' : 'text-gray-900'" class="ml-2">
2542
+ {{ formatDate(signingCert()?.notAfter) }}
2543
+ </span>
2544
+ @if (signingCert()?.isRevoked) {
2545
+ <span class="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-red-100 text-red-800">Revoked</span>
2546
+ }
2547
+ @if (!signingCert()?.isRevoked && signingCert()?.isExpired) {
2548
+ <ui-icon name="warning" class="h-4 w-4 text-red-500" />
2549
+ }
2550
+ @if (!signingCert()?.isRevoked && signingCert()?.isExpiringSoon && !signingCert()?.isExpired) {
2551
+ <ui-icon name="warning" class="h-4 w-4 text-yellow-500" />
2552
+ }
2553
+ </div>
2554
+ </div>
2555
+ </div>
2556
+ } @else {
2557
+ <div class="text-sm text-gray-500 italic">No certificate configured. Using global certificate.</div>
2558
+ }
2559
+
2560
+ <!-- Action buttons -->
2561
+ <div class="flex items-center gap-2 pt-2">
2562
+ <ui-button variant="secondary" size="sm" [disabled]="certLoading() === 'signing'" (clicked)="generateSigningCert()">
2563
+ @if (certLoading() === 'signing') {
2564
+ <svg class="h-4 w-4 mr-1.5 animate-spin" fill="none" viewBox="0 0 24 24" stroke="currentColor">
2565
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
2566
+ </svg>
2567
+ } @else {
2568
+ <ui-icon name="refresh" class="h-4 w-4 mr-1.5" />
2569
+ }
2570
+ Generate New
2571
+ </ui-button>
2572
+ <ui-button variant="secondary" size="sm" [disabled]="certLoading() === 'signing'" (clicked)="toggleSigningUpload()">
2573
+ <ui-icon name="arrow-up-tray" class="h-4 w-4 mr-1.5" />
2574
+ Upload PFX
2575
+ </ui-button>
2576
+ @if (signingCert()?.source !== 'Global') {
2577
+ <ui-button variant="secondary" size="sm" [disabled]="certLoading() === 'signing'" (clicked)="resetSigningCert()">
2578
+ Reset to Global
2579
+ </ui-button>
2580
+ }
2581
+ </div>
2582
+
2583
+ <!-- Upload form for signing cert -->
2584
+ @if (showSigningUpload()) {
2585
+ <div class="bg-white rounded border border-gray-200 p-3 space-y-3">
2586
+ <div>
2587
+ <label class="block text-sm font-medium text-gray-700">PFX Password (optional)</label>
2588
+ <input
2589
+ type="password"
2590
+ [(ngModel)]="signingPassword"
2591
+ placeholder="Enter password if PFX is protected"
2592
+ class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-primary-500 focus:ring-primary-500 sm:text-sm"
2593
+ />
2594
+ </div>
2595
+ <div>
2596
+ <label class="block text-sm font-medium text-gray-700">Select PFX File</label>
2597
+ <input
2598
+ type="file"
2599
+ accept=".pfx,.p12"
2600
+ (change)="onSigningFileSelect($event)"
2601
+ [disabled]="certLoading() === 'signing'"
2602
+ class="mt-1 block w-full text-sm text-gray-500 file:mr-4 file:py-2 file:px-4 file:rounded-md file:border-0 file:text-sm file:font-medium file:bg-primary-50 file:text-primary-700 hover:file:bg-primary-100"
2603
+ />
2604
+ </div>
2605
+ <button
2606
+ type="button"
2607
+ (click)="toggleSigningUpload()"
2608
+ class="text-sm text-gray-500 hover:text-gray-700"
2609
+ >
2610
+ Cancel
2611
+ </button>
2612
+ </div>
2613
+ }
2614
+ </div>
2615
+
2616
+ <!-- Encryption Certificate -->
2617
+ <div class="bg-gray-50 rounded-lg p-4 space-y-3">
2618
+ <div class="flex items-start justify-between">
2619
+ <div>
2620
+ <div class="flex items-center gap-2">
2621
+ <ui-icon name="shield-check" class="h-5 w-5 text-gray-400" />
2622
+ <h4 class="text-sm font-medium text-gray-900">Encryption Certificate</h4>
2623
+ @if (encryptionCert()) {
2624
+ <span [class]="getSourceBadgeClass(encryptionCert()?.source || '')" class="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium">
2625
+ {{ getSourceBadgeText(encryptionCert()?.source || '') }}
2626
+ </span>
2627
+ }
2628
+ </div>
2629
+ <p class="text-sm text-gray-500 mt-1">Used to decrypt incoming encrypted SAML requests (optional)</p>
2630
+ </div>
2631
+ </div>
2632
+
2633
+ @if (encryptionCert()?.hasCertificate) {
2634
+ <div [class]="encryptionCert()?.isRevoked ? 'bg-red-50 rounded border border-red-300 p-3 space-y-2' : 'bg-white rounded border border-gray-200 p-3 space-y-2'">
2635
+ @if (encryptionCert()?.isRevoked) {
2636
+ <div class="flex items-center gap-2 text-red-700 bg-red-100 rounded px-2 py-1 mb-2">
2637
+ <ui-icon name="warning" class="h-4 w-4" />
2638
+ <span class="text-sm font-medium">Certificate Revoked - Please generate a new certificate</span>
2639
+ </div>
2640
+ }
2641
+ <div class="grid grid-cols-2 gap-x-4 gap-y-2 text-sm">
2642
+ <div>
2643
+ <span class="text-gray-500">Subject:</span>
2644
+ <span class="ml-2 text-gray-900 font-mono text-xs">{{ encryptionCert()?.subject || 'N/A' }}</span>
2645
+ </div>
2646
+ <div>
2647
+ <span class="text-gray-500">Thumbprint:</span>
2648
+ <span class="ml-2 text-gray-900 font-mono text-xs">{{ truncateThumbprint(encryptionCert()?.thumbprint) }}</span>
2649
+ </div>
2650
+ <div>
2651
+ <span class="text-gray-500">Valid From:</span>
2652
+ <span class="ml-2 text-gray-900">{{ formatDate(encryptionCert()?.notBefore) }}</span>
2653
+ </div>
2654
+ <div class="flex items-center gap-2">
2655
+ <span class="text-gray-500">Valid Until:</span>
2656
+ <span [class]="encryptionCert()?.isRevoked ? 'text-red-600' : encryptionCert()?.isExpired ? 'text-red-600' : encryptionCert()?.isExpiringSoon ? 'text-yellow-600' : 'text-gray-900'" class="ml-2">
2657
+ {{ formatDate(encryptionCert()?.notAfter) }}
2658
+ </span>
2659
+ @if (encryptionCert()?.isRevoked) {
2660
+ <span class="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-red-100 text-red-800">Revoked</span>
2661
+ }
2662
+ @if (!encryptionCert()?.isRevoked && encryptionCert()?.isExpired) {
2663
+ <ui-icon name="warning" class="h-4 w-4 text-red-500" />
2664
+ }
2665
+ @if (!encryptionCert()?.isRevoked && encryptionCert()?.isExpiringSoon && !encryptionCert()?.isExpired) {
2666
+ <ui-icon name="warning" class="h-4 w-4 text-yellow-500" />
2667
+ }
2668
+ </div>
2669
+ </div>
2670
+ </div>
2671
+ } @else {
2672
+ <div class="text-sm text-gray-500 italic">No certificate configured. Using global certificate.</div>
2673
+ }
2674
+
2675
+ <!-- Action buttons -->
2676
+ <div class="flex items-center gap-2 pt-2">
2677
+ <ui-button variant="secondary" size="sm" [disabled]="certLoading() === 'encryption'" (clicked)="generateEncryptionCert()">
2678
+ @if (certLoading() === 'encryption') {
2679
+ <svg class="h-4 w-4 mr-1.5 animate-spin" fill="none" viewBox="0 0 24 24" stroke="currentColor">
2680
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
2681
+ </svg>
2682
+ } @else {
2683
+ <ui-icon name="refresh" class="h-4 w-4 mr-1.5" />
2684
+ }
2685
+ Generate New
2686
+ </ui-button>
2687
+ <ui-button variant="secondary" size="sm" [disabled]="certLoading() === 'encryption'" (clicked)="toggleEncryptionUpload()">
2688
+ <ui-icon name="arrow-up-tray" class="h-4 w-4 mr-1.5" />
2689
+ Upload PFX
2690
+ </ui-button>
2691
+ @if (encryptionCert()?.source !== 'Global') {
2692
+ <ui-button variant="secondary" size="sm" [disabled]="certLoading() === 'encryption'" (clicked)="resetEncryptionCert()">
2693
+ Reset to Global
2694
+ </ui-button>
2695
+ }
2696
+ </div>
2697
+
2698
+ <!-- Upload form for encryption cert -->
2699
+ @if (showEncryptionUpload()) {
2700
+ <div class="bg-white rounded border border-gray-200 p-3 space-y-3">
2701
+ <div>
2702
+ <label class="block text-sm font-medium text-gray-700">PFX Password (optional)</label>
2703
+ <input
2704
+ type="password"
2705
+ [(ngModel)]="encryptionPassword"
2706
+ placeholder="Enter password if PFX is protected"
2707
+ class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-primary-500 focus:ring-primary-500 sm:text-sm"
2708
+ />
2709
+ </div>
2710
+ <div>
2711
+ <label class="block text-sm font-medium text-gray-700">Select PFX File</label>
2712
+ <input
2713
+ type="file"
2714
+ accept=".pfx,.p12"
2715
+ (change)="onEncryptionFileSelect($event)"
2716
+ [disabled]="certLoading() === 'encryption'"
2717
+ class="mt-1 block w-full text-sm text-gray-500 file:mr-4 file:py-2 file:px-4 file:rounded-md file:border-0 file:text-sm file:font-medium file:bg-primary-50 file:text-primary-700 hover:file:bg-primary-100"
2718
+ />
2719
+ </div>
2720
+ <button
2721
+ type="button"
2722
+ (click)="toggleEncryptionUpload()"
2723
+ class="text-sm text-gray-500 hover:text-gray-700"
2724
+ >
2725
+ Cancel
2726
+ </button>
2727
+ </div>
2728
+ }
2729
+ </div>
2730
+ </div>
2731
+ }
2732
+
2733
+ <!-- Save Button -->
2734
+ <div class="flex justify-end items-center gap-4 pt-4 border-t border-gray-200">
2735
+ @if (hasChanges()) {
2736
+ <span class="text-sm text-gray-500">You have unsaved changes</span>
2737
+ }
2738
+ <ui-button [disabled]="!hasChanges() || saving()" (clicked)="handleSave()">
2739
+ {{ saving() ? 'Saving...' : 'Save Settings' }}
2740
+ </ui-button>
2741
+ </div>
2742
+ }
2743
+ </div>
2744
+ `, isInline: true, dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "component", type: ButtonComponent, selector: "ui-button", inputs: ["variant", "size", "disabled", "loading", "type", "fullWidth"], outputs: ["clicked"] }, { kind: "component", type: IconComponent, selector: "ui-icon", inputs: ["name", "strokeWidth"] }] });
2745
+ }
2746
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.25", ngImport: i0, type: SamlIdpSettingsTabComponent, decorators: [{
2747
+ type: Component,
2748
+ args: [{
2749
+ selector: 'saml-idp-settings-tab',
2750
+ standalone: true,
2751
+ imports: [CommonModule, FormsModule, ButtonComponent, IconComponent],
2752
+ template: `
2753
+ <div class="space-y-6">
2754
+ @if (error()) {
2755
+ <div class="bg-red-50 border border-red-200 rounded-lg p-4 flex items-center space-x-2">
2756
+ <ui-icon name="exclamation-circle" class="h-5 w-5 text-red-500" />
2757
+ <p class="text-red-700">{{ error() }}</p>
2758
+ </div>
2759
+ }
2760
+
2761
+ @if (success()) {
2762
+ <div class="bg-green-50 border border-green-200 rounded-lg p-4 flex items-center space-x-2">
2763
+ <ui-icon name="check-circle" class="h-5 w-5 text-green-500" />
2764
+ <p class="text-green-700">{{ success() }}</p>
2765
+ </div>
2766
+ }
2767
+
2768
+ @if (loading()) {
2769
+ <div class="space-y-6">
2770
+ <div class="h-8 w-64 bg-gray-200 rounded animate-pulse"></div>
2771
+ <div class="h-48 bg-gray-200 rounded-lg animate-pulse"></div>
2772
+ </div>
2773
+ } @else {
2774
+ <!-- SAML IdP Settings -->
2775
+ <div class="space-y-6">
2776
+ <div>
2777
+ <h3 class="text-lg font-medium text-gray-900">SAML IdP Configuration</h3>
2778
+ <p class="mt-1 text-sm text-gray-500">Enable and configure SAML Identity Provider functionality for this tenant</p>
2779
+ </div>
2780
+
2781
+ <!-- Enable Toggle -->
2782
+ <div class="flex items-center justify-between">
2783
+ <div>
2784
+ <p class="text-sm font-medium text-gray-700">Enable SAML IdP</p>
2785
+ <p class="text-sm text-gray-500">Allow external Service Providers to authenticate users via SAML</p>
2786
+ </div>
2787
+ <button
2788
+ type="button"
2789
+ (click)="toggleEnabled()"
2790
+ [class]="config.enabled ? 'bg-primary-600' : 'bg-gray-200'"
2791
+ class="relative inline-flex h-6 w-11 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none focus:ring-2 focus:ring-primary-500 focus:ring-offset-2"
2792
+ role="switch"
2793
+ [attr.aria-checked]="config.enabled"
2794
+ >
2795
+ <span
2796
+ [class]="config.enabled ? 'translate-x-5' : 'translate-x-0'"
2797
+ class="pointer-events-none inline-block h-5 w-5 transform rounded-full bg-white shadow ring-0 transition duration-200 ease-in-out"
2798
+ ></span>
2799
+ </button>
2800
+ </div>
2801
+
2802
+ <!-- Login Journey Name -->
2803
+ <div>
2804
+ <label for="loginJourneyName" class="block text-sm font-medium text-gray-700">Login Journey Name</label>
2805
+ <input
2806
+ type="text"
2807
+ id="loginJourneyName"
2808
+ [(ngModel)]="config.loginJourneyName"
2809
+ (ngModelChange)="onConfigChange()"
2810
+ placeholder="default-login"
2811
+ [disabled]="!config.enabled"
2812
+ class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-primary-500 focus:ring-primary-500 sm:text-sm disabled:bg-gray-100 disabled:text-gray-500"
2813
+ />
2814
+ <p class="mt-2 text-sm text-gray-500">
2815
+ Journey to use for SAML SSO authentication. Leave empty to use the standalone login page at
2816
+ <code class="text-xs bg-gray-100 px-1 py-0.5 rounded">/account/login</code>.
2817
+ </p>
2818
+ </div>
2819
+
2820
+ <!-- Info Box -->
2821
+ <div class="bg-blue-50 border border-blue-200 rounded-lg p-4">
2822
+ <div class="flex">
2823
+ <ui-icon name="information-circle" class="h-5 w-5 text-blue-500 flex-shrink-0" />
2824
+ <div class="ml-3">
2825
+ <h4 class="text-sm font-medium text-blue-800">How SAML IdP Authentication Works</h4>
2826
+ <p class="mt-1 text-sm text-blue-700">
2827
+ When a Service Provider sends an authentication request, users will be redirected to
2828
+ the configured login journey (or the standalone login page if not specified). After
2829
+ authentication, users are returned to the Service Provider with a SAML assertion.
2830
+ </p>
2831
+ </div>
2832
+ </div>
2833
+ </div>
2834
+ </div>
2835
+
2836
+ <!-- IdP Endpoints Info - Only show when enabled and info available -->
2837
+ @if (config.enabled && idpInfo()) {
2838
+ <div class="space-y-4">
2839
+ <div>
2840
+ <h3 class="text-lg font-medium text-gray-900">IdP Endpoints</h3>
2841
+ <p class="mt-1 text-sm text-gray-500">Share these URLs with Service Providers to configure SSO</p>
2842
+ </div>
2843
+ <div class="space-y-3">
2844
+ <div class="flex items-center justify-between">
2845
+ <div class="flex-1 min-w-0">
2846
+ <span class="text-sm font-medium text-gray-500">Metadata URL:</span>
2847
+ <code class="ml-2 text-sm text-gray-900 bg-gray-100 px-2 py-1 rounded">{{ idpInfo()?.metadataUrl }}</code>
2848
+ </div>
2849
+ <button (click)="copyToClipboard(idpInfo()?.metadataUrl || '', 'metadata')" class="ml-4 text-sm text-primary-600 hover:text-primary-700">
2850
+ {{ copied() === 'metadata' ? 'Copied!' : 'Copy' }}
2851
+ </button>
2852
+ </div>
2853
+ <div class="flex items-center justify-between">
2854
+ <div class="flex-1 min-w-0">
2855
+ <span class="text-sm font-medium text-gray-500">SSO Endpoint:</span>
2856
+ <code class="ml-2 text-sm text-gray-900 bg-gray-100 px-2 py-1 rounded">{{ idpInfo()?.singleSignOnServiceUrl }}</code>
2857
+ </div>
2858
+ <button (click)="copyToClipboard(idpInfo()?.singleSignOnServiceUrl || '', 'sso')" class="ml-4 text-sm text-primary-600 hover:text-primary-700">
2859
+ {{ copied() === 'sso' ? 'Copied!' : 'Copy' }}
2860
+ </button>
2861
+ </div>
2862
+ <div class="flex items-center justify-between">
2863
+ <div class="flex-1 min-w-0">
2864
+ <span class="text-sm font-medium text-gray-500">SLO Endpoint:</span>
2865
+ <code class="ml-2 text-sm text-gray-900 bg-gray-100 px-2 py-1 rounded">{{ idpInfo()?.singleLogoutServiceUrl }}</code>
2866
+ </div>
2867
+ <button (click)="copyToClipboard(idpInfo()?.singleLogoutServiceUrl || '', 'slo')" class="ml-4 text-sm text-primary-600 hover:text-primary-700">
2868
+ {{ copied() === 'slo' ? 'Copied!' : 'Copy' }}
2869
+ </button>
2870
+ </div>
2871
+ <div class="flex items-center justify-between">
2872
+ <div class="flex-1 min-w-0">
2873
+ <span class="text-sm font-medium text-gray-500">Entity ID:</span>
2874
+ <code class="ml-2 text-sm text-gray-900 bg-gray-100 px-2 py-1 rounded">{{ idpInfo()?.entityId }}</code>
2875
+ </div>
2876
+ <button (click)="copyToClipboard(idpInfo()?.entityId || '', 'entity')" class="ml-4 text-sm text-primary-600 hover:text-primary-700">
2877
+ {{ copied() === 'entity' ? 'Copied!' : 'Copy' }}
2878
+ </button>
2879
+ </div>
2880
+ </div>
2881
+ </div>
2882
+ }
2883
+
2884
+ <!-- Endpoints Preview - Show when enabled but IdP info not available -->
2885
+ @if (config.enabled && !idpInfo()) {
2886
+ <div class="space-y-4">
2887
+ <div>
2888
+ <h3 class="text-lg font-medium text-gray-900">IdP Endpoints</h3>
2889
+ <p class="mt-1 text-sm text-gray-500">These endpoints will be available once SAML IdP is enabled</p>
2890
+ </div>
2891
+ <div class="space-y-3">
2892
+ <div class="flex items-center justify-between">
2893
+ <div class="flex-1 min-w-0">
2894
+ <span class="text-sm font-medium text-gray-500">Metadata:</span>
2895
+ <code class="ml-2 text-sm text-gray-900 bg-gray-100 px-2 py-1 rounded">/saml/idp/metadata</code>
2896
+ </div>
2897
+ </div>
2898
+ <div class="flex items-center justify-between">
2899
+ <div class="flex-1 min-w-0">
2900
+ <span class="text-sm font-medium text-gray-500">SSO Endpoint:</span>
2901
+ <code class="ml-2 text-sm text-gray-900 bg-gray-100 px-2 py-1 rounded">/saml/idp/sso</code>
2902
+ </div>
2903
+ </div>
2904
+ <div class="flex items-center justify-between">
2905
+ <div class="flex-1 min-w-0">
2906
+ <span class="text-sm font-medium text-gray-500">SLO Endpoint:</span>
2907
+ <code class="ml-2 text-sm text-gray-900 bg-gray-100 px-2 py-1 rounded">/saml/idp/slo</code>
2908
+ </div>
2909
+ </div>
2910
+ </div>
2911
+ </div>
2912
+ }
2913
+
2914
+ <!-- IdP Certificates - Show when enabled -->
2915
+ @if (config.enabled) {
2916
+ <div class="space-y-4">
2917
+ <div>
2918
+ <h3 class="text-lg font-medium text-gray-900">IdP Certificates</h3>
2919
+ <p class="mt-1 text-sm text-gray-500">Configure certificates for signing SAML assertions and decrypting requests</p>
2920
+ </div>
2921
+
2922
+ <!-- Signing Certificate -->
2923
+ <div class="bg-gray-50 rounded-lg p-4 space-y-3">
2924
+ <div class="flex items-start justify-between">
2925
+ <div>
2926
+ <div class="flex items-center gap-2">
2927
+ <ui-icon name="shield-check" class="h-5 w-5 text-gray-400" />
2928
+ <h4 class="text-sm font-medium text-gray-900">Signing Certificate</h4>
2929
+ @if (signingCert()) {
2930
+ <span [class]="getSourceBadgeClass(signingCert()?.source || '')" class="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium">
2931
+ {{ getSourceBadgeText(signingCert()?.source || '') }}
2932
+ </span>
2933
+ }
2934
+ </div>
2935
+ <p class="text-sm text-gray-500 mt-1">Used to sign SAML assertions sent to Service Providers</p>
2936
+ </div>
2937
+ </div>
2938
+
2939
+ @if (signingCert()?.hasCertificate) {
2940
+ <div [class]="signingCert()?.isRevoked ? 'bg-red-50 rounded border border-red-300 p-3 space-y-2' : 'bg-white rounded border border-gray-200 p-3 space-y-2'">
2941
+ @if (signingCert()?.isRevoked) {
2942
+ <div class="flex items-center gap-2 text-red-700 bg-red-100 rounded px-2 py-1 mb-2">
2943
+ <ui-icon name="warning" class="h-4 w-4" />
2944
+ <span class="text-sm font-medium">Certificate Revoked - Please generate a new certificate</span>
2945
+ </div>
2946
+ }
2947
+ <div class="grid grid-cols-2 gap-x-4 gap-y-2 text-sm">
2948
+ <div>
2949
+ <span class="text-gray-500">Subject:</span>
2950
+ <span class="ml-2 text-gray-900 font-mono text-xs">{{ signingCert()?.subject || 'N/A' }}</span>
2951
+ </div>
2952
+ <div>
2953
+ <span class="text-gray-500">Thumbprint:</span>
2954
+ <span class="ml-2 text-gray-900 font-mono text-xs">{{ truncateThumbprint(signingCert()?.thumbprint) }}</span>
2955
+ </div>
2956
+ <div>
2957
+ <span class="text-gray-500">Valid From:</span>
2958
+ <span class="ml-2 text-gray-900">{{ formatDate(signingCert()?.notBefore) }}</span>
2959
+ </div>
2960
+ <div class="flex items-center gap-2">
2961
+ <span class="text-gray-500">Valid Until:</span>
2962
+ <span [class]="signingCert()?.isRevoked ? 'text-red-600' : signingCert()?.isExpired ? 'text-red-600' : signingCert()?.isExpiringSoon ? 'text-yellow-600' : 'text-gray-900'" class="ml-2">
2963
+ {{ formatDate(signingCert()?.notAfter) }}
2964
+ </span>
2965
+ @if (signingCert()?.isRevoked) {
2966
+ <span class="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-red-100 text-red-800">Revoked</span>
2967
+ }
2968
+ @if (!signingCert()?.isRevoked && signingCert()?.isExpired) {
2969
+ <ui-icon name="warning" class="h-4 w-4 text-red-500" />
2970
+ }
2971
+ @if (!signingCert()?.isRevoked && signingCert()?.isExpiringSoon && !signingCert()?.isExpired) {
2972
+ <ui-icon name="warning" class="h-4 w-4 text-yellow-500" />
2973
+ }
2974
+ </div>
2975
+ </div>
2976
+ </div>
2977
+ } @else {
2978
+ <div class="text-sm text-gray-500 italic">No certificate configured. Using global certificate.</div>
2979
+ }
2980
+
2981
+ <!-- Action buttons -->
2982
+ <div class="flex items-center gap-2 pt-2">
2983
+ <ui-button variant="secondary" size="sm" [disabled]="certLoading() === 'signing'" (clicked)="generateSigningCert()">
2984
+ @if (certLoading() === 'signing') {
2985
+ <svg class="h-4 w-4 mr-1.5 animate-spin" fill="none" viewBox="0 0 24 24" stroke="currentColor">
2986
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
2987
+ </svg>
2988
+ } @else {
2989
+ <ui-icon name="refresh" class="h-4 w-4 mr-1.5" />
2990
+ }
2991
+ Generate New
2992
+ </ui-button>
2993
+ <ui-button variant="secondary" size="sm" [disabled]="certLoading() === 'signing'" (clicked)="toggleSigningUpload()">
2994
+ <ui-icon name="arrow-up-tray" class="h-4 w-4 mr-1.5" />
2995
+ Upload PFX
2996
+ </ui-button>
2997
+ @if (signingCert()?.source !== 'Global') {
2998
+ <ui-button variant="secondary" size="sm" [disabled]="certLoading() === 'signing'" (clicked)="resetSigningCert()">
2999
+ Reset to Global
3000
+ </ui-button>
3001
+ }
3002
+ </div>
3003
+
3004
+ <!-- Upload form for signing cert -->
3005
+ @if (showSigningUpload()) {
3006
+ <div class="bg-white rounded border border-gray-200 p-3 space-y-3">
3007
+ <div>
3008
+ <label class="block text-sm font-medium text-gray-700">PFX Password (optional)</label>
3009
+ <input
3010
+ type="password"
3011
+ [(ngModel)]="signingPassword"
3012
+ placeholder="Enter password if PFX is protected"
3013
+ class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-primary-500 focus:ring-primary-500 sm:text-sm"
3014
+ />
3015
+ </div>
3016
+ <div>
3017
+ <label class="block text-sm font-medium text-gray-700">Select PFX File</label>
3018
+ <input
3019
+ type="file"
3020
+ accept=".pfx,.p12"
3021
+ (change)="onSigningFileSelect($event)"
3022
+ [disabled]="certLoading() === 'signing'"
3023
+ class="mt-1 block w-full text-sm text-gray-500 file:mr-4 file:py-2 file:px-4 file:rounded-md file:border-0 file:text-sm file:font-medium file:bg-primary-50 file:text-primary-700 hover:file:bg-primary-100"
3024
+ />
3025
+ </div>
3026
+ <button
3027
+ type="button"
3028
+ (click)="toggleSigningUpload()"
3029
+ class="text-sm text-gray-500 hover:text-gray-700"
3030
+ >
3031
+ Cancel
3032
+ </button>
3033
+ </div>
3034
+ }
3035
+ </div>
3036
+
3037
+ <!-- Encryption Certificate -->
3038
+ <div class="bg-gray-50 rounded-lg p-4 space-y-3">
3039
+ <div class="flex items-start justify-between">
3040
+ <div>
3041
+ <div class="flex items-center gap-2">
3042
+ <ui-icon name="shield-check" class="h-5 w-5 text-gray-400" />
3043
+ <h4 class="text-sm font-medium text-gray-900">Encryption Certificate</h4>
3044
+ @if (encryptionCert()) {
3045
+ <span [class]="getSourceBadgeClass(encryptionCert()?.source || '')" class="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium">
3046
+ {{ getSourceBadgeText(encryptionCert()?.source || '') }}
3047
+ </span>
3048
+ }
3049
+ </div>
3050
+ <p class="text-sm text-gray-500 mt-1">Used to decrypt incoming encrypted SAML requests (optional)</p>
3051
+ </div>
3052
+ </div>
3053
+
3054
+ @if (encryptionCert()?.hasCertificate) {
3055
+ <div [class]="encryptionCert()?.isRevoked ? 'bg-red-50 rounded border border-red-300 p-3 space-y-2' : 'bg-white rounded border border-gray-200 p-3 space-y-2'">
3056
+ @if (encryptionCert()?.isRevoked) {
3057
+ <div class="flex items-center gap-2 text-red-700 bg-red-100 rounded px-2 py-1 mb-2">
3058
+ <ui-icon name="warning" class="h-4 w-4" />
3059
+ <span class="text-sm font-medium">Certificate Revoked - Please generate a new certificate</span>
3060
+ </div>
3061
+ }
3062
+ <div class="grid grid-cols-2 gap-x-4 gap-y-2 text-sm">
3063
+ <div>
3064
+ <span class="text-gray-500">Subject:</span>
3065
+ <span class="ml-2 text-gray-900 font-mono text-xs">{{ encryptionCert()?.subject || 'N/A' }}</span>
3066
+ </div>
3067
+ <div>
3068
+ <span class="text-gray-500">Thumbprint:</span>
3069
+ <span class="ml-2 text-gray-900 font-mono text-xs">{{ truncateThumbprint(encryptionCert()?.thumbprint) }}</span>
3070
+ </div>
3071
+ <div>
3072
+ <span class="text-gray-500">Valid From:</span>
3073
+ <span class="ml-2 text-gray-900">{{ formatDate(encryptionCert()?.notBefore) }}</span>
3074
+ </div>
3075
+ <div class="flex items-center gap-2">
3076
+ <span class="text-gray-500">Valid Until:</span>
3077
+ <span [class]="encryptionCert()?.isRevoked ? 'text-red-600' : encryptionCert()?.isExpired ? 'text-red-600' : encryptionCert()?.isExpiringSoon ? 'text-yellow-600' : 'text-gray-900'" class="ml-2">
3078
+ {{ formatDate(encryptionCert()?.notAfter) }}
3079
+ </span>
3080
+ @if (encryptionCert()?.isRevoked) {
3081
+ <span class="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-red-100 text-red-800">Revoked</span>
3082
+ }
3083
+ @if (!encryptionCert()?.isRevoked && encryptionCert()?.isExpired) {
3084
+ <ui-icon name="warning" class="h-4 w-4 text-red-500" />
3085
+ }
3086
+ @if (!encryptionCert()?.isRevoked && encryptionCert()?.isExpiringSoon && !encryptionCert()?.isExpired) {
3087
+ <ui-icon name="warning" class="h-4 w-4 text-yellow-500" />
3088
+ }
3089
+ </div>
3090
+ </div>
3091
+ </div>
3092
+ } @else {
3093
+ <div class="text-sm text-gray-500 italic">No certificate configured. Using global certificate.</div>
3094
+ }
3095
+
3096
+ <!-- Action buttons -->
3097
+ <div class="flex items-center gap-2 pt-2">
3098
+ <ui-button variant="secondary" size="sm" [disabled]="certLoading() === 'encryption'" (clicked)="generateEncryptionCert()">
3099
+ @if (certLoading() === 'encryption') {
3100
+ <svg class="h-4 w-4 mr-1.5 animate-spin" fill="none" viewBox="0 0 24 24" stroke="currentColor">
3101
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
3102
+ </svg>
3103
+ } @else {
3104
+ <ui-icon name="refresh" class="h-4 w-4 mr-1.5" />
3105
+ }
3106
+ Generate New
3107
+ </ui-button>
3108
+ <ui-button variant="secondary" size="sm" [disabled]="certLoading() === 'encryption'" (clicked)="toggleEncryptionUpload()">
3109
+ <ui-icon name="arrow-up-tray" class="h-4 w-4 mr-1.5" />
3110
+ Upload PFX
3111
+ </ui-button>
3112
+ @if (encryptionCert()?.source !== 'Global') {
3113
+ <ui-button variant="secondary" size="sm" [disabled]="certLoading() === 'encryption'" (clicked)="resetEncryptionCert()">
3114
+ Reset to Global
3115
+ </ui-button>
3116
+ }
3117
+ </div>
3118
+
3119
+ <!-- Upload form for encryption cert -->
3120
+ @if (showEncryptionUpload()) {
3121
+ <div class="bg-white rounded border border-gray-200 p-3 space-y-3">
3122
+ <div>
3123
+ <label class="block text-sm font-medium text-gray-700">PFX Password (optional)</label>
3124
+ <input
3125
+ type="password"
3126
+ [(ngModel)]="encryptionPassword"
3127
+ placeholder="Enter password if PFX is protected"
3128
+ class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-primary-500 focus:ring-primary-500 sm:text-sm"
3129
+ />
3130
+ </div>
3131
+ <div>
3132
+ <label class="block text-sm font-medium text-gray-700">Select PFX File</label>
3133
+ <input
3134
+ type="file"
3135
+ accept=".pfx,.p12"
3136
+ (change)="onEncryptionFileSelect($event)"
3137
+ [disabled]="certLoading() === 'encryption'"
3138
+ class="mt-1 block w-full text-sm text-gray-500 file:mr-4 file:py-2 file:px-4 file:rounded-md file:border-0 file:text-sm file:font-medium file:bg-primary-50 file:text-primary-700 hover:file:bg-primary-100"
3139
+ />
3140
+ </div>
3141
+ <button
3142
+ type="button"
3143
+ (click)="toggleEncryptionUpload()"
3144
+ class="text-sm text-gray-500 hover:text-gray-700"
3145
+ >
3146
+ Cancel
3147
+ </button>
3148
+ </div>
3149
+ }
3150
+ </div>
3151
+ </div>
3152
+ }
3153
+
3154
+ <!-- Save Button -->
3155
+ <div class="flex justify-end items-center gap-4 pt-4 border-t border-gray-200">
3156
+ @if (hasChanges()) {
3157
+ <span class="text-sm text-gray-500">You have unsaved changes</span>
3158
+ }
3159
+ <ui-button [disabled]="!hasChanges() || saving()" (clicked)="handleSave()">
3160
+ {{ saving() ? 'Saving...' : 'Save Settings' }}
3161
+ </ui-button>
3162
+ </div>
3163
+ }
3164
+ </div>
3165
+ `,
3166
+ }]
3167
+ }] });
3168
+
3169
+ const SAML_ROUTES = [
3170
+ {
3171
+ path: '',
3172
+ redirectTo: 'dashboard',
3173
+ pathMatch: 'full',
3174
+ },
3175
+ {
3176
+ path: 'dashboard',
3177
+ component: SamlDashboardComponent,
3178
+ },
3179
+ {
3180
+ path: 'service-providers',
3181
+ component: ServiceProvidersPageComponent,
3182
+ },
3183
+ {
3184
+ path: 'service-providers/new',
3185
+ component: ServiceProviderDetailsPageComponent,
3186
+ },
3187
+ {
3188
+ path: 'service-providers/:id',
3189
+ component: ServiceProviderDetailsPageComponent,
3190
+ },
3191
+ ];
3192
+
3193
+ /**
3194
+ * SAML Admin Plugin
3195
+ *
3196
+ * Provides SAML Identity Provider management functionality:
3197
+ * - Dashboard with IdP info and stats
3198
+ * - Service Provider management (CRUD)
3199
+ * - Settings tab for IdP configuration
3200
+ */
3201
+ const SAML_PLUGIN = {
3202
+ name: 'oluso-saml',
3203
+ version: '1.0.0',
3204
+ // Plugin routes - will be automatically added under /saml path
3205
+ routes: [
3206
+ {
3207
+ path: 'saml',
3208
+ children: SAML_ROUTES,
3209
+ },
3210
+ ],
3211
+ navItems: [
3212
+ {
3213
+ id: 'saml',
3214
+ label: 'SAML',
3215
+ path: '/saml',
3216
+ icon: 'shield-check',
3217
+ order: 10,
3218
+ mergeIntoPath: '/identity-providers',
3219
+ children: [
3220
+ { label: 'SAML Dashboard', path: '/saml/dashboard', icon: 'shield-check', permission: 'saml.read', order: 10 },
3221
+ { label: 'Service Providers', path: '/saml/service-providers', icon: 'shield-check', permission: 'saml.read', order: 20 },
3222
+ ],
3223
+ },
3224
+ ],
3225
+ settingsTabs: [
3226
+ {
3227
+ id: 'saml-idp',
3228
+ label: 'SAML IdP',
3229
+ icon: 'shield-check',
3230
+ component: SamlIdpSettingsTabComponent,
3231
+ order: 400,
3232
+ },
3233
+ ],
3234
+ };
3235
+ /**
3236
+ * Helper function to get SAML navigation items
3237
+ */
3238
+ function getSamlNavItems() {
3239
+ return SAML_PLUGIN.navItems ?? [];
3240
+ }
3241
+
3242
+ // Models
3243
+
3244
+ /**
3245
+ * Generated bundle index. Do not edit.
3246
+ */
3247
+
3248
+ export { NAME_ID_FORMATS, SAML_PLUGIN, SAML_ROUTES, SSO_BINDINGS, SamlDashboardComponent, SamlIdpSettingsTabComponent, SamlService, ServiceProviderDetailsPageComponent, ServiceProvidersPageComponent, getSamlNavItems };
3249
+ //# sourceMappingURL=syndew-oluso-plugin-saml.mjs.map