From 20863d4a8dabe78fb5c52995b5bcb2da557a804e Mon Sep 17 00:00:00 2001 From: Gauthier Date: Tue, 11 Jun 2024 13:23:35 +0200 Subject: [PATCH 1/8] fix: empty email in user settings (#807) Email is mandatory for every user and required during the setup of Jellyseerr, but it is possible to set it empty afterwards in the user settings. When the email is empty, users are not able to connect to Jellyseer. This PR makes the email field mandatory in the user settings. fix #803 --- .../UserProfile/UserSettings/UserGeneralSettings/index.tsx | 5 +++++ src/i18n/locale/en.json | 2 ++ 2 files changed, 7 insertions(+) diff --git a/src/components/UserProfile/UserSettings/UserGeneralSettings/index.tsx b/src/components/UserProfile/UserSettings/UserGeneralSettings/index.tsx index 960746adf..b6371e7d4 100644 --- a/src/components/UserProfile/UserSettings/UserGeneralSettings/index.tsx +++ b/src/components/UserProfile/UserSettings/UserGeneralSettings/index.tsx @@ -53,6 +53,8 @@ const messages = defineMessages({ discordId: 'Discord User ID', discordIdTip: 'The multi-digit ID number associated with your Discord user account', + validationemailrequired: 'Email required', + validationemailformat: 'Valid email required', validationDiscordId: 'You must provide a valid Discord user ID', plexwatchlistsyncmovies: 'Auto-Request Movies', plexwatchlistsyncmoviestip: @@ -88,6 +90,9 @@ const UserGeneralSettings = () => { ); const UserGeneralSettingsSchema = Yup.object().shape({ + email: Yup.string() + .email(intl.formatMessage(messages.validationemailformat)) + .required(intl.formatMessage(messages.validationemailrequired)), discordId: Yup.string() .nullable() .matches(/^\d{17,19}$/, intl.formatMessage(messages.validationDiscordId)), diff --git a/src/i18n/locale/en.json b/src/i18n/locale/en.json index 367fabc37..0b7a8cbff 100644 --- a/src/i18n/locale/en.json +++ b/src/i18n/locale/en.json @@ -1177,6 +1177,8 @@ "components.UserProfile.UserSettings.UserGeneralSettings.toastSettingsSuccess": "Settings saved successfully!", "components.UserProfile.UserSettings.UserGeneralSettings.user": "User", "components.UserProfile.UserSettings.UserGeneralSettings.validationDiscordId": "You must provide a valid Discord user ID", + "components.UserProfile.UserSettings.UserGeneralSettings.validationemailformat": "Valid email required", + "components.UserProfile.UserSettings.UserGeneralSettings.validationemailrequired": "Email required", "components.UserProfile.UserSettings.UserNotificationSettings.deviceDefault": "Device Default", "components.UserProfile.UserSettings.UserNotificationSettings.discordId": "User ID", "components.UserProfile.UserSettings.UserNotificationSettings.discordIdTip": "The multi-digit ID number associated with your user account", From d31a2c37e639c1126b446277fa5d666d8102fef5 Mon Sep 17 00:00:00 2001 From: Gauthier Date: Tue, 11 Jun 2024 14:58:48 +0200 Subject: [PATCH 2/8] fix(jellyfinscanner): assign only 4k available badge for a 4k request instead of both badges (#805) When you have a 4k server setup, and request a 4k item, when it becomes available it also sets the normal item as available thus not allowing the user to request for the normal item --- server/lib/scanners/jellyfin/index.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/server/lib/scanners/jellyfin/index.ts b/server/lib/scanners/jellyfin/index.ts index f5b0f66a2..8007e6ef3 100644 --- a/server/lib/scanners/jellyfin/index.ts +++ b/server/lib/scanners/jellyfin/index.ts @@ -83,13 +83,17 @@ class JellyfinScanner { } const has4k = metadata.MediaSources?.some((MediaSource) => { - return MediaSource.MediaStreams.some((MediaStream) => { + return MediaSource.MediaStreams.filter( + (MediaStream) => MediaStream.Type === 'Video' + ).some((MediaStream) => { return (MediaStream.Width ?? 0) > 2000; }); }); const hasOtherResolution = metadata.MediaSources?.some((MediaSource) => { - return MediaSource.MediaStreams.some((MediaStream) => { + return MediaSource.MediaStreams.filter( + (MediaStream) => MediaStream.Type === 'Video' + ).some((MediaStream) => { return (MediaStream.Width ?? 0) <= 2000; }); }); From f52939e4cdcbee94fc35165f613f6b3e21599e3c Mon Sep 17 00:00:00 2001 From: Gauthier Date: Tue, 11 Jun 2024 16:47:02 +0200 Subject: [PATCH 3/8] fix: remove the settings button of media when useless (#809) After the Media Availability Sync job rund on deleted media, the setting button is still visible even if neither the media file nor the media request no longer exists. This PR hides this button when it's no longer the case --- src/components/MovieDetails/index.tsx | 59 +++++++++++++++------------ 1 file changed, 32 insertions(+), 27 deletions(-) diff --git a/src/components/MovieDetails/index.tsx b/src/components/MovieDetails/index.tsx index b7dc59172..4ed69b6b6 100644 --- a/src/components/MovieDetails/index.tsx +++ b/src/components/MovieDetails/index.tsx @@ -434,33 +434,38 @@ const MovieDetails = ({ movie }: MovieDetailsProps) => { )} - {hasPermission(Permission.MANAGE_REQUESTS) && data.mediaInfo && ( - - - - )} + {hasPermission(Permission.MANAGE_REQUESTS) && + data.mediaInfo && + (data.mediaInfo.jellyfinMediaId || + data.mediaInfo.jellyfinMediaId4k || + data.mediaInfo.status !== MediaStatus.UNKNOWN || + data.mediaInfo.status4k !== MediaStatus.UNKNOWN) && ( + + + + )}
From 46ee8a4ca13b026bd929b4027eb001cc74064bb8 Mon Sep 17 00:00:00 2001 From: Gauthier Date: Tue, 11 Jun 2024 23:56:10 +0200 Subject: [PATCH 4/8] fix(api): add DNS caching (#810) fix #387 #657 #728 --- package.json | 1 + server/index.ts | 11 +++++++++++ yarn.lock | 5 +++++ 3 files changed, 17 insertions(+) diff --git a/package.json b/package.json index 97c025503..32b66e19d 100644 --- a/package.json +++ b/package.json @@ -44,6 +44,7 @@ "axios-rate-limit": "1.3.0", "bcrypt": "5.1.0", "bowser": "2.11.0", + "cacheable-lookup": "^7.0.0", "connect-typeorm": "1.1.4", "cookie-parser": "1.4.6", "copy-to-clipboard": "3.3.3", diff --git a/server/index.ts b/server/index.ts index 477864c26..b62080778 100644 --- a/server/index.ts +++ b/server/index.ts @@ -23,6 +23,7 @@ import imageproxy from '@server/routes/imageproxy'; import { getAppVersion } from '@server/utils/appVersion'; import restartFlag from '@server/utils/restartFlag'; import { getClientIp } from '@supercharge/request-ip'; +import type CacheableLookupType from 'cacheable-lookup'; import { TypeormStore } from 'connect-typeorm/out'; import cookieParser from 'cookie-parser'; import csurf from 'csurf'; @@ -32,10 +33,14 @@ import * as OpenApiValidator from 'express-openapi-validator'; import type { Store } from 'express-session'; import session from 'express-session'; import next from 'next'; +import http from 'node:http'; +import https from 'node:https'; import path from 'path'; import swaggerUi from 'swagger-ui-express'; import YAML from 'yamljs'; +const _importDynamic = new Function('modulePath', 'return import(modulePath)'); + const API_SPEC_PATH = path.join(__dirname, '../overseerr-api.yml'); logger.info(`Starting Overseerr version ${getAppVersion()}`); @@ -46,6 +51,12 @@ const handle = app.getRequestHandler(); app .prepare() .then(async () => { + const CacheableLookup = (await _importDynamic('cacheable-lookup')) + .default as typeof CacheableLookupType; + const cacheable = new CacheableLookup(); + cacheable.install(http.globalAgent); + cacheable.install(https.globalAgent); + const dbConnection = await dataSource.initialize(); // Run migrations in production diff --git a/yarn.lock b/yarn.lock index 09b5a3ca0..b94855109 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5033,6 +5033,11 @@ cacache@^16.0.0, cacache@^16.1.0, cacache@^16.1.3: tar "^6.1.11" unique-filename "^2.0.0" +cacheable-lookup@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/cacheable-lookup/-/cacheable-lookup-7.0.0.tgz#3476a8215d046e5a3202a9209dd13fec1f933a27" + integrity sha512-+qJyx4xiKra8mZrcwhjMRMUhD5NR1R8esPkzIYxX96JiecFoxAXFuz/GpR3+ev4PE1WamHip78wV0vcmPQtp8w== + cachedir@2.3.0, cachedir@^2.3.0: version "2.3.0" resolved "https://registry.yarnpkg.com/cachedir/-/cachedir-2.3.0.tgz#0c75892a052198f0b21c7c1804d8331edfcae0e8" From 6eb88f8674d4ac1dd40424aa485aea18aad28211 Mon Sep 17 00:00:00 2001 From: Fallenbagel <98979876+Fallenbagel@users.noreply.github.com> Date: Wed, 12 Jun 2024 10:49:15 +0500 Subject: [PATCH 5/8] ci: temporarily disable snap release builds (#811) --- .github/workflows/release.yml | 108 +++++++++++++++++----------------- 1 file changed, 54 insertions(+), 54 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 1eeacd77d..8f838a52c 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -35,60 +35,60 @@ jobs: DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }} run: npx semantic-release - build-snap: - name: Build Snap Package (${{ matrix.architecture }}) - needs: semantic-release - runs-on: ubuntu-22.04 - strategy: - fail-fast: false - matrix: - architecture: - - amd64 - - arm64 - - armhf - steps: - - name: Checkout Code - uses: actions/checkout@v4 - with: - fetch-depth: 0 - - name: Switch to main branch - run: git checkout main - - name: Pull latest changes - run: git pull - - name: Prepare - id: prepare - run: | - git fetch --prune --tags - if [[ $GITHUB_REF == refs/tags/* || $GITHUB_REF == refs/heads/master ]]; then - echo "RELEASE=stable" >> $GITHUB_OUTPUT - else - echo "RELEASE=edge" >> $GITHUB_OUTPUT - fi - - name: Set Up QEMU - uses: docker/setup-qemu-action@v3 - with: - image: tonistiigi/binfmt@sha256:df15403e06a03c2f461c1f7938b171fda34a5849eb63a70e2a2109ed5a778bde - - name: Build Snap Package - uses: diddlesnaps/snapcraft-multiarch-action@v1 - id: build - with: - architecture: ${{ matrix.architecture }} - - name: Upload Snap Package - uses: actions/upload-artifact@v4 - with: - name: jellyseerr-snap-package-${{ matrix.architecture }} - path: ${{ steps.build.outputs.snap }} - - name: Review Snap Package - uses: diddlesnaps/snapcraft-review-tools-action@v1 - with: - snap: ${{ steps.build.outputs.snap }} - - name: Publish Snap Package - uses: snapcore/action-publish@v1 - env: - SNAPCRAFT_STORE_CREDENTIALS: ${{ secrets.SNAP_LOGIN }} - with: - snap: ${{ steps.build.outputs.snap }} - release: ${{ steps.prepare.outputs.RELEASE }} + # build-snap: + # name: Build Snap Package (${{ matrix.architecture }}) + # needs: semantic-release + # runs-on: ubuntu-22.04 + # strategy: + # fail-fast: false + # matrix: + # architecture: + # - amd64 + # - arm64 + # - armhf + # steps: + # - name: Checkout Code + # uses: actions/checkout@v4 + # with: + # fetch-depth: 0 + # - name: Switch to main branch + # run: git checkout main + # - name: Pull latest changes + # run: git pull + # - name: Prepare + # id: prepare + # run: | + # git fetch --prune --tags + # if [[ $GITHUB_REF == refs/tags/* || $GITHUB_REF == refs/heads/master ]]; then + # echo "RELEASE=stable" >> $GITHUB_OUTPUT + # else + # echo "RELEASE=edge" >> $GITHUB_OUTPUT + # fi + # - name: Set Up QEMU + # uses: docker/setup-qemu-action@v3 + # with: + # image: tonistiigi/binfmt@sha256:df15403e06a03c2f461c1f7938b171fda34a5849eb63a70e2a2109ed5a778bde + # - name: Build Snap Package + # uses: diddlesnaps/snapcraft-multiarch-action@v1 + # id: build + # with: + # architecture: ${{ matrix.architecture }} + # - name: Upload Snap Package + # uses: actions/upload-artifact@v4 + # with: + # name: jellyseerr-snap-package-${{ matrix.architecture }} + # path: ${{ steps.build.outputs.snap }} + # - name: Review Snap Package + # uses: diddlesnaps/snapcraft-review-tools-action@v1 + # with: + # snap: ${{ steps.build.outputs.snap }} + # - name: Publish Snap Package + # uses: snapcore/action-publish@v1 + # env: + # SNAPCRAFT_STORE_CREDENTIALS: ${{ secrets.SNAP_LOGIN }} + # with: + # snap: ${{ steps.build.outputs.snap }} + # release: ${{ steps.prepare.outputs.RELEASE }} discord: name: Send Discord Notification From 9aeb3604e6498c388df1d30dd0b613ba84160fc0 Mon Sep 17 00:00:00 2001 From: Fallenbagel <98979876+Fallenbagel@users.noreply.github.com> Date: Wed, 12 Jun 2024 18:50:00 +0500 Subject: [PATCH 6/8] fix(auth): validation of ipv6/ipv4 (#812) validation for ipv6 was sort of broken where for example `::1` was being sent as `1`, therefore, logins were broken. This PR fixes it by using nodejs `net.isIPv4()` & `net.isIPv6` for ipv4 and ipv6 validation. possibly related to and fixes #795 --- server/routes/auth.ts | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/server/routes/auth.ts b/server/routes/auth.ts index 82c34b153..52c63ff29 100644 --- a/server/routes/auth.ts +++ b/server/routes/auth.ts @@ -14,6 +14,7 @@ import { ApiError } from '@server/types/error'; import * as EmailValidator from 'email-validator'; import { Router } from 'express'; import gravatarUrl from 'gravatar-url'; +import net from 'net'; const authRoutes = Router(); @@ -271,11 +272,21 @@ authRoutes.post('/jellyfin', async (req, res, next) => { ? jellyfinHost.slice(0, -1) : jellyfinHost; - const ip = req.ip ? req.ip.split(':').reverse()[0] : undefined; + const ip = req.ip; + let clientIp; + + if (ip) { + if (net.isIPv4(ip)) { + clientIp = ip; + } else if (net.isIPv6(ip)) { + clientIp = ip.startsWith('::ffff:') ? ip.substring(7) : ip; + } + } + const account = await jellyfinserver.login( body.username, body.password, - ip + clientIp ); // Next let's see if the user already exists From b5a069901a9545772deaa9c491f2075261da0189 Mon Sep 17 00:00:00 2001 From: Fallenbagel <98979876+Fallenbagel@users.noreply.github.com> Date: Thu, 13 Jun 2024 04:53:12 +0500 Subject: [PATCH 7/8] fix: bypass cache-able lookups when resolving localhost (#813) * fix: bypass cache-able lookups when resolving localhost * fix: bypass cacheable-lookup when resolving localhost --------- Co-authored-by: Gauthier --- server/index.ts | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/server/index.ts b/server/index.ts index b62080778..a9a746562 100644 --- a/server/index.ts +++ b/server/index.ts @@ -27,6 +27,7 @@ import type CacheableLookupType from 'cacheable-lookup'; import { TypeormStore } from 'connect-typeorm/out'; import cookieParser from 'cookie-parser'; import csurf from 'csurf'; +import { lookup } from 'dns'; import type { NextFunction, Request, Response } from 'express'; import express from 'express'; import * as OpenApiValidator from 'express-openapi-validator'; @@ -54,6 +55,19 @@ app const CacheableLookup = (await _importDynamic('cacheable-lookup')) .default as typeof CacheableLookupType; const cacheable = new CacheableLookup(); + + const originalLookup = cacheable.lookup; + + // if hostname is localhost use dns.lookup instead of cacheable-lookup + cacheable.lookup = (...args: any) => { + const [hostname] = args; + if (hostname === 'localhost') { + lookup(...(args as Parameters)); + } else { + originalLookup(...(args as Parameters)); + } + }; + cacheable.install(http.globalAgent); cacheable.install(https.globalAgent); From a9741fa36d06710aa00d28db3dd2c29f2b0973d3 Mon Sep 17 00:00:00 2001 From: Fallenbagel <98979876+Fallenbagel@users.noreply.github.com> Date: Thu, 13 Jun 2024 14:16:07 +0500 Subject: [PATCH 8/8] fix(auth): improve login resilience with headerless fallback authentication (#814) adds fallback to authenticate without headers to ensure and improve resilience across different browsers and client configurations. --- server/api/jellyfin.ts | 30 ++++++++++++++++++------------ 1 file changed, 18 insertions(+), 12 deletions(-) diff --git a/server/api/jellyfin.ts b/server/api/jellyfin.ts index f23e9aceb..81b505f11 100644 --- a/server/api/jellyfin.ts +++ b/server/api/jellyfin.ts @@ -126,25 +126,31 @@ class JellyfinAPI extends ExternalAPI { Password?: string, ClientIP?: string ): Promise { - try { - const headers = ClientIP - ? { - 'X-Forwarded-For': ClientIP, - } - : {}; + const authenticate = async (useHeaders: boolean) => { + const headers = + useHeaders && ClientIP ? { 'X-Forwarded-For': ClientIP } : {}; - const authResponse = await this.post( + return this.post( '/Users/AuthenticateByName', { - Username: Username, + Username, Pw: Password, }, - { - headers: headers, - } + { headers } ); + }; - return authResponse; + try { + return await authenticate(true); + } catch (e) { + logger.debug(`Failed to authenticate with headers: ${e.message}`, { + label: 'Jellyfin API', + ip: ClientIP, + }); + } + + try { + return await authenticate(false); } catch (e) { const status = e.response?.status;