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 errorJSON(c, fiber.StatusServiceUnavailable, "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 errorJSON(c, fiber.StatusInternalServerError, err.Error()) } var keys []domain.ApiKey if err := h.DB.Order("created_at desc").Limit(limit).Offset(offset).Find(&keys).Error; err != nil { return errorJSON(c, fiber.StatusInternalServerError, 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 errorJSON(c, fiber.StatusServiceUnavailable, "database not available") } var req struct { Name string `json:"name"` Scopes []string `json:"scopes"` } if err := c.BodyParser(&req); err != nil { return errorJSON(c, fiber.StatusBadRequest, "invalid request body") } if strings.TrimSpace(req.Name) == "" { return errorJSON(c, fiber.StatusBadRequest, "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 errorJSON(c, fiber.StatusInternalServerError, "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 errorJSON(c, fiber.StatusInternalServerError, 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 errorJSON(c, fiber.StatusServiceUnavailable, "database not available") } id := c.Params("id") if id == "" { return errorJSON(c, fiber.StatusBadRequest, "id is required") } if err := h.DB.Delete(&domain.ApiKey{}, "id = ?", id).Error; err != nil { return errorJSON(c, fiber.StatusInternalServerError, err.Error()) } return c.SendStatus(fiber.StatusNoContent) }