244 lines
6.0 KiB
Go
244 lines
6.0 KiB
Go
package v1
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"strconv"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
"github.com/gochat/gochat/internal/middleware"
|
|
"github.com/gochat/gochat/internal/model"
|
|
"gorm.io/gorm"
|
|
)
|
|
|
|
func bindJSONWrappedOrRaw(c *gin.Context, wrapperKey string, target any) error {
|
|
if c.Request.Body == nil {
|
|
return fmt.Errorf("empty request body")
|
|
}
|
|
body, err := io.ReadAll(c.Request.Body)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
c.Request.Body = io.NopCloser(bytes.NewReader(body))
|
|
if len(bytes.TrimSpace(body)) == 0 {
|
|
return fmt.Errorf("empty request body")
|
|
}
|
|
|
|
var wrapper map[string]json.RawMessage
|
|
if err := json.Unmarshal(body, &wrapper); err == nil {
|
|
if raw, ok := wrapper[wrapperKey]; ok && len(raw) > 0 && string(raw) != "null" {
|
|
return json.Unmarshal(raw, target)
|
|
}
|
|
}
|
|
return json.Unmarshal(body, target)
|
|
}
|
|
|
|
// parseUintParam extracts a uint path parameter from the Gin context.
|
|
func parseUintParam(c *gin.Context, param string) (uint, error) {
|
|
val := c.Param(param)
|
|
n, err := strconv.ParseUint(val, 10, 32)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
return uint(n), nil
|
|
}
|
|
|
|
func parseOptionalUintQueryParam(c *gin.Context, param string) (uint, error) {
|
|
val := c.Query(param)
|
|
if val == "" {
|
|
return 0, nil
|
|
}
|
|
n, err := strconv.ParseUint(val, 10, 32)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
return uint(n), nil
|
|
}
|
|
|
|
func parseIntQueryDefault(c *gin.Context, param string, fallback int) (int, error) {
|
|
val := c.Query(param)
|
|
if val == "" {
|
|
return fallback, nil
|
|
}
|
|
n, err := strconv.Atoi(val)
|
|
if err != nil {
|
|
return fallback, err
|
|
}
|
|
return n, nil
|
|
}
|
|
|
|
func fixedPageOffset(c *gin.Context, perPage int) (int, int) {
|
|
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
|
|
if page < 1 {
|
|
page = 1
|
|
}
|
|
return page, (page - 1) * perPage
|
|
}
|
|
|
|
func parseUintAnyParam(c *gin.Context, params ...string) (uint, error) {
|
|
var lastErr error
|
|
for _, param := range params {
|
|
if c.Param(param) == "" {
|
|
continue
|
|
}
|
|
id, err := parseUintParam(c, param)
|
|
if err == nil && id != 0 {
|
|
return id, nil
|
|
}
|
|
return 0, err
|
|
}
|
|
return 0, lastErr
|
|
}
|
|
|
|
// parseAccountIDParam accepts both Chatwoot-style :account_id and older local
|
|
// tests/routes that still mount the account parameter as :id.
|
|
func parseAccountIDParam(c *gin.Context) uint {
|
|
if id, err := parseUintParam(c, "account_id"); err == nil && id != 0 {
|
|
return id
|
|
}
|
|
if id, err := parseUintParam(c, "id"); err == nil && id != 0 {
|
|
return id
|
|
}
|
|
return 0
|
|
}
|
|
|
|
// getAccountID extracts account ID from the request.
|
|
// Priority: URL param account_id > X-Account-ID header > JWT claims account_id
|
|
func getAccountID(c *gin.Context) uint {
|
|
// Try URL param — Chatwoot uses :account_id, GoChat routes use :account_id where possible
|
|
if id, err := parseUintParam(c, "account_id"); err == nil && id != 0 {
|
|
return id
|
|
}
|
|
// Try X-Account-ID header
|
|
headerAccountID := c.GetHeader("X-Account-ID")
|
|
if headerAccountID != "" {
|
|
id, err := strconv.ParseUint(headerAccountID, 10, 32)
|
|
if err == nil && id != 0 {
|
|
return uint(id)
|
|
}
|
|
}
|
|
// Fall back to JWT claims (set by AuthRequired middleware)
|
|
if accountID, exists := c.Get("account_id"); exists {
|
|
switch v := accountID.(type) {
|
|
case uint:
|
|
if v != 0 {
|
|
return v
|
|
}
|
|
case float64:
|
|
if v != 0 {
|
|
return uint(v)
|
|
}
|
|
case int:
|
|
if v != 0 {
|
|
return uint(v)
|
|
}
|
|
}
|
|
}
|
|
return 0
|
|
}
|
|
|
|
// getRole extracts the current user's role from the Gin context.
|
|
// Returns the role string (e.g. "administrator", "agent") from auth middleware claims.
|
|
func getRole(c *gin.Context) string {
|
|
if role, exists := c.Get("role"); exists {
|
|
if s, ok := role.(string); ok {
|
|
return s
|
|
}
|
|
}
|
|
return ""
|
|
}
|
|
|
|
// getCustomRoleID extracts the current user's custom role ID from the Gin context.
|
|
func getCustomRoleID(c *gin.Context) uint {
|
|
if id, exists := c.Get("custom_role_id"); exists {
|
|
switch v := id.(type) {
|
|
case uint:
|
|
return v
|
|
case float64:
|
|
return uint(v)
|
|
case int:
|
|
return uint(v)
|
|
}
|
|
}
|
|
return 0
|
|
}
|
|
|
|
// getUserID extracts the current user ID from the Gin context.
|
|
// Priority: JWT claims > X-User-ID header
|
|
func getUserID(c *gin.Context) uint {
|
|
if userID, exists := c.Get("user_id"); exists {
|
|
switch v := userID.(type) {
|
|
case uint:
|
|
return v
|
|
case float64:
|
|
return uint(v)
|
|
case int:
|
|
return uint(v)
|
|
case string:
|
|
n, err := strconv.ParseUint(v, 10, 32)
|
|
if err == nil {
|
|
return uint(n)
|
|
}
|
|
}
|
|
}
|
|
// Fallback: X-User-ID header
|
|
headerUserID := c.GetHeader("X-User-ID")
|
|
if headerUserID != "" {
|
|
n, err := strconv.ParseUint(headerUserID, 10, 32)
|
|
if err == nil {
|
|
return uint(n)
|
|
}
|
|
}
|
|
return 0
|
|
}
|
|
|
|
// getPage extracts page number from query params (default: 1).
|
|
func getPage(c *gin.Context) int {
|
|
val := c.DefaultQuery("page", "1")
|
|
n, err := strconv.Atoi(val)
|
|
if err != nil || n < 1 {
|
|
return 1
|
|
}
|
|
return n
|
|
}
|
|
|
|
// getPageSize extracts page size from query params (default: 25, max: 100).
|
|
func getPageSize(c *gin.Context) int {
|
|
val := c.DefaultQuery("page_size", "25")
|
|
n, err := strconv.Atoi(val)
|
|
if err != nil || n < 1 {
|
|
return 25
|
|
}
|
|
if n > 100 {
|
|
return 100
|
|
}
|
|
return n
|
|
}
|
|
|
|
func requireConnectorShangwutongConversation(c *gin.Context, db *gorm.DB, conversation *model.Conversation) bool {
|
|
if !middleware.IsConnectorService(c) {
|
|
return true
|
|
}
|
|
if c.GetHeader("X-GoChat-Schema-Version") != "1" || db == nil || conversation == nil {
|
|
connectorRouteError(c, http.StatusUnprocessableEntity, "unsupported_schema_version", "X-GoChat-Schema-Version must be 1")
|
|
return false
|
|
}
|
|
var count int64
|
|
if err := db.WithContext(c.Request.Context()).Model(&model.Inbox{}).Where(
|
|
"id = ? AND account_id = ? AND channel_type = ?", conversation.InboxID, conversation.AccountID, "shangwutong",
|
|
).Count(&count).Error; err != nil || count != 1 {
|
|
connectorRouteError(c, http.StatusForbidden, "forbidden", "connector cannot access this conversation")
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
|
|
func connectorRouteError(c *gin.Context, status int, code, message string) {
|
|
c.AbortWithStatusJSON(status, gin.H{"error": gin.H{
|
|
"code": code, "message": message, "retryable": false, "request_id": c.GetString("request_id"),
|
|
}})
|
|
}
|