Enhance staff application flow with email verification checks, refined error handling, and improved user feedback in frontend and backend.

This commit is contained in:
akastijn 2025-09-27 20:00:44 +02:00
parent cdbf862ecf
commit 311d77fcb2
5 changed files with 69 additions and 60 deletions

View File

@ -38,45 +38,37 @@ public class ApplicationController implements ApplicationsApi {
public ResponseEntity<FormResponseDto> submitStaffApplication(StaffApplicationDto staffApplicationDto) {
UUID userUuid = AuthenticatedUuid.getAuthenticatedUserUuid();
String email = staffApplicationDto.getEmail() == null ? null : staffApplicationDto.getEmail().toLowerCase();
Optional<EmailVerification> optionalEmail = fetchEmailVerification(userUuid, email);
if (optionalEmail.isEmpty() || !optionalEmail.get().verified()) {
log.warn("User {} attempted to submit an application without a verified email {}", userUuid, email);
return ResponseEntity.badRequest().build();
}
// Map and persist application
StaffApplication application = staffApplicationDataMapper.map(userUuid, staffApplicationDto);
saveApplication(application);
Optional<EmailVerification> optionalEmail = fetchEmailVerification(userUuid, application.email());
boolean verified = optionalEmail.map(EmailVerification::verified).orElse(false);
boolean success = true;
if (verified) {
// Send mail first; only if sent, send to Discord, then mark as sent
boolean mailSent = false;
try {
mailSent = staffApplicationMail.sendApplicationEmail(application);
} catch (Exception e) {
log.error("Error while sending staff application email for {}", application.id(), e);
success = false;
try {
if (!staffApplicationMail.sendApplicationEmail(application)) {
log.warn("Failed to send staff application email for {}", application.id());
return ResponseEntity.internalServerError().build();
}
if (mailSent) {
try {
staffApplicationDiscord.sendApplicationToDiscord(application);
} catch (Exception e) {
log.error("Failed to send staff application {} to Discord", application.id(), e);
success = false;
}
} else {
success = false;
}
if (success) {
markAsSent(application.id());
}
}
if (verified && !success) {
} catch (Exception e) {
log.error("Error while sending staff application email for {}", application.id(), e);
return ResponseEntity.internalServerError().build();
}
FormResponseDto response = buildResponse(application, verified);
return ResponseEntity.status(201).body(response);
try {
staffApplicationDiscord.sendApplicationToDiscord(application);
} catch (Exception e) {
log.error("Failed to send staff application {} to Discord", application.id(), e);
return ResponseEntity.internalServerError().build();
}
markAsSent(application.id());
FormResponseDto response = buildResponse(application);
return ResponseEntity.status(200).body(response);
}
private void saveApplication(StaffApplication application) {
@ -110,10 +102,8 @@ public class ApplicationController implements ApplicationsApi {
.runQuery(sqlSession -> sqlSession.getMapper(StaffApplicationMapper.class).markAsSent(applicationId));
}
private FormResponseDto buildResponse(StaffApplication application, boolean verified) {
String message = verified
? "Your staff application has been submitted. You will be notified when it has been reviewed."
: "Application created. Please verify your email to complete submission.";
private FormResponseDto buildResponse(StaffApplication application) {
String message = "Your staff application has been submitted. You will be notified when it has been reviewed.";
return new FormResponseDto(
application.id().toString(),
message,

View File

@ -67,7 +67,14 @@
<div class="valid-email">
<ng-container matSuffix>
<mat-icon>check</mat-icon>
<span>You have validated your email previously, and can continue to the next page!</span>
<span>You have validated your email previously.</span>
</ng-container>
</div>
} @else {
<div class="invalid-email">
<ng-container matSuffix>
<mat-icon>close</mat-icon>
<span>You have not used this email address before. Before going to the next page you will be asked to verify it.</span>
</ng-container>
</div>
}
@ -107,7 +114,8 @@
<!-- PC Requirements -->
<div class="checkbox-field">
<mat-checkbox formControlName="meetsRequirements">
I confirm that I meet the PC requirements (able to record video at 30fps 720p or higher, and able to talk in voice chat)
I confirm that I meet the PC requirements (able to record video at 30fps 720p or higher, and able
to talk in voice chat)
</mat-checkbox>
@if (form.controls.meetsRequirements.invalid && form.controls.meetsRequirements.touched) {
<mat-error class="checkbox-error">
@ -172,7 +180,8 @@
<label class="field-label">Available days for moderating:</label>
<div class="days-container">
@for (day of availableDays; track day) {
<div class="day-chip" [class.selected]="form.controls.availableDays.value.includes(day)" (click)="toggleDay(day)">
<div class="day-chip" [class.selected]="form.controls.availableDays.value.includes(day)"
(click)="toggleDay(day)">
{{ day }}
</div>
}
@ -188,9 +197,9 @@
<mat-form-field appearance="fill" style="width: 100%;">
<mat-label>Available times (Your timezone: {{ userTimezone }})</mat-label>
<textarea matInput
formControlName="availableTimes"
placeholder="e.g., 6PM-10PM weekdays, 2PM-8PM weekends"
rows="2"></textarea>
formControlName="availableTimes"
placeholder="e.g., 6PM-10PM weekdays, 2PM-8PM weekends"
rows="2"></textarea>
@if (form.controls.availableTimes.invalid && form.controls.availableTimes.touched) {
<mat-error>
Available times are required
@ -215,9 +224,9 @@
<mat-form-field appearance="fill" style="width: 100%;">
<mat-label>Previous experience (here or in other relevant places)</mat-label>
<textarea matInput
formControlName="previousExperience"
placeholder="Describe your previous experience"
rows="4"></textarea>
formControlName="previousExperience"
placeholder="Describe your previous experience"
rows="4"></textarea>
@if (form.controls.previousExperience.invalid && form.controls.previousExperience.touched) {
<mat-error>
@if (form.controls.previousExperience.errors?.['required']) {
@ -233,9 +242,9 @@
<mat-form-field appearance="fill" style="width: 100%;">
<mat-label>Experience with plugins that players use on our server</mat-label>
<textarea matInput
formControlName="pluginExperience"
placeholder="Describe your experience with our server plugins"
rows="4"></textarea>
formControlName="pluginExperience"
placeholder="Describe your experience with our server plugins"
rows="4"></textarea>
@if (form.controls.pluginExperience.invalid && form.controls.pluginExperience.touched) {
<mat-error>
@if (form.controls.pluginExperience.errors?.['required']) {
@ -251,9 +260,9 @@
<mat-form-field appearance="fill" style="width: 100%;">
<mat-label>What do you believe the expectations of a moderator are?</mat-label>
<textarea matInput
formControlName="moderatorExpectations"
placeholder="Describe what you think a moderator should do"
rows="4"></textarea>
formControlName="moderatorExpectations"
placeholder="Describe what you think a moderator should do"
rows="4"></textarea>
@if (form.controls.moderatorExpectations.invalid && form.controls.moderatorExpectations.touched) {
<mat-error>
@if (form.controls.moderatorExpectations.errors?.['required']) {
@ -269,9 +278,9 @@
<mat-form-field appearance="fill" style="width: 100%;">
<mat-label>Additional Information (Optional)</mat-label>
<textarea matInput
formControlName="additionalInfo"
placeholder="Any additional information you'd like to share"
rows="4"></textarea>
formControlName="additionalInfo"
placeholder="Any additional information you'd like to share"
rows="4"></textarea>
</mat-form-field>
</div>
<button mat-raised-button (click)="onSubmit()"

View File

@ -99,6 +99,16 @@ main {
background-color: rgba(76, 175, 80, 0.1);
}
.invalid-email {
display: flex;
align-items: center;
color: #af4c4c;
margin: 10px 0;
padding: 8px 12px;
border-radius: 4px;
background-color: rgba(76, 175, 80, 0.1);
}
.valid-email mat-icon {
color: #4CAF50;
margin-right: 10px;

View File

@ -107,7 +107,7 @@ export class StaffApplicationComponent implements OnInit, OnDestroy, AfterViewIn
}),
availableTimes: new FormControl('', {
nonNullable: true,
validators: [Validators.required, Validators.maxLength(1000)]
validators: [Validators.required, Validators.maxLength(900)]
}),
previousExperience: new FormControl('', {
nonNullable: true,
@ -234,10 +234,11 @@ export class StaffApplicationComponent implements OnInit, OnDestroy, AfterViewIn
const staffApplication: StaffApplication = this.mapToStaffApplication(this.form.getRawValue());
this.staffApplicationService.submitStaffApplication(staffApplication).subscribe(result => {
//TODO route to mail page
// Navigate to the sent page
if (!result.verified_mail) {
throw new Error('Submitted a form with an e-mail that was not verified.');
}
this.router.navigate(['/forms/sent'], {
state: {message: 'Your staff application has been submitted successfully. We will review your application and get back to you soon.'}
state: {message: result.message}
}).then();
})
}
@ -321,7 +322,7 @@ export class StaffApplicationComponent implements OnInit, OnDestroy, AfterViewIn
joinDate: joinDateString,
weeklyPlaytime: Number(formData.weeklyPlaytime),
availableDays: formData.availableDays,
availableTimes: formData.availableTimes,
availableTimes: `Timezone: ${this.userTimezone}\nAvailable Times:${formData.availableTimes}`,
previousExperience: formData.previousExperience,
pluginExperience: formData.pluginExperience,
moderatorExpectations: formData.moderatorExpectations,

View File

@ -66,7 +66,6 @@ components:
description: Preferred pronouns of the applicant
joinDate:
type: string
maxLength: 256
format: date
description: Date when the applicant joined the service
weeklyPlaytime: