fix : securite regex et message erreur et endpoint
This commit is contained in:
@@ -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,12 +136,14 @@ 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) {
|
||||
} catch {
|
||||
scripts.value = []
|
||||
isError.value = true
|
||||
message.value = `Erreur chargement scripts: ${error instanceof Error ? error.message : String(error)}`
|
||||
message.value = "Erreur lors de l'opération"
|
||||
emit("result", {
|
||||
key: null,
|
||||
label: "",
|
||||
@@ -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."
|
||||
@@ -173,8 +178,20 @@ const runScript = async (key: string) => {
|
||||
isError: false,
|
||||
downloadFolders: data.downloadFolders || []
|
||||
})
|
||||
} catch (error: any) {
|
||||
} catch (error: unknown) {
|
||||
isError.value = true
|
||||
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", {
|
||||
|
||||
@@ -4,8 +4,8 @@
|
||||
<h2 class="card-title">Speedtest</h2>
|
||||
<button
|
||||
class="reload-btn"
|
||||
@click="runTests"
|
||||
:disabled="isTesting"
|
||||
@click="runTests"
|
||||
>
|
||||
<IconifyIcon
|
||||
icon="mdi:reload"
|
||||
@@ -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'"
|
||||
|
||||
86
composables/useApiAuth.ts
Normal file
86
composables/useApiAuth.ts
Normal file
@@ -0,0 +1,86 @@
|
||||
function toHeadersObject(headers?: HeadersInit): Record<string, string> {
|
||||
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 <a href>.
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
3
eslint.config.mjs
Normal file
3
eslint.config.mjs
Normal file
@@ -0,0 +1,3 @@
|
||||
import createConfigForNuxt from "@nuxt/eslint-config"
|
||||
|
||||
export default createConfigForNuxt()
|
||||
@@ -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>
|
||||
|
||||
2440
package-lock.json
generated
2440
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -7,10 +7,13 @@
|
||||
"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",
|
||||
"@nuxt/eslint": "^1.15.2",
|
||||
"iconify": "^1.4.0",
|
||||
"nuxt": "^4.3.1",
|
||||
"vue": "^3.5.29",
|
||||
|
||||
@@ -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})
|
||||
|
||||
type DiskSourceResult = {
|
||||
key: string
|
||||
@@ -74,7 +74,6 @@ type DiagramItem = {
|
||||
totalText: string
|
||||
}
|
||||
|
||||
const selectedBackup = ref<string | null>(null)
|
||||
const rawResults = ref<DiskSourceResult[]>([])
|
||||
const loading = ref(false)
|
||||
const chartRadius = 52
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user