Introduce Chat API with message handling, server state updates, and IP-based security restrictions.
This commit is contained in:
parent
536439f324
commit
837f87d9be
|
|
@ -4,9 +4,11 @@ import com.alttd.altitudeweb.setup.Connection;
|
||||||
import org.springframework.boot.SpringApplication;
|
import org.springframework.boot.SpringApplication;
|
||||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||||
import org.springframework.context.annotation.EnableAspectJAutoProxy;
|
import org.springframework.context.annotation.EnableAspectJAutoProxy;
|
||||||
|
import org.springframework.scheduling.annotation.EnableScheduling;
|
||||||
|
|
||||||
@SpringBootApplication(scanBasePackages = {"com.alttd.altitudeweb"})
|
@SpringBootApplication(scanBasePackages = {"com.alttd.altitudeweb"})
|
||||||
@EnableAspectJAutoProxy
|
@EnableAspectJAutoProxy
|
||||||
|
@EnableScheduling
|
||||||
public class AltitudeWebApplication {
|
public class AltitudeWebApplication {
|
||||||
|
|
||||||
public static void main(String[] args) {
|
public static void main(String[] args) {
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@ import com.nimbusds.jose.jwk.source.JWKSource;
|
||||||
import com.nimbusds.jose.proc.SecurityContext;
|
import com.nimbusds.jose.proc.SecurityContext;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
import org.springframework.context.annotation.Bean;
|
import org.springframework.context.annotation.Bean;
|
||||||
import org.springframework.context.annotation.Configuration;
|
import org.springframework.context.annotation.Configuration;
|
||||||
import org.springframework.http.HttpMethod;
|
import org.springframework.http.HttpMethod;
|
||||||
|
|
@ -26,6 +27,7 @@ import org.springframework.security.oauth2.jwt.NimbusJwtDecoder;
|
||||||
import org.springframework.security.oauth2.jwt.NimbusJwtEncoder;
|
import org.springframework.security.oauth2.jwt.NimbusJwtEncoder;
|
||||||
import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationConverter;
|
import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationConverter;
|
||||||
import org.springframework.security.web.SecurityFilterChain;
|
import org.springframework.security.web.SecurityFilterChain;
|
||||||
|
import org.springframework.security.web.access.expression.WebExpressionAuthorizationManager;
|
||||||
|
|
||||||
import java.security.KeyPair;
|
import java.security.KeyPair;
|
||||||
import java.security.interfaces.RSAPrivateKey;
|
import java.security.interfaces.RSAPrivateKey;
|
||||||
|
|
@ -42,17 +44,24 @@ public class SecurityConfig {
|
||||||
private final KeyPairService keyPairService;
|
private final KeyPairService keyPairService;
|
||||||
private final SecurityAuthFailureHandler securityAuthFailureHandler;
|
private final SecurityAuthFailureHandler securityAuthFailureHandler;
|
||||||
|
|
||||||
|
@Value("${chat.allowed-ip}")
|
||||||
|
private String allowedIp;
|
||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
|
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
|
||||||
|
WebExpressionAuthorizationManager allowedIpCheck = new WebExpressionAuthorizationManager(
|
||||||
|
"hasIpAddress('%s')".formatted(allowedIp));
|
||||||
return http
|
return http
|
||||||
.authorizeHttpRequests(
|
.authorizeHttpRequests(
|
||||||
auth -> auth
|
auth -> auth
|
||||||
.requestMatchers(HttpMethod.OPTIONS, "/**").permitAll()
|
.requestMatchers(HttpMethod.OPTIONS, "/**").permitAll()
|
||||||
|
.requestMatchers("/api/chat/send/**").access(allowedIpCheck)
|
||||||
.requestMatchers("/api/form/**").authenticated()
|
.requestMatchers("/api/form/**").authenticated()
|
||||||
.requestMatchers("/api/login/getUsername").authenticated()
|
.requestMatchers("/api/login/getUsername").authenticated()
|
||||||
.requestMatchers("/api/mail/**").authenticated()
|
.requestMatchers("/api/mail/**").authenticated()
|
||||||
.requestMatchers("/api/site/vote").authenticated()
|
.requestMatchers("/api/site/vote").authenticated()
|
||||||
.requestMatchers("/api/appeal").authenticated()
|
.requestMatchers("/api/appeal").authenticated()
|
||||||
|
.requestMatchers("/api/chat/read/**").hasAuthority(PermissionClaimDto.HEAD_MOD.getValue())
|
||||||
.requestMatchers("/api/site/get-staff-playtime/**").hasAuthority(PermissionClaimDto.HEAD_MOD.getValue())
|
.requestMatchers("/api/site/get-staff-playtime/**").hasAuthority(PermissionClaimDto.HEAD_MOD.getValue())
|
||||||
.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())
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,33 @@
|
||||||
|
package com.alttd.altitudeweb.controllers.chat;
|
||||||
|
|
||||||
|
import com.alttd.altitudeweb.api.ChatApi;
|
||||||
|
import com.alttd.altitudeweb.model.ChatMessageDto;
|
||||||
|
import com.alttd.altitudeweb.model.ServerStateDto;
|
||||||
|
import com.alttd.altitudeweb.services.chat.ChatService;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.http.ResponseEntity;
|
||||||
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
|
@RestController
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class ChatController implements ChatApi {
|
||||||
|
|
||||||
|
private final ChatService chatService;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ResponseEntity<Void> sendChatMessages(List<ChatMessageDto> chatMessageDtoList) {
|
||||||
|
//TODO [Admin] [2026-07-18]: Implement handling chat messages (push to listeners)
|
||||||
|
chatMessageDtoList.stream().map(ChatMessageMapper::fromDto).forEach(chatService::addChatMessage);
|
||||||
|
throw new UnsupportedOperationException("Not implemented");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ResponseEntity<Void> updateServerStates(ServerStateDto serverStateDto) {
|
||||||
|
//TODO [Stijn] [2026-07-18]: Implement handling server state updates (push to listeners)
|
||||||
|
throw new UnsupportedOperationException("Not implemented");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,20 @@
|
||||||
|
package com.alttd.altitudeweb.controllers.chat;
|
||||||
|
|
||||||
|
import lombok.Builder;
|
||||||
|
import lombok.Getter;
|
||||||
|
|
||||||
|
import java.time.Instant;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
@Builder
|
||||||
|
@Getter
|
||||||
|
public class ChatMessage {
|
||||||
|
|
||||||
|
private final UUID uuid;
|
||||||
|
private final Instant timestamp;
|
||||||
|
private final String server;
|
||||||
|
//TODO [Stijn] [2026-07-18]: Handle channel types
|
||||||
|
//private final String channel;
|
||||||
|
private final String messageJson;
|
||||||
|
private final boolean blocked;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,19 @@
|
||||||
|
package com.alttd.altitudeweb.controllers.chat;
|
||||||
|
|
||||||
|
import com.alttd.altitudeweb.model.ChatMessageDto;
|
||||||
|
import lombok.experimental.UtilityClass;
|
||||||
|
|
||||||
|
@UtilityClass
|
||||||
|
public class ChatMessageMapper {
|
||||||
|
|
||||||
|
public ChatMessage fromDto(ChatMessageDto dto) {
|
||||||
|
return ChatMessage.builder()
|
||||||
|
.uuid(dto.getUuid())
|
||||||
|
.timestamp(dto.getTimestamp().toInstant())//TODO [Stijn] [2026-07-18]: Check this works, might be sending instant instead
|
||||||
|
.server(dto.getServer())
|
||||||
|
.messageJson(dto.getMessage())
|
||||||
|
.blocked(dto.getBlocked())
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -12,7 +12,6 @@ import com.alttd.altitudeweb.model.PunishmentHistoryDto;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.springframework.http.ResponseEntity;
|
import org.springframework.http.ResponseEntity;
|
||||||
import org.springframework.security.access.prepost.PreAuthorize;
|
|
||||||
import org.springframework.web.bind.annotation.*;
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,38 @@
|
||||||
|
package com.alttd.altitudeweb.services.chat;
|
||||||
|
|
||||||
|
import com.alttd.altitudeweb.controllers.chat.ChatMessage;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.scheduling.annotation.Scheduled;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import java.time.Duration;
|
||||||
|
import java.time.Instant;
|
||||||
|
import java.util.*;
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
|
@Service
|
||||||
|
public class ChatService {
|
||||||
|
|
||||||
|
private static final Duration MAX_AGE = Duration.ofHours(1);
|
||||||
|
private final NavigableMap<Instant, ChatMessage> chatMessages = new TreeMap<>();
|
||||||
|
|
||||||
|
@Scheduled(cron = "0 * * * * *")
|
||||||
|
public void clearOldMessages() {
|
||||||
|
Instant cutoff = Instant.now().minus(MAX_AGE);
|
||||||
|
int currentSize = chatMessages.size();
|
||||||
|
|
||||||
|
chatMessages.headMap(cutoff, false).clear();
|
||||||
|
|
||||||
|
log.info("Clearing old chat messages. Removed {}/{} messages",
|
||||||
|
currentSize - chatMessages.size(), currentSize);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void addChatMessage(ChatMessage chatMessage) {
|
||||||
|
chatMessages.put(chatMessage.getTimestamp(), chatMessage);
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<ChatMessage> getMessagesSince(Instant instant) {
|
||||||
|
return chatMessages.tailMap(instant, false).values().stream().toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -18,3 +18,4 @@ spring.mail.password=${MAIL_PASSWORD}
|
||||||
spring.mail.properties.mail.smtp.auth=true
|
spring.mail.properties.mail.smtp.auth=true
|
||||||
spring.mail.properties.mail.smtp.ssl.enable=true
|
spring.mail.properties.mail.smtp.ssl.enable=true
|
||||||
spring.mail.properties.mail.smtp.socketFactory.class=javax.net.ssl.SSLSocketFactory
|
spring.mail.properties.mail.smtp.socketFactory.class=javax.net.ssl.SSLSocketFactory
|
||||||
|
chat.allowed-ip=${ALLOWED_IP:10.0.0.0/24}
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,7 @@ repositories {
|
||||||
|
|
||||||
tasks.named("compileJava") {
|
tasks.named("compileJava") {
|
||||||
dependsOn("generateJavaApi")
|
dependsOn("generateJavaApi")
|
||||||
|
dependsOn("generateJavaChatApi")
|
||||||
dependsOn("generateFrontendApi")
|
dependsOn("generateFrontendApi")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -34,7 +35,7 @@ tasks.jar {
|
||||||
sourceSets {
|
sourceSets {
|
||||||
main {
|
main {
|
||||||
java {
|
java {
|
||||||
srcDir("${projectDir}/build/generated-resources/model/src/main/java")
|
srcDir("${projectDir}/build/generated-sources/model/src/main/java")
|
||||||
exclude("org/openapitools/configuration/SpringDocConfiguration.java")
|
exclude("org/openapitools/configuration/SpringDocConfiguration.java")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -51,7 +52,7 @@ dependencies {
|
||||||
implementation("org.springframework.hateoas:spring-hateoas:2.2.0")
|
implementation("org.springframework.hateoas:spring-hateoas:2.2.0")
|
||||||
}
|
}
|
||||||
|
|
||||||
tasks.register< GenerateTask>("generateJavaApi") {
|
tasks.register<GenerateTask>("generateJavaApi") {
|
||||||
inputs.file("${projectDir}/src/main/resources/api.yml")
|
inputs.file("${projectDir}/src/main/resources/api.yml")
|
||||||
inputs.file("${projectDir}/src/main/resources/config_backend.json")
|
inputs.file("${projectDir}/src/main/resources/config_backend.json")
|
||||||
inputs.dir("${projectDir}/src/main/resources/schemas")
|
inputs.dir("${projectDir}/src/main/resources/schemas")
|
||||||
|
|
@ -59,7 +60,33 @@ tasks.register< GenerateTask>("generateJavaApi") {
|
||||||
library.set("spring-boot")
|
library.set("spring-boot")
|
||||||
inputSpec.set("${projectDir}/src/main/resources/api.yml")
|
inputSpec.set("${projectDir}/src/main/resources/api.yml")
|
||||||
configFile.set("${projectDir}/src/main/resources/config_backend.json")
|
configFile.set("${projectDir}/src/main/resources/config_backend.json")
|
||||||
outputDir.set("${projectDir}/build/generated-resources/model")
|
outputDir.set("${projectDir}/build/generated-sources/model")
|
||||||
|
typeMappings.put("OffsetDateTime", "Instant")
|
||||||
|
importMappings.put("java.time.OffsetDateTime", "java.time.Instant")
|
||||||
|
modelNameSuffix.set("Dto")
|
||||||
|
|
||||||
|
// Make generator use Java 8 time types and map date-time -> Instant
|
||||||
|
additionalProperties.set(mapOf("dateLibrary" to "java8"))
|
||||||
|
typeMappings.set(mapOf("date-time" to "Instant"))
|
||||||
|
importMappings.set(mapOf("Instant" to "java.time.Instant"))
|
||||||
|
generateModelTests.set(false)
|
||||||
|
generateModelDocumentation.set(false)
|
||||||
|
generateApiTests.set(false)
|
||||||
|
generateApiDocumentation.set(false)
|
||||||
|
generateAliasAsModel.set(true)
|
||||||
|
modelPackage.set("com.alttd.altitudeweb.model")
|
||||||
|
generateApiTests.set(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
tasks.register<GenerateTask>("generateJavaChatApi") {
|
||||||
|
inputs.file("${projectDir}/src/main/resources/chat-api.yml")
|
||||||
|
inputs.file("${projectDir}/src/main/resources/config_backend.json")
|
||||||
|
inputs.dir("${projectDir}/src/main/resources/schemas")
|
||||||
|
generatorName.set("spring")
|
||||||
|
library.set("spring-boot")
|
||||||
|
inputSpec.set("${projectDir}/src/main/resources/chat-api.yml")
|
||||||
|
configFile.set("${projectDir}/src/main/resources/config_backend.json")
|
||||||
|
outputDir.set("${projectDir}/build/generated-sources/model")
|
||||||
typeMappings.put("OffsetDateTime", "Instant")
|
typeMappings.put("OffsetDateTime", "Instant")
|
||||||
importMappings.put("java.time.OffsetDateTime", "java.time.Instant")
|
importMappings.put("java.time.OffsetDateTime", "java.time.Instant")
|
||||||
modelNameSuffix.set("Dto")
|
modelNameSuffix.set("Dto")
|
||||||
|
|
|
||||||
142
open_api/src/main/resources/chat-api.yml
Normal file
142
open_api/src/main/resources/chat-api.yml
Normal file
|
|
@ -0,0 +1,142 @@
|
||||||
|
openapi: 3.0.3
|
||||||
|
|
||||||
|
info:
|
||||||
|
title: Minecraft Network API
|
||||||
|
version: 1.0.0
|
||||||
|
|
||||||
|
servers:
|
||||||
|
- url: http://localhost:8080/api
|
||||||
|
|
||||||
|
tags:
|
||||||
|
- name: chat
|
||||||
|
description: Data for displaying Chat messages to clients
|
||||||
|
|
||||||
|
paths:
|
||||||
|
/chat/send/chat/message:
|
||||||
|
post:
|
||||||
|
tags:
|
||||||
|
- chat
|
||||||
|
summary: Sends one or more chat messages
|
||||||
|
operationId: sendChatMessages
|
||||||
|
requestBody:
|
||||||
|
required: true
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
$ref: "#/components/schemas/ChatMessage"
|
||||||
|
responses:
|
||||||
|
"202":
|
||||||
|
description: Accepted
|
||||||
|
|
||||||
|
/chat/send/servers/state:
|
||||||
|
post:
|
||||||
|
tags:
|
||||||
|
- chat
|
||||||
|
summary: Update server states
|
||||||
|
operationId: updateServerStates
|
||||||
|
requestBody:
|
||||||
|
required: true
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: "#/components/schemas/ServerState"
|
||||||
|
responses:
|
||||||
|
"202":
|
||||||
|
description: Accepted
|
||||||
|
components:
|
||||||
|
schemas:
|
||||||
|
User:
|
||||||
|
type: object
|
||||||
|
required:
|
||||||
|
- uuid
|
||||||
|
- name
|
||||||
|
- styledName
|
||||||
|
|
||||||
|
properties:
|
||||||
|
uuid:
|
||||||
|
type: string
|
||||||
|
format: uuid
|
||||||
|
name:
|
||||||
|
type: string
|
||||||
|
styledName:
|
||||||
|
type: string
|
||||||
|
|
||||||
|
ChatMessage:
|
||||||
|
type: object
|
||||||
|
required:
|
||||||
|
- uuid
|
||||||
|
- message
|
||||||
|
- channel
|
||||||
|
- server
|
||||||
|
- timestamp
|
||||||
|
- blocked
|
||||||
|
properties:
|
||||||
|
uuid:
|
||||||
|
type: string
|
||||||
|
format: uuid
|
||||||
|
channel:
|
||||||
|
type: string
|
||||||
|
enum:
|
||||||
|
- CHAT
|
||||||
|
- PARTY
|
||||||
|
- AC
|
||||||
|
- GAC
|
||||||
|
message:
|
||||||
|
type: string
|
||||||
|
server:
|
||||||
|
type: string
|
||||||
|
timestamp:
|
||||||
|
type: string
|
||||||
|
format: date-time
|
||||||
|
blocked:
|
||||||
|
type: boolean
|
||||||
|
|
||||||
|
PrivateMessage:
|
||||||
|
type: object
|
||||||
|
required:
|
||||||
|
- sender
|
||||||
|
- receiver
|
||||||
|
- message
|
||||||
|
- senderServer
|
||||||
|
- receiverServer
|
||||||
|
properties:
|
||||||
|
sender:
|
||||||
|
type: string
|
||||||
|
format: uuid
|
||||||
|
receiver:
|
||||||
|
type: string
|
||||||
|
format: uuid
|
||||||
|
message:
|
||||||
|
type: string
|
||||||
|
senderServer:
|
||||||
|
type: string
|
||||||
|
receiverServer:
|
||||||
|
type: string
|
||||||
|
|
||||||
|
Server:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
name:
|
||||||
|
type: string
|
||||||
|
players:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
$ref: "#/components/schemas/User"
|
||||||
|
|
||||||
|
ServerState:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
servers:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
$ref: "#/components/schemas/Server"
|
||||||
|
parameters:
|
||||||
|
From:
|
||||||
|
name: from
|
||||||
|
in: path
|
||||||
|
required: true
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
format: date-time
|
||||||
Loading…
Reference in New Issue
Block a user