Align GoChat with Chatwoot frontend contracts
This commit is contained in:
@@ -5,7 +5,10 @@ package v1
|
||||
// Pattern follows Chatwoot AccountSamlSettings API (super_admin scoped).
|
||||
|
||||
import (
|
||||
"crypto/sha1"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
@@ -13,8 +16,8 @@ import (
|
||||
|
||||
"github.com/gochat/gochat/internal/model"
|
||||
"github.com/gochat/gochat/internal/repository"
|
||||
"github.com/gochat/gochat/pkg/response"
|
||||
applogger "github.com/gochat/gochat/pkg/logger"
|
||||
"github.com/gochat/gochat/pkg/response"
|
||||
)
|
||||
|
||||
// AccountSamlSettingsHandler handles account-scoped SAML configuration endpoints.
|
||||
@@ -61,10 +64,7 @@ func (h *AccountSamlSettingsHandler) Get(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, response.APIResponse{
|
||||
Success: true,
|
||||
Data: settings,
|
||||
})
|
||||
c.JSON(http.StatusOK, serializeSamlSettings(settings))
|
||||
}
|
||||
|
||||
// Create creates SAML settings for an account.
|
||||
@@ -83,8 +83,8 @@ func (h *AccountSamlSettingsHandler) Create(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
var req CreateSamlSettingsRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
req, err := bindCreateSamlSettingsRequest(c)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, response.APIResponse{
|
||||
Success: false,
|
||||
Error: &response.ErrorBody{
|
||||
@@ -122,10 +122,7 @@ func (h *AccountSamlSettingsHandler) Create(c *gin.Context) {
|
||||
}
|
||||
|
||||
applogger.L().Infof("SAML settings created for account %d", accountID)
|
||||
c.JSON(http.StatusCreated, response.APIResponse{
|
||||
Success: true,
|
||||
Data: settings,
|
||||
})
|
||||
c.JSON(http.StatusCreated, serializeSamlSettings(&settings))
|
||||
}
|
||||
|
||||
// Update updates SAML settings for an account.
|
||||
@@ -144,8 +141,8 @@ func (h *AccountSamlSettingsHandler) Update(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
var req UpdateSamlSettingsRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
req, err := bindUpdateSamlSettingsRequest(c)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, response.APIResponse{
|
||||
Success: false,
|
||||
Error: &response.ErrorBody{
|
||||
@@ -197,10 +194,7 @@ func (h *AccountSamlSettingsHandler) Update(c *gin.Context) {
|
||||
// Return updated settings
|
||||
settings, _ := h.repo.GetByAccount(uint(accountID))
|
||||
applogger.L().Infof("SAML settings updated for account %d", accountID)
|
||||
c.JSON(http.StatusOK, response.APIResponse{
|
||||
Success: true,
|
||||
Data: settings,
|
||||
})
|
||||
c.JSON(http.StatusOK, serializeSamlSettings(settings))
|
||||
}
|
||||
|
||||
// Delete removes SAML settings for an account.
|
||||
@@ -277,8 +271,8 @@ func (h *AccountSamlSettingsHandler) ToggleActive(c *gin.Context) {
|
||||
Success: true,
|
||||
Data: map[string]interface{}{
|
||||
"account_id": accountID,
|
||||
"active": req.Active,
|
||||
"status": status,
|
||||
"active": req.Active,
|
||||
"status": status,
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -287,6 +281,8 @@ func (h *AccountSamlSettingsHandler) ToggleActive(c *gin.Context) {
|
||||
|
||||
// CreateSamlSettingsRequest is the request body for creating SAML settings.
|
||||
type CreateSamlSettingsRequest struct {
|
||||
SsoURL string `json:"sso_url"`
|
||||
Certificate string `json:"certificate"`
|
||||
IdpEntityID string `json:"idp_entity_id" binding:"required"`
|
||||
IdpSsoTargetURL string `json:"idp_sso_target_url" binding:"required"`
|
||||
IdpSloTargetURL string `json:"idp_slo_target_url"`
|
||||
@@ -300,6 +296,7 @@ type CreateSamlSettingsRequest struct {
|
||||
|
||||
// ToModel converts a CreateSamlSettingsRequest to an AccountSamlSettings model.
|
||||
func (req *CreateSamlSettingsRequest) ToModel(accountID uint) model.AccountSamlSettings {
|
||||
req.normalizeChatwootAliases()
|
||||
return model.AccountSamlSettings{
|
||||
AccountID: accountID,
|
||||
IdpEntityID: req.IdpEntityID,
|
||||
@@ -314,9 +311,20 @@ func (req *CreateSamlSettingsRequest) ToModel(accountID uint) model.AccountSamlS
|
||||
}
|
||||
}
|
||||
|
||||
func (req *CreateSamlSettingsRequest) normalizeChatwootAliases() {
|
||||
if req.IdpSsoTargetURL == "" {
|
||||
req.IdpSsoTargetURL = req.SsoURL
|
||||
}
|
||||
if req.IdpCertificate == "" {
|
||||
req.IdpCertificate = req.Certificate
|
||||
}
|
||||
}
|
||||
|
||||
// UpdateSamlSettingsRequest is the request body for updating SAML settings.
|
||||
// All fields are optional — only non-nil/non-zero fields will be updated.
|
||||
type UpdateSamlSettingsRequest struct {
|
||||
SsoURL *string `json:"sso_url"`
|
||||
Certificate *string `json:"certificate"`
|
||||
IdpEntityID *string `json:"idp_entity_id"`
|
||||
IdpSsoTargetURL *string `json:"idp_sso_target_url"`
|
||||
IdpSloTargetURL *string `json:"idp_slo_target_url"`
|
||||
@@ -331,6 +339,12 @@ type UpdateSamlSettingsRequest struct {
|
||||
// ToUpdatesMap converts an UpdateSamlSettingsRequest to a map of fields to update.
|
||||
func (req *UpdateSamlSettingsRequest) ToUpdatesMap() map[string]interface{} {
|
||||
updates := make(map[string]interface{})
|
||||
if req.IdpSsoTargetURL == nil {
|
||||
req.IdpSsoTargetURL = req.SsoURL
|
||||
}
|
||||
if req.IdpCertificate == nil {
|
||||
req.IdpCertificate = req.Certificate
|
||||
}
|
||||
if req.IdpEntityID != nil {
|
||||
updates["idp_entity_id"] = *req.IdpEntityID
|
||||
}
|
||||
@@ -370,9 +384,79 @@ type ToggleActiveRequest struct {
|
||||
// Only accessible to account administrators (enforced by AccountScope middleware in router).
|
||||
// Reference: Chatwoot AccountSamlSettings API — enterprise SSO configuration
|
||||
func RegisterAccountSamlSettingsRoutes(g *gin.RouterGroup, h *AccountSamlSettingsHandler) {
|
||||
g.POST("/", h.Create) // Create a new SAML config
|
||||
g.GET("/:id", h.Get) // Get a specific SAML config
|
||||
g.PUT("/:id", h.Update) // Update a SAML config
|
||||
g.DELETE("/:id", h.Delete) // Delete a SAML config
|
||||
g.GET("", h.Get) // Chatwoot frontend: fetch account SAML config
|
||||
g.POST("", h.Create) // Chatwoot frontend: create account SAML config
|
||||
g.PUT("", h.Update) // Chatwoot frontend: update account SAML config
|
||||
g.DELETE("", h.Delete) // Chatwoot frontend: delete account SAML config
|
||||
g.POST("/", h.Create) // Backward compatibility for trailing slash clients
|
||||
g.GET("/:id", h.Get) // Backward compatibility for id-based callers
|
||||
g.PUT("/:id", h.Update) // Backward compatibility for id-based callers
|
||||
g.DELETE("/:id", h.Delete) // Backward compatibility for id-based callers
|
||||
g.POST("/:id/toggle_active", h.ToggleActive) // Enable/disable SAML for a config
|
||||
}
|
||||
}
|
||||
|
||||
func bindCreateSamlSettingsRequest(c *gin.Context) (CreateSamlSettingsRequest, error) {
|
||||
req := CreateSamlSettingsRequest{}
|
||||
if err := bindSamlSettingsPayload(c, &req); err != nil {
|
||||
return req, err
|
||||
}
|
||||
req.normalizeChatwootAliases()
|
||||
if req.IdpEntityID == "" || req.IdpSsoTargetURL == "" || req.IdpCertificate == "" {
|
||||
return req, errors.New("idp_entity_id, idp_sso_target_url, and idp_certificate are required")
|
||||
}
|
||||
return req, nil
|
||||
}
|
||||
|
||||
func bindUpdateSamlSettingsRequest(c *gin.Context) (UpdateSamlSettingsRequest, error) {
|
||||
req := UpdateSamlSettingsRequest{}
|
||||
return req, bindSamlSettingsPayload(c, &req)
|
||||
}
|
||||
|
||||
func bindSamlSettingsPayload(c *gin.Context, req interface{}) error {
|
||||
var payload map[string]json.RawMessage
|
||||
if err := json.NewDecoder(c.Request.Body).Decode(&payload); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if nested, ok := payload["saml_settings"]; ok {
|
||||
return json.Unmarshal(nested, req)
|
||||
}
|
||||
|
||||
flat, err := json.Marshal(payload)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return json.Unmarshal(flat, req)
|
||||
}
|
||||
|
||||
func serializeSamlSettings(settings *model.AccountSamlSettings) gin.H {
|
||||
if settings == nil {
|
||||
return gin.H{}
|
||||
}
|
||||
|
||||
return gin.H{
|
||||
"id": settings.ID,
|
||||
"account_id": settings.AccountID,
|
||||
"idp_entity_id": settings.IdpEntityID,
|
||||
"idp_sso_target_url": settings.IdpSsoTargetURL,
|
||||
"idp_slo_target_url": settings.IdpSloTargetURL,
|
||||
"idp_certificate": settings.IdpCertificate,
|
||||
"sso_url": settings.IdpSsoTargetURL,
|
||||
"certificate": settings.IdpCertificate,
|
||||
"sp_entity_id": settings.SpEntityID,
|
||||
"sp_x509_certificate": settings.SpX509Certificate,
|
||||
"role_mappings": settings.RoleMappings,
|
||||
"active": settings.Active,
|
||||
"fingerprint": samlCertificateFingerprint(settings.IdpCertificate),
|
||||
"created_at": settings.CreatedAt,
|
||||
"updated_at": settings.UpdatedAt,
|
||||
}
|
||||
}
|
||||
|
||||
func samlCertificateFingerprint(certificate string) string {
|
||||
if certificate == "" {
|
||||
return ""
|
||||
}
|
||||
sum := sha1.Sum([]byte(certificate))
|
||||
return hex.EncodeToString(sum[:])
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user