uVersion
Italiano
Scarica →

Wiki

REST API

Riferimento completo degli endpoint HTTP del server uVersion: auth, repos, files, locks, comments, watchlist, modification requests, builds, admin.

Il server uVersion espone un'API HTTP JSON autenticata tramite JWT. Questa pagina documenta la totalità degli endpoint usati dal client desktop, dai plugin per editor e dalla CLI uversion. Puoi chiamarli direttamente per integrare uVersion nel tuo tooling interno (dashboard personalizzata, script di audit, webhook, ecc.).

Convenzioni

URL di base

Tutti i percorsi documentati sono relativi all'URL della tua istanza. Gli esempi usano https://uversion.mygamestudio.com. Sostituiscilo con il tuo.

Header richiesti

HeaderValore
AuthorizationBearer <jwt> su tutte le route /api/* tranne /api/auth/* e /api/server-info (e /health)
Content-Typeapplication/json per i POST/PUT con body JSON. application/octet-stream per gli upload binari (build files).
Acceptapplication/json consigliato (il server restituisce JSON per impostazione predefinita)

Formato di risposta uniforme

Tutte le route rispondono con il seguente involucro:

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

In caso di errore:

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

Il campo success è sempre presente e indica se la richiesta è andata a buon fine. Il campo data è presente in caso di successo. Il campo error è presente in caso di fallimento. Mai entrambi insieme.

Paginazione

Le route che restituiscono liste potenzialmente lunghe accettano limit e offset come query param (?limit=20&offset=40). La risposta include total e has_more per facilitare l'iterazione. Limit massimo: 100.

Rate limiting

Il rate limiting globale è stato rimosso (vedi CLAUDE.md, sezione Security). Solo /api/auth/login è soggetto a rate limiting: 5 tentativi falliti per username ogni 15 minuti. I tentativi validi non contano.

Versioning dei token

Ogni JWT contiene un campo tv (token version) che corrisponde a users.token_version nel DB. Il middleware di auth verifica questo valore a ogni richiesta. Quando l'utente esegue POST /api/auth/logout, quando un admin ne reimposta la password, o quando un admin ne disattiva l'account, il token_version viene incrementato, invalidando istantaneamente tutti i token esistenti di quell'utente su tutte le sue macchine.

Curl

Per chiamare l'API da un terminale:

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

Autenticazione

POST /api/auth/login

Autentica un utente e restituisce un JWT di 30 giorni.

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
    }
  }
}

Errori:

  • 401: credenziali non valide o utente inattivo
  • 429: raggiunti 5 tentativi falliti per questo username nella finestra di 15 min

Il server esegue sempre argon2id::verify_password (contro un hash fittizio se l'utente non esiste) per neutralizzare gli attacchi di timing. La risposta impiega lo stesso tempo che l'utente esista o meno.

POST /api/auth/refresh

Rinnova il JWT senza reinserire la password.

Header: Authorization: Bearer <current_token>. Nessun body.

Response 200: identica a /login con una scadenza posticipata di 30 giorni e un nuovo token.

Errori:

  • 401: token attuale non valido, scaduto, o token_version non corrispondente

POST /api/auth/logout

Invalida tutti i token dell'utente corrente (su tutte le sue macchine) incrementando users.token_version. L'utente dovrà rieffettuare l'accesso ovunque.

Response 200: { "success": true }.

POST /api/auth/validate

Verifica che il token sia ancora valido. Per i token a meno di 7 giorni dalla scadenza, il server include un refreshed_token nella risposta (auto-estensione). Il client deve persisterlo al posto di quello vecchio.

Response 200:

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

POST /api/auth/register

Crea un account utente (route pubblica, senza autenticazione).

POST /api/auth/change-password

Cambia la password dell'utente corrente. Incrementa token_version, il che invalida tutti i vecchi token.

Repository

GET /api/repositories

Elenca i repository accessibili all'utente corrente (filtrati tramite la tabella dei permessi). Un utente che non ha alcun pattern di permesso su un repo non lo vede.

Query param: 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}

Dettaglio di un repository, incluse le statistiche aggregate.

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"
  }
}

Errori:

  • 403: nessun accesso a questo repo
  • 404: repo inesistente

POST /api/repositories

Crea un nuovo repository. Capability create_repos richiesta (admin per impostazione predefinita).

Body:

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

Validazione:

  • name: da 1 a 64 caratteri, alfanumerico + trattini + underscore. Univoco per owner.
  • description: da 0 a 1024 caratteri. Facoltativa.

Errori:

  • 400: nome non valido o troppo lungo
  • 403: capability create_repos mancante
  • 409: esiste già un repo con questo nome per questo owner

File

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

Carica un batch di chunk binari. I chunk identici (per SHA-256) vengono deduplicati automaticamente. Il server non ri-memorizza un chunk già presente. La risposta restituisce per ogni chunk il suo hash + dimensione + dimensione compressa, da usare nel successivo POST /commit.

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 }
    ]
  }
}

Limiti: body max 1 GB (configurabile tramite security.max_body_size_files). Per file più grandi, suddividere in più batch.

POST /api/files/{repo_id}/commit

Crea un commit atomico con una lista di file e i loro chunk. O passano tutti i file, o nessuno.

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"
    }
  ]
}

Azioni possibili: added, modified, deleted. Per added e modified, l'array chunks contiene gli hash restituiti da /upload-chunks. Per deleted, omettere chunks.

Response 200:

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

Errori:

  • 400: messaggio vuoto, file senza azione, chunk referenziati inesistenti
  • 403: manca la capability checkin, o nessun permesso write su uno dei file
  • 409: lock già detenuto da un altro utente su un file modificato, o commit concorrente (race condition sullo stesso file)

GET /api/files/{repo_id}/snapshot

Restituisce lo stato completo del repository alla revisione corrente: tutti i file, le loro revisioni e i loro chunk. Usato dal client per le operazioni di clone e sync forzato.

Query param:

  • revision (facoltativo): snapshot a una revisione passata. Predefinito: 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

Scarica il contenuto di un file a una data revisione (il server riassembla i chunk). Usato dal clone e dal sync.

Query param:

  • path: percorso del file (relativo alla radice del repo)
  • revision (facoltativo): numero di revisione. Predefinito: l'ultima.

Response 200: il contenuto binario del file.

GET /api/files/{repo_id}/history

Cronologia paginata dei commit del repository.

Query param:

  • limit (da 1 a 100, predefinito 20)
  • offset (predefinito 0)
  • path (facoltativo): filtra per percorso di file (commit che hanno modificato questo file)
  • author (facoltativo): filtra per username
  • since (facoltativo): 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 incrementale

Endpoint usati dal client desktop per sincronizzare in modo efficiente:

  • GET /api/files/{repo_id}/sync: file cambiati da una revisione, per un sync delta.
  • GET /api/files/{repo_id}/deletions: file eliminati lato server, per propagare le eliminazioni in locale.
  • GET /api/files/{repo_id}/list: elenco dei file del repo.
  • POST /api/files/{repo_id}/checkout: acquisisce i lock e prepara la modifica.

Lock

POST /api/locks/{repo_id}/acquire

Acquisisce lock su una lista di percorsi. Le acquisizioni sono indipendenti: la lista acquired contiene i successi, la lista failed contiene i fallimenti con la loro motivazione.

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"
      }
    ]
  }
}

I lock scadono dopo 60 minuti senza heartbeat. Il campo expires_at riflette questa scadenza. Chiama /heartbeat periodicamente per rinnovare il lock, o /release per rilasciarlo.

Motivazioni di failed:

  • ALREADY_LOCKED: un altro utente detiene il lock (i campi lock_holder e lock_acquired_at sono popolati)
  • PERMISSION_DENIED: nessun permesso write su questo percorso
  • INVALID_PATH: percorso malformato (assoluto, contiene .., ecc.)

POST /api/locks/{repo_id}/release

Rilascia i lock che possiedi. Body:

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

force: true richiede la capability force_unlock. L'azione viene registrata nell'audit.

POST /api/locks/{repo_id}/heartbeat

Aggiorna last_heartbeat_at sui tuoi lock. Consigliato ogni 5 minuti per i job CI a lunga esecuzione che vogliono che gli admin vedano l'attività.

GET /api/locks/{repo_id}/status

Elenca tutti i lock attivi del repository, con join su users e files per restituire direttamente i nomi.

Query param: user (filtra per 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
  }
}

Commenti e revisioni

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

Elenca i commenti di un commit, in thread (padre → figli).

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

Crea un commento. file_path è facoltativo (commento di commit vs. di file). parent_id per rispondere a un thread esistente.

Body:

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

POST /api/comments/{repo_id}/reviews

Invia una revisione su un commit. Capability approve_changes richiesta.

Body:

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

Status accettato: approved, changes_requested, pending.

Watchlist

GET /api/watchlist/{repo_id}/watchlist

Elenca i pattern di watch dell'utente corrente su questo 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

Aggiunge un pattern di watch. Body:

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

Gli eventi possibili: commit (un commit ha modificato un file corrispondente), lock (un lock è stato acquisito), review (una revisione è stata pubblicata).

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

Rimuove un pattern di watch. Restituisce { "success": true }.

Bacheca di produzione (task)

La vecchia API modification-requests è stata rimossa: la bacheca di produzione la assorbe (le richieste diventano schede, origin='request'). Gli endpoint sono annidati sotto /api/tasks/{repo_id}.

RouteDescrizione
GET /api/tasks/{repo_id}/boardBacheca completa: colonne + schede
POST /api/tasks/{repo_id}/tasksCrea una scheda
PUT/DELETE /api/tasks/{repo_id}/tasks/{task_id}Aggiorna / elimina una scheda
PUT /api/tasks/{repo_id}/tasks/{task_id}/assigneesAssegna utenti a una scheda
GET /api/tasks/{repo_id}/assignable-usersElenco degli utenti assegnabili
POST /api/tasks/{repo_id}/columnsConfigura le colonne della bacheca

Sotto-route anche disponibili: commenti di scheda, link ad assets e commit, e allegati (con immagine di copertina). La configurazione delle colonne richiede la capability manage_board (admin / lead).

Build

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

Carica un file di build in <storage_path>/builds/<hash>. Il server verifica lo SHA-256 fornito nel query param rispetto al contenuto ricevuto. Se l'hash esiste già lato server, restituisce 200 immediatamente senza ricopiare (dedup lato build storage).

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

Body: binario grezzo (senza wrapping JSON).

Limiti: 8 GB per file.

Errori:

  • 400: query param hash mancante o malformato, o SHA-256 calcolato != hash fornito
  • 413: file > 8 GB

POST /api/builds/{repo_id}/publish

Registra il manifest di una build dopo l'upload di tutti i suoi file. Capability publish_builds richiesta.

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}

Elenca le build pubblicate. Capability download_builds richiesta per vedere i link di 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

Scarica un file di una build. Capability download_builds richiesta. Il manifest di una build è disponibile tramite GET /api/builds/{repo_id}/{build_id}/manifest.

Admin

Tutte le route /api/admin/* richiedono che l'utente abbia almeno una capability admin (a seconda dell'endpoint: manage_users, manage_permissions, manage_rules, ecc.). Documentazione sintetica qui sotto. La maggior parte segue il pattern REST standard (GET/POST/PUT/DELETE).

RouteCapabilityDescrizione
GET/POST /api/admin/usersmanage_usersElenca / crea utenti
PUT/DELETE /api/admin/users/{id}manage_usersAggiorna / elimina
POST /api/admin/users/{id}/reset-passwordmanage_usersReimposta la password, incrementa token_version
GET/POST /api/admin/groupsmanage_usersGestione dei gruppi
GET/POST /api/admin/permissions/{repo_id}manage_permissionsRegole glob dei permessi per percorso
GET/POST /api/admin/repositories/{repo_id}/rulesmanage_rulesRegole di validazione pre-checkin
GET/POST /api/admin/repositories/{repo_id}/webhooksmanage_rulesDiscord / Slack / Teams / custom
GET /api/admin/auditview_all_activityAudit log filtrato e paginato
GET /api/admin/statsview_all_activityStorage metrics, dedup ratio, crescita
GET /api/admin/repositories/{repo_id}/gc/previewadminAnteprima del GC senza eseguire
POST /api/admin/repositories/{repo_id}/gcadminAvvia il garbage collection
POST /api/admin/locks/{id}/force-releaseforce_unlockForza lo sblocco di un lock altrui, registrato nell'audit

Salute

GET /health

Endpoint non autenticato che restituisce 200 OK se il server può accedere al database. Usato per gli health check di load balancer o di monitoraggio (Prometheus blackbox, Datadog synthetic, ecc.).

Response 200: OK (text/plain).

Response 503: se il database è irraggiungibile.

GET /api/server-info

Endpoint pubblico non autenticato che restituisce i metadati del server (versione, ecc.). Come /health, non è protetto dal middleware di auth.

Formato degli errori

In caso di errore, la risposta è { "success": false, "error": "..." } con uno status HTTP appropriato. Il campo error è un messaggio leggibile dall'uomo. Per i client che hanno bisogno di codici stabili leggibili dalla macchina, effettua il parse del prefisso (es.: "Permission denied: ..." inizia sempre con "Permission denied").

HTTPSignificatoQuando
400Bad requestParametro non valido, JSON malformato, body troppo grande (prima del limite rigido 413)
401Not authenticatedToken assente, scaduto, firma non valida, o token_version non corrispondente
403Permission deniedCapability mancante, o nessun permesso sul percorso / repo
404Resource not foundRepo / file / commit / utente inesistente o inaccessibile
409ConflictLock già detenuto, commit concorrente, vincolo unique violato
413Payload too largeBody oltre il limite (1 GB su /files, 8 GB su /builds/upload)
429Too many requestsSolo su /auth/login (rate limiting per utente)
500Internal server errorErrore DB, errore IO. Registrato lato server; allega il timestamp se segnali il bug.
503Service unavailableDatabase irraggiungibile (health check) o migrazione in corso