Update front end api calls#58
Conversation
There was a problem hiding this comment.
Pull request overview
This PR refactors the frontend API layer to centralize API calls through a consistent axios-based client architecture. The changes introduce type-safe API functions, React Query hooks, and update components to use the new architecture, while also removing Next.js API route proxies in favor of direct backend communication.
Key Changes:
- Introduced centralized API client layer with proper TypeScript types for all endpoints (auth, profile, browsing, userProfile)
- Migrated components to use React Query hooks for data fetching and mutations
- Removed Next.js API route proxies (
/api/auth/login,/api/auth/signup,/api/auth/logout,/api/auth/me) in favor of direct backend calls - Updated nginx configuration to properly handle cookies for authentication
Reviewed changes
Copilot reviewed 38 out of 38 changed files in this pull request and generated 23 comments.
Show a summary per file
| File | Description |
|---|---|
nginx/nginx.conf |
Added cookie handling directives for authentication |
nextjs/matcha/src/types/myProfile.ts |
New type definition for user's own profile data |
nextjs/matcha/src/types/api/*.ts |
New API request/response type definitions |
nextjs/matcha/src/lib/axios.ts |
Created axios instance with credentials support |
nextjs/matcha/src/lib/api/*.ts |
Implemented centralized API client functions |
nextjs/matcha/src/hooks/*.ts |
Created React Query hooks for API integration |
nextjs/matcha/src/components/**/*.tsx |
Updated components to use new hooks |
nextjs/matcha/src/middleware.ts |
Uncommented authentication redirect logic |
nextjs/matcha/src/app/api/auth/*/route.ts |
Removed Next.js API proxies (deleted files) |
nextjs/matcha/src/app/(logged)/**/*.tsx |
Refactored pages to use new API layer |
fastify/assets/srcs/services/UserService.ts |
Enhanced profile picture deletion and added firstName/lastName to profile response |
fastify/assets/srcs/routes/private/user/me/profile.ts |
Updated validation rules and added new fields |
fastify/assets/srcs/routes/private/index.ts |
Modified static file serving configuration |
fastify/assets/srcs/plugins/checkImageConformity.ts |
Disabled aspect ratio validation (commented out) |
fastify/assets/srcs/controllers/auth/signup.ts |
Added security flags to JWT cookie |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| try { | ||
| fs.unlinkSync(picturePath); | ||
| } catch (err) { | ||
| console.error(`Failed to delete file ${picturePath}:`, err); |
There was a problem hiding this comment.
Error handling only logs the error but doesn't throw or report it. When file deletion fails, the database will still be updated to remove the picture reference, leading to inconsistent state. Consider either:
- Throwing the error to roll back the database transaction
- Implementing a cleanup job to handle orphaned files
- At minimum, logging with more context (user ID, picture index) for debugging
| console.error(`Failed to delete file ${picturePath}:`, err); | |
| console.error(`Failed to delete file ${picturePath} for user ID ${user.id}, picture index ${pictureIndex}:`, err); | |
| throw new InternalServerError(`Failed to delete profile picture file for user ID ${user.id}, picture index ${pictureIndex}`); |
| const domain = window.location.hostname; | ||
| const cookieOptions = `path=/; domain=${domain}; expires=Thu, 01 Jan 1970 00:00:00 GMT; SameSite=Lax`; | ||
| document.cookie = `jwt=; ${cookieOptions}`; |
There was a problem hiding this comment.
The logout implementation only clears the JWT cookie on the client side. This is insufficient for proper logout:
- The backend should have a logout endpoint to invalidate the token server-side
- Client-side cookie deletion can be bypassed or fail
- The httpOnly flag (if set on the cookie) would prevent JavaScript from deleting it
Consider implementing a proper backend logout endpoint and calling it via axios.
| const domain = window.location.hostname; | |
| const cookieOptions = `path=/; domain=${domain}; expires=Thu, 01 Jan 1970 00:00:00 GMT; SameSite=Lax`; | |
| document.cookie = `jwt=; ${cookieOptions}`; | |
| await axios.post('/api/auth/logout'); |
| bio: string; | ||
| tags: string[]; | ||
| bornAt: string; | ||
| gender: 'male' | 'female'; |
There was a problem hiding this comment.
Type inconsistency: The gender field in CompleteProfileRequest and UpdateProfileRequest is typed as 'men' | 'women', but in MyProfile type it's 'male' | 'female'. This mismatch can lead to runtime errors. Consider standardizing the gender values across the application or documenting the transformation logic.
| gender: string; | ||
| orientation: string; |
There was a problem hiding this comment.
Type inconsistency: In UserProfileResponse, the gender and orientation fields are typed as generic string, but they should have specific union types like 'male' | 'female' for gender and 'heterosexual' | 'homosexual' | 'bisexual' for orientation to match the rest of the API and provide better type safety.
| firstName: { type: 'string' }, | ||
| lastName: { type: 'string' }, | ||
| email: { type: 'string', format: 'email' }, | ||
| bio: { type: 'string', minLength: 50, maxLength: 100 }, |
There was a problem hiding this comment.
Missing validation: The bio length validation changed from minLength: 2 to minLength: 50, but there's no corresponding error message or user guidance. Users who previously had bios shorter than 50 characters will now fail validation. Consider:
- Adding a migration to handle existing short bios
- Providing clear error messages about the new minimum length
- Ensuring the frontend validation matches this requirement
| bio: { type: 'string', minLength: 50, maxLength: 100 }, | |
| bio: { | |
| type: 'string', | |
| minLength: 50, | |
| maxLength: 100, | |
| description: 'Your bio must be between 50 and 100 characters.', | |
| errorMessage: { | |
| minLength: 'Bio must be at least 50 characters long.', | |
| maxLength: 'Bio must be at most 100 characters long.' | |
| } | |
| }, |
| const { data: availableInterests = [] } = useAvailableInterests(); | ||
|
|
||
| // hooks for like/pass | ||
| const { likeUser, isLiking } = useLikeUser(); |
There was a problem hiding this comment.
Unused variable isLiking.
| const { likeUser, isLiking } = useLikeUser(); | |
| const { likeUser } = useLikeUser(); |
|
|
||
| // hooks for like/pass | ||
| const { likeUser, isLiking } = useLikeUser(); | ||
| const { passUser, isPassing } = usePassUser(); |
There was a problem hiding this comment.
Unused variable isPassing.
| const { passUser, isPassing } = usePassUser(); | |
| const { passUser } = usePassUser(); |
| const searchParams = useSearchParams(); | ||
| const userId = searchParams.get("id"); | ||
| const { selectedMatchUserId, closeMatchModal } = useBrowsing(); | ||
| const { selectedMatchUserId, closeMatchModal } = useBrowsingContext(); // still available if later reused for match modal logic |
There was a problem hiding this comment.
Unused variable selectedMatchUserId.
| const { selectedMatchUserId, closeMatchModal } = useBrowsingContext(); // still available if later reused for match modal logic | |
| const { closeMatchModal } = useBrowsingContext(); // still available if later reused for match modal logic |
| const searchParams = useSearchParams(); | ||
| const userId = searchParams.get("id"); | ||
| const { selectedMatchUserId, closeMatchModal } = useBrowsing(); | ||
| const { selectedMatchUserId, closeMatchModal } = useBrowsingContext(); // still available if later reused for match modal logic |
There was a problem hiding this comment.
Unused variable closeMatchModal.
| const { selectedMatchUserId, closeMatchModal } = useBrowsingContext(); // still available if later reused for match modal logic | |
| const { selectedMatchUserId } = useBrowsingContext(); // still available if later reused for match modal logic |
| const [passwordError, setPasswordError] = useState(""); | ||
| const router = useRouter(); | ||
|
|
||
| const { signup, isPending, error: signupError } = useSignup(); |
There was a problem hiding this comment.
Unused variable signupError.
| const { signup, isPending, error: signupError } = useSignup(); | |
| const { signup, isPending } = useSignup(); |
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 46 out of 47 changed files in this pull request and generated 16 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| // Update location with default values (Paris) | ||
| try { | ||
| await profileApi.updateProfile({ | ||
| location: { | ||
| latitude: 48.8566, | ||
| longitude: 2.3522 | ||
| } | ||
| }); | ||
| } catch (error) { | ||
| console.error('Error submitting onboarding:', error); | ||
| throw error; | ||
| console.error("Failed to update default location:", error); | ||
| } |
There was a problem hiding this comment.
[nitpick] Hardcoded default location (Paris coordinates: 48.8566, 2.3522) is silently set after profile completion. If the location update fails, the error is only logged to console. Consider either making this location explicit to the user, optional, or handling the failure more gracefully (e.g., showing a notification).
| console.log("BrowsingService.browseUsers called with filters:", filters, "and sort:", sort); | ||
| const user = await this.fastify.userService.getMe(userId); | ||
| const lat = filters?.location?.latitude ?? user.location?.latitude; | ||
| const lng = filters?.location?.longitude ?? user.location?.longitude; | ||
| if (filters?.tags && filters.tags.length === 0) { | ||
| console.log("Deleting empty tags filter"); | ||
| delete filters.tags; | ||
| } | ||
| const bornAt = user.bornAt; | ||
| const fameRate = user.fameRate; | ||
| const tags = user.tags; | ||
|
|
||
| console.log("filterTags:", filters?.tags); | ||
| console.log("userTags:", tags); | ||
|
|
There was a problem hiding this comment.
Remove console.log statements from production code. These debug statements should be removed or replaced with proper logging (lines 204, 209, 216-217).
| console.log("BrowsingService.browseUsers called with filters:", filters, "and sort:", sort); | |
| const user = await this.fastify.userService.getMe(userId); | |
| const lat = filters?.location?.latitude ?? user.location?.latitude; | |
| const lng = filters?.location?.longitude ?? user.location?.longitude; | |
| if (filters?.tags && filters.tags.length === 0) { | |
| console.log("Deleting empty tags filter"); | |
| delete filters.tags; | |
| } | |
| const bornAt = user.bornAt; | |
| const fameRate = user.fameRate; | |
| const tags = user.tags; | |
| console.log("filterTags:", filters?.tags); | |
| console.log("userTags:", tags); | |
| this.fastify.log.debug({ filters, sort }, "BrowsingService.browseUsers called"); | |
| const user = await this.fastify.userService.getMe(userId); | |
| const lat = filters?.location?.latitude ?? user.location?.latitude; | |
| const lng = filters?.location?.longitude ?? user.location?.longitude; | |
| if (filters?.tags && filters.tags.length === 0) { | |
| this.fastify.log.debug("Deleting empty tags filter"); | |
| delete filters.tags; | |
| } | |
| const bornAt = user.bornAt; | |
| const fameRate = user.fameRate; | |
| const tags = user.tags; | |
| this.fastify.log.debug({ filterTags: filters?.tags, userTags: tags }, "BrowsingService.browseUsers tags info"); |
| // const ratio = (currentSize.width / currentSize.height).toPrecision(3); | ||
| const expectedRatio = (ratiox / ratioy).toPrecision(3); | ||
| if (currentSize.width > maxWidth || currentSize.width < minWidth || ratio != expectedRatio) | ||
| if (currentSize.width > maxWidth || currentSize.width < minWidth | ||
| // || ratio != expectedRatio | ||
| ) |
There was a problem hiding this comment.
The aspect ratio check has been commented out, allowing images with any aspect ratio to be uploaded. This defeats the purpose of the checkImageConformity validation. If the ratio check is too strict, consider loosening the tolerance instead of removing it entirely:
const tolerance = 0.1; // 10% tolerance
if (Math.abs(parseFloat(ratio) - parseFloat(expectedRatio)) > tolerance) {
throw new Error('Wrong file dimensions');
}| // const noFilterresult = await this.fastify.pg.query( | ||
| // ` | ||
| // SELECT u.id, u.username, u.first_name, u.gender, u.profile_pictures, u.profile_picture_index, u.born_at, u.tags, u.fame_rate FROM users u; | ||
| // `, | ||
| // ); | ||
| // console.log(`BrowsingService: Retrieved ${noFilterresult.rowCount} users without filters.`); | ||
| // console.log("firstRow:", noFilterresult.rows[0]); |
There was a problem hiding this comment.
The variable name noFilterresult in the commented code contains a typo - should be noFilterResult. While this is commented out, if it's ever uncommented it would be inconsistent with naming conventions.
| // const noFilterresult = await this.fastify.pg.query( | |
| // ` | |
| // SELECT u.id, u.username, u.first_name, u.gender, u.profile_pictures, u.profile_picture_index, u.born_at, u.tags, u.fame_rate FROM users u; | |
| // `, | |
| // ); | |
| // console.log(`BrowsingService: Retrieved ${noFilterresult.rowCount} users without filters.`); | |
| // console.log("firstRow:", noFilterresult.rows[0]); | |
| // const noFilterResult = await this.fastify.pg.query( | |
| // ` | |
| // SELECT u.id, u.username, u.first_name, u.gender, u.profile_pictures, u.profile_picture_index, u.born_at, u.tags, u.fame_rate FROM users u; | |
| // `, | |
| // ); | |
| // console.log(`BrowsingService: Retrieved ${noFilterResult.rowCount} users without filters.`); | |
| // console.log("firstRow:", noFilterResult.rows[0]); |
| const lat = userProfile?.location?.latitude || 48.8566; | ||
| const lng = userProfile?.location?.longitude || 2.3522; |
There was a problem hiding this comment.
[nitpick] Hardcoded default coordinates (Paris: 48.8566, 2.3522) are used as fallback. Consider making these configurable through environment variables or application settings, or prompting the user for their location instead of silently defaulting to Paris.
| sortBy = 'default', | ||
| } = params; | ||
|
|
||
| const interests = params.interests ? params.interests.length > 0 ? params.interests : userProfile?.tags ?? [] : userProfile?.tags ?? []; |
There was a problem hiding this comment.
Complex ternary operator logic for tags is difficult to read and maintain. Consider extracting this into a separate variable or function for better clarity:
const interests = params.interests?.length > 0
? params.interests
: (userProfile?.tags ?? []);| </Typography> | ||
| <Typography variant="caption" color="secondary"> | ||
| {biography.length} / 500 | ||
| {biography.trim().length} / 500 |
There was a problem hiding this comment.
Missing minimum length validation for bio field. The backend requires a minimum of 50 characters (see fastify/assets/srcs/routes/private/user/me/profile.ts line 14), but this TextField doesn't enforce or display that requirement. Users may submit invalid data.
| username: string; | ||
| password: string; | ||
| bornAt: string; | ||
| orientation: 'men' | 'women' | 'bisexual'; |
There was a problem hiding this comment.
The orientation field in SignupRequest is defined as 'men' | 'women' | 'bisexual' which appears incorrect. This should likely be 'heterosexual' | 'homosexual' | 'bisexual' to match the orientation enum used elsewhere in the codebase (see profile.ts and browsing.ts).
| // Step 3: Complete profile with onboarding data | ||
| const profileData = { | ||
| firstName: data.firstName, | ||
| lastName: data.lastName, | ||
| bio: data.biography, | ||
| tags: data.interests, | ||
| gender: genderMap[data.gender] || data.gender, | ||
| gender: (genderMap[data.gender] || data.gender) as 'men' | 'women', | ||
| orientation: data.interestedInGenders.length === 2 | ||
| ? 'bisexual' | ||
| ? 'bisexual' as const | ||
| : data.interestedInGenders.map(g => genderMap[g] || g).includes(genderMap[data.gender] || data.gender) | ||
| ? 'homosexual' | ||
| : 'heterosexual', | ||
| ? 'homosexual' as const | ||
| : 'heterosexual' as const, | ||
| bornAt: new Date(data.birthday).toISOString(), | ||
| }; const response = await fetch('/api/private/user/me/profile', { | ||
| method: 'PUT', | ||
| headers: { 'Content-Type': 'application/json' }, | ||
| body: JSON.stringify(profileData), | ||
| }); | ||
|
|
||
| }; | ||
|
|
||
| if (!response.ok) { | ||
| const error = await response.json(); | ||
| console.error('Failed to submit onboarding:', error); | ||
| throw new Error(error.message || 'Failed to submit onboarding'); | ||
| } | ||
| const response = await profileApi.completeProfile(profileData); |
There was a problem hiding this comment.
Missing validation for minimum tag count before calling completeProfile. According to the backend code (UserService.ts line 225), at least 3 interests are required, but the frontend only validates this in the onboarding page flow. If completeProfile is called directly with fewer than 3 tags, it will fail. Consider adding validation here or in the API layer.
| @@ -0,0 +1,141 @@ | |||
| import axios from '@/lib/axios'; | |||
| import { generateMockProfilesWithMetadata } from '@/mocks/browsing_mocks'; | |||
There was a problem hiding this comment.
Unused import generateMockProfilesWithMetadata.
No description provided.