feat(console): guide users when configuring IDPs (#7572)

* feat(console): show external idp remote config

* reuse copy-row

* finish google and saml sp

* finish apps

* add next steps modal

* rollout

* activate

* fix saml urls

* fix saml provider

* complete providers

* translate

* update google docs

* update entra id oidc docs

* update entra id saml docs

* update github docs

* update gitlab docs

* update apple docs

* update okta oidc docs

* update okta saml docs

* update keycloak docs

* update mocksaml

* cleanup

* lint

* lint

* fix overriden classes

* encapsulate styles

* fix icon classes

---------

Co-authored-by: peintnermax <[email protected]>
This commit is contained in:
Elio Bischof
2024-03-27 20:10:31 +00:00
committed by GitHub
co-authored by peintnermax
parent 84644214d7
commit d26391a642
79 changed files with 1960 additions and 604 deletions
@@ -5,7 +5,7 @@ import { MatTableDataSource } from '@angular/material/table';
import { Router, RouterLink } from '@angular/router';
import { TranslateService } from '@ngx-translate/core';
import { Duration } from 'google-protobuf/google/protobuf/duration_pb';
import { BehaviorSubject, Observable, Subject } from 'rxjs';
import { BehaviorSubject, firstValueFrom, Observable, Subject } from 'rxjs';
import {
ListProvidersRequest as AdminListProvidersRequest,
ListProvidersResponse as AdminListProvidersResponse,
@@ -36,6 +36,8 @@ import { ContextChangedWorkflowOverlays } from 'src/app/services/overlay/workflo
import { PageEvent, PaginatorComponent } from '../paginator/paginator.component';
import { PolicyComponentServiceType } from '../policies/policy-component-types.enum';
import { WarnDialogComponent } from '../warn-dialog/warn-dialog.component';
import { ActivateIdpService } from '../../services/activate-idp.service';
import { first } from 'rxjs/operators';
@Component({
selector: 'cnsl-idp-table',
@@ -70,6 +72,7 @@ export class IdpTableComponent implements OnInit, OnDestroy {
private toast: ToastService,
private dialog: MatDialog,
private router: Router,
private activateIdpSvc: ActivateIdpService,
) {
this.selection.changed.subscribe(() => {
this.changedSelection.emit(this.selection.selected);
@@ -298,93 +301,15 @@ export class IdpTableComponent implements OnInit, OnDestroy {
}
}
private addLoginPolicy(): Promise<AddCustomLoginPolicyResponse.AsObject> {
const mgmtreq = new AddCustomLoginPolicyRequest();
mgmtreq.setAllowExternalIdp(this.loginPolicy.allowExternalIdp);
mgmtreq.setAllowRegister(this.loginPolicy.allowRegister);
mgmtreq.setAllowUsernamePassword(this.loginPolicy.allowUsernamePassword);
mgmtreq.setForceMfa(this.loginPolicy.forceMfa);
mgmtreq.setPasswordlessType(this.loginPolicy.passwordlessType);
mgmtreq.setHidePasswordReset(this.loginPolicy.hidePasswordReset);
mgmtreq.setMultiFactorsList(this.loginPolicy.multiFactorsList);
mgmtreq.setSecondFactorsList(this.loginPolicy.secondFactorsList);
const pcl = new Duration()
.setSeconds(this.loginPolicy.passwordCheckLifetime?.seconds ?? 0)
.setNanos(this.loginPolicy.passwordCheckLifetime?.nanos ?? 0);
mgmtreq.setPasswordCheckLifetime(pcl);
const elcl = new Duration()
.setSeconds(this.loginPolicy.externalLoginCheckLifetime?.seconds ?? 0)
.setNanos(this.loginPolicy.externalLoginCheckLifetime?.nanos ?? 0);
mgmtreq.setExternalLoginCheckLifetime(elcl);
const misl = new Duration()
.setSeconds(this.loginPolicy.mfaInitSkipLifetime?.seconds ?? 0)
.setNanos(this.loginPolicy.mfaInitSkipLifetime?.nanos ?? 0);
mgmtreq.setMfaInitSkipLifetime(misl);
const sfcl = new Duration()
.setSeconds(this.loginPolicy.secondFactorCheckLifetime?.seconds ?? 0)
.setNanos(this.loginPolicy.secondFactorCheckLifetime?.nanos ?? 0);
mgmtreq.setSecondFactorCheckLifetime(sfcl);
const mficl = new Duration()
.setSeconds(this.loginPolicy.multiFactorCheckLifetime?.seconds ?? 0)
.setNanos(this.loginPolicy.multiFactorCheckLifetime?.nanos ?? 0);
mgmtreq.setMultiFactorCheckLifetime(mficl);
mgmtreq.setAllowDomainDiscovery(this.loginPolicy.allowDomainDiscovery);
mgmtreq.setIgnoreUnknownUsernames(this.loginPolicy.ignoreUnknownUsernames);
mgmtreq.setDefaultRedirectUri(this.loginPolicy.defaultRedirectUri);
return (this.service as ManagementService).addCustomLoginPolicy(mgmtreq);
}
public addIdp(idp: Provider.AsObject): Promise<any> {
switch (this.serviceType) {
case PolicyComponentServiceType.MGMT:
if (this.isDefault) {
return this.addLoginPolicy()
.then(() => {
this.loginPolicy.isDefault = false;
return (this.service as ManagementService).addIDPToLoginPolicy(idp.id, idp.owner).then(() => {
this.toast.showInfo('IDP.TOAST.ADDED', true);
setTimeout(() => {
this.reloadIDPs$.next();
}, 2000);
});
})
.catch((error) => {
this.toast.showError(error);
});
} else {
return (this.service as ManagementService)
.addIDPToLoginPolicy(idp.id, idp.owner)
.then(() => {
this.toast.showInfo('IDP.TOAST.ADDED', true);
setTimeout(() => {
this.reloadIDPs$.next();
}, 2000);
})
.catch((error) => {
this.toast.showError(error);
});
}
case PolicyComponentServiceType.ADMIN:
return (this.service as AdminService)
.addIDPToLoginPolicy(idp.id)
.then(() => {
this.toast.showInfo('IDP.TOAST.ADDED', true);
setTimeout(() => {
this.reloadIDPs$.next();
}, 2000);
})
.catch((error) => {
this.toast.showError(error);
});
}
return firstValueFrom(this.activateIdpSvc.activateIdp(this.service, idp.id, idp.owner, this.loginPolicy))
.then(() => {
this.toast.showInfo('IDP.TOAST.ADDED', true);
setTimeout(() => {
this.reloadIDPs$.next();
}, 2000);
})
.catch(this.toast.showError);
}
public removeIdp(idp: Provider.AsObject): void {
@@ -403,7 +328,8 @@ export class IdpTableComponent implements OnInit, OnDestroy {
switch (this.serviceType) {
case PolicyComponentServiceType.MGMT:
if (this.isDefault) {
this.addLoginPolicy()
this.activateIdpSvc
.addLoginPolicy(this.service as ManagementService, this.loginPolicy)
.then(() => {
this.loginPolicy.isDefault = false;
return (this.service as ManagementService)
@@ -7,9 +7,29 @@
<img class="idp-logo apple dark" src="./assets/images/idp/apple-dark.svg" alt="apple" />
<img class="idp-logo apple light" src="./assets/images/idp/apple.svg" alt="apple" />
<h1>{{ 'IDP.CREATE.APPLE.TITLE' | translate }}</h1>
<ng-container *ngIf="exists$ | async">
<div
*ngIf="{ isNotActive: (activateLink$ | async) } as idp"
class="cnsl-state-dot"
[matTooltip]="(idp.isNotActive ? 'IDP.STATES.2' : 'IDP.STATES.1') | translate"
[ngClass]="{ active: !idp.isNotActive, inactive: !!idp.isNotActive }"
></div>
</ng-container>
<mat-spinner diameter="25" *ngIf="loading" color="primary"></mat-spinner>
</div>
<cnsl-provider-next
[configureProvider]="(exists$ | async) === false"
[configureTitle]="'DESCRIPTIONS.SETTINGS.IDPS.CALLBACK.TITLE' | translate: { provider: 'Apple' }"
[configureDescription]="'DESCRIPTIONS.SETTINGS.IDPS.CALLBACK.DESCRIPTION' | translate: { provider: 'Apple' }"
configureLink="https://zitadel.com/docs/guides/integrate/identity-providers/apple#apple-configuration"
[autofillLink]="autofillLink$ | async"
[activateLink]="activateLink$ | async"
[copyUrls]="copyUrls$ | async"
[expanded]="!!(expandWhatNow$ | async)"
(activate)="activate()"
></cnsl-provider-next>
<p class="identity-provider-desc cnsl-secondary-text">
{{ !provider ? ('IDP.CREATE.APPLE.DESCRIPTION' | translate) : ('IDP.DETAIL.DESCRIPTION' | translate) }}
</p>
@@ -131,8 +151,9 @@
[disabled]="form.invalid || form.disabled"
type="submit"
>
<span *ngIf="id">{{ 'ACTIONS.SAVE' | translate }}</span>
<span *ngIf="!id">{{ 'ACTIONS.CREATE' | translate }}</span>
<span *ngIf="{ exists: (exists$ | async) } as idp">{{
(!!idp.exists ? 'ACTIONS.SAVE' : 'ACTIONS.CREATE') | translate
}}</span>
</button>
</div>
</form>
@@ -4,7 +4,7 @@ import { Component, Injector, Type } from '@angular/core';
import { AbstractControl, FormControl, FormGroup } from '@angular/forms';
import { MatChipInputEvent } from '@angular/material/chips';
import { ActivatedRoute } from '@angular/router';
import { take } from 'rxjs';
import { BehaviorSubject, Observable, take } from 'rxjs';
import {
AddAppleProviderRequest as AdminAddAppleProviderRequest,
GetProviderByIDRequest as AdminGetProviderByIDRequest,
@@ -24,6 +24,7 @@ import { ToastService } from 'src/app/services/toast.service';
import { requiredValidator } from '../../form-field/validators/validators';
import { PolicyComponentServiceType } from '../../policies/policy-component-types.enum';
import { ProviderNextService } from '../provider-next/provider-next.service';
const MAX_ALLOWED_SIZE = 5 * 1024;
@@ -34,8 +35,11 @@ const MAX_ALLOWED_SIZE = 5 * 1024;
export class ProviderAppleComponent {
public showOptional: boolean = false;
public options: Options = new Options().setIsCreationAllowed(true).setIsLinkingAllowed(true);
// DEPRECATED: use id$ instead
public id: string | null = '';
// DEPRECATED: assert service$ instead
public serviceType: PolicyComponentServiceType = PolicyComponentServiceType.MGMT;
// DEPRECATED: use service$ instead
private service!: ManagementService | AdminService;
public readonly separatorKeysCodes: number[] = [ENTER, COMMA, SPACE];
@@ -47,6 +51,25 @@ export class ProviderAppleComponent {
public provider?: Provider.AsObject;
public updatePrivateKey: boolean = false;
public justCreated$: BehaviorSubject<string> = new BehaviorSubject<string>('');
public justActivated$ = new BehaviorSubject<boolean>(false);
private service$ = this.nextSvc.service(this.route.data);
private id$ = this.nextSvc.id(this.route.paramMap, this.justCreated$);
public exists$ = this.nextSvc.exists(this.id$);
public autofillLink$ = this.nextSvc.autofillLink(
this.id$,
`https://zitadel.com/docs/guides/integrate/identity-providers/additional-information`,
);
public activateLink$ = this.nextSvc.activateLink(
this.id$,
this.justActivated$,
'https://zitadel.com/docs/guides/integrate/identity-providers/apple#activate-idp',
this.service$,
);
public expandWhatNow$ = this.nextSvc.expandWhatNow(this.id$, this.activateLink$, this.justCreated$);
public copyUrls$ = this.nextSvc.callbackUrls();
constructor(
private authService: GrpcAuthService,
private route: ActivatedRoute,
@@ -54,6 +77,7 @@ export class ProviderAppleComponent {
private injector: Injector,
private _location: Location,
private breadcrumbService: BreadcrumbService,
private nextSvc: ProviderNextService,
) {
this.form = new FormGroup({
name: new FormControl('', []),
@@ -115,6 +139,10 @@ export class ProviderAppleComponent {
});
}
public activate() {
this.nextSvc.activate(this.id$, this.justActivated$, this.service$);
}
private getData(id: string): void {
const req =
this.serviceType === PolicyComponentServiceType.ADMIN
@@ -138,7 +166,7 @@ export class ProviderAppleComponent {
}
public submitForm(): void {
this.provider ? this.updateAppleProvider() : this.addAppleProvider();
this.provider || this.justCreated$.value ? this.updateAppleProvider() : this.addAppleProvider();
}
public addAppleProvider(): void {
@@ -158,11 +186,9 @@ export class ProviderAppleComponent {
this.loading = true;
this.service
.addAppleProvider(req)
.then((idp) => {
setTimeout(() => {
this.loading = false;
this.close();
}, 2000);
.then((addedIDP) => {
this.justCreated$.next(addedIDP.id);
this.loading = false;
})
.catch((error) => {
this.toast.showError(error);
@@ -171,10 +197,10 @@ export class ProviderAppleComponent {
}
public updateAppleProvider(): void {
if (this.provider) {
if (this.provider || this.justCreated$.value) {
if (this.serviceType === PolicyComponentServiceType.MGMT) {
const req = new MgmtUpdateAppleProviderRequest();
req.setId(this.provider.id);
req.setId(this.provider?.id || this.justCreated$.value);
req.setName(this.name?.value);
req.setClientId(this.clientId?.value);
req.setTeamId(this.teamId?.value);
@@ -201,7 +227,7 @@ export class ProviderAppleComponent {
});
} else if (PolicyComponentServiceType.ADMIN) {
const req = new AdminUpdateAppleProviderRequest();
req.setId(this.provider.id);
req.setId(this.provider?.id || this.justCreated$.value);
req.setName(this.name?.value);
req.setClientId(this.clientId?.value);
req.setTeamId(this.teamId?.value);
@@ -6,9 +6,29 @@
<div class="title-row">
<img class="idp-logo" src="./assets/images/idp/ms.svg" alt="microsoft" />
<h1>{{ 'IDP.CREATE.AZUREAD.TITLE' | translate }}</h1>
<ng-container *ngIf="exists$ | async">
<div
*ngIf="{ isNotActive: (activateLink$ | async) } as idp"
class="cnsl-state-dot"
[matTooltip]="(idp.isNotActive ? 'IDP.STATES.2' : 'IDP.STATES.1') | translate"
[ngClass]="{ active: !idp.isNotActive, inactive: !!idp.isNotActive }"
></div>
</ng-container>
<mat-spinner diameter="25" *ngIf="loading" color="primary"></mat-spinner>
</div>
<cnsl-provider-next
[configureProvider]="(exists$ | async) === false"
[configureTitle]="'DESCRIPTIONS.SETTINGS.IDPS.CALLBACK.TITLE' | translate: { provider: 'Microsoft' }"
[configureDescription]="'DESCRIPTIONS.SETTINGS.IDPS.CALLBACK.DESCRIPTION' | translate: { provider: 'Microsoft' }"
configureLink="https://zitadel.com/docs/guides/integrate/identity-providers/azure-ad-oidc#entra-id-configuration"
[autofillLink]="autofillLink$ | async"
[activateLink]="activateLink$ | async"
[copyUrls]="copyUrls$ | async"
[expanded]="!!(expandWhatNow$ | async)"
(activate)="activate()"
></cnsl-provider-next>
<p class="identity-provider-desc cnsl-secondary-text">
{{ !provider ? ('IDP.CREATE.AZUREAD.DESCRIPTION' | translate) : ('IDP.DETAIL.DESCRIPTION' | translate) }}
</p>
@@ -104,8 +124,9 @@
[disabled]="form.invalid || form.disabled"
type="submit"
>
<span *ngIf="id">{{ 'ACTIONS.SAVE' | translate }}</span>
<span *ngIf="!id">{{ 'ACTIONS.CREATE' | translate }}</span>
<span *ngIf="{ exists: (exists$ | async) } as idp">{{
(!!idp.exists ? 'ACTIONS.SAVE' : 'ACTIONS.CREATE') | translate
}}</span>
</button>
</div>
</form>
@@ -4,13 +4,13 @@ import { Component, Injector, Type } from '@angular/core';
import { AbstractControl, FormControl, FormGroup } from '@angular/forms';
import { MatChipInputEvent } from '@angular/material/chips';
import { ActivatedRoute } from '@angular/router';
import { take } from 'rxjs';
import { BehaviorSubject, take } from 'rxjs';
import {
AddAzureADProviderRequest as AdminAddAzureADProviderRequest,
GetProviderByIDRequest as AdminGetProviderByIDRequest,
UpdateAzureADProviderRequest as AdminUpdateAzureADProviderRequest,
} from 'src/app/proto/generated/zitadel/admin_pb';
import { AzureADTenant, AzureADTenantType, Options, Provider } from 'src/app/proto/generated/zitadel/idp_pb';
import { AzureADTenant, AzureADTenantType, IDPOwnerType, Options, Provider } from 'src/app/proto/generated/zitadel/idp_pb';
import {
AddAzureADProviderRequest as MgmtAddAzureADProviderRequest,
GetProviderByIDRequest as MgmtGetProviderByIDRequest,
@@ -24,6 +24,7 @@ import { ToastService } from 'src/app/services/toast.service';
import { requiredValidator } from '../../form-field/validators/validators';
import { PolicyComponentServiceType } from '../../policies/policy-component-types.enum';
import { ProviderNextService } from '../provider-next/provider-next.service';
@Component({
selector: 'cnsl-provider-azure-ad',
@@ -32,8 +33,11 @@ import { PolicyComponentServiceType } from '../../policies/policy-component-type
export class ProviderAzureADComponent {
public showOptional: boolean = false;
public options: Options = new Options().setIsCreationAllowed(true).setIsLinkingAllowed(true);
// DEPRECATED: use id$ instead
public id: string | null = '';
// DEPRECATED: assert service$ instead
public serviceType: PolicyComponentServiceType = PolicyComponentServiceType.MGMT;
// DEPRECATED: use service$ instead
private service!: ManagementService | AdminService;
public readonly separatorKeysCodes: number[] = [ENTER, COMMA, SPACE];
@@ -53,6 +57,25 @@ export class ProviderAzureADComponent {
AzureADTenantType.AZURE_AD_TENANT_TYPE_CONSUMERS,
];
public justCreated$: BehaviorSubject<string> = new BehaviorSubject<string>('');
public justActivated$ = new BehaviorSubject<boolean>(false);
private service$ = this.nextSvc.service(this.route.data);
private id$ = this.nextSvc.id(this.route.paramMap, this.justCreated$);
public exists$ = this.nextSvc.exists(this.id$);
public autofillLink$ = this.nextSvc.autofillLink(
this.id$,
`https://zitadel.com/docs/guides/integrate/identity-providers/additional-information`,
);
public activateLink$ = this.nextSvc.activateLink(
this.id$,
this.justActivated$,
'https://zitadel.com/docs/guides/integrate/identity-providers/azure-ad-oidc#activate-idp',
this.service$,
);
public expandWhatNow$ = this.nextSvc.expandWhatNow(this.id$, this.activateLink$, this.justCreated$);
public copyUrls$ = this.nextSvc.callbackUrls();
constructor(
private authService: GrpcAuthService,
private route: ActivatedRoute,
@@ -60,6 +83,7 @@ export class ProviderAzureADComponent {
private injector: Injector,
private _location: Location,
private breadcrumbService: BreadcrumbService,
private nextSvc: ProviderNextService,
) {
this.form = new FormGroup({
name: new FormControl('', []),
@@ -122,6 +146,10 @@ export class ProviderAzureADComponent {
});
}
public activate() {
this.nextSvc.activate(this.id$, this.justActivated$, this.service$);
}
private getData(id: string): void {
const req =
this.serviceType === PolicyComponentServiceType.ADMIN
@@ -166,7 +194,7 @@ export class ProviderAzureADComponent {
}
public submitForm(): void {
this.provider ? this.updateAzureADProvider() : this.addAzureADProvider();
this.provider || this.justCreated$.value ? this.updateAzureADProvider() : this.addAzureADProvider();
}
public addAzureADProvider(): void {
@@ -194,11 +222,9 @@ export class ProviderAzureADComponent {
this.loading = true;
this.service
.addAzureADProvider(req)
.then((idp) => {
setTimeout(() => {
this.loading = false;
this.close();
}, 2000);
.then((addedIDP) => {
this.justCreated$.next(addedIDP.id);
this.loading = false;
})
.catch((error) => {
this.toast.showError(error);
@@ -207,13 +233,13 @@ export class ProviderAzureADComponent {
}
public updateAzureADProvider(): void {
if (this.provider) {
if (this.provider || this.justCreated$.value) {
const req =
this.serviceType === PolicyComponentServiceType.MGMT
? new MgmtUpdateAzureADProviderRequest()
: new AdminUpdateAzureADProviderRequest();
req.setId(this.provider.id);
req.setId(this.provider?.id || this.justCreated$.value);
req.setName(this.name?.value);
req.setClientId(this.clientId?.value);
req.setEmailVerified(this.emailVerified?.value);
@@ -6,11 +6,30 @@
<div class="title-row">
<img class="idp-logo dark" src="./assets/images/idp/github-dark.svg" alt="github" />
<img class="idp-logo light" src="./assets/images/idp/github.svg" alt="github" />
<h1>{{ 'IDP.CREATE.GITHUBES.TITLE' | translate }}</h1>
<ng-container *ngIf="exists$ | async">
<div
*ngIf="{ isNotActive: (activateLink$ | async) } as idp"
class="cnsl-state-dot"
[matTooltip]="(idp.isNotActive ? 'IDP.STATES.2' : 'IDP.STATES.1') | translate"
[ngClass]="{ active: !idp.isNotActive, inactive: !!idp.isNotActive }"
></div>
</ng-container>
<mat-spinner diameter="25" *ngIf="loading" color="primary"></mat-spinner>
</div>
<cnsl-provider-next
[configureProvider]="(exists$ | async) === false"
[configureTitle]="'DESCRIPTIONS.SETTINGS.IDPS.CALLBACK.TITLE' | translate: { provider: 'GitHub' }"
[configureDescription]="'DESCRIPTIONS.SETTINGS.IDPS.CALLBACK.DESCRIPTION' | translate: { provider: 'GitHub' }"
configureLink="https://zitadel.com/docs/guides/integrate/identity-providers/github#github-configuration"
[autofillLink]="autofillLink$ | async"
[activateLink]="activateLink$ | async"
[copyUrls]="copyUrls$ | async"
[expanded]="!!(expandWhatNow$ | async)"
(activate)="activate()"
></cnsl-provider-next>
<p class="identity-provider-desc cnsl-secondary-text">{{ 'IDP.CREATE.GITHUBES.DESCRIPTION' | translate }}</p>
<form [formGroup]="form" (ngSubmit)="submitForm()">
@@ -103,8 +122,9 @@
[disabled]="form.invalid || form.disabled"
type="submit"
>
<span *ngIf="id">{{ 'ACTIONS.SAVE' | translate }}</span>
<span *ngIf="!id">{{ 'ACTIONS.CREATE' | translate }}</span>
<span *ngIf="{ exists: (exists$ | async) } as idp">{{
(!!idp.exists ? 'ACTIONS.SAVE' : 'ACTIONS.CREATE') | translate
}}</span>
</button>
</div>
</form>
@@ -4,7 +4,7 @@ import { Component, Injector, Type } from '@angular/core';
import { AbstractControl, UntypedFormControl, UntypedFormGroup } from '@angular/forms';
import { MatChipInputEvent } from '@angular/material/chips';
import { ActivatedRoute } from '@angular/router';
import { take } from 'rxjs';
import { BehaviorSubject, take } from 'rxjs';
import {
AddGitHubEnterpriseServerProviderRequest as AdminAddGitHubEnterpriseServerProviderRequest,
GetProviderByIDRequest as AdminGetProviderByIDRequest,
@@ -24,6 +24,7 @@ import { ToastService } from 'src/app/services/toast.service';
import { requiredValidator } from '../../form-field/validators/validators';
import { PolicyComponentServiceType } from '../../policies/policy-component-types.enum';
import { ProviderNextService } from '../provider-next/provider-next.service';
@Component({
selector: 'cnsl-provider-github-es',
@@ -33,9 +34,12 @@ export class ProviderGithubESComponent {
public showOptional: boolean = false;
public options: Options = new Options().setIsCreationAllowed(true).setIsLinkingAllowed(true);
// DEPRECATED: use id$ instead
public id: string | null = '';
public updateClientSecret: boolean = false;
// DEPRECATED: assert service$ instead
public serviceType: PolicyComponentServiceType = PolicyComponentServiceType.MGMT;
// DEPRECATED: use service$ instead
private service!: ManagementService | AdminService;
public readonly separatorKeysCodes: number[] = [ENTER, COMMA, SPACE];
public form!: UntypedFormGroup;
@@ -44,6 +48,25 @@ export class ProviderGithubESComponent {
public provider?: Provider.AsObject;
public justCreated$: BehaviorSubject<string> = new BehaviorSubject<string>('');
public justActivated$ = new BehaviorSubject<boolean>(false);
private service$ = this.nextSvc.service(this.route.data);
private id$ = this.nextSvc.id(this.route.paramMap, this.justCreated$);
public exists$ = this.nextSvc.exists(this.id$);
public autofillLink$ = this.nextSvc.autofillLink(
this.id$,
`https://zitadel.com/docs/guides/integrate/identity-providers/github#optional-add-zitadel-action-to-autofill-userdata`,
);
public activateLink$ = this.nextSvc.activateLink(
this.id$,
this.justActivated$,
'https://zitadel.com/docs/guides/integrate/identity-providers/github#activate-idp',
this.service$,
);
public expandWhatNow$ = this.nextSvc.expandWhatNow(this.id$, this.activateLink$, this.justCreated$);
public copyUrls$ = this.nextSvc.callbackUrls();
constructor(
private authService: GrpcAuthService,
private route: ActivatedRoute,
@@ -51,6 +74,7 @@ export class ProviderGithubESComponent {
private injector: Injector,
private _location: Location,
breadcrumbService: BreadcrumbService,
private nextSvc: ProviderNextService,
) {
this.form = new UntypedFormGroup({
name: new UntypedFormControl('', [requiredValidator]),
@@ -113,6 +137,10 @@ export class ProviderGithubESComponent {
});
}
public activate() {
this.nextSvc.activate(this.id$, this.justActivated$, this.service$);
}
private getData(id: string): void {
this.loading = true;
const req =
@@ -137,7 +165,7 @@ export class ProviderGithubESComponent {
}
public submitForm(): void {
this.provider ? this.updateGenericOAuthProvider() : this.addGenericOAuthProvider();
this.provider || this.justCreated$.value ? this.updateGenericOAuthProvider() : this.addGenericOAuthProvider();
}
public addGenericOAuthProvider(): void {
@@ -158,11 +186,9 @@ export class ProviderGithubESComponent {
this.loading = true;
this.service
.addGitHubEnterpriseServerProvider(req)
.then((idp) => {
setTimeout(() => {
this.loading = false;
this.close();
}, 2000);
.then((addedIDP) => {
this.justCreated$.next(addedIDP.id);
this.loading = false;
})
.catch((error) => {
this.toast.showError(error);
@@ -171,12 +197,12 @@ export class ProviderGithubESComponent {
}
public updateGenericOAuthProvider(): void {
if (this.provider) {
if (this.provider || this.justCreated$.value) {
const req =
this.serviceType === PolicyComponentServiceType.MGMT
? new MgmtUpdateGitHubEnterpriseServerProviderRequest()
: new AdminUpdateGitHubEnterpriseServerProviderRequest();
req.setId(this.provider.id);
req.setId(this.provider?.id || this.justCreated$.value);
req.setName(this.name?.value);
req.setAuthorizationEndpoint(this.authorizationEndpoint?.value);
req.setTokenEndpoint(this.tokenEndpoint?.value);
@@ -7,9 +7,29 @@
<img class="idp-logo dark" src="./assets/images/idp/github-dark.svg" alt="github" />
<img class="idp-logo light" src="./assets/images/idp/github.svg" alt="github" />
<h1>{{ 'IDP.CREATE.GITHUB.TITLE' | translate }}</h1>
<ng-container *ngIf="exists$ | async">
<div
*ngIf="{ isNotActive: (activateLink$ | async) } as idp"
class="cnsl-state-dot"
[matTooltip]="(idp.isNotActive ? 'IDP.STATES.2' : 'IDP.STATES.1') | translate"
[ngClass]="{ active: !idp.isNotActive, inactive: !!idp.isNotActive }"
></div>
</ng-container>
<mat-spinner diameter="25" *ngIf="loading" color="primary"></mat-spinner>
</div>
<cnsl-provider-next
[configureProvider]="(exists$ | async) === false"
[configureTitle]="'DESCRIPTIONS.SETTINGS.IDPS.CALLBACK.TITLE' | translate: { provider: 'GitHub' }"
[configureDescription]="'DESCRIPTIONS.SETTINGS.IDPS.CALLBACK.DESCRIPTION' | translate: { provider: 'GitHub' }"
configureLink="https://zitadel.com/docs/guides/integrate/identity-providers/github#github-configuration"
[autofillLink]="autofillLink$ | async"
[activateLink]="activateLink$ | async"
[copyUrls]="copyUrls$ | async"
[expanded]="!!(expandWhatNow$ | async)"
(activate)="activate()"
></cnsl-provider-next>
<p class="identity-provider-desc cnsl-secondary-text">
{{ !provider ? ('IDP.CREATE.GITHUB.DESCRIPTION' | translate) : ('IDP.DETAIL.DESCRIPTION' | translate) }}
</p>
@@ -90,8 +110,9 @@
[disabled]="form.invalid || form.disabled"
type="submit"
>
<span *ngIf="id">{{ 'ACTIONS.SAVE' | translate }}</span>
<span *ngIf="!id">{{ 'ACTIONS.CREATE' | translate }}</span>
<span *ngIf="{ exists: (exists$ | async) } as idp">{{
(!!idp.exists ? 'ACTIONS.SAVE' : 'ACTIONS.CREATE') | translate
}}</span>
</button>
</div>
</form>
@@ -4,7 +4,7 @@ import { Component, Injector, Type } from '@angular/core';
import { AbstractControl, FormControl, FormGroup } from '@angular/forms';
import { MatChipInputEvent } from '@angular/material/chips';
import { ActivatedRoute } from '@angular/router';
import { take } from 'rxjs';
import { BehaviorSubject, take } from 'rxjs';
import {
AddGitHubProviderRequest as AdminAddGithubProviderRequest,
GetProviderByIDRequest as AdminGetProviderByIDRequest,
@@ -24,6 +24,7 @@ import { ToastService } from 'src/app/services/toast.service';
import { requiredValidator } from '../../form-field/validators/validators';
import { PolicyComponentServiceType } from '../../policies/policy-component-types.enum';
import { ProviderNextService } from '../provider-next/provider-next.service';
@Component({
selector: 'cnsl-provider-github',
@@ -32,8 +33,11 @@ import { PolicyComponentServiceType } from '../../policies/policy-component-type
export class ProviderGithubComponent {
public showOptional: boolean = false;
public options: Options = new Options().setIsCreationAllowed(true).setIsLinkingAllowed(true);
// DEPRECATED: use id$ instead
public id: string | null = '';
// DEPRECATED: assert service$ instead
public serviceType: PolicyComponentServiceType = PolicyComponentServiceType.MGMT;
// DEPRECATED: use service$ instead
private service!: ManagementService | AdminService;
public readonly separatorKeysCodes: number[] = [ENTER, COMMA, SPACE];
@@ -45,6 +49,25 @@ export class ProviderGithubComponent {
public provider?: Provider.AsObject;
public updateClientSecret: boolean = false;
public justCreated$: BehaviorSubject<string> = new BehaviorSubject<string>('');
public justActivated$ = new BehaviorSubject<boolean>(false);
private service$ = this.nextSvc.service(this.route.data);
private id$ = this.nextSvc.id(this.route.paramMap, this.justCreated$);
public exists$ = this.nextSvc.exists(this.id$);
public autofillLink$ = this.nextSvc.autofillLink(
this.id$,
`https://zitadel.com/docs/guides/integrate/identity-providers/github#optional-add-zitadel-action-to-autofill-userdata`,
);
public activateLink$ = this.nextSvc.activateLink(
this.id$,
this.justActivated$,
'https://zitadel.com/docs/guides/integrate/identity-providers/github#activate-idp',
this.service$,
);
public expandWhatNow$ = this.nextSvc.expandWhatNow(this.id$, this.activateLink$, this.justCreated$);
public copyUrls$ = this.nextSvc.callbackUrls();
constructor(
private authService: GrpcAuthService,
private route: ActivatedRoute,
@@ -52,6 +75,7 @@ export class ProviderGithubComponent {
private injector: Injector,
private _location: Location,
private breadcrumbService: BreadcrumbService,
private nextSvc: ProviderNextService,
) {
this.form = new FormGroup({
name: new FormControl('', []),
@@ -111,6 +135,10 @@ export class ProviderGithubComponent {
});
}
public activate() {
this.nextSvc.activate(this.id$, this.justActivated$, this.service$);
}
private getData(id: string): void {
const req =
this.serviceType === PolicyComponentServiceType.ADMIN
@@ -134,7 +162,7 @@ export class ProviderGithubComponent {
}
public submitForm(): void {
this.provider ? this.updateGithubProvider() : this.addGithubProvider();
this.provider || this.justCreated$.value ? this.updateGithubProvider() : this.addGithubProvider();
}
public addGithubProvider(): void {
@@ -152,11 +180,9 @@ export class ProviderGithubComponent {
this.loading = true;
this.service
.addGitHubProvider(req)
.then((idp) => {
setTimeout(() => {
this.loading = false;
this.close();
}, 2000);
.then((addedIDP) => {
this.justCreated$.next(addedIDP.id);
this.loading = false;
})
.catch((error) => {
this.toast.showError(error);
@@ -165,12 +191,12 @@ export class ProviderGithubComponent {
}
public updateGithubProvider(): void {
if (this.provider) {
if (this.provider || this.justCreated$.value) {
const req =
this.serviceType === PolicyComponentServiceType.MGMT
? new MgmtUpdateGithubProviderRequest()
: new AdminUpdateGithubProviderRequest();
req.setId(this.provider.id);
req.setId(this.provider?.id || this.justCreated$.value);
req.setName(this.name?.value);
req.setClientId(this.clientId?.value);
req.setScopesList(this.scopesList?.value);
@@ -6,9 +6,29 @@
<div class="title-row">
<img class="idp-logo" src="./assets/images/idp/gitlab.svg" alt="gitlab" />
<h1>{{ 'IDP.CREATE.GITLABSELFHOSTED.TITLE' | translate }}</h1>
<ng-container *ngIf="exists$ | async">
<div
*ngIf="{ isNotActive: (activateLink$ | async) } as idp"
class="cnsl-state-dot"
[matTooltip]="(idp.isNotActive ? 'IDP.STATES.2' : 'IDP.STATES.1') | translate"
[ngClass]="{ active: !idp.isNotActive, inactive: !!idp.isNotActive }"
></div>
</ng-container>
<mat-spinner diameter="25" *ngIf="loading" color="primary"></mat-spinner>
</div>
<cnsl-provider-next
[configureProvider]="(exists$ | async) === false"
[configureTitle]="'DESCRIPTIONS.SETTINGS.IDPS.CALLBACK.TITLE' | translate: { provider: 'GitLab' }"
[configureDescription]="'DESCRIPTIONS.SETTINGS.IDPS.CALLBACK.DESCRIPTION' | translate: { provider: 'GitLab' }"
configureLink="https://zitadel.com/docs/guides/integrate/identity-providers/gitlab#gitlab-configuration"
[autofillLink]="autofillLink$ | async"
[activateLink]="activateLink$ | async"
[copyUrls]="copyUrls$ | async"
[expanded]="!!(expandWhatNow$ | async)"
(activate)="activate()"
></cnsl-provider-next>
<p class="identity-provider-desc cnsl-secondary-text">
{{ !provider ? ('IDP.CREATE.GITLABSELFHOSTED.DESCRIPTION' | translate) : ('IDP.DETAIL.DESCRIPTION' | translate) }}
</p>
@@ -93,8 +113,9 @@
[disabled]="form.invalid || form.disabled"
type="submit"
>
<span *ngIf="id">{{ 'ACTIONS.SAVE' | translate }}</span>
<span *ngIf="!id">{{ 'ACTIONS.CREATE' | translate }}</span>
<span *ngIf="{ exists: (exists$ | async) } as idp">{{
(!!idp.exists ? 'ACTIONS.SAVE' : 'ACTIONS.CREATE') | translate
}}</span>
</button>
</div>
</form>
@@ -4,7 +4,7 @@ import { Component, Injector, Type } from '@angular/core';
import { AbstractControl, FormControl, FormGroup } from '@angular/forms';
import { MatChipInputEvent } from '@angular/material/chips';
import { ActivatedRoute } from '@angular/router';
import { take } from 'rxjs';
import { BehaviorSubject, take } from 'rxjs';
import {
AddGitLabSelfHostedProviderRequest as AdminAddGitLabSelfHostedProviderRequest,
GetProviderByIDRequest as AdminGetProviderByIDRequest,
@@ -24,6 +24,7 @@ import { ToastService } from 'src/app/services/toast.service';
import { requiredValidator } from '../../form-field/validators/validators';
import { PolicyComponentServiceType } from '../../policies/policy-component-types.enum';
import { ProviderNextService } from '../provider-next/provider-next.service';
@Component({
selector: 'cnsl-provider-gitlab-self-hosted',
@@ -32,8 +33,11 @@ import { PolicyComponentServiceType } from '../../policies/policy-component-type
export class ProviderGitlabSelfHostedComponent {
public showOptional: boolean = false;
public options: Options = new Options().setIsCreationAllowed(true).setIsLinkingAllowed(true);
// DEPRECATED: use id$ instead
public id: string | null = '';
// DEPRECATED: assert service$ instead
public serviceType: PolicyComponentServiceType = PolicyComponentServiceType.MGMT;
// DEPRECATED: use service$ instead
private service!: ManagementService | AdminService;
public readonly separatorKeysCodes: number[] = [ENTER, COMMA, SPACE];
@@ -45,6 +49,25 @@ export class ProviderGitlabSelfHostedComponent {
public provider?: Provider.AsObject;
public updateClientSecret: boolean = false;
public justCreated$: BehaviorSubject<string> = new BehaviorSubject<string>('');
public justActivated$ = new BehaviorSubject<boolean>(false);
private service$ = this.nextSvc.service(this.route.data);
private id$ = this.nextSvc.id(this.route.paramMap, this.justCreated$);
public exists$ = this.nextSvc.exists(this.id$);
public autofillLink$ = this.nextSvc.autofillLink(
this.id$,
`https://zitadel.com/docs/guides/integrate/identity-providers/gitlab#optional-add-zitadel-action-to-autofill-userdata`,
);
public activateLink$ = this.nextSvc.activateLink(
this.id$,
this.justActivated$,
'https://zitadel.com/docs/guides/integrate/identity-providers/gitlab#activate-idp',
this.service$,
);
public expandWhatNow$ = this.nextSvc.expandWhatNow(this.id$, this.activateLink$, this.justCreated$);
public copyUrls$ = this.nextSvc.callbackUrls();
constructor(
private authService: GrpcAuthService,
private route: ActivatedRoute,
@@ -52,6 +75,7 @@ export class ProviderGitlabSelfHostedComponent {
private injector: Injector,
private _location: Location,
private breadcrumbService: BreadcrumbService,
private nextSvc: ProviderNextService,
) {
this.form = new FormGroup({
name: new FormControl('', [requiredValidator]),
@@ -112,6 +136,10 @@ export class ProviderGitlabSelfHostedComponent {
});
}
public activate() {
this.nextSvc.activate(this.id$, this.justActivated$, this.service$);
}
private getData(id: string): void {
const req =
this.serviceType === PolicyComponentServiceType.ADMIN
@@ -135,7 +163,7 @@ export class ProviderGitlabSelfHostedComponent {
}
public submitForm(): void {
this.provider ? this.updateGitlabSelfHostedProvider() : this.addGitlabSelfHostedProvider();
this.provider || this.justCreated$.value ? this.updateGitlabSelfHostedProvider() : this.addGitlabSelfHostedProvider();
}
public addGitlabSelfHostedProvider(): void {
@@ -154,11 +182,9 @@ export class ProviderGitlabSelfHostedComponent {
this.loading = true;
this.service
.addGitLabSelfHostedProvider(req)
.then((idp) => {
setTimeout(() => {
this.loading = false;
this.close();
}, 2000);
.then((addedIDP) => {
this.justCreated$.next(addedIDP.id);
this.loading = false;
})
.catch((error) => {
this.toast.showError(error);
@@ -167,12 +193,12 @@ export class ProviderGitlabSelfHostedComponent {
}
public updateGitlabSelfHostedProvider(): void {
if (this.provider) {
if (this.provider || this.justCreated$.value) {
const req =
this.serviceType === PolicyComponentServiceType.MGMT
? new MgmtUpdateGitLabSelfHostedProviderRequest()
: new AdminUpdateGitLabSelfHostedProviderRequest();
req.setId(this.provider.id);
req.setId(this.provider?.id || this.justCreated$.value);
req.setName(this.name?.value);
req.setIssuer(this.issuer?.value);
req.setClientId(this.clientId?.value);
@@ -6,9 +6,29 @@
<div class="title-row">
<img class="idp-logo" src="./assets/images/idp/gitlab.svg" alt="gitlab" />
<h1>{{ 'IDP.CREATE.GITLAB.TITLE' | translate }}</h1>
<ng-container *ngIf="exists$ | async">
<div
*ngIf="{ isNotActive: (activateLink$ | async) } as idp"
class="cnsl-state-dot"
[matTooltip]="(idp.isNotActive ? 'IDP.STATES.2' : 'IDP.STATES.1') | translate"
[ngClass]="{ active: !idp.isNotActive, inactive: !!idp.isNotActive }"
></div>
</ng-container>
<mat-spinner diameter="25" *ngIf="loading" color="primary"></mat-spinner>
</div>
<cnsl-provider-next
[configureProvider]="(exists$ | async) === false"
[configureTitle]="'DESCRIPTIONS.SETTINGS.IDPS.CALLBACK.TITLE' | translate: { provider: 'GitLab' }"
[configureDescription]="'DESCRIPTIONS.SETTINGS.IDPS.CALLBACK.DESCRIPTION' | translate: { provider: 'GitLab' }"
configureLink="https://zitadel.com/docs/guides/integrate/identity-providers/gitlab#gitlab-configuration"
[autofillLink]="autofillLink$ | async"
[activateLink]="activateLink$ | async"
[copyUrls]="copyUrls$ | async"
[expanded]="!!(expandWhatNow$ | async)"
(activate)="activate()"
></cnsl-provider-next>
<p class="identity-provider-desc cnsl-secondary-text">
{{ !provider ? ('IDP.CREATE.GITLAB.DESCRIPTION' | translate) : ('IDP.DETAIL.DESCRIPTION' | translate) }}
</p>
@@ -89,8 +109,9 @@
[disabled]="form.invalid || form.disabled"
type="submit"
>
<span *ngIf="id">{{ 'ACTIONS.SAVE' | translate }}</span>
<span *ngIf="!id">{{ 'ACTIONS.CREATE' | translate }}</span>
<span *ngIf="{ exists: (exists$ | async) } as idp">{{
(!!idp.exists ? 'ACTIONS.SAVE' : 'ACTIONS.CREATE') | translate
}}</span>
</button>
</div>
</form>
@@ -4,7 +4,7 @@ import { Component, Injector, Type } from '@angular/core';
import { AbstractControl, FormControl, FormGroup } from '@angular/forms';
import { MatChipInputEvent } from '@angular/material/chips';
import { ActivatedRoute } from '@angular/router';
import { take } from 'rxjs';
import { BehaviorSubject, take } from 'rxjs';
import {
AddGitLabProviderRequest as AdminAddGitLabProviderRequest,
GetProviderByIDRequest as AdminGetProviderByIDRequest,
@@ -24,6 +24,7 @@ import { ToastService } from 'src/app/services/toast.service';
import { requiredValidator } from '../../form-field/validators/validators';
import { PolicyComponentServiceType } from '../../policies/policy-component-types.enum';
import { ProviderNextService } from '../provider-next/provider-next.service';
@Component({
selector: 'cnsl-provider-gitlab',
@@ -32,8 +33,11 @@ import { PolicyComponentServiceType } from '../../policies/policy-component-type
export class ProviderGitlabComponent {
public showOptional: boolean = false;
public options: Options = new Options().setIsCreationAllowed(true).setIsLinkingAllowed(true);
// DEPRECATED: use id$ instead
public id: string | null = '';
// DEPRECATED: assert service$ instead
public serviceType: PolicyComponentServiceType = PolicyComponentServiceType.MGMT;
// DEPRECATED: use service$ instead
private service!: ManagementService | AdminService;
public readonly separatorKeysCodes: number[] = [ENTER, COMMA, SPACE];
@@ -45,6 +49,25 @@ export class ProviderGitlabComponent {
public provider?: Provider.AsObject;
public updateClientSecret: boolean = false;
public justCreated$: BehaviorSubject<string> = new BehaviorSubject<string>('');
public justActivated$ = new BehaviorSubject<boolean>(false);
private service$ = this.nextSvc.service(this.route.data);
private id$ = this.nextSvc.id(this.route.paramMap, this.justCreated$);
public exists$ = this.nextSvc.exists(this.id$);
public autofillLink$ = this.nextSvc.autofillLink(
this.id$,
`https://zitadel.com/docs/guides/integrate/identity-providers/gitlab#optional-add-zitadel-action-to-autofill-userdata`,
);
public activateLink$ = this.nextSvc.activateLink(
this.id$,
this.justActivated$,
'https://zitadel.com/docs/guides/integrate/identity-providers/gitlab#activate-idp',
this.service$,
);
public expandWhatNow$ = this.nextSvc.expandWhatNow(this.id$, this.activateLink$, this.justCreated$);
public copyUrls$ = this.nextSvc.callbackUrls();
constructor(
private authService: GrpcAuthService,
private route: ActivatedRoute,
@@ -52,6 +75,7 @@ export class ProviderGitlabComponent {
private injector: Injector,
private _location: Location,
private breadcrumbService: BreadcrumbService,
private nextSvc: ProviderNextService,
) {
this.form = new FormGroup({
name: new FormControl('', []),
@@ -111,6 +135,10 @@ export class ProviderGitlabComponent {
});
}
public activate() {
this.nextSvc.activate(this.id$, this.justActivated$, this.service$);
}
private getData(id: string): void {
const req =
this.serviceType === PolicyComponentServiceType.ADMIN
@@ -134,7 +162,7 @@ export class ProviderGitlabComponent {
}
public submitForm(): void {
this.provider ? this.updateGitlabProvider() : this.addGitlabProvider();
this.provider || this.justCreated$.value ? this.updateGitlabProvider() : this.addGitlabProvider();
}
public addGitlabProvider(): void {
@@ -152,11 +180,9 @@ export class ProviderGitlabComponent {
this.loading = true;
this.service
.addGitLabProvider(req)
.then((idp) => {
setTimeout(() => {
this.loading = false;
this.close();
}, 2000);
.then((addedIDP) => {
this.justCreated$.next(addedIDP.id);
this.loading = false;
})
.catch((error) => {
this.toast.showError(error);
@@ -165,12 +191,12 @@ export class ProviderGitlabComponent {
}
public updateGitlabProvider(): void {
if (this.provider) {
if (this.provider || this.justCreated$.value) {
const req =
this.serviceType === PolicyComponentServiceType.MGMT
? new MgmtUpdateGitLabProviderRequest()
: new AdminUpdateGitLabProviderRequest();
req.setId(this.provider.id);
req.setId(this.provider?.id || this.justCreated$.value);
req.setName(this.name?.value);
req.setClientId(this.clientId?.value);
req.setScopesList(this.scopesList?.value);
@@ -6,9 +6,29 @@
<div class="title-row">
<img class="idp-logo" src="./assets/images/idp/google.png" alt="google" />
<h1>{{ 'IDP.CREATE.GOOGLE.TITLE' | translate }}</h1>
<ng-container *ngIf="exists$ | async">
<div
*ngIf="{ isNotActive: (activateLink$ | async) } as idp"
class="cnsl-state-dot"
[matTooltip]="(idp.isNotActive ? 'IDP.STATES.2' : 'IDP.STATES.1') | translate"
[ngClass]="{ active: !idp.isNotActive, inactive: !!idp.isNotActive }"
></div>
</ng-container>
<mat-spinner diameter="25" *ngIf="loading" color="primary"></mat-spinner>
</div>
<cnsl-provider-next
[configureProvider]="(exists$ | async) === false"
[configureTitle]="'DESCRIPTIONS.SETTINGS.IDPS.CALLBACK.TITLE' | translate: { provider: 'Google' }"
[configureDescription]="'DESCRIPTIONS.SETTINGS.IDPS.CALLBACK.DESCRIPTION' | translate: { provider: 'Google' }"
configureLink="https://zitadel.com/docs/guides/integrate/identity-providers/google#google-configuration"
[autofillLink]="autofillLink$ | async"
[activateLink]="activateLink$ | async"
[copyUrls]="copyUrls$ | async"
[expanded]="!!(expandWhatNow$ | async)"
(activate)="activate()"
></cnsl-provider-next>
<p class="identity-provider-desc cnsl-secondary-text">
{{ !provider ? ('IDP.CREATE.GOOGLE.DESCRIPTION' | translate) : ('IDP.DETAIL.DESCRIPTION' | translate) }}
</p>
@@ -88,8 +108,9 @@
[disabled]="form.invalid || form.disabled"
type="submit"
>
<span *ngIf="id">{{ 'ACTIONS.SAVE' | translate }}</span>
<span *ngIf="!id">{{ 'ACTIONS.CREATE' | translate }}</span>
<span *ngIf="{ exists: (exists$ | async) } as idp">{{
(!!idp.exists ? 'ACTIONS.SAVE' : 'ACTIONS.CREATE') | translate
}}</span>
</button>
</div>
</form>
@@ -4,7 +4,7 @@ import { Component, Injector, Type } from '@angular/core';
import { AbstractControl, FormControl, FormGroup } from '@angular/forms';
import { MatChipInputEvent } from '@angular/material/chips';
import { ActivatedRoute } from '@angular/router';
import { take } from 'rxjs';
import { BehaviorSubject, take } from 'rxjs';
import {
AddGoogleProviderRequest as AdminAddGoogleProviderRequest,
GetProviderByIDRequest as AdminGetProviderByIDRequest,
@@ -24,6 +24,7 @@ import { ToastService } from 'src/app/services/toast.service';
import { requiredValidator } from '../../form-field/validators/validators';
import { PolicyComponentServiceType } from '../../policies/policy-component-types.enum';
import { ProviderNextService } from '../provider-next/provider-next.service';
@Component({
selector: 'cnsl-provider-google',
@@ -32,8 +33,11 @@ import { PolicyComponentServiceType } from '../../policies/policy-component-type
export class ProviderGoogleComponent {
public showOptional: boolean = false;
public options: Options = new Options().setIsCreationAllowed(true).setIsLinkingAllowed(true);
// DEPRECATED: use id$ instead
public id: string | null = '';
// DEPRECATED: assert service$ instead
public serviceType: PolicyComponentServiceType = PolicyComponentServiceType.MGMT;
// DEPRECATED: use service$ instead
private service!: ManagementService | AdminService;
public readonly separatorKeysCodes: number[] = [ENTER, COMMA, SPACE];
@@ -45,6 +49,25 @@ export class ProviderGoogleComponent {
public provider?: Provider.AsObject;
public updateClientSecret: boolean = false;
public justCreated$: BehaviorSubject<string> = new BehaviorSubject<string>('');
public justActivated$ = new BehaviorSubject<boolean>(false);
private service$ = this.nextSvc.service(this.route.data);
private id$ = this.nextSvc.id(this.route.paramMap, this.justCreated$);
public exists$ = this.nextSvc.exists(this.id$);
public autofillLink$ = this.nextSvc.autofillLink(
this.id$,
`https://zitadel.com/docs/guides/integrate/identity-providers/additional-information`,
);
public activateLink$ = this.nextSvc.activateLink(
this.id$,
this.justActivated$,
'https://zitadel.com/docs/guides/integrate/identity-providers/google#activate-idp',
this.service$,
);
public expandWhatNow$ = this.nextSvc.expandWhatNow(this.id$, this.activateLink$, this.justCreated$);
public copyUrls$ = this.nextSvc.callbackUrls();
constructor(
private authService: GrpcAuthService,
private route: ActivatedRoute,
@@ -52,6 +75,7 @@ export class ProviderGoogleComponent {
private injector: Injector,
private _location: Location,
private breadcrumbService: BreadcrumbService,
private nextSvc: ProviderNextService,
) {
this.form = new FormGroup({
name: new FormControl('', []),
@@ -59,7 +83,6 @@ export class ProviderGoogleComponent {
clientSecret: new FormControl('', [requiredValidator]),
scopesList: new FormControl(['openid', 'profile', 'email'], []),
});
this.authService
.isAllowed(
this.serviceType === PolicyComponentServiceType.ADMIN
@@ -102,7 +125,6 @@ export class ProviderGoogleComponent {
this.breadcrumbService.setBreadcrumb([iamBread]);
break;
}
this.id = this.route.snapshot.paramMap.get('id');
if (this.id) {
this.clientSecret?.setValidators([]);
@@ -111,6 +133,10 @@ export class ProviderGoogleComponent {
});
}
public activate() {
this.nextSvc.activate(this.id$, this.justActivated$, this.service$);
}
private getData(id: string): void {
const req =
this.serviceType === PolicyComponentServiceType.ADMIN
@@ -134,7 +160,7 @@ export class ProviderGoogleComponent {
}
public submitForm(): void {
this.provider ? this.updateGoogleProvider() : this.addGoogleProvider();
this.provider || this.justCreated$.value ? this.updateGoogleProvider() : this.addGoogleProvider();
}
public addGoogleProvider(): void {
@@ -152,11 +178,9 @@ export class ProviderGoogleComponent {
this.loading = true;
this.service
.addGoogleProvider(req)
.then((idp) => {
setTimeout(() => {
this.loading = false;
this.close();
}, 2000);
.then((addedIDP) => {
this.justCreated$.next(addedIDP.id);
this.loading = false;
})
.catch((error) => {
this.toast.showError(error);
@@ -165,10 +189,10 @@ export class ProviderGoogleComponent {
}
public updateGoogleProvider(): void {
if (this.provider) {
if (this.provider || this.justCreated$.value) {
if (this.serviceType === PolicyComponentServiceType.MGMT) {
const req = new MgmtUpdateGoogleProviderRequest();
req.setId(this.provider.id);
req.setId(this.provider?.id || this.justCreated$.value);
req.setName(this.name?.value);
req.setClientId(this.clientId?.value);
req.setScopesList(this.scopesList?.value);
@@ -193,7 +217,7 @@ export class ProviderGoogleComponent {
});
} else if (PolicyComponentServiceType.ADMIN) {
const req = new AdminUpdateGoogleProviderRequest();
req.setId(this.provider.id);
req.setId(this.provider?.id || this.justCreated$.value);
req.setName(this.name?.value);
req.setClientId(this.clientId?.value);
req.setScopesList(this.scopesList?.value);
@@ -6,9 +6,28 @@
<div class="title-row">
<mat-icon class="idp-logo" svgIcon="mdi_jwt" alt="jwt" />
<h1>{{ 'IDP.CREATE.JWT.TITLE' | translate }}</h1>
<ng-container *ngIf="exists$ | async">
<div
*ngIf="{ isNotActive: (activateLink$ | async) } as idp"
class="cnsl-state-dot"
[matTooltip]="(idp.isNotActive ? 'IDP.STATES.2' : 'IDP.STATES.1') | translate"
[ngClass]="{ active: !idp.isNotActive, inactive: !!idp.isNotActive }"
></div>
</ng-container>
<mat-spinner diameter="25" *ngIf="loading" color="primary"></mat-spinner>
</div>
<cnsl-provider-next
[configureProvider]="(exists$ | async) === false"
[configureTitle]="'DESCRIPTIONS.SETTINGS.IDPS.JWT.TITLE' | translate"
[configureDescription]="'DESCRIPTIONS.SETTINGS.IDPS.JWT.DESCRIPTION' | translate"
configureLink="https://zitadel.com/docs/guides/integrate/identity-providers/jwt-idp"
[autofillLink]="autofillLink$ | async"
[activateLink]="activateLink$ | async"
[expanded]="!!(expandWhatNow$ | async)"
(activate)="activate()"
></cnsl-provider-next>
<p class="identity-provider-desc cnsl-secondary-text">{{ 'IDP.CREATE.JWT.DESCRIPTION' | translate }}</p>
<form [formGroup]="form" (ngSubmit)="submitForm()">
@@ -57,8 +76,9 @@
[disabled]="form.invalid || form.disabled"
type="submit"
>
<span *ngIf="id">{{ 'ACTIONS.SAVE' | translate }}</span>
<span *ngIf="!id">{{ 'ACTIONS.CREATE' | translate }}</span>
<span *ngIf="{ exists: (exists$ | async) } as idp">{{
(!!idp.exists ? 'ACTIONS.SAVE' : 'ACTIONS.CREATE') | translate
}}</span>
</button>
</div>
</form>
@@ -23,6 +23,8 @@ import { ToastService } from 'src/app/services/toast.service';
import { requiredValidator } from '../../form-field/validators/validators';
import { PolicyComponentServiceType } from '../../policies/policy-component-types.enum';
import { BehaviorSubject } from 'rxjs';
import { ProviderNextService } from '../provider-next/provider-next.service';
@Component({
selector: 'cnsl-provider-jwt',
@@ -32,8 +34,11 @@ export class ProviderJWTComponent {
public showOptional: boolean = false;
public options: Options = new Options().setIsCreationAllowed(true).setIsLinkingAllowed(true);
// DEPRECATED: use id$ instead
public id: string | null = '';
// DEPRECATED: assert service$ instead
public serviceType: PolicyComponentServiceType = PolicyComponentServiceType.MGMT;
// DEPRECATED: use service$ instead
private service!: ManagementService | AdminService;
public readonly separatorKeysCodes: number[] = [ENTER, COMMA, SPACE];
@@ -42,6 +47,24 @@ export class ProviderJWTComponent {
public provider?: Provider.AsObject;
public justCreated$: BehaviorSubject<string> = new BehaviorSubject<string>('');
public justActivated$ = new BehaviorSubject<boolean>(false);
private service$ = this.nextSvc.service(this.route.data);
private id$ = this.nextSvc.id(this.route.paramMap, this.justCreated$);
public exists$ = this.nextSvc.exists(this.id$);
public autofillLink$ = this.nextSvc.autofillLink(
this.id$,
`https://zitadel.com/docs/guides/integrate/identity-providers/additional-information`,
);
public activateLink$ = this.nextSvc.activateLink(
this.id$,
this.justActivated$,
'https://zitadel.com/docs/guides/integrate/identity-providers/okta-oidc#activate-idp',
this.service$,
);
public expandWhatNow$ = this.nextSvc.expandWhatNow(this.id$, this.activateLink$, this.justCreated$);
constructor(
private authService: GrpcAuthService,
private route: ActivatedRoute,
@@ -49,6 +72,7 @@ export class ProviderJWTComponent {
private injector: Injector,
private _location: Location,
breadcrumbService: BreadcrumbService,
private nextSvc: ProviderNextService,
) {
this.route.data.pipe(take(1)).subscribe((data) => {
this.serviceType = data['serviceType'];
@@ -108,6 +132,10 @@ export class ProviderJWTComponent {
});
}
public activate() {
this.nextSvc.activate(this.id$, this.justActivated$, this.service$);
}
private getData(id: string): void {
const req =
this.serviceType === PolicyComponentServiceType.ADMIN
@@ -131,7 +159,7 @@ export class ProviderJWTComponent {
}
public submitForm(): void {
this.provider ? this.updateJWTProvider() : this.addJWTProvider();
this.provider || this.justCreated$.value ? this.updateJWTProvider() : this.addJWTProvider();
}
public addJWTProvider(): void {
@@ -149,11 +177,9 @@ export class ProviderJWTComponent {
this.loading = true;
this.service
.addJWTProvider(req)
.then((idp) => {
setTimeout(() => {
this.loading = false;
this.close();
}, 2000);
.then((addedIDP) => {
this.justCreated$.next(addedIDP.id);
this.loading = false;
})
.catch((error) => {
this.toast.showError(error);
@@ -162,12 +188,12 @@ export class ProviderJWTComponent {
}
public updateJWTProvider(): void {
if (this.provider) {
if (this.provider || this.justCreated$.value) {
const req =
this.serviceType === PolicyComponentServiceType.MGMT
? new MgmtUpdateJWTProviderRequest()
: new AdminUpdateJWTProviderRequest();
req.setId(this.provider.id);
req.setId(this.provider?.id || this.justCreated$.value);
req.setName(this.name?.value);
req.setHeaderName(this.headerName?.value);
req.setIssuer(this.issuer?.value);
@@ -6,9 +6,27 @@
<div class="title-row">
<i class="idp-icon las la-building"></i>
<h1>{{ 'IDP.CREATE.LDAP.TITLE' | translate }}</h1>
<ng-container *ngIf="exists$ | async">
<div
*ngIf="{ isNotActive: (activateLink$ | async) } as idp"
class="cnsl-state-dot"
[matTooltip]="(idp.isNotActive ? 'IDP.STATES.2' : 'IDP.STATES.1') | translate"
[ngClass]="{ active: !idp.isNotActive, inactive: !!idp.isNotActive }"
></div>
</ng-container>
<mat-spinner diameter="25" *ngIf="loading" color="primary"></mat-spinner>
</div>
<cnsl-provider-next
[configureProvider]="(exists$ | async) === false"
[configureTitle]="'DESCRIPTIONS.SETTINGS.IDPS.LDAP.TITLE' | translate: { provider: 'Google' }"
[configureDescription]="'DESCRIPTIONS.SETTINGS.IDPS.LDAP.DESCRIPTION' | translate: { provider: 'Google' }"
configureLink="https://zitadel.com/docs/guides/integrate/identity-providers/ldap"
[activateLink]="activateLink$ | async"
[expanded]="!!(expandWhatNow$ | async)"
(activate)="activate()"
></cnsl-provider-next>
<p class="identity-provider-desc cnsl-secondary-text">
{{ !provider ? ('IDP.CREATE.LDAP.DESCRIPTION' | translate) : ('IDP.DETAIL.DESCRIPTION' | translate) }}
</p>
@@ -118,8 +136,9 @@
[disabled]="!form.valid || !attributes.toObject().idAttribute || form.disabled"
type="submit"
>
<span *ngIf="id">{{ 'ACTIONS.SAVE' | translate }}</span>
<span *ngIf="!id">{{ 'ACTIONS.CREATE' | translate }}</span>
<span *ngIf="{ exists: (exists$ | async) } as idp">{{
(!!idp.exists ? 'ACTIONS.SAVE' : 'ACTIONS.CREATE') | translate
}}</span>
</button>
</div>
</form>
@@ -3,7 +3,7 @@ import { Component, Injector, Type } from '@angular/core';
import { AbstractControl, FormControl, FormGroup } from '@angular/forms';
import { ActivatedRoute } from '@angular/router';
import { Duration } from 'google-protobuf/google/protobuf/duration_pb';
import { take } from 'rxjs';
import { BehaviorSubject, take } from 'rxjs';
import {
AddLDAPProviderRequest as AdminAddLDAPProviderRequest,
GetProviderByIDRequest as AdminGetProviderByIDRequest,
@@ -23,6 +23,7 @@ import { ToastService } from 'src/app/services/toast.service';
import { minArrayLengthValidator, requiredValidator } from '../../form-field/validators/validators';
import { PolicyComponentServiceType } from '../../policies/policy-component-types.enum';
import { ProviderNextService } from '../provider-next/provider-next.service';
@Component({
selector: 'cnsl-provider-ldap',
@@ -33,8 +34,11 @@ export class ProviderLDAPComponent {
public showOptional: boolean = false;
public options: Options = new Options().setIsCreationAllowed(true).setIsLinkingAllowed(true);
public attributes: LDAPAttributes = new LDAPAttributes();
// DEPRECATED: use id$ instead
public id: string | null = '';
// DEPRECATED: assert service$ instead
public serviceType: PolicyComponentServiceType = PolicyComponentServiceType.MGMT;
// DEPRECATED: use service$ instead
private service!: ManagementService | AdminService;
public form!: FormGroup;
@@ -43,6 +47,20 @@ export class ProviderLDAPComponent {
public provider?: Provider.AsObject;
public justCreated$: BehaviorSubject<string> = new BehaviorSubject<string>('');
public justActivated$ = new BehaviorSubject<boolean>(false);
private service$ = this.nextSvc.service(this.route.data);
private id$ = this.nextSvc.id(this.route.paramMap, this.justCreated$);
public exists$ = this.nextSvc.exists(this.id$);
public activateLink$ = this.nextSvc.activateLink(
this.id$,
this.justActivated$,
'https://zitadel.com/docs/guides/integrate/identity-providers/google#activate-idp',
this.service$,
);
public expandWhatNow$ = this.nextSvc.expandWhatNow(this.id$, this.activateLink$, this.justCreated$);
constructor(
private authService: GrpcAuthService,
private route: ActivatedRoute,
@@ -50,6 +68,7 @@ export class ProviderLDAPComponent {
private injector: Injector,
private _location: Location,
private breadcrumbService: BreadcrumbService,
private nextSvc: ProviderNextService,
) {
this.form = new FormGroup({
name: new FormControl('', [requiredValidator]),
@@ -116,6 +135,10 @@ export class ProviderLDAPComponent {
});
}
public activate() {
this.nextSvc.activate(this.id$, this.justActivated$, this.service$);
}
private getData(id: string): void {
const req =
this.serviceType === PolicyComponentServiceType.ADMIN
@@ -179,11 +202,9 @@ export class ProviderLDAPComponent {
this.loading = true;
(this.service as ManagementService)
.addLDAPProvider(req)
.then((idp) => {
setTimeout(() => {
this.loading = false;
this.close();
}, 2000);
.then((addedIDP) => {
this.justCreated$.next(addedIDP.id);
this.loading = false;
})
.catch((error) => {
this.toast.showError(error);
@@ -0,0 +1,77 @@
<div class="cnsl-provider-next">
<cnsl-card
[title]="'DESCRIPTIONS.SETTINGS.IDPS.NEXT' | translate"
[expanded]="!!expanded"
*ngIf="configureProvider || autofillLink || activateLink"
>
<cnsl-info-section *ngIf="activateLink">
<div class="title-row">
<div class="left">
<h2 class="title">{{ 'DESCRIPTIONS.SETTINGS.IDPS.ACTIVATE.TITLE' | translate }}</h2>
<div>
<a
mat-icon-button
*ngIf="activateLink"
card-actions
mat-icon-button
[href]="activateLink"
rel="noreferrer"
target="_blank"
>
<mat-icon class="next-icon">info_outline</mat-icon>
</a>
</div>
</div>
<div class="right">
<button color="primary" mat-raised-button class="continue-button" (click)="activate.emit()">
{{ 'ACTIONS.ACTIVATE' | translate }}
</button>
</div>
</div>
<p class="cnsl-secondary-text description">{{ 'DESCRIPTIONS.SETTINGS.IDPS.ACTIVATE.DESCRIPTION' | translate }}</p>
</cnsl-info-section>
<div class="section" *ngIf="configureProvider">
<div class="title-row">
<div class="left">
<h2 class="title">{{ configureTitle }}</h2>
<a
mat-icon-button
*ngIf="configureLink"
card-actions
mat-icon-button
[href]="configureLink"
rel="noreferrer"
target="_blank"
>
<mat-icon class="next-icon">info_outline</mat-icon>
</a>
</div>
</div>
<p class="cnsl-secondary-text description">{{ configureDescription }}</p>
<cnsl-copy-row *ngFor="let row of copyUrls" [label]="row.label" [value]="row.url" labelMinWidth="200px">
<a *ngIf="row.downloadable" class="download-button" mat-stroked-button [href]="row.url" download>{{
'ACTIONS.DOWNLOAD' | translate
}}</a>
</cnsl-copy-row>
</div>
<div class="section" *ngIf="autofillLink">
<div class="title-row">
<div class="left">
<h2 class="title">{{ 'DESCRIPTIONS.SETTINGS.IDPS.AUTOFILL.TITLE' | translate }}</h2>
<a
mat-icon-button
*ngIf="autofillLink"
card-actions
mat-icon-button
[href]="autofillLink"
rel="noreferrer"
target="_blank"
>
<mat-icon class="next-icon">info_outline</mat-icon>
</a>
</div>
</div>
<p class="cnsl-secondary-text description">{{ 'DESCRIPTIONS.SETTINGS.IDPS.AUTOFILL.DESCRIPTION' | translate }}</p>
</div>
</cnsl-card>
</div>
@@ -0,0 +1,69 @@
.cnsl-provider-next {
.section {
background-color: #8795a110;
border: 1px solid #8795a160;
border-radius: 4px;
padding: 0.25rem 1rem;
margin: 0.25rem 0rem;
.box {
display: flex;
flex-direction: row;
justify-content: space-between;
.right {
flex-shrink: 0;
}
}
}
.title-row {
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
.left {
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
}
.right {
flex-shrink: 0;
}
.title {
margin: 0;
}
.next-icon {
font-size: 1.2rem;
height: 1.2rem;
width: 1.2rem;
}
}
.description {
margin-block-start: 0;
font-size: 14px;
}
.download-button {
margin-left: 1rem;
}
}
@mixin provider-next-theme($theme) {
$foreground: map-get($theme, foreground);
$button-text-color: map-get($foreground, text);
$button-disabled-text-color: map-get($foreground, disabled-button);
.cnsl-provider-next {
.section {
color: $button-text-color;
&[disabled] {
color: $button-disabled-text-color;
}
}
}
}
@@ -0,0 +1,26 @@
import { Component, EventEmitter, Input, Output } from '@angular/core';
export interface CopyUrl {
label: string;
url: string;
downloadable?: boolean;
}
@Component({
selector: 'cnsl-provider-next',
templateUrl: './provider-next.component.html',
styleUrls: ['./provider-next.component.scss'],
})
export class ProviderNextComponent {
@Input() copyUrls?: CopyUrl[] | null;
@Input() autofillLink?: string | null;
@Input() activateLink?: string | null;
@Input() configureProvider?: boolean | null;
@Input() configureTitle?: string;
@Input() configureDescription?: string;
@Input() configureLink?: string;
@Input() expanded?: boolean;
@Output() activate = new EventEmitter<void>();
constructor() {}
}
@@ -0,0 +1,135 @@
import { Injectable, Injector, Type } from '@angular/core';
import { BehaviorSubject, combineLatestWith, from, Observable, of, shareReplay, switchMap, take } from 'rxjs';
import { filter, map, tap } from 'rxjs/operators';
import { EnvironmentService } from '../../../services/environment.service';
import { CopyUrl } from './provider-next.component';
import { ManagementService } from '../../../services/mgmt.service';
import { AdminService } from '../../../services/admin.service';
import { IDPOwnerType } from '../../../proto/generated/zitadel/idp_pb';
import { ToastService } from '../../../services/toast.service';
import { Data, ParamMap } from '@angular/router';
import { ActivateIdpService } from '../../../services/activate-idp.service';
import { PolicyComponentServiceType } from '../../policies/policy-component-types.enum';
@Injectable({
providedIn: 'root',
})
export class ProviderNextService {
constructor(
private env: EnvironmentService,
private toast: ToastService,
private addIdpSvc: ActivateIdpService,
private injector: Injector,
) {}
service(routeData: Observable<Data>): Observable<ManagementService | AdminService> {
return routeData.pipe(
map((data) => {
switch (data['serviceType']) {
case PolicyComponentServiceType.MGMT:
return this.injector.get(ManagementService as Type<ManagementService>);
case PolicyComponentServiceType.ADMIN:
return this.injector.get(AdminService as Type<AdminService>);
default:
throw new Error('Unknown Service Type');
}
}),
shareReplay(1),
);
}
id(paramMap: Observable<ParamMap>, justCreated$: Observable<string>): Observable<string | null> {
return paramMap.pipe(
// The ID observable should also emit when the IDP was just created
combineLatestWith(justCreated$),
map(([params, created]) => (created ? created : params.get('id'))),
shareReplay(1),
);
}
exists(id$: Observable<string | null>): Observable<boolean> {
return id$.pipe(
map((id) => !!id),
shareReplay(1),
);
}
autofillLink(id$: Observable<string | null>, link: string): Observable<string> {
return id$.pipe(
filter((id) => !!id),
map(() => link),
shareReplay(1),
);
}
activateLink(
id$: Observable<string | null>,
justActivated$: Observable<boolean>,
link: string,
service$: Observable<ManagementService | AdminService>,
): Observable<string> {
return id$.pipe(
combineLatestWith(justActivated$, service$),
// Because we also want to emit when the IDP is not active, we return an empty string if the IDP does not exist
switchMap(([id, activated, service]) =>
(!id || activated
? of(false)
: from(service.getLoginPolicy()).pipe(map((policy) => !policy.policy?.idpsList.find((idp) => idp.idpId === id)))
).pipe(map((show) => (!show ? '' : link))),
),
shareReplay(1),
);
}
callbackUrls(): Observable<CopyUrl[]> {
return this.env.env.pipe(
map((env) => [
{
label: 'ZITADEL Callback URL',
url: `${env.issuer}/ui/login/login/externalidp/callback`,
},
]),
);
}
expandWhatNow(
id$: Observable<string | null>,
activateLink$: Observable<string>,
justCreated$: Observable<string>,
): Observable<boolean> {
return id$.pipe(
combineLatestWith(activateLink$, justCreated$),
map(([id, activateLink, created]) => !id || activateLink || created),
map((expand) => !!expand),
shareReplay(1),
);
}
activate(
id$: Observable<string | null>,
emitActivated$: BehaviorSubject<boolean>,
service$: Observable<ManagementService | AdminService>,
): void {
id$
.pipe(
combineLatestWith(service$),
take(1),
switchMap(([id, service]) => {
if (!id) {
throw new Error('No ID');
}
return this.addIdpSvc.activateIdp(
service,
id,
service instanceof AdminService ? IDPOwnerType.IDP_OWNER_TYPE_SYSTEM : IDPOwnerType.IDP_OWNER_TYPE_ORG,
);
}),
)
.subscribe({
next: () => {
this.toast.showInfo('POLICY.LOGIN_POLICY.PROVIDER_ADDED', true);
emitActivated$.next(true);
},
error: (error) => this.toast.showError(error),
});
}
}
@@ -5,11 +5,25 @@
<div class="identity-provider-create-content">
<div class="title-row">
<img class="idp-logo" src="./assets/images/idp/oauth.svg" alt="oauth" />
<h1>{{ 'IDP.CREATE.OAUTH.TITLE' | translate }}</h1>
<ng-container *ngIf="exists$ | async">
<div
*ngIf="{ isNotActive: (activateLink$ | async) } as idp"
class="cnsl-state-dot"
[matTooltip]="(idp.isNotActive ? 'IDP.STATES.2' : 'IDP.STATES.1') | translate"
[ngClass]="{ active: !idp.isNotActive, inactive: !!idp.isNotActive }"
></div>
</ng-container>
<mat-spinner diameter="25" *ngIf="loading" color="primary"></mat-spinner>
</div>
<cnsl-provider-next
[autofillLink]="autofillLink$ | async"
[activateLink]="activateLink$ | async"
[expanded]="!!(expandWhatNow$ | async)"
(activate)="activate()"
></cnsl-provider-next>
<p class="identity-provider-desc cnsl-secondary-text">{{ 'IDP.CREATE.OAUTH.DESCRIPTION' | translate }}</p>
<form [formGroup]="form" (ngSubmit)="submitForm()">
@@ -107,8 +121,9 @@
[disabled]="form.invalid || form.disabled"
type="submit"
>
<span *ngIf="id">{{ 'ACTIONS.SAVE' | translate }}</span>
<span *ngIf="!id">{{ 'ACTIONS.CREATE' | translate }}</span>
<span *ngIf="{ exists: (exists$ | async) } as idp">{{
(!!idp.exists ? 'ACTIONS.SAVE' : 'ACTIONS.CREATE') | translate
}}</span>
</button>
</div>
</form>
@@ -4,7 +4,7 @@ import { Component, Injector, Type } from '@angular/core';
import { AbstractControl, UntypedFormControl, UntypedFormGroup } from '@angular/forms';
import { MatChipInputEvent } from '@angular/material/chips';
import { ActivatedRoute } from '@angular/router';
import { take } from 'rxjs';
import { BehaviorSubject, take } from 'rxjs';
import {
AddGenericOAuthProviderRequest as AdminAddGenericOAuthProviderRequest,
GetProviderByIDRequest as AdminGetProviderByIDRequest,
@@ -24,6 +24,7 @@ import { ToastService } from 'src/app/services/toast.service';
import { requiredValidator } from '../../form-field/validators/validators';
import { PolicyComponentServiceType } from '../../policies/policy-component-types.enum';
import { ProviderNextService } from '../provider-next/provider-next.service';
@Component({
selector: 'cnsl-provider-oauth',
@@ -33,9 +34,12 @@ export class ProviderOAuthComponent {
public showOptional: boolean = false;
public options: Options = new Options().setIsCreationAllowed(true).setIsLinkingAllowed(true);
// DEPRECATED: use id$ instead
public id: string | null = '';
public updateClientSecret: boolean = false;
// DEPRECATED: assert service$ instead
public serviceType: PolicyComponentServiceType = PolicyComponentServiceType.MGMT;
// DEPRECATED: use service$ instead
private service!: ManagementService | AdminService;
public readonly separatorKeysCodes: number[] = [ENTER, COMMA, SPACE];
public form!: UntypedFormGroup;
@@ -44,6 +48,24 @@ export class ProviderOAuthComponent {
public provider?: Provider.AsObject;
public justCreated$: BehaviorSubject<string> = new BehaviorSubject<string>('');
public justActivated$ = new BehaviorSubject<boolean>(false);
private service$ = this.nextSvc.service(this.route.data);
private id$ = this.nextSvc.id(this.route.paramMap, this.justCreated$);
public exists$ = this.nextSvc.exists(this.id$);
public autofillLink$ = this.nextSvc.autofillLink(
this.id$,
`https://zitadel.com/docs/guides/integrate/identity-providers/additional-information`,
);
public activateLink$ = this.nextSvc.activateLink(
this.id$,
this.justActivated$,
'https://zitadel.com/docs/guides/integrate/identity-providers/okta-oidc#activate-idp',
this.service$,
);
public expandWhatNow$ = this.nextSvc.expandWhatNow(this.id$, this.activateLink$, this.justCreated$);
constructor(
private authService: GrpcAuthService,
private route: ActivatedRoute,
@@ -51,6 +73,7 @@ export class ProviderOAuthComponent {
private injector: Injector,
private _location: Location,
breadcrumbService: BreadcrumbService,
private nextSvc: ProviderNextService,
) {
this.form = new UntypedFormGroup({
name: new UntypedFormControl('', [requiredValidator]),
@@ -114,6 +137,10 @@ export class ProviderOAuthComponent {
});
}
public activate() {
this.nextSvc.activate(this.id$, this.justActivated$, this.service$);
}
private getData(id: string): void {
this.loading = true;
const req =
@@ -138,7 +165,7 @@ export class ProviderOAuthComponent {
}
public submitForm(): void {
this.provider ? this.updateGenericOAuthProvider() : this.addGenericOAuthProvider();
this.provider || this.justCreated$.value ? this.updateGenericOAuthProvider() : this.addGenericOAuthProvider();
}
public addGenericOAuthProvider(): void {
@@ -160,11 +187,9 @@ export class ProviderOAuthComponent {
this.loading = true;
this.service
.addGenericOAuthProvider(req)
.then((idp) => {
setTimeout(() => {
this.loading = false;
this.close();
}, 2000);
.then((addedIDP) => {
this.justCreated$.next(addedIDP.id);
this.loading = false;
})
.catch((error) => {
this.toast.showError(error);
@@ -173,12 +198,12 @@ export class ProviderOAuthComponent {
}
public updateGenericOAuthProvider(): void {
if (this.provider) {
if (this.provider || this.justCreated$.value) {
const req =
this.serviceType === PolicyComponentServiceType.MGMT
? new MgmtUpdateGenericOAuthProviderRequest()
: new AdminUpdateGenericOAuthProviderRequest();
req.setId(this.provider.id);
req.setId(this.provider?.id || this.justCreated$.value);
req.setName(this.name?.value);
req.setAuthorizationEndpoint(this.authorizationEndpoint?.value);
req.setIdAttribute(this.idAttribute?.value);
@@ -6,9 +6,29 @@
<div class="title-row">
<mat-icon class="idp-logo" svgIcon="mdi_openid" alt="openid" />
<h1>{{ 'IDP.CREATE.OIDC.TITLE' | translate }}</h1>
<ng-container *ngIf="exists$ | async">
<div
*ngIf="{ isNotActive: (activateLink$ | async) } as idp"
class="cnsl-state-dot"
[matTooltip]="(idp.isNotActive ? 'IDP.STATES.2' : 'IDP.STATES.1') | translate"
[ngClass]="{ active: !idp.isNotActive, inactive: !!idp.isNotActive }"
></div>
</ng-container>
<mat-spinner diameter="25" *ngIf="loading" color="primary"></mat-spinner>
</div>
<cnsl-provider-next
[configureProvider]="(exists$ | async) === false"
[configureTitle]="'DESCRIPTIONS.SETTINGS.IDPS.CALLBACK.TITLE' | translate: { provider: 'OIDC' }"
[configureDescription]="'DESCRIPTIONS.SETTINGS.IDPS.CALLBACK.DESCRIPTION' | translate: { provider: 'OIDC' }"
configureLink="https://zitadel.com/docs/guides/integrate/identity-providers/google#google-configuration"
[autofillLink]="autofillLink$ | async"
[activateLink]="activateLink$ | async"
[copyUrls]="copyUrls$ | async"
[expanded]="!!(expandWhatNow$ | async)"
(activate)="activate()"
></cnsl-provider-next>
<p class="identity-provider-desc cnsl-secondary-text">{{ 'IDP.CREATE.OIDC.DESCRIPTION' | translate }}</p>
<form [formGroup]="form" (ngSubmit)="submitForm()">
@@ -95,8 +115,9 @@
[disabled]="form.invalid || form.disabled"
type="submit"
>
<span *ngIf="id">{{ 'ACTIONS.SAVE' | translate }}</span>
<span *ngIf="!id">{{ 'ACTIONS.CREATE' | translate }}</span>
<span *ngIf="{ exists: (exists$ | async) } as idp">{{
(!!idp.exists ? 'ACTIONS.SAVE' : 'ACTIONS.CREATE') | translate
}}</span>
</button>
</div>
</form>
@@ -4,7 +4,7 @@ import { Component, Injector, Type } from '@angular/core';
import { AbstractControl, UntypedFormControl, UntypedFormGroup } from '@angular/forms';
import { MatChipInputEvent } from '@angular/material/chips';
import { ActivatedRoute } from '@angular/router';
import { take } from 'rxjs';
import { BehaviorSubject, take } from 'rxjs';
import {
AddGenericOIDCProviderRequest as AdminAddGenericOIDCProviderRequest,
GetProviderByIDRequest as AdminGetProviderByIDRequest,
@@ -23,6 +23,7 @@ import { ToastService } from 'src/app/services/toast.service';
import { requiredValidator } from '../../form-field/validators/validators';
import { PolicyComponentServiceType } from '../../policies/policy-component-types.enum';
import { ProviderNextService } from '../provider-next/provider-next.service';
@Component({
selector: 'cnsl-provider-oidc',
@@ -33,9 +34,12 @@ export class ProviderOIDCComponent {
public showOptional: boolean = false;
public options: Options = new Options().setIsCreationAllowed(true).setIsLinkingAllowed(true);
// DEPRECATED: use id$ instead
public id: string | null = '';
public updateClientSecret: boolean = false;
// DEPRECATED: assert service$ instead
public serviceType: PolicyComponentServiceType = PolicyComponentServiceType.MGMT;
// DEPRECATED: use service$ instead
private service!: ManagementService | AdminService;
public readonly separatorKeysCodes: number[] = [ENTER, COMMA, SPACE];
public form!: UntypedFormGroup;
@@ -44,12 +48,32 @@ export class ProviderOIDCComponent {
public provider?: Provider.AsObject;
public justCreated$: BehaviorSubject<string> = new BehaviorSubject<string>('');
public justActivated$ = new BehaviorSubject<boolean>(false);
private service$ = this.nextSvc.service(this.route.data);
private id$ = this.nextSvc.id(this.route.paramMap, this.justCreated$);
public exists$ = this.nextSvc.exists(this.id$);
public autofillLink$ = this.nextSvc.autofillLink(
this.id$,
`https://zitadel.com/docs/guides/integrate/identity-providers/additional-information`,
);
public activateLink$ = this.nextSvc.activateLink(
this.id$,
this.justActivated$,
'https://zitadel.com/docs/guides/integrate/identity-providers/okta-oidc#activate-idp',
this.service$,
);
public expandWhatNow$ = this.nextSvc.expandWhatNow(this.id$, this.activateLink$, this.justCreated$);
public copyUrls$ = this.nextSvc.callbackUrls();
constructor(
private route: ActivatedRoute,
private toast: ToastService,
private injector: Injector,
private _location: Location,
breadcrumbService: BreadcrumbService,
private nextSvc: ProviderNextService,
) {
this.form = new UntypedFormGroup({
name: new UntypedFormControl('', [requiredValidator]),
@@ -94,6 +118,10 @@ export class ProviderOIDCComponent {
});
}
public activate() {
this.nextSvc.activate(this.id$, this.justActivated$, this.service$);
}
private getData(id: string): void {
this.loading = true;
const req =
@@ -118,7 +146,7 @@ export class ProviderOIDCComponent {
}
public submitForm(): void {
this.provider ? this.updateGenericOIDCProvider() : this.addGenericOIDCProvider();
this.provider || this.justCreated$.value ? this.updateGenericOIDCProvider() : this.addGenericOIDCProvider();
}
public addGenericOIDCProvider(): void {
@@ -138,11 +166,9 @@ export class ProviderOIDCComponent {
this.loading = true;
this.service
.addGenericOIDCProvider(req)
.then((idp) => {
setTimeout(() => {
this.loading = false;
this.close();
}, 2000);
.then((addedIDP) => {
this.justCreated$.next(addedIDP.id);
this.loading = false;
})
.catch((error) => {
this.toast.showError(error);
@@ -151,12 +177,12 @@ export class ProviderOIDCComponent {
}
public updateGenericOIDCProvider(): void {
if (this.provider) {
if (this.provider || this.justCreated$.value) {
const req =
this.serviceType === PolicyComponentServiceType.MGMT
? new MgmtUpdateGenericOIDCProviderRequest()
: new AdminUpdateGenericOIDCProviderRequest();
req.setId(this.provider.id);
req.setId(this.provider?.id || this.justCreated$.value);
req.setName(this.name?.value);
req.setClientId(this.clientId?.value);
req.setClientSecret(this.clientSecret?.value);
@@ -6,9 +6,29 @@
<div class="title-row">
<img class="idp-logo" src="./assets/images/idp/saml-icon.svg" alt="saml" />
<h1>{{ 'IDP.CREATE.SAML.TITLE' | translate }}</h1>
<ng-container *ngIf="exists$ | async">
<div
*ngIf="{ isNotActive: (activateLink$ | async) } as idp"
class="cnsl-state-dot"
[matTooltip]="(idp.isNotActive ? 'IDP.STATES.2' : 'IDP.STATES.1') | translate"
[ngClass]="{ active: !idp.isNotActive, inactive: !!idp.isNotActive }"
></div>
</ng-container>
<mat-spinner diameter="25" *ngIf="loading" color="primary"></mat-spinner>
</div>
<cnsl-provider-next
[configureProvider]="exists$ | async"
[configureTitle]="'DESCRIPTIONS.SETTINGS.IDPS.SAML.TITLE' | translate"
[configureDescription]="'DESCRIPTIONS.SETTINGS.IDPS.SAML.DESCRIPTION' | translate"
configureLink="https://zitadel.com/docs/guides/integrate/identity-providers/mocksaml"
[autofillLink]="autofillLink$ | async"
[activateLink]="activateLink$ | async"
[copyUrls]="copyUrls$ | async"
[expanded]="!!(expandWhatNow$ | async)"
(activate)="activate()"
></cnsl-provider-next>
<p class="identity-provider-desc cnsl-secondary-text">
{{ !provider ? ('IDP.CREATE.SAML.DESCRIPTION' | translate) : ('IDP.DETAIL.DESCRIPTION' | translate) }}
</p>
@@ -55,7 +75,6 @@
(optionsChanged)="options = $event"
></cnsl-provider-options>
</div>
<div class="identity-provider-create-actions">
<button
color="primary"
@@ -64,8 +83,9 @@
[disabled]="form.invalid || form.disabled"
type="submit"
>
<span *ngIf="id">{{ 'ACTIONS.SAVE' | translate }}</span>
<span *ngIf="!id">{{ 'ACTIONS.CREATE' | translate }}</span>
<span *ngIf="{ exists: (exists$ | async) } as idp">{{
(!!idp.exists ? 'ACTIONS.SAVE' : 'ACTIONS.CREATE') | translate
}}</span>
</button>
</div>
</form>
@@ -7,7 +7,7 @@ import { ManagementService } from '../../../services/mgmt.service';
import { AdminService } from '../../../services/admin.service';
import { ToastService } from '../../../services/toast.service';
import { GrpcAuthService } from '../../../services/grpc-auth.service';
import { take } from 'rxjs';
import { BehaviorSubject, shareReplay, switchMap, take } from 'rxjs';
import { ActivatedRoute } from '@angular/router';
import { Breadcrumb, BreadcrumbService, BreadcrumbType } from '../../../services/breadcrumb.service';
import { atLeastOneIsFilled, requiredValidator } from '../../form-field/validators/validators';
@@ -21,6 +21,9 @@ import {
GetProviderByIDRequest as MgmtGetProviderByIDRequest,
UpdateSAMLProviderRequest as MgmtUpdateSAMLProviderRequest,
} from 'src/app/proto/generated/zitadel/management_pb';
import { Environment, EnvironmentService } from '../../../services/environment.service';
import { filter, map } from 'rxjs/operators';
import { ProviderNextService } from '../provider-next/provider-next.service';
@Component({
selector: 'cnsl-provider-saml-sp',
@@ -28,17 +31,67 @@ import {
styleUrls: ['./provider-saml-sp.component.scss'],
})
export class ProviderSamlSpComponent {
// DEPRECATED: use id$ instead
public id: string | null = '';
public loading: boolean = false;
public provider?: Provider.AsObject;
public form!: FormGroup;
public showOptional: boolean = false;
public options: Options = new Options().setIsCreationAllowed(true).setIsLinkingAllowed(true);
// DEPRECATED: assert service$ instead
public serviceType: PolicyComponentServiceType = PolicyComponentServiceType.MGMT;
// DEPRECATED: use service$ instead
private service!: ManagementService | AdminService;
bindingValues: string[] = Object.keys(SAMLBinding);
public justCreated$: BehaviorSubject<string> = new BehaviorSubject<string>('');
public justActivated$ = new BehaviorSubject<boolean>(false);
private service$ = this.nextSvc.service(this.route.data);
private id$ = this.nextSvc.id(this.route.paramMap, this.justCreated$);
public exists$ = this.nextSvc.exists(this.id$);
public autofillLink$ = this.nextSvc.autofillLink(
this.id$,
`https://zitadel.com/docs/guides/integrate/identity-providers/mocksaml#optional-add-zitadel-action-to-autofill-userdata`,
);
public activateLink$ = this.nextSvc.activateLink(
this.id$,
this.justActivated$,
'https://zitadel.com/docs/guides/integrate/identity-providers/mocksaml#activate-idp',
this.service$,
);
public expandWhatNow$ = this.nextSvc.expandWhatNow(this.id$, this.activateLink$, this.justCreated$);
public copyUrls$ = this.id$.pipe(
filter((id) => !!id),
switchMap((id) =>
this.envSvc.env.pipe(
map((environment: Environment) => {
const idpBase = `${environment.issuer}/idps/${id}/saml`;
return [
{
label: 'ZITADEL Metadata',
url: `${idpBase}/metadata`,
downloadable: true,
},
{
label: 'ZITADEL ACS Login Form',
url: `${environment.issuer}/ui/login/login/externalidp/saml/acs`,
},
{
label: 'ZITADEL ACS Intent API',
url: `${idpBase}/acs`,
},
{
label: 'ZITADEL Single Logout',
url: `${idpBase}/slo`,
},
];
}),
),
),
shareReplay(1),
);
constructor(
private _location: Location,
private toast: ToastService,
@@ -46,6 +99,8 @@ export class ProviderSamlSpComponent {
private route: ActivatedRoute,
private injector: Injector,
private breadcrumbService: BreadcrumbService,
private envSvc: EnvironmentService,
private nextSvc: ProviderNextService,
) {
this._buildBreadcrumbs();
this._initializeForm();
@@ -117,14 +172,18 @@ export class ProviderSamlSpComponent {
});
}
public activate() {
this.nextSvc.activate(this.id$, this.justActivated$, this.service$);
}
public updateSAMLProvider(): void {
if (this.provider) {
if (this.provider || this.justCreated$.value) {
const req =
this.serviceType === PolicyComponentServiceType.MGMT
? new MgmtUpdateSAMLProviderRequest()
: new AdminUpdateSAMLProviderRequest();
req.setId(this.provider.id);
req.setId(this.provider?.id || this.justCreated$.value);
req.setName(this.name?.value);
if (this.metadataXml?.value) {
req.setMetadataXml(this.metadataXml?.value);
@@ -170,11 +229,9 @@ export class ProviderSamlSpComponent {
this.loading = true;
this.service
.addSAMLProvider(req)
.then(() => {
setTimeout(() => {
this.loading = false;
this.close();
}, 2000);
.then((addedIDP) => {
this.justCreated$.next(addedIDP.id);
this.loading = false;
})
.catch((error) => {
this.toast.showError(error);
@@ -183,7 +240,7 @@ export class ProviderSamlSpComponent {
}
public submitForm(): void {
this.provider ? this.updateSAMLProvider() : this.addSAMLProvider();
this.provider || this.justCreated$.value ? this.updateSAMLProvider() : this.addSAMLProvider();
}
private getData(id: string): void {
@@ -30,6 +30,9 @@ import { ProviderOAuthComponent } from './provider-oauth/provider-oauth.componen
import { ProviderOIDCComponent } from './provider-oidc/provider-oidc.component';
import { ProvidersRoutingModule } from './providers-routing.module';
import { ProviderSamlSpComponent } from './provider-saml-sp/provider-saml-sp.component';
import { CopyRowComponent } from '../../components/copy-row/copy-row.component';
import { ProviderNextComponent } from './provider-next/provider-next.component';
import { ProviderNextService } from './provider-next/provider-next.service';
@NgModule({
declarations: [
@@ -47,6 +50,7 @@ import { ProviderSamlSpComponent } from './provider-saml-sp/provider-saml-sp.com
ProviderLDAPComponent,
ProviderAppleComponent,
ProviderSamlSpComponent,
ProviderNextComponent,
],
imports: [
ProvidersRoutingModule,
@@ -67,6 +71,8 @@ import { ProviderSamlSpComponent } from './provider-saml-sp/provider-saml-sp.com
TranslateModule,
ProviderOptionsModule,
MatProgressSpinnerModule,
CopyRowComponent,
],
providers: [ProviderNextService],
})
export default class ProvidersModule {}
@@ -2,6 +2,8 @@
$is-dark-theme: map-get($theme, is-dark);
$background: map-get($theme, background);
$foreground: map-get($theme, foreground);
$button-text-color: map-get($foreground, text);
$button-disabled-text-color: map-get($foreground, disabled-button);
.identity-provider-desc {
font-size: 14px;
@@ -192,3 +194,18 @@
}
}
}
.cnsl-state-dot {
height: 8px;
width: 8px;
border-radius: 50%;
flex-shrink: 0;
&.active {
background-color: var(--success);
}
&.inactive {
background-color: var(--warn);
}
}