package controllers import ( "encoding/json" "errors" configs "goFiber/config" database "goFiber/database/config" "goFiber/database/models" "goFiber/middlewares" "log" "net/http" "strconv" "strings" "github.com/gofiber/fiber/v3" "github.com/redis/go-redis/v9" "gorm.io/gorm" ) const ( corsWhitelistCacheKey = "admin:cors:whitelist:list" corsBlacklistCacheKey = "admin:cors:blacklist:list" rateLimitCacheKey = "admin:rate_limit:list" securityCacheTTL = 60 ) type CorsWhitelistRequest struct { Origin string `json:"origin" validate:"required"` Description string `json:"description"` IsActive *bool `json:"is_active"` } type CorsBlacklistRequest struct { Origin string `json:"origin" validate:"required"` Reason string `json:"reason"` IsActive *bool `json:"is_active"` } type RateLimitSettingRequest struct { Name string `json:"name" validate:"required"` Description string `json:"description"` MaxRequests int64 `json:"max_requests" validate:"required,min=1"` WindowSeconds int `json:"window_seconds" validate:"required,min=1"` IsActive *bool `json:"is_active"` } // ListCorsWhitelists godoc // @Summary List CORS whitelists (admin only) // @Tags Admin Security // @Produce json // @Security BearerAuth // @Success 200 {object} map[string]interface{} // @Failure 401 {object} map[string]string // @Failure 403 {object} map[string]string // @Router /api/v1/admin/cors/whitelist [get] func ListCorsWhitelists(c fiber.Ctx) error { if database.DB == nil { return c.Status(http.StatusServiceUnavailable).JSON(fiber.Map{"error": "database is not configured"}) } var items []models.CorsWhitelist if cached, err := database.Get(corsWhitelistCacheKey); err == nil { if unmarshalErr := json.Unmarshal([]byte(cached), &items); unmarshalErr == nil { securityLogf("[security][cors-whitelist][cache-hit] count=%d", len(items)) return c.JSON(fiber.Map{"count": len(items), "items": items}) } } else if !errors.Is(err, redis.Nil) { return c.Status(http.StatusInternalServerError).JSON(fiber.Map{"error": "cache read error"}) } if err := database.DB.Order("id DESC").Find(&items).Error; err != nil { return c.Status(http.StatusInternalServerError).JSON(fiber.Map{"error": "database error"}) } cacheJSON, _ := json.Marshal(items) _ = database.SetEx(corsWhitelistCacheKey, string(cacheJSON), securityCacheTTL) securityLogf("[security][cors-whitelist][db-load] count=%d", len(items)) return c.JSON(fiber.Map{"count": len(items), "items": items}) } // CreateCorsWhitelist godoc // @Summary Create CORS whitelist (admin only) // @Tags Admin Security // @Accept json // @Produce json // @Security BearerAuth // @Param request body CorsWhitelistRequest true "Whitelist payload" // @Success 201 {object} map[string]interface{} // @Failure 400 {object} map[string]string // @Router /api/v1/admin/cors/whitelist [post] func CreateCorsWhitelist(c fiber.Ctx) error { var req CorsWhitelistRequest if err := c.Bind().JSON(&req); err != nil { return c.Status(http.StatusBadRequest).JSON(fiber.Map{"error": "invalid request body"}) } if err := validate.Struct(req); err != nil { return c.Status(http.StatusBadRequest).JSON(fiber.Map{"error": err.Error()}) } item := models.CorsWhitelist{ Origin: strings.TrimSpace(req.Origin), Description: strings.TrimSpace(req.Description), IsActive: boolValue(req.IsActive, true), CreatedBy: currentActor(c), } if err := database.DB.Create(&item).Error; err != nil { return c.Status(http.StatusBadRequest).JSON(fiber.Map{"error": "record could not be created"}) } invalidateSecurityCaches() securityLogf("[security][cors-whitelist][create] origin=%s by=%s", item.Origin, item.CreatedBy) return c.Status(http.StatusCreated).JSON(fiber.Map{"item": item}) } // UpdateCorsWhitelist godoc // @Summary Update CORS whitelist (admin only) // @Tags Admin Security // @Accept json // @Produce json // @Security BearerAuth // @Param id path int true "Whitelist ID" // @Param request body CorsWhitelistRequest true "Whitelist payload" // @Success 200 {object} map[string]interface{} // @Failure 400 {object} map[string]string // @Failure 404 {object} map[string]string // @Router /api/v1/admin/cors/whitelist/{id} [put] func UpdateCorsWhitelist(c fiber.Ctx) error { id, err := parseID(c.Params("id")) if err != nil { return c.Status(http.StatusBadRequest).JSON(fiber.Map{"error": "invalid id"}) } var req CorsWhitelistRequest if err := c.Bind().JSON(&req); err != nil { return c.Status(http.StatusBadRequest).JSON(fiber.Map{"error": "invalid request body"}) } if err := validate.Struct(req); err != nil { return c.Status(http.StatusBadRequest).JSON(fiber.Map{"error": err.Error()}) } var item models.CorsWhitelist if err := database.DB.First(&item, id).Error; err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return c.Status(http.StatusNotFound).JSON(fiber.Map{"error": "record not found"}) } return c.Status(http.StatusInternalServerError).JSON(fiber.Map{"error": "database error"}) } item.Origin = strings.TrimSpace(req.Origin) item.Description = strings.TrimSpace(req.Description) item.IsActive = boolValue(req.IsActive, item.IsActive) item.CreatedBy = currentActor(c) if err := database.DB.Save(&item).Error; err != nil { return c.Status(http.StatusInternalServerError).JSON(fiber.Map{"error": "record could not be updated"}) } invalidateSecurityCaches() securityLogf("[security][cors-whitelist][update] id=%d origin=%s by=%s", item.ID, item.Origin, item.CreatedBy) return c.JSON(fiber.Map{"item": item}) } // DeleteCorsWhitelist godoc // @Summary Soft delete CORS whitelist (admin only) // @Tags Admin Security // @Produce json // @Security BearerAuth // @Param id path int true "Whitelist ID" // @Success 200 {object} map[string]interface{} // @Failure 404 {object} map[string]string // @Router /api/v1/admin/cors/whitelist/{id} [delete] func DeleteCorsWhitelist(c fiber.Ctx) error { id, err := parseID(c.Params("id")) if err != nil { return c.Status(http.StatusBadRequest).JSON(fiber.Map{"error": "invalid id"}) } if err := database.DB.Delete(&models.CorsWhitelist{}, id).Error; err != nil { return c.Status(http.StatusInternalServerError).JSON(fiber.Map{"error": "record could not be deleted"}) } invalidateSecurityCaches() securityLogf("[security][cors-whitelist][soft-delete] id=%d by=%s", id, currentActor(c)) return c.JSON(fiber.Map{"message": "soft deleted", "id": id}) } // HardDeleteCorsWhitelist godoc // @Summary Hard delete CORS whitelist (admin only) // @Tags Admin Security // @Produce json // @Security BearerAuth // @Param id path int true "Whitelist ID" // @Success 200 {object} map[string]interface{} // @Failure 404 {object} map[string]string // @Router /api/v1/admin/cors/whitelist/{id}/hard [delete] func HardDeleteCorsWhitelist(c fiber.Ctx) error { id, err := parseID(c.Params("id")) if err != nil { return c.Status(http.StatusBadRequest).JSON(fiber.Map{"error": "invalid id"}) } if err := database.DB.Unscoped().Delete(&models.CorsWhitelist{}, id).Error; err != nil { return c.Status(http.StatusInternalServerError).JSON(fiber.Map{"error": "record could not be hard-deleted"}) } invalidateSecurityCaches() securityLogf("[security][cors-whitelist][hard-delete] id=%d by=%s", id, currentActor(c)) return c.JSON(fiber.Map{"message": "hard deleted", "id": id}) } // ListCorsBlacklists godoc // @Summary List CORS blacklists (admin only) // @Tags Admin Security // @Produce json // @Security BearerAuth // @Success 200 {object} map[string]interface{} // @Failure 401 {object} map[string]string // @Failure 403 {object} map[string]string // @Router /api/v1/admin/cors/blacklist [get] func ListCorsBlacklists(c fiber.Ctx) error { if database.DB == nil { return c.Status(http.StatusServiceUnavailable).JSON(fiber.Map{"error": "database is not configured"}) } var items []models.CorsBlacklist if cached, err := database.Get(corsBlacklistCacheKey); err == nil { if unmarshalErr := json.Unmarshal([]byte(cached), &items); unmarshalErr == nil { securityLogf("[security][cors-blacklist][cache-hit] count=%d", len(items)) return c.JSON(fiber.Map{"count": len(items), "items": items}) } } else if !errors.Is(err, redis.Nil) { return c.Status(http.StatusInternalServerError).JSON(fiber.Map{"error": "cache read error"}) } if err := database.DB.Order("id DESC").Find(&items).Error; err != nil { return c.Status(http.StatusInternalServerError).JSON(fiber.Map{"error": "database error"}) } cacheJSON, _ := json.Marshal(items) _ = database.SetEx(corsBlacklistCacheKey, string(cacheJSON), securityCacheTTL) securityLogf("[security][cors-blacklist][db-load] count=%d", len(items)) return c.JSON(fiber.Map{"count": len(items), "items": items}) } // CreateCorsBlacklist godoc // @Summary Create CORS blacklist (admin only) // @Tags Admin Security // @Accept json // @Produce json // @Security BearerAuth // @Param request body CorsBlacklistRequest true "Blacklist payload" // @Success 201 {object} map[string]interface{} // @Failure 400 {object} map[string]string // @Router /api/v1/admin/cors/blacklist [post] func CreateCorsBlacklist(c fiber.Ctx) error { var req CorsBlacklistRequest if err := c.Bind().JSON(&req); err != nil { return c.Status(http.StatusBadRequest).JSON(fiber.Map{"error": "invalid request body"}) } if err := validate.Struct(req); err != nil { return c.Status(http.StatusBadRequest).JSON(fiber.Map{"error": err.Error()}) } item := models.CorsBlacklist{ Origin: strings.TrimSpace(req.Origin), Reason: strings.TrimSpace(req.Reason), IsActive: boolValue(req.IsActive, true), CreatedBy: currentActor(c), } if err := database.DB.Create(&item).Error; err != nil { return c.Status(http.StatusBadRequest).JSON(fiber.Map{"error": "record could not be created"}) } invalidateSecurityCaches() securityLogf("[security][cors-blacklist][create] origin=%s by=%s", item.Origin, item.CreatedBy) return c.Status(http.StatusCreated).JSON(fiber.Map{"item": item}) } // UpdateCorsBlacklist godoc // @Summary Update CORS blacklist (admin only) // @Tags Admin Security // @Accept json // @Produce json // @Security BearerAuth // @Param id path int true "Blacklist ID" // @Param request body CorsBlacklistRequest true "Blacklist payload" // @Success 200 {object} map[string]interface{} // @Failure 400 {object} map[string]string // @Failure 404 {object} map[string]string // @Router /api/v1/admin/cors/blacklist/{id} [put] func UpdateCorsBlacklist(c fiber.Ctx) error { id, err := parseID(c.Params("id")) if err != nil { return c.Status(http.StatusBadRequest).JSON(fiber.Map{"error": "invalid id"}) } var req CorsBlacklistRequest if err := c.Bind().JSON(&req); err != nil { return c.Status(http.StatusBadRequest).JSON(fiber.Map{"error": "invalid request body"}) } if err := validate.Struct(req); err != nil { return c.Status(http.StatusBadRequest).JSON(fiber.Map{"error": err.Error()}) } var item models.CorsBlacklist if err := database.DB.First(&item, id).Error; err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return c.Status(http.StatusNotFound).JSON(fiber.Map{"error": "record not found"}) } return c.Status(http.StatusInternalServerError).JSON(fiber.Map{"error": "database error"}) } item.Origin = strings.TrimSpace(req.Origin) item.Reason = strings.TrimSpace(req.Reason) item.IsActive = boolValue(req.IsActive, item.IsActive) item.CreatedBy = currentActor(c) if err := database.DB.Save(&item).Error; err != nil { return c.Status(http.StatusInternalServerError).JSON(fiber.Map{"error": "record could not be updated"}) } invalidateSecurityCaches() securityLogf("[security][cors-blacklist][update] id=%d origin=%s by=%s", item.ID, item.Origin, item.CreatedBy) return c.JSON(fiber.Map{"item": item}) } // DeleteCorsBlacklist godoc // @Summary Soft delete CORS blacklist (admin only) // @Tags Admin Security // @Produce json // @Security BearerAuth // @Param id path int true "Blacklist ID" // @Success 200 {object} map[string]interface{} // @Failure 404 {object} map[string]string // @Router /api/v1/admin/cors/blacklist/{id} [delete] func DeleteCorsBlacklist(c fiber.Ctx) error { id, err := parseID(c.Params("id")) if err != nil { return c.Status(http.StatusBadRequest).JSON(fiber.Map{"error": "invalid id"}) } if err := database.DB.Delete(&models.CorsBlacklist{}, id).Error; err != nil { return c.Status(http.StatusInternalServerError).JSON(fiber.Map{"error": "record could not be deleted"}) } invalidateSecurityCaches() securityLogf("[security][cors-blacklist][soft-delete] id=%d by=%s", id, currentActor(c)) return c.JSON(fiber.Map{"message": "soft deleted", "id": id}) } // HardDeleteCorsBlacklist godoc // @Summary Hard delete CORS blacklist (admin only) // @Tags Admin Security // @Produce json // @Security BearerAuth // @Param id path int true "Blacklist ID" // @Success 200 {object} map[string]interface{} // @Failure 404 {object} map[string]string // @Router /api/v1/admin/cors/blacklist/{id}/hard [delete] func HardDeleteCorsBlacklist(c fiber.Ctx) error { id, err := parseID(c.Params("id")) if err != nil { return c.Status(http.StatusBadRequest).JSON(fiber.Map{"error": "invalid id"}) } if err := database.DB.Unscoped().Delete(&models.CorsBlacklist{}, id).Error; err != nil { return c.Status(http.StatusInternalServerError).JSON(fiber.Map{"error": "record could not be hard-deleted"}) } invalidateSecurityCaches() securityLogf("[security][cors-blacklist][hard-delete] id=%d by=%s", id, currentActor(c)) return c.JSON(fiber.Map{"message": "hard deleted", "id": id}) } // ListRateLimitSettings godoc // @Summary List rate limit settings (admin only) // @Tags Admin Security // @Produce json // @Security BearerAuth // @Success 200 {object} map[string]interface{} // @Failure 401 {object} map[string]string // @Failure 403 {object} map[string]string // @Router /api/v1/admin/rate-limit [get] func ListRateLimitSettings(c fiber.Ctx) error { if database.DB == nil { return c.Status(http.StatusServiceUnavailable).JSON(fiber.Map{"error": "database is not configured"}) } var items []models.RateLimitSetting if cached, err := database.Get(rateLimitCacheKey); err == nil { if unmarshalErr := json.Unmarshal([]byte(cached), &items); unmarshalErr == nil { securityLogf("[security][rate-limit][cache-hit] count=%d", len(items)) return c.JSON(fiber.Map{"count": len(items), "items": items}) } } else if !errors.Is(err, redis.Nil) { return c.Status(http.StatusInternalServerError).JSON(fiber.Map{"error": "cache read error"}) } if err := database.DB.Order("id DESC").Find(&items).Error; err != nil { return c.Status(http.StatusInternalServerError).JSON(fiber.Map{"error": "database error"}) } cacheJSON, _ := json.Marshal(items) _ = database.SetEx(rateLimitCacheKey, string(cacheJSON), securityCacheTTL) securityLogf("[security][rate-limit][db-load] count=%d", len(items)) return c.JSON(fiber.Map{"count": len(items), "items": items}) } // CreateRateLimitSetting godoc // @Summary Create rate limit setting (admin only) // @Tags Admin Security // @Accept json // @Produce json // @Security BearerAuth // @Param request body RateLimitSettingRequest true "Rate limit payload" // @Success 201 {object} map[string]interface{} // @Failure 400 {object} map[string]string // @Router /api/v1/admin/rate-limit [post] func CreateRateLimitSetting(c fiber.Ctx) error { var req RateLimitSettingRequest if err := c.Bind().JSON(&req); err != nil { return c.Status(http.StatusBadRequest).JSON(fiber.Map{"error": "invalid request body"}) } if err := validate.Struct(req); err != nil { return c.Status(http.StatusBadRequest).JSON(fiber.Map{"error": err.Error()}) } item := models.RateLimitSetting{ Name: strings.TrimSpace(req.Name), Description: strings.TrimSpace(req.Description), MaxRequests: req.MaxRequests, WindowSeconds: req.WindowSeconds, IsActive: boolValue(req.IsActive, true), UpdatedBy: currentActor(c), } if err := database.DB.Create(&item).Error; err != nil { return c.Status(http.StatusBadRequest).JSON(fiber.Map{"error": "record could not be created"}) } invalidateSecurityCaches() securityLogf("[security][rate-limit][create] name=%s max=%d window=%ds by=%s", item.Name, item.MaxRequests, item.WindowSeconds, item.UpdatedBy) return c.Status(http.StatusCreated).JSON(fiber.Map{"item": item}) } // UpdateRateLimitSetting godoc // @Summary Update rate limit setting (admin only) // @Tags Admin Security // @Accept json // @Produce json // @Security BearerAuth // @Param id path int true "Rate limit ID" // @Param request body RateLimitSettingRequest true "Rate limit payload" // @Success 200 {object} map[string]interface{} // @Failure 400 {object} map[string]string // @Failure 404 {object} map[string]string // @Router /api/v1/admin/rate-limit/{id} [put] func UpdateRateLimitSetting(c fiber.Ctx) error { id, err := parseID(c.Params("id")) if err != nil { return c.Status(http.StatusBadRequest).JSON(fiber.Map{"error": "invalid id"}) } var req RateLimitSettingRequest if err := c.Bind().JSON(&req); err != nil { return c.Status(http.StatusBadRequest).JSON(fiber.Map{"error": "invalid request body"}) } if err := validate.Struct(req); err != nil { return c.Status(http.StatusBadRequest).JSON(fiber.Map{"error": err.Error()}) } var item models.RateLimitSetting if err := database.DB.First(&item, id).Error; err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return c.Status(http.StatusNotFound).JSON(fiber.Map{"error": "record not found"}) } return c.Status(http.StatusInternalServerError).JSON(fiber.Map{"error": "database error"}) } item.Name = strings.TrimSpace(req.Name) item.Description = strings.TrimSpace(req.Description) item.MaxRequests = req.MaxRequests item.WindowSeconds = req.WindowSeconds item.IsActive = boolValue(req.IsActive, item.IsActive) item.UpdatedBy = currentActor(c) if err := database.DB.Save(&item).Error; err != nil { return c.Status(http.StatusInternalServerError).JSON(fiber.Map{"error": "record could not be updated"}) } invalidateSecurityCaches() securityLogf("[security][rate-limit][update] id=%d name=%s max=%d window=%ds by=%s", item.ID, item.Name, item.MaxRequests, item.WindowSeconds, item.UpdatedBy) return c.JSON(fiber.Map{"item": item}) } // DeleteRateLimitSetting godoc // @Summary Soft delete rate limit setting (admin only) // @Tags Admin Security // @Produce json // @Security BearerAuth // @Param id path int true "Rate limit ID" // @Success 200 {object} map[string]interface{} // @Failure 404 {object} map[string]string // @Router /api/v1/admin/rate-limit/{id} [delete] func DeleteRateLimitSetting(c fiber.Ctx) error { id, err := parseID(c.Params("id")) if err != nil { return c.Status(http.StatusBadRequest).JSON(fiber.Map{"error": "invalid id"}) } if err := database.DB.Delete(&models.RateLimitSetting{}, id).Error; err != nil { return c.Status(http.StatusInternalServerError).JSON(fiber.Map{"error": "record could not be deleted"}) } invalidateSecurityCaches() securityLogf("[security][rate-limit][soft-delete] id=%d by=%s", id, currentActor(c)) return c.JSON(fiber.Map{"message": "soft deleted", "id": id}) } // HardDeleteRateLimitSetting godoc // @Summary Hard delete rate limit setting (admin only) // @Tags Admin Security // @Produce json // @Security BearerAuth // @Param id path int true "Rate limit ID" // @Success 200 {object} map[string]interface{} // @Failure 404 {object} map[string]string // @Router /api/v1/admin/rate-limit/{id}/hard [delete] func HardDeleteRateLimitSetting(c fiber.Ctx) error { id, err := parseID(c.Params("id")) if err != nil { return c.Status(http.StatusBadRequest).JSON(fiber.Map{"error": "invalid id"}) } if err := database.DB.Unscoped().Delete(&models.RateLimitSetting{}, id).Error; err != nil { return c.Status(http.StatusInternalServerError).JSON(fiber.Map{"error": "record could not be hard-deleted"}) } invalidateSecurityCaches() securityLogf("[security][rate-limit][hard-delete] id=%d by=%s", id, currentActor(c)) return c.JSON(fiber.Map{"message": "hard deleted", "id": id}) } func parseID(param string) (uint, error) { v, err := strconv.ParseUint(strings.TrimSpace(param), 10, 64) if err != nil || v == 0 { return 0, errors.New("invalid id") } return uint(v), nil } func invalidateSecurityCaches() { _ = database.Delete(corsWhitelistCacheKey) _ = database.Delete(corsBlacklistCacheKey) _ = database.Delete(rateLimitCacheKey) _ = database.Delete("cors:active:whitelist") _ = database.Delete("cors:active:blacklist") } func currentActor(c fiber.Ctx) string { if claims, ok := middlewares.GetAuthClaims(c); ok && strings.TrimSpace(claims.Email) != "" { return claims.Email } return "system" } func boolValue(v *bool, fallback bool) bool { if v == nil { return fallback } return *v } func securityLogf(format string, args ...interface{}) { if configs.AppConfig != nil && configs.AppConfig.CorsDebug { log.Printf(format, args...) } }