Skip to content

Le trajet d'une requête ​

Pour comprendre l'API, le plus efficace est de suivre une requête de son arrivée à sa réponse. On prend Renee (id 5), qui pointe son arrivée depuis son téléphone :

http
POST /api/clocks/5
Authorization: Bearer eyJhbGciOi…
Content-Type: application/json

{ "action": "in", "client_id": "7f3c2a10-6c1e-4f1a-9a55-0d6c8f1b2e44" }

Le trajet côté serveur

Cette page part du moment où la requête arrive dans l'API. Pour ce qui se passe avant (Traefik, le réseau, les conteneurs), voir Le trajet d'une requête côté DevOps.

Vue d'ensemble ​

Étape par étape ​

1. L'Endpoint : la porte d'entrée ​

Fichier : lib/timemanager_web/endpoint.ex. La requête traverse une suite de plugs, dans l'ordre :

PlugRôle
Plug.RequestIddonne un identifiant unique à la requête, pour la retrouver dans les journaux
Plugs.CORSdit au navigateur quels sites ont le droit d'appeler l'API
Plug.Parserstransforme le corps JSON en map Elixir : %{"action" => "in", …}
TimemanagerWeb.Routeren dernier : l'aiguillage

2. Le Router : l'aiguillage ​

Fichier : lib/timemanager_web/router.ex.

elixir
post "/clocks/:userID", ClockController, :create

Le morceau :userID de l'adresse devient un paramètre : "userID" => "5". La route est dans un groupe qui passe par deux pipelines : :api (la requête doit accepter du JSON), puis :authenticated.

3. Authenticate : « qui es-tu ? » ​

Fichier : lib/timemanager_web/plugs/authenticate.ex. Ce plug :

  1. lit le jeton : l'en-tête Authorization: Bearer pour le mobile, ou le cookie et l'en-tête x-csrf-token pour le web ;
  2. vérifie sa signature et sa date d'expiration ;
  3. recharge le compte depuis la base : un compte supprimé est refusé tout de suite, même avec un jeton encore valide, et un changement de rôle s'applique immédiatement ;
  4. range le compte dans conn.assigns.current_user.

Si une étape échoue, il répond 401 et arrête la requête (halt) : le contrôleur n'est jamais appelé.

4. Le contrôleur : « en as-tu le droit ? » ​

Fichier : lib/timemanager_web/controllers/clock_controller.ex, fonction create/2.

elixir
def create(conn, %{"userID" => user_id} = params) do
  with {:ok, user_id} <- parse_id(user_id),
       :ok <- Authorization.authorize(conn.assigns.current_user, :clock, user_id),
       {:ok, clock, result} <- Clocks.clock(user_id, params) do
    conn
    |> put_status(status(result))        # 201 créé, 200 si déjà reçu
    |> render(:show, clock: clock)
  end
end

Le contrôleur ne contient aucune règle : il enchaîne trois questions et laisse le with s'arrêter à la première qui échoue.

Le droit de pointer est décidé dans lib/timemanager/authorization.ex :

elixir
defp allowed?(%User{id: id}, :clock, id), do: true

id apparaît deux fois : la règle ne vaut que si la personne connectée est la personne qui pointe. Personne ne pointe pour un autre, pas même le General Manager. Sinon : 403.

5. Le métier : les règles ​

Fichier : lib/timemanager/clocks.ex, fonction clock/2.

  1. DeviceActions.cast/2 vérifie ce que le téléphone envoie : action, client_id, et éventuellement time.
  2. Repo.transaction/1 ouvre une transaction : tout ce qui suit réussit ou échoue ensemble.
  3. La ligne de l'utilisateur est verrouillée (FOR UPDATE) : deux pointages simultanés de Renee passent l'un après l'autre, jamais en même temps.
  4. replayed/2 : ce client_id a-t-il déjà été reçu ? Si oui, on renvoie le pointage existant (réponse perdue, file hors ligne renvoyée).
  5. insert_clock/4 compare l'intention ("in") à l'état réel. Si Renee est déjà arrivée, c'est un 409 already_clocked_in.
  6. record_shift/3 ouvre la journée de travail avec Workingtime.start_shift/3.

Si l'ouverture de la journée échoue, la transaction annule aussi le pointage : il n'existe jamais de pointage sans sa journée.

6. PostgreSQL : le dernier filet ​

Même si le code vérifie tout, la base a ses propres contraintes :

  • un index unique garantit une seule journée ouverte par personne ;
  • un autre garantit qu'un client_id n'est enregistré qu'une fois par personne.

Si un bug laissait passer un doublon, la base dirait non. C'est la défense en profondeur.

7. La réponse ​

render(:show, clock: clock) passe par lib/timemanager_web/controllers/clock_json.ex, qui choisit exactement les champs envoyés :

json
{
  "data": {
    "id": 412, "user_id": 5, "status": true,
    "time": "2026-10-13T06:30:00Z",
    "client_id": "7f3c2a10-6c1e-4f1a-9a55-0d6c8f1b2e44",
    "received_at": "2026-10-13T06:30:00Z"
  }
}

8. Et si quelque chose échoue ? ​

En haut du contrôleur, action_fallback TimemanagerWeb.FallbackController dit à Phoenix : « si l'action ne renvoie pas une réponse, passe son résultat à ce module ». Le {:error, …} sorti du with y devient une réponse propre :

Ce qui sort du withRéponse
{:error, :forbidden}403 {"error": "forbidden", "code": "forbidden"}
{:error, {:conflict, :already_clocked_in}}409 {"code": "already_clocked_in"}
{:error, :week_validated}409 {"code": "week_validated"}
{:error, %Ecto.Changeset{}}422 avec errors (phrases) et details (codes)

Toutes les erreurs de l'API passent par ce module : elles ont toutes la même forme, et chacune a un code stable que le site et l'application traduisent dans leur langue.

À retenir ​

ÉtapeQuestionÉchec
Authenticatequi es-tu ?401
Authorizationen as-tu le droit ?403
Contexteest-ce permis par les règles ?409, 422
Baseles données restent-elles cohérentes ?422

Essaie toi-même

Dans Swagger (/api/docs) : connecte-toi avec "client": "mobile", clique sur Authorize, puis envoie deux fois POST /api/clocks/{userID} avec {"action": "in"}. La première réponse est 201, la seconde 409 already_clocked_in. Retrouve dans insert_clock/4 la ligne qui a produit ce 409.