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 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">
<IconifyIcon icon="mdi:file-hidden" class="text-3xl text-m-muted/40" />
<p class="mt-2 font-mono text-xs text-m-muted/50">
@@ -63,6 +70,7 @@ const props = defineProps<{
const backups = ref<string[]>([])
const loading = ref(false)
const errorMessage = ref("")
const apiAuthHeader = useApiAuthHeader()
const title = computed(() => {
if (!props.folder) return "Fichiers"
@@ -72,25 +80,36 @@ const title = computed(() => {
const downloadBackup = async (file: string) => {
if (!props.folder) return
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) => {
if (!folder) {
loading.value = false
backups.value = []
errorMessage.value = ""
return
}
loading.value = true
errorMessage.value = ""
try {
const data = await $fetch<string[]>(`/api/backups?folder=${encodeURIComponent(folder)}`, {
headers: apiAuthHeader
headers: apiAuthHeader,
server: false
})
backups.value = data
} catch (error) {
console.error("Erreur récupération backups:", error)
backups.value = []
errorMessage.value = "Erreur lors de l'opération"
} finally {
loading.value = false
}
@@ -124,6 +143,12 @@ watch(() => props.folder, async (folder) => {
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 {
display: flex;
flex-direction: column;

View File

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

View File

@@ -2,8 +2,9 @@
import {Icon as IconifyIcon} from "@iconify/vue"
import { useApiAuthHeader } from "~/composables/useApiAuth"
const { data: messages } = await useFetch('/api/discord/messages', {
headers: useApiAuthHeader()
const { data: messages, error } = await useFetch('/api/discord/messages', {
headers: useApiAuthHeader(),
server: false
})
</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>
</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" />
<p class="mt-2 font-mono text-xs text-m-muted/50">
Aucun message
@@ -78,6 +86,12 @@ const { data: messages } = await useFetch('/api/discord/messages', {
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 {
display: flex;
flex-direction: column;

View File

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

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: {
link: [
{ 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",
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
} catch (error) {
const message = `Erreur: ${error instanceof Error ? error.message : String(error)}`
rawResults.value = [
{
key: "error",
label: "Source indisponible",
ok: false,
output: message
output: "Erreur lors de l'opération"
}
]
} finally {

View File

@@ -50,9 +50,11 @@ export default defineEventHandler(async (event) => {
output: output.trim()
}
} catch (error) {
console.error("Erreur execution script:", error)
throw createError({
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) {
console.error("Erreur backups:", error)
return createError({
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 () => {
const token = process.env.DISCORD_BOT_TOKEN
const channel = process.env.DISCORD_CHANNEL_ID
const token = process.env.DISCORD_BOT_TOKEN
const channel = process.env.DISCORD_CHANNEL_ID
if (!token || !channel) {
throw createError({
statusCode: 503,
statusMessage: "Service indisponible"
})
}
try {
const messages = await $fetch(
`https://discord.com/api/v10/channels/${channel}/messages?limit=20`,
{
headers: {
Authorization: `Bot ${token}`
}
`https://discord.com/api/v10/channels/${channel}/messages?limit=20`,
{
headers: {
Authorization: `Bot ${token}`
}
}
)
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
}
} catch (error) {
console.error(`Erreur disk source ${source.key}:`, error)
return {
key: source.key,
label: source.label,
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 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, `'\\''`)}'`
function runSsh(command: string): Promise<string> {