260 lines
8.1 KiB
Go
260 lines
8.1 KiB
Go
package handlers
|
|
|
|
import (
|
|
"fmt"
|
|
"net/http"
|
|
"time"
|
|
|
|
"goyco/internal/config"
|
|
"goyco/internal/middleware"
|
|
"goyco/internal/repositories"
|
|
"goyco/internal/services"
|
|
"goyco/internal/version"
|
|
|
|
"github.com/go-chi/chi/v5"
|
|
"gorm.io/gorm"
|
|
)
|
|
|
|
type APIHandler struct {
|
|
config *config.Config
|
|
postRepo repositories.PostRepository
|
|
userRepo repositories.UserRepository
|
|
voteService *services.VoteService
|
|
dbMonitor middleware.DBMonitor
|
|
healthChecker *middleware.DatabaseHealthChecker
|
|
metricsCollector *middleware.MetricsCollector
|
|
}
|
|
|
|
func NewAPIHandler(config *config.Config, postRepo repositories.PostRepository, userRepo repositories.UserRepository, voteService *services.VoteService) *APIHandler {
|
|
return &APIHandler{
|
|
config: config,
|
|
postRepo: postRepo,
|
|
userRepo: userRepo,
|
|
voteService: voteService,
|
|
}
|
|
}
|
|
|
|
func NewAPIHandlerWithMonitoring(config *config.Config, postRepo repositories.PostRepository, userRepo repositories.UserRepository, voteService *services.VoteService, db *gorm.DB, dbMonitor middleware.DBMonitor) *APIHandler {
|
|
if db == nil {
|
|
return NewAPIHandler(config, postRepo, userRepo, voteService)
|
|
}
|
|
|
|
sqlDB, err := db.DB()
|
|
if err != nil {
|
|
return NewAPIHandler(config, postRepo, userRepo, voteService)
|
|
}
|
|
|
|
healthChecker := middleware.NewDatabaseHealthChecker(sqlDB, dbMonitor)
|
|
metricsCollector := middleware.NewMetricsCollector(dbMonitor)
|
|
|
|
return &APIHandler{
|
|
config: config,
|
|
postRepo: postRepo,
|
|
userRepo: userRepo,
|
|
voteService: voteService,
|
|
dbMonitor: dbMonitor,
|
|
healthChecker: healthChecker,
|
|
metricsCollector: metricsCollector,
|
|
}
|
|
}
|
|
|
|
type APIInfo = CommonResponse
|
|
|
|
// @Summary Get API information
|
|
// @Description Retrieve API metadata, available endpoints, authentication details, and response formats
|
|
// @Tags api
|
|
// @Accept json
|
|
// @Produce json
|
|
// @Success 200 {object} APIInfo "API information retrieved successfully"
|
|
// @Router /api [get]
|
|
func (h *APIHandler) GetAPIInfo(w http.ResponseWriter, r *http.Request) {
|
|
if r.URL.Path != "/api" {
|
|
http.NotFound(w, r)
|
|
return
|
|
}
|
|
|
|
apiInfo := map[string]any{
|
|
"name": fmt.Sprintf("%s API", h.config.App.Title),
|
|
"version": version.Version,
|
|
"description": "Y Combinator-style news board API",
|
|
"endpoints": map[string]any{
|
|
"authentication": map[string]any{
|
|
"POST /api/auth/register": "Register new user",
|
|
"POST /api/auth/login": "Login user",
|
|
"GET /api/auth/confirm": "Confirm email address",
|
|
"POST /api/auth/resend-verification": "Resend verification email",
|
|
"POST /api/auth/forgot-password": "Request password reset",
|
|
"POST /api/auth/reset-password": "Reset password",
|
|
"POST /api/auth/account/confirm": "Confirm account deletion",
|
|
"GET /api/auth/me": "Get current user profile",
|
|
"POST /api/auth/logout": "Logout user",
|
|
"PUT /api/auth/email": "Update email address",
|
|
"PUT /api/auth/username": "Update username",
|
|
"PUT /api/auth/password": "Update password",
|
|
"DELETE /api/auth/account": "Request account deletion",
|
|
},
|
|
"posts": map[string]any{
|
|
"GET /api/posts": "List all posts",
|
|
"GET /api/posts/search": "Search posts",
|
|
"GET /api/posts/title": "Fetch title from URL",
|
|
"GET /api/posts/{id}": "Get specific post",
|
|
"POST /api/posts": "Create new post",
|
|
"PUT /api/posts/{id}": "Update post",
|
|
"DELETE /api/posts/{id}": "Delete post",
|
|
},
|
|
"votes": map[string]any{
|
|
"POST /api/posts/{id}/vote": "Cast a vote",
|
|
"DELETE /api/posts/{id}/vote": "Remove vote",
|
|
"GET /api/posts/{id}/vote": "Get user's vote",
|
|
"GET /api/posts/{id}/votes": "Get all votes for post",
|
|
},
|
|
"users": map[string]any{
|
|
"GET /api/users": "List all users",
|
|
"POST /api/users": "Create new user",
|
|
"GET /api/users/{id}": "Get specific user",
|
|
"GET /api/users/{id}/posts": "Get user's posts",
|
|
},
|
|
"system": map[string]any{
|
|
"GET /health": "Health check",
|
|
"GET /metrics": "Service metrics",
|
|
},
|
|
},
|
|
"authentication": map[string]any{
|
|
"type": "Bearer Token (JWT)",
|
|
"note": "Include Authorization header with 'Bearer <token>' for protected endpoints",
|
|
},
|
|
"response_format": map[string]any{
|
|
"success": "boolean",
|
|
"message": "string",
|
|
"data": "object or array",
|
|
"error": "string (on error)",
|
|
},
|
|
}
|
|
|
|
SendSuccessResponse(w, "API information retrieved successfully", apiInfo)
|
|
}
|
|
|
|
// @Summary Health check
|
|
// @Description Check the API health status along with database connectivity details
|
|
// @Tags api
|
|
// @Accept json
|
|
// @Produce json
|
|
// @Success 200 {object} CommonResponse "Health check successful"
|
|
// @Router /health [get]
|
|
func (h *APIHandler) GetHealth(w http.ResponseWriter, r *http.Request) {
|
|
|
|
if h.healthChecker != nil {
|
|
health := h.healthChecker.CheckHealth()
|
|
health["version"] = version.Version
|
|
SendSuccessResponse(w, "Health check successful", health)
|
|
return
|
|
}
|
|
|
|
currentTimestamp := time.Now().UTC().Format(time.RFC3339)
|
|
|
|
health := map[string]any{
|
|
"status": "healthy",
|
|
"timestamp": currentTimestamp,
|
|
"version": version.Version,
|
|
"services": map[string]any{
|
|
"database": "connected",
|
|
"api": "running",
|
|
},
|
|
}
|
|
|
|
SendSuccessResponse(w, "Health check successful", health)
|
|
}
|
|
|
|
// @Summary Get metrics
|
|
// @Description Retrieve application metrics including aggregate counts and database performance data
|
|
// @Tags api
|
|
// @Accept json
|
|
// @Produce json
|
|
// @Success 200 {object} CommonResponse "Application metrics retrieved successfully"
|
|
// @Router /metrics [get]
|
|
func (h *APIHandler) GetMetrics(w http.ResponseWriter, r *http.Request) {
|
|
|
|
postCount, err := h.postRepo.Count()
|
|
if err != nil {
|
|
SendErrorResponse(w, "Failed to get post count", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
userCount, err := h.userRepo.Count()
|
|
if err != nil {
|
|
SendErrorResponse(w, "Failed to get user count", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
totalVoteCount, _, err := h.voteService.GetVoteStatistics()
|
|
if err != nil {
|
|
SendErrorResponse(w, "Failed to get vote statistics", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
topPosts, err := h.postRepo.GetTopPosts(5)
|
|
if err != nil {
|
|
SendErrorResponse(w, "Failed to get top posts", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
var avgVotesPerPost float64
|
|
if postCount > 0 {
|
|
avgVotesPerPost = float64(totalVoteCount) / float64(postCount)
|
|
}
|
|
|
|
var totalScore int
|
|
for _, post := range topPosts {
|
|
totalScore += post.Score
|
|
}
|
|
|
|
var avgScore float64
|
|
if len(topPosts) > 0 {
|
|
avgScore = float64(totalScore) / float64(len(topPosts))
|
|
}
|
|
|
|
metrics := map[string]any{
|
|
"posts": map[string]any{
|
|
"total_count": postCount,
|
|
"top_posts_count": len(topPosts),
|
|
"total_score": totalScore,
|
|
"average_score": avgScore,
|
|
},
|
|
"users": map[string]any{
|
|
"total_count": userCount,
|
|
},
|
|
"votes": map[string]any{
|
|
"total_count": totalVoteCount,
|
|
"average_per_post": avgVotesPerPost,
|
|
"note": "All votes are counted together",
|
|
},
|
|
"system": map[string]any{
|
|
"timestamp": time.Now().UTC().Format(time.RFC3339),
|
|
"version": version.Version,
|
|
},
|
|
}
|
|
|
|
if h.metricsCollector != nil {
|
|
performanceMetrics := h.metricsCollector.GetMetrics()
|
|
metrics["database"] = map[string]any{
|
|
"total_queries": performanceMetrics.DBStats.TotalQueries,
|
|
"slow_queries": performanceMetrics.DBStats.SlowQueries,
|
|
"average_duration": performanceMetrics.DBStats.AverageDuration.String(),
|
|
"max_duration": performanceMetrics.DBStats.MaxDuration.String(),
|
|
"error_count": performanceMetrics.DBStats.ErrorCount,
|
|
"last_query_time": performanceMetrics.DBStats.LastQueryTime.Format(time.RFC3339),
|
|
}
|
|
metrics["performance"] = map[string]any{
|
|
"request_count": performanceMetrics.RequestCount,
|
|
"average_response": performanceMetrics.AverageResponse.String(),
|
|
"max_response": performanceMetrics.MaxResponse.String(),
|
|
"error_count": performanceMetrics.ErrorCount,
|
|
}
|
|
}
|
|
|
|
SendSuccessResponse(w, "Metrics retrieved successfully", metrics)
|
|
}
|
|
|
|
func (h *APIHandler) MountRoutes(r chi.Router, config RouteModuleConfig) {
|
|
}
|