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));