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) commeuser_iddans 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)¶
- Charge un éventuel fichier
.env(dotenv). - Lit
TESTpuisDATABASE_URL: le programme panique si l'une d'elles est absente. - Initialise les logs (
tracing, niveau INFO). - Ouvre un pool PostgreSQL (
PgPoolOptions, 5 connexions max). Si la base est injoignable, le programme panique. - Construit le routeur (
routes::routing), qui litCLERK_SECRET_KEY(panique si absente). - É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 enAuthorization: Bearer …. - Après une création, modification, suppression ou un changement de statut, le composant bascule un état
reseter, ce qui relanceGET /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. descriptionnullable en base mais obligatoire en Rust : une ligne avecdescription = NULL(insérée hors API) fait échouer tout leGETde l'utilisateur concerné (500).- Bouton « modif » (
ToDoCard) : envoie unPUTavec un corps vide avant d'ouvrir la modale. L'API le rejette (champs manquants), sans effet visible. - Jeton dans la console :
App.jsxfait unconsole.logdu 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.