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.
4
backend/config/chatBridge.json
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
{
|
||||||
|
"host": "localhost",
|
||||||
|
"port": 1235
|
||||||
|
}
|
||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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 });
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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"],
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
307
backend/services/chatTcpBridge.js
Normal 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 });
|
||||||
|
}
|
||||||
|
}
|
||||||
BIN
frontend/public/images/icons/activity.png
Normal file
|
After Width: | Height: | Size: 189 KiB |
BIN
frontend/public/images/icons/colorpicker.png
Normal file
|
After Width: | Height: | Size: 124 KiB |
BIN
frontend/public/images/icons/multichat.png
Normal file
|
After Width: | Height: | Size: 9.4 KiB |
BIN
frontend/public/images/icons/multichat16.png
Normal file
|
After Width: | Height: | Size: 483 B |
BIN
frontend/public/images/icons/multichat24.png
Normal file
|
After Width: | Height: | Size: 631 B |
BIN
frontend/public/images/icons/scream.png
Normal file
|
After Width: | Height: | Size: 193 KiB |
BIN
frontend/public/images/icons/sendmessage.png
Normal file
|
After Width: | Height: | Size: 143 KiB |
@@ -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;
|
||||||
|
|||||||
6
frontend/src/api/chatApi.js
Normal 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, ... }
|
||||||
|
};
|
||||||
@@ -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() {
|
||||||
|
if (this.daemonSocket && this.daemonSocket.removeEventListener) {
|
||||||
this.daemonSocket.removeEventListener('workerStatus', this.handleDaemonMessage);
|
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() {
|
||||||
|
if (this.daemonSocket && this.daemonSocket.send) {
|
||||||
this.daemonSocket.send('{"event": "getWorkerStatus"}');
|
this.daemonSocket.send('{"event": "getWorkerStatus"}');
|
||||||
|
}
|
||||||
},
|
},
|
||||||
handleDaemonMessage(event) {
|
handleDaemonMessage(event) {
|
||||||
const status = JSON.parse(event.data);
|
const status = JSON.parse(event.data);
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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: {
|
||||||
|
|||||||
148
frontend/src/dialogues/chat/MultiChat.vue
Normal 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>
|
||||||
1711
frontend/src/dialogues/chat/MultiChatDialog.vue
Normal 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") }}
|
{{ $t("chat.randomchat.input") }}
|
||||||
<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() {
|
||||||
this.$refs.dialog.close();
|
// 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 axios.post('/api/chat/exit', { id: this.userId });
|
||||||
await this.removeUserFromChat();
|
await this.removeUserFromChat();
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
// Reset state and close widget
|
||||||
|
this.chatIsRunning = false;
|
||||||
|
this.partner = null;
|
||||||
|
this.messages = [];
|
||||||
|
this.$refs.dialog.close();
|
||||||
},
|
},
|
||||||
|
|
||||||
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}`;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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];
|
||||||
|
|||||||
@@ -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];
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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?"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,4 +1,45 @@
|
|||||||
{
|
{
|
||||||
|
"chat": {
|
||||||
|
"multichat": {
|
||||||
|
"title": "Multi-Chat",
|
||||||
|
"autoscroll": "Automatisch scrollen",
|
||||||
|
"options": "Optionen",
|
||||||
|
"send": "Senden",
|
||||||
|
"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"
|
||||||
|
}
|
||||||
|
},
|
||||||
"randomchat": {
|
"randomchat": {
|
||||||
"title": "Zufallschat",
|
"title": "Zufallschat",
|
||||||
"age": "Alter",
|
"age": "Alter",
|
||||||
@@ -27,4 +68,5 @@
|
|||||||
"startsearch": "Suche nächstes Gespräch",
|
"startsearch": "Suche nächstes Gespräch",
|
||||||
"selfstopped": "Du hast das Gespräch verlassen."
|
"selfstopped": "Du hast das Gespräch verlassen."
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
9
frontend/src/i18n/locales/de/passwordReset.json
Normal 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."
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,45 @@
|
|||||||
{
|
{
|
||||||
|
"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": {
|
"randomchat": {
|
||||||
"title": "Random Chat",
|
"title": "Random Chat",
|
||||||
"close": "Close",
|
"close": "Close",
|
||||||
@@ -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."
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
9
frontend/src/i18n/locales/en/passwordReset.json
Normal 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."
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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 }
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
87
frontend/src/services/chatWs.js
Normal 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'];
|
||||||
|
}
|
||||||
|
|
||||||
87
frontend/src/utils/chatConfig.js
Normal 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) };
|
||||||
|
}
|
||||||
@@ -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>
|
|
||||||
113
frontend/src/views/admin/RoomsView.vue
Normal 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>
|
||||||
@@ -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();
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 35 KiB |
115
package-lock.json
generated
@@ -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": {
|
||||||
|
|||||||
@@ -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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||