Refactor backend CORS settings to include default origins and improve error handling in chat services: Introduce dynamic CORS origin handling, enhance RabbitMQ message sending with fallback mechanisms, and update WebSocket service to manage pending messages. Update UI components for better accessibility and responsiveness, including adjustments to dialog and navigation elements. Enhance styling for improved user experience across various components.

This commit is contained in:
Torsten Schulz (local)
2026-03-19 14:44:04 +01:00
parent 4442937ebd
commit 9d44a265ca
67 changed files with 5426 additions and 1099 deletions

View File

@@ -1,7 +1,12 @@
<template>
<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 class="form-stack">
<div class="form-field">
<label for="password-reset-email">{{ $t("passwordReset.email") }}</label>
<input id="password-reset-email" type="email" v-model="email" required :class="{ 'field-error': emailTouched && !isEmailValid }" />
<span class="form-hint">Wir senden den Link an die hinterlegte E-Mail-Adresse.</span>
<span v-if="emailTouched && !isEmailValid" class="form-error">Bitte eine gueltige E-Mail-Adresse eingeben.</span>
</div>
</div>
</DialogWidget>
</template>
@@ -9,6 +14,7 @@
<script>
import apiClient from '@/utils/axios.js';
import DialogWidget from '@/components/DialogWidget.vue';
import { showApiError, showSuccess } from '@/utils/feedback.js';
export default {
name: 'PasswordResetDialog',
@@ -18,9 +24,21 @@ export default {
data() {
return {
email: '',
buttons: [{ text: 'passwordReset.reset', action: 'reset' }]
emailTouched: false,
buttons: [{ text: 'passwordReset.reset', action: 'reset', disabled: true }]
};
},
computed: {
isEmailValid() {
return /\S+@\S+\.\S+/.test(this.email);
}
},
watch: {
email() {
this.emailTouched = true;
this.buttons[0].disabled = !this.isEmailValid;
}
},
methods: {
open() {
this.$refs.dialog.open();
@@ -29,15 +47,18 @@ export default {
this.$refs.dialog.close();
},
async resetPassword() {
if (!this.isEmailValid) {
return;
}
try {
await apiClient.post('/api/users/requestPasswordReset', {
email: this.email
});
this.$refs.dialog.close();
alert(this.$t("passwordReset.success"));
showSuccess(this, 'tr:passwordReset.success');
} catch (error) {
console.error('Error resetting password:', error);
alert(this.$t("passwordReset.failure"));
showApiError(this, error, 'tr:passwordReset.failure');
}
}
}

View File

@@ -2,18 +2,27 @@
<DialogWidget ref="dialog" title="register.title" :show-close="true" :buttons="buttons" :modal="true"
@close="closeDialog" @register="register" width="35em" height="33em" name="RegisterDialog"
:isTitleTranslated="true">
<div class="form-content">
<div>
<label>{{ $t("register.email") }}<input type="email" v-model="email" /></label>
<div class="form-content form-stack">
<div class="form-field">
<label for="register-email">{{ $t("register.email") }}</label>
<input id="register-email" type="email" v-model="email" :class="{ 'field-error': emailTouched && !isEmailValid }" />
<span v-if="emailTouched && !isEmailValid" class="form-error">Bitte eine gueltige E-Mail-Adresse eingeben.</span>
</div>
<div>
<label>{{ $t("register.username") }}<input type="text" v-model="username" /></label>
<div class="form-field">
<label for="register-username">{{ $t("register.username") }}</label>
<input id="register-username" type="text" v-model="username" :class="{ 'field-error': usernameTouched && !isUsernameValid }" />
<span v-if="usernameTouched && !isUsernameValid" class="form-error">Der Benutzername sollte mindestens 3 Zeichen haben.</span>
</div>
<div>
<label>{{ $t("register.password") }}<input type="password" v-model="password" /></label>
<div class="form-field">
<label for="register-password">{{ $t("register.password") }}</label>
<input id="register-password" type="password" v-model="password" :class="{ 'field-error': passwordTouched && !isPasswordValid }" />
<span class="form-hint">Mindestens 8 Zeichen.</span>
<span v-if="passwordTouched && !isPasswordValid" class="form-error">Das Passwort ist noch zu kurz.</span>
</div>
<div>
<label>{{ $t("register.repeatPassword") }}<input type="password" v-model="repeatPassword" /></label>
<div class="form-field">
<label for="register-repeat-password">{{ $t("register.repeatPassword") }}</label>
<input id="register-repeat-password" type="password" v-model="repeatPassword" :class="{ 'field-error': repeatPasswordTouched && !doPasswordsMatch }" />
<span v-if="repeatPasswordTouched && !doPasswordsMatch" class="form-error">Die Passwoerter stimmen nicht ueberein.</span>
</div>
<SelectDropdownWidget labelTr="settings.personal.label.language" :v-model="language"
tooltipTr="settings.personal.tooltip.language" :list="languages" :value="language" />
@@ -26,6 +35,7 @@ import { mapActions } from 'vuex';
import apiClient from '@/utils/axios.js';
import DialogWidget from '@/components/DialogWidget.vue';
import SelectDropdownWidget from '@/components/form/SelectDropdownWidget.vue';
import { showApiError, showError } from '@/utils/feedback.js';
export default {
name: 'RegisterDialog',
@@ -41,6 +51,10 @@ export default {
repeatPassword: '',
language: null,
languages: [],
emailTouched: false,
usernameTouched: false,
passwordTouched: false,
repeatPasswordTouched: false,
buttons: [
{ text: 'register.close', action: 'close' },
{ text: 'register.register', action: 'register', disabled: !this.canRegister }
@@ -48,11 +62,35 @@ export default {
};
},
computed: {
isEmailValid() {
return /\S+@\S+\.\S+/.test(this.email);
},
isUsernameValid() {
return this.username.trim().length >= 3;
},
isPasswordValid() {
return this.password.length >= 8;
},
doPasswordsMatch() {
return Boolean(this.password) && this.password === this.repeatPassword;
},
canRegister() {
return this.password && this.repeatPassword && this.password === this.repeatPassword;
return this.isEmailValid && this.isUsernameValid && this.isPasswordValid && this.doPasswordsMatch && this.language;
}
},
watch: {
email() {
this.emailTouched = true;
},
username() {
this.usernameTouched = true;
},
password() {
this.passwordTouched = true;
},
repeatPassword() {
this.repeatPasswordTouched = true;
},
canRegister(newValue) {
this.buttons[1].disabled = !newValue;
}
@@ -82,7 +120,7 @@ export default {
},
async register() {
if (!this.canRegister) {
this.$root.$refs.errrorDialog.open('tr:register.passwordMismatch');
showError(this, 'tr:register.passwordMismatch');
return;
}
@@ -99,14 +137,14 @@ export default {
this.$refs.dialog.close();
this.$router.push('/activate');
} else {
this.$root.$refs.errrorDialog.open("tr:register.failure");
showError(this, 'tr:register.failure');
}
} catch (error) {
if (error.response && error.response.status === 409) {
this.$root.$refs.errrorDialog.open('tr:register.' + error.response.data.error);
showError(this, `tr:register.${error.response.data.error}`);
} else {
console.error('Error registering user:', error);
this.$root.$refs.errrorDialog.open('tr:register.' + error.response.data.error);
showApiError(this, error, 'tr:register.failure');
}
}
},
@@ -125,21 +163,11 @@ export default {
</script>
<style scoped>
.form-content>div {
margin-bottom: 1em;
}
label {
display: block;
margin-bottom: 0.5em;
}
input[type="email"],
input[type="text"],
input[type="password"],
select {
width: 100%;
padding: 0.5em;
box-sizing: border-box;
}
</style>

View File

@@ -60,7 +60,7 @@
{{ $t('falukant.branch.selection.selected') }}:
<strong>{{ selectedRegion.name }}</strong>
</div>
<label class="form-label">
<label class="form-label form-field">
{{ $t('falukant.branch.columns.type') }}
<select v-model="selectedType" class="form-control">
<option
@@ -72,8 +72,10 @@
({{ formatCost(computeBranchCost(type)) }})
</option>
</select>
<span class="form-hint">Waehle zuerst Region und dann den Niederlassungstyp.</span>
</label>
</div>
<div v-else class="form-hint">Waehle auf der Karte eine freie Region aus.</div>
</div>
</div>
</div>
@@ -83,6 +85,7 @@
<script>
import DialogWidget from '@/components/DialogWidget.vue';
import apiClient from '@/utils/axios.js';
import { showApiError, showError, showSuccess } from '@/utils/feedback.js';
export default {
name: 'CreateBranchDialog',
@@ -109,7 +112,7 @@
dialogButtons() {
return [
{ text: this.$t('Cancel'), action: this.close },
{ text: this.$t('falukant.branch.actions.create'), action: this.onConfirm },
{ text: this.$t('falukant.branch.actions.create'), action: this.onConfirm, disabled: !this.selectedRegion || !this.selectedType },
];
},
},
@@ -144,7 +147,10 @@
},
async onConfirm() {
if (!this.selectedRegion || !this.selectedType) return;
if (!this.selectedRegion || !this.selectedType) {
showError(this, 'Bitte zuerst Region und Typ auswaehlen.');
return;
}
try {
await apiClient.post('/api/falukant/branches', {
@@ -152,13 +158,14 @@
branchTypeId: this.selectedType,
});
this.$emit('create-branch');
showSuccess(this, 'Niederlassung erfolgreich erstellt.');
this.close();
} catch (e) {
if (e?.response?.status === 412 && e?.response?.data?.error === 'insufficientFunds') {
alert(this.$t('falukant.branch.actions.insufficientFunds'));
showError(this, this.$t('falukant.branch.actions.insufficientFunds'));
} else {
console.error('Error creating branch', e);
alert(this.$t('falukant.error.generic') || 'Fehler beim Erstellen der Niederlassung.');
showApiError(this, e, this.$t('falukant.error.generic') || 'Fehler beim Erstellen der Niederlassung.');
}
}
},
@@ -365,4 +372,4 @@
border-radius: 4px;
}
</style>

View File

@@ -1,7 +1,8 @@
<template>
<DialogWidget ref="dialog" title="error.title" :show-close="true" :buttons="buttons" :modal="true" width="25em"
height="15em" name="ErrorDialog" :isTitleTranslated=true>
<DialogWidget ref="dialog" title="error.title" :show-close="true" :buttons="buttons" :modal="true" width="28em"
height="16em" name="ErrorDialog" :isTitleTranslated=true>
<div class="error-content">
<span class="error-content__badge">Fehler</span>
<p>{{ translatedErrorMessage }}</p>
</div>
</DialogWidget>
@@ -45,8 +46,27 @@ export default {
<style scoped>
.error-content {
padding: 1em;
color: red;
display: grid;
gap: 12px;
padding: 1.2em;
text-align: center;
}
.error-content__badge {
justify-self: center;
padding: 4px 10px;
border-radius: 999px;
background: rgba(177, 59, 53, 0.12);
color: var(--color-danger);
font-size: 0.78rem;
font-weight: 700;
letter-spacing: 0.06em;
text-transform: uppercase;
}
.error-content p {
margin: 0;
color: var(--color-text-primary);
line-height: 1.55;
}
</style>

View File

@@ -1,7 +1,8 @@
<template>
<DialogWidget ref="dialog" :title="translatedTitle" :show-close="true" :buttons="translatedButtons" :modal="true" width="25em"
height="15em" name="MessageDialog" :isTitleTranslated=false>
<DialogWidget ref="dialog" :title="translatedTitle" :show-close="true" :buttons="translatedButtons" :modal="true" width="28em"
height="16em" name="MessageDialog" :isTitleTranslated=false>
<div class="message-content">
<span class="message-content__badge">Hinweis</span>
<p>{{ translatedMessage }}</p>
</div>
</DialogWidget>
@@ -41,14 +42,6 @@ export default {
if (this.message.startsWith('tr:')) {
const i18nKey = this.message.substring(3);
const translation = this.$t(i18nKey);
console.log('translatedMessage:', {
i18nKey: i18nKey,
translation: translation,
parameters: this.parameters,
allMinigames: this.$t('minigames'),
crashSection: this.$t('minigames.taxi.crash')
});
// Ersetze Parameter in der Übersetzung
return this.interpolateParameters(translation);
}
return this.message;
@@ -89,26 +82,16 @@ export default {
}
},
interpolateParameters(text) {
// Ersetze {key} Platzhalter mit den entsprechenden Werten
let result = text;
console.log('interpolateParameters:', {
originalText: text,
parameters: this.parameters
});
for (const [key, value] of Object.entries(this.parameters)) {
const placeholder = `{${key}}`;
const regex = new RegExp(placeholder.replace(/[{}]/g, '\\$&'), 'g');
result = result.replace(regex, value);
console.log(`Replaced ${placeholder} with ${value}:`, result);
}
console.log('Final result:', result);
return result;
}
},
beforeDestroy() {
// Stelle sicher, dass Event Listener entfernt wird
beforeUnmount() {
document.removeEventListener('keydown', this.handleKeyDown);
}
};
@@ -116,8 +99,27 @@ export default {
<style scoped>
.message-content {
padding: 1em;
color: #000000;
display: grid;
gap: 12px;
padding: 1.2em;
text-align: center;
}
.message-content__badge {
justify-self: center;
padding: 4px 10px;
border-radius: 999px;
background: rgba(120, 195, 138, 0.16);
color: #24523a;
font-size: 0.78rem;
font-weight: 700;
letter-spacing: 0.06em;
text-transform: uppercase;
}
.message-content p {
margin: 0;
color: var(--color-text-primary);
line-height: 1.55;
}
</style>