diff --git a/frontend/app/components/BackendAdd.vue b/frontend/app/components/BackendAdd.vue index 6be2e81c..8a2b8305 100644 --- a/frontend/app/components/BackendAdd.vue +++ b/frontend/app/components/BackendAdd.vue @@ -918,6 +918,8 @@ const getAccessToken = async (): Promise => { const getUsers = async ( showAlert: boolean = true, forceReload: boolean = false, + withTokens: boolean = false, + targetUser: string | null = null, ): Promise | undefined> => { const required_values = ['type', 'token', 'url', 'uuid']; @@ -963,7 +965,12 @@ const getUsers = async ( } const query = new URLSearchParams(); - query.append('tokens', '1'); + if (withTokens) { + query.append('tokens', '1'); + if (targetUser) { + query.append('target_user', targetUser); + } + } if (forceReload) { query.append('no_cache', '1'); } @@ -1091,6 +1098,30 @@ const changeStep = async (): Promise => { return; } + if ('plex' === backend.value.type) { + const selected = users.value.find((u) => u.id === backend.value.user); + if (!selected) { + notification('error', 'Error', 'Selected user not found.'); + return; + } + + const usersResponse = await getUsers(true, true, true, selected.uuid ?? selected.id); + const updated = usersResponse?.find((u) => u.id === selected.id); + const token = updated?.token ?? selected.token; + + if (!token) { + notification('error', 'Error', 'Selected user does not have a valid token.'); + return; + } + + if (!backend.value.options?.ADMIN_TOKEN) { + backend.value.options.ADMIN_TOKEN = backend.value.token; + } + backend.value.token = token; + backend.value.options.plex_user_name = updated?.name ?? selected.name; + backend.value.options.plex_user_uuid = updated?.uuid ?? selected.uuid ?? ''; + } + stage.value = 4; } @@ -1112,11 +1143,18 @@ const addBackend = async (): Promise => { } if ('plex' === backend.value.type) { - const token = users.value.find((u) => u.id === backend.value.user)?.token; + const selectedUser = users.value.find((u) => u.id === backend.value.user); + const token = selectedUser?.token; if (token && token !== backend.value.token) { - backend.value.options.ADMIN_TOKEN = backend.value.token; + if (!backend.value.options?.ADMIN_TOKEN) { + backend.value.options.ADMIN_TOKEN = backend.value.token; + } backend.value.token = token; } + if (selectedUser) { + backend.value.options.plex_user_name = selectedUser.name; + backend.value.options.plex_user_uuid = selectedUser.uuid ?? ''; + } } if (isLimited.value) { diff --git a/frontend/app/pages/backend/[backend]/edit.vue b/frontend/app/pages/backend/[backend]/edit.vue index 206d0d85..9382af8f 100644 --- a/frontend/app/pages/backend/[backend]/edit.vue +++ b/frontend/app/pages/backend/[backend]/edit.vue @@ -358,6 +358,17 @@ Reload +
+ +

@@ -781,7 +792,12 @@ const getUUid = async (): Promise => { backend.value.uuid = json.identifier; }; -const getUsers = async (showAlert: boolean = true, forceReload: boolean = false): Promise => { +const getUsers = async ( + showAlert: boolean = true, + forceReload: boolean = false, + withTokens: boolean = false, + targetUser: string | null = null, +): Promise | undefined> => { const required_values: Array = ['type', 'token', 'url', 'uuid']; if (required_values.some((v) => !backend.value[v])) { @@ -814,7 +830,12 @@ const getUsers = async (showAlert: boolean = true, forceReload: boolean = false) }); const query = new URLSearchParams(); - query.append('tokens', '1'); + if (withTokens) { + query.append('tokens', '1'); + if (targetUser) { + query.append('target_user', targetUser); + } + } if (forceReload) { query.append('no_cache', '1'); } @@ -842,6 +863,37 @@ const getUsers = async (showAlert: boolean = true, forceReload: boolean = false) } users.value = json; + return users.value; +}; + +const generateUserToken = async (): Promise => { + if ('plex' !== backend.value.type) { + return; + } + + if (!backend.value.user) { + notification('error', 'Error', 'Select a user to generate a token.'); + return; + } + + const selectedUser = users.value.find((user) => user.id === backend.value.user); + if (!selectedUser) { + notification('error', 'Error', 'Selected user not found.'); + return; + } + + const usersResponse = await getUsers(true, true, true, selectedUser.uuid ?? selectedUser.id); + const updated = usersResponse?.find((user) => user.id === selectedUser.id); + const token = updated?.token ?? selectedUser.token; + if (!token) { + notification('error', 'Error', 'User token not found'); + return; + } + + if (!backend.value.options?.ADMIN_TOKEN) { + backend.value.options.ADMIN_TOKEN = backend.value.token; + } + backend.value.token = token; }; // -- if users updated we need to reset the token in-case the plex auth changed @@ -870,12 +922,14 @@ watch( // Check if the user has a token if (!selectedUser.token) { - notification('error', 'Error', 'Selected user does not have a valid token'); return; } // Only update if the token has actually changed if (selectedUser.token !== backend.value.token) { + if (!backend.value.options?.ADMIN_TOKEN) { + backend.value.options.ADMIN_TOKEN = backend.value.token; + } backend.value.token = selectedUser.token; notification('info', 'Information', `Token updated for user: ${selectedUser.name}`); } @@ -1014,11 +1068,11 @@ watch( backend.value.options.plex_user_uuid = selectedUser.uuid; if (!selectedUser.token) { - notification('error', 'Error', 'User token not found'); return; } if (selectedUser.token !== backend.value.token) { + backend.value.options.ADMIN_TOKEN = backend.value.token; backend.value.token = selectedUser.token; notification('info', 'Information', `Token updated for user: ${selectedUser.name}`); } diff --git a/frontend/app/types/index.d.ts b/frontend/app/types/index.d.ts index b560f417..15de6b3c 100644 --- a/frontend/app/types/index.d.ts +++ b/frontend/app/types/index.d.ts @@ -300,6 +300,8 @@ export interface BackendUser { token?: string; /** Optional user type */ type?: string; + /** Optional user uuid */ + uuid?: string; } /** diff --git a/src/API/Backend/Users.php b/src/API/Backend/Users.php index fd671a3d..e5acd21a 100644 --- a/src/API/Backend/Users.php +++ b/src/API/Backend/Users.php @@ -41,6 +41,10 @@ public function __invoke(iRequest $request, string $name, iImport $mapper, iLogg $opts['tokens'] = true; } + if (null !== ($user = $params->get(Options::TARGET_USER, null))) { + $opts[Options::TARGET_USER] = $user; + } + if (true === (bool) $params->get('raw', false)) { $opts[Options::RAW_RESPONSE] = true; } diff --git a/src/API/Backends/Users.php b/src/API/Backends/Users.php index e8697e2d..9fe07726 100644 --- a/src/API/Backends/Users.php +++ b/src/API/Backends/Users.php @@ -36,6 +36,10 @@ public function __invoke(iRequest $request, string $type, iLogger $logger): iRes $opts[Options::GET_TOKENS] = true; } + if (null !== ($user = $params->get(Options::TARGET_USER, null))) { + $opts[Options::TARGET_USER] = $user; + } + if (true === (bool) $params->get('no_cache', false)) { $opts[Options::NO_CACHE] = true; } diff --git a/src/Backends/Plex/Action/GetUserToken.php b/src/Backends/Plex/Action/GetUserToken.php index 22f8fef0..a4e6a7ea 100644 --- a/src/Backends/Plex/Action/GetUserToken.php +++ b/src/Backends/Plex/Action/GetUserToken.php @@ -88,7 +88,7 @@ private function getUserToken(Context $context, int|string $userId, string $user 'url' => (string) $url, ]); - $opts['user_info'] = ['username' => $username]; + $opts['user_info'] = ['username' => $username, 'user_id' => $userId]; $response = $this->request(Method::POST, $url, Status::CREATED, $context, array_replace_recursive([ 'headers' => ['Accept' => 'application/json'], @@ -141,6 +141,10 @@ private function getUserToken(Context $context, int|string $userId, string $user ], ], $opts)); + if (true === $response instanceof Response) { + return $response; + } + $json = json_decode( json: $response->getContent(), associative: true, diff --git a/src/Backends/Plex/Action/GetUsersList.php b/src/Backends/Plex/Action/GetUsersList.php index 1af97ca6..57d7fbc3 100644 --- a/src/Backends/Plex/Action/GetUsersList.php +++ b/src/Backends/Plex/Action/GetUsersList.php @@ -152,7 +152,7 @@ private function getHomeUsers(Response $users, Context $context, array $opts = [ 'Accept' => 'application/json', ], ]); - } catch (InvalidArgumentException $e) { + } catch (InvalidArgumentException|iException $e) { return new Response( status: false, error: new Error( @@ -219,6 +219,12 @@ private function processHomeUsers( $external = $users->isSuccessful() ? $users->response : []; + if (null !== ($targetUser = ag($opts, Options::TARGET_USER, null))) { + if ('' === ($targetUser = (string) $targetUser)) { + $targetUser = null; + } + } + $list = []; foreach (ag($json, 'users', []) as $data) { @@ -243,23 +249,32 @@ private function processHomeUsers( ]; if (true === (bool) ag($opts, Options::GET_TOKENS)) { - $tokenRequest = Container::getNew(GetUserToken::class)( - context: $context, - userId: ag($data, 'uuid'), - username: ag($data, 'name'), - ); - - if ($tokenRequest->hasError() && $tokenRequest->error) { - $this->logger->log( - $tokenRequest->error->level(), - $tokenRequest->error->message, - $tokenRequest->error->context, + $matchesTarget = + null === $targetUser + || (string) $targetUser === (string) ag($data, 'id') + || (string) $targetUser === (string) ag($data, 'uuid'); + + if (true === $matchesTarget) { + $tokenRequest = Container::getNew(GetUserToken::class)( + context: $context, + userId: ag($data, 'uuid'), + username: ag($data, 'name'), ); - } - $data['token'] = $tokenRequest->isSuccessful() ? $tokenRequest->response : null; - if (true === $tokenRequest->hasError() && $tokenRequest->error) { - $data['token_error'] = ag($tokenRequest->error->extra, 'error', $tokenRequest->error->format()); + if ($tokenRequest->hasError() && $tokenRequest->error) { + $this->logger->log( + $tokenRequest->error->level(), + $tokenRequest->error->message, + $tokenRequest->error->context, + ); + } + + $data['token'] = $tokenRequest->isSuccessful() ? $tokenRequest->response : null; + if (true === $tokenRequest->hasError() && $tokenRequest->error) { + $data['token_error'] = ag($tokenRequest->error->extra, 'error', $tokenRequest->error->format()); + } + } elseif (null !== $targetUser) { + $data['token'] = null; } } @@ -340,7 +355,7 @@ private function getExternalUsers(Context $context, array $opts = []): Response if (true !== (bool) ag($opts, Options::GET_TOKENS) || count($users) < 1) { return new Response(status: true, response: $users); } - } catch (InvalidArgumentException $e) { + } catch (InvalidArgumentException|iException $e) { return new Response( status: false, error: new Error( @@ -377,7 +392,7 @@ private function getExternalUsers(Context $context, array $opts = []): Response 'Accept' => 'application/xml', ], ]); - } catch (InvalidArgumentException $e) { + } catch (InvalidArgumentException|iException $e) { return new Response( status: false, error: new Error( diff --git a/src/Libs/Options.php b/src/Libs/Options.php index c35030ed..9ec1e821 100644 --- a/src/Libs/Options.php +++ b/src/Libs/Options.php @@ -52,6 +52,7 @@ final class Options public const string ALT_NAME = 'ALT_NAME'; public const string ALT_ID = 'ALT_ID'; public const string CONTEXT_USER = 'CONTEXT_USER'; + public const string TARGET_USER = 'target_user'; public const string GET_TOKENS = 'tokens'; public const string LOG_CONTEXT = 'LOG_CONTEXT'; public const string DELAY_BY = 'DELAY_BY'; diff --git a/tests/Backends/Plex/GetUsersListTest.php b/tests/Backends/Plex/GetUsersListTest.php index 2eb24565..b5944aa6 100644 --- a/tests/Backends/Plex/GetUsersListTest.php +++ b/tests/Backends/Plex/GetUsersListTest.php @@ -5,8 +5,10 @@ namespace Tests\Backends\Plex; use App\Backends\Plex\Action\GetUsersList; +use App\Libs\Container; use App\Libs\Options; use Symfony\Component\HttpClient\Response\MockResponse; +use Symfony\Contracts\HttpClient\HttpClientInterface as iHttp; class GetUsersListTest extends PlexTestCase { @@ -66,6 +68,84 @@ public function test_get_users_list_home_users(): void $this->assertTrue($result->response[0]['admin']); } + public function test_get_users_list_target_token(): void + { + $externalUsersXml = ''; + $homeUsersJson = json_encode([ + 'users' => [ + [ + 'id' => 1, + 'uuid' => 'uuid-1', + 'friendlyName' => 'Test User 1', + 'admin' => true, + 'guest' => false, + 'restricted' => false, + 'protected' => false, + 'updatedAt' => '2024-01-01T00:00:00Z', + ], + [ + 'id' => 2, + 'uuid' => 'uuid-2', + 'friendlyName' => 'Test User 2', + 'admin' => false, + 'guest' => false, + 'restricted' => false, + 'protected' => false, + 'updatedAt' => '2024-01-02T00:00:00Z', + ], + ], + ]); + $switchJson = json_encode(['authToken' => 'temp-token']); + $resourcesJson = json_encode([ + [ + 'clientIdentifier' => 'plex-server-1', + 'accessToken' => 'token-uuid-2', + 'provides' => 'server', + 'name' => 'Plex Server', + ], + ]); + + $requests = []; + $http = new \App\Libs\Extends\MockHttpClient(function (string $method, string $url) use (&$requests, $externalUsersXml, $homeUsersJson, $switchJson, $resourcesJson) { + $requests[] = $url; + if (str_contains($url, '/api/v2/home/users/') && str_contains($url, '/switch')) { + if (str_contains($url, '/api/v2/home/users/uuid-2/switch')) { + return new MockResponse($switchJson, ['http_code' => 201]); + } + + return new MockResponse('denied', ['http_code' => 403]); + } + + if (str_contains($url, '/api/v2/resources')) { + return new MockResponse($resourcesJson, ['http_code' => 200]); + } + + if (str_contains($url, '/api/v2/home/users/')) { + return new MockResponse($homeUsersJson, ['http_code' => 200]); + } + + return new MockResponse($externalUsersXml, ['http_code' => 200]); + }); + + Container::add(iHttp::class, fn() => $http); + + $context = $this->makeContext(); + $action = new GetUsersList($http, $this->logger); + $result = $action($context, [Options::GET_TOKENS => true, Options::TARGET_USER => 'uuid-2']); + + $this->assertTrue($result->isSuccessful()); + $this->assertNull($result->response[0]['token'] ?? null); + $this->assertSame('token-uuid-2', $result->response[1]['token'] ?? null); + $this->assertFalse( + array_reduce( + $requests, + static fn(bool $found, string $url) => $found || str_contains($url, '/api/v2/home/users/uuid-1/switch'), + false, + ), + 'Unexpected token request for non-target user.', + ); + } + public function test_get_users_list_error_status(): void { $http = new \App\Libs\Extends\MockHttpClient([