Skip to content

feat: implement user profile picture upload with Cloudinary integration#298

Merged
yusuftomilola merged 5 commits intoDistinctCodes:mainfrom
Jayking40:main
Oct 1, 2025
Merged

feat: implement user profile picture upload with Cloudinary integration#298
yusuftomilola merged 5 commits intoDistinctCodes:mainfrom
Jayking40:main

Conversation

@Jayking40
Copy link
Contributor

@Jayking40 Jayking40 commented Sep 30, 2025

🎨 Profile Picture Upload Feature

📋 Overview

This PR implements a complete profile picture upload system with Cloudinary integration, allowing users to upload, update, and manage their profile pictures with automatic image optimization and cleanup.

OnlyDust Wave #3 Contribution 🌟

✨ What's New

🔧 Core Implementation

  • New Endpoint: POST /users/:id/profile-picture for uploading profile pictures
  • Enhanced Endpoint: GET /users/:id now includes profilePicture URL in response
  • Database Schema: Added profilePicture column to User entity (nullable, varchar(500))
  • Cloudinary Integration: Full-featured service with upload, delete, and URL extraction capabilities

🛡️ Security & Validation

  • ✅ File type validation (JPG, PNG, WEBP only)
  • ✅ File size validation (max 5MB)
  • ✅ Authorization enforcement (users can only update their own profile picture)
  • ✅ UUID validation for user IDs
  • ✅ JWT authentication required

🚀 Features

  • Automatic Image Optimization: Images are automatically resized to 500x500px (max) with quality optimization
  • Smart Cleanup: Old profile pictures are automatically deleted from Cloudinary when users upload new ones
  • Error Resilience: Comprehensive error handling ensures failed cleanup doesn't break uploads
  • Production Ready: Proper logging, error messages, and edge case handling

📦 New Files Created

src/
├── cloudinary/
│ ├── cloudinary.module.ts # Cloudinary module configuration
│ └── cloudinary.service.ts # Upload/delete service with transformations
├── common/
│ └── pipes/
│ └── file-validation.pipe.ts # File type & size validation
├── config/
│ └── cloudinary.config.ts # Cloudinary provider setup
└── users/
└── dto/
└── update-user-profile-picture.dto.ts

🔄 Modified Files

src/
├── users/
│ ├── entities/user.entity.ts # Added profilePicture column
│ ├── users.controller.ts # New upload endpoint + updated GET
│ ├── users.service.ts # Profile picture update logic
│ └── users.module.ts # Import CloudinaryModule

🧪 Testing Completed

✅ Happy Path

  • User successfully uploads profile picture (first time)
  • User updates existing profile picture (old image deleted)
  • Profile picture URL returned in user queries
  • Images optimized and stored correctly in Cloudinary

✅ Validation & Security

  • Rejects non-image files (PDF, TXT, etc.)
  • Rejects oversized files (>5MB)
  • Rejects unauthorized users (can't update others' pictures)
  • Rejects requests without authentication
  • Validates UUID format for user IDs

✅ Edge Cases

  • Handles missing user gracefully
  • Handles Cloudinary service errors
  • Handles cleanup failures without breaking uploads
  • Handles malformed image files
FireShot Capture 142 - Swagger UI -  localhost FireShot Capture 141 - Swagger UI -  localhost FireShot Capture 140 - Swagger UI -  localhost image

🔥 Related Issue: #193

@vercel
Copy link

vercel bot commented Sep 30, 2025

@Jayking40 is attempting to deploy a commit to the naijabuz's projects Team on Vercel.

A member of the Team first needs to authorize it.

}

@Get(':id')
@UseGuards(JwtAuthGuard)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The JwtAuthGuard is already been applied globally in the app module.

Also, move this logic of the findOne into the usersService and so the controller calls it from here.

@UseGuards(JwtAuthGuard)
@UseInterceptors(FileInterceptor('file'))
@HttpCode(HttpStatus.OK)
async uploadProfilePicture(
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

create a dedicated provider and move the logic of the uploadProfilePicture into that provider, then add the provider to the usersService. So the controller remains clean and just calls that provider instead from the usersService. You can look through the existing architecture of the codebase and see the pattern.

No need to manually add the JwtAuthguard as it is implemented in the appmodule.

@yusuftomilola yusuftomilola merged commit df454e5 into DistinctCodes:main Oct 1, 2025
1 check failed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants