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) <noreply@anthropic.com>
This commit is contained in:
@@ -274,25 +274,22 @@ const availableStatusTransitions = computed(() => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
function getProjectName(iri: string): string {
|
function getProjectName(iri: string): string {
|
||||||
const match = iri.match(/\/api\/projects\/(\d+)/)
|
const id = extractIdFromIri(iri)
|
||||||
if (!match) return ''
|
if (!id) return ''
|
||||||
const id = Number(match[1])
|
|
||||||
return projects.value.find(p => p.id === id)?.name ?? ''
|
return projects.value.find(p => p.id === id)?.name ?? ''
|
||||||
}
|
}
|
||||||
|
|
||||||
function getSubmitterName(iri: string | null): string {
|
function getSubmitterName(iri: string | null): string {
|
||||||
if (!iri) return '-'
|
if (!iri) return '-'
|
||||||
const match = iri.match(/\/api\/users\/(\d+)/)
|
const id = extractIdFromIri(iri)
|
||||||
if (!match) return ''
|
if (!id) return ''
|
||||||
const id = Number(match[1])
|
|
||||||
return users.value.find(u => u.id === id)?.username ?? ''
|
return users.value.find(u => u.id === id)?.username ?? ''
|
||||||
}
|
}
|
||||||
|
|
||||||
function getSubmitterUser(iri: string | null): UserData | undefined {
|
function getSubmitterUser(iri: string | null): UserData | undefined {
|
||||||
if (!iri) return undefined
|
if (!iri) return undefined
|
||||||
const match = iri.match(/\/api\/users\/(\d+)/)
|
const id = extractIdFromIri(iri)
|
||||||
if (!match) return undefined
|
if (!id) return undefined
|
||||||
const id = Number(match[1])
|
|
||||||
return users.value.find(u => u.id === id)
|
return users.value.find(u => u.id === id)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -177,13 +177,16 @@ export function useApi(): ApiClient {
|
|||||||
) {
|
) {
|
||||||
const needsJsonBody = method === 'POST' || method === 'PUT'
|
const needsJsonBody = method === 'POST' || method === 'PUT'
|
||||||
const needsMergePatch = method === 'PATCH'
|
const needsMergePatch = method === 'PATCH'
|
||||||
|
const isFormData = typeof FormData !== 'undefined' && options.body instanceof FormData
|
||||||
|
|
||||||
const headers = new Headers(options.headers as HeadersInit | undefined)
|
const headers = new Headers(options.headers as HeadersInit | undefined)
|
||||||
|
|
||||||
if (needsMergePatch && !headers.has('Content-Type')) {
|
if (!isFormData) {
|
||||||
headers.set('Content-Type', 'application/merge-patch+json')
|
if (needsMergePatch && !headers.has('Content-Type')) {
|
||||||
} else if (needsJsonBody && !headers.has('Content-Type')) {
|
headers.set('Content-Type', 'application/merge-patch+json')
|
||||||
headers.set('Content-Type', 'application/json')
|
} else if (needsJsonBody && !headers.has('Content-Type')) {
|
||||||
|
headers.set('Content-Type', 'application/json')
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return client<T>(url, { ...options, method, headers })
|
return client<T>(url, { ...options, method, headers })
|
||||||
|
|||||||
@@ -5,11 +5,13 @@ export function useAvatarService() {
|
|||||||
const formData = new FormData()
|
const formData = new FormData()
|
||||||
formData.append('file', file, 'avatar.png')
|
formData.append('file', file, 'avatar.png')
|
||||||
|
|
||||||
return $fetch(`/api/users/${userId}/avatar`, {
|
return api.post<{ avatarUrl: string }>(
|
||||||
method: 'POST',
|
`/users/${userId}/avatar`,
|
||||||
body: formData,
|
formData as unknown as Record<string, unknown>,
|
||||||
credentials: 'include',
|
{
|
||||||
})
|
toastSuccessKey: 'profile.avatarUpdated',
|
||||||
|
}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
async function remove(userId: number): Promise<void> {
|
async function remove(userId: number): Promise<void> {
|
||||||
|
|||||||
7
frontend/middleware/admin.ts
Normal file
7
frontend/middleware/admin.ts
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
export default defineNuxtRouteMiddleware(() => {
|
||||||
|
const auth = useAuthStore()
|
||||||
|
|
||||||
|
if (!auth.isAuthenticated || !auth.user?.roles?.includes('ROLE_ADMIN')) {
|
||||||
|
return navigateTo('/')
|
||||||
|
}
|
||||||
|
})
|
||||||
@@ -23,14 +23,6 @@ export default defineNuxtConfig({
|
|||||||
devServer: {
|
devServer: {
|
||||||
port: 3002,
|
port: 3002,
|
||||||
},
|
},
|
||||||
nitro: {
|
|
||||||
devProxy: {
|
|
||||||
'/api': {
|
|
||||||
target: 'http://nginx',
|
|
||||||
changeOrigin: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
components: [
|
components: [
|
||||||
{path: '~/components', pathPrefix: false},
|
{path: '~/components', pathPrefix: false},
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -35,6 +35,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
definePageMeta({ middleware: ['admin'] })
|
||||||
useHead({ title: 'Administration' })
|
useHead({ title: 'Administration' })
|
||||||
|
|
||||||
const tabs = [
|
const tabs = [
|
||||||
|
|||||||
@@ -53,10 +53,8 @@ const ticketCountByProject = computed(() => {
|
|||||||
const counts: Record<number, number> = {}
|
const counts: Record<number, number> = {}
|
||||||
for (const ticket of tickets.value) {
|
for (const ticket of tickets.value) {
|
||||||
if (ticket.status === 'new' || ticket.status === 'in_progress') {
|
if (ticket.status === 'new' || ticket.status === 'in_progress') {
|
||||||
// Extract project ID from IRI
|
const projectId = extractIdFromIri(ticket.project)
|
||||||
const match = ticket.project.match(/\/api\/projects\/(\d+)/)
|
if (projectId) {
|
||||||
if (match) {
|
|
||||||
const projectId = Number(match[1])
|
|
||||||
counts[projectId] = (counts[projectId] ?? 0) + 1
|
counts[projectId] = (counts[projectId] ?? 0) + 1
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
11
frontend/utils/iri.ts
Normal file
11
frontend/utils/iri.ts
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
/**
|
||||||
|
* Extract the numeric ID from an API Platform IRI string.
|
||||||
|
* Example: "/api/projects/5" → 5
|
||||||
|
*/
|
||||||
|
export function extractIdFromIri(iri: string | null | undefined): number {
|
||||||
|
if (!iri) return 0
|
||||||
|
const lastSlash = iri.lastIndexOf('/')
|
||||||
|
if (lastSlash === -1) return 0
|
||||||
|
const id = Number(iri.substring(lastSlash + 1))
|
||||||
|
return Number.isFinite(id) ? id : 0
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user