Added initial code to add custom GUID via API and implemented the WebUI form for it.

This commit is contained in:
Abdulmhsen B. A. A.
2024-10-05 19:42:45 +03:00
parent b770d3c0d3
commit db6f3edfe5
2 changed files with 279 additions and 72 deletions

View File

@@ -11,7 +11,7 @@
</div>
</div>
<div class="column is-12">
<form id="page_form" @submit.prevent="addIgnoreRule">
<form id="page_form" @submit.prevent="addNewGuid">
<div class="card">
<header class="card-header">
<p class="card-header-title is-unselectable is-justify-center">Add Custom GUID</p>
@@ -20,21 +20,33 @@
<div class="card-content">
<div class="field">
<label class="label is-unselectable" for="form_ignore_id">Name</label>
<label class="label is-unselectable" for="form_guid_name">Name</label>
<div class="control has-icons-left">
<input class="input" id="form_guid_name" type="text" v-model="form.name" placeholder="guid_foobar">
<div class="icon is-small is-left"><i class="fas fa-passport"></i></div>
</div>
<p class="help">
<span class="icon-text">
<span class="icon"><i class="fas fa-info"></i></span>
<span>All GUIDs names must start with <code>guid_</code>. For example,
<code>guid_foobar</code>. You cannot use the same name as an existing GUID.
</span>
<span class="icon"><i class="fas fa-info"></i></span>
<span>The internal GUID reference name. The name must starts with <code>guid</code>, followed by
<code>_</code>, <code>lower case [a-z]</code>, <code>0-9</code>, <code>no space</code>.
For example, <code>guid_imdb</code>.
</span>
</p>
</div>
<div class="field">
<label class="label is-unselectable" for="form_description">Description</label>
<div class="control has-icons-left">
<input class="input" id="form_description" type="text" v-model="form.description"
placeholder="This GUID is based on ... db reference">
<div class="icon is-small is-left"><i class="fas fa-envelope-open-text"></i></div>
</div>
<p class="help">
<span class="icon"><i class="fas fa-info"></i></span>
<span>GUID description, For information purposes only.</span>
</p>
</div>
<div class="field">
<label class="label is-unselectable" for="form_select_type">Type</label>
<div class="control has-icons-left">
@@ -49,25 +61,8 @@
</div>
</div>
<p class="help">
<span class="icon-text">
<span class="icon"><i class="fas fa-info"></i></span>
<span>We currently only support <code>string</code> type.</span>
</span>
</p>
</div>
<div class="field">
<label class="label is-unselectable" for="form_description">Description</label>
<div class="control has-icons-left">
<input class="input" id="form_description" type="text" v-model="form.description"
placeholder="This GUID is based on ... db reference">
<div class="icon is-small is-left"><i class="fas fa-envelope-open-text"></i></div>
</div>
<p class="help">
<span class="icon-text">
<span class="icon"><i class="fas fa-info"></i></span>
<span>GUID description, For information purposes only.</span>
</span>
<span class="icon"><i class="fas fa-info"></i></span>
<span>We currently only support <code>string</code> type.</span>
</p>
</div>
@@ -79,17 +74,28 @@
<div class="icon is-small is-left"><i class="fas fa-check"></i></div>
</div>
<p class="help">
<span class="icon-text">
<span class="icon"><i class="fas fa-info"></i></span>
<span>
A Valid regular expression to check the value GUID value. To test your patterns, you can use this
website
<NuxtLink target="_blank" to="https://regex101.com/#php73" v-text="'regex101.com'"/>
.
</span>
<span class="icon"><i class="fas fa-info"></i></span>
<span>
A Valid regular expression to check the value GUID value. To test your patterns, you can use this
website
<NuxtLink target="_blank" to="https://regex101.com/#php73" v-text="'regex101.com'"/>
.
</span>
</p>
</div>
<div class="field">
<label class="label is-unselectable" for="form_validation_example">Value example</label>
<div class="control has-icons-left">
<input class="input" id="form_validation_example" type="text" v-model="form.validator.example"
placeholder="(number)">
<div class="icon is-small is-left"><i class="fas fa-ear-deaf"></i></div>
</div>
<p class="help">
<span class="icon"><i class="fas fa-info"></i></span>
<span>The example to show when invalid value was checked. For example, <code>(number)</code>. For
information purposes only.</span>
</p>
</div>
<div class="field">
<label class="label is-unselectable">
@@ -100,7 +106,8 @@
<template v-for="(_, index) in form.validator.tests.valid" :key="`valid-${index}`">
<div class="column is-11">
<div class="control has-icons-left">
<input class="input" type="text" v-model="form.validator.tests.valid[index]">
<input class="input" type="text" :id="`valid-${index}`"
v-model="form.validator.tests.valid[index]">
<div class="icon is-small is-left"><i class="fas fa-check"></i></div>
</div>
</div>
@@ -114,13 +121,12 @@
</template>
</div>
<p class="help">
<span class="icon-text">
<span class="icon"><i class="fas fa-info"></i></span>
<span>
The values added here must match the pattern defined above. Example: <code>123</code>.
Additionally, the pattern also must support <code>/</code> being part of the value. as we used it
for relative GUIDs. There must be a minimum of 1 correct value.
</span>
<span class="icon"><i class="fas fa-info"></i></span>
<span>
The values added here must match the pattern defined above. Example: <code>123</code>.
Additionally, the pattern also must support <code>/</code> being part of the value. as we used it
for relative GUIDs. The <code>(number)/1/1</code> refers to a relative GUID.
There must be a minimum of 1 correct value.
</span>
</p>
</div>
@@ -134,7 +140,8 @@
<template v-for="(_, index) in form.validator.tests.invalid" :key="`valid-${index}`">
<div class="column is-11">
<div class="control has-icons-left">
<input class="input" type="text" v-model="form.validator.tests.invalid[index]">
<input class="input" type="text" :id="`invalid-${index}`"
v-model="form.validator.tests.invalid[index]">
<div class="icon is-small is-left"><i class="fas fa-check"></i></div>
</div>
</div>
@@ -148,18 +155,17 @@
</template>
</div>
<p class="help">
<span class="icon-text">
<span class="icon"><i class="fas fa-info"></i></span>
<span>GUID values with should not match the pattern defined above. Example: <code>abc</code>. There
must be a minimum of 1 incorrect value.</span>
</span>
<span class="icon"><i class="fas fa-info"></i></span>
<span>GUID values with should not match the pattern defined above. Example: <code>abc</code>. There
must be a minimum of 1 incorrect value.</span>
</p>
</div>
</div>
<div class="card-footer">
<div class="card-footer-item">
<button class="button is-fullwidth is-primary" type="submit" :disabled="false === checkForm">
<button class="button is-fullwidth is-primary" type="submit" :disabled="false === validForm || isSaving"
:class="{'is-loading':isSaving}">
<span class="icon-text">
<span class="icon"><i class="fas fa-save"></i></span>
<span>Save</span>
@@ -167,7 +173,7 @@
</button>
</div>
<div class="card-footer-item">
<button class="button is-fullwidth is-danger" type="button" @click="cancelForm">
<button class="button is-fullwidth is-danger" type="button" @click="navigateTo('/custom')">
<span class="icon-text">
<span class="icon"><i class="fas fa-cancel"></i></span>
<span>Cancel</span>
@@ -201,15 +207,21 @@ useHead({title: 'Add Custom GUID'})
const empty_form = {
name: '',
type: '',
type: 'string',
description: '',
validator: {pattern: '', example: '', tests: {valid: [''], invalid: ['']}}
validator: {
pattern: '/^[0-9\\\\/]+$/i',
example: '(number)',
tests: {
valid: ['1234567', '1234567/1/1'],
invalid: ['1234567a', 'a1234567']
}
}
}
const show_page_tips = useStorage('show_page_tips', true)
const items = ref([])
const form = ref(JSON.parse(JSON.stringify(empty_form)))
const guids = ref([])
const isSaving = ref(false)
onMounted(async () => {
try {
@@ -220,38 +232,121 @@ onMounted(async () => {
}
})
const addIgnoreRule = async () => {
const val = guids.value.find(g => g.guid === form.value.db)
if (val && val?.validator && val.validator.pattern) {
if (!stringToRegex(val.validator.pattern).test(form.value.id)) {
notification('error', 'Error', `Invalid GUID value, must match the pattern: '${val.validator.pattern}'. Example ${val.validator.example}`, 5000)
return
}
const addNewGuid = async () => {
if (!validForm.value) {
notification('error', 'Error', 'Invalid form data.', 5000)
return
}
let data = form.value
data.name = data.name.trim();
if (data.name.toLowerCase() !== data.name) {
notification('error', 'Error', `GUID name must be lowercase.`, 5000)
return
}
if (false === stringToRegex('/^[a-z0-9_]+$/').test(data.name)) {
notification('error', 'Error', `GUID name must be in ASCII, rules are [lower case, a-z, 0-9, no space] starts with guid_`, 5000)
return
}
if (data.name.includes(' ')) {
notification('error', 'Error', `GUID name must not contain spaces.`, 5000)
return
}
if (!data.name.startsWith('guid_')) {
notification('error', 'Error', `GUID name must start with 'guid_'.`, 5000)
return
}
data.type = data.type.trim().toLowerCase();
if (!['string'].includes(data.type)) {
notification('error', 'Error', `Invalid GUID type.`, 5000)
return
}
try {
const response = await request(`/ignore`, {
method: 'POST',
body: JSON.stringify(form.value)
toRaw(guids.value).forEach(g => {
const name = data.name.split('_')[1]
if (g.guid === name) {
throw new Error(`GUID with name '${data.name}' already exists.`)
}
})
} catch (e) {
notification('error', 'Error', `${e}`, 5000)
return false
}
try {
const validator = stringToRegex(data.validator.pattern);
for (let i = 0; i < data.validator.tests.valid.length; i++) {
if (!validator.test(data.validator.tests.valid[i])) {
notification('error', 'Error', `Correct value '${i}' '${data.validator.tests.valid[i]}' did not match '${data.validator.pattern}'.`, 5000)
return false
}
if (!validator.test(data.validator.tests.valid[i] + '/1')) {
notification('error', 'Error', `Correct value '${i}' with relative info '${data.validator.tests.valid[i] + '/1'}' did not match '${data.validator.pattern}'.`, 5000)
return false
}
}
for (let i = 0; i < data.validator.tests.invalid.length; i++) {
const invalid = data.validator.tests.invalid[i]
if (validator.test(data.validator.tests.invalid[i])) {
notification('error', 'Error', `Incorrect value '${i}' '${invalid}' matched '${data.validator.pattern}'.`, 5000)
return false
}
}
} catch (e) {
notification('error', 'Error', `Invalid regex pattern.`, 5000)
return false
}
isSaving.value = true
try {
const response = await request('/system/guids/custom', {
method: 'PUT',
body: JSON.stringify(data)
})
const json = await response.json()
const json = await parse_api_response(response)
if (!response.ok) {
notification('error', 'Error', `${json.error.code}: ${json.error.message}`, 5000)
return
}
items.value.push(json)
notification('success', 'Success', 'Successfully added new ignore rule.', 5000)
notification('success', 'Success', 'Successfully added new GUID.', 5000)
await navigateTo('/custom')
} catch (e) {
notification('error', 'Error', `Request error. ${e.message}`, 5000)
} finally {
isSaving.value = false
}
}
const checkForm = computed(() => {
const {id, type, backend, db} = form.value
return '' !== id && '' !== type && '' !== backend && '' !== db
const validForm = computed(() => {
const data = form.value
if (!data.name || !data.type || !data.description) {
return false
}
if (!data.validator.pattern || !data.validator.example) {
return false
}
if (!data.validator.tests.valid.length || !data.validator.tests.invalid.length) {
return false
}
return !(!data.validator.tests.valid[0] || !data.validator.tests.invalid[0]);
})
</script>

View File

@@ -13,9 +13,11 @@ use App\Libs\Container;
use App\Libs\DataUtil;
use App\Libs\Enums\Http\Status;
use App\Libs\Guid;
use InvalidArgumentException;
use Psr\Http\Message\ResponseInterface as iResponse;
use Psr\Http\Message\ServerRequestInterface as iRequest;
use Psr\Log\LoggerInterface;
use Throwable;
final class Guids
{
@@ -53,7 +55,102 @@ final class Guids
{
$params = DataUtil::fromRequest($request);
return api_response(Status::OK, $request->getParsedBody());
$requiredFields = [
'name',
'type',
'description',
'validator.pattern',
'validator.example',
'validator.tests.valid',
'validator.tests.invalid'
];
foreach ($requiredFields as $field) {
if (!$params->get($field)) {
return api_error(r("Field '{field}' is required. And is missing from request.", [
'field' => $field
]), Status::BAD_REQUEST);
}
}
try {
$this->validateName($params->get('name'));
} catch (InvalidArgumentException $e) {
return api_error($e->getMessage(), Status::BAD_REQUEST);
}
$pattern = stripslashes($params->get('validator.pattern'));
try {
preg_match($pattern, '');
} catch (Throwable) {
return api_error(r("Invalid regex pattern: '{pattern}'.", ['pattern' => $pattern]), Status::BAD_REQUEST);
}
if (count($params->get('validator.tests.valid')) < 1) {
return api_error('At least one valid test is required.', Status::BAD_REQUEST);
}
foreach ($params->get('validator.tests.valid', []) as $index => $test) {
if (empty($test)) {
return api_error(r("Empty value {index} - '{test}' is not allowed.", [
'index' => $index,
'test' => $test
]), Status::BAD_REQUEST);
}
if (1 === preg_match($pattern, (string)$test)) {
continue;
}
return api_error(
r("Correct value {index} - '{test}' did not match given pattern '{pattern}'.", [
'index' => $index,
'test' => $test,
'pattern' => $pattern,
]),
Status::BAD_REQUEST
);
}
if (count($params->get('validator.tests.invalid')) < 1) {
return api_error('At least one invalid test is required.', Status::BAD_REQUEST);
}
foreach ($params->get('validator.tests.invalid', []) as $index => $test) {
if (1 !== preg_match($pattern, (string)$test)) {
continue;
}
return api_error(r("Incorrect value {index} - '{test}' matched given pattern '{pattern}'.", [
'index' => $index,
'test' => $test,
'pattern' => $pattern
]), Status::BAD_REQUEST);
}
$data = [
'name' => $params->get('name'),
'type' => $params->get('type'),
'description' => $params->get('description'),
'validator' => [
'pattern' => $params->get('validator.pattern'),
'example' => $params->get('validator.example'),
'tests' => [
'valid' => $params->get('validator.tests.valid'),
'invalid' => $params->get('validator.tests.invalid')
]
]
];
$file = ConfigFile::open(Config::get('guid.file'), 'yaml', autoCreate: true, autoBackup: true);
if (!$file->has('guids') || !is_array($file->get('guids'))) {
$file->set('guids', []);
}
$file->set('guids.' . count($file->get('guids', [])), $data)->persist();
return api_response(Status::OK, $data);
}
#[Delete(self::URL . '/custom/{index:number}[/]', name: 'system.guids.custom.guid.remove')]
@@ -124,4 +221,19 @@ final class Guids
return $guids;
}
private function validateName(string $name): void
{
if (false === preg_match('/^[a-z0-9_]+$/i', $name)) {
throw new InvalidArgumentException('Name must be alphanumeric and underscores only.');
}
if (strtolower($name) !== $name) {
throw new InvalidArgumentException('Name must be lowercase.');
}
if (str_contains($name, ' ')) {
throw new InvalidArgumentException('Name must not contain spaces.');
}
}
}