Refactor how to we handle some sensitive env variables.
This commit is contained in:
41
FAQ.md
41
FAQ.md
@@ -487,38 +487,6 @@ https://watchstate.example.org {
|
||||
|
||||
---
|
||||
|
||||
# WS_API_AUTO
|
||||
|
||||
The `WS_API_AUTO` environment variable is designed to **automate the initial configuration process**, particularly
|
||||
useful for users who access the WebUI from multiple browsers or devices. Since the WebUI requires API settings to be
|
||||
configured before use, enabling this variable allows the system to auto-configure itself.
|
||||
|
||||
To enable it, write `WS_API_AUTO=true` to `/config/.env` file, note the file may not exist, and you may need to create
|
||||
it.
|
||||
|
||||
## Why You Might Use It
|
||||
|
||||
You may consider using this if:
|
||||
|
||||
- You're operating in a **secure, local environment**.
|
||||
- You want to **automate setup** across multiple devices or browsers without repeatedly entering API details.
|
||||
|
||||
## Why You Should **NOT** Use It (Recommended)
|
||||
|
||||
Enabling this poses a **serious security risk**:
|
||||
|
||||
- It **exposes your API key** publicly through the endpoint `/v1/api/system/auto`.
|
||||
- Anyone (or any bot) that can access the WebUI can retrieve your API key and gain **access** to any and all data that
|
||||
is exposed by the API including your media servers API keys.
|
||||
|
||||
**If WatchState is exposed to the internet, do not enable this setting.**
|
||||
|
||||
> [!IMPORTANT]
|
||||
> The `WS_API_AUTO` variable is a **major security risk**. It should only be used in isolated or trusted environments.
|
||||
> We strongly recommend keeping this option disabled.
|
||||
|
||||
---
|
||||
|
||||
# How to disable the included cache server and use an external cache server?
|
||||
|
||||
To disable the built-in cache server and connect to an external Redis instance, follow these steps:
|
||||
@@ -913,7 +881,8 @@ In order to sync the watch progress between media backends, you need to enable t
|
||||
$ docker exec -ti watchstate console system:env -k WS_SYNC_PROGRESS -e true
|
||||
```
|
||||
|
||||
For best experience, you should enable the [webhook](guides/webhooks.md) feature for the media backends you want to sync the watch
|
||||
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.
|
||||
@@ -922,8 +891,10 @@ To check if there is any watch progress events being registered, You can go to `
|
||||
`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.
|
||||
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.
|
||||
|
||||
```
|
||||
WS_CRON_IMPORT_AT=0 11 * * *
|
||||
|
||||
@@ -18,13 +18,13 @@ use Monolog\Level;
|
||||
|
||||
return (function () {
|
||||
$inContainer = inContainer();
|
||||
$progressTimeCheck = fn (int $v, int $d): int => 0 === $v || $v >= 180 ? $v : $d;
|
||||
$progressTimeCheck = fn(int $v, int $d): int => 0 === $v || $v >= 180 ? $v : $d;
|
||||
|
||||
$config = [
|
||||
'name' => 'WatchState',
|
||||
'version' => '$(version_via_ci)',
|
||||
'tz' => env('WS_TZ', env('TZ', 'UTC')),
|
||||
'path' => fixPath(env('WS_DATA_PATH', fn () => $inContainer ? '/config' : __DIR__ . '/../var')),
|
||||
'path' => fixPath(env('WS_DATA_PATH', fn() => $inContainer ? '/config' : __DIR__ . '/../var')),
|
||||
'logs' => [
|
||||
'context' => (bool)env('WS_LOGS_CONTEXT', false),
|
||||
'prune' => [
|
||||
@@ -35,7 +35,6 @@ 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_\-]+',
|
||||
'ubackend' => '[a-zA-Z0-9_\-\@]+',
|
||||
@@ -45,7 +44,7 @@ return (function () {
|
||||
'encode' => JSON_INVALID_UTF8_IGNORE | JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_AMP | JSON_HEX_QUOT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE,
|
||||
'headers' => [
|
||||
'Content-Type' => 'application/json',
|
||||
'X-Application-Version' => fn () => getAppVersion(),
|
||||
'X-Application-Version' => fn() => getAppVersion(),
|
||||
'Access-Control-Allow-Origin' => '*',
|
||||
],
|
||||
],
|
||||
@@ -155,14 +154,14 @@ return (function () {
|
||||
|
||||
$config['profiler'] = [
|
||||
'save' => (bool)env('WS_PROFILER_SAVE', true),
|
||||
'path' => env('WS_PROFILER_PATH', fn () => ag($config, 'tmpDir') . '/profiler'),
|
||||
'path' => env('WS_PROFILER_PATH', fn() => ag($config, 'tmpDir') . '/profiler'),
|
||||
'collector' => env('WS_PROFILER_COLLECTOR', null),
|
||||
];
|
||||
|
||||
$config['cache'] = [
|
||||
'prefix' => env('WS_CACHE_PREFIX', null),
|
||||
'url' => env('WS_CACHE_URL', 'redis://127.0.0.1:6379'),
|
||||
'path' => env('WS_CACHE_PATH', fn () => ag($config, 'tmpDir') . '/cache'),
|
||||
'path' => env('WS_CACHE_PATH', fn() => ag($config, 'tmpDir') . '/cache'),
|
||||
];
|
||||
|
||||
$config['logger'] = [
|
||||
|
||||
@@ -146,12 +146,6 @@ 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',
|
||||
],
|
||||
[
|
||||
'key' => 'WS_CONSOLE_ENABLE_ALL',
|
||||
'description' => 'All executing all commands in the console. They must be prefixed with $',
|
||||
@@ -232,7 +226,7 @@ return (function () {
|
||||
],
|
||||
[
|
||||
'key' => 'WS_SYSTEM_USER',
|
||||
'description' => '(NOT IMPLEMENTED YET) The login user name',
|
||||
'description' => 'The login user name',
|
||||
'type' => 'string',
|
||||
'validate' => function (mixed $value): string {
|
||||
if (!is_numeric($value) && empty($value)) {
|
||||
@@ -240,15 +234,18 @@ return (function () {
|
||||
}
|
||||
|
||||
if (false === isValidName($value)) {
|
||||
throw new ValidationException('Invalid username. Username can only contains [lower case a-z, 0-9 and _].');
|
||||
throw new ValidationException(
|
||||
'Invalid username. Username can only contains [lower case a-z, 0-9 and _].'
|
||||
);
|
||||
}
|
||||
return $value;
|
||||
},
|
||||
'mask' => true,
|
||||
'hidden' => true,
|
||||
],
|
||||
[
|
||||
'key' => 'WS_SYSTEM_PASSWORD',
|
||||
'description' => '(NOT IMPLEMENTED YET) The login password. The given plaintext password will be converted to hash.',
|
||||
'description' => 'The login password. The given plaintext password will be converted to hash.',
|
||||
'type' => 'string',
|
||||
'validate' => function (mixed $value): string {
|
||||
if (empty($value)) {
|
||||
@@ -270,6 +267,7 @@ return (function () {
|
||||
return $prefix . $hash;
|
||||
},
|
||||
'mask' => true,
|
||||
'hidden' => true,
|
||||
],
|
||||
];
|
||||
|
||||
|
||||
@@ -10,19 +10,19 @@
|
||||
<div class="field is-grouped">
|
||||
<div class="control has-icons-left" v-if="toggleFilter || query">
|
||||
<input type="search" v-model.lazy="query" class="input" id="filter"
|
||||
placeholder="Filter displayed content">
|
||||
<span class="icon is-left"><i class="fas fa-filter" /></span>
|
||||
placeholder="Filter displayed content">
|
||||
<span class="icon is-left"><i class="fas fa-filter"/></span>
|
||||
</div>
|
||||
|
||||
<div class="control">
|
||||
<button class="button is-danger is-light" @click="toggleFilter = !toggleFilter">
|
||||
<span class="icon"><i class="fas fa-filter" /></span>
|
||||
<span class="icon"><i class="fas fa-filter"/></span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<p class="control">
|
||||
<button class="button is-primary" v-tooltip.bottom="'Add new variable'" @click="toggleForm = !toggleForm"
|
||||
:disabled="isLoading">
|
||||
:disabled="isLoading">
|
||||
<span class="icon">
|
||||
<i class="fas fa-add"></i>
|
||||
</span>
|
||||
@@ -30,7 +30,7 @@
|
||||
</p>
|
||||
<p class="control">
|
||||
<button class="button is-info" @click="loadContent" :disabled="isLoading || toggleForm"
|
||||
:class="{ 'is-loading': isLoading }">
|
||||
:class="{ 'is-loading': isLoading }">
|
||||
<span class="icon"><i class="fas fa-sync"></i></span>
|
||||
</button>
|
||||
</p>
|
||||
@@ -45,9 +45,9 @@
|
||||
|
||||
<div class="column is-12" v-if="!toggleForm && filteredRows.length < 1">
|
||||
<Message v-if="isLoading" message_class="has-background-info-90 has-text-dark" title="Loading"
|
||||
icon="fas fa-spinner fa-spin" message="Loading data. Please wait..." />
|
||||
icon="fas fa-spinner fa-spin" message="Loading data. Please wait..."/>
|
||||
<Message v-else message_class="has-background-warning-90 has-text-dark"
|
||||
:title="query ? 'No results' : 'Information'" icon="fas fa-info-circle">
|
||||
:title="query ? 'No results' : 'Information'" icon="fas fa-info-circle">
|
||||
<p v-if="query">
|
||||
No environment variables found matching <strong>{{ query }}</strong>. Please try a different filter.
|
||||
</p>
|
||||
@@ -88,25 +88,25 @@
|
||||
<div class="field">
|
||||
<div class="field has-addons">
|
||||
<div class="control is-expanded">
|
||||
<input class="input" id="api_token" v-model="form_value" required placeholder="Masked value"
|
||||
:type="false === form_expose ? 'password' : 'text'">
|
||||
<input class="input" id="form_value" v-model="form_value" required placeholder="Masked value"
|
||||
:type="false === form_expose ? 'password' : 'text'">
|
||||
</div>
|
||||
<div class="control">
|
||||
<button type="button" class="button is-primary" @click="form_expose = !form_expose">
|
||||
<span class="icon" v-if="!form_expose"><i class="fas fa-eye" /></span>
|
||||
<span class="icon" v-else><i class="fas fa-eye-slash" /></span>
|
||||
<span class="icon" v-if="!form_expose"><i class="fas fa-eye"/></span>
|
||||
<span class="icon" v-else><i class="fas fa-eye-slash"/></span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<p class="help" v-html="getHelp(form_key)" />
|
||||
<p class="help title is-6" v-html="getHelp(form_key)"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="control has-icons-left">
|
||||
<template v-if="'bool' === form_type">
|
||||
<input id="form_value" type="checkbox" class="switch is-success" :checked="fixBool(form_value)"
|
||||
@change="form_value = !fixBool(form_value)">
|
||||
@change="form_value = !fixBool(form_value)">
|
||||
<label for="form_value">
|
||||
<template v-if="fixBool(form_value)">On (True)</template>
|
||||
<template v-else>Off (False)</template>
|
||||
@@ -114,7 +114,7 @@
|
||||
</template>
|
||||
<template v-else-if="'int' === form_type">
|
||||
<input class="input" id="form_value" type="number" placeholder="Value" v-model="form_value"
|
||||
pattern="[0-9]*" inputmode="numeric">
|
||||
pattern="[0-9]*" inputmode="numeric">
|
||||
<div class="icon is-small is-left">
|
||||
<i class="fas fa-font"></i>
|
||||
</div>
|
||||
@@ -124,7 +124,7 @@
|
||||
<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>
|
||||
<p class="help title is-6" v-html="getHelp(form_key)"></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -150,10 +150,11 @@
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div v-else class="column is-12" v-if="filteredRows">
|
||||
<div class="columns is-multiline">
|
||||
<div class="column" v-for="item in filteredRows" :key="item.key"
|
||||
:class="{ 'is-4': !item?.danger, 'is-12': item.danger }">
|
||||
: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">
|
||||
@@ -171,7 +172,7 @@
|
||||
</template>
|
||||
</p>
|
||||
<span class="card-header-icon" v-if="item.mask" @click="item.mask = false"
|
||||
v-tooltip="'Unmask the value'">
|
||||
v-tooltip="'Unmask the value'">
|
||||
<span class="icon"><i class="fas fa-unlock"></i></span>
|
||||
</span>
|
||||
</header>
|
||||
@@ -187,9 +188,10 @@
|
||||
</span>
|
||||
</p>
|
||||
<p v-else class="is-text-overflow is-clickable is-unselectable"
|
||||
:class="{ 'is-masked': item.mask, 'is-unselectable': item.mask }"
|
||||
@click="(e) => e.target.classList.toggle('is-text-overflow')">
|
||||
{{ item.value }}</p>
|
||||
: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 }}
|
||||
@@ -229,7 +231,7 @@
|
||||
|
||||
<div class="column is-12">
|
||||
<Message message_class="has-background-info-90 has-text-dark" :toggle="show_page_tips"
|
||||
@toggle="show_page_tips = !show_page_tips" :use-toggle="true" title="Tips" icon="fas fa-info-circle">
|
||||
@toggle="show_page_tips = !show_page_tips" :use-toggle="true" title="Tips" icon="fas fa-info-circle">
|
||||
<ul>
|
||||
<li>Some variables values are masked, to unmask them click on icon <i class="fa fa-unlock"></i>.</li>
|
||||
<li>Some values are too large to fit into the view, clicking on the value will show the full value.</li>
|
||||
@@ -250,13 +252,13 @@
|
||||
<script setup>
|
||||
import 'assets/css/bulma-switch.css'
|
||||
import request from '~/utils/request'
|
||||
import { awaitElement, copyText, notification } from '~/utils/index'
|
||||
import { useStorage } from '@vueuse/core'
|
||||
import {awaitElement, copyText, notification} from '~/utils/index'
|
||||
import {useStorage} from '@vueuse/core'
|
||||
import Message from '~/components/Message'
|
||||
|
||||
const route = useRoute()
|
||||
|
||||
useHead({ title: 'Environment Variables' })
|
||||
useHead({title: 'Environment Variables'})
|
||||
|
||||
const items = ref([])
|
||||
const toggleForm = ref(false)
|
||||
@@ -292,10 +294,15 @@ const loadContent = async () => {
|
||||
if (item && route.query?.value && !item?.value) {
|
||||
item.value = route.query.value
|
||||
}
|
||||
editEnv(item)
|
||||
if (!item) {
|
||||
notification('error', 'Error', `Invalid key '${route.query.edit}'.`, 2000)
|
||||
await cancelForm()
|
||||
} else {
|
||||
editEnv(item);
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
notification('error', 'Error', e.message, 5000)
|
||||
notification('error', 'Error', `Error. ${e.message}`, 5000)
|
||||
} finally {
|
||||
isLoading.value = false
|
||||
}
|
||||
@@ -307,10 +314,10 @@ const deleteEnv = async env => {
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await request(`/system/env/${env.key}`, { method: 'DELETE' })
|
||||
const response = await request(`/system/env/${env.key}`, {method: 'DELETE'})
|
||||
|
||||
if (200 !== response.status) {
|
||||
json = await parse_api_response(response)
|
||||
const json = await parse_api_response(response)
|
||||
notification('error', 'Error', `${json.error.code}: ${json.error.message}`, 5000)
|
||||
return
|
||||
}
|
||||
@@ -345,7 +352,7 @@ const addVariable = async () => {
|
||||
try {
|
||||
const response = await request(`/system/env/${key}`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ value: form_value.value })
|
||||
body: JSON.stringify({value: form_value.value})
|
||||
})
|
||||
|
||||
if (304 === response.status) {
|
||||
@@ -383,7 +390,7 @@ const editEnv = env => {
|
||||
form_mask.value = env.mask
|
||||
toggleForm.value = true
|
||||
if (!useRoute().query.edit) {
|
||||
useRouter().push({ 'path': '/env', query: { 'edit': env.key } })
|
||||
useRouter().push({'path': '/env', query: {'edit': env.key}})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -395,12 +402,12 @@ const cancelForm = async () => {
|
||||
form_mask.value = false
|
||||
toggleForm.value = false
|
||||
if (route.query?.callback) {
|
||||
await navigateTo({ path: route.query.callback })
|
||||
await navigateTo({path: route.query.callback})
|
||||
return
|
||||
}
|
||||
|
||||
if (route.query?.edit || route.query?.value) {
|
||||
await useRouter().push({ path: '/env' })
|
||||
await useRouter().push({path: '/env'})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -408,7 +415,7 @@ watch(toggleForm, async value => {
|
||||
if (!value) {
|
||||
await cancelForm()
|
||||
} else {
|
||||
awaitElement('#env_page_title', (_, el) => el.scrollIntoView({ behavior: 'smooth' }))
|
||||
awaitElement('#env_page_title', (_, el) => el.scrollIntoView({behavior: 'smooth'}))
|
||||
}
|
||||
})
|
||||
|
||||
@@ -426,7 +433,7 @@ const keyChanged = () => {
|
||||
form_value.value = false
|
||||
}
|
||||
});
|
||||
useRouter().push({ 'path': '/env', query: { 'edit': form_key.value } })
|
||||
useRouter().push({'path': '/env', query: {'edit': form_key.value}})
|
||||
}
|
||||
|
||||
const getHelp = key => {
|
||||
|
||||
@@ -1,60 +0,0 @@
|
||||
<?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\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
|
||||
{
|
||||
$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);
|
||||
}
|
||||
|
||||
$data = DataUtil::fromRequest($request);
|
||||
|
||||
return api_response(Status::OK, [
|
||||
'url' => $data->get('origin', ag($_SERVER, 'HTTP_ORIGIN', 'localhost')),
|
||||
'path' => Config::get('api.prefix'),
|
||||
'token' => Config::get('api.key'),
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -33,6 +33,10 @@ final class Env
|
||||
continue;
|
||||
}
|
||||
|
||||
if (true === (bool)ag($info, 'hidden', false)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$info['value'] = $this->setType($info, $this->envFile->get($info['key']));
|
||||
}
|
||||
|
||||
@@ -50,11 +54,14 @@ final class Env
|
||||
if (array_key_exists('validate', $info)) {
|
||||
unset($info['validate']);
|
||||
}
|
||||
if (true === (bool)ag($info, 'hidden', false)) {
|
||||
continue;
|
||||
}
|
||||
$list[] = $info;
|
||||
}
|
||||
|
||||
if (true === (bool)$params->get('set', false)) {
|
||||
$list = array_filter($list, fn ($info) => $this->envFile->has($info['key']));
|
||||
$list = array_filter($list, fn($info) => $this->envFile->has($info['key']));
|
||||
}
|
||||
|
||||
return api_response(Status::OK, [
|
||||
@@ -82,7 +89,7 @@ final class Env
|
||||
|
||||
return api_response(Status::OK, [
|
||||
'key' => $key,
|
||||
'value' => $this->settype($spec, ag($spec, 'value', fn () => $this->envFile->get($key))),
|
||||
'value' => $this->settype($spec, ag($spec, 'value', fn() => $this->envFile->get($key))),
|
||||
'description' => ag($spec, 'description'),
|
||||
'type' => ag($spec, 'type'),
|
||||
'mask' => (bool)ag($spec, 'mask', false),
|
||||
@@ -103,12 +110,17 @@ final class Env
|
||||
return api_error(r("Invalid key '{key}' was given.", ['key' => $key]), Status::BAD_REQUEST);
|
||||
}
|
||||
|
||||
$isHidden = true === (bool)ag($spec, 'hidden', false);
|
||||
if ($isHidden && false === $request->getAttribute('INTERNAL_REQUEST', false)) {
|
||||
return api_error(r("Key '{key}' is not set.", ['key' => $key]), Status::NOT_FOUND);
|
||||
}
|
||||
|
||||
if ('DELETE' === $request->getMethod()) {
|
||||
$this->envFile->remove($key)->persist();
|
||||
|
||||
return api_response(Status::OK, [
|
||||
'key' => $key,
|
||||
'value' => $this->setType($spec, ag($spec, 'value', fn () => $this->envFile->get($key))),
|
||||
'value' => $this->setType($spec, ag($spec, 'value', fn() => $this->envFile->get($key))),
|
||||
'description' => ag($spec, 'description'),
|
||||
'type' => ag($spec, 'type'),
|
||||
'mask' => (bool)ag($spec, 'mask', false),
|
||||
|
||||
@@ -6,7 +6,6 @@ namespace App\Commands\System;
|
||||
|
||||
use App\Command;
|
||||
use App\Libs\Attributes\Route\Cli;
|
||||
use App\Libs\Config;
|
||||
use App\Libs\Enums\Http\Status;
|
||||
use Symfony\Component\Console\Input\InputInterface as iInput;
|
||||
use Symfony\Component\Console\Input\InputOption;
|
||||
@@ -28,99 +27,12 @@ final class EnvCommand extends Command
|
||||
protected function configure(): void
|
||||
{
|
||||
$this->setName(self::ROUTE)
|
||||
->setDescription('Show/edit environment variables.')
|
||||
->setDescription('Manage Environment Variables.')
|
||||
->addOption('key', 'k', InputOption::VALUE_REQUIRED, 'Key to update.')
|
||||
->addOption('set', 'e', InputOption::VALUE_REQUIRED, 'Value to set.')
|
||||
->addOption('delete', 'd', InputOption::VALUE_NONE, 'Delete key.')
|
||||
->addOption('list', 'l', InputOption::VALUE_NONE, 'List All Supported keys.')
|
||||
->addOption('expose', 'x', InputOption::VALUE_NONE, 'Expose Hidden values.')
|
||||
->setHelp(
|
||||
r(
|
||||
<<<HELP
|
||||
|
||||
This command display the environment variables that was loaded during execution of the tool.
|
||||
|
||||
-------------------------------
|
||||
<notice>[ Environment variables rules ]</notice>
|
||||
-------------------------------
|
||||
|
||||
* The key MUST be in CAPITAL LETTERS. For example [<flag>WS_CRON_IMPORT</flag>].
|
||||
* The key MUST start with [<flag>WS_</flag>]. For example [<flag>WS_CRON_EXPORT</flag>].
|
||||
* The value is simple string. No complex data types are allowed. or shell expansion variables.
|
||||
* The value MUST be in one line. No multi-line values are allowed.
|
||||
* The key SHOULD attempt to mirror the key path in default config, If not applicable or otherwise impossible it
|
||||
should then use an approximate path.
|
||||
|
||||
-------
|
||||
<notice>[ FAQ ]</notice>
|
||||
-------
|
||||
|
||||
<question># How to load environment variables?</question>
|
||||
|
||||
For <comment>WatchState</comment> specific environment variables, we recommend using the <comment>WebUI</comment>,
|
||||
to manage the environment variables. However, you can also use this command to manage the environment variables.
|
||||
|
||||
We use this file to load your environment variables:
|
||||
|
||||
- <flag>{path}</flag>/<comment>.env</comment>
|
||||
|
||||
To load container specific variables i,e, the keys that does not start with <comment>WS_</comment> prefix,
|
||||
you can use the <comment>compose.yaml</comment> file.
|
||||
|
||||
For example,
|
||||
-------------------------------
|
||||
services:
|
||||
watchstate:
|
||||
image: ghcr.io/arabcoders/watchstate:latest
|
||||
restart: unless-stopped
|
||||
container_name: watchstate
|
||||
<flag>environment:</flag>
|
||||
- <flag>HTTP_PORT</flag>=<value>8080</value>
|
||||
- <flag>DISABLE_CACHE</flag>=<value>1</value>
|
||||
.......
|
||||
-------------------------------
|
||||
|
||||
<question># How to set environment variables?</question>
|
||||
|
||||
To set an environment variable, you can use the following command:
|
||||
|
||||
{cmd} <cmd>{route}</cmd> <flag>-k <value>ENV_NAME</value> -e <value>ENV_VALUE</value></flag>
|
||||
|
||||
<notice>Note: if you are using a space within the value you need to use the long form --set, for example:
|
||||
|
||||
{cmd} <cmd>{route}</cmd> <flag>-k <value>ENV_NAME</value> --set=<notice>"</notice><value>ENV VALUE</value><notice>"</notice></flag>
|
||||
|
||||
As you can notice the spaced value is wrapped with double <value>""</value> quotes.</notice>
|
||||
|
||||
<question># How to see all possible environment variables?</question>
|
||||
|
||||
{cmd} <cmd>{route}</cmd> <flag>--list</flag>
|
||||
|
||||
<question># How to delete environment variable?</question>
|
||||
|
||||
{cmd} <cmd>{route}</cmd> <flag>-d -k</flag> <value>ENV_NAME</value>
|
||||
|
||||
<question># How to get specific environment variable value?</question>
|
||||
|
||||
{cmd} <cmd>{route}</cmd> <flag>-k</flag> <value>ENV_NAME</value>
|
||||
|
||||
<notice>This will show the hidden value if the environment variable marked as sensitive.</notice>
|
||||
|
||||
<question># How to expose the hidden values for secret environment variables?</question>
|
||||
|
||||
You can use the <flag>--expose</flag> flag to expose the hidden values. for both <flag>--list</flag>
|
||||
or just the normal table display. For example:
|
||||
|
||||
{cmd} <cmd>{route}</cmd> <flag>--expose</flag>
|
||||
|
||||
HELP,
|
||||
[
|
||||
'cmd' => trim(commandContext()),
|
||||
'route' => self::ROUTE,
|
||||
'path' => after(Config::get('path') . '/config', ROOT_PATH),
|
||||
]
|
||||
)
|
||||
);
|
||||
->addOption('expose', 'x', InputOption::VALUE_NONE, 'Expose Hidden values.');
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user