feat(console): v2 feature cleanup, fix styling issues, instance naming (#3530)

* new console

* move npm ci to angular build

* rel path for assets

* local grpc copy

* login policy, rm clear views, features rel path

* login_hint param

* edit default color values

* remove features, zitadel-tier, contact styles

* html formatter

* rm feature restrictions, fix theming issues

* instance naming

* statehandler

* rm class

* rm class

* update footer links

* statehandler

* stroked color

Co-authored-by: Livio Amstutz <[email protected]>
This commit is contained in:
Max Peintner
2022-04-29 10:25:12 +02:00
committed by GitHub
co-authored by Livio Amstutz
parent f9e79ef8fe
commit 41e60b3d8a
93 changed files with 5256 additions and 13100 deletions
+78 -127
View File
@@ -4,84 +4,84 @@ import { BehaviorSubject, from, merge, Observable, of, Subject } from 'rxjs';
import { catchError, filter, finalize, map, mergeMap, switchMap, take, timeout } from 'rxjs/operators';
import {
AddMyAuthFactorOTPRequest,
AddMyAuthFactorOTPResponse,
AddMyAuthFactorU2FRequest,
AddMyAuthFactorU2FResponse,
AddMyPasswordlessLinkRequest,
AddMyPasswordlessLinkResponse,
AddMyPasswordlessRequest,
AddMyPasswordlessResponse,
GetMyEmailRequest,
GetMyEmailResponse,
GetMyPasswordComplexityPolicyRequest,
GetMyPasswordComplexityPolicyResponse,
GetMyPhoneRequest,
GetMyPhoneResponse,
GetMyProfileRequest,
GetMyProfileResponse,
GetMyUserRequest,
GetMyUserResponse,
GetSupportedLanguagesRequest,
GetSupportedLanguagesResponse,
ListMyAuthFactorsRequest,
ListMyAuthFactorsResponse,
ListMyLinkedIDPsRequest,
ListMyLinkedIDPsResponse,
ListMyMembershipsRequest,
ListMyMembershipsResponse,
ListMyPasswordlessRequest,
ListMyPasswordlessResponse,
ListMyProjectOrgsRequest,
ListMyProjectOrgsResponse,
ListMyUserChangesRequest,
ListMyUserChangesResponse,
ListMyUserGrantsRequest,
ListMyUserGrantsResponse,
ListMyUserSessionsRequest,
ListMyUserSessionsResponse,
ListMyZitadelFeaturesRequest,
ListMyZitadelFeaturesResponse,
ListMyZitadelPermissionsRequest,
ListMyZitadelPermissionsResponse,
RemoveMyAuthFactorOTPRequest,
RemoveMyAuthFactorOTPResponse,
RemoveMyAuthFactorU2FRequest,
RemoveMyAuthFactorU2FResponse,
RemoveMyAvatarRequest,
RemoveMyAvatarResponse,
RemoveMyLinkedIDPRequest,
RemoveMyLinkedIDPResponse,
RemoveMyPasswordlessRequest,
RemoveMyPasswordlessResponse,
RemoveMyPhoneRequest,
RemoveMyPhoneResponse,
RemoveMyUserRequest,
RemoveMyUserResponse,
ResendMyEmailVerificationRequest,
ResendMyEmailVerificationResponse,
ResendMyPhoneVerificationRequest,
ResendMyPhoneVerificationResponse,
SendMyPasswordlessLinkRequest,
SendMyPasswordlessLinkResponse,
SetMyEmailRequest,
SetMyEmailResponse,
SetMyPhoneRequest,
SetMyPhoneResponse,
UpdateMyPasswordRequest,
UpdateMyPasswordResponse,
UpdateMyProfileRequest,
UpdateMyProfileResponse,
UpdateMyUserNameRequest,
UpdateMyUserNameResponse,
VerifyMyAuthFactorOTPRequest,
VerifyMyAuthFactorOTPResponse,
VerifyMyAuthFactorU2FRequest,
VerifyMyAuthFactorU2FResponse,
VerifyMyPasswordlessRequest,
VerifyMyPasswordlessResponse,
VerifyMyPhoneRequest,
VerifyMyPhoneResponse,
AddMyAuthFactorOTPRequest,
AddMyAuthFactorOTPResponse,
AddMyAuthFactorU2FRequest,
AddMyAuthFactorU2FResponse,
AddMyPasswordlessLinkRequest,
AddMyPasswordlessLinkResponse,
AddMyPasswordlessRequest,
AddMyPasswordlessResponse,
GetMyEmailRequest,
GetMyEmailResponse,
GetMyPasswordComplexityPolicyRequest,
GetMyPasswordComplexityPolicyResponse,
GetMyPhoneRequest,
GetMyPhoneResponse,
GetMyProfileRequest,
GetMyProfileResponse,
GetMyUserRequest,
GetMyUserResponse,
GetSupportedLanguagesRequest,
GetSupportedLanguagesResponse,
ListMyAuthFactorsRequest,
ListMyAuthFactorsResponse,
ListMyLinkedIDPsRequest,
ListMyLinkedIDPsResponse,
ListMyMembershipsRequest,
ListMyMembershipsResponse,
ListMyPasswordlessRequest,
ListMyPasswordlessResponse,
ListMyProjectOrgsRequest,
ListMyProjectOrgsResponse,
ListMyUserChangesRequest,
ListMyUserChangesResponse,
ListMyUserGrantsRequest,
ListMyUserGrantsResponse,
ListMyUserSessionsRequest,
ListMyUserSessionsResponse,
ListMyZitadelFeaturesRequest,
ListMyZitadelFeaturesResponse,
ListMyZitadelPermissionsRequest,
ListMyZitadelPermissionsResponse,
RemoveMyAuthFactorOTPRequest,
RemoveMyAuthFactorOTPResponse,
RemoveMyAuthFactorU2FRequest,
RemoveMyAuthFactorU2FResponse,
RemoveMyAvatarRequest,
RemoveMyAvatarResponse,
RemoveMyLinkedIDPRequest,
RemoveMyLinkedIDPResponse,
RemoveMyPasswordlessRequest,
RemoveMyPasswordlessResponse,
RemoveMyPhoneRequest,
RemoveMyPhoneResponse,
RemoveMyUserRequest,
RemoveMyUserResponse,
ResendMyEmailVerificationRequest,
ResendMyEmailVerificationResponse,
ResendMyPhoneVerificationRequest,
ResendMyPhoneVerificationResponse,
SendMyPasswordlessLinkRequest,
SendMyPasswordlessLinkResponse,
SetMyEmailRequest,
SetMyEmailResponse,
SetMyPhoneRequest,
SetMyPhoneResponse,
UpdateMyPasswordRequest,
UpdateMyPasswordResponse,
UpdateMyProfileRequest,
UpdateMyProfileResponse,
UpdateMyUserNameRequest,
UpdateMyUserNameResponse,
VerifyMyAuthFactorOTPRequest,
VerifyMyAuthFactorOTPResponse,
VerifyMyAuthFactorU2FRequest,
VerifyMyAuthFactorU2FResponse,
VerifyMyPasswordlessRequest,
VerifyMyPasswordlessResponse,
VerifyMyPhoneRequest,
VerifyMyPhoneResponse,
} from '../proto/generated/zitadel/auth_pb';
import { ChangeQuery } from '../proto/generated/zitadel/change_pb';
import { ListQuery } from '../proto/generated/zitadel/object_pb';
@@ -98,10 +98,8 @@ export class GrpcAuthService {
public user!: Observable<User.AsObject | undefined>;
public userSubject: BehaviorSubject<User.AsObject | undefined> = new BehaviorSubject<User.AsObject | undefined>(undefined);
private zitadelPermissions: BehaviorSubject<string[]> = new BehaviorSubject(['user.resourceowner']);
private zitadelFeatures: BehaviorSubject<string[]> = new BehaviorSubject(['']);
public readonly fetchedZitadelPermissions: BehaviorSubject<boolean> = new BehaviorSubject(false as boolean);
public readonly fetchedZitadelFeatures: BehaviorSubject<boolean> = new BehaviorSubject(false as boolean);
private cachedOrgs: Org.AsObject[] = [];
@@ -134,7 +132,6 @@ export class GrpcAuthService {
}),
finalize(() => {
this.loadPermissions();
this.loadFeatures();
}),
);
@@ -142,7 +139,6 @@ export class GrpcAuthService {
this.activeOrgChanged.subscribe(() => {
this.loadPermissions();
this.loadFeatures();
});
}
@@ -217,22 +213,6 @@ export class GrpcAuthService {
});
}
private loadFeatures(): void {
from(this.listMyZitadelFeatures())
.pipe(
map((featuresResp) => featuresResp.resultList),
catchError((_) => {
return of([]);
}),
finalize(() => {
this.fetchedZitadelFeatures.next(true);
}),
)
.subscribe((features) => {
this.zitadelFeatures.next(features);
});
}
/**
* returns true if user has one of the provided roles
* @param roles roles of the user
@@ -262,35 +242,6 @@ export class GrpcAuthService {
);
}
/**
* returns true if user has one of the provided features
* @param features regex of the user
*/
public canUseFeature(features: string[] | RegExp[]): Observable<boolean> {
if (features && features.length > 0) {
return this.zitadelFeatures.pipe(switchMap((zFeatures) => of(this.hasFeature(zFeatures, features))));
} else {
return of(false);
}
}
/**
* returns true if user has one of the provided features
* @param userFeature features of the user
* @param requestedFeature required features for accessing the respective component
*/
public hasFeature(userFeatures: string[], requestedFeatures: string[] | RegExp[]): boolean {
return (
requestedFeatures.findIndex((regexp: any) => {
return (
userFeatures.findIndex((feature) => {
return new RegExp(regexp).test(feature);
}) > -1
);
}) > -1
);
}
public getMyProfile(): Promise<GetMyProfileResponse.AsObject> {
return this.grpcService.auth.getMyProfile(new GetMyProfileRequest(), null).then((resp) => resp.toObject());
}
@@ -6,16 +6,16 @@ import { BehaviorSubject, Observable, of, pairwise, Subject, takeUntil } from 'r
import { KeyboardShortcutsComponent } from '../../modules/keyboard-shortcuts/keyboard-shortcuts.component';
import { GrpcAuthService } from '../grpc-auth.service';
import {
ACTIONS,
DOMAINS,
HOME,
KeyboardShortcut,
ME,
ORG,
PROJECTS,
SYSTEM,
USERGRANTS,
USERS,
ACTIONS,
DOMAINS,
HOME,
KeyboardShortcut,
ME,
ORG,
PROJECTS,
SYSTEM,
USERGRANTS,
USERS,
} from './keyboard-shortcuts';
@Injectable({
@@ -54,47 +54,47 @@ export class KeyboardShortcutsService implements OnDestroy {
}
if (firstKey.code === 'KeyG' && secondKey.code === 'KeyH') {
if (this.hasPermission(HOME) && this.hasFeature(HOME)) {
if (this.hasPermission(HOME)) {
this.router.navigate(HOME.link);
}
}
if (firstKey.code === 'KeyG' && secondKey.code === 'KeyS') {
if (this.hasPermission(SYSTEM) && this.hasFeature(SYSTEM)) {
if (this.hasPermission(SYSTEM)) {
this.router.navigate(SYSTEM.link);
}
}
if (firstKey.code === 'KeyG' && secondKey.code === 'KeyO') {
if (this.hasPermission(ORG) && this.hasFeature(ORG)) {
if (this.hasPermission(ORG)) {
this.router.navigate(ORG.link);
}
}
if (firstKey.code === 'KeyM' && secondKey.code === 'KeyE') {
if (this.hasPermission(ME) && this.hasFeature(ME)) {
if (this.hasPermission(ME)) {
this.router.navigate(ME.link);
}
}
if (firstKey.code === 'KeyG' && secondKey.code === 'KeyP') {
if (this.hasPermission(PROJECTS) && this.hasFeature(PROJECTS)) {
if (this.hasPermission(PROJECTS)) {
this.router.navigate(PROJECTS.link);
}
}
if (firstKey.code === 'KeyG' && secondKey.code === 'KeyU') {
if (this.hasPermission(USERS) && this.hasFeature(USERS)) {
if (this.hasPermission(USERS)) {
this.router.navigate(USERS.link);
}
}
if (firstKey.code === 'KeyG' && secondKey.code === 'KeyA') {
if (this.hasPermission(USERGRANTS) && this.hasFeature(USERGRANTS)) {
if (this.hasPermission(USERGRANTS)) {
this.router.navigate(USERGRANTS.link);
}
}
if (firstKey.code === 'KeyG' && secondKey.code === 'KeyF') {
if (this.hasPermission(ACTIONS) && this.hasFeature(ACTIONS)) {
if (this.hasPermission(ACTIONS)) {
this.router.navigate(ACTIONS.link);
}
}
if (firstKey.code === 'KeyG' && secondKey.code === 'KeyD') {
if (this.hasPermission(DOMAINS) && this.hasFeature(DOMAINS)) {
if (this.hasPermission(DOMAINS)) {
this.router.navigate(DOMAINS.link);
}
}
@@ -136,12 +136,4 @@ export class KeyboardShortcutsService implements OnDestroy {
return of(true);
}
}
private hasFeature(shortcut: KeyboardShortcut): Observable<boolean> {
if (shortcut.features) {
return this.authService.canUseFeature(shortcut.features);
} else {
return of(true);
}
}
}
@@ -24,7 +24,6 @@ export interface CnslOverlay {
requirements?: {
media?: string;
permission?: string[];
feature?: string[];
};
}
@@ -83,10 +82,9 @@ export class OverlayWorkflowService implements OnDestroy {
return zip([
this.meetsMediaRequirements(overlay),
this.meetsPermissionRequirements(overlay),
this.meetsFeatureRequirements(overlay),
] as Observable<boolean>[]).pipe(
switchMap(([media, permission, feature]) => {
return of(media && permission && feature);
switchMap(([media, permission]) => {
return of(media && permission);
}),
);
}
@@ -114,15 +112,6 @@ export class OverlayWorkflowService implements OnDestroy {
}
}
public meetsFeatureRequirements(overlay: CnslOverlay): Observable<boolean> {
const regexArray = overlay.requirements?.feature;
if (regexArray && regexArray.length) {
return this.authService.canUseFeature(regexArray);
} else {
return of(true);
}
}
public reset(): void {
this.currentWorkflow$.next(null);
if (this.callback) {
@@ -21,7 +21,7 @@ export const IntroWorkflowOverlays: CnslOverlay[] = [
content: {
number: 2,
count: 4,
i18nText: 'OVERLAYS.SYSTEM.TEXT',
i18nText: 'OVERLAYS.INSTANCE.TEXT',
},
requirements: {
media: '(min-width: 600px)',
@@ -4,34 +4,36 @@ import { Observable, of } from 'rxjs';
import { v4 as uuidv4 } from 'uuid';
export abstract class StatehandlerProcessorService {
public abstract createState(url: string): Observable<string | undefined>;
public abstract restoreState(state?: string): void;
public abstract createState(url: string): Observable<string | undefined>;
public abstract restoreState(state?: string): void;
}
@Injectable()
export class StatehandlerProcessorServiceImpl
implements StatehandlerProcessorService {
constructor(private location: Location) { }
export class StatehandlerProcessorServiceImpl implements StatehandlerProcessorService {
constructor(private location: Location) {}
public createState(url: string): Observable<string> {
const externalUrl = this.location.prepareExternalUrl(url);
const urlId = uuidv4();
public createState(url: string): Observable<string> {
const externalUrl = this.location.prepareExternalUrl(url);
const urlId = uuidv4();
sessionStorage.setItem(urlId, externalUrl);
return of(urlId);
if (externalUrl.includes('login_hint=')) {
sessionStorage.setItem(urlId, externalUrl);
}
public restoreState(state?: string): void {
if (state === undefined) {
return;
}
return of(urlId);
}
const url = sessionStorage.getItem(state);
if (url === null) {
return;
}
window.location.href = window.location.origin + url;
public restoreState(state?: string): void {
if (state === undefined) {
return;
}
const url = sessionStorage.getItem(state);
if (url === null) {
return;
}
sessionStorage.removeItem(state);
window.location.href = window.location.origin + url;
}
}
@@ -1,59 +1,79 @@
import { Injectable, Injector, OnDestroy, Type } from '@angular/core';
import { GuardsCheckStart, Router, RouterEvent } from '@angular/router';
import { OAuthService } from 'angular-oauth2-oidc';
import { Observable, Subject, throwError } from 'rxjs';
import { Observable, of, Subject, throwError } from 'rxjs';
import { filter, map, shareReplay, switchMap, take, takeUntil } from 'rxjs/operators';
import { StatehandlerProcessorService } from './statehandler-processor.service';
export abstract class StatehandlerService {
public abstract createState(): Observable<string | undefined>;
public abstract initStateHandler(): void;
public abstract createState(): Observable<string | undefined>;
public abstract initStateHandler(): void;
}
@Injectable()
export class StatehandlerServiceImpl
implements StatehandlerService, OnDestroy {
private events?: Observable<string>;
private unsubscribe$: Subject<void> = new Subject();
export class StatehandlerServiceImpl implements StatehandlerService, OnDestroy {
private events?: Observable<string>;
private unsubscribe$: Subject<void> = new Subject();
constructor(
oauthService: OAuthService,
private injector: Injector,
private processor: StatehandlerProcessorService,
) {
oauthService.events
.pipe(
filter(event => event.type === 'token_received'),
map(() => oauthService.state),
takeUntil(this.unsubscribe$),
)
.subscribe(state => processor.restoreState(state));
constructor(oauthService: OAuthService, private injector: Injector, private processor: StatehandlerProcessorService) {
oauthService.events
.pipe(
filter((event) => event.type === 'token_received'),
map(() => oauthService.state),
takeUntil(this.unsubscribe$),
)
.subscribe((state) => processor.restoreState(state));
}
public initStateHandler(): void {
const router = this.injector.get(Router as Type<Router>);
this.events = (router.events as Observable<RouterEvent>).pipe(
filter((event) => event instanceof GuardsCheckStart),
map((event) => event.url),
shareReplay(1),
);
this.events.pipe(takeUntil(this.unsubscribe$)).subscribe();
}
public createState(): Observable<string | undefined> {
if (this.events === undefined) {
return throwError(() => new Error('no router events'));
}
public initStateHandler(): void {
const router = this.injector.get(Router as Type<Router>);
this.events = (router.events as Observable<RouterEvent>).pipe(
filter(event => event instanceof GuardsCheckStart),
map(event => event.url),
shareReplay(1),
);
this.events.pipe(takeUntil(this.unsubscribe$)).subscribe();
}
public createState(): Observable<string | undefined> {
if (this.events === undefined) {
return throwError('no router events');
return this.events.pipe(
take(1),
switchMap((url: string) => {
if (url.includes('?login_hint=')) {
const newUrl = this.removeParam('login_hint', url);
return of(newUrl);
} else {
return this.processor.createState(url);
}
}),
);
}
return this.events.pipe(
take(1),
switchMap(url => this.processor.createState(url)),
);
}
public ngOnDestroy(): void {
this.unsubscribe$.next();
}
public ngOnDestroy(): void {
this.unsubscribe$.next();
removeParam(key: string, sourceURL: string) {
var rtn = sourceURL.split('?')[0],
param,
params_arr = [],
queryString = sourceURL.indexOf('?') !== -1 ? sourceURL.split('?')[1] : '';
if (queryString !== '') {
params_arr = queryString.split('&');
for (var i = params_arr.length - 1; i >= 0; i -= 1) {
param = params_arr[i].split('=')[0];
if (param === key) {
params_arr.splice(i, 1);
}
}
if (params_arr.length) rtn = rtn + '?' + params_arr.join('&');
}
return rtn;
}
}
@@ -1,98 +0,0 @@
import { HttpClient } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { StorageService } from './storage.service';
const authorizationKey = 'Authorization';
const bearerPrefix = 'Bearer';
const accessTokenStorageKey = 'access_token';
export interface StripeCustomer {
contact: string;
company?: string;
address: string;
city: string;
postal_code: string;
country: string;
}
@Injectable({
providedIn: 'root',
})
export class SubscriptionService {
constructor(private http: HttpClient, private storageService: StorageService) {}
public getLink(orgId: string, redirectURI: string): Promise<any> {
return this.http
.get('./assets/environment.json')
.toPromise()
.then((data: any) => {
if (data && data.subscriptionServiceUrl) {
const serviceUrl = data.subscriptionServiceUrl;
const accessToken = this.storageService.getItem(accessTokenStorageKey);
return this.http
.get(`${serviceUrl}/redirect`, {
headers: {
[authorizationKey]: `${bearerPrefix} ${accessToken}`,
},
params: {
org: orgId,
return_url: encodeURI(redirectURI),
country: 'ch',
},
})
.toPromise();
} else {
return Promise.reject('Could not load environment');
}
});
}
public getCustomer(orgId: string): Promise<any> {
return this.http
.get('../../assets/environment.json')
.toPromise()
.then((data: any) => {
if (data && data.subscriptionServiceUrl) {
const serviceUrl = data.subscriptionServiceUrl;
const accessToken = this.storageService.getItem(accessTokenStorageKey);
return this.http
.get(`${serviceUrl}/customer`, {
headers: {
[authorizationKey]: `${bearerPrefix} ${accessToken}`,
},
params: {
org: orgId,
},
})
.toPromise();
} else {
return Promise.reject('Could not load environment');
}
});
}
public setCustomer(orgId: string, body: StripeCustomer): Promise<any> {
return this.http
.get('./assets/environment.json')
.toPromise()
.then((data: any) => {
if (data && data.subscriptionServiceUrl) {
const serviceUrl = data.subscriptionServiceUrl;
const accessToken = this.storageService.getItem(accessTokenStorageKey);
return this.http
.post(`${serviceUrl}/customer`, body, {
headers: {
[authorizationKey]: `${bearerPrefix} ${accessToken}`,
},
params: {
org: orgId,
},
})
.toPromise();
} else {
return Promise.reject('Could not load environment');
}
});
}
}