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)
|
@UseGuards(JwtAuthGuard, RolesGuard)
|
||||||
@Roles(UserRole.AGENT)
|
@Roles(UserRole.AGENT)
|
||||||
@ApiBearerAuth()
|
@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({
|
@ApiResponse({
|
||||||
status: 200,
|
status: 200,
|
||||||
description: 'Pre-signed URL generated successfully',
|
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')
|
@Delete(':key')
|
||||||
@UseGuards(JwtAuthGuard, RolesGuard)
|
@UseGuards(JwtAuthGuard, RolesGuard)
|
||||||
@Roles(UserRole.AGENT, UserRole.ADMIN)
|
@Roles(UserRole.AGENT, UserRole.ADMIN)
|
||||||
|
|||||||
@@ -179,6 +179,7 @@ export class UploadService {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Get presigned URL for avatar upload using agent profile ID as filename
|
* 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)
|
* This ensures the same agent's avatar is always overwritten (no orphan files)
|
||||||
*/
|
*/
|
||||||
async getAvatarPresignedUrl(
|
async getAvatarPresignedUrl(
|
||||||
@@ -189,9 +190,45 @@ export class UploadService {
|
|||||||
this.validateContentType(contentType, UploadFolder.AVATARS);
|
this.validateContentType(contentType, UploadFolder.AVATARS);
|
||||||
|
|
||||||
const extension = this.getFileExtension(filename);
|
const extension = this.getFileExtension(filename);
|
||||||
// Use agent profile ID as filename so it always overwrites
|
// S3 path: {env}/agents/{agentProfileId}/avatar.{ext}
|
||||||
const avatarFilename = `${agentProfileId}${extension ? `.${extension}` : ''}`;
|
const avatarFilename = `avatar${extension ? `.${extension}` : ''}`;
|
||||||
const key = this.getFullKey(UploadFolder.AVATARS, avatarFilename);
|
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({
|
const command = new PutObjectCommand({
|
||||||
Bucket: this.bucket,
|
Bucket: this.bucket,
|
||||||
|
|||||||
@@ -27,6 +27,62 @@ import { UserRole, UserStatus, VerificationStatus } from '@prisma/client';
|
|||||||
export class UsersController {
|
export class UsersController {
|
||||||
constructor(private readonly usersService: UsersService) {}
|
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()
|
@Get()
|
||||||
@Roles(UserRole.ADMIN)
|
@Roles(UserRole.ADMIN)
|
||||||
@ApiOperation({ summary: 'Get all users (Admin only)' })
|
@ApiOperation({ summary: 'Get all users (Admin only)' })
|
||||||
|
|||||||
@@ -10,6 +10,105 @@ type UserWithProfiles = Prisma.UserGetPayload<{
|
|||||||
export class UsersService {
|
export class UsersService {
|
||||||
constructor(private readonly prisma: PrismaService) {}
|
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) {
|
async findAll(page = 1, limit = 10, role?: UserRole) {
|
||||||
const skip = (page - 1) * limit;
|
const skip = (page - 1) * limit;
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user