s3 fix
This commit is contained in:
@@ -80,14 +80,15 @@ export class AgentsController {
|
|||||||
return this.agentsService.getProfile(userId);
|
return this.agentsService.getProfile(userId);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Public parameterized route - must come after specific routes
|
@Get('profile/field-values')
|
||||||
@Get(':id')
|
@UseGuards(JwtAuthGuard, RolesGuard)
|
||||||
@Public()
|
@Roles(UserRole.AGENT)
|
||||||
@ApiOperation({ summary: 'Get agent profile by ID' })
|
@ApiBearerAuth()
|
||||||
@ApiResponse({ status: 200, description: 'Agent profile retrieved successfully' })
|
@ApiOperation({ summary: 'Get dynamic field values for agent profile' })
|
||||||
@ApiResponse({ status: 404, description: 'Agent profile not found' })
|
@ApiResponse({ status: 200, description: 'Field values retrieved successfully' })
|
||||||
async getAgentById(@Param('id', ParseUUIDPipe) id: string) {
|
@ApiResponse({ status: 404, description: 'Profile not found' })
|
||||||
return this.agentsService.getProfileById(id);
|
async getFieldValues(@CurrentUser('id') userId: string) {
|
||||||
|
return this.agentsService.getFieldValues(userId);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Post('profile')
|
@Post('profile')
|
||||||
@@ -132,15 +133,23 @@ export class AgentsController {
|
|||||||
return this.agentsService.saveFieldValues(userId, dto);
|
return this.agentsService.saveFieldValues(userId, dto);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Get('profile/field-values')
|
// Public parameterized routes - MUST come after specific routes like profile/*
|
||||||
@UseGuards(JwtAuthGuard, RolesGuard)
|
@Get(':id')
|
||||||
@Roles(UserRole.AGENT)
|
@Public()
|
||||||
@ApiBearerAuth()
|
@ApiOperation({ summary: 'Get agent profile by ID' })
|
||||||
@ApiOperation({ summary: 'Get dynamic field values for agent profile' })
|
@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: 200, description: 'Field values retrieved successfully' })
|
||||||
@ApiResponse({ status: 404, description: 'Profile not found' })
|
@ApiResponse({ status: 404, description: 'Agent profile not found' })
|
||||||
async getFieldValues(@CurrentUser('id') userId: string) {
|
async getFieldValuesByAgentId(@Param('id', ParseUUIDPipe) id: string) {
|
||||||
return this.agentsService.getFieldValues(userId);
|
return this.agentsService.getFieldValuesByAgentId(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Admin endpoints
|
// Admin endpoints
|
||||||
|
|||||||
@@ -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(
|
private getValueData(
|
||||||
fieldType: string,
|
fieldType: string,
|
||||||
value: string | number | boolean | string[] | Record<string, unknown> | null,
|
value: string | number | boolean | string[] | Record<string, unknown> | null,
|
||||||
|
|||||||
@@ -2,17 +2,23 @@ import {
|
|||||||
Controller,
|
Controller,
|
||||||
Post,
|
Post,
|
||||||
Delete,
|
Delete,
|
||||||
|
Get,
|
||||||
Body,
|
Body,
|
||||||
Param,
|
Param,
|
||||||
|
Query,
|
||||||
UseGuards,
|
UseGuards,
|
||||||
NotFoundException,
|
NotFoundException,
|
||||||
|
Res,
|
||||||
|
BadRequestException,
|
||||||
} from '@nestjs/common';
|
} from '@nestjs/common';
|
||||||
import {
|
import {
|
||||||
ApiTags,
|
ApiTags,
|
||||||
ApiOperation,
|
ApiOperation,
|
||||||
ApiResponse,
|
ApiResponse,
|
||||||
ApiBearerAuth,
|
ApiBearerAuth,
|
||||||
|
ApiQuery,
|
||||||
} from '@nestjs/swagger';
|
} from '@nestjs/swagger';
|
||||||
|
import type { Response } from 'express';
|
||||||
import { UploadService } from './upload.service';
|
import { UploadService } from './upload.service';
|
||||||
import { PresignedUrlDto, PresignedUrlResponseDto, AvatarPresignedUrlDto } from './dto';
|
import { PresignedUrlDto, PresignedUrlResponseDto, AvatarPresignedUrlDto } from './dto';
|
||||||
import { JwtAuthGuard } from '../auth/guards/jwt-auth.guard';
|
import { JwtAuthGuard } from '../auth/guards/jwt-auth.guard';
|
||||||
@@ -24,9 +30,6 @@ import { PrismaService } from '../prisma/prisma.service';
|
|||||||
|
|
||||||
@ApiTags('upload')
|
@ApiTags('upload')
|
||||||
@Controller('upload')
|
@Controller('upload')
|
||||||
@UseGuards(JwtAuthGuard, RolesGuard)
|
|
||||||
@Roles(UserRole.AGENT, UserRole.ADMIN)
|
|
||||||
@ApiBearerAuth()
|
|
||||||
export class UploadController {
|
export class UploadController {
|
||||||
constructor(
|
constructor(
|
||||||
private readonly uploadService: UploadService,
|
private readonly uploadService: UploadService,
|
||||||
@@ -34,6 +37,9 @@ export class UploadController {
|
|||||||
) {}
|
) {}
|
||||||
|
|
||||||
@Post('presigned-url')
|
@Post('presigned-url')
|
||||||
|
@UseGuards(JwtAuthGuard, RolesGuard)
|
||||||
|
@Roles(UserRole.AGENT, UserRole.ADMIN)
|
||||||
|
@ApiBearerAuth()
|
||||||
@ApiOperation({ summary: 'Get a pre-signed URL for file upload' })
|
@ApiOperation({ summary: 'Get a pre-signed URL for file upload' })
|
||||||
@ApiResponse({
|
@ApiResponse({
|
||||||
status: 200,
|
status: 200,
|
||||||
@@ -48,7 +54,9 @@ export class UploadController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Post('avatar-presigned-url')
|
@Post('avatar-presigned-url')
|
||||||
|
@UseGuards(JwtAuthGuard, RolesGuard)
|
||||||
@Roles(UserRole.AGENT)
|
@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 avatar upload (uses agent ID as filename)' })
|
||||||
@ApiResponse({
|
@ApiResponse({
|
||||||
status: 200,
|
status: 200,
|
||||||
@@ -79,10 +87,34 @@ export class UploadController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Delete(':key')
|
@Delete(':key')
|
||||||
|
@UseGuards(JwtAuthGuard, RolesGuard)
|
||||||
|
@Roles(UserRole.AGENT, UserRole.ADMIN)
|
||||||
|
@ApiBearerAuth()
|
||||||
@ApiOperation({ summary: 'Delete an uploaded file' })
|
@ApiOperation({ summary: 'Delete an uploaded file' })
|
||||||
@ApiResponse({ status: 200, description: 'File deleted successfully' })
|
@ApiResponse({ status: 200, description: 'File deleted successfully' })
|
||||||
async deleteFile(@Param('key') key: string) {
|
async deleteFile(@Param('key') key: string) {
|
||||||
await this.uploadService.deleteFile(key);
|
await this.uploadService.deleteFile(key);
|
||||||
return { message: 'File deleted successfully' };
|
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');
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -211,4 +211,57 @@ export class UploadService {
|
|||||||
key,
|
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,
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user