Compare commits

..

6 Commits

Author SHA1 Message Date
gauthier-th
2786424f70 fix: add some missing getBasedPath 2025-05-27 20:50:14 +02:00
gauthier-th
89c4e119fe fix: remove some redundant basePath calls 2025-05-27 17:24:43 +02:00
gauthier-th
9d9ce8072a style: run prettier 2025-05-26 18:00:08 +02:00
gauthier-th
65d9efbd8e refactor: update to Axios 2025-05-26 17:55:13 +02:00
Aidan Hilt
2f14de8545 fix: experimental basePath support PR bugs (#1409)
* Bugfixes for the experimental basepath feature

* Added a redirect to baseURL for the user context

* Addressing PR comments

* Reverting formatting changes to _app.tsx

* Running pnpm format
2025-05-26 17:44:32 +02:00
fallenbagel
1d5378cf35 feat: experimental basePath support when built-from-source 2025-05-26 17:43:46 +02:00
55 changed files with 262 additions and 287 deletions

View File

@@ -3,8 +3,8 @@ kubeVersion: ">=1.23.0-0"
name: jellyseerr-chart
description: Jellyseerr helm chart for Kubernetes
type: application
version: 2.6.0
appVersion: "2.7.0"
version: 2.4.0
appVersion: "2.5.2"
maintainers:
- name: Jellyseerr
url: https://github.com/Fallenbagel/jellyseerr

View File

@@ -1,6 +1,6 @@
# jellyseerr-chart
![Version: 2.6.0](https://img.shields.io/badge/Version-2.6.0-informational?style=flat-square) ![Type: application](https://img.shields.io/badge/Type-application-informational?style=flat-square) ![AppVersion: 2.7.0](https://img.shields.io/badge/AppVersion-2.7.0-informational?style=flat-square)
![Version: 2.4.0](https://img.shields.io/badge/Version-2.4.0-informational?style=flat-square) ![Type: application](https://img.shields.io/badge/Type-application-informational?style=flat-square) ![AppVersion: 2.5.2](https://img.shields.io/badge/AppVersion-2.5.2-informational?style=flat-square)
Jellyseerr helm chart for Kubernetes

View File

@@ -105,12 +105,6 @@ In some places (like China), the ISP blocks not only the DNS resolution but also
You can configure Jellyseerr to use a proxy with the [HTTP(S) Proxy](/using-jellyseerr/settings/general#https-proxy) setting.
### Option 3: Force IPV4 resolution first
Sometimes there are configuration issues with IPV6 that prevent the hostname resolution from working correctly.
You can try to force the resolution to use IPV4 first by going to `Settings > Networking > Advanced Networking` and enabling `Force IPv4 Resolution First` setting and restarting Jellyseerr.
### Option 4: Check that your server can reach TMDB API
Make sure that your server can reach the TMDB API by running the following command:

View File

@@ -4107,7 +4107,7 @@ paths:
type: string
userAgent:
type: string
/user/{userId}/pushSubscription/{endpoint}:
/user/{userId}/pushSubscription/{key}:
get:
summary: Get web push notification settings for a user
description: |
@@ -4121,7 +4121,7 @@ paths:
schema:
type: number
- in: path
name: endpoint
name: key
required: true
schema:
type: string
@@ -4153,7 +4153,7 @@ paths:
schema:
type: number
- in: path
name: endpoint
name: key
required: true
schema:
type: string

View File

@@ -1,9 +1,15 @@
/**
* @type {import('next').NextConfig}
*/
// eslint-disable-next-line @typescript-eslint/no-var-requires
const path = require('path');
module.exports = {
basePath: process.env.NEXT_PUBLIC_BASE_PATH || '',
assetPrefix: process.env.NEXT_PUBLIC_BASE_PATH || '',
env: {
commitTag: process.env.COMMIT_TAG || 'local',
basePath: process.env.NEXT_PUBLIC_BASE_PATH || '',
},
images: {
remotePatterns: [
@@ -19,6 +25,9 @@ module.exports = {
issuer: /\.(js|ts)x?$/,
use: ['@svgr/webpack'],
});
config.resolve.alias['next/image'] = path.resolve(
'./src/components/Common/BaseImage/index.ts'
);
return config;
},

View File

@@ -43,8 +43,8 @@
"@supercharge/request-ip": "1.2.0",
"@svgr/webpack": "6.5.1",
"@tanem/react-nprogress": "5.0.30",
"@types/ua-parser-js": "^0.7.36",
"@types/wink-jaro-distance": "^2.0.2",
"@types/ua-parser-js": "^0.7.36",
"ace-builds": "1.15.2",
"axios": "1.3.4",
"axios-rate-limit": "1.3.0",
@@ -65,8 +65,6 @@
"express-session": "1.17.3",
"formik": "^2.4.6",
"gravatar-url": "3.1.0",
"http-proxy-agent": "^7.0.2",
"https-proxy-agent": "^7.0.6",
"lodash": "4.17.21",
"mime": "3",
"next": "^14.2.25",
@@ -103,8 +101,8 @@
"swr": "2.2.5",
"tailwind-merge": "^2.6.0",
"typeorm": "0.3.12",
"ua-parser-js": "^1.0.35",
"undici": "^7.3.0",
"ua-parser-js": "^1.0.35",
"web-push": "3.5.0",
"wink-jaro-distance": "^2.0.0",
"winston": "3.8.2",

36
pnpm-lock.yaml generated
View File

@@ -107,12 +107,6 @@ importers:
gravatar-url:
specifier: 3.1.0
version: 3.1.0
http-proxy-agent:
specifier: ^7.0.2
version: 7.0.2
https-proxy-agent:
specifier: ^7.0.6
version: 7.0.6
lodash:
specifier: 4.17.21
version: 4.17.21
@@ -3632,10 +3626,6 @@ packages:
resolution: {integrity: sha512-H0TSyFNDMomMNJQBn8wFV5YC/2eJ+VXECwOadZJT554xP6cODZHPX3H9QMQECxvrgiSOP1pHjy1sMWQVYJOUOA==}
engines: {node: '>= 14'}
agent-base@7.1.3:
resolution: {integrity: sha512-jRR5wdylq8CkOe6hei19GGZnxM6rBGwFl3Bg0YItGDimvjGtAvdZk4Pu6Cl4u4Igsws4a1fd1Vq3ezrhn4KmFw==}
engines: {node: '>= 14'}
agentkeepalive@4.5.0:
resolution: {integrity: sha512-5GG/5IbQQpC9FpkRGsSvZI5QYeSCzlJHdpBQntCsuTOxhKD8lqKhrleg2Yi7yvMIf82Ycmmqln9U8V9qwEiJew==}
engines: {node: '>= 8.0.0'}
@@ -5798,8 +5788,8 @@ packages:
resolution: {integrity: sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==}
engines: {node: '>= 6'}
https-proxy-agent@7.0.6:
resolution: {integrity: sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==}
https-proxy-agent@7.0.5:
resolution: {integrity: sha512-1e4Wqeblerz+tMKPIq2EMGiiWW1dIjZOksyHWSUm1rmuvw/how9hBHZ38lAGj5ID4Ik6EdkOw7NmWPy6LAwalw==}
engines: {node: '>= 14'}
human-signals@1.1.1:
@@ -11132,7 +11122,7 @@ snapshots:
'@babel/helper-split-export-declaration': 7.24.7
'@babel/parser': 7.24.7
'@babel/types': 7.24.7
debug: 4.4.0(supports-color@5.5.0)
debug: 4.3.5
globals: 11.12.0
transitivePeerDependencies:
- supports-color
@@ -13349,7 +13339,7 @@ snapshots:
fs-extra: 11.2.0
globby: 11.1.0
http-proxy-agent: 7.0.2
https-proxy-agent: 7.0.6
https-proxy-agent: 7.0.5
issue-parser: 6.0.0
lodash: 4.17.21
mime: 3.0.0
@@ -13971,7 +13961,7 @@ snapshots:
dependencies:
'@typescript-eslint/types': 7.2.0
'@typescript-eslint/visitor-keys': 7.2.0
debug: 4.4.0(supports-color@5.5.0)
debug: 4.3.5
globby: 11.1.0
is-glob: 4.0.3
minimatch: 9.0.3
@@ -14062,12 +14052,10 @@ snapshots:
agent-base@7.1.1:
dependencies:
debug: 4.4.0(supports-color@5.5.0)
debug: 4.3.5
transitivePeerDependencies:
- supports-color
agent-base@7.1.3: {}
agentkeepalive@4.5.0:
dependencies:
humanize-ms: 1.2.1
@@ -15811,7 +15799,7 @@ snapshots:
debug: 4.3.5
enhanced-resolve: 5.17.0
eslint: 8.35.0
eslint-module-utils: 2.8.1(@typescript-eslint/parser@7.2.0(eslint@8.35.0)(typescript@4.9.5))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@7.2.0(eslint@8.35.0)(typescript@4.9.5))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1)(eslint@8.35.0))(eslint@8.35.0)
eslint-module-utils: 2.8.1(@typescript-eslint/parser@7.2.0(eslint@8.35.0)(typescript@4.9.5))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1)(eslint@8.35.0)
eslint-plugin-import: 2.29.1(@typescript-eslint/parser@5.54.0(eslint@8.35.0)(typescript@4.9.5))(eslint@8.35.0)
fast-glob: 3.3.2
get-tsconfig: 4.7.5
@@ -15833,7 +15821,7 @@ snapshots:
transitivePeerDependencies:
- supports-color
eslint-module-utils@2.8.1(@typescript-eslint/parser@7.2.0(eslint@8.35.0)(typescript@4.9.5))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@7.2.0(eslint@8.35.0)(typescript@4.9.5))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1)(eslint@8.35.0))(eslint@8.35.0):
eslint-module-utils@2.8.1(@typescript-eslint/parser@7.2.0(eslint@8.35.0)(typescript@4.9.5))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1)(eslint@8.35.0):
dependencies:
debug: 3.2.7(supports-color@8.1.1)
optionalDependencies:
@@ -16833,7 +16821,7 @@ snapshots:
http-proxy-agent@7.0.2:
dependencies:
agent-base: 7.1.1
debug: 4.4.0(supports-color@5.5.0)
debug: 4.3.5
transitivePeerDependencies:
- supports-color
@@ -16860,10 +16848,10 @@ snapshots:
transitivePeerDependencies:
- supports-color
https-proxy-agent@7.0.6:
https-proxy-agent@7.0.5:
dependencies:
agent-base: 7.1.3
debug: 4.4.0(supports-color@5.5.0)
agent-base: 7.1.1
debug: 4.3.5
transitivePeerDependencies:
- supports-color

View File

@@ -130,7 +130,9 @@ class JellyfinAPI extends ExternalAPI {
const safeDeviceId =
deviceId && deviceId.length > 0
? deviceId
: Buffer.from('BOT_jellyseerr').toString('base64');
: Buffer.from(`BOT_jellyseerr_fallback_${Date.now()}`).toString(
'base64'
);
let authHeaderVal: string;
if (authToken) {

View File

@@ -17,7 +17,6 @@ import { DbAwareColumn } from '@server/utils/DbColumnHelper';
import { truncate } from 'lodash';
import {
AfterInsert,
AfterLoad,
AfterUpdate,
Column,
Entity,
@@ -702,13 +701,6 @@ export class MediaRequest {
}
}
@AfterLoad()
private sortSeasons() {
if (Array.isArray(this.seasons)) {
this.seasons.sort((a, b) => a.id - b.id);
}
}
static async sendNotification(
entity: MediaRequest,
media: Media,

View File

@@ -28,7 +28,6 @@ import { getAppVersion } from '@server/utils/appVersion';
import createCustomProxyAgent from '@server/utils/customProxyAgent';
import restartFlag from '@server/utils/restartFlag';
import { getClientIp } from '@supercharge/request-ip';
import axios from 'axios';
import { TypeormStore } from 'connect-typeorm/out';
import cookieParser from 'cookie-parser';
import type { NextFunction, Request, Response } from 'express';
@@ -36,8 +35,6 @@ import express from 'express';
import * as OpenApiValidator from 'express-openapi-validator';
import type { Store } from 'express-session';
import session from 'express-session';
import http from 'http';
import https from 'https';
import next from 'next';
import path from 'path';
import swaggerUi from 'swagger-ui-express';
@@ -76,11 +73,6 @@ app
const settings = await getSettings().load();
restartFlag.initializeSettings(settings);
if (settings.network.forceIpv4First) {
axios.defaults.httpAgent = new http.Agent({ family: 4 });
axios.defaults.httpsAgent = new https.Agent({ family: 4 });
}
// Register HTTP proxy
if (settings.network.proxy.enabled) {
await createCustomProxyAgent(settings.network.proxy);
@@ -162,11 +154,13 @@ app
});
if (settings.network.csrfProtection) {
server.use(
`${process.env.NEXT_PUBLIC_BASE_PATH || ''}`,
csurf({
cookie: {
httpOnly: true,
sameSite: true,
secure: !dev,
path: `${process.env.NEXT_PUBLIC_BASE_PATH || ''}` || '/',
},
})
);
@@ -182,7 +176,7 @@ app
// Set up sessions
const sessionRespository = getRepository(Session);
server.use(
'/api',
`${process.env.NEXT_PUBLIC_BASE_PATH || ''}/api`,
session({
secret: settings.clientId,
resave: false,
@@ -192,6 +186,7 @@ app
httpOnly: true,
sameSite: settings.network.csrfProtection ? 'strict' : 'lax',
secure: 'auto',
path: `${process.env.NEXT_PUBLIC_BASE_PATH || ''}` || '/',
},
store: new TypeormStore({
cleanupLimit: 2,
@@ -200,8 +195,13 @@ app
})
);
const apiDocs = YAML.load(API_SPEC_PATH);
server.use('/api-docs', swaggerUi.serve, swaggerUi.setup(apiDocs));
server.use(
`${process.env.NEXT_PUBLIC_BASE_PATH || ''}/api-docs`,
swaggerUi.serve,
swaggerUi.setup(apiDocs)
);
server.use(
`${process.env.NEXT_PUBLIC_BASE_PATH || ''}`,
OpenApiValidator.middleware({
apiSpec: API_SPEC_PATH,
validateRequests: true,
@@ -219,11 +219,12 @@ app
};
next();
});
server.use('/api/v1', routes);
const basePath = process.env.NEXT_PUBLIC_BASE_PATH || '';
server.use(`${basePath}/api/v1`, routes);
// Do not set cookies so CDNs can cache them
server.use('/imageproxy', clearCookies, imageproxy);
server.use('/avatarproxy', clearCookies, avatarproxy);
server.use(`${basePath}/imageproxy`, clearCookies, imageproxy);
server.use(`${basePath}/avatarproxy`, clearCookies, avatarproxy);
server.get('*', (req, res) => handle(req, res));
server.use(

View File

@@ -140,7 +140,6 @@ export interface MainSettings {
export interface NetworkSettings {
csrfProtection: boolean;
forceIpv4First: boolean;
trustProxy: boolean;
proxy: ProxySettings;
}
@@ -545,7 +544,6 @@ class Settings {
},
network: {
csrfProtection: false,
forceIpv4First: false,
trustProxy: false,
proxy: {
enabled: false,

View File

@@ -277,14 +277,11 @@ authRoutes.post('/jellyfin', async (req, res, next) => {
select: { id: true, jellyfinDeviceId: true },
});
let deviceId = 'BOT_jellyseerr';
if (user && user.id === 1) {
// Admin is always BOT_jellyseerr
deviceId = 'BOT_jellyseerr';
} else if (user && user.jellyfinDeviceId) {
deviceId = user.jellyfinDeviceId;
} else if (body.username) {
deviceId = Buffer.from(`BOT_jellyseerr_${body.username}`).toString(
let deviceId = '';
if (user) {
deviceId = user.jellyfinDeviceId ?? '';
} else {
deviceId = Buffer.from(`BOT_jellyseerr_${body.username ?? ''}`).toString(
'base64'
);
}

View File

@@ -23,7 +23,7 @@ async function initAvatarImageProxy() {
select: ['id', 'jellyfinUserId', 'jellyfinDeviceId'],
order: { id: 'ASC' },
});
const deviceId = admin?.jellyfinDeviceId || 'BOT_jellyseerr';
const deviceId = admin?.jellyfinDeviceId;
const authToken = getSettings().jellyfin.apiKey;
_avatarImageProxy = new ImageProxy('avatar', '', {
headers: {

View File

@@ -240,8 +240,8 @@ router.get<{ userId: number }>(
}
);
router.get<{ userId: number; endpoint: string }>(
'/:userId/pushSubscription/:endpoint',
router.get<{ userId: number; key: string }>(
'/:userId/pushSubscription/:key',
async (req, res, next) => {
try {
const userPushSubRepository = getRepository(UserPushSubscription);
@@ -252,7 +252,7 @@ router.get<{ userId: number; endpoint: string }>(
},
where: {
user: { id: req.params.userId },
endpoint: req.params.endpoint,
p256dh: req.params.key,
},
});
@@ -263,8 +263,8 @@ router.get<{ userId: number; endpoint: string }>(
}
);
router.delete<{ userId: number; endpoint: string }>(
'/:userId/pushSubscription/:endpoint',
router.delete<{ userId: number; key: string }>(
'/:userId/pushSubscription/:key',
async (req, res, next) => {
try {
const userPushSubRepository = getRepository(UserPushSubscription);
@@ -275,7 +275,7 @@ router.delete<{ userId: number; endpoint: string }>(
},
where: {
user: { id: req.params.userId },
endpoint: req.params.endpoint,
p256dh: req.params.key,
},
});
@@ -284,7 +284,7 @@ router.delete<{ userId: number; endpoint: string }>(
} catch (e) {
logger.error('Something went wrong deleting the user push subcription', {
label: 'API',
endpoint: req.params.endpoint,
key: req.params.key,
errorMessage: e.message,
});
return next({

View File

@@ -18,7 +18,6 @@ import { ApiError } from '@server/types/error';
import { getHostname } from '@server/utils/getHostname';
import { Router } from 'express';
import net from 'net';
import { Not } from 'typeorm';
import { canMakePermissionsChange } from '.';
const isOwnProfile = (): Middleware => {
@@ -126,9 +125,8 @@ userSettingsRoutes.post<
}
const existingUser = await userRepository.findOne({
where: { email: user.email, id: Not(user.id) },
where: { email: user.email },
});
if (oldEmail !== user.email && existingUser) {
throw new ApiError(400, ApiErrorCode.InvalidEmail);
}
@@ -421,9 +419,7 @@ userSettingsRoutes.post<{ username: string; password: string }>(
const hostname = getHostname();
const deviceId = Buffer.from(
req.user?.id === 1
? 'BOT_jellyseerr'
: `BOT_jellyseerr_${req.user.username ?? ''}`
`BOT_jellyseerr_${req.user.username ?? ''}`
).toString('base64');
const jellyfinserver = new JellyfinAPI(hostname, undefined, deviceId);

View File

@@ -1,8 +1,6 @@
import type { ProxySettings } from '@server/lib/settings';
import logger from '@server/logger';
import axios from 'axios';
import { HttpProxyAgent } from 'http-proxy-agent';
import { HttpsProxyAgent } from 'https-proxy-agent';
import type { Dispatcher } from 'undici';
import { Agent, ProxyAgent, setGlobalDispatcher } from 'undici';
@@ -56,33 +54,17 @@ export default async function createCustomProxyAgent(
: undefined;
try {
const proxyUrl =
(proxySettings.useSsl ? 'https://' : 'http://') +
proxySettings.hostname +
':' +
proxySettings.port;
const proxyAgent = new ProxyAgent({
uri: proxyUrl,
uri:
(proxySettings.useSsl ? 'https://' : 'http://') +
proxySettings.hostname +
':' +
proxySettings.port,
token,
keepAliveTimeout: 5000,
});
setGlobalDispatcher(proxyAgent.compose(noProxyInterceptor));
axios.defaults.httpAgent = new HttpProxyAgent(proxyUrl, {
headers: { 'proxy-authorization': token },
});
axios.defaults.httpsAgent = new HttpsProxyAgent(proxyUrl, {
headers: { 'proxy-authorization': token },
});
axios.interceptors.request.use((config) => {
if (config.url && skipUrl(config.url)) {
config.httpAgent = false;
config.httpsAgent = false;
}
return config;
});
} catch (e) {
logger.error('Failed to connect to the proxy: ' + e.message, {
label: 'Proxy',

View File

@@ -12,6 +12,7 @@ import { Permission, useUser } from '@app/hooks/useUser';
import globalMessages from '@app/i18n/globalMessages';
import Error from '@app/pages/_error';
import defineMessages from '@app/utils/defineMessages';
import { getBasedPath } from '@app/utils/navigationUtil';
import {
ChevronLeftIcon,
ChevronRightIcon,
@@ -122,7 +123,7 @@ const Blacklist = () => {
onChange={(e) => {
setCurrentFilter(e.target.value as Filter);
router.push({
pathname: router.pathname,
pathname: getBasedPath(router.pathname),
query: router.query.userId
? { userId: router.query.userId }
: {},

View File

@@ -0,0 +1,32 @@
// src/components/Common/BaseImage/index.ts
import type { ImageProps } from 'next/image';
import NextImage from 'next/image';
import React from 'react';
// Instead of defining our own props, extend from Next's ImageProps
const BaseImage = React.forwardRef<HTMLImageElement, ImageProps>(
(props, ref) => {
const basePath = process.env.NEXT_PUBLIC_BASE_PATH || '';
const modifiedSrc =
typeof props.src === 'string' && props.src.startsWith('/')
? `${basePath}${props.src}`
: props.src;
const shouldUnoptimize =
typeof props.src === 'string' && props.src.endsWith('.svg');
return React.createElement(NextImage, {
...props,
ref,
src: modifiedSrc,
unoptimized: shouldUnoptimize || props.unoptimized,
});
}
);
BaseImage.displayName = 'Image';
export default BaseImage;
// Re-export ImageProps type for consumers
export type { ImageProps };

View File

@@ -1,6 +1,6 @@
import Image from '@app/components/Common/BaseImage';
import useSettings from '@app/hooks/useSettings';
import type { ImageLoader, ImageProps } from 'next/image';
import Image from 'next/image';
const imageLoader: ImageLoader = ({ src }) => src;

View File

@@ -1,4 +1,5 @@
import { useUser } from '@app/hooks/useUser';
import { getBasedPath } from '@app/utils/navigationUtil';
import type { Permission } from '@server/lib/permissions';
import { hasPermission } from '@server/lib/permissions';
import Link from 'next/link';
@@ -85,10 +86,10 @@ const SettingsTabs = ({
</label>
<select
onChange={(e) => {
router.push(e.target.value);
router.push(getBasedPath(e.target.value));
}}
onBlur={(e) => {
router.push(e.target.value);
router.push(getBasedPath(e.target.value));
}}
defaultValue={
settingsRoutes.find((route) => !!router.pathname.match(route.regex))

View File

@@ -85,7 +85,7 @@ const DiscoverMovies = () => {
id="sortBy"
name="sortBy"
className="rounded-r-only"
value={preparedFilters.sortBy || SortOptions.PopularityDesc}
value={preparedFilters.sortBy}
onChange={(e) => updateQueryParams('sortBy', e.target.value)}
>
<option value={SortOptions.PopularityDesc}>

View File

@@ -83,7 +83,7 @@ const DiscoverTv = () => {
id="sortBy"
name="sortBy"
className="rounded-r-only"
value={preparedFilters.sortBy || SortOptions.PopularityDesc}
value={preparedFilters.sortBy}
onChange={(e) => updateQueryParams('sortBy', e.target.value)}
>
<option value={SortOptions.PopularityDesc}>

View File

@@ -13,6 +13,7 @@ import { Permission, useUser } from '@app/hooks/useUser';
import globalMessages from '@app/i18n/globalMessages';
import ErrorPage from '@app/pages/_error';
import defineMessages from '@app/utils/defineMessages';
import { getBasedPath } from '@app/utils/navigationUtil';
import { Transition } from '@headlessui/react';
import {
ChatBubbleOvalLeftEllipsisIcon,
@@ -166,7 +167,7 @@ const IssueDetails = () => {
appearance: 'success',
autoDismiss: true,
});
router.push('/issues');
router.push(getBasedPath('/issues'));
} catch (e) {
addToast(intl.formatMessage(messages.toastissuedeletefailed), {
appearance: 'error',

View File

@@ -6,6 +6,7 @@ import IssueItem from '@app/components/IssueList/IssueItem';
import { useUpdateQueryParams } from '@app/hooks/useUpdateQueryParams';
import globalMessages from '@app/i18n/globalMessages';
import defineMessages from '@app/utils/defineMessages';
import { getBasedPath } from '@app/utils/navigationUtil';
import {
BarsArrowDownIcon,
ChevronLeftIcon,
@@ -107,7 +108,7 @@ const IssueList = () => {
onChange={(e) => {
setCurrentFilter(e.target.value as Filter);
router.push({
pathname: router.pathname,
pathname: getBasedPath(router.pathname),
query: router.query.userId
? { userId: router.query.userId }
: {},
@@ -137,7 +138,7 @@ const IssueList = () => {
onChange={(e) => {
setCurrentSort(e.target.value as Sort);
router.push({
pathname: router.pathname,
pathname: getBasedPath(router.pathname),
query: router.query.userId
? { userId: router.query.userId }
: {},

View File

@@ -1,4 +1,5 @@
import Badge from '@app/components/Common/Badge';
import Image from '@app/components/Common/BaseImage';
import UserWarnings from '@app/components/Layout/UserWarnings';
import VersionStatus from '@app/components/Layout/VersionStatus';
import useClickOutside from '@app/hooks/useClickOutside';
@@ -16,7 +17,6 @@ import {
UsersIcon,
XMarkIcon,
} from '@heroicons/react/24/outline';
import Image from 'next/image';
import Link from 'next/link';
import { useRouter } from 'next/router';
import { Fragment, useEffect, useRef } from 'react';

View File

@@ -1,6 +1,6 @@
import CachedImage from '@app/components/Common/CachedImage';
import MiniQuotaDisplay from '@app/components/Layout/UserDropdown/MiniQuotaDisplay';
import { Permission, useUser } from '@app/hooks/useUser';
import { useUser } from '@app/hooks/useUser';
import defineMessages from '@app/utils/defineMessages';
import { Menu, Transition } from '@headlessui/react';
import {
@@ -36,7 +36,7 @@ ForwardedLink.displayName = 'ForwardedLink';
const UserDropdown = () => {
const intl = useIntl();
const { user, revalidate, hasPermission } = useUser();
const { user, revalidate } = useUser();
const logout = async () => {
const response = await axios.post('/api/v1/auth/logout');
@@ -118,14 +118,7 @@ const UserDropdown = () => {
<Menu.Item>
{({ active }) => (
<ForwardedLink
href={
hasPermission(
[Permission.MANAGE_REQUESTS, Permission.REQUEST_VIEW],
{ type: 'or' }
)
? `/users/${user?.id}/requests?filter=all`
: '/requests'
}
href={`/users/${user?.id}/requests?filter=all`}
className={`flex items-center rounded px-4 py-2 text-sm font-medium text-gray-200 transition duration-150 ease-in-out ${
active
? 'bg-gradient-to-br from-indigo-600 to-purple-600 text-white'

View File

@@ -1,6 +1,7 @@
import EmbyLogo from '@app/assets/services/emby-icon-only.svg';
import JellyfinLogo from '@app/assets/services/jellyfin-icon.svg';
import PlexLogo from '@app/assets/services/plex.svg';
import Image from '@app/components/Common/BaseImage';
import Button from '@app/components/Common/Button';
import ImageFader from '@app/components/Common/ImageFader';
import PageTitle from '@app/components/Common/PageTitle';
@@ -11,12 +12,12 @@ import PlexLoginButton from '@app/components/Login/PlexLoginButton';
import useSettings from '@app/hooks/useSettings';
import { useUser } from '@app/hooks/useUser';
import defineMessages from '@app/utils/defineMessages';
import { getBasedPath } from '@app/utils/navigationUtil';
import { Transition } from '@headlessui/react';
import { XCircleIcon } from '@heroicons/react/24/solid';
import { MediaServerType } from '@server/constants/server';
import axios from 'axios';
import { useRouter } from 'next/dist/client/router';
import Image from 'next/image';
import { useEffect, useRef, useState } from 'react';
import { useIntl } from 'react-intl';
import { CSSTransition, SwitchTransition } from 'react-transition-group';
@@ -44,6 +45,8 @@ const Login = () => {
settings.currentSettings.mediaServerLogin
);
const basePath = process.env.NEXT_PUBLIC_BASE_PATH || '';
// Effect that is triggered when the `authToken` comes back from the Plex OAuth
// We take the token and attempt to sign in. If we get a success message, we will
// ask swr to revalidate the user which _should_ come back with a valid user.
@@ -71,7 +74,7 @@ const Login = () => {
// valid user, we redirect the user to the home page as the login was successful.
useEffect(() => {
if (user) {
router.push('/');
router.push(getBasedPath('/'));
}
}, [user, router]);
@@ -129,7 +132,7 @@ const Login = () => {
>
{/* eslint-disable-next-line @next/next/no-img-element */}
<img
src="/os_icon.svg"
src={basePath + '/os_icon.svg'}
alt={settings.currentSettings.applicationTitle}
className="mr-2 h-5"
/>

View File

@@ -74,14 +74,6 @@ const MediaSlider = ({
);
}
if (settings.currentSettings.hideBlacklisted) {
titles = titles.filter(
(i) =>
(i.mediaType === 'movie' || i.mediaType === 'tv') &&
i.mediaInfo?.status !== MediaStatus.BLACKLISTED
);
}
useEffect(() => {
if (
titles.length < 24 &&

View File

@@ -30,6 +30,7 @@ import globalMessages from '@app/i18n/globalMessages';
import ErrorPage from '@app/pages/_error';
import { sortCrewPriority } from '@app/utils/creditHelpers';
import defineMessages from '@app/utils/defineMessages';
import { getBasedPath } from '@app/utils/navigationUtil';
import { refreshIntervalHelper } from '@app/utils/refreshIntervalHelper';
import {
ArrowRightCircleIcon,
@@ -468,7 +469,7 @@ const MovieDetails = ({ movie }: MovieDetailsProps) => {
onClose={() => {
setShowManager(false);
router.push({
pathname: router.pathname,
pathname: getBasedPath(router.pathname),
query: { movieId: router.query.movieId },
});
}}

View File

@@ -3,153 +3,154 @@ interface PWAHeaderProps {
}
const PWAHeader = ({ applicationTitle = 'Jellyseerr' }: PWAHeaderProps) => {
const basePath = process.env.NEXT_PUBLIC_BASE_PATH || '';
return (
<>
<link
rel="apple-touch-icon"
sizes="180x180"
href="/apple-touch-icon.png"
href={`${basePath}/apple-touch-icon.png`}
/>
<link
rel="icon"
type="image/png"
sizes="32x32"
href="/favicon-32x32.png"
href={`${basePath}/favicon-32x32.png`}
/>
<link
rel="icon"
type="image/png"
sizes="16x16"
href="/favicon-16x16.png"
href={`${basePath}/favicon-16x16.png`}
/>
<link
rel="apple-touch-startup-image"
href="/apple-splash-2048-2732.jpg"
href={`${basePath}/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={`${basePath}/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={`${basePath}/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={`${basePath}/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={`${basePath}/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={`${basePath}/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={`${basePath}/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={`${basePath}/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={`${basePath}/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={`${basePath}/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={`${basePath}/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={`${basePath}/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={`${basePath}/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={`${basePath}/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={`${basePath}/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={`${basePath}/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={`${basePath}/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={`${basePath}/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={`${basePath}/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={`${basePath}/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={`${basePath}/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={`${basePath}/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={`${basePath}/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={`${basePath}/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={`${basePath}/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={`${basePath}/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" />
@@ -159,7 +160,7 @@ const PWAHeader = ({ applicationTitle = 'Jellyseerr' }: PWAHeaderProps) => {
/>
<link
rel="manifest"
href="/site.webmanifest"
href={`${basePath}/site.webmanifest`}
crossOrigin="use-credentials"
/>
<meta name="apple-mobile-web-app-title" content={applicationTitle} />

View File

@@ -8,6 +8,7 @@ import { useUpdateQueryParams } from '@app/hooks/useUpdateQueryParams';
import { useUser } from '@app/hooks/useUser';
import globalMessages from '@app/i18n/globalMessages';
import defineMessages from '@app/utils/defineMessages';
import { getBasedPath } from '@app/utils/navigationUtil';
import {
ArrowDownIcon,
ArrowUpIcon,
@@ -173,7 +174,7 @@ const RequestList = () => {
onChange={(e) => {
setCurrentMediaType(e.target.value as MediaType);
router.push({
pathname: router.pathname,
pathname: getBasedPath(router.pathname),
query: router.query.userId
? { userId: router.query.userId }
: {},
@@ -203,7 +204,7 @@ const RequestList = () => {
onChange={(e) => {
setCurrentFilter(e.target.value as Filter);
router.push({
pathname: router.pathname,
pathname: getBasedPath(router.pathname),
query: router.query.userId
? { userId: router.query.userId }
: {},
@@ -251,7 +252,7 @@ const RequestList = () => {
onChange={(e) => {
setCurrentSort(e.target.value as Sort);
router.push({
pathname: router.pathname,
pathname: getBasedPath(router.pathname),
query: router.query.userId
? { userId: router.query.userId }
: {},

View File

@@ -1,3 +1,4 @@
import Image from '@app/components/Common/BaseImage';
import Button from '@app/components/Common/Button';
import ImageFader from '@app/components/Common/ImageFader';
import PageTitle from '@app/components/Common/PageTitle';
@@ -6,7 +7,6 @@ import defineMessages from '@app/utils/defineMessages';
import { ArrowLeftIcon, EnvelopeIcon } from '@heroicons/react/24/solid';
import axios from 'axios';
import { Field, Form, Formik } from 'formik';
import Image from 'next/image';
import Link from 'next/link';
import { useState } from 'react';
import { useIntl } from 'react-intl';

View File

@@ -1,3 +1,4 @@
import Image from '@app/components/Common/BaseImage';
import Button from '@app/components/Common/Button';
import ImageFader from '@app/components/Common/ImageFader';
import SensitiveInput from '@app/components/Common/SensitiveInput';
@@ -7,7 +8,6 @@ import defineMessages from '@app/utils/defineMessages';
import { LifebuoyIcon } from '@heroicons/react/24/outline';
import axios from 'axios';
import { Form, Formik } from 'formik';
import Image from 'next/image';
import Link from 'next/link';
import { useRouter } from 'next/router';
import { useState } from 'react';

View File

@@ -6,7 +6,6 @@ import globalMessages from '@app/i18n/globalMessages';
import defineMessages from '@app/utils/defineMessages';
import { isValidURL } from '@app/utils/urlValidationHelper';
import { ArrowDownOnSquareIcon, BeakerIcon } from '@heroicons/react/24/outline';
import type { NotificationAgentNtfy } from '@server/lib/settings';
import axios from 'axios';
import { Field, Form, Formik } from 'formik';
import { useState } from 'react';
@@ -45,7 +44,7 @@ const NotificationsNtfy = () => {
data,
error,
mutate: revalidate,
} = useSWR<NotificationAgentNtfy>('/api/v1/settings/notifications/ntfy');
} = useSWR('/api/v1/settings/notifications/ntfy');
const NotificationsNtfySchema = Yup.object().shape({
url: Yup.string()
@@ -79,15 +78,15 @@ const NotificationsNtfy = () => {
return (
<Formik
initialValues={{
enabled: data?.enabled,
types: data?.types,
url: data?.options.url,
topic: data?.options.topic,
authMethodUsernamePassword: data?.options.authMethodUsernamePassword,
username: data?.options.username,
password: data?.options.password,
authMethodToken: data?.options.authMethodToken,
token: data?.options.token,
enabled: data.enabled,
types: data.types,
url: data.options.url,
topic: data.options.topic,
authMethodUsernamePassword: data.options.authMethod,
username: data.options.username,
password: data.options.password,
authMethodToken: data.options.authMethodToken,
token: data.options.token,
}}
validationSchema={NotificationsNtfySchema}
onSubmit={async (values) => {
@@ -303,7 +302,7 @@ const NotificationsNtfy = () => {
</div>
)}
<NotificationTypeSelector
currentTypes={values.enabled ? values.types || 0 : 0}
currentTypes={values.enabled ? values.types : 0}
onUpdate={(newTypes) => {
setFieldValue('types', newTypes);
setFieldTouched('types');

View File

@@ -10,6 +10,7 @@ import { useUpdateQueryParams } from '@app/hooks/useUpdateQueryParams';
import globalMessages from '@app/i18n/globalMessages';
import Error from '@app/pages/_error';
import defineMessages from '@app/utils/defineMessages';
import { getBasedPath } from '@app/utils/navigationUtil';
import { Transition } from '@headlessui/react';
import {
ChevronLeftIcon,
@@ -286,7 +287,7 @@ const SettingsLogs = () => {
name="filter"
onChange={(e) => {
setCurrentFilter(e.target.value as Filter);
router.push(router.pathname);
router.push(getBasedPath(router.pathname));
}}
value={currentFilter}
className="rounded-r-only"

View File

@@ -66,8 +66,6 @@ const messages = defineMessages('components.Settings.SettingsMain', {
enableSpecialEpisodes: 'Allow Special Episodes Requests',
locale: 'Display Language',
youtubeUrl: 'YouTube URL',
youtubeUrlTip:
'Base URL for YouTube videos if a self-hosted YouTube instance is used.',
validationUrl: 'You must provide a valid URL',
validationUrlTrailingSlash: 'URL must not end in a trailing slash',
});
@@ -538,9 +536,6 @@ const SettingsMain = () => {
<div className="form-row">
<label htmlFor="youtubeUrl" className="text-label">
{intl.formatMessage(messages.youtubeUrl)}
<span className="label-tip">
{intl.formatMessage(messages.youtubeUrlTip)}
</span>
</label>
<div className="form-input-area">
<div className="form-input-field">

View File

@@ -42,9 +42,6 @@ const messages = defineMessages('components.Settings.SettingsNetwork', {
networkDisclaimer:
'Network parameters from your container/system should be used instead of these settings. See the {docs} for more information.',
docs: 'documentation',
forceIpv4First: 'Force IPv4 Resolution First',
forceIpv4FirstTip:
'Force Jellyseerr to resolve IPv4 addresses first instead of IPv6',
});
const SettingsNetwork = () => {
@@ -89,7 +86,6 @@ const SettingsNetwork = () => {
<Formik
initialValues={{
csrfProtection: data?.csrfProtection,
forceIpv4First: data?.forceIpv4First,
trustProxy: data?.trustProxy,
proxyEnabled: data?.proxy?.enabled,
proxyHostname: data?.proxy?.hostname,
@@ -106,7 +102,6 @@ const SettingsNetwork = () => {
try {
await axios.post('/api/v1/settings/network', {
csrfProtection: values.csrfProtection,
forceIpv4First: values.forceIpv4First,
trustProxy: values.trustProxy,
proxy: {
enabled: values.proxyEnabled,
@@ -198,29 +193,6 @@ const SettingsNetwork = () => {
</Tooltip>
</div>
</div>
<div className="form-row">
<label htmlFor="forceIpv4First" className="checkbox-label">
<span className="mr-2">
{intl.formatMessage(messages.forceIpv4First)}
</span>
<SettingsBadge badgeType="advanced" className="mr-2" />
<SettingsBadge badgeType="restartRequired" />
<SettingsBadge badgeType="experimental" />
<span className="label-tip">
{intl.formatMessage(messages.forceIpv4FirstTip)}
</span>
</label>
<div className="form-input-area">
<Field
type="checkbox"
id="forceIpv4First"
name="forceIpv4First"
onChange={() => {
setFieldValue('forceIpv4First', !values.forceIpv4First);
}}
/>
</div>
</div>
<div className="form-row">
<label htmlFor="proxyEnabled" className="checkbox-label">
<span className="mr-2">

View File

@@ -2,6 +2,7 @@ import EmbyLogo from '@app/assets/services/emby.svg';
import JellyfinLogo from '@app/assets/services/jellyfin.svg';
import PlexLogo from '@app/assets/services/plex.svg';
import AppDataWarning from '@app/components/AppDataWarning';
import Image from '@app/components/Common/BaseImage';
import Button from '@app/components/Common/Button';
import ImageFader from '@app/components/Common/ImageFader';
import PageTitle from '@app/components/Common/PageTitle';
@@ -13,10 +14,10 @@ import SetupSteps from '@app/components/Setup/SetupSteps';
import useLocale from '@app/hooks/useLocale';
import useSettings from '@app/hooks/useSettings';
import defineMessages from '@app/utils/defineMessages';
import { getBasedPath } from '@app/utils/navigationUtil';
import { MediaServerType } from '@server/constants/server';
import type { Library } from '@server/lib/settings';
import axios from 'axios';
import Image from 'next/image';
import { useRouter } from 'next/router';
import { useCallback, useEffect, useState } from 'react';
import { useIntl } from 'react-intl';
@@ -67,7 +68,7 @@ const Setup = () => {
await axios.post('/api/v1/settings/main', { locale });
mutate('/api/v1/settings/public');
router.push('/');
router.push(getBasedPath('/'));
}
};
@@ -108,7 +109,7 @@ const Setup = () => {
useEffect(() => {
if (settings.currentSettings.initialized) {
router.push('/');
router.push(getBasedPath('/'));
}
if (

View File

@@ -33,6 +33,7 @@ import globalMessages from '@app/i18n/globalMessages';
import Error from '@app/pages/_error';
import { sortCrewPriority } from '@app/utils/creditHelpers';
import defineMessages from '@app/utils/defineMessages';
import { getBasedPath } from '@app/utils/navigationUtil';
import { refreshIntervalHelper } from '@app/utils/refreshIntervalHelper';
import { Disclosure, Transition } from '@headlessui/react';
import {
@@ -520,7 +521,7 @@ const TvDetails = ({ tv }: TvDetailsProps) => {
onClose={() => {
setShowManager(false);
router.push({
pathname: router.pathname,
pathname: getBasedPath(router.pathname),
query: { tvId: router.query.tvId },
});
}}

View File

@@ -1,10 +1,10 @@
import Alert from '@app/components/Common/Alert';
import Image from '@app/components/Common/BaseImage';
import Modal from '@app/components/Common/Modal';
import useSettings from '@app/hooks/useSettings';
import globalMessages from '@app/i18n/globalMessages';
import defineMessages from '@app/utils/defineMessages';
import axios from 'axios';
import Image from 'next/image';
import { useState } from 'react';
import { useIntl } from 'react-intl';
import { useToasts } from 'react-toast-notifications';

View File

@@ -16,6 +16,7 @@ import type { User } from '@app/hooks/useUser';
import { Permission, UserType, useUser } from '@app/hooks/useUser';
import globalMessages from '@app/i18n/globalMessages';
import defineMessages from '@app/utils/defineMessages';
import { getBasedPath } from '@app/utils/navigationUtil';
import { Transition } from '@headlessui/react';
import {
BarsArrowDownIcon,
@@ -550,7 +551,7 @@ const UserList = () => {
name="sort"
onChange={(e) => {
setCurrentSort(e.target.value as Sort);
router.push(router.pathname);
router.push(getBasedPath(router.pathname));
}}
value={currentSort}
className="rounded-r-only"
@@ -717,7 +718,7 @@ const UserList = () => {
className="mr-2"
onClick={() =>
router.push(
'/users/[userId]/settings',
getBasedPath('/users/[userId]/settings'),
`/users/${user.id}/settings`
)
}

View File

@@ -33,14 +33,13 @@ const messages = defineMessages(
const DeviceItem = ({ disablePushNotifications, device }: DeviceItemProps) => {
const intl = useIntl();
const parsedUserAgent = UAParser(device.userAgent);
return (
<div className="relative flex w-full flex-col justify-between overflow-hidden rounded-xl bg-gray-800 py-4 text-gray-400 shadow-md ring-1 ring-gray-700 xl:h-28 xl:flex-row">
<div className="relative flex w-full flex-col justify-between overflow-hidden sm:flex-row">
<div className="relative z-10 flex w-full items-center overflow-hidden pl-4 pr-4 sm:pr-0 xl:w-7/12 2xl:w-2/3">
<div className="relative h-auto w-12 flex-shrink-0 scale-100 transform-gpu overflow-hidden rounded-md transition duration-300 hover:scale-105">
{parsedUserAgent.device.type === 'mobile' ? (
{UAParser(device.userAgent).device.type === 'mobile' ? (
<DevicePhoneMobileIcon />
) : (
<ComputerDesktopIcon />
@@ -57,8 +56,8 @@ const DeviceItem = ({ disablePushNotifications, device }: DeviceItemProps) => {
: 'N/A'}
</div>
<div className="mr-2 min-w-0 truncate text-lg font-bold text-white hover:underline xl:text-xl">
{device.userAgent && parsedUserAgent.device.model
? parsedUserAgent.device.model
{device.userAgent
? UAParser(device.userAgent).device.model
: intl.formatMessage(messages.unknown)}
</div>
</div>
@@ -69,7 +68,7 @@ const DeviceItem = ({ disablePushNotifications, device }: DeviceItemProps) => {
{intl.formatMessage(messages.operatingsystem)}
</span>
<span className="flex truncate text-sm text-gray-300">
{device.userAgent ? parsedUserAgent.os.name : 'N/A'}
{device.userAgent ? UAParser(device.userAgent).os.name : 'N/A'}
</span>
</div>
<div className="card-field">
@@ -77,7 +76,9 @@ const DeviceItem = ({ disablePushNotifications, device }: DeviceItemProps) => {
{intl.formatMessage(messages.browser)}
</span>
<span className="flex truncate text-sm text-gray-300">
{device.userAgent ? parsedUserAgent.browser.name : 'N/A'}
{device.userAgent
? UAParser(device.userAgent).browser.name
: 'N/A'}
</span>
</div>
<div className="card-field">
@@ -85,14 +86,16 @@ const DeviceItem = ({ disablePushNotifications, device }: DeviceItemProps) => {
{intl.formatMessage(messages.engine)}
</span>
<span className="flex truncate text-sm text-gray-300">
{device.userAgent ? parsedUserAgent.engine.name : 'N/A'}
{device.userAgent
? UAParser(device.userAgent).engine.name
: 'N/A'}
</span>
</div>
</div>
</div>
<div className="z-10 mt-4 flex w-full flex-col justify-center space-y-2 pl-4 pr-4 xl:mt-0 xl:w-96 xl:items-end xl:pl-0">
<ConfirmButton
onClick={() => disablePushNotifications(device.endpoint)}
onClick={() => disablePushNotifications(device.p256dh)}
confirmText={intl.formatMessage(globalMessages.areyousure)}
className="w-full"
>

View File

@@ -113,7 +113,7 @@ const UserWebPushSettings = () => {
// Unsubscribes from the push manager
// Deletes/disables corresponding push subscription from database
const disablePushNotifications = async (endpoint?: string) => {
const disablePushNotifications = async (p256dh?: string) => {
if ('serviceWorker' in navigator && user?.id) {
navigator.serviceWorker.getRegistration('/sw.js').then((registration) => {
registration?.pushManager
@@ -122,21 +122,17 @@ const UserWebPushSettings = () => {
const parsedSub = JSON.parse(JSON.stringify(subscription));
await axios.delete(
`/api/v1/user/${user.id}/pushSubscription/${encodeURIComponent(
endpoint ?? parsedSub.endpoint
)}`
`/api/v1/user/${user?.id}/pushSubscription/${
p256dh ? p256dh : parsedSub.keys.p256dh
}`
);
if (
subscription &&
(endpoint === parsedSub.endpoint || !endpoint)
) {
if (subscription && (p256dh === parsedSub.keys.p256dh || !p256dh)) {
subscription.unsubscribe();
setWebPushEnabled(false);
}
addToast(
intl.formatMessage(
endpoint
p256dh
? messages.subscriptiondeleted
: messages.webpushhasbeendisabled
),
@@ -149,7 +145,7 @@ const UserWebPushSettings = () => {
.catch(function () {
addToast(
intl.formatMessage(
endpoint
p256dh
? messages.subscriptiondeleteerror
: messages.disablingwebpusherror
),
@@ -180,17 +176,12 @@ const UserWebPushSettings = () => {
const parsedKey = JSON.parse(JSON.stringify(subscription));
const currentUserPushSub =
await axios.get<UserPushSubscription>(
`/api/v1/user/${
user.id
}/pushSubscription/${encodeURIComponent(
parsedKey.endpoint
)}`
`/api/v1/user/${user.id}/pushSubscription/${parsedKey.keys.p256dh}`
);
if (currentUserPushSub.data.endpoint !== parsedKey.endpoint) {
if (currentUserPushSub.data.p256dh !== parsedKey.keys.p256dh) {
return;
}
setWebPushEnabled(true);
} else {
setWebPushEnabled(false);

View File

@@ -160,12 +160,9 @@ const UserProfile = () => {
<dd className="mt-1 text-3xl font-semibold text-white">
<Link
href={
currentHasPermission(
[Permission.MANAGE_REQUESTS, Permission.REQUEST_VIEW],
{ type: 'or' }
)
? `/users/${user?.id}/requests?filter=all`
: '/requests'
user.id === currentUser?.id
? '/profile/requests?filter=all'
: `/users/${user?.id}/requests?filter=all`
}
>
{intl.formatNumber(user.requestCount)}
@@ -296,12 +293,9 @@ const UserProfile = () => {
<div className="slider-header">
<Link
href={
currentHasPermission(
[Permission.MANAGE_REQUESTS, Permission.REQUEST_VIEW],
{ type: 'or' }
)
? `/users/${user?.id}/requests?filter=all`
: '/requests'
user.id === currentUser?.id
? '/profile/requests?filter=all'
: `/users/${user?.id}/requests?filter=all`
}
className="slider-title"
>

View File

@@ -17,6 +17,7 @@ export const UserContext = ({ initialUser, children }: UserContextProps) => {
const { user, error, revalidate } = useUser({ initialData: initialUser });
const router = useRouter();
const routing = useRef(false);
const API_BASE = process.env.NEXT_PUBLIC_BASE_PATH || '';
useEffect(() => {
revalidate();
@@ -29,9 +30,9 @@ export const UserContext = ({ initialUser, children }: UserContextProps) => {
!routing.current
) {
routing.current = true;
location.href = '/login';
location.href = `${API_BASE}/login`;
}
}, [router, user, error]);
}, [router, user, error, API_BASE]);
return <>{children}</>;
};

View File

@@ -78,7 +78,8 @@ const useDiscover = <
)
.join('&');
return `${endpoint}?${finalQueryString}`;
const fullEndpoint = endpoint.startsWith('/') ? `${endpoint}` : endpoint;
return `${fullEndpoint}?${finalQueryString}`;
},
{
initialSize: 3,

View File

@@ -1,3 +1,4 @@
import { getBasedPath } from '@app/utils/navigationUtil';
import { useRouter } from 'next/router';
import { useEffect } from 'react';
import type { Permission, PermissionCheckOptions } from './useUser';
@@ -12,7 +13,7 @@ const useRouteGuard = (
useEffect(() => {
if (user && !hasPermission(permission, options)) {
router.push('/');
router.push(getBasedPath('/'));
}
}, [user, permission, router, hasPermission, options]);
};

View File

@@ -1,3 +1,4 @@
import { getBasedPath } from '@app/utils/navigationUtil';
import type { NextRouter } from 'next/router';
import { useRouter } from 'next/router';
import type { ParsedUrlQuery } from 'querystring';
@@ -106,9 +107,15 @@ export const useQueryParams = (): UseQueryParamReturnedFunction => {
if (newRoute.path !== router.asPath) {
if (routerAction === 'replace') {
router.replace(newRoute.pathname, newRoute.path);
router.replace(
getBasedPath(newRoute.pathname),
getBasedPath(newRoute.path)
);
} else {
router.push(newRoute.pathname, newRoute.path);
router.push(
getBasedPath(newRoute.pathname),
getBasedPath(newRoute.path)
);
}
}
},

View File

@@ -978,13 +978,10 @@
"components.Settings.SettingsMain.validationUrl": "You must provide a valid URL",
"components.Settings.SettingsMain.validationUrlTrailingSlash": "URL must not end in a trailing slash",
"components.Settings.SettingsMain.youtubeUrl": "YouTube URL",
"components.Settings.SettingsMain.youtubeUrlTip": "Base URL for YouTube videos if a self-hosted YouTube instance is used.",
"components.Settings.SettingsNetwork.csrfProtection": "Enable CSRF Protection",
"components.Settings.SettingsNetwork.csrfProtectionHoverTip": "Do NOT enable this setting unless you understand what you are doing!",
"components.Settings.SettingsNetwork.csrfProtectionTip": "Set external API access to read-only (requires HTTPS)",
"components.Settings.SettingsNetwork.docs": "documentation",
"components.Settings.SettingsNetwork.forceIpv4First": "Force IPv4 Resolution First",
"components.Settings.SettingsNetwork.forceIpv4FirstTip": "Force Jellyseerr to resolve IPv4 addresses first instead of IPv6",
"components.Settings.SettingsNetwork.network": "Network",
"components.Settings.SettingsNetwork.networkDisclaimer": "Network parameters from your container/system should be used instead of these settings. See the {docs} for more information.",
"components.Settings.SettingsNetwork.networksettings": "Network Settings",
@@ -1216,7 +1213,7 @@
"components.Setup.librarieserror": "Validation failed. Please toggle the libraries again to continue.",
"components.Setup.servertype": "Choose Server Type",
"components.Setup.setup": "Setup",
"components.Setup.signin": "Sign In",
"components.Setup.signin": "Sign in to your account",
"components.Setup.signinMessage": "Get started by signing in",
"components.Setup.signinWithEmby": "Enter your Emby details",
"components.Setup.signinWithJellyfin": "Enter your Jellyfin details",

View File

@@ -99,6 +99,14 @@ const loadLocaleData = (locale: AvailableLocale): Promise<any> => {
}
};
const API_BASE = process.env.NEXT_PUBLIC_BASE_PATH || '';
axios.interceptors.request.use((config) => {
if (config.url?.startsWith('/')) {
config.url = `${API_BASE}${config.url}`;
}
return config;
});
// Custom types so we can correctly type our GetInitialProps function
// with our combined user prop
// This is specific to _app.tsx. Other pages will not need to do this!
@@ -185,9 +193,9 @@ const CoreApp: Omit<NextAppComponentType, 'origGetInitialProps'> = ({
return (
<SWRConfig
value={{
fetcher: (url) => axios.get(url).then((res) => res.data),
fetcher: async (url) => axios.get(url).then((res) => res.data),
fallback: {
'/api/v1/auth/me': user,
[`${API_BASE}/api/v1/auth/me`]: user,
},
}}
>
@@ -256,7 +264,7 @@ CoreApp.getInitialProps = async (initialProps) => {
const response = await axios.get<PublicSettingsResponse>(
`http://${process.env.HOST || 'localhost'}:${
process.env.PORT || 5055
}/api/v1/settings/public`
}${API_BASE}/api/v1/settings/public`
);
currentSettings = response.data;
@@ -266,7 +274,7 @@ CoreApp.getInitialProps = async (initialProps) => {
if (!initialized) {
if (!router.pathname.match(/(setup|login\/plex)/)) {
ctx.res.writeHead(307, {
Location: '/setup',
Location: `${API_BASE}/setup`,
});
ctx.res.end();
}
@@ -276,7 +284,7 @@ CoreApp.getInitialProps = async (initialProps) => {
const response = await axios.get<User>(
`http://${process.env.HOST || 'localhost'}:${
process.env.PORT || 5055
}/api/v1/auth/me`,
}${API_BASE}/api/v1/auth/me`,
{
headers:
ctx.req && ctx.req.headers.cookie
@@ -288,7 +296,7 @@ CoreApp.getInitialProps = async (initialProps) => {
if (router.pathname.match(/(setup|login)/)) {
ctx.res.writeHead(307, {
Location: '/',
Location: `/`,
});
ctx.res.end();
}
@@ -298,7 +306,7 @@ CoreApp.getInitialProps = async (initialProps) => {
// before anything actually renders
if (!router.pathname.match(/(login|setup|resetpassword)/)) {
ctx.res.writeHead(307, {
Location: '/login',
Location: `${API_BASE}/login`,
});
ctx.res.end();
}

View File

@@ -11,13 +11,15 @@ const CollectionPage: NextPage<CollectionPageProps> = ({ collection }) => {
return <CollectionDetails collection={collection} />;
};
const API_BASE = process.env.NEXT_PUBLIC_BASE_PATH || '';
export const getServerSideProps: GetServerSideProps<
CollectionPageProps
> = async (ctx) => {
const response = await axios.get<Collection>(
`http://${process.env.HOST || 'localhost'}:${
process.env.PORT || 5055
}/api/v1/collection/${ctx.query.collectionId}`,
}${API_BASE}/api/v1/collection/${ctx.query.collectionId}`,
{
headers: ctx.req?.headers?.cookie
? { cookie: ctx.req.headers.cookie }

View File

@@ -11,13 +11,15 @@ const MoviePage: NextPage<MoviePageProps> = ({ movie }) => {
return <MovieDetails movie={movie} />;
};
const API_BASE = process.env.NEXT_PUBLIC_BASE_PATH || '';
export const getServerSideProps: GetServerSideProps<MoviePageProps> = async (
ctx
) => {
const response = await axios.get<MovieDetailsType>(
`http://${process.env.HOST || 'localhost'}:${
process.env.PORT || 5055
}/api/v1/movie/${ctx.query.movieId}`,
}${API_BASE}/api/v1/movie/${ctx.query.movieId}`,
{
headers: ctx.req?.headers?.cookie
? { cookie: ctx.req.headers.cookie }

View File

@@ -0,0 +1,8 @@
import RequestList from '@app/components/RequestList';
import type { NextPage } from 'next';
const UserRequestsPage: NextPage = () => {
return <RequestList />;
};
export default UserRequestsPage;

View File

@@ -11,13 +11,15 @@ const TvPage: NextPage<TvPageProps> = ({ tv }) => {
return <TvDetails tv={tv} />;
};
const API_BASE = process.env.NEXT_PUBLIC_BASE_PATH || '';
export const getServerSideProps: GetServerSideProps<TvPageProps> = async (
ctx
) => {
const response = await axios.get<TvDetailsType>(
`http://${process.env.HOST || 'localhost'}:${
process.env.PORT || 5055
}/api/v1/tv/${ctx.query.tvId}`,
}${API_BASE}/api/v1/tv/${ctx.query.tvId}`,
{
headers: ctx.req?.headers?.cookie
? { cookie: ctx.req.headers.cookie }

View File

@@ -0,0 +1,4 @@
export const getBasedPath = (path: string) => {
const API_BASE = process.env.NEXT_PUBLIC_BASE_PATH || '';
return path.startsWith('/') && path !== '/' ? `${API_BASE}${path}` : path;
};