Add password reset localization and chat configuration

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

View File

@@ -308,7 +308,20 @@ class AdminService {
}
async getRooms() {
// Only return necessary fields to the frontend
return await Room.findAll({
attributes: [
'id',
'title',
'roomTypeId',
'isPublic',
'genderRestrictionId',
'minAge',
'maxAge',
'friendsOfOwnerOnly',
'requiredUserRightId',
'password' // only if needed for editing, otherwise remove
],
include: [
{ model: RoomType, as: 'roomType' },
{ model: UserParamValue, as: 'genderRestriction' },
@@ -316,6 +329,13 @@ class AdminService {
});
}
async updateRoom(id, data) {
const room = await Room.findByPk(id);
if (!room) throw new Error('Room not found');
await room.update(data);
return room;
}
async createRoom(data) {
return await Room.create(data);
}

View File

@@ -132,6 +132,22 @@ class ChatService {
(chat.user1Id === user2HashId && chat.user2Id === user1HashId)
);
}
async getRoomList() {
// Nur öffentliche Räume, keine sensiblen Felder
const { default: Room } = await import('../models/chat/room.js');
const { default: RoomType } = await import('../models/chat/room_type.js');
return Room.findAll({
attributes: [
'id', 'title', 'roomTypeId', 'isPublic', 'genderRestrictionId',
'minAge', 'maxAge', 'friendsOfOwnerOnly', 'requiredUserRightId'
],
where: { isPublic: true },
include: [
{ model: RoomType, as: 'roomType' }
]
});
}
}
export default new ChatService();

View File

@@ -0,0 +1,307 @@
import net from 'net';
import fs from 'fs';
import path from 'path';
const DEFAULT_CONFIG = { host: 'localhost', port: 1235 };
function loadBridgeConfig() {
try {
const cfgPath = path.resolve(process.cwd(), 'backend/config/chatBridge.json');
if (fs.existsSync(cfgPath)) {
const raw = fs.readFileSync(cfgPath, 'utf-8');
const json = JSON.parse(raw);
return { ...DEFAULT_CONFIG, ...json };
}
} catch (e) {
console.warn('Failed to load chatBridge.json, using defaults:', e.message);
}
return { ...DEFAULT_CONFIG };
}
export default class ChatTcpBridge {
constructor(ioSocket, user) {
this.ioSocket = ioSocket;
this.user = user || {};
this.token = null;
this.tcp = null;
this.buffer = '';
this.config = loadBridgeConfig();
this.pending = [];
this.objStart = -1;
this.depthCurly = 0;
this.depthSquare = 0;
this.inString = false;
this.escapeNext = false;
}
connect(initialRoomName = '') {
if (this.tcp) return;
const { host, port } = this.config;
this.ioEmitStatus('connecting', `${host}:${port}`);
console.log(`[ChatBridge] Connecting to ${host}:${port}`);
this.tcp = new net.Socket();
this.tcp.on('connect', () => {
this.ioEmitStatus('connected', `${host}:${port}`);
console.log(`[ChatBridge] Connected to ${host}:${port}`);
// Erstnachricht: init
const initPayload = {
type: 'init',
name: this.user.username || '',
room: initialRoomName || '',
};
this.sendRaw(initPayload);
});
this.tcp.on('data', (data) => {
this.processChunk(data.toString('utf-8'));
});
this.tcp.on('error', (err) => {
this.ioEmitStatus('error', err.message);
console.error('[ChatBridge] Error:', err.message);
});
this.tcp.on('close', () => {
this.ioEmitStatus('disconnected');
console.warn('[ChatBridge] Disconnected');
});
this.tcp.connect(port, host);
}
handleIncomingLine(line) {
try {
const obj = JSON.parse(line);
this.handleIncoming(obj);
} catch (e) {
this.ioSocket.emit('chat:incoming', { type: 'system', text: line });
}
}
processChunk(chunk) {
if (!chunk) return;
for (let i = 0; i < chunk.length; i++) {
const ch = chunk[i];
this.buffer += ch;
if (this.escapeNext) {
this.escapeNext = false;
continue;
}
if (this.inString) {
if (ch === '\\') { this.escapeNext = true; }
else if (ch === '"') { this.inString = false; }
continue;
}
if (ch === '"') { this.inString = true; continue; }
if (ch === '{') { this.depthCurly++; if (this.objStart === -1) this.objStart = this.buffer.length - 1; }
else if (ch === '}') { this.depthCurly--; }
else if (ch === '[') { this.depthSquare++; }
else if (ch === ']') { this.depthSquare--; }
// Wenn ein komplettes Objekt abgeschlossen ist (außerhalb von Strings und beide Tiefen 0)
if (this.objStart !== -1 && this.depthCurly === 0) {
const jsonStr = this.buffer.slice(this.objStart, this.buffer.length);
// Alles vor objStart (falls vorhanden) rausfiltern
const prefix = this.buffer.slice(0, this.objStart);
if (prefix.trim()) {
// Unerwarteter Text, als System ausgeben
this.ioSocket.emit('chat:incoming', { type: 'system', text: prefix.trim() });
}
this.buffer = '';
this.objStart = -1;
this.parseAndHandle(jsonStr);
}
}
}
parseAndHandle(jsonStr) {
try {
const obj = JSON.parse(jsonStr);
// Forward raw parsed inbound message to browser console
this.ioSocket.emit('chat:debug', { dir: 'in', payload: obj });
this.handleIncoming(obj);
} catch (e) {
// Wenn mehrere Objekte direkt aneinander hängen (}{), versuchen sie zu splitten
const parts = jsonStr.split('}{').map((p, idx, arr) => (idx === 0 ? p + '}' : (idx === arr.length - 1 ? '{' + p : '{' + p + '}')));
if (parts.length > 1) {
parts.forEach(p => {
try {
const o = JSON.parse(p);
this.ioSocket.emit('chat:debug', { dir: 'in', payload: o });
this.handleIncoming(o);
} catch (_) {
this.ioSocket.emit('chat:debug', { dir: 'in', raw: p });
this.ioSocket.emit('chat:incoming', { type: 'system', text: p });
}
});
} else {
this.ioSocket.emit('chat:debug', { dir: 'in', raw: jsonStr });
this.ioSocket.emit('chat:incoming', { type: 'system', text: jsonStr });
}
}
}
handleIncoming(obj) {
if (!obj) return;
// Map numeric type codes
if (typeof obj.type === 'number') {
switch (obj.type) {
case 1: // token
if (typeof obj.message === 'string' && obj.message) {
this.token = obj.message;
this.ioSocket.emit('chat:incoming', { type: 'system', text: 'Token received' });
const actions = this.pending; this.pending = [];
actions.forEach(fn => { try { fn(); } catch (_) { } });
return;
}
break;
case 3: // room list
if (Array.isArray(obj.message)) {
const names = obj.message.map(r => r.name).filter(Boolean).join(', ');
this.ioSocket.emit('chat:incoming', { type: 'system', text: names ? `Rooms: ${names}` : 'Rooms updated' });
return;
}
break;
case 5: {
// generic server event (room entered, color changes, etc.)
const msg = obj.message;
if (!msg) break;
if (typeof msg === 'string') {
if (msg === 'room_entered') {
const to = obj.to || obj.name || obj.room || '';
this.ioSocket.emit('chat:incoming', { type: 'system', code: 'room_entered', tr: 'room_entered', to });
return;
}
if (msg === 'color_changed' || msg === 'user_color_changed') {
this.ioSocket.emit('chat:incoming', {
type: 'system',
code: msg,
color: obj.color,
userName: obj.userName || ''
});
return;
}
// unknown message string; fallthrough to text echo
this.ioSocket.emit('chat:incoming', { type: 'system', text: msg });
return;
}
if (typeof msg === 'object') {
const tr = msg.tr || 'room_entered';
const to = msg.to || msg.name || msg.room || '';
this.ioSocket.emit('chat:incoming', { type: 'system', code: tr, tr, to });
return;
}
break;
}
case 6: {
// scream
const userName = obj.userName || obj.user || obj.name || '';
const message = obj.message || '';
this.ioSocket.emit('chat:incoming', {
type: 'scream',
userName,
message,
color: obj.color || null
});
return;
}
default:
break;
}
}
if (obj.type === 'token' && obj.message) {
this.token = obj.message;
this.ioSocket.emit('chat:incoming', { type: 'system', text: 'Token received' });
// Ausstehende Aktionen jetzt mit Token senden
const actions = this.pending;
this.pending = [];
actions.forEach(fn => {
try { fn(); } catch (_) { }
});
return;
}
// Normalize generic messages
if (typeof obj.message === 'string' && obj.message && (obj.userName || obj.user || obj.name)) {
this.ioSocket.emit('chat:incoming', {
type: 'message',
message: obj.message,
userName: obj.userName || obj.user || obj.name,
color: obj.color || null
});
return;
}
// As system fallback
this.ioSocket.emit('chat:incoming', { type: 'system', text: JSON.stringify(obj) });
}
sendRaw(obj) {
if (!this.tcp) return;
try {
const str = JSON.stringify(obj) + '\n';
// Forward outbound payload to browser console
this.ioSocket.emit('chat:debug', { dir: 'out', payload: obj });
this.tcp.write(str);
} catch (e) {
this.ioEmitStatus('error', e.message);
}
}
sendWithToken(obj) {
if (this.token) obj.token = this.token;
this.sendRaw(obj);
}
withTokenOrQueue(fn) {
if (this.token) {
fn();
} else {
this.pending.push(fn);
}
}
joinRoom(roomName, password = '') {
// Prefer keys name/room if server expects them for room switch; fallback to newroom if required elsewhere
this.withTokenOrQueue(() => this.sendWithToken({ type: 'join', room: roomName, name: this.user.username || '', password }));
}
sendMessage(text) {
this.withTokenOrQueue(() => this.sendWithToken({ type: 'message', message: text, userName: this.user.username }));
}
scream(text) {
this.withTokenOrQueue(() => this.sendWithToken({ type: 'scream', message: text }));
}
doAction(text) {
this.withTokenOrQueue(() => this.sendWithToken({ type: 'do', message: text }));
}
dice(expr) {
this.withTokenOrQueue(() => this.sendWithToken({ type: 'dice', message: expr || '' }));
}
color(hex) {
let value = typeof hex === 'string' ? hex.trim() : '';
if (!value) return;
if (!value.startsWith('#')) value = '#' + value;
// Normalize 3-digit to 6-digit when possible
const m3 = /^#([0-9a-fA-F]{3})$/.exec(value);
if (m3) {
value = '#' + m3[1].split('').map(c => c + c).join('');
}
const m6 = /^#([0-9a-fA-F]{6})$/.exec(value);
if (!m6) return;
this.withTokenOrQueue(() => this.sendWithToken({ type: 'color', value }));
}
close() {
try { this.tcp?.destroy(); } catch (_) { }
this.tcp = null;
}
ioEmitStatus(type, detail) {
this.ioSocket.emit('chat:status', { type, detail });
}
}