Skip to content

Commit

Permalink
Profanity filter (#658)
Browse files Browse the repository at this point in the history
* ❌ 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
  • Loading branch information
devmount authored Sep 16, 2024
1 parent b9d0fcf commit 41f807e
Show file tree
Hide file tree
Showing 62 changed files with 298 additions and 164 deletions.
1 change: 1 addition & 0 deletions frontend/.eslintrc.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
10 changes: 10 additions & 0 deletions frontend/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
46 changes: 21 additions & 25 deletions frontend/src/App.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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({
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -172,16 +170,14 @@ const onPageLoad = async () => {
resolution: deviceRes,
effective_resolution: effectiveDeviceRes,
user_agent: navigator.userAgent,
locale: localStorage?.getItem('locale') ?? navigator.language,
locale: lang,
theme: getPreferredTheme(),
}).json();
const { data } = response;
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);
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/components/AppointmentCreatedModal.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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<Props>();
// component emits
Expand Down
4 changes: 2 additions & 2 deletions frontend/src/components/AppointmentModal.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand All @@ -30,7 +30,7 @@ const dj = inject(dayjsKey);
interface Props {
open: boolean, // modal state
appointment?: Appointment; // appointment data to display
};
}
const props = defineProps<Props>();
// attendees list
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/components/BookingModal.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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<Props>();
// Store
Expand Down
4 changes: 2 additions & 2 deletions frontend/src/components/CalendarManagement.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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<Props>();
// Filter by connected or not connected depending on the list type
Expand Down
6 changes: 3 additions & 3 deletions frontend/src/components/CalendarMiniMonth.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/components/CalendarQalendar.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/components/ConfirmationModal.vue
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ interface Props {
confirmLabel: string,
cancelLabel: string,
useCautionButton?: boolean,
};
}
defineProps<Props>();
// component emits
Expand Down
4 changes: 3 additions & 1 deletion frontend/src/components/FTUE/ConnectVideo.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
29 changes: 26 additions & 3 deletions frontend/src/components/FTUE/SetupProfile.vue
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,16 @@ 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';
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;
Expand All @@ -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<string>(null);
const errorUsername = ref<string>(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,
Expand All @@ -57,8 +76,12 @@ const onSubmit = async () => {
<template>
<div class="content">
<form ref="formRef" autocomplete="off" autofocus @submit.prevent @keyup.enter="onSubmit">
<text-input name="full-name" v-model="fullName" :required="true">{{ t('ftue.fullName') }}</text-input>
<text-input name="username" v-model="username" :required="true">{{ t('label.username') }}</text-input>
<text-input name="full-name" v-model="fullName" :required="true" :error="errorFullName">
{{ t('ftue.fullName') }}
</text-input>
<text-input name="username" v-model="username" :required="true" :error="errorUsername">
{{ t('label.username') }}
</text-input>
<select-input
name="timezone"
:options="timezoneOptions"
Expand Down
27 changes: 23 additions & 4 deletions frontend/src/components/FTUE/SetupSchedule.vue
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,9 @@ import { useFTUEStore } from '@/stores/ftue-store';
import { useUserStore } from '@/stores/user-store';
import { useCalendarStore } from '@/stores/calendar-store';
import { useScheduleStore } from '@/stores/schedule-store';
import { dayjsKey, callKey, isoWeekdaysKey } from '@/keys';
import {
dayjsKey, callKey, isoWeekdaysKey, hasProfanityKey,
} from '@/keys';
import { Error, SelectOption } from '@/models';
import TextInput from '@/tbpro/elements/TextInput.vue';
import SelectInput from '@/tbpro/elements/SelectInput.vue';
Expand All @@ -21,6 +23,7 @@ const { t } = useI18n();
const dj = inject(dayjsKey);
const call = inject(callKey);
const isoWeekdays = inject(isoWeekdaysKey);
const hasProfanity = inject(hasProfanityKey);
const ftueStore = useFTUEStore();
const {
Expand Down Expand Up @@ -63,15 +66,25 @@ const schedule = ref({
const duration = computed(() => `${schedule.value.duration} minute`);
const isLoading = ref(false);
// Form validation
const errorScheduleName = ref<string>(null);
const onSubmit = async () => {
isLoading.value = true;
errorMessage.value = null;
errorScheduleName.value = null;
if (!formRef.value.checkValidity()) {
isLoading.value = false;
return;
}
if (hasProfanity(schedule.value.name)) {
errorScheduleName.value = t('error.fieldContainsProfanity', { field: t('ftue.scheduleName') });
isLoading.value = false;
return;
}
const scheduleData = {
...schedules?.value[0] ?? {},
active: true,
Expand Down Expand Up @@ -133,10 +146,16 @@ onMounted(async () => {
<div class="content">
<form ref="formRef" autocomplete="off" autofocus @submit.prevent @keyup.enter="onSubmit">
<div class="column">
<text-input name="scheduleName" v-model="schedule.name" required>{{ t('ftue.scheduleName') }}</text-input>
<text-input name="scheduleName" v-model="schedule.name" required :error="errorScheduleName">
{{ t('ftue.scheduleName') }}
</text-input>
<div class="pair">
<text-input type="time" name="startTime" v-model="schedule.startTime" required>{{ t('label.startTime') }}</text-input>
<text-input type="time" name="endTime" v-model="schedule.endTime" required>{{ t('label.endTime') }}</text-input>
<text-input type="time" name="startTime" v-model="schedule.startTime" required>
{{ t('label.startTime') }}
</text-input>
<text-input type="time" name="endTime" v-model="schedule.endTime" required>
{{ t('label.endTime') }}
</text-input>
</div>
<bubble-select class="bubbleSelect" :options="scheduleDayOptions" v-model="schedule.days" :required="true">
{{ t('label.availableDays') }}
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/components/GenericModal.vue
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<script setup lang="ts">
import {
onMounted, inject, ref, toRefs, onUnmounted,
onMounted, inject, toRefs, onUnmounted,
} from 'vue';
import { refreshKey } from '@/keys';
import NoticeBar from '@/tbpro/elements/NoticeBar.vue';
Expand Down
6 changes: 1 addition & 5 deletions frontend/src/components/NavBar.vue
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
<script setup lang="ts">
import { inject } from 'vue';
import { useI18n } from 'vue-i18n';
import { useRoute, useRouter } from 'vue-router';
import { useRoute } from 'vue-router';
import { useUserStore } from '@/stores/user-store';
import { callKey } from '@/keys';
import UserAvatar from '@/elements/UserAvatar.vue';
import DropDown from '@/elements/DropDown.vue';
import NavBarItem from '@/elements/NavBarItem.vue';
Expand All @@ -13,9 +11,7 @@ import { IconExternalLink } from '@tabler/icons-vue';
// component constants
const user = useUserStore();
const route = useRoute();
const router = useRouter();
const { t } = useI18n();
const call = inject(callKey);
// component properties
interface Props {
Expand Down
Loading

0 comments on commit 41f807e

Please sign in to comment.