This commit is contained in:
pradeepkumar
2026-01-24 23:10:05 +05:30
parent 9c7e21e1ad
commit f031113781
4 changed files with 161 additions and 19 deletions

View File

@@ -80,14 +80,15 @@ export class AgentsController {
return this.agentsService.getProfile(userId);
}
// Public parameterized route - must come after specific routes
@Get(':id')
@Public()
@ApiOperation({ summary: 'Get agent profile by ID' })
@ApiResponse({ status: 200, description: 'Agent profile retrieved successfully' })
@ApiResponse({ status: 404, description: 'Agent profile not found' })
async getAgentById(@Param('id', ParseUUIDPipe) id: string) {
return this.agentsService.getProfileById(id);
@Get('profile/field-values')
@UseGuards(JwtAuthGuard, RolesGuard)
@Roles(UserRole.AGENT)
@ApiBearerAuth()
@ApiOperation({ summary: 'Get dynamic field values for agent profile' })
@ApiResponse({ status: 200, description: 'Field values retrieved successfully' })
@ApiResponse({ status: 404, description: 'Profile not found' })
async getFieldValues(@CurrentUser('id') userId: string) {
return this.agentsService.getFieldValues(userId);
}
@Post('profile')
@@ -132,15 +133,23 @@ export class AgentsController {
return this.agentsService.saveFieldValues(userId, dto);
}
@Get('profile/field-values')
@UseGuards(JwtAuthGuard, RolesGuard)
@Roles(UserRole.AGENT)
@ApiBearerAuth()
@ApiOperation({ summary: 'Get dynamic field values for agent profile' })
// Public parameterized routes - MUST come after specific routes like profile/*
@Get(':id')
@Public()
@ApiOperation({ summary: 'Get agent profile by ID' })
@ApiResponse({ status: 200, description: 'Agent profile retrieved successfully' })
@ApiResponse({ status: 404, description: 'Agent profile not found' })
async getAgentById(@Param('id', ParseUUIDPipe) id: string) {
return this.agentsService.getProfileById(id);
}
@Get(':id/field-values')
@Public()
@ApiOperation({ summary: 'Get field values for an agent profile by ID (public)' })
@ApiResponse({ status: 200, description: 'Field values retrieved successfully' })
@ApiResponse({ status: 404, description: 'Profile not found' })
async getFieldValues(@CurrentUser('id') userId: string) {
return this.agentsService.getFieldValues(userId);
@ApiResponse({ status: 404, description: 'Agent profile not found' })
async getFieldValuesByAgentId(@Param('id', ParseUUIDPipe) id: string) {
return this.agentsService.getFieldValuesByAgentId(id);
}
// Admin endpoints

View File

@@ -498,6 +498,54 @@ export class AgentsService {
};
}
async getFieldValuesByAgentId(agentId: string) {
// Get agent profile by ID
const profile = await this.prisma.agentProfile.findUnique({
where: { id: agentId },
});
if (!profile) {
throw new NotFoundException('Agent profile not found');
}
// Get all field values for this profile
const fieldValues = await this.prisma.agentProfileFieldValue.findMany({
where: { agentProfileId: profile.id },
include: {
field: {
select: {
id: true,
slug: true,
name: true,
fieldType: true,
section: {
select: {
id: true,
slug: true,
name: true,
},
},
},
},
},
});
// Transform to a more usable format
const transformedValues = fieldValues.map((fv) => ({
fieldSlug: fv.field.slug,
fieldName: fv.field.name,
fieldType: fv.field.fieldType,
sectionSlug: fv.field.section.slug,
sectionName: fv.field.section.name,
value: this.extractValue(fv),
}));
return {
agentProfileId: profile.id,
fieldValues: transformedValues,
};
}
private getValueData(
fieldType: string,
value: string | number | boolean | string[] | Record<string, unknown> | null,

View File

@@ -2,17 +2,23 @@ import {
Controller,
Post,
Delete,
Get,
Body,
Param,
Query,
UseGuards,
NotFoundException,
Res,
BadRequestException,
} from '@nestjs/common';
import {
ApiTags,
ApiOperation,
ApiResponse,
ApiBearerAuth,
ApiQuery,
} from '@nestjs/swagger';
import type { Response } from 'express';
import { UploadService } from './upload.service';
import { PresignedUrlDto, PresignedUrlResponseDto, AvatarPresignedUrlDto } from './dto';
import { JwtAuthGuard } from '../auth/guards/jwt-auth.guard';
@@ -24,9 +30,6 @@ import { PrismaService } from '../prisma/prisma.service';
@ApiTags('upload')
@Controller('upload')
@UseGuards(JwtAuthGuard, RolesGuard)
@Roles(UserRole.AGENT, UserRole.ADMIN)
@ApiBearerAuth()
export class UploadController {
constructor(
private readonly uploadService: UploadService,
@@ -34,6 +37,9 @@ export class UploadController {
) {}
@Post('presigned-url')
@UseGuards(JwtAuthGuard, RolesGuard)
@Roles(UserRole.AGENT, UserRole.ADMIN)
@ApiBearerAuth()
@ApiOperation({ summary: 'Get a pre-signed URL for file upload' })
@ApiResponse({
status: 200,
@@ -48,7 +54,9 @@ export class UploadController {
}
@Post('avatar-presigned-url')
@UseGuards(JwtAuthGuard, RolesGuard)
@Roles(UserRole.AGENT)
@ApiBearerAuth()
@ApiOperation({ summary: 'Get a pre-signed URL for avatar upload (uses agent ID as filename)' })
@ApiResponse({
status: 200,
@@ -79,10 +87,34 @@ export class UploadController {
}
@Delete(':key')
@UseGuards(JwtAuthGuard, RolesGuard)
@Roles(UserRole.AGENT, UserRole.ADMIN)
@ApiBearerAuth()
@ApiOperation({ summary: 'Delete an uploaded file' })
@ApiResponse({ status: 200, description: 'File deleted successfully' })
async deleteFile(@Param('key') key: string) {
await this.uploadService.deleteFile(key);
return { message: 'File deleted successfully' };
}
@Get('presigned-download-url')
@ApiOperation({ summary: 'Get a presigned download URL for an S3 key (public endpoint)' })
@ApiQuery({ name: 'key', required: true, description: 'S3 key (e.g., avatars/uuid.png)' })
@ApiResponse({ status: 200, description: 'Presigned URL returned successfully' })
@ApiResponse({ status: 400, description: 'Key is required' })
async getPresignedDownloadUrl(
@Query('key') key: string,
) {
if (!key) {
throw new BadRequestException('Key is required');
}
try {
const url = await this.uploadService.getSignedDownloadUrl(key);
return { url };
} catch (error) {
console.error('Error generating presigned URL:', error);
throw new NotFoundException('File not found');
}
}
}

View File

@@ -211,4 +211,57 @@ export class UploadService {
key,
};
}
/**
* Extract S3 key from a full S3 URL
*/
private extractKeyFromUrl(url: string): string {
try {
const urlObj = new URL(url);
// Remove leading slash from pathname
let key = urlObj.pathname.replace(/^\//, '');
// For Contabo/path-style URLs: endpoint/bucket/key
// The bucket name might be in the path, so we need to remove it
if (this.endpoint && key.startsWith(this.bucket + '/')) {
key = key.substring(this.bucket.length + 1);
}
return key;
} catch {
// If URL parsing fails, assume it's already a key
return url;
}
}
/**
* Fetch image from S3 and return as buffer
*/
async getImageFromUrl(url: string): Promise<{
buffer: Buffer;
contentType: string;
contentLength: number;
}> {
const key = this.extractKeyFromUrl(url);
const command = new GetObjectCommand({
Bucket: this.bucket,
Key: key,
});
const response = await this.s3Client.send(command);
// Convert readable stream to buffer
const chunks: Uint8Array[] = [];
for await (const chunk of response.Body as AsyncIterable<Uint8Array>) {
chunks.push(chunk);
}
const buffer = Buffer.concat(chunks);
return {
buffer,
contentType: response.ContentType || 'image/jpeg',
contentLength: response.ContentLength || buffer.length,
};
}
}