mirror of
https://github.com/andwati/zitadel.git
synced 2026-11-07 14:52:15 +00:00
feat: manage restrictions in console (#6965)
* feat: return 404 or 409 if org reg disallowed * fix: system limit permissions * feat: add iam limits api * feat: disallow public org registrations on default instance * add integration test * test: integration * fix test * docs: describe public org registrations * avoid updating docs deps * fix system limits integration test * silence integration tests * fix linting * ignore strange linter complaints * review * improve reset properties naming * redefine the api * use restrictions aggregate * test query * simplify and test projection * test commands * fix unit tests * move integration test * support restrictions on default instance * also test GetRestrictions * self review * lint * abstract away resource owner * fix tests * configure supported languages * fix allowed languages * fix tests * default lang must not be restricted * preferred language must be allowed * change preferred languages * check languages everywhere * lint * test command side * lint * add integration test * add integration test * restrict supported ui locales * lint * lint * cleanup * lint * allow undefined preferred language * fix integration tests * update main * fix env var * ignore linter * ignore linter * improve integration test config * reduce cognitive complexity * compile * fix(console): switch back to saved language * feat(API): get allowed languages * fix(console): only make allowed languages selectable * warn when editing not allowed languages * feat: manage restrictions in console * check for duplicates * remove useless restriction checks * review * revert restriction renaming * manage languages * fix language restrictions * lint * generate * allow custom texts for supported langs for now * fix tests * cleanup * cleanup * cleanup * lint * unsupported preferred lang is allowed * fix integration test * allow unsupported preferred languages * lint * fix languages lists * simplify default language selection * translate * discard * lint * load languages for tests * load languages * lint * cleanup * lint * cleanup * get allowed only on admin * cleanup * reduce flakiness on very limited postgres * simplify langSvc * refactor according to suggestions in pr * lint * improve ux * update central allowed languages * set first allowed language as default * readd lost translations * disable sorting disallowed languages * fix permissions * lint * selectionchange for language in msg texts * initialize login texts * init message texts * lint * fix drag and drop list styles * start from 1 * cleanup * prettier * correct orgdefaultlabel * unsubscribe * lint * docs: describe language settings --------- Co-authored-by: peintnermax <[email protected]>
This commit is contained in:
co-authored by
peintnermax
parent
3842319d07
commit
d639c5200a
@@ -1,27 +0,0 @@
|
||||
<h2>{{ 'SETTING.DEFAULTLANGUAGE' | translate }}</h2>
|
||||
|
||||
<div class="spinner-wr">
|
||||
<mat-spinner diameter="30" *ngIf="loading" color="primary"></mat-spinner>
|
||||
</div>
|
||||
|
||||
<cnsl-form-field class="default-language" label="Default Language" required="true">
|
||||
<cnsl-label>{{ 'SETTING.DEFAULTLANGUAGE' | translate }}</cnsl-label>
|
||||
<mat-select [(ngModel)]="defaultLanguage" [disabled]="(['iam.policy.write'] | hasRole | async) === false">
|
||||
<mat-option *ngFor="let lang of defaultLanguageOptions" [value]="lang">
|
||||
{{ lang }} - {{ 'SETTING.LANGUAGE.' + lang | translate }}
|
||||
</mat-option>
|
||||
</mat-select>
|
||||
</cnsl-form-field>
|
||||
|
||||
<div class="general-btn-container">
|
||||
<button
|
||||
class="save-button"
|
||||
(click)="savePolicy()"
|
||||
color="primary"
|
||||
type="submit"
|
||||
mat-raised-button
|
||||
[disabled]="(['iam.policy.write'] | hasRole | async) === false"
|
||||
>
|
||||
{{ 'ACTIONS.SAVE' | translate }}
|
||||
</button>
|
||||
</div>
|
||||
@@ -1,18 +0,0 @@
|
||||
.spinner-wr {
|
||||
margin: 0.5rem 0;
|
||||
}
|
||||
|
||||
.default-language {
|
||||
max-width: 400px;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.general-btn-container {
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
margin-top: 1rem;
|
||||
|
||||
.save-button {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
@@ -1,56 +0,0 @@
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
import { SetDefaultLanguageResponse } from 'src/app/proto/generated/zitadel/admin_pb';
|
||||
import { AdminService } from 'src/app/services/admin.service';
|
||||
import { ToastService } from 'src/app/services/toast.service';
|
||||
|
||||
@Component({
|
||||
selector: 'cnsl-general-settings',
|
||||
templateUrl: './general-settings.component.html',
|
||||
styleUrls: ['./general-settings.component.scss'],
|
||||
})
|
||||
export class GeneralSettingsComponent implements OnInit {
|
||||
public defaultLanguage: string = '';
|
||||
public defaultLanguageOptions: string[] = [];
|
||||
|
||||
public loading: boolean = false;
|
||||
constructor(
|
||||
private service: AdminService,
|
||||
private toast: ToastService,
|
||||
) {}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.fetchData();
|
||||
}
|
||||
|
||||
private fetchData(): void {
|
||||
this.service.getDefaultLanguage().then((langResp) => {
|
||||
this.defaultLanguage = langResp.language;
|
||||
});
|
||||
this.service.getAllowedLanguages().then((supportedResp) => {
|
||||
this.defaultLanguageOptions = supportedResp.languagesList;
|
||||
});
|
||||
}
|
||||
|
||||
private updateData(): Promise<SetDefaultLanguageResponse.AsObject> {
|
||||
return (this.service as AdminService).setDefaultLanguage(this.defaultLanguage);
|
||||
}
|
||||
|
||||
public savePolicy(): void {
|
||||
const prom = this.updateData();
|
||||
this.loading = true;
|
||||
if (prom) {
|
||||
prom
|
||||
.then(() => {
|
||||
this.toast.showInfo('POLICY.LOGIN_POLICY.SAVED', true);
|
||||
this.loading = false;
|
||||
setTimeout(() => {
|
||||
this.fetchData();
|
||||
}, 2000);
|
||||
})
|
||||
.catch((error) => {
|
||||
this.loading = false;
|
||||
this.toast.showError(error);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { NgModule } from '@angular/core';
|
||||
import { FormsModule } 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 { HasRolePipeModule } from 'src/app/pipes/has-role-pipe/has-role-pipe.module';
|
||||
|
||||
import { CardModule } from '../../card/card.module';
|
||||
import { FormFieldModule } from '../../form-field/form-field.module';
|
||||
import { GeneralSettingsComponent } from './general-settings.component';
|
||||
|
||||
@NgModule({
|
||||
declarations: [GeneralSettingsComponent],
|
||||
imports: [
|
||||
CommonModule,
|
||||
CardModule,
|
||||
FormsModule,
|
||||
MatButtonModule,
|
||||
FormFieldModule,
|
||||
MatProgressSpinnerModule,
|
||||
MatSelectModule,
|
||||
HasRolePipeModule,
|
||||
TranslateModule,
|
||||
],
|
||||
exports: [GeneralSettingsComponent],
|
||||
})
|
||||
export class GeneralSettingsModule {}
|
||||
@@ -0,0 +1,135 @@
|
||||
<h2>{{ 'SETTING.LANGUAGES.TITLE' | translate }}</h2>
|
||||
|
||||
<div class="spinner-wr">
|
||||
<mat-spinner diameter="30" *ngIf="loading" color="primary"></mat-spinner>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<form class="languages-container-wrapper">
|
||||
<div class="languages-container">
|
||||
<div
|
||||
class="languages-list"
|
||||
cdkDropList
|
||||
id="allowed"
|
||||
[cdkDropListConnectedTo]="['notallowed']"
|
||||
(cdkDropListDropped)="drop($event)"
|
||||
[cdkDropListData]="(localState$ | async)!.allowed"
|
||||
>
|
||||
<div class="languages-top-row">
|
||||
<span class="label">{{ 'SETTING.LANGUAGES.ALLOWED' | translate }}</span>
|
||||
<button
|
||||
class="list-button"
|
||||
[disabled]="(canWriteRestrictions$ | async) === false"
|
||||
(click)="allowAll()"
|
||||
mat-stroked-button
|
||||
>
|
||||
<div class="cnsl-action-button">
|
||||
<i class="las la-object-group"></i><span>{{ 'SETTING.LANGUAGES.ALLOW_ALL' | translate }}</span>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
<div class="languages-drop-target">
|
||||
<div
|
||||
class="card languages-box"
|
||||
*ngFor="let lang of (localState$ | async)!.allowed; index as i"
|
||||
[cdkDragData]="lang"
|
||||
cdkDrag
|
||||
[cdkDragDisabled]="(canWriteRestrictions$ | async) === false"
|
||||
mat-list-item
|
||||
>
|
||||
<span class="index">{{ i + 1 }}</span>
|
||||
<span class="locale">{{ lang }}</span>
|
||||
<span class="lang">{{ 'SETTING.LANGUAGES.OPTIONS.' + lang | translate }} </span>
|
||||
<span *ngIf="lang === (defaultLang$ | async)" class="state orgdefaultlabel">{{
|
||||
'SETTING.LANGUAGES.DEFAULT' | translate
|
||||
}}</span>
|
||||
|
||||
<button
|
||||
(click)="$event.stopPropagation()"
|
||||
*ngIf="lang !== (defaultLang$ | async) && (isRemotelyAllowed$(lang) | async) === true"
|
||||
mat-icon-button
|
||||
class="more-button"
|
||||
[matMenuTriggerFor]="actions"
|
||||
data-e2e="table-actions-button"
|
||||
>
|
||||
<mat-icon>more_vert</mat-icon>
|
||||
</button>
|
||||
|
||||
<mat-menu #actions="matMenu" xPosition="before" class="language-actions-menu">
|
||||
<button
|
||||
menuActions
|
||||
mat-menu-item
|
||||
(click)="setDefaultLang(lang)"
|
||||
[disabled]="(canWriteDefaultLanguage$ | async) === false"
|
||||
>
|
||||
{{ 'SETTING.LANGUAGES.SETASDEFAULT' | translate }}
|
||||
</button>
|
||||
</mat-menu>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="languages-container">
|
||||
<div
|
||||
class="languages-list"
|
||||
cdkDropList
|
||||
id="notallowed"
|
||||
[cdkDropListConnectedTo]="['allowed']"
|
||||
(cdkDropListDropped)="drop($event)"
|
||||
[cdkDropListData]="(localState$ | async)!.notAllowed"
|
||||
[cdkDropListEnterPredicate]="defaultLangPredicate"
|
||||
cdkDropListSortingDisabled
|
||||
>
|
||||
<div class="languages-top-row">
|
||||
<span class="label">{{ 'SETTING.LANGUAGES.NOT_ALLOWED' | translate }}</span>
|
||||
<button
|
||||
class="list-button"
|
||||
[disabled]="(canWriteRestrictions$ | async) === false"
|
||||
(click)="disallowAll()"
|
||||
mat-stroked-button
|
||||
>
|
||||
<div class="cnsl-action-button">
|
||||
<i class="las la-object-group"></i><span>{{ 'SETTING.LANGUAGES.DISALLOW_ALL' | translate }}</span>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
<div class="languages-drop-target">
|
||||
<div
|
||||
class="card languages-box"
|
||||
*ngFor="let lang of (localState$ | async)!.notAllowed"
|
||||
[cdkDragData]="lang"
|
||||
cdkDrag
|
||||
[cdkDragDisabled]="(canWriteRestrictions$ | async) === false"
|
||||
mat-list-item
|
||||
>
|
||||
<span class="locale">{{ lang }}</span>
|
||||
<span class="lang">{{ 'SETTING.LANGUAGES.OPTIONS.' + lang | translate }} </span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="general-btn-container">
|
||||
<button
|
||||
class="reset-button"
|
||||
[disabled]="(canWriteRestrictions$ | async) === false"
|
||||
(click)="discard()"
|
||||
color="message-text-warn"
|
||||
mat-stroked-button
|
||||
>
|
||||
<div class="cnsl-action-button">
|
||||
<i class="las la-history"></i><span>{{ 'ACTIONS.UNSAVED.DIALOG.DISCARD' | translate }}</span>
|
||||
</div>
|
||||
</button>
|
||||
<button
|
||||
class="save-button"
|
||||
color="primary"
|
||||
type="submit"
|
||||
(click)="submit()"
|
||||
mat-raised-button
|
||||
[disabled]="(canWriteRestrictions$ | async) === false"
|
||||
>
|
||||
{{ 'ACTIONS.SAVE' | translate }}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
@@ -0,0 +1,112 @@
|
||||
.languages-container-wrapper {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
column-gap: 2rem;
|
||||
|
||||
@media only screen and (max-width: 700px) {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.languages-container {
|
||||
display: inline-block;
|
||||
max-width: 400px;
|
||||
vertical-align: top;
|
||||
width: 100%;
|
||||
|
||||
.spinner-wr {
|
||||
margin: 0.5rem 0;
|
||||
}
|
||||
|
||||
.default-language {
|
||||
max-width: 400px;
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
.general-btn-container {
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
margin-top: 1rem;
|
||||
|
||||
.save-button {
|
||||
display: block;
|
||||
margin-left: 1rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.languages-list {
|
||||
overflow: hidden;
|
||||
display: block;
|
||||
height: 100%;
|
||||
|
||||
.languages-top-row {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
margin-bottom: 1rem;
|
||||
margin-top: 1.5rem;
|
||||
|
||||
.label {
|
||||
margin-right: 1rem;
|
||||
flex: 1;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.list-button {
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.languages-box {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 0.5rem 0.5rem 0.5rem 2rem;
|
||||
height: 56px;
|
||||
cursor: move;
|
||||
margin: 2px 0;
|
||||
|
||||
.index {
|
||||
position: absolute;
|
||||
top: 4px;
|
||||
left: 4px;
|
||||
opacity: 0.5;
|
||||
font-size: 10px;
|
||||
}
|
||||
|
||||
.locale {
|
||||
width: 35px;
|
||||
margin-right: 1rem;
|
||||
}
|
||||
|
||||
.lang {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
[hoveractions] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.more-button {
|
||||
height: 40px;
|
||||
width: 40px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
[hoveractions] {
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
|
||||
.defaultlanglabel {
|
||||
margin-left: 0.5rem;
|
||||
}
|
||||
}
|
||||
+6
-6
@@ -1,19 +1,19 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { GeneralSettingsComponent } from './general-settings.component';
|
||||
import { LanguageSettingsComponent } from './language-settings.component';
|
||||
|
||||
describe('GeneralSettingsComponent', () => {
|
||||
let component: GeneralSettingsComponent;
|
||||
let fixture: ComponentFixture<GeneralSettingsComponent>;
|
||||
describe('LanguageSettingsComponent', () => {
|
||||
let component: LanguageSettingsComponent;
|
||||
let fixture: ComponentFixture<LanguageSettingsComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
declarations: [GeneralSettingsComponent],
|
||||
declarations: [LanguageSettingsComponent],
|
||||
}).compileComponents();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(GeneralSettingsComponent);
|
||||
fixture = TestBed.createComponent(LanguageSettingsComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
@@ -0,0 +1,116 @@
|
||||
import { ChangeDetectorRef, Component, OnInit } from '@angular/core';
|
||||
import { AdminService } from 'src/app/services/admin.service';
|
||||
import { ToastService } from 'src/app/services/toast.service';
|
||||
import { UntypedFormBuilder } from '@angular/forms';
|
||||
import { LanguagesService } from '../../../services/languages.service';
|
||||
import { BehaviorSubject, concat, forkJoin, from, Observable, of, Subject, switchMap, take, takeUntil } from 'rxjs';
|
||||
import { GrpcAuthService } from '../../../services/grpc-auth.service';
|
||||
import { CdkDrag, CdkDragDrop, moveItemInArray, transferArrayItem } from '@angular/cdk/drag-drop';
|
||||
import { catchError, map } from 'rxjs/operators';
|
||||
|
||||
interface State {
|
||||
allowed: string[];
|
||||
notAllowed: string[];
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'cnsl-language-settings',
|
||||
templateUrl: './language-settings.component.html',
|
||||
styleUrls: ['./language-settings.component.scss'],
|
||||
})
|
||||
export class LanguageSettingsComponent {
|
||||
public canWriteRestrictions$: Observable<boolean> = this.authService.isAllowed(['iam.restrictions.write']);
|
||||
public canWriteDefaultLanguage$: Observable<boolean> = this.authService.isAllowed(['iam.write']);
|
||||
|
||||
public localState$ = new BehaviorSubject<State>({ allowed: [], notAllowed: [] });
|
||||
public remoteState$ = new BehaviorSubject<State>({ allowed: [], notAllowed: [] });
|
||||
public defaultLang$ = new BehaviorSubject<string>('');
|
||||
|
||||
public loading: boolean = false;
|
||||
constructor(
|
||||
private service: AdminService,
|
||||
private toast: ToastService,
|
||||
private langSvc: LanguagesService,
|
||||
private authService: GrpcAuthService,
|
||||
) {
|
||||
const sub = forkJoin([
|
||||
langSvc.allowed$.pipe(take(1)),
|
||||
langSvc.notAllowed$.pipe(take(1)),
|
||||
from(this.service.getDefaultLanguage()).pipe(take(1)),
|
||||
]).subscribe({
|
||||
next: ([allowed, notAllowed, { language: defaultLang }]) => {
|
||||
this.defaultLang$.next(defaultLang);
|
||||
this.remoteState$.next({ notAllowed: [...notAllowed], ...{ allowed: [...allowed] } });
|
||||
this.localState$.next({ notAllowed: [...notAllowed], ...{ allowed: [...allowed] } });
|
||||
},
|
||||
error: this.toast.showError,
|
||||
complete: () => {
|
||||
sub.unsubscribe();
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
drop(event: CdkDragDrop<string[]>) {
|
||||
if (event.previousContainer === event.container) {
|
||||
moveItemInArray(event.container.data, event.previousIndex, event.currentIndex);
|
||||
} else {
|
||||
transferArrayItem(event.previousContainer.data, event.container.data, event.previousIndex, event.currentIndex);
|
||||
}
|
||||
}
|
||||
|
||||
public defaultLangPredicate = (lang: CdkDrag<string>) => {
|
||||
return !!lang?.data && lang.data !== this.defaultLang$.value;
|
||||
};
|
||||
|
||||
public isRemotelyAllowed$(lang: string): Observable<boolean> {
|
||||
return this.remoteState$.pipe(map(({ allowed }) => allowed.includes(lang)));
|
||||
}
|
||||
|
||||
public allowAll(): void {
|
||||
this.localState$.next({ allowed: [...this.allLocalLangs()], notAllowed: [] });
|
||||
}
|
||||
|
||||
public disallowAll(): void {
|
||||
const disallowed = this.allLocalLangs().filter((lang) => lang !== this.defaultLang$.value);
|
||||
this.localState$.next({ allowed: [this.defaultLang$.value], notAllowed: disallowed });
|
||||
}
|
||||
|
||||
public submit(): void {
|
||||
const { allowed, notAllowed } = this.localState$.value;
|
||||
const sub = from(this.service.setRestrictions(undefined, allowed)).subscribe({
|
||||
next: () => {
|
||||
this.remoteState$.next({
|
||||
allowed: [...allowed],
|
||||
notAllowed: [...notAllowed],
|
||||
});
|
||||
this.langSvc.newAllowed(allowed);
|
||||
this.toast.showInfo('SETTING.LANGUAGES.ALLOWED_SAVED', true);
|
||||
},
|
||||
error: this.toast.showError,
|
||||
complete: () => {
|
||||
sub.unsubscribe();
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
public discard(): void {
|
||||
this.localState$.next(this.remoteState$.value);
|
||||
}
|
||||
|
||||
public setDefaultLang(lang: string): void {
|
||||
const sub = from(this.service.setDefaultLanguage(lang)).subscribe({
|
||||
next: () => {
|
||||
this.defaultLang$.next(lang);
|
||||
this.toast.showInfo('SETTING.LANGUAGES.DEFAULT_SAVED', true);
|
||||
},
|
||||
error: this.toast.showError,
|
||||
complete: () => {
|
||||
sub.unsubscribe();
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
private allLocalLangs(): string[] {
|
||||
return [...this.localState$.value.allowed, ...this.localState$.value.notAllowed];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
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 { HasRolePipeModule } from 'src/app/pipes/has-role-pipe/has-role-pipe.module';
|
||||
|
||||
import { CardModule } from '../../card/card.module';
|
||||
import { FormFieldModule } from '../../form-field/form-field.module';
|
||||
import { LanguageSettingsComponent } from './language-settings.component';
|
||||
import { MatListModule } from '@angular/material/list';
|
||||
import { MatFormFieldModule } from '@angular/material/form-field';
|
||||
import { DragDropModule } from '@angular/cdk/drag-drop';
|
||||
import { MatRadioModule } from '@angular/material/radio';
|
||||
import { MatTooltipModule } from '@angular/material/tooltip';
|
||||
import { TableActionsModule } from '../../table-actions/table-actions.module';
|
||||
import { MatMenuModule } from '@angular/material/menu';
|
||||
import { MatIconModule } from '@angular/material/icon';
|
||||
|
||||
@NgModule({
|
||||
declarations: [LanguageSettingsComponent],
|
||||
imports: [
|
||||
CommonModule,
|
||||
CardModule,
|
||||
FormsModule,
|
||||
ReactiveFormsModule,
|
||||
MatFormFieldModule,
|
||||
MatButtonModule,
|
||||
MatSelectModule,
|
||||
FormFieldModule,
|
||||
MatProgressSpinnerModule,
|
||||
MatSelectModule,
|
||||
HasRolePipeModule,
|
||||
TranslateModule,
|
||||
MatListModule,
|
||||
DragDropModule,
|
||||
MatRadioModule,
|
||||
MatTooltipModule,
|
||||
MatMenuModule,
|
||||
MatIconModule,
|
||||
],
|
||||
exports: [LanguageSettingsComponent],
|
||||
})
|
||||
export class LanguageSettingsModule {}
|
||||
@@ -242,14 +242,20 @@
|
||||
| async) === false
|
||||
"
|
||||
>
|
||||
{{ 'POLICY.DATA.ALLOWREGISTER' | translate }}
|
||||
{{ 'POLICY.DATA.ALLOWREGISTERUSERS' | translate }}
|
||||
</mat-checkbox>
|
||||
</div>
|
||||
<div class="login-policy-row">
|
||||
<mat-checkbox
|
||||
class="login-policy-toggle"
|
||||
color="primary"
|
||||
ngDefaultControl
|
||||
[(ngModel)]="allowOrgRegistration"
|
||||
*ngIf="serviceType === PolicyComponentServiceType.ADMIN"
|
||||
[disabled]="(['iam.restrictions.write'] | hasRole | async) === false"
|
||||
>
|
||||
{{ 'POLICY.DATA.ALLOWREGISTERORGS' | translate }}
|
||||
</mat-checkbox>
|
||||
|
||||
<!-- <ng-template #regInfo>
|
||||
<cnsl-info-section class="info">
|
||||
{{ 'POLICY.DATA.ALLOWREGISTER_DESC' | translate }}
|
||||
</cnsl-info-section>
|
||||
</ng-template> -->
|
||||
</div>
|
||||
<div class="login-policy-row">
|
||||
<mat-checkbox
|
||||
@@ -428,6 +434,7 @@
|
||||
type="submit"
|
||||
mat-raised-button
|
||||
[disabled]="
|
||||
(['iam.restrictions.write'] | hasRole | async) === false &&
|
||||
([
|
||||
serviceType === PolicyComponentServiceType.ADMIN
|
||||
? 'iam.policy.write'
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { Component, Injector, Input, OnInit, Type } from '@angular/core';
|
||||
import { Component, Injector, Input, OnDestroy, OnInit, Type } from '@angular/core';
|
||||
import { AbstractControl, UntypedFormBuilder, UntypedFormGroup } from '@angular/forms';
|
||||
import { MatDialog } from '@angular/material/dialog';
|
||||
import { Duration } from 'google-protobuf/google/protobuf/duration_pb';
|
||||
import { take } from 'rxjs';
|
||||
import { firstValueFrom, forkJoin, from, Observable, of, Subject, take } from 'rxjs';
|
||||
import {
|
||||
GetLoginPolicyResponse as AdminGetLoginPolicyResponse,
|
||||
UpdateLoginPolicyRequest,
|
||||
@@ -24,19 +24,21 @@ import { InfoSectionType } from '../../info-section/info-section.component';
|
||||
import { WarnDialogComponent } from '../../warn-dialog/warn-dialog.component';
|
||||
import { PolicyComponentServiceType } from '../policy-component-types.enum';
|
||||
import { LoginMethodComponentType } from './factor-table/factor-table.component';
|
||||
import { catchError, map, takeUntil } from 'rxjs/operators';
|
||||
|
||||
@Component({
|
||||
selector: 'cnsl-login-policy',
|
||||
templateUrl: './login-policy.component.html',
|
||||
styleUrls: ['./login-policy.component.scss'],
|
||||
})
|
||||
export class LoginPolicyComponent implements OnInit {
|
||||
export class LoginPolicyComponent implements OnInit, OnDestroy {
|
||||
public LoginMethodComponentType: any = LoginMethodComponentType;
|
||||
public passwordlessTypes: Array<PasswordlessType> = [
|
||||
PasswordlessType.PASSWORDLESS_TYPE_NOT_ALLOWED,
|
||||
PasswordlessType.PASSWORDLESS_TYPE_ALLOWED,
|
||||
];
|
||||
public loginData?: LoginPolicy.AsObject;
|
||||
public allowOrgRegistration: boolean = false;
|
||||
|
||||
public service!: ManagementService | AdminService;
|
||||
public PolicyComponentServiceType: any = PolicyComponentServiceType;
|
||||
@@ -52,6 +54,8 @@ export class LoginPolicyComponent implements OnInit {
|
||||
secondFactorCheckLifetime: [{ disabled: true }, [requiredValidator]],
|
||||
multiFactorCheckLifetime: [{ disabled: true }, [requiredValidator]],
|
||||
});
|
||||
private destroy$: Subject<void> = new Subject();
|
||||
|
||||
constructor(
|
||||
private toast: ToastService,
|
||||
private injector: Injector,
|
||||
@@ -60,43 +64,59 @@ export class LoginPolicyComponent implements OnInit {
|
||||
private dialog: MatDialog,
|
||||
) {}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.destroy$.next();
|
||||
this.destroy$.complete();
|
||||
}
|
||||
|
||||
public fetchData(): void {
|
||||
this.getData()
|
||||
.then((resp) => {
|
||||
if (resp.policy) {
|
||||
this.loginData = resp.policy;
|
||||
this.loading = false;
|
||||
const data$ = forkJoin([
|
||||
this.serviceType === PolicyComponentServiceType.ADMIN
|
||||
? from((this.service as AdminService).getRestrictions()).pipe(
|
||||
map(({ disallowPublicOrgRegistration }) => disallowPublicOrgRegistration),
|
||||
)
|
||||
: of(true),
|
||||
from(this.getData()),
|
||||
]);
|
||||
|
||||
this.passwordCheckLifetime?.setValue(
|
||||
this.loginData.passwordCheckLifetime?.seconds ? this.loginData.passwordCheckLifetime?.seconds / 60 / 60 : 0,
|
||||
);
|
||||
|
||||
this.externalLoginCheckLifetime?.setValue(
|
||||
this.loginData.externalLoginCheckLifetime?.seconds
|
||||
? this.loginData.externalLoginCheckLifetime?.seconds / 60 / 60
|
||||
: 0,
|
||||
);
|
||||
|
||||
this.mfaInitSkipLifetime?.setValue(
|
||||
this.loginData.mfaInitSkipLifetime?.seconds ? this.loginData.mfaInitSkipLifetime?.seconds / 60 / 60 : 0,
|
||||
);
|
||||
|
||||
this.secondFactorCheckLifetime?.setValue(
|
||||
this.loginData.secondFactorCheckLifetime?.seconds
|
||||
? this.loginData.secondFactorCheckLifetime?.seconds / 60 / 60
|
||||
: 0,
|
||||
);
|
||||
|
||||
this.multiFactorCheckLifetime?.setValue(
|
||||
this.loginData.multiFactorCheckLifetime?.seconds
|
||||
? this.loginData.multiFactorCheckLifetime?.seconds / 60 / 60
|
||||
: 0,
|
||||
);
|
||||
const sub = data$.pipe(takeUntil(this.destroy$)).subscribe({
|
||||
next: ([disallowPublicOrgRegistration, resp]) => {
|
||||
this.allowOrgRegistration = !disallowPublicOrgRegistration;
|
||||
if (!resp.policy) {
|
||||
return;
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
this.toast.showError(error);
|
||||
});
|
||||
this.loginData = resp.policy;
|
||||
this.loading = false;
|
||||
|
||||
this.passwordCheckLifetime?.setValue(
|
||||
this.loginData.passwordCheckLifetime?.seconds ? this.loginData.passwordCheckLifetime?.seconds / 60 / 60 : 0,
|
||||
);
|
||||
|
||||
this.externalLoginCheckLifetime?.setValue(
|
||||
this.loginData.externalLoginCheckLifetime?.seconds
|
||||
? this.loginData.externalLoginCheckLifetime?.seconds / 60 / 60
|
||||
: 0,
|
||||
);
|
||||
|
||||
this.mfaInitSkipLifetime?.setValue(
|
||||
this.loginData.mfaInitSkipLifetime?.seconds ? this.loginData.mfaInitSkipLifetime?.seconds / 60 / 60 : 0,
|
||||
);
|
||||
|
||||
this.secondFactorCheckLifetime?.setValue(
|
||||
this.loginData.secondFactorCheckLifetime?.seconds
|
||||
? this.loginData.secondFactorCheckLifetime?.seconds / 60 / 60
|
||||
: 0,
|
||||
);
|
||||
|
||||
this.multiFactorCheckLifetime?.setValue(
|
||||
this.loginData.multiFactorCheckLifetime?.seconds ? this.loginData.multiFactorCheckLifetime?.seconds / 60 / 60 : 0,
|
||||
);
|
||||
},
|
||||
error: this.toast.showError,
|
||||
complete: () => {
|
||||
sub.unsubscribe();
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
public ngOnInit(): void {
|
||||
@@ -142,7 +162,9 @@ export class LoginPolicyComponent implements OnInit {
|
||||
}
|
||||
}
|
||||
|
||||
private async updateData(): Promise<UpdateLoginPolicyResponse.AsObject> {
|
||||
private async updateData(): Promise<any> {
|
||||
const calls: Observable<any>[] = [];
|
||||
|
||||
if (this.loginData) {
|
||||
switch (this.serviceType) {
|
||||
case PolicyComponentServiceType.MGMT:
|
||||
@@ -179,7 +201,8 @@ export class LoginPolicyComponent implements OnInit {
|
||||
mgmtreq.setIgnoreUnknownUsernames(this.loginData.ignoreUnknownUsernames);
|
||||
mgmtreq.setDefaultRedirectUri(this.loginData.defaultRedirectUri);
|
||||
|
||||
return (this.service as ManagementService).addCustomLoginPolicy(mgmtreq);
|
||||
calls.push(from((this.service as ManagementService).addCustomLoginPolicy(mgmtreq)));
|
||||
break;
|
||||
} else {
|
||||
const mgmtreq = new UpdateCustomLoginPolicyRequest();
|
||||
mgmtreq.setAllowExternalIdp(this.loginData.allowExternalIdp);
|
||||
@@ -211,7 +234,8 @@ export class LoginPolicyComponent implements OnInit {
|
||||
mgmtreq.setIgnoreUnknownUsernames(this.loginData.ignoreUnknownUsernames);
|
||||
mgmtreq.setDefaultRedirectUri(this.loginData.defaultRedirectUri);
|
||||
|
||||
return (this.service as ManagementService).updateCustomLoginPolicy(mgmtreq);
|
||||
calls.push(from((this.service as ManagementService).updateCustomLoginPolicy(mgmtreq)));
|
||||
break;
|
||||
}
|
||||
case PolicyComponentServiceType.ADMIN:
|
||||
const adminreq = new UpdateLoginPolicyRequest();
|
||||
@@ -243,11 +267,21 @@ export class LoginPolicyComponent implements OnInit {
|
||||
adminreq.setIgnoreUnknownUsernames(this.loginData.ignoreUnknownUsernames);
|
||||
adminreq.setDefaultRedirectUri(this.loginData.defaultRedirectUri);
|
||||
|
||||
return (this.service as AdminService).updateLoginPolicy(adminreq);
|
||||
calls.push(from((this.service as AdminService).setRestrictions(!this.allowOrgRegistration)));
|
||||
calls.push(from((this.service as AdminService).updateLoginPolicy(adminreq)));
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
return Promise.reject();
|
||||
calls.push(from(Promise.reject()));
|
||||
}
|
||||
return firstValueFrom(
|
||||
forkJoin(calls).pipe(
|
||||
catchError((error, caught) => {
|
||||
// We just ignore the policy not changed error!
|
||||
return (error as { message: string }).message.includes('INSTANCE-5M9vdd') ? of(true) : caught;
|
||||
}),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
public savePolicy(): void {
|
||||
|
||||
@@ -6,9 +6,6 @@
|
||||
[settingsList]="settingsList"
|
||||
queryParam="id"
|
||||
>
|
||||
<ng-container *ngIf="currentSetting === 'general' && serviceType === PolicyComponentServiceType.ADMIN">
|
||||
<cnsl-general-settings></cnsl-general-settings>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="currentSetting === 'complexity'">
|
||||
<cnsl-password-complexity-policy [serviceType]="serviceType"></cnsl-password-complexity-policy>
|
||||
</ng-container>
|
||||
@@ -57,4 +54,7 @@
|
||||
<ng-container *ngIf="currentSetting === 'privacypolicy'">
|
||||
<cnsl-privacy-policy [serviceType]="serviceType"></cnsl-privacy-policy>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="currentSetting === 'languages' && serviceType === PolicyComponentServiceType.ADMIN">
|
||||
<cnsl-language-settings></cnsl-language-settings>
|
||||
</ng-container>
|
||||
</cnsl-sidenav>
|
||||
|
||||
@@ -7,7 +7,7 @@ import { HasRolePipeModule } from 'src/app/pipes/has-role-pipe/has-role-pipe.mod
|
||||
import { CardModule } from '../card/card.module';
|
||||
import DomainsModule from '../domains/domains.module';
|
||||
import { DomainPolicyModule } from '../policies/domain-policy/domain-policy.module';
|
||||
import { GeneralSettingsModule } from '../policies/general-settings/general-settings.module';
|
||||
import { LanguageSettingsModule } from '../policies/language-settings/language-settings.module';
|
||||
import { IdpSettingsModule } from '../policies/idp-settings/idp-settings.module';
|
||||
import { LoginPolicyModule } from '../policies/login-policy/login-policy.module';
|
||||
import { LoginTextsPolicyModule } from '../policies/login-texts/login-texts.module';
|
||||
@@ -36,7 +36,7 @@ import { SettingsListComponent } from './settings-list.component';
|
||||
PasswordComplexityPolicyModule,
|
||||
PasswordLockoutPolicyModule,
|
||||
PrivateLabelingPolicyModule,
|
||||
GeneralSettingsModule,
|
||||
LanguageSettingsModule,
|
||||
NotificationPolicyModule,
|
||||
IdpSettingsModule,
|
||||
PrivacyPolicyModule,
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { PolicyComponentServiceType } from '../policies/policy-component-types.enum';
|
||||
import { SidenavSetting } from '../sidenav/sidenav.component';
|
||||
|
||||
export const GENERAL: SidenavSetting = {
|
||||
id: 'general',
|
||||
i18nKey: 'SETTINGS.LIST.GENERAL',
|
||||
export const LANGUAGES: SidenavSetting = {
|
||||
id: 'languages',
|
||||
i18nKey: 'SETTINGS.LIST.LANGUAGES',
|
||||
requiredRoles: {
|
||||
[PolicyComponentServiceType.ADMIN]: ['iam.policy.read'],
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user