feat: add base path setting for reverse proxy support

issue #274
feat/basepath
megamit 3 years ago
parent d2241a4187
commit 7de426f483

@ -3,10 +3,13 @@
// previously cached resources to be updated from the network.
// This variable is intentionally declared and unused.
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const OFFLINE_VERSION = 3;
const OFFLINE_VERSION = 4;
const CACHE_NAME = "offline";
// use scope path to determine if we are running in basePath
const BASE_PATH = (new URL(self.registration.scope)).pathname.replace(/\/$/, '')
// Customize this with a different URL if needed.
const OFFLINE_URL = "/offline.html";
const OFFLINE_URL = BASE_PATH + "/offline.html";
self.addEventListener("install", (event) => {
event.waitUntil(
@ -120,16 +123,16 @@ self.addEventListener('notificationclick', (event) => {
event.notification.close();
if (event.action === 'approve') {
fetch(`/api/v1/request/${notificationData.requestId}/approve`, {
fetch(`${BASE_PATH}/api/v1/request/${notificationData.requestId}/approve`, {
method: 'POST',
});
} else if (event.action === 'decline') {
fetch(`/api/v1/request/${notificationData.requestId}/decline`, {
fetch(`${BASE_PATH}/api/v1/request/${notificationData.requestId}/decline`, {
method: 'POST',
});
}
if (notificationData.actionUrl) {
clients.openWindow(notificationData.actionUrl);
clients.openWindow(BASE_PATH + notificationData.actionUrl);
}
}, false);

@ -2,7 +2,7 @@ import { getClientIp } from '@supercharge/request-ip';
import { TypeormStore } from 'connect-typeorm/out';
import cookieParser from 'cookie-parser';
import csurf from 'csurf';
import express, { NextFunction, Request, Response } from 'express';
import express, { NextFunction, Request, Response, Router } from 'express';
import * as OpenApiValidator from 'express-openapi-validator';
import session, { Store } from 'express-session';
import next from 'next';
@ -30,17 +30,13 @@ import routes from './routes';
import { getAppVersion } from './utils/appVersion';
const API_SPEC_PATH = path.join(__dirname, '../overseerr-api.yml');
const NEXT_CONFIG_PATH = path.join(__dirname, '../next.config.js');
logger.info(`Starting Overseerr version ${getAppVersion()}`);
const dev = process.env.NODE_ENV !== 'production';
const app = next({ dev });
const handle = app.getRequestHandler();
app
.prepare()
.then(async () => {
const dbConnection = await createConnection();
createConnection()
.then(async (dbConnection) => {
// Run migrations in production
if (process.env.NODE_ENV === 'production') {
await dbConnection.query('PRAGMA foreign_keys=OFF');
@ -51,6 +47,34 @@ app
// Load Settings
const settings = getSettings().load();
const { default: nextConfig } = await import(NEXT_CONFIG_PATH);
let basePath = settings.main.basePath;
if (basePath) {
// Ensure base path is free of invalid characters
basePath = encodeURI(basePath);
logger.debug(`Serving from base path ${basePath}`);
nextConfig.rewrites = async () => ({
beforeFiles: [
/* Strip off base path for serving pages */
{
source: basePath + '/:path*',
destination: '/:path*',
},
],
});
nextConfig.publicRuntimeConfig = { basePath };
}
const app = next({ dev, conf: nextConfig });
const handle = app.getRequestHandler();
if (basePath) {
app.setAssetPrefix(basePath + '/');
}
await app.prepare();
// Migrate library types
if (
settings.plex.libraries.length > 1 &&
@ -131,7 +155,9 @@ app
// Set up sessions
const sessionRespository = getRepository(Session);
server.use(
const apiRoutes = Router();
server.use(basePath || '/', apiRoutes);
apiRoutes.use(
'/api',
session({
secret: settings.clientId,
@ -150,8 +176,8 @@ app
})
);
const apiDocs = YAML.load(API_SPEC_PATH);
server.use('/api-docs', swaggerUi.serve, swaggerUi.setup(apiDocs));
server.use(
apiRoutes.use('/api-docs', swaggerUi.serve, swaggerUi.setup(apiDocs));
apiRoutes.use(
OpenApiValidator.middleware({
apiSpec: API_SPEC_PATH,
validateRequests: true,
@ -162,15 +188,15 @@ app
* OpenAPI validator. Otherwise, they are treated as objects instead of strings
* and response validation will fail
*/
server.use((_req, res, next) => {
apiRoutes.use((_req, res, next) => {
const original = res.json;
res.json = function jsonp(json) {
return original.call(this, JSON.parse(JSON.stringify(json)));
};
next();
});
server.use('/api/v1', routes);
server.get('*', (req, res) => handle(req, res));
apiRoutes.use('/api/v1', routes);
apiRoutes.get('*', (req, res) => handle(req, res));
server.use(
(
err: { status: number; message: string; errors: string[] },

@ -92,6 +92,7 @@ export interface MainSettings {
trustProxy: boolean;
partialRequestsEnabled: boolean;
locale: string;
basePath: string;
}
interface PublicSettings {
@ -271,6 +272,7 @@ class Settings {
trustProxy: false,
partialRequestsEnabled: true,
locale: 'en',
basePath: '',
},
plex: {
name: '',

@ -1,6 +1,5 @@
import { DownloadIcon } from '@heroicons/react/outline';
import { uniq } from 'lodash';
import Link from 'next/link';
import { useRouter } from 'next/router';
import React, { useState } from 'react';
import { defineMessages, useIntl } from 'react-intl';
@ -15,6 +14,7 @@ import ButtonWithDropdown from '../Common/ButtonWithDropdown';
import CachedImage from '../Common/CachedImage';
import LoadingSpinner from '../Common/LoadingSpinner';
import PageTitle from '../Common/PageTitle';
import Link from '../Link';
import RequestModal from '../RequestModal';
import Slider from '../Slider';
import StatusBadge from '../StatusBadge';

@ -1,6 +1,7 @@
import Image, { ImageProps } from 'next/image';
import React from 'react';
import useSettings from '../../../hooks/useSettings';
import addBasePath from '../../../utils/addBasePath';
/**
* The CachedImage component should be used wherever
@ -12,6 +13,10 @@ import useSettings from '../../../hooks/useSettings';
const CachedImage: React.FC<ImageProps> = (props) => {
const { currentSettings } = useSettings();
/* Check for internal urls and add base path */
if (typeof props.src === 'string' && !props.src.includes('://')) {
props = { ...props, src: addBasePath(props.src) };
}
return <Image unoptimized={!currentSettings.cacheImages} {...props} />;
};

@ -1,8 +1,8 @@
import Link from 'next/link';
import { useRouter } from 'next/router';
import React from 'react';
import { hasPermission, Permission } from '../../../../server/lib/permissions';
import { useRouter } from '../../../hooks/useRouter';
import { useUser } from '../../../hooks/useUser';
import Link from '../../Link';
export interface SettingsRoute {
text: string;

@ -1,10 +1,10 @@
import React from 'react';
import type { MovieResult } from '../../../../server/models/Search';
import { useRouter } from '../../../hooks/useRouter';
import ListView from '../../Common/ListView';
import { defineMessages, useIntl } from 'react-intl';
import Header from '../../Common/Header';
import PageTitle from '../../Common/PageTitle';
import { useRouter } from 'next/router';
import globalMessages from '../../../i18n/globalMessages';
import useDiscover from '../../../hooks/useDiscover';
import Error from '../../../pages/_error';

@ -1,10 +1,10 @@
import { useRouter } from 'next/router';
import React from 'react';
import type { TvResult } from '../../../../server/models/Search';
import ListView from '../../Common/ListView';
import { defineMessages, useIntl } from 'react-intl';
import Header from '../../Common/Header';
import PageTitle from '../../Common/PageTitle';
import { useRouter } from 'next/router';
import globalMessages from '../../../i18n/globalMessages';
import useDiscover from '../../../hooks/useDiscover';
import Error from '../../../pages/_error';

@ -1,10 +1,10 @@
import React from 'react';
import type { MovieResult } from '../../../../server/models/Search';
import { useRouter } from '../../../hooks/useRouter';
import ListView from '../../Common/ListView';
import { defineMessages, useIntl } from 'react-intl';
import Header from '../../Common/Header';
import PageTitle from '../../Common/PageTitle';
import { useRouter } from 'next/router';
import globalMessages from '../../../i18n/globalMessages';
import useDiscover from '../../../hooks/useDiscover';
import Error from '../../../pages/_error';

@ -4,10 +4,10 @@ import ListView from '../../Common/ListView';
import { defineMessages, useIntl } from 'react-intl';
import Header from '../../Common/Header';
import PageTitle from '../../Common/PageTitle';
import { useRouter } from 'next/router';
import globalMessages from '../../../i18n/globalMessages';
import useDiscover from '../../../hooks/useDiscover';
import Error from '../../../pages/_error';
import { useRouter } from '../../../hooks/useRouter';
const messages = defineMessages({
genreSeries: '{genre} Series',

@ -1,10 +1,10 @@
import React from 'react';
import type { TvResult } from '../../../../server/models/Search';
import { useRouter } from '../../../hooks/useRouter';
import ListView from '../../Common/ListView';
import { defineMessages, useIntl } from 'react-intl';
import Header from '../../Common/Header';
import PageTitle from '../../Common/PageTitle';
import { useRouter } from 'next/router';
import globalMessages from '../../../i18n/globalMessages';
import useDiscover from '../../../hooks/useDiscover';
import Error from '../../../pages/_error';

@ -1,10 +1,10 @@
import { ArrowCircleRightIcon } from '@heroicons/react/outline';
import Link from 'next/link';
import React from 'react';
import { defineMessages, useIntl } from 'react-intl';
import useSWR from 'swr';
import { GenreSliderItem } from '../../../../server/interfaces/api/discoverInterfaces';
import GenreCard from '../../GenreCard';
import Link from '../../Link';
import Slider from '../../Slider';
import { genreColorMap } from '../constants';

@ -1,10 +1,10 @@
import { ArrowCircleRightIcon } from '@heroicons/react/outline';
import Link from 'next/link';
import React from 'react';
import { defineMessages, useIntl } from 'react-intl';
import useSWR from 'swr';
import { GenreSliderItem } from '../../../../server/interfaces/api/discoverInterfaces';
import GenreCard from '../../GenreCard';
import Link from '../../Link';
import Slider from '../../Slider';
import { genreColorMap } from '../constants';

@ -1,11 +1,11 @@
import { ArrowCircleRightIcon } from '@heroicons/react/outline';
import Link from 'next/link';
import React from 'react';
import { defineMessages, useIntl } from 'react-intl';
import useSWR from 'swr';
import type { MediaResultsResponse } from '../../../server/interfaces/api/mediaInterfaces';
import type { RequestResultsResponse } from '../../../server/interfaces/api/requestInterfaces';
import PageTitle from '../Common/PageTitle';
import Link from '../Link';
import MediaSlider from '../MediaSlider';
import RequestCard from '../RequestCard';
import Slider from '../Slider';

@ -1,7 +1,7 @@
import Link from 'next/link';
import React, { useState } from 'react';
import { withProperties } from '../../utils/typeHelpers';
import CachedImage from '../Common/CachedImage';
import Link from '../Link';
interface GenreCardProps {
name: string;

@ -4,13 +4,13 @@ import {
EyeIcon,
UserIcon,
} from '@heroicons/react/solid';
import Link from 'next/link';
import React from 'react';
import { useIntl } from 'react-intl';
import type Issue from '../../../server/entity/Issue';
import globalMessages from '../../i18n/globalMessages';
import Button from '../Common/Button';
import { issueOptions } from '../IssueModal/constants';
import Link from '../Link';
interface IssueBlockProps {
issue: Issue;

@ -3,7 +3,6 @@ import { ExclamationIcon } from '@heroicons/react/outline';
import { DotsVerticalIcon } from '@heroicons/react/solid';
import axios from 'axios';
import { Field, Form, Formik } from 'formik';
import Link from 'next/link';
import React, { useState } from 'react';
import { defineMessages, FormattedRelativeTime, useIntl } from 'react-intl';
import ReactMarkdown from 'react-markdown';
@ -12,6 +11,7 @@ import type { default as IssueCommentType } from '../../../../server/entity/Issu
import { Permission, useUser } from '../../../hooks/useUser';
import Button from '../../Common/Button';
import Modal from '../../Common/Modal';
import Link from '../../Link';
import Transition from '../../Transition';
const messages = defineMessages({

@ -8,8 +8,6 @@ import {
import { RefreshIcon } from '@heroicons/react/solid';
import axios from 'axios';
import { Field, Form, Formik } from 'formik';
import Link from 'next/link';
import { useRouter } from 'next/router';
import React, { useState } from 'react';
import { defineMessages, FormattedRelativeTime, useIntl } from 'react-intl';
import { useToasts } from 'react-toast-notifications';
@ -20,6 +18,7 @@ import { MediaType } from '../../../server/constants/media';
import type Issue from '../../../server/entity/Issue';
import type { MovieDetails } from '../../../server/models/Movie';
import type { TvDetails } from '../../../server/models/Tv';
import { useRouter } from '../../hooks/useRouter';
import { Permission, useUser } from '../../hooks/useUser';
import globalMessages from '../../i18n/globalMessages';
import Error from '../../pages/_error';
@ -30,6 +29,7 @@ import LoadingSpinner from '../Common/LoadingSpinner';
import Modal from '../Common/Modal';
import PageTitle from '../Common/PageTitle';
import { issueOptions } from '../IssueModal/constants';
import Link from '../Link';
import Transition from '../Transition';
import IssueComment from './IssueComment';
import IssueDescription from './IssueDescription';

@ -1,5 +1,4 @@
import { EyeIcon } from '@heroicons/react/solid';
import Link from 'next/link';
import React from 'react';
import { useInView } from 'react-intersection-observer';
import { defineMessages, FormattedRelativeTime, useIntl } from 'react-intl';
@ -15,6 +14,7 @@ import Badge from '../../Common/Badge';
import Button from '../../Common/Button';
import CachedImage from '../../Common/CachedImage';
import { issueOptions } from '../../IssueModal/constants';
import Link from '../../Link';
const messages = defineMessages({
openeduserdate: '{date} by {user}',

@ -4,12 +4,12 @@ import {
FilterIcon,
SortDescendingIcon,
} from '@heroicons/react/solid';
import { useRouter } from 'next/router';
import React, { useEffect, useState } from 'react';
import { defineMessages, useIntl } from 'react-intl';
import useSWR from 'swr';
import { IssueResultsResponse } from '../../../server/interfaces/api/issueInterfaces';
import Button from '../../components/Common/Button';
import { useRouter } from '../../hooks/useRouter';
import { useUpdateQueryParams } from '../../hooks/useUpdateQueryParams';
import globalMessages from '../../i18n/globalMessages';
import Header from '../Common/Header';

@ -3,7 +3,6 @@ import { ExclamationIcon } from '@heroicons/react/outline';
import { ArrowCircleRightIcon } from '@heroicons/react/solid';
import axios from 'axios';
import { Field, Formik } from 'formik';
import Link from 'next/link';
import React from 'react';
import { defineMessages, useIntl } from 'react-intl';
import { useToasts } from 'react-toast-notifications';
@ -18,6 +17,7 @@ import { Permission, useUser } from '../../../hooks/useUser';
import globalMessages from '../../../i18n/globalMessages';
import Button from '../../Common/Button';
import Modal from '../../Common/Modal';
import Link from '../../Link';
import { issueOptions } from '../constants';
const messages = defineMessages({

@ -6,12 +6,13 @@ import {
UsersIcon,
XIcon,
} from '@heroicons/react/outline';
import Link from 'next/link';
import { useRouter } from 'next/router';
import React, { ReactNode, useRef } from 'react';
import { defineMessages, useIntl } from 'react-intl';
import useClickOutside from '../../../hooks/useClickOutside';
import { useRouter } from '../../../hooks/useRouter';
import { Permission, useUser } from '../../../hooks/useUser';
import addBasePath from '../../../utils/addBasePath';
import Link from '../../Link';
import Transition from '../../Transition';
import VersionStatus from '../VersionStatus';
@ -130,8 +131,8 @@ const Sidebar: React.FC<SidebarProps> = ({ open, setClosed }) => {
>
<div className="flex items-center flex-shrink-0 px-2">
<span className="px-4 text-xl text-gray-50">
<a href="/">
<img src="/logo_full.svg" alt="Logo" />
<a href={addBasePath('/')}>
<img src={addBasePath('/logo_full.svg')} alt="Logo" />
</a>
</span>
</div>
@ -199,8 +200,8 @@ const Sidebar: React.FC<SidebarProps> = ({ open, setClosed }) => {
<div className="flex flex-col flex-1 pt-8 pb-4 overflow-y-auto">
<div className="flex items-center flex-shrink-0">
<span className="px-4 text-2xl text-gray-50">
<a href="/">
<img src="/logo_full.svg" alt="Logo" />
<a href={addBasePath('/')}>
<img src={addBasePath('/logo_full.svg')} alt="Logo" />
</a>
</span>
</div>

@ -1,11 +1,11 @@
import { LogoutIcon } from '@heroicons/react/outline';
import { CogIcon, UserIcon } from '@heroicons/react/solid';
import axios from 'axios';
import Link from 'next/link';
import React, { useRef, useState } from 'react';
import { defineMessages, useIntl } from 'react-intl';
import useClickOutside from '../../../hooks/useClickOutside';
import { useUser } from '../../../hooks/useUser';
import Link from '../../Link';
import Transition from '../../Transition';
const messages = defineMessages({

@ -4,11 +4,11 @@ import {
CodeIcon,
ServerIcon,
} from '@heroicons/react/outline';
import Link from 'next/link';
import React from 'react';
import { defineMessages, useIntl } from 'react-intl';
import useSWR from 'swr';
import { StatusResponse } from '../../../../server/interfaces/api/settingsInterfaces';
import Link from '../../Link';
const messages = defineMessages({
streamdevelop: 'Overseerr Develop',

@ -1,9 +1,9 @@
import { MenuAlt2Icon } from '@heroicons/react/outline';
import { ArrowLeftIcon } from '@heroicons/react/solid';
import { useRouter } from 'next/router';
import React, { useEffect, useState } from 'react';
import { AvailableLocale } from '../../context/LanguageContext';
import useLocale from '../../hooks/useLocale';
import { useRouter } from '../../hooks/useRouter';
import useSettings from '../../hooks/useSettings';
import { useUser } from '../../hooks/useUser';
import SearchInput from './SearchInput';

@ -0,0 +1,12 @@
import NextLink, { LinkProps } from 'next/link';
import React from 'react';
import addBasePath from '../../utils/addBasePath';
const Link: React.FC<LinkProps> = ({ href, ...props }) => {
if (props.as) {
props.as = addBasePath(props.as);
}
return <NextLink href={addBasePath(href)} {...props} />;
};
export default Link;

@ -1,7 +1,7 @@
import { NProgress } from '@tanem/react-nprogress';
import React, { useEffect, useState } from 'react';
import ReactDOM from 'react-dom';
import { useRouter } from 'next/router';
import { useRouter } from '../../hooks/useRouter';
interface BarProps {
progress: number;

@ -1,13 +1,13 @@
import { LoginIcon, SupportIcon } from '@heroicons/react/outline';
import axios from 'axios';
import { Field, Form, Formik } from 'formik';
import Link from 'next/link';
import React, { useState } from 'react';
import { defineMessages, useIntl } from 'react-intl';
import * as Yup from 'yup';
import useSettings from '../../hooks/useSettings';
import Button from '../Common/Button';
import SensitiveInput from '../Common/SensitiveInput';
import Link from '../Link';
const messages = defineMessages({
email: 'Email Address',

@ -1,11 +1,12 @@
import { XCircleIcon } from '@heroicons/react/solid';
import axios from 'axios';
import { useRouter } from 'next/dist/client/router';
import React, { useEffect, useState } from 'react';
import { defineMessages, useIntl } from 'react-intl';
import useSWR from 'swr';
import { useRouter } from '../../hooks/useRouter';
import useSettings from '../../hooks/useSettings';
import { useUser } from '../../hooks/useUser';
import addBasePath from '../../utils/addBasePath';
import Accordion from '../Common/Accordion';
import ImageFader from '../Common/ImageFader';
import PageTitle from '../Common/PageTitle';
@ -81,7 +82,11 @@ const Login: React.FC = () => {
<LanguagePicker />
</div>
<div className="relative z-40 flex flex-col items-center px-4 mt-10 sm:mx-auto sm:w-full sm:max-w-md">
<img src="/logo_stacked.svg" className="max-w-full mb-10" alt="Logo" />
<img
src={addBasePath('/logo_stacked.svg')}
className="max-w-full mb-10"
alt="Logo"
/>
<h2 className="mt-2 text-3xl font-extrabold leading-9 text-center text-gray-100">
{intl.formatMessage(messages.signinheader)}
</h2>

@ -1,7 +1,7 @@
import { ArrowCircleRightIcon } from '@heroicons/react/solid';
import Link from 'next/link';
import React, { useState } from 'react';
import { defineMessages, useIntl } from 'react-intl';
import Link from '../../Link';
const messages = defineMessages({
seemore: 'See More',

@ -1,5 +1,4 @@
import { ArrowCircleRightIcon } from '@heroicons/react/outline';
import Link from 'next/link';
import React, { useEffect } from 'react';
import { useSWRInfinite } from 'swr';
import { MediaStatus } from '../../../server/constants/media';
@ -9,6 +8,7 @@ import type {
TvResult,
} from '../../../server/models/Search';
import useSettings from '../../hooks/useSettings';
import Link from '../Link';
import PersonCard from '../PersonCard';
import Slider from '../Slider';
import TitleCard from '../TitleCard';

@ -1,13 +1,13 @@
import Link from 'next/link';
import { useRouter } from 'next/router';
import React from 'react';
import { defineMessages, useIntl } from 'react-intl';
import useSWR from 'swr';
import { MovieDetails } from '../../../../server/models/Movie';
import { useRouter } from '../../../hooks/useRouter';
import Error from '../../../pages/_error';
import Header from '../../Common/Header';
import LoadingSpinner from '../../Common/LoadingSpinner';
import PageTitle from '../../Common/PageTitle';
import Link from '../../Link';
import PersonCard from '../../PersonCard';
const messages = defineMessages({

@ -1,13 +1,13 @@
import Link from 'next/link';
import { useRouter } from 'next/router';
import React from 'react';
import { defineMessages, useIntl } from 'react-intl';
import useSWR from 'swr';
import { MovieDetails } from '../../../../server/models/Movie';
import { useRouter } from '../../../hooks/useRouter';
import Error from '../../../pages/_error';
import Header from '../../Common/Header';
import LoadingSpinner from '../../Common/LoadingSpinner';
import PageTitle from '../../Common/PageTitle';
import Link from '../../Link';
import PersonCard from '../../PersonCard';
const messages = defineMessages({

@ -1,4 +1,3 @@
import Link from 'next/link';
import { useRouter } from 'next/router';
import React from 'react';
import { defineMessages, useIntl } from 'react-intl';
@ -10,6 +9,7 @@ import Error from '../../pages/_error';
import Header from '../Common/Header';
import ListView from '../Common/ListView';
import PageTitle from '../Common/PageTitle';
import Link from '../Link';
const messages = defineMessages({
recommendations: 'Recommendations',

@ -1,15 +1,15 @@
import Link from 'next/link';
import { useRouter } from 'next/router';
import React from 'react';
import { defineMessages, useIntl } from 'react-intl';
import useSWR from 'swr';
import type { MovieDetails } from '../../../server/models/Movie';
import type { MovieResult } from '../../../server/models/Search';
import useDiscover from '../../hooks/useDiscover';
import { useRouter } from '../../hooks/useRouter';
import Error from '../../pages/_error';
import Header from '../Common/Header';
import ListView from '../Common/ListView';
import PageTitle from '../Common/PageTitle';
import Link from '../Link';
const messages = defineMessages({
similar: 'Similar Titles',

@ -14,8 +14,6 @@ import {
import { hasFlag } from 'country-flag-icons';
import 'country-flag-icons/3x2/flags.css';
import { uniqBy } from 'lodash';
import Link from 'next/link';
import { useRouter } from 'next/router';
import React, { useMemo, useState } from 'react';
import { defineMessages, useIntl } from 'react-intl';
import useSWR from 'swr';
@ -29,6 +27,7 @@ import RTFresh from '../../assets/rt_fresh.svg';
import RTRotten from '../../assets/rt_rotten.svg';
import TmdbLogo from '../../assets/tmdb_logo.svg';
import useLocale from '../../hooks/useLocale';
import { useRouter } from '../../hooks/useRouter';
import useSettings from '../../hooks/useSettings';
import { Permission, useUser } from '../../hooks/useUser';
import globalMessages from '../../i18n/globalMessages';
@ -41,6 +40,7 @@ import PageTitle from '../Common/PageTitle';
import PlayButton, { PlayButtonLink } from '../Common/PlayButton';
import ExternalLinkBlock from '../ExternalLinkBlock';
import IssueModal from '../IssueModal';
import Link from '../Link';
import ManageSlideOver from '../ManageSlideOver';
import MediaSlider from '../MediaSlider';
import PersonCard from '../PersonCard';

@ -1,4 +1,5 @@
import React from 'react';
import addBasePath from '../../utils/addBasePath';
interface PWAHeaderProps {
applicationTitle?: string;
@ -10,148 +11,148 @@ const PWAHeader: React.FC<PWAHeaderProps> = ({ applicationTitle }) => {
<link
rel="apple-touch-icon"
sizes="180x180"
href="/apple-touch-icon.png"
href={addBasePath('/apple-touch-icon.png')}
/>
<link
rel="icon"
type="image/png"
sizes="32x32"
href="/favicon-32x32.png"
href={addBasePath('/favicon-32x32.png')}
/>
<link
rel="icon"
type="image/png"
sizes="16x16"
href="/favicon-16x16.png"
href={addBasePath('/favicon-16x16.png')}
/>
<link
rel="apple-touch-startup-image"
href="/apple-splash-2048-2732.jpg"
href={addBasePath('/apple-splash-2048-2732.jpg')}
media="(device-width: 1024px) and (device-height: 1366px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)"
/>
<link
rel="apple-touch-startup-image"
href="/apple-splash-2732-2048.jpg"
href={addBasePath('/apple-splash-2732-2048.jpg')}
media="(device-width: 1024px) and (device-height: 1366px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)"
/>
<link
rel="apple-touch-startup-image"
href="/apple-splash-1668-2388.jpg"
href={addBasePath('/apple-splash-1668-2388.jpg')}
media="(device-width: 834px) and (device-height: 1194px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)"
/>
<link
rel="apple-touch-startup-image"
href="/apple-splash-2388-1668.jpg"
href={addBasePath('/apple-splash-2388-1668.jpg')}
media="(device-width: 834px) and (device-height: 1194px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)"
/>
<link
rel="apple-touch-startup-image"
href="/apple-splash-1536-2048.jpg"
href={addBasePath('/apple-splash-1536-2048.jpg')}
media="(device-width: 768px) and (device-height: 1024px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)"
/>
<link
rel="apple-touch-startup-image"
href="/apple-splash-2048-1536.jpg"
href={addBasePath('/apple-splash-2048-1536.jpg')}
media="(device-width: 768px) and (device-height: 1024px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)"
/>
<link
rel="apple-touch-startup-image"
href="/apple-splash-1668-2224.jpg"
href={addBasePath('/apple-splash-1668-2224.jpg')}
media="(device-width: 834px) and (device-height: 1112px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)"
/>
<link
rel="apple-touch-startup-image"
href="/apple-splash-2224-1668.jpg"
href={addBasePath('/apple-splash-2224-1668.jpg')}
media="(device-width: 834px) and (device-height: 1112px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)"
/>
<link
rel="apple-touch-startup-image"
href="/apple-splash-1620-2160.jpg"
href={addBasePath('/apple-splash-1620-2160.jpg')}
media="(device-width: 810px) and (device-height: 1080px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)"
/>
<link
rel="apple-touch-startup-image"
href="/apple-splash-2160-1620.jpg"
href={addBasePath('/apple-splash-2160-1620.jpg')}
media="(device-width: 810px) and (device-height: 1080px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)"
/>
<link
rel="apple-touch-startup-image"
href="/apple-splash-1284-2778.jpg"
href={addBasePath('/apple-splash-1284-2778.jpg')}
media="(device-width: 428px) and (device-height: 926px) and (-webkit-device-pixel-ratio: 3) and (orientation: portrait)"
/>
<link
rel="apple-touch-startup-image"
href="/apple-splash-2778-1284.jpg"
href={addBasePath('/apple-splash-2778-1284.jpg')}
media="(device-width: 428px) and (device-height: 926px) and (-webkit-device-pixel-ratio: 3) and (orientation: landscape)"
/>
<link
rel="apple-touch-startup-image"
href="/apple-splash-1170-2532.jpg"
href={addBasePath('/apple-splash-1170-2532.jpg')}
media="(device-width: 390px) and (device-height: 844px) and (-webkit-device-pixel-ratio: 3) and (orientation: portrait)"
/>
<link
rel="apple-touch-startup-image"
href="/apple-splash-2532-1170.jpg"
href={addBasePath('/apple-splash-2532-1170.jpg')}
media="(device-width: 390px) and (device-height: 844px) and (-webkit-device-pixel-ratio: 3) and (orientation: landscape)"
/>
<link
rel="apple-touch-startup-image"
href="/apple-splash-1125-2436.jpg"
href={addBasePath('/apple-splash-1125-2436.jpg')}
media="(device-width: 375px) and (device-height: 812px) and (-webkit-device-pixel-ratio: 3) and (orientation: portrait)"
/>
<link
rel="apple-touch-startup-image"
href="/apple-splash-2436-1125.jpg"
href={addBasePath('/apple-splash-2436-1125.jpg')}
media="(device-width: 375px) and (device-height: 812px) and (-webkit-device-pixel-ratio: 3) and (orientation: landscape)"
/>
<link
rel="apple-touch-startup-image"
href="/apple-splash-1242-2688.jpg"
href={addBasePath('/apple-splash-1242-2688.jpg')}
media="(device-width: 414px) and (device-height: 896px) and (-webkit-device-pixel-ratio: 3) and (orientation: portrait)"
/>
<link
rel="apple-touch-startup-image"
href="/apple-splash-2688-1242.jpg"
href={addBasePath('/apple-splash-2688-1242.jpg')}
media="(device-width: 414px) and (device-height: 896px) and (-webkit-device-pixel-ratio: 3) and (orientation: landscape)"
/>
<link
rel="apple-touch-startup-image"
href="/apple-splash-828-1792.jpg"
href={addBasePath('/apple-splash-828-1792.jpg')}
media="(device-width: 414px) and (device-height: 896px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)"
/>
<link
rel="apple-touch-startup-image"
href="/apple-splash-1792-828.jpg"
href={addBasePath('/apple-splash-1792-828.jpg')}
media="(device-width: 414px) and (device-height: 896px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)"
/>
<link
rel="apple-touch-startup-image"
href="/apple-splash-1242-2208.jpg"
href={addBasePath('/apple-splash-1242-2208.jpg')}
media="(device-width: 414px) and (device-height: 736px) and (-webkit-device-pixel-ratio: 3) and (orientation: portrait)"
/>
<link
rel="apple-touch-startup-image"
href="/apple-splash-2208-1242.jpg"
href={addBasePath('/apple-splash-2208-1242.jpg')}
media="(device-width: 414px) and (device-height: 736px) and (-webkit-device-pixel-ratio: 3) and (orientation: landscape)"
/>
<link
rel="apple-touch-startup-image"
href="/apple-splash-750-1334.jpg"
href={addBasePath('/apple-splash-750-1334.jpg')}
media="(device-width: 375px) and (device-height: 667px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)"
/>
<link
rel="apple-touch-startup-image"
href="/apple-splash-1334-750.jpg"
href={addBasePath('/apple-splash-1334-750.jpg')}
media="(device-width: 375px) and (device-height: 667px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)"
/>
<link
rel="apple-touch-startup-image"
href="/apple-splash-640-1136.jpg"
href={addBasePath('/apple-splash-640-1136.jpg')}
media="(device-width: 320px) and (device-height: 568px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)"
/>
<link
rel="apple-touch-startup-image"
href="/apple-splash-1136-640.jpg"
href={addBasePath('/apple-splash-1136-640.jpg')}
media="(device-width: 320px) and (device-height: 568px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)"
/>
<meta name="apple-mobile-web-app-capable" content="yes" />
@ -161,7 +162,7 @@ const PWAHeader: React.FC<PWAHeaderProps> = ({ applicationTitle }) => {
/>
<link
rel="manifest"
href="/site.webmanifest"
href={addBasePath('/site.webmanifest')}
crossOrigin="use-credentials"
/>
<meta name="application-name" content={applicationTitle ?? 'Overseerr'} />

@ -1,7 +1,7 @@
import { UserCircleIcon } from '@heroicons/react/solid';
import Link from 'next/link';
import React, { useState } from 'react';
import CachedImage from '../Common/CachedImage';
import Link from '../Link';
interface PersonCardProps {
personId: number;

@ -1,5 +1,4 @@
import { groupBy } from 'lodash';
import { useRouter } from 'next/router';
import React, { useMemo, useState } from 'react';
import { defineMessages, useIntl } from 'react-intl';
import TruncateMarkup from 'react-truncate-markup';
@ -7,6 +6,7 @@ import useSWR from 'swr';
import type { PersonCombinedCreditsResponse } from '../../../server/interfaces/api/personInterfaces';
import type { PersonDetail } from '../../../server/models/Person';
import Ellipsis from '../../assets/ellipsis.svg';
import { useRouter } from '../../hooks/useRouter';
import globalMessages from '../../i18n/globalMessages';
import Error from '../../pages/_error';
import CachedImage from '../Common/CachedImage';

@ -8,7 +8,6 @@ import {
XIcon,
} from '@heroicons/react/solid';
import axios from 'axios';
import Link from 'next/link';
import React, { useState } from 'react';
import { defineMessages, useIntl } from 'react-intl';
import { MediaRequestStatus } from '../../../server/constants/media';
@ -17,6 +16,7 @@ import useRequestOverride from '../../hooks/useRequestOverride';
import globalMessages from '../../i18n/globalMessages';
import Badge from '../Common/Badge';
import Button from '../Common/Button';
import Link from '../Link';
import RequestModal from '../RequestModal';
const messages = defineMessages({

@ -6,7 +6,6 @@ import {
XIcon,
} from '@heroicons/react/solid';
import axios from 'axios';
import Link from 'next/link';
import React, { useEffect, useState } from 'react';
import { useInView } from 'react-intersection-observer';
import { defineMessages, useIntl } from 'react-intl';
@ -25,6 +24,7 @@ import { withProperties } from '../../utils/typeHelpers';
import Badge from '../Common/Badge';
import Button from '../Common/Button';
import CachedImage from '../Common/CachedImage';
import Link from '../Link';
import RequestModal from '../RequestModal';
import StatusBadge from '../StatusBadge';

@ -6,7 +6,6 @@ import {
XIcon,
} from '@heroicons/react/solid';
import axios from 'axios';
import Link from 'next/link';
import React, { useState } from 'react';
import { useInView } from 'react-intersection-observer';
import { defineMessages, FormattedRelativeTime, useIntl } from 'react-intl';
@ -25,6 +24,7 @@ import Badge from '../../Common/Badge';
import Button from '../../Common/Button';
import CachedImage from '../../Common/CachedImage';
import ConfirmButton from '../../Common/ConfirmButton';
import Link from '../../Link';
import RequestModal from '../../RequestModal';
import StatusBadge from '../../StatusBadge';

@ -4,12 +4,11 @@ import {
FilterIcon,
SortDescendingIcon,
} from '@heroicons/react/solid';
import Link from 'next/link';
import { useRouter } from 'next/router';
import React, { useEffect, useState } from 'react';
import { defineMessages, useIntl } from 'react-intl';
import useSWR from 'swr';
import type { RequestResultsResponse } from '../../../server/interfaces/api/requestInterfaces';
import { useRouter } from '../../hooks/useRouter';
import { useUpdateQueryParams } from '../../hooks/useUpdateQueryParams';
import { useUser } from '../../hooks/useUser';
import globalMessages from '../../i18n/globalMessages';
@ -17,6 +16,7 @@ import Button from '../Common/Button';
import Header from '../Common/Header';
import LoadingSpinner from '../Common/LoadingSpinner';
import PageTitle from '../Common/PageTitle';
import Link from '../Link';
import RequestItem from './RequestItem';
const messages = defineMessages({

@ -1,9 +1,9 @@
import { ChevronDownIcon, ChevronUpIcon } from '@heroicons/react/solid';
import Link from 'next/link';
import React, { useState } from 'react';
import { defineMessages, useIntl } from 'react-intl';
import { QuotaStatus } from '../../../../server/interfaces/api/userInterfaces';
import ProgressCircle from '../../Common/ProgressCircle';
import Link from '../../Link';
const messages = defineMessages({
requestsremaining:

@ -5,6 +5,7 @@ import useSWR from 'swr';
import { SonarrSeries } from '../../../../server/api/servarr/sonarr';
import globalMessages from '../../../i18n/globalMessages';
import Alert from '../../Common/Alert';
import CachedImage from '../../Common/CachedImage';
import { SmallLoadingSpinner } from '../../Common/LoadingSpinner';
import Modal from '../../Common/Modal';
@ -72,7 +73,7 @@ const SearchByNameModal: React.FC<SearchByNameModalProps> = ({
} `}
>
<div className="flex items-center flex-none w-24 space-x-4">
<img
<CachedImage
src={
item.remotePoster ??
'/images/overseerr_poster_not_found.png'

@ -1,14 +1,15 @@
import { ArrowLeftIcon, MailIcon } from '@heroicons/react/solid';
import axios from 'axios';
import { Field, Form, Formik } from 'formik';
import Link from 'next/link';
import React, { useState } from 'react';
import { defineMessages, useIntl } from 'react-intl';
import * as Yup from 'yup';
import addBasePath from '../../utils/addBasePath';
import Button from '../Common/Button';
import ImageFader from '../Common/ImageFader';
import PageTitle from '../Common/PageTitle';
import LanguagePicker from '../Layout/LanguagePicker';
import Link from '../Link';
const messages = defineMessages({
passwordreset: 'Password Reset',
@ -49,7 +50,11 @@ const ResetPassword: React.FC = () => {
<LanguagePicker />
</div>
<div className="relative z-40 flex flex-col items-center px-4 mt-10 sm:mx-auto sm:w-full sm:max-w-md">
<img src="/logo_stacked.svg" className="max-w-full mb-10" alt="Logo" />
<img
src={addBasePath('/logo_stacked.svg')}
className="max-w-full mb-10"
alt="Logo"
/>
<h2 className="mt-2 text-3xl font-extrabold leading-9 text-center text-gray-100">
{intl.formatMessage(messages.resetpassword)}
</h2>

@ -1,16 +1,17 @@
import { SupportIcon } from '@heroicons/react/outline';
import axios from 'axios';
import { Form, Formik } from 'formik';
import Link from 'next/link';
import { useRouter } from 'next/router';
import React, { useState } from 'react';
import { defineMessages, useIntl } from 'react-intl';
import * as Yup from 'yup';
import { useRouter } from '../../hooks/useRouter';
import globalMessages from '../../i18n/globalMessages';
import addBasePath from '../../utils/addBasePath';
import Button from '../Common/Button';
import ImageFader from '../Common/ImageFader';
import SensitiveInput from '../Common/SensitiveInput';
import LanguagePicker from '../Layout/LanguagePicker';
import Link from '../Link';
const messages = defineMessages({
passwordreset: 'Password Reset',
@ -64,7 +65,11 @@ const ResetPassword: React.FC = () => {
<LanguagePicker />
</div>
<div className="relative z-40 flex flex-col items-center px-4 mt-10 sm:mx-auto sm:w-full sm:max-w-md">
<img src="/logo_stacked.svg" className="max-w-full mb-10" alt="Logo" />
<img
src={addBasePath('/logo_stacked.svg')}
className="max-w-full mb-10"
alt="Logo"
/>
<h2 className="mt-2 text-3xl font-extrabold leading-9 text-center text-gray-100">
{intl.formatMessage(messages.resetpassword)}
</h2>

@ -1,10 +1,10 @@
import React from 'react';
import { useRouter } from 'next/router';
import {
TvResult,
MovieResult,
PersonResult,
} from '../../../server/models/Search';
import { useRouter } from '../../hooks/useRouter';
import ListView from '../Common/ListView';
import { defineMessages, useIntl } from 'react-intl';
import Header from '../Common/Header';

@ -3,6 +3,7 @@ import axios from 'axios';
import React, { useEffect } from 'react';
import useSettings from '../../hooks/useSettings';
import { useUser } from '../../hooks/useUser';
import addBasePath from '../../utils/addBasePath';
const ServiceWorkerSetup: React.FC = () => {
const { currentSettings } = useSettings();
@ -10,7 +11,7 @@ const ServiceWorkerSetup: React.FC = () => {
useEffect(() => {
if ('serviceWorker' in navigator && user?.id) {
navigator.serviceWorker
.register('/sw.js')
.register(addBasePath('/sw.js'))
.then(async (registration) => {
console.log(
'[SW] Registration successful, scope is:',

@ -3,7 +3,6 @@ import { QuestionMarkCircleIcon, RefreshIcon } from '@heroicons/react/solid';
import axios from 'axios';
import { Field, Form, Formik } from 'formik';
import dynamic from 'next/dynamic';
import Link from 'next/link';
import React, { useState } from 'react';
import { defineMessages, useIntl } from 'react-intl';
import { useToasts } from 'react-toast-notifications';
@ -12,6 +11,7 @@ import * as Yup from 'yup';
import globalMessages from '../../../../i18n/globalMessages';
import Button from '../../../Common/Button';
import LoadingSpinner from '../../../Common/LoadingSpinner';
import Link from '../../../Link';
import NotificationTypeSelector from '../../../NotificationTypeSelector';
const JSONEditor = dynamic(() => import('../../../JSONEditor'), { ssr: false });

@ -8,7 +8,6 @@ import {
PlayIcon,
} from '@heroicons/react/solid';
import copy from 'copy-to-clipboard';
import { useRouter } from 'next/router';
import React, { useEffect, useState } from 'react';
import { defineMessages, useIntl } from 'react-intl';
import { useToasts } from 'react-toast-notifications';
@ -17,6 +16,7 @@ import {
LogMessage,
LogsResultsResponse,
} from '../../../../server/interfaces/api/settingsInterfaces';
import { useRouter } from '../../../hooks/useRouter';
import { useUpdateQueryParams } from '../../../hooks/useUpdateQueryParams';
import globalMessages from '../../../i18n/globalMessages';
import Error from '../../../pages/_error';

@ -33,6 +33,9 @@ const messages = defineMessages({
apikey: 'API Key',
applicationTitle: 'Application Title',
applicationurl: 'Application URL',
basePath: 'Base Path',
basePathTip:
'Configure Overseerr to run under a sub-path of a domain (Overseerr must be restarted for changes to take effect)',
region: 'Discover Region',
regionTip: 'Filter content by regional availability',
originallanguage: 'Discover Language',
@ -44,7 +47,7 @@ const messages = defineMessages({
hideAvailable: 'Hide Available Media',
csrfProtection: 'Enable CSRF Protection',
csrfProtectionTip:
'Set external API access to read-only (requires HTTPS, and Overseerr must be reloaded for changes to take effect)',
'Set external API access to read-only (requires HTTPS, and Overseerr must be restarted for changes to take effect)',
csrfProtectionHoverTip:
'Do NOT enable this setting unless you understand what you are doing!',
cacheImages: 'Enable Image Caching',
@ -52,10 +55,12 @@ const messages = defineMessages({
'Optimize and store all images locally (consumes a significant amount of disk space)',
trustProxy: 'Enable Proxy Support',
trustProxyTip:
'Allow Overseerr to correctly register client IP addresses behind a proxy (Overseerr must be reloaded for changes to take effect)',
'Allow Overseerr to correctly register client IP addresses behind a proxy (Overseerr must be restarted for changes to take effect)',
validationApplicationTitle: 'You must provide an application title',
validationApplicationUrl: 'You must provide a valid URL',
validationApplicationUrlTrailingSlash: 'URL must not end in a trailing slash',
validationBasePathLeadingSlash: 'Path must begin with a leading slash',
validationBasePathTrailingSlash: 'Path must not end in a trailing slash',
partialRequestsEnabled: 'Allow Partial Series Requests',
locale: 'Display Language',
});
@ -81,12 +86,18 @@ const SettingsMain: React.FC = () => {
.test(
'no-trailing-slash',
intl.formatMessage(messages.validationApplicationUrlTrailingSlash),
(value) => {
if (value?.substr(value.length - 1) === '/') {
return false;
}
return true;
}
(value) => !value || !value.endsWith('/')
),
basePath: Yup.string()
.test(
'leading-slash',
intl.formatMessage(messages.validationBasePathLeadingSlash),
(value) => !value || value.startsWith('/')
)
.test(
'no-trailing-slash',
intl.formatMessage(messages.validationBasePathTrailingSlash),
(value) => !value || !value.endsWith('/')
),
});
@ -132,6 +143,7 @@ const SettingsMain: React.FC = () => {
initialValues={{
applicationTitle: data?.applicationTitle,
applicationUrl: data?.applicationUrl,
basePath: data?.basePath,
csrfProtection: data?.csrfProtection,
hideAvailable: data?.hideAvailable,
locale: data?.locale ?? 'en',
@ -147,6 +159,7 @@ const SettingsMain: React.FC = () => {
await axios.post('/api/v1/settings/main', {
applicationTitle: values.applicationTitle,
applicationUrl: values.applicationUrl,
basePath: values.basePath,
csrfProtection: values.csrfProtection,
hideAvailable: values.hideAvailable,
locale: values.locale,
@ -255,6 +268,27 @@ const SettingsMain: React.FC = () => {
)}
</div>
</div>
<div className="form-row">
<label htmlFor="basePath" className="text-label">
<span>{intl.formatMessage(messages.basePath)}</span>
<span className="label-tip">
{intl.formatMessage(messages.basePathTip)}
</span>
</label>
<div className="form-input">
<div className="form-input-field">
<Field
id="basePath"
name="basePath"
type="text"
inputMode="url"
/>
</div>
{errors.basePath && touched.basePath && (
<div className="error">{errors.basePath}</div>
)}
</div>
</div>
<div className="form-row">
<label htmlFor="trustProxy" className="checkbox-label">
<span>{intl.formatMessage(messages.trustProxy)}</span>

@ -1,9 +1,10 @@
import axios from 'axios';
import { useRouter } from 'next/router';
import React, { useState } from 'react';
import { defineMessages, useIntl } from 'react-intl';
import useSWR, { mutate } from 'swr';
import useLocale from '../../hooks/useLocale';
import { useRouter } from '../../hooks/useRouter';
import addBasePath from '../../utils/addBasePath';
import AppDataWarning from '../AppDataWarning';
import Badge from '../Common/Badge';
import Button from '../Common/Button';
@ -72,7 +73,7 @@ const Setup: React.FC = () => {
</div>
<div className="relative z-40 px-4 sm:mx-auto sm:w-full sm:max-w-4xl">
<img
src="/logo_stacked.svg"
src={addBasePath('/logo_stacked.svg')}
className="max-w-full mb-10 sm:max-w-md sm:mx-auto"
alt="Logo"
/>

@ -1,6 +1,5 @@
import { DownloadIcon } from '@heroicons/react/outline';
import { BellIcon, CheckIcon, ClockIcon } from '@heroicons/react/solid';
import Link from 'next/link';
import React, { useCallback, useEffect, useState } from 'react';
import { useIntl } from 'react-intl';
import { MediaStatus } from '../../../server/constants/media';
@ -12,6 +11,7 @@ import globalMessages from '../../i18n/globalMessages';
import { withProperties } from '../../utils/typeHelpers';
import Button from '../Common/Button';
import CachedImage from '../Common/CachedImage';
import Link from '../Link';
import RequestModal from '../RequestModal';
import Transition from '../Transition';
import Placeholder from './Placeholder';

@ -1,13 +1,13 @@
import Link from 'next/link';
import { useRouter } from 'next/router';
import React from 'react';
import { defineMessages, useIntl } from 'react-intl';
import useSWR from 'swr';
import type { TvDetails } from '../../../../server/models/Tv';
import { useRouter } from '../../../hooks/useRouter';
import Error from '../../../pages/_error';
import Header from '../../Common/Header';
import LoadingSpinner from '../../Common/LoadingSpinner';
import PageTitle from '../../Common/PageTitle';
import Link from '../../Link';
import PersonCard from '../../PersonCard';
const messages = defineMessages({

@ -1,13 +1,13 @@
import Link from 'next/link';
import { useRouter } from 'next/router';
import React from 'react';
import { defineMessages, useIntl } from 'react-intl';
import useSWR from 'swr';
import type { TvDetails } from '../../../../server/models/Tv';
import { useRouter } from '../../../hooks/useRouter';
import Error from '../../../pages/_error';
import Header from '../../Common/Header';
import LoadingSpinner from '../../Common/LoadingSpinner';
import PageTitle from '../../Common/PageTitle';
import Link from '../../Link';
import PersonCard from '../../PersonCard';
const messages = defineMessages({

@ -1,15 +1,15 @@
import Link from 'next/link';
import { useRouter } from 'next/router';
import React from 'react';
import { defineMessages, useIntl } from 'react-intl';
import useSWR from 'swr';
import type { TvResult } from '../../../server/models/Search';
import { TvDetails } from '../../../server/models/Tv';
import useDiscover from '../../hooks/useDiscover';
import { useRouter } from '../../hooks/useRouter';
import Error from '../../pages/_error';
import Header from '../Common/Header';
import ListView from '../Common/ListView';
import PageTitle from '../Common/PageTitle';
import Link from '../Link';
const messages = defineMessages({
recommendations: 'Recommendations',

@ -1,15 +1,15 @@
import Link from 'next/link';
import { useRouter } from 'next/router';
import React from 'react';
import { defineMessages, useIntl } from 'react-intl';
import useSWR from 'swr';
import type { TvResult } from '../../../server/models/Search';
import type { TvDetails } from '../../../server/models/Tv';
import useDiscover from '../../hooks/useDiscover';
import { useRouter } from '../../hooks/useRouter';
import Error from '../../pages/_error';
import Header from '../Common/Header';
import ListView from '../Common/ListView';
import PageTitle from '../Common/PageTitle';
import Link from '../Link';
const messages = defineMessages({
similar: 'Similar Series',

@ -7,8 +7,6 @@ import {
} from '@heroicons/react/outline';
import { hasFlag } from 'country-flag-icons';
import 'country-flag-icons/3x2/flags.css';
import Link from 'next/link';
import { useRouter } from 'next/router';
import React, { useMemo, useState } from 'react';
import { defineMessages, useIntl } from 'react-intl';
import useSWR from 'swr';
@ -24,6 +22,7 @@ import RTFresh from '../../assets/rt_fresh.svg';
import RTRotten from '../../assets/rt_rotten.svg';
import TmdbLogo from '../../assets/tmdb_logo.svg';
import useLocale from '../../hooks/useLocale';
import { useRouter } from '../../hooks/useRouter';
import useSettings from '../../hooks/useSettings';
import { Permission, useUser } from '../../hooks/useUser';
import globalMessages from '../../i18n/globalMessages';
@ -36,6 +35,7 @@ import PageTitle from '../Common/PageTitle';
import PlayButton, { PlayButtonLink } from '../Common/PlayButton';
import ExternalLinkBlock from '../ExternalLinkBlock';
import IssueModal from '../IssueModal';
import Link from '../Link';
import ManageSlideOver from '../ManageSlideOver';
import MediaSlider from '../MediaSlider';
import PersonCard from '../PersonCard';

@ -9,8 +9,6 @@ import {
} from '@heroicons/react/solid';
import axios from 'axios';
import { Field, Form, Formik } from 'formik';
import Link from 'next/link';
import { useRouter } from 'next/router';
import React, { useEffect, useState } from 'react';
import { defineMessages, useIntl } from 'react-intl';
import { useToasts } from 'react-toast-notifications';
@ -18,6 +16,7 @@ import useSWR from 'swr';
import * as Yup from 'yup';
import type { UserResultsResponse } from '../../../server/interfaces/api/userInterfaces';
import { hasPermission } from '../../../server/lib/permissions';
import { useRouter } from '../../hooks/useRouter';
import useSettings from '../../hooks/useSettings';
import { useUpdateQueryParams } from '../../hooks/useUpdateQueryParams';
import { Permission, User, UserType, useUser } from '../../hooks/useUser';
@ -31,6 +30,7 @@ import Modal from '../Common/Modal';
import PageTitle from '../Common/PageTitle';
import SensitiveInput from '../Common/SensitiveInput';
import Table from '../Common/Table';
import Link from '../Link';
import Transition from '../Transition';
import BulkEditModal from './BulkEditModal';

@ -1,9 +1,9 @@
import { CogIcon, UserIcon } from '@heroicons/react/solid';
import Link from 'next/link';
import React from 'react';
import { defineMessages, useIntl } from 'react-intl';
import { Permission, User, useUser } from '../../../hooks/useUser';
import Button from '../../Common/Button';
import Link from '../../Link';
const messages = defineMessages({
settings: 'Edit Settings',

@ -1,7 +1,6 @@
import { SaveIcon } from '@heroicons/react/outline';
import axios from 'axios';
import { Field, Form, Formik } from 'formik';
import { useRouter } from 'next/router';
import React, { useEffect, useState } from 'react';
import { defineMessages, useIntl } from 'react-intl';
import { useToasts } from 'react-toast-notifications';
@ -12,6 +11,7 @@ import {
AvailableLocale,
} from '../../../../context/LanguageContext';
import useLocale from '../../../../hooks/useLocale';
import { useRouter } from '../../../../hooks/useRouter';
import useSettings from '../../../../hooks/useSettings';
import { Permission, UserType, useUser } from '../../../../hooks/useUser';
import globalMessages from '../../../../i18n/globalMessages';

@ -1,13 +1,13 @@
import { SaveIcon } from '@heroicons/react/outline';
import axios from 'axios';
import { Field, Form, Formik } from 'formik';
import { useRouter } from 'next/router';
import React from 'react';
import { defineMessages, useIntl } from 'react-intl';
import { useToasts } from 'react-toast-notifications';
import useSWR from 'swr';
import * as Yup from 'yup';
import { UserSettingsNotificationsResponse } from '../../../../../server/interfaces/api/userSettingsInterfaces';
import { useRouter } from '../../../../hooks/useRouter';
import { useUser } from '../../../../hooks/useUser';
import globalMessages from '../../../../i18n/globalMessages';
import Button from '../../../Common/Button';

@ -1,13 +1,13 @@
import { SaveIcon } from '@heroicons/react/outline';
import axios from 'axios';
import { Form, Formik } from 'formik';
import { useRouter } from 'next/router';
import React from 'react';
import { defineMessages, useIntl } from 'react-intl';
import { useToasts } from 'react-toast-notifications';
import useSWR from 'swr';
import * as Yup from 'yup';
import { UserSettingsNotificationsResponse } from '../../../../../server/interfaces/api/userSettingsInterfaces';
import { useRouter } from '../../../../hooks/useRouter';
import { useUser } from '../../../../hooks/useUser';
import globalMessages from '../../../../i18n/globalMessages';
import Badge from '../../../Common/Badge';

@ -1,12 +1,12 @@
import axios from 'axios';
import { Form, Formik } from 'formik';
import { useRouter } from 'next/router';
import React from 'react';
import { defineMessages, useIntl } from 'react-intl';
import { useToasts } from 'react-toast-notifications';
import useSWR from 'swr';
import * as Yup from 'yup';
import { UserSettingsNotificationsResponse } from '../../../../../server/interfaces/api/userSettingsInterfaces';
import { useRouter } from '../../../../hooks/useRouter';
import { useUser } from '../../../../hooks/useUser';
import globalMessages from '../../../../i18n/globalMessages';
import Button from '../../../Common/Button';

@ -1,12 +1,12 @@
import axios from 'axios';
import { Field, Form, Formik } from 'formik';
import { useRouter } from 'next/router';
import React from 'react';
import { defineMessages, useIntl } from 'react-intl';
import { useToasts } from 'react-toast-notifications';
import useSWR from 'swr';
import * as Yup from 'yup';
import { UserSettingsNotificationsResponse } from '../../../../../server/interfaces/api/userSettingsInterfaces';
import { useRouter } from '../../../../hooks/useRouter';
import useSettings from '../../../../hooks/useSettings';
import { useUser } from '../../../../hooks/useUser';
import globalMessages from '../../../../i18n/globalMessages';

@ -1,13 +1,13 @@
import { SaveIcon } from '@heroicons/react/outline';
import axios from 'axios';
import { Field, Form, Formik } from 'formik';
import { useRouter } from 'next/router';
import React from 'react';
import { defineMessages, useIntl } from 'react-intl';
import { useToasts } from 'react-toast-notifications';
import useSWR from 'swr';
import * as Yup from 'yup';
import { UserSettingsNotificationsResponse } from '../../../../../server/interfaces/api/userSettingsInterfaces';
import { useRouter } from '../../../../hooks/useRouter';
import { useUser } from '../../../../hooks/useUser';
import globalMessages from '../../../../i18n/globalMessages';
import Button from '../../../Common/Button';

@ -1,12 +1,12 @@
import { SaveIcon } from '@heroicons/react/outline';
import axios from 'axios';
import { Form, Formik } from 'formik';
import { useRouter } from 'next/router';
import React from 'react';
import { defineMessages, useIntl } from 'react-intl';
import { useToasts } from 'react-toast-notifications';
import useSWR, { mutate } from 'swr';
import { UserSettingsNotificationsResponse } from '../../../../../server/interfaces/api/userSettingsInterfaces';
import { useRouter } from '../../../../hooks/useRouter';
import { useUser } from '../../../../hooks/useUser';
import globalMessages from '../../../../i18n/globalMessages';
import Button from '../../../Common/Button';

@ -1,5 +1,4 @@
import { CloudIcon, MailIcon } from '@heroicons/react/solid';
import { useRouter } from 'next/router';
import React from 'react';
import { defineMessages, useIntl } from 'react-intl';
import useSWR from 'swr';
@ -8,6 +7,7 @@ import DiscordLogo from '../../../../assets/extlogos/discord.svg';
import PushbulletLogo from '../../../../assets/extlogos/pushbullet.svg';
import PushoverLogo from '../../../../assets/extlogos/pushover.svg';
import TelegramLogo from '../../../../assets/extlogos/telegram.svg';
import { useRouter } from '../../../../hooks/useRouter';
import { useUser } from '../../../../hooks/useUser';
import globalMessages from '../../../../i18n/globalMessages';
import Error from '../../../../pages/_error';

@ -1,12 +1,12 @@
import { SaveIcon } from '@heroicons/react/outline';
import axios from 'axios';
import { Form, Formik } from 'formik';
import { useRouter } from 'next/router';
import React from 'react';
import { defineMessages, useIntl } from 'react-intl';
import { useToasts } from 'react-toast-notifications';
import useSWR from 'swr';
import * as Yup from 'yup';
import { useRouter } from '../../../../hooks/useRouter';
import { Permission, useUser } from '../../../../hooks/useUser';
import globalMessages from '../../../../i18n/globalMessages';
import Error from '../../../../pages/_error';

@ -1,11 +1,11 @@
import { SaveIcon } from '@heroicons/react/outline';
import axios from 'axios';
import { Form, Formik } from 'formik';
import { useRouter } from 'next/router';
import React from 'react';
import { defineMessages, useIntl } from 'react-intl';
import { useToasts } from 'react-toast-notifications';
import useSWR from 'swr';
import { useRouter } from '../../../../hooks/useRouter';
import { useUser } from '../../../../hooks/useUser';
import globalMessages from '../../../../i18n/globalMessages';
import Error from '../../../../pages/_error';

@ -1,9 +1,9 @@
import { useRouter } from 'next/router';
import React from 'react';
import { defineMessages, useIntl } from 'react-intl';
import useSWR from 'swr';
import { UserSettingsNotificationsResponse } from '../../../../server/interfaces/api/userSettingsInterfaces';
import { hasPermission, Permission } from '../../../../server/lib/permissions';
import { useRouter } from '../../../hooks/useRouter';
import useSettings from '../../../hooks/useSettings';
import { useUser } from '../../../hooks/useUser';
import globalMessages from '../../../i18n/globalMessages';

@ -1,6 +1,4 @@
import { ArrowCircleRightIcon } from '@heroicons/react/outline';
import Link from 'next/link';
import { useRouter } from 'next/router';
import React, { useCallback, useEffect, useState } from 'react';
import { defineMessages, useIntl } from 'react-intl';
import useSWR from 'swr';
@ -10,12 +8,14 @@ import {
} from '../../../server/interfaces/api/userInterfaces';
import { MovieDetails } from '../../../server/models/Movie';
import { TvDetails } from '../../../server/models/Tv';
import { useRouter } from '../../hooks/useRouter';
import { Permission, useUser } from '../../hooks/useUser';
import Error from '../../pages/_error';
import ImageFader from '../Common/ImageFader';
import LoadingSpinner from '../Common/LoadingSpinner';
import PageTitle from '../Common/PageTitle';
import ProgressCircle from '../Common/ProgressCircle';
import Link from '../Link';
import RequestCard from '../RequestCard';
import Slider from '../Slider';
import ProfileHeader from './ProfileHeader';

@ -1,6 +1,7 @@
import React, { useEffect, useRef } from 'react';
import { useUser, User } from '../hooks/useUser';
import { useRouter } from 'next/dist/client/router';
import addBasePath from '../utils/addBasePath';
interface UserContextProps {
initialUser: User;
@ -30,7 +31,7 @@ export const UserContext: React.FC<UserContextProps> = ({
!routing.current
) {
routing.current = true;
location.href = '/login';
location.href = addBasePath('/login');
}
}, [router, user, error]);

@ -1,5 +1,5 @@
import { useRouter } from 'next/router';
import { useEffect } from 'react';
import { useRouter } from './useRouter';
import { Permission, PermissionCheckOptions, useUser } from './useUser';
const useRouteGuard = (

@ -0,0 +1,67 @@
import { NextRouter, useRouter as useNextRouter } from 'next/router';
import { useMemo } from 'react';
import addBasePath from '../utils/addBasePath';
const basePath = addBasePath('');
const urlPropertyFields = [
'pathname',
'events',
'route',
'query',
'asPath',
'components',
'isFallback',
'basePath',
'locale',
'locales',
'defaultLocale',
'isReady',
'isPreview',
'isLocaleDomain',
'domainLocales',
];
const coreMethodFields = ['reload', 'back', 'prefetch', 'beforePopState'];
const wrapRouter = (target: any): NextRouter => {
const router = {
push(url: URL, as?: URL, options?: never) {
return target.push(
addBasePath(url),
as ? addBasePath(as) : undefined,
options
);
},
replace(url: URL, as?: URL, options?: never) {
return target.push(
addBasePath(url),
as ? addBasePath(as) : undefined,
options
);
},
get basePath() {
return basePath;
},
};
urlPropertyFields.forEach((field: string) => {
Object.defineProperty(router, field, {
get() {
return target[field] as string;
},
});
});
coreMethodFields.forEach((field: string) => {
(router as any)[field] = (...args: any[]) => {
return target[field](...args);
};
});
return router as NextRouter;
};
export const useRouter = (): NextRouter => {
const router = useNextRouter();
return useMemo(() => wrapRouter(router), [router]);
};

@ -2,8 +2,8 @@
import type { UrlObject } from 'url';
import { useEffect, useState, Dispatch, SetStateAction } from 'react';
import useDebouncedState from './useDebouncedState';
import { useRouter } from 'next/router';
import type { Nullable } from '../utils/typeHelpers';
import { useRouter } from './useRouter';
type Url = string | UrlObject;

@ -1,6 +1,7 @@
import { NextRouter, useRouter } from 'next/router';
import { NextRouter } from 'next/router';
import { ParsedUrlQuery } from 'querystring';
import { useCallback } from 'react';
import { useRouter } from './useRouter';
type UseQueryParamReturnedFunction = (
query: ParsedUrlQuery,

@ -365,6 +365,7 @@
"components.ResetPassword.validationpasswordrequired": "You must provide a password",
"components.Search.search": "Search",
"components.Search.searchresults": "Search Results",
"components.Settings.Allow Overseerr to correctly register client IP addresses behind a proxy (Overseerr must be restarted for changes to take effect)": "Allow Overseerr to correctly register client IP addresses behind a proxy (Overseerr must be restarted for changes to take effect)",
"components.Settings.Notifications.NotificationsLunaSea.agentenabled": "Enable Agent",
"components.Settings.Notifications.NotificationsLunaSea.profileName": "Profile Name",
"components.Settings.Notifications.NotificationsLunaSea.profileNameTip": "Only required if not using the <code>default</code> profile",
@ -680,13 +681,15 @@
"components.Settings.apikey": "API Key",
"components.Settings.applicationTitle": "Application Title",
"components.Settings.applicationurl": "Application URL",
"components.Settings.basePath": "Base Path",
"components.Settings.basePathTip": "Configure Overseerr to run under a sub-path of a domain (Overseerr must be restarted for changes to take effect)",
"components.Settings.cacheImages": "Enable Image Caching",
"components.Settings.cacheImagesTip": "Optimize and store all images locally (consumes a significant amount of disk space)",
"components.Settings.cancelscan": "Cancel Scan",
"components.Settings.copied": "Copied API key to clipboard.",
"components.Settings.csrfProtection": "Enable CSRF Protection",
"components.Settings.csrfProtectionHoverTip": "Do NOT enable this setting unless you understand what you are doing!",
"components.Settings.csrfProtectionTip": "Set external API access to read-only (requires HTTPS, and Overseerr must be reloaded for changes to take effect)",
"components.Settings.csrfProtectionTip": "Set external API access to read-only (requires HTTPS, and Overseerr must be restarted for changes to take effect)",
"components.Settings.currentlibrary": "Current Library: {name}",
"components.Settings.default": "Default",
"components.Settings.default4k": "Default 4K",
@ -758,10 +761,11 @@
"components.Settings.toastSettingsFailure": "Something went wrong while saving settings.",
"components.Settings.toastSettingsSuccess": "Settings saved successfully!",
"components.Settings.trustProxy": "Enable Proxy Support",
"components.Settings.trustProxyTip": "Allow Overseerr to correctly register client IP addresses behind a proxy (Overseerr must be reloaded for changes to take effect)",
"components.Settings.validationApplicationTitle": "You must provide an application title",
"components.Settings.validationApplicationUrl": "You must provide a valid URL",
"components.Settings.validationApplicationUrlTrailingSlash": "URL must not end in a trailing slash",
"components.Settings.validationBasePathLeadingSlash": "Path must begin with a leading slash",
"components.Settings.validationBasePathTrailingSlash": "Path must not end in a trailing slash",
"components.Settings.validationHostnameRequired": "You must provide a valid hostname or IP address",
"components.Settings.validationPortRequired": "You must provide a valid port number",
"components.Settings.validationWebAppUrl": "You must provide a valid Plex Web App URL",

@ -1,8 +1,8 @@
import { ArrowCircleRightIcon } from '@heroicons/react/outline';
import Link from 'next/link';
import React from 'react';
import { defineMessages, useIntl } from 'react-intl';
import PageTitle from '../components/Common/PageTitle';
import Link from '../components/Link';
const messages = defineMessages({
errormessagewithcode: '{statusCode} - {error}',

@ -19,7 +19,9 @@ import { SettingsProvider } from '../context/SettingsContext';
import { UserContext } from '../context/UserContext';
import { User } from '../hooks/useUser';
import '../styles/globals.css';
import addBasePath from '../utils/addBasePath';
const basePath = addBasePath('');
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const loadLocaleData = (locale: AvailableLocale): Promise<any> => {
switch (locale) {
@ -85,6 +87,10 @@ if (typeof window === 'undefined') {
global.Intl = require('intl');
}
if (basePath) {
axios.defaults.baseURL = basePath;
}
const CoreApp: Omit<NextAppComponentType, 'origGetInitialProps'> = ({
Component,
pageProps,
@ -172,9 +178,10 @@ CoreApp.getInitialProps = async (initialProps) => {
};
if (ctx.res) {
const serverUrl = `http://localhost:${process.env.PORT || 5055}${basePath}`;
// Check if app is initialized and redirect if necessary
const response = await axios.get<PublicSettingsResponse>(
`http://localhost:${process.env.PORT || 5055}/api/v1/settings/public`
`${serverUrl}/api/v1/settings/public`
);
currentSettings = response.data;
@ -184,22 +191,21 @@ CoreApp.getInitialProps = async (initialProps) => {
if (!initialized) {
if (!router.pathname.match(/(setup|login\/plex)/)) {
ctx.res.writeHead(307, {
Location: '/setup',
Location: addBasePath('/setup'),
});
ctx.res.end();
}
} else {
try {
// Attempt to get the user by running a request to the local api
const response = await axios.get<User>(
`http://localhost:${process.env.PORT || 5055}/api/v1/auth/me`,
{ headers: ctx.req ? { cookie: ctx.req.headers.cookie } : undefined }
);
const response = await axios.get<User>(`${serverUrl}/api/v1/auth/me`, {
headers: ctx.req ? { cookie: ctx.req.headers.cookie } : undefined,
});
user = response.data;
if (router.pathname.match(/(setup|login)/)) {
ctx.res.writeHead(307, {
Location: '/',
Location: addBasePath('/'),
});
ctx.res.end();
}
@ -209,7 +215,7 @@ CoreApp.getInitialProps = async (initialProps) => {
// before anything actually renders
if (!router.pathname.match(/(login|setup|resetpassword)/)) {
ctx.res.writeHead(307, {
Location: '/login',
Location: addBasePath('/login'),
});
ctx.res.end();
}
@ -223,7 +229,7 @@ CoreApp.getInitialProps = async (initialProps) => {
);
const locale = user?.settings?.locale
? user.settings.locale
? user?.settings.locale
: currentSettings.locale;
const messages = await loadLocaleData(locale as AvailableLocale);

@ -1,9 +1,9 @@
import { ArrowCircleRightIcon } from '@heroicons/react/outline';
import type { NextPage } from 'next';
import Link from 'next/link';
import React from 'react';
import { defineMessages, useIntl } from 'react-intl';
import PageTitle from '../components/Common/PageTitle';
import Link from '../components/Link';
import type { Undefinable } from '../utils/typeHelpers';
interface ErrorProps {

@ -3,6 +3,7 @@ import { GetServerSideProps, NextPage } from 'next';
import React from 'react';
import type { Collection } from '../../../../server/models/Collection';
import CollectionDetails from '../../../components/CollectionDetails';
import addBasePath from '../../../utils/addBasePath';
interface CollectionPageProps {
collection?: Collection;
@ -12,12 +13,14 @@ const CollectionPage: NextPage<CollectionPageProps> = ({ collection }) => {
return <CollectionDetails collection={collection} />;
};
const basePath = addBasePath('');
export const getServerSideProps: GetServerSideProps<CollectionPageProps> =
async (ctx) => {
const response = await axios.get<Collection>(
`http://localhost:${process.env.PORT || 5055}/api/v1/collection/${
ctx.query.collectionId
}`,
`http://localhost:${
process.env.PORT || 5055
}${basePath}/api/v1/collection/${ctx.query.collectionId}`,
{
headers: ctx.req?.headers?.cookie
? { cookie: ctx.req.headers.cookie }

@ -3,11 +3,14 @@ import { NextPage } from 'next';
import React from 'react';
import type { MovieDetails as MovieDetailsType } from '../../../../server/models/Movie';
import MovieDetails from '../../../components/MovieDetails';
import addBasePath from '../../../utils/addBasePath';
interface MoviePageProps {
movie?: MovieDetailsType;
}
const basePath = addBasePath('');
const MoviePage: NextPage<MoviePageProps> = ({ movie }) => {
return <MovieDetails movie={movie} />;
};
@ -15,7 +18,7 @@ const MoviePage: NextPage<MoviePageProps> = ({ movie }) => {
MoviePage.getInitialProps = async (ctx) => {
if (ctx.req) {
const response = await axios.get<MovieDetailsType>(
`http://localhost:${process.env.PORT || 5055}/api/v1/movie/${
`http://localhost:${process.env.PORT || 5055}${basePath}/api/v1/movie/${
ctx.query.movieId
}`,
{

@ -3,11 +3,14 @@ import { NextPage } from 'next';
import React from 'react';
import type { TvDetails as TvDetailsType } from '../../../../server/models/Tv';
import TvDetails from '../../../components/TvDetails';
import addBasePath from '../../../utils/addBasePath';
interface TvPageProps {
tv?: TvDetailsType;
}
const basePath = addBasePath('');
const TvPage: NextPage<TvPageProps> = ({ tv }) => {
return <TvDetails tv={tv} />;
};
@ -15,7 +18,7 @@ const TvPage: NextPage<TvPageProps> = ({ tv }) => {
TvPage.getInitialProps = async (ctx) => {
if (ctx.req) {
const response = await axios.get<TvDetailsType>(
`http://localhost:${process.env.PORT || 5055}/api/v1/tv/${
`http://localhost:${process.env.PORT || 5055}${basePath}/api/v1/tv/${
ctx.query.tvId
}`,
{

@ -0,0 +1,20 @@
import getConfig from 'next/config';
import { UrlObject } from 'url';
const {
publicRuntimeConfig: { basePath = '' },
} = getConfig();
declare type Url = string | UrlObject;
export default function addBasePath(url: UrlObject): UrlObject;
export default function addBasePath(url: string): string;
export default function addBasePath(url: Url): Url;
export default function addBasePath(url: Url): Url {
if (typeof url === 'string') {
return basePath + url;
}
url.pathname = basePath + url.pathname;
return url;
}

@ -1,5 +1,6 @@
import axios from 'axios';
import Bowser from 'bowser';
import addBasePath from './addBasePath';
interface PlexHeaders {
Accept: string;
@ -177,7 +178,7 @@ class PlexOAuth {
//Set url to login/plex/loading so browser doesn't block popup
const newWindow = window.open(
'/login/plex/loading',
addBasePath('/login/plex/loading'),
title,
'scrollbars=yes, width=' +
w +

Loading…
Cancel
Save