Skip to content

Commit 2da4ff0

Browse files
committed
Finish Timetable Share Functionality
1 parent 33ee4a7 commit 2da4ff0

20 files changed

+838
-74
lines changed

course-matrix/backend/src/controllers/eventsController.ts

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -386,6 +386,46 @@ export default {
386386
}
387387
}),
388388

389+
/**
390+
* Get all events given a user_id and calendar_id
391+
*/
392+
getSharedEvents: asyncHandler(async (req: Request, res: Response) => {
393+
try {
394+
const { user_id, calendar_id } = req.params;
395+
396+
if (!user_id) {
397+
return res.status(400).json({ error: "user_id is required" });
398+
}
399+
if (!calendar_id) {
400+
return res.status(400).json({ error: "calendar_id is required" });
401+
}
402+
403+
const { data: courseEvents, error: courseError } = await supabase
404+
.schema("timetable")
405+
.from("course_events")
406+
.select("*")
407+
.eq("user_id", user_id)
408+
.eq("calendar_id", calendar_id);
409+
410+
const { data: userEvents, error: userError } = await supabase
411+
.schema("timetable")
412+
.from("user_events")
413+
.select("*")
414+
.eq("user_id", user_id)
415+
.eq("calendar_id", calendar_id);
416+
417+
if (courseError || userError) {
418+
return res
419+
.status(400)
420+
.json({ error: courseError?.message || userError?.message });
421+
}
422+
423+
return res.status(200).json({ courseEvents, userEvents });
424+
} catch (error) {
425+
return res.status(500).send({ error });
426+
}
427+
}),
428+
389429
/**
390430
* Update an event
391431
* The request should provide:

course-matrix/backend/src/controllers/sharesController.ts

Lines changed: 61 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -136,27 +136,69 @@ export default {
136136
const { data: shareData, error: sharedError } = await supabase
137137
.schema("timetable")
138138
.from("shared")
139-
.select(
140-
"id, calendar_id, owner_id, shared_id, timetables!inner(id, user_id, timetable_title, semester, favorite)",
141-
)
139+
.select("id, calendar_id, owner_id, shared_id, timetables!inner(*)")
142140
.eq("shared_id", user_id);
143141

144142
if (sharedError) {
145143
return res.status(400).json({ error: sharedError.message });
146144
}
147145

148-
if (!shareData || shareData.length === 0) {
149-
return res
150-
.status(404)
151-
.json({ error: "No shared timetables found for this user" });
152-
}
153-
154146
return res.status(200).json(shareData);
155147
} catch (error) {
156148
return res.status(500).send({ error });
157149
}
158150
}),
159151

152+
/**
153+
* Get all restrictions from a shared timetable id
154+
* @route GET /api/shared/restrictions/:calendar_id
155+
*/
156+
getSharedRestrictions: asyncHandler(
157+
async (req: Request, res: Response) => {
158+
try {
159+
const { user_id, calendar_id } = req.query;
160+
161+
if (!user_id || !calendar_id) {
162+
return res.status(400).json({
163+
error: "User ID and Calendar ID are required",
164+
});
165+
}
166+
167+
//Retrieve users allowed to access the timetable
168+
const { data: timetableData, error: timetableError } = await supabase
169+
.schema("timetable")
170+
.from("timetables")
171+
.select("*")
172+
.eq("id", calendar_id)
173+
.eq("user_id", user_id)
174+
.maybeSingle();
175+
176+
if (timetableError)
177+
return res.status(400).json({ error: timetableError.message });
178+
179+
//Validate timetable validity:
180+
if (!timetableData || timetableData.length === 0) {
181+
return res.status(404).json({ error: "Calendar id not found" });
182+
}
183+
184+
const { data: restrictionData, error: restrictionError } = await supabase
185+
.schema("timetable")
186+
.from("restriction")
187+
.select()
188+
.eq("user_id", user_id)
189+
.eq("calendar_id", calendar_id);
190+
191+
if (restrictionError) {
192+
return res.status(400).json({ error: restrictionError.message });
193+
}
194+
195+
return res.status(200).json(restrictionData);
196+
} catch (error) {
197+
return res.status(500).send({ error });
198+
}
199+
}
200+
),
201+
160202
/**
161203
* Delete all shared record for a timetable as the timetable's owner
162204
* @route DELETE /api/shared/owner/:id?
@@ -387,15 +429,20 @@ export default {
387429
deleteShare: asyncHandler(async (req: Request, res: Response) => {
388430
try {
389431
const shared_id = (req as any).user.id;
390-
const { id } = req.params;
391-
const { calendar_id } = req.body;
432+
const { calendar_id, owner_id } = req.body;
433+
434+
if (!calendar_id || !owner_id) {
435+
return res.status(400).json({
436+
error: "Calendar ID and Owner ID are required",
437+
});
438+
}
392439

393440
const { data: existingTimetable, error: existingTimetableError } =
394441
await supabase
395442
.schema("timetable")
396443
.from("shared")
397444
.select("*")
398-
.eq("id", id)
445+
.eq("owner_id", owner_id)
399446
.eq("calendar_id", calendar_id)
400447
.eq("shared_id", shared_id);
401448

@@ -412,15 +459,15 @@ export default {
412459
.schema("timetable")
413460
.from("shared")
414461
.delete()
415-
.eq("id", id)
462+
.eq("owner_id", owner_id)
416463
.eq("calendar_id", calendar_id)
417464
.eq("shared_id", shared_id);
418465
if (deleteError) {
419466
return res.status(400).json({ error: deleteError.message });
420467
}
421468

422469
return res.status(200).json({
423-
message: `Sharing record: ${id} of calendar: ${calendar_id} deleted successfully`,
470+
message: `Sharing record with owner_id of ${owner_id} and calendar_id of ${calendar_id} deleted successfully`,
424471
});
425472
} catch (error) {
426473
return res.status(500).send({ error });

course-matrix/backend/src/controllers/userController.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -294,3 +294,31 @@ export const updateUsername = asyncHandler(
294294
}
295295
},
296296
);
297+
298+
/**
299+
* @route GET
300+
* @description Gets a user's username from their user ID
301+
*
302+
* This endpoint:
303+
* - Takes 1 field, the user's id
304+
* - Calls supabase's getUsers() function
305+
* - Responds with the user's username if the user is found
306+
* - Responds with an error message if the user is not found
307+
*/
308+
export const usernameFromUserId = asyncHandler(
309+
async (req: Request, res: Response) => {
310+
const { user_id } = req.query;
311+
if (!user_id) {
312+
return res.status(400).json({ error: "User ID is required" });
313+
}
314+
315+
const { data: userData, error } = await supabase.auth.admin.getUserById(user_id as string);
316+
317+
if (error) {
318+
return res.status(400).json({ error: "Unable to get username from email" });
319+
} else {
320+
const username = userData?.user?.user_metadata?.username;
321+
return res.status(200).json(username);
322+
}
323+
},
324+
);

course-matrix/backend/src/routes/authRouter.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import {
1010
resetPassword,
1111
accountDelete,
1212
updateUsername,
13+
usernameFromUserId
1314
} from "../controllers/userController";
1415
import { authHandler } from "../middleware/authHandler";
1516

@@ -68,3 +69,9 @@ authRouter.delete("/accountDelete", accountDelete);
6869
* @route POST /updateUsername
6970
*/
7071
authRouter.post("/updateUsername", authHandler, updateUsername);
72+
73+
/**
74+
* Route to get the username from the user id
75+
* @route GET /username-from-user-id
76+
*/
77+
authRouter.get("/username-from-user-id", authHandler, usernameFromUserId);

course-matrix/backend/src/routes/timetableRouter.ts

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,13 @@ timetableRouter.get(
6565
eventController.getEvents,
6666
);
6767

68+
/**
69+
* Route to get all events in a calendar shared with the authenticated user
70+
* @route GET /api/timetables/events/shared
71+
* @middleware authHandler - Middleware to check if the user is authenticated.
72+
*/
73+
timetableRouter.get("/events/shared/:user_id/:calendar_id", authHandler, eventController.getSharedEvents);
74+
6875
/**
6976
* Route to update an event
7077
* @route PUT /api/timetables/events/:id
@@ -149,10 +156,17 @@ timetableRouter.get(
149156

150157
/**
151158
* Route to get all shared entry with authenticated user
152-
* @route GET /api/timetables/shared
159+
* @route GET /api/timetables/shared/me
153160
* @middleware authHandler - Middleware to check if the user is authenticated
154161
*/
155-
timetableRouter.get("/shared", authHandler, sharesController.getShare);
162+
timetableRouter.get("/shared/me", authHandler, sharesController.getShare);
163+
164+
/**
165+
* Route to get all the restrictions of a timetable
166+
* @route GET /api/timetables/shared/restrictions
167+
* @middleware authHandler - Middleware to check if the user is authenticated
168+
*/
169+
timetableRouter.get("/shared/restrictions", authHandler, sharesController.getSharedRestrictions);
156170

157171
/**
158172
* Route to delete all shared entries for a timetable as timetable's owner
@@ -167,11 +181,11 @@ timetableRouter.delete(
167181

168182
/**
169183
* Route to delete a single entry for the authneticate user
170-
* @route DELETE /api/timetables/shared/:calendar_id
184+
* @route DELETE /api/timetables/shared/me/:calendar_id
171185
* @middleware authHandler - Middleware to check if the user is authenticated
172186
*/
173187
timetableRouter.delete(
174-
"/shared/:id",
188+
"/shared/me",
175189
authHandler,
176190
sharesController.deleteShare,
177-
);
191+
);

course-matrix/frontend/src/api/authApiSlice.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { get } from "http";
12
import { apiSlice } from "./baseApiSlice";
23
import { AUTH_URL } from "./config";
34

@@ -70,6 +71,18 @@ export const authApiSlice = apiSlice.injectEndpoints({
7071
credentials: "include",
7172
}),
7273
}),
74+
getUsernameFromUserId: builder.query<any, string | number>({
75+
query: (user_id) => ({
76+
url: `${AUTH_URL}/username-from-user-id`,
77+
method: "GET",
78+
headers: {
79+
"Content-Type": "application/json",
80+
Accept: "application/json, text/plain, */*",
81+
},
82+
params: { user_id },
83+
credentials: "include",
84+
}),
85+
}),
7386
}),
7487
});
7588

@@ -80,4 +93,5 @@ export const {
8093
useGetSessionQuery,
8194
useAccountDeleteMutation,
8295
useUpdateUsernameMutation,
96+
useGetUsernameFromUserIdQuery,
8397
} = authApiSlice;

course-matrix/frontend/src/api/baseApiSlice.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ export const apiSlice = createApi({
1313
"Timetable",
1414
"Event",
1515
"Restrictions",
16+
"Shared",
1617
],
1718
endpoints: () => ({}),
1819
});

course-matrix/frontend/src/api/config.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,3 +7,4 @@ export const DEPARTMENT_URL = `${SERVER_URL}/api/departments`;
77
export const OFFERINGS_URL = `${SERVER_URL}/api/offerings`;
88
export const TIMETABLES_URL = `${SERVER_URL}/api/timetables`;
99
export const EVENTS_URL = `${SERVER_URL}/api/timetables/events`;
10+
export const SHARED_URL = `${SERVER_URL}/api/timetables/shared`;

course-matrix/frontend/src/api/eventsApiSlice.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,19 @@ export const eventsApiSlice = apiSlice.injectEndpoints({
3030
}),
3131
keepUnusedDataFor: 0,
3232
}),
33+
getSharedEvents: builder.query<unknown, { user_id: string; calendar_id: number }>({
34+
query: (data) => ({
35+
url: `${EVENTS_URL}/shared/${data.user_id}/${data.calendar_id}`,
36+
method: "GET",
37+
headers: {
38+
"Content-Type": "application/json",
39+
Accept: "application/json, text/plain, */*",
40+
},
41+
providesTags: ["Event"],
42+
credentials: "include",
43+
}),
44+
keepUnusedDataFor: 0,
45+
}),
3346
updateEvent: builder.mutation({
3447
query: (data) => ({
3548
url: `${EVENTS_URL}/${data.id}`,
@@ -62,6 +75,7 @@ export const eventsApiSlice = apiSlice.injectEndpoints({
6275
export const {
6376
useCreateEventMutation,
6477
useGetEventsQuery,
78+
useGetSharedEventsQuery,
6579
useUpdateEventMutation,
6680
useDeleteEventMutation,
6781
} = eventsApiSlice;

0 commit comments

Comments
 (0)