diff --git a/backend/config/chatBridge.json b/backend/config/chatBridge.json new file mode 100644 index 0000000..f9fb53a --- /dev/null +++ b/backend/config/chatBridge.json @@ -0,0 +1,4 @@ +{ + "host": "localhost", + "port": 1235 +} diff --git a/backend/controllers/adminController.js b/backend/controllers/adminController.js index ca4a804..d47a61a 100644 --- a/backend/controllers/adminController.js +++ b/backend/controllers/adminController.js @@ -177,9 +177,60 @@ class AdminController { } } + + + async updateRoom(req, res) { + try { + const userId = req.headers.userid; + if (!userId || !(await AdminService.hasUserAccess(userId, 'chatrooms'))) { + return res.status(403).json({ error: 'Keine Berechtigung.' }); + } + const schema = Joi.object({ + title: Joi.string().min(1).max(255).required(), + roomTypeId: Joi.number().integer().required(), + isPublic: Joi.boolean().required(), + genderRestrictionId: Joi.number().integer().allow(null), + minAge: Joi.number().integer().min(0).allow(null), + maxAge: Joi.number().integer().min(0).allow(null), + password: Joi.string().allow('', null), + friendsOfOwnerOnly: Joi.boolean().allow(null), + requiredUserRightId: Joi.number().integer().allow(null) + }); + const { error, value } = schema.validate(req.body); + if (error) { + return res.status(400).json({ error: error.details[0].message }); + } + const room = await AdminService.updateRoom(req.params.id, value); + res.status(200).json(room); + } catch (error) { + console.log(error); + res.status(500).json({ error: error.message }); + } + } + + async createRoom(req, res) { try { - const room = await AdminService.createRoom(req.body); + const userId = req.headers.userid; + if (!userId || !(await AdminService.hasUserAccess(userId, 'chatrooms'))) { + return res.status(403).json({ error: 'Keine Berechtigung.' }); + } + const schema = Joi.object({ + title: Joi.string().min(1).max(255).required(), + roomTypeId: Joi.number().integer().required(), + isPublic: Joi.boolean().required(), + genderRestrictionId: Joi.number().integer().allow(null), + minAge: Joi.number().integer().min(0).allow(null), + maxAge: Joi.number().integer().min(0).allow(null), + password: Joi.string().allow('', null), + friendsOfOwnerOnly: Joi.boolean().allow(null), + requiredUserRightId: Joi.number().integer().allow(null) + }); + const { error, value } = schema.validate(req.body); + if (error) { + return res.status(400).json({ error: error.details[0].message }); + } + const room = await AdminService.createRoom(value); res.status(201).json(room); } catch (error) { console.log(error); @@ -189,6 +240,10 @@ class AdminController { async deleteRoom(req, res) { try { + const userId = req.headers.userid; + if (!userId || !(await AdminService.hasUserAccess(userId, 'chatrooms'))) { + return res.status(403).json({ error: 'Keine Berechtigung.' }); + } await AdminService.deleteRoom(req.params.id); res.sendStatus(204); } catch (error) { diff --git a/backend/controllers/chatController.js b/backend/controllers/chatController.js index 9afc85a..dc90dae 100644 --- a/backend/controllers/chatController.js +++ b/backend/controllers/chatController.js @@ -1,4 +1,5 @@ import chatService from '../services/chatService.js'; +import Joi from 'joi'; class ChatController { constructor() { @@ -11,12 +12,20 @@ class ChatController { this.initOneToOne = this.initOneToOne.bind(this); this.sendOneToOneMessage = this.sendOneToOneMessage.bind(this); this.getOneToOneMessageHistory = this.getOneToOneMessageHistory.bind(this); + this.getRoomList = this.getRoomList.bind(this); } async getMessages(req, res) { - const { to, from } = req.body; + const schema = Joi.object({ + to: Joi.string().required(), + from: Joi.string().required() + }); + const { error, value } = schema.validate(req.body); + if (error) { + return res.status(400).json({ error: error.details[0].message }); + } try { - const messages = await chatService.getMessages(to, from); + const messages = await chatService.getMessages(value.to, value.from); res.status(200).json(messages); } catch (error) { res.status(500).json({ error: error.message }); @@ -24,9 +33,17 @@ class ChatController { } async findRandomChatMatch(req, res) { - const { genders, age, id } = req.body; + const schema = Joi.object({ + genders: Joi.array().items(Joi.string()).required(), + age: Joi.number().integer().min(0).required(), + id: Joi.string().required() + }); + const { error, value } = schema.validate(req.body); + if (error) { + return res.status(400).json({ error: error.details[0].message }); + } try { - const match = await chatService.findMatch(genders, age, id); + const match = await chatService.findMatch(value.genders, value.age, value.id); if (match) { res.status(200).json({ status: 'matched', user: match }); } else { @@ -38,9 +55,16 @@ class ChatController { } async registerUser(req, res) { - const { gender, age } = req.body; + const schema = Joi.object({ + gender: Joi.string().required(), + age: Joi.number().integer().min(0).required() + }); + const { error, value } = schema.validate(req.body); + if (error) { + return res.status(400).json({ error: error.details[0].message }); + } try { - const userId = await chatService.registerUser(gender, age); + const userId = await chatService.registerUser(value.gender, value.age); res.status(200).json({ id: userId }); } catch (error) { res.status(500).json({ error: error.message }); @@ -48,9 +72,17 @@ class ChatController { } async sendMessage(req, res) { - const { from, to, text } = req.body; + const schema = Joi.object({ + from: Joi.string().required(), + to: Joi.string().required(), + text: Joi.string().min(1).max(2000).required() + }); + const { error, value } = schema.validate(req.body); + if (error) { + return res.status(400).json({ error: error.details[0].message }); + } try { - const message = await chatService.addMessage(from, to, text); + const message = await chatService.addMessage(value.from, value.to, value.text); res.status(200).json(message); } catch (error) { res.status(500).json({ error: error.message }); @@ -58,9 +90,15 @@ class ChatController { } async removeUser(req, res) { - const { id } = req.body; + const schema = Joi.object({ + id: Joi.string().required() + }); + const { error, value } = schema.validate(req.body); + if (error) { + return res.status(400).json({ error: error.details[0].message }); + } try { - await chatService.removeUser(id); + await chatService.removeUser(value.id); res.sendStatus(200); } catch (error) { res.status(500).json({ error: error.message }); @@ -68,9 +106,15 @@ class ChatController { } async stopChat(req, res) { - const { id } = req.body; + const schema = Joi.object({ + id: Joi.string().required() + }); + const { error, value } = schema.validate(req.body); + if (error) { + return res.status(400).json({ error: error.details[0].message }); + } try { - await chatService.endChat(id); + await chatService.endChat(value.id); res.sendStatus(200); } catch (error) { res.status(500).json({ error: error.message }); @@ -78,10 +122,16 @@ class ChatController { } async initOneToOne(req, res) { + const schema = Joi.object({ + partnerHashId: Joi.string().required() + }); + const { error, value } = schema.validate(req.body); + if (error) { + return res.status(400).json({ error: error.details[0].message }); + } const { userid: hashedUserId } = req.headers; - const { partnerHashId } = req.body; try { - await chatService.initOneToOne(hashedUserId, partnerHashId); + await chatService.initOneToOne(hashedUserId, value.partnerHashId); res.status(200).json({ message: 'One-to-one chat initialization is pending implementation.' }); } catch (error) { res.status(500).json({ error: error.message }); @@ -89,9 +139,17 @@ class ChatController { } async sendOneToOneMessage(req, res) { - const { user1HashId, user2HashId, message } = req.body; + const schema = Joi.object({ + user1HashId: Joi.string().required(), + user2HashId: Joi.string().required(), + message: Joi.string().min(1).max(2000).required() + }); + const { error, value } = schema.validate(req.body); + if (error) { + return res.status(400).json({ error: error.details[0].message }); + } try { - await chatService.sendOneToOneMessage(user1HashId, user2HashId, message); + await chatService.sendOneToOneMessage(value.user1HashId, value.user2HashId, value.message); res.status(200).json({ status: 'message sent' }); } catch (error) { res.status(500).json({ error: error.message }); @@ -107,6 +165,16 @@ class ChatController { res.status(500).json({ error: error.message }); } } + + async getRoomList(req, res) { + // Öffentliche Räume für Chat-Frontend + try { + const rooms = await chatService.getRoomList(); + res.status(200).json(rooms); + } catch (error) { + res.status(500).json({ error: error.message }); + } + } } export default ChatController; diff --git a/backend/controllers/contactController.js b/backend/controllers/contactController.js index a0803d2..0b50bbb 100644 --- a/backend/controllers/contactController.js +++ b/backend/controllers/contactController.js @@ -1,4 +1,5 @@ import ContactService from '../services/ContactService.js'; +import Joi from 'joi'; class ContactController { constructor() { @@ -6,9 +7,18 @@ class ContactController { } async addContactMessage(req, res) { + const schema = Joi.object({ + email: Joi.string().email().required(), + name: Joi.string().min(1).max(255).required(), + message: Joi.string().min(1).max(2000).required(), + acceptDataSave: Joi.boolean().required() + }); + const { error, value } = schema.validate(req.body); + if (error) { + return res.status(400).json({ error: error.details[0].message }); + } try { - const { email, name, message, acceptDataSave } = req.body; - await ContactService.addContactMessage(email, name, message, acceptDataSave); + await ContactService.addContactMessage(value.email, value.name, value.message, value.acceptDataSave); res.status(200).json({ status: 'ok' }); } catch (error) { res.status(409).json({ error: error.message }); diff --git a/backend/controllers/forumController.js b/backend/controllers/forumController.js index a37c9d3..9f06836 100644 --- a/backend/controllers/forumController.js +++ b/backend/controllers/forumController.js @@ -1,11 +1,19 @@ import forumService from '../services/forumService.js'; +import Joi from 'joi'; const forumController = { async createForum(req, res) { + const schema = Joi.object({ + name: Joi.string().min(1).max(255).required(), + permissions: Joi.array().items(Joi.string().min(1).max(255)).required() + }); + const { error, value } = schema.validate(req.body); + if (error) { + return res.status(400).json({ error: error.details[0].message }); + } try { const { userid: userId } = req.headers; - const { name, permissions } = req.body; - const forum = await forumService.createForum(userId, name, permissions); + const forum = await forumService.createForum(userId, value.name, value.permissions); res.status(201).json(forum); } catch (error) { console.error('Error in createForum:', error); @@ -49,10 +57,18 @@ const forumController = { }, async createTopic(req, res) { + const schema = Joi.object({ + forumId: Joi.number().integer().required(), + title: Joi.string().min(1).max(255).required(), + content: Joi.string().min(1).max(5000).required() + }); + const { error, value } = schema.validate(req.body); + if (error) { + return res.status(400).json({ error: error.details[0].message }); + } try { const { userid: userId } = req.headers; - const { forumId, title, content } = req.body; - const result = await forumService.createTopic(userId, forumId, title, content); + const result = await forumService.createTopic(userId, value.forumId, value.title, value.content); res.status(201).json(result); } catch (error) { console.error('Error in createTopic:', error); @@ -73,11 +89,17 @@ const forumController = { }, async addMessage(req, res) { + const schema = Joi.object({ + content: Joi.string().min(1).max(5000).required() + }); + const { error, value } = schema.validate(req.body); + if (error) { + return res.status(400).json({ error: error.details[0].message }); + } try { const { userid: userId } = req.headers; const { id: topicId } = req.params; - const { content } = req.body; - const result = await forumService.addMessage(userId, topicId, content); + const result = await forumService.addMessage(userId, topicId, value.content); res.status(201).json(result); } catch (error) { console.error('Error in addMessage:', error); diff --git a/backend/controllers/friendshipController.js b/backend/controllers/friendshipController.js index 04b4b99..4c11689 100644 --- a/backend/controllers/friendshipController.js +++ b/backend/controllers/friendshipController.js @@ -1,12 +1,18 @@ import friendshipService from '../services/friendshipService.js'; +import Joi from 'joi'; const friendshipController = { async endFriendship(req, res) { + const schema = Joi.object({ + friendUserId: Joi.string().required() + }); + const { error, value } = schema.validate(req.body); + if (error) { + return res.status(400).json({ error: error.details[0].message }); + } try { const { userid: hashedUserId } = req.headers; - const { friendUserId } = req.body; - - await friendshipService.endFriendship(hashedUserId, friendUserId); + await friendshipService.endFriendship(hashedUserId, value.friendUserId); res.status(200).json({ message: 'Friendship ended successfully' }); } catch (error) { console.error('Error in endFriendship:', error); @@ -15,11 +21,16 @@ const friendshipController = { }, async acceptFriendship(req, res) { + const schema = Joi.object({ + friendUserId: Joi.string().required() + }); + const { error, value } = schema.validate(req.body); + if (error) { + return res.status(400).json({ error: error.details[0].message }); + } try { const { userid: hashedUserId } = req.headers; - const { friendUserId } = req.body; - - await friendshipService.acceptFriendship(hashedUserId, friendUserId); + await friendshipService.acceptFriendship(hashedUserId, value.friendUserId); res.status(200).json({ message: 'Friendship accepted successfully' }); } catch (error) { console.error('Error in acceptFriendship:', error); @@ -28,11 +39,16 @@ const friendshipController = { }, async rejectFriendship(req, res) { + const schema = Joi.object({ + friendUserId: Joi.string().required() + }); + const { error, value } = schema.validate(req.body); + if (error) { + return res.status(400).json({ error: error.details[0].message }); + } try { const { userid: hashedUserId } = req.headers; - const { friendUserId } = req.body; - - await friendshipService.rejectFriendship(hashedUserId, friendUserId); + await friendshipService.rejectFriendship(hashedUserId, value.friendUserId); res.status(200).json({ message: 'Friendship rejected successfully' }); } catch (error) { console.error('Error in rejectFriendship:', error); @@ -41,10 +57,16 @@ const friendshipController = { }, async withdrawRequest(req, res) { + const schema = Joi.object({ + friendUserId: Joi.string().required() + }); + const { error, value } = schema.validate(req.body); + if (error) { + return res.status(400).json({ error: error.details[0].message }); + } try { const { userid: hashedUserId } = req.headers; - const { friendUserId } = req.body; - await friendshipService.withdrawRequest(hashedUserId, friendUserId); + await friendshipService.withdrawRequest(hashedUserId, value.friendUserId); res.status(200).json({ message: 'Friendship request withdrawn successfully' }); } catch (error) { console.error('Error in withdrawRequest:', error); diff --git a/backend/controllers/navigationController.js b/backend/controllers/navigationController.js index ba36fd8..8547391 100644 --- a/backend/controllers/navigationController.js +++ b/backend/controllers/navigationController.js @@ -80,7 +80,8 @@ const menuStructure = { visible: ["over12"], action: "openMultiChat", view: "window", - class: "multiChatWindow" + class: "multiChatDialog", + icon: "multichat24.png" }, randomChat: { visible: ["over12"], diff --git a/backend/controllers/settingsController.js b/backend/controllers/settingsController.js index 5aad797..8255b47 100644 --- a/backend/controllers/settingsController.js +++ b/backend/controllers/settingsController.js @@ -1,4 +1,5 @@ import settingsService from '../services/settingsService.js'; +import Joi from 'joi'; class SettingsController { async filterSettings(req, res) { @@ -13,9 +14,17 @@ class SettingsController { } async updateSetting(req, res) { - const { userid, settingId, value } = req.body; + const schema = Joi.object({ + userid: Joi.string().required(), + settingId: Joi.number().integer().required(), + value: Joi.any().required() + }); + const { error, value } = schema.validate(req.body); + if (error) { + return res.status(400).json({ error: error.details[0].message }); + } try { - await settingsService.updateSetting(userid, settingId, value); + await settingsService.updateSetting(value.userid, value.settingId, value.value); res.status(200).json({ message: 'Setting updated successfully' }); } catch (error) { console.error('Error updating user setting:', error); @@ -68,8 +77,16 @@ class SettingsController { } async setAccountSettings(req, res) { + const schema = Joi.object({ + userId: Joi.string().required(), + settings: Joi.object().required() + }); + const { error, value } = schema.validate(req.body); + if (error) { + return res.status(400).json({ error: error.details[0].message }); + } try { - await settingsService.setAccountSettings(req.body); + await settingsService.setAccountSettings(value); res.status(200).json({ message: 'Account settings updated successfully' }); } catch (error) { console.error('Error updating account settings:', error); @@ -100,10 +117,16 @@ class SettingsController { } async addInterest(req, res) { + const schema = Joi.object({ + name: Joi.string().min(1).max(255).required() + }); + const { error, value } = schema.validate(req.body); + if (error) { + return res.status(400).json({ error: error.details[0].message }); + } try { const { userid: userId } = req.headers; - const { name } = req.body; - const interest = await settingsService.addInterest(userId, name); + const interest = await settingsService.addInterest(userId, value.name); res.status(200).json({ interest }); } catch (error) { console.error('Error adding interest:', error); @@ -112,10 +135,16 @@ class SettingsController { } async addUserInterest(req, res) { + const schema = Joi.object({ + interestid: Joi.number().integer().required() + }); + const { error, value } = schema.validate(req.body); + if (error) { + return res.status(400).json({ error: error.details[0].message }); + } try { const { userid: userId } = req.headers; - const { interestid: interestId } = req.body; - await settingsService.addUserInterest(userId, interestId); + await settingsService.addUserInterest(userId, value.interestid); res.status(200).json({ message: 'User interest added successfully' }); } catch (error) { console.error('Error adding user interest:', error); diff --git a/backend/routers/adminRouter.js b/backend/routers/adminRouter.js index d7396b6..373935b 100644 --- a/backend/routers/adminRouter.js +++ b/backend/routers/adminRouter.js @@ -12,6 +12,7 @@ router.get('/chat/gender-restrictions', authenticate, adminController.getGenderR router.get('/chat/user-rights', authenticate, adminController.getUserRights); router.get('/chat/rooms', authenticate, adminController.getRooms); router.post('/chat/rooms', authenticate, adminController.createRoom); +router.put('/chat/rooms/:id', authenticate, adminController.updateRoom); router.delete('/chat/rooms/:id', authenticate, adminController.deleteRoom); router.get('/interests/open', authenticate, adminController.getOpenInterests); diff --git a/backend/routers/chatRouter.js b/backend/routers/chatRouter.js index 12513d8..9d44ffc 100644 --- a/backend/routers/chatRouter.js +++ b/backend/routers/chatRouter.js @@ -14,5 +14,6 @@ router.post('/exit', chatController.removeUser); router.post('/initOneToOne', authenticate, chatController.initOneToOne); router.post('/oneToOne/sendMessage', authenticate, chatController.sendOneToOneMessage); // Neue Route zum Senden einer Nachricht router.get('/oneToOne/messageHistory', authenticate, chatController.getOneToOneMessageHistory); // Neue Route zum Abrufen der Nachrichtengeschichte +router.get('/rooms', chatController.getRoomList); export default router; diff --git a/backend/services/adminService.js b/backend/services/adminService.js index eb56554..b4f2fab 100644 --- a/backend/services/adminService.js +++ b/backend/services/adminService.js @@ -308,7 +308,20 @@ class AdminService { } async getRooms() { + // Only return necessary fields to the frontend return await Room.findAll({ + attributes: [ + 'id', + 'title', + 'roomTypeId', + 'isPublic', + 'genderRestrictionId', + 'minAge', + 'maxAge', + 'friendsOfOwnerOnly', + 'requiredUserRightId', + 'password' // only if needed for editing, otherwise remove + ], include: [ { model: RoomType, as: 'roomType' }, { model: UserParamValue, as: 'genderRestriction' }, @@ -316,6 +329,13 @@ class AdminService { }); } + async updateRoom(id, data) { + const room = await Room.findByPk(id); + if (!room) throw new Error('Room not found'); + await room.update(data); + return room; + } + async createRoom(data) { return await Room.create(data); } diff --git a/backend/services/chatService.js b/backend/services/chatService.js index b1516ef..a983090 100644 --- a/backend/services/chatService.js +++ b/backend/services/chatService.js @@ -132,6 +132,22 @@ class ChatService { (chat.user1Id === user2HashId && chat.user2Id === user1HashId) ); } + + async getRoomList() { + // Nur öffentliche Räume, keine sensiblen Felder + const { default: Room } = await import('../models/chat/room.js'); + const { default: RoomType } = await import('../models/chat/room_type.js'); + return Room.findAll({ + attributes: [ + 'id', 'title', 'roomTypeId', 'isPublic', 'genderRestrictionId', + 'minAge', 'maxAge', 'friendsOfOwnerOnly', 'requiredUserRightId' + ], + where: { isPublic: true }, + include: [ + { model: RoomType, as: 'roomType' } + ] + }); + } } export default new ChatService(); diff --git a/backend/services/chatTcpBridge.js b/backend/services/chatTcpBridge.js new file mode 100644 index 0000000..85f9448 --- /dev/null +++ b/backend/services/chatTcpBridge.js @@ -0,0 +1,307 @@ +import net from 'net'; +import fs from 'fs'; +import path from 'path'; + +const DEFAULT_CONFIG = { host: 'localhost', port: 1235 }; + +function loadBridgeConfig() { + try { + const cfgPath = path.resolve(process.cwd(), 'backend/config/chatBridge.json'); + if (fs.existsSync(cfgPath)) { + const raw = fs.readFileSync(cfgPath, 'utf-8'); + const json = JSON.parse(raw); + return { ...DEFAULT_CONFIG, ...json }; + } + } catch (e) { + console.warn('Failed to load chatBridge.json, using defaults:', e.message); + } + return { ...DEFAULT_CONFIG }; +} + +export default class ChatTcpBridge { + constructor(ioSocket, user) { + this.ioSocket = ioSocket; + this.user = user || {}; + this.token = null; + this.tcp = null; + this.buffer = ''; + this.config = loadBridgeConfig(); + this.pending = []; + this.objStart = -1; + this.depthCurly = 0; + this.depthSquare = 0; + this.inString = false; + this.escapeNext = false; + } + + connect(initialRoomName = '') { + if (this.tcp) return; + const { host, port } = this.config; + + this.ioEmitStatus('connecting', `${host}:${port}`); + console.log(`[ChatBridge] Connecting to ${host}:${port}`); + this.tcp = new net.Socket(); + + this.tcp.on('connect', () => { + this.ioEmitStatus('connected', `${host}:${port}`); + console.log(`[ChatBridge] Connected to ${host}:${port}`); + // Erstnachricht: init + const initPayload = { + type: 'init', + name: this.user.username || '', + room: initialRoomName || '', + }; + this.sendRaw(initPayload); + }); + + this.tcp.on('data', (data) => { + this.processChunk(data.toString('utf-8')); + }); + + this.tcp.on('error', (err) => { + this.ioEmitStatus('error', err.message); + console.error('[ChatBridge] Error:', err.message); + }); + + this.tcp.on('close', () => { + this.ioEmitStatus('disconnected'); + console.warn('[ChatBridge] Disconnected'); + }); + + this.tcp.connect(port, host); + } + + handleIncomingLine(line) { + try { + const obj = JSON.parse(line); + this.handleIncoming(obj); + } catch (e) { + this.ioSocket.emit('chat:incoming', { type: 'system', text: line }); + } + } + + processChunk(chunk) { + if (!chunk) return; + for (let i = 0; i < chunk.length; i++) { + const ch = chunk[i]; + this.buffer += ch; + if (this.escapeNext) { + this.escapeNext = false; + continue; + } + if (this.inString) { + if (ch === '\\') { this.escapeNext = true; } + else if (ch === '"') { this.inString = false; } + continue; + } + if (ch === '"') { this.inString = true; continue; } + if (ch === '{') { this.depthCurly++; if (this.objStart === -1) this.objStart = this.buffer.length - 1; } + else if (ch === '}') { this.depthCurly--; } + else if (ch === '[') { this.depthSquare++; } + else if (ch === ']') { this.depthSquare--; } + + // Wenn ein komplettes Objekt abgeschlossen ist (außerhalb von Strings und beide Tiefen 0) + if (this.objStart !== -1 && this.depthCurly === 0) { + const jsonStr = this.buffer.slice(this.objStart, this.buffer.length); + // Alles vor objStart (falls vorhanden) rausfiltern + const prefix = this.buffer.slice(0, this.objStart); + if (prefix.trim()) { + // Unerwarteter Text, als System ausgeben + this.ioSocket.emit('chat:incoming', { type: 'system', text: prefix.trim() }); + } + this.buffer = ''; + this.objStart = -1; + this.parseAndHandle(jsonStr); + } + } + } + + parseAndHandle(jsonStr) { + try { + const obj = JSON.parse(jsonStr); + // Forward raw parsed inbound message to browser console + this.ioSocket.emit('chat:debug', { dir: 'in', payload: obj }); + this.handleIncoming(obj); + } catch (e) { + // Wenn mehrere Objekte direkt aneinander hängen (}{), versuchen sie zu splitten + const parts = jsonStr.split('}{').map((p, idx, arr) => (idx === 0 ? p + '}' : (idx === arr.length - 1 ? '{' + p : '{' + p + '}'))); + if (parts.length > 1) { + parts.forEach(p => { + try { + const o = JSON.parse(p); + this.ioSocket.emit('chat:debug', { dir: 'in', payload: o }); + this.handleIncoming(o); + } catch (_) { + this.ioSocket.emit('chat:debug', { dir: 'in', raw: p }); + this.ioSocket.emit('chat:incoming', { type: 'system', text: p }); + } + }); + } else { + this.ioSocket.emit('chat:debug', { dir: 'in', raw: jsonStr }); + this.ioSocket.emit('chat:incoming', { type: 'system', text: jsonStr }); + } + } + } + + handleIncoming(obj) { + if (!obj) return; + // Map numeric type codes + if (typeof obj.type === 'number') { + switch (obj.type) { + case 1: // token + if (typeof obj.message === 'string' && obj.message) { + this.token = obj.message; + this.ioSocket.emit('chat:incoming', { type: 'system', text: 'Token received' }); + const actions = this.pending; this.pending = []; + actions.forEach(fn => { try { fn(); } catch (_) { } }); + return; + } + break; + case 3: // room list + if (Array.isArray(obj.message)) { + const names = obj.message.map(r => r.name).filter(Boolean).join(', '); + this.ioSocket.emit('chat:incoming', { type: 'system', text: names ? `Rooms: ${names}` : 'Rooms updated' }); + return; + } + break; + case 5: { + // generic server event (room entered, color changes, etc.) + const msg = obj.message; + if (!msg) break; + if (typeof msg === 'string') { + if (msg === 'room_entered') { + const to = obj.to || obj.name || obj.room || ''; + this.ioSocket.emit('chat:incoming', { type: 'system', code: 'room_entered', tr: 'room_entered', to }); + return; + } + if (msg === 'color_changed' || msg === 'user_color_changed') { + this.ioSocket.emit('chat:incoming', { + type: 'system', + code: msg, + color: obj.color, + userName: obj.userName || '' + }); + return; + } + // unknown message string; fallthrough to text echo + this.ioSocket.emit('chat:incoming', { type: 'system', text: msg }); + return; + } + if (typeof msg === 'object') { + const tr = msg.tr || 'room_entered'; + const to = msg.to || msg.name || msg.room || ''; + this.ioSocket.emit('chat:incoming', { type: 'system', code: tr, tr, to }); + return; + } + break; + } + case 6: { + // scream + const userName = obj.userName || obj.user || obj.name || ''; + const message = obj.message || ''; + this.ioSocket.emit('chat:incoming', { + type: 'scream', + userName, + message, + color: obj.color || null + }); + return; + } + default: + break; + } + } + if (obj.type === 'token' && obj.message) { + this.token = obj.message; + this.ioSocket.emit('chat:incoming', { type: 'system', text: 'Token received' }); + // Ausstehende Aktionen jetzt mit Token senden + const actions = this.pending; + this.pending = []; + actions.forEach(fn => { + try { fn(); } catch (_) { } + }); + return; + } + // Normalize generic messages + if (typeof obj.message === 'string' && obj.message && (obj.userName || obj.user || obj.name)) { + this.ioSocket.emit('chat:incoming', { + type: 'message', + message: obj.message, + userName: obj.userName || obj.user || obj.name, + color: obj.color || null + }); + return; + } + // As system fallback + this.ioSocket.emit('chat:incoming', { type: 'system', text: JSON.stringify(obj) }); + } + + sendRaw(obj) { + if (!this.tcp) return; + try { + const str = JSON.stringify(obj) + '\n'; + // Forward outbound payload to browser console + this.ioSocket.emit('chat:debug', { dir: 'out', payload: obj }); + this.tcp.write(str); + } catch (e) { + this.ioEmitStatus('error', e.message); + } + } + + sendWithToken(obj) { + if (this.token) obj.token = this.token; + this.sendRaw(obj); + } + + withTokenOrQueue(fn) { + if (this.token) { + fn(); + } else { + this.pending.push(fn); + } + } + + joinRoom(roomName, password = '') { + // Prefer keys name/room if server expects them for room switch; fallback to newroom if required elsewhere + this.withTokenOrQueue(() => this.sendWithToken({ type: 'join', room: roomName, name: this.user.username || '', password })); + } + + sendMessage(text) { + this.withTokenOrQueue(() => this.sendWithToken({ type: 'message', message: text, userName: this.user.username })); + } + + scream(text) { + this.withTokenOrQueue(() => this.sendWithToken({ type: 'scream', message: text })); + } + + doAction(text) { + this.withTokenOrQueue(() => this.sendWithToken({ type: 'do', message: text })); + } + + dice(expr) { + this.withTokenOrQueue(() => this.sendWithToken({ type: 'dice', message: expr || '' })); + } + + color(hex) { + let value = typeof hex === 'string' ? hex.trim() : ''; + if (!value) return; + if (!value.startsWith('#')) value = '#' + value; + // Normalize 3-digit to 6-digit when possible + const m3 = /^#([0-9a-fA-F]{3})$/.exec(value); + if (m3) { + value = '#' + m3[1].split('').map(c => c + c).join(''); + } + const m6 = /^#([0-9a-fA-F]{6})$/.exec(value); + if (!m6) return; + this.withTokenOrQueue(() => this.sendWithToken({ type: 'color', value })); + } + + close() { + try { this.tcp?.destroy(); } catch (_) { } + this.tcp = null; + } + + ioEmitStatus(type, detail) { + this.ioSocket.emit('chat:status', { type, detail }); + } +} diff --git a/frontend/public/images/icons/activity.png b/frontend/public/images/icons/activity.png new file mode 100644 index 0000000..8edc831 Binary files /dev/null and b/frontend/public/images/icons/activity.png differ diff --git a/frontend/public/images/icons/colorpicker.png b/frontend/public/images/icons/colorpicker.png new file mode 100644 index 0000000..e523803 Binary files /dev/null and b/frontend/public/images/icons/colorpicker.png differ diff --git a/frontend/public/images/icons/multichat.png b/frontend/public/images/icons/multichat.png new file mode 100644 index 0000000..ba2964d Binary files /dev/null and b/frontend/public/images/icons/multichat.png differ diff --git a/frontend/public/images/icons/multichat16.png b/frontend/public/images/icons/multichat16.png new file mode 100644 index 0000000..7b8871d Binary files /dev/null and b/frontend/public/images/icons/multichat16.png differ diff --git a/frontend/public/images/icons/multichat24.png b/frontend/public/images/icons/multichat24.png new file mode 100644 index 0000000..5b49e60 Binary files /dev/null and b/frontend/public/images/icons/multichat24.png differ diff --git a/frontend/public/images/icons/scream.png b/frontend/public/images/icons/scream.png new file mode 100644 index 0000000..d387fdb Binary files /dev/null and b/frontend/public/images/icons/scream.png differ diff --git a/frontend/public/images/icons/sendmessage.png b/frontend/public/images/icons/sendmessage.png new file mode 100644 index 0000000..c8d4fd5 Binary files /dev/null and b/frontend/public/images/icons/sendmessage.png differ diff --git a/frontend/src/App.vue b/frontend/src/App.vue index 174cbdd..23298ca 100644 --- a/frontend/src/App.vue +++ b/frontend/src/App.vue @@ -16,6 +16,7 @@ + @@ -37,6 +38,7 @@ import ErrorDialog from './dialogues/standard/ErrorDialog.vue'; import ImprintDialog from './dialogues/standard/ImprintDialog.vue'; import ShowImageDialog from './dialogues/socialnetwork/ShowImageDialog.vue'; import MessageDialog from './dialogues/standard/MessageDialog.vue'; +import MultiChatDialog from './dialogues/chat/MultiChatDialog.vue'; export default { name: 'App', @@ -63,6 +65,7 @@ export default { ImprintDialog, ShowImageDialog, MessageDialog, + MultiChatDialog, }, created() { this.$i18n.locale = this.$store.getters.language; diff --git a/frontend/src/api/chatApi.js b/frontend/src/api/chatApi.js new file mode 100644 index 0000000..68ea439 --- /dev/null +++ b/frontend/src/api/chatApi.js @@ -0,0 +1,6 @@ +import apiClient from "@/utils/axios.js"; + +export const fetchPublicRooms = async () => { + const response = await apiClient.get("/api/chat/rooms"); + return response.data; // expecting array of { id, title, ... } +}; diff --git a/frontend/src/components/AppFooter.vue b/frontend/src/components/AppFooter.vue index 399e0c4..b76c342 100644 --- a/frontend/src/components/AppFooter.vue +++ b/frontend/src/components/AppFooter.vue @@ -29,10 +29,14 @@ export default { ...mapState(['daemonSocket']), }, mounted() { - this.daemonSocket.addEventListener('workerStatus', () => { console.log('----'); }); + if (this.daemonSocket && this.daemonSocket.addEventListener) { + this.daemonSocket.addEventListener('workerStatus', this.handleDaemonMessage); + } }, beforeUnmount() { - this.daemonSocket.removeEventListener('workerStatus', this.handleDaemonMessage); + if (this.daemonSocket && this.daemonSocket.removeEventListener) { + this.daemonSocket.removeEventListener('workerStatus', this.handleDaemonMessage); + } }, methods: { openImprintDialog() { @@ -48,7 +52,9 @@ export default { this.$store.dispatch('dialogs/toggleDialogMinimize', dialogName); }, async showFalukantDaemonStatus() { - this.daemonSocket.send('{"event": "getWorkerStatus"}'); + if (this.daemonSocket && this.daemonSocket.send) { + this.daemonSocket.send('{"event": "getWorkerStatus"}'); + } }, handleDaemonMessage(event) { const status = JSON.parse(event.data); diff --git a/frontend/src/components/AppNavigation.vue b/frontend/src/components/AppNavigation.vue index 1a92c9d..3434cd3 100644 --- a/frontend/src/components/AppNavigation.vue +++ b/frontend/src/components/AppNavigation.vue @@ -110,6 +110,7 @@ import { createApp } from 'vue'; import apiClient from '@/utils/axios.js'; import RandomChatDialog from '../dialogues/chat/RandomChatDialog.vue'; +import MultiChatDialog from '../dialogues/chat/MultiChatDialog.vue'; // Wichtig: die zentrale Instanzen importieren import store from '@/store'; @@ -119,7 +120,8 @@ import i18n from '@/i18n'; export default { name: 'AppNavigation', components: { - RandomChatDialog + RandomChatDialog, + MultiChatDialog }, data() { return { @@ -160,6 +162,22 @@ export default { methods: { ...mapActions(['loadMenu', 'logout']), + openMultiChat() { + // Räume können später dynamisch geladen werden, hier als Platzhalter ein Beispiel: + const exampleRooms = [ + { id: 1, title: 'Allgemein' }, + { id: 2, title: 'Rollenspiel' } + ]; + const ref = this.$root.$refs.multiChatDialog; + if (ref && typeof ref.open === 'function') { + ref.open(exampleRooms); + } else if (ref?.$refs?.dialog && typeof ref.$refs.dialog.open === 'function') { + ref.$refs.dialog.open(); + } else { + console.error('MultiChatDialog nicht bereit oder ohne open()'); + } + }, + async fetchForums() { try { const res = await apiClient.get('/api/forum'); @@ -192,8 +210,6 @@ export default { // Datei erstellen und ans body anhängen const container = document.createElement('div'); document.body.appendChild(container); - // Programmatisch ein neues App-Instance randomChatauen, mit Store, Router & i18n - this.$root.$refs.randomChatDialog.open(contact); }, /** @@ -211,7 +227,19 @@ export default { // 2) view → Dialog/Window if (item.view) { - this.$root.$refs[item.class].open(); + const dialogRef = this.$root.$refs[item.class]; + if (!dialogRef) { + console.error(`Dialog-Ref '${item.class}' nicht gefunden! Bitte prüfe Ref und Menü-Konfiguration.`); + return; + } + // Robust öffnen: erst open(), sonst auf inneres DialogWidget zurückgreifen + if (typeof dialogRef.open === 'function') { + dialogRef.open(); + } else if (dialogRef.$refs?.dialog && typeof dialogRef.$refs.dialog.open === 'function') { + dialogRef.$refs.dialog.open(); + } else { + console.error(`Dialog '${item.class}' gefunden, aber keine open()-Methode verfügbar.`); + } return; } diff --git a/frontend/src/dialogues/auth/PasswordResetDialog.vue b/frontend/src/dialogues/auth/PasswordResetDialog.vue index a4d15b6..8b99465 100644 --- a/frontend/src/dialogues/auth/PasswordResetDialog.vue +++ b/frontend/src/dialogues/auth/PasswordResetDialog.vue @@ -1,5 +1,5 @@