fix : securite middle et execfile
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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
|
||||
})
|
||||
message.value = `${data.label} execute avec succes`
|
||||
output.value = data.output || "Aucune sortie retournee."
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 = [
|
||||
|
||||
@@ -35,7 +35,9 @@ export default defineNuxtConfig({
|
||||
}
|
||||
},
|
||||
runtimeConfig: {
|
||||
apiSecretKey: process.env.API_SECRET_KEY,
|
||||
public: {
|
||||
apiSecretKey: process.env.API_SECRET_KEY || "",
|
||||
appVersion: getRepoVersion()
|
||||
}
|
||||
},
|
||||
|
||||
@@ -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
|
||||
})
|
||||
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
|
||||
}
|
||||
|
||||
|
||||
@@ -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)}`
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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"
|
||||
]
|
||||
}
|
||||
]
|
||||
]
|
||||
@@ -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
30
server/middleware/auth.ts
Normal 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"
|
||||
})
|
||||
}
|
||||
})
|
||||
Reference in New Issue
Block a user