Added Backend Add code to WebUI.

This commit is contained in:
Abdulmhsen B. A. A
2024-05-06 21:32:48 +03:00
parent 70f45a863e
commit 9505fe4607
6 changed files with 417 additions and 20 deletions

View File

@@ -0,0 +1,329 @@
<template>
<form id="backend_add_form" @submit.prevent="addBackend" @change="changeStage">
<div class="box">
<div class="field">
<label class="label">Name</label>
<div class="control has-icons-left">
<input class="input" type="text" v-model="backend.name">
<div class="icon is-small is-left">
<i class="fas fa-user"></i>
</div>
<p class="help">
Choose a unique name for this backend. You cannot change it later. Backend name must be in <code>lower
case a-z, 0-9 and _</code> only.
</p>
</div>
</div>
<div class="field">
<label class="label">Type</label>
<div class="control has-icons-left">
<div class="select is-fullwidth">
<select v-model="backend.type" class="is-capitalized">
<option value="">Select Backend type.</option>
<option v-for="type in supported" :key="'type-'+type" :value="type">
{{ type }}
</option>
</select>
</div>
<div class="icon is-small is-left">
<i class="fas fa-cloud"></i>
</div>
</div>
</div>
<div class="field">
<label class="label">
<template v-if="'plex' !== backend.type">API Token</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-small is-left">
<i class="fas fa-key"></i>
</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>
<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="icon is-small is-left">
<i class="fas fa-link"></i>
</div>
<p class="help">
Enter the URL of the backend. For example <code>http://localhost:32400</code>.
</p>
</div>
</div>
<template v-if="stage >= 2">
<div class="field">
<label class="label">Backend Unique ID</label>
<div class="control has-icons-left">
<input class="input" type="text" v-model="backend.uuid" required>
<div class="icon is-small is-left">
<i class="fas fa-server" v-if="!uuidLoading"></i>
<i class="fas fa-spinner fa-pulse" v-else></i>
</div>
<p class="help">
The Unique identifier for the backend.
<a href="javascript:void(0)" @click="getUUid">Get from the backend.</a>
</p>
</div>
</div>
<div class="field">
<label class="label">Backend User ID</label>
<div class="control has-icons-left">
<div class="select is-fullwidth" v-if="users.length>0">
<select v-model="backend.user" class="is-capitalized">
<option value="">Select User</option>
<option v-for="user in users" :key="'uid-'+user.id" :value="user.id">
{{ user.name }}
</option>
</select>
</div>
<input class="input" type="text" v-model="backend.user" v-else>
<div class="icon is-small is-left">
<i class="fas fa-user-tie" v-if="!usersLoading"></i>
<i class="fas fa-spinner fa-pulse" v-else></i>
</div>
<p class="help">
The user ID of the backend.
<a href="javascript:void(0)" @click="getUsers">
Retrieve User ids from backend.
</a>
</p>
</div>
</div>
<div class="field" v-if="backend.import">
<label class="label" for="backend_import">Import data from this backend</label>
<div class="control">
<input id="backend_import" type="checkbox" class="switch is-success" v-model="backend.import.enabled">
<label for="backend_import">Enable</label>
<p class="help">
Import means to get the data from the backend and store it in the database.
</p>
</div>
</div>
<div class="field" v-if="backend.export">
<label class="label" for="backend_export">Export data to this backend</label>
<div class="control">
<input id="backend_export" type="checkbox" class="switch is-success" v-model="backend.export.enabled">
<label for="backend_export">Enable</label>
<p class="help">
Export means to send the data from the database to this backend.
</p>
</div>
</div>
<div class="field" v-if="backend.webhook">
<label class="label" for="webhook_match_user">Webhook match user</label>
<div class="control">
<input id="webhook_match_user" type="checkbox" class="switch is-success"
v-model="backend.webhook.match.user">
<label for="webhook_match_user">Enable</label>
<p class="help">
Check webhook payload for user id match. if it does not match, the payload will be ignored.
</p>
</div>
</div>
<div class="field" v-if="backend.webhook">
<label class="label" for="webhook_match_uuid">Webhook match backend id</label>
<div class="control">
<input id="webhook_match_uuid" type="checkbox" class="switch is-success"
v-model="backend.webhook.match.uuid">
<label for="webhook_match_uuid">Enable</label>
<p class="help">
Check webhook payload for backend unique id. if it does not match, the payload will be ignored.
</p>
</div>
</div>
</template>
<div class="field has-text-right">
<div class="control">
<button class="button is-primary" type="submit">
<span class="icon is-small"><i class="fas fa-plus"></i></span>
<span>Add Backend</span>
</button>
</div>
</div>
</div>
</form>
</template>
<script setup>
import 'assets/css/bulma-switch.css'
import {notification} from "~/utils/index.js";
const emit = defineEmits(['addBackend'])
const backend = ref({
name: '',
type: '',
url: '',
token: '',
uuid: '',
user: '',
import: {
enabled: false
},
export: {
enabled: false
},
webhook: {
match: {
user: false,
uuid: false
}
},
options: {}
})
const users = ref([])
const supported = ref([])
const stage = ref(0)
const usersLoading = ref(false)
const uuidLoading = ref(false)
const getUUid = async () => {
const required_values = ['type', 'token', 'url'];
if (required_values.some(v => !backend.value[v])) {
notification('error', 'Error', `Please fill all the required fields. ${required_values.join(', ')}.`)
return
}
uuidLoading.value = true
const response = await request(`/backends/uuid/${backend.value.type}`, {
method: 'POST',
body: JSON.stringify({
token: backend.value.token,
url: backend.value.url
})
})
const json = await response.json()
uuidLoading.value = false
if (!response.ok) {
notification('error', 'Error', 'Failed to get the UUID from the backend.')
return
}
backend.value.uuid = json.identifier
}
const getUsers = async (showAlert = true) => {
const required_values = ['type', 'token', 'url', 'uuid'];
if (required_values.some(v => !backend.value[v])) {
if (showAlert) {
required_values.forEach(v => {
if (!backend.value[v]) {
notification('error', 'Error', `Please fill the required field: ${v}.`)
}
})
}
return
}
usersLoading.value = true
let data = {
token: backend.value.token,
url: backend.value.url,
uuid: backend.value.uuid,
};
if (backend.value.options && backend.value.options.ADMIN_TOKEN) {
data.options = {
ADMIN_TOKEN: backend.value.options.ADMIN_TOKEN
}
}
const response = await request(`/backends/users/${backend.value.type}`, {
method: 'POST',
body: JSON.stringify(data)
})
const json = await response.json()
usersLoading.value = false
if (200 !== response.status) {
notification('error', 'Error', `${json.error.code}: ${json.error.message}`)
return
}
users.value = json
}
watch(stage, async (value) => {
if (value >= 2) {
if (!backend.value.uuid) {
await getUUid();
}
if (users.value.length < 1) {
await getUsers()
}
}
})
onMounted(async () => {
const response = await request('/system/supported')
supported.value = await response.json()
})
const changeStage = () => {
const required = ['name', 'type', 'token', 'url']
if (required.some(v => !backend.value[v])) {
stage.value = 0
} else {
stage.value = 2
}
}
const addBackend = async () => {
const response = await request(`/backends/`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(backend.value)
})
const json = await response.json()
if (response.status >= 400) {
notification('error', 'Error', `Failed to Add backend. (${json.error.code}: ${json.error.message}).`)
return false
}
notification('success', 'Information', `Backend ${backend.value.name} added successfully.`)
emit('addBackend', backend)
return true
}
</script>

View File

@@ -23,7 +23,7 @@
<div class="box"> <div class="box">
<div class="field"> <div class="field">
<label class="label">Backend Name</label> <label class="label">Name</label>
<div class="control has-icons-left"> <div class="control has-icons-left">
<input class="input" type="text" v-model="backend.name" required readonly disabled> <input class="input" type="text" v-model="backend.name" required readonly disabled>
<div class="icon is-small is-left"> <div class="icon is-small is-left">
@@ -37,7 +37,7 @@
</div> </div>
<div class="field"> <div class="field">
<label class="label">Backend Type</label> <label class="label">Type</label>
<div class="control has-icons-left"> <div class="control has-icons-left">
<input class="input" type="text" v-model="backend.type" readonly disabled> <input class="input" type="text" v-model="backend.type" readonly disabled>
<div class="icon is-small is-left"> <div class="icon is-small is-left">
@@ -47,7 +47,7 @@
</div> </div>
<div class="field"> <div class="field">
<label class="label">Backend URL</label> <label class="label">URL</label>
<div class="control has-icons-left"> <div class="control has-icons-left">
<input class="input" type="text" v-model="backend.url" required> <input class="input" type="text" v-model="backend.url" required>
<div class="icon is-small is-left"> <div class="icon is-small is-left">
@@ -62,7 +62,8 @@
<div class="field"> <div class="field">
<label class="label"> <label class="label">
Backend API token <template v-if="'plex' !== backend.type">API Token</template>
<template v-else>X-Plex-Token</template>
</label> </label>
<div class="control has-icons-left"> <div class="control has-icons-left">
<input class="input" type="text" v-model="backend.token" required> <input class="input" type="text" v-model="backend.token" required>
@@ -101,16 +102,14 @@
<div class="field"> <div class="field">
<label class="label">Backend User ID</label> <label class="label">Backend User ID</label>
<div class="control has-icons-left"> <div class="control has-icons-left">
<div class="select is-fullwidth"> <div class="select is-fullwidth" v-if="users.length>0">
<select v-model="backend.user" class="is-capitalized"> <select v-model="backend.user" class="is-capitalized">
<option v-for="user in users" :key="'uid-'+user.id" :value="user.id"> <option v-for="user in users" :key="'uid-'+user.id" :value="user.id">
<template v-if="'plex' === backend"> {{ user.name }}
{{ user.id }} - {{ user.name }}
</template>
<template v-else>{{ user.name }}</template>
</option> </option>
</select> </select>
</div> </div>
<input class="input" type="text" v-model="backend.user" v-else>
<div class="icon is-small is-left"> <div class="icon is-small is-left">
<i class="fas fa-user-tie" v-if="!usersLoading"></i> <i class="fas fa-user-tie" v-if="!usersLoading"></i>
<i class="fas fa-spinner fa-pulse" v-else></i> <i class="fas fa-spinner fa-pulse" v-else></i>
@@ -260,9 +259,13 @@ const saveContent = async () => {
}) })
const json = await response.json() const json = await response.json()
if (!response.ok) { if (200 !== response.status) {
notification('error', 'Error', 'Failed to save backend settings.') notification('error', 'Error', `Failed to save backend settings. (${json.error.code}: ${json.error.message}).`)
return
} }
notification('success', 'Information', `Backend settings saved successfully.`)
} }
const removeOption = async (key) => { const removeOption = async (key) => {

View File

@@ -1,13 +1,14 @@
<template> <template>
<div class="columns is-multiline"> <div class="columns is-multiline">
<div class="column is-12"> <div class="column is-12 is-clearfix">
<div class="p-2"> <div class="p-2">
<span class="title is-4">Backends</span> <span class="title is-4">Backends</span>
<div class="is-pulled-right"> <div class="is-pulled-right">
<div class="field is-grouped"> <div class="field is-grouped">
<p class="control"> <p class="control">
<button class="button is-primary is-light" v-tooltip="'Add New Backend'"> <button class="button is-primary is-light" v-tooltip="'Add New Backend'"
@click="toggleForm = !toggleForm">
<span class="icon"> <span class="icon">
<i class="fas fa-add"></i> <i class="fas fa-add"></i>
</span> </span>
@@ -24,6 +25,11 @@
</div> </div>
</div> </div>
</div> </div>
<div class="column is-12" v-if="toggleForm">
<BackendAdd @addBackend="toggleForm = false; loadContent()"/>
</div>
<div v-for="backend in backends" :key="backend.name" class="column is-6-tablet is-12-mobile"> <div v-for="backend in backends" :key="backend.name" class="column is-6-tablet is-12-mobile">
<div class="card"> <div class="card">
<header class="card-header"> <header class="card-header">
@@ -41,10 +47,12 @@
<div class="card-content"> <div class="card-content">
<div class="columns is-multiline is-mobile has-text-centered"> <div class="columns is-multiline is-mobile has-text-centered">
<div class="column is-6-mobile" v-if="backend.export.enabled"> <div class="column is-6-mobile" v-if="backend.export.enabled">
<strong>Last Export:</strong> {{ moment(backend.export.lastSync).fromNow() }} <strong>Last Export:</strong>
{{ backend.export.lastSync ? moment(backend.export.lastSync).fromNow() : 'None' }}
</div> </div>
<div class="column is-hidden-mobile" v-if="backend.import.enabled"> <div class="column is-hidden-mobile" v-if="backend.import.enabled">
<strong>Last Import:</strong> {{ moment(backend.import.lastSync).fromNow() }} <strong>Last Import:</strong>
{{ backend.import.lastSync ? moment(backend.import.lastSync).fromNow() : 'None' }}
</div> </div>
</div> </div>
</div> </div>
@@ -79,15 +87,21 @@
import 'assets/css/bulma-switch.css' import 'assets/css/bulma-switch.css'
import moment from "moment"; import moment from "moment";
import request from "~/utils/request.js"; import request from "~/utils/request.js";
import BackendAdd from "~/components/BackendAdd.vue";
useHead({title: 'Backends'}) useHead({title: 'Backends'})
const backends = ref([]) const backends = ref([])
const toggleForm = ref(false)
const loadContent = async () => { const loadContent = async () => {
backends.value = [] backends.value = []
const response = await request('/backends') const response = await request('/backends')
backends.value = await response.json() backends.value = await response.json()
if (backends.value.length === 0) {
toggleForm.value = true
notification('warning', 'Information', 'No backends found.')
}
} }
onMounted(() => loadContent()) onMounted(() => loadContent())

View File

@@ -0,0 +1,51 @@
<?php
declare(strict_types=1);
namespace App\API\Backends;
use App\Backends\Plex\PlexClient;
use App\Libs\Attributes\Route\Route;
use App\Libs\DataUtil;
use App\Libs\Exceptions\InvalidArgumentException;
use App\Libs\HTTP_STATUS;
use App\Libs\Traits\APITraits;
use Psr\Http\Message\ResponseInterface as iResponse;
use Psr\Http\Message\ServerRequestInterface as iRequest;
use Symfony\Contracts\HttpClient\HttpClientInterface as iHttp;
use Throwable;
final class Discover
{
use APITraits;
public function __construct(private readonly iHttp $http)
{
}
#[Route(['GET', 'POST'], Index::URL . '/discover/{type}[/]', name: 'backends.get.users')]
public function __invoke(iRequest $request, array $args = []): iResponse
{
if (null === ($type = ag($args, 'type'))) {
return api_error('Invalid value for type path parameter.', HTTP_STATUS::HTTP_BAD_REQUEST);
}
if ('plex' !== $type) {
return api_error('Discover only supported on plex.', HTTP_STATUS::HTTP_BAD_REQUEST);
}
try {
$client = $this->getBasicClient($type, DataUtil::fromRequest($request, true));
assert($client instanceof PlexClient);
} catch (InvalidArgumentException $e) {
return api_error($e->getMessage(), HTTP_STATUS::HTTP_BAD_REQUEST);
}
try {
$list = $client::discover($this->http, $client->getContext()->backendToken);
return api_response(HTTP_STATUS::HTTP_OK, ag($list, 'list', []));
} catch (Throwable $e) {
return api_error($e->getMessage(), HTTP_STATUS::HTTP_INTERNAL_SERVER_ERROR);
}
}
}

View File

@@ -17,6 +17,6 @@ final class Supported
#[Get(self::URL . '[/]', name: 'system.supported')] #[Get(self::URL . '[/]', name: 'system.supported')]
public function __invoke(iRequest $request): iResponse public function __invoke(iRequest $request): iResponse
{ {
return api_response(HTTP_STATUS::HTTP_OK, ['supported' => array_keys(Config::get('supported'))]); return api_response(HTTP_STATUS::HTTP_OK, array_keys(Config::get('supported')));
} }
} }

View File

@@ -57,12 +57,12 @@ trait APITraits
foreach ($list as $backendName => $backend) { foreach ($list as $backendName => $backend) {
$backend = ['name' => $backendName, ...$backend]; $backend = ['name' => $backendName, ...$backend];
if (null !== ag($backend, 'import.lastSync')) { if (null !== ($import = ag($backend, 'import.lastSync'))) {
$backend = ag_set($backend, 'import.lastSync', makeDate(ag($backend, 'import.lastSync'))); $backend = ag_set($backend, 'import.lastSync', $import ? makeDate($import) : null);
} }
if (null !== ag($backend, 'export.lastSync')) { if (null !== ($export = ag($backend, 'export.lastSync'))) {
$backend = ag_set($backend, 'export.lastSync', makeDate(ag($backend, 'export.lastSync'))); $backend = ag_set($backend, 'export.lastSync', $export ? makeDate($export) : null);
} }
$backends[] = $backend; $backends[] = $backend;