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;
border-radius: .5rem;
padding-top: 1rem;
min-width: 350px;
.header {
margin-top: 0;

View File

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

View File

@ -40,10 +40,15 @@
<app-card *ngIf="user" title="{{ 'USER.LOGINMETHODS.TITLE' | translate }}"
description="{{ 'USER.LOGINMETHODS.DESCRIPTION' | translate }}">
<app-contact *ngIf="user?.human" [human]="user.human" (savedPhone)="savePhone($event)"
(savedEmail)="saveEmail($event)" (enteredPhoneCode)="enteredPhoneCode($event)"
(deletedPhone)="deletePhone()" (resendEmailVerification)="resendEmailVerification()"
(resendPhoneVerification)="resendPhoneVerification()"></app-contact>
<button card-actions mat-icon-button (click)="refreshUser()">
<mat-icon>refresh</mat-icon>
</button>
<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-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 { MatDialog } from '@angular/material/dialog';
import { TranslateService } from '@ngx-translate/core';
import { Subscription } from 'rxjs';
import { ChangeType } from 'src/app/modules/changes/changes.component';
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 { ToastService } from 'src/app/services/toast.service';
import { EditDialogType } from '../user-detail/user-detail.component';
import { EditDialogComponent } from './edit-dialog/edit-dialog.component';
@Component({
selector: 'app-auth-user-detail',
templateUrl: './auth-user-detail.component.html',
@ -26,6 +38,7 @@ export class AuthUserDetailComponent implements OnDestroy {
public ChangeType: any = ChangeType;
public userLoginMustBeDomain: boolean = false;
public UserState: any = UserState;
public USERGRANTCONTEXT: UserGrantContext = UserGrantContext.USER;
@ -33,8 +46,13 @@ export class AuthUserDetailComponent implements OnDestroy {
public translate: TranslateService,
private toast: ToastService,
public userService: GrpcAuthService,
private dialog: MatDialog,
) {
this.loading = true;
this.refreshUser();
}
refreshUser(): void {
this.userService.GetMyUser().then(user => {
this.user = user.toObject();
this.loading = false;
@ -81,6 +99,7 @@ export class AuthUserDetailComponent implements OnDestroy {
this.toast.showInfo('USER.TOAST.EMAILSAVED', true);
if (this.user.human) {
this.user.human.email = data.toObject().email;
this.refreshUser();
}
}).catch(error => {
this.toast.showError(error);
@ -90,6 +109,7 @@ export class AuthUserDetailComponent implements OnDestroy {
public enteredPhoneCode(code: string): void {
this.userService.VerifyMyUserPhone(code).then(() => {
this.toast.showInfo('USER.TOAST.PHONESAVED', true);
this.refreshUser();
}).catch(error => {
this.toast.showError(error);
});
@ -99,14 +119,6 @@ export class AuthUserDetailComponent implements OnDestroy {
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 {
this.userService.ResendPhoneVerification().then(() => {
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 {
this.userService.RemoveMyUserPhone().then(() => {
this.toast.showInfo('USER.TOAST.PHONEREMOVED', true);
if (this.user.human) {
this.user.human.phone = '';
this.refreshUser();
}
}).catch(error => {
this.toast.showError(error);
@ -133,10 +154,54 @@ export class AuthUserDetailComponent implements OnDestroy {
this.toast.showInfo('USER.TOAST.PHONESAVED', true);
if (this.user.human) {
this.user.human.phone = data.toObject().phone;
this.refreshUser();
}
}).catch(error => {
this.toast.showError(error);
});
}
}
public openEditDialog(type: EditDialogType): void {
switch (type) {
case EditDialogType.PHONE:
const dialogRefPhone = this.dialog.open(EditDialogComponent, {
data: {
confirmKey: 'ACTIONS.SAVE',
cancelKey: 'ACTIONS.CANCEL',
labelKey: 'ACTIONS.NEWVALUE',
titleKey: 'USER.LOGINMETHODS.PHONE.EDITTITLE',
descriptionKey: 'USER.LOGINMETHODS.PHONE.EDITDESC',
value: this.user.human?.phone,
},
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-row">
<div class="left">
<span class="label">{{ 'USER.PROFILE.PASSWORD' | translate }}</span>
<span class="name">*********</span>
<span>*********</span>
<div class="overflow">
<ng-content select="[phoneAction]"></ng-content>
<a [disabled]="!canWrite" [routerLink]="['password']" mat-icon-button>
<mat-icon class="icon">chevron_right</mat-icon>
<ng-content select="[pwdAction]"></ng-content>
</div>
<div class="right">
<a matTooltip="{{'USER.PASSWORD.SET' | translate}}" [disabled]="!canWrite" [routerLink]="['password']"
mat-icon-button>
<i class="las la-edit"></i>
</a>
</div>
</div>
<div class="method-row">
<div class="left">
<span class="label">{{ 'USER.EMAIL' | translate }}</span>
<ng-container *ngIf="!emailEditState; else emailEdit">
<div class="actions">
<span class="name">{{human?.email}}</span>
<mat-icon class="icon" *ngIf="human?.isEmailVerified" color="primary" aria-hidden="false"
aria-label="verified icon">
check_circle_outline</mat-icon>
<ng-container *ngIf="human?.email && !human?.isEmailVerified">
<mat-icon class="icon" color="warn" aria-hidden="false" aria-label="not verified icon">
highlight_off
</mat-icon>
<a *ngIf="canWrite" class="verify" matTooltip="{{'USER.LOGINMETHODS.EMAIL.RESEND' | translate}}"
<span *ngIf="human?.isEmailVerified" class="state verified">{{'USER.EMAILVERIFIED' | translate}}</span>
<div *ngIf="!human?.isEmailVerified" class="block">
<span class="state notverified">{{'USER.NOTVERIFIED' | translate}}</span>
<ng-container *ngIf="human?.email">
<a *ngIf="canWrite && state != userStateEnum?.USERSTATE_INITIAL" class="verify"
matTooltip="{{'USER.LOGINMETHODS.EMAIL.RESEND' | translate}}"
(click)="emitEmailVerification()">{{'USER.LOGINMETHODS.RESENDCODE' | translate}}</a>
</ng-container>
</div>
<div>
<button [disabled]="!canWrite" (click)="emailEditState = true" mat-icon-button>
<mat-icon class="icon">edit</mat-icon>
<ng-content select="[emailAction]"></ng-content>
</div>
<div class="right">
<button [disabled]="!canWrite" (click)="openEditDialog(EditDialogType.EMAIL)" mat-icon-button>
<i class="las la-edit"></i>
</button>
</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 class="method-row">
<div class="left">
<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">
<div class="actions">
<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"
<ng-container *ngIf="human?.phone">
<a *ngIf="!disablePhoneCode && canWrite" class="verify"
matTooltip="{{'USER.LOGINMETHODS.ENTERCODE_DESC' | translate}}"
(click)="enterCode()">{{'USER.LOGINMETHODS.ENTERCODE' | translate}}</a>
<a *ngIf="canWrite" class="verify" matTooltip="{{'USER.LOGINMETHODS.PHONE.RESEND' | translate}}"
@ -71,27 +57,18 @@
</ng-container>
</div>
<div>
<button [disabled]="!canWrite" (click)="phoneEditState = true" mat-icon-button>
<mat-icon class="icon">edit</mat-icon>
</button>
<ng-content select="[phoneAction]"></ng-content>
</div>
</ng-container>
<ng-template #phoneEdit>
<mat-form-field class="name">
<mat-label>{{ 'USER.PHONE' | translate }}</mat-label>
<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>
<div class="right">
<button matTooltip="{{'ACTIONS.DELETE' | translate}}" *ngIf="human && human.phone" color="warn"
(click)="emitDeletePhone()" mat-icon-button>
<i class="las la-trash"></i>
</button>
<button *ngIf="human" [disabled]="!human.phone" type="button" color="primary" (click)="savePhone()"
mat-raised-button>{{ 'ACTIONS.SAVE' | translate }}</button>
</ng-template>
<button matTooltip="{{'ACTIONS.EDIT' | translate}}" [disabled]="!canWrite"
(click)="openEditDialog(EditDialogType.PHONE)" mat-icon-button>
<i class="las la-edit"></i>
</button>
</div>
</div>
</div>

View File

@ -5,29 +5,57 @@
.method-row {
display: flex;
align-items: center;
justify-content: space-between;
align-items: center;
padding: .5rem;
border-bottom: 1px solid #ffffff20;
flex-wrap: wrap;
.actions {
flex: 1;
.left {
.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;
justify-content: flex-end;
align-items: center;
flex-direction: column;
min-width: 150px;
}
.label {
font-size: .9rem;
max-width: 100px;
color: var(--grey);
}
.icon {
margin: .5rem;
.verified-icon {
font-size: 1.2rem;
line-height: 1.2rem;
height: 1.2rem;
cursor: default;
}
.verify {
@ -38,6 +66,7 @@
cursor: pointer;
word-wrap: none;
white-space: nowrap;
margin-right: 1rem;
&:hover {
text-decoration: underline;
@ -46,6 +75,6 @@
}
}
.overflow {
overflow: auto;
.mat-form-field-wrapper {
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 { HumanView as AuthHumanView } from 'src/app/proto/generated/auth_pb';
import { HumanView as MgmtHumanView } from 'src/app/proto/generated/management_pb';
import { WarnDialogComponent } from 'src/app/modules/warn-dialog/warn-dialog.component';
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 { EditDialogType } from '../user-detail/user-detail.component';
@Component({
selector: 'app-contact',
templateUrl: './contact.component.html',
styleUrls: ['./contact.component.scss'],
})
export class ContactComponent implements OnInit {
export class ContactComponent {
@Input() disablePhoneCode: boolean = false;
@Input() canWrite: boolean = false;
@Input() human!: AuthHumanView.AsObject | MgmtHumanView.AsObject;
@Output() savedPhone: EventEmitter<string> = new EventEmitter();
@Output() savedEmail: EventEmitter<string> = new EventEmitter();
@Input() state!: AuthUserState | MgmtUserState;
@Output() editType: EventEmitter<EditDialogType> = new EventEmitter();
@Output() resendEmailVerification: EventEmitter<void> = new EventEmitter();
@Output() resendPhoneVerification: EventEmitter<void> = new EventEmitter();
@Output() enteredPhoneCode: EventEmitter<string> = new EventEmitter();
@Output() deletedPhone: EventEmitter<void> = new EventEmitter();
@Input() public userStateEnum: any;
public emailEditState: boolean = false;
public phoneEditState: boolean = false;
public EditDialogType: any = EditDialogType;
constructor(private dialog: MatDialog) { }
ngOnInit(): void {
}
savePhone(): void {
this.phoneEditState = false;
this.savedPhone.emit(this.human.phone);
}
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();
}
saveEmail(): void {
this.emailEditState = false;
this.savedEmail.emit(this.human.email);
});
}
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 { CodeDialogComponent } from './auth-user-detail/code-dialog/code-dialog.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 { ContactComponent } from './contact/contact.component';
import { DetailFormMachineModule } from './detail-form-machine/detail-form-machine.module';
import { DetailFormModule } from './detail-form/detail-form.module';
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 { UserDetailComponent } from './user-detail/user-detail.component';
import { UserMfaComponent } from './user-detail/user-mfa/user-mfa.component';
import { ContactComponent } from './contact/contact.component';
@NgModule({
declarations: [
AuthUserDetailComponent,
UserDetailComponent,
DialogOtpComponent,
EditDialogComponent,
AuthUserMfaComponent,
UserMfaComponent,
ThemeSettingComponent,
@ -62,6 +65,7 @@ import { ContactComponent } from './contact/contact.component';
MachineKeysComponent,
ExternalIdpsComponent,
ContactComponent,
ResendEmailDialogComponent,
],
imports: [
UserDetailRoutingModule,

View File

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

View File

@ -1,9 +1,9 @@
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 { ActivatedRoute } from '@angular/router';
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 { UserGrantContext } from 'src/app/modules/user-grants/user-grants-datasource';
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 { 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({
selector: 'app-user-detail',
templateUrl: './user-detail.component.html',
styleUrls: ['./user-detail.component.scss'],
})
export class UserDetailComponent implements OnInit, OnDestroy {
export class UserDetailComponent implements OnInit {
public user!: UserView.AsObject;
public genders: Gender[] = [Gender.GENDER_MALE, Gender.GENDER_FEMALE, Gender.GENDER_DIVERSE];
public languages: string[] = ['de', 'en'];
private subscription: Subscription = new Subscription();
public ChangeType: any = ChangeType;
public loading: boolean = false;
@ -40,6 +46,8 @@ export class UserDetailComponent implements OnInit, OnDestroy {
public copied: string = '';
public USERGRANTCONTEXT: UserGrantContext = UserGrantContext.USER;
public EditDialogType: any = EditDialogType;
constructor(
public translate: TranslateService,
private route: ActivatedRoute,
@ -49,8 +57,8 @@ export class UserDetailComponent implements OnInit, OnDestroy {
private dialog: MatDialog,
) { }
public ngOnInit(): void {
this.subscription = this.route.params.subscribe(params => {
refreshUser(): void {
this.route.params.pipe(take(1)).subscribe(params => {
const { id } = params;
this.mgmtUserService.GetUserByID(id).then(user => {
this.user = user.toObject();
@ -60,8 +68,8 @@ export class UserDetailComponent implements OnInit, OnDestroy {
});
}
public ngOnDestroy(): void {
this.subscription.unsubscribe();
public ngOnInit(): void {
this.refreshUser();
}
public changeState(newState: UserState): void {
@ -149,6 +157,7 @@ export class UserDetailComponent implements OnInit, OnDestroy {
this.toast.showInfo('USER.TOAST.PHONEREMOVED', true);
if (this.user.human) {
this.user.human.phone = '';
this.refreshUser();
}
}).catch(error => {
this.toast.showError(error);
@ -158,9 +167,10 @@ export class UserDetailComponent implements OnInit, OnDestroy {
public saveEmail(email: string): void {
if (this.user.id && email) {
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) {
this.user.human.email = data.toObject().email;
this.refreshUser();
}
}).catch(error => {
this.toast.showError(error);
@ -175,6 +185,7 @@ export class UserDetailComponent implements OnInit, OnDestroy {
this.toast.showInfo('USER.TOAST.PHONESAVED', true);
if (this.user.human) {
this.user.human.phone = data.toObject().phone;
this.refreshUser();
}
}).catch(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);
}
public ResendMyEmailVerificationMail(): Promise<Empty> {
return this.grpcService.auth.resendMyEmailVerificationMail(
new Empty(),
);
}
public RemoveMyUserPhone(): Promise<Empty> {
return this.grpcService.auth.removeMyUserPhone(
new Empty(),

View File

@ -40,6 +40,7 @@ import {
IdpSearchResponse,
IdpUpdate,
IdpView,
InitialMailRequest,
LoginName,
LoginPolicy,
LoginPolicyRequest,
@ -786,6 +787,16 @@ export class ManagementService {
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> {
const req = new UserID();
req.setId(id);

View File

@ -77,7 +77,9 @@
"LOGIN":"Einloggen",
"EDIT":"Bearbeiten",
"PIN":"Anpinnen",
"CONFIGURE":"Konfigurieren"
"CONFIGURE":"Konfigurieren",
"SEND":"Senden",
"NEWVALUE":"Neuer Wert"
},
"ERRORS": {
"REQUIRED": "Bitte fülle alle benötigten Felder aus.",
@ -112,6 +114,11 @@
"DELETE_TITLE":"User löschen",
"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":{
"DEACTIVATE":"Deaktivieren",
"ACTIVATE":"Aktivieren",
@ -241,6 +248,10 @@
},
"EMAIL": "E-Mail",
"PHONE": "Telefonnummer",
"PHONEEMPTY":"Keine Telefonnummer hinterlegt",
"PHONEVERIFIED":"Telefonnummer bestätigt.",
"EMAILVERIFIED":"Email verifiziert",
"NOTVERIFIED":"nicht verifiziert",
"PREFERRED_LOGINNAME":"Bevorzugter Loginname",
"LOGINMETHODS": {
"TITLE": "Kontaktinformationen",
@ -248,12 +259,18 @@
"EMAIL": {
"TITLE": "E-Mail",
"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": {
"TITLE": "Telefon",
"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",
"ENTERCODE":"Verifizieren",
@ -294,10 +311,13 @@
"SIGNEDOUT_BTN":"Anmelden",
"EDITACCOUNT":"Konto bearbeiten",
"ADDACCOUNT":"Konto hinzufügen",
"RESENDINITIALEMAIL":"Neue Initialisierungsmail senden",
"RESENDEMAILNOTIFICATION":"Benachrichtigungsmail senden",
"TOAST": {
"CREATED":"Benutzer erfolgreich erstellt.",
"SAVED":"Profil gespeichert.",
"EMAILSAVED":"E-Mail gespeichert.",
"INITEMAILSENT":"Initialisierung Email gesendet.",
"PHONESAVED":"Telefonnummer gespeichert.",
"PHONEREMOVED":"Telefonnummer gelöscht.",
"PHONEVERIFIED":"Telefonnummer bestätigt.",

View File

@ -77,7 +77,9 @@
"LOGIN":"Login",
"EDIT":"Edit",
"PIN":"Pin / Unpin",
"CONFIGURE":"Configure"
"CONFIGURE":"Configure",
"SEND":"Send",
"NEWVALUE":"New Value"
},
"ERRORS": {
"REQUIRED": "Some required fields are missing.",
@ -112,6 +114,11 @@
"DELETE_TITLE":"Delete User",
"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":{
"DEACTIVATE":"Deactivate",
"ACTIVATE":"Activate",
@ -240,7 +247,11 @@
"NOTEQUAL":"The passwords provided do not match."
},
"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",
"LOGINMETHODS": {
"TITLE": "Contact Information",
@ -248,12 +259,18 @@
"EMAIL": {
"TITLE": "E-mail",
"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": {
"TITLE": "Phone",
"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",
"ENTERCODE":"Verify",
@ -294,10 +311,13 @@
"SIGNEDOUT_BTN":"Sign In",
"EDITACCOUNT":"Edit Account",
"ADDACCOUNT":"Log in With Another Account",
"RESENDINITIALEMAIL":"Send new initialisation mail",
"RESENDEMAILNOTIFICATION":"Resend Email notification",
"TOAST": {
"CREATED":"User created successfully.",
"SAVED":"Profile saved successfully.",
"EMAILSAVED":"E-mail saved successfully.",
"INITEMAILSENT":"Initializing mail sent.",
"PHONESAVED":"Phone saved successfully.",
"PHONEREMOVED":"Phone has been removed.",
"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/icon?family=Material+Icons|Material+Icons+Outlined" rel="stylesheet">
<link href="https://fonts.googleapis.com/css2?family=Lato&display=swap" 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">
<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
}
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) {
externalIDP, err := s.user.SearchExternalIDPs(ctx, externalIDPSearchRequestToModel(request))
if err != nil {

View File

@ -10,6 +10,7 @@ import (
usr_grant_event "github.com/caos/zitadel/internal/usergrant/repository/eventsourcing"
"github.com/caos/logging"
"github.com/caos/zitadel/internal/api/authz"
"github.com/caos/zitadel/internal/config/systemdefaults"
"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)
}
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) {
user, err := repo.UserByID(ctx, userID)
if err != nil {

View File

@ -54,4 +54,6 @@ type UserRepository interface {
ChangeAddress(ctx context.Context, address *model.Address) (*model.Address, 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
NotInactive: Benutzer ist nicht inaktiv
ShouldBeActiveOrInitial: Benutzer ist nicht aktiv oder initialisiert
AlreadyInitialised: Benutzer ist bereits initialisiert
NotInitialised: Benutzer ist noch nicht initialisiert
NotLocked: Benutzer ist nicht gesperrt
NoChanges: Keine Änderungen gefunden
InitCodeNotFound: Kein Initialisierungs Code gefunden

View File

@ -12,6 +12,8 @@ Errors:
AlreadyInactive: User already inactive
NotInactive: User is not inactive
ShouldBeActiveOrInitial: User is not active or inital
AlreadyInitialised: User is already initialised
NotInitialised: User is not yet initialised
NotLocked: User is not locked
NoChanges: No changes 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 {
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)
err = es.generatePasswordCode(passwordCode, notifyType)
@ -754,6 +757,35 @@ func (es *UserEventstore) RequestSetPassword(ctx context.Context, userID string,
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 {
if userID == "" {
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 {
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)
if err != nil {
@ -1018,6 +1053,9 @@ func (es *UserEventstore) CreateEmailVerificationCode(ctx context.Context, userI
if user.Human == nil {
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 {
return caos_errs.ThrowPreconditionFailed(nil, "EVENT-pdo9s", "Errors.User.EmailNotFound")
}

View File

@ -4,15 +4,15 @@ import (
"encoding/json"
"time"
"github.com/caos/zitadel/internal/id"
"github.com/golang/mock/gomock"
mock_cache "github.com/caos/zitadel/internal/cache/mock"
"github.com/caos/zitadel/internal/crypto"
"github.com/caos/zitadel/internal/eventstore/mock"
es_models "github.com/caos/zitadel/internal/eventstore/models"
"github.com/caos/zitadel/internal/id"
global_model "github.com/caos/zitadel/internal/model"
"github.com/caos/zitadel/internal/user/repository/eventsourcing/model"
"github.com/golang/mock/gomock"
)
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)
}
func GetMockManipulateUserWithEmailCodeGen(ctrl *gomock.Controller, user model.User) *UserEventstore {
func GetMockManipulateUserWithEmailCodeGen(ctrl *gomock.Controller, user model.User, verified bool) *UserEventstore {
data, _ := json.Marshal(user)
events := []*es_models.Event{
{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.EXPECT().FilterEvents(gomock.Any(), gomock.Any()).Return(events, nil)
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)
}
func GetMockManipulateUserWithPasswordCodeGen(ctrl *gomock.Controller, user model.User) *UserEventstore {
func GetMockManipulateUserWithPasswordCodeGen(ctrl *gomock.Controller, user model.User, verified bool) *UserEventstore {
data, _ := json.Marshal(user)
code, _ := json.Marshal(user.PasswordCode)
events := []*es_models.Event{
{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.EXPECT().FilterEvents(gomock.Any(), gomock.Any()).Return(events, nil)
@ -382,7 +392,7 @@ func GetMockManipulateUserVerifiedPhone(ctrl *gomock.Controller) *UserEventstore
return GetMockedEventstore(ctrl, mockEs)
}
func GetMockManipulateUserFull(ctrl *gomock.Controller) *UserEventstore {
func GetMockManipulateUserFull(ctrl *gomock.Controller, verified bool) *UserEventstore {
user := model.Human{
Profile: &model.Profile{
DisplayName: "DisplayName",
@ -407,6 +417,9 @@ func GetMockManipulateUserFull(ctrl *gomock.Controller) *UserEventstore {
events := []*es_models.Event{
{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.EXPECT().FilterEvents(gomock.Any(), gomock.Any()).Return(events, nil)
mockEs.EXPECT().AggregateCreator().Return(es_models.NewAggregateCreator("TEST"))

View File

@ -3,7 +3,6 @@ package eventsourcing
import (
"context"
"encoding/json"
iam_model "github.com/caos/zitadel/internal/iam/model"
"net"
"testing"
"time"
@ -15,6 +14,7 @@ import (
"github.com/caos/zitadel/internal/crypto"
caos_errs "github.com/caos/zitadel/internal/errors"
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"
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",
args: args{
es: GetMockManipulateUserFull(ctrl),
es: GetMockManipulateUserFull(ctrl, false),
ctx: authz.NewMockContext("orgID", "userID"),
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",
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"),
policy: &iam_model.PasswordComplexityPolicyView{},
password: &model.Password{ObjectRoot: es_models.ObjectRoot{AggregateID: "AggregateID"}, SecretString: "Password"},
@ -1516,8 +1516,10 @@ func TestCheckPassword(t *testing.T) {
Algorithm: "hash",
Crypted: []byte("password"),
}},
Email: &repo_model.Email{EmailAddress: "email"},
},
},
true,
),
ctx: authz.NewMockContext("orgID", "userID"),
userID: "userID",
@ -1582,8 +1584,10 @@ func TestSetPassword(t *testing.T) {
KeyID: "id",
Crypted: []byte("code"),
}},
Email: &repo_model.Email{EmailAddress: "email"},
},
},
true,
),
ctx: authz.NewMockContext("orgID", "userID"),
policy: &iam_model.PasswordComplexityPolicyView{},
@ -1627,8 +1631,11 @@ func TestSetPassword(t *testing.T) {
es: GetMockManipulateUserWithPasswordCodeGen(ctrl,
repo_model.User{
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"),
policy: &iam_model.PasswordComplexityPolicyView{},
@ -1653,8 +1660,10 @@ func TestSetPassword(t *testing.T) {
KeyID: "id",
Crypted: []byte("code2"),
}},
Email: &repo_model.Email{EmailAddress: "email"},
},
},
true,
),
ctx: authz.NewMockContext("orgID", "userID"),
policy: &iam_model.PasswordComplexityPolicyView{},
@ -1759,8 +1768,11 @@ func TestChangePassword(t *testing.T) {
es: GetMockManipulateUserWithPasswordCodeGen(ctrl,
repo_model.User{
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"),
policy: &iam_model.PasswordComplexityPolicyView{},
@ -1784,8 +1796,10 @@ func TestChangePassword(t *testing.T) {
Algorithm: "hash",
Crypted: []byte("older"),
}},
Email: &repo_model.Email{EmailAddress: "email"},
},
},
true,
),
ctx: authz.NewMockContext("orgID", "userID"),
policy: &iam_model.PasswordComplexityPolicyView{},
@ -1859,7 +1873,12 @@ func TestRequestSetPassword(t *testing.T) {
{
name: "create pw",
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"),
userID: "AggregateID",
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},
},
},
{
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",
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) {
ctrl := gomock.NewController(t)
type args struct {
@ -2170,7 +2279,7 @@ func TestProfileByID(t *testing.T) {
{
name: "get by id, ok",
args: args{
es: GetMockManipulateUserFull(ctrl),
es: GetMockManipulateUserFull(ctrl, false),
ctx: authz.NewMockContext("orgID", "userID"),
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",
args: args{
es: GetMockManipulateUserFull(ctrl),
es: GetMockManipulateUserFull(ctrl, false),
ctx: authz.NewMockContext("orgID", "userID"),
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",
args: args{
es: GetMockManipulateUserFull(ctrl),
es: GetMockManipulateUserFull(ctrl, false),
ctx: authz.NewMockContext("orgID", "userID"),
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",
args: args{
es: GetMockManipulateUserFull(ctrl),
es: GetMockManipulateUserFull(ctrl, true),
ctx: authz.NewMockContext("orgID", "userID"),
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"},
Email: &repo_model.Email{EmailAddress: "EmailAddress"},
},
}),
}, true),
ctx: authz.NewMockContext("orgID", "userID"),
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},
},
},
{
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",
args: args{
@ -2423,7 +2550,9 @@ func TestChangeEmail(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
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 == "" {
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 {
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{
Profile: &repo_model.Profile{DisplayName: "DisplayName"},
Email: &repo_model.Email{EmailAddress: "EmailAddress"},
}}),
}}, true),
ctx: authz.NewMockContext("orgID", "userID"),
userID: "userID",
},
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",
args: args{
@ -2606,7 +2750,7 @@ func TestCreateEmailVerificationCode(t *testing.T) {
err := tt.args.es.CreateEmailVerificationCode(tt.args.ctx, tt.args.userID)
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) {
t.Errorf("got wrong err: %v ", err)
@ -2695,7 +2839,7 @@ func TestPhoneByID(t *testing.T) {
{
name: "get by id, ok",
args: args{
es: GetMockManipulateUserFull(ctrl),
es: GetMockManipulateUserFull(ctrl, false),
ctx: authz.NewMockContext("orgID", "userID"),
user: &model.User{ObjectRoot: es_models.ObjectRoot{AggregateID: "AggregateID", Sequence: 1}},
},
@ -2762,7 +2906,7 @@ func TestChangePhone(t *testing.T) {
{
name: "change phone, verified",
args: args{
es: GetMockManipulateUserFull(ctrl),
es: GetMockManipulateUserFull(ctrl, false),
ctx: authz.NewMockContext("orgID", "userID"),
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",
args: args{
es: GetMockManipulateUserFull(ctrl),
es: GetMockManipulateUserFull(ctrl, false),
ctx: authz.NewMockContext("orgID", "userID"),
user: &model.User{ObjectRoot: es_models.ObjectRoot{AggregateID: "AggregateID", Sequence: 1}},
},
@ -3216,7 +3360,7 @@ func TestChangeAddress(t *testing.T) {
{
name: "change address ok",
args: args{
es: GetMockManipulateUserFull(ctrl),
es: GetMockManipulateUserFull(ctrl, false),
ctx: authz.NewMockContext("orgID", "userID"),
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() {
if h.State == int32(model.UserStateUnspecified) {
if h.State == int32(model.UserStateUnspecified) || h.State == int32(model.UserStateInitial) {
if h.Email != nil && h.IsEmailVerified {
h.State = int32(model.UserStateActive)
} else {

View File

@ -2,14 +2,15 @@ package eventsourcing
import (
"context"
"github.com/caos/zitadel/internal/api/authz"
iam_es_model "github.com/caos/zitadel/internal/iam/repository/eventsourcing/model"
"strings"
"github.com/caos/zitadel/internal/api/authz"
"github.com/caos/zitadel/internal/errors"
es_models "github.com/caos/zitadel/internal/eventstore/models"
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"
usr_model "github.com/caos/zitadel/internal/user/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) {
return func(ctx context.Context) (*es_models.Aggregate, error) {
agg, err := UserAggregate(ctx, aggCreator, user)

View File

@ -5,11 +5,11 @@ import (
"testing"
"time"
"github.com/caos/zitadel/internal/crypto"
"github.com/caos/zitadel/internal/api/authz"
"github.com/caos/zitadel/internal/crypto"
caos_errs "github.com/caos/zitadel/internal/errors"
"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"
)
@ -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) {
type args struct {
ctx context.Context

View File

@ -184,6 +184,11 @@ var ManagementService_AuthMethods = authz.MethodMapping{
CheckParam: "",
},
"/caos.zitadel.management.api.v1.ManagementService/ResendInitialMail": authz.Option{
Permission: "user.write",
CheckParam: "",
},
"/caos.zitadel.management.api.v1.ManagementService/SearchUserMemberships": authz.Option{
Permission: "user.membership.read",
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
} = 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
// in the proto definition for this message. If any rules are violated, an
// 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...)
}
// 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
func (m *MockManagementServiceClient) ResendPhoneVerificationCode(arg0 context.Context, arg1 *management.UserID, arg2 ...grpc.CallOption) (*emptypb.Empty, error) {
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) {
option (google.api.http) = {
post: "/users/{user_id}/memberships/_search"
@ -2049,6 +2060,11 @@ enum NotificationType {
NOTIFICATIONTYPE_SMS = 1;
}
message InitialMailRequest {
string id = 1 [(validate.rules).string.min_len = 1];
string email = 2;
}
enum PolicyState {
POLICYSTATE_UNSPECIFIED = 0;
POLICYSTATE_ACTIVE = 1;