feat(console): update deps, alternative hash function with fixed colors, use preferrenLoginName for hashing, fix iam write permissions, user img upload (#1846)

* chore(deps-dev): bump @types/jasmine from 3.6.9 to 3.7.7 in /console (#1824)

Bumps [@types/jasmine](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/jasmine) from 3.6.9 to 3.7.7.
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/jasmine)

---
updated-dependencies:
- dependency-name: "@types/jasmine"
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* chore(deps): bump google-protobuf from 3.15.8 to 3.17.2 in /console (#1823)

Bumps [google-protobuf](https://github.com/protocolbuffers/protobuf) from 3.15.8 to 3.17.2.
- [Release notes](https://github.com/protocolbuffers/protobuf/releases)
- [Changelog](https://github.com/protocolbuffers/protobuf/blob/master/generate_changelog.py)
- [Commits](https://github.com/protocolbuffers/protobuf/compare/v3.15.8...v3.17.2)

---
updated-dependencies:
- dependency-name: google-protobuf
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* chore(deps): bump @angular/animations from 12.0.0 to 12.0.3 in /console (#1821)

Bumps [@angular/animations](https://github.com/angular/angular/tree/HEAD/packages/animations) from 12.0.0 to 12.0.3.
- [Release notes](https://github.com/angular/angular/releases)
- [Changelog](https://github.com/angular/angular/blob/master/CHANGELOG.md)
- [Commits](https://github.com/angular/angular/commits/12.0.3/packages/animations)

---
updated-dependencies:
- dependency-name: "@angular/animations"
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* chore(deps): bump @angular/material from 12.0.0 to 12.0.3 in /console (#1819)

Bumps [@angular/material](https://github.com/angular/components) from 12.0.0 to 12.0.3.
- [Release notes](https://github.com/angular/components/releases)
- [Changelog](https://github.com/angular/components/blob/12.0.3/CHANGELOG.md)
- [Commits](https://github.com/angular/components/compare/12.0.0...12.0.3)

---
updated-dependencies:
- dependency-name: "@angular/material"
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* chore(deps-dev): bump prettier from 2.2.1 to 2.3.1 in /console (#1818)

Bumps [prettier](https://github.com/prettier/prettier) from 2.2.1 to 2.3.1.
- [Release notes](https://github.com/prettier/prettier/releases)
- [Changelog](https://github.com/prettier/prettier/blob/main/CHANGELOG.md)
- [Commits](https://github.com/prettier/prettier/compare/2.2.1...2.3.1)

---
updated-dependencies:
- dependency-name: prettier
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* chore(deps): bump @angular/platform-browser-dynamic in /console (#1817)

Bumps [@angular/platform-browser-dynamic](https://github.com/angular/angular/tree/HEAD/packages/platform-browser-dynamic) from 12.0.0 to 12.0.3.
- [Release notes](https://github.com/angular/angular/releases)
- [Changelog](https://github.com/angular/angular/blob/master/CHANGELOG.md)
- [Commits](https://github.com/angular/angular/commits/12.0.3/packages/platform-browser-dynamic)

---
updated-dependencies:
- dependency-name: "@angular/platform-browser-dynamic"
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* chore(deps-dev): bump @types/node from 14.14.37 to 15.12.1 in /console (#1815)

Bumps [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) from 14.14.37 to 15.12.1.
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node)

---
updated-dependencies:
- dependency-name: "@types/node"
  dependency-type: direct:development
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* chore(deps): bump @angular/common from 12.0.0 to 12.0.3 in /console (#1814)

Bumps [@angular/common](https://github.com/angular/angular/tree/HEAD/packages/common) from 12.0.0 to 12.0.3.
- [Release notes](https://github.com/angular/angular/releases)
- [Changelog](https://github.com/angular/angular/blob/master/CHANGELOG.md)
- [Commits](https://github.com/angular/angular/commits/12.0.3/packages/common)

---
updated-dependencies:
- dependency-name: "@angular/common"
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* chore(deps): bump @angular/material-moment-adapter from 12.0.0 to 12.0.3 in /console (#1816)

* chore(deps): bump @angular/material-moment-adapter in /console

Bumps [@angular/material-moment-adapter](https://github.com/angular/components) from 12.0.0 to 12.0.3.
- [Release notes](https://github.com/angular/components/releases)
- [Changelog](https://github.com/angular/components/blob/12.0.3/CHANGELOG.md)
- [Commits](https://github.com/angular/components/compare/12.0.0...12.0.3)

---
updated-dependencies:
- dependency-name: "@angular/material-moment-adapter"
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>

* sort

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Max Peintner <max@caos.ch>

* chore(deps-dev): bump @angular/language-service in /console (#1822)

Bumps [@angular/language-service](https://github.com/angular/angular/tree/HEAD/packages/language-service) from 12.0.0 to 12.0.3.
- [Release notes](https://github.com/angular/angular/releases)
- [Changelog](https://github.com/angular/angular/blob/master/CHANGELOG.md)
- [Commits](https://github.com/angular/angular/commits/12.0.3/packages/language-service)

---
updated-dependencies:
- dependency-name: "@angular/language-service"
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Max Peintner <max@caos.ch>

* image cropper

* fix: avatar bg colors, preferred username, login script

* lint

* membership color

* rem logs

* profile picture component

* pic comp

* dialog tirgger btn

* trigger dialog, styles

* lock

* interceptor for org, upload, remove

* tooltip

* lint

* stylelint

* generate same credentials of username as in login

* deletepic

* fix disable privatelabeling on missing feature, i18n

* lint

* stylelint

* block loading images if no feature

* lint

* optimize feature check

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
This commit is contained in:
Max Peintner
2021-06-11 11:15:04 +02:00
committed by GitHub
parent 2502f379d9
commit 1e77b8aeae
45 changed files with 11967 additions and 8113 deletions

16221
console/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -10,16 +10,16 @@
}, },
"private": true, "private": true,
"dependencies": { "dependencies": {
"@angular/animations": "~12.0.0", "@angular/animations": "~12.0.3",
"@angular/cdk": "~12.0.0", "@angular/cdk": "~12.0.0",
"@angular/common": "~12.0.0", "@angular/common": "~12.0.3",
"@angular/compiler": "~12.0.0", "@angular/compiler": "~12.0.0",
"@angular/core": "~12.0.0", "@angular/core": "~12.0.0",
"@angular/forms": "~12.0.0", "@angular/forms": "~12.0.0",
"@angular/material": "^12.0.0", "@angular/material": "^12.0.3",
"@angular/material-moment-adapter": "^12.0.0", "@angular/material-moment-adapter": "^12.0.3",
"@angular/platform-browser": "~12.0.0", "@angular/platform-browser": "~12.0.0",
"@angular/platform-browser-dynamic": "~12.0.0", "@angular/platform-browser-dynamic": "~12.0.3",
"@angular/router": "~12.0.0", "@angular/router": "~12.0.0",
"@angular/service-worker": "~12.0.0", "@angular/service-worker": "~12.0.0",
"@grpc/grpc-js": "^1.3.2", "@grpc/grpc-js": "^1.3.2",
@@ -33,10 +33,11 @@
"cors": "^2.8.5", "cors": "^2.8.5",
"file-saver": "^2.0.5", "file-saver": "^2.0.5",
"google-proto-files": "^2.4.0", "google-proto-files": "^2.4.0",
"google-protobuf": "^3.15.8", "google-protobuf": "^3.17.2",
"grpc-web": "^1.2.1", "grpc-web": "^1.2.1",
"libphonenumber-js": "^1.9.16", "libphonenumber-js": "^1.9.16",
"moment": "^2.29.1", "moment": "^2.29.1",
"ngx-image-cropper": "^3.3.5",
"ngx-color": "^7.0.0", "ngx-color": "^7.0.0",
"ngx-quicklink": "^0.2.6", "ngx-quicklink": "^0.2.6",
"rxjs": "~6.6.7", "rxjs": "~6.6.7",
@@ -49,10 +50,10 @@
"@angular-devkit/build-angular": "~12.0.0", "@angular-devkit/build-angular": "~12.0.0",
"@angular/cli": "~12.0.0", "@angular/cli": "~12.0.0",
"@angular/compiler-cli": "~12.0.0", "@angular/compiler-cli": "~12.0.0",
"@angular/language-service": "~12.0.0", "@angular/language-service": "~12.0.3",
"@types/jasmine": "~3.6.9", "@types/jasmine": "~3.7.7",
"@types/jasminewd2": "~2.0.3", "@types/jasminewd2": "~2.0.3",
"@types/node": "^14.14.37", "@types/node": "^15.12.1",
"codelyzer": "^6.0.0", "codelyzer": "^6.0.0",
"jasmine-core": "~3.7.1", "jasmine-core": "~3.7.1",
"jasmine-spec-reporter": "~7.0.0", "jasmine-spec-reporter": "~7.0.0",
@@ -61,7 +62,7 @@
"karma-coverage-istanbul-reporter": "~3.0.2", "karma-coverage-istanbul-reporter": "~3.0.2",
"karma-jasmine": "~4.0.0", "karma-jasmine": "~4.0.0",
"karma-jasmine-html-reporter": "^1.5.0", "karma-jasmine-html-reporter": "^1.5.0",
"prettier": "^2.2.1", "prettier": "^2.3.1",
"protractor": "~7.0.0", "protractor": "~7.0.0",
"stylelint": "^13.10.0", "stylelint": "^13.10.0",
"stylelint-config-standard": "^20.0.0", "stylelint-config-standard": "^20.0.0",

View File

@@ -60,7 +60,7 @@
<div (clickOutside)="closeAccountCard()" class="icon-container"> <div (clickOutside)="closeAccountCard()" class="icon-container">
<app-avatar <app-avatar
*ngIf="user && (user.human?.profile?.displayName || (user.human?.profile?.firstName && user.human?.profile?.lastName))" *ngIf="user && (user.human?.profile?.displayName || (user.human?.profile?.firstName && user.human?.profile?.lastName))"
class="avatar dontcloseonclick" (click)="showAccount = !showAccount" [active]="showAccount" class="avatar dontcloseonclick" (click)="showAccount = !showAccount" [active]="showAccount" [forColor]="user?.preferredLoginName"
[name]="user.human.profile.displayName ? user.human.profile.displayName : (user.human.profile.firstName + ' '+ user.human.profile.lastName)" [name]="user.human.profile.displayName ? user.human.profile.displayName : (user.human.profile.firstName + ' '+ user.human.profile.lastName)"
[size]="38"> [size]="38">
</app-avatar> </app-avatar>

View File

@@ -2,6 +2,7 @@
<app-avatar <app-avatar
*ngIf="user.human?.profile && (user.human?.profile?.displayName || (user.human?.profile?.firstName && user.human?.profile?.lastName))" *ngIf="user.human?.profile && (user.human?.profile?.displayName || (user.human?.profile?.firstName && user.human?.profile?.lastName))"
class="avatar" class="avatar"
[forColor]="user.preferredLoginName"
[name]="user.human?.profile?.displayName ? user.human?.profile?.displayName : (user.human?.profile?.firstName + ' '+ user.human?.profile?.lastName)" [name]="user.human?.profile?.displayName ? user.human?.profile?.displayName : (user.human?.profile?.firstName + ' '+ user.human?.profile?.lastName)"
[size]="80"> [size]="80">
</app-avatar> </app-avatar>
@@ -14,8 +15,7 @@
<div class="l-accounts"> <div class="l-accounts">
<mat-progress-bar *ngIf="loadingUsers" color="primary" mode="indeterminate"></mat-progress-bar> <mat-progress-bar *ngIf="loadingUsers" color="primary" mode="indeterminate"></mat-progress-bar>
<a class="row" *ngFor="let session of sessions" (click)="selectAccount(session.loginName)"> <a class="row" *ngFor="let session of sessions" (click)="selectAccount(session.loginName)">
<app-avatar *ngIf="session && session.displayName" class="small-avatar" <app-avatar *ngIf="session && session.displayName" class="small-avatar" [forColor]="session.loginName" [size]="32">
[name]="session.displayName ? session.displayName : ''" [size]="32">
</app-avatar> </app-avatar>
<div class="col"> <div class="col">

View File

@@ -6,69 +6,70 @@ import { AuthenticationService } from 'src/app/services/authentication.service';
import { GrpcAuthService } from 'src/app/services/grpc-auth.service'; import { GrpcAuthService } from 'src/app/services/grpc-auth.service';
@Component({ @Component({
selector: 'app-accounts-card', selector: 'app-accounts-card',
templateUrl: './accounts-card.component.html', templateUrl: './accounts-card.component.html',
styleUrls: ['./accounts-card.component.scss'], styleUrls: ['./accounts-card.component.scss'],
}) })
export class AccountsCardComponent implements OnInit { export class AccountsCardComponent implements OnInit {
@Input() public user!: User.AsObject; @Input() public user!: User.AsObject;
@Input() public iamuser: boolean = false; @Input() public iamuser: boolean = false;
@Output() public close: EventEmitter<void> = new EventEmitter(); @Output() public close: EventEmitter<void> = new EventEmitter();
public sessions: Session.AsObject[] = []; public sessions: Session.AsObject[] = [];
public loadingUsers: boolean = false; public loadingUsers: boolean = false;
constructor(public authService: AuthenticationService, private router: Router, private userService: GrpcAuthService) { constructor(public authService: AuthenticationService, private router: Router, private userService: GrpcAuthService) {
this.userService.listMyUserSessions().then(sessions => { this.userService.listMyUserSessions().then(sessions => {
this.sessions = sessions.resultList; this.sessions = sessions.resultList;
const index = this.sessions.findIndex(user => user.loginName === this.user.preferredLoginName); console.log(sessions.resultList);
if (index > -1) { const index = this.sessions.findIndex(user => user.loginName === this.user.preferredLoginName);
this.sessions.splice(index, 1); if (index > -1) {
} this.sessions.splice(index, 1);
}
this.loadingUsers = false; this.loadingUsers = false;
}).catch(() => { }).catch(() => {
this.loadingUsers = false; this.loadingUsers = false;
}); });
}
public ngOnInit(): void {
this.loadingUsers = true;
}
public editUserProfile(): void {
this.router.navigate(['users/me']);
this.close.emit();
}
public closeCard(element: HTMLElement): void {
if (!element.classList.contains('dontcloseonclick')) {
this.close.emit();
} }
}
public ngOnInit(): void { public selectAccount(loginHint?: string): void {
this.loadingUsers = true; const configWithPrompt: Partial<AuthConfig> = {
customQueryParams: {
// prompt: 'select_account',
} as any,
};
if (loginHint) {
(configWithPrompt as any).customQueryParams['login_hint'] = loginHint;
} }
this.authService.authenticate(configWithPrompt);
}
public editUserProfile(): void { public selectNewAccount(): void {
this.router.navigate(['users/me']); const configWithPrompt: Partial<AuthConfig> = {
this.close.emit(); customQueryParams: {
} prompt: 'login',
} as any,
};
this.authService.authenticate(configWithPrompt);
}
public closeCard(element: HTMLElement): void { public logout(): void {
if (!element.classList.contains('dontcloseonclick')) { this.authService.signout();
this.close.emit(); this.close.emit();
} }
}
public selectAccount(loginHint?: string): void {
const configWithPrompt: Partial<AuthConfig> = {
customQueryParams: {
// prompt: 'select_account',
} as any,
};
if (loginHint) {
(configWithPrompt as any).customQueryParams['login_hint'] = loginHint;
}
this.authService.authenticate(configWithPrompt);
}
public selectNewAccount(): void {
const configWithPrompt: Partial<AuthConfig> = {
customQueryParams: {
prompt: 'login',
} as any,
};
this.authService.authenticate(configWithPrompt);
}
public logout(): void {
this.authService.signout();
this.close.emit();
}
} }

View File

@@ -1,64 +1,87 @@
import { Component, Input, OnInit } from '@angular/core'; import { Component, Input, OnInit } from '@angular/core';
@Component({ @Component({
selector: 'app-avatar', selector: 'app-avatar',
templateUrl: './avatar.component.html', templateUrl: './avatar.component.html',
styleUrls: ['./avatar.component.scss'], styleUrls: ['./avatar.component.scss'],
}) })
export class AvatarComponent implements OnInit { export class AvatarComponent implements OnInit {
@Input() name: string = ''; @Input() name: string = '';
@Input() credentials: string = ''; @Input() credentials: string = '';
@Input() size: number = 24; @Input() size: number = 24;
@Input() fontSize: number = 14; @Input() fontSize: number = 14;
@Input() active: boolean = false; @Input() active: boolean = false;
@Input() color: string = ''; @Input() color: string = '';
constructor() { } @Input() forColor: string = '';
constructor() { }
ngOnInit(): void { ngOnInit(): void {
if (!this.credentials) { if (!this.credentials && this.forColor) {
const split: string[] = this.name.split(' '); this.credentials = this.getInitials(this.forColor);
this.credentials = split[0].charAt(0) + (split[1] ? split[1].charAt(0) : ''); if (!this.color) {
if (!this.color) { this.color = this.getColor(this.forColor || '');
this.color = this.getColor(this.name); }
}
}
if (this.size > 50) {
this.fontSize = 32;
}
} }
getColor(userName: string): string { if (this.size > 50) {
const colors = [ this.fontSize = 32;
'linear-gradient(40deg, #B44D51 30%, rgb(241,138,138))',
'linear-gradient(40deg, #B75073 30%, rgb(234,96,143))',
'linear-gradient(40deg, #84498E 30%, rgb(214,116,230))',
'linear-gradient(40deg, #705998 30%, rgb(163,131,220))',
'linear-gradient(40deg, #5C6598 30%, rgb(135,148,222))',
'linear-gradient(40deg, #7F90D3 30%, rgb(181,196,247))',
'linear-gradient(40deg, #3E93B9 30%, rgb(150,215,245))',
'linear-gradient(40deg, #3494A0 30%, rgb(71,205,222))',
'linear-gradient(40deg, #25716A 30%, rgb(58,185,173))',
'linear-gradient(40deg, #427E41 30%, rgb(97,185,96))',
'linear-gradient(40deg, #89A568 30%, rgb(176,212,133))',
'linear-gradient(40deg, #90924D 30%, rgb(187,189,98))',
'linear-gradient(40deg, #E2B032 30%, rgb(245,203,99))',
'linear-gradient(40deg, #C97358 30%, rgb(245,148,118))',
'linear-gradient(40deg, #6D5B54 30%, rgb(152,121,108))',
'linear-gradient(40deg, #6B7980 30%, rgb(134,163,177))',
];
let hash = 0;
if (userName.length === 0) {
return colors[hash];
}
for (let i = 0; i < userName.length; i++) {
// tslint:disable-next-line: no-bitwise
hash = userName.charCodeAt(i) + ((hash << 5) - hash);
// tslint:disable-next-line: no-bitwise
hash = hash & hash;
}
hash = ((hash % colors.length) + colors.length) % colors.length;
return colors[hash];
} }
}
getInitials(fromName: string): string {
const username = fromName.split('@')[0];
let separator = '_';
if (username.includes('-')) {
separator = '-';
}
if (username.includes('.')) {
separator = '.';
}
const split = username.split(separator);
const initials = split[0].charAt(0) + (split[1] ? split[1].charAt(0) : '');
return initials;
}
getColor(userName: string): string {
const colors = [
'linear-gradient(40deg, #B44D51 30%, rgb(241,138,138))',
'linear-gradient(40deg, #B75073 30%, rgb(234,96,143))',
'linear-gradient(40deg, #84498E 30%, rgb(214,116,230))',
'linear-gradient(40deg, #705998 30%, rgb(163,131,220))',
'linear-gradient(40deg, #5C6598 30%, rgb(135,148,222))',
'linear-gradient(40deg, #7F90D3 30%, rgb(181,196,247))',
'linear-gradient(40deg, #3E93B9 30%, rgb(150,215,245))',
'linear-gradient(40deg, #3494A0 30%, rgb(71,205,222))',
'linear-gradient(40deg, #25716A 30%, rgb(58,185,173))',
'linear-gradient(40deg, #427E41 30%, rgb(97,185,96))',
'linear-gradient(40deg, #89A568 30%, rgb(176,212,133))',
'linear-gradient(40deg, #90924D 30%, rgb(187,189,98))',
'linear-gradient(40deg, #E2B032 30%, rgb(245,203,99))',
'linear-gradient(40deg, #C97358 30%, rgb(245,148,118))',
'linear-gradient(40deg, #6D5B54 30%, rgb(152,121,108))',
'linear-gradient(40deg, #6B7980 30%, rgb(134,163,177))',
];
let hash = 0;
if (userName.length === 0) {
return colors[hash];
}
hash = this.hashCode(userName);
return colors[hash % colors.length];
}
// tslint:disable
private hashCode(str: string, seed: number = 0): number {
let h1 = 0xdeadbeef ^ seed, h2 = 0x41c6ce57 ^ seed;
for (let i = 0, ch; i < str.length; i++) {
ch = str.charCodeAt(i);
h1 = Math.imul(h1 ^ ch, 2654435761);
h2 = Math.imul(h2 ^ ch, 1597334677);
}
h1 = Math.imul(h1 ^ (h1 >>> 16), 2246822507) ^ Math.imul(h2 ^ (h2 >>> 13), 3266489909);
h2 = Math.imul(h2 ^ (h2 >>> 16), 2246822507) ^ Math.imul(h1 ^ (h1 >>> 13), 3266489909);
return 4294967296 * (2097151 & h2) + (h1 >>> 0);
}
// tslint:enable
} }

View File

@@ -13,7 +13,7 @@
<div class="row"> <div class="row">
<app-avatar matTooltip="{{ dayelement.editorDisplayName }}" <app-avatar matTooltip="{{ dayelement.editorDisplayName }}"
*ngIf="dayelement.editorDisplayName; else spacer" class="avatar" *ngIf="dayelement.editorDisplayName; else spacer" class="avatar"
[name]="dayelement.editorDisplayName" [size]="32"> [name]="dayelement.editorDisplayName" [size]="32" [forColor]="dayelement?.preferredLoginName">
</app-avatar> </app-avatar>
<ng-template #spacer> <ng-template #spacer>
<div class="spacer"></div> <div class="spacer"></div>

View File

@@ -6,254 +6,256 @@ import { catchError, debounceTime, scan, take, takeUntil, tap } from 'rxjs/opera
import { ListMyUserChangesResponse } from 'src/app/proto/generated/zitadel/auth_pb'; import { ListMyUserChangesResponse } from 'src/app/proto/generated/zitadel/auth_pb';
import { Change } from 'src/app/proto/generated/zitadel/change_pb'; import { Change } from 'src/app/proto/generated/zitadel/change_pb';
import { import {
ListAppChangesResponse, ListAppChangesResponse,
ListOrgChangesResponse, ListOrgChangesResponse,
ListProjectChangesResponse, ListProjectChangesResponse,
ListUserChangesResponse, ListUserChangesResponse,
} from 'src/app/proto/generated/zitadel/management_pb'; } from 'src/app/proto/generated/zitadel/management_pb';
import { GrpcAuthService } from 'src/app/services/grpc-auth.service'; 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';
export enum ChangeType { export enum ChangeType {
MYUSER = 'myuser', MYUSER = 'myuser',
USER = 'user', USER = 'user',
ORG = 'org', ORG = 'org',
PROJECT = 'project', PROJECT = 'project',
APP = 'app', APP = 'app',
} }
export interface MappedChange { export interface MappedChange {
key: string; key: string;
values: Array<{ values: Array<{
data: any[]; data: any[];
dates: Timestamp.AsObject[]; dates: Timestamp.AsObject[];
editorId: string; editorId: string;
editorName: string; editorName: string;
eventTypes: Array<{ key: string; localizedMessage: string; }>; eventTypes: Array<{ key: string; localizedMessage: string; }>;
sequences: number[]; sequences: number[];
}>; }>;
} }
type ListChanges = ListMyUserChangesResponse.AsObject | type ListChanges = ListMyUserChangesResponse.AsObject |
ListUserChangesResponse.AsObject | ListUserChangesResponse.AsObject |
ListProjectChangesResponse.AsObject | ListProjectChangesResponse.AsObject |
ListOrgChangesResponse.AsObject | ListOrgChangesResponse.AsObject |
ListAppChangesResponse.AsObject; ListAppChangesResponse.AsObject;
@Component({ @Component({
selector: 'app-changes', selector: 'app-changes',
templateUrl: './changes.component.html', templateUrl: './changes.component.html',
styleUrls: ['./changes.component.scss'], styleUrls: ['./changes.component.scss'],
}) })
export class ChangesComponent implements OnInit, OnDestroy { export class ChangesComponent implements OnInit, OnDestroy {
@Input() public changeType: ChangeType = ChangeType.USER; @Input() public changeType: ChangeType = ChangeType.USER;
@Input() public id: string = ''; @Input() public id: string = '';
@Input() public secId: string = ''; @Input() public secId: string = '';
@Input() public sortDirectionAsc: boolean = true; @Input() public sortDirectionAsc: boolean = true;
@Input() public refresh!: Observable<void>; @Input() public refresh!: Observable<void>;
public bottom: boolean = false; public bottom: boolean = false;
private _done: BehaviorSubject<any> = new BehaviorSubject(false); private _done: BehaviorSubject<any> = new BehaviorSubject(false);
private _loading: BehaviorSubject<any> = new BehaviorSubject(false); private _loading: BehaviorSubject<any> = new BehaviorSubject(false);
private _data: BehaviorSubject<any> = new BehaviorSubject([]); private _data: BehaviorSubject<any> = new BehaviorSubject([]);
loading: Observable<boolean> = this._loading.asObservable(); loading: Observable<boolean> = this._loading.asObservable();
public data!: Observable<MappedChange[]>; public data!: Observable<MappedChange[]>;
public changes!: ListChanges; public changes!: ListChanges;
private destroyed$: Subject<void> = new Subject(); private destroyed$: Subject<void> = new Subject();
constructor(private mgmtUserService: ManagementService, private authUserService: GrpcAuthService) { constructor(private mgmtUserService: ManagementService, private authUserService: GrpcAuthService) {
} }
ngOnInit(): void { ngOnInit(): void {
this.init();
if (this.refresh) {
this.refresh.pipe(takeUntil(this.destroyed$), debounceTime(2000)).subscribe(() => {
this.init(); this.init();
if (this.refresh) { });
this.refresh.pipe(takeUntil(this.destroyed$), debounceTime(2000)).subscribe(() => { }
this.init(); }
});
} ngOnDestroy(): void {
this.destroyed$.next();
}
public scrollHandler(e: any): void {
if (e === 'bottom') {
this.more();
}
}
public init(): void {
let first: Promise<ListChanges>;
switch (this.changeType) {
case ChangeType.MYUSER: first = this.authUserService.listMyUserChanges(20, 0);
break;
case ChangeType.USER: first = this.mgmtUserService.listUserChanges(this.id, 20, 0);
break;
case ChangeType.PROJECT: first = this.mgmtUserService.listProjectChanges(this.id, 20, 0);
break;
case ChangeType.ORG: first = this.mgmtUserService.listOrgChanges(20, 0);
break;
case ChangeType.APP: first = this.mgmtUserService.listAppChanges(this.id, this.secId, 20, 0);
break;
} }
ngOnDestroy(): void { this.mapAndUpdate(first);
this.destroyed$.next();
// Create the observable array for consumption in components
this.data = this._data.asObservable().pipe(
scan((acc, val) => {
return false ? val.concat(acc) : acc.concat(val);
}));
}
private more(): void {
const cursor = this.getCursor();
let more: Promise<ListChanges>;
switch (this.changeType) {
case ChangeType.MYUSER: more = this.authUserService.listMyUserChanges(20, cursor);
break;
case ChangeType.USER: more = this.mgmtUserService.listUserChanges(this.id, 20, cursor);
break;
case ChangeType.PROJECT: more = this.mgmtUserService.listProjectChanges(this.id, 20, cursor);
break;
case ChangeType.ORG: more = this.mgmtUserService.listOrgChanges(20, cursor);
break;
case ChangeType.APP: more = this.mgmtUserService.listAppChanges(this.id, this.secId, 20, cursor);
break;
} }
public scrollHandler(e: any): void { this.mapAndUpdate(more);
if (e === 'bottom') { }
this.more();
} // Determines the snapshot to paginate query
private getCursor(): number {
const current = this._data.value;
if (current.length) {
const lastElementValues = current[current.length - 1].values;
const seq = lastElementValues[lastElementValues.length - 1].sequences;
return seq[seq.length - 1];
} }
return 0;
}
public init(): void { // Maps the snapshot to usable format the updates source
let first: Promise<ListChanges>; private mapAndUpdate(col: Promise<ListChanges>): any {
switch (this.changeType) { if (this._done.value || this._loading.value) { return; }
case ChangeType.MYUSER: first = this.authUserService.listMyUserChanges(20, 0);
break;
case ChangeType.USER: first = this.mgmtUserService.listUserChanges(this.id, 20, 0);
break;
case ChangeType.PROJECT: first = this.mgmtUserService.listProjectChanges(this.id, 20, 0);
break;
case ChangeType.ORG: first = this.mgmtUserService.listOrgChanges(20, 0);
break;
case ChangeType.APP: first = this.mgmtUserService.listAppChanges(this.id, this.secId, 20, 0);
break;
}
this.mapAndUpdate(first); // Map snapshot with doc ref (needed for cursor)
if (!this.bottom) {
// loading
this._loading.next(true);
// Create the observable array for consumption in components return from(col).pipe(
this.data = this._data.asObservable().pipe( take(1),
scan((acc, val) => { tap((res: ListChanges) => {
return false ? val.concat(acc) : acc.concat(val); const values = res.resultList;
})); const mapped = this.mapChanges(values);
// update source with new values, done loading
// this._data.next(values);
this._data.next(mapped);
this._loading.next(false);
// no more values, mark done
if (!values.length) {
this._done.next(true);
}
}),
catchError(_ => {
this._loading.next(false);
this.bottom = true;
return of([]);
}),
).subscribe();
} }
}
private more(): void { private mapChanges(changes: Change.AsObject[]): {
const cursor = this.getCursor(); key: string; values: any[];
}[] {
const splitted: { [editorId: string]: any[]; } = {};
changes.forEach((change) => {
if (change.changeDate) {
const index = `${this.getDateString(change.changeDate)}`;
// `${this.getDateString(change.changeDate)}:${change.editorId}`;
let more: Promise<ListChanges>; if (index) {
if (splitted[index]) {
const userData: any = {
editor: change.editorDisplayName,
editorId: change.editorId,
editorDisplayName: change.editorDisplayName,
switch (this.changeType) { dates: [change.changeDate],
case ChangeType.MYUSER: more = this.authUserService.listMyUserChanges(20, cursor); // data: [change.data],
break; eventTypes: [change.eventType],
case ChangeType.USER: more = this.mgmtUserService.listUserChanges(this.id, 20, cursor); sequences: [change.sequence],
break; };
case ChangeType.PROJECT: more = this.mgmtUserService.listProjectChanges(this.id, 20, cursor); const lastIndex = splitted[index].length - 1;
break; if (lastIndex > -1 && splitted[index][lastIndex].editor === change.editorDisplayName) {
case ChangeType.ORG: more = this.mgmtUserService.listOrgChanges(20, cursor); splitted[index][lastIndex].dates.push(change.changeDate);
break; // splitted[index][lastIndex].data.push(change.data);
case ChangeType.APP: more = this.mgmtUserService.listAppChanges(this.id, this.secId, 20, cursor); splitted[index][lastIndex].eventTypes.push(change.eventType);
break; splitted[index][lastIndex].sequences.push(change.sequence);
} } else {
splitted[index].push(userData);
this.mapAndUpdate(more);
}
// Determines the snapshot to paginate query
private getCursor(): number {
const current = this._data.value;
if (current.length) {
const lastElementValues = current[current.length - 1].values;
const seq = lastElementValues[lastElementValues.length - 1].sequences;
return seq[seq.length - 1];
}
return 0;
}
// Maps the snapshot to usable format the updates source
private mapAndUpdate(col: Promise<ListChanges>): any {
if (this._done.value || this._loading.value) { return; }
// Map snapshot with doc ref (needed for cursor)
if (!this.bottom) {
// loading
this._loading.next(true);
return from(col).pipe(
take(1),
tap((res: ListChanges) => {
const values = res.resultList;
const mapped = this.mapChanges(values);
// update source with new values, done loading
// this._data.next(values);
this._data.next(mapped);
this._loading.next(false);
// no more values, mark done
if (!values.length) {
this._done.next(true);
}
}),
catchError(_ => {
this._loading.next(false);
this.bottom = true;
return of([]);
}),
).subscribe();
}
}
private mapChanges(changes: Change.AsObject[]): {
key: string; values: any[];
}[] {
const splitted: { [editorId: string]: any[]; } = {};
changes.forEach((change) => {
if (change.changeDate) {
const index = `${this.getDateString(change.changeDate)}`;
// `${this.getDateString(change.changeDate)}:${change.editorId}`;
if (index) {
if (splitted[index]) {
const userData: any = {
editor: change.editorDisplayName,
editorId: change.editorId,
editorDisplayName: change.editorDisplayName,
dates: [change.changeDate],
// data: [change.data],
eventTypes: [change.eventType],
sequences: [change.sequence],
};
const lastIndex = splitted[index].length - 1;
if (lastIndex > -1 && splitted[index][lastIndex].editor === change.editorDisplayName) {
splitted[index][lastIndex].dates.push(change.changeDate);
// splitted[index][lastIndex].data.push(change.data);
splitted[index][lastIndex].eventTypes.push(change.eventType);
splitted[index][lastIndex].sequences.push(change.sequence);
} else {
splitted[index].push(userData);
}
} else {
splitted[index] = [
{
editor: change.editorDisplayName,
editorId: change.editorId,
editorDisplayName: change.editorDisplayName,
dates: [change.changeDate],
// data: [change.data],
eventTypes: [change.eventType],
sequences: [change.sequence],
},
];
}
}
} }
}); } else {
const arr = Object.keys(splitted).map(key => { splitted[index] = [
return { key: key, values: splitted[key] }; {
}); editor: change.editorDisplayName,
editorId: change.editorId,
editorDisplayName: change.editorDisplayName,
arr.sort((a, b) => { dates: [change.changeDate],
return parseFloat(b.key) - parseFloat(a.key); // data: [change.data],
}); eventTypes: [change.eventType],
sequences: [change.sequence],
},
];
}
}
}
});
const arr = Object.keys(splitted).map(key => {
return { key: key, values: splitted[key] };
});
return arr; arr.sort((a, b) => {
} return parseFloat(b.key) - parseFloat(a.key);
});
getDateString(ts: Timestamp.AsObject): string { return arr;
const date = new Date(ts.seconds * 1000 + ts.nanos / 1000 / 1000); }
return date.getUTCFullYear() + this.pad(date.getUTCMonth() + 1) + this.pad(date.getUTCDate());
}
getTimestampIndex(date: any): number { getDateString(ts: Timestamp.AsObject): string {
const ts: Date = new Date(date.seconds * 1000 + date.nanos / 1000 / 1000); const date = new Date(ts.seconds * 1000 + ts.nanos / 1000 / 1000);
return ts.getTime(); return date.getUTCFullYear() + this.pad(date.getUTCMonth() + 1) + this.pad(date.getUTCDate());
} }
pad(n: number): string { getTimestampIndex(date: any): number {
return n < 10 ? '0' + n : n.toString(); const ts: Date = new Date(date.seconds * 1000 + date.nanos / 1000 / 1000);
} return ts.getTime();
}
// Order by ascending property value pad(n: number): string {
valueAscOrder = (a: KeyValue<number, string>, b: KeyValue<number, string>): number => { return n < 10 ? '0' + n : n.toString();
return a.value.localeCompare(b.value); }
}
// Order by descending property key // Order by ascending property value
keyDescOrder = (a: KeyValue<number, string>, b: KeyValue<number, string>): number => { // tslint:disable
return a.key > b.key ? -1 : (b.key > a.key ? 1 : 0); valueAscOrder = (a: KeyValue<number, string>, b: KeyValue<number, string>): number => {
} return a.value.localeCompare(b.value);
};
// Order by descending property key
keyDescOrder = (a: KeyValue<number, string>, b: KeyValue<number, string>): number => {
return a.key > b.key ? -1 : (b.key > a.key ? 1 : 0);
};
// tslint:enable
} }

View File

@@ -14,7 +14,7 @@
[ngStyle]="{'z-index': 100 - i}"> [ngStyle]="{'z-index': 100 - i}">
<app-avatar <app-avatar
*ngIf="member && member.displayName && member.firstName && member.lastName; else cog" *ngIf="member && member.displayName && member.firstName && member.lastName; else cog"
class="avatar dontcloseonclick" class="avatar dontcloseonclick" [forColor]="member?.userName"[forColor]="member?.preferredLoginName"
[name]="member.displayName ? member.displayName : (member.firstName + ' '+ member.lastName)" [name]="member.displayName ? member.displayName : (member.firstName + ' '+ member.lastName)"
[size]="32"> [size]="32">
</app-avatar> </app-avatar>

View File

@@ -22,7 +22,7 @@
<mat-checkbox [disabled]="!canWrite" color="primary" (click)="$event.stopPropagation()" <mat-checkbox [disabled]="!canWrite" color="primary" (click)="$event.stopPropagation()"
(change)="$event ? selection.toggle(row) : null" [checked]="selection.isSelected(row)"> (change)="$event ? selection.toggle(row) : null" [checked]="selection.isSelected(row)">
<app-avatar *ngIf="row?.displayName && row.firstName && row.lastName; else cog" class="avatar" <app-avatar *ngIf="row?.displayName && row.firstName && row.lastName; else cog" class="avatar"
[name]="row.displayName" [size]="32"> [name]="row.displayName" [forColor]="row?.preferredLoginName" [size]="32">
</app-avatar> </app-avatar>
<ng-template #cog> <ng-template #cog>
<div class="sa-icon"> <div class="sa-icon">

View File

@@ -24,7 +24,7 @@
<div class="content" *ngIf="loginData"> <div class="content" *ngIf="loginData">
<div class="row"> <div class="row">
<mat-slide-toggle class="toggle" color="primary" [disabled]="disabled" ngDefaultControl <mat-slide-toggle class="toggle" color="primary" [disabled]="disabled || serviceType == PolicyComponentServiceType.MGMT && (['login_policy.username_login'] | hasFeature | async) == false" ngDefaultControl
[(ngModel)]="loginData.allowUsernamePassword"> [(ngModel)]="loginData.allowUsernamePassword">
{{'POLICY.DATA.ALLOWUSERNAMEPASSWORD' | translate}} {{'POLICY.DATA.ALLOWUSERNAMEPASSWORD' | translate}}
</mat-slide-toggle> </mat-slide-toggle>
@@ -62,7 +62,7 @@
</ng-template> </ng-template>
</div> </div>
<div class="row"> <div class="row">
<mat-slide-toggle class="toggle" color="primary" [disabled]="disabled" ngDefaultControl <mat-slide-toggle class="toggle" color="primary" [disabled]="disabled || serviceType == PolicyComponentServiceType.MGMT && (['login_policy.idp'] | hasFeature | async) == false" ngDefaultControl
[(ngModel)]="loginData.allowExternalIdp"> [(ngModel)]="loginData.allowExternalIdp">
{{'POLICY.DATA.ALLOWEXTERNALIDP' | translate}} {{'POLICY.DATA.ALLOWEXTERNALIDP' | translate}}
</mat-slide-toggle> </mat-slide-toggle>
@@ -79,7 +79,7 @@
</ng-template> </ng-template>
</div> </div>
<div class="row"> <div class="row">
<mat-slide-toggle class="toggle" color="primary" [disabled]="disabled" ngDefaultControl <mat-slide-toggle class="toggle" color="primary" [disabled]="disabled || serviceType == PolicyComponentServiceType.MGMT && (['login_policy.factors'] | hasFeature | async) == false" ngDefaultControl
[(ngModel)]="loginData.forceMfa"> [(ngModel)]="loginData.forceMfa">
{{'POLICY.DATA.FORCEMFA' | translate}} {{'POLICY.DATA.FORCEMFA' | translate}}
</mat-slide-toggle> </mat-slide-toggle>
@@ -96,7 +96,7 @@
</ng-template> </ng-template>
</div> </div>
<div class="row"> <div class="row">
<mat-slide-toggle class="toggle" color="primary" [disabled]="disabled" ngDefaultControl <mat-slide-toggle class="toggle" color="primary" [disabled]="disabled || serviceType == PolicyComponentServiceType.MGMT && (['login_policy.password_reset'] | hasFeature | async) == false" ngDefaultControl
[(ngModel)]="loginData.hidePasswordReset"> [(ngModel)]="loginData.hidePasswordReset">
{{'POLICY.DATA.HIDEPASSWORDRESET' | translate}} {{'POLICY.DATA.HIDEPASSWORDRESET' | translate}}
</mat-slide-toggle> </mat-slide-toggle>

View File

@@ -98,7 +98,7 @@ export class LoginPolicyComponent implements OnDestroy {
if (resp.policy) { if (resp.policy) {
this.loginData = resp.policy; this.loginData = resp.policy;
this.loading = false; this.loading = false;
this.disabled = ((this.loginData as LoginPolicy.AsObject)?.isDefault) ?? false; this.disabled = this.isDefault;
} }
}); });
this.getIdps().then(resp => { this.getIdps().then(resp => {

View File

@@ -72,7 +72,6 @@
</div> </div>
<span class="fill-space"></span> <span class="fill-space"></span>
<div class="img-wrapper" *ngIf="images['darkLogo']"> <div class="img-wrapper" *ngIf="images['darkLogo']">
<!-- <mat-icon matTooltip="{{'ACTIONS.DELETE' | translate}}" color="warn" class="dl-btn" (click)="deleteAsset(AssetType.LOGO, theme, Preview.PREVIEW)">remove_circle</mat-icon> -->
<img matTooltip="Current" class="curr" [src]="images['darkLogo']" alt="dark logo"/> <img matTooltip="Current" class="curr" [src]="images['darkLogo']" alt="dark logo"/>
</div> </div>
</div> </div>
@@ -83,7 +82,6 @@
</div> </div>
<span class="fill-space"></span> <span class="fill-space"></span>
<div class="img-wrapper" *ngIf="images['logo']"> <div class="img-wrapper" *ngIf="images['logo']">
<!-- <mat-icon matTooltip="{{'ACTIONS.DELETE' | translate}}" color="warn" class="dl-btn" (click)="deleteAsset(AssetType.LOGO, theme, Preview.PREVIEW)">remove_circle</mat-icon> -->
<img matTooltip="Current" class="curr" [src]="images['logo']" alt="logo"/> <img matTooltip="Current" class="curr" [src]="images['logo']" alt="logo"/>
</div> </div>
</div> </div>
@@ -93,10 +91,10 @@
[class.hovering]="isHoveringOverDarkLogo"> [class.hovering]="isHoveringOverDarkLogo">
<label class="file-label"> <label class="file-label">
<input #selectedFile style="display: none;" class="file-input" type="file" (change)="onDropLogo(theme, $event.target.files)"> <input #selectedFile style="display: none;" class="file-input" type="file" (change)="onDropLogo(theme, $event.target.files)">
<input class="btn" mat-raised-button type="button" [value]="'POLICY.PRIVATELABELING.BTN' | translate" (click)="selectedFile.click();" /> <button mat-stroked-button class="btn" [disabled]="serviceType == PolicyComponentServiceType.MGMT && (['label_policy.private_label'] | hasFeature | async) == false" mat-raised-button type="button" (click)="selectedFile.click();">{{'POLICY.PRIVATELABELING.BTN' | translate}}</button>
<i class="icon las la-cloud-upload-alt"></i> <i class="icon las la-cloud-upload-alt"></i>
<span>{{isHoveringOverDarkLogo ? 'Release': 'Drop your Logo here'}}</span> <span>{{isHoveringOverDarkLogo ? ('POLICY.PRIVATELABELING.RELEASE' | translate): ('POLICY.PRIVATELABELING.DROP' | translate)}}</span>
</label> </label>
</div> </div>
</div> </div>
@@ -133,10 +131,10 @@
[class.hovering]="isHoveringOverDarkIcon"> [class.hovering]="isHoveringOverDarkIcon">
<label class="file-label"> <label class="file-label">
<input #selectedFileIcon style="display: none;" class="file-input" type="file" (change)="onDropIcon(theme, $event.target.files)"> <input #selectedFileIcon style="display: none;" class="file-input" type="file" (change)="onDropIcon(theme, $event.target.files)">
<input class="btn" mat-raised-button type="button" [value]="'POLICY.PRIVATELABELING.BTN' | translate" (click)="selectedFileIcon.click();" /> <button mat-stroked-button class="btn" [disabled]="serviceType == PolicyComponentServiceType.MGMT && (['label_policy.private_label'] | hasFeature | async) == false" mat-raised-button type="button" (click)="selectedFileIcon.click();">{{'POLICY.PRIVATELABELING.BTN' | translate}}</button>
<i class="icon las la-cloud-upload-alt"></i> <i class="icon las la-cloud-upload-alt"></i>
<span>{{isHoveringOverDarkIcon ? 'Release': 'Drop your Logo here'}}</span> <span>{{isHoveringOverDarkIcon ? ('POLICY.PRIVATELABELING.RELEASE' | translate): ('POLICY.PRIVATELABELING.DROP' | translate)}}</span>
</label> </label>
</div> </div>
</div> </div>
@@ -158,19 +156,19 @@
<ng-container *ngIf="theme==Theme.DARK"> <ng-container *ngIf="theme==Theme.DARK">
<div class="colors" *ngIf="data && previewData"> <div class="colors" *ngIf="data && previewData">
<div class="color"> <div class="color">
<cnsl-color [colorType]="ColorType.BACKGROUNDDARK" (previewChanged)="previewData.backgroundColorDark = $event" name="Background Color Dark" [color]="data.backgroundColorDark" [previewColor]="previewData.backgroundColorDark"></cnsl-color> <cnsl-color [colorType]="ColorType.BACKGROUNDDARK" (previewChanged)="previewData.backgroundColorDark = $event" name="Background Color" [color]="data.backgroundColorDark" [previewColor]="previewData.backgroundColorDark"></cnsl-color>
</div> </div>
<div class="color"> <div class="color">
<cnsl-color [colorType]="ColorType.PRIMARY"(previewChanged)="previewData.primaryColorDark = $event" name="Preview Primary Color Dark" [color]="data.primaryColorDark" [previewColor]="previewData.primaryColorDark"></cnsl-color> <cnsl-color [colorType]="ColorType.PRIMARY"(previewChanged)="previewData.primaryColorDark = $event" name="Primary Color" [color]="data.primaryColorDark" [previewColor]="previewData.primaryColorDark"></cnsl-color>
</div> </div>
<div class="color"> <div class="color">
<cnsl-color [colorType]="ColorType.WARN" (previewChanged)="previewData.warnColorDark = $event" name="Preview Warn Color Dark" [color]="data.warnColorDark" [previewColor]="previewData.warnColorDark"></cnsl-color> <cnsl-color [colorType]="ColorType.WARN" (previewChanged)="previewData.warnColorDark = $event" name="Warn Color" [color]="data.warnColorDark" [previewColor]="previewData.warnColorDark"></cnsl-color>
</div> </div>
<div class="color"> <div class="color">
<cnsl-color [colorType]="ColorType.FONTDARK"(previewChanged)="previewData.fontColorDark = $event" name="Font Color Dark" [color]="data.fontColorDark" [previewColor]="previewData.fontColorDark"></cnsl-color> <cnsl-color [colorType]="ColorType.FONTDARK"(previewChanged)="previewData.fontColorDark = $event" name="Font Color" [color]="data.fontColorDark" [previewColor]="previewData.fontColorDark"></cnsl-color>
</div> </div>
</div> </div>
</ng-container> </ng-container>
@@ -178,15 +176,15 @@
<ng-container *ngIf="theme==Theme.LIGHT"> <ng-container *ngIf="theme==Theme.LIGHT">
<div class="colors" *ngIf="data && previewData"> <div class="colors" *ngIf="data && previewData">
<div class="color"> <div class="color">
<cnsl-color [colorType]="ColorType.BACKGROUNDLIGHT" (previewChanged)="previewData.backgroundColor = $event" name="Background Color Light" [color]="data.backgroundColor" [previewColor]="previewData.backgroundColor"></cnsl-color> <cnsl-color [colorType]="ColorType.BACKGROUNDLIGHT" (previewChanged)="previewData.backgroundColor = $event" name="Background Color" [color]="data.backgroundColor" [previewColor]="previewData.backgroundColor"></cnsl-color>
</div> </div>
<div class="color"> <div class="color">
<cnsl-color [colorType]="ColorType.PRIMARY" (previewChanged)="previewData.primaryColor = $event" name="Preview Primary Color Light" [color]="data.primaryColor" [previewColor]="previewData.primaryColor"></cnsl-color> <cnsl-color [colorType]="ColorType.PRIMARY" (previewChanged)="previewData.primaryColor = $event" name="Primary Color" [color]="data.primaryColor" [previewColor]="previewData.primaryColor"></cnsl-color>
</div> </div>
<div class="color"> <div class="color">
<cnsl-color [colorType]="ColorType.WARN" name="Preview Warn Color Light" (previewChanged)="previewData.warnColor= $event" [color]="data.warnColor" [previewColor]="previewData.warnColor"></cnsl-color> <cnsl-color [colorType]="ColorType.WARN" name="Warn Color" (previewChanged)="previewData.warnColor= $event" [color]="data.warnColor" [previewColor]="previewData.warnColor"></cnsl-color>
</div> </div>
<div class="color"> <div class="color">
@@ -214,7 +212,7 @@
<span>ABC • abc • 123</span> <span>ABC • abc • 123</span>
<span class="fill-space"></span> <span class="fill-space"></span>
<button matTooltip="{{'ACTIONS.REMOVE' | translate}}" mat-icon-button color="warn" (click)="deleteFont()"><mat-icon>remove_circle</mat-icon></button> <button [disabled]="serviceType == PolicyComponentServiceType.MGMT && (['label_policy.private_label'] | hasFeature | async) == false" matTooltip="{{'ACTIONS.REMOVE' | translate}}" mat-icon-button color="warn" (click)="deleteFont()"><mat-icon>remove_circle</mat-icon></button>
</div> </div>
<div class="dropzone" cnslDropzone (hovered)="toggleHoverFont($event)" <div class="dropzone" cnslDropzone (hovered)="toggleHoverFont($event)"
@@ -222,10 +220,10 @@
[class.hovering]="isHoveringOverFont"> [class.hovering]="isHoveringOverFont">
<label class="file-label"> <label class="file-label">
<input #selectedFontFile style="display: none;" class="file-input" type="file" (change)="onDropFont($event.target.files)"> <input #selectedFontFile style="display: none;" class="file-input" type="file" (change)="onDropFont($event.target.files)">
<input class="btn" mat-raised-button type="button" [value]="'POLICY.PRIVATELABELING.BTN' | translate" (click)="selectedFontFile.click();" /> <button mat-stroked-button class="btn" [disabled]="serviceType == PolicyComponentServiceType.MGMT && (['label_policy.private_label'] | hasFeature | async) == false" mat-raised-button type="button" (click)="selectedFontFile.click();">{{'POLICY.PRIVATELABELING.BTN' | translate}}</button>
<i class="icon las la-cloud-upload-alt"></i> <i class="icon las la-cloud-upload-alt"></i>
<span >{{isHoveringOverFont ? 'Release': 'Drop your Logo here'}}</span> <span >{{isHoveringOverFont ? ('POLICY.PRIVATELABELING.RELEASE' | translate): ('POLICY.PRIVATELABELING.DROP' | translate)}}</span>
</label> </label>
</div> </div>
</div> </div>

View File

@@ -203,14 +203,20 @@
align-items: center; align-items: center;
.btn { .btn {
cursor: pointer;
border-radius: 6px;
padding: .5rem 1rem;
background-color: inherit;
border: 1px solid if($is-dark-theme, #ffffff20, #000);
color: if($is-dark-theme, white, #000);
margin-bottom: .5rem; margin-bottom: .5rem;
} }
.btn:not[disabled] {
cursor: pointer;
}
i {
font-size: 2.5rem;
}
span {
color: var(--grey);
}
} }
.icon { .icon {
@@ -274,6 +280,7 @@
max-width: 120px; max-width: 120px;
.dl-btn { .dl-btn {
z-index: 2;
position: absolute; position: absolute;
top: -12px; top: -12px;
left: -12px; left: -12px;

View File

@@ -3,7 +3,7 @@ import { Component, EventEmitter, Injector, OnDestroy, Type } from '@angular/cor
import { DomSanitizer } from '@angular/platform-browser'; import { DomSanitizer } from '@angular/platform-browser';
import { ActivatedRoute } from '@angular/router'; import { ActivatedRoute } from '@angular/router';
import { Subscription } from 'rxjs'; import { Subscription } from 'rxjs';
import { switchMap } from 'rxjs/operators'; import { switchMap, take } from 'rxjs/operators';
import { import {
GetLabelPolicyResponse as AdminGetLabelPolicyResponse, GetLabelPolicyResponse as AdminGetLabelPolicyResponse,
GetPreviewLabelPolicyResponse as AdminGetPreviewLabelPolicyResponse, GetPreviewLabelPolicyResponse as AdminGetPreviewLabelPolicyResponse,
@@ -15,10 +15,13 @@ import {
GetPreviewLabelPolicyResponse as MgmtGetPreviewLabelPolicyResponse, GetPreviewLabelPolicyResponse as MgmtGetPreviewLabelPolicyResponse,
UpdateCustomLabelPolicyRequest, UpdateCustomLabelPolicyRequest,
} from 'src/app/proto/generated/zitadel/management_pb'; } from 'src/app/proto/generated/zitadel/management_pb';
import { Org } from 'src/app/proto/generated/zitadel/org_pb';
import { LabelPolicy } from 'src/app/proto/generated/zitadel/policy_pb'; import { LabelPolicy } from 'src/app/proto/generated/zitadel/policy_pb';
import { AdminService } from 'src/app/services/admin.service'; import { AdminService } from 'src/app/services/admin.service';
import { AssetEndpoint, AssetService, AssetType } from 'src/app/services/asset.service'; import { AssetEndpoint, AssetService, AssetType } from 'src/app/services/asset.service';
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 { StorageService } from 'src/app/services/storage.service';
import { ToastService } from 'src/app/services/toast.service'; import { ToastService } from 'src/app/services/toast.service';
import { CnslLinks } from '../../links/links.component'; import { CnslLinks } from '../../links/links.component';
@@ -45,6 +48,8 @@ export enum ColorType {
BACKGROUNDLIGHT, BACKGROUNDLIGHT,
} }
const ORG_STORAGE_KEY = 'organization';
@Component({ @Component({
selector: 'app-private-labeling-policy', selector: 'app-private-labeling-policy',
templateUrl: './private-labeling-policy.component.html', templateUrl: './private-labeling-policy.component.html',
@@ -86,14 +91,23 @@ export class PrivateLabelingPolicyComponent implements OnDestroy {
public refreshPreview: EventEmitter<void> = new EventEmitter(); public refreshPreview: EventEmitter<void> = new EventEmitter();
public loadingImages: boolean = false; public loadingImages: boolean = false;
private org!: Org.AsObject;
constructor( constructor(
private authService: GrpcAuthService,
private route: ActivatedRoute, private route: ActivatedRoute,
private toast: ToastService, private toast: ToastService,
private injector: Injector, private injector: Injector,
private assetService: AssetService, private assetService: AssetService,
private sanitizer: DomSanitizer, private sanitizer: DomSanitizer,
private storageService: StorageService,
) { ) {
const org: Org.AsObject | null = (this.storageService.getItem(ORG_STORAGE_KEY));
if (org) {
this.org = org;
}
this.sub = this.route.data.pipe(switchMap(data => { this.sub = this.route.data.pipe(switchMap(data => {
this.serviceType = data.serviceType; this.serviceType = data.serviceType;
@@ -134,17 +148,17 @@ export class PrivateLabelingPolicyComponent implements OnDestroy {
if (theme === Theme.DARK) { if (theme === Theme.DARK) {
switch (this.serviceType) { switch (this.serviceType) {
case PolicyComponentServiceType.MGMT: case PolicyComponentServiceType.MGMT:
return this.handleUploadPromise(this.assetService.upload(AssetEndpoint.MGMTDARKLOGO, formData)); return this.handleUploadPromise(this.assetService.upload(AssetEndpoint.MGMTDARKLOGO, formData, this.org.id));
case PolicyComponentServiceType.ADMIN: case PolicyComponentServiceType.ADMIN:
return this.handleUploadPromise(this.assetService.upload(AssetEndpoint.IAMDARKLOGO, formData)); return this.handleUploadPromise(this.assetService.upload(AssetEndpoint.IAMDARKLOGO, formData, this.org.id));
} }
} }
if (theme === Theme.LIGHT) { if (theme === Theme.LIGHT) {
switch (this.serviceType) { switch (this.serviceType) {
case PolicyComponentServiceType.MGMT: case PolicyComponentServiceType.MGMT:
return this.handleUploadPromise(this.assetService.upload(AssetEndpoint.MGMTLOGO, formData)); return this.handleUploadPromise(this.assetService.upload(AssetEndpoint.MGMTLOGO, formData, this.org.id));
case PolicyComponentServiceType.ADMIN: case PolicyComponentServiceType.ADMIN:
return this.handleUploadPromise(this.assetService.upload(AssetEndpoint.IAMLOGO, formData)); return this.handleUploadPromise(this.assetService.upload(AssetEndpoint.IAMLOGO, formData, this.org.id));
} }
} }
@@ -158,9 +172,9 @@ export class PrivateLabelingPolicyComponent implements OnDestroy {
formData.append('file', file); formData.append('file', file);
switch (this.serviceType) { switch (this.serviceType) {
case PolicyComponentServiceType.MGMT: case PolicyComponentServiceType.MGMT:
return this.handleFontUploadPromise(this.assetService.upload(AssetEndpoint.MGMTFONT, formData)); return this.handleFontUploadPromise(this.assetService.upload(AssetEndpoint.MGMTFONT, formData, this.org.id));
case PolicyComponentServiceType.ADMIN: case PolicyComponentServiceType.ADMIN:
return this.handleFontUploadPromise(this.assetService.upload(AssetEndpoint.IAMFONT, formData)); return this.handleFontUploadPromise(this.assetService.upload(AssetEndpoint.IAMFONT, formData, this.org.id));
} }
} }
} }
@@ -257,20 +271,20 @@ export class PrivateLabelingPolicyComponent implements OnDestroy {
if (theme === Theme.DARK) { if (theme === Theme.DARK) {
switch (this.serviceType) { switch (this.serviceType) {
case PolicyComponentServiceType.MGMT: case PolicyComponentServiceType.MGMT:
this.handleUploadPromise(this.assetService.upload(AssetEndpoint.MGMTDARKICON, formData)); this.handleUploadPromise(this.assetService.upload(AssetEndpoint.MGMTDARKICON, formData, this.org.id));
break; break;
case PolicyComponentServiceType.ADMIN: case PolicyComponentServiceType.ADMIN:
this.handleUploadPromise(this.assetService.upload(AssetEndpoint.IAMDARKICON, formData)); this.handleUploadPromise(this.assetService.upload(AssetEndpoint.IAMDARKICON, formData, this.org.id));
break; break;
} }
} }
if (theme === Theme.LIGHT) { if (theme === Theme.LIGHT) {
switch (this.serviceType) { switch (this.serviceType) {
case PolicyComponentServiceType.MGMT: case PolicyComponentServiceType.MGMT:
this.handleUploadPromise(this.assetService.upload(AssetEndpoint.MGMTICON, formData)); this.handleUploadPromise(this.assetService.upload(AssetEndpoint.MGMTICON, formData, this.org.id));
break; break;
case PolicyComponentServiceType.ADMIN: case PolicyComponentServiceType.ADMIN:
this.handleUploadPromise(this.assetService.upload(AssetEndpoint.IAMICON, formData)); this.handleUploadPromise(this.assetService.upload(AssetEndpoint.IAMICON, formData, this.org.id));
break; break;
} }
} }
@@ -291,7 +305,7 @@ export class PrivateLabelingPolicyComponent implements OnDestroy {
} }
private handleUploadPromise(task: Promise<any>): Promise<any> { private handleUploadPromise(task: Promise<any>): Promise<any> {
return task.then(() => { const enhTask = task.then(() => {
this.toast.showInfo('POLICY.TOAST.UPLOADSUCCESS', true); this.toast.showInfo('POLICY.TOAST.UPLOADSUCCESS', true);
setTimeout(() => { setTimeout(() => {
this.loadingImages = true; this.loadingImages = true;
@@ -304,37 +318,53 @@ export class PrivateLabelingPolicyComponent implements OnDestroy {
}); });
}, 1000); }, 1000);
}).catch(error => this.toast.showError(error)); }).catch(error => this.toast.showError(error));
if (this.serviceType === PolicyComponentServiceType.MGMT && ((this.previewData as LabelPolicy.AsObject).isDefault)) {
return this.savePolicy().then(() => enhTask);
} else {
return enhTask;
}
} }
public fetchData(): void { public fetchData(): void {
this.loading = true; this.loading = true;
this.getPreviewData().then(data => { this.authService.canUseFeature(['label_policy.private_label']).pipe(take(1)).subscribe((canUse) => {
console.log('preview', data); this.getPreviewData().then(data => {
this.loadingImages = true; console.log('preview', data);
if (data.policy) { if (data.policy) {
this.previewData = data.policy; this.previewData = data.policy;
this.loading = false; this.loading = false;
this.loadPreviewImages(); if ((canUse === true && this.serviceType === PolicyComponentServiceType.MGMT) ||
} this.serviceType === PolicyComponentServiceType.ADMIN) {
}).catch(error => { this.loadingImages = true;
this.toast.showError(error); this.loadPreviewImages();
}
}
}).catch(error => {
this.toast.showError(error);
});
this.getData().then(data => {
console.log('data', data);
if (data.policy) {
this.data = data.policy;
this.loading = false;
if ((canUse === true && this.serviceType === PolicyComponentServiceType.MGMT) ||
this.serviceType === PolicyComponentServiceType.ADMIN) {
// this.loadingImages = true;
this.loadImages();
}
}
}).catch(error => {
this.toast.showError(error);
});
}); });
this.getData().then(data => {
console.log('data', data);
if (data.policy) {
this.data = data.policy;
this.loading = false;
this.loadImages();
}
}).catch(error => {
this.toast.showError(error);
});
} }
private loadImages(): void { private loadImages(): void {
@@ -451,7 +481,7 @@ export class PrivateLabelingPolicyComponent implements OnDestroy {
} }
private loadAsset(imagekey: string, url: string): Promise<any> { private loadAsset(imagekey: string, url: string): Promise<any> {
return this.assetService.load(`${url}`).then(data => { return this.assetService.load(`${url}`, this.org.id).then(data => {
const objectURL = URL.createObjectURL(data); const objectURL = URL.createObjectURL(data);
this.images[imagekey] = this.sanitizer.bypassSecurityTrustUrl(objectURL); this.images[imagekey] = this.sanitizer.bypassSecurityTrustUrl(objectURL);
this.refreshPreview.emit(); this.refreshPreview.emit();
@@ -462,7 +492,7 @@ export class PrivateLabelingPolicyComponent implements OnDestroy {
public removePolicy(): void { public removePolicy(): void {
if (this.service instanceof ManagementService) { if (this.service instanceof ManagementService) {
this.service.resetPasswordComplexityPolicyToDefault().then(() => { this.service.resetLabelPolicyToDefault().then(() => {
this.toast.showInfo('POLICY.TOAST.RESETSUCCESS', true); this.toast.showInfo('POLICY.TOAST.RESETSUCCESS', true);
setTimeout(() => { setTimeout(() => {
this.fetchData(); this.fetchData();
@@ -473,14 +503,14 @@ export class PrivateLabelingPolicyComponent implements OnDestroy {
} }
} }
public savePolicy(): void { public savePolicy(): Promise<any> {
switch (this.serviceType) { switch (this.serviceType) {
case PolicyComponentServiceType.MGMT: case PolicyComponentServiceType.MGMT:
if ((this.previewData as LabelPolicy.AsObject).isDefault) { if ((this.previewData as LabelPolicy.AsObject).isDefault) {
const req0 = new AddCustomLabelPolicyRequest(); const req0 = new AddCustomLabelPolicyRequest();
this.overwriteValues(req0); this.overwriteValues(req0);
(this.service as ManagementService).addCustomLabelPolicy(req0).then(() => { return (this.service as ManagementService).addCustomLabelPolicy(req0).then(() => {
this.toast.showInfo('POLICY.TOAST.SET', true); this.toast.showInfo('POLICY.TOAST.SET', true);
}).catch((error: HttpErrorResponse) => { }).catch((error: HttpErrorResponse) => {
this.toast.showError(error); this.toast.showError(error);
@@ -489,22 +519,20 @@ export class PrivateLabelingPolicyComponent implements OnDestroy {
const req1 = new UpdateCustomLabelPolicyRequest(); const req1 = new UpdateCustomLabelPolicyRequest();
this.overwriteValues(req1); this.overwriteValues(req1);
(this.service as ManagementService).updateCustomLabelPolicy(req1).then(() => { return (this.service as ManagementService).updateCustomLabelPolicy(req1).then(() => {
this.toast.showInfo('POLICY.TOAST.SET', true); this.toast.showInfo('POLICY.TOAST.SET', true);
}).catch(error => { }).catch(error => {
this.toast.showError(error); this.toast.showError(error);
}); });
} }
break;
case PolicyComponentServiceType.ADMIN: case PolicyComponentServiceType.ADMIN:
const req = new UpdateLabelPolicyRequest(); const req = new UpdateLabelPolicyRequest();
this.overwriteValues(req); this.overwriteValues(req);
(this.service as AdminService).updateLabelPolicy(req).then(() => { return (this.service as AdminService).updateLabelPolicy(req).then(() => {
this.toast.showInfo('POLICY.TOAST.SET', true); this.toast.showInfo('POLICY.TOAST.SET', true);
}).catch(error => { }).catch(error => {
this.toast.showError(error); this.toast.showError(error);
}); });
break;
} }
} }

View File

@@ -22,8 +22,8 @@ export const IAM_LOGIN_POLICY_LINK = {
}; };
export const IAM_PRIVATELABEL_LINK = { export const IAM_PRIVATELABEL_LINK = {
i18nTitle: 'POLICY.LABEL.TITLE', i18nTitle: 'POLICY.PRIVATELABELING.TITLE',
i18nDesc: 'POLICY.LABEL.DESCRIPTION', i18nDesc: 'POLICY.PRIVATELABELING.DESCRIPTION',
routerLink: ['/iam', 'policy', PolicyComponentType.PRIVATELABEL], routerLink: ['/iam', 'policy', PolicyComponentType.PRIVATELABEL],
withRole: ['iam.policy.read'], withRole: ['iam.policy.read'],
}; };
@@ -51,8 +51,8 @@ export const ORG_LOGIN_POLICY_LINK = {
export const ORG_PRIVATELABEL_LINK = { export const ORG_PRIVATELABEL_LINK = {
i18nTitle: 'POLICY.LABEL.TITLE', i18nTitle: 'POLICY.PRIVATELABELING.TITLE',
i18nDesc: 'POLICY.LABEL.DESCRIPTION', i18nDesc: 'POLICY.PRIVATELABELING.DESCRIPTION',
routerLink: ['/org', 'policy', PolicyComponentType.PRIVATELABEL], routerLink: ['/org', 'policy', PolicyComponentType.PRIVATELABEL],
withRole: ['policy.read'], withRole: ['policy.read'],
}; };

View File

@@ -57,7 +57,7 @@
<div class="circle"> <div class="circle">
<app-avatar <app-avatar
*ngIf="user.human && user.human.displayName && user.human?.firstName && user.human?.lastName; else cog" *ngIf="user.human && user.human.displayName && user.human?.firstName && user.human?.lastName; else cog"
class="avatar" [name]="user.human.displayName" [size]="32"> class="avatar" [name]="user.human.displayName" [forColor]="user?.preferredLoginName" [size]="32">
</app-avatar> </app-avatar>
<ng-template #cog> <ng-template #cog>
<div class="sa-icon"> <div class="sa-icon">

View File

@@ -31,7 +31,7 @@
(change)="$event ? selection.toggle(row) : null" [checked]="selection.isSelected(row)"> (change)="$event ? selection.toggle(row) : null" [checked]="selection.isSelected(row)">
<app-avatar <app-avatar
*ngIf="context !== UserGrantContext.USER && row && row?.displayName && row.firstName && row.lastName" *ngIf="context !== UserGrantContext.USER && row && row?.displayName && row.firstName && row.lastName"
class="avatar" [name]="row.displayName" [size]="32"> class="avatar" [name]="row.displayName" [forColor]="row?.preferredLoginName" [size]="32">
</app-avatar> </app-avatar>
</mat-checkbox> </mat-checkbox>
</td> </td>

View File

@@ -3,6 +3,7 @@
<app-avatar [routerLink]="['/users/me']" <app-avatar [routerLink]="['/users/me']"
*ngIf="user && (user.human?.profile?.displayName || (user.human?.profile?.firstName && user.human?.profile?.lastName))" *ngIf="user && (user.human?.profile?.displayName || (user.human?.profile?.firstName && user.human?.profile?.lastName))"
class="avatar" class="avatar"
[forColor]="user?.preferredLoginName"
[name]="user.human?.profile?.displayName ? user.human?.profile?.displayName : (user.human?.profile?.firstName + ' '+ user.human?.profile?.lastName)" [name]="user.human?.profile?.displayName ? user.human?.profile?.displayName : (user.human?.profile?.firstName + ' '+ user.human?.profile?.lastName)"
[size]="100"> [size]="100">
</app-avatar> </app-avatar>

View File

@@ -103,7 +103,6 @@ export class GrantedProjectListComponent implements OnInit, OnDestroy {
this.grid = false; this.grid = false;
} }
this.dataSource.data = this.grantedProjectList; this.dataSource.data = this.grantedProjectList;
console.log(resp.resultList);
this.loadingSubject.next(false); this.loadingSubject.next(false);
}).catch(error => { }).catch(error => {

View File

@@ -13,190 +13,189 @@ import { StorageKey, StorageService } from 'src/app/services/storage.service';
import { ToastService } from 'src/app/services/toast.service'; import { ToastService } from 'src/app/services/toast.service';
@Component({ @Component({
selector: 'app-user-grant-create', selector: 'app-user-grant-create',
templateUrl: './user-grant-create.component.html', templateUrl: './user-grant-create.component.html',
styleUrls: ['./user-grant-create.component.scss'], styleUrls: ['./user-grant-create.component.scss'],
}) })
export class UserGrantCreateComponent implements OnDestroy { export class UserGrantCreateComponent implements OnDestroy {
public context!: UserGrantContext; public context!: UserGrantContext;
public org!: Org.AsObject; public org!: Org.AsObject;
public userId: string = ''; public userId: string = '';
public projectId: string = ''; public projectId: string = '';
public project!: GrantedProject.AsObject | Project.AsObject; public project!: GrantedProject.AsObject | Project.AsObject;
public grantId: string = ''; public grantId: string = '';
public rolesList: string[] = []; public rolesList: string[] = [];
public STEPS: number = 2; // project, roles public STEPS: number = 2; // project, roles
public currentCreateStep: number = 1; public currentCreateStep: number = 1;
public filterValue: string = ''; public filterValue: string = '';
private subscription: Subscription = new Subscription(); private subscription: Subscription = new Subscription();
public UserGrantContext: any = UserGrantContext; public UserGrantContext: any = UserGrantContext;
public grantedRoleKeysList: string[] = []; public grantedRoleKeysList: string[] = [];
public user!: User.AsObject; public user!: User.AsObject;
public UserTarget: any = UserTarget; public UserTarget: any = UserTarget;
constructor( constructor(
private userService: ManagementService, private userService: ManagementService,
private toast: ToastService, private toast: ToastService,
private _location: Location, private _location: Location,
private route: ActivatedRoute, private route: ActivatedRoute,
private authService: GrpcAuthService, private authService: GrpcAuthService,
private mgmtService: ManagementService, private mgmtService: ManagementService,
private storage: StorageService, private storage: StorageService,
) { ) {
this.subscription = this.route.params.subscribe((params: Params) => { this.subscription = this.route.params.subscribe((params: Params) => {
const { projectid, grantid, userid } = params; const { projectid, grantid, userid } = params;
this.context = UserGrantContext.NONE; this.context = UserGrantContext.NONE;
this.projectId = projectid; this.projectId = projectid;
this.grantId = grantid; this.grantId = grantid;
this.userId = userid; this.userId = userid;
if (this.projectId && !this.grantId) { if (this.projectId && !this.grantId) {
this.context = UserGrantContext.OWNED_PROJECT; this.context = UserGrantContext.OWNED_PROJECT;
this.mgmtService.getProjectByID(this.projectId).then(resp => { this.mgmtService.getProjectByID(this.projectId).then(resp => {
if (resp.project) { if (resp.project) {
this.project = resp.project; this.project = resp.project;
} }
}).catch((error: any) => { }).catch((error: any) => {
this.toast.showError(error); this.toast.showError(error);
});
} else if (this.projectId && this.grantId) {
this.context = UserGrantContext.GRANTED_PROJECT;
this.mgmtService.getGrantedProjectByID(this.projectId, this.grantId).then(resp => {
if (resp.grantedProject) {
this.project = resp.grantedProject;
}
if (resp.grantedProject?.grantedRoleKeysList) {
this.grantedRoleKeysList = resp.grantedProject?.grantedRoleKeysList;
}
}).catch((error: any) => {
this.toast.showError(error);
});
} else if (this.userId) {
this.context = UserGrantContext.USER;
this.mgmtService.getUserByID(this.userId).then(resp => {
if (resp.user) {
this.user = resp.user;
}
}).catch((error: any) => {
this.toast.showError(error);
});
}
}); });
} else if (this.projectId && this.grantId) {
this.context = UserGrantContext.GRANTED_PROJECT;
this.mgmtService.getGrantedProjectByID(this.projectId, this.grantId).then(resp => {
if (resp.grantedProject) {
this.project = resp.grantedProject;
}
if (resp.grantedProject?.grantedRoleKeysList) {
this.grantedRoleKeysList = resp.grantedProject?.grantedRoleKeysList;
}
}).catch((error: any) => {
this.toast.showError(error);
});
} else if (this.userId) {
this.context = UserGrantContext.USER;
this.mgmtService.getUserByID(this.userId).then(resp => {
if (resp.user) {
this.user = resp.user;
}
}).catch((error: any) => {
this.toast.showError(error);
});
}
});
const temporg = this.storage.getItem<Org.AsObject>(StorageKey.organization); const temporg = this.storage.getItem<Org.AsObject>(StorageKey.organization);
if (temporg) { if (temporg) {
this.org = temporg; this.org = temporg;
}
}
public close(): void {
this._location.back();
}
public addGrant(): void {
switch (this.context) {
case UserGrantContext.OWNED_PROJECT:
this.userService.addUserGrant(
this.userId,
this.rolesList,
this.projectId,
).then(() => {
this.toast.showInfo('PROJECT.GRANT.TOAST.PROJECTGRANTADDED', true);
this.close();
}).catch((error: any) => {
this.toast.showError(error);
});
break;
case UserGrantContext.GRANTED_PROJECT:
this.userService.addUserGrant(
this.userId,
this.rolesList,
this.projectId,
this.grantId,
).then(() => {
this.toast.showInfo('PROJECT.GRANT.TOAST.PROJECTGRANTUSERGRANTADDED', true);
this.close();
}).catch((error: any) => {
this.toast.showError(error);
});
break;
case UserGrantContext.USER:
let grantId;
if ((this.project as GrantedProject.AsObject)?.grantId) {
grantId = (this.project as GrantedProject.AsObject).grantId;
} }
}
public close(): void { this.userService.addUserGrant(
this._location.back(); this.userId,
} this.rolesList,
this.projectId,
grantId,
).then(() => {
this.toast.showInfo('PROJECT.GRANT.TOAST.PROJECTGRANTUSERGRANTADDED', true);
this.close();
}).catch((error: any) => {
this.toast.showError(error);
});
break;
case UserGrantContext.NONE:
let tempGrantId;
public addGrant(): void { if ((this.project as GrantedProject.AsObject)?.grantId) {
switch (this.context) { tempGrantId = (this.project as GrantedProject.AsObject).grantId;
case UserGrantContext.OWNED_PROJECT:
this.userService.addUserGrant(
this.userId,
this.rolesList,
this.projectId,
).then(() => {
this.toast.showInfo('PROJECT.GRANT.TOAST.PROJECTGRANTADDED', true);
this.close();
}).catch((error: any) => {
this.toast.showError(error);
});
break;
case UserGrantContext.GRANTED_PROJECT:
this.userService.addUserGrant(
this.userId,
this.rolesList,
this.projectId,
this.grantId,
).then(() => {
this.toast.showInfo('PROJECT.GRANT.TOAST.PROJECTGRANTUSERGRANTADDED', true);
this.close();
}).catch((error: any) => {
this.toast.showError(error);
});
break;
case UserGrantContext.USER:
let grantId;
if ((this.project as GrantedProject.AsObject)?.grantId) {
grantId = (this.project as GrantedProject.AsObject).grantId;
}
this.userService.addUserGrant(
this.userId,
this.rolesList,
this.projectId,
grantId,
).then(() => {
this.toast.showInfo('PROJECT.GRANT.TOAST.PROJECTGRANTUSERGRANTADDED', true);
this.close();
}).catch((error: any) => {
this.toast.showError(error);
});
break;
case UserGrantContext.NONE:
console.log('none');
let tempGrantId;
if ((this.project as GrantedProject.AsObject)?.grantId) {
tempGrantId = (this.project as GrantedProject.AsObject).grantId;
}
this.userService.addUserGrant(
this.userId,
this.rolesList,
this.projectId,
tempGrantId,
).then(() => {
this.toast.showInfo('PROJECT.GRANT.TOAST.PROJECTGRANTUSERGRANTADDED', true);
this.close();
}).catch((error: any) => {
this.toast.showError(error);
});
break;
} }
}
public selectProject(project: Project.AsObject | GrantedProject.AsObject | any): void { this.userService.addUserGrant(
this.project = project; this.userId,
this.projectId = project.id || project.projectId; this.rolesList,
this.projectId,
this.grantedRoleKeysList = project.grantedRoleKeysList ?? []; tempGrantId,
).then(() => {
this.toast.showInfo('PROJECT.GRANT.TOAST.PROJECTGRANTUSERGRANTADDED', true);
this.close();
}).catch((error: any) => {
this.toast.showError(error);
});
break;
} }
}
public selectUser(user: User.AsObject): void { public selectProject(project: Project.AsObject | GrantedProject.AsObject | any): void {
this.userId = user.id; this.project = project;
} this.projectId = project.id || project.projectId;
public selectRoles(roles: Role.AsObject[]): void { this.grantedRoleKeysList = project.grantedRoleKeysList ?? [];
this.rolesList = roles.map(role => role.key); }
}
public next(): void { public selectUser(user: User.AsObject): void {
this.currentCreateStep++; this.userId = user.id;
} }
public previous(): void { public selectRoles(roles: Role.AsObject[]): void {
this.currentCreateStep--; this.rolesList = roles.map(role => role.key);
} }
public ngOnDestroy(): void { public next(): void {
this.subscription.unsubscribe(); this.currentCreateStep++;
} }
public previous(): void {
this.currentCreateStep--;
}
public ngOnDestroy(): void {
this.subscription.unsubscribe();
}
} }

View File

@@ -28,7 +28,7 @@
</app-card> </app-card>
<app-card *ngIf="user && user.human?.profile" class=" app-card" title="{{ 'USER.PROFILE.TITLE' | translate }}"> <app-card *ngIf="user && user.human?.profile" class=" app-card" title="{{ 'USER.PROFILE.TITLE' | translate }}">
<app-detail-form [genders]="genders" [languages]="languages" [username]="user.userName" [user]="user.human" <app-detail-form [showEditImage]="true" [preferredLoginName]="user.preferredLoginName" [genders]="genders" [languages]="languages" [username]="user.userName" [user]="user.human"
(changedLanguage)="changedLanguage($event)" (submitData)="saveProfile($event)"> (changedLanguage)="changedLanguage($event)" (submitData)="saveProfile($event)">
</app-detail-form> </app-detail-form>
</app-card> </app-card>

View File

@@ -11,196 +11,197 @@ import { ToastService } from 'src/app/services/toast.service';
import { EditDialogComponent, EditDialogType } from './edit-dialog/edit-dialog.component'; import { EditDialogComponent, EditDialogType } from './edit-dialog/edit-dialog.component';
@Component({ @Component({
selector: 'app-auth-user-detail', selector: 'app-auth-user-detail',
templateUrl: './auth-user-detail.component.html', templateUrl: './auth-user-detail.component.html',
styleUrls: ['./auth-user-detail.component.scss'], styleUrls: ['./auth-user-detail.component.scss'],
}) })
export class AuthUserDetailComponent implements OnDestroy { export class AuthUserDetailComponent implements OnDestroy {
public user!: User.AsObject; public user!: User.AsObject;
public genders: Gender[] = [Gender.GENDER_MALE, Gender.GENDER_FEMALE, Gender.GENDER_DIVERSE]; public genders: Gender[] = [Gender.GENDER_MALE, Gender.GENDER_FEMALE, Gender.GENDER_DIVERSE];
public languages: string[] = ['de', 'en']; public languages: string[] = ['de', 'en'];
private subscription: Subscription = new Subscription(); private subscription: Subscription = new Subscription();
public loading: boolean = false; public loading: boolean = false;
public copied: string = ''; public copied: string = '';
public ChangeType: any = ChangeType; public ChangeType: any = ChangeType;
public userLoginMustBeDomain: boolean = false; public userLoginMustBeDomain: boolean = false;
public UserState: any = UserState; public UserState: any = UserState;
public USERGRANTCONTEXT: UserGrantContext = UserGrantContext.USER; public USERGRANTCONTEXT: UserGrantContext = UserGrantContext.USER;
public refreshChanges$: EventEmitter<void> = new EventEmitter(); public refreshChanges$: EventEmitter<void> = new EventEmitter();
constructor( constructor(
public translate: TranslateService, public translate: TranslateService,
private toast: ToastService, private toast: ToastService,
public userService: GrpcAuthService, public userService: GrpcAuthService,
private dialog: MatDialog, private dialog: MatDialog,
) { ) {
this.loading = true; this.loading = true;
this.refreshUser();
}
refreshUser(): void {
this.refreshChanges$.emit();
this.userService.getMyUser().then(resp => {
if (resp.user) {
this.user = resp.user;
console.log(resp.user);
}
this.loading = false;
}).catch(error => {
this.toast.showError(error);
this.loading = false;
});
}
public ngOnDestroy(): void {
this.subscription.unsubscribe();
}
public saveProfile(profileData: Profile.AsObject): void {
if (this.user.human) {
this.user.human.profile = profileData;
this.userService
.updateMyProfile(
this.user.human.profile?.firstName,
this.user.human.profile?.lastName,
this.user.human.profile?.nickName,
this.user.human.profile?.displayName,
this.user.human.profile?.preferredLanguage,
this.user.human.profile?.gender,
)
.then(() => {
this.toast.showInfo('USER.TOAST.SAVED', true);
this.refreshChanges$.emit();
})
.catch(error => {
this.toast.showError(error);
});
}
}
public saveEmail(email: string): void {
this.userService
.setMyEmail(email).then(() => {
this.toast.showInfo('USER.TOAST.EMAILSAVED', true);
if (this.user.human) {
const mailToSet = new Email();
mailToSet.setEmail(email);
this.user.human.email = mailToSet.toObject();
this.refreshUser();
}
}).catch(error => {
this.toast.showError(error);
});
}
public enteredPhoneCode(code: string): void {
this.userService.verifyMyPhone(code).then(() => {
this.toast.showInfo('USER.TOAST.PHONESAVED', true);
this.refreshUser();
}).catch(error => {
this.toast.showError(error);
});
}
public changedLanguage(language: string): void {
this.translate.use(language);
}
public resendPhoneVerification(): void {
this.userService.resendMyPhoneVerification().then(() => {
this.toast.showInfo('USER.TOAST.PHONEVERIFICATIONSENT', true);
this.refreshChanges$.emit();
}).catch(error => {
this.toast.showError(error);
});
}
public resendEmailVerification(): void {
this.userService.resendMyEmailVerification().then(() => {
this.toast.showInfo('USER.TOAST.EMAILVERIFICATIONSENT', true);
this.refreshChanges$.emit();
}).catch(error => {
this.toast.showError(error);
});
}
public deletePhone(): void {
this.userService.removeMyPhone().then(() => {
this.toast.showInfo('USER.TOAST.PHONEREMOVED', true);
if (this.user.human?.phone) {
const phone = new Phone();
this.user.human.phone = phone.toObject();
this.refreshUser(); this.refreshUser();
} }
}).catch(error => {
this.toast.showError(error);
});
}
refreshUser(): void { public savePhone(phone: string): void {
this.refreshChanges$.emit(); if (this.user.human) {
this.userService.getMyUser().then(resp => { this.userService
if (resp.user) { .setMyPhone(phone).then(() => {
this.user = resp.user; this.toast.showInfo('USER.TOAST.PHONESAVED', true);
} if (this.user.human) {
this.loading = false; const phoneToSet = new Phone();
}).catch(error => { phoneToSet.setPhone(phone);
this.toast.showError(error); this.user.human.phone = phoneToSet.toObject();
this.loading = false;
});
}
public ngOnDestroy(): void {
this.subscription.unsubscribe();
}
public saveProfile(profileData: Profile.AsObject): void {
if (this.user.human) {
this.user.human.profile = profileData;
this.userService
.updateMyProfile(
this.user.human.profile?.firstName,
this.user.human.profile?.lastName,
this.user.human.profile?.nickName,
this.user.human.profile?.displayName,
this.user.human.profile?.preferredLanguage,
this.user.human.profile?.gender,
)
.then(() => {
this.toast.showInfo('USER.TOAST.SAVED', true);
this.refreshChanges$.emit();
})
.catch(error => {
this.toast.showError(error);
});
}
}
public saveEmail(email: string): void {
this.userService
.setMyEmail(email).then(() => {
this.toast.showInfo('USER.TOAST.EMAILSAVED', true);
if (this.user.human) {
const mailToSet = new Email();
mailToSet.setEmail(email);
this.user.human.email = mailToSet.toObject();
this.refreshUser();
}
}).catch(error => {
this.toast.showError(error);
});
}
public enteredPhoneCode(code: string): void {
this.userService.verifyMyPhone(code).then(() => {
this.toast.showInfo('USER.TOAST.PHONESAVED', true);
this.refreshUser(); this.refreshUser();
}
}).catch(error => { }).catch(error => {
this.toast.showError(error); this.toast.showError(error);
}); });
} }
}
public changedLanguage(language: string): void { public openEditDialog(type: EditDialogType): void {
this.translate.use(language); switch (type) {
} case EditDialogType.PHONE:
const dialogRefPhone = this.dialog.open(EditDialogComponent, {
public resendPhoneVerification(): void { data: {
this.userService.resendMyPhoneVerification().then(() => { confirmKey: 'ACTIONS.SAVE',
this.toast.showInfo('USER.TOAST.PHONEVERIFICATIONSENT', true); cancelKey: 'ACTIONS.CANCEL',
this.refreshChanges$.emit(); labelKey: 'ACTIONS.NEWVALUE',
}).catch(error => { titleKey: 'USER.LOGINMETHODS.PHONE.EDITTITLE',
this.toast.showError(error); descriptionKey: 'USER.LOGINMETHODS.PHONE.EDITDESC',
value: this.user.human?.phone?.phone,
type: type,
},
width: '400px',
}); });
}
public resendEmailVerification(): void { dialogRefPhone.afterClosed().subscribe(resp => {
this.userService.resendMyEmailVerification().then(() => { if (resp) {
this.toast.showInfo('USER.TOAST.EMAILVERIFICATIONSENT', true); this.savePhone(resp);
this.refreshChanges$.emit(); }
}).catch(error => {
this.toast.showError(error);
}); });
} break;
case EditDialogType.EMAIL:
public deletePhone(): void { const dialogRefEmail = this.dialog.open(EditDialogComponent, {
this.userService.removeMyPhone().then(() => { data: {
this.toast.showInfo('USER.TOAST.PHONEREMOVED', true); confirmKey: 'ACTIONS.SAVE',
if (this.user.human?.phone) { cancelKey: 'ACTIONS.CANCEL',
const phone = new Phone(); labelKey: 'ACTIONS.NEWVALUE',
this.user.human.phone = phone.toObject(); titleKey: 'USER.LOGINMETHODS.EMAIL.EDITTITLE',
this.refreshUser(); descriptionKey: 'USER.LOGINMETHODS.EMAIL.EDITDESC',
} value: this.user.human?.email?.email,
}).catch(error => { type: type,
this.toast.showError(error); },
width: '400px',
}); });
dialogRefEmail.afterClosed().subscribe(resp => {
if (resp) {
this.saveEmail(resp);
}
});
break;
} }
}
public savePhone(phone: string): void {
if (this.user.human) {
this.userService
.setMyPhone(phone).then(() => {
this.toast.showInfo('USER.TOAST.PHONESAVED', true);
if (this.user.human) {
const phoneToSet = new Phone();
phoneToSet.setPhone(phone);
this.user.human.phone = phoneToSet.toObject();
this.refreshUser();
}
}).catch(error => {
this.toast.showError(error);
});
}
}
public openEditDialog(type: EditDialogType): void {
switch (type) {
case EditDialogType.PHONE:
const dialogRefPhone = this.dialog.open(EditDialogComponent, {
data: {
confirmKey: 'ACTIONS.SAVE',
cancelKey: 'ACTIONS.CANCEL',
labelKey: 'ACTIONS.NEWVALUE',
titleKey: 'USER.LOGINMETHODS.PHONE.EDITTITLE',
descriptionKey: 'USER.LOGINMETHODS.PHONE.EDITDESC',
value: this.user.human?.phone?.phone,
type: type,
},
width: '400px',
});
dialogRefPhone.afterClosed().subscribe(resp => {
if (resp) {
this.savePhone(resp);
}
});
break;
case EditDialogType.EMAIL:
const dialogRefEmail = this.dialog.open(EditDialogComponent, {
data: {
confirmKey: 'ACTIONS.SAVE',
cancelKey: 'ACTIONS.CANCEL',
labelKey: 'ACTIONS.NEWVALUE',
titleKey: 'USER.LOGINMETHODS.EMAIL.EDITTITLE',
descriptionKey: 'USER.LOGINMETHODS.EMAIL.EDITDESC',
value: this.user.human?.email?.email,
type: type,
},
width: '400px',
});
dialogRefEmail.afterClosed().subscribe(resp => {
if (resp) {
this.saveEmail(resp);
}
});
break;
}
}
} }

View File

@@ -1,9 +1,22 @@
<form [formGroup]="profileForm" *ngIf="profileForm" (ngSubmit)="submitForm()"> <form [formGroup]="profileForm" *ngIf="profileForm" (ngSubmit)="submitForm()">
<div class="content"> <div class="user-form-content">
<div class="user-form-content inner">
<button class="camera-wrapper" type="button" (click)="showEditImage ? openUploadDialog() : null">
<div class="i-wrapper" *ngIf="showEditImage">
<i class="las la-camera"></i>
</div>
<img class="pic" [src]="profilePic" *ngIf="profilePic"/>
<app-avatar
*ngIf="!profilePic && user && user.profile?.displayName && user.profile?.firstName && user?.profile.lastName"
class="avatar" [name]="user.profile?.displayName" [forColor]="preferredLoginName" [size]="80">
</app-avatar>
</button>
<cnsl-form-field class="formfield"> <cnsl-form-field class="formfield">
<cnsl-label>{{ 'USER.PROFILE.USERNAME' | translate }}</cnsl-label> <cnsl-label>{{ 'USER.PROFILE.USERNAME' | translate }}</cnsl-label>
<input cnslInput formControlName="userName" /> <input cnslInput formControlName="userName" />
</cnsl-form-field> </cnsl-form-field>
</div>
<cnsl-form-field class="formfield"> <cnsl-form-field class="formfield">
<cnsl-label>{{ 'USER.PROFILE.FIRSTNAME' | translate }}</cnsl-label> <cnsl-label>{{ 'USER.PROFILE.FIRSTNAME' | translate }}</cnsl-label>
<input cnslInput formControlName="firstName" /> <input cnslInput formControlName="firstName" />

View File

@@ -1,10 +1,60 @@
.user-form-content {
.content {
display: flex; display: flex;
flex-direction: row; flex-direction: row;
flex-wrap: wrap; flex-wrap: wrap;
margin: 0 -.5rem; margin: 0 -.5rem;
&.inner {
margin: 0;
width: 100%;
}
.camera-wrapper {
margin: 0 .5rem;
position: relative;
border-radius: 50%;
padding: 0;
border: none;
display: flex;
align-items: center;
justify-content: center;
background: none;
cursor: pointer;
transition: all .3s ease;
.i-wrapper {
border-radius: 50%;
background-color: #00000050;
position: absolute;
top: 0;
z-index: 1;
left: 0;
right: 0;
bottom: 0;
display: flex;
align-items: center;
justify-content: center;
i {
font-size: 3rem;
color: white;
}
}
&:hover {
.i-wrapper {
background-color: #00000080;
}
}
.pic {
height: 80px;
width: 80px;
object-fit: contain;
border-radius: 50%;
}
}
.formfield { .formfield {
flex: 1 1 33%; flex: 1 1 33%;
margin: 0 .5rem; margin: 0 .5rem;

View File

@@ -1,92 +1,131 @@
import { Component, EventEmitter, Input, OnChanges, OnDestroy, Output } from '@angular/core'; import { Component, EventEmitter, Input, OnChanges, OnDestroy, Output } from '@angular/core';
import { AbstractControl, FormBuilder, FormGroup, Validators } from '@angular/forms'; import { AbstractControl, FormBuilder, FormGroup, Validators } from '@angular/forms';
import { MatDialog } from '@angular/material/dialog';
import { DomSanitizer } from '@angular/platform-browser';
import { Subscription } from 'rxjs'; import { Subscription } from 'rxjs';
import { Gender, Human, User } from 'src/app/proto/generated/zitadel/user_pb'; import { Gender, Human, User } from 'src/app/proto/generated/zitadel/user_pb';
import { AssetService } from 'src/app/services/asset.service';
import { ToastService } from 'src/app/services/toast.service';
import { ProfilePictureComponent } from './profile-picture/profile-picture.component';
@Component({ @Component({
selector: 'app-detail-form', selector: 'app-detail-form',
templateUrl: './detail-form.component.html', templateUrl: './detail-form.component.html',
styleUrls: ['./detail-form.component.scss'], styleUrls: ['./detail-form.component.scss'],
}) })
export class DetailFormComponent implements OnDestroy, OnChanges { export class DetailFormComponent implements OnDestroy, OnChanges {
@Input() public username!: string; @Input() public showEditImage: boolean = false;
@Input() public user!: Human.AsObject; @Input() public preferredLoginName: string = '';
@Input() public disabled: boolean = false; @Input() public username!: string;
@Input() public genders: Gender[] = []; @Input() public user!: Human.AsObject;
@Input() public languages: string[] = ['de', 'en']; @Input() public disabled: boolean = false;
@Output() public submitData: EventEmitter<User> = new EventEmitter<User>(); @Input() public genders: Gender[] = [];
@Output() public changedLanguage: EventEmitter<string> = new EventEmitter<string>(); @Input() public languages: string[] = ['de', 'en'];
@Output() public submitData: EventEmitter<User> = new EventEmitter<User>();
@Output() public changedLanguage: EventEmitter<string> = new EventEmitter<string>();
public profileForm!: FormGroup; public profilePic: any = null;
public profileForm!: FormGroup;
private sub: Subscription = new Subscription(); private sub: Subscription = new Subscription();
constructor(private fb: FormBuilder) { constructor(
this.profileForm = this.fb.group({ private fb: FormBuilder,
userName: [{ value: '', disabled: true }, [ private dialog: MatDialog,
Validators.required, private assetService: AssetService,
]], private toast: ToastService,
firstName: [{ value: '', disabled: this.disabled }, Validators.required], private sanitizer: DomSanitizer,
lastName: [{ value: '', disabled: this.disabled }, Validators.required], ) {
nickName: [{ value: '', disabled: this.disabled }], this.profileForm = this.fb.group({
displayName: [{ value: '', disabled: this.disabled }, Validators.required], userName: [{ value: '', disabled: true }, [
gender: [{ value: 0, disabled: this.disabled }], Validators.required,
preferredLanguage: [{ value: '', disabled: this.disabled }], ]],
}); firstName: [{ value: '', disabled: this.disabled }, Validators.required],
} lastName: [{ value: '', disabled: this.disabled }, Validators.required],
nickName: [{ value: '', disabled: this.disabled }],
displayName: [{ value: '', disabled: this.disabled }, Validators.required],
gender: [{ value: 0, disabled: this.disabled }],
preferredLanguage: [{ value: '', disabled: this.disabled }],
});
public ngOnChanges(): void { this.loadAvatar();
this.profileForm = this.fb.group({ }
userName: [{ value: '', disabled: true }, [
Validators.required,
]],
firstName: [{ value: '', disabled: this.disabled }, Validators.required],
lastName: [{ value: '', disabled: this.disabled }, Validators.required],
nickName: [{ value: '', disabled: this.disabled }],
displayName: [{ value: '', disabled: this.disabled }, Validators.required],
gender: [{ value: 0, disabled: this.disabled }],
preferredLanguage: [{ value: '', disabled: this.disabled }],
});
this.profileForm.patchValue({ userName: this.username, ...this.user.profile }); public ngOnChanges(): void {
this.profileForm = this.fb.group({
userName: [{ value: '', disabled: true }, [
Validators.required,
]],
firstName: [{ value: '', disabled: this.disabled }, Validators.required],
lastName: [{ value: '', disabled: this.disabled }, Validators.required],
nickName: [{ value: '', disabled: this.disabled }],
displayName: [{ value: '', disabled: this.disabled }, Validators.required],
gender: [{ value: 0, disabled: this.disabled }],
preferredLanguage: [{ value: '', disabled: this.disabled }],
});
if (this.preferredLanguage) { this.profileForm.patchValue({ userName: this.username, ...this.user.profile });
this.sub = this.preferredLanguage.valueChanges.subscribe(value => {
this.changedLanguage.emit(value);
});
}
}
public ngOnDestroy(): void { if (this.preferredLanguage) {
this.sub.unsubscribe(); this.sub = this.preferredLanguage.valueChanges.subscribe(value => {
this.changedLanguage.emit(value);
});
} }
}
public submitForm(): void { public ngOnDestroy(): void {
this.submitData.emit(this.profileForm.value); this.sub.unsubscribe();
} }
public get userName(): AbstractControl | null { public submitForm(): void {
return this.profileForm.get('userName'); this.submitData.emit(this.profileForm.value);
} }
public get firstName(): AbstractControl | null { public openUploadDialog(): void {
return this.profileForm.get('firstName'); const dialogRef = this.dialog.open(ProfilePictureComponent, {
} data: {
public get lastName(): AbstractControl | null { profilePic: this.profilePic,
return this.profileForm.get('lastName'); },
} width: '400px',
public get nickName(): AbstractControl | null { });
return this.profileForm.get('nickName');
} dialogRef.afterClosed().subscribe(resp => {
public get displayName(): AbstractControl | null { if (resp) {
return this.profileForm.get('displayName'); }
} });
public get gender(): AbstractControl | null { }
return this.profileForm.get('gender');
} public loadAvatar(): Promise<any> {
public get preferredLanguage(): AbstractControl | null { return this.assetService.load(`users/me/avatar`).then(data => {
return this.profileForm.get('preferredLanguage'); const objectURL = URL.createObjectURL(data);
} this.profilePic = this.sanitizer.bypassSecurityTrustUrl(objectURL);
}).catch(error => {
this.toast.showError(error);
});
}
public get userName(): AbstractControl | null {
return this.profileForm.get('userName');
}
public get firstName(): AbstractControl | null {
return this.profileForm.get('firstName');
}
public get lastName(): AbstractControl | null {
return this.profileForm.get('lastName');
}
public get nickName(): AbstractControl | null {
return this.profileForm.get('nickName');
}
public get displayName(): AbstractControl | null {
return this.profileForm.get('displayName');
}
public get gender(): AbstractControl | null {
return this.profileForm.get('gender');
}
public get preferredLanguage(): AbstractControl | null {
return this.profileForm.get('preferredLanguage');
}
} }

View File

@@ -4,28 +4,38 @@ import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { MatButtonModule } from '@angular/material/button'; import { MatButtonModule } from '@angular/material/button';
import { MatIconModule } from '@angular/material/icon'; import { MatIconModule } from '@angular/material/icon';
import { MatSelectModule } from '@angular/material/select'; import { MatSelectModule } from '@angular/material/select';
import { MatTooltipModule } from '@angular/material/tooltip';
import { TranslateModule } from '@ngx-translate/core'; import { TranslateModule } from '@ngx-translate/core';
import { ImageCropperModule } from 'ngx-image-cropper';
import { DropzoneModule } from 'src/app/directives/dropzone/dropzone.module';
import { AvatarModule } from 'src/app/modules/avatar/avatar.module';
import { InputModule } from 'src/app/modules/input/input.module'; import { InputModule } from 'src/app/modules/input/input.module';
import { DetailFormComponent } from './detail-form.component'; import { DetailFormComponent } from './detail-form.component';
import { ProfilePictureComponent } from './profile-picture/profile-picture.component';
@NgModule({ @NgModule({
declarations: [ declarations: [
DetailFormComponent, DetailFormComponent,
], ProfilePictureComponent,
imports: [ ],
CommonModule, imports: [
FormsModule, DropzoneModule,
ReactiveFormsModule, AvatarModule,
TranslateModule, CommonModule,
MatSelectModule, FormsModule,
MatButtonModule, ReactiveFormsModule,
MatIconModule, ImageCropperModule,
TranslateModule, TranslateModule,
InputModule, MatSelectModule,
], MatButtonModule,
exports: [ MatTooltipModule,
DetailFormComponent, MatIconModule,
], TranslateModule,
InputModule,
],
exports: [
DetailFormComponent,
],
}) })
export class DetailFormModule { } export class DetailFormModule { }

View File

@@ -0,0 +1,39 @@
<span class="title" mat-dialog-title>{{'USER.PROFILE.AVATAR.UPLOADTITLE' | translate}}</span>
<div mat-dialog-content>
<p class="desc">{{'USER.PROFILE.AVATAR.CURRENT' | translate}}</p>
<div class="current-pic-wrapper">
<img class="pic" [src]="data.profilePic" *ngIf="data.profilePic"/>
<span class="fill-space"></span>
<input #selectedFile style="display: none;" class="file-input" type="file" (change)="onDrop($event)">
<button class="btn" mat-stroked-button type="button" (click)="selectedFile.click();">{{'USER.PROFILE.AVATAR.UPLOADBTN' | translate}}</button>
<button *ngIf="data.profilePic" matTooltip="{{'ACTIONS.DELETE' | translate}}" color="warn" (click)="deletePic()" mat-icon-button><mat-icon>remove_circle</mat-icon></button>
</div>
<ng-container *ngIf="imageChangedEvent">
<p class="desc">{{'USER.PROFILE.AVATAR.PREVIEW' | translate}}</p>
<div class="cropped-preview" *ngIf="croppedImage">
<img class="pic" [src]="croppedImage"/>
<button color="primary" mat-raised-button (click)="upload()">{{'USER.PROFILE.AVATAR.UPLOAD' | translate}}</button>
</div>
<p class="error" *ngIf="showCropperError">{{'USER.PROFILE.AVATAR.CROPPERERROR' | translate}}</p>
<image-cropper
class="cropper"
[imageChangedEvent]="imageChangedEvent"
[maintainAspectRatio]="true"
[aspectRatio]="4 / 4"
[roundCropper]="true"
[autoCrop]="true"
(imageCropped)="imageCropped($event)"
(loadImageFailed)="loadImageFailed()"
></image-cropper>
</ng-container>
</div>
<div mat-dialog-actions class="action">
<button color="primary" mat-raised-button class="ok-button" (click)="closeDialog()">
{{'ACTIONS.CLOSE' | translate}}
</button>
</div>

View File

@@ -0,0 +1,68 @@
.title {
font-size: 1.2rem;
margin-top: 0;
margin-bottom: 1rem;
display: block;
}
.desc {
color: var(--grey);
font-size: 14px;
}
.current-pic-wrapper {
display: flex;
align-items: center;
margin-bottom: 1rem;
.fill-space {
flex: 1;
}
.pic {
height: 80px;
width: 80px;
object-fit: contain;
border-radius: 50%;
background-color: #00000030;
}
}
.error {
color: #f44336;
font-size: 14px;
}
.cropped-preview {
display: flex;
align-items: center;
width: 100%;
justify-content: space-between;
.pic {
height: 80px;
width: 80px;
object-fit: contain;
border-radius: 50%;
background-color: #00000030;
}
}
.cropper {
margin: 1rem 0;
width: auto;
border-radius: .5rem;
}
.action {
display: flex;
justify-content: flex-end;
.ok-button {
margin-left: .5rem;
}
}
:root {
--cropper-outline-color: black !important;
}

View File

@@ -0,0 +1,25 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { ProfilePictureComponent } from './profile-picture.component';
describe('ProfilePictureComponent', () => {
let component: ProfilePictureComponent;
let fixture: ComponentFixture<ProfilePictureComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [ProfilePictureComponent],
})
.compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(ProfilePictureComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@@ -0,0 +1,99 @@
import { Component, Inject, OnInit } from '@angular/core';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
import { ImageCroppedEvent } from 'ngx-image-cropper';
import { AssetService } from 'src/app/services/asset.service';
import { GrpcAuthService } from 'src/app/services/grpc-auth.service';
import { ToastService } from 'src/app/services/toast.service';
@Component({
selector: 'cnsl-profile-picture',
templateUrl: './profile-picture.component.html',
styleUrls: ['./profile-picture.component.scss'],
})
export class ProfilePictureComponent implements OnInit {
public isHovering: boolean = false;
public imageChangedEvent: any = '';
public imageChangedFormat: string = '';
public croppedImage: any = '';
public showCropperError: boolean = false;
constructor(
private authService: GrpcAuthService,
public dialogRef: MatDialogRef<ProfilePictureComponent>,
@Inject(MAT_DIALOG_DATA) public data: any,
private toast: ToastService,
private assetService: AssetService) { }
public ngOnInit(): void {
}
public toggleHover(isHovering: boolean): void {
this.isHovering = isHovering;
}
public onDrop(event: any): Promise<any> | void {
const filelist: FileList = event.target.files;
console.log(event.target.files);
this.imageChangedEvent = event;
const file = filelist.item(0);
if (file) {
this.imageChangedFormat = file.type;
}
}
public deletePic(): void {
console.log('delete');
this.authService.removeMyAvatar().then(() => {
this.toast.showInfo('USER.PROFILE.AVATAR.DELETESUCCESS', true);
this.data.profilePic = null;
}).catch(error => {
this.toast.showError(error);
});
}
private handleUploadPromise(task: Promise<any>): Promise<any> {
return task.then(() => {
this.toast.showInfo('POLICY.TOAST.UPLOADSUCCESS', true);
this.data.profilePic = this.croppedImage;
}).catch(error => this.toast.showError(error));
}
public fileChangeEvent(event: any): void {
this.imageChangedEvent = event;
}
public imageCropped(event: ImageCroppedEvent): void {
this.showCropperError = false;
this.croppedImage = event.base64;
}
public upload(): Promise<any> | void {
const formData = new FormData();
const splitted = this.croppedImage.split(';base64,');
if (splitted[1]) {
const blob = this.base64toBlob(splitted[1]);
formData.append('file', blob);
return this.handleUploadPromise(this.assetService.upload('users/me/avatar', formData));
}
}
public loadImageFailed(): void {
this.showCropperError = true;
}
public closeDialog(): void {
this.dialogRef.close(false);
}
public base64toBlob(b64: string): Blob {
const byteCharacters = atob(b64);
const byteNumbers = new Array(byteCharacters.length);
for (let i = 0; i < byteCharacters.length; i++) {
byteNumbers[i] = byteCharacters.charCodeAt(i);
}
const byteArray = new Uint8Array(byteNumbers);
const blob = new Blob([byteArray], { type: this.imageChangedFormat });
return blob;
}
}

View File

@@ -11,195 +11,195 @@ import { ManagementService } from 'src/app/services/mgmt.service';
import { ToastService } from 'src/app/services/toast.service'; import { ToastService } from 'src/app/services/toast.service';
@Component({ @Component({
selector: 'app-memberships', selector: 'app-memberships',
templateUrl: './memberships.component.html', templateUrl: './memberships.component.html',
styleUrls: ['./memberships.component.scss'], styleUrls: ['./memberships.component.scss'],
animations: [ animations: [
trigger('cardAnimation', [ trigger('cardAnimation', [
transition('* => *', [ transition('* => *', [
query('@animate', stagger('40ms', animateChild()), { optional: true }), query('@animate', stagger('40ms', animateChild()), { optional: true }),
]), ]),
]), ]),
trigger('animate', [ trigger('animate', [
transition(':enter', [ transition(':enter', [
animate('.2s ease-in', keyframes([ animate('.2s ease-in', keyframes([
style({ opacity: 0, offset: 0 }), style({ opacity: 0, offset: 0 }),
style({ opacity: .5, transform: 'scale(1.05)', offset: 0.3 }), style({ opacity: .5, transform: 'scale(1.05)', offset: 0.3 }),
style({ opacity: 1, transform: 'scale(1)', offset: 1 }), style({ opacity: 1, transform: 'scale(1)', offset: 1 }),
])), ])),
]), ]),
]), ]),
], ],
}) })
export class MembershipsComponent implements OnInit { export class MembershipsComponent implements OnInit {
public loading: boolean = false; public loading: boolean = false;
public memberships!: Membership.AsObject[] | UserGrant.AsObject[]; public memberships!: Membership.AsObject[] | UserGrant.AsObject[];
public totalResult: number = 0; public totalResult: number = 0;
@Input() public auth: boolean = false; @Input() public auth: boolean = false;
@Input() public user!: User.AsObject; @Input() public user!: User.AsObject;
@Input() public disabled: boolean = false; @Input() public disabled: boolean = false;
constructor( constructor(
private authService: GrpcAuthService, private authService: GrpcAuthService,
private mgmtService: ManagementService, private mgmtService: ManagementService,
private adminService: AdminService, private adminService: AdminService,
private dialog: MatDialog, private dialog: MatDialog,
private toast: ToastService, private toast: ToastService,
private router: Router, private router: Router,
) { } ) { }
ngOnInit(): void { ngOnInit(): void {
this.loadManager(this.user.id); this.loadManager(this.user.id);
}
public async loadManager(userId: string): Promise<void> {
if (this.auth) {
this.authService.listMyUserGrants(100, 0, []).then(resp => {
this.memberships = resp.resultList;
this.totalResult = resp.details?.totalResult || 0;
this.loading = false;
});
} else {
this.mgmtService.listUserMemberships(userId, 100, 0, []).then(resp => {
this.memberships = resp.resultList;
this.totalResult = resp.details?.totalResult || 0;
this.loading = false;
});
} }
}
public async loadManager(userId: string): Promise<void> { public navigateToObject(): void {
if (this.auth) { if (!this.disabled) {
this.authService.listMyUserGrants(100, 0, []).then(resp => { this.router.navigate(['/users', this.user.id, 'memberships']);
this.memberships = resp.resultList; }
this.totalResult = resp.details?.totalResult || 0; }
this.loading = false;
}); public addMember(): void {
} else { const dialogRef = this.dialog.open(MemberCreateDialogComponent, {
this.mgmtService.listUserMemberships(userId, 100, 0, []).then(resp => { width: '400px',
this.memberships = resp.resultList; data: {
this.totalResult = resp.details?.totalResult || 0; user: this.user,
this.loading = false; },
}); });
dialogRef.afterClosed().subscribe(resp => {
if (resp && resp.creationType !== undefined) {
switch (resp.creationType) {
case CreationType.IAM:
this.createIamMember(resp);
break;
case CreationType.ORG:
this.createOrgMember(resp);
break;
case CreationType.PROJECT_OWNED:
this.createOwnedProjectMember(resp);
break;
case CreationType.PROJECT_GRANTED:
this.createGrantedProjectMember(resp);
break;
} }
} }
});
}
public navigateToObject(): void { public createIamMember(response: any): void {
if (!this.disabled) { const users: User.AsObject[] = response.users;
this.router.navigate(['/users', this.user.id, 'memberships']); const roles: string[] = response.roles;
}
}
public addMember(): void { if (users && users.length && roles && roles.length) {
const dialogRef = this.dialog.open(MemberCreateDialogComponent, { Promise.all(users.map(user => {
width: '400px', return this.adminService.addIAMMember(user.id, roles);
data: { })).then(() => {
user: this.user, this.toast.showInfo('IAM.TOAST.MEMBERADDED', true);
}, setTimeout(() => {
}); this.loadManager(this.user.id);
}, 1000);
dialogRef.afterClosed().subscribe(resp => { }).catch(error => {
if (resp && resp.creationType !== undefined) { this.toast.showError(error);
switch (resp.creationType) { });
case CreationType.IAM: }
this.createIamMember(resp); }
break;
case CreationType.ORG: private createOrgMember(response: any): void {
this.createOrgMember(resp); const users: User.AsObject[] = response.users;
break; const roles: string[] = response.roles;
case CreationType.PROJECT_OWNED:
this.createOwnedProjectMember(resp); if (users && users.length && roles && roles.length) {
break; Promise.all(users.map(user => {
case CreationType.PROJECT_GRANTED: return this.mgmtService.addOrgMember(user.id, roles);
this.createGrantedProjectMember(resp); })).then(() => {
break; this.toast.showInfo('ORG.TOAST.MEMBERADDED', true);
} setTimeout(() => {
} this.loadManager(this.user.id);
}, 1000);
}).catch(error => {
this.toast.showError(error);
});
}
}
private createGrantedProjectMember(response: any): void {
const users: User.AsObject[] = response.users;
const roles: string[] = response.roles;
if (users && users.length && roles && roles.length) {
users.forEach(user => {
return this.mgmtService.addProjectGrantMember(
response.projectId,
response.grantId,
user.id,
roles,
).then(() => {
this.toast.showInfo('PROJECT.TOAST.MEMBERADDED', true);
setTimeout(() => {
this.loadManager(this.user.id);
}, 1000);
}).catch(error => {
this.toast.showError(error);
}); });
});
} }
}
public createIamMember(response: any): void { private createOwnedProjectMember(response: any): void {
const users: User.AsObject[] = response.users; const users: User.AsObject[] = response.users;
const roles: string[] = response.roles; const roles: string[] = response.roles;
if (users && users.length && roles && roles.length) { if (users && users.length && roles && roles.length) {
Promise.all(users.map(user => { users.forEach(user => {
return this.adminService.addIAMMember(user.id, roles); return this.mgmtService.addProjectMember(response.projectId, user.id, roles)
})).then(() => { .then(() => {
this.toast.showInfo('IAM.TOAST.MEMBERADDED', true); this.toast.showInfo('PROJECT.TOAST.MEMBERADDED', true);
setTimeout(() => { setTimeout(() => {
this.loadManager(this.user.id); this.loadManager(this.user.id);
}, 1000); }, 1000);
}).catch(error => { }).catch(error => {
this.toast.showError(error); this.toast.showError(error);
}); });
} });
} }
}
private createOrgMember(response: any): void { getColor(type: Membership.AsObject[] | UserGrant.AsObject[]): string {
const users: User.AsObject[] = response.users; const gen = type.toString();
const roles: string[] = response.roles; const colors = [
'rgb(201, 115, 88)',
'rgb(226, 176, 50)',
'rgb(112, 89, 152)',
];
if (users && users.length && roles && roles.length) { let hash = 0;
Promise.all(users.map(user => { if (gen.length === 0) {
return this.mgmtService.addOrgMember(user.id, roles); return colors[hash];
})).then(() => {
this.toast.showInfo('ORG.TOAST.MEMBERADDED', true);
setTimeout(() => {
this.loadManager(this.user.id);
}, 1000);
}).catch(error => {
this.toast.showError(error);
});
}
} }
for (let i = 0; i < gen.length; i++) {
private createGrantedProjectMember(response: any): void { // tslint:disable-next-line: no-bitwise
const users: User.AsObject[] = response.users; hash = gen.charCodeAt(i) + ((hash << 5) - hash);
const roles: string[] = response.roles; // tslint:disable-next-line: no-bitwise
hash = hash & hash;
if (users && users.length && roles && roles.length) {
users.forEach(user => {
return this.mgmtService.addProjectGrantMember(
response.projectId,
response.grantId,
user.id,
roles,
).then(() => {
this.toast.showInfo('PROJECT.TOAST.MEMBERADDED', true);
setTimeout(() => {
this.loadManager(this.user.id);
}, 1000);
}).catch(error => {
this.toast.showError(error);
});
});
}
}
private createOwnedProjectMember(response: any): void {
const users: User.AsObject[] = response.users;
const roles: string[] = response.roles;
if (users && users.length && roles && roles.length) {
users.forEach(user => {
return this.mgmtService.addProjectMember(response.projectId, user.id, roles)
.then(() => {
this.toast.showInfo('PROJECT.TOAST.MEMBERADDED', true);
setTimeout(() => {
this.loadManager(this.user.id);
}, 1000);
}).catch(error => {
this.toast.showError(error);
});
});
}
}
getColor(type: Membership.AsObject[] | UserGrant.AsObject[]): string {
const gen = type.toString();
const colors = [
'rgb(201, 115, 88)',
'rgb(226, 176, 50)',
'rgb(112, 89, 152)',
];
let hash = 0;
if (gen.length === 0) {
return colors[hash];
}
for (let i = 0; i < gen.length; i++) {
// tslint:disable-next-line: no-bitwise
hash = gen.charCodeAt(i) + ((hash << 5) - hash);
// tslint:disable-next-line: no-bitwise
hash = hash & hash;
}
hash = ((hash % colors.length) + colors.length) % colors.length;
return colors[hash];
} }
hash = ((hash % colors.length) + colors.length) % colors.length;
return colors[hash];
}
} }

View File

@@ -51,59 +51,59 @@ import { UserDetailComponent } from './user-detail/user-detail.component';
import { UserMfaComponent } from './user-detail/user-mfa/user-mfa.component'; import { UserMfaComponent } from './user-detail/user-mfa/user-mfa.component';
@NgModule({ @NgModule({
declarations: [ declarations: [
AuthUserDetailComponent, AuthUserDetailComponent,
UserDetailComponent, UserDetailComponent,
EditDialogComponent, EditDialogComponent,
AuthUserMfaComponent, AuthUserMfaComponent,
AuthPasswordlessComponent, AuthPasswordlessComponent,
UserMfaComponent, UserMfaComponent,
PasswordlessComponent, PasswordlessComponent,
ThemeSettingComponent, ThemeSettingComponent,
PasswordComponent, PasswordComponent,
CodeDialogComponent, CodeDialogComponent,
MembershipsComponent, MembershipsComponent,
ExternalIdpsComponent, ExternalIdpsComponent,
ContactComponent, ContactComponent,
ResendEmailDialogComponent, ResendEmailDialogComponent,
DialogU2FComponent, DialogU2FComponent,
AuthFactorDialogComponent, AuthFactorDialogComponent,
], ],
imports: [ imports: [
UserDetailRoutingModule, UserDetailRoutingModule,
ChangesModule, ChangesModule,
CommonModule, CommonModule,
FormsModule, FormsModule,
ReactiveFormsModule, ReactiveFormsModule,
DetailFormModule, DetailFormModule,
DetailFormMachineModule, DetailFormMachineModule,
WarnDialogModule, WarnDialogModule,
MatDialogModule, MatDialogModule,
QRCodeModule, QRCodeModule,
MetaLayoutModule, MetaLayoutModule,
MatCheckboxModule, MatCheckboxModule,
HasRolePipeModule, HasRolePipeModule,
UserGrantsModule, UserGrantsModule,
MatButtonModule, MatButtonModule,
MatIconModule, MatIconModule,
CardModule, CardModule,
MatProgressSpinnerModule, MatProgressSpinnerModule,
MatProgressBarModule, MatProgressBarModule,
MatTooltipModule, MatTooltipModule,
HasRoleModule, HasRoleModule,
TranslateModule, TranslateModule,
MatTableModule, MatTableModule,
PaginatorModule, PaginatorModule,
SharedModule, SharedModule,
RefreshTableModule, RefreshTableModule,
CopyToClipboardModule, CopyToClipboardModule,
DetailLayoutModule, DetailLayoutModule,
PasswordComplexityViewModule, PasswordComplexityViewModule,
MemberCreateDialogModule, MemberCreateDialogModule,
TimestampToDatePipeModule, TimestampToDatePipeModule,
LocalizedDatePipeModule, LocalizedDatePipeModule,
InputModule, InputModule,
MachineKeysModule, MachineKeysModule,
], ],
}) })
export class UserDetailModule { } export class UserDetailModule { }

View File

@@ -44,7 +44,7 @@
<ng-template appHasRole [appHasRole]="['user.read$', 'user.read:'+user?.id]"> <ng-template appHasRole [appHasRole]="['user.read$', 'user.read:'+user?.id]">
<app-card *ngIf="user.human" title="{{ 'USER.PROFILE.TITLE' | translate }}"> <app-card *ngIf="user.human" title="{{ 'USER.PROFILE.TITLE' | translate }}">
<app-detail-form [disabled]="(canWrite$ | async) == false" [genders]="genders" [languages]="languages" <app-detail-form [preferredLoginName]="user.preferredLoginName" [disabled]="(canWrite$ | async) == false" [genders]="genders" [languages]="languages"
[username]="user.userName" [user]="user.human" (submitData)="saveProfile($event)"> [username]="user.userName" [user]="user.human" (submitData)="saveProfile($event)">
</app-detail-form> </app-detail-form>
</app-card> </app-card>

View File

@@ -35,7 +35,7 @@
<app-avatar <app-avatar
*ngIf="user.human && user.human.profile.displayName && user.human?.profile.firstName && user.human?.profile.lastName; else cog" *ngIf="user.human && user.human.profile.displayName && user.human?.profile.firstName && user.human?.profile.lastName; else cog"
class="avatar" [name]="user.human.profile.displayName" [size]="32"> class="avatar" [name]="user.human.profile.displayName" [forColor]="user?.preferredLoginName" [size]="32">
</app-avatar> </app-avatar>
<ng-template #cog> <ng-template #cog>
<div class="sa-icon" *ngIf="user.machine"> <div class="sa-icon" *ngIf="user.machine">

View File

@@ -3,10 +3,8 @@ import { Injectable } from '@angular/core';
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 { Org } from '../proto/generated/zitadel/org_pb';
import { StorageService } from './storage.service'; import { StorageService } from './storage.service';
const ORG_STORAGE_KEY = 'organization';
const authorizationKey = 'Authorization'; const authorizationKey = 'Authorization';
const orgKey = 'x-zitadel-orgid'; const orgKey = 'x-zitadel-orgid';
@@ -70,62 +68,64 @@ export const ENDPOINT = {
providedIn: 'root', providedIn: 'root',
}) })
export class AssetService { export class AssetService {
private serviceUrl: string = ''; private serviceUrl!: Promise<string>;
private accessToken: string = ''; private accessToken: string = '';
private org!: Org.AsObject;
constructor(private http: HttpClient, private storageService: StorageService) { constructor(private http: HttpClient, private storageService: StorageService) {
const aT = this.storageService.getItem(accessTokenStorageKey);
http.get('./assets/environment.json') if (aT) {
this.accessToken = aT;
}
this.serviceUrl = this.getServiceUrl();
}
private async getServiceUrl(): Promise<string> {
const url = await this.http.get('./assets/environment.json')
.toPromise().then((data: any) => { .toPromise().then((data: any) => {
if (data && data.uploadServiceUrl) { if (data && data.assetServiceUrl) {
this.serviceUrl = data.uploadServiceUrl; console.log(data.assetServiceUrl);
const aT = this.storageService.getItem(accessTokenStorageKey); return data.assetServiceUrl;
if (aT) {
this.accessToken = aT;
}
const org: Org.AsObject | null = (this.storageService.getItem(ORG_STORAGE_KEY));
if (org) {
this.org = org;
}
} }
}).catch(error => { }).catch(error => {
console.error(error); console.error(error);
}); });
return url;
} }
public upload(endpoint: AssetEndpoint, body: any): Promise<any> { public upload(endpoint: AssetEndpoint | string, body: any, orgId?: string): Promise<any> {
return this.http.post(`${this.serviceUrl}/assets/v1/${endpoint}`, const headers: any = {
body, [authorizationKey]: `${bearerPrefix} ${this.accessToken}`,
{ };
headers: {
[authorizationKey]: `${bearerPrefix} ${this.accessToken}`, if (orgId) {
[orgKey]: `${this.org.id}`, headers[orgKey] = `${orgId}`;
}, }
}).toPromise();
return this.serviceUrl.then((url) =>
this.http.post(`${url}/assets/v1/${endpoint}`,
body,
{
headers: headers,
}).toPromise(),
);
} }
public load(endpoint: string): Promise<any> { public load(endpoint: string, orgId?: string): Promise<any> {
return this.http.get(`${this.serviceUrl}/assets/v1/${endpoint}`, const headers: any = {
[authorizationKey]: `${bearerPrefix} ${this.accessToken}`,
};
{ if (orgId) {
responseType: 'blob', headers[orgKey] = `${orgId}`;
headers: { }
[authorizationKey]: `${bearerPrefix} ${this.accessToken}`,
[orgKey]: `${this.org.id}`,
},
}).toPromise();
}
public delete(endpoint: AssetEndpoint): Promise<any> { return this.serviceUrl.then((url) =>
return this.http.delete(`${this.serviceUrl}/assets/v1/${endpoint}`, this.http.get(`${url}/assets/v1/${endpoint}`,
{ {
headers: { responseType: 'blob',
[authorizationKey]: `${bearerPrefix} ${this.accessToken}`, headers: headers,
[orgKey]: `${this.org.id}`, }).toPromise(),
}, );
}).toPromise();
} }
} }

View File

@@ -4,70 +4,72 @@ import { BehaviorSubject, from, merge, Observable, of, Subject } from 'rxjs';
import { catchError, filter, finalize, map, mergeMap, switchMap, take, timeout } from 'rxjs/operators'; import { catchError, filter, finalize, map, mergeMap, switchMap, take, timeout } from 'rxjs/operators';
import { import {
AddMyAuthFactorOTPRequest, AddMyAuthFactorOTPRequest,
AddMyAuthFactorOTPResponse, AddMyAuthFactorOTPResponse,
AddMyAuthFactorU2FRequest, AddMyAuthFactorU2FRequest,
AddMyAuthFactorU2FResponse, AddMyAuthFactorU2FResponse,
AddMyPasswordlessRequest, AddMyPasswordlessRequest,
AddMyPasswordlessResponse, AddMyPasswordlessResponse,
GetMyEmailRequest, GetMyEmailRequest,
GetMyEmailResponse, GetMyEmailResponse,
GetMyPasswordComplexityPolicyRequest, GetMyPasswordComplexityPolicyRequest,
GetMyPasswordComplexityPolicyResponse, GetMyPasswordComplexityPolicyResponse,
GetMyPhoneRequest, GetMyPhoneRequest,
GetMyPhoneResponse, GetMyPhoneResponse,
GetMyProfileRequest, GetMyProfileRequest,
GetMyProfileResponse, GetMyProfileResponse,
GetMyUserRequest, GetMyUserRequest,
GetMyUserResponse, GetMyUserResponse,
ListMyAuthFactorsRequest, ListMyAuthFactorsRequest,
ListMyAuthFactorsResponse, ListMyAuthFactorsResponse,
ListMyLinkedIDPsRequest, ListMyLinkedIDPsRequest,
ListMyLinkedIDPsResponse, ListMyLinkedIDPsResponse,
ListMyPasswordlessRequest, ListMyPasswordlessRequest,
ListMyPasswordlessResponse, ListMyPasswordlessResponse,
ListMyProjectOrgsRequest, ListMyProjectOrgsRequest,
ListMyProjectOrgsResponse, ListMyProjectOrgsResponse,
ListMyUserChangesRequest, ListMyUserChangesRequest,
ListMyUserChangesResponse, ListMyUserChangesResponse,
ListMyUserGrantsRequest, ListMyUserGrantsRequest,
ListMyUserGrantsResponse, ListMyUserGrantsResponse,
ListMyUserSessionsRequest, ListMyUserSessionsRequest,
ListMyUserSessionsResponse, ListMyUserSessionsResponse,
ListMyZitadelFeaturesRequest, ListMyZitadelFeaturesRequest,
ListMyZitadelFeaturesResponse, ListMyZitadelFeaturesResponse,
ListMyZitadelPermissionsRequest, ListMyZitadelPermissionsRequest,
ListMyZitadelPermissionsResponse, ListMyZitadelPermissionsResponse,
RemoveMyAuthFactorOTPRequest, RemoveMyAuthFactorOTPRequest,
RemoveMyAuthFactorOTPResponse, RemoveMyAuthFactorOTPResponse,
RemoveMyAuthFactorU2FRequest, RemoveMyAuthFactorU2FRequest,
RemoveMyAuthFactorU2FResponse, RemoveMyAuthFactorU2FResponse,
RemoveMyLinkedIDPRequest, RemoveMyAvatarRequest,
RemoveMyLinkedIDPResponse, RemoveMyAvatarResponse,
RemoveMyPasswordlessRequest, RemoveMyLinkedIDPRequest,
RemoveMyPasswordlessResponse, RemoveMyLinkedIDPResponse,
RemoveMyPhoneRequest, RemoveMyPasswordlessRequest,
RemoveMyPhoneResponse, RemoveMyPasswordlessResponse,
ResendMyEmailVerificationRequest, RemoveMyPhoneRequest,
ResendMyEmailVerificationResponse, RemoveMyPhoneResponse,
ResendMyPhoneVerificationRequest, ResendMyEmailVerificationRequest,
ResendMyPhoneVerificationResponse, ResendMyEmailVerificationResponse,
SetMyEmailRequest, ResendMyPhoneVerificationRequest,
SetMyEmailResponse, ResendMyPhoneVerificationResponse,
SetMyPhoneRequest, SetMyEmailRequest,
SetMyPhoneResponse, SetMyEmailResponse,
UpdateMyPasswordRequest, SetMyPhoneRequest,
UpdateMyPasswordResponse, SetMyPhoneResponse,
UpdateMyProfileRequest, UpdateMyPasswordRequest,
UpdateMyProfileResponse, UpdateMyPasswordResponse,
VerifyMyAuthFactorOTPRequest, UpdateMyProfileRequest,
VerifyMyAuthFactorOTPResponse, UpdateMyProfileResponse,
VerifyMyAuthFactorU2FRequest, VerifyMyAuthFactorOTPRequest,
VerifyMyAuthFactorU2FResponse, VerifyMyAuthFactorOTPResponse,
VerifyMyPasswordlessRequest, VerifyMyAuthFactorU2FRequest,
VerifyMyPasswordlessResponse, VerifyMyAuthFactorU2FResponse,
VerifyMyPhoneRequest, VerifyMyPasswordlessRequest,
VerifyMyPhoneResponse, VerifyMyPasswordlessResponse,
VerifyMyPhoneRequest,
VerifyMyPhoneResponse,
} from '../proto/generated/zitadel/auth_pb'; } from '../proto/generated/zitadel/auth_pb';
import { ChangeQuery } from '../proto/generated/zitadel/change_pb'; import { ChangeQuery } from '../proto/generated/zitadel/change_pb';
import { ListQuery } from '../proto/generated/zitadel/object_pb'; import { ListQuery } from '../proto/generated/zitadel/object_pb';
@@ -78,443 +80,448 @@ import { StorageKey, StorageService } from './storage.service';
@Injectable({ @Injectable({
providedIn: 'root', providedIn: 'root',
}) })
export class GrpcAuthService { export class GrpcAuthService {
private _activeOrgChanged: Subject<Org.AsObject> = new Subject(); private _activeOrgChanged: Subject<Org.AsObject> = new Subject();
public user!: Observable<User.AsObject | undefined>; public user!: Observable<User.AsObject | undefined>;
private zitadelPermissions: BehaviorSubject<string[]> = new BehaviorSubject(['user.resourceowner']); private zitadelPermissions: BehaviorSubject<string[]> = new BehaviorSubject(['user.resourceowner']);
private zitadelFeatures: BehaviorSubject<string[]> = new BehaviorSubject(['']); private zitadelFeatures: BehaviorSubject<string[]> = new BehaviorSubject(['']);
public readonly fetchedZitadelPermissions: BehaviorSubject<boolean> = new BehaviorSubject(false as boolean); public readonly fetchedZitadelPermissions: BehaviorSubject<boolean> = new BehaviorSubject(false as boolean);
public readonly fetchedZitadelFeatures: BehaviorSubject<boolean> = new BehaviorSubject(false as boolean); public readonly fetchedZitadelFeatures: BehaviorSubject<boolean> = new BehaviorSubject(false as boolean);
private cachedOrgs: Org.AsObject[] = []; private cachedOrgs: Org.AsObject[] = [];
constructor( constructor(
private readonly grpcService: GrpcService, private readonly grpcService: GrpcService,
private oauthService: OAuthService, private oauthService: OAuthService,
private storage: StorageService, private storage: StorageService,
) { ) {
this.user = merge( this.user = merge(
of(this.oauthService.getAccessToken()).pipe( of(this.oauthService.getAccessToken()).pipe(
filter(token => token ? true : false), filter(token => token ? true : false),
), ),
this.oauthService.events.pipe( this.oauthService.events.pipe(
filter(e => e.type === 'token_received'), filter(e => e.type === 'token_received'),
timeout(this.oauthService.waitForTokenInMsec || 0), timeout(this.oauthService.waitForTokenInMsec || 0),
catchError(_ => of(null)), // timeout is not an error catchError(_ => of(null)), // timeout is not an error
map(_ => this.oauthService.getAccessToken()), map(_ => this.oauthService.getAccessToken()),
), ),
).pipe( ).pipe(
take(1), take(1),
mergeMap(() => { mergeMap(() => {
return from(this.getMyUser().then(resp => { return from(this.getMyUser().then(resp => {
const user = resp.user; const user = resp.user;
if (user) { if (user) {
return user; return user;
} else { } else {
return undefined; return undefined;
} }
})); }));
}), }),
finalize(() => { finalize(() => {
this.loadPermissions(); this.loadPermissions();
this.loadFeatures(); this.loadFeatures();
}), }),
); );
this.activeOrgChanged.subscribe(() => { this.activeOrgChanged.subscribe(() => {
this.loadPermissions(); this.loadPermissions();
this.loadFeatures(); this.loadFeatures();
}); });
}
public async getActiveOrg(id?: string): Promise<Org.AsObject> {
if (id) {
const org = this.storage.getItem<Org.AsObject>(StorageKey.organization);
if (org && this.cachedOrgs.find(tmp => tmp.id === org.id)) {
return org;
}
return Promise.reject(new Error('no cached org'));
} else {
let orgs = this.cachedOrgs;
if (orgs.length === 0) {
orgs = (await this.listMyProjectOrgs(10, 0)).resultList;
this.cachedOrgs = orgs;
}
const org = this.storage.getItem<Org.AsObject>(StorageKey.organization);
if (org && orgs.find(tmp => tmp.id === org.id)) {
return org;
}
if (orgs.length === 0) {
return Promise.reject(new Error('No organizations found!'));
}
const orgToSet = orgs.find(element => element.id !== '0' && element.name !== '');
if (orgToSet) {
this.setActiveOrg(orgToSet);
return Promise.resolve(orgToSet);
}
return Promise.resolve(orgs[0]);
}
}
public get activeOrgChanged(): Observable<Org.AsObject> {
return this._activeOrgChanged;
}
public setActiveOrg(org: Org.AsObject): void {
this.storage.setItem(StorageKey.organization, org);
this._activeOrgChanged.next(org);
}
private loadPermissions(): void {
from(this.listMyZitadelPermissions()).pipe(
map(rolesResp => rolesResp.resultList),
catchError(_ => {
return of([]);
}),
finalize(() => {
this.fetchedZitadelPermissions.next(true);
}),
).subscribe(roles => {
this.zitadelPermissions.next(roles);
});
}
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
*/
public isAllowed(roles: string[] | RegExp[]): Observable<boolean> {
if (roles && roles.length > 0) {
return this.zitadelPermissions.pipe(switchMap(zroles => of(this.hasRoles(zroles, roles))));
} else {
return of(false);
}
}
/**
* returns true if user has one of the provided roles
* @param userRoles roles of the user
* @param requestedRoles required roles for accessing the respective component
*/
public hasRoles(userRoles: string[], requestedRoles: string[] | RegExp[]): boolean {
return requestedRoles.findIndex((regexp: any) => {
return userRoles.findIndex(role => {
return new RegExp(regexp).test(role);
}) > -1;
}) > -1;
}
/**
* 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());
}
public getMyPasswordComplexityPolicy(): Promise<GetMyPasswordComplexityPolicyResponse.AsObject> {
return this.grpcService.auth.getMyPasswordComplexityPolicy(
new GetMyPasswordComplexityPolicyRequest(), null,
).then(resp => resp.toObject());
}
public getMyUser(): Promise<GetMyUserResponse.AsObject> {
return this.grpcService.auth.getMyUser(
new GetMyUserRequest(), null,
).then(resp => resp.toObject());
}
public listMyMultiFactors(): Promise<ListMyAuthFactorsResponse.AsObject> {
return this.grpcService.auth.listMyAuthFactors(
new ListMyAuthFactorsRequest(), null,
).then(resp => resp.toObject());
}
public listMyProjectOrgs(
limit: number,
offset: number,
queryList?: OrgQuery[],
): Promise<ListMyProjectOrgsResponse.AsObject> {
const req = new ListMyProjectOrgsRequest();
const metadata = new ListQuery();
if (offset) {
metadata.setOffset(offset);
}
if (limit) {
metadata.setLimit(limit);
}
if (queryList) {
req.setQueriesList(queryList);
} }
public async getActiveOrg(id?: string): Promise<Org.AsObject> { return this.grpcService.auth.listMyProjectOrgs(req, null).then(resp => resp.toObject());
if (id) { }
const org = this.storage.getItem<Org.AsObject>(StorageKey.organization);
if (org && this.cachedOrgs.find(tmp => tmp.id === org.id)) {
return org;
}
return Promise.reject(new Error('no cached org'));
} else {
let orgs = this.cachedOrgs;
if (orgs.length === 0) {
orgs = (await this.listMyProjectOrgs(10, 0)).resultList;
this.cachedOrgs = orgs;
}
const org = this.storage.getItem<Org.AsObject>(StorageKey.organization); public updateMyProfile(
if (org && orgs.find(tmp => tmp.id === org.id)) { firstName?: string,
return org; lastName?: string,
} nickName?: string,
displayName?: string,
if (orgs.length === 0) { preferredLanguage?: string,
return Promise.reject(new Error('No organizations found!')); gender?: Gender,
} ): Promise<UpdateMyProfileResponse.AsObject> {
const orgToSet = orgs.find(element => element.id !== '0' && element.name !== ''); const req = new UpdateMyProfileRequest();
if (firstName) {
if (orgToSet) { req.setFirstName(firstName);
this.setActiveOrg(orgToSet);
return Promise.resolve(orgToSet);
}
return Promise.resolve(orgs[0]);
}
} }
if (lastName) {
public get activeOrgChanged(): Observable<Org.AsObject> { req.setLastName(lastName);
return this._activeOrgChanged;
} }
if (nickName) {
public setActiveOrg(org: Org.AsObject): void { req.setNickName(nickName);
this.storage.setItem(StorageKey.organization, org);
this._activeOrgChanged.next(org);
} }
if (displayName) {
private loadPermissions(): void { req.setDisplayName(displayName);
from(this.listMyZitadelPermissions()).pipe(
map(rolesResp => rolesResp.resultList),
catchError(_ => {
return of([]);
}),
finalize(() => {
this.fetchedZitadelPermissions.next(true);
}),
).subscribe(roles => {
this.zitadelPermissions.next(roles);
});
} }
if (gender) {
private loadFeatures(): void { req.setGender(gender);
from(this.listMyZitadelFeatures()).pipe(
map(featuresResp => featuresResp.resultList),
catchError(_ => {
return of([]);
}),
finalize(() => {
this.fetchedZitadelFeatures.next(true);
}),
).subscribe(features => {
this.zitadelFeatures.next(features);
});
} }
if (preferredLanguage) {
/** req.setPreferredLanguage(preferredLanguage);
* returns true if user has one of the provided roles
* @param roles roles of the user
*/
public isAllowed(roles: string[] | RegExp[]): Observable<boolean> {
if (roles && roles.length > 0) {
return this.zitadelPermissions.pipe(switchMap(zroles => of(this.hasRoles(zroles, roles))));
} else {
return of(false);
}
} }
return this.grpcService.auth.updateMyProfile(req, null).then(resp => resp.toObject());
}
/** public get zitadelPermissionsChanged(): Observable<string[]> {
* returns true if user has one of the provided roles return this.zitadelPermissions;
* @param userRoles roles of the user }
* @param requestedRoles required roles for accessing the respective component
*/ public listMyUserSessions(): Promise<ListMyUserSessionsResponse.AsObject> {
public hasRoles(userRoles: string[], requestedRoles: string[] | RegExp[]): boolean { const req = new ListMyUserSessionsRequest();
return requestedRoles.findIndex((regexp: any) => { return this.grpcService.auth.listMyUserSessions(req, null).then(resp => resp.toObject());
return userRoles.findIndex(role => { }
return new RegExp(regexp).test(role);
}) > -1; public listMyUserGrants(limit?: number, offset?: number, queryList?: ListQuery[]):
}) > -1; Promise<ListMyUserGrantsResponse.AsObject> {
const req = new ListMyUserGrantsRequest();
const query = new ListQuery();
if (limit) {
query.setLimit(limit);
} }
if (offset) {
/** query.setOffset(offset);
* 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);
}
} }
req.setQuery(query);
return this.grpcService.auth.listMyUserGrants(req, null).then(resp => resp.toObject());
}
/** public getMyEmail(): Promise<GetMyEmailResponse.AsObject> {
* returns true if user has one of the provided features const req = new GetMyEmailRequest();
* @param userFeature features of the user return this.grpcService.auth.getMyEmail(req, null).then(resp => resp.toObject());
* @param requestedFeature required features for accessing the respective component }
*/
public hasFeature(userFeatures: string[], requestedFeatures: string[] | RegExp[]): boolean { public setMyEmail(email: string): Promise<SetMyEmailResponse.AsObject> {
return requestedFeatures.findIndex((regexp: any) => { const req = new SetMyEmailRequest();
return userFeatures.findIndex(feature => { req.setEmail(email);
return new RegExp(regexp).test(feature); return this.grpcService.auth.setMyEmail(req, null).then(resp => resp.toObject());
}) > -1; }
}) > -1;
public resendMyEmailVerification(): Promise<ResendMyEmailVerificationResponse.AsObject> {
const req = new ResendMyEmailVerificationRequest();
return this.grpcService.auth.resendMyEmailVerification(req, null).then(resp => resp.toObject());
}
public removeMyPhone(): Promise<RemoveMyPhoneResponse.AsObject> {
return this.grpcService.auth.removeMyPhone(
new RemoveMyPhoneRequest(), null,
).then(resp => resp.toObject());
}
public listMyZitadelPermissions(): Promise<ListMyZitadelPermissionsResponse.AsObject> {
return this.grpcService.auth.listMyZitadelPermissions(
new ListMyZitadelPermissionsRequest(), null,
).then(resp => resp.toObject());
}
public listMyZitadelFeatures(): Promise<ListMyZitadelFeaturesResponse.AsObject> {
return this.grpcService.auth.listMyZitadelFeatures(
new ListMyZitadelFeaturesRequest(), null,
).then(resp => resp.toObject());
}
public getMyPhone(): Promise<GetMyPhoneResponse.AsObject> {
return this.grpcService.auth.getMyPhone(
new GetMyPhoneRequest(), null,
).then(resp => resp.toObject());
}
public setMyPhone(phone: string): Promise<SetMyPhoneResponse.AsObject> {
const req = new SetMyPhoneRequest();
req.setPhone(phone);
return this.grpcService.auth.setMyPhone(req, null).then(resp => resp.toObject());
}
public resendMyPhoneVerification(): Promise<ResendMyPhoneVerificationResponse.AsObject> {
const req = new ResendMyPhoneVerificationRequest();
return this.grpcService.auth.resendMyPhoneVerification(req, null).then(resp => resp.toObject());
}
public updateMyPassword(oldPassword: string, newPassword: string): Promise<UpdateMyPasswordResponse.AsObject> {
const req = new UpdateMyPasswordRequest();
req.setOldPassword(oldPassword);
req.setNewPassword(newPassword);
return this.grpcService.auth.updateMyPassword(req, null).then(resp => resp.toObject());
}
public removeMyLinkedIDP(
externalUserId: string,
idpId: string,
): Promise<RemoveMyLinkedIDPResponse.AsObject> {
const req = new RemoveMyLinkedIDPRequest();
req.setLinkedUserId(externalUserId);
req.setIdpId(idpId);
return this.grpcService.auth.removeMyLinkedIDP(req, null).then(resp => resp.toObject());
}
public removeMyAvatar(): Promise<RemoveMyAvatarResponse.AsObject> {
const req = new RemoveMyAvatarRequest();
return this.grpcService.auth.removeMyAvatar(req, null).then(resp => resp.toObject());
}
public listMyLinkedIDPs(
limit: number,
offset: number,
): Promise<ListMyLinkedIDPsResponse.AsObject> {
const req = new ListMyLinkedIDPsRequest();
const metadata = new ListQuery();
if (limit) {
metadata.setLimit(limit);
} }
if (offset) {
public getMyProfile(): Promise<GetMyProfileResponse.AsObject> { metadata.setOffset(offset);
return this.grpcService.auth.getMyProfile(new GetMyProfileRequest(), null).then(resp => resp.toObject());
} }
req.setQuery(metadata);
return this.grpcService.auth.listMyLinkedIDPs(req, null).then(resp => resp.toObject());
}
public getMyPasswordComplexityPolicy(): Promise<GetMyPasswordComplexityPolicyResponse.AsObject> { public addMyMultiFactorOTP(): Promise<AddMyAuthFactorOTPResponse.AsObject> {
return this.grpcService.auth.getMyPasswordComplexityPolicy( return this.grpcService.auth.addMyAuthFactorOTP(
new GetMyPasswordComplexityPolicyRequest(), null, new AddMyAuthFactorOTPRequest(), null,
).then(resp => resp.toObject()); ).then(resp => resp.toObject());
}
public addMyMultiFactorU2F(): Promise<AddMyAuthFactorU2FResponse.AsObject> {
return this.grpcService.auth.addMyAuthFactorU2F(
new AddMyAuthFactorU2FRequest(), null,
).then(resp => resp.toObject());
}
public removeMyMultiFactorU2F(tokenId: string): Promise<RemoveMyAuthFactorU2FResponse.AsObject> {
const req = new RemoveMyAuthFactorU2FRequest();
req.setTokenId(tokenId);
return this.grpcService.auth.removeMyAuthFactorU2F(req, null).then(resp => resp.toObject());
}
public verifyMyMultiFactorU2F(credential: string, tokenname: string): Promise<VerifyMyAuthFactorU2FResponse.AsObject> {
const req = new VerifyMyAuthFactorU2FRequest();
const verification = new WebAuthNVerification();
verification.setPublicKeyCredential(credential);
verification.setTokenName(tokenname);
req.setVerification(verification);
return this.grpcService.auth.verifyMyAuthFactorU2F(req, null).then(resp => resp.toObject());
}
public listMyPasswordless(): Promise<ListMyPasswordlessResponse.AsObject> {
return this.grpcService.auth.listMyPasswordless(
new ListMyPasswordlessRequest(), null,
).then(resp => resp.toObject());
}
public addMyPasswordless(): Promise<AddMyPasswordlessResponse.AsObject> {
return this.grpcService.auth.addMyPasswordless(
new AddMyPasswordlessRequest(), null,
).then(resp => resp.toObject());
}
public removeMyPasswordless(tokenId: string): Promise<RemoveMyPasswordlessResponse.AsObject> {
const req = new RemoveMyPasswordlessRequest();
req.setTokenId(tokenId);
return this.grpcService.auth.removeMyPasswordless(req, null).then(resp => resp.toObject());
}
public verifyMyPasswordless(credential: string, tokenname: string): Promise<VerifyMyPasswordlessResponse.AsObject> {
const req = new VerifyMyPasswordlessRequest();
const verification = new WebAuthNVerification();
verification.setTokenName(tokenname);
verification.setPublicKeyCredential(credential);
req.setVerification(verification);
return this.grpcService.auth.verifyMyPasswordless(
req, null,
).then(resp => resp.toObject());
}
public removeMyMultiFactorOTP(): Promise<RemoveMyAuthFactorOTPResponse.AsObject> {
return this.grpcService.auth.removeMyAuthFactorOTP(
new RemoveMyAuthFactorOTPRequest(), null,
).then(resp => resp.toObject());
}
public verifyMyMultiFactorOTP(code: string): Promise<VerifyMyAuthFactorOTPResponse.AsObject> {
const req = new VerifyMyAuthFactorOTPRequest();
req.setCode(code);
return this.grpcService.auth.verifyMyAuthFactorOTP(req, null).then(resp => resp.toObject());
}
public verifyMyPhone(code: string): Promise<VerifyMyPhoneResponse.AsObject> {
const req = new VerifyMyPhoneRequest();
req.setCode(code);
return this.grpcService.auth.verifyMyPhone(req, null).then(resp => resp.toObject());
}
public listMyUserChanges(limit: number, sequence: number): Promise<ListMyUserChangesResponse.AsObject> {
const req = new ListMyUserChangesRequest();
const query = new ChangeQuery();
if (limit) {
query.setLimit(limit);
} }
if (sequence) {
public getMyUser(): Promise<GetMyUserResponse.AsObject> { query.setSequence(sequence);
return this.grpcService.auth.getMyUser(
new GetMyUserRequest(), null,
).then(resp => resp.toObject());
}
public listMyMultiFactors(): Promise<ListMyAuthFactorsResponse.AsObject> {
return this.grpcService.auth.listMyAuthFactors(
new ListMyAuthFactorsRequest(), null,
).then(resp => resp.toObject());
}
public listMyProjectOrgs(
limit: number,
offset: number,
queryList?: OrgQuery[],
): Promise<ListMyProjectOrgsResponse.AsObject> {
const req = new ListMyProjectOrgsRequest();
const metadata = new ListQuery();
if (offset) {
metadata.setOffset(offset);
}
if (limit) {
metadata.setLimit(limit);
}
if (queryList) {
req.setQueriesList(queryList);
}
return this.grpcService.auth.listMyProjectOrgs(req, null).then(resp => resp.toObject());
}
public updateMyProfile(
firstName?: string,
lastName?: string,
nickName?: string,
displayName?: string,
preferredLanguage?: string,
gender?: Gender,
): Promise<UpdateMyProfileResponse.AsObject> {
const req = new UpdateMyProfileRequest();
if (firstName) {
req.setFirstName(firstName);
}
if (lastName) {
req.setLastName(lastName);
}
if (nickName) {
req.setNickName(nickName);
}
if (displayName) {
req.setDisplayName(displayName);
}
if (gender) {
req.setGender(gender);
}
if (preferredLanguage) {
req.setPreferredLanguage(preferredLanguage);
}
return this.grpcService.auth.updateMyProfile(req, null).then(resp => resp.toObject());
}
public get zitadelPermissionsChanged(): Observable<string[]> {
return this.zitadelPermissions;
}
public listMyUserSessions(): Promise<ListMyUserSessionsResponse.AsObject> {
const req = new ListMyUserSessionsRequest();
return this.grpcService.auth.listMyUserSessions(req, null).then(resp => resp.toObject());
}
public listMyUserGrants(limit?: number, offset?: number, queryList?: ListQuery[]):
Promise<ListMyUserGrantsResponse.AsObject> {
const req = new ListMyUserGrantsRequest();
const query = new ListQuery();
if (limit) {
query.setLimit(limit);
}
if (offset) {
query.setOffset(offset);
}
req.setQuery(query);
return this.grpcService.auth.listMyUserGrants(req, null).then(resp => resp.toObject());
}
public getMyEmail(): Promise<GetMyEmailResponse.AsObject> {
const req = new GetMyEmailRequest();
return this.grpcService.auth.getMyEmail(req, null).then(resp => resp.toObject());
}
public setMyEmail(email: string): Promise<SetMyEmailResponse.AsObject> {
const req = new SetMyEmailRequest();
req.setEmail(email);
return this.grpcService.auth.setMyEmail(req, null).then(resp => resp.toObject());
}
public resendMyEmailVerification(): Promise<ResendMyEmailVerificationResponse.AsObject> {
const req = new ResendMyEmailVerificationRequest();
return this.grpcService.auth.resendMyEmailVerification(req, null).then(resp => resp.toObject());
}
public removeMyPhone(): Promise<RemoveMyPhoneResponse.AsObject> {
return this.grpcService.auth.removeMyPhone(
new RemoveMyPhoneRequest(), null,
).then(resp => resp.toObject());
}
public listMyZitadelPermissions(): Promise<ListMyZitadelPermissionsResponse.AsObject> {
return this.grpcService.auth.listMyZitadelPermissions(
new ListMyZitadelPermissionsRequest(), null,
).then(resp => resp.toObject());
}
public listMyZitadelFeatures(): Promise<ListMyZitadelFeaturesResponse.AsObject> {
return this.grpcService.auth.listMyZitadelFeatures(
new ListMyZitadelFeaturesRequest(), null,
).then(resp => resp.toObject());
}
public getMyPhone(): Promise<GetMyPhoneResponse.AsObject> {
return this.grpcService.auth.getMyPhone(
new GetMyPhoneRequest(), null,
).then(resp => resp.toObject());
}
public setMyPhone(phone: string): Promise<SetMyPhoneResponse.AsObject> {
const req = new SetMyPhoneRequest();
req.setPhone(phone);
return this.grpcService.auth.setMyPhone(req, null).then(resp => resp.toObject());
}
public resendMyPhoneVerification(): Promise<ResendMyPhoneVerificationResponse.AsObject> {
const req = new ResendMyPhoneVerificationRequest();
return this.grpcService.auth.resendMyPhoneVerification(req, null).then(resp => resp.toObject());
}
public updateMyPassword(oldPassword: string, newPassword: string): Promise<UpdateMyPasswordResponse.AsObject> {
const req = new UpdateMyPasswordRequest();
req.setOldPassword(oldPassword);
req.setNewPassword(newPassword);
return this.grpcService.auth.updateMyPassword(req, null).then(resp => resp.toObject());
}
public removeMyLinkedIDP(
externalUserId: string,
idpId: string,
): Promise<RemoveMyLinkedIDPResponse.AsObject> {
const req = new RemoveMyLinkedIDPRequest();
req.setLinkedUserId(externalUserId);
req.setIdpId(idpId);
return this.grpcService.auth.removeMyLinkedIDP(req, null).then(resp => resp.toObject());
}
public listMyLinkedIDPs(
limit: number,
offset: number,
): Promise<ListMyLinkedIDPsResponse.AsObject> {
const req = new ListMyLinkedIDPsRequest();
const metadata = new ListQuery();
if (limit) {
metadata.setLimit(limit);
}
if (offset) {
metadata.setOffset(offset);
}
req.setQuery(metadata);
return this.grpcService.auth.listMyLinkedIDPs(req, null).then(resp => resp.toObject());
}
public addMyMultiFactorOTP(): Promise<AddMyAuthFactorOTPResponse.AsObject> {
return this.grpcService.auth.addMyAuthFactorOTP(
new AddMyAuthFactorOTPRequest(), null,
).then(resp => resp.toObject());
}
public addMyMultiFactorU2F(): Promise<AddMyAuthFactorU2FResponse.AsObject> {
return this.grpcService.auth.addMyAuthFactorU2F(
new AddMyAuthFactorU2FRequest(), null,
).then(resp => resp.toObject());
}
public removeMyMultiFactorU2F(tokenId: string): Promise<RemoveMyAuthFactorU2FResponse.AsObject> {
const req = new RemoveMyAuthFactorU2FRequest();
req.setTokenId(tokenId);
return this.grpcService.auth.removeMyAuthFactorU2F(req, null).then(resp => resp.toObject());
}
public verifyMyMultiFactorU2F(credential: string, tokenname: string): Promise<VerifyMyAuthFactorU2FResponse.AsObject> {
const req = new VerifyMyAuthFactorU2FRequest();
const verification = new WebAuthNVerification();
verification.setPublicKeyCredential(credential);
verification.setTokenName(tokenname);
req.setVerification(verification);
return this.grpcService.auth.verifyMyAuthFactorU2F(req, null).then(resp => resp.toObject());
}
public listMyPasswordless(): Promise<ListMyPasswordlessResponse.AsObject> {
return this.grpcService.auth.listMyPasswordless(
new ListMyPasswordlessRequest(), null,
).then(resp => resp.toObject());
}
public addMyPasswordless(): Promise<AddMyPasswordlessResponse.AsObject> {
return this.grpcService.auth.addMyPasswordless(
new AddMyPasswordlessRequest(), null,
).then(resp => resp.toObject());
}
public removeMyPasswordless(tokenId: string): Promise<RemoveMyPasswordlessResponse.AsObject> {
const req = new RemoveMyPasswordlessRequest();
req.setTokenId(tokenId);
return this.grpcService.auth.removeMyPasswordless(req, null).then(resp => resp.toObject());
}
public verifyMyPasswordless(credential: string, tokenname: string): Promise<VerifyMyPasswordlessResponse.AsObject> {
const req = new VerifyMyPasswordlessRequest();
const verification = new WebAuthNVerification();
verification.setTokenName(tokenname);
verification.setPublicKeyCredential(credential);
req.setVerification(verification);
return this.grpcService.auth.verifyMyPasswordless(
req, null,
).then(resp => resp.toObject());
}
public removeMyMultiFactorOTP(): Promise<RemoveMyAuthFactorOTPResponse.AsObject> {
return this.grpcService.auth.removeMyAuthFactorOTP(
new RemoveMyAuthFactorOTPRequest(), null,
).then(resp => resp.toObject());
}
public verifyMyMultiFactorOTP(code: string): Promise<VerifyMyAuthFactorOTPResponse.AsObject> {
const req = new VerifyMyAuthFactorOTPRequest();
req.setCode(code);
return this.grpcService.auth.verifyMyAuthFactorOTP(req, null).then(resp => resp.toObject());
}
public verifyMyPhone(code: string): Promise<VerifyMyPhoneResponse.AsObject> {
const req = new VerifyMyPhoneRequest();
req.setCode(code);
return this.grpcService.auth.verifyMyPhone(req, null).then(resp => resp.toObject());
}
public listMyUserChanges(limit: number, sequence: number): Promise<ListMyUserChangesResponse.AsObject> {
const req = new ListMyUserChangesRequest();
const query = new ChangeQuery();
if (limit) {
query.setLimit(limit);
}
if (sequence) {
query.setSequence(sequence);
}
req.setQuery(query);
return this.grpcService.auth.listMyUserChanges(req, null).then(resp => resp.toObject());
} }
req.setQuery(query);
return this.grpcService.auth.listMyUserChanges(req, null).then(resp => resp.toObject());
}
} }

View File

@@ -14,69 +14,68 @@ import { OrgInterceptor } from './interceptors/org.interceptor';
import { StorageService } from './storage.service'; import { StorageService } from './storage.service';
@Injectable({ @Injectable({
providedIn: 'root', providedIn: 'root',
}) })
export class GrpcService { export class GrpcService {
public auth!: AuthServiceClient; public auth!: AuthServiceClient;
public mgmt!: ManagementServiceClient; public mgmt!: ManagementServiceClient;
public admin!: AdminServiceClient; public admin!: AdminServiceClient;
constructor( constructor(
private http: HttpClient, private http: HttpClient,
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 toast: ToastService, ) { }
) { }
public async loadAppEnvironment(): Promise<any> { public async loadAppEnvironment(): Promise<any> {
return this.http.get('./assets/environment.json') return this.http.get('./assets/environment.json')
.toPromise().then((data: any) => { .toPromise().then((data: any) => {
if (data && data.authServiceUrl && data.mgmtServiceUrl && data.issuer) { if (data && data.authServiceUrl && data.mgmtServiceUrl && data.issuer) {
const interceptors = { const interceptors = {
unaryInterceptors: [ unaryInterceptors: [
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(), new I18nInterceptor(),
], ],
}; };
this.auth = new AuthServiceClient( this.auth = new AuthServiceClient(
data.authServiceUrl, data.authServiceUrl,
null, null,
// @ts-ignore // @ts-ignore
interceptors, interceptors,
); );
this.mgmt = new ManagementServiceClient( this.mgmt = new ManagementServiceClient(
data.mgmtServiceUrl, data.mgmtServiceUrl,
null, null,
// @ts-ignore // @ts-ignore
interceptors, interceptors,
); );
this.admin = new AdminServiceClient( this.admin = new AdminServiceClient(
// TODO: replace with service url // TODO: replace with service url
data.mgmtServiceUrl, data.mgmtServiceUrl,
null, null,
// @ts-ignore // @ts-ignore
interceptors, interceptors,
); );
const authConfig: AuthConfig = { const authConfig: AuthConfig = {
scope: 'openid profile email', scope: 'openid profile email',
responseType: 'code', responseType: 'code',
oidc: true, oidc: true,
clientId: data.clientid, clientId: data.clientid,
issuer: data.issuer, issuer: data.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',
}; };
this.authenticationService.initConfig(authConfig); this.authenticationService.initConfig(authConfig);
} }
return Promise.resolve(data); return Promise.resolve(data);
}).catch(() => { }).catch(() => {
console.error('Failed to load environment from assets'); console.error('Failed to load environment from assets');
}); });
} }
} }

View File

@@ -0,0 +1,45 @@
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 { StorageService } from '../storage.service';
const orgKey = 'x-zitadel-orgid';
const ORG_STORAGE_KEY = 'organization';
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(ORG_STORAGE_KEY));
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;
}
}

View File

@@ -3,7 +3,7 @@
"mgmtServiceUrl": "https://api.zitadel.dev", "mgmtServiceUrl": "https://api.zitadel.dev",
"adminServiceUrl":"https://api.zitadel.dev", "adminServiceUrl":"https://api.zitadel.dev",
"subscriptionServiceUrl":"https://sub.zitadel.dev", "subscriptionServiceUrl":"https://sub.zitadel.dev",
"uploadServiceUrl":"https://api.zitadel.dev", "assetServiceUrl":"https://api.zitadel.dev",
"issuer": "https://issuer.zitadel.dev", "issuer": "https://issuer.zitadel.dev",
"clientid": "70669160379706195@zitadel" "clientid": "70669160379706195@zitadel"
} }

View File

@@ -306,7 +306,16 @@
"DISPLAYNAME": "Anzeigename", "DISPLAYNAME": "Anzeigename",
"PREFERRED_LANGUAGE": "Sprache", "PREFERRED_LANGUAGE": "Sprache",
"GENDER": "Geschlecht", "GENDER": "Geschlecht",
"PASSWORD": "Passwort" "PASSWORD": "Passwort",
"AVATAR": {
"UPLOADTITLE":"Profilfoto uploaden",
"UPLOADBTN":"Datei auswählen",
"UPLOAD":"Hochladen",
"CURRENT":"Aktuelles Bild",
"PREVIEW":"Vorschau",
"DELETESUCCESS":"Erfolgreich gelöscht!",
"CROPPERERROR":"Ein Fehler beim hochladen Ihrer Datei ist fehlgeschlagen. Versuchen Sie es mit ggf mit einem anderen Format und Grösse."
}
}, },
"MACHINE": { "MACHINE": {
"TITLE": "Details Service-Benutzer", "TITLE": "Details Service-Benutzer",
@@ -642,7 +651,7 @@
"PRIVATELABELING": { "PRIVATELABELING": {
"TITLE":"Private Labeling", "TITLE":"Private Labeling",
"DESCRIPTION":"Verleihe dem Login deinen benutzerdefinierten Style und passe das Verhalten an.", "DESCRIPTION":"Verleihe dem Login deinen benutzerdefinierten Style und passe das Verhalten an.",
"PREVIEW_DESCRIPTION":"Änderungen dieser Richtlinie werden automatisch in der Preview Umgebung verfügbar. Um die Preview zu Testen muss dem login flow der scope 'x-preview' mitgegeben werden.", "PREVIEW_DESCRIPTION":"Änderungen dieser Richtlinie werden automatisch in der Preview Umgebung verfügbar.",
"BTN":"Datei auswählen", "BTN":"Datei auswählen",
"ACTIVATEPREVIEW":"Konfiguration übernehmen", "ACTIVATEPREVIEW":"Konfiguration übernehmen",
"DARK":"Dunkler Modus", "DARK":"Dunkler Modus",
@@ -653,6 +662,8 @@
"COLORS":"Farben", "COLORS":"Farben",
"FONT":"Schrift", "FONT":"Schrift",
"ADVANCEDBEHAVIOR":"Erweitertes Verhalten", "ADVANCEDBEHAVIOR":"Erweitertes Verhalten",
"DROP":"Bild hier ablegen",
"RELEASE":"Jetzt loslassen",
"PREVIEW": { "PREVIEW": {
"TITLE":"Anmeldung", "TITLE":"Anmeldung",
"SECOND":"mit ZITADEL-Konto anmelden.", "SECOND":"mit ZITADEL-Konto anmelden.",

View File

@@ -306,7 +306,16 @@
"DISPLAYNAME": "Display Name", "DISPLAYNAME": "Display Name",
"PREFERRED_LANGUAGE": "Language", "PREFERRED_LANGUAGE": "Language",
"GENDER": "Gender", "GENDER": "Gender",
"PASSWORD": "Password" "PASSWORD": "Password",
"AVATAR": {
"UPLOADTITLE":"Upload your Profile Picture",
"UPLOADBTN":"Choose file",
"UPLOAD":"Upload",
"CURRENT":"Current Picture",
"PREVIEW":"Preview",
"DELETESUCCESS":"Deleted successfully!",
"CROPPERERROR":"An error while uploading your file failed. Try a different format and size if necessary."
}
}, },
"MACHINE": { "MACHINE": {
"TITLE": "Service User Details", "TITLE": "Service User Details",
@@ -642,7 +651,7 @@
"PRIVATELABELING": { "PRIVATELABELING": {
"TITLE":"Private Labeling", "TITLE":"Private Labeling",
"DESCRIPTION":"Give the login your personalized style and modify its behavior.", "DESCRIPTION":"Give the login your personalized style and modify its behavior.",
"PREVIEW_DESCRIPTION":"Changes of the policy will automatically deployed to preview environment. To view those changes, a 'x-preview' scope will have to be added to your login scopes.", "PREVIEW_DESCRIPTION":"Changes of the policy will automatically deployed to preview environment.",
"BTN":"Select File", "BTN":"Select File",
"ACTIVATEPREVIEW":"Set preview as current configuration", "ACTIVATEPREVIEW":"Set preview as current configuration",
"DARK":"Dark Mode", "DARK":"Dark Mode",
@@ -653,6 +662,8 @@
"COLORS":"Colors", "COLORS":"Colors",
"FONT":"Font", "FONT":"Font",
"ADVANCEDBEHAVIOR":"Advanced Behavior", "ADVANCEDBEHAVIOR":"Advanced Behavior",
"DROP":"Drop image here",
"RELEASE":"Release",
"PREVIEW": { "PREVIEW": {
"TITLE":"Login", "TITLE":"Login",
"SECOND":"login with your ZITADEL-Account.", "SECOND":"login with your ZITADEL-Account.",

View File

@@ -118,7 +118,6 @@ export class AuthenticationService {
public async authenticate( public async authenticate(
setState: boolean = true, setState: boolean = true,
): Promise<boolean> { ): Promise<boolean> {
console.log('auth');
this.oauthService.configure(this.authConfig); this.oauthService.configure(this.authConfig);
this.oauthService.strictDiscoveryDocumentValidation = false; this.oauthService.strictDiscoveryDocumentValidation = false;

2
go.mod
View File

@@ -69,7 +69,7 @@ require (
go.opentelemetry.io/otel/exporters/stdout v0.13.0 go.opentelemetry.io/otel/exporters/stdout v0.13.0
go.opentelemetry.io/otel/sdk v0.13.0 go.opentelemetry.io/otel/sdk v0.13.0
golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2 golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c // indirect golang.org/x/sync v0.0.0-20210220032951-036812b2e83c
golang.org/x/text v0.3.6 golang.org/x/text v0.3.6
golang.org/x/tools v0.1.0 golang.org/x/tools v0.1.0
google.golang.org/api v0.34.0 google.golang.org/api v0.34.0

View File

@@ -1,36 +1,62 @@
const avatars = document.getElementsByClassName('lgn-avatar'); const avatars = document.getElementsByClassName('lgn-avatar');
for (let i = 0; i < avatars.length; i++) { for (let i = 0; i < avatars.length; i++) {
const displayName = avatars[i].getAttribute('loginname'); const displayName = avatars[i].getAttribute('loginname');
if (displayName) { if (displayName) {
const username = displayName.split('@')[0]; const username = displayName.split('@')[0];
let separator = '_'; let separator = '_';
if (username.includes('-')) { if (username.includes('-')) {
separator = '-'; separator = '-';
}
if (username.includes('.')) {
separator = '.';
}
const split = username.split(separator);
const initials = split[0].charAt(0) + (split[1] ? split[1].charAt(0) : '');
avatars[i].getElementsByClassName('initials')[0].innerHTML = initials;
avatars[i].style.background = this.getColor(displayName);
// set default white text instead of contrast text mode
avatars[i].style.color = '#ffffff';
} }
if (username.includes('.')) {
separator = '.';
}
const split = username.split(separator);
const initials = split[0].charAt(0) + (split[1] ? split[1].charAt(0) : '');
avatars[i].getElementsByClassName('initials')[0].innerHTML = initials;
avatars[i].style.background = this.getColor(displayName);
// set default white text instead of contrast text mode
avatars[i].style.color = '#ffffff';
}
} }
function getColor(username) { function getColor(userName) {
const s = 40; const colors = [
const l = 50; 'linear-gradient(40deg, #B44D51 30%, rgb(241,138,138))',
const l2 = 62.5; 'linear-gradient(40deg, #B75073 30%, rgb(234,96,143))',
let hash = 0; 'linear-gradient(40deg, #84498E 30%, rgb(214,116,230))',
for (let i = 0; i < username.length; i++) { 'linear-gradient(40deg, #705998 30%, rgb(163,131,220))',
hash = username.charCodeAt(i) + ((hash << 5) - hash); 'linear-gradient(40deg, #5C6598 30%, rgb(135,148,222))',
} 'linear-gradient(40deg, #7F90D3 30%, rgb(181,196,247))',
'linear-gradient(40deg, #3E93B9 30%, rgb(150,215,245))',
'linear-gradient(40deg, #3494A0 30%, rgb(71,205,222))',
'linear-gradient(40deg, #25716A 30%, rgb(58,185,173))',
'linear-gradient(40deg, #427E41 30%, rgb(97,185,96))',
'linear-gradient(40deg, #89A568 30%, rgb(176,212,133))',
'linear-gradient(40deg, #90924D 30%, rgb(187,189,98))',
'linear-gradient(40deg, #E2B032 30%, rgb(245,203,99))',
'linear-gradient(40deg, #C97358 30%, rgb(245,148,118))',
'linear-gradient(40deg, #6D5B54 30%, rgb(152,121,108))',
'linear-gradient(40deg, #6B7980 30%, rgb(134,163,177))',
];
const h = hash % 360; let hash = 0;
const col1 = 'hsl(' + h + ', ' + s + '%, ' + l + '%)'; if (userName.length === 0) {
const col2 = 'hsl(' + h + ', ' + s + '%, ' + l2 + '%)'; return colors[hash];
return 'linear-gradient(40deg, ' + col1 + ' 30%, ' + col2 + ')'; }
hash = this.hashCode(userName);
return colors[hash % colors.length];
}
function hashCode(str, seed = 0) {
let h1 = 0xdeadbeef ^ seed, h2 = 0x41c6ce57 ^ seed;
for (let i = 0, ch; i < str.length; i++) {
ch = str.charCodeAt(i);
h1 = Math.imul(h1 ^ ch, 2654435761);
h2 = Math.imul(h2 ^ ch, 1597334677);
}
h1 = Math.imul(h1 ^ (h1 >>> 16), 2246822507) ^ Math.imul(h2 ^ (h2 >>> 13), 3266489909);
h2 = Math.imul(h2 ^ (h2 >>> 16), 2246822507) ^ Math.imul(h1 ^ (h1 >>> 13), 3266489909);
return 4294967296 * (2097151 & h2) + (h1 >>> 0);
} }