Added WS_API_AUTO environment variable which can autoconfigure the WebUI

This commit is contained in:
Abdulmhsen B. A. A.
2024-06-23 21:34:09 +03:00
parent 196133a675
commit 912c41574f
10 changed files with 195 additions and 29 deletions

29
FAQ.md
View File

@@ -582,6 +582,31 @@ https://watchstate.example.org {
---
### WS_API_AUTO
The purpose of this environment variable is to automate the configuration process. It's mainly used for people who uses many browsers
to access the `WebUI` and want to automate the configuration process. as it's requires the API settings to be configured before it
can be used. This environment variable can be enabled by setting `WS_API_AUTO=true` in `${WS_DATA_PATH}/config/.env`.
#### Why you should use it?
You normally should not use it, as it's a **GREAT SECURITY RISK**. However, if you are using the tool in a secure environment
and not worried about exposing your API key, you can use it to automate the configuration process.
#### Why you should not use it?
Because, by exposing your API key, you are also exposing every data you have in the tool. This is a **GREAT SECURITY RISK**,
any person or bot that are able to access the `WebUI` will also be able to visit `/v1/api/system/auto` and get your API key. And with this key
they can do anything they want with your data. including viewing your media servers api keys.
So, please while we have this option available, we strongly recommend not to use it if `WatchState` is exposed to the internet.
> [!IMPORTANT]
> This environment variable is **GREAT SECURITY RISK**, and we strongly recommend not to use it if `WatchState` is exposed to the internet.
> I cannot stress this enough, please do not use it unless you are in a secure environment.
---
### How to disable the included cache server and use external cache server?
Set this environment variable in your `compose.yaml` file `DISABLE_CACHE` with value of `1`. to use external redis server
@@ -745,7 +770,7 @@ Once that is done you are ready to compile the `WebUI`.
```bash
$ cd frontend
$ yarn install --production --prefer-offline --frozen-lockfile
$ yarn install --production --prefer-offline --frozen-lockfile && yarn run generate
```
There should be a new directory called `exported`, you need to move that folder to the `public` directory.
@@ -761,7 +786,7 @@ ws:/opt/app/public$ ls
exported index.php
```
There must be exactly one `index.php` file and one `exported` directory. inside that directory.
There must be exactly one `index.php` file and one `exported` directory. inside that directory, or if you prefer, you can add `WS_WEBUI_PATH` environment variable to point to the `exported` directory.
* link the app to the frontend proxy. For caddy, you can use the following configuration.

16
NEWS.md
View File

@@ -1,5 +1,21 @@
# Old Updates
### 2024-05-14
We are happy to announce the beta testing of the `WebUI`. To get started on using it you just need to visit the url `http://localhost:8080` We are supposed to
enabled it by default tomorrow, but we decided to give you a head start. We are looking forward to your feedback. If you don't use the `WebUI` then you need to
add the environment variable `WEBUI_ENABLED=0` in your `compose.yaml` file. and restart the container.
### 2024-05-13
In preparation for the beta testing of `WebUI` in two days, we have made little breaking change, we have changed the
environment variable `WS_WEBUI_ENABLED` to just `WEBUI_ENABLED`, We made this change to make sure people don't disable
the `WebUI`by mistake via the environment page in the `WebUI`. The `WebUI` will be enabled by default, in two days from
now, to disable it from now add `WEBUI_ENABLED=false` to your `compose.yaml` file. As this environment variable is
system level, it cannot be set via `.env` file.
Note: `WS_WEBUI_ENABLED` will be gone in few weeks, However it will still work for now, if `WEBUI_ENABLED` is not set.
### 2024-05-05
**Edit** - We received requests that people are exposing watchstate externally, and there was concern that having open

View File

@@ -7,29 +7,29 @@
This tool primary goal is to sync your backends play state without relying on third party services,
out of the box, this tool support `Jellyfin`, `Plex` and `Emby` media servers.
## updates
## Updates
### 2024-05-14
### 2024-06-23
We are happy to announce the beta testing of the `WebUI`. To get started on using it you just need to visit the url `http://localhost:8080` We are supposed to
enabled it by default tomorrow, but we decided to give you a head start. We are looking forward to your feedback. If you don't use the `WebUI` then you need to
add the environment variable `WEBUI_ENABLED=0` in your `compose.yaml` file. and restart the container.
WE are happy to announce that the `WebUI` is ready for wider usage and we are planning to release it in the next few months.
We are actively working on it to improve it. If you have any feedback or suggestions, please let us know. We feel it's almost future complete
for the things that we want.
### 2024-05-13
On another related news, we have added new environment variable `WS_API_AUTO` "disabled by default" which can be used
to automatically expose your **API KEY/TOKEN**. This is useful for users who are using the `WebUI` from many different browsers
and want to automate the configuration process.
In preparation for the beta testing of `WebUI` in two days, we have made little breaking change, we have changed the
environment variable `WS_WEBUI_ENABLED` to just `WEBUI_ENABLED`, We made this change to make sure people don't disable
the `WebUI`by mistake via the environment page in the `WebUI`. The `WebUI` will be enabled by default, in two days from
now, to disable it from now add `WEBUI_ENABLED=false` to your `compose.yaml` file. As this environment variable is
system level, it cannot be set via `.env` file.
While the `WebUI` is included in the main project, it's a standalone feature and requires the API settings to be configured before it
can be used. This environment variable can be enabled by setting `WS_API_AUTO=true` in `${WS_DATA_PATH}/config/.env`.
Note: `WS_WEBUI_ENABLED` will be gone in few weeks, However it will still work for now, if `WEBUI_ENABLED` is not set.
> [!IMPORTANT]
> This environment variable is **GREAT SECURITY RISK**, and we strongly recommend not to use it if `WatchState` is exposed to the internet.
Refer to [NEWS](NEWS.md) for old updates.
# Features
* **NEW** WebUI. (Preview).
* WebUI.
* Sync backends play state (from many to many).
* Backup your backends play state into `portable` format.
* Receive Webhook events from media backends.

View File

@@ -34,6 +34,7 @@ return (function () {
'prefix' => '/v1/api',
'key' => env('WS_API_KEY', null),
'secure' => (bool)env('WS_SECURE_API_ENDPOINTS', false),
'auto' => (bool)env('WS_API_AUTO', false),
'pattern_match' => [
'backend' => '[a-zA-Z0-9_-]+',
],

View File

@@ -150,6 +150,12 @@ return (function () {
'description' => 'Expose debug information in the API when an error occurs.',
'type' => 'bool',
],
[
'key' => 'WS_API_AUTO',
'description' => 'PUBLICLY EXPOSE the api token for automated WebUI configuration. This should NEVER be enabled if WatchState is exposed to the internet.',
'danger' => true,
'type' => 'bool',
],
];
$validateCronExpression = function (string $value): string {

View File

@@ -26,7 +26,7 @@ onMounted(() => fetch(`${api_url.value}${props.file}`).then(response => response
text = text.replace(/\[!IMPORTANT\]/g, `
<span class="is-block title is-4">
<span class="icon-text">
<span class="icon"><i class="fas fa-exclamation-triangle has-text-danger"></i></span>
<span class="icon"><i class="fas fa-exclamation-triangle has-text-danger fa-fade"></i></span>
<span>Important</span>
</span>
</span>`)

View File

@@ -252,13 +252,38 @@
</div>
</div>
</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"></i>
</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>
</Message>
</div>
</div>
<template v-if="!api_url || !api_token">
<template v-if="!hasAPISettings">
<no-api/>
</template>
<template v-else>
<slot v-if="!showConnection"/>
<slot v-if="!showConnection && hasAPISettings"/>
</template>
<div class="columns is-multiline is-mobile mt-3">
@@ -297,7 +322,7 @@ import {dEvent} from '~/utils/index.js'
const selectedTheme = useStorage('theme', (() => window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light')())
const showConnection = ref(false)
const real_api_url = useStorage('api_url', '')
const real_api_url = useStorage('api_url', window.location.origin)
const real_api_path = useStorage('api_path', '/v1/api')
const real_api_token = useStorage('api_token', '')
@@ -357,9 +382,12 @@ const applyPreferredColorScheme = scheme => {
onMounted(async () => {
try {
applyPreferredColorScheme(selectedTheme.value)
if ('' === api_token.value) {
await autoConfig()
}
if ('' === api_token.value || '' === api_url.value) {
showConnection.value = true
return
@@ -430,18 +458,47 @@ const getVersion = async (updateStatus = true) => {
api_status.value = true
api_response.value = 'Status: OK'
}
} catch (e) {
return 'Unknown'
}
}
const autoConfig = async () => {
try {
const response = await fetch(`${api_url.value}${api_path.value}/system/auto`, {
method: 'POST',
headers: {
'Accept': 'application/json'
},
body: JSON.stringify({'origin': window.location.origin})
})
const json = await response.json()
if (200 !== response.status) {
return;
}
if (!api_url.value) {
api_url.value = json.url
}
if (!api_path.value) {
api_path.value = json.path
}
if (!api_token.value) {
api_token.value = json.token
}
await testApi();
} catch (e) {
}
}
const setOrigin = () => api_url.value = window.location.origin;
const hasAPISettings = computed(() => '' !== real_api_token.value && '' !== real_api_url.value)
const closeOverlay = () => {
loadFile.value = ''
}
const closeOverlay = () => loadFile.value = ''
</script>

View File

@@ -88,10 +88,12 @@
<input class="input" id="form_value" type="text" placeholder="Value" v-model="form_value">
<div class="icon is-small is-left"><i class="fas fa-font"></i></div>
</template>
<div>
<p class="help" v-html="getHelp(form_key)"></p>
</div>
</div>
</div>
</div>
<div class="card-footer">
<div class="card-footer-item">
<button class="button is-fullwidth is-primary" type="submit" :disabled="!form_key || '' === form_value">
@@ -115,11 +117,23 @@
</div>
<div v-else class="column is-12" v-if="items">
<div class="columns is-multiline">
<div class="column is-4" v-for="item in filteredRows(items)" :key="item.key">
<div class="card">
<div class="column" v-for="item in filteredRows(items)" :key="item.key"
:class="{'is-4':!item?.danger,'is-12':item.danger}">
<div class="card" :class="{ 'is-danger': item?.danger }">
<header class="card-header">
<p class="card-header-title is-unselectable">
<span class="has-tooltip is-clickable" v-tooltip="item.description">{{ item.key }}</span>
<template v-if="item?.danger">
<span class="title is-5 ">
<span class="icon" v-tooltip="'This option is considered dangerous.'">
<i class="has-text-danger fas fa-exclamation-triangle"></i>&nbsp;
</span> {{ item.key }}
</span>
</template>
<template v-else>
<span class="has-tooltip is-clickable" v-tooltip="item.description">
{{ item.key }}
</span>
</template>
</p>
<span class="card-header-icon" v-if="item.mask" @click="item.mask = false" v-tooltip="'Unmask the value'">
<span class="icon"><i class="fas fa-unlock"></i></span>
@@ -140,6 +154,10 @@
:class="{ 'is-masked': item.mask, 'is-unselectable': item.mask }"
@click="(e) => e.target.classList.toggle('is-text-overflow')">
{{ item.value }}</p>
<p v-if="item?.danger" class="title is-5 has-text-danger">
{{ item.description }}
</p>
</div>
</div>
<footer class="card-footer">
@@ -181,6 +199,10 @@
<li>Some values are too large to fit into the view, clicking on the value will show the full value.</li>
<li>These values are loaded from the <code>{{ file }}</code> file.</li>
<li>To add a new variable click on the <i class="fa fa-add"></i> button.</li>
<li>Environment variables with <span class="has-text-danger">red borders</span> and <i
class="fas fa-exclamation-triangle"></i> icon are considered
dangerous. Please be careful when editing them.
</li>
</ul>
</Message>
</div>
@@ -359,6 +381,10 @@ const getHelp = key => {
let text = `${data[0].description}`
if (data[0]?.danger) {
text = `<span class="has-text-danger title is-5"> <i class="has-text-warning fas fa-exclamation-triangle fa-bounce"></i> ${text}</span>`
}
if (data[0]?.type) {
text += ` Expects: <code>${data[0].type}</code>`
}

View File

@@ -0,0 +1,33 @@
<?php
declare(strict_types=1);
namespace App\API\System;
use App\Libs\Attributes\Route\Post;
use App\Libs\Config;
use App\Libs\DataUtil;
use App\Libs\HTTP_STATUS;
use Psr\Http\Message\ResponseInterface as iResponse;
use Psr\Http\Message\ServerRequestInterface as iRequest;
final class AutoConfig
{
public const string URL = '%{api.prefix}/system/auto';
#[Post(self::URL . '[/]', name: 'system.autoconfig')]
public function __invoke(iRequest $request): iResponse
{
if (false === (bool)Config::get('api.auto', false)) {
return api_error('auto configuration is disabled.', HTTP_STATUS::HTTP_FORBIDDEN);
}
$data = DataUtil::fromRequest($request);
return api_response(HTTP_STATUS::HTTP_OK, [
'url' => $data->get('origin', ag($_SERVER, 'HTTP_ORIGIN', 'localhost')),
'path' => Config::get('api.prefix'),
'token' => Config::get('api.key'),
]);
}
}

View File

@@ -4,6 +4,7 @@ declare(strict_types=1);
namespace App\Libs\Middlewares;
use App\API\System\AutoConfig;
use App\API\System\HealthCheck;
use App\Libs\Config;
use App\Libs\HTTP_STATUS;
@@ -22,6 +23,7 @@ final class APIKeyRequiredMiddleware implements MiddlewareInterface
*/
private const array PUBLIC_ROUTES = [
HealthCheck::URL,
AutoConfig::URL,
];
/**