Files
gochat/backend/internal/handler/api/v1/helpers.go
T
rogee aeddedf2a3 Reorganize repo: backend/, deploy/, docs/ layout + AGENTS.md
Restructure the monorepo into clear top-level directories:
- backend/: Go module root (cmd, internal, pkg, configs, migrations,
  docs/swagger, scripts, tests, go.mod, Makefile, .air.toml)
- deploy/: Docker (Dockerfile, docker-compose*), quickstart, fluentd
- docs/: project documentation + reports/ (moved from repo root)
- AGENTS.md: new AI coding-agent guide at repo root

Update all references to the new layout:
- Dockerfile: COPY backend/go.mod, COPY backend/ (context = repo root)
- docker-compose files: context ../.., dockerfile deploy/docker/Dockerfile,
  env_file ../../.env, volume mounts ../../backend:/app
- deploy/quickstart/compose.yaml: dockerfile deploy/docker/Dockerfile
- CI: working-directory: backend for go commands, file deploy/docker/Dockerfile,
  coverage path backend/coverage.out, health_check backend/scripts/
- backend/Makefile: docker target uses -f ../deploy/docker/Dockerfile ../
- README: architecture tree, quickstart, config paths updated

Move root stray scripts (rename_models.*, run_m11_tests.sh, verify_build.sh,
gorm_bool_main.go) to backend/scripts/legacy/. All moves via git mv to
preserve history. Build, vet, SQLite tests, and docker compose config verified.
2026-07-07 14:44:12 +08:00

216 lines
4.8 KiB
Go

package v1
import (
"bytes"
"encoding/json"
"fmt"
"io"
"strconv"
"github.com/gin-gonic/gin"
)
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
}