feat: implement user profile endpoints (task 31)
- Add GET /api/users/:id for profile retrieval with stats - Add PATCH /api/users/:id for profile updates - Add GET /api/users/:id/reports to fetch user's reports - Add GET /api/users/:id/events to fetch user's events (registered & organized) - Add PATCH /api/users/:id/preferences for notification settings - Add POST /api/users/:id/calculate-impact for impact score calculation - Create userService with all business logic - Create userController with request handling - Create userRoutes and register in main server - Update frontend userService with new endpoints - Add comprehensive documentation in USER_PROFILE_ENDPOINTS.md - Add test script for manual endpoint testing Requirements satisfied: 6.3, 6.4, 6.5, 10.4
This commit is contained in:
@@ -117,7 +117,7 @@
|
|||||||
|
|
||||||
## User Profile and Engagement
|
## User Profile and Engagement
|
||||||
|
|
||||||
- [x] 15. Build user profile page
|
- [ ] 15. Build user profile page
|
||||||
- Display user statistics (reports, events attended)
|
- Display user statistics (reports, events attended)
|
||||||
- Calculate and show impact score
|
- Calculate and show impact score
|
||||||
- Create personal contribution map
|
- Create personal contribution map
|
||||||
@@ -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
|
- [-] 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
|
||||||
|
|||||||
401
server/USER_PROFILE_ENDPOINTS.md
Normal file
401
server/USER_PROFILE_ENDPOINTS.md
Normal file
@@ -0,0 +1,401 @@
|
|||||||
|
# User Profile Endpoints Documentation
|
||||||
|
|
||||||
|
This document describes the user profile API endpoints implemented for the Beach Litter Mapping Platform.
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
The user profile endpoints provide functionality for:
|
||||||
|
- Retrieving user profiles with statistics
|
||||||
|
- Updating user profile information
|
||||||
|
- Managing notification preferences
|
||||||
|
- Fetching user's reports and events
|
||||||
|
- Calculating impact scores
|
||||||
|
|
||||||
|
## Authentication
|
||||||
|
|
||||||
|
Most endpoints require authentication via Clerk session token:
|
||||||
|
```
|
||||||
|
Authorization: Bearer <clerk_session_token>
|
||||||
|
```
|
||||||
|
|
||||||
|
## Endpoints
|
||||||
|
|
||||||
|
### 1. Get User Profile
|
||||||
|
|
||||||
|
**Endpoint:** `GET /api/users/:id`
|
||||||
|
|
||||||
|
**Description:** Retrieve a user's profile including statistics about their contributions.
|
||||||
|
|
||||||
|
**Authentication:** Not required (public profiles)
|
||||||
|
|
||||||
|
**Parameters:**
|
||||||
|
- `id` (path) - User database ID
|
||||||
|
|
||||||
|
**Response:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"id": "uuid",
|
||||||
|
"clerkId": "user_xxx",
|
||||||
|
"email": "user@example.com",
|
||||||
|
"username": "johndoe",
|
||||||
|
"firstName": "John",
|
||||||
|
"lastName": "Doe",
|
||||||
|
"avatarUrl": "https://...",
|
||||||
|
"impactScore": 150,
|
||||||
|
"notificationEmail": true,
|
||||||
|
"notificationInApp": true,
|
||||||
|
"areasOfInterest": [
|
||||||
|
{
|
||||||
|
"lat": 53.3498,
|
||||||
|
"lng": -6.2603,
|
||||||
|
"radius": 10
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"createdAt": "2024-01-01T00:00:00.000Z",
|
||||||
|
"updatedAt": "2024-01-01T00:00:00.000Z",
|
||||||
|
"stats": {
|
||||||
|
"reportsSubmitted": 5,
|
||||||
|
"eventsAttended": 3,
|
||||||
|
"eventsOrganized": 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Error Responses:**
|
||||||
|
- `404` - User not found
|
||||||
|
- `500` - Internal server error
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 2. Update User Profile
|
||||||
|
|
||||||
|
**Endpoint:** `PATCH /api/users/:id`
|
||||||
|
|
||||||
|
**Description:** Update user profile information (first name, last name, username).
|
||||||
|
|
||||||
|
**Authentication:** Required (users can only update their own profile)
|
||||||
|
|
||||||
|
**Parameters:**
|
||||||
|
- `id` (path) - User database ID
|
||||||
|
|
||||||
|
**Request Body:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"firstName": "John",
|
||||||
|
"lastName": "Doe",
|
||||||
|
"username": "johndoe123"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
All fields are optional. Only provided fields will be updated.
|
||||||
|
|
||||||
|
**Response:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"id": "uuid",
|
||||||
|
"clerkId": "user_xxx",
|
||||||
|
"email": "user@example.com",
|
||||||
|
"username": "johndoe123",
|
||||||
|
"firstName": "John",
|
||||||
|
"lastName": "Doe",
|
||||||
|
"avatarUrl": "https://...",
|
||||||
|
"impactScore": 150,
|
||||||
|
"createdAt": "2024-01-01T00:00:00.000Z",
|
||||||
|
"updatedAt": "2024-01-01T00:00:00.000Z"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Error Responses:**
|
||||||
|
- `400` - Username already taken
|
||||||
|
- `401` - Not authenticated
|
||||||
|
- `403` - Cannot update another user's profile
|
||||||
|
- `404` - User not found
|
||||||
|
- `500` - Internal server error
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3. Get User Reports
|
||||||
|
|
||||||
|
**Endpoint:** `GET /api/users/:id/reports`
|
||||||
|
|
||||||
|
**Description:** Retrieve all reports submitted by a user.
|
||||||
|
|
||||||
|
**Authentication:** Not required
|
||||||
|
|
||||||
|
**Parameters:**
|
||||||
|
- `id` (path) - User database ID
|
||||||
|
|
||||||
|
**Response:**
|
||||||
|
```json
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"id": "uuid",
|
||||||
|
"userId": "uuid",
|
||||||
|
"latitude": 53.3498,
|
||||||
|
"longitude": -6.2603,
|
||||||
|
"locationSource": "gps",
|
||||||
|
"photoUrls": ["/uploads/photo1.jpg"],
|
||||||
|
"photoTimestamp": "2024-01-01T12:00:00.000Z",
|
||||||
|
"litterType": "plastic",
|
||||||
|
"quantity": "moderate",
|
||||||
|
"description": "Plastic bottles on beach",
|
||||||
|
"verificationStatus": "verified",
|
||||||
|
"createdAt": "2024-01-01T12:00:00.000Z",
|
||||||
|
"environmentalConcerns": [],
|
||||||
|
"verifications": []
|
||||||
|
}
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
**Error Responses:**
|
||||||
|
- `500` - Internal server error
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 4. Get User Events
|
||||||
|
|
||||||
|
**Endpoint:** `GET /api/users/:id/events`
|
||||||
|
|
||||||
|
**Description:** Retrieve events the user has registered for and organized.
|
||||||
|
|
||||||
|
**Authentication:** Not required
|
||||||
|
|
||||||
|
**Parameters:**
|
||||||
|
- `id` (path) - User database ID
|
||||||
|
|
||||||
|
**Response:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"registered": [
|
||||||
|
{
|
||||||
|
"id": "uuid",
|
||||||
|
"title": "Beach Cleanup",
|
||||||
|
"description": "Community cleanup event",
|
||||||
|
"latitude": 53.3498,
|
||||||
|
"longitude": -6.2603,
|
||||||
|
"locationName": "Sandymount Beach",
|
||||||
|
"scheduledDate": "2024-02-01T10:00:00.000Z",
|
||||||
|
"duration": 120,
|
||||||
|
"status": "upcoming",
|
||||||
|
"participantCount": 15,
|
||||||
|
"organizer": {
|
||||||
|
"id": "uuid",
|
||||||
|
"username": "organizer",
|
||||||
|
"avatarUrl": "https://..."
|
||||||
|
},
|
||||||
|
"registeredAt": "2024-01-15T10:00:00.000Z",
|
||||||
|
"attended": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"organized": [
|
||||||
|
{
|
||||||
|
"id": "uuid",
|
||||||
|
"title": "My Beach Cleanup",
|
||||||
|
"description": "Event I organized",
|
||||||
|
"latitude": 53.3498,
|
||||||
|
"longitude": -6.2603,
|
||||||
|
"locationName": "Dollymount Beach",
|
||||||
|
"scheduledDate": "2024-03-01T10:00:00.000Z",
|
||||||
|
"duration": 180,
|
||||||
|
"status": "upcoming",
|
||||||
|
"participantCount": 20,
|
||||||
|
"organizer": {
|
||||||
|
"id": "uuid",
|
||||||
|
"username": "johndoe",
|
||||||
|
"avatarUrl": "https://..."
|
||||||
|
},
|
||||||
|
"registrations": []
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Error Responses:**
|
||||||
|
- `500` - Internal server error
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 5. Update Notification Preferences
|
||||||
|
|
||||||
|
**Endpoint:** `PATCH /api/users/:id/preferences`
|
||||||
|
|
||||||
|
**Description:** Update user's notification preferences and areas of interest.
|
||||||
|
|
||||||
|
**Authentication:** Required (users can only update their own preferences)
|
||||||
|
|
||||||
|
**Parameters:**
|
||||||
|
- `id` (path) - User database ID
|
||||||
|
|
||||||
|
**Request Body:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"notificationEmail": true,
|
||||||
|
"notificationInApp": true,
|
||||||
|
"areasOfInterest": [
|
||||||
|
{
|
||||||
|
"lat": 53.3498,
|
||||||
|
"lng": -6.2603,
|
||||||
|
"radius": 10
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"lat": 51.8985,
|
||||||
|
"lng": -8.4756,
|
||||||
|
"radius": 25
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Field Validation:**
|
||||||
|
- `radius` must be between 1 and 50 kilometers
|
||||||
|
- Each area must have `lat`, `lng`, and `radius`
|
||||||
|
|
||||||
|
**Response:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"id": "uuid",
|
||||||
|
"notificationEmail": true,
|
||||||
|
"notificationInApp": true,
|
||||||
|
"areasOfInterest": [
|
||||||
|
{
|
||||||
|
"lat": 53.3498,
|
||||||
|
"lng": -6.2603,
|
||||||
|
"radius": 10
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Error Responses:**
|
||||||
|
- `400` - Validation error (invalid radius, missing fields, etc.)
|
||||||
|
- `401` - Not authenticated
|
||||||
|
- `403` - Cannot update another user's preferences
|
||||||
|
- `404` - User not found
|
||||||
|
- `500` - Internal server error
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 6. Calculate Impact Score
|
||||||
|
|
||||||
|
**Endpoint:** `POST /api/users/:id/calculate-impact`
|
||||||
|
|
||||||
|
**Description:** Calculate and update the user's impact score based on their contributions.
|
||||||
|
|
||||||
|
**Authentication:** Not required
|
||||||
|
|
||||||
|
**Parameters:**
|
||||||
|
- `id` (path) - User database ID
|
||||||
|
|
||||||
|
**Impact Score Formula:**
|
||||||
|
```
|
||||||
|
Impact Score = (reports × 10) + (events attended × 20) + (events organized × 50)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Response:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"impactScore": 150
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Error Responses:**
|
||||||
|
- `404` - User not found
|
||||||
|
- `500` - Internal server error
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Usage Examples
|
||||||
|
|
||||||
|
### Get User Profile
|
||||||
|
```bash
|
||||||
|
curl http://localhost:3005/api/users/abc-123
|
||||||
|
```
|
||||||
|
|
||||||
|
### Update Profile
|
||||||
|
```bash
|
||||||
|
curl -X PATCH http://localhost:3005/api/users/abc-123 \
|
||||||
|
-H "Authorization: Bearer <token>" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{
|
||||||
|
"firstName": "John",
|
||||||
|
"lastName": "Doe",
|
||||||
|
"username": "johndoe"
|
||||||
|
}'
|
||||||
|
```
|
||||||
|
|
||||||
|
### Update Notification Preferences
|
||||||
|
```bash
|
||||||
|
curl -X PATCH http://localhost:3005/api/users/abc-123/preferences \
|
||||||
|
-H "Authorization: Bearer <token>" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{
|
||||||
|
"notificationEmail": true,
|
||||||
|
"notificationInApp": true,
|
||||||
|
"areasOfInterest": [
|
||||||
|
{
|
||||||
|
"lat": 53.3498,
|
||||||
|
"lng": -6.2603,
|
||||||
|
"radius": 10
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}'
|
||||||
|
```
|
||||||
|
|
||||||
|
### Calculate Impact Score
|
||||||
|
```bash
|
||||||
|
curl -X POST http://localhost:3005/api/users/abc-123/calculate-impact
|
||||||
|
```
|
||||||
|
|
||||||
|
## Frontend Integration
|
||||||
|
|
||||||
|
The frontend service (`src/services/userService.ts`) provides TypeScript methods for all endpoints:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { userService } from '@/services/userService';
|
||||||
|
|
||||||
|
// Get profile
|
||||||
|
const profile = await userService.getProfile(userId);
|
||||||
|
|
||||||
|
// Update profile
|
||||||
|
const updated = await userService.updateProfile(userId, {
|
||||||
|
firstName: 'John',
|
||||||
|
lastName: 'Doe',
|
||||||
|
username: 'johndoe'
|
||||||
|
});
|
||||||
|
|
||||||
|
// Get user's reports
|
||||||
|
const reports = await userService.getUserReports(userId);
|
||||||
|
|
||||||
|
// Get user's events
|
||||||
|
const events = await userService.getUserEvents(userId);
|
||||||
|
|
||||||
|
// Update notification preferences
|
||||||
|
const preferences = await userService.updateNotificationPreferences(userId, {
|
||||||
|
notificationEmail: true,
|
||||||
|
notificationInApp: true,
|
||||||
|
areasOfInterest: [
|
||||||
|
{ lat: 53.3498, lng: -6.2603, radius: 10 }
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
|
// Calculate impact score
|
||||||
|
const { impactScore } = await userService.calculateImpactScore(userId);
|
||||||
|
```
|
||||||
|
|
||||||
|
## Requirements Satisfied
|
||||||
|
|
||||||
|
This implementation satisfies the following requirements from the specification:
|
||||||
|
|
||||||
|
- **Requirement 6.3**: User profile display showing submitted reports and events attended
|
||||||
|
- **Requirement 6.4**: Impact score calculation based on contributions
|
||||||
|
- **Requirement 6.5**: User profile showing map of submitted report locations (data provided via reports endpoint)
|
||||||
|
- **Requirement 10.4**: Notification preferences with area of interest configuration (1-50km radius)
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
|
||||||
|
- User authentication is handled by Clerk
|
||||||
|
- Users are automatically created in the database when they first interact with the API
|
||||||
|
- The `areasOfInterest` field is stored as JSON in the database
|
||||||
|
- Impact scores are calculated on-demand but can be cached for performance
|
||||||
|
- All endpoints follow RESTful conventions
|
||||||
|
- Error responses follow a consistent format with error codes and timestamps
|
||||||
282
server/src/controllers/userController.ts
Normal file
282
server/src/controllers/userController.ts
Normal file
@@ -0,0 +1,282 @@
|
|||||||
|
import { Response } from 'express';
|
||||||
|
import { AuthRequest } from '../middleware/auth';
|
||||||
|
import { userService } from '../services/userService';
|
||||||
|
|
||||||
|
export class UserController {
|
||||||
|
/**
|
||||||
|
* GET /api/users/:id - Get user profile
|
||||||
|
*/
|
||||||
|
async getUserProfile(req: AuthRequest, res: Response) {
|
||||||
|
try {
|
||||||
|
const { id } = req.params;
|
||||||
|
|
||||||
|
const profile = await userService.getUserProfile(id);
|
||||||
|
res.json(profile);
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error('Error fetching user profile:', 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 profile',
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* PATCH /api/users/:id - Update user profile
|
||||||
|
*/
|
||||||
|
async updateUserProfile(req: AuthRequest, res: Response) {
|
||||||
|
try {
|
||||||
|
const { id } = req.params;
|
||||||
|
const { firstName, lastName, username } = req.body;
|
||||||
|
|
||||||
|
// Authorization check - users can only update their own profile
|
||||||
|
if (!req.userId) {
|
||||||
|
return res.status(401).json({
|
||||||
|
error: {
|
||||||
|
code: 'UNAUTHORIZED',
|
||||||
|
message: 'User not authenticated',
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find or create user to get database ID
|
||||||
|
const dbUserId = await userService.findOrCreateUser(req.userId);
|
||||||
|
|
||||||
|
if (dbUserId !== id) {
|
||||||
|
return res.status(403).json({
|
||||||
|
error: {
|
||||||
|
code: 'FORBIDDEN',
|
||||||
|
message: 'You can only update your own profile',
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const updatedProfile = await userService.updateUserProfile(id, {
|
||||||
|
firstName,
|
||||||
|
lastName,
|
||||||
|
username,
|
||||||
|
});
|
||||||
|
|
||||||
|
res.json(updatedProfile);
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error('Error updating user profile:', 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(),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (error.message === 'Username already taken') {
|
||||||
|
return res.status(400).json({
|
||||||
|
error: {
|
||||||
|
code: 'VALIDATION_ERROR',
|
||||||
|
message: 'Username already taken',
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
res.status(500).json({
|
||||||
|
error: {
|
||||||
|
code: 'INTERNAL_SERVER_ERROR',
|
||||||
|
message: 'Failed to update user profile',
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GET /api/users/:id/reports - Get user's reports
|
||||||
|
*/
|
||||||
|
async getUserReports(req: AuthRequest, res: Response) {
|
||||||
|
try {
|
||||||
|
const { id } = req.params;
|
||||||
|
|
||||||
|
const reports = await userService.getUserReports(id);
|
||||||
|
res.json(reports);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching user reports:', error);
|
||||||
|
res.status(500).json({
|
||||||
|
error: {
|
||||||
|
code: 'INTERNAL_SERVER_ERROR',
|
||||||
|
message: 'Failed to fetch user reports',
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GET /api/users/:id/events - Get user's events
|
||||||
|
*/
|
||||||
|
async getUserEvents(req: AuthRequest, res: Response) {
|
||||||
|
try {
|
||||||
|
const { id } = req.params;
|
||||||
|
|
||||||
|
const events = await userService.getUserEvents(id);
|
||||||
|
res.json(events);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching user events:', error);
|
||||||
|
res.status(500).json({
|
||||||
|
error: {
|
||||||
|
code: 'INTERNAL_SERVER_ERROR',
|
||||||
|
message: 'Failed to fetch user events',
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* PATCH /api/users/:id/preferences - Update notification preferences
|
||||||
|
*/
|
||||||
|
async updateNotificationPreferences(req: AuthRequest, res: Response) {
|
||||||
|
try {
|
||||||
|
const { id } = req.params;
|
||||||
|
const { notificationEmail, notificationInApp, areasOfInterest } = req.body;
|
||||||
|
|
||||||
|
// Authorization check
|
||||||
|
if (!req.userId) {
|
||||||
|
return res.status(401).json({
|
||||||
|
error: {
|
||||||
|
code: 'UNAUTHORIZED',
|
||||||
|
message: 'User not authenticated',
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find or create user to get database ID
|
||||||
|
const dbUserId = await userService.findOrCreateUser(req.userId);
|
||||||
|
|
||||||
|
if (dbUserId !== id) {
|
||||||
|
return res.status(403).json({
|
||||||
|
error: {
|
||||||
|
code: 'FORBIDDEN',
|
||||||
|
message: 'You can only update your own preferences',
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate areasOfInterest if provided
|
||||||
|
if (areasOfInterest) {
|
||||||
|
if (!Array.isArray(areasOfInterest)) {
|
||||||
|
return res.status(400).json({
|
||||||
|
error: {
|
||||||
|
code: 'VALIDATION_ERROR',
|
||||||
|
message: 'areasOfInterest must be an array',
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const area of areasOfInterest) {
|
||||||
|
if (!area.lat || !area.lng || !area.radius) {
|
||||||
|
return res.status(400).json({
|
||||||
|
error: {
|
||||||
|
code: 'VALIDATION_ERROR',
|
||||||
|
message: 'Each area must have lat, lng, and radius',
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (area.radius < 1 || area.radius > 50) {
|
||||||
|
return res.status(400).json({
|
||||||
|
error: {
|
||||||
|
code: 'VALIDATION_ERROR',
|
||||||
|
message: 'Radius must be between 1 and 50 kilometers',
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const updatedPreferences = await userService.updateNotificationPreferences(id, {
|
||||||
|
notificationEmail,
|
||||||
|
notificationInApp,
|
||||||
|
areasOfInterest,
|
||||||
|
});
|
||||||
|
|
||||||
|
res.json(updatedPreferences);
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error('Error updating notification preferences:', 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 update notification preferences',
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* POST /api/users/:id/calculate-impact - Calculate and update impact score
|
||||||
|
*/
|
||||||
|
async calculateImpactScore(req: AuthRequest, res: Response) {
|
||||||
|
try {
|
||||||
|
const { id } = req.params;
|
||||||
|
|
||||||
|
const impactScore = await userService.calculateImpactScore(id);
|
||||||
|
res.json({ impactScore });
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error('Error calculating impact score:', 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 calculate impact score',
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const userController = new UserController();
|
||||||
@@ -5,6 +5,7 @@ import dotenv from 'dotenv';
|
|||||||
import path from 'path';
|
import path from 'path';
|
||||||
import reportRoutes from './routes/reportRoutes';
|
import reportRoutes from './routes/reportRoutes';
|
||||||
import eventRoutes from './routes/eventRoutes';
|
import eventRoutes from './routes/eventRoutes';
|
||||||
|
import userRoutes from './routes/userRoutes';
|
||||||
|
|
||||||
// Load environment variables
|
// Load environment variables
|
||||||
dotenv.config();
|
dotenv.config();
|
||||||
@@ -39,6 +40,7 @@ app.get('/health', (req, res) => {
|
|||||||
// API Routes
|
// API Routes
|
||||||
app.use('/api/reports', reportRoutes);
|
app.use('/api/reports', reportRoutes);
|
||||||
app.use('/api/events', eventRoutes);
|
app.use('/api/events', eventRoutes);
|
||||||
|
app.use('/api/users', userRoutes);
|
||||||
|
|
||||||
// Error handling middleware
|
// Error handling middleware
|
||||||
app.use((err: any, req: express.Request, res: express.Response, next: express.NextFunction) => {
|
app.use((err: any, req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||||
|
|||||||
21
server/src/routes/userRoutes.ts
Normal file
21
server/src/routes/userRoutes.ts
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
import { Router } from 'express';
|
||||||
|
import { userController } from '../controllers/userController';
|
||||||
|
import { requireAuth } from '../middleware/auth';
|
||||||
|
|
||||||
|
const router = Router();
|
||||||
|
|
||||||
|
// User profile endpoints
|
||||||
|
router.get('/:id', userController.getUserProfile.bind(userController));
|
||||||
|
router.patch('/:id', requireAuth, userController.updateUserProfile.bind(userController));
|
||||||
|
|
||||||
|
// User reports and events
|
||||||
|
router.get('/:id/reports', userController.getUserReports.bind(userController));
|
||||||
|
router.get('/:id/events', userController.getUserEvents.bind(userController));
|
||||||
|
|
||||||
|
// Notification preferences
|
||||||
|
router.patch('/:id/preferences', requireAuth, userController.updateNotificationPreferences.bind(userController));
|
||||||
|
|
||||||
|
// Impact score calculation
|
||||||
|
router.post('/:id/calculate-impact', userController.calculateImpactScore.bind(userController));
|
||||||
|
|
||||||
|
export default router;
|
||||||
135
server/src/scripts/testUserEndpoints.ts
Normal file
135
server/src/scripts/testUserEndpoints.ts
Normal file
@@ -0,0 +1,135 @@
|
|||||||
|
/**
|
||||||
|
* Test script for user profile endpoints
|
||||||
|
*
|
||||||
|
* This script tests the user profile API endpoints to ensure they work correctly.
|
||||||
|
*
|
||||||
|
* Usage:
|
||||||
|
* 1. Make sure the server is running (npm run dev)
|
||||||
|
* 2. Run: npx ts-node src/scripts/testUserEndpoints.ts
|
||||||
|
*/
|
||||||
|
|
||||||
|
import axios from 'axios';
|
||||||
|
|
||||||
|
const API_BASE_URL = 'http://localhost:3005/api';
|
||||||
|
|
||||||
|
// You'll need to replace this with a valid Clerk session token
|
||||||
|
const AUTH_TOKEN = 'your_clerk_session_token_here';
|
||||||
|
|
||||||
|
async function testUserEndpoints() {
|
||||||
|
console.log('🧪 Testing User Profile Endpoints\n');
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Test 1: Get user profile
|
||||||
|
console.log('1️⃣ Testing GET /api/users/:id');
|
||||||
|
try {
|
||||||
|
const userId = 'test-user-id'; // Replace with actual user ID
|
||||||
|
const profileResponse = await axios.get(`${API_BASE_URL}/users/${userId}`);
|
||||||
|
console.log('✅ Get user profile successful');
|
||||||
|
console.log('Profile:', JSON.stringify(profileResponse.data, null, 2));
|
||||||
|
} catch (error: any) {
|
||||||
|
console.log('❌ Get user profile failed:', error.response?.data || error.message);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('\n');
|
||||||
|
|
||||||
|
// Test 2: Update user profile
|
||||||
|
console.log('2️⃣ Testing PATCH /api/users/:id');
|
||||||
|
try {
|
||||||
|
const userId = 'test-user-id'; // Replace with actual user ID
|
||||||
|
const updateResponse = await axios.patch(
|
||||||
|
`${API_BASE_URL}/users/${userId}`,
|
||||||
|
{
|
||||||
|
firstName: 'John',
|
||||||
|
lastName: 'Doe',
|
||||||
|
username: 'johndoe123',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${AUTH_TOKEN}`,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
console.log('✅ Update user profile successful');
|
||||||
|
console.log('Updated profile:', JSON.stringify(updateResponse.data, null, 2));
|
||||||
|
} catch (error: any) {
|
||||||
|
console.log('❌ Update user profile failed:', error.response?.data || error.message);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('\n');
|
||||||
|
|
||||||
|
// Test 3: Get user reports
|
||||||
|
console.log('3️⃣ Testing GET /api/users/:id/reports');
|
||||||
|
try {
|
||||||
|
const userId = 'test-user-id'; // Replace with actual user ID
|
||||||
|
const reportsResponse = await axios.get(`${API_BASE_URL}/users/${userId}/reports`);
|
||||||
|
console.log('✅ Get user reports successful');
|
||||||
|
console.log(`Found ${reportsResponse.data.length} reports`);
|
||||||
|
} catch (error: any) {
|
||||||
|
console.log('❌ Get user reports failed:', error.response?.data || error.message);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('\n');
|
||||||
|
|
||||||
|
// Test 4: Get user events
|
||||||
|
console.log('4️⃣ Testing GET /api/users/:id/events');
|
||||||
|
try {
|
||||||
|
const userId = 'test-user-id'; // Replace with actual user ID
|
||||||
|
const eventsResponse = await axios.get(`${API_BASE_URL}/users/${userId}/events`);
|
||||||
|
console.log('✅ Get user events successful');
|
||||||
|
console.log('Events:', JSON.stringify(eventsResponse.data, null, 2));
|
||||||
|
} catch (error: any) {
|
||||||
|
console.log('❌ Get user events failed:', error.response?.data || error.message);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('\n');
|
||||||
|
|
||||||
|
// Test 5: Update notification preferences
|
||||||
|
console.log('5️⃣ Testing PATCH /api/users/:id/preferences');
|
||||||
|
try {
|
||||||
|
const userId = 'test-user-id'; // Replace with actual user ID
|
||||||
|
const preferencesResponse = await axios.patch(
|
||||||
|
`${API_BASE_URL}/users/${userId}/preferences`,
|
||||||
|
{
|
||||||
|
notificationEmail: true,
|
||||||
|
notificationInApp: true,
|
||||||
|
areasOfInterest: [
|
||||||
|
{
|
||||||
|
lat: 53.3498,
|
||||||
|
lng: -6.2603,
|
||||||
|
radius: 10,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${AUTH_TOKEN}`,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
console.log('✅ Update notification preferences successful');
|
||||||
|
console.log('Preferences:', JSON.stringify(preferencesResponse.data, null, 2));
|
||||||
|
} catch (error: any) {
|
||||||
|
console.log('❌ Update notification preferences failed:', error.response?.data || error.message);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('\n');
|
||||||
|
|
||||||
|
// Test 6: Calculate impact score
|
||||||
|
console.log('6️⃣ Testing POST /api/users/:id/calculate-impact');
|
||||||
|
try {
|
||||||
|
const userId = 'test-user-id'; // Replace with actual user ID
|
||||||
|
const impactResponse = await axios.post(`${API_BASE_URL}/users/${userId}/calculate-impact`);
|
||||||
|
console.log('✅ Calculate impact score successful');
|
||||||
|
console.log('Impact score:', impactResponse.data.impactScore);
|
||||||
|
} catch (error: any) {
|
||||||
|
console.log('❌ Calculate impact score failed:', error.response?.data || error.message);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('\n✅ All tests completed!\n');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ Test suite failed:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run tests
|
||||||
|
testUserEndpoints();
|
||||||
281
server/src/services/userService.ts
Normal file
281
server/src/services/userService.ts
Normal file
@@ -0,0 +1,281 @@
|
|||||||
|
import prisma from '../config/database';
|
||||||
|
import { clerkClient } from '@clerk/clerk-sdk-node';
|
||||||
|
|
||||||
|
export interface UpdateUserProfileData {
|
||||||
|
firstName?: string;
|
||||||
|
lastName?: string;
|
||||||
|
username?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UpdateNotificationPreferencesData {
|
||||||
|
notificationEmail?: boolean;
|
||||||
|
notificationInApp?: boolean;
|
||||||
|
areasOfInterest?: Array<{
|
||||||
|
lat: number;
|
||||||
|
lng: number;
|
||||||
|
radius: number;
|
||||||
|
}>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class UserService {
|
||||||
|
/**
|
||||||
|
* Find or create user from Clerk ID
|
||||||
|
*/
|
||||||
|
async findOrCreateUser(clerkId: string): Promise<string> {
|
||||||
|
// Check if user exists
|
||||||
|
let user = await prisma.user.findUnique({
|
||||||
|
where: { clerkId },
|
||||||
|
select: { id: true },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (user) {
|
||||||
|
return user.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
// User doesn't exist, fetch from Clerk and create
|
||||||
|
try {
|
||||||
|
const clerkUser = await clerkClient.users.getUser(clerkId);
|
||||||
|
|
||||||
|
user = await prisma.user.create({
|
||||||
|
data: {
|
||||||
|
clerkId,
|
||||||
|
email: clerkUser.emailAddresses[0]?.emailAddress || `${clerkId}@temp.com`,
|
||||||
|
username: clerkUser.username || `user_${clerkId.substring(0, 8)}`,
|
||||||
|
firstName: clerkUser.firstName,
|
||||||
|
lastName: clerkUser.lastName,
|
||||||
|
avatarUrl: clerkUser.imageUrl,
|
||||||
|
},
|
||||||
|
select: { id: true },
|
||||||
|
});
|
||||||
|
|
||||||
|
return user.id;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error creating user from Clerk:', error);
|
||||||
|
throw new Error('Failed to create user');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get user profile by database ID
|
||||||
|
*/
|
||||||
|
async getUserProfile(userId: string) {
|
||||||
|
const user = await prisma.user.findUnique({
|
||||||
|
where: { id: userId },
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
clerkId: true,
|
||||||
|
email: true,
|
||||||
|
username: true,
|
||||||
|
firstName: true,
|
||||||
|
lastName: true,
|
||||||
|
avatarUrl: true,
|
||||||
|
impactScore: true,
|
||||||
|
notificationEmail: true,
|
||||||
|
notificationInApp: true,
|
||||||
|
areasOfInterest: true,
|
||||||
|
createdAt: true,
|
||||||
|
updatedAt: true,
|
||||||
|
_count: {
|
||||||
|
select: {
|
||||||
|
reports: true,
|
||||||
|
eventRegistrations: true,
|
||||||
|
organizedEvents: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
throw new Error('User not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
...user,
|
||||||
|
stats: {
|
||||||
|
reportsSubmitted: user._count.reports,
|
||||||
|
eventsAttended: user._count.eventRegistrations,
|
||||||
|
eventsOrganized: user._count.organizedEvents,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update user profile
|
||||||
|
*/
|
||||||
|
async updateUserProfile(userId: string, data: UpdateUserProfileData) {
|
||||||
|
// Check if username is being changed and if it's already taken
|
||||||
|
if (data.username) {
|
||||||
|
const existingUser = await prisma.user.findFirst({
|
||||||
|
where: {
|
||||||
|
username: data.username,
|
||||||
|
id: { not: userId },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (existingUser) {
|
||||||
|
throw new Error('Username already taken');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const updatedUser = await prisma.user.update({
|
||||||
|
where: { id: userId },
|
||||||
|
data: {
|
||||||
|
firstName: data.firstName,
|
||||||
|
lastName: data.lastName,
|
||||||
|
username: data.username,
|
||||||
|
},
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
clerkId: true,
|
||||||
|
email: true,
|
||||||
|
username: true,
|
||||||
|
firstName: true,
|
||||||
|
lastName: true,
|
||||||
|
avatarUrl: true,
|
||||||
|
impactScore: true,
|
||||||
|
createdAt: true,
|
||||||
|
updatedAt: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return updatedUser;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get user's reports
|
||||||
|
*/
|
||||||
|
async getUserReports(userId: string) {
|
||||||
|
const reports = await prisma.report.findMany({
|
||||||
|
where: { userId },
|
||||||
|
include: {
|
||||||
|
environmentalConcerns: true,
|
||||||
|
verifications: true,
|
||||||
|
},
|
||||||
|
orderBy: {
|
||||||
|
createdAt: 'desc',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return reports;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get user's events (both registered and organized)
|
||||||
|
*/
|
||||||
|
async getUserEvents(userId: string) {
|
||||||
|
// Get events user has registered for
|
||||||
|
const registeredEvents = await prisma.eventRegistration.findMany({
|
||||||
|
where: { userId },
|
||||||
|
include: {
|
||||||
|
event: {
|
||||||
|
include: {
|
||||||
|
organizer: {
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
username: true,
|
||||||
|
avatarUrl: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
orderBy: {
|
||||||
|
registeredAt: 'desc',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Get events user has organized
|
||||||
|
const organizedEvents = await prisma.event.findMany({
|
||||||
|
where: { organizerId: userId },
|
||||||
|
include: {
|
||||||
|
organizer: {
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
username: true,
|
||||||
|
avatarUrl: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
registrations: true,
|
||||||
|
},
|
||||||
|
orderBy: {
|
||||||
|
createdAt: 'desc',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
registered: registeredEvents.map(reg => ({
|
||||||
|
...reg.event,
|
||||||
|
registeredAt: reg.registeredAt,
|
||||||
|
attended: reg.attended,
|
||||||
|
})),
|
||||||
|
organized: organizedEvents,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update notification preferences
|
||||||
|
*/
|
||||||
|
async updateNotificationPreferences(
|
||||||
|
userId: string,
|
||||||
|
data: UpdateNotificationPreferencesData
|
||||||
|
) {
|
||||||
|
const updatedUser = await prisma.user.update({
|
||||||
|
where: { id: userId },
|
||||||
|
data: {
|
||||||
|
notificationEmail: data.notificationEmail,
|
||||||
|
notificationInApp: data.notificationInApp,
|
||||||
|
areasOfInterest: data.areasOfInterest ? JSON.stringify(data.areasOfInterest) : undefined,
|
||||||
|
},
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
notificationEmail: true,
|
||||||
|
notificationInApp: true,
|
||||||
|
areasOfInterest: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return updatedUser;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculate and update user impact score
|
||||||
|
* Impact score = (reports * 10) + (events attended * 20) + (events organized * 50)
|
||||||
|
*/
|
||||||
|
async calculateImpactScore(userId: string): Promise<number> {
|
||||||
|
const user = await prisma.user.findUnique({
|
||||||
|
where: { id: userId },
|
||||||
|
include: {
|
||||||
|
_count: {
|
||||||
|
select: {
|
||||||
|
reports: true,
|
||||||
|
eventRegistrations: {
|
||||||
|
where: { attended: true },
|
||||||
|
},
|
||||||
|
organizedEvents: {
|
||||||
|
where: { status: 'completed' },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
throw new Error('User not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
const impactScore =
|
||||||
|
user._count.reports * 10 +
|
||||||
|
user._count.eventRegistrations * 20 +
|
||||||
|
user._count.organizedEvents * 50;
|
||||||
|
|
||||||
|
// Update the impact score
|
||||||
|
await prisma.user.update({
|
||||||
|
where: { id: userId },
|
||||||
|
data: { impactScore },
|
||||||
|
});
|
||||||
|
|
||||||
|
return impactScore;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const userService = new UserService();
|
||||||
@@ -24,8 +24,16 @@ export interface UpdateProfileDTO {
|
|||||||
firstName?: string;
|
firstName?: string;
|
||||||
lastName?: string;
|
lastName?: string;
|
||||||
username?: string;
|
username?: string;
|
||||||
notificationEmail?: boolean;
|
}
|
||||||
notificationInApp?: boolean;
|
|
||||||
|
export interface NotificationPreferences {
|
||||||
|
notificationEmail: boolean;
|
||||||
|
notificationInApp: boolean;
|
||||||
|
areasOfInterest?: Array<{
|
||||||
|
lat: number;
|
||||||
|
lng: number;
|
||||||
|
radius: number;
|
||||||
|
}>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const userService = {
|
export const userService = {
|
||||||
@@ -63,4 +71,21 @@ export const userService = {
|
|||||||
async getUserActivity(userId: string): Promise<UserActivity[]> {
|
async getUserActivity(userId: string): Promise<UserActivity[]> {
|
||||||
return apiClient.get<UserActivity[]>(`/users/${userId}/activity`);
|
return apiClient.get<UserActivity[]>(`/users/${userId}/activity`);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update notification preferences
|
||||||
|
*/
|
||||||
|
async updateNotificationPreferences(
|
||||||
|
userId: string,
|
||||||
|
preferences: NotificationPreferences
|
||||||
|
): Promise<NotificationPreferences> {
|
||||||
|
return apiClient.patch<NotificationPreferences>(`/users/${userId}/preferences`, preferences);
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculate and update user impact score
|
||||||
|
*/
|
||||||
|
async calculateImpactScore(userId: string): Promise<{ impactScore: number }> {
|
||||||
|
return apiClient.post<{ impactScore: number }>(`/users/${userId}/calculate-impact`, {});
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user