cherry pick changes from main (#3371)

* feat: remove exif data from uploaded images (#3221)

* feat: remove exif tags from images

* feat: remove exif data

* feat: remove exif

* fix: add preferredLoginName to user grant response (#3271)

* chore: log webauthn parse error (#3272)

* log error

* log error

* feat: Help link in privacy policy

* fix: convert correct detail data on organization (#3279)

* fix: handle empty editor users

* fix: add some missing translations (#3291)

* fix: org policy translations

* fix: metadata event types translation

* fix: translations

* fix: filter resource owner correctly on project grant members (#3281)

* fix: filter resource owner correctly on project grant members

* fix: filter resource owner correctly on project grant members

* fix: add orgIDs to zitadel permissions request

Co-authored-by: fabi <fabienne.gerschwiler@gmail.com>

* fix: get IAM memberships correctly in MyZitadelPermissions (#3309)

* fix: correct login names on auth and notification users (#3349)

* fix: correct login names on auth and notification users

* fix: migration

* fix: handle resource owner in action flows (#3361)

* fix merge

* fix: exchange exif library (#3366)

* fix: exchange exif library

* ignore tiffs

* requested fixes

* feat: Help link in privacy policy

Co-authored-by: Fabi <38692350+fgerschwiler@users.noreply.github.com>
Co-authored-by: fabi <fabienne.gerschwiler@gmail.com>
This commit is contained in:
Livio Amstutz
2022-03-24 14:00:24 +01:00
committed by GitHub
parent 56b916a2b0
commit 504fe5b761
84 changed files with 1055 additions and 602 deletions

View File

@@ -1,31 +1,42 @@
<div *ngIf="currentMap">
<div *ngIf="currentMap">
<form [formGroup]="form" >
<form [formGroup]="form">
<ng-container *ngFor="let key of (current$ | async) | keyvalue">
<div class="block">
<div class="flex" *ngIf="(default$ | async) as defaultmap">
<cnsl-form-field class="formfield" >
<cnsl-form-field class="formfield">
<cnsl-label>{{key.key}}</cnsl-label>
<textarea class="text" cnslInput [formControlName]="key.key" [placeholder]="defaultmap[key.key]" [name]="key.key" [ngClass]="{'defaulttext': form.get(key.key)?.value === ''}"></textarea>
<textarea class="text" cnslInput [formControlName]="key.key" [placeholder]="defaultmap[key.key]"
[name]="key.key" [ngClass]="{'defaulttext': form.get(key.key)?.value === ''}"></textarea>
<div class="chips" *ngIf="warnText[key.key] === undefined">
<ng-container *ngFor="let chip of chips" >
<div class="chip" cnslCopyToClipboard [valueToCopy]="chip.value" (copiedValue)="copied = $event" (click)="addChip(key.key, chip.value)">
<span class="key">{{chip.key | translate}}</span>
<span class="value">{{chip.value}}</span>
<i *ngIf="copied !== chip.value" class="las la-clipboard"></i>
<i *ngIf="copied === chip.value" class="las la-clipboard-check"></i>
</div>
</ng-container>
<ng-container *ngFor="let chip of chips">
<div class="chip" cnslCopyToClipboard [valueToCopy]="chip.value" (copiedValue)="copied = $event"
(click)="addChip(key.key, chip.value)">
<span class="key">{{chip.key | translate}}</span>
<span class="value">{{chip.value}}</span>
<i *ngIf="copied !== chip.value" class="las la-clipboard"></i>
<i *ngIf="copied === chip.value" class="las la-clipboard-check"></i>
</div>
</ng-container>
</div>
</cnsl-form-field>
<div class="actions">
<button matTooltip="{{'ACTIONS.RESETDEFAULT'| translate }}" mat-icon-button [disabled]="form.get(key.key)?.value === defaultmap[key.key] || disabled" (click)="form.get(key.key)?.setValue(defaultmap[key.key])" (mouseenter) = "form.get(key.key)?.value !== defaultmap[key.key] && setWarnText(key.key, defaultmap[key.key])" (mouseleave) ="setWarnText(key.key, undefined)"><i class="las la-history"></i></button>
<button matTooltip="{{'ACTIONS.RESETCURRENT'| translate }}" mat-icon-button [disabled]="form.get(key.key)?.value === currentMap[key.key] || disabled" (click)="form.get(key.key)?.setValue(currentMap[key.key])" (mouseenter) = "form.get(key.key)?.value !== currentMap[key.key] && setWarnText(key.key, currentMap[key.key])" (mouseleave) ="setWarnText(key.key, undefined)"><i class="las la-undo"></i></button>
<button matTooltip="{{'ACTIONS.RESETDEFAULT'| translate }}" mat-icon-button
[disabled]="form.get(key.key)?.value === defaultmap[key.key] || disabled"
(click)="form.get(key.key)?.setValue(defaultmap[key.key])"
(mouseenter)="form.get(key.key)?.value !== defaultmap[key.key] && setWarnText(key.key, defaultmap[key.key])"
(mouseleave)="setWarnText(key.key, undefined)"><i class="las la-history"></i></button>
<button matTooltip="{{'ACTIONS.RESETCURRENT'| translate }}" mat-icon-button
[disabled]="form.get(key.key)?.value === currentMap[key.key] || disabled"
(click)="form.get(key.key)?.setValue(currentMap[key.key])"
(mouseenter)="form.get(key.key)?.value !== currentMap[key.key] && setWarnText(key.key, currentMap[key.key])"
(mouseleave)="setWarnText(key.key, undefined)"><i class="las la-undo"></i></button>
</div>
</div>
</div>
<cnsl-info-section *ngIf="warnText[key.key] !== undefined" class="info" [type]="InfoSectionType.WARN">{{'ACTIONS.RESETTO'| translate }} <cite>'{{warnText[key.key]}}'</cite></cnsl-info-section>
<cnsl-info-section *ngIf="warnText[key.key] !== undefined" class="info" [type]="InfoSectionType.WARN">
{{'ACTIONS.RESETTO'| translate }} <cite>'{{warnText[key.key]}}'</cite></cnsl-info-section>
</ng-container>
</form>
</div>

View File

@@ -71,7 +71,6 @@ export function mapRequestValues(map: Partial<Map>, req: Req): Req {
const r3 = new FooterText();
r3.setHelp(map.footerText?.help ?? '');
r3.setHelpLink(map.footerText?.helpLink ?? '');
r3.setPrivacyPolicy(map.footerText?.privacyPolicy ?? '');
r3.setTos(map.footerText?.tos ?? '');
req.setFooterText(r3);
@@ -102,7 +101,6 @@ export function mapRequestValues(map: Partial<Map>, req: Req): Req {
r6.setU2fOption(map.initMfaPromptText?.otpOption ?? '');
req.setInitMfaPromptText(r6);
const r7 = new InitMFAU2FScreenText();
r7.setDescription(map.initMfaU2fText?.description ?? '');
r7.setErrorRetry(map.initMfaU2fText?.errorRetry ?? '');
@@ -112,7 +110,6 @@ export function mapRequestValues(map: Partial<Map>, req: Req): Req {
r7.setTokenNameLabel(map.initMfaU2fText?.tokenNameLabel ?? '');
req.setInitMfaU2fText(r7);
const r8 = new InitPasswordDoneScreenText();
r8.setCancelButtonText(map.initPasswordDoneText?.cancelButtonText ?? '');
r8.setDescription(map.initPasswordDoneText?.description ?? '');
@@ -348,7 +345,6 @@ export function mapRequestValues(map: Partial<Map>, req: Req): Req {
r32.setTokenNameLabel(map.passwordlessRegistrationText?.tokenNameLabel ?? '');
req.setPasswordlessRegistrationText(r32);
const r33 = new PasswordlessScreenText();
r33.setDescription(map.passwordlessText?.description ?? '');
r33.setErrorRetry(map.passwordlessText?.errorRetry ?? '');

View File

@@ -1,35 +1,59 @@
<cnsl-detail-layout [backRouterLink]="[ serviceType === PolicyComponentServiceType.ADMIN ? '/iam/policies' : '/org']"
[title]="'POLICY.PRIVACY_POLICY.TITLE' | translate"
[description]="'POLICY.PRIVACY_POLICY.DESCRIPTION' | translate">
[title]="'POLICY.PRIVACY_POLICY.TITLE' | translate" [description]="'POLICY.PRIVACY_POLICY.DESCRIPTION' | translate">
<cnsl-info-section *ngIf="isDefault"> {{'POLICY.DEFAULTLABEL' | translate}}</cnsl-info-section>
<cnsl-info-section *ngIf="serviceType === PolicyComponentServiceType.MGMT && (['privacy_policy'] | hasFeature | async) === false" [featureLink]="['/org/features']" class="info" [type]="InfoSectionType.WARN">
<cnsl-info-section
*ngIf="serviceType === PolicyComponentServiceType.MGMT && (['privacy_policy'] | hasFeature | async) === false"
[featureLink]="['/org/features']" class="info" [type]="InfoSectionType.WARN">
<span [innerHTML]="'FEATURES.NOTAVAILABLE' | translate: ({value: 'privacy_policy'})"></span>
</cnsl-info-section>
<div class="divider"></div>
<div class="content" >
<div class="content">
<form *ngIf="form" [formGroup]="form">
<cnsl-form-field class="formfield">
<cnsl-label>{{ 'POLICY.PRIVACY_POLICY.TOSLINK' | translate }}</cnsl-label>
<input cnslInput name="tosLink" formControlName="tosLink" />
<cnsl-form-field class="privacy-policy-formfield">
<cnsl-label>{{ 'POLICY.PRIVACY_POLICY.TOSLINK' | translate }}</cnsl-label>
<input cnslInput name="tosLink" formControlName="tosLink" />
<template [ngTemplateOutlet]="templateRef" [ngTemplateOutletContext]="{key: 'tosLink'}"></template>
</cnsl-form-field>
<cnsl-form-field class="formfield">
<cnsl-form-field class="privacy-policy-formfield">
<cnsl-label>{{ 'POLICY.PRIVACY_POLICY.POLICYLINK' | translate }}</cnsl-label>
<input cnslInput name="privacyLink" formControlName="privacyLink" />
<template [ngTemplateOutlet]="templateRef" [ngTemplateOutletContext]="{key: 'privacyLink'}"></template>
</cnsl-form-field>
<cnsl-form-field class="privacy-policy-formfield">
<cnsl-label>{{ 'POLICY.PRIVACY_POLICY.HELPLINK' | translate }}</cnsl-label>
<input cnslInput name="helpLink" formControlName="helpLink" />
<template [ngTemplateOutlet]="templateRef" [ngTemplateOutletContext]="{key: 'helpLink'}"></template>
</cnsl-form-field>
</form>
</div>
<div class="actions">
<button *ngIf="!privacyPolicy?.isDefault" class="reset-button" [disabled]="serviceType === PolicyComponentServiceType.MGMT && (['privacy_policy'] | hasFeature | async) === false" (click)="resetDefault()" color="warn" type="submit"
mat-stroked-button><i class="las la-history"></i> {{ 'ACTIONS.RESETDEFAULT' | translate }}</button>
<button class="save-button" [disabled]="serviceType === PolicyComponentServiceType.MGMT && (['privacy_policy'] | hasFeature | async) === false" (click)="saveCurrentMessage()" color="primary" type="submit"
mat-raised-button>{{ 'ACTIONS.SAVE' | translate }}</button>
<button *ngIf="privacyPolicy && privacyPolicy.isDefault === false" class="reset-button"
[disabled]="serviceType === PolicyComponentServiceType.MGMT && (['privacy_policy'] | hasFeature | async) === false"
(click)="resetDefault()" color="warn" type="submit" mat-stroked-button><i class="las la-history"></i> {{
'ACTIONS.RESETDEFAULT' | translate }}</button>
<button class="save-button"
[disabled]="serviceType === PolicyComponentServiceType.MGMT && (['privacy_policy'] | hasFeature | async) === false"
(click)="saveCurrentMessage()" color="primary" type="submit" mat-raised-button>{{ 'ACTIONS.SAVE' | translate
}}</button>
</div>
<cnsl-policy-grid [currentPolicy]="currentPolicy" [type]="serviceType" tagForFilter="text"></cnsl-policy-grid>
<cnsl-policy-grid [currentPolicy]="currentPolicy" [type]="serviceType" tagForFilter="text"></cnsl-policy-grid>
</cnsl-detail-layout>
<ng-template #templateRef let-key="key">
<div class="chips">
<div class="chip" cnslCopyToClipboard [valueToCopy]="LANGPLACEHOLDER" (copiedValue)="copied = $event"
(click)="addChip(key, LANGPLACEHOLDER)">
<span class="key">{{LANGPLACEHOLDER}}</span>
<i *ngIf="copied !== LANGPLACEHOLDER" class="las la-clipboard"></i>
<i *ngIf="copied === LANGPLACEHOLDER" class="las la-clipboard-check"></i>
</div>
</div>
</ng-template>

View File

@@ -1,26 +1,26 @@
.spinner-wr {
margin: .5rem 0;
margin: 0.5rem 0;
}
.top-actions {
display: flex;
margin: 0 -.5rem;
margin: 0 -0.5rem;
flex-wrap: wrap;
.keys {
flex: 1;
margin: 0 .5rem;
margin: 0 0.5rem;
min-width: 150px;
}
.language {
margin: 0 .5rem;
margin: 0 0.5rem;
min-width: 150px;
.lighter {
font-size: 12px;
color: var(--grey);
padding: 0 .5rem;
padding: 0 0.5rem;
}
}
}
@@ -34,40 +34,66 @@
padding-top: 1rem;
}
.chips {
display: flex;
margin: 0 -.25rem;
.chip {
border-radius: 50vw;
padding: 2px .5rem;
font-size: 12px;
background: #cbf4c9;
color: #0e6245;
margin: .25rem;
.privacy-policy-formfield {
.chips {
display: flex;
align-items: center;
justify-content: center;
z-index: 10;
flex-wrap: wrap;
opacity: 0;
margin: 0 -0.25rem;
transition: all 0.2s ease;
.chip {
border-radius: 50vw;
padding: 4px 0.5rem;
font-size: 12px;
background: #5282c1;
color: white;
margin: 0.25rem;
display: flex;
align-items: center;
justify-content: center;
z-index: 10;
* {
transition: all 0.2s ease;
}
i {
opacity: 0.5;
font-size: 1.1rem;
margin-left: 0.5rem;
}
.key {
display: inline-block;
}
}
}
&.cnsl-focused {
.chips {
opacity: 1;
cursor: copy;
}
}
}
.divider {
width: 100%;
height: 1px;
background-color: rgba(#81868a, .5);
background-color: rgba(#81868a, 0.5);
margin: 1.5rem 0 0 0;
}
.actions {
display: flex;
justify-content: flex-end;
margin: 0 -.25rem;
margin: 0 -0.25rem;
.save-button,
.reset-button {
display: block;
margin: 0 .25rem 3rem .25rem;
margin: 0 0.25rem 3rem 0.25rem;
}
.reset-button {

View File

@@ -37,11 +37,14 @@ export class PrivacyPolicyComponent implements OnDestroy {
public nextLinks: CnslLinks[] = [];
private sub: Subscription = new Subscription();
public privacyPolicy!: PrivacyPolicy.AsObject;
public privacyPolicy: PrivacyPolicy.AsObject | undefined = undefined;
public form!: FormGroup;
public currentPolicy: GridPolicy = PRIVACY_POLICY;
public InfoSectionType: any = InfoSectionType;
public LANGPLACEHOLDER: string = '{{.Lang}}';
public copied: string = '';
constructor(
private route: ActivatedRoute,
private injector: Injector,
@@ -49,70 +52,108 @@ export class PrivacyPolicyComponent implements OnDestroy {
private toast: ToastService,
private fb: FormBuilder,
) {
this.form = this.fb.group({
tosLink: ['', []],
privacyLink: ['', []],
helpLink: ['', []],
});
this.route.data.pipe(switchMap(data => {
this.serviceType = data.serviceType;
switch (this.serviceType) {
case PolicyComponentServiceType.MGMT:
this.service = this.injector.get(ManagementService as Type<ManagementService>);
this.loadData();
break;
case PolicyComponentServiceType.ADMIN:
this.service = this.injector.get(AdminService as Type<AdminService>);
this.loadData();
break;
}
this.route.data
.pipe(
switchMap((data) => {
this.serviceType = data.serviceType;
switch (this.serviceType) {
case PolicyComponentServiceType.MGMT:
this.service = this.injector.get(ManagementService as Type<ManagementService>);
this.loadData();
break;
case PolicyComponentServiceType.ADMIN:
this.service = this.injector.get(AdminService as Type<AdminService>);
this.loadData();
break;
}
return this.route.params;
})).subscribe();
return this.route.params;
}),
)
.subscribe();
}
public addChip(formControlName: string, value: string): void {
const c = this.form.get(formControlName)?.value;
this.form.get(formControlName)?.setValue(`${c}${value}`);
}
public async loadData(): Promise<any> {
const getData = ():
Promise<AdminGetPrivacyPolicyResponse.AsObject | GetPrivacyPolicyResponse.AsObject> => {
return (this.service as AdminService).getPrivacyPolicy();
const getData = (): Promise<AdminGetPrivacyPolicyResponse.AsObject | GetPrivacyPolicyResponse.AsObject> => {
return this.service.getPrivacyPolicy();
};
getData().then(resp => {
if (resp.policy) {
this.privacyPolicy = resp.policy;
this.form.patchValue(this.privacyPolicy);
}
});
getData()
.then((resp) => {
if (resp.policy) {
this.privacyPolicy = resp.policy;
this.form.patchValue(this.privacyPolicy);
} else {
this.privacyPolicy = undefined;
this.form.patchValue({
tosLink: '',
privacyLink: '',
helpLink: '',
});
}
})
.catch((error) => {
this.privacyPolicy = undefined;
this.form.patchValue({
tosLink: '',
privacyLink: '',
helpLink: '',
});
});
}
public saveCurrentMessage(): void {
console.log(this.form.get('privacyLink')?.value, this.form.get('tosLink')?.value);
if (this.serviceType === PolicyComponentServiceType.MGMT) {
if ((this.privacyPolicy as PrivacyPolicy.AsObject).isDefault) {
if (!this.privacyPolicy || (this.privacyPolicy as PrivacyPolicy.AsObject).isDefault) {
const req = new AddCustomPrivacyPolicyRequest();
req.setPrivacyLink(this.form.get('privacyLink')?.value);
req.setTosLink(this.form.get('tosLink')?.value);
(this.service as ManagementService).addCustomPrivacyPolicy(req).then(() => {
this.toast.showInfo('POLICY.PRIVACY_POLICY.SAVED', true);
}).catch(error => this.toast.showError(error));
req.setHelpLink(this.form.get('helpLink')?.value);
(this.service as ManagementService)
.addCustomPrivacyPolicy(req)
.then(() => {
this.toast.showInfo('POLICY.PRIVACY_POLICY.SAVED', true);
this.loadData();
})
.catch((error) => this.toast.showError(error));
} else {
const req = new UpdateCustomPrivacyPolicyRequest();
req.setPrivacyLink(this.form.get('privacyLink')?.value);
req.setTosLink(this.form.get('tosLink')?.value);
(this.service as ManagementService).updateCustomPrivacyPolicy(req).then(() => {
this.toast.showInfo('POLICY.PRIVACY_POLICY.SAVED', true);
}).catch(error => this.toast.showError(error));
}
req.setHelpLink(this.form.get('helpLink')?.value);
(this.service as ManagementService)
.updateCustomPrivacyPolicy(req)
.then(() => {
this.toast.showInfo('POLICY.PRIVACY_POLICY.SAVED', true);
this.loadData();
})
.catch((error) => this.toast.showError(error));
}
} else if (this.serviceType === PolicyComponentServiceType.ADMIN) {
const req = new UpdatePrivacyPolicyRequest();
req.setPrivacyLink(this.form.get('privacyLink')?.value);
req.setTosLink(this.form.get('tosLink')?.value);
req.setHelpLink(this.form.get('helpLink')?.value);
(this.service as AdminService).updatePrivacyPolicy(req).then(() => {
this.toast.showInfo('POLICY.PRIVACY_POLICY.SAVED', true);
}).catch(error => this.toast.showError(error));
(this.service as AdminService)
.updatePrivacyPolicy(req)
.then(() => {
this.toast.showInfo('POLICY.PRIVACY_POLICY.SAVED', true);
this.loadData();
})
.catch((error) => this.toast.showError(error));
}
}
@@ -128,16 +169,19 @@ export class PrivacyPolicyComponent implements OnDestroy {
width: '400px',
});
dialogRef.afterClosed().subscribe(resp => {
dialogRef.afterClosed().subscribe((resp) => {
if (resp) {
if (this.serviceType === PolicyComponentServiceType.MGMT) {
(this.service as ManagementService).resetPrivacyPolicyToDefault().then(() => {
setTimeout(() => {
this.loadData();
}, 1000);
}).catch(error => {
this.toast.showError(error);
});
(this.service as ManagementService)
.resetPrivacyPolicyToDefault()
.then(() => {
setTimeout(() => {
this.loadData();
}, 1000);
})
.catch((error) => {
this.toast.showError(error);
});
}
}
});

View File

@@ -9,6 +9,7 @@ import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
import { MatSelectModule } from '@angular/material/select';
import { MatTooltipModule } from '@angular/material/tooltip';
import { TranslateModule } from '@ngx-translate/core';
import { CopyToClipboardModule } from 'src/app/directives/copy-to-clipboard/copy-to-clipboard.module';
import { HasRoleModule } from '../../../directives/has-role/has-role.module';
import { DetailLayoutModule } from '../../../modules/detail-layout/detail-layout.module';
@@ -32,6 +33,7 @@ import { PrivacyPolicyComponent } from './privacy-policy.component';
FormsModule,
InputModule,
FormFieldModule,
CopyToClipboardModule,
MatButtonModule,
HasFeaturePipeModule,
MatIconModule,
@@ -49,4 +51,4 @@ import { PrivacyPolicyComponent } from './privacy-policy.component';
InfoSectionModule,
],
})
export class PrivacyPolicyModule { }
export class PrivacyPolicyModule {}

View File

@@ -873,6 +873,7 @@
"DESCRIPTION": "Legen Sie Ihre Datenschutzrichtlinien und Nutzungsbedingungen fest",
"TOSLINK": "Link zu den Allgemeinen Geschäftsbedingungen",
"POLICYLINK": "Link zur den Datenschutzrichtlinien",
"HELPLINK": "Link zur Hilfestellung",
"SAVED": "Saved successfully!",
"RESET_TITLE": "Standardwerte wiederherstellen",
"RESET_DESCRIPTION": "Sie sind im Begriff die Standardlinks für die AGBs und Datenschutzrichtlinie wiederherzustellen. Wollen Sie fortfahren?"

View File

@@ -873,6 +873,7 @@
"DESCRIPTION": "Set your Privacy Policy and Terms of Service Links",
"TOSLINK": "Link to Terms of Service",
"POLICYLINK": "Link to Privacy Policy",
"HELPLINK": "Link to Help",
"SAVED": "Saved successfully!",
"RESET_TITLE": "Restore Default Values",
"RESET_DESCRIPTION": "You are about to restore the default Links for TOS and Privacy Policy. Do you really want to continue?"

View File

@@ -873,6 +873,7 @@
"DESCRIPTION": "Imposta i tuoi link all'informativa sulla privacy e ai termini di servizio",
"TOSLINK": "Link ai termini di servizio",
"POLICYLINK": "Link all'informativa sulla privacy",
"HELPLINK": "link per l'aiuto",
"SAVED": "Salvato con successo!",
"RESET_TITLE": "Ripristina i valori predefiniti",
"RESET_DESCRIPTION": "Stai per ripristinare i link predefiniti per i TOS e l'informativa sulla privacy. Vuoi davvero continuare?"