378 lines
12 KiB
Go
378 lines
12 KiB
Go
package v1
|
|
|
|
// Reference: M13 §2 — Account-scoped SAML config admin API
|
|
// CRUD endpoints for enterprise administrators to configure SAML SSO for their accounts.
|
|
// Pattern follows Chatwoot AccountSamlSettings API (super_admin scoped).
|
|
|
|
import (
|
|
"encoding/json"
|
|
"net/http"
|
|
"strconv"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
|
|
"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"
|
|
)
|
|
|
|
// AccountSamlSettingsHandler handles account-scoped SAML configuration endpoints.
|
|
// Only accessible to account administrators (role: administrator or super_admin).
|
|
type AccountSamlSettingsHandler struct {
|
|
repo *repository.AccountSamlSettingsRepo
|
|
}
|
|
|
|
// NewAccountSamlSettingsHandler creates a new AccountSamlSettings handler.
|
|
func NewAccountSamlSettingsHandler(repo *repository.AccountSamlSettingsRepo) *AccountSamlSettingsHandler {
|
|
return &AccountSamlSettingsHandler{repo: repo}
|
|
}
|
|
|
|
// Get retrieves SAML settings for an account.
|
|
// GET /api/v1/accounts/:account_id/saml_settings
|
|
func (h *AccountSamlSettingsHandler) Get(c *gin.Context) {
|
|
accountID, err := strconv.ParseUint(c.Param("account_id"), 10, 64)
|
|
if err != nil {
|
|
c.JSON(http.StatusBadRequest, response.APIResponse{
|
|
Success: false,
|
|
Error: &response.ErrorBody{
|
|
Code: response.ErrBadRequest,
|
|
Message: "Invalid account ID",
|
|
},
|
|
})
|
|
return
|
|
}
|
|
|
|
settings, err := h.repo.GetByAccount(uint(accountID))
|
|
if err != nil {
|
|
applogger.L().Errorf("Get SAML settings for account %d: %v", accountID, err)
|
|
response.AbortWithStatusError(c, http.StatusInternalServerError, response.ErrInternal, "failed to get SAML settings")
|
|
return
|
|
}
|
|
|
|
if settings == nil {
|
|
c.JSON(http.StatusNotFound, response.APIResponse{
|
|
Success: false,
|
|
Error: &response.ErrorBody{
|
|
Code: response.ErrNotFound,
|
|
Message: "SAML settings not found for this account",
|
|
},
|
|
})
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, response.APIResponse{
|
|
Success: true,
|
|
Data: settings,
|
|
})
|
|
}
|
|
|
|
// Create creates SAML settings for an account.
|
|
// POST /api/v1/accounts/:account_id/saml_settings
|
|
// Body: JSON with IdP configuration fields.
|
|
func (h *AccountSamlSettingsHandler) Create(c *gin.Context) {
|
|
accountID, err := strconv.ParseUint(c.Param("account_id"), 10, 64)
|
|
if err != nil {
|
|
c.JSON(http.StatusBadRequest, response.APIResponse{
|
|
Success: false,
|
|
Error: &response.ErrorBody{
|
|
Code: response.ErrBadRequest,
|
|
Message: "Invalid account ID",
|
|
},
|
|
})
|
|
return
|
|
}
|
|
|
|
var req CreateSamlSettingsRequest
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
c.JSON(http.StatusBadRequest, response.APIResponse{
|
|
Success: false,
|
|
Error: &response.ErrorBody{
|
|
Code: response.ErrBadRequest,
|
|
Message: "Invalid request body",
|
|
Detail: err.Error(),
|
|
},
|
|
})
|
|
return
|
|
}
|
|
|
|
// Check if settings already exist for this account
|
|
existing, err := h.repo.GetByAccount(uint(accountID))
|
|
if err != nil {
|
|
applogger.L().Errorf("Check existing SAML settings for account %d: %v", accountID, err)
|
|
response.AbortWithStatusError(c, http.StatusInternalServerError, response.ErrInternal, "failed to check existing settings")
|
|
return
|
|
}
|
|
if existing != nil {
|
|
c.JSON(http.StatusConflict, response.APIResponse{
|
|
Success: false,
|
|
Error: &response.ErrorBody{
|
|
Code: response.ErrBadRequest,
|
|
Message: "SAML settings already exist for this account",
|
|
},
|
|
})
|
|
return
|
|
}
|
|
|
|
settings := req.ToModel(uint(accountID))
|
|
if err := h.repo.Create(&settings); err != nil {
|
|
applogger.L().Errorf("Create SAML settings for account %d: %v", accountID, err)
|
|
response.AbortWithStatusError(c, http.StatusInternalServerError, response.ErrInternal, "failed to create SAML settings")
|
|
return
|
|
}
|
|
|
|
applogger.L().Infof("SAML settings created for account %d", accountID)
|
|
c.JSON(http.StatusCreated, response.APIResponse{
|
|
Success: true,
|
|
Data: settings,
|
|
})
|
|
}
|
|
|
|
// Update updates SAML settings for an account.
|
|
// PUT /api/v1/accounts/:account_id/saml_settings
|
|
// Body: JSON with fields to update (partial update supported).
|
|
func (h *AccountSamlSettingsHandler) Update(c *gin.Context) {
|
|
accountID, err := strconv.ParseUint(c.Param("account_id"), 10, 64)
|
|
if err != nil {
|
|
c.JSON(http.StatusBadRequest, response.APIResponse{
|
|
Success: false,
|
|
Error: &response.ErrorBody{
|
|
Code: response.ErrBadRequest,
|
|
Message: "Invalid account ID",
|
|
},
|
|
})
|
|
return
|
|
}
|
|
|
|
var req UpdateSamlSettingsRequest
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
c.JSON(http.StatusBadRequest, response.APIResponse{
|
|
Success: false,
|
|
Error: &response.ErrorBody{
|
|
Code: response.ErrBadRequest,
|
|
Message: "Invalid request body",
|
|
Detail: err.Error(),
|
|
},
|
|
})
|
|
return
|
|
}
|
|
|
|
// Verify settings exist
|
|
existing, err := h.repo.GetByAccount(uint(accountID))
|
|
if err != nil {
|
|
applogger.L().Errorf("Get SAML settings for account %d: %v", accountID, err)
|
|
response.AbortWithStatusError(c, http.StatusInternalServerError, response.ErrInternal, "failed to get SAML settings")
|
|
return
|
|
}
|
|
if existing == nil {
|
|
c.JSON(http.StatusNotFound, response.APIResponse{
|
|
Success: false,
|
|
Error: &response.ErrorBody{
|
|
Code: response.ErrNotFound,
|
|
Message: "SAML settings not found for this account",
|
|
},
|
|
})
|
|
return
|
|
}
|
|
|
|
// Build updates map
|
|
updates := req.ToUpdatesMap()
|
|
if len(updates) == 0 {
|
|
c.JSON(http.StatusBadRequest, response.APIResponse{
|
|
Success: false,
|
|
Error: &response.ErrorBody{
|
|
Code: response.ErrBadRequest,
|
|
Message: "No fields to update",
|
|
},
|
|
})
|
|
return
|
|
}
|
|
|
|
if err := h.repo.UpdateFields(uint(accountID), updates); err != nil {
|
|
applogger.L().Errorf("Update SAML settings for account %d: %v", accountID, err)
|
|
response.AbortWithStatusError(c, http.StatusInternalServerError, response.ErrInternal, "failed to update SAML settings")
|
|
return
|
|
}
|
|
|
|
// 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,
|
|
})
|
|
}
|
|
|
|
// Delete removes SAML settings for an account.
|
|
// DELETE /api/v1/accounts/:account_id/saml_settings
|
|
func (h *AccountSamlSettingsHandler) Delete(c *gin.Context) {
|
|
accountID, err := strconv.ParseUint(c.Param("account_id"), 10, 64)
|
|
if err != nil {
|
|
c.JSON(http.StatusBadRequest, response.APIResponse{
|
|
Success: false,
|
|
Error: &response.ErrorBody{
|
|
Code: response.ErrBadRequest,
|
|
Message: "Invalid account ID",
|
|
},
|
|
})
|
|
return
|
|
}
|
|
|
|
if err := h.repo.Delete(uint(accountID)); err != nil {
|
|
applogger.L().Errorf("Delete SAML settings for account %d: %v", accountID, err)
|
|
response.AbortWithStatusError(c, http.StatusInternalServerError, response.ErrInternal, "failed to delete SAML settings")
|
|
return
|
|
}
|
|
|
|
applogger.L().Infof("SAML settings deleted for account %d", accountID)
|
|
c.JSON(http.StatusOK, response.APIResponse{
|
|
Success: true,
|
|
Data: map[string]string{
|
|
"message": "SAML settings deleted",
|
|
},
|
|
})
|
|
}
|
|
|
|
// ToggleActive enables or disables SAML SSO for an account.
|
|
// POST /api/v1/accounts/:account_id/saml_settings/toggle_active
|
|
// Body: { "active": true/false }
|
|
func (h *AccountSamlSettingsHandler) ToggleActive(c *gin.Context) {
|
|
accountID, err := strconv.ParseUint(c.Param("account_id"), 10, 64)
|
|
if err != nil {
|
|
c.JSON(http.StatusBadRequest, response.APIResponse{
|
|
Success: false,
|
|
Error: &response.ErrorBody{
|
|
Code: response.ErrBadRequest,
|
|
Message: "Invalid account ID",
|
|
},
|
|
})
|
|
return
|
|
}
|
|
|
|
var req ToggleActiveRequest
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
c.JSON(http.StatusBadRequest, response.APIResponse{
|
|
Success: false,
|
|
Error: &response.ErrorBody{
|
|
Code: response.ErrBadRequest,
|
|
Message: "Invalid request body",
|
|
Detail: err.Error(),
|
|
},
|
|
})
|
|
return
|
|
}
|
|
|
|
if err := h.repo.SetActive(uint(accountID), req.Active); err != nil {
|
|
applogger.L().Errorf("Toggle SAML active for account %d: %v", accountID, err)
|
|
response.AbortWithStatusError(c, http.StatusInternalServerError, response.ErrInternal, "failed to toggle SAML active status")
|
|
return
|
|
}
|
|
|
|
status := "disabled"
|
|
if req.Active {
|
|
status = "enabled"
|
|
}
|
|
applogger.L().Infof("SAML SSO %s for account %d", status, accountID)
|
|
c.JSON(http.StatusOK, response.APIResponse{
|
|
Success: true,
|
|
Data: map[string]interface{}{
|
|
"account_id": accountID,
|
|
"active": req.Active,
|
|
"status": status,
|
|
},
|
|
})
|
|
}
|
|
|
|
// --- Request/Response types ---
|
|
|
|
// CreateSamlSettingsRequest is the request body for creating SAML settings.
|
|
type CreateSamlSettingsRequest struct {
|
|
IdpEntityID string `json:"idp_entity_id" binding:"required"`
|
|
IdpSsoTargetURL string `json:"idp_sso_target_url" binding:"required"`
|
|
IdpSloTargetURL string `json:"idp_slo_target_url"`
|
|
IdpCertificate string `json:"idp_certificate" binding:"required"`
|
|
SpEntityID string `json:"sp_entity_id"`
|
|
SpX509Certificate string `json:"sp_x509_certificate"`
|
|
SpPrivateKey string `json:"sp_private_key"`
|
|
RoleMappings json.RawMessage `json:"role_mappings"`
|
|
Active bool `json:"active"`
|
|
}
|
|
|
|
// ToModel converts a CreateSamlSettingsRequest to an AccountSamlSettings model.
|
|
func (req *CreateSamlSettingsRequest) ToModel(accountID uint) model.AccountSamlSettings {
|
|
return model.AccountSamlSettings{
|
|
AccountID: accountID,
|
|
IdpEntityID: req.IdpEntityID,
|
|
IdpSsoTargetURL: req.IdpSsoTargetURL,
|
|
IdpSloTargetURL: req.IdpSloTargetURL,
|
|
IdpCertificate: req.IdpCertificate,
|
|
SpEntityID: req.SpEntityID,
|
|
SpX509Certificate: req.SpX509Certificate,
|
|
SpPrivateKey: req.SpPrivateKey,
|
|
RoleMappings: req.RoleMappings,
|
|
Active: req.Active,
|
|
}
|
|
}
|
|
|
|
// 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 {
|
|
IdpEntityID *string `json:"idp_entity_id"`
|
|
IdpSsoTargetURL *string `json:"idp_sso_target_url"`
|
|
IdpSloTargetURL *string `json:"idp_slo_target_url"`
|
|
IdpCertificate *string `json:"idp_certificate"`
|
|
SpEntityID *string `json:"sp_entity_id"`
|
|
SpX509Certificate *string `json:"sp_x509_certificate"`
|
|
SpPrivateKey *string `json:"sp_private_key"`
|
|
RoleMappings json.RawMessage `json:"role_mappings"`
|
|
Active *bool `json:"active"`
|
|
}
|
|
|
|
// 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.IdpEntityID != nil {
|
|
updates["idp_entity_id"] = *req.IdpEntityID
|
|
}
|
|
if req.IdpSsoTargetURL != nil {
|
|
updates["idp_sso_target_url"] = *req.IdpSsoTargetURL
|
|
}
|
|
if req.IdpSloTargetURL != nil {
|
|
updates["idp_slo_target_url"] = *req.IdpSloTargetURL
|
|
}
|
|
if req.IdpCertificate != nil {
|
|
updates["idp_certificate"] = *req.IdpCertificate
|
|
}
|
|
if req.SpEntityID != nil {
|
|
updates["sp_entity_id"] = *req.SpEntityID
|
|
}
|
|
if req.SpX509Certificate != nil {
|
|
updates["sp_x509_certificate"] = *req.SpX509Certificate
|
|
}
|
|
if req.SpPrivateKey != nil {
|
|
updates["sp_private_key"] = *req.SpPrivateKey
|
|
}
|
|
if req.RoleMappings != nil {
|
|
updates["role_mappings"] = req.RoleMappings
|
|
}
|
|
if req.Active != nil {
|
|
updates["active"] = *req.Active
|
|
}
|
|
return updates
|
|
}
|
|
|
|
// ToggleActiveRequest toggles the active status of SAML settings.
|
|
type ToggleActiveRequest struct {
|
|
Active bool `json:"active"`
|
|
}
|
|
|
|
// RegisterAccountSamlSettingsRoutes maps account-scoped SAML config admin routes.
|
|
// 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.POST("/:id/toggle_active", h.ToggleActive) // Enable/disable SAML for a config
|
|
} |