From 65621924a28548030e8de1b94c35c23b02e84803 Mon Sep 17 00:00:00 2001 From: Camille Moussu Date: Thu, 3 Jul 2025 14:21:04 +0200 Subject: [PATCH] [#1] getting the calendars from the api --- .env.examble | 1 + src/components/Calendar/Calendar.tsx | 14 ++++- src/features/Calendars/CalendarApi.ts | 34 +++++++++++- src/features/Calendars/CalendarSlice.ts | 57 +++++++++++++++++++- src/features/Calendars/CalendarTypes.ts | 4 +- src/features/Events/EventsTypes.ts | 5 +- src/features/Events/eventUtils.ts | 72 +++++++++++++++++++++++++ src/features/User/LoginCallback.tsx | 5 +- src/features/User/userAPI.ts | 12 +++++ src/features/User/userSlice.ts | 7 ++- 10 files changed, 199 insertions(+), 12 deletions(-) create mode 100644 src/features/Events/eventUtils.ts create mode 100644 src/features/User/userAPI.ts diff --git a/.env.examble b/.env.examble index f44fa35..33b1a51 100644 --- a/.env.examble +++ b/.env.examble @@ -6,3 +6,4 @@ REACT_APP_SSO_REDIRECT_URI="https://example.com/callback" REACT_APP_SSO_RESPONSE_TYPE="code" REACT_APP_SSO_CODE_CHALLENGE_METHOD="S256" REACT_APP_SSO_POST_LOGOUT_REDIRECT="http://example.com?logout=1" +REACT_APP_CALENDAR_BASE_URL="https://calendar.example.com" \ No newline at end of file diff --git a/src/components/Calendar/Calendar.tsx b/src/components/Calendar/Calendar.tsx index 1cbf940..dee3dc1 100644 --- a/src/components/Calendar/Calendar.tsx +++ b/src/components/Calendar/Calendar.tsx @@ -7,19 +7,29 @@ import { CalendarApi, DateSelectArg } from "@fullcalendar/core"; import ReactCalendar from "react-calendar"; import "./Calendar.css"; import { useRef, useState } from "react"; -import { useAppSelector } from "../../app/hooks"; +import { useAppDispatch, useAppSelector } from "../../app/hooks"; import EventPopover from "../../features/Events/EventModal"; import CalendarPopover from "../../features/Calendars/CalendarModal"; import { CalendarEvent } from "../../features/Events/EventsTypes"; +import { + getCalendar, + getCalendars, +} from "../../features/Calendars/CalendarApi"; +import { access } from "node:fs"; +import getOpenPaasUserId from "../../features/User/userAPI"; +import { getCalendarsAsync } from "../../features/Calendars/CalendarSlice"; +import { Calendars } from "../../features/Calendars/CalendarTypes"; +import { parseCalendarEvent } from "../../features/Events/eventUtils"; export default function CalendarApp() { const calendarRef = useRef(null); const [selectedDate, setSelectedDate] = useState(new Date()); + const tokens = useAppSelector((state) => state.user.tokens); const calendars = useAppSelector((state) => state.calendars); const [selectedCalendars, setSelectedCalendars] = useState( Object.keys(calendars).map((id) => id) ); - + const dispatch = useAppDispatch(); let filteredEvents: CalendarEvent[] = []; selectedCalendars.forEach((id) => { filteredEvents = filteredEvents.concat(calendars[id].events); diff --git a/src/features/Calendars/CalendarApi.ts b/src/features/Calendars/CalendarApi.ts index ddcc25d..10b2c40 100644 --- a/src/features/Calendars/CalendarApi.ts +++ b/src/features/Calendars/CalendarApi.ts @@ -1,3 +1,33 @@ -export default async function getCalendars() { - return {} +import { cp } from "node:fs"; + +export async function getCalendars(userId: string, opaque_token: string) { + const response = await fetch( + `${process.env.REACT_APP_CALENDAR_BASE_URL}/dav/calendars/${userId}.json?personal=true&sharedDelegationStatus=accepted&sharedPublicSubscription=true&withRights=true`, + { + headers: { + Accept: "application/calendar+json", + Authorization: `Bearer ${opaque_token}`, + }, + } + ); + const calendars = await response.json(); + return calendars; +} + +export async function getCalendar(id: string, opaque_token: string) { + const response = await fetch( + `${process.env.REACT_APP_CALENDAR_BASE_URL}/dav/calendars/${id}/${id}.json`, + { + method: "REPORT", + headers: { + Accept: "application/json, text/plain, */*", + Authorization: `Bearer ${opaque_token}`, + }, + body: JSON.stringify({ + match: { start: "20250525T000000", end: "20250708T000000" }, + }), + } + ); + const calendar = await response.json(); + return calendar; } diff --git a/src/features/Calendars/CalendarSlice.ts b/src/features/Calendars/CalendarSlice.ts index 0d9e025..694aef5 100644 --- a/src/features/Calendars/CalendarSlice.ts +++ b/src/features/Calendars/CalendarSlice.ts @@ -1,6 +1,46 @@ -import { createSlice, PayloadAction } from "@reduxjs/toolkit"; +import { createAsyncThunk, createSlice, PayloadAction } from "@reduxjs/toolkit"; import { Calendars } from "./CalendarTypes"; import { CalendarEvent } from "../Events/EventsTypes"; +import { getCalendar, getCalendars } from "./CalendarApi"; +import getOpenPaasUserId from "../User/userAPI"; +import { parseCalendarEvent } from "../Events/eventUtils"; + +export const getCalendarsAsync = createAsyncThunk< + Record, // Return type + string // Arg type (access_token) +>("calendars/getCalendars", async (access_token: string) => { + const importedCalendars: Record = {}; + const user = await getOpenPaasUserId(access_token); + const calendars = await getCalendars(user.id, access_token); + const rawCalendars = calendars._embedded["dav:calendar"]; + + for (const cal of rawCalendars) { + const name = cal["dav:name"]; + const description = cal["dav:description"]; + const id = cal["calendarserver:source"] + ? cal["calendarserver:source"]._links.self.href + .replace("/calendars/", "") + .replace(".json", "") + .split("/")[0] + : cal._links.self.href + .replace("/calendars/", "") + .replace(".json", "") + .split("/")[0]; + + const color = cal["apple:color"]; + const calendarDetails = await getCalendar(id, access_token); + const events = calendarDetails._embedded["dav:item"].map( + (eventdata: any) => { + const datas = eventdata.data[2][0][1]; + return parseCalendarEvent(datas, color); + } + ); + + importedCalendars[id] = { id, name, description, color, events }; + } + + return importedCalendars; +}); const CalendarSlice = createSlice({ name: "calendars", @@ -18,7 +58,7 @@ const CalendarSlice = createSlice({ state, action: PayloadAction<{ calendarUid: string; event: CalendarEvent }> ) => { - console.log(action.payload) + console.log(action.payload); if (!state[action.payload.calendarUid].events) { state[action.payload.calendarUid].events = []; } @@ -37,6 +77,19 @@ const CalendarSlice = createSlice({ }); }, }, + extraReducers: (builder) => { + builder.addCase( + getCalendarsAsync.fulfilled, + (state, action: PayloadAction>) => { + console.log("ony est!!!!: ", action.payload); + console.log(Object.keys(action.payload)); + Object.keys(action.payload).forEach((id) => { + console.log(id); + state[id] = action.payload[id]; + }); + } + ); + }, }); export const { addEvent, removeEvent, createCalendar } = CalendarSlice.actions; diff --git a/src/features/Calendars/CalendarTypes.ts b/src/features/Calendars/CalendarTypes.ts index c9470fc..363460f 100644 --- a/src/features/Calendars/CalendarTypes.ts +++ b/src/features/Calendars/CalendarTypes.ts @@ -3,9 +3,9 @@ import { CalendarEvent } from "../Events/EventsTypes"; export interface Calendars { id: string; name: string; - prodid: string; + prodid?: string; color: string; - description: string; + description?: string; calscale?: string; version?: string; events: CalendarEvent[]; diff --git a/src/features/Events/EventsTypes.ts b/src/features/Events/EventsTypes.ts index 1ee80e5..5bea959 100644 --- a/src/features/Events/EventsTypes.ts +++ b/src/features/Events/EventsTypes.ts @@ -2,7 +2,7 @@ import { userAttendee, userOrganiser } from "../User/userDataTypes"; export interface CalendarEvent { uid: string; - transp: string; + transp?: string; start: Date; // ISO date end?: Date; class?: string; @@ -10,9 +10,10 @@ export interface CalendarEvent { title?: string; description?: string; location?: string; - organizer: userOrganiser; + organizer?: userOrganiser; attendee: userAttendee[]; stamp?: Date; sequence?: Number; color?: string; + allday?:Boolean } diff --git a/src/features/Events/eventUtils.ts b/src/features/Events/eventUtils.ts new file mode 100644 index 0000000..da62947 --- /dev/null +++ b/src/features/Events/eventUtils.ts @@ -0,0 +1,72 @@ +import { userAttendee } from "../User/userDataTypes"; +import { CalendarEvent } from "./EventsTypes"; + +type RawEntry = [string, Record, string, any]; + +export function parseCalendarEvent( + data: RawEntry[], + color: string +): CalendarEvent { + const event: Partial = { color, attendee: [] }; + + for (const [key, params, type, value] of data) { + switch (key.toLowerCase()) { + case "uid": + event.uid = value; + break; + case "transp": + event.transp = value; + break; + case "dtstart": + event.start = value; + break; + case "dtend": + event.end = value; + break; + case "class": + event.class = value; + break; + case "x-openpaas-videoconference": + event.x_openpass_videoconference = value; + break; + case "summary": + event.title = value; + break; + case "description": + event.description = value; + break; + case "location": + event.location = value; + break; + case "organizer": + event.organizer = { + cn: params?.cn || "", + cal_address: value.replace(/^mailto:/, ""), + }; + break; + case "attendee": + (event.attendee as userAttendee[]).push({ + cn: params?.cn || "", + cal_address: value.replace(/^mailto:/, ""), + partstat: params?.partstat || "", + rsvp: params?.rsvp || "", + role: params?.role || "", + cutype: params?.cutype || "", + }); + break; + case "dtstamp": + event.stamp = new Date(value); + break; + case "sequence": + event.sequence = Number(value); + break; + } + } + + if (!event.uid || !event.start) { + console.log(event); + throw new Error(`Missing required event fields`); + } + + return event as CalendarEvent; +} diff --git a/src/features/User/LoginCallback.tsx b/src/features/User/LoginCallback.tsx index e7a47b5..3243212 100644 --- a/src/features/User/LoginCallback.tsx +++ b/src/features/User/LoginCallback.tsx @@ -2,8 +2,9 @@ import { useEffect, useRef } from "react"; import { Callback } from "./oidcAuth"; import { useAppDispatch } from "../../app/hooks"; import { push } from "redux-first-history"; -import { setUserData } from "./userSlice"; +import { setTokens, setUserData } from "./userSlice"; import { Loading } from "../../components/Loading/Loading"; +import { getCalendarsAsync } from "../Calendars/CalendarSlice"; export function CallbackResume() { const dispatch = useAppDispatch(); @@ -21,6 +22,8 @@ export function CallbackResume() { const data = await Callback(saved?.code_verifier, saved?.state); console.log("data:", data); dispatch(setUserData(data?.userinfo)); + dispatch(setTokens(data?.tokenSet)); + dispatch(getCalendarsAsync(data?.tokenSet.access_token || "")); sessionStorage.removeItem("redirectState"); // Redirect to main page after successful callback dispatch(push("/")); diff --git a/src/features/User/userAPI.ts b/src/features/User/userAPI.ts new file mode 100644 index 0000000..9fa59df --- /dev/null +++ b/src/features/User/userAPI.ts @@ -0,0 +1,12 @@ +export default async function getOpenPaasUserId(opaque_token: string) { + const response = await fetch( + `${process.env.REACT_APP_CALENDAR_BASE_URL}/api/user`, + { + headers: { + Authorization: `Bearer ${opaque_token}`, + }, + } + ); + const user = await response.json(); + return user; +} diff --git a/src/features/User/userSlice.ts b/src/features/User/userSlice.ts index a77834b..710e574 100644 --- a/src/features/User/userSlice.ts +++ b/src/features/User/userSlice.ts @@ -6,6 +6,7 @@ export const userSlice = createSlice({ initialState: { userData: null as unknown as userData, organiserData: null as unknown as userOrganiser, + tokens: null as unknown as Record, }, reducers: { setUserData: (state, action) => { @@ -16,10 +17,14 @@ export const userSlice = createSlice({ state.organiserData.cn = action.payload.sub; state.organiserData.cal_address = `mailto:${action.payload.email}`; }, + setTokens: (state, action) => { + console.log(state.tokens); + state.tokens = action.payload; + }, }, }); // Action creators are generated for each case reducer function -export const { setUserData } = userSlice.actions; +export const { setUserData, setTokens } = userSlice.actions; export default userSlice.reducer;