forked from baron/baron-sso
146 lines
4.0 KiB
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)
|
|
}
|