diff --git a/src/chapter/chapter.controller.ts b/src/chapter/chapter.controller.ts index ba94715..6d8d674 100644 --- a/src/chapter/chapter.controller.ts +++ b/src/chapter/chapter.controller.ts @@ -48,6 +48,7 @@ import { Folder } from 'src/file/enums/folder.enum'; import { FileInterceptor } from '@nestjs/platform-express'; import { EnrollmentService } from 'src/enrollment/enrollment.service'; import { Public } from 'src/shared/decorators/public.decorator'; +import { VideoGuard } from './guards/video.guard'; @Controller('chapter') @ApiTags('Chapters') @@ -57,14 +58,13 @@ export class ChapterController { private readonly chapterService: ChapterService, private readonly courseModuleService: CourseModuleService, private readonly fileService: FileService, - private readonly enrollmentService: EnrollmentService, ) { } @Get(':id/video') @ApiParam({ name: 'id', type: String, - description: 'Course id', + description: 'chapter id', }) @Public() @ApiResponse({ @@ -72,6 +72,13 @@ export class ChapterController { description: 'Get chapter video', type: StreamableFile, }) + @ApiQuery({ + name: 'token', + type: String, + required: false, + description: 'Access token', + }) + @UseGuards(VideoGuard) async getVideo( @Param( 'id', diff --git a/src/chapter/guards/video.guard.ts b/src/chapter/guards/video.guard.ts new file mode 100644 index 0000000..1f46bfd --- /dev/null +++ b/src/chapter/guards/video.guard.ts @@ -0,0 +1,76 @@ +import { + CanActivate, + ExecutionContext, + Injectable, + UnauthorizedException, + NotFoundException, + ForbiddenException, +} from '@nestjs/common'; +import { JwtService } from '@nestjs/jwt'; +import { JwtPayloadDto } from 'src/auth/dtos/jwt-payload.dto'; +import { ConfigService } from '@nestjs/config'; +import { GLOBAL_CONFIG } from 'src/shared/constants/global-config.constant'; +import { AuthenticatedRequest } from 'src/auth/interfaces/authenticated-request.interface'; +import { EnrollmentService } from 'src/enrollment/enrollment.service'; +import { Role } from 'src/shared/enums'; +import { InjectRepository } from '@nestjs/typeorm'; +import { Chapter } from '../chapter.entity'; +import { Repository } from 'typeorm'; + +@Injectable() +export class VideoGuard implements CanActivate { + constructor( + @InjectRepository(Chapter) + private readonly chapterRepository: Repository, + private readonly jwtService: JwtService, + private readonly configService: ConfigService, + private readonly enrollmentService: EnrollmentService, + ) {} + + async canActivate(context: ExecutionContext): Promise { + const request: AuthenticatedRequest = context.switchToHttp().getRequest(); + const chapterId = request.params.id; + const chapter = await this.chapterRepository.findOne({ + where: { id: chapterId }, + relations: { + module: { + course: { + teacher: true, + }, + }, + } + }); + if (!chapter) throw new NotFoundException('Chapter not found'); + if (chapter.isPreview) return true; + const token = request.query.token as string; + if (!token) throw new UnauthorizedException('Unauthorized access'); + try { + request.user = await this.jwtService.verifyAsync(token, { + secret: this.configService.getOrThrow( + GLOBAL_CONFIG.JWT_ACCESS_SECRET, + ), + }); + } catch (error) { + throw new UnauthorizedException('Unauthorized access'); + } + const userId = request.user.id; + const courseId = chapter.module.course.id; + switch (request.user.role) { + case Role.TEACHER: + if (chapter.module.course.teacher.id !== userId) + throw new ForbiddenException('Insufficient permissions'); + break; + case Role.STUDENT: + const enrollment = await this.enrollmentService.findOne({ + course: { id: courseId }, + user: { id: userId }, + }); + if (!enrollment) + throw new ForbiddenException('Insufficient permissions'); + break; + default: + throw new ForbiddenException('Insufficient permissions'); + } + return true; + } +}