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:
@@ -10,6 +10,7 @@ import { AppController } from "./app.controller";
|
||||
import { AppService } from "./app.service";
|
||||
import { AuthModule } from "./auth/auth.module";
|
||||
import { CategoriesModule } from "./categories/categories.module";
|
||||
import { CommentsModule } from "./comments/comments.module";
|
||||
import { CommonModule } from "./common/common.module";
|
||||
import { CrawlerDetectionMiddleware } from "./common/middlewares/crawler-detection.middleware";
|
||||
import { HTTPLoggerMiddleware } from "./common/middlewares/http-logger.middleware";
|
||||
@@ -21,7 +22,9 @@ import { FavoritesModule } from "./favorites/favorites.module";
|
||||
import { HealthController } from "./health.controller";
|
||||
import { MailModule } from "./mail/mail.module";
|
||||
import { MediaModule } from "./media/media.module";
|
||||
import { MessagesModule } from "./messages/messages.module";
|
||||
import { ReportsModule } from "./reports/reports.module";
|
||||
import { RealtimeModule } from "./realtime/realtime.module";
|
||||
import { S3Module } from "./s3/s3.module";
|
||||
import { SessionsModule } from "./sessions/sessions.module";
|
||||
import { TagsModule } from "./tags/tags.module";
|
||||
@@ -37,12 +40,15 @@ import { UsersModule } from "./users/users.module";
|
||||
UsersModule,
|
||||
AuthModule,
|
||||
CategoriesModule,
|
||||
CommentsModule,
|
||||
ContentsModule,
|
||||
FavoritesModule,
|
||||
TagsModule,
|
||||
MediaModule,
|
||||
MessagesModule,
|
||||
SessionsModule,
|
||||
ReportsModule,
|
||||
RealtimeModule,
|
||||
ApiKeysModule,
|
||||
AdminModule,
|
||||
ScheduleModule.forRoot(),
|
||||
|
||||
41
backend/src/comments/comments.controller.ts
Normal file
41
backend/src/comments/comments.controller.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
13
backend/src/comments/comments.module.ts
Normal file
13
backend/src/comments/comments.module.ts
Normal 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 {}
|
||||
37
backend/src/comments/comments.service.ts
Normal file
37
backend/src/comments/comments.service.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
8
backend/src/comments/dto/create-comment.dto.ts
Normal file
8
backend/src/comments/dto/create-comment.dto.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import { IsNotEmpty, IsString, MaxLength } from "class-validator";
|
||||
|
||||
export class CreateCommentDto {
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
@MaxLength(1000)
|
||||
text!: string;
|
||||
}
|
||||
53
backend/src/comments/repositories/comments.repository.ts
Normal file
53
backend/src/comments/repositories/comments.repository.ts
Normal 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));
|
||||
}
|
||||
}
|
||||
31
backend/src/database/schemas/comments.ts
Normal file
31
backend/src/database/schemas/comments.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
import { index, pgTable, text, timestamp, uuid } from "drizzle-orm/pg-core";
|
||||
import { contents } from "./content";
|
||||
import { users } from "./users";
|
||||
|
||||
export const comments = pgTable(
|
||||
"comments",
|
||||
{
|
||||
id: uuid("id").primaryKey().defaultRandom(),
|
||||
contentId: uuid("content_id")
|
||||
.notNull()
|
||||
.references(() => contents.id, { onDelete: "cascade" }),
|
||||
userId: uuid("user_id")
|
||||
.notNull()
|
||||
.references(() => users.uuid, { onDelete: "cascade" }),
|
||||
text: text("text").notNull(),
|
||||
createdAt: timestamp("created_at", { withTimezone: true })
|
||||
.notNull()
|
||||
.defaultNow(),
|
||||
updatedAt: timestamp("updated_at", { withTimezone: true })
|
||||
.notNull()
|
||||
.defaultNow(),
|
||||
deletedAt: timestamp("deleted_at", { withTimezone: true }),
|
||||
},
|
||||
(table) => ({
|
||||
contentIdIdx: index("comments_content_id_idx").on(table.contentId),
|
||||
userIdIdx: index("comments_user_id_idx").on(table.userId),
|
||||
}),
|
||||
);
|
||||
|
||||
export type CommentInDb = typeof comments.$inferSelect;
|
||||
export type NewCommentInDb = typeof comments.$inferInsert;
|
||||
@@ -1,11 +1,13 @@
|
||||
export * from "./api_keys";
|
||||
export * from "./audit_logs";
|
||||
export * from "./categories";
|
||||
export * from "./comments";
|
||||
export * from "./content";
|
||||
export * from "./favorites";
|
||||
export * from "./pgp";
|
||||
export * from "./rbac";
|
||||
export * from "./reports";
|
||||
export * from "./messages";
|
||||
export * from "./sessions";
|
||||
export * from "./tags";
|
||||
export * from "./users";
|
||||
|
||||
Reference in New Issue
Block a user