Enhance staff application flow with email verification checks, refined error handling, and improved user feedback in frontend and backend.
This commit is contained in:
parent
cdbf862ecf
commit
311d77fcb2
|
|
@ -38,45 +38,37 @@ public class ApplicationController implements ApplicationsApi {
|
||||||
public ResponseEntity<FormResponseDto> submitStaffApplication(StaffApplicationDto staffApplicationDto) {
|
public ResponseEntity<FormResponseDto> submitStaffApplication(StaffApplicationDto staffApplicationDto) {
|
||||||
UUID userUuid = AuthenticatedUuid.getAuthenticatedUserUuid();
|
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);
|
StaffApplication application = staffApplicationDataMapper.map(userUuid, staffApplicationDto);
|
||||||
saveApplication(application);
|
saveApplication(application);
|
||||||
|
|
||||||
Optional<EmailVerification> optionalEmail = fetchEmailVerification(userUuid, application.email());
|
try {
|
||||||
boolean verified = optionalEmail.map(EmailVerification::verified).orElse(false);
|
if (!staffApplicationMail.sendApplicationEmail(application)) {
|
||||||
|
log.warn("Failed to send staff application email for {}", application.id());
|
||||||
boolean success = true;
|
return ResponseEntity.internalServerError().build();
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
if (mailSent) {
|
log.error("Error while sending staff application email for {}", application.id(), e);
|
||||||
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) {
|
|
||||||
return ResponseEntity.internalServerError().build();
|
return ResponseEntity.internalServerError().build();
|
||||||
}
|
}
|
||||||
|
|
||||||
FormResponseDto response = buildResponse(application, verified);
|
try {
|
||||||
return ResponseEntity.status(201).body(response);
|
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) {
|
private void saveApplication(StaffApplication application) {
|
||||||
|
|
@ -110,10 +102,8 @@ public class ApplicationController implements ApplicationsApi {
|
||||||
.runQuery(sqlSession -> sqlSession.getMapper(StaffApplicationMapper.class).markAsSent(applicationId));
|
.runQuery(sqlSession -> sqlSession.getMapper(StaffApplicationMapper.class).markAsSent(applicationId));
|
||||||
}
|
}
|
||||||
|
|
||||||
private FormResponseDto buildResponse(StaffApplication application, boolean verified) {
|
private FormResponseDto buildResponse(StaffApplication application) {
|
||||||
String message = verified
|
String message = "Your staff application has been submitted. You will be notified when it has been reviewed.";
|
||||||
? "Your staff application has been submitted. You will be notified when it has been reviewed."
|
|
||||||
: "Application created. Please verify your email to complete submission.";
|
|
||||||
return new FormResponseDto(
|
return new FormResponseDto(
|
||||||
application.id().toString(),
|
application.id().toString(),
|
||||||
message,
|
message,
|
||||||
|
|
|
||||||
|
|
@ -67,7 +67,14 @@
|
||||||
<div class="valid-email">
|
<div class="valid-email">
|
||||||
<ng-container matSuffix>
|
<ng-container matSuffix>
|
||||||
<mat-icon>check</mat-icon>
|
<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>
|
</ng-container>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|
@ -107,7 +114,8 @@
|
||||||
<!-- PC Requirements -->
|
<!-- PC Requirements -->
|
||||||
<div class="checkbox-field">
|
<div class="checkbox-field">
|
||||||
<mat-checkbox formControlName="meetsRequirements">
|
<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>
|
</mat-checkbox>
|
||||||
@if (form.controls.meetsRequirements.invalid && form.controls.meetsRequirements.touched) {
|
@if (form.controls.meetsRequirements.invalid && form.controls.meetsRequirements.touched) {
|
||||||
<mat-error class="checkbox-error">
|
<mat-error class="checkbox-error">
|
||||||
|
|
@ -172,7 +180,8 @@
|
||||||
<label class="field-label">Available days for moderating:</label>
|
<label class="field-label">Available days for moderating:</label>
|
||||||
<div class="days-container">
|
<div class="days-container">
|
||||||
@for (day of availableDays; track day) {
|
@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 }}
|
{{ day }}
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|
@ -188,9 +197,9 @@
|
||||||
<mat-form-field appearance="fill" style="width: 100%;">
|
<mat-form-field appearance="fill" style="width: 100%;">
|
||||||
<mat-label>Available times (Your timezone: {{ userTimezone }})</mat-label>
|
<mat-label>Available times (Your timezone: {{ userTimezone }})</mat-label>
|
||||||
<textarea matInput
|
<textarea matInput
|
||||||
formControlName="availableTimes"
|
formControlName="availableTimes"
|
||||||
placeholder="e.g., 6PM-10PM weekdays, 2PM-8PM weekends"
|
placeholder="e.g., 6PM-10PM weekdays, 2PM-8PM weekends"
|
||||||
rows="2"></textarea>
|
rows="2"></textarea>
|
||||||
@if (form.controls.availableTimes.invalid && form.controls.availableTimes.touched) {
|
@if (form.controls.availableTimes.invalid && form.controls.availableTimes.touched) {
|
||||||
<mat-error>
|
<mat-error>
|
||||||
Available times are required
|
Available times are required
|
||||||
|
|
@ -215,9 +224,9 @@
|
||||||
<mat-form-field appearance="fill" style="width: 100%;">
|
<mat-form-field appearance="fill" style="width: 100%;">
|
||||||
<mat-label>Previous experience (here or in other relevant places)</mat-label>
|
<mat-label>Previous experience (here or in other relevant places)</mat-label>
|
||||||
<textarea matInput
|
<textarea matInput
|
||||||
formControlName="previousExperience"
|
formControlName="previousExperience"
|
||||||
placeholder="Describe your previous experience"
|
placeholder="Describe your previous experience"
|
||||||
rows="4"></textarea>
|
rows="4"></textarea>
|
||||||
@if (form.controls.previousExperience.invalid && form.controls.previousExperience.touched) {
|
@if (form.controls.previousExperience.invalid && form.controls.previousExperience.touched) {
|
||||||
<mat-error>
|
<mat-error>
|
||||||
@if (form.controls.previousExperience.errors?.['required']) {
|
@if (form.controls.previousExperience.errors?.['required']) {
|
||||||
|
|
@ -233,9 +242,9 @@
|
||||||
<mat-form-field appearance="fill" style="width: 100%;">
|
<mat-form-field appearance="fill" style="width: 100%;">
|
||||||
<mat-label>Experience with plugins that players use on our server</mat-label>
|
<mat-label>Experience with plugins that players use on our server</mat-label>
|
||||||
<textarea matInput
|
<textarea matInput
|
||||||
formControlName="pluginExperience"
|
formControlName="pluginExperience"
|
||||||
placeholder="Describe your experience with our server plugins"
|
placeholder="Describe your experience with our server plugins"
|
||||||
rows="4"></textarea>
|
rows="4"></textarea>
|
||||||
@if (form.controls.pluginExperience.invalid && form.controls.pluginExperience.touched) {
|
@if (form.controls.pluginExperience.invalid && form.controls.pluginExperience.touched) {
|
||||||
<mat-error>
|
<mat-error>
|
||||||
@if (form.controls.pluginExperience.errors?.['required']) {
|
@if (form.controls.pluginExperience.errors?.['required']) {
|
||||||
|
|
@ -251,9 +260,9 @@
|
||||||
<mat-form-field appearance="fill" style="width: 100%;">
|
<mat-form-field appearance="fill" style="width: 100%;">
|
||||||
<mat-label>What do you believe the expectations of a moderator are?</mat-label>
|
<mat-label>What do you believe the expectations of a moderator are?</mat-label>
|
||||||
<textarea matInput
|
<textarea matInput
|
||||||
formControlName="moderatorExpectations"
|
formControlName="moderatorExpectations"
|
||||||
placeholder="Describe what you think a moderator should do"
|
placeholder="Describe what you think a moderator should do"
|
||||||
rows="4"></textarea>
|
rows="4"></textarea>
|
||||||
@if (form.controls.moderatorExpectations.invalid && form.controls.moderatorExpectations.touched) {
|
@if (form.controls.moderatorExpectations.invalid && form.controls.moderatorExpectations.touched) {
|
||||||
<mat-error>
|
<mat-error>
|
||||||
@if (form.controls.moderatorExpectations.errors?.['required']) {
|
@if (form.controls.moderatorExpectations.errors?.['required']) {
|
||||||
|
|
@ -269,9 +278,9 @@
|
||||||
<mat-form-field appearance="fill" style="width: 100%;">
|
<mat-form-field appearance="fill" style="width: 100%;">
|
||||||
<mat-label>Additional Information (Optional)</mat-label>
|
<mat-label>Additional Information (Optional)</mat-label>
|
||||||
<textarea matInput
|
<textarea matInput
|
||||||
formControlName="additionalInfo"
|
formControlName="additionalInfo"
|
||||||
placeholder="Any additional information you'd like to share"
|
placeholder="Any additional information you'd like to share"
|
||||||
rows="4"></textarea>
|
rows="4"></textarea>
|
||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
</div>
|
</div>
|
||||||
<button mat-raised-button (click)="onSubmit()"
|
<button mat-raised-button (click)="onSubmit()"
|
||||||
|
|
|
||||||
|
|
@ -99,6 +99,16 @@ main {
|
||||||
background-color: rgba(76, 175, 80, 0.1);
|
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 {
|
.valid-email mat-icon {
|
||||||
color: #4CAF50;
|
color: #4CAF50;
|
||||||
margin-right: 10px;
|
margin-right: 10px;
|
||||||
|
|
|
||||||
|
|
@ -107,7 +107,7 @@ export class StaffApplicationComponent implements OnInit, OnDestroy, AfterViewIn
|
||||||
}),
|
}),
|
||||||
availableTimes: new FormControl('', {
|
availableTimes: new FormControl('', {
|
||||||
nonNullable: true,
|
nonNullable: true,
|
||||||
validators: [Validators.required, Validators.maxLength(1000)]
|
validators: [Validators.required, Validators.maxLength(900)]
|
||||||
}),
|
}),
|
||||||
previousExperience: new FormControl('', {
|
previousExperience: new FormControl('', {
|
||||||
nonNullable: true,
|
nonNullable: true,
|
||||||
|
|
@ -234,10 +234,11 @@ export class StaffApplicationComponent implements OnInit, OnDestroy, AfterViewIn
|
||||||
const staffApplication: StaffApplication = this.mapToStaffApplication(this.form.getRawValue());
|
const staffApplication: StaffApplication = this.mapToStaffApplication(this.form.getRawValue());
|
||||||
|
|
||||||
this.staffApplicationService.submitStaffApplication(staffApplication).subscribe(result => {
|
this.staffApplicationService.submitStaffApplication(staffApplication).subscribe(result => {
|
||||||
//TODO route to mail page
|
if (!result.verified_mail) {
|
||||||
// Navigate to the sent page
|
throw new Error('Submitted a form with an e-mail that was not verified.');
|
||||||
|
}
|
||||||
this.router.navigate(['/forms/sent'], {
|
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();
|
}).then();
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
@ -321,7 +322,7 @@ export class StaffApplicationComponent implements OnInit, OnDestroy, AfterViewIn
|
||||||
joinDate: joinDateString,
|
joinDate: joinDateString,
|
||||||
weeklyPlaytime: Number(formData.weeklyPlaytime),
|
weeklyPlaytime: Number(formData.weeklyPlaytime),
|
||||||
availableDays: formData.availableDays,
|
availableDays: formData.availableDays,
|
||||||
availableTimes: formData.availableTimes,
|
availableTimes: `Timezone: ${this.userTimezone}\nAvailable Times:${formData.availableTimes}`,
|
||||||
previousExperience: formData.previousExperience,
|
previousExperience: formData.previousExperience,
|
||||||
pluginExperience: formData.pluginExperience,
|
pluginExperience: formData.pluginExperience,
|
||||||
moderatorExpectations: formData.moderatorExpectations,
|
moderatorExpectations: formData.moderatorExpectations,
|
||||||
|
|
|
||||||
|
|
@ -66,7 +66,6 @@ components:
|
||||||
description: Preferred pronouns of the applicant
|
description: Preferred pronouns of the applicant
|
||||||
joinDate:
|
joinDate:
|
||||||
type: string
|
type: string
|
||||||
maxLength: 256
|
|
||||||
format: date
|
format: date
|
||||||
description: Date when the applicant joined the service
|
description: Date when the applicant joined the service
|
||||||
weeklyPlaytime:
|
weeklyPlaytime:
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user