veza/veza-backend-api/internal/api/versioning.go
senke 64f62635a5 api-versioning: add X-API-Deprecated header and frontend deprecation warning
- Backend: Add X-API-Deprecated header alongside existing X-API-Version-Deprecated
- Frontend: Show deprecation warning toast when deprecated API version detected
- Warning shown only once per session to avoid spam
- Includes sunset date in warning message if available
2026-01-15 16:56:21 +01:00

257 lines
7.2 KiB
Go

package api
import (
"net/http"
"strings"
"github.com/gin-gonic/gin"
"go.uber.org/zap"
)
const (
// APIVersionHeader est le header HTTP pour spécifier la version de l'API
APIVersionHeader = "X-API-Version"
// AcceptHeaderVersion est le header Accept avec version (ex: application/vnd.veza.v1+json)
AcceptHeaderVersion = "Accept"
// DefaultAPIVersion est la version par défaut de l'API
DefaultAPIVersion = "v1"
// APIVersionKey est la clé utilisée pour stocker la version dans le contexte Gin
APIVersionKey = "api_version"
)
// APIVersion représente une version de l'API
type APIVersion struct {
Version string // Ex: "v1", "v2"
Deprecated bool // Indique si la version est dépréciée
SunsetDate string // Date de fin de support (RFC3339)
Description string // Description de la version
}
// VersionManager gère les versions de l'API (BE-SVC-019)
type VersionManager struct {
versions map[string]*APIVersion
defaultVersion string
logger *zap.Logger
}
// NewVersionManager crée un nouveau gestionnaire de versions
func NewVersionManager(logger *zap.Logger) *VersionManager {
vm := &VersionManager{
versions: make(map[string]*APIVersion),
defaultVersion: DefaultAPIVersion,
logger: logger,
}
// Enregistrer les versions par défaut
vm.RegisterVersion(&APIVersion{
Version: "v1",
Deprecated: false,
Description: "Current stable API version",
})
return vm
}
// RegisterVersion enregistre une nouvelle version de l'API
func (vm *VersionManager) RegisterVersion(version *APIVersion) {
vm.versions[version.Version] = version
vm.logger.Info("API version registered",
zap.String("version", version.Version),
zap.Bool("deprecated", version.Deprecated),
zap.String("description", version.Description))
}
// GetVersion retourne les informations sur une version
func (vm *VersionManager) GetVersion(version string) (*APIVersion, bool) {
v, exists := vm.versions[version]
return v, exists
}
// GetDefaultVersion retourne la version par défaut
func (vm *VersionManager) GetDefaultVersion() string {
return vm.defaultVersion
}
// SetDefaultVersion définit la version par défaut
func (vm *VersionManager) SetDefaultVersion(version string) {
if _, exists := vm.versions[version]; exists {
vm.defaultVersion = version
}
}
// GetAllVersions retourne toutes les versions disponibles
func (vm *VersionManager) GetAllVersions() map[string]*APIVersion {
result := make(map[string]*APIVersion)
for k, v := range vm.versions {
result[k] = v
}
return result
}
// VersionMiddleware est un middleware pour gérer le versioning de l'API (BE-SVC-019)
// Supporte plusieurs méthodes de spécification de version :
// 1. Header X-API-Version
// 2. Header Accept: application/vnd.veza.v1+json
// 3. URL path: /api/v1/...
func VersionMiddleware(versionManager *VersionManager) gin.HandlerFunc {
return func(c *gin.Context) {
// Extraire la version depuis différentes sources
version := extractAPIVersion(c)
// Valider la version
if version == "" {
version = versionManager.GetDefaultVersion()
}
// Vérifier si la version existe
apiVersion, exists := versionManager.GetVersion(version)
if !exists {
c.JSON(http.StatusBadRequest, gin.H{
"error": "Unsupported API version",
"version": version,
"available_versions": getAvailableVersions(versionManager),
})
c.Abort()
return
}
// Stocker la version dans le contexte
c.Set(APIVersionKey, version)
c.Set("api_version_info", apiVersion)
// Ajouter des headers de réponse pour indiquer la version utilisée
c.Header(APIVersionHeader, version)
if apiVersion.Deprecated {
c.Header("X-API-Deprecated", "true")
c.Header("X-API-Version-Deprecated", "true")
if apiVersion.SunsetDate != "" {
c.Header("Sunset", apiVersion.SunsetDate)
}
}
// Logger si version dépréciée
if apiVersion.Deprecated {
versionManager.logger.Warn("Deprecated API version used",
zap.String("version", version),
zap.String("path", c.Request.URL.Path),
zap.String("sunset_date", apiVersion.SunsetDate))
}
c.Next()
}
}
// extractAPIVersion extrait la version de l'API depuis différentes sources
func extractAPIVersion(c *gin.Context) string {
// 1. Header X-API-Version
if version := c.GetHeader(APIVersionHeader); version != "" {
return normalizeVersion(version)
}
// 2. Header Accept: application/vnd.veza.v1+json
if accept := c.GetHeader(AcceptHeaderVersion); accept != "" {
if version := parseAcceptHeader(accept); version != "" {
return version
}
}
// 3. URL path: /api/v1/... ou /api/v2/...
path := c.Request.URL.Path
if strings.HasPrefix(path, "/api/") {
parts := strings.Split(path, "/")
if len(parts) >= 3 && strings.HasPrefix(parts[2], "v") {
return normalizeVersion(parts[2])
}
}
return ""
}
// normalizeVersion normalise une version (v1 -> v1, 1 -> v1, etc.)
func normalizeVersion(version string) string {
version = strings.TrimSpace(version)
version = strings.ToLower(version)
if !strings.HasPrefix(version, "v") {
version = "v" + version
}
return version
}
// parseAcceptHeader parse le header Accept pour extraire la version
// Format: application/vnd.veza.v1+json
func parseAcceptHeader(accept string) string {
parts := strings.Split(accept, ",")
for _, part := range parts {
part = strings.TrimSpace(part)
// Chercher le pattern vnd.veza.vX
if strings.Contains(part, "vnd.veza.v") {
start := strings.Index(part, "vnd.veza.v")
if start != -1 {
start += len("vnd.veza.v")
end := start
for end < len(part) && (part[end] >= '0' && part[end] <= '9') {
end++
}
if end > start {
return normalizeVersion("v" + part[start:end])
}
}
}
}
return ""
}
// getAvailableVersions retourne la liste des versions disponibles
func getAvailableVersions(vm *VersionManager) []string {
versions := make([]string, 0, len(vm.versions))
for v := range vm.versions {
versions = append(versions, v)
}
return versions
}
// GetAPIVersion retourne la version de l'API depuis le contexte Gin
func GetAPIVersion(c *gin.Context) string {
if version, exists := c.Get(APIVersionKey); exists {
if v, ok := version.(string); ok {
return v
}
}
return DefaultAPIVersion
}
// GetAPIVersionInfo retourne les informations complètes sur la version depuis le contexte
func GetAPIVersionInfo(c *gin.Context) *APIVersion {
if info, exists := c.Get("api_version_info"); exists {
if apiVersion, ok := info.(*APIVersion); ok {
return apiVersion
}
}
return nil
}
// VersionInfoHandler retourne les informations sur les versions disponibles
func VersionInfoHandler(versionManager *VersionManager) gin.HandlerFunc {
return func(c *gin.Context) {
versions := versionManager.GetAllVersions()
response := gin.H{
"current_version": versionManager.GetDefaultVersion(),
"versions": make(map[string]interface{}),
}
for version, info := range versions {
versionInfo := gin.H{
"version": info.Version,
"deprecated": info.Deprecated,
"description": info.Description,
}
if info.SunsetDate != "" {
versionInfo["sunset_date"] = info.SunsetDate
}
response["versions"].(map[string]interface{})[version] = versionInfo
}
c.JSON(http.StatusOK, response)
}
}