mirror of
https://github.com/zitadel/zitadel.git
synced 2025-08-11 19:07:30 +00:00
feat(console): friendly quota exhausted screen (#5790)
* fix: remove access interceptor for console * feat: show dialog on exhausted requests * fix exhausted cookie handling * fix quota exhausted screen * read instance mgmt from environment.json * fix interceptors * don't check cookie on environment.json * fix environment.json path * exclude environment.json from cookie check * remove cookie before loading env * use UTC to delete the cookie * delete cookie before fetching env * simplify cookie handling * lint * review cleanup * use exhausted property from env json * fix bootstrapping * lint * always open mgmt link if present * chore: fetch env json before ng serve * wait for cookie to be removed * fix typo * don't wait for cookie to be set
This commit is contained in:
@@ -231,7 +231,6 @@ The commands in this section are tested against the following software versions:
|
|||||||
- [Node version v16.17.0](https://nodejs.org/en/download/)
|
- [Node version v16.17.0](https://nodejs.org/en/download/)
|
||||||
- [npm version 8.18.0](https://docs.npmjs.com/try-the-latest-stable-version-of-npm)
|
- [npm version 8.18.0](https://docs.npmjs.com/try-the-latest-stable-version-of-npm)
|
||||||
- [Cypress runtime dependencies](https://docs.cypress.io/guides/continuous-integration/introduction#Dependencies)
|
- [Cypress runtime dependencies](https://docs.cypress.io/guides/continuous-integration/introduction#Dependencies)
|
||||||
- [curl version 7.58.0](https://curl.se/download.html)
|
|
||||||
|
|
||||||
<details>
|
<details>
|
||||||
<summary>Note for WSL2 on Windows 10</summary>
|
<summary>Note for WSL2 on Windows 10</summary>
|
||||||
@@ -269,18 +268,17 @@ To allow console access via http://localhost:4200, you have to configure the ZIT
|
|||||||
You can run the local console development server now.
|
You can run the local console development server now.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Console loads its target environment from the file console/src/assets/environment.json.
|
# Install npm dependencies
|
||||||
# Load it from the backend.
|
npm install
|
||||||
curl http://localhost:8080/ui/console/assets/environment.json > ./src/assets/environment.json
|
|
||||||
|
|
||||||
# Generate source files from Protos
|
# Generate source files from Protos
|
||||||
npm run generate
|
npm run generate
|
||||||
|
|
||||||
# Install npm dependencies
|
|
||||||
npm install
|
|
||||||
|
|
||||||
# Start the server
|
# Start the server
|
||||||
npm start
|
npm start
|
||||||
|
|
||||||
|
# If you don't want to develop against http://localhost:8080, you can use another environment
|
||||||
|
ENVIRONMENT_JSON_URL=https://my-cloud-instance-abcdef.zitadel.cloud/ui/console/assets/environment.json npm start
|
||||||
```
|
```
|
||||||
|
|
||||||
Navigate to http://localhost:4200/.
|
Navigate to http://localhost:4200/.
|
||||||
|
@@ -3,7 +3,7 @@
|
|||||||
"version": "0.0.0",
|
"version": "0.0.0",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"ng": "ng",
|
"ng": "ng",
|
||||||
"start": "ng serve",
|
"start": "node prebuild.development.js && ng serve",
|
||||||
"build": "ng build --configuration production --base-href=/ui/console/",
|
"build": "ng build --configuration production --base-href=/ui/console/",
|
||||||
"prelint": "npm run generate",
|
"prelint": "npm run generate",
|
||||||
"lint": "ng lint && prettier --check src",
|
"lint": "ng lint && prettier --check src",
|
||||||
|
23
console/prebuild.development.js
Normal file
23
console/prebuild.development.js
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
var fs = require('fs');
|
||||||
|
var path = require('path')
|
||||||
|
var https = require('https');
|
||||||
|
|
||||||
|
var defaultEnvironmentJsonURL = 'http://localhost:8080/ui/console/assets/environment.json'
|
||||||
|
var devEnvFile = path.join(__dirname, "src", "assets", "environment.json")
|
||||||
|
var url = process.env["ENVIRONMENT_JSON_URL"] || defaultEnvironmentJsonURL;
|
||||||
|
|
||||||
|
https.get(url, function (res) {
|
||||||
|
var body = '';
|
||||||
|
|
||||||
|
res.on('data', function (chunk) {
|
||||||
|
body += chunk;
|
||||||
|
});
|
||||||
|
|
||||||
|
res.on('end', function () {
|
||||||
|
fs.writeFileSync(devEnvFile, body);
|
||||||
|
console.log("Developing against the following environment")
|
||||||
|
console.log(JSON.stringify(JSON.parse(body), null, 4))
|
||||||
|
});
|
||||||
|
}).on('error', function (e) {
|
||||||
|
console.error("Got an error: ", e);
|
||||||
|
});
|
@@ -1,5 +1,5 @@
|
|||||||
import { CommonModule, registerLocaleData } from '@angular/common';
|
import { CommonModule, registerLocaleData } from '@angular/common';
|
||||||
import { HttpClientModule } from '@angular/common/http';
|
import { HttpClientModule, HTTP_INTERCEPTORS } from '@angular/common/http';
|
||||||
import localeDe from '@angular/common/locales/de';
|
import localeDe from '@angular/common/locales/de';
|
||||||
import localeEn from '@angular/common/locales/en';
|
import localeEn from '@angular/common/locales/en';
|
||||||
import localeEs from '@angular/common/locales/es';
|
import localeEs from '@angular/common/locales/es';
|
||||||
@@ -27,7 +27,6 @@ import { RoleGuard } from 'src/app/guards/role.guard';
|
|||||||
import { UserGuard } from 'src/app/guards/user.guard';
|
import { UserGuard } from 'src/app/guards/user.guard';
|
||||||
import { InfoOverlayModule } from 'src/app/modules/info-overlay/info-overlay.module';
|
import { InfoOverlayModule } from 'src/app/modules/info-overlay/info-overlay.module';
|
||||||
import { AssetService } from 'src/app/services/asset.service';
|
import { AssetService } from 'src/app/services/asset.service';
|
||||||
|
|
||||||
import { AppRoutingModule } from './app-routing.module';
|
import { AppRoutingModule } from './app-routing.module';
|
||||||
import { AppComponent } from './app.component';
|
import { AppComponent } from './app.component';
|
||||||
import { HasRoleModule } from './directives/has-role/has-role.module';
|
import { HasRoleModule } from './directives/has-role/has-role.module';
|
||||||
@@ -40,9 +39,13 @@ import { HasRolePipeModule } from './pipes/has-role-pipe/has-role-pipe.module';
|
|||||||
import { AdminService } from './services/admin.service';
|
import { AdminService } from './services/admin.service';
|
||||||
import { AuthenticationService } from './services/authentication.service';
|
import { AuthenticationService } from './services/authentication.service';
|
||||||
import { BreadcrumbService } from './services/breadcrumb.service';
|
import { BreadcrumbService } from './services/breadcrumb.service';
|
||||||
|
import { EnvironmentService } from './services/environment.service';
|
||||||
|
import { ExhaustedService } from './services/exhausted.service';
|
||||||
import { GrpcAuthService } from './services/grpc-auth.service';
|
import { GrpcAuthService } from './services/grpc-auth.service';
|
||||||
import { GrpcService } from './services/grpc.service';
|
import { GrpcService } from './services/grpc.service';
|
||||||
import { AuthInterceptor } from './services/interceptors/auth.interceptor';
|
import { AuthInterceptor } from './services/interceptors/auth.interceptor';
|
||||||
|
import { ExhaustedGrpcInterceptor } from './services/interceptors/exhausted.grpc.interceptor';
|
||||||
|
import { ExhaustedHttpInterceptor } from './services/interceptors/exhausted.http.interceptor';
|
||||||
import { GRPC_INTERCEPTORS } from './services/interceptors/grpc-interceptor';
|
import { GRPC_INTERCEPTORS } from './services/interceptors/grpc-interceptor';
|
||||||
import { I18nInterceptor } from './services/interceptors/i18n.interceptor';
|
import { I18nInterceptor } from './services/interceptors/i18n.interceptor';
|
||||||
import { OrgInterceptor } from './services/interceptors/org.interceptor';
|
import { OrgInterceptor } from './services/interceptors/org.interceptor';
|
||||||
@@ -84,9 +87,9 @@ export class WebpackTranslateLoader implements TranslateLoader {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const appInitializerFn = (grpcServ: GrpcService) => {
|
const appInitializerFn = (grpcSvc: GrpcService) => {
|
||||||
return () => {
|
return () => {
|
||||||
return grpcServ.loadAppEnvironment();
|
return grpcSvc.loadAppEnvironment();
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -139,6 +142,8 @@ const authConfig: AuthConfig = {
|
|||||||
RoleGuard,
|
RoleGuard,
|
||||||
UserGuard,
|
UserGuard,
|
||||||
ThemeService,
|
ThemeService,
|
||||||
|
EnvironmentService,
|
||||||
|
ExhaustedService,
|
||||||
{
|
{
|
||||||
provide: APP_INITIALIZER,
|
provide: APP_INITIALIZER,
|
||||||
useFactory: appInitializerFn,
|
useFactory: appInitializerFn,
|
||||||
@@ -167,6 +172,16 @@ const authConfig: AuthConfig = {
|
|||||||
provide: OAuthStorage,
|
provide: OAuthStorage,
|
||||||
useClass: StorageService,
|
useClass: StorageService,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
provide: HTTP_INTERCEPTORS,
|
||||||
|
multi: true,
|
||||||
|
useClass: ExhaustedHttpInterceptor,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
provide: GRPC_INTERCEPTORS,
|
||||||
|
multi: true,
|
||||||
|
useClass: ExhaustedGrpcInterceptor,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
provide: GRPC_INTERCEPTORS,
|
provide: GRPC_INTERCEPTORS,
|
||||||
multi: true,
|
multi: true,
|
||||||
|
@@ -1,4 +1,4 @@
|
|||||||
import { Component } from '@angular/core';
|
import { Component, OnInit } from '@angular/core';
|
||||||
import { PrivacyPolicy } from 'src/app/proto/generated/zitadel/policy_pb';
|
import { PrivacyPolicy } from 'src/app/proto/generated/zitadel/policy_pb';
|
||||||
import { GrpcAuthService } from 'src/app/services/grpc-auth.service';
|
import { GrpcAuthService } from 'src/app/services/grpc-auth.service';
|
||||||
|
|
||||||
@@ -7,10 +7,12 @@ import { GrpcAuthService } from 'src/app/services/grpc-auth.service';
|
|||||||
templateUrl: './footer.component.html',
|
templateUrl: './footer.component.html',
|
||||||
styleUrls: ['./footer.component.scss'],
|
styleUrls: ['./footer.component.scss'],
|
||||||
})
|
})
|
||||||
export class FooterComponent {
|
export class FooterComponent implements OnInit {
|
||||||
public policy?: PrivacyPolicy.AsObject;
|
public policy?: PrivacyPolicy.AsObject;
|
||||||
constructor(public authService: GrpcAuthService) {
|
constructor(public authService: GrpcAuthService) {}
|
||||||
authService.getMyPrivacyPolicy().then((policyResp) => {
|
|
||||||
|
ngOnInit(): void {
|
||||||
|
this.authService.getMyPrivacyPolicy().then((policyResp) => {
|
||||||
if (policyResp.policy) {
|
if (policyResp.policy) {
|
||||||
this.policy = policyResp.policy;
|
this.policy = policyResp.policy;
|
||||||
}
|
}
|
||||||
|
@@ -83,7 +83,7 @@
|
|||||||
</a>
|
</a>
|
||||||
|
|
||||||
<a
|
<a
|
||||||
*ngIf="customerPortalLink"
|
*ngIf="customerPortalLink$ | async as customerPortalLink"
|
||||||
class="nav-item external-link"
|
class="nav-item external-link"
|
||||||
[href]="customerPortalLink"
|
[href]="customerPortalLink"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
|
@@ -1,16 +1,16 @@
|
|||||||
import { animate, keyframes, style, transition, trigger } from '@angular/animations';
|
import { animate, keyframes, style, transition, trigger } from '@angular/animations';
|
||||||
import { BreakpointObserver } from '@angular/cdk/layout';
|
import { BreakpointObserver } from '@angular/cdk/layout';
|
||||||
import { ConnectedPosition, ConnectionPositionPair } from '@angular/cdk/overlay';
|
import { ConnectedPosition, ConnectionPositionPair } from '@angular/cdk/overlay';
|
||||||
import { HttpClient } from '@angular/common/http';
|
|
||||||
import { Component, ElementRef, Input, OnDestroy, ViewChild } from '@angular/core';
|
import { Component, ElementRef, Input, OnDestroy, ViewChild } from '@angular/core';
|
||||||
import { UntypedFormControl } from '@angular/forms';
|
import { UntypedFormControl } from '@angular/forms';
|
||||||
import { Router } from '@angular/router';
|
import { Router } from '@angular/router';
|
||||||
import { BehaviorSubject, combineLatest, map, Observable, Subject, take } from 'rxjs';
|
import { BehaviorSubject, combineLatest, map, Observable, Subject } from 'rxjs';
|
||||||
import { Org } from 'src/app/proto/generated/zitadel/org_pb';
|
import { Org } from 'src/app/proto/generated/zitadel/org_pb';
|
||||||
import { User } from 'src/app/proto/generated/zitadel/user_pb';
|
import { User } from 'src/app/proto/generated/zitadel/user_pb';
|
||||||
import { AdminService } from 'src/app/services/admin.service';
|
import { AdminService } from 'src/app/services/admin.service';
|
||||||
import { AuthenticationService } from 'src/app/services/authentication.service';
|
import { AuthenticationService } from 'src/app/services/authentication.service';
|
||||||
import { BreadcrumbService, BreadcrumbType } from 'src/app/services/breadcrumb.service';
|
import { BreadcrumbService, BreadcrumbType } from 'src/app/services/breadcrumb.service';
|
||||||
|
import { EnvironmentService } from 'src/app/services/environment.service';
|
||||||
import { GrpcAuthService } from 'src/app/services/grpc-auth.service';
|
import { GrpcAuthService } from 'src/app/services/grpc-auth.service';
|
||||||
import { KeyboardShortcutsService } from 'src/app/services/keyboard-shortcuts/keyboard-shortcuts.service';
|
import { KeyboardShortcutsService } from 'src/app/services/keyboard-shortcuts/keyboard-shortcuts.service';
|
||||||
import { ManagementService } from 'src/app/services/mgmt.service';
|
import { ManagementService } from 'src/app/services/mgmt.service';
|
||||||
@@ -90,7 +90,7 @@ export class NavComponent implements OnDestroy {
|
|||||||
private destroy$: Subject<void> = new Subject();
|
private destroy$: Subject<void> = new Subject();
|
||||||
|
|
||||||
public BreadcrumbType: any = BreadcrumbType;
|
public BreadcrumbType: any = BreadcrumbType;
|
||||||
public customerPortalLink: string = '';
|
public customerPortalLink$ = this.envService.env.pipe(map((env) => env.customer_portal));
|
||||||
|
|
||||||
public positions: ConnectedPosition[] = [
|
public positions: ConnectedPosition[] = [
|
||||||
new ConnectionPositionPair({ originX: 'start', originY: 'bottom' }, { overlayX: 'start', overlayY: 'top' }, 0, 10),
|
new ConnectionPositionPair({ originX: 'start', originY: 'bottom' }, { overlayX: 'start', overlayY: 'top' }, 0, 10),
|
||||||
@@ -98,6 +98,7 @@ export class NavComponent implements OnDestroy {
|
|||||||
];
|
];
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
|
private envService: EnvironmentService,
|
||||||
public authService: GrpcAuthService,
|
public authService: GrpcAuthService,
|
||||||
public adminService: AdminService,
|
public adminService: AdminService,
|
||||||
public authenticationService: AuthenticationService,
|
public authenticationService: AuthenticationService,
|
||||||
@@ -105,23 +106,9 @@ export class NavComponent implements OnDestroy {
|
|||||||
public mgmtService: ManagementService,
|
public mgmtService: ManagementService,
|
||||||
private router: Router,
|
private router: Router,
|
||||||
private breakpointObserver: BreakpointObserver,
|
private breakpointObserver: BreakpointObserver,
|
||||||
private http: HttpClient,
|
|
||||||
private shortcutService: KeyboardShortcutsService,
|
private shortcutService: KeyboardShortcutsService,
|
||||||
private storageService: StorageService,
|
private storageService: StorageService,
|
||||||
) {
|
) {}
|
||||||
this.loadEnvironment();
|
|
||||||
}
|
|
||||||
|
|
||||||
public loadEnvironment(): void {
|
|
||||||
this.http
|
|
||||||
.get('./assets/environment.json')
|
|
||||||
.pipe(take(1))
|
|
||||||
.subscribe((data: any) => {
|
|
||||||
if (data && data.customer_portal) {
|
|
||||||
this.customerPortalLink = data.customer_portal;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public ngOnDestroy() {
|
public ngOnDestroy() {
|
||||||
this.destroy$.next();
|
this.destroy$.next();
|
||||||
|
@@ -397,8 +397,8 @@
|
|||||||
</ng-container>
|
</ng-container>
|
||||||
|
|
||||||
<ng-container *ngIf="currentSetting === 'urls'">
|
<ng-container *ngIf="currentSetting === 'urls'">
|
||||||
<cnsl-card title=" {{ 'APP.URLS' | translate }}">
|
<cnsl-card title=" {{ 'APP.URLS' | translate }}" *ngIf="environmentMap$ | async as environmentMap">
|
||||||
<cnsl-info-section *ngIf="environmentMap['issuer']">
|
<cnsl-info-section *ngIf="environmentMap.issuer">
|
||||||
<div
|
<div
|
||||||
[innerHtml]="
|
[innerHtml]="
|
||||||
'APP.OIDC.WELLKNOWN' | translate : { url: environmentMap['issuer'] + '/.well-known/openid-configuration' }
|
'APP.OIDC.WELLKNOWN' | translate : { url: environmentMap['issuer'] + '/.well-known/openid-configuration' }
|
||||||
@@ -426,7 +426,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="app-info-row">
|
<div class="app-info-row">
|
||||||
<div class="app-info-wrapper" *ngFor="let wellKnownV of wellKnownMap | keyvalue">
|
<div class="app-info-wrapper" *ngFor="let wellKnownV of wellKnownMap$ | async | keyvalue">
|
||||||
<p class="app-info-row-title cnsl-secondary-text">{{ wellKnownV.key }}</p>
|
<p class="app-info-row-title cnsl-secondary-text">{{ wellKnownV.key }}</p>
|
||||||
<div class="app-copy-row">
|
<div class="app-copy-row">
|
||||||
<div *ngIf="wellKnownV.value" class="environment">
|
<div *ngIf="wellKnownV.value" class="environment">
|
||||||
|
@@ -1,6 +1,5 @@
|
|||||||
import { COMMA, ENTER, SPACE } from '@angular/cdk/keycodes';
|
import { COMMA, ENTER, SPACE } from '@angular/cdk/keycodes';
|
||||||
import { Location } from '@angular/common';
|
import { Location } from '@angular/common';
|
||||||
import { HttpClient } from '@angular/common/http';
|
|
||||||
import { Component, OnDestroy, OnInit } from '@angular/core';
|
import { Component, OnDestroy, OnInit } from '@angular/core';
|
||||||
import { AbstractControl, FormControl, UntypedFormBuilder, UntypedFormControl, UntypedFormGroup } from '@angular/forms';
|
import { AbstractControl, FormControl, UntypedFormBuilder, UntypedFormControl, UntypedFormGroup } from '@angular/forms';
|
||||||
import { MatLegacyCheckboxChange as MatCheckboxChange } from '@angular/material/legacy-checkbox';
|
import { MatLegacyCheckboxChange as MatCheckboxChange } from '@angular/material/legacy-checkbox';
|
||||||
@@ -10,7 +9,7 @@ import { TranslateService } from '@ngx-translate/core';
|
|||||||
import { Buffer } from 'buffer';
|
import { Buffer } from 'buffer';
|
||||||
import { Duration } from 'google-protobuf/google/protobuf/duration_pb';
|
import { Duration } from 'google-protobuf/google/protobuf/duration_pb';
|
||||||
import { Subject, Subscription } from 'rxjs';
|
import { Subject, Subscription } from 'rxjs';
|
||||||
import { take } from 'rxjs/operators';
|
import { map, take } from 'rxjs/operators';
|
||||||
import { RadioItemAuthType } from 'src/app/modules/app-radio/app-auth-method-radio/app-auth-method-radio.component';
|
import { RadioItemAuthType } from 'src/app/modules/app-radio/app-auth-method-radio/app-auth-method-radio.component';
|
||||||
import { ChangeType } from 'src/app/modules/changes/changes.component';
|
import { ChangeType } from 'src/app/modules/changes/changes.component';
|
||||||
import { InfoSectionType } from 'src/app/modules/info-section/info-section.component';
|
import { InfoSectionType } from 'src/app/modules/info-section/info-section.component';
|
||||||
@@ -41,6 +40,7 @@ import { GrpcAuthService } from 'src/app/services/grpc-auth.service';
|
|||||||
import { ManagementService } from 'src/app/services/mgmt.service';
|
import { ManagementService } from 'src/app/services/mgmt.service';
|
||||||
import { ToastService } from 'src/app/services/toast.service';
|
import { ToastService } from 'src/app/services/toast.service';
|
||||||
|
|
||||||
|
import { EnvironmentService } from 'src/app/services/environment.service';
|
||||||
import { AppSecretDialogComponent } from '../app-secret-dialog/app-secret-dialog.component';
|
import { AppSecretDialogComponent } from '../app-secret-dialog/app-secret-dialog.component';
|
||||||
import {
|
import {
|
||||||
BASIC_AUTH_METHOD,
|
BASIC_AUTH_METHOD,
|
||||||
@@ -79,8 +79,17 @@ export class AppDetailComponent implements OnInit, OnDestroy {
|
|||||||
public projectId: string = '';
|
public projectId: string = '';
|
||||||
public app?: App.AsObject;
|
public app?: App.AsObject;
|
||||||
|
|
||||||
public environmentMap: { [key: string]: string } = {};
|
public environmentMap$ = this.envSvc.env.pipe(
|
||||||
public wellKnownMap: { [key: string]: string } = {};
|
map((env) => {
|
||||||
|
return {
|
||||||
|
issuer: env.issuer,
|
||||||
|
adminServiceUrl: `${env.api}/admin/v1`,
|
||||||
|
mgmtServiceUrl: `${env.api}/management/v1`,
|
||||||
|
authServiceUrl: `${env.api}/auth/v1`,
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
public wellKnownMap$ = this.envSvc.wellKnown;
|
||||||
|
|
||||||
public oidcResponseTypes: OIDCResponseType[] = [
|
public oidcResponseTypes: OIDCResponseType[] = [
|
||||||
OIDCResponseType.OIDC_RESPONSE_TYPE_CODE,
|
OIDCResponseType.OIDC_RESPONSE_TYPE_CODE,
|
||||||
@@ -138,6 +147,7 @@ export class AppDetailComponent implements OnInit, OnDestroy {
|
|||||||
public currentSetting: string | undefined = this.settingsList[0].id;
|
public currentSetting: string | undefined = this.settingsList[0].id;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
|
private envSvc: EnvironmentService,
|
||||||
public translate: TranslateService,
|
public translate: TranslateService,
|
||||||
private route: ActivatedRoute,
|
private route: ActivatedRoute,
|
||||||
private toast: ToastService,
|
private toast: ToastService,
|
||||||
@@ -148,7 +158,6 @@ export class AppDetailComponent implements OnInit, OnDestroy {
|
|||||||
private authService: GrpcAuthService,
|
private authService: GrpcAuthService,
|
||||||
private router: Router,
|
private router: Router,
|
||||||
private breadcrumbService: BreadcrumbService,
|
private breadcrumbService: BreadcrumbService,
|
||||||
private http: HttpClient,
|
|
||||||
) {
|
) {
|
||||||
this.oidcForm = this.fb.group({
|
this.oidcForm = this.fb.group({
|
||||||
devMode: [{ value: false, disabled: true }],
|
devMode: [{ value: false, disabled: true }],
|
||||||
@@ -176,25 +185,6 @@ export class AppDetailComponent implements OnInit, OnDestroy {
|
|||||||
metadataUrl: [{ value: '', disabled: true }],
|
metadataUrl: [{ value: '', disabled: true }],
|
||||||
metadataXml: [{ value: '', disabled: true }],
|
metadataXml: [{ value: '', disabled: true }],
|
||||||
});
|
});
|
||||||
|
|
||||||
this.http.get('./assets/environment.json').subscribe((env: any) => {
|
|
||||||
this.environmentMap = {
|
|
||||||
issuer: env.issuer,
|
|
||||||
adminServiceUrl: `${env.api}/admin/v1`,
|
|
||||||
mgmtServiceUrl: `${env.api}/management/v1`,
|
|
||||||
authServiceUrl: `${env.api}/auth/v1`,
|
|
||||||
};
|
|
||||||
|
|
||||||
this.http.get(`${env.issuer}/.well-known/openid-configuration`).subscribe((wellKnown: any) => {
|
|
||||||
this.wellKnownMap = {
|
|
||||||
authorization_endpoint: wellKnown.authorization_endpoint,
|
|
||||||
end_session_endpoint: wellKnown.end_session_endpoint,
|
|
||||||
introspection_endpoint: wellKnown.introspection_endpoint,
|
|
||||||
token_endpoint: wellKnown.token_endpoint,
|
|
||||||
userinfo_endpoint: wellKnown.userinfo_endpoint,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public formatClockSkewLabel(seconds: number): string {
|
public formatClockSkewLabel(seconds: number): string {
|
||||||
|
@@ -1,9 +1,10 @@
|
|||||||
import { HttpClient } from '@angular/common/http';
|
import { HttpClient } from '@angular/common/http';
|
||||||
import { Injectable } from '@angular/core';
|
import { Injectable } from '@angular/core';
|
||||||
import { lastValueFrom } from 'rxjs';
|
import { switchMap } from 'rxjs';
|
||||||
|
|
||||||
import { PolicyComponentServiceType } from '../modules/policies/policy-component-types.enum';
|
import { PolicyComponentServiceType } from '../modules/policies/policy-component-types.enum';
|
||||||
import { Theme } from '../modules/policies/private-labeling-policy/private-labeling-policy.component';
|
import { Theme } from '../modules/policies/private-labeling-policy/private-labeling-policy.component';
|
||||||
|
import { EnvironmentService } from './environment.service';
|
||||||
import { StorageService } from './storage.service';
|
import { StorageService } from './storage.service';
|
||||||
|
|
||||||
const authorizationKey = 'Authorization';
|
const authorizationKey = 'Authorization';
|
||||||
@@ -69,46 +70,29 @@ export const ENDPOINT = {
|
|||||||
providedIn: 'root',
|
providedIn: 'root',
|
||||||
})
|
})
|
||||||
export class AssetService {
|
export class AssetService {
|
||||||
private serviceUrl!: Promise<string>;
|
|
||||||
private accessToken: string = '';
|
private accessToken: string = '';
|
||||||
constructor(private http: HttpClient, private storageService: StorageService) {
|
constructor(private envService: EnvironmentService, private http: HttpClient, private storageService: StorageService) {
|
||||||
const aT = this.storageService.getItem(accessTokenStorageKey);
|
const aT = this.storageService.getItem(accessTokenStorageKey);
|
||||||
|
|
||||||
if (aT) {
|
if (aT) {
|
||||||
this.accessToken = aT;
|
this.accessToken = aT;
|
||||||
}
|
}
|
||||||
this.serviceUrl = this.getServiceUrl();
|
|
||||||
}
|
|
||||||
|
|
||||||
private async getServiceUrl(): Promise<string> {
|
|
||||||
const url = await lastValueFrom(this.http.get('./assets/environment.json'))
|
|
||||||
.then((data: any) => {
|
|
||||||
if (data && data.api) {
|
|
||||||
return data.api;
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch((error) => {
|
|
||||||
console.error(error);
|
|
||||||
});
|
|
||||||
|
|
||||||
return url;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public upload(endpoint: AssetEndpoint | string, body: any, orgId?: string): Promise<any> {
|
public upload(endpoint: AssetEndpoint | string, body: any, orgId?: string): Promise<any> {
|
||||||
const headers: any = {
|
const headers: any = {
|
||||||
[authorizationKey]: `${bearerPrefix} ${this.accessToken}`,
|
[authorizationKey]: `${bearerPrefix} ${this.accessToken}`,
|
||||||
};
|
};
|
||||||
|
|
||||||
if (orgId) {
|
if (orgId) {
|
||||||
headers[orgKey] = `${orgId}`;
|
headers[orgKey] = `${orgId}`;
|
||||||
}
|
}
|
||||||
|
return this.envService.env
|
||||||
return this.serviceUrl.then((url) =>
|
.pipe(
|
||||||
this.http
|
switchMap((env) =>
|
||||||
.post(`${url}/assets/v1/${endpoint}`, body, {
|
this.http.post(`${env.api}/assets/v1/${endpoint}`, body, {
|
||||||
headers: headers,
|
headers: headers,
|
||||||
})
|
}),
|
||||||
.toPromise(),
|
),
|
||||||
);
|
)
|
||||||
|
.toPromise();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -35,10 +35,8 @@ export class AuthenticationService {
|
|||||||
Object.assign(this.authConfig, partialConfig);
|
Object.assign(this.authConfig, partialConfig);
|
||||||
}
|
}
|
||||||
this.oauthService.configure(this.authConfig);
|
this.oauthService.configure(this.authConfig);
|
||||||
|
|
||||||
this.oauthService.strictDiscoveryDocumentValidation = false;
|
this.oauthService.strictDiscoveryDocumentValidation = false;
|
||||||
await this.oauthService.loadDiscoveryDocumentAndTryLogin();
|
await this.oauthService.loadDiscoveryDocumentAndTryLogin();
|
||||||
|
|
||||||
this._authenticated = this.oauthService.hasValidAccessToken();
|
this._authenticated = this.oauthService.hasValidAccessToken();
|
||||||
if (!this.oauthService.hasValidIdToken() || !this.authenticated || partialConfig || force) {
|
if (!this.oauthService.hasValidIdToken() || !this.authenticated || partialConfig || force) {
|
||||||
const newState = await lastValueFrom(this.statehandler.createState());
|
const newState = await lastValueFrom(this.statehandler.createState());
|
||||||
|
88
console/src/app/services/environment.service.ts
Normal file
88
console/src/app/services/environment.service.ts
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
import { HttpClient } from '@angular/common/http';
|
||||||
|
import { Injectable } from '@angular/core';
|
||||||
|
import { catchError, map, Observable, of, shareReplay, switchMap, throwError } from 'rxjs';
|
||||||
|
|
||||||
|
import { AdminServiceClient } from '../proto/generated/zitadel/AdminServiceClientPb';
|
||||||
|
import { AuthServiceClient } from '../proto/generated/zitadel/AuthServiceClientPb';
|
||||||
|
import { ManagementServiceClient } from '../proto/generated/zitadel/ManagementServiceClientPb';
|
||||||
|
import { ExhaustedService } from './exhausted.service';
|
||||||
|
|
||||||
|
export interface Environment {
|
||||||
|
api: string;
|
||||||
|
clientid: string;
|
||||||
|
issuer: string;
|
||||||
|
customer_portal?: string;
|
||||||
|
instance_management_url?: string;
|
||||||
|
exhausted?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface WellKnown {
|
||||||
|
authorization_endpoint: string;
|
||||||
|
end_session_endpoint: string;
|
||||||
|
introspection_endpoint: string;
|
||||||
|
token_endpoint: string;
|
||||||
|
userinfo_endpoint: string;
|
||||||
|
}
|
||||||
|
@Injectable({
|
||||||
|
providedIn: 'root',
|
||||||
|
})
|
||||||
|
export class EnvironmentService {
|
||||||
|
private environmentJsonPath = './assets/environment.json';
|
||||||
|
private wellknownPath = '/.well-known/openid-configuration`';
|
||||||
|
public auth!: AuthServiceClient;
|
||||||
|
public mgmt!: ManagementServiceClient;
|
||||||
|
public admin!: AdminServiceClient;
|
||||||
|
|
||||||
|
private environment$: Observable<Environment>;
|
||||||
|
private wellKnown$: Observable<WellKnown>;
|
||||||
|
|
||||||
|
constructor(private http: HttpClient, private exhaustedSvc: ExhaustedService) {
|
||||||
|
this.environment$ = this.createEnvironment();
|
||||||
|
this.wellKnown$ = this.createWellKnown(this.environment$);
|
||||||
|
}
|
||||||
|
|
||||||
|
// env returns an `Observable<Environment>` that can be subscribed to whenever needed.
|
||||||
|
// It makes the HTTP call exactly once and replays the cached result.
|
||||||
|
// If the responses exhausted property is true, the exhaused dialog is shown.
|
||||||
|
get env() {
|
||||||
|
return this.environment$;
|
||||||
|
}
|
||||||
|
|
||||||
|
// wellKnown returns an `Observable<Environment>` that can be subscribed to whenever needed.
|
||||||
|
// It makes the HTTP call exactly once and replays the cached result.
|
||||||
|
get wellKnown() {
|
||||||
|
return this.wellKnown$;
|
||||||
|
}
|
||||||
|
|
||||||
|
private createEnvironment() {
|
||||||
|
return this.http.get<Environment>(this.environmentJsonPath).pipe(
|
||||||
|
catchError((err) => {
|
||||||
|
console.error('Getting environment.json failed', err);
|
||||||
|
return throwError(() => err);
|
||||||
|
}),
|
||||||
|
switchMap((env) => {
|
||||||
|
const env$ = of(env);
|
||||||
|
if (env.exhausted) {
|
||||||
|
return this.exhaustedSvc.showExhaustedDialog(env$).pipe(map(() => env));
|
||||||
|
}
|
||||||
|
return env$;
|
||||||
|
}),
|
||||||
|
// Cache the first response, then replay it
|
||||||
|
shareReplay(1),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private createWellKnown(environment$: Observable<Environment>) {
|
||||||
|
return environment$.pipe(
|
||||||
|
catchError((err) => {
|
||||||
|
console.error('Getting well-known OIDC configuration failed', err);
|
||||||
|
return throwError(() => err);
|
||||||
|
}),
|
||||||
|
switchMap((env) => {
|
||||||
|
return this.http.get<WellKnown>(`${env.issuer}${this.wellknownPath}`);
|
||||||
|
}),
|
||||||
|
// Cache the first response, then replay it
|
||||||
|
shareReplay(1),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
41
console/src/app/services/exhausted.service.ts
Normal file
41
console/src/app/services/exhausted.service.ts
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
import { Injectable } from '@angular/core';
|
||||||
|
import { MatLegacyDialog as MatDialog } from '@angular/material/legacy-dialog';
|
||||||
|
import { map, Observable, of, switchMap, tap } from 'rxjs';
|
||||||
|
import { WarnDialogComponent } from '../modules/warn-dialog/warn-dialog.component';
|
||||||
|
import { Environment } from './environment.service';
|
||||||
|
|
||||||
|
@Injectable({
|
||||||
|
providedIn: 'root',
|
||||||
|
})
|
||||||
|
export class ExhaustedService {
|
||||||
|
private isClosed = true;
|
||||||
|
|
||||||
|
constructor(private dialog: MatDialog) {}
|
||||||
|
|
||||||
|
public showExhaustedDialog(env$: Observable<Environment>) {
|
||||||
|
if (!this.isClosed) {
|
||||||
|
return of(undefined);
|
||||||
|
}
|
||||||
|
this.isClosed = false;
|
||||||
|
return this.dialog
|
||||||
|
.open(WarnDialogComponent, {
|
||||||
|
data: {
|
||||||
|
confirmKey: 'ACTIONS.CONTINUE',
|
||||||
|
titleKey: 'ERRORS.EXHAUSTED.TITLE',
|
||||||
|
descriptionKey: 'ERRORS.EXHAUSTED.DESCRIPTION',
|
||||||
|
},
|
||||||
|
disableClose: true,
|
||||||
|
width: '400px',
|
||||||
|
id: 'authenticated-requests-exhausted-dialog',
|
||||||
|
})
|
||||||
|
.afterClosed()
|
||||||
|
.pipe(
|
||||||
|
switchMap(() => env$),
|
||||||
|
tap((env) => {
|
||||||
|
// Just reload if there is no instance management url
|
||||||
|
location.href = env.instance_management_url || location.href;
|
||||||
|
}),
|
||||||
|
map(() => undefined),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@@ -1,18 +1,22 @@
|
|||||||
import { PlatformLocation } from '@angular/common';
|
import { PlatformLocation } from '@angular/common';
|
||||||
import { HttpClient } from '@angular/common/http';
|
|
||||||
import { Injectable } from '@angular/core';
|
import { Injectable } from '@angular/core';
|
||||||
import { MatLegacyDialog as MatDialog } from '@angular/material/legacy-dialog';
|
import { MatLegacyDialog as MatDialog } from '@angular/material/legacy-dialog';
|
||||||
import { TranslateService } from '@ngx-translate/core';
|
import { TranslateService } from '@ngx-translate/core';
|
||||||
import { AuthConfig } from 'angular-oauth2-oidc';
|
import { AuthConfig } from 'angular-oauth2-oidc';
|
||||||
|
import { catchError, switchMap, tap, throwError } from 'rxjs';
|
||||||
|
|
||||||
import { AdminServiceClient } from '../proto/generated/zitadel/AdminServiceClientPb';
|
import { AdminServiceClient } from '../proto/generated/zitadel/AdminServiceClientPb';
|
||||||
import { AuthServiceClient } from '../proto/generated/zitadel/AuthServiceClientPb';
|
import { AuthServiceClient } from '../proto/generated/zitadel/AuthServiceClientPb';
|
||||||
import { ManagementServiceClient } from '../proto/generated/zitadel/ManagementServiceClientPb';
|
import { ManagementServiceClient } from '../proto/generated/zitadel/ManagementServiceClientPb';
|
||||||
import { AuthenticationService } from './authentication.service';
|
import { AuthenticationService } from './authentication.service';
|
||||||
|
import { EnvironmentService } from './environment.service';
|
||||||
|
import { ExhaustedService } from './exhausted.service';
|
||||||
import { AuthInterceptor } from './interceptors/auth.interceptor';
|
import { AuthInterceptor } from './interceptors/auth.interceptor';
|
||||||
|
import { ExhaustedGrpcInterceptor } from './interceptors/exhausted.grpc.interceptor';
|
||||||
import { I18nInterceptor } from './interceptors/i18n.interceptor';
|
import { I18nInterceptor } from './interceptors/i18n.interceptor';
|
||||||
import { OrgInterceptor } from './interceptors/org.interceptor';
|
import { OrgInterceptor } from './interceptors/org.interceptor';
|
||||||
import { StorageService } from './storage.service';
|
import { StorageService } from './storage.service';
|
||||||
|
import { ThemeService } from './theme.service';
|
||||||
|
|
||||||
@Injectable({
|
@Injectable({
|
||||||
providedIn: 'root',
|
providedIn: 'root',
|
||||||
@@ -23,22 +27,30 @@ export class GrpcService {
|
|||||||
public admin!: AdminServiceClient;
|
public admin!: AdminServiceClient;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private http: HttpClient,
|
private envService: EnvironmentService,
|
||||||
private platformLocation: PlatformLocation,
|
private platformLocation: PlatformLocation,
|
||||||
private authenticationService: AuthenticationService,
|
private authenticationService: AuthenticationService,
|
||||||
private storageService: StorageService,
|
private storageService: StorageService,
|
||||||
private dialog: MatDialog,
|
private dialog: MatDialog,
|
||||||
private translate: TranslateService,
|
private translate: TranslateService,
|
||||||
|
private exhaustedService: ExhaustedService,
|
||||||
|
private themeService: ThemeService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
public async loadAppEnvironment(): Promise<any> {
|
public loadAppEnvironment(): Promise<any> {
|
||||||
return this.http
|
this.themeService.applyLabelPolicy();
|
||||||
.get('./assets/environment.json')
|
// We use the browser language until we can make API requests to get the users configured language.
|
||||||
.toPromise()
|
return this.translate
|
||||||
.then((data: any) => {
|
.use(this.translate.getBrowserLang() || this.translate.defaultLang)
|
||||||
if (data && data.api && data.issuer) {
|
.pipe(
|
||||||
|
switchMap(() => this.envService.env),
|
||||||
|
tap((env) => {
|
||||||
|
if (!env?.api || !env?.issuer) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
const interceptors = {
|
const interceptors = {
|
||||||
unaryInterceptors: [
|
unaryInterceptors: [
|
||||||
|
new ExhaustedGrpcInterceptor(this.exhaustedService, this.envService),
|
||||||
new OrgInterceptor(this.storageService),
|
new OrgInterceptor(this.storageService),
|
||||||
new AuthInterceptor(this.authenticationService, this.storageService, this.dialog),
|
new AuthInterceptor(this.authenticationService, this.storageService, this.dialog),
|
||||||
new I18nInterceptor(this.translate),
|
new I18nInterceptor(this.translate),
|
||||||
@@ -46,19 +58,19 @@ export class GrpcService {
|
|||||||
};
|
};
|
||||||
|
|
||||||
this.auth = new AuthServiceClient(
|
this.auth = new AuthServiceClient(
|
||||||
data.api,
|
env.api,
|
||||||
null,
|
null,
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
interceptors,
|
interceptors,
|
||||||
);
|
);
|
||||||
this.mgmt = new ManagementServiceClient(
|
this.mgmt = new ManagementServiceClient(
|
||||||
data.api,
|
env.api,
|
||||||
null,
|
null,
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
interceptors,
|
interceptors,
|
||||||
);
|
);
|
||||||
this.admin = new AdminServiceClient(
|
this.admin = new AdminServiceClient(
|
||||||
data.api,
|
env.api,
|
||||||
null,
|
null,
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
interceptors,
|
interceptors,
|
||||||
@@ -68,19 +80,20 @@ export class GrpcService {
|
|||||||
scope: 'openid profile email',
|
scope: 'openid profile email',
|
||||||
responseType: 'code',
|
responseType: 'code',
|
||||||
oidc: true,
|
oidc: true,
|
||||||
clientId: data.clientid,
|
clientId: env.clientid,
|
||||||
issuer: data.issuer,
|
issuer: env.issuer,
|
||||||
redirectUri: window.location.origin + this.platformLocation.getBaseHrefFromDOM() + 'auth/callback',
|
redirectUri: window.location.origin + this.platformLocation.getBaseHrefFromDOM() + 'auth/callback',
|
||||||
postLogoutRedirectUri: window.location.origin + this.platformLocation.getBaseHrefFromDOM() + 'signedout',
|
postLogoutRedirectUri: window.location.origin + this.platformLocation.getBaseHrefFromDOM() + 'signedout',
|
||||||
requireHttps: false,
|
requireHttps: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
this.authenticationService.initConfig(authConfig);
|
this.authenticationService.initConfig(authConfig);
|
||||||
}
|
}),
|
||||||
return Promise.resolve(data);
|
catchError((err) => {
|
||||||
})
|
console.error('Failed to load environment from assets', err);
|
||||||
.catch(() => {
|
return throwError(() => err);
|
||||||
console.error('Failed to load environment from assets');
|
}),
|
||||||
});
|
)
|
||||||
|
.toPromise();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -43,7 +43,7 @@ export class AuthInterceptor<TReq = unknown, TResp = unknown> implements UnaryIn
|
|||||||
.then((response: any) => {
|
.then((response: any) => {
|
||||||
return response;
|
return response;
|
||||||
})
|
})
|
||||||
.catch((error: any) => {
|
.catch(async (error: any) => {
|
||||||
if (error.code === 16) {
|
if (error.code === 16) {
|
||||||
this.triggerDialog.next(true);
|
this.triggerDialog.next(true);
|
||||||
}
|
}
|
||||||
|
@@ -0,0 +1,29 @@
|
|||||||
|
import { Injectable } from '@angular/core';
|
||||||
|
import { Request, StatusCode, UnaryInterceptor, UnaryResponse } from 'grpc-web';
|
||||||
|
import { EnvironmentService } from '../environment.service';
|
||||||
|
import { ExhaustedService } from '../exhausted.service';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ExhaustedGrpcInterceptor shows the exhausted dialog after receiving a gRPC response status 8.
|
||||||
|
*/
|
||||||
|
@Injectable({ providedIn: 'root' })
|
||||||
|
export class ExhaustedGrpcInterceptor<TReq = unknown, TResp = unknown> implements UnaryInterceptor<TReq, TResp> {
|
||||||
|
constructor(private exhaustedSvc: ExhaustedService, private envSvc: EnvironmentService) {}
|
||||||
|
|
||||||
|
public async intercept(
|
||||||
|
request: Request<TReq, TResp>,
|
||||||
|
invoker: (request: Request<TReq, TResp>) => Promise<UnaryResponse<TReq, TResp>>,
|
||||||
|
): Promise<UnaryResponse<TReq, TResp>> {
|
||||||
|
return invoker(request).catch((error: any) => {
|
||||||
|
if (error.code === StatusCode.RESOURCE_EXHAUSTED) {
|
||||||
|
return this.exhaustedSvc
|
||||||
|
.showExhaustedDialog(this.envSvc.env)
|
||||||
|
.toPromise()
|
||||||
|
.then(() => {
|
||||||
|
throw error;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
throw error;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,24 @@
|
|||||||
|
import { HttpErrorResponse, HttpEvent, HttpHandler, HttpInterceptor, HttpRequest } from '@angular/common/http';
|
||||||
|
import { Injectable } from '@angular/core';
|
||||||
|
import { catchError, Observable, switchMap, throwError } from 'rxjs';
|
||||||
|
import { EnvironmentService } from '../environment.service';
|
||||||
|
import { ExhaustedService } from '../exhausted.service';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ExhaustedHttpInterceptor shows the exhausted dialog after receiving an HTTP response status 429.
|
||||||
|
*/
|
||||||
|
@Injectable()
|
||||||
|
export class ExhaustedHttpInterceptor implements HttpInterceptor {
|
||||||
|
constructor(private exhaustedSvc: ExhaustedService, private envSvc: EnvironmentService) {}
|
||||||
|
|
||||||
|
intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
|
||||||
|
return next.handle(req).pipe(
|
||||||
|
catchError((error: HttpErrorResponse) => {
|
||||||
|
if (error.status === 429) {
|
||||||
|
return this.exhaustedSvc.showExhaustedDialog(this.envSvc.env).pipe(switchMap(() => throwError(() => error)));
|
||||||
|
}
|
||||||
|
return throwError(() => error);
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@@ -1,43 +0,0 @@
|
|||||||
import { HttpEvent, HttpHandler, HttpInterceptor, HttpRequest } from '@angular/common/http';
|
|
||||||
import { OAuthModuleConfig } from 'angular-oauth2-oidc';
|
|
||||||
import { Observable } from 'rxjs';
|
|
||||||
|
|
||||||
import { Org } from '../../proto/generated/zitadel/org_pb';
|
|
||||||
import { StorageKey, StorageLocation, StorageService } from '../storage.service';
|
|
||||||
|
|
||||||
const orgKey = 'x-zitadel-orgid';
|
|
||||||
export abstract class HttpOrgInterceptor implements HttpInterceptor {
|
|
||||||
private org!: Org.AsObject;
|
|
||||||
|
|
||||||
protected get validUrls(): string[] {
|
|
||||||
return this.oauthModuleConfig.resourceServer.allowedUrls || [];
|
|
||||||
}
|
|
||||||
|
|
||||||
constructor(private storageService: StorageService, protected oauthModuleConfig: OAuthModuleConfig) {
|
|
||||||
const org: Org.AsObject | null = this.storageService.getItem(StorageKey.organization, StorageLocation.session);
|
|
||||||
|
|
||||||
if (org) {
|
|
||||||
this.org = org;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
|
|
||||||
if (!this.urlValidation(req.url)) {
|
|
||||||
return next.handle(req);
|
|
||||||
}
|
|
||||||
|
|
||||||
return next.handle(
|
|
||||||
req.clone({
|
|
||||||
setHeaders: {
|
|
||||||
[orgKey]: this.org.id,
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
private urlValidation(toIntercept: string): boolean {
|
|
||||||
const URLS = ['https://api.zitadel.dev/assets', 'https://api.zitadel.ch/assets'];
|
|
||||||
|
|
||||||
return URLS.findIndex((url) => toIntercept.startsWith(url)) > -1;
|
|
||||||
}
|
|
||||||
}
|
|
@@ -252,6 +252,10 @@
|
|||||||
"TITLE": "Du bist abgemeldet",
|
"TITLE": "Du bist abgemeldet",
|
||||||
"DESCRIPTION": "Klicke auf \"Einloggen\", um Dich erneut anzumelden."
|
"DESCRIPTION": "Klicke auf \"Einloggen\", um Dich erneut anzumelden."
|
||||||
},
|
},
|
||||||
|
"EXHAUSTED": {
|
||||||
|
"TITLE": "Dein Kontingent an authentifizierten Anfragen is erschöpft.",
|
||||||
|
"DESCRIPTION": "Lösche oder erhöhe die Grenze für diese ZITADEL Instanz."
|
||||||
|
},
|
||||||
"INVALID_FORMAT": "Das Format is ungültig.",
|
"INVALID_FORMAT": "Das Format is ungültig.",
|
||||||
"NOTANEMAIL": "Der eingegebene Wert ist keine E-Mail Adresse.",
|
"NOTANEMAIL": "Der eingegebene Wert ist keine E-Mail Adresse.",
|
||||||
"MINLENGTH": "Muss mindestens {{requiredLength}} Zeichen lang sein.",
|
"MINLENGTH": "Muss mindestens {{requiredLength}} Zeichen lang sein.",
|
||||||
|
@@ -253,6 +253,10 @@
|
|||||||
"TITLE": "Your authorization token has expired.",
|
"TITLE": "Your authorization token has expired.",
|
||||||
"DESCRIPTION": "Click the button below to log in again."
|
"DESCRIPTION": "Click the button below to log in again."
|
||||||
},
|
},
|
||||||
|
"EXHAUSTED": {
|
||||||
|
"TITLE": "Your quota for authenticated requests is exhausted.",
|
||||||
|
"DESCRIPTION": "Remove or increase the quota limit for this ZITADEL instance."
|
||||||
|
},
|
||||||
"INVALID_FORMAT": "The formatting is invalid.",
|
"INVALID_FORMAT": "The formatting is invalid.",
|
||||||
"NOTANEMAIL": "The given value is not an e-mail address.",
|
"NOTANEMAIL": "The given value is not an e-mail address.",
|
||||||
"MINLENGTH": "Must be at least {{requiredLength}} characters long.",
|
"MINLENGTH": "Must be at least {{requiredLength}} characters long.",
|
||||||
|
@@ -253,6 +253,10 @@
|
|||||||
"TITLE": "Tu token de autorización token ha caducado.",
|
"TITLE": "Tu token de autorización token ha caducado.",
|
||||||
"DESCRIPTION": "Haz clic en el botón más abajo para iniciar sesión otra vez."
|
"DESCRIPTION": "Haz clic en el botón más abajo para iniciar sesión otra vez."
|
||||||
},
|
},
|
||||||
|
"EXHAUSTED": {
|
||||||
|
"TITLE": "Su cuota de solicitudes autenticadas se ha agotado.",
|
||||||
|
"DESCRIPTION": "Borrar o aumentar el límite de esta instancia de ZITADEL."
|
||||||
|
},
|
||||||
"INVALID_FORMAT": "El formato no es valido.",
|
"INVALID_FORMAT": "El formato no es valido.",
|
||||||
"NOTANEMAIL": "El valor proporcionado no es una dirección de email.",
|
"NOTANEMAIL": "El valor proporcionado no es una dirección de email.",
|
||||||
"MINLENGTH": "Debe tener al menos {{requiredLength}} caracteres de longitud.",
|
"MINLENGTH": "Debe tener al menos {{requiredLength}} caracteres de longitud.",
|
||||||
|
@@ -252,6 +252,10 @@
|
|||||||
"TITLE": "Votre jeton d'autorisation a expiré.",
|
"TITLE": "Votre jeton d'autorisation a expiré.",
|
||||||
"DESCRIPTION": "Cliquez sur le bouton ci-dessous pour vous reconnecter."
|
"DESCRIPTION": "Cliquez sur le bouton ci-dessous pour vous reconnecter."
|
||||||
},
|
},
|
||||||
|
"EXHAUSTED": {
|
||||||
|
"TITLE": "Ton quota de demandes authentifiées est épuisé.",
|
||||||
|
"DESCRIPTION": "Supprimez ou augmentez la limite de cette instance ZITADEL."
|
||||||
|
},
|
||||||
"INVALID_FORMAT": "Le format n'est pas valide",
|
"INVALID_FORMAT": "Le format n'est pas valide",
|
||||||
"NOTANEMAIL": "La valeur donnée n'est pas une adresse e-mail",
|
"NOTANEMAIL": "La valeur donnée n'est pas une adresse e-mail",
|
||||||
"MINLENGTH": "Doit comporter au moins {{length}} caractères.",
|
"MINLENGTH": "Doit comporter au moins {{length}} caractères.",
|
||||||
|
@@ -252,6 +252,10 @@
|
|||||||
"TITLE": "Il tuo Access Token \u00e8 scaduto.",
|
"TITLE": "Il tuo Access Token \u00e8 scaduto.",
|
||||||
"DESCRIPTION": "Clicca il pulsante per richiedere una nuova sessione."
|
"DESCRIPTION": "Clicca il pulsante per richiedere una nuova sessione."
|
||||||
},
|
},
|
||||||
|
"EXHAUSTED": {
|
||||||
|
"TITLE": "La quota di richieste autenticate è esaurita.",
|
||||||
|
"DESCRIPTION": "Cancellare o aumentare il limite per questa istanza ZITADEL."
|
||||||
|
},
|
||||||
"INVALID_FORMAT": "Il formato non è valido.",
|
"INVALID_FORMAT": "Il formato non è valido.",
|
||||||
"NOTANEMAIL": "Il valore dato non \u00e8 un indirizzo e-mail.",
|
"NOTANEMAIL": "Il valore dato non \u00e8 un indirizzo e-mail.",
|
||||||
"MINLENGTH": "Deve essere lunga almeno {{requiredLength}} caratteri.",
|
"MINLENGTH": "Deve essere lunga almeno {{requiredLength}} caratteri.",
|
||||||
|
@@ -253,6 +253,10 @@
|
|||||||
"TITLE": "トークンが期限切れになりました。",
|
"TITLE": "トークンが期限切れになりました。",
|
||||||
"DESCRIPTION": "下のボタンをクリックして、もう一度ログインする。"
|
"DESCRIPTION": "下のボタンをクリックして、もう一度ログインする。"
|
||||||
},
|
},
|
||||||
|
"EXHAUSTED": {
|
||||||
|
"TITLE": "認証されたリクエストのクォータを使い果たしました",
|
||||||
|
"DESCRIPTION": "このZITADELインスタンスの制限を削除または増加させる"
|
||||||
|
},
|
||||||
"INVALID_FORMAT": "不正なフォーマットです",
|
"INVALID_FORMAT": "不正なフォーマットです",
|
||||||
"NOTANEMAIL": "入力された値がメールアドレスではありません。",
|
"NOTANEMAIL": "入力された値がメールアドレスではありません。",
|
||||||
"MINLENGTH": "{{requiredLength}} 文字以上の文字列が必要です。",
|
"MINLENGTH": "{{requiredLength}} 文字以上の文字列が必要です。",
|
||||||
|
@@ -252,6 +252,10 @@
|
|||||||
"TITLE": "Twój token autoryzacji wygasł.",
|
"TITLE": "Twój token autoryzacji wygasł.",
|
||||||
"DESCRIPTION": "Kliknij przycisk poniżej, aby ponownie się zalogować."
|
"DESCRIPTION": "Kliknij przycisk poniżej, aby ponownie się zalogować."
|
||||||
},
|
},
|
||||||
|
"EXHAUSTED": {
|
||||||
|
"TITLE": "Twój limit uwierzytelnionych wniosków został wyczerpany.",
|
||||||
|
"DESCRIPTION": "Usuń lub zwiększ limit dla tej instancji ZITADEL."
|
||||||
|
},
|
||||||
"INVALID_FORMAT": "Format jest nieprawidłowy.",
|
"INVALID_FORMAT": "Format jest nieprawidłowy.",
|
||||||
"NOTANEMAIL": "Podana wartość nie jest adresem e-mail.",
|
"NOTANEMAIL": "Podana wartość nie jest adresem e-mail.",
|
||||||
"MINLENGTH": "Musi mieć co najmniej {{requiredLength}} znaków.",
|
"MINLENGTH": "Musi mieć co najmniej {{requiredLength}} znaków.",
|
||||||
|
@@ -252,6 +252,10 @@
|
|||||||
"TITLE": "您的授权令牌已过期。",
|
"TITLE": "您的授权令牌已过期。",
|
||||||
"DESCRIPTION": "点击下方按钮再次登录。"
|
"DESCRIPTION": "点击下方按钮再次登录。"
|
||||||
},
|
},
|
||||||
|
"EXHAUSTED": {
|
||||||
|
"TITLE": "你的认证请求配额已用完.",
|
||||||
|
"DESCRIPTION": "删除或增加这个ZITADEL实例的限制。"
|
||||||
|
},
|
||||||
"INVALID_FORMAT": "格式是无效的。",
|
"INVALID_FORMAT": "格式是无效的。",
|
||||||
"NOTANEMAIL": "给定的值不是合法电子邮件地址。",
|
"NOTANEMAIL": "给定的值不是合法电子邮件地址。",
|
||||||
"MINLENGTH": "长度必须至少是{{requiredLength}}字符。",
|
"MINLENGTH": "长度必须至少是{{requiredLength}}字符。",
|
||||||
|
Reference in New Issue
Block a user