fix : correctif mr
This commit is contained in:
@@ -62,7 +62,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"
|
||||
import { apiFetch, downloadApiFile } from "~/composables/useApiAuth"
|
||||
|
||||
const props = defineProps<{
|
||||
folder: string | null
|
||||
@@ -71,7 +71,6 @@ 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"
|
||||
return `Backup — ${props.folder.toUpperCase()}`
|
||||
@@ -101,10 +100,7 @@ watch(() => props.folder, async (folder) => {
|
||||
loading.value = true
|
||||
errorMessage.value = ""
|
||||
try {
|
||||
const data = await $fetch<string[]>(`/api/backups?folder=${encodeURIComponent(folder)}`, {
|
||||
headers: apiAuthHeader,
|
||||
server: false
|
||||
})
|
||||
const data = await apiFetch<string[]>(`/api/backups?folder=${encodeURIComponent(folder)}`)
|
||||
backups.value = data
|
||||
} catch (error) {
|
||||
console.error("Erreur récupération backups:", error)
|
||||
|
||||
@@ -79,7 +79,7 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, ref } from "vue"
|
||||
import { Icon as IconifyIcon } from "@iconify/vue"
|
||||
import { useApiAuthHeader } from "~/composables/useApiAuth"
|
||||
import { apiFetch } from "~/composables/useApiAuth"
|
||||
|
||||
type BackupScript = {
|
||||
key: string
|
||||
@@ -119,7 +119,6 @@ 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"))
|
||||
|
||||
@@ -136,9 +135,7 @@ const loadScripts = async () => {
|
||||
downloadFolders: []
|
||||
})
|
||||
try {
|
||||
const data = await $fetch<BackupScriptListResponse>("/api/backup-script", {
|
||||
headers: apiAuthHeader
|
||||
})
|
||||
const data = await apiFetch<BackupScriptListResponse>("/api/backup-script")
|
||||
scripts.value = data.scripts
|
||||
} catch (error) {
|
||||
scripts.value = []
|
||||
@@ -164,10 +161,9 @@ const runScript = async (key: string) => {
|
||||
isError.value = false
|
||||
|
||||
try {
|
||||
const data = await $fetch<BackupScriptRunResponse>("/api/backup-script", {
|
||||
const data = await apiFetch<BackupScriptRunResponse>("/api/backup-script", {
|
||||
method: "POST",
|
||||
body: { key },
|
||||
headers: apiAuthHeader
|
||||
body: { key }
|
||||
})
|
||||
message.value = `${data.label} execute avec succes`
|
||||
output.value = data.output || "Aucune sortie retournee."
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
<script setup>
|
||||
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', {
|
||||
headers: useApiAuthHeader(),
|
||||
$fetch: apiFetch,
|
||||
server: false
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -46,14 +46,13 @@
|
||||
<script setup lang="ts">
|
||||
import {computed, ref} from "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 download = ref<number | null>(null)
|
||||
const upload = ref<number | null>(null)
|
||||
const isTesting = ref(false)
|
||||
const errorMessage = ref("")
|
||||
const apiAuthHeader = useApiAuthHeader()
|
||||
|
||||
const metrics = computed(() => [
|
||||
{ label: "Download", icon: "mdi:arrow-down-bold", value: download.value, unit: "Mbps" },
|
||||
@@ -63,9 +62,7 @@ const metrics = computed(() => [
|
||||
|
||||
async function testDownload() {
|
||||
const start = performance.now()
|
||||
const res = await fetch('/api/download', {
|
||||
headers: apiAuthHeader
|
||||
})
|
||||
const res = await apiRequest('/api/download')
|
||||
if (!res.ok) {
|
||||
throw new Error(`HTTP ${res.status}`)
|
||||
}
|
||||
@@ -80,7 +77,7 @@ async function testUpload() {
|
||||
const size = 5 * 1024 * 1024
|
||||
const data = new Uint8Array(size)
|
||||
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) {
|
||||
throw new Error(`HTTP ${response.status}`)
|
||||
}
|
||||
|
||||
@@ -43,7 +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"
|
||||
import { apiFetch } from "~/composables/useApiAuth"
|
||||
|
||||
interface StatusRow {
|
||||
label: string
|
||||
@@ -72,7 +72,6 @@ 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) => {
|
||||
@@ -86,9 +85,7 @@ const checkStatus = async () => {
|
||||
loading.value = true
|
||||
}
|
||||
try {
|
||||
const data = await $fetch<StatusResponse>(props.endpoint, {
|
||||
headers: apiAuthHeader
|
||||
})
|
||||
const data = await apiFetch<StatusResponse>(props.endpoint)
|
||||
rows.value = data.results
|
||||
} catch (error) {
|
||||
rows.value = [
|
||||
|
||||
@@ -32,26 +32,26 @@ function getDownloadFileName(contentDisposition: string | null, fallback: string
|
||||
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.
|
||||
export function withApiAuth(init: RequestInit = {}) {
|
||||
// Les appels frontend reutilisent les cookies httpOnly poses cote serveur.
|
||||
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) {
|
||||
// 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()
|
||||
})
|
||||
// Les telechargements passent aussi par fetch pour pouvoir recuperer
|
||||
// le contenu et le nom de fichier renvoye par l'API.
|
||||
const response = await apiRequest(url)
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP ${response.status}`)
|
||||
@@ -73,14 +73,3 @@ export async function downloadApiFile(url: string, fileNameFallback: string) {
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -37,7 +37,6 @@ export default defineNuxtConfig({
|
||||
runtimeConfig: {
|
||||
apiSecretKey: process.env.API_SECRET_KEY,
|
||||
public: {
|
||||
apiSecretKey: process.env.API_SECRET_KEY || "",
|
||||
appVersion: getRepoVersion()
|
||||
}
|
||||
},
|
||||
|
||||
@@ -96,7 +96,7 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from "vue"
|
||||
import BackupRun from "~/components/BackupRun.vue"
|
||||
import { downloadApiFile, useApiAuthHeader } from "~/composables/useApiAuth"
|
||||
import { apiFetch, downloadApiFile } from "~/composables/useApiAuth"
|
||||
|
||||
definePageMeta({ layout: false })
|
||||
|
||||
@@ -118,12 +118,9 @@ 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)}`, {
|
||||
headers: apiAuthHeader
|
||||
})
|
||||
const files = await apiFetch<string[]>(`/api/backups?folder=${encodeURIComponent(folder)}`)
|
||||
return files[0] || null
|
||||
}
|
||||
|
||||
|
||||
@@ -49,7 +49,7 @@
|
||||
<script setup lang="ts">
|
||||
definePageMeta({layout: false})
|
||||
import {computed, onMounted, ref} from "vue"
|
||||
import { useApiAuthHeader } from "~/composables/useApiAuth"
|
||||
import { apiFetch } from "~/composables/useApiAuth"
|
||||
|
||||
type DiskSourceResult = {
|
||||
key: string
|
||||
@@ -78,7 +78,6 @@ 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
|
||||
|
||||
@@ -153,9 +152,7 @@ const runScript = async () => {
|
||||
rawResults.value = []
|
||||
|
||||
try {
|
||||
const output = await $fetch<DiskApiResponse>("/api/disk", {
|
||||
headers: apiAuthHeader
|
||||
})
|
||||
const output = await apiFetch<DiskApiResponse>("/api/disk")
|
||||
rawResults.value = output.results
|
||||
} catch (error) {
|
||||
rawResults.value = [
|
||||
|
||||
25
server/middleware/auth-cookie.ts
Normal file
25
server/middleware/auth-cookie.ts
Normal 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: "/"
|
||||
})
|
||||
})
|
||||
@@ -9,6 +9,7 @@ export default defineEventHandler((event) => {
|
||||
|
||||
const runtimeConfig = useRuntimeConfig(event)
|
||||
const authorization = getHeader(event, "authorization")
|
||||
const cookieToken = getCookie(event, "api_auth_token")
|
||||
const expectedToken = runtimeConfig.apiSecretKey
|
||||
|
||||
// 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 :
|
||||
// Authorization: Bearer <token>
|
||||
if (authorization !== `Bearer ${expectedToken}`) {
|
||||
// Le secret peut venir soit d'un header serveur explicite,
|
||||
// soit du cookie httpOnly pose pour l'application web.
|
||||
if (authorization !== `Bearer ${expectedToken}` && cookieToken !== expectedToken) {
|
||||
throw createError({
|
||||
statusCode: 401,
|
||||
statusMessage: "Unauthorized"
|
||||
|
||||
Reference in New Issue
Block a user