From 41f807e825eb25a626dfaf3c3ba0e4c9a33f33d2 Mon Sep 17 00:00:00 2001 From: Andreas Date: Mon, 16 Sep 2024 06:19:09 +0200 Subject: [PATCH] Profanity filter (#658) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * ❌ Remove untyped inject keys * 📦 Add google profanity words * 📦 Switch to a working profanity lib * ➕ Provide profanity check function * ➕ Frontend validation for schedule creation/modification * ➕ Frontend validation for account settings * ➕ Frontend validation for FTUE * 🔨 Check all inputs on account settings at the same time * 🔨 User form validation for required fields * 👕 Fix linter issues * 🔨 Add required property --- frontend/.eslintrc.cjs | 1 + frontend/package-lock.json | 10 +++ frontend/package.json | 1 + frontend/src/App.vue | 46 ++++++------- .../components/AppointmentCreatedModal.vue | 2 +- frontend/src/components/AppointmentModal.vue | 4 +- frontend/src/components/BookingModal.vue | 2 +- .../src/components/CalendarManagement.vue | 4 +- frontend/src/components/CalendarMiniMonth.vue | 6 +- frontend/src/components/CalendarQalendar.vue | 2 +- frontend/src/components/ConfirmationModal.vue | 2 +- frontend/src/components/FTUE/ConnectVideo.vue | 4 +- frontend/src/components/FTUE/SetupProfile.vue | 29 +++++++- .../src/components/FTUE/SetupSchedule.vue | 27 ++++++-- frontend/src/components/GenericModal.vue | 2 +- frontend/src/components/NavBar.vue | 6 +- frontend/src/components/ScheduleCreation.vue | 42 ++++++++++-- frontend/src/components/SettingsAccount.vue | 66 +++++++++++++++---- frontend/src/components/SettingsCalendar.vue | 6 +- frontend/src/components/SettingsGeneral.vue | 8 ++- frontend/src/components/TabBar.vue | 2 +- .../bookingView/BookingViewError.vue | 2 +- .../bookingView/BookingViewSlotSelection.vue | 6 +- .../bookingView/BookingViewSuccess.vue | 4 +- frontend/src/composables/dayjs.ts | 7 +- frontend/src/elements/AlertBox.vue | 4 +- frontend/src/elements/AppointmentGridItem.vue | 4 +- frontend/src/elements/CautionButton.vue | 2 +- frontend/src/elements/EventPopup.vue | 4 +- .../src/elements/GoogleCalendarButton.vue | 2 +- frontend/src/elements/ListPagination.vue | 2 +- frontend/src/elements/NavBarItem.vue | 2 +- frontend/src/elements/PrimaryButton.vue | 2 +- frontend/src/elements/SecondaryButton.vue | 2 +- frontend/src/elements/SiteNotification.vue | 2 +- frontend/src/elements/SnackishBar.vue | 4 +- frontend/src/elements/SwitchToggle.vue | 2 +- frontend/src/elements/ToolTip.vue | 2 +- .../src/elements/calendar/CalendarEvent.vue | 8 ++- .../calendar/CalendarEventPlaceholder.vue | 2 +- .../calendar/CalendarEventPreview.vue | 4 +- .../elements/calendar/CalendarEventRemote.vue | 4 +- .../calendar/CalendarEventScheduled.vue | 4 +- .../calendar/CalendarMiniMonthDay.vue | 4 +- frontend/src/elements/home/InfoBox.vue | 2 +- frontend/src/keys.ts | 16 +++-- frontend/src/locales/de.json | 2 + frontend/src/locales/en.json | 2 + frontend/src/stores/appointment-store.ts | 4 +- frontend/src/tbpro/elements/NoticeBar.vue | 4 +- frontend/src/tbpro/elements/SelectInput.vue | 5 +- frontend/src/tbpro/elements/SyncCard.vue | 2 +- frontend/src/tbpro/elements/TextInput.vue | 7 +- frontend/src/tbpro/elements/ToolTip.vue | 4 +- frontend/src/tbpro/icons/NoticeInfoIcon.vue | 1 + frontend/src/views/AppointmentsView.vue | 2 +- .../src/views/FirstTimeUserExperienceView.vue | 7 +- frontend/src/views/HomeView.vue | 10 +-- frontend/src/views/ScheduleView.vue | 12 ++-- .../src/views/admin/InviteCodePanelView.vue | 11 ++-- .../src/views/admin/SubscriberPanelView.vue | 6 +- .../src/views/admin/WaitingListPanelView.vue | 14 ++-- 62 files changed, 298 insertions(+), 164 deletions(-) diff --git a/frontend/.eslintrc.cjs b/frontend/.eslintrc.cjs index 17353d58..510aa401 100644 --- a/frontend/.eslintrc.cjs +++ b/frontend/.eslintrc.cjs @@ -35,6 +35,7 @@ module.exports = { 'tailwindcss/no-custom-classname': 'off', 'import/prefer-default-export': 'off', radix: 'off', + 'dot-notation': 'off', '@typescript-eslint/no-explicit-any': 'off', // Disable full warning, and customize the typescript one // Warn about unused vars unless they start with an underscore diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 7b15d10f..561fb005 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -9,6 +9,7 @@ "version": "0.3.0", "license": "MPL-2.0", "dependencies": { + "@2toad/profanity": "^2.4.2", "@rushstack/eslint-patch": "^1.3.3", "@sentry/vite-plugin": "^2.10.2", "@sentry/vue": "^8.13.0", @@ -57,6 +58,15 @@ "node": ">=20.15.0" } }, + "node_modules/@2toad/profanity": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/@2toad/profanity/-/profanity-2.4.2.tgz", + "integrity": "sha512-8oT/KDXVD39ePyvhMXAdfuHuSBD197dSTcDGQzev/dtac6VU0cmfE4JCtvLei7wtGGNqwb1NpxBS9Q2UgqGvMQ==", + "license": "MIT", + "engines": { + "node": ">=12" + } + }, "node_modules/@alloc/quick-lru": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", diff --git a/frontend/package.json b/frontend/package.json index 8b087b9b..7593f191 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -12,6 +12,7 @@ "test": "vitest" }, "dependencies": { + "@2toad/profanity": "^2.4.2", "@rushstack/eslint-patch": "^1.3.3", "@sentry/vite-plugin": "^2.10.2", "@sentry/vue": "^8.13.0", diff --git a/frontend/src/App.vue b/frontend/src/App.vue index 617961dd..11d9f20b 100644 --- a/frontend/src/App.vue +++ b/frontend/src/App.vue @@ -4,36 +4,38 @@ import { inject, provide, computed, onMounted, } from 'vue'; import { useRoute, useRouter } from 'vue-router'; +import { storeToRefs } from 'pinia'; +import { getPreferredTheme } from '@/utils'; +import { + apiUrlKey, callKey, refreshKey, isPasswordAuthKey, isFxaAuthKey, fxaEditProfileUrlKey, hasProfanityKey, +} from '@/keys'; +import { StringResponse } from '@/models'; +import { usePosthog, posthog } from '@/composables/posthog'; +import UAParser from 'ua-parser-js'; +import { Profanity } from '@2toad/profanity'; + import NavBar from '@/components/NavBar.vue'; import TitleBar from '@/components/TitleBar.vue'; import FooterBar from '@/components/FooterBar.vue'; import SiteNotification from '@/elements/SiteNotification.vue'; -import { useSiteNotificationStore } from '@/stores/alert-store'; -import { storeToRefs } from 'pinia'; -import { getPreferredTheme } from '@/utils'; +import RouteNotFoundView from '@/views/errors/RouteNotFoundView.vue'; +import NotAuthenticatedView from '@/views/errors/NotAuthenticatedView.vue'; // stores +import { useSiteNotificationStore } from '@/stores/alert-store'; import { useUserStore } from '@/stores/user-store'; import { useCalendarStore } from '@/stores/calendar-store'; import { useAppointmentStore } from '@/stores/appointment-store'; import { useScheduleStore } from '@/stores/schedule-store'; -import RouteNotFoundView from '@/views/errors/RouteNotFoundView.vue'; -import NotAuthenticatedView from '@/views/errors/NotAuthenticatedView.vue'; - -import UAParser from 'ua-parser-js'; -import { - apiUrlKey, callKey, refreshKey, isPasswordAuthKey, isFxaAuthKey, fxaEditProfileUrlKey, -} from '@/keys'; -import { StringResponse } from '@/models'; -import { usePosthog, posthog } from '@/composables/posthog'; - // component constants const currentUser = useUserStore(); // data: { username, email, name, level, timezone, id } const apiUrl = inject(apiUrlKey); const route = useRoute(); const routeName = typeof route.name === 'string' ? route.name : ''; const router = useRouter(); +const lang = localStorage?.getItem('locale') ?? navigator.language; + const siteNotificationStore = useSiteNotificationStore(); const { isVisible: visibleNotification, @@ -48,6 +50,11 @@ const { lock: lockNotification, } = siteNotificationStore; +// Handle input filters +const profanity = new Profanity(); +const hasProfanity = (input: string) => profanity.exists(input); +provide(hasProfanityKey, hasProfanity); + // handle auth and fetch const isAuthenticated = computed(() => currentUser?.exists()); const call = createFetch({ @@ -100,18 +107,9 @@ const call = createFetch({ }, }); -// TODO: Deprecated - Please use callKey, as it's typed! -provide('call', call); provide(callKey, call); - -// TODO: Deprecated - Please use isPasswordAuthKey, as it's typed! -provide('isPasswordAuth', import.meta.env?.VITE_AUTH_SCHEME === 'password'); provide(isPasswordAuthKey, import.meta.env?.VITE_AUTH_SCHEME === 'password'); -// TODO: Deprecated - Please use isFxaAuthKey, as it's typed! -provide('isFxaAuth', import.meta.env?.VITE_AUTH_SCHEME === 'fxa'); provide(isFxaAuthKey, import.meta.env?.VITE_AUTH_SCHEME === 'fxa'); -// TODO: Deprecated - Please use fxaEditProfileUrlKey, as it's typed! -provide('fxaEditProfileUrl', import.meta.env?.VITE_FXA_EDIT_PROFILE); provide(fxaEditProfileUrlKey, import.meta.env?.VITE_FXA_EDIT_PROFILE); // menu items for main navigation @@ -172,7 +170,7 @@ const onPageLoad = async () => { resolution: deviceRes, effective_resolution: effectiveDeviceRes, user_agent: navigator.userAgent, - locale: localStorage?.getItem('locale') ?? navigator.language, + locale: lang, theme: getPreferredTheme(), }).json(); @@ -180,8 +178,6 @@ const onPageLoad = async () => { return data.value?.id ?? false; }; -// TODO: Deprecated - Please use refreshKey, as it's typed! -provide('refresh', getDbData); // provide refresh functions for components provide(refreshKey, getDbData); diff --git a/frontend/src/components/AppointmentCreatedModal.vue b/frontend/src/components/AppointmentCreatedModal.vue index 0f3612f8..17a64513 100644 --- a/frontend/src/components/AppointmentCreatedModal.vue +++ b/frontend/src/components/AppointmentCreatedModal.vue @@ -16,7 +16,7 @@ interface Props { isSchedule: boolean, // confirmation is for a schedule instead of a common appointment title: string, // title of created appointment publicLink: string, // public link of created appointment for sharing -}; +} defineProps(); // component emits diff --git a/frontend/src/components/AppointmentModal.vue b/frontend/src/components/AppointmentModal.vue index 3805f750..25f52b91 100644 --- a/frontend/src/components/AppointmentModal.vue +++ b/frontend/src/components/AppointmentModal.vue @@ -18,7 +18,7 @@ import { import PrimaryButton from '@/elements/PrimaryButton.vue'; import CautionButton from '@/elements/CautionButton.vue'; import { useUserStore } from '@/stores/user-store'; -import { dayjsKey } from "@/keys"; +import { dayjsKey } from '@/keys'; const user = useUserStore(); @@ -30,7 +30,7 @@ const dj = inject(dayjsKey); interface Props { open: boolean, // modal state appointment?: Appointment; // appointment data to display -}; +} const props = defineProps(); // attendees list diff --git a/frontend/src/components/BookingModal.vue b/frontend/src/components/BookingModal.vue index f321b469..b5933428 100644 --- a/frontend/src/components/BookingModal.vue +++ b/frontend/src/components/BookingModal.vue @@ -30,7 +30,7 @@ const emit = defineEmits(['book', 'close']); interface Props { event?: Appointment & Slot, // event data to display and book requiresConfirmation?: boolean, // Are we requesting a booking (availability) or booking it (one-off appointment.) -}; +} const props = defineProps(); // Store diff --git a/frontend/src/components/CalendarManagement.vue b/frontend/src/components/CalendarManagement.vue index 70818bb5..7959a4bb 100644 --- a/frontend/src/components/CalendarManagement.vue +++ b/frontend/src/components/CalendarManagement.vue @@ -14,10 +14,10 @@ const emit = defineEmits(['modify', 'remove', 'sync']); // component properties interface Props { calendars: Calendar[], // List of calendars to display - title: String, + title: string, type: CalendarManagementType, loading: boolean, -}; +} const props = defineProps(); // Filter by connected or not connected depending on the list type diff --git a/frontend/src/components/CalendarMiniMonth.vue b/frontend/src/components/CalendarMiniMonth.vue index 77832c3f..5d6c0f67 100644 --- a/frontend/src/components/CalendarMiniMonth.vue +++ b/frontend/src/components/CalendarMiniMonth.vue @@ -53,13 +53,13 @@ import { IconChevronLeft, IconChevronRight, } from '@tabler/icons-vue'; -import { dayjsKey } from "@/keys"; +import { dayjsKey, isoWeekdaysKey, isoFirstDayOfWeekKey } from '@/keys'; // component constants const { t } = useI18n(); const dj = inject(dayjsKey); -const isoWeekdays = inject('isoWeekdays'); -const isoFirstDayOfWeek = inject('isoFirstDayOfWeek'); +const isoWeekdays = inject(isoWeekdaysKey); +const isoFirstDayOfWeek = inject(isoFirstDayOfWeekKey); // component properties const props = defineProps({ diff --git a/frontend/src/components/CalendarQalendar.vue b/frontend/src/components/CalendarQalendar.vue index 3a5d7177..323d0a77 100644 --- a/frontend/src/components/CalendarQalendar.vue +++ b/frontend/src/components/CalendarQalendar.vue @@ -12,7 +12,7 @@ import { } from '@/definitions'; import { getLocale, getPreferredTheme, timeFormat } from '@/utils'; import { useRoute, useRouter } from 'vue-router'; -import { dayjsKey } from "@/keys"; +import { dayjsKey } from '@/keys'; // component constants const dj = inject(dayjsKey); diff --git a/frontend/src/components/ConfirmationModal.vue b/frontend/src/components/ConfirmationModal.vue index bc1cf08d..15958583 100644 --- a/frontend/src/components/ConfirmationModal.vue +++ b/frontend/src/components/ConfirmationModal.vue @@ -17,7 +17,7 @@ interface Props { confirmLabel: string, cancelLabel: string, useCautionButton?: boolean, -}; +} defineProps(); // component emits diff --git a/frontend/src/components/FTUE/ConnectVideo.vue b/frontend/src/components/FTUE/ConnectVideo.vue index 7db8c609..3e24f5cb 100644 --- a/frontend/src/components/FTUE/ConnectVideo.vue +++ b/frontend/src/components/FTUE/ConnectVideo.vue @@ -5,7 +5,9 @@ import { useFTUEStore } from '@/stores/ftue-store'; import { useExternalConnectionsStore } from '@/stores/external-connections-store'; import { storeToRefs } from 'pinia'; import { callKey } from '@/keys'; -import { AuthUrl, AuthUrlResponse, BooleanResponse, Exception, ExceptionDetail } from '@/models'; +import { + AuthUrl, AuthUrlResponse, BooleanResponse, Exception, ExceptionDetail, +} from '@/models'; import SecondaryButton from '@/tbpro/elements/SecondaryButton.vue'; import PrimaryButton from '@/tbpro/elements/PrimaryButton.vue'; import TextInput from '@/tbpro/elements/TextInput.vue'; diff --git a/frontend/src/components/FTUE/SetupProfile.vue b/frontend/src/components/FTUE/SetupProfile.vue index 4cdece9b..a25fe9a2 100644 --- a/frontend/src/components/FTUE/SetupProfile.vue +++ b/frontend/src/components/FTUE/SetupProfile.vue @@ -4,7 +4,7 @@ import { storeToRefs } from 'pinia'; import { useFTUEStore } from '@/stores/ftue-store'; import { useUserStore } from '@/stores/user-store'; import { useI18n } from 'vue-i18n'; -import { dayjsKey, callKey } from '@/keys'; +import { dayjsKey, callKey, hasProfanityKey } from '@/keys'; import TextInput from '@/tbpro/elements/TextInput.vue'; import SelectInput from '@/tbpro/elements/SelectInput.vue'; import PrimaryButton from '@/tbpro/elements/PrimaryButton.vue'; @@ -12,6 +12,8 @@ import PrimaryButton from '@/tbpro/elements/PrimaryButton.vue'; const { t } = useI18n(); const dj = inject(dayjsKey); const call = inject(callKey); +const hasProfanity = inject(hasProfanityKey); + const ftueStore = useFTUEStore(); const { hasNextStep } = storeToRefs(ftueStore); const { nextStep } = ftueStore; @@ -30,14 +32,31 @@ const username = ref(user.data?.username ?? ''); const timezone = ref(user.data.timezone ?? dj.tz.guess()); const isLoading = ref(false); +// Form validation +const errorFullName = ref(null); +const errorUsername = ref(null); + const onSubmit = async () => { isLoading.value = true; + errorFullName.value = null; + errorUsername.value = null; if (!formRef.value.checkValidity()) { isLoading.value = false; return; } + if (hasProfanity(fullName.value)) { + errorFullName.value = t('error.fieldContainsProfanity', { field: t('ftue.fullName') }); + } + if (hasProfanity(username.value)) { + errorUsername.value = t('error.fieldContainsProfanity', { field: t('label.username') }); + } + if (errorFullName.value || errorUsername.value) { + isLoading.value = false; + return; + } + const response = await user.updateUser(call, { name: fullName.value, username: username.value, @@ -57,8 +76,12 @@ const onSubmit = async () => {