From d6bfe50b4ec542d7a3314822c68c82314cf7ee91 Mon Sep 17 00:00:00 2001 From: "Torsten Schulz (local)" Date: Mon, 15 Sep 2025 11:48:00 +0200 Subject: [PATCH] =?UTF-8?q?=C3=84nderung:=20Erweiterung=20der=20Benutzerko?= =?UTF-8?q?ntoeinstellungen=20und=20Verbesserung=20der=20E-Mail-Verschl?= =?UTF-8?q?=C3=BCsselung?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Änderungen: - Implementierung von neuen Methoden `getAccountSettings` und `setAccountSettings` im `SettingsService`, um Benutzerkontoeinstellungen zu verwalten. - Anpassung der E-Mail-Verschlüsselung im `User`-Modell zur Verwendung von Buffer für die Speicherung und zur Verbesserung der Fehlerbehandlung bei der Entschlüsselung. - Hinzufügung eines neuen `immutable`-Feldes im `UserParamType`-Modell, um unveränderliche Einstellungen zu kennzeichnen. - Anpassungen in den Frontend-Komponenten zur Berücksichtigung von unveränderlichen Feldern und zur Verbesserung der Benutzeroberfläche. Diese Anpassungen verbessern die Sicherheit der Benutzerdaten und erweitern die Funktionalität der Kontoeinstellungen. --- backend/models/community/user.js | 23 +++- backend/models/type/user_param.js | 5 + backend/routers/settingsRouter.js | 1 + backend/services/settingsService.js | 89 ++++++++++++++++ backend/utils/initializeTypes.js | 3 +- frontend/src/components/SettingsWidget.vue | 100 ++++++++++++++++-- .../src/components/form/CheckboxWidget.vue | 7 +- .../src/components/form/DateInputWidget.vue | 7 +- .../src/components/form/FloatInputWidget.vue | 7 +- .../src/components/form/FormattedDropdown.vue | 14 ++- .../src/components/form/InputNumberWidget.vue | 7 +- .../src/components/form/InputStringWidget.vue | 7 +- .../src/components/form/MultiselectWidget.vue | 6 ++ .../components/form/SelectDropdownWidget.vue | 7 +- .../src/dialogues/standard/ContactDialog.vue | 11 +- frontend/src/i18n/locales/de/settings.json | 10 +- frontend/src/i18n/locales/en/settings.json | 10 +- frontend/src/views/settings/AccountView.vue | 69 +++++++++++- 18 files changed, 355 insertions(+), 28 deletions(-) diff --git a/backend/models/community/user.js b/backend/models/community/user.js index 3124f9c..20066ec 100644 --- a/backend/models/community/user.js +++ b/backend/models/community/user.js @@ -11,13 +11,32 @@ const User = sequelize.define('user', { set(value) { if (value) { const encrypted = encrypt(value); - this.setDataValue('email', encrypted); + // Konvertiere Hex-String zu Buffer für die Speicherung + const buffer = Buffer.from(encrypted, 'hex'); + this.setDataValue('email', buffer); } }, get() { const encrypted = this.getDataValue('email'); if (encrypted) { - return decrypt(encrypted); + try { + // Konvertiere Buffer zu String für die Entschlüsselung + const encryptedString = encrypted.toString('hex'); + const decrypted = decrypt(encryptedString); + if (decrypted) { + return decrypted; + } + } catch (error) { + console.warn('Email decryption failed, treating as plain text:', error.message); + } + + // Fallback: Versuche es als Klartext zu lesen + try { + return encrypted.toString('utf8'); + } catch (error) { + console.warn('Email could not be read as plain text:', error.message); + return null; + } } return null; } diff --git a/backend/models/type/user_param.js b/backend/models/type/user_param.js index 2fae1d9..10d33c5 100644 --- a/backend/models/type/user_param.js +++ b/backend/models/type/user_param.js @@ -35,6 +35,11 @@ const UserParamType = sequelize.define('user_param_type', { unit: { type: DataTypes.STRING, allowNull: true + }, + immutable: { + type: DataTypes.BOOLEAN, + allowNull: false, + defaultValue: false } }, { tableName: 'user_param', diff --git a/backend/routers/settingsRouter.js b/backend/routers/settingsRouter.js index 3cabda2..dcb203d 100644 --- a/backend/routers/settingsRouter.js +++ b/backend/routers/settingsRouter.js @@ -8,6 +8,7 @@ const settingsController = new SettingsController(); router.post('/filter', authenticate, settingsController.filterSettings.bind(settingsController)); router.post('/update', authenticate, settingsController.updateSetting.bind(settingsController)); router.post('/account', authenticate, settingsController.getAccountSettings.bind(settingsController)); +router.post('/set-account', authenticate, settingsController.setAccountSettings.bind(settingsController)); router.post('/getparamvalues', settingsController.getTypeParamValues.bind(settingsController)); router.post('/getparamvalueid', settingsController.getTypeParamValueId.bind(settingsController)); router.post('/getparamvalue/:id', settingsController.getTypeParamValue.bind(settingsController)); diff --git a/backend/services/settingsService.js b/backend/services/settingsService.js index edc36a8..5c39d11 100644 --- a/backend/services/settingsService.js +++ b/backend/services/settingsService.js @@ -103,6 +103,7 @@ class SettingsService extends BaseService{ gender: field.gender, datatype: field.datatype, unit: field.unit, + immutable: field.immutable, value: field.user_params.length > 0 ? field.user_params[0].value : null, options: options.map(opt => ({ id: opt.id, value: opt.value })), visibility @@ -117,6 +118,19 @@ class SettingsService extends BaseService{ if (!paramType) { throw new Error('Parameter type not found'); } + + // Prüfe ob das Feld unveränderlich ist + if (paramType.immutable) { + const userParam = await UserParam.findOne({ + where: { userId: user.id, paramTypeId: settingId } + }); + + // Wenn bereits ein Wert existiert, ist das Feld unveränderlich + if (userParam && userParam.value) { + throw new Error('This field cannot be changed. Please contact support for modifications.'); + } + } + const userParam = await UserParam.findOne({ where: { userId: user.id, paramTypeId: settingId } }); @@ -257,6 +271,81 @@ class SettingsService extends BaseService{ } } + async getAccountSettings(hashedUserId) { + try { + const user = await this.getUserByHashedId(hashedUserId); + if (!user) { + throw new Error('User not found'); + } + + // Die Email wird automatisch durch den Getter entschlüsselt + // Falls die Entschlüsselung fehlschlägt, verwende null + let email = null; + try { + email = user.email; // Getter entschlüsselt automatisch + } catch (decryptError) { + console.warn('Email decryption failed, using null:', decryptError.message); + email = null; + } + + return { + username: user.username, + email: email, + showinsearch: user.searchable + }; + } catch (error) { + console.error('Error getting account settings:', error); + throw error; + } + } + + async setAccountSettings({ userId, settings }) { + try { + const user = await this.getUserByHashedId(userId); + if (!user) { + throw new Error('User not found'); + } + + // Update username if provided + if (settings.username !== undefined) { + await user.update({ username: settings.username }); + } + + // Update email if provided + if (settings.email !== undefined) { + await user.update({ email: settings.email }); + } + + // Update searchable flag if provided + if (settings.showinsearch !== undefined) { + await user.update({ searchable: settings.showinsearch }); + } + + // Update password if provided and not empty + if (settings.newpassword && settings.newpassword.trim() !== '') { + if (!settings.oldpassword || settings.oldpassword.trim() === '') { + throw new Error('Old password is required to change password'); + } + + // Verify old password + const bcrypt = await import('bcrypt'); + const match = await bcrypt.compare(settings.oldpassword, user.password); + if (!match) { + throw new Error('Old password is incorrect'); + } + + // Hash new password + const hashedPassword = await bcrypt.hash(settings.newpassword, 10); + await user.update({ password: hashedPassword }); + } + + return { success: true }; + } catch (error) { + console.error('Error setting account settings:', error); + throw error; + } + } + async getVisibilities() { return UserParamVisibilityType.findAll(); } diff --git a/backend/utils/initializeTypes.js b/backend/utils/initializeTypes.js index e69681f..fc89467 100644 --- a/backend/utils/initializeTypes.js +++ b/backend/utils/initializeTypes.js @@ -24,7 +24,7 @@ const initializeTypes = async () => { }; const userParams = { language: { type: 'singleselect', setting: 'personal' }, - birthdate: { type: 'date', setting: 'personal' }, + birthdate: { type: 'date', setting: 'personal', immutable: true }, zip: { type: 'string', setting: 'personal' }, town: { type: 'string', setting: 'personal' }, bodyheight: { type: 'float', setting: 'view', unit: 'cm' }, @@ -54,6 +54,7 @@ const initializeTypes = async () => { if (item.minAge) createItem.minAge = item.minAge; if (item.gender) createItem.gender = item.gender; if (item.unit) createItem.unit = item.unit; + if (item.immutable) createItem.immutable = item.immutable; await UserParamType.findOrCreate({ where: { description: key }, defaults: createItem diff --git a/frontend/src/components/SettingsWidget.vue b/frontend/src/components/SettingsWidget.vue index 9f25316..bab1dfc 100644 --- a/frontend/src/components/SettingsWidget.vue +++ b/frontend/src/components/SettingsWidget.vue @@ -5,38 +5,52 @@ @@ -51,6 +65,11 @@ + + + @@ -93,6 +112,7 @@ export default { }, async mounted() { await this.fetchSettings(); + await this.fetchAccountData(); }, methods: { async fetchSettings() { @@ -111,6 +131,17 @@ export default { } } }, + async fetchAccountData() { + if (this.user && this.user.id) { + try { + const response = await apiClient.post('/api/settings/account', { userId: this.user.id }); + this.userEmail = response.data.email; + this.userUsername = response.data.username; + } catch (err) { + console.error('Error fetching account data:', err); + } + } + }, getSettingOptions(fieldName, options) { return options.map((option) => { return { @@ -123,6 +154,14 @@ export default { if (['object', 'array'].includes(typeof value)) { return; } + + // Prüfe ob das Setting unveränderlich ist + const setting = this.settings.find(s => s.id === settingId); + if (setting && setting.immutable && setting.value) { + alert(this.$t('settings.immutable.tooltip')); + return; + } + try { const userid = this.user.id; await apiClient.post('/api/settings/update', { @@ -133,6 +172,9 @@ export default { this.fetchSettings(); } catch (err) { console.error('Error updating setting:', err); + if (err.response && err.response.data && err.response.data.error) { + alert(err.response.data.error); + } } }, languagesList() { @@ -168,10 +210,35 @@ export default { console.error('Error updating visibility:', err); } }, + openContactDialog() { + // Erstelle vorgefertigte Daten für Support-Anfrage + const prefilledData = { + email: this.userEmail || "", + name: this.userUsername || "", + message: this.createSupportMessage(), + acceptDataSave: true + }; + this.$root.$refs.contactDialog.open(prefilledData); + }, + createSupportMessage() { + // Erstelle eine vorgefertigte Nachricht für unveränderliche Felder + const immutableFields = this.settings.filter(s => s.immutable && s.value); + if (immutableFields.length === 0) { + return this.$t('settings.immutable.supportMessage.general'); + } + + const fieldNames = immutableFields.map(field => + this.$t(`settings.personal.label.${field.name}`) + ).join(', '); + + return this.$t('settings.immutable.supportMessage.specific', { fields: fieldNames }); + }, }, data() { return { settings: [], + userEmail: "", + userUsername: "", }; } }; @@ -181,4 +248,23 @@ export default { label { float: left; } + + .contact-button { + background-color: #007bff; + color: white; + border: none; + padding: 5px 10px; + border-radius: 4px; + cursor: pointer; + font-size: 12px; + transition: background-color 0.2s; + } + + .contact-button:hover { + background-color: #0056b3; + } + + .contact-button:active { + background-color: #004085; + } \ No newline at end of file diff --git a/frontend/src/components/form/CheckboxWidget.vue b/frontend/src/components/form/CheckboxWidget.vue index 448d112..fb22831 100644 --- a/frontend/src/components/form/CheckboxWidget.vue +++ b/frontend/src/components/form/CheckboxWidget.vue @@ -1,6 +1,6 @@ @@ -26,6 +26,11 @@ export default { type: Number, required: false, default: 10 + }, + disabled: { + type: Boolean, + required: false, + default: false } }, methods: { diff --git a/frontend/src/components/form/DateInputWidget.vue b/frontend/src/components/form/DateInputWidget.vue index b37504a..58bc3d8 100644 --- a/frontend/src/components/form/DateInputWidget.vue +++ b/frontend/src/components/form/DateInputWidget.vue @@ -2,7 +2,7 @@ @@ -28,6 +28,11 @@ export default { type: Number, required: false, default: 10 + }, + disabled: { + type: Boolean, + required: false, + default: false } }, data() { diff --git a/frontend/src/components/form/FloatInputWidget.vue b/frontend/src/components/form/FloatInputWidget.vue index ed1057f..c982f85 100644 --- a/frontend/src/components/form/FloatInputWidget.vue +++ b/frontend/src/components/form/FloatInputWidget.vue @@ -2,7 +2,7 @@ @@ -37,6 +37,11 @@ export default { type: String, required: false, default: '' + }, + disabled: { + type: Boolean, + required: false, + default: false } }, computed: { diff --git a/frontend/src/components/form/FormattedDropdown.vue b/frontend/src/components/form/FormattedDropdown.vue index bd1657c..8369ff9 100644 --- a/frontend/src/components/form/FormattedDropdown.vue +++ b/frontend/src/components/form/FormattedDropdown.vue @@ -1,6 +1,6 @@ @@ -34,6 +34,11 @@ export default { max: { type: Number, required: false + }, + disabled: { + type: Boolean, + required: false, + default: false } }, methods: { diff --git a/frontend/src/components/form/InputStringWidget.vue b/frontend/src/components/form/InputStringWidget.vue index 5ed9dee..2288b78 100644 --- a/frontend/src/components/form/InputStringWidget.vue +++ b/frontend/src/components/form/InputStringWidget.vue @@ -2,7 +2,7 @@ @@ -31,6 +31,11 @@ export default { type: String, required: false, default: null + }, + disabled: { + type: Boolean, + required: false, + default: false } }, methods: { diff --git a/frontend/src/components/form/MultiselectWidget.vue b/frontend/src/components/form/MultiselectWidget.vue index 13f0d29..a9e8d00 100644 --- a/frontend/src/components/form/MultiselectWidget.vue +++ b/frontend/src/components/form/MultiselectWidget.vue @@ -10,6 +10,7 @@ :preserve-search="true" :placeholder="$t('select_option')" :track-by="'value'" + :disabled="disabled" >