chore: harden config, strengthen frontend typing, fix UTF-8 mojibake

Security config (3.1):
- config/session.ts: cookie.secure now follows app.inProduction instead of
  being hardcoded to false
- config/shield.ts: enable CSP with baseline directives compatible with
  Vite dev server and Inertia (self + @viteDevUrl/@viteHmrUrl)

Frontend typing (3.2):
- Replace 'any' props with concrete types (Computer, Component, ComponentType,
  ComputerState) across computers/*.vue and components/*.vue, using the
  existing inertia/pages/types/UITypes.ts definitions
- Fix UITypes.ts: export ComponentType/ComputerState, switch from interface
  to type (interfaces are never assignable to Inertia's ComponentProps index
  signature), drop the stale componentTypeId/componentStateId fields that
  didn't match the actual Lucid schema (real field is typeId, no component
  state exists)
- Add an asPageProp() boundary cast in the two controllers: Lucid's
  serialize() returns a loosely typed ModelObject that isn't structurally
  assignable to the concrete page prop shapes (and the inertia/** tree is
  excluded from the backend tsconfig, so real types can't be imported
  directly across that boundary)

UTF-8 mojibake fixes (3.3):
- Repair broken accented characters (├®/├á/├┤ -> é/à/ô, Ôé¼ -> €) left over
  from a bad encoding during the GWT->Inertia migration, across
  ComponentEditor.vue, ComputerComponentsTable.vue, SimpleGallery.vue,
  TableComponent.vue, isAdminMode.ts and the custom directives

Misc cleanup (3.5):
- computer.ts: replace console.warn with the app logger (includes
  computerId) when .cost is accessed without preloading components
This commit is contained in:
Kevin L'Hereec committed 2026-07-23 11:10:37 +02:00
1 parent 3579c9dfec
commit 94f89be605
19 files changed
+103 -48

No files matched your search

+2 -2
View File
@@ -1,9 +1,9 @@
import { ref, watch } from 'vue'
// Cr├®er une r├®f├®rence pour isAdminActive
// Créer une référence pour isAdminActive
const isAdminActive = ref(JSON.parse(localStorage.getItem('isAdminActive') || 'false'))
// Surveiller les changements de isAdminActive et mettre à jour localStorage
// Surveiller les changements de isAdminActive et mettre à jour localStorage
watch(isAdminActive, (newValue) => {
localStorage.setItem('isAdminActive', JSON.stringify(newValue))
})
+5 -5
View File
@@ -1,7 +1,7 @@
import { createApp, DirectiveBinding, h, ObjectDirective } from 'vue'
import DeleteConfirmationModal from '~/pages/widgets/DeleteConfirmationModal.vue'
// Interface pour ├®tendre HTMLElement avec _handleClick
// Interface pour étendre HTMLElement avec _handleClick
interface HTMLElementWithHandleClick extends HTMLElement {
_handleClick?: (event: Event) => void
}
@@ -9,8 +9,8 @@ interface HTMLElementWithHandleClick extends HTMLElement {
const deleteConfirmDirective: ObjectDirective<HTMLElementWithHandleClick> = {
mounted(el: HTMLElementWithHandleClick, binding: DirectiveBinding) {
const handleClick = (event: Event) => {
event.stopPropagation() // Emp├¬cher la propagation de l'├®v├®nement
event.preventDefault() // Emp├¬cher l'action par d├®faut
event.stopPropagation() // Empêcher la propagation de l'événement
event.preventDefault() // Empêcher l'action par défaut
const modalApp = createApp({
data() {
@@ -20,7 +20,7 @@ const deleteConfirmDirective: ObjectDirective<HTMLElementWithHandleClick> = {
confirm() {
this.show = false
document.body.removeChild(modalContainer)
binding.value() // Ex├®cuter la fonction de suppression
binding.value() // Exécuter la fonction de suppression
},
cancel() {
this.show = false
@@ -42,7 +42,7 @@ const deleteConfirmDirective: ObjectDirective<HTMLElementWithHandleClick> = {
}
el.addEventListener('click', handleClick)
el._handleClick = handleClick // Stocker la r├®f├®rence pour la suppression ult├®rieure
el._handleClick = handleClick // Stocker la référence pour la suppression ultérieure
},
unmounted(el: HTMLElementWithHandleClick) {
if (el._handleClick) {
+3 -3
View File
@@ -6,7 +6,7 @@ const formatPrice = (value: string): string => {
return Number.isNaN(numberValue) ? '' : numberValue.toFixed(2).replace('.', ',')
}
// Interface pour ├®tendre HTMLInputElement avec _handleBlur
// Interface pour étendre HTMLInputElement avec _handleBlur
interface HTMLInputElementWithHandleBlur extends HTMLInputElement {
_handleBlur?: () => void
}
@@ -15,10 +15,10 @@ const priceDirective: ObjectDirective<HTMLInputElementWithHandleBlur> = {
mounted(el: HTMLInputElementWithHandleBlur) {
const handleBlur = () => {
el.value = formatPrice(el.value)
el.dispatchEvent(new Event('input')) // Pour mettre à jour le v-model
el.dispatchEvent(new Event('input')) // Pour mettre à jour le v-model
}
el.addEventListener('blur', handleBlur)
el._handleBlur = handleBlur // Stocker la r├®f├®rence pour la suppression ult├®rieure
el._handleBlur = handleBlur // Stocker la référence pour la suppression ultérieure
},
unmounted(el: HTMLInputElementWithHandleBlur) {
if (el._handleBlur) {
+3 -3
View File
@@ -33,7 +33,7 @@ const validationErrorDirective: ObjectDirective<HTMLElement, string | null> = {
const vnode = createVNode(ValidationError, { message: errorMessage })
render(vnode, container)
// Force reflow pour d├®clencher la transition
// Force reflow pour déclencher la transition
void container.offsetWidth
container.classList.remove('w-0', 'opacity-0')
container.classList.add('w-full', 'opacity-100')
@@ -77,7 +77,7 @@ const validationErrorDirective: ObjectDirective<HTMLElement, string | null> = {
const vnode = createVNode(ValidationError, { message: errorMessage })
render(vnode, newContainer)
// Force reflow pour d├®clencher la transition
// Force reflow pour déclencher la transition
void newContainer.offsetWidth
newContainer.classList.remove('w-0', 'opacity-0')
newContainer.classList.add('w-100', 'opacity-100')
@@ -92,7 +92,7 @@ const validationErrorDirective: ObjectDirective<HTMLElement, string | null> = {
container.classList.remove('transition-all', 'duration-2500', 'ease-in-out') // Supprimez les classes de transition pour la disparition
container.classList.add('w-0', 'opacity-0')
// Supprimez le conteneur imm├®diatement sans d├®lai
// Supprimez le conteneur immédiatement sans délai
render(null, container)
container.remove()
;(el as any)._validationErrorContainer = null
+2 -1
View File
@@ -3,8 +3,9 @@ import { reactive } from 'vue'
import { Link } from '@adonisjs/inertia/vue'
import { useForm } from '@inertiajs/vue3'
import CustomSelect from '~/pages/widgets/CustomSelect.vue'
import type { ComponentType } from '~/pages/types/UITypes'
defineProps<{ types: any[]; errors?: Record<string, string> }>()
defineProps<{ types: ComponentType[]; errors?: Record<string, string> }>()
const form = reactive({
name: '',
price: 0,
+2 -1
View File
@@ -1,7 +1,8 @@
<script setup lang="ts">
import { Link } from '@adonisjs/inertia/vue'
import type { Component } from '~/pages/types/UITypes'
const props = defineProps<{ component: any }>()
const props = defineProps<{ component: Component }>()
</script>
<template>
+5 -4
View File
@@ -2,11 +2,12 @@
import { reactive } from 'vue'
import { Link } from '@adonisjs/inertia/vue'
import { router, usePage } from '@inertiajs/vue3'
import type { Component, Computer, ComputerState } from '~/pages/types/UITypes'
const { props } = usePage<{
computer: any
availableComponents: any[]
states: any[]
computer: Computer
availableComponents: Component[]
states: ComputerState[]
errors?: Record<string, string>
}>()
const { computer, states } = props
@@ -19,7 +20,7 @@ const form = reactive({
cpuScore: computer.cpuScore,
globalScore: computer.globalScore,
stateId: computer.state.id,
componentsId: computer.components.map((c: any) => c.id),
componentsId: computer.components.map((c) => c.id),
})
function deleteComputer(id: number) {
+2 -1
View File
@@ -1,7 +1,8 @@
<script setup lang="ts">
import { Link } from '@adonisjs/inertia/vue'
import type { Computer } from '~/pages/types/UITypes'
defineProps<{ computers: any[] }>()
defineProps<{ computers: Computer[] }>()
</script>
<template>
+2 -1
View File
@@ -1,7 +1,8 @@
<script setup lang="ts">
import { Link } from '@adonisjs/inertia/vue'
import type { Computer } from '~/pages/types/UITypes'
const { computer } = defineProps<{ computer: any }>()
const { computer } = defineProps<{ computer: Computer }>()
</script>
<template>
+2 -5
View File
@@ -1,10 +1,10 @@
interface ComponentType {
export type ComponentType = {
id: number
name: string
unique: boolean
}
interface ComputerState {
export type ComputerState = {
id: number
name: string
}
@@ -26,10 +26,7 @@ export type Component = {
id: number
name: string
price: string
componentTypeId: number
computerId: number | null
userId: number
componentStateId: number
purchaseDate: string
createdAt: string
updatedAt: string
+2 -2
View File
@@ -30,7 +30,7 @@ const componentTypeOrder = [
'Other',
]
// Fusionner les composants disponibles et initialement s├®lectionn├®s
// Fusionner les composants disponibles et initialement sélectionnés
const allComponents = computed(() => {
const mergedComponents = [...props.availableComponents]
if (props.initialSelectedComponents) {
@@ -134,7 +134,7 @@ function updateComponents(event: Event, type: string) {
</select>
</div>
<div v-else>
<!-- Utilisation de cases à cocher pour les composants non uniques avec grille responsive -->
<!-- Utilisation de cases à cocher pour les composants non uniques avec grille responsive -->
<div class="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4">
<div v-for="component in components" :key="component.id" class="flex items-center mt-1">
<input
@@ -46,7 +46,7 @@ const componentTypeOrder = [
'Other',
]
// Trier les composants selon l'ordre sp├®cifi├®
// Trier les composants selon l'ordre spécifié
const sortedComponents = computed(() => {
return components.slice().sort((a, b) => {
return componentTypeOrder.indexOf(a.type.name) - componentTypeOrder.indexOf(b.type.name)
@@ -67,7 +67,7 @@ function getIconPath(typeName: string): string {
'WiFi-Card': wiFiCard,
'Case': pcCase,
}
return typeToIconMap[typeName] || defaultIcon // Utilisez une ic├┤ne par d├®faut si n├®cessaire
return typeToIconMap[typeName] || defaultIcon // Utilisez une icône par défaut si nécessaire
}
</script>
@@ -106,7 +106,7 @@ function getIconPath(typeName: string): string {
</td>
<td v-if="isAdminActive" class="px-4 py-2 border-b border-gray-400 text-right">
Ôé¼{{ component.price }}
€{{ component.price }}
</td>
<td class="px-4 py-2 border-b border-gray-400 flex justify-center items-center">
<Link
@@ -125,7 +125,7 @@ function getIconPath(typeName: string): string {
<tr class="bg-gray-800 text-white">
<td class="px-4 py-2 border-b border-gray-400 whitespace-nowrap" colspan="3">Total</td>
<td class="px-4 py-2 border-b border-gray-400 whitespace-nowrap text-right">
Ôé¼{{ cost.toFixed(2) }}
€{{ cost.toFixed(2) }}
</td>
</tr>
</tfoot>
+1 -1
View File
@@ -165,7 +165,7 @@ function handleDragEnter(index: number) {
const sourceArray =
dragIndex.value < serverImages.value.length ? serverImages.value : newImages.value
const draggedItem = sourceArray.splice(dragIndex.value, 1)[0]
sourceArray.splice(index, 0, draggedItem as any) // Utilisation de 'as any' pour r├®soudre le probl├¿me de typage
sourceArray.splice(index, 0, draggedItem as any) // Utilisation de 'as any' pour résoudre le problème de typage
dragIndex.value = index
}
}
+2 -2
View File
@@ -38,7 +38,7 @@ function getIconPath(typeName: string): string {
'WiFi-Card': wiFiCard,
'Case': pcCase,
}
return typeToIconMap[typeName] || defaultIcon // Utilisez une ic├┤ne par d├®faut si n├®cessaire
return typeToIconMap[typeName] || defaultIcon // Utilisez une icône par défaut si nécessaire
}
function goToDetail(id: number) {
@@ -105,7 +105,7 @@ function deleteComponent(id: number) {
{{ component.name }}
</td>
<td v-if="isAdminActive" class="px-6 py-4 border-b border-gray-200 whitespace-nowrap">
Ôé¼{{ component.price }}
€{{ component.price }}
</td>
<td class="px-6 py-4 border-b border-gray-200 whitespace-nowrap">
{{ component.purchaseDate }}