Added initial support for browing as selected user in WebUI

This commit is contained in:
ArabCoders
2025-02-05 13:12:21 +03:00
parent a246ca7579
commit a4aec9ef99
4 changed files with 145 additions and 30 deletions

View 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>

View File

@@ -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()

View File

@@ -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
View 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)),
]);
}
}