diff --git a/backend/src/main/java/com/alttd/altitudeweb/config/SecurityConfig.java b/backend/src/main/java/com/alttd/altitudeweb/config/SecurityConfig.java
index 5781c43..ae66826 100644
--- a/backend/src/main/java/com/alttd/altitudeweb/config/SecurityConfig.java
+++ b/backend/src/main/java/com/alttd/altitudeweb/config/SecurityConfig.java
@@ -71,6 +71,7 @@ public class SecurityConfig {
.requestMatchers("/api/files/download/**").hasAuthority(PermissionClaimDto.HEAD_MOD.getValue())
.requestMatchers("/api/history/admin/**").hasAuthority(PermissionClaimDto.HEAD_MOD.getValue())
.requestMatchers("/api/login/userLogin/**").permitAll()
+ .requestMatchers("/api/site/chat/**").authenticated()
.requestMatchers("/api/chat/server/send/**").permitAll()
.anyRequest().permitAll()
)
diff --git a/backend/src/main/java/com/alttd/altitudeweb/controllers/chat/ServerMessageController.java b/backend/src/main/java/com/alttd/altitudeweb/controllers/chat/ServerMessageController.java
new file mode 100644
index 0000000..2f35041
--- /dev/null
+++ b/backend/src/main/java/com/alttd/altitudeweb/controllers/chat/ServerMessageController.java
@@ -0,0 +1,46 @@
+package com.alttd.altitudeweb.controllers.chat;
+
+import com.alttd.altitudeweb.api.ServerMessageApi;
+import com.alttd.altitudeweb.model.ServerMessageRequestDto;
+import com.alttd.altitudeweb.services.chat.to_server.ServerMessageService;
+import com.alttd.altitudeweb.services.chat.to_server.data.ChatFromWeb;
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.RestController;
+
+@Slf4j
+@RestController
+@RequiredArgsConstructor
+public class ServerMessageController implements ServerMessageApi {
+
+ private final ServerMessageService serverMessageService;
+ private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
+
+ public static String toJson(Object object) {
+ try {
+ return OBJECT_MAPPER.writeValueAsString(object);
+ } catch (JsonProcessingException e) {
+ throw new IllegalStateException("Failed to serialize object to JSON", e);
+ }
+ }
+
+ @Override
+ public ResponseEntity sendServerMessage(String server, ServerMessageRequestDto serverMessageRequestDto) {
+ ChatFromWeb chatFromWeb = ChatFromWeb.builder()
+ .sender(serverMessageRequestDto.getUuid())
+ .message(serverMessageRequestDto.getMessage())
+ .build();
+
+ String json = toJson(chatFromWeb);
+
+ if (serverMessageService.sendMessage(server, "web_chat", json)) {
+ return new ResponseEntity<>(HttpStatus.ACCEPTED);
+ } else {
+ return new ResponseEntity<>(HttpStatus.NOT_FOUND);
+ }
+ }
+}
diff --git a/frontend/src/app/pages/altitude/chat/components/chat/chat-box.component.html b/frontend/src/app/pages/altitude/chat/components/chat/chat-box.component.html
index 8791661..ea86efd 100644
--- a/frontend/src/app/pages/altitude/chat/components/chat/chat-box.component.html
+++ b/frontend/src/app/pages/altitude/chat/components/chat/chat-box.component.html
@@ -12,3 +12,15 @@
}
+
+@if (canChat()) {
+
+
+
+
+}
diff --git a/frontend/src/app/pages/altitude/chat/components/chat/chat-box.component.scss b/frontend/src/app/pages/altitude/chat/components/chat/chat-box.component.scss
index 8a101a1..2f96baa 100644
--- a/frontend/src/app/pages/altitude/chat/components/chat/chat-box.component.scss
+++ b/frontend/src/app/pages/altitude/chat/components/chat/chat-box.component.scss
@@ -1,12 +1,14 @@
:host {
- display: block;
+ display: flex;
+ flex-direction: column;
height: 100%;
}
.message-container {
+ flex: 1 1 auto;
display: flex;
flex-direction: column-reverse;
- height: 100%;
+ min-height: 0;
overflow-y: auto;
box-sizing: border-box;
scrollbar-width: thin;
@@ -41,3 +43,51 @@
.date-time {
color: gray;
}
+
+.chat-input-container {
+ flex: 0 0 auto;
+ padding: 10px;
+ display: flex;
+ gap: 10px;
+ background: rgba(0, 0, 0, 0.2);
+ border-top: 1px solid rgba(255, 255, 255, 0.1);
+
+ input {
+ flex: 1 1 auto;
+ background: rgba(255, 255, 255, 0.05);
+ border: 1px solid rgba(255, 255, 255, 0.1);
+ border-radius: 4px;
+ padding: 8px 12px;
+ color: white;
+ outline: none;
+ font-size: 14px;
+
+ &:focus {
+ border-color: #4f8cff;
+ background: rgba(255, 255, 255, 0.1);
+ }
+
+ &::placeholder {
+ color: rgba(255, 255, 255, 0.3);
+ }
+ }
+
+ button {
+ cursor: pointer;
+ border: 0;
+ border-radius: 4px;
+ padding: 8px 16px;
+ background: #4f8cff;
+ color: #ffffff;
+ font-weight: 700;
+ transition: background 0.2s;
+
+ &:hover {
+ background: #3b7dff;
+ }
+
+ &:active {
+ background: #2a6ae0;
+ }
+ }
+}
diff --git a/frontend/src/app/pages/altitude/chat/components/chat/chat-box.component.ts b/frontend/src/app/pages/altitude/chat/components/chat/chat-box.component.ts
index 24a7c6a..c81e85e 100644
--- a/frontend/src/app/pages/altitude/chat/components/chat/chat-box.component.ts
+++ b/frontend/src/app/pages/altitude/chat/components/chat/chat-box.component.ts
@@ -1,17 +1,63 @@
-import {Component, input} from '@angular/core';
+import {Component, computed, inject, input} from '@angular/core';
import {DatePipe} from '@angular/common';
import {ChatMessage} from '@pages/altitude/chat/objects/chat-message.object';
import {MiniMessageComponent} from '@pages/altitude/chat/components/mini-message/mini-message.component';
+import {AuthService} from '@services/auth.service';
+import {ChatService} from '@pages/altitude/chat/service/chat.service';
+import {ServerMessageService} from '@api';
+import {FormsModule} from '@angular/forms';
+import {MatSnackBar} from '@angular/material/snack-bar';
@Component({
selector: 'app-chat-box',
+ standalone: true,
imports: [
MiniMessageComponent,
- DatePipe
+ DatePipe,
+ FormsModule
],
templateUrl: './chat-box.component.html',
styleUrl: './chat-box.component.scss'
})
export class ChatBoxComponent {
+ private readonly authService = inject(AuthService);
+ private readonly chatService = inject(ChatService);
+ private readonly serverMessageService = inject(ServerMessageService);
+ private readonly matSnackBar = inject(MatSnackBar);
+
readonly messages = input.required();
+ chatText = '';
+
+ readonly canChat = computed(() => {
+ const hasPrivilege = this.authService.hasAccess(['SCOPE_head_mod']);
+ const isServerChannel = this.chatService.selectedChannel()?.type === 'SERVER';
+ if (!hasPrivilege || !isServerChannel) {
+ console.log(`User has privilege ${hasPrivilege}, user in server channel ${isServerChannel}`);
+ }
+ return hasPrivilege && isServerChannel;
+ });
+
+ sendMessage() {
+ const message = this.chatText.trim();
+ if (!message) return;
+
+ const selectedChannel = this.chatService.selectedChannel();
+ const uuid = this.authService.getUuid();
+
+ if (selectedChannel?.type === 'SERVER' && uuid) {
+ this.serverMessageService.sendServerMessage(selectedChannel.name, {
+ uuid,
+ message
+ }).subscribe({
+ next: () => {
+ this.chatText = '';
+ },
+ error: (err) => {
+ this.matSnackBar.open('Failed to send message', 'Dismiss', {
+ duration: 3000
+ });
+ }
+ });
+ }
+ }
}
diff --git a/open_api/src/main/resources/api.yml b/open_api/src/main/resources/api.yml
index 14c9b80..048a3b7 100644
--- a/open_api/src/main/resources/api.yml
+++ b/open_api/src/main/resources/api.yml
@@ -32,6 +32,8 @@ tags:
description: Actions related to small features on the site such as displaying vote stats or pt/rank stats
- name: chatInfo
description: All actions related to chat information
+ - name: server-message
+ description: All actions related to sending messages to servers
paths:
/api/team/{team}:
$ref: './schemas/team/team.yml#/getTeam'
@@ -103,3 +105,5 @@ paths:
$ref: './schemas/chat_info/chat_info.yml#/PartyName'
/api/site/chat/player/{uuid}/name:
$ref: './schemas/chat_info/chat_info.yml#/UserName'
+ /api/site/chat/send-message/{server}:
+ $ref: './schemas/server-message/server-message.yml#/sendServerMessage'
diff --git a/open_api/src/main/resources/schemas/server-message/server-message.yml b/open_api/src/main/resources/schemas/server-message/server-message.yml
new file mode 100644
index 0000000..e1f733f
--- /dev/null
+++ b/open_api/src/main/resources/schemas/server-message/server-message.yml
@@ -0,0 +1,44 @@
+sendServerMessage:
+ post:
+ tags:
+ - server-message
+ summary: Send a message to a specific server
+ operationId: sendServerMessage
+ parameters:
+ - $ref: '#/components/parameters/Server'
+ requestBody:
+ required: true
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/ServerMessageRequest'
+ responses:
+ '202':
+ description: Message accepted and sent
+ '404':
+ description: Server not found or not connected
+ '400':
+ description: Invalid request
+components:
+ parameters:
+ Server:
+ name: server
+ in: path
+ required: true
+ schema:
+ type: string
+ description: The name of the server
+ schemas:
+ ServerMessageRequest:
+ type: object
+ required:
+ - uuid
+ - message
+ properties:
+ uuid:
+ type: string
+ format: uuid
+ description: The sender UUID
+ message:
+ type: string
+ description: The message to send