Add admin endpoints for editing and removing punishments and implement frontend dialog for punishment management
This commit is contained in:
parent
b71ea7da8b
commit
41dab473b0
|
|
@ -47,6 +47,7 @@ public class SecurityConfig {
|
||||||
.requestMatchers("/api/head_mod/**").hasAuthority(PermissionClaimDto.HEAD_MOD.getValue())
|
.requestMatchers("/api/head_mod/**").hasAuthority(PermissionClaimDto.HEAD_MOD.getValue())
|
||||||
.requestMatchers("/api/particles/**").hasAuthority(PermissionClaimDto.HEAD_MOD.getValue())
|
.requestMatchers("/api/particles/**").hasAuthority(PermissionClaimDto.HEAD_MOD.getValue())
|
||||||
.requestMatchers("/api/files/save/**").hasAuthority(PermissionClaimDto.HEAD_MOD.getValue())
|
.requestMatchers("/api/files/save/**").hasAuthority(PermissionClaimDto.HEAD_MOD.getValue())
|
||||||
|
.requestMatchers("/api/history/admin/**").hasAuthority(PermissionClaimDto.HEAD_MOD.getValue())
|
||||||
.requestMatchers("/api/login/userLogin/**").permitAll()
|
.requestMatchers("/api/login/userLogin/**").permitAll()
|
||||||
.anyRequest().permitAll()
|
.anyRequest().permitAll()
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
package com.alttd.altitudeweb.controllers.history;
|
package com.alttd.altitudeweb.controllers.history;
|
||||||
|
|
||||||
import com.alttd.altitudeweb.api.HistoryApi;
|
import com.alttd.altitudeweb.api.HistoryApi;
|
||||||
|
import com.alttd.altitudeweb.controllers.data_from_auth.AuthenticatedUuid;
|
||||||
import com.alttd.altitudeweb.services.limits.RateLimit;
|
import com.alttd.altitudeweb.services.limits.RateLimit;
|
||||||
import com.alttd.altitudeweb.model.HistoryCountDto;
|
import com.alttd.altitudeweb.model.HistoryCountDto;
|
||||||
import com.alttd.altitudeweb.model.PunishmentHistoryListDto;
|
import com.alttd.altitudeweb.model.PunishmentHistoryListDto;
|
||||||
|
|
@ -10,7 +11,8 @@ import com.alttd.altitudeweb.database.litebans.*;
|
||||||
import com.alttd.altitudeweb.model.PunishmentHistoryDto;
|
import com.alttd.altitudeweb.model.PunishmentHistoryDto;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.springframework.http.ResponseEntity;
|
import org.springframework.http.ResponseEntity;
|
||||||
import org.springframework.web.bind.annotation.RestController;
|
import org.springframework.security.access.prepost.PreAuthorize;
|
||||||
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
|
|
@ -229,4 +231,109 @@ public class HistoryApiController implements HistoryApi {
|
||||||
.type(type)
|
.type(type)
|
||||||
.id(historyRecord.getId());
|
.id(historyRecord.getId());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Admin edit endpoints (restricted to head_mod scope)
|
||||||
|
@Override
|
||||||
|
@PreAuthorize("hasAuthority('SCOPE_head_mod')")
|
||||||
|
public ResponseEntity<PunishmentHistoryDto> updatePunishmentReason(String type, Integer id, String reason) {
|
||||||
|
HistoryType historyTypeEnum = HistoryType.getHistoryType(type);
|
||||||
|
CompletableFuture<PunishmentHistoryDto> result = new CompletableFuture<>();
|
||||||
|
|
||||||
|
Connection.getConnection(Databases.LITE_BANS).runQuery(sqlSession -> {
|
||||||
|
try {
|
||||||
|
IdHistoryMapper idMapper = sqlSession.getMapper(IdHistoryMapper.class);
|
||||||
|
EditHistoryMapper editMapper = sqlSession.getMapper(EditHistoryMapper.class);
|
||||||
|
HistoryRecord before = idMapper.getRecentHistory(historyTypeEnum, id);
|
||||||
|
if (before == null) {
|
||||||
|
result.complete(null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
int changed = editMapper.setReason(historyTypeEnum, id, reason);
|
||||||
|
HistoryRecord after = idMapper.getRecentHistory(historyTypeEnum, id);
|
||||||
|
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);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("Failed to update reason for {} id {}", type, id, e);
|
||||||
|
result.completeExceptionally(e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
PunishmentHistoryDto body = result.join();
|
||||||
|
if (body == null) {
|
||||||
|
return ResponseEntity.notFound().build();
|
||||||
|
}
|
||||||
|
return ResponseEntity.ok(body);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@PreAuthorize("hasAuthority('SCOPE_head_mod')")
|
||||||
|
public ResponseEntity<PunishmentHistoryDto> updatePunishmentUntil(String type, Integer id, Long until) {
|
||||||
|
HistoryType historyTypeEnum = HistoryType.getHistoryType(type);
|
||||||
|
CompletableFuture<PunishmentHistoryDto> result = new CompletableFuture<>();
|
||||||
|
|
||||||
|
Connection.getConnection(Databases.LITE_BANS).runQuery(sqlSession -> {
|
||||||
|
try {
|
||||||
|
IdHistoryMapper idMapper = sqlSession.getMapper(IdHistoryMapper.class);
|
||||||
|
EditHistoryMapper editMapper = sqlSession.getMapper(EditHistoryMapper.class);
|
||||||
|
HistoryRecord before = idMapper.getRecentHistory(historyTypeEnum, id);
|
||||||
|
if (before == null) {
|
||||||
|
result.complete(null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
int changed = editMapper.setUntil(historyTypeEnum, id, until);
|
||||||
|
HistoryRecord after = idMapper.getRecentHistory(historyTypeEnum, id);
|
||||||
|
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);
|
||||||
|
} catch (IllegalArgumentException e) {
|
||||||
|
log.warn("Invalid until edit for type {} id {}: {}", type, id, e.getMessage());
|
||||||
|
result.complete(null);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("Failed to update until for {} id {}", type, id, e);
|
||||||
|
result.completeExceptionally(e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
PunishmentHistoryDto body = result.join();
|
||||||
|
if (body == null) {
|
||||||
|
return ResponseEntity.notFound().build();
|
||||||
|
}
|
||||||
|
return ResponseEntity.ok(body);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@PreAuthorize("hasAuthority('SCOPE_head_mod')")
|
||||||
|
public ResponseEntity<Void> removePunishment(String type, Integer id) {
|
||||||
|
HistoryType historyTypeEnum = HistoryType.getHistoryType(type);
|
||||||
|
CompletableFuture<Boolean> result = new CompletableFuture<>();
|
||||||
|
|
||||||
|
Connection.getConnection(Databases.LITE_BANS).runQuery(sqlSession -> {
|
||||||
|
try {
|
||||||
|
IdHistoryMapper idMapper = sqlSession.getMapper(IdHistoryMapper.class);
|
||||||
|
EditHistoryMapper editMapper = sqlSession.getMapper(EditHistoryMapper.class);
|
||||||
|
HistoryRecord before = idMapper.getRecentHistory(historyTypeEnum, id);
|
||||||
|
if (before == null) {
|
||||||
|
result.complete(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
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={})",
|
||||||
|
actorName, actorUuid, historyTypeEnum, id,
|
||||||
|
before.getRemovedByName() == null ? 1 : 0, before.getRemovedByName(), before.getRemovedByReason(),
|
||||||
|
changed);
|
||||||
|
result.complete(changed > 0);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("Failed to remove punishment for {} id {}", type, id, e);
|
||||||
|
result.completeExceptionally(e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
Boolean ok = result.join();
|
||||||
|
if (ok == null || !ok) {
|
||||||
|
return ResponseEntity.notFound().build();
|
||||||
|
}
|
||||||
|
return ResponseEntity.noContent().build();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,64 @@
|
||||||
|
package com.alttd.altitudeweb.database.litebans;
|
||||||
|
|
||||||
|
import org.apache.ibatis.annotations.Delete;
|
||||||
|
import org.apache.ibatis.annotations.Param;
|
||||||
|
import org.apache.ibatis.annotations.Update;
|
||||||
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
|
||||||
|
public interface EditHistoryMapper {
|
||||||
|
|
||||||
|
@Update("""
|
||||||
|
UPDATE ${table_name}
|
||||||
|
SET reason = #{reason}
|
||||||
|
WHERE id = #{id}
|
||||||
|
""")
|
||||||
|
int updateReason(@Param("table_name") String tableName,
|
||||||
|
@Param("id") int id,
|
||||||
|
@Param("reason") String reason);
|
||||||
|
|
||||||
|
@Update("""
|
||||||
|
UPDATE ${table_name}
|
||||||
|
SET until = #{until}
|
||||||
|
WHERE id = #{id}
|
||||||
|
""")
|
||||||
|
int updateUntil(@Param("table_name") String tableName,
|
||||||
|
@Param("id") int id,
|
||||||
|
@Param("until") Long until);
|
||||||
|
|
||||||
|
@Delete("""
|
||||||
|
DELETE FROM ${table_name}
|
||||||
|
WHERE id = #{id}
|
||||||
|
""")
|
||||||
|
int deletePunishment(@Param("table_name") String tableName,
|
||||||
|
@Param("id") int id);
|
||||||
|
|
||||||
|
default int setReason(@NotNull HistoryType type, int id, String reason) {
|
||||||
|
return switch (type) {
|
||||||
|
case ALL -> throw new IllegalArgumentException("HistoryType.ALL is not supported");
|
||||||
|
case BAN -> updateReason("litebans_bans", id, reason);
|
||||||
|
case MUTE -> updateReason("litebans_mutes", id, reason);
|
||||||
|
case KICK -> updateReason("litebans_kicks", id, reason);
|
||||||
|
case WARN -> updateReason("litebans_warnings", id, reason);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
default int setUntil(@NotNull HistoryType type, int id, Long until) {
|
||||||
|
return switch (type) {
|
||||||
|
case ALL -> throw new IllegalArgumentException("HistoryType.ALL is not supported");
|
||||||
|
case BAN -> updateUntil("litebans_bans", id, until);
|
||||||
|
case MUTE -> updateUntil("litebans_mutes", id, until);
|
||||||
|
case KICK -> throw new IllegalArgumentException("KICK has no until");
|
||||||
|
case WARN -> throw new IllegalArgumentException("WARN has no until");
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
default int remove(@NotNull HistoryType type, int id) {
|
||||||
|
return switch (type) {
|
||||||
|
case ALL -> throw new IllegalArgumentException("HistoryType.ALL is not supported");
|
||||||
|
case BAN -> deletePunishment("litebans_bans", id);
|
||||||
|
case MUTE -> deletePunishment("litebans_mutes", id);
|
||||||
|
case KICK -> deletePunishment("litebans_kicks", id);
|
||||||
|
case WARN -> deletePunishment("litebans_warnings", id);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -19,6 +19,7 @@ public class InitializeLiteBans {
|
||||||
configuration.addMapper(UUIDHistoryMapper.class);
|
configuration.addMapper(UUIDHistoryMapper.class);
|
||||||
configuration.addMapper(HistoryCountMapper.class);
|
configuration.addMapper(HistoryCountMapper.class);
|
||||||
configuration.addMapper(IdHistoryMapper.class);
|
configuration.addMapper(IdHistoryMapper.class);
|
||||||
|
configuration.addMapper(EditHistoryMapper.class);
|
||||||
}).join()
|
}).join()
|
||||||
.runQuery(sqlSession -> {
|
.runQuery(sqlSession -> {
|
||||||
createAllPunishmentsView(sqlSession);
|
createAllPunishmentsView(sqlSession);
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,27 @@
|
||||||
|
<h2 mat-dialog-title>Edit {{ data.punishment.type }} #{{ data.punishment.id }}</h2>
|
||||||
|
<div mat-dialog-content>
|
||||||
|
<div class="dialog-content">
|
||||||
|
<mat-form-field appearance="fill">
|
||||||
|
<mat-label>Reason</mat-label>
|
||||||
|
<input matInput type="text" [(ngModel)]="reason" placeholder="Enter reason"/>
|
||||||
|
</mat-form-field>
|
||||||
|
|
||||||
|
<mat-checkbox [(ngModel)]="isPermanent">Permanent</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>
|
||||||
|
}
|
||||||
|
|
||||||
|
@if (errorMessage()) {
|
||||||
|
<div class="error">{{ errorMessage() }}</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div mat-dialog-actions align="end">
|
||||||
|
<button mat-button color="warn" (click)="onRemove()" [disabled]="isBusy()">Remove</button>
|
||||||
|
<button mat-raised-button color="primary" (click)="onUpdate()" [disabled]="isBusy()">Update</button>
|
||||||
|
<button mat-button (click)="onCancel()" [disabled]="isBusy()">Cancel</button>
|
||||||
|
</div>
|
||||||
|
|
@ -0,0 +1,10 @@
|
||||||
|
.dialog-content {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 12px;
|
||||||
|
margin-top: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error {
|
||||||
|
color: #d32f2f; /* Material error color */
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,150 @@
|
||||||
|
import {Component, Inject, inject, signal} from '@angular/core';
|
||||||
|
import {FormsModule} from '@angular/forms';
|
||||||
|
import {MatButtonModule} from '@angular/material/button';
|
||||||
|
import {MatFormFieldModule} from '@angular/material/form-field';
|
||||||
|
import {MatInput, MatLabel} from '@angular/material/input';
|
||||||
|
import {MatCheckboxModule} from '@angular/material/checkbox';
|
||||||
|
import {
|
||||||
|
MAT_DIALOG_DATA,
|
||||||
|
MatDialogActions,
|
||||||
|
MatDialogContent,
|
||||||
|
MatDialogRef,
|
||||||
|
MatDialogTitle
|
||||||
|
} from '@angular/material/dialog';
|
||||||
|
import {HistoryService, PunishmentHistory} from '@api';
|
||||||
|
import {firstValueFrom} from 'rxjs';
|
||||||
|
|
||||||
|
interface EditPunishmentData {
|
||||||
|
punishment: PunishmentHistory;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-edit-punishment-dialog',
|
||||||
|
standalone: true,
|
||||||
|
imports: [
|
||||||
|
FormsModule,
|
||||||
|
MatButtonModule,
|
||||||
|
MatFormFieldModule,
|
||||||
|
MatInput,
|
||||||
|
MatLabel,
|
||||||
|
MatCheckboxModule,
|
||||||
|
MatDialogTitle,
|
||||||
|
MatDialogContent,
|
||||||
|
MatDialogActions,
|
||||||
|
],
|
||||||
|
templateUrl: './edit-punishment-dialog.component.html',
|
||||||
|
styleUrl: './edit-punishment-dialog.component.scss'
|
||||||
|
})
|
||||||
|
export class EditPunishmentDialogComponent {
|
||||||
|
// Form model
|
||||||
|
reason: string = '';
|
||||||
|
isPermanent: boolean = false;
|
||||||
|
expiresAtLocal: string = '';
|
||||||
|
|
||||||
|
// UI state
|
||||||
|
isBusy = signal<boolean>(false);
|
||||||
|
errorMessage = signal<string | null>(null);
|
||||||
|
|
||||||
|
private historyApi = inject(HistoryService);
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
public dialogRef: MatDialogRef<EditPunishmentDialogComponent, PunishmentHistory | { removed: true } | null>,
|
||||||
|
@Inject(MAT_DIALOG_DATA) public data: EditPunishmentData
|
||||||
|
) {
|
||||||
|
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}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onCancel(): void {
|
||||||
|
if (this.isBusy()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.dialogRef.close(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
private computeUntilMs(): number {
|
||||||
|
if (this.isPermanent) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
if (!this.expiresAtLocal) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
const ms = new Date(this.expiresAtLocal).getTime();
|
||||||
|
return isNaN(ms) ? -1 : ms;
|
||||||
|
}
|
||||||
|
|
||||||
|
onUpdate(): void {
|
||||||
|
const punishment = this.data.punishment;
|
||||||
|
if (!window.confirm('Are you sure you want to update this punishment?')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.isBusy.set(true);
|
||||||
|
this.errorMessage.set(null);
|
||||||
|
|
||||||
|
const updates: Array<Promise<PunishmentHistory>> = [] as any;
|
||||||
|
|
||||||
|
// Update reason if changed
|
||||||
|
if ((this.reason ?? '') !== (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)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (updates.length === 0) {
|
||||||
|
this.isBusy.set(false);
|
||||||
|
this.dialogRef.close(null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Promise.all(updates)
|
||||||
|
.then(results => {
|
||||||
|
const updated = results[results.length - 1];
|
||||||
|
this.isBusy.set(false);
|
||||||
|
this.dialogRef.close(updated);
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
console.error(err);
|
||||||
|
this.errorMessage.set('Failed to update punishment');
|
||||||
|
this.isBusy.set(false);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
onRemove(): void {
|
||||||
|
const punishment = this.data.punishment;
|
||||||
|
if (!window.confirm('Are you sure you want to remove this punishment?')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.isBusy.set(true);
|
||||||
|
this.errorMessage.set(null);
|
||||||
|
|
||||||
|
this.historyApi.removePunishment(punishment.type as any, punishment.id).subscribe({
|
||||||
|
next: () => {
|
||||||
|
this.isBusy.set(false);
|
||||||
|
this.dialogRef.close({removed: true});
|
||||||
|
},
|
||||||
|
error: (err) => {
|
||||||
|
console.error(err);
|
||||||
|
this.errorMessage.set('Failed to remove punishment');
|
||||||
|
this.isBusy.set(false);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -6,14 +6,17 @@
|
||||||
<table [cellSpacing]="0">
|
<table [cellSpacing]="0">
|
||||||
<div class="historyTableHead">
|
<div class="historyTableHead">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th class="historyType">Type</th>
|
<th class="historyType">Type</th>
|
||||||
<th class="historyPlayer">Player</th>
|
<th class="historyPlayer">Player</th>
|
||||||
<th class="historyPlayer">Banned By</th>
|
<th class="historyPlayer">Banned By</th>
|
||||||
<th class="historyReason">Reason</th>
|
<th class="historyReason">Reason</th>
|
||||||
<th class="historyDate">Date</th>
|
<th class="historyDate">Date</th>
|
||||||
<th class="historyDate">Expires</th>
|
<th class="historyDate">Expires</th>
|
||||||
</tr>
|
@if (canEdit()) {
|
||||||
|
<th class="historyActions">Actions</th>
|
||||||
|
}
|
||||||
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
|
|
@ -26,30 +29,36 @@
|
||||||
<td class="historyPlayer" (click)="setSearch(entry.username, 'player')">
|
<td class="historyPlayer" (click)="setSearch(entry.username, 'player')">
|
||||||
<div class="playerContainer">
|
<div class="playerContainer">
|
||||||
<img class="avatar" [ngSrc]="this.historyFormat.getAvatarUrl(entry.uuid)" width="25" height="25"
|
<img class="avatar" [ngSrc]="this.historyFormat.getAvatarUrl(entry.uuid)" width="25" height="25"
|
||||||
alt="{{entry.username}}'s Minecraft skin">
|
alt="{{entry.username}}'s Minecraft skin">
|
||||||
<span class="username">{{ entry.username }}</span>
|
<span class="username">{{ entry.username }}</span>
|
||||||
</div>
|
</div>
|
||||||
|
</td>
|
||||||
|
<td class="historyPlayer" (click)="setSearch(entry.punishedBy, 'staff')">
|
||||||
|
<div class="playerContainer">
|
||||||
|
<img class="avatar" [ngSrc]="this.historyFormat.getAvatarUrl(entry.punishedByUuid)" width="25"
|
||||||
|
height="25"
|
||||||
|
alt="{{entry.punishedBy}}'s Minecraft skin">
|
||||||
|
<span>{{ entry.punishedBy }}</span>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td class="historyReason" (click)="showDetailedPunishment(entry)">
|
||||||
|
{{ entry.reason | removeTrailingPeriod }}
|
||||||
|
</td>
|
||||||
|
<td class="historyDate" (click)="showDetailedPunishment(entry)">
|
||||||
|
{{ this.historyFormat.getPunishmentTime(entry) }}
|
||||||
|
</td>
|
||||||
|
<td class="historyDate" (click)="showDetailedPunishment(entry)">
|
||||||
|
{{ this.historyFormat.getExpiredTime(entry) }}
|
||||||
|
</td>
|
||||||
|
@if (canEdit()) {
|
||||||
|
<td class="historyActions">
|
||||||
|
<button (click)="$event.stopPropagation(); openEdit(entry)">Edit</button>
|
||||||
</td>
|
</td>
|
||||||
<td class="historyPlayer" (click)="setSearch(entry.punishedBy, 'staff')">
|
|
||||||
<div class="playerContainer">
|
|
||||||
<img class="avatar" [ngSrc]="this.historyFormat.getAvatarUrl(entry.punishedByUuid)" width="25" height="25"
|
|
||||||
alt="{{entry.punishedBy}}'s Minecraft skin">
|
|
||||||
<span>{{ entry.punishedBy }}</span>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
<td class="historyReason" (click)="showDetailedPunishment(entry)">
|
|
||||||
{{ entry.reason | removeTrailingPeriod }}
|
|
||||||
</td>
|
|
||||||
<td class="historyDate" (click)="showDetailedPunishment(entry)">
|
|
||||||
{{ this.historyFormat.getPunishmentTime(entry) }}
|
|
||||||
</td>
|
|
||||||
<td class="historyDate" (click)="showDetailedPunishment(entry)">
|
|
||||||
{{ this.historyFormat.getExpiredTime(entry) }}
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
}
|
}
|
||||||
</tbody>
|
</tr>
|
||||||
</div>
|
}
|
||||||
</table>
|
</tbody>
|
||||||
}
|
</div>
|
||||||
|
</table>
|
||||||
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,9 @@ import {HttpErrorResponse} from '@angular/common/http';
|
||||||
import {HistoryFormatService} from '../history-format.service';
|
import {HistoryFormatService} from '../history-format.service';
|
||||||
import {SearchParams} from '../search-terms';
|
import {SearchParams} from '../search-terms';
|
||||||
import {Router} from '@angular/router';
|
import {Router} from '@angular/router';
|
||||||
|
import {AuthService} from '@services/auth.service';
|
||||||
|
import {MatDialog} from '@angular/material/dialog';
|
||||||
|
import {EditPunishmentDialogComponent} from '../edit-punishment-dialog/edit-punishment-dialog.component';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-history',
|
selector: 'app-history',
|
||||||
|
|
@ -35,7 +38,11 @@ export class HistoryComponent implements OnInit, OnChanges {
|
||||||
|
|
||||||
public history: PunishmentHistory[] = []
|
public history: PunishmentHistory[] = []
|
||||||
|
|
||||||
constructor(private historyApi: HistoryService, public historyFormat: HistoryFormatService, private router: Router) {
|
constructor(private historyApi: HistoryService,
|
||||||
|
public historyFormat: HistoryFormatService,
|
||||||
|
private router: Router,
|
||||||
|
private authService: AuthService,
|
||||||
|
private dialog: MatDialog) {
|
||||||
}
|
}
|
||||||
|
|
||||||
ngOnChanges(): void {
|
ngOnChanges(): void {
|
||||||
|
|
@ -101,4 +108,20 @@ export class HistoryComponent implements OnInit, OnChanges {
|
||||||
public showDetailedPunishment(entry: PunishmentHistory) {
|
public showDetailedPunishment(entry: PunishmentHistory) {
|
||||||
this.router.navigate([`bans/${entry.type}/${entry.id}`]).then();
|
this.router.navigate([`bans/${entry.type}/${entry.id}`]).then();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public canEdit(): boolean {
|
||||||
|
return this.authService.hasAccess(['SCOPE_head_mod']);
|
||||||
|
}
|
||||||
|
|
||||||
|
public openEdit(punishment: PunishmentHistory) {
|
||||||
|
if (!this.canEdit()) return;
|
||||||
|
const ref = this.dialog.open(EditPunishmentDialogComponent, {
|
||||||
|
data: {punishment}
|
||||||
|
});
|
||||||
|
ref.afterClosed().subscribe(result => {
|
||||||
|
if (result) {
|
||||||
|
this.reloadHistory();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -49,6 +49,12 @@ paths:
|
||||||
$ref: './schemas/bans/bans.yml#/getAllHistoryForUUID'
|
$ref: './schemas/bans/bans.yml#/getAllHistoryForUUID'
|
||||||
/api/history/total:
|
/api/history/total:
|
||||||
$ref: './schemas/bans/bans.yml#/getTotalPunishments'
|
$ref: './schemas/bans/bans.yml#/getTotalPunishments'
|
||||||
|
/api/history/admin/{type}/{id}/reason:
|
||||||
|
$ref: './schemas/bans/bans.yml#/updatePunishmentReason'
|
||||||
|
/api/history/admin/{type}/{id}/until:
|
||||||
|
$ref: './schemas/bans/bans.yml#/updatePunishmentUntil'
|
||||||
|
/api/history/admin/{type}/{id}:
|
||||||
|
$ref: './schemas/bans/bans.yml#/removePunishment'
|
||||||
/api/appeal/update-mail:
|
/api/appeal/update-mail:
|
||||||
$ref: './schemas/forms/appeal/appeal.yml#/UpdateMail'
|
$ref: './schemas/forms/appeal/appeal.yml#/UpdateMail'
|
||||||
/api/appeal/minecraft-appeal:
|
/api/appeal/minecraft-appeal:
|
||||||
|
|
|
||||||
|
|
@ -211,6 +211,73 @@ getAllHistoryForUUID:
|
||||||
application/json:
|
application/json:
|
||||||
schema:
|
schema:
|
||||||
$ref: '#/components/schemas/PunishmentHistoryList'
|
$ref: '#/components/schemas/PunishmentHistoryList'
|
||||||
|
updatePunishmentReason:
|
||||||
|
patch:
|
||||||
|
tags:
|
||||||
|
- history
|
||||||
|
summary: Update punishment reason
|
||||||
|
description: Updates the reason for a specific punishment history entry
|
||||||
|
operationId: updatePunishmentReason
|
||||||
|
parameters:
|
||||||
|
- $ref: '#/components/parameters/HistoryType'
|
||||||
|
- $ref: '#/components/parameters/Id'
|
||||||
|
- $ref: '#/components/parameters/NewPunishmentReason'
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: Updated punishment
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/PunishmentHistory'
|
||||||
|
default:
|
||||||
|
description: Unexpected error
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '../generic/errors.yml#/components/schemas/ApiError'
|
||||||
|
updatePunishmentUntil:
|
||||||
|
patch:
|
||||||
|
tags:
|
||||||
|
- history
|
||||||
|
summary: Update punishment expiry time
|
||||||
|
description: Updates the expiry time (until) for a specific punishment history entry (only for ban and mute)
|
||||||
|
operationId: updatePunishmentUntil
|
||||||
|
parameters:
|
||||||
|
- $ref: '#/components/parameters/HistoryType'
|
||||||
|
- $ref: '#/components/parameters/Id'
|
||||||
|
- $ref: '#/components/parameters/NewUntil'
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: Updated punishment
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/PunishmentHistory'
|
||||||
|
default:
|
||||||
|
description: Unexpected error
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '../generic/errors.yml#/components/schemas/ApiError'
|
||||||
|
removePunishment:
|
||||||
|
delete:
|
||||||
|
tags:
|
||||||
|
- history
|
||||||
|
summary: Remove punishment
|
||||||
|
description: Removes a punishment from history. For bans and mutes, the punishment is deactivated; for kicks and warnings, the row is deleted.
|
||||||
|
operationId: removePunishment
|
||||||
|
parameters:
|
||||||
|
- $ref: '#/components/parameters/HistoryType'
|
||||||
|
- $ref: '#/components/parameters/Id'
|
||||||
|
responses:
|
||||||
|
'204':
|
||||||
|
description: Punishment removed
|
||||||
|
default:
|
||||||
|
description: Unexpected error
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '../generic/errors.yml#/components/schemas/ApiError'
|
||||||
components:
|
components:
|
||||||
parameters:
|
parameters:
|
||||||
HistoryType:
|
HistoryType:
|
||||||
|
|
@ -250,6 +317,21 @@ components:
|
||||||
schema:
|
schema:
|
||||||
type: integer
|
type: integer
|
||||||
description: The id of the punishment that should be retrieved
|
description: The id of the punishment that should be retrieved
|
||||||
|
NewPunishmentReason:
|
||||||
|
name: reason
|
||||||
|
in: query
|
||||||
|
required: true
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
description: The new reason to set for the punishment
|
||||||
|
NewUntil:
|
||||||
|
name: until
|
||||||
|
in: query
|
||||||
|
required: true
|
||||||
|
schema:
|
||||||
|
type: integer
|
||||||
|
format: int64
|
||||||
|
description: The new expiry time (epoch millis)
|
||||||
schemas:
|
schemas:
|
||||||
SearchResults:
|
SearchResults:
|
||||||
type: integer
|
type: integer
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user