feat(console, login): v2 notification settings, login avatar (#3606)

* instance routing

* instance naming

* org list

* rm isonsystem

* breadcrumb  type

* routing

* instance members

* fragment refresh org

* settings pages

* settings list, sidenav grouping, i18n

* org-settings, policy changes

* lint

* grid

* rename grid

* fallback to general

* cleanup

* general settings, remove cards

* sidenav for settings, label policy

* i18n

* header, nav backbuild

* general, project nav rehaul

* login text background adapt

* org nav anim

* org, instance settings, fix policy layout, roles

* i18n, active route for project

* lint

* notification-settings

* idp create redirect, sms provider create, i18n

* oidc configuration

* settings list

* new avatar colors for login

* cleaner js

* avatar theme login

* remove avatar elevation
This commit is contained in:
Max Peintner 2022-05-11 08:01:40 +02:00 committed by GitHub
parent 06e3330d2e
commit d431ccb965
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
36 changed files with 1493 additions and 446 deletions

View File

@ -1,6 +1,6 @@
.card { .card {
margin: 1rem 0; margin: 1rem 0;
padding: 1.5rem; padding: 1rem 1.5rem;
border-radius: 0.5rem; border-radius: 0.5rem;
padding-top: 1rem; padding-top: 1rem;
min-width: 300px; min-width: 300px;

View File

@ -1,156 +1,179 @@
<div class="container"> <div class="max-width-container">
<div class="abort-container"> <div class="enlarged-container">
<button (click)="close()" mat-icon-button> <div class="abort-container">
<mat-icon>close</mat-icon> <button (click)="close()" mat-icon-button>
</button> <mat-icon>close</mat-icon>
<span class="abort">{{ 'IDP.CREATE.TITLE' | translate }}</span><span class="abort-2">Step </button>
{{ currentCreateStep }} of <span class="abort">{{ 'IDP.CREATE.TITLE' | translate }}</span
{{ createSteps }}</span> ><span class="abort-2">Step {{ currentCreateStep }} of {{ createSteps }}</span>
</div>
<h1>{{'IDP.CREATE.TITLE' | translate}}</h1>
<mat-progress-bar *ngIf="loading" color="primary" mode="indeterminate"></mat-progress-bar>
<ng-container *ngIf="currentCreateStep === 1">
<p class="desc cnsl-secondary-text">{{'IDP.CREATE.DESCRIPTION' | translate}}</p>
<cnsl-idp-type-radio [types]="idpTypes" (selectedType)="idpType = $event" [selected]="idpType">
</cnsl-idp-type-radio>
<div class="actions">
<button mat-raised-button [disabled]="!idpType" color="primary"
(click)="currentCreateStep = 2">{{'ACTIONS.CONTINUE' | translate}}</button>
</div> </div>
</ng-container>
<ng-container *ngIf="currentCreateStep === 2 && idpType === OIDC"> <div class="idp-create-content">
<p class="desc cnsl-secondary-text">{{'IDP.OIDC.DESCRIPTION' | translate}}</p> <h1>{{ 'IDP.CREATE.TITLE' | translate }}</h1>
<form [formGroup]="oidcFormGroup" (ngSubmit)="addOIDCIdp()"> <mat-progress-bar *ngIf="loading" color="primary" mode="indeterminate"></mat-progress-bar>
<div class="idp-content">
<cnsl-form-field appearance="outline" class="formfield">
<cnsl-label>{{ 'IDP.NAME' | translate }}</cnsl-label>
<input cnslInput formControlName="name" />
</cnsl-form-field>
<cnsl-form-field appearance="outline" class="formfield">
<cnsl-label>{{ 'IDP.ISSUER' | translate }}</cnsl-label>
<input cnslInput formControlName="issuer" />
</cnsl-form-field>
</div>
<cnsl-info-section class="auto-reg-info"> <ng-container *ngIf="currentCreateStep === 1">
<div> <p class="desc cnsl-secondary-text">{{ 'IDP.CREATE.DESCRIPTION' | translate }}</p>
<p class="auto-reg-desc">{{'IDP.AUTOREGISTER_DESC' | translate}}</p>
<mat-checkbox formControlName="autoRegister"> <cnsl-idp-type-radio [types]="idpTypes" (selectedType)="idpType = $event" [selected]="idpType">
{{'IDP.AUTOREGISTER' | translate}} </cnsl-idp-type-radio>
</mat-checkbox>
<div class="first-step-actions">
<button mat-raised-button [disabled]="!idpType" color="primary" (click)="currentCreateStep = 2">
{{ 'ACTIONS.CONTINUE' | translate }}
</button>
</div> </div>
</cnsl-info-section> </ng-container>
<div class="idp-content"> <ng-container *ngIf="currentCreateStep === 2 && idpType === OIDC">
<cnsl-form-field appearance="outline" class="formfield"> <p class="desc cnsl-secondary-text">{{ 'IDP.OIDC.DESCRIPTION' | translate }}</p>
<cnsl-label>{{ 'IDP.CLIENTID' | translate }}</cnsl-label>
<input cnslInput formControlName="clientId" />
</cnsl-form-field>
<cnsl-form-field appearance="outline" class="formfield">
<cnsl-label>{{ 'IDP.CLIENTSECRET' | translate }}</cnsl-label>
<input cnslInput formControlName="clientSecret" />
</cnsl-form-field>
</div>
<div class="idp-content">
<cnsl-form-field appearance="outline" class="formfield">
<cnsl-label>{{ 'IDP.SCOPESLIST' | translate }}</cnsl-label>
<mat-chip-list #chipScopesList aria-label="scope selection" *ngIf="scopesList">
<mat-chip class="chip" *ngFor="let scope of scopesList.value" selectable="false" removable
(removed)="removeScope(scope)">
{{scope}} <mat-icon matChipRemove>cancel</mat-icon>
</mat-chip>
<input cnslInput [matChipInputFor]="chipScopesList" [matChipInputSeparatorKeyCodes]="separatorKeysCodes"
[matChipInputAddOnBlur]="true" (matChipInputTokenEnd)="addScope($event)">
</mat-chip-list>
</cnsl-form-field>
</div>
<div class="idp-content">
<cnsl-form-field class="formfield" appearance="outline">
<cnsl-label>{{ 'IDP.IDPDISPLAYNAMMAPPING' | translate }}</cnsl-label>
<mat-select formControlName="idpDisplayNameMapping">
<mat-option *ngFor="let field of mappingFields" [value]="field">
{{ 'IDP.MAPPINGFIELD.'+field | translate }}
</mat-option>
</mat-select>
</cnsl-form-field>
<cnsl-form-field class="formfield" appearance="outline">
<cnsl-label>{{ 'IDP.USERNAMEMAPPING' | translate }}</cnsl-label>
<mat-select formControlName="usernameMapping">
<mat-option *ngFor="let field of mappingFields" [value]="field">
{{ 'IDP.MAPPINGFIELD.'+field | translate }}
</mat-option>
</mat-select>
</cnsl-form-field>
</div>
<div class="idp-create-actions"> <form [formGroup]="oidcFormGroup" (ngSubmit)="addOIDCIdp()">
<button color="primary" (click)="currentCreateStep = 1" mat-stroked-button class="back-button" type="button"> <div class="idp-content">
{{ 'ACTIONS.BACK' | translate }} <cnsl-form-field appearance="outline" class="formfield">
</button> <cnsl-label>{{ 'IDP.NAME' | translate }}</cnsl-label>
<button color="primary" mat-raised-button class="continue-button" [disabled]="oidcFormGroup.invalid" <input cnslInput formControlName="name" />
type="submit"> </cnsl-form-field>
{{ 'ACTIONS.SAVE' | translate }} <cnsl-form-field appearance="outline" class="formfield">
</button> <cnsl-label>{{ 'IDP.ISSUER' | translate }}</cnsl-label>
</div> <input cnslInput formControlName="issuer" />
</form> </cnsl-form-field>
</ng-container> </div>
<ng-container *ngIf="currentCreateStep === 2 && idpType === JWT"> <cnsl-info-section class="auto-reg-info">
<p class="desc cnsl-secondary-text">{{'IDP.JWT.DESCRIPTION' | translate}}</p> <div>
<p class="auto-reg-desc">{{ 'IDP.AUTOREGISTER_DESC' | translate }}</p>
<mat-checkbox formControlName="autoRegister">
{{ 'IDP.AUTOREGISTER' | translate }}
</mat-checkbox>
</div>
</cnsl-info-section>
<form [formGroup]="jwtFormGroup" (ngSubmit)="addJWTIdp()"> <div class="idp-content">
<div class="idp-content"> <cnsl-form-field appearance="outline" class="formfield">
<cnsl-form-field appearance="outline" class="formfield"> <cnsl-label>{{ 'IDP.CLIENTID' | translate }}</cnsl-label>
<cnsl-label>{{ 'IDP.NAME' | translate }}</cnsl-label> <input cnslInput formControlName="clientId" />
<input cnslInput formControlName="jwtName" /> </cnsl-form-field>
</cnsl-form-field> <cnsl-form-field appearance="outline" class="formfield">
<cnsl-form-field appearance="outline" class="formfield"> <cnsl-label>{{ 'IDP.CLIENTSECRET' | translate }}</cnsl-label>
<cnsl-label>{{ 'IDP.JWT.HEADERNAME' | translate }}</cnsl-label> <input cnslInput formControlName="clientSecret" />
<input cnslInput formControlName="jwtHeaderName" /> </cnsl-form-field>
</cnsl-form-field> </div>
<cnsl-form-field appearance="outline" class="formfield"> <div class="idp-content">
<cnsl-label>{{ 'IDP.ISSUER' | translate }}</cnsl-label> <cnsl-form-field appearance="outline" class="formfield">
<input cnslInput formControlName="jwtIssuer" /> <cnsl-label>{{ 'IDP.SCOPESLIST' | translate }}</cnsl-label>
</cnsl-form-field> <mat-chip-list #chipScopesList aria-label="scope selection" *ngIf="scopesList">
</div> <mat-chip
class="chip"
*ngFor="let scope of scopesList.value"
selectable="false"
removable
(removed)="removeScope(scope)"
>
{{ scope }} <mat-icon matChipRemove>cancel</mat-icon>
</mat-chip>
<input
cnslInput
[matChipInputFor]="chipScopesList"
[matChipInputSeparatorKeyCodes]="separatorKeysCodes"
[matChipInputAddOnBlur]="true"
(matChipInputTokenEnd)="addScope($event)"
/>
</mat-chip-list>
</cnsl-form-field>
</div>
<div class="idp-content">
<cnsl-form-field class="formfield" appearance="outline">
<cnsl-label>{{ 'IDP.IDPDISPLAYNAMMAPPING' | translate }}</cnsl-label>
<mat-select formControlName="idpDisplayNameMapping">
<mat-option *ngFor="let field of mappingFields" [value]="field">
{{ 'IDP.MAPPINGFIELD.' + field | translate }}
</mat-option>
</mat-select>
</cnsl-form-field>
<cnsl-form-field class="formfield" appearance="outline">
<cnsl-label>{{ 'IDP.USERNAMEMAPPING' | translate }}</cnsl-label>
<mat-select formControlName="usernameMapping">
<mat-option *ngFor="let field of mappingFields" [value]="field">
{{ 'IDP.MAPPINGFIELD.' + field | translate }}
</mat-option>
</mat-select>
</cnsl-form-field>
</div>
<cnsl-info-section class="auto-reg-info"> <div class="idp-create-actions">
<div> <button color="primary" (click)="currentCreateStep = 1" mat-stroked-button class="back-button" type="button">
<p class="auto-reg-desc">{{'IDP.AUTOREGISTER_DESC' | translate}}</p> {{ 'ACTIONS.BACK' | translate }}
<mat-checkbox formControlName="jwtAutoRegister"> </button>
{{'IDP.AUTOREGISTER' | translate}} <button
</mat-checkbox> color="primary"
</div> mat-raised-button
</cnsl-info-section> class="continue-button"
[disabled]="oidcFormGroup.invalid"
type="submit"
>
{{ 'ACTIONS.SAVE' | translate }}
</button>
</div>
</form>
</ng-container>
<div class="idp-content"> <ng-container *ngIf="currentCreateStep === 2 && idpType === JWT">
<cnsl-form-field appearance="outline" class="formfield"> <p class="desc cnsl-secondary-text">{{ 'IDP.JWT.DESCRIPTION' | translate }}</p>
<cnsl-label>{{ 'IDP.JWT.JWTENDPOINT' | translate }}</cnsl-label>
<input cnslInput formControlName="jwtEndpoint" />
</cnsl-form-field>
<cnsl-form-field appearance="outline" class="formfield">
<cnsl-label>{{ 'IDP.JWT.JWTKEYSENDPOINT' | translate }}</cnsl-label>
<input cnslInput formControlName="jwtKeysEndpoint" />
</cnsl-form-field>
</div>
<div class="idp-create-actions"> <form [formGroup]="jwtFormGroup" (ngSubmit)="addJWTIdp()">
<button color="primary" (click)="currentCreateStep = 1" mat-stroked-button class="back-button" type="button"> <div class="idp-content">
{{ 'ACTIONS.BACK' | translate }} <cnsl-form-field appearance="outline" class="formfield">
</button> <cnsl-label>{{ 'IDP.NAME' | translate }}</cnsl-label>
<button color="primary" mat-raised-button class="continue-button" [disabled]="jwtFormGroup.invalid" <input cnslInput formControlName="jwtName" />
type="submit"> </cnsl-form-field>
{{ 'ACTIONS.SAVE' | translate }} <cnsl-form-field appearance="outline" class="formfield">
</button> <cnsl-label>{{ 'IDP.JWT.HEADERNAME' | translate }}</cnsl-label>
</div> <input cnslInput formControlName="jwtHeaderName" />
</form> </cnsl-form-field>
</ng-container> <cnsl-form-field appearance="outline" class="formfield">
<cnsl-label>{{ 'IDP.ISSUER' | translate }}</cnsl-label>
<input cnslInput formControlName="jwtIssuer" />
</cnsl-form-field>
</div>
<cnsl-info-section class="auto-reg-info">
<div>
<p class="auto-reg-desc">{{ 'IDP.AUTOREGISTER_DESC' | translate }}</p>
<mat-checkbox formControlName="jwtAutoRegister">
{{ 'IDP.AUTOREGISTER' | translate }}
</mat-checkbox>
</div>
</cnsl-info-section>
<div class="idp-content">
<cnsl-form-field appearance="outline" class="formfield">
<cnsl-label>{{ 'IDP.JWT.JWTENDPOINT' | translate }}</cnsl-label>
<input cnslInput formControlName="jwtEndpoint" />
</cnsl-form-field>
<cnsl-form-field appearance="outline" class="formfield">
<cnsl-label>{{ 'IDP.JWT.JWTKEYSENDPOINT' | translate }}</cnsl-label>
<input cnslInput formControlName="jwtKeysEndpoint" />
</cnsl-form-field>
</div>
<div class="idp-create-actions">
<button color="primary" (click)="currentCreateStep = 1" mat-stroked-button class="back-button" type="button">
{{ 'ACTIONS.BACK' | translate }}
</button>
<button
color="primary"
mat-raised-button
class="continue-button"
[disabled]="jwtFormGroup.invalid"
type="submit"
>
{{ 'ACTIONS.SAVE' | translate }}
</button>
</div>
</form>
</ng-container>
</div>
</div>
</div> </div>

View File

@ -2,75 +2,75 @@
font-size: 14px; font-size: 14px;
} }
.container { .abort-container {
padding: 4rem 4rem 2rem 4rem; display: flex;
align-items: center;
margin-bottom: 2rem;
@media only screen and (max-width: 450px) { .abort {
padding: 4rem 1rem 2rem 1rem; font-size: 1.2rem;
margin-left: 2rem;
} }
.abort-container { .abort-2 {
font-size: 1.2rem;
margin-left: 2rem;
white-space: nowrap;
}
}
.add-line-btn {
margin-bottom: 1rem;
}
.idp-create-content {
padding: 0 0 0 72px;
.first-step-actions {
margin-top: 1rem;
}
.auto-reg-info {
display: block;
width: 100%;
.auto-reg-desc {
margin: 0 0 1rem 0;
}
}
.idp-content {
display: flex; display: flex;
align-items: center; margin: 0 -0.5rem;
margin-bottom: 2rem; flex-wrap: wrap;
.abort { .desc {
font-size: 1.2rem;
margin-left: 2rem;
}
.abort-2 {
font-size: 1.2rem;
margin-left: 2rem;
white-space: nowrap;
}
}
.add-line-btn {
margin-bottom: 1rem;
}
}
.auto-reg-info {
display: block;
width: 100%;
.auto-reg-desc {
margin: 0 0 1rem 0;
}
}
.idp-content {
display: flex;
margin: 0 -0.5rem;
flex-wrap: wrap;
.desc {
flex-basis: 100%;
margin: 0 0.5rem;
margin-bottom: 1rem;
}
.formfield {
flex: 1;
margin: 0 0.5rem;
@media only screen and (max-width: 450px) {
flex-basis: 100%; flex-basis: 100%;
margin: 0 0.5rem;
margin-bottom: 1rem;
}
.formfield {
flex: 1;
margin: 0 0.5rem;
@media only screen and (max-width: 450px) {
flex-basis: 100%;
}
}
}
.idp-create-actions {
display: flex;
justify-content: space-between;
margin-top: 1rem;
button[mat-stroked-button] {
border-radius: 0.5rem;
}
button[mat-raised-button] {
border-radius: 0.5rem;
} }
} }
} }
.idp-create-actions {
display: flex;
justify-content: space-between;
margin-top: 1rem;
button[mat-stroked-button] {
border-radius: 0.5rem;
}
button[mat-raised-button] {
border-radius: 0.5rem;
}
}

View File

@ -83,13 +83,12 @@ export class IdpCreateComponent implements OnInit, OnDestroy {
OIDCMappingField.OIDC_MAPPING_FIELD_PREFERRED_USERNAME, OIDCMappingField.OIDC_MAPPING_FIELD_PREFERRED_USERNAME,
OIDCMappingField.OIDC_MAPPING_FIELD_EMAIL, OIDCMappingField.OIDC_MAPPING_FIELD_EMAIL,
]; ];
const bread: Breadcrumb = {
type: BreadcrumbType.ORG,
routerLink: ['/org'],
};
const iamBread = new Breadcrumb({ breadcrumbService.setBreadcrumb([bread]);
type: BreadcrumbType.INSTANCE,
name: 'Instance',
routerLink: ['/instance'],
});
breadcrumbService.setBreadcrumb([iamBread]);
break; break;
case PolicyComponentServiceType.ADMIN: case PolicyComponentServiceType.ADMIN:
this.service = this.injector.get(AdminService as Type<AdminService>); this.service = this.injector.get(AdminService as Type<AdminService>);
@ -98,11 +97,12 @@ export class IdpCreateComponent implements OnInit, OnDestroy {
OIDCMappingField.OIDC_MAPPING_FIELD_EMAIL, OIDCMappingField.OIDC_MAPPING_FIELD_EMAIL,
]; ];
const bread: Breadcrumb = { const iamBread = new Breadcrumb({
type: BreadcrumbType.ORG, type: BreadcrumbType.ORG,
routerLink: ['/org'], name: 'Instance',
}; routerLink: ['/instance'],
breadcrumbService.setBreadcrumb([bread]); });
breadcrumbService.setBreadcrumb([iamBread]);
break; break;
} }
}); });
@ -139,15 +139,16 @@ export class IdpCreateComponent implements OnInit, OnDestroy {
.then((idp) => { .then((idp) => {
setTimeout(() => { setTimeout(() => {
this.loading = false; this.loading = false;
this.router.navigate([ this.router.navigate(
this.serviceType === PolicyComponentServiceType.MGMT [
? 'org' this.serviceType === PolicyComponentServiceType.MGMT
: this.serviceType === PolicyComponentServiceType.ADMIN ? '/org-settings'
? 'iam' : this.serviceType === PolicyComponentServiceType.ADMIN
: '', ? '/settings'
'policy', : '',
'login', ],
]); { queryParams: { id: 'idp' } },
);
}, 2000); }, 2000);
}) })
.catch((error) => { .catch((error) => {

View File

@ -1,20 +1,33 @@
<cnsl-top-view title="{{'IDP.DETAIL.TITLE' | translate}}" <cnsl-top-view
title="{{ 'IDP.DETAIL.TITLE' | translate }}"
[sub]="idp?.oidcConfig ? ('IDP.OIDC.TITLE' | translate) : idp?.jwtConfig ? ('IDP.JWT.TITLE' | translate) : ''" [sub]="idp?.oidcConfig ? ('IDP.OIDC.TITLE' | translate) : idp?.jwtConfig ? ('IDP.JWT.TITLE' | translate) : ''"
[isActive]="idp?.state === IDPState.IDP_STATE_ACTIVE" [isInactive]="idp?.state === IDPState.IDP_STATE_INACTIVE" [isActive]="idp?.state === IDPState.IDP_STATE_ACTIVE"
[hasContributors]="false" stateTooltip="{{'IDP.STATES.'+idp?.state | translate}}" [isInactive]="idp?.state === IDPState.IDP_STATE_INACTIVE"
[hasActions]="(serviceType === PolicyComponentServiceType.MGMT ? ['org.idp.write'] : ['iam.idp.write']) | hasRole | async"> [hasContributors]="false"
<ng-template topActions cnslHasRole stateTooltip="{{ 'IDP.STATES.' + idp?.state | translate }}"
[hasRole]="serviceType === PolicyComponentServiceType.MGMT ? ['org.idp.write'] : ['iam.idp.write']"> [hasActions]="(serviceType === PolicyComponentServiceType.MGMT ? ['org.idp.write'] : ['iam.idp.write']) | hasRole | async"
<button mat-menu-item *ngIf="idp?.state !== IDPState.IDP_STATE_INACTIVE" >
(click)="changeState(IDPState.IDP_STATE_INACTIVE)"> <ng-template
{{'ACTIONS.DEACTIVATE' | translate}} topActions
cnslHasRole
[hasRole]="serviceType === PolicyComponentServiceType.MGMT ? ['org.idp.write'] : ['iam.idp.write']"
>
<button
mat-menu-item
*ngIf="idp?.state !== IDPState.IDP_STATE_INACTIVE"
(click)="changeState(IDPState.IDP_STATE_INACTIVE)"
>
{{ 'ACTIONS.DEACTIVATE' | translate }}
</button> </button>
<button mat-menu-item *ngIf="idp?.state === IDPState.IDP_STATE_INACTIVE" <button
(click)="changeState(IDPState.IDP_STATE_ACTIVE)"> mat-menu-item
{{'ACTIONS.REACTIVATE' | translate}} *ngIf="idp?.state === IDPState.IDP_STATE_INACTIVE"
(click)="changeState(IDPState.IDP_STATE_ACTIVE)"
>
{{ 'ACTIONS.REACTIVATE' | translate }}
</button> </button>
<button mat-menu-item matTooltip="{{'IDP.DELETE' | translate}}" (click)="deleteIdp()"> <button mat-menu-item matTooltip="{{ 'IDP.DELETE' | translate }}" (click)="deleteIdp()">
<span [style.color]="'var(--warn)'">{{'IDP.DELETE_TITLE' | translate}}</span> <span [style.color]="'var(--warn)'">{{ 'IDP.DELETE_TITLE' | translate }}</span>
</button> </button>
</ng-template> </ng-template>
<cnsl-info-row topContent *ngIf="idp" [idp]="idp"></cnsl-info-row> <cnsl-info-row topContent *ngIf="idp" [idp]="idp"></cnsl-info-row>
@ -24,8 +37,7 @@
<form class="idp-form" (ngSubmit)="updateIdp()"> <form class="idp-form" (ngSubmit)="updateIdp()">
<ng-container [formGroup]="idpForm"> <ng-container [formGroup]="idpForm">
<div class="idp-content"> <div class="idp-content">
<cnsl-form-field class="formfield">
<cnsl-form-field appearance="outline" class="formfield">
<cnsl-label>{{ 'IDP.NAME' | translate }}</cnsl-label> <cnsl-label>{{ 'IDP.NAME' | translate }}</cnsl-label>
<input cnslInput formControlName="name" /> <input cnslInput formControlName="name" />
</cnsl-form-field> </cnsl-form-field>
@ -33,16 +45,16 @@
<cnsl-label>{{ 'IDP.STYLE' | translate }}</cnsl-label> <cnsl-label>{{ 'IDP.STYLE' | translate }}</cnsl-label>
<mat-select formControlName="stylingType"> <mat-select formControlName="stylingType">
<mat-option *ngFor="let field of styleFields" [value]="field"> <mat-option *ngFor="let field of styleFields" [value]="field">
{{ 'IDP.STYLEFIELD.'+field | translate }} {{ 'IDP.STYLEFIELD.' + field | translate }}
</mat-option> </mat-option>
</mat-select> </mat-select>
</cnsl-form-field> </cnsl-form-field>
<cnsl-info-section class="auto-reg-info"> <cnsl-info-section class="auto-reg-info">
<div> <div>
<p class="auto-reg-desc">{{'IDP.AUTOREGISTER_DESC' | translate}}</p> <p class="auto-reg-desc">{{ 'IDP.AUTOREGISTER_DESC' | translate }}</p>
<mat-checkbox formControlName="autoRegister" [disabled]="(canWrite | async) === false"> <mat-checkbox formControlName="autoRegister" [disabled]="(canWrite | async) === false">
{{'IDP.AUTOREGISTER' | translate}} {{ 'IDP.AUTOREGISTER' | translate }}
</mat-checkbox> </mat-checkbox>
</div> </div>
</cnsl-info-section> </cnsl-info-section>
@ -50,52 +62,72 @@
</ng-container> </ng-container>
<div class="btn-wrapper"> <div class="btn-wrapper">
<button color="primary" mat-raised-button class="continue-button" <button
[disabled]="idpForm.invalid || (canWrite | async) === false" type="submit"> color="primary"
mat-raised-button
class="continue-button"
[disabled]="idpForm.invalid || (canWrite | async) === false"
type="submit"
>
{{ 'ACTIONS.SAVE' | translate }} {{ 'ACTIONS.SAVE' | translate }}
</button> </button>
</div> </div>
</form> </form>
<ng-container *ngIf="idp?.oidcConfig && oidcConfigForm"> <ng-container *ngIf="idp?.oidcConfig && oidcConfigForm">
<h2>{{'IDP.OIDC.TITLE' | translate}}</h2> <h2>{{ 'IDP.OIDC.TITLE' | translate }}</h2>
<p class="idp-desc cnsl-secondary-text">{{'IDP.OIDC.DESCRIPTION' | translate}}</p> <p class="idp-desc cnsl-secondary-text">{{ 'IDP.OIDC.DESCRIPTION' | translate }}</p>
<form (ngSubmit)="updateOidcConfig()"> <form (ngSubmit)="updateOidcConfig()">
<ng-container [formGroup]="oidcConfigForm"> <ng-container [formGroup]="oidcConfigForm">
<div class="idp-content"> <div class="idp-content">
<cnsl-form-field appearance="outline" class="formfield"> <cnsl-form-field class="formfield">
<cnsl-label>{{ 'IDP.ISSUER' | translate }}</cnsl-label> <cnsl-label>{{ 'IDP.ISSUER' | translate }}</cnsl-label>
<input cnslInput formControlName="issuer" /> <input cnslInput formControlName="issuer" />
</cnsl-form-field> </cnsl-form-field>
<cnsl-form-field appearance="outline" class="formfield"> <cnsl-form-field class="formfield">
<cnsl-label>{{ 'IDP.CLIENTID' | translate }}</cnsl-label> <cnsl-label>{{ 'IDP.CLIENTID' | translate }}</cnsl-label>
<input cnslInput formControlName="clientId" /> <input cnslInput formControlName="clientId" />
</cnsl-form-field> </cnsl-form-field>
<mat-checkbox class="idp-desc cnsl-secondary-text" [(ngModel)]="showIdSecretSection" <mat-checkbox
[disabled]="(canWrite | async) === false" [ngModelOptions]="{standalone: true}"> class="idp-desc cnsl-secondary-text"
[(ngModel)]="showIdSecretSection"
[disabled]="(canWrite | async) === false"
[ngModelOptions]="{ standalone: true }"
>
Update Client Secret Update Client Secret
</mat-checkbox> </mat-checkbox>
<cnsl-form-field appearance="outline" class="formfield" *ngIf="showIdSecretSection"> <cnsl-form-field class="formfield" *ngIf="showIdSecretSection">
<cnsl-label>{{ 'IDP.CLIENTSECRET' | translate }}</cnsl-label> <cnsl-label>{{ 'IDP.CLIENTSECRET' | translate }}</cnsl-label>
<input cnslInput formControlName="clientSecret" /> <input cnslInput formControlName="clientSecret" />
</cnsl-form-field> </cnsl-form-field>
<div class="line"> <div class="line">
<cnsl-form-field appearance="outline" class="formfield"> <cnsl-form-field class="formfield">
<cnsl-label>{{ 'IDP.SCOPESLIST' | translate }}</cnsl-label> <cnsl-label>{{ 'IDP.SCOPESLIST' | translate }}</cnsl-label>
<input cnslInput [matChipInputFor]="chipScopesList" [matChipInputSeparatorKeyCodes]="separatorKeysCodes" <input
[matChipInputAddOnBlur]="true" (matChipInputTokenEnd)="addScope($event)"> cnslInput
[matChipInputFor]="chipScopesList"
[matChipInputSeparatorKeyCodes]="separatorKeysCodes"
[matChipInputAddOnBlur]="true"
(matChipInputTokenEnd)="addScope($event)"
/>
</cnsl-form-field> </cnsl-form-field>
<button (click)="addScope($any($event))" mat-icon-button> <button (click)="addScope($any($event))" mat-icon-button>
<mat-icon>add</mat-icon> <mat-icon>add</mat-icon>
</button> </button>
</div> </div>
<cnsl-form-field appearance="outline" class="formfield fullwidth"> <cnsl-form-field class="formfield fullwidth">
<mat-chip-list class="chip-list" #chipScopesList aria-label="scope selection"> <mat-chip-list class="chip-list" #chipScopesList aria-label="scope selection">
<mat-chip class="chip" *ngFor="let scope of scopesList?.value" selectable="false" removable <mat-chip
(removed)="removeScope(scope)" [disabled]="(canWrite | async) === false"> class="chip"
{{scope}} <mat-icon matChipRemove>cancel</mat-icon> *ngFor="let scope of scopesList?.value"
selectable="false"
removable
(removed)="removeScope(scope)"
[disabled]="(canWrite | async) === false"
>
{{ scope }} <mat-icon matChipRemove>cancel</mat-icon>
</mat-chip> </mat-chip>
</mat-chip-list> </mat-chip-list>
</cnsl-form-field> </cnsl-form-field>
@ -104,7 +136,7 @@
<cnsl-label>{{ 'IDP.IDPDISPLAYNAMMAPPING' | translate }}</cnsl-label> <cnsl-label>{{ 'IDP.IDPDISPLAYNAMMAPPING' | translate }}</cnsl-label>
<mat-select formControlName="displayNameMapping"> <mat-select formControlName="displayNameMapping">
<mat-option *ngFor="let field of mappingFields" [value]="field"> <mat-option *ngFor="let field of mappingFields" [value]="field">
{{ 'IDP.MAPPINGFIELD.'+field | translate }} {{ 'IDP.MAPPINGFIELD.' + field | translate }}
</mat-option> </mat-option>
</mat-select> </mat-select>
</cnsl-form-field> </cnsl-form-field>
@ -112,7 +144,7 @@
<cnsl-label>{{ 'IDP.USERNAMEMAPPING' | translate }}</cnsl-label> <cnsl-label>{{ 'IDP.USERNAMEMAPPING' | translate }}</cnsl-label>
<mat-select formControlName="usernameMapping"> <mat-select formControlName="usernameMapping">
<mat-option *ngFor="let field of mappingFields" [value]="field"> <mat-option *ngFor="let field of mappingFields" [value]="field">
{{ 'IDP.MAPPINGFIELD.'+field | translate }} {{ 'IDP.MAPPINGFIELD.' + field | translate }}
</mat-option> </mat-option>
</mat-select> </mat-select>
</cnsl-form-field> </cnsl-form-field>
@ -120,8 +152,13 @@
</ng-container> </ng-container>
<div class="btn-wrapper"> <div class="btn-wrapper">
<button color="primary" mat-raised-button class="continue-button" <button
[disabled]="oidcConfigForm.invalid || (canWrite | async) === false" type="submit"> color="primary"
mat-raised-button
class="continue-button"
[disabled]="oidcConfigForm.invalid || (canWrite | async) === false"
type="submit"
>
{{ 'ACTIONS.SAVE' | translate }} {{ 'ACTIONS.SAVE' | translate }}
</button> </button>
</div> </div>
@ -129,28 +166,28 @@
</ng-container> </ng-container>
<ng-container *ngIf="idp?.jwtConfig && jwtConfigForm"> <ng-container *ngIf="idp?.jwtConfig && jwtConfigForm">
<h2>{{'IDP.JWT.TITLE' | translate}}</h2> <h2>{{ 'IDP.JWT.TITLE' | translate }}</h2>
<p>{{'IDP.JWT.DESCRIPTION' | translate}}</p> <p>{{ 'IDP.JWT.DESCRIPTION' | translate }}</p>
<form (ngSubmit)="updateJwtConfig()"> <form (ngSubmit)="updateJwtConfig()">
<ng-container [formGroup]="jwtConfigForm"> <ng-container [formGroup]="jwtConfigForm">
<div class="idp-content"> <div class="idp-content">
<cnsl-form-field appearance="outline" class="formfield"> <cnsl-form-field class="formfield">
<cnsl-label>{{ 'IDP.ISSUER' | translate }}</cnsl-label> <cnsl-label>{{ 'IDP.ISSUER' | translate }}</cnsl-label>
<input cnslInput formControlName="issuer" /> <input cnslInput formControlName="issuer" />
</cnsl-form-field> </cnsl-form-field>
<cnsl-form-field appearance="outline" class="formfield"> <cnsl-form-field class="formfield">
<cnsl-label>{{ 'IDP.JWT.HEADERNAME' | translate }}</cnsl-label> <cnsl-label>{{ 'IDP.JWT.HEADERNAME' | translate }}</cnsl-label>
<input cnslInput formControlName="headerName" /> <input cnslInput formControlName="headerName" />
</cnsl-form-field> </cnsl-form-field>
<cnsl-form-field appearance="outline" class="formfield"> <cnsl-form-field class="formfield">
<cnsl-label>{{ 'IDP.JWT.JWTENDPOINT' | translate }}</cnsl-label> <cnsl-label>{{ 'IDP.JWT.JWTENDPOINT' | translate }}</cnsl-label>
<input cnslInput formControlName="jwtEndpoint" /> <input cnslInput formControlName="jwtEndpoint" />
</cnsl-form-field> </cnsl-form-field>
<cnsl-form-field appearance="outline" class="formfield"> <cnsl-form-field class="formfield">
<cnsl-label>{{ 'IDP.JWT.JWTKEYSENDPOINT' | translate }}</cnsl-label> <cnsl-label>{{ 'IDP.JWT.JWTKEYSENDPOINT' | translate }}</cnsl-label>
<input cnslInput formControlName="keysEndpoint" /> <input cnslInput formControlName="keysEndpoint" />
</cnsl-form-field> </cnsl-form-field>
@ -158,8 +195,13 @@
</ng-container> </ng-container>
<div class="btn-wrapper"> <div class="btn-wrapper">
<button color="primary" mat-raised-button class="continue-button" <button
[disabled]="jwtConfigForm.invalid || (canWrite | async) === false" type="submit"> color="primary"
mat-raised-button
class="continue-button"
[disabled]="jwtConfigForm.invalid || (canWrite | async) === false"
type="submit"
>
{{ 'ACTIONS.SAVE' | translate }} {{ 'ACTIONS.SAVE' | translate }}
</button> </button>
</div> </div>

View File

@ -10,6 +10,7 @@
.general-btn-container { .general-btn-container {
display: flex; display: flex;
justify-content: flex-start; justify-content: flex-start;
margin-top: 1rem;
.save-button { .save-button {
display: block; display: block;

View File

@ -1,55 +1,35 @@
import { Component, Injector, Input, OnInit, Type } from '@angular/core'; import { Component, OnInit } from '@angular/core';
import { SetDefaultLanguageResponse } from 'src/app/proto/generated/zitadel/admin_pb'; import { SetDefaultLanguageResponse } from 'src/app/proto/generated/zitadel/admin_pb';
import { AdminService } from 'src/app/services/admin.service'; import { AdminService } from 'src/app/services/admin.service';
import { ManagementService } from 'src/app/services/mgmt.service';
import { ToastService } from 'src/app/services/toast.service'; import { ToastService } from 'src/app/services/toast.service';
import { PolicyComponentServiceType } from '../policy-component-types.enum';
@Component({ @Component({
selector: 'cnsl-general-settings', selector: 'cnsl-general-settings',
templateUrl: './general-settings.component.html', templateUrl: './general-settings.component.html',
styleUrls: ['./general-settings.component.scss'], styleUrls: ['./general-settings.component.scss'],
}) })
export class GeneralSettingsComponent implements OnInit { export class GeneralSettingsComponent implements OnInit {
@Input() public serviceType!: PolicyComponentServiceType;
public service!: ManagementService | AdminService;
public defaultLanguage: string = ''; public defaultLanguage: string = '';
public defaultLanguageOptions: string[] = []; public defaultLanguageOptions: string[] = [];
public loading: boolean = false; public loading: boolean = false;
constructor(private injector: Injector, private toast: ToastService) {} constructor(private service: AdminService, private toast: ToastService) {}
ngOnInit(): void { ngOnInit(): void {
switch (this.serviceType) {
case PolicyComponentServiceType.MGMT:
this.service = this.injector.get(ManagementService as Type<ManagementService>);
break;
case PolicyComponentServiceType.ADMIN:
this.service = this.injector.get(AdminService as Type<AdminService>);
break;
}
this.fetchData(); this.fetchData();
} }
private fetchData(): void { private fetchData(): void {
if (this.serviceType === PolicyComponentServiceType.ADMIN) { this.service.getDefaultLanguage().then((langResp) => {
(this.service as AdminService).getDefaultLanguage().then((langResp) => { this.defaultLanguage = langResp.language;
this.defaultLanguage = langResp.language; });
}); this.service.getSupportedLanguages().then((supportedResp) => {
(this.service as AdminService).getSupportedLanguages().then((supportedResp) => { this.defaultLanguageOptions = supportedResp.languagesList;
this.defaultLanguageOptions = supportedResp.languagesList; });
});
}
} }
private updateData(): Promise<SetDefaultLanguageResponse.AsObject> | void { private updateData(): Promise<SetDefaultLanguageResponse.AsObject> {
if (this.serviceType === PolicyComponentServiceType.ADMIN) { return (this.service as AdminService).setDefaultLanguage(this.defaultLanguage);
return (this.service as AdminService).setDefaultLanguage(this.defaultLanguage);
} else {
return;
}
} }
public savePolicy(): void { public savePolicy(): void {
@ -68,21 +48,4 @@ export class GeneralSettingsComponent implements OnInit {
}); });
} }
} }
public removePolicy(): void {
if (this.serviceType === PolicyComponentServiceType.MGMT) {
(this.service as ManagementService)
.resetLoginPolicyToDefault()
.then(() => {
this.toast.showInfo('POLICY.TOAST.RESETSUCCESS', true);
this.loading = true;
setTimeout(() => {
this.fetchData();
}, 2000);
})
.catch((error) => {
this.toast.showError(error);
});
}
}
} }

View File

@ -15,7 +15,6 @@
justify-content: space-between; justify-content: space-between;
button { button {
margin-left: 0.5rem;
border-radius: 0.5rem; border-radius: 0.5rem;
} }
} }

View File

@ -21,7 +21,6 @@
<div class="mfa-list-btns"> <div class="mfa-list-btns">
<button <button
mat-stroked-button mat-stroked-button
color="primary"
class="new-mfa cnsl-action-button" class="new-mfa cnsl-action-button"
[disabled]="disabled" [disabled]="disabled"
(click)="!disabled ? addMfa() : null" (click)="!disabled ? addMfa() : null"

View File

@ -0,0 +1,48 @@
<h1 mat-dialog-title class="title">
<span>{{ 'SETTING.SMS.ADDPROVIDER' | translate }}</span>
</h1>
<div mat-dialog-content>
<p class="desc cnsl-secondary-text">{{ 'SETTING.SMS.ADDPROVIDERDESCRIPTION' | translate }}</p>
<cnsl-form-field class="form-field" label="Access Code" required="true">
<cnsl-label>{{ 'MFA.TYPE' | translate }}</cnsl-label>
<mat-select [(ngModel)]="provider">
<mat-option *ngFor="let prov of availableSMSProviders" [value]="prov">
<span *ngIf="prov === SMSProviderType.Twilio">Twilio</span>
</mat-option>
</mat-select>
</cnsl-form-field>
<form *ngIf="provider === SMSProviderType.Twilio" (ngSubmit)="closeDialogWithRequest()" [formGroup]="twilioForm">
<h2>Twilio</h2>
<cnsl-form-field class="sms-form-field" label="sid">
<cnsl-label>{{ 'SETTING.SMS.TWILIO.SID' | translate }}</cnsl-label>
<input cnslInput name="sid" formControlName="sid" />
</cnsl-form-field>
<cnsl-form-field class="sms-form-field" label="Token">
<cnsl-label>{{ 'SETTING.SMS.TWILIO.TOKEN' | translate }}</cnsl-label>
<input cnslInput name="token" formControlName="token" />
</cnsl-form-field>
<cnsl-form-field class="sms-form-field" label="Sender Number">
<cnsl-label>{{ 'SETTING.SMS.TWILIO.SENDERNUMBER' | translate }}</cnsl-label>
<input cnslInput name="senderNumber" formControlName="senderNumber" />
</cnsl-form-field>
</form>
</div>
<div mat-dialog-actions class="action">
<button mat-stroked-button (click)="closeDialog()">
<span>{{ 'ACTIONS.CLOSE' | translate }}</span>
</button>
<button
[disabled]="provider === SMSProviderType.Twilio && !twilioForm.valid"
mat-raised-button
class="ok-button"
color="primary"
(click)="closeDialogWithRequest()"
>
<span>{{ 'ACTIONS.OK' | translate }}</span>
</button>
</div>

View File

@ -0,0 +1,20 @@
.title {
font-size: 1.5rem;
}
.desc {
font-size: 14px;
}
.form-field {
width: 100%;
}
.action {
display: flex;
justify-content: space-between;
button {
border-radius: 0.5rem;
}
}

View File

@ -0,0 +1,58 @@
import { Component, Inject } from '@angular/core';
import { AbstractControl, FormBuilder, FormGroup, Validators } from '@angular/forms';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
import { AddSMSProviderTwilioRequest } from 'src/app/proto/generated/zitadel/admin_pb';
enum SMSProviderType {
Twilio = 1,
}
@Component({
selector: 'cnsl-dialog-add-sms-provider',
templateUrl: './dialog-add-sms-provider.component.html',
styleUrls: ['./dialog-add-sms-provider.component.scss'],
})
export class DialogAddSMSProviderComponent {
public SMSProviderType: any = SMSProviderType;
public availableSMSProviders: SMSProviderType[] = [SMSProviderType.Twilio];
public provider: SMSProviderType = SMSProviderType.Twilio;
public req: AddSMSProviderTwilioRequest = new AddSMSProviderTwilioRequest();
public twilioForm!: FormGroup;
constructor(
private fb: FormBuilder,
public dialogRef: MatDialogRef<DialogAddSMSProviderComponent>,
@Inject(MAT_DIALOG_DATA) public data: any,
) {
this.twilioForm = this.fb.group({
sid: ['', [Validators.required]],
token: ['', [Validators.required]],
senderNumber: ['', [Validators.required]],
});
}
public closeDialog(): void {
this.dialogRef.close();
}
public closeDialogWithRequest(): void {
this.req.setSid(this.sid?.value);
this.req.setToken(this.token?.value);
this.req.setSenderNumber(this.senderNumber?.value);
this.dialogRef.close(this.req);
}
public get senderNumber(): AbstractControl | null {
return this.twilioForm.get('senderNumber');
}
public get token(): AbstractControl | null {
return this.twilioForm.get('token');
}
public get sid(): AbstractControl | null {
return this.twilioForm.get('sid');
}
}

View File

@ -0,0 +1,75 @@
<div class="spinner-wr">
<mat-spinner diameter="30" *ngIf="loading" color="primary"></mat-spinner>
</div>
<h2>{{ 'SETTING.SMTP.TITLE' | translate }}</h2>
<form (ngSubmit)="savePolicy()" [formGroup]="form" autocomplete="off">
<cnsl-form-field class="smtp-form-field" label="Sender Address" required="true">
<cnsl-label>{{ 'SETTING.SMTP.SENDERADDRESS' | translate }}</cnsl-label>
<input cnslInput name="senderAddress" formControlName="senderAddress" />
</cnsl-form-field>
<cnsl-form-field class="smtp-form-field" label="Sender Name" required="true">
<cnsl-label>{{ 'SETTING.SMTP.SENDERNAME' | translate }}</cnsl-label>
<input cnslInput name="senderName" formControlName="senderName" />
</cnsl-form-field>
<mat-checkbox class="smtp-checkbox" formControlName="tls">
{{ 'SETTING.SMTP.TLS' | translate }}
</mat-checkbox>
<cnsl-form-field class="smtp-form-field" label="Host" required="true">
<cnsl-label>{{ 'SETTING.SMTP.HOST' | translate }}</cnsl-label>
<input cnslInput name="host" formControlName="host" />
</cnsl-form-field>
<cnsl-form-field class="smtp-form-field" label="User" required="true">
<cnsl-label>{{ 'SETTING.SMTP.USER' | translate }}</cnsl-label>
<input id="smtp-user" cnslInput name="smtp-user" autocomplete="smtp-user" formControlName="user" />
</cnsl-form-field>
<cnsl-form-field class="smtp-form-field" label="Password" required="true">
<cnsl-label>{{ 'SETTING.SMTP.PASSWORD' | translate }}</cnsl-label>
<input
id="smtp-password"
cnslInput
name="smtp-password"
autocomplete="smtp-password"
type="password"
formControlName="password"
/>
</cnsl-form-field>
<div class="general-btn-container">
<button class="save-button" (click)="savePolicy()" color="primary" type="submit" mat-raised-button>
{{ 'ACTIONS.SAVE' | translate }}
</button>
</div>
</form>
<br />
<h2>{{ 'SETTING.SMS.TITLE' | translate }}</h2>
<h3>{{ 'SETTING.SMS.PROVIDERS' | translate }}</h3>
<div class="sms-providers">
<cnsl-card *ngFor="let provider of smsProviders" class="sms-card">
<div *ngIf="provider.twilio" class="sms-provider">
<h4 class="title">Twilio</h4>
<span class="cnsl-secondary-text">{{ 'SETTING.SMS.PROVIDER' | translate }}</span>
<span
class="state"
[ngClass]="{
active: provider.state === SMSProviderConfigState.SMS_PROVIDER_CONFIG_ACTIVE,
inactive: provider.state === SMSProviderConfigState.SMS_PROVIDER_CONFIG_INACTIVE
}"
></span>
</div>
</cnsl-card>
<button mat-stroked-button (click)="addSMSProvider()">
<div class="sms-card add">
<mat-icon>add</mat-icon>
<span>{{ 'ACTIONS.ADD' | translate }}</span>
</div>
</button>
</div>

View File

@ -0,0 +1,55 @@
.spinner-wr {
margin: 0.5rem 0;
}
.smtp-form-field {
max-width: 400px;
display: block;
}
.smtp-checkbox {
max-width: 400px;
display: block;
margin: 1rem 0;
}
.general-btn-container {
display: flex;
justify-content: flex-start;
.save-button {
display: block;
}
}
.sms-providers {
display: flex;
align-items: center;
.sms-card {
margin-right: 1rem;
&.add {
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
}
.sms-provider {
display: flex;
flex-direction: row;
align-items: center;
margin-bottom: -0.5rem;
.title {
font-size: 16px;
margin: 0 1rem 0 0;
}
.fill-space {
flex: 1;
}
}
}
}

View File

@ -0,0 +1,24 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { NotificationSettingsComponent } from './notification-settings.component';
describe('NotificationSettingsComponent', () => {
let component: NotificationSettingsComponent;
let fixture: ComponentFixture<NotificationSettingsComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [NotificationSettingsComponent],
}).compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(NotificationSettingsComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -0,0 +1,158 @@
import { Component, Input, OnInit } from '@angular/core';
import { AbstractControl, FormBuilder, FormGroup, Validators } from '@angular/forms';
import { MatDialog } from '@angular/material/dialog';
import {
AddSMSProviderTwilioRequest,
UpdateSMTPConfigPasswordResponse,
UpdateSMTPConfigRequest,
} from 'src/app/proto/generated/zitadel/admin_pb';
import { SMSProvider, SMSProviderConfigState } from 'src/app/proto/generated/zitadel/settings_pb';
import { AdminService } from 'src/app/services/admin.service';
import { ToastService } from 'src/app/services/toast.service';
import { PolicyComponentServiceType } from '../policy-component-types.enum';
import { DialogAddSMSProviderComponent } from './dialog-add-sms-provider/dialog-add-sms-provider.component';
@Component({
selector: 'cnsl-notification-settings',
templateUrl: './notification-settings.component.html',
styleUrls: ['./notification-settings.component.scss'],
})
export class NotificationSettingsComponent implements OnInit {
@Input() public serviceType!: PolicyComponentServiceType;
public smsProviders: SMSProvider.AsObject[] = [];
public loading: boolean = false;
public form!: FormGroup;
public SMSProviderConfigState: any = SMSProviderConfigState;
constructor(
private service: AdminService,
private dialog: MatDialog,
private toast: ToastService,
private fb: FormBuilder,
) {
this.form = this.fb.group({
senderAddress: ['', [Validators.required]],
senderName: ['', [Validators.required]],
tls: [true, [Validators.required]],
host: ['', [Validators.required]],
user: ['', [Validators.required]],
password: ['', [Validators.required]],
});
}
ngOnInit(): void {
this.fetchData();
}
private fetchData(): void {
this.service
.getSMTPConfig()
.then((smtpConfig) => {
if (smtpConfig.smtpConfig) {
this.form.patchValue(smtpConfig.smtpConfig);
}
})
.catch((error) => {
if (error && error.code === 5) {
console.log(error);
}
});
this.service.listSMSProviders().then((smsProviders) => {
if (smsProviders.resultList) {
this.smsProviders = smsProviders.resultList;
console.log(this.smsProviders);
}
});
}
private updateData(): Promise<UpdateSMTPConfigPasswordResponse.AsObject> | any {
const req = new UpdateSMTPConfigRequest();
req.setHost(this.host?.value ?? '');
req.setSenderAddress(this.senderAddress?.value ?? '');
req.setSenderName(this.senderName?.value ?? '');
req.setTls(this.tls?.value ?? false);
req.setUser(this.user?.value ?? '');
console.log(req.toObject());
// return this.service.updateSMTPConfig(req).then(() => {
// let passwordReq: UpdateSMTPConfigPasswordRequest;
// if (this.password) {
// passwordReq = new UpdateSMTPConfigPasswordRequest();
// passwordReq.setPassword(this.password.value);
// return this.service.updateSMTPConfigPassword(passwordReq);
// } else {
// return;
// }
// });
}
public savePolicy(): void {
const prom = this.updateData();
if (prom) {
prom
.then(() => {
this.toast.showInfo('SETTING.SMTP.SAVED', true);
this.loading = true;
setTimeout(() => {
this.fetchData();
}, 2000);
})
.catch((error: unknown) => {
this.toast.showError(error);
});
}
}
public addSMSProvider(): void {
const dialogRef = this.dialog.open(DialogAddSMSProviderComponent, {
data: {
confirmKey: 'ACTIONS.DELETE',
cancelKey: 'ACTIONS.CANCEL',
titleKey: 'IDP.DELETE_TITLE',
descriptionKey: 'IDP.DELETE_DESCRIPTION',
},
width: '400px',
});
dialogRef.afterClosed().subscribe((req: AddSMSProviderTwilioRequest) => {
if (req) {
this.service
.addSMSProviderTwilio(req)
.then(() => {
this.toast.showInfo('SETTING.SMS.TWILIO.ADDED', true);
})
.catch((error) => {
this.toast.showError(error);
});
}
});
}
public get senderAddress(): AbstractControl | null {
return this.form.get('senderAddress');
}
public get senderName(): AbstractControl | null {
return this.form.get('senderName');
}
public get tls(): AbstractControl | null {
return this.form.get('tls');
}
public get user(): AbstractControl | null {
return this.form.get('user');
}
public get host(): AbstractControl | null {
return this.form.get('host');
}
public get password(): AbstractControl | null {
return this.form.get('password');
}
}

View File

@ -0,0 +1,36 @@
import { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { MatButtonModule } from '@angular/material/button';
import { MatCheckboxModule } from '@angular/material/checkbox';
import { MatIconModule } from '@angular/material/icon';
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
import { MatSelectModule } from '@angular/material/select';
import { TranslateModule } from '@ngx-translate/core';
import { CardModule } from '../../card/card.module';
import { FormFieldModule } from '../../form-field/form-field.module';
import { InputModule } from '../../input/input.module';
import { DialogAddSMSProviderComponent } from './dialog-add-sms-provider/dialog-add-sms-provider.component';
import { NotificationSettingsComponent } from './notification-settings.component';
@NgModule({
declarations: [NotificationSettingsComponent, DialogAddSMSProviderComponent],
imports: [
CommonModule,
CardModule,
FormsModule,
ReactiveFormsModule,
MatButtonModule,
MatCheckboxModule,
InputModule,
MatIconModule,
FormFieldModule,
MatSelectModule,
MatProgressSpinnerModule,
MatSelectModule,
TranslateModule,
],
exports: [NotificationSettingsComponent],
})
export class NotificationSettingsModule {}

View File

@ -0,0 +1,45 @@
<div class="spinner-wr">
<mat-spinner diameter="30" *ngIf="loading" color="primary"></mat-spinner>
</div>
<h2>{{ 'SETTING.OIDC.TITLE' | translate }}</h2>
<form (ngSubmit)="savePolicy()" [formGroup]="form" autocomplete="off">
<cnsl-form-field class="oidc-form-field" label="Sender Address" required="true">
<cnsl-label
>{{ 'SETTING.OIDC.ACCESSTOKENLIFETIME' | translate }}&nbsp;<strong
>({{ 'SETTING.OIDC.INHOURS' | translate }})</strong
></cnsl-label
>
<input cnslInput type="number" name="accessTokenLifetime" formControlName="accessTokenLifetime" />
</cnsl-form-field>
<cnsl-form-field class="oidc-form-field" label="Sender Name" required="true">
<cnsl-label
>{{ 'SETTING.OIDC.IDTOKENLIFETIME' | translate }}&nbsp;<strong
>({{ 'SETTING.OIDC.INHOURS' | translate }})</strong
></cnsl-label
>
<input cnslInput type="number" name="idTokenLifetime" formControlName="idTokenLifetime" />
</cnsl-form-field>
<cnsl-form-field class="oidc-form-field" label="Sender Address" required="true">
<cnsl-label
>{{ 'SETTING.OIDC.REFRESHTOKENEXPIRATION' | translate }}&nbsp;<strong
>({{ 'SETTING.OIDC.INDAYS' | translate }})</strong
></cnsl-label
>
<input cnslInput type="number" name="refreshTokenExpiration" formControlName="refreshTokenExpiration" />
</cnsl-form-field>
<cnsl-form-field class="oidc-form-field" label="Sender Name" required="true">
<cnsl-label
>{{ 'SETTING.OIDC.REFRESHTOKENIDLEEXPIRATION' | translate }}&nbsp;
<strong>({{ 'SETTING.OIDC.INDAYS' | translate }})</strong></cnsl-label
>
<input cnslInput type="number" name="refreshTokenIdleExpiration" formControlName="refreshTokenIdleExpiration" />
</cnsl-form-field>
</form>
<div class="oidc-btn-container">
<button class="save-button" (click)="savePolicy()" color="primary" type="submit" mat-raised-button>
{{ 'ACTIONS.SAVE' | translate }}
</button>
</div>

View File

@ -0,0 +1,18 @@
.spinner-wr {
margin: 0.5rem 0;
}
.oidc-form-field {
max-width: 400px;
display: block;
}
.oidc-btn-container {
display: flex;
justify-content: flex-start;
margin-top: 1rem;
.save-button {
display: block;
}
}

View File

@ -0,0 +1,24 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { OIDCConfigurationComponent } from './oidc-configuration.component';
describe('OIDCConfigurationComponent', () => {
let component: OIDCConfigurationComponent;
let fixture: ComponentFixture<OIDCConfigurationComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [OIDCConfigurationComponent],
}).compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(OIDCConfigurationComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -0,0 +1,119 @@
import { Component, OnInit } from '@angular/core';
import { AbstractControl, FormBuilder, FormGroup, Validators } from '@angular/forms';
import { Duration } from 'google-protobuf/google/protobuf/duration_pb';
import { SetDefaultLanguageResponse, UpdateOIDCSettingsRequest } from 'src/app/proto/generated/zitadel/admin_pb';
import { OIDCSettings } from 'src/app/proto/generated/zitadel/settings_pb';
import { AdminService } from 'src/app/services/admin.service';
import { ToastService } from 'src/app/services/toast.service';
@Component({
selector: 'cnsl-oidc-configuration',
templateUrl: './oidc-configuration.component.html',
styleUrls: ['./oidc-configuration.component.scss'],
})
export class OIDCConfigurationComponent implements OnInit {
public oidcSettings!: OIDCSettings.AsObject;
public loading: boolean = false;
public form!: FormGroup;
constructor(private service: AdminService, private fb: FormBuilder, private toast: ToastService) {
this.form = this.fb.group({
accessTokenLifetime: [12, [Validators.required]],
idTokenLifetime: [12, [Validators.required]],
refreshTokenExpiration: [30, [Validators.required]],
refreshTokenIdleExpiration: [90, [Validators.required]],
});
}
ngOnInit(): void {
this.fetchData();
}
private fetchData(): void {
this.service
.getOIDCSettings()
.then((oidcConfiguration) => {
if (oidcConfiguration.settings) {
this.oidcSettings = oidcConfiguration.settings;
this.accessTokenLifetime?.setValue(
oidcConfiguration.settings.accessTokenLifetime?.seconds
? oidcConfiguration.settings.accessTokenLifetime?.seconds / 60 / 60
: 12,
);
this.idTokenLifetime?.setValue(
oidcConfiguration.settings.idTokenLifetime?.seconds
? oidcConfiguration.settings.idTokenLifetime?.seconds / 60 / 60
: 12,
);
this.refreshTokenExpiration?.setValue(
oidcConfiguration.settings.refreshTokenExpiration?.seconds
? oidcConfiguration.settings.refreshTokenExpiration?.seconds / 60 / 60 / 24
: 30,
);
this.refreshTokenIdleExpiration?.setValue(
oidcConfiguration.settings.refreshTokenIdleExpiration?.seconds
? oidcConfiguration.settings.refreshTokenIdleExpiration?.seconds / 60 / 60 / 24
: 90,
);
}
})
.catch((error) => {
if (error.code === 5) {
} else {
this.toast.showError(error);
}
});
}
private updateData(): Promise<SetDefaultLanguageResponse.AsObject> {
const req = new UpdateOIDCSettingsRequest();
const accessToken = new Duration().setSeconds((this.accessTokenLifetime?.value ?? 12) * 60 * 60);
req.setAccessTokenLifetime(accessToken);
const idToken = new Duration().setSeconds((this.idTokenLifetime?.value ?? 12) * 60 * 60);
req.setIdTokenLifetime(idToken);
const refreshToken = new Duration().setSeconds((this.refreshTokenExpiration?.value ?? 30) * 60 * 60 * 24);
req.setRefreshTokenExpiration(refreshToken);
const refreshIdleToken = new Duration().setSeconds((this.refreshTokenIdleExpiration?.value ?? 90) * 60 * 60 * 24);
req.setRefreshTokenIdleExpiration(refreshIdleToken);
return (this.service as AdminService).updateOIDCSettings(req);
}
public savePolicy(): void {
const prom = this.updateData();
if (prom) {
prom
.then(() => {
this.toast.showInfo('SETTING.SMTP.SAVED', true);
this.loading = true;
setTimeout(() => {
this.fetchData();
}, 2000);
})
.catch((error) => {
this.toast.showError(error);
});
}
}
public get accessTokenLifetime(): AbstractControl | null {
return this.form.get('accessTokenLifetime');
}
public get idTokenLifetime(): AbstractControl | null {
return this.form.get('idTokenLifetime');
}
public get refreshTokenExpiration(): AbstractControl | null {
return this.form.get('refreshTokenExpiration');
}
public get refreshTokenIdleExpiration(): AbstractControl | null {
return this.form.get('refreshTokenIdleExpiration');
}
}

View File

@ -0,0 +1,30 @@
import { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { MatButtonModule } from '@angular/material/button';
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
import { MatSelectModule } from '@angular/material/select';
import { TranslateModule } from '@ngx-translate/core';
import { CardModule } from '../../card/card.module';
import { FormFieldModule } from '../../form-field/form-field.module';
import { InputModule } from '../../input/input.module';
import { OIDCConfigurationComponent } from './oidc-configuration.component';
@NgModule({
declarations: [OIDCConfigurationComponent],
imports: [
CommonModule,
CardModule,
FormsModule,
MatButtonModule,
FormFieldModule,
ReactiveFormsModule,
InputModule,
MatProgressSpinnerModule,
MatSelectModule,
TranslateModule,
],
exports: [OIDCConfigurationComponent],
})
export class OIDCConfigurationModule {}

View File

@ -42,9 +42,7 @@ export class PasswordLockoutPolicyComponent implements OnInit {
} }
private fetchData(): void { private fetchData(): void {
console.log(this.serviceType);
this.getData().then((resp) => { this.getData().then((resp) => {
console.log(resp);
if (resp.policy) { if (resp.policy) {
this.lockoutData = resp.policy; this.lockoutData = resp.policy;
} }

View File

@ -7,7 +7,7 @@
queryParam="id" queryParam="id"
> >
<ng-container *ngIf="currentSetting === 'general' && serviceType === PolicyComponentServiceType.ADMIN"> <ng-container *ngIf="currentSetting === 'general' && serviceType === PolicyComponentServiceType.ADMIN">
<cnsl-general-settings [serviceType]="serviceType"></cnsl-general-settings> <cnsl-general-settings></cnsl-general-settings>
</ng-container> </ng-container>
<ng-container *ngIf="currentSetting === 'complexity'"> <ng-container *ngIf="currentSetting === 'complexity'">
<cnsl-password-complexity-policy [serviceType]="serviceType"></cnsl-password-complexity-policy> <cnsl-password-complexity-policy [serviceType]="serviceType"></cnsl-password-complexity-policy>
@ -22,6 +22,14 @@
<ng-container *ngIf="currentSetting === 'idp'"> <ng-container *ngIf="currentSetting === 'idp'">
<cnsl-idp-settings [serviceType]="serviceType"></cnsl-idp-settings> <cnsl-idp-settings [serviceType]="serviceType"></cnsl-idp-settings>
</ng-container> </ng-container>
<ng-container *ngIf="currentSetting === 'notifications' && serviceType === PolicyComponentServiceType.ADMIN">
<cnsl-notification-settings [serviceType]="serviceType"></cnsl-notification-settings>
</ng-container>
<ng-container *ngIf="currentSetting === 'oidc' && serviceType === PolicyComponentServiceType.ADMIN">
<cnsl-oidc-configuration></cnsl-oidc-configuration>
</ng-container>
<ng-container *ngIf="currentSetting === 'branding'"> <ng-container *ngIf="currentSetting === 'branding'">
<cnsl-private-labeling-policy [serviceType]="serviceType"></cnsl-private-labeling-policy> <cnsl-private-labeling-policy [serviceType]="serviceType"></cnsl-private-labeling-policy>
</ng-container> </ng-container>

View File

@ -10,6 +10,8 @@ import { IdpSettingsModule } from '../policies/idp-settings/idp-settings.module'
import { LoginPolicyModule } from '../policies/login-policy/login-policy.module'; import { LoginPolicyModule } from '../policies/login-policy/login-policy.module';
import { LoginTextsPolicyModule } from '../policies/login-texts/login-texts.module'; import { LoginTextsPolicyModule } from '../policies/login-texts/login-texts.module';
import { MessageTextsPolicyModule } from '../policies/message-texts/message-texts.module'; import { MessageTextsPolicyModule } from '../policies/message-texts/message-texts.module';
import { NotificationSettingsModule } from '../policies/notification-settings/notification-settings.module';
import { OIDCConfigurationModule } from '../policies/oidc-configuration/oidc-configuration.module';
import { OrgIamPolicyModule } from '../policies/org-iam-policy/org-iam-policy.module'; import { OrgIamPolicyModule } from '../policies/org-iam-policy/org-iam-policy.module';
import { PasswordComplexityPolicyModule } from '../policies/password-complexity-policy/password-complexity-policy.module'; import { PasswordComplexityPolicyModule } from '../policies/password-complexity-policy/password-complexity-policy.module';
import { PasswordLockoutPolicyModule } from '../policies/password-lockout-policy/password-lockout-policy.module'; import { PasswordLockoutPolicyModule } from '../policies/password-lockout-policy/password-lockout-policy.module';
@ -37,6 +39,8 @@ import { SettingsListComponent } from './settings-list.component';
OrgIamPolicyModule, OrgIamPolicyModule,
TranslateModule, TranslateModule,
HasRolePipeModule, HasRolePipeModule,
NotificationSettingsModule,
OIDCConfigurationModule,
], ],
exports: [SettingsListComponent], exports: [SettingsListComponent],
}) })

View File

@ -5,6 +5,11 @@ export const GENERAL: SidenavSetting = {
i18nKey: 'SETTINGS.LIST.GENERAL', i18nKey: 'SETTINGS.LIST.GENERAL',
}; };
export const OIDC: SidenavSetting = {
id: 'oidc',
i18nKey: 'SETTINGS.LIST.OIDC',
};
export const LOGIN: SidenavSetting = { export const LOGIN: SidenavSetting = {
id: 'login', id: 'login',
i18nKey: 'SETTINGS.LIST.LOGIN', i18nKey: 'SETTINGS.LIST.LOGIN',
@ -29,12 +34,6 @@ export const COMPLEXITY: SidenavSetting = {
export const IDP: SidenavSetting = { id: 'idp', i18nKey: 'SETTINGS.LIST.IDP', groupI18nKey: 'SETTINGS.GROUPS.LOGIN' }; export const IDP: SidenavSetting = { id: 'idp', i18nKey: 'SETTINGS.LIST.IDP', groupI18nKey: 'SETTINGS.GROUPS.LOGIN' };
export const NOTIFICATIONPROVIDERS: SidenavSetting = {
id: 'notificationproviders',
i18nKey: 'SETTINGS.LIST.NOTIFICATIONPROVIDERS',
groupI18nKey: 'SETTINGS.GROUPS.NOTIFICATIONS',
};
export const NOTIFICATIONS: SidenavSetting = { export const NOTIFICATIONS: SidenavSetting = {
id: 'notifications', id: 'notifications',
i18nKey: 'SETTINGS.LIST.NOTIFICATIONS', i18nKey: 'SETTINGS.LIST.NOTIFICATIONS',

View File

@ -14,8 +14,8 @@ import {
LOGIN, LOGIN,
LOGINTEXTS, LOGINTEXTS,
MESSAGETEXTS, MESSAGETEXTS,
NOTIFICATIONPROVIDERS,
NOTIFICATIONS, NOTIFICATIONS,
OIDC,
PRIVACYPOLICY, PRIVACYPOLICY,
} from '../../modules/settings-list/settings'; } from '../../modules/settings-list/settings';
@ -29,16 +29,20 @@ export class InstanceSettingsComponent {
public PolicyComponentServiceType: any = PolicyComponentServiceType; public PolicyComponentServiceType: any = PolicyComponentServiceType;
public settingsList: SidenavSetting[] = [ public settingsList: SidenavSetting[] = [
GENERAL, GENERAL,
// notifications
NOTIFICATIONS,
// login
LOGIN, LOGIN,
COMPLEXITY, COMPLEXITY,
LOCKOUT, LOCKOUT,
IDP, IDP,
NOTIFICATIONS, // appearance
NOTIFICATIONPROVIDERS,
BRANDING, BRANDING,
MESSAGETEXTS, MESSAGETEXTS,
LOGINTEXTS, LOGINTEXTS,
// others
PRIVACYPOLICY, PRIVACYPOLICY,
OIDC,
]; ];
constructor(breadcrumbService: BreadcrumbService, activatedRoute: ActivatedRoute) { constructor(breadcrumbService: BreadcrumbService, activatedRoute: ActivatedRoute) {
const breadcrumbs = [ const breadcrumbs = [

View File

@ -13,8 +13,6 @@ import {
LOGIN, LOGIN,
LOGINTEXTS, LOGINTEXTS,
MESSAGETEXTS, MESSAGETEXTS,
NOTIFICATIONPROVIDERS,
NOTIFICATIONS,
PRIVACYPOLICY, PRIVACYPOLICY,
} from '../../modules/settings-list/settings'; } from '../../modules/settings-list/settings';
@ -31,8 +29,6 @@ export class OrgSettingsComponent {
COMPLEXITY, COMPLEXITY,
LOCKOUT, LOCKOUT,
IDP, IDP,
NOTIFICATIONS,
NOTIFICATIONPROVIDERS,
BRANDING, BRANDING,
MESSAGETEXTS, MESSAGETEXTS,
LOGINTEXTS, LOGINTEXTS,

View File

@ -17,6 +17,8 @@ import {
AddOIDCIDPResponse, AddOIDCIDPResponse,
AddSecondFactorToLoginPolicyRequest, AddSecondFactorToLoginPolicyRequest,
AddSecondFactorToLoginPolicyResponse, AddSecondFactorToLoginPolicyResponse,
AddSMSProviderTwilioRequest,
AddSMSProviderTwilioResponse,
DeactivateIDPRequest, DeactivateIDPRequest,
DeactivateIDPResponse, DeactivateIDPResponse,
GetCustomDomainClaimedMessageTextRequest, GetCustomDomainClaimedMessageTextRequest,
@ -51,6 +53,8 @@ import {
GetDefaultVerifyEmailMessageTextResponse, GetDefaultVerifyEmailMessageTextResponse,
GetDefaultVerifyPhoneMessageTextRequest, GetDefaultVerifyPhoneMessageTextRequest,
GetDefaultVerifyPhoneMessageTextResponse, GetDefaultVerifyPhoneMessageTextResponse,
GetFileSystemNotificationProviderRequest,
GetFileSystemNotificationProviderResponse,
GetIDPByIDRequest, GetIDPByIDRequest,
GetIDPByIDResponse, GetIDPByIDResponse,
GetLabelPolicyRequest, GetLabelPolicyRequest,
@ -59,6 +63,10 @@ import {
GetLockoutPolicyResponse, GetLockoutPolicyResponse,
GetLoginPolicyRequest, GetLoginPolicyRequest,
GetLoginPolicyResponse, GetLoginPolicyResponse,
GetLogNotificationProviderRequest,
GetLogNotificationProviderResponse,
GetOIDCSettingsRequest,
GetOIDCSettingsResponse,
GetOrgIAMPolicyRequest, GetOrgIAMPolicyRequest,
GetOrgIAMPolicyResponse, GetOrgIAMPolicyResponse,
GetPasswordAgePolicyRequest, GetPasswordAgePolicyRequest,
@ -69,6 +77,10 @@ import {
GetPreviewLabelPolicyResponse, GetPreviewLabelPolicyResponse,
GetPrivacyPolicyRequest, GetPrivacyPolicyRequest,
GetPrivacyPolicyResponse, GetPrivacyPolicyResponse,
GetSMSProviderRequest,
GetSMSProviderResponse,
GetSMTPConfigRequest,
GetSMTPConfigResponse,
GetSupportedLanguagesRequest, GetSupportedLanguagesRequest,
GetSupportedLanguagesResponse, GetSupportedLanguagesResponse,
IDPQuery, IDPQuery,
@ -86,6 +98,8 @@ import {
ListLoginPolicyMultiFactorsResponse, ListLoginPolicyMultiFactorsResponse,
ListLoginPolicySecondFactorsRequest, ListLoginPolicySecondFactorsRequest,
ListLoginPolicySecondFactorsResponse, ListLoginPolicySecondFactorsResponse,
ListSMSProvidersRequest,
ListSMSProvidersResponse,
ListViewsRequest, ListViewsRequest,
ListViewsResponse, ListViewsResponse,
ReactivateIDPRequest, ReactivateIDPRequest,
@ -150,6 +164,8 @@ import {
UpdateLockoutPolicyResponse, UpdateLockoutPolicyResponse,
UpdateLoginPolicyRequest, UpdateLoginPolicyRequest,
UpdateLoginPolicyResponse, UpdateLoginPolicyResponse,
UpdateOIDCSettingsRequest,
UpdateOIDCSettingsResponse,
UpdateOrgIAMPolicyRequest, UpdateOrgIAMPolicyRequest,
UpdateOrgIAMPolicyResponse, UpdateOrgIAMPolicyResponse,
UpdatePasswordAgePolicyRequest, UpdatePasswordAgePolicyRequest,
@ -158,6 +174,10 @@ import {
UpdatePasswordComplexityPolicyResponse, UpdatePasswordComplexityPolicyResponse,
UpdatePrivacyPolicyRequest, UpdatePrivacyPolicyRequest,
UpdatePrivacyPolicyResponse, UpdatePrivacyPolicyResponse,
UpdateSMTPConfigPasswordRequest,
UpdateSMTPConfigPasswordResponse,
UpdateSMTPConfigRequest,
UpdateSMTPConfigResponse,
} from '../proto/generated/zitadel/admin_pb'; } from '../proto/generated/zitadel/admin_pb';
import { SearchQuery } from '../proto/generated/zitadel/member_pb'; import { SearchQuery } from '../proto/generated/zitadel/member_pb';
import { ListQuery } from '../proto/generated/zitadel/object_pb'; import { ListQuery } from '../proto/generated/zitadel/object_pb';
@ -433,6 +453,37 @@ export class AdminService {
return this.grpcService.admin.setDefaultLanguage(req, null).then((resp) => resp.toObject()); return this.grpcService.admin.setDefaultLanguage(req, null).then((resp) => resp.toObject());
} }
/* notification settings */
public getSMTPConfig(): Promise<GetSMTPConfigResponse.AsObject> {
const req = new GetSMTPConfigRequest();
return this.grpcService.admin.getSMTPConfig(req, null).then((resp) => resp.toObject());
}
public updateSMTPConfig(req: UpdateSMTPConfigRequest): Promise<UpdateSMTPConfigResponse.AsObject> {
return this.grpcService.admin.updateSMTPConfig(req, null).then((resp) => resp.toObject());
}
public updateSMTPConfigPassword(req: UpdateSMTPConfigPasswordRequest): Promise<UpdateSMTPConfigPasswordResponse.AsObject> {
return this.grpcService.admin.updateSMTPConfigPassword(req, null).then((resp) => resp.toObject());
}
/* sms */
public listSMSProviders(): Promise<ListSMSProvidersResponse.AsObject> {
const req = new ListSMSProvidersRequest();
return this.grpcService.admin.listSMSProviders(req, null).then((resp) => resp.toObject());
}
public getSMSProvider(): Promise<GetSMSProviderResponse.AsObject> {
const req = new GetSMSProviderRequest();
return this.grpcService.admin.getSMSProvider(req, null).then((resp) => resp.toObject());
}
public addSMSProviderTwilio(req: AddSMSProviderTwilioRequest): Promise<AddSMSProviderTwilioResponse.AsObject> {
return this.grpcService.admin.addSMSProviderTwilio(req, null).then((resp) => resp.toObject());
}
/* lockout */ /* lockout */
public getLockoutPolicy(): Promise<GetLockoutPolicyResponse.AsObject> { public getLockoutPolicy(): Promise<GetLockoutPolicyResponse.AsObject> {
@ -504,6 +555,30 @@ export class AdminService {
return this.grpcService.admin.updateLoginPolicy(req, null).then((resp) => resp.toObject()); return this.grpcService.admin.updateLoginPolicy(req, null).then((resp) => resp.toObject());
} }
/* OIDC Configuration */
public getOIDCSettings(): Promise<GetOIDCSettingsResponse.AsObject> {
const req = new GetOIDCSettingsRequest();
return this.grpcService.admin.getOIDCSettings(req, null).then((resp) => resp.toObject());
}
public updateOIDCSettings(req: UpdateOIDCSettingsRequest): Promise<UpdateOIDCSettingsResponse.AsObject> {
return this.grpcService.admin.updateOIDCSettings(req, null).then((resp) => resp.toObject());
}
/* LOG and FILE Notifications */
public getLogNotificationProvider(): Promise<GetLogNotificationProviderResponse.AsObject> {
const req = new GetLogNotificationProviderRequest();
return this.grpcService.admin.getLogNotificationProvider(req, null).then((resp) => resp.toObject());
}
public getFileSystemNotificationProvider(
req: GetFileSystemNotificationProviderRequest,
): Promise<GetFileSystemNotificationProviderResponse.AsObject> {
return this.grpcService.admin.getFileSystemNotificationProvider(req, null).then((resp) => resp.toObject());
}
/* org iam */ /* org iam */
public getCustomOrgIAMPolicy(orgId: string): Promise<GetCustomOrgIAMPolicyResponse.AsObject> { public getCustomOrgIAMPolicy(orgId: string): Promise<GetCustomOrgIAMPolicyResponse.AsObject> {

View File

@ -816,12 +816,12 @@
"LOCKOUT": "Sperrmechanismen", "LOCKOUT": "Sperrmechanismen",
"COMPLEXITY": "Passwordkomplexität", "COMPLEXITY": "Passwordkomplexität",
"NOTIFICATIONS": "Benachrichtigungen", "NOTIFICATIONS": "Benachrichtigungen",
"NOTIFICATIONPROVIDERS": "Anbieter und SMTP",
"MESSAGETEXTS": "Benachrichtigungstexte", "MESSAGETEXTS": "Benachrichtigungstexte",
"IDP": "Identity Provider", "IDP": "Identity Provider",
"LOGINTEXTS": "Login Interface Texte", "LOGINTEXTS": "Login Interface Texte",
"BRANDING": "Branding", "BRANDING": "Branding",
"PRIVACYPOLICY": "Datenschutzrichtlinie" "PRIVACYPOLICY": "Datenschutzrichtlinie",
"OIDC": "OIDC Konfiguration"
}, },
"GROUPS": { "GROUPS": {
"NOTIFICATIONS": "Benachrichtigungen", "NOTIFICATIONS": "Benachrichtigungen",
@ -837,6 +837,38 @@
"de": "Deutsch", "de": "Deutsch",
"it": "Italiano", "it": "Italiano",
"en": "English" "en": "English"
},
"SMTP": {
"TITLE": "SMTP Einstellungen",
"SENDERADDRESS": "Sender Email-Adresse",
"SENDERNAME": "Sender Name",
"HOST": "Host",
"USER": "Benutzer",
"PASSWORD": "Passwort",
"TLS": "Transport Layer Security (TLS)",
"SAVED": "Erfolgreich gespeichert."
},
"SMS": {
"TITLE": "SMS Einstellungen",
"PROVIDERS": "Anbieter",
"PROVIDER": "SMS Anbieter",
"ADDPROVIDER": "Anbieter hinzufügen",
"ADDPROVIDERDESCRIPTION": "Wählen Sie einen der verfügbaren Anbieter und geben Sie die erforderlichen Daten ein.",
"TWILIO": {
"SID": "Sid",
"TOKEN": "Token",
"SENDERNUMBER": "Sender Number",
"ADDED": "Twilio erfolgreich hinzugefügt."
}
},
"OIDC": {
"TITLE": "OIDC Einstellungen",
"ACCESSTOKENLIFETIME": "Access Token Lifetime",
"IDTOKENLIFETIME": "Id Token Lifetime",
"REFRESHTOKENEXPIRATION": "Refresh Token Expiration",
"REFRESHTOKENIDLEEXPIRATION": "Refresh Token Idle Expiration",
"INHOURS": "Stunden",
"INDAYS": "Tage"
} }
}, },
"POLICY": { "POLICY": {

View File

@ -815,13 +815,13 @@
"LOGIN": "Login Behaviour and Security", "LOGIN": "Login Behaviour and Security",
"LOCKOUT": "Lockout", "LOCKOUT": "Lockout",
"COMPLEXITY": "Password complexity", "COMPLEXITY": "Password complexity",
"NOTIFICATIONS": "Notifications", "NOTIFICATIONS": "Notification providers and SMTP",
"NOTIFICATIONPROVIDERS": "Notification providers and SMTP",
"MESSAGETEXTS": "Message Texts", "MESSAGETEXTS": "Message Texts",
"IDP": "Identity Providers", "IDP": "Identity Providers",
"LOGINTEXTS": "Login Interface Texts", "LOGINTEXTS": "Login Interface Texts",
"BRANDING": "Branding", "BRANDING": "Branding",
"PRIVACYPOLICY": "Privacy Policy" "PRIVACYPOLICY": "Privacy Policy",
"OIDC": "OIDC Configuration"
}, },
"GROUPS": { "GROUPS": {
"NOTIFICATIONS": "Notifications", "NOTIFICATIONS": "Notifications",
@ -837,6 +837,38 @@
"de": "Deutsch", "de": "Deutsch",
"it": "Italiano", "it": "Italiano",
"en": "English" "en": "English"
},
"SMTP": {
"TITLE": "SMTP Settings",
"SENDERADDRESS": "Sender Email address",
"SENDERNAME": "Sender Name",
"HOST": "Host",
"USER": "User",
"PASSWORD": "Password",
"TLS": "Transport Layer Security (TLS)",
"SAVED": "Saved successfully!"
},
"SMS": {
"TITLE": "SMS Settings",
"PROVIDERS": "Providers",
"PROVIDER": "SMS Provider",
"ADDPROVIDER": "Add SMS Provider",
"ADDPROVIDERDESCRIPTION": "Choose one of the available providers and enter the required data.",
"TWILIO": {
"SID": "Sid",
"TOKEN": "Token",
"SENDERNUMBER": "Sender Number",
"ADDED": "Twilio added successfully."
}
},
"OIDC": {
"TITLE": "OIDC Settings",
"ACCESSTOKENLIFETIME": "Access Token Lifetime",
"IDTOKENLIFETIME": "Id Token Lifetime",
"REFRESHTOKENEXPIRATION": "Refresh Token Expiration",
"REFRESHTOKENIDLEEXPIRATION": "Refresh Token Idle Expiration",
"INHOURS": "hours",
"INDAYS": "Days"
} }
}, },
"POLICY": { "POLICY": {

View File

@ -806,18 +806,22 @@
"TITLE": "Impostazioni dell'istanza", "TITLE": "Impostazioni dell'istanza",
"DESCRIPTION": "Queste impostazioni si applicheranno a tutte le tue organizzazioni a meno che l'impostazione non venga sovrascritta." "DESCRIPTION": "Queste impostazioni si applicheranno a tutte le tue organizzazioni a meno che l'impostazione non venga sovrascritta."
}, },
"ORG": {
"TITLE": "Impostazioni dell'organizzazione",
"DESCRIPTION": "Queste impostazioni si applicheranno alla organizzazione corrente."
},
"LIST": { "LIST": {
"GENERAL": "Generale", "GENERAL": "Generale",
"LOGIN": "Comportamento login e sicurezza", "LOGIN": "Comportamento login e sicurezza",
"LOCKOUT": "Meccanismi di bloccaggio", "LOCKOUT": "Meccanismi di bloccaggio",
"COMPLEXITY": "complessità della password", "COMPLEXITY": "complessità della password",
"NOTIFICATIONS": "Notifiche", "NOTIFICATIONS": "Notifiche",
"NOTIFICATIONPROVIDERS": "Fornitori e SMTP",
"MESSAGETEXTS": "Testi di notifica", "MESSAGETEXTS": "Testi di notifica",
"IDP": "Identity Providers", "IDP": "Identity Providers",
"LOGINTEXTS": "Testi dell'interfaccia login", "LOGINTEXTS": "Testi dell'interfaccia login",
"BRANDING": "Branding", "BRANDING": "Branding",
"PRIVACYPOLICY": "Informativa sulla privacy e TOS" "PRIVACYPOLICY": "Informativa sulla privacy e TOS",
"OIDC": "OIDC Configuration"
}, },
"GROUPS": { "GROUPS": {
"NOTIFICATIONS": "Notifiche", "NOTIFICATIONS": "Notifiche",
@ -833,6 +837,38 @@
"de": "Deutsch", "de": "Deutsch",
"it": "Italiano", "it": "Italiano",
"en": "English" "en": "English"
},
"SMTP": {
"TITLE": "Impostazioni SMTP",
"SENDERADDRESS": "Indirizzo email del mittente",
"SENDERNAME": "Nome del mittente",
"HOST": "Host",
"USER": "Utente",
"PASSWORD": "Password",
"TLS": "Transport Layer Security (TLS)",
"SAVED": "Salvato con successo!"
},
"SMS": {
"TITLE": "Impostazioni SMS",
"PROVIDERS": "Fornitori",
"PROVIDER": "Fornitore SMS",
"ADDPROVIDER": "Aggiungi fornitore SMS",
"ADDPROVIDERDESCRIPTION": "Scegli uno dei provider disponibili e inserisci i dati richiesti.",
"TWILIO": {
"SID": "Sid",
"TOKEN": "Token",
"SENDERNUMBER": "Sender Number",
"ADDED": "Twilio aggiunto con successo."
}
},
"OIDC": {
"TITLE": "OIDC Einstellungen",
"ACCESSTOKENLIFETIME": "Access Token Lifetime",
"IDTOKENLIFETIME": "Id Token Lifetime",
"REFRESHTOKENEXPIRATION": "Refresh Token Expiration",
"REFRESHTOKENIDLEEXPIRATION": "Refresh Token Idle Expiration",
"INHOURS": "ore",
"INDAYS": "giorni"
} }
}, },
"POLICY": { "POLICY": {

View File

@ -1,62 +1,195 @@
const avatars = document.getElementsByClassName('lgn-avatar'); const COLORS = [
for (let i = 0; i < avatars.length; i++) { {
const displayName = avatars[i].getAttribute('loginname'); 500: "#ef4444",
if (displayName) { 200: "#fecaca",
const username = displayName.split('@')[0]; 300: "#fca5a5",
let separator = '_'; 600: "#dc2626",
if (username.includes('-')) { 700: "#b91c1c",
separator = '-'; 900: "#7f1d1d",
} },
if (username.includes('.')) { {
separator = '.'; 500: "#f97316",
} 200: "#fed7aa",
const split = username.split(separator); 300: "#fdba74",
const initials = split[0].charAt(0) + (split[1] ? split[1].charAt(0) : ''); 600: "#ea580c",
avatars[i].getElementsByClassName('initials')[0].innerHTML = initials; 700: "#c2410c",
900: "#7c2d12",
avatars[i].style.background = this.getColor(displayName); },
// set default white text instead of contrast text mode {
avatars[i].style.color = '#ffffff'; 500: "#f59e0b",
} 200: "#fde68a",
} 300: "#fcd34d",
600: "#d97706",
function getColor(userName) { 700: "#b45309",
const colors = [ 900: "#78350f",
'linear-gradient(40deg, #B44D51 30%, rgb(241,138,138))', },
'linear-gradient(40deg, #B75073 30%, rgb(234,96,143))', {
'linear-gradient(40deg, #84498E 30%, rgb(214,116,230))', 500: "#eab308",
'linear-gradient(40deg, #705998 30%, rgb(163,131,220))', 200: "#fef08a",
'linear-gradient(40deg, #5C6598 30%, rgb(135,148,222))', 300: "#fde047",
'linear-gradient(40deg, #7F90D3 30%, rgb(181,196,247))', 600: "#ca8a04",
'linear-gradient(40deg, #3E93B9 30%, rgb(150,215,245))', 700: "#a16207",
'linear-gradient(40deg, #3494A0 30%, rgb(71,205,222))', 900: "#713f12",
'linear-gradient(40deg, #25716A 30%, rgb(58,185,173))', },
'linear-gradient(40deg, #427E41 30%, rgb(97,185,96))', {
'linear-gradient(40deg, #89A568 30%, rgb(176,212,133))', 500: "#84cc16",
'linear-gradient(40deg, #90924D 30%, rgb(187,189,98))', 200: "#d9f99d",
'linear-gradient(40deg, #E2B032 30%, rgb(245,203,99))', 300: "#bef264",
'linear-gradient(40deg, #C97358 30%, rgb(245,148,118))', 600: "#65a30d",
'linear-gradient(40deg, #6D5B54 30%, rgb(152,121,108))', 700: "#4d7c0f",
'linear-gradient(40deg, #6B7980 30%, rgb(134,163,177))', 900: "#365314",
]; },
{
let hash = 0; 500: "#22c55e",
if (userName.length === 0) { 200: "#bbf7d0",
return colors[hash]; 300: "#86efac",
} 600: "#16a34a",
700: "#15803d",
hash = this.hashCode(userName); 900: "#14532d",
return colors[hash % colors.length]; },
} {
500: "#10b981",
200: "#a7f3d0",
300: "#6ee7b7",
600: "#059669",
700: "#047857",
900: "#064e3b",
},
{
500: "#14b8a6",
200: "#99f6e4",
300: "#5eead4",
600: "#0d9488",
700: "#0f766e",
900: "#134e4a",
},
{
500: "#06b6d4",
200: "#a5f3fc",
300: "#67e8f9",
600: "#0891b2",
700: "#0e7490",
900: "#164e63",
},
{
500: "#0ea5e9",
200: "#bae6fd",
300: "#7dd3fc",
600: "#0284c7",
700: "#0369a1",
900: "#0c4a6e",
},
{
500: "#3b82f6",
200: "#bfdbfe",
300: "#93c5fd",
600: "#2563eb",
700: "#1d4ed8",
900: "#1e3a8a",
},
{
500: "#6366f1",
200: "#c7d2fe",
300: "#a5b4fc",
600: "#4f46e5",
700: "#4338ca",
900: "#312e81",
},
{
500: "#8b5cf6",
200: "#ddd6fe",
300: "#c4b5fd",
600: "#7c3aed",
700: "#6d28d9",
900: "#4c1d95",
},
{
500: "#a855f7",
200: "#e9d5ff",
300: "#d8b4fe",
600: "#9333ea",
700: "#7e22ce",
900: "#581c87",
},
{
500: "#d946ef",
200: "#f5d0fe",
300: "#f0abfc",
600: "#c026d3",
700: "#a21caf",
900: "#701a75",
},
{
500: "#ec4899",
200: "#fbcfe8",
300: "#f9a8d4",
600: "#db2777",
700: "#be185d",
900: "#831843",
},
{
500: "#f43f5e",
200: "#fecdd3",
300: "#fda4af",
600: "#e11d48",
700: "#be123c",
900: "#881337",
},
];
function hashCode(str, seed = 0) { function hashCode(str, seed = 0) {
let h1 = 0xdeadbeef ^ seed, h2 = 0x41c6ce57 ^ seed; let h1 = 0xdeadbeef ^ seed,
h2 = 0x41c6ce57 ^ seed;
for (let i = 0, ch; i < str.length; i++) { for (let i = 0, ch; i < str.length; i++) {
ch = str.charCodeAt(i); ch = str.charCodeAt(i);
h1 = Math.imul(h1 ^ ch, 2654435761); h1 = Math.imul(h1 ^ ch, 2654435761);
h2 = Math.imul(h2 ^ ch, 1597334677); h2 = Math.imul(h2 ^ ch, 1597334677);
} }
h1 = Math.imul(h1 ^ (h1 >>> 16), 2246822507) ^ Math.imul(h2 ^ (h2 >>> 13), 3266489909); h1 =
h2 = Math.imul(h2 ^ (h2 >>> 16), 2246822507) ^ Math.imul(h1 ^ (h1 >>> 13), 3266489909); Math.imul(h1 ^ (h1 >>> 16), 2246822507) ^
Math.imul(h2 ^ (h2 >>> 13), 3266489909);
h2 =
Math.imul(h2 ^ (h2 >>> 16), 2246822507) ^
Math.imul(h1 ^ (h1 >>> 13), 3266489909);
return 4294967296 * (2097151 & h2) + (h1 >>> 0); return 4294967296 * (2097151 & h2) + (h1 >>> 0);
} }
function getColor(value) {
let hash = 0;
if (value.length === 0) {
return COLORS[hash];
}
hash = hashCode(value);
return COLORS[hash % COLORS.length];
}
const avatars = document.getElementsByClassName("lgn-avatar");
for (let i = 0; i < avatars.length; i++) {
const displayName = avatars[i].getAttribute("loginname");
if (displayName) {
const username = displayName.split("@")[0];
let separator = "_";
if (username.includes("-")) {
separator = "-";
}
if (username.includes(".")) {
separator = ".";
}
const split = username.split(separator);
const initials = split[0].charAt(0) + (split[1] ? split[1].charAt(0) : "");
avatars[i].getElementsByClassName("initials")[0].innerHTML = initials;
const colorPalette = this.getColor(displayName);
const isDark =
document.documentElement.classList.includes("lgn-dark-theme");
const backgroundShade = isDark ? 900 : 300;
const foregroundShade = isDark ? 200 : 900;
avatars[i].style.background = colorPalette[backgroundShade];
avatars[i].style.color = colorPalette[foregroundShade];
}
}

View File

@ -1,45 +1,53 @@
@import 'avatar'; @import "avatar";
@import "../elevation/elevation"; @import "../elevation/elevation";
@mixin lgn-avatar-theme() { @mixin lgn-avatar-theme() {
@include lgn-avatar-color(); @include lgn-avatar-color();
} }
@mixin lgn-avatar-color() { @mixin lgn-avatar-color() {
.lgn-avatar:not(.transparent) { .lgn-avatar:not(.transparent) {
@include _lgn-avatar-theme-property("background-color", false); @include _lgn-avatar-theme-property("background-color", false);
@include lgn-avatar-elevation(2); // @include lgn-avatar-elevation(2);
} }
.lgn-avatar .initials{ // .lgn-avatar .initials {
@include _lgn-avatar-theme-property("color", true); // @include _lgn-avatar-theme-property("color", true);
} // }
} }
@mixin _lgn-avatar-theme-property($property, $contrast) { @mixin _lgn-avatar-theme-property($property, $contrast) {
$color: if($contrast, var(--zitadel-color-primary-contrast), var(--zitadel-color-primary)); $color: if(
$contrast,
var(--zitadel-color-primary-contrast),
var(--zitadel-color-primary)
);
&.lgn-primary { &.lgn-primary {
#{$property}: $color; #{$property}: $color;
} }
&.lgn-accent { &.lgn-accent {
#{$property}: $color; #{$property}: $color;
} }
&.lgn-warn { &.lgn-warn {
#{$property}: $color; #{$property}: $color;
} }
&.lgn-primary, &.lgn-primary,
&.lgn-accent, &.lgn-accent,
&.lgn-warn, &.lgn-warn,
&[disabled] {
&[disabled] { &[disabled] {
&[disabled] { $btn-color: if(
$btn-color: if($property == "color", var(--zitadel-color-button-disabled), var(--itadel-color-button-disabled-background)); $property == "color",
#{$property}: $btn-color; var(--zitadel-color-button-disabled),
} var(--itadel-color-button-disabled-background)
);
#{$property}: $btn-color;
} }
}
} }
@mixin lgn-avatar-elevation($zValue, $opacity: $lgn-elevation-opacity) { @mixin lgn-avatar-elevation($zValue, $opacity: $lgn-elevation-opacity) {
@include lgn-elevation($zValue, rgb(0, 0, 0), $opacity); @include lgn-elevation($zValue, rgb(0, 0, 0), $opacity);
} }

View File

@ -2798,9 +2798,6 @@ a:hover, a:active {
color: var(--zitadel-color-warn); color: var(--zitadel-color-warn);
} }
.lgn-avatar:not(.transparent) {
box-shadow: 0px 3px 1px -2px rgba(0, 0, 0, 0.2), 0px 2px 2px 0px rgba(0, 0, 0, 0.14), 0px 1px 5px 0px rgba(0, 0, 0, 0.12);
}
.lgn-avatar:not(.transparent).lgn-primary { .lgn-avatar:not(.transparent).lgn-primary {
background-color: var(--zitadel-color-primary); background-color: var(--zitadel-color-primary);
} }
@ -2814,19 +2811,6 @@ a:hover, a:active {
background-color: var(--itadel-color-button-disabled-background); background-color: var(--itadel-color-button-disabled-background);
} }
.lgn-avatar .initials.lgn-primary {
color: var(--zitadel-color-primary-contrast);
}
.lgn-avatar .initials.lgn-accent {
color: var(--zitadel-color-primary-contrast);
}
.lgn-avatar .initials.lgn-warn {
color: var(--zitadel-color-primary-contrast);
}
.lgn-avatar .initials.lgn-primary[disabled], .lgn-avatar .initials.lgn-accent[disabled], .lgn-avatar .initials.lgn-warn[disabled], .lgn-avatar .initials[disabled][disabled] {
color: var(--zitadel-color-button-disabled);
}
.lgn-select, select { .lgn-select, select {
background-image: var(--zitadel-icon-select); background-image: var(--zitadel-icon-select);
} }

File diff suppressed because one or more lines are too long