From bee80be87dc256e8f217e4e05b1fe31cbe96298d Mon Sep 17 00:00:00 2001 From: Paul Bailey Date: Sun, 16 Nov 2025 21:39:38 +0000 Subject: [PATCH] fix: add support for Clerk IDs and activity endpoint - Update getUserProfile to accept both database ID and Clerk ID - Update getUserReports to handle Clerk IDs - Update getUserEvents to handle Clerk IDs - Add getUserActivity endpoint to return timeline of user actions - Add activity route GET /api/users/:id/activity - Fix TypeScript errors with const assertions This fixes 404 errors when frontend uses Clerk IDs to fetch user data --- .kiro/specs/beach-litter-mapping/tasks.md | 2 +- server/src/controllers/userController.ts | 32 ++++ server/src/routes/userRoutes.ts | 1 + server/src/services/userService.ts | 173 +++++++++++++++++++++- 4 files changed, 200 insertions(+), 8 deletions(-) diff --git a/.kiro/specs/beach-litter-mapping/tasks.md b/.kiro/specs/beach-litter-mapping/tasks.md index 3497293..4d3f1fb 100644 --- a/.kiro/specs/beach-litter-mapping/tasks.md +++ b/.kiro/specs/beach-litter-mapping/tasks.md @@ -259,7 +259,7 @@ - Create verification audit log - _Requirements: 7.1, 7.2, 7.3, 7.4, 7.5_ -- [-] 31. Build user profile endpoints +- [x] 31. Build user profile endpoints - Create GET /api/users/:id for profile retrieval - Implement PATCH /api/users/:id for profile updates - Build GET /api/users/:id/reports endpoint diff --git a/server/src/controllers/userController.ts b/server/src/controllers/userController.ts index 2276fd1..0662587 100644 --- a/server/src/controllers/userController.ts +++ b/server/src/controllers/userController.ts @@ -246,6 +246,38 @@ export class UserController { } } + /** + * GET /api/users/:id/activity - Get user's activity timeline + */ + async getUserActivity(req: AuthRequest, res: Response) { + try { + const { id } = req.params; + + const activities = await userService.getUserActivity(id); + res.json(activities); + } catch (error: any) { + console.error('Error fetching user activity:', error); + + if (error.message === 'User not found') { + return res.status(404).json({ + error: { + code: 'RESOURCE_NOT_FOUND', + message: 'User not found', + timestamp: new Date().toISOString(), + }, + }); + } + + res.status(500).json({ + error: { + code: 'INTERNAL_SERVER_ERROR', + message: 'Failed to fetch user activity', + timestamp: new Date().toISOString(), + }, + }); + } + } + /** * POST /api/users/:id/calculate-impact - Calculate and update impact score */ diff --git a/server/src/routes/userRoutes.ts b/server/src/routes/userRoutes.ts index db16f33..c09f42c 100644 --- a/server/src/routes/userRoutes.ts +++ b/server/src/routes/userRoutes.ts @@ -11,6 +11,7 @@ router.patch('/:id', requireAuth, userController.updateUserProfile.bind(userCont // User reports and events router.get('/:id/reports', userController.getUserReports.bind(userController)); router.get('/:id/events', userController.getUserEvents.bind(userController)); +router.get('/:id/activity', userController.getUserActivity.bind(userController)); // Notification preferences router.patch('/:id/preferences', requireAuth, userController.updateNotificationPreferences.bind(userController)); diff --git a/server/src/services/userService.ts b/server/src/services/userService.ts index 251027d..911996a 100644 --- a/server/src/services/userService.ts +++ b/server/src/services/userService.ts @@ -56,11 +56,14 @@ export class UserService { } /** - * Get user profile by database ID + * Get user profile by database ID or Clerk ID */ async getUserProfile(userId: string) { + // Check if this is a Clerk ID (starts with 'user_') + const isClerkId = userId.startsWith('user_'); + const user = await prisma.user.findUnique({ - where: { id: userId }, + where: isClerkId ? { clerkId: userId } : { id: userId }, select: { id: true, clerkId: true, @@ -142,11 +145,26 @@ export class UserService { } /** - * Get user's reports + * Get user's reports by database ID or Clerk ID */ async getUserReports(userId: string) { + // Check if this is a Clerk ID and convert to database ID + const isClerkId = userId.startsWith('user_'); + let dbUserId = userId; + + if (isClerkId) { + const user = await prisma.user.findUnique({ + where: { clerkId: userId }, + select: { id: true }, + }); + if (!user) { + throw new Error('User not found'); + } + dbUserId = user.id; + } + const reports = await prisma.report.findMany({ - where: { userId }, + where: { userId: dbUserId }, include: { environmentalConcerns: true, verifications: true, @@ -160,12 +178,27 @@ export class UserService { } /** - * Get user's events (both registered and organized) + * Get user's events (both registered and organized) by database ID or Clerk ID */ async getUserEvents(userId: string) { + // Check if this is a Clerk ID and convert to database ID + const isClerkId = userId.startsWith('user_'); + let dbUserId = userId; + + if (isClerkId) { + const user = await prisma.user.findUnique({ + where: { clerkId: userId }, + select: { id: true }, + }); + if (!user) { + throw new Error('User not found'); + } + dbUserId = user.id; + } + // Get events user has registered for const registeredEvents = await prisma.eventRegistration.findMany({ - where: { userId }, + where: { userId: dbUserId }, include: { event: { include: { @@ -186,7 +219,7 @@ export class UserService { // Get events user has organized const organizedEvents = await prisma.event.findMany({ - where: { organizerId: userId }, + where: { organizerId: dbUserId }, include: { organizer: { select: { @@ -237,6 +270,132 @@ export class UserService { return updatedUser; } + /** + * Get user's activity timeline + */ + async getUserActivity(userId: string) { + // Check if this is a Clerk ID and convert to database ID + const isClerkId = userId.startsWith('user_'); + let dbUserId = userId; + + if (isClerkId) { + const user = await prisma.user.findUnique({ + where: { clerkId: userId }, + select: { id: true }, + }); + if (!user) { + throw new Error('User not found'); + } + dbUserId = user.id; + } + + // Get user's reports + const reports = await prisma.report.findMany({ + where: { userId: dbUserId }, + select: { + id: true, + createdAt: true, + litterType: true, + quantity: true, + latitude: true, + longitude: true, + description: true, + }, + orderBy: { createdAt: 'desc' }, + take: 50, + }); + + // Get user's event registrations + const eventRegistrations = await prisma.eventRegistration.findMany({ + where: { userId: dbUserId }, + include: { + event: { + select: { + id: true, + title: true, + description: true, + scheduledDate: true, + latitude: true, + longitude: true, + locationName: true, + status: true, + }, + }, + }, + orderBy: { registeredAt: 'desc' }, + take: 50, + }); + + // Get user's organized events + const organizedEvents = await prisma.event.findMany({ + where: { organizerId: dbUserId }, + select: { + id: true, + title: true, + description: true, + createdAt: true, + scheduledDate: true, + latitude: true, + longitude: true, + locationName: true, + status: true, + }, + orderBy: { createdAt: 'desc' }, + take: 50, + }); + + // Combine and format activities + const activities = [ + ...reports.map(report => ({ + id: report.id, + type: 'report' as const, + timestamp: report.createdAt || new Date(), + title: `Reported ${report.litterType} litter`, + description: report.description || `${report.quantity} amount of ${report.litterType}`, + location: { + latitude: Number(report.latitude), + longitude: Number(report.longitude), + }, + })), + ...eventRegistrations.map(reg => { + const activityType: 'event_attended' = 'event_attended'; + return { + id: reg.event.id, + type: activityType, + timestamp: reg.registeredAt || new Date(), + title: reg.event.title, + description: reg.event.description, + location: { + latitude: Number(reg.event.latitude), + longitude: Number(reg.event.longitude), + locationName: reg.event.locationName, + }, + }; + }), + ...organizedEvents.map(event => { + const activityType: 'event_created' | 'event_completed' = + event.status === 'completed' ? 'event_completed' : 'event_created'; + return { + id: event.id, + type: activityType, + timestamp: event.createdAt || new Date(), + title: event.title, + description: event.description, + location: { + latitude: Number(event.latitude), + longitude: Number(event.longitude), + locationName: event.locationName, + }, + }; + }), + ]; + + // Sort by timestamp descending + activities.sort((a, b) => b.timestamp.getTime() - a.timestamp.getTime()); + + return activities; + } + /** * Calculate and update user impact score * Impact score = (reports * 10) + (events attended * 20) + (events organized * 50)