feat(api): error codes, idempotency keys, cursor pagination

- Stable machine-readable error codes alongside the error message
  (UNAUTHENTICATED, INVALID_TOKEN, ADMIN_REQUIRED, SCOPE_INSUFFICIENT,
  IDEMPOTENCY_IN_PROGRESS); CLI renders "CODE: message".
- Idempotency middleware on authed/admin groups: POST requests with an
  Idempotency-Key header replay the first response for 24h (Redis-backed,
  caller-scoped); concurrent duplicates get 409. X-Idempotent-Replay
  header marks replays.
- Shared pagination helpers: ?cursor= (opaque, preferred) alongside
  legacy ?page= on projects and deployments lists; pagination meta
  includes next_cursor; invalid cursors fail soft to page 1.
- CLI --idempotency-key flag for safe scripted retries.
- OpenAPI updated: error envelope, cursor params, idempotency contract.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
Tomas DvorakandDevin committed 2026-10-08 15:38:36 +02:00
1 parent 485e41a817
commit 18f78f00a3
15 files changed
+454 -56

No files matched your search

@@ -212,15 +212,15 @@ func Auth(jwtSecret string) gin.HandlerFunc {
}
if tokenErr != "" {
c.JSON(http.StatusUnauthorized, gin.H{"error": tokenErr})
c.JSON(http.StatusUnauthorized, gin.H{"error": tokenErr, "code": "INVALID_TOKEN"})
c.Abort()
return
}
if hasToken {
c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid token"})
c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid token", "code": "INVALID_TOKEN"})
} else {
c.JSON(http.StatusUnauthorized, gin.H{"error": "Authentication required"})
c.JSON(http.StatusUnauthorized, gin.H{"error": "Authentication required", "code": "UNAUTHENTICATED"})
}
c.Abort()
}
@@ -245,12 +245,12 @@ func OptionalAuth(jwtSecret string) gin.HandlerFunc {
func RequireAdmin() gin.HandlerFunc {
return func(c *gin.Context) {
if _, exists := c.Get("user_id"); !exists {
c.JSON(http.StatusUnauthorized, gin.H{"error": "Authentication required"})
c.JSON(http.StatusUnauthorized, gin.H{"error": "Authentication required", "code": "UNAUTHENTICATED"})
c.Abort()
return
}
if !c.GetBool("is_admin") {
c.JSON(http.StatusForbidden, gin.H{"error": "Admin access required"})
c.JSON(http.StatusForbidden, gin.H{"error": "Admin access required", "code": "ADMIN_REQUIRED"})
c.Abort()
return
}