fix : correctif mr

This commit is contained in:
2026-03-13 09:47:09 +01:00
parent b3fc6f77b1
commit 00dc2daa3d
11 changed files with 61 additions and 67 deletions

View File

@@ -62,7 +62,7 @@
import {Icon as IconifyIcon} from "@iconify/vue" import {Icon as IconifyIcon} from "@iconify/vue"
import CircleSkeleton from "~/components/skeleton/CircleSkeleton.vue" import CircleSkeleton from "~/components/skeleton/CircleSkeleton.vue"
import TextSkeleton from "~/components/skeleton/TextSkeleton.vue" import TextSkeleton from "~/components/skeleton/TextSkeleton.vue"
import { downloadApiFile, useApiAuthHeader } from "~/composables/useApiAuth" import { apiFetch, downloadApiFile } from "~/composables/useApiAuth"
const props = defineProps<{ const props = defineProps<{
folder: string | null folder: string | null
@@ -71,7 +71,6 @@ const props = defineProps<{
const backups = ref<string[]>([]) const backups = ref<string[]>([])
const loading = ref(false) const loading = ref(false)
const errorMessage = ref("") const errorMessage = ref("")
const apiAuthHeader = useApiAuthHeader()
const title = computed(() => { const title = computed(() => {
if (!props.folder) return "Fichiers" if (!props.folder) return "Fichiers"
return `Backup — ${props.folder.toUpperCase()}` return `Backup — ${props.folder.toUpperCase()}`
@@ -101,10 +100,7 @@ watch(() => props.folder, async (folder) => {
loading.value = true loading.value = true
errorMessage.value = "" errorMessage.value = ""
try { try {
const data = await $fetch<string[]>(`/api/backups?folder=${encodeURIComponent(folder)}`, { const data = await apiFetch<string[]>(`/api/backups?folder=${encodeURIComponent(folder)}`)
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)

View File

@@ -79,7 +79,7 @@
<script setup lang="ts"> <script setup lang="ts">
import { computed, onMounted, ref } from "vue" import { computed, onMounted, ref } from "vue"
import { Icon as IconifyIcon } from "@iconify/vue" import { Icon as IconifyIcon } from "@iconify/vue"
import { useApiAuthHeader } from "~/composables/useApiAuth" import { apiFetch } from "~/composables/useApiAuth"
type BackupScript = { type BackupScript = {
key: string key: string
@@ -119,7 +119,6 @@ const scripts = ref<BackupScript[]>([])
const output = ref<string>("") const output = ref<string>("")
const message = ref<string>("") const message = ref<string>("")
const isError = ref(false) const isError = ref(false)
const apiAuthHeader = useApiAuthHeader()
const statusClass = computed(() => (isError.value ? "status-error" : "status-success")) const statusClass = computed(() => (isError.value ? "status-error" : "status-success"))
@@ -136,9 +135,7 @@ const loadScripts = async () => {
downloadFolders: [] downloadFolders: []
}) })
try { try {
const data = await $fetch<BackupScriptListResponse>("/api/backup-script", { const data = await apiFetch<BackupScriptListResponse>("/api/backup-script")
headers: apiAuthHeader
})
scripts.value = data.scripts scripts.value = data.scripts
} catch (error) { } catch (error) {
scripts.value = [] scripts.value = []
@@ -164,10 +161,9 @@ const runScript = async (key: string) => {
isError.value = false isError.value = false
try { try {
const data = await $fetch<BackupScriptRunResponse>("/api/backup-script", { const data = await apiFetch<BackupScriptRunResponse>("/api/backup-script", {
method: "POST", method: "POST",
body: { key }, body: { key }
headers: apiAuthHeader
}) })
message.value = `${data.label} execute avec succes` message.value = `${data.label} execute avec succes`
output.value = data.output || "Aucune sortie retournee." output.value = data.output || "Aucune sortie retournee."

View File

@@ -1,9 +1,9 @@
<script setup> <script setup>
import {Icon as IconifyIcon} from "@iconify/vue" import {Icon as IconifyIcon} from "@iconify/vue"
import { useApiAuthHeader } from "~/composables/useApiAuth" import { apiFetch } from "~/composables/useApiAuth"
const { data: messages, error } = await useFetch('/api/discord/messages', { const { data: messages, error } = await useFetch('/api/discord/messages', {
headers: useApiAuthHeader(), $fetch: apiFetch,
server: false server: false
}) })
</script> </script>

View File

@@ -46,14 +46,13 @@
<script setup lang="ts"> <script setup lang="ts">
import {computed, ref} from "vue" import {computed, ref} from "vue"
import {Icon as IconifyIcon} from "@iconify/vue" import {Icon as IconifyIcon} from "@iconify/vue"
import { useApiAuthHeader, withApiAuth } from "~/composables/useApiAuth" import { apiRequest } from "~/composables/useApiAuth"
const ping = ref<number | null>(null) 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 errorMessage = ref("")
const apiAuthHeader = useApiAuthHeader()
const metrics = computed(() => [ const metrics = computed(() => [
{ label: "Download", icon: "mdi:arrow-down-bold", value: download.value, unit: "Mbps" }, { label: "Download", icon: "mdi:arrow-down-bold", value: download.value, unit: "Mbps" },
@@ -63,9 +62,7 @@ const metrics = computed(() => [
async function testDownload() { async function testDownload() {
const start = performance.now() const start = performance.now()
const res = await fetch('/api/download', { const res = await apiRequest('/api/download')
headers: apiAuthHeader
})
if (!res.ok) { if (!res.ok) {
throw new Error(`HTTP ${res.status}`) throw new Error(`HTTP ${res.status}`)
} }
@@ -80,7 +77,7 @@ 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()
const response = await fetch('/api/upload', withApiAuth({ method: 'POST', body: data })) const response = await apiRequest('/api/upload', { method: 'POST', body: data })
if (!response.ok) { if (!response.ok) {
throw new Error(`HTTP ${response.status}`) throw new Error(`HTTP ${response.status}`)
} }

View File

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

View File

@@ -32,26 +32,26 @@ function getDownloadFileName(contentDisposition: string | null, fallback: string
return fallback return fallback
} }
export function useApiAuthHeader() { export function withApiAuth(init: RequestInit = {}) {
const runtimeConfig = useRuntimeConfig() // Les appels frontend reutilisent les cookies httpOnly poses cote serveur.
const token = runtimeConfig.public.apiSecretKey
if (!token) {
return {}
}
// Tous les appels frontend vers /api/* reutilisent ce header commun.
return { return {
Authorization: `Bearer ${token}` ...init,
headers: {
...toHeadersObject(init.headers)
}
} }
} }
export const apiFetch = $fetch.create({})
export function apiRequest(input: RequestInfo | URL, init: RequestInit = {}) {
return fetch(input, withApiAuth(init))
}
export async function downloadApiFile(url: string, fileNameFallback: string) { export async function downloadApiFile(url: string, fileNameFallback: string) {
// Les telechargements passent aussi par fetch pour pouvoir envoyer // Les telechargements passent aussi par fetch pour pouvoir recuperer
// le header Authorization, contrairement a un simple <a href>. // le contenu et le nom de fichier renvoye par l'API.
const response = await fetch(url, { const response = await apiRequest(url)
headers: useApiAuthHeader()
})
if (!response.ok) { if (!response.ok) {
throw new Error(`HTTP ${response.status}`) throw new Error(`HTTP ${response.status}`)
@@ -73,14 +73,3 @@ export async function downloadApiFile(url: string, fileNameFallback: string) {
link.remove() link.remove()
URL.revokeObjectURL(objectUrl) 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

@@ -37,7 +37,6 @@ export default defineNuxtConfig({
runtimeConfig: { runtimeConfig: {
apiSecretKey: process.env.API_SECRET_KEY, apiSecretKey: process.env.API_SECRET_KEY,
public: { public: {
apiSecretKey: process.env.API_SECRET_KEY || "",
appVersion: getRepoVersion() appVersion: getRepoVersion()
} }
}, },

View File

@@ -96,7 +96,7 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref } from "vue" import { ref } from "vue"
import BackupRun from "~/components/BackupRun.vue" import BackupRun from "~/components/BackupRun.vue"
import { downloadApiFile, useApiAuthHeader } from "~/composables/useApiAuth" import { apiFetch, downloadApiFile } from "~/composables/useApiAuth"
definePageMeta({ layout: false }) definePageMeta({ layout: false })
@@ -118,12 +118,9 @@ const emptyScriptResult = (): ScriptResult => ({
const selectedBackup = ref<string | null>(null) const selectedBackup = ref<string | null>(null)
const scriptResult = ref<ScriptResult>(emptyScriptResult()) const scriptResult = ref<ScriptResult>(emptyScriptResult())
const apiAuthHeader = useApiAuthHeader()
const fetchLatestBackup = async (folder: string) => { const fetchLatestBackup = async (folder: string) => {
const files = await $fetch<string[]>(`/api/backups?folder=${encodeURIComponent(folder)}`, { const files = await apiFetch<string[]>(`/api/backups?folder=${encodeURIComponent(folder)}`)
headers: apiAuthHeader
})
return files[0] || null return files[0] || null
} }

View File

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

View File

@@ -0,0 +1,25 @@
export default defineEventHandler((event) => {
const path = event.path || event.node.req.url || ""
if (path.startsWith("/api/")) {
return
}
const runtimeConfig = useRuntimeConfig(event)
const expectedToken = runtimeConfig.apiSecretKey
if (!expectedToken) {
return
}
if (getCookie(event, "api_auth_token") === expectedToken) {
return
}
setCookie(event, "api_auth_token", expectedToken, {
httpOnly: true,
sameSite: "lax",
secure: process.env.NODE_ENV === "production",
path: "/"
})
})

View File

@@ -9,6 +9,7 @@ export default defineEventHandler((event) => {
const runtimeConfig = useRuntimeConfig(event) const runtimeConfig = useRuntimeConfig(event)
const authorization = getHeader(event, "authorization") const authorization = getHeader(event, "authorization")
const cookieToken = getCookie(event, "api_auth_token")
const expectedToken = runtimeConfig.apiSecretKey const expectedToken = runtimeConfig.apiSecretKey
// Si aucun secret n'est configure cote serveur, on refuse la requete. // Si aucun secret n'est configure cote serveur, on refuse la requete.
@@ -19,9 +20,9 @@ export default defineEventHandler((event) => {
}) })
} }
// Le header doit correspondre exactement au format attendu : // Le secret peut venir soit d'un header serveur explicite,
// Authorization: Bearer <token> // soit du cookie httpOnly pose pour l'application web.
if (authorization !== `Bearer ${expectedToken}`) { if (authorization !== `Bearer ${expectedToken}` && cookieToken !== expectedToken) {
throw createError({ throw createError({
statusCode: 401, statusCode: 401,
statusMessage: "Unauthorized" statusMessage: "Unauthorized"