From 4ccce7e1903466cc93161866b00845a8f5eec137 Mon Sep 17 00:00:00 2001 From: akastijn Date: Sat, 23 Aug 2025 22:32:44 +0200 Subject: [PATCH] Improve email verification flow by adding verified email pre-fill, validation handling, and dialog-based verification support. --- .../controllers/forms/AppealController.java | 26 +++++----------- .../mail/MailVerificationService.java | 24 ++++++++------- .../pages/forms/appeal/appeal.component.html | 10 +++++-- .../pages/forms/appeal/appeal.component.ts | 30 +++++++++++++++++-- .../app/pages/forms/sent/sent.component.ts | 20 ++++++++++--- 5 files changed, 73 insertions(+), 37 deletions(-) diff --git a/backend/src/main/java/com/alttd/altitudeweb/controllers/forms/AppealController.java b/backend/src/main/java/com/alttd/altitudeweb/controllers/forms/AppealController.java index a6a3e1f..644c0df 100644 --- a/backend/src/main/java/com/alttd/altitudeweb/controllers/forms/AppealController.java +++ b/backend/src/main/java/com/alttd/altitudeweb/controllers/forms/AppealController.java @@ -73,33 +73,23 @@ public class AppealController implements AppealsApi { log.debug("Retrieving mail by uuid and address"); EmailVerification verifiedMail = sqlSession.getMapper(EmailVerificationMapper.class) - .findByUserAndEmail(appeal.uuid(), appeal.email()); + .findByUserAndEmail(appeal.uuid(), appeal.email().toLowerCase()); emailVerificationCompletableFuture.complete(Optional.ofNullable(verifiedMail)); }); Optional optionalEmailVerification = emailVerificationCompletableFuture.join(); if (optionalEmailVerification.isEmpty()) { - return ResponseEntity.ok().body(new AppealResponseDto( - appeal.id().toString(), - "Your appeal has been saved and a verification mail has been send, please verify your email " + - "address by clicking the link in your email. Once it is verified we will review your appeal.", - false)); + return ResponseEntity.badRequest().build(); } EmailVerification emailVerification = optionalEmailVerification.get(); if (!emailVerification.verified()) { - return ResponseEntity.ok().body(new AppealResponseDto( - appeal.id().toString(), - "Your appeal has been saved and a verification mail has been resend, please verify your email " + - "address by clicking the link in your email. Once it is verified we will review your appeal.", - false - )); - } else { - AppealResponseDto appealResponseDto = new AppealResponseDto( - appeal.id().toString(), - "Your appeal has been submitted. You will be notified when it has been reviewed.", - true); - return ResponseEntity.ok().body(appealResponseDto); + return ResponseEntity.badRequest().build(); } + AppealResponseDto appealResponseDto = new AppealResponseDto( + appeal.id().toString(), + "Your appeal has been submitted. You will be notified when it has been reviewed.", + true); + return ResponseEntity.ok().body(appealResponseDto); } @Override diff --git a/backend/src/main/java/com/alttd/altitudeweb/services/mail/MailVerificationService.java b/backend/src/main/java/com/alttd/altitudeweb/services/mail/MailVerificationService.java index ed4b527..ca3107a 100644 --- a/backend/src/main/java/com/alttd/altitudeweb/services/mail/MailVerificationService.java +++ b/backend/src/main/java/com/alttd/altitudeweb/services/mail/MailVerificationService.java @@ -42,19 +42,20 @@ public class MailVerificationService { public EmailVerification submitEmail(UUID userUuid, String email) { String code = generateCode(); Instant now = Instant.now(); + final String finalEmail = email.toLowerCase(); CompletableFuture future = new CompletableFuture<>(); Connection.getConnection(Databases.DEFAULT) - .runQuery(sql -> { + .runQuery(sql -> { EmailVerificationMapper mapper = sql.getMapper(EmailVerificationMapper.class); - EmailVerification existing = mapper.findByUserAndEmail(userUuid, email); + EmailVerification existing = mapper.findByUserAndEmail(userUuid, finalEmail); EmailVerification toPersist; if (existing == null) { - toPersist = new EmailVerification(UUID.randomUUID(), userUuid, email, code, false, now, null, now); + toPersist = new EmailVerification(UUID.randomUUID(), userUuid, finalEmail, code, false, now, null, now); mapper.insert(toPersist); } else { mapper.updateCodeAndLastSent(existing.id(), code, now); - toPersist = new EmailVerification(existing.id(), userUuid, email, code, false, existing.createdAt(), null, now); + toPersist = new EmailVerification(existing.id(), userUuid, finalEmail, code, false, existing.createdAt(), null, now); } future.complete(toPersist); }); @@ -82,14 +83,16 @@ public class MailVerificationService { public EmailVerification resend(UUID userUuid, String email) { String code = generateCode(); Instant now = Instant.now(); + final String finalEmail = email.toLowerCase(); CompletableFuture future = new CompletableFuture<>(); Connection.getConnection(Databases.DEFAULT) - .runQuery(sql -> { + .runQuery(sql -> { EmailVerificationMapper mapper = sql.getMapper(EmailVerificationMapper.class); - EmailVerification existing = mapper.findByUserAndEmail(userUuid, email); + EmailVerification existing = mapper.findByUserAndEmail(userUuid, finalEmail); if (existing != null) { mapper.updateCodeAndLastSent(existing.id(), code, now); - future.complete(new EmailVerification(existing.id(), userUuid, email, code, false, existing.createdAt(), null, now)); + future.complete(new EmailVerification(existing.id(), userUuid, + finalEmail, code, false, existing.createdAt(), null, now)); } else { future.complete(null); } @@ -102,13 +105,14 @@ public class MailVerificationService { } public boolean delete(UUID userUuid, String email) { + final String finalEmail = email.toLowerCase(); CompletableFuture future = new CompletableFuture<>(); Connection.getConnection(Databases.DEFAULT) .runQuery(sql -> { EmailVerificationMapper mapper = sql.getMapper(EmailVerificationMapper.class); - EmailVerification existing = mapper.findByUserAndEmail(userUuid, email); + EmailVerification existing = mapper.findByUserAndEmail(userUuid, finalEmail); if (existing != null) { - mapper.deleteByUserAndEmail(userUuid, email); + mapper.deleteByUserAndEmail(userUuid, finalEmail); future.complete(true); } else { future.complete(false); @@ -122,7 +126,7 @@ public class MailVerificationService { MimeMessage message = mailSender.createMimeMessage(); MimeMessageHelper helper = new MimeMessageHelper(message, true); helper.setFrom(fromEmail); - helper.setTo(emailVerification.email()); + helper.setTo(emailVerification.email().toLowerCase()); helper.setSubject("Your verification code"); helper.setText("Your verification code is: " + emailVerification.verificationCode(), false); mailSender.send(message); diff --git a/frontend/src/app/pages/forms/appeal/appeal.component.html b/frontend/src/app/pages/forms/appeal/appeal.component.html index 5c344a9..e1a2181 100644 --- a/frontend/src/app/pages/forms/appeal/appeal.component.html +++ b/frontend/src/app/pages/forms/appeal/appeal.component.html @@ -75,10 +75,14 @@

Please enter your email.

-

It does not have to be your minecraft email.

+

It does not have to be your minecraft email. You will have to verify + it

Email - + @if (form.controls.email.invalid && form.controls.email.touched) { @if (form.controls.email.errors?.['required']) { @@ -90,7 +94,7 @@ }
-
diff --git a/frontend/src/app/pages/forms/appeal/appeal.component.ts b/frontend/src/app/pages/forms/appeal/appeal.component.ts index c30aeb6..9b7719c 100644 --- a/frontend/src/app/pages/forms/appeal/appeal.component.ts +++ b/frontend/src/app/pages/forms/appeal/appeal.component.ts @@ -21,6 +21,8 @@ import {MatFormFieldModule} from '@angular/material/form-field'; import {MatSelectModule} from '@angular/material/select'; import {MatInputModule} from '@angular/material/input'; import {HistoryFormatService} from '@pages/reference/bans/history-format.service'; +import {MatDialog} from '@angular/material/dialog'; +import {SentComponent} from '@pages/forms/sent/sent.component'; @Component({ selector: 'app-appeal', @@ -52,7 +54,11 @@ export class AppealComponent implements OnInit, OnDestroy, AfterViewInit { protected history = signal(null); protected selectedPunishment = signal(null); private emails = signal([]); - protected verifiedEmails = computed(() => this.emails().filter(email => email.verified)); + protected verifiedEmails = computed(() => this.emails() + .filter(email => email.verified) + .map(email => email.email.toLowerCase())); + protected validatedMail = signal(false); + protected dialog = inject(MatDialog); constructor( private elementRef: ElementRef, @@ -64,7 +70,12 @@ export class AppealComponent implements OnInit, OnDestroy, AfterViewInit { }); this.mailService.getUserEmails().subscribe(emails => { this.emails.set(emails); - }) + }); + this.form.valueChanges.subscribe(() => { + if (this.form.getRawValue().email.toLowerCase() in this.verifiedEmails()) { + this.validatedMail.set(true); + } + }); } ngOnInit() { @@ -202,6 +213,21 @@ export class AppealComponent implements OnInit, OnDestroy, AfterViewInit { onPunishmentSelected($event: PunishmentHistory) { this.selectedPunishment.set($event); } + + protected validateMailOrNextPage() { + if (this.validatedMail()) { + this.nextPage(); + return; + } + const dialogRef = this.dialog.open(SentComponent, { + data: {email: this.form.getRawValue().email}, + }); + dialogRef.afterClosed().subscribe(result => { + if (result === true) { + this.validatedMail.set(true); + } + }); + } } interface Appeal { diff --git a/frontend/src/app/pages/forms/sent/sent.component.ts b/frontend/src/app/pages/forms/sent/sent.component.ts index b120855..228b498 100644 --- a/frontend/src/app/pages/forms/sent/sent.component.ts +++ b/frontend/src/app/pages/forms/sent/sent.component.ts @@ -1,11 +1,17 @@ -import {Component, inject, Input, input, signal} from '@angular/core'; +import {Component, Inject, inject, input, signal} from '@angular/core'; import {FormControl, FormGroup, FormsModule, ReactiveFormsModule, Validators} from '@angular/forms'; import {MatInput, MatLabel} from '@angular/material/input'; import {MatFormFieldModule} from '@angular/material/form-field'; import {MailService, SubmitEmail, VerifyCode} from '@api'; import {AuthService} from '@services/auth.service'; import {MatButtonModule} from '@angular/material/button'; -import {MatDialogActions, MatDialogContent, MatDialogRef, MatDialogTitle} from '@angular/material/dialog'; +import { + MAT_DIALOG_DATA, + MatDialogActions, + MatDialogContent, + MatDialogRef, + MatDialogTitle +} from '@angular/material/dialog'; import {interval, Subscription} from 'rxjs'; @Component({ @@ -38,10 +44,11 @@ export class SentComponent { private mailService = inject(MailService); private authService = inject(AuthService); + protected readonly email: string; constructor( public dialogRef: MatDialogRef, - @Input() public email: string + @Inject(MAT_DIALOG_DATA) public data: InputMail ) { this.form = new FormGroup({ code: new FormControl('', { @@ -49,6 +56,8 @@ export class SentComponent { validators: [Validators.required, Validators.minLength(6), Validators.maxLength(6)] }) }); + this.email = data.email; + this.mailService.submitEmailForVerification({email: this.email.toLowerCase()}).subscribe(); } public onSubmit() { @@ -81,7 +90,6 @@ export class SentComponent { this.mailService.resendVerificationEmail(submitEmail).subscribe({ next: (response) => { - // Start cooldown timer this.startResendCooldown(); }, error: (error) => { @@ -147,3 +155,7 @@ interface VerifyMailData { verified: boolean; mail: string; } + +interface InputMail { + email: string; +}