Merge branch 'develop' of https://github.com/sct/overseerr into jellyfin-support
This commit is contained in:
@@ -1,14 +1,14 @@
|
||||
import { Router } from 'express';
|
||||
import { getRepository } from 'typeorm';
|
||||
import { User } from '../entity/User';
|
||||
import PlexTvAPI from '../api/plextv';
|
||||
import JellyfinAPI from '../api/jellyfin';
|
||||
import { isAuthenticated } from '../middleware/auth';
|
||||
import { Permission } from '../lib/permissions';
|
||||
import logger from '../logger';
|
||||
import { getSettings } from '../lib/settings';
|
||||
import { UserType } from '../constants/user';
|
||||
import PlexTvAPI from '../api/plextv';
|
||||
import { MediaServerType } from '../constants/server';
|
||||
import { UserType } from '../constants/user';
|
||||
import { User } from '../entity/User';
|
||||
import { Permission } from '../lib/permissions';
|
||||
import { getSettings } from '../lib/settings';
|
||||
import logger from '../logger';
|
||||
import { isAuthenticated } from '../middleware/auth';
|
||||
|
||||
const authRoutes = Router();
|
||||
|
||||
@@ -51,9 +51,13 @@ authRoutes.post('/plex', async (req, res, next) => {
|
||||
const account = await plextv.getUser();
|
||||
|
||||
// Next let's see if the user already exists
|
||||
let user = await userRepository.findOne({
|
||||
where: { plexId: account.id },
|
||||
});
|
||||
let user = await userRepository
|
||||
.createQueryBuilder('user')
|
||||
.where('user.plexId = :id', { id: account.id })
|
||||
.orWhere('user.email = :email', {
|
||||
email: account.email.toLowerCase(),
|
||||
})
|
||||
.getOne();
|
||||
|
||||
if (user) {
|
||||
// Let's check if their Plex token is up-to-date
|
||||
@@ -66,9 +70,12 @@ authRoutes.post('/plex', async (req, res, next) => {
|
||||
user.email = account.email;
|
||||
user.plexUsername = account.username;
|
||||
|
||||
if (user.username === account.username) {
|
||||
user.username = '';
|
||||
// In case the user was previously a local account
|
||||
if (user.userType === UserType.LOCAL) {
|
||||
user.userType = UserType.PLEX;
|
||||
user.plexId = account.id;
|
||||
}
|
||||
|
||||
await userRepository.save(user);
|
||||
} else {
|
||||
// Here we check if it's the first user. If it is, we create the user with no check
|
||||
@@ -93,6 +100,24 @@ authRoutes.post('/plex', async (req, res, next) => {
|
||||
|
||||
// Double check that we didn't create the first admin user before running this
|
||||
if (!user) {
|
||||
if (!settings.main.newPlexLogin) {
|
||||
logger.info(
|
||||
'Failed sign-in attempt from user who has not been imported to Overseerr.',
|
||||
{
|
||||
label: 'Auth',
|
||||
account: {
|
||||
...account,
|
||||
authentication_token: '__REDACTED__',
|
||||
authToken: '__REDACTED__',
|
||||
},
|
||||
}
|
||||
);
|
||||
return next({
|
||||
status: 403,
|
||||
message: 'Access denied.',
|
||||
});
|
||||
}
|
||||
|
||||
// If we get to this point, the user does not already exist so we need to create the
|
||||
// user _assuming_ they have access to the Plex server
|
||||
const mainUser = await userRepository.findOneOrFail({
|
||||
@@ -126,7 +151,7 @@ authRoutes.post('/plex', async (req, res, next) => {
|
||||
);
|
||||
return next({
|
||||
status: 403,
|
||||
message: 'You do not have access to this Plex server.',
|
||||
message: 'Access denied.',
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -142,7 +167,7 @@ authRoutes.post('/plex', async (req, res, next) => {
|
||||
logger.error(e.message, { label: 'Auth' });
|
||||
return next({
|
||||
status: 500,
|
||||
message: 'Something went wrong. Is your auth token valid?',
|
||||
message: 'Something went wrong.',
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -322,10 +347,11 @@ authRoutes.post('/local', async (req, res, next) => {
|
||||
});
|
||||
}
|
||||
try {
|
||||
const user = await userRepository.findOne({
|
||||
select: ['id', 'password'],
|
||||
where: { email: body.email },
|
||||
});
|
||||
const user = await userRepository
|
||||
.createQueryBuilder('user')
|
||||
.select(['user.id', 'user.password'])
|
||||
.where('user.email = :email', { email: body.email.toLowerCase() })
|
||||
.getOne();
|
||||
|
||||
const isCorrectCredentials = await user?.passwordMatch(body.password);
|
||||
|
||||
@@ -389,9 +415,10 @@ authRoutes.post('/reset-password', async (req, res) => {
|
||||
.json({ error: 'You must provide an email address.' });
|
||||
}
|
||||
|
||||
const user = await userRepository.findOne({
|
||||
where: { email: body.email },
|
||||
});
|
||||
const user = await userRepository
|
||||
.createQueryBuilder('user')
|
||||
.where('user.email = :email', { email: body.email.toLowerCase() })
|
||||
.getOne();
|
||||
|
||||
if (user) {
|
||||
await user.resetPassword();
|
||||
|
||||
@@ -11,7 +11,7 @@ collectionRoutes.get<{ id: string }>('/:id', async (req, res, next) => {
|
||||
try {
|
||||
const collection = await tmdb.getCollection({
|
||||
collectionId: Number(req.params.id),
|
||||
language: req.query.language as string,
|
||||
language: req.locale ?? (req.query.language as string),
|
||||
});
|
||||
|
||||
const media = await Media.getRelatedMedia(
|
||||
|
||||
@@ -1,11 +1,16 @@
|
||||
import { Router } from 'express';
|
||||
import { sortBy } from 'lodash';
|
||||
import TheMovieDb from '../api/themoviedb';
|
||||
import { mapMovieResult, mapTvResult, mapPersonResult } from '../models/Search';
|
||||
import Media from '../entity/Media';
|
||||
import { isMovie, isPerson } from '../utils/typeHelpers';
|
||||
import { MediaType } from '../constants/media';
|
||||
import { getSettings } from '../lib/settings';
|
||||
import Media from '../entity/Media';
|
||||
import { User } from '../entity/User';
|
||||
import { GenreSliderItem } from '../interfaces/api/discoverInterfaces';
|
||||
import { getSettings } from '../lib/settings';
|
||||
import logger from '../logger';
|
||||
import { mapProductionCompany } from '../models/Movie';
|
||||
import { mapMovieResult, mapPersonResult, mapTvResult } from '../models/Search';
|
||||
import { mapNetwork } from '../models/Tv';
|
||||
import { isMovie, isPerson } from '../utils/typeHelpers';
|
||||
|
||||
const createTmdbWithRegionLanaguage = (user?: User): TheMovieDb => {
|
||||
const settings = getSettings();
|
||||
@@ -37,7 +42,9 @@ discoverRoutes.get('/movies', async (req, res) => {
|
||||
|
||||
const data = await tmdb.getDiscoverMovies({
|
||||
page: Number(req.query.page),
|
||||
language: req.query.language as string,
|
||||
language: req.locale ?? (req.query.language as string),
|
||||
genre: req.query.genre ? Number(req.query.genre) : undefined,
|
||||
studio: req.query.studio ? Number(req.query.studio) : undefined,
|
||||
});
|
||||
|
||||
const media = await Media.getRelatedMedia(
|
||||
@@ -59,6 +66,133 @@ discoverRoutes.get('/movies', async (req, res) => {
|
||||
});
|
||||
});
|
||||
|
||||
discoverRoutes.get<{ language: string }>(
|
||||
'/movies/language/:language',
|
||||
async (req, res, next) => {
|
||||
const tmdb = createTmdbWithRegionLanaguage(req.user);
|
||||
|
||||
const languages = await tmdb.getLanguages();
|
||||
|
||||
const language = languages.find(
|
||||
(lang) => lang.iso_639_1 === req.params.language
|
||||
);
|
||||
|
||||
if (!language) {
|
||||
return next({ status: 404, message: 'Unable to retrieve language' });
|
||||
}
|
||||
|
||||
const data = await tmdb.getDiscoverMovies({
|
||||
page: Number(req.query.page),
|
||||
language: req.locale ?? (req.query.language as string),
|
||||
originalLanguage: req.params.language,
|
||||
});
|
||||
|
||||
const media = await Media.getRelatedMedia(
|
||||
data.results.map((result) => result.id)
|
||||
);
|
||||
|
||||
return res.status(200).json({
|
||||
page: data.page,
|
||||
totalPages: data.total_pages,
|
||||
totalResults: data.total_results,
|
||||
language,
|
||||
results: data.results.map((result) =>
|
||||
mapMovieResult(
|
||||
result,
|
||||
media.find(
|
||||
(req) =>
|
||||
req.tmdbId === result.id && req.mediaType === MediaType.MOVIE
|
||||
)
|
||||
)
|
||||
),
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
discoverRoutes.get<{ genreId: string }>(
|
||||
'/movies/genre/:genreId',
|
||||
async (req, res, next) => {
|
||||
const tmdb = createTmdbWithRegionLanaguage(req.user);
|
||||
|
||||
const genres = await tmdb.getMovieGenres({
|
||||
language: req.locale ?? (req.query.language as string),
|
||||
});
|
||||
|
||||
const genre = genres.find(
|
||||
(genre) => genre.id === Number(req.params.genreId)
|
||||
);
|
||||
|
||||
if (!genre) {
|
||||
return next({ status: 404, message: 'Unable to retrieve genre' });
|
||||
}
|
||||
|
||||
const data = await tmdb.getDiscoverMovies({
|
||||
page: Number(req.query.page),
|
||||
language: req.locale ?? (req.query.language as string),
|
||||
genre: Number(req.params.genreId),
|
||||
});
|
||||
|
||||
const media = await Media.getRelatedMedia(
|
||||
data.results.map((result) => result.id)
|
||||
);
|
||||
|
||||
return res.status(200).json({
|
||||
page: data.page,
|
||||
totalPages: data.total_pages,
|
||||
totalResults: data.total_results,
|
||||
genre,
|
||||
results: data.results.map((result) =>
|
||||
mapMovieResult(
|
||||
result,
|
||||
media.find(
|
||||
(req) =>
|
||||
req.tmdbId === result.id && req.mediaType === MediaType.MOVIE
|
||||
)
|
||||
)
|
||||
),
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
discoverRoutes.get<{ studioId: string }>(
|
||||
'/movies/studio/:studioId',
|
||||
async (req, res, next) => {
|
||||
const tmdb = new TheMovieDb();
|
||||
|
||||
try {
|
||||
const studio = await tmdb.getStudio(Number(req.params.studioId));
|
||||
|
||||
const data = await tmdb.getDiscoverMovies({
|
||||
page: Number(req.query.page),
|
||||
language: req.locale ?? (req.query.language as string),
|
||||
studio: Number(req.params.studioId),
|
||||
});
|
||||
|
||||
const media = await Media.getRelatedMedia(
|
||||
data.results.map((result) => result.id)
|
||||
);
|
||||
|
||||
return res.status(200).json({
|
||||
page: data.page,
|
||||
totalPages: data.total_pages,
|
||||
totalResults: data.total_results,
|
||||
studio: mapProductionCompany(studio),
|
||||
results: data.results.map((result) =>
|
||||
mapMovieResult(
|
||||
result,
|
||||
media.find(
|
||||
(med) =>
|
||||
med.tmdbId === result.id && med.mediaType === MediaType.MOVIE
|
||||
)
|
||||
)
|
||||
),
|
||||
});
|
||||
} catch (e) {
|
||||
return next({ status: 404, message: 'Unable to retrieve studio' });
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
discoverRoutes.get('/movies/upcoming', async (req, res) => {
|
||||
const tmdb = createTmdbWithRegionLanaguage(req.user);
|
||||
|
||||
@@ -70,7 +204,7 @@ discoverRoutes.get('/movies/upcoming', async (req, res) => {
|
||||
|
||||
const data = await tmdb.getDiscoverMovies({
|
||||
page: Number(req.query.page),
|
||||
language: req.query.language as string,
|
||||
language: req.locale ?? (req.query.language as string),
|
||||
primaryReleaseDateGte: date,
|
||||
});
|
||||
|
||||
@@ -98,7 +232,9 @@ discoverRoutes.get('/tv', async (req, res) => {
|
||||
|
||||
const data = await tmdb.getDiscoverTv({
|
||||
page: Number(req.query.page),
|
||||
language: req.query.language as string,
|
||||
language: req.locale ?? (req.query.language as string),
|
||||
genre: req.query.genre ? Number(req.query.genre) : undefined,
|
||||
network: req.query.network ? Number(req.query.network) : undefined,
|
||||
});
|
||||
|
||||
const media = await Media.getRelatedMedia(
|
||||
@@ -120,6 +256,131 @@ discoverRoutes.get('/tv', async (req, res) => {
|
||||
});
|
||||
});
|
||||
|
||||
discoverRoutes.get<{ language: string }>(
|
||||
'/tv/language/:language',
|
||||
async (req, res, next) => {
|
||||
const tmdb = createTmdbWithRegionLanaguage(req.user);
|
||||
|
||||
const languages = await tmdb.getLanguages();
|
||||
|
||||
const language = languages.find(
|
||||
(lang) => lang.iso_639_1 === req.params.language
|
||||
);
|
||||
|
||||
if (!language) {
|
||||
return next({ status: 404, message: 'Unable to retrieve language' });
|
||||
}
|
||||
|
||||
const data = await tmdb.getDiscoverTv({
|
||||
page: Number(req.query.page),
|
||||
language: req.locale ?? (req.query.language as string),
|
||||
originalLanguage: req.params.language,
|
||||
});
|
||||
|
||||
const media = await Media.getRelatedMedia(
|
||||
data.results.map((result) => result.id)
|
||||
);
|
||||
|
||||
return res.status(200).json({
|
||||
page: data.page,
|
||||
totalPages: data.total_pages,
|
||||
totalResults: data.total_results,
|
||||
language,
|
||||
results: data.results.map((result) =>
|
||||
mapTvResult(
|
||||
result,
|
||||
media.find(
|
||||
(med) => med.tmdbId === result.id && med.mediaType === MediaType.TV
|
||||
)
|
||||
)
|
||||
),
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
discoverRoutes.get<{ genreId: string }>(
|
||||
'/tv/genre/:genreId',
|
||||
async (req, res, next) => {
|
||||
const tmdb = createTmdbWithRegionLanaguage(req.user);
|
||||
|
||||
const genres = await tmdb.getTvGenres({
|
||||
language: req.locale ?? (req.query.language as string),
|
||||
});
|
||||
|
||||
const genre = genres.find(
|
||||
(genre) => genre.id === Number(req.params.genreId)
|
||||
);
|
||||
|
||||
if (!genre) {
|
||||
return next({ status: 404, message: 'Unable to retrieve genre' });
|
||||
}
|
||||
|
||||
const data = await tmdb.getDiscoverTv({
|
||||
page: Number(req.query.page),
|
||||
language: req.locale ?? (req.query.language as string),
|
||||
genre: Number(req.params.genreId),
|
||||
});
|
||||
|
||||
const media = await Media.getRelatedMedia(
|
||||
data.results.map((result) => result.id)
|
||||
);
|
||||
|
||||
return res.status(200).json({
|
||||
page: data.page,
|
||||
totalPages: data.total_pages,
|
||||
totalResults: data.total_results,
|
||||
genre,
|
||||
results: data.results.map((result) =>
|
||||
mapTvResult(
|
||||
result,
|
||||
media.find(
|
||||
(med) => med.tmdbId === result.id && med.mediaType === MediaType.TV
|
||||
)
|
||||
)
|
||||
),
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
discoverRoutes.get<{ networkId: string }>(
|
||||
'/tv/network/:networkId',
|
||||
async (req, res, next) => {
|
||||
const tmdb = new TheMovieDb();
|
||||
|
||||
try {
|
||||
const network = await tmdb.getNetwork(Number(req.params.networkId));
|
||||
|
||||
const data = await tmdb.getDiscoverTv({
|
||||
page: Number(req.query.page),
|
||||
language: req.locale ?? (req.query.language as string),
|
||||
network: Number(req.params.networkId),
|
||||
});
|
||||
|
||||
const media = await Media.getRelatedMedia(
|
||||
data.results.map((result) => result.id)
|
||||
);
|
||||
|
||||
return res.status(200).json({
|
||||
page: data.page,
|
||||
totalPages: data.total_pages,
|
||||
totalResults: data.total_results,
|
||||
network: mapNetwork(network),
|
||||
results: data.results.map((result) =>
|
||||
mapTvResult(
|
||||
result,
|
||||
media.find(
|
||||
(med) =>
|
||||
med.tmdbId === result.id && med.mediaType === MediaType.TV
|
||||
)
|
||||
)
|
||||
),
|
||||
});
|
||||
} catch (e) {
|
||||
return next({ status: 404, message: 'Unable to retrieve network' });
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
discoverRoutes.get('/tv/upcoming', async (req, res) => {
|
||||
const tmdb = createTmdbWithRegionLanaguage(req.user);
|
||||
|
||||
@@ -131,7 +392,7 @@ discoverRoutes.get('/tv/upcoming', async (req, res) => {
|
||||
|
||||
const data = await tmdb.getDiscoverTv({
|
||||
page: Number(req.query.page),
|
||||
language: req.query.language as string,
|
||||
language: req.locale ?? (req.query.language as string),
|
||||
firstAirDateGte: date,
|
||||
});
|
||||
|
||||
@@ -159,7 +420,7 @@ discoverRoutes.get('/trending', async (req, res) => {
|
||||
|
||||
const data = await tmdb.getAllTrending({
|
||||
page: Number(req.query.page),
|
||||
language: req.query.language as string,
|
||||
language: req.locale ?? (req.query.language as string),
|
||||
});
|
||||
|
||||
const media = await Media.getRelatedMedia(
|
||||
@@ -175,15 +436,18 @@ discoverRoutes.get('/trending', async (req, res) => {
|
||||
? mapMovieResult(
|
||||
result,
|
||||
media.find(
|
||||
(req) =>
|
||||
req.tmdbId === result.id && req.mediaType === MediaType.MOVIE
|
||||
(med) =>
|
||||
med.tmdbId === result.id && med.mediaType === MediaType.MOVIE
|
||||
)
|
||||
)
|
||||
: isPerson(result)
|
||||
? mapPersonResult(result)
|
||||
: mapTvResult(
|
||||
result,
|
||||
media.find((req) => req.tmdbId === result.id && MediaType.TV)
|
||||
media.find(
|
||||
(med) =>
|
||||
med.tmdbId === result.id && med.mediaType === MediaType.TV
|
||||
)
|
||||
)
|
||||
),
|
||||
});
|
||||
@@ -197,7 +461,7 @@ discoverRoutes.get<{ keywordId: string }>(
|
||||
const data = await tmdb.getMoviesByKeyword({
|
||||
keywordId: Number(req.params.keywordId),
|
||||
page: Number(req.query.page),
|
||||
language: req.query.language as string,
|
||||
language: req.locale ?? (req.query.language as string),
|
||||
});
|
||||
|
||||
const media = await Media.getRelatedMedia(
|
||||
@@ -212,8 +476,8 @@ discoverRoutes.get<{ keywordId: string }>(
|
||||
mapMovieResult(
|
||||
result,
|
||||
media.find(
|
||||
(req) =>
|
||||
req.tmdbId === result.id && req.mediaType === MediaType.MOVIE
|
||||
(med) =>
|
||||
med.tmdbId === result.id && med.mediaType === MediaType.MOVIE
|
||||
)
|
||||
)
|
||||
),
|
||||
@@ -221,4 +485,86 @@ discoverRoutes.get<{ keywordId: string }>(
|
||||
}
|
||||
);
|
||||
|
||||
discoverRoutes.get<{ language: string }, GenreSliderItem[]>(
|
||||
'/genreslider/movie',
|
||||
async (req, res, next) => {
|
||||
const tmdb = new TheMovieDb();
|
||||
|
||||
try {
|
||||
const mappedGenres: GenreSliderItem[] = [];
|
||||
|
||||
const genres = await tmdb.getMovieGenres({
|
||||
language: req.locale ?? (req.query.language as string),
|
||||
});
|
||||
|
||||
await Promise.all(
|
||||
genres.map(async (genre) => {
|
||||
const genreData = await tmdb.getDiscoverMovies({ genre: genre.id });
|
||||
|
||||
mappedGenres.push({
|
||||
id: genre.id,
|
||||
name: genre.name,
|
||||
backdrops: genreData.results
|
||||
.filter((title) => !!title.backdrop_path)
|
||||
.map((title) => title.backdrop_path) as string[],
|
||||
});
|
||||
})
|
||||
);
|
||||
|
||||
const sortedData = sortBy(mappedGenres, 'name');
|
||||
|
||||
return res.status(200).json(sortedData);
|
||||
} catch (e) {
|
||||
logger.error('Something went wrong retrieving the movie genre slider', {
|
||||
errorMessage: e.message,
|
||||
});
|
||||
return next({
|
||||
status: 500,
|
||||
message: 'Unable to retrieve movie genre slider.',
|
||||
});
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
discoverRoutes.get<{ language: string }, GenreSliderItem[]>(
|
||||
'/genreslider/tv',
|
||||
async (req, res, next) => {
|
||||
const tmdb = new TheMovieDb();
|
||||
|
||||
try {
|
||||
const mappedGenres: GenreSliderItem[] = [];
|
||||
|
||||
const genres = await tmdb.getTvGenres({
|
||||
language: req.locale ?? (req.query.language as string),
|
||||
});
|
||||
|
||||
await Promise.all(
|
||||
genres.map(async (genre) => {
|
||||
const genreData = await tmdb.getDiscoverTv({ genre: genre.id });
|
||||
|
||||
mappedGenres.push({
|
||||
id: genre.id,
|
||||
name: genre.name,
|
||||
backdrops: genreData.results
|
||||
.filter((title) => !!title.backdrop_path)
|
||||
.map((title) => title.backdrop_path) as string[],
|
||||
});
|
||||
})
|
||||
);
|
||||
|
||||
const sortedData = sortBy(mappedGenres, 'name');
|
||||
|
||||
return res.status(200).json(sortedData);
|
||||
} catch (e) {
|
||||
logger.error('Something went wrong retrieving the tv genre slider', {
|
||||
errorMessage: e.message,
|
||||
});
|
||||
return next({
|
||||
status: 500,
|
||||
message: 'Unable to retrieve tv genre slider.',
|
||||
});
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
export default discoverRoutes;
|
||||
|
||||
@@ -1,31 +1,75 @@
|
||||
import { Router } from 'express';
|
||||
import user from './user';
|
||||
import authRoutes from './auth';
|
||||
import { checkUser, isAuthenticated } from '../middleware/auth';
|
||||
import settingsRoutes from './settings';
|
||||
import GithubAPI from '../api/github';
|
||||
import TheMovieDb from '../api/themoviedb';
|
||||
import { StatusResponse } from '../interfaces/api/settingsInterfaces';
|
||||
import { Permission } from '../lib/permissions';
|
||||
import { getSettings } from '../lib/settings';
|
||||
import searchRoutes from './search';
|
||||
import discoverRoutes from './discover';
|
||||
import requestRoutes from './request';
|
||||
import movieRoutes from './movie';
|
||||
import tvRoutes from './tv';
|
||||
import mediaRoutes from './media';
|
||||
import personRoutes from './person';
|
||||
import collectionRoutes from './collection';
|
||||
import { checkUser, isAuthenticated } from '../middleware/auth';
|
||||
import { mapProductionCompany } from '../models/Movie';
|
||||
import { mapNetwork } from '../models/Tv';
|
||||
import { appDataPath, appDataStatus } from '../utils/appDataVolume';
|
||||
import { getAppVersion, getCommitTag } from '../utils/appVersion';
|
||||
import authRoutes from './auth';
|
||||
import collectionRoutes from './collection';
|
||||
import discoverRoutes from './discover';
|
||||
import mediaRoutes from './media';
|
||||
import movieRoutes from './movie';
|
||||
import personRoutes from './person';
|
||||
import requestRoutes from './request';
|
||||
import searchRoutes from './search';
|
||||
import serviceRoutes from './service';
|
||||
import { appDataStatus, appDataPath } from '../utils/appDataVolume';
|
||||
import TheMovieDb from '../api/themoviedb';
|
||||
import settingsRoutes from './settings';
|
||||
import tvRoutes from './tv';
|
||||
import user from './user';
|
||||
|
||||
const router = Router();
|
||||
|
||||
router.use(checkUser);
|
||||
|
||||
router.get('/status', (req, res) => {
|
||||
router.get<unknown, StatusResponse>('/status', async (req, res) => {
|
||||
const githubApi = new GithubAPI();
|
||||
|
||||
const currentVersion = getAppVersion();
|
||||
const commitTag = getCommitTag();
|
||||
let updateAvailable = false;
|
||||
let commitsBehind = 0;
|
||||
|
||||
if (currentVersion.startsWith('develop-') && commitTag !== 'local') {
|
||||
const commits = await githubApi.getOverseerrCommits();
|
||||
|
||||
if (commits.length) {
|
||||
const filteredCommits = commits.filter(
|
||||
(commit) => !commit.commit.message.includes('[skip ci]')
|
||||
);
|
||||
if (filteredCommits[0].sha !== commitTag) {
|
||||
updateAvailable = true;
|
||||
}
|
||||
|
||||
const commitIndex = filteredCommits.findIndex(
|
||||
(commit) => commit.sha === commitTag
|
||||
);
|
||||
|
||||
if (updateAvailable) {
|
||||
commitsBehind = commitIndex;
|
||||
}
|
||||
}
|
||||
} else if (commitTag !== 'local') {
|
||||
const releases = await githubApi.getOverseerrReleases();
|
||||
|
||||
if (releases.length) {
|
||||
const latestVersion = releases[0];
|
||||
|
||||
if (!latestVersion.name.includes(currentVersion)) {
|
||||
updateAvailable = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return res.status(200).json({
|
||||
version: getAppVersion(),
|
||||
commitTag: getCommitTag(),
|
||||
updateAvailable,
|
||||
commitsBehind,
|
||||
});
|
||||
});
|
||||
|
||||
@@ -37,10 +81,16 @@ router.get('/status/appdata', (_req, res) => {
|
||||
});
|
||||
|
||||
router.use('/user', isAuthenticated(), user);
|
||||
router.get('/settings/public', (_req, res) => {
|
||||
router.get('/settings/public', async (req, res) => {
|
||||
const settings = getSettings();
|
||||
|
||||
return res.status(200).json(settings.fullPublicSettings);
|
||||
if (!(req.user?.settings?.notificationTypes.webpush ?? true)) {
|
||||
return res
|
||||
.status(200)
|
||||
.json({ ...settings.fullPublicSettings, enablePushRegistration: false });
|
||||
} else {
|
||||
return res.status(200).json(settings.fullPublicSettings);
|
||||
}
|
||||
});
|
||||
router.use(
|
||||
'/settings',
|
||||
@@ -74,6 +124,42 @@ router.get('/languages', isAuthenticated(), async (req, res) => {
|
||||
return res.status(200).json(languages);
|
||||
});
|
||||
|
||||
router.get<{ id: string }>('/studio/:id', async (req, res) => {
|
||||
const tmdb = new TheMovieDb();
|
||||
|
||||
const studio = await tmdb.getStudio(Number(req.params.id));
|
||||
|
||||
return res.status(200).json(mapProductionCompany(studio));
|
||||
});
|
||||
|
||||
router.get<{ id: string }>('/network/:id', async (req, res) => {
|
||||
const tmdb = new TheMovieDb();
|
||||
|
||||
const network = await tmdb.getNetwork(Number(req.params.id));
|
||||
|
||||
return res.status(200).json(mapNetwork(network));
|
||||
});
|
||||
|
||||
router.get('/genres/movie', isAuthenticated(), async (req, res) => {
|
||||
const tmdb = new TheMovieDb();
|
||||
|
||||
const genres = await tmdb.getMovieGenres({
|
||||
language: req.locale ?? (req.query.language as string),
|
||||
});
|
||||
|
||||
return res.status(200).json(genres);
|
||||
});
|
||||
|
||||
router.get('/genres/tv', isAuthenticated(), async (req, res) => {
|
||||
const tmdb = new TheMovieDb();
|
||||
|
||||
const genres = await tmdb.getTvGenres({
|
||||
language: req.locale ?? (req.query.language as string),
|
||||
});
|
||||
|
||||
return res.status(200).json(genres);
|
||||
});
|
||||
|
||||
router.get('/', (_req, res) => {
|
||||
return res.status(200).json({
|
||||
api: 'Overseerr API',
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { Router } from 'express';
|
||||
import RottenTomatoes from '../api/rottentomatoes';
|
||||
import TheMovieDb from '../api/themoviedb';
|
||||
import { MediaType } from '../constants/media';
|
||||
import Media from '../entity/Media';
|
||||
import logger from '../logger';
|
||||
import { mapMovieDetails } from '../models/Movie';
|
||||
import { mapMovieResult } from '../models/Search';
|
||||
import Media from '../entity/Media';
|
||||
import RottenTomatoes from '../api/rottentomatoes';
|
||||
import logger from '../logger';
|
||||
import { MediaType } from '../constants/media';
|
||||
|
||||
const movieRoutes = Router();
|
||||
|
||||
@@ -15,7 +15,7 @@ movieRoutes.get('/:id', async (req, res, next) => {
|
||||
try {
|
||||
const tmdbMovie = await tmdb.getMovie({
|
||||
movieId: Number(req.params.id),
|
||||
language: req.query.language as string,
|
||||
language: req.locale ?? (req.query.language as string),
|
||||
});
|
||||
|
||||
const media = await Media.getMedia(tmdbMovie.id, MediaType.MOVIE);
|
||||
@@ -36,7 +36,7 @@ movieRoutes.get('/:id/recommendations', async (req, res) => {
|
||||
const results = await tmdb.getMovieRecommendations({
|
||||
movieId: Number(req.params.id),
|
||||
page: Number(req.query.page),
|
||||
language: req.query.language as string,
|
||||
language: req.locale ?? (req.query.language as string),
|
||||
});
|
||||
|
||||
const media = await Media.getRelatedMedia(
|
||||
@@ -64,7 +64,7 @@ movieRoutes.get('/:id/similar', async (req, res) => {
|
||||
const results = await tmdb.getMovieSimilar({
|
||||
movieId: Number(req.params.id),
|
||||
page: Number(req.query.page),
|
||||
language: req.query.language as string,
|
||||
language: req.locale ?? (req.query.language as string),
|
||||
});
|
||||
|
||||
const media = await Media.getRelatedMedia(
|
||||
|
||||
@@ -16,7 +16,7 @@ personRoutes.get('/:id', async (req, res, next) => {
|
||||
try {
|
||||
const person = await tmdb.getPerson({
|
||||
personId: Number(req.params.id),
|
||||
language: req.query.language as string,
|
||||
language: req.locale ?? (req.query.language as string),
|
||||
});
|
||||
return res.status(200).json(mapPersonDetails(person));
|
||||
} catch (e) {
|
||||
@@ -30,7 +30,7 @@ personRoutes.get('/:id/combined_credits', async (req, res) => {
|
||||
|
||||
const combinedCredits = await tmdb.getPersonCombinedCredits({
|
||||
personId: Number(req.params.id),
|
||||
language: req.query.language as string,
|
||||
language: req.locale ?? (req.query.language as string),
|
||||
});
|
||||
|
||||
const castMedia = await Media.getRelatedMedia(
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
import { Router } from 'express';
|
||||
import { isAuthenticated } from '../middleware/auth';
|
||||
import { Permission } from '../lib/permissions';
|
||||
import { getRepository } from 'typeorm';
|
||||
import { MediaRequest } from '../entity/MediaRequest';
|
||||
import TheMovieDb from '../api/themoviedb';
|
||||
import { MediaRequestStatus, MediaStatus, MediaType } from '../constants/media';
|
||||
import Media from '../entity/Media';
|
||||
import { MediaStatus, MediaRequestStatus, MediaType } from '../constants/media';
|
||||
import { MediaRequest } from '../entity/MediaRequest';
|
||||
import SeasonRequest from '../entity/SeasonRequest';
|
||||
import logger from '../logger';
|
||||
import { RequestResultsResponse } from '../interfaces/api/requestInterfaces';
|
||||
import { User } from '../entity/User';
|
||||
import { RequestResultsResponse } from '../interfaces/api/requestInterfaces';
|
||||
import { Permission } from '../lib/permissions';
|
||||
import logger from '../logger';
|
||||
import { isAuthenticated } from '../middleware/auth';
|
||||
|
||||
const requestRoutes = Router();
|
||||
|
||||
@@ -17,6 +17,9 @@ requestRoutes.get('/', async (req, res, next) => {
|
||||
try {
|
||||
const pageSize = req.query.take ? Number(req.query.take) : 10;
|
||||
const skip = req.query.skip ? Number(req.query.skip) : 0;
|
||||
const requestedBy = req.query.requestedBy
|
||||
? Number(req.query.requestedBy)
|
||||
: null;
|
||||
|
||||
let statusFilter: MediaRequestStatus[];
|
||||
|
||||
@@ -100,9 +103,20 @@ requestRoutes.get('/', async (req, res, next) => {
|
||||
{ type: 'or' }
|
||||
)
|
||||
) {
|
||||
if (requestedBy && requestedBy !== req.user?.id) {
|
||||
return next({
|
||||
status: 403,
|
||||
message: "You do not have permission to view this user's requests.",
|
||||
});
|
||||
}
|
||||
|
||||
query = query.andWhere('requestedBy.id = :id', {
|
||||
id: req.user?.id,
|
||||
});
|
||||
} else if (requestedBy) {
|
||||
query = query.andWhere('requestedBy.id = :id', {
|
||||
id: requestedBy,
|
||||
});
|
||||
}
|
||||
|
||||
const [requests, requestCount] = await query
|
||||
@@ -125,229 +139,299 @@ 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,
|
||||
])
|
||||
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 (
|
||||
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;
|
||||
}
|
||||
|
||||
if (media.status4k === MediaStatus.UNKNOWN && req.body.is4k) {
|
||||
media.status4k = MediaStatus.PENDING;
|
||||
}
|
||||
}
|
||||
|
||||
if (req.body.mediaType === MediaType.MOVIE) {
|
||||
const existing = await requestRepository
|
||||
.createQueryBuilder('request')
|
||||
.leftJoin('request.media', 'media')
|
||||
.where('request.is4k = :is4k', { is4k: req.body.is4k })
|
||||
.andWhere('media.tmdbId = :tmdbId', { tmdbId: tmdbMedia.id })
|
||||
.andWhere('request.status != :requestStatus', {
|
||||
requestStatus: MediaRequestStatus.DECLINED,
|
||||
})
|
||||
.getOne();
|
||||
|
||||
if (existing) {
|
||||
logger.warn('Duplicate request for media blocked', {
|
||||
tmdbId: tmdbMedia.id,
|
||||
mediaType: req.body.mediaType,
|
||||
is4k: req.body.is4k,
|
||||
label: 'Media Request',
|
||||
});
|
||||
return next({
|
||||
status: 409,
|
||||
message: 'Request for this media already exists.',
|
||||
});
|
||||
}
|
||||
|
||||
await mediaRepository.save(media);
|
||||
|
||||
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 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',
|
||||
});
|
||||
} else if (
|
||||
quotas.tv.limit &&
|
||||
finalSeasons.length > (quotas.tv.remaining ?? 0)
|
||||
) {
|
||||
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 },
|
||||
message: 'Series Quota Exceeded',
|
||||
});
|
||||
}
|
||||
|
||||
const tmdbMedia =
|
||||
req.body.mediaType === 'movie'
|
||||
? await tmdb.getMovie({ movieId: req.body.mediaId })
|
||||
: await tmdb.getTvShow({ tvId: req.body.mediaId });
|
||||
await mediaRepository.save(media);
|
||||
|
||||
let media = await mediaRepository.findOne({
|
||||
where: { tmdbId: req.body.mediaId, mediaType: req.body.mediaType },
|
||||
relations: ['requests'],
|
||||
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,
|
||||
})
|
||||
),
|
||||
});
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
if (media.status4k === MediaStatus.UNKNOWN && req.body.is4k) {
|
||||
media.status4k = MediaStatus.PENDING;
|
||||
}
|
||||
}
|
||||
|
||||
if (req.body.mediaType === 'movie') {
|
||||
const existing = await requestRepository.findOne({
|
||||
where: {
|
||||
media: {
|
||||
tmdbId: tmdbMedia.id,
|
||||
},
|
||||
requestedBy: req.user,
|
||||
is4k: req.body.is4k,
|
||||
},
|
||||
});
|
||||
|
||||
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.',
|
||||
});
|
||||
}
|
||||
|
||||
await mediaRepository.save(media);
|
||||
|
||||
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.user?.hasPermission(
|
||||
req.body.is4k
|
||||
? Permission.AUTO_APPROVE_4K_MOVIE
|
||||
: Permission.AUTO_APPROVE_MOVIE
|
||||
)
|
||||
? MediaRequestStatus.APPROVED
|
||||
: MediaRequestStatus.PENDING,
|
||||
modifiedBy:
|
||||
req.user?.hasPermission(
|
||||
req.body.is4k
|
||||
? Permission.AUTO_APPROVE_4K
|
||||
: Permission.AUTO_APPROVE
|
||||
) ||
|
||||
req.user?.hasPermission(
|
||||
req.body.is4k
|
||||
? Permission.AUTO_APPROVE_4K_MOVIE
|
||||
: Permission.AUTO_APPROVE_MOVIE
|
||||
)
|
||||
? req.user
|
||||
: undefined,
|
||||
is4k: req.body.is4k,
|
||||
serverId: req.body.serverId,
|
||||
profileId: req.body.profileId,
|
||||
rootFolder: req.body.rootFolder,
|
||||
});
|
||||
|
||||
await requestRepository.save(request);
|
||||
return res.status(201).json(request);
|
||||
} else if (req.body.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.user?.hasPermission(
|
||||
req.body.is4k
|
||||
? Permission.AUTO_APPROVE_4K_TV
|
||||
: Permission.AUTO_APPROVE_TV
|
||||
)
|
||||
? MediaRequestStatus.APPROVED
|
||||
: MediaRequestStatus.PENDING,
|
||||
modifiedBy:
|
||||
req.user?.hasPermission(
|
||||
req.body.is4k
|
||||
? Permission.AUTO_APPROVE_4K
|
||||
: Permission.AUTO_APPROVE
|
||||
) ||
|
||||
req.user?.hasPermission(
|
||||
req.body.is4k
|
||||
? Permission.AUTO_APPROVE_4K_TV
|
||||
: Permission.AUTO_APPROVE_TV
|
||||
)
|
||||
? req.user
|
||||
: undefined,
|
||||
is4k: req.body.is4k,
|
||||
serverId: req.body.serverId,
|
||||
profileId: req.body.profileId,
|
||||
rootFolder: req.body.rootFolder,
|
||||
languageProfileId: req.body.languageProfileId,
|
||||
seasons: finalSeasons.map(
|
||||
(sn) =>
|
||||
new SeasonRequest({
|
||||
seasonNumber: sn,
|
||||
status:
|
||||
req.user?.hasPermission(
|
||||
req.body.is4k
|
||||
? Permission.AUTO_APPROVE_4K
|
||||
: Permission.AUTO_APPROVE
|
||||
) ||
|
||||
req.user?.hasPermission(
|
||||
req.body.is4k
|
||||
? Permission.AUTO_APPROVE_4K_TV
|
||||
: Permission.AUTO_APPROVE_TV
|
||||
)
|
||||
? 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 });
|
||||
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);
|
||||
@@ -421,7 +505,6 @@ requestRoutes.get('/:requestId', async (req, res, next) => {
|
||||
|
||||
requestRoutes.put<{ requestId: string }>(
|
||||
'/:requestId',
|
||||
isAuthenticated(Permission.MANAGE_REQUESTS),
|
||||
async (req, res, next) => {
|
||||
const requestRepository = getRepository(MediaRequest);
|
||||
const userRepository = getRepository(User);
|
||||
@@ -431,17 +514,30 @@ requestRoutes.put<{ requestId: string }>(
|
||||
);
|
||||
|
||||
if (!request) {
|
||||
return next({ status: 404, message: 'Request not found' });
|
||||
return next({ status: 404, message: 'Request not found.' });
|
||||
}
|
||||
|
||||
if (
|
||||
(request.requestedBy.id !== req.user?.id ||
|
||||
(req.body.mediaType !== 'tv' &&
|
||||
!req.user?.hasPermission(Permission.REQUEST_ADVANCED))) &&
|
||||
!req.user?.hasPermission(Permission.MANAGE_REQUESTS)
|
||||
) {
|
||||
return next({
|
||||
status: 403,
|
||||
message: 'You do not have permission to modify this request.',
|
||||
});
|
||||
}
|
||||
|
||||
let requestUser = req.user;
|
||||
|
||||
if (
|
||||
req.body.userId &&
|
||||
!(
|
||||
req.user?.hasPermission(Permission.MANAGE_USERS) &&
|
||||
req.user?.hasPermission(Permission.MANAGE_REQUESTS)
|
||||
)
|
||||
req.body.userId !== req.user?.id &&
|
||||
!req.user?.hasPermission([
|
||||
Permission.MANAGE_USERS,
|
||||
Permission.MANAGE_REQUESTS,
|
||||
])
|
||||
) {
|
||||
return next({
|
||||
status: 403,
|
||||
@@ -453,25 +549,28 @@ requestRoutes.put<{ requestId: string }>(
|
||||
});
|
||||
}
|
||||
|
||||
if (req.body.mediaType === 'movie') {
|
||||
if (req.body.mediaType === MediaType.MOVIE) {
|
||||
request.serverId = req.body.serverId;
|
||||
request.profileId = req.body.profileId;
|
||||
request.rootFolder = req.body.rootFolder;
|
||||
request.tags = req.body.tags;
|
||||
request.requestedBy = requestUser as User;
|
||||
|
||||
requestRepository.save(request);
|
||||
} else if (req.body.mediaType === 'tv') {
|
||||
} else if (req.body.mediaType === MediaType.TV) {
|
||||
const mediaRepository = getRepository(Media);
|
||||
request.serverId = req.body.serverId;
|
||||
request.profileId = req.body.profileId;
|
||||
request.rootFolder = req.body.rootFolder;
|
||||
request.languageProfileId = req.body.languageProfileId;
|
||||
request.tags = req.body.tags;
|
||||
request.requestedBy = requestUser as User;
|
||||
|
||||
const requestedSeasons = req.body.seasons as number[] | undefined;
|
||||
|
||||
if (!requestedSeasons || requestedSeasons.length === 0) {
|
||||
throw new Error(
|
||||
'Missing seasons. If you want to cancel a tv request, use the DELETE method.'
|
||||
'Missing seasons. If you want to cancel a series request, use the DELETE method.'
|
||||
);
|
||||
}
|
||||
|
||||
@@ -558,7 +657,7 @@ requestRoutes.delete('/:requestId', async (req, res, next) => {
|
||||
) {
|
||||
return next({
|
||||
status: 401,
|
||||
message: 'You do not have permission to remove this request',
|
||||
message: 'You do not have permission to delete this request.',
|
||||
});
|
||||
}
|
||||
|
||||
@@ -567,7 +666,7 @@ requestRoutes.delete('/:requestId', async (req, res, next) => {
|
||||
return res.status(204).send();
|
||||
} catch (e) {
|
||||
logger.error(e.message);
|
||||
next({ status: 404, message: 'Request not found' });
|
||||
next({ status: 404, message: 'Request not found.' });
|
||||
}
|
||||
});
|
||||
|
||||
@@ -593,7 +692,7 @@ requestRoutes.post<{
|
||||
label: 'Media Request',
|
||||
message: e.message,
|
||||
});
|
||||
next({ status: 404, message: 'Request not found' });
|
||||
next({ status: 404, message: 'Request not found.' });
|
||||
}
|
||||
}
|
||||
);
|
||||
@@ -637,7 +736,7 @@ requestRoutes.post<{
|
||||
label: 'Media Request',
|
||||
message: e.message,
|
||||
});
|
||||
next({ status: 404, message: 'Request not found' });
|
||||
next({ status: 404, message: 'Request not found.' });
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Router } from 'express';
|
||||
import TheMovieDb from '../api/themoviedb';
|
||||
import { mapSearchResults } from '../models/Search';
|
||||
import Media from '../entity/Media';
|
||||
import { mapSearchResults } from '../models/Search';
|
||||
|
||||
const searchRoutes = Router();
|
||||
|
||||
@@ -11,7 +11,7 @@ searchRoutes.get('/', async (req, res) => {
|
||||
const results = await tmdb.searchMulti({
|
||||
query: req.query.query as string,
|
||||
page: Number(req.query.page),
|
||||
language: req.query.language as string,
|
||||
language: req.locale ?? (req.query.language as string),
|
||||
});
|
||||
|
||||
const media = await Media.getRelatedMedia(
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import { Router } from 'express';
|
||||
import RadarrAPI from '../api/radarr';
|
||||
import SonarrAPI from '../api/sonarr';
|
||||
import RadarrAPI from '../api/servarr/radarr';
|
||||
import SonarrAPI from '../api/servarr/sonarr';
|
||||
import TheMovieDb from '../api/themoviedb';
|
||||
import {
|
||||
ServiceCommonServer,
|
||||
ServiceCommonServerWithDetails,
|
||||
} from '../interfaces/api/serviceInterfaces';
|
||||
import { getSettings } from '../lib/settings';
|
||||
import TheMovieDb from '../api/themoviedb';
|
||||
import logger from '../logger';
|
||||
|
||||
const serviceRoutes = Router();
|
||||
@@ -22,6 +22,7 @@ serviceRoutes.get('/radarr', async (req, res) => {
|
||||
isDefault: radarr.isDefault,
|
||||
activeDirectory: radarr.activeDirectory,
|
||||
activeProfileId: radarr.activeProfileId,
|
||||
activeTags: radarr.tags ?? [],
|
||||
})
|
||||
);
|
||||
|
||||
@@ -46,11 +47,12 @@ serviceRoutes.get<{ radarrId: string }>(
|
||||
|
||||
const radarr = new RadarrAPI({
|
||||
apiKey: radarrSettings.apiKey,
|
||||
url: RadarrAPI.buildRadarrUrl(radarrSettings, '/api/v3'),
|
||||
url: RadarrAPI.buildUrl(radarrSettings, '/api/v3'),
|
||||
});
|
||||
|
||||
const profiles = await radarr.getProfiles();
|
||||
const rootFolders = await radarr.getRootFolders();
|
||||
const tags = await radarr.getTags();
|
||||
|
||||
return res.status(200).json({
|
||||
server: {
|
||||
@@ -60,6 +62,7 @@ serviceRoutes.get<{ radarrId: string }>(
|
||||
isDefault: radarrSettings.isDefault,
|
||||
activeDirectory: radarrSettings.activeDirectory,
|
||||
activeProfileId: radarrSettings.activeProfileId,
|
||||
activeTags: radarrSettings.tags,
|
||||
},
|
||||
profiles: profiles.map((profile) => ({
|
||||
id: profile.id,
|
||||
@@ -71,6 +74,7 @@ serviceRoutes.get<{ radarrId: string }>(
|
||||
path: folder.path,
|
||||
totalSpace: folder.totalSpace,
|
||||
})),
|
||||
tags,
|
||||
} as ServiceCommonServerWithDetails);
|
||||
}
|
||||
);
|
||||
@@ -90,6 +94,7 @@ serviceRoutes.get('/sonarr', async (req, res) => {
|
||||
activeAnimeDirectory: sonarr.activeAnimeDirectory,
|
||||
activeLanguageProfileId: sonarr.activeLanguageProfileId,
|
||||
activeAnimeLanguageProfileId: sonarr.activeAnimeLanguageProfileId,
|
||||
activeTags: [],
|
||||
})
|
||||
);
|
||||
|
||||
@@ -114,13 +119,14 @@ serviceRoutes.get<{ sonarrId: string }>(
|
||||
|
||||
const sonarr = new SonarrAPI({
|
||||
apiKey: sonarrSettings.apiKey,
|
||||
url: SonarrAPI.buildSonarrUrl(sonarrSettings, '/api/v3'),
|
||||
url: SonarrAPI.buildUrl(sonarrSettings, '/api/v3'),
|
||||
});
|
||||
|
||||
try {
|
||||
const profiles = await sonarr.getProfiles();
|
||||
const rootFolders = await sonarr.getRootFolders();
|
||||
const languageProfiles = await sonarr.getLanguageProfiles();
|
||||
const tags = await sonarr.getTags();
|
||||
|
||||
return res.status(200).json({
|
||||
server: {
|
||||
@@ -135,6 +141,8 @@ serviceRoutes.get<{ sonarrId: string }>(
|
||||
activeLanguageProfileId: sonarrSettings.activeLanguageProfileId,
|
||||
activeAnimeLanguageProfileId:
|
||||
sonarrSettings.activeAnimeLanguageProfileId,
|
||||
activeTags: sonarrSettings.tags,
|
||||
activeAnimeTags: sonarrSettings.animeTags,
|
||||
},
|
||||
profiles: profiles.map((profile) => ({
|
||||
id: profile.id,
|
||||
@@ -147,6 +155,7 @@ serviceRoutes.get<{ sonarrId: string }>(
|
||||
totalSpace: folder.totalSpace,
|
||||
})),
|
||||
languageProfiles: languageProfiles,
|
||||
tags,
|
||||
} as ServiceCommonServerWithDetails);
|
||||
} catch (e) {
|
||||
next({ status: 500, message: e.message });
|
||||
@@ -182,7 +191,7 @@ serviceRoutes.get<{ tmdbId: string }>(
|
||||
try {
|
||||
const tv = await tmdb.getTvShow({
|
||||
tvId: Number(req.params.tmdbId),
|
||||
language: req.query.language as string,
|
||||
language: req.locale ?? (req.query.language as string),
|
||||
});
|
||||
|
||||
const response = await sonarr.getSeriesByTitle(tv.name);
|
||||
|
||||
@@ -1,24 +1,34 @@
|
||||
import { Router } from 'express';
|
||||
import { getSettings, Library, MainSettings } from '../../lib/settings';
|
||||
import rateLimit from 'express-rate-limit';
|
||||
import fs from 'fs';
|
||||
import { merge, omit } from 'lodash';
|
||||
import path from 'path';
|
||||
import { getRepository } from 'typeorm';
|
||||
import { User } from '../../entity/User';
|
||||
import { URL } from 'url';
|
||||
import JellyfinAPI from '../../api/jellyfin';
|
||||
import PlexAPI from '../../api/plexapi';
|
||||
import PlexTvAPI from '../../api/plextv';
|
||||
import JellyfinAPI from '../../api/jellyfin';
|
||||
import { jobPlexFullSync } from '../../job/plexsync';
|
||||
import { jobJellyfinFullSync } from '../../job/jellyfinsync';
|
||||
import { scheduledJobs } from '../../job/schedule';
|
||||
import { Permission } from '../../lib/permissions';
|
||||
import { isAuthenticated } from '../../middleware/auth';
|
||||
import { merge, omit } from 'lodash';
|
||||
import Media from '../../entity/Media';
|
||||
import { MediaRequest } from '../../entity/MediaRequest';
|
||||
import { getAppVersion } from '../../utils/appVersion';
|
||||
import { SettingsAboutResponse } from '../../interfaces/api/settingsInterfaces';
|
||||
import notificationRoutes from './notifications';
|
||||
import sonarrRoutes from './sonarr';
|
||||
import radarrRoutes from './radarr';
|
||||
import { User } from '../../entity/User';
|
||||
import { PlexConnection } from '../../interfaces/api/plexInterfaces';
|
||||
import {
|
||||
LogMessage,
|
||||
LogsResultsResponse,
|
||||
SettingsAboutResponse,
|
||||
} from '../../interfaces/api/settingsInterfaces';
|
||||
import { jobJellyfinFullSync } from '../../job/jellyfinsync';
|
||||
import { scheduledJobs } from '../../job/schedule';
|
||||
import cacheManager, { AvailableCacheIds } from '../../lib/cache';
|
||||
import { Permission } from '../../lib/permissions';
|
||||
import { plexFullScanner } from '../../lib/scanners/plex';
|
||||
import { getSettings, MainSettings } from '../../lib/settings';
|
||||
import logger from '../../logger';
|
||||
import { isAuthenticated } from '../../middleware/auth';
|
||||
import { getAppVersion } from '../../utils/appVersion';
|
||||
import notificationRoutes from './notifications';
|
||||
import radarrRoutes from './radarr';
|
||||
import sonarrRoutes from './sonarr';
|
||||
|
||||
const settingsRoutes = Router();
|
||||
|
||||
@@ -107,7 +117,6 @@ settingsRoutes.post('/plex', async (req, res, next) => {
|
||||
|
||||
settingsRoutes.get('/plex/devices/servers', async (req, res, next) => {
|
||||
const userRepository = getRepository(User);
|
||||
const regexp = /(http(s?):\/\/)(.*)(:[0-9]*)/;
|
||||
try {
|
||||
const admin = await userRepository.findOneOrFail({
|
||||
select: ['id', 'plexToken'],
|
||||
@@ -120,40 +129,51 @@ settingsRoutes.get('/plex/devices/servers', async (req, res, next) => {
|
||||
return device.provides.includes('server') && device.owned;
|
||||
});
|
||||
const settings = getSettings();
|
||||
|
||||
if (devices) {
|
||||
await Promise.all(
|
||||
devices.map(async (device) => {
|
||||
const plexDirectConnections: PlexConnection[] = [];
|
||||
|
||||
device.connection.forEach((connection) => {
|
||||
const url = new URL(connection.uri);
|
||||
|
||||
if (url.hostname !== connection.address) {
|
||||
const plexDirectConnection = { ...connection };
|
||||
plexDirectConnection.address = url.hostname;
|
||||
plexDirectConnections.push(plexDirectConnection);
|
||||
|
||||
// Connect to IP addresses over HTTP
|
||||
connection.protocol = 'http';
|
||||
}
|
||||
});
|
||||
|
||||
plexDirectConnections.forEach((plexDirectConnection) => {
|
||||
device.connection.push(plexDirectConnection);
|
||||
});
|
||||
|
||||
await Promise.all(
|
||||
device.connection.map(async (connection) => {
|
||||
connection.host = connection.uri.replace(regexp, '$3');
|
||||
let msg:
|
||||
| { status: number; message: string }
|
||||
| undefined = undefined;
|
||||
const plexDeviceSettings = {
|
||||
...settings.plex,
|
||||
ip: connection.host,
|
||||
ip: connection.address,
|
||||
port: connection.port,
|
||||
useSsl: connection.protocol === 'https' ? true : false,
|
||||
useSsl: connection.protocol === 'https',
|
||||
};
|
||||
const plexClient = new PlexAPI({
|
||||
plexToken: admin.plexToken,
|
||||
plexSettings: plexDeviceSettings,
|
||||
timeout: 5000,
|
||||
});
|
||||
|
||||
try {
|
||||
await plexClient.getStatus();
|
||||
msg = {
|
||||
status: 200,
|
||||
message: 'OK',
|
||||
};
|
||||
connection.status = 200;
|
||||
connection.message = 'OK';
|
||||
} catch (e) {
|
||||
msg = {
|
||||
status: 500,
|
||||
message: e.message,
|
||||
};
|
||||
connection.status = 500;
|
||||
connection.message = e.message.split(':')[0];
|
||||
}
|
||||
connection.status = msg?.status;
|
||||
connection.message = msg?.message;
|
||||
})
|
||||
);
|
||||
})
|
||||
@@ -179,26 +199,7 @@ settingsRoutes.get('/plex/library', async (req, res) => {
|
||||
});
|
||||
const plexapi = new PlexAPI({ plexToken: admin.plexToken });
|
||||
|
||||
const libraries = await plexapi.getLibraries();
|
||||
|
||||
const newLibraries: Library[] = libraries
|
||||
// Remove libraries that are not movie or show
|
||||
.filter((library) => library.type === 'movie' || library.type === 'show')
|
||||
// Remove libraries that do not have a metadata agent set (usually personal video libraries)
|
||||
.filter((library) => library.agent !== 'com.plexapp.agents.none')
|
||||
.map((library) => {
|
||||
const existing = settings.plex.libraries.find(
|
||||
(l) => l.id === library.key && l.name === library.title
|
||||
);
|
||||
|
||||
return {
|
||||
id: library.key,
|
||||
name: library.title,
|
||||
enabled: existing?.enabled ?? false,
|
||||
};
|
||||
});
|
||||
|
||||
settings.plex.libraries = newLibraries;
|
||||
await plexapi.syncLibraries();
|
||||
}
|
||||
|
||||
const enabledLibraries = req.query.enable
|
||||
@@ -213,16 +214,16 @@ settingsRoutes.get('/plex/library', async (req, res) => {
|
||||
});
|
||||
|
||||
settingsRoutes.get('/plex/sync', (_req, res) => {
|
||||
return res.status(200).json(jobPlexFullSync.status());
|
||||
return res.status(200).json(plexFullScanner.status());
|
||||
});
|
||||
|
||||
settingsRoutes.post('/plex/sync', (req, res) => {
|
||||
if (req.body.cancel) {
|
||||
jobPlexFullSync.cancel();
|
||||
plexFullScanner.cancel();
|
||||
} else if (req.body.start) {
|
||||
jobPlexFullSync.run();
|
||||
plexFullScanner.run();
|
||||
}
|
||||
return res.status(200).json(jobPlexFullSync.status());
|
||||
return res.status(200).json(plexFullScanner.status());
|
||||
});
|
||||
|
||||
settingsRoutes.get('/jellyfin', (_req, res) => {
|
||||
@@ -297,6 +298,85 @@ settingsRoutes.post('/jellyfin/sync', (req, res) => {
|
||||
}
|
||||
return res.status(200).json(jobJellyfinFullSync.status());
|
||||
});
|
||||
settingsRoutes.get(
|
||||
'/logs',
|
||||
rateLimit({ windowMs: 60 * 1000, max: 50 }),
|
||||
(req, res, next) => {
|
||||
const pageSize = req.query.take ? Number(req.query.take) : 25;
|
||||
const skip = req.query.skip ? Number(req.query.skip) : 0;
|
||||
|
||||
let filter: string[] = [];
|
||||
switch (req.query.filter) {
|
||||
case 'debug':
|
||||
filter.push('debug');
|
||||
// falls through
|
||||
case 'info':
|
||||
filter.push('info');
|
||||
// falls through
|
||||
case 'warn':
|
||||
filter.push('warn');
|
||||
// falls through
|
||||
case 'error':
|
||||
filter.push('error');
|
||||
break;
|
||||
default:
|
||||
filter = ['debug', 'info', 'warn', 'error'];
|
||||
}
|
||||
|
||||
const logFile = process.env.CONFIG_DIRECTORY
|
||||
? `${process.env.CONFIG_DIRECTORY}/logs/overseerr.log`
|
||||
: path.join(__dirname, '../../../config/logs/overseerr.log');
|
||||
const logs: LogMessage[] = [];
|
||||
|
||||
try {
|
||||
fs.readFileSync(logFile)
|
||||
.toString()
|
||||
.split('\n')
|
||||
.forEach((line) => {
|
||||
if (!line.length) return;
|
||||
|
||||
const timestamp = line.match(new RegExp(/^.{24}/)) || [];
|
||||
const level = line.match(new RegExp(/\s\[\w+\]/)) || [];
|
||||
const label = line.match(new RegExp(/\]\[.+?\]/)) || [];
|
||||
const message = line.match(new RegExp(/:\s([^{}]+)({.*})?/)) || [];
|
||||
|
||||
if (level.length && filter.includes(level[0].slice(2, -1))) {
|
||||
logs.push({
|
||||
timestamp: timestamp[0],
|
||||
level: level.length ? level[0].slice(2, -1) : '',
|
||||
label: label.length ? label[0].slice(2, -1) : '',
|
||||
message: message.length && message[1] ? message[1] : '',
|
||||
data:
|
||||
message.length && message[2]
|
||||
? JSON.parse(message[2])
|
||||
: undefined,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
const displayedLogs = logs.reverse().slice(skip, skip + pageSize);
|
||||
|
||||
return res.status(200).json({
|
||||
pageInfo: {
|
||||
pages: Math.ceil(logs.length / pageSize),
|
||||
pageSize,
|
||||
results: logs.length,
|
||||
page: Math.ceil(skip / pageSize) + 1,
|
||||
},
|
||||
results: displayedLogs,
|
||||
} as LogsResultsResponse);
|
||||
} catch (error) {
|
||||
logger.error('Something went wrong while fetching the logs', {
|
||||
label: 'Logs',
|
||||
errorMessage: error.message,
|
||||
});
|
||||
return next({
|
||||
status: 500,
|
||||
message: 'Something went wrong while fetching the logs',
|
||||
});
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
settingsRoutes.get('/jobs', (_req, res) => {
|
||||
return res.status(200).json(
|
||||
|
||||
@@ -1,39 +1,18 @@
|
||||
import { Router } from 'express';
|
||||
import { getSettings } from '../../lib/settings';
|
||||
import { Notification } from '../../lib/notifications';
|
||||
import DiscordAgent from '../../lib/notifications/agents/discord';
|
||||
import EmailAgent from '../../lib/notifications/agents/email';
|
||||
import LunaSeaAgent from '../../lib/notifications/agents/lunasea';
|
||||
import PushbulletAgent from '../../lib/notifications/agents/pushbullet';
|
||||
import PushoverAgent from '../../lib/notifications/agents/pushover';
|
||||
import SlackAgent from '../../lib/notifications/agents/slack';
|
||||
import TelegramAgent from '../../lib/notifications/agents/telegram';
|
||||
import PushoverAgent from '../../lib/notifications/agents/pushover';
|
||||
import WebhookAgent from '../../lib/notifications/agents/webhook';
|
||||
import PushbulletAgent from '../../lib/notifications/agents/pushbullet';
|
||||
import WebPushAgent from '../../lib/notifications/agents/webpush';
|
||||
import { getSettings } from '../../lib/settings';
|
||||
|
||||
const notificationRoutes = Router();
|
||||
|
||||
notificationRoutes.get('/', (_req, res) => {
|
||||
const settings = getSettings().notifications;
|
||||
return res.status(200).json({
|
||||
enabled: settings.enabled,
|
||||
autoapprovalEnabled: settings.autoapprovalEnabled,
|
||||
});
|
||||
});
|
||||
|
||||
notificationRoutes.post('/', (req, res) => {
|
||||
const settings = getSettings();
|
||||
|
||||
Object.assign(settings.notifications, {
|
||||
enabled: req.body.enabled,
|
||||
autoapprovalEnabled: req.body.autoapprovalEnabled,
|
||||
});
|
||||
settings.save();
|
||||
|
||||
return res.status(200).json({
|
||||
enabled: settings.notifications.enabled,
|
||||
autoapprovalEnabled: settings.notifications.autoapprovalEnabled,
|
||||
});
|
||||
});
|
||||
|
||||
notificationRoutes.get('/discord', (_req, res) => {
|
||||
const settings = getSettings();
|
||||
|
||||
@@ -49,23 +28,30 @@ notificationRoutes.post('/discord', (req, res) => {
|
||||
res.status(200).json(settings.notifications.agents.discord);
|
||||
});
|
||||
|
||||
notificationRoutes.post('/discord/test', (req, res, next) => {
|
||||
notificationRoutes.post('/discord/test', async (req, res, next) => {
|
||||
if (!req.user) {
|
||||
return next({
|
||||
status: 500,
|
||||
message: 'User information missing from request',
|
||||
message: 'User information is missing from the request.',
|
||||
});
|
||||
}
|
||||
|
||||
const discordAgent = new DiscordAgent(req.body);
|
||||
discordAgent.send(Notification.TEST_NOTIFICATION, {
|
||||
notifyUser: req.user,
|
||||
subject: 'Test Notification',
|
||||
message:
|
||||
'This is a test notification! Check check, 1, 2, 3. Are we coming in clear?',
|
||||
});
|
||||
|
||||
return res.status(204).send();
|
||||
if (
|
||||
await discordAgent.send(Notification.TEST_NOTIFICATION, {
|
||||
notifyUser: req.user,
|
||||
subject: 'Test Notification',
|
||||
message:
|
||||
'This is a test notification! Check check, 1, 2, 3. Are we coming in clear?',
|
||||
})
|
||||
) {
|
||||
return res.status(204).send();
|
||||
} else {
|
||||
return next({
|
||||
status: 500,
|
||||
message: 'Failed to send Discord notification.',
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
notificationRoutes.get('/slack', (_req, res) => {
|
||||
@@ -83,23 +69,30 @@ notificationRoutes.post('/slack', (req, res) => {
|
||||
res.status(200).json(settings.notifications.agents.slack);
|
||||
});
|
||||
|
||||
notificationRoutes.post('/slack/test', (req, res, next) => {
|
||||
notificationRoutes.post('/slack/test', async (req, res, next) => {
|
||||
if (!req.user) {
|
||||
return next({
|
||||
status: 500,
|
||||
message: 'User information missing from request',
|
||||
message: 'User information is missing from the request.',
|
||||
});
|
||||
}
|
||||
|
||||
const slackAgent = new SlackAgent(req.body);
|
||||
slackAgent.send(Notification.TEST_NOTIFICATION, {
|
||||
notifyUser: req.user,
|
||||
subject: 'Test Notification',
|
||||
message:
|
||||
'This is a test notification! Check check, 1, 2, 3. Are we coming in clear?',
|
||||
});
|
||||
|
||||
return res.status(204).send();
|
||||
if (
|
||||
await slackAgent.send(Notification.TEST_NOTIFICATION, {
|
||||
notifyUser: req.user,
|
||||
subject: 'Test Notification',
|
||||
message:
|
||||
'This is a test notification! Check check, 1, 2, 3. Are we coming in clear?',
|
||||
})
|
||||
) {
|
||||
return res.status(204).send();
|
||||
} else {
|
||||
return next({
|
||||
status: 500,
|
||||
message: 'Failed to send Slack notification.',
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
notificationRoutes.get('/telegram', (_req, res) => {
|
||||
@@ -117,23 +110,30 @@ notificationRoutes.post('/telegram', (req, res) => {
|
||||
res.status(200).json(settings.notifications.agents.telegram);
|
||||
});
|
||||
|
||||
notificationRoutes.post('/telegram/test', (req, res, next) => {
|
||||
notificationRoutes.post('/telegram/test', async (req, res, next) => {
|
||||
if (!req.user) {
|
||||
return next({
|
||||
status: 500,
|
||||
message: 'User information missing from request',
|
||||
message: 'User information is missing from the request.',
|
||||
});
|
||||
}
|
||||
|
||||
const telegramAgent = new TelegramAgent(req.body);
|
||||
telegramAgent.send(Notification.TEST_NOTIFICATION, {
|
||||
notifyUser: req.user,
|
||||
subject: 'Test Notification',
|
||||
message:
|
||||
'This is a test notification! Check check, 1, 2, 3. Are we coming in clear?',
|
||||
});
|
||||
|
||||
return res.status(204).send();
|
||||
if (
|
||||
await telegramAgent.send(Notification.TEST_NOTIFICATION, {
|
||||
notifyUser: req.user,
|
||||
subject: 'Test Notification',
|
||||
message:
|
||||
'This is a test notification! Check check, 1, 2, 3. Are we coming in clear?',
|
||||
})
|
||||
) {
|
||||
return res.status(204).send();
|
||||
} else {
|
||||
return next({
|
||||
status: 500,
|
||||
message: 'Failed to send Telegram notification.',
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
notificationRoutes.get('/pushbullet', (_req, res) => {
|
||||
@@ -151,23 +151,30 @@ notificationRoutes.post('/pushbullet', (req, res) => {
|
||||
res.status(200).json(settings.notifications.agents.pushbullet);
|
||||
});
|
||||
|
||||
notificationRoutes.post('/pushbullet/test', (req, res, next) => {
|
||||
notificationRoutes.post('/pushbullet/test', async (req, res, next) => {
|
||||
if (!req.user) {
|
||||
return next({
|
||||
status: 500,
|
||||
message: 'User information missing from request',
|
||||
message: 'User information is missing from the request.',
|
||||
});
|
||||
}
|
||||
|
||||
const pushbulletAgent = new PushbulletAgent(req.body);
|
||||
pushbulletAgent.send(Notification.TEST_NOTIFICATION, {
|
||||
notifyUser: req.user,
|
||||
subject: 'Test Notification',
|
||||
message:
|
||||
'This is a test notification! Check check, 1, 2, 3. Are we coming in clear?',
|
||||
});
|
||||
|
||||
return res.status(204).send();
|
||||
if (
|
||||
await pushbulletAgent.send(Notification.TEST_NOTIFICATION, {
|
||||
notifyUser: req.user,
|
||||
subject: 'Test Notification',
|
||||
message:
|
||||
'This is a test notification! Check check, 1, 2, 3. Are we coming in clear?',
|
||||
})
|
||||
) {
|
||||
return res.status(204).send();
|
||||
} else {
|
||||
return next({
|
||||
status: 500,
|
||||
message: 'Failed to send Pushbullet notification.',
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
notificationRoutes.get('/pushover', (_req, res) => {
|
||||
@@ -185,23 +192,30 @@ notificationRoutes.post('/pushover', (req, res) => {
|
||||
res.status(200).json(settings.notifications.agents.pushover);
|
||||
});
|
||||
|
||||
notificationRoutes.post('/pushover/test', (req, res, next) => {
|
||||
notificationRoutes.post('/pushover/test', async (req, res, next) => {
|
||||
if (!req.user) {
|
||||
return next({
|
||||
status: 500,
|
||||
message: 'User information missing from request',
|
||||
message: 'User information is missing from the request.',
|
||||
});
|
||||
}
|
||||
|
||||
const pushoverAgent = new PushoverAgent(req.body);
|
||||
pushoverAgent.send(Notification.TEST_NOTIFICATION, {
|
||||
notifyUser: req.user,
|
||||
subject: 'Test Notification',
|
||||
message:
|
||||
'This is a test notification! Check check, 1, 2, 3. Are we coming in clear?',
|
||||
});
|
||||
|
||||
return res.status(204).send();
|
||||
if (
|
||||
await pushoverAgent.send(Notification.TEST_NOTIFICATION, {
|
||||
notifyUser: req.user,
|
||||
subject: 'Test Notification',
|
||||
message:
|
||||
'This is a test notification! Check check, 1, 2, 3. Are we coming in clear?',
|
||||
})
|
||||
) {
|
||||
return res.status(204).send();
|
||||
} else {
|
||||
return next({
|
||||
status: 500,
|
||||
message: 'Failed to send Pushover notification.',
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
notificationRoutes.get('/email', (_req, res) => {
|
||||
@@ -219,7 +233,48 @@ notificationRoutes.post('/email', (req, res) => {
|
||||
res.status(200).json(settings.notifications.agents.email);
|
||||
});
|
||||
|
||||
notificationRoutes.post('/email/test', (req, res, next) => {
|
||||
notificationRoutes.post('/email/test', async (req, res, next) => {
|
||||
if (!req.user) {
|
||||
return next({
|
||||
status: 500,
|
||||
message: 'User information is missing from the request.',
|
||||
});
|
||||
}
|
||||
|
||||
const emailAgent = new EmailAgent(req.body);
|
||||
if (
|
||||
await emailAgent.send(Notification.TEST_NOTIFICATION, {
|
||||
notifyUser: req.user,
|
||||
subject: 'Test Notification',
|
||||
message:
|
||||
'This is a test notification! Check check, 1, 2, 3. Are we coming in clear?',
|
||||
})
|
||||
) {
|
||||
return res.status(204).send();
|
||||
} else {
|
||||
return next({
|
||||
status: 500,
|
||||
message: 'Failed to send email notification.',
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
notificationRoutes.get('/webpush', (_req, res) => {
|
||||
const settings = getSettings();
|
||||
|
||||
res.status(200).json(settings.notifications.agents.webpush);
|
||||
});
|
||||
|
||||
notificationRoutes.post('/webpush', (req, res) => {
|
||||
const settings = getSettings();
|
||||
|
||||
settings.notifications.agents.webpush = req.body;
|
||||
settings.save();
|
||||
|
||||
res.status(200).json(settings.notifications.agents.webpush);
|
||||
});
|
||||
|
||||
notificationRoutes.post('/webpush/test', async (req, res, next) => {
|
||||
if (!req.user) {
|
||||
return next({
|
||||
status: 500,
|
||||
@@ -227,15 +282,22 @@ notificationRoutes.post('/email/test', (req, res, next) => {
|
||||
});
|
||||
}
|
||||
|
||||
const emailAgent = new EmailAgent(req.body);
|
||||
emailAgent.send(Notification.TEST_NOTIFICATION, {
|
||||
notifyUser: req.user,
|
||||
subject: 'Test Notification',
|
||||
message:
|
||||
'This is a test notification! Check check, 1, 2, 3. Are we coming in clear?',
|
||||
});
|
||||
|
||||
return res.status(204).send();
|
||||
const webpushAgent = new WebPushAgent(req.body);
|
||||
if (
|
||||
await webpushAgent.send(Notification.TEST_NOTIFICATION, {
|
||||
notifyUser: req.user,
|
||||
subject: 'Test Notification',
|
||||
message:
|
||||
'This is a test notification! Check check, 1, 2, 3. Are we coming in clear?',
|
||||
})
|
||||
) {
|
||||
return res.status(204).send();
|
||||
} else {
|
||||
return next({
|
||||
status: 500,
|
||||
message: 'Failed to send web push notification.',
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
notificationRoutes.get('/webhook', (_req, res) => {
|
||||
@@ -283,11 +345,11 @@ notificationRoutes.post('/webhook', (req, res, next) => {
|
||||
}
|
||||
});
|
||||
|
||||
notificationRoutes.post('/webhook/test', (req, res, next) => {
|
||||
notificationRoutes.post('/webhook/test', async (req, res, next) => {
|
||||
if (!req.user) {
|
||||
return next({
|
||||
status: 500,
|
||||
message: 'User information missing from request',
|
||||
message: 'User information is missing from the request.',
|
||||
});
|
||||
}
|
||||
|
||||
@@ -307,17 +369,65 @@ notificationRoutes.post('/webhook/test', (req, res, next) => {
|
||||
};
|
||||
|
||||
const webhookAgent = new WebhookAgent(testBody);
|
||||
webhookAgent.send(Notification.TEST_NOTIFICATION, {
|
||||
notifyUser: req.user,
|
||||
subject: 'Test Notification',
|
||||
message:
|
||||
'This is a test notification! Check check, 1, 2, 3. Are we coming in clear?',
|
||||
});
|
||||
|
||||
return res.status(204).send();
|
||||
if (
|
||||
await webhookAgent.send(Notification.TEST_NOTIFICATION, {
|
||||
notifyUser: req.user,
|
||||
subject: 'Test Notification',
|
||||
message:
|
||||
'This is a test notification! Check check, 1, 2, 3. Are we coming in clear?',
|
||||
})
|
||||
) {
|
||||
return res.status(204).send();
|
||||
} else {
|
||||
return next({
|
||||
status: 500,
|
||||
message: 'Failed to send webhook notification.',
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
next({ status: 500, message: e.message });
|
||||
}
|
||||
});
|
||||
|
||||
notificationRoutes.get('/lunasea', (_req, res) => {
|
||||
const settings = getSettings();
|
||||
|
||||
res.status(200).json(settings.notifications.agents.lunasea);
|
||||
});
|
||||
|
||||
notificationRoutes.post('/lunasea', (req, res) => {
|
||||
const settings = getSettings();
|
||||
|
||||
settings.notifications.agents.lunasea = req.body;
|
||||
settings.save();
|
||||
|
||||
res.status(200).json(settings.notifications.agents.lunasea);
|
||||
});
|
||||
|
||||
notificationRoutes.post('/lunasea/test', async (req, res, next) => {
|
||||
if (!req.user) {
|
||||
return next({
|
||||
status: 500,
|
||||
message: 'User information missing from request',
|
||||
});
|
||||
}
|
||||
|
||||
const lunaseaAgent = new LunaSeaAgent(req.body);
|
||||
if (
|
||||
await lunaseaAgent.send(Notification.TEST_NOTIFICATION, {
|
||||
notifyUser: req.user,
|
||||
subject: 'Test Notification',
|
||||
message:
|
||||
'This is a test notification! Check check, 1, 2, 3. Are we coming in clear?',
|
||||
})
|
||||
) {
|
||||
return res.status(204).send();
|
||||
} else {
|
||||
return next({
|
||||
status: 500,
|
||||
message: 'Failed to send web push notification.',
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
export default notificationRoutes;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Router } from 'express';
|
||||
import RadarrAPI from '../../api/radarr';
|
||||
import RadarrAPI from '../../api/servarr/radarr';
|
||||
import { getSettings, RadarrSettings } from '../../lib/settings';
|
||||
import logger from '../../logger';
|
||||
|
||||
@@ -35,15 +35,20 @@ radarrRoutes.post('/', (req, res) => {
|
||||
return res.status(201).json(newRadarr);
|
||||
});
|
||||
|
||||
radarrRoutes.post('/test', async (req, res, next) => {
|
||||
radarrRoutes.post<
|
||||
undefined,
|
||||
Record<string, unknown>,
|
||||
RadarrSettings & { tagLabel?: string }
|
||||
>('/test', async (req, res, next) => {
|
||||
try {
|
||||
const radarr = new RadarrAPI({
|
||||
apiKey: req.body.apiKey,
|
||||
url: RadarrAPI.buildRadarrUrl(req.body, '/api/v3'),
|
||||
url: RadarrAPI.buildUrl(req.body, '/api/v3'),
|
||||
});
|
||||
|
||||
const profiles = await radarr.getProfiles();
|
||||
const folders = await radarr.getRootFolders();
|
||||
const tags = await radarr.getTags();
|
||||
|
||||
return res.status(200).json({
|
||||
profiles,
|
||||
@@ -51,6 +56,7 @@ radarrRoutes.post('/test', async (req, res, next) => {
|
||||
id: folder.id,
|
||||
path: folder.path,
|
||||
})),
|
||||
tags,
|
||||
});
|
||||
} catch (e) {
|
||||
logger.error('Failed to test Radarr', {
|
||||
@@ -62,40 +68,41 @@ radarrRoutes.post('/test', async (req, res, next) => {
|
||||
}
|
||||
});
|
||||
|
||||
radarrRoutes.put<{ id: string }>('/:id', (req, res) => {
|
||||
const settings = getSettings();
|
||||
radarrRoutes.put<{ id: string }, RadarrSettings, RadarrSettings>(
|
||||
'/:id',
|
||||
(req, res, next) => {
|
||||
const settings = getSettings();
|
||||
|
||||
const radarrIndex = settings.radarr.findIndex(
|
||||
(r) => r.id === Number(req.params.id)
|
||||
);
|
||||
const radarrIndex = settings.radarr.findIndex(
|
||||
(r) => r.id === Number(req.params.id)
|
||||
);
|
||||
|
||||
if (radarrIndex === -1) {
|
||||
return res
|
||||
.status(404)
|
||||
.json({ status: '404', message: 'Settings instance not found' });
|
||||
if (radarrIndex === -1) {
|
||||
return next({ status: '404', message: 'Settings instance not found' });
|
||||
}
|
||||
|
||||
// If we are setting this as the default, clear any previous defaults for the same type first
|
||||
// ex: if is4k is true, it will only remove defaults for other servers that have is4k set to true
|
||||
// and are the default
|
||||
if (req.body.isDefault) {
|
||||
settings.radarr
|
||||
.filter((radarrInstance) => radarrInstance.is4k === req.body.is4k)
|
||||
.forEach((radarrInstance) => {
|
||||
radarrInstance.isDefault = false;
|
||||
});
|
||||
}
|
||||
|
||||
settings.radarr[radarrIndex] = {
|
||||
...req.body,
|
||||
id: Number(req.params.id),
|
||||
} as RadarrSettings;
|
||||
settings.save();
|
||||
|
||||
return res.status(200).json(settings.radarr[radarrIndex]);
|
||||
}
|
||||
);
|
||||
|
||||
// If we are setting this as the default, clear any previous defaults for the same type first
|
||||
// ex: if is4k is true, it will only remove defaults for other servers that have is4k set to true
|
||||
// and are the default
|
||||
if (req.body.isDefault) {
|
||||
settings.radarr
|
||||
.filter((radarrInstance) => radarrInstance.is4k === req.body.is4k)
|
||||
.forEach((radarrInstance) => {
|
||||
radarrInstance.isDefault = false;
|
||||
});
|
||||
}
|
||||
|
||||
settings.radarr[radarrIndex] = {
|
||||
...req.body,
|
||||
id: Number(req.params.id),
|
||||
} as RadarrSettings;
|
||||
settings.save();
|
||||
|
||||
return res.status(200).json(settings.radarr[radarrIndex]);
|
||||
});
|
||||
|
||||
radarrRoutes.get<{ id: string }>('/:id/profiles', async (req, res) => {
|
||||
radarrRoutes.get<{ id: string }>('/:id/profiles', async (req, res, next) => {
|
||||
const settings = getSettings();
|
||||
|
||||
const radarrSettings = settings.radarr.find(
|
||||
@@ -103,14 +110,12 @@ radarrRoutes.get<{ id: string }>('/:id/profiles', async (req, res) => {
|
||||
);
|
||||
|
||||
if (!radarrSettings) {
|
||||
return res
|
||||
.status(404)
|
||||
.json({ status: '404', message: 'Settings instance not found' });
|
||||
return next({ status: '404', message: 'Settings instance not found' });
|
||||
}
|
||||
|
||||
const radarr = new RadarrAPI({
|
||||
apiKey: radarrSettings.apiKey,
|
||||
url: RadarrAPI.buildRadarrUrl(radarrSettings, '/api/v3'),
|
||||
url: RadarrAPI.buildUrl(radarrSettings, '/api/v3'),
|
||||
});
|
||||
|
||||
const profiles = await radarr.getProfiles();
|
||||
@@ -123,7 +128,7 @@ radarrRoutes.get<{ id: string }>('/:id/profiles', async (req, res) => {
|
||||
);
|
||||
});
|
||||
|
||||
radarrRoutes.delete<{ id: string }>('/:id', (req, res) => {
|
||||
radarrRoutes.delete<{ id: string }>('/:id', (req, res, next) => {
|
||||
const settings = getSettings();
|
||||
|
||||
const radarrIndex = settings.radarr.findIndex(
|
||||
@@ -131,9 +136,7 @@ radarrRoutes.delete<{ id: string }>('/:id', (req, res) => {
|
||||
);
|
||||
|
||||
if (radarrIndex === -1) {
|
||||
return res
|
||||
.status(404)
|
||||
.json({ status: '404', message: 'Settings instance not found' });
|
||||
return next({ status: '404', message: 'Settings instance not found' });
|
||||
}
|
||||
|
||||
const removed = settings.radarr.splice(radarrIndex, 1);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Router } from 'express';
|
||||
import SonarrAPI from '../../api/sonarr';
|
||||
import SonarrAPI from '../../api/servarr/sonarr';
|
||||
import { getSettings, SonarrSettings } from '../../lib/settings';
|
||||
import logger from '../../logger';
|
||||
|
||||
@@ -39,12 +39,13 @@ sonarrRoutes.post('/test', async (req, res, next) => {
|
||||
try {
|
||||
const sonarr = new SonarrAPI({
|
||||
apiKey: req.body.apiKey,
|
||||
url: SonarrAPI.buildSonarrUrl(req.body, '/api/v3'),
|
||||
url: SonarrAPI.buildUrl(req.body, '/api/v3'),
|
||||
});
|
||||
|
||||
const profiles = await sonarr.getProfiles();
|
||||
const folders = await sonarr.getRootFolders();
|
||||
const languageProfiles = await sonarr.getLanguageProfiles();
|
||||
const tags = await sonarr.getTags();
|
||||
|
||||
return res.status(200).json({
|
||||
profiles,
|
||||
@@ -53,6 +54,7 @@ sonarrRoutes.post('/test', async (req, res, next) => {
|
||||
path: folder.path,
|
||||
})),
|
||||
languageProfiles,
|
||||
tags,
|
||||
});
|
||||
} catch (e) {
|
||||
logger.error('Failed to test Sonarr', {
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { Router } from 'express';
|
||||
import TheMovieDb from '../api/themoviedb';
|
||||
import { mapTvDetails, mapSeasonWithEpisodes } from '../models/Tv';
|
||||
import { mapTvResult } from '../models/Search';
|
||||
import Media from '../entity/Media';
|
||||
import RottenTomatoes from '../api/rottentomatoes';
|
||||
import logger from '../logger';
|
||||
import TheMovieDb from '../api/themoviedb';
|
||||
import { MediaType } from '../constants/media';
|
||||
import Media from '../entity/Media';
|
||||
import logger from '../logger';
|
||||
import { mapTvResult } from '../models/Search';
|
||||
import { mapSeasonWithEpisodes, mapTvDetails } from '../models/Tv';
|
||||
|
||||
const tvRoutes = Router();
|
||||
|
||||
@@ -14,7 +14,7 @@ tvRoutes.get('/:id', async (req, res, next) => {
|
||||
try {
|
||||
const tv = await tmdb.getTvShow({
|
||||
tvId: Number(req.params.id),
|
||||
language: req.query.language as string,
|
||||
language: req.locale ?? (req.query.language as string),
|
||||
});
|
||||
|
||||
const media = await Media.getMedia(tv.id, MediaType.TV);
|
||||
@@ -35,7 +35,7 @@ tvRoutes.get('/:id/season/:seasonNumber', async (req, res) => {
|
||||
const season = await tmdb.getTvSeason({
|
||||
tvId: Number(req.params.id),
|
||||
seasonNumber: Number(req.params.seasonNumber),
|
||||
language: req.query.language as string,
|
||||
language: req.locale ?? (req.query.language as string),
|
||||
});
|
||||
|
||||
return res.status(200).json(mapSeasonWithEpisodes(season));
|
||||
@@ -47,7 +47,7 @@ tvRoutes.get('/:id/recommendations', async (req, res) => {
|
||||
const results = await tmdb.getTvRecommendations({
|
||||
tvId: Number(req.params.id),
|
||||
page: Number(req.query.page),
|
||||
language: req.query.language as string,
|
||||
language: req.locale ?? (req.query.language as string),
|
||||
});
|
||||
|
||||
const media = await Media.getRelatedMedia(
|
||||
@@ -75,7 +75,7 @@ tvRoutes.get('/:id/similar', async (req, res) => {
|
||||
const results = await tmdb.getTvSimilar({
|
||||
tvId: Number(req.params.id),
|
||||
page: Number(req.query.page),
|
||||
language: req.query.language as string,
|
||||
language: req.locale ?? (req.query.language as string),
|
||||
});
|
||||
|
||||
const media = await Media.getRelatedMedia(
|
||||
|
||||
@@ -1,16 +1,20 @@
|
||||
import { Router } from 'express';
|
||||
import gravatarUrl from 'gravatar-url';
|
||||
import { getRepository, Not } from 'typeorm';
|
||||
import PlexTvAPI from '../../api/plextv';
|
||||
import { UserType } from '../../constants/user';
|
||||
import { MediaRequest } from '../../entity/MediaRequest';
|
||||
import { User } from '../../entity/User';
|
||||
import { UserPushSubscription } from '../../entity/UserPushSubscription';
|
||||
import {
|
||||
QuotaResponse,
|
||||
UserRequestsResponse,
|
||||
UserResultsResponse,
|
||||
} from '../../interfaces/api/userInterfaces';
|
||||
import { hasPermission, Permission } from '../../lib/permissions';
|
||||
import { getSettings } from '../../lib/settings';
|
||||
import logger from '../../logger';
|
||||
import gravatarUrl from 'gravatar-url';
|
||||
import { UserType } from '../../constants/user';
|
||||
import { isAuthenticated } from '../../middleware/auth';
|
||||
import { UserResultsResponse } from '../../interfaces/api/userInterfaces';
|
||||
import { UserRequestsResponse } from '../../interfaces/api/userInterfaces';
|
||||
import userSettingsRoutes from './usersettings';
|
||||
|
||||
const router = Router();
|
||||
@@ -27,7 +31,7 @@ router.get('/', async (req, res, next) => {
|
||||
break;
|
||||
case 'displayname':
|
||||
query = query.orderBy(
|
||||
'(CASE WHEN user.username IS NULL THEN user.plexUsername ELSE user.username END)',
|
||||
"(CASE WHEN (user.username IS NULL OR user.username = '') THEN (CASE WHEN (user.plexUsername IS NULL OR user.plexUsername = '') THEN user.email ELSE LOWER(user.plexUsername) END) ELSE LOWER(user.username) END)",
|
||||
'ASC'
|
||||
);
|
||||
break;
|
||||
@@ -78,10 +82,28 @@ router.post(
|
||||
const body = req.body;
|
||||
const userRepository = getRepository(User);
|
||||
|
||||
const existingUser = await userRepository
|
||||
.createQueryBuilder('user')
|
||||
.where('user.email = :email', {
|
||||
email: body.email.toLowerCase(),
|
||||
})
|
||||
.getOne();
|
||||
|
||||
if (existingUser) {
|
||||
return next({
|
||||
status: 409,
|
||||
message: 'User already exists with submitted email.',
|
||||
errors: ['USER_EXISTS'],
|
||||
});
|
||||
}
|
||||
|
||||
const passedExplicitPassword = body.password && body.password.length > 0;
|
||||
const avatar = gravatarUrl(body.email, { default: 'mm', size: 200 });
|
||||
|
||||
if (!passedExplicitPassword && !settings.notifications.agents.email) {
|
||||
if (
|
||||
!passedExplicitPassword &&
|
||||
!settings.notifications.agents.email.enabled
|
||||
) {
|
||||
throw new Error('Email notifications must be enabled');
|
||||
}
|
||||
|
||||
@@ -109,6 +131,48 @@ router.post(
|
||||
}
|
||||
);
|
||||
|
||||
router.post<
|
||||
never,
|
||||
unknown,
|
||||
{
|
||||
endpoint: string;
|
||||
p256dh: string;
|
||||
auth: string;
|
||||
}
|
||||
>('/registerPushSubscription', async (req, res, next) => {
|
||||
try {
|
||||
const userPushSubRepository = getRepository(UserPushSubscription);
|
||||
|
||||
const existingSubs = await userPushSubRepository.find({
|
||||
where: { auth: req.body.auth },
|
||||
});
|
||||
|
||||
if (existingSubs.length > 0) {
|
||||
logger.debug(
|
||||
'User push subscription already exists. Skipping registration.',
|
||||
{ label: 'API' }
|
||||
);
|
||||
return res.status(204).send();
|
||||
}
|
||||
|
||||
const userPushSubscription = new UserPushSubscription({
|
||||
auth: req.body.auth,
|
||||
endpoint: req.body.endpoint,
|
||||
p256dh: req.body.p256dh,
|
||||
user: req.user,
|
||||
});
|
||||
|
||||
userPushSubRepository.save(userPushSubscription);
|
||||
|
||||
return res.status(204).send();
|
||||
} catch (e) {
|
||||
logger.error('Failed to register user push subscription', {
|
||||
label: 'API',
|
||||
});
|
||||
next({ status: 500, message: 'Failed to register subscription.' });
|
||||
}
|
||||
});
|
||||
|
||||
router.get<{ id: string }>('/:id', async (req, res, next) => {
|
||||
try {
|
||||
const userRepository = getRepository(User);
|
||||
@@ -167,7 +231,10 @@ router.get<{ id: string }, UserRequestsResponse>(
|
||||
}
|
||||
);
|
||||
|
||||
const canMakePermissionsChange = (permissions: number, user?: User) =>
|
||||
export const canMakePermissionsChange = (
|
||||
permissions: number,
|
||||
user?: User
|
||||
): boolean =>
|
||||
// Only let the owner grant admin privileges
|
||||
!(hasPermission(Permission.ADMIN, permissions) && user?.id !== 1) ||
|
||||
// Only let users with the manage settings permission, grant the same permission
|
||||
@@ -275,7 +342,7 @@ router.delete<{ id: string }>(
|
||||
});
|
||||
}
|
||||
|
||||
if (user.hasPermission(Permission.ADMIN)) {
|
||||
if (user.hasPermission(Permission.ADMIN) && req.user?.id !== 1) {
|
||||
return next({
|
||||
status: 405,
|
||||
message: 'You cannot delete users with administrative privileges.',
|
||||
@@ -329,47 +396,45 @@ router.post(
|
||||
for (const rawUser of plexUsersResponse.MediaContainer.User) {
|
||||
const account = rawUser.$;
|
||||
|
||||
const user = await userRepository.findOne({
|
||||
where: [{ plexId: account.id }, { email: account.email }],
|
||||
});
|
||||
if (account.email) {
|
||||
const user = await userRepository
|
||||
.createQueryBuilder('user')
|
||||
.where('user.plexId = :id', { id: account.id })
|
||||
.orWhere('user.email = :email', {
|
||||
email: account.email.toLowerCase(),
|
||||
})
|
||||
.getOne();
|
||||
|
||||
if (user) {
|
||||
// Update the users avatar with their plex thumbnail (incase it changed)
|
||||
user.avatar = account.thumb;
|
||||
user.email = account.email;
|
||||
user.plexUsername = account.username;
|
||||
if (user) {
|
||||
// Update the user's avatar with their Plex thumbnail, in case it changed
|
||||
user.avatar = account.thumb;
|
||||
user.email = account.email;
|
||||
user.plexUsername = account.username;
|
||||
|
||||
// in-case the user was previously a local account
|
||||
if (user.userType === UserType.LOCAL) {
|
||||
user.userType = UserType.PLEX;
|
||||
user.plexId = parseInt(account.id);
|
||||
|
||||
if (user.username === account.username) {
|
||||
user.username = '';
|
||||
// In case the user was previously a local account
|
||||
if (user.userType === UserType.LOCAL) {
|
||||
user.userType = UserType.PLEX;
|
||||
user.plexId = parseInt(account.id);
|
||||
}
|
||||
await userRepository.save(user);
|
||||
} else {
|
||||
if (await mainPlexTv.checkUserAccess(parseInt(account.id))) {
|
||||
const newUser = new User({
|
||||
plexUsername: account.username,
|
||||
email: account.email,
|
||||
permissions: settings.main.defaultPermissions,
|
||||
plexId: parseInt(account.id),
|
||||
plexToken: '',
|
||||
avatar: account.thumb,
|
||||
userType: UserType.PLEX,
|
||||
});
|
||||
await userRepository.save(newUser);
|
||||
createdUsers.push(newUser);
|
||||
}
|
||||
}
|
||||
await userRepository.save(user);
|
||||
} else {
|
||||
// Check to make sure it's a real account
|
||||
if (
|
||||
account.email &&
|
||||
account.username &&
|
||||
(await mainPlexTv.checkUserAccess(Number(account.id)))
|
||||
) {
|
||||
const newUser = new User({
|
||||
plexUsername: account.username,
|
||||
email: account.email,
|
||||
permissions: settings.main.defaultPermissions,
|
||||
plexId: parseInt(account.id),
|
||||
plexToken: '',
|
||||
avatar: account.thumb,
|
||||
userType: UserType.PLEX,
|
||||
});
|
||||
await userRepository.save(newUser);
|
||||
createdUsers.push(newUser);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return res.status(201).json(User.filterMany(createdUsers));
|
||||
} catch (e) {
|
||||
next({ status: 500, message: e.message });
|
||||
@@ -377,4 +442,36 @@ router.post(
|
||||
}
|
||||
);
|
||||
|
||||
router.get<{ id: string }, QuotaResponse>(
|
||||
'/:id/quota',
|
||||
async (req, res, next) => {
|
||||
try {
|
||||
const userRepository = getRepository(User);
|
||||
|
||||
if (
|
||||
Number(req.params.id) !== req.user?.id &&
|
||||
!req.user?.hasPermission(
|
||||
[Permission.MANAGE_USERS, Permission.MANAGE_REQUESTS],
|
||||
{ type: 'and' }
|
||||
)
|
||||
) {
|
||||
return next({
|
||||
status: 403,
|
||||
message: 'You do not have permission to access this endpoint.',
|
||||
});
|
||||
}
|
||||
|
||||
const user = await userRepository.findOneOrFail({
|
||||
where: { id: Number(req.params.id) },
|
||||
});
|
||||
|
||||
const quotas = await user.getQuota();
|
||||
|
||||
return res.status(200).json(quotas);
|
||||
} catch (e) {
|
||||
next({ status: 404, message: e.message });
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
export default router;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Router } from 'express';
|
||||
import { getRepository } from 'typeorm';
|
||||
import { canMakePermissionsChange } from '.';
|
||||
import { User } from '../../entity/User';
|
||||
import { UserSettings } from '../../entity/UserSettings';
|
||||
import {
|
||||
@@ -7,6 +8,7 @@ import {
|
||||
UserSettingsNotificationsResponse,
|
||||
} from '../../interfaces/api/userSettingsInterfaces';
|
||||
import { Permission } from '../../lib/permissions';
|
||||
import { getSettings } from '../../lib/settings';
|
||||
import logger from '../../logger';
|
||||
import { isAuthenticated } from '../../middleware/auth';
|
||||
|
||||
@@ -21,6 +23,7 @@ const isOwnProfileOrAdmin = (): Middleware => {
|
||||
message: "You do not have permission to view this user's settings.",
|
||||
});
|
||||
}
|
||||
|
||||
next();
|
||||
};
|
||||
return authMiddleware;
|
||||
@@ -32,6 +35,9 @@ userSettingsRoutes.get<{ id: string }, UserSettingsGeneralResponse>(
|
||||
'/main',
|
||||
isOwnProfileOrAdmin(),
|
||||
async (req, res, next) => {
|
||||
const {
|
||||
main: { defaultQuotas },
|
||||
} = getSettings();
|
||||
const userRepository = getRepository(User);
|
||||
|
||||
try {
|
||||
@@ -45,8 +51,17 @@ userSettingsRoutes.get<{ id: string }, UserSettingsGeneralResponse>(
|
||||
|
||||
return res.status(200).json({
|
||||
username: user.username,
|
||||
locale: user.settings?.locale,
|
||||
region: user.settings?.region,
|
||||
originalLanguage: user.settings?.originalLanguage,
|
||||
movieQuotaLimit: user.movieQuotaLimit,
|
||||
movieQuotaDays: user.movieQuotaDays,
|
||||
tvQuotaLimit: user.tvQuotaLimit,
|
||||
tvQuotaDays: user.tvQuotaDays,
|
||||
globalMovieQuotaDays: defaultQuotas.movie.quotaDays,
|
||||
globalMovieQuotaLimit: defaultQuotas.movie.quotaLimit,
|
||||
globalTvQuotaDays: defaultQuotas.tv.quotaDays,
|
||||
globalTvQuotaLimit: defaultQuotas.tv.quotaLimit,
|
||||
});
|
||||
} catch (e) {
|
||||
next({ status: 500, message: e.message });
|
||||
@@ -70,21 +85,48 @@ userSettingsRoutes.post<
|
||||
return next({ status: 404, message: 'User not found.' });
|
||||
}
|
||||
|
||||
// "Owner" user settings cannot be modified by other users
|
||||
if (user.id === 1 && req.user?.id !== 1) {
|
||||
return next({
|
||||
status: 403,
|
||||
message: "You do not have permission to modify this user's settings.",
|
||||
});
|
||||
}
|
||||
|
||||
user.username = req.body.username;
|
||||
|
||||
// Update quota values only if the user has the correct permissions
|
||||
if (
|
||||
!user.hasPermission(Permission.MANAGE_USERS) &&
|
||||
req.user?.id !== user.id
|
||||
) {
|
||||
user.movieQuotaDays = req.body.movieQuotaDays;
|
||||
user.movieQuotaLimit = req.body.movieQuotaLimit;
|
||||
user.tvQuotaDays = req.body.tvQuotaDays;
|
||||
user.tvQuotaLimit = req.body.tvQuotaLimit;
|
||||
}
|
||||
|
||||
if (!user.settings) {
|
||||
user.settings = new UserSettings({
|
||||
user: req.user,
|
||||
locale: req.body.locale,
|
||||
region: req.body.region,
|
||||
originalLanguage: req.body.originalLanguage,
|
||||
});
|
||||
} else {
|
||||
user.settings.locale = req.body.locale;
|
||||
user.settings.region = req.body.region;
|
||||
user.settings.originalLanguage = req.body.originalLanguage;
|
||||
}
|
||||
|
||||
await userRepository.save(user);
|
||||
|
||||
return res.status(200).json({ username: user.username });
|
||||
return res.status(200).json({
|
||||
username: user.username,
|
||||
region: user.settings.region,
|
||||
locale: user.settings.locale,
|
||||
originalLanguage: user.settings.originalLanguage,
|
||||
});
|
||||
} catch (e) {
|
||||
next({ status: 500, message: e.message });
|
||||
}
|
||||
@@ -137,7 +179,19 @@ userSettingsRoutes.post<
|
||||
if (req.body.newPassword.length < 8) {
|
||||
return next({
|
||||
status: 400,
|
||||
message: 'Password must be at least 8 characters',
|
||||
message: 'Password must be at least 8 characters.',
|
||||
});
|
||||
}
|
||||
|
||||
if (
|
||||
(user.id === 1 && req.user?.id !== 1) ||
|
||||
(user.hasPermission(Permission.ADMIN) &&
|
||||
user.id !== req.user?.id &&
|
||||
req.user?.id !== 1)
|
||||
) {
|
||||
return next({
|
||||
status: 403,
|
||||
message: "You do not have permission to modify this user's password.",
|
||||
});
|
||||
}
|
||||
|
||||
@@ -184,6 +238,7 @@ userSettingsRoutes.get<{ id: string }, UserSettingsNotificationsResponse>(
|
||||
isOwnProfileOrAdmin(),
|
||||
async (req, res, next) => {
|
||||
const userRepository = getRepository(User);
|
||||
const settings = getSettings()?.notifications.agents;
|
||||
|
||||
try {
|
||||
const user = await userRepository.findOne({
|
||||
@@ -195,8 +250,19 @@ userSettingsRoutes.get<{ id: string }, UserSettingsNotificationsResponse>(
|
||||
}
|
||||
|
||||
return res.status(200).json({
|
||||
enableNotifications: user.settings?.enableNotifications ?? true,
|
||||
emailEnabled: settings?.email.enabled,
|
||||
pgpKey: user.settings?.pgpKey,
|
||||
discordEnabled: settings?.discord.enabled,
|
||||
discordEnabledTypes: settings?.discord.enabled
|
||||
? settings?.discord.types
|
||||
: 0,
|
||||
discordId: user.settings?.discordId,
|
||||
telegramEnabled: settings?.telegram.enabled,
|
||||
telegramBotUsername: settings?.telegram.options.botUsername,
|
||||
telegramChatId: user.settings?.telegramChatId,
|
||||
telegramSendSilently: user?.settings?.telegramSendSilently,
|
||||
webPushEnabled: settings?.webpush.enabled,
|
||||
notificationTypes: user.settings?.notificationTypes ?? {},
|
||||
});
|
||||
} catch (e) {
|
||||
next({ status: 500, message: e.message });
|
||||
@@ -204,43 +270,64 @@ userSettingsRoutes.get<{ id: string }, UserSettingsNotificationsResponse>(
|
||||
}
|
||||
);
|
||||
|
||||
userSettingsRoutes.post<
|
||||
{ id: string },
|
||||
UserSettingsNotificationsResponse,
|
||||
UserSettingsNotificationsResponse
|
||||
>('/notifications', isOwnProfileOrAdmin(), async (req, res, next) => {
|
||||
const userRepository = getRepository(User);
|
||||
userSettingsRoutes.post<{ id: string }, UserSettingsNotificationsResponse>(
|
||||
'/notifications',
|
||||
isOwnProfileOrAdmin(),
|
||||
async (req, res, next) => {
|
||||
const userRepository = getRepository(User);
|
||||
|
||||
try {
|
||||
const user = await userRepository.findOne({
|
||||
where: { id: Number(req.params.id) },
|
||||
});
|
||||
|
||||
if (!user) {
|
||||
return next({ status: 404, message: 'User not found.' });
|
||||
}
|
||||
|
||||
if (!user.settings) {
|
||||
user.settings = new UserSettings({
|
||||
user: req.user,
|
||||
enableNotifications: req.body.enableNotifications,
|
||||
discordId: req.body.discordId,
|
||||
try {
|
||||
const user = await userRepository.findOne({
|
||||
where: { id: Number(req.params.id) },
|
||||
});
|
||||
} else {
|
||||
user.settings.enableNotifications = req.body.enableNotifications;
|
||||
user.settings.discordId = req.body.discordId;
|
||||
|
||||
if (!user) {
|
||||
return next({ status: 404, message: 'User not found.' });
|
||||
}
|
||||
|
||||
// "Owner" user settings cannot be modified by other users
|
||||
if (user.id === 1 && req.user?.id !== 1) {
|
||||
return next({
|
||||
status: 403,
|
||||
message: "You do not have permission to modify this user's settings.",
|
||||
});
|
||||
}
|
||||
|
||||
if (!user.settings) {
|
||||
user.settings = new UserSettings({
|
||||
user: req.user,
|
||||
pgpKey: req.body.pgpKey,
|
||||
discordId: req.body.discordId,
|
||||
telegramChatId: req.body.telegramChatId,
|
||||
telegramSendSilently: req.body.telegramSendSilently,
|
||||
notificationTypes: req.body.notificationTypes,
|
||||
});
|
||||
} else {
|
||||
user.settings.pgpKey = req.body.pgpKey;
|
||||
user.settings.discordId = req.body.discordId;
|
||||
user.settings.telegramChatId = req.body.telegramChatId;
|
||||
user.settings.telegramSendSilently = req.body.telegramSendSilently;
|
||||
user.settings.notificationTypes = Object.assign(
|
||||
{},
|
||||
user.settings.notificationTypes,
|
||||
req.body.notificationTypes
|
||||
);
|
||||
}
|
||||
|
||||
userRepository.save(user);
|
||||
|
||||
return res.status(200).json({
|
||||
pgpKey: user.settings?.pgpKey,
|
||||
discordId: user.settings?.discordId,
|
||||
telegramChatId: user.settings?.telegramChatId,
|
||||
telegramSendSilently: user?.settings?.telegramSendSilently,
|
||||
notificationTypes: user.settings.notificationTypes,
|
||||
});
|
||||
} catch (e) {
|
||||
next({ status: 500, message: e.message });
|
||||
}
|
||||
|
||||
userRepository.save(user);
|
||||
|
||||
return res.status(200).json({
|
||||
enableNotifications: user.settings.enableNotifications,
|
||||
discordId: user.settings.discordId,
|
||||
});
|
||||
} catch (e) {
|
||||
next({ status: 500, message: e.message });
|
||||
}
|
||||
});
|
||||
);
|
||||
|
||||
userSettingsRoutes.get<{ id: string }, { permissions?: number }>(
|
||||
'/permissions',
|
||||
@@ -283,13 +370,20 @@ userSettingsRoutes.post<
|
||||
return next({ status: 404, message: 'User not found.' });
|
||||
}
|
||||
|
||||
if (user.id === 1) {
|
||||
// "Owner" user permissions cannot be modified, and users cannot set their own permissions
|
||||
if (user.id === 1 || req.user?.id === user.id) {
|
||||
return next({
|
||||
status: 500,
|
||||
message: 'Permissions for user with ID 1 cannot be modified',
|
||||
status: 403,
|
||||
message: 'You do not have permission to modify this user',
|
||||
});
|
||||
}
|
||||
|
||||
if (!canMakePermissionsChange(req.body.permissions, req.user)) {
|
||||
return next({
|
||||
status: 403,
|
||||
message: 'You do not have permission to grant this level of access',
|
||||
});
|
||||
}
|
||||
user.permissions = req.body.permissions;
|
||||
|
||||
await userRepository.save(user);
|
||||
|
||||
Reference in New Issue
Block a user