Compare commits
36 Commits
feature/ch
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7b78037175 | ||
|
|
ae6d7b1d70 | ||
|
|
2f5946a184 | ||
|
|
21657a3820 | ||
|
|
570a1fa1f0 | ||
| 2786252307 | |||
| 2187dccfeb | |||
| f7baa7492c | |||
| 4153f73ace | |||
| 0bebc58d1a | |||
| d334ecbf7e | |||
| 76fe9064eb | |||
| b2575fcdbf | |||
| 0816d215f7 | |||
| 0b541fbe4a | |||
| ece2f0006c | |||
| 96af94ad13 | |||
| f5bef999df | |||
| d275d0ab0c | |||
| 6d41a1274f | |||
| cdb676ea71 | |||
| 3443277d4a | |||
| 4f7247eb2d | |||
| 268444d6e8 | |||
| 0e98a94374 | |||
|
|
1018b86537 | ||
|
|
3bbc599ab4 | ||
|
|
416d648a14 | ||
|
|
25c9d5874c | ||
|
|
ace38d4025 | ||
|
|
511be5a535 | ||
|
|
150016d772 | ||
|
|
7e6431b5aa | ||
|
|
95e9af23e2 | ||
|
|
ef83b258d9 | ||
|
|
bcfdbb2c8b |
6
.gitignore
vendored
6
.gitignore
vendored
@@ -20,6 +20,12 @@ web/storage/*.key
|
||||
web/public/storage
|
||||
web/public/hot
|
||||
|
||||
# API
|
||||
api/sessions/
|
||||
api/logs/
|
||||
api/uploads/
|
||||
api/vendor/
|
||||
|
||||
# Général
|
||||
*.DS_Store
|
||||
*.log
|
||||
|
||||
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 |
608
MONITORING.md
Normal file
608
MONITORING.md
Normal file
@@ -0,0 +1,608 @@
|
||||
# Guide de Monitoring - Container Incus (Application Web PHP + MariaDB)
|
||||
|
||||
## Vue d'ensemble
|
||||
|
||||
Ce guide décrit les métriques essentielles à surveiller pour un container Incus hébergeant une application web PHP avec API mobile et base de données MariaDB.
|
||||
|
||||
---
|
||||
|
||||
## 1. Ressources Système
|
||||
|
||||
### CPU
|
||||
**Pourquoi ?** Identifier les pics de charge et les processus gourmands
|
||||
|
||||
```bash
|
||||
# Utilisation CPU globale du container
|
||||
incus info nx4 | grep "CPU utilisé"
|
||||
|
||||
# Détail par processus (dans le container)
|
||||
top -bn1 | head -20
|
||||
htop
|
||||
```
|
||||
|
||||
**Métriques à surveiller :**
|
||||
- Load average (devrait être < nombre de CPU)
|
||||
- % CPU par processus (MariaDB, PHP-FPM, nginx)
|
||||
|
||||
**Seuils d'alerte :**
|
||||
- ⚠️ Warning : Load average > 70% des CPU
|
||||
- 🚨 Critical : Load average > 150% des CPU pendant >5min
|
||||
|
||||
---
|
||||
|
||||
### Mémoire RAM
|
||||
**Pourquoi ?** Éviter les OOM (Out of Memory) qui tuent les processus
|
||||
|
||||
```bash
|
||||
# Vue globale depuis le host
|
||||
incus info nx4 | grep "Mémoire"
|
||||
|
||||
# Détail dans le container
|
||||
free -h
|
||||
ps aux --sort=-%mem | head -10
|
||||
```
|
||||
|
||||
**Métriques à surveiller :**
|
||||
- RAM utilisée / totale
|
||||
- Swap utilisé (devrait rester minimal)
|
||||
- Top processus consommateurs
|
||||
|
||||
**Seuils d'alerte :**
|
||||
- ⚠️ Warning : RAM > 85%
|
||||
- 🚨 Critical : RAM > 95% ou Swap > 1GB
|
||||
|
||||
---
|
||||
|
||||
### Disque I/O
|
||||
**Pourquoi ?** MariaDB est très sensible aux lenteurs disque
|
||||
|
||||
```bash
|
||||
# Sur le host
|
||||
iostat -x 2 5
|
||||
|
||||
# Dans le container
|
||||
iostat -x 1 3
|
||||
iotop -oa
|
||||
```
|
||||
|
||||
**Métriques à surveiller :**
|
||||
- Latence disque (await)
|
||||
- IOPS (r/s, w/s)
|
||||
- % utilisation disque
|
||||
|
||||
**Seuils d'alerte :**
|
||||
- ⚠️ Warning : await > 50ms
|
||||
- 🚨 Critical : await > 200ms ou %util > 90%
|
||||
|
||||
---
|
||||
|
||||
### Espace Disque
|
||||
**Pourquoi ?** MariaDB ne peut plus écrire si disque plein
|
||||
|
||||
```bash
|
||||
df -h
|
||||
du -sh /var/lib/mysql
|
||||
```
|
||||
|
||||
**Seuils d'alerte :**
|
||||
- ⚠️ Warning : > 80% utilisé
|
||||
- 🚨 Critical : > 90% utilisé
|
||||
|
||||
---
|
||||
|
||||
## 2. PHP-FPM (Application Web)
|
||||
|
||||
### Pool de Workers
|
||||
**Pourquoi ?** Cause #1 des timeouts et coupures de service (votre cas !)
|
||||
|
||||
```bash
|
||||
# Nombre de workers actifs
|
||||
ps aux | grep "php-fpm: pool www" | wc -l
|
||||
|
||||
# Config du pool
|
||||
grep "^pm" /etc/php/8.3/fpm/pool.d/www.conf
|
||||
|
||||
# Logs d'alertes
|
||||
tail -f /var/log/php8.3-fpm.log | grep "max_children"
|
||||
```
|
||||
|
||||
**Métriques critiques :**
|
||||
- Nombre de workers actifs vs `pm.max_children`
|
||||
- Warnings "max_children reached"
|
||||
- Slow requests (>2s)
|
||||
|
||||
**Seuils d'alerte :**
|
||||
- ⚠️ Warning : Workers actifs > 80% de max_children
|
||||
- 🚨 Critical : Warning "max_children reached" apparaît
|
||||
|
||||
**Configuration recommandée pour votre cas :**
|
||||
```ini
|
||||
pm = dynamic
|
||||
pm.max_children = 50-100 (selon RAM disponible)
|
||||
pm.start_servers = 10-20
|
||||
pm.min_spare_servers = 5-10
|
||||
pm.max_spare_servers = 20-35
|
||||
pm.max_requests = 500
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Temps de Réponse PHP
|
||||
**Pourquoi ?** Scripts lents = workers bloqués
|
||||
|
||||
```bash
|
||||
# Activer le slow log PHP-FPM
|
||||
# Dans /etc/php/8.3/fpm/pool.d/www.conf
|
||||
slowlog = /var/log/php8.3-fpm-slow.log
|
||||
request_slowlog_timeout = 3s
|
||||
|
||||
# Voir les requêtes lentes
|
||||
tail -f /var/log/php8.3-fpm-slow.log
|
||||
```
|
||||
|
||||
**Seuils d'alerte :**
|
||||
- ⚠️ Warning : Requêtes > 3s
|
||||
- 🚨 Critical : Requêtes > 10s ou timeouts fréquents
|
||||
|
||||
---
|
||||
|
||||
## 3. MariaDB / MySQL
|
||||
|
||||
### Connexions
|
||||
**Pourquoi ?** Trop de connexions = refus de nouvelles connexions
|
||||
|
||||
```bash
|
||||
mysql -e "SHOW STATUS LIKE 'Threads_connected';"
|
||||
mysql -e "SHOW STATUS LIKE 'Max_used_connections';"
|
||||
mysql -e "SHOW VARIABLES LIKE 'max_connections';"
|
||||
mysql -e "SHOW FULL PROCESSLIST;"
|
||||
```
|
||||
|
||||
**Métriques critiques :**
|
||||
- Connexions actives vs max_connections
|
||||
- Connexions en attente / bloquées
|
||||
- Requêtes longues (>5s)
|
||||
|
||||
**Seuils d'alerte :**
|
||||
- ⚠️ Warning : Connexions > 80% de max_connections
|
||||
- 🚨 Critical : Connexions = max_connections
|
||||
|
||||
**Config recommandée :**
|
||||
```ini
|
||||
max_connections = 200-500 (selon votre trafic)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Slow Queries
|
||||
**Pourquoi ?** Requêtes lentes = workers PHP bloqués
|
||||
|
||||
```bash
|
||||
# Activer le slow query log
|
||||
mysql -e "SET GLOBAL slow_query_log = 'ON';"
|
||||
mysql -e "SET GLOBAL long_query_time = 2;"
|
||||
mysql -e "SET GLOBAL log_queries_not_using_indexes = 'ON';"
|
||||
|
||||
# Voir les slow queries
|
||||
tail -f /var/lib/mysql/slow-query.log
|
||||
# ou
|
||||
mysqldumpslow /var/lib/mysql/slow-query.log
|
||||
```
|
||||
|
||||
**Seuils d'alerte :**
|
||||
- ⚠️ Warning : Requêtes > 2s
|
||||
- 🚨 Critical : Requêtes > 10s ou >50 slow queries/min
|
||||
|
||||
---
|
||||
|
||||
### InnoDB Buffer Pool
|
||||
**Pourquoi ?** Si trop petit, beaucoup de lectures disque (lent)
|
||||
|
||||
```bash
|
||||
mysql -e "SHOW VARIABLES LIKE 'innodb_buffer_pool_size';"
|
||||
mysql -e "SHOW STATUS LIKE 'Innodb_buffer_pool_read%';"
|
||||
```
|
||||
|
||||
**Métriques critiques :**
|
||||
- Buffer pool hit ratio (devrait être >99%)
|
||||
- Read requests vs reads from disk
|
||||
|
||||
**Config recommandée :**
|
||||
- `innodb_buffer_pool_size` = 70-80% de la RAM dédiée à MySQL
|
||||
- Pour 20GB de données : `innodb_buffer_pool_size = 16G`
|
||||
|
||||
---
|
||||
|
||||
### Deadlocks et Locks
|
||||
**Pourquoi ?** Peuvent causer des timeouts
|
||||
|
||||
```bash
|
||||
mysql -e "SHOW ENGINE INNODB STATUS\G" | grep -A 50 "LATEST DETECTED DEADLOCK"
|
||||
mysql -e "SHOW OPEN TABLES WHERE In_use > 0;"
|
||||
```
|
||||
|
||||
**Seuils d'alerte :**
|
||||
- ⚠️ Warning : >1 deadlock/heure
|
||||
- 🚨 Critical : Tables lockées >30s
|
||||
|
||||
---
|
||||
|
||||
## 4. Nginx / Serveur Web
|
||||
|
||||
### Connexions et Erreurs
|
||||
**Pourquoi ?** Identifier les 502/504 (backend timeout)
|
||||
|
||||
```bash
|
||||
# Connexions actives
|
||||
netstat -an | grep :80 | wc -l
|
||||
netstat -an | grep :443 | wc -l
|
||||
|
||||
# Erreurs récentes
|
||||
tail -100 /var/log/nginx/error.log | grep -E "502|504|timeout"
|
||||
tail -100 /var/log/nginx/access.log | grep -E " 502 | 504 "
|
||||
```
|
||||
|
||||
**Métriques critiques :**
|
||||
- Erreurs 502 Bad Gateway (PHP-FPM down)
|
||||
- Erreurs 504 Gateway Timeout (PHP-FPM trop lent)
|
||||
- Connexions actives
|
||||
|
||||
**Seuils d'alerte :**
|
||||
- ⚠️ Warning : >5 erreurs 502/504 en 5min
|
||||
- 🚨 Critical : >20 erreurs 502/504 en 5min
|
||||
|
||||
---
|
||||
|
||||
### Worker Connections
|
||||
**Pourquoi ?** Limite le nombre de connexions simultanées
|
||||
|
||||
```bash
|
||||
# Config nginx
|
||||
grep worker_connections /etc/nginx/nginx.conf
|
||||
ps aux | grep nginx | wc -l
|
||||
```
|
||||
|
||||
**Config recommandée :**
|
||||
```nginx
|
||||
worker_processes auto;
|
||||
worker_connections 2048;
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. Réseau
|
||||
|
||||
### Bande Passante
|
||||
**Pourquoi ?** Identifier les pics de trafic
|
||||
|
||||
```bash
|
||||
# Depuis le host
|
||||
incus info nx4 | grep -A 10 "eth0:"
|
||||
|
||||
# Dans le container
|
||||
iftop -i eth0
|
||||
vnstat -i eth0
|
||||
```
|
||||
|
||||
**Métriques :**
|
||||
- Octets reçus/émis
|
||||
- Paquets reçus/émis
|
||||
- Erreurs réseau
|
||||
|
||||
---
|
||||
|
||||
### Connexions TCP
|
||||
**Pourquoi ?** Trop de connexions TIME_WAIT = problème
|
||||
|
||||
```bash
|
||||
netstat -an | grep -E "ESTABLISHED|TIME_WAIT|CLOSE_WAIT" | wc -l
|
||||
ss -s
|
||||
```
|
||||
|
||||
**Seuils d'alerte :**
|
||||
- ⚠️ Warning : >1000 connexions TIME_WAIT
|
||||
- 🚨 Critical : >5000 connexions TIME_WAIT
|
||||
|
||||
---
|
||||
|
||||
## 6. Logs et Événements
|
||||
|
||||
### Logs Système
|
||||
```bash
|
||||
# Container Debian
|
||||
journalctl -u nginx -n 100
|
||||
journalctl -u php8.3-fpm -n 100
|
||||
journalctl -u mariadb -n 100
|
||||
|
||||
# Container Alpine (si pas de systemd)
|
||||
tail -100 /var/log/messages
|
||||
```
|
||||
|
||||
### Logs Applicatifs
|
||||
```bash
|
||||
# PHP errors
|
||||
tail -100 /var/log/php8.3-fpm.log
|
||||
tail -100 /var/www/html/logs/*.log
|
||||
|
||||
# Nginx
|
||||
tail -100 /var/log/nginx/error.log
|
||||
tail -100 /var/log/nginx/access.log
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 7. Scripts de Monitoring Automatisés
|
||||
|
||||
### Script de Monitoring Global
|
||||
|
||||
Créer `/root/monitor.sh` :
|
||||
|
||||
```bash
|
||||
#!/bin/bash
|
||||
|
||||
LOG_FILE="/var/log/system-monitor.log"
|
||||
ALERT_FILE="/var/log/alerts.log"
|
||||
|
||||
echo "=== Monitoring $(date) ===" >> $LOG_FILE
|
||||
|
||||
# 1. CPU
|
||||
LOAD=$(uptime | awk -F'load average:' '{print $2}' | awk '{print $1}')
|
||||
echo "Load: $LOAD" >> $LOG_FILE
|
||||
|
||||
# 2. RAM
|
||||
RAM_PERCENT=$(free | awk '/Mem:/ {printf("%.1f"), ($3/$2)*100}')
|
||||
echo "RAM: ${RAM_PERCENT}%" >> $LOG_FILE
|
||||
if (( $(echo "$RAM_PERCENT > 85" | bc -l) )); then
|
||||
echo "[WARNING] RAM > 85%: ${RAM_PERCENT}%" >> $ALERT_FILE
|
||||
fi
|
||||
|
||||
# 3. PHP-FPM Workers
|
||||
PHP_WORKERS=$(ps aux | grep "php-fpm: pool www" | wc -l)
|
||||
PHP_MAX=$(grep "^pm.max_children" /etc/php/8.3/fpm/pool.d/www.conf | awk '{print $3}')
|
||||
echo "PHP Workers: $PHP_WORKERS / $PHP_MAX" >> $LOG_FILE
|
||||
if [ $PHP_WORKERS -gt $(echo "$PHP_MAX * 0.8" | bc) ]; then
|
||||
echo "[WARNING] PHP Workers > 80%: $PHP_WORKERS / $PHP_MAX" >> $ALERT_FILE
|
||||
fi
|
||||
|
||||
# 4. MySQL Connexions
|
||||
MYSQL_CONN=$(mysql -e "SHOW STATUS LIKE 'Threads_connected';" | awk 'NR==2 {print $2}')
|
||||
MYSQL_MAX=$(mysql -e "SHOW VARIABLES LIKE 'max_connections';" | awk 'NR==2 {print $2}')
|
||||
echo "MySQL Connections: $MYSQL_CONN / $MYSQL_MAX" >> $LOG_FILE
|
||||
if [ $MYSQL_CONN -gt $(echo "$MYSQL_MAX * 0.8" | bc) ]; then
|
||||
echo "[WARNING] MySQL Connections > 80%: $MYSQL_CONN / $MYSQL_MAX" >> $ALERT_FILE
|
||||
fi
|
||||
|
||||
# 5. Disque
|
||||
DISK_PERCENT=$(df -h / | awk 'NR==2 {print $5}' | sed 's/%//')
|
||||
echo "Disk: ${DISK_PERCENT}%" >> $LOG_FILE
|
||||
if [ $DISK_PERCENT -gt 80 ]; then
|
||||
echo "[WARNING] Disk > 80%: ${DISK_PERCENT}%" >> $ALERT_FILE
|
||||
fi
|
||||
|
||||
# 6. Erreurs nginx
|
||||
NGINX_ERRORS=$(grep -c "error" /var/log/nginx/error.log | tail -100)
|
||||
if [ $NGINX_ERRORS -gt 10 ]; then
|
||||
echo "[WARNING] Nginx errors: $NGINX_ERRORS in last 100 lines" >> $ALERT_FILE
|
||||
fi
|
||||
|
||||
echo "" >> $LOG_FILE
|
||||
```
|
||||
|
||||
**Installation :**
|
||||
```bash
|
||||
chmod +x /root/monitor.sh
|
||||
|
||||
# Exécuter toutes les 5 minutes
|
||||
(crontab -l 2>/dev/null; echo "*/5 * * * * /root/monitor.sh") | crontab -
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Script de Monitoring PHP-FPM Spécifique
|
||||
|
||||
Créer `/root/monitor-php-fpm.sh` :
|
||||
|
||||
```bash
|
||||
#!/bin/bash
|
||||
|
||||
LOG="/var/log/php-fpm-monitor.log"
|
||||
TIMESTAMP=$(date '+%Y-%m-%d %H:%M:%S')
|
||||
|
||||
# Compter les workers (sans grep lui-même)
|
||||
WORKERS=$(ps aux | grep 'php-fpm: pool www' | grep -v grep | wc -l)
|
||||
MAX_CHILDREN=$(grep '^pm.max_children' /etc/php/8.3/fpm/pool.d/www.conf | awk '{print $3}')
|
||||
PERCENT=$(echo "scale=1; ($WORKERS / $MAX_CHILDREN) * 100" | bc)
|
||||
|
||||
# Log au format: timestamp,workers,max,percentage
|
||||
echo "$TIMESTAMP,$WORKERS,$MAX_CHILDREN,$PERCENT%" >> $LOG
|
||||
|
||||
# Alerte si >80%
|
||||
if (( $(echo "$PERCENT > 80" | bc -l) )); then
|
||||
echo "[$TIMESTAMP] [ALERT] PHP Workers > 80%: $WORKERS / $MAX_CHILDREN ($PERCENT%)" >> /var/log/alerts.log
|
||||
fi
|
||||
|
||||
# Vérifier si max_children atteint dans les logs récents
|
||||
if tail -10 /var/log/php8.3-fpm.log | grep -q "max_children"; then
|
||||
echo "[$TIMESTAMP] [CRITICAL] MAX_CHILDREN REACHED!" >> /var/log/alerts.log
|
||||
tail -3 /var/log/php8.3-fpm.log >> /var/log/alerts.log
|
||||
fi
|
||||
```
|
||||
|
||||
**Installation :**
|
||||
```bash
|
||||
chmod +x /root/monitor-php-fpm.sh
|
||||
|
||||
# Exécuter toutes les minutes
|
||||
(crontab -l 2>/dev/null; echo "* * * * * /root/monitor-php-fpm.sh") | crontab -
|
||||
```
|
||||
|
||||
**Visualiser les données :**
|
||||
```bash
|
||||
# Afficher les dernières 60 minutes
|
||||
tail -60 /var/log/php-fpm-monitor.log
|
||||
|
||||
# Voir l'évolution graphique (nécessite gnuplot)
|
||||
echo 'set datafile separator ","; plot "/var/log/php-fpm-monitor.log" using 2 with lines title "Workers"' | gnuplot -p
|
||||
|
||||
# Statistiques rapides
|
||||
echo "Max workers last hour: $(tail -60 /var/log/php-fpm-monitor.log | cut -d',' -f2 | sort -n | tail -1)"
|
||||
echo "Min workers last hour: $(tail -60 /var/log/php-fpm-monitor.log | cut -d',' -f2 | sort -n | head -1)"
|
||||
echo "Avg workers last hour: $(tail -60 /var/log/php-fpm-monitor.log | cut -d',' -f2 | awk '{sum+=$1} END {print sum/NR}')"
|
||||
|
||||
# Alertes récentes
|
||||
tail -20 /var/log/alerts.log | grep "PHP Workers"
|
||||
```
|
||||
|
||||
**Rotation automatique des logs :**
|
||||
Créer `/etc/logrotate.d/php-fpm-monitor` :
|
||||
```
|
||||
/var/log/php-fpm-monitor.log {
|
||||
daily
|
||||
rotate 30
|
||||
compress
|
||||
missingok
|
||||
notifempty
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 8. Solutions de Monitoring Automatisées
|
||||
|
||||
### Option 1 : Netdata (Recommandé)
|
||||
**Avantages :** Installation simple, interface web, détection auto des services
|
||||
|
||||
```bash
|
||||
# Installation
|
||||
bash <(curl -Ss https://get.netdata.cloud/kickstart.sh) --dont-wait
|
||||
|
||||
# Accessible sur http://IP:19999
|
||||
# Détecte automatiquement : PHP-FPM, MariaDB, Nginx, ressources système
|
||||
```
|
||||
|
||||
**Métriques auto-détectées :**
|
||||
- ✅ CPU, RAM, I/O, réseau
|
||||
- ✅ PHP-FPM (workers, slow requests)
|
||||
- ✅ MariaDB (connexions, queries, locks)
|
||||
- ✅ Nginx (connexions, erreurs)
|
||||
|
||||
---
|
||||
|
||||
### Option 2 : Prometheus + Grafana
|
||||
**Avantages :** Professionnel, historique long terme, alerting avancé
|
||||
|
||||
```bash
|
||||
# Exposer les métriques Incus
|
||||
incus config set core.metrics_address :8443
|
||||
|
||||
# Installer Prometheus + Grafana
|
||||
# (plus complexe, voir doc officielle)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Option 3 : Scripts + Monitoring Simple
|
||||
|
||||
Si vous préférez rester léger, surveillez manuellement avec :
|
||||
|
||||
```bash
|
||||
# Dashboard temps réel
|
||||
watch -n 2 'echo "=== RESSOURCES ===";
|
||||
free -h | head -2;
|
||||
echo "";
|
||||
echo "=== PHP-FPM ===";
|
||||
ps aux | grep "php-fpm: pool" | wc -l;
|
||||
echo "";
|
||||
echo "=== MySQL ===";
|
||||
mysql -e "SHOW STATUS LIKE \"Threads_connected\";" 2>/dev/null;
|
||||
echo "";
|
||||
echo "=== NGINX ===";
|
||||
netstat -an | grep :80 | wc -l'
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 9. Checklist de Diagnostic en Cas de Problème
|
||||
|
||||
### Coupures / Timeouts
|
||||
1. ✅ **Vérifier PHP-FPM** : `tail /var/log/php8.3-fpm.log | grep max_children`
|
||||
2. ✅ **Vérifier MySQL** : `mysql -e "SHOW PROCESSLIST;"`
|
||||
3. ✅ **Vérifier nginx** : `tail /var/log/nginx/error.log | grep -E "502|504"`
|
||||
4. ✅ **Vérifier RAM** : `free -h`
|
||||
5. ✅ **Vérifier I/O** : `iostat -x 1 5`
|
||||
|
||||
### Lenteurs
|
||||
1. ✅ **Slow queries MySQL** : `tail /var/lib/mysql/slow-query.log`
|
||||
2. ✅ **Slow PHP** : `tail /var/log/php8.3-fpm-slow.log`
|
||||
3. ✅ **CPU** : `top -bn1 | head -20`
|
||||
4. ✅ **I/O disque** : `iotop -oa`
|
||||
|
||||
### Crashes / Erreurs 502
|
||||
1. ✅ **PHP-FPM status** : `systemctl status php8.3-fpm`
|
||||
2. ✅ **OOM Killer** : `dmesg | grep -i "out of memory"`
|
||||
3. ✅ **Logs PHP** : `tail -100 /var/log/php8.3-fpm.log`
|
||||
|
||||
---
|
||||
|
||||
## 10. Optimisations Recommandées
|
||||
|
||||
### PHP-FPM
|
||||
```ini
|
||||
# /etc/php/8.3/fpm/pool.d/www.conf
|
||||
pm = dynamic
|
||||
pm.max_children = 50-100
|
||||
pm.start_servers = 10-20
|
||||
pm.min_spare_servers = 5-10
|
||||
pm.max_spare_servers = 20-35
|
||||
pm.max_requests = 500
|
||||
request_slowlog_timeout = 3s
|
||||
```
|
||||
|
||||
### MariaDB
|
||||
```ini
|
||||
# /etc/mysql/mariadb.conf.d/50-server.cnf
|
||||
max_connections = 200-500
|
||||
innodb_buffer_pool_size = 16G # 70-80% de la RAM MySQL
|
||||
slow_query_log = 1
|
||||
long_query_time = 2
|
||||
log_queries_not_using_indexes = 1
|
||||
```
|
||||
|
||||
### Nginx
|
||||
```nginx
|
||||
# /etc/nginx/nginx.conf
|
||||
worker_processes auto;
|
||||
worker_connections 2048;
|
||||
keepalive_timeout 30;
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Résumé des Seuils Critiques
|
||||
|
||||
| Métrique | Warning | Critical |
|
||||
|----------|---------|----------|
|
||||
| **RAM** | >85% | >95% |
|
||||
| **CPU Load** | >70% CPU count | >150% CPU count |
|
||||
| **Disk I/O await** | >50ms | >200ms |
|
||||
| **Disk Space** | >80% | >90% |
|
||||
| **PHP Workers** | >80% max | max_children reached |
|
||||
| **MySQL Connections** | >80% max | = max_connections |
|
||||
| **Slow Queries** | >2s | >10s ou >50/min |
|
||||
| **Nginx 502/504** | >5 en 5min | >20 en 5min |
|
||||
|
||||
---
|
||||
|
||||
## Contacts et Escalade
|
||||
|
||||
En cas d'incident critique :
|
||||
1. Vérifier les logs (`/var/log/alerts.log`)
|
||||
2. Identifier le goulot (CPU/RAM/PHP/MySQL)
|
||||
3. Appliquer le correctif approprié
|
||||
4. Documenter l'incident
|
||||
|
||||
---
|
||||
|
||||
**Date de création :** 2025-10-18
|
||||
**Dernière mise à jour :** 2025-10-18
|
||||
**Version :** 1.0
|
||||
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*
|
||||
BIN
__MACOSX/geo-app-20251014/._pubspec.yaml
Executable file
BIN
__MACOSX/geo-app-20251014/._pubspec.yaml
Executable file
Binary file not shown.
BIN
__MACOSX/geo-app-20251014/build/._ios
Executable file
BIN
__MACOSX/geo-app-20251014/build/._ios
Executable file
Binary file not shown.
BIN
__MACOSX/geo-app-20251014/build/ios/._Release-iphoneos
Executable file
BIN
__MACOSX/geo-app-20251014/build/ios/._Release-iphoneos
Executable file
Binary file not shown.
BIN
__MACOSX/geo-app-20251014/build/ios/Release-iphoneos/._Flutter
Executable file
BIN
__MACOSX/geo-app-20251014/build/ios/Release-iphoneos/._Flutter
Executable file
Binary file not shown.
BIN
__MACOSX/geo-app-20251014/build/ios/Release-iphoneos/._Stripe
Executable file
BIN
__MACOSX/geo-app-20251014/build/ios/Release-iphoneos/._Stripe
Executable file
Binary file not shown.
BIN
__MACOSX/geo-app-20251014/build/ios/Release-iphoneos/._StripeApplePay
Executable file
BIN
__MACOSX/geo-app-20251014/build/ios/Release-iphoneos/._StripeApplePay
Executable file
Binary file not shown.
BIN
__MACOSX/geo-app-20251014/build/ios/Release-iphoneos/._StripeCore
Executable file
BIN
__MACOSX/geo-app-20251014/build/ios/Release-iphoneos/._StripeCore
Executable file
Binary file not shown.
Binary file not shown.
BIN
__MACOSX/geo-app-20251014/build/ios/Release-iphoneos/._StripePaymentSheet
Executable file
BIN
__MACOSX/geo-app-20251014/build/ios/Release-iphoneos/._StripePaymentSheet
Executable file
Binary file not shown.
BIN
__MACOSX/geo-app-20251014/build/ios/Release-iphoneos/._StripePayments
Executable file
BIN
__MACOSX/geo-app-20251014/build/ios/Release-iphoneos/._StripePayments
Executable file
Binary file not shown.
BIN
__MACOSX/geo-app-20251014/build/ios/Release-iphoneos/._StripePaymentsUI
Executable file
BIN
__MACOSX/geo-app-20251014/build/ios/Release-iphoneos/._StripePaymentsUI
Executable file
Binary file not shown.
BIN
__MACOSX/geo-app-20251014/build/ios/Release-iphoneos/._StripeTerminal
Executable file
BIN
__MACOSX/geo-app-20251014/build/ios/Release-iphoneos/._StripeTerminal
Executable file
Binary file not shown.
BIN
__MACOSX/geo-app-20251014/build/ios/Release-iphoneos/._StripeUICore
Executable file
BIN
__MACOSX/geo-app-20251014/build/ios/Release-iphoneos/._StripeUICore
Executable file
Binary file not shown.
BIN
__MACOSX/geo-app-20251014/build/ios/Release-iphoneos/._battery_plus
Executable file
BIN
__MACOSX/geo-app-20251014/build/ios/Release-iphoneos/._battery_plus
Executable file
Binary file not shown.
BIN
__MACOSX/geo-app-20251014/build/ios/Release-iphoneos/._connectivity_plus
Executable file
BIN
__MACOSX/geo-app-20251014/build/ios/Release-iphoneos/._connectivity_plus
Executable file
Binary file not shown.
BIN
__MACOSX/geo-app-20251014/build/ios/Release-iphoneos/._device_info_plus
Executable file
BIN
__MACOSX/geo-app-20251014/build/ios/Release-iphoneos/._device_info_plus
Executable file
Binary file not shown.
Binary file not shown.
BIN
__MACOSX/geo-app-20251014/build/ios/Release-iphoneos/._geolocator_apple
Executable file
BIN
__MACOSX/geo-app-20251014/build/ios/Release-iphoneos/._geolocator_apple
Executable file
Binary file not shown.
BIN
__MACOSX/geo-app-20251014/build/ios/Release-iphoneos/._image_picker_ios
Executable file
BIN
__MACOSX/geo-app-20251014/build/ios/Release-iphoneos/._image_picker_ios
Executable file
Binary file not shown.
BIN
__MACOSX/geo-app-20251014/build/ios/Release-iphoneos/._mek_stripe_terminal
Executable file
BIN
__MACOSX/geo-app-20251014/build/ios/Release-iphoneos/._mek_stripe_terminal
Executable file
Binary file not shown.
BIN
__MACOSX/geo-app-20251014/build/ios/Release-iphoneos/._nfc_manager
Executable file
BIN
__MACOSX/geo-app-20251014/build/ios/Release-iphoneos/._nfc_manager
Executable file
Binary file not shown.
BIN
__MACOSX/geo-app-20251014/build/ios/Release-iphoneos/._path_provider_foundation
Executable file
BIN
__MACOSX/geo-app-20251014/build/ios/Release-iphoneos/._path_provider_foundation
Executable file
Binary file not shown.
BIN
__MACOSX/geo-app-20251014/build/ios/Release-iphoneos/._permission_handler_apple
Executable file
BIN
__MACOSX/geo-app-20251014/build/ios/Release-iphoneos/._permission_handler_apple
Executable file
Binary file not shown.
BIN
__MACOSX/geo-app-20251014/build/ios/Release-iphoneos/._stripe_ios
Executable file
BIN
__MACOSX/geo-app-20251014/build/ios/Release-iphoneos/._stripe_ios
Executable file
Binary file not shown.
BIN
__MACOSX/geo-app-20251014/build/ios/Release-iphoneos/._url_launcher_ios
Executable file
BIN
__MACOSX/geo-app-20251014/build/ios/Release-iphoneos/._url_launcher_ios
Executable file
Binary file not shown.
BIN
__MACOSX/geo-app-20251014/ios/._Runner.xcodeproj
Executable file
BIN
__MACOSX/geo-app-20251014/ios/._Runner.xcodeproj
Executable file
Binary file not shown.
BIN
__MACOSX/geo-app-20251014/ios/._Runner.xcworkspace
Executable file
BIN
__MACOSX/geo-app-20251014/ios/._Runner.xcworkspace
Executable file
Binary file not shown.
BIN
__MACOSX/geo-app-20251014/ios/Pods/._Pods.xcodeproj
generated
Executable file
BIN
__MACOSX/geo-app-20251014/ios/Pods/._Pods.xcodeproj
generated
Executable file
Binary file not shown.
43
api/.vscode/settings.json
vendored
Normal file → Executable file
43
api/.vscode/settings.json
vendored
Normal file → Executable file
@@ -4,7 +4,7 @@
|
||||
// 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.enabled": true, // 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,
|
||||
@@ -75,6 +75,16 @@
|
||||
"explorer.autoReveal": false,
|
||||
"explorer.confirmDragAndDrop": false,
|
||||
"emmet.triggerExpansionOnTab": true,
|
||||
"emmet.includeLanguages": {
|
||||
"javascript": "javascriptreact"
|
||||
},
|
||||
"problems.decorations.enabled": true,
|
||||
"explorer.decorations.colors": true,
|
||||
"explorer.decorations.badges": true,
|
||||
"php.validate.enable": true,
|
||||
"php.suggest.basic": false,
|
||||
"dart.analysisExcludedFolders": [],
|
||||
"dart.enableSdkFormatter": true,
|
||||
|
||||
// Fichiers
|
||||
"files.defaultLanguage": "markdown",
|
||||
@@ -87,7 +97,7 @@
|
||||
"typescript.preferences.importModuleSpecifierEnding": "js",
|
||||
|
||||
// Extensions
|
||||
"tailwindCSS.experimental.configFile": "frontend/tailwind.config.js",
|
||||
"tailwindCSS.experimental.configFile": "web/tailwind.config.js",
|
||||
"editor.quickSuggestions": {
|
||||
"strings": true
|
||||
},
|
||||
@@ -98,27 +108,28 @@
|
||||
},
|
||||
"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",
|
||||
|
||||
}
|
||||
|
||||
82
api/CLAUDE.md
Executable file
82
api/CLAUDE.md
Executable file
@@ -0,0 +1,82 @@
|
||||
# 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 DEV: `./deploy-api.sh` - deploy local code to dva-geo on IN3 (195.154.80.116)
|
||||
- Deploy to REC: `./deploy-api.sh rca` - deploy from dva-geo to rca-geo on IN3
|
||||
- Deploy to PROD: `./deploy-api.sh pra` - deploy from rca-geo (IN3) to pra-geo (IN4)
|
||||
- Export operations: `php export_operation.php` - export operations data
|
||||
|
||||
## Development Environment
|
||||
- **DEV Container**: dva-geo on IN3 server (195.154.80.116)
|
||||
- **DEV API URL Public**: https://dapp.geosector.fr/api/
|
||||
- **DEV API URL Internal**: http://13.23.33.43/api/
|
||||
- **Access**: Via Incus container on IN3 server
|
||||
|
||||
## 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
|
||||
651
api/PM7/d6back.sh
Normal file
651
api/PM7/d6back.sh
Normal file
@@ -0,0 +1,651 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -uo pipefail
|
||||
# Note: Removed -e to allow script to continue on errors
|
||||
# Errors are handled explicitly with ERROR_COUNT
|
||||
|
||||
# Parse command line arguments
|
||||
ONLY_DB=false
|
||||
if [[ "${1:-}" == "-onlydb" ]]; then
|
||||
ONLY_DB=true
|
||||
echo "Mode: Database backup only"
|
||||
fi
|
||||
|
||||
# Configuration
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
CONFIG_FILE="$SCRIPT_DIR/d6back.yaml"
|
||||
LOG_DIR="$SCRIPT_DIR/logs"
|
||||
mkdir -p "$LOG_DIR"
|
||||
LOG_FILE="$LOG_DIR/d6back-$(date +%Y%m%d).log"
|
||||
ERROR_COUNT=0
|
||||
RECAP_FILE="/tmp/backup_recap_$$.txt"
|
||||
|
||||
# Lock file to prevent concurrent executions
|
||||
LOCK_FILE="/var/lock/d6back.lock"
|
||||
exec 200>"$LOCK_FILE"
|
||||
if ! flock -n 200; then
|
||||
echo "ERROR: Another backup is already running" >&2
|
||||
exit 1
|
||||
fi
|
||||
trap 'flock -u 200' EXIT
|
||||
|
||||
# Clean old log files (keep only last 10)
|
||||
find "$LOG_DIR" -maxdepth 1 -name "d6back-*.log" -type f 2>/dev/null | sort -r | tail -n +11 | xargs -r rm -f || true
|
||||
|
||||
# Check dependencies - COMMENTED OUT
|
||||
# for cmd in yq ssh tar openssl; do
|
||||
# if ! command -v "$cmd" &> /dev/null; then
|
||||
# echo "ERROR: $cmd is required but not installed" | tee -a "$LOG_FILE"
|
||||
# exit 1
|
||||
# fi
|
||||
# done
|
||||
|
||||
# Load config
|
||||
DIR_BACKUP=$(yq '.global.dir_backup' "$CONFIG_FILE" | tr -d '"')
|
||||
ENC_KEY_PATH=$(yq '.global.enc_key' "$CONFIG_FILE" | tr -d '"')
|
||||
BACKUP_SERVER=$(yq '.global.backup_server // "BACKUP"' "$CONFIG_FILE" | tr -d '"')
|
||||
EMAIL_TO=$(yq '.global.email_to // "support@unikoffice.com"' "$CONFIG_FILE" | tr -d '"')
|
||||
KEEP_DIRS=$(yq '.global.keep_dirs' "$CONFIG_FILE" | tr -d '"')
|
||||
KEEP_DB=$(yq '.global.keep_db' "$CONFIG_FILE" | tr -d '"')
|
||||
|
||||
# Load encryption key
|
||||
if [[ ! -f "$ENC_KEY_PATH" ]]; then
|
||||
echo "ERROR: Encryption key not found: $ENC_KEY_PATH" | tee -a "$LOG_FILE"
|
||||
exit 1
|
||||
fi
|
||||
ENC_KEY=$(cat "$ENC_KEY_PATH")
|
||||
|
||||
echo "=== Backup Started $(date) ===" | tee -a "$LOG_FILE"
|
||||
echo "Backup directory: $DIR_BACKUP" | tee -a "$LOG_FILE"
|
||||
|
||||
# Check available disk space
|
||||
DISK_USAGE=$(df "$DIR_BACKUP" | tail -1 | awk '{print $5}' | sed 's/%//')
|
||||
DISK_FREE=$((100 - DISK_USAGE))
|
||||
|
||||
if [[ $DISK_FREE -lt 20 ]]; then
|
||||
echo "WARNING: Low disk space! Only ${DISK_FREE}% free on backup partition" | tee -a "$LOG_FILE"
|
||||
|
||||
# Send warning email
|
||||
echo "Sending DISK SPACE WARNING email to $EMAIL_TO (${DISK_FREE}% free)" | tee -a "$LOG_FILE"
|
||||
if command -v msmtp &> /dev/null; then
|
||||
{
|
||||
echo "To: $EMAIL_TO"
|
||||
echo "Subject: Backup${BACKUP_SERVER} WARNING - Low disk space (${DISK_FREE}% free)"
|
||||
echo ""
|
||||
echo "WARNING: Low disk space on $(hostname)"
|
||||
echo ""
|
||||
echo "Backup directory: $DIR_BACKUP"
|
||||
echo "Disk usage: ${DISK_USAGE}%"
|
||||
echo "Free space: ${DISK_FREE}%"
|
||||
echo ""
|
||||
echo "The backup will continue but please free up some space soon."
|
||||
echo ""
|
||||
echo "Date: $(date '+%d.%m.%Y %H:%M')"
|
||||
} | msmtp "$EMAIL_TO"
|
||||
echo "DISK SPACE WARNING email sent successfully to $EMAIL_TO" | tee -a "$LOG_FILE"
|
||||
else
|
||||
echo "WARNING: msmtp not found - DISK WARNING email NOT sent" | tee -a "$LOG_FILE"
|
||||
fi
|
||||
else
|
||||
echo "Disk space OK: ${DISK_FREE}% free" | tee -a "$LOG_FILE"
|
||||
fi
|
||||
|
||||
# Initialize recap file
|
||||
echo "BACKUP REPORT - $(hostname) - $(date '+%d.%m.%Y %H')h" > "$RECAP_FILE"
|
||||
echo "========================================" >> "$RECAP_FILE"
|
||||
echo "" >> "$RECAP_FILE"
|
||||
|
||||
# Function to format size in MB with thousand separator
|
||||
format_size_mb() {
|
||||
local file="$1"
|
||||
if [[ -f "$file" ]]; then
|
||||
local size_kb=$(du -k "$file" | cut -f1)
|
||||
local size_mb=$((size_kb / 1024))
|
||||
# Add thousand separator with printf and sed
|
||||
printf "%d" "$size_mb" | sed ':a;s/\B[0-9]\{3\}\>/\.&/;ta'
|
||||
else
|
||||
echo "0"
|
||||
fi
|
||||
}
|
||||
|
||||
# Function to calculate age in days
|
||||
get_age_days() {
|
||||
local file="$1"
|
||||
local now=$(date +%s)
|
||||
local file_time=$(stat -c %Y "$file" 2>/dev/null || echo 0)
|
||||
echo $(( (now - file_time) / 86400 ))
|
||||
}
|
||||
|
||||
# Function to get week number of year for a file
|
||||
get_week_year() {
|
||||
local file="$1"
|
||||
local file_time=$(stat -c %Y "$file" 2>/dev/null || echo 0)
|
||||
date -d "@$file_time" +"%Y-%W"
|
||||
}
|
||||
|
||||
# Function to cleanup old backups according to retention policy
|
||||
cleanup_old_backups() {
|
||||
local DELETED_COUNT=0
|
||||
local KEPT_COUNT=0
|
||||
|
||||
echo "" | tee -a "$LOG_FILE"
|
||||
echo "=== Starting Backup Retention Cleanup ===" | tee -a "$LOG_FILE"
|
||||
|
||||
# Parse retention periods
|
||||
local KEEP_DIRS_DAYS=${KEEP_DIRS%d} # Remove 'd' suffix
|
||||
|
||||
# Parse database retention (5d,3w,15m)
|
||||
IFS=',' read -r KEEP_DB_DAILY KEEP_DB_WEEKLY KEEP_DB_MONTHLY <<< "$KEEP_DB"
|
||||
local KEEP_DB_DAILY_DAYS=${KEEP_DB_DAILY%d}
|
||||
local KEEP_DB_WEEKLY_WEEKS=${KEEP_DB_WEEKLY%w}
|
||||
local KEEP_DB_MONTHLY_MONTHS=${KEEP_DB_MONTHLY%m}
|
||||
|
||||
# Convert to days
|
||||
local KEEP_DB_WEEKLY_DAYS=$((KEEP_DB_WEEKLY_WEEKS * 7))
|
||||
local KEEP_DB_MONTHLY_DAYS=$((KEEP_DB_MONTHLY_MONTHS * 30))
|
||||
|
||||
echo "Retention policy: dirs=${KEEP_DIRS_DAYS}d, db=${KEEP_DB_DAILY_DAYS}d/${KEEP_DB_WEEKLY_WEEKS}w/${KEEP_DB_MONTHLY_MONTHS}m" | tee -a "$LOG_FILE"
|
||||
|
||||
# Process each host directory
|
||||
for host_dir in "$DIR_BACKUP"/*; do
|
||||
if [[ ! -d "$host_dir" ]]; then
|
||||
continue
|
||||
fi
|
||||
|
||||
local host_name=$(basename "$host_dir")
|
||||
echo " Cleaning host: $host_name" | tee -a "$LOG_FILE"
|
||||
|
||||
# Clean directory backups (*.tar.gz but not *.sql.gz.enc)
|
||||
while IFS= read -r -d '' file; do
|
||||
if [[ $(basename "$file") == *".sql.gz.enc" ]]; then
|
||||
continue # Skip SQL files
|
||||
fi
|
||||
|
||||
local age_days=$(get_age_days "$file")
|
||||
|
||||
if [[ $age_days -gt $KEEP_DIRS_DAYS ]]; then
|
||||
rm -f "$file"
|
||||
echo " Deleted: $(basename "$file") (${age_days}d > ${KEEP_DIRS_DAYS}d)" | tee -a "$LOG_FILE"
|
||||
((DELETED_COUNT++))
|
||||
else
|
||||
((KEPT_COUNT++))
|
||||
fi
|
||||
done < <(find "$host_dir" -name "*.tar.gz" -type f -print0 2>/dev/null)
|
||||
|
||||
# Clean database backups with retention policy
|
||||
declare -A db_files
|
||||
|
||||
while IFS= read -r -d '' file; do
|
||||
local filename=$(basename "$file")
|
||||
local db_name=${filename%%_*}
|
||||
|
||||
if [[ -z "${db_files[$db_name]:-}" ]]; then
|
||||
db_files[$db_name]="$file"
|
||||
else
|
||||
db_files[$db_name]+=$'\n'"$file"
|
||||
fi
|
||||
done < <(find "$host_dir" -name "*.sql.gz.enc" -type f -print0 2>/dev/null)
|
||||
|
||||
# Process each database
|
||||
for db_name in "${!db_files[@]}"; do
|
||||
# Sort files by age (newest first)
|
||||
mapfile -t files < <(echo "${db_files[$db_name]}" | while IFS= read -r f; do
|
||||
echo "$f"
|
||||
done | xargs -I {} stat -c "%Y {}" {} 2>/dev/null | sort -rn | cut -d' ' -f2-)
|
||||
|
||||
# Track which files to keep
|
||||
declare -A keep_daily
|
||||
declare -A keep_weekly
|
||||
|
||||
for file in "${files[@]}"; do
|
||||
local age_days=$(get_age_days "$file")
|
||||
|
||||
if [[ $age_days -le $KEEP_DB_DAILY_DAYS ]]; then
|
||||
# Keep all files within daily retention
|
||||
((KEPT_COUNT++))
|
||||
|
||||
elif [[ $age_days -le $KEEP_DB_WEEKLY_DAYS ]]; then
|
||||
# Weekly retention: keep one per day
|
||||
local file_date=$(date -d "@$(stat -c %Y "$file")" +"%Y-%m-%d")
|
||||
|
||||
if [[ -z "${keep_daily[$file_date]:-}" ]]; then
|
||||
keep_daily[$file_date]="$file"
|
||||
((KEPT_COUNT++))
|
||||
else
|
||||
rm -f "$file"
|
||||
((DELETED_COUNT++))
|
||||
fi
|
||||
|
||||
elif [[ $age_days -le $KEEP_DB_MONTHLY_DAYS ]]; then
|
||||
# Monthly retention: keep one per week
|
||||
local week_year=$(get_week_year "$file")
|
||||
|
||||
if [[ -z "${keep_weekly[$week_year]:-}" ]]; then
|
||||
keep_weekly[$week_year]="$file"
|
||||
((KEPT_COUNT++))
|
||||
else
|
||||
rm -f "$file"
|
||||
((DELETED_COUNT++))
|
||||
fi
|
||||
|
||||
else
|
||||
# Beyond retention period
|
||||
rm -f "$file"
|
||||
echo " Deleted: $(basename "$file") (${age_days}d > ${KEEP_DB_MONTHLY_DAYS}d)" | tee -a "$LOG_FILE"
|
||||
((DELETED_COUNT++))
|
||||
fi
|
||||
done
|
||||
|
||||
unset keep_daily keep_weekly
|
||||
done
|
||||
|
||||
unset db_files
|
||||
done
|
||||
|
||||
echo "Cleanup completed: ${DELETED_COUNT} deleted, ${KEPT_COUNT} kept" | tee -a "$LOG_FILE"
|
||||
|
||||
# Add cleanup summary to recap file
|
||||
echo "" >> "$RECAP_FILE"
|
||||
echo "CLEANUP SUMMARY:" >> "$RECAP_FILE"
|
||||
echo " Files deleted: $DELETED_COUNT" >> "$RECAP_FILE"
|
||||
echo " Files kept: $KEPT_COUNT" >> "$RECAP_FILE"
|
||||
}
|
||||
|
||||
# Function to backup a single database (must be defined before use)
|
||||
backup_database() {
|
||||
local database="$1"
|
||||
local timestamp="$(date +%Y%m%d_%H)"
|
||||
local backup_file="$backup_dir/sql/${database}_${timestamp}.sql.gz.enc"
|
||||
|
||||
echo " Backing up database: $database" | tee -a "$LOG_FILE"
|
||||
|
||||
if [[ "$ssh_user" != "root" ]]; then
|
||||
CMD_PREFIX="sudo"
|
||||
else
|
||||
CMD_PREFIX=""
|
||||
fi
|
||||
|
||||
# Execute backup with encryption
|
||||
# First test MySQL connection to get clear error messages (|| true to continue on error)
|
||||
MYSQL_TEST=$(ssh -i "$ssh_key" -p "$ssh_port" -o ConnectTimeout=20 "$ssh_user@$host_ip" \
|
||||
"$CMD_PREFIX incus exec $container_name -- bash -c 'cat > /tmp/d6back.cnf << EOF
|
||||
[client]
|
||||
user=$db_user
|
||||
password=$db_pass
|
||||
host=$db_host
|
||||
EOF
|
||||
chmod 600 /tmp/d6back.cnf
|
||||
mariadb --defaults-extra-file=/tmp/d6back.cnf -e \"SELECT 1\" 2>&1
|
||||
rm -f /tmp/d6back.cnf'" 2>/dev/null || true)
|
||||
|
||||
if ssh -i "$ssh_key" -p "$ssh_port" -o ConnectTimeout=20 "$ssh_user@$host_ip" \
|
||||
"$CMD_PREFIX incus exec $container_name -- bash -c 'cat > /tmp/d6back.cnf << EOF
|
||||
[client]
|
||||
user=$db_user
|
||||
password=$db_pass
|
||||
host=$db_host
|
||||
EOF
|
||||
chmod 600 /tmp/d6back.cnf
|
||||
mariadb-dump --defaults-extra-file=/tmp/d6back.cnf --single-transaction --lock-tables=false --add-drop-table --create-options --databases $database 2>/dev/null | sed -e \"/^CREATE DATABASE/s/\\\`$database\\\`/\\\`${database}_${timestamp}\\\`/\" -e \"/^USE/s/\\\`$database\\\`/\\\`${database}_${timestamp}\\\`/\" | gzip
|
||||
rm -f /tmp/d6back.cnf'" | \
|
||||
openssl enc -aes-256-cbc -salt -pass pass:"$ENC_KEY" -pbkdf2 > "$backup_file" 2>/dev/null; then
|
||||
|
||||
# Validate backup file size (encrypted SQL should be > 100 bytes)
|
||||
if [[ -f "$backup_file" ]]; then
|
||||
file_size=$(stat -c%s "$backup_file" 2>/dev/null || echo 0)
|
||||
if [[ $file_size -lt 100 ]]; then
|
||||
# Analyze MySQL connection test results
|
||||
if [[ "$MYSQL_TEST" == *"Access denied"* ]]; then
|
||||
echo " ERROR: MySQL authentication failed for $database on $host_name/$container_name" | tee -a "$LOG_FILE"
|
||||
echo " User: $db_user@$db_host - Check password in configuration" | tee -a "$LOG_FILE"
|
||||
elif [[ "$MYSQL_TEST" == *"Unknown database"* ]]; then
|
||||
echo " ERROR: Database '$database' does not exist on $host_name/$container_name" | tee -a "$LOG_FILE"
|
||||
elif [[ "$MYSQL_TEST" == *"Can't connect"* ]]; then
|
||||
echo " ERROR: Cannot connect to MySQL server at $db_host in $container_name" | tee -a "$LOG_FILE"
|
||||
else
|
||||
echo " ERROR: Backup file too small (${file_size} bytes): $database on $host_name/$container_name" | tee -a "$LOG_FILE"
|
||||
fi
|
||||
|
||||
((ERROR_COUNT++))
|
||||
rm -f "$backup_file"
|
||||
else
|
||||
size=$(du -h "$backup_file" | cut -f1)
|
||||
size_mb=$(format_size_mb "$backup_file")
|
||||
echo " ✓ Saved (encrypted): $(basename "$backup_file") ($size)" | tee -a "$LOG_FILE"
|
||||
echo " SQL: $(basename "$backup_file") - ${size_mb} Mo" >> "$RECAP_FILE"
|
||||
|
||||
# Test backup integrity
|
||||
if ! openssl enc -aes-256-cbc -d -pass pass:"$ENC_KEY" -pbkdf2 -in "$backup_file" | gunzip -t 2>/dev/null; then
|
||||
echo " ERROR: Backup integrity check failed for $database" | tee -a "$LOG_FILE"
|
||||
((ERROR_COUNT++))
|
||||
fi
|
||||
fi
|
||||
else
|
||||
echo " ERROR: Backup file not created: $database" | tee -a "$LOG_FILE"
|
||||
((ERROR_COUNT++))
|
||||
fi
|
||||
else
|
||||
# Analyze MySQL connection test for failed backup
|
||||
if [[ "$MYSQL_TEST" == *"Access denied"* ]]; then
|
||||
echo " ERROR: MySQL authentication failed for $database on $host_name/$container_name" | tee -a "$LOG_FILE"
|
||||
echo " User: $db_user@$db_host - Check password in configuration" | tee -a "$LOG_FILE"
|
||||
elif [[ "$MYSQL_TEST" == *"Unknown database"* ]]; then
|
||||
echo " ERROR: Database '$database' does not exist on $host_name/$container_name" | tee -a "$LOG_FILE"
|
||||
elif [[ "$MYSQL_TEST" == *"Can't connect"* ]]; then
|
||||
echo " ERROR: Cannot connect to MySQL server at $db_host in $container_name" | tee -a "$LOG_FILE"
|
||||
else
|
||||
echo " ERROR: Failed to backup database $database on $host_name/$container_name" | tee -a "$LOG_FILE"
|
||||
fi
|
||||
|
||||
((ERROR_COUNT++))
|
||||
rm -f "$backup_file"
|
||||
fi
|
||||
}
|
||||
|
||||
# Process each host
|
||||
host_count=$(yq '.hosts | length' "$CONFIG_FILE")
|
||||
|
||||
for ((i=0; i<$host_count; i++)); do
|
||||
host_name=$(yq ".hosts[$i].name" "$CONFIG_FILE" | tr -d '"')
|
||||
host_ip=$(yq ".hosts[$i].ip" "$CONFIG_FILE" | tr -d '"')
|
||||
ssh_user=$(yq ".hosts[$i].user" "$CONFIG_FILE" | tr -d '"')
|
||||
ssh_key=$(yq ".hosts[$i].key" "$CONFIG_FILE" | tr -d '"')
|
||||
ssh_port=$(yq ".hosts[$i].port // 22" "$CONFIG_FILE" | tr -d '"')
|
||||
|
||||
echo "Processing host: $host_name ($host_ip)" | tee -a "$LOG_FILE"
|
||||
echo "" >> "$RECAP_FILE"
|
||||
echo "HOST: $host_name ($host_ip)" >> "$RECAP_FILE"
|
||||
echo "----------------------------" >> "$RECAP_FILE"
|
||||
|
||||
# Test SSH connection
|
||||
if ! ssh -i "$ssh_key" -p "$ssh_port" -o ConnectTimeout=20 -o StrictHostKeyChecking=no "$ssh_user@$host_ip" "true" 2>/dev/null; then
|
||||
echo " ERROR: Cannot connect to $host_name ($host_ip)" | tee -a "$LOG_FILE"
|
||||
((ERROR_COUNT++))
|
||||
continue
|
||||
fi
|
||||
|
||||
# Process containers
|
||||
container_count=$(yq ".hosts[$i].containers | length" "$CONFIG_FILE" 2>/dev/null || echo "0")
|
||||
|
||||
for ((c=0; c<$container_count; c++)); do
|
||||
container_name=$(yq ".hosts[$i].containers[$c].name" "$CONFIG_FILE" | tr -d '"')
|
||||
|
||||
echo " Processing container: $container_name" | tee -a "$LOG_FILE"
|
||||
|
||||
# Add container to recap
|
||||
echo "" >> "$RECAP_FILE"
|
||||
echo " Container: $container_name" >> "$RECAP_FILE"
|
||||
|
||||
# Create backup directories
|
||||
backup_dir="$DIR_BACKUP/$host_name/$container_name"
|
||||
mkdir -p "$backup_dir"
|
||||
mkdir -p "$backup_dir/sql"
|
||||
|
||||
# Backup directories (skip if -onlydb mode)
|
||||
if [[ "$ONLY_DB" == "false" ]]; then
|
||||
dir_count=$(yq ".hosts[$i].containers[$c].dirs | length" "$CONFIG_FILE" 2>/dev/null || echo "0")
|
||||
|
||||
for ((d=0; d<$dir_count; d++)); do
|
||||
dir_path=$(yq ".hosts[$i].containers[$c].dirs[$d]" "$CONFIG_FILE" | sed 's/^"\|"$//g')
|
||||
|
||||
# Use sudo if not root
|
||||
if [[ "$ssh_user" != "root" ]]; then
|
||||
CMD_PREFIX="sudo"
|
||||
else
|
||||
CMD_PREFIX=""
|
||||
fi
|
||||
|
||||
# Special handling for /var/www - backup each subdirectory separately
|
||||
if [[ "$dir_path" == "/var/www" ]]; then
|
||||
echo " Backing up subdirectories of $dir_path" | tee -a "$LOG_FILE"
|
||||
|
||||
# Get list of subdirectories
|
||||
subdirs=$(ssh -i "$ssh_key" -p "$ssh_port" -o ConnectTimeout=20 "$ssh_user@$host_ip" \
|
||||
"$CMD_PREFIX incus exec $container_name -- find /var/www -maxdepth 1 -type d ! -path /var/www" 2>/dev/null || echo "")
|
||||
|
||||
for subdir in $subdirs; do
|
||||
subdir_name=$(basename "$subdir" | tr '/' '_')
|
||||
backup_file="$backup_dir/www_${subdir_name}_$(date +%Y%m%d_%H).tar.gz"
|
||||
|
||||
echo " Backing up: $subdir" | tee -a "$LOG_FILE"
|
||||
|
||||
if ssh -i "$ssh_key" -p "$ssh_port" -o ConnectTimeout=20 "$ssh_user@$host_ip" \
|
||||
"$CMD_PREFIX incus exec $container_name -- tar czf - $subdir 2>/dev/null" > "$backup_file"; then
|
||||
|
||||
# Validate backup file size (tar.gz should be > 1KB)
|
||||
if [[ -f "$backup_file" ]]; then
|
||||
file_size=$(stat -c%s "$backup_file" 2>/dev/null || echo 0)
|
||||
if [[ $file_size -lt 1024 ]]; then
|
||||
echo " WARNING: Backup file very small (${file_size} bytes): $subdir" | tee -a "$LOG_FILE"
|
||||
# Keep the file but note it's small
|
||||
size=$(du -h "$backup_file" | cut -f1)
|
||||
size_mb=$(format_size_mb "$backup_file")
|
||||
echo " ✓ Saved (small): $(basename "$backup_file") ($size)" | tee -a "$LOG_FILE"
|
||||
echo " DIR: $(basename "$backup_file") - ${size_mb} Mo (WARNING: small)" >> "$RECAP_FILE"
|
||||
else
|
||||
size=$(du -h "$backup_file" | cut -f1)
|
||||
size_mb=$(format_size_mb "$backup_file")
|
||||
echo " ✓ Saved: $(basename "$backup_file") ($size)" | tee -a "$LOG_FILE"
|
||||
echo " DIR: $(basename "$backup_file") - ${size_mb} Mo" >> "$RECAP_FILE"
|
||||
fi
|
||||
|
||||
# Test tar integrity
|
||||
if ! tar tzf "$backup_file" >/dev/null 2>&1; then
|
||||
echo " ERROR: Tar integrity check failed" | tee -a "$LOG_FILE"
|
||||
((ERROR_COUNT++))
|
||||
fi
|
||||
else
|
||||
echo " ERROR: Backup file not created: $subdir" | tee -a "$LOG_FILE"
|
||||
((ERROR_COUNT++))
|
||||
fi
|
||||
else
|
||||
echo " ERROR: Failed to backup $subdir" | tee -a "$LOG_FILE"
|
||||
((ERROR_COUNT++))
|
||||
rm -f "$backup_file"
|
||||
fi
|
||||
done
|
||||
else
|
||||
# Normal backup for other directories
|
||||
dir_name=$(basename "$dir_path" | tr '/' '_')
|
||||
backup_file="$backup_dir/${dir_name}_$(date +%Y%m%d_%H).tar.gz"
|
||||
|
||||
echo " Backing up: $dir_path" | tee -a "$LOG_FILE"
|
||||
|
||||
if ssh -i "$ssh_key" -p "$ssh_port" -o ConnectTimeout=20 "$ssh_user@$host_ip" \
|
||||
"$CMD_PREFIX incus exec $container_name -- tar czf - $dir_path 2>/dev/null" > "$backup_file"; then
|
||||
|
||||
# Validate backup file size (tar.gz should be > 1KB)
|
||||
if [[ -f "$backup_file" ]]; then
|
||||
file_size=$(stat -c%s "$backup_file" 2>/dev/null || echo 0)
|
||||
if [[ $file_size -lt 1024 ]]; then
|
||||
echo " WARNING: Backup file very small (${file_size} bytes): $dir_path" | tee -a "$LOG_FILE"
|
||||
# Keep the file but note it's small
|
||||
size=$(du -h "$backup_file" | cut -f1)
|
||||
size_mb=$(format_size_mb "$backup_file")
|
||||
echo " ✓ Saved (small): $(basename "$backup_file") ($size)" | tee -a "$LOG_FILE"
|
||||
echo " DIR: $(basename "$backup_file") - ${size_mb} Mo (WARNING: small)" >> "$RECAP_FILE"
|
||||
else
|
||||
size=$(du -h "$backup_file" | cut -f1)
|
||||
size_mb=$(format_size_mb "$backup_file")
|
||||
echo " ✓ Saved: $(basename "$backup_file") ($size)" | tee -a "$LOG_FILE"
|
||||
echo " DIR: $(basename "$backup_file") - ${size_mb} Mo" >> "$RECAP_FILE"
|
||||
fi
|
||||
|
||||
# Test tar integrity
|
||||
if ! tar tzf "$backup_file" >/dev/null 2>&1; then
|
||||
echo " ERROR: Tar integrity check failed" | tee -a "$LOG_FILE"
|
||||
((ERROR_COUNT++))
|
||||
fi
|
||||
else
|
||||
echo " ERROR: Backup file not created: $dir_path" | tee -a "$LOG_FILE"
|
||||
((ERROR_COUNT++))
|
||||
fi
|
||||
else
|
||||
echo " ERROR: Failed to backup $dir_path" | tee -a "$LOG_FILE"
|
||||
((ERROR_COUNT++))
|
||||
rm -f "$backup_file"
|
||||
fi
|
||||
fi
|
||||
done
|
||||
fi # End of directory backup section
|
||||
|
||||
# Backup databases
|
||||
db_user=$(yq ".hosts[$i].containers[$c].db_user" "$CONFIG_FILE" 2>/dev/null | tr -d '"')
|
||||
db_pass=$(yq ".hosts[$i].containers[$c].db_pass" "$CONFIG_FILE" 2>/dev/null | tr -d '"')
|
||||
db_host=$(yq ".hosts[$i].containers[$c].db_host // \"localhost\"" "$CONFIG_FILE" 2>/dev/null | tr -d '"')
|
||||
|
||||
# Check if we're in onlydb mode
|
||||
if [[ "$ONLY_DB" == "true" ]]; then
|
||||
# Use onlydb list if it exists
|
||||
onlydb_count=$(yq ".hosts[$i].containers[$c].onlydb | length" "$CONFIG_FILE" 2>/dev/null || echo "0")
|
||||
if [[ "$onlydb_count" != "0" ]] && [[ "$onlydb_count" != "null" ]]; then
|
||||
db_count="$onlydb_count"
|
||||
use_onlydb=true
|
||||
else
|
||||
# No onlydb list, skip this container in onlydb mode
|
||||
continue
|
||||
fi
|
||||
else
|
||||
# Normal mode - use databases list
|
||||
db_count=$(yq ".hosts[$i].containers[$c].databases | length" "$CONFIG_FILE" 2>/dev/null || echo "0")
|
||||
use_onlydb=false
|
||||
fi
|
||||
|
||||
if [[ -n "$db_user" ]] && [[ -n "$db_pass" ]] && [[ "$db_count" != "0" ]]; then
|
||||
for ((db=0; db<$db_count; db++)); do
|
||||
if [[ "$use_onlydb" == "true" ]]; then
|
||||
db_name=$(yq ".hosts[$i].containers[$c].onlydb[$db]" "$CONFIG_FILE" | tr -d '"')
|
||||
else
|
||||
db_name=$(yq ".hosts[$i].containers[$c].databases[$db]" "$CONFIG_FILE" | tr -d '"')
|
||||
fi
|
||||
|
||||
if [[ "$db_name" == "ALL" ]]; then
|
||||
echo " Fetching all databases..." | tee -a "$LOG_FILE"
|
||||
|
||||
# Get database list
|
||||
if [[ "$ssh_user" != "root" ]]; then
|
||||
db_list=$(ssh -i "$ssh_key" -p "$ssh_port" -o ConnectTimeout=20 "$ssh_user@$host_ip" \
|
||||
"sudo incus exec $container_name -- bash -c 'cat > /tmp/d6back.cnf << EOF
|
||||
[client]
|
||||
user=$db_user
|
||||
password=$db_pass
|
||||
host=$db_host
|
||||
EOF
|
||||
chmod 600 /tmp/d6back.cnf
|
||||
mariadb --defaults-extra-file=/tmp/d6back.cnf -e \"SHOW DATABASES;\" 2>/dev/null
|
||||
rm -f /tmp/d6back.cnf'" | \
|
||||
grep -Ev '^(Database|information_schema|performance_schema|mysql|sys)$' || echo "")
|
||||
else
|
||||
db_list=$(ssh -i "$ssh_key" -p "$ssh_port" -o ConnectTimeout=20 "$ssh_user@$host_ip" \
|
||||
"incus exec $container_name -- bash -c 'cat > /tmp/d6back.cnf << EOF
|
||||
[client]
|
||||
user=$db_user
|
||||
password=$db_pass
|
||||
host=$db_host
|
||||
EOF
|
||||
chmod 600 /tmp/d6back.cnf
|
||||
mariadb --defaults-extra-file=/tmp/d6back.cnf -e \"SHOW DATABASES;\" 2>/dev/null
|
||||
rm -f /tmp/d6back.cnf'" | \
|
||||
grep -Ev '^(Database|information_schema|performance_schema|mysql|sys)$' || echo "")
|
||||
fi
|
||||
|
||||
# Backup each database
|
||||
for single_db in $db_list; do
|
||||
backup_database "$single_db"
|
||||
done
|
||||
else
|
||||
backup_database "$db_name"
|
||||
fi
|
||||
done
|
||||
fi
|
||||
done
|
||||
done
|
||||
|
||||
echo "=== Backup Completed $(date) ===" | tee -a "$LOG_FILE"
|
||||
|
||||
# Cleanup old backups according to retention policy
|
||||
cleanup_old_backups
|
||||
|
||||
# Show summary
|
||||
total_size=$(du -sh "$DIR_BACKUP" 2>/dev/null | cut -f1)
|
||||
echo "Total backup size: $total_size" | tee -a "$LOG_FILE"
|
||||
|
||||
# Add summary to recap
|
||||
echo "" >> "$RECAP_FILE"
|
||||
echo "========================================" >> "$RECAP_FILE"
|
||||
|
||||
# Add size details per host/container
|
||||
echo "BACKUP SIZES:" >> "$RECAP_FILE"
|
||||
for host_dir in "$DIR_BACKUP"/*; do
|
||||
if [[ -d "$host_dir" ]]; then
|
||||
host_name=$(basename "$host_dir")
|
||||
host_size=$(du -sh "$host_dir" 2>/dev/null | cut -f1)
|
||||
echo "" >> "$RECAP_FILE"
|
||||
echo " $host_name: $host_size" >> "$RECAP_FILE"
|
||||
|
||||
# Size per container
|
||||
for container_dir in "$host_dir"/*; do
|
||||
if [[ -d "$container_dir" ]]; then
|
||||
container_name=$(basename "$container_dir")
|
||||
container_size=$(du -sh "$container_dir" 2>/dev/null | cut -f1)
|
||||
echo " - $container_name: $container_size" >> "$RECAP_FILE"
|
||||
fi
|
||||
done
|
||||
fi
|
||||
done
|
||||
|
||||
echo "" >> "$RECAP_FILE"
|
||||
echo "TOTAL SIZE: $total_size" >> "$RECAP_FILE"
|
||||
echo "COMPLETED: $(date '+%d.%m.%Y %H:%M')" >> "$RECAP_FILE"
|
||||
|
||||
# Prepare email subject with date format
|
||||
DATE_SUBJECT=$(date '+%d.%m.%Y %H')
|
||||
|
||||
# Send recap email
|
||||
if [[ $ERROR_COUNT -gt 0 ]]; then
|
||||
echo "Total errors: $ERROR_COUNT" | tee -a "$LOG_FILE"
|
||||
|
||||
# Add errors to recap
|
||||
echo "" >> "$RECAP_FILE"
|
||||
echo "ERRORS DETECTED: $ERROR_COUNT" >> "$RECAP_FILE"
|
||||
echo "----------------------------" >> "$RECAP_FILE"
|
||||
grep -i "ERROR" "$LOG_FILE" >> "$RECAP_FILE"
|
||||
|
||||
# Send email with ERROR in subject
|
||||
echo "Sending ERROR email to $EMAIL_TO (Errors found: $ERROR_COUNT)" | tee -a "$LOG_FILE"
|
||||
if command -v msmtp &> /dev/null; then
|
||||
{
|
||||
echo "To: $EMAIL_TO"
|
||||
echo "Subject: Backup${BACKUP_SERVER} ERROR $DATE_SUBJECT"
|
||||
echo ""
|
||||
cat "$RECAP_FILE"
|
||||
} | msmtp "$EMAIL_TO"
|
||||
echo "ERROR email sent successfully to $EMAIL_TO" | tee -a "$LOG_FILE"
|
||||
else
|
||||
echo "WARNING: msmtp not found - ERROR email NOT sent" | tee -a "$LOG_FILE"
|
||||
fi
|
||||
else
|
||||
echo "Backup completed successfully with no errors" | tee -a "$LOG_FILE"
|
||||
|
||||
# Send success recap email
|
||||
echo "Sending SUCCESS recap email to $EMAIL_TO" | tee -a "$LOG_FILE"
|
||||
if command -v msmtp &> /dev/null; then
|
||||
{
|
||||
echo "To: $EMAIL_TO"
|
||||
echo "Subject: Backup${BACKUP_SERVER} $DATE_SUBJECT"
|
||||
echo ""
|
||||
cat "$RECAP_FILE"
|
||||
} | msmtp "$EMAIL_TO"
|
||||
echo "SUCCESS recap email sent successfully to $EMAIL_TO" | tee -a "$LOG_FILE"
|
||||
else
|
||||
echo "WARNING: msmtp not found - SUCCESS recap email NOT sent" | tee -a "$LOG_FILE"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Clean up recap file
|
||||
rm -f "$RECAP_FILE"
|
||||
|
||||
# Exit with error code if there were errors
|
||||
if [[ $ERROR_COUNT -gt 0 ]]; then
|
||||
exit 1
|
||||
fi
|
||||
112
api/PM7/d6back.yaml
Normal file
112
api/PM7/d6back.yaml
Normal file
@@ -0,0 +1,112 @@
|
||||
# Configuration for MariaDB and directories backup
|
||||
# Backup structure: $dir_backup/$hostname/$containername/ for dirs
|
||||
# $dir_backup/$hostname/$containername/sql/ for databases
|
||||
|
||||
# Global parameters
|
||||
global:
|
||||
backup_server: PM7 # Nom du serveur de backup (PM7, PM1, etc.)
|
||||
email_to: support@unikoffice.com # Email de notification
|
||||
dir_backup: /var/pierre/back # Base backup directory
|
||||
enc_key: /home/pierre/.key_enc # Encryption key for SQL backups
|
||||
keep_dirs: 7d # Garde 7 jours pour les dirs
|
||||
keep_db: 5d,3w,15m # 5 jours complets, 3 semaines (1/jour), 15 mois (1/semaine)
|
||||
|
||||
# Hosts configuration
|
||||
hosts:
|
||||
- name: IN2
|
||||
ip: 145.239.9.105
|
||||
user: debian
|
||||
key: /home/pierre/.ssh/backup_key
|
||||
port: 22
|
||||
dirs:
|
||||
- /etc/nginx
|
||||
containers:
|
||||
- name: nx4
|
||||
db_user: root
|
||||
db_pass: MyDebServer,90b
|
||||
db_host: localhost
|
||||
dirs:
|
||||
- /etc/nginx
|
||||
- /var/www
|
||||
databases:
|
||||
- ALL # Backup all databases
|
||||
onlydb: # Used only with -onlydb parameter (optional)
|
||||
- turing
|
||||
|
||||
- name: IN3
|
||||
ip: 195.154.80.116
|
||||
user: pierre
|
||||
key: /home/pierre/.ssh/backup_key
|
||||
port: 22
|
||||
dirs:
|
||||
- /etc/nginx
|
||||
containers:
|
||||
- name: nx4
|
||||
db_user: root
|
||||
db_pass: MyAlpLocal,90b
|
||||
db_host: localhost
|
||||
dirs:
|
||||
- /etc/nginx
|
||||
- /var/www
|
||||
databases:
|
||||
- ALL # Backup all databases
|
||||
onlydb: # Used only with -onlydb parameter (optional)
|
||||
- geosector
|
||||
|
||||
- name: rca-geo
|
||||
dirs:
|
||||
- /etc/nginx
|
||||
- /var/www
|
||||
|
||||
- name: dva-res
|
||||
db_user: root
|
||||
db_pass: MyAlpineDb.90b
|
||||
db_host: localhost
|
||||
dirs:
|
||||
- /etc/nginx
|
||||
- /var/www
|
||||
databases:
|
||||
- ALL
|
||||
onlydb:
|
||||
- resalice
|
||||
|
||||
- name: dva-front
|
||||
dirs:
|
||||
- /etc/nginx
|
||||
- /var/www
|
||||
|
||||
- name: maria3
|
||||
db_user: root
|
||||
db_pass: MyAlpLocal,90b
|
||||
db_host: localhost
|
||||
dirs:
|
||||
- /etc/my.cnf.d
|
||||
- /var/osm
|
||||
- /var/log
|
||||
databases:
|
||||
- ALL
|
||||
onlydb:
|
||||
- cleo
|
||||
- rca_geo
|
||||
|
||||
- name: IN4
|
||||
ip: 51.159.7.190
|
||||
user: pierre
|
||||
key: /home/pierre/.ssh/backup_key
|
||||
port: 22
|
||||
dirs:
|
||||
- /etc/nginx
|
||||
containers:
|
||||
- name: maria4
|
||||
db_user: root
|
||||
db_pass: MyAlpLocal,90b
|
||||
db_host: localhost
|
||||
dirs:
|
||||
- /etc/my.cnf.d
|
||||
- /var/osm
|
||||
- /var/log
|
||||
databases:
|
||||
- ALL
|
||||
onlydb:
|
||||
- cleo
|
||||
- pra_geo
|
||||
118
api/PM7/decpm7.sh
Normal file
118
api/PM7/decpm7.sh
Normal file
@@ -0,0 +1,118 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
# Colors for output
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
BLUE='\033[0;34m'
|
||||
NC='\033[0m' # No Color
|
||||
|
||||
# Configuration
|
||||
CONFIG_FILE="backpm7.yaml"
|
||||
|
||||
# Check if file argument is provided
|
||||
if [ $# -eq 0 ]; then
|
||||
echo -e "${RED}Error: No input file specified${NC}"
|
||||
echo "Usage: $0 <database.sql.gz.enc>"
|
||||
echo "Example: $0 wordpress_20250905_14.sql.gz.enc"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
INPUT_FILE="$1"
|
||||
|
||||
# Check if input file exists
|
||||
if [ ! -f "$INPUT_FILE" ]; then
|
||||
echo -e "${RED}Error: File not found: $INPUT_FILE${NC}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Function to load encryption key from config
|
||||
load_key_from_config() {
|
||||
if [ ! -f "$CONFIG_FILE" ]; then
|
||||
echo -e "${YELLOW}Warning: $CONFIG_FILE not found${NC}"
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Check for yq
|
||||
if ! command -v yq &> /dev/null; then
|
||||
echo -e "${RED}Error: yq is required to read config file${NC}"
|
||||
echo "Install with: sudo wget -qO /usr/local/bin/yq https://github.com/mikefarah/yq/releases/latest/download/yq_linux_amd64 && sudo chmod +x /usr/local/bin/yq"
|
||||
return 1
|
||||
fi
|
||||
|
||||
local key_path=$(yq '.global.enc_key' "$CONFIG_FILE" | tr -d '"')
|
||||
|
||||
if [ -z "$key_path" ]; then
|
||||
echo -e "${RED}Error: enc_key not found in $CONFIG_FILE${NC}"
|
||||
return 1
|
||||
fi
|
||||
|
||||
if [ ! -f "$key_path" ]; then
|
||||
echo -e "${RED}Error: Encryption key file not found: $key_path${NC}"
|
||||
return 1
|
||||
fi
|
||||
|
||||
ENC_KEY=$(cat "$key_path")
|
||||
echo -e "${GREEN}Encryption key loaded from: $key_path${NC}"
|
||||
return 0
|
||||
}
|
||||
|
||||
# Check file type early - accept both old and new naming
|
||||
if [[ "$INPUT_FILE" != *.sql.gz.enc ]] && [[ "$INPUT_FILE" != *.sql.tar.gz.enc ]]; then
|
||||
echo -e "${RED}Error: File must be a .sql.gz.enc or .sql.tar.gz.enc file${NC}"
|
||||
echo "This tool only decrypts SQL backup files created by backpm7.sh"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Get encryption key from config
|
||||
if ! load_key_from_config; then
|
||||
echo -e "${RED}Error: Cannot load encryption key${NC}"
|
||||
echo "Make sure $CONFIG_FILE exists and contains enc_key path"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Process SQL backup file
|
||||
echo -e "${BLUE}Decrypting SQL backup: $INPUT_FILE${NC}"
|
||||
|
||||
# Determine output file - extract just the filename and put in current directory
|
||||
BASENAME=$(basename "$INPUT_FILE")
|
||||
if [[ "$BASENAME" == *.sql.tar.gz.enc ]]; then
|
||||
OUTPUT_FILE="${BASENAME%.sql.tar.gz.enc}.sql"
|
||||
else
|
||||
OUTPUT_FILE="${BASENAME%.sql.gz.enc}.sql"
|
||||
fi
|
||||
|
||||
# Decrypt and decompress in one command
|
||||
echo "Decrypting to: $OUTPUT_FILE"
|
||||
|
||||
# Decrypt and decompress in one pipeline
|
||||
if openssl enc -aes-256-cbc -d -salt -pass pass:"$ENC_KEY" -pbkdf2 -in "$INPUT_FILE" | gunzip > "$OUTPUT_FILE" 2>/dev/null; then
|
||||
# Get file size
|
||||
size=$(du -h "$OUTPUT_FILE" | cut -f1)
|
||||
echo -e "${GREEN}✓ Successfully decrypted: $OUTPUT_FILE ($size)${NC}"
|
||||
|
||||
# Show first few lines of SQL
|
||||
echo -e "${BLUE}First 5 lines of SQL:${NC}"
|
||||
head -n 5 "$OUTPUT_FILE"
|
||||
else
|
||||
echo -e "${RED}✗ Decryption failed${NC}"
|
||||
echo "Possible causes:"
|
||||
echo " - Wrong encryption key"
|
||||
echo " - Corrupted file"
|
||||
echo " - File was encrypted differently"
|
||||
|
||||
# Try to help debug
|
||||
echo -e "\n${YELLOW}Debug info:${NC}"
|
||||
echo "File size: $(du -h "$INPUT_FILE" | cut -f1)"
|
||||
echo "First bytes (should start with 'Salted__'):"
|
||||
hexdump -C "$INPUT_FILE" | head -n 1
|
||||
|
||||
# Let's also check what key we're using (first 10 chars)
|
||||
echo "Key begins with: ${ENC_KEY:0:10}..."
|
||||
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo -e "${GREEN}Operation completed successfully${NC}"
|
||||
248
api/PM7/sync_geosector.sh
Normal file
248
api/PM7/sync_geosector.sh
Normal file
@@ -0,0 +1,248 @@
|
||||
#!/bin/bash
|
||||
#
|
||||
# sync_geosector.sh - Synchronise les backups geosector depuis PM7 vers maria3 (IN3) et maria4 (IN4)
|
||||
#
|
||||
# Ce script :
|
||||
# 1. Trouve le dernier backup chiffré de geosector sur PM7
|
||||
# 2. Le déchiffre et décompresse localement
|
||||
# 3. Le transfère et l'importe dans IN3/maria3/geosector
|
||||
# 4. Le transfère et l'importe dans IN4/maria4/geosector
|
||||
#
|
||||
# Installation: /var/pierre/bat/sync_geosector.sh
|
||||
# Usage: ./sync_geosector.sh [--force] [--date YYYYMMDD_HH]
|
||||
#
|
||||
|
||||
set -uo pipefail
|
||||
# Note: Removed -e to allow script to continue on sync errors
|
||||
# Errors are handled explicitly with ERROR_COUNT
|
||||
|
||||
# Configuration
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
CONFIG_FILE="$SCRIPT_DIR/d6back.yaml"
|
||||
BACKUP_DIR="/var/pierre/back/IN3/nx4/sql"
|
||||
ENC_KEY_FILE="/home/pierre/.key_enc"
|
||||
SSH_KEY="/home/pierre/.ssh/backup_key"
|
||||
TEMP_DIR="/tmp/geosector_sync"
|
||||
LOG_FILE="/var/pierre/bat/logs/sync_geosector.log"
|
||||
RECAP_FILE="/tmp/sync_geosector_recap_$$.txt"
|
||||
|
||||
# Load email config from d6back.yaml
|
||||
if [[ -f "$CONFIG_FILE" ]]; then
|
||||
EMAIL_TO=$(yq '.global.email_to // "support@unikoffice.com"' "$CONFIG_FILE" | tr -d '"')
|
||||
BACKUP_SERVER=$(yq '.global.backup_server // "BACKUP"' "$CONFIG_FILE" | tr -d '"')
|
||||
else
|
||||
EMAIL_TO="support@unikoffice.com"
|
||||
BACKUP_SERVER="BACKUP"
|
||||
fi
|
||||
|
||||
# Serveurs cibles
|
||||
IN3_HOST="195.154.80.116"
|
||||
IN3_USER="pierre"
|
||||
IN3_CONTAINER="maria3"
|
||||
|
||||
IN4_HOST="51.159.7.190"
|
||||
IN4_USER="pierre"
|
||||
IN4_CONTAINER="maria4"
|
||||
|
||||
# Credentials MariaDB
|
||||
DB_USER="root"
|
||||
IN3_DB_PASS="MyAlpLocal,90b" # maria3
|
||||
IN4_DB_PASS="MyAlpLocal,90b" # maria4
|
||||
DB_NAME="geosector"
|
||||
|
||||
# Fonctions utilitaires
|
||||
log() {
|
||||
echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*" | tee -a "$LOG_FILE"
|
||||
}
|
||||
|
||||
error() {
|
||||
log "ERROR: $*"
|
||||
exit 1
|
||||
}
|
||||
|
||||
cleanup() {
|
||||
if [[ -d "$TEMP_DIR" ]]; then
|
||||
log "Nettoyage de $TEMP_DIR"
|
||||
rm -rf "$TEMP_DIR"
|
||||
fi
|
||||
rm -f "$RECAP_FILE"
|
||||
}
|
||||
|
||||
trap cleanup EXIT
|
||||
|
||||
# Lecture de la clé de chiffrement
|
||||
if [[ ! -f "$ENC_KEY_FILE" ]]; then
|
||||
error "Clé de chiffrement non trouvée: $ENC_KEY_FILE"
|
||||
fi
|
||||
ENC_KEY=$(cat "$ENC_KEY_FILE")
|
||||
|
||||
# Parsing des arguments
|
||||
FORCE=0
|
||||
SPECIFIC_DATE=""
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case $1 in
|
||||
--force)
|
||||
FORCE=1
|
||||
shift
|
||||
;;
|
||||
--date)
|
||||
SPECIFIC_DATE="$2"
|
||||
shift 2
|
||||
;;
|
||||
*)
|
||||
echo "Usage: $0 [--force] [--date YYYYMMDD_HH]"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
# Trouver le fichier backup
|
||||
if [[ -n "$SPECIFIC_DATE" ]]; then
|
||||
BACKUP_FILE="$BACKUP_DIR/geosector_${SPECIFIC_DATE}.sql.gz.enc"
|
||||
if [[ ! -f "$BACKUP_FILE" ]]; then
|
||||
error "Backup non trouvé: $BACKUP_FILE"
|
||||
fi
|
||||
else
|
||||
# Chercher le plus récent
|
||||
BACKUP_FILE=$(find "$BACKUP_DIR" -name "geosector_*.sql.gz.enc" -type f -printf '%T@ %p\n' | sort -rn | head -1 | cut -d' ' -f2-)
|
||||
if [[ -z "$BACKUP_FILE" ]]; then
|
||||
error "Aucun backup geosector trouvé dans $BACKUP_DIR"
|
||||
fi
|
||||
fi
|
||||
|
||||
BACKUP_BASENAME=$(basename "$BACKUP_FILE")
|
||||
log "Backup sélectionné: $BACKUP_BASENAME"
|
||||
|
||||
# Initialiser le fichier récapitulatif
|
||||
echo "SYNC GEOSECTOR REPORT - $(hostname) - $(date '+%d.%m.%Y %H')h" > "$RECAP_FILE"
|
||||
echo "========================================" >> "$RECAP_FILE"
|
||||
echo "" >> "$RECAP_FILE"
|
||||
echo "Backup source: $BACKUP_BASENAME" >> "$RECAP_FILE"
|
||||
echo "" >> "$RECAP_FILE"
|
||||
|
||||
# Créer le répertoire temporaire
|
||||
mkdir -p "$TEMP_DIR"
|
||||
DECRYPTED_FILE="$TEMP_DIR/geosector.sql"
|
||||
|
||||
# Étape 1: Déchiffrer et décompresser
|
||||
log "Déchiffrement et décompression du backup..."
|
||||
if ! openssl enc -aes-256-cbc -d -pass pass:"$ENC_KEY" -pbkdf2 -in "$BACKUP_FILE" | gunzip > "$DECRYPTED_FILE"; then
|
||||
error "Échec du déchiffrement/décompression"
|
||||
fi
|
||||
|
||||
FILE_SIZE=$(du -h "$DECRYPTED_FILE" | cut -f1)
|
||||
log "Fichier SQL déchiffré: $FILE_SIZE"
|
||||
|
||||
echo "Decrypted SQL size: $FILE_SIZE" >> "$RECAP_FILE"
|
||||
echo "" >> "$RECAP_FILE"
|
||||
|
||||
# Compteur d'erreurs
|
||||
ERROR_COUNT=0
|
||||
|
||||
# Fonction pour synchroniser vers un serveur
|
||||
sync_to_server() {
|
||||
local HOST=$1
|
||||
local USER=$2
|
||||
local CONTAINER=$3
|
||||
local DB_PASS=$4
|
||||
local SERVER_NAME=$5
|
||||
|
||||
log "=== Synchronisation vers $SERVER_NAME ($HOST) ==="
|
||||
echo "TARGET: $SERVER_NAME ($HOST/$CONTAINER)" >> "$RECAP_FILE"
|
||||
|
||||
# Test de connexion SSH
|
||||
if ! ssh -i "$SSH_KEY" -o ConnectTimeout=10 "$USER@$HOST" "echo 'SSH OK'" &>/dev/null; then
|
||||
log "ERROR: Impossible de se connecter à $HOST via SSH"
|
||||
echo " ✗ SSH connection FAILED" >> "$RECAP_FILE"
|
||||
((ERROR_COUNT++))
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Import dans MariaDB
|
||||
log "Import dans $SERVER_NAME/$CONTAINER/geosector..."
|
||||
|
||||
# Drop et recréer la base sur le serveur distant
|
||||
if ! ssh -i "$SSH_KEY" "$USER@$HOST" "incus exec $CONTAINER --project default -- mariadb -u root -p'$DB_PASS' -e 'DROP DATABASE IF EXISTS $DB_NAME; CREATE DATABASE $DB_NAME CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;'"; then
|
||||
log "ERROR: Échec de la création de la base sur $SERVER_NAME"
|
||||
echo " ✗ Database creation FAILED" >> "$RECAP_FILE"
|
||||
((ERROR_COUNT++))
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Filtrer et importer le SQL (enlever CREATE DATABASE et USE avec timestamp)
|
||||
log "Filtrage et import du SQL..."
|
||||
if ! sed -e '/^CREATE DATABASE.*geosector_[0-9]/d' \
|
||||
-e '/^USE.*geosector_[0-9]/d' \
|
||||
"$DECRYPTED_FILE" | \
|
||||
ssh -i "$SSH_KEY" "$USER@$HOST" "incus exec $CONTAINER --project default -- mariadb -u root -p'$DB_PASS' $DB_NAME"; then
|
||||
log "ERROR: Échec de l'import sur $SERVER_NAME"
|
||||
echo " ✗ SQL import FAILED" >> "$RECAP_FILE"
|
||||
((ERROR_COUNT++))
|
||||
return 1
|
||||
fi
|
||||
|
||||
log "$SERVER_NAME: Import réussi"
|
||||
echo " ✓ Import SUCCESS" >> "$RECAP_FILE"
|
||||
echo "" >> "$RECAP_FILE"
|
||||
}
|
||||
|
||||
# Synchronisation vers IN3/maria3
|
||||
sync_to_server "$IN3_HOST" "$IN3_USER" "$IN3_CONTAINER" "$IN3_DB_PASS" "IN3/maria3"
|
||||
|
||||
# Synchronisation vers IN4/maria4
|
||||
sync_to_server "$IN4_HOST" "$IN4_USER" "$IN4_CONTAINER" "$IN4_DB_PASS" "IN4/maria4"
|
||||
|
||||
# Finaliser le récapitulatif
|
||||
echo "========================================" >> "$RECAP_FILE"
|
||||
echo "COMPLETED: $(date '+%d.%m.%Y %H:%M')" >> "$RECAP_FILE"
|
||||
|
||||
# Préparer le sujet email avec date
|
||||
DATE_SUBJECT=$(date '+%d.%m.%Y %H')
|
||||
|
||||
# Envoyer l'email récapitulatif
|
||||
if [[ $ERROR_COUNT -gt 0 ]]; then
|
||||
log "Total errors: $ERROR_COUNT"
|
||||
|
||||
# Ajouter les erreurs au récap
|
||||
echo "" >> "$RECAP_FILE"
|
||||
echo "ERRORS DETECTED: $ERROR_COUNT" >> "$RECAP_FILE"
|
||||
echo "----------------------------" >> "$RECAP_FILE"
|
||||
grep -i "ERROR" "$LOG_FILE" | tail -20 >> "$RECAP_FILE"
|
||||
|
||||
# Envoyer email avec ERROR dans le sujet
|
||||
log "Sending ERROR email to $EMAIL_TO (Errors found: $ERROR_COUNT)"
|
||||
if command -v msmtp &> /dev/null; then
|
||||
{
|
||||
echo "To: $EMAIL_TO"
|
||||
echo "Subject: Sync${BACKUP_SERVER} ERROR $DATE_SUBJECT"
|
||||
echo ""
|
||||
cat "$RECAP_FILE"
|
||||
} | msmtp "$EMAIL_TO"
|
||||
log "ERROR email sent successfully to $EMAIL_TO"
|
||||
else
|
||||
log "WARNING: msmtp not found - ERROR email NOT sent"
|
||||
fi
|
||||
|
||||
log "=== Synchronisation terminée avec des erreurs ==="
|
||||
exit 1
|
||||
else
|
||||
log "=== Synchronisation terminée avec succès ==="
|
||||
log "Les bases geosector sur maria3 et maria4 sont à jour avec le backup $BACKUP_BASENAME"
|
||||
|
||||
# Envoyer email de succès
|
||||
log "Sending SUCCESS recap email to $EMAIL_TO"
|
||||
if command -v msmtp &> /dev/null; then
|
||||
{
|
||||
echo "To: $EMAIL_TO"
|
||||
echo "Subject: Sync${BACKUP_SERVER} $DATE_SUBJECT"
|
||||
echo ""
|
||||
cat "$RECAP_FILE"
|
||||
} | msmtp "$EMAIL_TO"
|
||||
log "SUCCESS recap email sent successfully to $EMAIL_TO"
|
||||
else
|
||||
log "WARNING: msmtp not found - SUCCESS recap email NOT sent"
|
||||
fi
|
||||
|
||||
exit 0
|
||||
fi
|
||||
2701
api/TODO-API.md
Normal file
2701
api/TODO-API.md
Normal file
File diff suppressed because it is too large
Load Diff
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
20
api/composer.json
Normal file → Executable file
20
api/composer.json
Normal file → Executable file
@@ -3,16 +3,26 @@
|
||||
"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": "^5.0",
|
||||
"setasign/fpdf": "^1.8",
|
||||
"setasign/fpdi": "^2.6",
|
||||
"stripe/stripe-php": "^17.6"
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"App\\": "src/"
|
||||
}
|
||||
},
|
||||
"classmap": [
|
||||
"src/Core/",
|
||||
"src/Config/",
|
||||
"src/Utils/",
|
||||
"src/Controllers/LogController.php"
|
||||
]
|
||||
},
|
||||
"config": {
|
||||
"sort-packages": true,
|
||||
|
||||
783
api/composer.lock
generated
Normal file → Executable file
783
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": "936a7e1a35fde56354a4dea02b309267",
|
||||
"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.2.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/maennchen/ZipStream-PHP.git",
|
||||
"reference": "9712d8fa4cdf9240380b01eb4be55ad8dcf71416"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/maennchen/ZipStream-PHP/zipball/9712d8fa4cdf9240380b01eb4be55ad8dcf71416",
|
||||
"reference": "9712d8fa4cdf9240380b01eb4be55ad8dcf71416",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"ext-mbstring": "*",
|
||||
"ext-zlib": "*",
|
||||
"php-64bit": "^8.3"
|
||||
},
|
||||
"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": "^12.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.2.0"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
"url": "https://github.com/maennchen",
|
||||
"type": "github"
|
||||
}
|
||||
],
|
||||
"time": "2025-07-17T11:15:13+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.11.1",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/PHPMailer/PHPMailer.git",
|
||||
"reference": "d9e3b36b47f04b497a0164c5a20f92acb4593284"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/PHPMailer/PHPMailer/zipball/d9e3b36b47f04b497a0164c5a20f92acb4593284",
|
||||
"reference": "d9e3b36b47f04b497a0164c5a20f92acb4593284",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -38,6 +302,7 @@
|
||||
},
|
||||
"suggest": {
|
||||
"decomplexity/SendOauth2": "Adapter for using XOAUTH2 authentication",
|
||||
"ext-imap": "Needed to support advanced email address parsing according to RFC822",
|
||||
"ext-mbstring": "Needed to send email in multibyte encoding charset or decode encoded addresses",
|
||||
"ext-openssl": "Needed for secure SMTP sending and DKIM signing",
|
||||
"greew/oauth2-azure-provider": "Needed for Microsoft Azure XOAUTH2 authentication",
|
||||
@@ -77,7 +342,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.11.1"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
@@ -85,7 +350,501 @@
|
||||
"type": "github"
|
||||
}
|
||||
],
|
||||
"time": "2024-11-24T18:04:13+00:00"
|
||||
"time": "2025-09-30T11:54:53+00:00"
|
||||
},
|
||||
{
|
||||
"name": "phpoffice/phpspreadsheet",
|
||||
"version": "5.1.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/PHPOffice/PhpSpreadsheet.git",
|
||||
"reference": "fd26e45a814e94ae2aad0df757d9d1739c4bf2e0"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/PHPOffice/PhpSpreadsheet/zipball/fd26e45a814e94ae2aad0df757d9d1739c4bf2e0",
|
||||
"reference": "fd26e45a814e94ae2aad0df757d9d1739c4bf2e0",
|
||||
"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 || ^2.0",
|
||||
"phpstan/phpstan-deprecation-rules": "^1.0 || ^2.0",
|
||||
"phpstan/phpstan-phpunit": "^1.0 || ^2.0",
|
||||
"phpunit/phpunit": "^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/5.1.0"
|
||||
},
|
||||
"time": "2025-09-04T05:34:49+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": "setasign/fpdi",
|
||||
"version": "v2.6.4",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/Setasign/FPDI.git",
|
||||
"reference": "4b53852fde2734ec6a07e458a085db627c60eada"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/Setasign/FPDI/zipball/4b53852fde2734ec6a07e458a085db627c60eada",
|
||||
"reference": "4b53852fde2734ec6a07e458a085db627c60eada",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"ext-zlib": "*",
|
||||
"php": "^7.1 || ^8.0"
|
||||
},
|
||||
"conflict": {
|
||||
"setasign/tfpdf": "<1.31"
|
||||
},
|
||||
"require-dev": {
|
||||
"phpunit/phpunit": "^7",
|
||||
"setasign/fpdf": "~1.8.6",
|
||||
"setasign/tfpdf": "~1.33",
|
||||
"squizlabs/php_codesniffer": "^3.5",
|
||||
"tecnickcom/tcpdf": "^6.8"
|
||||
},
|
||||
"suggest": {
|
||||
"setasign/fpdf": "FPDI will extend this class but as it is also possible to use TCPDF or tFPDF as an alternative. There's no fixed dependency configured."
|
||||
},
|
||||
"type": "library",
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"setasign\\Fpdi\\": "src/"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Jan Slabon",
|
||||
"email": "jan.slabon@setasign.com",
|
||||
"homepage": "https://www.setasign.com"
|
||||
},
|
||||
{
|
||||
"name": "Maximilian Kresse",
|
||||
"email": "maximilian.kresse@setasign.com",
|
||||
"homepage": "https://www.setasign.com"
|
||||
}
|
||||
],
|
||||
"description": "FPDI is a collection of PHP classes facilitating developers to read pages from existing PDF documents and use them as templates in FPDF. Because it is also possible to use FPDI with TCPDF, there are no fixed dependencies defined. Please see suggestions for packages which evaluates the dependencies automatically.",
|
||||
"homepage": "https://www.setasign.com/fpdi",
|
||||
"keywords": [
|
||||
"fpdf",
|
||||
"fpdi",
|
||||
"pdf"
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/Setasign/FPDI/issues",
|
||||
"source": "https://github.com/Setasign/FPDI/tree/v2.6.4"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
"url": "https://tidelift.com/funding/github/packagist/setasign/fpdi",
|
||||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2025-08-05T09:57:14+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 +854,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.
200
api/config/nginx/pra-geo-http-only.conf
Normal file
200
api/config/nginx/pra-geo-http-only.conf
Normal file
@@ -0,0 +1,200 @@
|
||||
# =============================================================================
|
||||
# Configuration NGINX PRODUCTION pour pra-geo (IN4)
|
||||
# Date: 2025-10-07
|
||||
# Environnement: PRODUCTION
|
||||
# Server: Container pra-geo (13.23.34.43)
|
||||
# Port: 80 uniquement (HTTP)
|
||||
# SSL/HTTPS: Géré par le reverse proxy NGINX sur le host IN4
|
||||
# =============================================================================
|
||||
|
||||
# Site principal (web statique)
|
||||
server {
|
||||
listen 80;
|
||||
server_name geosector.fr;
|
||||
|
||||
root /var/www/geosector/web;
|
||||
index index.html;
|
||||
|
||||
# Logs PRODUCTION
|
||||
access_log /var/log/nginx/geosector-web_access.log combined;
|
||||
error_log /var/log/nginx/geosector-web_error.log warn;
|
||||
|
||||
location / {
|
||||
try_files $uri $uri/ /index.html;
|
||||
}
|
||||
|
||||
# Assets statiques avec cache agressif
|
||||
location ~* \.(jpg|jpeg|png|gif|ico|svg|webp|css|js|woff|woff2|ttf|eot)$ {
|
||||
expires 1y;
|
||||
add_header Cache-Control "public, immutable";
|
||||
access_log off;
|
||||
}
|
||||
|
||||
# Protection des fichiers sensibles
|
||||
location ~ /\.(?!well-known) {
|
||||
deny all;
|
||||
access_log off;
|
||||
log_not_found off;
|
||||
}
|
||||
}
|
||||
|
||||
# =============================================================================
|
||||
# APPLICATION FLUTTER + API PHP
|
||||
# =============================================================================
|
||||
|
||||
server {
|
||||
listen 80;
|
||||
server_name app3.geosector.fr;
|
||||
|
||||
# Logs PRODUCTION
|
||||
access_log /var/log/nginx/pra-app_access.log combined;
|
||||
error_log /var/log/nginx/pra-app_error.log warn;
|
||||
|
||||
# Récupérer le vrai IP du client depuis le reverse proxy
|
||||
set_real_ip_from 13.23.34.0/24; # Réseau Incus
|
||||
set_real_ip_from 51.159.7.190; # IP publique IN4
|
||||
real_ip_header X-Forwarded-For;
|
||||
real_ip_recursive on;
|
||||
|
||||
# Taille maximale des uploads (pour les logos, exports, etc.)
|
||||
client_max_body_size 10M;
|
||||
client_body_buffer_size 128k;
|
||||
|
||||
# Timeouts optimisés pour PRODUCTION
|
||||
client_body_timeout 30s;
|
||||
client_header_timeout 30s;
|
||||
send_timeout 60s;
|
||||
|
||||
# =============================================================================
|
||||
# APPLICATION FLUTTER (contenu statique)
|
||||
# =============================================================================
|
||||
location / {
|
||||
root /var/www/geosector/app;
|
||||
index index.html;
|
||||
try_files $uri $uri/ /index.html;
|
||||
|
||||
# Cache intelligent pour PRODUCTION
|
||||
# HTML : pas de cache (pour déploiements)
|
||||
location ~* \.html$ {
|
||||
expires -1;
|
||||
add_header Cache-Control "no-cache, no-store, must-revalidate";
|
||||
}
|
||||
|
||||
# Assets Flutter (JS, CSS, fonts) avec hash : cache agressif
|
||||
location ~* \.(js|css|woff|woff2|ttf|eot)$ {
|
||||
expires 1y;
|
||||
add_header Cache-Control "public, immutable";
|
||||
access_log off;
|
||||
}
|
||||
|
||||
# Images : cache longue durée
|
||||
location ~* \.(jpg|jpeg|png|gif|ico|svg|webp)$ {
|
||||
expires 30d;
|
||||
add_header Cache-Control "public";
|
||||
access_log off;
|
||||
}
|
||||
}
|
||||
|
||||
# =============================================================================
|
||||
# API PHP (RESTful)
|
||||
# =============================================================================
|
||||
location /api/ {
|
||||
root /var/www/geosector;
|
||||
|
||||
# CORS - Le reverse proxy IN4 ajoute déjà les headers CORS
|
||||
# On les ajoute ici pour les requêtes internes si besoin
|
||||
|
||||
# Cache API : pas de cache (données dynamiques)
|
||||
add_header Cache-Control "no-store, no-cache, must-revalidate" always;
|
||||
add_header Pragma "no-cache" always;
|
||||
add_header Expires "0" always;
|
||||
|
||||
# Rewrite vers index.php
|
||||
try_files $uri $uri/ /api/index.php$is_args$args;
|
||||
|
||||
# Traitement PHP
|
||||
location ~ ^/api/(.+\.php)$ {
|
||||
root /var/www/geosector;
|
||||
|
||||
# FastCGI PHP-FPM
|
||||
fastcgi_pass unix:/run/php-fpm83/php-fpm.sock;
|
||||
fastcgi_index index.php;
|
||||
include fastcgi_params;
|
||||
fastcgi_param SCRIPT_FILENAME $request_filename;
|
||||
|
||||
# Variable d'environnement PRODUCTION
|
||||
fastcgi_param APP_ENV "production";
|
||||
fastcgi_param SERVER_NAME "app3.geosector.fr";
|
||||
|
||||
# Headers transmis à PHP (viennent du reverse proxy)
|
||||
fastcgi_param HTTP_X_REAL_IP $http_x_real_ip;
|
||||
fastcgi_param HTTP_X_FORWARDED_FOR $http_x_forwarded_for;
|
||||
fastcgi_param HTTP_X_FORWARDED_PROTO $http_x_forwarded_proto;
|
||||
fastcgi_param HTTPS $http_x_forwarded_proto;
|
||||
|
||||
# Timeouts pour opérations longues (sync, exports)
|
||||
fastcgi_read_timeout 300;
|
||||
fastcgi_send_timeout 300;
|
||||
fastcgi_connect_timeout 60;
|
||||
|
||||
# Buffers optimisés
|
||||
fastcgi_buffer_size 128k;
|
||||
fastcgi_buffers 256 16k;
|
||||
fastcgi_busy_buffers_size 256k;
|
||||
fastcgi_temp_file_write_size 256k;
|
||||
}
|
||||
}
|
||||
|
||||
# =============================================================================
|
||||
# UPLOADS ET MÉDIAS
|
||||
# =============================================================================
|
||||
location /api/uploads/ {
|
||||
alias /var/www/geosector/api/uploads/;
|
||||
|
||||
# Cache pour les médias uploadés
|
||||
expires 7d;
|
||||
add_header Cache-Control "public";
|
||||
|
||||
# Sécurité : empêcher l'exécution de scripts
|
||||
location ~ \.(php|phtml|php3|php4|php5|phps)$ {
|
||||
deny all;
|
||||
}
|
||||
}
|
||||
|
||||
# =============================================================================
|
||||
# SÉCURITÉ
|
||||
# =============================================================================
|
||||
|
||||
# Bloquer l'accès aux fichiers sensibles
|
||||
location ~ /\.(?!well-known) {
|
||||
deny all;
|
||||
access_log off;
|
||||
log_not_found off;
|
||||
}
|
||||
|
||||
# Bloquer l'accès aux fichiers de configuration
|
||||
location ~* \.(env|sql|bak|backup|swp|config|conf|ini|log)$ {
|
||||
deny all;
|
||||
access_log off;
|
||||
log_not_found off;
|
||||
}
|
||||
|
||||
# Protection contre les requêtes invalides
|
||||
if ($request_method !~ ^(GET|HEAD|POST|PUT|DELETE|PATCH|OPTIONS)$) {
|
||||
return 405;
|
||||
}
|
||||
|
||||
# =============================================================================
|
||||
# MONITORING
|
||||
# =============================================================================
|
||||
|
||||
# Endpoint de health check (accessible en interne)
|
||||
location = /nginx-health {
|
||||
access_log off;
|
||||
allow 127.0.0.1;
|
||||
allow 13.23.34.0/24; # Réseau interne Incus
|
||||
deny all;
|
||||
return 200 "healthy\n";
|
||||
add_header Content-Type text/plain;
|
||||
}
|
||||
}
|
||||
290
api/config/nginx/pra-geo-production.conf
Normal file
290
api/config/nginx/pra-geo-production.conf
Normal file
@@ -0,0 +1,290 @@
|
||||
# =============================================================================
|
||||
# Configuration NGINX PRODUCTION pour pra-geo (IN4)
|
||||
# Date: 2025-10-07
|
||||
# Environnement: PRODUCTION
|
||||
# Server: IN4 (51.159.7.190)
|
||||
# =============================================================================
|
||||
|
||||
# Site principal (redirection vers www ou app)
|
||||
server {
|
||||
listen 80;
|
||||
server_name geosector.fr;
|
||||
|
||||
# Redirection permanente vers HTTPS
|
||||
return 301 https://$server_name$request_uri;
|
||||
}
|
||||
|
||||
server {
|
||||
listen 443 ssl http2;
|
||||
server_name geosector.fr;
|
||||
|
||||
# Certificats SSL
|
||||
ssl_certificate /etc/letsencrypt/live/geosector.fr/fullchain.pem;
|
||||
ssl_certificate_key /etc/letsencrypt/live/geosector.fr/privkey.pem;
|
||||
|
||||
# Configuration SSL optimisée
|
||||
ssl_protocols TLSv1.2 TLSv1.3;
|
||||
ssl_ciphers 'ECDHE-RSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-CHACHA20-POLY1305';
|
||||
ssl_prefer_server_ciphers on;
|
||||
ssl_session_cache shared:SSL:10m;
|
||||
ssl_session_timeout 10m;
|
||||
ssl_stapling on;
|
||||
ssl_stapling_verify on;
|
||||
|
||||
root /var/www/geosector/web;
|
||||
index index.html;
|
||||
|
||||
# Logs PRODUCTION
|
||||
access_log /var/log/nginx/geosector-web_access.log combined;
|
||||
error_log /var/log/nginx/geosector-web_error.log warn;
|
||||
|
||||
# Headers de sécurité
|
||||
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always;
|
||||
add_header X-Frame-Options "SAMEORIGIN" always;
|
||||
add_header X-Content-Type-Options "nosniff" always;
|
||||
add_header X-XSS-Protection "1; mode=block" always;
|
||||
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
|
||||
|
||||
location / {
|
||||
try_files $uri $uri/ /index.html;
|
||||
}
|
||||
|
||||
# Assets statiques avec cache agressif
|
||||
location ~* \.(jpg|jpeg|png|gif|ico|svg|webp|css|js|woff|woff2|ttf|eot)$ {
|
||||
expires 1y;
|
||||
add_header Cache-Control "public, immutable";
|
||||
access_log off;
|
||||
}
|
||||
|
||||
# Protection des fichiers sensibles
|
||||
location ~ /\.(?!well-known) {
|
||||
deny all;
|
||||
access_log off;
|
||||
log_not_found off;
|
||||
}
|
||||
}
|
||||
|
||||
# =============================================================================
|
||||
# APPLICATION FLUTTER + API PHP
|
||||
# =============================================================================
|
||||
|
||||
# Redirection HTTP → HTTPS
|
||||
server {
|
||||
listen 80;
|
||||
server_name app3.geosector.fr;
|
||||
|
||||
# Permettre Let's Encrypt validation
|
||||
location ^~ /.well-known/acme-challenge/ {
|
||||
root /var/www/letsencrypt;
|
||||
allow all;
|
||||
}
|
||||
|
||||
# Redirection permanente vers HTTPS
|
||||
return 301 https://$server_name$request_uri;
|
||||
}
|
||||
|
||||
server {
|
||||
listen 443 ssl http2;
|
||||
server_name app3.geosector.fr;
|
||||
|
||||
# Certificats SSL
|
||||
ssl_certificate /etc/letsencrypt/live/app3.geosector.fr/fullchain.pem;
|
||||
ssl_certificate_key /etc/letsencrypt/live/app3.geosector.fr/privkey.pem;
|
||||
|
||||
# Configuration SSL optimisée (même que ci-dessus)
|
||||
ssl_protocols TLSv1.2 TLSv1.3;
|
||||
ssl_ciphers 'ECDHE-RSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-CHACHA20-POLY1305';
|
||||
ssl_prefer_server_ciphers on;
|
||||
ssl_session_cache shared:SSL:10m;
|
||||
ssl_session_timeout 10m;
|
||||
ssl_stapling on;
|
||||
ssl_stapling_verify on;
|
||||
|
||||
# Logs PRODUCTION
|
||||
access_log /var/log/nginx/pra-app_access.log combined;
|
||||
error_log /var/log/nginx/pra-app_error.log warn;
|
||||
|
||||
# Headers de sécurité globaux
|
||||
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always;
|
||||
add_header X-Frame-Options "SAMEORIGIN" always;
|
||||
add_header X-Content-Type-Options "nosniff" always;
|
||||
add_header X-XSS-Protection "1; mode=block" always;
|
||||
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
|
||||
|
||||
# Taille maximale des uploads (pour les logos, exports, etc.)
|
||||
client_max_body_size 10M;
|
||||
client_body_buffer_size 128k;
|
||||
|
||||
# Timeouts optimisés pour PRODUCTION
|
||||
client_body_timeout 30s;
|
||||
client_header_timeout 30s;
|
||||
send_timeout 60s;
|
||||
|
||||
# =============================================================================
|
||||
# APPLICATION FLUTTER (contenu statique)
|
||||
# =============================================================================
|
||||
location / {
|
||||
root /var/www/geosector/app;
|
||||
index index.html;
|
||||
try_files $uri $uri/ /index.html;
|
||||
|
||||
# Cache intelligent pour PRODUCTION
|
||||
# HTML : pas de cache (pour déploiements)
|
||||
location ~* \.html$ {
|
||||
expires -1;
|
||||
add_header Cache-Control "no-cache, no-store, must-revalidate";
|
||||
}
|
||||
|
||||
# Assets Flutter (JS, CSS, fonts) avec hash : cache agressif
|
||||
location ~* \.(js|css|woff|woff2|ttf|eot)$ {
|
||||
expires 1y;
|
||||
add_header Cache-Control "public, immutable";
|
||||
access_log off;
|
||||
}
|
||||
|
||||
# Images : cache longue durée
|
||||
location ~* \.(jpg|jpeg|png|gif|ico|svg|webp)$ {
|
||||
expires 30d;
|
||||
add_header Cache-Control "public";
|
||||
access_log off;
|
||||
}
|
||||
}
|
||||
|
||||
# =============================================================================
|
||||
# API PHP (RESTful)
|
||||
# =============================================================================
|
||||
location /api/ {
|
||||
root /var/www/geosector;
|
||||
|
||||
# CORS - Liste blanche des origines autorisées en PRODUCTION
|
||||
set $cors_origin "";
|
||||
|
||||
# Autoriser uniquement les domaines de production
|
||||
if ($http_origin ~* ^https://(app\.geosector\.fr|geosector\.fr)$) {
|
||||
set $cors_origin $http_origin;
|
||||
}
|
||||
|
||||
# Gestion des preflight requests (OPTIONS)
|
||||
if ($request_method = 'OPTIONS') {
|
||||
add_header 'Access-Control-Allow-Origin' $cors_origin always;
|
||||
add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, DELETE, PATCH, OPTIONS' always;
|
||||
add_header 'Access-Control-Allow-Headers' 'DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range,Authorization' always;
|
||||
add_header 'Access-Control-Allow-Credentials' 'true' always;
|
||||
add_header 'Access-Control-Max-Age' 86400;
|
||||
add_header 'Content-Type' 'text/plain; charset=utf-8';
|
||||
add_header 'Content-Length' 0;
|
||||
return 204;
|
||||
}
|
||||
|
||||
# Headers CORS pour les requêtes normales
|
||||
add_header 'Access-Control-Allow-Origin' $cors_origin always;
|
||||
add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, DELETE, PATCH, OPTIONS' always;
|
||||
add_header 'Access-Control-Allow-Headers' 'DNT,X-CustomHeader,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Authorization' always;
|
||||
add_header 'Access-Control-Allow-Credentials' 'true' always;
|
||||
|
||||
# Cache API : pas de cache (données dynamiques)
|
||||
add_header Cache-Control "no-store, no-cache, must-revalidate, proxy-revalidate" always;
|
||||
add_header Pragma "no-cache" always;
|
||||
add_header Expires "0" always;
|
||||
|
||||
# Headers de sécurité spécifiques API
|
||||
add_header X-Content-Type-Options "nosniff" always;
|
||||
add_header X-Frame-Options "DENY" always;
|
||||
|
||||
# Rewrite vers index.php
|
||||
try_files $uri $uri/ /api/index.php$is_args$args;
|
||||
|
||||
# Traitement PHP
|
||||
location ~ ^/api/(.+\.php)$ {
|
||||
root /var/www/geosector;
|
||||
|
||||
# FastCGI PHP-FPM
|
||||
fastcgi_pass unix:/run/php-fpm83/php-fpm.sock;
|
||||
fastcgi_index index.php;
|
||||
include fastcgi_params;
|
||||
fastcgi_param SCRIPT_FILENAME $request_filename;
|
||||
|
||||
# Variable d'environnement PRODUCTION
|
||||
fastcgi_param APP_ENV "production";
|
||||
fastcgi_param SERVER_NAME "app3.geosector.fr";
|
||||
|
||||
# Headers transmis à PHP
|
||||
fastcgi_param HTTP_X_REAL_IP $remote_addr;
|
||||
fastcgi_param HTTP_X_FORWARDED_FOR $proxy_add_x_forwarded_for;
|
||||
fastcgi_param HTTP_X_FORWARDED_PROTO $scheme;
|
||||
|
||||
# Timeouts pour opérations longues (sync, exports)
|
||||
fastcgi_read_timeout 300;
|
||||
fastcgi_send_timeout 300;
|
||||
fastcgi_connect_timeout 60;
|
||||
|
||||
# Buffers optimisés
|
||||
fastcgi_buffer_size 128k;
|
||||
fastcgi_buffers 256 16k;
|
||||
fastcgi_busy_buffers_size 256k;
|
||||
fastcgi_temp_file_write_size 256k;
|
||||
|
||||
# Headers CORS pour réponses PHP
|
||||
add_header 'Access-Control-Allow-Origin' $cors_origin always;
|
||||
add_header 'Access-Control-Allow-Credentials' 'true' always;
|
||||
}
|
||||
}
|
||||
|
||||
# =============================================================================
|
||||
# UPLOADS ET MÉDIAS
|
||||
# =============================================================================
|
||||
location /api/uploads/ {
|
||||
alias /var/www/geosector/api/uploads/;
|
||||
|
||||
# Cache pour les médias uploadés
|
||||
expires 7d;
|
||||
add_header Cache-Control "public";
|
||||
|
||||
# Sécurité : empêcher l'exécution de scripts
|
||||
location ~ \.(php|phtml|php3|php4|php5|phps)$ {
|
||||
deny all;
|
||||
}
|
||||
}
|
||||
|
||||
# =============================================================================
|
||||
# SÉCURITÉ
|
||||
# =============================================================================
|
||||
|
||||
# Bloquer l'accès aux fichiers sensibles
|
||||
location ~ /\.(?!well-known) {
|
||||
deny all;
|
||||
access_log off;
|
||||
log_not_found off;
|
||||
}
|
||||
|
||||
# Bloquer l'accès aux fichiers de configuration
|
||||
location ~* \.(env|sql|bak|backup|swp|config|conf|ini|log)$ {
|
||||
deny all;
|
||||
access_log off;
|
||||
log_not_found off;
|
||||
}
|
||||
|
||||
# Bloquer les user-agents malveillants
|
||||
if ($http_user_agent ~* (bot|crawler|spider|scraper|wget|curl)) {
|
||||
return 403;
|
||||
}
|
||||
|
||||
# Protection contre les requêtes invalides
|
||||
if ($request_method !~ ^(GET|HEAD|POST|PUT|DELETE|PATCH|OPTIONS)$) {
|
||||
return 405;
|
||||
}
|
||||
|
||||
# =============================================================================
|
||||
# MONITORING
|
||||
# =============================================================================
|
||||
|
||||
# Endpoint de health check (accessible uniquement en local)
|
||||
location = /nginx-health {
|
||||
access_log off;
|
||||
allow 127.0.0.1;
|
||||
allow 13.23.34.0/24; # Réseau interne Incus
|
||||
deny all;
|
||||
return 200 "healthy\n";
|
||||
add_header Content-Type text/plain;
|
||||
}
|
||||
}
|
||||
1
api/config/whitelist_ip_cache.txt
Normal file
1
api/config/whitelist_ip_cache.txt
Normal file
@@ -0,0 +1 @@
|
||||
{"ip":"169.155.255.55","timestamp":1758618220,"retrieved_at":"2025-09-23 09:03:41"}
|
||||
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;
|
||||
30
api/data/README.md
Normal file
30
api/data/README.md
Normal file
@@ -0,0 +1,30 @@
|
||||
# Répertoire data
|
||||
|
||||
Ce répertoire contient les données de référence pour l'API.
|
||||
|
||||
## Fichiers
|
||||
|
||||
- `stripe_certified_devices.json` (optionnel) : Liste personnalisée des appareils certifiés Stripe Tap to Pay
|
||||
|
||||
## Format stripe_certified_devices.json
|
||||
|
||||
Si vous souhaitez ajouter des appareils supplémentaires à la liste intégrée, créez un fichier `stripe_certified_devices.json` avec le format suivant :
|
||||
|
||||
```json
|
||||
[
|
||||
{
|
||||
"manufacturer": "Samsung",
|
||||
"model": "Galaxy A55",
|
||||
"model_identifier": "SM-A556B",
|
||||
"min_android_version": 14
|
||||
},
|
||||
{
|
||||
"manufacturer": "Fairphone",
|
||||
"model": "Fairphone 5",
|
||||
"model_identifier": "FP5",
|
||||
"min_android_version": 13
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
Les appareils dans ce fichier seront ajoutés à la liste intégrée dans le script CRON.
|
||||
@@ -1,27 +1,42 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Script de déploiement pour GEOSECTOR API
|
||||
# Version: 3.0 (10 mai 2025)
|
||||
# Script de déploiement unifié pour GEOSECTOR API
|
||||
# Version: 4.0 (Janvier 2025)
|
||||
# Auteur: Pierre (avec l'aide de Claude)
|
||||
#
|
||||
# Usage:
|
||||
# ./deploy-api.sh # Déploiement local DEV (code → container geo)
|
||||
# ./deploy-api.sh rca # Livraison RECETTE (container geo → rca-geo)
|
||||
# ./deploy-api.sh pra # Livraison PRODUCTION (rca-geo → pra-geo)
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
# =====================================
|
||||
# Configuration générale
|
||||
# =====================================
|
||||
|
||||
# Paramètre optionnel pour l'environnement cible
|
||||
TARGET_ENV=${1:-dev}
|
||||
|
||||
# Configuration SSH
|
||||
HOST_KEY="/home/pierre/.ssh/id_rsa_mbpi"
|
||||
HOST_PORT="22"
|
||||
HOST_USER="root"
|
||||
|
||||
# Configuration des serveurs
|
||||
JUMP_USER="root"
|
||||
JUMP_HOST="195.154.80.116"
|
||||
JUMP_PORT="22"
|
||||
JUMP_KEY="/Users/pierre/.ssh/id_rsa_mbpi"
|
||||
RCA_HOST="195.154.80.116" # IN3 - Serveur de recette
|
||||
PRA_HOST="51.159.7.190" # IN4 - Serveur de production
|
||||
|
||||
# Paramètres du container Incus
|
||||
INCUS_PROJECT=default
|
||||
INCUS_CONTAINER=dva-geo
|
||||
CONTAINER_USER=root
|
||||
|
||||
# Paramètres de déploiement
|
||||
FINAL_PATH="/var/www/geosector/api"
|
||||
# Configuration Incus
|
||||
INCUS_PROJECT="default"
|
||||
API_PATH="/var/www/geosector/api"
|
||||
FINAL_OWNER="nginx"
|
||||
FINAL_GROUP="nginx"
|
||||
FINAL_OWNER_LOGS="nobody"
|
||||
FINAL_GROUP_LOGS="nginx"
|
||||
|
||||
# Configuration de sauvegarde
|
||||
BACKUP_DIR="/data/backup/geosector/api"
|
||||
|
||||
# Couleurs pour les messages
|
||||
GREEN='\033[0;32m'
|
||||
@@ -30,37 +45,111 @@ YELLOW='\033[0;33m'
|
||||
BLUE='\033[0;34m'
|
||||
NC='\033[0m' # No Color
|
||||
|
||||
run_in_container() {
|
||||
echo "-> Running: $*"
|
||||
incus exec "${INCUS_CONTAINER}" -- "$@" || {
|
||||
echo "❌ Failed to run: $*"
|
||||
exit 1
|
||||
}
|
||||
}
|
||||
# =====================================
|
||||
# Fonctions utilitaires
|
||||
# =====================================
|
||||
|
||||
# Fonction pour afficher les messages d'étape
|
||||
echo_step() {
|
||||
echo -e "${GREEN}==>${NC} $1"
|
||||
}
|
||||
|
||||
# Fonction pour afficher les informations
|
||||
echo_info() {
|
||||
echo -e "${BLUE}Info:${NC} $1"
|
||||
}
|
||||
|
||||
# Fonction pour afficher les avertissements
|
||||
echo_warning() {
|
||||
echo -e "${YELLOW}Warning:${NC} $1"
|
||||
}
|
||||
|
||||
# Fonction pour afficher les erreurs
|
||||
echo_error() {
|
||||
echo -e "${RED}Error:${NC} $1"
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Vérification de l'environnement
|
||||
echo_step "Verifying environment..."
|
||||
# Fonction pour nettoyer les anciens backups
|
||||
cleanup_old_backups() {
|
||||
local prefix=""
|
||||
case $TARGET_ENV in
|
||||
"dev") prefix="api-dev-" ;;
|
||||
"rca") prefix="api-rca-" ;;
|
||||
"pra") prefix="api-pra-" ;;
|
||||
esac
|
||||
|
||||
echo_info "Cleaning old backups (keeping last 5)..."
|
||||
ls -t "${BACKUP_DIR}"/${prefix}*.tar.gz 2>/dev/null | tail -n +6 | xargs -r rm -f && {
|
||||
REMAINING_BACKUPS=$(ls "${BACKUP_DIR}"/${prefix}*.tar.gz 2>/dev/null | wc -l)
|
||||
echo_info "Kept ${REMAINING_BACKUPS} backup(s) for ${TARGET_ENV}"
|
||||
}
|
||||
}
|
||||
|
||||
# =====================================
|
||||
# Détermination de la configuration selon l'environnement
|
||||
# =====================================
|
||||
|
||||
case $TARGET_ENV in
|
||||
"dev")
|
||||
echo_step "Configuring for DEV deployment on IN3"
|
||||
SOURCE_TYPE="local_code"
|
||||
DEST_CONTAINER="dva-geo"
|
||||
DEST_HOST="${RCA_HOST}" # IN3 pour le DEV aussi
|
||||
ENV_NAME="DEVELOPMENT"
|
||||
;;
|
||||
"rca")
|
||||
echo_step "Configuring for RECETTE delivery"
|
||||
SOURCE_TYPE="remote_container"
|
||||
SOURCE_CONTAINER="dva-geo"
|
||||
SOURCE_HOST="${RCA_HOST}"
|
||||
DEST_CONTAINER="rca-geo"
|
||||
DEST_HOST="${RCA_HOST}"
|
||||
ENV_NAME="RECETTE"
|
||||
;;
|
||||
"pra")
|
||||
echo_step "Configuring for PRODUCTION delivery"
|
||||
SOURCE_TYPE="remote_container"
|
||||
SOURCE_HOST="${RCA_HOST}"
|
||||
SOURCE_CONTAINER="rca-geo"
|
||||
DEST_CONTAINER="pra-geo"
|
||||
DEST_HOST="${PRA_HOST}"
|
||||
ENV_NAME="PRODUCTION"
|
||||
;;
|
||||
*)
|
||||
echo_error "Unknown environment: $TARGET_ENV. Use 'dev', 'rca' or 'pra'"
|
||||
;;
|
||||
esac
|
||||
|
||||
echo_info "Deployment flow: ${ENV_NAME}"
|
||||
|
||||
# =====================================
|
||||
# Création de l'archive selon la source
|
||||
# =====================================
|
||||
|
||||
# Créer le dossier de backup s'il n'existe pas
|
||||
if [ ! -d "${BACKUP_DIR}" ]; then
|
||||
echo_info "Creating backup directory ${BACKUP_DIR}..."
|
||||
mkdir -p "${BACKUP_DIR}" || echo_error "Failed to create backup directory"
|
||||
fi
|
||||
|
||||
# Horodatage format YYYYMMDDHH
|
||||
TIMESTAMP=$(date +%Y%m%d%H)
|
||||
|
||||
# Nom de l'archive selon l'environnement
|
||||
case $TARGET_ENV in
|
||||
"dev")
|
||||
ARCHIVE_NAME="api-dev-${TIMESTAMP}.tar.gz"
|
||||
;;
|
||||
"rca")
|
||||
ARCHIVE_NAME="api-rca-${TIMESTAMP}.tar.gz"
|
||||
;;
|
||||
"pra")
|
||||
ARCHIVE_NAME="api-pra-${TIMESTAMP}.tar.gz"
|
||||
;;
|
||||
esac
|
||||
|
||||
ARCHIVE_PATH="${BACKUP_DIR}/${ARCHIVE_NAME}"
|
||||
|
||||
if [ "$SOURCE_TYPE" = "local_code" ]; then
|
||||
# DEV: Créer une archive depuis le code local
|
||||
echo_step "Creating archive from local code..."
|
||||
|
||||
# Vérification des fichiers requis
|
||||
if [ ! -f "src/Config/AppConfig.php" ]; then
|
||||
@@ -71,75 +160,224 @@ if [ ! -f "composer.json" ] || [ ! -f "composer.lock" ]; then
|
||||
echo_error "Composer files missing"
|
||||
fi
|
||||
|
||||
# Étape 0: Définir le nom de l'archive
|
||||
ARCHIVE_NAME="api-deploy-$(date +%s).tar.gz"
|
||||
echo_info "Archive name will be: $ARCHIVE_NAME"
|
||||
|
||||
# Étape 1: Créer une archive du projet
|
||||
echo_step "Creating project archive..."
|
||||
tar --exclude='.git' \
|
||||
--exclude='.gitignore' \
|
||||
--exclude='.vscode' \
|
||||
--exclude='logs' \
|
||||
--exclude='sessions' \
|
||||
--exclude='opendata' \
|
||||
--exclude='*.template' \
|
||||
--exclude='*.sh' \
|
||||
--exclude='.env' \
|
||||
--exclude='.env_marker' \
|
||||
--exclude='*.log' \
|
||||
--exclude='.DS_Store' \
|
||||
--exclude='README.md' \
|
||||
--exclude="*.tar.gz" \
|
||||
--no-xattrs \
|
||||
-czf "${ARCHIVE_NAME}" . || echo_error "Failed to create archive"
|
||||
--exclude='node_modules' \
|
||||
--exclude='vendor' \
|
||||
--exclude='*.swp' \
|
||||
--exclude='*.swo' \
|
||||
--exclude='*~' \
|
||||
-czf "${ARCHIVE_PATH}" . 2>/dev/null || echo_error "Failed to create archive"
|
||||
|
||||
# Vérifier la taille de l'archive
|
||||
ARCHIVE_SIZE=$(du -h "${ARCHIVE_NAME}" | cut -f1)
|
||||
echo_info "Archive created: ${ARCHIVE_PATH}"
|
||||
echo_info "Archive size: $(du -h "${ARCHIVE_PATH}" | cut -f1)"
|
||||
|
||||
SSH_JUMP_CMD="ssh -i ${JUMP_KEY} -p ${JUMP_PORT} ${JUMP_USER}@${JUMP_HOST}"
|
||||
# Cette section n'est plus utilisée car RCA utilise maintenant remote_container
|
||||
|
||||
# É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"
|
||||
elif [ "$SOURCE_TYPE" = "remote_container" ]; then
|
||||
# RCA et PRA: Créer une archive depuis un container distant
|
||||
echo_step "Creating archive from remote container ${SOURCE_CONTAINER} on ${SOURCE_HOST}..."
|
||||
|
||||
# Étape 3: Exécuter les commandes sur le serveur de saut pour déployer dans le container Incus
|
||||
echo_step "Deploying to Incus container..."
|
||||
$SSH_JUMP_CMD "
|
||||
# Créer l'archive sur le serveur source
|
||||
ssh -i ${HOST_KEY} -p ${HOST_PORT} ${HOST_USER}@${SOURCE_HOST} "
|
||||
incus project switch ${INCUS_PROJECT} &&
|
||||
incus exec ${SOURCE_CONTAINER} -- tar \
|
||||
--exclude='logs' \
|
||||
--exclude='uploads' \
|
||||
--exclude='sessions' \
|
||||
--exclude='opendata' \
|
||||
-czf /tmp/${ARCHIVE_NAME} -C ${API_PATH} .
|
||||
" || echo_error "Failed to create archive on remote"
|
||||
|
||||
# Extraire l'archive du container vers l'hôte
|
||||
ssh -i ${HOST_KEY} -p ${HOST_PORT} ${HOST_USER}@${SOURCE_HOST} "
|
||||
incus file pull ${SOURCE_CONTAINER}/tmp/${ARCHIVE_NAME} /tmp/${ARCHIVE_NAME} &&
|
||||
incus exec ${SOURCE_CONTAINER} -- rm -f /tmp/${ARCHIVE_NAME}
|
||||
" || echo_error "Failed to extract archive from remote container"
|
||||
|
||||
# Copier l'archive vers la machine locale
|
||||
scp -i ${HOST_KEY} -P ${HOST_PORT} ${HOST_USER}@${SOURCE_HOST}:/tmp/${ARCHIVE_NAME} ${ARCHIVE_PATH} || echo_error "Failed to copy archive locally"
|
||||
|
||||
echo_info "Archive saved: ${ARCHIVE_PATH}"
|
||||
echo_info "Archive size: $(du -h "${ARCHIVE_PATH}" | cut -f1)"
|
||||
fi
|
||||
|
||||
# Nettoyer les anciens backups
|
||||
cleanup_old_backups
|
||||
|
||||
# =====================================
|
||||
# Déploiement selon la destination
|
||||
# =====================================
|
||||
|
||||
# Tous les déploiements se font maintenant sur des containers distants
|
||||
if [ "$DEST_HOST" != "local" ]; then
|
||||
# Déploiement sur container distant (DEV, RCA ou PRA)
|
||||
echo_step "Deploying to remote container ${DEST_CONTAINER} on ${DEST_HOST}..."
|
||||
|
||||
# Créer une sauvegarde sur le serveur de destination
|
||||
BACKUP_TIMESTAMP=$(date +"%Y%m%d_%H%M%S")
|
||||
REMOTE_BACKUP_DIR="${API_PATH}_backup_${BACKUP_TIMESTAMP}"
|
||||
|
||||
echo_info "Creating backup on destination..."
|
||||
ssh -i ${HOST_KEY} -p ${HOST_PORT} ${HOST_USER}@${DEST_HOST} "
|
||||
incus project switch ${INCUS_PROJECT} &&
|
||||
incus exec ${DEST_CONTAINER} -- test -d ${API_PATH} &&
|
||||
incus exec ${DEST_CONTAINER} -- cp -r ${API_PATH} ${REMOTE_BACKUP_DIR} &&
|
||||
echo 'Backup created: ${REMOTE_BACKUP_DIR}'
|
||||
" || echo_warning "No existing installation to backup"
|
||||
|
||||
# Transférer l'archive vers le serveur de destination
|
||||
echo_info "Transferring archive to ${DEST_HOST}..."
|
||||
|
||||
if [ "$SOURCE_TYPE" = "local_code" ]; then
|
||||
# Pour DEV: copier depuis local vers IN3
|
||||
scp -i ${HOST_KEY} -P ${HOST_PORT} ${ARCHIVE_PATH} ${HOST_USER}@${DEST_HOST}:/tmp/${ARCHIVE_NAME} || echo_error "Failed to copy archive to destination"
|
||||
elif [ "$SOURCE_TYPE" = "remote_container" ] && [ "$SOURCE_HOST" = "$DEST_HOST" ]; then
|
||||
# Pour RCA: même serveur (IN3), pas de transfert nécessaire, l'archive est déjà là
|
||||
echo_info "Archive already on destination server (same host)"
|
||||
else
|
||||
# Pour PRA: l'archive est déjà sur la machine locale (copiée depuis IN3)
|
||||
# On la transfère maintenant vers IN4
|
||||
echo_info "Transferring archive from local to IN4..."
|
||||
scp -i ${HOST_KEY} -P ${HOST_PORT} ${ARCHIVE_PATH} ${HOST_USER}@${DEST_HOST}:/tmp/${ARCHIVE_NAME} || echo_error "Failed to copy archive to IN4"
|
||||
|
||||
# Nettoyer sur le serveur source IN3
|
||||
ssh -i ${HOST_KEY} -p ${HOST_PORT} ${HOST_USER}@${SOURCE_HOST} "rm -f /tmp/${ARCHIVE_NAME}" || echo_warning "Could not clean source server"
|
||||
fi
|
||||
|
||||
# Déployer sur le container de destination
|
||||
echo_info "Extracting on destination container..."
|
||||
|
||||
# Déterminer le nom de l'environnement pour le marqueur
|
||||
case $TARGET_ENV in
|
||||
"dev") ENV_MARKER="development" ;;
|
||||
"rca") ENV_MARKER="recette" ;;
|
||||
"pra") ENV_MARKER="production" ;;
|
||||
esac
|
||||
|
||||
ssh -i ${HOST_KEY} -p ${HOST_PORT} ${HOST_USER}@${DEST_HOST} "
|
||||
set -euo pipefail
|
||||
|
||||
echo '✅ Passage au projet Incus...'
|
||||
incus project switch ${INCUS_PROJECT} || exit 1
|
||||
# Pousser l'archive dans le container
|
||||
incus project switch ${INCUS_PROJECT} &&
|
||||
incus file push /tmp/${ARCHIVE_NAME} ${DEST_CONTAINER}/tmp/${ARCHIVE_NAME} &&
|
||||
|
||||
echo '📦 Poussée de archive dans le conteneur...'
|
||||
incus file push /tmp/${ARCHIVE_NAME} ${INCUS_CONTAINER}/tmp/${ARCHIVE_NAME} || exit 1
|
||||
# Nettoyer sélectivement (préserver logs, uploads et sessions)
|
||||
incus exec ${DEST_CONTAINER} -- find ${API_PATH} -mindepth 1 -maxdepth 1 ! -name 'uploads' ! -name 'logs' ! -name 'sessions' -exec rm -rf {} \; 2>/dev/null || true &&
|
||||
|
||||
echo '📁 Préparation du dossier final...'
|
||||
incus exec ${INCUS_CONTAINER} -- mkdir -p ${FINAL_PATH} || exit 1
|
||||
incus exec ${INCUS_CONTAINER} -- rm -rf ${FINAL_PATH}/* || exit 1
|
||||
incus exec ${INCUS_CONTAINER} -- tar -xzf /tmp/${ARCHIVE_NAME} -C ${FINAL_PATH}/ || exit 1
|
||||
# Extraire l'archive
|
||||
incus exec ${DEST_CONTAINER} -- tar -xzf /tmp/${ARCHIVE_NAME} -C ${API_PATH}/ &&
|
||||
|
||||
echo '🔧 Réglage des permissions...'
|
||||
incus exec ${INCUS_CONTAINER} -- mkdir -p ${FINAL_PATH}/logs || exit 1
|
||||
incus exec ${INCUS_CONTAINER} -- chown -R ${FINAL_OWNER}:${FINAL_GROUP} ${FINAL_PATH} || exit 1
|
||||
incus exec ${INCUS_CONTAINER} -- find ${FINAL_PATH} -type d -exec chmod 755 {} \; || exit 1
|
||||
incus exec ${INCUS_CONTAINER} -- find ${FINAL_PATH} -type f -exec chmod 644 {} \; || exit 1
|
||||
# Créer le marqueur d'environnement pour la détection CLI
|
||||
incus exec ${DEST_CONTAINER} -- bash -c 'echo \"${ENV_MARKER}\" > ${API_PATH}/.env_marker' &&
|
||||
|
||||
# Permissions spéciales pour le dossier logs (pour permettre à PHP-FPM de l'utilisateur nobody d'y écrire)
|
||||
incus exec ${INCUS_CONTAINER} -- chown -R ${FINAL_OWNER}:${FINAL_OWNER_LOGS} ${FINAL_PATH}/logs || exit 1
|
||||
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
|
||||
# Permissions
|
||||
incus exec ${DEST_CONTAINER} -- chown -R ${FINAL_OWNER}:${FINAL_GROUP} ${API_PATH} &&
|
||||
incus exec ${DEST_CONTAINER} -- find ${API_PATH} -type d -exec chmod 755 {} \; &&
|
||||
incus exec ${DEST_CONTAINER} -- find ${API_PATH} -type f -exec chmod 644 {} \; &&
|
||||
|
||||
echo '🧹 Nettoyage...'
|
||||
incus exec ${INCUS_CONTAINER} -- rm -f /tmp/${ARCHIVE_NAME} || exit 1
|
||||
rm -f /tmp/${ARCHIVE_NAME} || exit 1
|
||||
"
|
||||
# Permissions spéciales pour logs
|
||||
incus exec ${DEST_CONTAINER} -- mkdir -p ${API_PATH}/logs/events &&
|
||||
incus exec ${DEST_CONTAINER} -- chown -R ${FINAL_OWNER_LOGS}:${FINAL_GROUP} ${API_PATH}/logs &&
|
||||
incus exec ${DEST_CONTAINER} -- find ${API_PATH}/logs -type d -exec chmod 750 {} \; &&
|
||||
incus exec ${DEST_CONTAINER} -- find ${API_PATH}/logs -type f -exec chmod 640 {} \; &&
|
||||
|
||||
# Nettoyage local
|
||||
rm -f "${ARCHIVE_NAME}"
|
||||
# Permissions spéciales pour uploads
|
||||
incus exec ${DEST_CONTAINER} -- mkdir -p ${API_PATH}/uploads &&
|
||||
incus exec ${DEST_CONTAINER} -- chown -R ${FINAL_OWNER_LOGS}:${FINAL_GROUP} ${API_PATH}/uploads &&
|
||||
incus exec ${DEST_CONTAINER} -- find ${API_PATH}/uploads -type d -exec chmod 750 {} \; &&
|
||||
incus exec ${DEST_CONTAINER} -- find ${API_PATH}/uploads -type f -exec chmod 640 {} \; &&
|
||||
|
||||
# Permissions spéciales pour sessions
|
||||
incus exec ${DEST_CONTAINER} -- mkdir -p ${API_PATH}/sessions &&
|
||||
incus exec ${DEST_CONTAINER} -- chown -R ${FINAL_OWNER_LOGS}:${FINAL_GROUP} ${API_PATH}/sessions &&
|
||||
incus exec ${DEST_CONTAINER} -- chmod 700 ${API_PATH}/sessions &&
|
||||
|
||||
# Composer (installation stricte - échec bloquant)
|
||||
incus exec ${DEST_CONTAINER} -- bash -c 'cd ${API_PATH} && composer install --no-dev --optimize-autoloader' || { echo 'ERROR: Composer install failed'; exit 1; } &&
|
||||
|
||||
# Nettoyage
|
||||
incus exec ${DEST_CONTAINER} -- rm -f /tmp/${ARCHIVE_NAME} &&
|
||||
rm -f /tmp/${ARCHIVE_NAME}
|
||||
" || echo_error "Deployment failed on destination"
|
||||
|
||||
echo_info "Remote backup saved: ${REMOTE_BACKUP_DIR} on ${DEST_CONTAINER}"
|
||||
|
||||
# Nettoyage des anciens backups sur le container distant
|
||||
echo_info "Cleaning old backup directories on ${DEST_CONTAINER}..."
|
||||
ssh -i ${HOST_KEY} -p ${HOST_PORT} ${HOST_USER}@${DEST_HOST} "
|
||||
incus exec ${DEST_CONTAINER} -- bash -c 'rm -rf ${API_PATH}_backup_*'
|
||||
" && echo_info "Old backups cleaned" || echo_warning "Could not clean old backups"
|
||||
|
||||
# =====================================
|
||||
# Configuration des tâches CRON
|
||||
# =====================================
|
||||
|
||||
echo_step "Configuring CRON tasks..."
|
||||
|
||||
ssh -i ${HOST_KEY} -p ${HOST_PORT} ${HOST_USER}@${DEST_HOST} "
|
||||
incus exec ${DEST_CONTAINER} -- bash <<'EOFCRON'
|
||||
# Sauvegarder les crons existants (hors geosector)
|
||||
crontab -l 2>/dev/null | grep -v 'geosector/api/scripts/cron' > /tmp/crontab_backup || true
|
||||
|
||||
# Créer le nouveau crontab avec les tâches CRON pour l'API
|
||||
cat /tmp/crontab_backup > /tmp/new_crontab
|
||||
cat >> /tmp/new_crontab <<'EOF'
|
||||
|
||||
# GEOSECTOR API - Email queue processing (every 5 minutes)
|
||||
*/5 * * * * /usr/bin/php /var/www/geosector/api/scripts/cron/process_email_queue.php >> /var/www/geosector/api/logs/email_queue.log 2>&1
|
||||
|
||||
# GEOSECTOR API - Security data cleanup (daily at 2am)
|
||||
0 2 * * * /usr/bin/php /var/www/geosector/api/scripts/cron/cleanup_security_data.php >> /var/www/geosector/api/logs/cleanup_security.log 2>&1
|
||||
|
||||
# GEOSECTOR API - Stripe devices update (weekly Sunday at 3am)
|
||||
0 3 * * 0 /usr/bin/php /var/www/geosector/api/scripts/cron/update_stripe_devices.php >> /var/www/geosector/api/logs/stripe_devices.log 2>&1
|
||||
EOF
|
||||
|
||||
# Installer le nouveau crontab
|
||||
crontab /tmp/new_crontab
|
||||
|
||||
# Nettoyer
|
||||
rm -f /tmp/crontab_backup /tmp/new_crontab
|
||||
|
||||
# Afficher les crons installés
|
||||
echo 'CRON tasks installed:'
|
||||
crontab -l | grep geosector
|
||||
EOFCRON
|
||||
" && echo_info "CRON tasks configured successfully" || echo_warning "CRON configuration failed"
|
||||
fi
|
||||
|
||||
# L'archive reste dans le dossier de backup, pas de nettoyage nécessaire
|
||||
echo_info "Archive preserved in backup directory: ${ARCHIVE_PATH}"
|
||||
|
||||
# =====================================
|
||||
# Résumé final
|
||||
echo_step "Deployment completed successfully."
|
||||
echo_info "Your API has been updated on the container."
|
||||
# =====================================
|
||||
|
||||
echo_step "Deployment completed successfully!"
|
||||
echo_info "Environment: ${ENV_NAME}"
|
||||
|
||||
if [ "$TARGET_ENV" = "dev" ]; then
|
||||
echo_info "Deployed from local code to container ${DEST_CONTAINER} on IN3 (${DEST_HOST})"
|
||||
elif [ "$TARGET_ENV" = "rca" ]; then
|
||||
echo_info "Delivered from ${SOURCE_CONTAINER} to ${DEST_CONTAINER} on ${DEST_HOST}"
|
||||
elif [ "$TARGET_ENV" = "pra" ]; then
|
||||
echo_info "Delivered from ${SOURCE_CONTAINER} on ${SOURCE_HOST} to ${DEST_CONTAINER} on ${DEST_HOST}"
|
||||
fi
|
||||
|
||||
echo_info "Deployment completed at: $(date)"
|
||||
|
||||
# Journaliser le déploiement
|
||||
echo "$(date '+%Y-%m-%d %H:%M:%S') - API deployed to ${JUMP_HOST}:${INCUS_CONTAINER}" >> ~/.geo_deploy_history
|
||||
echo "$(date '+%Y-%m-%d %H:%M:%S') - API deployed to ${ENV_NAME} (${DEST_CONTAINER}) - Archive: ${ARCHIVE_NAME}" >> ~/.geo_deploy_history
|
||||
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
|
||||
495
api/docs/EVENTS-LOG.md
Normal file
495
api/docs/EVENTS-LOG.md
Normal file
@@ -0,0 +1,495 @@
|
||||
# Système de logs d'événements JSONL
|
||||
|
||||
## 📋 Vue d'ensemble
|
||||
|
||||
Système de traçabilité des événements métier pour statistiques et audit, stocké en fichiers JSONL (JSON Lines) sans impact sur la base de données principale.
|
||||
|
||||
**Créé le :** 26 Octobre 2025
|
||||
**Rétention :** 15 mois
|
||||
**Format :** JSONL (une ligne = un événement JSON)
|
||||
|
||||
## 🎯 Objectifs
|
||||
|
||||
### Événements tracés
|
||||
|
||||
**Authentification**
|
||||
- Connexions réussies (user_id, entity_id, plateforme, IP)
|
||||
- Tentatives échouées (username, raison, IP, nb tentatives)
|
||||
|
||||
**CRUD métier**
|
||||
- **Passages** : création, modification, suppression
|
||||
- **Secteurs** : création, modification, suppression
|
||||
- **Membres** : création, modification, suppression
|
||||
- **Entités** : création, modification, suppression
|
||||
|
||||
### Cas d'usage
|
||||
|
||||
**1. Admin entité**
|
||||
- Stats de son entité : connexions, passages, secteurs sur 1 jour/semaine/mois
|
||||
- Activité des membres de l'entité
|
||||
|
||||
**2. Super-admin**
|
||||
- Stats globales : tous les passages modifiés sur 2 semaines
|
||||
- Événements toutes entités sur période donnée
|
||||
- Détection d'anomalies
|
||||
|
||||
## 📁 Architecture de stockage
|
||||
|
||||
### Structure des répertoires
|
||||
|
||||
```
|
||||
/logs/events/
|
||||
├── 2025-10-26.jsonl # Fichier du jour (écriture append)
|
||||
├── 2025-10-25.jsonl
|
||||
├── 2025-10-24.jsonl
|
||||
├── 2025-09-30.jsonl
|
||||
├── 2025-09-29.jsonl.gz # Compression auto après 30 jours
|
||||
└── archive/
|
||||
├── 2025-09.jsonl.gz # Archive mensuelle
|
||||
├── 2025-08.jsonl.gz
|
||||
└── 2024-07.jsonl.gz # Supprimé auto après 15 mois
|
||||
```
|
||||
|
||||
### Cycle de vie des fichiers
|
||||
|
||||
| Âge | État | Taille estimée | Accès |
|
||||
|-----|------|----------------|-------|
|
||||
| 0-30 jours | `.jsonl` non compressé | 1-10 MB/jour | Lecture directe rapide |
|
||||
| 30 jours-15 mois | `.jsonl.gz` compressé | ~100 KB/jour | Décompression à la volée |
|
||||
| > 15 mois | Supprimé automatiquement | - | - |
|
||||
|
||||
### Rotation et rétention
|
||||
|
||||
**CRON mensuel** : `scripts/cron/rotate_event_logs.php`
|
||||
- **Fréquence** : 1er du mois à 3h00
|
||||
- **Actions** :
|
||||
1. Compresser les fichiers `.jsonl` de plus de 30 jours en `.jsonl.gz`
|
||||
2. Supprimer les fichiers `.jsonl.gz` de plus de 15 mois
|
||||
3. Logger le résumé de rotation
|
||||
|
||||
**Commande manuelle** :
|
||||
```bash
|
||||
php scripts/cron/rotate_event_logs.php
|
||||
```
|
||||
|
||||
## 📊 Format des événements
|
||||
|
||||
### Structure commune
|
||||
|
||||
Tous les événements partagent ces champs :
|
||||
|
||||
```json
|
||||
{
|
||||
"timestamp": "2025-10-26T14:32:15Z", // ISO 8601 UTC
|
||||
"event": "nom_evenement", // Type d'événement
|
||||
"user_id": 123, // ID utilisateur (si authentifié)
|
||||
"entity_id": 5, // ID entité (si applicable)
|
||||
"ip": "192.168.1.100", // IP client
|
||||
"platform": "ios|android|web", // Plateforme
|
||||
"app_version": "3.3.6" // Version app (mobile uniquement)
|
||||
}
|
||||
```
|
||||
|
||||
### Événements d'authentification
|
||||
|
||||
#### Login réussi
|
||||
```jsonl
|
||||
{"timestamp":"2025-10-26T14:32:15Z","event":"login_success","user_id":123,"entity_id":5,"platform":"ios","app_version":"3.3.6","ip":"192.168.1.100","username":"user123"}
|
||||
```
|
||||
|
||||
#### Login échoué
|
||||
```jsonl
|
||||
{"timestamp":"2025-10-26T14:35:22Z","event":"login_failed","username":"test","reason":"invalid_password","ip":"192.168.1.101","attempt":3,"platform":"web"}
|
||||
```
|
||||
|
||||
**Raisons possibles** : `invalid_password`, `user_not_found`, `account_inactive`, `blocked_ip`
|
||||
|
||||
#### Logout
|
||||
```jsonl
|
||||
{"timestamp":"2025-10-26T16:45:00Z","event":"logout","user_id":123,"entity_id":5,"platform":"android","session_duration":7800}
|
||||
```
|
||||
|
||||
### Événements Passages
|
||||
|
||||
#### Création
|
||||
```jsonl
|
||||
{"timestamp":"2025-10-26T14:40:10Z","event":"passage_created","passage_id":45678,"user_id":123,"entity_id":5,"operation_id":789,"sector_id":12,"amount":50.00,"payment_type":"cash","platform":"android"}
|
||||
```
|
||||
|
||||
#### Modification
|
||||
```jsonl
|
||||
{"timestamp":"2025-10-26T14:42:05Z","event":"passage_updated","passage_id":45678,"user_id":123,"entity_id":5,"changes":{"amount":{"old":50.00,"new":75.00},"payment_type":{"old":"cash","new":"stripe"}},"platform":"ios"}
|
||||
```
|
||||
|
||||
#### Suppression
|
||||
```jsonl
|
||||
{"timestamp":"2025-10-26T14:45:30Z","event":"passage_deleted","passage_id":45678,"user_id":123,"entity_id":5,"operation_id":789,"deleted_by":123,"soft_delete":true,"platform":"web"}
|
||||
```
|
||||
|
||||
### Événements Secteurs
|
||||
|
||||
#### Création
|
||||
```jsonl
|
||||
{"timestamp":"2025-10-26T15:10:00Z","event":"sector_created","sector_id":456,"operation_id":789,"entity_id":5,"user_id":123,"sector_name":"Secteur A","platform":"web"}
|
||||
```
|
||||
|
||||
#### Modification
|
||||
```jsonl
|
||||
{"timestamp":"2025-10-26T15:12:00Z","event":"sector_updated","sector_id":456,"operation_id":789,"entity_id":5,"user_id":123,"changes":{"sector_name":{"old":"Secteur A","new":"Secteur Alpha"}},"platform":"web"}
|
||||
```
|
||||
|
||||
#### Suppression
|
||||
```jsonl
|
||||
{"timestamp":"2025-10-26T15:15:00Z","event":"sector_deleted","sector_id":456,"operation_id":789,"entity_id":5,"user_id":123,"deleted_by":123,"soft_delete":true,"platform":"web"}
|
||||
```
|
||||
|
||||
### Événements Membres (Users)
|
||||
|
||||
#### Création
|
||||
```jsonl
|
||||
{"timestamp":"2025-10-26T15:20:00Z","event":"user_created","new_user_id":789,"entity_id":5,"created_by":123,"role_id":1,"username":"newuser","platform":"web"}
|
||||
```
|
||||
|
||||
#### Modification
|
||||
```jsonl
|
||||
{"timestamp":"2025-10-26T15:25:00Z","event":"user_updated","user_id":789,"entity_id":5,"updated_by":123,"changes":{"role_id":{"old":1,"new":2},"encrypted_phone":true},"platform":"web"}
|
||||
```
|
||||
|
||||
**Note** : Les champs chiffrés sont indiqués par un booléen `true` sans exposer les valeurs
|
||||
|
||||
#### Suppression
|
||||
```jsonl
|
||||
{"timestamp":"2025-10-26T15:30:00Z","event":"user_deleted","user_id":789,"entity_id":5,"deleted_by":123,"soft_delete":true,"platform":"web"}
|
||||
```
|
||||
|
||||
### Événements Entités
|
||||
|
||||
#### Création
|
||||
```jsonl
|
||||
{"timestamp":"2025-10-26T15:35:00Z","event":"entity_created","entity_id":25,"created_by":1,"entity_type_id":1,"postal_code":"75001","platform":"web"}
|
||||
```
|
||||
|
||||
#### Modification
|
||||
```jsonl
|
||||
{"timestamp":"2025-10-26T15:40:00Z","event":"entity_updated","entity_id":25,"user_id":123,"updated_by":123,"changes":{"encrypted_name":true,"encrypted_email":true,"chk_stripe":{"old":0,"new":1}},"platform":"web"}
|
||||
```
|
||||
|
||||
#### Suppression (rare)
|
||||
```jsonl
|
||||
{"timestamp":"2025-10-26T15:45:00Z","event":"entity_deleted","entity_id":25,"deleted_by":1,"soft_delete":true,"reason":"duplicate","platform":"web"}
|
||||
```
|
||||
|
||||
### Événements Opérations
|
||||
|
||||
#### Création
|
||||
```jsonl
|
||||
{"timestamp":"2025-10-26T16:00:00Z","event":"operation_created","operation_id":999,"entity_id":5,"created_by":123,"date_start":"2025-11-01","date_end":"2025-11-30","platform":"web"}
|
||||
```
|
||||
|
||||
#### Modification
|
||||
```jsonl
|
||||
{"timestamp":"2025-10-26T16:05:00Z","event":"operation_updated","operation_id":999,"entity_id":5,"updated_by":123,"changes":{"date_end":{"old":"2025-11-30","new":"2025-12-15"},"chk_active":{"old":0,"new":1}},"platform":"web"}
|
||||
```
|
||||
|
||||
#### Suppression
|
||||
```jsonl
|
||||
{"timestamp":"2025-10-26T16:10:00Z","event":"operation_deleted","operation_id":999,"entity_id":5,"deleted_by":123,"soft_delete":true,"platform":"web"}
|
||||
```
|
||||
|
||||
## 🛠️ Implémentation
|
||||
|
||||
### Service EventLogService.php
|
||||
|
||||
**Emplacement** : `src/Services/EventLogService.php`
|
||||
|
||||
**Méthodes publiques** :
|
||||
```php
|
||||
EventLogService::logLoginSuccess($userId, $entityId, $username)
|
||||
EventLogService::logLoginFailed($username, $reason, $attempt)
|
||||
EventLogService::logLogout($userId, $entityId, $sessionDuration)
|
||||
|
||||
EventLogService::logPassageCreated($passageId, $operationId, $sectorId, $amount, $paymentType)
|
||||
EventLogService::logPassageUpdated($passageId, $changes)
|
||||
EventLogService::logPassageDeleted($passageId, $operationId, $softDelete)
|
||||
|
||||
EventLogService::logSectorCreated($sectorId, $operationId, $sectorName)
|
||||
EventLogService::logSectorUpdated($sectorId, $operationId, $changes)
|
||||
EventLogService::logSectorDeleted($sectorId, $operationId, $softDelete)
|
||||
|
||||
EventLogService::logUserCreated($newUserId, $entityId, $roleId, $username)
|
||||
EventLogService::logUserUpdated($userId, $changes)
|
||||
EventLogService::logUserDeleted($userId, $softDelete)
|
||||
|
||||
EventLogService::logEntityCreated($entityId, $entityTypeId, $postalCode)
|
||||
EventLogService::logEntityUpdated($entityId, $changes)
|
||||
EventLogService::logEntityDeleted($entityId, $reason)
|
||||
|
||||
EventLogService::logOperationCreated($operationId, $dateStart, $dateEnd)
|
||||
EventLogService::logOperationUpdated($operationId, $changes)
|
||||
EventLogService::logOperationDeleted($operationId, $softDelete)
|
||||
```
|
||||
|
||||
**Enrichissement automatique** :
|
||||
- `timestamp` : Généré automatiquement (UTC)
|
||||
- `user_id`, `entity_id` : Récupérés depuis `Session`
|
||||
- `ip` : Récupérée via `ClientDetector`
|
||||
- `platform` : Détecté via `ClientDetector` (ios/android/web)
|
||||
- `app_version` : Extrait du User-Agent pour mobile
|
||||
|
||||
### Intégration dans les Controllers
|
||||
|
||||
**Exemple dans PassageController** :
|
||||
```php
|
||||
public function createPassage(Request $request, Response $response): void {
|
||||
// ... validation et création ...
|
||||
|
||||
$passageId = $db->lastInsertId();
|
||||
|
||||
// Log de l'événement
|
||||
EventLogService::logPassageCreated(
|
||||
$passageId,
|
||||
$data['fk_operation'],
|
||||
$data['fk_sector'],
|
||||
$data['montant'],
|
||||
$data['fk_type_reglement']
|
||||
);
|
||||
|
||||
// ... suite du code ...
|
||||
}
|
||||
```
|
||||
|
||||
### Scripts d'analyse
|
||||
|
||||
#### 1. Stats entité
|
||||
|
||||
**Fichier** : `scripts/stats/entity_stats.php`
|
||||
|
||||
**Usage** :
|
||||
```bash
|
||||
# Stats entité 5 sur 7 derniers jours
|
||||
php scripts/stats/entity_stats.php --entity-id=5 --days=7
|
||||
|
||||
# Stats entité 5 entre deux dates
|
||||
php scripts/stats/entity_stats.php --entity-id=5 --from=2025-10-01 --to=2025-10-26
|
||||
|
||||
# Résultat JSON
|
||||
{
|
||||
"entity_id": 5,
|
||||
"period": {"from": "2025-10-20", "to": "2025-10-26"},
|
||||
"stats": {
|
||||
"logins": {"success": 45, "failed": 2},
|
||||
"passages": {"created": 120, "updated": 15, "deleted": 3},
|
||||
"sectors": {"created": 2, "updated": 8, "deleted": 0},
|
||||
"users": {"created": 1, "updated": 5, "deleted": 0}
|
||||
},
|
||||
"top_users": [
|
||||
{"user_id": 123, "actions": 85},
|
||||
{"user_id": 456, "actions": 42}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
#### 2. Stats globales super-admin
|
||||
|
||||
**Fichier** : `scripts/stats/global_stats.php`
|
||||
|
||||
**Usage** :
|
||||
```bash
|
||||
# Tous les passages modifiés sur 2 semaines
|
||||
php scripts/stats/global_stats.php --event=passage_updated --days=14
|
||||
|
||||
# Toutes les connexions échouées du mois
|
||||
php scripts/stats/global_stats.php --event=login_failed --month=2025-10
|
||||
|
||||
# Résultat JSON
|
||||
{
|
||||
"event": "passage_updated",
|
||||
"period": {"from": "2025-10-13", "to": "2025-10-26"},
|
||||
"total_events": 342,
|
||||
"by_entity": [
|
||||
{"entity_id": 5, "count": 120},
|
||||
{"entity_id": 12, "count": 85},
|
||||
{"entity_id": 18, "count": 67}
|
||||
],
|
||||
"by_day": {
|
||||
"2025-10-26": 45,
|
||||
"2025-10-25": 38,
|
||||
"2025-10-24": 52
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 3. Export CSV pour analyse externe
|
||||
|
||||
**Fichier** : `scripts/stats/export_events_csv.php`
|
||||
|
||||
**Usage** :
|
||||
```bash
|
||||
# Exporter toutes les connexions du mois en CSV
|
||||
php scripts/stats/export_events_csv.php \
|
||||
--event=login_success \
|
||||
--month=2025-10 \
|
||||
--output=/tmp/logins_october.csv
|
||||
```
|
||||
|
||||
### CRON de rotation
|
||||
|
||||
**Fichier** : `scripts/cron/rotate_event_logs.php`
|
||||
|
||||
**Configuration crontab** :
|
||||
```cron
|
||||
# Rotation des logs d'événements - 1er du mois à 3h
|
||||
0 3 1 * * cd /var/www/geosector/api && php scripts/cron/rotate_event_logs.php
|
||||
```
|
||||
|
||||
**Actions** :
|
||||
1. Compresser fichiers > 30 jours : `gzip logs/events/2025-09-*.jsonl`
|
||||
2. Supprimer archives > 15 mois : `rm logs/events/*-2024-06-*.jsonl.gz`
|
||||
3. Logger résumé dans `logs/rotation.log`
|
||||
|
||||
## 📈 Performances et volumétrie
|
||||
|
||||
### Estimations
|
||||
|
||||
**Volume quotidien moyen** (pour 50 entités actives) :
|
||||
- 500 connexions/jour = 500 lignes
|
||||
- 2000 passages créés/modifiés = 2000 lignes
|
||||
- 100 autres événements = 100 lignes
|
||||
- **Total : ~2600 événements/jour**
|
||||
|
||||
**Taille fichier** :
|
||||
- 1 événement ≈ 200-400 bytes JSON
|
||||
- 2600 événements ≈ 0.8-1 MB/jour non compressé
|
||||
- Compression gzip : ratio ~10:1 → **~100 KB/jour compressé**
|
||||
|
||||
**Rétention 15 mois** :
|
||||
- Non compressé (30 jours) : 30 MB
|
||||
- Compressé (14.5 mois) : 45 MB
|
||||
- **Total stockage : ~75 MB** pour 15 mois
|
||||
|
||||
### Optimisation lecture
|
||||
|
||||
**Lecture mono-fichier** : < 50ms pour analyser 1 jour (2600 événements)
|
||||
|
||||
**Lecture période 7 jours** :
|
||||
- 7 fichiers × 1 MB = 7 MB à lire
|
||||
- Filtrage `jq` ou PHP : ~200-300ms
|
||||
|
||||
**Lecture période 2 semaines (super-admin)** :
|
||||
- 14 fichiers × 1 MB = 14 MB à lire
|
||||
- Filtrage sur type événement : ~500ms
|
||||
|
||||
**Lecture archive compressée** :
|
||||
- Décompression à la volée : +100-200ms
|
||||
- Total : ~700-800ms pour 1 mois compressé
|
||||
|
||||
## 🔒 Sécurité et confidentialité
|
||||
|
||||
### Données sensibles
|
||||
|
||||
**❌ Jamais loggé en clair** :
|
||||
- Mots de passe
|
||||
- Contenu chiffré (noms, emails, téléphones, IBAN)
|
||||
- Tokens d'authentification
|
||||
|
||||
**✅ Loggé** :
|
||||
- IDs (user_id, entity_id, passage_id, etc.)
|
||||
- Montants financiers
|
||||
- Dates et timestamps
|
||||
- Types de modifications (indicateur booléen pour champs chiffrés)
|
||||
|
||||
### Exemple champ chiffré
|
||||
```json
|
||||
{
|
||||
"event": "user_updated",
|
||||
"changes": {
|
||||
"encrypted_name": true, // Indique modification sans valeur
|
||||
"encrypted_email": true,
|
||||
"role_id": {"old": 1, "new": 2} // Champ non sensible = valeurs OK
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Permissions d'accès
|
||||
|
||||
**Fichiers logs** :
|
||||
- Propriétaire : `nginx:nginx`
|
||||
- Permissions : `0640` (lecture nginx, écriture nginx, aucun autre)
|
||||
- Dossier `/logs/events/` : `0750`
|
||||
|
||||
**Scripts d'analyse** :
|
||||
- Exécution : root ou nginx uniquement
|
||||
- Pas d'accès direct via endpoints API (pour l'instant)
|
||||
|
||||
## 🚀 Roadmap et évolutions futures
|
||||
|
||||
### Phase 1 - MVP (actuel) ✅
|
||||
- [x] Architecture JSONL quotidienne
|
||||
- [x] Service EventLogService.php
|
||||
- [x] Intégration dans controllers (LoginController, PassageController, UserController, SectorController, OperationController, EntiteController)
|
||||
- [ ] CRON de rotation 15 mois
|
||||
- [ ] Scripts d'analyse de base
|
||||
|
||||
### Phase 2 - Dashboards (Q1 2026)
|
||||
- [ ] Endpoints API : `GET /api/stats/entity/{id}`, `GET /api/stats/global`
|
||||
- [ ] Interface web admin : graphiques connexions, passages
|
||||
- [ ] Filtres avancés (période, plateforme, utilisateur)
|
||||
|
||||
### Phase 3 - Alertes (Q2 2026)
|
||||
- [ ] Détection anomalies (pics de connexions échouées)
|
||||
- [ ] Alertes email super-admins
|
||||
- [ ] Seuils configurables par entité
|
||||
|
||||
### Phase 4 - Migration TimescaleDB (si besoin)
|
||||
- [ ] Évaluation volume : si > 50k événements/jour
|
||||
- [ ] Import JSONL → TimescaleDB
|
||||
- [ ] Rétention hybride : 90j TimescaleDB, archives JSONL
|
||||
|
||||
## 📝 Statut implémentation
|
||||
|
||||
**Date : 28 Octobre 2025**
|
||||
|
||||
### ✅ Terminé
|
||||
- Service `EventLogService.php` créé avec toutes les méthodes de logging
|
||||
- Intégration complète dans les 6 controllers principaux :
|
||||
- **LoginController** : login réussi/échoué, logout
|
||||
- **PassageController** : création, modification, suppression passages
|
||||
- **UserController** : création, modification, suppression utilisateurs
|
||||
- **SectorController** : création, modification, suppression secteurs
|
||||
- **OperationController** : création, modification, suppression opérations
|
||||
- **EntiteController** : création, modification entités
|
||||
- Enrichissement automatique : timestamp UTC, user_id, entity_id, IP, platform, app_version
|
||||
- Sécurité : champs sensibles loggés en booléen uniquement (pas de valeurs chiffrées)
|
||||
- Script de déploiement `deploy-api.sh` crée automatiquement `/logs/events/` avec permissions 0750
|
||||
|
||||
### 🔄 En attente
|
||||
- Scripts d'analyse (`entity_stats.php`, `global_stats.php`, `export_events_csv.php`)
|
||||
- CRON de rotation 15 mois (`rotate_event_logs.php`)
|
||||
- Tests en environnement DEV
|
||||
|
||||
## 📝 Checklist déploiement
|
||||
|
||||
### Environnement DEV (dva-geo)
|
||||
- [x] Créer dossier `/logs/events/` (permissions 0750) - Intégré dans deploy-api.sh
|
||||
- [x] Déployer `EventLogService.php`
|
||||
- [ ] Déployer scripts stats et rotation
|
||||
- [ ] Configurer CRON rotation
|
||||
- [ ] Tests : générer événements manuellement
|
||||
- [ ] Valider format JSONL et rotation
|
||||
|
||||
### Environnement RECETTE (rca-geo)
|
||||
- [ ] Déployer depuis DEV validé
|
||||
- [ ] Tests de charge : 10k événements/jour
|
||||
- [ ] Valider performances scripts d'analyse
|
||||
- [ ] Valider compression et suppression auto
|
||||
|
||||
### Environnement PRODUCTION (pra-geo)
|
||||
- [ ] Déployer depuis RECETTE validée
|
||||
- [ ] Monitoring volumétrie
|
||||
- [ ] Backups quotidiens `/logs/events/` (via CRON général)
|
||||
|
||||
---
|
||||
|
||||
**Dernière mise à jour :** 28 Octobre 2025
|
||||
**Version :** 1.1
|
||||
**Statut :** ✅ Service implémenté et intégré - Scripts d'analyse à développer
|
||||
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.
|
||||
699
api/docs/GESTION-SECTORS.md
Normal file
699
api/docs/GESTION-SECTORS.md
Normal file
@@ -0,0 +1,699 @@
|
||||
# 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 maria3/maria4)
|
||||
- **DVA** : maria3 (13.23.33.4) - base `adresses`
|
||||
- User : `adr_geo_user` / `d66,AdrGeoDev.User`
|
||||
- **RCA** : maria3 (13.23.33.4) - base `adresses`
|
||||
- User : `adr_geo_user` / `d66,AdrGeoRec.User`
|
||||
- **PROD** : maria4 (13.23.33.4) - base `adresses`
|
||||
- User : `adr_geo_user` / `d66,AdrGeoPrd.User`
|
||||
- Tables par département : `cp22`, `cp23`, etc.
|
||||
|
||||
3. **Base bâtiments** (dans conteneurs maria3/maria4)
|
||||
- **DVA** : maria3 (13.23.33.4) - base `batiments`
|
||||
- User : `adr_geo_user` / `d66,AdrGeoDev.User`
|
||||
- **RCA** : maria3 (13.23.33.4) - base `batiments`
|
||||
- User : `adr_geo_user` / `d66,AdrGeoRec.User`
|
||||
- **PROD** : maria4 (13.23.33.4) - base `batiments`
|
||||
- User : `adr_geo_user` / `d66,AdrGeoPrd.User`
|
||||
- Tables par département : `bat22`, `bat23`, etc.
|
||||
- Colonnes principales : `batiment_groupe_id`, `cle_interop_adr`, `nb_log`, `nb_niveau`, `residence`, `altitude_sol_mean`
|
||||
- Lien avec adresses : `bat{dept}.cle_interop_adr = cp{dept}.id`
|
||||
|
||||
### Configuration
|
||||
|
||||
Dans `src/Config/AppConfig.php` :
|
||||
|
||||
```php
|
||||
// DÉVELOPPEMENT
|
||||
'addresses_database' => [
|
||||
'host' => '13.23.33.4', // Container maria3 sur IN3
|
||||
'name' => 'adresses',
|
||||
'username' => 'adr_geo_user',
|
||||
'password' => 'd66,AdrGeoDev.User',
|
||||
],
|
||||
|
||||
// RECETTE
|
||||
'addresses_database' => [
|
||||
'host' => '13.23.33.4', // Container maria3 sur IN3
|
||||
'name' => 'adresses',
|
||||
'username' => 'adr_geo_user',
|
||||
'password' => 'd66,AdrGeoRec.User',
|
||||
],
|
||||
|
||||
// PRODUCTION
|
||||
'addresses_database' => [
|
||||
'host' => '13.23.33.4', // Container maria4 sur IN4
|
||||
'name' => 'adresses',
|
||||
'username' => 'adr_geo_user',
|
||||
'password' => 'd66,AdrGeoPrd.User',
|
||||
],
|
||||
|
||||
// DÉVELOPPEMENT - Bâtiments
|
||||
'buildings_database' => [
|
||||
'host' => '13.23.33.4', // Container maria3 sur IN3
|
||||
'name' => 'batiments',
|
||||
'username' => 'adr_geo_user',
|
||||
'password' => 'd66,AdrGeoDev.User',
|
||||
],
|
||||
|
||||
// RECETTE - Bâtiments
|
||||
'buildings_database' => [
|
||||
'host' => '13.23.33.4', // Container maria3 sur IN3
|
||||
'name' => 'batiments',
|
||||
'username' => 'adr_geo_user',
|
||||
'password' => 'd66,AdrGeoRec.User',
|
||||
],
|
||||
|
||||
// PRODUCTION - Bâtiments
|
||||
'buildings_database' => [
|
||||
'host' => '13.23.33.4', // Container maria4 sur IN4
|
||||
'name' => 'batiments',
|
||||
'username' => 'adr_geo_user',
|
||||
'password' => 'd66,AdrGeoPrd.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]
|
||||
]
|
||||
]
|
||||
```
|
||||
|
||||
### BuildingService
|
||||
|
||||
Enrichit les adresses avec les données bâtiments :
|
||||
|
||||
```php
|
||||
namespace App\Services;
|
||||
|
||||
class BuildingService {
|
||||
// Enrichit une liste d'adresses avec les métadonnées des bâtiments
|
||||
public function enrichAddresses(array $addresses): array
|
||||
}
|
||||
```
|
||||
|
||||
**Fonctionnement** :
|
||||
- Connexion à la base `batiments` externe
|
||||
- Interrogation des tables `bat{dept}` par département
|
||||
- JOIN sur `bat{dept}.cle_interop_adr = cp{dept}.id`
|
||||
- Ajout des métadonnées : `fk_batiment`, `fk_habitat`, `nb_niveau`, `nb_log`, `residence`, `alt_sol`
|
||||
- Fallback : `fk_habitat=1` (maison individuelle) si pas de bâtiment trouvé
|
||||
|
||||
**Données retournées** :
|
||||
```php
|
||||
[
|
||||
'id' => 'cp22.123456',
|
||||
'numero' => '10',
|
||||
'voie' => 'Rue Victor Hugo',
|
||||
'code_postal' => '22000',
|
||||
'commune' => 'Saint-Brieuc',
|
||||
'latitude' => 48.5149,
|
||||
'longitude' => -2.7658,
|
||||
// Données bâtiment enrichies :
|
||||
'fk_batiment' => 'BAT_123456', // null si maison
|
||||
'fk_habitat' => 2, // 1=individuel, 2=collectif
|
||||
'nb_niveau' => 4, // null si maison
|
||||
'nb_log' => 12, // null si maison
|
||||
'residence' => 'Résidence Les Pins', // '' si maison
|
||||
'alt_sol' => 25.5 // null si maison
|
||||
]
|
||||
```
|
||||
|
||||
## 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::getAddressesInPolygon()`
|
||||
8. **Enrichissement** avec données bâtiments via `AddressService::enrichAddressesWithBuildings()`
|
||||
9. **Stockage** des adresses dans `sectors_adresses` avec colonnes bâtiment :
|
||||
- `fk_batiment`, `fk_habitat`, `nb_niveau`, `nb_log`, `residence`, `alt_sol`
|
||||
10. **Création** des passages dans `ope_pass` :
|
||||
- **Maisons individuelles** (fk_habitat=1) : 1 passage par adresse
|
||||
- **Immeubles** (fk_habitat=2) : nb_log passages par adresse (1 par appartement)
|
||||
- Champs ajoutés : `residence`, `appt` (numéro 1 à nb_log), `fk_habitat`
|
||||
- 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
|
||||
11. **Commit** de la transaction ou **rollback** en cas d'erreur
|
||||
|
||||
## Processus de modification de secteur
|
||||
|
||||
### 1. Structure du payload UPDATE
|
||||
|
||||
```json
|
||||
{
|
||||
"libelle": "Secteur Centre-Ville Modifié",
|
||||
"color": "#00FF00",
|
||||
"sector": "48.117266/-1.6777926#48.118500/-1.6750000#...",
|
||||
"users": [12, 34],
|
||||
"chk_adresses_change": 1
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Paramètre chk_adresses_change
|
||||
|
||||
**Valeurs** :
|
||||
- `0` : Ne pas recalculer les adresses et passages (modification simple)
|
||||
- `1` : Recalculer les adresses et passages (défaut)
|
||||
|
||||
**Cas d'usage** :
|
||||
|
||||
#### chk_adresses_change = 0
|
||||
Modification rapide sans toucher aux adresses/passages :
|
||||
- ✅ Modification du libellé
|
||||
- ✅ Modification de la couleur
|
||||
- ✅ Modification des coordonnées du polygone (visuel uniquement)
|
||||
- ✅ Modification des membres affectés
|
||||
- ❌ Pas de recalcul des adresses dans sectors_adresses
|
||||
- ❌ Pas de mise à jour des passages (orphelins, créés, supprimés)
|
||||
- ❌ **Réponse sans passages_sector** (tableau vide)
|
||||
|
||||
**Utilité** : Permet aux admins de corriger rapidement un libellé, une couleur, ou d'ajuster légèrement le périmètre visuel sans déclencher un recalcul complet qui pourrait prendre plusieurs secondes.
|
||||
|
||||
**Réponse API** :
|
||||
```json
|
||||
{
|
||||
"status": "success",
|
||||
"message": "Secteur modifié avec succès",
|
||||
"sector": { "id": 123, "libelle": "...", "color": "...", "sector": "..." },
|
||||
"passages_sector": [], // Vide car chk_adresses_change = 0
|
||||
"passages_orphaned": 0,
|
||||
"passages_deleted": 0,
|
||||
"passages_updated": 0,
|
||||
"passages_created": 0,
|
||||
"passages_total": 0,
|
||||
"users_sectors": [...]
|
||||
}
|
||||
```
|
||||
|
||||
#### chk_adresses_change = 1 (défaut)
|
||||
Modification complète avec recalcul :
|
||||
- ✅ Modification du libellé/couleur/polygone
|
||||
- ✅ Modification des membres
|
||||
- ✅ Suppression et recréation de sectors_adresses
|
||||
- ✅ Application des règles de gestion des bâtiments
|
||||
- ✅ Mise en orphelin des passages hors périmètre
|
||||
- ✅ Création de nouveaux passages pour nouvelles adresses
|
||||
|
||||
### 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_adresse` : ID de l'adresse dans la base externe
|
||||
- `numero`, `rue`, `rue_bis`, `cp`, `ville`
|
||||
- `gps_lat`, `gps_lng`
|
||||
- **Colonnes bâtiment** :
|
||||
- `fk_batiment` : ID bâtiment (VARCHAR 50, null si maison)
|
||||
- `fk_habitat` : 1=individuel, 2=collectif (TINYINT UNSIGNED)
|
||||
- `nb_niveau` : Nombre d'étages (INT, null)
|
||||
- `nb_log` : Nombre de logements (INT, null)
|
||||
- `residence` : Nom résidence/copropriété (VARCHAR 75)
|
||||
- `alt_sol` : Altitude sol en mètres (DECIMAL 10,2, null)
|
||||
|
||||
### ope_pass (passages)
|
||||
- `fk_operation`, `fk_sector`, `fk_user`, `fk_adresse`
|
||||
- `numero`, `rue`, `rue_bis`, `ville`
|
||||
- `gps_lat`, `gps_lng`
|
||||
- **Colonnes bâtiment** :
|
||||
- `residence` : Nom résidence (VARCHAR 75)
|
||||
- `appt` : Numéro appartement (VARCHAR 10, saisie libre)
|
||||
- `niveau` : Étage (VARCHAR 10, saisie libre)
|
||||
- `fk_habitat` : 1=individuel, 2=collectif (TINYINT UNSIGNED)
|
||||
- `fk_type` : Type passage (2=à faire, autres valeurs pour fait/refus)
|
||||
- `encrypted_name`, `encrypted_email`, `encrypted_phone` : Données cryptées
|
||||
- `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`
|
||||
|
||||
## Règles de gestion des bâtiments lors de l'UPDATE
|
||||
|
||||
### Principe général
|
||||
|
||||
Lors de la mise à jour d'un secteur, le système applique une logique intelligente pour gérer les passages en fonction du type d'habitat (maison/immeuble) et du nombre de logements.
|
||||
|
||||
### Clé d'identification unique
|
||||
|
||||
**Tous les passages** sont identifiés par la clé : `numero|rue|rue_bis|ville`
|
||||
|
||||
Cette clé ne contient **pas** `residence` ni `appt` car ces champs sont en **saisie libre** par l'utilisateur.
|
||||
|
||||
### Cas 1 : Maison individuelle (fk_habitat=1)
|
||||
|
||||
#### Si 0 passage existant :
|
||||
```
|
||||
→ INSERT 1 nouveau passage
|
||||
- fk_habitat = 1
|
||||
- residence = ''
|
||||
- appt = ''
|
||||
```
|
||||
|
||||
#### Si 1+ passages existants :
|
||||
```
|
||||
→ UPDATE le premier passage
|
||||
- fk_habitat = 1
|
||||
- residence = ''
|
||||
→ Les autres passages restent INTACTS
|
||||
(peuvent correspondre à plusieurs habitants saisis manuellement)
|
||||
```
|
||||
|
||||
### Cas 2 : Immeuble (fk_habitat=2)
|
||||
|
||||
#### Étape 1 : UPDATE systématique
|
||||
```
|
||||
→ UPDATE TOUS les passages existants à cette adresse
|
||||
- fk_habitat = 2
|
||||
- residence = sectors_adresses.residence (si non vide)
|
||||
```
|
||||
|
||||
#### Étape 2a : Si nb_existants < nb_log (ex: 3 passages, nb_log=6)
|
||||
```
|
||||
→ INSERT (nb_log - nb_existants) nouveaux passages
|
||||
- fk_habitat = 2
|
||||
- residence = sectors_adresses.residence
|
||||
- appt = '' (pas de numéro prédéfini)
|
||||
- fk_type = 2 (à faire)
|
||||
|
||||
Résultat : 6 passages total (3 conservés + 3 créés)
|
||||
```
|
||||
|
||||
#### Étape 2b : Si nb_existants > nb_log (ex: 10 passages, nb_log=6)
|
||||
```
|
||||
→ DELETE max (nb_existants - nb_log) passages
|
||||
Conditions de suppression :
|
||||
- fk_type = 2 (à faire)
|
||||
- ET encrypted_name vide (non visité)
|
||||
- Tri par created_at ASC (les plus anciens d'abord)
|
||||
|
||||
Résultat : Entre 6 et 10 passages (selon combien sont visités)
|
||||
```
|
||||
|
||||
### Points importants
|
||||
|
||||
✅ **Préservation des données utilisateur** :
|
||||
- `appt` et `niveau` ne sont **JAMAIS modifiés** (saisie libre conservée)
|
||||
- Les passages visités (encrypted_name rempli) ne sont **JAMAIS supprimés**
|
||||
|
||||
✅ **Mise à jour conditionnelle** :
|
||||
- `residence` est mis à jour **uniquement si non vide** dans sectors_adresses
|
||||
- Permet de conserver une saisie manuelle si la base bâtiments n'a pas l'info
|
||||
|
||||
✅ **Gestion des transitions** :
|
||||
- Une adresse peut passer de maison (fk_habitat=1) à immeuble (fk_habitat=2) ou inversement
|
||||
- La logique s'adapte automatiquement au nouveau type d'habitat
|
||||
|
||||
✅ **Uniformisation GPS** :
|
||||
- **Tous les passages d'une même adresse partagent les mêmes coordonnées GPS** (gps_lat, gps_lng)
|
||||
- Ces coordonnées proviennent de `sectors_adresses` (enrichies depuis la base externe `adresses`)
|
||||
- Cette règle s'applique lors de la **création** et de la **mise à jour** avec `chk_adresses_change=1`
|
||||
- Garantit la cohérence géographique pour tous les passages d'un même immeuble
|
||||
|
||||
### Exemple concret
|
||||
|
||||
**Situation initiale** :
|
||||
- Adresse : "10 rue Victor Hugo, 22000 Saint-Brieuc"
|
||||
- 8 passages existants (dont 3 visités)
|
||||
- nb_log passe de 8 à 5
|
||||
|
||||
**Actions** :
|
||||
1. UPDATE les 8 passages → fk_habitat=2, residence="Les Chênes"
|
||||
2. Tentative suppression de (8-5) = 3 passages
|
||||
3. Recherche des passages avec fk_type=2 ET encrypted_name vide
|
||||
4. Suppose 5 passages non visités trouvés
|
||||
5. Suppression des 3 plus anciens non visités
|
||||
6. **Résultat** : 5 passages restants (3 visités + 2 non visités)
|
||||
|
||||
## 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
|
||||
818
api/docs/PLANNING-STRIPE-API.md
Normal file
818
api/docs/PLANNING-STRIPE-API.md
Normal file
@@ -0,0 +1,818 @@
|
||||
# PLANNING STRIPE - DÉVELOPPEUR BACKEND PHP
|
||||
## API PHP 8.3 - Intégration Stripe Tap to Pay (Mobile uniquement)
|
||||
### Période : 25/08/2024 - 05/09/2024
|
||||
### Mise à jour : Janvier 2025 - Simplification architecture
|
||||
|
||||
---
|
||||
|
||||
## 📅 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
|
||||
-- Modification de la table ope_pass existante (JANVIER 2025)
|
||||
ALTER TABLE `ope_pass`
|
||||
DROP COLUMN IF EXISTS `is_striped`,
|
||||
ADD COLUMN `stripe_payment_id` VARCHAR(50) DEFAULT NULL COMMENT 'ID du PaymentIntent Stripe (pi_xxx)',
|
||||
ADD INDEX `idx_stripe_payment` (`stripe_payment_id`);
|
||||
|
||||
-- Tables à créer (simplifiées)
|
||||
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)
|
||||
);
|
||||
|
||||
-- NOTE: Table payment_intents SUPPRIMÉE - on utilise directement stripe_payment_id dans ope_pass
|
||||
-- NOTE: Table terminal_readers SUPPRIMÉE - Tap to Pay uniquement, pas de terminaux externes
|
||||
|
||||
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);
|
||||
}
|
||||
```
|
||||
|
||||
#### ✅ Configuration Tap to Pay
|
||||
```php
|
||||
// POST /api/stripe/tap-to-pay/init
|
||||
public function initTapToPay(Request $request) {
|
||||
$userId = Session::getUserId();
|
||||
$entityId = Session::getEntityId();
|
||||
|
||||
// Vérifier que l'entité a un compte Stripe
|
||||
$account = $this->getStripeAccount($entityId);
|
||||
|
||||
return [
|
||||
'stripe_account_id' => $account->stripe_account_id,
|
||||
'tap_to_pay_enabled' => true
|
||||
];
|
||||
}
|
||||
```
|
||||
|
||||
### 🌆 Après-midi (4h)
|
||||
|
||||
#### ✅ Vérification compatibilité Device
|
||||
```php
|
||||
// POST /api/stripe/devices/check-tap-to-pay
|
||||
public function checkTapToPayCapability(Request $request) {
|
||||
$platform = $request->input('platform');
|
||||
$model = $request->input('device_model');
|
||||
$osVersion = $request->input('os_version');
|
||||
|
||||
if ($platform === 'iOS') {
|
||||
// iPhone XS et ultérieurs avec iOS 16.4+
|
||||
$supported = $this->checkiOSCompatibility($model, $osVersion);
|
||||
} else {
|
||||
// Android certifié pour la France
|
||||
$supported = $this->checkAndroidCertification($model);
|
||||
}
|
||||
|
||||
return [
|
||||
'tap_to_pay_supported' => $supported,
|
||||
'message' => $supported ?
|
||||
'Tap to Pay disponible' :
|
||||
'Appareil non compatible'
|
||||
];
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📅 MERCREDI 27/08 - Paiements et fees (8h)
|
||||
|
||||
### 🌅 Matin (4h)
|
||||
|
||||
#### ✅ Création PaymentIntent avec association au passage
|
||||
```php
|
||||
// POST /api/payments/create-intent
|
||||
public function createPaymentIntent(Request $request) {
|
||||
$validated = $request->validate([
|
||||
'amount' => 'required|integer|min:100', // en centimes
|
||||
'passage_id' => 'required|integer', // ID du passage ope_pass
|
||||
'entity_id' => 'required|integer',
|
||||
]);
|
||||
|
||||
$userId = Session::getUserId();
|
||||
$entity = $this->getEntity($validated['entity_id']);
|
||||
|
||||
// Commission à 0% (décision client)
|
||||
$applicationFee = 0;
|
||||
|
||||
$paymentIntent = \Stripe\PaymentIntent::create([
|
||||
'amount' => $validated['amount'],
|
||||
'currency' => 'eur',
|
||||
'payment_method_types' => ['card_present'],
|
||||
'capture_method' => 'automatic',
|
||||
'application_fee_amount' => $applicationFee,
|
||||
'transfer_data' => [
|
||||
'destination' => $entity->stripe_account_id,
|
||||
],
|
||||
'metadata' => [
|
||||
'passage_id' => $validated['passage_id'],
|
||||
'user_id' => $userId,
|
||||
'entity_id' => $entity->id,
|
||||
'year' => date('Y'),
|
||||
],
|
||||
]);
|
||||
|
||||
// Mise à jour directe dans ope_pass
|
||||
$this->db->prepare("
|
||||
UPDATE ope_pass
|
||||
SET stripe_payment_id = :stripe_id,
|
||||
date_modified = NOW()
|
||||
WHERE id = :passage_id
|
||||
")->execute([
|
||||
':stripe_id' => $paymentIntent->id,
|
||||
':passage_id' => $validated['passage_id']
|
||||
]);
|
||||
|
||||
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) {
|
||||
// Récupérer le passage depuis ope_pass
|
||||
$stmt = $this->db->prepare("
|
||||
SELECT id, stripe_payment_id, montant
|
||||
FROM ope_pass
|
||||
WHERE stripe_payment_id = :stripe_id
|
||||
");
|
||||
$stmt->execute([':stripe_id' => $paymentIntentId]);
|
||||
$passage = $stmt->fetch();
|
||||
|
||||
$paymentIntent = \Stripe\PaymentIntent::retrieve($paymentIntentId);
|
||||
|
||||
if ($paymentIntent->status === 'requires_capture') {
|
||||
$paymentIntent->capture();
|
||||
}
|
||||
|
||||
// Mettre à jour le statut dans ope_pass si nécessaire
|
||||
if ($paymentIntent->status === 'succeeded' && $passage) {
|
||||
$this->db->prepare("
|
||||
UPDATE ope_pass
|
||||
SET date_stripe_validated = NOW()
|
||||
WHERE id = :passage_id
|
||||
")->execute([':passage_id' => $passage['id']]);
|
||||
|
||||
// Envoyer email reçu si configuré
|
||||
$this->sendReceipt($passage['id']);
|
||||
}
|
||||
|
||||
return $paymentIntent;
|
||||
}
|
||||
|
||||
// GET /api/passages/{id}/stripe-status
|
||||
public function getPassageStripeStatus($passageId) {
|
||||
$stmt = $this->db->prepare("
|
||||
SELECT stripe_payment_id, montant, date_creat
|
||||
FROM ope_pass
|
||||
WHERE id = :id
|
||||
");
|
||||
$stmt->execute([':id' => $passageId]);
|
||||
$passage = $stmt->fetch();
|
||||
|
||||
if (!$passage['stripe_payment_id']) {
|
||||
return ['status' => 'no_stripe_payment'];
|
||||
}
|
||||
|
||||
// Récupérer le statut depuis Stripe
|
||||
$paymentIntent = \Stripe\PaymentIntent::retrieve($passage['stripe_payment_id']);
|
||||
|
||||
return [
|
||||
'stripe_payment_id' => $passage['stripe_payment_id'],
|
||||
'status' => $paymentIntent->status,
|
||||
'amount' => $paymentIntent->amount,
|
||||
'currency' => $paymentIntent->currency,
|
||||
'created_at' => $passage['date_creat']
|
||||
];
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📅 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 (TAP TO PAY UNIQUEMENT)
|
||||
|
||||
#### **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
|
||||
|
||||
#### **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 (MISE À JOUR JANVIER 2025)**
|
||||
- **Modification table `ope_pass`** : `stripe_payment_id` VARCHAR(50) remplace `is_striped`
|
||||
- **Table `payment_intents` supprimée** : Intégration directe dans `ope_pass`
|
||||
- Utilisation tables existantes (entites)
|
||||
- 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
|
||||
|
||||
---
|
||||
|
||||
## 📱 FLOW TAP TO PAY SIMPLIFIÉ (Janvier 2025)
|
||||
|
||||
### Architecture
|
||||
```
|
||||
Flutter App (Tap to Pay) ↔ API PHP ↔ Stripe API
|
||||
```
|
||||
|
||||
### Étape 1: Création PaymentIntent
|
||||
**Flutter → API**
|
||||
```json
|
||||
POST /api/stripe/payments/create-intent
|
||||
{
|
||||
"amount": 1500,
|
||||
"passage_id": 123,
|
||||
"entity_id": 5
|
||||
}
|
||||
```
|
||||
|
||||
**API → Stripe → Base de données**
|
||||
```php
|
||||
// 1. Créer le PaymentIntent
|
||||
$paymentIntent = Stripe\PaymentIntent::create([...]);
|
||||
|
||||
// 2. Sauvegarder dans ope_pass
|
||||
UPDATE ope_pass SET stripe_payment_id = 'pi_xxx' WHERE id = 123;
|
||||
```
|
||||
|
||||
**API → Flutter**
|
||||
```json
|
||||
{
|
||||
"client_secret": "pi_xxx_secret_yyy",
|
||||
"payment_intent_id": "pi_xxx"
|
||||
}
|
||||
```
|
||||
|
||||
### Étape 2: Collecte du paiement (Flutter)
|
||||
- L'app Flutter utilise le SDK Stripe Terminal
|
||||
- Le téléphone devient le terminal de paiement (Tap to Pay)
|
||||
- Utilise le client_secret pour collecter le paiement
|
||||
|
||||
### Étape 3: Confirmation (Webhook)
|
||||
**Stripe → API**
|
||||
- Event: `payment_intent.succeeded`
|
||||
- Met à jour le statut dans la base de données
|
||||
|
||||
### Tables nécessaires
|
||||
- ✅ `ope_pass.stripe_payment_id` - Association passage/paiement
|
||||
- ✅ `stripe_accounts` - Comptes Connect des amicales
|
||||
- ✅ `android_certified_devices` - Vérification compatibilité
|
||||
- ❌ ~~`stripe_payment_intents`~~ - Supprimée
|
||||
- ❌ ~~`terminal_readers`~~ - Pas de terminaux externes
|
||||
|
||||
### Endpoints essentiels
|
||||
1. `POST /api/stripe/payments/create-intent` - Créer PaymentIntent
|
||||
2. `POST /api/stripe/devices/check-tap-to-pay` - Vérifier compatibilité
|
||||
3. `POST /api/stripe/webhook` - Recevoir confirmations
|
||||
4. `GET /api/passages/{id}/stripe-status` - Vérifier statut
|
||||
|
||||
---
|
||||
|
||||
## 📝 CHANGELOG
|
||||
|
||||
### Janvier 2025 - Refactoring base de données
|
||||
- **Suppression** de la table `payment_intents` (non nécessaire)
|
||||
- **Migration** : `is_striped` → `stripe_payment_id` VARCHAR(50) dans `ope_pass`
|
||||
- **Simplification** : Association directe PaymentIntent ↔ Passage
|
||||
- **Avantage** : Traçabilité directe sans table intermédiaire
|
||||
|
||||
---
|
||||
|
||||
*Document créé le 24/08/2024 - Dernière mise à jour : 09/01/2025*
|
||||
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
|
||||
464
api/docs/STRIPE-BACKEND-MIGRATION.md
Normal file
464
api/docs/STRIPE-BACKEND-MIGRATION.md
Normal file
@@ -0,0 +1,464 @@
|
||||
# 🔧 Migration Backend Stripe - Option A (Tout en 1)
|
||||
|
||||
## 📋 Objectif
|
||||
|
||||
Optimiser la création de compte Stripe Connect en **1 seule requête** côté Flutter qui crée :
|
||||
1. Le compte Stripe Connect
|
||||
2. La Location Terminal
|
||||
3. Le lien d'onboarding
|
||||
|
||||
---
|
||||
|
||||
## 🗄️ 1. Modification de la base de données
|
||||
|
||||
### **Ajouter la colonne `stripe_location_id`**
|
||||
|
||||
```sql
|
||||
ALTER TABLE amicales
|
||||
ADD COLUMN stripe_location_id VARCHAR(255) NULL
|
||||
AFTER stripe_id;
|
||||
```
|
||||
|
||||
**Vérification** :
|
||||
```sql
|
||||
DESCRIBE amicales;
|
||||
```
|
||||
|
||||
Doit afficher :
|
||||
```
|
||||
+-------------------+--------------+------+-----+---------+-------+
|
||||
| Field | Type | Null | Key | Default | Extra |
|
||||
+-------------------+--------------+------+-----+---------+-------+
|
||||
| stripe_id | varchar(255) | YES | | NULL | |
|
||||
| stripe_location_id| varchar(255) | YES | | NULL | |
|
||||
+-------------------+--------------+------+-----+---------+-------+
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🔧 2. Modification de l'endpoint `POST /stripe/accounts`
|
||||
|
||||
### **Fichier** : `app/Http/Controllers/StripeController.php` (ou similaire)
|
||||
|
||||
### **Méthode** : `createAccount()` ou `store()`
|
||||
|
||||
### **Code proposé** :
|
||||
|
||||
```php
|
||||
<?php
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use App\Models\Amicale;
|
||||
|
||||
/**
|
||||
* Créer un compte Stripe Connect avec Location Terminal et lien d'onboarding
|
||||
*
|
||||
* @param Request $request
|
||||
* @return \Illuminate\Http\JsonResponse
|
||||
*/
|
||||
public function createStripeAccount(Request $request)
|
||||
{
|
||||
$request->validate([
|
||||
'fk_entite' => 'required|integer|exists:amicales,id',
|
||||
'return_url' => 'required|string|url',
|
||||
'refresh_url' => 'required|string|url',
|
||||
]);
|
||||
|
||||
$fkEntite = $request->fk_entite;
|
||||
$amicale = Amicale::findOrFail($fkEntite);
|
||||
|
||||
// Vérifier si un compte existe déjà
|
||||
if (!empty($amicale->stripe_id)) {
|
||||
return $this->handleExistingAccount($amicale, $request);
|
||||
}
|
||||
|
||||
DB::beginTransaction();
|
||||
|
||||
try {
|
||||
// Configurer la clé Stripe (selon environnement)
|
||||
\Stripe\Stripe::setApiKey(config('services.stripe.secret'));
|
||||
|
||||
// 1️⃣ Créer le compte Stripe Connect Express
|
||||
$account = \Stripe\Account::create([
|
||||
'type' => 'express',
|
||||
'country' => 'FR',
|
||||
'email' => $amicale->email,
|
||||
'business_type' => 'non_profit', // ou 'company' selon le cas
|
||||
'business_profile' => [
|
||||
'name' => $amicale->name,
|
||||
'url' => config('app.url'),
|
||||
],
|
||||
'capabilities' => [
|
||||
'card_payments' => ['requested' => true],
|
||||
'transfers' => ['requested' => true],
|
||||
],
|
||||
]);
|
||||
|
||||
\Log::info('Stripe account created', [
|
||||
'amicale_id' => $amicale->id,
|
||||
'account_id' => $account->id,
|
||||
]);
|
||||
|
||||
// 2️⃣ Créer la Location Terminal pour Tap to Pay
|
||||
$location = \Stripe\Terminal\Location::create([
|
||||
'display_name' => $amicale->name,
|
||||
'address' => [
|
||||
'line1' => $amicale->adresse1 ?: 'Non renseigné',
|
||||
'line2' => $amicale->adresse2,
|
||||
'city' => $amicale->ville ?: 'Non renseigné',
|
||||
'postal_code' => $amicale->code_postal ?: '00000',
|
||||
'country' => 'FR',
|
||||
],
|
||||
], [
|
||||
'stripe_account' => $account->id, // ← Important : Connect account
|
||||
]);
|
||||
|
||||
\Log::info('Stripe Terminal Location created', [
|
||||
'amicale_id' => $amicale->id,
|
||||
'location_id' => $location->id,
|
||||
]);
|
||||
|
||||
// 3️⃣ Créer le lien d'onboarding
|
||||
$accountLink = \Stripe\AccountLink::create([
|
||||
'account' => $account->id,
|
||||
'refresh_url' => $request->refresh_url,
|
||||
'return_url' => $request->return_url,
|
||||
'type' => 'account_onboarding',
|
||||
]);
|
||||
|
||||
\Log::info('Stripe onboarding link created', [
|
||||
'amicale_id' => $amicale->id,
|
||||
'account_id' => $account->id,
|
||||
]);
|
||||
|
||||
// 4️⃣ Sauvegarder TOUT en base de données
|
||||
$amicale->stripe_id = $account->id;
|
||||
$amicale->stripe_location_id = $location->id;
|
||||
$amicale->chk_stripe = true;
|
||||
$amicale->save();
|
||||
|
||||
DB::commit();
|
||||
|
||||
// 5️⃣ Retourner TOUTES les informations
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'account_id' => $account->id,
|
||||
'location_id' => $location->id,
|
||||
'onboarding_url' => $accountLink->url,
|
||||
'charges_enabled' => $account->charges_enabled,
|
||||
'payouts_enabled' => $account->payouts_enabled,
|
||||
'existing' => false,
|
||||
'message' => 'Compte Stripe Connect créé avec succès',
|
||||
], 201);
|
||||
|
||||
} catch (\Stripe\Exception\ApiErrorException $e) {
|
||||
DB::rollBack();
|
||||
|
||||
\Log::error('Stripe API error', [
|
||||
'amicale_id' => $amicale->id,
|
||||
'error' => $e->getMessage(),
|
||||
'type' => get_class($e),
|
||||
]);
|
||||
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => 'Erreur Stripe : ' . $e->getMessage(),
|
||||
], 500);
|
||||
|
||||
} catch (\Exception $e) {
|
||||
DB::rollBack();
|
||||
|
||||
\Log::error('Stripe account creation failed', [
|
||||
'amicale_id' => $amicale->id,
|
||||
'error' => $e->getMessage(),
|
||||
'trace' => $e->getTraceAsString(),
|
||||
]);
|
||||
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => 'Erreur lors de la création du compte Stripe',
|
||||
], 500);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gérer le cas d'un compte Stripe existant
|
||||
*/
|
||||
private function handleExistingAccount(Amicale $amicale, Request $request)
|
||||
{
|
||||
try {
|
||||
\Stripe\Stripe::setApiKey(config('services.stripe.secret'));
|
||||
|
||||
// Récupérer les infos du compte existant
|
||||
$account = \Stripe\Account::retrieve($amicale->stripe_id);
|
||||
|
||||
// Si pas de location_id, la créer maintenant
|
||||
if (empty($amicale->stripe_location_id)) {
|
||||
$location = \Stripe\Terminal\Location::create([
|
||||
'display_name' => $amicale->name,
|
||||
'address' => [
|
||||
'line1' => $amicale->adresse1 ?: 'Non renseigné',
|
||||
'city' => $amicale->ville ?: 'Non renseigné',
|
||||
'postal_code' => $amicale->code_postal ?: '00000',
|
||||
'country' => 'FR',
|
||||
],
|
||||
], [
|
||||
'stripe_account' => $amicale->stripe_id,
|
||||
]);
|
||||
|
||||
$amicale->stripe_location_id = $location->id;
|
||||
$amicale->save();
|
||||
|
||||
\Log::info('Location created for existing account', [
|
||||
'amicale_id' => $amicale->id,
|
||||
'location_id' => $location->id,
|
||||
]);
|
||||
}
|
||||
|
||||
// Si le compte est déjà complètement configuré
|
||||
if ($account->charges_enabled && $account->payouts_enabled) {
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'account_id' => $amicale->stripe_id,
|
||||
'location_id' => $amicale->stripe_location_id,
|
||||
'onboarding_url' => null,
|
||||
'charges_enabled' => true,
|
||||
'payouts_enabled' => true,
|
||||
'existing' => true,
|
||||
'message' => 'Compte Stripe déjà configuré et actif',
|
||||
]);
|
||||
}
|
||||
|
||||
// Compte existant mais configuration incomplète : générer un nouveau lien
|
||||
$accountLink = \Stripe\AccountLink::create([
|
||||
'account' => $amicale->stripe_id,
|
||||
'refresh_url' => $request->refresh_url,
|
||||
'return_url' => $request->return_url,
|
||||
'type' => 'account_onboarding',
|
||||
]);
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'account_id' => $amicale->stripe_id,
|
||||
'location_id' => $amicale->stripe_location_id,
|
||||
'onboarding_url' => $accountLink->url,
|
||||
'charges_enabled' => $account->charges_enabled,
|
||||
'payouts_enabled' => $account->payouts_enabled,
|
||||
'existing' => true,
|
||||
'message' => 'Compte existant, configuration à finaliser',
|
||||
]);
|
||||
|
||||
} catch (\Exception $e) {
|
||||
\Log::error('Error handling existing account', [
|
||||
'amicale_id' => $amicale->id,
|
||||
'error' => $e->getMessage(),
|
||||
]);
|
||||
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => 'Erreur lors de la vérification du compte existant',
|
||||
], 500);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📡 3. Modification de l'endpoint `GET /stripe/accounts/{id}/status`
|
||||
|
||||
Ajouter `location_id` dans la réponse :
|
||||
|
||||
```php
|
||||
public function checkAccountStatus($amicaleId)
|
||||
{
|
||||
$amicale = Amicale::findOrFail($amicaleId);
|
||||
|
||||
if (empty($amicale->stripe_id)) {
|
||||
return response()->json([
|
||||
'has_account' => false,
|
||||
'account_id' => null,
|
||||
'location_id' => null,
|
||||
'charges_enabled' => false,
|
||||
'payouts_enabled' => false,
|
||||
'onboarding_completed' => false,
|
||||
]);
|
||||
}
|
||||
|
||||
try {
|
||||
\Stripe\Stripe::setApiKey(config('services.stripe.secret'));
|
||||
$account = \Stripe\Account::retrieve($amicale->stripe_id);
|
||||
|
||||
return response()->json([
|
||||
'has_account' => true,
|
||||
'account_id' => $amicale->stripe_id,
|
||||
'location_id' => $amicale->stripe_location_id, // ← Ajouté
|
||||
'charges_enabled' => $account->charges_enabled,
|
||||
'payouts_enabled' => $account->payouts_enabled,
|
||||
'onboarding_completed' => $account->details_submitted,
|
||||
]);
|
||||
|
||||
} catch (\Exception $e) {
|
||||
return response()->json([
|
||||
'has_account' => false,
|
||||
'error' => $e->getMessage(),
|
||||
], 500);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🗑️ 4. Endpoint à SUPPRIMER (devenu inutile)
|
||||
|
||||
### **❌ `POST /stripe/locations`**
|
||||
|
||||
Cet endpoint n'est plus nécessaire car la Location est créée automatiquement dans `POST /stripe/accounts`.
|
||||
|
||||
**Option 1** : Supprimer complètement
|
||||
**Option 2** : Le garder pour compatibilité temporaire (si utilisé ailleurs)
|
||||
|
||||
---
|
||||
|
||||
## 📝 5. Modification du modèle Eloquent
|
||||
|
||||
### **Fichier** : `app/Models/Amicale.php`
|
||||
|
||||
Ajouter le champ `stripe_location_id` :
|
||||
|
||||
```php
|
||||
protected $fillable = [
|
||||
// ... autres champs
|
||||
'stripe_id',
|
||||
'stripe_location_id', // ← Ajouté
|
||||
'chk_stripe',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'chk_stripe' => 'boolean',
|
||||
];
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ✅ 6. Tests à effectuer
|
||||
|
||||
### **Test 1 : Nouvelle amicale**
|
||||
```bash
|
||||
curl -X POST http://localhost/api/stripe/accounts \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "Authorization: Bearer {token}" \
|
||||
-d '{
|
||||
"fk_entite": 123,
|
||||
"return_url": "https://app.geosector.fr/stripe/success",
|
||||
"refresh_url": "https://app.geosector.fr/stripe/refresh"
|
||||
}'
|
||||
```
|
||||
|
||||
**Réponse attendue** :
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"account_id": "acct_xxxxxxxxxxxxx",
|
||||
"location_id": "tml_xxxxxxxxxxxxx",
|
||||
"onboarding_url": "https://connect.stripe.com/setup/...",
|
||||
"charges_enabled": false,
|
||||
"payouts_enabled": false,
|
||||
"existing": false,
|
||||
"message": "Compte Stripe Connect créé avec succès"
|
||||
}
|
||||
```
|
||||
|
||||
### **Test 2 : Amicale avec compte existant**
|
||||
```bash
|
||||
curl -X POST http://localhost/api/stripe/accounts \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "Authorization: Bearer {token}" \
|
||||
-d '{
|
||||
"fk_entite": 456,
|
||||
"return_url": "https://app.geosector.fr/stripe/success",
|
||||
"refresh_url": "https://app.geosector.fr/stripe/refresh"
|
||||
}'
|
||||
```
|
||||
|
||||
**Réponse attendue** :
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"account_id": "acct_xxxxxxxxxxxxx",
|
||||
"location_id": "tml_xxxxxxxxxxxxx",
|
||||
"onboarding_url": null,
|
||||
"charges_enabled": true,
|
||||
"payouts_enabled": true,
|
||||
"existing": true,
|
||||
"message": "Compte Stripe déjà configuré et actif"
|
||||
}
|
||||
```
|
||||
|
||||
### **Test 3 : Vérifier la BDD**
|
||||
```sql
|
||||
SELECT id, name, stripe_id, stripe_location_id, chk_stripe
|
||||
FROM amicales
|
||||
WHERE id = 123;
|
||||
```
|
||||
|
||||
**Résultat attendu** :
|
||||
```
|
||||
+-----+------------------+-------------------+-------------------+------------+
|
||||
| id | name | stripe_id | stripe_location_id| chk_stripe |
|
||||
+-----+------------------+-------------------+-------------------+------------+
|
||||
| 123 | Pompiers Paris15 | acct_xxxxxxxxxxxxx| tml_xxxxxxxxxxxxx | 1 |
|
||||
+-----+------------------+-------------------+-------------------+------------+
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🚀 7. Déploiement
|
||||
|
||||
### **Étapes** :
|
||||
1. ✅ Appliquer la migration SQL
|
||||
2. ✅ Déployer le code Backend modifié
|
||||
3. ✅ Tester avec Postman/curl
|
||||
4. ✅ Déployer le code Flutter modifié
|
||||
5. ✅ Tester le flow complet depuis l'app
|
||||
|
||||
---
|
||||
|
||||
## 📊 Comparaison Avant/Après
|
||||
|
||||
| Aspect | Avant | Après |
|
||||
|--------|-------|-------|
|
||||
| **Appels API Flutter → Backend** | 3 | 1 |
|
||||
| **Appels Backend → Stripe** | 3 | 3 (mais atomiques) |
|
||||
| **Latence totale** | ~3-5s | ~1-2s |
|
||||
| **Gestion erreurs** | Complexe | Simplifié avec transaction |
|
||||
| **Atomicité** | ❌ Non | ✅ Oui (DB transaction) |
|
||||
| **Location ID sauvegardé** | ❌ Non | ✅ Oui |
|
||||
|
||||
---
|
||||
|
||||
## 🎯 Bénéfices
|
||||
|
||||
1. ✅ **Performance** : Latence divisée par 2-3
|
||||
2. ✅ **Fiabilité** : Transaction BDD garantit la cohérence
|
||||
3. ✅ **Simplicité** : Code Flutter plus simple
|
||||
4. ✅ **Maintenance** : Moins de code à maintenir
|
||||
5. ✅ **Traçabilité** : Logs centralisés côté Backend
|
||||
6. ✅ **Tap to Pay prêt** : `location_id` disponible immédiatement
|
||||
|
||||
---
|
||||
|
||||
## ⚠️ Points d'attention
|
||||
|
||||
1. **Rollback** : Si la transaction échoue, rien n'est sauvegardé (bon comportement)
|
||||
2. **Logs** : Bien logger chaque étape pour le debug
|
||||
3. **Stripe Connect limitations** : Respecter les rate limits Stripe
|
||||
4. **Tests** : Tester avec des comptes Stripe de test d'abord
|
||||
|
||||
---
|
||||
|
||||
## 📚 Ressources
|
||||
|
||||
- [Stripe Connect Express Accounts](https://stripe.com/docs/connect/express-accounts)
|
||||
- [Stripe Terminal Locations](https://stripe.com/docs/terminal/fleet/locations)
|
||||
- [Stripe Account Links](https://stripe.com/docs/connect/account-links)
|
||||
343
api/docs/STRIPE-TAP-TO-PAY-FLOW.md
Normal file
343
api/docs/STRIPE-TAP-TO-PAY-FLOW.md
Normal file
@@ -0,0 +1,343 @@
|
||||
# Flow de paiement Stripe Tap to Pay
|
||||
|
||||
## Vue d'ensemble
|
||||
|
||||
Ce document décrit le flow complet pour les paiements Stripe Tap to Pay dans l'application GeoSector, depuis la création du compte Stripe Connect jusqu'au paiement final.
|
||||
|
||||
---
|
||||
|
||||
## 🏢 PRÉALABLE : Création d'un compte Amicale Stripe Connect
|
||||
|
||||
Avant de pouvoir utiliser les paiements Stripe, chaque amicale doit créer son compte Stripe Connect.
|
||||
|
||||
### 📋 Flow de création du compte
|
||||
|
||||
#### 1. Initiation depuis l'application web admin
|
||||
|
||||
**Endpoint :** `POST /api/stripe/accounts/create`
|
||||
|
||||
**Requête :**
|
||||
```json
|
||||
{
|
||||
"amicale_id": 45,
|
||||
"type": "express", // Type de compte Stripe Connect
|
||||
"country": "FR",
|
||||
"email": "contact@amicale-pompiers-paris.fr",
|
||||
"business_profile": {
|
||||
"name": "Amicale des Pompiers de Paris",
|
||||
"product_description": "Vente de calendriers des pompiers",
|
||||
"mcc": "8398", // Code activité : organisations civiques
|
||||
"url": "https://www.amicale-pompiers-paris.fr"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 2. Création du compte Stripe
|
||||
|
||||
**Actions API :**
|
||||
1. Appel Stripe API pour créer un compte Express
|
||||
2. Génération d'un lien d'onboarding personnalisé
|
||||
3. Sauvegarde en base de données
|
||||
|
||||
**Réponse :**
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"stripe_account_id": "acct_1O3ABC456DEF789",
|
||||
"onboarding_url": "https://connect.stripe.com/express/oauth/authorize?...",
|
||||
"status": "pending"
|
||||
}
|
||||
```
|
||||
|
||||
#### 3. Processus d'onboarding Stripe
|
||||
|
||||
**Actions utilisateur (dirigeant amicale) :**
|
||||
1. Clic sur le lien d'onboarding
|
||||
2. Connexion/création compte Stripe
|
||||
3. Saisie des informations légales :
|
||||
- **Entité** : Association loi 1901
|
||||
- **SIRET** de l'amicale
|
||||
- **RIB** pour les virements
|
||||
- **Pièce d'identité** du représentant légal
|
||||
4. Validation des conditions d'utilisation
|
||||
|
||||
#### 4. Vérification et activation
|
||||
|
||||
**Webhook Stripe → API :**
|
||||
```json
|
||||
POST /api/stripe/webhooks
|
||||
{
|
||||
"type": "account.updated",
|
||||
"data": {
|
||||
"object": {
|
||||
"id": "acct_1O3ABC456DEF789",
|
||||
"charges_enabled": true,
|
||||
"payouts_enabled": true,
|
||||
"details_submitted": true
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Actions API :**
|
||||
1. Mise à jour du statut en base
|
||||
2. Notification email à l'amicale
|
||||
3. Activation des fonctionnalités de paiement
|
||||
|
||||
#### 5. Structure en base de données
|
||||
|
||||
**Table `stripe_accounts` :**
|
||||
```sql
|
||||
CREATE TABLE `stripe_accounts` (
|
||||
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
|
||||
`fk_entite` int(10) unsigned NOT NULL,
|
||||
`stripe_account_id` varchar(50) NOT NULL,
|
||||
`account_type` enum('express','standard','custom') DEFAULT 'express',
|
||||
`charges_enabled` tinyint(1) DEFAULT 0,
|
||||
`payouts_enabled` tinyint(1) DEFAULT 0,
|
||||
`details_submitted` tinyint(1) DEFAULT 0,
|
||||
`country` varchar(2) DEFAULT 'FR',
|
||||
`default_currency` varchar(3) DEFAULT 'eur',
|
||||
`business_name` varchar(255) DEFAULT NULL,
|
||||
`support_email` varchar(255) DEFAULT NULL,
|
||||
`onboarding_completed_at` timestamp NULL DEFAULT NULL,
|
||||
`created_at` timestamp NOT NULL DEFAULT current_timestamp(),
|
||||
`updated_at` timestamp NULL DEFAULT NULL ON UPDATE current_timestamp(),
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `stripe_account_id` (`stripe_account_id`),
|
||||
KEY `fk_entite` (`fk_entite`),
|
||||
CONSTRAINT `stripe_accounts_ibfk_1` FOREIGN KEY (`fk_entite`) REFERENCES `entites` (`id`)
|
||||
);
|
||||
```
|
||||
|
||||
### 🔐 Sécurité et validation
|
||||
|
||||
#### Prérequis pour créer un compte :
|
||||
- ✅ Utilisateur administrateur de l'amicale
|
||||
- ✅ Amicale active avec statut validé
|
||||
- ✅ Email de contact vérifié
|
||||
- ✅ Informations légales complètes (SIRET, adresse)
|
||||
|
||||
#### Validation avant paiements :
|
||||
- ✅ `charges_enabled = 1` (peut recevoir des paiements)
|
||||
- ✅ `payouts_enabled = 1` (peut recevoir des virements)
|
||||
- ✅ `details_submitted = 1` (onboarding terminé)
|
||||
|
||||
### 📊 États du compte Stripe
|
||||
|
||||
| État | Description | Actions possibles |
|
||||
|------|-------------|-------------------|
|
||||
| `pending` | Compte créé, onboarding en cours | Compléter l'onboarding |
|
||||
| `restricted` | Informations manquantes | Fournir documents manquants |
|
||||
| `restricted_soon` | Vérification en cours | Attendre validation Stripe |
|
||||
| `active` | Compte opérationnel | Recevoir des paiements ✅ |
|
||||
| `rejected` | Compte refusé par Stripe | Contacter support |
|
||||
|
||||
### 🚨 Gestion des erreurs
|
||||
|
||||
#### Erreurs courantes lors de la création :
|
||||
- **400** : Données manquantes ou invalides
|
||||
- **409** : Compte Stripe déjà existant pour cette amicale
|
||||
- **403** : Utilisateur non autorisé
|
||||
|
||||
#### Erreurs durant l'onboarding :
|
||||
- Documents manquants ou invalides
|
||||
- Informations bancaires incorrectes
|
||||
- Activité non autorisée par Stripe
|
||||
|
||||
### 📞 Support et résolution
|
||||
|
||||
#### Pour les amicales :
|
||||
1. **Email support** : support@geosector.fr
|
||||
2. **Documentation** : Guides d'onboarding disponibles
|
||||
3. **Assistance téléphonique** : Disponible aux heures ouvrables
|
||||
|
||||
#### Pour les développeurs :
|
||||
1. **Stripe Dashboard** : Accès aux comptes et statuts
|
||||
2. **Logs API** : Traçabilité complète des opérations
|
||||
3. **Webhook monitoring** : Suivi des événements Stripe
|
||||
|
||||
---
|
||||
|
||||
## 🚨 IMPORTANT : Nouveau Flow (v2)
|
||||
|
||||
**Le passage est TOUJOURS créé/modifié EN PREMIER** pour obtenir un ID réel, PUIS le PaymentIntent est créé avec cet ID.
|
||||
|
||||
## Flow détaillé
|
||||
|
||||
### 1. Sauvegarde du passage EN PREMIER
|
||||
|
||||
L'application crée ou modifie d'abord le passage pour obtenir un ID réel :
|
||||
|
||||
```
|
||||
POST /api/passages/create // Nouveau passage
|
||||
PUT /api/passages/456 // Mise à jour passage existant
|
||||
```
|
||||
|
||||
**Réponse avec l'ID réel :**
|
||||
```json
|
||||
{
|
||||
"status": "success",
|
||||
"passage_id": 456 // ID RÉEL du passage créé/modifié
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Création du PaymentIntent AVEC l'ID réel
|
||||
|
||||
Ensuite seulement, création du PaymentIntent avec le `passage_id` réel :
|
||||
|
||||
```
|
||||
POST /api/stripe/payments/create-intent
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"amount": 2500, // En centimes (25€)
|
||||
"passage_id": 456, // ID RÉEL du passage (JAMAIS 0)
|
||||
"payment_method_types": ["card_present"], // Tap to Pay
|
||||
"location_id": "tml_xxx", // Terminal reader location
|
||||
"amicale_id": 45,
|
||||
"member_id": 67,
|
||||
"stripe_account": "acct_xxx"
|
||||
}
|
||||
```
|
||||
|
||||
#### Réponse
|
||||
|
||||
```json
|
||||
{
|
||||
"status": "success",
|
||||
"data": {
|
||||
"client_secret": "pi_3QaXYZ_secret_xyz",
|
||||
"payment_intent_id": "pi_3QaXYZ123ABC456",
|
||||
"amount": 2500,
|
||||
"currency": "eur",
|
||||
"passage_id": 789, // 0 pour nouveau passage
|
||||
"type": "tap_to_pay"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Traitement du paiement côté client
|
||||
|
||||
L'application utilise le SDK Stripe pour traiter le paiement via NFC :
|
||||
|
||||
```dart
|
||||
// Flutter - Utilisation du client_secret
|
||||
final paymentResult = await stripe.collectPaymentMethod(
|
||||
clientSecret: response['client_secret'],
|
||||
// ... configuration Tap to Pay
|
||||
);
|
||||
```
|
||||
|
||||
### 3. Traitement du paiement Tap to Pay
|
||||
|
||||
L'application utilise le SDK Stripe Terminal avec le `client_secret` pour collecter le paiement via NFC.
|
||||
|
||||
### 4. Mise à jour du passage avec stripe_payment_id
|
||||
|
||||
Après succès du paiement, l'app met à jour le passage avec le `stripe_payment_id` :
|
||||
|
||||
```json
|
||||
PUT /api/passages/456
|
||||
{
|
||||
"stripe_payment_id": "pi_3QaXYZ123ABC456", // ← LIEN AVEC STRIPE
|
||||
// ... autres champs si nécessaire
|
||||
}
|
||||
```
|
||||
|
||||
## Points clés du nouveau flow
|
||||
|
||||
### ✅ Avantages
|
||||
|
||||
1. **Passage toujours existant** : Le passage existe toujours avec un ID réel avant le paiement
|
||||
2. **Traçabilité garantie** : Le `passage_id` dans Stripe est toujours valide
|
||||
3. **Gestion d'erreur robuste** : Si le paiement échoue, le passage existe déjà
|
||||
4. **Cohérence des données** : Pas de passage "orphelin" ou de paiement sans passage
|
||||
|
||||
### ❌ Ce qui n'est plus supporté
|
||||
|
||||
1. **passage_id=0** : Plus jamais utilisé dans `/create-intent`
|
||||
2. **operation_id** : Plus nécessaire car le passage existe déjà
|
||||
3. **Création conditionnelle** : Le passage est toujours créé avant
|
||||
|
||||
## Schéma de séquence (Nouveau Flow v2)
|
||||
|
||||
```
|
||||
┌─────────┐ ┌─────────┐ ┌────────┐ ┌────────────┐
|
||||
│ App │ │ API │ │ Stripe │ │ ope_pass │
|
||||
└────┬────┘ └────┬────┘ └────┬───┘ └─────┬──────┘
|
||||
│ │ │ │
|
||||
│ 1. CREATE/UPDATE passage │ │
|
||||
├──────────────>│ │ │
|
||||
│ ├────────────────┼───────────────>│
|
||||
│ │ │ INSERT/UPDATE │
|
||||
│ │ │ │
|
||||
│ 2. passage_id: 456 (réel) │ │
|
||||
│<──────────────│ │ │
|
||||
│ │ │ │
|
||||
│ 3. create-intent (id=456) │ │
|
||||
├──────────────>│ │ │
|
||||
│ │ │ │
|
||||
│ │ 4. Create PI │ │
|
||||
│ ├───────────────>│ │
|
||||
│ │ │ │
|
||||
│ │ 5. PI created │ │
|
||||
│ │<───────────────│ │
|
||||
│ │ │ │
|
||||
│ │ 6. UPDATE │ │
|
||||
│ ├────────────────┼───────────────>│
|
||||
│ │ stripe_payment_id = pi_xxx │
|
||||
│ │ │ │
|
||||
│ 7. client_secret + pi_id │ │
|
||||
│<──────────────│ │ │
|
||||
│ │ │ │
|
||||
│ 8. Tap to Pay │ │ │
|
||||
├───────────────┼───────────────>│ │
|
||||
│ avec SDK │ │ │
|
||||
│ │ │ │
|
||||
│ 9. Payment OK │ │ │
|
||||
│<──────────────┼────────────────│ │
|
||||
│ │ │ │
|
||||
│ 10. UPDATE passage (optionnel) │ │
|
||||
├──────────────>│ │ │
|
||||
│ ├────────────────┼───────────────>│
|
||||
│ │ Confirmer stripe_payment_id │
|
||||
│ │ │ │
|
||||
│ 11. Success │ │ │
|
||||
│<──────────────│ │ │
|
||||
│ │ │ │
|
||||
```
|
||||
|
||||
## Points importants (Nouveau Flow v2)
|
||||
|
||||
1. **Passage créé en premier** : Le passage est TOUJOURS créé/modifié AVANT le PaymentIntent
|
||||
2. **ID réel obligatoire** : Le `passage_id` ne peut jamais être 0 dans `/create-intent`
|
||||
3. **Lien Stripe automatique** : Le `stripe_payment_id` est ajouté automatiquement lors de la création du PaymentIntent
|
||||
4. **Idempotence** : Un passage ne peut avoir qu'un seul `stripe_payment_id`
|
||||
5. **Validation stricte** : Vérification du montant, propriété et existence du passage
|
||||
|
||||
## Erreurs possibles
|
||||
|
||||
- **400** :
|
||||
- `passage_id` manquant ou ≤ 0
|
||||
- Montant invalide (< 1€ ou > 999€)
|
||||
- Passage déjà payé par Stripe
|
||||
- Montant ne correspond pas au passage
|
||||
- **401** : Non authentifié
|
||||
- **403** : Passage non autorisé (pas le bon utilisateur)
|
||||
- **404** : Passage non trouvé
|
||||
|
||||
## Migration base de données
|
||||
|
||||
La colonne `stripe_payment_id VARCHAR(50)` a été ajoutée via :
|
||||
```sql
|
||||
ALTER TABLE `ope_pass` ADD COLUMN `stripe_payment_id` VARCHAR(50) DEFAULT NULL;
|
||||
ALTER TABLE `ope_pass` ADD INDEX `idx_stripe_payment` (`stripe_payment_id`);
|
||||
```
|
||||
|
||||
## Environnements
|
||||
|
||||
- **DEV** : dva-geo sur IN3 - Base mise à jour ✅
|
||||
- **REC** : rca-geo sur IN3 - Base mise à jour ✅
|
||||
- **PROD** : pra-geo sur IN4 - À mettre à jour
|
||||
197
api/docs/STRIPE-TAP-TO-PAY-REQUIREMENTS.md
Normal file
197
api/docs/STRIPE-TAP-TO-PAY-REQUIREMENTS.md
Normal file
@@ -0,0 +1,197 @@
|
||||
# Stripe Tap to Pay - Requirements officiels
|
||||
|
||||
> Document basé sur la documentation officielle Stripe - Dernière vérification : 29 septembre 2025
|
||||
|
||||
## 📱 iOS - Tap to Pay sur iPhone
|
||||
|
||||
### Configuration minimum requise
|
||||
|
||||
| Composant | Requirement | Notes |
|
||||
|-----------|------------|--------|
|
||||
| **Appareil** | iPhone XS ou plus récent | iPhone XS, XR, 11, 12, 13, 14, 15, 16 |
|
||||
| **iOS** | iOS 16.4 ou plus récent | Pour support PIN complet |
|
||||
| **SDK** | Terminal iOS SDK 2.23.0+ | Version 3.6.0+ pour Interac (Canada) |
|
||||
| **Entitlement** | Apple Tap to Pay | À demander sur Apple Developer |
|
||||
|
||||
### Fonctionnalités par version iOS
|
||||
|
||||
- **iOS 16.0-16.3** : Tap to Pay basique (sans PIN)
|
||||
- **iOS 16.4+** : Support PIN complet pour toutes les cartes
|
||||
- **Versions beta** : NON SUPPORTÉES
|
||||
|
||||
### Méthodes de paiement supportées
|
||||
|
||||
- ✅ Cartes sans contact : Visa, Mastercard, American Express
|
||||
- ✅ Wallets NFC : Apple Pay, Google Pay, Samsung Pay
|
||||
- ✅ Discover (USA uniquement)
|
||||
- ✅ Interac (Canada uniquement, SDK 3.6.0+)
|
||||
- ✅ eftpos (Australie uniquement)
|
||||
|
||||
### Limitations importantes
|
||||
|
||||
- ❌ iPad non supporté (pas de NFC)
|
||||
- ❌ Puerto Rico non disponible
|
||||
- ❌ Versions iOS beta non supportées
|
||||
|
||||
## 🤖 Android - Tap to Pay
|
||||
|
||||
### Configuration minimum requise
|
||||
|
||||
| Composant | Requirement | Notes |
|
||||
|-----------|------------|--------|
|
||||
| **Android** | Android 11 ou plus récent | API level 30+ |
|
||||
| **NFC** | Capteur NFC fonctionnel | Obligatoire |
|
||||
| **Processeur** | ARM | x86 non supporté |
|
||||
| **Sécurité** | Appareil non rooté | Bootloader verrouillé |
|
||||
| **Services** | Google Mobile Services | GMS obligatoire |
|
||||
| **Keystore** | Hardware keystore intégré | Pour sécurité |
|
||||
| **OS** | OS constructeur non modifié | Pas de ROM custom |
|
||||
|
||||
### Appareils certifiés en France (liste non exhaustive)
|
||||
|
||||
#### Samsung
|
||||
- Galaxy S21, S21+, S21 Ultra, S21 FE (Android 11+)
|
||||
- Galaxy S22, S22+, S22 Ultra (Android 12+)
|
||||
- Galaxy S23, S23+, S23 Ultra, S23 FE (Android 13+)
|
||||
- Galaxy S24, S24+, S24 Ultra (Android 14+)
|
||||
- Galaxy Z Fold 3, 4, 5, 6
|
||||
- Galaxy Z Flip 3, 4, 5, 6
|
||||
- Galaxy Note 20, Note 20 Ultra
|
||||
- Galaxy A54, A73 (haut de gamme)
|
||||
|
||||
#### Google Pixel
|
||||
- Pixel 6, 6 Pro, 6a (Android 12+)
|
||||
- Pixel 7, 7 Pro, 7a (Android 13+)
|
||||
- Pixel 8, 8 Pro, 8a (Android 14+)
|
||||
- Pixel 9, 9 Pro, 9 Pro XL (Android 14+)
|
||||
- Pixel Fold (Android 13+)
|
||||
- Pixel Tablet (Android 13+)
|
||||
|
||||
#### OnePlus
|
||||
- OnePlus 9, 9 Pro (Android 11+)
|
||||
- OnePlus 10 Pro, 10T (Android 12+)
|
||||
- OnePlus 11, 11R (Android 13+)
|
||||
- OnePlus 12, 12R (Android 14+)
|
||||
- OnePlus Open (Android 13+)
|
||||
|
||||
#### Xiaomi
|
||||
- Mi 11, 11 Ultra (Android 11+)
|
||||
- Xiaomi 12, 12 Pro, 12T Pro (Android 12+)
|
||||
- Xiaomi 13, 13 Pro, 13T Pro (Android 13+)
|
||||
- Xiaomi 14, 14 Pro, 14 Ultra (Android 14+)
|
||||
|
||||
#### Autres marques
|
||||
- OPPO Find X3/X5/X6 Pro, Find N2/N3
|
||||
- Realme GT 2 Pro, GT 3, GT 5 Pro
|
||||
- Honor Magic5/6 Pro, 90
|
||||
- ASUS Zenfone 9/10, ROG Phone 7
|
||||
- Nothing Phone (1), (2), (2a)
|
||||
|
||||
## 🌍 Disponibilité par pays
|
||||
|
||||
### Europe
|
||||
- ✅ France : Disponible
|
||||
- ✅ Royaume-Uni : Disponible
|
||||
- ✅ Allemagne : Disponible
|
||||
- ✅ Pays-Bas : Disponible
|
||||
- ✅ Irlande : Disponible
|
||||
- ✅ Italie : Disponible (récent)
|
||||
- ✅ Espagne : Disponible (récent)
|
||||
|
||||
### Amérique
|
||||
- ✅ États-Unis : Disponible (+ Discover)
|
||||
- ✅ Canada : Disponible (+ Interac)
|
||||
- ❌ Puerto Rico : Non disponible
|
||||
- ❌ Mexique : Non disponible
|
||||
|
||||
### Asie-Pacifique
|
||||
- ✅ Australie : Disponible (+ eftpos)
|
||||
- ✅ Nouvelle-Zélande : Disponible
|
||||
- ✅ Singapour : Disponible
|
||||
- ✅ Japon : Disponible (récent)
|
||||
|
||||
## 🔧 Intégration technique
|
||||
|
||||
### SDK Requirements
|
||||
|
||||
```javascript
|
||||
// iOS
|
||||
pod 'StripeTerminal', '~> 2.23.0' // Minimum pour Tap to Pay
|
||||
pod 'StripeTerminal', '~> 3.6.0' // Pour support Interac
|
||||
|
||||
// Android
|
||||
implementation 'com.stripe:stripeterminal-taptopay:3.7.1'
|
||||
implementation 'com.stripe:stripeterminal-core:3.7.1'
|
||||
|
||||
// React Native
|
||||
"@stripe/stripe-terminal-react-native": "^0.0.1-beta.17"
|
||||
|
||||
// Flutter
|
||||
stripe_terminal: ^3.2.0
|
||||
```
|
||||
|
||||
### Capacités requises
|
||||
|
||||
#### iOS Info.plist
|
||||
```xml
|
||||
<key>NSBluetoothAlwaysUsageDescription</key>
|
||||
<string>Bluetooth nécessaire pour Tap to Pay</string>
|
||||
<key>NFCReaderUsageDescription</key>
|
||||
<string>NFC nécessaire pour lire les cartes</string>
|
||||
<key>com.apple.developer.proximity-reader</key>
|
||||
<true/>
|
||||
```
|
||||
|
||||
#### Android Manifest
|
||||
```xml
|
||||
<uses-permission android:name="android.permission.NFC" />
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||
<uses-feature android:name="android.hardware.nfc" android:required="true" />
|
||||
```
|
||||
|
||||
## 📊 Limites techniques
|
||||
|
||||
| Limite | Valeur | Notes |
|
||||
|--------|--------|-------|
|
||||
| **Montant min** | 1€ / $1 | Selon devise |
|
||||
| **Montant max** | Variable par pays | France : 50€ sans PIN, illimité avec PIN |
|
||||
| **Timeout transaction** | 60 secondes | Après présentation carte |
|
||||
| **Distance NFC** | 4cm max | Distance optimale |
|
||||
| **Tentatives PIN** | 3 max | Puis carte bloquée |
|
||||
|
||||
## 🔐 Sécurité
|
||||
|
||||
### Certifications
|
||||
- PCI-DSS Level 1
|
||||
- EMV Contactless Level 1
|
||||
- Apple ProximityReader Framework
|
||||
- Google SafetyNet Attestation
|
||||
|
||||
### Données sensibles
|
||||
- Les données de carte ne transitent JAMAIS par l'appareil
|
||||
- Tokenisation end-to-end par Stripe
|
||||
- Pas de stockage local des données carte
|
||||
- PIN chiffré directement vers Stripe
|
||||
|
||||
## 📚 Ressources officielles
|
||||
|
||||
- [Documentation Stripe Terminal](https://docs.stripe.com/terminal)
|
||||
- [Tap to Pay sur iPhone - Apple Developer](https://developer.apple.com/tap-to-pay/)
|
||||
- [Guide d'intégration iOS](https://docs.stripe.com/terminal/payments/setup-reader/tap-to-pay?platform=ios)
|
||||
- [Guide d'intégration Android](https://docs.stripe.com/terminal/payments/setup-reader/tap-to-pay?platform=android)
|
||||
- [SDK Terminal iOS](https://github.com/stripe/stripe-terminal-ios)
|
||||
- [SDK Terminal Android](https://github.com/stripe/stripe-terminal-android)
|
||||
|
||||
## 🔄 Historique des versions
|
||||
|
||||
| Date | Version iOS | Changement |
|
||||
|------|-------------|------------|
|
||||
| Sept 2022 | iOS 16.0 | Lancement initial Tap to Pay |
|
||||
| Mars 2023 | iOS 16.4 | Ajout support PIN |
|
||||
| Sept 2023 | iOS 17.0 | Améliorations performances |
|
||||
| Sept 2024 | iOS 18.0 | Support étendu international |
|
||||
|
||||
---
|
||||
|
||||
*Document maintenu par l'équipe GeoSector - Dernière mise à jour : 29/09/2025*
|
||||
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
407
api/docs/TECHBOOK.md
Normal file → Executable file
407
api/docs/TECHBOOK.md
Normal file → Executable file
@@ -1,300 +1,175 @@
|
||||
# Documentation Technique API RESTful PHP 8.3
|
||||
# Documentation Technique API GeoSector
|
||||
|
||||
## Table des matières
|
||||
## 🏗️ Infrastructure
|
||||
|
||||
1. [Structure du projet](#structure-du-projet)
|
||||
2. [Configuration du serveur](#configuration-du-serveur)
|
||||
3. [Flux d'une requête](#flux-dune-requête)
|
||||
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)
|
||||
**Stack** : PHP 8.3 | NGINX | MariaDB 10.11 | Alpine Linux (Incus)
|
||||
|
||||
## Structure du projet
|
||||
| Environnement | Container | DB (IP) | URL | Serveur |
|
||||
|---------------|-----------|---------|-----|---------|
|
||||
| **DEV** | dva-geo | maria3 (13.23.33.4) | https://dapp.geosector.fr | IN3 (195.154.80.116) |
|
||||
| **REC** | rca-geo | maria3 (13.23.33.4) | https://rapp.geosector.fr | IN3 (195.154.80.116) |
|
||||
| **PROD** | pra-geo | maria4 (13.23.33.4) | https://app3.geosector.fr | IN4 (51.159.7.190) |
|
||||
|
||||
```plaintext
|
||||
/api/
|
||||
├── docs/
|
||||
│ └── TECHBOOK.md
|
||||
├── src/
|
||||
│ ├── Controllers/
|
||||
│ │ └── UserController.php
|
||||
│ ├── Core/
|
||||
│ │ ├── Router.php
|
||||
│ │ ├── Request.php
|
||||
│ │ ├── Response.php
|
||||
│ │ ├── Session.php
|
||||
│ │ └── Database.php
|
||||
│ └── Config/
|
||||
│ └── config.php
|
||||
├── index.php
|
||||
└── .htaccess
|
||||
**Architecture** : MVC sans framework | Point d'entrée : `index.php` | Config : `AppConfig.php` singleton
|
||||
|
||||
## 🗄️ Base de données
|
||||
|
||||
### Modèle d'isolation par opération (CRITIQUE)
|
||||
|
||||
**Principe** : Chaque opération est un **univers fermé** isolé de la table centrale `users`.
|
||||
|
||||
```
|
||||
users (table centrale, conservée après suppression opération)
|
||||
└── ope_users (id, fk_user, fk_operation) ← PIVOT par opération
|
||||
├── ope_users_sectors (fk_user → ope_users.id)
|
||||
└── ope_pass (fk_user → ope_users.id)
|
||||
```
|
||||
|
||||
## Configuration du serveur
|
||||
|
||||
### Prérequis
|
||||
|
||||
- Debian 12
|
||||
- NGINX
|
||||
- PHP 8.3-FPM
|
||||
- MariaDB 10.11
|
||||
|
||||
### Configuration NGINX
|
||||
|
||||
Le serveur NGINX est configuré pour rediriger toutes les requêtes vers le point d'entrée `index.php` de l'API.
|
||||
|
||||
### Configuration PHP-FPM
|
||||
|
||||
PHP-FPM est configuré pour gérer les processus PHP avec des paramètres optimisés pour une API.
|
||||
|
||||
## Flux d'une requête
|
||||
|
||||
Exemple détaillé du parcours d'une requête POST /api/users :
|
||||
|
||||
1. **Entrée de la requête**
|
||||
|
||||
- La requête arrive sur le serveur NGINX
|
||||
- NGINX redirige vers PHP-FPM via le socket unix
|
||||
- Le fichier .htaccess redirige vers index.php
|
||||
|
||||
2. **Initialisation (index.php)**
|
||||
|
||||
- Chargement des dépendances
|
||||
- Initialisation de la configuration
|
||||
- Démarrage de la session
|
||||
- Configuration des headers CORS
|
||||
- Initialisation du routeur
|
||||
|
||||
3. **Routage**
|
||||
|
||||
- Le Router analyse la méthode HTTP (POST)
|
||||
- Analyse de l'URI (/api/users)
|
||||
- Correspondance avec les routes enregistrées
|
||||
- Instanciation du Controller approprié
|
||||
|
||||
4. **Traitement (UserController)**
|
||||
|
||||
- Vérification de l'authentification
|
||||
- Récupération des données JSON
|
||||
- Validation des données reçues
|
||||
- Traitement métier
|
||||
- Interaction avec la base de données
|
||||
- Préparation de la réponse
|
||||
|
||||
5. **Réponse**
|
||||
- Formatage de la réponse en JSON
|
||||
- Configuration des headers de réponse
|
||||
- Envoi au client
|
||||
|
||||
## Architecture des composants
|
||||
|
||||
### Core Components
|
||||
|
||||
#### Router
|
||||
|
||||
- Gère le routage des requêtes
|
||||
- Associe les URLs aux Controllers
|
||||
- Gère les paramètres d'URL
|
||||
- Dispatch vers les méthodes appropriées
|
||||
|
||||
#### Request
|
||||
|
||||
- Parse les données entrantes
|
||||
- Gère les différents types de contenu
|
||||
- Nettoie et valide les entrées
|
||||
- Fournit une interface unifiée pour accéder aux données
|
||||
|
||||
#### Response
|
||||
|
||||
- Formate les réponses en JSON
|
||||
- Gère les codes HTTP
|
||||
- Configure les headers de réponse
|
||||
- Assure la cohérence des réponses
|
||||
|
||||
#### Session
|
||||
|
||||
- Gère l'état des sessions
|
||||
- Stocke les données d'authentification
|
||||
- Vérifie les permissions
|
||||
- Sécurise les données de session
|
||||
|
||||
#### Database
|
||||
|
||||
- Gère la connexion à MariaDB
|
||||
- Fournit une interface PDO
|
||||
- Gère le pool de connexions
|
||||
- Assure la sécurité des requêtes
|
||||
|
||||
## Sécurité
|
||||
|
||||
### Mesures implémentées
|
||||
|
||||
- Validation stricte des entrées
|
||||
- Protection contre les injections SQL (PDO)
|
||||
- Hachage sécurisé des mots de passe
|
||||
- Headers de sécurité HTTP
|
||||
- Gestion des CORS
|
||||
- Session sécurisée
|
||||
- Authentification requise
|
||||
|
||||
## Endpoints API
|
||||
|
||||
### Routes Publiques vs Privées
|
||||
|
||||
L'API distingue deux types de routes :
|
||||
|
||||
#### Routes Publiques
|
||||
|
||||
- POST /api/login
|
||||
- POST /api/register
|
||||
- GET /api/health
|
||||
|
||||
#### Routes Privées (Nécessitent une session authentifiée)
|
||||
|
||||
- Toutes les autres routes
|
||||
|
||||
### Authentification
|
||||
|
||||
L'authentification utilise le système de session PHP natif.
|
||||
|
||||
#### Login
|
||||
|
||||
```http
|
||||
POST /api/login
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"email": "user@example.com",
|
||||
"password": "SecurePassword123"
|
||||
}
|
||||
```
|
||||
|
||||
**Réponse réussie :**
|
||||
**⚠️ IMPORTANT pour Flutter** :
|
||||
- **`users.id`** = Identifiant global (gestion membres, login)
|
||||
- **`ope_users.id`** = Identifiant opération (passages, secteurs)
|
||||
- **Flutter doit gérer les 2 IDs** lors du login et des CRUD
|
||||
|
||||
**Réponse login :**
|
||||
```json
|
||||
{
|
||||
"message": "Connecté avec succès",
|
||||
"user": {
|
||||
"id": 123,
|
||||
"email": "user@example.com"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Notes importantes :**
|
||||
|
||||
- Un cookie de session PHP sécurisé est automatiquement créé
|
||||
- Le cookie est httpOnly, secure et SameSite=Strict
|
||||
- L'ID de session est régénéré à chaque login réussi
|
||||
|
||||
#### Logout
|
||||
|
||||
```http
|
||||
POST /api/logout
|
||||
```
|
||||
|
||||
**Réponse réussie :**
|
||||
|
||||
```json
|
||||
{
|
||||
"message": "Déconnecté avec succès"
|
||||
}
|
||||
```
|
||||
|
||||
### Sécurité des Sessions
|
||||
|
||||
La configuration des sessions inclut :
|
||||
|
||||
- Sessions PHP natives sécurisées
|
||||
- Protection contre la fixation de session
|
||||
- Cookies httpOnly (protection XSS)
|
||||
- Mode strict pour les cookies
|
||||
- Validation côté serveur à chaque requête
|
||||
|
||||
- use_strict_mode = 1
|
||||
- cookie_httponly = 1
|
||||
- cookie_secure = 1
|
||||
- cookie_samesite = Strict
|
||||
- Régénération de l'ID de session après login
|
||||
- Destruction complète de la session au logout
|
||||
|
||||
### Users
|
||||
|
||||
#### Création d'utilisateur
|
||||
|
||||
```http
|
||||
POST /api/users
|
||||
Content-Type: application/json
|
||||
|
||||
"users_sectors": [
|
||||
{
|
||||
"id": 123, // users.id (gestion membres)
|
||||
"ope_user_id": 50, // ope_users.id (passages/secteurs)
|
||||
"name": "John Doe",
|
||||
"email": "john@example.com",
|
||||
"password": "SecurePassword123"
|
||||
"fk_sector": 456
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
**Réponse réussie :**
|
||||
|
||||
**Requêtes API depuis Flutter :**
|
||||
```json
|
||||
{
|
||||
"message": "Utilisateur créé",
|
||||
"id": "123"
|
||||
}
|
||||
// ✅ Création secteur : envoyer ope_users.id
|
||||
POST /api/sectors { "users": [50, 51] } // ope_users.id
|
||||
|
||||
// ✅ Création passage : fk_user = ope_users.id
|
||||
POST /api/passages { "fk_user": 50 } // ope_users.id
|
||||
|
||||
// ✅ Modification membre : utiliser users.id
|
||||
PUT /api/users/123 // users.id
|
||||
```
|
||||
|
||||
**Codes de statut :**
|
||||
### Tables principales
|
||||
|
||||
- 201: Création réussie
|
||||
- 400: Données invalides
|
||||
- 401: Non authentifié
|
||||
- 500: Erreur serveur
|
||||
- **`entites`** : Amicales (chiffrement AES-256-CBC sur nom, email, IBAN)
|
||||
- **`users`** : Table centrale utilisateurs (conservée même si opération supprimée)
|
||||
- **`operations`** : Campagnes liées à une entité
|
||||
- **`ope_users`** : **PIVOT** users ↔ opérations (ON DELETE CASCADE depuis operations)
|
||||
- **`ope_sectors`** : Secteurs géographiques (polygones)
|
||||
- **`ope_users_sectors`** : Affectation users ↔ secteurs (FK → `ope_users.id`)
|
||||
- **`ope_pass`** : Passages (FK → `ope_users.id`, ON DELETE CASCADE)
|
||||
- **`medias`** : Fichiers (logos, exports, reçus) - Stockage : `/uploads/`
|
||||
|
||||
#### Autres endpoints
|
||||
## 🔒 Sécurité
|
||||
|
||||
- GET /api/users
|
||||
- GET /api/users/{id}
|
||||
- PUT /api/users/{id}
|
||||
- DELETE /api/users/{id}
|
||||
- **Auth** : Sessions PHP (httpOnly, secure, SameSite=Strict)
|
||||
- **Mots de passe** : NIST SP 800-63B, bcrypt, HIBP check (k-anonymity)
|
||||
- **Chiffrement** : AES-256-CBC (noms, emails, téléphones, IBAN)
|
||||
- **Protection** : Brute force (8 tentatives/5min), IP blocking, PDO prepared statements
|
||||
- **Monitoring** : `SecurityMonitor`, `AlertService`, `IPBlocker`
|
||||
|
||||
## Intégration Frontend
|
||||
## 💳 Stripe Connect
|
||||
|
||||
### Configuration des Requêtes
|
||||
- **DEV** : Clés TEST Pierre
|
||||
- **REC** : Clés TEST client + webhook `webhook-rca`
|
||||
- **PROD** : Clés LIVE client + webhook `webhook-pra`
|
||||
- **API** : `2025-08-27.basil`
|
||||
- **Tap to Pay** : iOS 16.4+ (iPhone XS+) | Android 11+ (95+ devices certifiés)
|
||||
- **Flow** : Passage créé → PaymentIntent → Tap to Pay → Mise à jour `stripe_payment_id`
|
||||
|
||||
Toutes les requêtes API depuis le frontend doivent inclure :
|
||||
## 📦 Fonctionnalités
|
||||
|
||||
```javascript
|
||||
fetch('/api/endpoint', {
|
||||
credentials: 'include', // Important pour les cookies de session
|
||||
// ... autres options
|
||||
});
|
||||
1. **Reçus fiscaux** : PDF auto (<5KB) pour dons, envoi email queue
|
||||
2. **Logos entités** : Upload PNG/JPG, redimensionnement 250x250px, base64
|
||||
3. **Migration** : Endpoints REST par entité (9 phases)
|
||||
4. **CRONs** : Email queue (*/5), cleanup sécurité (2h), Stripe devices (dim 3h)
|
||||
|
||||
## 🚀 Déploiement
|
||||
|
||||
```bash
|
||||
./deploy-api.sh # Local → dva-geo (DEV)
|
||||
./deploy-api.sh rca # dva-geo → rca-geo (REC)
|
||||
./deploy-api.sh pra # rca-geo → pra-geo (PROD)
|
||||
```
|
||||
|
||||
### Gestion des Sessions
|
||||
- Backup auto (10 versions)
|
||||
- Préservation `/logs/` et `/uploads/`
|
||||
- Permissions : `nginx:nginx` (code), `nginx:nginx` (logs/uploads)
|
||||
- Composer install avec `--optimize-autoloader`
|
||||
|
||||
- Les cookies de session sont automatiquement gérés par le navigateur
|
||||
- Pas besoin de stocker ou gérer des tokens manuellement
|
||||
- Redirection vers /login si session expirée (401)
|
||||
## ⚠️ Points d'attention API ↔ Flutter
|
||||
|
||||
## Maintenance et Déploiement
|
||||
### 1. Isolation opérations (depuis Oct 2025)
|
||||
|
||||
### Logs
|
||||
**Avant** : `ope_pass.fk_user` → `users.id` (table centrale)
|
||||
**Après** : `ope_pass.fk_user` → `ope_users.id` (pivot opération)
|
||||
|
||||
- Logs d'accès NGINX : /var/log/nginx/api-access.log
|
||||
- Logs d'erreur NGINX : /var/log/nginx/api-error.log
|
||||
- Logs PHP : /var/log/php/php-error.log
|
||||
**Impact Flutter** :
|
||||
- Login retourne **2 IDs** : `id` (users.id) + `ope_user_id` (ope_users.id)
|
||||
- Création secteur/passage : envoyer `ope_user_id`
|
||||
- Affichage passages : mapper avec `ope_user_id`
|
||||
|
||||
### Déploiement
|
||||
### 2. Endpoints critiques modifiés
|
||||
|
||||
1. Pull du repository
|
||||
2. Vérification des permissions
|
||||
3. Configuration de l'environnement
|
||||
4. Tests des endpoints
|
||||
5. Redémarrage des services
|
||||
| Endpoint | Body envoyé | Mapping |
|
||||
|----------|-------------|---------|
|
||||
| `POST /api/sectors` | `users: [50, 51]` | ope_users.id |
|
||||
| `PUT /api/sectors/{id}` | `users: [50, 51]` | ope_users.id |
|
||||
| `POST /api/passages` | `fk_user: 50` | ope_users.id |
|
||||
| `PUT /api/passages/{id}` | `fk_user: 50` | ope_users.id |
|
||||
| `POST /api/users` | - | Retourne `ope_user_id` |
|
||||
|
||||
### Surveillance
|
||||
### 3. Requête SQL typique
|
||||
|
||||
- Monitoring des processus PHP-FPM
|
||||
- Surveillance de la base de données
|
||||
- Monitoring des performances
|
||||
- Alertes sur erreurs critiques
|
||||
```sql
|
||||
-- ❌ AVANT (CASSÉ)
|
||||
SELECT op.*, u.encrypted_name
|
||||
FROM ope_pass op
|
||||
JOIN users u ON op.fk_user = u.id
|
||||
|
||||
-- ✅ APRÈS (CORRECT)
|
||||
SELECT op.*, u.encrypted_name
|
||||
FROM ope_pass op
|
||||
JOIN ope_users ou ON op.fk_user = ou.id
|
||||
JOIN users u ON ou.fk_user = u.id
|
||||
```
|
||||
|
||||
### 4. Suppression en cascade
|
||||
|
||||
```sql
|
||||
DELETE FROM operations WHERE id = 850;
|
||||
-- Supprime automatiquement (CASCADE) :
|
||||
-- - ope_users
|
||||
-- - ope_users_sectors
|
||||
-- - ope_pass
|
||||
-- - ope_sectors
|
||||
-- ✅ users conservé (table centrale)
|
||||
```
|
||||
|
||||
## 📝 Changelog critique
|
||||
|
||||
**Version 3.3.7 (26 Oct 2025)** :
|
||||
- 🔧 Correction bug `SectorController::update()` : Recherche users par `ope_users.id` au lieu de `users.id`
|
||||
- 🔧 Permissions logs corrigées : `nginx:nginx` + `750/640`
|
||||
- 🔧 PHP `display_errors = Off` (warnings loggés dans `/var/log/php83/error.log`)
|
||||
|
||||
**Version 3.3.6 (21 Oct 2025)** :
|
||||
- Validation inscription : Code postal + ville (doublon)
|
||||
|
||||
**Version 3.2.7 (16 Oct 2025)** :
|
||||
- Migration RCA-GEO vers maria3 complétée
|
||||
- URL PROD : `app3.geosector.fr`
|
||||
|
||||
**Version 3.2.4-3.2.6 (Sep 2025)** :
|
||||
- Stripe Connect complet (Tap to Pay, webhooks multi-env)
|
||||
|
||||
---
|
||||
|
||||
**Mis à jour : 26 Octobre 2025**
|
||||
|
||||
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/create_table_user_devices.sql
Normal file
53
api/docs/create_table_user_devices.sql
Normal file
@@ -0,0 +1,53 @@
|
||||
-- Table pour stocker les informations des devices des utilisateurs
|
||||
CREATE TABLE IF NOT EXISTS `user_devices` (
|
||||
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
|
||||
`fk_user` int(10) unsigned NOT NULL COMMENT 'Référence vers la table users',
|
||||
|
||||
-- Informations générales du device
|
||||
`platform` varchar(20) NOT NULL COMMENT 'Plateforme: iOS, Android, etc.',
|
||||
`device_model` varchar(100) DEFAULT NULL COMMENT 'Modèle du device (ex: iPhone13,2)',
|
||||
`device_name` varchar(255) DEFAULT NULL COMMENT 'Nom personnalisé du device',
|
||||
`device_manufacturer` varchar(100) DEFAULT NULL COMMENT 'Fabricant (Apple, Samsung, etc.)',
|
||||
`device_identifier` varchar(100) DEFAULT NULL COMMENT 'Identifiant unique du device',
|
||||
|
||||
-- Informations réseau (IPv4 uniquement)
|
||||
`device_ip_local` varchar(15) DEFAULT NULL COMMENT 'Adresse IP locale IPv4',
|
||||
`device_ip_public` varchar(15) DEFAULT NULL COMMENT 'Adresse IP publique IPv4',
|
||||
`device_wifi_name` varchar(255) DEFAULT NULL COMMENT 'Nom du réseau WiFi (SSID)',
|
||||
`device_wifi_bssid` varchar(17) DEFAULT NULL COMMENT 'BSSID du point d\'accès (format
|
||||
XX:XX:XX:XX:XX:XX)',
|
||||
|
||||
-- Capacités et version OS
|
||||
`ios_version` varchar(20) DEFAULT NULL COMMENT 'Version iOS/Android OS',
|
||||
`device_nfc_capable` tinyint(1) DEFAULT NULL COMMENT 'Support NFC (1=oui, 0=non)',
|
||||
`device_supports_tap_to_pay` tinyint(1) DEFAULT NULL COMMENT 'Support Tap to Pay (1=oui, 0=non)',
|
||||
|
||||
-- État batterie
|
||||
`battery_level` tinyint(3) unsigned DEFAULT NULL COMMENT 'Niveau batterie en pourcentage (0-100)',
|
||||
`battery_charging` tinyint(1) DEFAULT NULL COMMENT 'En charge (1=oui, 0=non)',
|
||||
`battery_state` varchar(20) DEFAULT NULL COMMENT 'État batterie (charging, discharging, full)',
|
||||
|
||||
-- Versions application
|
||||
`app_version` varchar(20) DEFAULT NULL COMMENT 'Version de l\'application (ex: 3.2.8)',
|
||||
`app_build` varchar(20) DEFAULT NULL COMMENT 'Numéro de build (ex: 328)',
|
||||
|
||||
-- Timestamps
|
||||
`last_device_info_check` timestamp NULL DEFAULT NULL COMMENT 'Dernier check des infos device côté
|
||||
app',
|
||||
`created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT 'Date de création de
|
||||
l\'enregistrement',
|
||||
`updated_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT
|
||||
'Date de dernière modification',
|
||||
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `idx_fk_user` (`fk_user`) COMMENT 'Index pour recherche par utilisateur',
|
||||
KEY `idx_updated_at` (`updated_at`) COMMENT 'Index pour tri par date de mise à jour',
|
||||
KEY `idx_last_check` (`last_device_info_check`) COMMENT 'Index pour recherche par dernière
|
||||
vérification',
|
||||
UNIQUE KEY `unique_user_device` (`fk_user`, `device_identifier`) COMMENT 'Un seul enregistrement
|
||||
par device/user',
|
||||
|
||||
CONSTRAINT `fk_user_devices_user` FOREIGN KEY (`fk_user`)
|
||||
REFERENCES `users` (`id`) ON DELETE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='Informations des devices
|
||||
utilisateurs';
|
||||
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
0
api/docs/geosector-db-diagram.md
Normal file → Executable file
0
api/docs/geosector-db-diagram.md
Normal file → Executable file
@@ -1,619 +0,0 @@
|
||||
-- Création de la base de données geo_app si elle n'existe pas
|
||||
DROP DATABASE IF EXISTS `geo_app`;
|
||||
CREATE DATABASE IF NOT EXISTS `geo_app` DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||
|
||||
-- Création de l'utilisateur et attribution des droits
|
||||
CREATE USER IF NOT EXISTS 'geo_app_user'@'localhost' IDENTIFIED BY 'QO:96df*?k{4W6m';
|
||||
GRANT SELECT, INSERT, UPDATE, DELETE ON `geo_app`.* TO 'geo_app_user'@'localhost';
|
||||
FLUSH PRIVILEGES;
|
||||
|
||||
USE geo_app;
|
||||
|
||||
--
|
||||
-- Table structure for table `email_counter`
|
||||
--
|
||||
|
||||
DROP TABLE IF EXISTS `email_counter`;
|
||||
/*!40101 SET @saved_cs_client = @@character_set_client */;
|
||||
/*!50503 SET character_set_client = utf8mb4 */;
|
||||
CREATE TABLE `email_counter` (
|
||||
`id` int unsigned NOT NULL DEFAULT '1',
|
||||
`hour_start` timestamp NULL DEFAULT NULL,
|
||||
`count` int unsigned DEFAULT '0',
|
||||
PRIMARY KEY (`id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||
|
||||
DROP TABLE IF EXISTS `x_devises`;
|
||||
/*!40101 SET @saved_cs_client = @@character_set_client */;
|
||||
/*!50503 SET character_set_client = utf8mb4 */;
|
||||
CREATE TABLE `x_devises` (
|
||||
`id` int 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;
|
||||
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||
|
||||
--
|
||||
-- Table structure for table `x_entites_types`
|
||||
--
|
||||
|
||||
DROP TABLE IF EXISTS `x_entites_types`;
|
||||
/*!40101 SET @saved_cs_client = @@character_set_client */;
|
||||
/*!50503 SET character_set_client = utf8mb4 */;
|
||||
CREATE TABLE `x_entites_types` (
|
||||
`id` int 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;
|
||||
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||
|
||||
--
|
||||
-- Table structure for table `x_types_passages`
|
||||
--
|
||||
|
||||
DROP TABLE IF EXISTS `x_types_passages`;
|
||||
/*!40101 SET @saved_cs_client = @@character_set_client */;
|
||||
/*!50503 SET character_set_client = utf8mb4 */;
|
||||
CREATE TABLE `x_types_passages` (
|
||||
`id` int unsigned NOT NULL AUTO_INCREMENT,
|
||||
`libelle` varchar(10) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL,
|
||||
`color_button` varchar(15) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL,
|
||||
`color_mark` varchar(15) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL,
|
||||
`color_table` varchar(15) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL,
|
||||
`chk_active` tinyint(1) unsigned NOT NULL DEFAULT '1',
|
||||
PRIMARY KEY (`id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||
|
||||
--
|
||||
-- Table structure for table `x_types_reglements`
|
||||
--
|
||||
|
||||
DROP TABLE IF EXISTS `x_types_reglements`;
|
||||
/*!40101 SET @saved_cs_client = @@character_set_client */;
|
||||
/*!50503 SET character_set_client = utf8mb4 */;
|
||||
CREATE TABLE `x_types_reglements` (
|
||||
`id` int unsigned NOT NULL AUTO_INCREMENT,
|
||||
`libelle` varchar(45) DEFAULT NULL,
|
||||
`chk_active` tinyint(1) unsigned DEFAULT '1',
|
||||
PRIMARY KEY (`id`)
|
||||
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||
|
||||
--
|
||||
-- Table structure for table `x_users_roles`
|
||||
--
|
||||
|
||||
DROP TABLE IF EXISTS `x_users_roles`;
|
||||
|
||||
CREATE TABLE `x_users_roles` (
|
||||
`id` int 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';
|
||||
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||
|
||||
DROP TABLE IF EXISTS `x_users_titres`;
|
||||
/*!40101 SET @saved_cs_client = @@character_set_client */;
|
||||
/*!50503 SET character_set_client = utf8mb4 */;
|
||||
CREATE TABLE `x_users_titres` (
|
||||
`id` int 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';
|
||||
|
||||
DROP TABLE IF EXISTS `x_pays`;
|
||||
/*!40101 SET @saved_cs_client = @@character_set_client */;
|
||||
/*!50503 SET character_set_client = utf8mb4 */;
|
||||
CREATE TABLE `x_pays` (
|
||||
`id` int unsigned NOT NULL AUTO_INCREMENT,
|
||||
`code` varchar(3) DEFAULT NULL,
|
||||
`fk_continent` int unsigned DEFAULT NULL,
|
||||
`fk_devise` int unsigned DEFAULT '1',
|
||||
`libelle` varchar(45) DEFAULT NULL,
|
||||
`chk_active` tinyint(1) unsigned DEFAULT '1',
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `id_UNIQUE` (`id`),
|
||||
CONSTRAINT `x_pays_ibfk_1` FOREIGN KEY (`fk_devise`) REFERENCES `x_devises` (`id`) ON DELETE RESTRICT ON UPDATE CASCADE
|
||||
) ENGINE=InnoDB AUTO_INCREMENT=10 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='Table des pays avec leurs codes';
|
||||
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||
|
||||
|
||||
DROP TABLE IF EXISTS `x_regions`;
|
||||
/*!40101 SET @saved_cs_client = @@character_set_client */;
|
||||
/*!50503 SET character_set_client = utf8mb4 */;
|
||||
CREATE TABLE `x_regions` (
|
||||
`id` int unsigned NOT NULL AUTO_INCREMENT,
|
||||
`fk_pays` int 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`),
|
||||
CONSTRAINT `x_regions_ibfk_1` FOREIGN KEY (`fk_pays`) REFERENCES `x_pays` (`id`) ON DELETE RESTRICT ON UPDATE CASCADE
|
||||
) ENGINE=InnoDB AUTO_INCREMENT=29 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||
|
||||
DROP TABLE IF EXISTS `x_departements`;
|
||||
/*!40101 SET @saved_cs_client = @@character_set_client */;
|
||||
/*!50503 SET character_set_client = utf8mb4 */;
|
||||
CREATE TABLE `x_departements` (
|
||||
`id` int unsigned NOT NULL AUTO_INCREMENT,
|
||||
`code` varchar(3) DEFAULT NULL,
|
||||
`fk_region` int unsigned DEFAULT '1',
|
||||
`libelle` varchar(45) DEFAULT NULL,
|
||||
`chk_active` tinyint(1) unsigned DEFAULT '1',
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `id_UNIQUE` (`id`),
|
||||
CONSTRAINT `x_departements_ibfk_1` FOREIGN KEY (`fk_region`) REFERENCES `x_regions` (`id`) ON DELETE RESTRICT ON UPDATE CASCADE
|
||||
) ENGINE=InnoDB AUTO_INCREMENT=105 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||
|
||||
DROP TABLE IF EXISTS `entites`;
|
||||
/*!40101 SET @saved_cs_client = @@character_set_client */;
|
||||
/*!50503 SET character_set_client = utf8mb4 */;
|
||||
CREATE TABLE `entites` (
|
||||
`id` int 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 unsigned DEFAULT NULL,
|
||||
`fk_type` int 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 '',
|
||||
`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 '1' COMMENT 'Gestion des mots de passe manuelle O/N',
|
||||
`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 unsigned DEFAULT NULL,
|
||||
`updated_at` timestamp NULL DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP COMMENT 'Date de modification',
|
||||
`fk_user_modif` int unsigned DEFAULT NULL,
|
||||
`chk_active` tinyint(1) unsigned DEFAULT '1',
|
||||
PRIMARY KEY (`id`),
|
||||
CONSTRAINT `entites_ibfk_1` FOREIGN KEY (`fk_region`) REFERENCES `x_regions` (`id`) ON DELETE RESTRICT ON UPDATE CASCADE,
|
||||
CONSTRAINT `entites_ibfk_2` FOREIGN KEY (`fk_type`) REFERENCES `x_entites_types` (`id`) ON DELETE RESTRICT ON UPDATE CASCADE
|
||||
) ENGINE=InnoDB AUTO_INCREMENT=0 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||
|
||||
DROP TABLE IF EXISTS `x_villes`;
|
||||
/*!40101 SET @saved_cs_client = @@character_set_client */;
|
||||
/*!50503 SET character_set_client = utf8mb4 */;
|
||||
CREATE TABLE `x_villes` (
|
||||
`id` int unsigned NOT NULL AUTO_INCREMENT,
|
||||
`fk_departement` int unsigned DEFAULT '1',
|
||||
`libelle` varchar(65) DEFAULT NULL,
|
||||
`cp` varchar(5) DEFAULT NULL,
|
||||
`code_insee` varchar(5) DEFAULT NULL,
|
||||
`departement` varchar(65) DEFAULT NULL,
|
||||
`chk_active` tinyint(1) unsigned DEFAULT '1',
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `id_UNIQUE` (`id`),
|
||||
CONSTRAINT `x_villes_ibfk_1` FOREIGN KEY (`fk_departement`) REFERENCES `x_departements` (`id`) ON DELETE RESTRICT ON UPDATE CASCADE
|
||||
) ENGINE=InnoDB AUTO_INCREMENT=0 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||
|
||||
DROP TABLE IF EXISTS `users`;
|
||||
/*!40101 SET @saved_cs_client = @@character_set_client */;
|
||||
/*!50503 SET character_set_client = utf8mb4 */;
|
||||
CREATE TABLE `users` (
|
||||
`id` int unsigned NOT NULL AUTO_INCREMENT,
|
||||
`fk_entite` int unsigned DEFAULT '1',
|
||||
`fk_role` int unsigned DEFAULT '1',
|
||||
`fk_titre` int 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 unsigned DEFAULT NULL,
|
||||
`updated_at` timestamp NULL DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP COMMENT 'Date de modification',
|
||||
`fk_user_modif` int unsigned DEFAULT NULL,
|
||||
`chk_active` tinyint(1) unsigned DEFAULT '1',
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `fk_entite` (`fk_entite`),
|
||||
KEY `username` (`encrypted_user_name`),
|
||||
CONSTRAINT `users_ibfk_1` FOREIGN KEY (`fk_entite`) REFERENCES `entites` (`id`) ON DELETE RESTRICT ON UPDATE CASCADE,
|
||||
CONSTRAINT `users_ibfk_2` FOREIGN KEY (`fk_role`) REFERENCES `x_users_roles` (`id`) ON DELETE RESTRICT ON UPDATE CASCADE,
|
||||
CONSTRAINT `users_ibfk_3` FOREIGN KEY (`fk_titre`) REFERENCES `x_users_titres` (`id`) ON DELETE RESTRICT ON UPDATE CASCADE
|
||||
) ENGINE=InnoDB AUTO_INCREMENT=0 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||
|
||||
DROP TABLE IF EXISTS `operations`;
|
||||
/*!40101 SET @saved_cs_client = @@character_set_client */;
|
||||
/*!50503 SET character_set_client = utf8mb4 */;
|
||||
CREATE TABLE `operations` (
|
||||
`id` int unsigned NOT NULL AUTO_INCREMENT,
|
||||
`fk_entite` int 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 unsigned NOT NULL DEFAULT '0',
|
||||
`updated_at` timestamp NULL DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP COMMENT 'Date de modification',
|
||||
`fk_user_modif` int 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 DELETE RESTRICT ON UPDATE CASCADE
|
||||
) ENGINE=InnoDB AUTO_INCREMENT=0 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||
|
||||
|
||||
DROP TABLE IF EXISTS `ope_sectors`;
|
||||
/*!40101 SET @saved_cs_client = @@character_set_client */;
|
||||
/*!50503 SET character_set_client = utf8mb4 */;
|
||||
CREATE TABLE `ope_sectors` (
|
||||
`id` int unsigned NOT NULL AUTO_INCREMENT,
|
||||
`fk_operation` int unsigned NOT NULL DEFAULT '0',
|
||||
`fk_old_sector` int unsigned NOT NULL DEFAULT '0',
|
||||
`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 unsigned NOT NULL DEFAULT '0',
|
||||
`updated_at` timestamp NULL DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP COMMENT 'Date de modification',
|
||||
`fk_user_modif` int 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 DELETE RESTRICT ON UPDATE CASCADE
|
||||
) ENGINE=InnoDB AUTO_INCREMENT=0 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
|
||||
DROP TABLE IF EXISTS `ope_users`;
|
||||
/*!40101 SET @saved_cs_client = @@character_set_client */;
|
||||
/*!50503 SET character_set_client = utf8mb4 */;
|
||||
CREATE TABLE `ope_users` (
|
||||
`id` int unsigned NOT NULL AUTO_INCREMENT,
|
||||
`fk_operation` int unsigned NOT NULL DEFAULT '0',
|
||||
`fk_user` int unsigned NOT NULL DEFAULT '0',
|
||||
`created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT 'Date de création',
|
||||
`fk_user_creat` int unsigned DEFAULT NULL,
|
||||
`updated_at` timestamp NULL DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP COMMENT 'Date de modification',
|
||||
`fk_user_modif` int unsigned DEFAULT NULL,
|
||||
`chk_active` tinyint(1) unsigned NOT NULL DEFAULT '1',
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `id_UNIQUE` (`id`),
|
||||
CONSTRAINT `ope_users_ibfk_1` FOREIGN KEY (`fk_operation`) REFERENCES `operations` (`id`) ON DELETE RESTRICT ON UPDATE CASCADE,
|
||||
CONSTRAINT `ope_users_ibfk_2` FOREIGN KEY (`fk_user`) REFERENCES `users` (`id`) ON DELETE RESTRICT ON UPDATE CASCADE
|
||||
) ENGINE=InnoDB AUTO_INCREMENT=0 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||
|
||||
DROP TABLE IF EXISTS `email_queue`;
|
||||
/*!40101 SET @saved_cs_client = @@character_set_client */;
|
||||
/*!50503 SET character_set_client = utf8mb4 */;
|
||||
CREATE TABLE `email_queue` (
|
||||
`id` int unsigned NOT NULL AUTO_INCREMENT,
|
||||
`fk_pass` int unsigned NOT NULL DEFAULT '0',
|
||||
`to_email` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
|
||||
`subject` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
|
||||
`body` text COLLATE utf8mb4_unicode_ci,
|
||||
`headers` text COLLATE utf8mb4_unicode_ci,
|
||||
`created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
`status` enum('pending','sent','failed') COLLATE utf8mb4_unicode_ci DEFAULT 'pending',
|
||||
`attempts` int unsigned DEFAULT '0',
|
||||
PRIMARY KEY (`id`)
|
||||
) ENGINE=InnoDB AUTO_INCREMENT=0 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||
|
||||
DROP TABLE IF EXISTS `ope_users_sectors`;
|
||||
/*!40101 SET @saved_cs_client = @@character_set_client */;
|
||||
/*!50503 SET character_set_client = utf8mb4 */;
|
||||
CREATE TABLE `ope_users_sectors` (
|
||||
`id` int unsigned NOT NULL AUTO_INCREMENT,
|
||||
`fk_operation` int unsigned NOT NULL DEFAULT '0',
|
||||
`fk_user` int unsigned NOT NULL DEFAULT '0',
|
||||
`fk_sector` int unsigned NOT NULL DEFAULT '0',
|
||||
`created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT 'Date de création',
|
||||
`fk_user_creat` int unsigned NOT NULL DEFAULT '0',
|
||||
`updated_at` timestamp NULL DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP COMMENT 'Date de modification',
|
||||
`fk_user_modif` int 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 DELETE RESTRICT ON UPDATE CASCADE,
|
||||
CONSTRAINT `ope_users_sectors_ibfk_2` FOREIGN KEY (`fk_user`) REFERENCES `users` (`id`) ON DELETE RESTRICT ON UPDATE CASCADE,
|
||||
CONSTRAINT `ope_users_sectors_ibfk_3` FOREIGN KEY (`fk_sector`) REFERENCES `ope_sectors` (`id`) ON DELETE RESTRICT ON UPDATE CASCADE
|
||||
) ENGINE=InnoDB AUTO_INCREMENT=0 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||
|
||||
DROP TABLE IF EXISTS `ope_users_suivis`;
|
||||
/*!40101 SET @saved_cs_client = @@character_set_client */;
|
||||
/*!50503 SET character_set_client = utf8mb4 */;
|
||||
CREATE TABLE `ope_users_suivis` (
|
||||
`id` int unsigned NOT NULL AUTO_INCREMENT,
|
||||
`fk_operation` int unsigned NOT NULL DEFAULT '0',
|
||||
`fk_user` int unsigned NOT NULL DEFAULT '0',
|
||||
`date_suivi` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT 'Date du suivi',
|
||||
`gps_lat` varchar(20) NOT NULL DEFAULT '',
|
||||
`gps_lng` varchar(20) NOT NULL DEFAULT '',
|
||||
`vitesse` varchar(20) NOT NULL DEFAULT '',
|
||||
`created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT 'Date de création',
|
||||
`fk_user_creat` int unsigned NOT NULL DEFAULT '0',
|
||||
`updated_at` timestamp NULL DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP COMMENT 'Date de modification',
|
||||
`fk_user_modif` int unsigned NOT NULL DEFAULT '0',
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `id_UNIQUE` (`id`)
|
||||
) ENGINE=InnoDB AUTO_INCREMENT=0 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||
|
||||
DROP TABLE IF EXISTS `sectors_adresses`;
|
||||
/*!40101 SET @saved_cs_client = @@character_set_client */;
|
||||
/*!50503 SET character_set_client = utf8mb4 */;
|
||||
CREATE TABLE `sectors_adresses` (
|
||||
`id` int unsigned NOT NULL AUTO_INCREMENT,
|
||||
`fk_adresse` varchar(25) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT 'adresses.cp??.id',
|
||||
`osm_id` int unsigned NOT NULL DEFAULT '0',
|
||||
`fk_sector` int unsigned NOT NULL DEFAULT '0',
|
||||
`osm_name` varchar(50) NOT NULL DEFAULT '',
|
||||
`numero` varchar(5) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '',
|
||||
`rue_bis` varchar(5) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '',
|
||||
`rue` varchar(60) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '',
|
||||
`cp` varchar(5) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '',
|
||||
`ville` varchar(60) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '',
|
||||
`gps_lat` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '',
|
||||
`gps_lng` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci 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 DELETE RESTRICT ON UPDATE CASCADE
|
||||
) ENGINE=InnoDB AUTO_INCREMENT=0 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
DROP TABLE IF EXISTS `ope_pass`;
|
||||
/*!40101 SET @saved_cs_client = @@character_set_client */;
|
||||
/*!50503 SET character_set_client = utf8mb4 */;
|
||||
CREATE TABLE `ope_pass` (
|
||||
`id` int unsigned NOT NULL AUTO_INCREMENT,
|
||||
`fk_operation` int unsigned NOT NULL DEFAULT '0',
|
||||
`fk_sector` int unsigned DEFAULT '0',
|
||||
`fk_user` int 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 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 unsigned DEFAULT '1',
|
||||
`appt` varchar(5) DEFAULT '',
|
||||
`niveau` varchar(5) DEFAULT '',
|
||||
`residence` varchar(75) DEFAULT '',
|
||||
`gps_lat` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '',
|
||||
`gps_lng` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '',
|
||||
`encrypted_name` varchar(255) NOT NULL DEFAULT '',
|
||||
`montant` decimal(7,2) NOT NULL DEFAULT '0.00',
|
||||
`fk_type_reglement` int unsigned DEFAULT '1',
|
||||
`remarque` text DEFAULT '',
|
||||
`encrypted_email` varchar(255) DEFAULT '',
|
||||
`nom_recu` varchar(50) COLLATE utf8mb4_unicode_ci 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 '',
|
||||
`chk_striped` tinyint(1) unsigned DEFAULT '0',
|
||||
`docremis` tinyint(1) unsigned DEFAULT '0',
|
||||
`date_repasser` timestamp NULL DEFAULT NULL COMMENT 'Date prévue pour repasser',
|
||||
`nb_passages` int 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 unsigned DEFAULT NULL,
|
||||
`updated_at` timestamp NULL DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP COMMENT 'Date de modification',
|
||||
`fk_user_modif` int 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` (`email`),
|
||||
CONSTRAINT `ope_pass_ibfk_1` FOREIGN KEY (`fk_operation`) REFERENCES `operations` (`id`) ON DELETE RESTRICT ON UPDATE CASCADE,
|
||||
CONSTRAINT `ope_pass_ibfk_2` FOREIGN KEY (`fk_sector`) REFERENCES `ope_sectors` (`id`) ON DELETE RESTRICT ON UPDATE CASCADE,
|
||||
CONSTRAINT `ope_pass_ibfk_3` FOREIGN KEY (`fk_user`) REFERENCES `users` (`id`) ON DELETE RESTRICT ON UPDATE CASCADE,
|
||||
CONSTRAINT `ope_pass_ibfk_4` FOREIGN KEY (`fk_type_reglement`) REFERENCES `x_types_reglements` (`id`) ON DELETE RESTRICT ON UPDATE CASCADE
|
||||
) ENGINE=InnoDB AUTO_INCREMENT=0 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||
|
||||
DROP TABLE IF EXISTS `ope_pass_histo`;
|
||||
/*!40101 SET @saved_cs_client = @@character_set_client */;
|
||||
/*!50503 SET character_set_client = utf8mb4 */;
|
||||
CREATE TABLE `ope_pass_histo` (
|
||||
`id` int unsigned NOT NULL AUTO_INCREMENT,
|
||||
`fk_pass` int unsigned NOT NULL DEFAULT '0',
|
||||
`date_histo` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT 'Date historique',
|
||||
`sujet` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci 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=0 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||
|
||||
DROP TABLE IF EXISTS `medias`;
|
||||
/*!40101 SET @saved_cs_client = @@character_set_client */;
|
||||
/*!50503 SET character_set_client = utf8mb4 */;
|
||||
CREATE TABLE `medias` (
|
||||
`id` int unsigned NOT NULL AUTO_INCREMENT,
|
||||
`support` varchar(45) NOT NULL DEFAULT '',
|
||||
`support_id` int unsigned NOT NULL DEFAULT '0',
|
||||
`fichier` varchar(250) NOT NULL DEFAULT '',
|
||||
`description` varchar(100) NOT NULL DEFAULT '',
|
||||
`created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
`fk_user_creat` int unsigned NOT NULL DEFAULT '0',
|
||||
`updated_at` timestamp NULL DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP,
|
||||
`fk_user_modif` int unsigned NOT NULL DEFAULT '0',
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `id_UNIQUE` (`id`)
|
||||
) ENGINE=InnoDB AUTO_INCREMENT=0 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||
|
||||
-- Création des tables pour le système de chat
|
||||
DROP TABLE IF EXISTS `chat_rooms`;
|
||||
-- Table des salles de discussion
|
||||
CREATE TABLE chat_rooms (
|
||||
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
||||
name VARCHAR(100) NOT NULL,
|
||||
type ENUM('privee', 'groupe', 'liste_diffusion') NOT NULL,
|
||||
date_creation timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT 'Date de création',
|
||||
fk_user INT UNSIGNED NOT NULL,
|
||||
fk_entite INT UNSIGNED,
|
||||
statut ENUM('active', 'archive') NOT NULL DEFAULT 'active',
|
||||
description TEXT,
|
||||
INDEX idx_user (fk_user),
|
||||
INDEX idx_entite (fk_entite)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
|
||||
DROP TABLE IF EXISTS `chat_participants`;
|
||||
-- Table des participants aux salles de discussion
|
||||
CREATE TABLE chat_participants (
|
||||
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
||||
id_room INT UNSIGNED NOT NULL,
|
||||
id_user INT UNSIGNED NOT NULL,
|
||||
role ENUM('administrateur', 'participant', 'en_lecture_seule') NOT NULL DEFAULT 'participant',
|
||||
date_ajout timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT 'Date ajout',
|
||||
notification_activee BOOLEAN NOT NULL DEFAULT TRUE,
|
||||
INDEX idx_room (id_room),
|
||||
INDEX idx_user (id_user),
|
||||
CONSTRAINT uc_room_user UNIQUE (id_room, id_user),
|
||||
FOREIGN KEY (id_room) REFERENCES chat_rooms(id) ON DELETE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
DROP TABLE IF EXISTS `chat_messages`;
|
||||
-- Table des messages
|
||||
CREATE TABLE chat_messages (
|
||||
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
||||
fk_room INT UNSIGNED NOT NULL,
|
||||
fk_user INT UNSIGNED NOT NULL,
|
||||
content TEXT,
|
||||
date_sent timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT 'Date envoi',
|
||||
type ENUM('texte', 'media', 'systeme') NOT NULL DEFAULT 'texte',
|
||||
statut ENUM('envoye', 'livre', 'lu') NOT NULL DEFAULT 'envoye',
|
||||
INDEX idx_room (fk_room),
|
||||
INDEX idx_user (fk_user),
|
||||
INDEX idx_date (date_sent),
|
||||
FOREIGN KEY (fk_room) REFERENCES chat_rooms(id) ON DELETE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
DROP TABLE IF EXISTS `chat_listes_diffusion`;
|
||||
-- Table des listes de diffusion
|
||||
CREATE TABLE chat_listes_diffusion (
|
||||
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
||||
fk_room INT UNSIGNED NOT NULL,
|
||||
name VARCHAR(100) NOT NULL,
|
||||
description TEXT,
|
||||
fk_user INT UNSIGNED NOT NULL,
|
||||
date_creation timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT 'Date de création',
|
||||
INDEX idx_room (fk_room),
|
||||
INDEX idx_user (fk_user),
|
||||
FOREIGN KEY (fk_room) REFERENCES chat_rooms(id) ON DELETE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
DROP TABLE IF EXISTS `chat_read_messages`;
|
||||
-- Table pour suivre la lecture des messages
|
||||
CREATE TABLE chat_read_messages (
|
||||
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
||||
fk_message INT UNSIGNED NOT NULL,
|
||||
fk_user INT UNSIGNED NOT NULL,
|
||||
date_read timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT 'Date de lecture',
|
||||
INDEX idx_message (fk_message),
|
||||
INDEX idx_user (fk_user),
|
||||
CONSTRAINT uc_message_user UNIQUE (fk_message, fk_user),
|
||||
FOREIGN KEY (fk_message) REFERENCES chat_messages(id) ON DELETE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
DROP TABLE IF EXISTS `chat_notifications`;
|
||||
-- Table des notifications
|
||||
CREATE TABLE chat_notifications (
|
||||
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
||||
fk_user INT UNSIGNED NOT NULL,
|
||||
fk_message INT UNSIGNED,
|
||||
fk_room INT UNSIGNED,
|
||||
type VARCHAR(50) NOT NULL,
|
||||
contenu TEXT,
|
||||
date_creation timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT 'Date de création',
|
||||
date_lecture timestamp NULL DEFAULT NULL COMMENT 'Date de lecture',
|
||||
statut ENUM('non_lue', 'lue') NOT NULL DEFAULT 'non_lue',
|
||||
INDEX idx_user (fk_user),
|
||||
INDEX idx_message (fk_message),
|
||||
INDEX idx_room (fk_room),
|
||||
FOREIGN KEY (fk_message) REFERENCES chat_messages(id) ON DELETE SET NULL,
|
||||
FOREIGN KEY (fk_room) REFERENCES chat_rooms(id) ON DELETE SET NULL
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
DROP TABLE IF EXISTS `z_params`;
|
||||
/*!40101 SET @saved_cs_client = @@character_set_client */;
|
||||
/*!50503 SET character_set_client = utf8mb4 */;
|
||||
CREATE TABLE `params` (
|
||||
`id` int 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;
|
||||
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||
|
||||
|
||||
DROP TABLE IF EXISTS `z_sessions`;
|
||||
/*!40101 SET @saved_cs_client = @@character_set_client */;
|
||||
/*!50503 SET character_set_client = utf8mb4 */;
|
||||
CREATE TABLE `z_sessions` (
|
||||
`sid` text NOT NULL,
|
||||
`fk_user` int 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
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||
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
|
||||
}
|
||||
166
api/docs/nouvelles-routes-session-refresh.txt
Normal file
166
api/docs/nouvelles-routes-session-refresh.txt
Normal file
@@ -0,0 +1,166 @@
|
||||
1. Route /session/refresh/all
|
||||
|
||||
Méthode : POSTAuthentification : Requise (via session_id dans headers ou cookies)
|
||||
|
||||
Headers requis :
|
||||
Authorization: Bearer {session_id}
|
||||
// ou
|
||||
Cookie: session_id={session_id}
|
||||
|
||||
Réponse attendue :
|
||||
{
|
||||
"status": "success",
|
||||
"message": "Session refreshed",
|
||||
"user": {
|
||||
// Mêmes données que le login
|
||||
"id": 123,
|
||||
"email": "user@example.com",
|
||||
"name": "John Doe",
|
||||
"fk_role": 2,
|
||||
"fk_entite": 1,
|
||||
// ...
|
||||
},
|
||||
"amicale": {
|
||||
// Données de l'amicale
|
||||
"id": 1,
|
||||
"name": "Amicale Pompiers",
|
||||
// ...
|
||||
},
|
||||
"operations": [...],
|
||||
"sectors": [...],
|
||||
"passages": [...],
|
||||
"membres": [...],
|
||||
"session_id": "current_session_id",
|
||||
"session_expiry": "2024-01-20T10:00:00Z"
|
||||
}
|
||||
|
||||
Code PHP suggéré :
|
||||
// routes/session.php
|
||||
Route::post('/session/refresh/all', function(Request $request) {
|
||||
$user = Auth::user();
|
||||
if (!$user) {
|
||||
return response()->json(['status' => 'error', 'message' => 'Not authenticated'], 401);
|
||||
}
|
||||
|
||||
// Retourner les mêmes données qu'un login normal
|
||||
return response()->json([
|
||||
'status' => 'success',
|
||||
'user' => $user->toArray(),
|
||||
'amicale' => $user->amicale,
|
||||
'operations' => Operation::where('fk_entite', $user->fk_entite)->get(),
|
||||
'sectors' => Sector::where('fk_entite', $user->fk_entite)->get(),
|
||||
'passages' => Passage::where('fk_entite', $user->fk_entite)->get(),
|
||||
'membres' => Membre::where('fk_entite', $user->fk_entite)->get(),
|
||||
'session_id' => session()->getId(),
|
||||
'session_expiry' => now()->addDays(7)->toIso8601String()
|
||||
]);
|
||||
});
|
||||
|
||||
2. Route /session/refresh/partial
|
||||
|
||||
Méthode : POSTAuthentification : Requise
|
||||
|
||||
Body requis :
|
||||
{
|
||||
"last_sync": "2024-01-19T10:00:00Z"
|
||||
}
|
||||
|
||||
Réponse attendue :
|
||||
{
|
||||
"status": "success",
|
||||
"message": "Partial refresh completed",
|
||||
"sectors": [
|
||||
// Uniquement les secteurs modifiés après last_sync
|
||||
{
|
||||
"id": 45,
|
||||
"name": "Secteur A",
|
||||
"updated_at": "2024-01-19T15:00:00Z",
|
||||
// ...
|
||||
}
|
||||
],
|
||||
"passages": [
|
||||
// Uniquement les passages modifiés après last_sync
|
||||
{
|
||||
"id": 789,
|
||||
"fk_sector": 45,
|
||||
"updated_at": "2024-01-19T14:30:00Z",
|
||||
// ...
|
||||
}
|
||||
],
|
||||
"operations": [...], // Si modifiées
|
||||
"membres": [...] // Si modifiés
|
||||
}
|
||||
|
||||
Code PHP suggéré :
|
||||
// routes/session.php
|
||||
Route::post('/session/refresh/partial', function(Request $request) {
|
||||
$user = Auth::user();
|
||||
if (!$user) {
|
||||
return response()->json(['status' => 'error', 'message' => 'Not authenticated'], 401);
|
||||
}
|
||||
|
||||
$lastSync = Carbon::parse($request->input('last_sync'));
|
||||
|
||||
// Récupérer uniquement les données modifiées après last_sync
|
||||
$response = [
|
||||
'status' => 'success',
|
||||
'message' => 'Partial refresh completed'
|
||||
];
|
||||
|
||||
// Secteurs modifiés
|
||||
$sectors = Sector::where('fk_entite', $user->fk_entite)
|
||||
->where('updated_at', '>', $lastSync)
|
||||
->get();
|
||||
if ($sectors->count() > 0) {
|
||||
$response['sectors'] = $sectors;
|
||||
}
|
||||
|
||||
// Passages modifiés
|
||||
$passages = Passage::where('fk_entite', $user->fk_entite)
|
||||
->where('updated_at', '>', $lastSync)
|
||||
->get();
|
||||
if ($passages->count() > 0) {
|
||||
$response['passages'] = $passages;
|
||||
}
|
||||
|
||||
// Opérations modifiées
|
||||
$operations = Operation::where('fk_entite', $user->fk_entite)
|
||||
->where('updated_at', '>', $lastSync)
|
||||
->get();
|
||||
if ($operations->count() > 0) {
|
||||
$response['operations'] = $operations;
|
||||
}
|
||||
|
||||
// Membres modifiés
|
||||
$membres = Membre::where('fk_entite', $user->fk_entite)
|
||||
->where('updated_at', '>', $lastSync)
|
||||
->get();
|
||||
if ($membres->count() > 0) {
|
||||
$response['membres'] = $membres;
|
||||
}
|
||||
|
||||
return response()->json($response);
|
||||
});
|
||||
|
||||
Points importants pour l'API :
|
||||
|
||||
1. Vérification de session : Les deux routes doivent vérifier que le session_id est valide et non expiré
|
||||
2. Timestamps : Assurez-vous que toutes vos tables ont des colonnes updated_at qui sont mises à jour automatiquement
|
||||
3. Gestion des suppressions : Pour le refresh partiel, vous pourriez ajouter un champ pour les éléments supprimés :
|
||||
{
|
||||
"deleted": {
|
||||
"sectors": [12, 34], // IDs des secteurs supprimés
|
||||
"passages": [567, 890]
|
||||
}
|
||||
}
|
||||
|
||||
4. Optimisation : Pour éviter de surcharger, limitez le refresh partiel aux dernières 24-48h maximum
|
||||
5. Gestion d'erreurs :
|
||||
{
|
||||
"status": "error",
|
||||
"message": "Session expired",
|
||||
"code": "SESSION_EXPIRED"
|
||||
}
|
||||
|
||||
L'app Flutter s'attend à ces formats de réponse et utilisera automatiquement le refresh partiel si la dernière sync
|
||||
date de moins de 24h, sinon elle fera un refresh complet.
|
||||
BIN
api/docs/recu_13718.pdf
Normal file
BIN
api/docs/recu_13718.pdf
Normal file
Binary file not shown.
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.
193
api/docs/traite_batiments.sql
Normal file
193
api/docs/traite_batiments.sql
Normal file
@@ -0,0 +1,193 @@
|
||||
USE batiments;
|
||||
|
||||
-- Table temp pour FFO (nb_niveau, nb_log)
|
||||
DROP TABLE IF EXISTS tmp_ffo_999;
|
||||
CREATE TABLE tmp_ffo_999 (
|
||||
batiment_groupe_id VARCHAR(50),
|
||||
code_departement_insee VARCHAR(5),
|
||||
nb_niveau INT,
|
||||
annee_construction INT,
|
||||
usage_niveau_1_txt VARCHAR(100),
|
||||
mat_mur_txt VARCHAR(100),
|
||||
mat_toit_txt VARCHAR(100),
|
||||
nb_log INT,
|
||||
KEY (batiment_groupe_id)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
|
||||
LOAD DATA LOCAL INFILE '/var/osm/csv/batiment_groupe_ffo_bat.csv'
|
||||
INTO TABLE tmp_ffo_999
|
||||
CHARACTER SET 'UTF8mb4'
|
||||
FIELDS TERMINATED BY ','
|
||||
OPTIONALLY ENCLOSED BY '"'
|
||||
IGNORE 1 LINES;
|
||||
|
||||
-- Table temp pour Adresse (lien BAN)
|
||||
DROP TABLE IF EXISTS tmp_adr_999;
|
||||
CREATE TABLE tmp_adr_999 (
|
||||
wkt TEXT,
|
||||
batiment_groupe_id VARCHAR(50),
|
||||
cle_interop_adr VARCHAR(50),
|
||||
code_departement_insee VARCHAR(5),
|
||||
classe VARCHAR(50),
|
||||
lien_valide TINYINT,
|
||||
origine VARCHAR(50),
|
||||
KEY (batiment_groupe_id),
|
||||
KEY (cle_interop_adr)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
|
||||
LOAD DATA LOCAL INFILE '/var/osm/csv/rel_batiment_groupe_adresse.csv'
|
||||
INTO TABLE tmp_adr_999
|
||||
CHARACTER SET 'UTF8mb4'
|
||||
FIELDS TERMINATED BY ','
|
||||
OPTIONALLY ENCLOSED BY '"'
|
||||
IGNORE 1 LINES;
|
||||
|
||||
-- Table temp pour RNC (copropriétés)
|
||||
DROP TABLE IF EXISTS tmp_rnc_999;
|
||||
CREATE TABLE tmp_rnc_999 (
|
||||
batiment_groupe_id VARCHAR(50),
|
||||
code_departement_insee VARCHAR(5),
|
||||
numero_immat_principal VARCHAR(50),
|
||||
periode_construction_max VARCHAR(50),
|
||||
l_annee_construction VARCHAR(100),
|
||||
nb_lot_garpark INT,
|
||||
nb_lot_tot INT,
|
||||
nb_log INT,
|
||||
nb_lot_tertiaire INT,
|
||||
l_nom_copro VARCHAR(200),
|
||||
l_siret VARCHAR(50),
|
||||
copro_dans_pvd TINYINT,
|
||||
KEY (batiment_groupe_id)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
|
||||
LOAD DATA LOCAL INFILE '/var/osm/csv/batiment_groupe_rnc.csv'
|
||||
INTO TABLE tmp_rnc_999
|
||||
CHARACTER SET 'UTF8mb4'
|
||||
FIELDS TERMINATED BY ','
|
||||
OPTIONALLY ENCLOSED BY '"'
|
||||
IGNORE 1 LINES;
|
||||
|
||||
-- Table temp pour BDTOPO (altitude)
|
||||
DROP TABLE IF EXISTS tmp_topo_999;
|
||||
CREATE TABLE tmp_topo_999 (
|
||||
batiment_groupe_id VARCHAR(50),
|
||||
code_departement_insee VARCHAR(5),
|
||||
l_nature VARCHAR(200),
|
||||
l_usage_1 VARCHAR(200),
|
||||
l_usage_2 VARCHAR(200),
|
||||
l_etat VARCHAR(100),
|
||||
hauteur_mean DECIMAL(10,2),
|
||||
max_hauteur DECIMAL(10,2),
|
||||
altitude_sol_mean DECIMAL(10,2),
|
||||
KEY (batiment_groupe_id)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
|
||||
LOAD DATA LOCAL INFILE '/var/osm/csv/batiment_groupe_bdtopo_bat.csv'
|
||||
INTO TABLE tmp_topo_999
|
||||
CHARACTER SET 'UTF8mb4'
|
||||
FIELDS TERMINATED BY ','
|
||||
OPTIONALLY ENCLOSED BY '"'
|
||||
IGNORE 1 LINES;
|
||||
|
||||
-- Table temp pour Usage principal
|
||||
DROP TABLE IF EXISTS tmp_usage_999;
|
||||
CREATE TABLE tmp_usage_999 (
|
||||
batiment_groupe_id VARCHAR(50),
|
||||
code_departement_insee VARCHAR(5),
|
||||
usage_principal_bdnb_open VARCHAR(100),
|
||||
KEY (batiment_groupe_id)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
|
||||
LOAD DATA LOCAL INFILE '/var/osm/csv/batiment_groupe_synthese_propriete_usage.csv'
|
||||
INTO TABLE tmp_usage_999
|
||||
CHARACTER SET 'UTF8mb4'
|
||||
FIELDS TERMINATED BY ','
|
||||
OPTIONALLY ENCLOSED BY '"'
|
||||
IGNORE 1 LINES;
|
||||
|
||||
-- Table temp pour DLE Enedis (compteurs électriques)
|
||||
DROP TABLE IF EXISTS tmp_dle_999;
|
||||
CREATE TABLE tmp_dle_999 (
|
||||
batiment_groupe_id VARCHAR(50),
|
||||
code_departement_insee VARCHAR(5),
|
||||
millesime VARCHAR(10),
|
||||
nb_pdl_res INT,
|
||||
nb_pdl_pro INT,
|
||||
nb_pdl_tot INT,
|
||||
conso_res DECIMAL(12,2),
|
||||
conso_pro DECIMAL(12,2),
|
||||
conso_tot DECIMAL(12,2),
|
||||
conso_res_par_pdl DECIMAL(12,2),
|
||||
conso_pro_par_pdl DECIMAL(12,2),
|
||||
conso_tot_par_pdl DECIMAL(12,2),
|
||||
KEY (batiment_groupe_id)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
|
||||
LOAD DATA LOCAL INFILE '/var/osm/csv/batiment_groupe_dle_elec_multimillesime.csv'
|
||||
INTO TABLE tmp_dle_999
|
||||
CHARACTER SET 'UTF8mb4'
|
||||
FIELDS TERMINATED BY ','
|
||||
OPTIONALLY ENCLOSED BY '"'
|
||||
IGNORE 1 LINES;
|
||||
|
||||
-- Création de la table finale avec jointure et filtre
|
||||
DROP TABLE IF EXISTS bat999;
|
||||
CREATE TABLE bat999 (
|
||||
batiment_groupe_id VARCHAR(50) PRIMARY KEY,
|
||||
code_departement_insee VARCHAR(5),
|
||||
cle_interop_adr VARCHAR(50),
|
||||
nb_niveau INT,
|
||||
nb_log INT,
|
||||
nb_pdl_tot INT,
|
||||
annee_construction INT,
|
||||
residence VARCHAR(200),
|
||||
usage_principal VARCHAR(100),
|
||||
altitude_sol_mean DECIMAL(10,2),
|
||||
gps_lat DECIMAL(10,7),
|
||||
gps_lng DECIMAL(10,7),
|
||||
KEY (cle_interop_adr),
|
||||
KEY (usage_principal),
|
||||
KEY (nb_log)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
|
||||
INSERT INTO bat999
|
||||
SELECT
|
||||
f.batiment_groupe_id,
|
||||
f.code_departement_insee,
|
||||
a.cle_interop_adr,
|
||||
f.nb_niveau,
|
||||
f.nb_log,
|
||||
d.nb_pdl_tot,
|
||||
f.annee_construction,
|
||||
REPLACE(REPLACE(REPLACE(REPLACE(r.l_nom_copro, '[', ''), ']', ''), '"', ''), ' ', ' ') as residence,
|
||||
u.usage_principal_bdnb_open as usage_principal,
|
||||
t.altitude_sol_mean,
|
||||
NULL as gps_lat,
|
||||
NULL as gps_lng
|
||||
FROM tmp_ffo_999 f
|
||||
INNER JOIN tmp_adr_999 a ON f.batiment_groupe_id = a.batiment_groupe_id AND a.lien_valide = 1
|
||||
LEFT JOIN tmp_rnc_999 r ON f.batiment_groupe_id = r.batiment_groupe_id
|
||||
LEFT JOIN tmp_topo_999 t ON f.batiment_groupe_id = t.batiment_groupe_id
|
||||
LEFT JOIN tmp_usage_999 u ON f.batiment_groupe_id = u.batiment_groupe_id
|
||||
LEFT JOIN tmp_dle_999 d ON f.batiment_groupe_id = d.batiment_groupe_id
|
||||
WHERE u.usage_principal_bdnb_open IN ('Résidentiel individuel', 'Résidentiel collectif', 'Secondaire', 'Tertiaire')
|
||||
AND f.nb_log > 1
|
||||
AND a.cle_interop_adr IS NOT NULL
|
||||
GROUP BY f.batiment_groupe_id;
|
||||
|
||||
-- Mise à jour des coordonnées GPS depuis la base adresses
|
||||
UPDATE bat999 b
|
||||
JOIN adresses.cp999 a ON b.cle_interop_adr = a.id
|
||||
SET b.gps_lat = a.gps_lat, b.gps_lng = a.gps_lng
|
||||
WHERE b.cle_interop_adr IS NOT NULL;
|
||||
|
||||
-- Nettoyage des tables temporaires
|
||||
DROP TABLE IF EXISTS tmp_ffo_999;
|
||||
DROP TABLE IF EXISTS tmp_adr_999;
|
||||
DROP TABLE IF EXISTS tmp_rnc_999;
|
||||
DROP TABLE IF EXISTS tmp_topo_999;
|
||||
DROP TABLE IF EXISTS tmp_usage_999;
|
||||
DROP TABLE IF EXISTS tmp_dle_999;
|
||||
|
||||
-- Historique
|
||||
INSERT INTO _histo SET date_import=NOW(), dept='999', nb_batiments=(SELECT COUNT(*) FROM bat999);
|
||||
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;
|
||||
}
|
||||
154
api/index.php
Normal file → Executable file
154
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,46 @@ 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';
|
||||
require_once __DIR__ . '/src/Controllers/MigrationController.php';
|
||||
require_once __DIR__ . '/src/Controllers/HealthController.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 +75,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;
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user