package server import ( "errors" "net/http" "strings" "github.com/gin-gonic/gin" "gorm.io/gorm" "code.littlelan.cn/CarrotAssistant/backend/internal/security" "code.littlelan.cn/CarrotAssistant/backend/internal/store" ) // RequireAppToken guards the public /api/v1 chat routes. It expects an // X-App-Token header whose SHA-256 hash matches an App row. The matched App // is placed on the context as "app". func (d Deps) RequireAppToken() gin.HandlerFunc { return func(c *gin.Context) { raw := strings.TrimSpace(c.GetHeader("X-App-Token")) if raw == "" { c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "缺少 X-App-Token"}) return } hash := security.HashToken(raw) var app store.App err := d.DB.Where("token_hash = ? AND status = ?", hash, "active").First(&app).Error if err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "无效的 token"}) return } c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"error": "查询失败"}) return } c.Set("app", &app) c.Next() } } // appCtx returns the App placed by RequireAppToken. func appCtx(c *gin.Context) *store.App { v, _ := c.Get("app") if v == nil { return nil } return v.(*store.App) } // createSession persists a new session row and returns it. func (d Deps) createSession(appID int64, title string) (*store.Session, error) { id, err := newUUID() if err != nil { return nil, err } s := &store.Session{ID: id, AppID: appID, Title: title} if err := d.DB.Create(s).Error; err != nil { return nil, err } return s, nil } // loadSessionOwnedBy returns a session only if it belongs to appID. func (d Deps) loadSessionOwnedBy(sessionID string, appID int64) (*store.Session, error) { var s store.Session err := d.DB.Where("id = ? AND app_id = ?", sessionID, appID).First(&s).Error if err != nil { return nil, err } return &s, nil }