uVersion
Français
Télécharger →

Wiki

REST API

Référence complète des endpoints HTTP du serveur uVersion : auth, repos, files, locks, comments, watchlist, modification requests, builds, admin.

Le serveur uVersion expose une API HTTP JSON authentifiée par JWT. Cette page documente l'intégralité des endpoints utilisés par le client desktop, les plugins éditeur et le CLI uversion. Vous pouvez les appeler directement pour intégrer uVersion dans votre tooling interne (dashboard custom, scripts d'audit, webhooks, etc.).

Conventions

Base URL

Tous les chemins documentés sont relatifs à l'URL de votre instance. Les exemples utilisent https://uversion.mygamestudio.com. Remplacez par la vôtre.

Headers requis

HeaderValeur
AuthorizationBearer <jwt> sur toutes les routes /api/* sauf /api/auth/* et /api/server-info (et /health)
Content-Typeapplication/json pour les POST/PUT avec body JSON. application/octet-stream pour les uploads binaires (build files).
Acceptapplication/json recommandé (le serveur retourne du JSON par défaut)

Format de réponse uniforme

Toutes les routes répondent avec l'enveloppe suivante :

{
  "success": true,
  "data": { /* payload */ }
}

En cas d'erreur :

{
  "success": false,
  "error": "Permission denied: capability 'manage_users' required"
}

Le champ success est toujours présent et indique si la requête a réussi. Le champ data est présent en cas de succès. Le champ error est présent en cas d'échec. Jamais les deux ensemble.

Pagination

Les routes qui retournent des listes potentiellement longues acceptent limit et offset en query params (?limit=20&offset=40). La réponse inclut total et has_more pour faciliter l'itération. Limit max : 100.

Rate limiting

Le rate limiting global a été retiré (voir CLAUDE.md, section Security). Seul /api/auth/login est rate-limité : 5 tentatives échouées par username toutes les 15 minutes. Les tentatives valides ne comptent pas.

Token versioning

Chaque JWT contient un champ tv (token version) qui correspond à users.token_version en DB. Le middleware d'auth vérifie cette valeur à chaque requête. Quand l'utilisateur fait POST /api/auth/logout, quand un admin reset son password, ou quand un admin désactive son compte, le token_version est bumpé, invalidant instantanément tous les tokens existants de cet utilisateur sur toutes ses machines.

Curl

Pour appeler l'API depuis un terminal :

TOKEN=$(curl -s -X POST https://uversion.mygamestudio.com/api/auth/login \
  -H "Content-Type: application/json" \
  -d '{"username":"alice","password":"secret"}' | jq -r '.data.token')

curl -H "Authorization: Bearer $TOKEN" \
  https://uversion.mygamestudio.com/api/repositories

Authentication

POST /api/auth/login

Authentifie un utilisateur et retourne un JWT 30 jours.

Body :

{
  "username": "alice",
  "password": "secret"
}

Response 200 :

{
  "success": true,
  "data": {
    "token": "eyJhbGciOiJIUzI1NiIs...",
    "expires_at": "2026-06-13T14:30:00Z",
    "user": {
      "id": 12,
      "username": "alice",
      "email": "alice@mygamestudio.com",
      "role": "lead",
      "is_active": true
    }
  }
}

Erreurs :

  • 401 : credentials invalides ou utilisateur inactif
  • 429 : 5 tentatives échouées atteintes pour ce username dans la fenêtre 15 min

Le serveur exécute toujours argon2id::verify_password (contre un hash dummy si l'utilisateur n'existe pas) pour neutraliser les attaques par timing. La réponse prend le même temps qu'un utilisateur existe ou non.

POST /api/auth/refresh

Renouvelle le JWT sans repasser par le password.

Headers : Authorization: Bearer <current_token>. Pas de body.

Response 200 : identique à /login avec une expiration repoussée de 30 jours et un nouveau token.

Erreurs :

  • 401 : token actuel invalide, expiré, ou token_version mismatch

POST /api/auth/logout

Invalide tous les tokens du user courant (sur toutes ses machines) en bumpant users.token_version. Le user devra se relogger partout.

Response 200 : { "success": true }.

POST /api/auth/validate

Vérifie que le token est encore valide. Pour les tokens à moins de 7 jours d'expirer, le serveur inclut un refreshed_token dans la réponse (auto-extend). Le client doit le persister à la place de l'ancien.

Response 200 :

{
  "success": true,
  "data": {
    "valid": true,
    "user_id": 12,
    "expires_at": "2026-06-13T14:30:00Z",
    "refreshed_token": "eyJhbGciOiJIUzI1NiIs..."
  }
}

POST /api/auth/register

Crée un compte utilisateur (route publique, sans authentification).

POST /api/auth/change-password

Change le mot de passe de l'utilisateur courant. Bumpe token_version, ce qui invalide tous les anciens tokens.

Repositories

GET /api/repositories

Liste les repositories accessibles par l'utilisateur courant (filtrés via la table de permissions). Un user qui n'a aucun pattern de permission sur un repo ne le voit pas.

Query params : limit, offset.

Response 200 :

{
  "success": true,
  "data": [
    {
      "id": 1,
      "name": "hero-rpg",
      "owner": "acme",
      "description": "Main RPG project",
      "current_revision": "7f3a9b1c2d...",
      "file_count": 8432,
      "size_bytes": 14211938405,
      "created_at": "2026-01-15T10:00:00Z"
    },
    {
      "id": 2,
      "name": "shared-assets",
      "owner": "acme",
      "description": "Shared asset library",
      "current_revision": "3a4b5c6d...",
      "file_count": 412,
      "size_bytes": 980000000,
      "created_at": "2026-02-01T09:00:00Z"
    }
  ]
}

GET /api/repositories/{repo_id}

Détail d'un repository, incluant les stats agrégées.

Response 200 :

{
  "success": true,
  "data": {
    "id": 1,
    "name": "hero-rpg",
    "owner": "acme",
    "description": "Main RPG project",
    "current_revision": "7f3a9b1c2d...",
    "file_count": 8432,
    "size_bytes": 14211938405,
    "dedup_ratio": 4.2,
    "created_at": "2026-01-15T10:00:00Z",
    "last_commit_at": "2026-05-15T08:30:00Z"
  }
}

Erreurs :

  • 403 : pas d'accès à ce repo
  • 404 : repo inexistant

POST /api/repositories

Crée un nouveau repository. Capability create_repos requise (admin par défaut).

Body :

{
  "name": "new-project",
  "description": "Optional description"
}

Validation :

  • name : 1 à 64 caractères, alphanumérique + tirets + underscores. Unique par owner.
  • description : 0 à 1024 caractères. Optionnel.

Erreurs :

  • 400 : nom invalide ou trop long
  • 403 : capability create_repos manquante
  • 409 : un repo avec ce nom existe déjà pour ce owner

Files

POST /api/files/{repo_id}/upload-chunks

Upload un batch de chunks binaires. Les chunks identiques (par SHA-256) sont dédupliqués automatiquement. Le serveur ne re-stocke pas un chunk déjà présent. La réponse retourne pour chaque chunk son hash + taille + taille compressée, à utiliser dans POST /commit suivant.

Body :

{
  "chunks": [
    { "data": "<base64-encoded chunk bytes>" },
    { "data": "<base64-encoded chunk bytes>" }
  ]
}

Response 200 :

{
  "success": true,
  "data": {
    "chunks": [
      { "hash": "abc123...", "size": 1048576, "compressed_size": 423152 },
      { "hash": "def456...", "size": 2097152, "compressed_size": 891204 }
    ]
  }
}

Limites : body max 1 GB (configurable via security.max_body_size_files). Pour des fichiers plus gros, splitter en plusieurs batches.

POST /api/files/{repo_id}/commit

Crée un commit atomique avec une liste de fichiers et leurs chunks. Soit tous les fichiers passent, soit aucun.

Body :

{
  "message": "Updated main level + hero pose pass",
  "files": [
    {
      "path": "Content/Maps/MainLevel.umap",
      "action": "modified",
      "chunks": ["abc123...", "def456..."]
    },
    {
      "path": "Content/Characters/NewVillain.uasset",
      "action": "added",
      "chunks": ["fed789..."]
    },
    {
      "path": "Content/OldAsset.uasset",
      "action": "deleted"
    }
  ]
}

Actions possibles : added, modified, deleted. Pour added et modified, le tableau chunks contient les hashes retournés par /upload-chunks. Pour deleted, omettre chunks.

Response 200 :

{
  "success": true,
  "data": {
    "commit_hash": "7f3a9b1c2d3e4f...",
    "files_changed": 3,
    "bytes_uploaded": 88080384,
    "bytes_deduped": 4194304,
    "revision": 47
  }
}

Erreurs :

  • 400 : message vide, fichier sans action, chunks référencés inexistants
  • 403 : pas la capability checkin, ou pas de permission write sur un des fichiers
  • 409 : lock déjà tenu par un autre user sur un fichier modifié, ou commit en concurrence (race condition sur le même fichier)

GET /api/files/{repo_id}/snapshot

Retourne l'état complet du repository à la révision courante : tous les fichiers, leurs révisions et leurs chunks. Utilisé par le client pour les opérations de clone et sync forcé.

Query params :

  • revision (optionnel) : snapshot à une révision passée. Défaut : HEAD.

Response 200 :

{
  "success": true,
  "data": {
    "revision": 47,
    "files": [
      {
        "path": "Content/Maps/MainLevel.umap",
        "revision": 12,
        "size_bytes": 84934656,
        "chunks": ["abc123...", "def456..."]
      }
    ]
  }
}

GET /api/files/{repo_id}/content

Télécharge le contenu d'un fichier à une révision donnée (le serveur réassemble les chunks). Utilisé par le clone et le sync.

Query params :

  • path : chemin du fichier (relatif à la racine du repo)
  • revision (optionnel) : numéro de révision. Défaut : dernière.

Response 200 : le contenu binaire du fichier.

GET /api/files/{repo_id}/history

Historique paginé des commits du repository.

Query params :

  • limit (1 à 100, défaut 20)
  • offset (défaut 0)
  • path (optionnel) : filtre par chemin de fichier (commits qui ont modifié ce fichier)
  • author (optionnel) : filtre par username
  • since (optionnel) : ISO 8601 timestamp

Response 200 :

{
  "success": true,
  "data": {
    "commits": [
      {
        "hash": "7f3a9b1c...",
        "author": "alice",
        "author_id": 12,
        "date": "2026-05-15T08:30:00Z",
        "message": "Fixed lighting in main level",
        "files_changed": 1,
        "bytes_uploaded": 84934656
      }
    ],
    "total": 423,
    "limit": 20,
    "offset": 0,
    "has_more": true
  }
}

Sync incrémental

Endpoints utilisés par le client desktop pour synchroniser efficacement :

  • GET /api/files/{repo_id}/sync : fichiers changés depuis une révision, pour un sync delta.
  • GET /api/files/{repo_id}/deletions : fichiers supprimés côté serveur, pour propager les suppressions en local.
  • GET /api/files/{repo_id}/list : liste des fichiers du repo.
  • POST /api/files/{repo_id}/checkout : acquiert les locks et prépare l'édition.

Locks

POST /api/locks/{repo_id}/acquire

Acquiert des locks sur une liste de chemins. Les acquisitions sont indépendantes : la liste acquired contient les succès, la liste failed contient les échecs avec leur raison.

Body :

{
  "paths": [
    "Content/Maps/MainLevel.umap",
    "Content/Characters/Hero.uasset"
  ]
}

Response 200 :

{
  "success": true,
  "data": {
    "acquired": [
      {
        "id": "550e8400-e29b-41d4-a716-446655440000",
        "file_id": 12345,
        "file_path": "Content/Maps/MainLevel.umap",
        "user_id": 12,
        "username": "alice",
        "acquired_at": "2026-05-15T14:30:00Z",
        "last_heartbeat_at": "2026-05-15T14:30:00Z",
        "expires_at": "2026-05-15T15:30:00Z"
      }
    ],
    "failed": [
      {
        "path": "Content/Characters/Hero.uasset",
        "reason": "ALREADY_LOCKED",
        "lock_holder": "bob",
        "lock_acquired_at": "2026-05-15T13:00:00Z"
      }
    ]
  }
}

Les locks expirent après 60 minutes sans heartbeat. Le champ expires_at reflète cette échéance. Appelez /heartbeat périodiquement pour renouveler le lock, ou /release pour le libérer.

Raisons de failed :

  • ALREADY_LOCKED : un autre user détient le lock (les champs lock_holder et lock_acquired_at sont peuplés)
  • PERMISSION_DENIED : pas de permission write sur ce path
  • INVALID_PATH : chemin malformé (absolu, contient .., etc.)

POST /api/locks/{repo_id}/release

Release des locks que vous possédez. Body :

{
  "paths": ["Content/Maps/MainLevel.umap"],
  "force": false
}

force: true requiert la capability force_unlock. L'action est auditée.

POST /api/locks/{repo_id}/heartbeat

Update last_heartbeat_at sur vos locks. Recommandé toutes les 5 minutes pour les long-running jobs CI qui veulent que les admins voient l'activité.

GET /api/locks/{repo_id}/status

Liste tous les locks actifs du repository, joins sur users et files pour retourner directement les noms.

Query params : user (filtre par username), limit, offset.

Response 200 :

{
  "success": true,
  "data": {
    "locks": [
      {
        "file_path": "Content/Maps/MainLevel.umap",
        "user_id": 12,
        "username": "alice",
        "acquired_at": "2026-05-15T14:30:00Z",
        "last_heartbeat_at": "2026-05-15T16:00:00Z"
      }
    ],
    "total": 1
  }
}

Comments & Reviews

GET /api/comments/{repo_id}/comments?commit_hash=<hash>

Liste les commentaires d'un commit, threadés (parent → enfants).

Response 200 :

{
  "success": true,
  "data": [
    {
      "id": 42,
      "commit_hash": "7f3a9b1c...",
      "file_path": "Content/Maps/MainLevel.umap",
      "author_id": 12,
      "author_username": "alice",
      "body": "LGTM, ship it",
      "parent_id": null,
      "created_at": "2026-05-15T15:00:00Z",
      "replies": [
        {
          "id": 43,
          "author_username": "bob",
          "body": "Thanks!",
          "parent_id": 42,
          "created_at": "2026-05-15T15:05:00Z"
        }
      ]
    }
  ]
}

POST /api/comments/{repo_id}/comments

Crée un commentaire. file_path est optionnel (commentaire de commit vs. de fichier). parent_id pour répondre à un thread existant.

Body :

{
  "commit_hash": "7f3a9b1c...",
  "file_path": "Content/Maps/MainLevel.umap",
  "body": "LGTM, ship it",
  "parent_id": null
}

POST /api/comments/{repo_id}/reviews

Submit un review sur un commit. Capability approve_changes requise.

Body :

{
  "commit_hash": "7f3a9b1c...",
  "status": "approved",
  "comment": "Lighting looks great, approving"
}

Status accepté : approved, changes_requested, pending.

Watchlist

GET /api/watchlist/{repo_id}/watchlist

Liste les patterns de watch de l'utilisateur courant sur ce repository.

Response 200 :

{
  "success": true,
  "data": [
    {
      "id": 1,
      "user_id": 12,
      "pattern": "Content/Characters/Hero/**",
      "notify_on": ["commit", "lock"],
      "created_at": "2026-04-10T10:00:00Z"
    }
  ]
}

POST /api/watchlist/{repo_id}/watchlist

Ajoute un pattern de watch. Body :

{
  "pattern": "Content/Characters/Hero/**",
  "notify_on": ["commit", "lock"]
}

Les événements possibles : commit (un commit a modifié un fichier matchant), lock (un lock a été acquis), review (un review a été posté).

DELETE /api/watchlist/{repo_id}/watchlist/{watch_id}

Retire un pattern de watch. Retourne { "success": true }.

Production board (tasks)

L'ancienne API modification-requests a été retirée : le board de production l'absorbe (les demandes deviennent des cartes, origin='request'). Les endpoints sont nichés sous /api/tasks/{repo_id}.

RouteDescription
GET /api/tasks/{repo_id}/boardBoard complet : colonnes + cartes
POST /api/tasks/{repo_id}/tasksCrée une carte
PUT/DELETE /api/tasks/{repo_id}/tasks/{task_id}Met à jour / supprime une carte
PUT /api/tasks/{repo_id}/tasks/{task_id}/assigneesAssigne des utilisateurs à une carte
GET /api/tasks/{repo_id}/assignable-usersListe des utilisateurs assignables
POST /api/tasks/{repo_id}/columnsConfigure les colonnes du board

Sous-routes également disponibles : commentaires de carte, liens vers assets et commits, et pièces jointes (avec image de couverture). La configuration des colonnes requiert la capability manage_board (admin / lead).

Builds

POST /api/builds/{repo_id}/upload?hash=<sha256>

Upload un fichier de build vers <storage_path>/builds/<hash>. Le serveur vérifie le SHA-256 fourni en query param contre le contenu reçu. Si le hash existe déjà côté serveur, retourne 200 immédiatement sans recopier (dédup côté build storage).

Headers : Content-Type: application/octet-stream.

Body : binaire brut (pas de JSON wrapping).

Limites : 8 GB par fichier.

Erreurs :

  • 400 : hash query param manquant ou malformé, ou SHA-256 calculé != hash fourni
  • 413 : fichier > 8 GB

POST /api/builds/{repo_id}/publish

Enregistre le manifest d'un build après upload de tous ses fichiers. Capability publish_builds requise.

Body :

{
  "version": "0.1.5-nightly",
  "config": "Development",
  "platform": "Win64",
  "executable_path": "HeroRPG/Binaries/Win64/HeroRPG.exe",
  "release_notes": "Nightly build of main branch, 2026-05-15",
  "files": [
    { "path": "HeroRPG.exe",                "hash": "abc123...", "size_bytes": 12345678 },
    { "path": "HeroRPG/Content/Paks/pak0.pak", "hash": "def456...", "size_bytes": 2147483648 }
  ]
}

GET /api/builds/{repo_id}

Liste les builds publiés. Capability download_builds requise pour voir les liens de download.

Response 200 :

{
  "success": true,
  "data": [
    {
      "id": 5,
      "version": "0.1.5-nightly",
      "config": "Development",
      "platform": "Win64",
      "published_by": "ci-nightly",
      "published_at": "2026-05-15T03:00:00Z",
      "size_bytes": 2159829326,
      "file_count": 142,
      "release_notes": "Nightly build of main branch, 2026-05-15"
    }
  ]
}

GET /api/builds/{repo_id}/{build_id}/file

Télécharge un fichier d'un build. Capability download_builds requise. Le manifest d'un build est disponible via GET /api/builds/{repo_id}/{build_id}/manifest.

Admin

Toutes les routes /api/admin/* requièrent que l'utilisateur ait au moins une capability admin (selon l'endpoint : manage_users, manage_permissions, manage_rules, etc.). Documentation succincte ci-dessous. La plupart suivent le pattern REST standard (GET/POST/PUT/DELETE).

RouteCapabilityDescription
GET/POST /api/admin/usersmanage_usersListe / crée des utilisateurs
PUT/DELETE /api/admin/users/{id}manage_usersMet à jour / supprime
POST /api/admin/users/{id}/reset-passwordmanage_usersReset le password, bumpe token_version
GET/POST /api/admin/groupsmanage_usersGestion des groupes
GET/POST /api/admin/permissions/{repo_id}manage_permissionsRègles glob de permissions par chemin
GET/POST /api/admin/repositories/{repo_id}/rulesmanage_rulesRègles de validation pré-checkin
GET/POST /api/admin/repositories/{repo_id}/webhooksmanage_rulesDiscord / Slack / Teams / custom
GET /api/admin/auditview_all_activityAudit log filtré et paginé
GET /api/admin/statsview_all_activityStorage metrics, dedup ratio, croissance
GET /api/admin/repositories/{repo_id}/gc/previewadminPreview du GC sans exécuter
POST /api/admin/repositories/{repo_id}/gcadminLance le garbage collection
POST /api/admin/locks/{id}/force-releaseforce_unlockForce-unlock un lock tiers, audité

Health

GET /health

Endpoint non authentifié retournant 200 OK si le serveur peut accéder à la base. Utilisé pour les health checks de load balancer ou de monitoring (Prometheus blackbox, Datadog synthetic, etc.).

Response 200 : OK (text/plain).

Response 503 : si la base est injoignable.

GET /api/server-info

Endpoint public non authentifié retournant les métadonnées du serveur (version, etc.). Comme /health, il n'est pas protégé par le middleware d'auth.

Error format

En cas d'erreur, la réponse est { "success": false, "error": "..." } avec un status HTTP approprié. Le champ error est un message lisible pour humain. Pour les clients qui ont besoin de codes stables machine-readable, parsez le préfixe (ex: "Permission denied: ..." commence toujours par "Permission denied").

HTTPSignificationQuand
400Bad requestParamètre invalide, JSON malformé, body trop gros (avant la limite hard 413)
401Not authenticatedToken absent, expiré, signature invalide, ou token_version mismatch
403Permission deniedCapability manquante, ou pas de permission sur le path / repo
404Resource not foundRepo / fichier / commit / user inexistant ou inaccessible
409ConflictLock déjà tenu, commit en concurrence, contrainte unique violée
413Payload too largeBody au-delà de la limite (1 GB sur /files, 8 GB sur /builds/upload)
429Too many requestsUniquement sur /auth/login (rate limiting per-user)
500Internal server errorDB error, IO error. Loggé côté serveur, joindre le timestamp si vous reportez le bug.
503Service unavailableBase injoignable (health check) ou migration en cours