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:
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -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));
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
Reference in New Issue
Block a user