Add password reset localization and chat configuration

- Implemented German and English localization for password reset functionality.
- Added WebSocket URL resolution logic in chat services to support various environments and configurations.
- Created centralized chat configuration for event keys and payload mappings.
- Developed RoomsView component for admin chat room management, including create, edit, and delete functionalities.
This commit is contained in:
Torsten Schulz (local)
2025-08-18 07:44:56 +02:00
parent 23f698d8fd
commit 19ee6ba0a1
50 changed files with 3117 additions and 359 deletions

View File

@@ -0,0 +1,4 @@
{
"host": "localhost",
"port": 1235
}

View File

@@ -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) { async createRoom(req, res) {
try { 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); res.status(201).json(room);
} catch (error) { } catch (error) {
console.log(error); console.log(error);
@@ -189,6 +240,10 @@ class AdminController {
async deleteRoom(req, res) { async deleteRoom(req, res) {
try { 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); await AdminService.deleteRoom(req.params.id);
res.sendStatus(204); res.sendStatus(204);
} catch (error) { } catch (error) {

View File

@@ -1,4 +1,5 @@
import chatService from '../services/chatService.js'; import chatService from '../services/chatService.js';
import Joi from 'joi';
class ChatController { class ChatController {
constructor() { constructor() {
@@ -11,12 +12,20 @@ class ChatController {
this.initOneToOne = this.initOneToOne.bind(this); this.initOneToOne = this.initOneToOne.bind(this);
this.sendOneToOneMessage = this.sendOneToOneMessage.bind(this); this.sendOneToOneMessage = this.sendOneToOneMessage.bind(this);
this.getOneToOneMessageHistory = this.getOneToOneMessageHistory.bind(this); this.getOneToOneMessageHistory = this.getOneToOneMessageHistory.bind(this);
this.getRoomList = this.getRoomList.bind(this);
} }
async getMessages(req, res) { 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 { try {
const messages = await chatService.getMessages(to, from); const messages = await chatService.getMessages(value.to, value.from);
res.status(200).json(messages); res.status(200).json(messages);
} catch (error) { } catch (error) {
res.status(500).json({ error: error.message }); res.status(500).json({ error: error.message });
@@ -24,9 +33,17 @@ class ChatController {
} }
async findRandomChatMatch(req, res) { 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 { try {
const match = await chatService.findMatch(genders, age, id); const match = await chatService.findMatch(value.genders, value.age, value.id);
if (match) { if (match) {
res.status(200).json({ status: 'matched', user: match }); res.status(200).json({ status: 'matched', user: match });
} else { } else {
@@ -38,9 +55,16 @@ class ChatController {
} }
async registerUser(req, res) { 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 { try {
const userId = await chatService.registerUser(gender, age); const userId = await chatService.registerUser(value.gender, value.age);
res.status(200).json({ id: userId }); res.status(200).json({ id: userId });
} catch (error) { } catch (error) {
res.status(500).json({ error: error.message }); res.status(500).json({ error: error.message });
@@ -48,9 +72,17 @@ class ChatController {
} }
async sendMessage(req, res) { 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 { 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); res.status(200).json(message);
} catch (error) { } catch (error) {
res.status(500).json({ error: error.message }); res.status(500).json({ error: error.message });
@@ -58,9 +90,15 @@ class ChatController {
} }
async removeUser(req, res) { 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 { try {
await chatService.removeUser(id); await chatService.removeUser(value.id);
res.sendStatus(200); res.sendStatus(200);
} catch (error) { } catch (error) {
res.status(500).json({ error: error.message }); res.status(500).json({ error: error.message });
@@ -68,9 +106,15 @@ class ChatController {
} }
async stopChat(req, res) { 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 { try {
await chatService.endChat(id); await chatService.endChat(value.id);
res.sendStatus(200); res.sendStatus(200);
} catch (error) { } catch (error) {
res.status(500).json({ error: error.message }); res.status(500).json({ error: error.message });
@@ -78,10 +122,16 @@ class ChatController {
} }
async initOneToOne(req, res) { 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 { userid: hashedUserId } = req.headers;
const { partnerHashId } = req.body;
try { 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.' }); res.status(200).json({ message: 'One-to-one chat initialization is pending implementation.' });
} catch (error) { } catch (error) {
res.status(500).json({ error: error.message }); res.status(500).json({ error: error.message });
@@ -89,9 +139,17 @@ class ChatController {
} }
async sendOneToOneMessage(req, res) { 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 { try {
await chatService.sendOneToOneMessage(user1HashId, user2HashId, message); await chatService.sendOneToOneMessage(value.user1HashId, value.user2HashId, value.message);
res.status(200).json({ status: 'message sent' }); res.status(200).json({ status: 'message sent' });
} catch (error) { } catch (error) {
res.status(500).json({ error: error.message }); res.status(500).json({ error: error.message });
@@ -107,6 +165,16 @@ class ChatController {
res.status(500).json({ error: error.message }); 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; export default ChatController;

View File

@@ -1,4 +1,5 @@
import ContactService from '../services/ContactService.js'; import ContactService from '../services/ContactService.js';
import Joi from 'joi';
class ContactController { class ContactController {
constructor() { constructor() {
@@ -6,9 +7,18 @@ class ContactController {
} }
async addContactMessage(req, res) { 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 { try {
const { email, name, message, acceptDataSave } = req.body; await ContactService.addContactMessage(value.email, value.name, value.message, value.acceptDataSave);
await ContactService.addContactMessage(email, name, message, acceptDataSave);
res.status(200).json({ status: 'ok' }); res.status(200).json({ status: 'ok' });
} catch (error) { } catch (error) {
res.status(409).json({ error: error.message }); res.status(409).json({ error: error.message });

View File

@@ -1,11 +1,19 @@
import forumService from '../services/forumService.js'; import forumService from '../services/forumService.js';
import Joi from 'joi';
const forumController = { const forumController = {
async createForum(req, res) { 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 { try {
const { userid: userId } = req.headers; const { userid: userId } = req.headers;
const { name, permissions } = req.body; const forum = await forumService.createForum(userId, value.name, value.permissions);
const forum = await forumService.createForum(userId, name, permissions);
res.status(201).json(forum); res.status(201).json(forum);
} catch (error) { } catch (error) {
console.error('Error in createForum:', error); console.error('Error in createForum:', error);
@@ -49,10 +57,18 @@ const forumController = {
}, },
async createTopic(req, res) { 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 { try {
const { userid: userId } = req.headers; const { userid: userId } = req.headers;
const { forumId, title, content } = req.body; const result = await forumService.createTopic(userId, value.forumId, value.title, value.content);
const result = await forumService.createTopic(userId, forumId, title, content);
res.status(201).json(result); res.status(201).json(result);
} catch (error) { } catch (error) {
console.error('Error in createTopic:', error); console.error('Error in createTopic:', error);
@@ -73,11 +89,17 @@ const forumController = {
}, },
async addMessage(req, res) { 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 { try {
const { userid: userId } = req.headers; const { userid: userId } = req.headers;
const { id: topicId } = req.params; const { id: topicId } = req.params;
const { content } = req.body; const result = await forumService.addMessage(userId, topicId, value.content);
const result = await forumService.addMessage(userId, topicId, content);
res.status(201).json(result); res.status(201).json(result);
} catch (error) { } catch (error) {
console.error('Error in addMessage:', error); console.error('Error in addMessage:', error);

View File

@@ -1,12 +1,18 @@
import friendshipService from '../services/friendshipService.js'; import friendshipService from '../services/friendshipService.js';
import Joi from 'joi';
const friendshipController = { const friendshipController = {
async endFriendship(req, res) { 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 { try {
const { userid: hashedUserId } = req.headers; const { userid: hashedUserId } = req.headers;
const { friendUserId } = req.body; await friendshipService.endFriendship(hashedUserId, value.friendUserId);
await friendshipService.endFriendship(hashedUserId, friendUserId);
res.status(200).json({ message: 'Friendship ended successfully' }); res.status(200).json({ message: 'Friendship ended successfully' });
} catch (error) { } catch (error) {
console.error('Error in endFriendship:', error); console.error('Error in endFriendship:', error);
@@ -15,11 +21,16 @@ const friendshipController = {
}, },
async acceptFriendship(req, res) { 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 { try {
const { userid: hashedUserId } = req.headers; const { userid: hashedUserId } = req.headers;
const { friendUserId } = req.body; await friendshipService.acceptFriendship(hashedUserId, value.friendUserId);
await friendshipService.acceptFriendship(hashedUserId, friendUserId);
res.status(200).json({ message: 'Friendship accepted successfully' }); res.status(200).json({ message: 'Friendship accepted successfully' });
} catch (error) { } catch (error) {
console.error('Error in acceptFriendship:', error); console.error('Error in acceptFriendship:', error);
@@ -28,11 +39,16 @@ const friendshipController = {
}, },
async rejectFriendship(req, res) { 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 { try {
const { userid: hashedUserId } = req.headers; const { userid: hashedUserId } = req.headers;
const { friendUserId } = req.body; await friendshipService.rejectFriendship(hashedUserId, value.friendUserId);
await friendshipService.rejectFriendship(hashedUserId, friendUserId);
res.status(200).json({ message: 'Friendship rejected successfully' }); res.status(200).json({ message: 'Friendship rejected successfully' });
} catch (error) { } catch (error) {
console.error('Error in rejectFriendship:', error); console.error('Error in rejectFriendship:', error);
@@ -41,10 +57,16 @@ const friendshipController = {
}, },
async withdrawRequest(req, res) { 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 { try {
const { userid: hashedUserId } = req.headers; const { userid: hashedUserId } = req.headers;
const { friendUserId } = req.body; await friendshipService.withdrawRequest(hashedUserId, value.friendUserId);
await friendshipService.withdrawRequest(hashedUserId, friendUserId);
res.status(200).json({ message: 'Friendship request withdrawn successfully' }); res.status(200).json({ message: 'Friendship request withdrawn successfully' });
} catch (error) { } catch (error) {
console.error('Error in withdrawRequest:', error); console.error('Error in withdrawRequest:', error);

View File

@@ -80,7 +80,8 @@ const menuStructure = {
visible: ["over12"], visible: ["over12"],
action: "openMultiChat", action: "openMultiChat",
view: "window", view: "window",
class: "multiChatWindow" class: "multiChatDialog",
icon: "multichat24.png"
}, },
randomChat: { randomChat: {
visible: ["over12"], visible: ["over12"],

View File

@@ -1,4 +1,5 @@
import settingsService from '../services/settingsService.js'; import settingsService from '../services/settingsService.js';
import Joi from 'joi';
class SettingsController { class SettingsController {
async filterSettings(req, res) { async filterSettings(req, res) {
@@ -13,9 +14,17 @@ class SettingsController {
} }
async updateSetting(req, res) { 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 { try {
await settingsService.updateSetting(userid, settingId, value); await settingsService.updateSetting(value.userid, value.settingId, value.value);
res.status(200).json({ message: 'Setting updated successfully' }); res.status(200).json({ message: 'Setting updated successfully' });
} catch (error) { } catch (error) {
console.error('Error updating user setting:', error); console.error('Error updating user setting:', error);
@@ -68,8 +77,16 @@ class SettingsController {
} }
async setAccountSettings(req, res) { 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 { try {
await settingsService.setAccountSettings(req.body); await settingsService.setAccountSettings(value);
res.status(200).json({ message: 'Account settings updated successfully' }); res.status(200).json({ message: 'Account settings updated successfully' });
} catch (error) { } catch (error) {
console.error('Error updating account settings:', error); console.error('Error updating account settings:', error);
@@ -100,10 +117,16 @@ class SettingsController {
} }
async addInterest(req, res) { 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 { try {
const { userid: userId } = req.headers; const { userid: userId } = req.headers;
const { name } = req.body; const interest = await settingsService.addInterest(userId, value.name);
const interest = await settingsService.addInterest(userId, name);
res.status(200).json({ interest }); res.status(200).json({ interest });
} catch (error) { } catch (error) {
console.error('Error adding interest:', error); console.error('Error adding interest:', error);
@@ -112,10 +135,16 @@ class SettingsController {
} }
async addUserInterest(req, res) { 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 { try {
const { userid: userId } = req.headers; const { userid: userId } = req.headers;
const { interestid: interestId } = req.body; await settingsService.addUserInterest(userId, value.interestid);
await settingsService.addUserInterest(userId, interestId);
res.status(200).json({ message: 'User interest added successfully' }); res.status(200).json({ message: 'User interest added successfully' });
} catch (error) { } catch (error) {
console.error('Error adding user interest:', error); console.error('Error adding user interest:', error);

View File

@@ -12,6 +12,7 @@ router.get('/chat/gender-restrictions', authenticate, adminController.getGenderR
router.get('/chat/user-rights', authenticate, adminController.getUserRights); router.get('/chat/user-rights', authenticate, adminController.getUserRights);
router.get('/chat/rooms', authenticate, adminController.getRooms); router.get('/chat/rooms', authenticate, adminController.getRooms);
router.post('/chat/rooms', authenticate, adminController.createRoom); router.post('/chat/rooms', authenticate, adminController.createRoom);
router.put('/chat/rooms/:id', authenticate, adminController.updateRoom);
router.delete('/chat/rooms/:id', authenticate, adminController.deleteRoom); router.delete('/chat/rooms/:id', authenticate, adminController.deleteRoom);
router.get('/interests/open', authenticate, adminController.getOpenInterests); router.get('/interests/open', authenticate, adminController.getOpenInterests);

View File

@@ -14,5 +14,6 @@ router.post('/exit', chatController.removeUser);
router.post('/initOneToOne', authenticate, chatController.initOneToOne); router.post('/initOneToOne', authenticate, chatController.initOneToOne);
router.post('/oneToOne/sendMessage', authenticate, chatController.sendOneToOneMessage); // Neue Route zum Senden einer Nachricht 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('/oneToOne/messageHistory', authenticate, chatController.getOneToOneMessageHistory); // Neue Route zum Abrufen der Nachrichtengeschichte
router.get('/rooms', chatController.getRoomList);
export default router; export default router;

View File

@@ -308,7 +308,20 @@ class AdminService {
} }
async getRooms() { async getRooms() {
// Only return necessary fields to the frontend
return await Room.findAll({ return await Room.findAll({
attributes: [
'id',
'title',
'roomTypeId',
'isPublic',
'genderRestrictionId',
'minAge',
'maxAge',
'friendsOfOwnerOnly',
'requiredUserRightId',
'password' // only if needed for editing, otherwise remove
],
include: [ include: [
{ model: RoomType, as: 'roomType' }, { model: RoomType, as: 'roomType' },
{ model: UserParamValue, as: 'genderRestriction' }, { 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) { async createRoom(data) {
return await Room.create(data); return await Room.create(data);
} }

View File

@@ -132,6 +132,22 @@ class ChatService {
(chat.user1Id === user2HashId && chat.user2Id === user1HashId) (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(); export default new ChatService();

View File

@@ -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 });
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 189 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 124 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 483 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 631 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 193 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 143 KiB

View File

@@ -16,6 +16,7 @@
<ImprintDialog ref="imprintDialog" /> <ImprintDialog ref="imprintDialog" />
<ShowImageDialog ref="showImageDialog" /> <ShowImageDialog ref="showImageDialog" />
<MessageDialog ref="messageDialog" /> <MessageDialog ref="messageDialog" />
<MultiChatDialog ref="multiChatDialog" />
</div> </div>
</template> </template>
@@ -37,6 +38,7 @@ import ErrorDialog from './dialogues/standard/ErrorDialog.vue';
import ImprintDialog from './dialogues/standard/ImprintDialog.vue'; import ImprintDialog from './dialogues/standard/ImprintDialog.vue';
import ShowImageDialog from './dialogues/socialnetwork/ShowImageDialog.vue'; import ShowImageDialog from './dialogues/socialnetwork/ShowImageDialog.vue';
import MessageDialog from './dialogues/standard/MessageDialog.vue'; import MessageDialog from './dialogues/standard/MessageDialog.vue';
import MultiChatDialog from './dialogues/chat/MultiChatDialog.vue';
export default { export default {
name: 'App', name: 'App',
@@ -63,6 +65,7 @@ export default {
ImprintDialog, ImprintDialog,
ShowImageDialog, ShowImageDialog,
MessageDialog, MessageDialog,
MultiChatDialog,
}, },
created() { created() {
this.$i18n.locale = this.$store.getters.language; this.$i18n.locale = this.$store.getters.language;

View File

@@ -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, ... }
};

View File

@@ -29,10 +29,14 @@ export default {
...mapState(['daemonSocket']), ...mapState(['daemonSocket']),
}, },
mounted() { mounted() {
this.daemonSocket.addEventListener('workerStatus', () => { console.log('----'); }); if (this.daemonSocket && this.daemonSocket.addEventListener) {
this.daemonSocket.addEventListener('workerStatus', this.handleDaemonMessage);
}
}, },
beforeUnmount() { beforeUnmount() {
this.daemonSocket.removeEventListener('workerStatus', this.handleDaemonMessage); if (this.daemonSocket && this.daemonSocket.removeEventListener) {
this.daemonSocket.removeEventListener('workerStatus', this.handleDaemonMessage);
}
}, },
methods: { methods: {
openImprintDialog() { openImprintDialog() {
@@ -48,7 +52,9 @@ export default {
this.$store.dispatch('dialogs/toggleDialogMinimize', dialogName); this.$store.dispatch('dialogs/toggleDialogMinimize', dialogName);
}, },
async showFalukantDaemonStatus() { async showFalukantDaemonStatus() {
this.daemonSocket.send('{"event": "getWorkerStatus"}'); if (this.daemonSocket && this.daemonSocket.send) {
this.daemonSocket.send('{"event": "getWorkerStatus"}');
}
}, },
handleDaemonMessage(event) { handleDaemonMessage(event) {
const status = JSON.parse(event.data); const status = JSON.parse(event.data);

View File

@@ -110,6 +110,7 @@ import { createApp } from 'vue';
import apiClient from '@/utils/axios.js'; import apiClient from '@/utils/axios.js';
import RandomChatDialog from '../dialogues/chat/RandomChatDialog.vue'; import RandomChatDialog from '../dialogues/chat/RandomChatDialog.vue';
import MultiChatDialog from '../dialogues/chat/MultiChatDialog.vue';
// Wichtig: die zentrale Instanzen importieren // Wichtig: die zentrale Instanzen importieren
import store from '@/store'; import store from '@/store';
@@ -119,7 +120,8 @@ import i18n from '@/i18n';
export default { export default {
name: 'AppNavigation', name: 'AppNavigation',
components: { components: {
RandomChatDialog RandomChatDialog,
MultiChatDialog
}, },
data() { data() {
return { return {
@@ -160,6 +162,22 @@ export default {
methods: { methods: {
...mapActions(['loadMenu', 'logout']), ...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() { async fetchForums() {
try { try {
const res = await apiClient.get('/api/forum'); const res = await apiClient.get('/api/forum');
@@ -192,8 +210,6 @@ export default {
// Datei erstellen und ans body anhängen // Datei erstellen und ans body anhängen
const container = document.createElement('div'); const container = document.createElement('div');
document.body.appendChild(container); 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 // 2) view → Dialog/Window
if (item.view) { 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; return;
} }

View File

@@ -1,5 +1,5 @@
<template> <template>
<DialogWidget ref="dialog" title="passwordReset.title" :show-close=true :buttons="buttons" @close="closeDialog" name="PasswordReset"> <DialogWidget ref="dialog" title="passwordReset.title" :isTitleTranslated="true" :show-close=true :buttons="buttons" @close="closeDialog" @reset="resetPassword" name="PasswordReset">
<div> <div>
<label>{{ $t("passwordReset.email") }} <input type="email" v-model="email" required /></label> <label>{{ $t("passwordReset.email") }} <input type="email" v-model="email" required /></label>
</div> </div>
@@ -18,7 +18,7 @@ export default {
data() { data() {
return { return {
email: '', email: '',
buttons: [{ text: this.$t("passwordReset.reset") }] buttons: [{ text: 'passwordReset.reset', action: 'reset' }]
}; };
}, },
methods: { methods: {

View File

@@ -0,0 +1,148 @@
<template>
<DialogWidget ref="dialog" :title="$t('chat.multichat.title')" :modal="true" width="40em" height="32em" name="MultiChat">
<div class="multi-chat-top">
<select v-model="selectedRoom" class="room-select">
<option v-for="room in rooms" :key="room.id" :value="room.id">{{ room.title }}</option>
</select>
<div class="options-popdown">
<label>
<input type="checkbox" v-model="autoscroll" />
{{ $t('chat.multichat.autoscroll') }}
</label>
<!-- Weitere Optionen können hier ergänzt werden -->
</div>
</div>
<div class="multi-chat-output" ref="output" @mouseenter="mouseOverOutput = true" @mouseleave="mouseOverOutput = false">
<div v-for="msg in messages" :key="msg.id" class="chat-message">
<span class="user">{{ msg.user }}:</span> <span class="text">{{ msg.text }}</span>
</div>
</div>
<div class="multi-chat-input">
<input v-model="input" @keyup.enter="sendMessage" class="chat-input" :placeholder="$t('chat.multichat.placeholder')" />
<button @click="sendMessage" class="send-btn">{{ $t('chat.multichat.send') }}</button>
<button @click="shout" class="mini-btn">{{ $t('chat.multichat.shout') }}</button>
<button @click="action" class="mini-btn">{{ $t('chat.multichat.action') }}</button>
<button @click="roll" class="mini-btn">{{ $t('chat.multichat.roll') }}</button>
</div>
</DialogWidget>
</template>
<script>
import DialogWidget from '@/components/DialogWidget.vue';
export default {
name: 'MultiChat',
components: { DialogWidget },
data() {
return {
rooms: [],
selectedRoom: null,
autoscroll: true,
mouseOverOutput: false,
messages: [],
input: ''
};
},
watch: {
messages() {
this.$nextTick(this.handleAutoscroll);
},
autoscroll(val) {
if (val) this.handleAutoscroll();
}
},
methods: {
open(rooms = []) {
this.rooms = rooms;
this.selectedRoom = rooms.length ? rooms[0].id : null;
this.autoscroll = true;
this.messages = [];
this.input = '';
this.$refs.dialog.open();
},
handleAutoscroll() {
if (this.autoscroll && !this.mouseOverOutput) {
const out = this.$refs.output;
if (out) out.scrollTop = out.scrollHeight;
}
},
sendMessage() {
if (!this.input.trim()) return;
this.messages.push({ id: Date.now(), user: 'Ich', text: this.input });
this.input = '';
},
shout() {
// Schreien-Logik
},
action() {
// Aktion-Logik
},
roll() {
// Würfeln-Logik
}
}
};
</script>
<style scoped>
.multi-chat-top {
display: flex;
align-items: center;
gap: 1em;
margin-bottom: 0.5em;
}
.room-select {
min-width: 10em;
}
.options-popdown {
background: #f5f5f5;
border-radius: 4px;
padding: 0.3em 0.8em;
font-size: 0.95em;
}
.multi-chat-output {
background: #222;
color: #fff;
height: 16em;
overflow-y: auto;
margin-bottom: 0.5em;
padding: 0.7em;
border-radius: 4px;
font-size: 1em;
}
.chat-message {
margin-bottom: 0.3em;
}
.user {
font-weight: bold;
color: #90caf9;
}
.multi-chat-input {
display: flex;
align-items: center;
gap: 0.5em;
}
.chat-input {
flex: 1;
padding: 0.4em 0.7em;
border-radius: 3px;
border: 1px solid #bbb;
}
.send-btn {
padding: 0.3em 1.1em;
border-radius: 3px;
background: #1976d2;
color: #fff;
border: none;
cursor: pointer;
}
.mini-btn {
padding: 0.2em 0.7em;
font-size: 0.95em;
border-radius: 3px;
background: #eee;
color: #333;
border: 1px solid #bbb;
cursor: pointer;
}
</style>

File diff suppressed because it is too large Load Diff

View File

@@ -1,26 +1,26 @@
<template> <template>
<DialogWidget ref="dialog" title="randomchat.title" icon="dice24.png" :show-close="true" :buttons="buttons" <DialogWidget ref="dialog" title="chat.randomchat.title" icon="dice24.png" :show-close="true" :buttons="buttons"
:modal="false" :isTitleTranslated="true" @close="closeDialog" name="RandomChat"> :modal="false" :isTitleTranslated="true" @close="closeDialog" name="RandomChat">
<div v-if="chatIsRunning" class="randomchat"> <div v-if="chatIsRunning" class="randomchat">
<div class="headline"> <div class="headline">
{{ $t("randomchat.agerange") }} {{ $t("chat.randomchat.agerange") }}
<input type="number" v-model="agefromsearch" min="18" max="150" size="5" /> <input type="number" v-model="agefromsearch" min="18" max="150" size="5" />
- -
<input type="number" v-model="agetosearch" min="18" max="150" size="5" /> <input type="number" v-model="agetosearch" min="18" max="150" size="5" />
<span class="multiselect"> <span class="multiselect">
{{ $t("randomchat.gendersearch") }} {{ $t("chat.randomchat.gendersearch") }}
<div> <div>
<label><input type="checkbox" v-model="searchmale" />{{ $t("randomchat.gender.male") }}</label> <label><input type="checkbox" v-model="searchmale" />{{ $t("chat.randomchat.gender.male") }}</label>
<label><input type="checkbox" v-model="searchfemale" />{{ $t("randomchat.gender.female") <label><input type="checkbox" v-model="searchfemale" />{{ $t("chat.randomchat.gender.female")
}}</label> }}</label>
</div> </div>
</span> </span>
<label><input type="checkbox" v-model="camonlysearch" />{{ $t("randomchat.camonly") }}</label> <label><input type="checkbox" v-model="camonlysearch" />{{ $t("chat.randomchat.camonly") }}</label>
<label><input type="checkbox" v-model="showcam" />{{ $t("randomchat.showcam") }}</label> <label><input type="checkbox" v-model="showcam" />{{ $t("chat.randomchat.showcam") }}</label>
<img v-if="isLoggedIn" src="/images/icons/friendsadd16.png" :tooltip="$t('randomchat.addfriend')" /> <img v-if="isLoggedIn" src="/images/icons/friendsadd16.png" :tooltip="$t('chat.randomchat.addfriend')" />
<label><input type="checkbox" v-model="autosearch" />{{ $t("randomchat.autosearch") }}</label> <label><input type="checkbox" v-model="autosearch" />{{ $t("chat.randomchat.autosearch") }}</label>
<button @click="nextUser" v-if="partner != null">{{ $t("randomchat.jumptonext") }}</button> <button @click="nextUser" v-if="partner != null">{{ $t("chat.randomchat.jumptonext") }}</button>
<button @click="startSearch" v-if="partner == null && !searching">{{ $t("randomchat.startsearch") <button @click="startSearch" v-if="partner == null && !searching">{{ $t("chat.randomchat.startsearch")
}}</button> }}</button>
</div> </div>
<div class="output"> <div class="output">
@@ -28,7 +28,7 @@
</div> </div>
<div class="inputline"> <div class="inputline">
<label> <label>
{{ $t("randomchat.input") }}&nbsp; {{ $t("chat.randomchat.input") }}&nbsp;
<input type="text" v-model="inputtext" @keyup.enter="sendMessage" /> <input type="text" v-model="inputtext" @keyup.enter="sendMessage" />
</label> </label>
<img src="/images/icons/enter16.png" @click="sendMessage" /> <img src="/images/icons/enter16.png" @click="sendMessage" />
@@ -37,20 +37,20 @@
</div> </div>
<div v-else> <div v-else>
<div> <div>
<label>{{ $t("randomchat.age") }} <label>{{ $t("chat.randomchat.age") }}
<input type="number" v-model="age" min="18" max="150" value="18" /> <input type="number" v-model="age" min="18" max="150" value="18" />
</label> </label>
</div> </div>
<div> <div>
<label>{{ $t("randomchat.gender.title") }} <label>{{ $t("chat.randomchat.gender.title") }}
<select v-model="gender"> <select v-model="gender">
<option value="f">{{ $t("randomchat.gender.female") }}</option> <option value="f">{{ $t("chat.randomchat.gender.female") }}</option>
<option value="m">{{ $t("randomchat.gender.male") }}</option> <option value="m">{{ $t("chat.randomchat.gender.male") }}</option>
</select> </select>
</label> </label>
</div> </div>
<div> <div>
<button @click="startRandomChat()">{{ $t("randomchat.start") }}</button> <button @click="startRandomChat()">{{ $t("chat.randomchat.start") }}</button>
</div> </div>
</div> </div>
</DialogWidget> </DialogWidget>
@@ -60,6 +60,7 @@
import DialogWidget from '@/components/DialogWidget.vue'; import DialogWidget from '@/components/DialogWidget.vue';
import { mapGetters } from 'vuex'; import { mapGetters } from 'vuex';
import axios from 'axios'; import axios from 'axios';
import DOMPurify from 'dompurify';
export default { export default {
name: 'RandomChatDialog', name: 'RandomChatDialog',
@@ -69,7 +70,8 @@ export default {
computed: { computed: {
...mapGetters(['isLoggedIn', 'user']), ...mapGetters(['isLoggedIn', 'user']),
buttons() { buttons() {
return [{ text: this.$t('randomchat.close') }]; // Use translation key; DialogWidget will translate when isTitleTranslated=true
return [{ text: 'chat.randomchat.close' }];
}, },
}, },
data() { data() {
@@ -113,26 +115,6 @@ export default {
this.$refs.dialog.open(); this.$refs.dialog.open();
}, },
async closeDialog() {
// ① Stoppe alle laufenden Intervalle
if (this.searchInterval) {
clearInterval(this.searchInterval);
this.searchInterval = null;
}
if (this.messagesInterval) {
clearInterval(this.messagesInterval);
this.messagesInterval = null;
}
// ② verlasse Chat auf Server-Seite
this.$refs.dialog.close();
await axios.post('/api/chat/exit', { id: this.userId });
await this.removeUserFromChat();
// Reset-Status
this.chatIsRunning = false;
this.partner = null;
this.messages = [];
},
async registerUser() { async registerUser() {
try { try {
const response = await axios.post('/api/chat/register', { const response = await axios.post('/api/chat/register', {
@@ -146,9 +128,29 @@ export default {
}, },
async closeDialog() { async closeDialog() {
// Stop intervals first
if (this.searchInterval) {
clearInterval(this.searchInterval);
this.searchInterval = null;
}
if (this.messagesInterval) {
clearInterval(this.messagesInterval);
this.messagesInterval = null;
}
// Inform backend and cleanup
try {
if (this.userId) {
await axios.post('/api/chat/exit', { id: this.userId });
await this.removeUserFromChat();
}
} catch (e) {
// ignore
}
// Reset state and close widget
this.chatIsRunning = false;
this.partner = null;
this.messages = [];
this.$refs.dialog.close(); this.$refs.dialog.close();
await axios.post('/api/chat/exit', { id: this.userId });
await this.removeUserFromChat();
}, },
async startRandomChat() { async startRandomChat() {
@@ -160,7 +162,7 @@ export default {
async startSearch() { async startSearch() {
this.searching = true; this.searching = true;
await this.findMatch(); await this.findMatch();
this.messages.push({ type: 'system', tr: 'randomchat.waitingForMatch' }); this.messages.push({ type: 'system', tr: 'chat.randomchat.waitingForMatch' });
this.searchInterval = setInterval(this.findMatch, 500); this.searchInterval = setInterval(this.findMatch, 500);
}, },
@@ -174,10 +176,10 @@ export default {
if (response.data.status === 'matched') { if (response.data.status === 'matched') {
this.searching = false; this.searching = false;
clearInterval(this.searchInterval); clearInterval(this.searchInterval);
const initText = this.$t('randomchat.chatpartner') const initText = this.$t('chat.randomchat.chatpartner')
.replace( .replace(
'<gender>', '<gender>',
this.$t(`randomchat.partnergender${response.data.user.gender}`) this.$t(`chat.randomchat.partnergender${response.data.user.gender}`)
) )
.replace('<age>', response.data.user.age); .replace('<age>', response.data.user.age);
this.messages = [{ type: 'system', text: initText }]; this.messages = [{ type: 'system', text: initText }];
@@ -220,7 +222,7 @@ export default {
activities.forEach((act) => { activities.forEach((act) => {
if (act.activity === 'otheruserleft') { if (act.activity === 'otheruserleft') {
this.partner = null; this.partner = null;
this.messages.push({ type: 'system', tr: 'randomchat.userleftchat' }); this.messages.push({ type: 'system', tr: 'chat.randomchat.userleftchat' });
} }
}); });
this.messages.push(...newMsgs); this.messages.push(...newMsgs);
@@ -237,10 +239,10 @@ export default {
async nextUser() { async nextUser() {
await axios.post('/api/chat/leave', { id: this.userId }); await axios.post('/api/chat/leave', { id: this.userId });
this.partner = null; this.partner = null;
this.messages.push({ type: 'system', tr: 'randomchat.selfstopped' }); this.messages.push({ type: 'system', tr: 'chat.randomchat.selfstopped' });
if (this.autosearch) { if (this.autosearch) {
this.searchInterval = setInterval(this.findMatch, 500); this.searchInterval = setInterval(this.findMatch, 500);
this.messages.push({ type: 'system', tr: 'randomchat.waitingForMatch' }); this.messages.push({ type: 'system', tr: 'chat.randomchat.waitingForMatch' });
} }
}, },
@@ -250,7 +252,7 @@ export default {
return `<span class="rc-system">${txt}</span>`; return `<span class="rc-system">${txt}</span>`;
} }
const cls = message.type === 'self' ? 'rc-self' : 'rc-partner'; const cls = message.type === 'self' ? 'rc-self' : 'rc-partner';
const who = message.type === 'self' ? this.$t('randomchat.self') : this.$t('randomchat.partner'); const who = message.type === 'self' ? this.$t('chat.randomchat.self') : this.$t('chat.randomchat.partner');
return `<span class="${cls}">${who}: </span>${message.text}`; return `<span class="${cls}">${who}: </span>${message.text}`;
}, },
}, },

View File

@@ -67,7 +67,7 @@
<div v-for="entry in guestbookEntries" :key="entry.id" class="guestbook-entry"> <div v-for="entry in guestbookEntries" :key="entry.id" class="guestbook-entry">
<img v-if="entry.image" :src="entry.image.url" alt="Entry Image" <img v-if="entry.image" :src="entry.image.url" alt="Entry Image"
style="max-width: 400px; max-height: 400px;" /> style="max-width: 400px; max-height: 400px;" />
<p v-html="entry.contentHtml"></p> <p v-html="sanitizedContent(entry)"></p>
<div class="entry-info"> <div class="entry-info">
<span class="entry-timestamp">{{ new Date(entry.createdAt).toLocaleString() }}</span> <span class="entry-timestamp">{{ new Date(entry.createdAt).toLocaleString() }}</span>
<span class="entry-user"> <span class="entry-user">
@@ -96,6 +96,7 @@ import apiClient from '@/utils/axios.js';
import FolderItem from '../../components/FolderItem.vue'; import FolderItem from '../../components/FolderItem.vue';
import { Editor, EditorContent } from '@tiptap/vue-3' import { Editor, EditorContent } from '@tiptap/vue-3'
import StarterKit from '@tiptap/starter-kit' import StarterKit from '@tiptap/starter-kit'
import DOMPurify from 'dompurify';
export default { export default {
name: 'UserProfileDialog', name: 'UserProfileDialog',
@@ -369,7 +370,10 @@ export default {
} else { } else {
this.friendshipState = 'waiting'; this.friendshipState = 'waiting';
} }
} },
sanitizedContent(entry) {
return DOMPurify.sanitize(entry.contentHtml);
},
} }
}; };
</script> </script>

View File

@@ -11,13 +11,14 @@
@ok="handleOk" @ok="handleOk"
name="DataPrivacyDialog" name="DataPrivacyDialog"
> >
<div v-html="dataPrivacyContent"></div> <div v-html="sanitizedContent"></div>
</DialogWidget> </DialogWidget>
</template> </template>
<script> <script>
import DialogWidget from '../../components/DialogWidget.vue'; import DialogWidget from '../../components/DialogWidget.vue';
import content from '../../content/content.js'; import content from '../../content/content.js';
import DOMPurify from 'dompurify';
export default { export default {
name: 'DataPrivacyDialog', name: 'DataPrivacyDialog',
@@ -29,6 +30,11 @@ export default {
dataPrivacyContent: content.dataPrivacy[this.$i18n.locale] dataPrivacyContent: content.dataPrivacy[this.$i18n.locale]
}; };
}, },
computed: {
sanitizedContent() {
return DOMPurify.sanitize(this.dataPrivacyContent);
}
},
watch: { watch: {
'$i18n.locale'(newLocale) { '$i18n.locale'(newLocale) {
this.dataPrivacyContent = content.dataPrivacy[newLocale]; this.dataPrivacyContent = content.dataPrivacy[newLocale];

View File

@@ -11,13 +11,14 @@
@ok="handleOk" @ok="handleOk"
name="ImprintDialog" name="ImprintDialog"
> >
<div v-html="imprintContent"></div> <div v-html="sanitizedContent"></div>
</DialogWidget> </DialogWidget>
</template> </template>
<script> <script>
import DialogWidget from '../../components/DialogWidget.vue'; import DialogWidget from '../../components/DialogWidget.vue';
import content from '../../content/content.js'; import content from '../../content/content.js';
import DOMPurify from 'dompurify';
export default { export default {
name: 'ImprintDialog', name: 'ImprintDialog',
@@ -29,6 +30,11 @@ export default {
imprintContent: content.imprint[this.$i18n.locale] imprintContent: content.imprint[this.$i18n.locale]
}; };
}, },
computed: {
sanitizedContent() {
return DOMPurify.sanitize(this.imprintContent);
}
},
watch: { watch: {
'$i18n.locale'(newLocale) { '$i18n.locale'(newLocale) {
this.imprintContent = content.imprint[newLocale]; this.imprintContent = content.imprint[newLocale];

View File

@@ -14,6 +14,7 @@ import enAdmin from './locales/en/admin.json';
import enSocialNetwork from './locales/en/socialnetwork.json'; import enSocialNetwork from './locales/en/socialnetwork.json';
import enFriends from './locales/en/friends.json'; import enFriends from './locales/en/friends.json';
import enFalukant from './locales/en/falukant.json'; import enFalukant from './locales/en/falukant.json';
import enPasswordReset from './locales/en/passwordReset.json';
import deGeneral from './locales/de/general.json'; import deGeneral from './locales/de/general.json';
import deHeader from './locales/de/header.json'; import deHeader from './locales/de/header.json';
@@ -28,6 +29,7 @@ import deAdmin from './locales/de/admin.json';
import deSocialNetwork from './locales/de/socialnetwork.json'; import deSocialNetwork from './locales/de/socialnetwork.json';
import deFriends from './locales/de/friends.json'; import deFriends from './locales/de/friends.json';
import deFalukant from './locales/de/falukant.json'; import deFalukant from './locales/de/falukant.json';
import dePasswordReset from './locales/de/passwordReset.json';
const messages = { const messages = {
en: { en: {
@@ -37,6 +39,7 @@ const messages = {
...enHome, ...enHome,
...enChat, ...enChat,
...enRegister, ...enRegister,
...enPasswordReset,
...enError, ...enError,
...enActivate, ...enActivate,
...enSettings, ...enSettings,
@@ -53,6 +56,7 @@ const messages = {
...deHome, ...deHome,
...deChat, ...deChat,
...deRegister, ...deRegister,
...dePasswordReset,
...deError, ...deError,
...deActivate, ...deActivate,
...deSettings, ...deSettings,

View File

@@ -91,7 +91,8 @@
"start game": "Spiel starten", "start game": "Spiel starten",
"open room": "Raum öffnen", "open room": "Raum öffnen",
"systemmessage": "Systemnachricht" "systemmessage": "Systemnachricht"
} },
"confirmDelete": "Soll dieser Chatraum wirklich gelöscht werden?"
} }
} }
} }

View File

@@ -1,30 +1,72 @@
{ {
"randomchat": { "chat": {
"title": "Zufallschat", "multichat": {
"age": "Alter", "title": "Multi-Chat",
"gender": { "autoscroll": "Automatisch scrollen",
"title": "Dein Geschlecht", "options": "Optionen",
"male": "Männlich", "send": "Senden",
"female": "Weiblich" "shout": "Schreien",
"action": "Aktion",
"roll": "Würfeln",
"colorpicker": "Farbe wählen",
"colorpicker_preview": "Vorschau: Diese Nachricht nutzt die gewählte Farbe.",
"hex": "HEX",
"invalid_hex": "Ungültiger Hex-Wert",
"hue": "Farbton",
"saturation": "Sättigung",
"lightness": "Helligkeit",
"ok": "Ok",
"cancel": "Abbrechen",
"placeholder": "Nachricht eingeben...",
"action_select_user": "Bitte Benutzer auswählen",
"action_to": "Aktion an {to}",
"action_phrases": {
"left_room": "wechselt zu Raum",
"leaves_room": "verlässt Raum",
"left_chat": "hat den Chat verlassen."
},
"system": {
"room_entered": "Du hast den Raum \"{room}\" betreten.",
"user_entered_room": "{user} hat den Raum betreten.",
"user_left_room": "{user} hat den Raum verlassen."
,
"color_changed_self": "Du hast deine Farbe zu {color} geändert.",
"color_changed_user": "{user} hat seine/ihre Farbe zu {color} geändert."
},
"status": {
"connecting": "Verbinden…",
"connected": "Verbunden",
"disconnected": "Getrennt",
"error": "Fehler bei der Verbindung"
}
}, },
"start": "Loslegen", "randomchat": {
"agerange": "Alter", "title": "Zufallschat",
"gendersearch": "Geschlechter", "age": "Alter",
"camonly": "Nur mit Cam", "gender": {
"showcam": "Eigene Cam anzeigen", "title": "Dein Geschlecht",
"addfriend": "Zu Freunden hinzufügen", "male": "Männlich",
"close": "Chat beenden", "female": "Weiblich"
"autosearch": "Automatisch suchen", },
"input": "Ihr Text", "start": "Loslegen",
"waitingForMatch": "Warten auf einen Teilnehmer...", "agerange": "Alter",
"chatpartner": "Du chattest jetzt mit einer <gender> Person im Alter von <age> Jahren.", "gendersearch": "Geschlechter",
"partnergenderm": "männlichen", "camonly": "Nur mit Cam",
"partnergenderf": "weiblichen", "showcam": "Eigene Cam anzeigen",
"self": "Du", "addfriend": "Zu Freunden hinzufügen",
"partner": "Partner", "close": "Chat beenden",
"jumptonext": "Diesen Chat beenden", "autosearch": "Automatisch suchen",
"userleftchat": "Der Gesprächstpartner hat den Chat verlassen.", "input": "Ihr Text",
"startsearch": "Suche nächstes Gespräch", "waitingForMatch": "Warten auf einen Teilnehmer...",
"selfstopped": "Du hast das Gespräch verlassen." "chatpartner": "Du chattest jetzt mit einer <gender> Person im Alter von <age> Jahren.",
"partnergenderm": "männlichen",
"partnergenderf": "weiblichen",
"self": "Du",
"partner": "Partner",
"jumptonext": "Diesen Chat beenden",
"userleftchat": "Der Gesprächstpartner hat den Chat verlassen.",
"startsearch": "Suche nächstes Gespräch",
"selfstopped": "Du hast das Gespräch verlassen."
}
} }
} }

View File

@@ -41,5 +41,12 @@
"transmale": "Trans-Mann", "transmale": "Trans-Mann",
"transfemale": "Trans-Frau", "transfemale": "Trans-Frau",
"nonbinary": "Nichtbinär" "nonbinary": "Nichtbinär"
},
"common": {
"edit": "Bearbeiten",
"delete": "Löschen",
"create": "Erstellen",
"yes": "Ja",
"no": "Nein"
} }
} }

View File

@@ -0,0 +1,9 @@
{
"passwordReset": {
"title": "Passwort zurücksetzen",
"email": "E-Mail",
"reset": "Zurücksetzen",
"success": "Falls die E-Mail existiert, wurde eine Anleitung zum Zurücksetzen gesendet.",
"failure": "Passwort-Zurücksetzen fehlgeschlagen. Bitte später erneut versuchen."
}
}

View File

@@ -1,5 +1,46 @@
{ {
"randomchat": { "chat": {
"multichat": {
"title": "Multi Chat",
"autoscroll": "Auto scroll",
"options": "Options",
"send": "Send",
"shout": "Shout",
"action": "Action",
"roll": "Roll",
"colorpicker": "Pick color",
"colorpicker_preview": "Preview: This message uses the chosen color.",
"hex": "HEX",
"invalid_hex": "Invalid hex value",
"hue": "Hue",
"saturation": "Saturation",
"lightness": "Lightness",
"ok": "Ok",
"cancel": "Cancel",
"placeholder": "Type a message…",
"action_select_user": "Please select a user",
"action_to": "Action to {to}",
"action_phrases": {
"left_room": "switches to room",
"leaves_room": "leaves room",
"left_chat": "has left the chat."
},
"system": {
"room_entered": "You entered the room \"{room}\".",
"user_entered_room": "{user} has entered the room.",
"user_left_room": "{user} has left the room."
,
"color_changed_self": "You changed your color to {color}.",
"color_changed_user": "{user} changed their color to {color}."
},
"status": {
"connecting": "Connecting…",
"connected": "Connected",
"disconnected": "Disconnected",
"error": "Connection error"
}
},
"randomchat": {
"title": "Random Chat", "title": "Random Chat",
"close": "Close", "close": "Close",
"age": "Age", "age": "Age",
@@ -16,6 +57,16 @@
"autosearch": "Auto Search", "autosearch": "Auto Search",
"input": "Input", "input": "Input",
"start": "Start", "start": "Start",
"waitingForMatch": "Waiting for a match..." "waitingForMatch": "Waiting for a match...",
"chatpartner": "You are now chatting with a <gender> person aged <age> years.",
"partnergenderm": "male",
"partnergenderf": "female",
"self": "You",
"partner": "Partner",
"jumptonext": "End this chat",
"userleftchat": "The chat partner has left the chat.",
"startsearch": "Search next conversation",
"selfstopped": "You left the conversation."
}
} }
} }

View File

@@ -0,0 +1,9 @@
{
"passwordReset": {
"title": "Reset Password",
"email": "Email",
"reset": "Reset",
"success": "If the email exists, we've sent reset instructions.",
"failure": "Password reset failed. Please try again later."
}
}

View File

@@ -1,6 +1,6 @@
import AdminInterestsView from '../views/admin/InterestsView.vue'; import AdminInterestsView from '../views/admin/InterestsView.vue';
import AdminContactsView from '../views/admin/ContactsView.vue'; import AdminContactsView from '../views/admin/ContactsView.vue';
import ChatRoomsView from '../views/admin/ChatRoomsView.vue'; import RoomsView from '../views/admin/RoomsView.vue';
import ForumAdminView from '../dialogues/admin/ForumAdminView.vue'; import ForumAdminView from '../dialogues/admin/ForumAdminView.vue';
import AdminFalukantEditUserView from '../views/admin/falukant/EditUserView.vue' import AdminFalukantEditUserView from '../views/admin/falukant/EditUserView.vue'
@@ -26,7 +26,7 @@ const adminRoutes = [
{ {
path: '/admin/chatrooms', path: '/admin/chatrooms',
name: 'AdminChatRooms', name: 'AdminChatRooms',
component: ChatRoomsView, component: RoomsView,
meta: { requiresAuth: true } meta: { requiresAuth: true }
}, },
{ {

View File

@@ -0,0 +1,87 @@
// Small helper to resolve the Chat WebSocket URL from env or sensible defaults
export function getChatWsUrl() {
// Prefer explicit env var
const override = (typeof window !== 'undefined' && window.localStorage) ? window.localStorage.getItem('chatWsOverride') : '';
if (override && typeof override === 'string' && override.trim()) {
return override.trim();
}
const envUrl = import.meta?.env?.VITE_CHAT_WS_URL;
if (envUrl && typeof envUrl === 'string' && envUrl.trim()) {
return envUrl.trim();
}
// Fallback: use current origin host with ws/wss and default port/path if provided by backend
const isHttps = typeof window !== 'undefined' && window.location.protocol === 'https:';
const proto = isHttps ? 'wss' : 'ws';
// If a reverse proxy exposes the chat at a path, you can change '/chat' here.
const host = typeof window !== 'undefined' ? window.location.hostname : 'localhost';
const port = (typeof window !== 'undefined' && window.location.port) ? `:${window.location.port}` : '';
// On localhost, prefer dedicated chat port 1235 by default
// Prefer IPv4 for localhost to avoid browsers resolving to ::1 (IPv6) where the server may not listen
if (host === 'localhost' || host === '127.0.0.1' || host === '::1' || host === '[::1]') {
return `${proto}://127.0.0.1:1235`;
}
// Default to same origin (no hardcoded chat port). Adjust via VITE_CHAT_WS_URL if needed.
const defaultUrl = `${proto}://${host}${port}`;
return defaultUrl;
}
// Provide a list of candidate WS URLs to try, in order of likelihood.
export function getChatWsCandidates() {
const override = (typeof window !== 'undefined' && window.localStorage) ? window.localStorage.getItem('chatWsOverride') : '';
if (override && typeof override === 'string' && override.trim()) {
return [override.trim()];
}
const envUrl = import.meta?.env?.VITE_CHAT_WS_URL;
if (envUrl && typeof envUrl === 'string' && envUrl.trim()) {
return [envUrl.trim()];
}
const isHttps = typeof window !== 'undefined' && window.location.protocol === 'https:';
const proto = isHttps ? 'wss' : 'ws';
const host = typeof window !== 'undefined' ? window.location.hostname : 'localhost';
const port = (typeof window !== 'undefined' && window.location.port) ? `:${window.location.port}` : '';
const candidates = [];
// Common local setups: include IPv4 and IPv6 loopback variants (root only)
if (host === 'localhost' || host === '127.0.0.1' || host === '::1' || host === '[::1]') {
// Prefer IPv6 loopback first when available
const localHosts = ['[::1]', '127.0.0.1', 'localhost'];
for (const h of localHosts) {
const base = `${proto}://${h}:1235`;
candidates.push(base);
candidates.push(`${base}/`);
}
}
// Same-origin root and common WS paths
const sameOriginBases = [`${proto}://${host}${port}`];
// If localhost-ish, also try 127.0.0.1 for same-origin port
if ((host === 'localhost' || host === '::1' || host === '[::1]') && port) {
sameOriginBases.push(`${proto}://[::1]${port}`);
sameOriginBases.push(`${proto}://127.0.0.1${port}`);
}
for (const base of sameOriginBases) {
candidates.push(base);
candidates.push(`${base}/`);
}
return candidates;
}
// Return optional subprotocols for the WebSocket handshake.
export function getChatWsProtocols() {
try {
const ls = (typeof window !== 'undefined' && window.localStorage) ? window.localStorage.getItem('chatWsProtocols') : '';
if (ls && ls.trim()) {
// Accept JSON array or comma-separated
if (ls.trim().startsWith('[')) return JSON.parse(ls);
return ls.split(',').map(s => s.trim()).filter(Boolean);
}
} catch (_) {}
const env = import.meta?.env?.VITE_CHAT_WS_PROTOCOLS;
if (env && typeof env === 'string' && env.trim()) {
try {
if (env.trim().startsWith('[')) return JSON.parse(env);
} catch (_) {}
return env.split(',').map(s => s.trim()).filter(Boolean);
}
// Default to the 'chat' subprotocol so the server can gate connections accordingly
return ['chat'];
}

View File

@@ -0,0 +1,87 @@
// Centralized config for YourChat protocol mapping and WS endpoint
// Override via .env (VITE_* variables)
const env = import.meta.env || {};
export const CHAT_WS_URL = env.VITE_CHAT_WS_URL
|| (env.VITE_CHAT_WS_HOST || env.VITE_CHAT_WS_PORT
? `ws://${env.VITE_CHAT_WS_HOST || 'localhost'}:${env.VITE_CHAT_WS_PORT || '1235'}`
: `ws://localhost:1235`);
// Event/type keys
export const CHAT_EVENT_KEY = env.VITE_CHAT_EVENT_KEY || 'type';
export const CHAT_EVT_TOKEN = env.VITE_CHAT_EVT_TOKEN || 'token';
export const CHAT_EVT_AUTH = env.VITE_CHAT_EVT_AUTH || 'auth'; // optional, YourChat nutzt Token
export const CHAT_EVT_JOIN = env.VITE_CHAT_EVT_JOIN || 'join';
export const CHAT_EVT_MESSAGE = env.VITE_CHAT_EVT_MESSAGE || 'message';
export const CHAT_EVT_SYSTEM = env.VITE_CHAT_EVT_SYSTEM || 'system';
// Field names for payloads
export const CHAT_JOIN_ROOM_KEY = env.VITE_CHAT_JOIN_ROOM_KEY || 'newroom';
export const CHAT_JOIN_LEAVE_ROOM_KEY = env.VITE_CHAT_JOIN_LEAVE_ROOM_KEY || 'password'; // YourChat erwartet password Feld; leave optional nicht vorgesehen
export const CHAT_MESSAGE_ROOM_KEY = env.VITE_CHAT_MESSAGE_ROOM_KEY || null; // YourChat braucht kein roomId im message payload
export const CHAT_MSG_TEXT_KEY = env.VITE_CHAT_MSG_TEXT_KEY || 'message';
export const CHAT_MSG_FROM_KEY = env.VITE_CHAT_MSG_FROM_KEY || 'userName';
export const CHAT_MSG_ID_KEY = env.VITE_CHAT_MSG_ID_KEY || 'id';
export const CHAT_TOKEN_KEY = env.VITE_CHAT_TOKEN_KEY || 'token';
// Auth payload mapping
export const CHAT_AUTH_PAYLOAD_KEY = env.VITE_CHAT_AUTH_PAYLOAD_KEY || 'data';
export const CHAT_AUTH_USER_ID_KEY = env.VITE_CHAT_AUTH_USER_ID_KEY || 'userId';
export const CHAT_AUTH_AUTHCODE_KEY = env.VITE_CHAT_AUTH_AUTHCODE_KEY || 'authCode';
export function buildAuthPayload(user) {
const payload = {};
payload[CHAT_EVENT_KEY] = CHAT_EVT_AUTH;
const dataObj = {};
if (user?.id != null) dataObj[CHAT_AUTH_USER_ID_KEY] = user.id;
if (user?.authCode != null) dataObj[CHAT_AUTH_AUTHCODE_KEY] = user.authCode;
payload[CHAT_AUTH_PAYLOAD_KEY] = dataObj;
return payload;
}
export function buildJoinPayload(roomName, password = '') {
const payload = {};
payload[CHAT_EVENT_KEY] = CHAT_EVT_JOIN;
if (roomName != null) payload[CHAT_JOIN_ROOM_KEY] = roomName;
if (CHAT_JOIN_LEAVE_ROOM_KEY) payload[CHAT_JOIN_LEAVE_ROOM_KEY] = password;
return payload;
}
export function buildMessagePayload(text) {
const payload = {};
payload[CHAT_EVENT_KEY] = CHAT_EVT_MESSAGE;
if (CHAT_MESSAGE_ROOM_KEY && typeof CHAT_MESSAGE_ROOM_KEY === 'string') payload[CHAT_MESSAGE_ROOM_KEY] = null;
payload[CHAT_MSG_TEXT_KEY] = text;
return payload;
}
export function parseIncomingMessage(data) {
// Try JSON, else return as system text
let obj = data;
if (typeof data === 'string') {
try { obj = JSON.parse(data); } catch (_) { return { type: 'system', text: String(data) }; }
}
const type = obj[CHAT_EVENT_KEY] || obj.type;
if (type === CHAT_EVT_TOKEN) {
return { type: 'token', token: obj.message };
}
if (type === CHAT_EVT_MESSAGE || type === 'message') {
return {
type: 'message',
id: obj[CHAT_MSG_ID_KEY] || Date.now(),
from: obj[CHAT_MSG_FROM_KEY] || 'User',
text: obj[CHAT_MSG_TEXT_KEY] || ''
};
}
if (type === CHAT_EVT_SYSTEM || type === 'system') {
return {
type: 'system',
id: obj[CHAT_MSG_ID_KEY] || Date.now(),
text: obj[CHAT_MSG_TEXT_KEY] || ''
};
}
// Fallback: unknown event -> show raw
return { type: 'system', id: Date.now(), text: JSON.stringify(obj) };
}

View File

@@ -1,175 +0,0 @@
<template>
<DialogWidget ref="dialog" :title="$t(room && room.id ? 'admin.chatrooms.edit' : 'admin.chatrooms.create')"
:show-close="true" :buttons="buttons" name="RoomDialog" :modal="true" :isTitleTranslated="true"
@close="closeDialog">
<form class="dialog-form" @submit.prevent="save">
<label>
{{ $t('admin.chatrooms.title') }}
<input v-model="localRoom.title" required />
</label>
<label>
{{ $t('admin.chatrooms.type') }}
<select v-model="localRoom.roomTypeId" required>
<option v-for="type in roomTypes" :key="type.id" :value="type.id">{{ type.tr }}</option>
</select>
</label>
<label>
<input type="checkbox" v-model="localRoom.isPublic" />
{{ $t('admin.chatrooms.isPublic') }}
</label>
<label>
<input type="checkbox" v-model="showGenderRestriction" />
{{ $t('admin.chatrooms.genderRestriction.show') }}
</label>
<label v-if="showGenderRestriction">
{{ $t('admin.chatrooms.genderRestriction.label') }}
<select v-model="localRoom.genderRestrictionId">
<option v-for="g in genderRestrictions" :key="g.id" :value="g.id">{{ g.tr }}</option>
</select>
</label>
<label>
<input type="checkbox" v-model="showMinAge" />
{{ $t('admin.chatrooms.minAge.show') }}
</label>
<label v-if="showMinAge">
{{ $t('admin.chatrooms.minAge.label') }}
<input v-model.number="localRoom.minAge" type="number" />
</label>
<label>
<input type="checkbox" v-model="showMaxAge" />
{{ $t('admin.chatrooms.maxAge.show') }}
</label>
<label v-if="showMaxAge">
{{ $t('admin.chatrooms.maxAge.label') }}
<input v-model.number="localRoom.maxAge" type="number" />
</label>
<label>
<input type="checkbox" v-model="showPassword" />
{{ $t('admin.chatrooms.password.show') }}
</label>
<label v-if="showPassword">
{{ $t('admin.chatrooms.password.label') }}
<input v-model="localRoom.password" type="password" />
</label>
<label>
<input type="checkbox" v-model="localRoom.friendsOfOwnerOnly" />
{{ $t('admin.chatrooms.friendsOfOwnerOnly') }}
</label>
<label>
<input type="checkbox" v-model="showRequiredUserRight" />
{{ $t('admin.chatrooms.requiredUserRight.show') }}
</label>
<label v-if="showRequiredUserRight">
{{ $t('admin.chatrooms.requiredUserRight.label') }}
<select v-model="localRoom.requiredUserRightId">
<option v-for="r in userRights" :key="r.id" :value="r.id">{{ r.tr }}</option>
</select>
</label>
</form>
</DialogWidget>
</template>
<script>
import DialogWidget from '@/components/DialogWidget.vue';
export default {
name: 'RoomDialog',
props: {
modelValue: Boolean,
room: Object
},
data() {
return {
dialog: null,
localRoom: this.room ? { ...this.room } : { title: '', isPublic: true },
roomTypes: [],
genderRestrictions: [],
userRights: [],
showGenderRestriction: !!(this.room && this.room.genderRestrictionId),
showMinAge: !!(this.room && this.room.minAge),
showMaxAge: !!(this.room && this.room.maxAge),
showPassword: !!(this.room && this.room.password),
showRequiredUserRight: !!(this.room && this.room.requiredUserRightId),
buttons: [
{ text: 'common.save', action: this.save },
{ text: 'common.cancel', action: this.closeDialog }
]
},
watch: {
room: {
handler(newVal) {
this.localRoom = newVal ? { ...newVal } : { title: '', isPublic: true };
this.showGenderRestriction = !!(newVal && newVal.genderRestrictionId);
this.showMinAge = !!(newVal && newVal.minAge);
this.showMaxAge = !!(newVal && newVal.maxAge);
this.showPassword = !!(newVal && newVal.password);
this.showRequiredUserRight = !!(newVal && newVal.requiredUserRightId);
},
immediate: true
}
},
mounted() {
this.dialog = this.$refs.dialog;
this.fetchRoomTypes();
this.fetchGenderRestrictions();
this.fetchUserRights();
},
methods: {
async fetchRoomTypes() {
// API-Call, z.B. this.roomTypes = await apiClient.get('/api/room-types')
},
async fetchGenderRestrictions() {
// API-Call, z.B. this.genderRestrictions = await apiClient.get('/api/gender-restrictions')
},
async fetchUserRights() {
// API-Call, z.B. this.userRights = await apiClient.get('/api/user-rights')
},
open(roomData) {
this.localRoom = roomData ? { ...roomData } : { title: '', isPublic: true };
this.dialog.open();
},
closeDialog() {
this.dialog.close();
},
save() {
this.$emit('save', this.localRoom);
this.closeDialog();
}
}
}
</script>
<style scoped>
.room-dialog-content {
padding: 16px;
}
.dialog-title {
font-weight: bold;
font-size: 1.2em;
margin-bottom: 12px;
display: block;
}
.dialog-fields > * {
margin-bottom: 8px;
}
.dialog-actions {
display: flex;
justify-content: flex-end;
gap: 8px;
margin-top: 16px;
}
.save-btn, .cancel-btn {
padding: 6px 18px;
border: none;
border-radius: 3px;
background: #eee;
cursor: pointer;
}
.save-btn {
background: #1976d2;
color: #fff;
}
.cancel-btn {
background: #eee;
color: #333;
}
</style>

View File

@@ -0,0 +1,113 @@
<template>
<div class="admin-chat-rooms">
<h2>{{ $t('admin.chatrooms.title') }}</h2>
<button class="create-btn" @click="openCreateDialog">{{ $t('admin.chatrooms.create') }}</button>
<table class="rooms-table">
<thead>
<tr>
<th>{{ $t('admin.chatrooms.roomName') }}</th>
<th>{{ $t('admin.chatrooms.type') }}</th>
<th>{{ $t('admin.chatrooms.isPublic') }}</th>
<th>{{ $t('admin.chatrooms.actions') }}</th>
</tr>
</thead>
<tbody>
<tr v-for="room in rooms" :key="room.id">
<td>{{ room.title }}</td>
<td>{{ room.roomTypeTr || room.roomTypeId }}</td>
<td>{{ room.isPublic ? $t('common.yes') : $t('common.no') }}</td>
<td>
<button @click="editRoom(room)">{{ $t('common.edit') }}</button>
<button @click="deleteRoom(room)">{{ $t('common.delete') }}</button>
</td>
</tr>
</tbody>
</table>
<RoomDialog ref="roomDialog" :room="selectedRoom" @save="saveRoom" />
<ChooseDialog ref="chooseDialog" />
</div>
</template>
<script>
import RoomDialog from '@/dialogues/admin/RoomDialog.vue';
import ChooseDialog from '@/dialogues/standard/ChooseDialog.vue';
import apiClient from '@/utils/axios.js';
export default {
name: 'RoomsView',
components: { RoomDialog, ChooseDialog },
data() {
return {
rooms: [],
selectedRoom: null,
}
},
mounted() {
this.fetchRooms();
},
methods: {
openCreateDialog() {
this.selectedRoom = null;
this.$refs.roomDialog.open();
},
editRoom(room) {
this.selectedRoom = { ...room };
this.$refs.roomDialog.open(this.selectedRoom);
},
async deleteRoom(room) {
if (!room.id) return;
const confirmed = await this.$refs.chooseDialog.open({
title: this.$t('common.confirm'),
message: this.$t('admin.chatrooms.confirmDelete')
});
if (!confirmed) return;
await apiClient.delete(`/api/admin/chat/rooms/${room.id}`);
this.fetchRooms();
},
async fetchRooms() {
const res = await apiClient.get('/api/admin/chat/rooms');
this.rooms = res.data;
},
async saveRoom(roomData) {
// Remove forbidden and associated object fields before sending to backend
const { id, ownerId, passwordHash, roomType, genderRestriction, ...cleanData } = roomData;
if (roomData.id) {
await apiClient.put(`/api/admin/chat/rooms/${roomData.id}`, cleanData);
} else {
await apiClient.post('/api/admin/chat/rooms', cleanData);
}
this.fetchRooms();
},
},
}
</script>
<style scoped>
.admin-chat-rooms {
max-width: 900px;
margin: 0 auto;
}
.create-btn {
margin-bottom: 12px;
padding: 6px 18px;
border: none;
border-radius: 3px;
background: #1976d2;
color: #fff;
cursor: pointer;
}
.rooms-table {
width: 100%;
border-collapse: collapse;
}
.rooms-table th, .rooms-table td {
border: 1px solid #ddd;
padding: 8px;
}
.rooms-table th {
background: #f5f5f5;
}
.rooms-table td button {
margin-right: 6px;
}
</style>

View File

@@ -68,13 +68,16 @@ export default {
methods: { methods: {
...mapActions(['login']), ...mapActions(['login']),
openRandomChat() { openRandomChat() {
this.$refs.randomChatDialog.open(); const dlg = this.$refs.randomChatDialog;
if (dlg && typeof dlg.open === 'function') dlg.open();
}, },
openRegisterDialog() { openRegisterDialog() {
this.$refs.registerDialog.open(); const dlg = this.$refs.registerDialog;
if (dlg && typeof dlg.open === 'function') dlg.open();
}, },
openPasswordResetDialog() { openPasswordResetDialog() {
this.$refs.passwordResetDialog.open(); const dlg = this.$refs.passwordResetDialog;
if (dlg && typeof dlg.open === 'function') dlg.open();
}, },
focusPassword() { focusPassword() {
this.$refs.passwordInput.focus(); this.$refs.passwordInput.focus();

View File

@@ -14,7 +14,7 @@
<div v-if="diaryEntries.length === 0">{{ $t('socialnetwork.diary.noEntries') }}</div> <div v-if="diaryEntries.length === 0">{{ $t('socialnetwork.diary.noEntries') }}</div>
<div v-else class="diary-entries"> <div v-else class="diary-entries">
<div v-for="entry in diaryEntries" :key="entry.id" class="diary-entry"> <div v-for="entry in diaryEntries" :key="entry.id" class="diary-entry">
<p v-html="entry.text"></p> <p v-html="sanitizedText(entry)"></p>
<div class="entry-info"> <div class="entry-info">
<span class="entry-timestamp">{{ new Date(entry.createdAt).toLocaleString() }}</span> <span class="entry-timestamp">{{ new Date(entry.createdAt).toLocaleString() }}</span>
<span class="entry-actions"> <span class="entry-actions">
@@ -39,6 +39,7 @@
import { mapGetters } from 'vuex'; import { mapGetters } from 'vuex';
import apiClient from '@/utils/axios.js'; import apiClient from '@/utils/axios.js';
import ChooseDialog from "@/dialogues/standard/ChooseDialog.vue"; import ChooseDialog from "@/dialogues/standard/ChooseDialog.vue";
import DOMPurify from 'dompurify';
export default { export default {
name: 'DiaryView', name: 'DiaryView',
@@ -59,6 +60,9 @@ export default {
...mapGetters(['user']), ...mapGetters(['user']),
}, },
methods: { methods: {
sanitizedText(entry) {
return DOMPurify.sanitize(entry.text);
},
async loadDiaryEntries(page) { async loadDiaryEntries(page) {
try { try {
console.log(page); console.log(page);

View File

@@ -3,7 +3,7 @@
<h3 v-if="forumTopic">{{ forumTopic }}</h3> <h3 v-if="forumTopic">{{ forumTopic }}</h3>
<ul class="messages"> <ul class="messages">
<li v-for="message in messages" :key="message.id"> <li v-for="message in messages" :key="message.id">
<div v-html="message.text"></div> <div v-html="sanitizedMessage(message)"></div>
<div class="footer"> <div class="footer">
<span class="link" @click="openProfile(message.lastMessageUser.hashedId)"> <span class="link" @click="openProfile(message.lastMessageUser.hashedId)">
{{ message.lastMessageUser.username }} {{ message.lastMessageUser.username }}
@@ -23,6 +23,7 @@
import { Editor, EditorContent } from '@tiptap/vue-3' import { Editor, EditorContent } from '@tiptap/vue-3'
import StarterKit from '@tiptap/starter-kit' import StarterKit from '@tiptap/starter-kit'
import apiClient from '../../utils/axios' import apiClient from '../../utils/axios'
import DOMPurify from 'dompurify'
export default { export default {
name: 'ForumTopicView', name: 'ForumTopicView',
@@ -87,6 +88,9 @@ export default {
}, },
openForum() { openForum() {
this.$router.push(`/socialnetwork/forum/${this.forumId}`); this.$router.push(`/socialnetwork/forum/${this.forumId}`);
},
sanitizedMessage(message) {
return DOMPurify.sanitize(message.text);
} }
} }
} }

View File

@@ -7,7 +7,7 @@
<div v-for="entry in guestbookEntries" :key="entry.id" class="guestbook-entry"> <div v-for="entry in guestbookEntries" :key="entry.id" class="guestbook-entry">
<img v-if="entry.image" :src="entry.image.url" alt="Entry Image" <img v-if="entry.image" :src="entry.image.url" alt="Entry Image"
style="max-width: 400px; max-height: 400px;" /> style="max-width: 400px; max-height: 400px;" />
<p v-html="entry.contentHtml"></p> <p v-html="sanitizedContent(entry)"></p>
<div class="entry-info"> <div class="entry-info">
<span class="entry-timestamp">{{ new Date(entry.createdAt).toLocaleString() }}</span> <span class="entry-timestamp">{{ new Date(entry.createdAt).toLocaleString() }}</span>
<span class="entry-user"> <span class="entry-user">
@@ -30,6 +30,7 @@
<script> <script>
import { mapActions, mapGetters } from 'vuex'; import { mapActions, mapGetters } from 'vuex';
import apiClient from '@/utils/axios.js'; import apiClient from '@/utils/axios.js';
import DOMPurify from 'dompurify';
export default { export default {
name: 'GuestbookView', name: 'GuestbookView',
@@ -73,6 +74,9 @@ export default {
console.error('Error fetching image:', error); console.error('Error fetching image:', error);
} }
}, },
sanitizedContent(entry) {
return DOMPurify.sanitize(entry.contentHtml);
},
}, },
mounted() { mounted() {
this.loadGuestbookEntries(1); this.loadGuestbookEntries(1);

115
package-lock.json generated
View File

@@ -9,23 +9,21 @@
"version": "3.0.0-pre-alpha.0.1", "version": "3.0.0-pre-alpha.0.1",
"dependencies": { "dependencies": {
"cors": "^2.8.5", "cors": "^2.8.5",
"dompurify": "^3.2.6",
"sequelize-cli": "^6.6.2" "sequelize-cli": "^6.6.2"
}, },
"devDependencies": { "devDependencies": {
"concurrently": "^7.0.0", "concurrently": "^7.0.0",
"nodemon": "^2.0.15", "nodemon": "^3.1.10",
"npm-run-all": "^4.1.5" "npm-run-all": "^4.1.5"
} }
}, },
"node_modules/@babel/runtime": { "node_modules/@babel/runtime": {
"version": "7.26.0", "version": "7.28.2",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.26.0.tgz", "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.2.tgz",
"integrity": "sha512-FDSOghenHTiToteC/QRlv2q3DhPZ/oOXTBoirfWNx1Cx3TMVcGWQtMMmQcSvb/JjpNeGzx8Pq/b4fKEJuWm1sw==", "integrity": "sha512-KHp2IflsnGywDjBWDkR9iEqiWSpc8GIi0lgTT3mOElT0PP1tG26P4tmFI2YvAdzgq9RGyoHZQEIEdZy6Ec5xCA==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": {
"regenerator-runtime": "^0.14.0"
},
"engines": { "engines": {
"node": ">=6.9.0" "node": ">=6.9.0"
} }
@@ -63,6 +61,13 @@
"node": ">=14" "node": ">=14"
} }
}, },
"node_modules/@types/trusted-types": {
"version": "2.0.7",
"resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz",
"integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==",
"license": "MIT",
"optional": true
},
"node_modules/abbrev": { "node_modules/abbrev": {
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/abbrev/-/abbrev-2.0.0.tgz", "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-2.0.0.tgz",
@@ -204,9 +209,9 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/brace-expansion": { "node_modules/brace-expansion": {
"version": "1.1.11", "version": "1.1.12",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
"integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
@@ -582,13 +587,21 @@
} }
}, },
"node_modules/debug": { "node_modules/debug": {
"version": "3.2.7", "version": "4.4.1",
"resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz",
"integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"ms": "^2.1.1" "ms": "^2.1.3"
},
"engines": {
"node": ">=6.0"
},
"peerDependenciesMeta": {
"supports-color": {
"optional": true
}
} }
}, },
"node_modules/define-data-property": { "node_modules/define-data-property": {
@@ -627,6 +640,15 @@
"url": "https://github.com/sponsors/ljharb" "url": "https://github.com/sponsors/ljharb"
} }
}, },
"node_modules/dompurify": {
"version": "3.2.6",
"resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.2.6.tgz",
"integrity": "sha512-/2GogDQlohXPZe6D6NOgQvXLPSYBqIWMnZ8zzOhn09REE4eyAzb+Hed3jhoM9OkuaJ8P6ZGTTVWQKAi8ieIzfQ==",
"license": "(MPL-2.0 OR Apache-2.0)",
"optionalDependencies": {
"@types/trusted-types": "^2.0.7"
}
},
"node_modules/eastasianwidth": { "node_modules/eastasianwidth": {
"version": "0.2.0", "version": "0.2.0",
"resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz",
@@ -652,9 +674,9 @@
} }
}, },
"node_modules/editorconfig/node_modules/brace-expansion": { "node_modules/editorconfig/node_modules/brace-expansion": {
"version": "2.0.1", "version": "2.0.2",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz",
"integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"balanced-match": "^1.0.0" "balanced-match": "^1.0.0"
@@ -1185,9 +1207,9 @@
} }
}, },
"node_modules/glob/node_modules/brace-expansion": { "node_modules/glob/node_modules/brace-expansion": {
"version": "2.0.1", "version": "2.0.2",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz",
"integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"balanced-match": "^1.0.0" "balanced-match": "^1.0.0"
@@ -1849,19 +1871,19 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/nodemon": { "node_modules/nodemon": {
"version": "2.0.22", "version": "3.1.10",
"resolved": "https://registry.npmjs.org/nodemon/-/nodemon-2.0.22.tgz", "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.10.tgz",
"integrity": "sha512-B8YqaKMmyuCO7BowF1Z1/mkPqLk6cs/l63Ojtd6otKjMx47Dq1utxfRxcavH1I7VSaL8n5BUaoutadnsX3AAVQ==", "integrity": "sha512-WDjw3pJ0/0jMFmyNDp3gvY2YizjLmmOUQo6DEBY+JgdvW/yQ9mEeSw6H5ythl5Ny2ytb7f9C2nIbjSxMNzbJXw==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"chokidar": "^3.5.2", "chokidar": "^3.5.2",
"debug": "^3.2.7", "debug": "^4",
"ignore-by-default": "^1.0.1", "ignore-by-default": "^1.0.1",
"minimatch": "^3.1.2", "minimatch": "^3.1.2",
"pstree.remy": "^1.1.8", "pstree.remy": "^1.1.8",
"semver": "^5.7.1", "semver": "^7.5.3",
"simple-update-notifier": "^1.0.7", "simple-update-notifier": "^2.0.0",
"supports-color": "^5.5.0", "supports-color": "^5.5.0",
"touch": "^3.1.0", "touch": "^3.1.0",
"undefsafe": "^2.0.5" "undefsafe": "^2.0.5"
@@ -1870,7 +1892,7 @@
"nodemon": "bin/nodemon.js" "nodemon": "bin/nodemon.js"
}, },
"engines": { "engines": {
"node": ">=8.10.0" "node": ">=10"
}, },
"funding": { "funding": {
"type": "opencollective", "type": "opencollective",
@@ -1887,6 +1909,19 @@
"node": ">=4" "node": ">=4"
} }
}, },
"node_modules/nodemon/node_modules/semver": {
"version": "7.7.2",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz",
"integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==",
"dev": true,
"license": "ISC",
"bin": {
"semver": "bin/semver.js"
},
"engines": {
"node": ">=10"
}
},
"node_modules/nodemon/node_modules/supports-color": { "node_modules/nodemon/node_modules/supports-color": {
"version": "5.5.0", "version": "5.5.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
@@ -2235,13 +2270,6 @@
"node": ">=8.10.0" "node": ">=8.10.0"
} }
}, },
"node_modules/regenerator-runtime": {
"version": "0.14.1",
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz",
"integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==",
"dev": true,
"license": "MIT"
},
"node_modules/regexp.prototype.flags": { "node_modules/regexp.prototype.flags": {
"version": "1.5.3", "version": "1.5.3",
"resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.3.tgz", "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.3.tgz",
@@ -2561,26 +2589,29 @@
} }
}, },
"node_modules/simple-update-notifier": { "node_modules/simple-update-notifier": {
"version": "1.1.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-1.1.0.tgz", "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz",
"integrity": "sha512-VpsrsJSUcJEseSbMHkrsrAVSdvVS5I96Qo1QAQ4FxQ9wXFcB+pjj7FB7/us9+GcgfW4ziHtYMc1J0PLczb55mg==", "integrity": "sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"semver": "~7.0.0" "semver": "^7.5.3"
}, },
"engines": { "engines": {
"node": ">=8.10.0" "node": ">=10"
} }
}, },
"node_modules/simple-update-notifier/node_modules/semver": { "node_modules/simple-update-notifier/node_modules/semver": {
"version": "7.0.0", "version": "7.7.2",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.0.0.tgz", "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz",
"integrity": "sha512-+GB6zVA9LWh6zovYQLALHwv5rb2PHGlJi3lfiqIHxR0uuwCgefcOJc59v9fv1w8GbStwxuuqqAjI9NMAOOgq1A==", "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==",
"dev": true, "dev": true,
"license": "ISC", "license": "ISC",
"bin": { "bin": {
"semver": "bin/semver.js" "semver": "bin/semver.js"
},
"engines": {
"node": ">=10"
} }
}, },
"node_modules/spawn-command": { "node_modules/spawn-command": {

View File

@@ -11,11 +11,12 @@
}, },
"devDependencies": { "devDependencies": {
"concurrently": "^7.0.0", "concurrently": "^7.0.0",
"nodemon": "^2.0.15", "nodemon": "^3.1.10",
"npm-run-all": "^4.1.5" "npm-run-all": "^4.1.5"
}, },
"dependencies": { "dependencies": {
"cors": "^2.8.5", "cors": "^2.8.5",
"dompurify": "^3.2.6",
"sequelize-cli": "^6.6.2" "sequelize-cli": "^6.6.2"
} }
} }