Make the first time webui access more user-friendly.

This commit is contained in:
arabcoders
2025-05-12 22:23:15 +03:00
parent efc5637d7c
commit 14f82c4275
6 changed files with 79 additions and 51 deletions

View File

@@ -151,36 +151,20 @@
</span>
</button>
</div>
<p class="has-text-left">
<span class="icon has-text-danger"><i class="fas fa-info"/></span>
<span>These settings are stored locally in your browser. You need to re-add them if you access the
<code>WebUI</code> from different browser.
</span>
</p>
</div>
<div class="column is-12 mt-2">
<Message title="Information" message_class="has-background-info-90 has-text-dark" icon="fas fa-info-circle">
<p>
It's possible to automatically setup the API connection for this client and <strong
class="has-text-danger">ALL VISITORS</strong> by setting the following environment variable
<code>WS_API_AUTO=true</code>
in <code>/config/.env</code> file. Understand that this option <strong class="has-text-danger">PUBLICLY
EXPOSES YOUR API TOKEN</strong> to <u>ALL VISITORS</u>. Anyone who is able to reach this page will be
granted access to your <code>WatchState API</code> which exposes your other media backends data including
their secrets. <strong>this option is great security risk and SHOULD NEVER be used if
<code>WatchState</code> is exposed to the internet.</strong>
</p>
<p>Please visit
<span class="icon">
<i class="fab fa-github"/>
</span>
<NuxtLink target="_blank" to="https://github.com/arabcoders/watchstate/blob/master/FAQ.md#ws_api_auto">
This link
</NuxtLink>
. to learn more, this environment variable is important enough to have its own section entry in the FAQ.
</p>
<ul>
<li>
These settings are stored locally in your browser. You need to re-add them if you access the
<strong>WebUI</strong> from different browser.
</li>
<li>
The very first time you access the <strong>WebUI</strong>, it will auto configure the connection if
possible.
</li>
</ul>
</Message>
</div>
</div>

View File

@@ -4,6 +4,7 @@
<div class="column is-12 is-clearfix is-unselectable">
<span class="title is-4">
<span class="icon"><i class="fas fa-server"/></span>
<span v-if="api_user">&nbsp;{{ ucFirst(api_user) }} @</span>
Backends
</span>
<div class="is-pulled-right">
@@ -23,7 +24,7 @@
</div>
</div>
<div class="is-hidden-mobile">
<span class="subtitle">This page contains all the backends that are currently configured.</span>
<span class="subtitle">Backends that are configured for the specific user.</span>
</div>
</div>
@@ -46,22 +47,6 @@
</Message>
</div>
<div class="column is-12">
<div class="content">
<h1 class="title is-4">
<span class="icon"><i class="fas fa-tools"/></span> Tools
</h1>
<ul>
<li>
<NuxtLink :to="`/tools/plex_token`" v-text="'Validate plex token'"/>
</li>
<li v-if="backends && backends.length>0 && 'main' === api_user">
<NuxtLink :to="`/tools/sub_users`" v-text="'Create sub-users'"/>
</li>
</ul>
</div>
</div>
<div v-for="backend in backends" :key="backend.name" class="column is-6-tablet is-12-mobile">
<div class="card">
<header class="card-header">
@@ -76,11 +61,13 @@
<NuxtLink :to="`/backend/${backend.name}/edit?redirect=/backends`"
v-tooltip="'Edit backend settings'">
<span class="icon has-text-warning"><i class="fas fa-cog"/></span>
<span class="is-hidden-mobile">Edit</span>
</NuxtLink>
</div>
<div class="control">
<NuxtLink :to="`/backend/${backend.name}/delete?redirect=/backends`" v-tooltip="'Delete backend'">
<span class="icon has-text-danger"><i class="fas fa-trash"/></span>
<span class="is-hidden-mobile">Delete</span>
</NuxtLink>
</div>
</div>
@@ -131,7 +118,9 @@
<input :id="backend.name+'_export'" type="checkbox" class="switch is-success"
:checked="backend.export.enabled"
@change="updateValue(backend, 'export.enabled', !backend.export.enabled)">
<label :for="backend.name+'_export'">Export</label>
<label class="has-tooltip" :for="backend.name+'_export'"
v-tooltip="'Send data from watchstate to this backend.'"
>Export</label>
</div>
</div>
<div class="card-footer-item">
@@ -139,7 +128,9 @@
<input :id="backend.name+'_import'" type="checkbox" class="switch is-success"
:checked="backend.import.enabled"
@change="updateValue(backend, 'import.enabled',!backend.import.enabled)">
<label :for="backend.name+'_import'">Import</label>
<label class="has-tooltip" :for="backend.name+'_import'"
v-tooltip="'Get data from this backend into watchstate.'">
Import</label>
</div>
</div>
<div class="card-footer-item">
@@ -177,10 +168,15 @@
@toggle="show_page_tips = !show_page_tips" :use-toggle="true" title="Tips" icon="fas fa-info-circle">
<ul>
<li>
<strong>Import</strong> means pulling data from the backends into the local database.
Think of the WatchState as a <strong>Central Hub</strong> your backends aren't aware of each other.
<strong>WatchState</strong> is the facilitator of the data flow. If you think like that, then <strong>import</strong>
and <strong>export</strong> would make sense.
</li>
<li>
<strong>Export</strong> means pushing data from the local database to the backends.
<strong>Import</strong>: Means getting data from the backend into watchstate.
</li>
<li>
<strong>Export</strong>: Means sending data from watchstate to the backend.
</li>
</ul>
</Message>
@@ -278,6 +274,7 @@ const loadContent = async () => {
return
}
backends.value = json
useHead({title: `${ucFirst(api_user.value)} @ Backends`})
} catch (e) {
notification('error', 'Error', `Failed to load backends. ${e.message}`)
} finally {

View File

@@ -8,17 +8,44 @@ use App\Libs\Attributes\Route\Post;
use App\Libs\Config;
use App\Libs\DataUtil;
use App\Libs\Enums\Http\Status;
use App\Libs\Mappers\ImportInterface as iImport;
use App\Libs\Stream;
use App\Libs\Traits\APITraits;
use Psr\Http\Message\ResponseInterface as iResponse;
use Psr\Http\Message\ServerRequestInterface as iRequest;
use Psr\Log\LoggerInterface as iLogger;
use Throwable;
final class AutoConfig
{
use APITraits;
public const string URL = '%{api.prefix}/system/auto';
public function __construct(private readonly iImport $mapper, private readonly iLogger $logger)
{
}
#[Post(self::URL . '[/]', name: 'system.autoconfig')]
public function __invoke(iRequest $request): iResponse
{
if (false === (bool)Config::get('api.auto', false)) {
$isEnabled = false;
try {
$initial_file = Config::get('path') . '/config/disable_auto_config.txt';
if (false === file_exists($initial_file)) {
$uc = $this->getUserContext($request, $this->mapper, $this->logger);
$isEnabled = 'main' === $uc->name && count($uc->config) < 1;
$stream = Stream::make($initial_file, 'w+');
$stream->write(r('Auto configure was called and disabled at {time}', [
'time' => makeDate('now'),
]));
$stream->close();
}
} catch (Throwable $e) {
syslog(LOG_ERR, __METHOD__ . ' Exception: ' . $e->getMessage() . ' ' . $e->getTraceAsString());
}
if (false === $isEnabled && false === (bool)Config::get('api.auto', false)) {
return api_error('auto configuration is disabled.', Status::FORBIDDEN);
}

View File

@@ -9,17 +9,31 @@ use App\Libs\Attributes\Route\Get;
use App\Libs\Database\DBLayer;
use App\Libs\Enums\Http\Method;
use App\Libs\Enums\Http\Status;
use App\Libs\Mappers\ImportInterface as iImport;
use App\Libs\Traits\APITraits;
use Psr\Http\Message\ResponseInterface as iResponse;
use Psr\Http\Message\ServerRequestInterface as iRequest;
use Psr\Log\LoggerInterface as iLogger;
use RuntimeException;
final class Images
{
use APITraits;
public const string URL = '%{api.prefix}/system/images';
public function __construct(private readonly iImport $mapper, private readonly iLogger $logger)
{
}
#[Get(self::URL . '/{type:poster|background}[/]', name: 'system.images')]
public function __invoke(DBLayer $db, string $type): iResponse
public function __invoke(iRequest $request, DBLayer $db, string $type): iResponse
{
try {
$uc = $this->getUserContext($request, $this->mapper, $this->logger);
if (count($uc->config) < 1) {
return api_response(Status::NO_CONTENT);
}
$resp = $this->getImage($db, $type);
} catch (RuntimeException) {
return api_response(Status::NO_CONTENT);

View File

@@ -5,6 +5,7 @@ declare(strict_types=1);
namespace App\Libs;
use ArrayAccess;
use Countable;
use InvalidArgumentException;
use Psr\Log\LoggerAwareInterface;
use Psr\Log\LoggerInterface;
@@ -16,7 +17,7 @@ use Symfony\Component\Yaml\Yaml;
*
* The ConfigFile class represents a configuration file.
*/
final class ConfigFile implements ArrayAccess, LoggerAwareInterface
final class ConfigFile implements ArrayAccess, LoggerAwareInterface, Countable
{
private const array CONTENT_TYPES = ['yaml', 'json', 'yml'];
private array $data = [];
@@ -368,4 +369,9 @@ final class ConfigFile implements ArrayAccess, LoggerAwareInterface
{
return hash_file('sha256', $this->file);
}
public function count(): int
{
return count($this->data);
}
}

View File

@@ -1275,7 +1275,7 @@ if (!function_exists('getUsersContext')) {
$configs = [
'main' => new UserContext(
name: 'main',
config: ConfigFile::open(Config::get('backends_file'), 'yaml'),
config: ConfigFile::open(Config::get('backends_file'), 'yaml', autoCreate: true),
mapper: $mapper,
cache: Container::get(iCache::class),
db: Container::get(iDB::class)->setOptions($dbOpts),