Files
gochat/backend/internal/middleware/platform_app_auth.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

86 lines
3.1 KiB
Go

package middleware
// Reference: Chatwoot PlatformController — AccessToken authentication
// Chatwoot authenticates Platform API requests via the api_access_token header,
// which maps to a PlatformApp owner. This middleware replicates that pattern:
//
// 1. Read api_access_token from request headers
// 2. Look up the AccessToken record (token is SHA-256 hashed in DB)
// 3. Verify the owner is a PlatformApp (not a User personal token)
// 4. Set platform_app in Gin context for downstream handlers
//
// Permissible validation is handled per-resource in each handler,
// matching Chatwoot's validate_platform_app_permissible callback.
import (
"crypto/sha256"
"encoding/hex"
"net/http"
"github.com/gin-gonic/gin"
"github.com/gochat/gochat/internal/model"
"github.com/gochat/gochat/pkg/response"
"gorm.io/gorm"
)
// PlatformAppAuth creates a middleware that authenticates Platform API requests
// via the api_access_token header (Chatwoot-compatible).
//
// The middleware:
// - Extracts api_access_token from request headers
// - Hashes the token (SHA-256) and queries the access_tokens table
// - Verifies the token owner is a PlatformApp
// - Loads the PlatformApp with its Permissibles for downstream checks
// - Sets platform_app_id and platform_app in Gin context
//
// If no token is provided, or the token is invalid, or the owner is not
// a PlatformApp, the request is rejected with 401 Unauthorized.
func PlatformAppAuth(db *gorm.DB) gin.HandlerFunc {
return func(c *gin.Context) {
// Step 1: Extract api_access_token from headers
// Chatwoot reads both :api_access_token and :HTTP_API_ACCESS_TOKEN
token := c.GetHeader("api_access_token")
if token == "" {
token = c.GetHeader("HTTP_API_ACCESS_TOKEN")
}
if token == "" {
response.AbortWithStatusError(c, http.StatusUnauthorized, response.ErrUnauthorized,
"api_access_token header required")
return
}
// Step 2: Hash the token (SHA-256) to match stored hash
hash := sha256.Sum256([]byte(token))
tokenHash := hex.EncodeToString(hash[:])
// Step 3: Query AccessToken by hashed token
var accessToken model.AccessToken
if err := db.Where("token = ? AND owner_type = ?", tokenHash, model.AccessTokenOwnerTypePlatformApp).
First(&accessToken).Error; err != nil {
if err == gorm.ErrRecordNotFound {
response.AbortWithStatusError(c, http.StatusUnauthorized, response.ErrUnauthorized,
"Invalid access_token")
return
}
response.AbortWithStatusError(c, http.StatusInternalServerError, response.ErrInternal,
"Failed to verify access token")
return
}
// Step 4: Load the PlatformApp with its Permissibles
var platformApp model.PlatformApp
if err := db.Preload("Permissibles").First(&platformApp, accessToken.OwnerID).Error; err != nil {
response.AbortWithStatusError(c, http.StatusUnauthorized, response.ErrUnauthorized,
"Invalid access_token")
return
}
// Step 5: Set platform_app in Gin context for downstream handlers
c.Set("platform_app", platformApp)
c.Set("platform_app_id", platformApp.ID)
c.Set("access_token_id", accessToken.ID)
c.Next()
}
}