eduverse-backend / src /modules /community /controllers /community-posts.controller.ts
tarekdx3's picture
Issue #27
32b4d43
Raw
History Blame Contribute Delete
8.63 kB
import {
Controller,
Get,
Post,
Put,
Patch,
Delete,
Body,
Param,
Query,
Req,
UseGuards,
ParseIntPipe,
HttpCode,
HttpStatus,
} from '@nestjs/common';
import {
ApiTags,
ApiOperation,
ApiParam,
ApiBody,
ApiResponse,
ApiBearerAuth,
ApiQuery,
} from '@nestjs/swagger';
import { JwtAuthGuard } from '../../auth/guards/jwt-auth.guard';
import { RolesGuard } from '../../auth/guards/roles.guard';
import { Roles } from '../../auth/roles.decorator';
import { RoleName } from '../../auth/entities/role.entity';
import { CommunityPostsService } from '../services/community-posts.service';
import {
CreatePostDto,
UpdatePostDto,
PostQueryDto,
CreateCommentDto,
CreateReactionDto,
} from '../dto';
@ApiTags('🌐 Community Posts')
@ApiBearerAuth('JWT-auth')
@Controller('api/community/posts')
@UseGuards(JwtAuthGuard, RolesGuard)
export class CommunityPostsController {
constructor(private readonly postsService: CommunityPostsService) {}
@Get()
@ApiOperation({
summary: 'List community posts',
description: `
List community posts with optional filtering by course, category, and post type.
**Sorting Options**:
- \`recent\`: Most recent posts first (default)
- \`popular\`: Most upvoted posts first
- \`most_comments\`: Posts with most comments first
**Note**: Pinned posts always appear at the top regardless of sort order.
`,
})
@ApiResponse({ status: 200, description: 'Paginated list of posts' })
async findAll(@Query() query: PostQueryDto) {
return this.postsService.findAll(query);
}
@Post()
@HttpCode(HttpStatus.CREATED)
@ApiOperation({
summary: 'Create a new post',
description: `
Create a new community post in a course.
**Post Types**:
- \`discussion\`: General discussion topic
- \`question\`: Question seeking answers
- \`announcement\`: Important announcement (different from admin announcements)
- \`resource\`: Shared resource or material
`,
})
@ApiBody({ type: CreatePostDto })
@ApiResponse({ status: 201, description: 'Post created successfully' })
async create(@Body() dto: CreatePostDto, @Req() req: any) {
const userId = req.user.userId || req.user.id;
return this.postsService.create(dto, userId);
}
@Get(':id')
@ApiOperation({
summary: 'Get post with comments',
description: 'Get a single post with paginated comments and reaction summary. Increments view count.',
})
@ApiParam({ name: 'id', description: 'Post ID', example: 1 })
@ApiQuery({ name: 'page', required: false, description: 'Comment page number', example: 1 })
@ApiQuery({ name: 'limit', required: false, description: 'Comments per page', example: 50 })
@ApiResponse({ status: 200, description: 'Post with comments and reactions' })
@ApiResponse({ status: 404, description: 'Post not found' })
async findOne(
@Param('id', ParseIntPipe) id: number,
@Req() req: any,
@Query('page') page?: number,
@Query('limit') limit?: number,
) {
const userId = req.user?.userId || req.user?.id;
return this.postsService.findOne(id, userId, page || 1, limit || 50);
}
@Put(':id')
@ApiOperation({
summary: 'Update post',
description: `
Update a community post.
**Note**: Only the post owner can update their posts.
`,
})
@ApiParam({ name: 'id', description: 'Post ID', example: 1 })
@ApiBody({ type: UpdatePostDto })
@ApiResponse({ status: 200, description: 'Post updated successfully' })
@ApiResponse({ status: 403, description: 'Forbidden - Not the owner' })
@ApiResponse({ status: 404, description: 'Post not found' })
async update(
@Param('id', ParseIntPipe) id: number,
@Body() dto: UpdatePostDto,
@Req() req: any,
) {
const userId = req.user.userId || req.user.id;
const roles = this.extractRoles(req.user);
return this.postsService.update(id, dto, userId, roles);
}
@Delete(':id')
@HttpCode(HttpStatus.NO_CONTENT)
@ApiOperation({
summary: 'Delete post',
description: `
Delete a community post.
**Allowed**: Post owner or Admin
`,
})
@ApiParam({ name: 'id', description: 'Post ID', example: 1 })
@ApiResponse({ status: 204, description: 'Post deleted successfully' })
@ApiResponse({ status: 403, description: 'Forbidden - Not owner or admin' })
@ApiResponse({ status: 404, description: 'Post not found' })
async remove(@Param('id', ParseIntPipe) id: number, @Req() req: any) {
const userId = req.user.userId || req.user.id;
const roles = this.extractRoles(req.user);
return this.postsService.remove(id, userId, roles);
}
@Post(':id/comment')
@HttpCode(HttpStatus.CREATED)
@ApiOperation({
summary: 'Add comment to post',
description: `
Add a comment to a community post. Supports nested replies via \`parentCommentId\`.
**Note**: Cannot comment on locked posts.
`,
})
@ApiParam({ name: 'id', description: 'Post ID', example: 1 })
@ApiBody({ type: CreateCommentDto })
@ApiResponse({ status: 201, description: 'Comment added successfully' })
@ApiResponse({ status: 400, description: 'Post is locked' })
@ApiResponse({ status: 404, description: 'Post not found' })
async addComment(
@Param('id', ParseIntPipe) id: number,
@Body() dto: CreateCommentDto,
@Req() req: any,
) {
const userId = req.user.userId || req.user.id;
return this.postsService.addComment(id, dto, userId);
}
@Post(':id/react')
@HttpCode(HttpStatus.OK)
@ApiOperation({
summary: 'Toggle reaction on post',
description: `
Add or remove a reaction on a post. If the same reaction type exists, it will be removed (toggle behavior).
**Reaction Types**: like, helpful, insightful, thanks
`,
})
@ApiParam({ name: 'id', description: 'Post ID', example: 1 })
@ApiBody({ type: CreateReactionDto })
@ApiResponse({ status: 200, description: 'Reaction toggled. Returns action (added/removed)' })
@ApiResponse({ status: 404, description: 'Post not found' })
async toggleReaction(
@Param('id', ParseIntPipe) id: number,
@Body() dto: CreateReactionDto,
@Req() req: any,
) {
const userId = req.user.userId || req.user.id;
return this.postsService.toggleReaction(id, dto, userId);
}
@Post(':id/upvote')
@HttpCode(HttpStatus.OK)
@ApiOperation({
summary: 'Toggle upvote on post',
description: 'Add or remove an upvote on a post.',
})
@ApiParam({ name: 'id', description: 'Post ID', example: 1 })
@ApiResponse({ status: 200, description: 'Upvote toggled. Returns action (added/removed)' })
@ApiResponse({ status: 404, description: 'Post not found' })
async toggleUpvote(
@Param('id', ParseIntPipe) id: number,
@Req() req: any,
) {
const userId = req.user.userId || req.user.id;
return this.postsService.toggleReaction(id, { reactionType: 'upvote' as any }, userId);
}
@Patch(':id/pin')
@Roles(RoleName.INSTRUCTOR, RoleName.ADMIN, RoleName.IT_ADMIN)
@ApiOperation({
summary: 'Toggle pin post',
description: `
Pin or unpin a community post. Pinned posts appear at the top of listings.
**Allowed Roles**: Instructor, Admin
`,
})
@ApiParam({ name: 'id', description: 'Post ID', example: 1 })
@ApiResponse({ status: 200, description: 'Pin status toggled' })
@ApiResponse({ status: 403, description: 'Forbidden - Insufficient permissions' })
@ApiResponse({ status: 404, description: 'Post not found' })
async togglePin(@Param('id', ParseIntPipe) id: number, @Req() req: any) {
const userId = req.user.userId || req.user.id;
const roles = this.extractRoles(req.user);
return this.postsService.togglePin(id, userId, roles);
}
@Patch(':id/lock')
@Roles(RoleName.INSTRUCTOR, RoleName.ADMIN, RoleName.IT_ADMIN)
@ApiOperation({
summary: 'Toggle lock post',
description: `
Lock or unlock a community post. Locked posts cannot receive new comments.
**Allowed Roles**: Instructor, Admin
`,
})
@ApiParam({ name: 'id', description: 'Post ID', example: 1 })
@ApiResponse({ status: 200, description: 'Lock status toggled' })
@ApiResponse({ status: 403, description: 'Forbidden - Insufficient permissions' })
@ApiResponse({ status: 404, description: 'Post not found' })
async toggleLock(@Param('id', ParseIntPipe) id: number, @Req() req: any) {
const userId = req.user.userId || req.user.id;
const roles = this.extractRoles(req.user);
return this.postsService.toggleLock(id, userId, roles);
}
/**
* Helper to extract role names from user object
*/
private extractRoles(user: any): string[] {
if (!user.roles) return [];
return user.roles.map((r: any) => r.roleName || r.name || r);
}
}