feat: Web Keys management (#9526)

# Which Problems Are Solved
Adds Web Keys managment to the Instance settings.

# How the Problems Are Solved
Added new page to the Instance settings.

![grafik](https://github.com/user-attachments/assets/c66bc8d7-b277-453c-9f5c-d9629cb49cb2)

# Additional Changes
Removed dataSize input from refresh table as it's not actually used
anywhere.

# Additional Context
It should always be **Web Keys** not Webkeys etc. as that is the
official naming, if I missed it somewhere please mark it.
At the moment the code won't compile because the @zitadel/client package
is not released with the new functionality.
In the code at someplaces the V3 Alpha proto is referenced because I
wasn't able to npm link @zitadel/client and @zitadel/proto at the same
time. I will fix this as soon as we have released the @zitadel/client.
Translations are also missing at the moment as I'd like @skewis6 to take
a look at the english texts first.

- Closes #8033

Open Todo's
- [ ] Fix V3 Alpha importants once @zitadel/client is released
- [ ] Add Translations
- [ ] Fix warn dialog with correct translations aswell
- [ ] Remove all todo's
- [ ] Merge main into this branch

---------

Co-authored-by: Livio Spring <livio.a@gmail.com>
Co-authored-by: Elio Bischof <elio@zitadel.com>
Co-authored-by: Kenta Yamaguchi <56732734+KEY60228@users.noreply.github.com>
Co-authored-by: Harsha Reddy <harsha.reddy@klaviyo.com>
Co-authored-by: Silvan <27845747+adlerhurst@users.noreply.github.com>
Co-authored-by: Livio Spring <livio@zitadel.com>
Co-authored-by: Stefan Benz <46600784+stebenz@users.noreply.github.com>
Co-authored-by: Max Peintner <max@caos.ch>
This commit is contained in:
Ramon
2025-03-19 15:24:13 +01:00
committed by GitHub
parent 1d6112bcbe
commit cb0623d0c6
68 changed files with 1313 additions and 128 deletions

View File

@@ -34,7 +34,7 @@
"@netlify/framework-info": "^9.8.13",
"@ngx-translate/core": "^15.0.0",
"@zitadel/client": "^1.0.6",
"@zitadel/proto": "^1.0.3",
"@zitadel/proto": "1.0.5-sha-47a2ab5",
"angular-oauth2-oidc": "^15.0.1",
"angularx-qrcode": "^16.0.0",
"buffer": "^6.0.3",

View File

@@ -1,7 +1,6 @@
<cnsl-refresh-table
[loading]="loading$ | async"
(refreshed)="refreshPage()"
[dataSize]="dataSource.data.length"
[timestamp]="keyResult?.details?.viewTimestamp"
[selection]="selection"
>

View File

@@ -6,12 +6,7 @@
</div>
<p class="events-desc cnsl-secondary-text">{{ 'DESCRIPTIONS.SETTINGS.IAM_EVENTS.DESCRIPTION' | translate }}</p>
<cnsl-refresh-table
[hideRefresh]="true"
(refreshed)="refresh()"
[dataSize]="dataSource.data.length"
[loading]="_loading | async"
>
<cnsl-refresh-table [hideRefresh]="true" (refreshed)="refresh()" [loading]="_loading | async">
<div actions>
<cnsl-filter-events (requestChanged)="filterChanged($event)"></cnsl-filter-events>
</div>

View File

@@ -2,7 +2,7 @@
<p class="failed-events-desc cnsl-secondary-text">{{ 'DESCRIPTIONS.SETTINGS.IAM_FAILED_EVENTS.DESCRIPTION' | translate }}</p>
<div class="table-wrapper">
<cnsl-refresh-table (refreshed)="loadEvents()" [dataSize]="eventDataSource.data.length" [loading]="loading$ | async">
<cnsl-refresh-table (refreshed)="loadEvents()" [loading]="loading$ | async">
<table [dataSource]="eventDataSource" mat-table class="table" aria-label="Elements">
<ng-container matColumnDef="viewName">
<th mat-header-cell *matHeaderCellDef>{{ 'IAM.FAILEDEVENTS.VIEWNAME' | translate }}</th>

View File

@@ -1,7 +1,7 @@
<h2>{{ 'DESCRIPTIONS.SETTINGS.IAM_VIEWS.TITLE' | translate }}</h2>
<p class="views-desc cnsl-secondary-text">{{ 'DESCRIPTIONS.SETTINGS.IAM_VIEWS.DESCRIPTION' | translate }}</p>
<cnsl-refresh-table (refreshed)="loadViews()" [dataSize]="dataSource.data.length" [loading]="loading$ | async">
<cnsl-refresh-table (refreshed)="loadViews()" [loading]="loading$ | async">
<table [dataSource]="dataSource" mat-table class="table views-table" aria-label="Views" matSort>
<ng-container matColumnDef="viewName">
<th mat-header-cell *matHeaderCellDef mat-sort-header>{{ 'IAM.VIEWS.VIEWNAME' | translate }}</th>

View File

@@ -1,7 +1,6 @@
<cnsl-refresh-table
[loading]="loading$ | async"
(refreshed)="refreshPage()"
[dataSize]="dataSource.data.length"
[emitRefreshOnPreviousRoutes]="['/instance/idp/create']"
[timestamp]="idpResult?.details?.viewTimestamp"
[selection]="selection"

View File

@@ -1,7 +1,6 @@
<cnsl-refresh-table
[loading]="loading$ | async"
(refreshed)="refreshPage()"
[dataSize]="dataSource.data.length"
[timestamp]="keyResult?.details?.viewTimestamp"
[selection]="selection"
>

View File

@@ -1,7 +1,6 @@
<cnsl-refresh-table
*ngIf="dataSource"
(refreshed)="changePage()"
[dataSize]="dataSource.totalResult"
[timestamp]="dataSource.viewTimestamp"
[selection]="selection"
[loading]="dataSource.loading$ | async"

View File

@@ -1,7 +1,6 @@
<cnsl-refresh-table
*ngIf="dataSource"
(refreshed)="changePage()"
[dataSize]="dataSource.totalResult"
[timestamp]="dataSource.viewTimestamp"
[hideRefresh]="true"
[selection]="selection"

View File

@@ -1,12 +1,7 @@
<cnsl-card class="metadata-details" [title]="'DESCRIPTIONS.METADATA_TITLE' | translate" [description]="description">
<mat-spinner card-actions class="spinner" diameter="20" *ngIf="loading"></mat-spinner>
<cnsl-refresh-table
*ngIf="dataSource$ | async as dataSource"
[loading]="loading"
(refreshed)="refresh.emit()"
[dataSize]="dataSource.data.length"
>
<cnsl-refresh-table *ngIf="dataSource$ | async as dataSource" [loading]="loading" (refreshed)="refresh.emit()">
<button actions [disabled]="disabled" mat-raised-button color="primary" class="edit" (click)="editClicked.emit()">
{{ 'ACTIONS.EDIT' | translate }}
</button>

View File

@@ -1,9 +1,4 @@
<cnsl-refresh-table
[hideRefresh]="true"
(refreshed)="refresh()"
[dataSize]="dataSource.data.length"
[loading]="loading$ | async"
>
<cnsl-refresh-table [hideRefresh]="true" (refreshed)="refresh()" [loading]="loading$ | async">
<cnsl-filter-org actions (filterChanged)="applySearchQuery($any($event))" (filterOpen)="filterOpen = $event">
</cnsl-filter-org>

View File

@@ -36,7 +36,6 @@ export class OrgTableComponent {
private loadingSubject: BehaviorSubject<boolean> = new BehaviorSubject<boolean>(false);
public loading$: Observable<boolean> = this.loadingSubject.asObservable();
public activeOrg!: Org.AsObject;
public OrgListSearchKey: any = OrgListSearchKey;
public initialLimit: number = 20;
public timestamp: Timestamp.AsObject | undefined = undefined;
public totalResult: number = 0;

View File

@@ -1,7 +1,6 @@
<cnsl-refresh-table
[loading]="loading$ | async"
(refreshed)="refreshPage()"
[dataSize]="dataSource.data.length"
[timestamp]="keyResult?.details?.viewTimestamp"
[selection]="selection"
>

View File

@@ -0,0 +1,60 @@
<cnsl-card
[title]="'DESCRIPTIONS.SETTINGS.WEB_KEYS.CREATE.TITLE' | translate"
[description]="'DESCRIPTIONS.SETTINGS.WEB_KEYS.CREATE.DESCRIPTION' | translate"
>
<cnsl-form-field>
<cnsl-label>{{ 'DESCRIPTIONS.SETTINGS.WEB_KEYS.CREATE.KEY_TYPE' | translate }}</cnsl-label>
<mat-select [value]="keyType()" (valueChange)="keyType.set($event)">
<mat-option value="rsa">RSA</mat-option>
<mat-option value="ecdsa">ECDSA</mat-option>
<mat-option value="ed25519">ED25519</mat-option>
</mat-select>
</cnsl-form-field>
<ng-container [ngSwitch]="keyType()">
<form *ngSwitchCase="'rsa'" [formGroup]="rsaForm" (ngSubmit)="ngSubmit.emit(rsaForm.getRawValue())">
<cnsl-form-field>
<cnsl-label>{{ 'DESCRIPTIONS.SETTINGS.WEB_KEYS.CREATE.BITS' | translate }}</cnsl-label>
<mat-select formControlName="bits">
<ng-container *ngFor="let bit of RSABits | keyvalue">
<mat-option *ngIf="Number(bit.key) as key" [value]="key">{{
$any(bit.value).replace('RSA_BITS_', '')
}}</mat-option>
</ng-container>
</mat-select>
</cnsl-form-field>
<cnsl-form-field>
<cnsl-label>{{ 'DESCRIPTIONS.SETTINGS.WEB_KEYS.CREATE.HASHER' | translate }}</cnsl-label>
<mat-select formControlName="hasher">
<ng-container *ngFor="let hasher of RSAHasher | keyvalue">
<mat-option *ngIf="Number(hasher.key) as key" [value]="key">{{
$any(hasher.value).replace('RSA_HASHER_', '')
}}</mat-option>
</ng-container>
</mat-select>
</cnsl-form-field>
<ng-container *ngTemplateOutlet="submitButton; context: { form: rsaForm }" />
</form>
<form *ngSwitchCase="'ecdsa'" [formGroup]="ecdsaForm" (ngSubmit)="ngSubmit.emit(ecdsaForm.getRawValue())">
<cnsl-form-field>
<cnsl-label>{{ 'DESCRIPTIONS.SETTINGS.WEB_KEYS.CREATE.CURVE' | translate }}</cnsl-label>
<mat-select formControlName="curve">
<ng-container *ngFor="let curve of ECDSACurve | keyvalue">
<mat-option *ngIf="Number(curve.key) as key" [value]="key">{{
$any(curve.value).replace('ECDSA_CURVE_', '')
}}</mat-option>
</ng-container>
</mat-select>
</cnsl-form-field>
<ng-container *ngTemplateOutlet="submitButton; context: { form: ecdsaForm }" />
</form>
<form *ngSwitchCase="'ed25519'" (submit)="emitEd25519($event)">
<ng-container *ngTemplateOutlet="submitButton" />
</form>
<ng-template #submitButton let-form="form">
<button [disabled]="(loading$ | async) || form?.invalid" mat-raised-button color="primary" type="submit">
<mat-spinner diameter="20" *ngIf="loading$ | async"></mat-spinner>
<span *ngIf="(loading$ | async) === false || (loading$ | async) === null">{{ 'ACTIONS.CREATE' | translate }}</span>
</button>
</ng-template>
</ng-container>
</cnsl-card>

View File

@@ -0,0 +1,60 @@
import { ChangeDetectionStrategy, Component, EventEmitter, Input, Output, signal, WritableSignal } from '@angular/core';
import { FormBuilder, FormControl, FormGroup, Validators } from '@angular/forms';
import { WebKey } from '@zitadel/proto/zitadel/webkey/v2beta/key_pb';
import { ReplaySubject } from 'rxjs';
import { RSAHasher, RSABits, ECDSACurve } from '@zitadel/proto/zitadel/webkey/v2beta/key_pb';
type RawValue<T extends FormGroup> = ReturnType<T['getRawValue']>;
@Component({
selector: 'cnsl-oidc-webkeys-create',
templateUrl: './oidc-webkeys-create.component.html',
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class OidcWebKeysCreateComponent {
protected readonly keyType: WritableSignal<NonNullable<WebKey['key']['case']>> = signal('rsa');
protected readonly RSAHasher = RSAHasher;
protected readonly RSABits = RSABits;
protected readonly ECDSACurve = ECDSACurve;
protected readonly Number = Number;
protected readonly rsaForm = this.buildRsaForm();
protected readonly ecdsaForm = this.buildEcdsaForm();
protected readonly loading$ = new ReplaySubject<boolean>();
@Output()
public readonly ngSubmit = new EventEmitter<RawValue<typeof this.rsaForm> | RawValue<typeof this.ecdsaForm> | void>();
@Input()
public set loading(loading: boolean) {
this.loading$.next(loading);
}
constructor(private readonly fb: FormBuilder) {}
private buildRsaForm() {
return this.fb.group({
bits: new FormControl<RSABits>(RSABits.RSA_BITS_2048, {
nonNullable: true,
validators: [Validators.required],
}),
hasher: new FormControl<RSAHasher>(RSAHasher.RSA_HASHER_SHA256, {
nonNullable: true,
validators: [Validators.required],
}),
});
}
private buildEcdsaForm() {
return this.fb.group({
curve: new FormControl<ECDSACurve>(ECDSACurve.ECDSA_CURVE_P256, {
nonNullable: true,
validators: [Validators.required],
}),
});
}
protected emitEd25519(event: SubmitEvent) {
event.preventDefault();
this.ngSubmit.emit();
}
}

View File

@@ -0,0 +1,40 @@
<cnsl-card
[title]="'DESCRIPTIONS.SETTINGS.WEB_KEYS.PREVIOUS_TABLE.TITLE' | translate"
[description]="'DESCRIPTIONS.SETTINGS.WEB_KEYS.PREVIOUS_TABLE.DESCRIPTION' | translate"
>
<cnsl-refresh-table [hideRefresh]="true" [loading]="(dataSource$ | async) === null">
<div class="table-wrapper">
<table
*ngIf="dataSource$ | async as dataSource"
mat-table
class="table"
aria-label="Elements"
[dataSource]="dataSource"
>
<ng-container matColumnDef="timestamp">
<th mat-header-cell *matHeaderCellDef>
{{ 'DESCRIPTIONS.SETTINGS.WEB_KEYS.PREVIOUS_TABLE.DEACTIVATED_ON' | translate }}
</th>
<td mat-cell *cnslCellDef="let row; dataSource: dataSource">
{{ row.changeDate | timestampToDate | localizedDate: 'EEE dd. MMM, HH:mm' }}
</td>
</ng-container>
<ng-container matColumnDef="id">
<th mat-header-cell *matHeaderCellDef>{{ 'APP.PAGES.ID' | translate }}</th>
<td mat-cell *cnslCellDef="let row; dataSource: dataSource">
{{ row.id }}
</td>
</ng-container>
<ng-container matColumnDef="type">
<th mat-header-cell *matHeaderCellDef>{{ 'PROJECT.TYPE.TITLE' | translate }}</th>
<td mat-cell *cnslCellDef="let row; dataSource: dataSource">
{{ row.key.case | uppercase }}
</td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="['timestamp', 'id', 'type']"></tr>
<tr mat-row *matRowDef="let row; columns: ['timestamp', 'id', 'type']"></tr>
</table>
</div>
</cnsl-refresh-table>
</cnsl-card>

View File

@@ -0,0 +1,23 @@
import { ChangeDetectionStrategy, Component, Input } from '@angular/core';
import { ReplaySubject } from 'rxjs';
import { filter, map } from 'rxjs/operators';
import { MatTableDataSource } from '@angular/material/table';
import { WebKey } from '@zitadel/proto/zitadel/webkey/v2beta/key_pb';
@Component({
selector: 'cnsl-oidc-webkeys-inactive-table',
templateUrl: './oidc-webkeys-inactive-table.component.html',
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class OidcWebKeysInactiveTableComponent {
@Input({ required: true })
public set InactiveWebKeys(webKeys: WebKey[] | null) {
this.inactiveWebKeys$.next(webKeys);
}
private inactiveWebKeys$ = new ReplaySubject<WebKey[] | null>(1);
protected dataSource$ = this.inactiveWebKeys$.pipe(
filter(Boolean),
map((webKeys) => new MatTableDataSource(webKeys)),
);
}

View File

@@ -0,0 +1,57 @@
<cnsl-refresh-table (refreshed)="refresh.emit()" [loading]="(dataSource$ | async) === null">
<div actions>
<ng-content></ng-content>
</div>
<div class="table-wrapper">
<table *ngIf="dataSource$ | async as dataSource" mat-table class="table" aria-label="Elements" [dataSource]="dataSource">
<ng-container matColumnDef="state">
<th mat-header-cell *matHeaderCellDef>{{ 'APP.PAGES.STATE' | translate }}</th>
<td mat-cell *cnslCellDef="let row; let i = index; dataSource: dataSource">
<span
class="state"
[ngClass]="{
active: i === 0,
neutral: i === 1,
}"
[ngSwitch]="i"
>
<ng-container *ngSwitchCase="0">{{ 'DESCRIPTIONS.SETTINGS.WEB_KEYS.TABLE.ACTIVE' | translate }}</ng-container>
<ng-container *ngSwitchCase="1">{{ 'DESCRIPTIONS.SETTINGS.WEB_KEYS.TABLE.NEXT' | translate }}</ng-container>
<ng-container *ngSwitchDefault>{{ 'DESCRIPTIONS.SETTINGS.WEB_KEYS.TABLE.FUTURE' | translate }}</ng-container>
</span>
</td>
</ng-container>
<ng-container matColumnDef="id">
<th mat-header-cell *matHeaderCellDef>{{ 'APP.PAGES.ID' | translate }}</th>
<td mat-cell *cnslCellDef="let row; dataSource: dataSource">
{{ row.id }}
</td>
</ng-container>
<ng-container matColumnDef="type">
<th mat-header-cell *matHeaderCellDef>{{ 'PROJECT.TYPE.TITLE' | translate }}</th>
<td mat-cell *cnslCellDef="let row; dataSource: dataSource">
{{ row.key.case | uppercase }}
</td>
</ng-container>
<ng-container matColumnDef="actions" stickyEnd>
<th mat-header-cell *matHeaderCellDef></th>
<td mat-cell *cnslCellDef="let row; dataSource: dataSource">
<cnsl-table-actions *ngIf="row.state === State.INITIAL">
<button
actions
matTooltip="{{ 'ACTIONS.REMOVE' | translate }}"
color="warn"
(click)="delete.emit(row)"
mat-icon-button
>
<i class="las la-trash"></i>
</button>
</cnsl-table-actions>
</td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="['state', 'id', 'type', 'actions']"></tr>
<tr mat-row *matRowDef="let row; columns: ['state', 'id', 'type', 'actions']"></tr>
</table>
</div>
</cnsl-refresh-table>

View File

@@ -0,0 +1,4 @@
.state.next {
color: #0e6245;
border: 1px solid #0e6245;
}

View File

@@ -0,0 +1,31 @@
import { ChangeDetectionStrategy, Component, EventEmitter, Input, Output } from '@angular/core';
import { ReplaySubject } from 'rxjs';
import { filter, map } from 'rxjs/operators';
import { MatTableDataSource } from '@angular/material/table';
import { State, WebKey } from '@zitadel/proto/zitadel/webkey/v2beta/key_pb';
@Component({
selector: 'cnsl-oidc-webkeys-table',
templateUrl: './oidc-webkeys-table.component.html',
styleUrls: ['./oidc-webkeys-table.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class OidcWebKeysTableComponent {
@Output()
public readonly refresh = new EventEmitter<void>();
@Output()
public readonly delete = new EventEmitter<WebKey>();
@Input({ required: true })
public set webKeys(webKeys: WebKey[] | null) {
this.webKeys$.next(webKeys);
}
private readonly webKeys$ = new ReplaySubject<WebKey[] | null>(1);
protected readonly dataSource$ = this.webKeys$.pipe(
filter(Boolean),
map((keys) => new MatTableDataSource(keys)),
);
protected readonly State = State;
}

View File

@@ -0,0 +1,19 @@
<h2>{{ 'SETTINGS.LIST.WEB_KEYS' | translate }}</h2>
<p>{{ 'DESCRIPTIONS.SETTINGS.WEB_KEYS.DESCRIPTION' | translate }}</p>
<h3>{{ 'DESCRIPTIONS.SETTINGS.WEB_KEYS.TABLE.TITLE' | translate }}</h3>
<p>{{ 'DESCRIPTIONS.SETTINGS.WEB_KEYS.TABLE.DESCRIPTION' | translate }}</p>
<p>{{ 'DESCRIPTIONS.SETTINGS.WEB_KEYS.TABLE.NOTE' | translate }}</p>
<cnsl-oidc-webkeys-table (refresh)="refresh.next(true)" (delete)="deleteWebKey($event)" [webKeys]="webKeys$ | async">
<button
*ngIf="nextWebKeyCandidate$ | async as nextWebKeyCandidate"
[disabled]="activateLoading()"
mat-raised-button
color="primary"
(click)="activateWebKey(nextWebKeyCandidate)"
>
<mat-spinner diameter="20" *ngIf="activateLoading()"></mat-spinner>
<span *ngIf="!activateLoading()">{{ 'DESCRIPTIONS.SETTINGS.WEB_KEYS.TABLE.ACTIVATE' | translate }}</span>
</button>
</cnsl-oidc-webkeys-table>
<cnsl-oidc-webkeys-create [loading]="createLoading()" (ngSubmit)="createWebKey($event)" />
<cnsl-oidc-webkeys-inactive-table [InactiveWebKeys]="inactiveWebKeys$ | async" />

View File

@@ -0,0 +1,217 @@
import { ChangeDetectionStrategy, Component, DestroyRef, OnInit, signal } from '@angular/core';
import { WebKeysService } from 'src/app/services/webkeys.service';
import { defer, EMPTY, firstValueFrom, Observable, ObservedValueOf, of, shareReplay, Subject, switchMap } from 'rxjs';
import { catchError, map, startWith, withLatestFrom } from 'rxjs/operators';
import { ToastService } from 'src/app/services/toast.service';
import { MessageInitShape } from '@bufbuild/protobuf';
import { OidcWebKeysCreateComponent } from './oidc-webkeys-create/oidc-webkeys-create.component';
import { TimestampToDatePipe } from 'src/app/pipes/timestamp-to-date-pipe/timestamp-to-date.pipe';
import { MatDialog } from '@angular/material/dialog';
import { WarnDialogComponent } from 'src/app/modules/warn-dialog/warn-dialog.component';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { State, WebKey } from '@zitadel/proto/zitadel/webkey/v2beta/key_pb';
import { CreateWebKeyRequestSchema } from '@zitadel/proto/zitadel/webkey/v2beta/webkey_service_pb';
import { RSAHasher, RSABits, ECDSACurve } from '@zitadel/proto/zitadel/webkey/v2beta/key_pb';
import { NewFeatureService } from 'src/app/services/new-feature.service';
import { ActivatedRoute, Router } from '@angular/router';
const CACHE_WARNING_MS = 5 * 60 * 1000; // 5 minutes
@Component({
selector: 'cnsl-oidc-webkeys',
templateUrl: './oidc-webkeys.component.html',
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class OidcWebKeysComponent implements OnInit {
protected readonly refresh = new Subject<true>();
protected readonly webKeysEnabled$: Observable<boolean>;
protected readonly webKeys$: Observable<WebKey[]>;
protected readonly inactiveWebKeys$: Observable<WebKey[]>;
protected readonly nextWebKeyCandidate$: Observable<WebKey | undefined>;
protected readonly activateLoading = signal(false);
protected readonly createLoading = signal(false);
constructor(
private readonly webKeysService: WebKeysService,
private readonly featureService: NewFeatureService,
private readonly toast: ToastService,
private readonly timestampToDatePipe: TimestampToDatePipe,
private readonly dialog: MatDialog,
private readonly destroyRef: DestroyRef,
private readonly router: Router,
private readonly route: ActivatedRoute,
) {
this.webKeysEnabled$ = this.getWebKeysEnabled().pipe(shareReplay({ refCount: true, bufferSize: 1 }));
const webKeys$ = this.getWebKeys(this.webKeysEnabled$).pipe(shareReplay({ refCount: true, bufferSize: 1 }));
this.webKeys$ = webKeys$.pipe(map((webKeys) => webKeys.filter((webKey) => webKey.state !== State.INACTIVE)));
this.inactiveWebKeys$ = webKeys$.pipe(map((webKeys) => webKeys.filter((webKey) => webKey.state === State.INACTIVE)));
this.nextWebKeyCandidate$ = this.getNextWebKeyCandidate(this.webKeys$);
}
ngOnInit(): void {
// redirect away from this page if web keys are not enabled
// this also preloads the web keys enabled state
this.webKeysEnabled$.pipe(takeUntilDestroyed(this.destroyRef)).subscribe(async (webKeysEnabled) => {
if (webKeysEnabled) {
return;
}
await this.router.navigate([], {
relativeTo: this.route,
queryParamsHandling: 'merge',
queryParams: {
id: null,
},
});
});
}
private getWebKeysEnabled() {
return defer(() => this.featureService.getInstanceFeatures()).pipe(
map((features) => features.webKey?.enabled ?? false),
catchError((err) => {
this.toast.showError(err);
return of(false);
}),
);
}
private getWebKeys(webKeysEnabled$: Observable<boolean>) {
return this.refresh.pipe(
startWith(true),
switchMap(() => {
return this.webKeysService.ListWebKeys();
}),
map(({ webKeys }) => webKeys),
catchError(async (err) => {
const webKeysEnabled = await firstValueFrom(webKeysEnabled$);
// suppress errors if web keys are not enabled
if (!webKeysEnabled) {
return [];
}
this.toast.showError(err);
return [];
}),
);
}
private getNextWebKeyCandidate(webKeys$: Observable<WebKey[]>) {
return webKeys$.pipe(
map((webKeys) => {
if (webKeys.length < 2) {
return undefined;
}
const [webKey, nextWebKey] = webKeys;
if (webKey.state !== State.ACTIVE) {
return undefined;
}
if (nextWebKey.state !== State.INITIAL) {
return undefined;
}
return nextWebKey;
}),
);
}
protected async createWebKey(event: ObservedValueOf<OidcWebKeysCreateComponent['ngSubmit']>) {
try {
this.createLoading.set(true);
const req = !event
? this.createEd25519()
: 'curve' in event
? this.createEcdsa(event.curve)
: this.createRsa(event.bits, event.hasher);
await this.webKeysService.CreateWebKey(req);
this.refresh.next(true);
} catch (error) {
this.toast.showError(error);
} finally {
this.createLoading.set(false);
}
}
private createEd25519(): MessageInitShape<typeof CreateWebKeyRequestSchema> {
return {
key: {
case: 'ed25519',
value: {},
},
};
}
private createEcdsa(curve: ECDSACurve): MessageInitShape<typeof CreateWebKeyRequestSchema> {
return {
key: {
case: 'ecdsa',
value: {
curve,
},
},
};
}
private createRsa(bits: RSABits, hasher: RSAHasher): MessageInitShape<typeof CreateWebKeyRequestSchema> {
return {
key: {
case: 'rsa',
value: {
bits,
hasher,
},
},
};
}
protected async deleteWebKey(row: WebKey) {
try {
await this.webKeysService.DeleteWebKey(row.id);
this.refresh.next(true);
} catch (err) {
this.toast.showError(err);
}
}
protected async activateWebKey(nextWebKey: WebKey) {
try {
this.activateLoading.set(true);
const creationDate = this.timestampToDatePipe.transform(nextWebKey.creationDate);
if (!creationDate) {
// noinspection ExceptionCaughtLocallyJS
throw new Error('Invalid creation date');
}
const diffToCurrentTime = Date.now() - creationDate.getTime();
if (diffToCurrentTime < CACHE_WARNING_MS && !(await this.openCacheWarnDialog())) {
return;
}
await this.webKeysService.ActivateWebKey(nextWebKey.id);
this.refresh.next(true);
} catch (error) {
this.toast.showError(error);
} finally {
this.activateLoading.set(false);
}
}
private openCacheWarnDialog() {
const dialogRef = this.dialog.open(WarnDialogComponent, {
data: {
confirmKey: 'DESCRIPTIONS.SETTINGS.WEB_KEYS.TABLE.ACTIVATE',
cancelKey: 'ACTIONS.CANCEL',
titleKey: 'Web Key is less then 5 min old',
descriptionKey: 'DESCRIPTIONS.SETTINGS.WEB_KEYS.TABLE.NOTE',
},
width: '400px',
});
const obs = dialogRef.afterClosed().pipe(map(Boolean), takeUntilDestroyed(this.destroyRef));
return firstValueFrom(obs);
}
}

View File

@@ -0,0 +1,58 @@
import { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core';
import { TranslateModule } from '@ngx-translate/core';
import { OidcWebKeysComponent } from './oidc-webkeys.component';
import { RefreshTableModule } from 'src/app/modules/refresh-table/refresh-table.module';
import { MatCheckboxModule } from '@angular/material/checkbox';
import { MatTableModule } from '@angular/material/table';
import { MatMenuModule } from '@angular/material/menu';
import { TableActionsModule } from 'src/app/modules/table-actions/table-actions.module';
import { MatButtonModule } from '@angular/material/button';
import { ActionKeysModule } from 'src/app/modules/action-keys/action-keys.module';
import { MatIconModule } from '@angular/material/icon';
import { FormFieldModule } from 'src/app/modules/form-field/form-field.module';
import { MatSelectModule } from '@angular/material/select';
import { ReactiveFormsModule } from '@angular/forms';
import { CardModule } from 'src/app/modules/card/card.module';
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
import { OidcWebKeysCreateComponent } from './oidc-webkeys-create/oidc-webkeys-create.component';
import { OidcWebKeysTableComponent } from './oidc-webkeys-table/oidc-webkeys-table.component';
import { TimestampToDatePipeModule } from 'src/app/pipes/timestamp-to-date-pipe/timestamp-to-date-pipe.module';
import { LocalizedDatePipeModule } from 'src/app/pipes/localized-date-pipe/localized-date-pipe.module';
import { OidcWebKeysInactiveTableComponent } from './oidc-webkeys-inactive-table/oidc-webkeys-inactive-table.component';
import { TypeSafeCellDefDirective } from './type-safe-cell-def.directive';
import { TimestampToDatePipe } from '../../../pipes/timestamp-to-date-pipe/timestamp-to-date.pipe';
import { MatTooltipModule } from '@angular/material/tooltip';
@NgModule({
declarations: [
OidcWebKeysComponent,
OidcWebKeysCreateComponent,
OidcWebKeysTableComponent,
OidcWebKeysInactiveTableComponent,
TypeSafeCellDefDirective,
],
providers: [TimestampToDatePipe],
imports: [
CommonModule,
TranslateModule,
RefreshTableModule,
MatCheckboxModule,
MatTableModule,
MatMenuModule,
TableActionsModule,
MatButtonModule,
ActionKeysModule,
MatIconModule,
FormFieldModule,
MatSelectModule,
ReactiveFormsModule,
CardModule,
MatProgressSpinnerModule,
TimestampToDatePipeModule,
LocalizedDatePipeModule,
MatTooltipModule,
],
exports: [OidcWebKeysComponent],
})
export class OidcWebkeysModule {}

View File

@@ -0,0 +1,16 @@
import { Directive, Input } from '@angular/core';
import { DataSource } from '@angular/cdk/collections';
import { MatCellDef } from '@angular/material/table';
import { CdkCellDef } from '@angular/cdk/table';
@Directive({
selector: '[cnslCellDef]',
providers: [{ provide: CdkCellDef, useExisting: TypeSafeCellDefDirective }],
})
export class TypeSafeCellDefDirective<T> extends MatCellDef {
@Input({ required: true }) cnslCellDefDataSource!: DataSource<T>;
static ngTemplateContextGuard<T>(_dir: TypeSafeCellDefDirective<T>, _ctx: any): _ctx is { $implicit: T; index: number } {
return true;
}
}

View File

@@ -2,7 +2,6 @@
[showSelectionActionButton]="showSelectionActionButton"
*ngIf="projectId"
(refreshed)="refreshPage()"
[dataSize]="dataSource.totalResult"
[emitRefreshOnPreviousRoutes]="['/projects/' + projectId + '/roles/create']"
[selection]="selection"
[loading]="dataSource.loading$ | async"

View File

@@ -29,7 +29,6 @@ const rotate = animation([
export class RefreshTableComponent implements OnInit {
@Input() public selection: SelectionModel<any> = new SelectionModel<any>(true, []);
@Input() public timestamp: Timestamp.AsObject | ConnectTimestamp | undefined = undefined;
@Input() public dataSize: number = 0;
@Input() public emitRefreshAfterTimeoutInMs: number = 0;
@Input() public loading: boolean | null = false;
@Input() public emitRefreshOnPreviousRoutes: string[] = [];

View File

@@ -49,6 +49,9 @@
<ng-container *ngIf="currentSetting === 'oidc' && serviceType === PolicyComponentServiceType.ADMIN">
<cnsl-oidc-configuration></cnsl-oidc-configuration>
</ng-container>
<ng-container *ngIf="currentSetting === 'webkeys' && serviceType === PolicyComponentServiceType.ADMIN">
<cnsl-oidc-webkeys />
</ng-container>
<ng-container *ngIf="currentSetting === 'secrets' && serviceType === PolicyComponentServiceType.ADMIN">
<cnsl-secret-generator></cnsl-secret-generator>
</ng-container>

View File

@@ -31,6 +31,7 @@ import { OrgTableModule } from '../org-table/org-table.module';
import { NotificationSMTPProviderModule } from '../policies/notification-smtp-provider/notification-smtp-provider.module';
import { FeaturesComponent } from 'src/app/components/features/features.component';
import OrgListModule from 'src/app/pages/org-list/org-list.module';
import { OidcWebkeysModule } from '../policies/oidc-webkeys/oidc-webkeys.module';
@NgModule({
declarations: [SettingsListComponent],
@@ -62,6 +63,7 @@ import OrgListModule from 'src/app/pages/org-list/org-list.module';
NotificationSMTPProviderModule,
NotificationSMSProviderModule,
OIDCConfigurationModule,
OidcWebkeysModule,
SecretGeneratorModule,
FailedEventsModule,
IamViewsModule,

View File

@@ -35,6 +35,14 @@ export const OIDC: SidenavSetting = {
},
};
export const WEBKEYS: SidenavSetting = {
id: 'webkeys',
i18nKey: 'SETTINGS.LIST.WEB_KEYS',
requiredRoles: {
[PolicyComponentServiceType.ADMIN]: ['iam.policy.read'],
},
};
export const SECRETS: SidenavSetting = {
id: 'secrets',
i18nKey: 'SETTINGS.LIST.SECRETS',

View File

@@ -1,7 +1,6 @@
<cnsl-refresh-table
[loading]="loading$ | async"
(refreshed)="refreshPage()"
[dataSize]="dataSource.data.length"
[emitRefreshOnPreviousRoutes]="[
'/instance/smtpprovider/aws-ses/create',
'/instance/smtpprovider/generic/create',

View File

@@ -4,7 +4,6 @@
[hideRefresh]="true"
[emitRefreshOnPreviousRoutes]="refreshOnPreviousRoutes"
[timestamp]="dataSource.viewTimestamp"
[dataSize]="dataSource.totalResult"
[selection]="selection"
>
<button

View File

@@ -2,7 +2,6 @@
[hideRefresh]="true"
[loading]="loading$ | async"
(refreshed)="refreshPage()"
[dataSize]="dataSource.data.length"
[timestamp]="actionsResult?.details?.viewTimestamp"
[selection]="selection"
>

View File

@@ -1,8 +1,8 @@
import { Component, OnDestroy, OnInit } from '@angular/core';
import { Component, DestroyRef } from '@angular/core';
import { MatDialog } from '@angular/material/dialog';
import { ActivatedRoute, Params, Router } from '@angular/router';
import { BehaviorSubject, from, Observable, of, Subject } from 'rxjs';
import { catchError, finalize, map, takeUntil } from 'rxjs/operators';
import { BehaviorSubject, defer, from, Observable, of, TimeoutError } from 'rxjs';
import { catchError, finalize, map, timeout } from 'rxjs/operators';
import { CreationType, MemberCreateDialogComponent } from 'src/app/modules/add-member-dialog/member-create-dialog.component';
import { PolicyComponentServiceType } from 'src/app/modules/policies/policy-component-types.enum';
import { InstanceDetail, State } from 'src/app/proto/generated/zitadel/instance_pb';
@@ -24,6 +24,7 @@ import {
MESSAGETEXTS,
NOTIFICATIONS,
OIDC,
WEBKEYS,
PRIVACYPOLICY,
SECRETS,
SECURITY,
@@ -38,22 +39,25 @@ import {
import { SidenavSetting } from 'src/app/modules/sidenav/sidenav.component';
import { GrpcAuthService } from 'src/app/services/grpc-auth.service';
import { EnvironmentService } from 'src/app/services/environment.service';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { NewFeatureService } from '../../services/new-feature.service';
import { withLatestFromSynchronousFix } from '../../utils/withLatestFromSynchronousFix';
@Component({
selector: 'cnsl-instance',
templateUrl: './instance.component.html',
styleUrls: ['./instance.component.scss'],
})
export class InstanceComponent implements OnInit, OnDestroy {
public instance?: InstanceDetail.AsObject;
public PolicyComponentServiceType: any = PolicyComponentServiceType;
private loadingSubject: BehaviorSubject<boolean> = new BehaviorSubject<boolean>(false);
public loading$: Observable<boolean> = this.loadingSubject.asObservable();
public totalMemberResult: number = 0;
public membersSubject: BehaviorSubject<Member.AsObject[]> = new BehaviorSubject<Member.AsObject[]>([]);
public State: any = State;
export class InstanceComponent {
protected instance?: InstanceDetail.AsObject;
protected readonly PolicyComponentServiceType = PolicyComponentServiceType;
private readonly loadingSubject: BehaviorSubject<boolean> = new BehaviorSubject<boolean>(false);
protected readonly loading$: Observable<boolean> = this.loadingSubject.asObservable();
protected totalMemberResult: number = 0;
protected readonly membersSubject: BehaviorSubject<Member.AsObject[]> = new BehaviorSubject<Member.AsObject[]>([]);
protected readonly State = State;
public id: string = '';
public defaultSettingsList: SidenavSetting[] = [
protected id: string = '';
protected readonly defaultSettingsList: SidenavSetting[] = [
ORGANIZATIONS,
FEATURESETTINGS,
// notifications
@@ -81,23 +85,25 @@ export class InstanceComponent implements OnInit, OnDestroy {
PRIVACYPOLICY,
LANGUAGES,
OIDC,
WEBKEYS,
SECRETS,
SECURITY,
];
public settingsList: Observable<SidenavSetting[]> = of([]);
public customerPortalLink$ = this.envService.env.pipe(map((env) => env.customer_portal));
protected readonly settingsList: Observable<SidenavSetting[]>;
protected readonly customerPortalLink$ = this.envService.env.pipe(map((env) => env.customer_portal));
private destroy$: Subject<void> = new Subject();
constructor(
public adminService: AdminService,
private dialog: MatDialog,
private toast: ToastService,
protected readonly adminService: AdminService,
private readonly dialog: MatDialog,
private readonly toast: ToastService,
breadcrumbService: BreadcrumbService,
private router: Router,
private authService: GrpcAuthService,
private envService: EnvironmentService,
private readonly router: Router,
private readonly authService: GrpcAuthService,
private readonly envService: EnvironmentService,
activatedRoute: ActivatedRoute,
private readonly destroyRef: DestroyRef,
private readonly featureService: NewFeatureService,
) {
this.loadMembers();
@@ -120,12 +126,39 @@ export class InstanceComponent implements OnInit, OnDestroy {
this.toast.showError(error);
});
activatedRoute.queryParams.pipe(takeUntil(this.destroy$)).subscribe((params: Params) => {
activatedRoute.queryParams.pipe(takeUntilDestroyed(this.destroyRef)).subscribe((params: Params) => {
const { id } = params;
if (id) {
this.id = id;
}
});
this.settingsList = this.getSettingsList();
}
public getSettingsList(): Observable<SidenavSetting[]> {
const webKeysEnabled$ = defer(() => this.featureService.getInstanceFeatures()).pipe(
map(({ webKey }) => webKey?.enabled ?? false),
timeout(1000),
catchError((error) => {
if (!(error instanceof TimeoutError)) {
this.toast.showError(error);
}
return of(false);
}),
);
return this.authService
.isAllowedMapper(this.defaultSettingsList, (setting) => setting.requiredRoles.admin || [])
.pipe(
withLatestFromSynchronousFix(webKeysEnabled$),
map(([settings, webKeysEnabled]) => {
if (webKeysEnabled) {
return settings;
}
return settings.filter((setting) => setting.id !== WEBKEYS.id);
}),
);
}
public loadMembers(): void {
@@ -185,18 +218,6 @@ export class InstanceComponent implements OnInit, OnDestroy {
}
public showDetail(): void {
this.router.navigate(['/instance', 'members']);
}
ngOnInit(): void {
this.settingsList = this.authService.isAllowedMapper(
this.defaultSettingsList,
(setting) => setting.requiredRoles.admin || [],
);
}
ngOnDestroy(): void {
this.destroy$.next();
this.destroy$.complete();
this.router.navigate(['/instance', 'members']).then();
}
}

View File

@@ -2,7 +2,6 @@
[loading]="dataSource.loading$ | async"
[selection]="selection"
(refreshed)="refreshPage()"
[dataSize]="dataSource.totalResult"
[timestamp]="dataSource.viewTimestamp"
>
<ng-template cnslHasRole [hasRole]="['project.app.write']" actions>

View File

@@ -5,7 +5,6 @@
[loading]="dataSource.loading$ | async"
*ngIf="projectId"
(refreshed)="refreshPage()"
[dataSize]="dataSource.totalResult"
[selection]="selection"
[timestamp]="dataSource.viewTimestamp"
(refreshed)="getRoleOptions(projectId)"

View File

@@ -2,7 +2,6 @@
<cnsl-refresh-table
[hideRefresh]="true"
(refreshed)="refreshPage(type)"
[dataSize]="totalResult"
[timestamp]="viewTimestamp"
[selection]="selection"
[loading]="loading$ | async"

View File

@@ -11,12 +11,7 @@
>
<mat-icon class="icon">refresh</mat-icon>
</button>
<cnsl-refresh-table
[hideRefresh]="true"
[loading]="loading$ | async"
(refreshed)="getPasswordless()"
[dataSize]="dataSource.data.length"
>
<cnsl-refresh-table [hideRefresh]="true" [loading]="loading$ | async" (refreshed)="getPasswordless()">
<button
actions
class="button"

View File

@@ -9,12 +9,7 @@
<mat-icon class="icon">refresh</mat-icon>
</button>
<cnsl-refresh-table
[hideRefresh]="true"
[loading]="loading$ | async"
(refreshed)="getMFAs()"
[dataSize]="dataSource.data.length"
>
<cnsl-refresh-table [hideRefresh]="true" [loading]="loading$ | async" (refreshed)="getMFAs()">
<button
actions
class="button"

View File

@@ -8,13 +8,7 @@
>
<mat-icon class="icon">refresh</mat-icon>
</button>
<cnsl-refresh-table
[hideRefresh]="true"
[loading]="loading$ | async"
[dataSize]="dataSource.data.length"
[timestamp]="viewTimestamp"
[selection]="selection"
>
<cnsl-refresh-table [hideRefresh]="true" [loading]="loading$ | async" [timestamp]="viewTimestamp" [selection]="selection">
<div class="table-wrapper">
<table class="table" mat-table [dataSource]="dataSource">
<ng-container matColumnDef="select">

View File

@@ -11,7 +11,7 @@
>
<mat-icon class="icon">refresh</mat-icon>
</button>
<cnsl-refresh-table [hideRefresh]="true" [loading]="loading$ | async" [dataSize]="dataSource.data.length">
<cnsl-refresh-table [hideRefresh]="true" [loading]="loading$ | async">
<button
actions
[disabled]="disabled"

View File

@@ -12,12 +12,7 @@
>
<mat-icon class="icon">refresh</mat-icon>
</button>
<cnsl-refresh-table
[hideRefresh]="true"
[loading]="mfaQuery.state === 'loading'"
(refreshed)="refresh$.next(true)"
[dataSize]="mfaQuery.value.data.length"
>
<cnsl-refresh-table [hideRefresh]="true" [loading]="mfaQuery.state === 'loading'" (refreshed)="refresh$.next(true)">
<table class="table" mat-table [dataSource]="mfaQuery.value">
<ng-container matColumnDef="type">
<th mat-header-cell *matHeaderCellDef>{{ 'USER.MFA.TABLETYPE' | translate }}</th>

View File

@@ -2,7 +2,6 @@
*ngIf="type$ | async as type"
[loading]="loading()"
(refreshed)="this.refresh$.next(true)"
[dataSize]="dataSize()"
[hideRefresh]="true"
[timestamp]="(users$ | async)?.details?.timestamp"
[selection]="selection"

View File

@@ -1,18 +1,15 @@
import { Pipe, PipeTransform } from '@angular/core';
import { Timestamp as ConnectTimestamp } from '@bufbuild/protobuf/wkt';
import { Timestamp as BufTimestamp } from '@bufbuild/protobuf/wkt';
import { Timestamp } from 'src/app/proto/generated/google/protobuf/timestamp_pb';
@Pipe({
name: 'timestampToDate',
})
export class TimestampToDatePipe implements PipeTransform {
transform(value: ConnectTimestamp | Timestamp.AsObject, ...args: unknown[]): unknown {
return this.dateFromTimestamp(value);
}
private dateFromTimestamp(date: ConnectTimestamp | Timestamp.AsObject): any {
if (date?.seconds !== undefined && date?.nanos !== undefined) {
transform(date: BufTimestamp | Timestamp.AsObject | undefined): Date | undefined {
if (date?.seconds && date.nanos) {
return new Date(Number(date.seconds) * 1000 + date.nanos / 1000 / 1000);
}
return undefined;
}
}

View File

@@ -14,15 +14,20 @@ import { ExhaustedService } from './exhausted.service';
import { AuthInterceptor, AuthInterceptorProvider, NewConnectWebAuthInterceptor } from './interceptors/auth.interceptor';
import { ExhaustedGrpcInterceptor } from './interceptors/exhausted.grpc.interceptor';
import { I18nInterceptor } from './interceptors/i18n.interceptor';
import { OrgInterceptor } from './interceptors/org.interceptor';
import { NewConnectWebOrgInterceptor, OrgInterceptor, OrgInterceptorProvider } from './interceptors/org.interceptor';
import { StorageService } from './storage.service';
import { UserServiceClient } from '../proto/generated/zitadel/user/v2/User_serviceServiceClientPb';
//@ts-ignore
import { createUserServiceClient } from '@zitadel/client/v2';
import { createFeatureServiceClient, createUserServiceClient } from '@zitadel/client/v2';
//@ts-ignore
import { createAuthServiceClient, createManagementServiceClient } from '@zitadel/client/v1';
import { createGrpcWebTransport } from '@connectrpc/connect-web';
import { FeatureServiceClient } from '../proto/generated/zitadel/feature/v2/Feature_serviceServiceClientPb';
import { WebKeyService } from '@zitadel/proto/zitadel/webkey/v2beta/webkey_service_pb';
// @ts-ignore
import { createClientFor } from '@zitadel/client';
const createWebKeyServiceClient = createClientFor(WebKeyService);
@Injectable({
providedIn: 'root',
@@ -36,6 +41,8 @@ export class GrpcService {
public userNew!: ReturnType<typeof createUserServiceClient>;
public mgmtNew!: ReturnType<typeof createManagementServiceClient>;
public authNew!: ReturnType<typeof createAuthServiceClient>;
public featureNew!: ReturnType<typeof createFeatureServiceClient>;
public webKey!: ReturnType<typeof createWebKeyServiceClient>;
constructor(
private readonly envService: EnvironmentService,
@@ -46,6 +53,7 @@ export class GrpcService {
private readonly exhaustedService: ExhaustedService,
private readonly authInterceptor: AuthInterceptor,
private readonly authInterceptorProvider: AuthInterceptorProvider,
private readonly orgInterceptorProvider: OrgInterceptorProvider,
) {}
public loadAppEnvironment(): Promise<any> {
@@ -62,7 +70,7 @@ export class GrpcService {
const interceptors = {
unaryInterceptors: [
new ExhaustedGrpcInterceptor(this.exhaustedService, this.envService),
new OrgInterceptor(this.storageService),
new OrgInterceptor(this.orgInterceptorProvider),
this.authInterceptor,
new I18nInterceptor(this.translate),
],
@@ -103,9 +111,18 @@ export class GrpcService {
baseUrl: env.api,
interceptors: [NewConnectWebAuthInterceptor(this.authInterceptorProvider)],
});
const transportOldAPIs = createGrpcWebTransport({
baseUrl: env.api,
interceptors: [
NewConnectWebAuthInterceptor(this.authInterceptorProvider),
NewConnectWebOrgInterceptor(this.orgInterceptorProvider),
],
});
this.userNew = createUserServiceClient(transport);
this.mgmtNew = createManagementServiceClient(transport);
this.mgmtNew = createManagementServiceClient(transportOldAPIs);
this.authNew = createAuthServiceClient(transport);
this.featureNew = createFeatureServiceClient(transport);
this.webKey = createWebKeyServiceClient(transport);
const authConfig: AuthConfig = {
scope: 'openid profile email',

View File

@@ -3,32 +3,63 @@ import { Request, RpcError, StatusCode, UnaryInterceptor, UnaryResponse } from '
import { Org } from 'src/app/proto/generated/zitadel/org_pb';
import { StorageKey, StorageLocation, StorageService } from '../storage.service';
import { ConnectError, Interceptor } from '@connectrpc/connect';
import { firstValueFrom, identity, Observable, Subject } from 'rxjs';
import { debounceTime, filter, map } from 'rxjs/operators';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
const ORG_HEADER_KEY = 'x-zitadel-orgid';
@Injectable({ providedIn: 'root' })
export class OrgInterceptor<TReq = unknown, TResp = unknown> implements UnaryInterceptor<TReq, TResp> {
constructor(private readonly storageService: StorageService) {}
constructor(private readonly orgInterceptorProvider: OrgInterceptorProvider) {}
public async intercept(request: Request<TReq, TResp>, invoker: any): Promise<UnaryResponse<TReq, TResp>> {
const metadata = request.getMetadata();
const org: Org.AsObject | null = this.storageService.getItem(StorageKey.organization, StorageLocation.session);
if (org) {
metadata[ORG_HEADER_KEY] = `${org.id}`;
const orgId = this.orgInterceptorProvider.getOrgId();
if (orgId) {
metadata[ORG_HEADER_KEY] = orgId;
}
try {
return await invoker(request);
} catch (error: any) {
if (
error instanceof RpcError &&
error.code === StatusCode.PERMISSION_DENIED &&
error.message.startsWith("Organisation doesn't exist")
) {
this.storageService.removeItem(StorageKey.organization, StorageLocation.session);
}
throw error;
}
return invoker(request).catch(this.orgInterceptorProvider.handleError);
}
}
export function NewConnectWebOrgInterceptor(orgInterceptorProvider: OrgInterceptorProvider): Interceptor {
return (next) => async (req) => {
if (!req.header.get(ORG_HEADER_KEY)) {
const orgId = orgInterceptorProvider.getOrgId();
if (orgId) {
req.header.set(ORG_HEADER_KEY, orgId);
}
}
return next(req).catch(orgInterceptorProvider.handleError);
};
}
@Injectable({ providedIn: 'root' })
export class OrgInterceptorProvider {
constructor(private storageService: StorageService) {}
getOrgId() {
const org: Org.AsObject | null = this.storageService.getItem(StorageKey.organization, StorageLocation.session);
return org?.id;
}
handleError = (error: any): never => {
if (!(error instanceof RpcError) && !(error instanceof ConnectError)) {
throw error;
}
if (
error instanceof RpcError &&
error.code === StatusCode.PERMISSION_DENIED &&
error.message.startsWith("Organisation doesn't exist")
) {
this.storageService.removeItem(StorageKey.organization, StorageLocation.session);
}
throw error;
};
}

View File

@@ -0,0 +1,14 @@
import { Injectable } from '@angular/core';
import { GrpcService } from './grpc.service';
import { GetInstanceFeaturesResponse } from '@zitadel/proto/zitadel/feature/v2/instance_pb';
@Injectable({
providedIn: 'root',
})
export class NewFeatureService {
constructor(private readonly grpcService: GrpcService) {}
public getInstanceFeatures(): Promise<GetInstanceFeaturesResponse> {
return this.grpcService.featureNew.getInstanceFeatures({});
}
}

View File

@@ -0,0 +1,33 @@
import { Injectable } from '@angular/core';
import { GrpcService } from './grpc.service';
import type { MessageInitShape } from '@bufbuild/protobuf';
import {
DeleteWebKeyResponse,
ListWebKeysResponse,
CreateWebKeyRequestSchema,
CreateWebKeyResponse,
ActivateWebKeyResponse,
} from '@zitadel/proto/zitadel/webkey/v2beta/webkey_service_pb';
@Injectable({
providedIn: 'root',
})
export class WebKeysService {
constructor(private readonly grpcService: GrpcService) {}
public ListWebKeys(): Promise<ListWebKeysResponse> {
return this.grpcService.webKey.listWebKeys({});
}
public DeleteWebKey(id: string): Promise<DeleteWebKeyResponse> {
return this.grpcService.webKey.deleteWebKey({ id });
}
public CreateWebKey(req: MessageInitShape<typeof CreateWebKeyRequestSchema>): Promise<CreateWebKeyResponse> {
return this.grpcService.webKey.createWebKey(req);
}
public ActivateWebKey(id: string): Promise<ActivateWebKeyResponse> {
return this.grpcService.webKey.activateWebKey({ id });
}
}

View File

@@ -185,6 +185,32 @@
"DESCRIPTION": "Животът на неактивния refresh токен е максималното време, през което refresh токен може да не се използва."
}
},
"WEB_KEYS": {
"DESCRIPTION": "Управлявайте вашите OIDC уеб ключове, за да подписвате и валидирате токени сигурно за вашата ZITADEL инстанция.",
"TABLE": {
"TITLE": "Активни и бъдещи уеб ключове",
"DESCRIPTION": "Вашите активни и предстоящи уеб ключове. Активирането на нов ключ ще деактивира текущия.",
"NOTE": "Забележка: Крайна точка JWKs OIDC връща кешируем отговор (по подразбиране 5 минути). Избягвайте активирането на ключ твърде рано, тъй като той може да не е наличен в кеша и клиентите.",
"ACTIVATE": "Активирайте следващия уеб ключ",
"ACTIVE": "В момента активен",
"NEXT": "Следващ в опашката",
"FUTURE": "Бъдещ",
"WARNING": "Уеб ключът е на по-малко от 5 минути"
},
"CREATE": {
"TITLE": "Създаване на нов уеб ключ",
"DESCRIPTION": "Създаването на нов уеб ключ го добавя към вашия списък. ZITADEL използва ключове RSA2048 с хеш SHA256 по подразбиране.",
"KEY_TYPE": "Тип ключ",
"BITS": "Битове",
"HASHER": "Хешер",
"CURVE": "Крива"
},
"PREVIOUS_TABLE": {
"TITLE": "Предишни уеб ключове",
"DESCRIPTION": "Това са вашите предишни уеб ключове, които вече не са активни.",
"DEACTIVATED_ON": "Деактивиран на"
}
},
"MESSAGE_TEXTS": {
"TITLE": "Текстове на съобщенията",
"DESCRIPTION": "Персонализирайте текстовете на вашите имейл или SMS уведомления. Ако искате да деактивирате някои от езиците, ограничете ги в настройките за език на вашите инстанции.",
@@ -1352,6 +1378,7 @@
"BRANDING": "Брандиране",
"PRIVACYPOLICY": "Политика за бедност",
"OIDC": "Живот и изтичане на OIDC Token",
"WEB_KEYS": "OIDC Web Keys",
"SECRETS": "Тайна поява",
"SECURITY": "Настройки на сигурността",
"EVENTS": "Събития",

View File

@@ -185,6 +185,32 @@
"DESCRIPTION": "Životnost nečinného refresh tokenu je maximální doba, po kterou může být refresh token nepoužitý."
}
},
"WEB_KEYS": {
"DESCRIPTION": "Spravujte své OIDC webové klíče pro bezpečné podepisování a ověřování tokenů ve vaší instanci ZITADEL.",
"TABLE": {
"TITLE": "Aktivní a budoucí webové klíče",
"DESCRIPTION": "Vaše aktivní a nadcházející webové klíče. Aktivací nového klíče dojde k deaktivaci aktuálního.",
"NOTE": "Poznámka: Koncový bod JWKs OIDC vrací odpověď uložitelnou do mezipaměti (výchozí 5 minut). Vyhněte se příliš brzké aktivaci klíče, protože nemusí být dostupný v mezipaměti a klientům.",
"ACTIVATE": "Aktivovat další webový klíč",
"ACTIVE": "Aktuálně aktivní",
"NEXT": "Další v řadě",
"FUTURE": "Budoucí",
"WARNING": "Webový klíč je starý méně než 5 minut"
},
"CREATE": {
"TITLE": "Vytvořit nový webový klíč",
"DESCRIPTION": "Vytvořením nového webového klíče jej přidáte do svého seznamu. ZITADEL používá klíče RSA2048 s hashováním SHA256 jako výchozí.",
"KEY_TYPE": "Typ klíče",
"BITS": "Bity",
"HASHER": "Hasher",
"CURVE": "Křivka"
},
"PREVIOUS_TABLE": {
"TITLE": "Předchozí webové klíče",
"DESCRIPTION": "Toto jsou vaše předchozí webové klíče, které již nejsou aktivní.",
"DEACTIVATED_ON": "Deaktivováno dne"
}
},
"MESSAGE_TEXTS": {
"TITLE": "Texty zpráv",
"DESCRIPTION": "Přizpůsob si texty svých e-mailových nebo SMS notifikací. Pokud chceš některé z jazyků zakázat, omez je ve svém nastavení jazyků instance.",
@@ -1353,6 +1379,7 @@
"BRANDING": "Branding",
"PRIVACYPOLICY": "Zásady ochrany osobních údajů",
"OIDC": "Životnost a expirace OIDC tokenu",
"WEB_KEYS": "OIDC Web Keys",
"SECRETS": "Generátor tajemství",
"SECURITY": "Bezpečnostní nastavení",
"EVENTS": "Události",

View File

@@ -185,6 +185,32 @@
"DESCRIPTION": "Die maximale Inaktivitätsdauer eines Aktualisierungstokens ist die maximale Zeit, in der ein Aktualisierungstoken unbenutzt sein kann."
}
},
"WEB_KEYS": {
"DESCRIPTION": "Verwalte deine OIDC Web Keys, um Tokens für deine ZITADEL-Instanz sicher zu signieren und zu validieren.",
"TABLE": {
"TITLE": "Aktive und zukünftige Web Keys",
"DESCRIPTION": "Deine aktiven und kommenden Web Keys. Das Aktivieren eines neuen Schlüssels deaktiviert den aktuellen.",
"NOTE": "Hinweis: Der JWKs OIDC-Endpunkt gibt eine zwischenspeicherbare Antwort zurück (Standard: 5 Min.). Vermeide es, einen Schlüssel zu früh zu aktivieren, da er möglicherweise noch nicht in Caches und Clients verfügbar ist.",
"ACTIVATE": "Nächsten Web Key aktivieren",
"ACTIVE": "Derzeit aktiv",
"NEXT": "Als Nächstes in der Warteschlange",
"FUTURE": "Zukünftig",
"WARNING": "Der Web Key ist weniger als 5 Minuten alt"
},
"CREATE": {
"TITLE": "Neuen Web Key erstellen",
"DESCRIPTION": "Das Erstellen eines neuen Web Keys fügt ihn zu deiner Liste hinzu. ZITADEL verwendet standardmäßig RSA2048-Schlüssel mit einem SHA256-Hasher.",
"KEY_TYPE": "Schlüsseltyp",
"BITS": "Bits",
"HASHER": "Hasher",
"CURVE": "Kurve"
},
"PREVIOUS_TABLE": {
"TITLE": "Frühere Web Keys",
"DESCRIPTION": "Dies sind deine früheren Web Keys, die nicht mehr aktiv sind.",
"DEACTIVATED_ON": "Deaktiviert am"
}
},
"MESSAGE_TEXTS": {
"TITLE": "Nachrichtentexte",
"DESCRIPTION": "Passe die Texte deiner Benachrichtigungs-E-Mails oder SMS-Nachrichten an. Wenn du einige der Sprachen deaktivieren möchtest, beschränke sie in den Spracheinstellungen deiner Instanz.",
@@ -1353,6 +1379,7 @@
"BRANDING": "Branding",
"PRIVACYPOLICY": "Datenschutzrichtlinie",
"OIDC": "OIDC Token Lifetime und Expiration",
"WEB_KEYS": "OIDC Web Keys",
"SECRETS": "Secret Generator",
"SECURITY": "Sicherheitseinstellungen",
"EVENTS": "Events",

View File

@@ -185,6 +185,32 @@
"DESCRIPTION": "The idle refresh token lifetime is the maximum time a refresh token can be unused."
}
},
"WEB_KEYS": {
"DESCRIPTION": "Manage your OIDC Web Keys to securely sign and validate tokens for your ZITADEL instance.",
"TABLE": {
"TITLE": "Active and Future Web Keys",
"DESCRIPTION": "Your active and upcoming web keys. Activating a new key will deactivate the current one.",
"NOTE": "Note: The JWKs OIDC endpoint returns a cacheable response (default 5 min). Avoid activating a key too soon, as it may not be available to caches and clients yet.",
"ACTIVATE": "Activate next Web Key",
"ACTIVE": "Currently active",
"NEXT": "Next in queue",
"FUTURE": "Future",
"WARNING": "Web Key is less than 5 min old"
},
"CREATE": {
"TITLE": "Create new Web Key",
"DESCRIPTION": "Creating a new web key adds it to your list. ZITADEL uses RSA2048 keys with a SHA256 hasher by default.",
"KEY_TYPE": "Key Type",
"BITS": "Bits",
"HASHER": "Hasher",
"CURVE": "Curve"
},
"PREVIOUS_TABLE": {
"TITLE": "Previous Web Keys",
"DESCRIPTION": "These are your previous web keys that are no longer active.",
"DEACTIVATED_ON": "Deactivated on"
}
},
"MESSAGE_TEXTS": {
"TITLE": "Message Texts",
"DESCRIPTION": "Customize the texts of your notification email or SMS messages. If you want to disable some of the languages, restrict them in your instances language settings.",
@@ -1353,6 +1379,7 @@
"BRANDING": "Branding",
"PRIVACYPOLICY": "External links",
"OIDC": "OIDC Token lifetime and expiration",
"WEB_KEYS": "OIDC Web Keys",
"SECRETS": "Secret Generator",
"SECURITY": "Security settings",
"EVENTS": "Events",

View File

@@ -185,6 +185,32 @@
"DESCRIPTION": "La duración de vida del token de actualización en espera es el tiempo máximo que un token de actualización puede estar sin usar."
}
},
"WEB_KEYS": {
"DESCRIPTION": "Administra tus claves web OIDC para firmar y validar tokens de manera segura en tu instancia de ZITADEL.",
"TABLE": {
"TITLE": "Claves Web Activas y Futuras",
"DESCRIPTION": "Tus claves web activas y próximas. Activar una nueva clave desactivará la actual.",
"NOTE": "Nota: El endpoint JWKs OIDC devuelve una respuesta almacenable en caché (por defecto 5 min). Evita activar una clave demasiado pronto, ya que puede que aún no esté disponible en cachés y clientes.",
"ACTIVATE": "Activar la siguiente Clave Web",
"ACTIVE": "Actualmente activa",
"NEXT": "Siguiente en la cola",
"FUTURE": "Futuro",
"WARNING": "La clave web tiene menos de 5 minutos"
},
"CREATE": {
"TITLE": "Crear nueva Clave Web",
"DESCRIPTION": "Crear una nueva clave web la añadirá a tu lista. ZITADEL usa por defecto claves RSA2048 con un algoritmo de hash SHA256.",
"KEY_TYPE": "Tipo de Clave",
"BITS": "Bits",
"HASHER": "Algoritmo de Hash",
"CURVE": "Curva"
},
"PREVIOUS_TABLE": {
"TITLE": "Claves Web Anteriores",
"DESCRIPTION": "Estas son tus claves web anteriores que ya no están activas.",
"DEACTIVATED_ON": "Desactivada el"
}
},
"MESSAGE_TEXTS": {
"TITLE": "Textos de Mensajes",
"DESCRIPTION": "Personaliza los textos de tus mensajes de correo electrónico de notificación o mensajes SMS. Si deseas desactivar algunos de los idiomas, restríngelos en la configuración de idiomas de tus instancias.",
@@ -1354,6 +1380,7 @@
"BRANDING": "Imagen de marca",
"PRIVACYPOLICY": "Política de privacidad",
"OIDC": "OIDC Token lifetime and expiration",
"WEB_KEYS": "OIDC Web Keys",
"SECRETS": "Apariencia del secreto",
"SECURITY": "Ajustes de seguridad",
"EVENTS": "Eventos",

View File

@@ -185,6 +185,32 @@
"DESCRIPTION": "La durée de vie du token de rafraîchissement inactif est le temps maximum qu'un token de rafraîchissement peut être inutilisé."
}
},
"WEB_KEYS": {
"DESCRIPTION": "Gérez vos clés Web OIDC pour signer et valider en toute sécurité les jetons de votre instance ZITADEL.",
"TABLE": {
"TITLE": "Clés Web Actives et Futures",
"DESCRIPTION": "Vos clés Web actives et à venir. L'activation d'une nouvelle clé désactivera l'actuelle.",
"NOTE": "Remarque : Le point de terminaison JWKs OIDC renvoie une réponse mise en cache (par défaut 5 min). Évitez d'activer une clé trop tôt, car elle pourrait ne pas encore être disponible pour les caches et les clients.",
"ACTIVATE": "Activer la prochaine Clé Web",
"ACTIVE": "Actuellement active",
"NEXT": "Prochaine dans la file d'attente",
"FUTURE": "Futur",
"WARNING": "La clé Web a moins de 5 minutes"
},
"CREATE": {
"TITLE": "Créer une nouvelle Clé Web",
"DESCRIPTION": "Créer une nouvelle clé Web l'ajoutera à votre liste. ZITADEL utilise par défaut des clés RSA2048 avec un hacheur SHA256.",
"KEY_TYPE": "Type de Clé",
"BITS": "Bits",
"HASHER": "Hacheur",
"CURVE": "Courbe"
},
"PREVIOUS_TABLE": {
"TITLE": "Clés Web Précédentes",
"DESCRIPTION": "Voici vos anciennes clés Web qui ne sont plus actives.",
"DEACTIVATED_ON": "Désactivée le"
}
},
"MESSAGE_TEXTS": {
"TITLE": "Textes des Messages",
"DESCRIPTION": "Personnalisez les textes de vos e-mails de notification ou messages SMS. Si vous souhaitez désactiver certaines langues, restreignez-les dans les paramètres de langue de vos instances.",
@@ -1353,6 +1379,7 @@
"BRANDING": "Image de marque",
"PRIVACYPOLICY": "Politique de confidentialité",
"OIDC": "Durée de vie et expiration des jetons OIDC",
"WEB_KEYS": "OIDC Web Keys",
"SECRETS": "Générateur de secrets",
"SECURITY": "Paramètres de sécurité",
"EVENTS": "Événements",

View File

@@ -185,6 +185,32 @@
"DESCRIPTION": "A tétlen frissítő token élettartama az a maximális idő, ameddig a frissítő token használaton kívül maradhat."
}
},
"WEB_KEYS": {
"DESCRIPTION": "Kezelje az OIDC Webkulcsokat, hogy biztonságosan aláírja és érvényesítse a tokeneket a ZITADEL példányában.",
"TABLE": {
"TITLE": "Aktív és Jövőbeli Webkulcsok",
"DESCRIPTION": "Az aktív és közelgő webkulcsai. Egy új kulcs aktiválása deaktiválja az aktuálisat.",
"NOTE": "Megjegyzés: A JWKs OIDC végpont egy gyorsítótárazható választ ad vissza (alapértelmezett: 5 perc). Kerülje a kulcs túl korai aktiválását, mivel lehet, hogy még nem érhető el a gyorsítótárakban és a klienseknél.",
"ACTIVATE": "Következő Webkulcs aktiválása",
"ACTIVE": "Jelenleg aktív",
"NEXT": "Következő a sorban",
"FUTURE": "Jövőbeli",
"WARNING": "A webkulcs kevesebb mint 5 perces"
},
"CREATE": {
"TITLE": "Új Webkulcs létrehozása",
"DESCRIPTION": "Egy új webkulcs létrehozása hozzáadja azt a listájához. A ZITADEL alapértelmezés szerint RSA2048 kulcsokat használ SHA256 hasheléssel.",
"KEY_TYPE": "Kulcstípus",
"BITS": "Bitek",
"HASHER": "Hasher",
"CURVE": "Görbe"
},
"PREVIOUS_TABLE": {
"TITLE": "Korábbi Webkulcsok",
"DESCRIPTION": "Ezek a korábbi webkulcsai, amelyek már nem aktívak.",
"DEACTIVATED_ON": "Deaktiválva ekkor"
}
},
"MESSAGE_TEXTS": {
"TITLE": "Üzenet Szövegek",
"DESCRIPTION": "Testreszabhatod az értesítési e-mailjeid vagy SMS üzeneteid szövegeit. Ha le szeretnél tiltani néhány nyelvet, korlátozd azokat az instance nyelvi beállításaiban.",
@@ -1353,6 +1379,7 @@
"BRANDING": "Márkaépítés",
"PRIVACYPOLICY": "Külső hivatkozások",
"OIDC": "OIDC token élettartam és lejárat",
"WEB_KEYS": "OIDC Web Keys",
"SECRETS": "Titokgenerátor",
"SECURITY": "Biztonsági beállítások",
"EVENTS": "Események",

View File

@@ -173,6 +173,32 @@
"DESCRIPTION": "Masa pakai token penyegaran yang menganggur adalah waktu maksimum token penyegaran tidak dapat digunakan."
}
},
"WEB_KEYS": {
"DESCRIPTION": "Kelola Kunci Web OIDC Anda untuk menandatangani dan memvalidasi token dengan aman untuk instance ZITADEL Anda.",
"TABLE": {
"TITLE": "Kunci Web Aktif dan Mendatang",
"DESCRIPTION": "Kunci web Anda yang aktif dan akan datang. Mengaktifkan kunci baru akan menonaktifkan kunci yang sedang digunakan.",
"NOTE": "Catatan: Endpoint JWKs OIDC mengembalikan respons yang dapat di-cache (default 5 menit). Hindari mengaktifkan kunci terlalu cepat, karena mungkin belum tersedia di cache dan klien.",
"ACTIVATE": "Aktifkan Kunci Web Berikutnya",
"ACTIVE": "Saat ini aktif",
"NEXT": "Berikutnya dalam antrean",
"FUTURE": "Mendatang",
"WARNING": "Kunci Web berusia kurang dari 5 menit"
},
"CREATE": {
"TITLE": "Buat Kunci Web Baru",
"DESCRIPTION": "Membuat kunci web baru akan menambahkannya ke daftar Anda. ZITADEL secara default menggunakan kunci RSA2048 dengan fungsi hash SHA256.",
"KEY_TYPE": "Jenis Kunci",
"BITS": "Bit",
"HASHER": "Hasher",
"CURVE": "Kurva"
},
"PREVIOUS_TABLE": {
"TITLE": "Kunci Web Sebelumnya",
"DESCRIPTION": "Ini adalah kunci web sebelumnya yang tidak lagi aktif.",
"DEACTIVATED_ON": "Dinonaktifkan pada"
}
},
"MESSAGE_TEXTS": {
"TITLE": "Teks Pesan",
"DESCRIPTION": "Sesuaikan teks email notifikasi atau pesan SMS Anda. Jika Anda ingin menonaktifkan beberapa bahasa, batasi bahasa tersebut di pengaturan bahasa instance Anda.",
@@ -1231,6 +1257,7 @@
"BRANDING": "merek",
"PRIVACYPOLICY": "Tautan eksternal",
"OIDC": "Masa berlaku dan masa berlaku Token OIDC",
"WEB_KEYS": "OIDC Web Keys",
"SECRETS": "Pembuat Rahasia",
"SECURITY": "Pengaturan keamanan",
"EVENTS": "Acara",

View File

@@ -185,6 +185,32 @@
"DESCRIPTION": "La durata massima di un token di refresh inattivo è il tempo massimo in cui un token di refresh può rimanere inutilizzato."
}
},
"WEB_KEYS": {
"DESCRIPTION": "Gestisci le tue chiavi Web OIDC per firmare e convalidare in modo sicuro i token per la tua istanza di ZITADEL.",
"TABLE": {
"TITLE": "Chiavi Web Attive e Future",
"DESCRIPTION": "Le tue chiavi web attive e future. L'attivazione di una nuova chiave disattiverà quella attuale.",
"NOTE": "Nota: L'endpoint JWKs OIDC restituisce una risposta memorizzabile nella cache (predefinito 5 min). Evita di attivare una chiave troppo presto, poiché potrebbe non essere ancora disponibile nelle cache e nei client.",
"ACTIVATE": "Attiva la prossima Chiave Web",
"ACTIVE": "Attualmente attiva",
"NEXT": "Prossima in coda",
"FUTURE": "Futura",
"WARNING": "La chiave web ha meno di 5 minuti"
},
"CREATE": {
"TITLE": "Crea una nuova Chiave Web",
"DESCRIPTION": "Creare una nuova chiave web la aggiungerà alla tua lista. ZITADEL utilizza chiavi RSA2048 con hash SHA256 per impostazione predefinita.",
"KEY_TYPE": "Tipo di Chiave",
"BITS": "Bit",
"HASHER": "Hasher",
"CURVE": "Curva"
},
"PREVIOUS_TABLE": {
"TITLE": "Chiavi Web Precedenti",
"DESCRIPTION": "Queste sono le tue chiavi web precedenti che non sono più attive.",
"DEACTIVATED_ON": "Disattivata il"
}
},
"MESSAGE_TEXTS": {
"TITLE": "Testi dei Messaggi",
"DESCRIPTION": "Personalizza i testi delle tue email di notifica o messaggi SMS. Se vuoi disabilitare alcune lingue, limitale nelle impostazioni lingua delle tue istanze.",
@@ -1353,6 +1379,7 @@
"BRANDING": "Branding",
"PRIVACYPOLICY": "Informativa sulla privacy e TOS",
"OIDC": "OIDC Token lifetime e scadenza",
"WEB_KEYS": "OIDC Web Keys",
"SECRETS": "Aspetto dei segreti",
"SECURITY": "Impostazioni di sicurezza",
"EVENTS": "Eventi",

View File

@@ -185,6 +185,32 @@
"DESCRIPTION": "アイドル状態のリフレッシュトークンの有効期間は、リフレッシュトークンが使用されない最大時間です。"
}
},
"WEB_KEYS": {
"DESCRIPTION": "ZITADELインスタンスのトークンを安全に署名および検証するために、OIDC Webキーを管理します。",
"TABLE": {
"TITLE": "アクティブおよび今後のWebキー",
"DESCRIPTION": "現在アクティブなWebキーと、今後使用予定のWebキーです。新しいキーをアクティブ化すると、現在のキーは無効になります。",
"NOTE": "注意: JWKs OIDCエンドポイントはキャッシュ可能なレスポンスを返しますデフォルト5分。キーを早くアクティブ化しすぎると、キャッシュやクライアントでまだ利用できない可能性があります。",
"ACTIVATE": "次のWebキーをアクティブ化",
"ACTIVE": "現在アクティブ",
"NEXT": "次のキュー",
"FUTURE": "今後",
"WARNING": "ウェブキーは5分未満です。"
},
"CREATE": {
"TITLE": "新しいWebキーを作成",
"DESCRIPTION": "新しいWebキーを作成すると、リストに追加されます。ZITADELはデフォルトでRSA2048キーとSHA256ハッシュを使用します。",
"KEY_TYPE": "キーの種類",
"BITS": "ビット",
"HASHER": "ハッシュ方式",
"CURVE": "カーブ"
},
"PREVIOUS_TABLE": {
"TITLE": "以前のWebキー",
"DESCRIPTION": "これらは、すでに無効になった以前のWebキーです。",
"DEACTIVATED_ON": "無効化日"
}
},
"MESSAGE_TEXTS": {
"TITLE": "メッセージテキスト",
"DESCRIPTION": "通知メールやSMSメッセージのテキストをカスタマイズします。一部の言語を無効にしたい場合は、インスタンスの言語設定で制限してください。",
@@ -1353,6 +1379,7 @@
"BRANDING": "ブランディング",
"PRIVACYPOLICY": "プライバシーポリシー",
"OIDC": "OIDCトークンのライフタイムと有効期限",
"WEB_KEYS": "OIDC Web Keys",
"SECRETS": "シークレット設定",
"SECURITY": "セキュリティ設定",
"EVENTS": "イベント",

View File

@@ -185,6 +185,32 @@
"DESCRIPTION": "유휴 갱신 토큰 수명은 갱신 토큰이 사용되지 않는 최대 기간을 의미합니다."
}
},
"WEB_KEYS": {
"DESCRIPTION": "ZITADEL 인스턴스의 토큰을 안전하게 서명하고 검증하기 위해 OIDC 웹 키를 관리하세요.",
"TABLE": {
"TITLE": "활성 및 예정된 웹 키",
"DESCRIPTION": "현재 활성화된 웹 키와 앞으로 활성화될 웹 키입니다. 새로운 키를 활성화하면 기존 키는 비활성화됩니다.",
"NOTE": "참고: JWKs OIDC 엔드포인트는 캐시 가능한 응답을 반환합니다 (기본값: 5분). 키를 너무 빨리 활성화하면 캐시 및 클라이언트에서 아직 사용할 수 없을 수 있습니다.",
"ACTIVATE": "다음 웹 키 활성화",
"ACTIVE": "현재 활성화됨",
"NEXT": "대기 중인 다음 키",
"FUTURE": "향후 사용 예정",
"WARNING": "웹 키가 5분 미만입니다."
},
"CREATE": {
"TITLE": "새 웹 키 생성",
"DESCRIPTION": "새 웹 키를 생성하면 목록에 추가됩니다. ZITADEL은 기본적으로 RSA2048 키와 SHA256 해시 알고리즘을 사용합니다.",
"KEY_TYPE": "키 유형",
"BITS": "비트",
"HASHER": "해시 알고리즘",
"CURVE": "곡선"
},
"PREVIOUS_TABLE": {
"TITLE": "이전 웹 키",
"DESCRIPTION": "더 이상 활성 상태가 아닌 이전 웹 키 목록입니다.",
"DEACTIVATED_ON": "비활성화된 날짜"
}
},
"MESSAGE_TEXTS": {
"TITLE": "메시지 텍스트",
"DESCRIPTION": "알림 이메일 또는 SMS 메시지의 텍스트를 사용자 정의하세요. 언어를 비활성화하려면 인스턴스의 언어 설정에서 제한하세요.",
@@ -1353,6 +1379,7 @@
"BRANDING": "브랜딩",
"PRIVACYPOLICY": "외부 링크",
"OIDC": "OIDC 토큰 수명 및 만료",
"WEB_KEYS": "OIDC Web Keys",
"SECRETS": "시크릿 생성기",
"SECURITY": "보안 설정",
"EVENTS": "이벤트",

View File

@@ -185,6 +185,32 @@
"DESCRIPTION": "Животниот век на неактивниот токен за освежување е максималното време кое токен за освежување може да не се користи."
}
},
"WEB_KEYS": {
"DESCRIPTION": "Управувајте со вашите OIDC веб-клучеви за безбедно потпишување и валидација на токени за вашата ZITADEL инстанца.",
"TABLE": {
"TITLE": "Активни и Идни Веб-Клучеви",
"DESCRIPTION": "Вашите активни и претстојни веб-клучеви. Активирањето на нов клуч ќе го деактивира тековниот.",
"NOTE": "Забелешка: JWKs OIDC крајната точка враќа одговор што може да се кешира (стандардно 5 минути). Избегнувајте активирање на клучот пребрзо, бидејќи можеби сè уште не е достапен во кешот и кај клиентите.",
"ACTIVATE": "Активирај го следниот веб-клуч",
"ACTIVE": "Моментално активен",
"NEXT": "Следен во редот",
"FUTURE": "Иднина",
"WARNING": "Веб-клучот е помалку од 5 минути стар"
},
"CREATE": {
"TITLE": "Креирај нов веб-клуч",
"DESCRIPTION": "Креирањето нов веб-клуч го додава на вашата листа. ZITADEL стандардно користи RSA2048 клучеви со SHA256 алгоритам за хаширање.",
"KEY_TYPE": "Тип на клуч",
"BITS": "Битови",
"HASHER": "Алгоритам за хаширање",
"CURVE": "Крива"
},
"PREVIOUS_TABLE": {
"TITLE": "Претходни веб-клучеви",
"DESCRIPTION": "Ова се вашите претходни веб-клучеви кои повеќе не се активни.",
"DEACTIVATED_ON": "Деактивиран на"
}
},
"MESSAGE_TEXTS": {
"TITLE": "Текстови на пораки",
"DESCRIPTION": "Прилагодете ги текстовите на вашите е-маил или SMS пораки за известување. Ако сакате да оневозможите некои јазици, ограничете ги во поставките за јазик на вашите инстанци.",
@@ -1354,6 +1380,7 @@
"BRANDING": "Брендирање",
"PRIVACYPOLICY": "Политика за приватност",
"OIDC": "OIDC времетраење и истекување на токени",
"WEB_KEYS": "OIDC Web Keys",
"SECRETS": "Изглед на тајни",
"SECURITY": "Подесувања за безбедност",
"EVENTS": "Настани",

View File

@@ -185,6 +185,32 @@
"DESCRIPTION": "De levensduur van het inactieve vernieuwingstoken is de maximale tijd dat een vernieuwingstoken ongebruikt kan zijn."
}
},
"WEB_KEYS": {
"DESCRIPTION": "Beheer je OIDC Web Keys om tokens veilig te ondertekenen en te valideren voor je ZITADEL-instantie.",
"TABLE": {
"TITLE": "Actieve en Toekomstige Websleutels",
"DESCRIPTION": "Je actieve en aankomende websleutels. Het activeren van een nieuwe sleutel deactiveert de huidige.",
"NOTE": "Opmerking: Het JWKs OIDC-eindpunt geeft een cachebare respons terug (standaard 5 minuten). Vermijd het te vroeg activeren van een sleutel, omdat deze mogelijk nog niet beschikbaar is in caches en bij clients.",
"ACTIVATE": "Volgende Websleutel activeren",
"ACTIVE": "Momenteel actief",
"NEXT": "Volgende in de wachtrij",
"FUTURE": "Toekomstig",
"WARNING": "De websleutel is minder dan 5 minuten oud"
},
"CREATE": {
"TITLE": "Nieuwe Websleutel aanmaken",
"DESCRIPTION": "Het aanmaken van een nieuwe websleutel voegt deze toe aan je lijst. ZITADEL gebruikt standaard RSA2048-sleutels met een SHA256-hasher.",
"KEY_TYPE": "Sleuteltype",
"BITS": "Bits",
"HASHER": "Hasher",
"CURVE": "Curve"
},
"PREVIOUS_TABLE": {
"TITLE": "Vorige Websleutels",
"DESCRIPTION": "Dit zijn je vorige websleutels die niet langer actief zijn.",
"DEACTIVATED_ON": "Gedeactiveerd op"
}
},
"MESSAGE_TEXTS": {
"TITLE": "Berichtteksten",
"DESCRIPTION": "Pas de teksten van je notificatie-e-mail of SMS-berichten aan. Als je sommige talen wilt uitschakelen, beperk ze dan in de taalinstellingen van je instanties.",
@@ -1353,6 +1379,7 @@
"BRANDING": "Branding",
"PRIVACYPOLICY": "Privacybeleid",
"OIDC": "OIDC Token levensduur en vervaldatum",
"WEB_KEYS": "OIDC Web Keys",
"SECRETS": "Secret Generator",
"SECURITY": "Beveiligingsinstellingen",
"EVENTS": "Evenementen",

View File

@@ -185,6 +185,32 @@
"DESCRIPTION": "Czas życia bezczynnego tokena odświeżania to maksymalny czas, przez który token odświeżania może pozostać nieużywany."
}
},
"WEB_KEYS": {
"DESCRIPTION": "Zarządzaj swoimi kluczami internetowymi OIDC, aby bezpiecznie podpisywać i weryfikować tokeny w swojej instancji ZITADEL.",
"TABLE": {
"TITLE": "Aktywne i Przyszłe Klucze Internetowe",
"DESCRIPTION": "Twoje aktywne i nadchodzące klucze internetowe. Aktywacja nowego klucza spowoduje dezaktywację obecnego.",
"NOTE": "Uwaga: Punkt końcowy JWKs OIDC zwraca odpowiedź możliwą do buforowania (domyślnie 5 minut). Unikaj zbyt wczesnej aktywacji klucza, ponieważ może on nie być jeszcze dostępny w pamięci podręcznej i dla klientów.",
"ACTIVATE": "Aktywuj następny klucz internetowy",
"ACTIVE": "Obecnie aktywny",
"NEXT": "Następny w kolejce",
"FUTURE": "Przyszłe",
"WARNING": "Klucz sieciowy ma mniej niż 5 minut"
},
"CREATE": {
"TITLE": "Utwórz nowy klucz internetowy",
"DESCRIPTION": "Utworzenie nowego klucza internetowego doda go do Twojej listy. ZITADEL domyślnie używa kluczy RSA2048 z haszowaniem SHA256.",
"KEY_TYPE": "Typ klucza",
"BITS": "Bity",
"HASHER": "Haszowanie",
"CURVE": "Krzywa"
},
"PREVIOUS_TABLE": {
"TITLE": "Poprzednie Klucze Internetowe",
"DESCRIPTION": "To są Twoje poprzednie klucze internetowe, które nie są już aktywne.",
"DEACTIVATED_ON": "Dezaktywowany dnia"
}
},
"MESSAGE_TEXTS": {
"TITLE": "Teksty wiadomości",
"DESCRIPTION": "Dostosuj teksty swoich e-maili lub wiadomości SMS z powiadomieniami. Jeśli chcesz wyłączyć niektóre języki, ogranicz je w ustawieniach językowych swoich instancji.",
@@ -1352,6 +1378,7 @@
"BRANDING": "Marka",
"PRIVACYPOLICY": "Polityka prywatności",
"OIDC": "Czas trwania tokenów OIDC i wygaśnięcie",
"WEB_KEYS": "OIDC Web Keys",
"SECRETS": "Wygląd sekretów",
"SECURITY": "Ustawienia bezpieczeństwa",
"EVENTS": "Zdarzenia",

View File

@@ -185,6 +185,32 @@
"DESCRIPTION": "A vida útil do token de atualização inativo é o tempo máximo que um token de atualização pode ficar sem uso."
}
},
"WEB_KEYS": {
"DESCRIPTION": "Gerencie suas Chaves Web OIDC para assinar e validar tokens com segurança em sua instância do ZITADEL.",
"TABLE": {
"TITLE": "Chaves Web Ativas e Futuras",
"DESCRIPTION": "Suas chaves web ativas e futuras. Ativar uma nova chave desativará a atual.",
"NOTE": "Nota: O endpoint JWKs OIDC retorna uma resposta que pode ser armazenada em cache (padrão: 5 min). Evite ativar uma chave muito cedo, pois ela pode ainda não estar disponível no cache e para os clientes.",
"ACTIVATE": "Ativar próxima Chave Web",
"ACTIVE": "Atualmente ativa",
"NEXT": "Próxima na fila",
"FUTURE": "Futuro",
"WARNING": "A chave da Web tem menos de 5 minutos"
},
"CREATE": {
"TITLE": "Criar nova Chave Web",
"DESCRIPTION": "Criar uma nova chave web a adicionará à sua lista. O ZITADEL usa, por padrão, chaves RSA2048 com um algoritmo de hash SHA256.",
"KEY_TYPE": "Tipo de Chave",
"BITS": "Bits",
"HASHER": "Algoritmo de Hash",
"CURVE": "Curva"
},
"PREVIOUS_TABLE": {
"TITLE": "Chaves Web Anteriores",
"DESCRIPTION": "Estas são suas chaves web anteriores que não estão mais ativas.",
"DEACTIVATED_ON": "Desativada em"
}
},
"MESSAGE_TEXTS": {
"TITLE": "Textos de Mensagens",
"DESCRIPTION": "Personalize os textos do seu e-mail de notificação ou mensagens SMS. Se desejar desativar alguns idiomas, restrinja-os nas configurações de idioma da sua instância.",
@@ -1354,6 +1380,7 @@
"BRANDING": "Marca",
"PRIVACYPOLICY": "Política de Privacidade",
"OIDC": "Tempo de Vida e Expiração do Token OIDC",
"WEB_KEYS": "OIDC Web Keys",
"SECRETS": "Aparência de Segredo",
"SECURITY": "Configurações de Segurança",
"EVENTS": "Eventos",

View File

@@ -185,6 +185,32 @@
"DESCRIPTION": "Durata de viață inactivă a tokenului de reîmprospătare este timpul maxim în care un token de reîmprospătare poate fi neutilizat."
}
},
"WEB_KEYS": {
"DESCRIPTION": "Gestionează-ți cheile web OIDC pentru a semna și valida în siguranță tokenurile pentru instanța ta ZITADEL.",
"TABLE": {
"TITLE": "Chei Web Active și Viitoare",
"DESCRIPTION": "Cheile tale web active și viitoare. Activarea unei noi chei va dezactiva cheia curentă.",
"NOTE": "Notă: Endpoint-ul JWKs OIDC returnează un răspuns care poate fi stocat în cache (implicit 5 min). Evită activarea unei chei prea devreme, deoarece este posibil să nu fie încă disponibilă în cache și pentru clienți.",
"ACTIVATE": "Activează următoarea Cheie Web",
"ACTIVE": "În prezent activă",
"NEXT": "Următoarea în coadă",
"FUTURE": "Viitoare",
"WARNING": "Cheia web are mai puțin de 5 minute"
},
"CREATE": {
"TITLE": "Creează o nouă Cheie Web",
"DESCRIPTION": "Crearea unei noi chei web o va adăuga pe lista ta. ZITADEL folosește implicit chei RSA2048 cu un algoritm de hash SHA256.",
"KEY_TYPE": "Tip de Cheie",
"BITS": "Biti",
"HASHER": "Algoritm de Hash",
"CURVE": "Curbă"
},
"PREVIOUS_TABLE": {
"TITLE": "Chei Web Anterioare",
"DESCRIPTION": "Acestea sunt cheile tale web anterioare care nu mai sunt active.",
"DEACTIVATED_ON": "Dezactivată pe"
}
},
"MESSAGE_TEXTS": {
"TITLE": "Texte de mesaje",
"DESCRIPTION": "Personalizați textele mesajelor de e-mail sau SMS de notificare. Dacă doriți să dezactivați unele dintre limbi, restricționați-le în setările de limbă ale instanțelor dvs.",
@@ -1351,6 +1377,7 @@
"BRANDING": "Branding",
"PRIVACYPOLICY": "Linkuri externe",
"OIDC": "Durata de viață și expirarea tokenului OIDC",
"WEB_KEYS": "OIDC Web Keys",
"SECRETS": "Generator de secrete",
"SECURITY": "Setări de securitate",
"EVENTS": "Evenimente",

View File

@@ -185,6 +185,33 @@
"DESCRIPTION": "Срок действия неактивного токена обновления - это максимальное время, в течение которого токен обновления может оставаться неиспользованным."
}
},
"WEB_KEYS": {
"DESCRIPTION": "Управляйте своими OIDC веб-ключами для безопасной подписи и валидации токенов в вашем экземпляре ZITADEL.",
"TABLE": {
"TITLE": "Активные и будущие веб-ключи",
"DESCRIPTION": "Ваши активные и будущие веб-ключи. Активация нового ключа приведёт к деактивации текущего.",
"NOTE": "Примечание: Конечная точка JWKs OIDC возвращает кэшируемый ответ (по умолчанию 5 минут). Избегайте слишком ранней активации ключа, так как он может ещё не быть доступен в кэше и у клиентов.",
"ACTIVATE": "Активировать следующий веб-ключ",
"ACTIVE": "В настоящее время активен",
"NEXT": "Следующий в очереди",
"FUTURE": "Будущий",
"WARNING": "Веб-ключу менее 5 минут"
},
"CREATE": {
"TITLE": "Создать новый веб-ключ",
"DESCRIPTION": "Создание нового веб-ключа добавит его в ваш список. ZITADEL по умолчанию использует ключи RSA2048 с хешированием SHA256.",
"KEY_TYPE": "Тип ключа",
"BITS": "Биты",
"HASHER": "Алгоритм хеширования",
"CURVE": "Кривая"
},
"PREVIOUS_TABLE": {
"TITLE": "Предыдущие веб-ключи",
"DESCRIPTION": "Это ваши предыдущие веб-ключи, которые больше не активны.",
"DEACTIVATED_ON": "Деактивирован"
}
},
"MESSAGE_TEXTS": {
"TITLE": "Тексты сообщений",
"DESCRIPTION": "Настройте тексты ваших уведомлений по электронной почте или SMS. Если вы хотите отключить некоторые языки, ограничьте их в настройках языка ваших экземпляров.",
@@ -1397,6 +1424,7 @@
"BRANDING": "Брендинг",
"PRIVACYPOLICY": "Политика конфиденциальности",
"OIDC": "Срок действия токена OIDC",
"WEB_KEYS": "OIDC Web Keys",
"SECRETS": "Отображение ключа",
"SECURITY": "Настройки безопасности",
"EVENTS": "События",

View File

@@ -185,6 +185,32 @@
"DESCRIPTION": "Den inaktiva förnyelsetokenens livslängd är den maximala tiden en förnyelsetoken kan vara oanvänd."
}
},
"WEB_KEYS": {
"DESCRIPTION": "Hantera dina OIDC-webbnycklar för att säkert signera och validera tokens för din ZITADEL-instans.",
"TABLE": {
"TITLE": "Aktiva och framtida webbnycklar",
"DESCRIPTION": "Dina aktiva och kommande webbnycklar. Aktivering av en ny nyckel kommer att inaktivera den nuvarande.",
"NOTE": "Observera: JWKs OIDC-slutpunkten returnerar ett cachebart svar (standard 5 min). Undvik att aktivera en nyckel för tidigt, eftersom den kanske ännu inte är tillgänglig i cache och för klienter.",
"ACTIVATE": "Aktivera nästa webbnyckel",
"ACTIVE": "För närvarande aktiv",
"NEXT": "Nästa i kön",
"FUTURE": "Framtida",
"WARNING": "Webbnyckeln är mindre än 5 minuter gammal"
},
"CREATE": {
"TITLE": "Skapa ny webbnyckel",
"DESCRIPTION": "Att skapa en ny webbnyckel lägger till den i din lista. ZITADEL använder som standard RSA2048-nycklar med en SHA256-hasher.",
"KEY_TYPE": "Nyckeltyp",
"BITS": "Bitar",
"HASHER": "Hasher",
"CURVE": "Kurva"
},
"PREVIOUS_TABLE": {
"TITLE": "Tidigare webbnycklar",
"DESCRIPTION": "Detta är dina tidigare webbnycklar som inte längre är aktiva.",
"DEACTIVATED_ON": "Inaktiverad den"
}
},
"MESSAGE_TEXTS": {
"TITLE": "Meddelandetexter",
"DESCRIPTION": "Anpassa texterna i dina notifikationsmail eller SMS-meddelanden. Om du vill inaktivera några av språken, begränsa dem i dina instansers språkinställningar.",
@@ -1357,6 +1383,7 @@
"BRANDING": "Varumärke",
"PRIVACYPOLICY": "Externa länkar",
"OIDC": "OIDC-token livstid och utgång",
"WEB_KEYS": "OIDC Web Keys",
"SECRETS": "Hemlighetsgenerator",
"SECURITY": "Säkerhetsinställningar",
"EVENTS": "Händelser",

View File

@@ -185,6 +185,32 @@
"DESCRIPTION": "空闲刷新令牌的生命周期是刷新令牌可以未使用的最长时间。"
}
},
"WEB_KEYS": {
"DESCRIPTION": "管理您的 OIDC Web 密钥,以安全地签署和验证您的 ZITADEL 实例的令牌。",
"TABLE": {
"TITLE": "活动和未来的 Web 密钥",
"DESCRIPTION": "您的当前活动和即将启用的 Web 密钥。激活新密钥将会停用当前密钥。",
"NOTE": "注意JWKs OIDC 端点返回可缓存的响应(默认 5 分钟)。请避免过早激活密钥,否则它可能尚未在缓存或客户端中可用。",
"ACTIVATE": "激活下一个 Web 密钥",
"ACTIVE": "当前活动",
"NEXT": "队列中的下一个",
"FUTURE": "未来",
"WARNING": "Web密钥不到5分钟。"
},
"CREATE": {
"TITLE": "创建新的 Web 密钥",
"DESCRIPTION": "创建新的 Web 密钥会将其添加到您的列表。ZITADEL 默认使用 RSA2048 密钥和 SHA256 哈希算法。",
"KEY_TYPE": "密钥类型",
"BITS": "位数",
"HASHER": "哈希算法",
"CURVE": "曲线"
},
"PREVIOUS_TABLE": {
"TITLE": "先前的 Web 密钥",
"DESCRIPTION": "这些是您之前使用但不再活动的 Web 密钥。",
"DEACTIVATED_ON": "停用时间"
}
},
"MESSAGE_TEXTS": {
"TITLE": "消息文本",
"DESCRIPTION": "自定义您的通知电子邮件或短信消息的文本。如果您想禁用某些语言,请在您的实例语言设置中限制它们。",
@@ -1353,6 +1379,7 @@
"BRANDING": "品牌标识",
"PRIVACYPOLICY": "隐私政策",
"OIDC": "OIDC 令牌有效期和过期时间",
"WEB_KEYS": "OIDC Web Keys",
"SECRETS": "验证码外观",
"SECURITY": "安全设置",
"EVENTS": "活动",

View File

@@ -133,6 +133,11 @@
color: if($is-dark-theme, #ffc1c1, #620e0e);
background-color: if($is-dark-theme, map-get($background, state-inactive), #ffc1c1);
}
&.neutral {
background: if($is-dark-theme, #01489c78, #47a8ff82);
color: if($is-dark-theme, #47a8ff, #01489c);
}
}
.bg-state {

View File

@@ -3528,13 +3528,20 @@
"@zitadel/proto" "1.0.3"
jose "^5.3.0"
"@zitadel/proto@1.0.3", "@zitadel/proto@^1.0.3":
"@zitadel/proto@1.0.3":
version "1.0.3"
resolved "https://registry.yarnpkg.com/@zitadel/proto/-/proto-1.0.3.tgz#28721710d9e87009adf14f90e0c8cb9bae5275ec"
integrity sha512-95XPGgFgfTwU1A3oQYxTv4p+Qy/9yMO/o21VRtPBfVhPusFFCW0ddg4YoKTKpQl9FbIG7VYMLmRyuJBPuf3r+g==
dependencies:
"@bufbuild/protobuf" "^2.2.2"
"@zitadel/proto@1.0.5-sha-47a2ab5":
version "1.0.5-sha-47a2ab5"
resolved "https://registry.yarnpkg.com/@zitadel/proto/-/proto-1.0.5-sha-47a2ab5.tgz#c0ef7d9e7d0c41d12206e5762a255b3336763732"
integrity sha512-ZqPGJEs9Rl5ialU3OY48pU8QcR3yy79SBDMFluFNc256VJI73t7EI/aAxlJn6BcyQ7/CTASg3QfdVDKLMNEU5g==
dependencies:
"@bufbuild/protobuf" "^2.2.2"
"@zkochan/js-yaml@0.0.6":
version "0.0.6"
resolved "https://registry.yarnpkg.com/@zkochan/js-yaml/-/js-yaml-0.0.6.tgz#975f0b306e705e28b8068a07737fa46d3fc04826"