feat: implement ContentsModule with controllers, services, and DTOs

Added a new ContentsModule to handle content creation, upload, and management. Includes controller endpoints for CRUD operations, content exploration, and tagging. Integrated caching, file processing, S3 storage, and database logic.
This commit is contained in:
Mathis HERRIOT
2026-01-08 15:25:28 +01:00
parent e210f1f95f
commit 342e9b99da
5 changed files with 598 additions and 0 deletions

View File

@@ -0,0 +1,178 @@
import {
Body,
Controller,
DefaultValuePipe,
Delete,
Get,
Header,
Param,
ParseBoolPipe,
ParseIntPipe,
Post,
Query,
Req,
Res,
UploadedFile,
UseGuards,
UseInterceptors,
NotFoundException,
} from "@nestjs/common";
import { CacheInterceptor, CacheTTL } from "@nestjs/cache-manager";
import { FileInterceptor } from "@nestjs/platform-express";
import type { Request, Response } from "express";
import { AuthGuard } from "../auth/guards/auth.guard";
import type { AuthenticatedRequest } from "../common/interfaces/request.interface";
import { ContentsService } from "./contents.service";
import { CreateContentDto } from "./dto/create-content.dto";
import { UploadContentDto } from "./dto/upload-content.dto";
@Controller("contents")
export class ContentsController {
constructor(private readonly contentsService: ContentsService) {}
@Post()
@UseGuards(AuthGuard)
create(
@Req() req: AuthenticatedRequest,
@Body() createContentDto: CreateContentDto,
) {
return this.contentsService.create(req.user.sub, createContentDto);
}
@Post("upload-url")
@UseGuards(AuthGuard)
getUploadUrl(
@Req() req: AuthenticatedRequest,
@Query("fileName") fileName: string,
) {
return this.contentsService.getUploadUrl(req.user.sub, fileName);
}
@Post("upload")
@UseGuards(AuthGuard)
@UseInterceptors(FileInterceptor("file"))
upload(
@Req() req: AuthenticatedRequest,
@UploadedFile()
file: Express.Multer.File,
@Body() uploadContentDto: UploadContentDto,
) {
return this.contentsService.uploadAndProcess(
req.user.sub,
file,
uploadContentDto,
);
}
@Get("explore")
@UseInterceptors(CacheInterceptor)
@CacheTTL(60)
@Header("Cache-Control", "public, max-age=60")
explore(
@Query("limit", new DefaultValuePipe(10), ParseIntPipe) limit: number,
@Query("offset", new DefaultValuePipe(0), ParseIntPipe) offset: number,
@Query("sort") sort?: "trend" | "recent",
@Query("tag") tag?: string,
@Query("category") category?: string,
@Query("author") author?: string,
@Query("query") query?: string,
@Query("favoritesOnly", new DefaultValuePipe(false), ParseBoolPipe)
favoritesOnly?: boolean,
@Query("userId") userId?: string,
) {
return this.contentsService.findAll({
limit,
offset,
sortBy: sort,
tag,
category,
author,
query,
favoritesOnly,
userId,
});
}
@Get("trends")
@UseInterceptors(CacheInterceptor)
@CacheTTL(300)
@Header("Cache-Control", "public, max-age=300")
trends(
@Query("limit", new DefaultValuePipe(10), ParseIntPipe) limit: number,
@Query("offset", new DefaultValuePipe(0), ParseIntPipe) offset: number,
) {
return this.contentsService.findAll({ limit, offset, sortBy: "trend" });
}
@Get("recent")
@UseInterceptors(CacheInterceptor)
@CacheTTL(60)
@Header("Cache-Control", "public, max-age=60")
recent(
@Query("limit", new DefaultValuePipe(10), ParseIntPipe) limit: number,
@Query("offset", new DefaultValuePipe(0), ParseIntPipe) offset: number,
) {
return this.contentsService.findAll({ limit, offset, sortBy: "recent" });
}
@Get(":idOrSlug")
@UseInterceptors(CacheInterceptor)
@CacheTTL(3600)
@Header("Cache-Control", "public, max-age=3600")
async findOne(
@Param("idOrSlug") idOrSlug: string,
@Req() req: Request,
@Res() res: Response,
) {
const content = await this.contentsService.findOne(idOrSlug);
if (!content) {
throw new NotFoundException("Contenu non trouvé");
}
const userAgent = req.headers["user-agent"] || "";
const isBot = /bot|googlebot|crawler|spider|robot|crawling|facebookexternalhit|twitterbot/i.test(
userAgent,
);
if (isBot) {
const imageUrl = this.contentsService.getFileUrl(content.storageKey);
const html = `<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>${content.title}</title>
<meta property="og:title" content="${content.title}" />
<meta property="og:type" content="website" />
<meta property="og:image" content="${imageUrl}" />
<meta property="og:description" content="Découvrez ce meme sur Memegoat" />
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:title" content="${content.title}" />
<meta name="twitter:image" content="${imageUrl}" />
</head>
<body>
<h1>${content.title}</h1>
<img src="${imageUrl}" alt="${content.title}" />
</body>
</html>`;
return res.send(html);
}
return res.json(content);
}
@Post(":id/view")
incrementViews(@Param("id") id: string) {
return this.contentsService.incrementViews(id);
}
@Post(":id/use")
incrementUsage(@Param("id") id: string) {
return this.contentsService.incrementUsage(id);
}
@Delete(":id")
@UseGuards(AuthGuard)
remove(@Param("id") id: string, @Req() req: AuthenticatedRequest) {
return this.contentsService.remove(id, req.user.sub);
}
}