mirror of
https://github.com/zitadel/zitadel.git
synced 2025-01-07 23:17:41 +00:00
feat(console): set email verified on change, user create (#2847)
* feat: set email verified on change * user create
This commit is contained in:
parent
a533872c66
commit
fb43b13232
@ -7,17 +7,17 @@
|
||||
<cnsl-form-field class="formfield">
|
||||
<cnsl-label>{{ 'USER.MACHINE.USERNAME' | translate }}*</cnsl-label>
|
||||
<input cnslInput formControlName="userName" required />
|
||||
<span cnsl-error *ngIf="userName?.invalid && userName?.errors?.required">
|
||||
<span cnslError *ngIf="userName?.invalid && userName?.errors?.required">
|
||||
{{ 'USER.VALIDATION.REQUIRED' | translate }}
|
||||
</span>
|
||||
<span cnsl-error *ngIf="userName?.invalid && userName?.errors?.noEmailValidator">
|
||||
<span cnslError *ngIf="userName?.invalid && userName?.errors?.noEmailValidator">
|
||||
{{ 'USER.VALIDATION.NOEMAIL' | translate }}
|
||||
</span>
|
||||
</cnsl-form-field>
|
||||
<cnsl-form-field class="formfield">
|
||||
<cnsl-label>{{ 'USER.MACHINE.NAME' | translate }}*</cnsl-label>
|
||||
<input cnslInput formControlName="name" required />
|
||||
<span cnsl-error *ngIf="name?.invalid && name?.errors?.required">
|
||||
<span cnslError *ngIf="name?.invalid && name?.errors?.required">
|
||||
{{ 'USER.VALIDATION.REQUIRED' | translate }}
|
||||
</span>
|
||||
</cnsl-form-field>
|
||||
|
@ -52,6 +52,15 @@
|
||||
</span>
|
||||
</cnsl-form-field>
|
||||
|
||||
<div class="email-is-verified">
|
||||
<mat-checkbox class="verified-checkbox" formControlName="isVerified">
|
||||
{{'USER.LOGINMETHODS.EMAIL.ISVERIFIED' | translate}}
|
||||
</mat-checkbox>
|
||||
<cnsl-info-section class="full-width desc">
|
||||
<span>{{'USER.LOGINMETHODS.EMAIL.ISVERIFIEDDESC' | translate}}</span>
|
||||
</cnsl-info-section>
|
||||
</div>
|
||||
|
||||
<p class="section">{{ 'USER.CREATE.GENDERLANGSECTION' | translate }}</p>
|
||||
|
||||
<cnsl-form-field class="formfield">
|
||||
|
@ -1,4 +1,3 @@
|
||||
|
||||
.form {
|
||||
width: 100%;
|
||||
padding-top: 1rem;
|
||||
@ -7,8 +6,8 @@
|
||||
button {
|
||||
margin-top: 3rem;
|
||||
display: block;
|
||||
padding: .5rem 4rem;
|
||||
border-radius: .5rem;
|
||||
padding: 0.5rem 4rem;
|
||||
border-radius: 0.5rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -18,19 +17,25 @@
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
flex-direction: row;
|
||||
margin: 0 -.5rem;
|
||||
margin: 0 -0.5rem;
|
||||
|
||||
.section {
|
||||
padding: .5rem;
|
||||
padding: 0.5rem;
|
||||
flex-basis: 100%;
|
||||
color: var(--grey);
|
||||
font-size: .9rem;
|
||||
letter-spacing: .05em;
|
||||
font-size: 0.9rem;
|
||||
letter-spacing: 0.05em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.formfield {
|
||||
flex: 1 0 33%;
|
||||
margin: 0 .5rem;
|
||||
margin: 0 0.5rem;
|
||||
}
|
||||
|
||||
.email-is-verified {
|
||||
margin: 0 0.5rem;
|
||||
flex-basis: 100%;
|
||||
margin-top: 1.5rem;
|
||||
}
|
||||
}
|
||||
|
@ -38,7 +38,9 @@ export class UserCreateComponent implements OnDestroy {
|
||||
) {
|
||||
this.loading = true;
|
||||
this.loadOrg();
|
||||
this.mgmtService.getOrgIAMPolicy().then((resp) => {
|
||||
this.mgmtService
|
||||
.getOrgIAMPolicy()
|
||||
.then((resp) => {
|
||||
if (resp.policy?.userLoginMustBeDomain) {
|
||||
this.userLoginMustBeDomain = resp.policy.userLoginMustBeDomain;
|
||||
}
|
||||
@ -46,7 +48,8 @@ export class UserCreateComponent implements OnDestroy {
|
||||
this.loading = false;
|
||||
this.envSuffixLabel = this.envSuffix();
|
||||
this.changeDetRef.detectChanges();
|
||||
}).catch(error => {
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error(error);
|
||||
this.initForm();
|
||||
this.loading = false;
|
||||
@ -56,8 +59,8 @@ export class UserCreateComponent implements OnDestroy {
|
||||
}
|
||||
|
||||
private async loadOrg(): Promise<void> {
|
||||
const domains = (await this.mgmtService.listOrgDomains());
|
||||
const found = domains.resultList.find(resp => resp.isPrimary);
|
||||
const domains = await this.mgmtService.listOrgDomains();
|
||||
const found = domains.resultList.find((resp) => resp.isPrimary);
|
||||
if (found) {
|
||||
this.primaryDomain = found;
|
||||
}
|
||||
@ -66,23 +69,17 @@ export class UserCreateComponent implements OnDestroy {
|
||||
private initForm(): void {
|
||||
this.userForm = this.fb.group({
|
||||
email: ['', [Validators.required, Validators.email]],
|
||||
userName: ['',
|
||||
[
|
||||
Validators.required,
|
||||
Validators.minLength(2),
|
||||
],
|
||||
],
|
||||
userName: ['', [Validators.required, Validators.minLength(2)]],
|
||||
firstName: ['', Validators.required],
|
||||
lastName: ['', Validators.required],
|
||||
nickName: [''],
|
||||
gender: [],
|
||||
preferredLanguage: [''],
|
||||
phone: [''],
|
||||
isVerified: [false, []],
|
||||
});
|
||||
|
||||
this.userForm.controls['phone'].valueChanges.pipe(
|
||||
takeUntil(this.destroyed$),
|
||||
debounceTime(300)).subscribe(value => {
|
||||
this.userForm.controls['phone'].valueChanges.pipe(takeUntil(this.destroyed$), debounceTime(300)).subscribe((value) => {
|
||||
const phoneNumber = parsePhoneNumber(value ?? '', 'CH');
|
||||
if (phoneNumber) {
|
||||
const formmatted = phoneNumber.formatInternational();
|
||||
@ -110,7 +107,10 @@ export class UserCreateComponent implements OnDestroy {
|
||||
humanReq.setUserName(this.userName?.value);
|
||||
humanReq.setProfile(profileReq);
|
||||
|
||||
humanReq.setEmail(new AddHumanUserRequest.Email().setEmail(this.email?.value));
|
||||
const emailreq = new AddHumanUserRequest.Email();
|
||||
emailreq.setEmail(this.email?.value);
|
||||
emailreq.setIsEmailVerified(this.isVerified?.value);
|
||||
humanReq.setEmail(emailreq);
|
||||
|
||||
if (this.phone && this.phone.value) {
|
||||
humanReq.setPhone(new AddHumanUserRequest.Phone().setPhone(this.phone.value));
|
||||
@ -123,7 +123,7 @@ export class UserCreateComponent implements OnDestroy {
|
||||
this.toast.showInfo('USER.TOAST.CREATED', true);
|
||||
this.router.navigate(['users', data.userId]);
|
||||
})
|
||||
.catch(error => {
|
||||
.catch((error) => {
|
||||
this.loading = false;
|
||||
this.toast.showError(error);
|
||||
});
|
||||
@ -137,6 +137,9 @@ export class UserCreateComponent implements OnDestroy {
|
||||
public get email(): AbstractControl | null {
|
||||
return this.userForm.get('email');
|
||||
}
|
||||
public get isVerified(): AbstractControl | null {
|
||||
return this.userForm.get('isVerified');
|
||||
}
|
||||
public get userName(): AbstractControl | null {
|
||||
return this.userForm.get('userName');
|
||||
}
|
||||
|
@ -11,6 +11,7 @@ import { MatSelectModule } from '@angular/material/select';
|
||||
import { MatTooltipModule } from '@angular/material/tooltip';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
import { DetailLayoutModule } from 'src/app/modules/detail-layout/detail-layout.module';
|
||||
import { InfoSectionModule } from 'src/app/modules/info-section/info-section.module';
|
||||
import { InputModule } from 'src/app/modules/input/input.module';
|
||||
|
||||
import { UserCreateRoutingModule } from './user-create-routing.module';
|
||||
@ -31,6 +32,7 @@ import { UserCreateComponent } from './user-create.component';
|
||||
MatCheckboxModule,
|
||||
MatTooltipModule,
|
||||
TranslateModule,
|
||||
InfoSectionModule,
|
||||
DetailLayoutModule,
|
||||
InputModule,
|
||||
MatRippleModule,
|
||||
|
@ -27,11 +27,11 @@
|
||||
.btn-container {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
margin: 0 -.5rem;
|
||||
margin: 0 -0.5rem;
|
||||
|
||||
button {
|
||||
border-radius: .5rem;
|
||||
margin: 0 .5rem;
|
||||
border-radius: 0.5rem;
|
||||
margin: 0 0.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
@ -39,11 +39,11 @@
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: stretch;
|
||||
margin: -1.5rem -.5rem;
|
||||
margin: -1.5rem -0.5rem;
|
||||
|
||||
.app-card {
|
||||
flex: 1;
|
||||
margin: .5rem;
|
||||
margin: 0.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
@ -55,6 +55,7 @@
|
||||
|
||||
.resendemail {
|
||||
margin-right: 1rem;
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
|
||||
.side-padding {
|
||||
|
@ -227,9 +227,9 @@ export class AuthUserDetailComponent implements OnDestroy {
|
||||
width: '400px',
|
||||
});
|
||||
|
||||
dialogRefPhone.afterClosed().subscribe((resp) => {
|
||||
if (resp) {
|
||||
this.savePhone(resp);
|
||||
dialogRefPhone.afterClosed().subscribe((resp: { value: string; isVerified: boolean }) => {
|
||||
if (resp && resp.value) {
|
||||
this.savePhone(resp.value);
|
||||
}
|
||||
});
|
||||
break;
|
||||
@ -247,9 +247,9 @@ export class AuthUserDetailComponent implements OnDestroy {
|
||||
width: '400px',
|
||||
});
|
||||
|
||||
dialogRefEmail.afterClosed().subscribe((resp) => {
|
||||
if (resp) {
|
||||
this.saveEmail(resp);
|
||||
dialogRefEmail.afterClosed().subscribe((resp: { value: string; isVerified: boolean }) => {
|
||||
if (resp && resp.value) {
|
||||
this.saveEmail(resp.value);
|
||||
}
|
||||
});
|
||||
break;
|
||||
|
@ -9,6 +9,16 @@
|
||||
<input [formControl]="valueControl" cnslInput
|
||||
(keydown.enter)="valueControl.valid ? closeDialogWithValue() : null" />
|
||||
</cnsl-form-field>
|
||||
|
||||
<ng-container *ngIf="data.type === EditDialogType.EMAIL && data.isVerifiedTextKey">
|
||||
<mat-checkbox class="verified-checkbox" [(ngModel)]="isVerified">
|
||||
{{data.isVerifiedTextKey |
|
||||
translate}}
|
||||
</mat-checkbox>
|
||||
<cnsl-info-section class="full-width desc">
|
||||
<span>{{data.isVerifiedTextDescKey | translate}}</span>
|
||||
</cnsl-info-section>
|
||||
</ng-container>
|
||||
</div>
|
||||
<div mat-dialog-actions class="action">
|
||||
<button cdkFocusInitial color="primary" mat-button class="ok-button" (click)="closeDialog()">
|
||||
|
@ -2,6 +2,10 @@
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.verified-checkbox {
|
||||
margin-top: 1.5rem;
|
||||
}
|
||||
|
||||
.desc {
|
||||
font-size: 14px;
|
||||
color: var(--grey);
|
||||
@ -12,10 +16,10 @@
|
||||
justify-content: flex-end;
|
||||
|
||||
.ok-button {
|
||||
margin-left: .5rem;
|
||||
margin-left: 0.5rem;
|
||||
}
|
||||
|
||||
button {
|
||||
border-radius: .5rem;
|
||||
border-radius: 0.5rem;
|
||||
}
|
||||
}
|
||||
|
@ -15,16 +15,17 @@ export enum EditDialogType {
|
||||
})
|
||||
export class EditDialogComponent {
|
||||
public isPhone: boolean = false;
|
||||
public isVerified: boolean = false;
|
||||
public phoneCountry: string = 'CH';
|
||||
public valueControl: FormControl = new FormControl(['', [Validators.required]]);
|
||||
constructor(public dialogRef: MatDialogRef<EditDialogComponent>,
|
||||
@Inject(MAT_DIALOG_DATA) public data: any) {
|
||||
public EditDialogType: any = EditDialogType;
|
||||
constructor(public dialogRef: MatDialogRef<EditDialogComponent>, @Inject(MAT_DIALOG_DATA) public data: any) {
|
||||
this.valueControl.setValue(data.value);
|
||||
if (data.type === EditDialogType.PHONE) {
|
||||
this.isPhone = true;
|
||||
}
|
||||
|
||||
this.valueControl.valueChanges.subscribe(value => {
|
||||
this.valueControl.valueChanges.subscribe((value) => {
|
||||
if (value && value.length > 1) {
|
||||
this.changeValue(value);
|
||||
}
|
||||
@ -53,6 +54,6 @@ export class EditDialogComponent {
|
||||
}
|
||||
|
||||
closeDialogWithValue(): void {
|
||||
this.dialogRef.close(this.valueControl.value);
|
||||
this.dialogRef.close({ value: this.valueControl.value, isVerified: this.isVerified });
|
||||
}
|
||||
}
|
||||
|
@ -19,7 +19,7 @@
|
||||
}
|
||||
|
||||
p {
|
||||
margin: .5rem 0;
|
||||
margin: 0.5rem 0;
|
||||
font-size: 14px;
|
||||
color: var(--grey);
|
||||
}
|
||||
@ -30,13 +30,13 @@
|
||||
}
|
||||
|
||||
.actions-trigger {
|
||||
margin-top: .25rem;
|
||||
margin-top: 0.25rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.icon {
|
||||
margin-left: .5rem;
|
||||
margin-right: -.5rem;
|
||||
margin-left: 0.5rem;
|
||||
margin-right: -0.5rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -66,3 +66,7 @@
|
||||
.side-padding {
|
||||
padding-top: 1rem;
|
||||
}
|
||||
|
||||
.resendemail {
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
|
@ -229,10 +229,10 @@ export class UserDetailComponent implements OnInit {
|
||||
});
|
||||
}
|
||||
|
||||
public saveEmail(email: string): void {
|
||||
public saveEmail(email: string, isVerified: boolean): void {
|
||||
if (this.user.id && email) {
|
||||
this.mgmtUserService
|
||||
.updateHumanEmail(this.user.id, email)
|
||||
.updateHumanEmail(this.user.id, email, isVerified)
|
||||
.then(() => {
|
||||
this.toast.showInfo('USER.TOAST.EMAILSAVED', true);
|
||||
if (this.user.state === UserState.USER_STATE_INITIAL) {
|
||||
@ -355,9 +355,9 @@ export class UserDetailComponent implements OnInit {
|
||||
width: '400px',
|
||||
});
|
||||
|
||||
dialogRefPhone.afterClosed().subscribe((resp) => {
|
||||
if (resp) {
|
||||
this.savePhone(resp);
|
||||
dialogRefPhone.afterClosed().subscribe((resp: { value: string; isVerified: boolean }) => {
|
||||
if (resp && resp.value) {
|
||||
this.savePhone(resp.value);
|
||||
}
|
||||
});
|
||||
break;
|
||||
@ -369,15 +369,17 @@ export class UserDetailComponent implements OnInit {
|
||||
labelKey: 'ACTIONS.NEWVALUE',
|
||||
titleKey: 'USER.LOGINMETHODS.EMAIL.EDITTITLE',
|
||||
descriptionKey: 'USER.LOGINMETHODS.EMAIL.EDITDESC',
|
||||
isVerifiedTextKey: 'USER.LOGINMETHODS.EMAIL.ISVERIFIED',
|
||||
isVerifiedTextDescKey: 'USER.LOGINMETHODS.EMAIL.ISVERIFIEDDESC',
|
||||
value: this.user.human?.email?.email,
|
||||
type: EditDialogType.EMAIL,
|
||||
},
|
||||
width: '400px',
|
||||
});
|
||||
|
||||
dialogRefEmail.afterClosed().subscribe((resp) => {
|
||||
if (resp) {
|
||||
this.saveEmail(resp);
|
||||
dialogRefEmail.afterClosed().subscribe((resp: { value: string; isVerified: boolean }) => {
|
||||
if (resp && resp.value) {
|
||||
this.saveEmail(resp.value, resp.isVerified);
|
||||
}
|
||||
});
|
||||
break;
|
||||
|
@ -899,32 +899,24 @@ export class ManagementService {
|
||||
return this.grpcService.mgmt.listHumanLinkedIDPs(req, null).then((resp) => resp.toObject());
|
||||
}
|
||||
|
||||
public getAction(
|
||||
id: string,
|
||||
): Promise<GetActionResponse.AsObject> {
|
||||
public getAction(id: string): Promise<GetActionResponse.AsObject> {
|
||||
const req = new GetActionRequest();
|
||||
req.setId(id);
|
||||
return this.grpcService.mgmt.getAction(req, null).then(resp => resp.toObject());
|
||||
return this.grpcService.mgmt.getAction(req, null).then((resp) => resp.toObject());
|
||||
}
|
||||
|
||||
public createAction(
|
||||
req: CreateActionRequest,
|
||||
): Promise<CreateActionResponse.AsObject> {
|
||||
return this.grpcService.mgmt.createAction(req, null).then(resp => resp.toObject());
|
||||
public createAction(req: CreateActionRequest): Promise<CreateActionResponse.AsObject> {
|
||||
return this.grpcService.mgmt.createAction(req, null).then((resp) => resp.toObject());
|
||||
}
|
||||
|
||||
public updateAction(
|
||||
req: UpdateActionRequest,
|
||||
): Promise<UpdateActionResponse.AsObject> {
|
||||
return this.grpcService.mgmt.updateAction(req, null).then(resp => resp.toObject());
|
||||
public updateAction(req: UpdateActionRequest): Promise<UpdateActionResponse.AsObject> {
|
||||
return this.grpcService.mgmt.updateAction(req, null).then((resp) => resp.toObject());
|
||||
}
|
||||
|
||||
public deleteAction(
|
||||
id: string,
|
||||
): Promise<DeleteActionResponse.AsObject> {
|
||||
public deleteAction(id: string): Promise<DeleteActionResponse.AsObject> {
|
||||
const req = new DeleteActionRequest();
|
||||
req.setId(id);
|
||||
return this.grpcService.mgmt.deleteAction(req, null).then(resp => resp.toObject());
|
||||
return this.grpcService.mgmt.deleteAction(req, null).then((resp) => resp.toObject());
|
||||
}
|
||||
|
||||
public listActions(
|
||||
@ -949,23 +941,19 @@ export class ManagementService {
|
||||
metadata.setAsc(asc);
|
||||
}
|
||||
req.setQuery(metadata);
|
||||
return this.grpcService.mgmt.listActions(req, null).then(resp => resp.toObject());
|
||||
return this.grpcService.mgmt.listActions(req, null).then((resp) => resp.toObject());
|
||||
}
|
||||
|
||||
public getFlow(
|
||||
type: FlowType
|
||||
): Promise<GetFlowResponse.AsObject> {
|
||||
public getFlow(type: FlowType): Promise<GetFlowResponse.AsObject> {
|
||||
const req = new GetFlowRequest();
|
||||
req.setType(type);
|
||||
return this.grpcService.mgmt.getFlow(req, null).then(resp => resp.toObject());
|
||||
return this.grpcService.mgmt.getFlow(req, null).then((resp) => resp.toObject());
|
||||
}
|
||||
|
||||
public clearFlow(
|
||||
type: FlowType
|
||||
): Promise<ClearFlowResponse.AsObject> {
|
||||
public clearFlow(type: FlowType): Promise<ClearFlowResponse.AsObject> {
|
||||
const req = new ClearFlowRequest();
|
||||
req.setType(type);
|
||||
return this.grpcService.mgmt.clearFlow(req, null).then(resp => resp.toObject());
|
||||
return this.grpcService.mgmt.clearFlow(req, null).then((resp) => resp.toObject());
|
||||
}
|
||||
|
||||
public setTriggerActions(
|
||||
@ -977,7 +965,7 @@ export class ManagementService {
|
||||
req.setActionIdsList(actionIdsList);
|
||||
req.setFlowType(type);
|
||||
req.setTriggerType(triggerType);
|
||||
return this.grpcService.mgmt.setTriggerActions(req, null).then(resp => resp.toObject());
|
||||
return this.grpcService.mgmt.setTriggerActions(req, null).then((resp) => resp.toObject());
|
||||
}
|
||||
|
||||
public getIAM(): Promise<GetIAMResponse.AsObject> {
|
||||
@ -1468,10 +1456,13 @@ export class ManagementService {
|
||||
return this.grpcService.mgmt.getHumanEmail(req, null).then((resp) => resp.toObject());
|
||||
}
|
||||
|
||||
public updateHumanEmail(userId: string, email: string): Promise<UpdateHumanEmailResponse.AsObject> {
|
||||
public updateHumanEmail(userId: string, email: string, isVerified?: boolean): Promise<UpdateHumanEmailResponse.AsObject> {
|
||||
const req = new UpdateHumanEmailRequest();
|
||||
req.setUserId(userId);
|
||||
req.setEmail(email);
|
||||
if (isVerified) {
|
||||
req.setIsEmailVerified(isVerified);
|
||||
}
|
||||
return this.grpcService.mgmt.updateHumanEmail(req, null).then((resp) => resp.toObject());
|
||||
}
|
||||
|
||||
|
@ -419,6 +419,8 @@
|
||||
"EMAIL": {
|
||||
"TITLE": "E-Mail",
|
||||
"VALID": "Validiert",
|
||||
"ISVERIFIED": "Email Verifiziert",
|
||||
"ISVERIFIEDDESC": "Wenn die Email als verifiziert angegeben wird, wird keine Initialisierungsmail versendet.",
|
||||
"RESEND": "Verifikationsmail erneut senden",
|
||||
"EDITTITLE": "Email ändern",
|
||||
"EDITDESC": "Geben Sie die neue Email in dem darunterliegenden Feld ein!"
|
||||
|
@ -419,6 +419,8 @@
|
||||
"EMAIL": {
|
||||
"TITLE": "E-mail",
|
||||
"VALID": "validated",
|
||||
"ISVERIFIED": "Email Verified",
|
||||
"ISVERIFIEDDESC": "If the email is indicated as verified, no initialization email will be sent.",
|
||||
"RESEND": "Resend Verification E-mail",
|
||||
"EDITTITLE": "Change Email",
|
||||
"EDITDESC": "Enter the new email in the field below."
|
||||
|
@ -419,6 +419,8 @@
|
||||
"EMAIL": {
|
||||
"TITLE": "E-mail",
|
||||
"VALID": "convalidato",
|
||||
"ISVERIFIED": "Email Verificato",
|
||||
"ISVERIFIEDDESC": "Se l'email viene indicata come verificata, non verrà inviata alcuna email di inizializzazione.",
|
||||
"RESEND": "Invia di nuovo l'e-mail di verifica",
|
||||
"EDITTITLE": "Cambiare l'e-mail",
|
||||
"EDITDESC": "Inserisci la nuova email nel campo sottostante."
|
||||
|
Loading…
x
Reference in New Issue
Block a user