finalizing the move to user/pass auth.

This commit is contained in:
arabcoders
2025-05-15 00:10:20 +03:00
parent 44da11212e
commit 77dea258ce
12 changed files with 99 additions and 308 deletions

242
FAQ.md
View File

@@ -1,57 +1,5 @@
# FAQ
# How to find the API key?
There are two ways to locate the API key:
## Via `.env` file
The API key is stored in the following file `/config/config/.env`, Open the file and look for the line starting with:
```
WS_API_KEY=random_string
```
The value after the equals sign is your API key.
## Via command
You can also retrieve the API key by running the following command on the docker host machine:
```bash
docker exec watchstate console system:apikey
```
This command will show the following lines:
```
Current API key:
random_string
```
The `random_string` is your API key.
----
# What Is the API key used for?
The API key is used to authenticate requests to the system and prevent unauthorized access.
It is required for all API endpoints, **except** the following:
```
/v1/api/[user@backend_name]/webhook
```
This webhook endpoint is open by default, unless you have enabled the `WS_SECURE_API_ENDPOINTS` environment variable.
If enabled, the API key will also be required for webhook access.
> [!IMPORTANT]
> The WebUI operates in standalone mode and is decoupled from the backend, so it requires the API key to fetch and
> display data.
----
# How to enable scheduled/automatic tasks?
To turn on automatic import or export tasks:
@@ -351,7 +299,7 @@ If there are no errors, the database has been repaired successfully. And you can
---
# Which external db ids `GUIDS` supported for Plex Media Server?
# Which Providers id `GUIDs` supported for Plex Media Server?
* tvdb://(id) `New plex agent`
* imdb://(id) `New plex agent`
@@ -371,7 +319,7 @@ If there are no errors, the database has been repaired successfully. And you can
---
# Which external db ids supported for Jellyfin and Emby?
# Which Providers id supported for Jellyfin and Emby?
* imdb://(id)
* tvdb://(id)
@@ -390,15 +338,14 @@ If there are no errors, the database has been repaired successfully. And you can
# Environment Variables
The recommended approach is for keys that starts with `WS_` use the `WebUI > Env` page, or `system:env` command via CLI.
For other keys that aren't directly related to the tool, you **MUST** load them via container environment or
the `compose.yaml` file.
The recommended approach is for keys that starts with `WS_` use the `WebUI > Env` page. For other keys that aren't
directly related to the tool, you **MUST** load them via container environment or the `compose.yaml` file.
to see list of loaded environment variables, click on `Env` page in the WebUI.
## Tool specific environment variables.
## WatchState specific environment variables.
These environment variables relates to the tool itself, You should manage them via `WebUI > Env` page
Should be added/managed via the `WebUI > Env` page.
| Key | Type | Description | Default |
|-------------------------|---------|-------------------------------------------------------------------------|--------------------------|
@@ -418,13 +365,11 @@ These environment variables relates to the tool itself, You should manage them v
| WS_CACHE_URL | string | Cache server URL. | `redis://127.0.0.1:6379` |
| WS_SECURE_API_ENDPOINTS | bool | Disregard the open route policy and require API key for all endpoints. | `false` |
> [!IMPORTANT]
> [!NOTE]
> for environment variables that has `{TASK}` tag, you **MUST** replace it with one of `IMPORT`, `EXPORT`, `BACKUP`,
`PRUNE`, `INDEXES` or `VALIDATE`.
## Add tool specific environment variables
Go to the `Env` page, click `+` button, you will get list of all supported keys with description.
>
> Note, those are just sample of the environment variables, to see the entire, please visit the `Env` page in the WebUI.
## Container specific environment variables.
@@ -432,22 +377,18 @@ Go to the `Env` page, click `+` button, you will get list of all supported keys
> These environment variables relates to the container itself, and MUST be added via container environment or by
> the `compose.yaml` file.
| Key | Type | Description | Default |
|---------------|---------|--------------------------------------|----------|
| WEBUI_ENABLED | bool | Enable WebUI. Value casted to a bool | `true` |
| DISABLE_HTTP | integer | Disable included `HTTP Server`. | `0` |
| DISABLE_CRON | integer | Disable included `Task Scheduler`. | `0` |
| DISABLE_CACHE | integer | Disable included `Cache Server`. | `0` |
| HTTP_PORT | string | Change the `HTTP` listen port. | `"8080"` |
| FPM_PORT | string | Change the `PHP-FPM` listen port. | `"9000"` |
| Key | Type | Description | Default |
|---------------|---------|------------------------------------|----------|
| DISABLE_HTTP | integer | Disable included `HTTP Server`. | `0` |
| DISABLE_CRON | integer | Disable included `Task Scheduler`. | `0` |
| DISABLE_CACHE | integer | Disable included `Cache Server`. | `0` |
| HTTP_PORT | string | Change the `HTTP` listen port. | `"8080"` |
| FPM_PORT | string | Change the `PHP-FPM` listen port. | `"9000"` |
> [!NOTE]
> You need to restart the container after changing these environment variables. those variables are not managed by the
> WatchState tool, they are managed by the container itself.
---
---
# How to disable the included HTTP server and use external server?
@@ -620,107 +561,6 @@ jellyfin devs fixes the issue. Please take look at the webhooks section to enabl
---
# Bare metal installation
We officially only support the docker container, however for the brave souls who want to install the tool directly on
their server, You can follow these steps.
## Requirements
* [PHP 8.4](http://https://www.php.net/downloads.php) with both the `CLI` and `fpm` mode.
* PHP Extensions `pdo`, `pdo-sqlite`, `mbstring`, `json`, `ctype`, `curl`, `redis`, `sodium` and `simplexml`.
* [Composer](https://getcomposer.org/download/) for dependency management.
* [Redis-server](https://redis.io/) for caching or a compatible implementation that works
with [php-redis](https://github.com/phpredis/phpredis).
* [Caddy](https://caddyserver.com/) for frontend handling. However, you can use whatever you like. As long as it has
support for fastcgi.
* [Node.js v20+](https://nodejs.org/en/download/) for `WebUI` compilation.
## Installation
* Clone the repository.
```bash
$ git clone https://github.com/arabcoders/watchstate.git
```
* Install the dependencies.
```bash
$ cd watchstate
$ composer install --no-dev
```
* If you your redis server on external server, run the following command
```bash
$ ./bin/console system:env -k WS_CACHE_URL -e '"redis://127.0.0.1:6379?password=your_password"'
```
Change the connection string to match your redis server.
* Compile the `WebUI`.
First you need to install `yarn` as it's our package manager of choice.
```bash
$ npm -g install yarn
```
Once that is done you are ready to compile the `WebUI`.
```bash
$ cd frontend
$ 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.
```bash
$ mv exported ../public
```
If you do `ls ../public` you should see the following contents
```bash
ws:/opt/app/public$ ls
exported index.php
```
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.
> [!NOTE]
> frontend server is needed All the `API`, `WebUI` and `Webhooks` operations.
```Caddyfile
http://watchstate.example.org {
# Change "[user]" to your user name.
root * /home/[user]/watchstate/public
# Change "unix//var/run/php/php8.3-fpm.sock" to your php-fpm socket.
php_fastcgi unix//var/run/php/php8.3-fpm.sock
}
```
* To access the console you can run the following command.
```bash
$ ./bin/console help
```
* To make the tasks scheduler work you need to add the following to your crontab.
```crontab
* * * * * /home/[user]/watchstate/bin/console system:tasks --run --save-log
```
For more information, please refer to the [Dockerfile](/Dockerfile). On how we do things to get the tool running.
---
# How does the file integrity feature works?
The feature first scan your entire history for reported media file paths. Depending on the results we do the following:
@@ -875,58 +715,22 @@ database indexes with the new guids.
# Sync watch progress.
In order to sync the watch progress between media backends, you need to enable the following environment variable
`WS_SYNC_PROGRESS` in `(WebUI) > Env` page or via the cli using the following command:
```bash
$ docker exec -ti watchstate console system:env -k WS_SYNC_PROGRESS -e true
```
`WS_SYNC_PROGRESS` in `WebUI > Env` page.
For best experience, you should enable the [webhook](guides/webhooks.md) feature for the media backends you want to sync
the watch
progress,
however, if you are unable to do so, the `Tasks > import` task will also generate progress watch events. However, it's
not as reliable as the `Webhooks` or as fast. using `Webhooks` is the recommended way and offers the best experience.
the watch progress, however, if you are unable to do so, the `Tasks > import` task will also generate progress watch
events. However, it's not as reliable as the `Webhooks` or as fast. using `Webhooks` is the recommended way and offers
the best experience.
To check if there is any watch progress events being registered, You can go to `(WebUI) > More > Events` and check
To check if there is any watch progress events being registered, You can go to `WebUI > More > Events` and check
`on_progress` events, if you are seeing those, this means the progress is being synced. Check the `Tasks logs` to see
the event log.
If this is setup and working you may be ok with changing the `WS_CRON_IMPORT_AT/WS_CRON_EXPORT_AT` schedule to something
less frequenet as
the sync progress working will update the progress near realtime. For example you could change these tasks to run daily
instead of hourly.
less frequent as the sync progress working will update the progress near realtime. For example you could change these
tasks to run daily instead of hourly.
```
WS_CRON_IMPORT_AT=0 11 * * *
WS_CRON_EXPORT_AT=30 11 * * *
```
---
# Sub users support.
### API/WebUI endpoints that supports sub users.
These endpoints support sub-users via `?user=username` query parameter, or via `X-User` header. The recommended
approach is to use the header.
* `/v1/api/backend/*`.
* `/v1/api/system/parity`.
* `/v1/api/system/integrity`.
* `/v1/api/ignore/*`.
* `/v1/api/history/*`.
### CLI commands that supports sub users.
These commands sub users can via `[-u, --user]` option flag.
* `state:import`.
* `state:export`.
* `state:backup`.
* `config:list`.
* `config:view`.
* `config:edit`.
* `db:list`.
* `backend:restore`.
* `backend:ignore:*`.

View File

@@ -9,8 +9,24 @@ box, this tool support `Jellyfin`, `Plex` and `Emby` media servers.
# Updates
### 2025-05-14
**Breaking change**, we have switched to using user/password form of authentication instead of API key for the WebUI,
this will lead to better security and easier to use. The API key is still available for the API, but not for the WebUI.
The first time you access the WebUI after the update, you will be asked to create a new system user/password. This is a
one time operation. Sorry about that. if you somehow lost your password, you can reset it by running the following
command from the host machine.
```bash
# change docker to podman if you are using podman
$ docker exec watchstate console system:resetpassword
```
Please refer to [NEWS](/NEWS.md) for the latest updates and changes.
------
# Features
* Management via WebUI.
@@ -97,36 +113,15 @@ container, but it will be mapped to the user in which the command was run under.
After starting the container, you can access the WebUI by visiting `http://localhost:8080` in your browser.
> [!NOTE]
> The very first time you access the WebUI, we will attempt to autoconfigure your API connection. Should this fail, you
> can manually configure the API connection by following the instructions below.
At the start you won't see anything as the `WebUI` is decoupled from the WatchState and need to be configured to be able
to access the API. In the top right corner, you will see a cogwheel icon, click on it and then Configure the connection
settings.
![Connection settings](screenshots/api_settings.png)
As shown in the screenshot, to get your `API Token`, you can do via two methods
### Method 1
view the contents of the `./data/config/.env` file, and copy the contents after `WS_API_KEY=` variable.
### Method 2
From the host machine, you can run the following command
```bash
# change docker to podman if you are using podman
$ docker exec watchstate console system:apikey
```
Insert the `API key` into the `API Token` field and make sure to set the `API URL` or click the `current page URL` link.
If everything is ok, the reset of the navbar will show up.
> Note, For the first time, you will be prompted to create a new system user, this is a one time operation.
To add your backends, please click on the help button in the top right corner, and choose which method you
want [one-way](guides/one-way-sync.md) or [two-way](guides/two-way-sync.md) sync. and follow the instructions.
Once you have setup your backends and imported your data you should see something like
![WebUI](/screenshots/index.png)
### Supported import methods
Currently, the tool supports three methods to import data from backends.
@@ -141,14 +136,12 @@ Currently, the tool supports three methods to import data from backends.
> [!NOTE]
> Even if all your backends support webhooks, you should keep import task enabled. This help keep healthy relationship
> and pick up any missed events. For more information please check the [webhook guide](/guides/webhooks.md) to
> understand
> webhooks limitations.
> understand webhooks limitations.
# FAQ
Take look at this [frequently asked questions](FAQ.md) page, or the [guides](/guides/) for more in-depth guides on how
to
configure things.
to configure things.
# Social channels

View File

@@ -73,7 +73,6 @@
</template>
<script setup>
import {useStorage} from '@vueuse/core'
import {marked} from 'marked'
import {baseUrl} from 'marked-base-url'
import markedAlert from 'marked-alert'
@@ -88,7 +87,6 @@ const props = defineProps({
});
const content = ref('')
const api_url = useStorage('api_url', '')
const error = ref('')
const isLoading = ref(true)
@@ -138,7 +136,7 @@ const removeListeners = () => {
onMounted(async () => {
try {
isLoading.value = true
const response = await fetch(`${api_url.value}${props.file}?_=${Date.now()}`)
const response = await fetch(`${props.file}?_=${Date.now()}`)
if (!response.ok) {
const err = await parse_api_response(response)
console.log(err)
@@ -148,36 +146,15 @@ onMounted(async () => {
const text = await response.text()
marked.use(baseUrl(api_url.value))
marked.use(baseUrl(window.origin))
marked.use(markedAlert());
marked.use({
gfm: true,
hooks: {
postprocess: (text) => {
// // -- replace GitHub [! with icon
// text = text.replace(/\[!IMPORTANT]/g, `
// <span class="is-block title mb-2 is-5">
// <span class="icon-text">
// <span class="icon"><i class="fas fa-exclamation-triangle has-text-danger"></i></span>
// <span>IMPORTANT</span>
// </span>
// </span>`)
//
// text = text.replace(/\[!NOTE]/g, `
// <span class="is-block title is-5">
// <span class="icon-text">
// <span class="icon"><i class="fas fa-info has-text-info-50"></i></span>
// <span>NOTE</span>
// </span>
// </span>`)
text = text.replace(
/<!--\s*?i:([\w.-]+)\s*?-->/gi,
(_, list) => `<span class="icon"><i class="fas ${list.split('.').map(n => n.trim()).join(' ')}"></i></span>`
);
return text
}
postprocess: (text) => text.replace(
/<!--\s*?i:([\w.-]+)\s*?-->/gi,
(_, list) => `<span class="icon"><i class="fas ${list.split('.').map(n => n.trim()).join(' ')}"></i></span>`
)
},
walkTokens: token => {
if ('link' !== token.type) {
@@ -187,16 +164,19 @@ onMounted(async () => {
if (true === token.href.startsWith('#')) {
return;
}
const urls = ['/FAQ.md', '/README.md', '/NEWS.md'];
const list = ['/guides/', ...urls];
const urls = ['FAQ.md', 'README.md', 'NEWS.md'];
const list = ['guides/', ...urls];
if (false === list.some(l => token.href.includes(l))) {
return;
}
if (urls.some(l => token.href.includes(l))) {
const url = new URL(token.href);
if (!token.href.startsWith('/')) {
token.href = '/' + token.href;
}
const url = new URL(window.origin + token.href);
url.pathname = `/guides${url.pathname}`;
token.href = url.pathname;
token.href = url.toString();
}
token.href = token.href.replace('/guides/', '/help/').replace('.md', '');

View File

@@ -11,7 +11,11 @@ try {
'/v1/api/': {
target: API_URL + '/v1/api/',
changeOrigin: true
}
},
'/guides/': {
target: API_URL + '/guides/',
changeOrigin: true
},
}
}
}
@@ -20,7 +24,7 @@ try {
export default defineNuxtConfig({
ssr: false,
devtools: { enabled: true },
devtools: {enabled: true},
devServer: {
port: 8081,
@@ -35,13 +39,13 @@ export default defineNuxtConfig({
app: {
head: {
"meta": [
{ "charset": "utf-8" },
{ "name": "viewport", "content": "width=device-width, initial-scale=1.0, maximum-scale=1.0" },
{ "name": "theme-color", "content": "#000000" }
{"charset": "utf-8"},
{"name": "viewport", "content": "width=device-width, initial-scale=1.0, maximum-scale=1.0"},
{"name": "theme-color", "content": "#000000"}
],
},
buildAssetsDir: "assets",
pageTransition: { name: 'page', mode: 'out-in' }
pageTransition: {name: 'page', mode: 'out-in'}
},
router: {

View File

@@ -8,7 +8,7 @@
<script setup>
const route = useRoute()
const slug = ref(`${route.params.slug?.length > 0 ? route.params.slug?.join('/') : ''}`)
const slug = ref(`${route.params.slug?.length > 0 ? route.params.slug.join('/') : ''}`)
const url = ref('')
onMounted(async () => {
const to_lower = String(slug.value).toLowerCase()
@@ -19,7 +19,7 @@ onMounted(async () => {
const special = ['faq', 'readme', 'news']
if (special.includes(to_lower)) {
url.value = '/' + to_lower.toUpperCase() + '.md'
url.value = '/guides/' + to_lower.toUpperCase() + '.md'
return
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 40 KiB

After

Width:  |  Height:  |  Size: 1.6 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 37 KiB

BIN
screenshots/index.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 MiB

View File

@@ -23,6 +23,12 @@ final class Auth
public const string URL = '%{api.prefix}/system/auth';
#[Get(self::URL . '/test[/]', name: 'system.auth.test')]
public function test_open(): iResponse
{
return api_response(Status::OK);
}
#[Get(self::URL . '/has_user[/]', name: 'system.auth.has_user')]
public function has_user(): iResponse
{

View File

@@ -20,7 +20,7 @@ final class ServeStatic implements LoggerAwareInterface
use LoggerAwareTrait;
private finfo|null $mimeType = null;
private const array CONTENT_TYPE = [
'css' => 'text/css; charset=utf-8',
'js' => 'text/javascript; charset=utf-8',
@@ -39,6 +39,9 @@ final class ServeStatic implements LoggerAwareInterface
'/NEWS.md' => __DIR__ . '/../../NEWS.md',
'/FAQ.md' => __DIR__ . '/../../FAQ.md',
'/CHANGELOG.md' => __DIR__ . '/../../CHANGELOG.md',
'/guides/README.md' => __DIR__ . '/../../README.md',
'/guides/NEWS.md' => __DIR__ . '/../../NEWS.md',
'/guides/FAQ.md' => __DIR__ . '/../../FAQ.md',
];
private const array MD_IMAGES = [

View File

@@ -4,7 +4,7 @@ declare(strict_types=1);
namespace Tests\Libs\Middlewares;
use App\API\System\AutoConfig;
use App\API\System\Auth;
use App\API\System\HealthCheck;
use App\Libs\Config;
use App\Libs\Enums\Http\Method;
@@ -38,19 +38,20 @@ class AuthorizationMiddlewareTest extends TestCase
handler: $this->getHandler()
);
$this->assertSame(Status::OK->value, $result->getStatusCode(), 'Options request failed');
$this->assertSame(Status::OK, Status::from($result->getStatusCode()), 'Options request failed');
}
public function test_open_routes()
{
Config::save('api.prefix', '/v1/api');
$routes = [
HealthCheck::URL,
AutoConfig::URL,
Auth::URL . '/test'
];
$routesSemiOpen = [
'/webhook',
'%{api.prefix}/player/',
];
foreach ($routes as $route) {
@@ -59,7 +60,7 @@ class AuthorizationMiddlewareTest extends TestCase
request: $this->getRequest(uri: $uri),
handler: $this->getHandler()
);
$this->assertSame(Status::OK->value, $result->getStatusCode(), "Open route '{$route}' failed");
$this->assertSame(Status::OK, Status::from($result->getStatusCode()), "Open route '{$uri}' failed");
}
foreach ($routesSemiOpen as $route) {
@@ -68,7 +69,7 @@ class AuthorizationMiddlewareTest extends TestCase
request: $this->getRequest(uri: $uri),
handler: $this->getHandler()
);
$this->assertSame(Status::OK->value, $result->getStatusCode(), "Open route '{$route}' failed");
$this->assertSame(Status::OK, Status::from($result->getStatusCode()), "Open route '{$uri}' failed");
}
Config::save('api.secure', true);
@@ -80,9 +81,9 @@ class AuthorizationMiddlewareTest extends TestCase
handler: $this->getHandler()
);
$this->assertSame(
Status::BAD_REQUEST->value,
$result->getStatusCode(),
"Route '{$route}' should fail without API key"
Status::BAD_REQUEST,
Status::from($result->getStatusCode()),
"Route '{$uri}' should fail without API key"
);
}
@@ -93,9 +94,9 @@ class AuthorizationMiddlewareTest extends TestCase
handler: $this->getHandler()
);
$this->assertSame(
Status::UNAUTHORIZED->value,
$result->getStatusCode(),
"Route '{$route}' should fail without correct API key"
Status::UNAUTHORIZED,
Status::from($result->getStatusCode()),
"Route '{$uri}' should fail without correct API key"
);
}
@@ -110,9 +111,9 @@ class AuthorizationMiddlewareTest extends TestCase
handler: $this->getHandler()
);
$this->assertSame(
Status::OK->value,
$result->getStatusCode(),
"Route '{$route}' should pass with correct API key"
Status::OK,
Status::from($result->getStatusCode()),
"Route '{$uri}' should pass with correct API key"
);
}

View File

@@ -100,11 +100,11 @@ class ServeStaticTest extends TestCase
// -- test screenshots serving, as screenshots path is not in public directory and not subject
// -- to same path restrictions as other files.
$response = $this->server->serve($this->createRequest('GET', '/screenshots/add_backend.png'));
$response = $this->server->serve($this->createRequest('GET', '/screenshots/index.png'));
$this->assertEquals(Status::OK->value, $response->getStatusCode());
$this->assertEquals('image/png', $response->getHeaderLine('Content-Type'));
$this->assertEquals(
file_get_contents(__DIR__ . '/../../screenshots/add_backend.png'),
file_get_contents(__DIR__ . '/../../screenshots/index.png'),
(string)$response->getBody()
);