diff --git a/package-lock.json b/package-lock.json index 84a4c3a2..330c0ecf 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,6 +10,7 @@ "dependencies": { "@tailwindcss/vite": "^4.2.2", "canvas-confetti": "^1.9.4", + "leo-profanity": "^1.9.0", "lucide-vue-next": "^1.0.0", "pinia": "^3.0.4", "tailwindcss": "^4.2.2", @@ -3509,6 +3510,13 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/french-badwords-list": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/french-badwords-list/-/french-badwords-list-1.0.7.tgz", + "integrity": "sha512-H1ziKs2PJh2+UXZ9oCGJ/rRQpsI9NBykGf2Sc7WaKaj1OnWFuBXfsvANTdRcfVmOghGQaUmRyZ1hJOPbDpy04Q==", + "license": "MIT", + "optional": true + }, "node_modules/fsevents": { "version": "2.3.2", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", @@ -3919,6 +3927,19 @@ "json-buffer": "3.0.1" } }, + "node_modules/leo-profanity": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/leo-profanity/-/leo-profanity-1.9.0.tgz", + "integrity": "sha512-vMrrrjsbT+fA5I/1rlBEVT5YjJsw1ASIVF8/xBEdZ6ylsg5AEIBrEvHxwe5XAmfVuTBi7aw2KcO2L3s9ddpKzw==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "french-badwords-list": "^1.0.7", + "russian-bad-words": "^0.5.0" + } + }, "node_modules/levn": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", @@ -5033,6 +5054,16 @@ "queue-microtask": "^1.2.2" } }, + "node_modules/russian-bad-words": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/russian-bad-words/-/russian-bad-words-0.5.0.tgz", + "integrity": "sha512-euNvEYki6iYYpkNbeudW+lEMMYGEmN7EBwVF8ezlbv0bZoQpVYB7W10cCeUIGV7Ed50sJynLQ0c559q5iI0ejQ==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=10" + } + }, "node_modules/sax": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/sax/-/sax-1.6.0.tgz", diff --git a/package.json b/package.json index 4debe6d3..a0f74bbe 100644 --- a/package.json +++ b/package.json @@ -23,6 +23,7 @@ "dependencies": { "@tailwindcss/vite": "^4.2.2", "canvas-confetti": "^1.9.4", + "leo-profanity": "^1.9.0", "lucide-vue-next": "^1.0.0", "pinia": "^3.0.4", "tailwindcss": "^4.2.2", diff --git a/src/classrooms/pages/ClassroomModerationPage.vue b/src/classrooms/pages/ClassroomModerationPage.vue index e4a5c843..0e8f6c04 100644 --- a/src/classrooms/pages/ClassroomModerationPage.vue +++ b/src/classrooms/pages/ClassroomModerationPage.vue @@ -10,6 +10,7 @@ import { requestUpdateTeacherClassroomBannedWord, type ClassroomBannedWordResponse, } from '@/shared/moderation/moderation.api' +import { useProfanityFilter } from '@/shared/composables/useProfanityFilter' const route = useRoute() const { t } = useI18n() @@ -22,6 +23,7 @@ const errorMessage = ref(null) const newWord = ref('') const editingId = ref(null) const editingWord = ref('') +const { hasProfanity } = useProfanityFilter() const authorizationHeader = computed(() => authStore.authorizationHeader) @@ -50,6 +52,10 @@ async function loadBannedWords() { } } +function normalizeWord(word: string) { + return word.trim().toLowerCase() +} + function startEditing(entry: ClassroomBannedWordResponse) { editingId.value = entry.id editingWord.value = entry.word @@ -66,12 +72,24 @@ async function createWord() { return } - const normalizedWord = newWord.value.trim() + const normalizedWord = normalizeWord(newWord.value) + if (!normalizedWord) { errorMessage.value = t('classroomModeration.errors.missingWord') return } + if (hasProfanity(normalizedWord)) { + errorMessage.value = t('classroomModeration.errors.alreadyBlockedGlobally') + return + } + + // avoid duplicates + if (bannedWords.value.some((w) => normalizeWord(w.word) === normalizedWord)) { + errorMessage.value = t('classroomModeration.errors.duplicateWord') + return + } + status.value = 'saving' errorMessage.value = null @@ -106,6 +124,19 @@ async function saveWord(entryId: number) { return } + if (hasProfanity(normalizedWord)) { + errorMessage.value = t('classroomModeration.errors.alreadyBlockedGlobally') + return + } + if ( + bannedWords.value.some( + (w) => w.id !== entryId && normalizeWord(w.word) === normalizedWord, + ) + ) { + errorMessage.value = t('classroomModeration.errors.duplicateWord') + return + } + status.value = 'saving' errorMessage.value = null @@ -167,7 +198,7 @@ onMounted(() => {