feat: add comments functionality and integrate Socket.io for real-time updates

- Implemented a full comments module in the backend with repository, service, controller, and DTOs using NestJS.
- Added frontend support for comments with a `CommentSection` component and integration into content pages.
- Introduced `SocketProvider` on the frontend and integrated Socket.io for real-time communication.
- Updated dependencies and configurations for Socket.io and WebSockets support.
This commit is contained in:
Mathis HERRIOT
2026-01-29 14:33:34 +01:00
parent e73ae80fc5
commit 01117aad6d
14 changed files with 479 additions and 36 deletions

View File

@@ -0,0 +1,41 @@
import {
Body,
Controller,
Delete,
Get,
Param,
Post,
Req,
UseGuards,
} from "@nestjs/common";
import { AuthGuard } from "../auth/guards/auth.guard";
import type { AuthenticatedRequest } from "../common/interfaces/request.interface";
import { CommentsService } from "./comments.service";
import { CreateCommentDto } from "./dto/create-comment.dto";
@Controller()
export class CommentsController {
constructor(private readonly commentsService: CommentsService) {}
@Get("contents/:contentId/comments")
findAllByContentId(@Param("contentId") contentId: string) {
return this.commentsService.findAllByContentId(contentId);
}
@Post("contents/:contentId/comments")
@UseGuards(AuthGuard)
create(
@Req() req: AuthenticatedRequest,
@Param("contentId") contentId: string,
@Body() dto: CreateCommentDto,
) {
return this.commentsService.create(req.user.sub, contentId, dto);
}
@Delete("comments/:id")
@UseGuards(AuthGuard)
remove(@Req() req: AuthenticatedRequest, @Param("id") id: string) {
const isAdmin = req.user.role === "admin" || req.user.role === "moderator";
return this.commentsService.remove(req.user.sub, id, isAdmin);
}
}

View File

@@ -0,0 +1,13 @@
import { Module } from "@nestjs/common";
import { AuthModule } from "../auth/auth.module";
import { CommentsController } from "./comments.controller";
import { CommentsService } from "./comments.service";
import { CommentsRepository } from "./repositories/comments.repository";
@Module({
imports: [AuthModule],
controllers: [CommentsController],
providers: [CommentsService, CommentsRepository],
exports: [CommentsService],
})
export class CommentsModule {}

View File

@@ -0,0 +1,37 @@
import {
ForbiddenException,
Injectable,
NotFoundException,
} from "@nestjs/common";
import { CommentsRepository } from "./repositories/comments.repository";
import type { CreateCommentDto } from "./dto/create-comment.dto";
@Injectable()
export class CommentsService {
constructor(private readonly commentsRepository: CommentsRepository) {}
async create(userId: string, contentId: string, dto: CreateCommentDto) {
return this.commentsRepository.create({
userId,
contentId,
text: dto.text,
});
}
async findAllByContentId(contentId: string) {
return this.commentsRepository.findAllByContentId(contentId);
}
async remove(userId: string, commentId: string, isAdmin = false) {
const comment = await this.commentsRepository.findOne(commentId);
if (!comment) {
throw new NotFoundException("Comment not found");
}
if (!isAdmin && comment.userId !== userId) {
throw new ForbiddenException("You cannot delete this comment");
}
await this.commentsRepository.delete(commentId);
}
}

View File

@@ -0,0 +1,8 @@
import { IsNotEmpty, IsString, MaxLength } from "class-validator";
export class CreateCommentDto {
@IsString()
@IsNotEmpty()
@MaxLength(1000)
text!: string;
}

View File

@@ -0,0 +1,53 @@
import { Injectable } from "@nestjs/common";
import { and, desc, eq, isNull } from "drizzle-orm";
import { DatabaseService } from "../../database/database.service";
import { comments, users } from "../../database/schemas";
import type { NewCommentInDb } from "../../database/schemas/comments";
@Injectable()
export class CommentsRepository {
constructor(private readonly databaseService: DatabaseService) {}
async create(data: NewCommentInDb) {
const [comment] = await this.databaseService.db
.insert(comments)
.values(data)
.returning();
return comment;
}
async findAllByContentId(contentId: string) {
return this.databaseService.db
.select({
id: comments.id,
text: comments.text,
createdAt: comments.createdAt,
updatedAt: comments.updatedAt,
user: {
uuid: users.uuid,
username: users.username,
displayName: users.displayName,
avatarUrl: users.avatarUrl,
},
})
.from(comments)
.innerJoin(users, eq(comments.userId, users.uuid))
.where(and(eq(comments.contentId, contentId), isNull(comments.deletedAt)))
.orderBy(desc(comments.createdAt));
}
async findOne(id: string) {
const [comment] = await this.databaseService.db
.select()
.from(comments)
.where(and(eq(comments.id, id), isNull(comments.deletedAt)));
return comment;
}
async delete(id: string) {
await this.databaseService.db
.update(comments)
.set({ deletedAt: new Date() })
.where(eq(comments.id, id));
}
}