feat: Implement user profile management and avatar upload functionality for regular users.

This commit is contained in:
pradeepkumar
2026-02-01 00:50:46 +05:30
parent eece0f7401
commit 6624b35e59
4 changed files with 218 additions and 4 deletions

View File

@@ -57,7 +57,7 @@ export class UploadController {
@UseGuards(JwtAuthGuard, RolesGuard)
@Roles(UserRole.AGENT)
@ApiBearerAuth()
@ApiOperation({ summary: 'Get a pre-signed URL for avatar upload (uses agent ID as filename)' })
@ApiOperation({ summary: 'Get a pre-signed URL for agent avatar upload (S3 path: agents/{agentId}/avatar)' })
@ApiResponse({
status: 200,
description: 'Pre-signed URL generated successfully',
@@ -86,6 +86,28 @@ export class UploadController {
);
}
@Post('user-avatar-presigned-url')
@UseGuards(JwtAuthGuard, RolesGuard)
@Roles(UserRole.USER)
@ApiBearerAuth()
@ApiOperation({ summary: 'Get a pre-signed URL for user avatar upload (S3 path: users/{userId}/avatar)' })
@ApiResponse({
status: 200,
description: 'Pre-signed URL generated successfully',
type: PresignedUrlResponseDto,
})
@ApiResponse({ status: 400, description: 'Invalid content type' })
async getUserAvatarPresignedUrl(
@CurrentUser('id') userId: string,
@Body() dto: AvatarPresignedUrlDto,
): Promise<PresignedUrlResponseDto> {
return this.uploadService.getUserAvatarPresignedUrl(
userId,
dto.filename,
dto.contentType,
);
}
@Delete(':key')
@UseGuards(JwtAuthGuard, RolesGuard)
@Roles(UserRole.AGENT, UserRole.ADMIN)

View File

@@ -179,6 +179,7 @@ export class UploadService {
/**
* Get presigned URL for avatar upload using agent profile ID as filename
* S3 path: {env}/agents/{agentProfileId}/avatar.{ext}
* This ensures the same agent's avatar is always overwritten (no orphan files)
*/
async getAvatarPresignedUrl(
@@ -189,9 +190,45 @@ export class UploadService {
this.validateContentType(contentType, UploadFolder.AVATARS);
const extension = this.getFileExtension(filename);
// Use agent profile ID as filename so it always overwrites
const avatarFilename = `${agentProfileId}${extension ? `.${extension}` : ''}`;
const key = this.getFullKey(UploadFolder.AVATARS, avatarFilename);
// S3 path: {env}/agents/{agentProfileId}/avatar.{ext}
const avatarFilename = `avatar${extension ? `.${extension}` : ''}`;
const key = `${this.folderPrefix}/agents/${agentProfileId}/${avatarFilename}`;
const command = new PutObjectCommand({
Bucket: this.bucket,
Key: key,
ContentType: contentType,
});
const uploadUrl = await getSignedUrl(this.s3Client, command, {
expiresIn: 3600, // 1 hour
});
const publicUrl = this.getPublicUrl(key);
return {
uploadUrl,
publicUrl,
key,
};
}
/**
* Get presigned URL for user avatar upload using user ID as folder
* S3 path: {env}/users/{userId}/avatar.{ext}
* This ensures the same user's avatar is always overwritten (no orphan files)
*/
async getUserAvatarPresignedUrl(
userId: string,
filename: string,
contentType: string,
): Promise<PresignedUrlResponseDto> {
this.validateContentType(contentType, UploadFolder.AVATARS);
const extension = this.getFileExtension(filename);
// S3 path: {env}/users/{userId}/avatar.{ext}
const avatarFilename = `avatar${extension ? `.${extension}` : ''}`;
const key = `${this.folderPrefix}/users/${userId}/${avatarFilename}`;
const command = new PutObjectCommand({
Bucket: this.bucket,

View File

@@ -27,6 +27,62 @@ import { UserRole, UserStatus, VerificationStatus } from '@prisma/client';
export class UsersController {
constructor(private readonly usersService: UsersService) {}
// =============================================
// USER PROFILE ENDPOINTS (for regular users)
// =============================================
@Get('profile/me')
@Roles(UserRole.USER)
@ApiOperation({ summary: 'Get current user profile (Regular users only)' })
@ApiResponse({
status: 200,
description: 'User profile retrieved',
schema: {
example: {
id: 'uuid',
userId: 'uuid',
email: 'user@example.com',
firstName: 'John',
lastName: 'Doe',
phone: '+1234567890',
avatar: 'users/uuid/avatar/image.jpg',
city: 'New York',
state: 'NY',
country: 'USA',
},
},
})
async getMyProfile(@CurrentUser() user: { id: string }) {
return this.usersService.getMyUserProfile(user.id);
}
@Patch('profile/me')
@Roles(UserRole.USER)
@ApiOperation({ summary: 'Update current user profile (Regular users only)' })
@ApiResponse({
status: 200,
description: 'User profile updated',
})
async updateMyProfile(
@CurrentUser() user: { id: string },
@Body()
data: {
firstName?: string;
lastName?: string;
phone?: string;
avatar?: string | null;
city?: string;
state?: string;
country?: string;
},
) {
return this.usersService.updateMyUserProfile(user.id, data);
}
// =============================================
// ADMIN ENDPOINTS
// =============================================
@Get()
@Roles(UserRole.ADMIN)
@ApiOperation({ summary: 'Get all users (Admin only)' })

View File

@@ -10,6 +10,105 @@ type UserWithProfiles = Prisma.UserGetPayload<{
export class UsersService {
constructor(private readonly prisma: PrismaService) {}
// =============================================
// USER PROFILE METHODS (for regular users)
// =============================================
async getMyUserProfile(userId: string) {
const user = await this.prisma.user.findUnique({
where: { id: userId },
include: { userProfile: true },
});
if (!user) {
throw new NotFoundException('User not found');
}
if (user.role !== UserRole.USER) {
throw new BadRequestException('This endpoint is for regular users only');
}
// Create user profile if it doesn't exist
let profile = user.userProfile;
if (!profile) {
profile = await this.prisma.userProfile.create({
data: {
userId: user.id,
},
});
}
return {
id: profile.id,
userId: user.id,
email: user.email,
firstName: profile.firstName,
lastName: profile.lastName,
phone: profile.phone,
avatar: profile.avatar,
city: profile.city,
state: profile.state,
country: profile.country,
createdAt: profile.createdAt,
updatedAt: profile.updatedAt,
};
}
async updateMyUserProfile(
userId: string,
data: {
firstName?: string;
lastName?: string;
phone?: string;
avatar?: string | null;
city?: string;
state?: string;
country?: string;
},
) {
const user = await this.prisma.user.findUnique({
where: { id: userId },
include: { userProfile: true },
});
if (!user) {
throw new NotFoundException('User not found');
}
if (user.role !== UserRole.USER) {
throw new BadRequestException('This endpoint is for regular users only');
}
// Create or update user profile
const profile = await this.prisma.userProfile.upsert({
where: { userId },
create: {
userId,
...data,
},
update: data,
});
return {
id: profile.id,
userId: user.id,
email: user.email,
firstName: profile.firstName,
lastName: profile.lastName,
phone: profile.phone,
avatar: profile.avatar,
city: profile.city,
state: profile.state,
country: profile.country,
createdAt: profile.createdAt,
updatedAt: profile.updatedAt,
};
}
// =============================================
// ADMIN METHODS
// =============================================
async findAll(page = 1, limit = 10, role?: UserRole) {
const skip = (page - 1) * limit;