Feature: Validate JSON before uploading
This commit is contained in:
parent
225cab676a
commit
8019eaf182
@ -575,7 +575,7 @@
|
|||||||
"YieldDayCorrection": "Corrección de Rendimiento Diario",
|
"YieldDayCorrection": "Corrección de Rendimiento Diario",
|
||||||
"YieldDayCorrectionHint": "Sumar el rendimiento diario incluso si el inversor se reinicia. El valor se restablecerá a medianoche"
|
"YieldDayCorrectionHint": "Sumar el rendimiento diario incluso si el inversor se reinicia. El valor se restablecerá a medianoche"
|
||||||
},
|
},
|
||||||
"configadmin": {
|
"fileadmin": {
|
||||||
"ConfigManagement": "Gestión de Configuración",
|
"ConfigManagement": "Gestión de Configuración",
|
||||||
"BackupHeader": "Copia de seguridad: Copia de Seguridad del Archivo de Configuración",
|
"BackupHeader": "Copia de seguridad: Copia de Seguridad del Archivo de Configuración",
|
||||||
"BackupConfig": "Copia de seguridad del archivo de configuración",
|
"BackupConfig": "Copia de seguridad del archivo de configuración",
|
||||||
@ -592,7 +592,9 @@
|
|||||||
"FactoryReset": "Restablecimiento de Fábrica",
|
"FactoryReset": "Restablecimiento de Fábrica",
|
||||||
"ResetMsg": "¿Está seguro de que desea eliminar la configuración actual y restablecer todas las configuraciones a sus valores predeterminados de fábrica?",
|
"ResetMsg": "¿Está seguro de que desea eliminar la configuración actual y restablecer todas las configuraciones a sus valores predeterminados de fábrica?",
|
||||||
"ResetConfirm": "Restablecimiento de Fábrica",
|
"ResetConfirm": "Restablecimiento de Fábrica",
|
||||||
"Cancel": "@:base.Cancel"
|
"Cancel": "@:base.Cancel",
|
||||||
|
"InvalidJson": "JSON file is formatted incorrectly.",
|
||||||
|
"InvalidJsonContent": "JSON file has the wrong content."
|
||||||
},
|
},
|
||||||
"login": {
|
"login": {
|
||||||
"Login": "Iniciar Sesión",
|
"Login": "Iniciar Sesión",
|
||||||
|
|||||||
@ -575,7 +575,7 @@
|
|||||||
"YieldDayCorrection": "Correzione energia giornaliera",
|
"YieldDayCorrection": "Correzione energia giornaliera",
|
||||||
"YieldDayCorrectionHint": "Aggiungi questo valore all'energia giornaliera se l'inverter è stato riavviato. Questo valore sarò resettato a mezzanotte"
|
"YieldDayCorrectionHint": "Aggiungi questo valore all'energia giornaliera se l'inverter è stato riavviato. Questo valore sarò resettato a mezzanotte"
|
||||||
},
|
},
|
||||||
"configadmin": {
|
"fileadmin": {
|
||||||
"ConfigManagement": "Configurazione Gestione",
|
"ConfigManagement": "Configurazione Gestione",
|
||||||
"BackupHeader": "Backup: Configurazione File Backup",
|
"BackupHeader": "Backup: Configurazione File Backup",
|
||||||
"BackupConfig": "Esegui il backup del file:",
|
"BackupConfig": "Esegui il backup del file:",
|
||||||
@ -592,7 +592,9 @@
|
|||||||
"FactoryReset": "Factory Reset",
|
"FactoryReset": "Factory Reset",
|
||||||
"ResetMsg": "Sei sicuro di voler cancellare la configurazione attuale e applicare la configurazione di fabbrica?",
|
"ResetMsg": "Sei sicuro di voler cancellare la configurazione attuale e applicare la configurazione di fabbrica?",
|
||||||
"ResetConfirm": "Factory Reset!",
|
"ResetConfirm": "Factory Reset!",
|
||||||
"Cancel": "@:base.Cancel"
|
"Cancel": "@:base.Cancel",
|
||||||
|
"InvalidJson": "JSON file is formatted incorrectly.",
|
||||||
|
"InvalidJsonContent": "JSON file has the wrong content."
|
||||||
},
|
},
|
||||||
"login": {
|
"login": {
|
||||||
"Login": "Login",
|
"Login": "Login",
|
||||||
|
|||||||
@ -582,7 +582,9 @@
|
|||||||
"Name": "Name",
|
"Name": "Name",
|
||||||
"Size": "Größe",
|
"Size": "Größe",
|
||||||
"Action": "Aktion",
|
"Action": "Aktion",
|
||||||
"Cancel": "@:base.Cancel"
|
"Cancel": "@:base.Cancel",
|
||||||
|
"InvalidJson": "JSON-Datei ist falsch formatiert.",
|
||||||
|
"InvalidJsonContent": "JSON-Datei hat den falschen Inhalt."
|
||||||
},
|
},
|
||||||
"login": {
|
"login": {
|
||||||
"Login": "Anmeldung",
|
"Login": "Anmeldung",
|
||||||
|
|||||||
@ -582,7 +582,9 @@
|
|||||||
"Name": "Name",
|
"Name": "Name",
|
||||||
"Size": "Size",
|
"Size": "Size",
|
||||||
"Action": "Action",
|
"Action": "Action",
|
||||||
"Cancel": "@:base.Cancel"
|
"Cancel": "@:base.Cancel",
|
||||||
|
"InvalidJson": "JSON file is formatted incorrectly.",
|
||||||
|
"InvalidJsonContent": "JSON file has the wrong content."
|
||||||
},
|
},
|
||||||
"login": {
|
"login": {
|
||||||
"Login": "Login",
|
"Login": "Login",
|
||||||
|
|||||||
@ -564,7 +564,9 @@
|
|||||||
"Name": "Name",
|
"Name": "Name",
|
||||||
"Size": "Size",
|
"Size": "Size",
|
||||||
"Action": "Action",
|
"Action": "Action",
|
||||||
"Cancel": "@:base.Cancel"
|
"Cancel": "@:base.Cancel",
|
||||||
|
"InvalidJson": "JSON file is formatted incorrectly.",
|
||||||
|
"InvalidJsonContent": "JSON file has the wrong content."
|
||||||
},
|
},
|
||||||
"login": {
|
"login": {
|
||||||
"Login": "Connexion",
|
"Login": "Connexion",
|
||||||
|
|||||||
25
webapp/src/utils/structure.ts
Normal file
25
webapp/src/utils/structure.ts
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
export type Schema = {
|
||||||
|
[key: string]: 'string' | 'number' | 'boolean' | 'object' | 'array' | Schema;
|
||||||
|
};
|
||||||
|
|
||||||
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
|
export function hasStructure(obj: any, schema: Schema): boolean {
|
||||||
|
if (typeof obj !== 'object' || obj === null) return false;
|
||||||
|
|
||||||
|
for (const key in schema) {
|
||||||
|
const expectedType = schema[key];
|
||||||
|
|
||||||
|
if (['string', 'number', 'boolean'].includes(expectedType as string)) {
|
||||||
|
if (typeof obj[key] !== expectedType) return false;
|
||||||
|
} else if (expectedType === 'object') {
|
||||||
|
if (typeof obj[key] !== 'object' || obj[key] === null) return false;
|
||||||
|
} else if (expectedType === 'array') {
|
||||||
|
if (!Array.isArray(obj[key])) return false;
|
||||||
|
} else if (typeof expectedType === 'object') {
|
||||||
|
// Recursively check nested objects
|
||||||
|
if (!hasStructure(obj[key], expectedType as Schema)) return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
@ -57,17 +57,23 @@
|
|||||||
<div v-else-if="!uploading">
|
<div v-else-if="!uploading">
|
||||||
<div class="row g-3 align-items-center form-group pt-2">
|
<div class="row g-3 align-items-center form-group pt-2">
|
||||||
<div class="col-sm">
|
<div class="col-sm">
|
||||||
<select class="form-select" v-model="restoreFileSelect">
|
<select class="form-select" v-model="restoreFileSelect" @change="onUploadFileChange">
|
||||||
<option selected value="config.json">Main Config (config.json)</option>
|
<option v-for="file in restoreList" :key="file.name" :value="file.name">
|
||||||
<option selected value="pin_mapping.json">Pin Mapping (pin_mapping.json)</option>
|
{{ file.descr }}
|
||||||
<option selected value="pack.lang.json">Language Pack (pack.lang.json)</option>
|
</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-sm">
|
<div class="col-sm">
|
||||||
<input class="form-control" type="file" ref="file" accept=".json" />
|
<input
|
||||||
|
class="form-control"
|
||||||
|
type="file"
|
||||||
|
ref="file"
|
||||||
|
accept=".json"
|
||||||
|
@change="onUploadFileChange"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-sm">
|
<div class="col-sm">
|
||||||
<button class="btn btn-primary" @click="onUpload">
|
<button class="btn btn-primary" @click="onUpload" :disabled="!isValidJson">
|
||||||
{{ $t('fileadmin.Restore') }}
|
{{ $t('fileadmin.Restore') }}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@ -132,6 +138,8 @@ import ModalDialog from '@/components/ModalDialog.vue';
|
|||||||
import type { AlertResponse } from '@/types/AlertResponse';
|
import type { AlertResponse } from '@/types/AlertResponse';
|
||||||
import type { FileInfo } from '@/types/File';
|
import type { FileInfo } from '@/types/File';
|
||||||
import { authHeader, handleResponse } from '@/utils/authentication';
|
import { authHeader, handleResponse } from '@/utils/authentication';
|
||||||
|
import type { Schema } from '@/utils/structure';
|
||||||
|
import { hasStructure } from '@/utils/structure';
|
||||||
import { waitRestart } from '@/utils/waitRestart';
|
import { waitRestart } from '@/utils/waitRestart';
|
||||||
import * as bootstrap from 'bootstrap';
|
import * as bootstrap from 'bootstrap';
|
||||||
import {
|
import {
|
||||||
@ -169,6 +177,24 @@ export default defineComponent({
|
|||||||
UploadError: '',
|
UploadError: '',
|
||||||
UploadSuccess: false,
|
UploadSuccess: false,
|
||||||
restoreFileSelect: 'config.json',
|
restoreFileSelect: 'config.json',
|
||||||
|
restoreList: [
|
||||||
|
{
|
||||||
|
name: 'config.json',
|
||||||
|
descr: 'Main Config (config.json)',
|
||||||
|
template: { cfg: 'object' } as Schema,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'pin_mapping.json',
|
||||||
|
descr: 'Pin Mapping (pin_mapping.json)',
|
||||||
|
template: { name: 'string' } as Schema,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'pack.lang.json',
|
||||||
|
descr: 'Language Pack (pack.lang.json)',
|
||||||
|
template: { meta: 'object' } as Schema,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
isValidJson: false,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
@ -230,6 +256,43 @@ export default defineComponent({
|
|||||||
this.callFileApiEndpoint('delete', JSON.stringify({ file: this.selectedFile.name }));
|
this.callFileApiEndpoint('delete', JSON.stringify({ file: this.selectedFile.name }));
|
||||||
this.onCloseModal(this.modalDelete);
|
this.onCloseModal(this.modalDelete);
|
||||||
},
|
},
|
||||||
|
onUploadFileChange() {
|
||||||
|
const target = this.$refs.file as HTMLInputElement;
|
||||||
|
if (target.files !== null) {
|
||||||
|
this.file = target.files[0];
|
||||||
|
}
|
||||||
|
if (!this.file) return;
|
||||||
|
|
||||||
|
// Read the file content
|
||||||
|
const reader = new FileReader();
|
||||||
|
reader.onload = (e) => {
|
||||||
|
try {
|
||||||
|
const checkTemplate = this.restoreList.find((i) => i.name == this.restoreFileSelect)?.template;
|
||||||
|
// Parse the file content as JSON
|
||||||
|
let checkValue = JSON.parse(e.target?.result as string);
|
||||||
|
if (Array.isArray(checkValue)) {
|
||||||
|
checkValue = checkValue[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (checkValue && checkTemplate && hasStructure(checkValue, checkTemplate)) {
|
||||||
|
this.isValidJson = true;
|
||||||
|
this.alert.show = false;
|
||||||
|
} else {
|
||||||
|
this.isValidJson = false;
|
||||||
|
this.alert.message = this.$t('fileadmin.InvalidJsonContent');
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
this.isValidJson = false;
|
||||||
|
this.alert.message = this.$t('fileadmin.InvalidJson');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.isValidJson) {
|
||||||
|
this.alert.type = 'warning';
|
||||||
|
this.alert.show = true;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
reader.readAsText(this.file);
|
||||||
|
},
|
||||||
onUpload() {
|
onUpload() {
|
||||||
this.uploading = true;
|
this.uploading = true;
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user