fix: user init mail (for wrong email) (#891)

* add resendInitialMail

* disable email notifications (when not initialised)

* fix resend init mail

* add tests

* cleanup

* cleanup

* fix tests

* add resend trigger, dialog

* refactor contact component, add sendinitmail fnc

* skip email if empty

* reload user on phone email changes, i18n warndialog on dl

* lint

* rebuild mgmt proto

* remove initial focus

* Update console/src/assets/i18n/de.json

Co-authored-by: Fabi <38692350+fgerschwiler@users.noreply.github.com>

Co-authored-by: Max Peintner <max@caos.ch>
Co-authored-by: Fabi <38692350+fgerschwiler@users.noreply.github.com>
This commit is contained in:
Livio Amstutz 2020-11-16 11:43:22 +01:00 committed by GitHub
parent 69c39b5eb2
commit 376fba72d8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
42 changed files with 11465 additions and 18601 deletions

View File

@ -4,6 +4,7 @@
padding: 1.5rem; padding: 1.5rem;
border-radius: .5rem; border-radius: .5rem;
padding-top: 1rem; padding-top: 1rem;
min-width: 350px;
.header { .header {
margin-top: 0; margin-top: 0;

View File

@ -11,6 +11,10 @@
.action { .action {
display: flex; display: flex;
button {
border-radius: .5rem;
}
.ok-button { .ok-button {
margin-left: .5rem; margin-left: .5rem;
} }

View File

@ -40,10 +40,15 @@
<app-card *ngIf="user" title="{{ 'USER.LOGINMETHODS.TITLE' | translate }}" <app-card *ngIf="user" title="{{ 'USER.LOGINMETHODS.TITLE' | translate }}"
description="{{ 'USER.LOGINMETHODS.DESCRIPTION' | translate }}"> description="{{ 'USER.LOGINMETHODS.DESCRIPTION' | translate }}">
<app-contact *ngIf="user?.human" [human]="user.human" (savedPhone)="savePhone($event)" <button card-actions mat-icon-button (click)="refreshUser()">
(savedEmail)="saveEmail($event)" (enteredPhoneCode)="enteredPhoneCode($event)" <mat-icon>refresh</mat-icon>
(deletedPhone)="deletePhone()" (resendEmailVerification)="resendEmailVerification()" </button>
(resendPhoneVerification)="resendPhoneVerification()"></app-contact> <app-contact *ngIf="user?.human" [human]="user.human" [state]="user.state" canWrite="true"
[userStateEnum]="UserState" (editType)="openEditDialog($event)"
(enteredPhoneCode)="enteredPhoneCode($event)" (deletedPhone)="deletePhone()"
(resendEmailVerification)="resendEmailVerification()"
(resendPhoneVerification)="resendPhoneVerification()">
</app-contact>
</app-card> </app-card>
<app-auth-user-mfa *ngIf="user" #mfaComponent></app-auth-user-mfa> <app-auth-user-mfa *ngIf="user" #mfaComponent></app-auth-user-mfa>

View File

@ -98,3 +98,7 @@
} }
} }
} }
.resendemail {
margin-right: 1rem;
}

View File

@ -1,12 +1,24 @@
import { Component, OnDestroy } from '@angular/core'; import { Component, OnDestroy } from '@angular/core';
import { MatDialog } from '@angular/material/dialog';
import { TranslateService } from '@ngx-translate/core'; import { TranslateService } from '@ngx-translate/core';
import { Subscription } from 'rxjs'; import { Subscription } from 'rxjs';
import { ChangeType } from 'src/app/modules/changes/changes.component'; import { ChangeType } from 'src/app/modules/changes/changes.component';
import { UserGrantContext } from 'src/app/modules/user-grants/user-grants-datasource'; import { UserGrantContext } from 'src/app/modules/user-grants/user-grants-datasource';
import { Gender, UserAddress, UserEmail, UserPhone, UserProfile, UserView } from 'src/app/proto/generated/auth_pb'; import {
Gender,
UserAddress,
UserEmail,
UserPhone,
UserProfile,
UserState,
UserView,
} from 'src/app/proto/generated/auth_pb';
import { GrpcAuthService } from 'src/app/services/grpc-auth.service'; import { GrpcAuthService } from 'src/app/services/grpc-auth.service';
import { ToastService } from 'src/app/services/toast.service'; import { ToastService } from 'src/app/services/toast.service';
import { EditDialogType } from '../user-detail/user-detail.component';
import { EditDialogComponent } 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',
@ -26,6 +38,7 @@ export class AuthUserDetailComponent implements OnDestroy {
public ChangeType: any = ChangeType; public ChangeType: any = ChangeType;
public userLoginMustBeDomain: boolean = false; public userLoginMustBeDomain: boolean = false;
public UserState: any = UserState;
public USERGRANTCONTEXT: UserGrantContext = UserGrantContext.USER; public USERGRANTCONTEXT: UserGrantContext = UserGrantContext.USER;
@ -33,8 +46,13 @@ export class AuthUserDetailComponent implements OnDestroy {
public translate: TranslateService, public translate: TranslateService,
private toast: ToastService, private toast: ToastService,
public userService: GrpcAuthService, public userService: GrpcAuthService,
private dialog: MatDialog,
) { ) {
this.loading = true; this.loading = true;
this.refreshUser();
}
refreshUser(): void {
this.userService.GetMyUser().then(user => { this.userService.GetMyUser().then(user => {
this.user = user.toObject(); this.user = user.toObject();
this.loading = false; this.loading = false;
@ -81,6 +99,7 @@ export class AuthUserDetailComponent implements OnDestroy {
this.toast.showInfo('USER.TOAST.EMAILSAVED', true); this.toast.showInfo('USER.TOAST.EMAILSAVED', true);
if (this.user.human) { if (this.user.human) {
this.user.human.email = data.toObject().email; this.user.human.email = data.toObject().email;
this.refreshUser();
} }
}).catch(error => { }).catch(error => {
this.toast.showError(error); this.toast.showError(error);
@ -90,6 +109,7 @@ export class AuthUserDetailComponent implements OnDestroy {
public enteredPhoneCode(code: string): void { public enteredPhoneCode(code: string): void {
this.userService.VerifyMyUserPhone(code).then(() => { this.userService.VerifyMyUserPhone(code).then(() => {
this.toast.showInfo('USER.TOAST.PHONESAVED', true); this.toast.showInfo('USER.TOAST.PHONESAVED', true);
this.refreshUser();
}).catch(error => { }).catch(error => {
this.toast.showError(error); this.toast.showError(error);
}); });
@ -99,14 +119,6 @@ export class AuthUserDetailComponent implements OnDestroy {
this.translate.use(language); this.translate.use(language);
} }
public resendEmailVerification(): void {
this.userService.ResendEmailVerification().then(() => {
this.toast.showInfo('USER.TOAST.EMAILSAVED', true);
}).catch(error => {
this.toast.showError(error);
});
}
public resendPhoneVerification(): void { public resendPhoneVerification(): void {
this.userService.ResendPhoneVerification().then(() => { this.userService.ResendPhoneVerification().then(() => {
this.toast.showInfo('USER.TOAST.PHONEVERIFICATIONSENT', true); this.toast.showInfo('USER.TOAST.PHONEVERIFICATIONSENT', true);
@ -115,11 +127,20 @@ export class AuthUserDetailComponent implements OnDestroy {
}); });
} }
public resendEmailVerification(): void {
this.userService.ResendMyEmailVerificationMail().then(() => {
this.toast.showInfo('USER.TOAST.EMAILVERIFICATIONSENT', true);
}).catch(error => {
this.toast.showError(error);
});
}
public deletePhone(): void { public deletePhone(): void {
this.userService.RemoveMyUserPhone().then(() => { this.userService.RemoveMyUserPhone().then(() => {
this.toast.showInfo('USER.TOAST.PHONEREMOVED', true); this.toast.showInfo('USER.TOAST.PHONEREMOVED', true);
if (this.user.human) { if (this.user.human) {
this.user.human.phone = ''; this.user.human.phone = '';
this.refreshUser();
} }
}).catch(error => { }).catch(error => {
this.toast.showError(error); this.toast.showError(error);
@ -133,10 +154,54 @@ export class AuthUserDetailComponent implements OnDestroy {
this.toast.showInfo('USER.TOAST.PHONESAVED', true); this.toast.showInfo('USER.TOAST.PHONESAVED', true);
if (this.user.human) { if (this.user.human) {
this.user.human.phone = data.toObject().phone; this.user.human.phone = data.toObject().phone;
this.refreshUser();
} }
}).catch(error => { }).catch(error => {
this.toast.showError(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,
},
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,
},
width: '400px',
});
dialogRefEmail.afterClosed().subscribe(resp => {
if (resp) {
this.saveEmail(resp);
}
});
break;
}
}
} }

View File

@ -0,0 +1,20 @@
<h1 mat-dialog-title>
<span class="title">{{data.titleKey | translate}}</span>
</h1>
<p class="desc">{{data.descriptionKey | translate}}</p>
<div mat-dialog-content>
<mat-form-field class="formfield">
<mat-label>{{data.labelKey | translate }}</mat-label>
<input matInput [(ngModel)]="value" />
</mat-form-field>
</div>
<div mat-dialog-actions class="action">
<button cdkFocusInitial color="primary" mat-button class="ok-button" (click)="closeDialog()">
{{data.cancelKey | translate}}
</button>
<button [disabled]="!value" cdkFocusInitial color="primary" mat-raised-button class="ok-button"
(click)="closeDialogWithValue(value)">
{{data.confirmKey | translate}}
</button>
</div>

View File

@ -0,0 +1,21 @@
.formfield {
width: 100%;
}
.desc {
font-size: 14px;
color: var(--grey);
}
.action {
display: flex;
justify-content: flex-end;
.ok-button {
margin-left: .5rem;
}
button {
border-radius: .5rem;
}
}

View File

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

View File

@ -0,0 +1,23 @@
import { Component, Inject } from '@angular/core';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
@Component({
selector: 'app-edit-email-dialog',
templateUrl: './edit-dialog.component.html',
styleUrls: ['./edit-dialog.component.scss'],
})
export class EditDialogComponent {
public value: string = '';
constructor(public dialogRef: MatDialogRef<EditDialogComponent>,
@Inject(MAT_DIALOG_DATA) public data: any) {
this.value = data.value;
}
closeDialog(email: string = ''): void {
this.dialogRef.close(email);
}
closeDialogWithValue(value: string = ''): void {
this.dialogRef.close(value);
}
}

View File

@ -0,0 +1,19 @@
<h1 mat-dialog-title>
<span class="title">{{'USER.SENDEMAILDIALOG.TITLE' | translate}} {{data?.number}}</span>
</h1>
<p class="desc">{{'USER.SENDEMAILDIALOG.DESCRIPTION' | translate}}</p>
<div mat-dialog-content>
<mat-form-field class="formfield">
<mat-label>{{ 'USER.SENDEMAILDIALOG.NEWEMAIL' | translate }}</mat-label>
<input matInput [(ngModel)]="email" />
</mat-form-field>
</div>
<div mat-dialog-actions class="action">
<button color="primary" mat-button class="ok-button" (click)="closeDialog()">
{{'ACTIONS.CLOSE' | translate}}
</button>
<button cdkFocusInitial color="primary" mat-raised-button class="ok-button" (click)="closeDialogWithSend(email)">
{{'ACTIONS.SEND' | translate}}
</button>
</div>

View File

@ -0,0 +1,21 @@
.formfield {
width: 100%;
}
.desc {
font-size: 14px;
color: var(--grey);
}
.action {
display: flex;
justify-content: flex-end;
.ok-button {
margin-left: .5rem;
}
button {
border-radius: .5rem;
}
}

View File

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

View File

@ -0,0 +1,21 @@
import { Component, Inject } from '@angular/core';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
@Component({
selector: 'app-resend-email-dialog',
templateUrl: './resend-email-dialog.component.html',
styleUrls: ['./resend-email-dialog.component.scss'],
})
export class ResendEmailDialogComponent {
public email: string = '';
constructor(public dialogRef: MatDialogRef<ResendEmailDialogComponent>,
@Inject(MAT_DIALOG_DATA) public data: any) { }
closeDialog(email: string = ''): void {
this.dialogRef.close(email);
}
closeDialogWithSend(email: string = ''): void {
this.dialogRef.close({ send: true, email });
}
}

View File

@ -1,69 +1,55 @@
<div class="method-col"> <div class="method-col">
<div class="method-row"> <div class="method-row">
<div class="left">
<span class="label">{{ 'USER.PROFILE.PASSWORD' | translate }}</span> <span class="label">{{ 'USER.PROFILE.PASSWORD' | translate }}</span>
<span class="name">*********</span>
<span>*********</span> <ng-content select="[pwdAction]"></ng-content>
<div class="overflow"> </div>
<ng-content select="[phoneAction]"></ng-content>
<a [disabled]="!canWrite" [routerLink]="['password']" mat-icon-button> <div class="right">
<mat-icon class="icon">chevron_right</mat-icon> <a matTooltip="{{'USER.PASSWORD.SET' | translate}}" [disabled]="!canWrite" [routerLink]="['password']"
mat-icon-button>
<i class="las la-edit"></i>
</a> </a>
</div> </div>
</div> </div>
<div class="method-row"> <div class="method-row">
<div class="left">
<span class="label">{{ 'USER.EMAIL' | translate }}</span> <span class="label">{{ 'USER.EMAIL' | translate }}</span>
<ng-container *ngIf="!emailEditState; else emailEdit">
<div class="actions">
<span class="name">{{human?.email}}</span> <span class="name">{{human?.email}}</span>
<mat-icon class="icon" *ngIf="human?.isEmailVerified" color="primary" aria-hidden="false" <span *ngIf="human?.isEmailVerified" class="state verified">{{'USER.EMAILVERIFIED' | translate}}</span>
aria-label="verified icon"> <div *ngIf="!human?.isEmailVerified" class="block">
check_circle_outline</mat-icon> <span class="state notverified">{{'USER.NOTVERIFIED' | translate}}</span>
<ng-container *ngIf="human?.email && !human?.isEmailVerified">
<mat-icon class="icon" color="warn" aria-hidden="false" aria-label="not verified icon"> <ng-container *ngIf="human?.email">
highlight_off <a *ngIf="canWrite && state != userStateEnum?.USERSTATE_INITIAL" class="verify"
</mat-icon> matTooltip="{{'USER.LOGINMETHODS.EMAIL.RESEND' | translate}}"
<a *ngIf="canWrite" class="verify" matTooltip="{{'USER.LOGINMETHODS.EMAIL.RESEND' | translate}}"
(click)="emitEmailVerification()">{{'USER.LOGINMETHODS.RESENDCODE' | translate}}</a> (click)="emitEmailVerification()">{{'USER.LOGINMETHODS.RESENDCODE' | translate}}</a>
</ng-container> </ng-container>
</div> </div>
<div> <ng-content select="[emailAction]"></ng-content>
<button [disabled]="!canWrite" (click)="emailEditState = true" mat-icon-button> </div>
<mat-icon class="icon">edit</mat-icon>
<div class="right">
<button [disabled]="!canWrite" (click)="openEditDialog(EditDialogType.EMAIL)" mat-icon-button>
<i class="las la-edit"></i>
</button> </button>
</div> </div>
</ng-container>
<ng-template #emailEdit>
<mat-form-field class="name">
<mat-label>{{ 'USER.EMAIL' | translate }}</mat-label>
<input *ngIf="human && human.email !== undefined && human.email !== null" matInput
[(ngModel)]="human.email" />
</mat-form-field>
<button (click)="emailEditState = false" mat-icon-button>
<mat-icon class="icon">close</mat-icon>
</button>
<button *ngIf="human" [disabled]="!human.email" type="button" color="primary" (click)="saveEmail()"
mat-raised-button>{{ 'ACTIONS.SAVE' | translate }}</button>
</ng-template>
</div> </div>
<div class="method-row"> <div class="method-row">
<div class="left">
<span class="label">{{ 'USER.PHONE' | translate }}</span> <span class="label">{{ 'USER.PHONE' | translate }}</span>
<span class="name">{{human?.phone ? human.phone : ('USER.PHONEEMPTY' | translate)}}</span>
<span *ngIf="human?.isPhoneVerified" class="state verified">{{'USER.PHONEVERIFIED' | translate}}</span>
<div *ngIf="!human?.isPhoneVerified" class="block">
<span class="state notverified">{{'USER.NOTVERIFIED' | translate}}</span>
<ng-container *ngIf="!phoneEditState; else phoneEdit"> <ng-container *ngIf="human?.phone">
<div class="actions"> <a *ngIf="!disablePhoneCode && canWrite" class="verify"
<span class="name">{{human?.phone}}</span>
<mat-icon class="icon" *ngIf="human?.isPhoneVerified" color="primary" aria-hidden="false"
aria-label="verified icon">
check_circle_outline</mat-icon>
<ng-container *ngIf="human?.phone && !human?.isPhoneVerified">
<mat-icon class="icon" matTooltip="not verified" color="warn" aria-hidden="false"
aria-label="not verified icon">
highlight_off
</mat-icon>
<a *ngIf="!disablePhoneCode && !canWrite" class="verify"
matTooltip="{{'USER.LOGINMETHODS.ENTERCODE_DESC' | translate}}" matTooltip="{{'USER.LOGINMETHODS.ENTERCODE_DESC' | translate}}"
(click)="enterCode()">{{'USER.LOGINMETHODS.ENTERCODE' | translate}}</a> (click)="enterCode()">{{'USER.LOGINMETHODS.ENTERCODE' | translate}}</a>
<a *ngIf="canWrite" class="verify" matTooltip="{{'USER.LOGINMETHODS.PHONE.RESEND' | translate}}" <a *ngIf="canWrite" class="verify" matTooltip="{{'USER.LOGINMETHODS.PHONE.RESEND' | translate}}"
@ -71,27 +57,18 @@
</ng-container> </ng-container>
</div> </div>
<div> <ng-content select="[phoneAction]"></ng-content>
<button [disabled]="!canWrite" (click)="phoneEditState = true" mat-icon-button>
<mat-icon class="icon">edit</mat-icon>
</button>
</div> </div>
</ng-container>
<ng-template #phoneEdit> <div class="right">
<mat-form-field class="name"> <button matTooltip="{{'ACTIONS.DELETE' | translate}}" *ngIf="human && human.phone" color="warn"
<mat-label>{{ 'USER.PHONE' | translate }}</mat-label> (click)="emitDeletePhone()" mat-icon-button>
<input *ngIf="human && human.phone !== undefined && human.phone !== null" matInput
[(ngModel)]="human.phone" />
</mat-form-field>
<button (click)="phoneEditState = false" mat-icon-button>
<mat-icon class="icon">close</mat-icon>
</button>
<button *ngIf="human && human.phone" color="warn" (click)="emitDeletePhone()" mat-icon-button>
<i class="las la-trash"></i> <i class="las la-trash"></i>
</button> </button>
<button *ngIf="human" [disabled]="!human.phone" type="button" color="primary" (click)="savePhone()" <button matTooltip="{{'ACTIONS.EDIT' | translate}}" [disabled]="!canWrite"
mat-raised-button>{{ 'ACTIONS.SAVE' | translate }}</button> (click)="openEditDialog(EditDialogType.PHONE)" mat-icon-button>
</ng-template> <i class="las la-edit"></i>
</button>
</div>
</div> </div>
</div> </div>

View File

@ -5,29 +5,57 @@
.method-row { .method-row {
display: flex; display: flex;
align-items: center;
justify-content: space-between; justify-content: space-between;
align-items: center;
padding: .5rem; padding: .5rem;
border-bottom: 1px solid #ffffff20; border-bottom: 1px solid #ffffff20;
flex-wrap: wrap; flex-wrap: wrap;
.actions { .left {
flex: 1; .label {
font-size: 13px;
margin-bottom: .5rem;
min-width: 100px;
color: var(--grey);
display: block;
}
.name {
display: block;
margin-bottom: .5rem;
}
.state {
font-size: 14px;
margin-bottom: .5rem;
&.verified {
color: #85d996;
display: block;
}
&.notverified {
color: #ff4436;
margin-right: 1rem;
}
}
.block {
display: block;
}
}
.right {
flex-basis: 70px;
display: flex; display: flex;
justify-content: flex-end; justify-content: flex-end;
align-items: center;
flex-direction: column;
min-width: 150px;
} }
.label { .verified-icon {
font-size: .9rem; font-size: 1.2rem;
max-width: 100px; line-height: 1.2rem;
color: var(--grey); height: 1.2rem;
} cursor: default;
.icon {
margin: .5rem;
} }
.verify { .verify {
@ -38,6 +66,7 @@
cursor: pointer; cursor: pointer;
word-wrap: none; word-wrap: none;
white-space: nowrap; white-space: nowrap;
margin-right: 1rem;
&:hover { &:hover {
text-decoration: underline; text-decoration: underline;
@ -46,6 +75,6 @@
} }
} }
.overflow { .mat-form-field-wrapper {
overflow: auto; padding-bottom: 0 !important;
} }

View File

@ -1,46 +1,48 @@
import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core'; import { Component, EventEmitter, Input, Output } from '@angular/core';
import { MatDialog } from '@angular/material/dialog'; import { MatDialog } from '@angular/material/dialog';
import { HumanView as AuthHumanView } from 'src/app/proto/generated/auth_pb'; import { WarnDialogComponent } from 'src/app/modules/warn-dialog/warn-dialog.component';
import { HumanView as MgmtHumanView } from 'src/app/proto/generated/management_pb'; import { HumanView as AuthHumanView, UserState as AuthUserState } from 'src/app/proto/generated/auth_pb';
import { HumanView as MgmtHumanView, UserState as MgmtUserState } from 'src/app/proto/generated/management_pb';
import { CodeDialogComponent } from '../auth-user-detail/code-dialog/code-dialog.component'; import { CodeDialogComponent } from '../auth-user-detail/code-dialog/code-dialog.component';
import { EditDialogType } from '../user-detail/user-detail.component';
@Component({ @Component({
selector: 'app-contact', selector: 'app-contact',
templateUrl: './contact.component.html', templateUrl: './contact.component.html',
styleUrls: ['./contact.component.scss'], styleUrls: ['./contact.component.scss'],
}) })
export class ContactComponent implements OnInit { export class ContactComponent {
@Input() disablePhoneCode: boolean = false; @Input() disablePhoneCode: boolean = false;
@Input() canWrite: boolean = false; @Input() canWrite: boolean = false;
@Input() human!: AuthHumanView.AsObject | MgmtHumanView.AsObject; @Input() human!: AuthHumanView.AsObject | MgmtHumanView.AsObject;
@Output() savedPhone: EventEmitter<string> = new EventEmitter(); @Input() state!: AuthUserState | MgmtUserState;
@Output() savedEmail: EventEmitter<string> = new EventEmitter(); @Output() editType: EventEmitter<EditDialogType> = new EventEmitter();
@Output() resendEmailVerification: EventEmitter<void> = new EventEmitter(); @Output() resendEmailVerification: EventEmitter<void> = new EventEmitter();
@Output() resendPhoneVerification: EventEmitter<void> = new EventEmitter(); @Output() resendPhoneVerification: EventEmitter<void> = new EventEmitter();
@Output() enteredPhoneCode: EventEmitter<string> = new EventEmitter(); @Output() enteredPhoneCode: EventEmitter<string> = new EventEmitter();
@Output() deletedPhone: EventEmitter<void> = new EventEmitter(); @Output() deletedPhone: EventEmitter<void> = new EventEmitter();
@Input() public userStateEnum: any;
public emailEditState: boolean = false; public EditDialogType: any = EditDialogType;
public phoneEditState: boolean = false;
constructor(private dialog: MatDialog) { } constructor(private dialog: MatDialog) { }
ngOnInit(): void {
}
savePhone(): void {
this.phoneEditState = false;
this.savedPhone.emit(this.human.phone);
}
emitDeletePhone(): void { emitDeletePhone(): void {
this.phoneEditState = false; const dialogRef = this.dialog.open(WarnDialogComponent, {
data: {
confirmKey: 'ACTIONS.DELETE',
cancelKey: 'ACTIONS.CANCEL',
titleKey: 'USER.LOGINMETHODS.PHONE.DELETETITLE',
descriptionKey: 'USER.LOGINMETHODS.PHONE.DELETEDESC',
},
width: '400px',
});
dialogRef.afterClosed().subscribe(resp => {
if (resp) {
this.deletedPhone.emit(); this.deletedPhone.emit();
} }
});
saveEmail(): void {
this.emailEditState = false;
this.savedEmail.emit(this.human.email);
} }
emitEmailVerification(): void { emitEmailVerification(): void {
@ -67,4 +69,8 @@ export class ContactComponent implements OnInit {
}); });
} }
} }
public openEditDialog(type: EditDialogType): void {
this.editType.emit(type);
}
} }

View File

@ -34,7 +34,10 @@ import { AuthUserDetailComponent } from './auth-user-detail/auth-user-detail.com
import { AuthUserMfaComponent } from './auth-user-detail/auth-user-mfa/auth-user-mfa.component'; import { AuthUserMfaComponent } from './auth-user-detail/auth-user-mfa/auth-user-mfa.component';
import { CodeDialogComponent } from './auth-user-detail/code-dialog/code-dialog.component'; import { CodeDialogComponent } from './auth-user-detail/code-dialog/code-dialog.component';
import { DialogOtpComponent } from './auth-user-detail/dialog-otp/dialog-otp.component'; import { DialogOtpComponent } from './auth-user-detail/dialog-otp/dialog-otp.component';
import { EditDialogComponent } from './auth-user-detail/edit-dialog/edit-dialog.component';
import { ResendEmailDialogComponent } from './auth-user-detail/resend-email-dialog/resend-email-dialog.component';
import { ThemeSettingComponent } from './auth-user-detail/theme-setting/theme-setting.component'; import { ThemeSettingComponent } from './auth-user-detail/theme-setting/theme-setting.component';
import { ContactComponent } from './contact/contact.component';
import { DetailFormMachineModule } from './detail-form-machine/detail-form-machine.module'; import { DetailFormMachineModule } from './detail-form-machine/detail-form-machine.module';
import { DetailFormModule } from './detail-form/detail-form.module'; import { DetailFormModule } from './detail-form/detail-form.module';
import { ExternalIdpsComponent } from './external-idps/external-idps.component'; import { ExternalIdpsComponent } from './external-idps/external-idps.component';
@ -46,13 +49,13 @@ import { PasswordComponent } from './password/password.component';
import { UserDetailRoutingModule } from './user-detail-routing.module'; import { UserDetailRoutingModule } from './user-detail-routing.module';
import { UserDetailComponent } from './user-detail/user-detail.component'; 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';
import { ContactComponent } from './contact/contact.component';
@NgModule({ @NgModule({
declarations: [ declarations: [
AuthUserDetailComponent, AuthUserDetailComponent,
UserDetailComponent, UserDetailComponent,
DialogOtpComponent, DialogOtpComponent,
EditDialogComponent,
AuthUserMfaComponent, AuthUserMfaComponent,
UserMfaComponent, UserMfaComponent,
ThemeSettingComponent, ThemeSettingComponent,
@ -62,6 +65,7 @@ import { ContactComponent } from './contact/contact.component';
MachineKeysComponent, MachineKeysComponent,
ExternalIdpsComponent, ExternalIdpsComponent,
ContactComponent, ContactComponent,
ResendEmailDialogComponent,
], ],
imports: [ imports: [
UserDetailRoutingModule, UserDetailRoutingModule,

View File

@ -67,15 +67,20 @@
<app-card *ngIf="user.human" title="{{ 'USER.LOGINMETHODS.TITLE' | translate }}" <app-card *ngIf="user.human" title="{{ 'USER.LOGINMETHODS.TITLE' | translate }}"
description="{{ 'USER.LOGINMETHODS.DESCRIPTION' | translate }}"> description="{{ 'USER.LOGINMETHODS.DESCRIPTION' | translate }}">
<button card-actions mat-icon-button (click)="refreshUser()">
<mat-icon>refresh</mat-icon>
</button>
<app-contact disablePhoneCode="true" <app-contact disablePhoneCode="true"
[canWrite]="(['user.write:' + user?.id, 'user.write$'] | hasRole | async)" *ngIf="user?.human" [canWrite]="(['user.write:' + user?.id, 'user.write$'] | hasRole | async)" *ngIf="user?.human"
[human]="user.human" (savedPhone)="savePhone($event)" (savedEmail)="saveEmail($event)" [human]="user.human" (editType)="openEditDialog($event)" (deletedPhone)="deletePhone()"
(deletedPhone)="deletePhone()" (resendEmailVerification)="resendEmailVerification()" (resendEmailVerification)="resendEmailVerification()"
(resendPhoneVerification)="resendPhoneVerification()"> (resendPhoneVerification)="resendPhoneVerification()">
<button phoneAction [disabled]="(canWrite$ | async) == false" (click)="sendSetPasswordNotification()" <button pwdAction [disabled]="(canWrite$ | async) == false" (click)="sendSetPasswordNotification()"
mat-stroked-button color="primary" mat-stroked-button color="primary"
*ngIf="user.state === UserState.USERSTATE_INITIAL">{{ 'USER.PASSWORD.RESENDNOTIFICATION' | translate }}</button> *ngIf="user.state === UserState.USERSTATE_INITIAL">{{ 'USER.PASSWORD.RESENDNOTIFICATION' | translate }}</button>
<button emailAction class="resendemail" *ngIf="user.state == UserState.USERSTATE_INITIAL"
mat-stroked-button color="primary"
(click)="resendInitEmail()">{{'USER.RESENDINITIALEMAIL' | translate}}</button>
</app-contact> </app-contact>
</app-card> </app-card>

View File

@ -1,9 +1,9 @@
import { Location } from '@angular/common'; import { Location } from '@angular/common';
import { Component, OnDestroy, OnInit } from '@angular/core'; import { Component, OnInit } from '@angular/core';
import { MatDialog } from '@angular/material/dialog'; import { MatDialog } from '@angular/material/dialog';
import { ActivatedRoute } from '@angular/router'; import { ActivatedRoute } from '@angular/router';
import { TranslateService } from '@ngx-translate/core'; import { TranslateService } from '@ngx-translate/core';
import { Subscription } from 'rxjs'; import { take } from 'rxjs/operators';
import { ChangeType } from 'src/app/modules/changes/changes.component'; import { ChangeType } from 'src/app/modules/changes/changes.component';
import { UserGrantContext } from 'src/app/modules/user-grants/user-grants-datasource'; import { UserGrantContext } from 'src/app/modules/user-grants/user-grants-datasource';
import { WarnDialogComponent } from 'src/app/modules/warn-dialog/warn-dialog.component'; import { WarnDialogComponent } from 'src/app/modules/warn-dialog/warn-dialog.component';
@ -21,18 +21,24 @@ import {
import { ManagementService } from 'src/app/services/mgmt.service'; import { ManagementService } from 'src/app/services/mgmt.service';
import { ToastService } from 'src/app/services/toast.service'; import { ToastService } from 'src/app/services/toast.service';
import { EditDialogComponent } from '../auth-user-detail/edit-dialog/edit-dialog.component';
import { ResendEmailDialogComponent } from '../auth-user-detail/resend-email-dialog/resend-email-dialog.component';
export enum EditDialogType {
PHONE = 1,
EMAIL = 2,
}
@Component({ @Component({
selector: 'app-user-detail', selector: 'app-user-detail',
templateUrl: './user-detail.component.html', templateUrl: './user-detail.component.html',
styleUrls: ['./user-detail.component.scss'], styleUrls: ['./user-detail.component.scss'],
}) })
export class UserDetailComponent implements OnInit, OnDestroy { export class UserDetailComponent implements OnInit {
public user!: UserView.AsObject; public user!: UserView.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();
public ChangeType: any = ChangeType; public ChangeType: any = ChangeType;
public loading: boolean = false; public loading: boolean = false;
@ -40,6 +46,8 @@ export class UserDetailComponent implements OnInit, OnDestroy {
public copied: string = ''; public copied: string = '';
public USERGRANTCONTEXT: UserGrantContext = UserGrantContext.USER; public USERGRANTCONTEXT: UserGrantContext = UserGrantContext.USER;
public EditDialogType: any = EditDialogType;
constructor( constructor(
public translate: TranslateService, public translate: TranslateService,
private route: ActivatedRoute, private route: ActivatedRoute,
@ -49,8 +57,8 @@ export class UserDetailComponent implements OnInit, OnDestroy {
private dialog: MatDialog, private dialog: MatDialog,
) { } ) { }
public ngOnInit(): void { refreshUser(): void {
this.subscription = this.route.params.subscribe(params => { this.route.params.pipe(take(1)).subscribe(params => {
const { id } = params; const { id } = params;
this.mgmtUserService.GetUserByID(id).then(user => { this.mgmtUserService.GetUserByID(id).then(user => {
this.user = user.toObject(); this.user = user.toObject();
@ -60,8 +68,8 @@ export class UserDetailComponent implements OnInit, OnDestroy {
}); });
} }
public ngOnDestroy(): void { public ngOnInit(): void {
this.subscription.unsubscribe(); this.refreshUser();
} }
public changeState(newState: UserState): void { public changeState(newState: UserState): void {
@ -149,6 +157,7 @@ export class UserDetailComponent implements OnInit, OnDestroy {
this.toast.showInfo('USER.TOAST.PHONEREMOVED', true); this.toast.showInfo('USER.TOAST.PHONEREMOVED', true);
if (this.user.human) { if (this.user.human) {
this.user.human.phone = ''; this.user.human.phone = '';
this.refreshUser();
} }
}).catch(error => { }).catch(error => {
this.toast.showError(error); this.toast.showError(error);
@ -158,9 +167,10 @@ export class UserDetailComponent implements OnInit, OnDestroy {
public saveEmail(email: string): void { public saveEmail(email: string): void {
if (this.user.id && email) { if (this.user.id && email) {
this.mgmtUserService.SaveUserEmail(this.user.id, email).then((data: UserEmail) => { this.mgmtUserService.SaveUserEmail(this.user.id, email).then((data: UserEmail) => {
this.toast.showInfo('USER.TOAST.EMAILSENT', true); this.toast.showInfo('USER.TOAST.EMAILSAVED', true);
if (this.user.human) { if (this.user.human) {
this.user.human.email = data.toObject().email; this.user.human.email = data.toObject().email;
this.refreshUser();
} }
}).catch(error => { }).catch(error => {
this.toast.showError(error); this.toast.showError(error);
@ -175,6 +185,7 @@ export class UserDetailComponent implements OnInit, OnDestroy {
this.toast.showInfo('USER.TOAST.PHONESAVED', true); this.toast.showInfo('USER.TOAST.PHONESAVED', true);
if (this.user.human) { if (this.user.human) {
this.user.human.phone = data.toObject().phone; this.user.human.phone = data.toObject().phone;
this.refreshUser();
} }
}).catch(error => { }).catch(error => {
this.toast.showError(error); this.toast.showError(error);
@ -217,4 +228,63 @@ export class UserDetailComponent implements OnInit, OnDestroy {
} }
}); });
} }
public resendInitEmail(): void {
const dialogRef = this.dialog.open(ResendEmailDialogComponent, {
width: '400px',
});
dialogRef.afterClosed().subscribe(resp => {
if (resp.send && this.user.id) {
this.mgmtUserService.ResendInitialMail(this.user.id, resp.email ?? '').then(() => {
this.toast.showInfo('USER.TOAST.INITEMAILSENT', true);
}).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,
},
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,
},
width: '400px',
});
dialogRefEmail.afterClosed().subscribe(resp => {
if (resp) {
this.saveEmail(resp);
}
});
break;
}
}
} }

View File

@ -249,6 +249,12 @@ export class GrpcAuthService {
return this.grpcService.auth.changeMyUserEmail(req); return this.grpcService.auth.changeMyUserEmail(req);
} }
public ResendMyEmailVerificationMail(): Promise<Empty> {
return this.grpcService.auth.resendMyEmailVerificationMail(
new Empty(),
);
}
public RemoveMyUserPhone(): Promise<Empty> { public RemoveMyUserPhone(): Promise<Empty> {
return this.grpcService.auth.removeMyUserPhone( return this.grpcService.auth.removeMyUserPhone(
new Empty(), new Empty(),

View File

@ -40,6 +40,7 @@ import {
IdpSearchResponse, IdpSearchResponse,
IdpUpdate, IdpUpdate,
IdpView, IdpView,
InitialMailRequest,
LoginName, LoginName,
LoginPolicy, LoginPolicy,
LoginPolicyRequest, LoginPolicyRequest,
@ -786,6 +787,16 @@ export class ManagementService {
return this.grpcService.mgmt.resendEmailVerificationMail(req); return this.grpcService.mgmt.resendEmailVerificationMail(req);
} }
public ResendInitialMail(userId: string, newemail: string): Promise<Empty> {
const req = new InitialMailRequest();
if (newemail) {
req.setEmail(newemail);
}
req.setId(userId);
return this.grpcService.mgmt.resendInitialMail(req);
}
public ResendPhoneVerification(id: string): Promise<any> { public ResendPhoneVerification(id: string): Promise<any> {
const req = new UserID(); const req = new UserID();
req.setId(id); req.setId(id);

View File

@ -77,7 +77,9 @@
"LOGIN":"Einloggen", "LOGIN":"Einloggen",
"EDIT":"Bearbeiten", "EDIT":"Bearbeiten",
"PIN":"Anpinnen", "PIN":"Anpinnen",
"CONFIGURE":"Konfigurieren" "CONFIGURE":"Konfigurieren",
"SEND":"Senden",
"NEWVALUE":"Neuer Wert"
}, },
"ERRORS": { "ERRORS": {
"REQUIRED": "Bitte fülle alle benötigten Felder aus.", "REQUIRED": "Bitte fülle alle benötigten Felder aus.",
@ -112,6 +114,11 @@
"DELETE_TITLE":"User löschen", "DELETE_TITLE":"User löschen",
"DELETE_DESCRIPTION":"Sie sind im Begriff einen Benutzer endgültig zu löschen. Wollen Sie dies wirklich tun?" "DELETE_DESCRIPTION":"Sie sind im Begriff einen Benutzer endgültig zu löschen. Wollen Sie dies wirklich tun?"
}, },
"SENDEMAILDIALOG":{
"TITLE":"Email Benachrichtigung senden",
"DESCRIPTION":"Klicken Sie den untenstehenden Button um ein verifizierungs Email an die aktuelle Adresse zu versenden oder ändern Sie die Emailadresse in dem Feld.",
"NEWEMAIL":"Neue Email"
},
"TABLE":{ "TABLE":{
"DEACTIVATE":"Deaktivieren", "DEACTIVATE":"Deaktivieren",
"ACTIVATE":"Aktivieren", "ACTIVATE":"Aktivieren",
@ -241,6 +248,10 @@
}, },
"EMAIL": "E-Mail", "EMAIL": "E-Mail",
"PHONE": "Telefonnummer", "PHONE": "Telefonnummer",
"PHONEEMPTY":"Keine Telefonnummer hinterlegt",
"PHONEVERIFIED":"Telefonnummer bestätigt.",
"EMAILVERIFIED":"Email verifiziert",
"NOTVERIFIED":"nicht verifiziert",
"PREFERRED_LOGINNAME":"Bevorzugter Loginname", "PREFERRED_LOGINNAME":"Bevorzugter Loginname",
"LOGINMETHODS": { "LOGINMETHODS": {
"TITLE": "Kontaktinformationen", "TITLE": "Kontaktinformationen",
@ -248,12 +259,18 @@
"EMAIL": { "EMAIL": {
"TITLE": "E-Mail", "TITLE": "E-Mail",
"VALID": "Validiert", "VALID": "Validiert",
"RESEND": "Verifikationsmail erneut senden" "RESEND": "Verifikationsmail erneut senden",
"EDITTITLE":"Email ändern",
"EDITDESC":"Geben Sie die neue Email in dem darunterliegenden Feld ein!"
}, },
"PHONE": { "PHONE": {
"TITLE": "Telefon", "TITLE": "Telefon",
"VALID": "Validiert", "VALID": "Validiert",
"RESEND": "Verifikationsnachricht erneut senden" "RESEND": "Verifikationsnachricht erneut senden",
"EDITTITLE":"Nummer ändern",
"EDITDESC":"Geben Sie die neue Nummer in dem darunterliegenden Feld ein!",
"DELETETITLE":"Telefonnummer löschen",
"DELETEDESC":"Wollen Sie die Telefonnummer wirklich löschen?"
}, },
"RESENDCODE": "Code erneut senden", "RESENDCODE": "Code erneut senden",
"ENTERCODE":"Verifizieren", "ENTERCODE":"Verifizieren",
@ -294,10 +311,13 @@
"SIGNEDOUT_BTN":"Anmelden", "SIGNEDOUT_BTN":"Anmelden",
"EDITACCOUNT":"Konto bearbeiten", "EDITACCOUNT":"Konto bearbeiten",
"ADDACCOUNT":"Konto hinzufügen", "ADDACCOUNT":"Konto hinzufügen",
"RESENDINITIALEMAIL":"Neue Initialisierungsmail senden",
"RESENDEMAILNOTIFICATION":"Benachrichtigungsmail senden",
"TOAST": { "TOAST": {
"CREATED":"Benutzer erfolgreich erstellt.", "CREATED":"Benutzer erfolgreich erstellt.",
"SAVED":"Profil gespeichert.", "SAVED":"Profil gespeichert.",
"EMAILSAVED":"E-Mail gespeichert.", "EMAILSAVED":"E-Mail gespeichert.",
"INITEMAILSENT":"Initialisierung Email gesendet.",
"PHONESAVED":"Telefonnummer gespeichert.", "PHONESAVED":"Telefonnummer gespeichert.",
"PHONEREMOVED":"Telefonnummer gelöscht.", "PHONEREMOVED":"Telefonnummer gelöscht.",
"PHONEVERIFIED":"Telefonnummer bestätigt.", "PHONEVERIFIED":"Telefonnummer bestätigt.",

View File

@ -77,7 +77,9 @@
"LOGIN":"Login", "LOGIN":"Login",
"EDIT":"Edit", "EDIT":"Edit",
"PIN":"Pin / Unpin", "PIN":"Pin / Unpin",
"CONFIGURE":"Configure" "CONFIGURE":"Configure",
"SEND":"Send",
"NEWVALUE":"New Value"
}, },
"ERRORS": { "ERRORS": {
"REQUIRED": "Some required fields are missing.", "REQUIRED": "Some required fields are missing.",
@ -112,6 +114,11 @@
"DELETE_TITLE":"Delete User", "DELETE_TITLE":"Delete User",
"DELETE_DESCRIPTION":"You are about to permanently delete a user. Are you sure?" "DELETE_DESCRIPTION":"You are about to permanently delete a user. Are you sure?"
}, },
"SENDEMAILDIALOG":{
"TITLE":"Send Email Notification",
"DESCRIPTION":"Click the button below to send a notification to the current email address or change the email address in the field.",
"NEWEMAIL":"New email address"
},
"TABLE":{ "TABLE":{
"DEACTIVATE":"Deactivate", "DEACTIVATE":"Deactivate",
"ACTIVATE":"Activate", "ACTIVATE":"Activate",
@ -240,7 +247,11 @@
"NOTEQUAL":"The passwords provided do not match." "NOTEQUAL":"The passwords provided do not match."
}, },
"EMAIL": "E-mail", "EMAIL": "E-mail",
"PHONE": "Phone Number", "PHONE": "Phonenumber",
"PHONEEMPTY":"No phonenumber defined",
"PHONEVERIFIED":"Phonenumber verified.",
"EMAILVERIFIED":"Email verified",
"NOTVERIFIED":"not verified",
"PREFERRED_LOGINNAME":"Preferred Loginname", "PREFERRED_LOGINNAME":"Preferred Loginname",
"LOGINMETHODS": { "LOGINMETHODS": {
"TITLE": "Contact Information", "TITLE": "Contact Information",
@ -248,12 +259,18 @@
"EMAIL": { "EMAIL": {
"TITLE": "E-mail", "TITLE": "E-mail",
"VALID": "validated", "VALID": "validated",
"RESEND": "Resend Verification E-mail" "RESEND": "Resend Verification E-mail",
"EDITTITLE":"Change Email",
"EDITDESC":"Enter the new email in the field below."
}, },
"PHONE": { "PHONE": {
"TITLE": "Phone", "TITLE": "Phone",
"VALID": "validated", "VALID": "validated",
"RESEND": "Resend Verification Text Message" "RESEND": "Resend Verification Text Message",
"EDITTITLE":"Change number",
"EDITDESC":"Enter the new phonenumber in the field below.",
"DELETETITLE":"Delete Phonenumber",
"DELETEDESC":"Do you really want to delete the phonenumber"
}, },
"RESENDCODE": "Resend Code", "RESENDCODE": "Resend Code",
"ENTERCODE":"Verify", "ENTERCODE":"Verify",
@ -294,10 +311,13 @@
"SIGNEDOUT_BTN":"Sign In", "SIGNEDOUT_BTN":"Sign In",
"EDITACCOUNT":"Edit Account", "EDITACCOUNT":"Edit Account",
"ADDACCOUNT":"Log in With Another Account", "ADDACCOUNT":"Log in With Another Account",
"RESENDINITIALEMAIL":"Send new initialisation mail",
"RESENDEMAILNOTIFICATION":"Resend Email notification",
"TOAST": { "TOAST": {
"CREATED":"User created successfully.", "CREATED":"User created successfully.",
"SAVED":"Profile saved successfully.", "SAVED":"Profile saved successfully.",
"EMAILSAVED":"E-mail saved successfully.", "EMAILSAVED":"E-mail saved successfully.",
"INITEMAILSENT":"Initializing mail sent.",
"PHONESAVED":"Phone saved successfully.", "PHONESAVED":"Phone saved successfully.",
"PHONEREMOVED":"Phone has been removed.", "PHONEREMOVED":"Phone has been removed.",
"PHONEVERIFIED":"Phone verified successfully.", "PHONEVERIFIED":"Phone verified successfully.",

View File

@ -11,6 +11,7 @@
<link href="https://fonts.googleapis.com/css?family=Roboto:300,400,500&amp;display=swap" rel="stylesheet"> <link href="https://fonts.googleapis.com/css?family=Roboto:300,400,500&amp;display=swap" rel="stylesheet">
<link href="https://fonts.googleapis.com/icon?family=Material+Icons|Material+Icons+Outlined" rel="stylesheet"> <link href="https://fonts.googleapis.com/icon?family=Material+Icons|Material+Icons+Outlined" rel="stylesheet">
<link href="https://fonts.googleapis.com/css2?family=Lato&display=swap" rel="stylesheet"> <link href="https://fonts.googleapis.com/css2?family=Lato&display=swap" rel="stylesheet">
<link rel="stylesheet" <link rel="stylesheet"
href="https://maxst.icons8.com/vue-static/landings/line-awesome/line-awesome/1.3.0/css/line-awesome.min.css"> href="https://maxst.icons8.com/vue-static/landings/line-awesome/line-awesome/1.3.0/css/line-awesome.min.css">
<link rel="manifest" href="manifest.webmanifest"> <link rel="manifest" href="manifest.webmanifest">

View File

@ -195,6 +195,11 @@ func (s *Server) SetInitialPassword(ctx context.Context, request *management.Pas
return &empty.Empty{}, err return &empty.Empty{}, err
} }
func (s *Server) ResendInitialMail(ctx context.Context, request *management.InitialMailRequest) (*empty.Empty, error) {
err := s.user.ResendInitialMail(ctx, request.Id, request.Email)
return &empty.Empty{}, err
}
func (s *Server) SearchUserExternalIDPs(ctx context.Context, request *management.ExternalIDPSearchRequest) (*management.ExternalIDPSearchResponse, error) { func (s *Server) SearchUserExternalIDPs(ctx context.Context, request *management.ExternalIDPSearchRequest) (*management.ExternalIDPSearchResponse, error) {
externalIDP, err := s.user.SearchExternalIDPs(ctx, externalIDPSearchRequestToModel(request)) externalIDP, err := s.user.SearchExternalIDPs(ctx, externalIDPSearchRequestToModel(request))
if err != nil { if err != nil {

View File

@ -10,6 +10,7 @@ import (
usr_grant_event "github.com/caos/zitadel/internal/usergrant/repository/eventsourcing" usr_grant_event "github.com/caos/zitadel/internal/usergrant/repository/eventsourcing"
"github.com/caos/logging" "github.com/caos/logging"
"github.com/caos/zitadel/internal/api/authz" "github.com/caos/zitadel/internal/api/authz"
"github.com/caos/zitadel/internal/config/systemdefaults" "github.com/caos/zitadel/internal/config/systemdefaults"
"github.com/caos/zitadel/internal/errors" "github.com/caos/zitadel/internal/errors"
@ -238,6 +239,10 @@ func (repo *UserRepo) RequestSetPassword(ctx context.Context, id string, notifyT
return repo.UserEvents.RequestSetPassword(ctx, id, notifyType) return repo.UserEvents.RequestSetPassword(ctx, id, notifyType)
} }
func (repo *UserRepo) ResendInitialMail(ctx context.Context, userID, email string) error {
return repo.UserEvents.ResendInitialMail(ctx, userID, email)
}
func (repo *UserRepo) ProfileByID(ctx context.Context, userID string) (*usr_model.Profile, error) { func (repo *UserRepo) ProfileByID(ctx context.Context, userID string) (*usr_model.Profile, error) {
user, err := repo.UserByID(ctx, userID) user, err := repo.UserByID(ctx, userID)
if err != nil { if err != nil {

View File

@ -54,4 +54,6 @@ type UserRepository interface {
ChangeAddress(ctx context.Context, address *model.Address) (*model.Address, error) ChangeAddress(ctx context.Context, address *model.Address) (*model.Address, error)
SearchUserMemberships(ctx context.Context, request *model.UserMembershipSearchRequest) (*model.UserMembershipSearchResponse, error) SearchUserMemberships(ctx context.Context, request *model.UserMembershipSearchRequest) (*model.UserMembershipSearchResponse, error)
ResendInitialMail(ctx context.Context, userID, email string) error
} }

View File

@ -12,6 +12,8 @@ Errors:
AlreadyInactive: Benutzer ist bereits deaktiviert AlreadyInactive: Benutzer ist bereits deaktiviert
NotInactive: Benutzer ist nicht inaktiv NotInactive: Benutzer ist nicht inaktiv
ShouldBeActiveOrInitial: Benutzer ist nicht aktiv oder initialisiert ShouldBeActiveOrInitial: Benutzer ist nicht aktiv oder initialisiert
AlreadyInitialised: Benutzer ist bereits initialisiert
NotInitialised: Benutzer ist noch nicht initialisiert
NotLocked: Benutzer ist nicht gesperrt NotLocked: Benutzer ist nicht gesperrt
NoChanges: Keine Änderungen gefunden NoChanges: Keine Änderungen gefunden
InitCodeNotFound: Kein Initialisierungs Code gefunden InitCodeNotFound: Kein Initialisierungs Code gefunden

View File

@ -12,6 +12,8 @@ Errors:
AlreadyInactive: User already inactive AlreadyInactive: User already inactive
NotInactive: User is not inactive NotInactive: User is not inactive
ShouldBeActiveOrInitial: User is not active or inital ShouldBeActiveOrInitial: User is not active or inital
AlreadyInitialised: User is already initialised
NotInitialised: User is not yet initialised
NotLocked: User is not locked NotLocked: User is not locked
NoChanges: No changes found NoChanges: No changes found
InitCodeNotFound: Initialization Code not found InitCodeNotFound: Initialization Code not found

View File

@ -737,6 +737,9 @@ func (es *UserEventstore) RequestSetPassword(ctx context.Context, userID string,
if user.Human == nil { if user.Human == nil {
return errors.ThrowPreconditionFailed(nil, "EVENT-33ywz", "Errors.User.NotHuman") return errors.ThrowPreconditionFailed(nil, "EVENT-33ywz", "Errors.User.NotHuman")
} }
if user.State == usr_model.UserStateInitial {
return errors.ThrowPreconditionFailed(nil, "EVENT-Hs11s", "Errors.User.NotInitialised")
}
passwordCode := new(model.PasswordCode) passwordCode := new(model.PasswordCode)
err = es.generatePasswordCode(passwordCode, notifyType) err = es.generatePasswordCode(passwordCode, notifyType)
@ -754,6 +757,35 @@ func (es *UserEventstore) RequestSetPassword(ctx context.Context, userID string,
return nil return nil
} }
func (es *UserEventstore) ResendInitialMail(ctx context.Context, userID, email string) error {
if userID == "" {
return caos_errs.ThrowPreconditionFailed(nil, "EVENT-G4bmn", "Errors.User.UserIDMissing")
}
user, err := es.UserByID(ctx, userID)
if err != nil {
return err
}
if user.Human == nil {
return errors.ThrowPreconditionFailed(nil, "EVENT-Hfsww", "Errors.User.NotHuman")
}
if user.State != usr_model.UserStateInitial {
return errors.ThrowPreconditionFailed(nil, "EVENT-BGbbe", "Errors.User.AlreadyInitialised")
}
err = user.GenerateInitCodeIfNeeded(es.InitializeUserCode)
if err != nil {
return err
}
repoUser := model.UserFromModel(user)
agg := ResendInitialPasswordAggregate(es.AggregateCreator(), repoUser, user.InitCode, email)
err = es_sdk.Push(ctx, es.PushAggregates, repoUser.AppendEvents, agg)
if err != nil {
return err
}
es.userCache.cacheUser(repoUser)
return nil
}
func (es *UserEventstore) PasswordCodeSent(ctx context.Context, userID string) error { func (es *UserEventstore) PasswordCodeSent(ctx context.Context, userID string) error {
if userID == "" { if userID == "" {
return caos_errs.ThrowPreconditionFailed(nil, "EVENT-s09ow", "Errors.User.UserIDMissing") return caos_errs.ThrowPreconditionFailed(nil, "EVENT-s09ow", "Errors.User.UserIDMissing")
@ -946,6 +978,9 @@ func (es *UserEventstore) ChangeEmail(ctx context.Context, email *usr_model.Emai
if user.Human == nil { if user.Human == nil {
return nil, errors.ThrowPreconditionFailed(nil, "EVENT-tgBdL", "Errors.User.NotHuman") return nil, errors.ThrowPreconditionFailed(nil, "EVENT-tgBdL", "Errors.User.NotHuman")
} }
if user.State == usr_model.UserStateInitial {
return nil, errors.ThrowPreconditionFailed(nil, "EVENT-3H4q", "Errors.User.NotInitialised")
}
emailCode, err := email.GenerateEmailCodeIfNeeded(es.EmailVerificationCode) emailCode, err := email.GenerateEmailCodeIfNeeded(es.EmailVerificationCode)
if err != nil { if err != nil {
@ -1018,6 +1053,9 @@ func (es *UserEventstore) CreateEmailVerificationCode(ctx context.Context, userI
if user.Human == nil { if user.Human == nil {
return errors.ThrowPreconditionFailed(nil, "EVENT-hqUZP", "Errors.User.NotHuman") return errors.ThrowPreconditionFailed(nil, "EVENT-hqUZP", "Errors.User.NotHuman")
} }
if user.State == usr_model.UserStateInitial {
return errors.ThrowPreconditionFailed(nil, "EVENT-E3fbw", "Errors.User.NotInitialised")
}
if user.Email == nil { if user.Email == nil {
return caos_errs.ThrowPreconditionFailed(nil, "EVENT-pdo9s", "Errors.User.EmailNotFound") return caos_errs.ThrowPreconditionFailed(nil, "EVENT-pdo9s", "Errors.User.EmailNotFound")
} }

View File

@ -4,15 +4,15 @@ import (
"encoding/json" "encoding/json"
"time" "time"
"github.com/caos/zitadel/internal/id" "github.com/golang/mock/gomock"
mock_cache "github.com/caos/zitadel/internal/cache/mock" mock_cache "github.com/caos/zitadel/internal/cache/mock"
"github.com/caos/zitadel/internal/crypto" "github.com/caos/zitadel/internal/crypto"
"github.com/caos/zitadel/internal/eventstore/mock" "github.com/caos/zitadel/internal/eventstore/mock"
es_models "github.com/caos/zitadel/internal/eventstore/models" es_models "github.com/caos/zitadel/internal/eventstore/models"
"github.com/caos/zitadel/internal/id"
global_model "github.com/caos/zitadel/internal/model" global_model "github.com/caos/zitadel/internal/model"
"github.com/caos/zitadel/internal/user/repository/eventsourcing/model" "github.com/caos/zitadel/internal/user/repository/eventsourcing/model"
"github.com/golang/mock/gomock"
) )
func GetMockedEventstore(ctrl *gomock.Controller, mockEs *mock.MockEventstore) *UserEventstore { func GetMockedEventstore(ctrl *gomock.Controller, mockEs *mock.MockEventstore) *UserEventstore {
@ -162,11 +162,18 @@ func GetMockManipulateUserWithPasswordAndEmailCodeGen(ctrl *gomock.Controller, u
return GetMockedEventstoreWithPw(ctrl, mockEs, false, true, false, true) return GetMockedEventstoreWithPw(ctrl, mockEs, false, true, false, true)
} }
func GetMockManipulateUserWithEmailCodeGen(ctrl *gomock.Controller, user model.User) *UserEventstore { func GetMockManipulateUserWithEmailCodeGen(ctrl *gomock.Controller, user model.User, verified bool) *UserEventstore {
data, _ := json.Marshal(user) data, _ := json.Marshal(user)
events := []*es_models.Event{ events := []*es_models.Event{
{AggregateID: "AggregateID", AggregateVersion: "v1", Sequence: 1, Type: model.UserAdded, Data: data}, {AggregateID: "AggregateID", AggregateVersion: "v1", Sequence: 1, Type: model.UserAdded, Data: data},
} }
if verified {
email, _ := json.Marshal(model.Email{EmailAddress: "address"})
events = append(events,
&es_models.Event{AggregateID: "AggregateID", AggregateVersion: "v1", Sequence: 2, Type: model.HumanEmailVerified},
&es_models.Event{AggregateID: "AggregateID", AggregateVersion: "v1", Sequence: 3, Type: model.HumanEmailChanged, Data: email},
)
}
mockEs := mock.NewMockEventstore(ctrl) mockEs := mock.NewMockEventstore(ctrl)
mockEs.EXPECT().FilterEvents(gomock.Any(), gomock.Any()).Return(events, nil) mockEs.EXPECT().FilterEvents(gomock.Any(), gomock.Any()).Return(events, nil)
mockEs.EXPECT().AggregateCreator().Return(es_models.NewAggregateCreator("TEST")) mockEs.EXPECT().AggregateCreator().Return(es_models.NewAggregateCreator("TEST"))
@ -186,12 +193,15 @@ func GetMockManipulateUserWithPhoneCodeGen(ctrl *gomock.Controller, user model.U
return GetMockedEventstoreWithPw(ctrl, mockEs, false, false, true, false) return GetMockedEventstoreWithPw(ctrl, mockEs, false, false, true, false)
} }
func GetMockManipulateUserWithPasswordCodeGen(ctrl *gomock.Controller, user model.User) *UserEventstore { func GetMockManipulateUserWithPasswordCodeGen(ctrl *gomock.Controller, user model.User, verified bool) *UserEventstore {
data, _ := json.Marshal(user) data, _ := json.Marshal(user)
code, _ := json.Marshal(user.PasswordCode) code, _ := json.Marshal(user.PasswordCode)
events := []*es_models.Event{ events := []*es_models.Event{
{AggregateID: "AggregateID", AggregateVersion: "v1", Sequence: 1, Type: model.UserAdded, Data: data}, {AggregateID: "AggregateID", AggregateVersion: "v1", Sequence: 1, Type: model.UserAdded, Data: data},
{AggregateID: "AggregateID", AggregateVersion: "v1", Sequence: 1, Type: model.UserPasswordCodeAdded, Data: code}, {AggregateID: "AggregateID", AggregateVersion: "v1", Sequence: 2, Type: model.UserPasswordCodeAdded, Data: code},
}
if verified {
events = append(events, &es_models.Event{AggregateID: "AggregateID", AggregateVersion: "v1", Sequence: 3, Type: model.HumanEmailVerified})
} }
mockEs := mock.NewMockEventstore(ctrl) mockEs := mock.NewMockEventstore(ctrl)
mockEs.EXPECT().FilterEvents(gomock.Any(), gomock.Any()).Return(events, nil) mockEs.EXPECT().FilterEvents(gomock.Any(), gomock.Any()).Return(events, nil)
@ -382,7 +392,7 @@ func GetMockManipulateUserVerifiedPhone(ctrl *gomock.Controller) *UserEventstore
return GetMockedEventstore(ctrl, mockEs) return GetMockedEventstore(ctrl, mockEs)
} }
func GetMockManipulateUserFull(ctrl *gomock.Controller) *UserEventstore { func GetMockManipulateUserFull(ctrl *gomock.Controller, verified bool) *UserEventstore {
user := model.Human{ user := model.Human{
Profile: &model.Profile{ Profile: &model.Profile{
DisplayName: "DisplayName", DisplayName: "DisplayName",
@ -407,6 +417,9 @@ func GetMockManipulateUserFull(ctrl *gomock.Controller) *UserEventstore {
events := []*es_models.Event{ events := []*es_models.Event{
{AggregateID: "AggregateID", AggregateVersion: "v1", Sequence: 1, Type: model.UserAdded, Data: dataUser}, {AggregateID: "AggregateID", AggregateVersion: "v1", Sequence: 1, Type: model.UserAdded, Data: dataUser},
} }
if verified {
events = append(events, &es_models.Event{AggregateID: "AggregateID", AggregateVersion: "v1", Sequence: 2, Type: model.HumanEmailVerified})
}
mockEs := mock.NewMockEventstore(ctrl) mockEs := mock.NewMockEventstore(ctrl)
mockEs.EXPECT().FilterEvents(gomock.Any(), gomock.Any()).Return(events, nil) mockEs.EXPECT().FilterEvents(gomock.Any(), gomock.Any()).Return(events, nil)
mockEs.EXPECT().AggregateCreator().Return(es_models.NewAggregateCreator("TEST")) mockEs.EXPECT().AggregateCreator().Return(es_models.NewAggregateCreator("TEST"))

View File

@ -3,7 +3,6 @@ package eventsourcing
import ( import (
"context" "context"
"encoding/json" "encoding/json"
iam_model "github.com/caos/zitadel/internal/iam/model"
"net" "net"
"testing" "testing"
"time" "time"
@ -15,6 +14,7 @@ import (
"github.com/caos/zitadel/internal/crypto" "github.com/caos/zitadel/internal/crypto"
caos_errs "github.com/caos/zitadel/internal/errors" caos_errs "github.com/caos/zitadel/internal/errors"
es_models "github.com/caos/zitadel/internal/eventstore/models" es_models "github.com/caos/zitadel/internal/eventstore/models"
iam_model "github.com/caos/zitadel/internal/iam/model"
"github.com/caos/zitadel/internal/user/model" "github.com/caos/zitadel/internal/user/model"
repo_model "github.com/caos/zitadel/internal/user/repository/eventsourcing/model" repo_model "github.com/caos/zitadel/internal/user/repository/eventsourcing/model"
) )
@ -1287,7 +1287,7 @@ func TestPasswordID(t *testing.T) {
{ {
name: "get by id, ok", name: "get by id, ok",
args: args{ args: args{
es: GetMockManipulateUserFull(ctrl), es: GetMockManipulateUserFull(ctrl, false),
ctx: authz.NewMockContext("orgID", "userID"), ctx: authz.NewMockContext("orgID", "userID"),
user: &model.User{ObjectRoot: es_models.ObjectRoot{AggregateID: "AggregateID", Sequence: 1}}, user: &model.User{ObjectRoot: es_models.ObjectRoot{AggregateID: "AggregateID", Sequence: 1}},
}, },
@ -1366,7 +1366,7 @@ func TestSetOneTimePassword(t *testing.T) {
{ {
name: "create one time pw", name: "create one time pw",
args: args{ args: args{
es: GetMockManipulateUserWithPasswordCodeGen(ctrl, repo_model.User{ObjectRoot: es_models.ObjectRoot{AggregateID: "AggregateID"}, Human: &repo_model.Human{}}), es: GetMockManipulateUserWithPasswordCodeGen(ctrl, repo_model.User{ObjectRoot: es_models.ObjectRoot{AggregateID: "AggregateID"}, Human: &repo_model.Human{Email: &repo_model.Email{EmailAddress: "email"}}}, true),
ctx: authz.NewMockContext("orgID", "userID"), ctx: authz.NewMockContext("orgID", "userID"),
policy: &iam_model.PasswordComplexityPolicyView{}, policy: &iam_model.PasswordComplexityPolicyView{},
password: &model.Password{ObjectRoot: es_models.ObjectRoot{AggregateID: "AggregateID"}, SecretString: "Password"}, password: &model.Password{ObjectRoot: es_models.ObjectRoot{AggregateID: "AggregateID"}, SecretString: "Password"},
@ -1516,8 +1516,10 @@ func TestCheckPassword(t *testing.T) {
Algorithm: "hash", Algorithm: "hash",
Crypted: []byte("password"), Crypted: []byte("password"),
}}, }},
Email: &repo_model.Email{EmailAddress: "email"},
}, },
}, },
true,
), ),
ctx: authz.NewMockContext("orgID", "userID"), ctx: authz.NewMockContext("orgID", "userID"),
userID: "userID", userID: "userID",
@ -1582,8 +1584,10 @@ func TestSetPassword(t *testing.T) {
KeyID: "id", KeyID: "id",
Crypted: []byte("code"), Crypted: []byte("code"),
}}, }},
Email: &repo_model.Email{EmailAddress: "email"},
}, },
}, },
true,
), ),
ctx: authz.NewMockContext("orgID", "userID"), ctx: authz.NewMockContext("orgID", "userID"),
policy: &iam_model.PasswordComplexityPolicyView{}, policy: &iam_model.PasswordComplexityPolicyView{},
@ -1627,8 +1631,11 @@ func TestSetPassword(t *testing.T) {
es: GetMockManipulateUserWithPasswordCodeGen(ctrl, es: GetMockManipulateUserWithPasswordCodeGen(ctrl,
repo_model.User{ repo_model.User{
ObjectRoot: es_models.ObjectRoot{AggregateID: "AggregateID"}, ObjectRoot: es_models.ObjectRoot{AggregateID: "AggregateID"},
Human: &repo_model.Human{}, Human: &repo_model.Human{
Email: &repo_model.Email{EmailAddress: "email"},
}, },
},
true,
), ),
ctx: authz.NewMockContext("orgID", "userID"), ctx: authz.NewMockContext("orgID", "userID"),
policy: &iam_model.PasswordComplexityPolicyView{}, policy: &iam_model.PasswordComplexityPolicyView{},
@ -1653,8 +1660,10 @@ func TestSetPassword(t *testing.T) {
KeyID: "id", KeyID: "id",
Crypted: []byte("code2"), Crypted: []byte("code2"),
}}, }},
Email: &repo_model.Email{EmailAddress: "email"},
}, },
}, },
true,
), ),
ctx: authz.NewMockContext("orgID", "userID"), ctx: authz.NewMockContext("orgID", "userID"),
policy: &iam_model.PasswordComplexityPolicyView{}, policy: &iam_model.PasswordComplexityPolicyView{},
@ -1759,8 +1768,11 @@ func TestChangePassword(t *testing.T) {
es: GetMockManipulateUserWithPasswordCodeGen(ctrl, es: GetMockManipulateUserWithPasswordCodeGen(ctrl,
repo_model.User{ repo_model.User{
ObjectRoot: es_models.ObjectRoot{AggregateID: "AggregateID"}, ObjectRoot: es_models.ObjectRoot{AggregateID: "AggregateID"},
Human: &repo_model.Human{}, Human: &repo_model.Human{
Email: &repo_model.Email{EmailAddress: "email"},
}, },
},
true,
), ),
ctx: authz.NewMockContext("orgID", "userID"), ctx: authz.NewMockContext("orgID", "userID"),
policy: &iam_model.PasswordComplexityPolicyView{}, policy: &iam_model.PasswordComplexityPolicyView{},
@ -1784,8 +1796,10 @@ func TestChangePassword(t *testing.T) {
Algorithm: "hash", Algorithm: "hash",
Crypted: []byte("older"), Crypted: []byte("older"),
}}, }},
Email: &repo_model.Email{EmailAddress: "email"},
}, },
}, },
true,
), ),
ctx: authz.NewMockContext("orgID", "userID"), ctx: authz.NewMockContext("orgID", "userID"),
policy: &iam_model.PasswordComplexityPolicyView{}, policy: &iam_model.PasswordComplexityPolicyView{},
@ -1859,7 +1873,12 @@ func TestRequestSetPassword(t *testing.T) {
{ {
name: "create pw", name: "create pw",
args: args{ args: args{
es: GetMockManipulateUserWithPasswordCodeGen(ctrl, repo_model.User{ObjectRoot: es_models.ObjectRoot{AggregateID: "AggregateID"}, Human: &repo_model.Human{}}), es: GetMockManipulateUserWithPasswordCodeGen(ctrl, repo_model.User{
ObjectRoot: es_models.ObjectRoot{AggregateID: "AggregateID"},
Human: &repo_model.Human{
Email: &repo_model.Email{EmailAddress: "email"},
}},
true),
ctx: authz.NewMockContext("orgID", "userID"), ctx: authz.NewMockContext("orgID", "userID"),
userID: "AggregateID", userID: "AggregateID",
notifyType: model.NotificationTypeEmail, notifyType: model.NotificationTypeEmail,
@ -1868,6 +1887,18 @@ func TestRequestSetPassword(t *testing.T) {
password: &model.Password{ObjectRoot: es_models.ObjectRoot{AggregateID: "AggregateID", Sequence: 1}, ChangeRequired: false}, password: &model.Password{ObjectRoot: es_models.ObjectRoot{AggregateID: "AggregateID", Sequence: 1}, ChangeRequired: false},
}, },
}, },
{
name: "initial state",
args: args{
es: GetMockManipulateUserWithPasswordCodeGen(ctrl, repo_model.User{ObjectRoot: es_models.ObjectRoot{AggregateID: "AggregateID"}, Human: &repo_model.Human{}}, false),
ctx: authz.NewMockContext("orgID", "userID"),
userID: "AggregateID",
notifyType: model.NotificationTypeEmail,
},
res: res{
errFunc: caos_errs.IsPreconditionFailed,
},
},
{ {
name: "empty userid", name: "empty userid",
args: args{ args: args{
@ -1905,6 +1936,84 @@ func TestRequestSetPassword(t *testing.T) {
} }
} }
func TestResendInitialMail(t *testing.T) {
ctrl := gomock.NewController(t)
type args struct {
es *UserEventstore
ctx context.Context
userID string
mail string
}
type res struct {
password *model.Password
errFunc func(err error) bool
}
tests := []struct {
name string
args args
res res
}{
{
name: "resend ok",
args: args{
es: GetMockManipulateUserWithInitCode(ctrl, repo_model.User{ObjectRoot: es_models.ObjectRoot{AggregateID: "AggregateID"}, Human: &repo_model.Human{}}),
ctx: authz.NewMockContext("orgID", "userID"),
userID: "AggregateID",
mail: "",
},
res: res{
password: &model.Password{ObjectRoot: es_models.ObjectRoot{AggregateID: "AggregateID", Sequence: 1}, ChangeRequired: false},
},
},
{
name: "resend with email ok",
args: args{
es: GetMockManipulateUserWithInitCode(ctrl, repo_model.User{ObjectRoot: es_models.ObjectRoot{AggregateID: "AggregateID"}, Human: &repo_model.Human{}}),
ctx: authz.NewMockContext("orgID", "userID"),
userID: "AggregateID",
mail: "email",
},
res: res{
password: &model.Password{ObjectRoot: es_models.ObjectRoot{AggregateID: "AggregateID", Sequence: 1}, ChangeRequired: false},
},
},
{
name: "empty userid",
args: args{
es: GetMockManipulateUser(ctrl),
mail: "",
},
res: res{
errFunc: caos_errs.IsPreconditionFailed,
},
},
{
name: "existing user not found",
args: args{
es: GetMockManipulateUserNoEvents(ctrl),
ctx: authz.NewMockContext("orgID", "userID"),
userID: "AggregateID",
mail: "",
},
res: res{
errFunc: caos_errs.IsNotFound,
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := tt.args.es.ResendInitialMail(tt.args.ctx, tt.args.userID, tt.args.mail)
if tt.res.errFunc == nil && err != nil {
t.Errorf("should not get err")
}
if tt.res.errFunc != nil && !tt.res.errFunc(err) {
t.Errorf("got wrong err: %v ", err)
}
})
}
}
func TestPasswordCodeSent(t *testing.T) { func TestPasswordCodeSent(t *testing.T) {
ctrl := gomock.NewController(t) ctrl := gomock.NewController(t)
type args struct { type args struct {
@ -2170,7 +2279,7 @@ func TestProfileByID(t *testing.T) {
{ {
name: "get by id, ok", name: "get by id, ok",
args: args{ args: args{
es: GetMockManipulateUserFull(ctrl), es: GetMockManipulateUserFull(ctrl, false),
ctx: authz.NewMockContext("orgID", "userID"), ctx: authz.NewMockContext("orgID", "userID"),
user: &model.User{ObjectRoot: es_models.ObjectRoot{AggregateID: "AggregateID", Sequence: 1}, Human: &model.Human{}}, user: &model.User{ObjectRoot: es_models.ObjectRoot{AggregateID: "AggregateID", Sequence: 1}, Human: &model.Human{}},
}, },
@ -2237,7 +2346,7 @@ func TestChangeProfile(t *testing.T) {
{ {
name: "get by id, ok", name: "get by id, ok",
args: args{ args: args{
es: GetMockManipulateUserFull(ctrl), es: GetMockManipulateUserFull(ctrl, false),
ctx: authz.NewMockContext("orgID", "userID"), ctx: authz.NewMockContext("orgID", "userID"),
profile: &model.Profile{ObjectRoot: es_models.ObjectRoot{AggregateID: "AggregateID", Sequence: 1}, FirstName: "FirstName Changed", LastName: "LastName Changed"}, profile: &model.Profile{ObjectRoot: es_models.ObjectRoot{AggregateID: "AggregateID", Sequence: 1}, FirstName: "FirstName Changed", LastName: "LastName Changed"},
}, },
@ -2304,7 +2413,7 @@ func TestEmailByID(t *testing.T) {
{ {
name: "get by id, ok", name: "get by id, ok",
args: args{ args: args{
es: GetMockManipulateUserFull(ctrl), es: GetMockManipulateUserFull(ctrl, false),
ctx: authz.NewMockContext("orgID", "userID"), ctx: authz.NewMockContext("orgID", "userID"),
user: &model.User{ObjectRoot: es_models.ObjectRoot{AggregateID: "AggregateID", Sequence: 1}}, user: &model.User{ObjectRoot: es_models.ObjectRoot{AggregateID: "AggregateID", Sequence: 1}},
}, },
@ -2371,7 +2480,7 @@ func TestChangeEmail(t *testing.T) {
{ {
name: "change email address, verified", name: "change email address, verified",
args: args{ args: args{
es: GetMockManipulateUserFull(ctrl), es: GetMockManipulateUserFull(ctrl, true),
ctx: authz.NewMockContext("orgID", "userID"), ctx: authz.NewMockContext("orgID", "userID"),
email: &model.Email{ObjectRoot: es_models.ObjectRoot{AggregateID: "AggregateID", Sequence: 1}, EmailAddress: "EmailAddressChanged", IsEmailVerified: true}, email: &model.Email{ObjectRoot: es_models.ObjectRoot{AggregateID: "AggregateID", Sequence: 1}, EmailAddress: "EmailAddressChanged", IsEmailVerified: true},
}, },
@ -2389,7 +2498,7 @@ func TestChangeEmail(t *testing.T) {
Profile: &repo_model.Profile{DisplayName: "DisplayName"}, Profile: &repo_model.Profile{DisplayName: "DisplayName"},
Email: &repo_model.Email{EmailAddress: "EmailAddress"}, Email: &repo_model.Email{EmailAddress: "EmailAddress"},
}, },
}), }, true),
ctx: authz.NewMockContext("orgID", "userID"), ctx: authz.NewMockContext("orgID", "userID"),
email: &model.Email{ObjectRoot: es_models.ObjectRoot{AggregateID: "AggregateID", Sequence: 1}, EmailAddress: "EmailAddressChanged", IsEmailVerified: false}, email: &model.Email{ObjectRoot: es_models.ObjectRoot{AggregateID: "AggregateID", Sequence: 1}, EmailAddress: "EmailAddressChanged", IsEmailVerified: false},
}, },
@ -2397,6 +2506,24 @@ func TestChangeEmail(t *testing.T) {
email: &model.Email{ObjectRoot: es_models.ObjectRoot{AggregateID: "AggregateID", Sequence: 1}, EmailAddress: "EmailAddressChanged", IsEmailVerified: false}, email: &model.Email{ObjectRoot: es_models.ObjectRoot{AggregateID: "AggregateID", Sequence: 1}, EmailAddress: "EmailAddressChanged", IsEmailVerified: false},
}, },
}, },
{
name: "user state initial",
args: args{
es: GetMockManipulateUserWithEmailCodeGen(ctrl, repo_model.User{
ObjectRoot: es_models.ObjectRoot{AggregateID: "AggregateID", Sequence: 1},
UserName: "UserName",
Human: &repo_model.Human{
Profile: &repo_model.Profile{DisplayName: "DisplayName"},
Email: &repo_model.Email{EmailAddress: "EmailAddress"},
},
}, false),
ctx: authz.NewMockContext("orgID", "userID"),
email: &model.Email{ObjectRoot: es_models.ObjectRoot{AggregateID: "AggregateID", Sequence: 1}, EmailAddress: "EmailAddressChanged", IsEmailVerified: false},
},
res: res{
errFunc: caos_errs.IsPreconditionFailed,
},
},
{ {
name: "empty userid", name: "empty userid",
args: args{ args: args{
@ -2423,7 +2550,9 @@ func TestChangeEmail(t *testing.T) {
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
result, err := tt.args.es.ChangeEmail(tt.args.ctx, tt.args.email) result, err := tt.args.es.ChangeEmail(tt.args.ctx, tt.args.email)
if (tt.res.errFunc != nil && !tt.res.errFunc(err)) || (tt.res.errFunc == nil && err != nil) {
t.Errorf("got wrong err: %v ", err)
}
if tt.res.errFunc == nil && result.AggregateID == "" { if tt.res.errFunc == nil && result.AggregateID == "" {
t.Errorf("result has no id") t.Errorf("result has no id")
} }
@ -2433,9 +2562,6 @@ func TestChangeEmail(t *testing.T) {
if tt.res.errFunc == nil && result.IsEmailVerified != tt.res.email.IsEmailVerified { if tt.res.errFunc == nil && result.IsEmailVerified != tt.res.email.IsEmailVerified {
t.Errorf("got wrong result change required: expected: %v, actual: %v ", tt.res.email.IsEmailVerified, result.IsEmailVerified) t.Errorf("got wrong result change required: expected: %v, actual: %v ", tt.res.email.IsEmailVerified, result.IsEmailVerified)
} }
if tt.res.errFunc != nil && !tt.res.errFunc(err) {
t.Errorf("got wrong err: %v ", err)
}
}) })
} }
} }
@ -2551,12 +2677,30 @@ func TestCreateEmailVerificationCode(t *testing.T) {
Human: &repo_model.Human{ Human: &repo_model.Human{
Profile: &repo_model.Profile{DisplayName: "DisplayName"}, Profile: &repo_model.Profile{DisplayName: "DisplayName"},
Email: &repo_model.Email{EmailAddress: "EmailAddress"}, Email: &repo_model.Email{EmailAddress: "EmailAddress"},
}}), }}, true),
ctx: authz.NewMockContext("orgID", "userID"), ctx: authz.NewMockContext("orgID", "userID"),
userID: "userID", userID: "userID",
}, },
res: res{}, res: res{},
}, },
{
name: "initial state",
args: args{
es: GetMockManipulateUserWithEmailCodeGen(ctrl, repo_model.User{
ObjectRoot: es_models.ObjectRoot{AggregateID: "AggregateID", Sequence: 1},
UserName: "UserName",
Human: &repo_model.Human{
Profile: &repo_model.Profile{DisplayName: "DisplayName"},
Email: &repo_model.Email{EmailAddress: "EmailAddress"},
},
}, false),
ctx: authz.NewMockContext("orgID", "userID"),
userID: "userID",
},
res: res{
errFunc: caos_errs.IsPreconditionFailed,
},
},
{ {
name: "empty userid", name: "empty userid",
args: args{ args: args{
@ -2606,7 +2750,7 @@ func TestCreateEmailVerificationCode(t *testing.T) {
err := tt.args.es.CreateEmailVerificationCode(tt.args.ctx, tt.args.userID) err := tt.args.es.CreateEmailVerificationCode(tt.args.ctx, tt.args.userID)
if tt.res.errFunc == nil && err != nil { if tt.res.errFunc == nil && err != nil {
t.Errorf("should not ger err") t.Errorf("should not get err, got: %v", err)
} }
if tt.res.errFunc != nil && !tt.res.errFunc(err) { if tt.res.errFunc != nil && !tt.res.errFunc(err) {
t.Errorf("got wrong err: %v ", err) t.Errorf("got wrong err: %v ", err)
@ -2695,7 +2839,7 @@ func TestPhoneByID(t *testing.T) {
{ {
name: "get by id, ok", name: "get by id, ok",
args: args{ args: args{
es: GetMockManipulateUserFull(ctrl), es: GetMockManipulateUserFull(ctrl, false),
ctx: authz.NewMockContext("orgID", "userID"), ctx: authz.NewMockContext("orgID", "userID"),
user: &model.User{ObjectRoot: es_models.ObjectRoot{AggregateID: "AggregateID", Sequence: 1}}, user: &model.User{ObjectRoot: es_models.ObjectRoot{AggregateID: "AggregateID", Sequence: 1}},
}, },
@ -2762,7 +2906,7 @@ func TestChangePhone(t *testing.T) {
{ {
name: "change phone, verified", name: "change phone, verified",
args: args{ args: args{
es: GetMockManipulateUserFull(ctrl), es: GetMockManipulateUserFull(ctrl, false),
ctx: authz.NewMockContext("orgID", "userID"), ctx: authz.NewMockContext("orgID", "userID"),
phone: &model.Phone{ObjectRoot: es_models.ObjectRoot{AggregateID: "AggregateID", Sequence: 1}, PhoneNumber: "0711234567", IsPhoneVerified: true}, phone: &model.Phone{ObjectRoot: es_models.ObjectRoot{AggregateID: "AggregateID", Sequence: 1}, PhoneNumber: "0711234567", IsPhoneVerified: true},
}, },
@ -3149,7 +3293,7 @@ func TestAddressByID(t *testing.T) {
{ {
name: "get by id, ok", name: "get by id, ok",
args: args{ args: args{
es: GetMockManipulateUserFull(ctrl), es: GetMockManipulateUserFull(ctrl, false),
ctx: authz.NewMockContext("orgID", "userID"), ctx: authz.NewMockContext("orgID", "userID"),
user: &model.User{ObjectRoot: es_models.ObjectRoot{AggregateID: "AggregateID", Sequence: 1}}, user: &model.User{ObjectRoot: es_models.ObjectRoot{AggregateID: "AggregateID", Sequence: 1}},
}, },
@ -3216,7 +3360,7 @@ func TestChangeAddress(t *testing.T) {
{ {
name: "change address ok", name: "change address ok",
args: args{ args: args{
es: GetMockManipulateUserFull(ctrl), es: GetMockManipulateUserFull(ctrl, false),
ctx: authz.NewMockContext("orgID", "userID"), ctx: authz.NewMockContext("orgID", "userID"),
address: &model.Address{ObjectRoot: es_models.ObjectRoot{AggregateID: "AggregateID", Sequence: 1}, Country: "CountryChanged"}, address: &model.Address{ObjectRoot: es_models.ObjectRoot{AggregateID: "AggregateID", Sequence: 1}, Country: "CountryChanged"},
}, },

View File

@ -189,7 +189,7 @@ func (h *Human) AppendEvent(event *es_models.Event) (err error) {
} }
func (h *Human) ComputeObject() { func (h *Human) ComputeObject() {
if h.State == int32(model.UserStateUnspecified) { if h.State == int32(model.UserStateUnspecified) || h.State == int32(model.UserStateInitial) {
if h.Email != nil && h.IsEmailVerified { if h.Email != nil && h.IsEmailVerified {
h.State = int32(model.UserStateActive) h.State = int32(model.UserStateActive)
} else { } else {

View File

@ -2,14 +2,15 @@ package eventsourcing
import ( import (
"context" "context"
"github.com/caos/zitadel/internal/api/authz"
iam_es_model "github.com/caos/zitadel/internal/iam/repository/eventsourcing/model"
"strings" "strings"
"github.com/caos/zitadel/internal/api/authz"
"github.com/caos/zitadel/internal/errors" "github.com/caos/zitadel/internal/errors"
es_models "github.com/caos/zitadel/internal/eventstore/models" es_models "github.com/caos/zitadel/internal/eventstore/models"
es_sdk "github.com/caos/zitadel/internal/eventstore/sdk" es_sdk "github.com/caos/zitadel/internal/eventstore/sdk"
iam_es_model "github.com/caos/zitadel/internal/iam/repository/eventsourcing/model"
org_es_model "github.com/caos/zitadel/internal/org/repository/eventsourcing/model" org_es_model "github.com/caos/zitadel/internal/org/repository/eventsourcing/model"
usr_model "github.com/caos/zitadel/internal/user/model"
"github.com/caos/zitadel/internal/user/repository/eventsourcing/model" "github.com/caos/zitadel/internal/user/repository/eventsourcing/model"
) )
@ -454,6 +455,25 @@ func RequestSetPassword(aggCreator *es_models.AggregateCreator, user *model.User
} }
} }
func ResendInitialPasswordAggregate(aggCreator *es_models.AggregateCreator, user *model.User, code *usr_model.InitUserCode, email string) func(ctx context.Context) (*es_models.Aggregate, error) {
return func(ctx context.Context) (*es_models.Aggregate, error) {
if code == nil {
return nil, errors.ThrowPreconditionFailed(nil, "EVENT-dfs3q", "Errors.Internal")
}
agg, err := UserAggregate(ctx, aggCreator, user)
if err != nil {
return nil, err
}
if email != "" && user.Email != nil && email != user.Email.EmailAddress {
agg, err = agg.AppendEvent(model.HumanEmailChanged, map[string]interface{}{"email": email})
if err != nil {
return nil, err
}
}
return agg.AppendEvent(model.InitializedHumanCodeAdded, code)
}
}
func PasswordCodeSentAggregate(aggCreator *es_models.AggregateCreator, user *model.User) func(ctx context.Context) (*es_models.Aggregate, error) { func PasswordCodeSentAggregate(aggCreator *es_models.AggregateCreator, user *model.User) func(ctx context.Context) (*es_models.Aggregate, error) {
return func(ctx context.Context) (*es_models.Aggregate, error) { return func(ctx context.Context) (*es_models.Aggregate, error) {
agg, err := UserAggregate(ctx, aggCreator, user) agg, err := UserAggregate(ctx, aggCreator, user)

View File

@ -5,11 +5,11 @@ import (
"testing" "testing"
"time" "time"
"github.com/caos/zitadel/internal/crypto"
"github.com/caos/zitadel/internal/api/authz" "github.com/caos/zitadel/internal/api/authz"
"github.com/caos/zitadel/internal/crypto"
caos_errs "github.com/caos/zitadel/internal/errors" caos_errs "github.com/caos/zitadel/internal/errors"
"github.com/caos/zitadel/internal/eventstore/models" "github.com/caos/zitadel/internal/eventstore/models"
usr_model "github.com/caos/zitadel/internal/user/model"
"github.com/caos/zitadel/internal/user/repository/eventsourcing/model" "github.com/caos/zitadel/internal/user/repository/eventsourcing/model"
) )
@ -1199,6 +1199,114 @@ func TestRequestSetPasswordAggregate(t *testing.T) {
} }
} }
func TestResendInitialPasswordAggregate(t *testing.T) {
type args struct {
ctx context.Context
user *model.User
aggCreator *models.AggregateCreator
initcode *usr_model.InitUserCode
email string
}
type res struct {
eventLen int
eventType models.EventType
errFunc func(err error) bool
}
tests := []struct {
name string
args args
res res
}{
{
name: "resend initial password aggregate ok",
args: args{
ctx: authz.NewMockContext("orgID", "userID"),
user: &model.User{
ObjectRoot: models.ObjectRoot{AggregateID: "ID"},
UserName: "UserName",
Human: &model.Human{
Profile: &model.Profile{DisplayName: "DisplayName"},
Email: &model.Email{},
},
},
aggCreator: models.NewAggregateCreator("Test"),
initcode: &usr_model.InitUserCode{Expiry: time.Hour * 1},
},
res: res{
eventLen: 1,
},
},
{
name: "resend initial password with same email ok",
args: args{
ctx: authz.NewMockContext("orgID", "userID"),
user: &model.User{
ObjectRoot: models.ObjectRoot{AggregateID: "ID"},
UserName: "UserName",
Human: &model.Human{
Profile: &model.Profile{DisplayName: "DisplayName"},
Email: &model.Email{EmailAddress: "email"},
},
},
aggCreator: models.NewAggregateCreator("Test"),
initcode: &usr_model.InitUserCode{Expiry: time.Hour * 1},
email: "email",
},
res: res{
eventLen: 1,
},
},
{
name: "resend initial password with new email ok",
args: args{
ctx: authz.NewMockContext("orgID", "userID"),
user: &model.User{
ObjectRoot: models.ObjectRoot{AggregateID: "ID"},
UserName: "UserName",
Human: &model.Human{
Profile: &model.Profile{DisplayName: "DisplayName"},
Email: &model.Email{EmailAddress: "old"},
},
},
aggCreator: models.NewAggregateCreator("Test"),
initcode: &usr_model.InitUserCode{Expiry: time.Hour * 1},
email: "new",
},
res: res{
eventLen: 2,
},
},
{
name: "request nil",
args: args{
ctx: authz.NewMockContext("orgID", "userID"),
user: &model.User{
ObjectRoot: models.ObjectRoot{AggregateID: "ID"},
UserName: "UserName",
Human: &model.Human{
Profile: &model.Profile{DisplayName: "DisplayName"},
},
},
aggCreator: models.NewAggregateCreator("Test"),
},
res: res{
errFunc: caos_errs.IsPreconditionFailed,
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
agg, err := ResendInitialPasswordAggregate(tt.args.aggCreator, tt.args.user, tt.args.initcode, tt.args.email)(tt.args.ctx)
if (tt.res.errFunc == nil && err != nil) || (tt.res.errFunc != nil && !tt.res.errFunc(err)) {
t.Errorf("got wrong err: %v ", err)
}
if tt.res.errFunc == nil && len(agg.Events) != tt.res.eventLen {
t.Errorf("got wrong event len: expected: %v, actual: %v ", tt.res.eventLen, len(agg.Events))
}
})
}
}
func TestPasswordCodeSentAggregate(t *testing.T) { func TestPasswordCodeSentAggregate(t *testing.T) {
type args struct { type args struct {
ctx context.Context ctx context.Context

View File

@ -184,6 +184,11 @@ var ManagementService_AuthMethods = authz.MethodMapping{
CheckParam: "", CheckParam: "",
}, },
"/caos.zitadel.management.api.v1.ManagementService/ResendInitialMail": authz.Option{
Permission: "user.write",
CheckParam: "",
},
"/caos.zitadel.management.api.v1.ManagementService/SearchUserMemberships": authz.Option{ "/caos.zitadel.management.api.v1.ManagementService/SearchUserMemberships": authz.Option{
Permission: "user.membership.read", Permission: "user.membership.read",
CheckParam: "", CheckParam: "",

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -4316,6 +4316,82 @@ var _ interface {
ErrorName() string ErrorName() string
} = SetPasswordNotificationRequestValidationError{} } = SetPasswordNotificationRequestValidationError{}
// Validate checks the field values on InitialMailRequest with the rules
// defined in the proto definition for this message. If any rules are
// violated, an error is returned.
func (m *InitialMailRequest) Validate() error {
if m == nil {
return nil
}
if utf8.RuneCountInString(m.GetId()) < 1 {
return InitialMailRequestValidationError{
field: "Id",
reason: "value length must be at least 1 runes",
}
}
// no validation rules for Email
return nil
}
// InitialMailRequestValidationError is the validation error returned by
// InitialMailRequest.Validate if the designated constraints aren't met.
type InitialMailRequestValidationError struct {
field string
reason string
cause error
key bool
}
// Field function returns field value.
func (e InitialMailRequestValidationError) Field() string { return e.field }
// Reason function returns reason value.
func (e InitialMailRequestValidationError) Reason() string { return e.reason }
// Cause function returns cause value.
func (e InitialMailRequestValidationError) Cause() error { return e.cause }
// Key function returns key value.
func (e InitialMailRequestValidationError) Key() bool { return e.key }
// ErrorName returns error name.
func (e InitialMailRequestValidationError) ErrorName() string {
return "InitialMailRequestValidationError"
}
// Error satisfies the builtin error interface
func (e InitialMailRequestValidationError) Error() string {
cause := ""
if e.cause != nil {
cause = fmt.Sprintf(" | caused by: %v", e.cause)
}
key := ""
if e.key {
key = "key for "
}
return fmt.Sprintf(
"invalid %sInitialMailRequest.%s: %s%s",
key,
e.field,
e.reason,
cause)
}
var _ error = InitialMailRequestValidationError{}
var _ interface {
Field() string
Reason() string
Key() bool
Cause() error
ErrorName() string
} = InitialMailRequestValidationError{}
// Validate checks the field values on OrgIamPolicyView with the rules defined // Validate checks the field values on OrgIamPolicyView with the rules defined
// in the proto definition for this message. If any rules are violated, an // in the proto definition for this message. If any rules are violated, an
// error is returned. // error is returned.

File diff suppressed because it is too large Load Diff

View File

@ -2157,6 +2157,26 @@ func (mr *MockManagementServiceClientMockRecorder) ResendEmailVerificationMail(a
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ResendEmailVerificationMail", reflect.TypeOf((*MockManagementServiceClient)(nil).ResendEmailVerificationMail), varargs...) return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ResendEmailVerificationMail", reflect.TypeOf((*MockManagementServiceClient)(nil).ResendEmailVerificationMail), varargs...)
} }
// ResendInitialMail mocks base method
func (m *MockManagementServiceClient) ResendInitialMail(arg0 context.Context, arg1 *management.InitialMailRequest, arg2 ...grpc.CallOption) (*emptypb.Empty, error) {
m.ctrl.T.Helper()
varargs := []interface{}{arg0, arg1}
for _, a := range arg2 {
varargs = append(varargs, a)
}
ret := m.ctrl.Call(m, "ResendInitialMail", varargs...)
ret0, _ := ret[0].(*emptypb.Empty)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// ResendInitialMail indicates an expected call of ResendInitialMail
func (mr *MockManagementServiceClientMockRecorder) ResendInitialMail(arg0, arg1 interface{}, arg2 ...interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]interface{}{arg0, arg1}, arg2...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ResendInitialMail", reflect.TypeOf((*MockManagementServiceClient)(nil).ResendInitialMail), varargs...)
}
// ResendPhoneVerificationCode mocks base method // ResendPhoneVerificationCode mocks base method
func (m *MockManagementServiceClient) ResendPhoneVerificationCode(arg0 context.Context, arg1 *management.UserID, arg2 ...grpc.CallOption) (*emptypb.Empty, error) { func (m *MockManagementServiceClient) ResendPhoneVerificationCode(arg0 context.Context, arg1 *management.UserID, arg2 ...grpc.CallOption) (*emptypb.Empty, error) {
m.ctrl.T.Helper() m.ctrl.T.Helper()

View File

@ -424,6 +424,17 @@ rpc GetUserByID(UserID) returns (UserView) {
}; };
} }
rpc ResendInitialMail(InitialMailRequest) returns (google.protobuf.Empty) {
option (google.api.http) = {
post: "/users/{id}/_resendinitialisation"
body: "*"
};
option (caos.zitadel.utils.v1.auth_option) = {
permission: "user.write"
};
}
rpc SearchUserMemberships(UserMembershipSearchRequest) returns (UserMembershipSearchResponse) { rpc SearchUserMemberships(UserMembershipSearchRequest) returns (UserMembershipSearchResponse) {
option (google.api.http) = { option (google.api.http) = {
post: "/users/{user_id}/memberships/_search" post: "/users/{user_id}/memberships/_search"
@ -2049,6 +2060,11 @@ enum NotificationType {
NOTIFICATIONTYPE_SMS = 1; NOTIFICATIONTYPE_SMS = 1;
} }
message InitialMailRequest {
string id = 1 [(validate.rules).string.min_len = 1];
string email = 2;
}
enum PolicyState { enum PolicyState {
POLICYSTATE_UNSPECIFIED = 0; POLICYSTATE_UNSPECIFIED = 0;
POLICYSTATE_ACTIVE = 1; POLICYSTATE_ACTIVE = 1;