From 34c43408711224972b270e3a41429647e6a9f5f3 Mon Sep 17 00:00:00 2001 From: David Ogden Date: Thu, 12 Jun 2025 15:30:19 +0100 Subject: [PATCH 1/7] feat: :sparkles: Set up back end routes for user Events GetAllEvents now gets user events by mode as well and returns an pbject of all events and the object Ids of the saved user events. Those id's are then checked on the front end against the rest of the events. The handle user events handles the toggling of the save button. --- shared | 2 +- src/controllers/event.controller.ts | 43 +++++++++++++++++++++++------ src/controllers/user.controller.ts | 34 +++++++++++++++++++++-- src/routes/user.routes.ts | 18 ++++-------- src/services/user.service.ts | 40 +++++++++++++++++++++++---- 5 files changed, 107 insertions(+), 30 deletions(-) diff --git a/shared b/shared index bddcaa7..e33f2b8 160000 --- a/shared +++ b/shared @@ -1 +1 @@ -Subproject commit bddcaa7804d4987f6cb84dc19d07359c1c16a8c7 +Subproject commit e33f2b8fb41afa8dbeab22570c0039cb53c1af45 diff --git a/src/controllers/event.controller.ts b/src/controllers/event.controller.ts index faebce5..a2c6d96 100644 --- a/src/controllers/event.controller.ts +++ b/src/controllers/event.controller.ts @@ -2,26 +2,37 @@ import { Context, ObjectId, RouterContext, Status } from '../../deps.ts'; import { eventService } from '../services/event.service.ts'; import { generateEvents } from '../services/openai.service.ts'; import { eventFilterSchema, FullEvent } from 'models/event.model.ts'; +import { verifyToken } from '../utils/token.utils.ts'; +import { Payload } from '../../deps.ts'; +import { userService } from '../services/user.service.ts'; export const getAllEvents = async (ctx: Context) => { - const params = ctx.request.url.searchParams; + const auth = ctx.request.headers.get('Authorization'); + const token = auth && auth.split(' ')[1]; + + let userId: string | null = null; + if (token) { + try { + const user: Payload = await verifyToken(token); + userId = user._id as string; + } catch (error) { + + console.warn('Invalid token in /events/with-saved: ' + error); + } + } + const params = ctx.request.url.searchParams; const rawModes = params.getAll('mode'); - const normalizedModes = rawModes .flatMap((param) => param.split(',')) .map((mode) => mode.trim().toLowerCase()) - .filter(Boolean); // removes empty strings + .filter(Boolean); const parseResult = eventFilterSchema.safeParse({ mode: normalizedModes.length > 0 ? normalizedModes : undefined, }); - let allEvents; - if (parseResult.success) { - console.log(parseResult.data); - allEvents = await eventService.getAllEvents(parseResult.data); - } else { + if (!parseResult.success) { ctx.response.status = Status.BadRequest; ctx.response.body = { error: 'Invalid query parameters', @@ -30,7 +41,21 @@ export const getAllEvents = async (ctx: Context) => { return; } - ctx.response.body = allEvents; + try { + const [allEvents, savedEvents] = await Promise.all([ + eventService.getAllEvents(parseResult.data), + userId ? userService.getUserEvents(userId) : Promise.resolve([]), + ]); + + ctx.response.body = { + events: allEvents, + savedEventIds: savedEvents.map(x => x._id), + }; + } catch (err) { + console.error("Error fetching combined event data:", err); + ctx.response.status = Status.InternalServerError; + ctx.response.body = { error: "Internal server error" }; + } }; export const getEventById = async (ctx: RouterContext<'/:id'>) => { diff --git a/src/controllers/user.controller.ts b/src/controllers/user.controller.ts index d6f52cd..902c382 100644 --- a/src/controllers/user.controller.ts +++ b/src/controllers/user.controller.ts @@ -1,8 +1,10 @@ // deno-lint-ignore-file require-await -import { Context, RouterContext } from '../../deps.ts'; +import { Context, Payload, RouterContext } from '../../deps.ts'; import { userService } from '../services/user.service.ts'; import { Status } from '../../deps.ts'; import { toSafeUser } from 'models/user.model.ts'; +import { verifyToken } from '../utils/token.utils.ts'; + export const getUserProfile = async (ctx: Context) => { // TODO: Get user data from ctx.state.user ctx.response.body = { message: 'Get user profile' }; @@ -17,7 +19,7 @@ export const deleteUserAccount = async (ctx: Context) => { // TODO: Call userService.deleteUser(ctx.state.user.id) ctx.response.body = { message: 'Delete user account' }; }; -export const getAllUsers = async (ctx: RouterContext<'/:role'>) => { +export const getAllUsers = async (ctx: RouterContext<'/getUsers:role'>) => { const role = ctx.params.role; if (role !== 'user' && role !== 'admin' && role !== 'all') { @@ -40,3 +42,31 @@ export const getAllUsers = async (ctx: RouterContext<'/:role'>) => { } } }; + +export const handleUserEvents = async (ctx: Context) => { + const auth = ctx.request.headers.get('Authorization'); + const token = auth && auth.split(' ')[1]; + +if (!token) { + return ctx.response.status = Status.Unauthorized; + +} + + try { + const user: Payload = await verifyToken(token); + + if (!user || !user._id) { + ctx.response.status = Status.Unauthorized; + ctx.response.body = { message: 'Unauthorized: Invalid token' }; + return; + } + const body = await ctx.request.body.json(); + const { eventId, active } = body; + const userId = user._id as string; + + await userService.handleUserEvents(eventId, userId, active); + return ctx.response.body = "User event handled" + } catch (error) { + console.error('Issue saving user events: ' + error) + } +} diff --git a/src/routes/user.routes.ts b/src/routes/user.routes.ts index 36ff637..8356787 100644 --- a/src/routes/user.routes.ts +++ b/src/routes/user.routes.ts @@ -1,19 +1,11 @@ import { Router } from '../../deps.ts'; import protectAdmin from '../middleware/requireAdmin.ts'; import ProtectRoute from '../middleware/protectRoute.ts'; -import { getAllUsers } from '../controllers/user.controller.ts'; -const router = new Router(); +import { getAllUsers, handleUserEvents } from '../controllers/user.controller.ts'; -// Routes under /users -// Temporary stub for testing -router.get('/', (ctx) => { - ctx.response.body = 'User route root'; -}); -router.get('/:role', ProtectRoute, protectAdmin, getAllUsers); -// -> to controllers -// router.get("/me", getUserProfile) -// router.put("/me", updateUserProfile) -// router.delete("/me", deleteUserAccount) +const router = new Router(); -export default router; +router.get('/getUsers:role', ProtectRoute, protectAdmin, getAllUsers); +router.post('/saveUserEvents', ProtectRoute, handleUserEvents); +export default router; \ No newline at end of file diff --git a/src/services/user.service.ts b/src/services/user.service.ts index 502575d..7170d97 100644 --- a/src/services/user.service.ts +++ b/src/services/user.service.ts @@ -6,10 +6,12 @@ import 'https://deno.land/std@0.224.0/dotenv/load.ts'; import { db } from '../database/connect.ts'; import { UserInDB } from 'models/user.model.ts'; -import { hash, OptionalId } from '../../deps.ts'; +import { hash, ObjectId, OptionalId } from '../../deps.ts'; import { NewUser } from 'models/user.model.ts'; +import { FullEvent } from '../../shared/src/models/event.model.ts'; const users = db.collection>('users'); +const events = db.collection('events') const getAllUsers = async ( role: 'user' | 'admin' | 'all', @@ -18,10 +20,6 @@ const getAllUsers = async ( return await users.find(query).toArray(); }; -export const userService = { - getAllUsers, -}; - const _createAdmin = async () => { const passwordHash = await hash('supersecretadminpassword'); const userToInsert: NewUser = { @@ -37,3 +35,35 @@ const _createAdmin = async () => { console.log('Admin created:', result); }; + +const handleUserEvents = async (eventId: string, userId: string, saving: boolean) => { + console.log(saving) + if (saving === true) { + await users.updateOne( + { _id: new ObjectId(userId) }, + { $addToSet : { saved_events: new ObjectId(eventId) } } + ) + } else { + await users.updateOne( + { _id: new ObjectId(userId) }, + { $pull: { saved_events: new ObjectId(eventId) } } + ) + } +} + +const getUserEvents = async (userId: string) => { + const user = await users.findOne({_id: new ObjectId(userId) }); + const savedEvents = user?.saved_events ?? []; + const eventsList = await events.find({ _id: { $in: savedEvents } }).toArray(); + return eventsList; +} + +export const userService = { + getAllUsers, + handleUserEvents, + getUserEvents +}; +/* +User clicks heart. Event target id? This is passed to backend via post request. Route grabs this. Calls this function via the +user controller. Save the id which should reference +*/ \ No newline at end of file From 28b2db73894e40429bb179d86901044517b5fcfe Mon Sep 17 00:00:00 2001 From: David Ogden Date: Thu, 12 Jun 2025 15:37:06 +0100 Subject: [PATCH 2/7] style: :art: Ran lint, check and fmt. --- src/controllers/event.controller.ts | 9 ++++---- src/controllers/user.controller.ts | 13 +++++------ src/routes/user.routes.ts | 8 ++++--- src/services/user.service.ts | 36 ++++++++++++++++------------- 4 files changed, 35 insertions(+), 31 deletions(-) diff --git a/src/controllers/event.controller.ts b/src/controllers/event.controller.ts index a2c6d96..ec7d15e 100644 --- a/src/controllers/event.controller.ts +++ b/src/controllers/event.controller.ts @@ -7,7 +7,7 @@ import { Payload } from '../../deps.ts'; import { userService } from '../services/user.service.ts'; export const getAllEvents = async (ctx: Context) => { - const auth = ctx.request.headers.get('Authorization'); + const auth = ctx.request.headers.get('Authorization'); const token = auth && auth.split(' ')[1]; let userId: string | null = null; @@ -16,7 +16,6 @@ export const getAllEvents = async (ctx: Context) => { const user: Payload = await verifyToken(token); userId = user._id as string; } catch (error) { - console.warn('Invalid token in /events/with-saved: ' + error); } } @@ -49,12 +48,12 @@ export const getAllEvents = async (ctx: Context) => { ctx.response.body = { events: allEvents, - savedEventIds: savedEvents.map(x => x._id), + savedEventIds: savedEvents.map((x) => x._id), }; } catch (err) { - console.error("Error fetching combined event data:", err); + console.error('Error fetching combined event data:', err); ctx.response.status = Status.InternalServerError; - ctx.response.body = { error: "Internal server error" }; + ctx.response.body = { error: 'Internal server error' }; } }; diff --git a/src/controllers/user.controller.ts b/src/controllers/user.controller.ts index 902c382..f92752f 100644 --- a/src/controllers/user.controller.ts +++ b/src/controllers/user.controller.ts @@ -47,10 +47,9 @@ export const handleUserEvents = async (ctx: Context) => { const auth = ctx.request.headers.get('Authorization'); const token = auth && auth.split(' ')[1]; -if (!token) { - return ctx.response.status = Status.Unauthorized; - -} + if (!token) { + return ctx.response.status = Status.Unauthorized; + } try { const user: Payload = await verifyToken(token); @@ -65,8 +64,8 @@ if (!token) { const userId = user._id as string; await userService.handleUserEvents(eventId, userId, active); - return ctx.response.body = "User event handled" + return ctx.response.body = 'User event handled'; } catch (error) { - console.error('Issue saving user events: ' + error) + console.error('Issue saving user events: ' + error); } -} +}; diff --git a/src/routes/user.routes.ts b/src/routes/user.routes.ts index 8356787..fabb53b 100644 --- a/src/routes/user.routes.ts +++ b/src/routes/user.routes.ts @@ -1,11 +1,13 @@ import { Router } from '../../deps.ts'; import protectAdmin from '../middleware/requireAdmin.ts'; import ProtectRoute from '../middleware/protectRoute.ts'; -import { getAllUsers, handleUserEvents } from '../controllers/user.controller.ts'; - +import { + getAllUsers, + handleUserEvents, +} from '../controllers/user.controller.ts'; const router = new Router(); router.get('/getUsers:role', ProtectRoute, protectAdmin, getAllUsers); router.post('/saveUserEvents', ProtectRoute, handleUserEvents); -export default router; \ No newline at end of file +export default router; diff --git a/src/services/user.service.ts b/src/services/user.service.ts index 7170d97..e3cc139 100644 --- a/src/services/user.service.ts +++ b/src/services/user.service.ts @@ -11,7 +11,7 @@ import { NewUser } from 'models/user.model.ts'; import { FullEvent } from '../../shared/src/models/event.model.ts'; const users = db.collection>('users'); -const events = db.collection('events') +const events = db.collection('events'); const getAllUsers = async ( role: 'user' | 'admin' | 'all', @@ -36,34 +36,38 @@ const _createAdmin = async () => { console.log('Admin created:', result); }; -const handleUserEvents = async (eventId: string, userId: string, saving: boolean) => { - console.log(saving) +const handleUserEvents = async ( + eventId: string, + userId: string, + saving: boolean, +) => { + console.log(saving); if (saving === true) { - await users.updateOne( - { _id: new ObjectId(userId) }, - { $addToSet : { saved_events: new ObjectId(eventId) } } - ) + await users.updateOne( + { _id: new ObjectId(userId) }, + { $addToSet: { saved_events: new ObjectId(eventId) } }, + ); } else { await users.updateOne( { _id: new ObjectId(userId) }, - { $pull: { saved_events: new ObjectId(eventId) } } - ) + { $pull: { saved_events: new ObjectId(eventId) } }, + ); } -} +}; const getUserEvents = async (userId: string) => { - const user = await users.findOne({_id: new ObjectId(userId) }); + const user = await users.findOne({ _id: new ObjectId(userId) }); const savedEvents = user?.saved_events ?? []; const eventsList = await events.find({ _id: { $in: savedEvents } }).toArray(); return eventsList; -} +}; export const userService = { getAllUsers, handleUserEvents, - getUserEvents + getUserEvents, }; /* -User clicks heart. Event target id? This is passed to backend via post request. Route grabs this. Calls this function via the -user controller. Save the id which should reference -*/ \ No newline at end of file +User clicks heart. Event target id? This is passed to backend via post request. Route grabs this. Calls this function via the +user controller. Save the id which should reference +*/ From 4f7b6b0d64815c66243b8ca3435f92bd107b778d Mon Sep 17 00:00:00 2001 From: David Ogden Date: Thu, 12 Jun 2025 17:14:49 +0100 Subject: [PATCH 3/7] Change to update event to make its fullevent a partial to match front end --- src/controllers/event.controller.ts | 4 ++-- src/services/event.service.ts | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/controllers/event.controller.ts b/src/controllers/event.controller.ts index ec7d15e..a4582a2 100644 --- a/src/controllers/event.controller.ts +++ b/src/controllers/event.controller.ts @@ -88,8 +88,8 @@ export const updateEventById = async (ctx: RouterContext<'/:id'>) => { } const id: string = ctx.params.id; - const event: FullEvent = await ctx.request.body.json(); - + const event: Partial = await ctx.request.body.json(); +console.log(event) if (!ObjectId.isValid(id)) { ctx.response.status = Status.BadRequest; ctx.response.body = `Invalid event id "${id}"`; diff --git a/src/services/event.service.ts b/src/services/event.service.ts index db9d2ff..f9621b2 100644 --- a/src/services/event.service.ts +++ b/src/services/event.service.ts @@ -32,7 +32,7 @@ const getEventById = async (id: string): Promise => { return await events.findOne({ _id: new ObjectId(id) }); }; -const updateEventById = async (id: string, event: FullEvent) => { +const updateEventById = async (id: string, event: Partial) => { return await events.updateOne({ _id: new ObjectId(id) }, { $set: event }); }; From 54bc436dba8c2d9b6a5c5b4fb92fdfa65f7b6d99 Mon Sep 17 00:00:00 2001 From: David Ogden Date: Thu, 12 Jun 2025 17:21:41 +0100 Subject: [PATCH 4/7] Ran check, fmt, lint --- src/controllers/event.controller.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/controllers/event.controller.ts b/src/controllers/event.controller.ts index a4582a2..c0ee9e8 100644 --- a/src/controllers/event.controller.ts +++ b/src/controllers/event.controller.ts @@ -89,7 +89,7 @@ export const updateEventById = async (ctx: RouterContext<'/:id'>) => { const id: string = ctx.params.id; const event: Partial = await ctx.request.body.json(); -console.log(event) + console.log(event); if (!ObjectId.isValid(id)) { ctx.response.status = Status.BadRequest; ctx.response.body = `Invalid event id "${id}"`; From 023241c2cb6732c0d6cede4c451e71465fd13d24 Mon Sep 17 00:00:00 2001 From: David Ogden Date: Thu, 12 Jun 2025 18:24:38 +0100 Subject: [PATCH 5/7] Change to console.log --- src/controllers/event.controller.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/controllers/event.controller.ts b/src/controllers/event.controller.ts index c0ee9e8..d3d1dd5 100644 --- a/src/controllers/event.controller.ts +++ b/src/controllers/event.controller.ts @@ -16,7 +16,7 @@ export const getAllEvents = async (ctx: Context) => { const user: Payload = await verifyToken(token); userId = user._id as string; } catch (error) { - console.warn('Invalid token in /events/with-saved: ' + error); + console.warn('Invalid token in /events: ' + error); } } From c6d741bd79e93584fbc8b911a734b4960e3c3961 Mon Sep 17 00:00:00 2001 From: David Ogden Date: Thu, 12 Jun 2025 18:28:45 +0100 Subject: [PATCH 6/7] Simplified handleUserEvents auth --- src/controllers/user.controller.ts | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/src/controllers/user.controller.ts b/src/controllers/user.controller.ts index f92752f..70d71cd 100644 --- a/src/controllers/user.controller.ts +++ b/src/controllers/user.controller.ts @@ -3,7 +3,6 @@ import { Context, Payload, RouterContext } from '../../deps.ts'; import { userService } from '../services/user.service.ts'; import { Status } from '../../deps.ts'; import { toSafeUser } from 'models/user.model.ts'; -import { verifyToken } from '../utils/token.utils.ts'; export const getUserProfile = async (ctx: Context) => { // TODO: Get user data from ctx.state.user @@ -44,15 +43,9 @@ export const getAllUsers = async (ctx: RouterContext<'/getUsers:role'>) => { }; export const handleUserEvents = async (ctx: Context) => { - const auth = ctx.request.headers.get('Authorization'); - const token = auth && auth.split(' ')[1]; - - if (!token) { - return ctx.response.status = Status.Unauthorized; - } try { - const user: Payload = await verifyToken(token); + const user: Payload = ctx.state.user if (!user || !user._id) { ctx.response.status = Status.Unauthorized; From bc48ef165e5cb47127fce57047f9d86cc4e4cb4f Mon Sep 17 00:00:00 2001 From: David Ogden Date: Thu, 12 Jun 2025 18:35:45 +0100 Subject: [PATCH 7/7] Ran deno fmt --- src/controllers/user.controller.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/controllers/user.controller.ts b/src/controllers/user.controller.ts index 70d71cd..40053e8 100644 --- a/src/controllers/user.controller.ts +++ b/src/controllers/user.controller.ts @@ -43,9 +43,8 @@ export const getAllUsers = async (ctx: RouterContext<'/getUsers:role'>) => { }; export const handleUserEvents = async (ctx: Context) => { - try { - const user: Payload = ctx.state.user + const user: Payload = ctx.state.user; if (!user || !user._id) { ctx.response.status = Status.Unauthorized;