package v1 import ( "net/http" "strconv" "github.com/gin-gonic/gin" "github.com/gochat/gochat/internal/service" applogger "github.com/gochat/gochat/pkg/logger" "github.com/gochat/gochat/pkg/pagination" "github.com/gochat/gochat/pkg/response" ) // PortalHandler handles Portal CRUD endpoints. type PortalHandler struct { svc *service.PortalService } // NewPortalHandler creates a new PortalHandler. func NewPortalHandler(svc *service.PortalService) *PortalHandler { return &PortalHandler{svc: svc} } // Create creates a new portal. // POST /api/v1/accounts/:account_id/portals func (h *PortalHandler) Create(c *gin.Context) { accountID, err := strconv.ParseUint(c.Param("account_id"), 10, 64) if err != nil { response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid account_id") return } // Chatwoot: params.require(:portal) → {"portal": {...}} var wrapper struct { Portal service.CreatePortalRequest `json:"portal"` } if err := c.ShouldBindJSON(&wrapper); err != nil { response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrValidation, err.Error()) return } req := wrapper.Portal portal, err := h.svc.Create(c.Request.Context(), uint(accountID), &req) if err != nil { applogger.L().Errorf("Create portal: %v", err) response.AbortWithStatusError(c, http.StatusInternalServerError, response.ErrInternal, "failed to create portal") return } response.Created(c, portal) } // Get retrieves a portal by ID. // GET /api/v1/accounts/:account_id/portals/:portal_id func (h *PortalHandler) Get(c *gin.Context) { portalID, err := strconv.ParseUint(c.Param("portal_id"), 10, 64) if err != nil { response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid portal_id") return } portal, err := h.svc.GetByID(c.Request.Context(), uint(portalID)) if err != nil { applogger.L().Errorf("Get portal: %v", err) response.AbortWithStatusError(c, http.StatusNotFound, response.ErrNotFound, "portal not found") return } response.OK(c, portal) } // Update modifies an existing portal. // PUT /api/v1/accounts/:account_id/portals/:portal_id func (h *PortalHandler) Update(c *gin.Context) { portalID, err := strconv.ParseUint(c.Param("portal_id"), 10, 64) if err != nil { response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid portal_id") return } // Chatwoot: params.require(:portal) → {"portal": {...}} var wrapper struct { Portal service.UpdatePortalRequest `json:"portal"` } if err := c.ShouldBindJSON(&wrapper); err != nil { response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrValidation, err.Error()) return } req := wrapper.Portal portal, err := h.svc.Update(c.Request.Context(), uint(portalID), &req) if err != nil { applogger.L().Errorf("Update portal: %v", err) response.AbortWithStatusError(c, http.StatusInternalServerError, response.ErrInternal, "failed to update portal") return } response.OK(c, portal) } // Delete soft-deletes a portal. // DELETE /api/v1/accounts/:account_id/portals/:portal_id func (h *PortalHandler) Delete(c *gin.Context) { portalID, err := strconv.ParseUint(c.Param("portal_id"), 10, 64) if err != nil { response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid portal_id") return } if err := h.svc.Delete(c.Request.Context(), uint(portalID)); err != nil { applogger.L().Errorf("Delete portal: %v", err) response.AbortWithStatusError(c, http.StatusInternalServerError, response.ErrInternal, "failed to delete portal") return } response.NoContent(c) } // List retrieves all portals for an account (paginated). // GET /api/v1/accounts/:account_id/portals func (h *PortalHandler) List(c *gin.Context) { accountID, err := strconv.ParseUint(c.Param("account_id"), 10, 64) if err != nil { response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid account_id") return } pg := pagination.Parse(c) portals, total, err := h.svc.ListByAccountID(c.Request.Context(), uint(accountID), pg.Page, pg.PerPage) if err != nil { applogger.L().Errorf("List portals: %v", err) response.AbortWithStatusError(c, http.StatusInternalServerError, response.ErrInternal, "failed to list portals") return } response.OKWithMeta(c, portals, pg.Page, pg.PerPage, total) } // Archive sets archived=true on a portal. // POST /api/v1/accounts/:account_id/portals/:portal_id/archive func (h *PortalHandler) Archive(c *gin.Context) { portalID, err := strconv.ParseUint(c.Param("portal_id"), 10, 64) if err != nil { response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid portal_id") return } portal, err := h.svc.Archive(c.Request.Context(), uint(portalID)) if err != nil { applogger.L().Errorf("Archive portal: %v", err) response.AbortWithStatusError(c, http.StatusInternalServerError, response.ErrInternal, "failed to archive portal") return } response.OK(c, portal) } // RemoveLogo clears the logo_url on a portal. // DELETE /api/v1/accounts/:account_id/portals/:portal_id/logo func (h *PortalHandler) RemoveLogo(c *gin.Context) { portalID, err := strconv.ParseUint(c.Param("portal_id"), 10, 64) if err != nil { response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid portal_id") return } portal, err := h.svc.RemoveLogo(c.Request.Context(), uint(portalID)) if err != nil { applogger.L().Errorf("Remove portal logo: %v", err) response.AbortWithStatusError(c, http.StatusInternalServerError, response.ErrInternal, "failed to remove portal logo") return } response.OK(c, portal) } // SendInstructions sends CNAME configuration instructions to an email address. // POST /api/v1/accounts/:account_id/portals/:portal_id/send_instructions func (h *PortalHandler) SendInstructions(c *gin.Context) { portalID, err := strconv.ParseUint(c.Param("portal_id"), 10, 64) if err != nil { response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid portal_id") return } var req service.SendInstructionsRequest if err := c.ShouldBindJSON(&req); err != nil { response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrValidation, err.Error()) return } if err := h.svc.SendInstructions(c.Request.Context(), uint(portalID), &req); err != nil { applogger.L().Errorf("Send portal instructions: %v", err) response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, err.Error()) return } response.NoContent(c) } // SSLStatus returns the SSL certificate status for a portal's custom domain. // GET /api/v1/accounts/:account_id/portals/:portal_id/ssl_status func (h *PortalHandler) SSLStatus(c *gin.Context) { portalID, err := strconv.ParseUint(c.Param("portal_id"), 10, 64) if err != nil { response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid portal_id") return } status, err := h.svc.SSLStatus(c.Request.Context(), uint(portalID)) if err != nil { applogger.L().Errorf("Get SSL status: %v", err) response.AbortWithStatusError(c, http.StatusInternalServerError, response.ErrInternal, "failed to get SSL status") return } response.OK(c, status) }