feat: Implement user profile management and avatar upload functionality for regular users.
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)' })
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user