mirror of
https://github.com/sct/overseerr.git
synced 2025-09-17 17:24:35 +02:00
feat(perms): add separate REQUEST_MOVIE and REQUEST_TV permissions (#1474)
* feat(perms): add separate REQUEST_MOVIE and REQUEST_TV permissions * fix(perms): do not require regular request perms for 4K requests
This commit is contained in:
@@ -139,287 +139,289 @@ requestRoutes.get('/', async (req, res, next) => {
|
||||
}
|
||||
});
|
||||
|
||||
requestRoutes.post(
|
||||
'/',
|
||||
isAuthenticated(Permission.REQUEST),
|
||||
async (req, res, next) => {
|
||||
const tmdb = new TheMovieDb();
|
||||
const mediaRepository = getRepository(Media);
|
||||
const requestRepository = getRepository(MediaRequest);
|
||||
const userRepository = getRepository(User);
|
||||
requestRoutes.post('/', async (req, res, next) => {
|
||||
const tmdb = new TheMovieDb();
|
||||
const mediaRepository = getRepository(Media);
|
||||
const requestRepository = getRepository(MediaRequest);
|
||||
const userRepository = getRepository(User);
|
||||
|
||||
try {
|
||||
let requestUser = req.user;
|
||||
try {
|
||||
let requestUser = req.user;
|
||||
|
||||
if (
|
||||
req.body.userId &&
|
||||
!req.user?.hasPermission([
|
||||
Permission.MANAGE_USERS,
|
||||
Permission.MANAGE_REQUESTS,
|
||||
])
|
||||
) {
|
||||
return next({
|
||||
status: 403,
|
||||
message: 'You do not have permission to modify the request user.',
|
||||
});
|
||||
} else if (req.body.userId) {
|
||||
requestUser = await userRepository.findOneOrFail({
|
||||
where: { id: req.body.userId },
|
||||
});
|
||||
}
|
||||
if (
|
||||
req.body.userId &&
|
||||
!req.user?.hasPermission([
|
||||
Permission.MANAGE_USERS,
|
||||
Permission.MANAGE_REQUESTS,
|
||||
])
|
||||
) {
|
||||
return next({
|
||||
status: 403,
|
||||
message: 'You do not have permission to modify the request user.',
|
||||
});
|
||||
} else if (req.body.userId) {
|
||||
requestUser = await userRepository.findOneOrFail({
|
||||
where: { id: req.body.userId },
|
||||
});
|
||||
}
|
||||
|
||||
if (!requestUser) {
|
||||
return next({
|
||||
status: 500,
|
||||
message: 'User missing from request context.',
|
||||
});
|
||||
}
|
||||
if (!requestUser) {
|
||||
return next({
|
||||
status: 500,
|
||||
message: 'User missing from request context.',
|
||||
});
|
||||
}
|
||||
|
||||
if (req.body.is4k) {
|
||||
if (
|
||||
req.body.mediaType === MediaType.MOVIE &&
|
||||
!req.user?.hasPermission(
|
||||
[Permission.REQUEST_4K, Permission.REQUEST_4K_MOVIE],
|
||||
{
|
||||
type: 'or',
|
||||
}
|
||||
)
|
||||
) {
|
||||
return next({
|
||||
status: 403,
|
||||
message: 'You do not have permission to make 4K movie requests.',
|
||||
});
|
||||
} else if (
|
||||
req.body.mediaType === MediaType.TV &&
|
||||
!req.user?.hasPermission(
|
||||
[Permission.REQUEST_4K, Permission.REQUEST_4K_TV],
|
||||
{
|
||||
type: 'or',
|
||||
}
|
||||
)
|
||||
) {
|
||||
return next({
|
||||
status: 403,
|
||||
message: 'You do not have permission to make 4K series requests.',
|
||||
});
|
||||
if (
|
||||
req.body.mediaType === MediaType.MOVIE &&
|
||||
!req.user?.hasPermission(
|
||||
req.body.is4k
|
||||
? [Permission.REQUEST_4K, Permission.REQUEST_4K_MOVIE]
|
||||
: [Permission.REQUEST, Permission.REQUEST_MOVIE],
|
||||
{
|
||||
type: 'or',
|
||||
}
|
||||
)
|
||||
) {
|
||||
return next({
|
||||
status: 403,
|
||||
message: `You do not have permission to make ${
|
||||
req.body.is4k ? '4K ' : ''
|
||||
}movie requests.`,
|
||||
});
|
||||
} else if (
|
||||
req.body.mediaType === MediaType.TV &&
|
||||
!req.user?.hasPermission(
|
||||
req.body.is4k
|
||||
? [Permission.REQUEST_4K, Permission.REQUEST_4K_TV]
|
||||
: [Permission.REQUEST, Permission.REQUEST_TV],
|
||||
{
|
||||
type: 'or',
|
||||
}
|
||||
)
|
||||
) {
|
||||
return next({
|
||||
status: 403,
|
||||
message: `You do not have permission to make ${
|
||||
req.body.is4k ? '4K ' : ''
|
||||
}series requests.`,
|
||||
});
|
||||
}
|
||||
|
||||
const quotas = await requestUser.getQuota();
|
||||
|
||||
if (req.body.mediaType === MediaType.MOVIE && quotas.movie.restricted) {
|
||||
return next({
|
||||
status: 403,
|
||||
message: 'Movie Quota Exceeded',
|
||||
});
|
||||
} else if (req.body.mediaType === MediaType.TV && quotas.tv.restricted) {
|
||||
return next({
|
||||
status: 403,
|
||||
message: 'Series Quota Exceeded',
|
||||
});
|
||||
}
|
||||
|
||||
const tmdbMedia =
|
||||
req.body.mediaType === MediaType.MOVIE
|
||||
? await tmdb.getMovie({ movieId: req.body.mediaId })
|
||||
: await tmdb.getTvShow({ tvId: req.body.mediaId });
|
||||
|
||||
let media = await mediaRepository.findOne({
|
||||
where: { tmdbId: req.body.mediaId, mediaType: req.body.mediaType },
|
||||
relations: ['requests'],
|
||||
});
|
||||
|
||||
if (!media) {
|
||||
media = new Media({
|
||||
tmdbId: tmdbMedia.id,
|
||||
tvdbId: req.body.tvdbId ?? tmdbMedia.external_ids.tvdb_id,
|
||||
status: !req.body.is4k ? MediaStatus.PENDING : MediaStatus.UNKNOWN,
|
||||
status4k: req.body.is4k ? MediaStatus.PENDING : MediaStatus.UNKNOWN,
|
||||
mediaType: req.body.mediaType,
|
||||
});
|
||||
} else {
|
||||
if (media.status === MediaStatus.UNKNOWN && !req.body.is4k) {
|
||||
media.status = MediaStatus.PENDING;
|
||||
}
|
||||
|
||||
const quotas = await requestUser.getQuota();
|
||||
|
||||
if (req.body.mediaType === MediaType.MOVIE && quotas.movie.restricted) {
|
||||
return next({
|
||||
status: 403,
|
||||
message: 'Movie Quota Exceeded',
|
||||
});
|
||||
} else if (req.body.mediaType === MediaType.TV && quotas.tv.restricted) {
|
||||
return next({
|
||||
status: 403,
|
||||
message: 'Series Quota Exceeded',
|
||||
});
|
||||
if (media.status4k === MediaStatus.UNKNOWN && req.body.is4k) {
|
||||
media.status4k = MediaStatus.PENDING;
|
||||
}
|
||||
}
|
||||
|
||||
const tmdbMedia =
|
||||
req.body.mediaType === MediaType.MOVIE
|
||||
? await tmdb.getMovie({ movieId: req.body.mediaId })
|
||||
: await tmdb.getTvShow({ tvId: req.body.mediaId });
|
||||
|
||||
let media = await mediaRepository.findOne({
|
||||
where: { tmdbId: req.body.mediaId, mediaType: req.body.mediaType },
|
||||
relations: ['requests'],
|
||||
if (req.body.mediaType === MediaType.MOVIE) {
|
||||
const existing = await requestRepository.findOne({
|
||||
where: {
|
||||
media: {
|
||||
tmdbId: tmdbMedia.id,
|
||||
},
|
||||
requestedBy: req.user,
|
||||
is4k: req.body.is4k,
|
||||
},
|
||||
});
|
||||
|
||||
if (!media) {
|
||||
media = new Media({
|
||||
if (existing) {
|
||||
logger.warn('Duplicate request for media blocked', {
|
||||
tmdbId: tmdbMedia.id,
|
||||
tvdbId: req.body.tvdbId ?? tmdbMedia.external_ids.tvdb_id,
|
||||
status: !req.body.is4k ? MediaStatus.PENDING : MediaStatus.UNKNOWN,
|
||||
status4k: req.body.is4k ? MediaStatus.PENDING : MediaStatus.UNKNOWN,
|
||||
mediaType: req.body.mediaType,
|
||||
});
|
||||
} else {
|
||||
if (media.status === MediaStatus.UNKNOWN && !req.body.is4k) {
|
||||
media.status = MediaStatus.PENDING;
|
||||
}
|
||||
|
||||
if (media.status4k === MediaStatus.UNKNOWN && req.body.is4k) {
|
||||
media.status4k = MediaStatus.PENDING;
|
||||
}
|
||||
return next({
|
||||
status: 409,
|
||||
message: 'Request for this media already exists.',
|
||||
});
|
||||
}
|
||||
|
||||
if (req.body.mediaType === MediaType.MOVIE) {
|
||||
const existing = await requestRepository.findOne({
|
||||
where: {
|
||||
media: {
|
||||
tmdbId: tmdbMedia.id,
|
||||
},
|
||||
requestedBy: req.user,
|
||||
is4k: req.body.is4k,
|
||||
},
|
||||
});
|
||||
await mediaRepository.save(media);
|
||||
|
||||
if (existing) {
|
||||
logger.warn('Duplicate request for media blocked', {
|
||||
tmdbId: tmdbMedia.id,
|
||||
mediaType: req.body.mediaType,
|
||||
});
|
||||
return next({
|
||||
status: 409,
|
||||
message: 'Request for this media already exists.',
|
||||
});
|
||||
}
|
||||
const request = new MediaRequest({
|
||||
type: MediaType.MOVIE,
|
||||
media,
|
||||
requestedBy: requestUser,
|
||||
// If the user is an admin or has the "auto approve" permission, automatically approve the request
|
||||
status: req.user?.hasPermission(
|
||||
[
|
||||
req.body.is4k
|
||||
? Permission.AUTO_APPROVE_4K
|
||||
: Permission.AUTO_APPROVE,
|
||||
req.body.is4k
|
||||
? Permission.AUTO_APPROVE_4K_MOVIE
|
||||
: Permission.AUTO_APPROVE_MOVIE,
|
||||
Permission.MANAGE_REQUESTS,
|
||||
],
|
||||
{ type: 'or' }
|
||||
)
|
||||
? MediaRequestStatus.APPROVED
|
||||
: MediaRequestStatus.PENDING,
|
||||
modifiedBy: req.user?.hasPermission(
|
||||
[
|
||||
req.body.is4k
|
||||
? Permission.AUTO_APPROVE_4K
|
||||
: Permission.AUTO_APPROVE,
|
||||
req.body.is4k
|
||||
? Permission.AUTO_APPROVE_4K_MOVIE
|
||||
: Permission.AUTO_APPROVE_MOVIE,
|
||||
Permission.MANAGE_REQUESTS,
|
||||
],
|
||||
{ type: 'or' }
|
||||
)
|
||||
? req.user
|
||||
: undefined,
|
||||
is4k: req.body.is4k,
|
||||
serverId: req.body.serverId,
|
||||
profileId: req.body.profileId,
|
||||
rootFolder: req.body.rootFolder,
|
||||
tags: req.body.tags,
|
||||
});
|
||||
|
||||
await mediaRepository.save(media);
|
||||
await requestRepository.save(request);
|
||||
return res.status(201).json(request);
|
||||
} else if (req.body.mediaType === MediaType.TV) {
|
||||
const requestedSeasons = req.body.seasons as number[];
|
||||
let existingSeasons: number[] = [];
|
||||
|
||||
const request = new MediaRequest({
|
||||
type: MediaType.MOVIE,
|
||||
media,
|
||||
requestedBy: requestUser,
|
||||
// If the user is an admin or has the "auto approve" permission, automatically approve the request
|
||||
status: req.user?.hasPermission(
|
||||
[
|
||||
req.body.is4k
|
||||
? Permission.AUTO_APPROVE_4K
|
||||
: Permission.AUTO_APPROVE,
|
||||
req.body.is4k
|
||||
? Permission.AUTO_APPROVE_4K_MOVIE
|
||||
: Permission.AUTO_APPROVE_MOVIE,
|
||||
Permission.MANAGE_REQUESTS,
|
||||
],
|
||||
{ type: 'or' }
|
||||
// We need to check existing requests on this title to make sure we don't double up on seasons that were
|
||||
// already requested. In the case they were, we just throw out any duplicates but still approve the request.
|
||||
// (Unless there are no seasons, in which case we abort)
|
||||
if (media.requests) {
|
||||
existingSeasons = media.requests
|
||||
.filter(
|
||||
(request) =>
|
||||
request.is4k === req.body.is4k &&
|
||||
request.status !== MediaRequestStatus.DECLINED
|
||||
)
|
||||
? MediaRequestStatus.APPROVED
|
||||
: MediaRequestStatus.PENDING,
|
||||
modifiedBy: req.user?.hasPermission(
|
||||
[
|
||||
req.body.is4k
|
||||
? Permission.AUTO_APPROVE_4K
|
||||
: Permission.AUTO_APPROVE,
|
||||
req.body.is4k
|
||||
? Permission.AUTO_APPROVE_4K_MOVIE
|
||||
: Permission.AUTO_APPROVE_MOVIE,
|
||||
Permission.MANAGE_REQUESTS,
|
||||
],
|
||||
{ type: 'or' }
|
||||
)
|
||||
? req.user
|
||||
: undefined,
|
||||
is4k: req.body.is4k,
|
||||
serverId: req.body.serverId,
|
||||
profileId: req.body.profileId,
|
||||
rootFolder: req.body.rootFolder,
|
||||
tags: req.body.tags,
|
||||
});
|
||||
.reduce((seasons, request) => {
|
||||
const combinedSeasons = request.seasons.map(
|
||||
(season) => season.seasonNumber
|
||||
);
|
||||
|
||||
await requestRepository.save(request);
|
||||
return res.status(201).json(request);
|
||||
} else if (req.body.mediaType === MediaType.TV) {
|
||||
const requestedSeasons = req.body.seasons as number[];
|
||||
let existingSeasons: number[] = [];
|
||||
|
||||
// We need to check existing requests on this title to make sure we don't double up on seasons that were
|
||||
// already requested. In the case they were, we just throw out any duplicates but still approve the request.
|
||||
// (Unless there are no seasons, in which case we abort)
|
||||
if (media.requests) {
|
||||
existingSeasons = media.requests
|
||||
.filter(
|
||||
(request) =>
|
||||
request.is4k === req.body.is4k &&
|
||||
request.status !== MediaRequestStatus.DECLINED
|
||||
)
|
||||
.reduce((seasons, request) => {
|
||||
const combinedSeasons = request.seasons.map(
|
||||
(season) => season.seasonNumber
|
||||
);
|
||||
|
||||
return [...seasons, ...combinedSeasons];
|
||||
}, [] as number[]);
|
||||
}
|
||||
|
||||
const finalSeasons = requestedSeasons.filter(
|
||||
(rs) => !existingSeasons.includes(rs)
|
||||
);
|
||||
|
||||
if (finalSeasons.length === 0) {
|
||||
return next({
|
||||
status: 202,
|
||||
message: 'No seasons available to request',
|
||||
});
|
||||
}
|
||||
|
||||
await mediaRepository.save(media);
|
||||
|
||||
const request = new MediaRequest({
|
||||
type: MediaType.TV,
|
||||
media,
|
||||
requestedBy: requestUser,
|
||||
// If the user is an admin or has the "auto approve" permission, automatically approve the request
|
||||
status: req.user?.hasPermission(
|
||||
[
|
||||
req.body.is4k
|
||||
? Permission.AUTO_APPROVE_4K
|
||||
: Permission.AUTO_APPROVE,
|
||||
req.body.is4k
|
||||
? Permission.AUTO_APPROVE_4K_TV
|
||||
: Permission.AUTO_APPROVE_TV,
|
||||
Permission.MANAGE_REQUESTS,
|
||||
],
|
||||
{ type: 'or' }
|
||||
)
|
||||
? MediaRequestStatus.APPROVED
|
||||
: MediaRequestStatus.PENDING,
|
||||
modifiedBy: req.user?.hasPermission(
|
||||
[
|
||||
req.body.is4k
|
||||
? Permission.AUTO_APPROVE_4K
|
||||
: Permission.AUTO_APPROVE,
|
||||
req.body.is4k
|
||||
? Permission.AUTO_APPROVE_4K_TV
|
||||
: Permission.AUTO_APPROVE_TV,
|
||||
Permission.MANAGE_REQUESTS,
|
||||
],
|
||||
{ type: 'or' }
|
||||
)
|
||||
? req.user
|
||||
: undefined,
|
||||
is4k: req.body.is4k,
|
||||
serverId: req.body.serverId,
|
||||
profileId: req.body.profileId,
|
||||
rootFolder: req.body.rootFolder,
|
||||
languageProfileId: req.body.languageProfileId,
|
||||
tags: req.body.tags,
|
||||
seasons: finalSeasons.map(
|
||||
(sn) =>
|
||||
new SeasonRequest({
|
||||
seasonNumber: sn,
|
||||
status: req.user?.hasPermission(
|
||||
[
|
||||
req.body.is4k
|
||||
? Permission.AUTO_APPROVE_4K
|
||||
: Permission.AUTO_APPROVE,
|
||||
req.body.is4k
|
||||
? Permission.AUTO_APPROVE_4K_TV
|
||||
: Permission.AUTO_APPROVE_TV,
|
||||
Permission.MANAGE_REQUESTS,
|
||||
],
|
||||
{ type: 'or' }
|
||||
)
|
||||
? MediaRequestStatus.APPROVED
|
||||
: MediaRequestStatus.PENDING,
|
||||
})
|
||||
),
|
||||
});
|
||||
|
||||
await requestRepository.save(request);
|
||||
return res.status(201).json(request);
|
||||
return [...seasons, ...combinedSeasons];
|
||||
}, [] as number[]);
|
||||
}
|
||||
|
||||
next({ status: 500, message: 'Invalid media type' });
|
||||
} catch (e) {
|
||||
next({ status: 500, message: e.message });
|
||||
const finalSeasons = requestedSeasons.filter(
|
||||
(rs) => !existingSeasons.includes(rs)
|
||||
);
|
||||
|
||||
if (finalSeasons.length === 0) {
|
||||
return next({
|
||||
status: 202,
|
||||
message: 'No seasons available to request',
|
||||
});
|
||||
}
|
||||
|
||||
await mediaRepository.save(media);
|
||||
|
||||
const request = new MediaRequest({
|
||||
type: MediaType.TV,
|
||||
media,
|
||||
requestedBy: requestUser,
|
||||
// If the user is an admin or has the "auto approve" permission, automatically approve the request
|
||||
status: req.user?.hasPermission(
|
||||
[
|
||||
req.body.is4k
|
||||
? Permission.AUTO_APPROVE_4K
|
||||
: Permission.AUTO_APPROVE,
|
||||
req.body.is4k
|
||||
? Permission.AUTO_APPROVE_4K_TV
|
||||
: Permission.AUTO_APPROVE_TV,
|
||||
Permission.MANAGE_REQUESTS,
|
||||
],
|
||||
{ type: 'or' }
|
||||
)
|
||||
? MediaRequestStatus.APPROVED
|
||||
: MediaRequestStatus.PENDING,
|
||||
modifiedBy: req.user?.hasPermission(
|
||||
[
|
||||
req.body.is4k
|
||||
? Permission.AUTO_APPROVE_4K
|
||||
: Permission.AUTO_APPROVE,
|
||||
req.body.is4k
|
||||
? Permission.AUTO_APPROVE_4K_TV
|
||||
: Permission.AUTO_APPROVE_TV,
|
||||
Permission.MANAGE_REQUESTS,
|
||||
],
|
||||
{ type: 'or' }
|
||||
)
|
||||
? req.user
|
||||
: undefined,
|
||||
is4k: req.body.is4k,
|
||||
serverId: req.body.serverId,
|
||||
profileId: req.body.profileId,
|
||||
rootFolder: req.body.rootFolder,
|
||||
languageProfileId: req.body.languageProfileId,
|
||||
tags: req.body.tags,
|
||||
seasons: finalSeasons.map(
|
||||
(sn) =>
|
||||
new SeasonRequest({
|
||||
seasonNumber: sn,
|
||||
status: req.user?.hasPermission(
|
||||
[
|
||||
req.body.is4k
|
||||
? Permission.AUTO_APPROVE_4K
|
||||
: Permission.AUTO_APPROVE,
|
||||
req.body.is4k
|
||||
? Permission.AUTO_APPROVE_4K_TV
|
||||
: Permission.AUTO_APPROVE_TV,
|
||||
Permission.MANAGE_REQUESTS,
|
||||
],
|
||||
{ type: 'or' }
|
||||
)
|
||||
? MediaRequestStatus.APPROVED
|
||||
: MediaRequestStatus.PENDING,
|
||||
})
|
||||
),
|
||||
});
|
||||
|
||||
await requestRepository.save(request);
|
||||
return res.status(201).json(request);
|
||||
}
|
||||
|
||||
next({ status: 500, message: 'Invalid media type' });
|
||||
} catch (e) {
|
||||
next({ status: 500, message: e.message });
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
requestRoutes.get('/count', async (_req, res, next) => {
|
||||
const requestRepository = getRepository(MediaRequest);
|
||||
|
Reference in New Issue
Block a user