diff --git a/server/bruno/Notangles API/AutoTimetable Routes/New Request.bru b/server/bruno/Notangles API/AutoTimetable Routes/New Request.bru new file mode 100644 index 000000000..0edc9e8b4 --- /dev/null +++ b/server/bruno/Notangles API/AutoTimetable Routes/New Request.bru @@ -0,0 +1,15 @@ +meta { + name: New Request + type: http + seq: 1 +} + +get { + url: + body: none + auth: inherit +} + +settings { + encodeUrl: true +} diff --git a/server/bruno/Notangles API/AutoTimetable Routes/folder.bru b/server/bruno/Notangles API/AutoTimetable Routes/folder.bru new file mode 100644 index 000000000..f2190d717 --- /dev/null +++ b/server/bruno/Notangles API/AutoTimetable Routes/folder.bru @@ -0,0 +1,8 @@ +meta { + name: AutoTimetable Routes + seq: 7 +} + +auth { + mode: inherit +} diff --git a/server/bruno/Notangles API/Class Routes/Get ClassesID from Course.bru b/server/bruno/Notangles API/Class Routes/Get ClassesID from Course.bru new file mode 100644 index 000000000..66d63c4f5 --- /dev/null +++ b/server/bruno/Notangles API/Class Routes/Get ClassesID from Course.bru @@ -0,0 +1,24 @@ +meta { + name: Get ClassesID from Course + type: http + seq: 1 +} + +get { + url: {{HostURL}}/user/timetables/classes/:timetableId/:courseId + body: none + auth: none +} + +params:path { + timetableId: 7050d820-fa7f-41e1-806e-88df665340f0 + courseId: COMP2521Undergraduate +} + +headers { + Cookie: {{Session_Cookie}} +} + +settings { + encodeUrl: true +} diff --git a/server/bruno/Notangles API/Class Routes/Remove Class for Course.bru b/server/bruno/Notangles API/Class Routes/Remove Class for Course.bru new file mode 100644 index 000000000..7696861cb --- /dev/null +++ b/server/bruno/Notangles API/Class Routes/Remove Class for Course.bru @@ -0,0 +1,25 @@ +meta { + name: Remove Class for Course + type: http + seq: 3 +} + +delete { + url: {{HostURL}}/user/timetables/class/:timetableId/:courseId/:classId + body: json + auth: none +} + +params:path { + timetableId: 1aba85cf-b2bc-41ea-acdf-008e9e335f34 + courseId: ACTL3301Undergraduate + classId: ACTL3301Undergraduate-10135-T2-2025 +} + +headers { + Cookie: {{Session_Cookie}} +} + +settings { + encodeUrl: true +} diff --git a/server/bruno/Notangles API/Class Routes/Select Class for Course.bru b/server/bruno/Notangles API/Class Routes/Select Class for Course.bru new file mode 100644 index 000000000..3c64d9190 --- /dev/null +++ b/server/bruno/Notangles API/Class Routes/Select Class for Course.bru @@ -0,0 +1,31 @@ +meta { + name: Select Class for Course + type: http + seq: 2 +} + +patch { + url: {{HostURL}}/user/timetables/class/:timetableId/:courseId + body: json + auth: none +} + +params:path { + timetableId: 1aba85cf-b2bc-41ea-acdf-008e9e335f34 + courseId: ACTL3301Undergraduate +} + +headers { + Cookie: {{Session_Cookie}} +} + +body:json { + { + "classId": "ACTL3301Undergraduate-10135-T2-2025" + // "classId": "ACTL3301Undergraduate-10138-T2-2025" + } +} + +settings { + encodeUrl: true +} diff --git a/server/bruno/Notangles API/Class Routes/folder.bru b/server/bruno/Notangles API/Class Routes/folder.bru new file mode 100644 index 000000000..53584e54a --- /dev/null +++ b/server/bruno/Notangles API/Class Routes/folder.bru @@ -0,0 +1,8 @@ +meta { + name: Class Routes + seq: 4 +} + +auth { + mode: inherit +} diff --git a/server/bruno/Notangles API/Course Routes/Add Course.bru b/server/bruno/Notangles API/Course Routes/Add Course.bru new file mode 100644 index 000000000..d9dbdb464 --- /dev/null +++ b/server/bruno/Notangles API/Course Routes/Add Course.bru @@ -0,0 +1,31 @@ +meta { + name: Add Course + type: http + seq: 2 +} + +post { + url: {{HostURL}}/user/timetables/course/:timetableId/:courseId + body: json + auth: none +} + +params:path { + timetableId: d9b19625-0e05-40d0-bef8-cc3bfdf1a6d3 + courseId: COMP6420Undergraduate +} + +headers { + Cookie: {{Session_Cookie}} +} + +body:json { + { + "colour": "default-1" + } +} + +settings { + encodeUrl: true + timeout: 0 +} diff --git a/server/bruno/Notangles API/Course Routes/Get CourseID.bru b/server/bruno/Notangles API/Course Routes/Get CourseID.bru new file mode 100644 index 000000000..eccca5111 --- /dev/null +++ b/server/bruno/Notangles API/Course Routes/Get CourseID.bru @@ -0,0 +1,38 @@ +meta { + name: Get CourseID + type: http + seq: 1 +} + +get { + url: {{HostURL}}/user/timetables/courses/:timetableId + body: none + auth: none +} + +params:path { + timetableId: d9b19625-0e05-40d0-bef8-cc3bfdf1a6d3 +} + +headers { + Cookie: {{Session_Cookie}} +} + +script:post-response { + test("Status code is 200", function () { + expect(res.getStatus()).to.equal(200); + }); + + test("Response is correct format", function () { + var jsonData = res.getBody(); + expect(jsonData).to.be.an('array'); + jsonData.forEach(function(id) { + expect(id).to.be.a('string'); + }); + }); +} + +settings { + encodeUrl: true + timeout: 0 +} diff --git a/server/bruno/Notangles API/Course Routes/Remove Course.bru b/server/bruno/Notangles API/Course Routes/Remove Course.bru new file mode 100644 index 000000000..051f7f295 --- /dev/null +++ b/server/bruno/Notangles API/Course Routes/Remove Course.bru @@ -0,0 +1,25 @@ +meta { + name: Remove Course + type: http + seq: 4 +} + +delete { + url: {{HostURL}}/user/timetables/course/:timetableId/:courseId + body: none + auth: none +} + +params:path { + timetableId: 6f4cbeb8-0979-4ec8-95ec-b301eb054715 + courseId: COMP2521Undergraduate +} + +headers { + Cookie: {{Session_Cookie}} +} + +settings { + encodeUrl: true + timeout: 0 +} diff --git a/server/bruno/Notangles API/Course Routes/Update Course Colour.bru b/server/bruno/Notangles API/Course Routes/Update Course Colour.bru new file mode 100644 index 000000000..f219ec191 --- /dev/null +++ b/server/bruno/Notangles API/Course Routes/Update Course Colour.bru @@ -0,0 +1,31 @@ +meta { + name: Update Course Colour + type: http + seq: 3 +} + +patch { + url: {{HostURL}}/user/timetables/course/:timetableId/:courseId/colour + body: json + auth: none +} + +params:path { + timetableId: 1aba85cf-b2bc-41ea-acdf-008e9e335f34 + courseId: ACTL3301Undergraduate +} + +headers { + Cookie: {{Session_Cookie}} +} + +body:json { + { + "colour": "default-7" + } +} + +settings { + encodeUrl: true + timeout: 0 +} diff --git a/server/bruno/Notangles API/Course Routes/folder.bru b/server/bruno/Notangles API/Course Routes/folder.bru new file mode 100644 index 000000000..c424ae673 --- /dev/null +++ b/server/bruno/Notangles API/Course Routes/folder.bru @@ -0,0 +1,8 @@ +meta { + name: Course Routes + seq: 3 +} + +auth { + mode: none +} diff --git a/server/bruno/Notangles API/Event Routes/Add event.bru b/server/bruno/Notangles API/Event Routes/Add event.bru new file mode 100644 index 000000000..bfa4fd95c --- /dev/null +++ b/server/bruno/Notangles API/Event Routes/Add event.bru @@ -0,0 +1,40 @@ +meta { + name: Add event + type: http + seq: 2 +} + +post { + url: {{HostURL}}/user/timetables/event + body: json + auth: none +} + +params:query { + ~timetableId: {} +} + +headers { + Cookie: {{Session_Cookie}} +} + +body:json { + { + "timetableId": "225ac72d-6015-40da-b41f-66099f080d99", + "event": { + "title": "2069 LEc", + "description": "Calculus 101", + "start": 10, + "end": 190, + "location": "Room 301", + "colour": "default-7", + "type": "CUSTOM", + "dayOfWeek": 1 + } + } + +} + +settings { + encodeUrl: true +} diff --git a/server/bruno/Notangles API/Event Routes/Get event.bru b/server/bruno/Notangles API/Event Routes/Get event.bru new file mode 100644 index 000000000..8aef49f7c --- /dev/null +++ b/server/bruno/Notangles API/Event Routes/Get event.bru @@ -0,0 +1,24 @@ +meta { + name: Get event + type: http + seq: 4 +} + +get { + url: {{HostURL}}/user/classes/:timetableId/:eventId + body: none + auth: none +} + +params:path { + timetableId: + eventId: +} + +headers { + Cookie: {{Session_Cookie}} +} + +settings { + encodeUrl: true +} diff --git a/server/bruno/Notangles API/Event Routes/TODO Delete event.bru b/server/bruno/Notangles API/Event Routes/TODO Delete event.bru new file mode 100644 index 000000000..873bfb33b --- /dev/null +++ b/server/bruno/Notangles API/Event Routes/TODO Delete event.bru @@ -0,0 +1,16 @@ +meta { + name: TODO Delete event + type: http + seq: 3 +} + +delete { + url: Delete event + body: none + auth: inherit +} + +settings { + encodeUrl: true + timeout: 0 +} diff --git a/server/bruno/Notangles API/Event Routes/Update event.bru b/server/bruno/Notangles API/Event Routes/Update event.bru new file mode 100644 index 000000000..da5216449 --- /dev/null +++ b/server/bruno/Notangles API/Event Routes/Update event.bru @@ -0,0 +1,37 @@ +meta { + name: Update event + type: http + seq: 1 +} + +patch { + url: {{HostURL}}/user/timetables/event/:eventid + body: json + auth: none +} + +params:path { + eventid: 5 +} + +headers { + Cookie: {{Session_Cookie}} +} + +body:json { + { + "title": "Math Tute", + "description": "Calculus 101", + "start": 10, + "end": 100, + "location": "Room 301", + "colour": "default-7", + "dayOfWeek": 1 + } + +} + +settings { + encodeUrl: true + timeout: 0 +} diff --git a/server/bruno/Notangles API/Event Routes/folder.bru b/server/bruno/Notangles API/Event Routes/folder.bru new file mode 100644 index 000000000..a9b5717b0 --- /dev/null +++ b/server/bruno/Notangles API/Event Routes/folder.bru @@ -0,0 +1,8 @@ +meta { + name: Event Routes + seq: 5 +} + +auth { + mode: inherit +} diff --git a/server/bruno/Notangles API/Friend Routes/Accept Friend Request.bru b/server/bruno/Notangles API/Friend Routes/Accept Friend Request.bru new file mode 100644 index 000000000..a59612d9a --- /dev/null +++ b/server/bruno/Notangles API/Friend Routes/Accept Friend Request.bru @@ -0,0 +1,15 @@ +meta { + name: Accept Friend Request + type: http + seq: 5 +} + +post { + url: {{HostURL}}/friendships/accept + body: none + auth: inherit +} + +settings { + encodeUrl: true +} diff --git a/server/bruno/Notangles API/Friend Routes/Cancel Friend Request.bru b/server/bruno/Notangles API/Friend Routes/Cancel Friend Request.bru new file mode 100644 index 000000000..fa7c754e6 --- /dev/null +++ b/server/bruno/Notangles API/Friend Routes/Cancel Friend Request.bru @@ -0,0 +1,22 @@ +meta { + name: Cancel Friend Request + type: http + seq: 5 +} + +post { + url: friendships/cancel + body: json + auth: inherit +} + +body:json { + { + "requesteeCode": "" + } +} + +settings { + encodeUrl: true + timeout: 0 +} diff --git a/server/bruno/Notangles API/Friend Routes/Create Friend Request.bru b/server/bruno/Notangles API/Friend Routes/Create Friend Request.bru new file mode 100644 index 000000000..7c0b074e1 --- /dev/null +++ b/server/bruno/Notangles API/Friend Routes/Create Friend Request.bru @@ -0,0 +1,22 @@ +meta { + name: Create Friend Request + type: http + seq: 1 +} + +post { + url: {{HostURL}}/friendships + body: json + auth: none +} + +body:json { + { + "requesteeCode": "" + } +} + +settings { + encodeUrl: true + timeout: 0 +} diff --git a/server/bruno/Notangles API/Friend Routes/Get Friends.bru b/server/bruno/Notangles API/Friend Routes/Get Friends.bru new file mode 100644 index 000000000..f6df5f0d9 --- /dev/null +++ b/server/bruno/Notangles API/Friend Routes/Get Friends.bru @@ -0,0 +1,15 @@ +meta { + name: Get Friends + type: http + seq: 4 +} + +get { + url: {{HostURL}}/friendships/ + body: none + auth: inherit +} + +settings { + encodeUrl: true +} diff --git a/server/bruno/Notangles API/Friend Routes/Get Outgoing Requests.bru b/server/bruno/Notangles API/Friend Routes/Get Outgoing Requests.bru new file mode 100644 index 000000000..7e1f35ed8 --- /dev/null +++ b/server/bruno/Notangles API/Friend Routes/Get Outgoing Requests.bru @@ -0,0 +1,20 @@ +meta { + name: Get Outgoing Requests + type: http + seq: 2 +} + +get { + url: {{HostURL}}/friendships/requests + body: none + auth: none +} + +headers { + Cookie: {{Session_Cookie}} +} + +settings { + encodeUrl: true + timeout: 0 +} diff --git a/server/bruno/Notangles API/Friend Routes/Reject Friend Request.bru b/server/bruno/Notangles API/Friend Routes/Reject Friend Request.bru new file mode 100644 index 000000000..60fcab59d --- /dev/null +++ b/server/bruno/Notangles API/Friend Routes/Reject Friend Request.bru @@ -0,0 +1,22 @@ +meta { + name: Reject Friend Request + type: http + seq: 6 +} + +get { + url: {{HostURL}}/friendships/reject + body: json + auth: inherit +} + +body:json { + { + "requestorCode": "" + } +} + +settings { + encodeUrl: true + timeout: 0 +} diff --git a/server/bruno/Notangles API/Friend Routes/Remove Friend Request.bru b/server/bruno/Notangles API/Friend Routes/Remove Friend Request.bru new file mode 100644 index 000000000..cfd0d47a1 --- /dev/null +++ b/server/bruno/Notangles API/Friend Routes/Remove Friend Request.bru @@ -0,0 +1,22 @@ +meta { + name: Remove Friend Request + type: http + seq: 6 +} + +get { + url: {{HostURL}}/friendships/remove + body: json + auth: inherit +} + +body:json { + { + "otherCode": "" + } +} + +settings { + encodeUrl: true + timeout: 0 +} diff --git a/server/bruno/Notangles API/Friend Routes/folder.bru b/server/bruno/Notangles API/Friend Routes/folder.bru new file mode 100644 index 000000000..6d1f702f1 --- /dev/null +++ b/server/bruno/Notangles API/Friend Routes/folder.bru @@ -0,0 +1,8 @@ +meta { + name: Friend Routes + seq: 6 +} + +auth { + mode: inherit +} diff --git a/server/bruno/Notangles API/Timetable Routes/Create Timetable.bru b/server/bruno/Notangles API/Timetable Routes/Create Timetable.bru new file mode 100644 index 000000000..3b8d22f15 --- /dev/null +++ b/server/bruno/Notangles API/Timetable Routes/Create Timetable.bru @@ -0,0 +1,28 @@ +meta { + name: Create Timetable + type: http + seq: 3 +} + +post { + url: {{HostURL}}/user/timetables + body: json + auth: none +} + +headers { + Cookie: {{Session_Cookie}} +} + +body:json { + { + "name":"New Timetable 1", + "year":2025, + "term": "T3" + } +} + +settings { + encodeUrl: true + timeout: 0 +} diff --git a/server/bruno/Notangles API/Timetable Routes/Delete Timetable.bru b/server/bruno/Notangles API/Timetable Routes/Delete Timetable.bru new file mode 100644 index 000000000..de331f288 --- /dev/null +++ b/server/bruno/Notangles API/Timetable Routes/Delete Timetable.bru @@ -0,0 +1,24 @@ +meta { + name: Delete Timetable + type: http + seq: 4 +} + +delete { + url: {{HostURL}}/user/timetables/:id + body: none + auth: none +} + +params:path { + id: 4ed45635-90c6-44cc-b6ce-3d4622c0d0f7 +} + +headers { + Cookie: {{Session_Cookie}} +} + +settings { + encodeUrl: true + timeout: 0 +} diff --git a/server/bruno/Notangles API/Timetable Routes/Get Timetable By ID.bru b/server/bruno/Notangles API/Timetable Routes/Get Timetable By ID.bru new file mode 100644 index 000000000..93e7ecedc --- /dev/null +++ b/server/bruno/Notangles API/Timetable Routes/Get Timetable By ID.bru @@ -0,0 +1,24 @@ +meta { + name: Get Timetable By ID + type: http + seq: 2 +} + +get { + url: {{HostURL}}/user/timetables/:id + body: none + auth: none +} + +params:path { + id: 4ed45635-90c6-44cc-b6ce-3d4622c0d0f7 +} + +headers { + Cookie: {{Session_Cookie}} +} + +settings { + encodeUrl: true + timeout: 0 +} diff --git a/server/bruno/Notangles API/Timetable Routes/Get User Timetables.bru b/server/bruno/Notangles API/Timetable Routes/Get User Timetables.bru new file mode 100644 index 000000000..68efbb058 --- /dev/null +++ b/server/bruno/Notangles API/Timetable Routes/Get User Timetables.bru @@ -0,0 +1,25 @@ +meta { + name: Get User Timetables + type: http + seq: 1 +} + +get { + url: {{HostURL}}/user/timetables?year=2025&term=T3 + body: none + auth: none +} + +params:query { + year: 2025 + term: T3 +} + +headers { + Set-Cookie: {{Session_Cookie}} +} + +settings { + encodeUrl: true + timeout: 0 +} diff --git a/server/bruno/Notangles API/Timetable Routes/Rename Timetable.bru b/server/bruno/Notangles API/Timetable Routes/Rename Timetable.bru new file mode 100644 index 000000000..77a5b39ff --- /dev/null +++ b/server/bruno/Notangles API/Timetable Routes/Rename Timetable.bru @@ -0,0 +1,30 @@ +meta { + name: Rename Timetable + type: http + seq: 5 +} + +patch { + url: {{HostURL}}/user/timetables/:id/rename + body: json + auth: none +} + +params:path { + id: ab83b07a-a1cf-4579-83a2-f9dcf71eecc4 +} + +headers { + Cookie: {{Session_Cookie}} +} + +body:json { + { + "name": "Test Rename Timetable" + } +} + +settings { + encodeUrl: true + timeout: 0 +} diff --git a/server/bruno/Notangles API/Timetable Routes/Timetable Make Primary.bru b/server/bruno/Notangles API/Timetable Routes/Timetable Make Primary.bru new file mode 100644 index 000000000..d414aaf1d --- /dev/null +++ b/server/bruno/Notangles API/Timetable Routes/Timetable Make Primary.bru @@ -0,0 +1,24 @@ +meta { + name: Timetable Make Primary + type: http + seq: 6 +} + +patch { + url: {{HostURL}}/user/timetables/:id/change-primary + body: none + auth: none +} + +params:path { + id: 3b0cd623-3105-4503-9e0c-0e949134be51 +} + +headers { + Cookie: {{Session_Cookie}} +} + +settings { + encodeUrl: true + timeout: 0 +} diff --git a/server/bruno/Notangles API/Timetable Routes/folder.bru b/server/bruno/Notangles API/Timetable Routes/folder.bru new file mode 100644 index 000000000..1fa863202 --- /dev/null +++ b/server/bruno/Notangles API/Timetable Routes/folder.bru @@ -0,0 +1,8 @@ +meta { + name: Timetable Routes + seq: 2 +} + +auth { + mode: none +} diff --git a/server/bruno/Notangles API/User Routes/Get User Profile.bru b/server/bruno/Notangles API/User Routes/Get User Profile.bru new file mode 100644 index 000000000..656fc3e58 --- /dev/null +++ b/server/bruno/Notangles API/User Routes/Get User Profile.bru @@ -0,0 +1,20 @@ +meta { + name: Get User Profile + type: http + seq: 2 +} + +get { + url: {{HostURL}}/user/profile + body: none + auth: none +} + +headers { + Cookie: {{Session_Cookie}} +} + +settings { + encodeUrl: true + timeout: 0 +} diff --git a/server/bruno/Notangles API/User Routes/Guest Login.bru b/server/bruno/Notangles API/User Routes/Guest Login.bru new file mode 100644 index 000000000..7c346063d --- /dev/null +++ b/server/bruno/Notangles API/User Routes/Guest Login.bru @@ -0,0 +1,24 @@ +meta { + name: Guest Login + type: http + seq: 1 +} + +get { + url: {{HostURL}}/auth/login/guest + body: none + auth: none +} + +script:post-response { + const sessionCookie = res.getHeader('set-cookie') + bru.setEnvVar("Session_Cookie", sessionCookie[0].split(';')[0]); + +} + +settings { + encodeUrl: true + timeout: 0 + followRedirects: false + maxRedirects: 5 +} diff --git a/server/bruno/Notangles API/User Routes/Post User Profile Picture.bru b/server/bruno/Notangles API/User Routes/Post User Profile Picture.bru new file mode 100644 index 000000000..a1f1d0895 --- /dev/null +++ b/server/bruno/Notangles API/User Routes/Post User Profile Picture.bru @@ -0,0 +1,26 @@ +meta { + name: Post User Profile Picture + type: http + seq: 3 +} + +post { + url: {{HostURL}}/user/profile/picture + body: json + auth: none +} + +headers { + Set-Cookie: {{Session_Cookie}} +} + +body:json { + { + "url": "data:image/gif;base64,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" + } +} + +settings { + encodeUrl: true + timeout: 0 +} diff --git a/server/bruno/Notangles API/User Routes/folder.bru b/server/bruno/Notangles API/User Routes/folder.bru new file mode 100644 index 000000000..6e605b75d --- /dev/null +++ b/server/bruno/Notangles API/User Routes/folder.bru @@ -0,0 +1,8 @@ +meta { + name: User Routes + seq: 1 +} + +auth { + mode: none +} diff --git a/server/bruno/Notangles API/bruno.json b/server/bruno/Notangles API/bruno.json new file mode 100644 index 000000000..ab46cab73 --- /dev/null +++ b/server/bruno/Notangles API/bruno.json @@ -0,0 +1,9 @@ +{ + "version": "1", + "name": "Notangles API", + "type": "collection", + "ignore": [ + "node_modules", + ".git" + ] +} \ No newline at end of file diff --git a/server/bruno/Notangles API/collection.bru b/server/bruno/Notangles API/collection.bru new file mode 100644 index 000000000..97ee1a39d --- /dev/null +++ b/server/bruno/Notangles API/collection.bru @@ -0,0 +1,25 @@ +meta { + name: Notangles API +} + +auth { + mode: none +} + +docs { + # Notangles API Collection + + This collection contains all **Notangles API** endpoints and usage notes. + + ## Setup Instructions + + Before using the requests, follow these steps to set up your Bruno environment variables: + + 1. Go to **Settings > General** and disable: + - `Store Cookies automatically` + - `Send Cookies automatically` + 2. Select one of the user environments (e.g., **Alice**, **Bob**). + 3. Use the **Guest Login** endpoint to get a session cookie first. + - If you see an error about not finding the FE on port `:5173`, check that the request setting **Automatically Follow Redirects** is disabled. + 4. Use the other endpoints as needed. +} diff --git a/server/bruno/Notangles API/environments/Alice.bru b/server/bruno/Notangles API/environments/Alice.bru new file mode 100644 index 000000000..e43d3f42a --- /dev/null +++ b/server/bruno/Notangles API/environments/Alice.bru @@ -0,0 +1,4 @@ +vars { + HostURL: http://localhost:3001/api + Session_Cookie : cookie.sid= +} diff --git a/server/bruno/Notangles API/environments/Bob.bru b/server/bruno/Notangles API/environments/Bob.bru new file mode 100644 index 000000000..e43d3f42a --- /dev/null +++ b/server/bruno/Notangles API/environments/Bob.bru @@ -0,0 +1,4 @@ +vars { + HostURL: http://localhost:3001/api + Session_Cookie : cookie.sid= +} diff --git a/server/prisma/migrations/20251015081258_add_friends/migration.sql b/server/prisma/migrations/20251015081258_add_friends/migration.sql new file mode 100644 index 000000000..49a302fa9 --- /dev/null +++ b/server/prisma/migrations/20251015081258_add_friends/migration.sql @@ -0,0 +1,37 @@ +/* + Warnings: + + - A unique constraint covering the columns `[inviteCode]` on the table `user` will be added. If there are existing duplicate values, this will fail. + - Added the required column `inviteCode` to the `user` table without a default value. This is not possible if the table is not empty. + +*/ +-- CreateEnum +CREATE TYPE "Status" AS ENUM ('REQ_UID1', 'REQ_UID2', 'FRIEND'); + +-- AlterTable +ALTER TABLE "user" ADD COLUMN "inviteCode" CHAR(6) NOT NULL; + +-- CreateTable +CREATE TABLE "friendship" ( + "id" TEXT NOT NULL, + "user1Id" TEXT NOT NULL, + "user2Id" TEXT NOT NULL, + "status" "Status" NOT NULL, + + CONSTRAINT "friendship_pkey" PRIMARY KEY ("id") +); + +-- CreateIndex +CREATE INDEX "friendshipId" ON "friendship"("id"); + +-- CreateIndex +CREATE UNIQUE INDEX "friendship_user1Id_user2Id_key" ON "friendship"("user1Id", "user2Id"); + +-- CreateIndex +CREATE UNIQUE INDEX "user_inviteCode_key" ON "user"("inviteCode"); + +-- CreateIndex +CREATE INDEX "friendsId" ON "user"("inviteCode"); + +-- CreateIndex +CREATE INDEX "userId" ON "user"("id"); diff --git a/server/prisma/schema.prisma b/server/prisma/schema.prisma index 4fba3b329..105cab716 100644 --- a/server/prisma/schema.prisma +++ b/server/prisma/schema.prisma @@ -24,6 +24,7 @@ model User { firstName String lastName String + inviteCode String @unique @db.Char(6) profilePictureUrl String? timetables Timetable[] settings Settings? @@ -32,26 +33,27 @@ model User { lastLogin DateTime @default(now()) @@unique([authProvider, authSubject]) + @@index([inviteCode], name: "friendsId") + @@index([id], name: "userId") @@map("user") } -// model FriendRequest { -// id String @id @default(uuid()) -// fromId String -// toId String +model Friendship { + id String @id @default(uuid()) + user1Id String + user2Id String + status Status -// @@unique([fromId, toId]) -// @@map("friend_request") -// } - -// model Friendship { -// id String @id @default(uuid()) -// user1Id String -// user2Id String + @@unique([user1Id, user2Id]) + @@index([id], name: "friendshipId") + @@map("friendship") +} -// @@unique([user1Id, user2Id]) -// @@map("friendship") -// } +enum Status { + REQ_UID1 + REQ_UID2 + FRIEND +} model Settings { user User @relation(fields: [userId], references: [id], onDelete: Cascade) diff --git a/server/src/app.module.ts b/server/src/app.module.ts index 6654b8744..dad688ebd 100644 --- a/server/src/app.module.ts +++ b/server/src/app.module.ts @@ -5,6 +5,7 @@ import { ConfigModule } from '@nestjs/config'; import config from './config'; import { UserModule } from './user/user.module'; import { AuthModule } from './auth/auth.module'; +import { FriendshipModule } from './friendship/friendship.module'; import { TimetableModule } from './timetable/timetable.module'; @Module({ @@ -15,6 +16,7 @@ import { TimetableModule } from './timetable/timetable.module'; expandVariables: true, }), UserModule, + FriendshipModule, TimetableModule, AuthModule, ], diff --git a/server/src/auth/auth.module.ts b/server/src/auth/auth.module.ts index aedc63d95..c918311f1 100644 --- a/server/src/auth/auth.module.ts +++ b/server/src/auth/auth.module.ts @@ -29,9 +29,9 @@ const OidcStrategyFactory = { PrismaService, UserService, GraphqlService, - OidcStrategyFactory, - GithubStrategy, - GoogleStrategy, + // OidcStrategyFactory, + // GithubStrategy, + // GoogleStrategy, GuestStrategy, SessionSerializer, AuthService, diff --git a/server/src/auth/auth.service.ts b/server/src/auth/auth.service.ts index 9c6d9d696..b014af1f7 100644 --- a/server/src/auth/auth.service.ts +++ b/server/src/auth/auth.service.ts @@ -1,6 +1,7 @@ import { Injectable } from '@nestjs/common'; import { PrismaService } from 'src/prisma/prisma.service'; import { AuthProvider, Prisma, User } from 'src/generated/prisma/client'; +import { UserService } from '../user/user.service'; import { Term } from 'src/timetable/types'; import { GraphqlService } from 'src/graphql/graphql.service'; @@ -17,6 +18,7 @@ export class AuthService { constructor( private readonly prisma: PrismaService, private readonly graphql: GraphqlService, + private readonly user: UserService, ) {} private readonly TIMETABLE_DEFAULT_NAME = 'My Timetable'; @@ -58,6 +60,7 @@ export class AuthService { authSubject: subject, firstName: params.firstName, lastName: params.lastName, + inviteCode: await this.user.generateUniqueInviteCode(), isGuest: params.isGuest, settings: { create: {} }, }, @@ -87,6 +90,7 @@ export class AuthService { firstName: params.firstName, lastName: params.lastName, isGuest: params.isGuest, + inviteCode: await this.user.generateUniqueInviteCode(), settings: { create: {} }, timetables: { create: availableTerms.map((availableTerm) => { diff --git a/server/src/auth/oidc.strategy.ts b/server/src/auth/oidc.strategy.ts index 5add31be2..e406126d7 100644 --- a/server/src/auth/oidc.strategy.ts +++ b/server/src/auth/oidc.strategy.ts @@ -74,6 +74,7 @@ export class OidcStrategy extends PassportStrategy(Strategy, 'oidc') { const userData = userInfo.userData as { firstName: string; lastName: string; + inviteCode: string; zid: string; department: string; program: number; diff --git a/server/src/friendship/friendship.controller.ts b/server/src/friendship/friendship.controller.ts new file mode 100644 index 000000000..6dd7adfe4 --- /dev/null +++ b/server/src/friendship/friendship.controller.ts @@ -0,0 +1,99 @@ +import { Controller, Get, Body, Post, Req, UseGuards } from '@nestjs/common'; +import { FriendshipService } from './friendship.service'; +import { + CreateFriendRequestDto, + CancelFriendRequestDto, + AcceptFriendRequestDto, + RejectFriendRequestDto, + RemoveFriendDto, +} from './types'; +import { AuthenticatedGuard } from 'src/auth/authenticated.guard'; +import { AuthenticatedRequest } from 'src/auth/auth.controller'; + +@Controller('friendships') +export class FriendshipController { + constructor(private readonly friendshipService: FriendshipService) {} + + @Post() + @UseGuards(AuthenticatedGuard) + async createFriendRequest( + @Req() req: AuthenticatedRequest, + @Body() createFriendRequest: CreateFriendRequestDto, + ) { + await this.friendshipService.createRelationship( + await this.friendshipService.fetchUserFriendCode(req.user.id), + createFriendRequest.requesteeCode, + ); + } + + @Post('cancel') + @UseGuards(AuthenticatedGuard) + async cancelFriendRequest( + @Req() req: AuthenticatedRequest, + @Body() cancelFriendRequest: CancelFriendRequestDto, + ) { + await this.friendshipService.deleteRelationship( + await this.friendshipService.fetchUserFriendCode(req.user.id), + cancelFriendRequest.requesteeCode, + true, + ); + } + + @Get('requests') + @UseGuards(AuthenticatedGuard) + async getOutgoingFriendRequests(@Req() req: AuthenticatedRequest) { + const userCode = await this.friendshipService.fetchUserFriendCode( + req.user.id, + ); + return await this.friendshipService.getUserFriendRequests(userCode); + } + + @Get() + @UseGuards(AuthenticatedGuard) + async getUserFriends(@Req() req: AuthenticatedRequest) { + const userCode = await this.friendshipService.fetchUserFriendCode( + req.user.id, + ); + return await this.friendshipService.getUserFriendships(userCode); + } + + @Post('accept') + @UseGuards(AuthenticatedGuard) + async acceptFriendRequest( + @Req() req: AuthenticatedRequest, + @Body() acceptFriendRequest: AcceptFriendRequestDto, + ) { + const userCode = await this.friendshipService.fetchUserFriendCode( + req.user.id, + ); + return await this.friendshipService.acceptFriendRequest( + userCode, + acceptFriendRequest.requestorCode, + ); + } + + @Post('reject') + @UseGuards(AuthenticatedGuard) + async rejectFriendRequest( + @Req() req: AuthenticatedRequest, + @Body() rejectFriendRequest: RejectFriendRequestDto, + ) { + await this.friendshipService.deleteRelationship( + await this.friendshipService.fetchUserFriendCode(req.user.id), + rejectFriendRequest.requestorCode, + false, + ); + } + + @Post('remove') + @UseGuards(AuthenticatedGuard) + async removeFriend( + @Req() req: AuthenticatedRequest, + @Body() removeFriendRequest: RemoveFriendDto, + ) { + await this.friendshipService.deleteFriendship( + await this.friendshipService.fetchUserFriendCode(req.user.id), + removeFriendRequest.otherCode, + ); + } +} diff --git a/server/src/friendship/friendship.module.ts b/server/src/friendship/friendship.module.ts new file mode 100644 index 000000000..23608d9fe --- /dev/null +++ b/server/src/friendship/friendship.module.ts @@ -0,0 +1,11 @@ +import { Module } from '@nestjs/common'; +import { GraphqlService } from 'src/graphql/graphql.service'; +import { PrismaService } from 'src/prisma/prisma.service'; +import { FriendshipController } from './friendship.controller'; +import { FriendshipService } from './friendship.service'; + +@Module({ + providers: [FriendshipService, PrismaService, GraphqlService], + controllers: [FriendshipController], +}) +export class FriendshipModule {} diff --git a/server/src/friendship/friendship.service.ts b/server/src/friendship/friendship.service.ts new file mode 100644 index 000000000..38f603c81 --- /dev/null +++ b/server/src/friendship/friendship.service.ts @@ -0,0 +1,188 @@ +import { HttpStatus, HttpException, Injectable } from '@nestjs/common'; +import { PrismaService } from 'src/prisma/prisma.service'; +import { Friendship } from 'src/generated/prisma/client'; + +@Injectable({}) +export class FriendshipService { + constructor(private readonly prisma: PrismaService) {} + // send request [x] + // cancel request ==> deleteRelationship + // see outgoing [X] + // see incoming [X] + // accept request [X] + // reject request [x] ==> deleteRelationship + // remove existing [x] ==> deleteRelationship + // reset friendcode ==> in user module + + // Returns the status string of the ORIGINAL parameter. + private async doesRelationshipExist( + user1Id: string, + user2Id: string, + ): Promise { + const doSwap = user1Id < user2Id; + + [user1Id, user2Id] = doSwap ? [user1Id, user2Id] : [user2Id, user1Id]; + + const friendship = await this.prisma.friendship.findUnique({ + where: { + user1Id_user2Id: { user1Id: user1Id, user2Id: user2Id }, + }, + }); + + if (friendship == undefined) return undefined; + if (friendship?.status == 'FRIEND') return 'FRIEND'; + + let ret; + if (!doSwap) ret = friendship?.status; + else ret = friendship?.status == 'REQ_UID1' ? 'REQ_UID2' : 'REQ_UID1'; + return ret; + } + async getUserFriendships(userId: string): Promise { + return await this.prisma.friendship.findMany({ + where: { + status: 'FRIEND', + OR: [{ user1Id: userId }, { user2Id: userId }], + }, + }); + } + + async getUserFriendRequests(userId: string): Promise { + // Infulences the ordering ==> Should re-order in the future + return await this.prisma.friendship.findMany({ + where: { + OR: [ + { + status: 'REQ_UID1', + user1Id: userId, + }, + { + status: 'REQ_UID2', + user2Id: userId, + }, + ], + }, + }); + } + + async getFriendRequestsToUser(userId: string): Promise { + // Infulences the ordering ==> Should re-order in the future + return await this.prisma.friendship.findMany({ + where: { + OR: [ + { + status: 'REQ_UID2', + user1Id: userId, + }, + { + status: 'REQ_UID1', + user2Id: userId, + }, + ], + }, + }); + } + + async acceptFriendRequest(userId: string, otherId: string): Promise { + const relationship = await this.prisma.friendship.findFirst({ + where: { + OR: [ + { + status: 'REQ_UID1', + user1Id: otherId, + user2Id: userId, + }, + { + status: 'REQ_UID2', + user1Id: userId, + user2Id: otherId, + }, + ], + }, + }); + // TODO: How to handle this proplery? + if (relationship === null) return; + + await this.prisma.friendship.update({ + where: { id: relationship.id }, + data: { status: 'FRIEND' }, + }); + } + + async deleteRelationship( + userId: string, + otherId: string, + requestedByUser: boolean, + ): Promise { + const relationship = await this.prisma.friendship.findFirst({ + where: { + OR: [ + { + status: 'REQ_UID1', + user1Id: requestedByUser ? userId : otherId, + user2Id: requestedByUser ? otherId : userId, + }, + { + status: 'REQ_UID2', + user1Id: requestedByUser ? otherId : userId, + user2Id: requestedByUser ? userId : otherId, + }, + ], + }, + }); + + if (relationship === undefined) return; + await this.prisma.friendship.delete({ + where: { id: relationship?.id }, + }); + } + + async deleteFriendship(userId: string, otherId: string): Promise { + const relationship = await this.prisma.friendship.findFirst({ + where: { + status: 'FRIEND', + user1Id: userId < otherId ? userId : otherId, + user2Id: userId < otherId ? otherId : userId, + }, + }); + + if (relationship === undefined) return; + await this.prisma.friendship.delete({ + where: { id: relationship?.id }, + }); + } + + async fetchUserFriendCode(userId: string): Promise { + const inviteCode = await this.prisma.user.findFirst({ + where: { id: userId }, + select: { inviteCode: true }, + }); + + if (inviteCode === null) { + throw new HttpException( + '[fetchUserFriendCode]: no inviteCode found!', + HttpStatus.NOT_FOUND, + ); + } + return inviteCode.inviteCode; + } + + async createRelationship(userId: string, otherId: string): Promise { + const isOrdered = userId < otherId; + const statusIfExists = await this.doesRelationshipExist(userId, otherId); + + if (statusIfExists !== undefined) { + throw new HttpException( + '[createRelationship]: relationship already exists!', + HttpStatus.BAD_REQUEST, + ); + } + + await this.prisma.friendship.create({ + data: { + user1Id: isOrdered ? userId : otherId, + user2Id: isOrdered ? otherId : userId, + status: isOrdered ? 'REQ_UID1' : 'REQ_UID2', + }, + }); + } +} diff --git a/server/src/friendship/types.ts b/server/src/friendship/types.ts new file mode 100644 index 000000000..2f2e17976 --- /dev/null +++ b/server/src/friendship/types.ts @@ -0,0 +1,23 @@ +// todo: Should these be combined or kept explicit? +// Im just doing this explicit but now but will combine them :D +// Union types? + +export class CreateFriendRequestDto { + requesteeCode: string; +} + +export class CancelFriendRequestDto { + requesteeCode: string; +} + +export class AcceptFriendRequestDto { + requestorCode: string; +} + +export class RejectFriendRequestDto { + requestorCode: string; +} + +export class RemoveFriendDto { + otherCode: string; +} diff --git a/server/src/graphql/graphql.service.ts b/server/src/graphql/graphql.service.ts index 1d0168d36..42dbbfd15 100644 --- a/server/src/graphql/graphql.service.ts +++ b/server/src/graphql/graphql.service.ts @@ -3,7 +3,7 @@ import { GraphQLClient } from 'graphql-request'; import { getSdk } from '../generated/graphql'; import type { ClassDetails } from './types'; -const HASURAGRES_GRAPHQL_API = 'https://graphql.csesoc.app/v1/graphql'; +const HASURAGRES_GRAPHQL_API = 'https://graphql.devsoc.app/v1/graphql'; @Injectable() export class GraphqlService { diff --git a/server/src/user/types.ts b/server/src/user/types.ts index 08515837a..70c6f450c 100644 --- a/server/src/user/types.ts +++ b/server/src/user/types.ts @@ -2,6 +2,7 @@ export class UserInfo { id: string; firstName: string; lastName: string; + inviteCode: string; profilePictureUrl?: string; } diff --git a/server/src/user/user.service.ts b/server/src/user/user.service.ts index 2df6f138a..a74471f3d 100644 --- a/server/src/user/user.service.ts +++ b/server/src/user/user.service.ts @@ -25,6 +25,7 @@ export class UserService { id: data.id, firstName: data.firstName, lastName: data.lastName, + inviteCode: data.inviteCode, profilePictureUrl: data.profilePictureUrl ?? undefined, }; } @@ -72,4 +73,37 @@ export class UserService { }, }); } + + private async isInviteCodeAlreadyUsed(inviteCode: string): Promise { + const code = await this.prisma.user.findFirst({ + where: { + inviteCode: inviteCode, + }, + select: { + inviteCode: true, + }, + }); + + return code !== null; + } + + // Note: this does NOT guarantee uniqueness, use generateUniqueInviteCode for that. + private generateInviteCode(): string { + return new Array(6) + .fill(undefined) + .map(() => + Math.floor(Math.random() * 36) + .toString(36) + .toUpperCase(), + ) + .join(''); + } + + async generateUniqueInviteCode(): Promise { + let code = this.generateInviteCode(); + while (await this.isInviteCodeAlreadyUsed(code)) { + code = this.generateInviteCode(); + } + return code; + } }