Updated how we present the plex servers associated with token, we hardened the Backend update with validateContext to caught errors before saving the updated backend settings.
This commit is contained in:
@@ -52,7 +52,7 @@
|
||||
<div class="control has-icons-left">
|
||||
<input class="input" type="text" v-model="backend.name" required>
|
||||
<div class="icon is-left">
|
||||
<i class="fas fa-user"></i>
|
||||
<i class="fas fa-id-badge"></i>
|
||||
</div>
|
||||
<p class="help">
|
||||
Choose a unique name for this backend. You cannot change it later. Backend name must be in <code>lower
|
||||
@@ -63,25 +63,38 @@
|
||||
|
||||
<div class="field">
|
||||
<label class="label">
|
||||
<template v-if="'plex' !== backend.type">API Token</template>
|
||||
<template v-if="'plex' !== backend.type">API Key</template>
|
||||
<template v-else>X-Plex-Token</template>
|
||||
</label>
|
||||
<div class="control has-icons-left">
|
||||
<input class="input" type="text" v-model="backend.token" required>
|
||||
<div class="icon is-left">
|
||||
<i class="fas fa-key"></i>
|
||||
<div class="field-body">
|
||||
<div class="field">
|
||||
<div class="field has-addons">
|
||||
<div class="control is-expanded has-icons-left">
|
||||
<input class="input" v-model="backend.token" required
|
||||
:type="false === exposeToken ? 'password' : 'text'">
|
||||
<div class="icon is-left">
|
||||
<i class="fas fa-key"></i>
|
||||
</div>
|
||||
</div>
|
||||
<div class="control">
|
||||
<button type="button" class="button is-primary" @click="exposeToken = !exposeToken"
|
||||
v-tooltip="'Toggle token'">
|
||||
<span class="icon" v-if="!exposeToken"><i class="fas fa-eye"></i></span>
|
||||
<span class="icon" v-else><i class="fas fa-eye-slash"></i></span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<p class="help">
|
||||
<template v-if="'plex'===backend.type">
|
||||
Enter the <code>X-Plex-Token</code>.
|
||||
<NuxtLink target="_blank" to="https://support.plex.tv/articles/204059436"
|
||||
v-text="'Visit This article for more information.'"/>
|
||||
</template>
|
||||
<template v-else>
|
||||
Generate a new API Key from <code>Dashboard > Settings > API Keys</code>.
|
||||
</template>
|
||||
</p>
|
||||
</div>
|
||||
<p class="help">
|
||||
<template v-if="'plex'===backend.type">
|
||||
Enter the <code>X-Plex-Token</code>.
|
||||
<NuxtLink target="_blank" href="https://support.plex.tv/articles/204059436">
|
||||
Visit This article for more information.
|
||||
</NuxtLink>
|
||||
</template>
|
||||
<template v-else>
|
||||
Generate a new API token from <code>Dashboard > Settings > API Keys</code>.
|
||||
</template>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -91,7 +104,7 @@
|
||||
<div class="select is-fullwidth" v-if="servers.length > 0">
|
||||
<select v-model="backend.url" class="is-capital" @change="updateIdentifier" required>
|
||||
<option v-for="server in servers" :key="'server-'+server.uuid" :value="server.uri">
|
||||
{{ server.name }} - {{ server.address }}
|
||||
{{ server.name }} - {{ server.uri }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
@@ -105,7 +118,7 @@
|
||||
<template v-if="servers.length<1">
|
||||
Enter the URL of the backend. For example <code>http://localhost:32400</code>.
|
||||
</template>
|
||||
<a href="javascript:void(0)" @click="getServers">Attempt to discover servers associated with the token</a>.
|
||||
<NuxtLink @click="getServers" v-text="'Attempt to discover servers associated with the token.'"/>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -135,7 +148,7 @@
|
||||
<p class="help">
|
||||
The backend unique ID is a random string generated on server setup. It is used to identify the backend
|
||||
uniquely. This is used for webhook matching and filtering.
|
||||
<a href="javascript:void(0)" @click="getUUid">Load automatically.</a>
|
||||
<NuxtLink @click="getUUid" v-text="'Load automatically.'"/>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -169,9 +182,7 @@
|
||||
data we get from the backend. And for webhook matching and filtering.
|
||||
</span>
|
||||
This tool is meant for single user use.
|
||||
<a href="javascript:void(0)" @click="getUsers">
|
||||
Retrieve User ids from backend.
|
||||
</a>
|
||||
<NuxtLink @click="getUsers" v-text="'Retrieve User ids from backend.'"/>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -292,6 +303,7 @@ const stage = ref(0)
|
||||
const usersLoading = ref(false)
|
||||
const uuidLoading = ref(false)
|
||||
const serversLoading = ref(false)
|
||||
const exposeToken = ref(false)
|
||||
|
||||
const getUUid = async () => {
|
||||
const required_values = ['type', 'token', 'url'];
|
||||
@@ -496,5 +508,4 @@ const updateIdentifier = async () => {
|
||||
backend.value.uuid = servers.value.find(s => s.uri === backend.value.url).identifier
|
||||
await getUsers()
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
@@ -122,7 +122,7 @@
|
||||
:type="false === exposeToken ? 'password' : 'text'">
|
||||
</div>
|
||||
<div class="control">
|
||||
<button class="button is-primary" @click="exposeToken = !exposeToken"
|
||||
<button type="button" class="button is-primary" @click="exposeToken = !exposeToken"
|
||||
v-tooltip="'Show/Hide token'">
|
||||
<span class="icon" v-if="!exposeToken"><i class="fas fa-eye"></i></span>
|
||||
<span class="icon" v-else><i class="fas fa-eye-slash"></i></span>
|
||||
|
||||
@@ -32,7 +32,7 @@
|
||||
<div class="control has-icons-left">
|
||||
<input class="input" type="text" v-model="backend.name" required readonly disabled>
|
||||
<div class="icon is-left">
|
||||
<i class="fas fa-user"></i>
|
||||
<i class="fas fa-id-badge"></i>
|
||||
</div>
|
||||
<p class="help">
|
||||
Choose a unique name for this backend. You cannot change it later. Backend name must be in <code>lower
|
||||
@@ -46,7 +46,7 @@
|
||||
<div class="control has-icons-left">
|
||||
<input class="input" type="text" v-model="backend.type" readonly disabled>
|
||||
<div class="icon is-left">
|
||||
<i class="fas fa-globe"></i>
|
||||
<i class="fas fa-server"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -54,38 +54,67 @@
|
||||
<div class="field">
|
||||
<label class="label">URL</label>
|
||||
<div class="control has-icons-left">
|
||||
<input class="input" type="text" v-model="backend.url" required>
|
||||
<div class="select is-fullwidth" v-if="servers.length > 0">
|
||||
<select v-model="backend.url" class="is-capital" @change="updateIdentifier" required>
|
||||
<option value="" disabled>Select Server</option>
|
||||
<option v-for="server in servers" :key="server.uuid" :value="server.uri">
|
||||
{{ server.name }} - {{ server.uri }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
<input class="input" type="text" v-model="backend.url" v-else required>
|
||||
<div class="icon is-left">
|
||||
<i class="fas fa-link"></i>
|
||||
</div>
|
||||
<p class="help">
|
||||
Enter the URL of the backend.
|
||||
<a v-if="'plex' === backend.type" href="javascript:void(0)">Get associated servers with token. NYI</a>
|
||||
<template v-if="servers.length<1">
|
||||
Enter the URL of the backend. For example
|
||||
<code v-if="'plex' === backend.type">http://192.168.8.11:32400</code>
|
||||
<code v-else>http://192.168.8.100:8096</code>
|
||||
.
|
||||
</template>
|
||||
<template v-else>
|
||||
Those are the servers associated with the Plex Token. Select the server you want to use.
|
||||
</template>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label class="label">
|
||||
<template v-if="'plex' !== backend.type">API Token</template>
|
||||
<template v-if="'plex' !== backend.type">API Key</template>
|
||||
<template v-else>X-Plex-Token</template>
|
||||
</label>
|
||||
<div class="control has-icons-left">
|
||||
<input class="input" type="text" v-model="backend.token" required>
|
||||
<div class="icon is-left">
|
||||
<i class="fas fa-key"></i>
|
||||
<div class="field-body">
|
||||
<div class="field">
|
||||
<div class="field has-addons">
|
||||
<div class="control is-expanded has-icons-left">
|
||||
<input class="input" v-model="backend.token" required
|
||||
:type="false === exposeToken ? 'password' : 'text'">
|
||||
<div class="icon is-left">
|
||||
<i class="fas fa-key"></i>
|
||||
</div>
|
||||
</div>
|
||||
<div class="control">
|
||||
<button type="button" class="button is-primary" @click="exposeToken = !exposeToken"
|
||||
v-tooltip="'Toggle token'">
|
||||
<span class="icon" v-if="!exposeToken"><i class="fas fa-eye"></i></span>
|
||||
<span class="icon" v-else><i class="fas fa-eye-slash"></i></span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<p class="help">
|
||||
<template v-if="'plex'===backend.type">
|
||||
Enter the <code>X-Plex-Token</code>.
|
||||
<NuxtLink target="_blank"
|
||||
to="https://support.plex.tv/articles/204059436-finding-an-authentication-token-x-plex-token/"
|
||||
v-text="'Visit This article for more information.'"/>
|
||||
</template>
|
||||
<template v-else>
|
||||
You can generate a new API key from <code>Dashboard > Settings > API Keys</code>.
|
||||
</template>
|
||||
</p>
|
||||
</div>
|
||||
<p class="help">
|
||||
<template v-if="'plex'===backend.type">
|
||||
Enter the <code>X-Plex-Token</code>. <a target="_blank"
|
||||
href="https://support.plex.tv/articles/204059436-finding-an-authentication-token-x-plex-token/">
|
||||
Visit This article for more information
|
||||
</a>.
|
||||
</template>
|
||||
<template v-else>
|
||||
Generate a new API token from <code>Dashboard > Settings > API Keys</code>.
|
||||
</template>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -94,7 +123,7 @@
|
||||
<div class="control has-icons-left">
|
||||
<input class="input" type="text" v-model="backend.uuid" required>
|
||||
<div class="icon is-left">
|
||||
<i class="fas fa-server" v-if="!uuidLoading"></i>
|
||||
<i class="fas fa-cloud" v-if="!uuidLoading"></i>
|
||||
<i class="fas fa-spinner fa-pulse" v-else></i>
|
||||
</div>
|
||||
<p class="help">
|
||||
@@ -287,6 +316,7 @@
|
||||
<script setup>
|
||||
import 'assets/css/bulma-switch.css'
|
||||
import {notification, ucFirst} from '~/utils/index.js'
|
||||
import {ref} from "vue";
|
||||
|
||||
const id = useRoute().params.backend
|
||||
const backend = ref({
|
||||
@@ -309,6 +339,9 @@ const uuidLoading = ref(false)
|
||||
const optionsList = ref([])
|
||||
const selectedOption = ref('')
|
||||
const newOptions = ref({})
|
||||
const exposeToken = ref(false)
|
||||
const servers = ref([])
|
||||
const serversLoading = ref(false)
|
||||
|
||||
const selectedOptionHelp = computed(() => {
|
||||
const option = optionsList.value.find(v => v.key === selectedOption.value)
|
||||
@@ -321,6 +354,10 @@ const loadContent = async () => {
|
||||
const content = await request(`/backend/${id}`)
|
||||
backend.value = await content.json()
|
||||
|
||||
if ('plex' === backend.value.type) {
|
||||
await getServers()
|
||||
}
|
||||
|
||||
await getUsers()
|
||||
|
||||
isLoading.value = false
|
||||
@@ -475,6 +512,46 @@ const filteredOptions = (options) => {
|
||||
return options.filter(v => !backend.value.options[v.key] && !newOptions.value[v.key])
|
||||
}
|
||||
|
||||
|
||||
const getServers = async () => {
|
||||
if ('plex' !== backend.value.type || servers.value.length > 0) {
|
||||
return
|
||||
}
|
||||
|
||||
if (!backend.value.token) {
|
||||
notification('error', 'Error', `Token is required to get list of servers.`)
|
||||
return
|
||||
}
|
||||
|
||||
serversLoading.value = true
|
||||
|
||||
let data = {
|
||||
token: backend.value.token,
|
||||
url: window.location.origin,
|
||||
};
|
||||
|
||||
const response = await request(`/backends/discover/${backend.value.type}`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(data)
|
||||
})
|
||||
|
||||
serversLoading.value = false
|
||||
|
||||
const json = await response.json()
|
||||
|
||||
if (200 !== response.status) {
|
||||
notification('error', 'Error', `${json.error.code}: ${json.error.message}`)
|
||||
return
|
||||
}
|
||||
|
||||
servers.value = json
|
||||
}
|
||||
|
||||
const updateIdentifier = async () => {
|
||||
backend.value.uuid = servers.value.find(s => s.uri === backend.value.url).identifier
|
||||
await getUsers()
|
||||
}
|
||||
|
||||
onMounted(async () => await loadContent())
|
||||
|
||||
</script>
|
||||
|
||||
@@ -4,14 +4,19 @@ declare(strict_types=1);
|
||||
|
||||
namespace App\API\Backend;
|
||||
|
||||
use App\Backends\Common\Cache as BackendCache;
|
||||
use App\Backends\Common\ClientInterface as iClient;
|
||||
use App\Backends\Common\Context;
|
||||
use App\Libs\Attributes\Route\Patch;
|
||||
use App\Libs\Attributes\Route\Put;
|
||||
use App\Libs\Config;
|
||||
use App\Libs\ConfigFile;
|
||||
use App\Libs\Container;
|
||||
use App\Libs\DataUtil;
|
||||
use App\Libs\Exceptions\Backends\InvalidContextException;
|
||||
use App\Libs\HTTP_STATUS;
|
||||
use App\Libs\Traits\APITraits;
|
||||
use App\Libs\Uri;
|
||||
use JsonException;
|
||||
use Psr\Http\Message\ResponseInterface as iResponse;
|
||||
use Psr\Http\Message\ServerRequestInterface as iRequest;
|
||||
@@ -48,20 +53,40 @@ final class Update
|
||||
return api_error(r("Backend '{name}' not found.", ['name' => $name]), HTTP_STATUS::HTTP_NOT_FOUND);
|
||||
}
|
||||
|
||||
$this->backendFile->set(
|
||||
$name,
|
||||
$this->fromRequest($this->backendFile->get($name), $request, $this->getClient($name))
|
||||
)->persist();
|
||||
try {
|
||||
$client = $this->getClient($name);
|
||||
|
||||
$backend = $this->getBackends(name: $name);
|
||||
$config = DataUtil::fromArray($this->fromRequest($this->backendFile->get($name), $request, $client));
|
||||
|
||||
if (empty($backend)) {
|
||||
return api_error(r("Backend '{name}' not found.", ['name' => $name]), HTTP_STATUS::HTTP_NOT_FOUND);
|
||||
$context = new Context(
|
||||
clientName: $this->backendFile->get("{$name}.type"),
|
||||
backendName: $name,
|
||||
backendUrl: new Uri($config->get('url')),
|
||||
cache: Container::get(BackendCache::class),
|
||||
backendId: $config->get('uuid', null),
|
||||
backendToken: $this->backendFile->get("{$name}.token", null),
|
||||
backendUser: $config->get('user', null),
|
||||
options: $config->get('options', []),
|
||||
);
|
||||
|
||||
if (false === $client->validateContext($context)) {
|
||||
return api_error('Context information validation failed.', HTTP_STATUS::HTTP_BAD_REQUEST);
|
||||
}
|
||||
|
||||
$this->backendFile->set($name, $config->getAll())->persist();
|
||||
|
||||
$backend = $this->getBackends(name: $name);
|
||||
|
||||
if (empty($backend)) {
|
||||
return api_error(r("Backend '{name}' not found.", ['name' => $name]), HTTP_STATUS::HTTP_NOT_FOUND);
|
||||
}
|
||||
|
||||
$backend = array_pop($backend);
|
||||
|
||||
return api_response(HTTP_STATUS::HTTP_OK, $backend);
|
||||
} catch (InvalidContextException $e) {
|
||||
return api_error($e->getMessage(), HTTP_STATUS::HTTP_BAD_REQUEST);
|
||||
}
|
||||
|
||||
$backend = array_pop($backend);
|
||||
|
||||
return api_response(HTTP_STATUS::HTTP_OK, $backend);
|
||||
}
|
||||
|
||||
#[Patch(Index::URL . '/{name:backend}[/]', name: 'backend.patch')]
|
||||
|
||||
@@ -692,6 +692,16 @@ class PlexClient implements iClient
|
||||
$arr['AccessToken'] = ag($attr, 'accessToken');
|
||||
}
|
||||
|
||||
if (false !== filter_var(ag($arr, 'address'), FILTER_VALIDATE_IP)) {
|
||||
$list['list'][] = array_replace_recursive($arr, [
|
||||
'proto' => 'http',
|
||||
'uri' => r('http://{ip}:{port}', [
|
||||
'ip' => ag($arr, 'address'),
|
||||
'port' => ag($arr, 'port')
|
||||
]),
|
||||
]);
|
||||
}
|
||||
|
||||
$list['list'][] = $arr;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -37,7 +37,7 @@ final class APIKeyRequiredMiddleware implements MiddlewareInterface
|
||||
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
|
||||
{
|
||||
if ('OPTIONS' === $request->getMethod() || true === (bool)$request->getAttribute('INTERNAL_REQUEST', false)) {
|
||||
return $handler->handle($request);
|
||||
return $handler->handle($request->withoutAttribute('INTERNAL_REQUEST'));
|
||||
}
|
||||
|
||||
$requestPath = rtrim($request->getUri()->getPath(), '/');
|
||||
|
||||
@@ -1383,7 +1383,7 @@ if (!function_exists('APIRequest')) {
|
||||
'REQUEST_URI' => Config::get('api.prefix') . $uri->getPath(),
|
||||
'SERVER_NAME' => 'localhost',
|
||||
'SERVER_PORT' => 80,
|
||||
'HTTP_USER_AGENT' => 'Mozilla/5.0 (WatchState/' . getAppVersion() . '; Internal API Request)',
|
||||
'HTTP_USER_AGENT' => Config::get('http.default.options.headers.User-Agent', 'APIRequest'),
|
||||
...ag($opts, 'server', []),
|
||||
];
|
||||
|
||||
|
||||
Reference in New Issue
Block a user