mirror of
https://github.com/fallenbagel/jellyseerr.git
synced 2025-12-24 02:39:18 -05:00
Compare commits
6 Commits
preview-mo
...
test-disab
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
acc3599f1b | ||
|
|
64f4610b9f | ||
|
|
2d3b777daf | ||
|
|
cf59102ef9 | ||
|
|
ca838a00fa | ||
|
|
f2ed101e52 |
@@ -18,7 +18,7 @@ config/logs/*
|
||||
config/*.json
|
||||
dist
|
||||
Dockerfile*
|
||||
docker-compose.yml
|
||||
compose.yaml
|
||||
docs
|
||||
LICENSE
|
||||
node_modules
|
||||
|
||||
2
.gitattributes
vendored
2
.gitattributes
vendored
@@ -40,7 +40,7 @@ docs export-ignore
|
||||
.all-contributorsrc export-ignore
|
||||
.editorconfig export-ignore
|
||||
Dockerfile.local export-ignore
|
||||
docker-compose.yml export-ignore
|
||||
compose.yaml export-ignore
|
||||
stylelint.config.js export-ignore
|
||||
|
||||
public/os_logo_filled.png export-ignore
|
||||
|
||||
@@ -52,7 +52,7 @@ All help is welcome and greatly appreciated! If you would like to contribute to
|
||||
pnpm dev
|
||||
```
|
||||
|
||||
- Alternatively, you can use [Docker](https://www.docker.com/) with `docker-compose up -d`. This method does not require installing NodeJS or Yarn on your machine directly.
|
||||
- Alternatively, you can use [Docker](https://www.docker.com/) with `docker compose up -d`. This method does not require installing NodeJS or Yarn on your machine directly.
|
||||
|
||||
5. Create your patch and test your changes.
|
||||
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
version: '3'
|
||||
services:
|
||||
jellyseerr:
|
||||
build:
|
||||
@@ -190,7 +190,7 @@ Caddy will automatically obtain and renew SSL certificates for your domain.
|
||||
|
||||
## Traefik (v2)
|
||||
|
||||
Add the following labels to the Jellyseerr service in your `docker-compose.yml` file:
|
||||
Add the following labels to the Jellyseerr service in your `compose.yaml` file:
|
||||
|
||||
```yaml
|
||||
labels:
|
||||
|
||||
@@ -71,7 +71,7 @@ You could also use [diun](https://github.com/crazy-max/diun) to receive notifica
|
||||
For details on how to use Docker Compose, please [review the official Compose documentation](https://docs.docker.com/compose/reference/).
|
||||
|
||||
#### Installation:
|
||||
Define the `jellyseerr` service in your `docker-compose.yml` as follows:
|
||||
Define the `jellyseerr` service in your `compose.yaml` as follows:
|
||||
```yaml
|
||||
---
|
||||
services:
|
||||
@@ -94,17 +94,17 @@ If you are using emby, make sure to set the `JELLYFIN_TYPE` environment variable
|
||||
|
||||
Then, start all services defined in the Compose file:
|
||||
```bash
|
||||
docker-compose up -d
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
#### Updating:
|
||||
Pull the latest image:
|
||||
```bash
|
||||
docker-compose pull jellyseerr
|
||||
docker compose pull jellyseerr
|
||||
```
|
||||
Then, restart all services defined in the Compose file:
|
||||
```bash
|
||||
docker-compose up -d
|
||||
docker compose up -d
|
||||
```
|
||||
:::tip
|
||||
You may alternatively use a third-party mechanism like [dockge](https://github.com/louislam/dockge) to manage your docker compose files.
|
||||
|
||||
2
next-env.d.ts
vendored
2
next-env.d.ts
vendored
@@ -2,4 +2,4 @@
|
||||
/// <reference types="next/image-types/global" />
|
||||
|
||||
// NOTE: This file should not be edited
|
||||
// see https://nextjs.org/docs/basic-features/typescript for more information.
|
||||
// see https://nextjs.org/docs/pages/building-your-application/configuring/typescript for more information.
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { RateLimitOptions } from '@server/utils/rateLimit';
|
||||
import rateLimit from '@server/utils/rateLimit';
|
||||
// import rateLimit from '@server/utils/rateLimit';
|
||||
import type NodeCache from 'node-cache';
|
||||
|
||||
// 5 minute default TTL (in seconds)
|
||||
@@ -26,19 +26,34 @@ class ExternalAPI {
|
||||
params: Record<string, string> = {},
|
||||
options: ExternalAPIOptions = {}
|
||||
) {
|
||||
if (options.rateLimit) {
|
||||
this.fetch = rateLimit(fetch, options.rateLimit);
|
||||
} else {
|
||||
this.fetch = fetch;
|
||||
// if (options.rateLimit) {
|
||||
// this.fetch = rateLimit(fetch, options.rateLimit);
|
||||
// } else {
|
||||
// this.fetch = fetch;
|
||||
// }
|
||||
this.fetch = fetch;
|
||||
|
||||
const url = new URL(baseUrl);
|
||||
|
||||
this.defaultHeaders = {
|
||||
'Content-Type': 'application/json',
|
||||
Accept: 'application/json',
|
||||
...((url.username || url.password) && {
|
||||
Authorization: `Basic ${Buffer.from(
|
||||
`${url.username}:${url.password}`
|
||||
).toString('base64')}`,
|
||||
}),
|
||||
...options.headers,
|
||||
};
|
||||
|
||||
if (url.username || url.password) {
|
||||
url.username = '';
|
||||
url.password = '';
|
||||
baseUrl = url.toString();
|
||||
}
|
||||
|
||||
this.baseUrl = baseUrl;
|
||||
this.params = params;
|
||||
this.defaultHeaders = {
|
||||
'Content-Type': 'application/json',
|
||||
Accept: 'application/json',
|
||||
...options.headers,
|
||||
};
|
||||
this.cache = options.nodeCache;
|
||||
}
|
||||
|
||||
|
||||
@@ -410,7 +410,7 @@ class JellyfinAPI extends ExternalAPI {
|
||||
).AccessToken;
|
||||
} catch (e) {
|
||||
logger.error(
|
||||
`Something went wrong while creating an API key the Jellyfin server: ${e.message}`,
|
||||
`Something went wrong while creating an API key from the Jellyfin server: ${e.message}`,
|
||||
{ label: 'Jellyfin API' }
|
||||
);
|
||||
|
||||
|
||||
@@ -180,7 +180,7 @@ class PlexAPI {
|
||||
settings.plex.libraries = [];
|
||||
}
|
||||
|
||||
settings.save();
|
||||
await settings.save();
|
||||
}
|
||||
|
||||
public async getLibraryContents(
|
||||
|
||||
@@ -23,6 +23,7 @@ import avatarproxy from '@server/routes/avatarproxy';
|
||||
import imageproxy from '@server/routes/imageproxy';
|
||||
import { appDataPermissions } from '@server/utils/appDataVolume';
|
||||
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 { TypeormStore } from 'connect-typeorm/out';
|
||||
@@ -38,7 +39,6 @@ import dns from 'node:dns';
|
||||
import net from 'node:net';
|
||||
import path from 'path';
|
||||
import swaggerUi from 'swagger-ui-express';
|
||||
import { ProxyAgent, setGlobalDispatcher } from 'undici';
|
||||
import YAML from 'yamljs';
|
||||
|
||||
if (process.env.forceIpv4First === 'true') {
|
||||
@@ -76,8 +76,8 @@ app
|
||||
restartFlag.initializeSettings(settings.main);
|
||||
|
||||
// Register HTTP proxy
|
||||
if (settings.main.httpProxy) {
|
||||
setGlobalDispatcher(new ProxyAgent(settings.main.httpProxy));
|
||||
if (settings.main.proxy.enabled) {
|
||||
await createCustomProxyAgent(settings.main.proxy);
|
||||
}
|
||||
|
||||
// Migrate library types
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import logger from '@server/logger';
|
||||
import type { RateLimitOptions } from '@server/utils/rateLimit';
|
||||
import rateLimit from '@server/utils/rateLimit';
|
||||
// import rateLimit from '@server/utils/rateLimit';
|
||||
import { createHash } from 'crypto';
|
||||
import { promises } from 'fs';
|
||||
import mime from 'mime/lite';
|
||||
@@ -150,13 +150,14 @@ class ImageProxy {
|
||||
this.baseUrl = baseUrl;
|
||||
this.key = key;
|
||||
|
||||
if (options.rateLimitOptions) {
|
||||
this.fetch = rateLimit(fetch, {
|
||||
...options.rateLimitOptions,
|
||||
});
|
||||
} else {
|
||||
this.fetch = fetch;
|
||||
}
|
||||
// if (options.rateLimitOptions) {
|
||||
// this.fetch = rateLimit(fetch, {
|
||||
// ...options.rateLimitOptions,
|
||||
// });
|
||||
// } else {
|
||||
// this.fetch = fetch;
|
||||
// }
|
||||
this.fetch = fetch;
|
||||
this.headers = options.headers || null;
|
||||
}
|
||||
|
||||
|
||||
@@ -129,7 +129,7 @@ class PlexScanner
|
||||
});
|
||||
|
||||
settings.plex.libraries = newLibraries;
|
||||
settings.save();
|
||||
await settings.save();
|
||||
}
|
||||
} else {
|
||||
for (const library of this.libraries) {
|
||||
|
||||
@@ -2,7 +2,7 @@ import { MediaServerType } from '@server/constants/server';
|
||||
import { Permission } from '@server/lib/permissions';
|
||||
import { runMigrations } from '@server/lib/settings/migrator';
|
||||
import { randomUUID } from 'crypto';
|
||||
import fs from 'fs';
|
||||
import fs from 'fs/promises';
|
||||
import { merge } from 'lodash';
|
||||
import path from 'path';
|
||||
import webpush from 'web-push';
|
||||
@@ -99,6 +99,17 @@ interface Quota {
|
||||
quotaDays?: number;
|
||||
}
|
||||
|
||||
export interface ProxySettings {
|
||||
enabled: boolean;
|
||||
hostname: string;
|
||||
port: number;
|
||||
useSsl: boolean;
|
||||
user: string;
|
||||
password: string;
|
||||
bypassFilter: string;
|
||||
bypassLocalAddresses: boolean;
|
||||
}
|
||||
|
||||
export interface MainSettings {
|
||||
apiKey: string;
|
||||
applicationTitle: string;
|
||||
@@ -119,7 +130,7 @@ export interface MainSettings {
|
||||
mediaServerType: number;
|
||||
partialRequestsEnabled: boolean;
|
||||
locale: string;
|
||||
httpProxy: string;
|
||||
proxy: ProxySettings;
|
||||
}
|
||||
|
||||
interface PublicSettings {
|
||||
@@ -326,7 +337,16 @@ class Settings {
|
||||
mediaServerType: MediaServerType.NOT_CONFIGURED,
|
||||
partialRequestsEnabled: true,
|
||||
locale: 'en',
|
||||
httpProxy: '',
|
||||
proxy: {
|
||||
enabled: false,
|
||||
hostname: '',
|
||||
port: 8080,
|
||||
useSsl: false,
|
||||
user: '',
|
||||
password: '',
|
||||
bypassFilter: '',
|
||||
bypassLocalAddresses: true,
|
||||
},
|
||||
},
|
||||
plex: {
|
||||
name: '',
|
||||
@@ -481,10 +501,6 @@ class Settings {
|
||||
}
|
||||
|
||||
get main(): MainSettings {
|
||||
if (!this.data.main.apiKey) {
|
||||
this.data.main.apiKey = this.generateApiKey();
|
||||
this.save();
|
||||
}
|
||||
return this.data.main;
|
||||
}
|
||||
|
||||
@@ -586,29 +602,20 @@ class Settings {
|
||||
}
|
||||
|
||||
get clientId(): string {
|
||||
if (!this.data.clientId) {
|
||||
this.data.clientId = randomUUID();
|
||||
this.save();
|
||||
}
|
||||
|
||||
return this.data.clientId;
|
||||
}
|
||||
|
||||
get vapidPublic(): string {
|
||||
this.generateVapidKeys();
|
||||
|
||||
return this.data.vapidPublic;
|
||||
}
|
||||
|
||||
get vapidPrivate(): string {
|
||||
this.generateVapidKeys();
|
||||
|
||||
return this.data.vapidPrivate;
|
||||
}
|
||||
|
||||
public regenerateApiKey(): MainSettings {
|
||||
public async regenerateApiKey(): Promise<MainSettings> {
|
||||
this.main.apiKey = this.generateApiKey();
|
||||
this.save();
|
||||
await this.save();
|
||||
return this.main;
|
||||
}
|
||||
|
||||
@@ -620,15 +627,6 @@ class Settings {
|
||||
}
|
||||
}
|
||||
|
||||
private generateVapidKeys(force = false): void {
|
||||
if (!this.data.vapidPublic || !this.data.vapidPrivate || force) {
|
||||
const vapidKeys = webpush.generateVAPIDKeys();
|
||||
this.data.vapidPrivate = vapidKeys.privateKey;
|
||||
this.data.vapidPublic = vapidKeys.publicKey;
|
||||
this.save();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Settings Load
|
||||
*
|
||||
@@ -643,30 +641,51 @@ class Settings {
|
||||
return this;
|
||||
}
|
||||
|
||||
if (!fs.existsSync(SETTINGS_PATH)) {
|
||||
this.save();
|
||||
let data;
|
||||
try {
|
||||
data = await fs.readFile(SETTINGS_PATH, 'utf-8');
|
||||
} catch {
|
||||
await this.save();
|
||||
}
|
||||
const data = fs.readFileSync(SETTINGS_PATH, 'utf-8');
|
||||
|
||||
if (data) {
|
||||
const parsedJson = JSON.parse(data);
|
||||
this.data = await runMigrations(parsedJson, SETTINGS_PATH);
|
||||
|
||||
this.data = merge(this.data, parsedJson);
|
||||
|
||||
if (process.env.API_KEY) {
|
||||
if (this.main.apiKey != process.env.API_KEY) {
|
||||
this.main.apiKey = process.env.API_KEY;
|
||||
}
|
||||
}
|
||||
|
||||
this.save();
|
||||
const migratedData = await runMigrations(parsedJson, SETTINGS_PATH);
|
||||
this.data = merge(this.data, migratedData);
|
||||
}
|
||||
|
||||
// generate keys and ids if it's missing
|
||||
let change = false;
|
||||
if (!this.data.main.apiKey) {
|
||||
this.data.main.apiKey = this.generateApiKey();
|
||||
change = true;
|
||||
} else if (process.env.API_KEY) {
|
||||
if (this.main.apiKey != process.env.API_KEY) {
|
||||
this.main.apiKey = process.env.API_KEY;
|
||||
}
|
||||
}
|
||||
if (!this.data.clientId) {
|
||||
this.data.clientId = randomUUID();
|
||||
change = true;
|
||||
}
|
||||
if (!this.data.vapidPublic || !this.data.vapidPrivate) {
|
||||
const vapidKeys = webpush.generateVAPIDKeys();
|
||||
this.data.vapidPrivate = vapidKeys.privateKey;
|
||||
this.data.vapidPublic = vapidKeys.publicKey;
|
||||
change = true;
|
||||
}
|
||||
if (change) {
|
||||
await this.save();
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
public save(): void {
|
||||
fs.writeFileSync(SETTINGS_PATH, JSON.stringify(this.data, undefined, ' '));
|
||||
public async save(): Promise<void> {
|
||||
await fs.writeFile(
|
||||
SETTINGS_PATH,
|
||||
JSON.stringify(this.data, undefined, ' ')
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,15 +1,14 @@
|
||||
import type { AllSettings } from '@server/lib/settings';
|
||||
|
||||
const migrateHostname = (settings: any): AllSettings => {
|
||||
const oldJellyfinSettings = settings.jellyfin;
|
||||
if (oldJellyfinSettings && oldJellyfinSettings.hostname) {
|
||||
const { hostname } = oldJellyfinSettings;
|
||||
if (settings.jellyfin?.hostname) {
|
||||
const { hostname } = settings.jellyfin;
|
||||
const protocolMatch = hostname.match(/^(https?):\/\//i);
|
||||
const useSsl = protocolMatch && protocolMatch[1].toLowerCase() === 'https';
|
||||
const remainingUrl = hostname.replace(/^(https?):\/\//i, '');
|
||||
const urlMatch = remainingUrl.match(/^([^:]+)(:([0-9]+))?(\/.*)?$/);
|
||||
|
||||
delete oldJellyfinSettings.hostname;
|
||||
delete settings.jellyfin.hostname;
|
||||
if (urlMatch) {
|
||||
const [, ip, , port, urlBase] = urlMatch;
|
||||
settings.jellyfin = {
|
||||
@@ -21,9 +20,7 @@ const migrateHostname = (settings: any): AllSettings => {
|
||||
};
|
||||
}
|
||||
}
|
||||
if (settings.jellyfin && settings.jellyfin.hostname) {
|
||||
delete settings.jellyfin.hostname;
|
||||
}
|
||||
|
||||
return settings;
|
||||
};
|
||||
|
||||
|
||||
@@ -27,8 +27,14 @@ const migrateApiTokens = async (settings: any): Promise<AllSettings> => {
|
||||
admin.jellyfinDeviceId
|
||||
);
|
||||
jellyfinClient.setUserId(admin.jellyfinUserId ?? '');
|
||||
const apiKey = await jellyfinClient.createApiToken('Jellyseerr');
|
||||
settings.jellyfin.apiKey = apiKey;
|
||||
try {
|
||||
const apiKey = await jellyfinClient.createApiToken('Jellyseerr');
|
||||
settings.jellyfin.apiKey = apiKey;
|
||||
} catch {
|
||||
throw new Error(
|
||||
"Failed to create Jellyfin API token from admin account. Please check your network configuration or edit your settings.json by adding an 'apiKey' field inside of the 'jellyfin' section to fix this issue."
|
||||
);
|
||||
}
|
||||
}
|
||||
return settings;
|
||||
};
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
/* eslint-disable no-console */
|
||||
import type { AllSettings } from '@server/lib/settings';
|
||||
import logger from '@server/logger';
|
||||
import fs from 'fs/promises';
|
||||
@@ -15,9 +14,9 @@ export const runMigrations = async (
|
||||
try {
|
||||
// we read old backup and create a backup of currents settings
|
||||
const BACKUP_PATH = SETTINGS_PATH.replace('.json', '.old.json');
|
||||
let oldBackup: Buffer | null = null;
|
||||
let oldBackup: string | null = null;
|
||||
try {
|
||||
oldBackup = await fs.readFile(BACKUP_PATH);
|
||||
oldBackup = await fs.readFile(BACKUP_PATH, 'utf-8');
|
||||
} catch {
|
||||
/* empty */
|
||||
}
|
||||
@@ -37,7 +36,7 @@ export const runMigrations = async (
|
||||
const { default: migrationFn } = await import(
|
||||
path.join(migrationsDir, migration)
|
||||
);
|
||||
const newSettings = await migrationFn(migrated);
|
||||
const newSettings = await migrationFn(structuredClone(migrated));
|
||||
if (JSON.stringify(migrated) !== JSON.stringify(newSettings)) {
|
||||
logger.debug(`Migration '${migration}' has been applied.`, {
|
||||
label: 'Settings Migrator',
|
||||
@@ -45,10 +44,20 @@ export const runMigrations = async (
|
||||
}
|
||||
migrated = newSettings;
|
||||
} catch (e) {
|
||||
logger.error(`Error while running migration '${migration}'`, {
|
||||
label: 'Settings Migrator',
|
||||
});
|
||||
throw e;
|
||||
// we stop jellyseerr if the migration failed
|
||||
logger.error(
|
||||
`Error while running migration '${migration}': ${e.message}`,
|
||||
{
|
||||
label: 'Settings Migrator',
|
||||
}
|
||||
);
|
||||
logger.error(
|
||||
'A common cause for this error is a permission issue with your configuration folder, a network issue or a corrupted database.',
|
||||
{
|
||||
label: 'Settings Migrator',
|
||||
}
|
||||
);
|
||||
process.exit();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -72,22 +81,18 @@ export const runMigrations = async (
|
||||
await fs.writeFile(BACKUP_PATH, oldBackup.toString());
|
||||
}
|
||||
} catch (e) {
|
||||
// we stop jellyseerr if the migration failed
|
||||
logger.error(
|
||||
`Something went wrong while running settings migrations: ${e.message}`,
|
||||
{ label: 'Settings Migrator' }
|
||||
{
|
||||
label: 'Settings Migrator',
|
||||
}
|
||||
);
|
||||
// we stop jellyseerr if the migration failed
|
||||
console.log(
|
||||
'===================================================================='
|
||||
);
|
||||
console.log(
|
||||
' SOMETHING WENT WRONG WHILE RUNNING SETTINGS MIGRATIONS '
|
||||
);
|
||||
console.log(
|
||||
' Please check that your configuration folder is properly set up '
|
||||
);
|
||||
console.log(
|
||||
'===================================================================='
|
||||
logger.error(
|
||||
'A common cause for this issue is a permission error of your configuration folder.',
|
||||
{
|
||||
label: 'Settings Migrator',
|
||||
}
|
||||
);
|
||||
process.exit();
|
||||
}
|
||||
|
||||
@@ -87,7 +87,7 @@ authRoutes.post('/plex', async (req, res, next) => {
|
||||
});
|
||||
|
||||
settings.main.mediaServerType = MediaServerType.PLEX;
|
||||
settings.save();
|
||||
await settings.save();
|
||||
startJobs();
|
||||
|
||||
await userRepository.save(user);
|
||||
@@ -299,54 +299,84 @@ authRoutes.post('/jellyfin', async (req, res, next) => {
|
||||
where: { jellyfinUserId: account.User.Id },
|
||||
});
|
||||
|
||||
if (!user && !(await userRepository.count())) {
|
||||
const missingAdminUser = !user && !(await userRepository.count());
|
||||
if (
|
||||
missingAdminUser ||
|
||||
settings.main.mediaServerType === MediaServerType.NOT_CONFIGURED
|
||||
) {
|
||||
// Check if user is admin on jellyfin
|
||||
if (account.User.Policy.IsAdministrator === false) {
|
||||
throw new ApiError(403, ApiErrorCode.NotAdmin);
|
||||
}
|
||||
|
||||
logger.info(
|
||||
'Sign-in attempt from Jellyfin user with access to the media server; creating initial admin user for Overseerr',
|
||||
{
|
||||
label: 'API',
|
||||
ip: req.ip,
|
||||
if (
|
||||
body.serverType !== MediaServerType.JELLYFIN &&
|
||||
body.serverType !== MediaServerType.EMBY
|
||||
) {
|
||||
throw new Error('select_server_type');
|
||||
}
|
||||
settings.main.mediaServerType = body.serverType;
|
||||
|
||||
if (missingAdminUser) {
|
||||
logger.info(
|
||||
'Sign-in attempt from Jellyfin user with access to the media server; creating initial admin user for Jellyseerr',
|
||||
{
|
||||
label: 'API',
|
||||
ip: req.ip,
|
||||
jellyfinUsername: account.User.Name,
|
||||
}
|
||||
);
|
||||
|
||||
// User doesn't exist, and there are no users in the database, we'll create the user
|
||||
// with admin permissions
|
||||
|
||||
user = new User({
|
||||
id: 1,
|
||||
email: body.email || account.User.Name,
|
||||
jellyfinUsername: account.User.Name,
|
||||
jellyfinUserId: account.User.Id,
|
||||
jellyfinDeviceId: deviceId,
|
||||
jellyfinAuthToken: account.AccessToken,
|
||||
permissions: Permission.ADMIN,
|
||||
avatar: `/avatarproxy/${account.User.Id}`,
|
||||
userType:
|
||||
body.serverType === MediaServerType.JELLYFIN
|
||||
? UserType.JELLYFIN
|
||||
: UserType.EMBY,
|
||||
});
|
||||
|
||||
await userRepository.save(user);
|
||||
} else {
|
||||
logger.info(
|
||||
'Sign-in attempt from Jellyfin user with access to the media server; editing admin user for Jellyseerr',
|
||||
{
|
||||
label: 'API',
|
||||
ip: req.ip,
|
||||
jellyfinUsername: account.User.Name,
|
||||
}
|
||||
);
|
||||
|
||||
// User alread exist but settings.json is not configured, we'll edit the admin user
|
||||
|
||||
user = await userRepository.findOne({
|
||||
where: { id: 1 },
|
||||
});
|
||||
if (!user) {
|
||||
throw new Error('Unable to find admin user to edit');
|
||||
}
|
||||
);
|
||||
user.email = body.email || account.User.Name;
|
||||
user.jellyfinUsername = account.User.Name;
|
||||
user.jellyfinUserId = account.User.Id;
|
||||
user.jellyfinDeviceId = deviceId;
|
||||
user.jellyfinAuthToken = account.AccessToken;
|
||||
user.permissions = Permission.ADMIN;
|
||||
user.avatar = `/avatarproxy/${account.User.Id}`;
|
||||
user.userType =
|
||||
body.serverType === MediaServerType.JELLYFIN
|
||||
? UserType.JELLYFIN
|
||||
: UserType.EMBY;
|
||||
|
||||
// User doesn't exist, and there are no users in the database, we'll create the user
|
||||
// with admin permissions
|
||||
switch (body.serverType) {
|
||||
case MediaServerType.EMBY:
|
||||
settings.main.mediaServerType = MediaServerType.EMBY;
|
||||
user = new User({
|
||||
email: body.email || account.User.Name,
|
||||
jellyfinUsername: account.User.Name,
|
||||
jellyfinUserId: account.User.Id,
|
||||
jellyfinDeviceId: deviceId,
|
||||
jellyfinAuthToken: account.AccessToken,
|
||||
permissions: Permission.ADMIN,
|
||||
avatar: `/avatarproxy/${account.User.Id}`,
|
||||
userType: UserType.EMBY,
|
||||
});
|
||||
|
||||
break;
|
||||
case MediaServerType.JELLYFIN:
|
||||
settings.main.mediaServerType = MediaServerType.JELLYFIN;
|
||||
user = new User({
|
||||
email: body.email || account.User.Name,
|
||||
jellyfinUsername: account.User.Name,
|
||||
jellyfinUserId: account.User.Id,
|
||||
jellyfinDeviceId: deviceId,
|
||||
jellyfinAuthToken: account.AccessToken,
|
||||
permissions: Permission.ADMIN,
|
||||
avatar: `/avatarproxy/${account.User.Id}`,
|
||||
userType: UserType.JELLYFIN,
|
||||
});
|
||||
|
||||
break;
|
||||
default:
|
||||
throw new Error('select_server_type');
|
||||
await userRepository.save(user);
|
||||
}
|
||||
|
||||
// Create an API key on Jellyfin from this admin user
|
||||
@@ -366,10 +396,8 @@ authRoutes.post('/jellyfin', async (req, res, next) => {
|
||||
settings.jellyfin.urlBase = body.urlBase ?? '';
|
||||
settings.jellyfin.useSsl = body.useSsl ?? false;
|
||||
settings.jellyfin.apiKey = apiKey;
|
||||
settings.save();
|
||||
await settings.save();
|
||||
startJobs();
|
||||
|
||||
await userRepository.save(user);
|
||||
}
|
||||
// User already exists, let's update their information
|
||||
else if (account.User.Id === user?.jellyfinUserId) {
|
||||
|
||||
@@ -69,19 +69,19 @@ settingsRoutes.get('/main', (req, res, next) => {
|
||||
res.status(200).json(filteredMainSettings(req.user, settings.main));
|
||||
});
|
||||
|
||||
settingsRoutes.post('/main', (req, res) => {
|
||||
settingsRoutes.post('/main', async (req, res) => {
|
||||
const settings = getSettings();
|
||||
|
||||
settings.main = merge(settings.main, req.body);
|
||||
settings.save();
|
||||
await settings.save();
|
||||
|
||||
return res.status(200).json(settings.main);
|
||||
});
|
||||
|
||||
settingsRoutes.post('/main/regenerate', (req, res, next) => {
|
||||
settingsRoutes.post('/main/regenerate', async (req, res, next) => {
|
||||
const settings = getSettings();
|
||||
|
||||
const main = settings.regenerateApiKey();
|
||||
const main = await settings.regenerateApiKey();
|
||||
|
||||
if (!req.user) {
|
||||
return next({ status: 500, message: 'User missing from request.' });
|
||||
@@ -118,7 +118,7 @@ settingsRoutes.post('/plex', async (req, res, next) => {
|
||||
settings.plex.machineId = result.MediaContainer.machineIdentifier;
|
||||
settings.plex.name = result.MediaContainer.friendlyName;
|
||||
|
||||
settings.save();
|
||||
await settings.save();
|
||||
} catch (e) {
|
||||
logger.error('Something went wrong testing Plex connection', {
|
||||
label: 'API',
|
||||
@@ -231,7 +231,7 @@ settingsRoutes.get('/plex/library', async (req, res) => {
|
||||
...library,
|
||||
enabled: enabledLibraries.includes(library.id),
|
||||
}));
|
||||
settings.save();
|
||||
await settings.save();
|
||||
return res.status(200).json(settings.plex.libraries);
|
||||
});
|
||||
|
||||
@@ -282,7 +282,7 @@ settingsRoutes.post('/jellyfin', async (req, res, next) => {
|
||||
Object.assign(settings.jellyfin, req.body);
|
||||
settings.jellyfin.serverId = result.Id;
|
||||
settings.jellyfin.name = result.ServerName;
|
||||
settings.save();
|
||||
await settings.save();
|
||||
} catch (e) {
|
||||
if (e instanceof ApiError) {
|
||||
logger.error('Something went wrong testing Jellyfin connection', {
|
||||
@@ -370,7 +370,7 @@ settingsRoutes.get('/jellyfin/library', async (req, res, next) => {
|
||||
...library,
|
||||
enabled: enabledLibraries.includes(library.id),
|
||||
}));
|
||||
settings.save();
|
||||
await settings.save();
|
||||
return res.status(200).json(settings.jellyfin.libraries);
|
||||
});
|
||||
|
||||
@@ -434,7 +434,7 @@ settingsRoutes.post('/tautulli', async (req, res, next) => {
|
||||
throw new Error('Tautulli version not supported');
|
||||
}
|
||||
|
||||
settings.save();
|
||||
await settings.save();
|
||||
} catch (e) {
|
||||
logger.error('Something went wrong testing Tautulli connection', {
|
||||
label: 'API',
|
||||
@@ -695,7 +695,7 @@ settingsRoutes.post<{ jobId: JobId }>(
|
||||
|
||||
settingsRoutes.post<{ jobId: JobId }>(
|
||||
'/jobs/:jobId/schedule',
|
||||
(req, res, next) => {
|
||||
async (req, res, next) => {
|
||||
const scheduledJob = scheduledJobs.find(
|
||||
(job) => job.id === req.params.jobId
|
||||
);
|
||||
@@ -709,7 +709,7 @@ settingsRoutes.post<{ jobId: JobId }>(
|
||||
|
||||
if (result) {
|
||||
settings.jobs[scheduledJob.id].schedule = req.body.schedule;
|
||||
settings.save();
|
||||
await settings.save();
|
||||
|
||||
scheduledJob.cronSchedule = req.body.schedule;
|
||||
|
||||
@@ -766,11 +766,11 @@ settingsRoutes.post<{ cacheId: AvailableCacheIds }>(
|
||||
settingsRoutes.post(
|
||||
'/initialize',
|
||||
isAuthenticated(Permission.ADMIN),
|
||||
(_req, res) => {
|
||||
async (_req, res) => {
|
||||
const settings = getSettings();
|
||||
|
||||
settings.public.initialized = true;
|
||||
settings.save();
|
||||
await settings.save();
|
||||
|
||||
return res.status(200).json(settings.public);
|
||||
}
|
||||
|
||||
@@ -31,11 +31,11 @@ notificationRoutes.get('/discord', (_req, res) => {
|
||||
res.status(200).json(settings.notifications.agents.discord);
|
||||
});
|
||||
|
||||
notificationRoutes.post('/discord', (req, res) => {
|
||||
notificationRoutes.post('/discord', async (req, res) => {
|
||||
const settings = getSettings();
|
||||
|
||||
settings.notifications.agents.discord = req.body;
|
||||
settings.save();
|
||||
await settings.save();
|
||||
|
||||
res.status(200).json(settings.notifications.agents.discord);
|
||||
});
|
||||
@@ -65,11 +65,11 @@ notificationRoutes.get('/slack', (_req, res) => {
|
||||
res.status(200).json(settings.notifications.agents.slack);
|
||||
});
|
||||
|
||||
notificationRoutes.post('/slack', (req, res) => {
|
||||
notificationRoutes.post('/slack', async (req, res) => {
|
||||
const settings = getSettings();
|
||||
|
||||
settings.notifications.agents.slack = req.body;
|
||||
settings.save();
|
||||
await settings.save();
|
||||
|
||||
res.status(200).json(settings.notifications.agents.slack);
|
||||
});
|
||||
@@ -99,11 +99,11 @@ notificationRoutes.get('/telegram', (_req, res) => {
|
||||
res.status(200).json(settings.notifications.agents.telegram);
|
||||
});
|
||||
|
||||
notificationRoutes.post('/telegram', (req, res) => {
|
||||
notificationRoutes.post('/telegram', async (req, res) => {
|
||||
const settings = getSettings();
|
||||
|
||||
settings.notifications.agents.telegram = req.body;
|
||||
settings.save();
|
||||
await settings.save();
|
||||
|
||||
res.status(200).json(settings.notifications.agents.telegram);
|
||||
});
|
||||
@@ -133,11 +133,11 @@ notificationRoutes.get('/pushbullet', (_req, res) => {
|
||||
res.status(200).json(settings.notifications.agents.pushbullet);
|
||||
});
|
||||
|
||||
notificationRoutes.post('/pushbullet', (req, res) => {
|
||||
notificationRoutes.post('/pushbullet', async (req, res) => {
|
||||
const settings = getSettings();
|
||||
|
||||
settings.notifications.agents.pushbullet = req.body;
|
||||
settings.save();
|
||||
await settings.save();
|
||||
|
||||
res.status(200).json(settings.notifications.agents.pushbullet);
|
||||
});
|
||||
@@ -167,11 +167,11 @@ notificationRoutes.get('/pushover', (_req, res) => {
|
||||
res.status(200).json(settings.notifications.agents.pushover);
|
||||
});
|
||||
|
||||
notificationRoutes.post('/pushover', (req, res) => {
|
||||
notificationRoutes.post('/pushover', async (req, res) => {
|
||||
const settings = getSettings();
|
||||
|
||||
settings.notifications.agents.pushover = req.body;
|
||||
settings.save();
|
||||
await settings.save();
|
||||
|
||||
res.status(200).json(settings.notifications.agents.pushover);
|
||||
});
|
||||
@@ -201,11 +201,11 @@ notificationRoutes.get('/email', (_req, res) => {
|
||||
res.status(200).json(settings.notifications.agents.email);
|
||||
});
|
||||
|
||||
notificationRoutes.post('/email', (req, res) => {
|
||||
notificationRoutes.post('/email', async (req, res) => {
|
||||
const settings = getSettings();
|
||||
|
||||
settings.notifications.agents.email = req.body;
|
||||
settings.save();
|
||||
await settings.save();
|
||||
|
||||
res.status(200).json(settings.notifications.agents.email);
|
||||
});
|
||||
@@ -235,11 +235,11 @@ notificationRoutes.get('/webpush', (_req, res) => {
|
||||
res.status(200).json(settings.notifications.agents.webpush);
|
||||
});
|
||||
|
||||
notificationRoutes.post('/webpush', (req, res) => {
|
||||
notificationRoutes.post('/webpush', async (req, res) => {
|
||||
const settings = getSettings();
|
||||
|
||||
settings.notifications.agents.webpush = req.body;
|
||||
settings.save();
|
||||
await settings.save();
|
||||
|
||||
res.status(200).json(settings.notifications.agents.webpush);
|
||||
});
|
||||
@@ -284,7 +284,7 @@ notificationRoutes.get('/webhook', (_req, res) => {
|
||||
res.status(200).json(response);
|
||||
});
|
||||
|
||||
notificationRoutes.post('/webhook', (req, res, next) => {
|
||||
notificationRoutes.post('/webhook', async (req, res, next) => {
|
||||
const settings = getSettings();
|
||||
try {
|
||||
JSON.parse(req.body.options.jsonPayload);
|
||||
@@ -300,7 +300,7 @@ notificationRoutes.post('/webhook', (req, res, next) => {
|
||||
authHeader: req.body.options.authHeader,
|
||||
},
|
||||
};
|
||||
settings.save();
|
||||
await settings.save();
|
||||
|
||||
res.status(200).json(settings.notifications.agents.webhook);
|
||||
} catch (e) {
|
||||
@@ -351,11 +351,11 @@ notificationRoutes.get('/lunasea', (_req, res) => {
|
||||
res.status(200).json(settings.notifications.agents.lunasea);
|
||||
});
|
||||
|
||||
notificationRoutes.post('/lunasea', (req, res) => {
|
||||
notificationRoutes.post('/lunasea', async (req, res) => {
|
||||
const settings = getSettings();
|
||||
|
||||
settings.notifications.agents.lunasea = req.body;
|
||||
settings.save();
|
||||
await settings.save();
|
||||
|
||||
res.status(200).json(settings.notifications.agents.lunasea);
|
||||
});
|
||||
@@ -385,11 +385,11 @@ notificationRoutes.get('/gotify', (_req, res) => {
|
||||
res.status(200).json(settings.notifications.agents.gotify);
|
||||
});
|
||||
|
||||
notificationRoutes.post('/gotify', (req, res) => {
|
||||
notificationRoutes.post('/gotify', async (req, res) => {
|
||||
const settings = getSettings();
|
||||
|
||||
settings.notifications.agents.gotify = req.body;
|
||||
settings.save();
|
||||
await settings.save();
|
||||
|
||||
res.status(200).json(settings.notifications.agents.gotify);
|
||||
});
|
||||
|
||||
@@ -12,7 +12,7 @@ radarrRoutes.get('/', (_req, res) => {
|
||||
res.status(200).json(settings.radarr);
|
||||
});
|
||||
|
||||
radarrRoutes.post('/', (req, res) => {
|
||||
radarrRoutes.post('/', async (req, res) => {
|
||||
const settings = getSettings();
|
||||
|
||||
const newRadarr = req.body as RadarrSettings;
|
||||
@@ -31,7 +31,7 @@ radarrRoutes.post('/', (req, res) => {
|
||||
}
|
||||
|
||||
settings.radarr = [...settings.radarr, newRadarr];
|
||||
settings.save();
|
||||
await settings.save();
|
||||
|
||||
return res.status(201).json(newRadarr);
|
||||
});
|
||||
@@ -76,7 +76,7 @@ radarrRoutes.post<
|
||||
|
||||
radarrRoutes.put<{ id: string }, RadarrSettings, RadarrSettings>(
|
||||
'/:id',
|
||||
(req, res, next) => {
|
||||
async (req, res, next) => {
|
||||
const settings = getSettings();
|
||||
|
||||
const radarrIndex = settings.radarr.findIndex(
|
||||
@@ -102,7 +102,7 @@ radarrRoutes.put<{ id: string }, RadarrSettings, RadarrSettings>(
|
||||
...req.body,
|
||||
id: Number(req.params.id),
|
||||
} as RadarrSettings;
|
||||
settings.save();
|
||||
await settings.save();
|
||||
|
||||
return res.status(200).json(settings.radarr[radarrIndex]);
|
||||
}
|
||||
@@ -134,7 +134,7 @@ radarrRoutes.get<{ id: string }>('/:id/profiles', async (req, res, next) => {
|
||||
);
|
||||
});
|
||||
|
||||
radarrRoutes.delete<{ id: string }>('/:id', (req, res, next) => {
|
||||
radarrRoutes.delete<{ id: string }>('/:id', async (req, res, next) => {
|
||||
const settings = getSettings();
|
||||
|
||||
const radarrIndex = settings.radarr.findIndex(
|
||||
@@ -146,7 +146,7 @@ radarrRoutes.delete<{ id: string }>('/:id', (req, res, next) => {
|
||||
}
|
||||
|
||||
const removed = settings.radarr.splice(radarrIndex, 1);
|
||||
settings.save();
|
||||
await settings.save();
|
||||
|
||||
return res.status(200).json(removed[0]);
|
||||
});
|
||||
|
||||
@@ -12,7 +12,7 @@ sonarrRoutes.get('/', (_req, res) => {
|
||||
res.status(200).json(settings.sonarr);
|
||||
});
|
||||
|
||||
sonarrRoutes.post('/', (req, res) => {
|
||||
sonarrRoutes.post('/', async (req, res) => {
|
||||
const settings = getSettings();
|
||||
|
||||
const newSonarr = req.body as SonarrSettings;
|
||||
@@ -31,7 +31,7 @@ sonarrRoutes.post('/', (req, res) => {
|
||||
}
|
||||
|
||||
settings.sonarr = [...settings.sonarr, newSonarr];
|
||||
settings.save();
|
||||
await settings.save();
|
||||
|
||||
return res.status(201).json(newSonarr);
|
||||
});
|
||||
@@ -73,7 +73,7 @@ sonarrRoutes.post('/test', async (req, res, next) => {
|
||||
}
|
||||
});
|
||||
|
||||
sonarrRoutes.put<{ id: string }>('/:id', (req, res) => {
|
||||
sonarrRoutes.put<{ id: string }>('/:id', async (req, res) => {
|
||||
const settings = getSettings();
|
||||
|
||||
const sonarrIndex = settings.sonarr.findIndex(
|
||||
@@ -101,12 +101,12 @@ sonarrRoutes.put<{ id: string }>('/:id', (req, res) => {
|
||||
...req.body,
|
||||
id: Number(req.params.id),
|
||||
} as SonarrSettings;
|
||||
settings.save();
|
||||
await settings.save();
|
||||
|
||||
return res.status(200).json(settings.sonarr[sonarrIndex]);
|
||||
});
|
||||
|
||||
sonarrRoutes.delete<{ id: string }>('/:id', (req, res) => {
|
||||
sonarrRoutes.delete<{ id: string }>('/:id', async (req, res) => {
|
||||
const settings = getSettings();
|
||||
|
||||
const sonarrIndex = settings.sonarr.findIndex(
|
||||
@@ -120,7 +120,7 @@ sonarrRoutes.delete<{ id: string }>('/:id', (req, res) => {
|
||||
}
|
||||
|
||||
const removed = settings.sonarr.splice(sonarrIndex, 1);
|
||||
settings.save();
|
||||
await settings.save();
|
||||
|
||||
return res.status(200).json(removed[0]);
|
||||
});
|
||||
|
||||
111
server/utils/customProxyAgent.ts
Normal file
111
server/utils/customProxyAgent.ts
Normal file
@@ -0,0 +1,111 @@
|
||||
import type { ProxySettings } from '@server/lib/settings';
|
||||
import logger from '@server/logger';
|
||||
import type { Dispatcher } from 'undici';
|
||||
import { Agent, ProxyAgent, setGlobalDispatcher } from 'undici';
|
||||
|
||||
export default async function createCustomProxyAgent(
|
||||
proxySettings: ProxySettings
|
||||
) {
|
||||
const defaultAgent = new Agent();
|
||||
|
||||
const skipUrl = (url: string) => {
|
||||
const hostname = new URL(url).hostname;
|
||||
|
||||
if (proxySettings.bypassLocalAddresses && isLocalAddress(hostname)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
for (const address of proxySettings.bypassFilter.split(',')) {
|
||||
const trimmedAddress = address.trim();
|
||||
if (!trimmedAddress) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (trimmedAddress.startsWith('*')) {
|
||||
const domain = trimmedAddress.slice(1);
|
||||
if (hostname.endsWith(domain)) {
|
||||
return true;
|
||||
}
|
||||
} else if (hostname === trimmedAddress) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
const noProxyInterceptor = (
|
||||
dispatch: Dispatcher['dispatch']
|
||||
): Dispatcher['dispatch'] => {
|
||||
return (opts, handler) => {
|
||||
const url = opts.origin?.toString();
|
||||
return url && skipUrl(url)
|
||||
? defaultAgent.dispatch(opts, handler)
|
||||
: dispatch(opts, handler);
|
||||
};
|
||||
};
|
||||
|
||||
const token =
|
||||
proxySettings.user && proxySettings.password
|
||||
? `Basic ${Buffer.from(
|
||||
`${proxySettings.user}:${proxySettings.password}`
|
||||
).toString('base64')}`
|
||||
: undefined;
|
||||
|
||||
try {
|
||||
const proxyAgent = new ProxyAgent({
|
||||
uri:
|
||||
(proxySettings.useSsl ? 'https://' : 'http://') +
|
||||
proxySettings.hostname +
|
||||
':' +
|
||||
proxySettings.port,
|
||||
token,
|
||||
interceptors: {
|
||||
Client: [noProxyInterceptor],
|
||||
},
|
||||
});
|
||||
|
||||
setGlobalDispatcher(proxyAgent);
|
||||
} catch (e) {
|
||||
logger.error('Failed to connect to the proxy: ' + e.message, {
|
||||
label: 'Proxy',
|
||||
});
|
||||
setGlobalDispatcher(defaultAgent);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const res = await fetch('https://www.google.com', { method: 'HEAD' });
|
||||
if (res.ok) {
|
||||
logger.debug('HTTP(S) proxy connected successfully', { label: 'Proxy' });
|
||||
} else {
|
||||
logger.error('Proxy responded, but with a non-OK status: ' + res.status, {
|
||||
label: 'Proxy',
|
||||
});
|
||||
setGlobalDispatcher(defaultAgent);
|
||||
}
|
||||
} catch (e) {
|
||||
logger.error(
|
||||
'Failed to connect to the proxy: ' + e.message + ': ' + e.cause,
|
||||
{ label: 'Proxy' }
|
||||
);
|
||||
setGlobalDispatcher(defaultAgent);
|
||||
}
|
||||
}
|
||||
|
||||
function isLocalAddress(hostname: string) {
|
||||
if (hostname === 'localhost' || hostname === '127.0.0.1') {
|
||||
return true;
|
||||
}
|
||||
|
||||
const privateIpRanges = [
|
||||
/^10\./, // 10.x.x.x
|
||||
/^172\.(1[6-9]|2[0-9]|3[0-1])\./, // 172.16.x.x - 172.31.x.x
|
||||
/^192\.168\./, // 192.168.x.x
|
||||
];
|
||||
if (privateIpRanges.some((regex) => regex.test(hostname))) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
@@ -14,7 +14,7 @@ class RestartFlag {
|
||||
return (
|
||||
this.settings.csrfProtection !== settings.csrfProtection ||
|
||||
this.settings.trustProxy !== settings.trustProxy ||
|
||||
this.settings.httpProxy !== settings.httpProxy
|
||||
this.settings.proxy.enabled !== settings.proxy.enabled
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -55,8 +55,17 @@ const messages = defineMessages('components.Settings.SettingsMain', {
|
||||
validationApplicationUrlTrailingSlash: 'URL must not end in a trailing slash',
|
||||
partialRequestsEnabled: 'Allow Partial Series Requests',
|
||||
locale: 'Display Language',
|
||||
httpProxy: 'HTTP Proxy',
|
||||
httpProxyTip: 'Tooltip to write',
|
||||
proxyEnabled: 'HTTP(S) Proxy',
|
||||
proxyHostname: 'Proxy Hostname',
|
||||
proxyPort: 'Proxy Port',
|
||||
proxySsl: 'Use SSL For Proxy',
|
||||
proxyUser: 'Proxy Username',
|
||||
proxyPassword: 'Proxy Password',
|
||||
proxyBypassFilter: 'Proxy Ignored Addresses',
|
||||
proxyBypassFilterTip:
|
||||
"Use ',' as a separator, and '*.' as a wildcard for subdomains",
|
||||
proxyBypassLocalAddresses: 'Bypass Proxy for Local Addresses',
|
||||
validationProxyPort: 'You must provide a valid port',
|
||||
});
|
||||
|
||||
const SettingsMain = () => {
|
||||
@@ -84,9 +93,12 @@ const SettingsMain = () => {
|
||||
intl.formatMessage(messages.validationApplicationUrlTrailingSlash),
|
||||
(value) => !value || !value.endsWith('/')
|
||||
),
|
||||
httpProxy: Yup.string().url(
|
||||
intl.formatMessage(messages.validationApplicationUrl)
|
||||
),
|
||||
proxyPort: Yup.number().when('proxyEnabled', {
|
||||
is: (proxyEnabled: boolean) => proxyEnabled,
|
||||
then: Yup.number().required(
|
||||
intl.formatMessage(messages.validationProxyPort)
|
||||
),
|
||||
}),
|
||||
});
|
||||
|
||||
const regenerate = async () => {
|
||||
@@ -142,7 +154,14 @@ const SettingsMain = () => {
|
||||
partialRequestsEnabled: data?.partialRequestsEnabled,
|
||||
trustProxy: data?.trustProxy,
|
||||
cacheImages: data?.cacheImages,
|
||||
httpProxy: data?.httpProxy,
|
||||
proxyEnabled: data?.proxy?.enabled,
|
||||
proxyHostname: data?.proxy?.hostname,
|
||||
proxyPort: data?.proxy?.port,
|
||||
proxySsl: data?.proxy?.useSsl,
|
||||
proxyUser: data?.proxy?.user,
|
||||
proxyPassword: data?.proxy?.password,
|
||||
proxyBypassFilter: data?.proxy?.bypassFilter,
|
||||
proxyBypassLocalAddresses: data?.proxy?.bypassLocalAddresses,
|
||||
}}
|
||||
enableReinitialize
|
||||
validationSchema={MainSettingsSchema}
|
||||
@@ -164,7 +183,16 @@ const SettingsMain = () => {
|
||||
partialRequestsEnabled: values.partialRequestsEnabled,
|
||||
trustProxy: values.trustProxy,
|
||||
cacheImages: values.cacheImages,
|
||||
httpProxy: values.httpProxy,
|
||||
proxy: {
|
||||
enabled: values.proxyEnabled,
|
||||
hostname: values.proxyHostname,
|
||||
port: values.proxyPort,
|
||||
useSsl: values.proxySsl,
|
||||
user: values.proxyUser,
|
||||
password: values.proxyPassword,
|
||||
bypassFilter: values.proxyBypassFilter,
|
||||
bypassLocalAddresses: values.proxyBypassLocalAddresses,
|
||||
},
|
||||
}),
|
||||
});
|
||||
if (!res.ok) throw new Error();
|
||||
@@ -445,27 +473,175 @@ const SettingsMain = () => {
|
||||
</div>
|
||||
</div>
|
||||
<div className="form-row">
|
||||
<label htmlFor="httpProxy" className="checkbox-label">
|
||||
<label htmlFor="proxyEnabled" className="checkbox-label">
|
||||
<span className="mr-2">
|
||||
{intl.formatMessage(messages.httpProxy)}
|
||||
{intl.formatMessage(messages.proxyEnabled)}
|
||||
</span>
|
||||
<SettingsBadge badgeType="advanced" className="mr-2" />
|
||||
<SettingsBadge badgeType="restartRequired" />
|
||||
<span className="label-tip">
|
||||
{intl.formatMessage(messages.httpProxyTip)}
|
||||
</span>
|
||||
</label>
|
||||
<div className="form-input-area">
|
||||
<div className="form-input-field">
|
||||
<Field id="httpProxy" name="httpProxy" type="text" />
|
||||
</div>
|
||||
{errors.httpProxy &&
|
||||
touched.httpProxy &&
|
||||
typeof errors.httpProxy === 'string' && (
|
||||
<div className="error">{errors.httpProxy}</div>
|
||||
)}
|
||||
<Field
|
||||
type="checkbox"
|
||||
id="proxyEnabled"
|
||||
name="proxyEnabled"
|
||||
onChange={() => {
|
||||
setFieldValue('proxyEnabled', !values.proxyEnabled);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{values.proxyEnabled && (
|
||||
<>
|
||||
<div className="form-row">
|
||||
<label htmlFor="proxyHostname" className="checkbox-label">
|
||||
<span className="mr-2 ml-4">
|
||||
{intl.formatMessage(messages.proxyHostname)}
|
||||
</span>
|
||||
</label>
|
||||
<div className="form-input-area">
|
||||
<div className="form-input-field">
|
||||
<Field
|
||||
id="proxyHostname"
|
||||
name="proxyHostname"
|
||||
type="text"
|
||||
/>
|
||||
</div>
|
||||
{errors.proxyHostname &&
|
||||
touched.proxyHostname &&
|
||||
typeof errors.proxyHostname === 'string' && (
|
||||
<div className="error">{errors.proxyHostname}</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="form-row">
|
||||
<label htmlFor="proxyPort" className="checkbox-label">
|
||||
<span className="mr-2 ml-4">
|
||||
{intl.formatMessage(messages.proxyPort)}
|
||||
</span>
|
||||
</label>
|
||||
<div className="form-input-area">
|
||||
<div className="form-input-field">
|
||||
<Field id="proxyPort" name="proxyPort" type="text" />
|
||||
</div>
|
||||
{errors.proxyPort &&
|
||||
touched.proxyPort &&
|
||||
typeof errors.proxyPort === 'string' && (
|
||||
<div className="error">{errors.proxyPort}</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="form-row">
|
||||
<label htmlFor="proxySsl" className="checkbox-label">
|
||||
<span className="mr-2 ml-4">
|
||||
{intl.formatMessage(messages.proxySsl)}
|
||||
</span>
|
||||
</label>
|
||||
<div className="form-input-area">
|
||||
<Field
|
||||
type="checkbox"
|
||||
id="proxySsl"
|
||||
name="proxySsl"
|
||||
onChange={() => {
|
||||
setFieldValue('proxySsl', !values.proxySsl);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="form-row">
|
||||
<label htmlFor="proxyUser" className="checkbox-label">
|
||||
<span className="mr-2 ml-4">
|
||||
{intl.formatMessage(messages.proxyUser)}
|
||||
</span>
|
||||
</label>
|
||||
<div className="form-input-area">
|
||||
<div className="form-input-field">
|
||||
<Field id="proxyUser" name="proxyUser" type="text" />
|
||||
</div>
|
||||
{errors.proxyUser &&
|
||||
touched.proxyUser &&
|
||||
typeof errors.proxyUser === 'string' && (
|
||||
<div className="error">{errors.proxyUser}</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="form-row">
|
||||
<label htmlFor="proxyPassword" className="checkbox-label">
|
||||
<span className="mr-2 ml-4">
|
||||
{intl.formatMessage(messages.proxyPassword)}
|
||||
</span>
|
||||
</label>
|
||||
<div className="form-input-area">
|
||||
<div className="form-input-field">
|
||||
<Field
|
||||
id="proxyPassword"
|
||||
name="proxyPassword"
|
||||
type="password"
|
||||
/>
|
||||
</div>
|
||||
{errors.proxyPassword &&
|
||||
touched.proxyPassword &&
|
||||
typeof errors.proxyPassword === 'string' && (
|
||||
<div className="error">{errors.proxyPassword}</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="form-row">
|
||||
<label
|
||||
htmlFor="proxyBypassFilter"
|
||||
className="checkbox-label"
|
||||
>
|
||||
<span className="mr-2 ml-4">
|
||||
{intl.formatMessage(messages.proxyBypassFilter)}
|
||||
</span>
|
||||
<span className="label-tip ml-4">
|
||||
{intl.formatMessage(messages.proxyBypassFilterTip)}
|
||||
</span>
|
||||
</label>
|
||||
<div className="form-input-area">
|
||||
<div className="form-input-field">
|
||||
<Field
|
||||
id="proxyBypassFilter"
|
||||
name="proxyBypassFilter"
|
||||
type="text"
|
||||
/>
|
||||
</div>
|
||||
{errors.proxyBypassFilter &&
|
||||
touched.proxyBypassFilter &&
|
||||
typeof errors.proxyBypassFilter === 'string' && (
|
||||
<div className="error">
|
||||
{errors.proxyBypassFilter}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="form-row">
|
||||
<label
|
||||
htmlFor="proxyBypassLocalAddresses"
|
||||
className="checkbox-label"
|
||||
>
|
||||
<span className="mr-2 ml-4">
|
||||
{intl.formatMessage(
|
||||
messages.proxyBypassLocalAddresses
|
||||
)}
|
||||
</span>
|
||||
</label>
|
||||
<div className="form-input-area">
|
||||
<Field
|
||||
type="checkbox"
|
||||
id="proxyBypassLocalAddresses"
|
||||
name="proxyBypassLocalAddresses"
|
||||
onChange={() => {
|
||||
setFieldValue(
|
||||
'proxyBypassLocalAddresses',
|
||||
!values.proxyBypassLocalAddresses
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
<div className="actions">
|
||||
<div className="flex justify-end">
|
||||
<span className="ml-3 inline-flex rounded-md shadow-sm">
|
||||
|
||||
Reference in New Issue
Block a user