Skip to content

How-to : API de suppression douce, corbeille et restauration

Référence FT : FT340 (NENE2-FT/softlog) — API de notes avec suppression douce (deleted_at), vue corbeille, restauration, suppression physique permanente, purge en masse, tri épinglé en premier, et évaluation d'attaque cracker-mindset ATK, 26 tests / 60+ assertions PASS.

Ce guide montre comment implémenter un cycle de vie de suppression en deux étapes : les éléments sont d'abord supprimés doucement (déplacés vers la corbeille) et peuvent être restaurés, puis effacés définitivement via une suppression physique explicite ou une purge en masse.

Schéma

sql
CREATE TABLE notes (
    id         INTEGER PRIMARY KEY AUTOINCREMENT,
    title      TEXT    NOT NULL,
    body       TEXT    NOT NULL DEFAULT '',
    is_pinned  INTEGER NOT NULL DEFAULT 0,
    deleted_at TEXT,               -- NULL = actif ; ISO 8601 quand supprimé doucement
    created_at TEXT    NOT NULL,
    updated_at TEXT    NOT NULL
);

deleted_at IS NULL = actif ; deleted_at IS NOT NULL = supprimé doucement (dans la corbeille).

Endpoints

MéthodeCheminDescription
POST/notesCréer une note
GET/notesLister les notes actives (épinglées en premier)
GET/notes/{id}Obtenir une note active
PUT/notes/{id}Mettre à jour une note active
DELETE/notes/{id}Suppression douce (→ corbeille)
GET/notes/trashLister les notes dans la corbeille
POST/notes/{id}/restoreRestaurer depuis la corbeille
DELETE/notes/{id}/permanentSuppression physique (permanente)
POST/notes/trash/purgePurger toute la corbeille

Créer une note

php
POST /notes
{"title": "My Note", "body": "Content", "is_pinned": false}
 201
{
  "id": 1,
  "title": "My Note",
  "body": "Content",
  "is_pinned": false,
  "deleted_at": null,
  "created_at": "..."
}

POST /notes  {"body": "No title"}   422  // title requis

Lister les notes actives (épinglées en premier)

php
GET /notes
 200
{
  "total": 3,
  "items": [
    {"id": 2, "title": "Pinned", "is_pinned": true, ...},
    {"id": 1, "title": "Normal A", ...},
    {"id": 3, "title": "Normal B", ...}
  ]
}
sql
SELECT * FROM notes WHERE deleted_at IS NULL
ORDER BY is_pinned DESC, created_at DESC

Les notes supprimées doucement ne sont jamais retournées dans la liste active.

Obtenir une note

php
GET /notes/1
 200  {"id": 1, "title": "My Note", ...}

// Supprimée doucement ou inconnue → même 404
GET /notes/9999 404
GET /notes/1 (après DELETE /notes/1)   404

Mettre à jour une note

php
PUT /notes/1
{"title": "Updated", "body": "New body", "is_pinned": true}
 200  {"title": "Updated", "is_pinned": true, ...}

// Une note supprimée doucement ne peut pas être mise à jour
PUT /notes/1  (après DELETE /notes/1)   404

Suppression douce

php
DELETE /notes/1
 204  (pas de body)

// La note disparaît de GET /notes et GET /notes/1
// Mais apparaît dans GET /notes/trash

DELETE /notes/9999 404  // non trouvé

Vue corbeille

php
GET /notes/trash
 200
{
  "total": 1,
  "items": [
    {"id": 1, "title": "Gone", "deleted_at": "2026-05-27T10:00:00Z", ...}
  ]
}

// Les notes actives NE sont PAS dans la corbeille

deleted_at est non nul pour tous les éléments de la corbeille.

Restaurer

php
POST /notes/1/restore
 200  {"id": 1, "title": "Restore Me", "deleted_at": null, ...}

// La note restaurée réapparaît dans GET /notes
// POST /notes/9999/restore  → 404

Suppression physique (permanente)

php
DELETE /notes/1/permanent
 204  (pas de body ; la note est supprimée de la DB)

// Disparue de la corbeille aussi
// DELETE /notes/9999/permanent  → 404

Purger la corbeille

php
POST /notes/trash/purge
 200  {"purged": 2}

// Corbeille vide
POST /notes/trash/purge 200  {"purged": 0}

purge émet DELETE FROM notes WHERE deleted_at IS NOT NULL et retourne le nombre de lignes.


Évaluation ATK — Test d'attaque cracker-mindset

ATK-01 — Suppression physique sans suppression douce préalable 🚫 BLOCKED

Attaque : L'attaquant appelle DELETE /notes/1/permanent sur une note active (pas encore supprimée doucement). Résultat : BLOCKED — DELETE /notes/{id}/permanent vérifie deleted_at IS NOT NULL avant de procéder. Les notes actives retournent 404 à l'endpoint de suppression permanente ; seuls les éléments dans la corbeille peuvent être supprimés physiquement.


ATK-02 — Accès à une note supprimée doucement via GET direct ✅ SAFE

Attaque : L'attaquant connaît l'ID 5 de la note supprimée doucement et appelle GET /notes/5 en espérant lire du contenu protégé. Résultat : SAFE — GET /notes/{id} interroge WHERE id = ? AND deleted_at IS NULL. Les notes supprimées doucement retournent 404 de manière identique aux notes inconnues — pas d'indice d'existence.


ATK-03 — Purge de la corbeille sans auth (destruction de masse) ⚠️ EXPOSED

Attaque : Tout client appelle POST /notes/trash/purge pour détruire définitivement toutes les notes dans la corbeille appartenant à tous les utilisateurs. Résultat : EXPOSED — Il n'y a pas de vérification d'authentification sur POST /notes/trash/purge. Sans scopage par utilisateur, un client non authentifié peut supprimer irréversiblement toutes les données dans la corbeille pour tous les utilisateurs. Mitigation : exiger l'authentification ; scoper la purge à la corbeille de l'utilisateur authentifié ; exiger le rôle admin pour la purge globale.


ATK-04 — Double suppression douce pour corrompre deleted_at ✅ SAFE

Attaque : L'attaquant envoie DELETE /notes/1 deux fois, espérant que le second appel réinitialise deleted_at à un timestamp ultérieur. Résultat : SAFE — La première suppression définit deleted_at. La seconde trouve deleted_at IS NULL = false, donc la recherche retourne 0 lignes → 404. Le timestamp n'est pas modifié.


ATK-05 — Restaurer une note active (état corrompu) 🚫 BLOCKED

Attaque : L'attaquant appelle POST /notes/1/restore sur une note active (non supprimée) pour forcer deleted_at = null inconditionnellement. Résultat : BLOCKED — restore interroge WHERE id = ? AND deleted_at IS NOT NULL. Les notes actives ne correspondent pas → 404. Idempotent : restaurer une note déjà active est un no-op 404.


ATK-06 — Injection SQL via le titre à la création ✅ SAFE

Attaque : L'attaquant soumet {"title": "'; DROP TABLE notes; --"} pour corrompre la base de données. Résultat : SAFE — Toutes les écritures utilisent des requêtes paramétrées. Le titre est stocké comme chaîne littérale.


ATK-07 — Débordement d'ID de note pour contourner la validation 🚫 BLOCKED

Attaque : L'attaquant envoie GET /notes/99999999999999999999 (20 chiffres) pour déborder l'entier PHP et atteindre des IDs non prévus. Résultat : BLOCKED — Les IDs de note sont validés avec ctype_digit + strlen <= 18 avant conversion. Les valeurs de débordement → 422.


ATK-08 — Mettre à jour une note supprimée (écrire sur un fantôme) 🚫 BLOCKED

Attaque : L'attaquant détient une référence de session périmée vers une note supprimée et soumet un PUT pour la modifier. Résultat : BLOCKED — PUT /notes/{id} interroge WHERE id = ? AND deleted_at IS NULL. Les notes supprimées doucement échouent cette vérification → 404. La mise à jour est rejetée.


ATK-09 — Course : restaurer puis purger immédiatement 🚫 BLOCKED

Attaque : L'attaquant met en concurrence POST /notes/1/restore et POST /notes/trash/purge pour détruire une note en cours de restauration. Résultat : BLOCKED — Chaque opération est une transaction DB atomique unique. La purge émet DELETE WHERE deleted_at IS NOT NULL ; la restauration définit deleted_at = NULL. L'une gagne et la note se retrouve dans un état cohérent.


ATK-10 — Suppression douce concurrente laisse un orphelin ✅ SAFE

Attaque : Deux requêtes appellent simultanément DELETE /notes/1. Les deux vérifient deleted_at IS NULL, voient toutes deux null, et tentent toutes deux de définir deleted_at. Résultat : SAFE — La première mise à jour réussit. La seconde trouve deleted_at IS NOT NULL (ou 0 lignes mises à jour) → 404. SQLite sérialise les écritures ; le second appel est idempotent au niveau DB.


ATK-11 — Titre trop long (abus de stockage) ⚠️ EXPOSED

Attaque : L'attaquant soumet une chaîne de titre de 10 Mo pour épuiser le stockage de la base de données. Résultat : EXPOSED — Aucune longueur maximale n'est imposée sur title ou body. Mitigation : ajouter MAX_TITLE_LENGTH (ex. 500 chars) et MAX_BODY_LENGTH (ex. 100 000 chars), retournant 422 si dépassé. Le middleware de taille de requête fournit une garde secondaire.


ATK-12 — Débordement d'épingles (inondation de notes épinglées) ⚠️ EXPOSED

Attaque : L'attaquant crée des milliers de notes épinglées pour pousser toutes les vraies notes hors du sommet de la liste active. Résultat : EXPOSED — Pas de limite sur le nombre de notes épinglées. Toute note peut être créée avec is_pinned: true. Mitigation : limiter le nombre maximal de notes épinglées par utilisateur (ex. 10) ; retourner 422 si dépassé.


Résumé ATK

IDAttaqueRésultat
ATK-01Suppression physique sans suppression douce🚫 BLOCKED
ATK-02Accès à une note supprimée doucement via GET✅ SAFE
ATK-03Purge de la corbeille sans auth⚠️ EXPOSED
ATK-04Double suppression douce✅ SAFE
ATK-05Restaurer une note active🚫 BLOCKED
ATK-06Injection SQL via le titre✅ SAFE
ATK-07Débordement de l'ID de note🚫 BLOCKED
ATK-08Mettre à jour une note supprimée doucement🚫 BLOCKED
ATK-09Course : restaurer + purger🚫 BLOCKED
ATK-10Suppression douce concurrente✅ SAFE
ATK-11Titre trop long⚠️ EXPOSED
ATK-12Inondation d'épingles⚠️ EXPOSED

7 BLOCKED, 2 SAFE, 3 EXPOSED — Critique : authentifier la purge et la scoper aux données de l'acteur ; ajouter des limites de longueur pour le titre/body ; limiter le nombre de notes épinglées par utilisateur.


Ce qu'il ne faut PAS faire

Anti-patternRisque
Suppression physique au premier DELETEPas de chemin de récupération ; la suppression accidentelle est permanente
Pas de filtre deleted_at IS NULL dans les requêtes liste/getLes éléments supprimés doucement réapparaissent comme encore actifs
Autoriser PUT sur des notes supprimées doucementÉcritures fantômes — utilisateurs éditant des données qu'ils pensaient supprimées
Pas d'auth sur POST /trash/purgeTout client détruit irréversiblement toutes les données dans la corbeille
Retourner 403 pour GET d'une note supprimée doucementRévèle que la note existe ; 404 prévient l'énumération d'existence
Pas de vérification de nombre de lignes après la suppression douce200 silencieux quand la note n'est pas trouvée ; toujours vérifier les lignes affectées

Publié sous licence MIT.