fix : securite regex et message erreur et endpoint

This commit is contained in:
2026-03-12 08:58:58 +01:00
parent 47bc8ba966
commit b3fc6f77b1
12 changed files with 198 additions and 24 deletions

View File

@@ -22,6 +22,13 @@
</div> </div>
</div> </div>
<div v-else-if="errorMessage" class="empty-state error-state">
<IconifyIcon icon="mdi:alert-circle-outline" class="text-3xl text-m-error/70" />
<p class="mt-2 font-mono text-xs text-m-error/80">
{{ errorMessage }}
</p>
</div>
<div v-else-if="backups.length === 0" class="empty-state"> <div v-else-if="backups.length === 0" class="empty-state">
<IconifyIcon icon="mdi:file-hidden" class="text-3xl text-m-muted/40" /> <IconifyIcon icon="mdi:file-hidden" class="text-3xl text-m-muted/40" />
<p class="mt-2 font-mono text-xs text-m-muted/50"> <p class="mt-2 font-mono text-xs text-m-muted/50">
@@ -63,6 +70,7 @@ const props = defineProps<{
const backups = ref<string[]>([]) const backups = ref<string[]>([])
const loading = ref(false) const loading = ref(false)
const errorMessage = ref("")
const apiAuthHeader = useApiAuthHeader() const apiAuthHeader = useApiAuthHeader()
const title = computed(() => { const title = computed(() => {
if (!props.folder) return "Fichiers" if (!props.folder) return "Fichiers"
@@ -72,25 +80,36 @@ const title = computed(() => {
const downloadBackup = async (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)}`
await downloadApiFile(url, file) errorMessage.value = ""
try {
await downloadApiFile(url, file)
} catch (error) {
console.error("Erreur telechargement backup:", error)
errorMessage.value = "Erreur lors de l'opération"
}
} }
watch(() => props.folder, async (folder) => { watch(() => props.folder, async (folder) => {
if (!folder) { if (!folder) {
loading.value = false loading.value = false
backups.value = [] backups.value = []
errorMessage.value = ""
return return
} }
loading.value = true loading.value = true
errorMessage.value = ""
try { try {
const data = await $fetch<string[]>(`/api/backups?folder=${encodeURIComponent(folder)}`, { const data = await $fetch<string[]>(`/api/backups?folder=${encodeURIComponent(folder)}`, {
headers: apiAuthHeader headers: apiAuthHeader,
server: false
}) })
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)
backups.value = [] backups.value = []
errorMessage.value = "Erreur lors de l'opération"
} finally { } finally {
loading.value = false loading.value = false
} }
@@ -124,6 +143,12 @@ watch(() => props.folder, async (folder) => {
padding: 2.5rem 1rem; padding: 2.5rem 1rem;
} }
.error-state {
border-radius: 8px;
border: 1px solid rgb(var(--m-error) / 0.12);
background: rgb(var(--m-error) / 0.06);
}
.file-list { .file-list {
display: flex; display: flex;
flex-direction: column; flex-direction: column;

View File

@@ -143,7 +143,7 @@ const loadScripts = async () => {
} catch (error) { } catch (error) {
scripts.value = [] scripts.value = []
isError.value = true 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", { emit("result", {
key: null, key: null,
label: "", label: "",
@@ -180,7 +180,7 @@ const runScript = async (key: string) => {
}) })
} catch (error: any) { } catch (error: any) {
isError.value = true isError.value = true
message.value = error?.data?.statusMessage || "Erreur execution script" message.value = error?.data?.statusMessage || "Erreur lors de l'opération"
output.value = "" output.value = ""
emit("result", { emit("result", {
key, key,

View File

@@ -2,8 +2,9 @@
import {Icon as IconifyIcon} from "@iconify/vue" import {Icon as IconifyIcon} from "@iconify/vue"
import { useApiAuthHeader } from "~/composables/useApiAuth" import { useApiAuthHeader } from "~/composables/useApiAuth"
const { data: messages } = await useFetch('/api/discord/messages', { const { data: messages, error } = await useFetch('/api/discord/messages', {
headers: useApiAuthHeader() headers: useApiAuthHeader(),
server: false
}) })
</script> </script>
@@ -17,7 +18,14 @@ const { data: messages } = await useFetch('/api/discord/messages', {
<span class="font-mono text-[10px] text-m-muted tracking-widest uppercase">Messages</span> <span class="font-mono text-[10px] text-m-muted tracking-widest uppercase">Messages</span>
</div> </div>
<div v-if="!messages || messages.length === 0" class="empty-state"> <div v-if="error" class="empty-state error-state">
<IconifyIcon icon="mdi:alert-circle-outline" class="text-3xl text-m-error/70" />
<p class="mt-2 font-mono text-xs text-m-error/80">
Erreur lors de l'opération
</p>
</div>
<div v-else-if="!messages || messages.length === 0" class="empty-state">
<IconifyIcon icon="mdi:chat-outline" class="text-3xl text-m-muted/40" /> <IconifyIcon icon="mdi:chat-outline" class="text-3xl text-m-muted/40" />
<p class="mt-2 font-mono text-xs text-m-muted/50"> <p class="mt-2 font-mono text-xs text-m-muted/50">
Aucun message Aucun message
@@ -78,6 +86,12 @@ const { data: messages } = await useFetch('/api/discord/messages', {
padding: 2rem 1rem; padding: 2rem 1rem;
} }
.error-state {
border-radius: 8px;
border: 1px solid rgb(var(--m-error) / 0.12);
background: rgb(var(--m-error) / 0.06);
}
.message-list { .message-list {
display: flex; display: flex;
flex-direction: column; flex-direction: column;

View File

@@ -36,6 +36,10 @@
</div> </div>
</div> </div>
</div> </div>
<p v-if="errorMessage" class="error-text" role="status" aria-live="polite">
{{ errorMessage }}
</p>
</div> </div>
</template> </template>
@@ -48,6 +52,7 @@ 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 errorMessage = ref("")
const apiAuthHeader = useApiAuthHeader() const apiAuthHeader = useApiAuthHeader()
const metrics = computed(() => [ const metrics = computed(() => [
@@ -61,6 +66,9 @@ async function testDownload() {
const res = await fetch('/api/download', { const res = await fetch('/api/download', {
headers: apiAuthHeader headers: apiAuthHeader
}) })
if (!res.ok) {
throw new Error(`HTTP ${res.status}`)
}
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
@@ -72,7 +80,10 @@ 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', 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 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)
@@ -80,7 +91,10 @@ async function testUpload() {
async function testPing() { async function testPing() {
const start = performance.now() 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() const end = performance.now()
ping.value = Math.round(end - start) ping.value = Math.round(end - start)
} }
@@ -90,11 +104,15 @@ async function runTests() {
download.value = null download.value = null
upload.value = null upload.value = null
ping.value = null ping.value = null
errorMessage.value = ""
try { try {
await testDownload() await testDownload()
await testUpload() await testUpload()
await testPing() await testPing()
} catch (error) {
console.error("Erreur speedtest:", error)
errorMessage.value = "Erreur lors de l'opération"
} finally { } finally {
isTesting.value = false isTesting.value = false
} }
@@ -193,4 +211,15 @@ async function runTests() {
letter-spacing: 0.1em; letter-spacing: 0.1em;
color: rgb(var(--m-muted)); 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));
}
</style> </style>

86
composables/useApiAuth.ts Normal file
View 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)
}
}
}

View File

@@ -26,7 +26,7 @@ export default defineNuxtConfig({
head: { head: {
link: [ link: [
{ rel: "preconnect", href: "https://fonts.googleapis.com" }, { 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", 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" 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"

View File

@@ -158,13 +158,12 @@ const runScript = async () => {
}) })
rawResults.value = output.results rawResults.value = output.results
} catch (error) { } catch (error) {
const message = `Erreur: ${error instanceof Error ? error.message : String(error)}`
rawResults.value = [ rawResults.value = [
{ {
key: "error", key: "error",
label: "Source indisponible", label: "Source indisponible",
ok: false, ok: false,
output: message output: "Erreur lors de l'opération"
} }
] ]
} finally { } finally {

View File

@@ -50,9 +50,11 @@ export default defineEventHandler(async (event) => {
output: output.trim() output: output.trim()
} }
} catch (error) { } catch (error) {
console.error("Erreur execution script:", error)
throw createError({ throw createError({
statusCode: 500, statusCode: 500,
statusMessage: `Erreur execution script: ${String(error)}` statusMessage: "Erreur lors de l'opération"
}) })
} }
}) })

View File

@@ -31,9 +31,11 @@ function isMissingPathError(error: unknown): boolean {
} }
function toServerError(error: unknown) { function toServerError(error: unknown) {
console.error("Erreur backups:", error)
return createError({ return createError({
statusCode: 500, statusCode: 500,
statusMessage: `Erreur SSH backups: ${String(error)}` statusMessage: "Erreur lors de l'opération"
}) })
} }

View File

@@ -1,15 +1,31 @@
export default defineEventHandler(async () => { export default defineEventHandler(async () => {
const token = process.env.DISCORD_BOT_TOKEN const token = process.env.DISCORD_BOT_TOKEN
const channel = process.env.DISCORD_CHANNEL_ID const channel = process.env.DISCORD_CHANNEL_ID
if (!token || !channel) {
throw createError({
statusCode: 503,
statusMessage: "Service indisponible"
})
}
try {
const messages = await $fetch( const messages = await $fetch(
`https://discord.com/api/v10/channels/${channel}/messages?limit=20`, `https://discord.com/api/v10/channels/${channel}/messages?limit=20`,
{ {
headers: { headers: {
Authorization: `Bot ${token}` Authorization: `Bot ${token}`
}
} }
}
) )
return messages return messages
}) } catch (error) {
console.error("Erreur Discord messages:", error)
throw createError({
statusCode: 500,
statusMessage: "Erreur lors de l'opération"
})
}
})

View File

@@ -40,11 +40,12 @@ export default defineEventHandler(async () => {
output output
} }
} catch (error) { } catch (error) {
console.error(`Erreur disk source ${source.key}:`, error)
return { return {
key: source.key, key: source.key,
label: source.label, label: source.label,
ok: false, ok: false,
output: `Erreur: ${String(error)}` output: "Erreur lors de l'opération"
} }
} }
}) })

View File

@@ -7,7 +7,7 @@ const REMOTE_ROOT = process.env.BACKUPS_REMOTE_ROOT || "/home/malio-b/backups"
const FOLDER_MAP = folderMap as Record<string, string> const FOLDER_MAP = folderMap as Record<string, string>
const isSafeFolder = (value: string) => /^[a-zA-Z0-9._-]+$/.test(value) 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, `'\\''`)}'` const shellQuote = (value: string) => `'${value.replace(/'/g, `'\\''`)}'`
function runSsh(command: string): Promise<string> { function runSsh(command: string): Promise<string> {