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 6e35c60..517a84d 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 @@ -3,11 +3,11 @@ package com.alttd.altitudeweb.controllers.history; import com.alttd.altitudeweb.api.HistoryApi; import com.alttd.altitudeweb.controllers.limits.RateLimit; import com.alttd.altitudeweb.model.HistoryCountDto; +import com.alttd.altitudeweb.model.PunishmentHistoryListDto; 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 com.alttd.altitudeweb.model.PunishmentHistoryInnerDto; import lombok.extern.slf4j.Slf4j; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.RestController; @@ -23,15 +23,15 @@ import java.util.concurrent.TimeUnit; public class HistoryApiController implements HistoryApi { @Override - public ResponseEntity getHistoryForAll(String userType, String type, Integer page) { + public ResponseEntity getHistoryForAll(String userType, String type, Integer page) { return getHistoryForUsers(userType, type, "", page); } @Override - public ResponseEntity getHistoryForUsers(String userType, String type, String user, Integer page) { + public ResponseEntity getHistoryForUsers(String userType, String type, String user, Integer page) { UserType userTypeEnum = UserType.getUserType(userType); HistoryType historyTypeEnum = HistoryType.getHistoryType(type); - PunishmentHistoryDto punishmentHistory = new PunishmentHistoryDto(); + PunishmentHistoryListDto punishmentHistoryList = new PunishmentHistoryListDto(); CompletableFuture> historyRecords = new CompletableFuture<>(); Connection.getConnection(Databases.LITE_BANS) @@ -46,14 +46,14 @@ public class HistoryApiController implements HistoryApi { historyRecords.completeExceptionally(e); } }); - return mapPunishmentHistory(punishmentHistory, historyRecords); + return mapPunishmentHistory(punishmentHistoryList, historyRecords); } @Override - public ResponseEntity getHistoryForUuid(String userType, String type, String uuid, Integer page) { + public ResponseEntity getHistoryForUuid(String userType, String type, String uuid, Integer page) { UserType userTypeEnum = UserType.getUserType(userType); HistoryType historyTypeEnum = HistoryType.getHistoryType(type); - PunishmentHistoryDto punishmentHistory = new PunishmentHistoryDto(); + PunishmentHistoryListDto punishmentHistoryList = new PunishmentHistoryListDto(); CompletableFuture> historyRecords = new CompletableFuture<>(); Connection.getConnection(Databases.LITE_BANS) @@ -68,7 +68,7 @@ public class HistoryApiController implements HistoryApi { historyRecords.completeExceptionally(e); } }); - return mapPunishmentHistory(punishmentHistory, historyRecords); + return mapPunishmentHistory(punishmentHistoryList, historyRecords); } @Override @@ -153,6 +153,26 @@ public class HistoryApiController implements HistoryApi { return ResponseEntity.ok().body(searchResultCountCompletableFuture.join()); } + @Override + public ResponseEntity getHistoryById(String type, Integer id) { + HistoryType historyTypeEnum = HistoryType.getHistoryType(type); + CompletableFuture historyRecordCompletableFuture = new CompletableFuture<>(); + + Connection.getConnection(Databases.LITE_BANS) + .runQuery(sqlSession -> { + log.debug("Loading history by id"); + try { + HistoryRecord punishment = sqlSession.getMapper(IdHistoryMapper.class) + .getRecentHistory(historyTypeEnum, id); + historyRecordCompletableFuture.complete(punishment); + } catch (Exception e) { + log.error("Failed to load history count", e); + historyRecordCompletableFuture.completeExceptionally(e); + } + }); + return ResponseEntity.ok().body(mapPunishmentHistory(historyRecordCompletableFuture.join())); + } + private ResponseEntity mapHistoryCount(HistoryCount historyCount) { HistoryCountDto historyCountDto = new HistoryCountDto(); historyCountDto.setBans(historyCount.getBans()); @@ -162,28 +182,33 @@ public class HistoryApiController implements HistoryApi { return ResponseEntity.ok().body(historyCountDto); } - private ResponseEntity mapPunishmentHistory(PunishmentHistoryDto punishmentHistory, CompletableFuture> historyRecords) { + private ResponseEntity mapPunishmentHistory(PunishmentHistoryListDto punishmentHistoryList, CompletableFuture> historyRecords) { historyRecords.join().forEach(historyRecord -> { - PunishmentHistoryInnerDto.TypeEnum type = switch (historyRecord.getType().toLowerCase()) { - case "ban" -> PunishmentHistoryInnerDto.TypeEnum.BAN; - case "mute" -> PunishmentHistoryInnerDto.TypeEnum.MUTE; - case "warn" -> PunishmentHistoryInnerDto.TypeEnum.WARN; - case "kick" -> PunishmentHistoryInnerDto.TypeEnum.KICK; - default -> throw new IllegalStateException("Unexpected value: " + historyRecord.getType()); - }; - PunishmentHistoryInnerDto innerDto = new PunishmentHistoryInnerDto() - .uuid(historyRecord.getUuid()) - .username(historyRecord.getPunishedName()) - .reason(historyRecord.getReason()) - .punishedByUuid(historyRecord.getBannedByUuid()) - .punishedBy(historyRecord.getBannedByName()) - .removedBy(historyRecord.getRemovedByName()) - .punishmentTime(historyRecord.getTime()) - .expiryTime(historyRecord.getUntil()) - .removedReason(historyRecord.getRemovedByReason()) - .type(type); - punishmentHistory.add(innerDto); + PunishmentHistoryDto innerDto = mapPunishmentHistory(historyRecord); + punishmentHistoryList.add(innerDto); }); - return ResponseEntity.ok().body(punishmentHistory); + return ResponseEntity.ok().body(punishmentHistoryList); + } + + private static PunishmentHistoryDto mapPunishmentHistory(HistoryRecord historyRecord) { + PunishmentHistoryDto.TypeEnum type = switch (historyRecord.getType().toLowerCase()) { + case "ban" -> PunishmentHistoryDto.TypeEnum.BAN; + case "mute" -> PunishmentHistoryDto.TypeEnum.MUTE; + case "warn" -> PunishmentHistoryDto.TypeEnum.WARN; + case "kick" -> PunishmentHistoryDto.TypeEnum.KICK; + default -> throw new IllegalStateException("Unexpected value: " + historyRecord.getType()); + }; + return new PunishmentHistoryDto() + .uuid(historyRecord.getUuid()) + .username(historyRecord.getPunishedName()) + .reason(historyRecord.getReason()) + .punishedByUuid(historyRecord.getBannedByUuid()) + .punishedBy(historyRecord.getBannedByName()) + .removedBy(historyRecord.getRemovedByName()) + .punishmentTime(historyRecord.getTime()) + .expiryTime(historyRecord.getUntil()) + .removedReason(historyRecord.getRemovedByReason()) + .type(type) + .id(historyRecord.getId()); } } diff --git a/database/build.gradle.kts b/database/build.gradle.kts index 34cf887..31b9fae 100644 --- a/database/build.gradle.kts +++ b/database/build.gradle.kts @@ -16,6 +16,7 @@ dependencies { implementation("org.mybatis:mybatis:3.5.13") compileOnly("org.slf4j:slf4j-api:2.0.17") compileOnly("org.slf4j:slf4j-simple:2.0.17") + compileOnly("org.jetbrains:annotations:26.0.2") } tasks.test { diff --git a/database/src/main/java/com/alttd/altitudeweb/database/litebans/HistoryRecord.java b/database/src/main/java/com/alttd/altitudeweb/database/litebans/HistoryRecord.java index 52f5b34..a1b1236 100644 --- a/database/src/main/java/com/alttd/altitudeweb/database/litebans/HistoryRecord.java +++ b/database/src/main/java/com/alttd/altitudeweb/database/litebans/HistoryRecord.java @@ -5,6 +5,7 @@ import lombok.Data; @Data public class HistoryRecord { private String uuid; + private int id; private String punishedName; private String reason; private String bannedByUuid; diff --git a/database/src/main/java/com/alttd/altitudeweb/database/litebans/IdHistoryMapper.java b/database/src/main/java/com/alttd/altitudeweb/database/litebans/IdHistoryMapper.java new file mode 100644 index 0000000..0d0945c --- /dev/null +++ b/database/src/main/java/com/alttd/altitudeweb/database/litebans/IdHistoryMapper.java @@ -0,0 +1,54 @@ +package com.alttd.altitudeweb.database.litebans; + +import org.apache.ibatis.annotations.Param; +import org.apache.ibatis.annotations.Result; +import org.apache.ibatis.annotations.Results; +import org.apache.ibatis.annotations.Select; + +public interface IdHistoryMapper { + @Results({ + @Result(property = "id", column = "id"), + @Result(property = "uuid", column = "uuid"), + @Result(property = "punishedName", column = "punished_name"), + @Result(property = "reason", column = "reason"), + @Result(property = "bannedByUuid", column = "banned_by_uuid"), + @Result(property = "bannedByName", column = "banned_by_name"), + @Result(property = "removedByName", column = "removed_by_name"), + @Result(property = "time", column = "time"), + @Result(property = "until", column = "until"), + @Result(property = "removedByReason", column = "removed_by_reason") + }) + @Select(""" + SELECT punishment.id, + punishment.uuid, + user_lookup.name AS punished_name, + punishment.reason, + punishment.banned_by_uuid, + punishment.banned_by_name, + punishment.removed_by_name, + punishment.time, + punishment.until, + punishment.removed_by_reason + FROM (SELECT * + FROM ${table_name} AS punishment + WHERE punishment.id = #{id}) + AS punishment + INNER JOIN user_lookup + ON user_lookup.uuid = punishment.uuid + """) + HistoryRecord getHistoryForId(@Param("table_name") String tableName, + @Param("type") String type, + @Param("id") int id); + + default HistoryRecord getRecentHistory(HistoryType historyType, int id) { + HistoryRecord historyRecord = switch (historyType) { + case ALL -> throw new IllegalArgumentException("HistoryType.ALL is not supported"); + case BAN -> getHistoryForId("litebans_bans", "ban", id); + case MUTE -> getHistoryForId("litebans_mutes", "mute", id); + case KICK -> getHistoryForId("litebans_kicks", "kick", id); + case WARN -> getHistoryForId("litebans_warnings", "warn", id); + }; + historyRecord.setType(historyType.name().toLowerCase()); + return historyRecord; + } +} diff --git a/database/src/main/java/com/alttd/altitudeweb/database/litebans/NameHistoryMapper.java b/database/src/main/java/com/alttd/altitudeweb/database/litebans/NameHistoryMapper.java index ef2197f..22ecd39 100644 --- a/database/src/main/java/com/alttd/altitudeweb/database/litebans/NameHistoryMapper.java +++ b/database/src/main/java/com/alttd/altitudeweb/database/litebans/NameHistoryMapper.java @@ -4,6 +4,7 @@ import org.apache.ibatis.annotations.Param; import org.apache.ibatis.annotations.Result; import org.apache.ibatis.annotations.Results; import org.apache.ibatis.annotations.Select; +import org.jetbrains.annotations.NotNull; import java.util.List; @@ -24,6 +25,7 @@ public interface NameHistoryMapper { * order */ @Results({ + @Result(property = "id", column = "id"), @Result(property = "uuid", column = "uuid"), @Result(property = "punishedName", column = "punished_name"), @Result(property = "reason", column = "reason"), @@ -36,7 +38,8 @@ public interface NameHistoryMapper { @Result(property = "type", column = "type") }) @Select(""" - SELECT all_punishments.uuid, + SELECT id, + all_punishments.uuid, user_lookup.name AS punished_name, reason, banned_by_uuid, @@ -62,7 +65,6 @@ public interface NameHistoryMapper { * Retrieves a list of all types of recent punishment history records sorted * in descending time order. This result does NOT contain kicks history * - * @param nameColumn the column name in the database indicating the name to use for filtering * @param limit the maximum number of records to fetch * @param offset the starting offset position of the result set * @@ -70,6 +72,7 @@ public interface NameHistoryMapper { * order */ @Results({ + @Result(property = "id", column = "id"), @Result(property = "uuid", column = "uuid"), @Result(property = "punishedName", column = "punished_name"), @Result(property = "reason", column = "reason"), @@ -82,17 +85,19 @@ public interface NameHistoryMapper { @Result(property = "type", column = "type") }) @Select(""" - SELECT punishments.uuid, + SELECT punishment.id, + punishment.uuid, user_lookup.name AS punished_name, - punishments.reason, - punishments.banned_by_uuid, - punishments.banned_by_name, - punishments.removed_by_name, - punishments.time, - punishments.until, - punishments.removed_by_reason, - punishments.type + punishment.reason, + punishment.banned_by_uuid, + punishment.banned_by_name, + punishment.removed_by_name, + punishment.time, + punishment.until, + punishment.removed_by_reason, + punishment.type FROM (SELECT uuid, + id, reason, banned_by_uuid, banned_by_name, @@ -103,9 +108,9 @@ public interface NameHistoryMapper { type FROM all_punishments ORDER BY time DESC - LIMIT #{limit} OFFSET #{offset}) AS punishments + LIMIT #{limit} OFFSET #{offset}) AS punishment INNER JOIN user_lookup - ON user_lookup.uuid = punishments.uuid; + ON user_lookup.uuid = punishment.uuid; """) List getRecentAllHistory(@Param("limit") int limit, @Param("offset") int offset); @@ -124,6 +129,7 @@ public interface NameHistoryMapper { * order */ @Results({ + @Result(property = "id", column = "id"), @Result(property = "uuid", column = "uuid"), @Result(property = "punishedName", column = "punished_name"), @Result(property = "reason", column = "reason"), @@ -135,7 +141,8 @@ public interface NameHistoryMapper { @Result(property = "removedByReason", column = "removed_by_reason") }) @Select(""" - SELECT punishment.uuid, + SELECT punishment.id, + punishment.uuid, user_lookup.name AS punished_name, punishment.reason, punishment.banned_by_uuid, @@ -162,8 +169,6 @@ public interface NameHistoryMapper { * parameters. The records are sorted in descending order by time and limited to a specified range. * * @param tableName the name of the database table to query the history records from - * @param partialName a partial or complete name of the user to filter the records, case-insensitive - * @param nameColumn the column name in the database indicating the name to use for filtering * @param limit the maximum number of records to fetch * @param offset the starting offset position of the result set * @@ -171,6 +176,7 @@ public interface NameHistoryMapper { * order */ @Results({ + @Result(property = "id", column = "id"), @Result(property = "uuid", column = "uuid"), @Result(property = "punishedName", column = "punished_name"), @Result(property = "reason", column = "reason"), @@ -182,7 +188,8 @@ public interface NameHistoryMapper { @Result(property = "removedByReason", column = "removed_by_reason") }) @Select(""" - SELECT punishment.uuid, + SELECT punishment.id, + punishment.uuid, user_lookup.name AS punished_name, punishment.reason, punishment.banned_by_uuid, @@ -192,7 +199,8 @@ public interface NameHistoryMapper { punishment.until, punishment.removed_by_reason FROM ( - SELECT uuid, + SELECT id, + uuid, reason, banned_by_uuid, banned_by_name, @@ -254,23 +262,23 @@ public interface NameHistoryMapper { } } - private List getRecentBans(UserType userType, String partialName, int page) { + private @NotNull List getRecentBans(UserType userType, String partialName, int page) { return addType(getRecent("litebans_bans", userType, partialName, page), "ban"); } - private List getRecentKicks(UserType userType, String partialName, int page) { + private @NotNull List getRecentKicks(UserType userType, String partialName, int page) { return addType(getRecent("litebans_kicks", userType, partialName, page), "kick"); } - private List getRecentMutes(UserType userType, String partialName, int page) { + private @NotNull List getRecentMutes(UserType userType, String partialName, int page) { return addType(getRecent("litebans_mutes", userType, partialName, page), "mute"); } - private List getRecentWarns(UserType userType, String partialName, int page) { + private @NotNull List getRecentWarns(UserType userType, String partialName, int page) { return addType(getRecent("litebans_warnings", userType, partialName, page), "warn"); } - private List addType(List historyRecords, String type) { + private @NotNull List addType(@NotNull List historyRecords, String type) { historyRecords.forEach(historyRecord -> historyRecord.setType(type)); return historyRecords; } diff --git a/database/src/main/java/com/alttd/altitudeweb/database/litebans/UUIDHistoryMapper.java b/database/src/main/java/com/alttd/altitudeweb/database/litebans/UUIDHistoryMapper.java index 4442125..80e8a16 100644 --- a/database/src/main/java/com/alttd/altitudeweb/database/litebans/UUIDHistoryMapper.java +++ b/database/src/main/java/com/alttd/altitudeweb/database/litebans/UUIDHistoryMapper.java @@ -1,10 +1,11 @@ package com.alttd.altitudeweb.database.litebans; -import com.alttd.altitudeweb.type_handler.UUIDTypeHandler; import org.apache.ibatis.annotations.Param; import org.apache.ibatis.annotations.Result; import org.apache.ibatis.annotations.Results; import org.apache.ibatis.annotations.Select; +import org.jetbrains.annotations.Contract; +import org.jetbrains.annotations.NotNull; import java.util.List; import java.util.UUID; @@ -14,10 +15,11 @@ public interface UUIDHistoryMapper { int PAGE_SIZE = 10; @Results({ - @Result(property = "uuid", column = "uuid", javaType = UUID.class, typeHandler = UUIDTypeHandler.class), + @Result(property = "id", column = "id"), + @Result(property = "uuid", column = "uuid"), @Result(property = "punishedName", column = "punished_name"), @Result(property = "reason", column = "reason"), - @Result(property = "bannedByUuid", column = "banned_by_uuid", javaType = UUID.class, typeHandler = UUIDTypeHandler.class), + @Result(property = "bannedByUuid", column = "banned_by_uuid"), @Result(property = "bannedByName", column = "banned_by_name"), @Result(property = "removedByName", column = "removed_by_name"), @Result(property = "time", column = "time"), @@ -26,7 +28,7 @@ public interface UUIDHistoryMapper { @Result(property = "type", column = "type") }) @Select(""" - SELECT all_punishments.uuid, user_lookup.name AS punished_name, reason, banned_by_uuid, banned_by_name, + SELECT id, all_punishments.uuid, user_lookup.name AS punished_name, reason, banned_by_uuid, banned_by_name, removed_by_name, time, until, removed_by_reason, type FROM all_punishments INNER JOIN user_lookup @@ -41,10 +43,11 @@ public interface UUIDHistoryMapper { @Param("offset") int offset); @Results({ - @Result(property = "uuid", column = "uuid", javaType = UUID.class, typeHandler = UUIDTypeHandler.class), + @Result(property = "id", column = "id"), + @Result(property = "uuid", column = "uuid"), @Result(property = "punishedName", column = "punished_name"), @Result(property = "reason", column = "reason"), - @Result(property = "bannedByUuid", column = "banned_by_uuid", javaType = UUID.class, typeHandler = UUIDTypeHandler.class), + @Result(property = "bannedByUuid", column = "banned_by_uuid"), @Result(property = "bannedByName", column = "banned_by_name"), @Result(property = "removedByName", column = "removed_by_name"), @Result(property = "time", column = "time"), @@ -52,7 +55,7 @@ public interface UUIDHistoryMapper { @Result(property = "removedByReason", column = "removed_by_reason") }) @Select(""" - SELECT punishment.uuid, user_lookup.name AS punished_name, reason, banned_by_uuid, banned_by_name, + SELECT punishment.id, punishment.uuid, user_lookup.name AS punished_name, reason, banned_by_uuid, banned_by_name, removed_by_name, time, until, removed_by_reason FROM ${tableName} AS punishment INNER JOIN user_lookup ON user_lookup.uuid = punishment.uuid @@ -66,7 +69,8 @@ public interface UUIDHistoryMapper { @Param("limit") int limit, @Param("offset") int offset); - default List getRecent(HistoryType historyType, UserType userType, UUID uuid, int page) { + default List getRecent(@NotNull HistoryType historyType, @NotNull UserType userType, + @NotNull UUID uuid, int page) { return switch (historyType) { case ALL -> getRecentAll(userType, uuid, page); case BAN -> getRecentBans(userType, uuid, page); @@ -76,7 +80,8 @@ public interface UUIDHistoryMapper { }; } - private List getRecent(String tableName, UserType userType, UUID uuid, int page) { + private List getRecent(@NotNull String tableName, @NotNull UserType userType, + @NotNull UUID uuid, int page) { int offset = page * PAGE_SIZE; int limit = PAGE_SIZE; return switch (userType) { @@ -85,7 +90,7 @@ public interface UUIDHistoryMapper { }; } - private List getRecentAll(UserType userType, UUID uuid, int page) { + private List getRecentAll(@NotNull UserType userType, @NotNull UUID uuid, int page) { int offset = page * PAGE_SIZE; int limit = PAGE_SIZE; return switch (userType) { @@ -96,23 +101,24 @@ public interface UUIDHistoryMapper { }; } - private List getRecentBans(UserType userType, UUID uuid, int page) { + private @NotNull List getRecentBans(@NotNull UserType userType, @NotNull UUID uuid, int page) { return addType(getRecent("litebans_bans", userType, uuid, page), "ban"); } - private List getRecentKicks(UserType userType, UUID uuid, int page) { + private @NotNull List getRecentKicks(@NotNull UserType userType, @NotNull UUID uuid, int page) { return addType(getRecent("litebans_kicks", userType, uuid, page), "kick"); } - private List getRecentMutes(UserType userType, UUID uuid, int page) { + private @NotNull List getRecentMutes(@NotNull UserType userType, @NotNull UUID uuid, int page) { return addType(getRecent("litebans_mutes", userType, uuid, page), "mute"); } - private List getRecentWarns(UserType userType, UUID uuid, int page) { + private @NotNull List getRecentWarns(@NotNull UserType userType, @NotNull UUID uuid, int page) { return addType(getRecent("litebans_warnings", userType, uuid, page), "warn"); } - private List addType(List historyRecords, String type) { + @Contract("_, _ -> param1") + private @NotNull List addType(@NotNull List historyRecords, @NotNull String type) { historyRecords.forEach(historyRecord -> historyRecord.setType(type)); return historyRecords; } diff --git a/database/src/main/java/com/alttd/altitudeweb/setup/InitializeLiteBans.java b/database/src/main/java/com/alttd/altitudeweb/setup/InitializeLiteBans.java index 0dc06b3..50fd3c5 100644 --- a/database/src/main/java/com/alttd/altitudeweb/setup/InitializeLiteBans.java +++ b/database/src/main/java/com/alttd/altitudeweb/setup/InitializeLiteBans.java @@ -18,6 +18,7 @@ public class InitializeLiteBans { configuration.addMapper(NameHistoryMapper.class); configuration.addMapper(UUIDHistoryMapper.class); configuration.addMapper(HistoryCountMapper.class); + configuration.addMapper(IdHistoryMapper.class); }).join() .runQuery(sqlSession -> { createAllPunishmentsView(sqlSession); @@ -29,15 +30,15 @@ public class InitializeLiteBans { private static void createAllPunishmentsView(SqlSession sqlSession) { String query = """ CREATE VIEW IF NOT EXISTS all_punishments AS - SELECT uuid, reason, banned_by_uuid, banned_by_name, removed_by_name, time, until, removed_by_reason, + SELECT id, uuid, reason, banned_by_uuid, banned_by_name, removed_by_name, time, until, removed_by_reason, 'ban' as type FROM litebans_bans UNION ALL - SELECT uuid, reason, banned_by_uuid, banned_by_name, removed_by_name, time, until, removed_by_reason, + SELECT id, uuid, reason, banned_by_uuid, banned_by_name, removed_by_name, time, until, removed_by_reason, 'mute' as type FROM litebans_mutes UNION ALL - SELECT uuid, reason, banned_by_uuid, banned_by_name, removed_by_name, time, until, removed_by_reason, + SELECT id, uuid, reason, banned_by_uuid, banned_by_name, removed_by_name, time, until, removed_by_reason, 'warn' as type FROM litebans_warnings ORDER BY time DESC; diff --git a/frontend/src/app/app.routes.ts b/frontend/src/app/app.routes.ts index ab24da7..c7c3f7c 100644 --- a/frontend/src/app/app.routes.ts +++ b/frontend/src/app/app.routes.ts @@ -45,6 +45,10 @@ export const routes: Routes = [ path: 'bans', loadComponent: () => import('./bans/bans.component').then(m => m.BansComponent) }, + { + path: 'bans/:type/:id', + loadComponent: () => import('./bans/details/details.component').then(m => m.DetailsComponent) + }, { path: 'economy', loadComponent: () => import('./economy/economy.component').then(m => m.EconomyComponent) diff --git a/frontend/src/app/bans/bans.component.html b/frontend/src/app/bans/bans.component.html index 3cceb89..687b754 100644 --- a/frontend/src/app/bans/bans.component.html +++ b/frontend/src/app/bans/bans.component.html @@ -57,7 +57,8 @@
+ [page]="page" [searchTerm]="finalSearchTerm" (pageChange)="updatePageSize($event)" + (selectItem)="setSearch($event)">
diff --git a/frontend/src/app/bans/bans.component.ts b/frontend/src/app/bans/bans.component.ts index 38a0872..58ab210 100644 --- a/frontend/src/app/bans/bans.component.ts +++ b/frontend/src/app/bans/bans.component.ts @@ -5,6 +5,7 @@ import {HistoryCount, HistoryService} from '../../api'; import {NgClass, NgForOf, NgIf} from '@angular/common'; import {FormsModule} from '@angular/forms'; import {catchError, map, Observable} from 'rxjs'; +import {SearchParams} from './search-terms'; @Component({ selector: 'app-bans', @@ -210,4 +211,11 @@ export class BansComponent implements OnInit { this.actualPage = this.page; } } + + public setSearch(searchParams: SearchParams) { + this.userType = searchParams.userType + this.searchTerm = searchParams.searchTerm + this.punishmentType = searchParams.punishmentType + this.search(); + } } diff --git a/frontend/src/app/bans/details/details.component.html b/frontend/src/app/bans/details/details.component.html new file mode 100644 index 0000000..0111720 --- /dev/null +++ b/frontend/src/app/bans/details/details.component.html @@ -0,0 +1,62 @@ + + > +
+

Minecraft Punishments

+
+
+ + +

Loading...

+
+ + + +
+

type: {{ this.historyFormat.getType(punishment) }}

+

is active: {{ this.historyFormat.isActive(punishment) }}

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Player +
+ {{punishment.username}}'s Minecraft skin + {{ punishment.username }} +
+
Moderator +
+ {{punishment.punishedBy}}'s Minecraft skin + {{ punishment.punishedBy }} +
+
Reason{{ punishment.reason | removeTrailingPeriod }}
Date{{ this.historyFormat.getPunishmentTime(punishment) }}
Expires{{ this.historyFormat.getExpiredTime(punishment) }}
Un{{ this.historyFormat.getType(punishment).toLocaleLowerCase() }} reason{{ punishment.removedReason == null ? 'No reason specified' : punishment.removedReason }}
+
+
diff --git a/frontend/src/app/bans/details/details.component.scss b/frontend/src/app/bans/details/details.component.scss new file mode 100644 index 0000000..e69de29 diff --git a/frontend/src/app/bans/details/details.component.spec.ts b/frontend/src/app/bans/details/details.component.spec.ts new file mode 100644 index 0000000..82e1a05 --- /dev/null +++ b/frontend/src/app/bans/details/details.component.spec.ts @@ -0,0 +1,23 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { DetailsComponent } from './details.component'; + +describe('DetailsComponent', () => { + let component: DetailsComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [DetailsComponent] + }) + .compileComponents(); + + fixture = TestBed.createComponent(DetailsComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/frontend/src/app/bans/details/details.component.ts b/frontend/src/app/bans/details/details.component.ts new file mode 100644 index 0000000..d67e35a --- /dev/null +++ b/frontend/src/app/bans/details/details.component.ts @@ -0,0 +1,69 @@ +import {Component, OnInit} from '@angular/core'; +import {HistoryService, PunishmentHistory} from '../../../api'; +import {NgIf, NgOptimizedImage} from '@angular/common'; +import {RemoveTrailingPeriodPipe} from '../../util/RemoveTrailingPeriodPipe'; +import {HistoryFormatService} from '../history-format.service'; +import {ActivatedRoute} from '@angular/router'; +import {catchError, map} from 'rxjs'; +import {HeaderComponent} from '../../header/header.component'; + +@Component({ + selector: 'app-details', + imports: [ + NgIf, + NgOptimizedImage, + RemoveTrailingPeriodPipe, + HeaderComponent + ], + templateUrl: './details.component.html', + styleUrl: './details.component.scss' +}) +export class DetailsComponent implements OnInit { + type: 'all' | 'ban' | 'mute' | 'kick' | 'warn' = 'all'; + id: number = -1; + punishment: PunishmentHistory | undefined; + + constructor(private route: ActivatedRoute, + private historyApi: HistoryService, + public historyFormat: HistoryFormatService) { + } + + ngOnInit(): void { + this.route.paramMap.subscribe(params => { + switch (params.get('type')) { + case 'ban': + this.type = 'ban'; + break; + case 'mute': + this.type = 'mute'; + break; + case 'kick': + this.type = 'kick'; + break; + case 'warn': + this.type = 'warn'; + break; + default: + throw new Error("Invalid type"); + } + this.id = Number(params.get('id') || '-1'); + this.loadPunishmentData(); + }); + } + + private loadPunishmentData() { + if (!this.type || this.type === 'all' || this.id < 0 || isNaN(this.id)) { + console.error("Invalid type or id"); + return; + } + this.historyApi.getHistoryById(this.type, this.id).pipe( + map(punishment => { + this.punishment = punishment; + }), + catchError(err => { + console.error(err); + return []; + }) + ).subscribe(); + } +} diff --git a/frontend/src/app/bans/history-format.service.spec.ts b/frontend/src/app/bans/history-format.service.spec.ts new file mode 100644 index 0000000..2e6ff12 --- /dev/null +++ b/frontend/src/app/bans/history-format.service.spec.ts @@ -0,0 +1,16 @@ +import { TestBed } from '@angular/core/testing'; + +import { HistoryFormatService } from './history-format.service'; + +describe('HistoryFormatService', () => { + let service: HistoryFormatService; + + beforeEach(() => { + TestBed.configureTestingModule({}); + service = TestBed.inject(HistoryFormatService); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); +}); diff --git a/frontend/src/app/bans/history-format.service.ts b/frontend/src/app/bans/history-format.service.ts new file mode 100644 index 0000000..75956a7 --- /dev/null +++ b/frontend/src/app/bans/history-format.service.ts @@ -0,0 +1,58 @@ +import {Injectable} from '@angular/core'; +import {PunishmentHistory} from '../../api'; + +@Injectable({ + providedIn: 'root' +}) +export class HistoryFormatService { + + constructor() { + } + + public getPunishmentTime(entry: PunishmentHistory) { + const date = new Date(entry.punishmentTime); + return date.toLocaleString(navigator.language, { + year: 'numeric', + month: 'short', + day: 'numeric', + hour: '2-digit', + minute: '2-digit', + hour12: false + }); + } + + public isActive(entry: PunishmentHistory): boolean { + return entry.expiryTime > Date.now(); + } + + public getType(entry: PunishmentHistory): string { + return entry.type.charAt(0).toUpperCase() + entry.type.slice(1); + } + + public getExpiredTime(entry: PunishmentHistory): string { + let suffix: string = ''; + if (entry.removedBy !== null && entry.removedBy !== undefined) { + suffix = '(' + entry.removedBy + ')'; + } + if (entry.expiryTime <= 0) { + return "Permanent " + this.getType(entry) + " " + suffix; + } + const date = new Date(entry.expiryTime); + return date.toLocaleString(navigator.language, { + year: 'numeric', + month: 'short', + day: 'numeric', + hour: '2-digit', + minute: '2-digit', + hour12: false + }) + " " + suffix; + } + + public getAvatarUrl(entry: string): string { + let uuid = entry.replace('-', ''); + if (uuid === 'C') { + uuid = "f78a4d8dd51b4b3998a3230f2de0c670" + } + return `https://crafatar.com/avatars/${uuid}?size=25&overlay`; + } +} diff --git a/frontend/src/app/bans/history/history.component.html b/frontend/src/app/bans/history/history.component.html index d53bfa4..ececb08 100644 --- a/frontend/src/app/bans/history/history.component.html +++ b/frontend/src/app/bans/history/history.component.html @@ -19,24 +19,32 @@
- {{ getType(entry) }} - + + {{ this.historyFormat.getType(entry) }} + +
- {{entry.username}}'s Minecraft skin {{ entry.username }}
- +
- {{entry.punishedBy}}'s Minecraft skin {{ entry.punishedBy }}
- {{ entry.reason | removeTrailingPeriod }} - {{ getPunishmentTime(entry) }} - {{ getExpiredTime(entry) }} + + {{ entry.reason | removeTrailingPeriod }} + + + {{ this.historyFormat.getPunishmentTime(entry) }} + + + {{ this.historyFormat.getExpiredTime(entry) }} +
diff --git a/frontend/src/app/bans/history/history.component.scss b/frontend/src/app/bans/history/history.component.scss index 3438cff..6181041 100644 --- a/frontend/src/app/bans/history/history.component.scss +++ b/frontend/src/app/bans/history/history.component.scss @@ -62,6 +62,7 @@ table tr td { .historyPlayerRow:hover { background-color: var(--history-table-row-color); + cursor: pointer; } .historyTableHead { diff --git a/frontend/src/app/bans/history/history.component.ts b/frontend/src/app/bans/history/history.component.ts index 0c01803..3104c0f 100644 --- a/frontend/src/app/bans/history/history.component.ts +++ b/frontend/src/app/bans/history/history.component.ts @@ -1,11 +1,14 @@ import {Component, EventEmitter, Input, OnChanges, OnInit, Output} from '@angular/core'; -import {BASE_PATH, HistoryService, PunishmentHistoryInner} from '../../../api'; +import {BASE_PATH, HistoryService, PunishmentHistory} from '../../../api'; import {catchError, map, Observable, shareReplay} from 'rxjs'; import {NgForOf, NgIf, NgOptimizedImage} from '@angular/common'; import {CookieService} from 'ngx-cookie-service'; import {RemoveTrailingPeriodPipe} from '../../util/RemoveTrailingPeriodPipe'; import {HttpErrorResponse} from '@angular/common/http'; import {environment} from '../../../environments/environment'; +import {HistoryFormatService} from '../history-format.service'; +import {SearchParams} from '../search-terms'; +import {Router} from '@angular/router'; @Component({ selector: 'app-history', @@ -30,12 +33,13 @@ export class HistoryComponent implements OnInit, OnChanges { @Input() searchTerm: string = ''; @Output() pageChange = new EventEmitter(); + @Output() selectItem = new EventEmitter(); private uuidRegex = /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/; - public history: PunishmentHistoryInner[] = [] + public history: PunishmentHistory[] = [] - constructor(private historyApi: HistoryService) { + constructor(private historyApi: HistoryService, public historyFormat: HistoryFormatService, private router: Router) { } ngOnChanges(): void { @@ -47,7 +51,7 @@ export class HistoryComponent implements OnInit, OnChanges { } private reloadHistory(): void { - let historyObservable: Observable; + let historyObservable: Observable; if (this.searchTerm.length === 0) { historyObservable = this.historyApi.getHistoryForAll(this.userType, this.punishmentType, this.page); } else { @@ -89,42 +93,31 @@ export class HistoryComponent implements OnInit, OnChanges { ).subscribe(); } - public getPunishmentTime(entry: PunishmentHistoryInner) { - const date = new Date(entry.punishmentTime); - return date.toLocaleString(navigator.language, { - year: 'numeric', - month: 'short', - day: 'numeric', - hour: '2-digit', - minute: '2-digit', - hour12: false - }); - } - - public getType(entry: PunishmentHistoryInner) { - return entry.type.charAt(0).toUpperCase() + entry.type.slice(1); - } - - public getExpiredTime(entry: PunishmentHistoryInner) { - if (entry.expiryTime <= 0) { - return "Permanent " + this.getType(entry); + setSearch(type: PunishmentHistory.TypeEnum, name: string, userType: 'player' | 'staff') { + let punishmentType: 'all' | 'ban' | 'mute' | 'kick' | 'warn' = 'all'; + switch (type) { + case 'ban': + punishmentType = 'ban'; + break; + case 'mute': + punishmentType = 'mute'; + break; + case 'kick': + punishmentType = 'kick'; + break; + case 'warn': + punishmentType = 'warn'; + break; } - const date = new Date(entry.expiryTime); - return date.toLocaleString(navigator.language, { - year: 'numeric', - month: 'short', - day: 'numeric', - hour: '2-digit', - minute: '2-digit', - hour12: false - }); + let searchParams: SearchParams = { + userType: userType, + punishmentType: punishmentType, + searchTerm: name + } + this.selectItem.emit(searchParams); } - public getAvatarUrl(entry: string): string { - let uuid = entry.replace('-', ''); - if (uuid === 'C') { - uuid = "f78a4d8dd51b4b3998a3230f2de0c670" - } - return `https://crafatar.com/avatars/${uuid}?size=25&overlay`; + public showDetailedPunishment(entry: PunishmentHistory) { + this.router.navigate([`bans/${entry.type}/${entry.id}`]).then(); } } diff --git a/frontend/src/app/bans/search-terms.ts b/frontend/src/app/bans/search-terms.ts new file mode 100644 index 0000000..1ca4a01 --- /dev/null +++ b/frontend/src/app/bans/search-terms.ts @@ -0,0 +1,5 @@ +export interface SearchParams { + userType: 'player' | 'staff'; + searchTerm: string; + punishmentType: 'all' | 'ban' | 'mute' | 'kick' | 'warn'; +} diff --git a/open_api/src/main/resources/api.yml b/open_api/src/main/resources/api.yml index b73aad3..7102696 100644 --- a/open_api/src/main/resources/api.yml +++ b/open_api/src/main/resources/api.yml @@ -26,6 +26,8 @@ paths: $ref: './schemas/bans/bans.yml#/getTotalResultsForUuidSearch' /history/{userType}/search-results/user/{type}/{user}: $ref: './schemas/bans/bans.yml#/getTotalResultsForUserSearch' + /history/single/{type}/{id}: + $ref: './schemas/bans/bans.yml#/getHistoryById' /history/total: $ref: './schemas/bans/bans.yml#/getTotalPunishments' /appeal/update-mail: diff --git a/open_api/src/main/resources/schemas/bans/bans.yml b/open_api/src/main/resources/schemas/bans/bans.yml index df67f19..4215805 100644 --- a/open_api/src/main/resources/schemas/bans/bans.yml +++ b/open_api/src/main/resources/schemas/bans/bans.yml @@ -44,7 +44,7 @@ getHistoryForUsers: content: application/json: schema: - $ref: '#/components/schemas/PunishmentHistory' + $ref: '#/components/schemas/PunishmentHistoryList' default: description: Unexpected error content: @@ -70,7 +70,7 @@ getHistoryForAll: content: application/json: schema: - $ref: '#/components/schemas/PunishmentHistory' + $ref: '#/components/schemas/PunishmentHistoryList' default: description: Unexpected error content: @@ -97,7 +97,7 @@ getHistoryForUuid: content: application/json: schema: - $ref: '#/components/schemas/PunishmentHistory' + $ref: '#/components/schemas/PunishmentHistoryList' default: description: Unexpected error content: @@ -172,6 +172,29 @@ getTotalResultsForUuidSearch: application/json: schema: $ref: '../generic/errors.yml#/components/schemas/ApiError' +getHistoryById: + get: + tags: + - history + summary: Gets history for specified id + description: Retrieves a specific history record + operationId: getHistoryById + parameters: + - $ref: '#/components/parameters/HistoryType' + - $ref: '#/components/parameters/Id' + responses: + '200': + description: Successful operation + content: + application/json: + schema: + $ref: '#/components/schemas/PunishmentHistory' + default: + description: Unexpected error + content: + application/json: + schema: + $ref: '../generic/errors.yml#/components/schemas/ApiError' components: parameters: HistoryType: @@ -211,57 +234,70 @@ components: schema: type: integer description: The page that should be retrieved + Id: + name: id + in: path + required: true + schema: + type: integer + description: The id of the punishment that should be retrieved schemas: SearchResults: type: integer description: A number representing the total count of results for the search query - PunishmentHistory: + PunishmentHistoryList: type: array items: - type: object - required: - - username - - uuid - - reason - - type - - punishmentTime - - expiryTime - - punishedBy - - punishedByUuid - properties: - username: - type: string - description: The username of the user - uuid: - type: string - description: The UUID of the user - reason: - type: string - description: The reason for the punishment - type: - type: string - description: The type of punishment - enum: [ ban, mute, kick, warn ] - punishmentTime: - type: integer - format: int64 - description: The time when the punishment was given - expiryTime: - type: integer - format: int64 - description: The time when the punishment expires - punishedBy: - type: string - description: The username of the punishment issuer - punishedByUuid: - type: string - description: The UUID of the punishment issuer - removedBy: - type: string - description: The name of the staff member who removed the punishment - removedReason: - type: string - description: The reason why the punishment was removed + $ref: '#/components/schemas/PunishmentHistory' + PunishmentHistory: + type: object + required: + - username + - uuid + - reason + - type + - punishmentTime + - expiryTime + - punishedBy + - punishedByUuid + - id + properties: + username: + type: string + description: The username of the user + uuid: + type: string + description: The UUID of the user + reason: + type: string + description: The reason for the punishment + type: + type: string + description: The type of punishment + enum: [ ban, mute, kick, warn ] + punishmentTime: + type: integer + format: int64 + description: The time when the punishment was given + expiryTime: + type: integer + format: int64 + description: The time when the punishment expires + punishedBy: + type: string + description: The username of the punishment issuer + punishedByUuid: + type: string + description: The UUID of the punishment issuer + removedBy: + type: string + description: The name of the staff member who removed the punishment + removedReason: + type: string + description: The reason why the punishment was removed + id: + type: integer + description: Id of the punishment Player: type: object properties: