Add Minecraft appeal functionality with database integration, UUID handling, and API response adjustments.
This commit is contained in:
parent
101794d8f2
commit
770a2e0d14
|
|
@ -0,0 +1,33 @@
|
|||
package com.alttd.altitudeweb.controllers.data_from_auth;
|
||||
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.core.context.SecurityContextHolder;
|
||||
import org.springframework.security.oauth2.jwt.Jwt;
|
||||
import org.springframework.web.server.ResponseStatusException;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
public class AuthenticatedUuid {
|
||||
/**
|
||||
* Extracts and validates the authenticated user's UUID from the JWT token.
|
||||
*
|
||||
* @return The UUID of the authenticated user
|
||||
* @throws ResponseStatusException with 401 status if authentication is invalid
|
||||
*/
|
||||
public static UUID getAuthenticatedUserUuid() {
|
||||
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
|
||||
|
||||
if (authentication == null || !(authentication.getPrincipal() instanceof Jwt jwt)) {
|
||||
throw new ResponseStatusException(HttpStatus.UNAUTHORIZED, "Authentication required");
|
||||
}
|
||||
|
||||
String stringUuid = jwt.getSubject();
|
||||
|
||||
try {
|
||||
return UUID.fromString(stringUuid);
|
||||
} catch (IllegalArgumentException e) {
|
||||
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Invalid UUID format");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,32 +1,65 @@
|
|||
package com.alttd.altitudeweb.controllers.forms;
|
||||
|
||||
import com.alttd.altitudeweb.api.AppealsApi;
|
||||
import com.alttd.altitudeweb.services.limits.RateLimit;
|
||||
import com.alttd.altitudeweb.database.Databases;
|
||||
import com.alttd.altitudeweb.database.web_db.forms.Appeal;
|
||||
import com.alttd.altitudeweb.database.web_db.forms.AppealMapper;
|
||||
import com.alttd.altitudeweb.mappers.AppealDataMapper;
|
||||
import com.alttd.altitudeweb.model.AppealResponseDto;
|
||||
import com.alttd.altitudeweb.model.DiscordAppealDto;
|
||||
import com.alttd.altitudeweb.model.MinecraftAppealDto;
|
||||
import com.alttd.altitudeweb.model.UpdateMailDto;
|
||||
import com.alttd.altitudeweb.services.limits.RateLimit;
|
||||
import com.alttd.altitudeweb.setup.Connection;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.http.HttpStatusCode;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.web.server.ResponseStatusException;
|
||||
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
@Slf4j
|
||||
@RestController
|
||||
@AllArgsConstructor
|
||||
@RateLimit(limit = 30, timeValue = 1, timeUnit = TimeUnit.HOURS)
|
||||
public class AppealController implements AppealsApi {
|
||||
|
||||
private final AppealDataMapper mapper;
|
||||
|
||||
@RateLimit(limit = 3, timeValue = 1, timeUnit = TimeUnit.HOURS, key = "discordAppeal")
|
||||
@Override
|
||||
public ResponseEntity<MinecraftAppealDto> submitDiscordAppeal(DiscordAppealDto discordAppealDto) {
|
||||
public ResponseEntity<AppealResponseDto> submitDiscordAppeal(DiscordAppealDto discordAppealDto) {
|
||||
throw new ResponseStatusException(HttpStatusCode.valueOf(501), "Discord appeals are not yet supported");
|
||||
}
|
||||
|
||||
@RateLimit(limit = 3, timeValue = 1, timeUnit = TimeUnit.HOURS, key = "minecraftAppeal")
|
||||
@Override
|
||||
public ResponseEntity<AppealResponseDto> submitMinecraftAppeal(MinecraftAppealDto minecraftAppealDto) {
|
||||
throw new ResponseStatusException(HttpStatusCode.valueOf(501), "Minecraft appeals are not yet supported");
|
||||
CompletableFuture<Appeal> appealCompletableFuture = new CompletableFuture<>();
|
||||
|
||||
Connection.getConnection(Databases.DEFAULT)
|
||||
.runQuery(sqlSession -> {
|
||||
log.debug("Loading history by id");
|
||||
try {
|
||||
Appeal appeal = sqlSession.getMapper(AppealMapper.class)
|
||||
.createAppeal(mapper.minecraftAppealDtoToAppeal(minecraftAppealDto));
|
||||
appealCompletableFuture.complete(appeal);
|
||||
} catch (Exception e) {
|
||||
log.error("Failed to load history count", e);
|
||||
appealCompletableFuture.completeExceptionally(e);
|
||||
}
|
||||
});
|
||||
Appeal appeal = appealCompletableFuture.join();
|
||||
|
||||
AppealResponseDto appealResponseDto = new AppealResponseDto(
|
||||
appeal.id().toString(),
|
||||
"Your appeal has been submitted. You will be notified when it has been reviewed.",
|
||||
false);
|
||||
|
||||
return ResponseEntity.ok().body(appealResponseDto);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
package com.alttd.altitudeweb.controllers.login;
|
||||
|
||||
import com.alttd.altitudeweb.api.LoginApi;
|
||||
import com.alttd.altitudeweb.controllers.data_from_auth.AuthenticatedUuid;
|
||||
import com.alttd.altitudeweb.database.litebans.HistoryRecord;
|
||||
import com.alttd.altitudeweb.database.litebans.RecentNamesMapper;
|
||||
import com.alttd.altitudeweb.database.litebans.UUIDHistoryMapper;
|
||||
|
|
@ -47,7 +48,8 @@ public class LoginController implements LoginApi {
|
|||
@Value("${my-server.address:#{null}}")
|
||||
private String serverAddress;
|
||||
|
||||
private record CacheEntry(UUID uuid, Instant expiry) {}
|
||||
private record CacheEntry(UUID uuid, Instant expiry) {
|
||||
}
|
||||
|
||||
private static final ConcurrentMap<String, CacheEntry> cache = new ConcurrentHashMap<>();
|
||||
|
||||
|
|
@ -79,9 +81,9 @@ public class LoginController implements LoginApi {
|
|||
}
|
||||
|
||||
Optional<String> key = cache.entrySet().stream()
|
||||
.filter(entry -> entry.getValue().uuid.equals(uuidFromString))
|
||||
.map(Map.Entry::getKey)
|
||||
.findFirst();
|
||||
.filter(entry -> entry.getValue().uuid.equals(uuidFromString))
|
||||
.map(Map.Entry::getKey)
|
||||
.findFirst();
|
||||
|
||||
if (key.isPresent()) {
|
||||
return ResponseEntity.ok(key.get());
|
||||
|
|
@ -94,25 +96,23 @@ public class LoginController implements LoginApi {
|
|||
@Override
|
||||
public ResponseEntity<UsernameDto> getUsername() {
|
||||
log.debug("Loading username for logged in user");
|
||||
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
|
||||
log.debug("Loaded authentication for logged in user {}", authentication);
|
||||
if (authentication == null || !(authentication.getPrincipal() instanceof Jwt jwt)) {
|
||||
log.debug("Loaded authentication for logged in user is null or not a jwt");
|
||||
return ResponseEntity.status(401).build();
|
||||
}
|
||||
String stringUuid = jwt.getSubject();
|
||||
UUID uuid;
|
||||
try {
|
||||
uuid = UUID.fromString(stringUuid);
|
||||
log.debug("Loaded username for logged in user {}", uuid);
|
||||
} catch (IllegalArgumentException e) {
|
||||
return ResponseEntity.badRequest().build();
|
||||
}
|
||||
|
||||
UsernameDto usernameDto = new UsernameDto();
|
||||
usernameDto.setUsername(getUsername(uuid));
|
||||
log.debug("Loaded username for logged in user {}", usernameDto.getUsername());
|
||||
return ResponseEntity.ok(usernameDto);
|
||||
try {
|
||||
// Get authenticated UUID using the utility method
|
||||
UUID uuid = AuthenticatedUuid.getAuthenticatedUserUuid();
|
||||
log.debug("Loaded username for logged in user {}", uuid);
|
||||
|
||||
// Create response with username
|
||||
UsernameDto usernameDto = new UsernameDto();
|
||||
usernameDto.setUsername(getUsername(uuid));
|
||||
log.debug("Loaded username for logged in user {}", usernameDto.getUsername());
|
||||
|
||||
return ResponseEntity.ok(usernameDto);
|
||||
|
||||
} catch (ResponseStatusException e) {
|
||||
// The utility method already throws proper exceptions, we just need to convert them to ResponseEntity
|
||||
return ResponseEntity.status(e.getStatusCode()).build();
|
||||
}
|
||||
}
|
||||
|
||||
private String getUsername(UUID uuid) {
|
||||
|
|
@ -165,7 +165,7 @@ public class LoginController implements LoginApi {
|
|||
loginCode.append(characters.charAt(index));
|
||||
}
|
||||
CacheEntry cacheEntry = new CacheEntry(uuid,
|
||||
Instant.now().plusSeconds(TimeUnit.MINUTES.toSeconds(15)));
|
||||
Instant.now().plusSeconds(TimeUnit.MINUTES.toSeconds(15)));
|
||||
cache.put(loginCode.toString(), cacheEntry);
|
||||
return loginCode.toString();
|
||||
}
|
||||
|
|
@ -193,18 +193,25 @@ public class LoginController implements LoginApi {
|
|||
CompletableFuture<Optional<PrivilegedUser>> privilegedUserCompletableFuture = new CompletableFuture<>();
|
||||
List<PermissionClaimDto> claimList = new ArrayList<>();
|
||||
Connection.getConnection(Databases.DEFAULT)
|
||||
.runQuery(sqlSession -> {
|
||||
try {
|
||||
log.debug("Loading user by uuid {}", uuid.toString());
|
||||
Optional<PrivilegedUser> privilegedUser = sqlSession.getMapper(PrivilegedUserMapper.class)
|
||||
.getUserByUuid(uuid.toString());
|
||||
.runQuery(sqlSession -> {
|
||||
try {
|
||||
log.debug("Loading user by uuid {}", uuid.toString());
|
||||
PrivilegedUserMapper mapper = sqlSession.getMapper(PrivilegedUserMapper.class);
|
||||
Optional<PrivilegedUser> privilegedUser = mapper
|
||||
.getUserByUuid(uuid);
|
||||
|
||||
privilegedUserCompletableFuture.complete(privilegedUser);
|
||||
} catch (Exception e) {
|
||||
log.error("Failed to load user by uuid", e);
|
||||
privilegedUserCompletableFuture.completeExceptionally(e);
|
||||
}
|
||||
});
|
||||
if (privilegedUser.isEmpty()) {
|
||||
int privilegedUserId = mapper.createPrivilegedUser(uuid);
|
||||
privilegedUserCompletableFuture.complete(
|
||||
Optional.of(new PrivilegedUser(privilegedUserId, uuid, List.of())));
|
||||
} else {
|
||||
privilegedUserCompletableFuture.complete(privilegedUser);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("Failed to load user by uuid", e);
|
||||
privilegedUserCompletableFuture.completeExceptionally(e);
|
||||
}
|
||||
});
|
||||
Optional<PrivilegedUser> privilegedUser = privilegedUserCompletableFuture.join();
|
||||
claimList.add(PermissionClaimDto.USER);
|
||||
privilegedUser.ifPresent(user -> user.getPermissions().forEach(permission -> {
|
||||
|
|
@ -218,12 +225,13 @@ public class LoginController implements LoginApi {
|
|||
log.debug("Generated token for user {} with claims {}", uuid.toString(),
|
||||
claimList.stream().map(PermissionClaimDto::getValue).toList());
|
||||
JwtClaimsSet claims = JwtClaimsSet.builder()
|
||||
.issuer(serverAddress)
|
||||
.claim("authorities", claimList.stream().map(PermissionClaimDto::getValue).toList())
|
||||
.issuedAt(now)
|
||||
.expiresAt(expiryTime)
|
||||
.subject(uuid.toString())
|
||||
.build();
|
||||
.issuer(serverAddress)
|
||||
.claim("authorities",
|
||||
claimList.stream().map(PermissionClaimDto::getValue).toList())
|
||||
.issuedAt(now)
|
||||
.expiresAt(expiryTime)
|
||||
.subject(uuid.toString())
|
||||
.build();
|
||||
|
||||
return jwtEncoder.encode(JwtEncoderParameters.from(claims)).getTokenValue();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,30 @@
|
|||
package com.alttd.altitudeweb.mappers;
|
||||
|
||||
import com.alttd.altitudeweb.database.web_db.forms.Appeal;
|
||||
import com.alttd.altitudeweb.model.MinecraftAppealDto;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
@Service
|
||||
public class AppealDataMapper {
|
||||
public MinecraftAppealDto appealToMinecraftAppealDto(Appeal appeal) {
|
||||
MinecraftAppealDto minecraftAppealDto = new MinecraftAppealDto();
|
||||
minecraftAppealDto.setAppeal(appeal.reason());
|
||||
minecraftAppealDto.setUsername(appeal.username());
|
||||
minecraftAppealDto.setUuid(appeal.uuid());
|
||||
minecraftAppealDto.setEmail(appeal.email());
|
||||
return minecraftAppealDto;
|
||||
}
|
||||
|
||||
public Appeal minecraftAppealDtoToAppeal(MinecraftAppealDto minecraftAppealDto) {
|
||||
return new Appeal(
|
||||
null,
|
||||
minecraftAppealDto.getUuid(),
|
||||
minecraftAppealDto.getUsername(),
|
||||
minecraftAppealDto.getAppeal(),
|
||||
null,
|
||||
null,
|
||||
minecraftAppealDto.getEmail(),
|
||||
null
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -5,12 +5,13 @@ import lombok.Data;
|
|||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class PrivilegedUser {
|
||||
private int id;
|
||||
private String uuid;
|
||||
private UUID uuid;
|
||||
private List<String> permissions;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import org.jetbrains.annotations.Nullable;
|
|||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
|
||||
public interface PrivilegedUserMapper {
|
||||
|
||||
|
|
@ -24,7 +25,7 @@ public interface PrivilegedUserMapper {
|
|||
@Result(property = "permissions", column = "id", javaType = List.class,
|
||||
many = @Many(select = "getPermissionsForUser"))
|
||||
})
|
||||
Optional<PrivilegedUser> getUserByUuid(@Param("uuid") String uuid);
|
||||
Optional<PrivilegedUser> getUserByUuid(@Param("uuid") UUID uuid);
|
||||
|
||||
/**
|
||||
* Retrieves all privileged users with their permissions
|
||||
|
|
@ -100,4 +101,11 @@ public interface PrivilegedUserMapper {
|
|||
WHERE user_id = #{userId} AND privileges = #{permission}
|
||||
""")
|
||||
int removePermissionFromUser(@Param("userId") int userId, @Param("permission") String permission);
|
||||
|
||||
@Insert("""
|
||||
INSERT INTO privileged_users (uuid)
|
||||
VALUES (#{uuid})
|
||||
""")
|
||||
@Options(useGeneratedKeys = true, keyProperty = "id", keyColumn = "id")
|
||||
int createPrivilegedUser(UUID uuid);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,16 @@
|
|||
package com.alttd.altitudeweb.database.web_db.forms;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.UUID;
|
||||
|
||||
public record Appeal(
|
||||
UUID id,
|
||||
UUID uuid,
|
||||
String username,
|
||||
String reason,
|
||||
Instant createdAt,
|
||||
Instant sendAt,
|
||||
String email,
|
||||
Long assignedTo
|
||||
) {
|
||||
}
|
||||
|
|
@ -0,0 +1,44 @@
|
|||
package com.alttd.altitudeweb.database.web_db.forms;
|
||||
|
||||
import com.alttd.altitudeweb.model.MinecraftAppealDto;
|
||||
import org.apache.ibatis.annotations.Insert;
|
||||
import org.apache.ibatis.annotations.Options;
|
||||
import org.apache.ibatis.annotations.Select;
|
||||
import org.apache.ibatis.annotations.Update;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public interface AppealMapper {
|
||||
|
||||
@Insert("""
|
||||
INSERT INTO appeals (uuid, username, reason, created_at, send_at, e_mail, assigned_to)
|
||||
VALUES (#{uuid}, #{username}, #{reason}, #{createdAt}, #{sendAt}, #{email}, #{assignedTo})
|
||||
""")
|
||||
@Options(useGeneratedKeys = true, keyProperty = "id")
|
||||
Appeal createAppeal(Appeal appeal);
|
||||
|
||||
@Update("""
|
||||
UPDATE appeals
|
||||
SET reason = #{reason},
|
||||
created_at = #{createdAt},
|
||||
send_at = #{sendAt},
|
||||
e_mail = #{email},
|
||||
assigned_to = #{assignedTo}
|
||||
WHERE id = #{id}
|
||||
""")
|
||||
void updateAppeal(Appeal appeal);
|
||||
|
||||
@Select("""
|
||||
SELECT id, uuid, reason, created_at AS createdAt, send_at AS sendAt, e_mail AS email, assigned_to AS assignedTo
|
||||
FROM appeals
|
||||
WHERE id = #{id}
|
||||
""")
|
||||
Appeal getAppealById(int id);
|
||||
|
||||
@Select("""
|
||||
SELECT id, uuid, reason, created_at AS createdAt, send_at AS sendAt, e_mail AS email, assigned_to AS assignedTo
|
||||
FROM appeals
|
||||
WHERE uuid = #{uuid}
|
||||
""")
|
||||
List<Appeal> getAppealsByUuid(String uuid);
|
||||
}
|
||||
|
|
@ -4,6 +4,7 @@ import com.alttd.altitudeweb.database.Databases;
|
|||
import com.alttd.altitudeweb.database.web_db.KeyPairMapper;
|
||||
import com.alttd.altitudeweb.database.web_db.PrivilegedUserMapper;
|
||||
import com.alttd.altitudeweb.database.web_db.SettingsMapper;
|
||||
import com.alttd.altitudeweb.database.web_db.forms.AppealMapper;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.ibatis.session.SqlSession;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
|
@ -20,12 +21,14 @@ public class InitializeWebDb {
|
|||
configuration.addMapper(SettingsMapper.class);
|
||||
configuration.addMapper(KeyPairMapper.class);
|
||||
configuration.addMapper(PrivilegedUserMapper.class);
|
||||
configuration.addMapper(AppealMapper.class);
|
||||
}).join()
|
||||
.runQuery(SqlSession -> {
|
||||
createSettingsTable(SqlSession);
|
||||
createKeyTable(SqlSession);
|
||||
createPrivilegedUsersTable(SqlSession);
|
||||
createPrivilegesTable(SqlSession);
|
||||
.runQuery(sqlSession -> {
|
||||
createSettingsTable(sqlSession);
|
||||
createKeyTable(sqlSession);
|
||||
createPrivilegedUsersTable(sqlSession);
|
||||
createPrivilegesTable(sqlSession);
|
||||
createAppealTable(sqlSession);
|
||||
});
|
||||
log.debug("Initialized WebDb");
|
||||
}
|
||||
|
|
@ -70,7 +73,7 @@ public class InitializeWebDb {
|
|||
String query = """
|
||||
CREATE TABLE IF NOT EXISTS privileged_users (
|
||||
id int NOT NULL AUTO_INCREMENT PRIMARY KEY,
|
||||
uuid VARCHAR(36) NOT NULL
|
||||
uuid UUID UNIQUE NOT NULL
|
||||
);
|
||||
""";
|
||||
try (Statement statement = sqlSession.getConnection().createStatement()) {
|
||||
|
|
@ -99,4 +102,25 @@ public class InitializeWebDb {
|
|||
}
|
||||
}
|
||||
|
||||
private static void createAppealTable(@NotNull SqlSession sqlSession) {
|
||||
String query = """
|
||||
CREATE TABLE IF NOT EXISTS appeals (
|
||||
id UUID NOT NULL DEFAULT (UUID()) PRIMARY KEY,
|
||||
uuid UUID NOT NULL,
|
||||
username VARCHAR(16) NOT NULL,
|
||||
reason TEXT NOT NULL,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL,
|
||||
send_at TIMESTAMP NULL,
|
||||
e_mail TEXT NOT NULL,
|
||||
assigned_to BIGINT UNSIGNED NULL,
|
||||
FOREIGN KEY (uuid) REFERENCES privileged_users(uuid) ON DELETE CASCADE ON UPDATE CASCADE
|
||||
);
|
||||
""";
|
||||
try (Statement statement = sqlSession.getConnection().createStatement()) {
|
||||
statement.execute(query);
|
||||
} catch (SQLException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -75,7 +75,7 @@ DiscordAppeal:
|
|||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/MinecraftAppeal'
|
||||
$ref: '#/components/schemas/AppealResponse'
|
||||
default:
|
||||
description: Unexpected error
|
||||
content:
|
||||
|
|
@ -136,6 +136,7 @@ components:
|
|||
required:
|
||||
- id
|
||||
- message
|
||||
- verified_mail
|
||||
properties:
|
||||
id:
|
||||
type: string
|
||||
|
|
@ -143,6 +144,9 @@ components:
|
|||
message:
|
||||
type: string
|
||||
description: Confirmation message
|
||||
verified_mail:
|
||||
type: boolean
|
||||
description: If this user has verified their mail already
|
||||
UpdateMail:
|
||||
type: object
|
||||
required:
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user