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

View File

@@ -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<string[]>([])
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<string[]>(`/api/backups?folder=${encodeURIComponent(folder)}`)
const data = await $fetch<string[]>(`/api/backups?folder=${encodeURIComponent(folder)}`, {
headers: apiAuthHeader
})
backups.value = data
} catch (error) {
console.error("Erreur récupération backups:", error)

View File

@@ -79,6 +79,7 @@
<script setup lang="ts">
import { computed, onMounted, ref } from "vue"
import { Icon as IconifyIcon } from "@iconify/vue"
import { useApiAuthHeader } from "~/composables/useApiAuth"
type BackupScript = {
key: string
@@ -118,6 +119,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"))
@@ -134,7 +136,9 @@ const loadScripts = async () => {
downloadFolders: []
})
try {
const data = await $fetch<BackupScriptListResponse>("/api/backup-script")
const data = await $fetch<BackupScriptListResponse>("/api/backup-script", {
headers: apiAuthHeader
})
scripts.value = data.scripts
} catch (error) {
scripts.value = []
@@ -162,7 +166,8 @@ const runScript = async (key: string) => {
try {
const data = await $fetch<BackupScriptRunResponse>("/api/backup-script", {
method: "POST",
body: { key }
body: { key },
headers: apiAuthHeader

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`
output.value = data.output || "Aucune sortie retournee."

View File

@@ -1,6 +1,10 @@
<script setup>
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>
<template>

View File

@@ -42,11 +42,13 @@
<script setup lang="ts">
import {computed, ref} from "vue"
import {Icon as IconifyIcon} from "@iconify/vue"
import { useApiAuthHeader, withApiAuth } from "~/composables/useApiAuth"
const ping = ref<number | null>(null)
const download = ref<number | null>(null)
const upload = ref<number | null>(null)
const isTesting = ref(false)
const apiAuthHeader = useApiAuthHeader()
const metrics = computed(() => [
{ label: "Download", icon: "mdi:arrow-down-bold", value: download.value, unit: "Mbps" },
@@ -56,7 +58,9 @@ const metrics = computed(() => [
async function testDownload() {
const start = performance.now()
const res = await fetch('/api/download')
const res = await fetch('/api/download', {
headers: apiAuthHeader
})
const blob = await res.blob()
const end = performance.now()
const size = blob.size
@@ -68,7 +72,7 @@ async function testUpload() {
const size = 5 * 1024 * 1024
const data = new Uint8Array(size)
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 seconds = (end - start) / 1000
upload.value = Math.round((size * 8) / seconds / 1000000)

View File

@@ -43,6 +43,7 @@
import CircleSkeleton from "~/components/skeleton/CircleSkeleton.vue"
import TextSkeleton from "~/components/skeleton/TextSkeleton.vue"
import {onBeforeUnmount, onMounted, ref} from "vue"
import { useApiAuthHeader } from "~/composables/useApiAuth"
interface StatusRow {
label: string
@@ -71,6 +72,7 @@ const props = withDefaults(
const rows = ref<StatusRow[]>([])
const loading = ref(true)
const initialized = ref(false)
const apiAuthHeader = useApiAuthHeader()
let timer: ReturnType<typeof setInterval> | null = null
const statusLabel = (status: number) => {
@@ -84,7 +86,9 @@ const checkStatus = async () => {
loading.value = true
}
try {
const data = await $fetch<StatusResponse>(props.endpoint)
const data = await $fetch<StatusResponse>(props.endpoint, {
headers: apiAuthHeader
})
rows.value = data.results
} catch (error) {
rows.value = [

View File

@@ -35,7 +35,9 @@ export default defineNuxtConfig({
}
},
runtimeConfig: {
apiSecretKey: process.env.API_SECRET_KEY,
public: {
apiSecretKey: process.env.API_SECRET_KEY || "",

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()
}
},

View File

@@ -96,6 +96,7 @@
<script setup lang="ts">
import { ref } from "vue"
import BackupRun from "~/components/BackupRun.vue"
import { downloadApiFile, useApiAuthHeader } from "~/composables/useApiAuth"
definePageMeta({ layout: false })
@@ -117,35 +118,30 @@ const emptyScriptResult = (): ScriptResult => ({
const selectedBackup = ref<string | null>(null)
const scriptResult = ref<ScriptResult>(emptyScriptResult())
const apiAuthHeader = useApiAuthHeader()
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

ici fetch global et les autres endroit

ici fetch global et les autres endroit
})
return files[0] || null
}
const triggerDownload = (folder: string, file: string) => {
const link = document.createElement("a")
link.href = `/api/download?folder=${encodeURIComponent(folder)}&file=${encodeURIComponent(file)}`
link.style.display = "none"
document.body.appendChild(link)
link.click()
link.remove()
const triggerDownload = async (folder: string, file: string) => {
const url = `/api/download?folder=${encodeURIComponent(folder)}&file=${encodeURIComponent(file)}`
await downloadApiFile(url, file)
}
const triggerBatchDownload = (folders: string[]) => {
const link = document.createElement("a")
link.href = `/api/download-latest?folders=${encodeURIComponent(folders.join(","))}`
link.style.display = "none"
document.body.appendChild(link)
link.click()
link.remove()
const triggerBatchDownload = async (folders: string[]) => {
const url = `/api/download-latest?folders=${encodeURIComponent(folders.join(","))}`
await downloadApiFile(url, "backup-latest.tar.gz")
}
const downloadLatestBackup = async (folder: string) => {
const latestFile = await fetchLatestBackup(folder)
if (latestFile) {
triggerDownload(folder, latestFile)
await triggerDownload(folder, latestFile)
}
}
@@ -157,7 +153,7 @@ const handleScriptResult = async (payload: ScriptResult) => {
}
if (payload.downloadFolders.length > 1) {
triggerBatchDownload(payload.downloadFolders)
await triggerBatchDownload(payload.downloadFolders)
return
}

View File

@@ -49,6 +49,7 @@
<script setup lang="ts">
definePageMeta({layout: false})
import {computed, onMounted, ref} from "vue"
import { useApiAuthHeader } from "~/composables/useApiAuth"
type DiskSourceResult = {
key: string
@@ -77,6 +78,7 @@ type DiagramItem = {
const selectedBackup = ref<string | null>(null)
const rawResults = ref<DiskSourceResult[]>([])
const loading = ref(false)
const apiAuthHeader = useApiAuthHeader()
const chartRadius = 52
const chartCircumference = 2 * Math.PI * chartRadius
@@ -151,7 +153,9 @@ const runScript = async () => {
rawResults.value = []
try {
const output = await $fetch<DiskApiResponse>("/api/disk")
const output = await $fetch<DiskApiResponse>("/api/disk", {
headers: apiAuthHeader
})
rawResults.value = output.results
} catch (error) {
const message = `Erreur: ${error instanceof Error ? error.message : String(error)}`

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"
type BackupScript = {
@@ -6,11 +6,12 @@ type BackupScript = {
label: string
downloadFolders?: string[]
command: string
args?: string[]
}
function runCommand(command: string): Promise<string> {
function runCommand(command: string, args: string[] = []): Promise<string> {
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) {
reject(stderr || error.message)
return
@@ -40,7 +41,7 @@ export default defineEventHandler(async (event) => {
}
try {
const output = await runCommand(script.command)
const output = await runCommand(script.command, script.args || [])
return {
ok: true,
key: script.key,

View File

@@ -1,10 +1,11 @@
import { exec } from "child_process"
import { execFile } from "child_process"
import diskSources from "../config/disk-commands.json"
type DiskSource = {
key: string
label: string
command: string
args?: string[]
}
function getCommand(source: DiskSource) {
@@ -15,9 +16,9 @@ function getCommand(source: DiskSource) {
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) => {
exec(command, (error, stdout, stderr) => {
execFile(command, args, (error, stdout, stderr) => {
if (error) {
reject(stderr || error.message)
return
@@ -31,7 +32,7 @@ export default defineEventHandler(async () => {
const results = await Promise.all(
(diskSources as DiskSource[]).map(async (source) => {
try {
const output = await runCommand(getCommand(source))
const output = await runCommand(source.command, source.args || [])
return {
key: source.key,
label: source.label,

View File

@@ -4,19 +4,31 @@
"label": "Backup BDD recette",
"icon": "mdi:database-export",
"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",
"label": "Check statut recette",
"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",
"label": "Backup vaultwarden",
"icon": "mdi:data",
"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"
]
}
]
]

View File

@@ -2,11 +2,18 @@
{
"key": "remote",
"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",
"label": "Machine locale",
"command": "bash /home/kevin/check_storage.sh"
"command": "bash",
"args": [
"/home/kevin/check_storage.sh"
]
}
]
]

30
server/middleware/auth.ts Normal file
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"
})
}
})