package handler import ( "net/http" "strconv" "tss-rocks-be/internal/config" "tss-rocks-be/internal/service" "tss-rocks-be/internal/types" "github.com/gin-gonic/gin" "github.com/rs/zerolog/log" ) type Handler struct { cfg *config.Config config *config.Config service service.Service } func NewHandler(cfg *config.Config, service service.Service) *Handler { return &Handler{ cfg: cfg, config: cfg, service: service, } } // RegisterRoutes registers all the routes func (h *Handler) RegisterRoutes(r *gin.Engine) { api := r.Group("/api/v1") { // Auth routes auth := api.Group("/auth") { auth.POST("/register", h.Register) auth.POST("/login", h.Login) } // User routes users := api.Group("/users") { users.GET("", h.ListUsers) users.POST("", h.CreateUser) users.GET("/:id", h.GetUser) users.PUT("/:id", h.UpdateUser) users.DELETE("/:id", h.DeleteUser) users.GET("/me", h.GetCurrentUser) users.PUT("/me", h.UpdateCurrentUser) } // Category routes categories := api.Group("/categories") { categories.GET("", h.ListCategories) categories.GET("/:slug", h.GetCategory) categories.POST("", h.CreateCategory) categories.POST("/:id/contents", h.AddCategoryContent) } // Post routes posts := api.Group("/posts") { posts.GET("", h.ListPosts) posts.GET("/:slug", h.GetPost) posts.POST("", h.CreatePost) posts.POST("/:id/contents", h.AddPostContent) } // Contributor routes contributors := api.Group("/contributors") { contributors.GET("", h.ListContributors) contributors.GET("/:id", h.GetContributor) contributors.POST("", h.CreateContributor) contributors.POST("/:id/social-links", h.AddContributorSocialLink) } // Daily routes dailies := api.Group("/dailies") { dailies.GET("", h.ListDailies) dailies.GET("/:id", h.GetDaily) dailies.POST("", h.CreateDaily) dailies.POST("/:id/contents", h.AddDailyContent) } // Media routes media := api.Group("/media") { media.GET("", h.ListMedia) media.POST("", h.UploadMedia) media.GET("/:id", h.GetMedia) media.DELETE("/:id", h.DeleteMedia) } } } // Category handlers func (h *Handler) ListCategories(c *gin.Context) { langCode := c.Query("lang") if langCode == "" { langCode = "en" // Default to English } categories, err := h.service.ListCategories(c.Request.Context(), langCode) if err != nil { log.Error().Err(err).Msg("Failed to list categories") c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to list categories"}) return } c.JSON(http.StatusOK, categories) } func (h *Handler) GetCategory(c *gin.Context) { langCode := c.Query("lang") if langCode == "" { langCode = "en" // Default to English } slug := c.Param("slug") category, err := h.service.GetCategoryBySlug(c.Request.Context(), langCode, slug) if err != nil { if err == types.ErrNotFound { c.JSON(http.StatusNotFound, gin.H{"error": "Category not found"}) return } log.Error().Err(err).Msg("Failed to get category") c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to get category"}) return } c.JSON(http.StatusOK, category) } func (h *Handler) CreateCategory(c *gin.Context) { category, err := h.service.CreateCategory(c.Request.Context()) if err != nil { log.Error().Err(err).Msg("Failed to create category") c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create category"}) return } c.JSON(http.StatusCreated, category) } type AddCategoryContentRequest struct { LanguageCode string `json:"language_code" binding:"required"` Name string `json:"name" binding:"required"` Description *string `json:"description"` Slug string `json:"slug" binding:"required"` } func (h *Handler) AddCategoryContent(c *gin.Context) { var req AddCategoryContentRequest if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } categoryID, err := strconv.Atoi(c.Param("id")) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid category ID"}) return } var description string if req.Description != nil { description = *req.Description } content, err := h.service.AddCategoryContent(c.Request.Context(), categoryID, req.LanguageCode, req.Name, description, req.Slug) if err != nil { log.Error().Err(err).Msg("Failed to add category content") c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to add category content"}) return } c.JSON(http.StatusCreated, content) } // Post handlers func (h *Handler) ListPosts(c *gin.Context) { langCode := c.Query("lang") if langCode == "" { langCode = "en" // Default to English } var categoryID *int if catIDStr := c.Query("category_id"); catIDStr != "" { if id, err := strconv.Atoi(catIDStr); err == nil { categoryID = &id } } limit := 10 // Default limit if limitStr := c.Query("limit"); limitStr != "" { if l, err := strconv.Atoi(limitStr); err == nil && l > 0 { limit = l } } offset := 0 // Default offset if offsetStr := c.Query("offset"); offsetStr != "" { if o, err := strconv.Atoi(offsetStr); err == nil && o >= 0 { offset = o } } posts, err := h.service.ListPosts(c.Request.Context(), langCode, categoryID, limit, offset) if err != nil { log.Error().Err(err).Msg("Failed to list posts") c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to list posts"}) return } c.JSON(http.StatusOK, posts) } func (h *Handler) GetPost(c *gin.Context) { langCode := c.Query("lang") if langCode == "" { langCode = "en" // Default to English } slug := c.Param("slug") post, err := h.service.GetPostBySlug(c.Request.Context(), langCode, slug) if err != nil { log.Error().Err(err).Msg("Failed to get post") c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to get post"}) return } // Convert to a map to control the fields response := gin.H{ "id": post.ID, "status": post.Status, "slug": post.Slug, "edges": gin.H{ "contents": []gin.H{}, }, } contents := make([]gin.H, 0, len(post.Edges.Contents)) for _, content := range post.Edges.Contents { contents = append(contents, gin.H{ "language_code": content.LanguageCode, "title": content.Title, "content_markdown": content.ContentMarkdown, "summary": content.Summary, }) } response["edges"].(gin.H)["contents"] = contents c.JSON(http.StatusOK, response) } func (h *Handler) CreatePost(c *gin.Context) { post, err := h.service.CreatePost(c.Request.Context(), "draft") // Default to draft status if err != nil { log.Error().Err(err).Msg("Failed to create post") c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create post"}) return } // Convert to a map to control the fields response := gin.H{ "id": post.ID, "status": post.Status, "edges": gin.H{ "contents": []interface{}{}, }, } c.JSON(http.StatusCreated, response) } type AddPostContentRequest struct { LanguageCode string `json:"language_code" binding:"required"` Title string `json:"title" binding:"required"` ContentMarkdown string `json:"content_markdown" binding:"required"` Summary string `json:"summary" binding:"required"` MetaKeywords string `json:"meta_keywords"` MetaDescription string `json:"meta_description"` } func (h *Handler) AddPostContent(c *gin.Context) { var req AddPostContentRequest if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } postID, err := strconv.Atoi(c.Param("id")) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid post ID"}) return } content, err := h.service.AddPostContent(c.Request.Context(), postID, req.LanguageCode, req.Title, req.ContentMarkdown, req.Summary, req.MetaKeywords, req.MetaDescription) if err != nil { log.Error().Err(err).Msg("Failed to add post content") c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to add post content"}) return } c.JSON(http.StatusCreated, gin.H{ "title": content.Title, "content_markdown": content.ContentMarkdown, "language_code": content.LanguageCode, "summary": content.Summary, "meta_keywords": content.MetaKeywords, "meta_description": content.MetaDescription, "edges": gin.H{}, }) } // Contributor handlers func (h *Handler) ListContributors(c *gin.Context) { contributors, err := h.service.ListContributors(c.Request.Context()) if err != nil { log.Error().Err(err).Msg("Failed to list contributors") c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to list contributors"}) return } response := make([]gin.H, len(contributors)) for i, contributor := range contributors { socialLinks := make([]gin.H, len(contributor.Edges.SocialLinks)) for j, link := range contributor.Edges.SocialLinks { socialLinks[j] = gin.H{ "type": link.Type, "value": link.Value, "edges": gin.H{}, } } response[i] = gin.H{ "id": contributor.ID, "name": contributor.Name, "created_at": contributor.CreatedAt, "updated_at": contributor.UpdatedAt, "edges": gin.H{ "social_links": socialLinks, }, } } c.JSON(http.StatusOK, response) } func (h *Handler) GetContributor(c *gin.Context) { id, err := strconv.Atoi(c.Param("id")) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid contributor ID"}) return } contributor, err := h.service.GetContributorByID(c.Request.Context(), id) if err != nil { log.Error().Err(err).Msg("Failed to get contributor") c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to get contributor"}) return } c.JSON(http.StatusOK, contributor) } type CreateContributorRequest struct { Name string `json:"name" binding:"required"` AvatarURL *string `json:"avatar_url"` Bio *string `json:"bio"` } func (h *Handler) CreateContributor(c *gin.Context) { var req CreateContributorRequest if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } contributor, err := h.service.CreateContributor(c.Request.Context(), req.Name, req.AvatarURL, req.Bio) if err != nil { log.Error().Err(err).Msg("Failed to create contributor") c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create contributor"}) return } c.JSON(http.StatusCreated, contributor) } type AddContributorSocialLinkRequest struct { Type string `json:"type" binding:"required"` Name *string `json:"name"` Value string `json:"value" binding:"required"` } func (h *Handler) AddContributorSocialLink(c *gin.Context) { var req AddContributorSocialLinkRequest if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } contributorID, err := strconv.Atoi(c.Param("id")) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid contributor ID"}) return } name := "" if req.Name != nil { name = *req.Name } link, err := h.service.AddContributorSocialLink(c.Request.Context(), contributorID, req.Type, name, req.Value) if err != nil { log.Error().Err(err).Msg("Failed to add contributor social link") c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to add contributor social link"}) return } c.JSON(http.StatusCreated, link) } // Daily handlers func (h *Handler) ListDailies(c *gin.Context) { langCode := c.Query("lang") if langCode == "" { langCode = "en" // Default to English } var categoryID *int if catIDStr := c.Query("category_id"); catIDStr != "" { if id, err := strconv.Atoi(catIDStr); err == nil { categoryID = &id } } limit := 10 // Default limit if limitStr := c.Query("limit"); limitStr != "" { if l, err := strconv.Atoi(limitStr); err == nil && l > 0 { limit = l } } offset := 0 // Default offset if offsetStr := c.Query("offset"); offsetStr != "" { if o, err := strconv.Atoi(offsetStr); err == nil && o >= 0 { offset = o } } dailies, err := h.service.ListDailies(c.Request.Context(), langCode, categoryID, limit, offset) if err != nil { log.Error().Err(err).Msg("Failed to list dailies") c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to list dailies"}) return } c.JSON(http.StatusOK, dailies) } func (h *Handler) GetDaily(c *gin.Context) { id := c.Param("id") daily, err := h.service.GetDailyByID(c.Request.Context(), id) if err != nil { log.Error().Err(err).Msg("Failed to get daily") c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to get daily"}) return } c.JSON(http.StatusOK, daily) } type CreateDailyRequest struct { ID string `json:"id" binding:"required"` CategoryID int `json:"category_id" binding:"required"` ImageURL string `json:"image_url" binding:"required"` } func (h *Handler) CreateDaily(c *gin.Context) { var req CreateDailyRequest if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } daily, err := h.service.CreateDaily(c.Request.Context(), req.ID, req.CategoryID, req.ImageURL) if err != nil { log.Error().Err(err).Msg("Failed to create daily") c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create daily"}) return } c.JSON(http.StatusCreated, daily) } type AddDailyContentRequest struct { LanguageCode string `json:"language_code" binding:"required"` Quote string `json:"quote" binding:"required"` } func (h *Handler) AddDailyContent(c *gin.Context) { var req AddDailyContentRequest if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } dailyID := c.Param("id") content, err := h.service.AddDailyContent(c.Request.Context(), dailyID, req.LanguageCode, req.Quote) if err != nil { log.Error().Err(err).Msg("Failed to add daily content") c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to add daily content"}) return } c.JSON(http.StatusCreated, content) } // Helper functions func stringPtr(s *string) string { if s == nil { return "" } return *s }