From 455121132d593c460ee75238a22f23cd861c620c Mon Sep 17 00:00:00 2001 From: Matthieu Date: Tue, 17 Mar 2026 15:24:47 +0100 Subject: [PATCH] feat(frontend) : admin middleware, fix avatar upload, centralize IRI extraction, remove Nitro proxy - Add admin middleware protecting /admin page (ROLE_ADMIN check) - Fix useAvatarService to use useApi() with FormData detection - Create extractIdFromIri() utility, replace manual IRI parsing - Remove redundant Nitro devProxy (Vite proxy handles dev) Tickets: T-014, T-015, T-017, T-021 Co-Authored-By: Claude Opus 4.6 (1M context) --- .../components/admin/AdminClientTicketTab.vue | 15 ++++++--------- frontend/composables/useApi.ts | 11 +++++++---- frontend/composables/useAvatarService.ts | 12 +++++++----- frontend/middleware/admin.ts | 7 +++++++ frontend/nuxt.config.ts | 8 -------- frontend/pages/admin.vue | 1 + frontend/pages/portal/index.vue | 6 ++---- frontend/utils/iri.ts | 11 +++++++++++ 8 files changed, 41 insertions(+), 30 deletions(-) create mode 100644 frontend/middleware/admin.ts create mode 100644 frontend/utils/iri.ts diff --git a/frontend/components/admin/AdminClientTicketTab.vue b/frontend/components/admin/AdminClientTicketTab.vue index 64401a5..a9b3b63 100644 --- a/frontend/components/admin/AdminClientTicketTab.vue +++ b/frontend/components/admin/AdminClientTicketTab.vue @@ -274,25 +274,22 @@ const availableStatusTransitions = computed(() => { }) function getProjectName(iri: string): string { - const match = iri.match(/\/api\/projects\/(\d+)/) - if (!match) return '' - const id = Number(match[1]) + const id = extractIdFromIri(iri) + if (!id) return '' return projects.value.find(p => p.id === id)?.name ?? '' } function getSubmitterName(iri: string | null): string { if (!iri) return '-' - const match = iri.match(/\/api\/users\/(\d+)/) - if (!match) return '' - const id = Number(match[1]) + const id = extractIdFromIri(iri) + if (!id) return '' return users.value.find(u => u.id === id)?.username ?? '' } function getSubmitterUser(iri: string | null): UserData | undefined { if (!iri) return undefined - const match = iri.match(/\/api\/users\/(\d+)/) - if (!match) return undefined - const id = Number(match[1]) + const id = extractIdFromIri(iri) + if (!id) return undefined return users.value.find(u => u.id === id) } diff --git a/frontend/composables/useApi.ts b/frontend/composables/useApi.ts index 760df5b..bac2544 100644 --- a/frontend/composables/useApi.ts +++ b/frontend/composables/useApi.ts @@ -177,13 +177,16 @@ export function useApi(): ApiClient { ) { const needsJsonBody = method === 'POST' || method === 'PUT' const needsMergePatch = method === 'PATCH' + const isFormData = typeof FormData !== 'undefined' && options.body instanceof FormData const headers = new Headers(options.headers as HeadersInit | undefined) - if (needsMergePatch && !headers.has('Content-Type')) { - headers.set('Content-Type', 'application/merge-patch+json') - } else if (needsJsonBody && !headers.has('Content-Type')) { - headers.set('Content-Type', 'application/json') + if (!isFormData) { + if (needsMergePatch && !headers.has('Content-Type')) { + headers.set('Content-Type', 'application/merge-patch+json') + } else if (needsJsonBody && !headers.has('Content-Type')) { + headers.set('Content-Type', 'application/json') + } } return client(url, { ...options, method, headers }) diff --git a/frontend/composables/useAvatarService.ts b/frontend/composables/useAvatarService.ts index 010aad2..1b3072d 100644 --- a/frontend/composables/useAvatarService.ts +++ b/frontend/composables/useAvatarService.ts @@ -5,11 +5,13 @@ export function useAvatarService() { const formData = new FormData() formData.append('file', file, 'avatar.png') - return $fetch(`/api/users/${userId}/avatar`, { - method: 'POST', - body: formData, - credentials: 'include', - }) + return api.post<{ avatarUrl: string }>( + `/users/${userId}/avatar`, + formData as unknown as Record, + { + toastSuccessKey: 'profile.avatarUpdated', + } + ) } async function remove(userId: number): Promise { diff --git a/frontend/middleware/admin.ts b/frontend/middleware/admin.ts new file mode 100644 index 0000000..b464389 --- /dev/null +++ b/frontend/middleware/admin.ts @@ -0,0 +1,7 @@ +export default defineNuxtRouteMiddleware(() => { + const auth = useAuthStore() + + if (!auth.isAuthenticated || !auth.user?.roles?.includes('ROLE_ADMIN')) { + return navigateTo('/') + } +}) diff --git a/frontend/nuxt.config.ts b/frontend/nuxt.config.ts index ebec92b..fb3003e 100644 --- a/frontend/nuxt.config.ts +++ b/frontend/nuxt.config.ts @@ -23,14 +23,6 @@ export default defineNuxtConfig({ devServer: { port: 3002, }, - nitro: { - devProxy: { - '/api': { - target: 'http://nginx', - changeOrigin: true, - }, - }, - }, components: [ {path: '~/components', pathPrefix: false}, ], diff --git a/frontend/pages/admin.vue b/frontend/pages/admin.vue index fdc9854..0f8f91e 100644 --- a/frontend/pages/admin.vue +++ b/frontend/pages/admin.vue @@ -35,6 +35,7 @@