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.

This commit is contained in:
2025-10-24 21:10:34 +02:00
parent f117cb2477
commit 64ea68ab39
15 changed files with 176 additions and 62 deletions
@@ -6,13 +6,25 @@
<input matInput type="text" [(ngModel)]="reason" placeholder="Enter reason"/>
</mat-form-field>
<mat-checkbox [(ngModel)]="isPermanent">Permanent</mat-checkbox>
<mat-checkbox [(ngModel)]="isPermanent"><span style="color: black">Permanent</span></mat-checkbox>
@if (!isPermanent) {
<mat-form-field appearance="fill">
<mat-label>Expires</mat-label>
<input matInput type="datetime-local" [(ngModel)]="expiresAtLocal" [disabled]="isPermanent"/>
</mat-form-field>
<div class="datetime-container">
<mat-form-field appearance="fill" class="date-field">
<mat-label>Expiry Date</mat-label>
<input matInput [matDatepicker]="dateTimePicker" [(ngModel)]="expiryDate" [disabled]="isPermanent">
<mat-hint>MM/DD/YYYY</mat-hint>
<mat-datepicker-toggle matIconSuffix [for]="dateTimePicker"></mat-datepicker-toggle>
<mat-datepicker #dateTimePicker></mat-datepicker>
</mat-form-field>
<mat-form-field appearance="fill" class="time-field">
<mat-label>Expiry Time</mat-label>
<input matInput type="time" [(ngModel)]="expiryTime" [disabled]="isPermanent">
<mat-hint>HH:MM (24-hour)</mat-hint>
</mat-form-field>
<p>Time in timezone: {{ getTimezone() }}</p>
</div>
}
@if (errorMessage()) {
@@ -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;
}
@@ -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<boolean>(false);
errorMessage = signal<string | null>(null);
protected isBusy = signal<boolean>(false);
protected errorMessage = signal<string | null>(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<Promise<PunishmentHistory>> = [] 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';
}
}
@@ -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`;
@@ -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) {
+2
View File
@@ -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));