Files
channels-seerr/server/routes/index.ts
fallenbagel ddf347994a chore(deps): update dependencies and fix security vulnerabilities (#2342)
* chore(deps): update dependencies and fix security vulnerabilities

Update TypeScript 4.9 → 5.4. Update Zod 3 → 4. Update nodemailer 6 → 7. Update @typescript-eslint
packages to v7. Update xml2js, undici, lodash, axios, swr, winston- Add pnpm.overrides for
transitive dependency vulnerabilities

* chore: fix import ordering for TypeScript 5.4 compatibility

prettier-plugin-organize-imports behaves differently with TypeScript 5.4 vs 4.9, causing CI
formatting checks to fail. This reformats imports to match the ordering expected by the plugin with
the upgraded TS version.
2026-01-27 19:00:42 +01:00

454 lines
12 KiB
TypeScript

import GithubAPI from '@server/api/github';
import PushoverAPI from '@server/api/pushover';
import TheMovieDb from '@server/api/themoviedb';
import type {
TmdbMovieResult,
TmdbTvResult,
} from '@server/api/themoviedb/interfaces';
import { getRepository } from '@server/datasource';
import DiscoverSlider from '@server/entity/DiscoverSlider';
import type { StatusResponse } from '@server/interfaces/api/settingsInterfaces';
import { Permission } from '@server/lib/permissions';
import { getSettings } from '@server/lib/settings';
import logger from '@server/logger';
import { checkUser, isAuthenticated } from '@server/middleware/auth';
import { mapProductionCompany } from '@server/models/Movie';
import { mapNetwork } from '@server/models/Tv';
import { mapWatchProviderDetails } from '@server/models/common';
import overrideRuleRoutes from '@server/routes/overrideRule';
import settingsRoutes from '@server/routes/settings';
import watchlistRoutes from '@server/routes/watchlist';
import {
appDataPath,
appDataPermissions,
appDataStatus,
} from '@server/utils/appDataVolume';
import { getAppVersion, getCommitTag } from '@server/utils/appVersion';
import restartFlag from '@server/utils/restartFlag';
import { isPerson } from '@server/utils/typeHelpers';
import { Router } from 'express';
import authRoutes from './auth';
import blacklistRoutes from './blacklist';
import collectionRoutes from './collection';
import discoverRoutes, { createTmdbWithRegionLanguage } from './discover';
import issueRoutes from './issue';
import issueCommentRoutes from './issueComment';
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 tvRoutes from './tv';
import user from './user';
const router = Router();
router.use(checkUser);
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.getSeerrCommits();
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.getSeerrReleases();
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,
restartRequired: restartFlag.isSet(),
});
});
router.get('/status/appdata', (_req, res) => {
return res.status(200).json({
appData: appDataStatus(),
appDataPath: appDataPath(),
appDataPermissions: appDataPermissions(),
});
});
router.use('/user', isAuthenticated(), user);
router.get('/settings/public', async (req, res) => {
const settings = getSettings();
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.get('/settings/discover', isAuthenticated(), async (_req, res) => {
const sliderRepository = getRepository(DiscoverSlider);
const sliders = await sliderRepository.find({ order: { order: 'ASC' } });
return res.json(sliders);
});
router.get(
'/settings/notifications/pushover/sounds',
isAuthenticated(),
async (req, res, next) => {
const pushoverApi = new PushoverAPI();
try {
if (!req.query.token) {
throw new Error('Pushover application token missing from request');
}
const sounds = await pushoverApi.getSounds(req.query.token as string);
res.status(200).json(sounds);
} catch (e) {
logger.debug('Something went wrong retrieving Pushover sounds', {
label: 'API',
errorMessage: e.message,
});
return next({
status: 500,
message: 'Unable to retrieve Pushover sounds.',
});
}
}
);
router.use('/settings', isAuthenticated(Permission.ADMIN), settingsRoutes);
router.use('/search', isAuthenticated(), searchRoutes);
router.use('/discover', isAuthenticated(), discoverRoutes);
router.use('/request', isAuthenticated(), requestRoutes);
router.use('/watchlist', isAuthenticated(), watchlistRoutes);
router.use('/blacklist', isAuthenticated(), blacklistRoutes);
router.use('/movie', isAuthenticated(), movieRoutes);
router.use('/tv', isAuthenticated(), tvRoutes);
router.use('/media', isAuthenticated(), mediaRoutes);
router.use('/person', isAuthenticated(), personRoutes);
router.use('/collection', isAuthenticated(), collectionRoutes);
router.use('/service', isAuthenticated(), serviceRoutes);
router.use('/issue', isAuthenticated(), issueRoutes);
router.use('/issueComment', isAuthenticated(), issueCommentRoutes);
router.use('/auth', authRoutes);
router.use(
'/overrideRule',
isAuthenticated(Permission.ADMIN),
overrideRuleRoutes
);
router.get('/regions', isAuthenticated(), async (req, res, next) => {
const tmdb = new TheMovieDb();
try {
const regions = await tmdb.getRegions();
return res.status(200).json(regions);
} catch (e) {
logger.debug('Something went wrong retrieving regions', {
label: 'API',
errorMessage: e.message,
});
return next({
status: 500,
message: 'Unable to retrieve regions.',
});
}
});
router.get('/languages', isAuthenticated(), async (req, res, next) => {
const tmdb = new TheMovieDb();
try {
const languages = await tmdb.getLanguages();
return res.status(200).json(languages);
} catch (e) {
logger.debug('Something went wrong retrieving languages', {
label: 'API',
errorMessage: e.message,
});
return next({
status: 500,
message: 'Unable to retrieve languages.',
});
}
});
router.get<{ id: string }>('/studio/:id', async (req, res, next) => {
const tmdb = new TheMovieDb();
try {
const studio = await tmdb.getStudio(Number(req.params.id));
return res.status(200).json(mapProductionCompany(studio));
} catch (e) {
logger.debug('Something went wrong retrieving studio', {
label: 'API',
errorMessage: e.message,
studioId: req.params.id,
});
return next({
status: 500,
message: 'Unable to retrieve studio.',
});
}
});
router.get<{ id: string }>('/network/:id', async (req, res, next) => {
const tmdb = new TheMovieDb();
try {
const network = await tmdb.getNetwork(Number(req.params.id));
return res.status(200).json(mapNetwork(network));
} catch (e) {
logger.debug('Something went wrong retrieving network', {
label: 'API',
errorMessage: e.message,
networkId: req.params.id,
});
return next({
status: 500,
message: 'Unable to retrieve network.',
});
}
});
router.get('/genres/movie', isAuthenticated(), async (req, res, next) => {
const tmdb = new TheMovieDb();
try {
const genres = await tmdb.getMovieGenres({
language: (req.query.language as string) ?? req.locale,
});
return res.status(200).json(genres);
} catch (e) {
logger.debug('Something went wrong retrieving movie genres', {
label: 'API',
errorMessage: e.message,
});
return next({
status: 500,
message: 'Unable to retrieve movie genres.',
});
}
});
router.get('/genres/tv', isAuthenticated(), async (req, res, next) => {
const tmdb = new TheMovieDb();
try {
const genres = await tmdb.getTvGenres({
language: (req.query.language as string) ?? req.locale,
});
return res.status(200).json(genres);
} catch (e) {
logger.debug('Something went wrong retrieving series genres', {
label: 'API',
errorMessage: e.message,
});
return next({
status: 500,
message: 'Unable to retrieve series genres.',
});
}
});
router.get('/backdrops', async (req, res, next) => {
const tmdb = createTmdbWithRegionLanguage();
try {
const data = (
await tmdb.getAllTrending({
page: 1,
timeWindow: 'week',
})
).results.filter((result) => !isPerson(result)) as (
| TmdbMovieResult
| TmdbTvResult
)[];
return res
.status(200)
.json(
data
.map((result) => result.backdrop_path)
.filter((backdropPath) => !!backdropPath)
);
} catch (e) {
logger.debug('Something went wrong retrieving backdrops', {
label: 'API',
errorMessage: e.message,
});
return next({
status: 500,
message: 'Unable to retrieve backdrops.',
});
}
});
router.get('/keyword/:keywordId', async (req, res, next) => {
const tmdb = createTmdbWithRegionLanguage();
try {
const result = await tmdb.getKeywordDetails({
keywordId: Number(req.params.keywordId),
});
return res.status(200).json(result);
} catch (e) {
logger.debug('Something went wrong retrieving keyword data', {
label: 'API',
errorMessage: e.message,
});
return next({
status: 500,
message: 'Unable to retrieve keyword data.',
});
}
});
router.get('/watchproviders/regions', async (req, res, next) => {
const tmdb = createTmdbWithRegionLanguage();
try {
const result = await tmdb.getAvailableWatchProviderRegions({});
return res.status(200).json(result);
} catch (e) {
logger.debug('Something went wrong retrieving watch provider regions', {
label: 'API',
errorMessage: e.message,
});
return next({
status: 500,
message: 'Unable to retrieve watch provider regions.',
});
}
});
router.get('/watchproviders/movies', async (req, res, next) => {
const tmdb = createTmdbWithRegionLanguage();
try {
const result = await tmdb.getMovieWatchProviders({
watchRegion: req.query.watchRegion as string,
});
return res.status(200).json(mapWatchProviderDetails(result));
} catch (e) {
logger.debug('Something went wrong retrieving movie watch providers', {
label: 'API',
errorMessage: e.message,
});
return next({
status: 500,
message: 'Unable to retrieve movie watch providers.',
});
}
});
router.get('/watchproviders/tv', async (req, res, next) => {
const tmdb = createTmdbWithRegionLanguage();
try {
const result = await tmdb.getTvWatchProviders({
watchRegion: req.query.watchRegion as string,
});
return res.status(200).json(mapWatchProviderDetails(result));
} catch (e) {
logger.debug('Something went wrong retrieving tv watch providers', {
label: 'API',
errorMessage: e.message,
});
return next({
status: 500,
message: 'Unable to retrieve tv watch providers.',
});
}
});
router.get(
'/certifications/movie',
isAuthenticated(),
async (req, res, next) => {
const tmdb = new TheMovieDb();
try {
const certifications = await tmdb.getMovieCertifications();
return res.status(200).json(certifications);
} catch (e) {
logger.error('Something went wrong retrieving movie certifications', {
label: 'API',
errorMessage: e.message,
});
return next({
status: 500,
message: 'Unable to retrieve movie certifications.',
});
}
}
);
router.get('/certifications/tv', isAuthenticated(), async (req, res, next) => {
const tmdb = new TheMovieDb();
try {
const certifications = await tmdb.getTvCertifications();
return res.status(200).json(certifications);
} catch (e) {
logger.debug('Something went wrong retrieving TV certifications', {
label: 'API',
errorMessage: e.message,
});
return next({
status: 500,
message: 'Unable to retrieve TV certifications.',
});
}
});
router.get('/', (_req, res) => {
return res.status(200).json({
api: 'Seerr API',
version: '1.0',
});
});
export default router;