Aller au contenu

Architecture

Vue d'ensemble

flowchart LR
    U["Navigateur"]
    subgraph Dokploy["Dokploy (projet « todos »)"]
        F["frontend<br/>todo.dunkan.xyz<br/>fichiers statiques React"]
        B["Backend<br/>api.todo.dunkan.xyz<br/>Axum :8080"]
        P[("postgres<br/>base todos")]
    end
    C["Clerk"]

    U -- "charge l'app" --> F
    U -- "connexion, getToken()" --> C
    U -- "REST + Authorization: Bearer JWT" --> B
    B -- "clés publiques JWKS" --> C
    B -- "SQL filtré par user_id" --> P
  • Le front est une SPA statique : tous les appels à l'API partent du navigateur, pas du serveur du front.
  • Clerk gère les comptes et les sessions. Le front récupère un JWT de session (getToken()) et l'envoie à chaque requête.
  • L'API vérifie le JWT, puis utilise son sub (identifiant utilisateur Clerk) comme user_id dans toutes les requêtes SQL.

Flux d'une requête authentifiée

sequenceDiagram
    autonumber
    participant N as Navigateur (React)
    participant K as Clerk
    participant A as API Axum
    participant D as PostgreSQL

    N->>K: getToken()
    K-->>N: JWT de session
    N->>A: GET /todo/todo<br/>Authorization: Bearer JWT
    Note over A: CorsLayer vérifie l'origine
    A->>K: JWKS (au premier appel, puis cache mémoire)
    Note over A: ClerkLayer : signature RS256, exp, nbf<br/>sinon 401
    A->>D: SELECT * FROM todos WHERE user_id = sub
    D-->>A: lignes
    A-->>N: 200 + JSON

Backend (todoback/)

Démarrage (main.rs)

  1. Charge un éventuel fichier .env (dotenv).
  2. Lit TEST puis DATABASE_URL : le programme panique si l'une d'elles est absente.
  3. Initialise les logs (tracing, niveau INFO).
  4. Ouvre un pool PostgreSQL (PgPoolOptions, 5 connexions max). Si la base est injoignable, le programme panique.
  5. Construit le routeur (routes::routing), qui lit CLERK_SECRET_KEY (panique si absente).
  6. Écoute sur 0.0.0.0:8080 (port codé en dur).

Migrations

L'API n'exécute pas les migrations au démarrage (pas de sqlx::migrate!). Le schéma doit être appliqué à la main, voir Développement.

Pile de middlewares (routes/mod.rs)

Ordre d'exécution d'une requête entrante, de l'extérieur vers l'intérieur :

# Couche Rôle
1 TraceLayer Log de chaque requête et réponse (niveau INFO)
2 CorsLayer Origines autorisées, réponse directe aux requêtes préliminaires OPTIONS (voir Déploiement)
3 ClerkLayer Vérifie le JWT sur toutes les routes (routes = None) et injecte ClerkJwt dans la requête
4 Extension(PgPool) Rend le pool SQL accessible aux handlers
5 Routeur nest("/todo", …) puis routes /todo… : préfixe effectif /todo/todo

Authentification Clerk

Point Détail
Configuration ClerkConfiguration construite avec la seule CLERK_SECRET_KEY
Clés de vérification Récupérées auprès de Clerk (JWKS) et mises en cache en mémoire (MemoryCacheJwksProvider)
Lecture du jeton En-tête Authorization (préfixe Bearer retiré). À défaut, cookie __session (option validate_session_cookie = true)
Vérifications Algorithme RS256, signature, exp, nbf
Échec 401 avec un message texte (ex. Error: No Authorization header or session cookie found on the request payload!)
Succès Le handler reçoit Extension<ClerkJwt> et utilise claim.sub comme user_id

Middleware maison abandonné

src/middlewares/auth.rs contient une ébauche de middleware vide. Le module n'est pas déclaré dans middlewares/mod.rs (vide), il n'est donc pas compilé : c'est du code mort. L'authentification repose entièrement sur ClerkLayer.

Modules

Fichier Contenu
src/main.rs Point d'entrée : variables d'env, logs, pool SQL, serveur
src/routes/mod.rs CORS, Clerk, assemblage du routeur
src/routes/todo_route.rs Déclaration des 6 routes
src/controllers/todo_controller.rs Handlers : requêtes SQL (SQLx, requêtes non vérifiées à la compilation)
src/models/todo_model.rs Structs ToDo, ToDoCreateBody, ToDoUpdateBody
src/middlewares/ Code mort (voir ci-dessus)

Frontend (todofront/)

SPA React en JavaScript, construite par Vite et servie en fichiers statiques.

Fichier Rôle
src/main.jsx Monte <App /> dans un ClerkProvider (clé VITE_CLERK_PUBLISHABLE_KEY)
src/App.jsx Affiche la page de connexion (SignedOut) ou la liste (SignedIn), charge les tâches, ouvre les modales
src/components/api.js Instance axios avec baseURL = VITE_BACKEND_URL
src/components/ToDoCard.jsx Carte d'une tâche : boutons modifier, supprimer, Done / unDo
src/components/CreateTodo.jsx Modale de création (refuse titre et description vides)
src/components/UpdateTodo.jsx Modale de modification
src/index.css Tailwind + daisyUI (thèmes retro / forest)
nginx.conf, Dockerfile Service via Nginx, utilisé seulement par docker-compose (pas par Dokploy)

Fonctionnement :

  • Chaque appel API demande un jeton frais (await getToken()) et l'envoie en Authorization: Bearer ….
  • Après une création, modification, suppression ou un changement de statut, le composant bascule un état reseter, ce qui relance GET /todo/todo : la liste est toujours rechargée depuis l'API.
  • Pas de routeur côté client : l'application tient sur une seule page.

Base de données

Une seule table todos, créée par migrations/20251107230825_init.sql. Schéma détaillé dans la référence API.

Structure du dépôt

todolist/
├── docker-compose.yaml        # postgres + todoback + todofront (dev local)
├── migrations/
│   └── 20251107230825_init.sql
├── todoback/                  # API Rust
│   ├── Cargo.toml
│   ├── Dockerfile             # multi-étapes (compose uniquement)
│   ├── .env.exemple
│   ├── check_output.txt       # ancien log de `cargo check` (obsolète)
│   └── src/
│       ├── main.rs
│       ├── routes/            # mod.rs (CORS, Clerk), todo_route.rs
│       ├── controllers/       # todo_controller.rs
│       ├── models/            # todo_model.rs
│       └── middlewares/       # code mort
└── todofront/                 # SPA React
    ├── package.json
    ├── vite.config.js
    ├── Dockerfile, nginx.conf # compose uniquement
    ├── .env.exemple
    └── src/
        ├── main.jsx, App.jsx, index.css
        └── components/        # api.js, ToDoCard, CreateTodo, UpdateTodo

Limites connues

Points relevés dans le code

  • Codes de retour : création, modification, suppression et changement de statut répondent 200 "done" même si aucune ligne n'est touchée (id inexistant ou appartenant à un autre utilisateur).
  • Message d'erreur : toute erreur SQL renvoie 500 "Unable to fetch users", quelle que soit l'opération.
  • description nullable en base mais obligatoire en Rust : une ligne avec description = NULL (insérée hors API) fait échouer tout le GET de l'utilisateur concerné (500).
  • Bouton « modif » (ToDoCard) : envoie un PUT avec un corps vide avant d'ouvrir la modale. L'API le rejette (champs manquants), sans effet visible.
  • Jeton dans la console : App.jsx fait un console.log du JWT au chargement.
  • Liste vide : le message « no todo » ne s'affiche que si la liste n'a pas pu être chargée, pas quand elle est vide.