From 64ea68ab3910cfe68ce6cb97c52cd70a9273c2a1 Mon Sep 17 00:00:00 2001 From: akastijn Date: Fri, 24 Oct 2025 21:10:34 +0200 Subject: [PATCH] Refactor `AuthenticatedUuid` to singleton service and replace static calls across the codebase. Add JWT authority converters, improve punishment expiry handling, and enhance frontend dialog functionality for editing punishments. Extend CORS allowed methods and origins. --- .../altitudeweb/config/SecurityConfig.java | 51 +++++++++++++- .../altitudeweb/controllers/CorsConfig.java | 2 +- .../data_from_auth/AuthenticatedUuid.java | 11 ++- .../forms/ApplicationController.java | 3 +- .../controllers/forms/MailController.java | 11 +-- .../history/HistoryApiController.java | 17 +++-- .../controllers/login/LoginController.java | 3 +- .../controllers/site/SiteController.java | 3 +- .../resources/application-test.properties | 2 +- .../edit-punishment-dialog.component.html | 22 ++++-- .../edit-punishment-dialog.component.scss | 24 ++++++- .../edit-punishment-dialog.component.ts | 69 +++++++++++-------- .../reference/bans/history-format.service.ts | 2 +- .../bans/history/history.component.ts | 16 ++--- frontend/src/main.ts | 2 + 15 files changed, 176 insertions(+), 62 deletions(-) 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 6fac873..2c185ee 100644 --- a/backend/src/main/java/com/alttd/altitudeweb/config/SecurityConfig.java +++ b/backend/src/main/java/com/alttd/altitudeweb/config/SecurityConfig.java @@ -9,6 +9,7 @@ import com.nimbusds.jose.jwk.source.ImmutableJWKSet; import com.nimbusds.jose.jwk.source.JWKSource; import com.nimbusds.jose.proc.SecurityContext; import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.http.HttpMethod; @@ -17,16 +18,22 @@ import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; import org.springframework.security.config.http.SessionCreationPolicy; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.security.oauth2.jwt.JwtDecoder; import org.springframework.security.oauth2.jwt.JwtEncoder; import org.springframework.security.oauth2.jwt.NimbusJwtDecoder; import org.springframework.security.oauth2.jwt.NimbusJwtEncoder; +import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationConverter; import org.springframework.security.web.SecurityFilterChain; import java.security.KeyPair; import java.security.interfaces.RSAPrivateKey; import java.security.interfaces.RSAPublicKey; +import java.util.*; +import java.util.stream.Collectors; +@Slf4j @Configuration @EnableWebSecurity @RequiredArgsConstructor @@ -54,7 +61,7 @@ public class SecurityConfig { .csrf(AbstractHttpConfigurer::disable) .oauth2ResourceServer( oauth2 -> oauth2 - .jwt(Customizer.withDefaults()) + .jwt(jwt -> jwt.jwtAuthenticationConverter(jwtAuthenticationConverter())) .authenticationEntryPoint(securityAuthFailureHandler) .accessDeniedHandler(securityAuthFailureHandler) ) @@ -85,4 +92,46 @@ public class SecurityConfig { KeyPair keyPair = keyPairService.getJwtSigningKeyPair(); return NimbusJwtDecoder.withPublicKey((RSAPublicKey) keyPair.getPublic()).build(); } + + + + @Bean + public JwtAuthenticationConverter jwtAuthenticationConverter() { + JwtAuthenticationConverter converter = new JwtAuthenticationConverter(); + converter.setJwtGrantedAuthoritiesConverter(jwt -> { + Map claims = jwt.getClaims(); + + Object authoritiesClaim = claims.get("authorities"); + if (authoritiesClaim instanceof List authorities) { + Collection authorityList = authorities.stream() + .map(Object::toString) + .map(SimpleGrantedAuthority::new) + .collect(Collectors.toList()); + log.debug("Authorities found in authorities: {}", authorityList); + return authorityList; + } + + Object scopeClaim = claims.get("scope"); + if (scopeClaim instanceof String scopeString) { + Collection authorityList = Arrays.stream(scopeString.split(" ")) + .map(scope -> new SimpleGrantedAuthority("SCOPE_" + scope)) + .collect(Collectors.toList()); + log.debug("Authorities found in authorities scope string: {}", authorityList); + return authorityList; + } + + if (scopeClaim instanceof List scopeList) { + Collection authorityList = scopeList.stream() + .map(Object::toString) + .map(scope -> new SimpleGrantedAuthority("SCOPE_" + scope)) + .collect(Collectors.toList()); + log.debug("Authorities found in authorities scope list: {}", authorityList); + return authorityList; + } + + log.debug("No granted authorities found"); + return Collections.emptyList(); + }); + return converter; + } } diff --git a/backend/src/main/java/com/alttd/altitudeweb/controllers/CorsConfig.java b/backend/src/main/java/com/alttd/altitudeweb/controllers/CorsConfig.java index 421f901..5f317b2 100644 --- a/backend/src/main/java/com/alttd/altitudeweb/controllers/CorsConfig.java +++ b/backend/src/main/java/com/alttd/altitudeweb/controllers/CorsConfig.java @@ -18,7 +18,7 @@ public class CorsConfig implements WebMvcConfigurer { log.info("Registering CORS mappings for {}", String.join(", ", allowedOrigins)); registry.addMapping("/**") .allowedOrigins(allowedOrigins) - .allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS") + .allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS", "PATCH") .allowedHeaders("*") .allowCredentials(true); } diff --git a/backend/src/main/java/com/alttd/altitudeweb/controllers/data_from_auth/AuthenticatedUuid.java b/backend/src/main/java/com/alttd/altitudeweb/controllers/data_from_auth/AuthenticatedUuid.java index 204239d..c765e9e 100644 --- a/backend/src/main/java/com/alttd/altitudeweb/controllers/data_from_auth/AuthenticatedUuid.java +++ b/backend/src/main/java/com/alttd/altitudeweb/controllers/data_from_auth/AuthenticatedUuid.java @@ -1,24 +1,33 @@ package com.alttd.altitudeweb.controllers.data_from_auth; +import org.springframework.beans.factory.annotation.Value; import org.springframework.http.HttpStatus; import org.springframework.security.core.Authentication; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.oauth2.jwt.Jwt; +import org.springframework.stereotype.Service; import org.springframework.web.server.ResponseStatusException; import java.util.UUID; +@Service public class AuthenticatedUuid { + @Value("${UNSECURED:#{false}}") + private boolean unsecured; + /** * Extracts and validates the authenticated user's UUID from the JWT token. * * @return The UUID of the authenticated user * @throws ResponseStatusException with 401 status if authentication is invalid */ - public static UUID getAuthenticatedUserUuid() { + public UUID getAuthenticatedUserUuid() { Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); if (authentication == null || !(authentication.getPrincipal() instanceof Jwt jwt)) { + if (unsecured) { + return UUID.fromString("55e46bc3-2a29-4c53-850f-dbd944dc5c5f"); + } throw new ResponseStatusException(HttpStatus.UNAUTHORIZED, "Authentication required"); } 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 index 5719399..aef4343 100644 --- a/backend/src/main/java/com/alttd/altitudeweb/controllers/forms/ApplicationController.java +++ b/backend/src/main/java/com/alttd/altitudeweb/controllers/forms/ApplicationController.java @@ -33,6 +33,7 @@ import java.util.concurrent.TimeUnit; @RateLimit(limit = 30, timeValue = 1, timeUnit = TimeUnit.HOURS) public class ApplicationController implements ApplicationsApi { + private final AuthenticatedUuid authenticatedUuid; private final StaffApplicationDataMapper staffApplicationDataMapper; private final StaffApplicationMail staffApplicationMail; private final StaffApplicationDiscord staffApplicationDiscord; @@ -55,7 +56,7 @@ public class ApplicationController implements ApplicationsApi { if (!isOpen()) { return ResponseEntity.status(HttpStatus.FORBIDDEN).build(); } - UUID userUuid = AuthenticatedUuid.getAuthenticatedUserUuid(); + UUID userUuid = authenticatedUuid.getAuthenticatedUserUuid(); String email = staffApplicationDto.getEmail() == null ? null : staffApplicationDto.getEmail().toLowerCase(); Optional optionalEmail = fetchEmailVerification(userUuid, email); diff --git a/backend/src/main/java/com/alttd/altitudeweb/controllers/forms/MailController.java b/backend/src/main/java/com/alttd/altitudeweb/controllers/forms/MailController.java index 3bbe45f..722ba53 100644 --- a/backend/src/main/java/com/alttd/altitudeweb/controllers/forms/MailController.java +++ b/backend/src/main/java/com/alttd/altitudeweb/controllers/forms/MailController.java @@ -28,11 +28,12 @@ import java.util.concurrent.TimeUnit; public class MailController implements MailApi { private final MailVerificationService mailVerificationService; + private final AuthenticatedUuid authenticatedUuid; @Override @RateLimit(limit = 5, timeValue = 1, timeUnit = TimeUnit.HOURS, key = "mailSubmit") public ResponseEntity submitEmailForVerification(SubmitEmailDto submitEmailDto) { - UUID uuid = AuthenticatedUuid.getAuthenticatedUserUuid(); + UUID uuid = authenticatedUuid.getAuthenticatedUserUuid(); boolean emailAlreadyVerified = mailVerificationService.listAll(uuid).stream() .filter(EmailVerification::verified) .map(EmailVerification::email) @@ -52,7 +53,7 @@ public class MailController implements MailApi { @Override @RateLimit(limit = 20, timeValue = 1, timeUnit = TimeUnit.HOURS, key = "mailVerify") public ResponseEntity verifyEmailCode(VerifyCodeDto verifyCodeDto) { - UUID uuid = AuthenticatedUuid.getAuthenticatedUserUuid(); + UUID uuid = authenticatedUuid.getAuthenticatedUserUuid(); Optional optionalEmailVerification = mailVerificationService.verifyCode(uuid, verifyCodeDto.getCode()); if (optionalEmailVerification.isEmpty()) { throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Invalid verification code"); @@ -68,7 +69,7 @@ public class MailController implements MailApi { @Override @RateLimit(limit = 5, timeValue = 1, timeUnit = TimeUnit.HOURS, key = "mailResend") public ResponseEntity resendVerificationEmail(SubmitEmailDto submitEmailDto) { - UUID uuid = AuthenticatedUuid.getAuthenticatedUserUuid(); + UUID uuid = authenticatedUuid.getAuthenticatedUserUuid(); EmailVerification updated = mailVerificationService.resend(uuid, submitEmailDto.getEmail()); if (updated == null) { throw new ResponseStatusException(HttpStatus.NOT_FOUND, "Email not found for user"); @@ -83,7 +84,7 @@ public class MailController implements MailApi { @Override @RateLimit(limit = 10, timeValue = 1, timeUnit = TimeUnit.HOURS, key = "mailDelete") public ResponseEntity deleteEmail(SubmitEmailDto submitEmailDto) { - UUID uuid = AuthenticatedUuid.getAuthenticatedUserUuid(); + UUID uuid = authenticatedUuid.getAuthenticatedUserUuid(); boolean deleted = mailVerificationService.delete(uuid, submitEmailDto.getEmail()); if (!deleted) { throw new ResponseStatusException(HttpStatus.NOT_FOUND, "Email not found for user"); @@ -93,7 +94,7 @@ public class MailController implements MailApi { @Override public ResponseEntity> getUserEmails() { - UUID uuid = AuthenticatedUuid.getAuthenticatedUserUuid(); + UUID uuid = authenticatedUuid.getAuthenticatedUserUuid(); List emails = mailVerificationService.listAll(uuid); List result = emails.stream() .map(ev -> new EmailEntryDto().email(ev.email()).verified(ev.verified())) diff --git a/backend/src/main/java/com/alttd/altitudeweb/controllers/history/HistoryApiController.java b/backend/src/main/java/com/alttd/altitudeweb/controllers/history/HistoryApiController.java index 76ce05c..7983f80 100644 --- a/backend/src/main/java/com/alttd/altitudeweb/controllers/history/HistoryApiController.java +++ b/backend/src/main/java/com/alttd/altitudeweb/controllers/history/HistoryApiController.java @@ -9,6 +9,7 @@ import com.alttd.altitudeweb.setup.Connection; import com.alttd.altitudeweb.database.Databases; import com.alttd.altitudeweb.database.litebans.*; import com.alttd.altitudeweb.model.PunishmentHistoryDto; +import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.http.ResponseEntity; import org.springframework.security.access.prepost.PreAuthorize; @@ -22,8 +23,11 @@ import java.util.concurrent.TimeUnit; @Slf4j @RestController @RateLimit(limit = 30, timeValue = 10, timeUnit = TimeUnit.SECONDS) +@RequiredArgsConstructor public class HistoryApiController implements HistoryApi { + private final AuthenticatedUuid authenticatedUuid; + @Override public ResponseEntity getHistoryForAll(String userType, String type, Integer page) { return getHistoryForUsers(userType, type, "", page); @@ -232,10 +236,9 @@ public class HistoryApiController implements HistoryApi { .id(historyRecord.getId()); } - // Admin edit endpoints (restricted to head_mod scope) @Override - @PreAuthorize("hasAuthority('SCOPE_head_mod')") public ResponseEntity updatePunishmentReason(String type, Integer id, String reason) { + log.debug("Updating reason for {} id {} to {}", type, id, reason); HistoryType historyTypeEnum = HistoryType.getHistoryType(type); CompletableFuture result = new CompletableFuture<>(); @@ -250,7 +253,7 @@ public class HistoryApiController implements HistoryApi { } int changed = editMapper.setReason(historyTypeEnum, id, reason); HistoryRecord after = idMapper.getRecentHistory(historyTypeEnum, id); - UUID actor = AuthenticatedUuid.getAuthenticatedUserUuid(); + UUID actor = authenticatedUuid.getAuthenticatedUserUuid(); log.info("[Punishment Edit] Actor={} Type={} Id={} Reason: '{}' -> '{}' (rows={})", actor, historyTypeEnum, id, before.getReason(), after != null ? after.getReason() : null, changed); result.complete(after != null ? mapPunishmentHistory(after) : null); @@ -267,8 +270,8 @@ public class HistoryApiController implements HistoryApi { } @Override - @PreAuthorize("hasAuthority('SCOPE_head_mod')") public ResponseEntity updatePunishmentUntil(String type, Integer id, Long until) { + log.debug("Updating until for {} id {} to {}", type, id, until); HistoryType historyTypeEnum = HistoryType.getHistoryType(type); CompletableFuture result = new CompletableFuture<>(); @@ -283,7 +286,7 @@ public class HistoryApiController implements HistoryApi { } int changed = editMapper.setUntil(historyTypeEnum, id, until); HistoryRecord after = idMapper.getRecentHistory(historyTypeEnum, id); - UUID actor = AuthenticatedUuid.getAuthenticatedUserUuid(); + UUID actor = authenticatedUuid.getAuthenticatedUserUuid(); log.info("[Punishment Edit] Actor={} Type={} Id={} Until: '{}' -> '{}' (rows={})", actor, historyTypeEnum, id, before.getUntil(), after != null ? after.getUntil() : null, changed); result.complete(after != null ? mapPunishmentHistory(after) : null); @@ -303,8 +306,8 @@ public class HistoryApiController implements HistoryApi { } @Override - @PreAuthorize("hasAuthority('SCOPE_head_mod')") public ResponseEntity removePunishment(String type, Integer id) { + log.debug("Removing punishment for {} id {}", type, id); HistoryType historyTypeEnum = HistoryType.getHistoryType(type); CompletableFuture result = new CompletableFuture<>(); @@ -317,7 +320,7 @@ public class HistoryApiController implements HistoryApi { result.complete(false); return; } - UUID actorUuid = AuthenticatedUuid.getAuthenticatedUserUuid(); + UUID actorUuid = authenticatedUuid.getAuthenticatedUserUuid(); String actorName = sqlSession.getMapper(RecentNamesMapper.class).getUsername(actorUuid.toString()); int changed = editMapper.remove(historyTypeEnum, id); log.info("[Punishment Remove] Actor={} ({}) Type={} Id={} Before(active={} removedBy={} reason='{}') (rows={})", diff --git a/backend/src/main/java/com/alttd/altitudeweb/controllers/login/LoginController.java b/backend/src/main/java/com/alttd/altitudeweb/controllers/login/LoginController.java index 18f1389..1238f58 100644 --- a/backend/src/main/java/com/alttd/altitudeweb/controllers/login/LoginController.java +++ b/backend/src/main/java/com/alttd/altitudeweb/controllers/login/LoginController.java @@ -37,6 +37,7 @@ import java.util.concurrent.TimeUnit; public class LoginController implements LoginApi { private final JwtEncoder jwtEncoder; + private final AuthenticatedUuid authenticatedUuid; @Value("${login.secret:#{null}}") private String loginSecret; @@ -99,7 +100,7 @@ public class LoginController implements LoginApi { try { // Get authenticated UUID using the utility method - UUID uuid = AuthenticatedUuid.getAuthenticatedUserUuid(); + UUID uuid = authenticatedUuid.getAuthenticatedUserUuid(); log.debug("Loaded username for logged in user {}", uuid); // Create response with username diff --git a/backend/src/main/java/com/alttd/altitudeweb/controllers/site/SiteController.java b/backend/src/main/java/com/alttd/altitudeweb/controllers/site/SiteController.java index c9c47a1..d9242b1 100644 --- a/backend/src/main/java/com/alttd/altitudeweb/controllers/site/SiteController.java +++ b/backend/src/main/java/com/alttd/altitudeweb/controllers/site/SiteController.java @@ -22,10 +22,11 @@ import java.util.concurrent.TimeUnit; public class SiteController implements SiteApi { private final VoteService voteService; + private final AuthenticatedUuid authenticatedUuid; @Override public ResponseEntity getVoteStats() { - UUID uuid = AuthenticatedUuid.getAuthenticatedUserUuid(); + UUID uuid = authenticatedUuid.getAuthenticatedUserUuid(); Optional optionalVoteDataDto = voteService.getVoteStats(uuid); if (optionalVoteDataDto.isEmpty()) { return ResponseEntity.noContent().build(); diff --git a/backend/src/main/resources/application-test.properties b/backend/src/main/resources/application-test.properties index be52ca8..0111b30 100644 --- a/backend/src/main/resources/application-test.properties +++ b/backend/src/main/resources/application-test.properties @@ -1,4 +1,4 @@ -cors.allowed-origins=${CORS:http://localhost:4200,http://localhost:8080} +cors.allowed-origins=${CORS:http://localhost:4200,http://localhost:8080,http://localhost:80} my-server.address=${SERVER_ADDRESS:http://localhost:8080} logging.level.com.alttd.altitudeweb=DEBUG logging.level.org.springframework.security=DEBUG diff --git a/frontend/src/app/pages/reference/bans/edit-punishment-dialog/edit-punishment-dialog.component.html b/frontend/src/app/pages/reference/bans/edit-punishment-dialog/edit-punishment-dialog.component.html index b773dc9..7132a3c 100644 --- a/frontend/src/app/pages/reference/bans/edit-punishment-dialog/edit-punishment-dialog.component.html +++ b/frontend/src/app/pages/reference/bans/edit-punishment-dialog/edit-punishment-dialog.component.html @@ -6,13 +6,25 @@ - Permanent + Permanent @if (!isPermanent) { - - Expires - - +
+ + Expiry Date + + MM/DD/YYYY + + + + + + Expiry Time + + HH:MM (24-hour) + +

Time in timezone: {{ getTimezone() }}

+
} @if (errorMessage()) { diff --git a/frontend/src/app/pages/reference/bans/edit-punishment-dialog/edit-punishment-dialog.component.scss b/frontend/src/app/pages/reference/bans/edit-punishment-dialog/edit-punishment-dialog.component.scss index 10e3fd2..c7ba775 100644 --- a/frontend/src/app/pages/reference/bans/edit-punishment-dialog/edit-punishment-dialog.component.scss +++ b/frontend/src/app/pages/reference/bans/edit-punishment-dialog/edit-punishment-dialog.component.scss @@ -5,6 +5,26 @@ margin-top: 8px; } -.error { - color: #d32f2f; /* Material error color */ +.datetime-container { + display: flex; + flex-wrap: wrap; + gap: 16px; + + .date-field { + flex: 1; + min-width: 180px; + } + + .time-field { + min-width: 120px; + } + + p { + color: #beb8b8; + } +} + +.error { + color: red; + margin-top: 16px; } diff --git a/frontend/src/app/pages/reference/bans/edit-punishment-dialog/edit-punishment-dialog.component.ts b/frontend/src/app/pages/reference/bans/edit-punishment-dialog/edit-punishment-dialog.component.ts index 2198824..9ff48ac 100644 --- a/frontend/src/app/pages/reference/bans/edit-punishment-dialog/edit-punishment-dialog.component.ts +++ b/frontend/src/app/pages/reference/bans/edit-punishment-dialog/edit-punishment-dialog.component.ts @@ -13,6 +13,7 @@ import { } from '@angular/material/dialog'; import {HistoryService, PunishmentHistory} from '@api'; import {firstValueFrom} from 'rxjs'; +import {MatDatepickerModule} from '@angular/material/datepicker'; interface EditPunishmentData { punishment: PunishmentHistory; @@ -31,19 +32,22 @@ interface EditPunishmentData { MatDialogTitle, MatDialogContent, MatDialogActions, + MatDatepickerModule, ], templateUrl: './edit-punishment-dialog.component.html', styleUrl: './edit-punishment-dialog.component.scss' }) export class EditPunishmentDialogComponent { // Form model - reason: string = ''; - isPermanent: boolean = false; - expiresAtLocal: string = ''; + protected reason: string = ''; + protected isPermanent: boolean = false; + protected expiresAt: Date; + protected expiryDate: Date | null = null; + protected expiryTime: string = ''; // UI state - isBusy = signal(false); - errorMessage = signal(null); + protected isBusy = signal(false); + protected errorMessage = signal(null); private historyApi = inject(HistoryService); @@ -53,17 +57,15 @@ export class EditPunishmentDialogComponent { ) { const punishment = data.punishment; this.reason = punishment.reason ?? ''; - const permanent = punishment.expiryTime <= 0; - this.isPermanent = permanent; - if (!permanent) { - const date = new Date(punishment.expiryTime); - const pad = (n: number) => n.toString().padStart(2, '0'); - const year = date.getFullYear(); - const month = pad(date.getMonth() + 1); - const day = pad(date.getDate()); - const hours = pad(date.getHours()); - const minutes = pad(date.getMinutes()); - this.expiresAtLocal = `${year}-${month}-${day}T${hours}:${minutes}`; + this.isPermanent = punishment.expiryTime <= 0; + this.expiresAt = new Date(punishment.expiryTime); + + if (this.expiresAt && !isNaN(this.expiresAt.getTime())) { + this.expiryDate = new Date(this.expiresAt); + + const hours = this.expiresAt.getHours().toString().padStart(2, '0'); + const minutes = this.expiresAt.getMinutes().toString().padStart(2, '0'); + this.expiryTime = `${hours}:${minutes}`; } } @@ -76,12 +78,23 @@ export class EditPunishmentDialogComponent { private computeUntilMs(): number { if (this.isPermanent) { - return -1; + return 0; } - if (!this.expiresAtLocal) { - return -1; + + if (!this.expiryDate) { + return 0; } - const ms = new Date(this.expiresAtLocal).getTime(); + + const combinedDateTime = new Date(this.expiryDate); + + if (this.expiryTime) { + const [hours, minutes] = this.expiryTime.split(':').map(Number); + combinedDateTime.setHours(hours, minutes, 0, 0); + } + + this.expiresAt = combinedDateTime; + + const ms = combinedDateTime.getTime(); return isNaN(ms) ? -1 : ms; } @@ -95,17 +108,15 @@ export class EditPunishmentDialogComponent { const updates: Array> = [] as any; - // Update reason if changed if ((this.reason ?? '') !== (punishment.reason ?? '')) { + console.log('Changing reason to ', this.reason, ' from ', punishment.reason, '') updates.push(firstValueFrom(this.historyApi.updatePunishmentReason(punishment.type, punishment.id, this.reason))); } - // Update expiry for ban/mute only - if (punishment.type === 'ban' || punishment.type === 'mute') { - const newUntil = this.computeUntilMs(); - if (newUntil !== punishment.expiryTime) { - updates.push(firstValueFrom(this.historyApi.updatePunishmentUntil(punishment.type, punishment.id, newUntil))); - } + const newUntil = this.computeUntilMs(); + if (newUntil !== punishment.expiryTime) { + console.log('Changing until to ', newUntil, ' from ', punishment.expiryTime, '') + updates.push(firstValueFrom(this.historyApi.updatePunishmentUntil(punishment.type, punishment.id, newUntil))); } if (updates.length === 0) { @@ -147,4 +158,8 @@ export class EditPunishmentDialogComponent { } }) } + + getTimezone() { + return Intl.DateTimeFormat().resolvedOptions().timeZone ?? 'Unknown'; + } } diff --git a/frontend/src/app/pages/reference/bans/history-format.service.ts b/frontend/src/app/pages/reference/bans/history-format.service.ts index 7694292..9bf8b34 100644 --- a/frontend/src/app/pages/reference/bans/history-format.service.ts +++ b/frontend/src/app/pages/reference/bans/history-format.service.ts @@ -60,7 +60,7 @@ export class HistoryFormatService { public getAvatarUrl(entry: string, size: string = '25'): string { let uuid = entry.replace('-', ''); - if (uuid === 'C') { + if (uuid === 'C' || uuid === 'Console' || uuid === '[Console]') { uuid = "f78a4d8dd51b4b3998a3230f2de0c670" } return `https://crafatar.com/avatars/${uuid}?size=${size}&overlay`; diff --git a/frontend/src/app/pages/reference/bans/history/history.component.ts b/frontend/src/app/pages/reference/bans/history/history.component.ts index a8c14af..2027122 100644 --- a/frontend/src/app/pages/reference/bans/history/history.component.ts +++ b/frontend/src/app/pages/reference/bans/history/history.component.ts @@ -1,4 +1,4 @@ -import {Component, EventEmitter, Input, OnChanges, OnInit, Output} from '@angular/core'; +import {Component, EventEmitter, inject, Input, OnChanges, OnInit, Output} from '@angular/core'; import {HistoryService, PunishmentHistory} from '@api'; import {catchError, map, Observable, shareReplay} from 'rxjs'; import {NgOptimizedImage} from '@angular/common'; @@ -38,12 +38,11 @@ export class HistoryComponent implements OnInit, OnChanges { public history: PunishmentHistory[] = [] - constructor(private historyApi: HistoryService, - public historyFormat: HistoryFormatService, - private router: Router, - private authService: AuthService, - private dialog: MatDialog) { - } + private historyApi: HistoryService = inject(HistoryService) + public historyFormat: HistoryFormatService = inject(HistoryFormatService) + private router: Router = inject(Router) + private authService: AuthService = inject(AuthService) + private dialog: MatDialog = inject(MatDialog) ngOnChanges(): void { this.reloadHistory(); @@ -116,7 +115,8 @@ export class HistoryComponent implements OnInit, OnChanges { public openEdit(punishment: PunishmentHistory) { if (!this.canEdit()) return; const ref = this.dialog.open(EditPunishmentDialogComponent, { - data: {punishment} + data: {punishment}, + width: '500px', }); ref.afterClosed().subscribe(result => { if (result) { diff --git a/frontend/src/main.ts b/frontend/src/main.ts index 8557039..5e50aee 100644 --- a/frontend/src/main.ts +++ b/frontend/src/main.ts @@ -4,6 +4,7 @@ import {provideRouter} from '@angular/router'; import {routes} from './app/app.routes'; import {provideHttpClient, withInterceptors} from '@angular/common/http'; import {authInterceptor} from '@services/AuthInterceptor'; +import {provideNativeDateAdapter} from '@angular/material/core'; bootstrapApplication(AppComponent, { providers: [ @@ -12,6 +13,7 @@ bootstrapApplication(AppComponent, { provideHttpClient( withInterceptors([authInterceptor]) ), + provideNativeDateAdapter() ] }).catch(err => console.error(err));