Aller au contenu

schema

⬇️ Télécharger cette page en Markdown


Table incidents — colonne email_notification_sent

Ajoutée en v3.0.1. Indique si la notification email a été envoyée à la mairie pour cet incident.

ALTER TABLE incidents
  ADD COLUMN email_notification_sent TINYINT(1) NOT NULL DEFAULT 0
  AFTER statut;
Valeur Signification
0 Email non encore envoyé (état initial)
1 Email envoyé avec succès

Rôle dans submit_incident.php : avant d'envoyer l'email, le code vérifie email_notification_sent = 0. En cas de retry WorkManager (l'incident est déjà en base suite à un crash), l'email ne part qu'une seule fois même si la requête est reçue plusieurs fois.

-- Chemin normal : marquer après envoi réussi
UPDATE incidents SET email_notification_sent = 1 WHERE id = ?;

-- Chemin doublon : envoyer si pas encore envoyé
SELECT id, email_notification_sent FROM incidents
WHERE citoyen_id = ? AND type_id = ? AND description = ?
  AND ABS(latitude - ?) < 0.0005 AND ABS(longitude - ?) < 0.0005
  AND created_at > DATE_SUB(NOW(), INTERVAL 6 HOUR)
LIMIT 1;

Table webauthn_credentials (v3.1, 2026-08-25)

Clés de sécurité (WebAuthn/FIDO2) pour la connexion admin en 2 facteurs. Voir Conformité ANSSI § 15 pour le flux complet.

CREATE TABLE webauthn_credentials (
    id            INT AUTO_INCREMENT PRIMARY KEY,
    user_id       INT NOT NULL,
    credential_id VARCHAR(512) CHARACTER SET ascii COLLATE ascii_bin NOT NULL,
    public_key    TEXT NOT NULL,
    sign_counter  INT UNSIGNED NOT NULL DEFAULT 0,
    nom           VARCHAR(100) NULL,
    rp_id         VARCHAR(255) NOT NULL,
    created_at    TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
    last_used_at  TIMESTAMP NULL,
    CONSTRAINT fk_webauthn_credentials_user FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
    UNIQUE KEY uq_webauthn_credential_id (credential_id),
    KEY idx_webauthn_credentials_user_rp (user_id, rp_id)
);

rp_id = domaine d'enregistrement (urbafix.fr/monquartier.fr distincts) ; une ligne = une clé physique, gérée en self-service depuis securite.php.


types et services sont des VUES, pas des tables (v3.1, 2026-08-25)

Découvert en implémentant les alertes SLA. types et services — utilisées partout dans le code applicatif (types.php, services.php, incidents.php, incident_detail.php, Auth.php...) — ne sont pas des tables de base mais des vues :

SHOW CREATE VIEW types;    -- VIEW ... AS SELECT ... FROM types_incident (ordre exposé aussi sous l'alias `priorite`)
SHOW CREATE VIEW services; -- VIEW ... AS SELECT ... FROM services_mairie

Les vraies tables sont types_incident et services_mairie. Les deux vues sont de simples projections column-à-column (pas d'agrégation), donc updatable — INSERT/UPDATE/DELETE via types/services fonctionnent normalement et écrivent directement dans les tables de base (testé et vérifié). Toute ALTER TABLE doit cibler types_incident/services_mairie, jamais types/services — et si une vue doit exposer une nouvelle colonne, il faut la recréer (CREATE OR REPLACE VIEW, en conservant son DEFINER d'origine).

Pas de dérive de données malgré l'apparence trompeuse de duplication : contrairement à photos/photos_incident ou historique_incident/incident_historique (autres paires de noms similaires dans ce schéma), il n'y a ici qu'une seule source de vérité.


Colonnes SLA — types_incident.sla_delai_heures / services_mairie.sla_delai_heures (v3.1, 2026-08-25)

Alertes de délai configurables, avec repli du service vers le type si l'incident n'est attribué à aucun service :

ALTER TABLE services_mairie ADD COLUMN sla_delai_heures INT NULL AFTER telephone;
ALTER TABLE types_incident  ADD COLUMN sla_delai_heures INT NULL AFTER ordre;
-- + recréation des vues `services` et `types` pour exposer la colonne (voir section ci-dessus)

NULL = pas de délai configuré = pas d'alerte (comportement par défaut inchangé tant que rien n'est configuré). Logique de calcul (incidents.php, incident_detail.php, statistiques.php) :

COALESCE(sv.sla_delai_heures, t.sla_delai_heures) as sla_delai_heures,
(
    i.statut NOT IN ('resolu', 'ferme')
    AND COALESCE(sv.sla_delai_heures, t.sla_delai_heures) IS NOT NULL
    AND TIMESTAMPDIFF(HOUR, i.created_at, NOW()) > COALESCE(sv.sla_delai_heures, t.sla_delai_heures)
) as sla_depasse
FROM incidents i
JOIN types_incident t ON i.type_id = t.id
LEFT JOIN services sv ON i.service_id = sv.id

Un incident resolu ou ferme n'est jamais considéré en retard. Configuration via types.php et services.php (champ « Délai d'alerte SLA (heures) »). Voir Interface Administration → Alertes SLA.


Colonne users.incidents_view_pref (v3.1, 2026-08-25)

Préférence liste/kanban sur incidents.php, mémorisée par utilisateur :

ALTER TABLE users
    ADD COLUMN incidents_view_pref ENUM('liste','kanban') NOT NULL DEFAULT 'liste' AFTER role;

Table saved_filters (v3.1, 2026-08-25)

Vues sauvegardées / filtres favoris sur incidents.php, par utilisateur :

CREATE TABLE IF NOT EXISTS saved_filters (
    id INT AUTO_INCREMENT PRIMARY KEY,
    user_id INT NOT NULL,
    page VARCHAR(50) NOT NULL DEFAULT 'incidents',
    nom VARCHAR(100) NOT NULL,
    query_string VARCHAR(500) NOT NULL,
    created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
    CONSTRAINT fk_saved_filters_user FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
    KEY idx_saved_filters_user_page (user_id, page)
);

query_string stocke la query string GET telle quelle (statut=nouveau&priorite=urgente&vue=kanban) — pas de JSON, réappliquée directement en href.


Table app_settings

Table clé-valeur pour la configuration runtime de l'application (sans redémarrage Docker).

CREATE TABLE IF NOT EXISTS app_settings (
    `key`      VARCHAR(100) NOT NULL PRIMARY KEY,
    `value`    TEXT         NOT NULL,
    updated_at TIMESTAMP    NOT NULL DEFAULT CURRENT_TIMESTAMP
                            ON UPDATE CURRENT_TIMESTAMP
);

-- Valeur initiale
INSERT INTO app_settings (`key`, `value`) VALUES ('email_test_mode', '1');

Entrées actuelles

Clé Valeur initiale Description
email_test_mode '1' '1' = emails vers l'adresse de test (test) · '0' = emails vers mairie réelle (prod)

Accès PHP

$row = Database::getInstance()->fetchOne(
    "SELECT `value` FROM app_settings WHERE `key` = ?",
    ['email_test_mode']
);
$isTestMode = $row ? (bool)(int)$row['value'] : true; // fallback TEST si erreur DB

Modification

Via le endpoint POST /api/admin/email_test_mode.php (depuis l'app Android en DEBUG) ou directement en SQL :

UPDATE app_settings SET `value` = '0' WHERE `key` = 'email_test_mode'; -- passer en PROD
UPDATE app_settings SET `value` = '1' WHERE `key` = 'email_test_mode'; -- repasser en TEST

Table registration_tokens

Stocke les tokens de validation générés lors des demandes d'inscription mairie.

CREATE TABLE registration_tokens (
  id         INT(11)      NOT NULL AUTO_INCREMENT,
  token      VARCHAR(64)  NOT NULL,
  mairie_id  INT(11)      NOT NULL,
  expires_at DATETIME     NOT NULL,
  used_at    DATETIME     DEFAULT NULL,
  created_at TIMESTAMP    NOT NULL DEFAULT CURRENT_TIMESTAMP,
  PRIMARY KEY (id),
  UNIQUE KEY token (token),
  KEY expires_at (expires_at),
  CONSTRAINT fk_reg_tokens_mairie
    FOREIGN KEY (mairie_id) REFERENCES mairies(id) ON DELETE CASCADE
);
Colonne Description
token 64 car. hex — bin2hex(random_bytes(32)) — 256 bits d'entropie
mairie_id Référence à mairies.id (CASCADE DELETE)
expires_at created_at + 24h — passé ce délai, le token est rejeté
used_at NULL tant que non consommé ; rempli par confirm_registration.php à la création du compte

Règle : à chaque nouvelle demande pour une même mairie, les tokens non utilisés existants sont supprimés avant d'en générer un nouveau.

Voir Inscription des mairies pour le flux complet.