v2.0 Production 🔒 Chiffrement AES-256 🚀 Stateless-Ready

đŸ“± UrbaFix

Documentation Technique ComplĂšte

Version 1.5.0 Production Janvier 2026

1. Vue d'Ensemble

Qu'est-ce que UrbaFix ?

UrbaFix est une application mobile native Android permettant aux citoyens de signaler des incidents urbains (nids de poule, déchets, éclairage défectueux, etc.) à leur municipalité de maniÚre simple et rapide.

✅
Signalement rapide

30 secondes via smartphone

📍
GPS automatique

Localisation précise

đŸ“·
Photos & Vidéos

Preuve visuelle (vidéo 5s max)

🔄
Mode offline

Sync automatique

Technologies Utilisées

Android

  • Kotlin 1.9.20
  • Jetpack Compose
  • Room Database
  • Retrofit + OkHttp
  • WorkManager

Backend

  • PHP 8.1+
  • MySQL 8.0
  • Nginx
  • Docker Swarm
  • Let's Encrypt

Sécurité

  • AES-256-GCM
  • HKDF
  • SHA-256
  • TLS 1.3
  • Docker Secrets
📊 Chiffres ClĂ©s
  • Temps de signalement : 30 secondes
  • Types d'incidents : 8 catĂ©gories par dĂ©faut
  • Zone gĂ©ographique : France + Monaco
  • Android minimum : Android 8.0 (API 26)

2. Contexte et Objectifs

ProblÚme Résolu

❌ Avant UrbaFix

  • Communication difficile citoyen ↔ mairie
  • DĂ©lais de signalement longs (appels, courriers)
  • Absence de suivi des demandes
  • Pas de preuve photographique

✅ Avec UrbaFix

  • Signalement instantanĂ© via app mobile
  • GĂ©olocalisation GPS automatique
  • Suivi en temps rĂ©el de l'Ă©tat
  • Photos attachĂ©es pour preuve

Objectifs Stratégiques

  1. Améliorer la réactivité municipale

    Réduire le délai moyen de prise en charge de 7 jours à 48h

  2. Augmenter la participation citoyenne

    Objectif : 500 signalements/mois pour une ville de 30 000 habitants

  3. Digitaliser la relation citoyen-mairie

    80% des signalements via app d'ici 2025

3. Acteurs et Utilisateurs

đŸ‘€

Sophie, 34 ans

Citoyenne Active

Profil : MÚre de famille, travaille en télétravail, utilise smartphone quotidiennement

Besoins :

  • Signaler rapidement sans compte
  • Voir l'Ă©tat d'avancement
  • Fonctionnement offline (zones blanches)
đŸ›ïž

Marc, 45 ans

Agent Municipal

Profil : Responsable service voirie, gĂšre 50 incidents/semaine

Besoins :

  • Dashboard centralisĂ©
  • Filtrage par type/statut/date
  • Export pour reporting
đŸ’»

Julie, 28 ans

Développeuse

Profil : Développeuse full-stack, maintient l'application

Besoins :

  • Documentation technique complĂšte
  • API REST bien documentĂ©e
  • Guides de dĂ©ploiement

4. Besoins Fonctionnels

BF-001

Signalement d'Incidents

Description : Un citoyen doit pouvoir signaler un incident urbain en moins de 60 secondes.

CritĂšres d'acceptation :

  • ✓ GPS dĂ©tecte position automatiquement
  • ✓ SĂ©lection type incident via liste prĂ©dĂ©finie
  • ✓ Ajout de 0 Ă  3 photos
  • ✓ Description optionnelle (500 caractĂšres max)
  • ✓ Fonctionne offline avec sync diffĂ©rĂ©e
BF-002

Visualisation sur Carte

Description : Les incidents doivent ĂȘtre affichĂ©s sur une carte interactive (OpenStreetMap).

CritĂšres d'acceptation :

  • ✓ Carte centrĂ©e sur position utilisateur
  • ✓ Markers colorĂ©s par type d'incident
  • ✓ Tap sur marker affiche dĂ©tails
  • ✓ Filtrage par type/statut
BF-003

Suivi des Incidents

Description : Un citoyen peut consulter l'historique de ses signalements.

CritĂšres d'acceptation :

  • ✓ Liste incidents triĂ©e par date
  • ✓ Statuts : En attente / En cours / RĂ©solu / RefusĂ©
  • ✓ DĂ©tails : photos, description, position
  • ✓ Anonymat prĂ©servĂ© (pas de compte obligatoire)
BF-004

Dashboard Mairie

Description : Les mairies disposent d'une interface web pour gérer les incidents.

CritĂšres d'acceptation :

  • ✓ Authentification JWT
  • ✓ Filtrage par statut/type/date
  • ✓ Changement statut incident
  • ✓ Export CSV pour statistiques
BF-005

Gestion RGPD

Description : Conformité RGPD complÚte (anonymat, droit à l'oubli, portabilité).

CritĂšres d'acceptation :

  • ✓ Anonymat par dĂ©faut (fingerprinting)
  • ✓ Droit d'accĂšs (GET /get_citoyen.php)
  • ✓ Droit Ă  l'oubli (DELETE /delete_citoyen.php)
  • ✓ PortabilitĂ© (export JSON)
  • ✓ Chiffrement AES-256-GCM

5. Besoins Non-Fonctionnels

⚡ Performance

Métrique Objectif
Temps de chargement app < 2 secondes
Temps d'envoi incident < 3 secondes (réseau 4G)
Affichage carte (100 incidents) < 1 seconde
Compression photo < 1 seconde par photo

🔒 SĂ©curitĂ©

  • Chiffrement AES-256-GCM pour donnĂ©es sensibles
  • HTTPS uniquement (TLS 1.3)
  • Protection SQL injection (requĂȘtes prĂ©parĂ©es)
  • Protection XSS (Ă©chappement HTML)
  • Rate limiting (10 req/s par IP)
  • Master key en Docker Secret (RAM only)

📈 DisponibilitĂ©

  • SLA : 99.5% (43h downtime/an max)
  • Backup BDD : Quotidien (rĂ©tention 30j)
  • Mode dĂ©gradĂ© : Offline-first (app fonctionne sans rĂ©seau)
  • Monitoring : Nginx logs + alertes email

đŸ“± CompatibilitĂ©

Plateforme Version minimale
Android 8.0 (API 26)
PHP 8.1
MySQL 8.0
Navigateurs (dashboard) Chrome 90+, Firefox 88+

6. Contraintes

🔧 Contraintes Techniques

  • Pas d'identifiants matĂ©riels interdits : IMEI, MAC address (RGPD)
  • Stockage limitĂ© : Max 3 photos par incident, compression JPEG 85%
  • Offline-first obligatoire : Zones rurales sans rĂ©seau
  • OpenStreetMap uniquement : Pas de Google Maps (coĂ»ts)

⚖ Contraintes RĂ©glementaires

  • RGPD (EU 2016/679) : ConformitĂ© complĂšte obligatoire
  • ePrivacy : Pas de tracking sans consentement
  • HĂ©bergement France : DonnĂ©es sensibles sur territoire UE
  • AccessibilitĂ© : WCAG 2.1 niveau AA (dashboard)

💰 Contraintes BudgĂ©taires

  • HĂ©bergement : < 50€/mois (VPS OVH)
  • Pas de licences payantes : Open-source uniquement
  • Bande passante : Optimiser taille photos

7. Parcours Utilisateur Citoyen

1

Premier Lancement

  • Affichage politique confidentialitĂ©
  • Demande permissions GPS + Photos
  • GĂ©nĂ©ration fingerprint (SHA-256)
  • Ouverture sur onglet "Accueil"
2

Signalement Incident

  • Tap onglet "Signaler"
  • GPS dĂ©tecte position automatiquement
  • SĂ©lection type (8 catĂ©gories)
  • Prise de photos (0 Ă  3)
  • Description facultative
  • Tap "Envoyer"
3

Confirmation

  • Message "Signalement envoyĂ© avec succĂšs"
  • Si offline : "Sera envoyĂ© quand rĂ©seau disponible"
  • Retour automatique Ă  l'onglet "Accueil"
  • Incident visible sur carte
4

Suivi

  • Onglet "Mes incidents"
  • Liste avec statuts colorĂ©s
  • Tap pour voir dĂ©tails
  • Notification si statut change (optionnel)

8. Parcours Utilisateur Mairie

AccĂšs Dashboard

  1. Navigation vers https://urbafix.fr/dashboard/
  2. Connexion JWT (email + mot de passe)
  3. Affichage liste incidents avec filtres

Traitement Incident

État : En attente

↓ Agent consulte dĂ©tails + photos

Action : Prise en charge

↓ Clic "Marquer en cours"

État : En cours

↓ Intervention terrain

Action : Résolution

↓ Clic "Marquer rĂ©solu"

État : RĂ©solu

✓ ArchivĂ© aprĂšs 2 ans

9. Cas d'Usage Détaillés

CU-001

Signalement Basique (Mode Online)

Acteur principal : Sophie (citoyenne)

Préconditions : App installée, permissions GPS accordées, réseau 4G disponible

Scénario principal :

  1. Sophie constate un nid de poule devant chez elle
  2. Elle ouvre UrbaFix et tap "Signaler"
  3. GPS détecte automatiquement : 43.7737, 7.4951
  4. Elle sélectionne type "Voirie"
  5. Elle prend 2 photos du trou
  6. Elle écrit "Trou dangereux 30cm, voie de droite"
  7. Elle tap "Envoyer"
  8. App compresse photos → 800 KB total
  9. App envoie POST /signaler_mairie_avec_compte.php
  10. API retourne : {"success": true, "incident_id": 1234}
  11. App affiche "Signalement envoyé avec succÚs"
  12. Incident visible dans "Mes incidents" avec statut "En attente"

Postconditions : Incident stocké en base MySQL, visible dans dashboard mairie

CU-002

Signalement en Mode Offline

Acteur principal : Sophie

Préconditions : App installée, aucun réseau disponible

Scénario principal :

  1. Sophie est en zone blanche (pas de 4G)
  2. Elle constate un dépÎt sauvage
  3. Elle ouvre UrbaFix → indicateur "Offline" affichĂ©
  4. Elle tap "Signaler" (fonctionne quand mĂȘme)
  5. GPS détecte position via satellite (pas de réseau nécessaire)
  6. Elle sélectionne type "Déchets"
  7. Elle prend 1 photo
  8. Elle tap "Envoyer"
  9. App sauvegarde en Room Database avec syncStatus = PENDING
  10. Message : "Signalement sauvegardé. Sera envoyé quand réseau disponible."
  11. 15 minutes plus tard, Sophie rentre chez elle (WiFi)
  12. WorkManager détecte réseau disponible
  13. SyncIncidentsWorker démarre automatiquement
  14. Incident envoyé à l'API
  15. syncStatus passe Ă  SYNCED

Postconditions : Incident synchronisé, statut "Synced" dans l'app

10. Architecture Globale

Schéma d'Architecture

┌─────────────────────────────────────────────────┐
│           APPLICATION ANDROID                   │
│  ┌──────────┐  ┌──────────┐  ┌──────────┐      │
│  │  UI      │  │ViewModel │  │Repository│      │
│  │ (Compose)│←→│  (MVVM)  │←→│ (Mediator)      │
│  └──────────┘  └──────────┘  └────┬─────┘      │
│                                    │            │
│               ┌────────────────────┌─────────┐  │
│               â–Œ                    â–Œ         │  │
│         ┌──────────┐        ┌──────────┐    │  │
│         │   Room   │        │ Retrofit │    │  │
│         │ Database │        │   API    │    │  │
│         └──────────┘        └─────┬────┘    │  │
│         (SQLite local)            │         │  │
└───────────────────────────────────┌─────────┘  │
                                    │ HTTPS      │
                                    â–Œ            │
                    ┌────────────────────────────┐│
                    │     BACKEND PHP            ││
                    │  ┌────────────────────┐    ││
                    │  │ Nginx + PHP-FPM    │    ││
                    │  │ - Endpoints REST   │    ││
                    │  │ - CryptoManager    │    ││
                    │  │ - Repositories     │    ││
                    │  └─────────┬──────────┘    ││
                    │            │               ││
                    │            â–Œ               ││
                    │  ┌────────────────────┐    ││
                    │  │   MySQL Database   │    ││
                    │  │ - incidents        │    ││
                    │  │ - citoyens (chiffrĂ©)   ││
                    │  │ - mairies          │    ││
                    │  │ - photos           │    ││
                    │  └────────────────────┘    ││
                    │                            ││
                    │  Docker Secret:            ││
                    │  /run/secrets/master_key   ││
                    └────────────────────────────┘│
                                                  │
                    

Flux de Données

  1. Création incident (online) :

    UI → ViewModel → Repository → Retrofit → API → MySQL

  2. Création incident (offline) :

    UI → ViewModel → Repository → Room → (WorkManager) → Retrofit → API

  3. Lecture incidents :

    UI ← ViewModel ← Repository ← Room (source unique de vĂ©ritĂ©)

  4. Synchronisation :

    WorkManager → Repository → (SELECT PENDING) → Retrofit → API → (UPDATE SYNCED)

11. Architecture Android (MVVM)

Pattern MVVM

VIEW

Composables Jetpack Compose

ReportScreen.kt
  • Affichage UI
  • Gestion interactions
  • collectAsState()
↕
VIEWMODEL

Logique de présentation

ReportViewModel.kt
  • StateFlow<UiState>
  • Gestion Ă©tat UI
  • Appels Repository
↕
MODEL

Logique métier + données

IncidentRepository.kt
  • AccĂšs Room + Retrofit
  • Logique offline-first
  • EntitĂ©s mĂ©tier

Structure des Packages

app/src/main/java/com/example/monquartierkotlin/
├── MainActivity.kt
├── UrbafixApplication.kt
│
├── data/
│   ├── local/
│   │   ├── AppDatabase.kt
│   │   ├── Converters.kt
│   │   ├── entities/
│   │   │   ├── IncidentEntity.kt
│   │   │   ├── PhotoEntity.kt
│   │   │   └── MairieEntity.kt
│   │   └── dao/
│   │       ├── IncidentDao.kt
│   │       └── PhotoDao.kt
│   │
│   ├── remote/
│   │   ├── UrbafixApi.kt
│   │   ├── RetrofitClient.kt
│   │   └── dto/
│   │       ├── IncidentDto.kt
│   │       └── DtoMappers.kt
│   │
│   └── repository/
│       └── IncidentRepository.kt
│
├── ui/
│   ├── screens/
│   │   ├── home/
│   │   │   ├── HomeScreen.kt
│   │   │   └── HomeViewModel.kt
│   │   ├── report/
│   │   │   ├── ReportScreen.kt
│   │   │   └── ReportViewModel.kt
│   │   └── ...
│   │
│   └── components/
│       ├── MapView.kt
│       └── IncidentCard.kt
│
├── workers/
│   └── SyncIncidentsWorker.kt
│
└── utils/
    ├── FingerprintManager.kt
    ├── DeviceIdManager.kt
    └── ImageCompressor.kt
                    

12. Architecture Backend (PHP)

Structure Backend

www/
├── index.php                    # Router principal
├── composer.json
│
├── src/
│   ├── Database.php             # Singleton PDO
│   │
│   ├── Crypto/
│   │   └── CryptoManager.php    # AES-256-GCM
│   │
│   ├── Utils/
│   │   ├── FingerprintValidator.php
│   │   └── AuditLogger.php
│   │
│   └── Repositories/
│       ├── IncidentRepository.php
│       └── CitoyenRepository.php
│
├── ── Endpoints API ──
├── check_mairie.php
├── signaler_mairie_avec_compte.php
├── get_incidents.php
├── update_citoyen.php
└── ...
                    

Pattern Repository

// src/Repositories/IncidentRepository.php

class IncidentRepository {

    private $db;

    public function __construct() {
        $this->db = Database::getInstance();
    }

    public function create($data): int {
        return $this->db->execute(
            "INSERT INTO incidents (...) VALUES (...)",
            [$data]
        );
    }

    public function findByCitoyen(int $citoyenId): array {
        return $this->db->query(
            "SELECT * FROM incidents WHERE citoyen_id = ?",
            [$citoyenId]
        );
    }
}

13. Base de Données

Schéma Complet

-- Table citoyens (donnĂ©es CHIFFRÉES)
CREATE TABLE citoyens (
    id INT AUTO_INCREMENT PRIMARY KEY,
    fingerprint VARCHAR(64) NOT NULL UNIQUE,
    salt VARCHAR(44) NOT NULL,

    -- Chiffrées AES-256-GCM (base64)
    nom TEXT NULL,
    prenom TEXT NULL,
    email TEXT NULL,

    code_postal VARCHAR(5) NOT NULL,
    created_at DATETIME NOT NULL
);

-- Table incidents
CREATE TABLE incidents (
    id INT AUTO_INCREMENT PRIMARY KEY,
    citoyen_id INT NULL,  -- NULL si citoyen supprimé
    mairie_id INT NOT NULL,
    type_id INT NOT NULL,

    description TEXT,
    latitude DECIMAL(10,8) NOT NULL,
    longitude DECIMAL(11,8) NOT NULL,
    code_insee VARCHAR(5),

    status ENUM('en_attente', 'en_cours', 'resolu', 'refuse'),
    created_at DATETIME NOT NULL,
    updated_at DATETIME,
    resolved_at DATETIME,

    FOREIGN KEY (citoyen_id) REFERENCES citoyens(id) ON DELETE SET NULL,  -- Préserve incidents
    FOREIGN KEY (mairie_id) REFERENCES mairies(id),
    FOREIGN KEY (type_id) REFERENCES types(id)
);

-- Index pour performances
CREATE INDEX idx_incidents_mairie_status
    ON incidents(mairie_id, status, created_at DESC);

CREATE INDEX idx_incidents_gps
    ON incidents(latitude, longitude);
                    
🔒 SĂ©curitĂ© et IntĂ©gritĂ© Base de DonnĂ©es
  • DonnĂ©es chiffrĂ©es : nom, prenom, email (AES-256-GCM)
  • Master key : StockĂ©e en Docker Secret (RAM uniquement)
  • PrĂ©servation statistiques : Suppression citoyen → incidents prĂ©servĂ©s avec citoyen_id=NULL
  • Fingerprint : SHA-256, 64 caractĂšres hexadĂ©cimaux
  • GPS sans dĂ©faut : Application mobile sans position GPS par dĂ©faut (position: {lat: null, lng: null})

14. API REST - Endpoints

GET /check_mairie.php

Description : Vérifie si une mairie existe et possÚde un compte actif.

ParamĂštres :

"code_postal": "06500"

Réponse 200 :

{
  "success": true,
  "has_account": true,
  "mairie_id": 42,
  "nom": "Menton"
}
POST /signaler_mairie_avec_compte.php

Description : Envoie un incident vers une mairie ayant un compte.

Content-Type : multipart/form-data

ParamĂštres :

  • citoyen_fingerprint : String (64 chars)
  • mairie_id : Integer
  • type_id : Integer (1-8)
  • description : String (max 500)
  • latitude : Double
  • longitude : Double
  • photo1, photo2, photo3 : File (JPEG, max 5MB)

Réponse 201 :

{
  "success": true,
  "incident_id": 1234,
  "message": "Signalement enregistré avec succÚs"
}
GET /get_incidents.php

Description : RécupÚre la liste des incidents d'un citoyen.

ParamĂštres :

  • citoyen_fingerprint : String
  • limit : Integer (dĂ©faut: 50)
  • offset : Integer (dĂ©faut: 0)

Réponse 200 :

{
  "success": true,
  "total": 127,
  "incidents": [
    {
      "id": 1234,
      "type_nom": "Voirie",
      "description": "Nid de poule",
      "status": "en_cours",
      "created_at": "2024-12-15T10:30:00Z",
      "photos": [...]
    }
  ]
}

Codes d'Erreur HTTP

Code Signification Exemple
200 OK RequĂȘte rĂ©ussie
201 Created Incident créé avec succÚs
400 Bad Request ParamĂštre manquant ou invalide
401 Unauthorized Fingerprint invalide
404 Not Found Ressource non trouvée
500 Internal Server Error Erreur serveur

15. Format des Données

DTOs (Data Transfer Objects)

Les DTOs servent à transférer des données entre l'application Android et l'API backend.

IncidentDto (Android → API)

data class IncidentDto(
    @SerializedName("citoyen_fingerprint")
    val citoyenFingerprint: String,

    @SerializedName("type_id")
    val typeId: Int,

    @SerializedName("latitude")
    val latitude: Double,

    @SerializedName("longitude")
    val longitude: Double
)

Format des Dates

ISO 8601 utilisé partout : 2024-12-15T10:30:00Z

Kotlin

val timestamp = Instant.parse(
    "2024-12-15T10:30:00Z"
).toEpochMilli()

PHP

$datetime = new DateTime(
    '2024-12-15T10:30:00Z'
);

Enums et Constantes

SyncStatus (Android)

enum class SyncStatus {
    PENDING,   // En attente de sync
    SYNCING,   // En cours de synchronisation
    SYNCED,    // Synchronisé avec succÚs
    ERROR      // Erreur lors de la sync
}

IncidentStatus (Backend)

ENUM('en_attente', 'en_cours', 'resolu', 'refuse')

16. Authentification et Sécurité

Device Fingerprinting

Principe : Chaque appareil Android génÚre un identifiant unique basé sur ses caractéristiques matérielles.

1. Android ID

Unique par appareil

2. Build Info

Manufacturer, Model, Hardware

3. Sensors

Liste capteurs (nom, vendor)

4. SHA-256

Hash de tous les composants

Résultat :

a7b3c9e4f1d2e8a6b5c4d3e2f1a0b9c8d7e6f5a4b3c2d1e0f9a8b7c6d5e4f3a2
✓ DĂ©terministe ✓ Stable ✓ Anonyme ✓ Conforme RGPD

Sécurité HTTPS

  • Certificat : Let's Encrypt (renouvellement auto)
  • Protocoles : TLS 1.2, TLS 1.3 uniquement
  • HSTS : Force HTTPS pendant 2 ans
  • Headers : X-Frame-Options, X-Content-Type-Options, X-XSS-Protection
⚠ Protection SQL Injection

Toujours utiliser des requĂȘtes prĂ©parĂ©es (PDO) :

// ✅ BON (sĂ©curisĂ©)
$result = $db->query(
    "SELECT * FROM citoyens WHERE id = ?",
    [$userId]
);

// ❌ MAUVAIS (vulnĂ©rable)
$result = $db->query(
    "SELECT * FROM citoyens WHERE id = $userId"
);

17. SystĂšme de Signalement

Flux Complet

1
Citoyen

Ouvre app, tap "Signaler"

→
2
ReportScreen

Affiche carte + formulaire

→
3
GPS

Détecte coordonnées

→
4
Photos

Compression JPEG 85%

→
5
Repository

Save local + API

→
6
API

INSERT MySQL

Types d'Incidents

đŸ›Łïž Voirie

Nids de poule, chaussée dégradée

💡 Éclairage

Lampadaire défectueux

đŸ—‘ïž DĂ©chets

DépÎt sauvage, poubelle

🚾 Signalisation

Panneau manquant, marquage

🌳 Espaces verts

Arbre dangereux, entretien

đŸȘ‘ Mobilier urbain

Banc cassé, abribus

🎹 Graffiti

Tag, dégradation

❓ Autre

Autre type d'incident

18. Géolocalisation et Cartographie

Détection Position GPS

Stratégie de fallback pour maximiser la précision :

  1. FusedLocationProvider (Google Play Services) : Précision optimale
  2. GPS Provider : Si Play Services indisponible
  3. Network Provider : Si GPS désactivé
  4. Position par défaut : Menton (43.7737, 7.4951)

Intégration OpenStreetMap

BibliothĂšque : osmdroid 6.1.18

Avantages :

  • ✅ Gratuit (pas d'API key)
  • ✅ DonnĂ©es ouvertes (OpenStreetMap)
  • ✅ Offline tiles possible
  • ✅ Pas de limite de requĂȘtes

Géocodage Inverse (Code INSEE)

API : geo.gouv.fr

Endpoint : GET /communes?lat=43.7737&lon=7.4951

Réponse :

{
  "nom": "Menton",
  "code": "06083",
  "codesPostaux": ["06500"]
}

Utilité : Identifier la commune précise (plusieurs communes peuvent partager un code postal).

19. Gestion des Photos & Vidéos

đŸ“· Compression d'Images

Objectif : Réduire la taille des photos pour optimiser upload et stockage.

ParamĂštres :

  • Max Width : 1920px
  • Max Height : 1080px
  • JPEG Quality : 85%
  • Format de sortie : Base64 (pour stockage Room)

RĂ©sultat typique : Photo 4MB → 400KB (-90%)

đŸŽ„ Support VidĂ©o (v1.5.0)

⚡ Nouveau : Ajout du support vidĂ©o avec limitations optimisĂ©es pour le mobile

Caractéristiques Vidéo

  • DurĂ©e maximale : 5 secondes (troncature automatique si dĂ©passement)
  • Taille maximale : 5 MB
  • Format : MP4 (conversion automatique via MediaMuxer)
  • Upload : Base64 (compatible infrastructure existante)

Traitement Vidéo (VideoCompressor.kt)

suspend fun getVideoInfo(context: Context, uri: Uri): VideoInfo
suspend fun compressVideo(context: Context, uri: Uri): File?
suspend fun trimVideoTo5Seconds(context: Context, uri: Uri): File?

Interface Utilisateur

  • 4 boutons de capture : Photo, Galerie, VidĂ©o 5s, Galerie ▶
  • Aperçu vidĂ©os avec indicateurs visuels :
    • Bordure bleue (2dp) pour distinguer des photos
    • IcĂŽne ▶ centrĂ©e
    • Badge durĂ©e en bas Ă  droite (ex: "3s")
  • Compteur sĂ©parĂ© : "2 photo(s) ‱ 1 vidĂ©o(s)"

⚠ Avertissement RGPD Obligatoire

⚠ Important - Protection des donnĂ©es

Contrairement aux photos, le floutage automatique des visages n'est pas disponible pour les vidéos.

📋 Recommandations RGPD :
  • Évitez de filmer des personnes identifiables
  • Concentrez-vous sur le problĂšme Ă  signaler
  • Respectez la vie privĂ©e des passants

En continuant, l'utilisateur confirme avoir pris connaissance de ces recommandations et s'engage à respecter la protection des données personnelles.

Base de Données

@Entity(tableName = "photos_incident")
data class PhotoEntity(
    val mediaType: MediaType = MediaType.PHOTO,  // PHOTO ou VIDEO
    val durationMs: Long? = null,                // Durée vidéo en ms
    val photoData: String? = null,               // Base64
    // ... autres champs
)

enum class MediaType { PHOTO, VIDEO }

🔌 API Backend - Upload VidĂ©os

Format JSON (nouveau en v1.5.0) :

// Structure DTO
data class VideoData(
    val video: String,        // "data:video/mp4;base64,..."
    val latitude: Double?,    // Coordonnées GPS
    val longitude: Double?
)

data class SubmitIncidentRequest(
    val mairieId: Long,
    val typeId: Long,
    val adresse: String,
    val latitude: Double,
    val longitude: Double,
    val description: String,
    val deviceId: String,
    val photos: List<String>?,      // Base64 avec prefix
    val videos: List<VideoData>?    // Objets avec GPS
)

Endpoint API :

  • POST /api/submit_incident.php
  • Content-Type: application/json
  • MĂ©thode Retrofit : submitIncidentJson(@Body request)

Traitement PHP (submit_incident.php:176-241) :

// Extraction tableau videos depuis JSON
foreach ($data['videos'] as $index => $videoData) {
    $videoBase64 = $videoData['video'];  // "data:video/mp4;base64,..."
    $videoLat = $videoData['latitude'];
    $videoLng = $videoData['longitude'];

    // Décodage et sauvegarde dans /uploads/videos/
    $filename = $incidentId . '_video_' . time() . '_' . $index . '.mp4';

    // Insertion dans table videos_incident
    INSERT INTO videos_incident (
        incident_id, type, filepath, filename,
        latitude, longitude, filesize, mime_type
    ) VALUES (...)
}

Table videos_incident (serveur MySQL) :

  • Colonnes principales : id, incident_id, type, filename, filepath, description
  • MĂ©tadonnĂ©es : duration (secondes), filesize (octets), mime_type
  • GPS : latitude, longitude (DECIMAL 10,8 et 11,8)
  • SĂ©curitĂ© : encrypted (TINYINT), created_at (TIMESTAMP)
  • Contrainte : FK incident_id → incidents(id) ON DELETE CASCADE
  • Index : incident_id, type, encrypted, GPS (lat+lng)

Stockage serveur :

  • RĂ©pertoire : /uploads/videos/
  • Format fichier : 123_video_1735917234_0.mp4
  • Format vidĂ©o : MP4 (H.264 + AAC)
  • DurĂ©e max : 5 secondes
  • Taille max : 5 MB

Stockage

đŸ“± Local (Android)

  • Room Database
  • Table photos
  • Colonne photo_data : TEXT (Base64)
  • Compression avant insert

☁ Serveur (Backend)

  • RĂ©pertoire uploads/incidents/
  • Format : incident_1234_1.jpg
  • Thumbnails : thumbs/
  • Permissions : 0644 (lecture publique)

Upload Multipart

// Kotlin - Préparation upload
val file = File(photoPath)
val requestBody = file.asRequestBody(
    "image/jpeg".toMediaTypeOrNull()
)
val part = MultipartBody.Part.createFormData(
    "photo1",
    file.name,
    requestBody
)

20. Synchronisation Offline-First

Architecture Offline-First

Principe : L'application fonctionne d'abord en mode local, puis synchronise quand le réseau est disponible.

1. Création (offline)

INSERT Room Database

syncStatus = PENDING
↓
2. WorkManager

Planifie sync (15 min)

Contrainte: réseau
↓
3. Réseau disponible

Worker démarre

SELECT WHERE PENDING
↓
4. Upload API

POST /signaler_*.php

multipart/form-data
↓
5. Mise Ă  jour

UPDATE Room

syncStatus = SYNCED

SyncIncidentsWorker

Déclenchement :

  • PĂ©riodicitĂ© : 15 minutes
  • Contrainte : RĂ©seau disponible (CONNECTED)
  • Backoff : Exponentiel si Ă©chec

Logique :

  1. SELECT incidents WHERE syncStatus = PENDING
  2. Pour chaque incident :
    • UPDATE syncStatus = SYNCING
    • Appel API avec photos
    • Si succĂšs : syncStatus = SYNCED + serverId
    • Si Ă©chec : syncStatus = ERROR
  3. Retourner Result.success() ou Result.retry()
💡 Avantages Offline-First
  • Fonctionne en zones sans rĂ©seau (rural, tunnel)
  • UX immĂ©diate (pas d'attente rĂ©seau)
  • RĂ©silience aux coupures rĂ©seau
  • Économie de batterie (moins de requĂȘtes)

21. Chiffrement des Données

✅ DÉPLOYÉ EN PRODUCTION - 19 DĂ©cembre 2024

Le chiffrement AES-256-GCM a été déployé avec succÚs sur le serveur de production.

Architecture de Chiffrement Implémentée

Master Key

Fichier sécurisé

32 bytes random (256 bits) /run/secrets/app_encryption_key
↓ AES-256-GCM
IV Unique

Par opération

12 bytes random (96 bits) Généré à chaque chiffrement
↓ Encode
Données Chiffrées

Base64 encoded

IV || ciphertext || tag Stocké en TEXT (MySQL)
📋 Format de Stockage
  • IV: 12 bytes (96 bits) - Vecteur d'initialisation unique
  • Ciphertext: Taille variable selon donnĂ©e en clair
  • Tag: 16 bytes (128 bits) - Tag d'authentification GCM
  • Total: EncodĂ© en Base64 et stockĂ© en TEXT

Données Chiffrées (Table citoyens)

Champ Type BDD Chiffré ? Raison
nom TEXT ✓ OUI DonnĂ©es personnelles (RGPD Art. 32)
prenom TEXT ✓ OUI DonnĂ©es personnelles (RGPD Art. 32)
email TEXT ✓ OUI DonnĂ©es personnelles (RGPD Art. 32)
telephone TEXT ✓ OUI DonnĂ©es personnelles (RGPD Art. 32)
device_id VARCHAR(255) ✗ NON Hash SHA-256 anonyme (fingerprint)
Description incidents TEXT ✗ NON NĂ©cessaire pour recherche full-text
GPS (lat/lng) DECIMAL ✗ NON NĂ©cessaire pour clustering carte

Implémentation Technique

Classe EncryptionManager (PHP)

class EncryptionManager {
    private const SECRET_PATH = '/run/secrets/app_encryption_key';
    private const CIPHER_METHOD = 'aes-256-gcm';
    private const IV_LENGTH = 12;  // 96 bits
    private const TAG_LENGTH = 16; // 128 bits

    public function encrypt(?string $plaintext): ?string {
        if ($plaintext === null || $plaintext === '') {
            return null;
        }

        // Générer IV unique
        $iv = random_bytes(self::IV_LENGTH);
        $tag = '';

        // Chiffrer avec AES-256-GCM
        $ciphertext = openssl_encrypt(
            $plaintext,
            self::CIPHER_METHOD,
            $this->key,
            OPENSSL_RAW_DATA,
            $iv,
            $tag
        );

        // Retourner: base64(IV || ciphertext || tag)
        return base64_encode($iv . $ciphertext . $tag);
    }
}

Hook beforeSave (update_profile.php)

$encryption = EncryptionManager::getInstance();

// Chiffrer avant sauvegarde
$nomChiffre = $encryption->encrypt($nom);
$prenomChiffre = $encryption->encrypt($prenom);
$emailChiffre = $encryption->encrypt($email);
$telephoneChiffre = $encryption->encrypt($telephone);

// UPDATE avec données chiffrées
$db->execute(
    "UPDATE citoyens SET nom = ?, prenom = ?, email = ?, telephone = ? WHERE id = ?",
    [$nomChiffre, $prenomChiffre, $emailChiffre, $telephoneChiffre, $id]
);

Hook afterLoad (get_profile.php)

$encryption = EncryptionManager::getInstance();

// Récupérer depuis BDD (chiffré)
$citoyen = $db->fetchOne("SELECT * FROM citoyens WHERE device_id = ?", [$deviceId]);

// Déchiffrer aprÚs lecture
$nomDechiffre = $encryption->decrypt($citoyen['nom']);
$prenomDechiffre = $encryption->decrypt($citoyen['prenom']);
$emailDechiffre = $encryption->decrypt($citoyen['email']);
$telephoneDechiffre = $encryption->decrypt($citoyen['telephone']);

Stockage de la Clé

⚠ SĂ©curitĂ© Critique

La clé de chiffrement est stockée dans un fichier sécurisé sur le serveur avec permissions restrictives.

Aspect Implémentation
Emplacement serveur /srv/urbafix/secrets/app_encryption_key
Emplacement conteneur /run/secrets/app_encryption_key
Permissions fichier 600 (lecture/écriture propriétaire uniquement)
Permissions dossier 700 (accÚs propriétaire uniquement)
Montage Docker Volume en lecture seule (:ro)
Backup Sauvegardé hors serveur (gestionnaire de secrets)

Configuration docker-compose.yml

services:
  web:
    volumes:
      - ./:/var/www/html
      - ./secrets/app_encryption_key:/run/secrets/app_encryption_key:ro

Architecture Mobile vs Dashboard

đŸ“± Application Android
  • Ne peut PAS dĂ©chiffrer les donnĂ©es du serveur
  • Raison: Pas d'accĂšs Ă  la clĂ© de chiffrement (sĂ©curitĂ© serveur)
  • Source de vĂ©ritĂ©: SharedPreferences local (donnĂ©es en clair)
  • RĂŽle API: Backup chiffrĂ© uniquement (write-only)
đŸ–„ïž Dashboard Web
  • Peut dĂ©chiffrer les donnĂ©es du serveur
  • Raison: AccĂšs Ă  la clĂ© via le conteneur Docker
  • Endpoint: get_profile.php avec dĂ©chiffrement automatique
  • Usage: Visualisation et gestion des profils citoyens
Flux de données:
Android App
    ↓ (donnĂ©es en clair)
SharedPreferences (local, source de vérité)
    ↓ (sync via update_profile.php)
API Backend (chiffrement AES-256-GCM)
    ↓ (stockage)
Base de données MySQL (données chiffrées en TEXT)
    ↓ (lecture via get_profile.php)
Dashboard Web (déchiffrement automatique)
✅ BĂ©nĂ©fices du Chiffrement
  • Protection contre vol de BDD: Dump SQL inutilisable sans la clĂ©
  • ConformitĂ© RGPD Article 32: Mesures techniques appropriĂ©es
  • ConfidentialitĂ© + IntĂ©gritĂ©: GCM garantit authentification
  • SĂ©paration clĂ©/donnĂ©es: Master key hors de la base de donnĂ©es
  • IV unique: Pas de rĂ©utilisation, sĂ©curitĂ© maximale
  • Migration transparente: DonnĂ©es existantes converties progressivement

Documentation ComplĂšte

Pour plus d'informations sur le déploiement, la sécurité et le dépannage :

  • 📄 README_ENCRYPTION.md - Guide complet de dĂ©ploiement
  • 📄 DEPLOYMENT_SUCCESS.md - Rapport de dĂ©ploiement production
  • 📄 migration_encrypt_citoyens.sql - Script de migration SQL
  • 📄 EncryptionManager.php - Classe de chiffrement (src/)

22. Gestion des Identités

Anonymat par Défaut

Principe : L'utilisateur est anonyme jusqu'à ce qu'il décide de renseigner ses informations.

1
Premier lancement

Génération fingerprint unique

a7b3c9e4f1d2...
2
Signalements anonymes

Nom, prénom, email : NULL

10 incidents créés
3
Identification volontaire

Utilisateur remplit profil

nom, prenom, email chiffrés
4
Liaison rétroactive

10 incidents existants liés à l'identité

Via fingerprint unique

Avantages

  • ✅ BarriĂšre d'entrĂ©e minimale (pas de compte obligatoire)
  • ✅ ConformitĂ© RGPD (minimisation des donnĂ©es)
  • ✅ ContrĂŽle utilisateur sur son identitĂ©
  • ✅ Persistance entre rĂ©installations (fingerprint stable)

23. Conformité RGPD

Principes RGPD Appliqués

Principe Article Implémentation UrbaFix
Minimisation des données Art. 5.1.c Nom/prénom/email facultatifs, anonymat par défaut
Limitation conservation Art. 5.1.e Incidents résolus supprimés aprÚs 2 ans
Intégrité et confidentialité Art. 5.1.f Chiffrement AES-256-GCM, HTTPS, fingerprinting
Droit d'accÚs Art. 15 GET /get_citoyen.php retourne toutes les données
Droit Ă  l'oubli Art. 17 DELETE /delete_citoyen.php supprime tout
Portabilité Art. 20 GET /export_data.php export JSON complet
Privacy by Design Art. 25 Fingerprinting au lieu de compte obligatoire

Droits des Utilisateurs

📖 Droit d'accùs

Consulter ses données dans l'onglet "Profil"

GET /get_citoyen.php

đŸ—‘ïž Droit Ă  l'oubli

Supprimer son compte Ă  tout moment

DELETE /delete_citoyen.php

📩 PortabilitĂ©

Exporter ses données au format JSON

GET /export_data.php
📋 Base LĂ©gale

Article 6.1.a (Consentement) : Pour le stockage du nom/prénom/email (facultatif)

Article 6.1.f (IntĂ©rĂȘt lĂ©gitime) : Pour la gestion des incidents (intĂ©rĂȘt public local)

24. Guide Développeur Android

Setup Environnement

Prérequis :

  • Android Studio Hedgehog (2023.1.1+)
  • JDK 17
  • Android SDK 34
  • Émulateur Android 8.0+ (API 26)

Installation :

git clone <repo_url> UrbafixKotlin
cd UrbafixKotlin
./gradlew build

Commandes Utiles

./gradlew clean assembleDebug

Build APK debug

./gradlew installDebug

Installer sur appareil

./gradlew test

Lancer tests unitaires

adb logcat | grep ViewModel

Voir logs en temps réel

Bonnes Pratiques

  • ✓ Toujours passer par ViewModel → Repository → DAO
  • ✓ Utiliser viewModelScope pour coroutines
  • ✓ Dispatchers.IO pour base de donnĂ©es/rĂ©seau
  • ✓ StateFlow pour exposer l'Ă©tat UI
  • ✓ Pattern sealed class pour Ă©tats (Loading, Success, Error)
  • ✓ Sauvegarder en local d'abord (offline-first)

25. Guide Développeur Backend

Setup Local

cd /home/franck/AndroidStudioProjects/UrbaFix/www
composer install
cp config/database.example.php config/database.php

# Créer base de données
mysql -u root -p < database/schema.sql
⚠ ATTENTION : SSHFS Production

Le répertoire www/ est un montage SSHFS vers la production.

Toute modification est LIVE immédiatement !

OBLIGATOIRE avant modification :

./backup-before-deploy.sh "Description changement"

Rollback si problĂšme :

LATEST=$(ls -t backups/ | head -1)
cp -r backups/$LATEST/* www/

Créer un Endpoint

  1. Créer fichier www/mon_endpoint.php
  2. Inclure Database.php et utilitaires nécessaires
  3. Valider paramÚtres d'entrée
  4. Utiliser requĂȘtes prĂ©parĂ©es (PDO)
  5. Retourner JSON avec header Content-Type: application/json
  6. Gérer codes HTTP appropriés (200, 400, 404, 500)

26. Guide Déploiement

Déploiement Android (Google Play)

  1. Build release :
    ./gradlew clean assembleRelease
  2. Signer APK :
    jarsigner -keystore urbafix-release.keystore app-release.apk
  3. Upload : Google Play Console → Production
  4. Review : Attendre validation (1-3 jours)

Déploiement Backend (Production)

Architecture :

  • VPS OVH (Debian 12)
  • Nginx + PHP 8.1-FPM
  • MySQL 8.0
  • Let's Encrypt SSL

Déploiement :

ssh root@urbafix.fr
cd /var/www/urbafix
git pull origin main
composer install --no-dev
systemctl restart php8.1-fpm nginx

Docker (Recommandé)

# Générer master key
openssl rand -base64 32 | docker secret create urbafix_master_key -

# Démarrer stack
docker-compose up -d

# Vérifier logs
docker-compose logs -f php

Docker Hardened Images (DHI)

État : En Évaluation

Docker Hardened Images sont des images sécurisées rendues gratuites depuis le 17 décembre 2025.

Authentification :

# Se connecter avec PAT Docker Hub
docker login dhi.io -u username -p YOUR_PAT

Disponibilité pour UrbaFix :

  • ✓ PHP 8.2 : Disponible mais CLI uniquement (pas FPM)
  • ✗ MariaDB : Non disponible dans le catalogue
  • ✗ MySQL : Erreurs lors du pull

Images actuelles (Production) :

# Dockerfile
FROM php:8.2-fpm

# docker-compose.yml
db:
  image: mariadb:10.11
phpmyadmin:
  image: phpmyadmin:latest

CritĂšres pour adoption DHI :

  1. Disponibilité de dhi.io/php:8.2-fpm
  2. Support de MariaDB ou MySQL stable
  3. Tests de compatibilité réussis

Alternative sécurisée immédiate : Utiliser les variantes Alpine des images officielles pour réduire la surface d'attaque.

Ressources :

27. Glossaire

AES-256-GCM

Advanced Encryption Standard avec Galois/Counter Mode. Chiffrement symétrique avec authentification intégrée (AEAD).

DAO

Data Access Object. Pattern pour abstraire l'accÚs à la base de données.

DHI (Docker Hardened Images)

Images Docker sécurisées avec surface d'attaque réduite, SBOM intégré, et mises à jour de sécurité réguliÚres. Disponibles sur dhi.io depuis décembre 2025.

Docker Secret

Mécanisme Docker pour stocker des données sensibles de maniÚre sécurisée (RAM uniquement, jamais écrit sur disque).

DTO

Data Transfer Object. Objet utilisĂ© pour transfĂ©rer des donnĂ©es entre couches (API ↔ App).

Fingerprint

Empreinte unique d'un appareil basée sur ses caractéristiques matérielles et logicielles (SHA-256).

HKDF

Hash-based Key Derivation Function (RFC 5869). Dérive des clés cryptographiques à partir d'une clé maßtre.

MVVM

Model-View-ViewModel. Pattern architectural séparant logique métier, état et interface.

Offline-First

Architecture oĂč l'application fonctionne d'abord en mode local, puis synchronise quand rĂ©seau disponible.

Room

BibliothĂšque Android ORM (Object-Relational Mapping) pour SQLite.

WorkManager

API Android pour planifier des tùches asynchrones avec contraintes (réseau, batterie).

28. FAQ Technique

Q : Pourquoi un fingerprint au lieu de login/mot de passe ?

R : Pour réduire la friction et maximiser l'adoption. Créer un compte est une barriÚre psychologique. Le fingerprinting permet :

  • ✅ Utilisation immĂ©diate (0 friction)
  • ✅ Pas de mot de passe Ă  retenir
  • ✅ Anonymat par dĂ©faut (conformitĂ© RGPD)
  • ✅ Persistance entre rĂ©installations

Q : Que se passe-t-il si l'utilisateur change de téléphone ?

R : Le nouveau téléphone aura un nouveau fingerprint, donc l'historique des anciens incidents n'est pas accessible. L'utilisateur peut recommencer à signaler normalement. Une solution future serait d'implémenter un transfert de compte via email.

Q : Comment gérer les incidents en doublon ?

R : ImplĂ©menter une dĂ©tection de proximitĂ© cĂŽtĂ© API : avant insertion, vĂ©rifier s'il existe dĂ©jĂ  un incident du mĂȘme type dans un rayon de 50m créé dans les 7 derniers jours. Si oui, retourner HTTP 409 (Conflict) avec l'ID de l'incident existant.

Q : Est-ce que le chiffrement ralentit l'application ?

R : Impact négligeable. AES-256-GCM prend ~1-2ms pour 500 caractÚres. Le bottleneck est le réseau (50-200ms), pas le chiffrement.

29. Roadmap et Évolutions

✓ ComplĂ©tĂ©

Phase 1 : MVP

  • ✓ Signalement avec GPS + photos
  • ✓ Synchronisation offline-first
  • ✓ Carte OpenStreetMap
  • ✓ Fingerprinting device
  • ✓ Chiffrement AES-256-GCM
⏳ En cours

Phase 2 : Améliorations UX (3 mois)

  • Notifications push (Firebase)
  • DĂ©tection doublons radius-based
  • Filtres avancĂ©s sur carte
  • Mode sombre
  • Tutorial onboarding
📋 PlanifiĂ©

Phase 3 : Fonctionnalités (6 mois)

  • Commentaires sur incidents
  • SystĂšme de votes (upvote)
  • CatĂ©gories personnalisĂ©es mairies
  • Zones de travaux
  • Export PDF rapports
🔼 Futur

Phase 4 : Intelligence (12 mois)

  • ML prĂ©dictif (dĂ©lai rĂ©solution)
  • Classification auto via IA
  • Heatmap incidents
  • Analyse tendances
  • Recommandations citoyens