package models import ( "crypto/rand" "crypto/sha256" "encoding/hex" "fmt" "net" "strings" "time" "github.com/sujit-baniya/fiber-boilerplate/app" "gorm.io/gorm" ) // ApiKey es una credencial para /api/v2, alternativa al ADMIN_API_KEY único de // entorno (que sigue funcionando como llave maestra para no romper lo que ya // depende de él). A diferencia de esa llave maestra, cada ApiKey: // - Solo funciona desde la IP/CIDR que se le asignó (obligatoria, no opcional // — a diferencia del patrón de Pagos Externos, aquí se decidió exigirla // siempre porque esta llave puede dar acceso a datos internos, no solo a // pedir un cobro). // - Solo puede usar los scopes (grupos de endpoints) que se le habilitaron. // - Se puede revocar individualmente sin afectar a otras integraciones. type ApiKey struct { gorm.Model Nombre string `json:"nombre" gorm:"column:nombre;size:150;not null"` TokenHash string `json:"-" gorm:"column:token_hash;uniqueIndex;size:64"` TokenPreview string `json:"token_preview" gorm:"column:token_preview;size:12"` IPPermitida string `json:"ip_permitida" gorm:"column:ip_permitida;size:60;not null"` // IP exacta o CIDR, obligatoria Scopes string `json:"scopes" gorm:"column:scopes;size:300"` // comma-separated, ej: "oss,query_runner" Activa bool `json:"activa" gorm:"column:activa;default:true"` CreadoPorID uint `json:"creado_por_id" gorm:"column:creado_por_id"` UltimoUsoAt *time.Time `json:"ultimo_uso_at" gorm:"column:ultimo_uso_at"` UltimoUsoIP string `json:"ultimo_uso_ip" gorm:"column:ultimo_uso_ip;size:60"` } func (ApiKey) TableName() string { return "api_keys" } // GenerarApiKeyToken crea un token aleatorio de 32 bytes y su hash SHA-256, // mismo esquema que ServicioPagoExterno: el token crudo se devuelve una sola // vez, en la base solo queda el hash. func GenerarApiKeyToken() (raw string, hash string, err error) { b := make([]byte, 32) if _, err := rand.Read(b); err != nil { return "", "", fmt.Errorf("no se pudo generar el token: %w", err) } raw = "sak_" + hex.EncodeToString(b) sum := sha256.Sum256([]byte(raw)) hash = hex.EncodeToString(sum[:]) return raw, hash, nil } func apiKeyTokenPreview(raw string) string { if len(raw) <= 8 { return raw } return "..." + raw[len(raw)-6:] } func CreateApiKey(k *ApiKey) (tokenPlano string, err error) { raw, hash, err := GenerarApiKeyToken() if err != nil { return "", err } k.TokenHash = hash k.TokenPreview = apiKeyTokenPreview(raw) if err := app.Http.Database.DB.Create(k).Error; err != nil { return "", err } return raw, nil } func RegenerarApiKeyToken(id uint) (tokenPlano string, err error) { raw, hash, err := GenerarApiKeyToken() if err != nil { return "", err } result := app.Http.Database.DB.Model(&ApiKey{}).Where("id = ?", id). Updates(map[string]interface{}{"token_hash": hash, "token_preview": apiKeyTokenPreview(raw)}) if result.Error != nil { return "", result.Error } if result.RowsAffected == 0 { return "", fmt.Errorf("api key no encontrada") } return raw, nil } func GetAllApiKeys(limit, offset int) ([]ApiKey, int64, error) { var items []ApiKey var total int64 db := app.Http.Database.DB.Model(&ApiKey{}) if err := db.Count(&total).Error; err != nil { return nil, 0, err } if err := db.Order("id DESC").Limit(limit).Offset(offset).Find(&items).Error; err != nil { return nil, 0, err } return items, total, nil } func GetApiKeyByID(id uint) (*ApiKey, error) { var k ApiKey if err := app.Http.Database.DB.First(&k, id).Error; err != nil { return nil, err } return &k, nil } // FindApiKeyActivaByToken resuelve la llave a partir del token crudo recibido // en el header Authorization/X-API-Key. Solo hace match si está activa. func FindApiKeyActivaByToken(rawToken string) (*ApiKey, error) { sum := sha256.Sum256([]byte(rawToken)) hash := hex.EncodeToString(sum[:]) var k ApiKey if err := app.Http.Database.DB.Where("token_hash = ? AND activa = ?", hash, true).First(&k).Error; err != nil { return nil, err } return &k, nil } func UpdateApiKey(id uint, updates map[string]interface{}) error { return app.Http.Database.DB.Model(&ApiKey{}).Where("id = ?", id).Updates(updates).Error } func DeleteApiKey(id uint) error { return app.Http.Database.DB.Delete(&ApiKey{}, id).Error } // RegistrarUsoApiKey deja constancia de la última vez (y desde qué IP) que se // usó la llave, para poder detectar llaves zombis o uso desde un origen raro. func RegistrarUsoApiKey(id uint, ip string) { now := time.Now() app.Http.Database.DB.Model(&ApiKey{}).Where("id = ?", id). Updates(map[string]interface{}{"ultimo_uso_at": now, "ultimo_uso_ip": ip}) } // IPPermitida valida la IP del caller contra el CIDR/IP exacta configurada. // A diferencia de ServicioPagoExterno, aquí es obligatoria: una ApiKey sin // IP válida configurada nunca debe dar acceso (fail-closed). func (k *ApiKey) IPValida(ip string) bool { entrada := strings.TrimSpace(k.IPPermitida) if entrada == "" { return false } callerIP := net.ParseIP(ip) if callerIP == nil { return false } if strings.Contains(entrada, "/") { _, red, err := net.ParseCIDR(entrada) return err == nil && red.Contains(callerIP) } permitida := net.ParseIP(entrada) return permitida != nil && permitida.Equal(callerIP) } // ScopesList devuelve los scopes habilitados para esta llave. func (k *ApiKey) ScopesList() []string { return SplitModulos(k.Scopes) } // TieneScope indica si la llave puede usar ese grupo de endpoints. func (k *ApiKey) TieneScope(scope string) bool { for _, s := range k.ScopesList() { if s == scope { return true } } return false }