diff --git a/.all-contributorsrc b/.all-contributorsrc
index 91017932..3614dbd1 100644
--- a/.all-contributorsrc
+++ b/.all-contributorsrc
@@ -439,6 +439,15 @@
"contributions": [
"code"
]
+ },
+ {
+ "login": "M0NsTeRRR",
+ "name": "Ludovic Ortega",
+ "avatar_url": "https://avatars.githubusercontent.com/u/37785089?v=4",
+ "profile": "https://github.com/M0NsTeRRR",
+ "contributions": [
+ "security"
+ ]
}
]
}
diff --git a/README.md b/README.md
index f91fe384..fb6c8790 100644
--- a/README.md
+++ b/README.md
@@ -11,7 +11,7 @@
-
+
**Jellyseerr** is a free and open source software application for managing requests for your media library.
@@ -146,6 +146,7 @@ Thanks goes to these wonderful people from Overseerr ([emoji key](https://allcon
 Baraa 💻 |
 Francisco Sales 💻 |
 Oliver Laing 💻 |
+  Ludovic Ortega 🛡️ |
diff --git a/server/api/rating/rottentomatoes.ts b/server/api/rating/rottentomatoes.ts
index e86c2488..f4fbe12b 100644
--- a/server/api/rating/rottentomatoes.ts
+++ b/server/api/rating/rottentomatoes.ts
@@ -182,7 +182,7 @@ class RottenTomatoes extends ExternalAPI {
);
}
- if (!tvshow) {
+ if (!tvshow || !tvshow.rottenTomatoes) {
return null;
}
diff --git a/server/entity/Media.ts b/server/entity/Media.ts
index 4f64178a..de10cebc 100644
--- a/server/entity/Media.ts
+++ b/server/entity/Media.ts
@@ -231,7 +231,7 @@ class Media {
this.mediaUrl = `${jellyfinHost}/web/index.html#!/${pageName}?id=${this.jellyfinMediaId}&context=home&serverId=${serverId}`;
}
if (this.jellyfinMediaId4k) {
- this.mediaUrl4k = `${jellyfinHost}/web/index.html#!/${pageName}?id=${this.jellyfinMediaId}&context=home&serverId=${serverId}`;
+ this.mediaUrl4k = `${jellyfinHost}/web/index.html#!/${pageName}?id=${this.jellyfinMediaId4k}&context=home&serverId=${serverId}`;
}
}
}
diff --git a/server/routes/auth.ts b/server/routes/auth.ts
index 4e7f7727..560f04d5 100644
--- a/server/routes/auth.ts
+++ b/server/routes/auth.ts
@@ -262,8 +262,6 @@ authRoutes.post('/jellyfin', async (req, res, next) => {
urlBase: body.urlBase,
});
- const { externalHostname } = getSettings().jellyfin;
-
// Try to find deviceId that corresponds to jellyfin user, else generate a new one
let user = await userRepository.findOne({
where: { jellyfinUsername: body.username },
@@ -281,11 +279,6 @@ authRoutes.post('/jellyfin', async (req, res, next) => {
// First we need to attempt to log the user in to jellyfin
const jellyfinserver = new JellyfinAPI(hostname ?? '', undefined, deviceId);
- const jellyfinHost =
- externalHostname && externalHostname.length > 0
- ? externalHostname
- : hostname;
-
const ip = req.ip;
let clientIp;
@@ -336,7 +329,7 @@ authRoutes.post('/jellyfin', async (req, res, next) => {
jellyfinAuthToken: account.AccessToken,
permissions: Permission.ADMIN,
avatar: account.User.PrimaryImageTag
- ? `${jellyfinHost}/Users/${account.User.Id}/Images/Primary/?tag=${account.User.PrimaryImageTag}&quality=90`
+ ? `/Users/${account.User.Id}/Images/Primary/?tag=${account.User.PrimaryImageTag}&quality=90`
: gravatarUrl(body.email || account.User.Name, {
default: 'mm',
size: 200,
@@ -355,7 +348,7 @@ authRoutes.post('/jellyfin', async (req, res, next) => {
jellyfinAuthToken: account.AccessToken,
permissions: Permission.ADMIN,
avatar: account.User.PrimaryImageTag
- ? `${jellyfinHost}/Users/${account.User.Id}/Images/Primary/?tag=${account.User.PrimaryImageTag}&quality=90`
+ ? `/Users/${account.User.Id}/Images/Primary/?tag=${account.User.PrimaryImageTag}&quality=90`
: gravatarUrl(body.email || account.User.Name, {
default: 'mm',
size: 200,
@@ -410,7 +403,7 @@ authRoutes.post('/jellyfin', async (req, res, next) => {
);
// Update the users avatar with their jellyfin profile pic (incase it changed)
if (account.User.PrimaryImageTag) {
- const avatar = `${jellyfinHost}/Users/${account.User.Id}/Images/Primary/?tag=${account.User.PrimaryImageTag}&quality=90`;
+ const avatar = `/Users/${account.User.Id}/Images/Primary/?tag=${account.User.PrimaryImageTag}&quality=90`;
if (avatar !== user.avatar) {
const avatarProxy = new ImageProxy('avatar', '');
avatarProxy.clearCachedImage(user.avatar);
@@ -467,7 +460,7 @@ authRoutes.post('/jellyfin', async (req, res, next) => {
jellyfinDeviceId: deviceId,
permissions: settings.main.defaultPermissions,
avatar: account.User.PrimaryImageTag
- ? `${jellyfinHost}/Users/${account.User.Id}/Images/Primary/?tag=${account.User.PrimaryImageTag}&quality=90`
+ ? `/Users/${account.User.Id}/Images/Primary/?tag=${account.User.PrimaryImageTag}&quality=90`
: gravatarUrl(body.email || account.User.Name, {
default: 'mm',
size: 200,
diff --git a/server/routes/avatarproxy.ts b/server/routes/avatarproxy.ts
index 65638df2..e6f6f3b5 100644
--- a/server/routes/avatarproxy.ts
+++ b/server/routes/avatarproxy.ts
@@ -1,5 +1,8 @@
+import { MediaServerType } from '@server/constants/server';
import ImageProxy from '@server/lib/imageproxy';
+import { getSettings } from '@server/lib/settings';
import logger from '@server/logger';
+import { getHostname } from '@server/utils/getHostname';
import { Router } from 'express';
const router = Router();
@@ -7,9 +10,25 @@ const router = Router();
const avatarImageProxy = new ImageProxy('avatar', '');
// Proxy avatar images
router.get('/*', async (req, res) => {
- const imagePath = req.url.startsWith('/') ? req.url.slice(1) : req.url;
-
+ let imagePath = '';
try {
+ const jellyfinAvatar = req.url.match(
+ /(\/Users\/\w+\/Images\/Primary\/?\?tag=\w+&quality=90)$/
+ )?.[1];
+ if (!jellyfinAvatar) {
+ const mediaServerType = getSettings().main.mediaServerType;
+ throw new Error(
+ `Provided URL is not ${
+ mediaServerType === MediaServerType.JELLYFIN
+ ? 'a Jellyfin'
+ : 'an Emby'
+ } avatar.`
+ );
+ }
+
+ const imageUrl = new URL(jellyfinAvatar, getHostname());
+ imagePath = imageUrl.toString();
+
const imageData = await avatarImageProxy.getImage(imagePath);
res.writeHead(200, {
diff --git a/server/routes/settings/index.ts b/server/routes/settings/index.ts
index 30c854af..3d6b6b0d 100644
--- a/server/routes/settings/index.ts
+++ b/server/routes/settings/index.ts
@@ -377,11 +377,6 @@ settingsRoutes.get('/jellyfin/library', async (req, res, next) => {
settingsRoutes.get('/jellyfin/users', async (req, res) => {
const settings = getSettings();
- const { externalHostname } = settings.jellyfin;
- const jellyfinHost =
- externalHostname && externalHostname.length > 0
- ? externalHostname
- : getHostname();
const userRepository = getRepository(User);
const admin = await userRepository.findOneOrFail({
@@ -401,7 +396,7 @@ settingsRoutes.get('/jellyfin/users', async (req, res) => {
username: user.Name,
id: user.Id,
thumb: user.PrimaryImageTag
- ? `${jellyfinHost}/Users/${user.Id}/Images/Primary/?tag=${user.PrimaryImageTag}&quality=90`
+ ? `/Users/${user.Id}/Images/Primary/?tag=${user.PrimaryImageTag}&quality=90`
: gravatarUrl(user.Name, { default: 'mm', size: 200 }),
email: user.Name,
}));
diff --git a/server/routes/user/index.ts b/server/routes/user/index.ts
index f8a0d41a..83ad0910 100644
--- a/server/routes/user/index.ts
+++ b/server/routes/user/index.ts
@@ -516,12 +516,6 @@ router.post(
//const jellyfinUsersResponse = await jellyfinClient.getUsers();
const createdUsers: User[] = [];
- const { externalHostname } = getSettings().jellyfin;
-
- const jellyfinHost =
- externalHostname && externalHostname.length > 0
- ? externalHostname
- : hostname;
jellyfinClient.setUserId(admin.jellyfinUserId ?? '');
const jellyfinUsers = await jellyfinClient.getUsers();
@@ -546,7 +540,7 @@ router.post(
email: jellyfinUser?.Name,
permissions: settings.main.defaultPermissions,
avatar: jellyfinUser?.PrimaryImageTag
- ? `${jellyfinHost}/Users/${jellyfinUser.Id}/Images/Primary/?tag=${jellyfinUser.PrimaryImageTag}&quality=90`
+ ? `/Users/${jellyfinUser.Id}/Images/Primary/?tag=${jellyfinUser.PrimaryImageTag}&quality=90`
: gravatarUrl(jellyfinUser?.Name ?? '', {
default: 'mm',
size: 200,
diff --git a/src/components/Blacklist/index.tsx b/src/components/Blacklist/index.tsx
index 217f4cef..a752e95f 100644
--- a/src/components/Blacklist/index.tsx
+++ b/src/components/Blacklist/index.tsx
@@ -268,6 +268,7 @@ const BlacklistedItem = ({ item, revalidateList }: BlacklistedItemProps) => {
{title && title.backdropPath && (
{
className="relative h-auto w-12 flex-shrink-0 scale-100 transform-gpu overflow-hidden rounded-md transition duration-300 hover:scale-105"
>
{
{
{data.backdropPath && (
{
src;
+export type CachedImageProps = ImageProps & {
+ src: string;
+ type: 'tmdb' | 'avatar';
+};
+
/**
* The CachedImage component should be used wherever
* we want to offer the option to locally cache images.
**/
-const CachedImage = ({ src, ...props }: ImageProps) => {
+const CachedImage = ({ src, type, ...props }: CachedImageProps) => {
const { currentSettings } = useSettings();
- let imageUrl = src;
+ let imageUrl: string;
- if (typeof imageUrl === 'string' && imageUrl.startsWith('http')) {
- const parsedUrl = new URL(imageUrl);
-
- if (parsedUrl.host === 'image.tmdb.org') {
- if (currentSettings.cacheImages)
- imageUrl = imageUrl.replace('https://image.tmdb.org', '/imageproxy');
- } else if (parsedUrl.host !== 'gravatar.com') {
- imageUrl = '/avatarproxy/' + imageUrl;
- }
+ if (type === 'tmdb') {
+ // tmdb stuff
+ imageUrl =
+ currentSettings.cacheImages && !src.startsWith('/')
+ ? src.replace(/^https:\/\/image\.tmdb\.org\//, '/imageproxy/')
+ : src;
+ } else if (type === 'avatar') {
+ // jellyfin avatar (in any)
+ const jellyfinAvatar = src.match(
+ /(\/Users\/\w+\/Images\/Primary\/?\?tag=\w+&quality=90)$/
+ )?.[1];
+ imageUrl = jellyfinAvatar ? `/avatarproxy` + jellyfinAvatar : src;
+ } else {
+ return null;
}
return ;
diff --git a/src/components/Common/ImageFader/index.tsx b/src/components/Common/ImageFader/index.tsx
index 20ccb698..930471e9 100644
--- a/src/components/Common/ImageFader/index.tsx
+++ b/src/components/Common/ImageFader/index.tsx
@@ -61,6 +61,7 @@ const ImageFader: ForwardRefRenderFunction = (
{...props}
>
(
{backdrop && (
{
>
{
tabIndex={0}
>
{
{data.backdropPath && (
{
{
className="group ml-1 inline-flex h-full items-center xl:ml-1.5"
>
{
{title.backdropPath && (
{
className="relative h-auto w-12 flex-shrink-0 scale-100 transform-gpu overflow-hidden rounded-md transition duration-300 hover:scale-105"
>
{
className="group flex items-center truncate"
>
{
data-testid="user-menu"
>
{
{
{data.backdropPath && (
{
{
{
{data.profilePath && (
{
>
{
{title.backdropPath && (
{
>
{
className="w-20 flex-shrink-0 scale-100 transform-gpu cursor-pointer overflow-hidden rounded-md shadow-sm transition duration-300 hover:scale-105 hover:shadow-md sm:w-28"
>
{
{title.backdropPath && (
{
className="relative h-auto w-12 flex-shrink-0 scale-100 transform-gpu overflow-hidden rounded-md transition duration-300 hover:scale-105"
>
{
>
{
>
{
{data.backdropPath && (
{
= ({
{
className="h-10 w-10 flex-shrink-0"
>
{
{
+ if (typeof window !== 'undefined') {
+ const match = document.cookie.match(/XSRF-TOKEN=([^;]+)/);
+ return match ? decodeURIComponent(match[1]) : null;
+ }
+ return null;
+};
+
+const isSameOrigin = (url: RequestInfo | URL): boolean => {
+ const parsedUrl = new URL(
+ url instanceof Request ? url.url : url.toString(),
+ window.location.origin
+ );
+ return parsedUrl.origin === window.location.origin;
+};
+
+// We are using a custom fetch implementation to add the X-XSRF-TOKEN heade
+// to all requests. This is required when CSRF protection is enabled.
+if (typeof window !== 'undefined') {
+ const originalFetch: typeof fetch = window.fetch;
+
+ (window as typeof globalThis).fetch = async (
+ input: RequestInfo | URL,
+ init?: RequestInit
+ ): Promise => {
+ if (!isSameOrigin(input)) {
+ return originalFetch(input, init);
+ }
+
+ const csrfToken = getCsrfToken();
+
+ const headers = {
+ ...(init?.headers || {}),
+ ...(csrfToken ? { 'XSRF-TOKEN': csrfToken } : {}),
+ };
+
+ const newInit: RequestInit = {
+ ...init,
+ headers,
+ };
+
+ return originalFetch(input, newInit);
+ };
+}
+
+export {};
|