Initial support for adding plex users with PIN. Fixes #542
This commit is contained in:
@@ -173,5 +173,11 @@ return [
|
|||||||
'visible' => false,
|
'visible' => false,
|
||||||
'description' => 'Whether the token has limited access.',
|
'description' => 'Whether the token has limited access.',
|
||||||
],
|
],
|
||||||
|
[
|
||||||
|
'key' => 'options.PLEX_USER_PIN',
|
||||||
|
'type' => 'int',
|
||||||
|
'visible' => false,
|
||||||
|
'description' => 'Plex user PIN.',
|
||||||
|
],
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|||||||
@@ -175,6 +175,16 @@
|
|||||||
<NuxtLink @click="getUsers" v-text="'Retrieve User ids from backend.'" v-if="stage < 4"/>
|
<NuxtLink @click="getUsers" v-text="'Retrieve User ids from backend.'" v-if="stage < 4"/>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
<template v-if="'plex' === backend.type">
|
||||||
|
<label class="label">User PIN</label>
|
||||||
|
<div class="control has-icons-left">
|
||||||
|
<input class="input" type="text" v-model="backend.options.PLEX_USER_PIN" :disabled="stage > 3">
|
||||||
|
<div class="icon is-left"><i class="fas fa-key"></i></div>
|
||||||
|
<p class="help">
|
||||||
|
If the selected user is using <code>PIN</code> to login, enter the PIN here.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<template v-if="stage >= 4">
|
<template v-if="stage >= 4">
|
||||||
@@ -272,11 +282,17 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="card-footer">
|
<div class="card-footer">
|
||||||
|
|
||||||
|
<div class="card-footer-item" v-if="stage >= 1">
|
||||||
|
<button class="button is-fullwidth is-warning" type="button" @click="stage = stage-1">
|
||||||
|
<span class="icon"><i class="fas fa-arrow-left"></i></span>
|
||||||
|
<span>Previous Step</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="card-footer-item" v-if="stage < maxStages">
|
<div class="card-footer-item" v-if="stage < maxStages">
|
||||||
<button class="button is-fullwidth is-primary" type="submit" @click="changeStep()">
|
<button class="button is-fullwidth is-info" type="submit" @click="changeStep()">
|
||||||
<span class="icon">
|
<span class="icon"><i class="fas fa-arrow-right"></i></span>
|
||||||
<i class="fas fa-arrow-right"></i>
|
|
||||||
</span>
|
|
||||||
<span>Next Step</span>
|
<span>Next Step</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -500,10 +516,6 @@ onMounted(async () => {
|
|||||||
backend.value.type = supported.value[0]
|
backend.value.type = supported.value[0]
|
||||||
})
|
})
|
||||||
|
|
||||||
watch(stage, v => {
|
|
||||||
console.log(v);
|
|
||||||
}, {immediate: true})
|
|
||||||
|
|
||||||
const changeStep = async () => {
|
const changeStep = async () => {
|
||||||
let _
|
let _
|
||||||
|
|
||||||
|
|||||||
@@ -590,6 +590,12 @@ const getServers = async () => {
|
|||||||
url: window.location.origin,
|
url: window.location.origin,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (backend.value?.options && backend.value.options?.ADMIN_TOKEN) {
|
||||||
|
data.options = {
|
||||||
|
ADMIN_TOKEN: backend.value.options.ADMIN_TOKEN
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const response = await request(`/backends/discover/${backend.value.type}`, {
|
const response = await request(`/backends/discover/${backend.value.type}`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
body: JSON.stringify(data)
|
body: JSON.stringify(data)
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ use App\Backends\Plex\PlexClient;
|
|||||||
use App\Libs\Attributes\Route\Get;
|
use App\Libs\Attributes\Route\Get;
|
||||||
use App\Libs\Enums\Http\Status;
|
use App\Libs\Enums\Http\Status;
|
||||||
use App\Libs\Exceptions\InvalidArgumentException;
|
use App\Libs\Exceptions\InvalidArgumentException;
|
||||||
|
use App\Libs\Options;
|
||||||
use App\Libs\Traits\APITraits;
|
use App\Libs\Traits\APITraits;
|
||||||
use Psr\Http\Message\ResponseInterface as iResponse;
|
use Psr\Http\Message\ResponseInterface as iResponse;
|
||||||
use Psr\Http\Message\ServerRequestInterface as iRequest;
|
use Psr\Http\Message\ServerRequestInterface as iRequest;
|
||||||
@@ -42,7 +43,14 @@ final class Discover
|
|||||||
|
|
||||||
assert($client instanceof PlexClient);
|
assert($client instanceof PlexClient);
|
||||||
|
|
||||||
$list = $client::discover($this->http, $client->getContext()->backendToken);
|
$context = $client->getContext();
|
||||||
|
$opts = [];
|
||||||
|
|
||||||
|
if (null !== ($adminToken = ag($context->options, Options::ADMIN_TOKEN))) {
|
||||||
|
$opts[Options::ADMIN_TOKEN] = $adminToken;
|
||||||
|
}
|
||||||
|
|
||||||
|
$list = $client::discover($this->http, $context->backendToken, $opts);
|
||||||
return api_response(Status::OK, ag($list, 'list', []));
|
return api_response(Status::OK, ag($list, 'list', []));
|
||||||
} catch (InvalidArgumentException $e) {
|
} catch (InvalidArgumentException $e) {
|
||||||
return api_error($e->getMessage(), Status::NOT_FOUND);
|
return api_error($e->getMessage(), Status::NOT_FOUND);
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ use App\Libs\Attributes\Route\Route;
|
|||||||
use App\Libs\DataUtil;
|
use App\Libs\DataUtil;
|
||||||
use App\Libs\Enums\Http\Status;
|
use App\Libs\Enums\Http\Status;
|
||||||
use App\Libs\Exceptions\InvalidArgumentException;
|
use App\Libs\Exceptions\InvalidArgumentException;
|
||||||
|
use App\Libs\Options;
|
||||||
use App\Libs\Traits\APITraits;
|
use App\Libs\Traits\APITraits;
|
||||||
use Psr\Http\Message\ResponseInterface as iResponse;
|
use Psr\Http\Message\ResponseInterface as iResponse;
|
||||||
use Psr\Http\Message\ServerRequestInterface as iRequest;
|
use Psr\Http\Message\ServerRequestInterface as iRequest;
|
||||||
@@ -42,7 +43,13 @@ final class Discover
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
$list = $client::discover($this->http, $client->getContext()->backendToken);
|
$opts = [];
|
||||||
|
|
||||||
|
if (null !== ($adminToken = ag($request->getParsedBody(), 'options.' . Options::ADMIN_TOKEN))) {
|
||||||
|
$opts[Options::ADMIN_TOKEN] = $adminToken;
|
||||||
|
}
|
||||||
|
|
||||||
|
$list = $client::discover($this->http, $client->getContext()->backendToken, $opts);
|
||||||
return api_response(Status::OK, ag($list, 'list', []));
|
return api_response(Status::OK, ag($list, 'list', []));
|
||||||
} catch (Throwable $e) {
|
} catch (Throwable $e) {
|
||||||
return api_error($e->getMessage(), Status::INTERNAL_SERVER_ERROR);
|
return api_error($e->getMessage(), Status::INTERNAL_SERVER_ERROR);
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ use App\Backends\Common\Error;
|
|||||||
use App\Backends\Common\Levels;
|
use App\Backends\Common\Levels;
|
||||||
use App\Backends\Common\Response;
|
use App\Backends\Common\Response;
|
||||||
use App\Libs\Container;
|
use App\Libs\Container;
|
||||||
|
use App\Libs\Options;
|
||||||
use Psr\Http\Message\UriInterface;
|
use Psr\Http\Message\UriInterface;
|
||||||
use Psr\Log\LoggerInterface;
|
use Psr\Log\LoggerInterface;
|
||||||
use Symfony\Component\HttpClient\RetryableHttpClient;
|
use Symfony\Component\HttpClient\RetryableHttpClient;
|
||||||
@@ -71,6 +72,10 @@ final class GetUserToken
|
|||||||
'url' => (string)$url,
|
'url' => (string)$url,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
if (null !== ($pin = ag($context->options, Options::PLEX_USER_PIN))) {
|
||||||
|
$url = $url->withQuery(http_build_query(['pin' => $pin]));
|
||||||
|
}
|
||||||
|
|
||||||
$response = $this->http->request('POST', (string)$url, [
|
$response = $this->http->request('POST', (string)$url, [
|
||||||
'headers' => [
|
'headers' => [
|
||||||
'Accept' => 'application/json',
|
'Accept' => 'application/json',
|
||||||
@@ -83,13 +88,14 @@ final class GetUserToken
|
|||||||
return new Response(
|
return new Response(
|
||||||
status: false,
|
status: false,
|
||||||
error: new Error(
|
error: new Error(
|
||||||
message: 'Request for temporary access token for [{backend}] user [{username}] failed due to rate limit. error 429.',
|
message: "Request for temporary access token for '{backend}' user '{username}'{pin} failed due to rate limit. error 429.",
|
||||||
context: [
|
context: [
|
||||||
'backend' => $context->backendName,
|
'backend' => $context->backendName,
|
||||||
'username' => $username,
|
'username' => $username,
|
||||||
'user_id' => $userId,
|
'user_id' => $userId,
|
||||||
'status_code' => $response->getStatusCode(),
|
'status_code' => $response->getStatusCode(),
|
||||||
'headers' => $response->getHeaders(),
|
'headers' => $response->getHeaders(),
|
||||||
|
'pin' => null !== $pin ? ' with pin' : '',
|
||||||
],
|
],
|
||||||
level: Levels::ERROR
|
level: Levels::ERROR
|
||||||
),
|
),
|
||||||
@@ -100,13 +106,14 @@ final class GetUserToken
|
|||||||
return new Response(
|
return new Response(
|
||||||
status: false,
|
status: false,
|
||||||
error: new Error(
|
error: new Error(
|
||||||
message: 'Request for [{backend}] user [{username}] temporary access token responded with unexpected [{status_code}] status code.',
|
message: "Request for '{backend}' user '{username}'{pin} temporary access token responded with unexpected '{status_code}' status code.",
|
||||||
context: [
|
context: [
|
||||||
'backend' => $context->backendName,
|
'backend' => $context->backendName,
|
||||||
'username' => $username,
|
'username' => $username,
|
||||||
'user_id' => $userId,
|
'user_id' => $userId,
|
||||||
'status_code' => $response->getStatusCode(),
|
'status_code' => $response->getStatusCode(),
|
||||||
'headers' => $response->getHeaders(),
|
'headers' => $response->getHeaders(),
|
||||||
|
'pin' => null !== $pin ? ' with pin' : '',
|
||||||
],
|
],
|
||||||
level: Levels::ERROR
|
level: Levels::ERROR
|
||||||
),
|
),
|
||||||
@@ -120,13 +127,14 @@ final class GetUserToken
|
|||||||
);
|
);
|
||||||
|
|
||||||
if ($context->trace) {
|
if ($context->trace) {
|
||||||
$this->logger->debug('Parsing temporary access token for [{backend}] user [{username}] payload.', [
|
$this->logger->debug("Parsing temporary access token for '{backend}' user '{username}'{pin} payload.", [
|
||||||
'backend' => $context->backendName,
|
'backend' => $context->backendName,
|
||||||
'username' => $username,
|
'username' => $username,
|
||||||
'user_id' => $userId,
|
'user_id' => $userId,
|
||||||
'url' => (string)$url,
|
'url' => (string)$url,
|
||||||
'trace' => $json,
|
'trace' => $json,
|
||||||
'headers' => $response->getHeaders(),
|
'headers' => $response->getHeaders(),
|
||||||
|
'pin' => null !== $pin ? ' with pin' : '',
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -141,11 +149,12 @@ final class GetUserToken
|
|||||||
])
|
])
|
||||||
);
|
);
|
||||||
|
|
||||||
$this->logger->debug('Requesting permanent access token for [{backend}] user [{username}].', [
|
$this->logger->debug("Requesting permanent access token for '{backend}' user '{username}'{pin}.", [
|
||||||
'backend' => $context->backendName,
|
'backend' => $context->backendName,
|
||||||
'username' => $username,
|
'username' => $username,
|
||||||
'user_id' => $userId,
|
'user_id' => $userId,
|
||||||
'url' => (string)$url,
|
'url' => (string)$url,
|
||||||
|
'pin' => null !== $pin ? ' with pin' : '',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$response = $this->http->request('GET', (string)$url, [
|
$response = $this->http->request('GET', (string)$url, [
|
||||||
@@ -163,12 +172,13 @@ final class GetUserToken
|
|||||||
);
|
);
|
||||||
|
|
||||||
if ($context->trace) {
|
if ($context->trace) {
|
||||||
$this->logger->debug('Parsing permanent access token for [{backend}] user [{username}] payload.', [
|
$this->logger->debug("Parsing permanent access token for '{backend}' user '{username}'{pin} payload.", [
|
||||||
'backend' => $context->backendName,
|
'backend' => $context->backendName,
|
||||||
'username' => $username,
|
'username' => $username,
|
||||||
'user_id' => $userId,
|
'user_id' => $userId,
|
||||||
'url' => (string)$url,
|
'url' => (string)$url,
|
||||||
'trace' => $json,
|
'trace' => $json,
|
||||||
|
'pin' => null !== $pin ? ' with pin' : '',
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -189,7 +199,7 @@ final class GetUserToken
|
|||||||
}
|
}
|
||||||
|
|
||||||
$this->logger->error(
|
$this->logger->error(
|
||||||
'Response had [{count}] associated servers, non match [{backend} - [{backend_id}] unique identifier.',
|
"Response had '{count}' associated servers, non match '{backend}: {backend_id}' unique identifier.",
|
||||||
[
|
[
|
||||||
'count' => count(($json)),
|
'count' => count(($json)),
|
||||||
'backend' => $context->backendName,
|
'backend' => $context->backendName,
|
||||||
@@ -201,11 +211,12 @@ final class GetUserToken
|
|||||||
return new Response(
|
return new Response(
|
||||||
status: false,
|
status: false,
|
||||||
error: new Error(
|
error: new Error(
|
||||||
message: 'No permanent access token was found for [{username}] in [{backend}] response. Likely invalid unique identifier was selected or plex.tv API error, check https://status.plex.tv or try running same command with [--debug] flag for more information.',
|
message: "No permanent access token was found for '{username}'{pin} in '{backend}' response. Likely invalid unique identifier was selected or plex.tv API error, check https://status.plex.tv or try running same command with [--debug] flag for more information.",
|
||||||
context: [
|
context: [
|
||||||
'backend' => $context->backendName,
|
'backend' => $context->backendName,
|
||||||
'username' => $username,
|
'username' => $username,
|
||||||
'user_id' => $userId,
|
'user_id' => $userId,
|
||||||
|
'pin' => null !== $pin ? ' with pin' : '',
|
||||||
],
|
],
|
||||||
level: Levels::ERROR
|
level: Levels::ERROR
|
||||||
),
|
),
|
||||||
@@ -214,10 +225,11 @@ final class GetUserToken
|
|||||||
return new Response(
|
return new Response(
|
||||||
status: false,
|
status: false,
|
||||||
error: new Error(
|
error: new Error(
|
||||||
message: 'Exception [{error.kind}] was thrown unhandled during [{client}: {backend}] request for [{username}] access token. Error [{error.message} @ {error.file}:{error.line}].',
|
message: "Exception '{error.kind}' was thrown unhandled during '{client}: {backend}' request for '{username}'{pin} access token. Error '{error.message}' at '{error.file}:{error.line}'.",
|
||||||
context: [
|
context: [
|
||||||
'backend' => $context->backendName,
|
'backend' => $context->backendName,
|
||||||
'client' => $context->clientName,
|
'client' => $context->clientName,
|
||||||
|
'pin' => isset($pin) ? ' with pin' : '',
|
||||||
'error' => [
|
'error' => [
|
||||||
'kind' => $e::class,
|
'kind' => $e::class,
|
||||||
'line' => $e->getLine(),
|
'line' => $e->getLine(),
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ use App\Libs\Config;
|
|||||||
use App\Libs\Container;
|
use App\Libs\Container;
|
||||||
use App\Libs\DataUtil;
|
use App\Libs\DataUtil;
|
||||||
use App\Libs\Entity\StateInterface as iState;
|
use App\Libs\Entity\StateInterface as iState;
|
||||||
|
use App\Libs\Enums\Http\Status;
|
||||||
use App\Libs\Exceptions\Backends\RuntimeException;
|
use App\Libs\Exceptions\Backends\RuntimeException;
|
||||||
use App\Libs\Exceptions\HttpException;
|
use App\Libs\Exceptions\HttpException;
|
||||||
use App\Libs\Mappers\ImportInterface as iImport;
|
use App\Libs\Mappers\ImportInterface as iImport;
|
||||||
@@ -671,28 +672,38 @@ class PlexClient implements iClient
|
|||||||
|
|
||||||
$payload = $response->getContent(false);
|
$payload = $response->getContent(false);
|
||||||
|
|
||||||
if (200 !== $response->getStatusCode()) {
|
if (Status::OK !== Status::from($response->getStatusCode())) {
|
||||||
|
if (Status::UNAUTHORIZED === Status::from($response->getStatusCode())) {
|
||||||
|
if (null !== ($adminToken = ag($opts, Options::ADMIN_TOKEN))) {
|
||||||
|
$opts['with_admin'] = true;
|
||||||
|
return self::discover($http, $adminToken, ag_delete($opts, Options::ADMIN_TOKEN));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
throw new RuntimeException(
|
throw new RuntimeException(
|
||||||
r(
|
r(
|
||||||
text: 'PlexClient: Request for servers list returned with unexpected [{status_code}] status code. {context}',
|
text: "PlexClient: Request for servers list returned with unexpected '{status_code}' status code. {context}",
|
||||||
context: [
|
context: [
|
||||||
'status_code' => $response->getStatusCode(),
|
'status_code' => $response->getStatusCode(),
|
||||||
'context' => arrayToString(['payload' => $payload]),
|
'context' => arrayToString([
|
||||||
|
'with_admin' => true === ag($opts, 'with_admin'),
|
||||||
|
'payload' => $payload
|
||||||
|
]),
|
||||||
]
|
]
|
||||||
)
|
), $response->getStatusCode()
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
} catch (TransportExceptionInterface $e) {
|
} catch (TransportExceptionInterface $e) {
|
||||||
throw new RuntimeException(
|
throw new RuntimeException(
|
||||||
r(
|
r(
|
||||||
text: 'PlexClient: Exception [{kind}] was thrown unhandled during request for plex servers list, likely network related error. [{error} @ {file}:{line}]',
|
text: "PlexClient: Exception '{kind}' was thrown unhandled during request for plex servers list, likely network related error. '{error}' at '{file}:{line}'.",
|
||||||
context: [
|
context: [
|
||||||
'kind' => $e::class,
|
'kind' => $e::class,
|
||||||
'error' => $e->getMessage(),
|
'error' => $e->getMessage(),
|
||||||
'line' => $e->getLine(),
|
'line' => $e->getLine(),
|
||||||
'file' => after($e->getFile(), ROOT_PATH),
|
'file' => after($e->getFile(), ROOT_PATH),
|
||||||
]
|
]
|
||||||
)
|
), code: 500, previous: $e
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -409,9 +409,31 @@ class PlexManage implements ManageInterface
|
|||||||
$backend = ag_set($backend, 'user', $map[$user]);
|
$backend = ag_set($backend, 'user', $map[$user]);
|
||||||
$backend = ag_set($backend, 'options.plex_user_uuid', $uuid[$user]);
|
$backend = ag_set($backend, 'options.plex_user_uuid', $uuid[$user]);
|
||||||
|
|
||||||
|
$question = new Question(
|
||||||
|
r(
|
||||||
|
<<<HELP
|
||||||
|
<question>[<value>{name}</value>] User PIN</question>. {default}
|
||||||
|
------------------
|
||||||
|
<notice>Leave empty if the user doesn't have a PIN.</notice>
|
||||||
|
------------------
|
||||||
|
HELP. PHP_EOL . '> ',
|
||||||
|
[
|
||||||
|
'name' => ag($backend, 'name'),
|
||||||
|
'default' => null !== $choice ? "<value>[Default: {$choice}]</value>" : ''
|
||||||
|
]
|
||||||
|
),
|
||||||
|
ag($backend, 'options.' . Options::PLEX_USER_PIN)
|
||||||
|
);
|
||||||
|
|
||||||
|
$pin = $this->questionHelper->ask($this->input, $this->output, $question);
|
||||||
|
if (!empty($pin)) {
|
||||||
|
$backend = ag_set($backend, 'options.' . Options::PLEX_USER_PIN, $pin);
|
||||||
|
}
|
||||||
|
|
||||||
$this->output->writeln(
|
$this->output->writeln(
|
||||||
r('<info>Requesting plex token for [{user}] from plex.tv API.</info>', [
|
r("<info>Requesting plex token for '{user}'{pin} from plex.tv API.</info>", [
|
||||||
'user' => ag($userInfo[$map[$user]], 'name') ?? 'None',
|
'user' => ag($userInfo[$map[$user]], 'name') ?? 'None',
|
||||||
|
'pin' => empty($pin) ? '' : ' with PIN'
|
||||||
])
|
])
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ final class Options
|
|||||||
public const string LIMIT_RESULTS = 'LIMIT_RESULTS';
|
public const string LIMIT_RESULTS = 'LIMIT_RESULTS';
|
||||||
public const string NO_CHECK = 'NO_CHECK';
|
public const string NO_CHECK = 'NO_CHECK';
|
||||||
public const string LOG_WRITER = 'LOG_WRITER';
|
public const string LOG_WRITER = 'LOG_WRITER';
|
||||||
|
public const string PLEX_USER_PIN = 'PLEX_USER_PIN';
|
||||||
|
|
||||||
private function __construct()
|
private function __construct()
|
||||||
{
|
{
|
||||||
|
|||||||
Reference in New Issue
Block a user