re-enforce the backend/user name requirement of being a-z_0-9, due to recent refactor the check was not working as expected we added new tests to cover invalid names.

This commit is contained in:
ArabCoders
2025-02-26 17:11:23 +03:00
parent 1e73822cb8
commit 8a998eb96f
7 changed files with 92 additions and 43 deletions

View File

@@ -1,11 +1,12 @@
<template>
<Message title="Important" message_class="has-background-warning-80 has-text-dark" icon="fas fa-info-circle">
<ul>
<li>
WatchState is single user tool. It doesn't support syncing multiple users play state.
<li v-if="api_user === 'main'">
Support for sub users is in early stages. For more information please visit
<NuxtLink target="_blank" v-text="'Visit this link'"
to="https://github.com/arabcoders/watchstate/blob/master/FAQ.md#is-there-support-for-multi-user-setup"/>
to learn more.
to learn more. <b>DO NOT</b> add sub users backends directly. Use the create sub-users button after setting up
the main user.
</li>
<li>
If you are adding new backend that is fresh and doesn't have your current watch state, you should turn off
@@ -17,11 +18,10 @@
</ul>
</Message>
<form id="backend_add_form" @submit.prevent="stage<4 ? changeStep() : addBackend()">
<div class="card">
<div class="card-header">
<p class="card-header-title is-justify-center">Add Backend</p>
<p class="card-header-title">Add backend to '<u class="has-text-danger">{{ api_user }}</u>' user config.</p>
</div>
<div class="card-content">
@@ -32,6 +32,25 @@
</Message>
</div>
<template v-if="stage>=0">
<div class="field">
<label class="label">Local User</label>
<div class="control has-icons-left">
<div class="select is-fullwidth">
<select class="is-capitalized" disabled>
<option v-text="api_user"/>
</select>
</div>
<div class="icon is-left">
<i class="fas fa-users"></i>
</div>
<p class="help">
The local user which this backend will be associated with. You can change this user via the users icon
on top.
</p>
</div>
</div>
<div class="field">
<label class="label">Type</label>
<div class="control has-icons-left">
@@ -46,8 +65,8 @@
<i class="fas fa-server"></i>
</div>
<p class="help">
Select the type of backend you want to add. Supported backends are: <code>{{
supported.join(', ')
The backend server type. The supported types are <code>{{
supported.map(v => ucFirst(v)).join(', ')
}}</code>.
</p>
</div>
@@ -61,8 +80,8 @@
<i class="fas fa-id-badge"></i>
</div>
<p class="help">
Choose a unique name for this backend. You cannot change it later. Backend name must be in <code>lower
case a-z, 0-9 and _</code> only.
Choose a unique name for this backend. <b class="has-text-danger">You CANNOT change it later</b>.
Backend name must be in <code>lower case a-z, 0-9 and _</code> and cannot start with number.
</p>
</div>
</div>
@@ -94,7 +113,8 @@
<template v-if="'plex'===backend.type">
Enter the <code>X-Plex-Token</code>.
<NuxtLink target="_blank" to="https://support.plex.tv/articles/204059436"
v-text="'Visit This article for more information.'"/>
v-text="'Visit This link'"/>
to learn how to get the token.
</template>
<template v-else>
Generate a new API Key from <code>Dashboard > Settings > API Keys</code>.<br>
@@ -114,7 +134,8 @@
<input class="input" type="text" v-model="backend.options.PLEX_USER_PIN" :disabled="stage > 1">
<div class="icon is-left"><i class="fas fa-key"></i></div>
<p class="help">
If the user you going to select is using <code>PIN</code> to login, enter the PIN here.
If the user you are going to select has <code>PIN</code> enabled, you need to enter the pin here.
Otherwise it will fail to authenticate.
</p>
</div>
</template>
@@ -292,7 +313,7 @@
</div>
<div class="card-footer-item" v-if="stage < maxStages">
<button class="button is-fullwidth is-info" type="submit" @click="changeStep()">
<button class="button is-fullwidth is-info" type="button" @click="changeStep()">
<span class="icon"><i class="fas fa-arrow-right"></i></span>
<span>Next Step</span>
</button>
@@ -311,7 +332,8 @@
<script setup>
import 'assets/css/bulma-switch.css'
import request from '~/utils/request'
import {awaitElement, explode, notification} from '~/utils/index'
import {awaitElement, explode, notification, ucFirst} from '~/utils/index'
import {useStorage} from "@vueuse/core";
const emit = defineEmits(['addBackend', 'forceExport', 'runImport'])
@@ -343,6 +365,7 @@ const backend = ref({
},
options: {}
})
const api_user = useStorage('api_user', 'main')
const users = ref([])
const supported = ref([])
const servers = ref([])
@@ -518,8 +541,7 @@ const getUsers = async (showAlert = true) => {
}
onMounted(async () => {
const response = await request('/system/supported')
supported.value = await response.json()
supported.value = await (await request('/system/supported')).json()
backend.value.type = supported.value[0]
})
@@ -538,6 +560,11 @@ const changeStep = async () => {
return
}
if (false === /^[a-z_0-9]+$/.test(backend.value.name)) {
notification('error', 'Error', `Backend name must be in lower case a-z, 0-9 and _ only.`)
return
}
if (props.backends.find(b => b.name === backend.value.name)) {
notification('error', 'Error', `Backend with name '${backend.value.name}' already exists.`)
return

View File

@@ -45,10 +45,26 @@
<form id="backend_edit_form" @submit.prevent="saveContent">
<div class="card">
<header class="card-header">
<p class="card-header-title is-justify-center">Edit Backend - {{ backend.name }}</p>
<p class="card-header-title">
Edit Backend:&nbsp;<u class="has-text-danger">{{ api_user }}</u>@{{ backend.name }}</p>
</header>
<div class="card-content">
<div class="field">
<label class="label">Local User</label>
<div class="control has-icons-left">
<div class="select is-fullwidth">
<select class="is-capitalized" disabled>
<option v-text="api_user"/>
</select>
</div>
<div class="icon is-left">
<i class="fas fa-users"></i>
</div>
<p class="help">The local user which this backend is associated with.</p>
</div>
</div>
<div class="field">
<label class="label">Name</label>
<div class="control has-icons-left">
@@ -70,6 +86,11 @@
<div class="icon is-left">
<i class="fas fa-server"></i>
</div>
<p class="help">
The backend server type. The supported types are <code>{{
supported.map(v => ucFirst(v)).join(', ')
}}</code>.
</p>
</div>
</div>
@@ -347,6 +368,8 @@
import 'assets/css/bulma-switch.css'
import {notification, ucFirst} from '~/utils/index'
import Message from '~/components/Message'
import {useStorage} from "@vueuse/core";
import request from "~/utils/request.js";
const id = useRoute().params.backend
const redirect = useRoute().query?.redirect ?? `/backend/${id}`
@@ -363,9 +386,11 @@ const backend = ref({
webhook: {match: {user: false, uuid: false}},
options: {}
})
const showOptions = ref(false)
const isLoading = ref(true)
const users = ref([])
const supported = ref([])
const usersLoading = ref(false)
const uuidLoading = ref(false)
const optionsList = ref([])
@@ -375,6 +400,7 @@ const exposeToken = ref(false)
const servers = ref([])
const serversLoading = ref(false)
const isLimitedToken = computed(() => Boolean(backend.value.options?.is_limited_token))
const api_user = useStorage('api_user', 'main')
const selectedOptionHelp = computed(() => {
const option = optionsList.value.find(v => v.key === selectedOption.value)
@@ -384,6 +410,8 @@ const selectedOptionHelp = computed(() => {
useHead({title: 'Backends - Edit: ' + id})
const loadContent = async () => {
supported.value = await (await request('/system/supported')).json()
const content = await request(`/backend/${id}`)
let json = await content.json()

View File

@@ -53,7 +53,10 @@ final class Add
}
if (false === isValidName($name)) {
return api_error('Invalid name was given.', Status::BAD_REQUEST);
return api_error(
'Invalid name was given. Backend name must only contain [lowercase a-z, 0-9, _].',
Status::BAD_REQUEST
);
}
$backend = $this->getBackends(name: $name, userContext: $userContext);

View File

@@ -5,15 +5,15 @@ declare(strict_types=1);
namespace App\API\System;
use App\Libs\Attributes\Route\Delete;
use App\Libs\Exceptions\RuntimeException;
use App\Libs\Enums\Http\Status;
use Psr\Http\Message\ResponseInterface as iResponse;
use Psr\Http\Message\ServerRequestInterface as iRequest;
use Redis;
use RedisException;
use App\Libs\Exceptions\RuntimeException;
use App\Libs\Mappers\ImportInterface as iImport;
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 Redis;
use RedisException;
final class Reset
{
@@ -33,10 +33,7 @@ final class Reset
try {
$ns = getAppVersion();
if (true === isValidName($user)) {
$ns .= isValidName($user) ? '.' . $user : '.' . md5($user);
}
$ns .= isValidName($user) ? '.' . $user : '.' . md5($user);
$keys = $redis->keys("{$ns}*");

View File

@@ -542,7 +542,7 @@ class CreateUsersCommand extends Command
// Build final row: "name" + sub-array "backends"
$row = [
'name' => $finalName,
'name' => strtolower($finalName),
'backends' => [],
];

View File

@@ -904,7 +904,11 @@ if (!function_exists('isValidName')) {
*/
function isValidName(string $name): bool
{
return 1 === preg_match('/^\w+$/', $name);
if (true === ctype_digit($name[0])) {
return false;
}
return 1 === preg_match('/^[a-z_0-9]+$/', $name);
}
}
@@ -2413,10 +2417,7 @@ if (!function_exists('perUserCacheAdapter')) {
}
$ns = getAppVersion();
if (true === isValidName($user)) {
$ns .= isValidName($user) ? '.' . $user : '.' . md5($user);
}
$ns .= isValidName($user) ? '.' . $user : '.' . md5($user);
try {
$backend = new RedisAdapter(redis: Container::get(Redis::class), namespace: $ns);

View File

@@ -680,20 +680,13 @@ class HelpersTest extends TestCase
{
$this->assertTrue(isValidName('foo'), 'When name is valid, true is returned.');
$this->assertTrue(isValidName('foo_bar'), 'When name is valid, true is returned.');
$this->assertFalse(isValidName('foo_baR'), 'When name is invalid, false is returned.');
$this->assertFalse(isValidName('3oo_bar'), 'When name is invalid, false is returned.');
$invalidNames = [
'foo bar',
'foo-bar',
'foo/bar',
'foo?bar',
'foo*bar',
];
$invalidNames = ['foo bar', 'foo-bar', 'foo/bar', 'foo?bar', 'foo*bar', '1foo', 'foo_baR', 'FOOBAR'];
foreach ($invalidNames as $name) {
$this->assertFalse(
isValidName($name),
"When name ({$name}) is invalid, false is returned."
);
$this->assertFalse(isValidName($name), "When given name is '{$name}', false should be is returned.");
}
}