Refactor email encryption handling in user and contact message models: Introduce utility functions for encoding and decoding encrypted values, simplifying the encryption process. Update the registerUser and handleForgotPassword functions to support multiple encrypted email formats. Enhance the VocabCourseView and VocabLessonView components with new methods for managing language assistant interactions, improving user experience.
This commit is contained in:
@@ -3,6 +3,44 @@ import { DataTypes } from 'sequelize';
|
|||||||
import { encrypt, decrypt } from '../../utils/encryption.js';
|
import { encrypt, decrypt } from '../../utils/encryption.js';
|
||||||
import crypto from 'crypto';
|
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', {
|
const User = sequelize.define('user', {
|
||||||
email: {
|
email: {
|
||||||
type: DataTypes.BLOB,
|
type: DataTypes.BLOB,
|
||||||
@@ -10,35 +48,12 @@ const User = sequelize.define('user', {
|
|||||||
unique: true,
|
unique: true,
|
||||||
set(value) {
|
set(value) {
|
||||||
if (value) {
|
if (value) {
|
||||||
const encrypted = encrypt(value);
|
this.setDataValue('email', encodeEncryptedValueToBlob(value));
|
||||||
// Konvertiere Hex-String zu Buffer für die Speicherung
|
|
||||||
const buffer = Buffer.from(encrypted, 'hex');
|
|
||||||
this.setDataValue('email', buffer);
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
get() {
|
get() {
|
||||||
const encrypted = this.getDataValue('email');
|
const encrypted = this.getDataValue('email');
|
||||||
if (encrypted) {
|
return decodeEncryptedBlob(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;
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
salt: {
|
salt: {
|
||||||
|
|||||||
@@ -9,13 +9,13 @@ const ContactMessage = sequelize.define('contact_message', {
|
|||||||
set(value) {
|
set(value) {
|
||||||
if (value) {
|
if (value) {
|
||||||
const encryptedValue = encrypt(value);
|
const encryptedValue = encrypt(value);
|
||||||
this.setDataValue('email', encryptedValue.toString('hex'));
|
this.setDataValue('email', encryptedValue);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
get() {
|
get() {
|
||||||
const value = this.getDataValue('email');
|
const value = this.getDataValue('email');
|
||||||
if (value) {
|
if (value) {
|
||||||
return decrypt(Buffer.from(value, 'hex'));
|
return decrypt(value);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -25,13 +25,13 @@ const ContactMessage = sequelize.define('contact_message', {
|
|||||||
set(value) {
|
set(value) {
|
||||||
if (value) {
|
if (value) {
|
||||||
const encryptedValue = encrypt(value);
|
const encryptedValue = encrypt(value);
|
||||||
this.setDataValue('message', encryptedValue.toString('hex'));
|
this.setDataValue('message', encryptedValue);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
get() {
|
get() {
|
||||||
const value = this.getDataValue('message');
|
const value = this.getDataValue('message');
|
||||||
if (value) {
|
if (value) {
|
||||||
return decrypt(Buffer.from(value, 'hex'));
|
return decrypt(value);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -41,13 +41,13 @@ const ContactMessage = sequelize.define('contact_message', {
|
|||||||
set(value) {
|
set(value) {
|
||||||
if (value) {
|
if (value) {
|
||||||
const encryptedValue = encrypt(value);
|
const encryptedValue = encrypt(value);
|
||||||
this.setDataValue('name', encryptedValue.toString('hex'));
|
this.setDataValue('name', encryptedValue);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
get() {
|
get() {
|
||||||
const value = this.getDataValue('name');
|
const value = this.getDataValue('name');
|
||||||
if (value) {
|
if (value) {
|
||||||
return decrypt(Buffer.from(value, 'hex'));
|
return decrypt(value);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -67,13 +67,13 @@ const ContactMessage = sequelize.define('contact_message', {
|
|||||||
set(value) {
|
set(value) {
|
||||||
if (value) {
|
if (value) {
|
||||||
const encryptedValue = encrypt(value);
|
const encryptedValue = encrypt(value);
|
||||||
this.setDataValue('answer', encryptedValue.toString('hex'));
|
this.setDataValue('answer', encryptedValue);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
get() {
|
get() {
|
||||||
const value = this.getDataValue('answer');
|
const value = this.getDataValue('answer');
|
||||||
if (value) {
|
if (value) {
|
||||||
return decrypt(Buffer.from(value, 'hex'));
|
return decrypt(value);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -14,6 +14,14 @@ import Friendship from '../models/community/friendship.js';
|
|||||||
|
|
||||||
const saltRounds = 10;
|
const saltRounds = 10;
|
||||||
|
|
||||||
|
const buildEncryptedEmailCandidates = (email) => {
|
||||||
|
const encrypted = encrypt(email);
|
||||||
|
return [
|
||||||
|
Buffer.from(encrypted, 'utf8'),
|
||||||
|
Buffer.from(encrypted, 'hex')
|
||||||
|
];
|
||||||
|
};
|
||||||
|
|
||||||
const getFriends = async (userId) => {
|
const getFriends = async (userId) => {
|
||||||
console.log('getFriends', userId);
|
console.log('getFriends', userId);
|
||||||
try {
|
try {
|
||||||
@@ -54,13 +62,13 @@ const getFriends = async (userId) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const registerUser = async ({ email, username, password, language }) => {
|
export const registerUser = async ({ email, username, password, language }) => {
|
||||||
const encryptedEmail = encrypt(email);
|
const encryptedEmailCandidates = buildEncryptedEmailCandidates(email);
|
||||||
const query = `
|
const query = `
|
||||||
SELECT id FROM community.user
|
SELECT id FROM community.user
|
||||||
WHERE email = :encryptedEmail
|
WHERE email = ANY(:encryptedEmails)
|
||||||
`;
|
`;
|
||||||
const existingUser = await sequelize.query(query, {
|
const existingUser = await sequelize.query(query, {
|
||||||
replacements: { encryptedEmail },
|
replacements: { encryptedEmails: encryptedEmailCandidates },
|
||||||
type: sequelize.QueryTypes.SELECT,
|
type: sequelize.QueryTypes.SELECT,
|
||||||
});
|
});
|
||||||
if (existingUser.length > 0) {
|
if (existingUser.length > 0) {
|
||||||
@@ -170,7 +178,14 @@ export const logoutUser = async (hashedUserId) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const handleForgotPassword = async ({ email }) => {
|
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) {
|
if (!user) {
|
||||||
throw new Error('Email not found');
|
throw new Error('Email not found');
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,7 +31,7 @@
|
|||||||
<button
|
<button
|
||||||
v-if="assistantAvailable && currentLesson"
|
v-if="assistantAvailable && currentLesson"
|
||||||
type="button"
|
type="button"
|
||||||
@click="openLesson(currentLesson.id)"
|
@click="openLessonAssistant(currentLesson.id)"
|
||||||
>
|
>
|
||||||
{{ $t('socialnetwork.vocab.courses.languageAssistantOpenLesson') }}
|
{{ $t('socialnetwork.vocab.courses.languageAssistantOpenLesson') }}
|
||||||
</button>
|
</button>
|
||||||
@@ -319,6 +319,9 @@ export default {
|
|||||||
openLesson(lessonId) {
|
openLesson(lessonId) {
|
||||||
this.$router.push(`/socialnetwork/vocab/courses/${this.courseId}/lessons/${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() {
|
editCourse() {
|
||||||
this.$router.push(`/socialnetwork/vocab/courses/${this.courseId}/edit`);
|
this.$router.push(`/socialnetwork/vocab/courses/${this.courseId}/edit`);
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -101,7 +101,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="didactic-card language-assistant-card">
|
<div ref="assistantCard" class="didactic-card language-assistant-card" :class="{ 'language-assistant-card--focused': isAssistantFocused }">
|
||||||
<div class="language-assistant-card__header">
|
<div class="language-assistant-card__header">
|
||||||
<div>
|
<div>
|
||||||
<h4>{{ $t('socialnetwork.vocab.courses.languageAssistantTitle') }}</h4>
|
<h4>{{ $t('socialnetwork.vocab.courses.languageAssistantTitle') }}</h4>
|
||||||
@@ -696,6 +696,7 @@ export default {
|
|||||||
assistantInput: '',
|
assistantInput: '',
|
||||||
assistantError: '',
|
assistantError: '',
|
||||||
assistantMode: 'practice',
|
assistantMode: 'practice',
|
||||||
|
isAssistantFocused: false,
|
||||||
nextLessonId: null,
|
nextLessonId: null,
|
||||||
showCompletionDialog: false,
|
showCompletionDialog: false,
|
||||||
showErrorDialog: false,
|
showErrorDialog: false,
|
||||||
@@ -979,11 +980,19 @@ export default {
|
|||||||
if (tabParam === 'learn') {
|
if (tabParam === 'learn') {
|
||||||
this.activeTab = 'learn';
|
this.activeTab = 'learn';
|
||||||
}
|
}
|
||||||
|
if (this.$route.query.assistant) {
|
||||||
|
this.activeTab = 'learn';
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const res = await apiClient.get(`/api/vocab/lessons/${this.lessonId}`);
|
const res = await apiClient.get(`/api/vocab/lessons/${this.lessonId}`);
|
||||||
this.lesson = res.data;
|
this.lesson = res.data;
|
||||||
debugLog('[VocabLessonView] Geladene Lektion:', this.lesson?.id, this.lesson?.title);
|
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)
|
// Initialisiere mit effectiveExercises (für Review: reviewVocabExercises, sonst: grammarExercises)
|
||||||
this.$nextTick(async () => {
|
this.$nextTick(async () => {
|
||||||
const exercises = this.effectiveExercises;
|
const exercises = this.effectiveExercises;
|
||||||
@@ -1002,6 +1011,17 @@ export default {
|
|||||||
this.loading = false;
|
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() {
|
async loadAssistantSettings() {
|
||||||
this.assistantLoading = true;
|
this.assistantLoading = true;
|
||||||
try {
|
try {
|
||||||
@@ -1793,6 +1813,11 @@ export default {
|
|||||||
gap: 14px;
|
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 {
|
.language-assistant-card__header {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
|
|||||||
Reference in New Issue
Block a user