1
0
forked from baron/baron-sso
Files
baron-sso/backend/internal/handler/api_key_handler.go

146 lines
4.0 KiB
Go

package handler
import (
"baron-sso-backend/internal/domain"
"strings"
"time"
"github.com/gofiber/fiber/v2"
"golang.org/x/crypto/bcrypt"
"gorm.io/gorm"
)
type ApiKeyHandler struct {
DB *gorm.DB
}
func NewApiKeyHandler(db *gorm.DB) *ApiKeyHandler {
return &ApiKeyHandler{DB: db}
}
type apiKeySummary struct {
ID string `json:"id"`
Name string `json:"name"`
ClientID string `json:"client_id"`
Scopes []string `json:"scopes"`
Status string `json:"status"`
LastUsedAt *string `json:"lastUsedAt"`
CreatedAt time.Time `json:"createdAt"`
}
type apiKeyListResponse struct {
Items []apiKeySummary `json:"items"`
Total int64 `json:"total"`
}
func (h *ApiKeyHandler) ListApiKeys(c *fiber.Ctx) error {
if h.DB == nil {
return c.Status(fiber.StatusServiceUnavailable).JSON(fiber.Map{"error": "database not available"})
}
limit := c.QueryInt("limit", 50)
offset := c.QueryInt("offset", 0)
var total int64
if err := h.DB.Model(&domain.ApiKey{}).Count(&total).Error; err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()})
}
var keys []domain.ApiKey
if err := h.DB.Order("created_at desc").Limit(limit).Offset(offset).Find(&keys).Error; err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()})
}
items := make([]apiKeySummary, 0, len(keys))
for _, k := range keys {
lastUsed := ""
if k.LastUsedAt != nil {
lastUsed = k.LastUsedAt.Format(time.RFC3339)
}
items = append(items, apiKeySummary{
ID: k.ID,
Name: k.Name,
ClientID: k.ClientID,
Scopes: strings.Fields(strings.ReplaceAll(k.Scopes, ",", " ")),
Status: k.Status,
LastUsedAt: &lastUsed,
CreatedAt: k.CreatedAt,
})
}
return c.JSON(apiKeyListResponse{Items: items, Total: total})
}
func (h *ApiKeyHandler) CreateApiKey(c *fiber.Ctx) error {
if h.DB == nil {
return c.Status(fiber.StatusServiceUnavailable).JSON(fiber.Map{"error": "database not available"})
}
var req struct {
Name string `json:"name"`
Scopes []string `json:"scopes"`
}
if err := c.BodyParser(&req); err != nil {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "invalid request body"})
}
if strings.TrimSpace(req.Name) == "" {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "name is required"})
}
// Generate Client ID (16 chars hex)
clientID := GenerateSecureToken(8)
// Generate plain secret (16 chars hex)
plainSecret := GenerateSecureToken(8)
hashedSecret, err := bcrypt.GenerateFromPassword([]byte(plainSecret), bcrypt.DefaultCost)
if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "failed to hash secret"})
}
apiKey := domain.ApiKey{
Name: req.Name,
ClientID: clientID,
ClientSecretHash: string(hashedSecret),
Scopes: strings.Join(req.Scopes, " "),
Status: "active",
}
if err := h.DB.Create(&apiKey).Error; err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()})
}
// Return summary + PLAIN SECRET (only this time)
lastUsed := ""
return c.Status(fiber.StatusCreated).JSON(fiber.Map{
"apiKey": apiKeySummary{
ID: apiKey.ID,
Name: apiKey.Name,
ClientID: apiKey.ClientID,
Scopes: req.Scopes,
Status: apiKey.Status,
LastUsedAt: &lastUsed,
CreatedAt: apiKey.CreatedAt,
},
"clientSecret": plainSecret, // VERY IMPORTANT: user must save this now
})
}
func (h *ApiKeyHandler) DeleteApiKey(c *fiber.Ctx) error {
if h.DB == nil {
return c.Status(fiber.StatusServiceUnavailable).JSON(fiber.Map{"error": "database not available"})
}
id := c.Params("id")
if id == "" {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "id is required"})
}
if err := h.DB.Delete(&domain.ApiKey{}, "id = ?", id).Error; err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()})
}
return c.SendStatus(fiber.StatusNoContent)
}