Refactor history APIs and integrate LiteBans database support

Redesigned history-related APIs to streamline handling of user and UUID punishments, moving from POST to GET endpoints. Added support for LiteBans database with mappers for retrieving punishment records by name and UUID, and implemented global exception handling for better error reporting. Updated schema paths and added enums (UserType, HistoryType) and a new Gradle dependency.
This commit is contained in:
Teriuihi 2025-04-11 01:12:46 +02:00
parent 43a5dafd82
commit 0f9761da3a
12 changed files with 475 additions and 65 deletions

View File

@ -34,6 +34,7 @@ dependencies {
implementation("org.mybatis:mybatis:3.5.13")
testRuntimeOnly("org.junit.platform:junit-platform-launcher")
implementation("org.springframework.boot:spring-boot-configuration-processor")
implementation("org.springframework.boot:spring-boot-starter-hateoas")
}
tasks.withType<Test> {

View File

@ -0,0 +1,19 @@
package com.alttd.altitudeweb.controllers;
import com.alttd.altitudeweb.model.ApiErrorDto;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler;
@ControllerAdvice
public class GlobalExceptionHandler extends ResponseEntityExceptionHandler {
@ExceptionHandler(Exception.class)
public ResponseEntity<ApiErrorDto> handleException(Exception ex) {
ApiErrorDto error = new ApiErrorDto();
error.reason("Error: " + ex.getMessage());
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(error);
}
}

View File

@ -1,21 +1,106 @@
package com.alttd.altitudeweb.controllers.history;
import com.alttd.altitudeweb.api.HistoryApi;
import com.alttd.altitudeweb.database.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;
import java.util.List;
import java.util.UUID;
import java.util.concurrent.CompletableFuture;
@Slf4j
@RestController
public class HistoryApiController implements HistoryApi {
@Override
public ResponseEntity<Void> getHistoryForUsers(String userType, String type, String user) throws Exception {
return null;
public ResponseEntity<PunishmentHistoryDto> getHistoryForUsers(String userType, String type, String user) throws Exception {
UserType userTypeEnum = UserType.getUserType(userType);
HistoryType historyTypeEnum = HistoryType.getHistoryType(type);
PunishmentHistoryDto punishmentHistory = new PunishmentHistoryDto();
CompletableFuture<List<HistoryRecord>> historyRecords = new CompletableFuture<>();
Connection.getConnection(Databases.LITE_BANS, configuration -> configuration.addMapper(NameHistoryMapper.class))
.thenApply(connection -> {
connection.runQuery(sqlSession -> {
log.debug("Loading user names for type {}", type);
List<HistoryRecord> temp = sqlSession.getMapper(NameHistoryMapper.class)
.getRecent(historyTypeEnum, userTypeEnum, user);
historyRecords.complete(temp);
});
return connection;
});
historyRecords.join().forEach(historyRecord -> {
PunishmentHistoryInnerDto innerDto = new PunishmentHistoryInnerDto()
.uuid(historyRecord.getUuid())
.username(historyRecord.getPunishedName())
.reason(historyRecord.getReason())
.punishmentUserUuid(historyRecord.getBannedByUuid())
.punishmentUser(historyRecord.getBannedByName())
.removedBy(historyRecord.getRemovedByName())
.punishmentTime(historyRecord.getTime())
.expiryTime(historyRecord.getUntil())
.removedReason(historyRecord.getRemovedByReason())
.type(historyRecord.getType());
punishmentHistory.add(innerDto);
});
return ResponseEntity.ok().body(punishmentHistory);
}
@Override
public ResponseEntity<Void> getHistoryForUuid(String userType, String type, String uuid) throws Exception {
return null;
public ResponseEntity<PunishmentHistoryDto> getHistoryForUuid(String userType, String type, String uuid) throws Exception {
UserType userTypeEnum = UserType.getUserType(userType);
HistoryType historyTypeEnum = HistoryType.getHistoryType(type);
PunishmentHistoryDto punishmentHistory = new PunishmentHistoryDto();
CompletableFuture<List<HistoryRecord>> historyRecords = new CompletableFuture<>();
Connection.getConnection(Databases.LITE_BANS, configuration -> configuration.addMapper(UUIDHistoryMapper.class))
.thenApply(connection -> {
connection.runQuery(sqlSession -> {
log.debug("Loading user names for type {}", type);
List<HistoryRecord> temp = sqlSession.getMapper(UUIDHistoryMapper.class)
.getRecent(historyTypeEnum, userTypeEnum, UUID.fromString(uuid));
historyRecords.complete(temp);
});
return connection;
});
historyRecords.join().forEach(historyRecord -> {
PunishmentHistoryInnerDto innerDto = new PunishmentHistoryInnerDto()
.uuid(historyRecord.getUuid())
.username(historyRecord.getPunishedName())
.reason(historyRecord.getReason())
.punishmentUserUuid(historyRecord.getBannedByUuid())
.punishmentUser(historyRecord.getBannedByName())
.removedBy(historyRecord.getRemovedByName())
.punishmentTime(historyRecord.getTime())
.expiryTime(historyRecord.getUntil())
.removedReason(historyRecord.getRemovedByReason())
.type(historyRecord.getType());
punishmentHistory.add(innerDto);
});
return ResponseEntity.ok().body(punishmentHistory);
}
@Override
public ResponseEntity<Void> getUserNames(String userType, String type) throws Exception {
return null;
public ResponseEntity<List<String>> getUserNames(String userType, String type) {
UserType userTypeEnum = UserType.getUserType(userType);
HistoryType historyTypeEnum = HistoryType.getHistoryType(type);
CompletableFuture<List<String>> playerGroupFuture = new CompletableFuture<>();
Connection.getConnection(Databases.LITE_BANS, configuration -> configuration.addMapper(RecentNamesMapper.class))
.thenApply(connection -> {
connection.runQuery(sqlSession -> {
log.debug("Loading user names for type {}", type);
List<String> temp = sqlSession.getMapper(RecentNamesMapper.class)
.getRecent(historyTypeEnum, userTypeEnum);
playerGroupFuture.complete(temp);
});
return connection;
});
return ResponseEntity.ok().body(playerGroupFuture.join());
}
}

View File

@ -5,7 +5,8 @@ import lombok.Getter;
@Getter
public enum Databases {
DEFAULT("web_db"),
LUCK_PERMS("luckperms");
LUCK_PERMS("luckperms"),
LITE_BANS("litebans");
private final String internalName;

View File

@ -0,0 +1,17 @@
package com.alttd.altitudeweb.database.litebans;
import lombok.Data;
@Data
public class HistoryRecord {
private String uuid;
private String punishedName;
private String reason;
private String bannedByUuid;
private String bannedByName;
private String removedByName;
private Long time;
private Long until;
private String removedByReason;
private String type;
}

View File

@ -0,0 +1,13 @@
package com.alttd.altitudeweb.database.litebans;
public enum HistoryType {
ALL,
BANS,
MUTES,
KICKS,
WARNS;
public static HistoryType getHistoryType(String historyType) {
return HistoryType.valueOf(historyType.toUpperCase());
}
}

View File

@ -0,0 +1,94 @@
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;
import java.util.ArrayList;
import java.util.List;
public interface NameHistoryMapper {
@Results({
@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.uuid, user_table.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 (
SELECT history_1.uuid, history_1.name
FROM litebans.litebans_history history_1
INNER JOIN (
SELECT uuid, MAX(id) as max_id
FROM litebans.litebans_history
GROUP BY uuid
) history_2 ON history_1.uuid = history_2.uuid AND history_1.id = history_2.max_id
) AS user_table ON punishment.uuid = user_table.uuid
WHERE LOWER(punishment.${name_column}) LIKE #{partialName}
""")
List<HistoryRecord> getRecentHistory(@Param("tableName") String tableName, @Param("partialName") String partialName,
@Param("name_column") String nameColumn);
default List<HistoryRecord> getRecent(HistoryType historyType, UserType userType, String partialName) {
return switch (historyType) {
case ALL -> getRecentAll(userType, partialName);
case BANS -> getRecentBans(userType, partialName);
case MUTES -> getRecentMutes(userType, partialName);
case KICKS -> getRecentKicks(userType, partialName);
case WARNS -> getRecentWarns(userType, partialName);
};
}
private List<HistoryRecord> getRecent(String tableName, UserType userType, String partialName) {
switch (userType) {
case PLAYER -> {
return getRecentHistory(tableName, partialName.toLowerCase() + "%", "name");
}
case STAFF -> {
return getRecentHistory(tableName, partialName.toLowerCase() + "%", "banned_by_name");
}
default -> throw new IllegalArgumentException("Invalid user type");
}
}
private List<HistoryRecord> getRecentBans(UserType userType, String partialName) {
return addType(getRecent("litebans_bans", userType, partialName), "ban");
}
private List<HistoryRecord> getRecentKicks(UserType userType, String partialName) {
return addType(getRecent("litebans_kicks", userType, partialName), "kick");
}
private List<HistoryRecord> getRecentMutes(UserType userType, String partialName) {
return addType(getRecent("litebans_mutes", userType, partialName), "mute");
}
private List<HistoryRecord> getRecentWarns(UserType userType, String partialName) {
return addType(getRecent("litebans_warnings", userType, partialName), "warn");
}
private List<HistoryRecord> getRecentAll(UserType userType, String partialName) {
List<HistoryRecord> all = new ArrayList<>();
all.addAll(getRecentBans(userType, partialName));
// Removed recent kicks as we do not show those
// all.addAll(getRecentKicks(userType, partialName));
all.addAll(getRecentMutes(userType, partialName));
all.addAll(getRecentWarns(userType, partialName));
return all;
}
private List<HistoryRecord> addType(List<HistoryRecord> historyRecords, String type) {
historyRecords.forEach(historyRecord -> historyRecord.setType(type));
return historyRecords;
}
}

View File

@ -0,0 +1,74 @@
package com.alttd.altitudeweb.database.litebans;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;
import java.util.ArrayList;
import java.util.List;
public interface RecentNamesMapper {
@Select("""
SELECT DISTINCT user_table.name AS punished_name
FROM ${tableName} AS punishment
INNER JOIN (
SELECT history_1.uuid, history_1.name
FROM litebans.litebans_history history_1
INNER JOIN (
SELECT uuid, MAX(id) as max_id
FROM litebans.litebans_history
GROUP BY uuid
) history_2 ON history_1.uuid = history_2.uuid AND history_1.id = history_2.max_id
) AS user_table ON punishment.${uuid_column} = user_table.uuid
WHERE punishment.time > UNIX_TIMESTAMP(DATE_SUB(NOW(), INTERVAL 1 YEAR)) * 1000
""")
List<String> getRecentHistory(@Param("tableName") String tableName, @Param("uuid_column") String uuidColumn);
default List<String> getRecent(HistoryType historyType, UserType userType) {
return switch (historyType) {
case ALL -> getRecentAll(userType);
case BANS -> getRecentBans(userType);
case MUTES -> getRecentMutes(userType);
case KICKS -> getRecentKicks(userType);
case WARNS -> getRecentWarns(userType);
};
}
private List<String> getRecent(String tableName, UserType userType) {
switch (userType) {
case PLAYER -> {
return getRecentHistory(tableName, "uuid");
}
case STAFF -> {
return getRecentHistory(tableName, "banned_by_uuid");
}
default -> throw new IllegalArgumentException("Invalid user type");
}
}
private List<String> getRecentBans(UserType userType) {
return getRecent("litebans_bans", userType);
}
private List<String> getRecentKicks(UserType userType) {
return getRecent("litebans_kicks", userType);
}
private List<String> getRecentMutes(UserType userType) {
return getRecent("litebans_mutes", userType);
}
private List<String> getRecentWarns(UserType userType) {
return getRecent("litebans_warnings", userType);
}
private List<String> getRecentAll(UserType userType) {
List<String> all = new ArrayList<>();
all.addAll(getRecentBans(userType));
// Removed recent kicks as we do not show those
// all.addAll(getRecentKicks(userType));
all.addAll(getRecentMutes(userType));
all.addAll(getRecentWarns(userType));
return all;
}
}

View File

@ -0,0 +1,96 @@
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 java.util.ArrayList;
import java.util.List;
import java.util.UUID;
public interface UUIDHistoryMapper {
@Results({
@Result(property = "uuid", column = "uuid", javaType = UUID.class, typeHandler = UUIDTypeHandler.class),
@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 = "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.uuid, user_table.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 (
SELECT history_1.uuid, history_1.name
FROM litebans.litebans_history history_1
INNER JOIN (
SELECT uuid, MAX(id) as max_id
FROM litebans.litebans_history
GROUP BY uuid
) history_2 ON history_1.uuid = history_2.uuid AND history_1.id = history_2.max_id
) AS user_table ON punishment.uuid = user_table.uuid
WHERE punishment.uuid = #{uuid}
""")
List<HistoryRecord> getRecentHistory(@Param("tableName") String tableName, @Param("uuid") String uuid,
@Param("uuid_column") String uuidColumn);
default List<HistoryRecord> getRecent(HistoryType historyType, UserType userType, UUID uuid) {
return switch (historyType) {
case ALL -> getRecentAll(userType, uuid);
case BANS -> getRecentBans(userType, uuid);
case MUTES -> getRecentMutes(userType, uuid);
case KICKS -> getRecentKicks(userType, uuid);
case WARNS -> getRecentKicks(userType, uuid);
};
}
private List<HistoryRecord> getRecent(String tableName, UserType userType, UUID uuid) {
switch (userType) {
case PLAYER -> {
return getRecentHistory(tableName, uuid.toString(), "uuid");
}
case STAFF -> {
return getRecentHistory(tableName, uuid.toString(), "banned_by_uuid");
}
default -> throw new IllegalArgumentException("Invalid user type");
}
}
private List<HistoryRecord> getRecentBans(UserType userType, UUID uuid) {
return addType(getRecent("litebans_bans", userType, uuid), "ban");
}
private List<HistoryRecord> getRecentKicks(UserType userType, UUID uuid) {
return addType(getRecent("litebans_kicks", userType, uuid), "kick");
}
private List<HistoryRecord> getRecentMutes(UserType userType, UUID uuid) {
return addType(getRecent("litebans_mutes", userType, uuid), "mute");
}
private List<HistoryRecord> getRecentWarns(UserType userType, UUID uuid) {
return addType(getRecent("litebans_warnings", userType, uuid), "warn");
}
private List<HistoryRecord> getRecentAll(UserType userType, UUID uuid) {
List<HistoryRecord> all = new ArrayList<>();
all.addAll(getRecentBans(userType, uuid));
// Removed recent kicks as we do not show those
// all.addAll(getRecentKicks(userType, uuid));
all.addAll(getRecentMutes(userType, uuid));
all.addAll(getRecentWarns(userType, uuid));
return all;
}
private List<HistoryRecord> addType(List<HistoryRecord> historyRecords, String type) {
historyRecords.forEach(historyRecord -> historyRecord.setType(type));
return historyRecords;
}
}

View File

@ -0,0 +1,10 @@
package com.alttd.altitudeweb.database.litebans;
public enum UserType {
PLAYER,
STAFF;
public static UserType getUserType(String userType) {
return UserType.valueOf(userType.toUpperCase());
}
}

View File

@ -16,7 +16,7 @@ paths:
$ref: './schemas/team/team.yml#/getTeam'
/history/{userType}/search/{type}:
$ref: './schemas/bans/bans.yml#/getUserNames'
/history/{userType}/{type}/{user}:
/history/{userType}/name/{type}/{user}:
$ref: './schemas/bans/bans.yml#/getHistoryForUsers'
/history/{userType}/{type}/{uuid}:
/history/{userType}/uuid/{type}/{uuid}:
$ref: './schemas/bans/bans.yml#/getHistoryForUuid'

View File

@ -1,31 +1,31 @@
getUserNames:
post:
get:
tags:
- history
summary: Get user names
description: Get all user names with punishment history of the specified type
operationId: getUserNames
parameters:
- $ref: '#/components/parameters/UserType'
- $ref: '#/components/parameters/HistoryType'
responses:
'200':
description: Successful operation
content:
application/json:
schema:
type: array
items:
type: string
description: A list of users
default:
description: Unexpected error
content:
application/json:
schema:
$ref: "../generic/errors.yml#/components/schemas/ApiError"
parameters:
- $ref: '#/components/parameters/UserType'
- $ref: '#/components/parameters/HistoryType'
responses:
'200':
description: Successful operation
content:
application/json:
schema:
type: array
items:
type: string
description: A list of users
default:
description: Unexpected error
content:
application/json:
schema:
$ref: "../generic/errors.yml#/components/schemas/ApiError"
getHistoryForUsers:
post:
get:
tags:
- history
summary: Get history for users
@ -33,25 +33,25 @@ getHistoryForUsers:
Get all users with punishment history of the specified type,
where the user name starts with the search query
operationId: getHistoryForUsers
parameters:
- $ref: '#/components/parameters/UserType'
- $ref: '#/components/parameters/HistoryType'
- $ref: '#/components/parameters/User'
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"
parameters:
- $ref: '#/components/parameters/UserType'
- $ref: '#/components/parameters/HistoryType'
- $ref: '#/components/parameters/User'
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"
getHistoryForUuid:
post:
get:
tags:
- history
summary: Get user
@ -59,23 +59,23 @@ getHistoryForUuid:
Get all users with punishment history of the specified type,
where the user uuid matches
operationId: getHistoryForUuid
parameters:
- $ref: '#/components/parameters/UserType'
- $ref: '#/components/parameters/HistoryType'
- $ref: '#/components/parameters/Uuid'
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"
parameters:
- $ref: '#/components/parameters/UserType'
- $ref: '#/components/parameters/HistoryType'
- $ref: '#/components/parameters/Uuid'
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: