diff --git a/backend/build.gradle.kts b/backend/build.gradle.kts index 9b1f12d..fc44155 100644 --- a/backend/build.gradle.kts +++ b/backend/build.gradle.kts @@ -40,6 +40,10 @@ dependencies { implementation("org.springframework.boot:spring-boot-starter-mail:3.1.5") implementation("org.springframework.boot:spring-boot-starter-thymeleaf") + //Open API + implementation("io.swagger.core.v3:swagger-annotations:2.2.37") + implementation("io.swagger.core.v3:swagger-models:2.2.37") + //AOP implementation("org.aspectj:aspectjrt:1.9.19") implementation("org.aspectj:aspectjweaver:1.9.19") 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 6ba3f6d..2a397d1 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 @@ -10,8 +10,8 @@ import com.alttd.altitudeweb.database.web_db.forms.AppealMapper; import com.alttd.altitudeweb.database.web_db.mail.EmailVerification; import com.alttd.altitudeweb.database.web_db.mail.EmailVerificationMapper; import com.alttd.altitudeweb.mappers.AppealDataMapper; -import com.alttd.altitudeweb.model.AppealResponseDto; import com.alttd.altitudeweb.model.DiscordAppealDto; +import com.alttd.altitudeweb.model.FormResponseDto; import com.alttd.altitudeweb.model.MinecraftAppealDto; import com.alttd.altitudeweb.model.UpdateMailDto; import com.alttd.altitudeweb.services.limits.RateLimit; @@ -25,7 +25,6 @@ import org.springframework.web.bind.annotation.RestController; import org.springframework.web.server.ResponseStatusException; import java.util.Optional; -import java.util.UUID; import java.util.concurrent.CompletableFuture; import java.util.concurrent.TimeUnit; @@ -41,13 +40,13 @@ public class AppealController implements AppealsApi { @RateLimit(limit = 3, timeValue = 1, timeUnit = TimeUnit.HOURS, key = "discordAppeal") @Override - public ResponseEntity submitDiscordAppeal(DiscordAppealDto discordAppealDto) { + public ResponseEntity submitDiscordAppeal(DiscordAppealDto discordAppealDto) { throw new ResponseStatusException(HttpStatusCode.valueOf(501), "Discord appeals are not yet supported"); } @RateLimit(limit = 3, timeValue = 1, timeUnit = TimeUnit.HOURS, key = "minecraftAppeal") @Override - public ResponseEntity submitMinecraftAppeal(MinecraftAppealDto minecraftAppealDto) { + public ResponseEntity submitMinecraftAppeal(MinecraftAppealDto minecraftAppealDto) { boolean success = true; CompletableFuture appealCompletableFuture = new CompletableFuture<>(); @@ -105,7 +104,7 @@ public class AppealController implements AppealsApi { sqlSession.getMapper(AppealMapper.class) .markAppealAsSent(appeal.id()); }); - AppealResponseDto appealResponseDto = new AppealResponseDto( + FormResponseDto appealResponseDto = new FormResponseDto( appeal.id().toString(), "Your appeal has been submitted. You will be notified when it has been reviewed.", true); @@ -113,7 +112,8 @@ public class AppealController implements AppealsApi { } @Override - public ResponseEntity updateMail(UpdateMailDto updateMailDto) { + public ResponseEntity updateMail(UpdateMailDto updateMailDto) { + //TODO move to its own endpoint throw new ResponseStatusException(HttpStatusCode.valueOf(501), "Updating mail is not yet supported"); } diff --git a/backend/src/main/java/com/alttd/altitudeweb/controllers/forms/ApplicationController.java b/backend/src/main/java/com/alttd/altitudeweb/controllers/forms/ApplicationController.java new file mode 100644 index 0000000..dcab6c8 --- /dev/null +++ b/backend/src/main/java/com/alttd/altitudeweb/controllers/forms/ApplicationController.java @@ -0,0 +1,15 @@ +package com.alttd.altitudeweb.controllers.forms; + +import com.alttd.altitudeweb.api.ApplicationsApi; +import com.alttd.altitudeweb.model.FormResponseDto; +import com.alttd.altitudeweb.model.StaffApplicationDto; +import org.springframework.http.HttpStatusCode; +import org.springframework.http.ResponseEntity; +import org.springframework.web.server.ResponseStatusException; + +public class ApplicationController implements ApplicationsApi { + @Override + public ResponseEntity submitStaffApplication(StaffApplicationDto staffApplicationDto) { + throw new ResponseStatusException(HttpStatusCode.valueOf(501), "Staff applications are not yet supported"); + } +} diff --git a/frontend/src/app/app.routes.ts b/frontend/src/app/app.routes.ts index 9dbaac1..7cea8be 100644 --- a/frontend/src/app/app.routes.ts +++ b/frontend/src/app/app.routes.ts @@ -126,6 +126,14 @@ export const routes: Routes = [ requiredAuthorizations: ['SCOPE_user'] } }, + { + path: 'forms/staff-application', + loadComponent: () => import('./pages/forms/staff-application/staff-application.component').then(m => m.StaffApplicationComponent), + canActivate: [AuthGuard], + data: { + requiredAuthorizations: ['SCOPE_user'] + } + }, { path: 'community', loadComponent: () => import('./pages/altitude/community/community.component').then(m => m.CommunityComponent) diff --git a/frontend/src/app/pages/forms/forms.component.html b/frontend/src/app/pages/forms/forms.component.html index 29f2061..cef82a3 100644 --- a/frontend/src/app/pages/forms/forms.component.html +++ b/frontend/src/app/pages/forms/forms.component.html @@ -17,6 +17,14 @@

+ diff --git a/frontend/src/app/pages/forms/staff-application/staff-application.component.html b/frontend/src/app/pages/forms/staff-application/staff-application.component.html new file mode 100644 index 0000000..395e821 --- /dev/null +++ b/frontend/src/app/pages/forms/staff-application/staff-application.component.html @@ -0,0 +1,309 @@ +
+ +
+

Staff Application

+
+
+
+
+
+
+ + @if (currentPageIndex === 0) { +
+ Logo +

Moderator Application

+

Thank you for your interest in becoming a moderator on our Minecraft server.

+

Please take your time to fill out this application thoroughly.

+ +
+ } + + + @if (currentPageIndex === 1) { +
+
+

You are logged in as {{ authService.username() }}. If this is the correct account + please continue

+
+

Notice: Submitting a staff application is not an instant process. + We will review your application carefully and get back to you if we think you're a good fit.

+

Applications that seem to have been made with + little to no effort will be automatically rejected.

+
+ +
+ } + +
+ + @if (currentPageIndex === 2) { +
+
+

Basic Information

+ + + + Email + + @if (form.controls.email.invalid && form.controls.email.touched) { + + @if (form.controls.email.errors?.['required']) { + Email is required + } @else if (form.controls.email.errors?.['email']) { + Please enter a valid email address + } + + } + + @if (emailIsValid()) { +
+ + check + You have validated your email previously, and can continue to the next page! + +
+ } + + + + Age + + @if (form.controls.age.invalid && form.controls.age.touched) { + + @if (form.controls.age.errors?.['required']) { + Age is required + } @else if (form.controls.age.errors?.['min']) { + You must be at least 13 years old + } @else if (form.controls.age.errors?.['pattern']) { + Please enter a valid number + } + + } + + + + + Discord Username + + @if (form.controls.discordUsername.invalid && form.controls.discordUsername.touched) { + + Discord username is required + + } + + + +
+ + I confirm that I meet the PC requirements (able to record video at 30fps 720p or higher, and able to talk in voice chat) + + @if (form.controls.meetsRequirements.invalid && form.controls.meetsRequirements.touched) { + + You must meet the PC requirements to apply + + } +
+ + + + Pronouns (Optional) + + +
+ +
+ } + + + @if (currentPageIndex === 3) { +
+
+

Experience & Availability

+ + + + When did you join our server? (Estimate) + + + + @if (form.controls.joinDate.invalid && form.controls.joinDate.touched) { + + Join date is required + + } + + + + + Average expected playtime in a week (hours) + + @if (form.controls.weeklyPlaytime.invalid && form.controls.weeklyPlaytime.touched) { + + @if (form.controls.weeklyPlaytime.errors?.['required']) { + Weekly playtime is required + } @else if (form.controls.weeklyPlaytime.errors?.['min']) { + Weekly playtime must be at least 1 hour + } + + } + + + +
+ +
+ @for (day of availableDays; track day) { +
+ {{ day }} +
+ } +
+ @if (form.controls.availableDays.invalid && form.controls.availableDays.touched) { + + Please select at least one day + + } +
+ + + + Available times (Your timezone: {{ userTimezone }}) + + @if (form.controls.availableTimes.invalid && form.controls.availableTimes.touched) { + + Available times are required + + } + +
+ +
+ } + + + @if (currentPageIndex === 4) { +
+
+

Qualifications & Expectations

+ + + + Previous experience (here or in other relevant places) + + @if (form.controls.previousExperience.invalid && form.controls.previousExperience.touched) { + + @if (form.controls.previousExperience.errors?.['required']) { + Previous experience is required + } @else if (form.controls.previousExperience.errors?.['minlength']) { + Please provide more details (at least 10 characters) + } + + } + + + + + Experience with plugins that players use on our server + + @if (form.controls.pluginExperience.invalid && form.controls.pluginExperience.touched) { + + @if (form.controls.pluginExperience.errors?.['required']) { + Plugin experience is required + } @else if (form.controls.pluginExperience.errors?.['minlength']) { + Please provide more details (at least 10 characters) + } + + } + + + + + What do you believe the expectations of a moderator are? + + @if (form.controls.moderatorExpectations.invalid && form.controls.moderatorExpectations.touched) { + + @if (form.controls.moderatorExpectations.errors?.['required']) { + Moderator expectations are required + } @else if (form.controls.moderatorExpectations.errors?.['minlength']) { + Please provide more details (at least 10 characters) + } + + } + + + + + Additional Information (Optional) + + +
+ +
+ } +
+
+ + + @if (totalPages.length > 1) { +
+ + + @for (i of totalPages; track i) { + + } + + +
+ } +
+
+
+
diff --git a/frontend/src/app/pages/forms/staff-application/staff-application.component.scss b/frontend/src/app/pages/forms/staff-application/staff-application.component.scss new file mode 100644 index 0000000..535a690 --- /dev/null +++ b/frontend/src/app/pages/forms/staff-application/staff-application.component.scss @@ -0,0 +1,164 @@ +:host { + display: block; +} + +.staff-application-container { + display: flex; + flex-direction: column; + min-height: 80vh; +} + +main { + flex: 1; + display: flex; + flex-direction: column; +} + +.form-container { + position: relative; + height: 100%; + width: 100%; + display: flex; + flex-direction: column; + flex: 1; +} + +.formPage { + display: flex; + flex-direction: column; + align-items: center; + text-align: center; + width: 100%; + height: 100%; + animation: fadeIn 0.5s ease-in-out; +} + +@keyframes fadeIn { + from { + opacity: 0; + transform: translateY(20px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +.navigation-buttons { + display: flex; + gap: 16px; + margin-top: 20px; +} + +.form-navigation { + display: flex; + justify-content: center; + gap: 10px; + position: absolute; + bottom: 0; + left: 0; + right: 0; +} + +.nav-dot { + width: 12px; + height: 12px; + border-radius: 50%; + background-color: rgba(255, 255, 255, 0.3); + cursor: pointer; + transition: background-color 0.3s ease; + margin-top: auto; + margin-bottom: auto; + + &.active { + background-color: #fff; + } +} + +.nav-button { + color: #1f9bde; +} + +.pages { + margin-top: auto; + margin-bottom: auto; +} + +.description { + max-width: 75ch; + text-align: left; +} + +.valid-email { + display: flex; + align-items: center; + color: #4CAF50; + 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; +} + +.valid-email span { + color: #4CAF50; + font-weight: 500; +} + +.checkbox-field { + margin: 16px 0; + display: flex; + flex-direction: column; + align-items: flex-start; + width: 100%; +} + +.checkbox-error { + color: #f44336; + font-size: 12px; + margin-top: 4px; +} + +.field-container { + margin: 16px 0; + width: 100%; +} + +.field-label { + font-size: 16px; + margin-bottom: 8px; + display: block; + color: rgba(255, 255, 255, 0.7); +} + +.days-container { + display: flex; + flex-wrap: wrap; + gap: 8px; + margin-bottom: 8px; +} + +.day-chip { + padding: 8px 12px; + border-radius: 16px; + background-color: rgba(255, 255, 255, 0.1); + cursor: pointer; + transition: all 0.3s ease; + + &.selected { + background-color: #1f9bde; + color: white; + } + + &:hover { + background-color: rgba(255, 255, 255, 0.2); + } +} + +mat-form-field { + margin-bottom: 16px; +} diff --git a/frontend/src/app/pages/forms/staff-application/staff-application.component.ts b/frontend/src/app/pages/forms/staff-application/staff-application.component.ts new file mode 100644 index 0000000..5333b28 --- /dev/null +++ b/frontend/src/app/pages/forms/staff-application/staff-application.component.ts @@ -0,0 +1,348 @@ +import { + AfterViewInit, + Component, + computed, + ElementRef, + inject, + OnDestroy, + OnInit, + Renderer2, + signal +} from '@angular/core'; +import {FormControl, FormGroup, ReactiveFormsModule, Validators} from '@angular/forms'; +import {ApplicationsService, EmailEntry, MailService, StaffApplication} from '@api'; +import {HeaderComponent} from '@header/header.component'; +import {NgOptimizedImage} from '@angular/common'; +import {MatButtonModule} from '@angular/material/button'; +import {MatIconModule} from '@angular/material/icon'; +import {AuthService} from '@services/auth.service'; +import {MatProgressSpinnerModule} from '@angular/material/progress-spinner'; +import {MatFormFieldModule} from '@angular/material/form-field'; +import {MatSelectModule} from '@angular/material/select'; +import {MatInputModule} from '@angular/material/input'; +import {MatDialog} from '@angular/material/dialog'; +import {VerifyMailDialogComponent} from '@pages/forms/verify-mail-dialog/verify-mail-dialog.component'; +import {Router} from '@angular/router'; +import {MatCheckboxModule} from '@angular/material/checkbox'; +import {MatDatepickerModule} from '@angular/material/datepicker'; +import {MatNativeDateModule} from '@angular/material/core'; +import {MatChipsModule} from '@angular/material/chips'; + +@Component({ + selector: 'app-staff-application', + imports: [ + HeaderComponent, + NgOptimizedImage, + MatButtonModule, + MatIconModule, + MatProgressSpinnerModule, + MatFormFieldModule, + MatSelectModule, + MatInputModule, + ReactiveFormsModule, + MatCheckboxModule, + MatDatepickerModule, + MatNativeDateModule, + MatChipsModule + ], + templateUrl: './staff-application.component.html', + styleUrl: './staff-application.component.scss' +}) +export class StaffApplicationComponent implements OnInit, OnDestroy, AfterViewInit { + + private mailService = inject(MailService); + public authService = inject(AuthService); + public staffApplicationService = inject(ApplicationsService) + private resizeObserver: ResizeObserver | null = null; + private boundHandleResize: any; + + protected form: FormGroup; + private emails = signal([]); + protected verifiedEmails = computed(() => this.emails() + .filter(email => email.verified) + .map(email => email.email.toLowerCase())); + protected emailIsValid = signal(false); + protected dialog = inject(MatDialog); + protected availableDays: string[] = ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday']; + protected selectedDays: string[] = []; + protected userTimezone: string = Intl.DateTimeFormat().resolvedOptions().timeZone; + + + constructor( + private elementRef: ElementRef, + private renderer: Renderer2 + ) { + const staffApplication: StaffApplicationForm = { + email: new FormControl('', { + nonNullable: true, + validators: [Validators.required, Validators.email, Validators.maxLength(320)] + }), + age: new FormControl('', { + nonNullable: true, + validators: [Validators.required, Validators.min(13), Validators.pattern('^[0-9]*$')] + }), + discordUsername: new FormControl('', { + nonNullable: true, + validators: [Validators.required, Validators.maxLength(32)] + }), + meetsRequirements: new FormControl(false, { + nonNullable: true, + validators: [Validators.requiredTrue] + }), + pronouns: new FormControl('', { + nonNullable: true, + validators: [Validators.maxLength(32)] + }), + joinDate: new FormControl('', { + nonNullable: true, + validators: [Validators.required] + }), + weeklyPlaytime: new FormControl('', { + nonNullable: true, + validators: [Validators.required, Validators.min(1)] + }), + availableDays: new FormControl([], { + nonNullable: true, + validators: [Validators.required] + }), + availableTimes: new FormControl('', { + nonNullable: true, + validators: [Validators.required, Validators.maxLength(1000)] + }), + previousExperience: new FormControl('', { + nonNullable: true, + validators: [Validators.required, Validators.minLength(10), Validators.maxLength(4000)] + }), + pluginExperience: new FormControl('', { + nonNullable: true, + validators: [Validators.required, Validators.minLength(10), Validators.maxLength(4000)] + }), + moderatorExpectations: new FormControl('', { + nonNullable: true, + validators: [Validators.required, Validators.minLength(10), Validators.maxLength(4000)] + }), + additionalInfo: new FormControl('', { + nonNullable: true, + validators: [Validators.maxLength(4000)] + }) + } + this.form = new FormGroup(staffApplication); + + this.mailService.getUserEmails().subscribe(emails => { + this.emails.set(emails); + }); + + this.form.valueChanges.subscribe(() => { + if (this.verifiedEmails().includes(this.form.getRawValue().email.toLowerCase())) { + this.emailIsValid.set(true); + } else { + this.emailIsValid.set(false); + } + }); + + computed(() => { + if (this.verifiedEmails().length > 0) { + this.form.get('email')?.setValue(this.verifiedEmails()[0]); + this.emailIsValid.set(true); + } + }); + } + + ngOnInit() { + const uuid = this.authService.getUuid(); + if (uuid === null) { + alert('Error retrieving token, please relog on the website and try again') + throw new Error('JWT subject is null, are you logged in?'); + } + } + + ngAfterViewInit() { + this.setupResizeObserver(); + this.updateContainerHeight(); + + this.boundHandleResize = this.handleResize.bind(this); + window.addEventListener('resize', this.boundHandleResize); + + setTimeout(() => this.updateContainerHeight(), 0); + } + + ngOnDestroy() { + if (this.resizeObserver) { + this.resizeObserver.disconnect(); + this.resizeObserver = null; + } + + if (this.boundHandleResize) { + window.removeEventListener('resize', this.boundHandleResize); + } + } + + private handleResize() { + this.updateContainerHeight(); + } + + private setupResizeObserver() { + this.resizeObserver = new ResizeObserver(() => { + this.updateContainerHeight(); + }); + + const headerElement = document.querySelector('app-header'); + if (headerElement) { + this.resizeObserver.observe(headerElement); + } + + const footerElement = document.querySelector('footer'); + if (footerElement) { + this.resizeObserver.observe(footerElement); + } + } + + private updateContainerHeight() { + const headerElement = document.querySelector('app-header'); + const footerElement = document.querySelector('footer'); + + const container = this.elementRef.nativeElement.querySelector('.staff-application-container'); + + if (headerElement && footerElement && container) { + const headerHeight = headerElement.getBoundingClientRect().height; + const footerHeight = footerElement.getBoundingClientRect().height; + + const calculatedHeight = `calc(100vh - ${headerHeight}px - ${footerHeight}px)`; + this.renderer.setStyle(container, 'min-height', calculatedHeight); + } + } + + public onSubmit() { + if (this.form === undefined) { + console.error('Form is undefined'); + return + } + if (this.form.valid) { + this.sendForm() + } else { + // Mark all fields as touched to show validation errors + Object.keys(this.form.controls).forEach(field => { + const control = this.form.get(field); + control?.markAsTouched(); + }); + } + } + + private router = inject(Router) + + private sendForm() { + const staffApplication: StaffApplication = this.mapToStaffApplication(this.form.getRawValue()); + + this.staffApplicationService.submitStaffApplication(staffApplication).subscribe(result => { + //TODO route to mail page + // Navigate to the sent page + 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.'} + }).then(); + }) + } + + public currentPageIndex: number = 0; + public totalPages: number[] = [0, 1, 2, 3, 4]; + + public goToPage(pageIndex: number): void { + if (pageIndex >= 0 && pageIndex < this.totalPages.length) { + this.currentPageIndex = pageIndex; + } + } + + public previousPage() { + this.goToPage(this.currentPageIndex - 1); + } + + public nextPage() { + this.goToPage(this.currentPageIndex + 1); + } + + public isFirstPage(): boolean { + return this.currentPageIndex === 0; + } + + public isLastPage(): boolean { + return this.currentPageIndex === this.totalPages.length - 1; + } + + protected validateMailOrNextPage() { + if (this.emailIsValid()) { + this.nextPage(); + return; + } + const dialogRef = this.dialog.open(VerifyMailDialogComponent, { + data: {email: this.form.getRawValue().email}, + }); + dialogRef.afterClosed().subscribe(result => { + if (result === true) { + this.emailIsValid.set(true); + this.nextPage(); + } + }); + } + + toggleDay(day: string) { + const availableDaysControl = this.form.get('availableDays'); + const currentDays = [...(availableDaysControl?.value || [])]; + + if (currentDays.includes(day)) { + const index = currentDays.indexOf(day); + currentDays.splice(index, 1); + } else { + currentDays.push(day); + } + + availableDaysControl?.setValue(currentDays); + } + + private mapToStaffApplication(formData: any): StaffApplication { + let joinDateString: string; + + if (formData.joinDate instanceof Date) { + joinDateString = formData.joinDate.toISOString(); + } else if (typeof formData.joinDate === 'string' && formData.joinDate.trim() !== '') { + const parsedDate = new Date(formData.joinDate); + if (isNaN(parsedDate.getTime())) { + throw new Error('Invalid date string'); + } + joinDateString = parsedDate.toISOString(); + } else { + throw new Error('Invalid date string'); + } + + return { + email: formData.email, + age: Number(formData.age), + discordUsername: formData.discordUsername, + meetsRequirements: formData.meetsRequirements, + pronouns: formData.pronouns || '', + joinDate: joinDateString, + weeklyPlaytime: Number(formData.weeklyPlaytime), + availableDays: formData.availableDays, + availableTimes: formData.availableTimes, + previousExperience: formData.previousExperience, + pluginExperience: formData.pluginExperience, + moderatorExpectations: formData.moderatorExpectations, + additionalInfo: formData.additionalInfo || '' + }; + + } +} + +interface StaffApplicationForm { + email: FormControl; + age: FormControl; + discordUsername: FormControl; + meetsRequirements: FormControl; + pronouns: FormControl; + joinDate: FormControl; + weeklyPlaytime: FormControl; + availableDays: FormControl; + availableTimes: FormControl; + previousExperience: FormControl; + pluginExperience: FormControl; + moderatorExpectations: FormControl; + additionalInfo: FormControl; +} diff --git a/open_api/build.gradle.kts b/open_api/build.gradle.kts index d02fd1e..335bf55 100644 --- a/open_api/build.gradle.kts +++ b/open_api/build.gradle.kts @@ -43,9 +43,9 @@ sourceSets { dependencies { implementation("org.springframework.boot:spring-boot-starter-web") implementation("org.springframework.boot:spring-boot-starter-validation") - implementation("io.swagger.core.v3:swagger-annotations:2.2.20") - implementation("io.swagger.core.v3:swagger-models:2.2.8") - implementation("io.swagger.core.v3:swagger-core:2.2.8") + implementation("io.swagger.core.v3:swagger-annotations:2.2.37") + implementation("io.swagger.core.v3:swagger-models:2.2.37") + implementation("io.swagger.core.v3:swagger-core:2.2.37") implementation("org.openapitools:jackson-databind-nullable:0.2.6") implementation("org.springframework.hateoas:spring-hateoas:2.2.0") diff --git a/open_api/src/main/resources/api.yml b/open_api/src/main/resources/api.yml index f617581..7fb448e 100644 --- a/open_api/src/main/resources/api.yml +++ b/open_api/src/main/resources/api.yml @@ -55,6 +55,8 @@ paths: $ref: './schemas/forms/appeal/appeal.yml#/MinecraftAppeal' /api/appeal/discord-appeal: $ref: './schemas/forms/appeal/appeal.yml#/DiscordAppeal' + /api/apply/staff-application: + $ref: './schemas/forms/staff_apply/staff_apply.yml#/StaffApply' /api/login/requestNewUserLogin/{uuid}: $ref: './schemas/login/login.yml#/RequestNewUserLogin' /api/login/userLogin/{code}: diff --git a/open_api/src/main/resources/schemas/forms/appeal/appeal.yml b/open_api/src/main/resources/schemas/forms/appeal/appeal.yml index 213eb26..176c840 100644 --- a/open_api/src/main/resources/schemas/forms/appeal/appeal.yml +++ b/open_api/src/main/resources/schemas/forms/appeal/appeal.yml @@ -23,7 +23,7 @@ UpdateMail: content: application/json: schema: - $ref: '#/components/schemas/AppealResponse' + $ref: '../../generic/components.yml#/components/schemas/FormResponse' default: description: Unexpected error content: @@ -49,7 +49,7 @@ MinecraftAppeal: content: application/json: schema: - $ref: '#/components/schemas/AppealResponse' + $ref: '../../generic/components.yml#/components/schemas/FormResponse' default: description: Unexpected error content: @@ -75,7 +75,7 @@ DiscordAppeal: content: application/json: schema: - $ref: '#/components/schemas/AppealResponse' + $ref: '../../generic/components.yml#/components/schemas/FormResponse' default: description: Unexpected error content: @@ -136,22 +136,6 @@ components: appeal: type: string description: Appeal text explaining why the punishment should be reconsidered - AppealResponse: - type: object - required: - - id - - message - - verified_mail - properties: - id: - type: string - description: Unique identifier for the submitted appeal for referring to it later - message: - type: string - description: Confirmation message - verified_mail: - type: boolean - description: If this user has verified their mail already UpdateMail: type: object required: diff --git a/open_api/src/main/resources/schemas/forms/staff_apply/staff_apply.yml b/open_api/src/main/resources/schemas/forms/staff_apply/staff_apply.yml new file mode 100644 index 0000000..ace07b5 --- /dev/null +++ b/open_api/src/main/resources/schemas/forms/staff_apply/staff_apply.yml @@ -0,0 +1,104 @@ +StaffApply: + post: + tags: + - applications + summary: Submit a Staff appeal + description: Submit an staff application + operationId: submitStaffApplication + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/StaffApplication' + responses: + '201': + description: Application created please verify email + content: + application/json: + schema: + $ref: '../../generic/components.yml#/components/schemas/FormResponse' + default: + description: Unexpected error + content: + application/json: + schema: + $ref: '../../generic/errors.yml#/components/schemas/ApiError' +components: + schemas: + StaffApplication: + type: object + description: Schema for staff application + required: + - email + - age + - discordUsername + - meetsRequirements + - pronouns + - joinDate + - weeklyPlaytime + - availableDays + - availableTimes + - previousExperience + - pluginExperience + - moderatorExpectations + - additionalInfo + properties: + email: + type: string + format: email + maxLength: 320 + description: Email address of the applicant + age: + type: integer + minimum: 13 + description: Age of the applicant, must be 13 or older + discordUsername: + type: string + maxLength: 32 + description: Discord username of the applicant + meetsRequirements: + type: boolean + description: Confirmation that the applicant meets all requirements + pronouns: + type: string + maxLength: 32 + description: Preferred pronouns of the applicant + joinDate: + type: string + maxLength: 256 + format: date + description: Date when the applicant joined the service + weeklyPlaytime: + type: integer + minimum: 1 + description: Average weekly playtime in hours + availableDays: + type: array + items: + type: string + maxLength: 256 + description: Days of the week when the applicant is available + availableTimes: + type: string + maxLength: 1000 + description: Time ranges when the applicant is available + previousExperience: + type: string + minLength: 10 + maxLength: 4000 + description: Description of previous relevant experience + pluginExperience: + type: string + minLength: 10 + maxLength: 4000 + description: Description of experience with plugins + moderatorExpectations: + type: string + minLength: 10 + maxLength: 4000 + description: Applicant's expectations and understanding of moderator responsibilities + additionalInfo: + type: string + maxLength: 4000 + description: Any additional information the applicant wishes to provide diff --git a/open_api/src/main/resources/schemas/generic/components.yml b/open_api/src/main/resources/schemas/generic/components.yml new file mode 100644 index 0000000..a8f7cb4 --- /dev/null +++ b/open_api/src/main/resources/schemas/generic/components.yml @@ -0,0 +1,18 @@ +components: + schemas: + FormResponse: + type: object + required: + - id + - message + - verified_mail + properties: + id: + type: string + description: Unique identifier for the submitted form for referring to it later + message: + type: string + description: Confirmation message + verified_mail: + type: boolean + description: If this user has verified their mail already