You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
overseerr/server/routes/issue.ts

407 lines
11 KiB

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<Record<string, string>, 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<string, string>,
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, 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 (
!req.user?.hasPermission(Permission.MANAGE_ISSUES) &&
issue.createdBy.id !== req.user?.id
) {
return next({
status: 401,
message: 'You do not have permission to modify this issue.',
});
}
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;