Compare commits

...

10 Commits

Author SHA1 Message Date
gitea-actions b59d0f8a44 chore: bump version to v0.1.29
Build & Push Docker Image / build (push) Failing after 16s
Auto Tag Develop / tag (push) Successful in 5s
2026-04-14 13:12:49 +00:00
Matthieu 5cb8cff4ce Merge branch 'feature/ERP-7-mise-en-place-du-modular-monolith' into develop
Auto Tag Develop / tag (push) Has been cancelled
# Conflicts:
#	docker-compose.yml
2026-04-14 15:11:59 +02:00
gitea-actions c62f054da1 chore: bump version to v0.1.28
Auto Tag Develop / tag (push) Successful in 5s
Build & Push Docker Image / build (push) Successful in 53s
2026-04-14 13:07:45 +00:00
Matthieu 168dad4657 feat(infra) : add logs volume to prod docker-compose
Persist var/log/ via named volume coltura_logs so logs survive
container restarts.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 14:28:09 +02:00
Matthieu 68bdb6ff72 docs : add code review report for PR #1
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 14:16:33 +02:00
Matthieu 7045debc66 feat : add ESLint linter to frontend with pre-commit hook
Add ESLint with @nuxt/eslint-config enforcing 4-space indentation.
Add make nuxt-lint and nuxt-lint-fix targets.
Add ESLint check to pre-commit hook (lint only, no auto-fix).
Fix auth.vue indentation from 2 to 4 spaces.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 14:16:25 +02:00
Matthieu 180bc5c556 fix : fix UserOutput type and use UserRepositoryInterface in CreateUserCommand
Change UserOutput.id from int to ?int to match User::getId() return type.
Replace EntityManagerInterface with UserRepositoryInterface in CreateUserCommand.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 14:16:14 +02:00
Matthieu 999cccabaf fix : reset sidebar state on logout
Add resetSidebar() to useSidebar composable and call it on logout
to prevent stale sidebar data after re-login.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 14:16:03 +02:00
Matthieu d42311f22f docs : update ports and fix CHANGELOG project name
Update CLAUDE.md to reflect actual ports (PG 5437, frontend 3004).
Fix CHANGELOG.md header from "Ferme" to "Coltura".

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 14:15:53 +02:00
Matthieu be57451d29 fix : change frontend dev port from 3003 to 3004 to avoid conflicts
Auto Tag Develop / tag (push) Has been cancelled
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 11:25:17 +02:00
17 changed files with 530 additions and 160 deletions
+6 -3
View File
@@ -1,10 +1,13 @@
# Changelog
Liste des évolutions du projet Ferme
Liste des évolutions du projet Coltura
## [0.0.0]
### Parameters
Ajouter dans le fichier .env
- DEFAULT_URI
- DATABASE_URL
- PONT_BASCULE_BYPASS (doit être à true en dev)
@@ -15,13 +18,13 @@ Ajouter dans le fichier .env
- COOKIE_SECURE=0 (en dev 0 et en prod 1)
Ajouter dans le fichier .env du frontend
- NUXT_PUBLIC_API_BASE
### Added
* [#ERP-7] Mise en place du modular monolith
- [#ERP-7] Mise en place du modular monolith
### Changed
### Fixed
+4 -3
View File
@@ -148,6 +148,7 @@ Le code du module Commercial n'est pas touche.
- Le layout `default.vue` itere sur les sections retournees par l'API, applique `t()` sur les labels
- Middleware `auth.global.ts` charge la sidebar apres authentification
- Middleware `modules.global.ts` redirige si la route demandee est dans `disabledRoutes`
- Les composables avec state singleton (refs module-level) doivent exposer une fonction `reset*()` et etre reinitialises au logout (ex: `useSidebar().resetSidebar()`)
- **Interdit** : `.module.ts`, `modules-loader.ts`, hardcode de la sidebar, edition manuelle de `extends` dans `nuxt.config.ts`
## Stack
@@ -155,7 +156,7 @@ Le code du module Commercial n'est pas touche.
- **Backend** : PHP 8.4, Symfony 8.0, API Platform 4, Doctrine ORM, PostgreSQL 16
- **Frontend** : Nuxt 4 (SSR off / SPA), Vue 3, Pinia, Tailwind CSS, @malio/layer-ui, nuxt-toast, @nuxtjs/i18n, @nuxt/icon
- **Auth** : JWT HTTP-only cookie (lexik/jwt-authentication-bundle), login a `/login_check`, cookie `BEARER`
- **Docker** : PHP-FPM + Node 24, Nginx (port 8083), PostgreSQL (port 5436)
- **Docker** : PHP-FPM + Node 24, Nginx (port 8083), PostgreSQL (port 5437)
## Commandes
@@ -165,7 +166,7 @@ make stop # Arreter les containers
make restart # Redemarrer les containers
make install # Install complet (composer, migrations, fixtures, build Nuxt)
make reset # Tout supprimer et reinstaller (supprime la BDD)
make dev-nuxt # Dev server Nuxt (hot reload, port 3003)
make dev-nuxt # Dev server Nuxt (hot reload, port 3004)
make shell # Shell dans le container PHP
make shell-root # Shell root dans le container PHP
make cache-clear # Vider le cache Symfony
@@ -252,7 +253,7 @@ Exemples : `feat : add login page`, `fix(auth) : prevent null token crash`
- Container PHP : `php-coltura-fpm`
- Container Nginx : `nginx-coltura`
- Container DB : PostgreSQL sur port **5436** (interne et externe)
- Container DB : PostgreSQL sur port **5437** (interne et externe)
- Config Docker dev : `infra/dev/.env.docker` (override local : `infra/dev/.env.docker.local`)
- Config Docker prod : `infra/prod/` (Dockerfile multi-stage, docker-compose.prod.yml)
- Apres modif nginx : `docker restart nginx-coltura`
+1 -1
View File
@@ -1,2 +1,2 @@
parameters:
app.version: '0.1.27'
app.version: '0.1.29'
+267
View File
@@ -0,0 +1,267 @@
# Code Review — PR #1 [ERP-7] Mise en place du modular monolith
**Branche** : `feature/ERP-7-mise-en-place-du-modular-monolith``develop`
**Auteur** : Tristan
**Date de review** : 2026-04-09
**Scope** : 55 commits, 85 fichiers modifiés, ~30 000 lignes ajoutées
---
## Résumé de la PR
Cette PR restructure Coltura (CRM/ERP) en **architecture modulaire DDD** (Domain-Driven Design) :
- **Backend** : introduction de bounded contexts (`Module/Core`, `Module/Commercial`) avec séparation Domain / Application / Infrastructure
- **Shared** : couche partagée (events, value objects, contracts, bus interfaces)
- **Modules activables** : `config/modules.php` comme source de vérité, activation/désactivation sans toucher au code
- **Sidebar dynamique** : `config/sidebar.php` déclare la navigation, le backend filtre selon les modules actifs, le frontend consomme via `/api/sidebar`
- **Frontend** : réorganisation en `app/` (shell), `shared/` (composables, stores, types), `modules/` (Nuxt layers auto-détectés)
- **Infra** : migration Doctrine, Dockerfile prod, deploy.sh, nginx-proxy, maintenance mode
---
## Issues trouvées
### Issue 1 — CHANGELOG mentionne le mauvais projet
| | |
|---|---|
| **Sévérité** | Critique |
| **Fichier** | `CHANGELOG.md`, ligne 3 |
| **Confiance** | 100/100 |
**Constat** : Le header du CHANGELOG dit :
```
Liste des évolutions du projet Ferme
```
Ce fichier appartient à **Coltura**, pas au projet Ferme. C'est une erreur de copier-coller lors du scaffolding initial.
**Correction** : Remplacer "Ferme" par "Coltura".
---
### Issue 2 — Sidebar link `/suppliers` pointe vers une page inexistante (404)
| | |
|---|---|
| **Sévérité** | Majeure |
| **Fichier** | `config/sidebar.php`, ligne 49 |
| **Confiance** | 75/100 (confirmé par 3 agents indépendants) |
**Constat** : La sidebar déclare un lien vers `/suppliers` pour le module commercial :
```php
// config/sidebar.php
[
'label' => 'sidebar.commercial.suppliers',
'icon' => 'i-heroicons-truck',
'to' => '/suppliers', // ← route inexistante
'module' => 'commercial',
],
```
Mais la seule page du module commercial est `frontend/modules/commercial/pages/commercial.vue`, que Nuxt mappe sur la route `/commercial` (pas `/suppliers`).
**Impact** : Cliquer sur ce lien dans la sidebar donnera un 404.
**Correction** : Soit renommer la page en `suppliers.vue`, soit changer le `'to'` en `'/commercial'`.
---
### Issue 3 — Port PostgreSQL changé de 5436 à 5437
| | |
|---|---|
| **Sévérité** | Majeure |
| **Fichier** | `infra/dev/.env.docker` |
| **Règle violée** | Workspace `CLAUDE.md` : "Coltura — 8083 / 3003 / **5436**" |
| **Confiance** | 75/100 |
**Constat** : Le fichier `.env.docker` définit `POSTGRES_PORT=5437`, alors que le port documenté pour Coltura est `5436`.
**Impact** : Tout développeur qui suit les ports documentés (ou qui utilise des scripts basés sur ces ports) ne pourra pas se connecter à la base.
**Correction** : Revenir à `5436` ou mettre à jour les CLAUDE.md (workspace + projet).
---
### Issue 4 — Port frontend changé de 3003 à 3004 sans mise à jour de la documentation
| | |
|---|---|
| **Sévérité** | Majeure |
| **Fichiers** | `frontend/nuxt.config.ts` (ligne 40), `docker-compose.yml` (ligne 33) |
| **Règle violée** | Workspace `CLAUDE.md` : "Coltura — 8083 / **3003** / 5436" et `CLAUDE.md` projet : "make dev-nuxt # port 3003" |
| **Confiance** | 75/100 (confirmé par 3 agents indépendants) |
**Constat** :
```typescript
// nuxt.config.ts
devServer: { port: 3004 } // était 3003
```
```yaml
# docker-compose.yml
ports:
- "3004:3004" # était 3004:3003
```
Les deux `CLAUDE.md` documentent toujours le port 3003.
**Correction** : Mettre à jour les deux CLAUDE.md pour refléter le nouveau port, ou revenir à 3003.
---
### Issue 5 — `useSidebar` : state singleton jamais réinitialisé au logout
| | |
|---|---|
| **Sévérité** | Majeure |
| **Fichier** | `frontend/shared/composables/useSidebar.ts`, lignes 3-5 |
| **Confiance** | 75/100 |
**Constat** : Les refs `sections`, `disabledRoutes` et `loaded` sont déclarées au niveau module (en dehors de la fonction composable) :
```typescript
const sections = ref<SidebarSection[]>([])
const disabledRoutes = ref<string[]>([])
const loaded = ref(false)
```
Ce sont des singletons partagés sur toute la durée de vie de l'app. Après un logout + re-login :
1. `loaded.value` reste `true`
2. `loadSidebar()` n'est jamais rappelé
3. La sidebar affiche les données de la session précédente
Le middleware `auth.global.ts` ne recharge que si `!loaded.value`, et `logout.vue` ne reset jamais `loaded`.
**Impact** : Sidebar périmée après re-connexion. Si les modules changent côté serveur, le frontend ne le saura jamais sans un hard refresh.
**Correction** : Ajouter une fonction `resetSidebar()` appelée au logout, ou conditionner le rechargement autrement (ex: toujours recharger après login).
---
### Issue 6 — `UserOutput` DTO : type mismatch `int` vs `?int` + dead code
| | |
|---|---|
| **Sévérité** | Moyenne |
| **Fichier** | `src/Module/Core/Application/DTO/UserOutput.php`, lignes 13 et 23 |
| **Confiance** | 75/100 |
**Constat** :
```php
// Le constructeur attend un int non-nullable
public function __construct(
public int $id, // ← int, pas ?int
// ...
)
// Mais User::getId() retourne ?int
public static function fromEntity(User $user): self
{
return new self(
id: $user->getId(), // ← peut être null
// ...
);
}
```
Avec `declare(strict_types=1)`, passer `null` à un paramètre `int` lève un `TypeError`.
**De plus** : Ce DTO n'est utilisé nulle part. `MeProvider` retourne directement l'entité `User` via `$this->security->getUser()`. Le DTO est du dead code.
**Correction** : Soit utiliser le DTO dans `MeProvider` (comme l'architecture le prévoit), soit le supprimer. Dans tous les cas, changer `int $id` en `?int $id`.
---
### Issue 7 — `CreateUserCommand` contourne `UserRepositoryInterface`
| | |
|---|---|
| **Sévérité** | Moyenne |
| **Fichier** | `src/Module/Core/Infrastructure/Console/CreateUserCommand.php`, lignes 22-27 |
| **Règle violée** | `CLAUDE.md` projet : "les repositories sont des interfaces" |
| **Confiance** | 75/100 (confirmé par 2 agents indépendants) |
**Constat** :
```php
public function __construct(
private EntityManagerInterface $em, // ← injection directe de Doctrine
private UserPasswordHasherInterface $hasher,
) {}
// ...
$this->em->persist($user);
$this->em->flush();
```
Le `UserRepositoryInterface::save(User $user)` existe et est implémenté par `DoctrineUserRepository`. La commande devrait l'utiliser :
```php
// Correction attendue
public function __construct(
private UserRepositoryInterface $userRepository,
private UserPasswordHasherInterface $hasher,
) {}
// ...
$this->userRepository->save($user);
```
**Impact** : Viole le pattern DDD introduit dans cette même PR et crée un second chemin de persistance non contrôlé.
---
### Issue 8 — `auth.vue` : indentation 2 espaces au lieu de 4
| | |
|---|---|
| **Sévérité** | Mineure |
| **Fichier** | `frontend/app/layouts/auth.vue` |
| **Règle violée** | `CLAUDE.md` projet : "4 espaces d'indentation" |
| **Confiance** | 75/100 |
**Constat** : Le fichier utilise 2 espaces d'indentation :
```vue
<template>
<div class="min-h-screen">
<slot />
</div>
</template>
```
Tous les autres fichiers Vue de la PR (`default.vue`, `login.vue`, `index.vue`, `logout.vue`, `commercial.vue`) utilisent correctement 4 espaces.
**Correction** : Passer en 4 espaces.
---
## Issues mineures non retenues (score < 75)
Pour information, ces points ont été identifiés mais jugés moins critiques :
- **`/api/sidebar` et `/api/modules` en PUBLIC_ACCESS** : intentionnel selon le CLAUDE.md qui documente ces endpoints comme publics
- **`doctrine.yaml` ne mappe que le module Core** : les entités de futurs modules ne seront pas détectées automatiquement (à documenter)
- **Middleware `modules.global.ts`** : boucle de redirection infinie possible si `/` est dans `disabledRoutes` (requiert une mauvaise config)
- **Lien sidebar `/admin`** : pointe vers une page inexistante (pré-existant sur develop)
- **Labels hardcodés en français dans `login.vue`** : `"Nom d'utilisateur"`, `"Mot de passe"`, `"Se connecter"` au lieu de `$t('auth.username')` etc. (les clés i18n existent dans `fr.json`)
---
## Verdict
La PR pose de bonnes bases architecturales (DDD, modules activables, sidebar dynamique). Les issues principales sont :
- **2 bugs fonctionnels** : sidebar `/suppliers` en 404 et state `useSidebar` jamais reset
- **3 incohérences config/doc** : ports PG et frontend changés sans MAJ CLAUDE.md, CHANGELOG mauvais projet
- **2 incohérences architecturales** : `CreateUserCommand` et `UserOutput` ne respectent pas les patterns introduits dans la PR
- **1 style** : indentation auth.vue
Aucun de ces problèmes ne bloque le merge mais les 2 bugs fonctionnels (issues 2 et 5) devraient être corrigés avant.
+5 -5
View File
@@ -1,7 +1,7 @@
<template>
<div class="min-h-screen bg-tertiary-500 from-tertiary-500 via-white to-neutral-100 text-neutral-900">
<main class="mx-auto flex min-h-screen w-full max-w-[720px] items-center px-6 py-12">
<slot />
</main>
</div>
<div class="min-h-screen bg-tertiary-500 from-tertiary-500 via-white to-neutral-100 text-neutral-900">
<main class="mx-auto flex min-h-screen w-full max-w-[720px] items-center px-6 py-12">
<slot />
</main>
</div>
</template>
+3 -2
View File
@@ -16,8 +16,9 @@
<div class="h-full flex-1 flex flex-col min-h-0 min-w-0">
<main
class="flex flex-1 flex-col overflow-y-auto overflow-x-hidden bg-white px-4 pb-24 sm:px-8 lg:px-16">
<div aria-hidden="true"
class="pointer-events-none sticky top-0 z-30 h-8 flex-shrink-0 bg-white sm:h-12"/>
<div
aria-hidden="true"
class="pointer-events-none sticky top-0 z-30 h-8 flex-shrink-0 bg-white sm:h-12"/>
<slot/>
</main>
</div>
+60
View File
@@ -0,0 +1,60 @@
import nuxt from '@nuxt/eslint-config'
export default await nuxt(
{
features: {
stylistic: false,
typescript: true,
nuxt: {
sortConfigKeys: false,
},
},
dirs: {
root: ['.', './app'],
},
},
{
name: 'coltura/custom-overrides',
rules: {
// Indentation 4 espaces (convention CLAUDE.md)
'vue/html-indent': ['error', 4],
indent: ['error', 4, { SwitchCase: 1 }],
// Vue — relaxed
'vue/multi-word-component-names': 'off',
'vue/no-multiple-template-root': 'off',
'vue/require-default-prop': 'off',
'vue/html-self-closing': 'off',
'vue/singleline-html-element-content-newline': 'off',
'vue/multiline-html-element-content-newline': 'off',
'vue/attributes-order': 'off',
'vue/v-on-event-hyphenation': 'off',
// Console — allow console.error only
'no-console': ['warn', { allow: ['error'] }],
// Unused vars — warn, ignore underscore-prefixed
'no-unused-vars': 'off',
'@typescript-eslint/no-unused-vars': ['warn', {
argsIgnorePattern: '^_',
varsIgnorePattern: '^_',
caughtErrorsIgnorePattern: '^_',
}],
// TypeScript — progressive strictness
'@typescript-eslint/no-explicit-any': 'warn',
'@typescript-eslint/no-dynamic-delete': 'off',
'@typescript-eslint/no-invalid-void-type': 'off',
// Formatting — leave to stylistic tools
'require-await': 'off',
'comma-dangle': 'off',
curly: 'off',
semi: 'off',
quotes: 'off',
'no-trailing-spaces': 'off',
'no-multiple-empty-lines': 'off',
'no-irregular-whitespace': 'off',
},
},
)
+12 -12
View File
@@ -1,10 +1,10 @@
<template>
<div class="mx-auto w-full max-w-lg">
<span
class="flex items-center justify-center bg-white text-xl font-bold uppercase text-primary-500 p-4"
>
<img src="/LOGO_MALIO.png" alt="Logo" class="w-[150px]"/>
</span>
<span
class="flex items-center justify-center bg-white text-xl font-bold uppercase text-primary-500 p-4"
>
<img src="/LOGO_MALIO.png" alt="Logo" class="w-[150px]"/>
</span>
<form
class="mt-8 space-y-6 rounded-lg border border-neutral-200 bg-white p-6 shadow-sm"
@submit.prevent="handleSubmit"
@@ -13,16 +13,16 @@
label="Nom d'utilisateur"
autocomplete="username"
group-class="mt-0"
inputClass="w-full"
input-class="w-full"
v-model="username"
/>
<MalioInputPassword
v-model="password"
label="Mot de passe"
autocomplete="current-password"
inputClass="w-full"
/>
<MalioInputPassword
v-model="password"
label="Mot de passe"
autocomplete="current-password"
input-class="w-full"
/>
<MalioButton
label="Se connecter"
+2
View File
@@ -8,9 +8,11 @@
definePageMeta({ layout: 'auth' })
const auth = useAuthStore()
const { resetSidebar } = useSidebar()
onMounted(async () => {
await auth.logout()
resetSidebar()
await navigateTo('/login')
})
</script>
+11 -1
View File
@@ -8,7 +8,9 @@
"generate": "nuxt generate",
"preview": "nuxt preview",
"postinstall": "nuxt prepare",
"build:dist": "nuxt generate && rm -rf dist && cp -R .output/public dist"
"build:dist": "nuxt generate && rm -rf dist && cp -R .output/public dist",
"lint": "eslint .",
"lint:fix": "eslint . --fix"
},
"dependencies": {
"@malio/layer-ui": "^1.2.3",
@@ -21,5 +23,13 @@
"pinia": "^3.0.4",
"vue": "^3.5.29",
"vue-router": "^4.6.4"
},
"devDependencies": {
"@nuxt/eslint-config": "^1.9.0",
"@typescript-eslint/eslint-plugin": "^8.44.1",
"@typescript-eslint/parser": "^8.44.1",
"eslint": "^9.36.0",
"eslint-plugin-vue": "^10.5.0",
"vue-eslint-parser": "^10.2.0"
}
}
+128 -128
View File
@@ -1,5 +1,5 @@
import type { FetchOptions } from 'ofetch'
import { $fetch, FetchError } from 'ofetch'
import type { FetchOptions , FetchError } from 'ofetch'
import { $fetch } from 'ofetch'
export type AnyObject = Record<string, unknown>
@@ -25,178 +25,178 @@ export type ApiFetchOptions<ResponseType extends 'json' | 'blob'> =
let isHandlingUnauthorized = false
export function useApi(): ApiClient {
const config = useRuntimeConfig()
const baseURL = config.public.apiBase || '/api'
const toast = useToast()
const auth = useAuthStore()
const nuxtApp = useNuxtApp()
const i18n = nuxtApp.$i18n as
const config = useRuntimeConfig()
const baseURL = config.public.apiBase || '/api'
const toast = useToast()
const auth = useAuthStore()
const nuxtApp = useNuxtApp()
const i18n = nuxtApp.$i18n as
| {
t: (key: string) => string
te?: (key: string) => boolean
}
| undefined
const t = (key: string) => (i18n?.t ? String(i18n.t(key)) : key)
const te = (key: string) => (i18n?.te ? i18n.te(key) : false)
const t = (key: string) => (i18n?.t ? String(i18n.t(key)) : key)
const te = (key: string) => (i18n?.te ? i18n.te(key) : false)
function extractErrorMessage(error: unknown, responseData?: unknown): string {
const data = responseData ?? (error as FetchError)?.data
function extractErrorMessage(error: unknown, responseData?: unknown): string {
const data = responseData ?? (error as FetchError)?.data
if (typeof data === 'string') {
return data
}
if (typeof data === 'string') {
return data
}
if (data && typeof data === 'object') {
const record = data as Record<string, unknown>
return (
(record['hydra:description'] as string) ||
if (data && typeof data === 'object') {
const record = data as Record<string, unknown>
return (
(record['hydra:description'] as string) ||
(record.detail as string) ||
(record.message as string) ||
(record.error as string) ||
(record.title as string) ||
(record['hydra:title'] as string) ||
''
)
)
}
return (error as FetchError)?.message ?? 'Erreur inconnue.'
}
return (error as FetchError)?.message ?? 'Erreur inconnue.'
}
const methodErrorKeys: Record<string, string> = {
GET: 'errors.http.get',
POST: 'errors.http.post',
PUT: 'errors.http.put',
PATCH: 'errors.http.patch',
DELETE: 'errors.http.delete'
}
const methodErrorKeys: Record<string, string> = {
GET: 'errors.http.get',
POST: 'errors.http.post',
PUT: 'errors.http.put',
PATCH: 'errors.http.patch',
DELETE: 'errors.http.delete'
}
const client = $fetch.create({
baseURL,
retry: 0,
credentials: 'include',
onResponse({ options, response }) {
const apiOptions = options as ApiFetchOptions<'json'>
if (apiOptions?.toast === false) {
return
}
const client = $fetch.create({
baseURL,
retry: 0,
credentials: 'include',
onResponse({ options, response }) {
const apiOptions = options as ApiFetchOptions<'json'>
if (apiOptions?.toast === false) {
return
}
if (response?.status && response.status >= 400) {
return
}
if (response?.status && response.status >= 400) {
return
}
const successKey = apiOptions?.toastSuccessKey
const successMessage =
const successKey = apiOptions?.toastSuccessKey
const successMessage =
apiOptions?.toastSuccessMessage ||
(successKey ? (te(successKey) ? t(successKey) : successKey) : '')
if (successMessage) {
toast.success({
title: 'Succes',
message: successMessage
})
}
},
async onResponseError({ response, error, options }) {
const apiOptions = options as ApiFetchOptions<'json'>
if (response?.status === 401) {
const requestUrl = typeof options?.url === 'string' ? options.url : ''
const isLoginCheck = requestUrl.includes('/login_check')
const isLogout = requestUrl.includes('/logout')
const shouldToast401 = apiOptions?.toastOn401 === true && apiOptions?.toast !== false
if (successMessage) {
toast.success({
title: 'Succes',
message: successMessage
})
}
},
async onResponseError({ response, error, options }) {
const apiOptions = options as ApiFetchOptions<'json'>
if (response?.status === 401) {
const requestUrl = typeof options?.url === 'string' ? options.url : ''
const isLoginCheck = requestUrl.includes('/login_check')
const isLogout = requestUrl.includes('/logout')
const shouldToast401 = apiOptions?.toastOn401 === true && apiOptions?.toast !== false
if (shouldToast401) {
const errorKey = apiOptions?.toastErrorKey
const errorMessage =
if (shouldToast401) {
const errorKey = apiOptions?.toastErrorKey
const errorMessage =
errorKey ? (te(errorKey) ? t(errorKey) : errorKey) : ''
const extractedMessage = extractErrorMessage(error, response?._data)
const message =
const extractedMessage = extractErrorMessage(error, response?._data)
const message =
apiOptions?.toastErrorMessage ||
errorMessage ||
extractedMessage ||
'Une erreur est survenue.'
toast.error({
title: apiOptions?.toastTitle ?? 'Erreur',
message
})
}
toast.error({
title: apiOptions?.toastTitle ?? 'Erreur',
message
})
}
if (!isLoginCheck && !isLogout) {
if (!isHandlingUnauthorized) {
isHandlingUnauthorized = true
auth.clearSession()
await navigateTo('/login')
isHandlingUnauthorized = false
}
}
if (!isLoginCheck && !isLogout) {
if (!isHandlingUnauthorized) {
isHandlingUnauthorized = true
auth.clearSession()
await navigateTo('/login')
isHandlingUnauthorized = false
}
}
return
}
return
}
if (apiOptions?.toast === false) {
return
}
if (apiOptions?.toast === false) {
return
}
const method =
const method =
typeof options?.method === 'string' ? options.method.toUpperCase() : 'GET'
const defaultKey = methodErrorKeys[method]
const defaultMessage =
const defaultKey = methodErrorKeys[method]
const defaultMessage =
defaultKey && te(defaultKey) ? t(defaultKey) : ''
const errorKey = apiOptions?.toastErrorKey
const errorMessage =
const errorKey = apiOptions?.toastErrorKey
const errorMessage =
errorKey ? (te(errorKey) ? t(errorKey) : errorKey) : ''
const extractedMessage = extractErrorMessage(error, response?._data)
const message =
const extractedMessage = extractErrorMessage(error, response?._data)
const message =
apiOptions?.toastErrorMessage ||
errorMessage ||
defaultMessage ||
extractedMessage ||
'Une erreur est survenue.'
toast.error({
title: apiOptions?.toastTitle ?? 'Erreur',
message
})
}
})
toast.error({
title: apiOptions?.toastTitle ?? 'Erreur',
message
})
}
})
function request<T>(
method: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE',
url: string,
options: ApiFetchOptions<'json'> = {}
) {
const needsJsonBody = method === 'POST' || method === 'PUT'
const needsMergePatch = method === 'PATCH'
const isFormData = typeof FormData !== 'undefined' && options.body instanceof FormData
function request<T>(
method: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE',
url: string,
options: ApiFetchOptions<'json'> = {}
) {
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)
const headers = new Headers(options.headers as HeadersInit | undefined)
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')
}
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<T>(url, { ...options, method, headers })
}
return client<T>(url, { ...options, method, headers })
}
return {
get<T>(url: string, query: AnyObject = {}, options: ApiFetchOptions<'json'> = {}) {
return request<T>('GET', url, { ...options, query })
},
post<T>(url: string, body: AnyObject = {}, options: ApiFetchOptions<'json'> = {}) {
return request<T>('POST', url, { ...options, body })
},
put<T>(url: string, body: AnyObject = {}, options: ApiFetchOptions<'json'> = {}) {
return request<T>('PUT', url, { ...options, body })
},
patch<T>(url: string, body: AnyObject = {}, options: ApiFetchOptions<'json'> = {}) {
return request<T>('PATCH', url, { ...options, body })
},
delete<T>(url: string, query: AnyObject = {}, options: ApiFetchOptions<'json'> = {}) {
return request<T>('DELETE', url, { ...options, query })
return {
get<T>(url: string, query: AnyObject = {}, options: ApiFetchOptions<'json'> = {}) {
return request<T>('GET', url, { ...options, query })
},
post<T>(url: string, body: AnyObject = {}, options: ApiFetchOptions<'json'> = {}) {
return request<T>('POST', url, { ...options, body })
},
put<T>(url: string, body: AnyObject = {}, options: ApiFetchOptions<'json'> = {}) {
return request<T>('PUT', url, { ...options, body })
},
patch<T>(url: string, body: AnyObject = {}, options: ApiFetchOptions<'json'> = {}) {
return request<T>('PATCH', url, { ...options, body })
},
delete<T>(url: string, query: AnyObject = {}, options: ApiFetchOptions<'json'> = {}) {
return request<T>('DELETE', url, { ...options, query })
}
}
}
}
@@ -29,11 +29,18 @@ export function useSidebar() {
)
}
function resetSidebar() {
sections.value = []
disabledRoutes.value = []
loaded.value = false
}
return {
sections,
disabledRoutes,
loaded,
loadSidebar,
resetSidebar,
isRouteDisabled,
}
}
+4
View File
@@ -7,6 +7,10 @@ services:
- "8086:80"
volumes:
- ./config/jwt:/var/www/html/config/jwt:ro
- coltura_logs:/var/www/html/var/log
extra_hosts:
- "host.docker.internal:host-gateway"
restart: unless-stopped
volumes:
coltura_logs:
+6
View File
@@ -53,6 +53,12 @@ build-nuxtJS:
dev-nuxt:
$(EXEC_PHP) sh -c "cd frontend && npm run dev"
nuxt-lint:
$(EXEC_PHP) sh -c "cd frontend && npm run lint"
nuxt-lint-fix:
$(EXEC_PHP) sh -c "cd frontend && npm run lint:fix"
delete_built_dir:
CURRENT_UID=$(shell id -u) CURRENT_GID=$(shell id -g) $(DOCKER_COMPOSE) up -d
$(DOCKER) exec -u root $(PHP_CONTAINER) rm -rf vendor/
+10
View File
@@ -24,6 +24,16 @@ else
fi
echo "--- php-cs-fixer pre commit hook finish---"
echo "--- eslint pre commit hook start ---"
make nuxt-lint
ESLINT_RESULT=$?
if [ $ESLINT_RESULT -ne 0 ]; then
echo "ESLint failed. Aborting commit."
exit 1
fi
echo "--- eslint pre commit hook finished ---"
echo "--- phpunit pre commit hook start ---"
make test
PHPUNIT_RESULT=$?
@@ -10,7 +10,7 @@ use DateTimeImmutable;
final readonly class UserOutput
{
public function __construct(
public int $id,
public ?int $id,
public string $username,
/** @var list<string> */
public array $roles,
@@ -5,7 +5,7 @@ declare(strict_types=1);
namespace App\Module\Core\Infrastructure\Console;
use App\Module\Core\Domain\Entity\User;
use Doctrine\ORM\EntityManagerInterface;
use App\Module\Core\Domain\Repository\UserRepositoryInterface;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument;
@@ -22,7 +22,7 @@ use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface;
class CreateUserCommand extends Command
{
public function __construct(
private readonly EntityManagerInterface $em,
private readonly UserRepositoryInterface $userRepository,
private readonly UserPasswordHasherInterface $passwordHasher,
) {
parent::__construct();
@@ -52,8 +52,7 @@ class CreateUserCommand extends Command
$user->setRoles(['ROLE_ADMIN']);
}
$this->em->persist($user);
$this->em->flush();
$this->userRepository->save($user);
$io->success(sprintf('User "%s" created%s.', $username, $input->getOption('admin') ? ' with ROLE_ADMIN' : ''));