diff --git a/backend/models/community/user.js b/backend/models/community/user.js index 20066ec..4de6d12 100644 --- a/backend/models/community/user.js +++ b/backend/models/community/user.js @@ -3,6 +3,44 @@ import { DataTypes } from 'sequelize'; import { encrypt, decrypt } from '../../utils/encryption.js'; import crypto from 'crypto'; +function encodeEncryptedValueToBlob(value) { + const encrypted = encrypt(value); + return Buffer.from(encrypted, 'utf8'); +} + +function decodeEncryptedBlob(value) { + if (!value) { + return null; + } + + try { + const encryptedUtf8 = value.toString('utf8'); + const decryptedUtf8 = decrypt(encryptedUtf8); + if (decryptedUtf8) { + return decryptedUtf8; + } + } catch (error) { + console.warn('Email utf8 decryption failed, trying legacy hex format:', error.message); + } + + try { + const encryptedHex = value.toString('hex'); + const decryptedHex = decrypt(encryptedHex); + if (decryptedHex) { + return decryptedHex; + } + } catch (error) { + console.warn('Email legacy hex decryption failed:', error.message); + } + + try { + return value.toString('utf8'); + } catch (error) { + console.warn('Email could not be read as plain text:', error.message); + return null; + } +} + const User = sequelize.define('user', { email: { type: DataTypes.BLOB, @@ -10,35 +48,12 @@ const User = sequelize.define('user', { unique: true, set(value) { if (value) { - const encrypted = encrypt(value); - // Konvertiere Hex-String zu Buffer für die Speicherung - const buffer = Buffer.from(encrypted, 'hex'); - this.setDataValue('email', buffer); + this.setDataValue('email', encodeEncryptedValueToBlob(value)); } }, get() { const encrypted = this.getDataValue('email'); - if (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; + return decodeEncryptedBlob(encrypted); } }, salt: { diff --git a/backend/models/service/contactmessage.js b/backend/models/service/contactmessage.js index d3cbe27..8da295e 100644 --- a/backend/models/service/contactmessage.js +++ b/backend/models/service/contactmessage.js @@ -9,13 +9,13 @@ const ContactMessage = sequelize.define('contact_message', { set(value) { if (value) { const encryptedValue = encrypt(value); - this.setDataValue('email', encryptedValue.toString('hex')); + this.setDataValue('email', encryptedValue); } }, get() { const value = this.getDataValue('email'); if (value) { - return decrypt(Buffer.from(value, 'hex')); + return decrypt(value); } } }, @@ -25,13 +25,13 @@ const ContactMessage = sequelize.define('contact_message', { set(value) { if (value) { const encryptedValue = encrypt(value); - this.setDataValue('message', encryptedValue.toString('hex')); + this.setDataValue('message', encryptedValue); } }, get() { const value = this.getDataValue('message'); if (value) { - return decrypt(Buffer.from(value, 'hex')); + return decrypt(value); } } }, @@ -41,13 +41,13 @@ const ContactMessage = sequelize.define('contact_message', { set(value) { if (value) { const encryptedValue = encrypt(value); - this.setDataValue('name', encryptedValue.toString('hex')); + this.setDataValue('name', encryptedValue); } }, get() { const value = this.getDataValue('name'); if (value) { - return decrypt(Buffer.from(value, 'hex')); + return decrypt(value); } } }, @@ -67,13 +67,13 @@ const ContactMessage = sequelize.define('contact_message', { set(value) { if (value) { const encryptedValue = encrypt(value); - this.setDataValue('answer', encryptedValue.toString('hex')); + this.setDataValue('answer', encryptedValue); } }, get() { const value = this.getDataValue('answer'); if (value) { - return decrypt(Buffer.from(value, 'hex')); + return decrypt(value); } } }, diff --git a/backend/services/authService.js b/backend/services/authService.js index 1dd31ae..024f349 100644 --- a/backend/services/authService.js +++ b/backend/services/authService.js @@ -14,6 +14,14 @@ import Friendship from '../models/community/friendship.js'; const saltRounds = 10; +const buildEncryptedEmailCandidates = (email) => { + const encrypted = encrypt(email); + return [ + Buffer.from(encrypted, 'utf8'), + Buffer.from(encrypted, 'hex') + ]; +}; + const getFriends = async (userId) => { console.log('getFriends', userId); try { @@ -54,13 +62,13 @@ const getFriends = async (userId) => { }; export const registerUser = async ({ email, username, password, language }) => { - const encryptedEmail = encrypt(email); + const encryptedEmailCandidates = buildEncryptedEmailCandidates(email); const query = ` SELECT id FROM community.user - WHERE email = :encryptedEmail + WHERE email = ANY(:encryptedEmails) `; const existingUser = await sequelize.query(query, { - replacements: { encryptedEmail }, + replacements: { encryptedEmails: encryptedEmailCandidates }, type: sequelize.QueryTypes.SELECT, }); if (existingUser.length > 0) { @@ -170,7 +178,14 @@ export const logoutUser = async (hashedUserId) => { }; export const handleForgotPassword = async ({ email }) => { - const user = await User.findOne({ where: { email } }); + const encryptedEmailCandidates = buildEncryptedEmailCandidates(email); + const user = await User.findOne({ + where: { + email: { + [Op.in]: encryptedEmailCandidates + } + } + }); if (!user) { throw new Error('Email not found'); } diff --git a/frontend/src/views/social/VocabCourseView.vue b/frontend/src/views/social/VocabCourseView.vue index 97b8646..5ba54bc 100644 --- a/frontend/src/views/social/VocabCourseView.vue +++ b/frontend/src/views/social/VocabCourseView.vue @@ -31,7 +31,7 @@ @@ -319,6 +319,9 @@ export default { openLesson(lessonId) { this.$router.push(`/socialnetwork/vocab/courses/${this.courseId}/lessons/${lessonId}`); }, + openLessonAssistant(lessonId) { + this.$router.push(`/socialnetwork/vocab/courses/${this.courseId}/lessons/${lessonId}?assistant=1`); + }, editCourse() { this.$router.push(`/socialnetwork/vocab/courses/${this.courseId}/edit`); }, diff --git a/frontend/src/views/social/VocabLessonView.vue b/frontend/src/views/social/VocabLessonView.vue index 57d4a08..ca7c838 100644 --- a/frontend/src/views/social/VocabLessonView.vue +++ b/frontend/src/views/social/VocabLessonView.vue @@ -101,7 +101,7 @@ -
+

{{ $t('socialnetwork.vocab.courses.languageAssistantTitle') }}

@@ -696,6 +696,7 @@ export default { assistantInput: '', assistantError: '', assistantMode: 'practice', + isAssistantFocused: false, nextLessonId: null, showCompletionDialog: false, showErrorDialog: false, @@ -979,11 +980,19 @@ export default { if (tabParam === 'learn') { this.activeTab = 'learn'; } + if (this.$route.query.assistant) { + this.activeTab = 'learn'; + } try { const res = await apiClient.get(`/api/vocab/lessons/${this.lessonId}`); this.lesson = res.data; debugLog('[VocabLessonView] Geladene Lektion:', this.lesson?.id, this.lesson?.title); + if (this.$route.query.assistant) { + this.$nextTick(() => { + this.focusAssistantCard(); + }); + } // Initialisiere mit effectiveExercises (für Review: reviewVocabExercises, sonst: grammarExercises) this.$nextTick(async () => { const exercises = this.effectiveExercises; @@ -1002,6 +1011,17 @@ export default { this.loading = false; } }, + focusAssistantCard() { + const target = this.$refs.assistantCard; + if (!target || typeof target.scrollIntoView !== 'function') { + return; + } + target.scrollIntoView({ behavior: 'smooth', block: 'start' }); + this.isAssistantFocused = true; + window.setTimeout(() => { + this.isAssistantFocused = false; + }, 2200); + }, async loadAssistantSettings() { this.assistantLoading = true; try { @@ -1793,6 +1813,11 @@ export default { gap: 14px; } +.language-assistant-card--focused { + border-color: var(--color-primary-orange); + box-shadow: 0 0 0 3px rgba(248, 162, 43, 0.18); +} + .language-assistant-card__header { display: flex; justify-content: space-between;