From 5258ed3f2a38c8f8879853f487d18b204d5d0b71 Mon Sep 17 00:00:00 2001 From: Alexis <35.alexis.pele@gmail.com> Date: Sun, 24 Nov 2024 15:30:50 +0100 Subject: [PATCH 1/6] Added server limit to event title and description --- supabase/migrations/202401_init.sql | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/supabase/migrations/202401_init.sql b/supabase/migrations/202401_init.sql index 5d34e3a..efb4546 100644 --- a/supabase/migrations/202401_init.sql +++ b/supabase/migrations/202401_init.sql @@ -88,6 +88,14 @@ create table public.calendar_events ( ); comment on table public.calendar_events is 'Events linked to a world'; +-- Calendar Events restrictions +alter table public.calendar_events + add constraint calendar_events_maxlen_check + check ( + char_length(title) <= 240 AND + (description IS NULL OR char_length(description) <= 1200) + ); + -- Link table for events - categories create table public.calendar_event_categories_links ( calendar_event_id bigint references public.calendar_events on delete cascade, From c7069164b01d70ddbd338669568b534bfdebf1b9 Mon Sep 17 00:00:00 2001 From: Alexis <35.alexis.pele@gmail.com> Date: Sun, 24 Nov 2024 19:56:00 +0100 Subject: [PATCH 2/6] Added check on client side (api) --- components/calendar/form/UpdateEvent.vue | 22 +++++++++++++++++----- components/ui/toast/ToastViewport.vue | 2 +- i18n.config.ts | 10 ++++++++++ models/CalendarEvent.ts | 4 ++-- models/Errors.ts | 14 ++++++++++++++ nuxt.config.ts | 2 +- server/api/calendars/events/[id].patch.ts | 16 +++++++++++++--- server/api/calendars/events/create.post.ts | 2 ++ stores/CalendarStore.ts | 4 ++-- stores/EventStore.ts | 16 ++++++---------- 10 files changed, 68 insertions(+), 24 deletions(-) create mode 100644 models/Errors.ts diff --git a/components/calendar/form/UpdateEvent.vue b/components/calendar/form/UpdateEvent.vue index a814efd..70a5652 100644 --- a/components/calendar/form/UpdateEvent.vue +++ b/components/calendar/form/UpdateEvent.vue @@ -1,9 +1,14 @@ diff --git a/i18n.config.ts b/i18n.config.ts index d59fc6e..7653fcd 100644 --- a/i18n.config.ts +++ b/i18n.config.ts @@ -140,6 +140,11 @@ export default defineI18nConfig(() => ({ title: "Edit event", subtitle: "Update event data", }, + editErrors: { + toastTitle: "Event wasn't updated.", + title_too_big: "Title should be less than 240 characters long.", + description_too_big: "Description should be less than 1200 characters long.", + }, deleteDialog: { title: "Delete this event", subtitle: "Data associated with this event will be lost and you won't be able to retrieve it !", @@ -348,6 +353,11 @@ export default defineI18nConfig(() => ({ title: "Modifier l'évènement", subtitle: "Mettre à jour les données de l'évènement", }, + editErrors: { + toastTitle: "L'évènement n'a pas été modifié", + title_too_big: "Le titre doit être inférieur à 240 caractères.", + description_too_big: "La description doit être inférieure à 1200 caractères.", + }, deleteDialog: { title: "Supprimer l'évènement", subtitle: "Les données associés à cet évènement seront supprimées et vous ne pourrez plus les récupérer !", diff --git a/models/CalendarEvent.ts b/models/CalendarEvent.ts index 042b43d..43cfc9e 100644 --- a/models/CalendarEvent.ts +++ b/models/CalendarEvent.ts @@ -20,8 +20,8 @@ export interface CalendarEvent { */ export const postEventBodySchema = z.object({ event: z.object({ - title: z.string(), - description: z.string().optional().nullable(), + title: z.string().max(240), + description: z.string().max(1200).optional().nullable(), location: z.string().optional().nullable(), startDate: dateSchema.required(), endDate: dateSchema.optional().nullable(), diff --git a/models/Errors.ts b/models/Errors.ts new file mode 100644 index 0000000..16b6e8d --- /dev/null +++ b/models/Errors.ts @@ -0,0 +1,14 @@ +export interface ValidationError { + path: (string | number)[] + message: string + code: string +} + +export interface APIError { + statusCode: number + statusMessage: string + message: string + data: { + errors: ValidationError[] + } +} diff --git a/nuxt.config.ts b/nuxt.config.ts index 0dadd11..f4eea9f 100644 --- a/nuxt.config.ts +++ b/nuxt.config.ts @@ -1,6 +1,6 @@ // https://nuxt.com/docs/api/configuration/nuxt-config export default defineNuxtConfig({ - devtools: { enabled: true }, + devtools: { enabled: false }, modules: [ "@nuxtjs/supabase", diff --git a/server/api/calendars/events/[id].patch.ts b/server/api/calendars/events/[id].patch.ts index f0ab3d9..46ed8c2 100644 --- a/server/api/calendars/events/[id].patch.ts +++ b/server/api/calendars/events/[id].patch.ts @@ -22,12 +22,22 @@ export default defineEventHandler(async (event) => { } if (bodyError) { - throw createError({ + const error = createError({ cause: "Utilisateur", fatal: false, - message: "Le schéma de la requête n'est pas complet ou mal renseigné.", - status: 401, + statusCode: 401, + statusMessage: "Validation Error", + message: "Erreur de validation du schéma, cela est probablement dû à une erreur utilisateur.", + data: { + errors: bodyError.issues.map(issue => ({ + path: issue.path, + message: issue.message, + code: issue.code + })) + } }) + + throw error } try { diff --git a/server/api/calendars/events/create.post.ts b/server/api/calendars/events/create.post.ts index 780aa46..46efe29 100644 --- a/server/api/calendars/events/create.post.ts +++ b/server/api/calendars/events/create.post.ts @@ -11,6 +11,8 @@ export default defineEventHandler(async (event) => { cause: "Utilisateur", fatal: false, message: "Le schéma de la requête n'est pas complet ou mal renseigné.", + data: schemaError.issues, + statusMessage: "test", status: 401, }) } diff --git a/stores/CalendarStore.ts b/stores/CalendarStore.ts index 35acc89..cdbe5f2 100644 --- a/stores/CalendarStore.ts +++ b/stores/CalendarStore.ts @@ -869,8 +869,8 @@ export const useCalendar = defineStore("calendar", () => { const eventIndex = baseEvents.value.findIndex(e => e.id === eventSkeleton.value.id) baseEvents.value[eventIndex] = res - } catch (err) { - console.log(err) + + return res } finally { abortController = null isUpdatingEvent.value = false diff --git a/stores/EventStore.ts b/stores/EventStore.ts index 3eecbac..935c59c 100644 --- a/stores/EventStore.ts +++ b/stores/EventStore.ts @@ -247,17 +247,13 @@ export const useCalendarEvents = defineStore("calendar-events", () => { abortController = new AbortController() isUpdatingEvent.value = true - try { - const res = await $fetch(`/api/calendars/events/${eventSkeleton.value.id}`, { method: "PATCH", body: { event : eventSkeleton.value, calendarId: activeCalendar?.id }, signal: abortController.signal }) + const res = await $fetch(`/api/calendars/events/${eventSkeleton.value.id}`, { method: "PATCH", body: { event : eventSkeleton.value, calendarId: activeCalendar?.id }, signal: abortController.signal }) - const eventIndex = baseEvents.value.findIndex(e => e.id === eventSkeleton.value.id) - baseEvents.value[eventIndex] = res - } catch (err) { - console.log(err) - } finally { - abortController = null - isUpdatingEvent.value = false - } + const eventIndex = baseEvents.value.findIndex(e => e.id === eventSkeleton.value.id) + baseEvents.value[eventIndex] = res + + abortController = null + isUpdatingEvent.value = false } async function deleteEventFromSkeleton() { From 372c7a49e1ee5d2b8d22dd6e638d75d8b2dc9f66 Mon Sep 17 00:00:00 2001 From: Alexis <35.alexis.pele@gmail.com> Date: Sun, 24 Nov 2024 20:08:30 +0100 Subject: [PATCH 3/6] Added validation for location as well --- i18n.config.ts | 2 ++ models/CalendarEvent.ts | 2 +- server/api/calendars/events/[id].patch.ts | 2 +- server/api/calendars/events/create.post.ts | 18 +++++++++++++----- supabase/migrations/202401_init.sql | 3 ++- 5 files changed, 19 insertions(+), 8 deletions(-) diff --git a/i18n.config.ts b/i18n.config.ts index 7653fcd..e655bfc 100644 --- a/i18n.config.ts +++ b/i18n.config.ts @@ -144,6 +144,7 @@ export default defineI18nConfig(() => ({ toastTitle: "Event wasn't updated.", title_too_big: "Title should be less than 240 characters long.", description_too_big: "Description should be less than 1200 characters long.", + location_too_big: "Location should be less than 240 characters long.", }, deleteDialog: { title: "Delete this event", @@ -357,6 +358,7 @@ export default defineI18nConfig(() => ({ toastTitle: "L'évènement n'a pas été modifié", title_too_big: "Le titre doit être inférieur à 240 caractères.", description_too_big: "La description doit être inférieure à 1200 caractères.", + location_too_big: "La localisation doit être inférieure à 240 caractères.", }, deleteDialog: { title: "Supprimer l'évènement", diff --git a/models/CalendarEvent.ts b/models/CalendarEvent.ts index 43cfc9e..cd6dfc2 100644 --- a/models/CalendarEvent.ts +++ b/models/CalendarEvent.ts @@ -22,7 +22,7 @@ export const postEventBodySchema = z.object({ event: z.object({ title: z.string().max(240), description: z.string().max(1200).optional().nullable(), - location: z.string().optional().nullable(), + location: z.string().max(240).optional().nullable(), startDate: dateSchema.required(), endDate: dateSchema.optional().nullable(), hidden: z.boolean().optional().nullable(), diff --git a/server/api/calendars/events/[id].patch.ts b/server/api/calendars/events/[id].patch.ts index 46ed8c2..ca0aa9a 100644 --- a/server/api/calendars/events/[id].patch.ts +++ b/server/api/calendars/events/[id].patch.ts @@ -27,7 +27,7 @@ export default defineEventHandler(async (event) => { fatal: false, statusCode: 401, statusMessage: "Validation Error", - message: "Erreur de validation du schéma, cela est probablement dû à une erreur utilisateur.", + message: "Erreur de validation du schéma, probablement dûe à une erreur utilisateur.", data: { errors: bodyError.issues.map(issue => ({ path: issue.path, diff --git a/server/api/calendars/events/create.post.ts b/server/api/calendars/events/create.post.ts index 46efe29..c0fe7f0 100644 --- a/server/api/calendars/events/create.post.ts +++ b/server/api/calendars/events/create.post.ts @@ -7,14 +7,22 @@ export default defineEventHandler(async (event) => { const { data: bodyData, error: schemaError } = await readValidatedBody(event, body => postEventBodySchema.safeParse(body)) if (schemaError) { - throw createError({ + const error = createError({ cause: "Utilisateur", fatal: false, - message: "Le schéma de la requête n'est pas complet ou mal renseigné.", - data: schemaError.issues, - statusMessage: "test", - status: 401, + statusCode: 401, + statusMessage: "Validation Error", + message: "Erreur de validation du schéma, probablement dûe à une erreur utilisateur.", + data: { + errors: schemaError.issues.map(issue => ({ + path: issue.path, + message: issue.message, + code: issue.code + })) + } }) + + throw error } try { diff --git a/supabase/migrations/202401_init.sql b/supabase/migrations/202401_init.sql index efb4546..96d19a4 100644 --- a/supabase/migrations/202401_init.sql +++ b/supabase/migrations/202401_init.sql @@ -93,7 +93,8 @@ alter table public.calendar_events add constraint calendar_events_maxlen_check check ( char_length(title) <= 240 AND - (description IS NULL OR char_length(description) <= 1200) + (description IS NULL OR char_length(description) <= 1200) AND + (location IS NULL OR char_length(location) <= 240) ); -- Link table for events - categories From 863937bb160d259671ef422b368a0024791025cc Mon Sep 17 00:00:00 2001 From: Alexis <35.alexis.pele@gmail.com> Date: Mon, 25 Nov 2024 16:33:02 +0100 Subject: [PATCH 4/6] Changed limit and added scrollbars on details --- components/calendar/CalendarEventDetails.vue | 32 ++++++++++++-------- components/calendar/form/UpdateEvent.vue | 6 +++- i18n.config.ts | 8 ++--- models/CalendarEvent.ts | 4 +-- supabase/migrations/202401_init.sql | 4 +-- 5 files changed, 33 insertions(+), 21 deletions(-) diff --git a/components/calendar/CalendarEventDetails.vue b/components/calendar/CalendarEventDetails.vue index eac1413..cbbc47f 100644 --- a/components/calendar/CalendarEventDetails.vue +++ b/components/calendar/CalendarEventDetails.vue @@ -75,7 +75,7 @@ function deployDeleteModal() { >
-
+
{{ event.title }}
@@ -100,18 +100,26 @@ function deployDeleteModal() {
-

- {{ dateDifference }} -

+
+
+ +
+

{{ dateDifference }}

+
@@ -134,7 +142,7 @@ function deployDeleteModal() { diff --git a/components/calendar/form/UpdateEvent.vue b/components/calendar/form/UpdateEvent.vue index 70a5652..644e550 100644 --- a/components/calendar/form/UpdateEvent.vue +++ b/components/calendar/form/UpdateEvent.vue @@ -104,6 +104,7 @@ function handleCancel() { name="new-event-title" required :placeholder="$t('entity.calendar.event.title')" + :maxlength="120" class="w-full -my-1 py-1 -mx-1 px-1 text-lg border-b-[1px] bg-transparent focus-visible:outline-none focus-visible:border-blue-600" >
@@ -116,6 +117,7 @@ function handleCancel() { name="new-event-description" :placeholder="$t('entity.addDescription')" class="w-full -my-1 py-1 -mx-1 px-1 min-h-24 max-h-36 text-sm border-b-[1px] bg-transparent focus-visible:outline-none focus-visible:border-blue-600" + :maxlength="1200" />
@@ -170,7 +172,9 @@ function handleCancel() { type="text" name="new-event-location" :placeholder="$t('entity.calendar.event.addLocation')" - class="w-full -my-1 py-2 px-2 text-sm border-b-[1px] bg-transparent focus-visible:outline-none focus-visible:border-blue-600"> + class="w-full -my-1 py-2 px-2 text-sm border-b-[1px] bg-transparent focus-visible:outline-none focus-visible:border-blue-600" + :maxlength="160" + > diff --git a/i18n.config.ts b/i18n.config.ts index e655bfc..628b124 100644 --- a/i18n.config.ts +++ b/i18n.config.ts @@ -142,9 +142,9 @@ export default defineI18nConfig(() => ({ }, editErrors: { toastTitle: "Event wasn't updated.", - title_too_big: "Title should be less than 240 characters long.", + title_too_big: "Title should be less than 120 characters long.", description_too_big: "Description should be less than 1200 characters long.", - location_too_big: "Location should be less than 240 characters long.", + location_too_big: "Location should be less than 160 characters long.", }, deleteDialog: { title: "Delete this event", @@ -356,9 +356,9 @@ export default defineI18nConfig(() => ({ }, editErrors: { toastTitle: "L'évènement n'a pas été modifié", - title_too_big: "Le titre doit être inférieur à 240 caractères.", + title_too_big: "Le titre doit être inférieur à 120 caractères.", description_too_big: "La description doit être inférieure à 1200 caractères.", - location_too_big: "La localisation doit être inférieure à 240 caractères.", + location_too_big: "La localisation doit être inférieure à 160 caractères.", }, deleteDialog: { title: "Supprimer l'évènement", diff --git a/models/CalendarEvent.ts b/models/CalendarEvent.ts index cd6dfc2..65562fa 100644 --- a/models/CalendarEvent.ts +++ b/models/CalendarEvent.ts @@ -20,9 +20,9 @@ export interface CalendarEvent { */ export const postEventBodySchema = z.object({ event: z.object({ - title: z.string().max(240), + title: z.string().max(120), description: z.string().max(1200).optional().nullable(), - location: z.string().max(240).optional().nullable(), + location: z.string().max(160).optional().nullable(), startDate: dateSchema.required(), endDate: dateSchema.optional().nullable(), hidden: z.boolean().optional().nullable(), diff --git a/supabase/migrations/202401_init.sql b/supabase/migrations/202401_init.sql index 96d19a4..7916e7c 100644 --- a/supabase/migrations/202401_init.sql +++ b/supabase/migrations/202401_init.sql @@ -92,9 +92,9 @@ comment on table public.calendar_events is 'Events linked to a world'; alter table public.calendar_events add constraint calendar_events_maxlen_check check ( - char_length(title) <= 240 AND + char_length(title) <= 120 AND (description IS NULL OR char_length(description) <= 1200) AND - (location IS NULL OR char_length(location) <= 240) + (location IS NULL OR char_length(location) <= 160) ); -- Link table for events - categories From d774fde1dd091f3c17b8e9aa6adeae4635485852 Mon Sep 17 00:00:00 2001 From: Alexis <35.alexis.pele@gmail.com> Date: Mon, 25 Nov 2024 17:41:40 +0100 Subject: [PATCH 5/6] Added client side validation for fields --- components/calendar/form/CreateEvent.vue | 47 +++++++++++++----- components/calendar/form/UpdateEvent.vue | 63 +++++++++++++++--------- i18n.config.ts | 10 ++++ 3 files changed, 87 insertions(+), 33 deletions(-) diff --git a/components/calendar/form/CreateEvent.vue b/components/calendar/form/CreateEvent.vue index 3a63e4f..65003ee 100644 --- a/components/calendar/form/CreateEvent.vue +++ b/components/calendar/form/CreateEvent.vue @@ -5,6 +5,9 @@ import { PhAlarm, PhCircleNotch, PhEye, PhEyeClosed, PhMapPinArea, PhTag } from const { eventSkeleton, operationInProgress } = storeToRefs(useCalendar()) const { resetSkeleton, submitSkeleton, cancelLatestRequest } = useCalendar() const popoverOpen = ref(false) + +const { t } = useI18n() + const isLoading = ref(false) const formErrors = reactive<{ message: string | null }>({ @@ -88,7 +91,7 @@ function handleCancel() { :collision-padding="60" :disable-outside-pointer-events="true" :trap-focus="true" - class="pl-3 min-w-96 border-indigo-200 dark:bg-slate-950 dark:border-indigo-950" + class="pl-3 w-[30rem] max-w-full border-indigo-200 dark:bg-slate-950 dark:border-indigo-950" @escape-key-down="handleClosing" @focus-outside="handleClosing" @interact-outside="handleClosing" @@ -104,8 +107,14 @@ function handleCancel() { name="new-event-title" required :placeholder="$t('entity.calendar.event.title')" - class="w-full -my-1 py-1 -mx-1 px-1 text-lg border-b-[1px] bg-transparent focus-visible:outline-none focus-visible:border-blue-600" + :maxlength="120" + pattern="([A-Za-zÀ-ÖØ-öø-ÿ0-9\s\&\-\~]+){3,120}" + class="w-full -my-1 py-1 -mx-1 px-1 text-lg border-b-[1px] bg-transparent focus-visible:outline-none focus-visible:border-blue-600 invalid:border-red-500" > + +
+ {{ t('entity.calendar.event.patterns.title') }} +
@@ -113,8 +122,13 @@ function handleCancel() { id="new-event-description" v-model="eventSkeleton.description" name="new-event-description" - :placeholder="$t('entity.addDescription')" class="w-full -my-1 py-1 -mx-1 px-1 min-h-24 max-h-36 text-sm border-b-[1px] bg-transparent focus-visible:outline-none focus-visible:border-blue-600" + :placeholder="$t('entity.addDescription')" + :maxlength="1200" + class="w-full -my-1 py-1 -mx-1 px-1 min-h-24 max-h-36 text-sm border-b-[1px] bg-transparent focus-visible:outline-none focus-visible:border-blue-600 invalid:border-red-500" /> +
+ {{ t('entity.calendar.event.patterns.description') }} +
@@ -142,7 +156,10 @@ function handleCancel() {
- +
@@ -158,13 +175,21 @@ function handleCancel() {
- +
+ +
+ {{ t('entity.calendar.event.patterns.location') }} +
+
diff --git a/components/calendar/form/UpdateEvent.vue b/components/calendar/form/UpdateEvent.vue index 644e550..3bf6975 100644 --- a/components/calendar/form/UpdateEvent.vue +++ b/components/calendar/form/UpdateEvent.vue @@ -97,16 +97,22 @@ function handleCancel() {
- +
+ +
+ {{ t('entity.calendar.event.patterns.title') }} +
+
@@ -116,9 +122,12 @@ function handleCancel() { v-model="eventSkeleton.description" name="new-event-description" :placeholder="$t('entity.addDescription')" - class="w-full -my-1 py-1 -mx-1 px-1 min-h-24 max-h-36 text-sm border-b-[1px] bg-transparent focus-visible:outline-none focus-visible:border-blue-600" :maxlength="1200" + class="w-full -my-1 py-1 -mx-1 px-1 min-h-24 max-h-36 text-sm border-b-[1px] bg-transparent focus-visible:outline-none focus-visible:border-blue-600 invalid:border-red-500" /> +
+ {{ t('entity.calendar.event.patterns.description') }} +
@@ -147,7 +156,10 @@ function handleCancel() {
- +
@@ -157,7 +169,7 @@ function handleCancel() {
- +
--> @@ -166,15 +178,22 @@ function handleCancel() {
- +
+ + +
+ {{ t('entity.calendar.event.patterns.location') }} +
+
diff --git a/i18n.config.ts b/i18n.config.ts index 628b124..c4ff153 100644 --- a/i18n.config.ts +++ b/i18n.config.ts @@ -136,6 +136,11 @@ export default defineI18nConfig(() => ({ addLocation: "Add a place", prevPage: "Previous page with events", nextPage: "Next page with events", + patterns: { + title: "Between 3 and 120 caracters, without special symbols, except & - ~ ()", + description: "1200 caracters max.", + location: "Between 3 and 160 caracters, without special symbols, except & - ~ ()", + }, editDialog: { title: "Edit event", subtitle: "Update event data", @@ -350,6 +355,11 @@ export default defineI18nConfig(() => ({ addLocation: "Ajouter un endroit", prevPage: "Précédente page à évènements", nextPage: "Prochaine page à évènements", + patterns: { + title: "Entre 3 et 120 charactères, sans symboles spéciaux, sauf & - ~ ()", + description: "Maximum 1200 charactères.", + location: "Entre 3 et 160 charactères, sans symboles spéciaux, sauf & - ~ ()", + }, editDialog: { title: "Modifier l'évènement", subtitle: "Mettre à jour les données de l'évènement", From 378f07a862c0156d4115edcf0cc23467804c1bc2 Mon Sep 17 00:00:00 2001 From: Alexis <35.alexis.pele@gmail.com> Date: Mon, 25 Nov 2024 18:09:18 +0100 Subject: [PATCH 6/6] User locale now saves ! --- nuxt.config.ts | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/nuxt.config.ts b/nuxt.config.ts index f4eea9f..3db37db 100644 --- a/nuxt.config.ts +++ b/nuxt.config.ts @@ -16,12 +16,21 @@ export default defineNuxtConfig({ i18n: { vueI18n: "./i18n.config.ts", + strategy: "no_prefix", detectBrowserLanguage: { useCookie: true, cookieKey: "tttools_lang", - redirectOn: "all", - alwaysRedirect: true, - } + }, + locales: [ + { + code: "en", + name: "English" + }, + { + code: "fr", + name: "Français" + } + ] }, supabase: {