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.
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" />
|
||||
<ShowImageDialog ref="showImageDialog" />
|
||||
<MessageDialog ref="messageDialog" />
|
||||
<MultiChatDialog ref="multiChatDialog" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -37,6 +38,7 @@ import ErrorDialog from './dialogues/standard/ErrorDialog.vue';
|
||||
import ImprintDialog from './dialogues/standard/ImprintDialog.vue';
|
||||
import ShowImageDialog from './dialogues/socialnetwork/ShowImageDialog.vue';
|
||||
import MessageDialog from './dialogues/standard/MessageDialog.vue';
|
||||
import MultiChatDialog from './dialogues/chat/MultiChatDialog.vue';
|
||||
|
||||
export default {
|
||||
name: 'App',
|
||||
@@ -63,6 +65,7 @@ export default {
|
||||
ImprintDialog,
|
||||
ShowImageDialog,
|
||||
MessageDialog,
|
||||
MultiChatDialog,
|
||||
},
|
||||
created() {
|
||||
this.$i18n.locale = this.$store.getters.language;
|
||||
|
||||
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']),
|
||||
},
|
||||
mounted() {
|
||||
this.daemonSocket.addEventListener('workerStatus', () => { console.log('----'); });
|
||||
if (this.daemonSocket && this.daemonSocket.addEventListener) {
|
||||
this.daemonSocket.addEventListener('workerStatus', this.handleDaemonMessage);
|
||||
}
|
||||
},
|
||||
beforeUnmount() {
|
||||
this.daemonSocket.removeEventListener('workerStatus', this.handleDaemonMessage);
|
||||
if (this.daemonSocket && this.daemonSocket.removeEventListener) {
|
||||
this.daemonSocket.removeEventListener('workerStatus', this.handleDaemonMessage);
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
openImprintDialog() {
|
||||
@@ -48,7 +52,9 @@ export default {
|
||||
this.$store.dispatch('dialogs/toggleDialogMinimize', dialogName);
|
||||
},
|
||||
async showFalukantDaemonStatus() {
|
||||
this.daemonSocket.send('{"event": "getWorkerStatus"}');
|
||||
if (this.daemonSocket && this.daemonSocket.send) {
|
||||
this.daemonSocket.send('{"event": "getWorkerStatus"}');
|
||||
}
|
||||
},
|
||||
handleDaemonMessage(event) {
|
||||
const status = JSON.parse(event.data);
|
||||
|
||||
@@ -110,6 +110,7 @@ import { createApp } from 'vue';
|
||||
import apiClient from '@/utils/axios.js';
|
||||
|
||||
import RandomChatDialog from '../dialogues/chat/RandomChatDialog.vue';
|
||||
import MultiChatDialog from '../dialogues/chat/MultiChatDialog.vue';
|
||||
|
||||
// Wichtig: die zentrale Instanzen importieren
|
||||
import store from '@/store';
|
||||
@@ -119,7 +120,8 @@ import i18n from '@/i18n';
|
||||
export default {
|
||||
name: 'AppNavigation',
|
||||
components: {
|
||||
RandomChatDialog
|
||||
RandomChatDialog,
|
||||
MultiChatDialog
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
@@ -160,6 +162,22 @@ export default {
|
||||
methods: {
|
||||
...mapActions(['loadMenu', 'logout']),
|
||||
|
||||
openMultiChat() {
|
||||
// Räume können später dynamisch geladen werden, hier als Platzhalter ein Beispiel:
|
||||
const exampleRooms = [
|
||||
{ id: 1, title: 'Allgemein' },
|
||||
{ id: 2, title: 'Rollenspiel' }
|
||||
];
|
||||
const ref = this.$root.$refs.multiChatDialog;
|
||||
if (ref && typeof ref.open === 'function') {
|
||||
ref.open(exampleRooms);
|
||||
} else if (ref?.$refs?.dialog && typeof ref.$refs.dialog.open === 'function') {
|
||||
ref.$refs.dialog.open();
|
||||
} else {
|
||||
console.error('MultiChatDialog nicht bereit oder ohne open()');
|
||||
}
|
||||
},
|
||||
|
||||
async fetchForums() {
|
||||
try {
|
||||
const res = await apiClient.get('/api/forum');
|
||||
@@ -192,8 +210,6 @@ export default {
|
||||
// Datei erstellen und ans body anhängen
|
||||
const container = document.createElement('div');
|
||||
document.body.appendChild(container);
|
||||
// Programmatisch ein neues App-Instance randomChatauen, mit Store, Router & i18n
|
||||
this.$root.$refs.randomChatDialog.open(contact);
|
||||
},
|
||||
|
||||
/**
|
||||
@@ -211,7 +227,19 @@ export default {
|
||||
|
||||
// 2) view → Dialog/Window
|
||||
if (item.view) {
|
||||
this.$root.$refs[item.class].open();
|
||||
const dialogRef = this.$root.$refs[item.class];
|
||||
if (!dialogRef) {
|
||||
console.error(`Dialog-Ref '${item.class}' nicht gefunden! Bitte prüfe Ref und Menü-Konfiguration.`);
|
||||
return;
|
||||
}
|
||||
// Robust öffnen: erst open(), sonst auf inneres DialogWidget zurückgreifen
|
||||
if (typeof dialogRef.open === 'function') {
|
||||
dialogRef.open();
|
||||
} else if (dialogRef.$refs?.dialog && typeof dialogRef.$refs.dialog.open === 'function') {
|
||||
dialogRef.$refs.dialog.open();
|
||||
} else {
|
||||
console.error(`Dialog '${item.class}' gefunden, aber keine open()-Methode verfügbar.`);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<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>
|
||||
<label>{{ $t("passwordReset.email") }} <input type="email" v-model="email" required /></label>
|
||||
</div>
|
||||
@@ -18,7 +18,7 @@ export default {
|
||||
data() {
|
||||
return {
|
||||
email: '',
|
||||
buttons: [{ text: this.$t("passwordReset.reset") }]
|
||||
buttons: [{ text: 'passwordReset.reset', action: 'reset' }]
|
||||
};
|
||||
},
|
||||
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>
|
||||
<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">
|
||||
<div v-if="chatIsRunning" class="randomchat">
|
||||
<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="agetosearch" min="18" max="150" size="5" />
|
||||
<span class="multiselect">
|
||||
{{ $t("randomchat.gendersearch") }}
|
||||
{{ $t("chat.randomchat.gendersearch") }}
|
||||
<div>
|
||||
<label><input type="checkbox" v-model="searchmale" />{{ $t("randomchat.gender.male") }}</label>
|
||||
<label><input type="checkbox" v-model="searchfemale" />{{ $t("randomchat.gender.female")
|
||||
<label><input type="checkbox" v-model="searchmale" />{{ $t("chat.randomchat.gender.male") }}</label>
|
||||
<label><input type="checkbox" v-model="searchfemale" />{{ $t("chat.randomchat.gender.female")
|
||||
}}</label>
|
||||
</div>
|
||||
</span>
|
||||
<label><input type="checkbox" v-model="camonlysearch" />{{ $t("randomchat.camonly") }}</label>
|
||||
<label><input type="checkbox" v-model="showcam" />{{ $t("randomchat.showcam") }}</label>
|
||||
<img v-if="isLoggedIn" src="/images/icons/friendsadd16.png" :tooltip="$t('randomchat.addfriend')" />
|
||||
<label><input type="checkbox" v-model="autosearch" />{{ $t("randomchat.autosearch") }}</label>
|
||||
<button @click="nextUser" v-if="partner != null">{{ $t("randomchat.jumptonext") }}</button>
|
||||
<button @click="startSearch" v-if="partner == null && !searching">{{ $t("randomchat.startsearch")
|
||||
<label><input type="checkbox" v-model="camonlysearch" />{{ $t("chat.randomchat.camonly") }}</label>
|
||||
<label><input type="checkbox" v-model="showcam" />{{ $t("chat.randomchat.showcam") }}</label>
|
||||
<img v-if="isLoggedIn" src="/images/icons/friendsadd16.png" :tooltip="$t('chat.randomchat.addfriend')" />
|
||||
<label><input type="checkbox" v-model="autosearch" />{{ $t("chat.randomchat.autosearch") }}</label>
|
||||
<button @click="nextUser" v-if="partner != null">{{ $t("chat.randomchat.jumptonext") }}</button>
|
||||
<button @click="startSearch" v-if="partner == null && !searching">{{ $t("chat.randomchat.startsearch")
|
||||
}}</button>
|
||||
</div>
|
||||
<div class="output">
|
||||
@@ -28,7 +28,7 @@
|
||||
</div>
|
||||
<div class="inputline">
|
||||
<label>
|
||||
{{ $t("randomchat.input") }}
|
||||
{{ $t("chat.randomchat.input") }}
|
||||
<input type="text" v-model="inputtext" @keyup.enter="sendMessage" />
|
||||
</label>
|
||||
<img src="/images/icons/enter16.png" @click="sendMessage" />
|
||||
@@ -37,20 +37,20 @@
|
||||
</div>
|
||||
<div v-else>
|
||||
<div>
|
||||
<label>{{ $t("randomchat.age") }}
|
||||
<label>{{ $t("chat.randomchat.age") }}
|
||||
<input type="number" v-model="age" min="18" max="150" value="18" />
|
||||
</label>
|
||||
</div>
|
||||
<div>
|
||||
<label>{{ $t("randomchat.gender.title") }}
|
||||
<label>{{ $t("chat.randomchat.gender.title") }}
|
||||
<select v-model="gender">
|
||||
<option value="f">{{ $t("randomchat.gender.female") }}</option>
|
||||
<option value="m">{{ $t("randomchat.gender.male") }}</option>
|
||||
<option value="f">{{ $t("chat.randomchat.gender.female") }}</option>
|
||||
<option value="m">{{ $t("chat.randomchat.gender.male") }}</option>
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
<div>
|
||||
<button @click="startRandomChat()">{{ $t("randomchat.start") }}</button>
|
||||
<button @click="startRandomChat()">{{ $t("chat.randomchat.start") }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</DialogWidget>
|
||||
@@ -60,6 +60,7 @@
|
||||
import DialogWidget from '@/components/DialogWidget.vue';
|
||||
import { mapGetters } from 'vuex';
|
||||
import axios from 'axios';
|
||||
import DOMPurify from 'dompurify';
|
||||
|
||||
export default {
|
||||
name: 'RandomChatDialog',
|
||||
@@ -69,7 +70,8 @@ export default {
|
||||
computed: {
|
||||
...mapGetters(['isLoggedIn', 'user']),
|
||||
buttons() {
|
||||
return [{ text: this.$t('randomchat.close') }];
|
||||
// Use translation key; DialogWidget will translate when isTitleTranslated=true
|
||||
return [{ text: 'chat.randomchat.close' }];
|
||||
},
|
||||
},
|
||||
data() {
|
||||
@@ -113,26 +115,6 @@ export default {
|
||||
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() {
|
||||
try {
|
||||
const response = await axios.post('/api/chat/register', {
|
||||
@@ -146,9 +128,29 @@ export default {
|
||||
},
|
||||
|
||||
async closeDialog() {
|
||||
// Stop intervals first
|
||||
if (this.searchInterval) {
|
||||
clearInterval(this.searchInterval);
|
||||
this.searchInterval = null;
|
||||
}
|
||||
if (this.messagesInterval) {
|
||||
clearInterval(this.messagesInterval);
|
||||
this.messagesInterval = null;
|
||||
}
|
||||
// Inform backend and cleanup
|
||||
try {
|
||||
if (this.userId) {
|
||||
await axios.post('/api/chat/exit', { id: this.userId });
|
||||
await this.removeUserFromChat();
|
||||
}
|
||||
} catch (e) {
|
||||
// ignore
|
||||
}
|
||||
// Reset state and close widget
|
||||
this.chatIsRunning = false;
|
||||
this.partner = null;
|
||||
this.messages = [];
|
||||
this.$refs.dialog.close();
|
||||
await axios.post('/api/chat/exit', { id: this.userId });
|
||||
await this.removeUserFromChat();
|
||||
},
|
||||
|
||||
async startRandomChat() {
|
||||
@@ -160,7 +162,7 @@ export default {
|
||||
async startSearch() {
|
||||
this.searching = true;
|
||||
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);
|
||||
},
|
||||
|
||||
@@ -174,10 +176,10 @@ export default {
|
||||
if (response.data.status === 'matched') {
|
||||
this.searching = false;
|
||||
clearInterval(this.searchInterval);
|
||||
const initText = this.$t('randomchat.chatpartner')
|
||||
const initText = this.$t('chat.randomchat.chatpartner')
|
||||
.replace(
|
||||
'<gender>',
|
||||
this.$t(`randomchat.partnergender${response.data.user.gender}`)
|
||||
this.$t(`chat.randomchat.partnergender${response.data.user.gender}`)
|
||||
)
|
||||
.replace('<age>', response.data.user.age);
|
||||
this.messages = [{ type: 'system', text: initText }];
|
||||
@@ -220,7 +222,7 @@ export default {
|
||||
activities.forEach((act) => {
|
||||
if (act.activity === 'otheruserleft') {
|
||||
this.partner = null;
|
||||
this.messages.push({ type: 'system', tr: 'randomchat.userleftchat' });
|
||||
this.messages.push({ type: 'system', tr: 'chat.randomchat.userleftchat' });
|
||||
}
|
||||
});
|
||||
this.messages.push(...newMsgs);
|
||||
@@ -237,10 +239,10 @@ export default {
|
||||
async nextUser() {
|
||||
await axios.post('/api/chat/leave', { id: this.userId });
|
||||
this.partner = null;
|
||||
this.messages.push({ type: 'system', tr: 'randomchat.selfstopped' });
|
||||
this.messages.push({ type: 'system', tr: 'chat.randomchat.selfstopped' });
|
||||
if (this.autosearch) {
|
||||
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>`;
|
||||
}
|
||||
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}`;
|
||||
},
|
||||
},
|
||||
|
||||
@@ -67,7 +67,7 @@
|
||||
<div v-for="entry in guestbookEntries" :key="entry.id" class="guestbook-entry">
|
||||
<img v-if="entry.image" :src="entry.image.url" alt="Entry Image"
|
||||
style="max-width: 400px; max-height: 400px;" />
|
||||
<p v-html="entry.contentHtml"></p>
|
||||
<p v-html="sanitizedContent(entry)"></p>
|
||||
<div class="entry-info">
|
||||
<span class="entry-timestamp">{{ new Date(entry.createdAt).toLocaleString() }}</span>
|
||||
<span class="entry-user">
|
||||
@@ -96,6 +96,7 @@ import apiClient from '@/utils/axios.js';
|
||||
import FolderItem from '../../components/FolderItem.vue';
|
||||
import { Editor, EditorContent } from '@tiptap/vue-3'
|
||||
import StarterKit from '@tiptap/starter-kit'
|
||||
import DOMPurify from 'dompurify';
|
||||
|
||||
export default {
|
||||
name: 'UserProfileDialog',
|
||||
@@ -369,7 +370,10 @@ export default {
|
||||
} else {
|
||||
this.friendshipState = 'waiting';
|
||||
}
|
||||
}
|
||||
},
|
||||
sanitizedContent(entry) {
|
||||
return DOMPurify.sanitize(entry.contentHtml);
|
||||
},
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -11,13 +11,14 @@
|
||||
@ok="handleOk"
|
||||
name="DataPrivacyDialog"
|
||||
>
|
||||
<div v-html="dataPrivacyContent"></div>
|
||||
<div v-html="sanitizedContent"></div>
|
||||
</DialogWidget>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import DialogWidget from '../../components/DialogWidget.vue';
|
||||
import content from '../../content/content.js';
|
||||
import DOMPurify from 'dompurify';
|
||||
|
||||
export default {
|
||||
name: 'DataPrivacyDialog',
|
||||
@@ -29,6 +30,11 @@ export default {
|
||||
dataPrivacyContent: content.dataPrivacy[this.$i18n.locale]
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
sanitizedContent() {
|
||||
return DOMPurify.sanitize(this.dataPrivacyContent);
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
'$i18n.locale'(newLocale) {
|
||||
this.dataPrivacyContent = content.dataPrivacy[newLocale];
|
||||
|
||||
@@ -11,13 +11,14 @@
|
||||
@ok="handleOk"
|
||||
name="ImprintDialog"
|
||||
>
|
||||
<div v-html="imprintContent"></div>
|
||||
<div v-html="sanitizedContent"></div>
|
||||
</DialogWidget>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import DialogWidget from '../../components/DialogWidget.vue';
|
||||
import content from '../../content/content.js';
|
||||
import DOMPurify from 'dompurify';
|
||||
|
||||
export default {
|
||||
name: 'ImprintDialog',
|
||||
@@ -29,6 +30,11 @@ export default {
|
||||
imprintContent: content.imprint[this.$i18n.locale]
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
sanitizedContent() {
|
||||
return DOMPurify.sanitize(this.imprintContent);
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
'$i18n.locale'(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 enFriends from './locales/en/friends.json';
|
||||
import enFalukant from './locales/en/falukant.json';
|
||||
import enPasswordReset from './locales/en/passwordReset.json';
|
||||
|
||||
import deGeneral from './locales/de/general.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 deFriends from './locales/de/friends.json';
|
||||
import deFalukant from './locales/de/falukant.json';
|
||||
import dePasswordReset from './locales/de/passwordReset.json';
|
||||
|
||||
const messages = {
|
||||
en: {
|
||||
@@ -37,6 +39,7 @@ const messages = {
|
||||
...enHome,
|
||||
...enChat,
|
||||
...enRegister,
|
||||
...enPasswordReset,
|
||||
...enError,
|
||||
...enActivate,
|
||||
...enSettings,
|
||||
@@ -53,6 +56,7 @@ const messages = {
|
||||
...deHome,
|
||||
...deChat,
|
||||
...deRegister,
|
||||
...dePasswordReset,
|
||||
...deError,
|
||||
...deActivate,
|
||||
...deSettings,
|
||||
|
||||
@@ -91,7 +91,8 @@
|
||||
"start game": "Spiel starten",
|
||||
"open room": "Raum öffnen",
|
||||
"systemmessage": "Systemnachricht"
|
||||
}
|
||||
},
|
||||
"confirmDelete": "Soll dieser Chatraum wirklich gelöscht werden?"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,30 +1,72 @@
|
||||
{
|
||||
"randomchat": {
|
||||
"title": "Zufallschat",
|
||||
"age": "Alter",
|
||||
"gender": {
|
||||
"title": "Dein Geschlecht",
|
||||
"male": "Männlich",
|
||||
"female": "Weiblich"
|
||||
"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"
|
||||
}
|
||||
},
|
||||
"start": "Loslegen",
|
||||
"agerange": "Alter",
|
||||
"gendersearch": "Geschlechter",
|
||||
"camonly": "Nur mit Cam",
|
||||
"showcam": "Eigene Cam anzeigen",
|
||||
"addfriend": "Zu Freunden hinzufügen",
|
||||
"close": "Chat beenden",
|
||||
"autosearch": "Automatisch suchen",
|
||||
"input": "Ihr Text",
|
||||
"waitingForMatch": "Warten auf einen Teilnehmer...",
|
||||
"chatpartner": "Du chattest jetzt mit einer <gender> Person im Alter von <age> Jahren.",
|
||||
"partnergenderm": "männlichen",
|
||||
"partnergenderf": "weiblichen",
|
||||
"self": "Du",
|
||||
"partner": "Partner",
|
||||
"jumptonext": "Diesen Chat beenden",
|
||||
"userleftchat": "Der Gesprächstpartner hat den Chat verlassen.",
|
||||
"startsearch": "Suche nächstes Gespräch",
|
||||
"selfstopped": "Du hast das Gespräch verlassen."
|
||||
"randomchat": {
|
||||
"title": "Zufallschat",
|
||||
"age": "Alter",
|
||||
"gender": {
|
||||
"title": "Dein Geschlecht",
|
||||
"male": "Männlich",
|
||||
"female": "Weiblich"
|
||||
},
|
||||
"start": "Loslegen",
|
||||
"agerange": "Alter",
|
||||
"gendersearch": "Geschlechter",
|
||||
"camonly": "Nur mit Cam",
|
||||
"showcam": "Eigene Cam anzeigen",
|
||||
"addfriend": "Zu Freunden hinzufügen",
|
||||
"close": "Chat beenden",
|
||||
"autosearch": "Automatisch suchen",
|
||||
"input": "Ihr Text",
|
||||
"waitingForMatch": "Warten auf einen Teilnehmer...",
|
||||
"chatpartner": "Du chattest jetzt mit einer <gender> Person im Alter von <age> Jahren.",
|
||||
"partnergenderm": "männlichen",
|
||||
"partnergenderf": "weiblichen",
|
||||
"self": "Du",
|
||||
"partner": "Partner",
|
||||
"jumptonext": "Diesen Chat beenden",
|
||||
"userleftchat": "Der Gesprächstpartner hat den Chat verlassen.",
|
||||
"startsearch": "Suche nächstes Gespräch",
|
||||
"selfstopped": "Du hast das Gespräch verlassen."
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -41,5 +41,12 @@
|
||||
"transmale": "Trans-Mann",
|
||||
"transfemale": "Trans-Frau",
|
||||
"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,5 +1,46 @@
|
||||
{
|
||||
"randomchat": {
|
||||
"chat": {
|
||||
"multichat": {
|
||||
"title": "Multi Chat",
|
||||
"autoscroll": "Auto scroll",
|
||||
"options": "Options",
|
||||
"send": "Send",
|
||||
"shout": "Shout",
|
||||
"action": "Action",
|
||||
"roll": "Roll",
|
||||
"colorpicker": "Pick color",
|
||||
"colorpicker_preview": "Preview: This message uses the chosen color.",
|
||||
"hex": "HEX",
|
||||
"invalid_hex": "Invalid hex value",
|
||||
"hue": "Hue",
|
||||
"saturation": "Saturation",
|
||||
"lightness": "Lightness",
|
||||
"ok": "Ok",
|
||||
"cancel": "Cancel",
|
||||
"placeholder": "Type a message…",
|
||||
"action_select_user": "Please select a user",
|
||||
"action_to": "Action to {to}",
|
||||
"action_phrases": {
|
||||
"left_room": "switches to room",
|
||||
"leaves_room": "leaves room",
|
||||
"left_chat": "has left the chat."
|
||||
},
|
||||
"system": {
|
||||
"room_entered": "You entered the room \"{room}\".",
|
||||
"user_entered_room": "{user} has entered the room.",
|
||||
"user_left_room": "{user} has left the room."
|
||||
,
|
||||
"color_changed_self": "You changed your color to {color}.",
|
||||
"color_changed_user": "{user} changed their color to {color}."
|
||||
},
|
||||
"status": {
|
||||
"connecting": "Connecting…",
|
||||
"connected": "Connected",
|
||||
"disconnected": "Disconnected",
|
||||
"error": "Connection error"
|
||||
}
|
||||
},
|
||||
"randomchat": {
|
||||
"title": "Random Chat",
|
||||
"close": "Close",
|
||||
"age": "Age",
|
||||
@@ -16,6 +57,16 @@
|
||||
"autosearch": "Auto Search",
|
||||
"input": "Input",
|
||||
"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 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 AdminFalukantEditUserView from '../views/admin/falukant/EditUserView.vue'
|
||||
|
||||
@@ -26,7 +26,7 @@ const adminRoutes = [
|
||||
{
|
||||
path: '/admin/chatrooms',
|
||||
name: 'AdminChatRooms',
|
||||
component: ChatRoomsView,
|
||||
component: RoomsView,
|
||||
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: {
|
||||
...mapActions(['login']),
|
||||
openRandomChat() {
|
||||
this.$refs.randomChatDialog.open();
|
||||
const dlg = this.$refs.randomChatDialog;
|
||||
if (dlg && typeof dlg.open === 'function') dlg.open();
|
||||
},
|
||||
openRegisterDialog() {
|
||||
this.$refs.registerDialog.open();
|
||||
const dlg = this.$refs.registerDialog;
|
||||
if (dlg && typeof dlg.open === 'function') dlg.open();
|
||||
},
|
||||
openPasswordResetDialog() {
|
||||
this.$refs.passwordResetDialog.open();
|
||||
const dlg = this.$refs.passwordResetDialog;
|
||||
if (dlg && typeof dlg.open === 'function') dlg.open();
|
||||
},
|
||||
focusPassword() {
|
||||
this.$refs.passwordInput.focus();
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
<div v-if="diaryEntries.length === 0">{{ $t('socialnetwork.diary.noEntries') }}</div>
|
||||
<div v-else class="diary-entries">
|
||||
<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">
|
||||
<span class="entry-timestamp">{{ new Date(entry.createdAt).toLocaleString() }}</span>
|
||||
<span class="entry-actions">
|
||||
@@ -39,6 +39,7 @@
|
||||
import { mapGetters } from 'vuex';
|
||||
import apiClient from '@/utils/axios.js';
|
||||
import ChooseDialog from "@/dialogues/standard/ChooseDialog.vue";
|
||||
import DOMPurify from 'dompurify';
|
||||
|
||||
export default {
|
||||
name: 'DiaryView',
|
||||
@@ -59,6 +60,9 @@ export default {
|
||||
...mapGetters(['user']),
|
||||
},
|
||||
methods: {
|
||||
sanitizedText(entry) {
|
||||
return DOMPurify.sanitize(entry.text);
|
||||
},
|
||||
async loadDiaryEntries(page) {
|
||||
try {
|
||||
console.log(page);
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<h3 v-if="forumTopic">{{ forumTopic }}</h3>
|
||||
<ul class="messages">
|
||||
<li v-for="message in messages" :key="message.id">
|
||||
<div v-html="message.text"></div>
|
||||
<div v-html="sanitizedMessage(message)"></div>
|
||||
<div class="footer">
|
||||
<span class="link" @click="openProfile(message.lastMessageUser.hashedId)">
|
||||
{{ message.lastMessageUser.username }}
|
||||
@@ -23,6 +23,7 @@
|
||||
import { Editor, EditorContent } from '@tiptap/vue-3'
|
||||
import StarterKit from '@tiptap/starter-kit'
|
||||
import apiClient from '../../utils/axios'
|
||||
import DOMPurify from 'dompurify'
|
||||
|
||||
export default {
|
||||
name: 'ForumTopicView',
|
||||
@@ -87,6 +88,9 @@ export default {
|
||||
},
|
||||
openForum() {
|
||||
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">
|
||||
<img v-if="entry.image" :src="entry.image.url" alt="Entry Image"
|
||||
style="max-width: 400px; max-height: 400px;" />
|
||||
<p v-html="entry.contentHtml"></p>
|
||||
<p v-html="sanitizedContent(entry)"></p>
|
||||
<div class="entry-info">
|
||||
<span class="entry-timestamp">{{ new Date(entry.createdAt).toLocaleString() }}</span>
|
||||
<span class="entry-user">
|
||||
@@ -30,6 +30,7 @@
|
||||
<script>
|
||||
import { mapActions, mapGetters } from 'vuex';
|
||||
import apiClient from '@/utils/axios.js';
|
||||
import DOMPurify from 'dompurify';
|
||||
|
||||
export default {
|
||||
name: 'GuestbookView',
|
||||
@@ -73,6 +74,9 @@ export default {
|
||||
console.error('Error fetching image:', error);
|
||||
}
|
||||
},
|
||||
sanitizedContent(entry) {
|
||||
return DOMPurify.sanitize(entry.contentHtml);
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.loadGuestbookEntries(1);
|
||||
|
||||