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 all 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
5 changes: 5 additions & 0 deletions .env.development.example
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
# More info here: https://docs.movary.org/configuration/

# Environment
ENV=development
USER_ID=1000
Expand All @@ -21,6 +23,9 @@ DATABASE_MYSQL_ROOT_PASSWORD=movary
TMDB_API_KEY=
TMDB_ENABLE_IMAGE_CACHING=0

# Plex
#PLEX_IDENTIFIER=

# Logging
LOG_LEVEL=debug
LOG_ENABLE_STACKTRACE=1
Expand Down
5 changes: 5 additions & 0 deletions .env.production.example
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
# More info here: https://docs.movary.org/configuration/

# Environment
ENV=production
TIMEZONE="Europe/Berlin"
Expand All @@ -17,6 +19,9 @@ DATABASE_MYSQL_CHARSET=utf8mb4
TMDB_API_KEY=
TMDB_ENABLE_IMAGE_CACHING=1

# Plex
#PLEX_IDENTIFIER=

# Logging
LOG_LEVEL=warning

Expand Down
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 AddPlexOAuthColumnsToUserTable 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,
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
<?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 ,
`is_admin` TINYINT(1) DEFAULT 0,
`dashboard_visible_rows` TEXT DEFAULT NULL,
`dashboard_extended_rows` TEXT DEFAULT NULL,
`dashboard_order_rows` TEXT DEFAULT NULL,
`privacy_level` INTEGER DEFAULT 1,
`date_format_id` INTEGER DEFAULT 0,
`trakt_user_name` TEXT,
`plex_webhook_uuid` TEXT,
`jellyfin_webhook_uuid` TEXT,
`emby_webhook_uuid` TEXT,
`trakt_client_id` TEXT,
`jellyfin_scrobble_views` INTEGER DEFAULT 1,
`emby_scrobble_views` INTEGER DEFAULT 1,
`plex_scrobble_views` INTEGER DEFAULT 1,
`plex_scrobble_ratings` INTEGER DEFAULT 0,
`watchlist_automatic_removal_enabled` 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`,
`is_admin`,
`dashboard_visible_rows`,
`dashboard_extended_rows`,
`dashboard_order_rows`,
`privacy_level`,
`date_format_id`,
`trakt_user_name`,
`plex_webhook_uuid`,
`jellyfin_webhook_uuid`,
`emby_webhook_uuid`,
`trakt_client_id`,
`jellyfin_scrobble_views`,
`emby_scrobble_views`,
`plex_scrobble_views`,
`plex_scrobble_ratings`,
`watchlist_automatic_removal_enabled`,
`core_account_changes_disabled`,
`created_at`
) SELECT
`id`,
`email`,
`name`,
`password`,
`is_admin`,
`dashboard_visible_rows`,
`dashboard_extended_rows`,
`dashboard_order_rows`,
`privacy_level`,
`date_format_id`,
`trakt_user_name`,
`plex_webhook_uuid`,
`jellyfin_webhook_uuid`,
`emby_webhook_uuid`,
`trakt_client_id`,
`jellyfin_scrobble_views`,
`emby_scrobble_views`,
`plex_scrobble_views`,
`plex_scrobble_ratings`,
`watchlist_automatic_removal_enabled`,
`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 ,
`is_admin` TINYINT(1) DEFAULT 0,
`dashboard_visible_rows` TEXT DEFAULT NULL,
`dashboard_extended_rows` TEXT DEFAULT NULL,
`dashboard_order_rows` TEXT DEFAULT NULL,
`privacy_level` INTEGER DEFAULT 1,
`date_format_id` INTEGER DEFAULT 0,
`trakt_user_name` TEXT,
`plex_webhook_uuid` TEXT,
`jellyfin_webhook_uuid` TEXT,
`emby_webhook_uuid` TEXT,
`trakt_client_id` TEXT,
`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,
`jellyfin_scrobble_views` INTEGER DEFAULT 1,
`emby_scrobble_views` INTEGER DEFAULT 1,
`plex_scrobble_views` INTEGER DEFAULT 1,
`plex_scrobble_ratings` INTEGER DEFAULT 0,
`watchlist_automatic_removal_enabled` 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`,
`is_admin`,
`dashboard_visible_rows`,
`dashboard_extended_rows`,
`dashboard_order_rows`,
`privacy_level`,
`date_format_id`,
`trakt_user_name`,
`plex_webhook_uuid`,
`jellyfin_webhook_uuid`,
`emby_webhook_uuid`,
`trakt_client_id`,
`jellyfin_scrobble_views`,
`emby_scrobble_views`,
`plex_scrobble_views`,
`plex_scrobble_ratings`,
`watchlist_automatic_removal_enabled`,
`core_account_changes_disabled`,
`created_at`
) SELECT * FROM user',
);
$this->execute('DROP TABLE `user`');
$this->execute('ALTER TABLE `tmp_user` RENAME TO `user`');
}
}
21 changes: 11 additions & 10 deletions docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,16 +15,17 @@ The `Web UI` column is set to yes if an environment variable can alternatively b

### General

| NAME | DEFAULT VALUE | INFO | Web UI |
|:--------------------------------------------|:-----------------:|:------------------------------------------------------------------------|:------:|
| `TMDB_API_KEY` | - | **Required** (get key [here](https://www.themoviedb.org/settings/api)) | yes |
| `APPLICATION_URL` | - | Public base url of the application (e.g. `htttp://localhost`) | yes |
| `TMDB_ENABLE_IMAGE_CACHING` | `0` | More info [here](features/tmdb-data.md#image-cache) | |
| `ENABLE_REGISTRATION` | `0` | Enables public user registration | |
| `MIN_RUNTIME_IN_SECONDS_FOR_JOB_PROCESSING` | `15` | Minimum time between background jobs processing | |
| `TIMEZONE` | `"Europe/Berlin"` | Supported timezones [here](https://www.php.net/manual/en/timezones.php) | |
| `DEFAULT_LOGIN_EMAIL` | - | Email address to always autofill on login page | |
| `DEFAULT_LOGIN_PASSWORD` | - | Password to always autofill on login page | |
| NAME | DEFAULT VALUE | INFO | Web UI |
|:--------------------------------------------|:-----------------:|:-------------------------------------------------------------------------------|:------:|
| `TMDB_API_KEY` | - | **Required** (get key [here](https://www.themoviedb.org/settings/api)) | yes |
| `APPLICATION_URL` | - | Public base url of the application (e.g. `htttp://localhost`) | yes |
| `TMDB_ENABLE_IMAGE_CACHING` | `0` | More info [here](features/tmdb-data.md#image-cache) | |
| `PLEX_IDENTIFIER` | - | Required for Plex Authentication. Generate with e.g. `openssl rand -base64 32` | |
| `ENABLE_REGISTRATION` | `0` | Enables public user registration | |
| `MIN_RUNTIME_IN_SECONDS_FOR_JOB_PROCESSING` | `15` | Minimum time between background jobs processing | |
| `TIMEZONE` | `"Europe/Berlin"` | Supported timezones [here](https://www.php.net/manual/en/timezones.php) | |
| `DEFAULT_LOGIN_EMAIL` | - | Email address to always autofill on login page | |
| `DEFAULT_LOGIN_PASSWORD` | - | Password to always autofill on login page | |

### Database

Expand Down
1 change: 1 addition & 0 deletions public/css/bootstrap-icons-1.10.2.css
Original file line number Diff line number Diff line change
Expand Up @@ -42,3 +42,4 @@ url("../fonts/bootstrap-icons.woff?24e3eb84d0bcaf83d77f904c78ac1f47") format("wo
.bi-x-circle-fill::before { content: "\f622"; }
.bi-chevron-expand::before { content: "\f283"; }
.bi-chevron-contract::before { content: "\f27d"; }
.bi-question-lg::before { content: "\f64e"; }
132 changes: 132 additions & 0 deletions public/js/settings-integration-plex.js
Original file line number Diff line number Diff line change
Expand Up @@ -91,3 +91,135 @@ 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) {
if (response.status === 400) {
addAlert('alertPlexAuthenticationDiv', await response.text(), 'danger')

return
}

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 === ''
});
Loading