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
This commit is contained in:
2025-11-16 21:39:38 +00:00
parent c84bcb2fd6
commit bee80be87d
4 changed files with 200 additions and 8 deletions

View File

@@ -259,7 +259,7 @@
- Create verification audit log - Create verification audit log
- _Requirements: 7.1, 7.2, 7.3, 7.4, 7.5_ - _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 - Create GET /api/users/:id for profile retrieval
- Implement PATCH /api/users/:id for profile updates - Implement PATCH /api/users/:id for profile updates
- Build GET /api/users/:id/reports endpoint - Build GET /api/users/:id/reports endpoint

View File

@@ -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 * POST /api/users/:id/calculate-impact - Calculate and update impact score
*/ */

View File

@@ -11,6 +11,7 @@ router.patch('/:id', requireAuth, userController.updateUserProfile.bind(userCont
// User reports and events // User reports and events
router.get('/:id/reports', userController.getUserReports.bind(userController)); router.get('/:id/reports', userController.getUserReports.bind(userController));
router.get('/:id/events', userController.getUserEvents.bind(userController)); router.get('/:id/events', userController.getUserEvents.bind(userController));
router.get('/:id/activity', userController.getUserActivity.bind(userController));
// Notification preferences // Notification preferences
router.patch('/:id/preferences', requireAuth, userController.updateNotificationPreferences.bind(userController)); router.patch('/:id/preferences', requireAuth, userController.updateNotificationPreferences.bind(userController));

View File

@@ -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) { 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({ const user = await prisma.user.findUnique({
where: { id: userId }, where: isClerkId ? { clerkId: userId } : { id: userId },
select: { select: {
id: true, id: true,
clerkId: 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) { 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({ const reports = await prisma.report.findMany({
where: { userId }, where: { userId: dbUserId },
include: { include: {
environmentalConcerns: true, environmentalConcerns: true,
verifications: 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) { 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 // Get events user has registered for
const registeredEvents = await prisma.eventRegistration.findMany({ const registeredEvents = await prisma.eventRegistration.findMany({
where: { userId }, where: { userId: dbUserId },
include: { include: {
event: { event: {
include: { include: {
@@ -186,7 +219,7 @@ export class UserService {
// Get events user has organized // Get events user has organized
const organizedEvents = await prisma.event.findMany({ const organizedEvents = await prisma.event.findMany({
where: { organizerId: userId }, where: { organizerId: dbUserId },
include: { include: {
organizer: { organizer: {
select: { select: {
@@ -237,6 +270,132 @@ export class UserService {
return updatedUser; 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 * Calculate and update user impact score
* Impact score = (reports * 10) + (events attended * 20) + (events organized * 50) * Impact score = (reports * 10) + (events attended * 20) + (events organized * 50)