Skip to content

Commit

Permalink
Feature: Validate JSON before uploading
Browse files Browse the repository at this point in the history
  • Loading branch information
tbnobody committed Nov 1, 2024
1 parent 225cab6 commit 8019eaf
Show file tree
Hide file tree
Showing 7 changed files with 111 additions and 13 deletions.
6 changes: 4 additions & 2 deletions lang/es.lang.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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",
Expand Down
6 changes: 4 additions & 2 deletions lang/it.lang.json
Original file line number Diff line number Diff line change
Expand Up @@ -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:",
Expand All @@ -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",
Expand Down
4 changes: 3 additions & 1 deletion webapp/src/locales/de.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
4 changes: 3 additions & 1 deletion webapp/src/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
4 changes: 3 additions & 1 deletion webapp/src/locales/fr.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
25 changes: 25 additions & 0 deletions webapp/src/utils/structure.ts
Original file line number Diff line number Diff line change
@@ -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;
}
75 changes: 69 additions & 6 deletions webapp/src/views/ConfigAdminView.vue
Original file line number Diff line number Diff line change
Expand Up @@ -57,17 +57,23 @@
<div v-else-if="!uploading">
<div class="row g-3 align-items-center form-group pt-2">
<div class="col-sm">
<select class="form-select" v-model="restoreFileSelect">
<option selected value="config.json">Main Config (config.json)</option>
<option selected value="pin_mapping.json">Pin Mapping (pin_mapping.json)</option>
<option selected value="pack.lang.json">Language Pack (pack.lang.json)</option>
<select class="form-select" v-model="restoreFileSelect" @change="onUploadFileChange">
<option v-for="file in restoreList" :key="file.name" :value="file.name">
{{ file.descr }}
</option>
</select>
</div>
<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 class="col-sm">
<button class="btn btn-primary" @click="onUpload">
<button class="btn btn-primary" @click="onUpload" :disabled="!isValidJson">
{{ $t('fileadmin.Restore') }}
</button>
</div>
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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() {
Expand Down Expand Up @@ -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();
Expand Down

0 comments on commit 8019eaf

Please sign in to comment.