mirror of
https://github.com/sct/overseerr.git
synced 2025-09-17 17:24:35 +02:00
feat: issues (#2180)
This commit is contained in:
325
server/routes/issue.ts
Normal file
325
server/routes/issue.ts
Normal file
@@ -0,0 +1,325 @@
|
||||
import { Router } from 'express';
|
||||
import { getRepository } from 'typeorm';
|
||||
import { IssueStatus } from '../constants/issue';
|
||||
import Issue from '../entity/Issue';
|
||||
import IssueComment from '../entity/IssueComment';
|
||||
import Media from '../entity/Media';
|
||||
import { IssueResultsResponse } from '../interfaces/api/issueInterfaces';
|
||||
import { Permission } from '../lib/permissions';
|
||||
import logger from '../logger';
|
||||
import { isAuthenticated } from '../middleware/auth';
|
||||
|
||||
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 created 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<{ 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;
|
||||
|
||||
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', async (req, res, next) => {
|
||||
const issueRepository = getRepository(Issue);
|
||||
|
||||
try {
|
||||
const issue = await issueRepository.findOneOrFail({
|
||||
where: { id: Number(req.params.issueId) },
|
||||
relations: ['createdBy'],
|
||||
});
|
||||
|
||||
if (
|
||||
!req.user?.hasPermission(Permission.MANAGE_ISSUES) &&
|
||||
issue.createdBy.id !== req.user?.id
|
||||
) {
|
||||
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;
|
Reference in New Issue
Block a user