From ae1e97243842d003a57c6ad355cdb325641fdbbc Mon Sep 17 00:00:00 2001 From: akastijn Date: Tue, 5 Aug 2025 23:11:38 +0200 Subject: [PATCH] Implement appeal form flow with dynamic pages, integrate punishment selection, and add username retrieval logic. Update API schema and enhance `auth.service` for username handling. --- .../altitudeweb/config/SecurityConfig.java | 1 + .../AppealController.java | 2 +- .../pages/forms/appeal/appeal.component.html | 144 ++++++++++++++---- .../pages/forms/appeal/appeal.component.scss | 5 + .../pages/forms/appeal/appeal.component.ts | 56 +++++-- frontend/src/app/services/auth.service.ts | 30 +++- open_api/src/main/resources/api.yml | 6 + .../main/resources/schemas/login/login.yml | 25 +++ 8 files changed, 229 insertions(+), 40 deletions(-) rename backend/src/main/java/com/alttd/altitudeweb/controllers/{application => forms}/AppealController.java (96%) diff --git a/backend/src/main/java/com/alttd/altitudeweb/config/SecurityConfig.java b/backend/src/main/java/com/alttd/altitudeweb/config/SecurityConfig.java index 6133934..6c63203 100644 --- a/backend/src/main/java/com/alttd/altitudeweb/config/SecurityConfig.java +++ b/backend/src/main/java/com/alttd/altitudeweb/config/SecurityConfig.java @@ -40,6 +40,7 @@ public class SecurityConfig { .authorizeHttpRequests( auth -> auth .requestMatchers("/api/form/**").hasAuthority(PermissionClaimDto.USER.getValue()) + .requestMatchers("/api/login/userLogin").hasAuthority(PermissionClaimDto.USER.getValue()) .requestMatchers("/api/head_mod/**").hasAuthority(PermissionClaimDto.HEAD_MOD.getValue()) .requestMatchers("/api/particles/**").hasAuthority(PermissionClaimDto.HEAD_MOD.getValue()) .requestMatchers("/api/files/save/**").hasAuthority(PermissionClaimDto.HEAD_MOD.getValue()) diff --git a/backend/src/main/java/com/alttd/altitudeweb/controllers/application/AppealController.java b/backend/src/main/java/com/alttd/altitudeweb/controllers/forms/AppealController.java similarity index 96% rename from backend/src/main/java/com/alttd/altitudeweb/controllers/application/AppealController.java rename to backend/src/main/java/com/alttd/altitudeweb/controllers/forms/AppealController.java index 044ddcf..d78eebe 100644 --- a/backend/src/main/java/com/alttd/altitudeweb/controllers/application/AppealController.java +++ b/backend/src/main/java/com/alttd/altitudeweb/controllers/forms/AppealController.java @@ -1,4 +1,4 @@ -package com.alttd.altitudeweb.controllers.application; +package com.alttd.altitudeweb.controllers.forms; import com.alttd.altitudeweb.api.AppealsApi; import com.alttd.altitudeweb.services.limits.RateLimit; diff --git a/frontend/src/app/pages/forms/appeal/appeal.component.html b/frontend/src/app/pages/forms/appeal/appeal.component.html index 5d1d9f1..5c344a9 100644 --- a/frontend/src/app/pages/forms/appeal/appeal.component.html +++ b/frontend/src/app/pages/forms/appeal/appeal.component.html @@ -10,48 +10,140 @@
@if (currentPageIndex === 0) { -
- Discord -

Punishment Appeal

-

We aim to respond within 48 hours.

-
+ @if (history()?.length === 0) { +
+ Discord +

Punishment Appeal

+

You have no punishments to appeal.

+
+ } @else { +
+ Discord +

Punishment Appeal

+

We aim to respond within 48 hours.

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

Page 2

-

This is the second page of the form.

+
+

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

+
+

Notice: Submitting an appeal is not an instant process. + We will investigate the punishment you are appealing and respond within 48 hours.

+

Appeals that seem to have been made with + little to no effort will be automatically denied.

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

Page 3

-

This is the third page of the form.

+
+

Please select the punishment you want to appeal

+
+ + Punishment + + @for (punishment of history(); track punishment) { + {{ punishment.type }} - {{ punishment.reason }} + } + + + @if (selectedPunishment() != null) { + + }
} + +
+ @if (currentPageIndex === 3) { +
+
+

Please enter your email.

+

It does not have to be your minecraft email.

+ + 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 (currentPageIndex === 4) { +
+
+

Why should your {{ selectedPunishment()?.type }} be reduced or removed?

+

Please take your time writing this, we're more likely to accept an + appeal if effort was put into it.

+ + Reason + + @if (form.controls.appeal.invalid && form.controls.appeal.touched) { + + @if (form.controls.appeal.errors?.['required']) { + Reason is required + } @else if (form.controls.appeal.errors?.['minlength']) { + Reason must be at least 10 characters + } + + } + +
+ +
+ } +
-
- + @if (totalPages.length > 1) { +
+ - @for (i of totalPages; track i) { - - } + @for (i of totalPages; track i) { + + } - -
+ +
+ }
diff --git a/frontend/src/app/pages/forms/appeal/appeal.component.scss b/frontend/src/app/pages/forms/appeal/appeal.component.scss index 75ab28c..e701705 100644 --- a/frontend/src/app/pages/forms/appeal/appeal.component.scss +++ b/frontend/src/app/pages/forms/appeal/appeal.component.scss @@ -84,3 +84,8 @@ main { margin-top: auto; margin-bottom: auto; } + +.description { + max-width: 75ch; + text-align: left; +} diff --git a/frontend/src/app/pages/forms/appeal/appeal.component.ts b/frontend/src/app/pages/forms/appeal/appeal.component.ts index 1c1f1cc..14d1b94 100644 --- a/frontend/src/app/pages/forms/appeal/appeal.component.ts +++ b/frontend/src/app/pages/forms/appeal/appeal.component.ts @@ -1,10 +1,15 @@ -import {AfterViewInit, Component, ElementRef, OnInit, Renderer2} from '@angular/core'; -import {FormControl, FormGroup, Validators} from '@angular/forms'; -import {AppealsService, MinecraftAppeal} from '@api'; +import {AfterViewInit, Component, ElementRef, OnInit, Renderer2, signal} from '@angular/core'; +import {FormControl, FormGroup, ReactiveFormsModule, Validators} from '@angular/forms'; +import {AppealsService, HistoryService, MinecraftAppeal, PunishmentHistory} 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'; @Component({ selector: 'app-appeal', @@ -12,7 +17,12 @@ import {MatIconModule} from '@angular/material/icon'; HeaderComponent, NgOptimizedImage, MatButtonModule, - MatIconModule + MatIconModule, + MatProgressSpinnerModule, + MatFormFieldModule, + MatSelectModule, + MatInputModule, + ReactiveFormsModule, ], templateUrl: './appeal.component.html', styleUrl: './appeal.component.scss' @@ -22,21 +32,30 @@ export class AppealComponent implements OnInit, AfterViewInit { public form: FormGroup; private resizeObserver: ResizeObserver | null = null; private boundHandleResize: any; + protected history = signal(null); + protected selectedPunishment = signal(null); constructor( private appealApi: AppealsService, + private historyApi: HistoryService, + protected authService: AuthService, private elementRef: ElementRef, private renderer: Renderer2 ) { this.form = new FormGroup({ - username: new FormControl('', {nonNullable: true, validators: [Validators.required]}), - punishmentId: new FormControl('', {nonNullable: true, validators: [Validators.required]}), email: new FormControl('', {nonNullable: true, validators: [Validators.required, Validators.email]}), appeal: new FormControl('', {nonNullable: true, validators: [Validators.required, Validators.minLength(10)]}) }); } ngOnInit() { + const uuid = this.authService.getUuid(); + if (uuid === null) { + throw new Error('JWT subject is null, are you logged in?'); + } + this.historyApi.getAllHistoryForUUID(uuid).subscribe(history => { + this.history.set(history); + }) } ngAfterViewInit() { @@ -116,18 +135,22 @@ export class AppealComponent implements OnInit, AfterViewInit { private sendForm() { const rawValue = this.form.getRawValue(); + const uuid = this.authService.getUuid(); + if (uuid === null) { + throw new Error('JWT subject is null, are you logged in?'); + } const appeal: MinecraftAppeal = { appeal: rawValue.appeal, email: rawValue.email, - punishmentId: parseInt(rawValue.punishmentId), - username: rawValue.username, - uuid: ''//TODO + punishmentId: this.selectedPunishment()!.id, + username: this.authService.username()!, + uuid: uuid } this.appealApi.submitMinecraftAppeal(appeal).subscribe() } public currentPageIndex: number = 0; - public totalPages: number[] = [0, 1, 2]; + public totalPages: number[] = [0]; public goToPage(pageIndex: number): void { if (pageIndex >= 0 && pageIndex < this.totalPages.length) { @@ -140,6 +163,11 @@ export class AppealComponent implements OnInit, AfterViewInit { } public nextPage() { + if (this.currentPageIndex === this.totalPages.length - 1) { + console.log('Adding page'); + this.totalPages.push(this.currentPageIndex + 1); + console.log(this.totalPages); + } this.goToPage(this.currentPageIndex + 1); } @@ -150,11 +178,15 @@ export class AppealComponent implements OnInit, AfterViewInit { public isLastPage(): boolean { return this.currentPageIndex === this.totalPages.length - 1; } + + protected readonly length = length; + + onPunishmentSelected($event: PunishmentHistory) { + this.selectedPunishment.set($event); + } } interface Appeal { - username: FormControl; - punishmentId: FormControl; email: FormControl; appeal: FormControl; } diff --git a/frontend/src/app/services/auth.service.ts b/frontend/src/app/services/auth.service.ts index 7a3044a..ac8616f 100644 --- a/frontend/src/app/services/auth.service.ts +++ b/frontend/src/app/services/auth.service.ts @@ -1,4 +1,4 @@ -import {Injectable} from '@angular/core'; +import {Injectable, signal} from '@angular/core'; import {LoginService} from '@api'; import {BehaviorSubject, Observable, throwError} from 'rxjs'; import {catchError, tap} from 'rxjs/operators'; @@ -17,6 +17,8 @@ export class AuthService { private userClaimsSubject = new BehaviorSubject(null); public userClaims$ = this.userClaimsSubject.asObservable(); private jwtHelper = new JwtHelperService(); + private _username = signal(environment.defaultAuthStatus ? 'akastijn' : null); + public readonly username = this._username.asReadonly(); constructor( private loginService: LoginService, @@ -34,6 +36,8 @@ export class AuthService { tap(jwt => { this.saveJwt(jwt); this.isAuthenticatedSubject.next(true); + + this.reloadUsername(); }), catchError(error => { this.snackBar.open('Login failed', '', {duration: 2000}); @@ -42,6 +46,17 @@ export class AuthService { ); } + private reloadUsername() { + this.loginService.getUsername().pipe( + tap(username => { + this._username.set(username.username); + }), + catchError(error => { + return throwError(() => error); + }) + ) + } + /** * Log the user out by removing the JWT */ @@ -49,6 +64,7 @@ export class AuthService { localStorage.removeItem('jwt'); this.isAuthenticatedSubject.next(false); this.userClaimsSubject.next(null); + this._username.set(null); } /** @@ -68,6 +84,7 @@ export class AuthService { console.log("User claims: ", claims); this.userClaimsSubject.next(claims); this.isAuthenticatedSubject.next(true); + this.reloadUsername(); return true; } catch (e) { this.logout(); @@ -113,4 +130,15 @@ export class AuthService { const userAuthorizations = this.getUserAuthorizations(); return requiredAuthorizations.some(auth => userAuthorizations.includes(auth)); } + + public getUuid(): string | null { + if (environment.defaultAuthStatus) { + return '55e46bc3-2a29-4c53-850f-dbd944dc5c5f'; + } + const jwtClaims = this.userClaimsSubject.getValue(); + if (jwtClaims === null) { + return null; + } + return jwtClaims.sub ?? null; + } } diff --git a/open_api/src/main/resources/api.yml b/open_api/src/main/resources/api.yml index 5fedc78..243390f 100644 --- a/open_api/src/main/resources/api.yml +++ b/open_api/src/main/resources/api.yml @@ -22,6 +22,10 @@ tags: description: Retrieves information about the staff team - name: particles description: All actions related to particles + - name: forms + description: All actions shared between forms + - name: appeals + description: All action related to appeals paths: /api/team/{team}: $ref: './schemas/team/team.yml#/getTeam' @@ -53,6 +57,8 @@ paths: $ref: './schemas/login/login.yml#/RequestNewUserLogin' /api/login/userLogin/{code}: $ref: './schemas/login/login.yml#/UserLogin' + /api/login/getUsername: + $ref: './schemas/login/login.yml#/GetUsername' /api/files/save/{filename}: $ref: './schemas/particles/particles.yml#/SaveFile' /api/files/save/{uuid}/{filename}: diff --git a/open_api/src/main/resources/schemas/login/login.yml b/open_api/src/main/resources/schemas/login/login.yml index 570bc68..67c2bd9 100644 --- a/open_api/src/main/resources/schemas/login/login.yml +++ b/open_api/src/main/resources/schemas/login/login.yml @@ -62,6 +62,25 @@ RequestNewUserLogin: application/json: schema: $ref: '../generic/errors.yml#/components/schemas/ApiError' +GetUsername: + get: + tags: + - login + summary: Get the username for the logged in user + operationId: getUsername + responses: + '200': + description: Username retrieved + content: + application/json: + schema: + $ref: '#/components/schemas/Username' + default: + description: Unexpected error + content: + application/json: + schema: + $ref: '../generic/errors.yml#/components/schemas/ApiError' components: parameters: Code: @@ -72,6 +91,12 @@ components: type: string description: The code to log in with schemas: + Username: + properties: + username: + type: string + required: + - username LoginData: type: object required: