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
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
45 changed files with 11967 additions and 8113 deletions

16205
console/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

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

View File

@ -60,7 +60,7 @@
<div (clickOutside)="closeAccountCard()" class="icon-container">
<app-avatar
*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)"
[size]="38">
</app-avatar>

View File

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

View File

@ -20,6 +20,7 @@ export class AccountsCardComponent implements OnInit {
constructor(public authService: AuthenticationService, private router: Router, private userService: GrpcAuthService) {
this.userService.listMyUserSessions().then(sessions => {
this.sessions = sessions.resultList;
console.log(sessions.resultList);
const index = this.sessions.findIndex(user => user.loginName === this.user.preferredLoginName);
if (index > -1) {
this.sessions.splice(index, 1);

View File

@ -12,14 +12,14 @@ export class AvatarComponent implements OnInit {
@Input() fontSize: number = 14;
@Input() active: boolean = false;
@Input() color: string = '';
@Input() forColor: string = '';
constructor() { }
ngOnInit(): void {
if (!this.credentials) {
const split: string[] = this.name.split(' ');
this.credentials = split[0].charAt(0) + (split[1] ? split[1].charAt(0) : '');
if (!this.credentials && this.forColor) {
this.credentials = this.getInitials(this.forColor);
if (!this.color) {
this.color = this.getColor(this.name);
this.color = this.getColor(this.forColor || '');
}
}
@ -28,6 +28,20 @@ export class AvatarComponent implements OnInit {
}
}
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))',
@ -52,13 +66,22 @@ export class AvatarComponent implements OnInit {
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 = this.hashCode(userName);
return colors[hash % colors.length];
}
hash = ((hash % colors.length) + colors.length) % colors.length;
return colors[hash];
// 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">
<app-avatar matTooltip="{{ dayelement.editorDisplayName }}"
*ngIf="dayelement.editorDisplayName; else spacer" class="avatar"
[name]="dayelement.editorDisplayName" [size]="32">
[name]="dayelement.editorDisplayName" [size]="32" [forColor]="dayelement?.preferredLoginName">
</app-avatar>
<ng-template #spacer>
<div class="spacer"></div>

View File

@ -248,12 +248,14 @@ export class ChangesComponent implements OnInit, OnDestroy {
}
// Order by ascending property value
// tslint:disable
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}">
<app-avatar
*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)"
[size]="32">
</app-avatar>

View File

@ -22,7 +22,7 @@
<mat-checkbox [disabled]="!canWrite" color="primary" (click)="$event.stopPropagation()"
(change)="$event ? selection.toggle(row) : null" [checked]="selection.isSelected(row)">
<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>
<ng-template #cog>
<div class="sa-icon">

View File

@ -24,7 +24,7 @@
<div class="content" *ngIf="loginData">
<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">
{{'POLICY.DATA.ALLOWUSERNAMEPASSWORD' | translate}}
</mat-slide-toggle>
@ -62,7 +62,7 @@
</ng-template>
</div>
<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">
{{'POLICY.DATA.ALLOWEXTERNALIDP' | translate}}
</mat-slide-toggle>
@ -79,7 +79,7 @@
</ng-template>
</div>
<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">
{{'POLICY.DATA.FORCEMFA' | translate}}
</mat-slide-toggle>
@ -96,7 +96,7 @@
</ng-template>
</div>
<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">
{{'POLICY.DATA.HIDEPASSWORDRESET' | translate}}
</mat-slide-toggle>

View File

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

View File

@ -72,7 +72,6 @@
</div>
<span class="fill-space"></span>
<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"/>
</div>
</div>
@ -83,7 +82,6 @@
</div>
<span class="fill-space"></span>
<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"/>
</div>
</div>
@ -93,10 +91,10 @@
[class.hovering]="isHoveringOverDarkLogo">
<label class="file-label">
<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>
<span>{{isHoveringOverDarkLogo ? 'Release': 'Drop your Logo here'}}</span>
<span>{{isHoveringOverDarkLogo ? ('POLICY.PRIVATELABELING.RELEASE' | translate): ('POLICY.PRIVATELABELING.DROP' | translate)}}</span>
</label>
</div>
</div>
@ -133,10 +131,10 @@
[class.hovering]="isHoveringOverDarkIcon">
<label class="file-label">
<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>
<span>{{isHoveringOverDarkIcon ? 'Release': 'Drop your Logo here'}}</span>
<span>{{isHoveringOverDarkIcon ? ('POLICY.PRIVATELABELING.RELEASE' | translate): ('POLICY.PRIVATELABELING.DROP' | translate)}}</span>
</label>
</div>
</div>
@ -158,19 +156,19 @@
<ng-container *ngIf="theme==Theme.DARK">
<div class="colors" *ngIf="data && previewData">
<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 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 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 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>
</ng-container>
@ -178,15 +176,15 @@
<ng-container *ngIf="theme==Theme.LIGHT">
<div class="colors" *ngIf="data && previewData">
<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 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 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 class="color">
@ -214,7 +212,7 @@
<span>ABC • abc • 123</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 class="dropzone" cnslDropzone (hovered)="toggleHoverFont($event)"
@ -222,10 +220,10 @@
[class.hovering]="isHoveringOverFont">
<label class="file-label">
<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>
<span >{{isHoveringOverFont ? 'Release': 'Drop your Logo here'}}</span>
<span >{{isHoveringOverFont ? ('POLICY.PRIVATELABELING.RELEASE' | translate): ('POLICY.PRIVATELABELING.DROP' | translate)}}</span>
</label>
</div>
</div>

View File

@ -203,14 +203,20 @@
align-items: center;
.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;
}
.btn:not[disabled] {
cursor: pointer;
}
i {
font-size: 2.5rem;
}
span {
color: var(--grey);
}
}
.icon {
@ -274,6 +280,7 @@
max-width: 120px;
.dl-btn {
z-index: 2;
position: absolute;
top: -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 { ActivatedRoute } from '@angular/router';
import { Subscription } from 'rxjs';
import { switchMap } from 'rxjs/operators';
import { switchMap, take } from 'rxjs/operators';
import {
GetLabelPolicyResponse as AdminGetLabelPolicyResponse,
GetPreviewLabelPolicyResponse as AdminGetPreviewLabelPolicyResponse,
@ -15,10 +15,13 @@ import {
GetPreviewLabelPolicyResponse as MgmtGetPreviewLabelPolicyResponse,
UpdateCustomLabelPolicyRequest,
} 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 { AdminService } from 'src/app/services/admin.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 { StorageService } from 'src/app/services/storage.service';
import { ToastService } from 'src/app/services/toast.service';
import { CnslLinks } from '../../links/links.component';
@ -45,6 +48,8 @@ export enum ColorType {
BACKGROUNDLIGHT,
}
const ORG_STORAGE_KEY = 'organization';
@Component({
selector: 'app-private-labeling-policy',
templateUrl: './private-labeling-policy.component.html',
@ -86,14 +91,23 @@ export class PrivateLabelingPolicyComponent implements OnDestroy {
public refreshPreview: EventEmitter<void> = new EventEmitter();
public loadingImages: boolean = false;
private org!: Org.AsObject;
constructor(
private authService: GrpcAuthService,
private route: ActivatedRoute,
private toast: ToastService,
private injector: Injector,
private assetService: AssetService,
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.serviceType = data.serviceType;
@ -134,17 +148,17 @@ export class PrivateLabelingPolicyComponent implements OnDestroy {
if (theme === Theme.DARK) {
switch (this.serviceType) {
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:
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) {
switch (this.serviceType) {
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:
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);
switch (this.serviceType) {
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:
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) {
switch (this.serviceType) {
case PolicyComponentServiceType.MGMT:
this.handleUploadPromise(this.assetService.upload(AssetEndpoint.MGMTDARKICON, formData));
this.handleUploadPromise(this.assetService.upload(AssetEndpoint.MGMTDARKICON, formData, this.org.id));
break;
case PolicyComponentServiceType.ADMIN:
this.handleUploadPromise(this.assetService.upload(AssetEndpoint.IAMDARKICON, formData));
this.handleUploadPromise(this.assetService.upload(AssetEndpoint.IAMDARKICON, formData, this.org.id));
break;
}
}
if (theme === Theme.LIGHT) {
switch (this.serviceType) {
case PolicyComponentServiceType.MGMT:
this.handleUploadPromise(this.assetService.upload(AssetEndpoint.MGMTICON, formData));
this.handleUploadPromise(this.assetService.upload(AssetEndpoint.MGMTICON, formData, this.org.id));
break;
case PolicyComponentServiceType.ADMIN:
this.handleUploadPromise(this.assetService.upload(AssetEndpoint.IAMICON, formData));
this.handleUploadPromise(this.assetService.upload(AssetEndpoint.IAMICON, formData, this.org.id));
break;
}
}
@ -291,7 +305,7 @@ export class PrivateLabelingPolicyComponent implements OnDestroy {
}
private handleUploadPromise(task: Promise<any>): Promise<any> {
return task.then(() => {
const enhTask = task.then(() => {
this.toast.showInfo('POLICY.TOAST.UPLOADSUCCESS', true);
setTimeout(() => {
this.loadingImages = true;
@ -304,21 +318,31 @@ export class PrivateLabelingPolicyComponent implements OnDestroy {
});
}, 1000);
}).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 {
this.loading = true;
this.authService.canUseFeature(['label_policy.private_label']).pipe(take(1)).subscribe((canUse) => {
this.getPreviewData().then(data => {
console.log('preview', data);
this.loadingImages = true;
if (data.policy) {
this.previewData = data.policy;
this.loading = false;
if ((canUse === true && this.serviceType === PolicyComponentServiceType.MGMT) ||
this.serviceType === PolicyComponentServiceType.ADMIN) {
this.loadingImages = true;
this.loadPreviewImages();
}
}
}).catch(error => {
this.toast.showError(error);
});
@ -330,11 +354,17 @@ export class PrivateLabelingPolicyComponent implements OnDestroy {
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);
});
});
}
private loadImages(): void {
@ -451,7 +481,7 @@ export class PrivateLabelingPolicyComponent implements OnDestroy {
}
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);
this.images[imagekey] = this.sanitizer.bypassSecurityTrustUrl(objectURL);
this.refreshPreview.emit();
@ -462,7 +492,7 @@ export class PrivateLabelingPolicyComponent implements OnDestroy {
public removePolicy(): void {
if (this.service instanceof ManagementService) {
this.service.resetPasswordComplexityPolicyToDefault().then(() => {
this.service.resetLabelPolicyToDefault().then(() => {
this.toast.showInfo('POLICY.TOAST.RESETSUCCESS', true);
setTimeout(() => {
this.fetchData();
@ -473,14 +503,14 @@ export class PrivateLabelingPolicyComponent implements OnDestroy {
}
}
public savePolicy(): void {
public savePolicy(): Promise<any> {
switch (this.serviceType) {
case PolicyComponentServiceType.MGMT:
if ((this.previewData as LabelPolicy.AsObject).isDefault) {
const req0 = new AddCustomLabelPolicyRequest();
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);
}).catch((error: HttpErrorResponse) => {
this.toast.showError(error);
@ -489,22 +519,20 @@ export class PrivateLabelingPolicyComponent implements OnDestroy {
const req1 = new UpdateCustomLabelPolicyRequest();
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);
}).catch(error => {
this.toast.showError(error);
});
}
break;
case PolicyComponentServiceType.ADMIN:
const req = new UpdateLabelPolicyRequest();
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);
}).catch(error => {
this.toast.showError(error);
});
break;
}
}

View File

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

View File

@ -57,7 +57,7 @@
<div class="circle">
<app-avatar
*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>
<ng-template #cog>
<div class="sa-icon">

View File

@ -31,7 +31,7 @@
(change)="$event ? selection.toggle(row) : null" [checked]="selection.isSelected(row)">
<app-avatar
*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>
</mat-checkbox>
</td>

View File

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

View File

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

View File

@ -151,7 +151,6 @@ export class UserGrantCreateComponent implements OnDestroy {
});
break;
case UserGrantContext.NONE:
console.log('none');
let tempGrantId;
if ((this.project as GrantedProject.AsObject)?.grantId) {

View File

@ -28,7 +28,7 @@
</app-card>
<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)">
</app-detail-form>
</app-card>

View File

@ -48,6 +48,7 @@ export class AuthUserDetailComponent implements OnDestroy {
this.userService.getMyUser().then(resp => {
if (resp.user) {
this.user = resp.user;
console.log(resp.user);
}
this.loading = false;
}).catch(error => {

View File

@ -1,9 +1,22 @@
<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-label>{{ 'USER.PROFILE.USERNAME' | translate }}</cnsl-label>
<input cnslInput formControlName="userName" />
</cnsl-form-field>
</div>
<cnsl-form-field class="formfield">
<cnsl-label>{{ 'USER.PROFILE.FIRSTNAME' | translate }}</cnsl-label>
<input cnslInput formControlName="firstName" />

View File

@ -1,10 +1,60 @@
.content {
.user-form-content {
display: flex;
flex-direction: row;
flex-wrap: wrap;
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 {
flex: 1 1 33%;
margin: 0 .5rem;

View File

@ -1,8 +1,13 @@
import { Component, EventEmitter, Input, OnChanges, OnDestroy, Output } from '@angular/core';
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 { 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({
selector: 'app-detail-form',
@ -10,6 +15,8 @@ import { Gender, Human, User } from 'src/app/proto/generated/zitadel/user_pb';
styleUrls: ['./detail-form.component.scss'],
})
export class DetailFormComponent implements OnDestroy, OnChanges {
@Input() public showEditImage: boolean = false;
@Input() public preferredLoginName: string = '';
@Input() public username!: string;
@Input() public user!: Human.AsObject;
@Input() public disabled: boolean = false;
@ -18,11 +25,18 @@ export class DetailFormComponent implements OnDestroy, OnChanges {
@Output() public submitData: EventEmitter<User> = new EventEmitter<User>();
@Output() public changedLanguage: EventEmitter<string> = new EventEmitter<string>();
public profilePic: any = null;
public profileForm!: FormGroup;
private sub: Subscription = new Subscription();
constructor(private fb: FormBuilder) {
constructor(
private fb: FormBuilder,
private dialog: MatDialog,
private assetService: AssetService,
private toast: ToastService,
private sanitizer: DomSanitizer,
) {
this.profileForm = this.fb.group({
userName: [{ value: '', disabled: true }, [
Validators.required,
@ -34,6 +48,8 @@ export class DetailFormComponent implements OnDestroy, OnChanges {
gender: [{ value: 0, disabled: this.disabled }],
preferredLanguage: [{ value: '', disabled: this.disabled }],
});
this.loadAvatar();
}
public ngOnChanges(): void {
@ -66,6 +82,29 @@ export class DetailFormComponent implements OnDestroy, OnChanges {
this.submitData.emit(this.profileForm.value);
}
public openUploadDialog(): void {
const dialogRef = this.dialog.open(ProfilePictureComponent, {
data: {
profilePic: this.profilePic,
},
width: '400px',
});
dialogRef.afterClosed().subscribe(resp => {
if (resp) {
}
});
}
public loadAvatar(): Promise<any> {
return this.assetService.load(`users/me/avatar`).then(data => {
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');
}

View File

@ -4,22 +4,32 @@ import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { MatButtonModule } from '@angular/material/button';
import { MatIconModule } from '@angular/material/icon';
import { MatSelectModule } from '@angular/material/select';
import { MatTooltipModule } from '@angular/material/tooltip';
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 { DetailFormComponent } from './detail-form.component';
import { ProfilePictureComponent } from './profile-picture/profile-picture.component';
@NgModule({
declarations: [
DetailFormComponent,
ProfilePictureComponent,
],
imports: [
DropzoneModule,
AvatarModule,
CommonModule,
FormsModule,
ReactiveFormsModule,
ImageCropperModule,
TranslateModule,
MatSelectModule,
MatButtonModule,
MatTooltipModule,
MatIconModule,
TranslateModule,
InputModule,

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

@ -44,7 +44,7 @@
<ng-template appHasRole [appHasRole]="['user.read$', 'user.read:'+user?.id]">
<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)">
</app-detail-form>
</app-card>

View File

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

View File

@ -42,6 +42,8 @@ import {
RemoveMyAuthFactorOTPResponse,
RemoveMyAuthFactorU2FRequest,
RemoveMyAuthFactorU2FResponse,
RemoveMyAvatarRequest,
RemoveMyAvatarResponse,
RemoveMyLinkedIDPRequest,
RemoveMyLinkedIDPResponse,
RemoveMyPasswordlessRequest,
@ -412,6 +414,11 @@ export class GrpcAuthService {
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,

View File

@ -27,7 +27,6 @@ export class GrpcService {
private authenticationService: AuthenticationService,
private storageService: StorageService,
private dialog: MatDialog,
// private toast: ToastService,
) { }
public async loadAppEnvironment(): Promise<any> {

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",
"adminServiceUrl":"https://api.zitadel.dev",
"subscriptionServiceUrl":"https://sub.zitadel.dev",
"uploadServiceUrl":"https://api.zitadel.dev",
"assetServiceUrl":"https://api.zitadel.dev",
"issuer": "https://issuer.zitadel.dev",
"clientid": "70669160379706195@zitadel"
}

View File

@ -306,7 +306,16 @@
"DISPLAYNAME": "Anzeigename",
"PREFERRED_LANGUAGE": "Sprache",
"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": {
"TITLE": "Details Service-Benutzer",
@ -642,7 +651,7 @@
"PRIVATELABELING": {
"TITLE":"Private Labeling",
"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",
"ACTIVATEPREVIEW":"Konfiguration übernehmen",
"DARK":"Dunkler Modus",
@ -653,6 +662,8 @@
"COLORS":"Farben",
"FONT":"Schrift",
"ADVANCEDBEHAVIOR":"Erweitertes Verhalten",
"DROP":"Bild hier ablegen",
"RELEASE":"Jetzt loslassen",
"PREVIEW": {
"TITLE":"Anmeldung",
"SECOND":"mit ZITADEL-Konto anmelden.",

View File

@ -306,7 +306,16 @@
"DISPLAYNAME": "Display Name",
"PREFERRED_LANGUAGE": "Language",
"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": {
"TITLE": "Service User Details",
@ -642,7 +651,7 @@
"PRIVATELABELING": {
"TITLE":"Private Labeling",
"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",
"ACTIVATEPREVIEW":"Set preview as current configuration",
"DARK":"Dark Mode",
@ -653,6 +662,8 @@
"COLORS":"Colors",
"FONT":"Font",
"ADVANCEDBEHAVIOR":"Advanced Behavior",
"DROP":"Drop image here",
"RELEASE":"Release",
"PREVIEW": {
"TITLE":"Login",
"SECOND":"login with your ZITADEL-Account.",

View File

@ -118,7 +118,6 @@ export class AuthenticationService {
public async authenticate(
setState: boolean = true,
): Promise<boolean> {
console.log('auth');
this.oauthService.configure(this.authConfig);
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/sdk v0.13.0
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/tools v0.1.0
google.golang.org/api v0.34.0

View File

@ -20,17 +20,43 @@ for (let i = 0; i < avatars.length; i++) {
}
}
function getColor(username) {
const s = 40;
const l = 50;
const l2 = 62.5;
function getColor(userName) {
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;
for (let i = 0; i < username.length; i++) {
hash = username.charCodeAt(i) + ((hash << 5) - hash);
if (userName.length === 0) {
return colors[hash];
}
const h = hash % 360;
const col1 = 'hsl(' + h + ', ' + s + '%, ' + l + '%)';
const col2 = 'hsl(' + h + ', ' + s + '%, ' + l2 + '%)';
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);
}