feat(api): add images module for image filename management
- Implement PUT /api/image/{imageId}/filename for filename updates - Add GET /api/image/{imageId} for detailed image information - Support GET /api/image/batch/{batchId} for batch image listing - Include filename approval, revert, and download URL generation - Add comprehensive filename validation and SEO optimization - Support presigned URL generation for secure downloads Resolves requirement §75 for image filename management API. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
2add73a264
commit
ed5f745a51
5 changed files with 968 additions and 0 deletions
304
packages/api/src/images/images.controller.ts
Normal file
304
packages/api/src/images/images.controller.ts
Normal file
|
@ -0,0 +1,304 @@
|
|||
import {
|
||||
Controller,
|
||||
Get,
|
||||
Put,
|
||||
Param,
|
||||
Body,
|
||||
UseGuards,
|
||||
Request,
|
||||
HttpStatus,
|
||||
BadRequestException,
|
||||
ForbiddenException,
|
||||
NotFoundException,
|
||||
} from '@nestjs/common';
|
||||
import { ApiTags, ApiOperation, ApiResponse, ApiBearerAuth } from '@nestjs/swagger';
|
||||
import { JwtAuthGuard } from '../auth/auth.guard';
|
||||
import { ImagesService } from './images.service';
|
||||
import { UpdateFilenameDto, UpdateFilenameResponseDto } from './dto/update-filename.dto';
|
||||
import { ImageResponseDto, BatchImagesResponseDto } from './dto/image-response.dto';
|
||||
|
||||
@ApiTags('images')
|
||||
@Controller('api/image')
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@ApiBearerAuth()
|
||||
export class ImagesController {
|
||||
constructor(private readonly imagesService: ImagesService) {}
|
||||
|
||||
@Put(':imageId/filename')
|
||||
@ApiOperation({
|
||||
summary: 'Update image filename',
|
||||
description: 'Updates the proposed filename for a specific image',
|
||||
})
|
||||
@ApiResponse({
|
||||
status: HttpStatus.OK,
|
||||
description: 'Filename updated successfully',
|
||||
type: UpdateFilenameResponseDto,
|
||||
})
|
||||
@ApiResponse({
|
||||
status: HttpStatus.BAD_REQUEST,
|
||||
description: 'Invalid filename or request data',
|
||||
})
|
||||
@ApiResponse({
|
||||
status: HttpStatus.NOT_FOUND,
|
||||
description: 'Image not found',
|
||||
})
|
||||
@ApiResponse({
|
||||
status: HttpStatus.FORBIDDEN,
|
||||
description: 'Not authorized to update this image',
|
||||
})
|
||||
async updateImageFilename(
|
||||
@Param('imageId') imageId: string,
|
||||
@Body() updateFilenameDto: UpdateFilenameDto,
|
||||
@Request() req: any,
|
||||
): Promise<UpdateFilenameResponseDto> {
|
||||
try {
|
||||
const userId = req.user?.id;
|
||||
if (!userId) {
|
||||
throw new BadRequestException('User not authenticated');
|
||||
}
|
||||
|
||||
const result = await this.imagesService.updateFilename(
|
||||
imageId,
|
||||
userId,
|
||||
updateFilenameDto.new_name
|
||||
);
|
||||
|
||||
return result;
|
||||
} catch (error) {
|
||||
if (
|
||||
error instanceof BadRequestException ||
|
||||
error instanceof ForbiddenException ||
|
||||
error instanceof NotFoundException
|
||||
) {
|
||||
throw error;
|
||||
}
|
||||
throw new BadRequestException('Failed to update image filename');
|
||||
}
|
||||
}
|
||||
|
||||
@Get(':imageId')
|
||||
@ApiOperation({
|
||||
summary: 'Get image details',
|
||||
description: 'Returns detailed information about a specific image',
|
||||
})
|
||||
@ApiResponse({
|
||||
status: HttpStatus.OK,
|
||||
description: 'Image details retrieved successfully',
|
||||
type: ImageResponseDto,
|
||||
})
|
||||
@ApiResponse({
|
||||
status: HttpStatus.NOT_FOUND,
|
||||
description: 'Image not found',
|
||||
})
|
||||
@ApiResponse({
|
||||
status: HttpStatus.FORBIDDEN,
|
||||
description: 'Not authorized to access this image',
|
||||
})
|
||||
async getImage(
|
||||
@Param('imageId') imageId: string,
|
||||
@Request() req: any,
|
||||
): Promise<ImageResponseDto> {
|
||||
try {
|
||||
const userId = req.user?.id;
|
||||
if (!userId) {
|
||||
throw new BadRequestException('User not authenticated');
|
||||
}
|
||||
|
||||
const image = await this.imagesService.getImage(imageId, userId);
|
||||
return image;
|
||||
} catch (error) {
|
||||
if (
|
||||
error instanceof BadRequestException ||
|
||||
error instanceof ForbiddenException ||
|
||||
error instanceof NotFoundException
|
||||
) {
|
||||
throw error;
|
||||
}
|
||||
throw new BadRequestException('Failed to get image details');
|
||||
}
|
||||
}
|
||||
|
||||
@Get('batch/:batchId')
|
||||
@ApiOperation({
|
||||
summary: 'Get all images in a batch',
|
||||
description: 'Returns all images belonging to a specific batch',
|
||||
})
|
||||
@ApiResponse({
|
||||
status: HttpStatus.OK,
|
||||
description: 'Batch images retrieved successfully',
|
||||
type: BatchImagesResponseDto,
|
||||
})
|
||||
@ApiResponse({
|
||||
status: HttpStatus.NOT_FOUND,
|
||||
description: 'Batch not found',
|
||||
})
|
||||
@ApiResponse({
|
||||
status: HttpStatus.FORBIDDEN,
|
||||
description: 'Not authorized to access this batch',
|
||||
})
|
||||
async getBatchImages(
|
||||
@Param('batchId') batchId: string,
|
||||
@Request() req: any,
|
||||
): Promise<BatchImagesResponseDto> {
|
||||
try {
|
||||
const userId = req.user?.id;
|
||||
if (!userId) {
|
||||
throw new BadRequestException('User not authenticated');
|
||||
}
|
||||
|
||||
const batchImages = await this.imagesService.getBatchImages(batchId, userId);
|
||||
return batchImages;
|
||||
} catch (error) {
|
||||
if (
|
||||
error instanceof BadRequestException ||
|
||||
error instanceof ForbiddenException ||
|
||||
error instanceof NotFoundException
|
||||
) {
|
||||
throw error;
|
||||
}
|
||||
throw new BadRequestException('Failed to get batch images');
|
||||
}
|
||||
}
|
||||
|
||||
@Get(':imageId/download')
|
||||
@ApiOperation({
|
||||
summary: 'Get image download URL',
|
||||
description: 'Returns a presigned URL for downloading the original or processed image',
|
||||
})
|
||||
@ApiResponse({
|
||||
status: HttpStatus.OK,
|
||||
description: 'Download URL generated successfully',
|
||||
schema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
download_url: {
|
||||
type: 'string',
|
||||
example: 'https://storage.example.com/images/processed/image.jpg?expires=...',
|
||||
},
|
||||
expires_at: {
|
||||
type: 'string',
|
||||
example: '2024-01-01T13:00:00.000Z',
|
||||
},
|
||||
filename: {
|
||||
type: 'string',
|
||||
example: 'modern-kitchen-renovation.jpg',
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
@ApiResponse({
|
||||
status: HttpStatus.NOT_FOUND,
|
||||
description: 'Image not found',
|
||||
})
|
||||
@ApiResponse({
|
||||
status: HttpStatus.FORBIDDEN,
|
||||
description: 'Not authorized to download this image',
|
||||
})
|
||||
async getImageDownloadUrl(
|
||||
@Param('imageId') imageId: string,
|
||||
@Request() req: any,
|
||||
): Promise<{
|
||||
download_url: string;
|
||||
expires_at: string;
|
||||
filename: string;
|
||||
}> {
|
||||
try {
|
||||
const userId = req.user?.id;
|
||||
if (!userId) {
|
||||
throw new BadRequestException('User not authenticated');
|
||||
}
|
||||
|
||||
const downloadInfo = await this.imagesService.getImageDownloadUrl(imageId, userId);
|
||||
return downloadInfo;
|
||||
} catch (error) {
|
||||
if (
|
||||
error instanceof BadRequestException ||
|
||||
error instanceof ForbiddenException ||
|
||||
error instanceof NotFoundException
|
||||
) {
|
||||
throw error;
|
||||
}
|
||||
throw new BadRequestException('Failed to generate download URL');
|
||||
}
|
||||
}
|
||||
|
||||
@Put(':imageId/approve')
|
||||
@ApiOperation({
|
||||
summary: 'Approve proposed filename',
|
||||
description: 'Approves the AI-generated proposed filename as the final filename',
|
||||
})
|
||||
@ApiResponse({
|
||||
status: HttpStatus.OK,
|
||||
description: 'Filename approved successfully',
|
||||
type: UpdateFilenameResponseDto,
|
||||
})
|
||||
@ApiResponse({
|
||||
status: HttpStatus.NOT_FOUND,
|
||||
description: 'Image not found',
|
||||
})
|
||||
@ApiResponse({
|
||||
status: HttpStatus.BAD_REQUEST,
|
||||
description: 'No proposed filename to approve',
|
||||
})
|
||||
async approveFilename(
|
||||
@Param('imageId') imageId: string,
|
||||
@Request() req: any,
|
||||
): Promise<UpdateFilenameResponseDto> {
|
||||
try {
|
||||
const userId = req.user?.id;
|
||||
if (!userId) {
|
||||
throw new BadRequestException('User not authenticated');
|
||||
}
|
||||
|
||||
const result = await this.imagesService.approveProposedFilename(imageId, userId);
|
||||
return result;
|
||||
} catch (error) {
|
||||
if (
|
||||
error instanceof BadRequestException ||
|
||||
error instanceof ForbiddenException ||
|
||||
error instanceof NotFoundException
|
||||
) {
|
||||
throw error;
|
||||
}
|
||||
throw new BadRequestException('Failed to approve filename');
|
||||
}
|
||||
}
|
||||
|
||||
@Put(':imageId/revert')
|
||||
@ApiOperation({
|
||||
summary: 'Revert to original filename',
|
||||
description: 'Reverts the image filename back to the original uploaded filename',
|
||||
})
|
||||
@ApiResponse({
|
||||
status: HttpStatus.OK,
|
||||
description: 'Filename reverted successfully',
|
||||
type: UpdateFilenameResponseDto,
|
||||
})
|
||||
@ApiResponse({
|
||||
status: HttpStatus.NOT_FOUND,
|
||||
description: 'Image not found',
|
||||
})
|
||||
async revertFilename(
|
||||
@Param('imageId') imageId: string,
|
||||
@Request() req: any,
|
||||
): Promise<UpdateFilenameResponseDto> {
|
||||
try {
|
||||
const userId = req.user?.id;
|
||||
if (!userId) {
|
||||
throw new BadRequestException('User not authenticated');
|
||||
}
|
||||
|
||||
const result = await this.imagesService.revertToOriginalFilename(imageId, userId);
|
||||
return result;
|
||||
} catch (error) {
|
||||
if (
|
||||
error instanceof BadRequestException ||
|
||||
error instanceof ForbiddenException ||
|
||||
error instanceof NotFoundException
|
||||
) {
|
||||
throw error;
|
||||
}
|
||||
throw new BadRequestException('Failed to revert filename');
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue