diff --git a/lang/es.lang.json b/lang/es.lang.json index 46b814a3..383e7a3a 100644 --- a/lang/es.lang.json +++ b/lang/es.lang.json @@ -575,7 +575,7 @@ "YieldDayCorrection": "Corrección de Rendimiento Diario", "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", "BackupHeader": "Copia de seguridad: 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", "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", - "Cancel": "@:base.Cancel" + "Cancel": "@:base.Cancel", + "InvalidJson": "JSON file is formatted incorrectly.", + "InvalidJsonContent": "JSON file has the wrong content." }, "login": { "Login": "Iniciar Sesión", diff --git a/lang/it.lang.json b/lang/it.lang.json index 40a11a5c..541a310a 100644 --- a/lang/it.lang.json +++ b/lang/it.lang.json @@ -575,7 +575,7 @@ "YieldDayCorrection": "Correzione energia giornaliera", "YieldDayCorrectionHint": "Aggiungi questo valore all'energia giornaliera se l'inverter è stato riavviato. Questo valore sarò resettato a mezzanotte" }, - "configadmin": { + "fileadmin": { "ConfigManagement": "Configurazione Gestione", "BackupHeader": "Backup: Configurazione File Backup", "BackupConfig": "Esegui il backup del file:", @@ -592,7 +592,9 @@ "FactoryReset": "Factory Reset", "ResetMsg": "Sei sicuro di voler cancellare la configurazione attuale e applicare la configurazione di fabbrica?", "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", diff --git a/webapp/src/locales/de.json b/webapp/src/locales/de.json index 2cedd649..84dd73ad 100644 --- a/webapp/src/locales/de.json +++ b/webapp/src/locales/de.json @@ -582,7 +582,9 @@ "Name": "Name", "Size": "Größe", "Action": "Aktion", - "Cancel": "@:base.Cancel" + "Cancel": "@:base.Cancel", + "InvalidJson": "JSON-Datei ist falsch formatiert.", + "InvalidJsonContent": "JSON-Datei hat den falschen Inhalt." }, "login": { "Login": "Anmeldung", diff --git a/webapp/src/locales/en.json b/webapp/src/locales/en.json index c8419722..76cf932c 100644 --- a/webapp/src/locales/en.json +++ b/webapp/src/locales/en.json @@ -582,7 +582,9 @@ "Name": "Name", "Size": "Size", "Action": "Action", - "Cancel": "@:base.Cancel" + "Cancel": "@:base.Cancel", + "InvalidJson": "JSON file is formatted incorrectly.", + "InvalidJsonContent": "JSON file has the wrong content." }, "login": { "Login": "Login", diff --git a/webapp/src/locales/fr.json b/webapp/src/locales/fr.json index 37c405d5..e15c72c9 100644 --- a/webapp/src/locales/fr.json +++ b/webapp/src/locales/fr.json @@ -564,7 +564,9 @@ "Name": "Name", "Size": "Size", "Action": "Action", - "Cancel": "@:base.Cancel" + "Cancel": "@:base.Cancel", + "InvalidJson": "JSON file is formatted incorrectly.", + "InvalidJsonContent": "JSON file has the wrong content." }, "login": { "Login": "Connexion", diff --git a/webapp/src/utils/structure.ts b/webapp/src/utils/structure.ts new file mode 100644 index 00000000..d7e29142 --- /dev/null +++ b/webapp/src/utils/structure.ts @@ -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; +} diff --git a/webapp/src/views/ConfigAdminView.vue b/webapp/src/views/ConfigAdminView.vue index 08ed31d8..c88fcf59 100644 --- a/webapp/src/views/ConfigAdminView.vue +++ b/webapp/src/views/ConfigAdminView.vue @@ -57,17 +57,23 @@
- +
- +
-
@@ -132,6 +138,8 @@ import ModalDialog from '@/components/ModalDialog.vue'; import type { AlertResponse } from '@/types/AlertResponse'; import type { FileInfo } from '@/types/File'; import { authHeader, handleResponse } from '@/utils/authentication'; +import type { Schema } from '@/utils/structure'; +import { hasStructure } from '@/utils/structure'; import { waitRestart } from '@/utils/waitRestart'; import * as bootstrap from 'bootstrap'; import { @@ -169,6 +177,24 @@ export default defineComponent({ UploadError: '', UploadSuccess: false, 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() { @@ -230,6 +256,43 @@ export default defineComponent({ this.callFileApiEndpoint('delete', JSON.stringify({ file: this.selectedFile.name })); 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() { this.uploading = true; const formData = new FormData();