Compare commits
28 Commits
main
...
08f4bff358
| Author | SHA1 | Date | |
|---|---|---|---|
| 08f4bff358 | |||
| 50f55d825d | |||
| 92a69c978a | |||
| 234c4eb2cc | |||
| fe19a56983 | |||
| dadd0b69ca | |||
| a548ef8890 | |||
| f597c9aeb5 | |||
| 604294af96 | |||
| 41a4505b4b | |||
| 6c8853e553 | |||
| 4c2e809a35 | |||
| 890da22329 | |||
| 5ab03751e1 | |||
| c1f23c4345 | |||
| 5e255ebf5e | |||
| 206c76c7db | |||
|
|
599b9fcda0 | ||
|
|
6a609fb467 | ||
|
|
7763d02fae | ||
|
|
b9672a6228 | ||
|
|
4244b961fd | ||
|
|
f3f1a9c5e8 | ||
|
|
15a0f2d2be | ||
|
|
86a9a35594 | ||
|
|
e5ab857913 | ||
|
|
2aa2706179 | ||
|
|
41f1db1169 |
40
CHANGELOG-v3.1.6.md
Normal file
40
CHANGELOG-v3.1.6.md
Normal file
@@ -0,0 +1,40 @@
|
||||
# Changelog Version 3.1.6
|
||||
|
||||
## Date: 2025-08-21
|
||||
|
||||
### Améliorations des flux de passages
|
||||
|
||||
#### Interfaces utilisateur
|
||||
- Optimisation de l'affichage des listes de passages
|
||||
- Amélioration de l'ergonomie de navigation
|
||||
- Ajout de filtres avancés pour la recherche de passages
|
||||
- Mise à jour de l'interface responsive mobile
|
||||
|
||||
#### Flux de création
|
||||
- Simplification du processus de création de passage
|
||||
- Validation en temps réel des données saisies
|
||||
- Ajout de modèles de passages prédéfinis
|
||||
- Amélioration de la gestion des erreurs
|
||||
|
||||
#### Flux de consultation
|
||||
- Affichage optimisé des détails de passage
|
||||
- Historique complet des modifications
|
||||
- Export des données en plusieurs formats
|
||||
- Amélioration des performances de chargement
|
||||
|
||||
#### Flux de modification
|
||||
- Interface de modification intuitive
|
||||
- Suivi des changements avec comparaison avant/après
|
||||
- Validation multi-niveaux des modifications
|
||||
- Notifications automatiques des mises à jour
|
||||
|
||||
### Corrections de bugs
|
||||
- Correction de l'affichage sur écrans de petite taille
|
||||
- Résolution des problèmes de synchronisation
|
||||
- Amélioration de la stabilité générale
|
||||
|
||||
### Améliorations techniques
|
||||
- Optimisation des requêtes base de données
|
||||
- Mise en cache des données fréquemment consultées
|
||||
- Amélioration des temps de réponse API
|
||||
- Refactoring du code pour une meilleure maintenabilité
|
||||
111
CONTEXT-AI.md
Normal file → Executable file
111
CONTEXT-AI.md
Normal file → Executable file
@@ -120,36 +120,115 @@
|
||||
|
||||
### Branches GitLab
|
||||
|
||||
- **main/master**: [Production-ready code]
|
||||
- **develop**: [Integration branch for features]
|
||||
- **feature/[feature-name]**: [Feature development]
|
||||
- **bugfix/[bug-name]**: [Bug fixes]
|
||||
- **release/[version]**: [Release preparation]
|
||||
- **main**: Code stable prêt pour la production
|
||||
- **develop**: Branche d'intégration pour les fonctionnalités en cours de développement
|
||||
- **feature/[feature-name]**: Branches de développement pour les nouvelles fonctionnalités
|
||||
- Exemple: `feature/geolocalisation-casernes` pour l'ajout de la géolocalisation des casernes
|
||||
- **bugfix/[bug-name]**: Branches pour les corrections de bugs
|
||||
- **release/[version]**: Branches de préparation des versions
|
||||
|
||||
### Processus de Merge Request
|
||||
|
||||
1. [Créer une branche à partir de develop]
|
||||
2. [Développer la fonctionnalité/correction]
|
||||
3. [Soumettre une MR vers develop]
|
||||
4. [Code review]
|
||||
5. [CI/CD validation]
|
||||
6. [Merge]
|
||||
1. Créer une branche à partir de `main` ou `develop` selon la nature du changement
|
||||
|
||||
```bash
|
||||
git checkout -b feature/nom-de-la-fonctionnalite main
|
||||
```
|
||||
|
||||
2. Développer la fonctionnalité ou correction avec des commits atomiques
|
||||
|
||||
```bash
|
||||
git add fichier1 fichier2
|
||||
git commit -m "Description claire du changement"
|
||||
```
|
||||
|
||||
3. Pousser la branche vers le dépôt distant
|
||||
|
||||
```bash
|
||||
git push -u origin feature/nom-de-la-fonctionnalite
|
||||
```
|
||||
|
||||
4. Créer une Merge Request via l'interface GitLab ou en utilisant l'URL fournie
|
||||
|
||||
- URL: `http://51.68.36.203/d6soft/geosector/-/merge_requests/new?merge_request%5Bsource_branch%5D=feature/nom-de-la-fonctionnalite`
|
||||
|
||||
5. Attendre la revue de code et les validations CI/CD
|
||||
|
||||
6. Une fois approuvée, fusionner la branche:
|
||||
```bash
|
||||
git checkout main
|
||||
git merge feature/nom-de-la-fonctionnalite
|
||||
git push origin main
|
||||
```
|
||||
|
||||
### CI/CD Pipeline
|
||||
|
||||
[Description de votre pipeline CI/CD dans GitLab]
|
||||
Le projet utilise un pipeline CI/CD GitLab pour automatiser les tests et le déploiement:
|
||||
|
||||
1. **Build**: Compilation du code et vérification de la syntaxe
|
||||
|
||||
- PHP: Vérification de la syntaxe et des dépendances Composer
|
||||
- Flutter: Compilation et génération des assets
|
||||
|
||||
2. **Test**: Exécution des tests automatisés
|
||||
|
||||
- Tests unitaires pour l'API PHP
|
||||
- Tests de widgets pour l'application Flutter
|
||||
|
||||
3. **Deploy**: Déploiement automatique vers les environnements
|
||||
- Déploiement vers DEV après chaque merge dans `develop`
|
||||
- Déploiement vers RECETTE après validation manuelle
|
||||
- Déploiement vers PROD après validation manuelle sur une MR vers `main`
|
||||
|
||||
## Intégration avec GitLab
|
||||
|
||||
### Issues et Kanban
|
||||
|
||||
- **Labels**: [Liste des labels principaux et leur signification]
|
||||
- **Milestones**: [Comment les milestones sont utilisées]
|
||||
- **Boards**: [Description des tableaux Kanban]
|
||||
- **Labels**:
|
||||
|
||||
- `feature`: Nouvelles fonctionnalités
|
||||
- `bug`: Corrections de bugs
|
||||
- `enhancement`: Améliorations de fonctionnalités existantes
|
||||
- `documentation`: Mises à jour de la documentation
|
||||
- `api`: Modifications de l'API
|
||||
- `ui`: Modifications de l'interface utilisateur
|
||||
- `priority:high`: Priorité élevée
|
||||
- `priority:medium`: Priorité moyenne
|
||||
- `priority:low`: Priorité basse
|
||||
|
||||
- **Milestones**:
|
||||
|
||||
- Organisées par versions majeures (1.0, 1.1, etc.)
|
||||
- Chaque milestone contient les issues prévues pour la version
|
||||
- Date d'échéance définie pour chaque milestone
|
||||
|
||||
- **Boards**:
|
||||
- **Backlog**: Issues à traiter dans le futur
|
||||
- **To Do**: Issues prêtes à être développées
|
||||
- **In Progress**: Issues en cours de développement
|
||||
- **Review**: Issues en attente de revue de code
|
||||
- **Done**: Issues terminées et déployées
|
||||
|
||||
### Automatisations
|
||||
|
||||
[Description des automatisations GitLab utilisées]
|
||||
- **Webhooks**: Notifications automatiques dans Slack pour les événements importants
|
||||
|
||||
- Nouvelles Merge Requests
|
||||
- Commentaires sur les MRs
|
||||
- Builds échoués
|
||||
- Déploiements réussis
|
||||
|
||||
- **Merge Request Templates**: Templates prédéfinis pour les MRs avec:
|
||||
|
||||
- Description de la fonctionnalité
|
||||
- Checklist de vérification
|
||||
- Instructions de test
|
||||
- Captures d'écran (si applicable)
|
||||
|
||||
- **CI/CD Automatisé**: Déclenchement automatique des pipelines sur:
|
||||
- Push vers une branche
|
||||
- Création d'une Merge Request
|
||||
- Mise à jour d'une Merge Request
|
||||
|
||||
## Déploiement
|
||||
|
||||
|
||||
BIN
Capture.png
Normal file
BIN
Capture.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 255 KiB |
249
PLANNING-STRIPE-ADMIN.md
Normal file
249
PLANNING-STRIPE-ADMIN.md
Normal file
@@ -0,0 +1,249 @@
|
||||
# PLANNING STRIPE - TÂCHES ADMINISTRATIVES
|
||||
## Intégration Stripe Connect + Terminal pour Calendriers Pompiers
|
||||
### Période : 25/08/2024 - 05/09/2024
|
||||
|
||||
---
|
||||
|
||||
## 📅 LUNDI 25/08 - Préparation (4h)
|
||||
|
||||
### ✅ Compte Stripe Platform
|
||||
- [x] Créer compte Stripe sur https://dashboard.stripe.com/register
|
||||
- [x] Remplir informations entreprise (SIRET, adresse, etc.)
|
||||
- [x] Vérifier email de confirmation
|
||||
- [x] Activer l'authentification 2FA
|
||||
|
||||
### ✅ Activation des produits
|
||||
- [x] Activer Stripe Connect dans Dashboard → Products
|
||||
- [x] Choisir type "Express accounts" pour les amicales
|
||||
- [x] Activer Stripe Terminal dans Dashboard
|
||||
- [x] Demander accès "Tap to Pay on iPhone" via formulaire support
|
||||
|
||||
### ✅ Configuration initiale
|
||||
- [x] Définir les frais de plateforme (DECISION: 0% commission plateforme - 100% pour les amicales)
|
||||
- [x] Configurer les paramètres de virement (J+2 recommandé)
|
||||
- [x] Ajouter logo et branding pour les pages Stripe
|
||||
|
||||
---
|
||||
|
||||
## 📅 MARDI 26/08 - Setup environnements (2h)
|
||||
|
||||
### ✅ Clés API et Webhooks
|
||||
- [x] Récupérer clés TEST (pk_test_... et sk_test_...)
|
||||
- [x] Créer endpoint webhook : https://dapp.geosector.fr/api/stripe/webhook
|
||||
- [x] Sélectionner événements webhook :
|
||||
- `account.updated`
|
||||
- `account.application.authorized`
|
||||
- `payment_intent.succeeded`
|
||||
- `payment_intent.payment_failed`
|
||||
- `charge.dispute.created`
|
||||
- [x] Noter le Webhook signing secret (whsec_...)
|
||||
|
||||
### ✅ Documentation amicales
|
||||
- [x] Préparer template email pour amicales
|
||||
- [x] Créer guide PDF "Activer les paiements CB"
|
||||
- [x] Lister documents requis :
|
||||
- Statuts association
|
||||
- RIB avec IBAN/BIC
|
||||
- Pièce identité responsable
|
||||
- Justificatif adresse siège
|
||||
|
||||
---
|
||||
|
||||
## 📅 MERCREDI 27/08 - Amicale pilote (3h)
|
||||
|
||||
### ✅ Onboarding première amicale
|
||||
- [x] Contacter amicale pilote (Amicale ID: 5)
|
||||
- [x] Créer compte Connect Express via API
|
||||
- [x] Envoyer lien onboarding à l'amicale
|
||||
- [x] Suivre progression dans Dashboard → Connect → Accounts
|
||||
- [x] Vérifier statut "Charges enabled"
|
||||
|
||||
### ✅ Configuration compte amicale
|
||||
- [x] Vérifier informations bancaires (IBAN)
|
||||
- [x] Configurer email notifications
|
||||
- [x] Tester micro-virement de vérification
|
||||
- [x] Noter le compte ID : acct_1S2YfNP63A07c33Y
|
||||
|
||||
---
|
||||
|
||||
## 📅 JEUDI 28/08 - Tests paiements (2h)
|
||||
|
||||
### ✅ Configuration Terminal Test
|
||||
- [x] Créer "Location" test dans Dashboard → Terminal (Location ID: tml_GLJ21w7KCYX4Wj)
|
||||
- [x] Générer reader test virtuel pour Simulator
|
||||
- [x] Configurer les montants de test (10€, 20€, 30€)
|
||||
|
||||
### ✅ Cartes de test
|
||||
- [x] Préparer liste cartes test :
|
||||
- 4242 4242 4242 4242 : Succès
|
||||
- 4000 0000 0000 9995 : Refus
|
||||
- 4000 0025 0000 3155 : Authentification requise
|
||||
- [x] Documenter processus de test pour développeurs
|
||||
|
||||
---
|
||||
|
||||
## 📅 VENDREDI 29/08 - Compliance et sécurité (2h)
|
||||
|
||||
### ✅ Conformité légale
|
||||
- [ ] Vérifier statut PCI DSS (auto-évaluation SAQ A)
|
||||
- [ ] Préparer mentions légales paiement
|
||||
- [ ] Créer template CGV pour paiements
|
||||
- [ ] Documenter process RGPD
|
||||
|
||||
### ✅ Limites et sécurité
|
||||
- [ ] Configurer limites de paiement (max 500€/transaction ?)
|
||||
- [ ] Activer Radar (protection fraude) rules
|
||||
- [ ] Configurer alertes email pour transactions > 100€
|
||||
- [ ] Définir politique remboursements
|
||||
|
||||
---
|
||||
|
||||
## 📅 SAMEDI 30/08 - Monitoring (1h)
|
||||
|
||||
### ✅ Dashboard et alertes
|
||||
- [ ] Créer vues personnalisées Dashboard
|
||||
- [ ] Configurer alertes :
|
||||
- Taux d'échec > 10%
|
||||
- Nouveau litige (chargeback)
|
||||
- Compte amicale suspendu
|
||||
- [ ] Installer app mobile Stripe (iOS/Android)
|
||||
|
||||
---
|
||||
|
||||
## 📅 LUNDI 02/09 - Préparation production (3h)
|
||||
|
||||
### ✅ Amicales supplémentaires
|
||||
- [ ] Onboarder 2-3 amicales test supplémentaires
|
||||
- [ ] Vérifier leurs statuts de compte
|
||||
- [ ] Former les responsables à l'interface Stripe
|
||||
|
||||
### ✅ Documentation finale
|
||||
- [ ] Guide administrateur plateforme
|
||||
- [ ] FAQ amicales (comment voir mes ventes ?)
|
||||
- [ ] Process de support niveau 1
|
||||
|
||||
---
|
||||
|
||||
## 📅 MARDI 03/09 - Tests grandeur nature (2h)
|
||||
|
||||
### ✅ Simulation production
|
||||
- [ ] Paiement test avec vraie carte (sera remboursé)
|
||||
- [ ] Vérifier apparition dans Dashboard amicale
|
||||
- [ ] Tester virement vers compte bancaire
|
||||
- [ ] Vérifier commissions plateforme
|
||||
|
||||
---
|
||||
|
||||
## 📅 MERCREDI 04/09 - Go/No-Go Production (2h)
|
||||
|
||||
### ✅ Checklist production
|
||||
- [ ] Obtenir clés PRODUCTION (pk_live_... et sk_live_...)
|
||||
- [ ] ⚠️ JAMAIS les commiter dans le code
|
||||
- [ ] Configurer webhook production
|
||||
- [ ] Vérifier tous les comptes amicales "enabled"
|
||||
- [ ] Backup des configurations
|
||||
|
||||
### ✅ Plan de bascule
|
||||
- [ ] Planifier fenêtre de maintenance
|
||||
- [ ] Préparer rollback si besoin
|
||||
- [ ] Numéro hotline Stripe : +33 1 88 45 05 35
|
||||
|
||||
---
|
||||
|
||||
## 📅 JEUDI 05/09 - Support jour J (4h)
|
||||
|
||||
### ✅ Surveillance active
|
||||
- [ ] Monitoring Dashboard en temps réel
|
||||
- [ ] Vérifier premiers paiements réels
|
||||
- [ ] Support hotline pour amicales
|
||||
- [ ] Documenter issues rencontrées
|
||||
|
||||
---
|
||||
|
||||
## 📊 RÉCAPITULATIF TEMPS ADMIN
|
||||
- **Préparation** : 4h
|
||||
- **Configuration** : 7h
|
||||
- **Tests** : 4h
|
||||
- **Production** : 6h
|
||||
- **TOTAL** : 21h sur 11 jours
|
||||
|
||||
## 🔐 INFORMATIONS SENSIBLES À STOCKER
|
||||
|
||||
```env
|
||||
# JAMAIS dans le code source !
|
||||
STRIPE_PUBLIC_KEY_TEST=pk_test_...
|
||||
STRIPE_SECRET_KEY_TEST=sk_test_...
|
||||
STRIPE_WEBHOOK_SECRET_TEST=whsec_...
|
||||
|
||||
STRIPE_PUBLIC_KEY_LIVE=pk_live_...
|
||||
STRIPE_SECRET_KEY_LIVE=sk_live_...
|
||||
STRIPE_WEBHOOK_SECRET_LIVE=whsec_...
|
||||
|
||||
STRIPE_PLATFORM_ACCOUNT_ID=acct_...
|
||||
```
|
||||
|
||||
## 📞 CONTACTS UTILES
|
||||
|
||||
- **Support Stripe France** : +33 1 88 45 05 35
|
||||
- **Email support** : support@stripe.com
|
||||
- **Dashboard** : https://dashboard.stripe.com
|
||||
- **Statut système** : https://status.stripe.com
|
||||
|
||||
## ⚠️ POINTS D'ATTENTION CRITIQUES
|
||||
|
||||
1. **NE JAMAIS** partager les clés secrètes (sk_)
|
||||
2. **TOUJOURS** commencer en mode TEST
|
||||
3. **VÉRIFIER** 2x avant passage en LIVE
|
||||
4. Les virements vers comptes amicales prennent 2-7 jours
|
||||
5. Garder 1 personne dispo pour support le jour J
|
||||
6. **Android Tap to Pay** : Vérifier certification SoftPOS des appareils
|
||||
7. **Maintenir** liste des modèles Android certifiés à jour
|
||||
|
||||
---
|
||||
|
||||
## 🎯 BILAN DES ACCOMPLISSEMENTS (01/09/2024)
|
||||
|
||||
### ✅ RÉALISATIONS CLÉS
|
||||
1. **Intégration Stripe Connect complète**
|
||||
- API PHP 8.3 fonctionnelle avec tous les endpoints
|
||||
- Interface Flutter pour gestion Stripe dans l'amicale
|
||||
- Webhooks configurés et testés
|
||||
|
||||
2. **Compte amicale pilote opérationnel**
|
||||
- Amicale ID: 5 avec compte Stripe : acct_1S2YfNP63A07c33Y
|
||||
- Location Terminal créée : tml_GLJ21w7KCYX4Wj
|
||||
- Onboarding Stripe complété avec succès
|
||||
|
||||
3. **Configuration 0% commission plateforme**
|
||||
- 100% des paiements vont aux amicales
|
||||
- Seuls les frais Stripe standard s'appliquent (~1.4% + 0.25€)
|
||||
- Interface UI mise à jour pour refléter cette politique
|
||||
|
||||
4. **Corrections techniques majeures**
|
||||
- Problèmes de déchiffrement des données résolus
|
||||
- Erreurs 502 nginx corrigées (logs debug supprimés)
|
||||
- Base de données et API entièrement fonctionnelles
|
||||
|
||||
### 🔧 PROBLÈMES RÉSOLUS
|
||||
- **Erreur 500** : "Database not found" → Fixed
|
||||
- **Erreur 400** : "Invalid email address" → Fixed (déchiffrement ajouté)
|
||||
- **Erreur 502** : "upstream sent too big header" → Fixed (logs supprimés)
|
||||
- **Commission plateforme** : Supprimée comme demandé (0%)
|
||||
- **UI messaging** : Corrigé pour refléter "100% pour votre amicale"
|
||||
|
||||
### 📊 APIs FONCTIONNELLES
|
||||
- ✅ POST /api/stripe/accounts - Création compte
|
||||
- ✅ GET /api/stripe/accounts/:id/status - Statut compte
|
||||
- ✅ POST /api/stripe/accounts/:id/onboarding-link - Lien onboarding
|
||||
- ✅ POST /api/stripe/locations - Création location Terminal
|
||||
- ✅ POST /api/stripe/webhook - Réception événements
|
||||
|
||||
### 🎯 PROCHAINES ÉTAPES
|
||||
1. Tests de paiements réels avec Terminal
|
||||
2. Déploiement en environnement de recette
|
||||
3. Formation des amicales pilotes
|
||||
4. Monitoring des premiers paiements
|
||||
|
||||
---
|
||||
|
||||
*Document créé le 24/08/2024 - Dernière mise à jour : 01/09/2024*
|
||||
128
api/.vscode/settings.json
vendored
128
api/.vscode/settings.json
vendored
@@ -1,124 +1,22 @@
|
||||
{
|
||||
"window.zoomLevel": 1, // Permet de zoomer, pratique si vous faites une présentation
|
||||
|
||||
// Apparence
|
||||
// -- Editeur
|
||||
"workbench.startupEditor": "none", // On ne veut pas une page d'accueil chargée
|
||||
"editor.minimap.enabled": false, // On veut voir la minimap
|
||||
"editor.minimap.showSlider": "always", // On veut voir la minimap
|
||||
"editor.minimap.size": "fill", // On veut voir la minimap
|
||||
"editor.minimap.scale": 2,
|
||||
"editor.tokenColorCustomizations": {
|
||||
"textMateRules": [
|
||||
{
|
||||
"scope": ["storage.type.function", "storage.type.class"],
|
||||
"settings": {
|
||||
"fontStyle": "bold",
|
||||
"foreground": "#4B9CD3"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"editor.minimap.renderCharacters": true,
|
||||
"editor.minimap.maxColumn": 120,
|
||||
"breadcrumbs.enabled": false,
|
||||
// -- Tabs
|
||||
"workbench.editor.wrapTabs": true, // On veut voir les tabs
|
||||
"workbench.editor.tabSizing": "shrink", // On veut voir les tabs
|
||||
"workbench.editor.pinnedTabSizing": "compact",
|
||||
"workbench.editor.enablePreview": false, // Un clic sur un fichier l'ouvre
|
||||
|
||||
// -- Sidebar
|
||||
"workbench.tree.indent": 15, // Indente plus pour plus de clarté dans la sidebar
|
||||
"workbench.tree.renderIndentGuides": "always",
|
||||
// -- Code
|
||||
"editor.occurrencesHighlight": "singleFile", // On veut voir les occurences d'une variable
|
||||
"editor.renderWhitespace": "trailing", // On ne veut pas laisser d'espace en fin de ligne
|
||||
"editor.renderControlCharacters": true, // On veut voir les caractères de contrôle
|
||||
// Thème
|
||||
"editor.fontFamily": "'JetBrains Mono', 'Fira Code', 'Operator Mono Lig', monospace",
|
||||
"editor.fontLigatures": false,
|
||||
"editor.fontSize": 13,
|
||||
"editor.lineHeight": 22,
|
||||
"editor.guides.bracketPairs": "active",
|
||||
|
||||
// Ergonomie
|
||||
"editor.wordWrap": "off",
|
||||
"editor.rulers": [],
|
||||
"editor.suggest.insertMode": "replace", // L'autocomplétion remplace le mot en cours
|
||||
"editor.acceptSuggestionOnCommitCharacter": false, // Evite que l'autocomplétion soit accepté lors d'un . par exemple
|
||||
"editor.formatOnSave": true,
|
||||
"editor.formatOnPaste": true,
|
||||
"editor.linkedEditing": true, // Quand on change un élément HTML, change la balise fermante
|
||||
"editor.tabSize": 2,
|
||||
"editor.unicodeHighlight.nonBasicASCII": false,
|
||||
|
||||
"[php]": {
|
||||
"editor.defaultFormatter": "bmewburn.vscode-intelephense-client",
|
||||
"editor.formatOnSave": true,
|
||||
"editor.formatOnPaste": true
|
||||
},
|
||||
"intelephense.format.braces": "k&r",
|
||||
"intelephense.format.enable": true,
|
||||
|
||||
"[javascript]": {
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode",
|
||||
"editor.formatOnSave": true,
|
||||
"editor.formatOnPaste": true
|
||||
},
|
||||
"prettier.printWidth": 360,
|
||||
"prettier.semi": true,
|
||||
"prettier.singleQuote": true,
|
||||
"prettier.tabWidth": 2,
|
||||
"prettier.trailingComma": "es5",
|
||||
|
||||
"explorer.autoReveal": false,
|
||||
"explorer.confirmDragAndDrop": false,
|
||||
"emmet.triggerExpansionOnTab": true,
|
||||
|
||||
// Fichiers
|
||||
"files.defaultLanguage": "markdown",
|
||||
"files.autoSaveWorkspaceFilesOnly": true,
|
||||
"files.exclude": {
|
||||
"**/.idea": true
|
||||
},
|
||||
// Languages
|
||||
"javascript.preferences.importModuleSpecifierEnding": "js",
|
||||
"typescript.preferences.importModuleSpecifierEnding": "js",
|
||||
|
||||
// Extensions
|
||||
"tailwindCSS.experimental.configFile": "frontend/tailwind.config.js",
|
||||
"editor.quickSuggestions": {
|
||||
"strings": true
|
||||
},
|
||||
|
||||
"[svelte]": {
|
||||
"editor.defaultFormatter": "svelte.svelte-vscode",
|
||||
"editor.formatOnSave": true
|
||||
},
|
||||
"prettier.documentSelectors": ["**/*.svelte"],
|
||||
"svelte.plugin.svelte.diagnostics.enable": false,
|
||||
"problems.decorations.enabled": false,
|
||||
"js/ts.implicitProjectConfig.checkJs": false,
|
||||
"svelte.enable-ts-plugin": false,
|
||||
"workbench.colorCustomizations": {
|
||||
"activityBar.activeBackground": "#ff6433",
|
||||
"activityBar.background": "#ff6433",
|
||||
"activityBar.foreground": "#15202b",
|
||||
"activityBar.inactiveForeground": "#15202b99",
|
||||
"activityBarBadge.background": "#00ff3d",
|
||||
"activityBarBadge.foreground": "#15202b",
|
||||
"activityBar.activeBackground": "#fa1b49",
|
||||
"activityBar.background": "#fa1b49",
|
||||
"activityBar.foreground": "#e7e7e7",
|
||||
"activityBar.inactiveForeground": "#e7e7e799",
|
||||
"activityBarBadge.background": "#155e02",
|
||||
"activityBarBadge.foreground": "#e7e7e7",
|
||||
"commandCenter.border": "#e7e7e799",
|
||||
"sash.hoverBorder": "#ff6433",
|
||||
"statusBar.background": "#ff3d00",
|
||||
"sash.hoverBorder": "#fa1b49",
|
||||
"statusBar.background": "#dd0531",
|
||||
"statusBar.foreground": "#e7e7e7",
|
||||
"statusBarItem.hoverBackground": "#ff6433",
|
||||
"statusBarItem.remoteBackground": "#ff3d00",
|
||||
"statusBarItem.hoverBackground": "#fa1b49",
|
||||
"statusBarItem.remoteBackground": "#dd0531",
|
||||
"statusBarItem.remoteForeground": "#e7e7e7",
|
||||
"titleBar.activeBackground": "#ff3d00",
|
||||
"titleBar.activeBackground": "#dd0531",
|
||||
"titleBar.activeForeground": "#e7e7e7",
|
||||
"titleBar.inactiveBackground": "#ff3d0099",
|
||||
"titleBar.inactiveBackground": "#dd053199",
|
||||
"titleBar.inactiveForeground": "#e7e7e799"
|
||||
},
|
||||
"peacock.color": "#ff3d00"
|
||||
"peacock.color": "#dd0531"
|
||||
}
|
||||
|
||||
75
api/CLAUDE.md
Executable file
75
api/CLAUDE.md
Executable file
@@ -0,0 +1,75 @@
|
||||
# CLAUDE.md
|
||||
|
||||
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||
|
||||
## Directives importantes
|
||||
- **Langue** : Toujours répondre en français
|
||||
- **Approche de travail** :
|
||||
- Travailler par étapes claires et structurées
|
||||
- TOUJOURS présenter et proposer les modifications avant de les implémenter
|
||||
- Attendre la validation de l'utilisateur avant de modifier le code
|
||||
- Expliquer le problème identifié et la solution proposée
|
||||
- **Vérification du schéma** : TOUJOURS vérifier `docs/geo_app.sql` avant de faire des modifications sur les tables de la base de données
|
||||
|
||||
## Build Commands
|
||||
- Install dependencies: `composer install` - install PHP dependencies
|
||||
- Update dependencies: `composer update` - update PHP dependencies to latest versions
|
||||
- Deploy to REC: `./livre-api.sh rec` - deploy from DVA to RECETTE environment
|
||||
- Deploy to PROD: `./livre-api.sh prod` - deploy from RECETTE to PRODUCTION environment
|
||||
- Export operations: `php export_operation.php` - export operations data
|
||||
|
||||
## Code Architecture
|
||||
This is a PHP 8.3 API without framework, using a custom MVC-like architecture:
|
||||
|
||||
- **Entry point**: `index.php` handles all requests through custom Router
|
||||
- **Core components**:
|
||||
- `Router`: Maps URLs to controller methods, handles HTTP methods
|
||||
- `Database`: PDO wrapper for MariaDB connections
|
||||
- `Session`: Secure session management
|
||||
- `Request/Response`: HTTP request/response handling
|
||||
- **Controllers**: Located in `src/Controllers/`, handle business logic
|
||||
- **Services**: Located in `src/Services/`, provide reusable functionality (logging, email, exports)
|
||||
- **Configuration**: `src/Config/AppConfig.php` - singleton configuration management
|
||||
|
||||
## Key Patterns
|
||||
- No framework dependency - pure PHP 8.3 with composer autoloading
|
||||
- PDO for database access with prepared statements
|
||||
- RESTful API design with JSON responses
|
||||
- CORS handling for cross-origin requests
|
||||
- Session-based authentication
|
||||
- File uploads handled in `uploads/` directory
|
||||
- Logs stored in `logs/` directory
|
||||
|
||||
## Database
|
||||
- MariaDB 10.11 with InnoDB tables
|
||||
- Migration scripts in `scripts/php/migrate_*.php`
|
||||
- Schema comparison tool: `scripts/python/compare_schemas.py`
|
||||
- Database sync: `scripts/cron/sync_databases.php`
|
||||
|
||||
## Security Considerations
|
||||
- Session cookies with httponly, secure flags
|
||||
- CORS configured for specific origins
|
||||
- XSS, clickjacking protection headers
|
||||
- PDO prepared statements for SQL injection prevention
|
||||
- File upload validation in FileService
|
||||
|
||||
## Bonnes pratiques spécifiques
|
||||
|
||||
### Gestion des transactions PDO
|
||||
- Toujours vérifier `$db->inTransaction()` avant d'appeler `rollBack()`
|
||||
- Encadrer les opérations critiques dans des try/catch avec transaction
|
||||
|
||||
### Paramètres SQL
|
||||
- Utiliser des noms de paramètres uniques dans les requêtes SQL
|
||||
- Ne jamais réutiliser le même nom de paramètre plusieurs fois dans une requête
|
||||
- Exemple : `:sector_polygon1`, `:sector_polygon2` au lieu de `:sector_polygon` répété
|
||||
|
||||
### Format des réponses API
|
||||
- Les données doivent être placées à la racine de la réponse JSON, pas dans un groupe "data"
|
||||
- Suivre le modèle de `LoginController` pour la structure des réponses
|
||||
- Retourner des objets complets, pas seulement des IDs (ex: sector complet, pas sector_id)
|
||||
|
||||
### Gestion des sessions
|
||||
- La session stocke `entity_id` depuis `fk_entite` lors du login
|
||||
- Utiliser `Session::getEntityId()` pour récupérer l'ID de l'entité
|
||||
- L'authentification utilise des Bearer tokens contenant le session_id
|
||||
117
api/TODO-API.md
Normal file
117
api/TODO-API.md
Normal file
@@ -0,0 +1,117 @@
|
||||
# TODO-API.md
|
||||
|
||||
## 📋 Liste des tâches à implémenter
|
||||
|
||||
### 🔴 PRIORITÉ HAUTE
|
||||
|
||||
#### 1. Système de backup pour les suppressions (DELETE)
|
||||
**Demandé le :** 20/08/2025
|
||||
**Objectif :** Sauvegarder toutes les données supprimées (soft delete) dans un fichier SQL pour pouvoir les restaurer en cas d'erreur humaine.
|
||||
|
||||
**Détails techniques :**
|
||||
- Créer un système de backup automatique lors de chaque DELETE
|
||||
- Stocker les données dans un fichier SQL avec structure permettant la réintégration facile
|
||||
- Format suggéré : `/backups/deleted/{année}/{mois}/deleted_{table}_{YYYYMMDD}.sql`
|
||||
|
||||
**Tables concernées :**
|
||||
- `ope_pass` (passages) - DELETE /passages/{id}
|
||||
- `users` (utilisateurs) - DELETE /users/{id}
|
||||
- `operations` (opérations) - DELETE /operations/{id}
|
||||
- `ope_sectors` (secteurs) - DELETE /sectors/{id}
|
||||
|
||||
**Structure du backup suggérée :**
|
||||
```sql
|
||||
-- Backup deletion: ope_pass
|
||||
-- Date: 2025-08-20 14:30:45
|
||||
-- User: 9999985 (cv_mobile)
|
||||
-- Entity: 5
|
||||
-- Original ID: 19500576
|
||||
|
||||
INSERT INTO ope_pass_backup (
|
||||
original_id,
|
||||
deleted_at,
|
||||
deleted_by_user_id,
|
||||
deleted_by_entity_id,
|
||||
-- tous les champs originaux
|
||||
fk_operation,
|
||||
fk_sector,
|
||||
fk_user,
|
||||
montant,
|
||||
encrypted_name,
|
||||
encrypted_email,
|
||||
-- etc...
|
||||
) VALUES (
|
||||
19500576,
|
||||
'2025-08-20 14:30:45',
|
||||
9999985,
|
||||
5,
|
||||
-- valeurs originales
|
||||
...
|
||||
);
|
||||
|
||||
-- Pour restauration facile :
|
||||
-- UPDATE ope_pass SET chk_active = 1 WHERE id = 19500576;
|
||||
```
|
||||
|
||||
**Fonctionnalités à implémenter :**
|
||||
1. **Service de backup** : `BackupService.php`
|
||||
- Méthode `backupDeletedRecord($table, $id, $data)`
|
||||
- Génération automatique du SQL de restauration
|
||||
- Rotation des fichiers (garder 90 jours)
|
||||
|
||||
2. **Intégration dans les controllers**
|
||||
- Ajouter l'appel au BackupService avant chaque soft delete
|
||||
- Logger l'emplacement du backup
|
||||
|
||||
3. **Interface de restauration** (optionnel)
|
||||
- Endpoint GET /api/backups/deleted pour lister les backups
|
||||
- Endpoint POST /api/backups/restore/{backup_id} pour restaurer
|
||||
|
||||
4. **Commande de restauration manuelle**
|
||||
- Script PHP : `php scripts/restore_deleted.php --table=ope_pass --id=19500576`
|
||||
|
||||
**Avantages :**
|
||||
- Traçabilité complète des suppressions
|
||||
- Restauration rapide en cas d'erreur
|
||||
- Audit trail pour conformité
|
||||
- Tranquillité d'esprit pour le client
|
||||
|
||||
---
|
||||
|
||||
### 🟡 PRIORITÉ MOYENNE
|
||||
|
||||
#### 2. Amélioration des logs
|
||||
- Ajouter plus de contexte dans les logs
|
||||
- Rotation automatique des logs
|
||||
- Dashboard de monitoring
|
||||
|
||||
#### 3. Optimisation des performances
|
||||
- Cache des requêtes fréquentes
|
||||
- Index sur les tables volumineuses
|
||||
- Pagination optimisée
|
||||
|
||||
---
|
||||
|
||||
### 🟢 PRIORITÉ BASSE
|
||||
|
||||
#### 4. Documentation API
|
||||
- Génération automatique OpenAPI/Swagger
|
||||
- Documentation interactive
|
||||
- Exemples de code pour chaque endpoint
|
||||
|
||||
#### 5. Tests automatisés
|
||||
- Tests unitaires pour les services critiques
|
||||
- Tests d'intégration pour les endpoints
|
||||
- Tests de charge
|
||||
|
||||
---
|
||||
|
||||
## 📝 Notes
|
||||
|
||||
- Les tâches marquées 🔴 doivent être traitées en priorité
|
||||
- Chaque tâche implémentée doit être documentée
|
||||
- Prévoir des tests pour chaque nouvelle fonctionnalité
|
||||
|
||||
---
|
||||
|
||||
**Dernière mise à jour :** 20/08/2025
|
||||
13
api/alter_table_geometry.sql
Normal file
13
api/alter_table_geometry.sql
Normal file
@@ -0,0 +1,13 @@
|
||||
-- Modifier la table pour accepter tous les types de géométries (POLYGON et MULTIPOLYGON)
|
||||
|
||||
-- Option 1 : Modifier la colonne existante (recommandé)
|
||||
ALTER TABLE x_departements_contours
|
||||
MODIFY COLUMN contour GEOMETRY NOT NULL COMMENT 'Géométrie du contour du département (Polygon ou MultiPolygon)';
|
||||
|
||||
-- Vérifier la modification
|
||||
DESCRIBE x_departements_contours;
|
||||
|
||||
-- Option 2 : Si l'option 1 ne fonctionne pas, recréer la colonne
|
||||
-- ALTER TABLE x_departements_contours DROP COLUMN contour;
|
||||
-- ALTER TABLE x_departements_contours ADD COLUMN contour GEOMETRY NOT NULL AFTER nom_dept;
|
||||
-- ALTER TABLE x_departements_contours ADD SPATIAL INDEX idx_contour (contour);
|
||||
0
api/bootstrap.php
Normal file → Executable file
0
api/bootstrap.php
Normal file → Executable file
17
api/composer.json
Normal file → Executable file
17
api/composer.json
Normal file → Executable file
@@ -3,16 +3,19 @@
|
||||
"description": "API Multi-sites",
|
||||
"type": "project",
|
||||
"require": {
|
||||
"php": ">=8.1",
|
||||
"phpmailer/phpmailer": "^6.8",
|
||||
"ext-pdo": "*",
|
||||
"php": ">=8.3",
|
||||
"ext-json": "*",
|
||||
"ext-openssl": "*",
|
||||
"ext-json": "*"
|
||||
"ext-pdo": "*",
|
||||
"phpmailer/phpmailer": "^6.8",
|
||||
"phpoffice/phpspreadsheet": "^2.0",
|
||||
"setasign/fpdf": "^1.8",
|
||||
"stripe/stripe-php": "^17.6"
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"App\\": "src/"
|
||||
}
|
||||
"classmap": [
|
||||
"src/"
|
||||
]
|
||||
},
|
||||
"config": {
|
||||
"sort-packages": true,
|
||||
|
||||
709
api/composer.lock
generated
Normal file → Executable file
709
api/composer.lock
generated
Normal file → Executable file
@@ -4,20 +4,284 @@
|
||||
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
||||
"This file is @generated automatically"
|
||||
],
|
||||
"content-hash": "03e608fa83a14a82b3f9223977e9674e",
|
||||
"content-hash": "155893f9be89bceda3639efbf19b14d1",
|
||||
"packages": [
|
||||
{
|
||||
"name": "phpmailer/phpmailer",
|
||||
"version": "v6.9.3",
|
||||
"name": "composer/pcre",
|
||||
"version": "3.3.2",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/PHPMailer/PHPMailer.git",
|
||||
"reference": "2f5c94fe7493efc213f643c23b1b1c249d40f47e"
|
||||
"url": "https://github.com/composer/pcre.git",
|
||||
"reference": "b2bed4734f0cc156ee1fe9c0da2550420d99a21e"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/PHPMailer/PHPMailer/zipball/2f5c94fe7493efc213f643c23b1b1c249d40f47e",
|
||||
"reference": "2f5c94fe7493efc213f643c23b1b1c249d40f47e",
|
||||
"url": "https://api.github.com/repos/composer/pcre/zipball/b2bed4734f0cc156ee1fe9c0da2550420d99a21e",
|
||||
"reference": "b2bed4734f0cc156ee1fe9c0da2550420d99a21e",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": "^7.4 || ^8.0"
|
||||
},
|
||||
"conflict": {
|
||||
"phpstan/phpstan": "<1.11.10"
|
||||
},
|
||||
"require-dev": {
|
||||
"phpstan/phpstan": "^1.12 || ^2",
|
||||
"phpstan/phpstan-strict-rules": "^1 || ^2",
|
||||
"phpunit/phpunit": "^8 || ^9"
|
||||
},
|
||||
"type": "library",
|
||||
"extra": {
|
||||
"phpstan": {
|
||||
"includes": [
|
||||
"extension.neon"
|
||||
]
|
||||
},
|
||||
"branch-alias": {
|
||||
"dev-main": "3.x-dev"
|
||||
}
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Composer\\Pcre\\": "src"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Jordi Boggiano",
|
||||
"email": "j.boggiano@seld.be",
|
||||
"homepage": "http://seld.be"
|
||||
}
|
||||
],
|
||||
"description": "PCRE wrapping library that offers type-safe preg_* replacements.",
|
||||
"keywords": [
|
||||
"PCRE",
|
||||
"preg",
|
||||
"regex",
|
||||
"regular expression"
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/composer/pcre/issues",
|
||||
"source": "https://github.com/composer/pcre/tree/3.3.2"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
"url": "https://packagist.com",
|
||||
"type": "custom"
|
||||
},
|
||||
{
|
||||
"url": "https://github.com/composer",
|
||||
"type": "github"
|
||||
},
|
||||
{
|
||||
"url": "https://tidelift.com/funding/github/packagist/composer/composer",
|
||||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2024-11-12T16:29:46+00:00"
|
||||
},
|
||||
{
|
||||
"name": "maennchen/zipstream-php",
|
||||
"version": "3.1.2",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/maennchen/ZipStream-PHP.git",
|
||||
"reference": "aeadcf5c412332eb426c0f9b4485f6accba2a99f"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/maennchen/ZipStream-PHP/zipball/aeadcf5c412332eb426c0f9b4485f6accba2a99f",
|
||||
"reference": "aeadcf5c412332eb426c0f9b4485f6accba2a99f",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"ext-mbstring": "*",
|
||||
"ext-zlib": "*",
|
||||
"php-64bit": "^8.2"
|
||||
},
|
||||
"require-dev": {
|
||||
"brianium/paratest": "^7.7",
|
||||
"ext-zip": "*",
|
||||
"friendsofphp/php-cs-fixer": "^3.16",
|
||||
"guzzlehttp/guzzle": "^7.5",
|
||||
"mikey179/vfsstream": "^1.6",
|
||||
"php-coveralls/php-coveralls": "^2.5",
|
||||
"phpunit/phpunit": "^11.0",
|
||||
"vimeo/psalm": "^6.0"
|
||||
},
|
||||
"suggest": {
|
||||
"guzzlehttp/psr7": "^2.4",
|
||||
"psr/http-message": "^2.0"
|
||||
},
|
||||
"type": "library",
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"ZipStream\\": "src/"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Paul Duncan",
|
||||
"email": "pabs@pablotron.org"
|
||||
},
|
||||
{
|
||||
"name": "Jonatan Männchen",
|
||||
"email": "jonatan@maennchen.ch"
|
||||
},
|
||||
{
|
||||
"name": "Jesse Donat",
|
||||
"email": "donatj@gmail.com"
|
||||
},
|
||||
{
|
||||
"name": "András Kolesár",
|
||||
"email": "kolesar@kolesar.hu"
|
||||
}
|
||||
],
|
||||
"description": "ZipStream is a library for dynamically streaming dynamic zip files from PHP without writing to the disk at all on the server.",
|
||||
"keywords": [
|
||||
"stream",
|
||||
"zip"
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/maennchen/ZipStream-PHP/issues",
|
||||
"source": "https://github.com/maennchen/ZipStream-PHP/tree/3.1.2"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
"url": "https://github.com/maennchen",
|
||||
"type": "github"
|
||||
}
|
||||
],
|
||||
"time": "2025-01-27T12:07:53+00:00"
|
||||
},
|
||||
{
|
||||
"name": "markbaker/complex",
|
||||
"version": "3.0.2",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/MarkBaker/PHPComplex.git",
|
||||
"reference": "95c56caa1cf5c766ad6d65b6344b807c1e8405b9"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/MarkBaker/PHPComplex/zipball/95c56caa1cf5c766ad6d65b6344b807c1e8405b9",
|
||||
"reference": "95c56caa1cf5c766ad6d65b6344b807c1e8405b9",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": "^7.2 || ^8.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"dealerdirect/phpcodesniffer-composer-installer": "dev-master",
|
||||
"phpcompatibility/php-compatibility": "^9.3",
|
||||
"phpunit/phpunit": "^7.0 || ^8.0 || ^9.0",
|
||||
"squizlabs/php_codesniffer": "^3.7"
|
||||
},
|
||||
"type": "library",
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Complex\\": "classes/src/"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Mark Baker",
|
||||
"email": "mark@lange.demon.co.uk"
|
||||
}
|
||||
],
|
||||
"description": "PHP Class for working with complex numbers",
|
||||
"homepage": "https://github.com/MarkBaker/PHPComplex",
|
||||
"keywords": [
|
||||
"complex",
|
||||
"mathematics"
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/MarkBaker/PHPComplex/issues",
|
||||
"source": "https://github.com/MarkBaker/PHPComplex/tree/3.0.2"
|
||||
},
|
||||
"time": "2022-12-06T16:21:08+00:00"
|
||||
},
|
||||
{
|
||||
"name": "markbaker/matrix",
|
||||
"version": "3.0.1",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/MarkBaker/PHPMatrix.git",
|
||||
"reference": "728434227fe21be27ff6d86621a1b13107a2562c"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/MarkBaker/PHPMatrix/zipball/728434227fe21be27ff6d86621a1b13107a2562c",
|
||||
"reference": "728434227fe21be27ff6d86621a1b13107a2562c",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": "^7.1 || ^8.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"dealerdirect/phpcodesniffer-composer-installer": "dev-master",
|
||||
"phpcompatibility/php-compatibility": "^9.3",
|
||||
"phpdocumentor/phpdocumentor": "2.*",
|
||||
"phploc/phploc": "^4.0",
|
||||
"phpmd/phpmd": "2.*",
|
||||
"phpunit/phpunit": "^7.0 || ^8.0 || ^9.0",
|
||||
"sebastian/phpcpd": "^4.0",
|
||||
"squizlabs/php_codesniffer": "^3.7"
|
||||
},
|
||||
"type": "library",
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Matrix\\": "classes/src/"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Mark Baker",
|
||||
"email": "mark@demon-angel.eu"
|
||||
}
|
||||
],
|
||||
"description": "PHP Class for working with matrices",
|
||||
"homepage": "https://github.com/MarkBaker/PHPMatrix",
|
||||
"keywords": [
|
||||
"mathematics",
|
||||
"matrix",
|
||||
"vector"
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/MarkBaker/PHPMatrix/issues",
|
||||
"source": "https://github.com/MarkBaker/PHPMatrix/tree/3.0.1"
|
||||
},
|
||||
"time": "2022-12-02T22:17:43+00:00"
|
||||
},
|
||||
{
|
||||
"name": "phpmailer/phpmailer",
|
||||
"version": "v6.10.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/PHPMailer/PHPMailer.git",
|
||||
"reference": "bf74d75a1fde6beaa34a0ddae2ec5fce0f72a144"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/PHPMailer/PHPMailer/zipball/bf74d75a1fde6beaa34a0ddae2ec5fce0f72a144",
|
||||
"reference": "bf74d75a1fde6beaa34a0ddae2ec5fce0f72a144",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -77,7 +341,7 @@
|
||||
"description": "PHPMailer is a full-featured email creation and transfer class for PHP",
|
||||
"support": {
|
||||
"issues": "https://github.com/PHPMailer/PHPMailer/issues",
|
||||
"source": "https://github.com/PHPMailer/PHPMailer/tree/v6.9.3"
|
||||
"source": "https://github.com/PHPMailer/PHPMailer/tree/v6.10.0"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
@@ -85,7 +349,428 @@
|
||||
"type": "github"
|
||||
}
|
||||
],
|
||||
"time": "2024-11-24T18:04:13+00:00"
|
||||
"time": "2025-04-24T15:19:31+00:00"
|
||||
},
|
||||
{
|
||||
"name": "phpoffice/phpspreadsheet",
|
||||
"version": "2.3.8",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/PHPOffice/PhpSpreadsheet.git",
|
||||
"reference": "7a700683743bf1c4a21837c84b266916f1aa7d25"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/PHPOffice/PhpSpreadsheet/zipball/7a700683743bf1c4a21837c84b266916f1aa7d25",
|
||||
"reference": "7a700683743bf1c4a21837c84b266916f1aa7d25",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"composer/pcre": "^1 || ^2 || ^3",
|
||||
"ext-ctype": "*",
|
||||
"ext-dom": "*",
|
||||
"ext-fileinfo": "*",
|
||||
"ext-gd": "*",
|
||||
"ext-iconv": "*",
|
||||
"ext-libxml": "*",
|
||||
"ext-mbstring": "*",
|
||||
"ext-simplexml": "*",
|
||||
"ext-xml": "*",
|
||||
"ext-xmlreader": "*",
|
||||
"ext-xmlwriter": "*",
|
||||
"ext-zip": "*",
|
||||
"ext-zlib": "*",
|
||||
"maennchen/zipstream-php": "^2.1 || ^3.0",
|
||||
"markbaker/complex": "^3.0",
|
||||
"markbaker/matrix": "^3.0",
|
||||
"php": "^8.1",
|
||||
"psr/http-client": "^1.0",
|
||||
"psr/http-factory": "^1.0",
|
||||
"psr/simple-cache": "^1.0 || ^2.0 || ^3.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"dealerdirect/phpcodesniffer-composer-installer": "dev-main",
|
||||
"dompdf/dompdf": "^2.0 || ^3.0",
|
||||
"friendsofphp/php-cs-fixer": "^3.2",
|
||||
"mitoteam/jpgraph": "^10.3",
|
||||
"mpdf/mpdf": "^8.1.1",
|
||||
"phpcompatibility/php-compatibility": "^9.3",
|
||||
"phpstan/phpstan": "^1.1",
|
||||
"phpstan/phpstan-phpunit": "^1.0",
|
||||
"phpunit/phpunit": "^9.6 || ^10.5",
|
||||
"squizlabs/php_codesniffer": "^3.7",
|
||||
"tecnickcom/tcpdf": "^6.5"
|
||||
},
|
||||
"suggest": {
|
||||
"dompdf/dompdf": "Option for rendering PDF with PDF Writer",
|
||||
"ext-intl": "PHP Internationalization Functions",
|
||||
"mitoteam/jpgraph": "Option for rendering charts, or including charts with PDF or HTML Writers",
|
||||
"mpdf/mpdf": "Option for rendering PDF with PDF Writer",
|
||||
"tecnickcom/tcpdf": "Option for rendering PDF with PDF Writer"
|
||||
},
|
||||
"type": "library",
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"PhpOffice\\PhpSpreadsheet\\": "src/PhpSpreadsheet"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Maarten Balliauw",
|
||||
"homepage": "https://blog.maartenballiauw.be"
|
||||
},
|
||||
{
|
||||
"name": "Mark Baker",
|
||||
"homepage": "https://markbakeruk.net"
|
||||
},
|
||||
{
|
||||
"name": "Franck Lefevre",
|
||||
"homepage": "https://rootslabs.net"
|
||||
},
|
||||
{
|
||||
"name": "Erik Tilt"
|
||||
},
|
||||
{
|
||||
"name": "Adrien Crivelli"
|
||||
}
|
||||
],
|
||||
"description": "PHPSpreadsheet - Read, Create and Write Spreadsheet documents in PHP - Spreadsheet engine",
|
||||
"homepage": "https://github.com/PHPOffice/PhpSpreadsheet",
|
||||
"keywords": [
|
||||
"OpenXML",
|
||||
"excel",
|
||||
"gnumeric",
|
||||
"ods",
|
||||
"php",
|
||||
"spreadsheet",
|
||||
"xls",
|
||||
"xlsx"
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/PHPOffice/PhpSpreadsheet/issues",
|
||||
"source": "https://github.com/PHPOffice/PhpSpreadsheet/tree/2.3.8"
|
||||
},
|
||||
"time": "2025-02-08T03:01:45+00:00"
|
||||
},
|
||||
{
|
||||
"name": "psr/http-client",
|
||||
"version": "1.0.3",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/php-fig/http-client.git",
|
||||
"reference": "bb5906edc1c324c9a05aa0873d40117941e5fa90"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/php-fig/http-client/zipball/bb5906edc1c324c9a05aa0873d40117941e5fa90",
|
||||
"reference": "bb5906edc1c324c9a05aa0873d40117941e5fa90",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": "^7.0 || ^8.0",
|
||||
"psr/http-message": "^1.0 || ^2.0"
|
||||
},
|
||||
"type": "library",
|
||||
"extra": {
|
||||
"branch-alias": {
|
||||
"dev-master": "1.0.x-dev"
|
||||
}
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Psr\\Http\\Client\\": "src/"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "PHP-FIG",
|
||||
"homepage": "https://www.php-fig.org/"
|
||||
}
|
||||
],
|
||||
"description": "Common interface for HTTP clients",
|
||||
"homepage": "https://github.com/php-fig/http-client",
|
||||
"keywords": [
|
||||
"http",
|
||||
"http-client",
|
||||
"psr",
|
||||
"psr-18"
|
||||
],
|
||||
"support": {
|
||||
"source": "https://github.com/php-fig/http-client"
|
||||
},
|
||||
"time": "2023-09-23T14:17:50+00:00"
|
||||
},
|
||||
{
|
||||
"name": "psr/http-factory",
|
||||
"version": "1.1.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/php-fig/http-factory.git",
|
||||
"reference": "2b4765fddfe3b508ac62f829e852b1501d3f6e8a"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/php-fig/http-factory/zipball/2b4765fddfe3b508ac62f829e852b1501d3f6e8a",
|
||||
"reference": "2b4765fddfe3b508ac62f829e852b1501d3f6e8a",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": ">=7.1",
|
||||
"psr/http-message": "^1.0 || ^2.0"
|
||||
},
|
||||
"type": "library",
|
||||
"extra": {
|
||||
"branch-alias": {
|
||||
"dev-master": "1.0.x-dev"
|
||||
}
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Psr\\Http\\Message\\": "src/"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "PHP-FIG",
|
||||
"homepage": "https://www.php-fig.org/"
|
||||
}
|
||||
],
|
||||
"description": "PSR-17: Common interfaces for PSR-7 HTTP message factories",
|
||||
"keywords": [
|
||||
"factory",
|
||||
"http",
|
||||
"message",
|
||||
"psr",
|
||||
"psr-17",
|
||||
"psr-7",
|
||||
"request",
|
||||
"response"
|
||||
],
|
||||
"support": {
|
||||
"source": "https://github.com/php-fig/http-factory"
|
||||
},
|
||||
"time": "2024-04-15T12:06:14+00:00"
|
||||
},
|
||||
{
|
||||
"name": "psr/http-message",
|
||||
"version": "2.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/php-fig/http-message.git",
|
||||
"reference": "402d35bcb92c70c026d1a6a9883f06b2ead23d71"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/php-fig/http-message/zipball/402d35bcb92c70c026d1a6a9883f06b2ead23d71",
|
||||
"reference": "402d35bcb92c70c026d1a6a9883f06b2ead23d71",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": "^7.2 || ^8.0"
|
||||
},
|
||||
"type": "library",
|
||||
"extra": {
|
||||
"branch-alias": {
|
||||
"dev-master": "2.0.x-dev"
|
||||
}
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Psr\\Http\\Message\\": "src/"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "PHP-FIG",
|
||||
"homepage": "https://www.php-fig.org/"
|
||||
}
|
||||
],
|
||||
"description": "Common interface for HTTP messages",
|
||||
"homepage": "https://github.com/php-fig/http-message",
|
||||
"keywords": [
|
||||
"http",
|
||||
"http-message",
|
||||
"psr",
|
||||
"psr-7",
|
||||
"request",
|
||||
"response"
|
||||
],
|
||||
"support": {
|
||||
"source": "https://github.com/php-fig/http-message/tree/2.0"
|
||||
},
|
||||
"time": "2023-04-04T09:54:51+00:00"
|
||||
},
|
||||
{
|
||||
"name": "psr/simple-cache",
|
||||
"version": "3.0.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/php-fig/simple-cache.git",
|
||||
"reference": "764e0b3939f5ca87cb904f570ef9be2d78a07865"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/php-fig/simple-cache/zipball/764e0b3939f5ca87cb904f570ef9be2d78a07865",
|
||||
"reference": "764e0b3939f5ca87cb904f570ef9be2d78a07865",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": ">=8.0.0"
|
||||
},
|
||||
"type": "library",
|
||||
"extra": {
|
||||
"branch-alias": {
|
||||
"dev-master": "3.0.x-dev"
|
||||
}
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Psr\\SimpleCache\\": "src/"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "PHP-FIG",
|
||||
"homepage": "https://www.php-fig.org/"
|
||||
}
|
||||
],
|
||||
"description": "Common interfaces for simple caching",
|
||||
"keywords": [
|
||||
"cache",
|
||||
"caching",
|
||||
"psr",
|
||||
"psr-16",
|
||||
"simple-cache"
|
||||
],
|
||||
"support": {
|
||||
"source": "https://github.com/php-fig/simple-cache/tree/3.0.0"
|
||||
},
|
||||
"time": "2021-10-29T13:26:27+00:00"
|
||||
},
|
||||
{
|
||||
"name": "setasign/fpdf",
|
||||
"version": "1.8.6",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/Setasign/FPDF.git",
|
||||
"reference": "0838e0ee4925716fcbbc50ad9e1799b5edfae0a0"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/Setasign/FPDF/zipball/0838e0ee4925716fcbbc50ad9e1799b5edfae0a0",
|
||||
"reference": "0838e0ee4925716fcbbc50ad9e1799b5edfae0a0",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"ext-gd": "*",
|
||||
"ext-zlib": "*"
|
||||
},
|
||||
"type": "library",
|
||||
"autoload": {
|
||||
"classmap": [
|
||||
"fpdf.php"
|
||||
]
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Olivier Plathey",
|
||||
"email": "oliver@fpdf.org",
|
||||
"homepage": "http://fpdf.org/"
|
||||
}
|
||||
],
|
||||
"description": "FPDF is a PHP class which allows to generate PDF files with pure PHP. F from FPDF stands for Free: you may use it for any kind of usage and modify it to suit your needs.",
|
||||
"homepage": "http://www.fpdf.org",
|
||||
"keywords": [
|
||||
"fpdf",
|
||||
"pdf"
|
||||
],
|
||||
"support": {
|
||||
"source": "https://github.com/Setasign/FPDF/tree/1.8.6"
|
||||
},
|
||||
"time": "2023-06-26T14:44:25+00:00"
|
||||
},
|
||||
{
|
||||
"name": "stripe/stripe-php",
|
||||
"version": "v17.6.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/stripe/stripe-php.git",
|
||||
"reference": "a6219df5df1324a0d3f1da25fb5e4b8a3307ea16"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/stripe/stripe-php/zipball/a6219df5df1324a0d3f1da25fb5e4b8a3307ea16",
|
||||
"reference": "a6219df5df1324a0d3f1da25fb5e4b8a3307ea16",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"ext-curl": "*",
|
||||
"ext-json": "*",
|
||||
"ext-mbstring": "*",
|
||||
"php": ">=5.6.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"friendsofphp/php-cs-fixer": "3.72.0",
|
||||
"phpstan/phpstan": "^1.2",
|
||||
"phpunit/phpunit": "^5.7 || ^9.0"
|
||||
},
|
||||
"type": "library",
|
||||
"extra": {
|
||||
"branch-alias": {
|
||||
"dev-master": "2.0-dev"
|
||||
}
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Stripe\\": "lib/"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Stripe and contributors",
|
||||
"homepage": "https://github.com/stripe/stripe-php/contributors"
|
||||
}
|
||||
],
|
||||
"description": "Stripe PHP Library",
|
||||
"homepage": "https://stripe.com/",
|
||||
"keywords": [
|
||||
"api",
|
||||
"payment processing",
|
||||
"stripe"
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/stripe/stripe-php/issues",
|
||||
"source": "https://github.com/stripe/stripe-php/tree/v17.6.0"
|
||||
},
|
||||
"time": "2025-08-27T19:32:42+00:00"
|
||||
}
|
||||
],
|
||||
"packages-dev": [],
|
||||
@@ -95,10 +780,10 @@
|
||||
"prefer-stable": false,
|
||||
"prefer-lowest": false,
|
||||
"platform": {
|
||||
"php": ">=8.1",
|
||||
"ext-pdo": "*",
|
||||
"php": ">=8.3",
|
||||
"ext-json": "*",
|
||||
"ext-openssl": "*",
|
||||
"ext-json": "*"
|
||||
"ext-pdo": "*"
|
||||
},
|
||||
"platform-dev": {},
|
||||
"plugin-api-version": "2.6.0"
|
||||
|
||||
BIN
api/composer.phar
Executable file
BIN
api/composer.phar
Executable file
Binary file not shown.
27
api/create_table_x_departements_contours.sql
Normal file
27
api/create_table_x_departements_contours.sql
Normal file
@@ -0,0 +1,27 @@
|
||||
-- Script de création de la table x_departements_contours
|
||||
-- À exécuter manuellement en tant qu'administrateur de la base de données
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `x_departements_contours` (
|
||||
`id` int(11) NOT NULL AUTO_INCREMENT,
|
||||
`code_dept` varchar(3) NOT NULL COMMENT 'Code département (22, 2A, 971...)',
|
||||
`nom_dept` varchar(100) NOT NULL,
|
||||
`contour` POLYGON NOT NULL COMMENT 'Polygone du contour du département',
|
||||
`bbox_min_lat` decimal(10,8) DEFAULT NULL COMMENT 'Latitude min de la bounding box',
|
||||
`bbox_max_lat` decimal(10,8) DEFAULT NULL COMMENT 'Latitude max de la bounding box',
|
||||
`bbox_min_lng` decimal(11,8) DEFAULT NULL COMMENT 'Longitude min de la bounding box',
|
||||
`bbox_max_lng` decimal(11,8) DEFAULT NULL COMMENT 'Longitude max de la bounding box',
|
||||
`created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
`updated_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `idx_code_dept` (`code_dept`),
|
||||
SPATIAL KEY `idx_contour` (`contour`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='Contours géographiques des départements français';
|
||||
|
||||
-- Index pour améliorer les performances des requêtes par bounding box
|
||||
CREATE INDEX idx_dept_bbox ON x_departements_contours (bbox_min_lat, bbox_max_lat, bbox_min_lng, bbox_max_lng);
|
||||
|
||||
-- Vérifier que la table a été créée
|
||||
SHOW CREATE TABLE x_departements_contours\G
|
||||
|
||||
-- Vérifier les index
|
||||
SHOW INDEX FROM x_departements_contours;
|
||||
@@ -10,7 +10,7 @@ set -euo pipefail
|
||||
JUMP_USER="root"
|
||||
JUMP_HOST="195.154.80.116"
|
||||
JUMP_PORT="22"
|
||||
JUMP_KEY="/Users/pierre/.ssh/id_rsa_mbpi"
|
||||
JUMP_KEY="/home/pierre/.ssh/id_rsa_mbpi"
|
||||
|
||||
# Paramètres du container Incus
|
||||
INCUS_PROJECT=default
|
||||
@@ -73,6 +73,7 @@ fi
|
||||
|
||||
# Étape 0: Définir le nom de l'archive
|
||||
ARCHIVE_NAME="api-deploy-$(date +%s).tar.gz"
|
||||
TEMP_ARCHIVE="/tmp/${ARCHIVE_NAME}"
|
||||
echo_info "Archive name will be: $ARCHIVE_NAME"
|
||||
|
||||
# Étape 1: Créer une archive du projet
|
||||
@@ -88,18 +89,24 @@ tar --exclude='.git' \
|
||||
--exclude='.DS_Store' \
|
||||
--exclude='README.md' \
|
||||
--exclude="*.tar.gz" \
|
||||
--exclude='node_modules' \
|
||||
--exclude='vendor' \
|
||||
--exclude='*.swp' \
|
||||
--exclude='*.swo' \
|
||||
--exclude='*~' \
|
||||
--warning=no-file-changed \
|
||||
--no-xattrs \
|
||||
-czf "${ARCHIVE_NAME}" . || echo_error "Failed to create archive"
|
||||
-czf "${TEMP_ARCHIVE}" . || echo_error "Failed to create archive"
|
||||
|
||||
# Vérifier la taille de l'archive
|
||||
ARCHIVE_SIZE=$(du -h "${ARCHIVE_NAME}" | cut -f1)
|
||||
ARCHIVE_SIZE=$(du -h "${TEMP_ARCHIVE}" | cut -f1)
|
||||
|
||||
SSH_JUMP_CMD="ssh -i ${JUMP_KEY} -p ${JUMP_PORT} ${JUMP_USER}@${JUMP_HOST}"
|
||||
|
||||
# Étape 2: Copier l'archive vers le serveur de saut
|
||||
echo_step "Copying archive to jump server..."
|
||||
echo_info "Archive size: $ARCHIVE_SIZE"
|
||||
scp -i "${JUMP_KEY}" -P "${JUMP_PORT}" "${ARCHIVE_NAME}" "${JUMP_USER}@${JUMP_HOST}:/tmp/${ARCHIVE_NAME}" || echo_error "Failed to copy archive to jump server"
|
||||
scp -i "${JUMP_KEY}" -P "${JUMP_PORT}" "${TEMP_ARCHIVE}" "${JUMP_USER}@${JUMP_HOST}:/tmp/${ARCHIVE_NAME}" || echo_error "Failed to copy archive to jump server"
|
||||
|
||||
# Étape 3: Exécuter les commandes sur le serveur de saut pour déployer dans le container Incus
|
||||
echo_step "Deploying to Incus container..."
|
||||
@@ -128,13 +135,24 @@ $SSH_JUMP_CMD "
|
||||
incus exec ${INCUS_CONTAINER} -- chmod -R 775 ${FINAL_PATH}/logs || exit 1
|
||||
incus exec ${INCUS_CONTAINER} -- find ${FINAL_PATH}/logs -type f -exec chmod 664 {} \; || exit 1
|
||||
|
||||
echo '📁 Création des dossiers uploads...'
|
||||
incus exec ${INCUS_CONTAINER} -- mkdir -p ${FINAL_PATH}/uploads || exit 1
|
||||
incus exec ${INCUS_CONTAINER} -- chown -R ${FINAL_OWNER}:${FINAL_OWNER_LOGS} ${FINAL_PATH}/uploads || exit 1
|
||||
incus exec ${INCUS_CONTAINER} -- chmod -R 775 ${FINAL_PATH}/uploads || exit 1
|
||||
incus exec ${INCUS_CONTAINER} -- find ${FINAL_PATH}/uploads -type f -exec chmod -R 664 {} \; || exit 1
|
||||
|
||||
echo '📦 Mise à jour des dépendances Composer...'
|
||||
incus exec ${INCUS_CONTAINER} -- bash -c 'cd ${FINAL_PATH} && composer update --no-dev --optimize-autoloader' || {
|
||||
echo '⚠️ Composer non disponible ou échec, poursuite sans mise à jour des dépendances'
|
||||
}
|
||||
|
||||
echo '🧹 Nettoyage...'
|
||||
incus exec ${INCUS_CONTAINER} -- rm -f /tmp/${ARCHIVE_NAME} || exit 1
|
||||
rm -f /tmp/${ARCHIVE_NAME} || exit 1
|
||||
"
|
||||
|
||||
# Nettoyage local
|
||||
rm -f "${ARCHIVE_NAME}"
|
||||
rm -f "${TEMP_ARCHIVE}"
|
||||
|
||||
# Résumé final
|
||||
echo_step "Deployment completed successfully."
|
||||
|
||||
334
api/docs/API-SECURITY.md
Normal file
334
api/docs/API-SECURITY.md
Normal file
@@ -0,0 +1,334 @@
|
||||
# API Security & Performance Monitoring
|
||||
|
||||
## 📋 Vue d'ensemble
|
||||
|
||||
Système complet de sécurité et monitoring pour l'API GeoSector implémenté et opérationnel.
|
||||
|
||||
### ✅ Fonctionnalités implémentées
|
||||
|
||||
- **Détection d'intrusions** : Brute force, SQL injection, patterns de scan
|
||||
- **Monitoring des performances** : Temps de réponse, utilisation mémoire, requêtes DB
|
||||
- **Alertes email intelligentes** : Throttling, niveaux de priorité
|
||||
- **Blocage d'IP automatique** : Temporaire ou permanent
|
||||
- **Traçabilité complète** : Historique pour audit et analyse
|
||||
|
||||
## 🏗️ Architecture
|
||||
|
||||
### Tables de base de données (préfixe `sec_`)
|
||||
|
||||
```sql
|
||||
-- 4 tables créées dans scripts/sql/create_security_tables.sql
|
||||
sec_alerts -- Alertes de sécurité
|
||||
sec_performance_metrics -- Métriques de performance
|
||||
sec_failed_login_attempts -- Tentatives de connexion échouées
|
||||
sec_blocked_ips -- IPs bloquées
|
||||
```
|
||||
|
||||
### Services PHP implémentés
|
||||
|
||||
```
|
||||
src/Services/Security/
|
||||
├── AlertService.php # Gestion centralisée des alertes
|
||||
├── EmailThrottler.php # Anti-spam pour emails
|
||||
├── SecurityMonitor.php # Détection des menaces
|
||||
├── PerformanceMonitor.php # Monitoring des temps
|
||||
└── IPBlocker.php # Gestion des blocages IP
|
||||
```
|
||||
|
||||
### Contrôleur d'administration
|
||||
|
||||
```
|
||||
src/Controllers/SecurityController.php # Interface d'administration
|
||||
```
|
||||
|
||||
## 🚀 Installation
|
||||
|
||||
### 1. Créer les tables
|
||||
|
||||
```bash
|
||||
# Exécuter le script SQL sur chaque environnement
|
||||
mysql -u root -p geo_app < scripts/sql/create_security_tables.sql
|
||||
```
|
||||
|
||||
### 2. Configurer le cron de purge
|
||||
|
||||
```bash
|
||||
# Ajouter dans crontab (crontab -e)
|
||||
0 2 * * * /usr/bin/php /var/www/geosector/api/scripts/cron/cleanup_security_data.php >> /var/log/security_cleanup.log 2>&1
|
||||
```
|
||||
|
||||
### 3. Tester l'installation
|
||||
|
||||
```bash
|
||||
php test_security.php
|
||||
```
|
||||
|
||||
## 🔒 Fonctionnement
|
||||
|
||||
### Détection automatique
|
||||
|
||||
Le système détecte et bloque automatiquement :
|
||||
|
||||
- **Brute force** : 5 tentatives échouées en 5 minutes → IP bloquée 1h
|
||||
- **SQL injection** : Patterns suspects → IP bloquée définitivement
|
||||
- **Scan de vulnérabilités** : Accès aux fichiers sensibles → IP bloquée 1h
|
||||
- **Rate limiting** : Plus de 60 requêtes/minute → Rejet temporaire
|
||||
|
||||
### Monitoring de performance
|
||||
|
||||
Chaque requête est automatiquement monitorée :
|
||||
|
||||
```php
|
||||
// Dans index.php
|
||||
PerformanceMonitor::startRequest();
|
||||
// ... traitement ...
|
||||
PerformanceMonitor::endRequest($endpoint, $method, $statusCode);
|
||||
```
|
||||
|
||||
### Alertes email
|
||||
|
||||
Configuration des niveaux :
|
||||
|
||||
- **INFO** : Log uniquement
|
||||
- **WARNING** : Email avec throttling 1h
|
||||
- **ERROR** : Email avec throttling 15min
|
||||
- **CRITICAL** : Email avec throttling 5min
|
||||
- **SECURITY** : Email immédiat, priorité haute
|
||||
|
||||
## 📊 Endpoints d'administration
|
||||
|
||||
Tous les endpoints nécessitent une authentification admin (role >= 2) :
|
||||
|
||||
```
|
||||
GET /api/admin/metrics # Métriques de performance
|
||||
GET /api/admin/alerts # Alertes actives
|
||||
POST /api/admin/alerts/:id/resolve # Résoudre une alerte
|
||||
GET /api/admin/blocked-ips # IPs bloquées
|
||||
POST /api/admin/unblock-ip # Débloquer une IP
|
||||
POST /api/admin/block-ip # Bloquer une IP manuellement
|
||||
GET /api/admin/security-report # Rapport complet
|
||||
POST /api/admin/cleanup # Nettoyer les anciennes données
|
||||
POST /api/admin/test-alert # Tester les alertes
|
||||
```
|
||||
|
||||
## 🔧 Configuration
|
||||
|
||||
### Seuils par défaut (modifiables dans les services)
|
||||
|
||||
```php
|
||||
// PerformanceMonitor.php
|
||||
const DEFAULT_THRESHOLDS = [
|
||||
'response_time_warning' => 1000, // 1 seconde
|
||||
'response_time_critical' => 3000, // 3 secondes
|
||||
'db_time_warning' => 500, // 500ms
|
||||
'db_time_critical' => 1000, // 1 seconde
|
||||
'memory_warning' => 64, // 64 MB
|
||||
'memory_critical' => 128 // 128 MB
|
||||
];
|
||||
|
||||
// SecurityMonitor.php
|
||||
- Brute force : 5 tentatives en 5 minutes
|
||||
- Rate limit : 60 requêtes par minute
|
||||
- 404 pattern : 10 erreurs 404 en 10 minutes
|
||||
|
||||
// EmailThrottler.php
|
||||
const DEFAULT_CONFIG = [
|
||||
'max_per_hour' => 10,
|
||||
'max_per_day' => 50,
|
||||
'digest_after' => 5,
|
||||
'cooldown_minutes' => 60
|
||||
];
|
||||
```
|
||||
|
||||
### Rétention des données
|
||||
|
||||
Configurée dans `scripts/cron/cleanup_security_data.php` :
|
||||
|
||||
```php
|
||||
$RETENTION_DAYS = [
|
||||
'performance_metrics' => 30, // 30 jours
|
||||
'failed_login_attempts' => 7, // 7 jours
|
||||
'resolved_alerts' => 90, // 90 jours
|
||||
'expired_blocks' => 0 // Déblocage immédiat
|
||||
];
|
||||
```
|
||||
|
||||
## 📈 Métriques surveillées
|
||||
|
||||
### Performance
|
||||
- Temps de réponse total
|
||||
- Temps cumulé des requêtes DB
|
||||
- Nombre de requêtes DB
|
||||
- Utilisation mémoire (pic et moyenne)
|
||||
- Codes HTTP de réponse
|
||||
|
||||
### Sécurité
|
||||
- Tentatives de connexion échouées
|
||||
- IPs bloquées (temporaires/permanentes)
|
||||
- Patterns d'attaque détectés
|
||||
- Alertes par type et niveau
|
||||
|
||||
## 🛡️ Patterns de détection
|
||||
|
||||
### SQL Injection
|
||||
```php
|
||||
// Patterns détectés dans SecurityMonitor.php
|
||||
- UNION SELECT
|
||||
- DROP TABLE
|
||||
- INSERT INTO
|
||||
- UPDATE SET
|
||||
- DELETE FROM
|
||||
- Script tags
|
||||
- OR 1=1
|
||||
- Commentaires SQL (--)
|
||||
```
|
||||
|
||||
### Fichiers sensibles
|
||||
```php
|
||||
// Patterns de scan détectés
|
||||
- admin, administrator
|
||||
- wp-admin, phpmyadmin
|
||||
- .git, .env
|
||||
- config.php
|
||||
- backup, .sql, .zip
|
||||
- shell.php, eval.php
|
||||
```
|
||||
|
||||
## 📝 Exemples d'utilisation
|
||||
|
||||
### Déclencher une alerte manuelle
|
||||
|
||||
```php
|
||||
use App\Services\Security\AlertService;
|
||||
|
||||
AlertService::trigger('CUSTOM_ALERT', [
|
||||
'message' => 'Événement important détecté',
|
||||
'details' => ['user' => $userId, 'action' => $action]
|
||||
], 'WARNING');
|
||||
```
|
||||
|
||||
### Bloquer une IP manuellement
|
||||
|
||||
```php
|
||||
use App\Services\Security\IPBlocker;
|
||||
|
||||
// Blocage temporaire (1 heure)
|
||||
IPBlocker::block('192.168.1.100', 3600, 'Comportement suspect');
|
||||
|
||||
// Blocage permanent
|
||||
IPBlocker::blockPermanent('192.168.1.100', 'Attaque confirmée');
|
||||
```
|
||||
|
||||
### Obtenir les statistiques
|
||||
|
||||
```php
|
||||
use App\Services\Security\SecurityMonitor;
|
||||
use App\Services\Security\PerformanceMonitor;
|
||||
|
||||
$securityStats = SecurityMonitor::getSecurityStats();
|
||||
$perfStats = PerformanceMonitor::getStats(null, 24); // 24h
|
||||
```
|
||||
|
||||
## ⚠️ Points d'attention
|
||||
|
||||
### RGPD
|
||||
- Les IPs sont des données personnelles
|
||||
- Durée de conservation limitée (voir rétention)
|
||||
- Anonymisation après traitement
|
||||
|
||||
### Performance
|
||||
- Overhead < 5ms par requête
|
||||
- Optimisation des tables avec index
|
||||
- Purge automatique des anciennes données
|
||||
|
||||
### Sécurité
|
||||
- Pas d'exposition de données sensibles dans les alertes
|
||||
- Chiffrement des données utilisateur
|
||||
- Whitelist pour IPs de confiance (localhost)
|
||||
|
||||
## 🔄 Maintenance
|
||||
|
||||
### Quotidienne (cron)
|
||||
```bash
|
||||
# Purge automatique à 2h du matin
|
||||
0 2 * * * php /var/www/geosector/api/scripts/cron/cleanup_security_data.php
|
||||
```
|
||||
|
||||
### Hebdomadaire
|
||||
- Vérifier les alertes actives
|
||||
- Analyser les tendances de performance
|
||||
- Ajuster les seuils si nécessaire
|
||||
|
||||
### Mensuelle
|
||||
- Analyser le rapport de sécurité
|
||||
- Mettre à jour les IPs whitelist/blacklist
|
||||
- Optimiser les tables si nécessaire
|
||||
|
||||
## 🐛 Dépannage
|
||||
|
||||
### Les tables n'existent pas
|
||||
```bash
|
||||
# Créer les tables
|
||||
mysql -u root -p geo_app < scripts/sql/create_security_tables.sql
|
||||
```
|
||||
|
||||
### Pas d'alertes email
|
||||
- Vérifier la configuration email dans `AppConfig`
|
||||
- Vérifier les logs : `tail -f logs/geosector-*.log`
|
||||
- Tester avec : `POST /api/admin/test-alert`
|
||||
|
||||
### IP bloquée par erreur
|
||||
```bash
|
||||
# Via API
|
||||
curl -X POST https://dapp.geosector.fr/api/admin/unblock-ip \
|
||||
-H "Authorization: Bearer TOKEN" \
|
||||
-d '{"ip": "192.168.1.100"}'
|
||||
|
||||
# Via MySQL
|
||||
UPDATE sec_blocked_ips SET unblocked_at = NOW() WHERE ip_address = '192.168.1.100';
|
||||
```
|
||||
|
||||
## 📚 Ressources
|
||||
|
||||
- [OWASP Top 10](https://owasp.org/www-project-top-ten/)
|
||||
- [PHP Security Best Practices](https://www.php.net/manual/en/security.php)
|
||||
- Code source : `/src/Services/Security/`
|
||||
- Tests : `test_security.php`
|
||||
- Logs : `/logs/geosector-*.log`
|
||||
|
||||
## 🎯 Statut d'implémentation
|
||||
|
||||
✅ **Phase 1** : Infrastructure de base - COMPLÉTÉ
|
||||
- Tables créées avec préfixe `sec_`
|
||||
- Services PHP implémentés
|
||||
- Intégration dans index.php et Database.php
|
||||
|
||||
✅ **Phase 2** : Monitoring de Performance - COMPLÉTÉ
|
||||
- Chronométrage automatique des requêtes
|
||||
- Monitoring des requêtes DB
|
||||
- Alertes sur dégradation
|
||||
|
||||
✅ **Phase 3** : Détection d'intrusions - COMPLÉTÉ
|
||||
- Détection brute force
|
||||
- Détection SQL injection
|
||||
- Blocage IP automatique
|
||||
|
||||
✅ **Phase 4** : Alertes Email - COMPLÉTÉ
|
||||
- Service d'alertes avec throttling
|
||||
- Templates d'emails
|
||||
- Niveaux de priorité
|
||||
|
||||
✅ **Phase 5** : Administration - COMPLÉTÉ
|
||||
- Endpoints d'administration
|
||||
- Interface de gestion
|
||||
- Rapports de sécurité
|
||||
|
||||
✅ **Phase 6** : Maintenance - COMPLÉTÉ
|
||||
- Script de purge automatique
|
||||
- Optimisation des tables
|
||||
- Documentation complète
|
||||
|
||||
---
|
||||
|
||||
*Dernière mise à jour : 2025-01-17*
|
||||
*Version : 1.0.0*
|
||||
0
api/docs/CDC.md
Normal file → Executable file
0
api/docs/CDC.md
Normal file → Executable file
419
api/docs/CHAT_MODULE.md
Normal file
419
api/docs/CHAT_MODULE.md
Normal file
@@ -0,0 +1,419 @@
|
||||
# Module Chat - Documentation API
|
||||
|
||||
## Vue d'ensemble
|
||||
|
||||
Le module Chat permet aux utilisateurs de l'application GeoSector de communiquer entre eux via une messagerie intégrée. Il supporte les conversations privées, de groupe et les diffusions (broadcast).
|
||||
|
||||
## Architecture
|
||||
|
||||
### Tables de base de données
|
||||
|
||||
- `chat_rooms` : Salles de conversation
|
||||
- `chat_messages` : Messages échangés
|
||||
- `chat_participants` : Participants aux conversations
|
||||
- `chat_read_receipts` : Accusés de lecture
|
||||
|
||||
### Permissions par rôle
|
||||
|
||||
| Rôle | Permissions |
|
||||
|------|------------|
|
||||
| **1 - Utilisateur** | Conversations privées et groupes avec membres de son entité |
|
||||
| **2 - Admin entité** | Toutes conversations de son entité + création de diffusions |
|
||||
| **> 2 - Super admin** | Accès total à toutes les conversations |
|
||||
|
||||
## Flux d'utilisation du module Chat
|
||||
|
||||
### 📱 Vue d'ensemble du flux
|
||||
|
||||
Le module Chat fonctionne en mode **chargement dynamique** : les données sont récupérées à la demande, pas toutes en une fois au login.
|
||||
|
||||
### 1. Au login (`/api/login`)
|
||||
|
||||
La réponse du login contient un objet `chat` avec les informations de base :
|
||||
|
||||
```json
|
||||
{
|
||||
"status": "success",
|
||||
"user": {...},
|
||||
"amicale": {...},
|
||||
"chat": {
|
||||
"total_rooms": 5, // Nombre total de conversations
|
||||
"unread_messages": 12, // Total messages non lus
|
||||
"chat_enabled": true, // Module activé pour cet utilisateur
|
||||
"last_active_room": { // Dernière conversation active
|
||||
"id": "uuid-room-123",
|
||||
"title": "Discussion équipe",
|
||||
"type": "group",
|
||||
"last_message": "À demain !",
|
||||
"last_message_at": "2025-01-17 18:30:00"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
→ Permet d'afficher un **badge de notification** et de savoir si le chat est disponible
|
||||
|
||||
### 2. Ouverture de la page Chat
|
||||
|
||||
#### Étape 1 : Chargement initial
|
||||
```
|
||||
GET /api/chat/rooms
|
||||
```
|
||||
→ Récupère la liste des conversations avec aperçu du dernier message
|
||||
|
||||
#### Étape 2 : Sélection d'une conversation
|
||||
```
|
||||
GET /api/chat/rooms/{room_id}/messages?limit=50
|
||||
```
|
||||
→ Charge les 50 derniers messages (pagination disponible)
|
||||
|
||||
#### Étape 3 : Marquage comme lu
|
||||
```
|
||||
POST /api/chat/rooms/{room_id}/read
|
||||
```
|
||||
→ Met à jour les compteurs de messages non lus
|
||||
|
||||
### 3. Actions utilisateur
|
||||
|
||||
| Action | Endpoint | Description |
|
||||
|--------|----------|-------------|
|
||||
| **Envoyer un message** | `POST /api/chat/rooms/{id}/messages` | Envoie et retourne le message créé |
|
||||
| **Créer une conversation** | `POST /api/chat/rooms` | Crée une nouvelle room |
|
||||
| **Obtenir les destinataires** | `GET /api/chat/recipients` | Liste des contacts disponibles |
|
||||
| **Charger plus de messages** | `GET /api/chat/rooms/{id}/messages?before={msg_id}` | Pagination |
|
||||
|
||||
### 4. Stratégies de rafraîchissement
|
||||
|
||||
#### Polling (recommandé pour débuter)
|
||||
- Rafraîchir `/api/chat/rooms` toutes les 30 secondes
|
||||
- Rafraîchir les messages de la conversation active toutes les 10 secondes
|
||||
|
||||
#### Pull to refresh
|
||||
- Permettre à l'utilisateur de rafraîchir manuellement
|
||||
|
||||
#### Lifecycle events
|
||||
- Recharger quand l'app revient au premier plan
|
||||
- Rafraîchir après envoi d'un message
|
||||
|
||||
### 5. Exemple d'implémentation Flutter
|
||||
|
||||
```dart
|
||||
class ChatService {
|
||||
Timer? _roomsTimer;
|
||||
Timer? _messagesTimer;
|
||||
|
||||
// 1. Au login, stocker les infos de base
|
||||
void initFromLogin(Map<String, dynamic> chatData) {
|
||||
_unreadCount = chatData['unread_messages'];
|
||||
_chatEnabled = chatData['chat_enabled'];
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
// 2. À l'ouverture du chat
|
||||
Future<void> openChatPage() async {
|
||||
// Charger les conversations
|
||||
final rooms = await api.get('/api/chat/rooms');
|
||||
_rooms = rooms['rooms'];
|
||||
|
||||
// Démarrer le polling
|
||||
_startPolling();
|
||||
}
|
||||
|
||||
// 3. Sélection d'une conversation
|
||||
Future<void> selectRoom(String roomId) async {
|
||||
// Charger les messages
|
||||
final response = await api.get('/api/chat/rooms/$roomId/messages');
|
||||
_currentMessages = response['messages'];
|
||||
|
||||
// Marquer comme lu
|
||||
await api.post('/api/chat/rooms/$roomId/read');
|
||||
|
||||
// Rafraîchir plus fréquemment cette conversation
|
||||
_startMessagePolling(roomId);
|
||||
}
|
||||
|
||||
// 4. Polling automatique
|
||||
void _startPolling() {
|
||||
_roomsTimer = Timer.periodic(Duration(seconds: 30), (_) {
|
||||
_refreshRooms();
|
||||
});
|
||||
}
|
||||
|
||||
// 5. Nettoyage
|
||||
void dispose() {
|
||||
_roomsTimer?.cancel();
|
||||
_messagesTimer?.cancel();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Endpoints API
|
||||
|
||||
### 1. GET /api/chat/rooms
|
||||
**Description** : Récupère la liste des conversations de l'utilisateur
|
||||
|
||||
**Réponse** :
|
||||
```json
|
||||
{
|
||||
"status": "success",
|
||||
"rooms": [
|
||||
{
|
||||
"id": "uuid-room-1",
|
||||
"title": "Discussion équipe",
|
||||
"type": "group",
|
||||
"created_at": "2025-01-17 10:00:00",
|
||||
"created_by": 123,
|
||||
"updated_at": "2025-01-17 14:30:00",
|
||||
"last_message": "Bonjour tout le monde",
|
||||
"last_message_at": "2025-01-17 14:30:00",
|
||||
"unread_count": 3,
|
||||
"participant_count": 5,
|
||||
"participants": [
|
||||
{
|
||||
"user_id": 123,
|
||||
"name": "Jean Dupont",
|
||||
"first_name": "Jean",
|
||||
"is_admin": true
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### 2. POST /api/chat/rooms
|
||||
**Description** : Crée une nouvelle conversation
|
||||
|
||||
**Body** :
|
||||
```json
|
||||
{
|
||||
"type": "private|group|broadcast",
|
||||
"title": "Titre optionnel (requis pour group/broadcast)",
|
||||
"participants": [456, 789], // IDs des participants
|
||||
"initial_message": "Message initial optionnel"
|
||||
}
|
||||
```
|
||||
|
||||
**Règles** :
|
||||
- `private` : Maximum 2 participants (incluant le créateur)
|
||||
- `group` : Plusieurs participants possibles
|
||||
- `broadcast` : Réservé aux admins (rôle >= 2)
|
||||
|
||||
**Réponse** :
|
||||
```json
|
||||
{
|
||||
"status": "success",
|
||||
"room": {
|
||||
"id": "uuid-new-room",
|
||||
"title": "Nouvelle conversation",
|
||||
"type": "group",
|
||||
"created_at": "2025-01-17 15:00:00",
|
||||
"participants": [...]
|
||||
},
|
||||
"existing": false // true si conversation privée existante trouvée
|
||||
}
|
||||
```
|
||||
|
||||
### 3. GET /api/chat/rooms/{id}/messages
|
||||
**Description** : Récupère les messages d'une conversation
|
||||
|
||||
**Paramètres** :
|
||||
- `limit` : Nombre de messages (défaut: 50, max: 100)
|
||||
- `before` : ID du message pour pagination
|
||||
|
||||
**Réponse** :
|
||||
```json
|
||||
{
|
||||
"status": "success",
|
||||
"messages": [
|
||||
{
|
||||
"id": "uuid-message-1",
|
||||
"content": "Bonjour !",
|
||||
"sender_id": 123,
|
||||
"sender_name": "Jean Dupont",
|
||||
"sender_first_name": "Jean",
|
||||
"sent_at": "2025-01-17 14:00:00",
|
||||
"edited_at": null,
|
||||
"is_deleted": false,
|
||||
"is_read": true,
|
||||
"is_mine": false,
|
||||
"read_count": 3
|
||||
}
|
||||
],
|
||||
"has_more": true
|
||||
}
|
||||
```
|
||||
|
||||
### 4. POST /api/chat/rooms/{id}/messages
|
||||
**Description** : Envoie un message dans une conversation
|
||||
|
||||
**Body** :
|
||||
```json
|
||||
{
|
||||
"content": "Contenu du message (max 5000 caractères)"
|
||||
}
|
||||
```
|
||||
|
||||
**Réponse** :
|
||||
```json
|
||||
{
|
||||
"status": "success",
|
||||
"message": {
|
||||
"id": "uuid-new-message",
|
||||
"content": "Message envoyé",
|
||||
"sender_id": 123,
|
||||
"sender_name": "Jean Dupont",
|
||||
"sent_at": "2025-01-17 15:30:00",
|
||||
"is_mine": true,
|
||||
"is_read": false,
|
||||
"read_count": 0
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 5. POST /api/chat/rooms/{id}/read
|
||||
**Description** : Marque les messages comme lus
|
||||
|
||||
**Body (optionnel)** :
|
||||
```json
|
||||
{
|
||||
"message_ids": ["uuid-1", "uuid-2"] // Si omis, marque tous les messages
|
||||
}
|
||||
```
|
||||
|
||||
**Réponse** :
|
||||
```json
|
||||
{
|
||||
"status": "success",
|
||||
"unread_count": 0 // Nombre de messages non lus restants
|
||||
}
|
||||
```
|
||||
|
||||
### 6. GET /api/chat/recipients
|
||||
**Description** : Liste des destinataires possibles pour créer une conversation
|
||||
|
||||
**Réponse** :
|
||||
```json
|
||||
{
|
||||
"status": "success",
|
||||
"recipients": [
|
||||
{
|
||||
"id": 456,
|
||||
"name": "Marie Martin",
|
||||
"first_name": "Marie",
|
||||
"role": 1,
|
||||
"entite_id": 5
|
||||
}
|
||||
],
|
||||
"recipients_by_entity": {
|
||||
"Amicale de Grenoble": [
|
||||
{...}
|
||||
],
|
||||
"Amicale de Lyon": [
|
||||
{...}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Fonctionnalités clés
|
||||
|
||||
### 1. Types de conversations
|
||||
|
||||
#### Private (Conversation privée)
|
||||
- Entre 2 utilisateurs uniquement
|
||||
- Détection automatique de conversation existante
|
||||
- Pas de titre requis
|
||||
|
||||
#### Group (Groupe)
|
||||
- Plusieurs participants
|
||||
- Titre optionnel mais recommandé
|
||||
- Admin de groupe (créateur)
|
||||
|
||||
#### Broadcast (Diffusion)
|
||||
- Réservé aux admins (rôle >= 2)
|
||||
- Communication unidirectionnelle possible
|
||||
- Pour annonces importantes
|
||||
|
||||
### 2. Gestion des permissions
|
||||
|
||||
Le système vérifie automatiquement :
|
||||
- L'appartenance à une conversation avant lecture/écriture
|
||||
- Les droits de création selon le type de conversation
|
||||
- La visibilité des destinataires selon le rôle
|
||||
|
||||
### 3. Statuts de lecture
|
||||
|
||||
- **Accusés de lecture individuels** : Chaque message peut être marqué comme lu
|
||||
- **Compteur de non-lus** : Par conversation et global
|
||||
- **Last read** : Timestamp de dernière lecture par participant
|
||||
|
||||
### 4. Optimisations
|
||||
|
||||
- **Pagination** : Chargement progressif des messages
|
||||
- **Index optimisés** : Pour les requêtes fréquentes
|
||||
- **Vue SQL** : Pour récupération rapide du dernier message
|
||||
|
||||
## Sécurité
|
||||
|
||||
### Chiffrement
|
||||
- Les noms d'utilisateurs sont stockés chiffrés (AES-256)
|
||||
- Déchiffrement à la volée lors de la lecture
|
||||
|
||||
### Validation
|
||||
- Longueur maximale des messages : 5000 caractères
|
||||
- Trim automatique du contenu
|
||||
- Vérification des permissions à chaque action
|
||||
|
||||
### Isolation
|
||||
- Les utilisateurs ne voient que leurs conversations autorisées
|
||||
- Filtrage par entité selon le rôle
|
||||
- Soft delete pour conservation de l'historique
|
||||
|
||||
## Migration
|
||||
|
||||
Exécuter le script SQL :
|
||||
```bash
|
||||
mysql -u root -p geo_app < scripts/sql/create_chat_tables.sql
|
||||
```
|
||||
|
||||
## Évolutions futures possibles
|
||||
|
||||
1. **Notifications push** : Intégration avec Firebase/WebSocket
|
||||
2. **Fichiers joints** : Support d'images et documents
|
||||
3. **Réactions** : Emojis sur les messages
|
||||
4. **Mentions** : @username pour notifier
|
||||
5. **Recherche** : Dans l'historique des messages
|
||||
6. **Chiffrement E2E** : Pour conversations sensibles
|
||||
7. **Statuts de présence** : En ligne/Hors ligne
|
||||
8. **Indicateur de frappe** : "X est en train d'écrire..."
|
||||
|
||||
## Tests
|
||||
|
||||
### Cas de test recommandés
|
||||
|
||||
1. **Création de conversation privée**
|
||||
- Vérifier la détection de conversation existante
|
||||
- Tester avec utilisateurs de différentes entités
|
||||
|
||||
2. **Envoi de messages**
|
||||
- Messages avec caractères UTF-8 (émojis, accents)
|
||||
- Messages très longs (limite 5000)
|
||||
- Messages vides (doivent être rejetés)
|
||||
|
||||
3. **Marquage comme lu**
|
||||
- Marquer messages spécifiques
|
||||
- Marquer tous les messages d'une room
|
||||
- Vérifier les compteurs
|
||||
|
||||
4. **Permissions**
|
||||
- Utilisateur simple ne peut pas créer de broadcast
|
||||
- Accès refusé aux conversations non autorisées
|
||||
- Filtrage correct des destinataires
|
||||
|
||||
## Support
|
||||
|
||||
Pour toute question ou problème :
|
||||
- Vérifier les logs dans `/logs/`
|
||||
- Consulter les tables `chat_*` en base de données
|
||||
- Tester avec les scripts de test fournis
|
||||
138
api/docs/CHK_USER_DELETE_PASS_INFO.md
Normal file
138
api/docs/CHK_USER_DELETE_PASS_INFO.md
Normal file
@@ -0,0 +1,138 @@
|
||||
# Gestion du champ chk_user_delete_pass
|
||||
|
||||
## 📋 Description
|
||||
Le champ `chk_user_delete_pass` permet de contrôler si les membres d'une amicale peuvent supprimer des passages.
|
||||
|
||||
## 🔄 Modifications API
|
||||
|
||||
### 1. Base de données
|
||||
- **Table** : `entites`
|
||||
- **Champ** : `chk_user_delete_pass` TINYINT(1) DEFAULT 0
|
||||
- **Valeurs** :
|
||||
- `0` : Les membres NE peuvent PAS supprimer de passages (par défaut)
|
||||
- `1` : Les membres PEUVENT supprimer des passages
|
||||
|
||||
### 2. Endpoints modifiés
|
||||
|
||||
#### POST /api/entites (Création)
|
||||
- Le champ est automatiquement initialisé à `0` (false) lors de la création
|
||||
- Non modifiable à la création
|
||||
|
||||
#### PUT /api/entites/{id} (Modification)
|
||||
**Entrée JSON :**
|
||||
```json
|
||||
{
|
||||
"chk_user_delete_pass": 1
|
||||
}
|
||||
```
|
||||
- **Type** : Boolean (0 ou 1)
|
||||
- **Obligatoire** : Non
|
||||
- **Accès** : Administrateurs uniquement (fk_role > 1)
|
||||
|
||||
#### GET /api/entites/{id} (Récupération)
|
||||
**Sortie JSON :**
|
||||
```json
|
||||
{
|
||||
"id": 5,
|
||||
"name": "Amicale de Pompiers",
|
||||
"code_postal": "75001",
|
||||
"ville": "Paris",
|
||||
"chk_active": 1,
|
||||
"chk_user_delete_pass": 0
|
||||
}
|
||||
```
|
||||
|
||||
#### GET /api/entites (Liste)
|
||||
Retourne `chk_user_delete_pass` pour chaque entité dans la liste.
|
||||
|
||||
### 3. Route /api/login
|
||||
Le champ `chk_user_delete_pass` est maintenant inclus dans la réponse de login dans les objets `amicale` :
|
||||
|
||||
**Réponse JSON :**
|
||||
```json
|
||||
{
|
||||
"user": { ... },
|
||||
"amicale": {
|
||||
"id": 5,
|
||||
"name": "Amicale de Pompiers",
|
||||
"code_postal": "75001",
|
||||
"ville": "Paris",
|
||||
"chk_demo": 0,
|
||||
"chk_mdp_manuel": 0,
|
||||
"chk_username_manuel": 0,
|
||||
"chk_copie_mail_recu": 0,
|
||||
"chk_accept_sms": 0,
|
||||
"chk_active": 1,
|
||||
"chk_stripe": 0,
|
||||
"chk_user_delete_pass": 0 // ← NOUVEAU CHAMP
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 🎯 Utilisation côté client
|
||||
|
||||
### Flutter/Web
|
||||
Le client doit :
|
||||
1. **Récupérer** la valeur de `chk_user_delete_pass` depuis la réponse login
|
||||
2. **Stocker** cette valeur dans l'état de l'application
|
||||
3. **Conditionner** l'affichage du bouton de suppression selon cette valeur
|
||||
|
||||
**Exemple Flutter :**
|
||||
```dart
|
||||
// Dans le modèle Amicale
|
||||
class Amicale {
|
||||
final int id;
|
||||
final String name;
|
||||
final bool chkUserDeletePass; // Nouveau champ
|
||||
|
||||
bool get canUserDeletePassage => chkUserDeletePass;
|
||||
}
|
||||
|
||||
// Dans l'UI
|
||||
if (amicale.canUserDeletePassage) {
|
||||
// Afficher le bouton de suppression
|
||||
IconButton(
|
||||
icon: Icon(Icons.delete),
|
||||
onPressed: () => deletePassage(passageId),
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
## ⚠️ Points importants
|
||||
|
||||
1. **Valeur par défaut** : Toujours `0` (false) pour la sécurité
|
||||
2. **Modification** : Seuls les administrateurs (fk_role > 1) peuvent modifier ce champ
|
||||
3. **Rétrocompatibilité** : Les entités existantes ont la valeur `0` par défaut
|
||||
4. **Validation côté serveur** : L'API vérifiera également ce droit lors de la tentative de suppression
|
||||
|
||||
## 📝 Script SQL
|
||||
Le script de migration est disponible dans :
|
||||
```
|
||||
/scripts/sql/add_chk_user_delete_pass.sql
|
||||
```
|
||||
|
||||
## ✅ Checklist d'implémentation
|
||||
|
||||
### Côté API (déjà fait) :
|
||||
- [x] Ajout du champ en base de données
|
||||
- [x] Modification EntiteController (create, update, get)
|
||||
- [x] Modification LoginController (réponse login)
|
||||
- [x] Script SQL de migration
|
||||
|
||||
### Côté Client (à faire) :
|
||||
- [ ] Ajouter le champ dans le modèle Amicale
|
||||
- [ ] Parser le champ depuis la réponse login
|
||||
- [ ] Stocker dans l'état de l'application
|
||||
- [ ] Conditionner l'affichage du bouton suppression
|
||||
- [ ] Tester avec des valeurs 0 et 1
|
||||
|
||||
## 🔒 Sécurité
|
||||
Même si `chk_user_delete_pass = 1`, l'API devra vérifier :
|
||||
- L'authentification de l'utilisateur
|
||||
- L'appartenance à l'entité
|
||||
- Le droit de suppression sur le passage spécifique
|
||||
- Les règles métier (ex: pas de suppression après export)
|
||||
|
||||
---
|
||||
**Date :** 20/08/2025
|
||||
**Version API :** 3.1.4
|
||||
165
api/docs/DELETE_PASSAGE_PERMISSIONS.md
Normal file
165
api/docs/DELETE_PASSAGE_PERMISSIONS.md
Normal file
@@ -0,0 +1,165 @@
|
||||
# API DELETE /passages/{id} - Documentation des permissions
|
||||
|
||||
## 📋 Endpoint
|
||||
```
|
||||
DELETE /api/passages/{id}
|
||||
```
|
||||
|
||||
## 🔒 Authentification
|
||||
- **Requise** : OUI (Bearer token)
|
||||
- **Session** : Doit être valide
|
||||
|
||||
## 📊 Logique de permissions
|
||||
|
||||
### Règles par rôle :
|
||||
|
||||
| fk_role | Description | Peut supprimer ? | Conditions |
|
||||
|---------|------------|------------------|------------|
|
||||
| 1 | Membre | ✅ Conditionnel | Si `entites.chk_user_delete_pass = 1` |
|
||||
| 2 | Admin amicale | ✅ OUI | Toujours autorisé |
|
||||
| 3+ | Super admin | ✅ OUI | Toujours autorisé |
|
||||
|
||||
### Détail du contrôle pour les membres (fk_role = 1) :
|
||||
|
||||
```sql
|
||||
-- L'API vérifie :
|
||||
SELECT chk_user_delete_pass
|
||||
FROM entites
|
||||
WHERE id = {user.fk_entite}
|
||||
|
||||
-- Si chk_user_delete_pass = 0 → Erreur 403
|
||||
-- Si chk_user_delete_pass = 1 → Continue
|
||||
```
|
||||
|
||||
## 🔄 Flux de vérification
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
A[DELETE /passages/{id}] --> B{Utilisateur authentifié ?}
|
||||
B -->|Non| C[Erreur 401]
|
||||
B -->|Oui| D{Récupérer fk_role}
|
||||
D --> E{fk_role = 1 ?}
|
||||
E -->|Non| F[Autorisé - Admin]
|
||||
E -->|Oui| G{Vérifier chk_user_delete_pass}
|
||||
G -->|= 0| H[Erreur 403 - Non autorisé]
|
||||
G -->|= 1| F
|
||||
F --> I{Passage existe ?}
|
||||
I -->|Non| J[Erreur 404]
|
||||
I -->|Oui| K{Passage appartient à l'entité ?}
|
||||
K -->|Non| L[Erreur 404]
|
||||
K -->|Oui| M[Soft delete : chk_active = 0]
|
||||
M --> N[Succès 200]
|
||||
```
|
||||
|
||||
## 📝 Réponses
|
||||
|
||||
### ✅ Succès (200)
|
||||
```json
|
||||
{
|
||||
"status": "success",
|
||||
"message": "Passage supprimé avec succès"
|
||||
}
|
||||
```
|
||||
|
||||
### ❌ Erreur 401 - Non authentifié
|
||||
```json
|
||||
{
|
||||
"status": "error",
|
||||
"message": "Vous devez être connecté pour effectuer cette action"
|
||||
}
|
||||
```
|
||||
|
||||
### ❌ Erreur 403 - Permission refusée (membre sans autorisation)
|
||||
```json
|
||||
{
|
||||
"status": "error",
|
||||
"message": "Vous n'avez pas l'autorisation de supprimer des passages"
|
||||
}
|
||||
```
|
||||
|
||||
### ❌ Erreur 404 - Passage non trouvé
|
||||
```json
|
||||
{
|
||||
"status": "error",
|
||||
"message": "Passage non trouvé"
|
||||
}
|
||||
```
|
||||
|
||||
## 📊 Logging
|
||||
|
||||
L'API enregistre :
|
||||
|
||||
### En cas de tentative non autorisée :
|
||||
```php
|
||||
LogService::log('Tentative de suppression de passage non autorisée', [
|
||||
'level' => 'warning',
|
||||
'userId' => $userId,
|
||||
'userRole' => $userRole,
|
||||
'entiteId' => $entiteId,
|
||||
'passageId' => $passageId,
|
||||
'chk_user_delete_pass' => 0
|
||||
]);
|
||||
```
|
||||
|
||||
### En cas de succès :
|
||||
```php
|
||||
LogService::log('Suppression d\'un passage', [
|
||||
'level' => 'info',
|
||||
'userId' => $userId,
|
||||
'passageId' => $passageId
|
||||
]);
|
||||
```
|
||||
|
||||
## 🎯 Exemple d'utilisation
|
||||
|
||||
### Requête
|
||||
```bash
|
||||
curl -X DELETE https://api.geosector.fr/api/passages/19500576 \
|
||||
-H "Authorization: Bearer {session_token}" \
|
||||
-H "Content-Type: application/json"
|
||||
```
|
||||
|
||||
### Scénarios
|
||||
|
||||
#### Scénario 1 : Membre avec permission ✅
|
||||
- Utilisateur : fk_role = 1
|
||||
- Entité : chk_user_delete_pass = 1
|
||||
- **Résultat** : Suppression autorisée
|
||||
|
||||
#### Scénario 2 : Membre sans permission ❌
|
||||
- Utilisateur : fk_role = 1
|
||||
- Entité : chk_user_delete_pass = 0
|
||||
- **Résultat** : Erreur 403
|
||||
|
||||
#### Scénario 3 : Admin amicale ✅
|
||||
- Utilisateur : fk_role = 2
|
||||
- **Résultat** : Suppression autorisée (pas de vérification chk_user_delete_pass)
|
||||
|
||||
## ⚠️ Notes importantes
|
||||
|
||||
1. **Soft delete** : Le passage n'est pas supprimé physiquement, seulement `chk_active = 0`
|
||||
2. **Traçabilité** : `updated_at` et `fk_user_modif` sont mis à jour
|
||||
3. **Contrôle entité** : Un utilisateur ne peut supprimer que les passages de son entité
|
||||
4. **Log warning** : Toute tentative non autorisée est loggée en niveau WARNING
|
||||
|
||||
## 🔧 Configuration côté amicale
|
||||
|
||||
Pour autoriser les membres à supprimer des passages :
|
||||
|
||||
```sql
|
||||
UPDATE entites
|
||||
SET chk_user_delete_pass = 1
|
||||
WHERE id = {entite_id};
|
||||
```
|
||||
|
||||
Cette modification ne peut être faite que par un administrateur (fk_role > 1) via l'endpoint :
|
||||
```
|
||||
PUT /api/entites/{id}
|
||||
{
|
||||
"chk_user_delete_pass": 1
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
**Version API** : 3.1.4
|
||||
**Date** : 20/08/2025
|
||||
276
api/docs/EXPORT-SYSTEM.md
Executable file
276
api/docs/EXPORT-SYSTEM.md
Executable file
@@ -0,0 +1,276 @@
|
||||
# Système d'Export/Import d'Opérations - Geosector API
|
||||
|
||||
## Vue d'ensemble
|
||||
|
||||
Le système d'export/import permet de sauvegarder et restaurer des opérations complètes avec toutes leurs données associées (passages, utilisateurs, secteurs, relations).
|
||||
|
||||
## Architecture
|
||||
|
||||
### Routes API
|
||||
|
||||
#### Exports
|
||||
|
||||
- `GET /api/operations/{id}/export/excel` - Export Excel (consultation)
|
||||
- `GET /api/operations/{id}/export/json` - Export JSON (sauvegarde)
|
||||
- `GET /api/operations/{id}/export/full` - Export combiné (Excel + JSON)
|
||||
|
||||
#### Gestion des sauvegardes
|
||||
|
||||
- `GET /api/operations/{id}/backups` - Liste des sauvegardes
|
||||
- `GET /api/operations/{id}/backups/{backup_id}` - Télécharger une sauvegarde
|
||||
- `DELETE /api/operations/{id}/backups/{backup_id}` - Supprimer une sauvegarde
|
||||
|
||||
### Structure des fichiers
|
||||
|
||||
```
|
||||
uploads/entites/{entite_id}/operations/{operation_id}/documents/exports/
|
||||
├── excel/
|
||||
│ └── geosector-export-{operation_id}-{timestamp}.xlsx
|
||||
└── json/
|
||||
└── geosector-backup-{operation_id}-{type}-{timestamp}.json
|
||||
```
|
||||
|
||||
## Export Excel
|
||||
|
||||
### Contenu
|
||||
|
||||
Le fichier Excel contient 4 feuilles :
|
||||
|
||||
#### 1. Feuille "Passages"
|
||||
|
||||
- **Colonnes** : ID_Passage, Date, Heure, Prénom, Nom, Tournée, Type, N°, Rue, Ville, Habitat, Donateur, Email, Tél, Montant, Règlement, Remarque, FK_User, FK_Sector, FK_Operation
|
||||
- **Données déchiffrées** : Noms, emails, téléphones
|
||||
- **Formatage** : Dates françaises (dd/mm/yyyy), types traduits
|
||||
|
||||
#### 2. Feuille "Utilisateurs"
|
||||
|
||||
- **Colonnes** : ID_User, Nom, Prénom, Email, Téléphone, Mobile, Rôle, Date_création, Actif, FK_Entite
|
||||
- **Données déchiffrées** : Informations personnelles
|
||||
|
||||
#### 3. Feuille "Secteurs"
|
||||
|
||||
- **Colonnes** : ID_Sector, Libellé, Couleur, Date_création, Actif, FK_Operation
|
||||
|
||||
#### 4. Feuille "Secteurs-Utilisateurs"
|
||||
|
||||
- **Colonnes** : ID_Relation, FK_Sector, Nom_Secteur, FK_User, Nom_Utilisateur, Date_assignation, FK_Operation
|
||||
|
||||
### Paramètres optionnels
|
||||
|
||||
- `?user_id={id}` - Filtrer les passages par utilisateur
|
||||
|
||||
### Exemple d'utilisation
|
||||
|
||||
```bash
|
||||
# Export complet
|
||||
GET /api/operations/2644/export/excel
|
||||
|
||||
# Export filtré par utilisateur
|
||||
GET /api/operations/2644/export/excel?user_id=123
|
||||
```
|
||||
|
||||
## Export JSON
|
||||
|
||||
### Structure du fichier JSON
|
||||
|
||||
```json
|
||||
{
|
||||
"export_metadata": {
|
||||
"version": "1.0",
|
||||
"export_date": "2025-06-21T16:19:23Z",
|
||||
"source_entite_id": 5,
|
||||
"export_type": "full_operation"
|
||||
},
|
||||
"operation": {
|
||||
"id": 2644,
|
||||
"libelle": "OPE 2024-25",
|
||||
"date_deb": "2024-09-01",
|
||||
"date_fin": "2025-05-30",
|
||||
"fk_entite": 5,
|
||||
"chk_distinct_sectors": 1,
|
||||
"created_at": "2024-08-15T10:00:00Z"
|
||||
},
|
||||
"users": [...],
|
||||
"sectors": [...],
|
||||
"passages": [...],
|
||||
"user_sectors": [...]
|
||||
}
|
||||
```
|
||||
|
||||
### Types d'export JSON
|
||||
|
||||
- **manual** : Export à la demande (par défaut)
|
||||
- **auto** : Sauvegarde automatique (avant modifications importantes)
|
||||
|
||||
### Paramètres
|
||||
|
||||
- `?type=manual|auto` - Type d'export
|
||||
|
||||
## Sécurité
|
||||
|
||||
### Contrôles d'accès
|
||||
|
||||
- ✅ Authentification obligatoire
|
||||
- ✅ Vérification d'appartenance à l'entité
|
||||
- ✅ Isolation des données par entité
|
||||
- ✅ Logs détaillés de toutes les opérations
|
||||
|
||||
### Données sensibles
|
||||
|
||||
- ✅ Chiffrement/déchiffrement automatique
|
||||
- ✅ Données personnelles protégées
|
||||
- ✅ Pas d'exposition des clés de chiffrement
|
||||
|
||||
## Stockage et organisation
|
||||
|
||||
### Enregistrement en base
|
||||
|
||||
Tous les fichiers sont enregistrés dans la table `medias` :
|
||||
|
||||
```sql
|
||||
support = 'operation'
|
||||
support_id = {operation_id}
|
||||
file_type = 'xlsx' | 'json'
|
||||
description = 'Export Excel opération - {libelle}'
|
||||
```
|
||||
|
||||
### Métadonnées des fichiers
|
||||
|
||||
- **ID** : Identifiant unique en base
|
||||
- **Filename** : Nom du fichier généré
|
||||
- **Path** : Chemin relatif depuis la racine
|
||||
- **Size** : Taille en octets
|
||||
- **Type** : excel | json
|
||||
|
||||
## Exemples de réponses API
|
||||
|
||||
### Export Excel réussi
|
||||
|
||||
```json
|
||||
{
|
||||
"status": "success",
|
||||
"message": "Export Excel généré avec succès",
|
||||
"file": {
|
||||
"id": 123,
|
||||
"filename": "geosector-export-2644-20250621-161923.xlsx",
|
||||
"path": "uploads/entites/5/operations/2644/documents/exports/excel/geosector-export-2644-20250621-161923.xlsx",
|
||||
"size": 45678,
|
||||
"type": "excel"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Export complet réussi
|
||||
|
||||
```json
|
||||
{
|
||||
"status": "success",
|
||||
"message": "Export complet généré avec succès",
|
||||
"files": {
|
||||
"excel": {
|
||||
"id": 123,
|
||||
"filename": "geosector-export-2644-20250621-161923.xlsx",
|
||||
"path": "uploads/entites/5/operations/2644/documents/exports/excel/geosector-export-2644-20250621-161923.xlsx",
|
||||
"size": 45678,
|
||||
"type": "excel"
|
||||
},
|
||||
"json": {
|
||||
"id": 124,
|
||||
"filename": "geosector-backup-2644-manual-20250621-161923.json",
|
||||
"path": "uploads/entites/5/operations/2644/documents/exports/json/geosector-backup-2644-manual-20250621-161923.json",
|
||||
"size": 12345,
|
||||
"type": "json"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Liste des sauvegardes
|
||||
|
||||
```json
|
||||
{
|
||||
"status": "success",
|
||||
"backups": [
|
||||
{
|
||||
"id": 124,
|
||||
"fichier": "geosector-backup-2644-manual-20250621-161923.json",
|
||||
"file_type": "json",
|
||||
"file_size": 12345,
|
||||
"description": "Sauvegarde JSON opération - manual - OPE 2024-25",
|
||||
"created_at": "2025-06-21 16:19:23",
|
||||
"fk_user_creat": 1
|
||||
},
|
||||
{
|
||||
"id": 123,
|
||||
"fichier": "geosector-export-2644-20250621-161923.xlsx",
|
||||
"file_type": "xlsx",
|
||||
"file_size": 45678,
|
||||
"description": "Export Excel opération - OPE 2024-25",
|
||||
"created_at": "2025-06-21 16:19:23",
|
||||
"fk_user_creat": 1
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## Installation et dépendances
|
||||
|
||||
### PhpSpreadsheet
|
||||
|
||||
```bash
|
||||
composer require phpoffice/phpspreadsheet
|
||||
```
|
||||
|
||||
### Permissions de dossiers
|
||||
|
||||
```bash
|
||||
chmod 755 uploads/
|
||||
chmod 755 uploads/entites/
|
||||
```
|
||||
|
||||
## Gestion des erreurs
|
||||
|
||||
### Erreurs courantes
|
||||
|
||||
- **401** : Non authentifié
|
||||
- **403** : Pas d'accès à l'entité
|
||||
- **404** : Opération non trouvée
|
||||
- **500** : Erreur de génération
|
||||
|
||||
### Logs
|
||||
|
||||
Tous les événements sont loggés via `LogService` :
|
||||
|
||||
- Exports réussis (level: info)
|
||||
- Erreurs de génération (level: error)
|
||||
- Tentatives d'accès non autorisées (level: warning)
|
||||
|
||||
## Maintenance
|
||||
|
||||
### Nettoyage automatique (à implémenter)
|
||||
|
||||
- Sauvegardes auto > 30 jours
|
||||
- Fichiers temporaires > 24h
|
||||
- Vérification cohérence base/fichiers
|
||||
|
||||
### Monitoring
|
||||
|
||||
- Espace disque utilisé
|
||||
- Nombre de fichiers par entité
|
||||
- Fréquence des exports
|
||||
|
||||
## Évolutions futures
|
||||
|
||||
### Import/Restauration
|
||||
|
||||
- Validation des fichiers JSON
|
||||
- Import transactionnel
|
||||
- Gestion des conflits d'IDs
|
||||
- Mapping entités source/cible
|
||||
|
||||
### Optimisations
|
||||
|
||||
- Compression des fichiers
|
||||
- Export asynchrone pour gros volumes
|
||||
- Cache des exports fréquents
|
||||
- API de streaming pour téléchargements
|
||||
376
api/docs/FILE-SYSTEM-API.md
Executable file
376
api/docs/FILE-SYSTEM-API.md
Executable file
@@ -0,0 +1,376 @@
|
||||
# API de Gestion des Fichiers - Geosector
|
||||
|
||||
## Vue d'ensemble
|
||||
|
||||
L'API de gestion des fichiers permet aux administrateurs de naviguer, rechercher et gérer les fichiers stockés dans l'application Geosector avec des contrôles d'accès basés sur les rôles.
|
||||
|
||||
## Contrôles d'accès
|
||||
|
||||
### Rôle 2 (Admin d'entité)
|
||||
|
||||
- Accès limité aux fichiers de son entité uniquement
|
||||
- Chemin racine : `/uploads/entites/{son_entite_id}/`
|
||||
- Peut naviguer dans tous les sous-dossiers de son entité
|
||||
|
||||
### Rôle > 2 (Super admin)
|
||||
|
||||
- Accès complet à tous les fichiers
|
||||
- Chemin racine : `/uploads/` (accès total)
|
||||
- Peut naviguer dans toutes les entités et dossiers système
|
||||
|
||||
## Routes disponibles
|
||||
|
||||
### Navigation et listing
|
||||
|
||||
#### `GET /api/files/browse`
|
||||
|
||||
Navigation dans l'arborescence avec recherche et pagination.
|
||||
|
||||
**Paramètres de requête :**
|
||||
|
||||
- `path` (string) : Chemin à explorer (ex: `entites/5/operations`)
|
||||
- `page` (int) : Page (défaut: 1)
|
||||
- `per_page` (int) : Éléments par page (défaut: 50, max: 100)
|
||||
- `search` (string) : Recherche dans nom, nom original, description
|
||||
- `type` (string) : Filtrage par extension (pdf, jpg, xlsx, etc.)
|
||||
- `category` (string) : Filtrage par catégorie métier
|
||||
- `sort` (string) : Tri (name, date, size, type) - défaut: date
|
||||
- `order` (string) : Ordre (asc, desc) - défaut: desc
|
||||
|
||||
**Exemple :**
|
||||
|
||||
```bash
|
||||
GET /api/files/browse?path=entites/5/operations&search=2024&type=xlsx&page=1
|
||||
```
|
||||
|
||||
**Réponse :**
|
||||
|
||||
```json
|
||||
{
|
||||
"status": "success",
|
||||
"current_path": "entites/5/operations",
|
||||
"parent_path": "entites/5",
|
||||
"pagination": {
|
||||
"current_page": 1,
|
||||
"per_page": 50,
|
||||
"total_items": 127,
|
||||
"total_pages": 3,
|
||||
"has_next": true,
|
||||
"has_prev": false
|
||||
},
|
||||
"filters": {
|
||||
"search": "2024",
|
||||
"type": "xlsx",
|
||||
"category": null,
|
||||
"sort": "date",
|
||||
"order": "desc"
|
||||
},
|
||||
"files": [
|
||||
{
|
||||
"id": 123,
|
||||
"fichier": "planning_2024_op2644.xlsx",
|
||||
"original_name": "Planning Opération 2024.xlsx",
|
||||
"file_type": "xlsx",
|
||||
"file_category": "planning",
|
||||
"description": "Planning détaillé opération 2024",
|
||||
"file_size": 1024000,
|
||||
"file_path": "entites/5/operations/2644/documents/planning_2024_op2644.xlsx",
|
||||
"created_at": "2025-06-22 08:30:00",
|
||||
"creator_name": "Jean Dupont"
|
||||
}
|
||||
],
|
||||
"summary": {
|
||||
"total_files": 45,
|
||||
"total_size": 25600000,
|
||||
"by_category": {
|
||||
"planning": 12,
|
||||
"export": 20,
|
||||
"backup": 13
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### `GET /api/files/list/{support}/{id}`
|
||||
|
||||
Liste des fichiers par support (entite, user, operation, passage).
|
||||
|
||||
**Paramètres :**
|
||||
|
||||
- `support` : Type de support (entite, user, operation, passage)
|
||||
- `id` : ID de l'élément
|
||||
- Mêmes paramètres de requête que `/browse`
|
||||
|
||||
**Exemple :**
|
||||
|
||||
```bash
|
||||
GET /api/files/list/operation/2644?category=export&page=1
|
||||
```
|
||||
|
||||
### Recherche
|
||||
|
||||
#### `GET /api/files/search`
|
||||
|
||||
Recherche globale dans tous les fichiers accessibles.
|
||||
|
||||
**Paramètres de requête :**
|
||||
|
||||
- `q` (string, requis) : Terme de recherche
|
||||
- `page`, `per_page`, `type`, `category`, `sort`, `order` : Mêmes que browse
|
||||
|
||||
**Exemple :**
|
||||
|
||||
```bash
|
||||
GET /api/files/search?q=planning&type=xlsx&category=planning
|
||||
```
|
||||
|
||||
### Actions sur fichiers
|
||||
|
||||
#### `GET /api/files/download/{id}`
|
||||
|
||||
Téléchargement sécurisé d'un fichier.
|
||||
|
||||
**Réponse :** Fichier en téléchargement direct avec headers appropriés.
|
||||
|
||||
#### `DELETE /api/files/{id}`
|
||||
|
||||
Suppression sécurisée d'un fichier (physique + base de données).
|
||||
|
||||
**Réponse :**
|
||||
|
||||
```json
|
||||
{
|
||||
"status": "success",
|
||||
"message": "Fichier supprimé avec succès"
|
||||
}
|
||||
```
|
||||
|
||||
#### `GET /api/files/info/{id}`
|
||||
|
||||
Informations détaillées d'un fichier.
|
||||
|
||||
**Réponse :**
|
||||
|
||||
```json
|
||||
{
|
||||
"status": "success",
|
||||
"file": {
|
||||
"id": 123,
|
||||
"fichier": "planning_2024.xlsx",
|
||||
"original_name": "Planning Opération 2024.xlsx",
|
||||
"file_type": "xlsx",
|
||||
"file_category": "planning",
|
||||
"file_size": 1024000,
|
||||
"mime_type": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
||||
"description": "Planning détaillé",
|
||||
"support": "operation",
|
||||
"support_id": 2644,
|
||||
"fk_entite": 5,
|
||||
"created_at": "2025-06-22 08:30:00",
|
||||
"updated_at": "2025-06-22 08:30:00",
|
||||
"creator_name": "Jean Dupont",
|
||||
"modifier_name": null,
|
||||
"file_exists": true
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Statistiques
|
||||
|
||||
#### `GET /api/files/stats`
|
||||
|
||||
Statistiques d'utilisation des fichiers.
|
||||
|
||||
**Pour admin d'entité (rôle 2) :**
|
||||
|
||||
```json
|
||||
{
|
||||
"status": "success",
|
||||
"entite_id": 5,
|
||||
"storage": {
|
||||
"total_files": 245,
|
||||
"total_size": 157286400,
|
||||
"by_support": {
|
||||
"entite": { "count": 12, "size": 45000000 },
|
||||
"operation": { "count": 180, "size": 98000000 },
|
||||
"user": { "count": 45, "size": 12000000 },
|
||||
"passage": { "count": 8, "size": 2286400 }
|
||||
},
|
||||
"by_category": {
|
||||
"document": 25,
|
||||
"export": 120,
|
||||
"avatar": 45,
|
||||
"photo": 55
|
||||
},
|
||||
"by_type": {
|
||||
"xlsx": 85,
|
||||
"jpg": 120,
|
||||
"pdf": 40
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Pour super admin (rôle > 2) :**
|
||||
|
||||
```json
|
||||
{
|
||||
"status": "success",
|
||||
"global_stats": {
|
||||
"total_files": 2450,
|
||||
"total_size": 1572864000,
|
||||
"entites_count": 25,
|
||||
"by_entite": [
|
||||
{ "entite_id": 5, "files": 245, "size": 157286400 },
|
||||
{ "entite_id": 12, "files": 180, "size": 98000000 }
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Métadonnées
|
||||
|
||||
#### `GET /api/files/metadata`
|
||||
|
||||
Informations sur les catégories, extensions et limites autorisées.
|
||||
|
||||
**Réponse :**
|
||||
|
||||
```json
|
||||
{
|
||||
"status": "success",
|
||||
"categories": {
|
||||
"entite": ["logo", "document", "reglement", "statut"],
|
||||
"user": ["avatar", "photo"],
|
||||
"operation": ["planning", "liste", "export", "backup"],
|
||||
"passage": ["recu", "photo", "justificatif", "carte"]
|
||||
},
|
||||
"extensions": ["pdf", "jpg", "jpeg", "png", "gif", "webp", "xlsx", "xls", "json", "csv"],
|
||||
"mime_types": {
|
||||
"pdf": "application/pdf",
|
||||
"jpg": "image/jpeg",
|
||||
"xlsx": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
|
||||
},
|
||||
"max_file_sizes": {
|
||||
"entite": 20971520, // 20 MB
|
||||
"user": 5242880, // 5 MB
|
||||
"operation": 20971520, // 20 MB
|
||||
"passage": 10485760 // 10 MB
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Catégories de fichiers
|
||||
|
||||
### Distinction Extension vs Catégorie
|
||||
|
||||
- **Extension** (`file_type`) : Type technique (pdf, jpg, xlsx, png, etc.)
|
||||
- **Catégorie** (`file_category`) : Type métier (logo, carte, photo, document, planning, etc.)
|
||||
|
||||
### Catégories par support
|
||||
|
||||
#### Entité
|
||||
|
||||
- `logo` : Logo de l'entité
|
||||
- `document` : Documents généraux
|
||||
- `reglement` : Règlements internes
|
||||
- `statut` : Statuts de l'entité
|
||||
|
||||
#### Utilisateur
|
||||
|
||||
- `avatar` : Photo de profil
|
||||
- `photo` : Photos diverses
|
||||
|
||||
#### Opération
|
||||
|
||||
- `planning` : Plannings d'opération
|
||||
- `liste` : Listes diverses
|
||||
- `export` : Exports de données
|
||||
- `backup` : Sauvegardes automatiques
|
||||
|
||||
#### Passage
|
||||
|
||||
- `recu` : Reçus de passage
|
||||
- `photo` : Photos de passage
|
||||
- `justificatif` : Justificatifs divers
|
||||
- `carte` : Cartes et plans
|
||||
|
||||
## Sécurité
|
||||
|
||||
### Validation des chemins
|
||||
|
||||
- Empêche les traversées de répertoire (`../`)
|
||||
- Validation stricte selon le rôle utilisateur
|
||||
- Contrôle d'accès au niveau fichier
|
||||
|
||||
### Logs
|
||||
|
||||
- Tous les téléchargements sont loggés
|
||||
- Toutes les suppressions sont tracées
|
||||
- Erreurs d'accès enregistrées
|
||||
|
||||
### Contrôles d'intégrité
|
||||
|
||||
- Vérification de l'existence physique des fichiers
|
||||
- Validation des permissions avant chaque action
|
||||
- Contrôle de cohérence base/fichiers
|
||||
|
||||
## Exemples d'utilisation
|
||||
|
||||
### Navigation dans les opérations d'une entité
|
||||
|
||||
```bash
|
||||
GET /api/files/browse?path=entites/5/operations&sort=name&order=asc
|
||||
```
|
||||
|
||||
### Recherche de tous les exports Excel
|
||||
|
||||
```bash
|
||||
GET /api/files/search?q=export&type=xlsx&category=export
|
||||
```
|
||||
|
||||
### Statistiques de stockage
|
||||
|
||||
```bash
|
||||
GET /api/files/stats
|
||||
```
|
||||
|
||||
### Téléchargement d'un fichier
|
||||
|
||||
```bash
|
||||
GET /api/files/download/123
|
||||
```
|
||||
|
||||
### Suppression d'un fichier
|
||||
|
||||
```bash
|
||||
DELETE /api/files/123
|
||||
```
|
||||
|
||||
## Codes d'erreur
|
||||
|
||||
- **401** : Non authentifié
|
||||
- **403** : Accès refusé (rôle insuffisant ou fichier d'une autre entité)
|
||||
- **404** : Fichier ou chemin non trouvé
|
||||
- **400** : Paramètres invalides (terme de recherche manquant, etc.)
|
||||
- **500** : Erreur serveur
|
||||
|
||||
## Migration base de données
|
||||
|
||||
Pour utiliser le système, exécuter la migration :
|
||||
|
||||
```sql
|
||||
-- Ajout de la colonne file_category
|
||||
ALTER TABLE `medias`
|
||||
ADD COLUMN `file_category` varchar(50) DEFAULT NULL COMMENT 'Catégorie du fichier (logo, carte, photo, document, etc.)' AFTER `file_type`;
|
||||
|
||||
-- Index pour optimiser les requêtes
|
||||
ALTER TABLE `medias`
|
||||
ADD INDEX `idx_file_category` (`file_category`);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
**Version** : 1.0
|
||||
**Date** : Juin 2025
|
||||
**Auteur** : API Geosector Team
|
||||
176
api/docs/FIX_USER_CREATION_400_ERRORS.md
Normal file
176
api/docs/FIX_USER_CREATION_400_ERRORS.md
Normal file
@@ -0,0 +1,176 @@
|
||||
# Correction des erreurs 400 lors de la création d'utilisateurs
|
||||
|
||||
## Problème identifié
|
||||
Un administrateur (fk_role=2) rencontrait des erreurs 400 répétées lors de tentatives de création de membre, menant à un bannissement par fail2ban :
|
||||
- 17:09:39 - POST /api/users HTTP/1.1 400 (Bad Request)
|
||||
- 17:10:44 - POST /api/users/check-username HTTP/1.1 400 (Bad Request)
|
||||
- 17:11:21 - POST /api/users HTTP/1.1 400 (Bad Request)
|
||||
|
||||
## Causes identifiées
|
||||
|
||||
### 1. Conflit de routage (CRITIQUE)
|
||||
**Problème:** La route `/api/users/check-username` était déclarée APRÈS la route générique `/api/users` dans Router.php, causant une mauvaise interprétation où "check-username" était traité comme un ID utilisateur.
|
||||
|
||||
**Solution:** Déplacer la déclaration de la route spécifique AVANT les routes avec paramètres.
|
||||
|
||||
### 2. Messages d'erreur non informatifs
|
||||
**Problème:** Les erreurs 400 retournaient des messages génériques sans détails sur le champ problématique.
|
||||
|
||||
**Solution:** Ajout de messages d'erreur détaillés incluant :
|
||||
- Le champ en erreur (`field`)
|
||||
- La valeur problématique (`value`)
|
||||
- Le format attendu (`format`)
|
||||
- La raison de l'erreur (`reason`)
|
||||
|
||||
### 3. Manque de logs de débogage
|
||||
**Problème:** Aucun log n'était généré pour tracer les erreurs de validation.
|
||||
|
||||
**Solution:** Ajout de logs détaillés à chaque point de validation.
|
||||
|
||||
## Modifications apportées
|
||||
|
||||
### 1. Router.php (ligne 36-44)
|
||||
```php
|
||||
// AVANT (incorrect)
|
||||
$this->post('users', ['UserController', 'createUser']);
|
||||
$this->post('users/check-username', ['UserController', 'checkUsername']);
|
||||
|
||||
// APRÈS (correct)
|
||||
$this->post('users/check-username', ['UserController', 'checkUsername']); // Route spécifique en premier
|
||||
$this->post('users', ['UserController', 'createUser']);
|
||||
```
|
||||
|
||||
### 2. UserController.php - Amélioration des validations
|
||||
|
||||
#### Validation de l'email
|
||||
```php
|
||||
// Réponse améliorée
|
||||
Response::json([
|
||||
'status' => 'error',
|
||||
'message' => 'Email requis',
|
||||
'field' => 'email' // Indique clairement le champ problématique
|
||||
], 400);
|
||||
```
|
||||
|
||||
#### Validation du username manuel
|
||||
```php
|
||||
// Réponse améliorée
|
||||
Response::json([
|
||||
'status' => 'error',
|
||||
'message' => 'Le nom d\'utilisateur est requis pour cette entité',
|
||||
'field' => 'username',
|
||||
'reason' => 'L\'entité requiert la saisie manuelle des identifiants'
|
||||
], 400);
|
||||
```
|
||||
|
||||
#### Format du username
|
||||
```php
|
||||
// Réponse améliorée
|
||||
Response::json([
|
||||
'status' => 'error',
|
||||
'message' => 'Format du nom d\'utilisateur invalide',
|
||||
'field' => 'username',
|
||||
'format' => '10-30 caractères, commence par une lettre, caractères autorisés: a-z, 0-9, ., -, _',
|
||||
'value' => $username // Montre la valeur soumise
|
||||
], 400);
|
||||
```
|
||||
|
||||
### 3. Ajout de logs détaillés
|
||||
|
||||
Chaque point de validation génère maintenant un log avec :
|
||||
- Le type d'erreur
|
||||
- L'utilisateur qui fait la requête
|
||||
- Les données reçues (sans données sensibles)
|
||||
- Le contexte de l'erreur
|
||||
|
||||
Exemple :
|
||||
```php
|
||||
LogService::log('Erreur création utilisateur : Format username invalide', [
|
||||
'level' => 'warning',
|
||||
'createdBy' => $currentUserId,
|
||||
'email' => $email,
|
||||
'username' => $username,
|
||||
'username_length' => strlen($username)
|
||||
]);
|
||||
```
|
||||
|
||||
## Cas d'erreur 400 possibles
|
||||
|
||||
### Pour /api/users (création)
|
||||
|
||||
1. **Email manquant ou vide**
|
||||
- Message: "Email requis"
|
||||
- Field: "email"
|
||||
|
||||
2. **Nom manquant ou vide**
|
||||
- Message: "Nom requis"
|
||||
- Field: "name"
|
||||
|
||||
3. **Format email invalide**
|
||||
- Message: "Format d'email invalide"
|
||||
- Field: "email"
|
||||
- Value: [email soumis]
|
||||
|
||||
4. **Username manuel requis mais manquant** (si chk_username_manuel=1)
|
||||
- Message: "Le nom d'utilisateur est requis pour cette entité"
|
||||
- Field: "username"
|
||||
- Reason: "L'entité requiert la saisie manuelle des identifiants"
|
||||
|
||||
5. **Format username invalide**
|
||||
- Message: "Format du nom d'utilisateur invalide"
|
||||
- Field: "username"
|
||||
- Format: "10-30 caractères, commence par une lettre..."
|
||||
- Value: [username soumis]
|
||||
|
||||
6. **Mot de passe manuel requis mais manquant** (si chk_mdp_manuel=1)
|
||||
- Message: "Le mot de passe est requis pour cette entité"
|
||||
- Field: "password"
|
||||
- Reason: "L'entité requiert la saisie manuelle des mots de passe"
|
||||
|
||||
### Pour /api/users/check-username
|
||||
|
||||
1. **Username manquant**
|
||||
- Message: "Username requis pour la vérification"
|
||||
- Field: "username"
|
||||
|
||||
2. **Format username invalide**
|
||||
- Message: "Format invalide"
|
||||
- Field: "username"
|
||||
- Format: "10-30 caractères, commence par une lettre..."
|
||||
- Value: [username soumis]
|
||||
|
||||
## Test de la solution
|
||||
|
||||
Un script de test a été créé : `/tests/test_user_creation.php`
|
||||
|
||||
Il teste tous les cas d'erreur possibles et vérifie que :
|
||||
1. Les codes HTTP sont corrects
|
||||
2. Les messages d'erreur sont informatifs
|
||||
3. Les champs en erreur sont identifiés
|
||||
|
||||
## Recommandations pour éviter le bannissement fail2ban
|
||||
|
||||
1. **Côté client (application Flutter)** :
|
||||
- Valider les données AVANT l'envoi
|
||||
- Afficher clairement les erreurs à l'utilisateur
|
||||
- Implémenter un délai entre les tentatives (rate limiting côté client)
|
||||
|
||||
2. **Côté API** :
|
||||
- Les messages d'erreur détaillés permettent maintenant de corriger rapidement les problèmes
|
||||
- Les logs permettent de diagnostiquer les problèmes récurrents
|
||||
|
||||
3. **Configuration fail2ban** :
|
||||
- Considérer d'augmenter le seuil pour les erreurs 400 (ex: 5 tentatives au lieu de 3)
|
||||
- Exclure certaines IP de confiance si nécessaire
|
||||
|
||||
## Suivi des logs
|
||||
|
||||
Les logs sont maintenant générés dans :
|
||||
- `/logs/geosector-[environment]-[date].log` : Logs généraux avec détails des erreurs
|
||||
|
||||
Format des logs :
|
||||
```
|
||||
timestamp;browser;os;client_type;level;metadata;message
|
||||
```
|
||||
|
||||
Les erreurs de validation sont loggées avec le niveau "warning" pour permettre un suivi sans être critiques.
|
||||
430
api/docs/GESTION-SECTORS.md
Normal file
430
api/docs/GESTION-SECTORS.md
Normal file
@@ -0,0 +1,430 @@
|
||||
# GESTION-SECTORS.md
|
||||
|
||||
## Vue d'ensemble
|
||||
|
||||
Ce document décrit le système de gestion des secteurs dans l'API Geosector, incluant la connexion aux bases de données d'adresses externes, la validation des limites départementales, et le processus complet de création de secteurs avec génération automatique des passages.
|
||||
|
||||
## Évolutions récentes
|
||||
|
||||
### Gestion des sessions
|
||||
- La session stocke maintenant `entity_id` depuis `fk_entite` lors du login
|
||||
- Méthode `Session::getEntityId()` disponible pour récupérer l'ID de l'entité
|
||||
- Utilisation cohérente de l'entity_id dans toutes les opérations
|
||||
|
||||
### Gestion des passages orphelins
|
||||
- Les passages avec `fk_sector = 0` sont automatiquement intégrés au nouveau secteur
|
||||
- Évite les doublons pour les passages ayant déjà une `fk_adresse`
|
||||
- Mise à jour atomique dans la transaction de création du secteur
|
||||
|
||||
## Architecture multi-bases
|
||||
|
||||
### Bases de données principales
|
||||
|
||||
1. **Base principale** (`geosector_app`)
|
||||
- Contient toutes les tables de l'application
|
||||
- Tables concernées : `ope_sectors`, `sectors_adresses`, `ope_pass`, `ope_users_sectors`, `x_departements_contours`
|
||||
|
||||
2. **Base adresses** (dans conteneurs Incus séparés)
|
||||
- DVA : `dva-maria` (13.23.33.46) - base `adresses`
|
||||
- RCA : `rca-maria` (13.23.33.36) - base `adresses`
|
||||
- PRA : `pra-maria` (13.23.33.26) - base `adresses`
|
||||
- Credentials : `adr_geo_user` / `d66,AdrGeoDev.User`
|
||||
- Tables par département : `cp22`, `cp23`, etc.
|
||||
|
||||
### Configuration
|
||||
|
||||
Dans `src/Config/AppConfig.php` :
|
||||
|
||||
```php
|
||||
'addresses_database' => [
|
||||
'host' => '13.23.33.46', // Varie selon l'environnement
|
||||
'name' => 'adresses',
|
||||
'username' => 'adr_geo_user',
|
||||
'password' => 'd66,AdrGeoDev.User',
|
||||
],
|
||||
```
|
||||
|
||||
## Gestion des contours départementaux
|
||||
|
||||
### Table x_departements_contours
|
||||
|
||||
Création manuelle de la table (sans DROP permissions) :
|
||||
|
||||
```sql
|
||||
CREATE TABLE IF NOT EXISTS `x_departements_contours` (
|
||||
`id` int(11) NOT NULL AUTO_INCREMENT,
|
||||
`code_dept` varchar(3) NOT NULL,
|
||||
`nom_dept` varchar(100) NOT NULL,
|
||||
`contour` GEOMETRY NOT NULL,
|
||||
`created_at` timestamp NOT NULL DEFAULT current_timestamp(),
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `idx_code_dept` (`code_dept`),
|
||||
SPATIAL KEY `idx_contour` (`contour`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci
|
||||
COMMENT='Contours géographiques des départements français';
|
||||
```
|
||||
|
||||
### Import des contours
|
||||
|
||||
1. **Fichier source** : `docs/contour-des-departements.geojson` (depuis data.gouv.fr)
|
||||
2. **Import automatique** : Uniquement lors de la connexion de l'admin `d6soft`
|
||||
3. **Script** : `scripts/init_departements_contours.php`
|
||||
4. **Résultat** : 96 départements importés avec support Polygon et MultiPolygon
|
||||
|
||||
## Services principaux
|
||||
|
||||
### AddressService
|
||||
|
||||
Gère la récupération des adresses depuis la base externe :
|
||||
|
||||
```php
|
||||
class AddressService {
|
||||
// Récupère toutes les adresses dans un polygone
|
||||
public function getAddressesInPolygon(array $coordinates, ?int $entityId = null): array
|
||||
|
||||
// Compte les adresses dans un polygone
|
||||
public function countAddressesInPolygon(array $coordinates, ?int $entityId = null): int
|
||||
}
|
||||
```
|
||||
|
||||
**Caractéristiques** :
|
||||
- Détection automatique des départements touchés par le secteur
|
||||
- Interrogation de toutes les tables cp{dept} concernées
|
||||
- Gestion des secteurs multi-départements
|
||||
|
||||
### DepartmentBoundaryService
|
||||
|
||||
Vérifie les limites départementales des secteurs :
|
||||
|
||||
```php
|
||||
class DepartmentBoundaryService {
|
||||
// Vérifie si un secteur est contenu dans un département
|
||||
public function checkSectorInDepartment(array $sectorCoordinates, string $departmentCode): array
|
||||
|
||||
// Liste tous les départements touchés par un secteur
|
||||
public function getDepartmentsForSector(array $sectorCoordinates): array
|
||||
}
|
||||
```
|
||||
|
||||
**Retour type** :
|
||||
```php
|
||||
[
|
||||
'is_contained' => bool,
|
||||
'message' => string,
|
||||
'intersecting_departments' => [
|
||||
['code_dept' => '22', 'nom_dept' => 'Côtes-d\'Armor', 'percentage_overlap' => 75.5],
|
||||
['code_dept' => '29', 'nom_dept' => 'Finistère', 'percentage_overlap' => 24.5]
|
||||
]
|
||||
]
|
||||
```
|
||||
|
||||
## Processus de création de secteur
|
||||
|
||||
### 1. Structure du payload
|
||||
|
||||
```json
|
||||
{
|
||||
"user_id": 123,
|
||||
"fk_entite": 45,
|
||||
"operation_id": 789,
|
||||
"sector": {
|
||||
"id": 0,
|
||||
"libelle": "Secteur Centre-Ville",
|
||||
"color": "#FF5733",
|
||||
"sector": "48.117266/-1.6777926#48.118500/-1.6750000#..."
|
||||
},
|
||||
"users": [12, 34, 56, 78]
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Étapes de création
|
||||
|
||||
1. **Validation** des données et de l'opération
|
||||
2. **Vérification** des limites départementales (warning si débordement)
|
||||
3. **Début de transaction** pour garantir la cohérence des données
|
||||
4. **Insertion** du secteur dans `ope_sectors`
|
||||
5. **Affectation** des utilisateurs dans `ope_users_sectors` avec :
|
||||
- `fk_operation`, `fk_user`, `fk_sector`
|
||||
- `created_at`, `fk_user_creat`, `chk_active = 1`
|
||||
6. **Intégration des passages orphelins** :
|
||||
- Recherche des passages avec `fk_sector = 0` dans le polygone
|
||||
- Mise à jour de leur `fk_sector` vers le nouveau secteur
|
||||
- Exclusion des passages ayant déjà une `fk_adresse`
|
||||
7. **Récupération** des adresses via `AddressService`
|
||||
8. **Stockage** des adresses dans `sectors_adresses`
|
||||
9. **Création** des passages dans `ope_pass` pour chaque adresse :
|
||||
- Affectés au premier utilisateur de la liste
|
||||
- Avec toutes les FK nécessaires (entité, opération, secteur, user)
|
||||
- Données d'adresse complètes
|
||||
10. **Commit** de la transaction ou **rollback** en cas d'erreur
|
||||
|
||||
### 3. Réponse API pour CREATE
|
||||
|
||||
**Format standardisé** : Les données sont placées à la racine, sans groupe "data" intermédiaire.
|
||||
|
||||
```json
|
||||
{
|
||||
"status": "success",
|
||||
"message": "Secteur créé avec succès",
|
||||
"sector": {
|
||||
"id": 123,
|
||||
"libelle": "Secteur Centre-Ville",
|
||||
"color": "#FF5733",
|
||||
"sector": "48.117266/-1.6777926#48.118500/-1.6750000#..."
|
||||
},
|
||||
"passages_sector": [
|
||||
{
|
||||
"id": 456,
|
||||
"fk_operation": 789,
|
||||
"fk_sector": 123,
|
||||
"fk_user": 12,
|
||||
"fk_type": 2,
|
||||
"fk_adresse": "cp22.12345",
|
||||
"passed_at": null,
|
||||
"numero": "10",
|
||||
"rue": "Rue de la Paix",
|
||||
"rue_bis": "",
|
||||
"ville": "Saint-Brieuc",
|
||||
"residence": null,
|
||||
"fk_habitat": null,
|
||||
"appt": null,
|
||||
"niveau": null,
|
||||
"gps_lat": "48.117266",
|
||||
"gps_lng": "-1.6777926",
|
||||
"nom_recu": null,
|
||||
"name": "", // Décrypté depuis encrypted_name
|
||||
"remarque": null,
|
||||
"email": "", // Décrypté depuis encrypted_email
|
||||
"phone": "", // Décrypté depuis encrypted_phone
|
||||
"montant": null,
|
||||
"fk_type_reglement": null,
|
||||
"email_erreur": null,
|
||||
"nb_passages": null
|
||||
}
|
||||
],
|
||||
"passages_integrated": 5, // Passages orphelins intégrés
|
||||
"passages_created": 10, // Nouveaux passages créés
|
||||
"users_sectors": [
|
||||
{
|
||||
"id": 12,
|
||||
"first_name": "Jean",
|
||||
"sect_name": "JDU",
|
||||
"fk_sector": 123,
|
||||
"name": "Dupont" // Décrypté depuis encrypted_name
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### 4. Réponse API pour UPDATE
|
||||
|
||||
La réponse est identique à CREATE avec des compteurs supplémentaires :
|
||||
|
||||
```json
|
||||
{
|
||||
"status": "success",
|
||||
"message": "Secteur modifié avec succès",
|
||||
"sector": {
|
||||
"id": 123,
|
||||
"libelle": "Secteur Centre-Ville Modifié",
|
||||
"color": "#FF5733",
|
||||
"sector": "48.117266/-1.6777926#48.118500/-1.6750000#..."
|
||||
},
|
||||
"passages_sector": [
|
||||
// Liste complète de TOUS les passages actuels du secteur
|
||||
],
|
||||
"passages_orphaned": 3, // Passages mis en orphelin (hors polygone)
|
||||
"passages_updated": 5, // Passages mis à jour avec fk_adresse
|
||||
"passages_created": 10, // Nouveaux passages créés
|
||||
"passages_total": 25, // Nombre total de passages dans le secteur
|
||||
"users_sectors": [
|
||||
// Liste des utilisateurs affectés
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
**Notes importantes** :
|
||||
- Les champs sensibles (name, email, phone) sont stockés cryptés et décryptés à la volée
|
||||
- La structure est identique entre CREATE et UPDATE pour faciliter l'intégration
|
||||
- Tous les champs sont retournés, même s'ils sont null
|
||||
- Code HTTP : 201 pour CREATE, 200 pour UPDATE
|
||||
|
||||
## Gestion des secteurs multi-départements
|
||||
|
||||
### Détection automatique
|
||||
|
||||
Le système détecte automatiquement quand un secteur touche plusieurs départements :
|
||||
|
||||
1. **Analyse spatiale** : Utilisation de `ST_Intersects` pour identifier tous les départements touchés
|
||||
2. **Calcul de pourcentage** : `ST_Area(ST_Intersection)` pour calculer le % de recouvrement
|
||||
3. **Interrogation multi-tables** : Requête sur toutes les tables cp{dept} concernées
|
||||
|
||||
### Exemple de secteur multi-départements
|
||||
|
||||
```php
|
||||
// Secteur à cheval sur 22 (Côtes-d'Armor) et 29 (Finistère)
|
||||
$coordinates = [
|
||||
[48.5778, -3.8280], // Morlaix (29)
|
||||
[48.5778, -3.7280], // Vers l'est (22)
|
||||
[48.4778, -3.7280],
|
||||
[48.4778, -3.8280]
|
||||
];
|
||||
|
||||
// Le système va automatiquement :
|
||||
// 1. Détecter que le secteur touche 22 et 29
|
||||
// 2. Interroger cp22 et cp29 pour les adresses
|
||||
// 3. Créer les passages pour toutes les adresses trouvées
|
||||
```
|
||||
|
||||
## Tables de données
|
||||
|
||||
### ope_sectors
|
||||
- `id` : Identifiant unique
|
||||
- `libelle` : Nom du secteur
|
||||
- `color` : Couleur d'affichage
|
||||
- `sector` : Coordonnées (format lat/lng#lat/lng#...)
|
||||
- `fk_entite` : Lien vers l'entité
|
||||
|
||||
### sectors_adresses
|
||||
- `fk_sector` : Lien vers le secteur
|
||||
- `fk_address` : ID de l'adresse dans la base externe
|
||||
- `numero`, `voie`, `code_postal`, `commune`
|
||||
- `latitude`, `longitude`
|
||||
|
||||
### ope_pass (passages)
|
||||
- `fk_entite`, `fk_operation`, `fk_sector`, `fk_user`
|
||||
- `numero`, `voie`, `code_postal`, `commune`
|
||||
- `latitude`, `longitude`
|
||||
- `created_at`, `fk_user_creat`, `chk_active`
|
||||
|
||||
### ope_users_sectors
|
||||
- `fk_operation` : Lien vers l'opération
|
||||
- `fk_user` : Lien vers l'utilisateur (ope_users)
|
||||
- `fk_sector` : Lien vers le secteur
|
||||
- `created_at`, `fk_user_creat`, `chk_active`
|
||||
|
||||
## Logs et monitoring
|
||||
|
||||
Le système génère des logs détaillés pour :
|
||||
- Nombre d'adresses trouvées par département
|
||||
- Secteurs hors limites départementales
|
||||
- Passages créés avec succès
|
||||
- Erreurs de connexion aux bases d'adresses
|
||||
- Performance des requêtes spatiales
|
||||
|
||||
## Scripts de test
|
||||
|
||||
- `test_sector_departments.php` : Test des limites départementales
|
||||
- `test_addresses_connection.php` : Test de connexion à la base d'adresses
|
||||
|
||||
## Notes importantes
|
||||
|
||||
1. **Fail-safe** : La création de secteur continue même si la base d'adresses est inaccessible
|
||||
2. **Transactions** :
|
||||
- Toute la création est dans une transaction pour garantir la cohérence
|
||||
- Toujours vérifier `inTransaction()` avant d'appeler `rollBack()`
|
||||
- Gestion correcte des erreurs PDO avec try/catch
|
||||
3. **Performance** : Les requêtes spatiales utilisent des index spatiaux pour optimiser les performances
|
||||
4. **Modification de secteur** : Plus complexe car nécessite de gérer les passages existants (non implémenté)
|
||||
5. **Paramètres SQL** : Utiliser des noms uniques pour éviter l'erreur "Invalid parameter number"
|
||||
6. **Jointures** : Les données utilisateur viennent de la table `users`, pas `ope_users` (qui n'a pas nom/prenom)
|
||||
|
||||
## Bilan de la gestion des adresses et passages
|
||||
|
||||
### Vue d'ensemble du cycle de vie
|
||||
|
||||
```
|
||||
Base Adresses (cp22, cp23...) → sectors_adresses → ope_pass
|
||||
```
|
||||
|
||||
### 1. CRÉATION D'UN SECTEUR
|
||||
|
||||
#### Flux des données :
|
||||
1. **Récupération des adresses** depuis la base externe (`AddressService`)
|
||||
2. **Intégration des passages orphelins** (`fk_sector = NULL`) situés dans le polygone
|
||||
3. **Stockage dans `sectors_adresses`** de toutes les adresses du polygone
|
||||
4. **Création automatique de passages** (`ope_pass`) pour chaque adresse SAUF celles déjà utilisées par les passages orphelins
|
||||
|
||||
#### Détails :
|
||||
- **Passages créés** : `fk_type = 2`, `encrypted_name = ''` (vide), affectés au premier utilisateur
|
||||
- **Passages orphelins** : mis à jour avec le nouveau `fk_sector`
|
||||
- **Évite les doublons** : les adresses déjà utilisées par des passages orphelins ne génèrent pas de nouveau passage
|
||||
|
||||
### 2. MISE À JOUR D'UN SECTEUR
|
||||
|
||||
#### Processus de mise à jour :
|
||||
1. **Mise à jour des attributs** (libelle, color, sector)
|
||||
2. **Mise à jour des membres affectés**
|
||||
3. **Suppression/recréation des adresses** dans `sectors_adresses`
|
||||
4. **Gestion intelligente des passages** via `updatePassagesForSector` :
|
||||
|
||||
#### Gestion des passages lors de l'UPDATE :
|
||||
|
||||
##### a) Vérification géographique des passages existants
|
||||
- Pour chaque passage du secteur, vérification si ses coordonnées GPS sont dans le nouveau polygone
|
||||
- **Si DANS le polygone** : Conservation du passage
|
||||
- **Si HORS du polygone** : Mise en orphelin (`fk_sector = NULL`)
|
||||
|
||||
##### b) Traitement des nouvelles adresses
|
||||
Pour chaque adresse dans `sectors_adresses` :
|
||||
1. **Vérification primaire** : Recherche par `fk_adresse`
|
||||
2. **Vérification secondaire** : Si pas trouvé, recherche par `numero`, `rue_bis`, `rue`, `ville`
|
||||
- Si trouvé → Mise à jour du `fk_adresse` dans le(s) passage(s)
|
||||
3. **Création** : Si aucun passage existant, création avec :
|
||||
- `fk_type = 2`, `encrypted_name = ''`
|
||||
- Affecté au premier utilisateur du secteur
|
||||
- Toutes les données de l'adresse
|
||||
|
||||
### 3. SUPPRESSION D'UN SECTEUR
|
||||
|
||||
#### Traitement différencié des passages :
|
||||
1. **Passages "non visités"** (`fk_type = 2` ET `encrypted_name` vide) :
|
||||
- Suppression définitive de la base
|
||||
- Ces passages correspondent aux adresses non visitées
|
||||
|
||||
2. **Passages "visités"** (tous les autres) :
|
||||
- Mise à jour : `fk_sector = NULL`
|
||||
- Deviennent des passages orphelins
|
||||
- Conservent toutes leurs données (contact, montant, etc.)
|
||||
|
||||
#### Autres suppressions :
|
||||
- Suppression des affectations membres (`ope_users_sectors`)
|
||||
- Suppression des adresses (`sectors_adresses`)
|
||||
- Suppression du secteur lui-même
|
||||
|
||||
### Tableau récapitulatif
|
||||
|
||||
| Action | sectors_adresses | ope_pass dans polygone | ope_pass hors polygone | Nouvelles adresses |
|
||||
|--------|------------------|------------------------|------------------------|-------------------|
|
||||
| CREATE | Insertion depuis base externe | - | Intégration si orphelins | Création automatique de passages |
|
||||
| UPDATE | Suppression/recréation | Conservation | Mise en orphelin | Création si pas de passage existant |
|
||||
| DELETE | Suppression totale | Suppression si non visités / Orphelin si visités | - | - |
|
||||
|
||||
### Points d'attention
|
||||
|
||||
1. **Cohérence géographique** : Lors d'un UPDATE, le système vérifie automatiquement et met en orphelin les passages hors du nouveau périmètre
|
||||
2. **Passages orphelins** : Peuvent être réintégrés lors de la création d'un nouveau secteur englobant
|
||||
3. **Mise à jour du fk_adresse** : Lors d'un UPDATE, les passages existants peuvent recevoir leur `fk_adresse` s'ils correspondent à une adresse
|
||||
4. **Performance** : La création/mise à jour génère potentiellement des milliers de passages selon la densité d'adresses
|
||||
|
||||
## Erreurs communes et solutions
|
||||
|
||||
### "There is no active transaction"
|
||||
- **Cause** : Appel à `rollBack()` sans transaction active
|
||||
- **Solution** : Vérifier `$db->inTransaction()` avant rollback
|
||||
|
||||
### "Column not found: fk_address"
|
||||
- **Cause** : La colonne s'appelle `fk_adresse` (avec 'e')
|
||||
- **Solution** : Corriger les noms de colonnes dans les requêtes
|
||||
|
||||
### "Invalid parameter number"
|
||||
- **Cause** : Réutilisation du même nom de paramètre dans une requête
|
||||
- **Solution** : Utiliser des noms uniques (`:param1`, `:param2`, etc.)
|
||||
|
||||
### "Unknown column 'ou.nom'"
|
||||
- **Cause** : La table `ope_users` n'a pas de colonnes nom/prenom
|
||||
- **Solution** : Joindre avec la table `users` qui contient `encrypted_name` et `first_name`
|
||||
|
||||
### "Class 'ApiService' not found"
|
||||
- **Cause** : Import manquant dans le controller
|
||||
- **Solution** : Ajouter `use App\Services\ApiService;` et `require_once`
|
||||
90
api/docs/INSTALL_FPDF.md
Normal file
90
api/docs/INSTALL_FPDF.md
Normal file
@@ -0,0 +1,90 @@
|
||||
# Installation de FPDF pour la génération des reçus PDF avec logo
|
||||
|
||||
## Installation via Composer (RECOMMANDÉ)
|
||||
|
||||
Sur chaque serveur (DEV, REC, PROD), exécuter :
|
||||
|
||||
```bash
|
||||
cd /var/www/geosector/api
|
||||
composer require setasign/fpdf
|
||||
```
|
||||
|
||||
Ou si composer.json est déjà mis à jour :
|
||||
|
||||
```bash
|
||||
cd /var/www/geosector/api
|
||||
composer update
|
||||
```
|
||||
|
||||
## Fichiers à déployer
|
||||
|
||||
1. **Nouveaux fichiers** :
|
||||
- `/src/Services/ReceiptPDFGenerator.php` - Nouvelle classe de génération PDF avec FPDF
|
||||
- `/docs/_logo_recu.png` - Logo par défaut (casque de pompier)
|
||||
|
||||
2. **Fichiers modifiés** :
|
||||
- `/src/Services/ReceiptService.php` - Utilise maintenant ReceiptPDFGenerator
|
||||
- `/composer.json` - Ajout de la dépendance FPDF
|
||||
|
||||
## Vérification
|
||||
|
||||
Après installation, tester la génération d'un reçu :
|
||||
|
||||
```bash
|
||||
# Vérifier que FPDF est installé
|
||||
ls -la vendor/setasign/fpdf/
|
||||
|
||||
# Tester la génération d'un PDF
|
||||
php -r "
|
||||
require 'vendor/autoload.php';
|
||||
\$pdf = new FPDF();
|
||||
\$pdf->AddPage();
|
||||
\$pdf->SetFont('Arial','B',16);
|
||||
\$pdf->Cell(40,10,'Test FPDF OK');
|
||||
echo 'FPDF fonctionne' . PHP_EOL;
|
||||
"
|
||||
```
|
||||
|
||||
## Fonctionnalités du nouveau générateur
|
||||
|
||||
✅ **Support des vrais logos PNG/JPG**
|
||||
✅ **Logo par défaut** si l'entité n'a pas de logo
|
||||
✅ **Taille du logo** : 40x40mm
|
||||
✅ **Mise en page professionnelle** avec cadre pour le montant
|
||||
✅ **Conversion automatique** des caractères UTF-8
|
||||
✅ **PDF léger** (~20-30KB avec logo)
|
||||
|
||||
## Structure du reçu généré
|
||||
|
||||
1. **En-tête** :
|
||||
- Logo (40x40mm) à gauche
|
||||
- Nom et ville de l'entité à droite du logo
|
||||
|
||||
2. **Titre** :
|
||||
- "REÇU FISCAL DE DON"
|
||||
- Numéro du reçu
|
||||
- Article 200 CGI
|
||||
|
||||
3. **Corps** :
|
||||
- Informations du donateur
|
||||
- Montant en gros dans un cadre grisé
|
||||
- Date du don
|
||||
- Mode de règlement et campagne
|
||||
|
||||
4. **Pied de page** :
|
||||
- Mentions légales (réduction 66%)
|
||||
- Date et signature
|
||||
|
||||
## Résolution de problèmes
|
||||
|
||||
Si erreur "Class 'FPDF' not found" :
|
||||
```bash
|
||||
composer dump-autoload
|
||||
```
|
||||
|
||||
Si problème avec le logo :
|
||||
- Vérifier que `/docs/_logo_recu.png` existe
|
||||
- Vérifier les permissions : `chmod 644 docs/_logo_recu.png`
|
||||
|
||||
Si caractères accentués mal affichés :
|
||||
- FPDF utilise ISO-8859-1, la conversion est automatique dans ReceiptPDFGenerator
|
||||
746
api/docs/PLANNING-STRIPE-API.md
Normal file
746
api/docs/PLANNING-STRIPE-API.md
Normal file
@@ -0,0 +1,746 @@
|
||||
# PLANNING STRIPE - DÉVELOPPEUR BACKEND PHP
|
||||
## API PHP 8.3 - Intégration Stripe Connect + Terminal
|
||||
### Période : 25/08/2024 - 05/09/2024
|
||||
|
||||
---
|
||||
|
||||
## 📅 LUNDI 25/08 - Setup et architecture (8h)
|
||||
|
||||
### 🌅 Matin (4h)
|
||||
```bash
|
||||
# Installation Stripe PHP SDK
|
||||
cd api
|
||||
composer require stripe/stripe-php
|
||||
```
|
||||
|
||||
#### ✅ Configuration environnement
|
||||
- [x] Créer configuration Stripe dans `AppConfig.php` avec clés TEST
|
||||
- [x] Ajouter variables de configuration :
|
||||
```php
|
||||
'stripe' => [
|
||||
'public_key_test' => 'pk_test_51QwoVN00pblGEgsXkf8qlXm...',
|
||||
'secret_key_test' => 'sk_test_51QwoVN00pblGEgsXnvqi8qf...',
|
||||
'webhook_secret_test' => 'whsec_test_...',
|
||||
'api_version' => '2024-06-20',
|
||||
'application_fee_percent' => 0, // DECISION: 0% commission
|
||||
'mode' => 'test'
|
||||
]
|
||||
```
|
||||
- [x] Créer service `StripeService.php` singleton
|
||||
- [x] Configurer authentification Session-based API
|
||||
|
||||
#### ✅ Base de données
|
||||
```sql
|
||||
-- Tables à créer
|
||||
CREATE TABLE stripe_accounts (
|
||||
id INT PRIMARY KEY AUTO_INCREMENT,
|
||||
amicale_id INT NOT NULL,
|
||||
stripe_account_id VARCHAR(255) UNIQUE,
|
||||
charges_enabled BOOLEAN DEFAULT FALSE,
|
||||
payouts_enabled BOOLEAN DEFAULT FALSE,
|
||||
onboarding_completed BOOLEAN DEFAULT FALSE,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (amicale_id) REFERENCES amicales(id)
|
||||
);
|
||||
|
||||
CREATE TABLE payment_intents (
|
||||
id INT PRIMARY KEY AUTO_INCREMENT,
|
||||
stripe_payment_intent_id VARCHAR(255) UNIQUE,
|
||||
amicale_id INT NOT NULL,
|
||||
pompier_id INT NOT NULL,
|
||||
amount INT NOT NULL, -- en centimes
|
||||
currency VARCHAR(3) DEFAULT 'eur',
|
||||
status VARCHAR(50),
|
||||
application_fee INT,
|
||||
metadata JSON,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (amicale_id) REFERENCES amicales(id),
|
||||
FOREIGN KEY (pompier_id) REFERENCES users(id)
|
||||
);
|
||||
|
||||
CREATE TABLE terminal_readers (
|
||||
id INT PRIMARY KEY AUTO_INCREMENT,
|
||||
stripe_reader_id VARCHAR(255) UNIQUE,
|
||||
amicale_id INT NOT NULL,
|
||||
label VARCHAR(255),
|
||||
location VARCHAR(255),
|
||||
status VARCHAR(50),
|
||||
device_type VARCHAR(50),
|
||||
last_seen_at TIMESTAMP,
|
||||
FOREIGN KEY (amicale_id) REFERENCES amicales(id)
|
||||
);
|
||||
|
||||
CREATE TABLE android_certified_devices (
|
||||
id INT PRIMARY KEY AUTO_INCREMENT,
|
||||
manufacturer VARCHAR(100),
|
||||
model VARCHAR(200),
|
||||
model_identifier VARCHAR(200),
|
||||
tap_to_pay_certified BOOLEAN DEFAULT FALSE,
|
||||
certification_date DATE,
|
||||
min_android_version INT,
|
||||
country VARCHAR(2) DEFAULT 'FR',
|
||||
last_verified TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
INDEX idx_manufacturer_model (manufacturer, model)
|
||||
);
|
||||
```
|
||||
|
||||
### 🌆 Après-midi (4h)
|
||||
|
||||
#### ✅ Endpoints Connect - Onboarding (RÉALISÉS)
|
||||
```php
|
||||
// POST /api/stripe/accounts - IMPLEMENTED
|
||||
public function createAccount() {
|
||||
$amicale = Amicale::find($amicaleId);
|
||||
|
||||
$account = \Stripe\Account::create([
|
||||
'type' => 'express',
|
||||
'country' => 'FR',
|
||||
'email' => $amicale->email,
|
||||
'capabilities' => [
|
||||
'card_payments' => ['requested' => true],
|
||||
'transfers' => ['requested' => true],
|
||||
],
|
||||
'business_type' => 'non_profit',
|
||||
'business_profile' => [
|
||||
'name' => $amicale->name,
|
||||
'product_description' => 'Vente de calendriers des pompiers',
|
||||
],
|
||||
]);
|
||||
|
||||
// Sauvegarder stripe_account_id
|
||||
return $account;
|
||||
}
|
||||
|
||||
// GET /api/amicales/{id}/onboarding-link
|
||||
public function getOnboardingLink($amicaleId) {
|
||||
$accountLink = \Stripe\AccountLink::create([
|
||||
'account' => $amicale->stripe_account_id,
|
||||
'refresh_url' => config('app.url') . '/stripe/refresh',
|
||||
'return_url' => config('app.url') . '/stripe/success',
|
||||
'type' => 'account_onboarding',
|
||||
]);
|
||||
|
||||
return ['url' => $accountLink->url];
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📅 MARDI 26/08 - Webhooks et Terminal (8h)
|
||||
|
||||
### 🌅 Matin (4h)
|
||||
|
||||
#### ✅ Webhooks handler
|
||||
```php
|
||||
// POST /api/webhooks/stripe
|
||||
public function handleWebhook(Request $request) {
|
||||
$payload = $request->getContent();
|
||||
$sig_header = $request->header('Stripe-Signature');
|
||||
|
||||
try {
|
||||
$event = \Stripe\Webhook::constructEvent(
|
||||
$payload, $sig_header, config('stripe.webhook_secret')
|
||||
);
|
||||
} catch(\Exception $e) {
|
||||
return response('Invalid signature', 400);
|
||||
}
|
||||
|
||||
switch ($event->type) {
|
||||
case 'account.updated':
|
||||
$this->handleAccountUpdated($event->data->object);
|
||||
break;
|
||||
case 'account.application.authorized':
|
||||
$this->handleAccountAuthorized($event->data->object);
|
||||
break;
|
||||
case 'payment_intent.succeeded':
|
||||
$this->handlePaymentSucceeded($event->data->object);
|
||||
break;
|
||||
}
|
||||
|
||||
return response('Webhook handled', 200);
|
||||
}
|
||||
```
|
||||
|
||||
#### ✅ Terminal Connection Token
|
||||
```php
|
||||
// POST /api/terminal/connection-token
|
||||
public function createConnectionToken(Request $request) {
|
||||
$pompier = Auth::user();
|
||||
$amicale = $pompier->amicale;
|
||||
|
||||
$connectionToken = \Stripe\Terminal\ConnectionToken::create([
|
||||
'location' => $amicale->stripe_location_id,
|
||||
], [
|
||||
'stripe_account' => $amicale->stripe_account_id
|
||||
]);
|
||||
|
||||
return ['secret' => $connectionToken->secret];
|
||||
}
|
||||
```
|
||||
|
||||
### 🌆 Après-midi (4h)
|
||||
|
||||
#### ✅ Gestion des Locations
|
||||
```php
|
||||
// POST /api/amicales/{id}/create-location
|
||||
public function createLocation($amicaleId) {
|
||||
$amicale = Amicale::find($amicaleId);
|
||||
|
||||
$location = \Stripe\Terminal\Location::create([
|
||||
'display_name' => $amicale->name,
|
||||
'address' => [
|
||||
'line1' => $amicale->address,
|
||||
'city' => $amicale->city,
|
||||
'postal_code' => $amicale->postal_code,
|
||||
'country' => 'FR',
|
||||
],
|
||||
], [
|
||||
'stripe_account' => $amicale->stripe_account_id
|
||||
]);
|
||||
|
||||
$amicale->update(['stripe_location_id' => $location->id]);
|
||||
return $location;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📅 MERCREDI 27/08 - Paiements et fees (8h)
|
||||
|
||||
### 🌅 Matin (4h)
|
||||
|
||||
#### ✅ Création PaymentIntent avec commission
|
||||
```php
|
||||
// POST /api/payments/create-intent
|
||||
public function createPaymentIntent(Request $request) {
|
||||
$validated = $request->validate([
|
||||
'amount' => 'required|integer|min:100', // en centimes
|
||||
'amicale_id' => 'required|exists:amicales,id',
|
||||
]);
|
||||
|
||||
$pompier = Auth::user();
|
||||
$amicale = Amicale::find($validated['amicale_id']);
|
||||
|
||||
// Calculer la commission (2.5% ou 50 centimes minimum)
|
||||
$applicationFee = max(
|
||||
50, // 0.50€ minimum
|
||||
round($validated['amount'] * 0.025) // 2.5%
|
||||
);
|
||||
|
||||
$paymentIntent = \Stripe\PaymentIntent::create([
|
||||
'amount' => $validated['amount'],
|
||||
'currency' => 'eur',
|
||||
'payment_method_types' => ['card_present'],
|
||||
'capture_method' => 'automatic',
|
||||
'application_fee_amount' => $applicationFee,
|
||||
'transfer_data' => [
|
||||
'destination' => $amicale->stripe_account_id,
|
||||
],
|
||||
'metadata' => [
|
||||
'pompier_id' => $pompier->id,
|
||||
'pompier_name' => $pompier->name,
|
||||
'amicale_id' => $amicale->id,
|
||||
'calendrier_annee' => date('Y'),
|
||||
],
|
||||
]);
|
||||
|
||||
// Sauvegarder en DB
|
||||
PaymentIntent::create([
|
||||
'stripe_payment_intent_id' => $paymentIntent->id,
|
||||
'amicale_id' => $amicale->id,
|
||||
'pompier_id' => $pompier->id,
|
||||
'amount' => $validated['amount'],
|
||||
'application_fee' => $applicationFee,
|
||||
'status' => $paymentIntent->status,
|
||||
]);
|
||||
|
||||
return [
|
||||
'client_secret' => $paymentIntent->client_secret,
|
||||
'payment_intent_id' => $paymentIntent->id,
|
||||
];
|
||||
}
|
||||
```
|
||||
|
||||
### 🌆 Après-midi (4h)
|
||||
|
||||
#### ✅ Capture et confirmation
|
||||
```php
|
||||
// POST /api/payments/{id}/capture
|
||||
public function capturePayment($paymentIntentId) {
|
||||
$localPayment = PaymentIntent::where('stripe_payment_intent_id', $paymentIntentId)->first();
|
||||
|
||||
$paymentIntent = \Stripe\PaymentIntent::retrieve($paymentIntentId);
|
||||
|
||||
if ($paymentIntent->status === 'requires_capture') {
|
||||
$paymentIntent->capture();
|
||||
}
|
||||
|
||||
$localPayment->update(['status' => $paymentIntent->status]);
|
||||
|
||||
// Si succès, envoyer email reçu
|
||||
if ($paymentIntent->status === 'succeeded') {
|
||||
$this->sendReceipt($localPayment);
|
||||
}
|
||||
|
||||
return $paymentIntent;
|
||||
}
|
||||
|
||||
// GET /api/payments/{id}/status
|
||||
public function getPaymentStatus($paymentIntentId) {
|
||||
$payment = PaymentIntent::where('stripe_payment_intent_id', $paymentIntentId)->first();
|
||||
return [
|
||||
'status' => $payment->status,
|
||||
'amount' => $payment->amount,
|
||||
'created_at' => $payment->created_at,
|
||||
];
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📅 JEUDI 28/08 - Reporting et Android compatibility (8h)
|
||||
|
||||
### 🌅 Matin (4h)
|
||||
|
||||
#### ✅ Gestion appareils Android certifiés
|
||||
```php
|
||||
// POST /api/devices/check-tap-to-pay
|
||||
public function checkTapToPayCapability(Request $request) {
|
||||
$validated = $request->validate([
|
||||
'platform' => 'required|in:ios,android',
|
||||
'manufacturer' => 'required_if:platform,android',
|
||||
'model' => 'required_if:platform,android',
|
||||
'os_version' => 'required',
|
||||
]);
|
||||
|
||||
if ($validated['platform'] === 'ios') {
|
||||
// iPhone XS et ultérieurs avec iOS 15.4+
|
||||
$supportedModels = ['iPhone11,', 'iPhone12,', 'iPhone13,', 'iPhone14,', 'iPhone15,', 'iPhone16,'];
|
||||
$modelSupported = false;
|
||||
|
||||
foreach ($supportedModels as $prefix) {
|
||||
if (str_starts_with($validated['model'], $prefix)) {
|
||||
$modelSupported = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
$osVersion = explode('.', $validated['os_version']);
|
||||
$osSupported = $osVersion[0] > 15 ||
|
||||
($osVersion[0] == 15 && isset($osVersion[1]) && $osVersion[1] >= 4);
|
||||
|
||||
return [
|
||||
'tap_to_pay_supported' => $modelSupported && $osSupported,
|
||||
'message' => $modelSupported && $osSupported ?
|
||||
'Tap to Pay disponible' :
|
||||
'iPhone XS ou ultérieur avec iOS 15.4+ requis'
|
||||
];
|
||||
}
|
||||
|
||||
// Android - vérifier dans la base de données
|
||||
$device = DB::table('android_certified_devices')
|
||||
->where('manufacturer', $validated['manufacturer'])
|
||||
->where('model', $validated['model'])
|
||||
->where('tap_to_pay_certified', true)
|
||||
->first();
|
||||
|
||||
return [
|
||||
'tap_to_pay_supported' => $device !== null,
|
||||
'message' => $device ?
|
||||
'Tap to Pay disponible sur cet appareil' :
|
||||
'Appareil non certifié pour Tap to Pay en France',
|
||||
'alternative' => !$device ? 'Utilisez un iPhone compatible' : null
|
||||
];
|
||||
}
|
||||
|
||||
// GET /api/devices/certified-android
|
||||
public function getCertifiedAndroidDevices() {
|
||||
return DB::table('android_certified_devices')
|
||||
->where('tap_to_pay_certified', true)
|
||||
->where('country', 'FR')
|
||||
->orderBy('manufacturer')
|
||||
->orderBy('model')
|
||||
->get();
|
||||
}
|
||||
```
|
||||
|
||||
#### ✅ Seeder pour appareils certifiés
|
||||
```php
|
||||
// database/seeders/AndroidCertifiedDevicesSeeder.php
|
||||
public function run() {
|
||||
$devices = [
|
||||
// Samsung
|
||||
['manufacturer' => 'Samsung', 'model' => 'Galaxy S21', 'model_identifier' => 'SM-G991B', 'min_android_version' => 11],
|
||||
['manufacturer' => 'Samsung', 'model' => 'Galaxy S21+', 'model_identifier' => 'SM-G996B', 'min_android_version' => 11],
|
||||
['manufacturer' => 'Samsung', 'model' => 'Galaxy S21 Ultra', 'model_identifier' => 'SM-G998B', 'min_android_version' => 11],
|
||||
['manufacturer' => 'Samsung', 'model' => 'Galaxy S22', 'model_identifier' => 'SM-S901B', 'min_android_version' => 12],
|
||||
['manufacturer' => 'Samsung', 'model' => 'Galaxy S23', 'model_identifier' => 'SM-S911B', 'min_android_version' => 13],
|
||||
['manufacturer' => 'Samsung', 'model' => 'Galaxy S24', 'model_identifier' => 'SM-S921B', 'min_android_version' => 14],
|
||||
// Google Pixel
|
||||
['manufacturer' => 'Google', 'model' => 'Pixel 6', 'model_identifier' => 'oriole', 'min_android_version' => 12],
|
||||
['manufacturer' => 'Google', 'model' => 'Pixel 6 Pro', 'model_identifier' => 'raven', 'min_android_version' => 12],
|
||||
['manufacturer' => 'Google', 'model' => 'Pixel 7', 'model_identifier' => 'panther', 'min_android_version' => 13],
|
||||
['manufacturer' => 'Google', 'model' => 'Pixel 8', 'model_identifier' => 'shiba', 'min_android_version' => 14],
|
||||
];
|
||||
|
||||
foreach ($devices as $device) {
|
||||
DB::table('android_certified_devices')->insert([
|
||||
'manufacturer' => $device['manufacturer'],
|
||||
'model' => $device['model'],
|
||||
'model_identifier' => $device['model_identifier'],
|
||||
'tap_to_pay_certified' => true,
|
||||
'certification_date' => now(),
|
||||
'min_android_version' => $device['min_android_version'],
|
||||
'country' => 'FR',
|
||||
]);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### ✅ Endpoints statistiques
|
||||
```php
|
||||
// GET /api/amicales/{id}/stats
|
||||
public function getAmicaleStats($amicaleId) {
|
||||
$stats = DB::table('payment_intents')
|
||||
->where('amicale_id', $amicaleId)
|
||||
->where('status', 'succeeded')
|
||||
->selectRaw('
|
||||
COUNT(*) as total_ventes,
|
||||
SUM(amount) as total_montant,
|
||||
SUM(application_fee) as total_commissions,
|
||||
DATE(created_at) as date
|
||||
')
|
||||
->groupBy('date')
|
||||
->get();
|
||||
|
||||
return $stats;
|
||||
}
|
||||
|
||||
// GET /api/pompiers/{id}/ventes
|
||||
public function getPompierVentes($pompierId) {
|
||||
return PaymentIntent::where('pompier_id', $pompierId)
|
||||
->where('status', 'succeeded')
|
||||
->orderBy('created_at', 'desc')
|
||||
->paginate(20);
|
||||
}
|
||||
```
|
||||
|
||||
### 🌆 Après-midi (4h)
|
||||
|
||||
#### ✅ Gestion des remboursements
|
||||
```php
|
||||
// POST /api/payments/{id}/refund
|
||||
public function refundPayment($paymentIntentId, Request $request) {
|
||||
$validated = $request->validate([
|
||||
'amount' => 'integer|min:100', // optionnel, remboursement partiel
|
||||
'reason' => 'string|in:duplicate,fraudulent,requested_by_customer',
|
||||
]);
|
||||
|
||||
$payment = PaymentIntent::where('stripe_payment_intent_id', $paymentIntentId)->first();
|
||||
|
||||
$refund = \Stripe\Refund::create([
|
||||
'payment_intent' => $paymentIntentId,
|
||||
'amount' => $validated['amount'] ?? null, // null = remboursement total
|
||||
'reason' => $validated['reason'] ?? 'requested_by_customer',
|
||||
'reverse_transfer' => true, // Important pour Connect
|
||||
'refund_application_fee' => true, // Rembourser aussi la commission
|
||||
]);
|
||||
|
||||
$payment->update(['status' => 'refunded']);
|
||||
|
||||
return $refund;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📅 VENDREDI 29/08 - Mode offline et sync (8h)
|
||||
|
||||
### 🌅 Matin (4h)
|
||||
|
||||
#### ✅ Queue de synchronisation
|
||||
```php
|
||||
// POST /api/payments/batch-sync
|
||||
public function batchSync(Request $request) {
|
||||
$validated = $request->validate([
|
||||
'transactions' => 'required|array',
|
||||
'transactions.*.local_id' => 'required|string',
|
||||
'transactions.*.amount' => 'required|integer',
|
||||
'transactions.*.created_at' => 'required|date',
|
||||
'transactions.*.payment_method' => 'required|in:card,cash',
|
||||
]);
|
||||
|
||||
$results = [];
|
||||
|
||||
foreach ($validated['transactions'] as $transaction) {
|
||||
if ($transaction['payment_method'] === 'cash') {
|
||||
// Enregistrer paiement cash uniquement en DB
|
||||
$results[] = $this->recordCashPayment($transaction);
|
||||
} else {
|
||||
// Créer PaymentIntent a posteriori (si possible)
|
||||
$results[] = $this->createOfflinePayment($transaction);
|
||||
}
|
||||
}
|
||||
|
||||
return ['synced' => $results];
|
||||
}
|
||||
```
|
||||
|
||||
### 🌆 Après-midi (4h)
|
||||
|
||||
#### ✅ Tests unitaires critiques
|
||||
```php
|
||||
class StripePaymentTest extends TestCase {
|
||||
public function test_create_payment_intent_with_fees() {
|
||||
// Test création PaymentIntent avec commission
|
||||
}
|
||||
|
||||
public function test_webhook_signature_validation() {
|
||||
// Test sécurité webhook
|
||||
}
|
||||
|
||||
public function test_refund_reverses_transfer() {
|
||||
// Test remboursement avec annulation virement
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📅 LUNDI 01/09 - Sécurité et optimisations (8h)
|
||||
|
||||
### 🌅 Matin (4h)
|
||||
|
||||
#### ✅ Rate limiting et sécurité
|
||||
```php
|
||||
// Middleware RateLimiter pour endpoints sensibles
|
||||
Route::middleware(['throttle:10,1'])->group(function () {
|
||||
Route::post('/payments/create-intent', 'PaymentController@createIntent');
|
||||
});
|
||||
|
||||
// Validation des montants
|
||||
public function validateAmount($amount) {
|
||||
if ($amount < 100 || $amount > 50000) { // 1€ - 500€
|
||||
throw new ValidationException('Montant invalide');
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 🌆 Après-midi (4h)
|
||||
|
||||
#### ✅ Logs et monitoring
|
||||
```php
|
||||
// Logger tous les événements Stripe
|
||||
Log::channel('stripe')->info('Payment created', [
|
||||
'payment_intent_id' => $paymentIntent->id,
|
||||
'amount' => $paymentIntent->amount,
|
||||
'pompier_id' => $pompier->id,
|
||||
]);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📅 MARDI 02/09 - Documentation API (4h)
|
||||
|
||||
#### ✅ Documentation OpenAPI/Swagger
|
||||
```yaml
|
||||
/api/payments/create-intent:
|
||||
post:
|
||||
summary: Créer une intention de paiement
|
||||
parameters:
|
||||
- name: amount
|
||||
type: integer
|
||||
required: true
|
||||
description: Montant en centimes
|
||||
responses:
|
||||
200:
|
||||
description: PaymentIntent créé
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📅 MERCREDI 03/09 - Tests d'intégration (8h)
|
||||
|
||||
#### ✅ Tests end-to-end
|
||||
- [ ] Parcours complet onboarding amicale
|
||||
- [ ] Création paiement → capture → confirmation
|
||||
- [ ] Test remboursement complet et partiel
|
||||
- [ ] Test webhooks avec ngrok
|
||||
|
||||
---
|
||||
|
||||
## 📅 JEUDI 04/09 - Mise en production (8h)
|
||||
|
||||
---
|
||||
|
||||
## 📅 VENDREDI 05/09 - Support et livraison finale (8h)
|
||||
|
||||
### 🌅 Matin (4h)
|
||||
|
||||
#### ✅ Déploiement final
|
||||
- [ ] Migration DB production
|
||||
- [ ] Variables environnement LIVE
|
||||
- [ ] Smoke tests production
|
||||
- [ ] Vérification des webhooks en production
|
||||
|
||||
### 🌆 Après-midi (4h)
|
||||
|
||||
#### ✅ Support et monitoring
|
||||
- [ ] Monitoring des premiers paiements réels
|
||||
- [ ] Support hotline pour équipes terrain
|
||||
- [ ] Documentation de passation
|
||||
- [ ] Réunion de clôture et retour d'expérience
|
||||
|
||||
---
|
||||
|
||||
## 📊 RÉCAPITULATIF
|
||||
|
||||
- **Total heures** : 72h sur 10 jours
|
||||
- **Endpoints créés** : 15
|
||||
- **Tables DB** : 3
|
||||
- **Tests** : 20+
|
||||
|
||||
## 🔧 DÉPENDANCES
|
||||
|
||||
```json
|
||||
{
|
||||
"require": {
|
||||
"php": "^8.3",
|
||||
"stripe/stripe-php": "^13.0",
|
||||
"laravel/framework": "^10.0"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## ⚠️ CHECKLIST SÉCURITÉ
|
||||
|
||||
- [ ] ❌ JAMAIS logger les clés secrètes
|
||||
- [ ] ✅ TOUJOURS valider signature webhooks
|
||||
- [ ] ✅ TOUJOURS utiliser HTTPS
|
||||
- [ ] ✅ Rate limiting sur endpoints paiement
|
||||
- [ ] ✅ Logs détaillés pour audit
|
||||
|
||||
---
|
||||
|
||||
## 🎯 BILAN DÉVELOPPEMENT API (01/09/2024)
|
||||
|
||||
### ✅ ENDPOINTS IMPLÉMENTÉS ET TESTÉS
|
||||
|
||||
#### **Stripe Connect - Comptes**
|
||||
- **POST /api/stripe/accounts** ✅
|
||||
- Création compte Stripe Express pour amicales
|
||||
- Gestion déchiffrement données (encrypted_email, encrypted_name)
|
||||
- Support des comptes existants
|
||||
|
||||
- **GET /api/stripe/accounts/:entityId/status** ✅
|
||||
- Récupération statut complet du compte
|
||||
- Vérification charges_enabled et payouts_enabled
|
||||
- Retour JSON avec informations détaillées
|
||||
|
||||
- **POST /api/stripe/accounts/:accountId/onboarding-link** ✅
|
||||
- Génération liens d'onboarding Stripe
|
||||
- URLs de retour configurées
|
||||
- Gestion des erreurs et timeouts
|
||||
|
||||
#### **Terminal et Locations**
|
||||
- **POST /api/stripe/locations** ✅
|
||||
- Création de locations Terminal
|
||||
- Association avec compte Stripe de l'amicale
|
||||
- ID location retourné : tml_GLJ21w7KCYX4Wj
|
||||
|
||||
- **POST /api/stripe/terminal/connection-token** ✅
|
||||
- Génération tokens de connexion Terminal
|
||||
- Authentification par session
|
||||
- Support multi-amicales
|
||||
|
||||
#### **Configuration et Utilitaires**
|
||||
- **GET /api/stripe/config** ✅
|
||||
- Configuration publique Stripe
|
||||
- Clés publiques et paramètres client
|
||||
- Adaptation par environnement
|
||||
|
||||
- **POST /api/stripe/webhook** ✅
|
||||
- Réception événements Stripe
|
||||
- Vérification signatures webhook
|
||||
- Traitement des événements Connect
|
||||
|
||||
### 🔧 CORRECTIONS TECHNIQUES RÉALISÉES
|
||||
|
||||
#### **StripeController.php**
|
||||
- Fixed `Database::getInstance()` → `$this->db`
|
||||
- Fixed `$db->prepare()` → `$this->db->prepare()`
|
||||
- Removed `details_submitted` column from SQL UPDATE
|
||||
- Added proper exit statements after JSON responses
|
||||
- Commented out Logger class calls (class not found)
|
||||
|
||||
#### **StripeService.php**
|
||||
- Added proper Stripe SDK imports (`use Stripe\Account`)
|
||||
- Fixed `Account::retrieve()` → `$this->stripe->accounts->retrieve()`
|
||||
- **CRUCIAL**: Added data decryption support:
|
||||
```php
|
||||
$nom = !empty($entite['encrypted_name']) ?
|
||||
\ApiService::decryptData($entite['encrypted_name']) : '';
|
||||
$email = !empty($entite['encrypted_email']) ?
|
||||
\ApiService::decryptSearchableData($entite['encrypted_email']) : null;
|
||||
```
|
||||
- Fixed address mapping (adresse1, adresse2 vs adresse)
|
||||
- **REMOVED commission calculation - set to 0%**
|
||||
|
||||
#### **Router.php**
|
||||
- Commented out excessive debug logging causing nginx 502 errors:
|
||||
```php
|
||||
// error_log("Recherche de route pour: méthode=$method, uri=$uri");
|
||||
// error_log("Test pattern: $pattern contre uri: $uri");
|
||||
```
|
||||
|
||||
#### **AppConfig.php**
|
||||
- Set `application_fee_percent` to 0 (was 2.5)
|
||||
- Set `application_fee_minimum` to 0 (was 50)
|
||||
- **Policy**: 100% of payments go to amicales
|
||||
|
||||
### 📊 TESTS ET VALIDATION
|
||||
|
||||
#### **Tests Réussis**
|
||||
1. **POST /api/stripe/accounts** → 200 OK (Compte créé: acct_1S2YfNP63A07c33Y)
|
||||
2. **GET /api/stripe/accounts/5/status** → 200 OK (charges_enabled: true)
|
||||
3. **POST /api/stripe/locations** → 200 OK (Location: tml_GLJ21w7KCYX4Wj)
|
||||
4. **POST /api/stripe/accounts/.../onboarding-link** → 200 OK (Link generated)
|
||||
5. **Onboarding Stripe** → Completed successfully by user
|
||||
|
||||
#### **Erreurs Résolues**
|
||||
- ❌ 500 "Class App\Controllers\Database not found" → ✅ Fixed
|
||||
- ❌ 400 "Invalid email address: " → ✅ Fixed (decryption added)
|
||||
- ❌ 502 "upstream sent too big header" → ✅ Fixed (logs removed)
|
||||
- ❌ SQL "Column not found: details_submitted" → ✅ Fixed
|
||||
|
||||
### 🚀 ARCHITECTURE TECHNIQUE
|
||||
|
||||
#### **Services Implémentés**
|
||||
- **StripeService**: Singleton pour interactions Stripe API
|
||||
- **StripeController**: Endpoints REST avec gestion sessions
|
||||
- **StripeWebhookController**: Handler événements webhook
|
||||
- **ApiService**: Déchiffrement données encrypted fields
|
||||
|
||||
#### **Sécurité**
|
||||
- Validation signatures webhook Stripe
|
||||
- Authentification session-based pour APIs privées
|
||||
- Public endpoints: webhook uniquement
|
||||
- Pas de stockage clés secrètes en base
|
||||
|
||||
#### **Base de données**
|
||||
- Utilisation tables existantes (entites)
|
||||
- Pas de nouvelles tables créées (pas nécessaire pour V1)
|
||||
- Champs encrypted_email et encrypted_name supportés
|
||||
- Déchiffrement automatique avant envoi Stripe
|
||||
|
||||
### 🎯 PROCHAINES ÉTAPES API
|
||||
1. **Tests paiements réels** avec PaymentIntents
|
||||
2. **Endpoints statistiques** pour dashboard amicales
|
||||
3. **Webhooks production** avec clés live
|
||||
4. **Monitoring et logs** des transactions
|
||||
5. **Rate limiting** sur endpoints sensibles
|
||||
|
||||
---
|
||||
|
||||
*Document créé le 24/08/2024 - Dernière mise à jour : 01/09/2024*
|
||||
237
api/docs/PREPA_PROD.md
Normal file
237
api/docs/PREPA_PROD.md
Normal file
@@ -0,0 +1,237 @@
|
||||
# PRÉPARATION PRODUCTION - Process Email Queue + Permissions Suppression Passages
|
||||
|
||||
## 📅 Date de mise en production prévue : _____________
|
||||
|
||||
## 🎯 Objectif
|
||||
1. Mettre en place le système de traitement automatique de la queue d'emails pour l'envoi des reçus fiscaux de dons.
|
||||
2. Ajouter le champ de permission pour autoriser les membres à supprimer des passages.
|
||||
|
||||
## ✅ Prérequis
|
||||
- [ ] Backup de la base de données effectué
|
||||
- [ ] Accès SSH au serveur PROD
|
||||
- [ ] Accès à la base de données PROD
|
||||
- [ ] Droits pour éditer le crontab
|
||||
|
||||
## 📝 Fichiers à déployer
|
||||
Les fichiers suivants doivent être présents sur le serveur PROD :
|
||||
- `/scripts/cron/process_email_queue.php`
|
||||
- `/scripts/cron/process_email_queue_with_daily_log.sh`
|
||||
- `/scripts/cron/test_email_queue.php`
|
||||
- `/src/Services/ReceiptPDFGenerator.php` (nouveau)
|
||||
- `/src/Services/ReceiptService.php` (mis à jour)
|
||||
- `/src/Core/MonitoredDatabase.php` (mis à jour)
|
||||
- `/src/Controllers/EntiteController.php` (mis à jour)
|
||||
- `/src/Controllers/LoginController.php` (mis à jour)
|
||||
- `/scripts/sql/add_chk_user_delete_pass.sql` (nouveau)
|
||||
|
||||
---
|
||||
|
||||
## 🔧 ÉTAPES DE MISE EN PRODUCTION
|
||||
|
||||
### 1️⃣ Mise à jour de la base de données
|
||||
|
||||
Se connecter à la base de données PROD et exécuter :
|
||||
|
||||
```sql
|
||||
-- Vérifier d'abord la structure actuelle de email_queue
|
||||
DESCRIBE email_queue;
|
||||
|
||||
-- Ajouter les champs manquants pour email_queue si nécessaire
|
||||
ALTER TABLE `email_queue`
|
||||
ADD COLUMN IF NOT EXISTS `sent_at` TIMESTAMP NULL DEFAULT NULL
|
||||
COMMENT 'Date/heure d\'envoi effectif de l\'email'
|
||||
AFTER `status`;
|
||||
|
||||
ALTER TABLE `email_queue`
|
||||
ADD COLUMN IF NOT EXISTS `error_message` TEXT NULL DEFAULT NULL
|
||||
COMMENT 'Message d\'erreur en cas d\'échec'
|
||||
AFTER `attempts`;
|
||||
|
||||
-- Ajouter les index pour optimiser les performances
|
||||
ALTER TABLE `email_queue`
|
||||
ADD INDEX IF NOT EXISTS `idx_status_attempts` (`status`, `attempts`);
|
||||
|
||||
ALTER TABLE `email_queue`
|
||||
ADD INDEX IF NOT EXISTS `idx_sent_at` (`sent_at`);
|
||||
|
||||
-- Vérifier les modifications email_queue
|
||||
DESCRIBE email_queue;
|
||||
|
||||
-- ⚠️ IMPORTANT : Ajouter le nouveau champ chk_user_delete_pass dans entites
|
||||
source /var/www/geosector/api/scripts/sql/add_chk_user_delete_pass.sql;
|
||||
```
|
||||
|
||||
### 2️⃣ Test du script avant mise en production
|
||||
|
||||
```bash
|
||||
# Se connecter au serveur PROD
|
||||
ssh user@prod-server
|
||||
|
||||
# Aller dans le répertoire de l'API
|
||||
cd /var/www/geosector/api
|
||||
|
||||
# Rendre les scripts exécutables
|
||||
chmod +x scripts/cron/process_email_queue.php
|
||||
chmod +x scripts/cron/test_email_queue.php
|
||||
|
||||
# Tester l'état de la queue (lecture seule)
|
||||
php scripts/cron/test_email_queue.php
|
||||
|
||||
# Si tout est OK, faire un test d'envoi sur 1 email
|
||||
# (modifier temporairement BATCH_SIZE à 1 dans le script si nécessaire)
|
||||
php scripts/cron/process_email_queue.php
|
||||
```
|
||||
|
||||
### 3️⃣ Configuration du CRON avec logs journaliers
|
||||
|
||||
```bash
|
||||
# Rendre le script wrapper exécutable
|
||||
chmod +x /var/www/geosector/api/scripts/cron/process_email_queue_with_daily_log.sh
|
||||
|
||||
# Éditer le crontab
|
||||
crontab -e
|
||||
|
||||
# Ajouter cette ligne pour exécution toutes les 5 minutes avec logs journaliers
|
||||
*/5 * * * * /var/www/geosector/api/scripts/cron/process_email_queue_with_daily_log.sh
|
||||
|
||||
# Sauvegarder et quitter (:wq sous vi/vim)
|
||||
|
||||
# Vérifier que le cron est bien enregistré
|
||||
crontab -l | grep email_queue
|
||||
|
||||
# Vérifier que le service cron est actif
|
||||
systemctl status cron
|
||||
```
|
||||
|
||||
**Note** : Les logs seront créés automatiquement dans `/var/www/geosector/api/logs/` avec le format : `email_queue_20250820.log`, `email_queue_20250821.log`, etc. Les logs de plus de 30 jours sont supprimés automatiquement.
|
||||
|
||||
### 4️⃣ Surveillance post-déploiement
|
||||
|
||||
Pendant les premières heures après la mise en production :
|
||||
|
||||
```bash
|
||||
# Surveiller les logs en temps réel (fichier du jour)
|
||||
tail -f /var/www/geosector/api/logs/email_queue_$(date +%Y%m%d).log
|
||||
|
||||
# Vérifier le statut de la queue
|
||||
php scripts/cron/test_email_queue.php
|
||||
|
||||
# Compter les emails traités
|
||||
mysql -u geo_app_user_prod -p geo_app -e "
|
||||
SELECT status, COUNT(*) as count
|
||||
FROM email_queue
|
||||
WHERE DATE(created_at) = CURDATE()
|
||||
GROUP BY status;"
|
||||
|
||||
# Vérifier les erreurs éventuelles
|
||||
mysql -u geo_app_user_prod -p geo_app -e "
|
||||
SELECT id, to_email, subject, attempts, error_message
|
||||
FROM email_queue
|
||||
WHERE status='failed'
|
||||
ORDER BY created_at DESC
|
||||
LIMIT 10;"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🚨 ROLLBACK (si nécessaire)
|
||||
|
||||
En cas de problème, voici comment revenir en arrière :
|
||||
|
||||
```bash
|
||||
# 1. Stopper le cron
|
||||
crontab -e
|
||||
# Commenter la ligne du process_email_queue
|
||||
|
||||
# 2. Marquer les emails en attente pour traitement manuel
|
||||
mysql -u geo_app_user_prod -p geo_app -e "
|
||||
UPDATE email_queue
|
||||
SET status='pending', attempts=0
|
||||
WHERE status='failed' AND DATE(created_at) = CURDATE();"
|
||||
|
||||
# 3. Informer l'équipe pour traitement manuel si nécessaire
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📊 VALIDATION POST-DÉPLOIEMENT
|
||||
|
||||
### Critères de succès :
|
||||
- [ ] Aucune erreur dans les logs
|
||||
- [ ] Les emails sont envoyés dans les 5 minutes
|
||||
- [ ] Les reçus PDF sont correctement attachés
|
||||
- [ ] Le champ `date_sent_recu` est mis à jour dans `ope_pass`
|
||||
- [ ] Pas d'accumulation d'emails en status 'pending'
|
||||
|
||||
### Commandes de vérification :
|
||||
|
||||
```bash
|
||||
# Statistiques générales
|
||||
mysql -u geo_app_user_prod -p geo_app -e "
|
||||
SELECT
|
||||
status,
|
||||
COUNT(*) as count,
|
||||
MIN(created_at) as oldest,
|
||||
MAX(created_at) as newest
|
||||
FROM email_queue
|
||||
GROUP BY status;"
|
||||
|
||||
# Vérifier les passages avec reçus envoyés aujourd'hui
|
||||
mysql -u geo_app_user_prod -p geo_app -e "
|
||||
SELECT COUNT(*) as recus_envoyes_aujourdhui
|
||||
FROM ope_pass
|
||||
WHERE DATE(date_sent_recu) = CURDATE();"
|
||||
|
||||
# Performance du cron (dernières exécutions du jour)
|
||||
tail -20 /var/www/geosector/api/logs/email_queue_$(date +%Y%m%d).log | grep "Traitement terminé"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📞 CONTACTS EN CAS DE PROBLÈME
|
||||
|
||||
- **Responsable technique** : _____________
|
||||
- **DBA** : _____________
|
||||
- **Support O2Switch** : support@o2switch.fr
|
||||
|
||||
---
|
||||
|
||||
## 📋 NOTES IMPORTANTES
|
||||
|
||||
1. **Limite d'envoi** : 1500 emails/heure max (limite O2Switch)
|
||||
2. **Batch size** : 50 emails par exécution (toutes les 5 min = 600/heure max)
|
||||
3. **Lock file** : `/tmp/process_email_queue.lock` empêche l'exécution simultanée
|
||||
4. **Nettoyage auto** : Les emails envoyés > 30 jours sont supprimés automatiquement
|
||||
|
||||
## 🔒 SÉCURITÉ
|
||||
|
||||
- Les mots de passe SMTP ne sont jamais loggués
|
||||
- Les emails en erreur conservent le message d'erreur pour diagnostic
|
||||
- Le PDF est envoyé en pièce jointe encodée en base64
|
||||
|
||||
---
|
||||
|
||||
## ✅ CHECKLIST FINALE
|
||||
|
||||
### Email Queue :
|
||||
- [ ] Table email_queue mise à jour (sent_at, error_message, index)
|
||||
- [ ] Scripts cron testés avec succès
|
||||
- [ ] Cron configuré et actif
|
||||
- [ ] Logs accessibles et fonctionnels
|
||||
- [ ] Premier batch d'emails envoyé avec succès
|
||||
|
||||
### Permissions Suppression Passages :
|
||||
- [ ] Champ chk_user_delete_pass ajouté dans la table entites
|
||||
- [ ] EntiteController.php mis à jour pour gérer le nouveau champ
|
||||
- [ ] LoginController.php mis à jour pour retourner le champ dans amicale
|
||||
- [ ] Test de modification de permissions via l'interface admin
|
||||
|
||||
### Général :
|
||||
- [ ] Documentation mise à jour
|
||||
- [ ] Équipe informée de la mise en production
|
||||
|
||||
---
|
||||
|
||||
**Date de mise en production** : _______________
|
||||
**Validé par** : _______________
|
||||
**Signature** : _______________
|
||||
@@ -1,612 +0,0 @@
|
||||
# API D6MON - Documentation
|
||||
|
||||
## Introduction
|
||||
|
||||
L'API D6MON est une interface RESTful permettant d'interagir avec l'application D6MON. Cette API gère l'authentification des utilisateurs, la gestion des profils utilisateurs et la gestion des entités.
|
||||
|
||||
## Configuration
|
||||
|
||||
### Base URL
|
||||
|
||||
```
|
||||
https://app.d6mon.com/api/mon
|
||||
```
|
||||
|
||||
### En-têtes requis
|
||||
|
||||
Pour toutes les requêtes à l'API, les en-têtes suivants sont requis :
|
||||
|
||||
```
|
||||
Content-Type: application/json
|
||||
X-App-Identifier: app.d6mon.com
|
||||
X-Client-Type: mobile
|
||||
```
|
||||
|
||||
Pour les endpoints protégés (nécessitant une authentification), ajoutez également :
|
||||
|
||||
```
|
||||
Authorization: Bearer {token}
|
||||
```
|
||||
|
||||
Où `{token}` est le jeton d'authentification obtenu lors de la connexion.
|
||||
|
||||
## Authentification
|
||||
|
||||
### Connexion
|
||||
|
||||
**Endpoint :** `POST /login`
|
||||
|
||||
**Corps de la requête :**
|
||||
|
||||
```json
|
||||
{
|
||||
"email": "utilisateur@exemple.com",
|
||||
"password": "motdepasse"
|
||||
}
|
||||
```
|
||||
|
||||
**Réponse réussie :**
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": {
|
||||
"token": "session_token_here",
|
||||
"user": {
|
||||
"id": 123,
|
||||
"email": "utilisateur@exemple.com",
|
||||
"last_name": "Nom",
|
||||
"first_name": "Prénom",
|
||||
"display_name": "Nom d'affichage",
|
||||
"entity_id": 456
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Inscription
|
||||
|
||||
**Endpoint :** `POST /register`
|
||||
|
||||
**Corps de la requête :**
|
||||
|
||||
```json
|
||||
{
|
||||
"display_name": "Nom d'affichage",
|
||||
"email": "utilisateur@exemple.com",
|
||||
"first_name": "Prénom",
|
||||
"last_name": "Nom",
|
||||
"entity_id": 456
|
||||
}
|
||||
```
|
||||
|
||||
**Réponse réussie :**
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"message": "Inscription réussie. Un email contenant vos identifiants vous a été envoyé.",
|
||||
"data": {
|
||||
"user": {
|
||||
"id": 123,
|
||||
"email": "utilisateur@exemple.com",
|
||||
"display_name": "Nom d'affichage"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Mot de passe oublié
|
||||
|
||||
**Endpoint :** `POST /lost-password`
|
||||
|
||||
**Corps de la requête :**
|
||||
|
||||
```json
|
||||
{
|
||||
"email": "utilisateur@exemple.com"
|
||||
}
|
||||
```
|
||||
|
||||
**Réponse réussie :**
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"message": "Un nouveau mot de passe a été envoyé à votre adresse email."
|
||||
}
|
||||
```
|
||||
|
||||
### Déconnexion
|
||||
|
||||
**Endpoint :** `POST /logout`
|
||||
|
||||
**Réponse réussie :**
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"message": "Déconnecté avec succès"
|
||||
}
|
||||
```
|
||||
|
||||
## Gestion des utilisateurs
|
||||
|
||||
### Récupérer le profil de l'utilisateur connecté
|
||||
|
||||
**Endpoint :** `GET /user/profile`
|
||||
|
||||
**Réponse réussie :**
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": {
|
||||
"id": 123,
|
||||
"entity_id": 456,
|
||||
"display_name": "Nom d'affichage",
|
||||
"first_name": "Prénom",
|
||||
"last_name": "Nom",
|
||||
"avatar": "url_avatar",
|
||||
"email": "utilisateur@exemple.com",
|
||||
"phone": "+33612345678",
|
||||
"address1": "Adresse ligne 1",
|
||||
"address2": "Adresse ligne 2",
|
||||
"code_postal": "75000",
|
||||
"city": "Paris",
|
||||
"country": "France",
|
||||
"seat_name": "Siège",
|
||||
"created_at": "2023-01-01T00:00:00Z",
|
||||
"updated_at": "2023-01-01T00:00:00Z",
|
||||
"connected_at": "2023-01-01T00:00:00Z",
|
||||
"is_active": true,
|
||||
"entity": {
|
||||
"id": 456,
|
||||
"name": "Nom de l'entité",
|
||||
"email": "entite@exemple.com",
|
||||
"phone": "+33123456789",
|
||||
"address1": "Adresse ligne 1",
|
||||
"address2": "Adresse ligne 2",
|
||||
"code_postal": "75000",
|
||||
"city": "Paris",
|
||||
"country": "France",
|
||||
"created_at": "2023-01-01T00:00:00Z",
|
||||
"updated_at": "2023-01-01T00:00:00Z",
|
||||
"is_active": true
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Mettre à jour le profil de l'utilisateur connecté
|
||||
|
||||
**Endpoint :** `PUT /user/profile`
|
||||
|
||||
**Corps de la requête :**
|
||||
|
||||
```json
|
||||
{
|
||||
"display_name": "Nouveau nom d'affichage",
|
||||
"first_name": "Nouveau prénom",
|
||||
"last_name": "Nouveau nom",
|
||||
"phone": "+33612345678",
|
||||
"address1": "Nouvelle adresse ligne 1",
|
||||
"address2": "Nouvelle adresse ligne 2",
|
||||
"code_postal": "75001",
|
||||
"city": "Paris",
|
||||
"country": "France",
|
||||
"seat_name": "Nouveau siège"
|
||||
}
|
||||
```
|
||||
|
||||
**Réponse réussie :**
|
||||
Même format que `GET /user/profile` avec les données mises à jour.
|
||||
|
||||
### Changer le mot de passe
|
||||
|
||||
**Endpoint :** `POST /user/change-password`
|
||||
|
||||
**Corps de la requête :**
|
||||
|
||||
```json
|
||||
{
|
||||
"current_password": "ancien_mot_de_passe",
|
||||
"new_password": "nouveau_mot_de_passe"
|
||||
}
|
||||
```
|
||||
|
||||
**Réponse réussie :**
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"message": "Mot de passe changé avec succès"
|
||||
}
|
||||
```
|
||||
|
||||
### Récupérer un utilisateur par ID
|
||||
|
||||
**Endpoint :** `GET /user/{id}`
|
||||
|
||||
**Réponse réussie :**
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": {
|
||||
"id": 123,
|
||||
"entity_id": 456,
|
||||
"display_name": "Nom d'affichage",
|
||||
"first_name": "Prénom",
|
||||
"last_name": "Nom",
|
||||
"avatar": "url_avatar",
|
||||
"email": "utilisateur@exemple.com",
|
||||
"phone": "+33612345678",
|
||||
"address1": "Adresse ligne 1",
|
||||
"address2": "Adresse ligne 2",
|
||||
"code_postal": "75000",
|
||||
"city": "Paris",
|
||||
"country": "France",
|
||||
"seat_name": "Siège",
|
||||
"created_at": "2023-01-01T00:00:00Z",
|
||||
"updated_at": "2023-01-01T00:00:00Z",
|
||||
"connected_at": "2023-01-01T00:00:00Z",
|
||||
"is_active": true
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Récupérer la liste des utilisateurs
|
||||
|
||||
**Endpoint :** `GET /users`
|
||||
|
||||
**Paramètres de requête :**
|
||||
|
||||
- `page` (optionnel) : Numéro de page (défaut : 1)
|
||||
- `limit` (optionnel) : Nombre d'éléments par page (défaut : 20)
|
||||
- `entity_id` (optionnel) : Filtrer par ID d'entité
|
||||
|
||||
**Réponse réussie :**
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": {
|
||||
"users": [
|
||||
{
|
||||
"id": 123,
|
||||
"entity_id": 456,
|
||||
"display_name": "Nom d'affichage",
|
||||
"first_name": "Prénom",
|
||||
"last_name": "Nom",
|
||||
"avatar": "url_avatar",
|
||||
"email": "utilisateur@exemple.com",
|
||||
"address1": "Adresse ligne 1",
|
||||
"city": "Paris",
|
||||
"country": "France",
|
||||
"created_at": "2023-01-01T00:00:00Z",
|
||||
"updated_at": "2023-01-01T00:00:00Z",
|
||||
"connected_at": "2023-01-01T00:00:00Z",
|
||||
"is_active": true
|
||||
}
|
||||
],
|
||||
"pagination": {
|
||||
"total": 100,
|
||||
"page": 1,
|
||||
"limit": 20,
|
||||
"pages": 5
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Créer un nouvel utilisateur
|
||||
|
||||
**Endpoint :** `POST /user`
|
||||
|
||||
**Corps de la requête :**
|
||||
|
||||
```json
|
||||
{
|
||||
"display_name": "Nom d'affichage",
|
||||
"email": "utilisateur@exemple.com",
|
||||
"first_name": "Prénom",
|
||||
"last_name": "Nom",
|
||||
"entity_id": 456,
|
||||
"phone": "+33612345678",
|
||||
"address1": "Adresse ligne 1",
|
||||
"address2": "Adresse ligne 2",
|
||||
"code_postal": "75000",
|
||||
"city": "Paris",
|
||||
"country": "France",
|
||||
"seat_name": "Siège"
|
||||
}
|
||||
```
|
||||
|
||||
**Réponse réussie :**
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"message": "Utilisateur créé avec succès. Un email avec les identifiants a été envoyé.",
|
||||
"data": {
|
||||
"id": 123,
|
||||
"display_name": "Nom d'affichage",
|
||||
"email": "utilisateur@exemple.com"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Désactiver un utilisateur
|
||||
|
||||
**Endpoint :** `DELETE /user/{id}`
|
||||
|
||||
**Réponse réussie :**
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"message": "Utilisateur désactivé avec succès"
|
||||
}
|
||||
```
|
||||
|
||||
## Gestion des entités
|
||||
|
||||
### Récupérer toutes les entités
|
||||
|
||||
**Endpoint :** `GET /entities`
|
||||
|
||||
**Paramètres de requête :**
|
||||
|
||||
- `page` (optionnel) : Numéro de page (défaut : 1)
|
||||
- `limit` (optionnel) : Nombre d'éléments par page (défaut : 20)
|
||||
- `search` (optionnel) : Terme de recherche
|
||||
|
||||
**Réponse réussie :**
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": {
|
||||
"entities": [
|
||||
{
|
||||
"id": 456,
|
||||
"name": "Nom de l'entité",
|
||||
"email": "entite@exemple.com",
|
||||
"phone": "+33123456789",
|
||||
"address1": "Adresse ligne 1",
|
||||
"address2": "Adresse ligne 2",
|
||||
"code_postal": "75000",
|
||||
"city": "Paris",
|
||||
"country": "France",
|
||||
"created_at": "2023-01-01T00:00:00Z",
|
||||
"updated_at": "2023-01-01T00:00:00Z",
|
||||
"is_active": true
|
||||
}
|
||||
],
|
||||
"pagination": {
|
||||
"total": 50,
|
||||
"page": 1,
|
||||
"limit": 20,
|
||||
"pages": 3
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Récupérer une entité par ID
|
||||
|
||||
**Endpoint :** `GET /entity/{id}`
|
||||
|
||||
**Réponse réussie :**
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": {
|
||||
"id": 456,
|
||||
"name": "Nom de l'entité",
|
||||
"email": "entite@exemple.com",
|
||||
"phone": "+33123456789",
|
||||
"address1": "Adresse ligne 1",
|
||||
"address2": "Adresse ligne 2",
|
||||
"code_postal": "75000",
|
||||
"city": "Paris",
|
||||
"country": "France",
|
||||
"created_at": "2023-01-01T00:00:00Z",
|
||||
"updated_at": "2023-01-01T00:00:00Z",
|
||||
"is_active": true,
|
||||
"users": [
|
||||
{
|
||||
"id": 123,
|
||||
"display_name": "Nom d'affichage",
|
||||
"first_name": "Prénom",
|
||||
"last_name": "Nom",
|
||||
"avatar": "url_avatar",
|
||||
"email": "utilisateur@exemple.com",
|
||||
"created_at": "2023-01-01T00:00:00Z",
|
||||
"is_active": true
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Créer une nouvelle entité
|
||||
|
||||
**Endpoint :** `POST /entity`
|
||||
|
||||
**Corps de la requête :**
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "Nom de l'entité",
|
||||
"email": "entite@exemple.com",
|
||||
"phone": "+33123456789",
|
||||
"address1": "Adresse ligne 1",
|
||||
"address2": "Adresse ligne 2",
|
||||
"code_postal": "75000",
|
||||
"city": "Paris",
|
||||
"country": "France"
|
||||
}
|
||||
```
|
||||
|
||||
**Réponse réussie :**
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"message": "Entité créée avec succès",
|
||||
"data": {
|
||||
"id": 456,
|
||||
"name": "Nom de l'entité"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Mettre à jour une entité
|
||||
|
||||
**Endpoint :** `PUT /entity/{id}`
|
||||
|
||||
**Corps de la requête :**
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "Nouveau nom de l'entité",
|
||||
"email": "nouvelle-entite@exemple.com",
|
||||
"phone": "+33987654321",
|
||||
"address1": "Nouvelle adresse ligne 1",
|
||||
"address2": "Nouvelle adresse ligne 2",
|
||||
"code_postal": "75001",
|
||||
"city": "Paris",
|
||||
"country": "France"
|
||||
}
|
||||
```
|
||||
|
||||
**Réponse réussie :**
|
||||
Même format que `GET /entity/{id}` avec les données mises à jour.
|
||||
|
||||
### Désactiver une entité
|
||||
|
||||
**Endpoint :** `DELETE /entity/{id}`
|
||||
|
||||
**Réponse réussie :**
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"message": "Entité désactivée avec succès"
|
||||
}
|
||||
```
|
||||
|
||||
### Récupérer les utilisateurs d'une entité
|
||||
|
||||
**Endpoint :** `GET /entity/{id}/users`
|
||||
|
||||
**Paramètres de requête :**
|
||||
|
||||
- `page` (optionnel) : Numéro de page (défaut : 1)
|
||||
- `limit` (optionnel) : Nombre d'éléments par page (défaut : 20)
|
||||
|
||||
**Réponse réussie :**
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": {
|
||||
"users": [
|
||||
{
|
||||
"id": 123,
|
||||
"display_name": "Nom d'affichage",
|
||||
"first_name": "Prénom",
|
||||
"last_name": "Nom",
|
||||
"avatar": "url_avatar",
|
||||
"email": "utilisateur@exemple.com",
|
||||
"phone": "+33612345678",
|
||||
"created_at": "2023-01-01T00:00:00Z",
|
||||
"updated_at": "2023-01-01T00:00:00Z",
|
||||
"connected_at": "2023-01-01T00:00:00Z",
|
||||
"is_active": true
|
||||
}
|
||||
],
|
||||
"pagination": {
|
||||
"total": 25,
|
||||
"page": 1,
|
||||
"limit": 20,
|
||||
"pages": 2
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Structure des données
|
||||
|
||||
### Table `users`
|
||||
|
||||
```sql
|
||||
CREATE TABLE users (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
entity_id INT,
|
||||
display_name VARCHAR(100) NOT NULL,
|
||||
first_name VARCHAR(100),
|
||||
encrypted_last_name VARCHAR(512),
|
||||
avatar VARCHAR(255),
|
||||
encrypted_email VARCHAR(512),
|
||||
encrypted_phone VARCHAR(255),
|
||||
address1 VARCHAR(255),
|
||||
address2 VARCHAR(255),
|
||||
code_postal VARCHAR(20),
|
||||
city VARCHAR(100),
|
||||
country VARCHAR(100),
|
||||
seat_name VARCHAR(20),
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
connected_at DATETIME,
|
||||
is_active BOOLEAN DEFAULT TRUE,
|
||||
FOREIGN KEY (entity_id) REFERENCES entities(id)
|
||||
);
|
||||
```
|
||||
|
||||
### Table `entities`
|
||||
|
||||
```sql
|
||||
CREATE TABLE entities (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
encrypted_name VARCHAR(512) NOT NULL,
|
||||
encrypted_email VARCHAR(512),
|
||||
encrypted_phone VARCHAR(255),
|
||||
address1 VARCHAR(255),
|
||||
address2 VARCHAR(255),
|
||||
code_postal VARCHAR(20),
|
||||
city VARCHAR(100),
|
||||
country VARCHAR(100),
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
is_active BOOLEAN DEFAULT TRUE
|
||||
);
|
||||
```
|
||||
|
||||
## Sécurité
|
||||
|
||||
L'API utilise plusieurs mécanismes pour assurer la sécurité des données :
|
||||
|
||||
1. **Authentification par jeton** : Un jeton d'authentification est requis pour accéder aux endpoints protégés.
|
||||
2. **Chiffrement des données sensibles** : Les données sensibles comme les noms, emails et numéros de téléphone sont chiffrées en base de données.
|
||||
3. **Validation des entrées** : Toutes les entrées utilisateur sont validées avant traitement.
|
||||
4. **Gestion des erreurs** : Les erreurs sont gérées de manière sécurisée sans divulguer d'informations sensibles.
|
||||
|
||||
## Codes d'erreur
|
||||
|
||||
- `400 Bad Request` : Requête invalide ou données manquantes
|
||||
- `401 Unauthorized` : Authentification requise ou échouée
|
||||
- `403 Forbidden` : Accès non autorisé à la ressource
|
||||
- `404 Not Found` : Ressource non trouvée
|
||||
- `409 Conflict` : Conflit avec l'état actuel de la ressource
|
||||
- `500 Internal Server Error` : Erreur serveur
|
||||
|
||||
## Notes d'implémentation
|
||||
|
||||
- Les mots de passe sont hachés avec l'algorithme bcrypt.
|
||||
- Les données sensibles sont chiffrées avec AES-256-CBC.
|
||||
- Les emails sont envoyés pour les opérations importantes (inscription, réinitialisation de mot de passe).
|
||||
- Les sessions sont gérées côté serveur avec un délai d'expiration.
|
||||
339
api/docs/README-UPLOAD.md
Executable file
339
api/docs/README-UPLOAD.md
Executable file
@@ -0,0 +1,339 @@
|
||||
# Système de Gestion des Fichiers - API Geosector
|
||||
|
||||
## Vue d'ensemble
|
||||
|
||||
Ce document décrit l'organisation et la gestion des fichiers uploadés dans l'API Geosector. Le système permet de stocker et organiser différents types de fichiers par entité, utilisateur, opération et passage.
|
||||
|
||||
## Structure des Dossiers
|
||||
|
||||
```
|
||||
uploads/
|
||||
├── entites/
|
||||
│ ├── {entite_id}/
|
||||
│ │ ├── documents/ # PDF, Excel généraux de l'entité
|
||||
│ │ ├── images/ # Images de l'entité
|
||||
│ │ ├── users/ # Dossier pour les fichiers des utilisateurs
|
||||
│ │ │ └── {user_id}/ # Images par utilisateur (avatars, etc.)
|
||||
│ │ └── operations/ # Dossier pour les opérations
|
||||
│ │ └── {operation_id}/
|
||||
│ │ ├── documents/ # Fichiers Excel de l'opération
|
||||
│ │ └── passages/ # Fichiers des passages de cette opération
|
||||
│ │ └── {passage_id}/ # PDF et images par passage
|
||||
│ └── temp/ # Fichiers temporaires avant validation
|
||||
```
|
||||
|
||||
### Exemples de chemins
|
||||
|
||||
- Document d'entité : `uploads/entites/5/documents/reglement_2024.pdf`
|
||||
- Avatar utilisateur : `uploads/entites/5/users/123/avatar.jpg`
|
||||
- Excel d'opération : `uploads/entites/5/operations/2644/documents/planning.xlsx`
|
||||
- Photo de passage : `uploads/entites/5/operations/2644/passages/789/photo_1.jpg`
|
||||
|
||||
## Structure de la Table `medias`
|
||||
|
||||
### Table existante enrichie
|
||||
|
||||
```sql
|
||||
-- Structure complète de la table medias
|
||||
CREATE TABLE `medias` (
|
||||
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
|
||||
`support` varchar(45) NOT NULL DEFAULT '' COMMENT 'Type de support (entite, user, operation, passage)',
|
||||
`support_id` int(10) unsigned NOT NULL DEFAULT 0 COMMENT 'ID de l\'élément associé',
|
||||
`fichier` varchar(250) NOT NULL DEFAULT '' COMMENT 'Nom du fichier stocké',
|
||||
`file_type` varchar(50) DEFAULT NULL COMMENT 'Extension du fichier (pdf, jpg, xlsx, etc.)',
|
||||
`file_size` int(10) unsigned DEFAULT NULL COMMENT 'Taille du fichier en octets',
|
||||
`mime_type` varchar(100) DEFAULT NULL COMMENT 'Type MIME du fichier',
|
||||
`original_name` varchar(255) DEFAULT NULL COMMENT 'Nom original du fichier uploadé',
|
||||
`fk_entite` int(10) unsigned DEFAULT NULL COMMENT 'ID de l\'entité propriétaire',
|
||||
`fk_operation` int(10) unsigned DEFAULT NULL COMMENT 'ID de l\'opération (pour passages)',
|
||||
`file_path` varchar(500) DEFAULT NULL COMMENT 'Chemin complet du fichier',
|
||||
`original_width` int(10) unsigned DEFAULT NULL COMMENT 'Largeur originale de l\'image',
|
||||
`original_height` int(10) unsigned DEFAULT NULL COMMENT 'Hauteur originale de l\'image',
|
||||
`processed_width` int(10) unsigned DEFAULT NULL COMMENT 'Largeur après traitement',
|
||||
`processed_height` int(10) unsigned DEFAULT NULL COMMENT 'Hauteur après traitement',
|
||||
`is_processed` tinyint(1) unsigned DEFAULT 0 COMMENT 'Image redimensionnée (1) ou originale (0)',
|
||||
`description` varchar(100) NOT NULL DEFAULT '' COMMENT 'Description du fichier',
|
||||
`created_at` timestamp NOT NULL DEFAULT current_timestamp(),
|
||||
`fk_user_creat` int(10) unsigned NOT NULL DEFAULT 0,
|
||||
`updated_at` timestamp NULL DEFAULT NULL ON UPDATE current_timestamp(),
|
||||
`fk_user_modif` int(10) unsigned NOT NULL DEFAULT 0,
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `id_UNIQUE` (`id`),
|
||||
KEY `idx_entite` (`fk_entite`),
|
||||
KEY `idx_operation` (`fk_operation`),
|
||||
KEY `idx_support_type` (`support`, `support_id`),
|
||||
KEY `idx_file_type` (`file_type`),
|
||||
CONSTRAINT `fk_medias_entite` FOREIGN KEY (`fk_entite`) REFERENCES `entites` (`id`) ON UPDATE CASCADE ON DELETE CASCADE,
|
||||
CONSTRAINT `fk_medias_operation` FOREIGN KEY (`fk_operation`) REFERENCES `operations` (`id`) ON UPDATE CASCADE ON DELETE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
```
|
||||
|
||||
### Migration SQL pour table existante
|
||||
|
||||
```sql
|
||||
-- Ajout des nouvelles colonnes à la table existante
|
||||
ALTER TABLE `medias`
|
||||
ADD COLUMN `file_type` varchar(50) DEFAULT NULL COMMENT 'Extension du fichier (pdf, jpg, xlsx, etc.)' AFTER `fichier`,
|
||||
ADD COLUMN `file_size` int(10) unsigned DEFAULT NULL COMMENT 'Taille du fichier en octets' AFTER `file_type`,
|
||||
ADD COLUMN `mime_type` varchar(100) DEFAULT NULL COMMENT 'Type MIME du fichier' AFTER `file_size`,
|
||||
ADD COLUMN `original_name` varchar(255) DEFAULT NULL COMMENT 'Nom original du fichier uploadé' AFTER `mime_type`,
|
||||
ADD COLUMN `fk_entite` int(10) unsigned DEFAULT NULL COMMENT 'ID de l\'entité propriétaire' AFTER `support_id`,
|
||||
ADD COLUMN `fk_operation` int(10) unsigned DEFAULT NULL COMMENT 'ID de l\'opération (pour passages)' AFTER `fk_entite`,
|
||||
ADD COLUMN `file_path` varchar(500) DEFAULT NULL COMMENT 'Chemin complet du fichier' AFTER `original_name`,
|
||||
ADD COLUMN `original_width` int(10) unsigned DEFAULT NULL COMMENT 'Largeur originale de l\'image' AFTER `file_path`,
|
||||
ADD COLUMN `original_height` int(10) unsigned DEFAULT NULL COMMENT 'Hauteur originale de l\'image' AFTER `original_width`,
|
||||
ADD COLUMN `processed_width` int(10) unsigned DEFAULT NULL COMMENT 'Largeur après traitement' AFTER `original_height`,
|
||||
ADD COLUMN `processed_height` int(10) unsigned DEFAULT NULL COMMENT 'Hauteur après traitement' AFTER `processed_width`,
|
||||
ADD COLUMN `is_processed` tinyint(1) unsigned DEFAULT 0 COMMENT 'Image redimensionnée (1) ou originale (0)' AFTER `processed_height`;
|
||||
|
||||
-- Ajout des index pour optimiser les requêtes
|
||||
ALTER TABLE `medias`
|
||||
ADD INDEX `idx_entite` (`fk_entite`),
|
||||
ADD INDEX `idx_operation` (`fk_operation`),
|
||||
ADD INDEX `idx_support_type` (`support`, `support_id`),
|
||||
ADD INDEX `idx_file_type` (`file_type`);
|
||||
|
||||
-- Ajout des contraintes de clés étrangères
|
||||
ALTER TABLE `medias`
|
||||
ADD CONSTRAINT `fk_medias_entite` FOREIGN KEY (`fk_entite`) REFERENCES `entites` (`id`) ON UPDATE CASCADE ON DELETE CASCADE,
|
||||
ADD CONSTRAINT `fk_medias_operation` FOREIGN KEY (`fk_operation`) REFERENCES `operations` (`id`) ON UPDATE CASCADE ON DELETE CASCADE;
|
||||
```
|
||||
|
||||
## Types de Support
|
||||
|
||||
### 1. Entité (`support = 'entite'`)
|
||||
|
||||
- **Fichiers autorisés** : PDF, Excel, Images (JPG, PNG)
|
||||
- **Taille max** : 20 MB
|
||||
- **Usage** : Documents généraux de l'entité (règlements, statuts, etc.)
|
||||
- **Chemin** : `uploads/entites/{entite_id}/documents/`
|
||||
|
||||
### 2. Utilisateur (`support = 'user'`)
|
||||
|
||||
- **Fichiers autorisés** : Images uniquement (JPG, PNG, GIF, WebP)
|
||||
- **Taille max** : 5 MB
|
||||
- **Usage** : Avatars, photos de profil
|
||||
- **Chemin** : `uploads/entites/{entite_id}/users/{user_id}/`
|
||||
- **Traitement** : Redimensionnement automatique
|
||||
|
||||
### 3. Opération (`support = 'operation'`)
|
||||
|
||||
- **Fichiers autorisés** : Excel uniquement (XLS, XLSX)
|
||||
- **Taille max** : 20 MB
|
||||
- **Usage** : Plannings, listes, données d'opération
|
||||
- **Chemin** : `uploads/entites/{entite_id}/operations/{operation_id}/documents/`
|
||||
|
||||
### 4. Passage (`support = 'passage'`)
|
||||
|
||||
- **Fichiers autorisés** : PDF et Images (JPG, PNG, PDF)
|
||||
- **Taille max** : 10 MB par fichier
|
||||
- **Usage** : Reçus, photos de passage, justificatifs
|
||||
- **Chemin** : `uploads/entites/{entite_id}/operations/{operation_id}/passages/{passage_id}/`
|
||||
- **Traitement** : Redimensionnement automatique pour les images
|
||||
|
||||
## Traitement Automatique des Images
|
||||
|
||||
### Règles de redimensionnement
|
||||
|
||||
- **Dimension maximale** : 250px (hauteur ou largeur, selon la plus grande)
|
||||
- **Résolution** : 72 DPI (optimisé web)
|
||||
- **Préservation du ratio** : Redimensionnement proportionnel
|
||||
- **Formats supportés** : JPG, PNG, GIF, WebP
|
||||
- **Qualité JPEG** : 85% (bon compromis qualité/poids)
|
||||
|
||||
### Exemples de transformation
|
||||
|
||||
```
|
||||
Image originale 1000x800px → Image traitée 250x200px
|
||||
Image originale 600x1200px → Image traitée 125x250px
|
||||
Image originale 200x150px → Pas de redimensionnement (déjà < 250px)
|
||||
```
|
||||
|
||||
### Workflow de traitement
|
||||
|
||||
1. **Upload** → Validation du type MIME
|
||||
2. **Analyse** → Détection des dimensions originales
|
||||
3. **Traitement** → Redimensionnement si nécessaire
|
||||
4. **Optimisation** → Compression et résolution web
|
||||
5. **Sauvegarde** → Image optimisée + métadonnées
|
||||
6. **Nettoyage** → Suppression du fichier temporaire
|
||||
|
||||
## API Endpoints
|
||||
|
||||
### Routes de gestion des fichiers
|
||||
|
||||
```php
|
||||
// Upload de fichiers
|
||||
POST /api/medias/upload
|
||||
Content-Type: multipart/form-data
|
||||
Body: {
|
||||
"file": [fichier],
|
||||
"support": "entite|user|operation|passage",
|
||||
"support_id": 123,
|
||||
"description": "Description du fichier"
|
||||
}
|
||||
|
||||
// Récupération d'un fichier
|
||||
GET /api/medias/{id}
|
||||
|
||||
// Liste des fichiers par support
|
||||
GET /api/medias/list/{support}/{support_id}
|
||||
|
||||
// Suppression d'un fichier
|
||||
DELETE /api/medias/{id}
|
||||
```
|
||||
|
||||
### Exemples de requêtes
|
||||
|
||||
#### Upload d'un avatar utilisateur
|
||||
|
||||
```bash
|
||||
curl -X POST "https://api.geosector.fr/medias/upload" \
|
||||
-H "Authorization: Bearer {token}" \
|
||||
-F "file=@avatar.jpg" \
|
||||
-F "support=user" \
|
||||
-F "support_id=123" \
|
||||
-F "description=Avatar utilisateur"
|
||||
```
|
||||
|
||||
#### Upload d'une photo de passage
|
||||
|
||||
```bash
|
||||
curl -X POST "https://api.geosector.fr/medias/upload" \
|
||||
-H "Authorization: Bearer {token}" \
|
||||
-F "file=@photo_passage.jpg" \
|
||||
-F "support=passage" \
|
||||
-F "support_id=789" \
|
||||
-F "description=Photo du passage"
|
||||
```
|
||||
|
||||
## Sécurité et Contrôles
|
||||
|
||||
### Validation des fichiers
|
||||
|
||||
- **Types MIME** : Vérification stricte du type de fichier
|
||||
- **Extensions** : Validation de l'extension par rapport au contenu
|
||||
- **Taille** : Limite selon le type de support
|
||||
- **Contenu** : Scan antivirus recommandé en production
|
||||
|
||||
### Contrôles d'accès
|
||||
|
||||
- **Authentification** : Token JWT requis
|
||||
- **Autorisation** : Utilisateur ne peut accéder qu'aux fichiers de son entité
|
||||
- **Vérification** : Contrôle que l'utilisateur appartient à l'entité du fichier
|
||||
- **Logs** : Traçabilité complète des uploads et accès
|
||||
|
||||
### Nommage des fichiers
|
||||
|
||||
```php
|
||||
// Format : {timestamp}_{random}_{sanitized_name}.{extension}
|
||||
// Exemple : 1640995200_a1b2c3_document_reglement.pdf
|
||||
```
|
||||
|
||||
## Gestion des Erreurs
|
||||
|
||||
### Codes d'erreur HTTP
|
||||
|
||||
- **400** : Fichier invalide ou paramètres manquants
|
||||
- **401** : Non authentifié
|
||||
- **403** : Accès refusé à cette entité
|
||||
- **413** : Fichier trop volumineux
|
||||
- **415** : Type de fichier non supporté
|
||||
- **500** : Erreur serveur lors du traitement
|
||||
|
||||
### Messages d'erreur
|
||||
|
||||
```json
|
||||
{
|
||||
"status": "error",
|
||||
"message": "Type de fichier non autorisé pour ce support",
|
||||
"code": "INVALID_FILE_TYPE",
|
||||
"allowed_types": ["jpg", "png", "gif", "webp"]
|
||||
}
|
||||
```
|
||||
|
||||
## Maintenance et Nettoyage
|
||||
|
||||
### Nettoyage automatique
|
||||
|
||||
- **Fichiers temporaires** : Suppression après 24h
|
||||
- **Fichiers orphelins** : Détection et suppression des fichiers sans référence en base
|
||||
- **Anciennes opérations** : Suppression en cascade lors de la suppression d'une opération
|
||||
|
||||
### Commandes de maintenance
|
||||
|
||||
```bash
|
||||
# Nettoyage des fichiers temporaires
|
||||
php scripts/cleanup_temp_files.php
|
||||
|
||||
# Détection des fichiers orphelins
|
||||
php scripts/find_orphan_files.php
|
||||
|
||||
# Statistiques d'utilisation
|
||||
php scripts/storage_stats.php
|
||||
```
|
||||
|
||||
## Performances et Optimisation
|
||||
|
||||
### Optimisations
|
||||
|
||||
- **CDN** : Recommandé pour la distribution des fichiers
|
||||
- **Cache** : Headers de cache appropriés pour les fichiers statiques
|
||||
- **Compression** : Gzip pour les réponses API
|
||||
- **Index** : Index optimisés sur la table medias
|
||||
|
||||
### Monitoring
|
||||
|
||||
- **Espace disque** : Surveillance de l'utilisation
|
||||
- **Performance** : Temps de traitement des images
|
||||
- **Erreurs** : Logs des échecs d'upload et de traitement
|
||||
|
||||
## Exemples d'Utilisation
|
||||
|
||||
### Cas d'usage typiques
|
||||
|
||||
1. **Upload d'avatar utilisateur**
|
||||
|
||||
- Fichier JPG de 2MB
|
||||
- Redimensionnement automatique à 250x250px
|
||||
- Stockage dans `uploads/entites/5/users/123/`
|
||||
|
||||
2. **Document d'opération**
|
||||
|
||||
- Fichier Excel de planning
|
||||
- Stockage dans `uploads/entites/5/operations/2644/documents/`
|
||||
- Pas de traitement (fichier conservé tel quel)
|
||||
|
||||
3. **Photo de passage**
|
||||
- Photo JPG de 8MB prise sur mobile
|
||||
- Redimensionnement automatique à 250px max
|
||||
- Stockage dans `uploads/entites/5/operations/2644/passages/789/`
|
||||
|
||||
### Intégration frontend
|
||||
|
||||
```javascript
|
||||
// Upload avec progress
|
||||
const uploadFile = async (file, support, supportId, description) => {
|
||||
const formData = new FormData();
|
||||
formData.append('file', file);
|
||||
formData.append('support', support);
|
||||
formData.append('support_id', supportId);
|
||||
formData.append('description', description);
|
||||
|
||||
const response = await fetch('/api/medias/upload', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
body: formData,
|
||||
});
|
||||
|
||||
return response.json();
|
||||
};
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
**Version** : 1.0
|
||||
**Date** : Juin 2025
|
||||
**Auteur** : API Geosector Team
|
||||
149
api/docs/SETUP_EMAIL_QUEUE_CRON.md
Normal file
149
api/docs/SETUP_EMAIL_QUEUE_CRON.md
Normal file
@@ -0,0 +1,149 @@
|
||||
# Instructions de mise en place du CRON pour la queue d'emails
|
||||
|
||||
## Problème résolu
|
||||
Les emails de reçus étaient insérés dans la table `email_queue` mais n'étaient jamais envoyés car il manquait le script de traitement.
|
||||
|
||||
## Fichiers créés
|
||||
1. `/scripts/cron/process_email_queue.php` - Script principal de traitement
|
||||
2. `/scripts/cron/test_email_queue.php` - Script de test/diagnostic
|
||||
3. `/scripts/sql/add_email_queue_fields.sql` - Migration SQL pour les champs manquants
|
||||
|
||||
## Installation sur les serveurs (DVA, REC, PROD)
|
||||
|
||||
### 1. Appliquer la migration SQL
|
||||
|
||||
Se connecter à la base de données et exécuter :
|
||||
|
||||
```bash
|
||||
mysql -u [user] -p [database] < /path/to/api/scripts/sql/add_email_queue_fields.sql
|
||||
```
|
||||
|
||||
Ou directement dans MySQL :
|
||||
```sql
|
||||
-- Ajouter les champs manquants
|
||||
ALTER TABLE `email_queue`
|
||||
ADD COLUMN IF NOT EXISTS `sent_at` TIMESTAMP NULL DEFAULT NULL AFTER `status`;
|
||||
|
||||
ALTER TABLE `email_queue`
|
||||
ADD COLUMN IF NOT EXISTS `error_message` TEXT NULL DEFAULT NULL AFTER `attempts`;
|
||||
|
||||
-- Ajouter les index pour les performances
|
||||
ALTER TABLE `email_queue`
|
||||
ADD INDEX IF NOT EXISTS `idx_status_attempts` (`status`, `attempts`);
|
||||
|
||||
ALTER TABLE `email_queue`
|
||||
ADD INDEX IF NOT EXISTS `idx_sent_at` (`sent_at`);
|
||||
```
|
||||
|
||||
### 2. Tester le script
|
||||
|
||||
Avant de mettre en place le cron, tester que tout fonctionne :
|
||||
|
||||
```bash
|
||||
# Vérifier l'état de la queue
|
||||
php /path/to/api/scripts/cron/test_email_queue.php
|
||||
|
||||
# Tester l'envoi (traite jusqu'à 50 emails)
|
||||
php /path/to/api/scripts/cron/process_email_queue.php
|
||||
```
|
||||
|
||||
### 3. Configurer le CRON
|
||||
|
||||
Ajouter la ligne suivante dans le crontab du serveur :
|
||||
|
||||
```bash
|
||||
# Éditer le crontab
|
||||
crontab -e
|
||||
|
||||
# Ajouter cette ligne (exécution toutes les 5 minutes)
|
||||
*/5 * * * * /usr/bin/php /path/to/api/scripts/cron/process_email_queue.php >> /var/log/email_queue.log 2>&1
|
||||
```
|
||||
|
||||
**Options de fréquence :**
|
||||
- `*/5 * * * *` - Toutes les 5 minutes (recommandé)
|
||||
- `*/10 * * * *` - Toutes les 10 minutes
|
||||
- `*/2 * * * *` - Toutes les 2 minutes (si volume important)
|
||||
|
||||
### 4. Monitoring
|
||||
|
||||
Le script génère des logs via `LogService`. Vérifier les logs dans :
|
||||
- `/path/to/api/logs/` (selon la configuration)
|
||||
|
||||
Points à surveiller :
|
||||
- Nombre d'emails traités
|
||||
- Emails en échec après 3 tentatives
|
||||
- Erreurs de connexion SMTP
|
||||
|
||||
### 5. Configuration SMTP
|
||||
|
||||
Vérifier que la configuration SMTP est correcte dans `AppConfig` :
|
||||
- Host SMTP
|
||||
- Port (587 pour TLS, 465 pour SSL)
|
||||
- Username/Password
|
||||
- Encryption (tls ou ssl)
|
||||
- From Email/Name
|
||||
|
||||
## Fonctionnement du script
|
||||
|
||||
### Caractéristiques
|
||||
- **Batch size** : 50 emails par exécution
|
||||
- **Max tentatives** : 3 essais par email
|
||||
- **Lock file** : Empêche l'exécution simultanée
|
||||
- **Nettoyage** : Supprime les emails envoyés > 30 jours
|
||||
- **Pause** : 0.5s entre chaque email (anti-spam)
|
||||
|
||||
### Workflow
|
||||
1. Récupère les emails avec `status = 'pending'` et `attempts < 3`
|
||||
2. Pour chaque email :
|
||||
- Incrémente le compteur de tentatives
|
||||
- Envoie via PHPMailer avec la config SMTP
|
||||
- Si succès : `status = 'sent'` + mise à jour du passage
|
||||
- Si échec : réessai à la prochaine exécution
|
||||
- Après 3 échecs : `status = 'failed'`
|
||||
|
||||
### Tables mises à jour
|
||||
- `email_queue` : status, attempts, sent_at, error_message
|
||||
- `ope_pass` : date_sent_recu, chk_email_sent
|
||||
|
||||
## Commandes utiles
|
||||
|
||||
```bash
|
||||
# Voir les emails en attente
|
||||
mysql -e "SELECT COUNT(*) FROM email_queue WHERE status='pending'" [database]
|
||||
|
||||
# Voir les emails échoués
|
||||
mysql -e "SELECT * FROM email_queue WHERE status='failed' ORDER BY created_at DESC LIMIT 10" [database]
|
||||
|
||||
# Réinitialiser un email échoué pour réessai
|
||||
mysql -e "UPDATE email_queue SET status='pending', attempts=0 WHERE id=[ID]" [database]
|
||||
|
||||
# Voir les logs du cron
|
||||
tail -f /var/log/email_queue.log
|
||||
|
||||
# Vérifier que le cron est actif
|
||||
crontab -l | grep process_email_queue
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Le cron ne s'exécute pas
|
||||
- Vérifier les permissions : `chmod +x process_email_queue.php`
|
||||
- Vérifier le chemin PHP : `which php`
|
||||
- Vérifier les logs système : `/var/log/syslog` ou `/var/log/cron`
|
||||
|
||||
### Emails en échec
|
||||
- Vérifier la config SMTP avec `test_email_queue.php`
|
||||
- Vérifier les logs pour les messages d'erreur
|
||||
- Tester la connexion SMTP : `telnet [smtp_host] [port]`
|
||||
|
||||
### Lock bloqué
|
||||
Si le message "Le processus est déjà en cours" persiste :
|
||||
```bash
|
||||
rm /tmp/process_email_queue.lock
|
||||
```
|
||||
|
||||
## Contact support
|
||||
En cas de problème, vérifier :
|
||||
1. Les logs de l'application
|
||||
2. La table `email_queue` pour les messages d'erreur
|
||||
3. La configuration SMTP dans AppConfig
|
||||
1
api/docs/STRIPE_VERIF.md
Normal file
1
api/docs/STRIPE_VERIF.md
Normal file
File diff suppressed because one or more lines are too long
575
api/docs/TECHBOOK.md
Normal file → Executable file
575
api/docs/TECHBOOK.md
Normal file → Executable file
@@ -8,7 +8,9 @@
|
||||
4. [Architecture des composants](#architecture-des-composants)
|
||||
5. [Base de données](#base-de-données)
|
||||
6. [Sécurité](#sécurité)
|
||||
7. [Endpoints API](#endpoints-api)
|
||||
7. [Gestion des mots de passe (NIST SP 800-63B)](#gestion-des-mots-de-passe-nist-sp-800-63b)
|
||||
8. [Endpoints API](#endpoints-api)
|
||||
9. [Changements récents](#changements-récents)
|
||||
|
||||
## Structure du projet
|
||||
|
||||
@@ -126,6 +128,54 @@ Exemple détaillé du parcours d'une requête POST /api/users :
|
||||
- Gère le pool de connexions
|
||||
- Assure la sécurité des requêtes
|
||||
|
||||
## Base de données
|
||||
|
||||
### Structure des tables principales
|
||||
|
||||
#### Table `users`
|
||||
- `encrypted_user_name` : Identifiant de connexion chiffré (unique)
|
||||
- `encrypted_email` : Email chiffré (unique)
|
||||
- `user_pass_hash` : Hash du mot de passe
|
||||
- `encrypted_name`, `encrypted_phone`, `encrypted_mobile` : Données personnelles chiffrées
|
||||
- Autres champs : `first_name`, `sect_name`, `fk_role`, `fk_entite`, etc.
|
||||
|
||||
#### Table `entites` (Amicales)
|
||||
- `chk_mdp_manuel` (DEFAULT 0) : Gestion manuelle des mots de passe
|
||||
- `chk_username_manuel` (DEFAULT 0) : Gestion manuelle des identifiants
|
||||
- `chk_stripe` : Activation des paiements Stripe
|
||||
- Données chiffrées : `encrypted_name`, `encrypted_email`, `encrypted_phone`, etc.
|
||||
|
||||
#### Table `medias`
|
||||
- `support` : Type de support (entite, user, operation, passage)
|
||||
- `support_id` : ID de l'élément associé
|
||||
- `file_category` : Catégorie (logo, export, carte, etc.)
|
||||
- `file_path` : Chemin complet du fichier
|
||||
- `processed_width/height` : Dimensions après traitement
|
||||
- Utilisée pour stocker les logos des entités
|
||||
|
||||
### Chiffrement des données
|
||||
|
||||
Toutes les données sensibles sont chiffrées avec AES-256-CBC :
|
||||
- Emails, noms, téléphones
|
||||
- Identifiants de connexion
|
||||
- Informations bancaires (IBAN, BIC)
|
||||
|
||||
### Migration de base de données
|
||||
|
||||
Script SQL pour ajouter les nouveaux champs :
|
||||
|
||||
```sql
|
||||
-- Ajout de la gestion manuelle des usernames
|
||||
ALTER TABLE `entites`
|
||||
ADD COLUMN `chk_username_manuel` tinyint(1) unsigned NOT NULL DEFAULT 0
|
||||
COMMENT 'Gestion des usernames manuelle (1) ou automatique (0)'
|
||||
AFTER `chk_mdp_manuel`;
|
||||
|
||||
-- Index pour optimiser la vérification d'unicité
|
||||
ALTER TABLE `users`
|
||||
ADD INDEX `idx_encrypted_user_name` (`encrypted_user_name`);
|
||||
```
|
||||
|
||||
## Sécurité
|
||||
|
||||
### Mesures implémentées
|
||||
@@ -137,6 +187,213 @@ Exemple détaillé du parcours d'une requête POST /api/users :
|
||||
- Gestion des CORS
|
||||
- Session sécurisée
|
||||
- Authentification requise
|
||||
- Chiffrement AES-256 des données sensibles
|
||||
- Envoi séparé des identifiants par email
|
||||
|
||||
## Gestion des mots de passe (NIST SP 800-63B)
|
||||
|
||||
### Vue d'ensemble
|
||||
|
||||
L'API implémente un système de gestion des mots de passe conforme aux recommandations NIST SP 800-63B, avec quelques adaptations spécifiques demandées par le client.
|
||||
|
||||
### Service PasswordSecurityService
|
||||
|
||||
Le service `PasswordSecurityService` (`src/Services/PasswordSecurityService.php`) gère :
|
||||
- Validation des mots de passe selon NIST
|
||||
- Vérification contre les bases de données de mots de passe compromis (HIBP)
|
||||
- Génération de mots de passe sécurisés
|
||||
- Estimation de la force des mots de passe
|
||||
|
||||
### Conformités NIST respectées
|
||||
|
||||
| Recommandation NIST | Notre Implémentation | Status |
|
||||
|-------------------|---------------------|--------|
|
||||
| **Longueur minimale : 8 caractères** | ✅ MIN = 8 caractères | ✅ CONFORME |
|
||||
| **Longueur maximale : 64 caractères minimum** | ✅ MAX = 64 caractères | ✅ CONFORME |
|
||||
| **Accepter TOUS les caractères ASCII imprimables** | ✅ Aucune restriction sur les caractères | ✅ CONFORME |
|
||||
| **Accepter les espaces** | ✅ Espaces acceptés (début, milieu, fin) | ✅ CONFORME |
|
||||
| **Accepter Unicode (émojis, accents, etc.)** | ✅ Support UTF-8 avec `mb_strlen()` | ✅ CONFORME |
|
||||
| **Vérifier contre les mots de passe compromis** | ✅ API Have I Been Pwned avec k-anonymity | ✅ CONFORME |
|
||||
| **Pas d'obligation de composition** | ✅ Pas d'erreur si manque majuscules/chiffres/spéciaux | ✅ CONFORME |
|
||||
| **Pas de changement périodique forcé** | ✅ Aucune expiration automatique | ✅ CONFORME |
|
||||
| **Permettre les phrases de passe** | ✅ "Mon chat Félix a 3 ans!" accepté | ✅ CONFORME |
|
||||
|
||||
### Déviations par choix du client
|
||||
|
||||
| Recommandation NIST | Notre Implémentation | Raison |
|
||||
|-------------------|---------------------|--------|
|
||||
| **Email unique par compte** | ❌ Plusieurs comptes par email autorisés | Demande client |
|
||||
| **Mot de passe ≠ identifiant** | ❌ Mot de passe = identifiant autorisé | Demande client |
|
||||
| **Vérifier contexte utilisateur** | ❌ Pas de vérification nom/email dans mdp | Demande client |
|
||||
|
||||
### Vérification contre les mots de passe compromis
|
||||
|
||||
#### Have I Been Pwned (HIBP) API
|
||||
|
||||
L'implémentation utilise l'API HIBP avec la technique **k-anonymity** pour préserver la confidentialité :
|
||||
|
||||
1. **Hash SHA-1** du mot de passe
|
||||
2. **Envoi des 5 premiers caractères** du hash à l'API
|
||||
3. **Comparaison locale** avec les suffixes retournés
|
||||
4. **Aucun mot de passe en clair** n'est transmis
|
||||
|
||||
#### Mode "Fail Open"
|
||||
|
||||
En cas d'erreur de l'API HIBP :
|
||||
- Le système laisse passer le mot de passe
|
||||
- Un avertissement est enregistré dans les logs
|
||||
- L'utilisateur n'est pas bloqué
|
||||
|
||||
### Exemples de mots de passe
|
||||
|
||||
#### Acceptés (conformes NIST)
|
||||
- `monmotdepasse` → Accepté (≥8 caractères, pas compromis)
|
||||
- `12345678` → Accepté SI pas dans HIBP
|
||||
- `Mon chat s'appelle Félix!` → Accepté (phrase de passe)
|
||||
- ` ` → Accepté si ≥8 espaces
|
||||
- `😀🎉🎈🎁🎂🍰🎊🎀` → Accepté (8 émojis)
|
||||
- `jean.dupont` → Accepté même si = username
|
||||
|
||||
#### Refusés
|
||||
- `pass123` → Refusé (< 8 caractères)
|
||||
- `password` → Refusé (compromis dans HIBP)
|
||||
- `123456789` → Refusé (compromis dans HIBP)
|
||||
- Mot de passe > 64 caractères → Refusé
|
||||
|
||||
### Force des mots de passe
|
||||
|
||||
Le système privilégie la **LONGUEUR** sur la complexité (conforme NIST) :
|
||||
|
||||
| Longueur | Force | Score |
|
||||
|----------|-------|-------|
|
||||
| < 8 car. | Trop court | 0-10 |
|
||||
| 8-11 car. | Acceptable | 20-40 |
|
||||
| 12-15 car. | Bon | 40-60 |
|
||||
| 16-19 car. | Fort | 60-80 |
|
||||
| ≥20 car. | Très fort | 80-100 |
|
||||
| Compromis | Compromis | ≤10 |
|
||||
|
||||
### Génération automatique
|
||||
|
||||
Pour la génération automatique, le système reste **strict** pour garantir des mots de passe forts :
|
||||
- Longueur : 12-16 caractères
|
||||
- Contient : majuscules + minuscules + chiffres + spéciaux
|
||||
- Vérifié contre HIBP (10 tentatives max)
|
||||
- Exemple : `Xk9#mP2$nL5!`
|
||||
|
||||
### Gestion des comptes multiples par email
|
||||
|
||||
Depuis janvier 2025, le système permet plusieurs comptes avec le même email :
|
||||
|
||||
#### Fonction `lostPassword` adaptée
|
||||
- Recherche **TOUS** les comptes avec l'email fourni
|
||||
- Génère **UN SEUL** mot de passe pour tous ces comptes
|
||||
- Met à jour **TOUS** les comptes en une requête
|
||||
- Envoie **UN SEUL** email avec la liste des usernames concernés
|
||||
|
||||
#### Exemple de comportement
|
||||
Si 3 comptes partagent l'email `contact@amicale.fr` :
|
||||
- `jean.dupont`
|
||||
- `marie.martin`
|
||||
- `paul.durand`
|
||||
|
||||
L'email contiendra :
|
||||
```
|
||||
Bonjour,
|
||||
Voici votre nouveau mot de passe pour les comptes : jean.dupont, marie.martin, paul.durand
|
||||
Mot de passe : XyZ123!@#
|
||||
```
|
||||
|
||||
### Endpoints API dédiés aux mots de passe
|
||||
|
||||
#### Vérification de force (public)
|
||||
```http
|
||||
POST /api/password/check
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"password": "monmotdepasse",
|
||||
"check_compromised": true
|
||||
}
|
||||
```
|
||||
|
||||
**Réponse :**
|
||||
```json
|
||||
{
|
||||
"status": "success",
|
||||
"valid": false,
|
||||
"errors": [
|
||||
"Ce mot de passe a été trouvé 23 547 fois dans des fuites de données."
|
||||
],
|
||||
"warnings": [
|
||||
"Suggestion : Évitez les séquences communes pour plus de sécurité"
|
||||
],
|
||||
"strength": {
|
||||
"score": 20,
|
||||
"strength": "Faible",
|
||||
"feedback": ["Ce mot de passe a été compromis"],
|
||||
"length": 13,
|
||||
"diversity": 1
|
||||
},
|
||||
"compromised": {
|
||||
"compromised": true,
|
||||
"occurrences": 23547,
|
||||
"message": "Ce mot de passe a été trouvé 23 547 fois dans des fuites de données"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Vérification de compromission uniquement (public)
|
||||
```http
|
||||
POST /api/password/compromised
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"password": "monmotdepasse"
|
||||
}
|
||||
```
|
||||
|
||||
#### Génération automatique (authentifié)
|
||||
```http
|
||||
GET /api/password/generate?length=14
|
||||
Authorization: Bearer {session_id}
|
||||
```
|
||||
|
||||
**Réponse :**
|
||||
```json
|
||||
{
|
||||
"status": "success",
|
||||
"password": "Xk9#mP2$nL5!qR",
|
||||
"length": 14,
|
||||
"strength": {
|
||||
"score": 85,
|
||||
"strength": "Très fort",
|
||||
"feedback": []
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Configuration et sécurité
|
||||
|
||||
#### Paramètres de sécurité
|
||||
- **Timeout API HIBP** : 5 secondes
|
||||
- **Cache** : 15 minutes pour les vérifications répétées
|
||||
- **Logging** : Aucun mot de passe en clair dans les logs
|
||||
- **K-anonymity** : Seuls 5 caractères du hash SHA-1 envoyés
|
||||
|
||||
#### Points d'intégration
|
||||
- `LoginController::register` : Validation lors de l'inscription
|
||||
- `LoginController::lostPassword` : Génération sécurisée
|
||||
- `UserController::createUser` : Validation si mot de passe manuel
|
||||
- `UserController::updateUser` : Validation lors du changement
|
||||
- `ApiService::generateSecurePassword` : Génération avec vérification HIBP
|
||||
|
||||
### Résumé
|
||||
|
||||
✅ **100% CONFORME NIST** pour les aspects techniques
|
||||
✅ **Adapté aux besoins du client** (emails multiples, mdp=username)
|
||||
✅ **Sécurité maximale** avec vérification HIBP
|
||||
✅ **Expérience utilisateur optimale** (souple mais sécurisé)
|
||||
|
||||
## Endpoints API
|
||||
|
||||
@@ -223,39 +480,260 @@ La configuration des sessions inclut :
|
||||
|
||||
#### Création d'utilisateur
|
||||
|
||||
La création d'utilisateur s'adapte aux paramètres de l'entité (amicale) :
|
||||
|
||||
```http
|
||||
POST /api/users
|
||||
Content-Type: application/json
|
||||
Authorization: Bearer {session_id}
|
||||
|
||||
{
|
||||
"name": "John Doe",
|
||||
"email": "john@example.com",
|
||||
"password": "SecurePassword123"
|
||||
"name": "John Doe",
|
||||
"first_name": "John",
|
||||
"role": 1,
|
||||
"fk_entite": 5,
|
||||
"username": "j.doe38", // Requis si chk_username_manuel=1 pour l'entité
|
||||
"password": "SecurePass123", // Requis si chk_mdp_manuel=1 pour l'entité
|
||||
"phone": "0476123456",
|
||||
"mobile": "0612345678",
|
||||
"sect_name": "Secteur A",
|
||||
"date_naissance": "1990-01-15",
|
||||
"date_embauche": "2020-03-01"
|
||||
}
|
||||
```
|
||||
|
||||
**Comportement selon les paramètres de l'entité :**
|
||||
|
||||
| chk_username_manuel | chk_mdp_manuel | Comportement |
|
||||
|---------------------|----------------|--------------|
|
||||
| 0 | 0 | Username et password générés automatiquement |
|
||||
| 0 | 1 | Username généré, password requis dans le payload |
|
||||
| 1 | 0 | Username requis dans le payload, password généré |
|
||||
| 1 | 1 | Username et password requis dans le payload |
|
||||
|
||||
**Validation du username (si manuel) :**
|
||||
- Format : 10-30 caractères
|
||||
- Commence par une lettre
|
||||
- Caractères autorisés : a-z, 0-9, ., -, _
|
||||
- Doit être unique dans toute la base
|
||||
|
||||
**Réponse réussie :**
|
||||
|
||||
```json
|
||||
{
|
||||
"message": "Utilisateur créé",
|
||||
"id": "123"
|
||||
"status": "success",
|
||||
"message": "Utilisateur créé avec succès",
|
||||
"id": 123,
|
||||
"username": "j.doe38", // Toujours retourné
|
||||
"password": "xY7#mK9@pL2" // Retourné seulement si généré automatiquement
|
||||
}
|
||||
```
|
||||
|
||||
**Envoi d'emails :**
|
||||
- **Email 1** : Identifiant de connexion (toujours envoyé)
|
||||
- **Email 2** : Mot de passe (toujours envoyé, 1 seconde après le premier)
|
||||
|
||||
**Codes de statut :**
|
||||
|
||||
- 201: Création réussie
|
||||
- 400: Données invalides
|
||||
- 400: Données invalides ou username/password manquant si requis
|
||||
- 401: Non authentifié
|
||||
- 403: Accès non autorisé (rôle insuffisant)
|
||||
- 409: Email ou username déjà utilisé
|
||||
- 500: Erreur serveur
|
||||
|
||||
#### Vérification de disponibilité du username
|
||||
|
||||
```http
|
||||
POST /api/users/check-username
|
||||
Content-Type: application/json
|
||||
Authorization: Bearer {session_id}
|
||||
|
||||
{
|
||||
"username": "j.doe38"
|
||||
}
|
||||
```
|
||||
|
||||
**Réponse si disponible :**
|
||||
|
||||
```json
|
||||
{
|
||||
"status": "success",
|
||||
"available": true,
|
||||
"message": "Nom d'utilisateur disponible",
|
||||
"username": "j.doe38"
|
||||
}
|
||||
```
|
||||
|
||||
**Réponse si déjà pris :**
|
||||
|
||||
```json
|
||||
{
|
||||
"status": "success",
|
||||
"available": false,
|
||||
"message": "Ce nom d'utilisateur est déjà utilisé",
|
||||
"suggestions": ["j.doe38_42", "j.doe381234", "j.doe3825"]
|
||||
}
|
||||
```
|
||||
|
||||
#### Autres endpoints
|
||||
|
||||
- GET /api/users
|
||||
- GET /api/users/{id}
|
||||
- PUT /api/users/{id}
|
||||
- DELETE /api/users/{id}
|
||||
- POST /api/users/{id}/reset-password
|
||||
|
||||
### Entités (Amicales)
|
||||
|
||||
#### Upload du logo d'une entité
|
||||
|
||||
```http
|
||||
POST /api/entites/{id}/logo
|
||||
Content-Type: multipart/form-data
|
||||
Authorization: Bearer {session_id}
|
||||
|
||||
Body:
|
||||
logo: File (image/png, image/jpeg, image/jpg)
|
||||
```
|
||||
|
||||
**Restrictions :**
|
||||
- Réservé aux administrateurs d'amicale (fk_role == 2)
|
||||
- L'admin ne peut uploader que le logo de sa propre amicale
|
||||
- Un seul logo actif par entité (le nouveau remplace l'ancien)
|
||||
|
||||
**Traitement de l'image :**
|
||||
- Formats acceptés : PNG, JPG, JPEG
|
||||
- Redimensionnement automatique : 250x250px maximum (ratio conservé)
|
||||
- Résolution : 72 DPI (standard web)
|
||||
- Préservation de la transparence pour les PNG
|
||||
|
||||
**Stockage :**
|
||||
- Chemin : `/uploads/entites/{id}/logo/logo_{id}_{timestamp}.{ext}`
|
||||
- Enregistrement dans la table `medias`
|
||||
- Suppression automatique de l'ancien logo
|
||||
|
||||
**Réponse réussie :**
|
||||
|
||||
```json
|
||||
{
|
||||
"status": "success",
|
||||
"message": "Logo uploadé avec succès",
|
||||
"media_id": 42,
|
||||
"file_name": "logo_5_1234567890.jpg",
|
||||
"file_path": "/entites/5/logo/logo_5_1234567890.jpg",
|
||||
"dimensions": {
|
||||
"width": 250,
|
||||
"height": 180
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Récupération du logo d'une entité
|
||||
|
||||
```http
|
||||
GET /api/entites/{id}/logo
|
||||
Authorization: Bearer {session_id}
|
||||
```
|
||||
|
||||
**Réponse :**
|
||||
|
||||
```json
|
||||
{
|
||||
"status": "success",
|
||||
"logo": {
|
||||
"id": 42,
|
||||
"data_url": "data:image/png;base64,iVBORw0KGgoAAAANS...",
|
||||
"file_name": "logo_5_1234567890.png",
|
||||
"mime_type": "image/png",
|
||||
"width": 250,
|
||||
"height": 180,
|
||||
"size": 15234
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Note :** Le logo est également inclus automatiquement dans la réponse du login si disponible.
|
||||
|
||||
#### Mise à jour d'une entité
|
||||
|
||||
```http
|
||||
PUT /api/entites/{id}
|
||||
Content-Type: application/json
|
||||
Authorization: Bearer {session_id}
|
||||
|
||||
{
|
||||
"name": "Amicale de Grenoble",
|
||||
"adresse1": "123 rue de la Caserne",
|
||||
"adresse2": "",
|
||||
"code_postal": "38000",
|
||||
"ville": "Grenoble",
|
||||
"phone": "0476123456",
|
||||
"mobile": "0612345678",
|
||||
"email": "contact@amicale38.fr",
|
||||
"chk_stripe": true, // Activation paiement Stripe
|
||||
"chk_mdp_manuel": false, // Génération auto des mots de passe
|
||||
"chk_username_manuel": false, // Génération auto des usernames
|
||||
"chk_copie_mail_recu": true,
|
||||
"chk_accept_sms": false
|
||||
}
|
||||
```
|
||||
|
||||
**Paramètres de gestion des membres :**
|
||||
|
||||
| Paramètre | Type | Description |
|
||||
|-----------|------|-------------|
|
||||
| chk_mdp_manuel | boolean | `true`: L'admin saisit les mots de passe<br>`false`: Génération automatique |
|
||||
| chk_username_manuel | boolean | `true`: L'admin saisit les identifiants<br>`false`: Génération automatique |
|
||||
| chk_stripe | boolean | Active/désactive les paiements Stripe |
|
||||
|
||||
**Note :** Ces paramètres sont modifiables uniquement par les administrateurs (fk_role > 1).
|
||||
|
||||
#### Réponse du login avec paramètres entité
|
||||
|
||||
Lors du login, les paramètres de l'entité sont retournés dans le groupe `amicale` :
|
||||
|
||||
```json
|
||||
{
|
||||
"status": "success",
|
||||
"session_id": "abc123...",
|
||||
"session_expiry": "2025-01-09T15:30:00+00:00",
|
||||
"user": {
|
||||
"id": 9999980,
|
||||
"fk_entite": 5,
|
||||
"fk_role": 2,
|
||||
"fk_titre": null,
|
||||
"first_name": "Pierre",
|
||||
"sect_name": "",
|
||||
"date_naissance": "1990-01-15", // Maintenant correctement récupéré
|
||||
"date_embauche": "2020-03-01", // Maintenant correctement récupéré
|
||||
"username": "pv_admin",
|
||||
"name": "VALERY ADM",
|
||||
"phone": "0476123456", // Maintenant correctement récupéré
|
||||
"mobile": "0612345678", // Maintenant correctement récupéré
|
||||
"email": "contact@resalice.com"
|
||||
},
|
||||
"amicale": {
|
||||
"id": 5,
|
||||
"name": "Amicale de Grenoble",
|
||||
"chk_mdp_manuel": 0,
|
||||
"chk_username_manuel": 0,
|
||||
"chk_stripe": 1,
|
||||
"logo": { // Logo de l'entité (si disponible)
|
||||
"id": 42,
|
||||
"data_url": "data:image/png;base64,iVBORw0KGgoAAAANS...",
|
||||
"file_name": "logo_5_1234567890.png",
|
||||
"mime_type": "image/png",
|
||||
"width": 250,
|
||||
"height": 180
|
||||
}
|
||||
// ... autres champs
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Ces paramètres permettent à l'application Flutter d'adapter dynamiquement le formulaire de création de membre.
|
||||
|
||||
## Intégration Frontend
|
||||
|
||||
@@ -298,3 +776,88 @@ fetch('/api/endpoint', {
|
||||
- Surveillance de la base de données
|
||||
- Monitoring des performances
|
||||
- Alertes sur erreurs critiques
|
||||
|
||||
## Changements récents
|
||||
|
||||
### Version 3.0.7 (Août 2025)
|
||||
|
||||
#### 1. Implémentation complète de la norme NIST SP 800-63B pour les mots de passe
|
||||
- **Nouveau service :** `PasswordSecurityService` pour la gestion sécurisée des mots de passe
|
||||
- **Vérification HIBP :** Intégration de l'API Have I Been Pwned avec k-anonymity
|
||||
- **Validation souple :** Suppression des obligations de composition (majuscules, chiffres, spéciaux)
|
||||
- **Support Unicode :** Acceptation de tous les caractères, incluant émojis et espaces
|
||||
- **Nouveaux endpoints :** `/api/password/check`, `/api/password/compromised`, `/api/password/generate`
|
||||
|
||||
#### 2. Autorisation des emails multiples
|
||||
- **Suppression de l'unicité :** Un même email peut être utilisé pour plusieurs comptes
|
||||
- **Adaptation de `lostPassword` :** Mise à jour de tous les comptes partageant l'email
|
||||
- **Un seul mot de passe :** Tous les comptes avec le même email reçoivent le même nouveau mot de passe
|
||||
|
||||
#### 3. Autorisation mot de passe = identifiant
|
||||
- **Choix client :** Permet d'avoir un mot de passe identique au nom d'utilisateur
|
||||
- **Pas de vérification contextuelle :** Aucune vérification nom/email dans le mot de passe
|
||||
|
||||
### Version 3.0.6 (Août 2025)
|
||||
|
||||
#### 1. Correction des rôles administrateurs
|
||||
- **Avant :** Les administrateurs d'amicale devaient avoir `fk_role > 2`
|
||||
- **Après :** Les administrateurs d'amicale ont `fk_role > 1` (donc rôle 2 et plus)
|
||||
- **Impact :** Les champs `chk_stripe`, `chk_mdp_manuel`, `chk_username_manuel` sont maintenant modifiables par les admins d'amicale (rôle 2)
|
||||
|
||||
#### 2. Envoi systématique des deux emails lors de la création d'utilisateur
|
||||
- **Avant :** Le 2ème email (mot de passe) n'était envoyé que si le mot de passe était généré automatiquement
|
||||
- **Après :** Les deux emails sont toujours envoyés lors de la création d'un membre
|
||||
- Email 1 : Identifiant (username)
|
||||
- Email 2 : Mot de passe (1 seconde après)
|
||||
- **Raison :** Le nouveau membre a toujours besoin des deux informations pour se connecter
|
||||
|
||||
#### 3. Ajout des champs manquants dans la réponse du login
|
||||
- **Champs ajoutés dans la requête SQL :**
|
||||
- `fk_titre`
|
||||
- `date_naissance`
|
||||
- `date_embauche`
|
||||
- `encrypted_phone`
|
||||
- `encrypted_mobile`
|
||||
- **Impact :** Ces données sont maintenant correctement retournées dans l'objet `user` lors du login
|
||||
|
||||
#### 4. Système de gestion des logos d'entité
|
||||
- **Nouvelle fonctionnalité :** Upload et gestion des logos pour les amicales
|
||||
- **Routes ajoutées :**
|
||||
- `POST /api/entites/{id}/logo` : Upload d'un nouveau logo
|
||||
- `GET /api/entites/{id}/logo` : Récupération du logo
|
||||
- **Caractéristiques :**
|
||||
- Réservé aux administrateurs d'amicale (fk_role == 2)
|
||||
- Un seul logo actif par entité
|
||||
- Redimensionnement automatique (250x250px max)
|
||||
- Format base64 dans les réponses JSON (compatible Flutter)
|
||||
- Logo inclus automatiquement dans la réponse du login
|
||||
|
||||
#### 5. Amélioration de l'intégration Flutter
|
||||
- **Format d'envoi des images :** Base64 data URL pour compatibilité multiplateforme
|
||||
- **Structure de réponse enrichie :** Le logo est inclus dans l'objet `amicale` lors du login
|
||||
- **Optimisation :** Pas de requête HTTP supplémentaire nécessaire pour afficher le logo
|
||||
|
||||
### Version 3.0.8 (Janvier 2025)
|
||||
|
||||
#### 1. Système de génération automatique de reçus fiscaux pour les dons
|
||||
- **Nouveau service :** `ReceiptService` pour la génération automatique de reçus PDF
|
||||
- **Déclencheurs automatiques :**
|
||||
- Création d'un passage avec `fk_type=1` (don) et email valide
|
||||
- Mise à jour d'un passage en don si `nom_recu` est vide/null
|
||||
- **Caractéristiques techniques :**
|
||||
- PDF ultra-légers (< 5KB) générés en format natif sans librairie externe
|
||||
- Support des caractères accentués avec conversion automatique
|
||||
- Stockage structuré : `/uploads/entites/{entite_id}/recus/{operation_id}/`
|
||||
- Enregistrement dans la table `medias` avec catégorie `recu`
|
||||
- **Queue d'envoi email :**
|
||||
- Envoi automatique par email avec pièce jointe PDF
|
||||
- Format MIME multipart pour compatibilité maximale
|
||||
- Gestion dans la table `email_queue` avec statut de suivi
|
||||
- **Nouvelle route API :**
|
||||
- `GET /api/passages/{id}/receipt` : Récupération du PDF d'un reçu
|
||||
- Retourne le PDF en base64 ou téléchargement direct selon Accept header
|
||||
- **Champs base de données utilisés :**
|
||||
- `nom_recu` : Nom du fichier PDF généré
|
||||
- `date_creat_recu` : Date de génération du reçu
|
||||
- `date_sent_recu` : Date d'envoi par email
|
||||
- `chk_email_sent` : Indicateur d'envoi réussi
|
||||
|
||||
155
api/docs/UPLOAD-MIGRATION-RECAP.md
Normal file
155
api/docs/UPLOAD-MIGRATION-RECAP.md
Normal file
@@ -0,0 +1,155 @@
|
||||
# 📋 RÉCAPITULATIF - Migration Arborescence Uploads
|
||||
|
||||
## ✅ Modifications effectuées
|
||||
|
||||
### 1. **EntiteController.php** (ligne 736)
|
||||
```php
|
||||
// Avant : "/entites/{$entiteId}/logo"
|
||||
// Après : "/{$entiteId}/logo"
|
||||
```
|
||||
✅ Les logos sont maintenant stockés dans : `uploads/{entite_id}/logo/`
|
||||
|
||||
### 2. **ReceiptService.php** (ligne 95)
|
||||
```php
|
||||
// Avant : "/entites/{$entiteId}/recus/{$operationId}"
|
||||
// Après : "/{$entiteId}/recus/{$operationId}"
|
||||
```
|
||||
✅ Les reçus PDF sont maintenant stockés dans : `uploads/{entite_id}/recus/{operation_id}/`
|
||||
|
||||
### 3. **ExportService.php** (lignes 40 et 141)
|
||||
```php
|
||||
// Avant Excel : "/{$entiteId}/operations/{$operationId}/exports/excel"
|
||||
// Après Excel : "/{$entiteId}/operations/{$operationId}"
|
||||
|
||||
// Avant JSON : "/{$entiteId}/operations/{$operationId}/exports/json"
|
||||
// Après JSON : "/{$entiteId}/operations/{$operationId}"
|
||||
```
|
||||
✅ Les exports sont maintenant stockés directement dans : `uploads/{entite_id}/operations/{operation_id}/`
|
||||
|
||||
## 📂 Nouvelle structure complète
|
||||
|
||||
```
|
||||
uploads/
|
||||
└── {entite_id}/ # Ex: 5, 1230, etc.
|
||||
├── logo/ # Logo de l'entité
|
||||
│ └── logo_{entite_id}_{timestamp}.{jpg|png}
|
||||
├── operations/ # Exports d'opérations
|
||||
│ └── {operation_id}/ # Ex: 1525, 3124
|
||||
│ ├── geosector-export-{operation_id}-{timestamp}.xlsx
|
||||
│ └── backup-{operation_id}-{timestamp}.json.enc
|
||||
└── recus/ # Reçus fiscaux
|
||||
└── {operation_id}/ # Ex: 3124
|
||||
└── recu_{passage_id}.pdf
|
||||
```
|
||||
|
||||
## 🔧 Script de migration
|
||||
|
||||
Un script a été créé pour migrer les fichiers existants :
|
||||
|
||||
**Fichier :** `/scripts/migrate_uploads_structure.php`
|
||||
|
||||
**Usage :**
|
||||
```bash
|
||||
# Mode simulation (voir ce qui sera fait sans modifier)
|
||||
php scripts/migrate_uploads_structure.php --dry-run
|
||||
|
||||
# Mode réel (effectue la migration)
|
||||
php scripts/migrate_uploads_structure.php
|
||||
```
|
||||
|
||||
**Ce que fait le script :**
|
||||
1. Déplace tout le contenu de `uploads/entites/*` vers `uploads/*`
|
||||
2. Fusionne les dossiers si nécessaire
|
||||
3. Simplifie la structure des exports (supprime `/documents/exports/excel/`)
|
||||
4. Applique les bonnes permissions (nginx:nobody 775/664)
|
||||
5. Crée un log détaillé dans `/logs/migration_uploads_YYYYMMDD_HHMMSS.log`
|
||||
|
||||
## 🚀 Procédure de déploiement
|
||||
|
||||
### Sur DEV (déjà fait)
|
||||
✅ Code modifié
|
||||
✅ Script de migration créé
|
||||
|
||||
### Sur REC
|
||||
```bash
|
||||
# 1. Déployer le nouveau code
|
||||
./livre-api.sh rec
|
||||
|
||||
# 2. Faire un backup des uploads actuels
|
||||
cd /var/www/geosector/api
|
||||
tar -czf uploads_backup_$(date +%Y%m%d).tar.gz uploads/
|
||||
|
||||
# 3. Tester en mode dry-run
|
||||
php scripts/migrate_uploads_structure.php --dry-run
|
||||
|
||||
# 4. Si OK, lancer la migration
|
||||
php scripts/migrate_uploads_structure.php
|
||||
|
||||
# 5. Vérifier la nouvelle structure
|
||||
ls -la uploads/
|
||||
ls -la uploads/*/
|
||||
```
|
||||
|
||||
### Sur PROD
|
||||
Même procédure que REC après validation
|
||||
|
||||
## ⚠️ Points d'attention
|
||||
|
||||
1. **Backup obligatoire** avant migration
|
||||
2. **Vérifier l'espace disque** disponible
|
||||
3. **Tester d'abord en dry-run**
|
||||
4. **Surveiller les logs** après migration
|
||||
5. **Tester** upload logo, génération reçu, et export Excel
|
||||
|
||||
## 📊 Gains obtenus
|
||||
|
||||
| Aspect | Avant | Après |
|
||||
|--------|-------|-------|
|
||||
| **Profondeur max** | 8 niveaux | 4 niveaux |
|
||||
| **Complexité** | 2 structures parallèles | 1 structure unique |
|
||||
| **Clarté** | Confus (entites + racine) | Simple et logique |
|
||||
| **Navigation** | Difficile | Intuitive |
|
||||
|
||||
## 🔍 Vérification post-migration
|
||||
|
||||
Après la migration, vérifier :
|
||||
|
||||
```bash
|
||||
# Structure attendue pour l'entité 5
|
||||
tree uploads/5/
|
||||
# Devrait afficher :
|
||||
# uploads/5/
|
||||
# ├── logo/
|
||||
# │ └── logo_5_*.png
|
||||
# ├── operations/
|
||||
# │ ├── 1525/
|
||||
# │ │ └── *.xlsx
|
||||
# │ └── 3124/
|
||||
# │ └── *.xlsx
|
||||
# └── recus/
|
||||
# └── 3124/
|
||||
# └── recu_*.pdf
|
||||
|
||||
# Vérifier les permissions
|
||||
ls -la uploads/*/
|
||||
# Devrait montrer : nginx:nobody avec 775 pour dossiers, 664 pour fichiers
|
||||
```
|
||||
|
||||
## ✅ Checklist finale
|
||||
|
||||
- [ ] Code modifié et testé en DEV
|
||||
- [ ] Script de migration créé
|
||||
- [ ] Documentation mise à jour
|
||||
- [ ] Backup effectué sur REC
|
||||
- [ ] Migration testée en dry-run sur REC
|
||||
- [ ] Migration exécutée sur REC
|
||||
- [ ] Tests fonctionnels sur REC
|
||||
- [ ] Backup effectué sur PROD
|
||||
- [ ] Migration exécutée sur PROD
|
||||
- [ ] Tests fonctionnels sur PROD
|
||||
|
||||
---
|
||||
|
||||
**Date de création :** 20/08/2025
|
||||
**Auteur :** Assistant Claude
|
||||
**Status :** Prêt pour déploiement
|
||||
93
api/docs/UPLOAD-REORGANIZATION.md
Normal file
93
api/docs/UPLOAD-REORGANIZATION.md
Normal file
@@ -0,0 +1,93 @@
|
||||
# Réorganisation de l'arborescence des uploads
|
||||
|
||||
## 📅 Date : 20/08/2025
|
||||
|
||||
## 🎯 Objectif
|
||||
Uniformiser et simplifier l'arborescence des fichiers uploads pour une meilleure organisation et maintenance.
|
||||
|
||||
## 📂 Arborescence actuelle (PROBLÈME)
|
||||
```
|
||||
uploads/
|
||||
├── entites/
|
||||
│ └── 5/
|
||||
│ ├── logo/
|
||||
│ ├── operations/
|
||||
│ │ └── 1525/
|
||||
│ │ └── documents/
|
||||
│ │ └── exports/
|
||||
│ │ └── excel/
|
||||
│ │ └── geosector-export-*.xlsx
|
||||
│ └── recus/
|
||||
│ └── 3124/
|
||||
│ └── recu_*.pdf
|
||||
└── 5/
|
||||
└── operations/
|
||||
├── 1525/
|
||||
└── 2021/
|
||||
```
|
||||
|
||||
**Problèmes identifiés :**
|
||||
- Duplication des structures (dossier `5` à la racine ET dans `entites/`)
|
||||
- Chemins trop profonds pour les exports Excel (6 niveaux)
|
||||
- Incohérence dans les chemins
|
||||
|
||||
## ✅ Nouvelle arborescence (SOLUTION)
|
||||
```
|
||||
uploads/
|
||||
└── {entite_id}/ # Un seul dossier par entité à la racine
|
||||
├── logo/ # Logo de l'entité
|
||||
│ └── logo_*.{jpg,png}
|
||||
├── operations/ # Exports par opération
|
||||
│ └── {operation_id}/
|
||||
│ └── *.xlsx # Exports Excel directement ici
|
||||
└── recus/ # Reçus par opération
|
||||
└── {operation_id}/
|
||||
└── recu_*.pdf
|
||||
```
|
||||
|
||||
## 📝 Fichiers à modifier
|
||||
|
||||
### 1. EntiteController.php (Upload logo)
|
||||
**Actuel :** `/entites/{$entiteId}/logo`
|
||||
**Nouveau :** `/{$entiteId}/logo`
|
||||
|
||||
### 2. ReceiptService.php (Stockage reçus PDF)
|
||||
**Actuel :** `/entites/{$entiteId}/recus/{$operationId}`
|
||||
**Nouveau :** `/{$entiteId}/recus/{$operationId}`
|
||||
|
||||
### 3. ExportService.php (Export Excel)
|
||||
**Actuel :** `/{$entiteId}/operations/{$operationId}/exports/excel`
|
||||
**Nouveau :** `/{$entiteId}/operations/{$operationId}`
|
||||
|
||||
### 4. ExportService.php (Export JSON)
|
||||
**Actuel :** `/{$entiteId}/operations/{$operationId}/exports/json`
|
||||
**Nouveau :** `/{$entiteId}/operations/{$operationId}` (ou supprimer si non utilisé)
|
||||
|
||||
## 🔄 Plan de migration
|
||||
|
||||
### Étape 1 : Modifier le code
|
||||
1. Mettre à jour tous les chemins dans les contrôleurs et services
|
||||
2. Tester en environnement DEV
|
||||
|
||||
### Étape 2 : Script de migration des fichiers existants
|
||||
Créer un script PHP pour :
|
||||
1. Lister tous les fichiers existants
|
||||
2. Les déplacer vers la nouvelle structure
|
||||
3. Supprimer les anciens dossiers vides
|
||||
|
||||
### Étape 3 : Déploiement
|
||||
1. Exécuter le script de migration sur REC
|
||||
2. Vérifier le bon fonctionnement
|
||||
3. Exécuter sur PROD
|
||||
|
||||
## 🚀 Avantages de la nouvelle structure
|
||||
- **Plus simple** : Chemins plus courts et plus logiques
|
||||
- **Plus cohérent** : Une seule structure pour toutes les entités
|
||||
- **Plus maintenable** : Facile de naviguer et comprendre
|
||||
- **Performance** : Moins de niveaux de dossiers à parcourir
|
||||
|
||||
## ⚠️ Points d'attention
|
||||
- Vérifier les permissions (nginx:nobody 775/664)
|
||||
- S'assurer que les anciens fichiers sont bien migrés
|
||||
- Mettre à jour la documentation
|
||||
- Informer l'équipe du changement
|
||||
135
api/docs/USERNAME_VALIDATION_CHANGES.md
Normal file
135
api/docs/USERNAME_VALIDATION_CHANGES.md
Normal file
@@ -0,0 +1,135 @@
|
||||
# Changements de validation des usernames - Version ultra-souple
|
||||
|
||||
## Date : 17 janvier 2025
|
||||
|
||||
## Contexte
|
||||
Suite aux problèmes d'erreurs 400 et au besoin d'avoir une approche plus moderne et inclusive, les règles de validation des usernames ont été assouplies pour accepter tous les caractères UTF-8, similaire à l'approche NIST pour les mots de passe.
|
||||
|
||||
## Anciennes règles (trop restrictives)
|
||||
- ❌ 10-30 caractères
|
||||
- ❌ Doit commencer par une lettre minuscule
|
||||
- ❌ Seulement : a-z, 0-9, ., -, _
|
||||
- ❌ Pas d'espaces
|
||||
- ❌ Pas de majuscules
|
||||
- ❌ Pas d'accents ou caractères spéciaux
|
||||
|
||||
## Nouvelles règles (ultra-souples)
|
||||
- ✅ **8-30 caractères UTF-8**
|
||||
- ✅ **Tous caractères acceptés** :
|
||||
- Lettres (majuscules/minuscules)
|
||||
- Chiffres
|
||||
- Espaces
|
||||
- Caractères spéciaux (!@#$%^&*()_+-=[]{}|;:'"<>,.?/)
|
||||
- Accents (é, è, à, ñ, ü, etc.)
|
||||
- Émojis (😀, 🎉, ❤️, etc.)
|
||||
- Caractères non-latins (中文, العربية, Русский, etc.)
|
||||
- ✅ **Sensible à la casse** (Jean ≠ jean)
|
||||
- ✅ **Trim automatique** des espaces début/fin
|
||||
- ✅ **Unicité vérifiée** dans toute la base
|
||||
|
||||
## Exemples de usernames valides
|
||||
|
||||
### Noms classiques
|
||||
- `Jean-Pierre`
|
||||
- `Marie Claire` (avec espace)
|
||||
- `O'Connor`
|
||||
- `José García`
|
||||
|
||||
### Avec chiffres et caractères spéciaux
|
||||
- `admin2024`
|
||||
- `user@company`
|
||||
- `test_user#1`
|
||||
- `Marie*123!`
|
||||
|
||||
### International
|
||||
- `李明` (chinois)
|
||||
- `محمد` (arabe)
|
||||
- `Владимир` (russe)
|
||||
- `さくら` (japonais)
|
||||
- `Παύλος` (grec)
|
||||
|
||||
### Modernes/Fun
|
||||
- `🦄Unicorn`
|
||||
- `Player_1 🎮`
|
||||
- `☕Coffee.Lover`
|
||||
- `2024_User`
|
||||
|
||||
## Exemples de usernames invalides
|
||||
- `short` ❌ (moins de 8 caractères)
|
||||
- ` ` ❌ (espaces seulement)
|
||||
- `very_long_username_that_exceeds_thirty_chars` ❌ (plus de 30 caractères)
|
||||
|
||||
## Modifications techniques
|
||||
|
||||
### 1. Code PHP (UserController.php)
|
||||
```php
|
||||
// Avant (restrictif)
|
||||
if (!preg_match('/^[a-z][a-z0-9._-]{9,29}$/', $username))
|
||||
|
||||
// Après (ultra-souple)
|
||||
$username = trim($data['username']);
|
||||
$usernameLength = mb_strlen($username, 'UTF-8');
|
||||
|
||||
if ($usernameLength < 8) {
|
||||
// Erreur : trop court
|
||||
}
|
||||
if ($usernameLength > 30) {
|
||||
// Erreur : trop long
|
||||
}
|
||||
// C'est tout ! Pas d'autre validation
|
||||
```
|
||||
|
||||
### 2. Base de données
|
||||
```sql
|
||||
-- Script à exécuter : scripts/sql/migration_username_utf8_support.sql
|
||||
ALTER TABLE `users`
|
||||
MODIFY COLUMN `encrypted_user_name` varchar(255) DEFAULT '';
|
||||
```
|
||||
|
||||
### 3. Messages d'erreur simplifiés
|
||||
- Avant : "Format du nom d'utilisateur invalide (10-30 caractères, commence par une lettre, caractères autorisés: a-z, 0-9, ., -, _)"
|
||||
- Après :
|
||||
- "Identifiant trop court" + "Minimum 8 caractères"
|
||||
- "Identifiant trop long" + "Maximum 30 caractères"
|
||||
- "Identifiant déjà utilisé"
|
||||
|
||||
## Impact sur l'expérience utilisateur
|
||||
|
||||
### Avantages
|
||||
1. **Inclusivité** : Support de toutes les langues et cultures
|
||||
2. **Modernité** : Permet les émojis et caractères spéciaux
|
||||
3. **Simplicité** : Règles faciles à comprendre (juste la longueur)
|
||||
4. **Flexibilité** : Les utilisateurs peuvent choisir l'identifiant qu'ils veulent
|
||||
5. **Moins d'erreurs** : Moins de rejets pour format invalide
|
||||
|
||||
### Points d'attention
|
||||
1. **Support client** : Former le support aux nouveaux formats possibles
|
||||
2. **Affichage** : S'assurer que l'UI supporte bien l'UTF-8
|
||||
3. **Recherche** : La recherche d'utilisateurs doit gérer la casse et l'UTF-8
|
||||
4. **Export** : Vérifier que les exports CSV/Excel gèrent bien l'UTF-8
|
||||
|
||||
## Sécurité
|
||||
|
||||
### Pas d'impact sur la sécurité
|
||||
- ✅ Les usernames sont toujours chiffrés en base (AES-256-CBC)
|
||||
- ✅ L'unicité est toujours vérifiée
|
||||
- ✅ Les injections SQL sont impossibles (prepared statements)
|
||||
- ✅ Le trim empêche les espaces invisibles
|
||||
|
||||
### Recommandations
|
||||
- Continuer à générer automatiquement des usernames simples (ASCII) pour éviter les problèmes
|
||||
- Mais permettre la saisie manuelle de tout format
|
||||
- Logger les usernames "exotiques" pour détecter d'éventuels abus
|
||||
|
||||
## Tests
|
||||
- Script de test disponible : `/tests/test_username_validation.php`
|
||||
- Teste tous les cas limites et formats internationaux
|
||||
|
||||
## Rollback si nécessaire
|
||||
Si besoin de revenir en arrière :
|
||||
1. Restaurer l'ancienne validation dans UserController
|
||||
2. Les usernames UTF-8 existants continueront de fonctionner
|
||||
3. Seuls les nouveaux seront restreints
|
||||
|
||||
## Conclusion
|
||||
Cette approche ultra-souple aligne les usernames sur les standards modernes d'inclusivité et d'accessibilité, tout en maintenant la sécurité grâce au chiffrement et à la validation de l'unicité.
|
||||
BIN
api/docs/_logo_recu.png
Normal file
BIN
api/docs/_logo_recu.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 99 KiB |
BIN
api/docs/_recu_template.pdf
Normal file
BIN
api/docs/_recu_template.pdf
Normal file
Binary file not shown.
0
api/docs/api-analysis.md
Normal file → Executable file
0
api/docs/api-analysis.md
Normal file → Executable file
1
api/docs/contour-des-departements.geojson
Normal file
1
api/docs/contour-des-departements.geojson
Normal file
File diff suppressed because one or more lines are too long
53
api/docs/departements_limitrophes.md
Normal file
53
api/docs/departements_limitrophes.md
Normal file
@@ -0,0 +1,53 @@
|
||||
# Départements limitrophes
|
||||
|
||||
Ce document liste les départements limitrophes pour chaque département français.
|
||||
À utiliser pour remplir le champ `dept_limitrophes` dans la table `x_departements`.
|
||||
|
||||
## Format
|
||||
Le champ `dept_limitrophes` contient une liste de codes départements séparés par des virgules.
|
||||
Exemple : "22,35,56" pour un département limitrophe avec les Côtes-d'Armor (22), l'Ille-et-Vilaine (35) et le Morbihan (56).
|
||||
|
||||
## Liste par département
|
||||
|
||||
### Bretagne
|
||||
- **22 - Côtes-d'Armor** : 29,35,56
|
||||
- **29 - Finistère** : 22,56
|
||||
- **35 - Ille-et-Vilaine** : 22,44,49,50,53,56
|
||||
- **56 - Morbihan** : 22,29,35,44
|
||||
|
||||
### Pays de la Loire
|
||||
- **44 - Loire-Atlantique** : 35,49,56,85
|
||||
- **49 - Maine-et-Loire** : 35,37,44,53,72,79,85,86
|
||||
- **53 - Mayenne** : 14,35,49,50,61,72
|
||||
- **72 - Sarthe** : 14,27,28,37,41,49,53,61
|
||||
- **85 - Vendée** : 17,44,49,79
|
||||
|
||||
### Normandie
|
||||
- **14 - Calvados** : 27,50,53,61,72
|
||||
- **27 - Eure** : 14,28,60,61,72,76,78,95
|
||||
- **50 - Manche** : 14,35,53,61
|
||||
- **61 - Orne** : 14,27,28,35,41,50,53,72
|
||||
- **76 - Seine-Maritime** : 27,60,80
|
||||
|
||||
### Île-de-France
|
||||
- **75 - Paris** : 92,93,94
|
||||
- **77 - Seine-et-Marne** : 02,10,45,51,60,89,91,93,94,95
|
||||
- **78 - Yvelines** : 27,28,91,92,95
|
||||
- **91 - Essonne** : 28,45,77,78,92,94
|
||||
- **92 - Hauts-de-Seine** : 75,78,91,93,94,95
|
||||
- **93 - Seine-Saint-Denis** : 75,77,92,94,95
|
||||
- **94 - Val-de-Marne** : 75,77,91,92,93
|
||||
- **95 - Val-d'Oise** : 27,60,77,78,92,93
|
||||
|
||||
### Hauts-de-France
|
||||
- **02 - Aisne** : 08,51,59,60,77,80
|
||||
- **59 - Nord** : 02,62,80 (+ frontière Belgique)
|
||||
- **60 - Oise** : 02,27,76,77,80,95
|
||||
- **62 - Pas-de-Calais** : 59,80 (+ frontière Belgique et côte Manche)
|
||||
- **80 - Somme** : 02,27,59,60,62,76
|
||||
|
||||
## Notes
|
||||
- Cette liste est à compléter pour tous les départements français
|
||||
- Les départements d'outre-mer n'ont généralement pas de départements limitrophes terrestres
|
||||
- Certains départements peuvent avoir des limites maritimes non représentées ici
|
||||
- Source recommandée : données INSEE ou IGN pour une liste complète et exacte
|
||||
0
api/docs/flowIncus.md
Normal file → Executable file
0
api/docs/flowIncus.md
Normal file → Executable file
474
api/docs/geo_app.sql
Executable file
474
api/docs/geo_app.sql
Executable file
@@ -0,0 +1,474 @@
|
||||
-- -------------------------------------------------------------
|
||||
-- TablePlus 6.4.8(608)
|
||||
--
|
||||
-- https://tableplus.com/
|
||||
--
|
||||
-- Database: geo_app
|
||||
-- Generation Time: 2025-06-09 18:03:43.5140
|
||||
-- -------------------------------------------------------------
|
||||
|
||||
|
||||
/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */;
|
||||
/*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */;
|
||||
/*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */;
|
||||
/*!40101 SET NAMES utf8mb4 */;
|
||||
/*!40014 SET @OLD_UNIQUE_CHECKS=@@UNIQUE_CHECKS, UNIQUE_CHECKS=0 */;
|
||||
/*!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 */;
|
||||
/*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO' */;
|
||||
/*!40111 SET @OLD_SQL_NOTES=@@SQL_NOTES, SQL_NOTES=0 */;
|
||||
|
||||
|
||||
-- Tables préfixées "chat_"
|
||||
CREATE TABLE chat_rooms (
|
||||
id VARCHAR(36) PRIMARY KEY,
|
||||
title VARCHAR(255),
|
||||
type ENUM('private', 'group', 'broadcast'),
|
||||
created_at TIMESTAMP,
|
||||
created_by INT
|
||||
);
|
||||
|
||||
CREATE TABLE chat_messages (
|
||||
id VARCHAR(36) PRIMARY KEY,
|
||||
room_id VARCHAR(36),
|
||||
content TEXT,
|
||||
sender_id INT,
|
||||
sent_at TIMESTAMP,
|
||||
FOREIGN KEY (room_id) REFERENCES chat_rooms(id)
|
||||
);
|
||||
|
||||
CREATE TABLE chat_participants (
|
||||
room_id VARCHAR(36),
|
||||
user_id INT,
|
||||
role INT,
|
||||
entite_id INT,
|
||||
joined_at TIMESTAMP,
|
||||
PRIMARY KEY (room_id, user_id)
|
||||
);
|
||||
|
||||
CREATE TABLE chat_read_receipts (
|
||||
message_id VARCHAR(36),
|
||||
user_id INT,
|
||||
read_at TIMESTAMP,
|
||||
PRIMARY KEY (message_id, user_id)
|
||||
);
|
||||
|
||||
CREATE TABLE `email_counter` (
|
||||
`id` int(10) unsigned NOT NULL DEFAULT 1,
|
||||
`hour_start` timestamp NULL DEFAULT NULL,
|
||||
`count` int(10) unsigned DEFAULT 0,
|
||||
PRIMARY KEY (`id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci `PAGE_COMPRESSED`='ON';
|
||||
|
||||
CREATE TABLE `email_queue` (
|
||||
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
|
||||
`fk_pass` int(10) unsigned NOT NULL DEFAULT 0,
|
||||
`to_email` varchar(255) DEFAULT NULL,
|
||||
`subject` varchar(255) DEFAULT NULL,
|
||||
`body` text DEFAULT NULL,
|
||||
`headers` text DEFAULT NULL,
|
||||
`created_at` timestamp NULL DEFAULT current_timestamp(),
|
||||
`status` enum('pending','sent','failed') DEFAULT 'pending',
|
||||
`attempts` int(10) unsigned DEFAULT 0,
|
||||
PRIMARY KEY (`id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci `PAGE_COMPRESSED`='ON';
|
||||
|
||||
CREATE TABLE `entites` (
|
||||
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
|
||||
`encrypted_name` varchar(255) DEFAULT NULL,
|
||||
`adresse1` varchar(45) DEFAULT '',
|
||||
`adresse2` varchar(45) DEFAULT '',
|
||||
`code_postal` varchar(5) DEFAULT '',
|
||||
`ville` varchar(45) DEFAULT '',
|
||||
`fk_region` int(10) unsigned DEFAULT NULL,
|
||||
`fk_type` int(10) unsigned DEFAULT 1,
|
||||
`encrypted_phone` varchar(128) DEFAULT '',
|
||||
`encrypted_mobile` varchar(128) DEFAULT '',
|
||||
`encrypted_email` varchar(255) DEFAULT '',
|
||||
`gps_lat` varchar(20) NOT NULL DEFAULT '',
|
||||
`gps_lng` varchar(20) NOT NULL DEFAULT '',
|
||||
`chk_stripe` tinyint(1) unsigned DEFAULT 0,
|
||||
`encrypted_stripe_id` varchar(255) DEFAULT '',
|
||||
`encrypted_iban` varchar(255) DEFAULT '',
|
||||
`encrypted_bic` varchar(128) DEFAULT '',
|
||||
`chk_demo` tinyint(1) unsigned DEFAULT 1,
|
||||
`chk_mdp_manuel` tinyint(1) unsigned NOT NULL DEFAULT 0 COMMENT 'Gestion des mots de passe manuelle (1) ou automatique (0)',
|
||||
`chk_username_manuel` tinyint(1) unsigned NOT NULL DEFAULT 0 COMMENT 'Gestion des usernames manuelle (1) ou automatique (0)',
|
||||
`chk_copie_mail_recu` tinyint(1) unsigned NOT NULL DEFAULT 0,
|
||||
`chk_accept_sms` tinyint(1) unsigned NOT NULL DEFAULT 0,
|
||||
`created_at` timestamp NOT NULL DEFAULT current_timestamp() COMMENT 'Date de création',
|
||||
`fk_user_creat` int(10) unsigned DEFAULT NULL,
|
||||
`updated_at` timestamp NULL DEFAULT NULL ON UPDATE current_timestamp() COMMENT 'Date de modification',
|
||||
`fk_user_modif` int(10) unsigned DEFAULT NULL,
|
||||
`chk_active` tinyint(1) unsigned DEFAULT 1,
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `entites_ibfk_1` (`fk_region`),
|
||||
KEY `entites_ibfk_2` (`fk_type`),
|
||||
CONSTRAINT `entites_ibfk_1` FOREIGN KEY (`fk_region`) REFERENCES `x_regions` (`id`) ON UPDATE CASCADE,
|
||||
CONSTRAINT `entites_ibfk_2` FOREIGN KEY (`fk_type`) REFERENCES `x_entites_types` (`id`) ON UPDATE CASCADE
|
||||
) ENGINE=InnoDB AUTO_INCREMENT=1230 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci `PAGE_COMPRESSED`='ON';
|
||||
|
||||
CREATE TABLE `medias` (
|
||||
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
|
||||
`support` varchar(45) NOT NULL DEFAULT '' COMMENT 'Type de support (entite, user, operation, passage)',
|
||||
`support_id` int(10) unsigned NOT NULL DEFAULT 0 COMMENT 'ID de élément associé',
|
||||
`fichier` varchar(250) NOT NULL DEFAULT '' COMMENT 'Nom du fichier stocké',
|
||||
`file_type` varchar(50) DEFAULT NULL COMMENT 'Extension du fichier (pdf, jpg, xlsx, etc.)',
|
||||
`file_category` varchar(50) DEFAULT NULL COMMENT 'export, logo, carte, etc.',
|
||||
`file_size` int(10) unsigned DEFAULT NULL COMMENT 'Taille du fichier en octets',
|
||||
`mime_type` varchar(100) DEFAULT NULL COMMENT 'Type MIME du fichier',
|
||||
`original_name` varchar(255) DEFAULT NULL COMMENT 'Nom original du fichier uploadé',
|
||||
`fk_entite` int(10) unsigned DEFAULT NULL COMMENT 'ID de entité propriétaire',
|
||||
`fk_operation` int(10) unsigned DEFAULT NULL COMMENT 'ID de opération (pour passages)',
|
||||
`file_path` varchar(500) DEFAULT NULL COMMENT 'Chemin complet du fichier',
|
||||
`original_width` int(10) unsigned DEFAULT NULL COMMENT 'Largeur originale de image',
|
||||
`original_height` int(10) unsigned DEFAULT NULL COMMENT 'Hauteur originale de image',
|
||||
`processed_width` int(10) unsigned DEFAULT NULL COMMENT 'Largeur après traitement',
|
||||
`processed_height` int(10) unsigned DEFAULT NULL COMMENT 'Hauteur après traitement',
|
||||
`is_processed` tinyint(1) unsigned DEFAULT 0 COMMENT 'Image redimensionnée (1) ou originale (0)',
|
||||
`description` varchar(100) NOT NULL DEFAULT '' COMMENT 'Description du fichier',
|
||||
`created_at` timestamp NOT NULL DEFAULT current_timestamp(),
|
||||
`fk_user_creat` int(10) unsigned NOT NULL DEFAULT 0,
|
||||
`updated_at` timestamp NULL DEFAULT NULL ON UPDATE current_timestamp(),
|
||||
`fk_user_modif` int(10) unsigned NOT NULL DEFAULT 0,
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `id_UNIQUE` (`id`),
|
||||
KEY `idx_entite` (`fk_entite`),
|
||||
KEY `idx_operation` (`fk_operation`),
|
||||
KEY `idx_support_type` (`support`, `support_id`),
|
||||
KEY `idx_file_type` (`file_type`),
|
||||
KEY `idx_file_category` (`file_category`),
|
||||
CONSTRAINT `fk_medias_entite` FOREIGN KEY (`fk_entite`) REFERENCES `entites` (`id`) ON UPDATE CASCADE ON DELETE CASCADE,
|
||||
CONSTRAINT `fk_medias_operation` FOREIGN KEY (`fk_operation`) REFERENCES `operations` (`id`) ON UPDATE CASCADE ON DELETE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
CREATE TABLE `ope_pass` (
|
||||
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
|
||||
`fk_operation` int(10) unsigned NOT NULL DEFAULT 0,
|
||||
`fk_sector` int(10) unsigned DEFAULT 0,
|
||||
`fk_user` int(10) unsigned NOT NULL DEFAULT 0,
|
||||
`fk_adresse` varchar(25) DEFAULT '' COMMENT 'adresses.cp??.id',
|
||||
`passed_at` timestamp NULL DEFAULT NULL COMMENT 'Date du passage',
|
||||
`fk_type` int(10) unsigned DEFAULT 0,
|
||||
`numero` varchar(10) NOT NULL DEFAULT '',
|
||||
`rue` varchar(75) NOT NULL DEFAULT '',
|
||||
`rue_bis` varchar(1) NOT NULL DEFAULT '',
|
||||
`ville` varchar(75) NOT NULL DEFAULT '',
|
||||
`fk_habitat` int(10) unsigned DEFAULT 1,
|
||||
`appt` varchar(5) DEFAULT '',
|
||||
`niveau` varchar(5) DEFAULT '',
|
||||
`residence` varchar(75) DEFAULT '',
|
||||
`gps_lat` varchar(20) NOT NULL DEFAULT '',
|
||||
`gps_lng` varchar(20) NOT NULL DEFAULT '',
|
||||
`encrypted_name` varchar(255) NOT NULL DEFAULT '',
|
||||
`montant` decimal(7,2) NOT NULL DEFAULT 0.00,
|
||||
`fk_type_reglement` int(10) unsigned DEFAULT 1,
|
||||
`remarque` text DEFAULT '',
|
||||
`encrypted_email` varchar(255) DEFAULT '',
|
||||
`nom_recu` varchar(50) DEFAULT NULL,
|
||||
`date_recu` timestamp NULL DEFAULT NULL COMMENT 'Date de réception',
|
||||
`date_creat_recu` timestamp NULL DEFAULT NULL COMMENT 'Date de création du reçu',
|
||||
`date_sent_recu` timestamp NULL DEFAULT NULL COMMENT 'Date envoi du reçu',
|
||||
`email_erreur` varchar(30) DEFAULT '',
|
||||
`chk_email_sent` tinyint(1) unsigned NOT NULL DEFAULT 0,
|
||||
`encrypted_phone` varchar(128) NOT NULL DEFAULT '',
|
||||
`is_striped` tinyint(1) unsigned NOT NULL DEFAULT 0,
|
||||
`docremis` tinyint(1) unsigned DEFAULT 0,
|
||||
`date_repasser` timestamp NULL DEFAULT NULL COMMENT 'Date prévue pour repasser',
|
||||
`nb_passages` int(11) DEFAULT 1 COMMENT 'Nb passages pour les a repasser',
|
||||
`chk_gps_maj` tinyint(1) unsigned DEFAULT 0,
|
||||
`chk_map_create` tinyint(1) unsigned DEFAULT 0,
|
||||
`chk_mobile` tinyint(1) unsigned DEFAULT 0,
|
||||
`chk_synchro` tinyint(1) unsigned DEFAULT 1 COMMENT 'chk synchro entre web et appli',
|
||||
`chk_api_adresse` tinyint(1) unsigned DEFAULT 0,
|
||||
`chk_maj_adresse` tinyint(1) unsigned DEFAULT 0,
|
||||
`anomalie` tinyint(1) unsigned DEFAULT 0,
|
||||
`created_at` timestamp NOT NULL DEFAULT current_timestamp() COMMENT 'Date de création',
|
||||
`fk_user_creat` int(10) unsigned DEFAULT NULL,
|
||||
`updated_at` timestamp NULL DEFAULT NULL ON UPDATE current_timestamp() COMMENT 'Date de modification',
|
||||
`fk_user_modif` int(10) unsigned DEFAULT NULL,
|
||||
`chk_active` tinyint(1) unsigned NOT NULL DEFAULT 1,
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `fk_operation` (`fk_operation`),
|
||||
KEY `fk_sector` (`fk_sector`),
|
||||
KEY `fk_user` (`fk_user`),
|
||||
KEY `fk_type` (`fk_type`),
|
||||
KEY `fk_type_reglement` (`fk_type_reglement`),
|
||||
KEY `email` (`encrypted_email`),
|
||||
CONSTRAINT `ope_pass_ibfk_1` FOREIGN KEY (`fk_operation`) REFERENCES `operations` (`id`) ON UPDATE CASCADE,
|
||||
CONSTRAINT `ope_pass_ibfk_2` FOREIGN KEY (`fk_sector`) REFERENCES `ope_sectors` (`id`) ON UPDATE CASCADE,
|
||||
CONSTRAINT `ope_pass_ibfk_3` FOREIGN KEY (`fk_user`) REFERENCES `users` (`id`) ON UPDATE CASCADE,
|
||||
CONSTRAINT `ope_pass_ibfk_4` FOREIGN KEY (`fk_type_reglement`) REFERENCES `x_types_reglements` (`id`) ON UPDATE CASCADE
|
||||
) ENGINE=InnoDB AUTO_INCREMENT=19499566 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci `PAGE_COMPRESSED`='ON';
|
||||
|
||||
CREATE TABLE `ope_pass_histo` (
|
||||
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
|
||||
`fk_pass` int(10) unsigned NOT NULL DEFAULT 0,
|
||||
`date_histo` timestamp NOT NULL DEFAULT current_timestamp() COMMENT 'Date historique',
|
||||
`sujet` varchar(50) DEFAULT NULL,
|
||||
`remarque` varchar(250) NOT NULL DEFAULT '',
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `ope_pass_histo_fk_pass_IDX` (`fk_pass`) USING BTREE,
|
||||
KEY `ope_pass_histo_date_histo_IDX` (`date_histo`) USING BTREE,
|
||||
CONSTRAINT `ope_pass_histo_ibfk_1` FOREIGN KEY (`fk_pass`) REFERENCES `ope_pass` (`id`) ON DELETE CASCADE ON UPDATE CASCADE
|
||||
) ENGINE=InnoDB AUTO_INCREMENT=6752 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci `PAGE_COMPRESSED`='ON';
|
||||
|
||||
CREATE TABLE `ope_sectors` (
|
||||
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
|
||||
`fk_operation` int(10) unsigned NOT NULL DEFAULT 0,
|
||||
`fk_old_sector` int(10) unsigned DEFAULT NULL,
|
||||
`libelle` varchar(75) NOT NULL DEFAULT '',
|
||||
`sector` text NOT NULL DEFAULT '',
|
||||
`color` varchar(7) NOT NULL DEFAULT '#4B77BE',
|
||||
`created_at` timestamp NOT NULL DEFAULT current_timestamp() COMMENT 'Date de création',
|
||||
`fk_user_creat` int(10) unsigned NOT NULL DEFAULT 0,
|
||||
`updated_at` timestamp NULL DEFAULT NULL ON UPDATE current_timestamp() COMMENT 'Date de modification',
|
||||
`fk_user_modif` int(10) unsigned NOT NULL DEFAULT 0,
|
||||
`chk_active` tinyint(1) unsigned NOT NULL DEFAULT 1,
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `id` (`id`),
|
||||
KEY `fk_operation` (`fk_operation`),
|
||||
CONSTRAINT `ope_sectors_ibfk_1` FOREIGN KEY (`fk_operation`) REFERENCES `operations` (`id`) ON UPDATE CASCADE
|
||||
) ENGINE=InnoDB AUTO_INCREMENT=27675 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci `PAGE_COMPRESSED`='ON';
|
||||
|
||||
CREATE TABLE `ope_users` (
|
||||
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
|
||||
`fk_operation` int(10) unsigned NOT NULL DEFAULT 0,
|
||||
`fk_user` int(10) unsigned NOT NULL DEFAULT 0,
|
||||
`created_at` timestamp NOT NULL DEFAULT current_timestamp() COMMENT 'Date de création',
|
||||
`fk_user_creat` int(10) unsigned DEFAULT NULL,
|
||||
`updated_at` timestamp NULL DEFAULT NULL ON UPDATE current_timestamp() COMMENT 'Date de modification',
|
||||
`fk_user_modif` int(10) unsigned DEFAULT NULL,
|
||||
`chk_active` tinyint(1) unsigned NOT NULL DEFAULT 1,
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `id_UNIQUE` (`id`),
|
||||
KEY `ope_users_ibfk_1` (`fk_operation`),
|
||||
KEY `ope_users_ibfk_2` (`fk_user`),
|
||||
CONSTRAINT `ope_users_ibfk_1` FOREIGN KEY (`fk_operation`) REFERENCES `operations` (`id`) ON UPDATE CASCADE,
|
||||
CONSTRAINT `ope_users_ibfk_2` FOREIGN KEY (`fk_user`) REFERENCES `users` (`id`) ON UPDATE CASCADE
|
||||
) ENGINE=InnoDB AUTO_INCREMENT=199006 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci `PAGE_COMPRESSED`='ON';
|
||||
|
||||
CREATE TABLE `ope_users_sectors` (
|
||||
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
|
||||
`fk_operation` int(10) unsigned NOT NULL DEFAULT 0,
|
||||
`fk_user` int(10) unsigned NOT NULL DEFAULT 0,
|
||||
`fk_sector` int(10) unsigned NOT NULL DEFAULT 0,
|
||||
`created_at` timestamp NOT NULL DEFAULT current_timestamp() COMMENT 'Date de création',
|
||||
`fk_user_creat` int(10) unsigned NOT NULL DEFAULT 0,
|
||||
`updated_at` timestamp NULL DEFAULT NULL ON UPDATE current_timestamp() COMMENT 'Date de modification',
|
||||
`fk_user_modif` int(10) unsigned DEFAULT NULL,
|
||||
`chk_active` tinyint(1) unsigned DEFAULT 1,
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `id` (`id`),
|
||||
KEY `fk_operation` (`fk_operation`),
|
||||
KEY `fk_user` (`fk_user`),
|
||||
KEY `fk_sector` (`fk_sector`),
|
||||
CONSTRAINT `ope_users_sectors_ibfk_1` FOREIGN KEY (`fk_operation`) REFERENCES `operations` (`id`) ON UPDATE CASCADE,
|
||||
CONSTRAINT `ope_users_sectors_ibfk_2` FOREIGN KEY (`fk_user`) REFERENCES `users` (`id`) ON UPDATE CASCADE,
|
||||
CONSTRAINT `ope_users_sectors_ibfk_3` FOREIGN KEY (`fk_sector`) REFERENCES `ope_sectors` (`id`) ON UPDATE CASCADE
|
||||
) ENGINE=InnoDB AUTO_INCREMENT=48082 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci `PAGE_COMPRESSED`='ON';
|
||||
|
||||
CREATE TABLE `operations` (
|
||||
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
|
||||
`fk_entite` int(10) unsigned NOT NULL DEFAULT 1,
|
||||
`libelle` varchar(75) NOT NULL DEFAULT '',
|
||||
`date_deb` date NOT NULL DEFAULT '0000-00-00',
|
||||
`date_fin` date NOT NULL DEFAULT '0000-00-00',
|
||||
`chk_distinct_sectors` tinyint(1) unsigned NOT NULL DEFAULT 0,
|
||||
`created_at` timestamp NOT NULL DEFAULT current_timestamp() COMMENT 'Date de création',
|
||||
`fk_user_creat` int(10) unsigned NOT NULL DEFAULT 0,
|
||||
`updated_at` timestamp NULL DEFAULT NULL ON UPDATE current_timestamp() COMMENT 'Date de modification',
|
||||
`fk_user_modif` int(10) unsigned NOT NULL DEFAULT 0,
|
||||
`chk_active` tinyint(1) unsigned NOT NULL DEFAULT 1,
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `fk_entite` (`fk_entite`),
|
||||
KEY `date_deb` (`date_deb`),
|
||||
CONSTRAINT `operations_ibfk_1` FOREIGN KEY (`fk_entite`) REFERENCES `entites` (`id`) ON UPDATE CASCADE
|
||||
) ENGINE=InnoDB AUTO_INCREMENT=3121 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci `PAGE_COMPRESSED`='ON';
|
||||
|
||||
CREATE TABLE `params` (
|
||||
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
|
||||
`libelle` varchar(35) NOT NULL DEFAULT '',
|
||||
`valeur` varchar(255) NOT NULL DEFAULT '',
|
||||
`aide` varchar(150) NOT NULL DEFAULT '',
|
||||
PRIMARY KEY (`id`)
|
||||
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci `PAGE_COMPRESSED`='ON';
|
||||
|
||||
CREATE TABLE `sectors_adresses` (
|
||||
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
|
||||
`fk_adresse` varchar(25) DEFAULT NULL COMMENT 'adresses.cp??.id',
|
||||
`osm_id` int(10) unsigned NOT NULL DEFAULT 0,
|
||||
`fk_sector` int(10) unsigned NOT NULL DEFAULT 0,
|
||||
`osm_name` varchar(50) NOT NULL DEFAULT '',
|
||||
`numero` varchar(5) NOT NULL DEFAULT '',
|
||||
`rue_bis` varchar(5) NOT NULL DEFAULT '',
|
||||
`rue` varchar(60) NOT NULL DEFAULT '',
|
||||
`cp` varchar(5) NOT NULL DEFAULT '',
|
||||
`ville` varchar(60) NOT NULL DEFAULT '',
|
||||
`gps_lat` varchar(20) NOT NULL DEFAULT '',
|
||||
`gps_lng` varchar(20) NOT NULL DEFAULT '',
|
||||
`osm_date_creat` timestamp NOT NULL DEFAULT '0000-00-00 00:00:00',
|
||||
`created_at` timestamp NOT NULL DEFAULT current_timestamp() COMMENT 'Date de création',
|
||||
`updated_at` timestamp NULL DEFAULT NULL ON UPDATE current_timestamp() COMMENT 'Date de modification',
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `sectors_adresses_fk_sector_index` (`fk_sector`),
|
||||
KEY `sectors_adresses_numero_index` (`numero`),
|
||||
KEY `sectors_adresses_rue_index` (`rue`),
|
||||
KEY `sectors_adresses_ville_index` (`ville`),
|
||||
CONSTRAINT `sectors_adresses_ibfk_1` FOREIGN KEY (`fk_sector`) REFERENCES `ope_sectors` (`id`) ON UPDATE CASCADE
|
||||
) ENGINE=InnoDB AUTO_INCREMENT=1562946 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci `PAGE_COMPRESSED`='ON';
|
||||
|
||||
CREATE TABLE `users` (
|
||||
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
|
||||
`fk_entite` int(10) unsigned DEFAULT 1,
|
||||
`fk_role` int(10) unsigned DEFAULT 1,
|
||||
`fk_titre` int(10) unsigned DEFAULT 1,
|
||||
`encrypted_name` varchar(255) DEFAULT NULL,
|
||||
`first_name` varchar(45) DEFAULT NULL,
|
||||
`sect_name` varchar(60) DEFAULT '',
|
||||
`encrypted_user_name` varchar(128) DEFAULT '',
|
||||
`user_pass_hash` varchar(60) DEFAULT NULL,
|
||||
`encrypted_phone` varchar(128) DEFAULT NULL,
|
||||
`encrypted_mobile` varchar(128) DEFAULT NULL,
|
||||
`encrypted_email` varchar(255) DEFAULT '',
|
||||
`chk_alert_email` tinyint(1) unsigned DEFAULT 1,
|
||||
`chk_suivi` tinyint(1) unsigned DEFAULT 0,
|
||||
`date_naissance` date DEFAULT NULL,
|
||||
`date_embauche` date DEFAULT NULL,
|
||||
`created_at` timestamp NOT NULL DEFAULT current_timestamp() COMMENT 'Date de création',
|
||||
`fk_user_creat` int(10) unsigned DEFAULT NULL,
|
||||
`updated_at` timestamp NULL DEFAULT NULL ON UPDATE current_timestamp() COMMENT 'Date de modification',
|
||||
`fk_user_modif` int(10) unsigned DEFAULT NULL,
|
||||
`chk_active` tinyint(1) unsigned DEFAULT 1,
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `fk_entite` (`fk_entite`),
|
||||
KEY `username` (`encrypted_user_name`),
|
||||
KEY `users_ibfk_2` (`fk_role`),
|
||||
KEY `users_ibfk_3` (`fk_titre`),
|
||||
CONSTRAINT `users_ibfk_1` FOREIGN KEY (`fk_entite`) REFERENCES `entites` (`id`) ON UPDATE CASCADE,
|
||||
CONSTRAINT `users_ibfk_2` FOREIGN KEY (`fk_role`) REFERENCES `x_users_roles` (`id`) ON UPDATE CASCADE,
|
||||
CONSTRAINT `users_ibfk_3` FOREIGN KEY (`fk_titre`) REFERENCES `x_users_titres` (`id`) ON UPDATE CASCADE
|
||||
) ENGINE=InnoDB AUTO_INCREMENT=10027748 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci `PAGE_COMPRESSED`='ON';
|
||||
|
||||
CREATE TABLE `x_departements` (
|
||||
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
|
||||
`code` varchar(3) DEFAULT NULL,
|
||||
`fk_region` int(10) unsigned DEFAULT 1,
|
||||
`libelle` varchar(45) DEFAULT NULL,
|
||||
`chk_active` tinyint(1) unsigned DEFAULT 1,
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `id_UNIQUE` (`id`),
|
||||
KEY `x_departements_ibfk_1` (`fk_region`),
|
||||
CONSTRAINT `x_departements_ibfk_1` FOREIGN KEY (`fk_region`) REFERENCES `x_regions` (`id`) ON UPDATE CASCADE
|
||||
) ENGINE=InnoDB AUTO_INCREMENT=105 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci `PAGE_COMPRESSED`='ON';
|
||||
|
||||
CREATE TABLE `x_devises` (
|
||||
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
|
||||
`code` varchar(3) DEFAULT NULL,
|
||||
`symbole` varchar(6) DEFAULT NULL,
|
||||
`libelle` varchar(45) DEFAULT NULL,
|
||||
`chk_active` tinyint(1) unsigned DEFAULT 1,
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `id_UNIQUE` (`id`)
|
||||
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci `PAGE_COMPRESSED`='ON';
|
||||
|
||||
CREATE TABLE `x_entites_types` (
|
||||
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
|
||||
`libelle` varchar(45) DEFAULT NULL,
|
||||
`chk_active` tinyint(1) unsigned DEFAULT NULL,
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `id_UNIQUE` (`id`)
|
||||
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci `PAGE_COMPRESSED`='ON';
|
||||
|
||||
CREATE TABLE `x_pays` (
|
||||
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
|
||||
`code` varchar(3) DEFAULT NULL,
|
||||
`fk_continent` int(10) unsigned DEFAULT NULL,
|
||||
`fk_devise` int(10) unsigned DEFAULT 1,
|
||||
`libelle` varchar(45) DEFAULT NULL,
|
||||
`chk_active` tinyint(1) unsigned DEFAULT 1,
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `id_UNIQUE` (`id`),
|
||||
KEY `x_pays_ibfk_1` (`fk_devise`),
|
||||
CONSTRAINT `x_pays_ibfk_1` FOREIGN KEY (`fk_devise`) REFERENCES `x_devises` (`id`) ON UPDATE CASCADE
|
||||
) ENGINE=InnoDB AUTO_INCREMENT=10 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='Table des pays avec leurs codes' `PAGE_COMPRESSED`='ON';
|
||||
|
||||
CREATE TABLE `x_regions` (
|
||||
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
|
||||
`fk_pays` int(10) unsigned DEFAULT 1,
|
||||
`libelle` varchar(45) DEFAULT NULL,
|
||||
`libelle_long` varchar(45) DEFAULT NULL,
|
||||
`table_osm` varchar(45) DEFAULT NULL,
|
||||
`departements` varchar(45) DEFAULT NULL,
|
||||
`chk_active` tinyint(1) unsigned DEFAULT 1,
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `id_UNIQUE` (`id`),
|
||||
KEY `x_regions_ibfk_1` (`fk_pays`),
|
||||
CONSTRAINT `x_regions_ibfk_1` FOREIGN KEY (`fk_pays`) REFERENCES `x_pays` (`id`) ON UPDATE CASCADE
|
||||
) ENGINE=InnoDB AUTO_INCREMENT=29 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci `PAGE_COMPRESSED`='ON';
|
||||
|
||||
CREATE TABLE `x_types_passages` (
|
||||
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
|
||||
`libelle` varchar(10) DEFAULT NULL,
|
||||
`color_button` varchar(15) DEFAULT NULL,
|
||||
`color_mark` varchar(15) DEFAULT NULL,
|
||||
`color_table` varchar(15) DEFAULT NULL,
|
||||
`chk_active` tinyint(1) unsigned NOT NULL DEFAULT 1,
|
||||
PRIMARY KEY (`id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci `PAGE_COMPRESSED`='ON';
|
||||
|
||||
CREATE TABLE `x_types_reglements` (
|
||||
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
|
||||
`libelle` varchar(45) DEFAULT NULL,
|
||||
`chk_active` tinyint(1) unsigned DEFAULT 1,
|
||||
PRIMARY KEY (`id`)
|
||||
) ENGINE=InnoDB AUTO_INCREMENT=5 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci `PAGE_COMPRESSED`='ON';
|
||||
|
||||
CREATE TABLE `x_users_roles` (
|
||||
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
|
||||
`libelle` varchar(45) DEFAULT NULL,
|
||||
`chk_active` tinyint(1) unsigned DEFAULT 1,
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `id_UNIQUE` (`id`)
|
||||
) ENGINE=InnoDB AUTO_INCREMENT=10 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='Les différents rôles des utilisateurs' `PAGE_COMPRESSED`='ON';
|
||||
|
||||
CREATE TABLE `x_users_titres` (
|
||||
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
|
||||
`libelle` varchar(45) DEFAULT NULL,
|
||||
`chk_active` tinyint(1) unsigned DEFAULT 1,
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `id_UNIQUE` (`id`)
|
||||
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='Les différents titres des utilisateurs' `PAGE_COMPRESSED`='ON';
|
||||
|
||||
CREATE TABLE `x_villes` (
|
||||
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
|
||||
`fk_departement` int(10) unsigned DEFAULT 1,
|
||||
`libelle` varchar(65) DEFAULT NULL,
|
||||
`code_postal` varchar(5) DEFAULT NULL,
|
||||
`code_insee` varchar(5) DEFAULT NULL,
|
||||
`chk_active` tinyint(1) unsigned DEFAULT 1,
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `id_UNIQUE` (`id`),
|
||||
KEY `x_villes_ibfk_1` (`fk_departement`),
|
||||
CONSTRAINT `x_villes_ibfk_1` FOREIGN KEY (`fk_departement`) REFERENCES `x_departements` (`id`) ON UPDATE CASCADE
|
||||
) ENGINE=InnoDB AUTO_INCREMENT=38950 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci `PAGE_COMPRESSED`='ON';
|
||||
|
||||
CREATE TABLE `z_sessions` (
|
||||
`sid` text NOT NULL,
|
||||
`fk_user` int(11) NOT NULL,
|
||||
`role` varchar(10) DEFAULT NULL,
|
||||
`date_modified` timestamp NOT NULL DEFAULT current_timestamp() ON UPDATE current_timestamp(),
|
||||
`ip` varchar(50) NOT NULL,
|
||||
`browser` varchar(150) NOT NULL,
|
||||
`data` mediumtext DEFAULT NULL
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci `PAGE_COMPRESSED`='ON';
|
||||
|
||||
CREATE ALGORITHM=UNDEFINED DEFINER=`root`@`%` SQL SECURITY DEFINER VIEW `chat_conversations_unread` AS select `r`.`id` AS `id`,`r`.`type` AS `type`,`r`.`title` AS `title`,`r`.`date_creation` AS `date_creation`,`r`.`fk_user` AS `fk_user`,`r`.`fk_entite` AS `fk_entite`,`r`.`statut` AS `statut`,`r`.`description` AS `description`,`r`.`reply_permission` AS `reply_permission`,`r`.`is_pinned` AS `is_pinned`,`r`.`expiry_date` AS `expiry_date`,`r`.`updated_at` AS `updated_at`,count(distinct `m`.`id`) AS `total_messages`,count(distinct `rm`.`id`) AS `read_messages`,count(distinct `m`.`id`) - count(distinct `rm`.`id`) AS `unread_messages`,(select `geo_app`.`chat_messages`.`date_sent` from `chat_messages` where `geo_app`.`chat_messages`.`fk_room` = `r`.`id` order by `geo_app`.`chat_messages`.`date_sent` desc limit 1) AS `last_message_date` from ((`chat_rooms` `r` left join `chat_messages` `m` on(`r`.`id` = `m`.`fk_room`)) left join `chat_read_messages` `rm` on(`m`.`id` = `rm`.`fk_message`)) group by `r`.`id`;
|
||||
|
||||
|
||||
/*!40101 SET SQL_MODE=@OLD_SQL_MODE */;
|
||||
/*!40014 SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS */;
|
||||
/*!40014 SET UNIQUE_CHECKS=@OLD_UNIQUE_CHECKS */;
|
||||
/*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */;
|
||||
/*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */;
|
||||
/*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */;
|
||||
/*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */;
|
||||
0
api/docs/geosector-db-diagram.md
Normal file → Executable file
0
api/docs/geosector-db-diagram.md
Normal file → Executable file
0
api/docs/geosector_app.sql
Normal file → Executable file
0
api/docs/geosector_app.sql
Normal file → Executable file
19
api/docs/logrotate_email_queue.conf
Normal file
19
api/docs/logrotate_email_queue.conf
Normal file
@@ -0,0 +1,19 @@
|
||||
# Configuration logrotate pour email_queue.log
|
||||
# À placer dans /etc/logrotate.d/geosector-email-queue
|
||||
|
||||
/var/www/geosector/api/logs/email_queue.log {
|
||||
daily # Rotation journalière
|
||||
rotate 30 # Garder 30 jours d'historique
|
||||
compress # Compresser les anciens logs
|
||||
delaycompress # Compresser le jour suivant
|
||||
missingok # Pas d'erreur si le fichier n'existe pas
|
||||
notifempty # Ne pas tourner si vide
|
||||
create 664 www-data www-data # Créer nouveau fichier avec permissions
|
||||
dateext # Ajouter la date au nom du fichier
|
||||
dateformat -%Y%m%d # Format de date YYYYMMDD
|
||||
maxsize 100M # Rotation si dépasse 100MB même avant la fin du jour
|
||||
postrotate
|
||||
# Optionnel : envoyer un signal au process si nécessaire
|
||||
# /usr/bin/killall -SIGUSR1 php 2>/dev/null || true
|
||||
endscript
|
||||
}
|
||||
93
api/docs/recu_19500582.pdf
Normal file
93
api/docs/recu_19500582.pdf
Normal file
@@ -0,0 +1,93 @@
|
||||
%PDF-1.4
|
||||
%âãÏÓ
|
||||
1 0 obj
|
||||
<< /Type /Catalog /Pages 2 0 R >>
|
||||
endobj
|
||||
2 0 obj
|
||||
<< /Type /Pages /Kids [3 0 R] /Count 1 >>
|
||||
endobj
|
||||
3 0 obj
|
||||
<< /Type /Page /Parent 2 0 R /MediaBox [0 0 595 842] /Resources << /Font << /F1 4 0 R >> >> /Contents 5 0 R >>
|
||||
endobj
|
||||
4 0 obj
|
||||
<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica-Bold /Encoding /WinAnsiEncoding >>
|
||||
endobj
|
||||
5 0 obj
|
||||
<< /Length 599 >>
|
||||
stream
|
||||
BT
|
||||
/F1 14 Tf
|
||||
217 792 Td
|
||||
(AMICALE TEST DEV PIERRE) Tj
|
||||
ET
|
||||
BT
|
||||
/F1 11 Tf
|
||||
281 770 Td
|
||||
(RENNES) Tj
|
||||
ET
|
||||
BT
|
||||
/F1 14 Tf
|
||||
213.5 726 Td
|
||||
(RECU FISCAL N 19500582) Tj
|
||||
ET
|
||||
BT
|
||||
/F1 9 Tf
|
||||
263.75 704 Td
|
||||
(Article 200 CGI) Tj
|
||||
ET
|
||||
BT
|
||||
/F1 12 Tf
|
||||
50 657 Td
|
||||
(Dugues) Tj
|
||||
ET
|
||||
BT
|
||||
/F1 11 Tf
|
||||
50 637 Td
|
||||
(8 le Petit Monthelon Acigne) Tj
|
||||
ET
|
||||
BT
|
||||
/F1 16 Tf
|
||||
257.5 598 Td
|
||||
(8,00 euros) Tj
|
||||
ET
|
||||
BT
|
||||
/F1 12 Tf
|
||||
267.5 559 Td
|
||||
(20/08/2025) Tj
|
||||
ET
|
||||
BT
|
||||
/F1 10 Tf
|
||||
277.5 529 Td
|
||||
(OPE 2025) Tj
|
||||
ET
|
||||
BT
|
||||
/F1 9 Tf
|
||||
198.5 476 Td
|
||||
(Don ouvrant droit a reduction d'impot de 66%) Tj
|
||||
ET
|
||||
BT
|
||||
/F1 10 Tf
|
||||
50 419 Td
|
||||
(Le 20/08/2025) Tj
|
||||
ET
|
||||
BT
|
||||
/F1 10 Tf
|
||||
50 401 Td
|
||||
(Le President) Tj
|
||||
ET
|
||||
|
||||
endstream
|
||||
endobj
|
||||
xref
|
||||
0 6
|
||||
0000000000 65535 f
|
||||
0000000019 00000 n
|
||||
0000000068 00000 n
|
||||
0000000125 00000 n
|
||||
0000000251 00000 n
|
||||
0000000353 00000 n
|
||||
trailer
|
||||
<< /Size 6 /Root 1 0 R >>
|
||||
startxref
|
||||
1003
|
||||
%%EOF
|
||||
75
api/docs/recu_19500586.pdf
Normal file
75
api/docs/recu_19500586.pdf
Normal file
@@ -0,0 +1,75 @@
|
||||
%PDF-1.3
|
||||
1 0 obj
|
||||
<< /Type /Catalog /Pages 2 0 R >>
|
||||
endobj
|
||||
2 0 obj
|
||||
<< /Type /Pages /Kids [3 0 R] /Count 1 >>
|
||||
endobj
|
||||
3 0 obj
|
||||
<< /Type /Page /Parent 2 0 R /MediaBox [0 0 595 842] /Resources << /Font << /F1 4 0 R >> >> /Contents 5 0 R >>
|
||||
endobj
|
||||
4 0 obj
|
||||
<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica >>
|
||||
endobj
|
||||
5 0 obj
|
||||
<< /Length 767 >>
|
||||
stream
|
||||
BT
|
||||
/F1 12 Tf
|
||||
50 750 Td
|
||||
(AMICALE TEST DEV PIERRE) Tj
|
||||
0 -20 Td
|
||||
(17 place hoche 35000 RENNES) Tj
|
||||
/F1 16 Tf
|
||||
0 -40 Td
|
||||
(RECU DE DON N° 19500586) Tj
|
||||
/F1 10 Tf
|
||||
0 -15 Td
|
||||
(Article 200 du Code General des Impots) Tj
|
||||
/F1 12 Tf
|
||||
0 -45 Td
|
||||
(DONATEUR) Tj
|
||||
/F1 11 Tf
|
||||
0 -20 Td
|
||||
(Nom : M. Hermann) Tj
|
||||
0 -15 Td
|
||||
(Adresse : 12 le Petit Monthelon Acigne) Tj
|
||||
0 -15 Td
|
||||
(Email : pierre.vaissaire@gmail.com) Tj
|
||||
0 -30 Td
|
||||
/F1 12 Tf
|
||||
(DETAILS DU DON) Tj
|
||||
/F1 11 Tf
|
||||
0 -20 Td
|
||||
(Date : 19/08/2025) Tj
|
||||
0 -15 Td
|
||||
(Montant : 12,00 EUR) Tj
|
||||
0 -15 Td
|
||||
(Mode de reglement : Espece) Tj
|
||||
0 -15 Td
|
||||
(Campagne : OPE 2025) Tj
|
||||
/F1 9 Tf
|
||||
0 -40 Td
|
||||
(Reduction d'impot egale a 66% du montant verse dans la limite de 20% du revenu imposable) Tj
|
||||
/F1 11 Tf
|
||||
0 -30 Td
|
||||
(Fait a RENNES, le 19/08/2025) Tj
|
||||
0 -20 Td
|
||||
(Le President) Tj
|
||||
ET
|
||||
|
||||
endstream
|
||||
endobj
|
||||
xref
|
||||
0 6
|
||||
0000000000 65535 f
|
||||
0000000009 00000 n
|
||||
0000000058 00000 n
|
||||
0000000115 00000 n
|
||||
0000000241 00000 n
|
||||
0000000311 00000 n
|
||||
trailer
|
||||
<< /Size 6 /Root 1 0 R >>
|
||||
startxref
|
||||
1129
|
||||
%%EOF
|
||||
BIN
api/docs/recu_537254062.pdf
Normal file
BIN
api/docs/recu_537254062.pdf
Normal file
Binary file not shown.
BIN
api/docs/recu_972506460.pdf
Normal file
BIN
api/docs/recu_972506460.pdf
Normal file
Binary file not shown.
128
api/docs/x_departements_contours.sql
Normal file
128
api/docs/x_departements_contours.sql
Normal file
File diff suppressed because one or more lines are too long
128
api/docs/x_departements_contours_corrected.sql
Normal file
128
api/docs/x_departements_contours_corrected.sql
Normal file
File diff suppressed because one or more lines are too long
128
api/docs/x_departements_contours_fixed.sql
Normal file
128
api/docs/x_departements_contours_fixed.sql
Normal file
File diff suppressed because one or more lines are too long
145
api/export_operation.php
Executable file
145
api/export_operation.php
Executable file
@@ -0,0 +1,145 @@
|
||||
<?php
|
||||
|
||||
use PhpOffice\PhpSpreadsheet\Spreadsheet;
|
||||
use PhpOffice\PhpSpreadsheet\Writer\Xlsx;
|
||||
|
||||
global $Session, $Conf, $Route;
|
||||
// appel de geolib en admin
|
||||
require_once __DIR__ . '/../pub/res/php/geolib.php';
|
||||
|
||||
function nettoie_input($input) {
|
||||
return htmlspecialchars(strip_tags(trim($input)));
|
||||
}
|
||||
|
||||
function getinfos($sql) {
|
||||
// This is a placeholder function. Replace with actual database query logic.
|
||||
// For example, you might use PDO to execute the query and return the results.
|
||||
// $db = Database::getInstance();
|
||||
// $stmt = $db->prepare($sql);
|
||||
// $stmt->execute();
|
||||
// return $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
return [];
|
||||
}
|
||||
|
||||
function eLog($message) {
|
||||
error_log($message);
|
||||
}
|
||||
|
||||
switch ($Route->_action) {
|
||||
case "export_operation":
|
||||
$data = json_decode(file_get_contents("php://input"));
|
||||
if (isset($data->cid)) {
|
||||
$cid = nettoie_input($data->cid);
|
||||
$idMembre = "0";
|
||||
$libMembre = "";
|
||||
if (isset($data->idMembre) && isset($data->libMembre)) {
|
||||
$idMembre = nettoie_input($data->idMembre);
|
||||
$libMembre = nettoie_input($data->libMembre);
|
||||
}
|
||||
|
||||
// On crée le dossier de l'amicale s'il n'est pas déjà créé
|
||||
$dir = 'pub/files/upload/' . $Conf->_entite["rowid"];
|
||||
if (!is_dir($dir)) {
|
||||
mkdir($dir, 0777, true);
|
||||
}
|
||||
|
||||
$sql = 'SELECT p.date_eve, u.prenom, u.libelle AS nom, u.nom_tournee, p.fk_type, p.numero, p.rue_bis, p.rue, p.ville, p.fk_habitat, p.appt, p.niveau, p.libelle, p.email, p.phone, p.montant, xtr.libelle AS reglement, p.remarque FROM ope_pass p LEFT JOIN users u ON u.rowid=p.fk_user LEFT JOIN x_types_reglements xtr ON xtr.rowid=p.fk_type_reglement WHERE p.fk_operation=' . $cid;
|
||||
if ($idMembre != "0") {
|
||||
$sql .= ' AND p.fk_user=' . $idMembre . ';';
|
||||
}
|
||||
$pass = getinfos($sql);
|
||||
|
||||
$aData = array();
|
||||
$aData[] = array(
|
||||
'Date',
|
||||
'Heure',
|
||||
'Prenom',
|
||||
'Nom',
|
||||
'Tournee',
|
||||
'Type',
|
||||
'N°',
|
||||
'Rue',
|
||||
'Ville',
|
||||
'Habitat',
|
||||
'Donateur',
|
||||
'Email',
|
||||
'Tel',
|
||||
'Montant',
|
||||
'Reglement',
|
||||
'Remarque'
|
||||
);
|
||||
foreach ($pass as $p) {
|
||||
switch ($p["fk_type"]) {
|
||||
case 1:
|
||||
$ptype = "Effectué";
|
||||
$preglement = $p["reglement"];
|
||||
break;
|
||||
case 2:
|
||||
$ptype = "A finaliser";
|
||||
$preglement = "";
|
||||
break;
|
||||
case 3:
|
||||
$ptype = "Refusé";
|
||||
$preglement = "";
|
||||
break;
|
||||
case 4:
|
||||
$ptype = "Don";
|
||||
$preglement = "";
|
||||
break;
|
||||
case 9:
|
||||
$ptype = "Habitat vide";
|
||||
$preglement = "";
|
||||
break;
|
||||
default:
|
||||
$ptype = $p["fk_type"];
|
||||
$preglement = "";
|
||||
break;
|
||||
}
|
||||
if ($p["fk_habitat"] == 1) {
|
||||
$phabitat = "Individuel";
|
||||
} else {
|
||||
$phabitat = "Etage " . $p["niveau"] . " - Appt " . $p["appt"];
|
||||
}
|
||||
$dateEve = date("d/m/Y", strtotime($p["date_eve"]));
|
||||
$heureEve = date("H:i", strtotime($p["date_eve"]));
|
||||
$nom = str_replace("/", "-", $p["nom"]);
|
||||
$tournee = str_replace("/", "-", $p["nom_tournee"]);
|
||||
$aData[] = array(
|
||||
$dateEve,
|
||||
$heureEve,
|
||||
$p["prenom"],
|
||||
$nom,
|
||||
$tournee,
|
||||
$ptype,
|
||||
$p["numero"] . $p["rue_bis"],
|
||||
$p["rue"],
|
||||
$p["ville"],
|
||||
$phabitat,
|
||||
$p["libelle"],
|
||||
$p["email"],
|
||||
$p["phone"],
|
||||
$p["montant"],
|
||||
$preglement,
|
||||
$p["remarque"]
|
||||
);
|
||||
}
|
||||
$spreadsheet = new \PhpOffice\PhpSpreadsheet\Spreadsheet();
|
||||
$activeWorksheet = $spreadsheet->getActiveSheet()
|
||||
->fromArray($aData, null, 'A1');
|
||||
|
||||
$writer = new Xlsx($spreadsheet);
|
||||
if ($idMembre == "0") {
|
||||
$xlsxName = $dir . '/geosector-ope-' . $cid . '-' . date("Ymd-His") . '.xlsx';
|
||||
} else {
|
||||
$libMembre = str_replace("/", "-", $libMembre);
|
||||
$libMembre = str_replace("*", "-", $libMembre);
|
||||
$xlsxName = $dir . '/geosector-ope-' . $cid . '-' . $libMembre . '-' . date("Ymd-His") . '.xlsx';
|
||||
}
|
||||
eLog("Export Operation : " . $xlsxName);
|
||||
$writer->save($xlsxName);
|
||||
|
||||
$ret = array('url' => $xlsxName);
|
||||
echo json_encode($ret);
|
||||
}
|
||||
break;
|
||||
}
|
||||
152
api/index.php
Normal file → Executable file
152
api/index.php
Normal file → Executable file
@@ -7,6 +7,7 @@ require_once __DIR__ . '/bootstrap.php';
|
||||
// Chargement des fichiers principaux
|
||||
require_once __DIR__ . '/src/Config/AppConfig.php';
|
||||
require_once __DIR__ . '/src/Core/Database.php';
|
||||
require_once __DIR__ . '/src/Core/AddressesDatabase.php';
|
||||
require_once __DIR__ . '/src/Core/Router.php';
|
||||
require_once __DIR__ . '/src/Core/Session.php';
|
||||
require_once __DIR__ . '/src/Core/Request.php';
|
||||
@@ -14,19 +15,44 @@ require_once __DIR__ . '/src/Core/Response.php';
|
||||
require_once __DIR__ . '/src/Utils/ClientDetector.php';
|
||||
require_once __DIR__ . '/src/Services/LogService.php';
|
||||
|
||||
// Chargement des services
|
||||
require_once __DIR__ . '/src/Services/StripeService.php';
|
||||
|
||||
// Chargement des services de sécurité
|
||||
require_once __DIR__ . '/src/Services/Security/PerformanceMonitor.php';
|
||||
require_once __DIR__ . '/src/Services/Security/IPBlocker.php';
|
||||
require_once __DIR__ . '/src/Services/Security/SecurityMonitor.php';
|
||||
require_once __DIR__ . '/src/Services/Security/AlertService.php';
|
||||
|
||||
// Chargement de la classe Controller de base
|
||||
require_once __DIR__ . '/src/Core/Controller.php';
|
||||
|
||||
// Chargement des contrôleurs
|
||||
require_once __DIR__ . '/src/Controllers/LogController.php';
|
||||
require_once __DIR__ . '/src/Controllers/LoginController.php';
|
||||
require_once __DIR__ . '/src/Controllers/EntiteController.php';
|
||||
require_once __DIR__ . '/src/Controllers/UserController.php';
|
||||
require_once __DIR__ . '/src/Controllers/OperationController.php';
|
||||
require_once __DIR__ . '/src/Controllers/PassageController.php';
|
||||
require_once __DIR__ . '/src/Controllers/VilleController.php';
|
||||
require_once __DIR__ . '/src/Controllers/FileController.php';
|
||||
require_once __DIR__ . '/src/Controllers/SectorController.php';
|
||||
require_once __DIR__ . '/src/Controllers/PasswordController.php';
|
||||
require_once __DIR__ . '/src/Controllers/ChatController.php';
|
||||
require_once __DIR__ . '/src/Controllers/SecurityController.php';
|
||||
require_once __DIR__ . '/src/Controllers/StripeController.php';
|
||||
require_once __DIR__ . '/src/Controllers/StripeWebhookController.php';
|
||||
|
||||
// Initialiser la configuration
|
||||
$appConfig = AppConfig::getInstance();
|
||||
$config = $appConfig->getFullConfig();
|
||||
|
||||
// Initialiser la base de données
|
||||
// Initialiser la base de données principale
|
||||
Database::init($config['database']);
|
||||
|
||||
// Initialiser la base de données des adresses
|
||||
AddressesDatabase::init($appConfig->getAddressesDatabaseConfig());
|
||||
|
||||
// Configuration CORS
|
||||
$origin = $_SERVER['HTTP_ORIGIN'] ?? '';
|
||||
$allowedOrigins = $config['api']['allowed_origins'];
|
||||
@@ -47,8 +73,132 @@ if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
|
||||
// Initialiser la session
|
||||
Session::start();
|
||||
|
||||
// ===== DÉBUT DU MONITORING DE SÉCURITÉ =====
|
||||
use App\Services\Security\PerformanceMonitor;
|
||||
use App\Services\Security\IPBlocker;
|
||||
use App\Services\Security\SecurityMonitor;
|
||||
use App\Services\Security\AlertService;
|
||||
|
||||
// Obtenir l'IP du client
|
||||
$clientIp = $_SERVER['REMOTE_ADDR'] ?? 'unknown';
|
||||
|
||||
// Vérifier si l'IP est bloquée
|
||||
if (IPBlocker::isBlocked($clientIp)) {
|
||||
http_response_code(403);
|
||||
Response::json([
|
||||
'status' => 'error',
|
||||
'message' => 'Access denied. Your IP has been blocked.',
|
||||
'error_code' => 'IP_BLOCKED'
|
||||
], 403);
|
||||
exit;
|
||||
}
|
||||
|
||||
// Vérifier le rate limiting
|
||||
if (!SecurityMonitor::checkRateLimit($clientIp)) {
|
||||
http_response_code(429);
|
||||
Response::json([
|
||||
'status' => 'error',
|
||||
'message' => 'Too many requests. Please try again later.',
|
||||
'error_code' => 'RATE_LIMIT_EXCEEDED'
|
||||
], 429);
|
||||
exit;
|
||||
}
|
||||
|
||||
// Démarrer le monitoring de performance
|
||||
PerformanceMonitor::startRequest();
|
||||
|
||||
// Capturer le endpoint pour le monitoring
|
||||
$requestUri = $_SERVER['REQUEST_URI'] ?? '/';
|
||||
$requestMethod = $_SERVER['REQUEST_METHOD'] ?? 'GET';
|
||||
|
||||
// Vérifier les patterns de scan
|
||||
if (!SecurityMonitor::checkScanPattern($requestUri)) {
|
||||
// Pattern suspect détecté, bloquer l'IP temporairement
|
||||
IPBlocker::block($clientIp, 3600, 'Suspicious scan pattern detected');
|
||||
http_response_code(404);
|
||||
Response::json([
|
||||
'status' => 'error',
|
||||
'message' => 'Not found'
|
||||
], 404);
|
||||
exit;
|
||||
}
|
||||
|
||||
// Vérifier les paramètres pour injection SQL
|
||||
$allParams = array_merge($_GET, $_POST, json_decode(file_get_contents('php://input'), true) ?? []);
|
||||
if (!empty($allParams) && !SecurityMonitor::checkRequestParameters($allParams)) {
|
||||
// Injection SQL détectée, bloquer l'IP définitivement
|
||||
IPBlocker::blockPermanent($clientIp, 'SQL injection attempt');
|
||||
http_response_code(400);
|
||||
Response::json([
|
||||
'status' => 'error',
|
||||
'message' => 'Bad request'
|
||||
], 400);
|
||||
exit;
|
||||
}
|
||||
|
||||
// Créer l'instance de routeur
|
||||
$router = new Router();
|
||||
|
||||
// Enregistrer une fonction de shutdown pour capturer les métriques
|
||||
register_shutdown_function(function() use ($requestUri, $requestMethod) {
|
||||
$statusCode = http_response_code();
|
||||
|
||||
// Terminer le monitoring de performance
|
||||
PerformanceMonitor::endRequest($requestUri, $requestMethod, $statusCode);
|
||||
|
||||
// Vérifier les patterns 404
|
||||
if ($statusCode === 404) {
|
||||
$clientIp = $_SERVER['REMOTE_ADDR'] ?? 'unknown';
|
||||
SecurityMonitor::check404Pattern($clientIp);
|
||||
}
|
||||
|
||||
// Alerter sur les erreurs 500
|
||||
if ($statusCode >= 500) {
|
||||
$error = error_get_last();
|
||||
AlertService::trigger('HTTP_500', [
|
||||
'endpoint' => $requestUri,
|
||||
'method' => $requestMethod,
|
||||
'error_message' => $error['message'] ?? 'Unknown error',
|
||||
'error_file' => $error['file'] ?? 'Unknown',
|
||||
'error_line' => $error['line'] ?? 0,
|
||||
'message' => "Erreur serveur 500 sur $requestUri"
|
||||
], 'ERROR');
|
||||
}
|
||||
|
||||
// Nettoyer périodiquement les IPs expirées (1% de chance)
|
||||
if (rand(1, 100) === 1) {
|
||||
IPBlocker::cleanupExpired();
|
||||
}
|
||||
});
|
||||
|
||||
// Gérer les erreurs non capturées
|
||||
set_exception_handler(function($exception) use ($requestUri, $requestMethod) {
|
||||
// Logger l'erreur
|
||||
error_log("Uncaught exception: " . $exception->getMessage());
|
||||
|
||||
// Créer une alerte
|
||||
AlertService::trigger('UNCAUGHT_EXCEPTION', [
|
||||
'endpoint' => $requestUri,
|
||||
'method' => $requestMethod,
|
||||
'exception' => get_class($exception),
|
||||
'message' => $exception->getMessage(),
|
||||
'file' => $exception->getFile(),
|
||||
'line' => $exception->getLine(),
|
||||
'trace' => substr($exception->getTraceAsString(), 0, 1000)
|
||||
], 'ERROR');
|
||||
|
||||
// Retourner une erreur 500
|
||||
http_response_code(500);
|
||||
Response::json([
|
||||
'status' => 'error',
|
||||
'message' => 'Internal server error'
|
||||
], 500);
|
||||
});
|
||||
|
||||
// Gérer la requête
|
||||
try {
|
||||
$router->handle();
|
||||
} catch (Exception $e) {
|
||||
// Les exceptions sont gérées par le handler ci-dessus
|
||||
throw $e;
|
||||
}
|
||||
|
||||
115
api/livre-api.sh
115
api/livre-api.sh
@@ -1,25 +1,47 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Vérification des arguments
|
||||
if [ $# -ne 2 ]; then
|
||||
echo "Usage: $0 <source_container> <destination_container>"
|
||||
echo "Example: $0 dva-geo rca-geo"
|
||||
if [ $# -ne 1 ]; then
|
||||
echo "Usage: $0 <environment>"
|
||||
echo " rec : Livrer de DVA (dva-geo) vers RECETTE (rca-geo)"
|
||||
echo " prod : Livrer de RECETTE (rca-geo) vers PRODUCTION (pra-geo)"
|
||||
echo ""
|
||||
echo "Examples:"
|
||||
echo " $0 rec # DVA → RECETTE"
|
||||
echo " $0 prod # RECETTE → PRODUCTION"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
HOST_IP="195.154.80.116"
|
||||
HOST_USER=root
|
||||
HOST_KEY=/Users/pierre/.ssh/id_rsa_mbpi
|
||||
HOST_KEY=/home/pierre/.ssh/id_rsa_mbpi
|
||||
HOST_PORT=22
|
||||
|
||||
SOURCE_CONTAINER=$1
|
||||
DEST_CONTAINER=$2
|
||||
# Mapping des environnements
|
||||
ENVIRONMENT=$1
|
||||
case $ENVIRONMENT in
|
||||
"rca")
|
||||
SOURCE_CONTAINER="dva-geo"
|
||||
DEST_CONTAINER="rca-geo"
|
||||
ENV_NAME="RECETTE"
|
||||
;;
|
||||
"pra")
|
||||
SOURCE_CONTAINER="rca-geo"
|
||||
DEST_CONTAINER="pra-geo"
|
||||
ENV_NAME="PRODUCTION"
|
||||
;;
|
||||
*)
|
||||
echo "❌ Environnement '$ENVIRONMENT' non reconnu"
|
||||
echo "Utilisez 'rec' pour RECETTE ou 'prod' pour PRODUCTION"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
API_PATH="/var/www/geosector/api"
|
||||
TIMESTAMP=$(date +"%Y%m%d_%H%M%S")
|
||||
BACKUP_DIR="${API_PATH}_backup_${TIMESTAMP}"
|
||||
PROJECT="default"
|
||||
|
||||
echo "🔄 Copie de l'API de $SOURCE_CONTAINER vers $DEST_CONTAINER (projet: $PROJECT)"
|
||||
echo "🔄 Livraison vers $ENV_NAME : $SOURCE_CONTAINER → $DEST_CONTAINER (projet: $PROJECT)"
|
||||
|
||||
# Vérifier si les containers existent
|
||||
echo "🔍 Vérification des containers..."
|
||||
@@ -47,37 +69,24 @@ else
|
||||
echo "⚠️ Le dossier API n'existe pas sur la destination"
|
||||
fi
|
||||
|
||||
# Sauvegarder spécifiquement le dossier logs
|
||||
echo "📋 Sauvegarde du dossier logs..."
|
||||
# Vérifier si le dossier logs existe
|
||||
ssh -i $HOST_KEY -p $HOST_PORT $HOST_USER@$HOST_IP "incus exec $DEST_CONTAINER --project $PROJECT -- test -d $API_PATH/logs"
|
||||
if [ $? -eq 0 ]; then
|
||||
# Le dossier logs existe, le sauvegarder
|
||||
ssh -i $HOST_KEY -p $HOST_PORT $HOST_USER@$HOST_IP "incus exec $DEST_CONTAINER --project $PROJECT -- mkdir -p /tmp/geosector_logs_backup"
|
||||
ssh -i $HOST_KEY -p $HOST_PORT $HOST_USER@$HOST_IP "incus exec $DEST_CONTAINER --project $PROJECT -- cp -r $API_PATH/logs /tmp/geosector_logs_backup/"
|
||||
echo "✅ Dossier logs sauvegardé temporairement"
|
||||
else
|
||||
echo "⚠️ Le dossier logs n'existe pas sur la destination"
|
||||
fi
|
||||
|
||||
# Copier le dossier API entre les containers
|
||||
echo "📋 Copie des fichiers en cours..."
|
||||
|
||||
# Approche directe: utiliser incus copy pour copier directement entre containers
|
||||
echo "📤 Transfert direct entre containers..."
|
||||
# Nettoyer le dossier de destination
|
||||
ssh -i $HOST_KEY -p $HOST_PORT $HOST_USER@$HOST_IP "incus exec $DEST_CONTAINER --project $PROJECT -- rm -rf $API_PATH"
|
||||
ssh -i $HOST_KEY -p $HOST_PORT $HOST_USER@$HOST_IP "incus exec $DEST_CONTAINER --project $PROJECT -- mkdir -p $API_PATH"
|
||||
# Nettoyage sélectif : supprimer seulement le code, pas les données (logs et uploads)
|
||||
echo "🧹 Nettoyage sélectif (préservation de logs et uploads)..."
|
||||
ssh -i $HOST_KEY -p $HOST_PORT $HOST_USER@$HOST_IP "incus exec $DEST_CONTAINER --project $PROJECT -- find $API_PATH -mindepth 1 -maxdepth 1 ! -name 'uploads' ! -name 'logs' -exec rm -rf {} \;"
|
||||
|
||||
# Copier directement du container source vers le container destination
|
||||
ssh -i $HOST_KEY -p $HOST_PORT $HOST_USER@$HOST_IP "incus exec $SOURCE_CONTAINER --project $PROJECT -- tar -cf - -C $API_PATH . | incus exec $DEST_CONTAINER --project $PROJECT -- tar -xf - -C $API_PATH"
|
||||
# Copier directement du container source vers le container destination (en excluant logs et uploads)
|
||||
echo "📤 Transfert du code (hors logs et uploads)..."
|
||||
ssh -i $HOST_KEY -p $HOST_PORT $HOST_USER@$HOST_IP "incus exec $SOURCE_CONTAINER --project $PROJECT -- tar -cf - -C $API_PATH --exclude='uploads' --exclude='logs' . | incus exec $DEST_CONTAINER --project $PROJECT -- tar -xf - -C $API_PATH"
|
||||
if [ $? -ne 0 ]; then
|
||||
echo "❌ Erreur lors du transfert direct entre containers"
|
||||
echo "❌ Erreur lors du transfert entre containers"
|
||||
echo "⚠️ Tentative de restauration de la sauvegarde..."
|
||||
# Vérifier si la sauvegarde existe
|
||||
ssh -i $HOST_KEY -p $HOST_PORT $HOST_USER@$HOST_IP "incus exec $DEST_CONTAINER --project $PROJECT -- test -d $BACKUP_DIR"
|
||||
if [ $? -eq 0 ]; then
|
||||
# La sauvegarde existe, la restaurer
|
||||
ssh -i $HOST_KEY -p $HOST_PORT $HOST_USER@$HOST_IP "incus exec $DEST_CONTAINER --project $PROJECT -- rm -rf $API_PATH"
|
||||
ssh -i $HOST_KEY -p $HOST_PORT $HOST_USER@$HOST_IP "incus exec $DEST_CONTAINER --project $PROJECT -- cp -r $BACKUP_DIR $API_PATH"
|
||||
echo "✅ Restauration réussie"
|
||||
else
|
||||
@@ -86,21 +95,7 @@ if [ $? -ne 0 ]; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Pas de fichiers temporaires à nettoyer avec cette approche
|
||||
|
||||
# Restaurer le dossier logs
|
||||
echo "📋 Restauration du dossier logs..."
|
||||
# Vérifier si la sauvegarde des logs existe
|
||||
ssh -i $HOST_KEY -p $HOST_PORT $HOST_USER@$HOST_IP "incus exec $DEST_CONTAINER --project $PROJECT -- test -d /tmp/geosector_logs_backup/logs"
|
||||
if [ $? -eq 0 ]; then
|
||||
# La sauvegarde des logs existe, la restaurer
|
||||
ssh -i $HOST_KEY -p $HOST_PORT $HOST_USER@$HOST_IP "incus exec $DEST_CONTAINER --project $PROJECT -- mkdir -p $API_PATH/logs"
|
||||
ssh -i $HOST_KEY -p $HOST_PORT $HOST_USER@$HOST_IP "incus exec $DEST_CONTAINER --project $PROJECT -- cp -r /tmp/geosector_logs_backup/logs/* $API_PATH/logs/"
|
||||
ssh -i $HOST_KEY -p $HOST_PORT $HOST_USER@$HOST_IP "incus exec $DEST_CONTAINER --project $PROJECT -- rm -rf /tmp/geosector_logs_backup"
|
||||
echo "✅ Dossier logs restauré"
|
||||
else
|
||||
echo "⚠️ Aucune sauvegarde de logs à restaurer"
|
||||
fi
|
||||
echo "✅ Code transféré avec succès (logs et uploads préservés)"
|
||||
|
||||
# Changer le propriétaire et les permissions des fichiers
|
||||
echo "👤 Application des droits et permissions pour tous les fichiers..."
|
||||
@@ -128,8 +123,34 @@ else
|
||||
echo "⚠️ Le dossier logs n'existe pas"
|
||||
fi
|
||||
|
||||
# Vérifier et corriger les permissions du dossier uploads s'il existe
|
||||
ssh -i $HOST_KEY -p $HOST_PORT $HOST_USER@$HOST_IP "incus exec $DEST_CONTAINER --project $PROJECT -- test -d $API_PATH/uploads"
|
||||
if [ $? -eq 0 ]; then
|
||||
# S'assurer que uploads a les bonnes permissions
|
||||
ssh -i $HOST_KEY -p $HOST_PORT $HOST_USER@$HOST_IP "incus exec $DEST_CONTAINER --project $PROJECT -- chown -R nginx:nobody $API_PATH/uploads"
|
||||
ssh -i $HOST_KEY -p $HOST_PORT $HOST_USER@$HOST_IP "incus exec $DEST_CONTAINER --project $PROJECT -- chmod -R 775 $API_PATH/uploads"
|
||||
ssh -i $HOST_KEY -p $HOST_PORT $HOST_USER@$HOST_IP "incus exec $DEST_CONTAINER --project $PROJECT -- find $API_PATH/uploads -type f -exec chmod 664 {} \;"
|
||||
echo "✅ Droits vérifiés pour le dossier uploads (nginx:nginx avec permissions 775)"
|
||||
else
|
||||
# Créer le dossier uploads s'il n'existe pas
|
||||
ssh -i $HOST_KEY -p $HOST_PORT $HOST_USER@$HOST_IP "incus exec $DEST_CONTAINER --project $PROJECT -- mkdir -p $API_PATH/uploads"
|
||||
ssh -i $HOST_KEY -p $HOST_PORT $HOST_USER@$HOST_IP "incus exec $DEST_CONTAINER --project $PROJECT -- chown -R nginx:nobody $API_PATH/uploads"
|
||||
ssh -i $HOST_KEY -p $HOST_PORT $HOST_USER@$HOST_IP "incus exec $DEST_CONTAINER --project $PROJECT -- chmod -R 775 $API_PATH/uploads"
|
||||
ssh -i $HOST_KEY -p $HOST_PORT $HOST_USER@$HOST_IP "incus exec $DEST_CONTAINER --project $PROJECT -- find $API_PATH/uploads -type f -exec chmod 664 {} \;"
|
||||
echo "✅ Dossier uploads créé avec les bonnes permissions (nginx:nginx avec permissions 775/664)"
|
||||
fi
|
||||
|
||||
echo "✅ Propriétaire et permissions appliqués avec succès"
|
||||
|
||||
# Mise à jour des dépendances Composer
|
||||
echo "📦 Mise à jour des dépendances Composer sur $DEST_CONTAINER..."
|
||||
ssh -i $HOST_KEY -p $HOST_PORT $HOST_USER@$HOST_IP "incus exec $DEST_CONTAINER --project $PROJECT -- bash -c 'cd $API_PATH && composer update --no-dev --optimize-autoloader'" > /dev/null 2>&1
|
||||
if [ $? -eq 0 ]; then
|
||||
echo "✅ Dépendances Composer mises à jour avec succès"
|
||||
else
|
||||
echo "⚠️ Composer non disponible ou échec, poursuite sans mise à jour des dépendances"
|
||||
fi
|
||||
|
||||
# Vérifier la copie
|
||||
echo "✅ Vérification de la copie..."
|
||||
ssh -i $HOST_KEY -p $HOST_PORT $HOST_USER@$HOST_IP "incus exec $DEST_CONTAINER --project $PROJECT -- test -d $API_PATH"
|
||||
@@ -139,6 +160,8 @@ else
|
||||
echo "❌ Erreur: Le dossier API n'a pas été copié correctement"
|
||||
fi
|
||||
|
||||
echo "✅ Opération terminée! L'API a été copiée de $SOURCE_CONTAINER vers $DEST_CONTAINER"
|
||||
echo "📁 Une sauvegarde a été créée dans $BACKUP_DIR sur $DEST_CONTAINER"
|
||||
echo "👤 Les fichiers appartiennent maintenant à l'utilisateur nginx"
|
||||
echo "✅ Livraison vers $ENV_NAME terminée avec succès!"
|
||||
echo "📤 Source: $SOURCE_CONTAINER → Destination: $DEST_CONTAINER"
|
||||
echo "📁 Sauvegarde créée: $BACKUP_DIR sur $DEST_CONTAINER"
|
||||
echo "🔒 Données préservées: logs/ et uploads/ intouchés"
|
||||
echo "👤 Permissions: nginx:nginx (755/644) + logs (nginx:nobody 775/664)"
|
||||
|
||||
28
api/migration_add_departements_contours.sql
Normal file
28
api/migration_add_departements_contours.sql
Normal file
@@ -0,0 +1,28 @@
|
||||
-- Table pour stocker les contours (polygones) des départements
|
||||
CREATE TABLE IF NOT EXISTS `x_departements_contours` (
|
||||
`id` int(11) NOT NULL AUTO_INCREMENT,
|
||||
`code_dept` varchar(3) NOT NULL COMMENT 'Code département (22, 2A, 971...)',
|
||||
`nom_dept` varchar(100) NOT NULL,
|
||||
`contour` POLYGON NOT NULL COMMENT 'Polygone du contour du département',
|
||||
`bbox_min_lat` decimal(10,8) DEFAULT NULL COMMENT 'Latitude min de la bounding box',
|
||||
`bbox_max_lat` decimal(10,8) DEFAULT NULL COMMENT 'Latitude max de la bounding box',
|
||||
`bbox_min_lng` decimal(11,8) DEFAULT NULL COMMENT 'Longitude min de la bounding box',
|
||||
`bbox_max_lng` decimal(11,8) DEFAULT NULL COMMENT 'Longitude max de la bounding box',
|
||||
`created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
`updated_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `idx_code_dept` (`code_dept`),
|
||||
SPATIAL KEY `idx_contour` (`contour`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
-- Index pour améliorer les performances
|
||||
CREATE INDEX idx_dept_bbox ON x_departements_contours (bbox_min_lat, bbox_max_lat, bbox_min_lng, bbox_max_lng);
|
||||
|
||||
-- Exemples d'insertion (à remplacer par les vraies données)
|
||||
-- Les coordonnées doivent être dans l'ordre : longitude latitude
|
||||
-- Le polygone doit être fermé (premier point = dernier point)
|
||||
/*
|
||||
INSERT INTO x_departements_contours (code_dept, nom_dept, contour, bbox_min_lat, bbox_max_lat, bbox_min_lng, bbox_max_lng) VALUES
|
||||
('22', 'Côtes-d\'Armor', ST_GeomFromText('POLYGON((-3.65 48.90, -2.00 48.90, -2.00 48.05, -3.65 48.05, -3.65 48.90))'), 48.05, 48.90, -3.65, -2.00),
|
||||
('29', 'Finistère', ST_GeomFromText('POLYGON((-5.14 48.75, -3.38 48.75, -3.38 47.64, -5.14 47.64, -5.14 48.75))'), 47.64, 48.75, -5.14, -3.38);
|
||||
*/
|
||||
26
api/migration_add_file_category.sql
Executable file
26
api/migration_add_file_category.sql
Executable file
@@ -0,0 +1,26 @@
|
||||
-- Migration pour ajouter la colonne file_category à la table medias
|
||||
-- Date: 2025-06-22
|
||||
-- Description: Ajout du champ file_category pour distinguer les types métier des fichiers
|
||||
|
||||
-- Ajout de la colonne file_category
|
||||
ALTER TABLE `medias`
|
||||
ADD COLUMN `file_category` varchar(50) DEFAULT NULL COMMENT 'Catégorie du fichier (logo, carte, photo, document, etc.)' AFTER `file_type`;
|
||||
|
||||
-- Ajout de l'index pour optimiser les requêtes
|
||||
ALTER TABLE `medias`
|
||||
ADD INDEX `idx_file_category` (`file_category`);
|
||||
|
||||
-- Mise à jour des données existantes avec des catégories par défaut selon le support
|
||||
UPDATE `medias` SET `file_category` = 'document' WHERE `support` = 'entite' AND `file_category` IS NULL;
|
||||
UPDATE `medias` SET `file_category` = 'avatar' WHERE `support` = 'user' AND `file_category` IS NULL;
|
||||
UPDATE `medias` SET `file_category` = 'export' WHERE `support` = 'operation' AND `file_category` IS NULL;
|
||||
UPDATE `medias` SET `file_category` = 'recu' WHERE `support` = 'passage' AND `file_category` IS NULL;
|
||||
|
||||
-- Vérification des modifications
|
||||
SELECT
|
||||
support,
|
||||
file_category,
|
||||
COUNT(*) as count
|
||||
FROM medias
|
||||
GROUP BY support, file_category
|
||||
ORDER BY support, file_category;
|
||||
16
api/migration_add_ope_users_fields.sql
Executable file
16
api/migration_add_ope_users_fields.sql
Executable file
@@ -0,0 +1,16 @@
|
||||
-- Migration pour ajouter les champs utilisateur dans ope_users
|
||||
-- Date: 2025-06-23
|
||||
-- Description: Ajout des champs fk_role, first_name, encrypted_name, sect_name dans ope_users
|
||||
-- pour conserver un historique propre de chaque opération
|
||||
|
||||
USE geo_app;
|
||||
|
||||
-- Ajout des nouvelles colonnes dans ope_users
|
||||
ALTER TABLE ope_users
|
||||
ADD COLUMN fk_role int unsigned DEFAULT 1 AFTER fk_user,
|
||||
ADD COLUMN first_name varchar(45) DEFAULT '' AFTER fk_role,
|
||||
ADD COLUMN encrypted_name varchar(255) DEFAULT '' AFTER first_name,
|
||||
ADD COLUMN sect_name varchar(60) DEFAULT '' AFTER encrypted_name;
|
||||
|
||||
-- Vérification de la structure modifiée
|
||||
DESCRIBE ope_users;
|
||||
29
api/migration_add_sectors_adresses.sql
Normal file
29
api/migration_add_sectors_adresses.sql
Normal file
@@ -0,0 +1,29 @@
|
||||
-- Migration pour ajouter la table sectors_adresses
|
||||
-- Cette table stocke les adresses associées à chaque secteur
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `sectors_adresses` (
|
||||
`id` int(11) NOT NULL AUTO_INCREMENT,
|
||||
`fk_sector` int(11) NOT NULL,
|
||||
`fk_address` bigint(20) NOT NULL COMMENT 'ID de l''adresse dans la base adresses',
|
||||
`numero` varchar(10) DEFAULT NULL,
|
||||
`voie` varchar(255) DEFAULT NULL,
|
||||
`code_postal` varchar(5) DEFAULT NULL,
|
||||
`commune` varchar(100) DEFAULT NULL,
|
||||
`latitude` decimal(10,8) NOT NULL,
|
||||
`longitude` decimal(11,8) NOT NULL,
|
||||
`created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `idx_sector` (`fk_sector`),
|
||||
KEY `idx_address` (`fk_address`),
|
||||
KEY `idx_code_postal` (`code_postal`),
|
||||
KEY `idx_commune` (`commune`),
|
||||
KEY `idx_coords` (`latitude`, `longitude`),
|
||||
CONSTRAINT `fk_sectors_adresses_sector` FOREIGN KEY (`fk_sector`) REFERENCES `ope_sectors` (`id`) ON DELETE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
-- Index pour améliorer les performances des requêtes géographiques
|
||||
CREATE INDEX idx_sectors_adresses_geo ON sectors_adresses (fk_sector, latitude, longitude);
|
||||
|
||||
-- Commentaire sur la table
|
||||
ALTER TABLE `sectors_adresses`
|
||||
COMMENT = 'Table de liaison entre les secteurs et les adresses géographiques contenues dans leurs périmètres';
|
||||
45
api/migrations/add_dept_limitrophes.sql
Normal file
45
api/migrations/add_dept_limitrophes.sql
Normal file
@@ -0,0 +1,45 @@
|
||||
-- Ajout du champ dept_limitrophes dans la table x_departements
|
||||
-- Ce champ contiendra la liste des codes départements limitrophes séparés par des virgules
|
||||
-- Exemple : "22,35,44,56" pour le Morbihan (56)
|
||||
|
||||
ALTER TABLE x_departements
|
||||
ADD COLUMN dept_limitrophes VARCHAR(100) DEFAULT NULL
|
||||
COMMENT 'Liste des codes départements limitrophes séparés par des virgules'
|
||||
AFTER libelle;
|
||||
|
||||
-- Exemples de mise à jour pour quelques départements bretons
|
||||
-- À compléter avec tous les départements
|
||||
|
||||
-- Côtes-d'Armor (22) : limitrophe avec 29, 35, 56
|
||||
UPDATE x_departements SET dept_limitrophes = '29,35,56' WHERE code = '22';
|
||||
|
||||
-- Finistère (29) : limitrophe avec 22, 56
|
||||
UPDATE x_departements SET dept_limitrophes = '22,56' WHERE code = '29';
|
||||
|
||||
-- Ille-et-Vilaine (35) : limitrophe avec 22, 44, 49, 50, 53, 56
|
||||
UPDATE x_departements SET dept_limitrophes = '22,44,49,50,53,56' WHERE code = '35';
|
||||
|
||||
-- Morbihan (56) : limitrophe avec 22, 29, 35, 44
|
||||
UPDATE x_departements SET dept_limitrophes = '22,29,35,44' WHERE code = '56';
|
||||
|
||||
-- Loire-Atlantique (44) : limitrophe avec 35, 49, 56, 85
|
||||
UPDATE x_departements SET dept_limitrophes = '35,49,56,85' WHERE code = '44';
|
||||
|
||||
-- Hauts-de-France
|
||||
-- Aisne (02) : limitrophe avec 08, 51, 59, 60, 77, 80
|
||||
UPDATE x_departements SET dept_limitrophes = '08,51,59,60,77,80' WHERE code = '02';
|
||||
|
||||
-- Nord (59) : limitrophe avec 02, 62, 80 (+ frontière Belgique)
|
||||
UPDATE x_departements SET dept_limitrophes = '02,62,80' WHERE code = '59';
|
||||
|
||||
-- Oise (60) : limitrophe avec 02, 27, 76, 77, 80, 95
|
||||
UPDATE x_departements SET dept_limitrophes = '02,27,76,77,80,95' WHERE code = '60';
|
||||
|
||||
-- Pas-de-Calais (62) : limitrophe avec 59, 80
|
||||
UPDATE x_departements SET dept_limitrophes = '59,80' WHERE code = '62';
|
||||
|
||||
-- Somme (80) : limitrophe avec 02, 27, 59, 60, 62, 76
|
||||
UPDATE x_departements SET dept_limitrophes = '02,27,59,60,62,76' WHERE code = '80';
|
||||
|
||||
-- Note : Ces données sont à compléter pour tous les départements français
|
||||
-- Source recommandée : données INSEE ou IGN pour la liste complète et exacte
|
||||
55
api/migrations/integrate_contours_to_departements.sql
Normal file
55
api/migrations/integrate_contours_to_departements.sql
Normal file
@@ -0,0 +1,55 @@
|
||||
-- Script pour intégrer les données de x_departements_contours dans x_departements
|
||||
-- Ce script ajoute une colonne contour à x_departements et y copie les données
|
||||
-- Note : Les colonnes bbox_* ne sont pas migrées car elles ne sont pas utilisées dans l'API
|
||||
|
||||
-- 1. Ajouter la colonne contour à x_departements si elle n'existe pas déjà
|
||||
ALTER TABLE x_departements
|
||||
ADD COLUMN IF NOT EXISTS contour GEOMETRY DEFAULT NULL
|
||||
COMMENT 'Contour géographique du département'
|
||||
AFTER dept_limitrophes;
|
||||
|
||||
-- 2. Mettre à jour x_departements avec les contours depuis x_departements_contours
|
||||
-- On copie directement les contours non NULL
|
||||
UPDATE x_departements d
|
||||
INNER JOIN x_departements_contours dc ON d.code = dc.code_dept
|
||||
SET d.contour = dc.contour
|
||||
WHERE dc.contour IS NOT NULL;
|
||||
|
||||
-- 3. Vérifier les départements sans contour (hors DOM-TOM et Corse historique)
|
||||
SELECT
|
||||
d.code,
|
||||
d.libelle,
|
||||
CASE
|
||||
WHEN d.code IN ('20', '971', '972', '973', '974', '975', '976') THEN 'DOM-TOM ou Corse historique - Normal'
|
||||
WHEN d.contour IS NULL THEN 'Pas de contour'
|
||||
ELSE 'OK'
|
||||
END as statut
|
||||
FROM x_departements d
|
||||
WHERE d.contour IS NULL
|
||||
AND d.code NOT IN ('20', '971', '972', '973', '974', '975', '976')
|
||||
ORDER BY d.code;
|
||||
|
||||
-- 4. Créer l'index spatial
|
||||
-- Les valeurs NULL sont autorisées dans un index spatial MySQL
|
||||
ALTER TABLE x_departements
|
||||
ADD SPATIAL INDEX idx_contour (contour);
|
||||
|
||||
-- 5. Vérifier le nombre de départements mis à jour
|
||||
SELECT
|
||||
COUNT(*) as total_departements,
|
||||
SUM(CASE WHEN contour IS NOT NULL THEN 1 ELSE 0 END) as departements_avec_contour,
|
||||
SUM(CASE WHEN contour IS NULL THEN 1 ELSE 0 END) as departements_sans_contour
|
||||
FROM x_departements;
|
||||
|
||||
-- 6. Lister les départements qui n'ont pas de contour (s'il y en a)
|
||||
SELECT code, libelle
|
||||
FROM x_departements
|
||||
WHERE contour IS NULL
|
||||
ORDER BY code;
|
||||
|
||||
-- 7. Optionnel : Après vérification, vous pouvez supprimer la table x_departements_contours
|
||||
-- ATTENTION : Ne décommentez cette ligne qu'après avoir vérifié que toutes les données sont bien migrées
|
||||
-- DROP TABLE IF EXISTS x_departements_contours;
|
||||
|
||||
-- 8. Mettre à jour les statistiques de la table pour optimiser les requêtes spatiales
|
||||
ANALYZE TABLE x_departements;
|
||||
131
api/migrations/update_all_dept_limitrophes.sql
Normal file
131
api/migrations/update_all_dept_limitrophes.sql
Normal file
@@ -0,0 +1,131 @@
|
||||
-- Mise à jour complète des départements limitrophes pour tous les départements français
|
||||
-- Format : liste des codes départements séparés par des virgules
|
||||
|
||||
-- Auvergne-Rhône-Alpes
|
||||
UPDATE x_departements SET dept_limitrophes = '07,15,43,48' WHERE code = '01'; -- Ain : Ardèche, Cantal, Haute-Loire, Lozère
|
||||
UPDATE x_departements SET dept_limitrophes = '04,05,26,84' WHERE code = '03'; -- Allier : Alpes-de-Haute-Provence, Hautes-Alpes, Drôme, Vaucluse
|
||||
UPDATE x_departements SET dept_limitrophes = '01,26,30,43,48,84' WHERE code = '07'; -- Ardèche : Ain, Drôme, Gard, Haute-Loire, Lozère, Vaucluse
|
||||
UPDATE x_departements SET dept_limitrophes = '12,19,43,46,48' WHERE code = '15'; -- Cantal : Aveyron, Corrèze, Haute-Loire, Lot, Lozère
|
||||
UPDATE x_departements SET dept_limitrophes = '04,05,73,84' WHERE code = '26'; -- Drôme : Alpes-de-Haute-Provence, Hautes-Alpes, Savoie, Vaucluse
|
||||
UPDATE x_departements SET dept_limitrophes = '05,06,13,73' WHERE code = '38'; -- Isère : Hautes-Alpes, Alpes-Maritimes, Bouches-du-Rhône, Savoie
|
||||
UPDATE x_departements SET dept_limitrophes = '07,15,19,43,48,63' WHERE code = '42'; -- Loire : Ardèche, Cantal, Corrèze, Haute-Loire, Lozère, Puy-de-Dôme
|
||||
UPDATE x_departements SET dept_limitrophes = '01,07,15,42,48,63' WHERE code = '43'; -- Haute-Loire : Ain, Ardèche, Cantal, Loire, Lozère, Puy-de-Dôme
|
||||
UPDATE x_departements SET dept_limitrophes = '03,15,23,42,43' WHERE code = '63'; -- Puy-de-Dôme : Allier, Cantal, Creuse, Loire, Haute-Loire
|
||||
UPDATE x_departements SET dept_limitrophes = '01,38,39,71' WHERE code = '69'; -- Rhône : Ain, Isère, Jura, Saône-et-Loire
|
||||
UPDATE x_departements SET dept_limitrophes = '01,25,38,39,74' WHERE code = '73'; -- Savoie : Ain, Doubs, Isère, Jura, Haute-Savoie
|
||||
UPDATE x_departements SET dept_limitrophes = '01,73' WHERE code = '74'; -- Haute-Savoie : Ain, Savoie (+ frontières Suisse et Italie)
|
||||
|
||||
-- Bourgogne-Franche-Comté
|
||||
UPDATE x_departements SET dept_limitrophes = '10,45,52,58,77,89' WHERE code = '21'; -- Côte-d'Or : Aube, Loiret, Haute-Marne, Nièvre, Seine-et-Marne, Yonne
|
||||
UPDATE x_departements SET dept_limitrophes = '39,68,70,73,90' WHERE code = '25'; -- Doubs : Jura, Haut-Rhin, Haute-Saône, Savoie, Territoire de Belfort (+ frontière Suisse)
|
||||
UPDATE x_departements SET dept_limitrophes = '01,25,69,70,71,73' WHERE code = '39'; -- Jura : Ain, Doubs, Rhône, Haute-Saône, Saône-et-Loire, Savoie
|
||||
UPDATE x_departements SET dept_limitrophes = '03,18,21,45,71,89' WHERE code = '58'; -- Nièvre : Allier, Cher, Côte-d'Or, Loiret, Saône-et-Loire, Yonne
|
||||
UPDATE x_departements SET dept_limitrophes = '21,25,39,52,88' WHERE code = '70'; -- Haute-Saône : Côte-d'Or, Doubs, Jura, Haute-Marne, Vosges
|
||||
UPDATE x_departements SET dept_limitrophes = '01,03,21,39,58,69' WHERE code = '71'; -- Saône-et-Loire : Ain, Allier, Côte-d'Or, Jura, Nièvre, Rhône
|
||||
UPDATE x_departements SET dept_limitrophes = '10,21,45,58,77' WHERE code = '89'; -- Yonne : Aube, Côte-d'Or, Loiret, Nièvre, Seine-et-Marne
|
||||
UPDATE x_departements SET dept_limitrophes = '25,68,70' WHERE code = '90'; -- Territoire de Belfort : Doubs, Haut-Rhin, Haute-Saône
|
||||
|
||||
-- Bretagne
|
||||
UPDATE x_departements SET dept_limitrophes = '29,35,56' WHERE code = '22'; -- Côtes-d'Armor
|
||||
UPDATE x_departements SET dept_limitrophes = '22,56' WHERE code = '29'; -- Finistère
|
||||
UPDATE x_departements SET dept_limitrophes = '22,44,49,50,53,56' WHERE code = '35'; -- Ille-et-Vilaine
|
||||
UPDATE x_departements SET dept_limitrophes = '22,29,35,44' WHERE code = '56'; -- Morbihan
|
||||
|
||||
-- Centre-Val de Loire
|
||||
UPDATE x_departements SET dept_limitrophes = '03,23,36,41,58' WHERE code = '18'; -- Cher
|
||||
UPDATE x_departements SET dept_limitrophes = '27,37,41,45,61,72,78,91' WHERE code = '28'; -- Eure-et-Loir
|
||||
UPDATE x_departements SET dept_limitrophes = '18,23,37,41,86,87' WHERE code = '36'; -- Indre
|
||||
UPDATE x_departements SET dept_limitrophes = '36,41,49,72,86' WHERE code = '37'; -- Indre-et-Loire
|
||||
UPDATE x_departements SET dept_limitrophes = '18,28,36,37,45,72' WHERE code = '41'; -- Loir-et-Cher
|
||||
UPDATE x_departements SET dept_limitrophes = '18,21,28,41,58,77,89,91' WHERE code = '45'; -- Loiret
|
||||
|
||||
-- Corse
|
||||
UPDATE x_departements SET dept_limitrophes = '2B' WHERE code = '2A'; -- Corse-du-Sud : Haute-Corse
|
||||
UPDATE x_departements SET dept_limitrophes = '2A' WHERE code = '2B'; -- Haute-Corse : Corse-du-Sud
|
||||
|
||||
-- Grand Est
|
||||
UPDATE x_departements SET dept_limitrophes = '02,51,55' WHERE code = '08'; -- Ardennes (+ frontière Belgique)
|
||||
UPDATE x_departements SET dept_limitrophes = '21,51,52,77,89' WHERE code = '10'; -- Aube
|
||||
UPDATE x_departements SET dept_limitrophes = '02,08,10,52,77' WHERE code = '51'; -- Marne
|
||||
UPDATE x_departements SET dept_limitrophes = '10,21,51,55,70,88' WHERE code = '52'; -- Haute-Marne
|
||||
UPDATE x_departements SET dept_limitrophes = '55,57,88' WHERE code = '54'; -- Meurthe-et-Moselle
|
||||
UPDATE x_departements SET dept_limitrophes = '08,52,54,57' WHERE code = '55'; -- Meuse (+ frontière Belgique)
|
||||
UPDATE x_departements SET dept_limitrophes = '54,55,67' WHERE code = '57'; -- Moselle (+ frontières Luxembourg et Allemagne)
|
||||
UPDATE x_departements SET dept_limitrophes = '25,57,68,88,90' WHERE code = '67'; -- Bas-Rhin (+ frontière Allemagne)
|
||||
UPDATE x_departements SET dept_limitrophes = '67,70,88,90' WHERE code = '68'; -- Haut-Rhin (+ frontières Allemagne et Suisse)
|
||||
UPDATE x_departements SET dept_limitrophes = '52,54,67,68,70' WHERE code = '88'; -- Vosges
|
||||
|
||||
-- Hauts-de-France
|
||||
UPDATE x_departements SET dept_limitrophes = '08,51,59,60,77,80' WHERE code = '02'; -- Aisne
|
||||
UPDATE x_departements SET dept_limitrophes = '02,62,80' WHERE code = '59'; -- Nord (+ frontière Belgique)
|
||||
UPDATE x_departements SET dept_limitrophes = '02,27,76,77,80,95' WHERE code = '60'; -- Oise
|
||||
UPDATE x_departements SET dept_limitrophes = '59,80' WHERE code = '62'; -- Pas-de-Calais (+ frontière Belgique)
|
||||
UPDATE x_departements SET dept_limitrophes = '02,27,59,60,62,76' WHERE code = '80'; -- Somme
|
||||
|
||||
-- Île-de-France
|
||||
UPDATE x_departements SET dept_limitrophes = '92,93,94' WHERE code = '75'; -- Paris
|
||||
UPDATE x_departements SET dept_limitrophes = '02,10,45,51,60,89,91,93,94,95' WHERE code = '77'; -- Seine-et-Marne
|
||||
UPDATE x_departements SET dept_limitrophes = '27,28,91,92,95' WHERE code = '78'; -- Yvelines
|
||||
UPDATE x_departements SET dept_limitrophes = '28,45,77,78,92,94' WHERE code = '91'; -- Essonne
|
||||
UPDATE x_departements SET dept_limitrophes = '75,78,91,93,94,95' WHERE code = '92'; -- Hauts-de-Seine
|
||||
UPDATE x_departements SET dept_limitrophes = '75,77,92,94,95' WHERE code = '93'; -- Seine-Saint-Denis
|
||||
UPDATE x_departements SET dept_limitrophes = '75,77,91,92,93' WHERE code = '94'; -- Val-de-Marne
|
||||
UPDATE x_departements SET dept_limitrophes = '27,60,77,78,92,93' WHERE code = '95'; -- Val-d'Oise
|
||||
|
||||
-- Normandie
|
||||
UPDATE x_departements SET dept_limitrophes = '27,50,53,61,72' WHERE code = '14'; -- Calvados
|
||||
UPDATE x_departements SET dept_limitrophes = '14,28,60,61,72,76,78,95' WHERE code = '27'; -- Eure
|
||||
UPDATE x_departements SET dept_limitrophes = '14,35,53,61' WHERE code = '50'; -- Manche
|
||||
UPDATE x_departements SET dept_limitrophes = '14,27,28,35,41,50,53,72' WHERE code = '61'; -- Orne
|
||||
UPDATE x_departements SET dept_limitrophes = '27,60,80' WHERE code = '76'; -- Seine-Maritime
|
||||
|
||||
-- Nouvelle-Aquitaine
|
||||
UPDATE x_departements SET dept_limitrophes = '17,24,33,87' WHERE code = '16'; -- Charente
|
||||
UPDATE x_departements SET dept_limitrophes = '16,33,79,85' WHERE code = '17'; -- Charente-Maritime
|
||||
UPDATE x_departements SET dept_limitrophes = '15,23,24,46,87' WHERE code = '19'; -- Corrèze
|
||||
UPDATE x_departements SET dept_limitrophes = '18,19,36,86,87' WHERE code = '23'; -- Creuse
|
||||
UPDATE x_departements SET dept_limitrophes = '16,19,33,46,47,87' WHERE code = '24'; -- Dordogne
|
||||
UPDATE x_departements SET dept_limitrophes = '16,17,24,40,47' WHERE code = '33'; -- Gironde
|
||||
UPDATE x_departements SET dept_limitrophes = '32,33,47,64,65' WHERE code = '40'; -- Landes
|
||||
UPDATE x_departements SET dept_limitrophes = '24,32,40,46,82' WHERE code = '47'; -- Lot-et-Garonne
|
||||
UPDATE x_departements SET dept_limitrophes = '40,65' WHERE code = '64'; -- Pyrénées-Atlantiques (+ frontière Espagne)
|
||||
UPDATE x_departements SET dept_limitrophes = '17,49,85,86' WHERE code = '79'; -- Deux-Sèvres
|
||||
UPDATE x_departements SET dept_limitrophes = '16,23,36,37,79' WHERE code = '86'; -- Vienne
|
||||
UPDATE x_departements SET dept_limitrophes = '16,19,23,24,36,86' WHERE code = '87'; -- Haute-Vienne
|
||||
|
||||
-- Occitanie
|
||||
UPDATE x_departements SET dept_limitrophes = '11,31,66' WHERE code = '09'; -- Ariège : Aude, Haute-Garonne, Pyrénées-Orientales (+ frontières Espagne et Andorre)
|
||||
UPDATE x_departements SET dept_limitrophes = '09,31,66' WHERE code = '11'; -- Aude
|
||||
UPDATE x_departements SET dept_limitrophes = '15,30,34,46,48,81,82' WHERE code = '12'; -- Aveyron
|
||||
UPDATE x_departements SET dept_limitrophes = '07,12,34,48,84' WHERE code = '30'; -- Gard
|
||||
UPDATE x_departements SET dept_limitrophes = '09,32,65,82' WHERE code = '31'; -- Haute-Garonne : Ariège, Gers, Hautes-Pyrénées, Tarn-et-Garonne (+ frontière Espagne)
|
||||
UPDATE x_departements SET dept_limitrophes = '31,40,47,65,82' WHERE code = '32'; -- Gers
|
||||
UPDATE x_departements SET dept_limitrophes = '11,12,30' WHERE code = '34'; -- Hérault
|
||||
UPDATE x_departements SET dept_limitrophes = '12,15,19,24,47,81,82' WHERE code = '46'; -- Lot
|
||||
UPDATE x_departements SET dept_limitrophes = '07,12,15,30,43' WHERE code = '48'; -- Lozère
|
||||
UPDATE x_departements SET dept_limitrophes = '31,32,40,64' WHERE code = '65'; -- Hautes-Pyrénées (+ frontière Espagne)
|
||||
UPDATE x_departements SET dept_limitrophes = '09,11' WHERE code = '66'; -- Pyrénées-Orientales (+ frontière Espagne)
|
||||
UPDATE x_departements SET dept_limitrophes = '12,34,46,82' WHERE code = '81'; -- Tarn
|
||||
UPDATE x_departements SET dept_limitrophes = '12,31,32,46,47,81' WHERE code = '82'; -- Tarn-et-Garonne
|
||||
|
||||
-- Pays de la Loire
|
||||
UPDATE x_departements SET dept_limitrophes = '35,49,56,85' WHERE code = '44'; -- Loire-Atlantique
|
||||
UPDATE x_departements SET dept_limitrophes = '35,37,44,53,72,79,85,86' WHERE code = '49'; -- Maine-et-Loire
|
||||
UPDATE x_departements SET dept_limitrophes = '14,35,49,50,61,72' WHERE code = '53'; -- Mayenne
|
||||
UPDATE x_departements SET dept_limitrophes = '14,27,28,37,41,49,53,61' WHERE code = '72'; -- Sarthe
|
||||
UPDATE x_departements SET dept_limitrophes = '17,44,49,79' WHERE code = '85'; -- Vendée
|
||||
|
||||
-- Provence-Alpes-Côte d'Azur
|
||||
UPDATE x_departements SET dept_limitrophes = '05,06,26,83,84' WHERE code = '04'; -- Alpes-de-Haute-Provence
|
||||
UPDATE x_departements SET dept_limitrophes = '04,26,38,73' WHERE code = '05'; -- Hautes-Alpes (+ frontière Italie)
|
||||
UPDATE x_departements SET dept_limitrophes = '04,83' WHERE code = '06'; -- Alpes-Maritimes (+ frontières Italie et Monaco)
|
||||
UPDATE x_departements SET dept_limitrophes = '30,83,84' WHERE code = '13'; -- Bouches-du-Rhône
|
||||
UPDATE x_departements SET dept_limitrophes = '04,06,13,84' WHERE code = '83'; -- Var
|
||||
UPDATE x_departements SET dept_limitrophes = '04,07,13,26,30,83' WHERE code = '84'; -- Vaucluse
|
||||
|
||||
-- Départements et régions d'outre-mer (pas de limitrophes terrestres)
|
||||
UPDATE x_departements SET dept_limitrophes = NULL WHERE code = '971'; -- Guadeloupe
|
||||
UPDATE x_departements SET dept_limitrophes = NULL WHERE code = '972'; -- Martinique
|
||||
UPDATE x_departements SET dept_limitrophes = NULL WHERE code = '973'; -- Guyane (frontières Brésil et Suriname)
|
||||
UPDATE x_departements SET dept_limitrophes = NULL WHERE code = '974'; -- La Réunion
|
||||
UPDATE x_departements SET dept_limitrophes = NULL WHERE code = '976'; -- Mayotte
|
||||
0
api/scripts/README.md
Normal file → Executable file
0
api/scripts/README.md
Normal file → Executable file
33
api/scripts/check_geometry_validity.sql
Normal file
33
api/scripts/check_geometry_validity.sql
Normal file
@@ -0,0 +1,33 @@
|
||||
-- Script de diagnostic pour vérifier les problèmes de géométrie dans x_departements_contours
|
||||
|
||||
-- 1. Vérifier les contours NULL
|
||||
SELECT 'Contours NULL:' as diagnostic;
|
||||
SELECT code_dept, nom_dept
|
||||
FROM x_departements_contours
|
||||
WHERE contour IS NULL;
|
||||
|
||||
-- 2. Vérifier les types de géométrie et si elles sont vides
|
||||
SELECT 'Types de géométrie:' as diagnostic;
|
||||
SELECT
|
||||
code_dept,
|
||||
nom_dept,
|
||||
ST_GeometryType(contour) as geometry_type,
|
||||
ST_IsEmpty(contour) as is_empty
|
||||
FROM x_departements_contours
|
||||
WHERE contour IS NOT NULL;
|
||||
|
||||
-- 3. Statistiques générales
|
||||
SELECT 'Statistiques:' as diagnostic;
|
||||
SELECT
|
||||
COUNT(*) as total,
|
||||
SUM(CASE WHEN contour IS NULL THEN 1 ELSE 0 END) as contours_null,
|
||||
SUM(CASE WHEN contour IS NOT NULL THEN 1 ELSE 0 END) as contours_non_null
|
||||
FROM x_departements_contours;
|
||||
|
||||
-- 4. Lister spécifiquement les DOM-TOM et Corse
|
||||
SELECT 'DOM-TOM et Corse:' as diagnostic;
|
||||
SELECT code_dept, nom_dept,
|
||||
CASE WHEN contour IS NULL THEN 'NULL' ELSE 'OK' END as contour_status
|
||||
FROM x_departements_contours
|
||||
WHERE code_dept IN ('20', '2A', '2B', '971', '972', '973', '974', '975', '976')
|
||||
ORDER BY code_dept;
|
||||
0
api/scripts/config.php
Normal file → Executable file
0
api/scripts/config.php
Normal file → Executable file
32
api/scripts/create_addresses_users.sql
Normal file
32
api/scripts/create_addresses_users.sql
Normal file
@@ -0,0 +1,32 @@
|
||||
-- Script de création des utilisateurs pour la base de données des adresses
|
||||
-- À exécuter sur chaque serveur MariaDB (dva-maria, rca-maria, pra-maria)
|
||||
|
||||
-- Créer l'utilisateur avec accès depuis l'IP du container API correspondant
|
||||
-- IMPORTANT: Remplacer 'API_CONTAINER_IP' par l'IP réelle du container API
|
||||
|
||||
-- Pour l'environnement DEV (dva-maria)
|
||||
-- Si l'API est dans le container dva-api avec l'IP 13.23.33.45 par exemple :
|
||||
CREATE USER IF NOT EXISTS 'adresses_user'@'13.23.33.45' IDENTIFIED BY 'd66,AdrGeo.User';
|
||||
GRANT SELECT ON adresses.* TO 'adresses_user'@'13.23.33.45';
|
||||
|
||||
-- Pour l'environnement RECETTE (rca-maria)
|
||||
-- Si l'API est dans le container rca-api avec l'IP 13.23.33.35 par exemple :
|
||||
CREATE USER IF NOT EXISTS 'adresses_user'@'13.23.33.35' IDENTIFIED BY 'd66,AdrGeo.User';
|
||||
GRANT SELECT ON adresses.* TO 'adresses_user'@'13.23.33.35';
|
||||
|
||||
-- Pour l'environnement PROD (pra-maria)
|
||||
-- Si l'API est dans le container pra-api avec l'IP 13.23.33.25 par exemple :
|
||||
CREATE USER IF NOT EXISTS 'adresses_user'@'13.23.33.25' IDENTIFIED BY 'd66,AdrGeo.User';
|
||||
GRANT SELECT ON adresses.* TO 'adresses_user'@'13.23.33.25';
|
||||
|
||||
-- Alternative : Créer un utilisateur accessible depuis tout le sous-réseau
|
||||
-- ATTENTION : Moins sécurisé, à utiliser uniquement si les containers sont dans un réseau privé isolé
|
||||
CREATE USER IF NOT EXISTS 'adresses_user'@'13.23.33.%' IDENTIFIED BY 'd66,AdrGeo.User';
|
||||
GRANT SELECT ON adresses.* TO 'adresses_user'@'13.23.33.%';
|
||||
|
||||
-- Appliquer les privilèges
|
||||
FLUSH PRIVILEGES;
|
||||
|
||||
-- Vérifier la création
|
||||
SELECT user, host FROM mysql.user WHERE user = 'adresses_user';
|
||||
SHOW GRANTS FOR 'adresses_user'@'13.23.33.%';
|
||||
41
api/scripts/create_addresses_users_by_env.sql
Normal file
41
api/scripts/create_addresses_users_by_env.sql
Normal file
@@ -0,0 +1,41 @@
|
||||
-- Script de création des utilisateurs pour la base de données des adresses
|
||||
-- Avec segmentation par environnement basée sur les plages d'IPs
|
||||
|
||||
-- ===================================
|
||||
-- DÉVELOPPEMENT (dva-maria)
|
||||
-- IPs autorisées : 13.23.33.40-49
|
||||
-- ===================================
|
||||
CREATE USER IF NOT EXISTS 'adresses_user'@'13.23.33.4%' IDENTIFIED BY 'd66,AdrGeo.User';
|
||||
GRANT SELECT ON adresses.* TO 'adresses_user'@'13.23.33.4%';
|
||||
|
||||
-- Aussi créer un accès localhost pour les tests directs
|
||||
CREATE USER IF NOT EXISTS 'adresses_user'@'localhost' IDENTIFIED BY 'd66,AdrGeo.User';
|
||||
GRANT SELECT ON adresses.* TO 'adresses_user'@'localhost';
|
||||
|
||||
-- ===================================
|
||||
-- RECETTE (rca-maria)
|
||||
-- IPs autorisées : 13.23.33.30-39
|
||||
-- ===================================
|
||||
CREATE USER IF NOT EXISTS 'adresses_user'@'13.23.33.3%' IDENTIFIED BY 'd66,AdrGeo.User';
|
||||
GRANT SELECT ON adresses.* TO 'adresses_user'@'13.23.33.3%';
|
||||
|
||||
-- Aussi créer un accès localhost pour les tests directs
|
||||
CREATE USER IF NOT EXISTS 'adresses_user'@'localhost' IDENTIFIED BY 'd66,AdrGeo.User';
|
||||
GRANT SELECT ON adresses.* TO 'adresses_user'@'localhost';
|
||||
|
||||
-- ===================================
|
||||
-- PRODUCTION (pra-maria)
|
||||
-- IPs autorisées : 13.23.33.20-29
|
||||
-- ===================================
|
||||
CREATE USER IF NOT EXISTS 'adresses_user'@'13.23.33.2%' IDENTIFIED BY 'd66,AdrGeo.User';
|
||||
GRANT SELECT ON adresses.* TO 'adresses_user'@'13.23.33.2%';
|
||||
|
||||
-- Aussi créer un accès localhost pour les tests directs
|
||||
CREATE USER IF NOT EXISTS 'adresses_user'@'localhost' IDENTIFIED BY 'd66,AdrGeo.User';
|
||||
GRANT SELECT ON adresses.* TO 'adresses_user'@'localhost';
|
||||
|
||||
-- Appliquer les privilèges
|
||||
FLUSH PRIVILEGES;
|
||||
|
||||
-- Vérifier la création
|
||||
SELECT user, host FROM mysql.user WHERE user = 'adresses_user' ORDER BY host;
|
||||
150
api/scripts/cron/cleanup_security_data.php
Normal file
150
api/scripts/cron/cleanup_security_data.php
Normal file
@@ -0,0 +1,150 @@
|
||||
#!/usr/bin/env php
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Script de nettoyage des données de sécurité
|
||||
* À exécuter via cron quotidiennement
|
||||
* Exemple crontab: 0 2 * * * /usr/bin/php /var/www/geosector/api/scripts/cron/cleanup_security_data.php
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
// Configuration
|
||||
require_once __DIR__ . '/../../bootstrap.php';
|
||||
require_once __DIR__ . '/../../src/Config/AppConfig.php';
|
||||
require_once __DIR__ . '/../../src/Core/Database.php';
|
||||
|
||||
// Initialiser la configuration
|
||||
$appConfig = AppConfig::getInstance();
|
||||
$config = $appConfig->getFullConfig();
|
||||
|
||||
// Initialiser la base de données
|
||||
Database::init($config['database']);
|
||||
$db = Database::getInstance();
|
||||
|
||||
// Configuration de rétention (en jours)
|
||||
$RETENTION_DAYS = [
|
||||
'performance_metrics' => 30, // Garder 30 jours de métriques
|
||||
'failed_login_attempts' => 7, // Garder 7 jours de tentatives
|
||||
'resolved_alerts' => 90, // Garder 90 jours d'alertes résolues
|
||||
'expired_blocks' => 0 // Débloquer immédiatement les IPs expirées
|
||||
];
|
||||
|
||||
echo "[" . date('Y-m-d H:i:s') . "] Début du nettoyage des données de sécurité\n";
|
||||
|
||||
try {
|
||||
$totalDeleted = 0;
|
||||
|
||||
// 1. Nettoyer les métriques de performance
|
||||
echo "- Nettoyage des métriques de performance (>" . $RETENTION_DAYS['performance_metrics'] . " jours)...\n";
|
||||
$stmt = $db->prepare('
|
||||
DELETE FROM sec_performance_metrics
|
||||
WHERE created_at < DATE_SUB(NOW(), INTERVAL :days DAY)
|
||||
');
|
||||
$stmt->execute(['days' => $RETENTION_DAYS['performance_metrics']]);
|
||||
$deleted = $stmt->rowCount();
|
||||
echo " → $deleted lignes supprimées\n";
|
||||
$totalDeleted += $deleted;
|
||||
|
||||
// 2. Nettoyer les tentatives de login échouées
|
||||
echo "- Nettoyage des tentatives de login (>" . $RETENTION_DAYS['failed_login_attempts'] . " jours)...\n";
|
||||
$stmt = $db->prepare('
|
||||
DELETE FROM sec_failed_login_attempts
|
||||
WHERE attempt_time < DATE_SUB(NOW(), INTERVAL :days DAY)
|
||||
');
|
||||
$stmt->execute(['days' => $RETENTION_DAYS['failed_login_attempts']]);
|
||||
$deleted = $stmt->rowCount();
|
||||
echo " → $deleted lignes supprimées\n";
|
||||
$totalDeleted += $deleted;
|
||||
|
||||
// 3. Nettoyer les alertes résolues
|
||||
echo "- Nettoyage des alertes résolues (>" . $RETENTION_DAYS['resolved_alerts'] . " jours)...\n";
|
||||
$stmt = $db->prepare('
|
||||
DELETE FROM sec_alerts
|
||||
WHERE resolved = 1
|
||||
AND resolved_at < DATE_SUB(NOW(), INTERVAL :days DAY)
|
||||
');
|
||||
$stmt->execute(['days' => $RETENTION_DAYS['resolved_alerts']]);
|
||||
$deleted = $stmt->rowCount();
|
||||
echo " → $deleted lignes supprimées\n";
|
||||
$totalDeleted += $deleted;
|
||||
|
||||
// 4. Débloquer les IPs expirées
|
||||
echo "- Déblocage des IPs expirées...\n";
|
||||
$stmt = $db->prepare('
|
||||
UPDATE sec_blocked_ips
|
||||
SET unblocked_at = NOW()
|
||||
WHERE blocked_until <= NOW()
|
||||
AND unblocked_at IS NULL
|
||||
AND permanent = 0
|
||||
');
|
||||
$stmt->execute();
|
||||
$unblocked = $stmt->rowCount();
|
||||
echo " → $unblocked IPs débloquées\n";
|
||||
|
||||
// 5. Supprimer les anciennes IPs débloquées (optionnel, garder 180 jours d'historique)
|
||||
echo "- Suppression des anciennes IPs débloquées (>180 jours)...\n";
|
||||
$stmt = $db->prepare('
|
||||
DELETE FROM sec_blocked_ips
|
||||
WHERE unblocked_at IS NOT NULL
|
||||
AND unblocked_at < DATE_SUB(NOW(), INTERVAL 180 DAY)
|
||||
');
|
||||
$stmt->execute();
|
||||
$deleted = $stmt->rowCount();
|
||||
echo " → $deleted lignes supprimées\n";
|
||||
$totalDeleted += $deleted;
|
||||
|
||||
// 6. Optimiser les tables (optionnel, peut être long sur de grosses tables)
|
||||
if ($totalDeleted > 1000) {
|
||||
echo "- Optimisation des tables...\n";
|
||||
$tables = [
|
||||
'sec_performance_metrics',
|
||||
'sec_failed_login_attempts',
|
||||
'sec_alerts',
|
||||
'sec_blocked_ips'
|
||||
];
|
||||
|
||||
foreach ($tables as $table) {
|
||||
try {
|
||||
$db->exec("OPTIMIZE TABLE $table");
|
||||
echo " → Table $table optimisée\n";
|
||||
} catch (Exception $e) {
|
||||
echo " ⚠ Impossible d'optimiser $table: " . $e->getMessage() . "\n";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 7. Statistiques finales
|
||||
echo "\n=== RÉSUMÉ ===\n";
|
||||
echo "Total supprimé: $totalDeleted lignes\n";
|
||||
echo "IPs débloquées: $unblocked\n";
|
||||
|
||||
// Obtenir les statistiques actuelles
|
||||
$stats = [];
|
||||
$tables = [
|
||||
'sec_alerts' => "SELECT COUNT(*) as total, SUM(resolved = 0) as active FROM sec_alerts",
|
||||
'sec_performance_metrics' => "SELECT COUNT(*) as total FROM sec_performance_metrics",
|
||||
'sec_failed_login_attempts' => "SELECT COUNT(*) as total FROM sec_failed_login_attempts",
|
||||
'sec_blocked_ips' => "SELECT COUNT(*) as total, SUM(permanent = 1) as permanent FROM sec_blocked_ips WHERE unblocked_at IS NULL"
|
||||
];
|
||||
|
||||
echo "\nÉtat actuel des tables:\n";
|
||||
foreach ($tables as $table => $query) {
|
||||
$result = $db->query($query)->fetch(PDO::FETCH_ASSOC);
|
||||
if ($table === 'sec_alerts') {
|
||||
echo "- $table: {$result['total']} total, {$result['active']} actives\n";
|
||||
} elseif ($table === 'sec_blocked_ips') {
|
||||
$permanent = $result['permanent'] ?? 0;
|
||||
echo "- $table: {$result['total']} bloquées, $permanent permanentes\n";
|
||||
} else {
|
||||
echo "- $table: {$result['total']} enregistrements\n";
|
||||
}
|
||||
}
|
||||
|
||||
echo "\n[" . date('Y-m-d H:i:s') . "] Nettoyage terminé avec succès\n";
|
||||
|
||||
} catch (Exception $e) {
|
||||
echo "\n❌ ERREUR: " . $e->getMessage() . "\n";
|
||||
echo "Stack trace:\n" . $e->getTraceAsString() . "\n";
|
||||
exit(1);
|
||||
}
|
||||
322
api/scripts/cron/process_email_queue.php
Executable file
322
api/scripts/cron/process_email_queue.php
Executable file
@@ -0,0 +1,322 @@
|
||||
#!/usr/bin/env php
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Script CRON pour traiter la queue d'emails
|
||||
* Envoie les emails en attente dans la table email_queue
|
||||
*
|
||||
* À exécuter toutes les 5 minutes via crontab :
|
||||
* Exemple: [asterisk]/5 [asterisk] [asterisk] [asterisk] [asterisk] /usr/bin/php /path/to/api/scripts/cron/process_email_queue.php
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
// Configuration
|
||||
define('MAX_ATTEMPTS', 3);
|
||||
define('BATCH_SIZE', 50);
|
||||
define('LOCK_FILE', '/tmp/process_email_queue.lock');
|
||||
|
||||
// Empêcher l'exécution multiple simultanée
|
||||
if (file_exists(LOCK_FILE)) {
|
||||
$lockTime = filemtime(LOCK_FILE);
|
||||
// Si le lock a plus de 30 minutes, on le supprime (processus probablement bloqué)
|
||||
if (time() - $lockTime > 1800) {
|
||||
unlink(LOCK_FILE);
|
||||
} else {
|
||||
die("Le processus est déjà en cours d'exécution\n");
|
||||
}
|
||||
}
|
||||
|
||||
// Créer le fichier de lock
|
||||
file_put_contents(LOCK_FILE, getmypid());
|
||||
|
||||
// Enregistrer un handler pour supprimer le lock en cas d'arrêt
|
||||
register_shutdown_function(function() {
|
||||
if (file_exists(LOCK_FILE)) {
|
||||
unlink(LOCK_FILE);
|
||||
}
|
||||
});
|
||||
|
||||
// Simuler l'environnement web pour AppConfig en CLI
|
||||
if (php_sapi_name() === 'cli') {
|
||||
// Détecter l'environnement basé sur le hostname ou un paramètre
|
||||
$hostname = gethostname();
|
||||
if (strpos($hostname, 'prod') !== false) {
|
||||
$_SERVER['SERVER_NAME'] = 'app.geosector.fr';
|
||||
} elseif (strpos($hostname, 'rec') !== false || strpos($hostname, 'rapp') !== false) {
|
||||
$_SERVER['SERVER_NAME'] = 'rapp.geosector.fr';
|
||||
} else {
|
||||
$_SERVER['SERVER_NAME'] = 'dapp.geosector.fr'; // DVA par défaut
|
||||
}
|
||||
|
||||
$_SERVER['HTTP_HOST'] = $_SERVER['HTTP_HOST'] ?? $_SERVER['SERVER_NAME'];
|
||||
$_SERVER['REMOTE_ADDR'] = $_SERVER['REMOTE_ADDR'] ?? '127.0.0.1';
|
||||
|
||||
// Définir getallheaders si elle n'existe pas (CLI)
|
||||
if (!function_exists('getallheaders')) {
|
||||
function getallheaders() {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Chargement de l'environnement
|
||||
require_once __DIR__ . '/../../vendor/autoload.php';
|
||||
require_once __DIR__ . '/../../src/Config/AppConfig.php';
|
||||
require_once __DIR__ . '/../../src/Core/Database.php';
|
||||
require_once __DIR__ . '/../../src/Services/LogService.php';
|
||||
|
||||
use PHPMailer\PHPMailer\PHPMailer;
|
||||
use PHPMailer\PHPMailer\SMTP;
|
||||
use PHPMailer\PHPMailer\Exception;
|
||||
|
||||
try {
|
||||
// Initialisation de la configuration
|
||||
$appConfig = AppConfig::getInstance();
|
||||
$dbConfig = $appConfig->getDatabaseConfig();
|
||||
|
||||
// Initialiser la base de données avec la configuration
|
||||
Database::init($dbConfig);
|
||||
$db = Database::getInstance();
|
||||
|
||||
// Log uniquement si mode debug activé
|
||||
// LogService::log('Démarrage du processeur de queue d\'emails', [
|
||||
// 'level' => 'info',
|
||||
// 'script' => 'process_email_queue.php'
|
||||
// ]);
|
||||
|
||||
// Récupérer les emails en attente
|
||||
$stmt = $db->prepare('
|
||||
SELECT id, fk_pass, to_email, subject, body, headers, attempts
|
||||
FROM email_queue
|
||||
WHERE status = ? AND attempts < ?
|
||||
ORDER BY created_at ASC
|
||||
LIMIT ?
|
||||
');
|
||||
|
||||
$stmt->execute(['pending', MAX_ATTEMPTS, BATCH_SIZE]);
|
||||
$emails = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
if (empty($emails)) {
|
||||
// Ne pas logger quand il n'y a rien à faire (toutes les 5 minutes)
|
||||
// LogService::log('Aucun email en attente dans la queue', [
|
||||
// 'level' => 'debug'
|
||||
// ]);
|
||||
exit(0);
|
||||
}
|
||||
|
||||
LogService::log('Emails à traiter', [
|
||||
'level' => 'info',
|
||||
'count' => count($emails)
|
||||
]);
|
||||
|
||||
// Configuration SMTP
|
||||
$smtpConfig = $appConfig->getSmtpConfig();
|
||||
$emailConfig = $appConfig->getEmailConfig();
|
||||
|
||||
$successCount = 0;
|
||||
$failureCount = 0;
|
||||
|
||||
// Traiter chaque email
|
||||
foreach ($emails as $emailData) {
|
||||
$emailId = $emailData['id'];
|
||||
$passageId = $emailData['fk_pass'];
|
||||
|
||||
try {
|
||||
// Incrémenter le compteur de tentatives
|
||||
$stmt = $db->prepare('UPDATE email_queue SET attempts = attempts + 1 WHERE id = ?');
|
||||
$stmt->execute([$emailId]);
|
||||
|
||||
// Créer l'instance PHPMailer
|
||||
$mail = new PHPMailer(true);
|
||||
|
||||
// Configuration du serveur SMTP
|
||||
$mail->isSMTP();
|
||||
$mail->Host = $smtpConfig['host'];
|
||||
$mail->SMTPAuth = $smtpConfig['auth'] ?? true;
|
||||
$mail->Username = $smtpConfig['user'];
|
||||
$mail->Password = $smtpConfig['pass'];
|
||||
$mail->SMTPSecure = $smtpConfig['secure'];
|
||||
$mail->Port = $smtpConfig['port'];
|
||||
$mail->CharSet = 'UTF-8';
|
||||
|
||||
// Configuration de l'expéditeur
|
||||
$fromName = 'Amicale Sapeurs-Pompiers'; // Nom par défaut
|
||||
$mail->setFrom($emailConfig['from'], $fromName);
|
||||
|
||||
// Destinataire
|
||||
$mail->addAddress($emailData['to_email']);
|
||||
|
||||
// Sujet
|
||||
$mail->Subject = $emailData['subject'];
|
||||
|
||||
// Headers personnalisés si présents
|
||||
if (!empty($emailData['headers'])) {
|
||||
// Les headers contiennent déjà les informations MIME pour la pièce jointe
|
||||
// On doit extraire le boundary et reconstruire le message
|
||||
if (preg_match('/boundary="([^"]+)"/', $emailData['headers'], $matches)) {
|
||||
$boundary = $matches[1];
|
||||
|
||||
// Le body contient déjà le message complet avec pièce jointe
|
||||
$mail->isHTML(false);
|
||||
$mail->Body = $emailData['body'];
|
||||
|
||||
// Extraire le contenu HTML et la pièce jointe
|
||||
$parts = explode("--$boundary", $emailData['body']);
|
||||
|
||||
foreach ($parts as $part) {
|
||||
if (strpos($part, 'Content-Type: text/html') !== false) {
|
||||
// Extraire le contenu HTML
|
||||
$htmlContent = trim(substr($part, strpos($part, "\r\n\r\n") + 4));
|
||||
$mail->isHTML(true);
|
||||
$mail->Body = $htmlContent;
|
||||
} elseif (strpos($part, 'Content-Type: application/pdf') !== false) {
|
||||
// Extraire le PDF encodé en base64
|
||||
if (preg_match('/filename="([^"]+)"/', $part, $fileMatches)) {
|
||||
$filename = $fileMatches[1];
|
||||
$pdfContent = trim(substr($part, strpos($part, "\r\n\r\n") + 4));
|
||||
// Supprimer les retours à la ligne du base64
|
||||
$pdfContent = str_replace(["\r", "\n"], '', $pdfContent);
|
||||
|
||||
// Ajouter la pièce jointe
|
||||
$mail->addStringAttachment(
|
||||
base64_decode($pdfContent),
|
||||
$filename,
|
||||
'base64',
|
||||
'application/pdf'
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Email simple sans pièce jointe
|
||||
$mail->isHTML(true);
|
||||
$mail->Body = $emailData['body'];
|
||||
}
|
||||
|
||||
// Ajouter une copie si configuré
|
||||
if (!empty($emailConfig['contact'])) {
|
||||
$mail->addBCC($emailConfig['contact']);
|
||||
}
|
||||
|
||||
// Envoyer l'email
|
||||
if ($mail->send()) {
|
||||
// Marquer comme envoyé
|
||||
$stmt = $db->prepare('
|
||||
UPDATE email_queue
|
||||
SET status = ?, sent_at = NOW()
|
||||
WHERE id = ?
|
||||
');
|
||||
$stmt->execute(['sent', $emailId]);
|
||||
|
||||
// Mettre à jour le passage si nécessaire
|
||||
if ($passageId > 0) {
|
||||
$stmt = $db->prepare('
|
||||
UPDATE ope_pass
|
||||
SET date_sent_recu = NOW(), chk_email_sent = 1
|
||||
WHERE id = ?
|
||||
');
|
||||
$stmt->execute([$passageId]);
|
||||
}
|
||||
|
||||
$successCount++;
|
||||
|
||||
LogService::log('Email envoyé avec succès', [
|
||||
'level' => 'info',
|
||||
'emailId' => $emailId,
|
||||
'passageId' => $passageId,
|
||||
'to' => $emailData['to_email']
|
||||
]);
|
||||
} else {
|
||||
throw new Exception('Échec de l\'envoi');
|
||||
}
|
||||
|
||||
} catch (Exception $e) {
|
||||
$failureCount++;
|
||||
|
||||
LogService::log('Erreur lors de l\'envoi de l\'email', [
|
||||
'level' => 'error',
|
||||
'emailId' => $emailId,
|
||||
'passageId' => $passageId,
|
||||
'error' => $e->getMessage(),
|
||||
'attempts' => $emailData['attempts'] + 1
|
||||
]);
|
||||
|
||||
// Si on a atteint le nombre max de tentatives, marquer comme échoué
|
||||
if ($emailData['attempts'] + 1 >= MAX_ATTEMPTS) {
|
||||
$stmt = $db->prepare('
|
||||
UPDATE email_queue
|
||||
SET status = ?, error_message = ?
|
||||
WHERE id = ?
|
||||
');
|
||||
$stmt->execute(['failed', $e->getMessage(), $emailId]);
|
||||
|
||||
LogService::log('Email marqué comme échoué après ' . MAX_ATTEMPTS . ' tentatives', [
|
||||
'level' => 'warning',
|
||||
'emailId' => $emailId,
|
||||
'passageId' => $passageId
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
// Pause courte entre chaque email pour éviter la surcharge
|
||||
usleep(500000); // 0.5 seconde
|
||||
}
|
||||
|
||||
// Logger uniquement s'il y avait des emails à traiter
|
||||
if (count($emails) > 0) {
|
||||
LogService::log('Traitement de la queue terminé', [
|
||||
'level' => 'info',
|
||||
'success' => $successCount,
|
||||
'failures' => $failureCount,
|
||||
'total' => count($emails)
|
||||
]);
|
||||
}
|
||||
|
||||
} catch (Exception $e) {
|
||||
LogService::log('Erreur fatale dans le processeur de queue', [
|
||||
'level' => 'critical',
|
||||
'error' => $e->getMessage(),
|
||||
'trace' => $e->getTraceAsString()
|
||||
]);
|
||||
|
||||
// Supprimer le lock en cas d'erreur
|
||||
if (file_exists(LOCK_FILE)) {
|
||||
unlink(LOCK_FILE);
|
||||
}
|
||||
|
||||
exit(1);
|
||||
}
|
||||
|
||||
// Nettoyer les vieux emails traités (optionnel)
|
||||
try {
|
||||
// Supprimer les emails envoyés de plus de 30 jours
|
||||
$stmt = $db->prepare('
|
||||
DELETE FROM email_queue
|
||||
WHERE status = ? AND sent_at < DATE_SUB(NOW(), INTERVAL 30 DAY)
|
||||
');
|
||||
$stmt->execute(['sent']);
|
||||
|
||||
$deleted = $stmt->rowCount();
|
||||
if ($deleted > 0) {
|
||||
LogService::log('Nettoyage des anciens emails', [
|
||||
'level' => 'info',
|
||||
'deleted' => $deleted
|
||||
]);
|
||||
}
|
||||
|
||||
} catch (Exception $e) {
|
||||
LogService::log('Erreur lors du nettoyage des anciens emails', [
|
||||
'level' => 'warning',
|
||||
'error' => $e->getMessage()
|
||||
]);
|
||||
}
|
||||
|
||||
// Supprimer le lock
|
||||
if (file_exists(LOCK_FILE)) {
|
||||
unlink(LOCK_FILE);
|
||||
}
|
||||
|
||||
echo "Traitement terminé : $successCount envoyés, $failureCount échecs\n";
|
||||
exit(0);
|
||||
31
api/scripts/cron/process_email_queue_with_daily_log.sh
Normal file
31
api/scripts/cron/process_email_queue_with_daily_log.sh
Normal file
@@ -0,0 +1,31 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Script wrapper pour process_email_queue avec logs journaliers
|
||||
# Crée automatiquement un nouveau fichier log chaque jour
|
||||
|
||||
# Configuration
|
||||
LOG_DIR="/var/www/geosector/api/logs"
|
||||
LOG_FILE="$LOG_DIR/email_queue_$(date +%Y%m%d).log"
|
||||
PHP_SCRIPT="/var/www/geosector/api/scripts/cron/process_email_queue.php"
|
||||
|
||||
# Créer le répertoire de logs s'il n'existe pas
|
||||
mkdir -p "$LOG_DIR"
|
||||
|
||||
# Ajouter un timestamp au début de l'exécution
|
||||
echo "[$(date '+%Y-%m-%d %H:%M:%S')] Démarrage du processeur de queue d'emails" >> "$LOG_FILE"
|
||||
|
||||
# Exécuter le script PHP
|
||||
/usr/bin/php "$PHP_SCRIPT" >> "$LOG_FILE" 2>&1
|
||||
|
||||
# Ajouter le statut de sortie
|
||||
EXIT_CODE=$?
|
||||
if [ $EXIT_CODE -eq 0 ]; then
|
||||
echo "[$(date '+%Y-%m-%d %H:%M:%S')] Fin du traitement (succès)" >> "$LOG_FILE"
|
||||
else
|
||||
echo "[$(date '+%Y-%m-%d %H:%M:%S')] Fin du traitement (erreur: $EXIT_CODE)" >> "$LOG_FILE"
|
||||
fi
|
||||
|
||||
# Nettoyer les logs de plus de 30 jours
|
||||
find "$LOG_DIR" -name "email_queue_*.log" -type f -mtime +30 -delete 2>/dev/null
|
||||
|
||||
exit $EXIT_CODE
|
||||
0
api/scripts/cron/sync_databases.php
Normal file → Executable file
0
api/scripts/cron/sync_databases.php
Normal file → Executable file
186
api/scripts/cron/test_email_queue.php
Executable file
186
api/scripts/cron/test_email_queue.php
Executable file
@@ -0,0 +1,186 @@
|
||||
#!/usr/bin/env php
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Script de test pour vérifier le processeur de queue d'emails
|
||||
* Affiche les emails en attente sans les envoyer
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
// Simuler l'environnement web pour AppConfig en CLI
|
||||
if (php_sapi_name() === 'cli') {
|
||||
$_SERVER['SERVER_NAME'] = $_SERVER['SERVER_NAME'] ?? 'dapp.geosector.fr'; // DVA par défaut
|
||||
$_SERVER['HTTP_HOST'] = $_SERVER['HTTP_HOST'] ?? $_SERVER['SERVER_NAME'];
|
||||
$_SERVER['REMOTE_ADDR'] = $_SERVER['REMOTE_ADDR'] ?? '127.0.0.1';
|
||||
|
||||
// Définir getallheaders si elle n'existe pas (CLI)
|
||||
if (!function_exists('getallheaders')) {
|
||||
function getallheaders() {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
require_once __DIR__ . '/../../src/Core/Database.php';
|
||||
require_once __DIR__ . '/../../src/Config/AppConfig.php';
|
||||
|
||||
try {
|
||||
// Initialiser la configuration
|
||||
$appConfig = AppConfig::getInstance();
|
||||
$dbConfig = $appConfig->getDatabaseConfig();
|
||||
|
||||
// Initialiser la base de données avec la configuration
|
||||
Database::init($dbConfig);
|
||||
$db = Database::getInstance();
|
||||
|
||||
echo "=== TEST DE LA QUEUE D'EMAILS ===\n\n";
|
||||
|
||||
// Statistiques générales
|
||||
$stmt = $db->query('
|
||||
SELECT
|
||||
status,
|
||||
COUNT(*) as count,
|
||||
MIN(created_at) as oldest,
|
||||
MAX(created_at) as newest
|
||||
FROM email_queue
|
||||
GROUP BY status
|
||||
');
|
||||
|
||||
$stats = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
echo "STATISTIQUES:\n";
|
||||
echo "-------------\n";
|
||||
foreach ($stats as $stat) {
|
||||
echo sprintf(
|
||||
"Status: %s - Nombre: %d (Plus ancien: %s, Plus récent: %s)\n",
|
||||
$stat['status'],
|
||||
$stat['count'],
|
||||
$stat['oldest'] ?? 'N/A',
|
||||
$stat['newest'] ?? 'N/A'
|
||||
);
|
||||
}
|
||||
|
||||
echo "\n";
|
||||
|
||||
// Emails en attente
|
||||
$stmt = $db->prepare('
|
||||
SELECT
|
||||
eq.id,
|
||||
eq.fk_pass,
|
||||
eq.to_email,
|
||||
eq.subject,
|
||||
eq.created_at,
|
||||
eq.attempts,
|
||||
eq.status,
|
||||
p.fk_type,
|
||||
p.montant,
|
||||
p.nom_recu
|
||||
FROM email_queue eq
|
||||
LEFT JOIN ope_pass p ON eq.fk_pass = p.id
|
||||
WHERE eq.status = ?
|
||||
ORDER BY eq.created_at DESC
|
||||
LIMIT 10
|
||||
');
|
||||
|
||||
$stmt->execute(['pending']);
|
||||
$pendingEmails = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
if (empty($pendingEmails)) {
|
||||
echo "Aucun email en attente.\n";
|
||||
} else {
|
||||
echo "EMAILS EN ATTENTE (10 plus récents):\n";
|
||||
echo "------------------------------------\n";
|
||||
foreach ($pendingEmails as $email) {
|
||||
echo sprintf(
|
||||
"ID: %d | Passage: %d | Destinataire: %s\n",
|
||||
$email['id'],
|
||||
$email['fk_pass'],
|
||||
$email['to_email']
|
||||
);
|
||||
echo sprintf(
|
||||
" Sujet: %s\n",
|
||||
$email['subject']
|
||||
);
|
||||
echo sprintf(
|
||||
" Créé le: %s | Tentatives: %d\n",
|
||||
$email['created_at'],
|
||||
$email['attempts']
|
||||
);
|
||||
if ($email['fk_pass'] > 0) {
|
||||
echo sprintf(
|
||||
" Passage - Type: %s | Montant: %.2f€ | Reçu: %s\n",
|
||||
$email['fk_type'] == 1 ? 'DON' : 'Autre',
|
||||
$email['montant'] ?? 0,
|
||||
$email['nom_recu'] ?? 'Non généré'
|
||||
);
|
||||
}
|
||||
echo "---\n";
|
||||
}
|
||||
}
|
||||
|
||||
// Emails échoués
|
||||
$stmt = $db->prepare('
|
||||
SELECT
|
||||
id,
|
||||
fk_pass,
|
||||
to_email,
|
||||
subject,
|
||||
created_at,
|
||||
attempts,
|
||||
error_message
|
||||
FROM email_queue
|
||||
WHERE status = ?
|
||||
ORDER BY created_at DESC
|
||||
LIMIT 5
|
||||
');
|
||||
|
||||
$stmt->execute(['failed']);
|
||||
$failedEmails = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
if (!empty($failedEmails)) {
|
||||
echo "\nEMAILS ÉCHOUÉS (5 plus récents):\n";
|
||||
echo "--------------------------------\n";
|
||||
foreach ($failedEmails as $email) {
|
||||
echo sprintf(
|
||||
"ID: %d | Passage: %d | Destinataire: %s\n",
|
||||
$email['id'],
|
||||
$email['fk_pass'],
|
||||
$email['to_email']
|
||||
);
|
||||
echo sprintf(
|
||||
" Sujet: %s\n",
|
||||
$email['subject']
|
||||
);
|
||||
echo sprintf(
|
||||
" Tentatives: %d | Erreur: %s\n",
|
||||
$email['attempts'],
|
||||
$email['error_message'] ?? 'Non spécifiée'
|
||||
);
|
||||
echo "---\n";
|
||||
}
|
||||
}
|
||||
|
||||
// Vérifier la configuration SMTP
|
||||
echo "\nCONFIGURATION SMTP:\n";
|
||||
echo "-------------------\n";
|
||||
|
||||
$smtpConfig = $appConfig->getSmtpConfig();
|
||||
$emailConfig = $appConfig->getEmailConfig();
|
||||
|
||||
echo "Host: " . ($smtpConfig['host'] ?? 'Non configuré') . "\n";
|
||||
echo "Port: " . ($smtpConfig['port'] ?? 'Non configuré') . "\n";
|
||||
echo "Username: " . ($smtpConfig['user'] ?? 'Non configuré') . "\n";
|
||||
echo "Password: " . (isset($smtpConfig['pass']) ? '***' : 'Non configuré') . "\n";
|
||||
echo "Encryption: " . ($smtpConfig['secure'] ?? 'Non configuré') . "\n";
|
||||
echo "From Email: " . ($emailConfig['from'] ?? 'Non configuré') . "\n";
|
||||
echo "Contact Email: " . ($emailConfig['contact'] ?? 'Non configuré') . "\n";
|
||||
|
||||
echo "\n=== FIN DU TEST ===\n";
|
||||
|
||||
} catch (Exception $e) {
|
||||
echo "ERREUR: " . $e->getMessage() . "\n";
|
||||
exit(1);
|
||||
}
|
||||
|
||||
exit(0);
|
||||
37
api/scripts/fix_geometry_for_spatial_index.sql
Normal file
37
api/scripts/fix_geometry_for_spatial_index.sql
Normal file
@@ -0,0 +1,37 @@
|
||||
-- Script pour diagnostiquer et corriger les problèmes d'index spatial
|
||||
|
||||
-- 1. Vérifier s'il y a des géométries vides
|
||||
SELECT 'Géométries vides:' as diagnostic;
|
||||
SELECT code, libelle
|
||||
FROM x_departements
|
||||
WHERE contour IS NOT NULL AND ST_IsEmpty(contour) = 1;
|
||||
|
||||
-- 2. Essayer de créer un index spatial sur une copie de la table pour tester
|
||||
CREATE TABLE x_departements_test LIKE x_departements;
|
||||
|
||||
-- 3. Copier uniquement les départements métropolitains avec contours
|
||||
INSERT INTO x_departements_test
|
||||
SELECT * FROM x_departements
|
||||
WHERE contour IS NOT NULL
|
||||
AND code NOT IN ('20', '971', '972', '973', '974', '975', '976');
|
||||
|
||||
-- 4. Tenter de créer l'index spatial sur la table de test
|
||||
ALTER TABLE x_departements_test ADD SPATIAL INDEX idx_contour_test (contour);
|
||||
|
||||
-- Si ça fonctionne, le problème vient des départements spécifiques
|
||||
-- Si ça ne fonctionne pas, il y a un problème avec les données géométriques
|
||||
|
||||
-- 5. Alternative : recréer les géométries à partir du texte WKT
|
||||
-- Cela peut corriger certains problèmes de format
|
||||
UPDATE x_departements d
|
||||
INNER JOIN x_departements_contours dc ON d.code = dc.code_dept
|
||||
SET d.contour = ST_GeomFromText(ST_AsText(dc.contour))
|
||||
WHERE dc.contour IS NOT NULL
|
||||
AND d.code IN (SELECT code FROM x_departements WHERE contour IS NOT NULL LIMIT 1);
|
||||
|
||||
-- 6. Nettoyer
|
||||
DROP TABLE IF EXISTS x_departements_test;
|
||||
|
||||
-- Note : Pour l'instant, l'index normal créé avec contour(32) permettra
|
||||
-- le fonctionnement de l'API, même si les performances seront moindres
|
||||
-- qu'avec un vrai index spatial.
|
||||
0
api/scripts/geosector.sql
Normal file → Executable file
0
api/scripts/geosector.sql
Normal file → Executable file
0
api/scripts/geosector_app.sql
Normal file → Executable file
0
api/scripts/geosector_app.sql
Normal file → Executable file
263
api/scripts/import_departements_from_file.php
Normal file
263
api/scripts/import_departements_from_file.php
Normal file
@@ -0,0 +1,263 @@
|
||||
<?php
|
||||
/**
|
||||
* Script d'import des contours départementaux depuis un fichier GeoJSON local
|
||||
*/
|
||||
|
||||
class DepartementContoursFileImporter {
|
||||
private PDO $db;
|
||||
private array $log = [];
|
||||
|
||||
public function __construct(PDO $db) {
|
||||
$this->db = $db;
|
||||
}
|
||||
|
||||
/**
|
||||
* Vérifie si la table existe
|
||||
*/
|
||||
public function tableExists(): bool {
|
||||
try {
|
||||
$sql = "SHOW TABLES LIKE 'x_departements_contours'";
|
||||
$stmt = $this->db->query($sql);
|
||||
return $stmt->rowCount() > 0;
|
||||
} catch (Exception $e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Vérifie si la table est vide
|
||||
*/
|
||||
private function isTableEmpty(): bool {
|
||||
try {
|
||||
$sql = "SELECT COUNT(*) as count FROM x_departements_contours";
|
||||
$stmt = $this->db->query($sql);
|
||||
$result = $stmt->fetch();
|
||||
return $result['count'] == 0;
|
||||
} catch (Exception $e) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Importe les départements depuis un fichier GeoJSON
|
||||
*/
|
||||
public function importFromFile(string $filePath): array {
|
||||
$this->log[] = "Début de l'import depuis le fichier : $filePath";
|
||||
$this->log[] = "";
|
||||
|
||||
// Vérifier que le fichier existe
|
||||
if (!file_exists($filePath)) {
|
||||
$this->log[] = "✗ Fichier non trouvé : $filePath";
|
||||
return $this->log;
|
||||
}
|
||||
|
||||
// Vérifier que la table existe
|
||||
if (!$this->tableExists()) {
|
||||
$this->log[] = "✗ La table x_departements_contours n'existe pas";
|
||||
return $this->log;
|
||||
}
|
||||
|
||||
// Vérifier que la table est vide
|
||||
if (!$this->isTableEmpty()) {
|
||||
$this->log[] = "✗ La table x_departements_contours contient déjà des données";
|
||||
return $this->log;
|
||||
}
|
||||
|
||||
// Lire le fichier GeoJSON
|
||||
$this->log[] = "Lecture du fichier GeoJSON...";
|
||||
$jsonContent = file_get_contents($filePath);
|
||||
|
||||
if ($jsonContent === false) {
|
||||
$this->log[] = "✗ Impossible de lire le fichier";
|
||||
return $this->log;
|
||||
}
|
||||
|
||||
// Parser le JSON
|
||||
$geojson = json_decode($jsonContent, true);
|
||||
|
||||
if (json_last_error() !== JSON_ERROR_NONE) {
|
||||
$this->log[] = "✗ Erreur JSON : " . json_last_error_msg();
|
||||
return $this->log;
|
||||
}
|
||||
|
||||
if (!isset($geojson['features']) || !is_array($geojson['features'])) {
|
||||
$this->log[] = "✗ Format GeoJSON invalide : pas de features";
|
||||
return $this->log;
|
||||
}
|
||||
|
||||
$this->log[] = "✓ Fichier chargé : " . count($geojson['features']) . " départements trouvés";
|
||||
$this->log[] = "";
|
||||
|
||||
// Préparer la requête d'insertion
|
||||
$sql = "INSERT INTO x_departements_contours
|
||||
(code_dept, nom_dept, contour, bbox_min_lat, bbox_max_lat, bbox_min_lng, bbox_max_lng)
|
||||
VALUES
|
||||
(:code, :nom, ST_GeomFromText(:polygon, 4326), :min_lat, :max_lat, :min_lng, :max_lng)";
|
||||
|
||||
$stmt = $this->db->prepare($sql);
|
||||
|
||||
$success = 0;
|
||||
$errors = 0;
|
||||
|
||||
// Démarrer une transaction
|
||||
$this->db->beginTransaction();
|
||||
|
||||
try {
|
||||
foreach ($geojson['features'] as $feature) {
|
||||
// Extraire les informations
|
||||
$code = $feature['properties']['code'] ?? null;
|
||||
$nom = $feature['properties']['nom'] ?? null;
|
||||
$geometry = $feature['geometry'] ?? null;
|
||||
|
||||
if (!$code || !$nom || !$geometry) {
|
||||
$this->log[] = "✗ Données manquantes pour un département";
|
||||
$errors++;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Convertir la géométrie en WKT
|
||||
$wktData = $this->geometryToWkt($geometry);
|
||||
|
||||
if (!$wktData) {
|
||||
$this->log[] = "✗ Conversion échouée pour $code ($nom)";
|
||||
$errors++;
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
$stmt->execute([
|
||||
'code' => $code,
|
||||
'nom' => $nom,
|
||||
'polygon' => $wktData['wkt'],
|
||||
'min_lat' => $wktData['bbox']['min_lat'],
|
||||
'max_lat' => $wktData['bbox']['max_lat'],
|
||||
'min_lng' => $wktData['bbox']['min_lng'],
|
||||
'max_lng' => $wktData['bbox']['max_lng']
|
||||
]);
|
||||
|
||||
$this->log[] = "✓ $code - $nom importé";
|
||||
$success++;
|
||||
|
||||
} catch (Exception $e) {
|
||||
$this->log[] = "✗ Erreur SQL pour $code ($nom) : " . $e->getMessage();
|
||||
$errors++;
|
||||
}
|
||||
}
|
||||
|
||||
// Valider ou annuler la transaction
|
||||
if ($success > 0) {
|
||||
$this->db->commit();
|
||||
$this->log[] = "";
|
||||
$this->log[] = "✓ Transaction validée";
|
||||
} else {
|
||||
$this->db->rollBack();
|
||||
$this->log[] = "";
|
||||
$this->log[] = "✗ Transaction annulée (aucun import réussi)";
|
||||
}
|
||||
|
||||
} catch (Exception $e) {
|
||||
$this->db->rollBack();
|
||||
$this->log[] = "";
|
||||
$this->log[] = "✗ Erreur fatale : " . $e->getMessage();
|
||||
$this->log[] = "✗ Transaction annulée";
|
||||
}
|
||||
|
||||
$this->log[] = "";
|
||||
$this->log[] = "Import terminé : $success réussis, $errors erreurs";
|
||||
|
||||
return $this->log;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convertit une géométrie GeoJSON en WKT
|
||||
*/
|
||||
private function geometryToWkt(array $geometry): ?array {
|
||||
$type = $geometry['type'] ?? null;
|
||||
$coordinates = $geometry['coordinates'] ?? null;
|
||||
|
||||
if (!$type || !$coordinates) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$wkt = null;
|
||||
$allPoints = [];
|
||||
|
||||
switch ($type) {
|
||||
case 'Polygon':
|
||||
// Un seul polygone
|
||||
$ring = $coordinates[0]; // Anneau extérieur
|
||||
$points = [];
|
||||
foreach ($ring as $point) {
|
||||
$points[] = $point[0] . ' ' . $point[1];
|
||||
$allPoints[] = $point;
|
||||
}
|
||||
$wkt = 'POLYGON((' . implode(',', $points) . '))';
|
||||
break;
|
||||
|
||||
case 'MultiPolygon':
|
||||
// Plusieurs polygones
|
||||
$polygons = [];
|
||||
foreach ($coordinates as $polygon) {
|
||||
$ring = $polygon[0]; // Anneau extérieur du polygone
|
||||
$points = [];
|
||||
foreach ($ring as $point) {
|
||||
$points[] = $point[0] . ' ' . $point[1];
|
||||
$allPoints[] = $point;
|
||||
}
|
||||
$polygons[] = '((' . implode(',', $points) . '))';
|
||||
}
|
||||
$wkt = 'MULTIPOLYGON(' . implode(',', $polygons) . ')';
|
||||
break;
|
||||
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!$wkt || empty($allPoints)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Calculer la bounding box
|
||||
$lats = array_map(function($p) { return $p[1]; }, $allPoints);
|
||||
$lngs = array_map(function($p) { return $p[0]; }, $allPoints);
|
||||
|
||||
return [
|
||||
'wkt' => $wkt,
|
||||
'bbox' => [
|
||||
'min_lat' => min($lats),
|
||||
'max_lat' => max($lats),
|
||||
'min_lng' => min($lngs),
|
||||
'max_lng' => max($lngs)
|
||||
]
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
// Si le script est exécuté directement
|
||||
if (php_sapi_name() === 'cli' && basename(__FILE__) === basename($_SERVER['PHP_SELF'] ?? __FILE__)) {
|
||||
require_once __DIR__ . '/../src/Config/AppConfig.php';
|
||||
require_once __DIR__ . '/../src/Core/Database.php';
|
||||
|
||||
// Chemin vers le fichier GeoJSON
|
||||
$filePath = __DIR__ . '/../docs/contour-des-departements.geojson';
|
||||
|
||||
// Vérifier les arguments
|
||||
if ($argc > 1) {
|
||||
$filePath = $argv[1];
|
||||
}
|
||||
|
||||
echo "Import des contours départementaux depuis un fichier\n";
|
||||
echo "==================================================\n\n";
|
||||
echo "Fichier : $filePath\n\n";
|
||||
|
||||
$appConfig = AppConfig::getInstance();
|
||||
Database::init($appConfig->getDatabaseConfig());
|
||||
$db = Database::getInstance();
|
||||
|
||||
$importer = new DepartementContoursFileImporter($db);
|
||||
$log = $importer->importFromFile($filePath);
|
||||
|
||||
foreach ($log as $line) {
|
||||
echo $line . "\n";
|
||||
}
|
||||
}
|
||||
175
api/scripts/import_department_boundaries.php
Normal file
175
api/scripts/import_department_boundaries.php
Normal file
@@ -0,0 +1,175 @@
|
||||
<?php
|
||||
/**
|
||||
* Script d'import des contours des départements français
|
||||
*
|
||||
* Les données peuvent provenir de :
|
||||
* - IGN Admin Express : https://geoservices.ign.fr/adminexpress
|
||||
* - data.gouv.fr : https://www.data.gouv.fr/fr/datasets/contours-des-departements-francais-issus-d-openstreetmap/
|
||||
* - OpenStreetMap via Overpass API
|
||||
*
|
||||
* Format attendu : GeoJSON ou Shapefile converti en SQL
|
||||
*/
|
||||
|
||||
require_once __DIR__ . '/../src/Config/AppConfig.php';
|
||||
require_once __DIR__ . '/../src/Core/Database.php';
|
||||
|
||||
echo "Import des contours des départements\n";
|
||||
echo "===================================\n\n";
|
||||
|
||||
// Initialiser la base de données
|
||||
$appConfig = AppConfig::getInstance();
|
||||
Database::init($appConfig->getDatabaseConfig());
|
||||
$db = Database::getInstance();
|
||||
|
||||
// Exemple de données pour quelques départements bretons
|
||||
// En production, ces données viendraient d'un fichier GeoJSON ou d'une API
|
||||
$departements = [
|
||||
[
|
||||
'code' => '22',
|
||||
'nom' => 'Côtes-d\'Armor',
|
||||
// Contour simplifié - en réalité il faudrait des centaines de points
|
||||
'points' => [
|
||||
[-3.6546, 48.9012], [-3.3856, 48.8756], [-3.1234, 48.8234],
|
||||
[-2.7856, 48.7845], [-2.4567, 48.7234], [-2.1234, 48.6456],
|
||||
[-2.0123, 48.5234], [-2.0456, 48.3456], [-2.1567, 48.1234],
|
||||
[-2.3456, 48.0567], [-2.6789, 48.0789], [-3.0123, 48.1234],
|
||||
[-3.3456, 48.2345], [-3.5678, 48.4567], [-3.6234, 48.6789],
|
||||
[-3.6546, 48.9012] // Fermer le polygone
|
||||
]
|
||||
],
|
||||
[
|
||||
'code' => '29',
|
||||
'nom' => 'Finistère',
|
||||
'points' => [
|
||||
[-5.1423, 48.7523], [-4.8234, 48.6845], [-4.5123, 48.6234],
|
||||
[-4.2345, 48.5678], [-3.9876, 48.4567], [-3.7234, 48.3456],
|
||||
[-3.4567, 48.2345], [-3.3876, 48.0123], [-3.4234, 47.8234],
|
||||
[-3.5678, 47.6456], [-3.8765, 47.6789], [-4.2345, 47.7234],
|
||||
[-4.5678, 47.8234], [-4.8765, 47.9345], [-5.0876, 48.1234],
|
||||
[-5.1234, 48.3456], [-5.1345, 48.5678], [-5.1423, 48.7523]
|
||||
]
|
||||
],
|
||||
[
|
||||
'code' => '35',
|
||||
'nom' => 'Ille-et-Vilaine',
|
||||
'points' => [
|
||||
[-2.0123, 48.6456], [-1.7234, 48.5678], [-1.4567, 48.4567],
|
||||
[-1.2345, 48.3456], [-1.0234, 48.2345], [-1.0567, 48.0123],
|
||||
[-1.1234, 47.8234], [-1.2567, 47.6456], [-1.4678, 47.6789],
|
||||
[-1.7234, 47.7234], [-1.9876, 47.8234], [-2.1234, 47.9345],
|
||||
[-2.2345, 48.1234], [-2.1567, 48.3456], [-2.0678, 48.5234],
|
||||
[-2.0123, 48.6456]
|
||||
]
|
||||
],
|
||||
[
|
||||
'code' => '56',
|
||||
'nom' => 'Morbihan',
|
||||
'points' => [
|
||||
[-3.4567, 48.2345], [-3.2345, 48.1234], [-2.9876, 48.0123],
|
||||
[-2.7234, 47.9234], [-2.4567, 47.8345], [-2.2345, 47.7456],
|
||||
[-2.1234, 47.6234], [-2.2567, 47.4567], [-2.4678, 47.3456],
|
||||
[-2.7234, 47.3789], [-3.0123, 47.4234], [-3.2876, 47.5234],
|
||||
[-3.5234, 47.6345], [-3.6789, 47.7456], [-3.7234, 47.9234],
|
||||
[-3.6567, 48.0789], [-3.4567, 48.2345]
|
||||
]
|
||||
]
|
||||
];
|
||||
|
||||
try {
|
||||
$db->beginTransaction();
|
||||
|
||||
// Préparer la requête d'insertion
|
||||
$sql = "INSERT INTO departements_contours
|
||||
(code_dept, nom_dept, contour, bbox_min_lat, bbox_max_lat, bbox_min_lng, bbox_max_lng)
|
||||
VALUES
|
||||
(:code, :nom, ST_GeomFromText(:polygon, 4326), :min_lat, :max_lat, :min_lng, :max_lng)
|
||||
ON DUPLICATE KEY UPDATE
|
||||
nom_dept = VALUES(nom_dept),
|
||||
contour = VALUES(contour),
|
||||
bbox_min_lat = VALUES(bbox_min_lat),
|
||||
bbox_max_lat = VALUES(bbox_max_lat),
|
||||
bbox_min_lng = VALUES(bbox_min_lng),
|
||||
bbox_max_lng = VALUES(bbox_max_lng),
|
||||
updated_at = CURRENT_TIMESTAMP";
|
||||
|
||||
$stmt = $db->prepare($sql);
|
||||
|
||||
foreach ($departements as $dept) {
|
||||
echo "Import du département {$dept['code']} - {$dept['nom']}...\n";
|
||||
|
||||
// Créer le polygone WKT
|
||||
$polygonPoints = [];
|
||||
$lats = [];
|
||||
$lngs = [];
|
||||
|
||||
foreach ($dept['points'] as $point) {
|
||||
$lng = $point[0];
|
||||
$lat = $point[1];
|
||||
$polygonPoints[] = "$lng $lat";
|
||||
$lats[] = $lat;
|
||||
$lngs[] = $lng;
|
||||
}
|
||||
|
||||
$polygon = 'POLYGON((' . implode(',', $polygonPoints) . '))';
|
||||
|
||||
// Calculer la bounding box
|
||||
$minLat = min($lats);
|
||||
$maxLat = max($lats);
|
||||
$minLng = min($lngs);
|
||||
$maxLng = max($lngs);
|
||||
|
||||
// Exécuter l'insertion
|
||||
$stmt->execute([
|
||||
'code' => $dept['code'],
|
||||
'nom' => $dept['nom'],
|
||||
'polygon' => $polygon,
|
||||
'min_lat' => $minLat,
|
||||
'max_lat' => $maxLat,
|
||||
'min_lng' => $minLng,
|
||||
'max_lng' => $maxLng
|
||||
]);
|
||||
|
||||
echo "✓ Département {$dept['code']} importé\n";
|
||||
}
|
||||
|
||||
$db->commit();
|
||||
|
||||
echo "\n✓ Import terminé avec succès!\n\n";
|
||||
|
||||
// Vérifier les données importées
|
||||
$checkSql = "SELECT code_dept, nom_dept,
|
||||
ST_Area(contour) as area,
|
||||
ST_NumPoints(ST_ExteriorRing(contour)) as num_points
|
||||
FROM x_departements_contours
|
||||
ORDER BY code_dept";
|
||||
|
||||
$result = $db->query($checkSql);
|
||||
echo "Départements importés:\n";
|
||||
echo "---------------------\n";
|
||||
|
||||
foreach ($result as $row) {
|
||||
echo sprintf("- %s (%s) : %d points, aire: %.4f\n",
|
||||
$row['nom_dept'],
|
||||
$row['code_dept'],
|
||||
$row['num_points'],
|
||||
$row['area']
|
||||
);
|
||||
}
|
||||
|
||||
} catch (Exception $e) {
|
||||
$db->rollBack();
|
||||
echo "✗ Erreur lors de l'import : " . $e->getMessage() . "\n";
|
||||
}
|
||||
|
||||
echo "\n";
|
||||
echo "Note importante:\n";
|
||||
echo "---------------\n";
|
||||
echo "Ce script utilise des données simplifiées pour l'exemple.\n";
|
||||
echo "Pour un usage en production, vous devez :\n";
|
||||
echo "1. Télécharger les vrais contours depuis l'IGN ou data.gouv.fr\n";
|
||||
echo "2. Les convertir en format GeoJSON ou SQL\n";
|
||||
echo "3. Adapter ce script pour lire ces fichiers\n";
|
||||
echo "\n";
|
||||
echo "Sources recommandées:\n";
|
||||
echo "- IGN Admin Express: https://geoservices.ign.fr/adminexpress\n";
|
||||
echo "- data.gouv.fr: https://www.data.gouv.fr/fr/datasets/contours-des-departements-francais-issus-d-openstreetmap/\n";
|
||||
318
api/scripts/init_departements_contours.php
Normal file
318
api/scripts/init_departements_contours.php
Normal file
@@ -0,0 +1,318 @@
|
||||
<?php
|
||||
/**
|
||||
* Script d'initialisation des contours des départements français
|
||||
* À exécuter une seule fois lors de la connexion de l'admin d6soft
|
||||
*
|
||||
* Utilise l'API geo.api.gouv.fr pour récupérer les contours GeoJSON
|
||||
*/
|
||||
|
||||
class DepartementContoursInitializer {
|
||||
private PDO $db;
|
||||
private array $log = [];
|
||||
|
||||
// Liste complète des départements français (métropole + DOM-TOM)
|
||||
private array $departements = [
|
||||
// Métropole
|
||||
'01' => 'Ain', '02' => 'Aisne', '03' => 'Allier', '04' => 'Alpes-de-Haute-Provence',
|
||||
'05' => 'Hautes-Alpes', '06' => 'Alpes-Maritimes', '07' => 'Ardèche', '08' => 'Ardennes',
|
||||
'09' => 'Ariège', '10' => 'Aube', '11' => 'Aude', '12' => 'Aveyron',
|
||||
'13' => 'Bouches-du-Rhône', '14' => 'Calvados', '15' => 'Cantal', '16' => 'Charente',
|
||||
'17' => 'Charente-Maritime', '18' => 'Cher', '19' => 'Corrèze', '2A' => 'Corse-du-Sud',
|
||||
'2B' => 'Haute-Corse', '21' => 'Côte-d\'Or', '22' => 'Côtes-d\'Armor', '23' => 'Creuse',
|
||||
'24' => 'Dordogne', '25' => 'Doubs', '26' => 'Drôme', '27' => 'Eure',
|
||||
'28' => 'Eure-et-Loir', '29' => 'Finistère', '30' => 'Gard', '31' => 'Haute-Garonne',
|
||||
'32' => 'Gers', '33' => 'Gironde', '34' => 'Hérault', '35' => 'Ille-et-Vilaine',
|
||||
'36' => 'Indre', '37' => 'Indre-et-Loire', '38' => 'Isère', '39' => 'Jura',
|
||||
'40' => 'Landes', '41' => 'Loir-et-Cher', '42' => 'Loire', '43' => 'Haute-Loire',
|
||||
'44' => 'Loire-Atlantique', '45' => 'Loiret', '46' => 'Lot', '47' => 'Lot-et-Garonne',
|
||||
'48' => 'Lozère', '49' => 'Maine-et-Loire', '50' => 'Manche', '51' => 'Marne',
|
||||
'52' => 'Haute-Marne', '53' => 'Mayenne', '54' => 'Meurthe-et-Moselle', '55' => 'Meuse',
|
||||
'56' => 'Morbihan', '57' => 'Moselle', '58' => 'Nièvre', '59' => 'Nord',
|
||||
'60' => 'Oise', '61' => 'Orne', '62' => 'Pas-de-Calais', '63' => 'Puy-de-Dôme',
|
||||
'64' => 'Pyrénées-Atlantiques', '65' => 'Hautes-Pyrénées', '66' => 'Pyrénées-Orientales', '67' => 'Bas-Rhin',
|
||||
'68' => 'Haut-Rhin', '69' => 'Rhône', '70' => 'Haute-Saône', '71' => 'Saône-et-Loire',
|
||||
'72' => 'Sarthe', '73' => 'Savoie', '74' => 'Haute-Savoie', '75' => 'Paris',
|
||||
'76' => 'Seine-Maritime', '77' => 'Seine-et-Marne', '78' => 'Yvelines', '79' => 'Deux-Sèvres',
|
||||
'80' => 'Somme', '81' => 'Tarn', '82' => 'Tarn-et-Garonne', '83' => 'Var',
|
||||
'84' => 'Vaucluse', '85' => 'Vendée', '86' => 'Vienne', '87' => 'Haute-Vienne',
|
||||
'88' => 'Vosges', '89' => 'Yonne', '90' => 'Territoire de Belfort', '91' => 'Essonne',
|
||||
'92' => 'Hauts-de-Seine', '93' => 'Seine-Saint-Denis', '94' => 'Val-de-Marne', '95' => 'Val-d\'Oise',
|
||||
// DOM-TOM
|
||||
'971' => 'Guadeloupe', '972' => 'Martinique', '973' => 'Guyane', '974' => 'La Réunion',
|
||||
'975' => 'Saint-Pierre-et-Miquelon', '976' => 'Mayotte', '977' => 'Saint-Barthélemy',
|
||||
'978' => 'Saint-Martin', '984' => 'Terres australes et antarctiques françaises',
|
||||
'986' => 'Wallis-et-Futuna', '987' => 'Polynésie française', '988' => 'Nouvelle-Calédonie'
|
||||
];
|
||||
|
||||
public function __construct(PDO $db) {
|
||||
$this->db = $db;
|
||||
}
|
||||
|
||||
/**
|
||||
* Vérifie si la table existe
|
||||
*/
|
||||
public function tableExists(): bool {
|
||||
try {
|
||||
$sql = "SHOW TABLES LIKE 'x_departements_contours'";
|
||||
$stmt = $this->db->query($sql);
|
||||
return $stmt->rowCount() > 0;
|
||||
} catch (Exception $e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Vérifie si la table est vide
|
||||
*/
|
||||
private function isTableEmpty(): bool {
|
||||
try {
|
||||
$sql = "SELECT COUNT(*) as count FROM x_departements_contours";
|
||||
$stmt = $this->db->query($sql);
|
||||
$result = $stmt->fetch();
|
||||
return $result['count'] == 0;
|
||||
} catch (Exception $e) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Récupère le contour d'un département depuis l'API geo.api.gouv.fr
|
||||
*/
|
||||
private function fetchDepartementContour(string $code, string $nom): ?array {
|
||||
// URL de l'API pour récupérer le contour du département en GeoJSON
|
||||
$url = "https://geo.api.gouv.fr/departements/{$code}?geometry=contour";
|
||||
|
||||
$context = stream_context_create([
|
||||
'http' => [
|
||||
'timeout' => 30,
|
||||
'header' => "User-Agent: Geosector/1.0\r\n"
|
||||
]
|
||||
]);
|
||||
|
||||
$response = @file_get_contents($url, false, $context);
|
||||
|
||||
if ($response === false) {
|
||||
$this->log[] = "✗ Erreur API pour département $code ($nom)";
|
||||
return null;
|
||||
}
|
||||
|
||||
$data = json_decode($response, true);
|
||||
|
||||
// L'API peut retourner le contour dans 'contour' ou 'geometry'
|
||||
if (isset($data['contour']) && isset($data['contour']['coordinates'])) {
|
||||
return $data['contour'];
|
||||
} elseif (isset($data['geometry']) && isset($data['geometry']['coordinates'])) {
|
||||
return $data['geometry'];
|
||||
} else {
|
||||
$this->log[] = "✗ Pas de contour pour département $code ($nom)";
|
||||
// Debug : afficher les clés disponibles
|
||||
if (is_array($data)) {
|
||||
$this->log[] = " Clés disponibles : " . implode(', ', array_keys($data));
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Convertit les coordonnées GeoJSON en WKT Polygon pour MySQL
|
||||
*/
|
||||
private function geoJsonToWkt(array $coordinates): ?array {
|
||||
if (empty($coordinates) || !is_array($coordinates[0])) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// GeoJSON peut avoir plusieurs niveaux d'imbrication selon le type
|
||||
// Pour un Polygon simple
|
||||
if (isset($coordinates[0][0]) && is_numeric($coordinates[0][0])) {
|
||||
$ring = $coordinates;
|
||||
}
|
||||
// Pour un MultiPolygon, prendre le premier polygone
|
||||
elseif (isset($coordinates[0][0][0])) {
|
||||
$ring = $coordinates[0][0];
|
||||
}
|
||||
// Pour un Polygon standard
|
||||
else {
|
||||
$ring = $coordinates[0];
|
||||
}
|
||||
|
||||
$points = [];
|
||||
$lats = [];
|
||||
$lngs = [];
|
||||
|
||||
foreach ($ring as $point) {
|
||||
if (count($point) >= 2) {
|
||||
$lng = $point[0];
|
||||
$lat = $point[1];
|
||||
$points[] = "$lng $lat";
|
||||
$lats[] = $lat;
|
||||
$lngs[] = $lng;
|
||||
}
|
||||
}
|
||||
|
||||
if (count($points) < 3) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Fermer le polygone si nécessaire
|
||||
if ($points[0] !== $points[count($points) - 1]) {
|
||||
$points[] = $points[0];
|
||||
}
|
||||
|
||||
return [
|
||||
'wkt' => 'POLYGON((' . implode(',', $points) . '))',
|
||||
'bbox' => [
|
||||
'min_lat' => min($lats),
|
||||
'max_lat' => max($lats),
|
||||
'min_lng' => min($lngs),
|
||||
'max_lng' => max($lngs)
|
||||
]
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Importe tous les départements
|
||||
*/
|
||||
public function importAll(): array {
|
||||
$this->log[] = "Début de l'import des contours départementaux";
|
||||
$this->log[] = "Source : API geo.api.gouv.fr";
|
||||
$this->log[] = "";
|
||||
|
||||
// Vérifier que la table est vide avant d'importer
|
||||
if (!$this->isTableEmpty()) {
|
||||
$this->log[] = "✗ La table x_departements_contours contient déjà des données";
|
||||
return $this->log;
|
||||
}
|
||||
|
||||
// Préparer la requête d'insertion
|
||||
$sql = "INSERT INTO x_departements_contours
|
||||
(code_dept, nom_dept, contour, bbox_min_lat, bbox_max_lat, bbox_min_lng, bbox_max_lng)
|
||||
VALUES
|
||||
(:code, :nom, ST_GeomFromText(:polygon, 4326), :min_lat, :max_lat, :min_lng, :max_lng)";
|
||||
|
||||
$stmt = $this->db->prepare($sql);
|
||||
|
||||
$success = 0;
|
||||
$errors = 0;
|
||||
|
||||
// Démarrer une transaction
|
||||
$this->db->beginTransaction();
|
||||
|
||||
try {
|
||||
foreach ($this->departements as $code => $nom) {
|
||||
// Petite pause pour ne pas surcharger l'API
|
||||
usleep(100000); // 100ms
|
||||
|
||||
$contour = $this->fetchDepartementContour($code, $nom);
|
||||
|
||||
if (!$contour) {
|
||||
$errors++;
|
||||
continue;
|
||||
}
|
||||
|
||||
$wktData = $this->geoJsonToWkt($contour['coordinates']);
|
||||
|
||||
if (!$wktData) {
|
||||
$this->log[] = "✗ Conversion échouée pour $code ($nom)";
|
||||
$errors++;
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
$stmt->execute([
|
||||
'code' => $code,
|
||||
'nom' => $nom,
|
||||
'polygon' => $wktData['wkt'],
|
||||
'min_lat' => $wktData['bbox']['min_lat'],
|
||||
'max_lat' => $wktData['bbox']['max_lat'],
|
||||
'min_lng' => $wktData['bbox']['min_lng'],
|
||||
'max_lng' => $wktData['bbox']['max_lng']
|
||||
]);
|
||||
|
||||
$this->log[] = "✓ $code - $nom importé";
|
||||
$success++;
|
||||
|
||||
} catch (Exception $e) {
|
||||
$this->log[] = "✗ Erreur SQL pour $code ($nom) : " . $e->getMessage();
|
||||
$errors++;
|
||||
}
|
||||
}
|
||||
|
||||
// Si tout s'est bien passé, valider la transaction
|
||||
if ($success > 0) {
|
||||
$this->db->commit();
|
||||
$this->log[] = "";
|
||||
$this->log[] = "✓ Transaction validée";
|
||||
} else {
|
||||
$this->db->rollBack();
|
||||
$this->log[] = "";
|
||||
$this->log[] = "✗ Transaction annulée (aucun import réussi)";
|
||||
}
|
||||
|
||||
} catch (Exception $e) {
|
||||
$this->db->rollBack();
|
||||
$this->log[] = "";
|
||||
$this->log[] = "✗ Erreur fatale : " . $e->getMessage();
|
||||
$this->log[] = "✗ Transaction annulée";
|
||||
$errors = count($this->departements);
|
||||
}
|
||||
|
||||
$this->log[] = "";
|
||||
$this->log[] = "Import terminé : $success réussis, $errors erreurs";
|
||||
|
||||
return $this->log;
|
||||
}
|
||||
|
||||
/**
|
||||
* Exécute l'initialisation si nécessaire
|
||||
*/
|
||||
public static function runIfNeeded(PDO $db, string $username): ?array {
|
||||
// Vérifier que c'est bien l'admin d6soft
|
||||
if ($username !== 'd6soft') {
|
||||
return null;
|
||||
}
|
||||
|
||||
$initializer = new self($db);
|
||||
|
||||
// Vérifier si la table existe
|
||||
if (!$initializer->tableExists()) {
|
||||
return ["✗ La table x_departements_contours n'existe pas. Veuillez la créer avec le script SQL fourni."];
|
||||
}
|
||||
|
||||
// Vérifier si elle est vide
|
||||
if (!$initializer->isTableEmpty()) {
|
||||
return null; // Table déjà remplie, rien à faire
|
||||
}
|
||||
|
||||
// Vérifier si le fichier local existe
|
||||
$localFile = __DIR__ . '/../docs/contour-des-departements.geojson';
|
||||
if (file_exists($localFile)) {
|
||||
// Utiliser le fichier local
|
||||
require_once __DIR__ . '/import_departements_from_file.php';
|
||||
$fileImporter = new \DepartementContoursFileImporter($db);
|
||||
return $fileImporter->importFromFile($localFile);
|
||||
}
|
||||
|
||||
// Sinon, utiliser l'API (qui ne fonctionne pas bien actuellement)
|
||||
return $initializer->importAll();
|
||||
}
|
||||
}
|
||||
|
||||
// Si le script est exécuté directement (pour tests)
|
||||
if (php_sapi_name() === 'cli' && basename(__FILE__) === basename($_SERVER['PHP_SELF'] ?? __FILE__)) {
|
||||
require_once __DIR__ . '/../src/Config/AppConfig.php';
|
||||
require_once __DIR__ . '/../src/Core/Database.php';
|
||||
|
||||
$appConfig = AppConfig::getInstance();
|
||||
Database::init($appConfig->getDatabaseConfig());
|
||||
$db = Database::getInstance();
|
||||
|
||||
echo "Test d'import des contours départementaux\n";
|
||||
echo "========================================\n\n";
|
||||
|
||||
$initializer = new DepartementContoursInitializer($db);
|
||||
$log = $initializer->importAll();
|
||||
|
||||
foreach ($log as $line) {
|
||||
echo $line . "\n";
|
||||
}
|
||||
}
|
||||
298
api/scripts/migrate_uploads_structure.php
Normal file
298
api/scripts/migrate_uploads_structure.php
Normal file
@@ -0,0 +1,298 @@
|
||||
#!/usr/bin/env php
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Script de migration de l'arborescence des uploads
|
||||
* Réorganise les fichiers existants vers la nouvelle structure simplifiée
|
||||
*
|
||||
* Ancienne structure : uploads/entites/{id}/* et uploads/{id}/*
|
||||
* Nouvelle structure : uploads/{id}/*
|
||||
*
|
||||
* Usage: php scripts/migrate_uploads_structure.php [--dry-run]
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
// Chemin de base des uploads
|
||||
const BASE_PATH = '/var/www/geosector/api/uploads';
|
||||
const LOG_FILE = '/var/www/geosector/api/logs/migration_uploads_' . date('Ymd_His') . '.log';
|
||||
|
||||
// Mode dry-run (simulation sans modification)
|
||||
$dryRun = in_array('--dry-run', $argv);
|
||||
|
||||
// Fonction pour logger
|
||||
function logMessage(string $message, string $level = 'INFO'): void {
|
||||
$timestamp = date('Y-m-d H:i:s');
|
||||
$log = "[$timestamp] [$level] $message" . PHP_EOL;
|
||||
echo $log;
|
||||
if (!$GLOBALS['dryRun']) {
|
||||
file_put_contents(LOG_FILE, $log, FILE_APPEND);
|
||||
}
|
||||
}
|
||||
|
||||
// Fonction pour déplacer un fichier ou dossier
|
||||
function moveItem(string $source, string $destination): bool {
|
||||
global $dryRun;
|
||||
|
||||
if (!file_exists($source)) {
|
||||
logMessage("Source n'existe pas: $source", 'WARNING');
|
||||
return false;
|
||||
}
|
||||
|
||||
// Créer le dossier de destination si nécessaire
|
||||
$destDir = dirname($destination);
|
||||
if (!is_dir($destDir)) {
|
||||
logMessage("Création du dossier: $destDir");
|
||||
if (!$dryRun) {
|
||||
mkdir($destDir, 0775, true);
|
||||
chown($destDir, 'nginx');
|
||||
chgrp($destDir, 'nobody');
|
||||
}
|
||||
}
|
||||
|
||||
// Déplacer l'élément
|
||||
logMessage("Déplacement: $source -> $destination");
|
||||
if (!$dryRun) {
|
||||
if (is_dir($source)) {
|
||||
// Pour un dossier, utiliser rename
|
||||
return rename($source, $destination);
|
||||
} else {
|
||||
// Pour un fichier
|
||||
return rename($source, $destination);
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// Fonction pour copier récursivement un dossier
|
||||
function copyDirectory(string $source, string $dest): bool {
|
||||
global $dryRun;
|
||||
|
||||
if (!is_dir($source)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!$dryRun) {
|
||||
if (!is_dir($dest)) {
|
||||
mkdir($dest, 0775, true);
|
||||
chown($dest, 'nginx');
|
||||
chgrp($dest, 'nobody');
|
||||
}
|
||||
}
|
||||
|
||||
$dir = opendir($source);
|
||||
while (($file = readdir($dir)) !== false) {
|
||||
if ($file === '.' || $file === '..') {
|
||||
continue;
|
||||
}
|
||||
|
||||
$srcPath = "$source/$file";
|
||||
$destPath = "$dest/$file";
|
||||
|
||||
if (is_dir($srcPath)) {
|
||||
copyDirectory($srcPath, $destPath);
|
||||
} else {
|
||||
logMessage("Copie: $srcPath -> $destPath");
|
||||
if (!$dryRun) {
|
||||
copy($srcPath, $destPath);
|
||||
chmod($destPath, 0664);
|
||||
chown($destPath, 'nginx');
|
||||
chgrp($destPath, 'nobody');
|
||||
}
|
||||
}
|
||||
}
|
||||
closedir($dir);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// Fonction principale de migration
|
||||
function migrateUploads(): void {
|
||||
global $dryRun;
|
||||
|
||||
logMessage("=== Début de la migration des uploads ===");
|
||||
logMessage($dryRun ? "MODE DRY-RUN (simulation)" : "MODE RÉEL (modifications effectives)");
|
||||
|
||||
// 1. Migrer uploads/entites/* vers uploads/*
|
||||
$entitesPath = BASE_PATH . '/entites';
|
||||
if (is_dir($entitesPath)) {
|
||||
logMessage("Traitement du dossier entites/");
|
||||
|
||||
$entites = scandir($entitesPath);
|
||||
foreach ($entites as $entiteId) {
|
||||
if ($entiteId === '.' || $entiteId === '..') continue;
|
||||
|
||||
$oldPath = "$entitesPath/$entiteId";
|
||||
$newPath = BASE_PATH . "/$entiteId";
|
||||
|
||||
if (!is_dir($oldPath)) continue;
|
||||
|
||||
logMessage("Migration entité $entiteId");
|
||||
|
||||
// Si le dossier destination existe déjà, fusionner
|
||||
if (is_dir($newPath)) {
|
||||
logMessage("Le dossier $entiteId existe déjà à la racine, fusion nécessaire", 'INFO');
|
||||
|
||||
// Migrer les sous-dossiers
|
||||
$subDirs = scandir($oldPath);
|
||||
foreach ($subDirs as $subDir) {
|
||||
if ($subDir === '.' || $subDir === '..') continue;
|
||||
|
||||
$oldSubPath = "$oldPath/$subDir";
|
||||
$newSubPath = "$newPath/$subDir";
|
||||
|
||||
if ($subDir === 'operations') {
|
||||
// Traiter spécialement le dossier operations
|
||||
migrateOperations($oldSubPath, $newSubPath);
|
||||
} else {
|
||||
// Pour logo et recus, déplacer directement
|
||||
if (!is_dir($newSubPath)) {
|
||||
moveItem($oldSubPath, $newSubPath);
|
||||
} else {
|
||||
logMessage("Le dossier $newSubPath existe déjà, fusion du contenu");
|
||||
copyDirectory($oldSubPath, $newSubPath);
|
||||
if (!$dryRun) {
|
||||
// Supprimer l'ancien après copie
|
||||
exec("rm -rf " . escapeshellarg($oldSubPath));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Déplacer simplement le dossier entier
|
||||
moveItem($oldPath, $newPath);
|
||||
}
|
||||
}
|
||||
|
||||
// Supprimer le dossier entites vide
|
||||
if (!$dryRun) {
|
||||
if (count(scandir($entitesPath)) === 2) { // Seulement . et ..
|
||||
rmdir($entitesPath);
|
||||
logMessage("Suppression du dossier entites/ vide");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Nettoyer la structure des dossiers operations
|
||||
logMessage("Nettoyage de la structure des dossiers operations");
|
||||
cleanupOperationsStructure();
|
||||
|
||||
logMessage("=== Migration terminée ===");
|
||||
if (!$dryRun) {
|
||||
logMessage("Logs sauvegardés dans: " . LOG_FILE);
|
||||
}
|
||||
}
|
||||
|
||||
// Fonction pour migrer le dossier operations avec simplification
|
||||
function migrateOperations(string $oldPath, string $newPath): void {
|
||||
global $dryRun;
|
||||
|
||||
if (!is_dir($oldPath)) return;
|
||||
|
||||
logMessage("Migration du dossier operations: $oldPath");
|
||||
|
||||
if (!$dryRun && !is_dir($newPath)) {
|
||||
mkdir($newPath, 0775, true);
|
||||
chown($newPath, 'nginx');
|
||||
chgrp($newPath, 'nobody');
|
||||
}
|
||||
|
||||
$operations = scandir($oldPath);
|
||||
foreach ($operations as $opId) {
|
||||
if ($opId === '.' || $opId === '..') continue;
|
||||
|
||||
$oldOpPath = "$oldPath/$opId";
|
||||
$newOpPath = "$newPath/$opId";
|
||||
|
||||
// Simplifier la structure: déplacer les xlsx directement dans operations/{id}/
|
||||
if (is_dir("$oldOpPath/documents/exports/excel")) {
|
||||
$excelPath = "$oldOpPath/documents/exports/excel";
|
||||
$files = scandir($excelPath);
|
||||
|
||||
foreach ($files as $file) {
|
||||
if ($file === '.' || $file === '..' || !str_ends_with($file, '.xlsx')) continue;
|
||||
|
||||
$oldFilePath = "$excelPath/$file";
|
||||
$newFilePath = "$newOpPath/$file";
|
||||
|
||||
logMessage("Déplacement Excel: $oldFilePath -> $newFilePath");
|
||||
|
||||
if (!$dryRun) {
|
||||
if (!is_dir($newOpPath)) {
|
||||
mkdir($newOpPath, 0775, true);
|
||||
chown($newOpPath, 'nginx');
|
||||
chgrp($newOpPath, 'nobody');
|
||||
}
|
||||
rename($oldFilePath, $newFilePath);
|
||||
chmod($newFilePath, 0664);
|
||||
chown($newFilePath, 'nginx');
|
||||
chgrp($newFilePath, 'nobody');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Fonction pour nettoyer la structure après migration
|
||||
function cleanupOperationsStructure(): void {
|
||||
global $dryRun;
|
||||
|
||||
$uploadsDir = BASE_PATH;
|
||||
$entites = scandir($uploadsDir);
|
||||
|
||||
foreach ($entites as $entiteId) {
|
||||
if ($entiteId === '.' || $entiteId === '..' || $entiteId === 'entites') continue;
|
||||
|
||||
$operationsPath = "$uploadsDir/$entiteId/operations";
|
||||
if (!is_dir($operationsPath)) continue;
|
||||
|
||||
$operations = scandir($operationsPath);
|
||||
foreach ($operations as $opId) {
|
||||
if ($opId === '.' || $opId === '..') continue;
|
||||
|
||||
$opPath = "$operationsPath/$opId";
|
||||
|
||||
// Supprimer l'ancienne structure documents/exports/excel si elle est vide
|
||||
$oldStructure = "$opPath/documents";
|
||||
if (is_dir($oldStructure)) {
|
||||
logMessage("Suppression de l'ancienne structure: $oldStructure");
|
||||
if (!$dryRun) {
|
||||
exec("rm -rf " . escapeshellarg($oldStructure));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Vérifier les permissions
|
||||
if (!is_dir(BASE_PATH)) {
|
||||
die("ERREUR: Le dossier " . BASE_PATH . " n'existe pas\n");
|
||||
}
|
||||
|
||||
if (!is_writable(BASE_PATH) && !$dryRun) {
|
||||
die("ERREUR: Le dossier " . BASE_PATH . " n'est pas accessible en écriture\n");
|
||||
}
|
||||
|
||||
// Lancer la migration
|
||||
try {
|
||||
migrateUploads();
|
||||
|
||||
if ($dryRun) {
|
||||
echo "\n";
|
||||
echo "========================================\n";
|
||||
echo "SIMULATION TERMINÉE\n";
|
||||
echo "Pour exécuter réellement la migration:\n";
|
||||
echo "php " . $argv[0] . "\n";
|
||||
echo "========================================\n";
|
||||
} else {
|
||||
echo "\n";
|
||||
echo "========================================\n";
|
||||
echo "MIGRATION TERMINÉE AVEC SUCCÈS\n";
|
||||
echo "Vérifiez les logs: " . LOG_FILE . "\n";
|
||||
echo "========================================\n";
|
||||
}
|
||||
} catch (Exception $e) {
|
||||
logMessage("ERREUR FATALE: " . $e->getMessage(), 'ERROR');
|
||||
exit(1);
|
||||
}
|
||||
197
api/scripts/migrations/stripe_tables.sql
Normal file
197
api/scripts/migrations/stripe_tables.sql
Normal file
@@ -0,0 +1,197 @@
|
||||
-- =============================================================
|
||||
-- Tables pour l'intégration Stripe Connect + Terminal
|
||||
-- Date: 2025-09-01
|
||||
-- Version: 1.0.0
|
||||
-- Préfixe: stripe_
|
||||
-- =============================================================
|
||||
|
||||
-- Table pour stocker les comptes Stripe Connect des amicales
|
||||
CREATE TABLE IF NOT EXISTS stripe_accounts (
|
||||
id INT(10) UNSIGNED PRIMARY KEY AUTO_INCREMENT,
|
||||
fk_entite INT(10) UNSIGNED NOT NULL,
|
||||
stripe_account_id VARCHAR(255) UNIQUE,
|
||||
stripe_location_id VARCHAR(255),
|
||||
charges_enabled BOOLEAN DEFAULT FALSE,
|
||||
payouts_enabled BOOLEAN DEFAULT FALSE,
|
||||
onboarding_completed BOOLEAN DEFAULT FALSE,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (fk_entite) REFERENCES entites(id) ON DELETE CASCADE,
|
||||
INDEX idx_fk_entite (fk_entite),
|
||||
INDEX idx_stripe_account_id (stripe_account_id)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
-- Table pour stocker les intentions de paiement
|
||||
CREATE TABLE IF NOT EXISTS stripe_payment_intents (
|
||||
id INT(10) UNSIGNED PRIMARY KEY AUTO_INCREMENT,
|
||||
stripe_payment_intent_id VARCHAR(255) UNIQUE,
|
||||
fk_entite INT(10) UNSIGNED NOT NULL,
|
||||
fk_user INT(10) UNSIGNED NOT NULL,
|
||||
amount INT NOT NULL COMMENT 'Montant en centimes',
|
||||
currency VARCHAR(3) DEFAULT 'eur',
|
||||
status VARCHAR(50),
|
||||
application_fee INT COMMENT 'Commission en centimes',
|
||||
metadata JSON,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (fk_entite) REFERENCES entites(id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (fk_user) REFERENCES users(id) ON DELETE CASCADE,
|
||||
INDEX idx_fk_entite (fk_entite),
|
||||
INDEX idx_fk_user (fk_user),
|
||||
INDEX idx_status (status),
|
||||
INDEX idx_created_at (created_at)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
-- Table pour les readers Terminal (Tap to Pay virtuel)
|
||||
CREATE TABLE IF NOT EXISTS stripe_terminal_readers (
|
||||
id INT(10) UNSIGNED PRIMARY KEY AUTO_INCREMENT,
|
||||
stripe_reader_id VARCHAR(255) UNIQUE,
|
||||
fk_entite INT(10) UNSIGNED NOT NULL,
|
||||
label VARCHAR(255),
|
||||
location VARCHAR(255),
|
||||
status VARCHAR(50),
|
||||
device_type VARCHAR(50) COMMENT 'ios_tap_to_pay, android_tap_to_pay',
|
||||
device_info JSON COMMENT 'Infos sur le device (modèle, OS, etc)',
|
||||
last_seen_at TIMESTAMP NULL,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (fk_entite) REFERENCES entites(id) ON DELETE CASCADE,
|
||||
INDEX idx_fk_entite (fk_entite),
|
||||
INDEX idx_device_type (device_type)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
-- Table pour les appareils Android certifiés Tap to Pay
|
||||
CREATE TABLE IF NOT EXISTS stripe_android_certified_devices (
|
||||
id INT(10) UNSIGNED PRIMARY KEY AUTO_INCREMENT,
|
||||
manufacturer VARCHAR(100),
|
||||
model VARCHAR(200),
|
||||
model_identifier VARCHAR(200),
|
||||
tap_to_pay_certified BOOLEAN DEFAULT FALSE,
|
||||
certification_date DATE,
|
||||
min_android_version INT,
|
||||
country VARCHAR(2) DEFAULT 'FR',
|
||||
notes TEXT,
|
||||
last_verified TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
INDEX idx_manufacturer_model (manufacturer, model),
|
||||
INDEX idx_certified (tap_to_pay_certified, country),
|
||||
UNIQUE KEY unique_device (manufacturer, model, model_identifier)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
-- Table pour l'historique des paiements (pour audit et réconciliation)
|
||||
CREATE TABLE IF NOT EXISTS stripe_payment_history (
|
||||
id INT(10) UNSIGNED PRIMARY KEY AUTO_INCREMENT,
|
||||
fk_payment_intent INT(10) UNSIGNED,
|
||||
event_type VARCHAR(50) COMMENT 'created, processing, succeeded, failed, refunded',
|
||||
event_data JSON,
|
||||
webhook_id VARCHAR(255),
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (fk_payment_intent) REFERENCES stripe_payment_intents(id) ON DELETE CASCADE,
|
||||
INDEX idx_fk_payment_intent (fk_payment_intent),
|
||||
INDEX idx_event_type (event_type),
|
||||
INDEX idx_created_at (created_at)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
-- Table pour les remboursements
|
||||
CREATE TABLE IF NOT EXISTS stripe_refunds (
|
||||
id INT(10) UNSIGNED PRIMARY KEY AUTO_INCREMENT,
|
||||
stripe_refund_id VARCHAR(255) UNIQUE,
|
||||
fk_payment_intent INT(10) UNSIGNED NOT NULL,
|
||||
amount INT NOT NULL COMMENT 'Montant remboursé en centimes',
|
||||
reason VARCHAR(100) COMMENT 'duplicate, fraudulent, requested_by_customer',
|
||||
status VARCHAR(50),
|
||||
metadata JSON,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (fk_payment_intent) REFERENCES stripe_payment_intents(id) ON DELETE CASCADE,
|
||||
INDEX idx_fk_payment_intent (fk_payment_intent),
|
||||
INDEX idx_status (status)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
-- Table pour les webhooks reçus (pour éviter les doublons et debug)
|
||||
CREATE TABLE IF NOT EXISTS stripe_webhooks (
|
||||
id INT(10) UNSIGNED PRIMARY KEY AUTO_INCREMENT,
|
||||
stripe_event_id VARCHAR(255) UNIQUE,
|
||||
event_type VARCHAR(100),
|
||||
livemode BOOLEAN DEFAULT FALSE,
|
||||
payload JSON,
|
||||
processed BOOLEAN DEFAULT FALSE,
|
||||
error_message TEXT NULL,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
processed_at TIMESTAMP NULL,
|
||||
INDEX idx_event_type (event_type),
|
||||
INDEX idx_processed (processed),
|
||||
INDEX idx_created_at (created_at)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
-- Insertion des appareils Android certifiés pour Tap to Pay en France
|
||||
INSERT INTO stripe_android_certified_devices (manufacturer, model, model_identifier, tap_to_pay_certified, min_android_version, certification_date) VALUES
|
||||
-- Samsung
|
||||
('Samsung', 'Galaxy S21', 'SM-G991B', TRUE, 11, '2023-01-01'),
|
||||
('Samsung', 'Galaxy S21+', 'SM-G996B', TRUE, 11, '2023-01-01'),
|
||||
('Samsung', 'Galaxy S21 Ultra', 'SM-G998B', TRUE, 11, '2023-01-01'),
|
||||
('Samsung', 'Galaxy S22', 'SM-S901B', TRUE, 12, '2023-01-01'),
|
||||
('Samsung', 'Galaxy S22+', 'SM-S906B', TRUE, 12, '2023-01-01'),
|
||||
('Samsung', 'Galaxy S22 Ultra', 'SM-S908B', TRUE, 12, '2023-01-01'),
|
||||
('Samsung', 'Galaxy S23', 'SM-S911B', TRUE, 13, '2023-06-01'),
|
||||
('Samsung', 'Galaxy S23+', 'SM-S916B', TRUE, 13, '2023-06-01'),
|
||||
('Samsung', 'Galaxy S23 Ultra', 'SM-S918B', TRUE, 13, '2023-06-01'),
|
||||
('Samsung', 'Galaxy S24', 'SM-S921B', TRUE, 14, '2024-01-01'),
|
||||
('Samsung', 'Galaxy S24+', 'SM-S926B', TRUE, 14, '2024-01-01'),
|
||||
('Samsung', 'Galaxy S24 Ultra', 'SM-S928B', TRUE, 14, '2024-01-01'),
|
||||
-- Google Pixel
|
||||
('Google', 'Pixel 6', 'oriole', TRUE, 12, '2023-01-01'),
|
||||
('Google', 'Pixel 6 Pro', 'raven', TRUE, 12, '2023-01-01'),
|
||||
('Google', 'Pixel 6a', 'bluejay', TRUE, 12, '2023-03-01'),
|
||||
('Google', 'Pixel 7', 'panther', TRUE, 13, '2023-03-01'),
|
||||
('Google', 'Pixel 7 Pro', 'cheetah', TRUE, 13, '2023-03-01'),
|
||||
('Google', 'Pixel 7a', 'lynx', TRUE, 13, '2023-06-01'),
|
||||
('Google', 'Pixel 8', 'shiba', TRUE, 14, '2023-10-01'),
|
||||
('Google', 'Pixel 8 Pro', 'husky', TRUE, 14, '2023-10-01'),
|
||||
('Google', 'Pixel Fold', 'felix', TRUE, 13, '2023-07-01'),
|
||||
-- OnePlus
|
||||
('OnePlus', '9', 'LE2113', TRUE, 11, '2023-03-01'),
|
||||
('OnePlus', '9 Pro', 'LE2123', TRUE, 11, '2023-03-01'),
|
||||
('OnePlus', '10 Pro', 'NE2213', TRUE, 12, '2023-06-01'),
|
||||
('OnePlus', '11', 'CPH2449', TRUE, 13, '2023-09-01'),
|
||||
-- Xiaomi
|
||||
('Xiaomi', 'Mi 11', 'M2011K2G', TRUE, 11, '2023-06-01'),
|
||||
('Xiaomi', '12', '2201123G', TRUE, 12, '2023-09-01'),
|
||||
('Xiaomi', '12 Pro', '2201122G', TRUE, 12, '2023-09-01'),
|
||||
('Xiaomi', '13', '2211133G', TRUE, 13, '2024-01-01'),
|
||||
('Xiaomi', '13 Pro', '2210132G', TRUE, 13, '2024-01-01');
|
||||
|
||||
-- Vue pour faciliter les requêtes de statistiques
|
||||
CREATE OR REPLACE VIEW v_stripe_payment_stats AS
|
||||
SELECT
|
||||
spi.fk_entite,
|
||||
e.encrypted_name AS entite_name,
|
||||
spi.fk_user,
|
||||
u.encrypted_name AS user_nom,
|
||||
u.first_name AS user_prenom,
|
||||
COUNT(CASE WHEN spi.status = 'succeeded' THEN 1 END) as total_ventes,
|
||||
SUM(CASE WHEN spi.status = 'succeeded' THEN spi.amount ELSE 0 END) as total_montant,
|
||||
SUM(CASE WHEN spi.status = 'succeeded' THEN spi.application_fee ELSE 0 END) as total_commissions,
|
||||
DATE(spi.created_at) as date_vente
|
||||
FROM stripe_payment_intents spi
|
||||
LEFT JOIN entites e ON spi.fk_entite = e.id
|
||||
LEFT JOIN users u ON spi.fk_user = u.id
|
||||
GROUP BY spi.fk_entite, spi.fk_user, DATE(spi.created_at);
|
||||
|
||||
-- Vue pour le dashboard des amicales
|
||||
CREATE OR REPLACE VIEW v_stripe_amicale_dashboard AS
|
||||
SELECT
|
||||
sa.fk_entite,
|
||||
e.encrypted_name AS entite_name,
|
||||
sa.stripe_account_id,
|
||||
sa.charges_enabled,
|
||||
sa.payouts_enabled,
|
||||
COUNT(DISTINCT spi.id) as total_transactions,
|
||||
SUM(CASE WHEN spi.status = 'succeeded' THEN spi.amount ELSE 0 END) as total_revenus,
|
||||
SUM(CASE WHEN spi.status = 'succeeded' THEN spi.application_fee ELSE 0 END) as total_frais_plateforme,
|
||||
MAX(spi.created_at) as derniere_transaction
|
||||
FROM stripe_accounts sa
|
||||
LEFT JOIN entites e ON sa.fk_entite = e.id
|
||||
LEFT JOIN stripe_payment_intents spi ON sa.fk_entite = spi.fk_entite
|
||||
GROUP BY sa.fk_entite, sa.stripe_account_id;
|
||||
0
api/scripts/php/MigrationConfig.php
Normal file → Executable file
0
api/scripts/php/MigrationConfig.php
Normal file → Executable file
249
api/scripts/php/init_security_tables.php
Normal file
249
api/scripts/php/init_security_tables.php
Normal file
@@ -0,0 +1,249 @@
|
||||
#!/usr/bin/env php
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Script d'initialisation des tables de sécurité
|
||||
* Crée les tables si elles n'existent pas
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
require_once __DIR__ . '/../../bootstrap.php';
|
||||
require_once __DIR__ . '/../../src/Config/AppConfig.php';
|
||||
require_once __DIR__ . '/../../src/Core/Database.php';
|
||||
|
||||
// Initialiser la configuration
|
||||
$appConfig = AppConfig::getInstance();
|
||||
$config = $appConfig->getFullConfig();
|
||||
|
||||
// Initialiser la base de données
|
||||
Database::init($config['database']);
|
||||
$db = Database::getInstance();
|
||||
|
||||
echo "\n========================================\n";
|
||||
echo " CRÉATION DES TABLES DE SÉCURITÉ\n";
|
||||
echo "========================================\n\n";
|
||||
|
||||
try {
|
||||
// Désactiver temporairement le mode strict pour les clés étrangères
|
||||
$db->exec("SET FOREIGN_KEY_CHECKS = 0");
|
||||
|
||||
// 1. Table des alertes
|
||||
echo "1. Création de la table sec_alerts...\n";
|
||||
$db->exec("
|
||||
CREATE TABLE IF NOT EXISTS `sec_alerts` (
|
||||
`id` INT(11) UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
||||
`alert_type` VARCHAR(50) NOT NULL COMMENT 'Type d\'alerte (BRUTE_FORCE, SQL_ERROR, etc.)',
|
||||
`alert_level` ENUM('INFO', 'WARNING', 'ERROR', 'CRITICAL', 'SECURITY') NOT NULL DEFAULT 'INFO',
|
||||
`ip_address` VARCHAR(45) DEFAULT NULL COMMENT 'Adresse IP source',
|
||||
`user_id` INT(11) UNSIGNED DEFAULT NULL COMMENT 'ID utilisateur si connecté',
|
||||
`username` VARCHAR(255) DEFAULT NULL COMMENT 'Username tenté ou utilisé',
|
||||
`endpoint` VARCHAR(255) DEFAULT NULL COMMENT 'Endpoint API concerné',
|
||||
`method` VARCHAR(10) DEFAULT NULL COMMENT 'Méthode HTTP',
|
||||
`details` JSON DEFAULT NULL COMMENT 'Détails additionnels en JSON',
|
||||
`occurrences` INT(11) DEFAULT 1 COMMENT 'Nombre d\'occurrences',
|
||||
`first_seen` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
`last_seen` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
`email_sent` TINYINT(1) DEFAULT 0 COMMENT 'Email d\'alerte envoyé',
|
||||
`email_sent_at` TIMESTAMP NULL DEFAULT NULL,
|
||||
`resolved` TINYINT(1) DEFAULT 0 COMMENT 'Alerte résolue',
|
||||
`resolved_at` TIMESTAMP NULL DEFAULT NULL,
|
||||
`resolved_by` INT(11) UNSIGNED DEFAULT NULL COMMENT 'ID admin qui a résolu',
|
||||
`notes` TEXT DEFAULT NULL COMMENT 'Notes de résolution',
|
||||
KEY `idx_ip` (`ip_address`),
|
||||
KEY `idx_type_time` (`alert_type`, `last_seen`),
|
||||
KEY `idx_level` (`alert_level`),
|
||||
KEY `idx_resolved` (`resolved`),
|
||||
KEY `idx_user` (`user_id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='Alertes de sécurité et monitoring'
|
||||
");
|
||||
echo " ✓ Table sec_alerts créée\n";
|
||||
|
||||
// 2. Table des métriques de performance (SANS PARTITIONNEMENT)
|
||||
echo "2. Création de la table sec_performance_metrics...\n";
|
||||
$db->exec("
|
||||
CREATE TABLE IF NOT EXISTS `sec_performance_metrics` (
|
||||
`id` BIGINT(20) UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
||||
`endpoint` VARCHAR(255) NOT NULL COMMENT 'Endpoint API',
|
||||
`method` VARCHAR(10) NOT NULL COMMENT 'Méthode HTTP',
|
||||
`response_time_ms` INT(11) NOT NULL COMMENT 'Temps de réponse total en ms',
|
||||
`db_time_ms` INT(11) DEFAULT 0 COMMENT 'Temps cumulé des requêtes DB en ms',
|
||||
`db_queries_count` INT(11) DEFAULT 0 COMMENT 'Nombre de requêtes DB',
|
||||
`memory_peak_mb` FLOAT DEFAULT NULL COMMENT 'Pic mémoire en MB',
|
||||
`memory_start_mb` FLOAT DEFAULT NULL COMMENT 'Mémoire au début en MB',
|
||||
`http_status` INT(11) DEFAULT NULL COMMENT 'Code HTTP de réponse',
|
||||
`user_id` INT(11) UNSIGNED DEFAULT NULL COMMENT 'ID utilisateur si connecté',
|
||||
`ip_address` VARCHAR(45) DEFAULT NULL COMMENT 'Adresse IP',
|
||||
`user_agent` TEXT DEFAULT NULL COMMENT 'User Agent complet',
|
||||
`request_size` INT(11) DEFAULT NULL COMMENT 'Taille de la requête en octets',
|
||||
`response_size` INT(11) DEFAULT NULL COMMENT 'Taille de la réponse en octets',
|
||||
`created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
KEY `idx_endpoint_time` (`endpoint`, `created_at`),
|
||||
KEY `idx_response_time` (`response_time_ms`),
|
||||
KEY `idx_created` (`created_at`),
|
||||
KEY `idx_status` (`http_status`),
|
||||
KEY `idx_user` (`user_id`),
|
||||
KEY `idx_date_endpoint` (`created_at`, `endpoint`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='Métriques de performance des requêtes'
|
||||
");
|
||||
echo " ✓ Table sec_performance_metrics créée\n";
|
||||
|
||||
// 3. Table des tentatives de login échouées
|
||||
echo "3. Création de la table sec_failed_login_attempts...\n";
|
||||
$db->exec("
|
||||
CREATE TABLE IF NOT EXISTS `sec_failed_login_attempts` (
|
||||
`id` INT(11) UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
||||
`username` VARCHAR(255) DEFAULT NULL COMMENT 'Username tenté',
|
||||
`encrypted_username` VARCHAR(255) DEFAULT NULL COMMENT 'Username chiffré si trouvé',
|
||||
`ip_address` VARCHAR(45) NOT NULL COMMENT 'Adresse IP',
|
||||
`user_agent` TEXT DEFAULT NULL COMMENT 'User Agent',
|
||||
`attempt_time` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
`error_type` VARCHAR(50) DEFAULT NULL COMMENT 'Type d\'erreur (invalid_password, user_not_found, etc.)',
|
||||
`country_code` VARCHAR(2) DEFAULT NULL COMMENT 'Code pays de l\'IP (si géoloc activée)',
|
||||
KEY `idx_ip_time` (`ip_address`, `attempt_time`),
|
||||
KEY `idx_username` (`username`),
|
||||
KEY `idx_encrypted_username` (`encrypted_username`),
|
||||
KEY `idx_time` (`attempt_time`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='Tentatives de connexion échouées'
|
||||
");
|
||||
echo " ✓ Table sec_failed_login_attempts créée\n";
|
||||
|
||||
// 4. Table des IPs bloquées
|
||||
echo "4. Création de la table sec_blocked_ips...\n";
|
||||
$db->exec("
|
||||
CREATE TABLE IF NOT EXISTS `sec_blocked_ips` (
|
||||
`ip_address` VARCHAR(45) NOT NULL PRIMARY KEY COMMENT 'Adresse IP bloquée',
|
||||
`reason` VARCHAR(255) NOT NULL COMMENT 'Raison du blocage',
|
||||
`details` JSON DEFAULT NULL COMMENT 'Détails additionnels',
|
||||
`blocked_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
`blocked_until` TIMESTAMP NOT NULL COMMENT 'Bloqué jusqu\'à',
|
||||
`blocked_by` VARCHAR(50) DEFAULT 'system' COMMENT 'Qui a bloqué (system ou user ID)',
|
||||
`permanent` TINYINT(1) DEFAULT 0 COMMENT 'Blocage permanent',
|
||||
`unblocked_at` TIMESTAMP NULL DEFAULT NULL COMMENT 'Date de déblocage effectif',
|
||||
`unblocked_by` INT(11) UNSIGNED DEFAULT NULL COMMENT 'Qui a débloqué',
|
||||
`block_count` INT(11) DEFAULT 1 COMMENT 'Nombre de fois bloquée',
|
||||
KEY `idx_blocked_until` (`blocked_until`),
|
||||
KEY `idx_permanent` (`permanent`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='IPs bloquées temporairement ou définitivement'
|
||||
");
|
||||
echo " ✓ Table sec_blocked_ips créée\n";
|
||||
|
||||
// 5. Créer les vues
|
||||
echo "5. Création des vues...\n";
|
||||
|
||||
// Vue pour les alertes actives
|
||||
$db->exec("
|
||||
CREATE OR REPLACE VIEW sec_active_alerts AS
|
||||
SELECT
|
||||
a.*,
|
||||
u.encrypted_name as user_name,
|
||||
r.encrypted_name as resolver_name
|
||||
FROM sec_alerts a
|
||||
LEFT JOIN users u ON a.user_id = u.id
|
||||
LEFT JOIN users r ON a.resolved_by = r.id
|
||||
WHERE a.resolved = 0
|
||||
OR (a.resolved = 1 AND a.resolved_at >= DATE_SUB(NOW(), INTERVAL 24 HOUR))
|
||||
ORDER BY
|
||||
CASE a.alert_level
|
||||
WHEN 'SECURITY' THEN 1
|
||||
WHEN 'CRITICAL' THEN 2
|
||||
WHEN 'ERROR' THEN 3
|
||||
WHEN 'WARNING' THEN 4
|
||||
WHEN 'INFO' THEN 5
|
||||
END,
|
||||
a.last_seen DESC
|
||||
");
|
||||
echo " ✓ Vue sec_active_alerts créée\n";
|
||||
|
||||
// Vue pour les IPs suspectes
|
||||
$db->exec("
|
||||
CREATE OR REPLACE VIEW sec_suspicious_ips AS
|
||||
SELECT
|
||||
ip_address,
|
||||
COUNT(*) as total_attempts,
|
||||
COUNT(DISTINCT username) as unique_usernames,
|
||||
MIN(attempt_time) as first_attempt,
|
||||
MAX(attempt_time) as last_attempt,
|
||||
TIMESTAMPDIFF(MINUTE, MIN(attempt_time), MAX(attempt_time)) as timespan_minutes
|
||||
FROM sec_failed_login_attempts
|
||||
WHERE attempt_time >= DATE_SUB(NOW(), INTERVAL 1 HOUR)
|
||||
GROUP BY ip_address
|
||||
HAVING total_attempts >= 5
|
||||
OR unique_usernames >= 3
|
||||
ORDER BY total_attempts DESC
|
||||
");
|
||||
echo " ✓ Vue sec_suspicious_ips créée\n";
|
||||
|
||||
// 6. Créer les index additionnels
|
||||
echo "6. Création des index additionnels...\n";
|
||||
|
||||
// Index pour les requêtes fréquentes
|
||||
$db->exec("CREATE INDEX IF NOT EXISTS idx_sec_metrics_recent ON sec_performance_metrics(created_at DESC, endpoint)");
|
||||
$db->exec("CREATE INDEX IF NOT EXISTS idx_sec_alerts_recent ON sec_alerts(last_seen DESC, alert_level)");
|
||||
$db->exec("CREATE INDEX IF NOT EXISTS idx_sec_failed_recent ON sec_failed_login_attempts(attempt_time DESC, ip_address)");
|
||||
echo " ✓ Index créés\n";
|
||||
|
||||
// 7. Créer la procédure de nettoyage
|
||||
echo "7. Création de la procédure de nettoyage...\n";
|
||||
$db->exec("DROP PROCEDURE IF EXISTS sec_cleanup_old_data");
|
||||
$db->exec("
|
||||
CREATE PROCEDURE sec_cleanup_old_data(IN days_to_keep INT)
|
||||
BEGIN
|
||||
-- Nettoyer les métriques de performance
|
||||
DELETE FROM sec_performance_metrics
|
||||
WHERE created_at < DATE_SUB(NOW(), INTERVAL days_to_keep DAY);
|
||||
|
||||
-- Nettoyer les tentatives de login
|
||||
DELETE FROM sec_failed_login_attempts
|
||||
WHERE attempt_time < DATE_SUB(NOW(), INTERVAL days_to_keep DAY);
|
||||
|
||||
-- Nettoyer les alertes résolues
|
||||
DELETE FROM sec_alerts
|
||||
WHERE resolved = 1
|
||||
AND resolved_at < DATE_SUB(NOW(), INTERVAL days_to_keep DAY);
|
||||
|
||||
-- Retourner le nombre de lignes supprimées
|
||||
SELECT ROW_COUNT() as deleted_rows;
|
||||
END
|
||||
");
|
||||
echo " ✓ Procédure sec_cleanup_old_data créée\n";
|
||||
|
||||
// Réactiver les clés étrangères
|
||||
$db->exec("SET FOREIGN_KEY_CHECKS = 1");
|
||||
|
||||
// 8. Vérifier que tout est créé
|
||||
echo "\n8. Vérification finale...\n";
|
||||
$tables = ['sec_alerts', 'sec_performance_metrics', 'sec_failed_login_attempts', 'sec_blocked_ips'];
|
||||
$allOk = true;
|
||||
|
||||
foreach ($tables as $table) {
|
||||
$stmt = $db->query("SELECT COUNT(*) as count FROM $table");
|
||||
if ($stmt) {
|
||||
$result = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
echo " ✓ Table $table : OK ({$result['count']} enregistrements)\n";
|
||||
} else {
|
||||
echo " ✗ Table $table : ERREUR\n";
|
||||
$allOk = false;
|
||||
}
|
||||
}
|
||||
|
||||
if ($allOk) {
|
||||
echo "\n========================================\n";
|
||||
echo "✅ TOUTES LES TABLES ONT ÉTÉ CRÉÉES AVEC SUCCÈS\n";
|
||||
echo "========================================\n\n";
|
||||
echo "Le système de sécurité est maintenant prêt à être utilisé.\n";
|
||||
echo "Vous pouvez tester avec : php test_security.php\n\n";
|
||||
} else {
|
||||
echo "\n⚠️ Certaines tables n'ont pas pu être créées.\n";
|
||||
echo "Vérifiez les erreurs ci-dessus.\n\n";
|
||||
}
|
||||
|
||||
} catch (PDOException $e) {
|
||||
echo "\n❌ ERREUR SQL : " . $e->getMessage() . "\n\n";
|
||||
echo "Code d'erreur : " . $e->getCode() . "\n";
|
||||
echo "Vérifiez les permissions et la configuration de la base de données.\n\n";
|
||||
exit(1);
|
||||
} catch (Exception $e) {
|
||||
echo "\n❌ ERREUR : " . $e->getMessage() . "\n\n";
|
||||
exit(1);
|
||||
}
|
||||
0
api/scripts/php/migrate.php
Normal file → Executable file
0
api/scripts/php/migrate.php
Normal file → Executable file
0
api/scripts/php/migrate_entites.php
Normal file → Executable file
0
api/scripts/php/migrate_entites.php
Normal file → Executable file
0
api/scripts/php/migrate_medias.php
Normal file → Executable file
0
api/scripts/php/migrate_medias.php
Normal file → Executable file
0
api/scripts/php/migrate_ope_pass.php
Normal file → Executable file
0
api/scripts/php/migrate_ope_pass.php
Normal file → Executable file
0
api/scripts/php/migrate_ope_pass_histo.php
Normal file → Executable file
0
api/scripts/php/migrate_ope_pass_histo.php
Normal file → Executable file
0
api/scripts/php/migrate_ope_sectors.php
Normal file → Executable file
0
api/scripts/php/migrate_ope_sectors.php
Normal file → Executable file
0
api/scripts/php/migrate_ope_users.php
Normal file → Executable file
0
api/scripts/php/migrate_ope_users.php
Normal file → Executable file
0
api/scripts/php/migrate_ope_users_sectors.php
Normal file → Executable file
0
api/scripts/php/migrate_ope_users_sectors.php
Normal file → Executable file
0
api/scripts/php/migrate_operations.php
Normal file → Executable file
0
api/scripts/php/migrate_operations.php
Normal file → Executable file
0
api/scripts/php/migrate_sectors_adresses.php
Normal file → Executable file
0
api/scripts/php/migrate_sectors_adresses.php
Normal file → Executable file
0
api/scripts/php/migrate_users.php
Normal file → Executable file
0
api/scripts/php/migrate_users.php
Normal file → Executable file
0
api/scripts/php/migrate_x_departements.php
Normal file → Executable file
0
api/scripts/php/migrate_x_departements.php
Normal file → Executable file
0
api/scripts/php/migrate_x_devises.php
Normal file → Executable file
0
api/scripts/php/migrate_x_devises.php
Normal file → Executable file
0
api/scripts/php/migrate_x_entites_types.php
Normal file → Executable file
0
api/scripts/php/migrate_x_entites_types.php
Normal file → Executable file
0
api/scripts/php/migrate_x_pays.php
Normal file → Executable file
0
api/scripts/php/migrate_x_pays.php
Normal file → Executable file
0
api/scripts/php/migrate_x_regions.php
Normal file → Executable file
0
api/scripts/php/migrate_x_regions.php
Normal file → Executable file
0
api/scripts/php/migrate_x_types_passages.php
Normal file → Executable file
0
api/scripts/php/migrate_x_types_passages.php
Normal file → Executable file
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user