From 47bc8ba96684354ff808056bdf97ebd55be7bdd5 Mon Sep 17 00:00:00 2001 From: kevin Date: Thu, 12 Mar 2026 08:37:53 +0100 Subject: [PATCH 1/3] fix : securite middle et execfile --- components/BackupList.vue | 10 +++++++--- components/BackupRun.vue | 9 +++++++-- components/MessageDiscord.vue | 6 +++++- components/Speedtest.vue | 8 ++++++-- components/StatusSite.vue | 6 +++++- nuxt.config.ts | 2 ++ pages/backup.vue | 30 +++++++++++++----------------- pages/index.vue | 6 +++++- server/api/backup-script.post.ts | 9 +++++---- server/api/disk.get.ts | 9 +++++---- server/config/backup-script.json | 20 ++++++++++++++++---- server/config/disk-commands.json | 13 ++++++++++--- server/middleware/auth.ts | 30 ++++++++++++++++++++++++++++++ 13 files changed, 116 insertions(+), 42 deletions(-) create mode 100644 server/middleware/auth.ts diff --git a/components/BackupList.vue b/components/BackupList.vue index 9dcad3b..1aa8a38 100644 --- a/components/BackupList.vue +++ b/components/BackupList.vue @@ -55,6 +55,7 @@ import {Icon as IconifyIcon} from "@iconify/vue" import CircleSkeleton from "~/components/skeleton/CircleSkeleton.vue" import TextSkeleton from "~/components/skeleton/TextSkeleton.vue" +import { downloadApiFile, useApiAuthHeader } from "~/composables/useApiAuth" const props = defineProps<{ folder: string | null @@ -62,15 +63,16 @@ const props = defineProps<{ const backups = ref([]) const loading = ref(false) +const apiAuthHeader = useApiAuthHeader() const title = computed(() => { if (!props.folder) return "Fichiers" return `Backup — ${props.folder.toUpperCase()}` }) -const downloadBackup = (file: string) => { +const downloadBackup = async (file: string) => { if (!props.folder) return const url = `/api/download?folder=${encodeURIComponent(props.folder)}&file=${encodeURIComponent(file)}` - window.location.href = url + await downloadApiFile(url, file) } watch(() => props.folder, async (folder) => { @@ -82,7 +84,9 @@ watch(() => props.folder, async (folder) => { loading.value = true try { - const data = await $fetch(`/api/backups?folder=${encodeURIComponent(folder)}`) + const data = await $fetch(`/api/backups?folder=${encodeURIComponent(folder)}`, { + headers: apiAuthHeader + }) backups.value = data } catch (error) { console.error("Erreur récupération backups:", error) diff --git a/components/BackupRun.vue b/components/BackupRun.vue index cae972c..36b9db2 100644 --- a/components/BackupRun.vue +++ b/components/BackupRun.vue @@ -79,6 +79,7 @@ @@ -48,6 +52,7 @@ const ping = ref(null) const download = ref(null) const upload = ref(null) const isTesting = ref(false) +const errorMessage = ref("") const apiAuthHeader = useApiAuthHeader() const metrics = computed(() => [ @@ -61,6 +66,9 @@ async function testDownload() { const res = await fetch('/api/download', { headers: apiAuthHeader }) + if (!res.ok) { + throw new Error(`HTTP ${res.status}`) + } const blob = await res.blob() const end = performance.now() const size = blob.size @@ -72,7 +80,10 @@ async function testUpload() { const size = 5 * 1024 * 1024 const data = new Uint8Array(size) const start = performance.now() - await fetch('/api/upload', withApiAuth({ method: 'POST', body: data })) + const response = await fetch('/api/upload', withApiAuth({ method: 'POST', body: data })) + if (!response.ok) { + throw new Error(`HTTP ${response.status}`) + } const end = performance.now() const seconds = (end - start) / 1000 upload.value = Math.round((size * 8) / seconds / 1000000) @@ -80,7 +91,10 @@ async function testUpload() { async function testPing() { const start = performance.now() - await fetch('/api/ping') + const response = await fetch('/api/ping') + if (!response.ok) { + throw new Error(`HTTP ${response.status}`) + } const end = performance.now() ping.value = Math.round(end - start) } @@ -90,11 +104,15 @@ async function runTests() { download.value = null upload.value = null ping.value = null + errorMessage.value = "" try { await testDownload() await testUpload() await testPing() + } catch (error) { + console.error("Erreur speedtest:", error) + errorMessage.value = "Erreur lors de l'opération" } finally { isTesting.value = false } @@ -193,4 +211,15 @@ async function runTests() { letter-spacing: 0.1em; color: rgb(var(--m-muted)); } + +.error-text { + margin-top: 0.75rem; + border-radius: 8px; + border: 1px solid rgb(var(--m-error) / 0.12); + background: rgb(var(--m-error) / 0.06); + padding: 0.75rem 0.875rem; + font-family: var(--font-mono); + font-size: 0.75rem; + color: rgb(var(--m-error)); +} diff --git a/composables/useApiAuth.ts b/composables/useApiAuth.ts new file mode 100644 index 0000000..18b3210 --- /dev/null +++ b/composables/useApiAuth.ts @@ -0,0 +1,86 @@ +function toHeadersObject(headers?: HeadersInit): Record { + if (!headers) { + return {} + } + + if (headers instanceof Headers) { + return Object.fromEntries(headers.entries()) + } + + if (Array.isArray(headers)) { + return Object.fromEntries(headers) + } + + return { ...headers } +} + +function getDownloadFileName(contentDisposition: string | null, fallback: string) { + if (!contentDisposition) { + return fallback + } + + const utf8Match = contentDisposition.match(/filename\*=UTF-8''([^;]+)/i) + if (utf8Match?.[1]) { + return decodeURIComponent(utf8Match[1]) + } + + const asciiMatch = contentDisposition.match(/filename="([^"]+)"/i) + if (asciiMatch?.[1]) { + return asciiMatch[1] + } + + return fallback +} + +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 async function downloadApiFile(url: string, fileNameFallback: string) { + // Les telechargements passent aussi par fetch pour pouvoir envoyer + // le header Authorization, contrairement a un simple . + const response = await fetch(url, { + headers: useApiAuthHeader() + }) + + if (!response.ok) { + throw new Error(`HTTP ${response.status}`) + } + + const blob = await response.blob() + const objectUrl = URL.createObjectURL(blob) + const fileName = getDownloadFileName( + response.headers.get("content-disposition"), + fileNameFallback + ) + const link = document.createElement("a") + + link.href = objectUrl + link.download = fileName + link.style.display = "none" + document.body.appendChild(link) + link.click() + 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) + } + } +} diff --git a/nuxt.config.ts b/nuxt.config.ts index 0145bef..897c822 100644 --- a/nuxt.config.ts +++ b/nuxt.config.ts @@ -26,7 +26,7 @@ export default defineNuxtConfig({ head: { link: [ { rel: "preconnect", href: "https://fonts.googleapis.com" }, - { rel: "preconnect", href: "https://fonts.gstatic.com", crossorigin: "" }, + { rel: "preconnect", href: "https://fonts.gstatic.com ", crossorigin: "" }, { rel: "stylesheet", href: "https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;600;700&family=Outfit:wght@300;400;500;600;700;800;900&display=swap" diff --git a/pages/index.vue b/pages/index.vue index 1f6a1ff..ac25caf 100644 --- a/pages/index.vue +++ b/pages/index.vue @@ -158,13 +158,12 @@ const runScript = async () => { }) rawResults.value = output.results } catch (error) { - const message = `Erreur: ${error instanceof Error ? error.message : String(error)}` rawResults.value = [ { key: "error", label: "Source indisponible", ok: false, - output: message + output: "Erreur lors de l'opération" } ] } finally { diff --git a/server/api/backup-script.post.ts b/server/api/backup-script.post.ts index a9eb7b1..a018c74 100644 --- a/server/api/backup-script.post.ts +++ b/server/api/backup-script.post.ts @@ -50,9 +50,11 @@ export default defineEventHandler(async (event) => { output: output.trim() } } catch (error) { + console.error("Erreur execution script:", error) + throw createError({ statusCode: 500, - statusMessage: `Erreur execution script: ${String(error)}` + statusMessage: "Erreur lors de l'opération" }) } }) diff --git a/server/api/backups.get.ts b/server/api/backups.get.ts index f81ecb9..ee2582c 100644 --- a/server/api/backups.get.ts +++ b/server/api/backups.get.ts @@ -31,9 +31,11 @@ function isMissingPathError(error: unknown): boolean { } function toServerError(error: unknown) { + console.error("Erreur backups:", error) + return createError({ statusCode: 500, - statusMessage: `Erreur SSH backups: ${String(error)}` + statusMessage: "Erreur lors de l'opération" }) } diff --git a/server/api/discord/messages.get.ts b/server/api/discord/messages.get.ts index c004834..4e67f88 100644 --- a/server/api/discord/messages.get.ts +++ b/server/api/discord/messages.get.ts @@ -1,15 +1,31 @@ export default defineEventHandler(async () => { - const token = process.env.DISCORD_BOT_TOKEN - const channel = process.env.DISCORD_CHANNEL_ID + const token = process.env.DISCORD_BOT_TOKEN + const channel = process.env.DISCORD_CHANNEL_ID + if (!token || !channel) { + throw createError({ + statusCode: 503, + statusMessage: "Service indisponible" + }) + } + + try { const messages = await $fetch( - `https://discord.com/api/v10/channels/${channel}/messages?limit=20`, - { - headers: { - Authorization: `Bot ${token}` - } + `https://discord.com/api/v10/channels/${channel}/messages?limit=20`, + { + headers: { + Authorization: `Bot ${token}` } + } ) return messages -}) \ No newline at end of file + } catch (error) { + console.error("Erreur Discord messages:", error) + + throw createError({ + statusCode: 500, + statusMessage: "Erreur lors de l'opération" + }) + } +}) diff --git a/server/api/disk.get.ts b/server/api/disk.get.ts index 0c59016..b469220 100644 --- a/server/api/disk.get.ts +++ b/server/api/disk.get.ts @@ -40,11 +40,12 @@ export default defineEventHandler(async () => { output } } catch (error) { + console.error(`Erreur disk source ${source.key}:`, error) return { key: source.key, label: source.label, ok: false, - output: `Erreur: ${String(error)}` + output: "Erreur lors de l'opération" } } }) diff --git a/server/api/download.get.ts b/server/api/download.get.ts index f317d59..7ca0ebd 100644 --- a/server/api/download.get.ts +++ b/server/api/download.get.ts @@ -7,7 +7,7 @@ const REMOTE_ROOT = process.env.BACKUPS_REMOTE_ROOT || "/home/malio-b/backups" const FOLDER_MAP = folderMap as Record const isSafeFolder = (value: string) => /^[a-zA-Z0-9._-]+$/.test(value) -const isSafeFile = (value: string) => /^[^/\\]+$/.test(value) +const isSafeFile = (value: string) => /^[a-zA-Z0-9._-]+$/.test(value) const shellQuote = (value: string) => `'${value.replace(/'/g, `'\\''`)}'` function runSsh(command: string): Promise { -- 2.39.5 From 00dc2daa3d62c6eeaf8f530362bbc70b89e418eb Mon Sep 17 00:00:00 2001 From: kevin Date: Fri, 13 Mar 2026 09:47:09 +0100 Subject: [PATCH 3/3] fix : correctif mr --- components/BackupList.vue | 8 ++----- components/BackupRun.vue | 12 ++++------ components/MessageDiscord.vue | 4 ++-- components/Speedtest.vue | 9 +++---- components/StatusSite.vue | 7 ++---- composables/useApiAuth.ts | 41 ++++++++++++-------------------- nuxt.config.ts | 1 - pages/backup.vue | 7 ++---- pages/index.vue | 7 ++---- server/middleware/auth-cookie.ts | 25 +++++++++++++++++++ server/middleware/auth.ts | 7 +++--- 11 files changed, 61 insertions(+), 67 deletions(-) create mode 100644 server/middleware/auth-cookie.ts diff --git a/components/BackupList.vue b/components/BackupList.vue index cca8806..63a63ea 100644 --- a/components/BackupList.vue +++ b/components/BackupList.vue @@ -62,7 +62,7 @@ import {Icon as IconifyIcon} from "@iconify/vue" import CircleSkeleton from "~/components/skeleton/CircleSkeleton.vue" import TextSkeleton from "~/components/skeleton/TextSkeleton.vue" -import { downloadApiFile, useApiAuthHeader } from "~/composables/useApiAuth" +import { apiFetch, downloadApiFile } from "~/composables/useApiAuth" const props = defineProps<{ folder: string | null @@ -71,7 +71,6 @@ const props = defineProps<{ const backups = ref([]) const loading = ref(false) const errorMessage = ref("") -const apiAuthHeader = useApiAuthHeader() const title = computed(() => { if (!props.folder) return "Fichiers" return `Backup — ${props.folder.toUpperCase()}` @@ -101,10 +100,7 @@ watch(() => props.folder, async (folder) => { loading.value = true errorMessage.value = "" try { - const data = await $fetch(`/api/backups?folder=${encodeURIComponent(folder)}`, { - headers: apiAuthHeader, - server: false - }) + const data = await apiFetch(`/api/backups?folder=${encodeURIComponent(folder)}`) backups.value = data } catch (error) { console.error("Erreur récupération backups:", error) diff --git a/components/BackupRun.vue b/components/BackupRun.vue index 7ea5d5f..77e3c6f 100644 --- a/components/BackupRun.vue +++ b/components/BackupRun.vue @@ -79,7 +79,7 @@ diff --git a/components/Speedtest.vue b/components/Speedtest.vue index cad8942..9c16fbf 100644 --- a/components/Speedtest.vue +++ b/components/Speedtest.vue @@ -46,14 +46,13 @@