Improve exercise type validation and encryption in settings service: Enhance error handling for exercise type name checks in both create-bisaya-course-content and update-week1-bisaya-exercises scripts. Implement encryption for API keys and user settings in settingsService, ensuring sensitive data is securely stored. Update localization files to include new terms related to model patterns in English, German, and Spanish.
This commit is contained in:
@@ -1367,20 +1367,25 @@ async function resolveExerciseTypeId(exercise) {
|
|||||||
return exercise.exerciseTypeId;
|
return exercise.exerciseTypeId;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!exercise.exerciseTypeName) {
|
const trimmedName =
|
||||||
throw new Error(`Kein exerciseTypeId oder exerciseTypeName für "${exercise.title}" definiert`);
|
exercise.exerciseTypeName != null && exercise.exerciseTypeName !== ''
|
||||||
|
? String(exercise.exerciseTypeName).trim()
|
||||||
|
: '';
|
||||||
|
|
||||||
|
if (!trimmedName) {
|
||||||
|
throw new Error(`Kein exerciseTypeId oder exerciseTypeName für Übung "${exercise.title || 'unbenannt'}" definiert`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const [type] = await sequelize.query(
|
const [type] = await sequelize.query(
|
||||||
`SELECT id FROM community.vocab_grammar_exercise_type WHERE name = :name LIMIT 1`,
|
`SELECT id FROM community.vocab_grammar_exercise_type WHERE name = :name LIMIT 1`,
|
||||||
{
|
{
|
||||||
replacements: { name: exercise.exerciseTypeName },
|
replacements: { name: trimmedName },
|
||||||
type: sequelize.QueryTypes.SELECT
|
type: sequelize.QueryTypes.SELECT
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!type) {
|
if (!type) {
|
||||||
throw new Error(`Übungstyp "${exercise.exerciseTypeName}" nicht gefunden`);
|
throw new Error(`Übungstyp "${trimmedName}" nicht gefunden`);
|
||||||
}
|
}
|
||||||
|
|
||||||
return Number(type.id);
|
return Number(type.id);
|
||||||
|
|||||||
@@ -48,16 +48,21 @@ async function resolveExerciseTypeId(exercise) {
|
|||||||
return exercise.exerciseTypeId;
|
return exercise.exerciseTypeId;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const name = exercise.exerciseTypeName;
|
||||||
|
if (name === undefined || name === null || String(name).trim() === '') {
|
||||||
|
throw new Error(`Kein exerciseTypeId oder exerciseTypeName für Übung "${exercise.title || 'unbenannt'}"`);
|
||||||
|
}
|
||||||
|
|
||||||
const [type] = await sequelize.query(
|
const [type] = await sequelize.query(
|
||||||
`SELECT id FROM community.vocab_grammar_exercise_type WHERE name = :name LIMIT 1`,
|
`SELECT id FROM community.vocab_grammar_exercise_type WHERE name = :name LIMIT 1`,
|
||||||
{
|
{
|
||||||
replacements: { name: exercise.exerciseTypeName },
|
replacements: { name: String(name).trim() },
|
||||||
type: sequelize.QueryTypes.SELECT
|
type: sequelize.QueryTypes.SELECT
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!type) {
|
if (!type) {
|
||||||
throw new Error(`Übungstyp "${exercise.exerciseTypeName}" nicht gefunden`);
|
throw new Error(`Übungstyp "${String(name).trim()}" nicht gefunden`);
|
||||||
}
|
}
|
||||||
|
|
||||||
return Number(type.id);
|
return Number(type.id);
|
||||||
|
|||||||
@@ -10,6 +10,18 @@ import InterestTranslation from '../models/type/interest_translation.js';
|
|||||||
import { Op } from 'sequelize';
|
import { Op } from 'sequelize';
|
||||||
import UserParamVisibilityType from '../models/type/user_param_visibility.js';
|
import UserParamVisibilityType from '../models/type/user_param_visibility.js';
|
||||||
import UserParamVisibility from '../models/community/user_param_visibility.js';
|
import UserParamVisibility from '../models/community/user_param_visibility.js';
|
||||||
|
import { encrypt } from '../utils/encryption.js';
|
||||||
|
import { sequelize } from '../utils/sequelize.js';
|
||||||
|
|
||||||
|
/** Wie UserParam.value-Setter: bei Verschlüsselungsfehler leeren String speichern, nicht crashen. */
|
||||||
|
function encryptUserParamValue(plain) {
|
||||||
|
try {
|
||||||
|
return encrypt(plain);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error encrypting user_param value:', error);
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
class SettingsService extends BaseService{
|
class SettingsService extends BaseService{
|
||||||
async getUserParams(userId, paramDescriptions) {
|
async getUserParams(userId, paramDescriptions) {
|
||||||
@@ -451,55 +463,61 @@ class SettingsService extends BaseService{
|
|||||||
|
|
||||||
const { apiKey, clearKey, baseUrl, model, enabled } = payload;
|
const { apiKey, clearKey, baseUrl, model, enabled } = payload;
|
||||||
|
|
||||||
if (clearKey) {
|
await sequelize.transaction(async (transaction) => {
|
||||||
const keyRow = await UserParam.findOne({
|
if (clearKey) {
|
||||||
where: { userId: user.id, paramTypeId: apiKeyType.id }
|
const keyRow = await UserParam.findOne({
|
||||||
});
|
where: { userId: user.id, paramTypeId: apiKeyType.id },
|
||||||
if (keyRow) {
|
transaction
|
||||||
await keyRow.destroy();
|
});
|
||||||
|
if (keyRow) {
|
||||||
|
await keyRow.destroy({ transaction });
|
||||||
|
}
|
||||||
|
delete parsed.keyLast4;
|
||||||
|
} else if (apiKey !== undefined && String(apiKey).trim() !== '') {
|
||||||
|
const plain = String(apiKey).trim();
|
||||||
|
parsed.keyLast4 = plain.length >= 4 ? plain.slice(-4) : plain;
|
||||||
|
const encKey = encryptUserParamValue(plain);
|
||||||
|
const [keyRow] = await UserParam.findOrCreate({
|
||||||
|
where: { userId: user.id, paramTypeId: apiKeyType.id },
|
||||||
|
defaults: {
|
||||||
|
userId: user.id,
|
||||||
|
paramTypeId: apiKeyType.id,
|
||||||
|
// Platzhalter: Setter verschlüsselt; wird sofort durch encKey überschrieben.
|
||||||
|
value: ' '
|
||||||
|
},
|
||||||
|
transaction
|
||||||
|
});
|
||||||
|
keyRow.setDataValue('value', encKey);
|
||||||
|
await keyRow.save({ fields: ['value'], transaction });
|
||||||
}
|
}
|
||||||
delete parsed.keyLast4;
|
|
||||||
} else if (apiKey !== undefined && String(apiKey).trim() !== '') {
|
if (baseUrl !== undefined) {
|
||||||
const plain = String(apiKey).trim();
|
parsed.baseUrl = String(baseUrl).trim();
|
||||||
parsed.keyLast4 = plain.length >= 4 ? plain.slice(-4) : plain;
|
}
|
||||||
const [keyRow, keyCreated] = await UserParam.findOrCreate({
|
if (model !== undefined) {
|
||||||
where: { userId: user.id, paramTypeId: apiKeyType.id },
|
parsed.model = String(model).trim() || 'gpt-4o-mini';
|
||||||
|
}
|
||||||
|
if (enabled !== undefined) {
|
||||||
|
parsed.enabled = Boolean(enabled);
|
||||||
|
}
|
||||||
|
if (!parsed.model) {
|
||||||
|
parsed.model = 'gpt-4o-mini';
|
||||||
|
}
|
||||||
|
|
||||||
|
const jsonStr = JSON.stringify(parsed);
|
||||||
|
const encMeta = encryptUserParamValue(jsonStr);
|
||||||
|
const [metaRow] = await UserParam.findOrCreate({
|
||||||
|
where: { userId: user.id, paramTypeId: settingsType.id },
|
||||||
defaults: {
|
defaults: {
|
||||||
userId: user.id,
|
userId: user.id,
|
||||||
paramTypeId: apiKeyType.id,
|
paramTypeId: settingsType.id,
|
||||||
value: plain
|
value: ' '
|
||||||
}
|
},
|
||||||
|
transaction
|
||||||
});
|
});
|
||||||
if (!keyCreated) {
|
metaRow.setDataValue('value', encMeta);
|
||||||
await keyRow.update({ value: plain });
|
await metaRow.save({ fields: ['value'], transaction });
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (baseUrl !== undefined) {
|
|
||||||
parsed.baseUrl = String(baseUrl).trim();
|
|
||||||
}
|
|
||||||
if (model !== undefined) {
|
|
||||||
parsed.model = String(model).trim() || 'gpt-4o-mini';
|
|
||||||
}
|
|
||||||
if (enabled !== undefined) {
|
|
||||||
parsed.enabled = Boolean(enabled);
|
|
||||||
}
|
|
||||||
if (!parsed.model) {
|
|
||||||
parsed.model = 'gpt-4o-mini';
|
|
||||||
}
|
|
||||||
|
|
||||||
const jsonStr = JSON.stringify(parsed);
|
|
||||||
const [metaRow, metaCreated] = await UserParam.findOrCreate({
|
|
||||||
where: { userId: user.id, paramTypeId: settingsType.id },
|
|
||||||
defaults: {
|
|
||||||
userId: user.id,
|
|
||||||
paramTypeId: settingsType.id,
|
|
||||||
value: jsonStr
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
if (!metaCreated) {
|
|
||||||
await metaRow.update({ value: jsonStr });
|
|
||||||
}
|
|
||||||
|
|
||||||
return { success: true };
|
return { success: true };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -516,7 +516,7 @@ export default {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
@import '../assets/styles.scss';
|
@use '../assets/styles.scss' as *;
|
||||||
|
|
||||||
.app-navigation,
|
.app-navigation,
|
||||||
.nav-primary > ul {
|
.nav-primary > ul {
|
||||||
|
|||||||
@@ -424,6 +424,7 @@
|
|||||||
"modelSentence": "Modellsatz",
|
"modelSentence": "Modellsatz",
|
||||||
"modelDialogLine": "Mögliche Dialogzeile",
|
"modelDialogLine": "Mögliche Dialogzeile",
|
||||||
"modelResponse": "Mögliche Antwort",
|
"modelResponse": "Mögliche Antwort",
|
||||||
|
"modelPattern": "Möglicher Mustersatz",
|
||||||
"patternPrompt": "Muster",
|
"patternPrompt": "Muster",
|
||||||
"readingAloudInstruction": "Lies den Text laut vor. Klicke auf 'Aufnahme starten' und beginne zu sprechen.",
|
"readingAloudInstruction": "Lies den Text laut vor. Klicke auf 'Aufnahme starten' und beginne zu sprechen.",
|
||||||
"speakingFromMemoryInstruction": "Sprich frei aus dem Kopf. Verwende die angezeigten Schlüsselwörter.",
|
"speakingFromMemoryInstruction": "Sprich frei aus dem Kopf. Verwende die angezeigten Schlüsselwörter.",
|
||||||
|
|||||||
@@ -424,6 +424,7 @@
|
|||||||
"modelSentence": "Model sentence",
|
"modelSentence": "Model sentence",
|
||||||
"modelDialogLine": "Possible dialog line",
|
"modelDialogLine": "Possible dialog line",
|
||||||
"modelResponse": "Possible response",
|
"modelResponse": "Possible response",
|
||||||
|
"modelPattern": "Possible pattern example",
|
||||||
"patternPrompt": "Pattern",
|
"patternPrompt": "Pattern",
|
||||||
"readingAloudInstruction": "Read the text aloud. Click 'Start Recording' and begin speaking.",
|
"readingAloudInstruction": "Read the text aloud. Click 'Start Recording' and begin speaking.",
|
||||||
"speakingFromMemoryInstruction": "Speak freely from memory. Use the displayed keywords.",
|
"speakingFromMemoryInstruction": "Speak freely from memory. Use the displayed keywords.",
|
||||||
|
|||||||
@@ -421,6 +421,7 @@
|
|||||||
"modelSentence": "Frase modelo",
|
"modelSentence": "Frase modelo",
|
||||||
"modelDialogLine": "Línea posible del diálogo",
|
"modelDialogLine": "Línea posible del diálogo",
|
||||||
"modelResponse": "Respuesta posible",
|
"modelResponse": "Respuesta posible",
|
||||||
|
"modelPattern": "Ejemplo posible del patrón",
|
||||||
"patternPrompt": "Patrón",
|
"patternPrompt": "Patrón",
|
||||||
"readingAloudInstruction": "Lee el texto en voz alta. Haz clic en 'Iniciar grabación' y comienza a hablar.",
|
"readingAloudInstruction": "Lee el texto en voz alta. Haz clic en 'Iniciar grabación' y comienza a hablar.",
|
||||||
"speakingFromMemoryInstruction": "Habla de memoria. Usa las palabras clave mostradas.",
|
"speakingFromMemoryInstruction": "Habla de memoria. Usa las palabras clave mostradas.",
|
||||||
|
|||||||
Reference in New Issue
Block a user