import { IssueStatus, IssueType } from '@server/constants/issue'; import { getRepository } from '@server/datasource'; import Issue from '@server/entity/Issue'; import IssueComment from '@server/entity/IssueComment'; import Media from '@server/entity/Media'; import type { IssueResultsResponse } from '@server/interfaces/api/issueInterfaces'; import { Permission } from '@server/lib/permissions'; import logger from '@server/logger'; import { isAuthenticated } from '@server/middleware/auth'; import { Router } from 'express'; const issueRoutes = Router(); issueRoutes.get, IssueResultsResponse>( '/', isAuthenticated( [ Permission.MANAGE_ISSUES, Permission.VIEW_ISSUES, Permission.CREATE_ISSUES, ], { type: 'or' } ), async (req, res, next) => { const pageSize = req.query.take ? Number(req.query.take) : 10; const skip = req.query.skip ? Number(req.query.skip) : 0; const createdBy = req.query.createdBy ? Number(req.query.createdBy) : null; let sortFilter: string; switch (req.query.sort) { case 'modified': sortFilter = 'issue.updatedAt'; break; default: sortFilter = 'issue.createdAt'; } let statusFilter: IssueStatus[]; switch (req.query.filter) { case 'open': statusFilter = [IssueStatus.OPEN]; break; case 'resolved': statusFilter = [IssueStatus.RESOLVED]; break; default: statusFilter = [IssueStatus.OPEN, IssueStatus.RESOLVED]; } let query = getRepository(Issue) .createQueryBuilder('issue') .leftJoinAndSelect('issue.createdBy', 'createdBy') .leftJoinAndSelect('issue.media', 'media') .leftJoinAndSelect('issue.modifiedBy', 'modifiedBy') .where('issue.status IN (:...issueStatus)', { issueStatus: statusFilter, }); if ( !req.user?.hasPermission( [Permission.MANAGE_ISSUES, Permission.VIEW_ISSUES], { type: 'or' } ) ) { if (createdBy && createdBy !== req.user?.id) { return next({ status: 403, message: 'You do not have permission to view issues reported by other users', }); } query = query.andWhere('createdBy.id = :id', { id: req.user?.id }); } else if (createdBy) { query = query.andWhere('createdBy.id = :id', { id: createdBy }); } const [issues, issueCount] = await query .orderBy(sortFilter, 'DESC') .take(pageSize) .skip(skip) .getManyAndCount(); return res.status(200).json({ pageInfo: { pages: Math.ceil(issueCount / pageSize), pageSize, results: issueCount, page: Math.ceil(skip / pageSize) + 1, }, results: issues, }); } ); issueRoutes.post< Record, Issue, { message: string; mediaId: number; issueType: number; problemSeason: number; problemEpisode: number; } >( '/', isAuthenticated([Permission.MANAGE_ISSUES, Permission.CREATE_ISSUES], { type: 'or', }), async (req, res, next) => { // Satisfy typescript here. User is set, we assure you! if (!req.user) { return next({ status: 500, message: 'User missing from request.' }); } const issueRepository = getRepository(Issue); const mediaRepository = getRepository(Media); const media = await mediaRepository.findOne({ where: { id: req.body.mediaId }, }); if (!media) { return next({ status: 404, message: 'Media does not exist.' }); } const issue = new Issue({ createdBy: req.user, issueType: req.body.issueType, problemSeason: req.body.problemSeason, problemEpisode: req.body.problemEpisode, media, comments: [ new IssueComment({ user: req.user, message: req.body.message, }), ], }); const newIssue = await issueRepository.save(issue); return res.status(200).json(newIssue); } ); issueRoutes.get('/count', async (req, res, next) => { const issueRepository = getRepository(Issue); try { const query = issueRepository.createQueryBuilder('issue'); const totalCount = await query.getCount(); const videoCount = await query .where('issue.issueType = :issueType', { issueType: IssueType.VIDEO, }) .getCount(); const audioCount = await query .where('issue.issueType = :issueType', { issueType: IssueType.AUDIO, }) .getCount(); const subtitlesCount = await query .where('issue.issueType = :issueType', { issueType: IssueType.SUBTITLES, }) .getCount(); const othersCount = await query .where('issue.issueType = :issueType', { issueType: IssueType.OTHER, }) .getCount(); const openCount = await query .where('issue.status = :issueStatus', { issueStatus: IssueStatus.OPEN, }) .getCount(); const closedCount = await query .where('issue.status = :issueStatus', { issueStatus: IssueStatus.RESOLVED, }) .getCount(); return res.status(200).json({ total: totalCount, video: videoCount, audio: audioCount, subtitles: subtitlesCount, others: othersCount, open: openCount, closed: closedCount, }); } catch (e) { logger.debug('Something went wrong retrieving issue counts.', { label: 'API', errorMessage: e.message, }); next({ status: 500, message: 'Unable to retrieve issue counts.' }); } }); issueRoutes.get<{ issueId: string }>( '/:issueId', isAuthenticated( [ Permission.MANAGE_ISSUES, Permission.VIEW_ISSUES, Permission.CREATE_ISSUES, ], { type: 'or' } ), async (req, res, next) => { const issueRepository = getRepository(Issue); // Satisfy typescript here. User is set, we assure you! if (!req.user) { return next({ status: 500, message: 'User missing from request.' }); } try { const issue = await issueRepository .createQueryBuilder('issue') .leftJoinAndSelect('issue.comments', 'comments') .leftJoinAndSelect('issue.createdBy', 'createdBy') .leftJoinAndSelect('comments.user', 'user') .leftJoinAndSelect('issue.media', 'media') .where('issue.id = :issueId', { issueId: Number(req.params.issueId) }) .getOneOrFail(); if ( issue.createdBy.id !== req.user.id && !req.user.hasPermission( [Permission.MANAGE_ISSUES, Permission.VIEW_ISSUES], { type: 'or' } ) ) { return next({ status: 403, message: 'You do not have permission to view this issue.', }); } return res.status(200).json(issue); } catch (e) { logger.debug('Failed to retrieve issue.', { label: 'API', errorMessage: e.message, }); next({ status: 500, message: 'Issue not found.' }); } } ); issueRoutes.post<{ issueId: string }, Issue, { message: string }>( '/:issueId/comment', isAuthenticated([Permission.MANAGE_ISSUES, Permission.CREATE_ISSUES], { type: 'or', }), async (req, res, next) => { const issueRepository = getRepository(Issue); // Satisfy typescript here. User is set, we assure you! if (!req.user) { return next({ status: 500, message: 'User missing from request.' }); } try { const issue = await issueRepository.findOneOrFail({ where: { id: Number(req.params.issueId) }, }); if ( issue.createdBy.id !== req.user.id && !req.user.hasPermission(Permission.MANAGE_ISSUES) ) { return next({ status: 403, message: 'You do not have permission to comment on this issue.', }); } const comment = new IssueComment({ message: req.body.message, user: req.user, }); issue.comments = [...issue.comments, comment]; await issueRepository.save(issue); return res.status(200).json(issue); } catch (e) { logger.debug('Something went wrong creating an issue comment.', { label: 'API', errorMessage: e.message, }); next({ status: 500, message: 'Issue not found.' }); } } ); issueRoutes.post<{ issueId: string; status: string }, Issue>( '/:issueId/:status', isAuthenticated(Permission.MANAGE_ISSUES), async (req, res, next) => { const issueRepository = getRepository(Issue); // Satisfy typescript here. User is set, we assure you! if (!req.user) { return next({ status: 500, message: 'User missing from request.' }); } try { const issue = await issueRepository.findOneOrFail({ where: { id: Number(req.params.issueId) }, }); let newStatus: IssueStatus | undefined; switch (req.params.status) { case 'resolved': newStatus = IssueStatus.RESOLVED; break; case 'open': newStatus = IssueStatus.OPEN; } if (!newStatus) { return next({ status: 400, message: 'You must provide a valid status', }); } issue.status = newStatus; issue.modifiedBy = req.user; await issueRepository.save(issue); return res.status(200).json(issue); } catch (e) { logger.debug('Something went wrong creating an issue comment.', { label: 'API', errorMessage: e.message, }); next({ status: 500, message: 'Issue not found.' }); } } ); issueRoutes.delete( '/:issueId', isAuthenticated([Permission.MANAGE_ISSUES, Permission.CREATE_ISSUES], { type: 'or', }), async (req, res, next) => { const issueRepository = getRepository(Issue); try { const issue = await issueRepository.findOneOrFail({ where: { id: Number(req.params.issueId) }, relations: { createdBy: true }, }); if ( !req.user?.hasPermission(Permission.MANAGE_ISSUES) && (issue.createdBy.id !== req.user?.id || issue.comments.length > 1) ) { return next({ status: 401, message: 'You do not have permission to delete this issue.', }); } await issueRepository.remove(issue); return res.status(204).send(); } catch (e) { logger.error('Something went wrong deleting an issue.', { label: 'API', errorMessage: e.message, }); next({ status: 404, message: 'Issue not found.' }); } } ); export default issueRoutes;