Merge pull request 'fix : securite regex et message erreur et endpoint' (#13) from feat/add-module-lint into develop
All checks were successful
Release / release (push) Successful in 30s

Reviewed-on: #13
Reviewed-by: THOLOT DECHENE Matthieu <matthieu@yuno.malio.fr>
This commit was merged in pull request #13.
This commit is contained in:
2026-03-13 09:19:02 +00:00
13 changed files with 2489 additions and 763 deletions

8
.env.example Normal file
View File

@@ -0,0 +1,8 @@
API_SECRET_KEY=
DISCORD_BOT_TOKEN=
DISCORD_CHANNEL_ID=
BACKUPS_REMOTE_HOST=
BACKUPS_REMOTE_ROOT=
BACKUPS_MAX_FILES=
DISK_COMMAND_REMOTE=
DISK_COMMAND_LOCAL=

View File

@@ -80,6 +80,7 @@
import { computed, onMounted, ref } from "vue"
import { Icon as IconifyIcon } from "@iconify/vue"
import { apiFetch } from "~/composables/useApiAuth"
import { useApiAuthHeader } from "~/composables/useApiAuth"
type BackupScript = {
key: string
@@ -119,6 +120,7 @@ const scripts = ref<BackupScript[]>([])
const output = ref<string>("")
const message = ref<string>("")
const isError = ref(false)
const apiAuthHeader = useApiAuthHeader()
const statusClass = computed(() => (isError.value ? "status-error" : "status-success"))
@@ -137,7 +139,7 @@ const loadScripts = async () => {
try {
const data = await apiFetch<BackupScriptListResponse>("/api/backup-script")
scripts.value = data.scripts
} catch (error) {
} catch {
scripts.value = []
isError.value = true
message.value = "Erreur lors de l'opération"
@@ -174,9 +176,21 @@ const runScript = async (key: string) => {
isError: false,
downloadFolders: data.downloadFolders || []
})
} catch (error: any) {
} catch (error: unknown) {
isError.value = true
message.value = error?.data?.statusMessage || "Erreur lors de l'opération"
const statusMessage =
typeof error === "object" &&
error !== null &&
"data" in error &&
typeof error.data === "object" &&
error.data !== null &&
"statusMessage" in error.data &&
typeof error.data.statusMessage === "string"
? error.data.statusMessage
: null
message.value = statusMessage || "Erreur lors de l'opération"
message.value = error?.data?.statusMessage || "Erreur execution script"
output.value = ""
emit("result", {
key,

View File

@@ -4,8 +4,8 @@
<h2 class="card-title">Speedtest</h2>
<button
class="reload-btn"
@click="runTests"
:disabled="isTesting"
@click="runTests"
>
<IconifyIcon
icon="mdi:reload"

View File

@@ -20,8 +20,8 @@
</template>
<div
v-else
v-for="row in rows"
v-else
:key="`${row.label}-${row.url}`"
class="status-row"
:class="row.status === 200 ? 'row-ok' : 'row-error'"

View File

@@ -32,14 +32,18 @@ function getDownloadFileName(contentDisposition: string | null, fallback: string
return fallback
}
export function withApiAuth(init: RequestInit = {}) {
// Les appels frontend reutilisent les cookies httpOnly poses cote serveur.
return {
...init,
headers: {
...toHeadersObject(init.headers)
export function useApiAuthHeader() {
const runtimeConfig = useRuntimeConfig()
const token = runtimeConfig.public.apiSecretKey
if (!token) {
return {}
}
// Tous les appels frontend vers /api/* reutilisent ce header commun.
return {
Authorization: `Bearer ${token}`
}
}
}
export const apiFetch = $fetch.create({})
@@ -73,3 +77,14 @@ export async function downloadApiFile(url: string, fileNameFallback: string) {
link.remove()
URL.revokeObjectURL(objectUrl)
}
export function withApiAuth(init: RequestInit = {}) {
// Fusionne le header d'auth avec d'eventuels headers deja fournis.
return {
...init,
headers: {
...useApiAuthHeader(),
...toHeadersObject(init.headers)
}
}
}

3
eslint.config.mjs Normal file
View File

@@ -0,0 +1,3 @@
import createConfigForNuxt from "@nuxt/eslint-config"
export default createConfigForNuxt()

View File

@@ -7,7 +7,7 @@
:src="logoSrc"
alt="Logo Malio"
class="logo"
/>
>
</div>
<div class="brand-copy">
<p class="brand-title">Supervisor</p>
@@ -67,7 +67,7 @@
:src="logoSrc"
alt="Logo Malio"
class="logo"
/>
>
</div>
<div class="brand-copy">
<p class="brand-kicker">Control Center</p>

3138
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -7,14 +7,14 @@
"dev": "nuxt dev",
"generate": "nuxt generate",
"preview": "nuxt preview",
"postinstall": "nuxt prepare"
"postinstall": "nuxt prepare",
"lint": "eslint .",
"lint:fix": "eslint . --fix"
},
"dependencies": {
"@iconify/vue": "^5.0.0",
"iconify": "^1.4.0",
"nuxt": "^4.3.1",
"vue": "^3.5.29",
"vue-router": "^4.6.4"
"@nuxt/eslint": "^1.15.2",
"nuxt": "^4.3.1"
},
"devDependencies": {
"@semantic-release/changelog": "^6.0.3",

View File

@@ -33,7 +33,7 @@
</div>
<div class="grid-middle">
<Speedtest class="animate-fade-in-up speedtest-card-mobile" style="animation-delay: 150ms" />
<SpeedTest class="animate-fade-in-up speedtest-card-mobile" style="animation-delay: 150ms" />
</div>
</div>
</div>
@@ -47,8 +47,8 @@
</template>
<script setup lang="ts">
definePageMeta({layout: false})
import {computed, onMounted, ref} from "vue"
definePageMeta({layout: false})
import { apiFetch } from "~/composables/useApiAuth"
type DiskSourceResult = {
@@ -75,7 +75,6 @@ type DiagramItem = {
totalText: string
}
const selectedBackup = ref<string | null>(null)
const rawResults = ref<DiskSourceResult[]>([])
const loading = ref(false)
const chartRadius = 52

View File

@@ -1,9 +1,9 @@
import { execFile } from "node:child_process"
import folderMap from "../config/backup-folders.json"
const REMOTE_HOST = process.env.BACKUPS_REMOTE_HOST || "malio-b"
const REMOTE_ROOT = process.env.BACKUPS_REMOTE_ROOT || "/home/malio-b/backups"
const MAX_FILES_PER_FOLDER = Number(process.env.BACKUPS_MAX_FILES || "200")
const REMOTE_HOST = process.env.BACKUPS_REMOTE_HOST
const REMOTE_ROOT = process.env.BACKUPS_REMOTE_ROOT
const MAX_FILES_PER_FOLDER = Number(process.env.BACKUPS_MAX_FILES)
const isSafeFolder = (value: string) => /^[a-zA-Z0-9._-]+$/.test(value)
const shellQuote = (value: string) => `'${value.replace(/'/g, `'\\''`)}'`
const FOLDER_MAP = folderMap as Record<string, string>

View File

@@ -1,4 +1,4 @@
import { execFile } from "child_process"
import { exec, execFile } from "child_process"
import diskSources from "../config/disk-commands.json"
type DiskSource = {
@@ -8,12 +8,12 @@ type DiskSource = {
args?: string[]
}
function getCommand(source: DiskSource) {
function getEnvCommand(source: DiskSource) {
const envKey = `DISK_COMMAND_${source.key.toUpperCase()}`
const legacyEnvKey =
source.key === "remote" ? "DISK_REMOTE_COMMAND" : source.key === "local" ? "DISK_LOCAL_COMMAND" : ""
return process.env[envKey] || (legacyEnvKey ? process.env[legacyEnvKey] : undefined) || source.command
return process.env[envKey] || (legacyEnvKey ? process.env[legacyEnvKey] : undefined) || null
}
function runCommand(command: string, args: string[] = []): Promise<string> {
@@ -28,11 +28,26 @@ function runCommand(command: string, args: string[] = []): Promise<string> {
})
}
function runShellCommand(command: string): Promise<string> {
return new Promise((resolve, reject) => {
exec(command, (error, stdout, stderr) => {
if (error) {
reject(stderr || error.message)
return
}
resolve(stdout)
})
})
}
export default defineEventHandler(async () => {
const results = await Promise.all(
(diskSources as DiskSource[]).map(async (source) => {
try {
const output = await runCommand(source.command, source.args || [])
const envCommand = getEnvCommand(source)
const output = envCommand
? await runShellCommand(envCommand)
: await runCommand(source.command, source.args || [])
return {
key: source.key,
label: source.label,

View File

@@ -56,7 +56,7 @@ function buildContentDisposition(fileName: string) {
return `attachment; filename="${asciiName}"; filename*=UTF-8''${encodeURIComponent(fileName)}`
}
function speedtestStream(event: any) {
function speedtestStream(event: H3Event) {
const size = 128 * 1024 * 1024
let sent = 0