Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Plex OAuth #413

Merged
merged 28 commits into from
Jul 1, 2023
Merged
Show file tree
Hide file tree
Changes from 15 commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
c9bcc8a
Add Plex OAuth
JVT038 Jun 27, 2023
08a2796
Some refactoring
leepeuker Jun 28, 2023
38dd165
Add missing data
leepeuker Jun 28, 2023
e37c0f2
Update local plex api
leepeuker Jun 28, 2023
9e44345
Fix tests
leepeuker Jun 28, 2023
2814e0a
Remove obsolete code
leepeuker Jun 28, 2023
c6cfafd
Cleanup
leepeuker Jun 28, 2023
75cfd44
Merge pull request #415 from leepeuker/add-plex-oauth-2
leepeuker Jun 28, 2023
9073427
Use url value objects in for plex
leepeuker Jun 28, 2023
a6bf51c
Simplify code
leepeuker Jun 28, 2023
eda5991
Naming and tests
leepeuker Jun 28, 2023
ca07a32
Merge pull request #416 from leepeuker/add-plex-oauth-url
leepeuker Jun 28, 2023
45b7cc4
Simplify UI
leepeuker Jun 28, 2023
b5dceb0
Fix invalid header issue
leepeuker Jun 28, 2023
b84991b
Rework plex auth frontend
leepeuker Jun 29, 2023
e7d4f27
Add help modal for plex server url
leepeuker Jun 29, 2023
65ebcf8
Improve error handling on not set application url
leepeuker Jun 29, 2023
9ed70ae
Improve help text
leepeuker Jun 29, 2023
1ccba27
Add PLEX_IDENTIFIER env variable to docs
leepeuker Jun 29, 2023
6a7ee10
Add link to configuration docs page to example env files
leepeuker Jun 29, 2023
328d5ee
Naming changes
leepeuker Jun 29, 2023
5d62cda
Simplify exception message
leepeuker Jun 29, 2023
9812f83
Improve and unfiy help modals
leepeuker Jun 30, 2023
7f8a2fd
Improve error handling on missing server config
leepeuker Jun 30, 2023
fcbb58d
Return 400 response if user is already authenticated
leepeuker Jul 1, 2023
8aad533
Remove obsolete code
leepeuker Jul 1, 2023
3c5092b
Fix and improve database migrations
leepeuker Jul 1, 2023
130e53a
Fix naming
leepeuker Jul 1, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions bootstrap.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@
\Movary\Service\ImageCacheService::class => DI\factory([Factory::class, 'createImageCacheService']),
\Movary\JobQueue\JobQueueScheduler::class => DI\factory([Factory::class, 'createJobQueueScheduler']),
\Movary\Api\Tmdb\TmdbClient::class => DI\factory([Factory::class, 'createTmdbApiClient']),
\Movary\Api\Plex\PlexLocalServerClient::class => DI\factory([Factory::class, 'createPlexLocalServerClient']),
\Movary\Api\Plex\PlexTvClient::class => DI\factory([Factory::class, 'createPlexTvClient']),
\Movary\Service\UrlGenerator::class => DI\factory([Factory::class, 'createUrlGenerator']),
\Movary\Service\Export\ExportService::class => DI\factory([Factory::class, 'createExportService']),
\Movary\HttpController\CreateUserController::class => DI\factory([Factory::class, 'createCreateUserController']),
Expand Down
32 changes: 32 additions & 0 deletions db/migrations/mysql/20230627162519_add_plex_columns.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
<?php declare(strict_types=1);

use Phinx\Migration\AbstractMigration;

final class AddPlexColumns extends AbstractMigration
{
public function down() : void
{
$this->execute(
<<<SQL
ALTER TABLE user DROP COLUMN plex_client_id;
ALTER TABLE user DROP COLUMN plex_client_temporary_code;
ALTER TABLE user DROP COLUMN plex_access_token;
ALTER TABLE user DROP COLUMN plex_account_id;
ALTER TABLE user DROP COLUMN plex_server_url;
SQL,
);
}

public function up() : void
{
$this->execute(
<<<SQL
ALTER TABLE user ADD COLUMN plex_client_id CHAR(64) DEFAULT NULL AFTER trakt_client_id;
ALTER TABLE user ADD COLUMN plex_client_temporary_code CHAR(64) DEFAULT NULL AFTER plex_client_id;
ALTER TABLE user ADD COLUMN plex_access_token CHAR(128) DEFAULT NULL AFTER plex_client_temporary_code;
ALTER TABLE user ADD COLUMN plex_account_id CHAR(64) DEFAULT NULL AFTER plex_access_token;
ALTER TABLE user ADD COLUMN plex_server_url CHAR(128) DEFAULT NULL AFTER plex_account_id;
SQL,
);
}
}
73 changes: 73 additions & 0 deletions db/migrations/sqlite/20230627162519_AddPlexColumns.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
<?php declare(strict_types=1);

use Phinx\Migration\AbstractMigration;

final class AddPlexColumns extends AbstractMigration
{
public function down() : void
{
$this->execute(
<<<SQL
CREATE TABLE `tmp_user` (
`id` INTEGER,
`email` TEXT NOT NULL,
`name` TEXT NOT NULL,
`password` TEXT NOT NULL ,
`privacy_level` INTEGER DEFAULT 1,
`date_format_id` INTEGER DEFAULT 0,
`trakt_user_name` TEXT,
`plex_webhook_uuid` TEXT,
`jellyfin_webhook_uuid` TEXT,
`trakt_client_id` TEXT,
`jellyfin_scrobble_views` INTEGER DEFAULT 1,
`plex_scrobble_views` INTEGER DEFAULT 1,
`plex_scrobble_ratings` INTEGER DEFAULT 0,
`core_account_changes_disabled` INTEGER DEFAULT 0,
`created_at` TEXT NOT NULL,
PRIMARY KEY (`id`),
UNIQUE (`email`),
UNIQUE (`name`)
)
SQL,
);
$this->execute('INSERT INTO `tmp_user` (id, email, name, password, privacy_level, date_format_id, trakt_user_name, plex_webhook_uuid, jellyfin_webhook_uuid, trakt_client_id, jellyfin_scrobble_views, plex_scrobble_views, plex_scrobble_ratings, core_account_changes_disabled, created_at) SELECT `id`, `email`, `name`, `password`, `privacy_level`, `date_format_id`, `trakt_user_name`, `plex_webhook_uuid`, `jellyfin_webhook_uuid`, `trakt_client_id`, `jellyfin_scrobble_views`, `plex_scrobble_views`, `plex_scrobble_ratings`, `core_account_changes_disabled`, `created_at` FROM user');
$this->execute('DROP TABLE `user`');
$this->execute('ALTER TABLE `tmp_user` RENAME TO `user`');
}

public function up() : void
{
$this->execute(
<<<SQL
CREATE TABLE `tmp_user` (
`id` INTEGER,
`email` TEXT NOT NULL,
`name` TEXT NOT NULL,
`password` TEXT NOT NULL ,
`privacy_level` INTEGER DEFAULT 1,
`date_format_id` INTEGER DEFAULT 0,
`trakt_user_name` TEXT,
`plex_webhook_uuid` TEXT,
`jellyfin_webhook_uuid` TEXT,
`trakt_client_id` TEXT,
`jellyfin_scrobble_views` INTEGER DEFAULT 1,
`plex_scrobble_views` INTEGER DEFAULT 1,
`plex_scrobble_ratings` INTEGER DEFAULT 0,
`plex_client_id` TEXT DEFAULT NULL,
`plex_client_temporary_code` TEXT DEFAULT NULL,
`plex_access_token` TEXT DEFAULT NULL,
`plex_account_id` TEXT DEFAULT NULL,
`plex_server_url` TEXT DEFAULT NULL,
`core_account_changes_disabled` INTEGER DEFAULT 0,
`created_at` TEXT NOT NULL,
PRIMARY KEY (`id`),
UNIQUE (`email`),
UNIQUE (`name`)
)
SQL,
);
$this->execute('INSERT INTO `tmp_user` (id, email, name, password, privacy_level, date_format_id, trakt_user_name, plex_webhook_uuid, jellyfin_webhook_uuid, trakt_client_id, jellyfin_scrobble_views, plex_scrobble_views, plex_scrobble_ratings, core_account_changes_disabled, created_at) SELECT * FROM user');
$this->execute('DROP TABLE `user`');
$this->execute('ALTER TABLE `tmp_user` RENAME TO `user`');
}
}
126 changes: 126 additions & 0 deletions public/js/settings-integration-plex.js
JVT038 marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
Expand Up @@ -91,3 +91,129 @@ async function updateScrobbleOptions() {
addAlert('alertWebhookOptionsDiv', 'Could not update scrobble options', 'danger')
});
}

async function authenticateWithPlex() {
const response = await fetch(
'/settings/plex/authentication-url',
{signal: AbortSignal.timeout(4000)}
).catch(function (error) {
document.getElementById('alertPlexServerUrlLoadingSpinner').classList.add('d-none')

console.log(error)
addAlert('alertPlexServerUrlDiv', 'Authentication did not work', 'danger')
});

if (!response.ok) {
addAlert('alertPlexAuthenticationDiv', 'Authentication did not work', 'danger')

return
}

const data = await response.json()

location.href = data.authenticationUrl;
}

async function removePlexAuthentication() {
const response = await fetch(
'/settings/plex/logout',
{signal: AbortSignal.timeout(4000)}
).catch(function (error) {
console.log(error)

addAlert('alertPlexAuthenticationDiv', 'Could not remove authentication', 'danger')
});

if (!response.ok) {
addAlert('alertPlexAuthenticationDiv', 'Could not remove authentication', 'danger')

return
}

document.getElementById('plexServerUrlInput').disabled = true
document.getElementById('plexServerUrlInput').value = ''
document.getElementById('saveServerUrlButton').disabled = true
document.getElementById('verifyServerUrlButton').disabled = true

document.getElementById('authenticateWithPlexDiv').classList.remove('d-none')
document.getElementById('removeAuthenticationWithPlexDiv').classList.add('d-none')

addAlert('alertPlexAuthenticationDiv', 'Plex authentication was removed', 'success')
}

async function savePlexServerUrl() {
const response = await fetch('/settings/plex/server-url-save', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
'plexServerUrl': document.getElementById('plexServerUrlInput').value,
})
}).then(async function (response) {
return {'status': response.status, 'message': await response.text()};
}).then(function (data) {
if (data.status === 200) {
addAlert('alertPlexServerUrlDiv', 'Server URL was updated', 'success')

return
}

if (data.status === 400) {
addAlert('alertPlexServerUrlDiv', data.message, 'danger')

return
}

addAlert('alertPlexServerUrlDiv', 'Server URL could not be updated', 'danger')
}).catch(function (error) {
document.getElementById('alertPlexServerUrlLoadingSpinner').classList.add('d-none')

console.log(error)
addAlert('alertPlexServerUrlDiv', 'Server URL could not be updated', 'danger')
});
}

async function verifyPlexServerUrl() {
document.getElementById('alertPlexServerUrlLoadingSpinner').classList.remove('d-none')
removeAlert('alertPlexServerUrlDiv')

const response = await fetch('/settings/plex/server-url-verify', {
signal: AbortSignal.timeout(4000),
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
'plexServerUrl': document.getElementById('plexServerUrlInput').value,
})
}).then(async function (response) {
document.getElementById('alertPlexServerUrlLoadingSpinner').classList.add('d-none')

return {'status': response.status, 'message': await response.json()};
}).then(function (data) {
if (data.status === 200 && data.message === true) {
addAlert('alertPlexServerUrlDiv', 'Connection test successful', 'success')

return
}

if (data.status === 400) {
addAlert('alertPlexServerUrlDiv', data.message, 'danger')

return
}

addAlert('alertPlexServerUrlDiv', 'Connection test failed', 'danger')
}).catch(function (error) {
document.getElementById('alertPlexServerUrlLoadingSpinner').classList.add('d-none')

console.log(error)
addAlert('alertPlexServerUrlDiv', 'Connection test failed', 'danger')
});
}

document.getElementById('verifyServerUrlButton').disabled = document.getElementById('plexServerUrlInput').value === ''
document.getElementById('plexServerUrlInput').addEventListener('input', function (e) {
document.getElementById('verifyServerUrlButton').disabled = e.target.value === ''
});
25 changes: 25 additions & 0 deletions settings/routes.php
Original file line number Diff line number Diff line change
Expand Up @@ -218,6 +218,31 @@
'/settings/integrations/plex',
[\Movary\HttpController\SettingsController::class, 'renderPlexPage'],
);
$routeCollector->addRoute(
'GET',
'/settings/plex/logout',
[\Movary\HttpController\PlexController::class, 'removePlexAccessTokens'],
);
$routeCollector->addRoute(
'POST',
'/settings/plex/server-url-save',
[\Movary\HttpController\PlexController::class, 'savePlexServerUrl'],
);
$routeCollector->addRoute(
'POST',
'/settings/plex/server-url-verify',
[\Movary\HttpController\PlexController::class, 'verifyPlexServerUrl'],
);
$routeCollector->addRoute(
'GET',
'/settings/plex/authentication-url',
[\Movary\HttpController\PlexController::class, 'generatePlexAuthenticationUrl'],
);
$routeCollector->addRoute(
'GET',
'/settings/plex/callback',
[\Movary\HttpController\PlexController::class, 'processPlexCallback'],
);
$routeCollector->addRoute(
'POST',
'/settings/plex',
Expand Down
21 changes: 21 additions & 0 deletions src/Api/Plex/Dto/PlexAccessToken.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<?php declare(strict_types=1);

namespace Movary\Api\Plex\Dto;

class PlexAccessToken
{
public function __construct(
private readonly string $plexAccessToken,
) {
}

public static function createPlexAccessToken(string $plexAccessToken) : self
{
return new self($plexAccessToken);
}

public function getPlexAccessTokenAsString() : string
{
return $this->plexAccessToken;
}
}
27 changes: 27 additions & 0 deletions src/Api/Plex/Dto/PlexAccount.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
<?php declare(strict_types=1);

namespace Movary\Api\Plex\Dto;

class PlexAccount
{
public function __construct(
private readonly int $plexId,
private readonly string $username,
) {
}

public static function createPlexAccount(int $plexId, string $username) : self
{
return new self($plexId, $username);
}

public function getPlexId() : int
{
return $this->plexId;
}

public function getPlexUsername() : string
{
return $this->username;
}
}
11 changes: 11 additions & 0 deletions src/Api/Plex/Exception/MovaryApplicationUrlNotSet.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<?php declare(strict_types=1);

namespace Movary\Api\Plex\Exception;

class MovaryApplicationUrlNotSet extends \RuntimeException
{
public static function create() : self
{
return new self('Movary application url is not set');
}
}
13 changes: 13 additions & 0 deletions src/Api/Plex/Exception/PlexAuthenticationError.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<?php declare(strict_types=1);

namespace Movary\Api\Plex\Exception;

use RuntimeException;

class PlexAuthenticationError extends RuntimeException
{
public static function create() : self
{
return new self('The access token is invalid.');
}
}
13 changes: 13 additions & 0 deletions src/Api/Plex/Exception/PlexNoClientIdentifier.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<?php declare(strict_types=1);

namespace Movary\Api\Plex\Exception;

use RuntimeException;

class PlexNoClientIdentifier extends RuntimeException
{
public static function create() : self
{
return new self('No client identifier has been found. Please add it in the environment variables before using the Plex API');
}
}
13 changes: 13 additions & 0 deletions src/Api/Plex/Exception/PlexNoLibrariesAvailable.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<?php declare(strict_types=1);

namespace Movary\Api\Plex\Exception;

use RuntimeException;

class PlexNoLibrariesAvailable extends RuntimeException
{
public static function create(int $userId) : self
{
return new self('No libraries have been found for the user with id ' . $userId);
}
}
14 changes: 14 additions & 0 deletions src/Api/Plex/Exception/PlexNotFoundError.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<?php declare(strict_types=1);

namespace Movary\Api\Plex\Exception;

use Movary\ValueObject\Url;
use RuntimeException;

class PlexNotFoundError extends RuntimeException
{
public static function create(Url $requestUri) : self
{
return new self('The requested URI does not exist: ' . $requestUri);
}
}
Loading