Added initial support for browing as selected user in WebUI
This commit is contained in:
63
frontend/components/UserSelection.vue
Normal file
63
frontend/components/UserSelection.vue
Normal file
@@ -0,0 +1,63 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="field">
|
||||
<label class="label" for="user">Browse as</label>
|
||||
<div class="control has-icons-left">
|
||||
<div class="select is-fullwidth">
|
||||
<select v-model="api_user" class="is-capitalized" :disabled="isLoading">
|
||||
<option v-for="user in users" :key="'user-'+user" :value="user">
|
||||
{{ user }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="icon is-left">
|
||||
<i class="fas fa-user"></i>
|
||||
</div>
|
||||
</div>
|
||||
<p class="has-text-danger">
|
||||
<span class="icon"><i class="fas fa-exclamation"/></span>
|
||||
Browse the WebUI as the selected user. This feature is new and not all endpoints supports it yet, over time we
|
||||
plan to add support for this feature to all endpoints. If the endpoint doesn't support this feature, the main
|
||||
user will be used instead.
|
||||
</p>
|
||||
</div>
|
||||
<div class="control has-text-right">
|
||||
<button type="submit" class="button is-primary" :disabled="!api_user || isLoading" @click="reloadPage">
|
||||
<span class="icon"><i class="fas fa-sync"/></span>
|
||||
<span>Reload</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import {useStorage} from '@vueuse/core'
|
||||
import request from '~/utils/request'
|
||||
import {notification} from "~/utils/index.js";
|
||||
|
||||
const api_user = useStorage('api_user', 'main')
|
||||
const users = ref(['main'])
|
||||
const isLoading = ref(true)
|
||||
|
||||
|
||||
onMounted(async () => {
|
||||
try {
|
||||
isLoading.value = true
|
||||
const response = await request('/system/users');
|
||||
if (!response.ok) {
|
||||
notification('error', 'Failed to fetch users.');
|
||||
return;
|
||||
}
|
||||
const json = await response.json();
|
||||
if ('users' in json) {
|
||||
users.value = json?.users;
|
||||
}
|
||||
} catch (e) {
|
||||
notification('error', `Failed to fetch users. ${e}`);
|
||||
} finally {
|
||||
isLoading.value = false
|
||||
}
|
||||
})
|
||||
|
||||
const reloadPage = () => window.location.reload()
|
||||
</script>
|
||||
@@ -30,7 +30,7 @@
|
||||
</NuxtLink>
|
||||
|
||||
<NuxtLink class="navbar-item" to="/history"
|
||||
@click.native="(e) => changeRoute(e, () => dEvent('history_main_link_clicked', { 'clear': true }))">
|
||||
@click.native="(e) => changeRoute(e, () => dEvent('history_main_link_clicked', { 'clear': true }))">
|
||||
<span class="icon-text">
|
||||
<span class="icon"><i class="fas fa-history"></i></span>
|
||||
<span>History</span>
|
||||
@@ -130,16 +130,16 @@
|
||||
|
||||
<div class="navbar-item">
|
||||
<button class="button is-dark has-tooltip-bottom" v-tooltip.bottom="'Switch to Light theme'"
|
||||
v-if="'auto' === selectedTheme" @click="selectTheme('light')">
|
||||
<span class="icon has-text-warning"><i class="fas fa-sun" /></span>
|
||||
v-if="'auto' === selectedTheme" @click="selectTheme('light')">
|
||||
<span class="icon has-text-warning"><i class="fas fa-sun"/></span>
|
||||
</button>
|
||||
<button class="button is-dark has-tooltip-bottom" v-tooltip.bottom="'Switch to Dark theme'"
|
||||
v-if="'light' === selectedTheme" @click="selectTheme('dark')">
|
||||
<span class="icon"><i class="fas fa-moon" /></span>
|
||||
v-if="'light' === selectedTheme" @click="selectTheme('dark')">
|
||||
<span class="icon"><i class="fas fa-moon"/></span>
|
||||
</button>
|
||||
<button class="button is-dark has-tooltip-bottom" v-tooltip.bottom="'Switch to auto theme'"
|
||||
v-if="'dark' === selectedTheme" @click="selectTheme('auto')">
|
||||
<span class="icon"><i class="fas fa-microchip" /></span>
|
||||
v-if="'dark' === selectedTheme" @click="selectTheme('auto')">
|
||||
<span class="icon"><i class="fas fa-microchip"/></span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -147,14 +147,20 @@
|
||||
<button class="button is-dark" @click="showTaskRunner = !showTaskRunner" v-tooltip="'Task Runner Status'">
|
||||
<span class="icon">
|
||||
<i class="fas fa-microchip"
|
||||
:class="{ 'has-text-success': taskRunner.status, 'has-text-warning': !taskRunner.status }"></i>
|
||||
:class="{ 'has-text-success': taskRunner.status, 'has-text-warning': !taskRunner.status }"></i>
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="navbar-item" v-if="hasAPISettings">
|
||||
<button class="button is-dark" @click="showUserSelection = !showUserSelection" v-tooltip="'Change User'">
|
||||
<span class="icon"><i class="fas fa-users"/></span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="navbar-item">
|
||||
<button class="button is-dark" @click="showConnection = !showConnection" v-tooltip="'Configure connection'">
|
||||
<span class="icon"><i class="fas fa-cog"></i></span>
|
||||
<span class="icon"><i class="fas fa-cog"/></span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -186,12 +192,12 @@
|
||||
<div class="field has-addons">
|
||||
<div class="control is-expanded">
|
||||
<input class="input" id="api_token" v-model="api_token" required placeholder="API Token..."
|
||||
@keyup="api_status = false; api_response = ''"
|
||||
:type="false === exposeToken ? 'password' : 'text'">
|
||||
@keyup="api_status = false; api_response = ''"
|
||||
:type="false === exposeToken ? 'password' : 'text'">
|
||||
</div>
|
||||
<div class="control">
|
||||
<button type="button" class="button is-primary" @click="exposeToken = !exposeToken"
|
||||
v-tooltip="'Show/Hide token'">
|
||||
v-tooltip="'Show/Hide token'">
|
||||
<span class="icon" v-if="!exposeToken"><i class="fas fa-eye"></i></span>
|
||||
<span class="icon" v-else><i class="fas fa-eye-slash"></i></span>
|
||||
</button>
|
||||
@@ -218,7 +224,8 @@
|
||||
<div class="field">
|
||||
<div class="control">
|
||||
<input class="input" id="api_url" type="url" v-model="api_url" required
|
||||
placeholder="API URL... http://localhost:8081" @keyup="api_status = false; api_response = ''">
|
||||
placeholder="API URL... http://localhost:8081"
|
||||
@keyup="api_status = false; api_response = ''">
|
||||
<p class="help">
|
||||
Use <a href="javascript:void(0)" @click="setOrigin">current page URL</a>.
|
||||
</p>
|
||||
@@ -238,7 +245,7 @@
|
||||
<div class="field">
|
||||
<div class="control">
|
||||
<input class="input" id="api_path" type="text" v-model="api_path" required
|
||||
placeholder="API Path... /v1/api" @keyup="api_status = false; api_response = ''">
|
||||
placeholder="API Path... /v1/api" @keyup="api_status = false; api_response = ''">
|
||||
<p class="help">
|
||||
Use <a href="javascript:void(0)" @click="api_path = '/v1/api'">Set default API Path</a>.
|
||||
</p>
|
||||
@@ -253,7 +260,7 @@
|
||||
<div class="field has-addons">
|
||||
<div class="control is-expanded">
|
||||
<input class="input" type="text" v-model="api_response" readonly disabled
|
||||
:class="{ 'has-background-success': true === api_status, 'has-background-warning': true !== api_status }">
|
||||
:class="{ 'has-background-success': true === api_status, 'has-background-warning': true !== api_status }">
|
||||
</div>
|
||||
<div class="control">
|
||||
<button type="submit" class="button is-primary" :disabled="!api_url || !api_token">
|
||||
@@ -287,10 +294,10 @@
|
||||
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
|
||||
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>
|
||||
<code>WatchState</code> is exposed to the internet.</strong>
|
||||
</p>
|
||||
|
||||
<p>Please visit
|
||||
@@ -308,9 +315,9 @@
|
||||
|
||||
<div>
|
||||
<TaskRunnerStatus v-if="showTaskRunner || false === taskRunner?.status" :status="taskRunner"
|
||||
@taskrunner_update="e => taskRunner = e" />
|
||||
<NuxtPage v-if="!showConnection && hasAPISettings" />
|
||||
<no-api v-if="!hasAPISettings" />
|
||||
@taskrunner_update="e => taskRunner = e"/>
|
||||
<NuxtPage v-if="!showConnection && hasAPISettings"/>
|
||||
<no-api v-if="!hasAPISettings"/>
|
||||
</div>
|
||||
|
||||
<div class="columns is-multiline is-mobile mt-3">
|
||||
@@ -322,13 +329,13 @@
|
||||
</div>
|
||||
|
||||
<div class="column is-6 is-9-mobile has-text-left">
|
||||
<NuxtLink @click="loadFile = '/README.md'" v-text="'README'" />
|
||||
<NuxtLink @click="loadFile = '/README.md'" v-text="'README'"/>
|
||||
-
|
||||
<NuxtLink @click="loadFile = '/FAQ.md'" v-text="'FAQ'" />
|
||||
<NuxtLink @click="loadFile = '/FAQ.md'" v-text="'FAQ'"/>
|
||||
-
|
||||
<NuxtLink @click="loadFile = '/NEWS.md'" v-text="'News'" />
|
||||
<NuxtLink @click="loadFile = '/NEWS.md'" v-text="'News'"/>
|
||||
-
|
||||
<NuxtLink :to="changelog_url" v-text="'ChangeLog'" />
|
||||
<NuxtLink :to="changelog_url" v-text="'ChangeLog'"/>
|
||||
</div>
|
||||
<div class="column is-6 is-4-mobile has-text-right">
|
||||
{{ api_version }} - <a href="https://github.com/arabcoders/watchstate" target="_blank">WatchState</a>
|
||||
@@ -337,7 +344,13 @@
|
||||
|
||||
<template v-if="loadFile">
|
||||
<Overlay @closeOverlay="closeOverlay" :title="loadFile">
|
||||
<Markdown :file="loadFile" />
|
||||
<Markdown :file="loadFile"/>
|
||||
</Overlay>
|
||||
</template>
|
||||
|
||||
<template v-if="showUserSelection">
|
||||
<Overlay @closeOverlay="() => showUserSelection = false" title="Change User">
|
||||
<UserSelection/>
|
||||
</Overlay>
|
||||
</template>
|
||||
</div>
|
||||
@@ -345,27 +358,31 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
import {ref} from 'vue'
|
||||
import 'assets/css/bulma.css'
|
||||
import 'assets/css/style.css'
|
||||
import 'assets/css/all.css'
|
||||
import { useStorage } from '@vueuse/core'
|
||||
import {useStorage} from '@vueuse/core'
|
||||
import request from '~/utils/request.js'
|
||||
import Markdown from '~/components/Markdown.vue'
|
||||
import TaskRunnerStatus from "~/components/TaskRunnerStatus.vue";
|
||||
import TaskRunnerStatus from '~/components/TaskRunnerStatus.vue'
|
||||
import UserSelection from '~/components/UserSelection.vue'
|
||||
|
||||
const selectedTheme = useStorage('theme', (() => window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light')())
|
||||
const showConnection = ref(false)
|
||||
|
||||
const taskRunner = ref({ status: true, message: '', restartable: false })
|
||||
const taskRunner = ref({status: true, message: '', restartable: false})
|
||||
const showTaskRunner = ref(false)
|
||||
const showUserSelection = ref(false)
|
||||
|
||||
const real_api_user = useStorage('api_user', 'main')
|
||||
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', '')
|
||||
|
||||
const api_url = ref(toRaw(real_api_url.value))
|
||||
const api_path = ref(toRaw(real_api_path.value))
|
||||
const api_user = ref(toRaw(real_api_user.value))
|
||||
const api_token = ref(toRaw(real_api_token.value))
|
||||
|
||||
const api_status = ref(false)
|
||||
@@ -481,6 +498,7 @@ const testApi = async () => {
|
||||
|
||||
if (200 === response.status) {
|
||||
real_api_url.value = api_url.value
|
||||
real_api_user.value = api_user.value
|
||||
real_api_path.value = api_path.value
|
||||
real_api_token.value = api_token.value
|
||||
await getVersion(false)
|
||||
@@ -521,7 +539,7 @@ const autoConfig = async () => {
|
||||
headers: {
|
||||
'Accept': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({ 'origin': window.location.origin })
|
||||
body: JSON.stringify({'origin': window.location.origin})
|
||||
})
|
||||
|
||||
const json = await response.json()
|
||||
|
||||
@@ -3,6 +3,7 @@ import {useStorage} from "@vueuse/core";
|
||||
const api_path = useStorage('api_path', '/v1/api')
|
||||
const api_url = useStorage('api_url', '')
|
||||
const api_token = useStorage('api_token', '')
|
||||
const api_user = useStorage('api_user', 'main')
|
||||
|
||||
/**
|
||||
* Request content from the API. This function will automatically add the API token to the request headers.
|
||||
@@ -29,6 +30,11 @@ export default async function request(url, options = {}) {
|
||||
if (options.headers['Accept'] === undefined) {
|
||||
options.headers['Accept'] = 'application/json';
|
||||
}
|
||||
|
||||
if (options.headers['X-User'] === undefined) {
|
||||
options.headers['X-User'] = api_user.value;
|
||||
}
|
||||
|
||||
return fetch(`${api_url.value}${api_path.value}${url}`, options);
|
||||
}
|
||||
|
||||
|
||||
28
src/API/System/Users.php
Normal file
28
src/API/System/Users.php
Normal file
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\API\System;
|
||||
|
||||
use App\Libs\Attributes\Route\Get;
|
||||
use App\Libs\Enums\Http\Status;
|
||||
use App\Libs\Mappers\ExtendedImportInterface as iEImport;
|
||||
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;
|
||||
|
||||
final class Users
|
||||
{
|
||||
use APITraits;
|
||||
|
||||
public const string URL = '%{api.prefix}/system/users';
|
||||
|
||||
#[Get(self::URL . '[/]', name: 'system.users')]
|
||||
public function __invoke(iRequest $request, iEImport $mapper, iLogger $logger): iResponse
|
||||
{
|
||||
return api_response(Status::OK, [
|
||||
'users' => array_keys(getUsersContext($mapper, $logger)),
|
||||
]);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user