Skip to content

Commit 265aba0

Browse files
authored
Ax/scrum 60 Timetable Sharing Feature (#117)
Co-authored-by: Austin-X <[email protected]>
1 parent 7550a07 commit 265aba0

23 files changed

+1042
-95
lines changed

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

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -410,6 +410,46 @@ export default {
410410
}
411411
}),
412412

413+
/**
414+
* Get all events given a user_id and calendar_id
415+
*/
416+
getSharedEvents: asyncHandler(async (req: Request, res: Response) => {
417+
try {
418+
const { user_id, calendar_id } = req.params;
419+
420+
if (!user_id) {
421+
return res.status(400).json({ error: "user_id is required" });
422+
}
423+
if (!calendar_id) {
424+
return res.status(400).json({ error: "calendar_id is required" });
425+
}
426+
427+
const { data: courseEvents, error: courseError } = await supabase
428+
.schema("timetable")
429+
.from("course_events")
430+
.select("*")
431+
.eq("user_id", user_id)
432+
.eq("calendar_id", calendar_id);
433+
434+
const { data: userEvents, error: userError } = await supabase
435+
.schema("timetable")
436+
.from("user_events")
437+
.select("*")
438+
.eq("user_id", user_id)
439+
.eq("calendar_id", calendar_id);
440+
441+
if (courseError || userError) {
442+
return res
443+
.status(400)
444+
.json({ error: courseError?.message || userError?.message });
445+
}
446+
447+
return res.status(200).json({ courseEvents, userEvents });
448+
} catch (error) {
449+
return res.status(500).send({ error });
450+
}
451+
}),
452+
413453
/**
414454
* Update an event
415455
* The request should provide:

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

Lines changed: 58 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -136,22 +136,62 @@ 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" });
146+
return res.status(200).json(shareData);
147+
} catch (error) {
148+
return res.status(500).send({ error });
149+
}
150+
}),
151+
152+
/**
153+
* Get all restrictions from a shared timetable id
154+
* @route GET /api/shared/restrictions/:calendar_id
155+
*/
156+
getSharedRestrictions: asyncHandler(async (req: Request, res: Response) => {
157+
try {
158+
const { user_id, calendar_id } = req.query;
159+
160+
if (!user_id || !calendar_id) {
161+
return res.status(400).json({
162+
error: "User ID and Calendar ID are required",
163+
});
152164
}
153165

154-
return res.status(200).json(shareData);
166+
//Retrieve users allowed to access the timetable
167+
const { data: timetableData, error: timetableError } = await supabase
168+
.schema("timetable")
169+
.from("timetables")
170+
.select("*")
171+
.eq("id", calendar_id)
172+
.eq("user_id", user_id)
173+
.maybeSingle();
174+
175+
if (timetableError)
176+
return res.status(400).json({ error: timetableError.message });
177+
178+
//Validate timetable validity:
179+
if (!timetableData || timetableData.length === 0) {
180+
return res.status(404).json({ error: "Calendar id not found" });
181+
}
182+
183+
const { data: restrictionData, error: restrictionError } = await supabase
184+
.schema("timetable")
185+
.from("restriction")
186+
.select()
187+
.eq("user_id", user_id)
188+
.eq("calendar_id", calendar_id);
189+
190+
if (restrictionError) {
191+
return res.status(400).json({ error: restrictionError.message });
192+
}
193+
194+
return res.status(200).json(restrictionData);
155195
} catch (error) {
156196
return res.status(500).send({ error });
157197
}
@@ -387,15 +427,20 @@ export default {
387427
deleteShare: asyncHandler(async (req: Request, res: Response) => {
388428
try {
389429
const shared_id = (req as any).user.id;
390-
const { id } = req.params;
391-
const { calendar_id } = req.body;
430+
const { calendar_id, owner_id } = req.body;
431+
432+
if (!calendar_id || !owner_id) {
433+
return res.status(400).json({
434+
error: "Calendar ID and Owner ID are required",
435+
});
436+
}
392437

393438
const { data: existingTimetable, error: existingTimetableError } =
394439
await supabase
395440
.schema("timetable")
396441
.from("shared")
397442
.select("*")
398-
.eq("id", id)
443+
.eq("owner_id", owner_id)
399444
.eq("calendar_id", calendar_id)
400445
.eq("shared_id", shared_id);
401446

@@ -412,15 +457,15 @@ export default {
412457
.schema("timetable")
413458
.from("shared")
414459
.delete()
415-
.eq("id", id)
460+
.eq("owner_id", owner_id)
416461
.eq("calendar_id", calendar_id)
417462
.eq("shared_id", shared_id);
418463
if (deleteError) {
419464
return res.status(400).json({ error: deleteError.message });
420465
}
421466

422467
return res.status(200).json({
423-
message: `Sharing record: ${id} of calendar: ${calendar_id} deleted successfully`,
468+
message: `Sharing record with owner_id of ${owner_id} and calendar_id of ${calendar_id} deleted successfully`,
424469
});
425470
} catch (error) {
426471
return res.status(500).send({ error });

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

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -294,3 +294,35 @@ 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(
316+
user_id as string,
317+
);
318+
319+
if (error) {
320+
return res
321+
.status(400)
322+
.json({ error: "Unable to get username from email" });
323+
} else {
324+
const username = userData?.user?.user_metadata?.username;
325+
return res.status(200).json(username);
326+
}
327+
},
328+
);

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: 26 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,17 @@ 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(
74+
"/events/shared/:user_id/:calendar_id",
75+
authHandler,
76+
eventController.getSharedEvents,
77+
);
78+
6879
/**
6980
* Route to update an event
7081
* @route PUT /api/timetables/events/:id
@@ -149,10 +160,21 @@ timetableRouter.get(
149160

150161
/**
151162
* Route to get all shared entry with authenticated user
152-
* @route GET /api/timetables/shared
163+
* @route GET /api/timetables/shared/me
153164
* @middleware authHandler - Middleware to check if the user is authenticated
154165
*/
155-
timetableRouter.get("/shared", authHandler, sharesController.getShare);
166+
timetableRouter.get("/shared/me", authHandler, sharesController.getShare);
167+
168+
/**
169+
* Route to get all the restrictions of a timetable
170+
* @route GET /api/timetables/shared/restrictions
171+
* @middleware authHandler - Middleware to check if the user is authenticated
172+
*/
173+
timetableRouter.get(
174+
"/shared/restrictions",
175+
authHandler,
176+
sharesController.getSharedRestrictions,
177+
);
156178

157179
/**
158180
* Route to delete all shared entries for a timetable as timetable's owner
@@ -167,11 +189,7 @@ timetableRouter.delete(
167189

168190
/**
169191
* Route to delete a single entry for the authneticate user
170-
* @route DELETE /api/timetables/shared/:calendar_id
192+
* @route DELETE /api/timetables/shared/me/:calendar_id
171193
* @middleware authHandler - Middleware to check if the user is authenticated
172194
*/
173-
timetableRouter.delete(
174-
"/shared/:id",
175-
authHandler,
176-
sharesController.deleteShare,
177-
);
195+
timetableRouter.delete("/shared/me", authHandler, sharesController.deleteShare);

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: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ export const apiSlice = createApi({
1313
"Timetable",
1414
"Event",
1515
"Restrictions",
16+
"Shared",
1617
],
1718
endpoints: () => ({}),
19+
refetchOnMountOrArgChange: true,
1820
});

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: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,22 @@ export const eventsApiSlice = apiSlice.injectEndpoints({
3030
}),
3131
keepUnusedDataFor: 0,
3232
}),
33+
getSharedEvents: builder.query<
34+
unknown,
35+
{ user_id: string; calendar_id: number }
36+
>({
37+
query: (data) => ({
38+
url: `${EVENTS_URL}/shared/${data.user_id}/${data.calendar_id}`,
39+
method: "GET",
40+
headers: {
41+
"Content-Type": "application/json",
42+
Accept: "application/json, text/plain, */*",
43+
},
44+
providesTags: ["Event"],
45+
credentials: "include",
46+
}),
47+
keepUnusedDataFor: 0,
48+
}),
3349
updateEvent: builder.mutation({
3450
query: (data) => ({
3551
url: `${EVENTS_URL}/${data.id}`,
@@ -62,6 +78,7 @@ export const eventsApiSlice = apiSlice.injectEndpoints({
6278
export const {
6379
useCreateEventMutation,
6480
useGetEventsQuery,
81+
useGetSharedEventsQuery,
6582
useUpdateEventMutation,
6683
useDeleteEventMutation,
6784
} = eventsApiSlice;

0 commit comments

Comments
 (0)