fix/correctif-sec #12

Merged
kevin merged 3 commits from fix/correctif-sec into develop 2026-03-13 08:48:27 +00:00
13 changed files with 116 additions and 42 deletions
Showing only changes of commit 47bc8ba966 - Show all commits
+7 -3
View File
@@ -55,6 +55,7 @@
import {Icon as IconifyIcon} from "@iconify/vue" import {Icon as IconifyIcon} from "@iconify/vue"
import CircleSkeleton from "~/components/skeleton/CircleSkeleton.vue" import CircleSkeleton from "~/components/skeleton/CircleSkeleton.vue"
import TextSkeleton from "~/components/skeleton/TextSkeleton.vue" import TextSkeleton from "~/components/skeleton/TextSkeleton.vue"
import { downloadApiFile, useApiAuthHeader } from "~/composables/useApiAuth"
const props = defineProps<{ const props = defineProps<{
folder: string | null folder: string | null
@@ -62,15 +63,16 @@ const props = defineProps<{
const backups = ref<string[]>([]) const backups = ref<string[]>([])
const loading = ref(false) const loading = ref(false)
const apiAuthHeader = useApiAuthHeader()
const title = computed(() => { const title = computed(() => {
if (!props.folder) return "Fichiers" if (!props.folder) return "Fichiers"
return `Backup — ${props.folder.toUpperCase()}` return `Backup — ${props.folder.toUpperCase()}`
}) })
const downloadBackup = (file: string) => { const downloadBackup = async (file: string) => {
if (!props.folder) return if (!props.folder) return
const url = `/api/download?folder=${encodeURIComponent(props.folder)}&file=${encodeURIComponent(file)}` const url = `/api/download?folder=${encodeURIComponent(props.folder)}&file=${encodeURIComponent(file)}`
window.location.href = url await downloadApiFile(url, file)
} }
watch(() => props.folder, async (folder) => { watch(() => props.folder, async (folder) => {
@@ -82,7 +84,9 @@ watch(() => props.folder, async (folder) => {
loading.value = true loading.value = true
try { try {
const data = await $fetch<string[]>(`/api/backups?folder=${encodeURIComponent(folder)}`) const data = await $fetch<string[]>(`/api/backups?folder=${encodeURIComponent(folder)}`, {
headers: apiAuthHeader
})
backups.value = data backups.value = data
} catch (error) { } catch (error) {
console.error("Erreur récupération backups:", error) console.error("Erreur récupération backups:", error)
+7 -2
View File
@@ -79,6 +79,7 @@
<script setup lang="ts"> <script setup lang="ts">
import { computed, onMounted, ref } from "vue" import { computed, onMounted, ref } from "vue"
import { Icon as IconifyIcon } from "@iconify/vue" import { Icon as IconifyIcon } from "@iconify/vue"
import { useApiAuthHeader } from "~/composables/useApiAuth"
type BackupScript = { type BackupScript = {
key: string key: string
@@ -118,6 +119,7 @@ const scripts = ref<BackupScript[]>([])
const output = ref<string>("") const output = ref<string>("")
const message = ref<string>("") const message = ref<string>("")
const isError = ref(false) const isError = ref(false)
const apiAuthHeader = useApiAuthHeader()
const statusClass = computed(() => (isError.value ? "status-error" : "status-success")) const statusClass = computed(() => (isError.value ? "status-error" : "status-success"))
@@ -134,7 +136,9 @@ const loadScripts = async () => {
downloadFolders: [] downloadFolders: []
}) })
try { try {
const data = await $fetch<BackupScriptListResponse>("/api/backup-script") const data = await $fetch<BackupScriptListResponse>("/api/backup-script", {
headers: apiAuthHeader
})
scripts.value = data.scripts scripts.value = data.scripts
} catch (error) { } catch (error) {
scripts.value = [] scripts.value = []
@@ -162,7 +166,8 @@ const runScript = async (key: string) => {
try { try {
const data = await $fetch<BackupScriptRunResponse>("/api/backup-script", { const data = await $fetch<BackupScriptRunResponse>("/api/backup-script", {
method: "POST", method: "POST",
body: { key } body: { key },
headers: apiAuthHeader
Outdated
Review

tu peux créer un fetch global avec apiAuthHeader intégrré automatiquement pour tes deux type de fetch

tu peux créer un fetch global avec apiAuthHeader intégrré automatiquement pour tes deux type de fetch
}) })
message.value = `${data.label} execute avec succes` message.value = `${data.label} execute avec succes`
output.value = data.output || "Aucune sortie retournee." output.value = data.output || "Aucune sortie retournee."
+5 -1
View File
@@ -1,6 +1,10 @@
<script setup> <script setup>
import {Icon as IconifyIcon} from "@iconify/vue" import {Icon as IconifyIcon} from "@iconify/vue"
const { data: messages } = await useFetch('/api/discord/messages') import { useApiAuthHeader } from "~/composables/useApiAuth"
const { data: messages } = await useFetch('/api/discord/messages', {
headers: useApiAuthHeader()
})
</script> </script>
<template> <template>
+6 -2
View File
@@ -42,11 +42,13 @@
<script setup lang="ts"> <script setup lang="ts">
import {computed, ref} from "vue" import {computed, ref} from "vue"
import {Icon as IconifyIcon} from "@iconify/vue" import {Icon as IconifyIcon} from "@iconify/vue"
import { useApiAuthHeader, withApiAuth } from "~/composables/useApiAuth"
const ping = ref<number | null>(null) const ping = ref<number | null>(null)
const download = ref<number | null>(null) const download = ref<number | null>(null)
const upload = ref<number | null>(null) const upload = ref<number | null>(null)
const isTesting = ref(false) const isTesting = ref(false)
const apiAuthHeader = useApiAuthHeader()
const metrics = computed(() => [ const metrics = computed(() => [
{ label: "Download", icon: "mdi:arrow-down-bold", value: download.value, unit: "Mbps" }, { label: "Download", icon: "mdi:arrow-down-bold", value: download.value, unit: "Mbps" },
@@ -56,7 +58,9 @@ const metrics = computed(() => [
async function testDownload() { async function testDownload() {
const start = performance.now() const start = performance.now()
const res = await fetch('/api/download') const res = await fetch('/api/download', {
headers: apiAuthHeader
})
const blob = await res.blob() const blob = await res.blob()
const end = performance.now() const end = performance.now()
const size = blob.size const size = blob.size
@@ -68,7 +72,7 @@ async function testUpload() {
const size = 5 * 1024 * 1024 const size = 5 * 1024 * 1024
const data = new Uint8Array(size) const data = new Uint8Array(size)
const start = performance.now() const start = performance.now()
await fetch('/api/upload', { method: 'POST', body: data }) await fetch('/api/upload', withApiAuth({ method: 'POST', body: data }))
const end = performance.now() const end = performance.now()
const seconds = (end - start) / 1000 const seconds = (end - start) / 1000
upload.value = Math.round((size * 8) / seconds / 1000000) upload.value = Math.round((size * 8) / seconds / 1000000)
1
+5 -1
View File
@@ -43,6 +43,7 @@
import CircleSkeleton from "~/components/skeleton/CircleSkeleton.vue" import CircleSkeleton from "~/components/skeleton/CircleSkeleton.vue"
import TextSkeleton from "~/components/skeleton/TextSkeleton.vue" import TextSkeleton from "~/components/skeleton/TextSkeleton.vue"
import {onBeforeUnmount, onMounted, ref} from "vue" import {onBeforeUnmount, onMounted, ref} from "vue"
import { useApiAuthHeader } from "~/composables/useApiAuth"
interface StatusRow { interface StatusRow {
label: string label: string
@@ -71,6 +72,7 @@ const props = withDefaults(
const rows = ref<StatusRow[]>([]) const rows = ref<StatusRow[]>([])
const loading = ref(true) const loading = ref(true)
const initialized = ref(false) const initialized = ref(false)
const apiAuthHeader = useApiAuthHeader()
let timer: ReturnType<typeof setInterval> | null = null let timer: ReturnType<typeof setInterval> | null = null
const statusLabel = (status: number) => { const statusLabel = (status: number) => {
@@ -84,7 +86,9 @@ const checkStatus = async () => {
loading.value = true loading.value = true
} }
try { try {
const data = await $fetch<StatusResponse>(props.endpoint) const data = await $fetch<StatusResponse>(props.endpoint, {
headers: apiAuthHeader
})
rows.value = data.results rows.value = data.results
} catch (error) { } catch (error) {
rows.value = [ rows.value = [
+2
View File
@@ -35,7 +35,9 @@ export default defineNuxtConfig({
} }
}, },
runtimeConfig: { runtimeConfig: {
apiSecretKey: process.env.API_SECRET_KEY,
public: { public: {
apiSecretKey: process.env.API_SECRET_KEY || "",
Outdated
Review

runtimeConfig: {
apiSecretKey: process.env.API_SECRET_KEY,
public: {
apiSecretKey: process.env.API_SECRET_KEY || "",

PK il y est deux fois

runtimeConfig: { apiSecretKey: process.env.API_SECRET_KEY, public: { apiSecretKey: process.env.API_SECRET_KEY || "", PK il y est deux fois
appVersion: getRepoVersion() appVersion: getRepoVersion()
} }
}, },
+13 -17
View File
@@ -96,6 +96,7 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref } from "vue" import { ref } from "vue"
import BackupRun from "~/components/BackupRun.vue" import BackupRun from "~/components/BackupRun.vue"
import { downloadApiFile, useApiAuthHeader } from "~/composables/useApiAuth"
definePageMeta({ layout: false }) definePageMeta({ layout: false })
@@ -117,35 +118,30 @@ const emptyScriptResult = (): ScriptResult => ({
const selectedBackup = ref<string | null>(null) const selectedBackup = ref<string | null>(null)
const scriptResult = ref<ScriptResult>(emptyScriptResult()) const scriptResult = ref<ScriptResult>(emptyScriptResult())
const apiAuthHeader = useApiAuthHeader()
const fetchLatestBackup = async (folder: string) => { const fetchLatestBackup = async (folder: string) => {
const files = await $fetch<string[]>(`/api/backups?folder=${encodeURIComponent(folder)}`) const files = await $fetch<string[]>(`/api/backups?folder=${encodeURIComponent(folder)}`, {
headers: apiAuthHeader
Outdated
Review

ici fetch global et les autres endroit

ici fetch global et les autres endroit
})
return files[0] || null return files[0] || null
} }
const triggerDownload = (folder: string, file: string) => { const triggerDownload = async (folder: string, file: string) => {
const link = document.createElement("a") const url = `/api/download?folder=${encodeURIComponent(folder)}&file=${encodeURIComponent(file)}`
link.href = `/api/download?folder=${encodeURIComponent(folder)}&file=${encodeURIComponent(file)}` await downloadApiFile(url, file)
link.style.display = "none"
document.body.appendChild(link)
link.click()
link.remove()
} }
const triggerBatchDownload = (folders: string[]) => { const triggerBatchDownload = async (folders: string[]) => {
const link = document.createElement("a") const url = `/api/download-latest?folders=${encodeURIComponent(folders.join(","))}`
link.href = `/api/download-latest?folders=${encodeURIComponent(folders.join(","))}` await downloadApiFile(url, "backup-latest.tar.gz")
link.style.display = "none"
document.body.appendChild(link)
link.click()
link.remove()
} }
const downloadLatestBackup = async (folder: string) => { const downloadLatestBackup = async (folder: string) => {
const latestFile = await fetchLatestBackup(folder) const latestFile = await fetchLatestBackup(folder)
if (latestFile) { if (latestFile) {
triggerDownload(folder, latestFile) await triggerDownload(folder, latestFile)
} }
} }
@@ -157,7 +153,7 @@ const handleScriptResult = async (payload: ScriptResult) => {
} }
if (payload.downloadFolders.length > 1) { if (payload.downloadFolders.length > 1) {
triggerBatchDownload(payload.downloadFolders) await triggerBatchDownload(payload.downloadFolders)
return return
} }
+5 -1
View File
@@ -49,6 +49,7 @@
<script setup lang="ts"> <script setup lang="ts">
definePageMeta({layout: false}) definePageMeta({layout: false})
import {computed, onMounted, ref} from "vue" import {computed, onMounted, ref} from "vue"
import { useApiAuthHeader } from "~/composables/useApiAuth"
type DiskSourceResult = { type DiskSourceResult = {
key: string key: string
@@ -77,6 +78,7 @@ type DiagramItem = {
const selectedBackup = ref<string | null>(null) const selectedBackup = ref<string | null>(null)
const rawResults = ref<DiskSourceResult[]>([]) const rawResults = ref<DiskSourceResult[]>([])
const loading = ref(false) const loading = ref(false)
const apiAuthHeader = useApiAuthHeader()
const chartRadius = 52 const chartRadius = 52
const chartCircumference = 2 * Math.PI * chartRadius const chartCircumference = 2 * Math.PI * chartRadius
@@ -151,7 +153,9 @@ const runScript = async () => {
rawResults.value = [] rawResults.value = []
try { try {
const output = await $fetch<DiskApiResponse>("/api/disk") const output = await $fetch<DiskApiResponse>("/api/disk", {
headers: apiAuthHeader
})
rawResults.value = output.results rawResults.value = output.results
} catch (error) { } catch (error) {
const message = `Erreur: ${error instanceof Error ? error.message : String(error)}` const message = `Erreur: ${error instanceof Error ? error.message : String(error)}`
+5 -4
View File
@@ -1,4 +1,4 @@
import { exec } from "node:child_process" import { execFile } from "node:child_process"
import scripts from "../config/backup-script.json" import scripts from "../config/backup-script.json"
type BackupScript = { type BackupScript = {
@@ -6,11 +6,12 @@ type BackupScript = {
label: string label: string
downloadFolders?: string[] downloadFolders?: string[]
command: string command: string
args?: string[]
} }
function runCommand(command: string): Promise<string> { function runCommand(command: string, args: string[] = []): Promise<string> {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
exec(command, { timeout: 10 * 60 * 1000 }, (error, stdout, stderr) => { execFile(command, args, { timeout: 10 * 60 * 1000 }, (error, stdout, stderr) => {
if (error) { if (error) {
reject(stderr || error.message) reject(stderr || error.message)
return return
@@ -40,7 +41,7 @@ export default defineEventHandler(async (event) => {
} }
try { try {
const output = await runCommand(script.command) const output = await runCommand(script.command, script.args || [])
return { return {
ok: true, ok: true,
key: script.key, key: script.key,
+5 -4
View File
@@ -1,10 +1,11 @@
import { exec } from "child_process" import { execFile } from "child_process"
import diskSources from "../config/disk-commands.json" import diskSources from "../config/disk-commands.json"
type DiskSource = { type DiskSource = {
key: string key: string
label: string label: string
command: string command: string
args?: string[]
} }
function getCommand(source: DiskSource) { function getCommand(source: DiskSource) {
@@ -15,9 +16,9 @@ function getCommand(source: DiskSource) {
return process.env[envKey] || (legacyEnvKey ? process.env[legacyEnvKey] : undefined) || source.command return process.env[envKey] || (legacyEnvKey ? process.env[legacyEnvKey] : undefined) || source.command
} }
function runCommand(command: string): Promise<string> { function runCommand(command: string, args: string[] = []): Promise<string> {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
exec(command, (error, stdout, stderr) => { execFile(command, args, (error, stdout, stderr) => {
if (error) { if (error) {
reject(stderr || error.message) reject(stderr || error.message)
return return
@@ -31,7 +32,7 @@ export default defineEventHandler(async () => {
const results = await Promise.all( const results = await Promise.all(
(diskSources as DiskSource[]).map(async (source) => { (diskSources as DiskSource[]).map(async (source) => {
try { try {
const output = await runCommand(getCommand(source)) const output = await runCommand(source.command, source.args || [])
return { return {
key: source.key, key: source.key,
label: source.label, label: source.label,
+16 -4
View File
@@ -4,19 +4,31 @@
"label": "Backup BDD recette", "label": "Backup BDD recette",
"icon": "mdi:database-export", "icon": "mdi:database-export",
"downloadFolders": ["ferme", "inventory", "sirh", "user"], "downloadFolders": ["ferme", "inventory", "sirh", "user"],
"command": "ssh ferme 'cd /home/malio/Malio-ops/RecetteScripts && bash backup-bdd-recette.sh && exit'" "command": "ssh",
"args": [
"ferme",
"cd /home/malio/Malio-ops/RecetteScripts && bash backup-bdd-recette.sh"
]
}, },
{ {
"key": "check-statut-recette", "key": "check-statut-recette",
"label": "Check statut recette", "label": "Check statut recette",
"icon": "mdi:server-network", "icon": "mdi:server-network",
"command": "ssh ferme 'cd /home/malio/Malio-ops/RecetteScripts && bash check-statut-recette.sh && exit'" "command": "ssh",
"args": [
"ferme",
"cd /home/malio/Malio-ops/RecetteScripts && bash check-statut-recette.sh"
]
}, },
{ {
"key": "backup-vaultwarden", "key": "backup-vaultwarden",
"label": "Backup vaultwarden", "label": "Backup vaultwarden",
"icon": "mdi:data", "icon": "mdi:data",
"downloadFolders": ["bitwarden"], "downloadFolders": ["bitwarden"],
"command": "ssh bitwarden 'cd /home/matt/vaultwarden/Malio-ops/BackupVaultWarden && bash backup-vaultwarden.sh && exit'" "command": "ssh",
"args": [
"bitwarden",
"cd /home/matt/vaultwarden/Malio-ops/BackupVaultWarden && bash backup-vaultwarden.sh"
]
} }
] ]
+10 -3
View File
@@ -2,11 +2,18 @@
{ {
"key": "remote", "key": "remote",
"label": "Serveur distant", "label": "Serveur distant",
"command": "ssh malio-b 'cd /home/malio-b/Malio-ops/CheckStorage && bash check-storage.sh && exit'" "command": "ssh",
"args": [
"malio-b",
"cd /home/malio-b/Malio-ops/CheckStorage && bash check-storage.sh"
]
}, },
{ {
"key": "local", "key": "local",
"label": "Machine locale", "label": "Machine locale",
"command": "bash /home/kevin/check_storage.sh" "command": "bash",
"args": [
"/home/kevin/check_storage.sh"
]
} }
] ]
+30
View File
@@ -0,0 +1,30 @@
export default defineEventHandler((event) => {
const path = event.path || event.node.req.url || ""
// Le middleware ne s'applique qu'aux routes API, sauf l'endpoint de ping
// qui reste public pour les tests de connectivite.
if (!path.startsWith("/api/") || path === "/api/ping") {
return
}
const runtimeConfig = useRuntimeConfig(event)
const authorization = getHeader(event, "authorization")
const expectedToken = runtimeConfig.apiSecretKey
// Si aucun secret n'est configure cote serveur, on refuse la requete.
if (!expectedToken) {
throw createError({
statusCode: 401,
statusMessage: "Unauthorized"
})
}
// Le header doit correspondre exactement au format attendu :
// Authorization: Bearer <token>
if (authorization !== `Bearer ${expectedToken}`) {
throw createError({
statusCode: 401,
statusMessage: "Unauthorized"
})
}
})