From 7f922cbea1abbfe87287e50a7174514f2cc4af3c Mon Sep 17 00:00:00 2001 From: Camille Moussu <66134347+Eriikah@users.noreply.github.com> Date: Tue, 21 Apr 2026 11:20:44 +0200 Subject: [PATCH] [#741] separation of base eventAPI to EventDao and eventApi (#812) --- __test__/features/Events/FreeBusy.test.tsx | 218 ++++++++--- src/components/Attendees/useFreeBusy.ts | 12 +- .../EventCounterModal.tsx | 2 +- src/features/Events/EventApi.ts | 351 ++++++++++-------- src/features/Events/EventDao.ts | 123 ++++++ src/features/Events/FreeBusyApi.ts | 66 ++++ src/features/Events/FreeBusyDao.ts | 59 +++ .../api/getFreeBusyForAddedAttendeesREPORT.ts | 37 -- .../api/getFreeBusyForEventAttendeesPOST.ts | 48 --- .../Events/api/getUserDataFromEmail.ts | 11 - .../Events/api/sendCounterProposal.ts | 84 ----- src/features/Events/api/updateSeries.ts | 46 +-- src/features/User/UserDao.ts | 14 + src/features/User/userAPI.ts | 11 +- 14 files changed, 657 insertions(+), 425 deletions(-) create mode 100644 src/features/Events/EventDao.ts create mode 100644 src/features/Events/FreeBusyApi.ts create mode 100644 src/features/Events/FreeBusyDao.ts delete mode 100644 src/features/Events/api/getFreeBusyForAddedAttendeesREPORT.ts delete mode 100644 src/features/Events/api/getFreeBusyForEventAttendeesPOST.ts delete mode 100644 src/features/Events/api/getUserDataFromEmail.ts delete mode 100644 src/features/Events/api/sendCounterProposal.ts create mode 100644 src/features/User/UserDao.ts diff --git a/__test__/features/Events/FreeBusy.test.tsx b/__test__/features/Events/FreeBusy.test.tsx index 0463587..b3b786a 100644 --- a/__test__/features/Events/FreeBusy.test.tsx +++ b/__test__/features/Events/FreeBusy.test.tsx @@ -2,9 +2,9 @@ import { hasFreeBusyConflict, useAttendeesFreeBusy } from '@/components/Attendees/useFreeBusy' -import * as getFreeBusyREPORT from '@/features/Events/api/getFreeBusyForAddedAttendeesREPORT' -import * as getFreeBusyPOST from '@/features/Events/api/getFreeBusyForEventAttendeesPOST' -import * as getUserData from '@/features/Events/api/getUserDataFromEmail' +import * as CalendarApi from '@/features/Calendars/CalendarApi' +import * as FreeBusyDao from '@/features/Events/FreeBusyDao' +import * as UserDao from '@/features/User/UserDao' import { renderHook, waitFor } from '@testing-library/react' jest.mock('moment-timezone', () => { @@ -12,26 +12,130 @@ jest.mock('moment-timezone', () => { return actual }) -jest.mock('@/features/Events/api/getUserDataFromEmail') -jest.mock('@/features/Events/api/getFreeBusyForAddedAttendeesREPORT') -jest.mock('@/features/Events/api/getFreeBusyForEventAttendeesPOST') +jest.mock('@/features/Events/FreeBusyDao') +jest.mock('@/features/User/UserDao') +jest.mock('@/features/Calendars/CalendarApi') -const mockGetUserData = getUserData.getUserDataFromEmail as jest.MockedFunction< - typeof getUserData.getUserDataFromEmail +const mockGetUserData = UserDao.fetchUserByEmail as jest.MockedFunction< + typeof UserDao.fetchUserByEmail > -const mockREPORT = - getFreeBusyREPORT.getFreeBusyForAddedAttendeesREPORT as jest.MockedFunction< - typeof getFreeBusyREPORT.getFreeBusyForAddedAttendeesREPORT - > -const mockPOST = - getFreeBusyPOST.getFreeBusyForEventAttendeesPOST as jest.MockedFunction< - typeof getFreeBusyPOST.getFreeBusyForEventAttendeesPOST - > +const mockREPORT = FreeBusyDao.fetchFreeBusyReports as jest.MockedFunction< + typeof FreeBusyDao.fetchFreeBusyReports +> +const mockPOST = FreeBusyDao.fetchFreeBusyPost as jest.MockedFunction< + typeof FreeBusyDao.fetchFreeBusyPost +> +const mockGetCalendars = CalendarApi.getCalendars as jest.MockedFunction< + typeof CalendarApi.getCalendars +> + +const mockCalendarsResponse = { + _embedded: { + 'dav:calendar': [ + { + _links: { + self: { + href: '/calendars/user-carol/user-carol' + } + } + } + ] + } +} + +const freeIcal = [ + { + _links: { + self: { + href: '\/calendars\/user-carol\/e38d1ea7-6d2c-46c6-8933-cdb325d4458e' + } + }, + data: [ + 'vcalendar', + [], + [ + [ + 'vfreebusy', + [ + ['dtstart', {}, 'date-time', '2026-03-14T14:00:00Z'], + ['dtend', {}, 'date-time', '2026-03-14T15:00:00Z'], + ['dtstamp', {}, 'date-time', '2026-03-14T13:00:00Z'] + ], + [] + ] + ] + ] + } +] + +const busyIcal = [ + { + _links: { + self: { + href: '\/calendars\/user-carol\/e38d1ea7-6d2c-46c6-8933-cdb325d4458e' + } + }, + data: [ + 'vcalendar', + [], + [ + [ + 'vfreebusy', + [ + ['dtstart', {}, 'date-time', '2026-03-14T14:00:00Z'], + ['dtend', {}, 'date-time', '2026-03-14T15:00:00Z'], + ['dtstamp', {}, 'date-time', '2026-03-14T13:00:00Z'], + [ + 'freebusy', + {}, + 'period', + ['2026-03-14T14:00:00Z', '2026-03-14T15:00:00Z'] + ] + ], + [] + ] + ] + ] + } +] const START = '2026-03-14T14:00:00' const END = '2026-03-14T15:00:00' const TZ = 'Europe/Paris' +const freePostResponse = { + start: '20260314T130000', + end: '20260314T140000', + users: [ + { + id: 'user-carol', + calendars: [{ id: 'user-carol', busy: [] }] + } + ] +} + +const busyPostResponse = { + start: '20260314T130000', + end: '20260314T140000', + users: [ + { + id: 'user-carol', + calendars: [ + { + id: 'user-carol', + busy: [ + { + uid: 'fa507111-07d5-44b4-a611-c27845a66ccf', + start: START, + end: END + } + ] + } + ] + } + ] +} + describe('hasFreeBusyConflict', () => { it('returns false for non-vcalendar data', () => { expect(hasFreeBusyConflict({ data: ['not-vcalendar', [], []] })).toBe(false) @@ -82,13 +186,16 @@ describe('hasFreeBusyConflict', () => { }) describe('useAttendeesFreeBusy — Flow B (new attendees)', () => { - beforeEach(() => jest.clearAllMocks()) + beforeEach(() => { + jest.clearAllMocks() + mockGetCalendars.mockResolvedValue(mockCalendarsResponse as never) + }) const newAttendee = { email: 'alice@example.com', userId: 'user-alice' } it('shows loading then free for a new attendee', async () => { - mockREPORT.mockResolvedValue(false) - + mockGetCalendars.mockResolvedValue(mockCalendarsResponse as never) + mockREPORT.mockResolvedValue(freeIcal) const { result } = renderHook(() => useAttendeesFreeBusy({ existingAttendees: [], @@ -105,8 +212,8 @@ describe('useAttendeesFreeBusy — Flow B (new attendees)', () => { }) it('shows busy when REPORT returns true', async () => { - mockREPORT.mockResolvedValue(true) - + mockGetCalendars.mockResolvedValue(mockCalendarsResponse as never) + mockREPORT.mockResolvedValue(busyIcal) const { result } = renderHook(() => useAttendeesFreeBusy({ existingAttendees: [], @@ -140,8 +247,20 @@ describe('useAttendeesFreeBusy — Flow B (new attendees)', () => { it('resolves userId via getUserDataFromEmail when not provided', async () => { mockGetUserData.mockResolvedValue([{ _id: 'resolved-id' }] as never) - mockREPORT.mockResolvedValue(false) - + mockGetCalendars.mockResolvedValue({ + _embedded: { + 'dav:calendar': [ + { + _links: { + self: { + href: '/calendars/resolved-id/resolved-id' + } + } + } + ] + } + } as never) + mockREPORT.mockResolvedValue(freeIcal) const attendeeWithoutId = { email: 'bob@example.com' } const { result } = renderHook(() => @@ -159,9 +278,11 @@ describe('useAttendeesFreeBusy — Flow B (new attendees)', () => { ) expect(mockGetUserData).toHaveBeenCalledWith('bob@example.com') expect(mockREPORT).toHaveBeenCalledWith( - 'resolved-id', - expect.any(String), - expect.any(String) + expect.objectContaining({ + hrefs: ['/calendars/resolved-id/resolved-id'], + start: expect.any(String), + end: expect.any(String) + }) ) }) @@ -185,8 +306,8 @@ describe('useAttendeesFreeBusy — Flow B (new attendees)', () => { }) it('does not re-fetch an attendee already fetched', async () => { - mockREPORT.mockResolvedValue(false) - + mockGetCalendars.mockResolvedValue(mockCalendarsResponse as never) + mockREPORT.mockResolvedValue(freeIcal) const { result, rerender } = renderHook( ({ attendees }) => useAttendeesFreeBusy({ @@ -206,8 +327,8 @@ describe('useAttendeesFreeBusy — Flow B (new attendees)', () => { }) it('removes departed attendee from the map', async () => { - mockREPORT.mockResolvedValue(false) - + mockGetCalendars.mockResolvedValue(mockCalendarsResponse as never) + mockREPORT.mockResolvedValue(freeIcal) const { result, rerender } = renderHook( ({ attendees }) => useAttendeesFreeBusy({ @@ -230,8 +351,8 @@ describe('useAttendeesFreeBusy — Flow B (new attendees)', () => { }) it('invalidates cache and re-fetches when time window changes', async () => { - mockREPORT.mockResolvedValue(false) - + mockGetCalendars.mockResolvedValue(mockCalendarsResponse as never) + mockREPORT.mockResolvedValue(freeIcal) const { result, rerender } = renderHook( ({ start, end }) => useAttendeesFreeBusy({ @@ -254,8 +375,8 @@ describe('useAttendeesFreeBusy — Flow B (new attendees)', () => { }) it('passes UTC-converted iCal times to the API', async () => { - mockREPORT.mockResolvedValue(false) - + mockGetCalendars.mockResolvedValue(mockCalendarsResponse as never) + mockREPORT.mockResolvedValue(freeIcal) renderHook(() => useAttendeesFreeBusy({ existingAttendees: [], @@ -270,9 +391,10 @@ describe('useAttendeesFreeBusy — Flow B (new attendees)', () => { // Paris is UTC+1 in March (before DST), so 16:00 → 15:00 UTC expect(mockREPORT).toHaveBeenCalledWith( - 'user-alice', - '20260314T150000', - '20260314T160000' + expect.objectContaining({ + start: '20260314T150000', + end: '20260314T160000' + }) ) }) }) @@ -297,7 +419,7 @@ describe('useAttendeesFreeBusy — Flow A (existing attendees)', () => { }) it('shows loading then free for existing attendees', async () => { - mockPOST.mockResolvedValue({ 'user-carol': false }) + mockPOST.mockResolvedValue(freePostResponse) const { result } = renderHook(() => useAttendeesFreeBusy({ @@ -318,7 +440,7 @@ describe('useAttendeesFreeBusy — Flow A (existing attendees)', () => { }) it('shows busy when POST returns busy for userId', async () => { - mockPOST.mockResolvedValue({ 'user-carol': true }) + mockPOST.mockResolvedValue(busyPostResponse) const { result } = renderHook(() => useAttendeesFreeBusy({ @@ -331,9 +453,9 @@ describe('useAttendeesFreeBusy — Flow A (existing attendees)', () => { }) ) - await waitFor(() => + await waitFor(() => { expect(result.current[existingAttendee.email]).toBe('busy') - ) + }) }) it('shows unknown when POST throws', async () => { @@ -356,7 +478,7 @@ describe('useAttendeesFreeBusy — Flow A (existing attendees)', () => { }) it('passes the eventUid to the POST API', async () => { - mockPOST.mockResolvedValue({ 'user-carol': false }) + mockPOST.mockResolvedValue(freePostResponse) renderHook(() => useAttendeesFreeBusy({ @@ -370,12 +492,12 @@ describe('useAttendeesFreeBusy — Flow A (existing attendees)', () => { ) await waitFor(() => expect(mockPOST).toHaveBeenCalled()) - expect(mockPOST).toHaveBeenCalledWith( - ['user-carol'], - expect.any(String), - expect.any(String), - 'event-abc' - ) + expect(mockPOST).toHaveBeenCalledWith({ + end: '20260314T140000', + eventUid: 'event-abc', + start: '20260314T130000', + userIds: ['user-carol'] + }) }) }) diff --git a/src/components/Attendees/useFreeBusy.ts b/src/components/Attendees/useFreeBusy.ts index abbaf42..8bd9ee7 100644 --- a/src/components/Attendees/useFreeBusy.ts +++ b/src/components/Attendees/useFreeBusy.ts @@ -1,6 +1,8 @@ -import { getFreeBusyForAddedAttendeesREPORT } from '@/features/Events/api/getFreeBusyForAddedAttendeesREPORT' -import { getFreeBusyForEventAttendeesPOST } from '@/features/Events/api/getFreeBusyForEventAttendeesPOST' -import { getUserDataFromEmail } from '@/features/Events/api/getUserDataFromEmail' +import { + getFreeBusyForAddedAttendee, + getFreeBusyForEventAttendees +} from '@/features/Events/FreeBusyApi' +import { getUserDataFromEmail } from '@/features/User/userAPI' import moment from 'moment-timezone' import { useEffect, useRef, useState } from 'react' @@ -150,7 +152,7 @@ export function useAttendeesFreeBusy({ setStatusMap(prev => ({ ...prev, ...toLoadingMap(existingAttendees) })) fetchFreeBusyMap(existingAttendees, resolved => - getFreeBusyForEventAttendeesPOST( + getFreeBusyForEventAttendees( resolved.map(r => r.userId), toUtcIcal(start, timezone), toUtcIcal(end, timezone), @@ -208,7 +210,7 @@ export function useAttendeesFreeBusy({ Promise.all( resolved.map(async ({ email, userId }) => { try { - const busy = await getFreeBusyForAddedAttendeesREPORT( + const busy = await getFreeBusyForAddedAttendee( userId, moment.tz(start, timezone).utc().format('YYYYMMDDTHHmmss'), moment.tz(end, timezone).utc().format('YYYYMMDDTHHmmss') diff --git a/src/features/Events/AttendanceValidation/EventCounterModal.tsx b/src/features/Events/AttendanceValidation/EventCounterModal.tsx index c8a5ab2..57c8b7e 100644 --- a/src/features/Events/AttendanceValidation/EventCounterModal.tsx +++ b/src/features/Events/AttendanceValidation/EventCounterModal.tsx @@ -8,7 +8,7 @@ import { SnackbarAlert } from '@/components/Loading/SnackBarAlert' import moment from 'moment-timezone' import { useEffect, useState } from 'react' import { useI18n } from 'twake-i18n' -import { postCounterProposal } from '../api/sendCounterProposal' +import { postCounterProposal } from '../EventApi' import { EventTimeSubtitle } from '../EventPreview/EventTimeSubtitle' import { ContextualizedEvent } from '../EventsTypes' diff --git a/src/features/Events/EventApi.ts b/src/features/Events/EventApi.ts index 1aae0a4..5a88e14 100644 --- a/src/features/Events/EventApi.ts +++ b/src/features/Events/EventApi.ts @@ -1,4 +1,4 @@ -import { api } from '@/utils/apiUtils' +import { extractEventBaseUuid } from '@/utils/extractEventBaseUuid' import { convertEventDateTimeToISO, resolveTimezoneId } from '@/utils/timezone' import { TIMEZONES } from '@/utils/timezone-data' import ICAL from 'ical.js' @@ -10,6 +10,19 @@ import { VObjectValue } from '../Calendars/types/CalendarData' import { SearchEventsResponse } from '../Search/types/SearchEventsResponse' +import { + CounterProposalPayload, + deleteEventRaw, + fetchAllRecurrentVevents, + fetchEventIcs, + fetchEventRaw, + importEventRaw, + moveEventRaw, + postCounterProposalRaw, + putEventRaw, + reportEventRaw, + searchEventRaw +} from './EventDao' import { CalendarEvent } from './EventsTypes' import { calendarEventToJCal, @@ -22,36 +35,30 @@ export async function reportEvent( event: CalendarEvent, match: { start: string; end: string } ): Promise { - const response = await api(`dav${event.URL}`, { - method: 'REPORT', - body: JSON.stringify({ match }), - headers: { Accept: 'application/json' } - }) - if (!response.ok) { - throw new Error(`REPORT request failed with status ${response.status}`) - } - const eventData: CalDavItem = await response.json() - return eventData + return reportEventRaw(event, match) } -export async function getEvent(event: CalendarEvent, isMaster?: boolean) { - const response = await api.get(`dav${event.URL}`) - const eventData = await response.text() +export async function getEvent( + event: CalendarEvent, + isMaster?: boolean +): Promise { + const eventData = await fetchEventRaw(event) - const eventical = ICAL.parse(eventData) - const vevents = (eventical[2] || []).filter( - ([name]: [string]) => name.toLowerCase() === 'vevent' + const eventical = ICAL.parse(eventData) as VCalComponent + const vevents = (eventical[2] ?? []).filter( + ([name]) => name.toLowerCase() === 'vevent' + ) + const vtimezones = (eventical[2] ?? []).filter( + ([name]) => name.toLowerCase() === 'vtimezone' ) - const vtimezones = (eventical[2] || []).filter( - ([name]: [string]) => name.toLowerCase() === 'vtimezone' - ) - - let targetVevent + let targetVevent: VCalComponent | undefined if (isMaster) { targetVevent = vevents.find( - ([, props]: VCalComponent) => - !props.find(([k]) => k.toLowerCase() === 'recurrence-id') + ([, props]) => + !(props as VObjectProperty[]).find( + ([k]) => k.toLowerCase() === 'recurrence-id' + ) ) if (!targetVevent) { targetVevent = vevents[0] @@ -63,11 +70,11 @@ export async function getEvent(event: CalendarEvent, isMaster?: boolean) { let timezoneFromVTimezone: string | undefined if (vtimezones.length > 0) { const vtimezone = vtimezones[0] - const tzidProp = vtimezone[1]?.find( - ([k]: string[]) => k.toLowerCase() === 'tzid' + const tzidProp = (vtimezone[1] as VObjectProperty[]).find( + ([k]) => k.toLowerCase() === 'tzid' ) - if (tzidProp && tzidProp[3]) { - const resolvedTz = resolveTimezoneId(tzidProp[3]) + if (tzidProp?.[3]) { + const resolvedTz = resolveTimezoneId(tzidProp[3] as string) if (resolvedTz) { timezoneFromVTimezone = resolvedTz } @@ -75,47 +82,41 @@ export async function getEvent(event: CalendarEvent, isMaster?: boolean) { } let timezoneFromDTSTART: string | undefined - const dtstartProp = targetVevent[1]?.find( - ([k]: string[]) => k.toLowerCase() === 'dtstart' + const dtstartProp = (targetVevent[1] as VObjectProperty[]).find( + ([k]) => k.toLowerCase() === 'dtstart' ) - if (dtstartProp) { - const dtstartParams = dtstartProp[1] - const dtstartValue = dtstartProp[3] - if (dtstartParams) { - const tzParam = - dtstartParams.tzid || - dtstartParams.TZID || - dtstartParams.Tzid || - dtstartParams.tZid || - dtstartParams.tzId - if (tzParam) { - const resolvedTz = resolveTimezoneId(tzParam) - if (resolvedTz) { - timezoneFromDTSTART = resolvedTz - } - } - } - if ( - !timezoneFromDTSTART && - typeof dtstartValue === 'string' && - dtstartValue.endsWith('Z') - ) { - timezoneFromDTSTART = 'Etc/UTC' - } + const dtstartParams = dtstartProp?.[1] as Record + const dtstartValue = dtstartProp?.[3] + const tzParam = + dtstartParams?.['tzid'] ?? + dtstartParams?.['TZID'] ?? + dtstartParams?.['Tzid'] ?? + dtstartParams?.['tZid'] ?? + dtstartParams?.['tzId'] + + timezoneFromDTSTART = resolveTimezoneId(tzParam) + + if ( + !timezoneFromDTSTART && + typeof dtstartValue === 'string' && + dtstartValue.endsWith('Z') + ) { + timezoneFromDTSTART = 'Etc/UTC' } const eventjson = parseCalendarEvent( - targetVevent[1], + targetVevent[1] as VObjectProperty[], event.color ?? {}, { id: event?.calId } as Calendar, event.URL ) const finalTimezone = - timezoneFromVTimezone || - timezoneFromDTSTART || - eventjson.timezone || + timezoneFromVTimezone ?? + timezoneFromDTSTART ?? + eventjson.timezone ?? 'Etc/UTC' + eventjson.timezone = finalTimezone if (!eventjson.allday && eventjson.start && finalTimezone) { @@ -132,31 +133,22 @@ export async function getEvent(event: CalendarEvent, isMaster?: boolean) { } } - if (isMaster) { - const merged = { ...event, ...eventjson } - merged.timezone = finalTimezone - return merged - } const merged = { ...event, ...eventjson } merged.timezone = finalTimezone return merged } -export async function dlEvent(event: CalendarEvent) { - const response = await api.get(`dav${event.URL}?export=`) - const eventData = await response.text() - - return eventData +export async function dlEvent(event: CalendarEvent): Promise { + const response = await fetchEventIcs(event) + return response } -export async function putEvent(event: CalendarEvent, calOwnerEmail?: string) { - const response = await api(`dav${event.URL}`, { - method: 'PUT', - body: JSON.stringify(calendarEventToJCal(event, calOwnerEmail)), - headers: { - 'content-type': 'text/calendar; charset=utf-8' - } - }) +export async function putEvent( + event: CalendarEvent, + calOwnerEmail?: string +): Promise { + const jCal = calendarEventToJCal(event, calOwnerEmail) + const response = await putEventRaw(event, jCal) if (response.status === 201) { console.info('Event created successfully:', response.url || event.URL) @@ -168,8 +160,8 @@ export async function putEvent(event: CalendarEvent, calOwnerEmail?: string) { export async function putEventWithOverrides( updatedEvent: CalendarEvent, calOwnerEmail?: string -) { - const vevents = await getAllRecurrentEvent(updatedEvent) +): Promise { + const vevents = await fetchAllRecurrentVevents(updatedEvent) const updatedVevent = makeVevent( updatedEvent, @@ -180,61 +172,64 @@ export async function putEventWithOverrides( let replaced = false for (let i = 0; i < vevents.length; i++) { const ve = vevents[i] - const recurrenceId = ve[1].find(([k]: string[]) => k === 'recurrence-id') + const recurrenceId = (ve[1] as VObjectProperty[]).find( + ([k]) => k === 'recurrence-id' + ) if (recurrenceId && recurrenceId[3] === updatedEvent.recurrenceId) { - vevents[i] = updatedVevent // replace + vevents[i] = updatedVevent as VCalComponent // replace replaced = true break } } if (!replaced && updatedEvent.recurrenceId) { - vevents.push(updatedVevent) // add new override + vevents.push(updatedVevent as VCalComponent) // add new override } const timezoneData = TIMEZONES.zones[updatedEvent.timezone] const vtimezone = makeTimezone(timezoneData, updatedEvent) const newJCal = ['vcalendar', [], [...vevents, vtimezone.component.jCal]] - - return api(`dav${updatedEvent.URL}`, { - method: 'PUT', - body: JSON.stringify(newJCal), - headers: { - 'content-type': 'text/calendar; charset=utf-8' - } - }) + return putEventRaw(updatedEvent, newJCal) } -export const deleteEventInstance = async (event: CalendarEvent) => { +export const deleteEventInstance = async ( + event: CalendarEvent +): Promise => { // Get all VEVENTs (master + overrides) from the series - const vevents = await getAllRecurrentEvent(event) + const vevents = await fetchAllRecurrentVevents(event) // Find the master VEVENT const masterIndex = vevents.findIndex( - ([, props]: VCalComponent) => - !props.find(([k]) => k.toLowerCase() === 'recurrence-id') + ([, props]) => + !(props as VObjectProperty[]).find( + ([k]) => k.toLowerCase() === 'recurrence-id' + ) ) if (masterIndex === -1) { throw new Error('No master VEVENT found for this series') } - const exdateValue = event.recurrenceId || event.start + const exdateValue = event.recurrenceId ?? event.start const seriesEvent = parseCalendarEvent( - vevents[masterIndex][1], + vevents[masterIndex][1] as VObjectProperty[], {}, { id: event.calId } as Calendar, '' ) - const masterProps = vevents[masterIndex][1] + const masterProps = vevents[masterIndex][1] as VObjectProperty[] + + const normalizeRecurrenceId = (id: VObjectValue): string => + (typeof id === 'string' || typeof id === 'number' + ? String(id) + : '' + ).replace(/Z$/, '') - // Check if this date is already in EXDATE (avoid duplicates) - const normalizeRecurrenceId = (id: VObjectValue) => - String(id ?? '').replace(/Z$/, '') const isDuplicate = masterProps.some((prop: VObjectProperty) => { if (prop[0].toLowerCase() === 'exdate' && prop[3]) { return ( - normalizeRecurrenceId(prop[3]) === normalizeRecurrenceId(exdateValue) + normalizeRecurrenceId(prop[3]) === + normalizeRecurrenceId(exdateValue as VObjectValue) ) } return false @@ -250,14 +245,14 @@ export const deleteEventInstance = async (event: CalendarEvent) => { vevents[masterIndex][1] = masterProps // Remove the override instance if it exists (in case it was an override being deleted) - const filteredVevents = vevents.filter(([, props]: VCalComponent) => { - const recurrenceIdProp = props.find( + const filteredVevents = vevents.filter(([, props]) => { + const recurrenceIdProp = (props as VObjectProperty[]).find( ([k]) => k.toLowerCase() === 'recurrence-id' ) if (!recurrenceIdProp) return true // Keep master return ( normalizeRecurrenceId(recurrenceIdProp[3]) !== - normalizeRecurrenceId(event.recurrenceId ?? '') + normalizeRecurrenceId((event.recurrenceId ?? '') as VObjectValue) ) // Remove matching override }) @@ -271,39 +266,33 @@ export const deleteEventInstance = async (event: CalendarEvent) => { [...filteredVevents, vtimezone.component.jCal] ] - return api(`dav${event.URL}`, { - method: 'PUT', - body: JSON.stringify(newJCal), - headers: { - 'content-type': 'text/calendar; charset=utf-8' - } - }) + return putEventRaw(event, newJCal) } export const updateSeriesPartstat = async ( event: CalendarEvent, attendeeEmail: string, partstat: string -) => { - const vevents = await getAllRecurrentEvent(event) +): Promise => { + const vevents = await fetchAllRecurrentVevents(event) // Update PARTSTAT in ALL VEVENTs (master + exceptions) const updatedVevents = vevents.map((vevent: VCalComponent) => { - const properties = vevent[1] + const properties = vevent[1] as VObjectProperty[] const updatedProperties = properties.map((prop: VObjectProperty) => { - // Find ATTENDEE properties - if (prop[0] === 'attendee') { - const calAddress = prop[3] as string - // Check if this is the target attendee - if (calAddress.toLowerCase().includes(attendeeEmail.toLowerCase())) { - // Update PARTSTAT parameter - const params = { ...prop[1], partstat: partstat } - return [prop[0], params, prop[2], prop[3]] - } + const calAddress = prop[3] as string + // Find ATTENDEE properties & Check if this is the target attendee + if ( + prop[0] === 'attendee' && + calAddress.toLowerCase().includes(attendeeEmail.toLowerCase()) + ) { + // Update PARTSTAT parameter + const params = { ...(prop[1] as Record), partstat } + return [prop[0], params, prop[2], prop[3]] as VObjectProperty } return prop }) - return [vevent[0], updatedProperties, vevent[2]] + return [vevent[0], updatedProperties, vevent[2]] as VCalComponent }) const timezoneData = TIMEZONES.zones[event.timezone] @@ -315,45 +304,31 @@ export const updateSeriesPartstat = async ( [...updatedVevents, vtimezone.component.jCal] ] - return api(`dav${event.URL}`, { - method: 'PUT', - body: JSON.stringify(newJCal), - headers: { - 'content-type': 'text/calendar; charset=utf-8' - } - }) + return putEventRaw(event, newJCal) } -export async function getAllRecurrentEvent(event: CalendarEvent) { - const response = await api.get(`dav${event.URL}`) - const eventData = await response.text() - const jcal = ICAL.parse(eventData) - const vevents = jcal[2].filter(([name]: string[]) => name === 'vevent') - return vevents +export async function getAllRecurrentEvent( + event: CalendarEvent +): Promise { + return fetchAllRecurrentVevents(event) } -export async function moveEvent(event: CalendarEvent, newUrl: string) { - const response = await api(`dav${event.URL}`, { - method: 'MOVE', - headers: { - destination: newUrl - } - }) - return response +export async function moveEvent( + event: CalendarEvent, + newUrl: string +): Promise { + return moveEventRaw(event, newUrl) } -export async function deleteEvent(eventURL: string) { - const response = await api(`dav${eventURL}`, { - method: 'DELETE' - }) - return response +export async function deleteEvent(eventURL: string): Promise { + return deleteEventRaw({ URL: eventURL } as CalendarEvent) } -export async function importEventFromFile(id: string, calLink: string) { - const response = await api.post(`api/import`, { - body: JSON.stringify({ fileId: id, target: calLink }) - }) - return response +export async function importEventFromFile( + id: string, + calLink: string +): Promise { + return importEventRaw(id, calLink) } export async function searchEvent( @@ -385,11 +360,65 @@ export async function searchEvent( if (attendees.length) { reqParam.attendees = attendees } - const response = await api - .post('calendar/api/events/search?limit=30&offset=0', { - body: JSON.stringify(reqParam) - }) - .json() - - return response as SearchEventsResponse + return searchEventRaw(reqParam) +} + +export async function postCounterProposal({ + event, + senderEmail, + recipientEmail, + proposedStart, + proposedEnd, + message +}: { + event: CalendarEvent + senderEmail: string + recipientEmail: string + proposedStart: string + proposedEnd: string + message?: string +}): Promise { + // Build the counter event with proposed dates + const counterEvent: CalendarEvent = { + ...event, + start: proposedStart, + end: proposedEnd, + sequence: event.sequence ?? 0 + } + // Build vevent jCal + const vevent = makeVevent( + counterEvent, + counterEvent.timezone, + senderEmail, + !event.recurrenceId + ) + if (message) { + vevent[1].push(['comment', {}, 'text', message]) + } + // Build vtimezone + const timezoneData = TIMEZONES.zones[counterEvent.timezone] + const vtimezone = makeTimezone(timezoneData, counterEvent) + + // Assemble full vcalendar with METHOD:COUNTER + const jcal = [ + 'vcalendar', + [ + ['version', {}, 'text', '2.0'], + ['prodid', {}, 'text', '-//OpenPaaS//OpenPaaS//EN'], + ['method', {}, 'text', 'COUNTER'] + ], + [vevent, vtimezone.component.jCal] + ] + + // Serialize to raw ICS + const counterICS = new ICAL.Component(jcal).toString() + const payload: CounterProposalPayload = { + ical: counterICS, + sender: senderEmail, + recipient: recipientEmail, + uid: extractEventBaseUuid(event.uid), + sequence: counterEvent.sequence ?? 0, + method: 'COUNTER' + } + return postCounterProposalRaw(event, payload) } diff --git a/src/features/Events/EventDao.ts b/src/features/Events/EventDao.ts new file mode 100644 index 0000000..77110ce --- /dev/null +++ b/src/features/Events/EventDao.ts @@ -0,0 +1,123 @@ +import { api } from '@/utils/apiUtils' +import ICAL from 'ical.js' +import { CalDavItem } from '../Calendars/api/types' +import { VCalComponent } from '../Calendars/types/CalendarData' +import { SearchEventsResponse } from '../Search/types/SearchEventsResponse' +import { CalendarEvent } from './EventsTypes' + +export async function reportEventRaw( + event: CalendarEvent, + match: { start: string; end: string } +): Promise { + const response = await api(`dav${event.URL}`, { + method: 'REPORT', + body: JSON.stringify({ match }), + headers: { Accept: 'application/json' } + }) + if (!response.ok) { + throw new Error(`REPORT request failed with status ${response.status}`) + } + return response.json() +} + +export async function fetchEventRaw(event: CalendarEvent): Promise { + const response = await api.get(`dav${event.URL}`) + return response.text() +} + +export async function fetchEventIcs(event: CalendarEvent): Promise { + const response = await api.get(`dav${event.URL}?export=`) + return response.text() +} + +export async function putEventRaw( + event: CalendarEvent, + jCal: unknown[] +): Promise { + return api(`dav${event.URL}`, { + method: 'PUT', + body: JSON.stringify(jCal), + headers: { 'content-type': 'text/calendar; charset=utf-8' } + }) +} + +export async function deleteEventRaw(event: CalendarEvent): Promise { + return api(`dav${event.URL}`, { method: 'DELETE' }) +} + +export async function moveEventRaw( + event: CalendarEvent, + toURL: string +): Promise { + return api(`dav${event.URL}`, { + method: 'MOVE', + headers: { destination: toURL } + }) +} + +export async function importEventRaw( + id: string, + calLink: string +): Promise { + return api.post('api/import', { + body: JSON.stringify({ fileId: id, target: calLink }) + }) +} + +export async function searchEventRaw(reqParam: { + query: string + calendars: { calendarId: string; userId: string }[] + organizers?: string[] + attendees?: string[] +}): Promise { + return api + .post('calendar/api/events/search?limit=30&offset=0', { + body: JSON.stringify(reqParam) + }) + .json() +} + +/** + * Fetches all VEVENTs (master + overrides) for a recurring series. + * Returns the raw jCal VEVENT components — no transformation applied. + */ +export async function fetchAllRecurrentVevents( + event: CalendarEvent +): Promise { + const response = await api.get(`dav${event.URL}`) + const eventData = await response.text() + const jcal = ICAL.parse(eventData) as VCalComponent + return (jcal[2] ?? []).filter(([name]) => name === 'vevent') +} + +/** + * POSTs an iTIP COUNTER proposal for a calendar event. + * Accepts a pre-serialized ICS string and the envelope metadata. + */ +export interface CounterProposalPayload { + ical: string + sender: string + recipient: string + uid: string + sequence: number + method: 'COUNTER' +} +export async function postCounterProposalRaw( + event: CalendarEvent, + payload: CounterProposalPayload +): Promise { + const response = await api(`dav${event.URL}`, { + method: 'POST', + body: JSON.stringify(payload), + headers: { + accept: 'application/json,text/plain,*/*', + 'content-type': 'application/calendar+json', + Prefer: 'return=representation', + 'X-Http-Method-Override': 'ITIP' + } + }) + if (!response.ok) { + throw new Error(`postCounterProposal failed with status ${response.status}`) + } + return response +} diff --git a/src/features/Events/FreeBusyApi.ts b/src/features/Events/FreeBusyApi.ts new file mode 100644 index 0000000..f7f2355 --- /dev/null +++ b/src/features/Events/FreeBusyApi.ts @@ -0,0 +1,66 @@ +import { hasFreeBusyConflict } from '@/components/Attendees/useFreeBusy' +import { extractCalendarHrefs } from '@/utils/extractCalendarHrefs' +import { extractEventBaseUuid } from '@/utils/extractEventBaseUuid' +import { getCalendars } from '../Calendars/CalendarApi' +import { + fetchFreeBusyPost, + fetchFreeBusyReports, + FreeBusyPostQuery +} from './FreeBusyDao' + +export async function getFreeBusyForAddedAttendee( + userId: string, + start: string, + end: string +): Promise { + const calendars = await getCalendars( + userId, + 'withFreeBusy=true&withRights=true' + ) + const hrefs = extractCalendarHrefs(calendars) + if (hrefs.length === 0) return false + + const results = await fetchFreeBusyReports({ hrefs, start, end }) + return results.some(data => (data ? hasFreeBusyConflict(data) : false)) +} + +interface BusySlot { + uid: string + start: string + end: string +} + +interface CalendarFreeBusy { + id: string + busy: BusySlot[] +} + +interface UserFreeBusy { + id: string + calendars: CalendarFreeBusy[] +} +interface FreeBusyPayload { + users?: UserFreeBusy[] +} + +export async function getFreeBusyForEventAttendees( + userIds: string[], + start: string, + end: string, + eventUid: string +): Promise> { + const query: FreeBusyPostQuery = { userIds, start, end, eventUid } + const payload = (await fetchFreeBusyPost(query)) as FreeBusyPayload + const users = Array.isArray(payload.users) ? payload.users : [] + const eventUidBase = extractEventBaseUuid(eventUid) + return Object.fromEntries( + users.map(u => { + const isBusy = (u.calendars ?? []).some(cal => + (cal.busy ?? []).some( + slot => extractEventBaseUuid(slot.uid) !== eventUidBase + ) + ) + return [u.id, isBusy] + }) + ) +} diff --git a/src/features/Events/FreeBusyDao.ts b/src/features/Events/FreeBusyDao.ts new file mode 100644 index 0000000..a7f979d --- /dev/null +++ b/src/features/Events/FreeBusyDao.ts @@ -0,0 +1,59 @@ +import { api } from '@/utils/apiUtils' + +export interface FreeBusyQuery { + hrefs: string[] + start: string + end: string +} +/** + * Fires REPORT requests against each calendar href to collect raw free/busy + * data. Returns an array of parsed JSON payloads (null on failure). + * It's for getting the free busy for added attendees in an event (new or while modifying) + */ +export async function fetchFreeBusyReports( + query: FreeBusyQuery +): Promise { + const body = JSON.stringify({ + type: 'free-busy-query', + match: { start: query.start, end: query.end } + }) + return Promise.all( + query.hrefs.map(href => + api(`dav${href}`, { + method: 'REPORT', + headers: { Accept: 'application/json, text/plain, */*' }, + body + }) + .then(r => (r.ok ? r.json() : null)) + .catch(() => null) + ) + ) +} + +export interface FreeBusyPostQuery { + userIds: string[] + start: string + end: string + eventUid: string +} +/** + * POSTs to the freebusy endpoint and returns the raw response payload. + * Throws on non-OK status. + * It's for getting the free busy for present attendees while editing event + */ +export async function fetchFreeBusyPost( + query: FreeBusyPostQuery +): Promise { + const r = await api('dav/calendars/freebusy', { + method: 'POST', + headers: { Accept: 'application/json, text/plain, */*' }, + body: JSON.stringify({ + start: query.start, + end: query.end, + users: query.userIds, + uids: [query.eventUid] + }) + }) + if (!r.ok) throw new Error(`HTTP ${r.status}`) + return r.json() +} diff --git a/src/features/Events/api/getFreeBusyForAddedAttendeesREPORT.ts b/src/features/Events/api/getFreeBusyForAddedAttendeesREPORT.ts deleted file mode 100644 index 35209d5..0000000 --- a/src/features/Events/api/getFreeBusyForAddedAttendeesREPORT.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { getCalendars } from '@/features/Calendars/CalendarApi' - -import { api } from '@/utils/apiUtils' -import { extractCalendarHrefs } from '@/utils/extractCalendarHrefs' -import { hasFreeBusyConflict } from '../../../components/Attendees/useFreeBusy' - -export async function getFreeBusyForAddedAttendeesREPORT( - userId: string, - start: string, - end: string -): Promise { - const calendars = await getCalendars( - userId, - 'withFreeBusy=true&withRights=true' - ) - const hrefs = extractCalendarHrefs(calendars) - if (hrefs.length === 0) return false - - const freeBusyBody = JSON.stringify({ - type: 'free-busy-query', - match: { start, end } - }) - - const results = await Promise.all( - hrefs.map(href => - api(`dav${href}`, { - method: 'REPORT', - headers: { Accept: 'application/json, text/plain, */*' }, - body: freeBusyBody - }) - .then(r => (r.ok ? r.json() : null)) - .then(data => (data ? hasFreeBusyConflict(data) : false)) - .catch(() => false) - ) - ) - return results.some(Boolean) -} diff --git a/src/features/Events/api/getFreeBusyForEventAttendeesPOST.ts b/src/features/Events/api/getFreeBusyForEventAttendeesPOST.ts deleted file mode 100644 index dc7d2b2..0000000 --- a/src/features/Events/api/getFreeBusyForEventAttendeesPOST.ts +++ /dev/null @@ -1,48 +0,0 @@ -import { api } from '@/utils/apiUtils' -import { extractEventBaseUuid } from '@/utils/extractEventBaseUuid' - -interface BusySlot { - uid: string - start: string - end: string -} - -interface CalendarFreeBusy { - id: string - busy: BusySlot[] -} - -interface UserFreeBusy { - id: string - calendars: CalendarFreeBusy[] -} - -export async function getFreeBusyForEventAttendeesPOST( - userIds: string[], - start: string, - end: string, - eventUid: string -): Promise> { - const r = await api('dav/calendars/freebusy', { - method: 'POST', - headers: { Accept: 'application/json, text/plain, */*' }, - body: JSON.stringify({ start, end, users: userIds, uids: [eventUid] }) - }) - if (!r.ok) throw new Error(`HTTP ${r.status}`) - - const payload: unknown = await r.json() - const users = Array.isArray((payload as { users?: unknown })?.users) - ? ((payload as { users: UserFreeBusy[] }).users ?? []) - : [] - - const eventUidBase = extractEventBaseUuid(eventUid) - - return Object.fromEntries( - users.map(u => { - const isBusy = u.calendars.some(cal => - cal.busy.some(slot => extractEventBaseUuid(slot.uid) !== eventUidBase) - ) - return [u.id, isBusy] - }) - ) -} diff --git a/src/features/Events/api/getUserDataFromEmail.ts b/src/features/Events/api/getUserDataFromEmail.ts deleted file mode 100644 index 1d0f21c..0000000 --- a/src/features/Events/api/getUserDataFromEmail.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { api } from '@/utils/apiUtils' -import { isValidEmail } from '@/utils/isValidEmail' - -export async function getUserDataFromEmail(email: string) { - if (!isValidEmail(email)) { - return [] - } - const r = await api(`api/users?email=${encodeURIComponent(email)}`) - const result: Array> = await r.json() - return result -} diff --git a/src/features/Events/api/sendCounterProposal.ts b/src/features/Events/api/sendCounterProposal.ts deleted file mode 100644 index 8b3f579..0000000 --- a/src/features/Events/api/sendCounterProposal.ts +++ /dev/null @@ -1,84 +0,0 @@ -import { api } from '@/utils/apiUtils' -import { extractEventBaseUuid } from '@/utils/extractEventBaseUuid' -import { TIMEZONES } from '@/utils/timezone-data' -import ICAL from 'ical.js' -import { CalendarEvent } from '../EventsTypes' -import { makeTimezone, makeVevent } from '../utils' - -export async function postCounterProposal({ - event, - senderEmail, - recipientEmail, - proposedStart, - proposedEnd, - message -}: { - event: CalendarEvent - senderEmail: string - recipientEmail: string - proposedStart: string - proposedEnd: string - message?: string -}): Promise { - // Build the counter event with proposed dates - const counterEvent: CalendarEvent = { - ...event, - start: proposedStart, - end: proposedEnd, - sequence: event.sequence ?? 0 - } - - // Build vevent jCal - const vevent = makeVevent( - counterEvent, - counterEvent.timezone, - senderEmail, - !event.recurrenceId - ) - if (message) { - vevent?.[1]?.push(['comment', {}, 'text', message]) - } - // Build vtimezone - const timezoneData = TIMEZONES.zones[counterEvent.timezone] - const vtimezone = makeTimezone(timezoneData, counterEvent) - - // Assemble full vcalendar with METHOD:COUNTER - const jcal = [ - 'vcalendar', - [ - ['version', {}, 'text', '2.0'], - ['prodid', {}, 'text', '-//OpenPaaS//OpenPaaS//EN'], - ['method', {}, 'text', 'COUNTER'] - ], - [vevent, vtimezone.component.jCal] - ] - - // Serialize to raw ICS - const counterICS = new ICAL.Component(jcal).toString() - - const postResponse = await api(`dav${event.URL}`, { - method: 'POST', - body: JSON.stringify({ - ical: counterICS, - sender: senderEmail, - recipient: recipientEmail, - uid: extractEventBaseUuid(event.uid), - sequence: counterEvent.sequence, - method: 'COUNTER' - }), - headers: { - accept: 'application/json,text/plain,*/*', - 'content-type': 'application/calendar+json', - Prefer: 'return=representation', - 'X-Http-Method-Override': 'ITIP' - } - }) - - if (!postResponse.ok) { - throw new Error( - `postCounterProposal failed with status ${postResponse.status}` - ) - } - - return postResponse -} diff --git a/src/features/Events/api/updateSeries.ts b/src/features/Events/api/updateSeries.ts index f7c8f4d..4dc59dc 100644 --- a/src/features/Events/api/updateSeries.ts +++ b/src/features/Events/api/updateSeries.ts @@ -1,10 +1,9 @@ -import { api } from '@/utils/apiUtils' import { TIMEZONES } from '@/utils/timezone-data' import { VCalComponent, VObjectProperty } from '../../Calendars/types/CalendarData' -import { getAllRecurrentEvent } from '../EventApi' +import { fetchAllRecurrentVevents, putEventRaw } from '../EventDao' import { CalendarEvent } from '../EventsTypes' import { makeTimezone, makeVevent } from '../utils' @@ -20,26 +19,22 @@ const METADATA_FIELDS = [ ] as const // Helper function to get field values from props -const getFieldValues = (props: VObjectProperty[], fieldName: string) => { - return props.filter(([k]) => k.toLowerCase() === fieldName.toLowerCase()) -} +const getFieldValues = (props: VObjectProperty[], fieldName: string) => + props.filter(([k]) => k.toLowerCase() === fieldName.toLowerCase()) // Helper function to find a single field value from props -const findFieldValue = (props: VObjectProperty[], fieldName: string) => { - return props.find(([k]) => k.toLowerCase() === fieldName.toLowerCase()) -} +const findFieldValue = (props: VObjectProperty[], fieldName: string) => + props.find(([k]) => k.toLowerCase() === fieldName.toLowerCase()) // Helper function to serialize for comparison -const serialize = (values: VObjectProperty[] | VCalComponent[]) => { - return JSON.stringify(values) -} +const serialize = (values: VObjectProperty[] | VCalComponent[]) => + JSON.stringify(values) // Helper function to filter components by name -const filterComponentsByName = (components: VCalComponent[], name: string) => { - return components.filter( +const filterComponentsByName = (components: VCalComponent[], name: string) => + components.filter( ([componentName]) => componentName.toLowerCase() === name.toLowerCase() ) -} // Detect which metadata fields changed between old and new master const detectChangedMetadataFields = ( @@ -125,11 +120,10 @@ const incrementSequenceNumber = (props: VObjectProperty[]) => { const updateValarmComponents = ( components: VCalComponent[], newValarm: VCalComponent[] -) => { - return components +) => + components .filter(([name]) => name.toLowerCase() !== 'valarm') .concat(newValarm) -} // Update a single vevent with metadata changes const updateVeventWithMetadataChanges = ( @@ -156,10 +150,9 @@ const updateVeventWithMetadataChanges = ( } // Handle VALARM component updates - let updatedComponents = components - if (valarmChanged) { - updatedComponents = updateValarmComponents(components, newValarm) - } + const updatedComponents = valarmChanged + ? updateValarmComponents(components, newValarm) + : components return [veventType, newProps, updatedComponents] } @@ -205,7 +198,8 @@ export const updateSeries = async ( calOwnerEmail?: string, removeOverrides: boolean = true ) => { - const vevents = (await getAllRecurrentEvent(event)) as VCalComponent[] + const vevents = await fetchAllRecurrentVevents(event) + const masterIndex = vevents.findIndex( ([, props]) => !findFieldValue(props, 'recurrence-id') ) @@ -249,11 +243,5 @@ export const updateSeries = async ( const newJCal = ['vcalendar', [], [...finalVevents, vtimezone.component.jCal]] - return api(`dav${event.URL}`, { - method: 'PUT', - body: JSON.stringify(newJCal), - headers: { - 'content-type': 'text/calendar; charset=utf-8' - } - }) + return putEventRaw(event, newJCal) } diff --git a/src/features/User/UserDao.ts b/src/features/User/UserDao.ts new file mode 100644 index 0000000..ef15698 --- /dev/null +++ b/src/features/User/UserDao.ts @@ -0,0 +1,14 @@ +import { api } from '@/utils/apiUtils' + +/** + * Fetches user data by email. Returns raw JSON array. + */ +export async function fetchUserByEmail( + email: string +): Promise>> { + const r = await api(`api/users?email=${encodeURIComponent(email)}`) + if (!r.ok) { + return [] + } + return r.json() +} diff --git a/src/features/User/userAPI.ts b/src/features/User/userAPI.ts index 1a282e3..e93ac6a 100644 --- a/src/features/User/userAPI.ts +++ b/src/features/User/userAPI.ts @@ -1,13 +1,15 @@ import { User } from '@/components/Attendees/types' import { api } from '@/utils/apiUtils' +import { isValidEmail } from '@/utils/isValidEmail' import { BusinessHour } from '../Settings/SettingsSlice' import { OpenPaasUserData } from './type/OpenPaasUserData' +import { ResourceData } from './type/ResourceData' +import { fetchUserByEmail } from './UserDao' import { ConfigurationItem, ModuleConfiguration, SearchResponseItem } from './userDataTypes' -import { ResourceData } from './type/ResourceData' export async function getOpenPaasUser() { const user = await api.get(`api/user`) @@ -146,3 +148,10 @@ export async function updateUserConfigurations( json: modules }) } + +export async function getUserDataFromEmail( + email: string +): Promise>> { + if (!isValidEmail(email)) return [] + return fetchUserByEmail(email) +}