fix: restrict languages in console (#6964)

* 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

* check for duplicates

* remove useless restriction checks

* review

* revert restriction renaming

* 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

* load languages for tests

* cleanup

* lint

* cleanup

* get allowed only on admin

* cleanup

* reduce flakiness on very limited postgres

* simplify langSvc

* refactor according to suggestions in pr

* lint

* set first allowed language as default

* selectionchange for language in msg texts

* initialize login texts

* init message texts

* lint

---------

Co-authored-by: peintnermax <[email protected]>
This commit is contained in:
Elio Bischof
2023-12-07 08:43:23 +00:00
committed by GitHub
co-authored by peintnermax
parent f09fbf8709
commit 8c85318fbd
46 changed files with 479 additions and 312 deletions
@@ -26,7 +26,7 @@ export class GeneralSettingsComponent implements OnInit {
this.service.getDefaultLanguage().then((langResp) => {
this.defaultLanguage = langResp.language;
});
this.service.getSupportedLanguages().then((supportedResp) => {
this.service.getAllowedLanguages().then((supportedResp) => {
this.defaultLanguageOptions = supportedResp.languagesList;
});
}
@@ -1,5 +1,8 @@
<h2>{{ 'POLICY.LOGIN_TEXTS.TITLE' | translate }}</h2>
<p class="cnsl-secondary-text">{{ 'POLICY.LOGIN_TEXTS.DESCRIPTION' | translate }}</p>
<cnsl-info-section class="locked" *ngIf="langSvc.isNotAllowed(language) | async" [type]="InfoSectionType.WARN">
{{ 'POLICY.LOGIN_TEXTS.ACTIVE_LANGUAGE_NOT_ALLOWED' | translate }}</cnsl-info-section
>
<div *ngIf="loading" class="spinner-wr">
<mat-spinner diameter="30" color="primary"></mat-spinner>
@@ -24,7 +27,7 @@
</div>
</button>
</div>
<form *ngIf="form" class="top-actions" [formGroup]="form">
<form *ngIf="allowed$ | async" class="top-actions" [formGroup]="form">
<cnsl-form-field class="keys">
<cnsl-label>{{ 'POLICY.LOGIN_TEXTS.KEYNAME' | translate }}</cnsl-label>
<mat-select formControlName="currentSubMap" name="currentSubMap">
@@ -35,18 +38,30 @@
</cnsl-form-field>
<cnsl-form-field class="language">
<cnsl-label>{{ 'POLICY.LOGIN_TEXTS.LOCALE' | translate }}</cnsl-label>
<mat-select formControlName="locale" name="locale">
<mat-option *ngFor="let loc of LOCALES" [value]="loc">
<cnsl-label>{{ 'POLICY.LOGIN_TEXTS.LANGUAGE' | translate }}</cnsl-label>
<mat-select formControlName="language" name="language">
<mat-option *ngFor="let lang of allowed$ | async" [value]="lang">
<div class="centerline">
<span
>{{ loc }}
>{{ lang }}
<span class="lighter cnsl-secondary-text"
>|&nbsp;{{ 'POLICY.LOGIN_TEXTS.LOCALES.' + loc | translate }}</span
>|&nbsp;{{ 'POLICY.LOGIN_TEXTS.LANGUAGES.' + lang | translate }}</span
></span
>
</div>
</mat-option>
<mat-optgroup [label]="'POLICY.LOGIN_TEXTS.LANGUAGES_NOT_ALLOWED' | translate" *ngIf="langSvc.restricted$ | async">
<mat-option *ngFor="let lang of langSvc.notAllowed$ | async" [value]="lang">
<div class="centerline">
<span
>{{ lang }}
<span class="lighter cnsl-secondary-text"
>|&nbsp;{{ 'POLICY.LOGIN_TEXTS.LANGUAGES.' + lang | translate }}</span
></span
>
</div>
</mat-option>
</mat-optgroup>
</mat-select>
</cnsl-form-field>
</form>
@@ -1,8 +1,8 @@
import { Component, Injector, Input, OnDestroy, OnInit, Type } from '@angular/core';
import { UntypedFormControl, UntypedFormGroup } from '@angular/forms';
import { FormControl, UntypedFormGroup } from '@angular/forms';
import { MatDialog } from '@angular/material/dialog';
import { Timestamp } from 'google-protobuf/google/protobuf/timestamp_pb';
import { BehaviorSubject, from, interval, Observable, of, Subject, Subscription } from 'rxjs';
import { BehaviorSubject, from, interval, Observable, of, Subject, Subscription, switchMap, take, tap } from 'rxjs';
import { map, pairwise, startWith, takeUntil } from 'rxjs/operators';
import {
GetCustomLoginTextsRequest as AdminGetCustomLoginTextsRequest,
@@ -19,11 +19,11 @@ import { GrpcAuthService } from 'src/app/services/grpc-auth.service';
import { ManagementService } from 'src/app/services/mgmt.service';
import { ToastService } from 'src/app/services/toast.service';
import { supportedLanguages } from 'src/app/utils/language';
import { InfoSectionType } from '../../info-section/info-section.component';
import { WarnDialogComponent } from '../../warn-dialog/warn-dialog.component';
import { PolicyComponentServiceType } from '../policy-component-types.enum';
import { mapRequestValues } from './helper';
import { LanguagesService } from '../../../services/languages.service';
const MIN_INTERVAL_SECONDS = 10; // if the difference of a newer version to the current exceeds this time, a refresh button is shown.
@@ -110,7 +110,6 @@ export class LoginTextsComponent implements OnInit, OnDestroy {
@Input() public serviceType: PolicyComponentServiceType = PolicyComponentServiceType.MGMT;
public KeyNamesArray: string[] = KeyNamesArray;
public LOCALES: string[] = supportedLanguages;
private sub: Subscription = new Subscription();
@@ -119,9 +118,15 @@ export class LoginTextsComponent implements OnInit, OnDestroy {
public destroy$: Subject<void> = new Subject();
public InfoSectionType: any = InfoSectionType;
public form: UntypedFormGroup = new UntypedFormGroup({
currentSubMap: new UntypedFormControl('emailVerificationDoneText'),
locale: new UntypedFormControl('en'),
currentSubMap: new FormControl<string>('emailVerificationDoneText'),
language: new FormControl<string>('en'),
});
public allowed$: Observable<string[]> = this.langSvc.allowed$.pipe(
take(1),
tap(([firstAllowed]) => {
this.form.get('language')?.setValue(firstAllowed);
}),
);
public isDefault: boolean = false;
@@ -137,9 +142,10 @@ export class LoginTextsComponent implements OnInit, OnDestroy {
private injector: Injector,
private dialog: MatDialog,
private toast: ToastService,
public langSvc: LanguagesService,
) {
this.form.valueChanges
.pipe(startWith({ currentSubMap: 'emailVerificationDoneText', locale: 'en' }), pairwise(), takeUntil(this.destroy$))
.pipe(startWith({ currentSubMap: 'emailVerificationDoneText', language: 'en' }), pairwise(), takeUntil(this.destroy$))
.subscribe((pair) => {
this.checkForUnsaved(pair[0].currentSubMap).then((wantsToSave) => {
if (wantsToSave) {
@@ -162,21 +168,9 @@ export class LoginTextsComponent implements OnInit, OnDestroy {
switch (this.serviceType) {
case PolicyComponentServiceType.MGMT:
this.service = this.injector.get(ManagementService as Type<ManagementService>);
this.service.getSupportedLanguages().then((lang) => {
this.LOCALES = lang.languagesList;
});
this.loadData();
break;
case PolicyComponentServiceType.ADMIN:
this.service = this.injector.get(AdminService as Type<AdminService>);
this.service.getSupportedLanguages().then((lang) => {
this.LOCALES = lang.languagesList;
});
this.loadData();
break;
}
@@ -215,10 +209,10 @@ export class LoginTextsComponent implements OnInit, OnDestroy {
public async loadData(): Promise<any> {
this.loading = true;
const reqDefaultInit = REQUESTMAP[this.serviceType].getDefault;
reqDefaultInit.setLanguage(this.locale);
reqDefaultInit.setLanguage(this.language);
this.getDefaultInitMessageTextMap$ = from(this.getDefaultValues(reqDefaultInit)).pipe(map((m) => m[this.currentSubMap]));
const reqCustomInit = REQUESTMAP[this.serviceType].get.setLanguage(this.locale);
const reqCustomInit = REQUESTMAP[this.serviceType].get.setLanguage(this.language);
return this.getCurrentValues(reqCustomInit)
.then((policy) => {
this.loading = false;
@@ -236,14 +230,14 @@ export class LoginTextsComponent implements OnInit, OnDestroy {
}
private async patchSingleCurrentMap(): Promise<any> {
const reqCustomInit = REQUESTMAP[this.serviceType].get.setLanguage(this.locale);
const reqCustomInit = REQUESTMAP[this.serviceType].get.setLanguage(this.language);
this.getCurrentValues(reqCustomInit).then((policy) => {
this.getCustomInitMessageTextMap$.next(policy[this.currentSubMap]);
});
}
public checkForChanges(): void {
const reqCustomInit = REQUESTMAP[this.serviceType].get.setLanguage(this.locale);
const reqCustomInit = REQUESTMAP[this.serviceType].get.setLanguage(this.language);
(this.service as ManagementService).getCustomLoginTexts(reqCustomInit).then((policy) => {
this.newerPolicyChangeDate = policy.customText?.details?.changeDate;
@@ -282,7 +276,7 @@ export class LoginTextsComponent implements OnInit, OnDestroy {
this.totalCustomPolicy[this.currentSubMap] = values;
this.updateRequest = setFcn(this.totalCustomPolicy);
this.updateRequest.setLanguage(this.locale);
this.updateRequest.setLanguage(this.language);
}
}
@@ -350,7 +344,7 @@ export class LoginTextsComponent implements OnInit, OnDestroy {
if (resp) {
if (this.serviceType === PolicyComponentServiceType.MGMT) {
(this.service as ManagementService)
.resetCustomLoginTextToDefault(this.locale)
.resetCustomLoginTextToDefault(this.language)
.then(() => {
this.updateCurrentPolicyDate();
this.isDefault = true;
@@ -363,7 +357,7 @@ export class LoginTextsComponent implements OnInit, OnDestroy {
});
} else if (this.serviceType === PolicyComponentServiceType.ADMIN) {
(this.service as AdminService)
.resetCustomLoginTextToDefault(this.locale)
.resetCustomLoginTextToDefault(this.language)
.then(() => {
this.updateCurrentPolicyDate();
setTimeout(() => {
@@ -397,8 +391,12 @@ export class LoginTextsComponent implements OnInit, OnDestroy {
}
}
public get locale(): string {
return this.form.get('locale')?.value;
public get language(): string {
return this.form.get('language')?.value;
}
public set language(lang: string) {
this.form.get('language')?.setValue(lang);
}
public get currentSubMap(): string {
@@ -1,70 +1,86 @@
<h2>{{ 'POLICY.MESSAGE_TEXTS.TITLE' | translate }}</h2>
<p class="cnsl-secondary-text">{{ 'POLICY.MESSAGE_TEXTS.DESCRIPTION' | translate }}</p>
<cnsl-info-section class="locked" *ngIf="langSvc.isNotAllowed(language) | async" [type]="InfoSectionType.WARN">
{{ 'POLICY.LOGIN_TEXTS.ACTIVE_LANGUAGE_NOT_ALLOWED' | translate }}</cnsl-info-section
>
<div *ngIf="loading" class="spinner-wr">
<mat-spinner diameter="30" color="primary"></mat-spinner>
</div>
<div class="message-texts-top-actions">
<cnsl-form-field class="type">
<cnsl-label>{{ 'POLICY.MESSAGE_TEXTS.TYPE' | translate }}</cnsl-label>
<mat-select [(ngModel)]="currentType" name="currentSubMap" (selectionChange)="changedCurrentType()">
<mat-option *ngFor="let type of MESSAGETYPES | keyvalue" [value]="type.value">
{{ 'POLICY.MESSAGE_TEXTS.TYPES.' + type.value | translate }}
</mat-option>
</mat-select>
</cnsl-form-field>
<div *ngIf="allowed$ | async">
<div class="message-texts-top-actions">
<cnsl-form-field class="type">
<cnsl-label>{{ 'POLICY.MESSAGE_TEXTS.TYPE' | translate }}</cnsl-label>
<mat-select [(ngModel)]="currentType" name="currentSubMap" (selectionChange)="changedCurrentType()">
<mat-option *ngFor="let type of MESSAGETYPES | keyvalue" [value]="type.value">
{{ 'POLICY.MESSAGE_TEXTS.TYPES.' + type.value | translate }}
</mat-option>
</mat-select>
</cnsl-form-field>
<cnsl-form-field class="language">
<cnsl-label>{{ 'POLICY.LOGIN_TEXTS.LANGUAGE' | translate }}</cnsl-label>
<mat-select [(ngModel)]="language" (selectionChange)="changeLocale($event)" name="language">
<mat-option *ngFor="let lang of langSvc.allowed$ | async" [value]="lang">
<div class="centerline">
<span
>{{ lang }}
<span class="lighter cnsl-secondary-text"
>|&nbsp;{{ 'POLICY.LOGIN_TEXTS.LANGUAGES.' + lang | translate }}</span
></span
>
</div>
</mat-option>
<mat-optgroup [label]="'POLICY.LOGIN_TEXTS.LANGUAGES_NOT_ALLOWED' | translate" *ngIf="langSvc.restricted$ | async">
<mat-option *ngFor="let lang of langSvc.notAllowed$ | async" [value]="lang">
<div class="centerline">
<span
>{{ lang }}
<span class="lighter cnsl-secondary-text"
>|&nbsp;{{ 'POLICY.LOGIN_TEXTS.LANGUAGES.' + lang | translate }}</span
></span
>
</div>
</mat-option>
</mat-optgroup>
</mat-select>
</cnsl-form-field>
</div>
<cnsl-form-field class="language">
<cnsl-label>{{ 'POLICY.LOGIN_TEXTS.LOCALE' | translate }}</cnsl-label>
<mat-select [(ngModel)]="locale" name="locale" (selectionChange)="changeLocale($event)">
<mat-option *ngFor="let loc of LOCALES" [value]="loc">
<div class="centerline">
<span
>{{ loc }}
<span class="lighter cnsl-secondary-text"
>|&nbsp;{{ 'POLICY.LOGIN_TEXTS.LOCALES.' + loc | translate }}</span
></span
>
</div>
</mat-option>
</mat-select>
</cnsl-form-field>
</div>
<div class="message-text-content">
<cnsl-edit-text
[chips]="chips[currentType]"
[disabled]="(canWrite$ | async) === false"
label="one"
[default$]="getDefaultMessageTextMap$"
[current$]="getCustomMessageTextMap$"
(changedValues)="updateCurrentValues($event)"
></cnsl-edit-text>
</div>
<div class="message-text-content">
<cnsl-edit-text
[chips]="chips[currentType]"
[disabled]="(canWrite$ | async) === false"
label="one"
[default$]="getDefaultMessageTextMap$"
[current$]="getCustomMessageTextMap$"
(changedValues)="updateCurrentValues($event)"
></cnsl-edit-text>
</div>
<div class="message-text-actions">
<button
class="reset-button"
*ngIf="(getCustomMessageTextMap$ | async) && (getCustomMessageTextMap$ | async)?.['isDefault'] === false"
[disabled]="(canWrite$ | async) === false"
(click)="resetDefault()"
color="message-text-warn"
type="submit"
mat-stroked-button
>
<div class="cnsl-action-button">
<i class="las la-history"></i><span>{{ 'ACTIONS.RESETDEFAULT' | translate }}</span>
</div>
</button>
<button
class="save-button"
[disabled]="!updateRequest || (canWrite$ | async) === false"
(click)="saveCurrentMessage()"
color="primary"
type="submit"
mat-raised-button
>
{{ 'ACTIONS.SAVE' | translate }}
</button>
<div class="message-text-actions">
<button
class="reset-button"
*ngIf="(getCustomMessageTextMap$ | async) && (getCustomMessageTextMap$ | async)?.['isDefault'] === false"
[disabled]="(canWrite$ | async) === false"
(click)="resetDefault()"
color="message-text-warn"
type="submit"
mat-stroked-button
>
<div class="cnsl-action-button">
<i class="las la-history"></i><span>{{ 'ACTIONS.RESETDEFAULT' | translate }}</span>
</div>
</button>
<button
class="save-button"
[disabled]="!updateRequest || (canWrite$ | async) === false"
(click)="saveCurrentMessage()"
color="primary"
type="submit"
mat-raised-button
>
{{ 'ACTIONS.SAVE' | translate }}
</button>
</div>
</div>
@@ -1,7 +1,7 @@
import { Component, Injector, Input, OnDestroy, OnInit, Type } from '@angular/core';
import { MatDialog } from '@angular/material/dialog';
import { MatSelectChange } from '@angular/material/select';
import { BehaviorSubject, from, Observable, of, Subscription } from 'rxjs';
import { BehaviorSubject, from, Observable, of, Subscription, switchMap, take, tap } from 'rxjs';
import {
GetDefaultDomainClaimedMessageTextRequest as AdminGetDefaultDomainClaimedMessageTextRequest,
GetDefaultInitMessageTextRequest as AdminGetDefaultInitMessageTextRequest,
@@ -57,10 +57,11 @@ import { GrpcAuthService } from 'src/app/services/grpc-auth.service';
import { ManagementService } from 'src/app/services/mgmt.service';
import { ToastService } from 'src/app/services/toast.service';
import { supportedLanguages } from 'src/app/utils/language';
import { InfoSectionType } from '../../info-section/info-section.component';
import { WarnDialogComponent } from '../../warn-dialog/warn-dialog.component';
import { PolicyComponentServiceType } from '../policy-component-types.enum';
import { map } from 'rxjs/operators';
import { LanguagesService } from '../../../services/languages.service';
enum MESSAGETYPES {
INIT = 'INIT',
@@ -537,8 +538,15 @@ export class MessageTextsComponent implements OnInit, OnDestroy {
],
};
public locale: string = 'en';
public LOCALES: string[] = supportedLanguages;
public language: string = 'en';
public allowed$: Observable<string[]> = this.langSvc.allowed$.pipe(
take(1),
tap(([firstAllowed]) => {
this.language = firstAllowed;
this.loadData(this.currentType);
}),
);
private sub: Subscription = new Subscription();
public canWrite$: Observable<boolean> = this.authService.isAllowed([
this.serviceType === PolicyComponentServiceType.ADMIN
@@ -553,23 +561,16 @@ export class MessageTextsComponent implements OnInit, OnDestroy {
private toast: ToastService,
private injector: Injector,
private dialog: MatDialog,
public langSvc: LanguagesService,
) {}
ngOnInit(): void {
switch (this.serviceType) {
case PolicyComponentServiceType.MGMT:
this.service = this.injector.get(ManagementService as Type<ManagementService>);
this.service.getSupportedLanguages().then((lang) => {
this.LOCALES = lang.languagesList;
});
this.loadData(this.currentType);
break;
case PolicyComponentServiceType.ADMIN:
this.service = this.injector.get(AdminService as Type<AdminService>);
this.service.getSupportedLanguages().then((lang) => {
this.LOCALES = lang.languagesList;
});
this.loadData(this.currentType);
break;
}
}
@@ -623,7 +624,7 @@ export class MessageTextsComponent implements OnInit, OnDestroy {
}
public changeLocale(selection: MatSelectChange): void {
this.locale = selection.value;
this.language = selection.value;
this.loadData(this.currentType);
}
@@ -631,11 +632,11 @@ export class MessageTextsComponent implements OnInit, OnDestroy {
if (this.serviceType === PolicyComponentServiceType.MGMT) {
const reqDefaultInit = REQUESTMAP[this.serviceType][type].getDefault;
reqDefaultInit.setLanguage(this.locale);
reqDefaultInit.setLanguage(this.language);
this.getDefaultMessageTextMap$ = from(this.getDefaultValues(type, reqDefaultInit));
}
const reqCustomInit = REQUESTMAP[this.serviceType][type].get.setLanguage(this.locale);
const reqCustomInit = REQUESTMAP[this.serviceType][type].get.setLanguage(this.language);
this.loading = true;
return this.getCurrentValues(type, reqCustomInit)
?.then((data) => {
@@ -652,7 +653,7 @@ export class MessageTextsComponent implements OnInit, OnDestroy {
const req = REQUESTMAP[this.serviceType][this.currentType].setFcn;
const mappedValues = req(values);
this.updateRequest = mappedValues;
this.updateRequest.setLanguage(this.locale);
this.updateRequest.setLanguage(this.language);
}
public saveCurrentMessage(): any {
@@ -741,23 +742,23 @@ export class MessageTextsComponent implements OnInit, OnDestroy {
switch (this.currentType) {
case MESSAGETYPES.INIT:
return handler(this.service.resetCustomInitMessageTextToDefault(this.locale));
return handler(this.service.resetCustomInitMessageTextToDefault(this.language));
case MESSAGETYPES.VERIFYPHONE:
return handler(this.service.resetCustomVerifyPhoneMessageTextToDefault(this.locale));
return handler(this.service.resetCustomVerifyPhoneMessageTextToDefault(this.language));
case MESSAGETYPES.VERIFYSMSOTP:
return handler(this.service.resetCustomVerifySMSOTPMessageTextToDefault(this.locale));
return handler(this.service.resetCustomVerifySMSOTPMessageTextToDefault(this.language));
case MESSAGETYPES.VERIFYEMAILOTP:
return handler(this.service.resetCustomVerifyEmailOTPMessageTextToDefault(this.locale));
return handler(this.service.resetCustomVerifyEmailOTPMessageTextToDefault(this.language));
case MESSAGETYPES.VERIFYEMAIL:
return handler(this.service.resetCustomVerifyEmailMessageTextToDefault(this.locale));
return handler(this.service.resetCustomVerifyEmailMessageTextToDefault(this.language));
case MESSAGETYPES.PASSWORDRESET:
return handler(this.service.resetCustomPasswordResetMessageTextToDefault(this.locale));
return handler(this.service.resetCustomPasswordResetMessageTextToDefault(this.language));
case MESSAGETYPES.DOMAINCLAIMED:
return handler(this.service.resetCustomDomainClaimedMessageTextToDefault(this.locale));
return handler(this.service.resetCustomDomainClaimedMessageTextToDefault(this.language));
case MESSAGETYPES.PASSWORDLESS:
return handler(this.service.resetCustomPasswordlessRegistrationMessageTextToDefault(this.locale));
return handler(this.service.resetCustomPasswordlessRegistrationMessageTextToDefault(this.language));
case MESSAGETYPES.PASSWORDCHANGE:
return handler(this.service.resetCustomPasswordChangeMessageTextToDefault(this.locale));
return handler(this.service.resetCustomPasswordChangeMessageTextToDefault(this.language));
default:
return Promise.reject();
}