Update all handler functions to use *echo.Context (pointer) instead of echo.Context (value) as required by Echo v5. Changes across all handler files: - analytics.go: Update handler signatures - auth.go: Update authentication handler signatures - book_matching.go: Update matching handler signatures - collections.go: Update collection handler signatures - collections_preview_test.go: Update test signatures - commonhandlers.go: Update common handler signatures - conflicts.go: Update conflict handler signatures - context.go: Update context handler signatures - dashboard.go: Update dashboard handler signatures - devices.go: Update device handler signatures - jobs.go: Update job handler signatures - kobo.go: Update Kobo handler signatures - koreader.go: Update Koreader handler signatures - library.go: Update library handler signatures - matching.go: Update matching handler signatures - media.go: Update media handler signatures - opds.go: Update OPDS handler signatures - progress.go: Update progress handler signatures - queue.go: Update queue handler signatures - refresh_token.go: Update token handler signatures - scanner.go: Update scanner handler signatures - sidecar.go: Update sidecar handler signatures - sync.go: Update sync handler signatures - system_settings.go: Update settings handler signatures - websocket.go: Update WebSocket handler signatures All handlers now properly implement Echo v5's pointer-based context pattern. This change is necessary for type safety and compatibility with Echo v5's improved context handling and WebSocket support.
231 lines
6.2 KiB
Go
231 lines
6.2 KiB
Go
package handlers
|
|
|
|
import (
|
|
"bookhoard/internal/database"
|
|
"bookhoard/internal/middleware"
|
|
"bookhoard/internal/sync"
|
|
"context"
|
|
"log"
|
|
"net/http"
|
|
"time"
|
|
|
|
jwt "github.com/golang-jwt/jwt/v5"
|
|
"github.com/google/uuid"
|
|
"github.com/gorilla/websocket"
|
|
"github.com/jackc/pgx/v5/pgtype"
|
|
"github.com/labstack/echo/v5"
|
|
)
|
|
|
|
var upgrader = websocket.Upgrader{
|
|
ReadBufferSize: 1024,
|
|
WriteBufferSize: 1024,
|
|
CheckOrigin: func(r *http.Request) bool {
|
|
return true
|
|
},
|
|
}
|
|
|
|
type WSHandler struct {
|
|
db *database.Queries
|
|
connManager *sync.ConnectionManager
|
|
jwtSecret string
|
|
deviceAuthMiddleware *middleware.DeviceAuthMiddleware
|
|
}
|
|
|
|
func NewWSHandler(db *database.Queries, connManager *sync.ConnectionManager, jwtSecret string, deviceAuth *middleware.DeviceAuthMiddleware) *WSHandler {
|
|
return &WSHandler{
|
|
db: db,
|
|
connManager: connManager,
|
|
jwtSecret: jwtSecret,
|
|
deviceAuthMiddleware: deviceAuth,
|
|
}
|
|
}
|
|
|
|
type WSMessage struct {
|
|
Type string `json:"type"`
|
|
Timestamp string `json:"timestamp"`
|
|
Data map[string]interface{} `json:"data"`
|
|
}
|
|
|
|
type ClientInfo struct {
|
|
UserID string
|
|
DeviceID string
|
|
DeviceType string
|
|
DeviceName string
|
|
IsDevice bool
|
|
}
|
|
|
|
func (h *WSHandler) HandleWebSocket(c *echo.Context) error {
|
|
token := c.QueryParam("token")
|
|
if token == "" {
|
|
return c.JSON(http.StatusUnauthorized, map[string]string{"error": "token required"})
|
|
}
|
|
|
|
clientInfo, err := h.authenticateClient(token, c.Request().Header.Get("Authorization"))
|
|
if err != nil {
|
|
return c.JSON(http.StatusUnauthorized, map[string]string{"error": err.Error()})
|
|
}
|
|
|
|
ws, err := upgrader.Upgrade(c.Response(), c.Request(), nil)
|
|
if err != nil {
|
|
log.Printf("WebSocket upgrade failed: %v", err)
|
|
return err
|
|
}
|
|
|
|
conn := h.createConnection(clientInfo)
|
|
h.connManager.AddConnection(conn)
|
|
|
|
go h.readPump(conn, ws, clientInfo)
|
|
go h.writePump(conn, ws, clientInfo)
|
|
|
|
return nil
|
|
}
|
|
|
|
func (h *WSHandler) authenticateClient(token string, authHeader string) (*ClientInfo, error) {
|
|
if authHeader != "" && len(authHeader) > 7 && authHeader[:7] == "Bearer " {
|
|
deviceToken := authHeader[7:]
|
|
device, err := h.deviceAuthMiddleware.ValidateDeviceToken(deviceToken)
|
|
if err == nil {
|
|
userID := uuid.UUID(device.UserID.Bytes).String()
|
|
return &ClientInfo{
|
|
UserID: userID,
|
|
DeviceID: uuid.UUID(device.ID.Bytes).String(),
|
|
DeviceType: device.DeviceType,
|
|
DeviceName: device.DeviceName,
|
|
IsDevice: true,
|
|
}, nil
|
|
}
|
|
}
|
|
|
|
parsedToken, err := jwt.Parse(token, func(token *jwt.Token) (interface{}, error) {
|
|
return []byte(h.jwtSecret), nil
|
|
})
|
|
if err == nil && parsedToken.Valid {
|
|
claims := parsedToken.Claims.(jwt.MapClaims)
|
|
userID := claims["user_id"].(string)
|
|
return &ClientInfo{
|
|
UserID: userID,
|
|
DeviceID: "web-" + userID,
|
|
DeviceType: "web",
|
|
DeviceName: "Web Client",
|
|
IsDevice: false,
|
|
}, nil
|
|
}
|
|
|
|
return nil, echo.NewHTTPError(http.StatusUnauthorized, "invalid token")
|
|
}
|
|
|
|
func (h *WSHandler) createConnection(clientInfo *ClientInfo) *sync.DeviceConnection {
|
|
return &sync.DeviceConnection{
|
|
ID: clientInfo.DeviceID,
|
|
UserID: clientInfo.UserID,
|
|
DeviceType: clientInfo.DeviceType,
|
|
DeviceName: clientInfo.DeviceName,
|
|
Connected: time.Now(),
|
|
LastPing: time.Now(),
|
|
Send: make(chan sync.BroadcastMessage, 100),
|
|
Disconnected: make(chan struct{}),
|
|
}
|
|
}
|
|
|
|
func (h *WSHandler) readPump(conn *sync.DeviceConnection, ws *websocket.Conn, clientInfo *ClientInfo) {
|
|
defer func() {
|
|
h.connManager.RemoveConnection(conn.ID)
|
|
ws.Close()
|
|
}()
|
|
|
|
ws.SetReadDeadline(time.Now().Add(90 * time.Second))
|
|
ws.SetPongHandler(func(string) error {
|
|
ws.SetReadDeadline(time.Now().Add(90 * time.Second))
|
|
conn.LastPing = time.Now()
|
|
return nil
|
|
})
|
|
|
|
for {
|
|
_, message, err := ws.ReadMessage()
|
|
if err != nil {
|
|
if websocket.IsUnexpectedCloseError(err, websocket.CloseGoingAway, websocket.CloseAbnormalClosure) {
|
|
log.Printf("WebSocket error for %s: %v", conn.DeviceName, err)
|
|
}
|
|
break
|
|
}
|
|
|
|
h.handleMessage(message, conn, clientInfo)
|
|
}
|
|
}
|
|
|
|
func (h *WSHandler) writePump(conn *sync.DeviceConnection, ws *websocket.Conn, clientInfo *ClientInfo) {
|
|
ticker := time.NewTicker(30 * time.Second)
|
|
defer func() {
|
|
ticker.Stop()
|
|
ws.Close()
|
|
}()
|
|
|
|
initialState := h.getInitialState(clientInfo.UserID)
|
|
conn.Send <- sync.BroadcastMessage{
|
|
Type: sync.MessageTypeInitial,
|
|
Timestamp: time.Now().Format(time.RFC3339),
|
|
Data: initialState,
|
|
}
|
|
|
|
for {
|
|
select {
|
|
case msg, ok := <-conn.Send:
|
|
if !ok {
|
|
return
|
|
}
|
|
ws.SetWriteDeadline(time.Now().Add(10 * time.Second))
|
|
if err := ws.WriteJSON(msg); err != nil {
|
|
log.Printf("WebSocket write error for %s: %v", conn.DeviceName, err)
|
|
return
|
|
}
|
|
case <-conn.Disconnected:
|
|
return
|
|
case <-ticker.C:
|
|
ws.SetWriteDeadline(time.Now().Add(10 * time.Second))
|
|
if err := ws.WriteMessage(websocket.PingMessage, nil); err != nil {
|
|
return
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func (h *WSHandler) handleMessage(message []byte, conn *sync.DeviceConnection, clientInfo *ClientInfo) {
|
|
log.Printf("WebSocket message from %s: %s", conn.DeviceName, string(message))
|
|
}
|
|
|
|
func (h *WSHandler) getInitialState(userID string) map[string]interface{} {
|
|
userUUID, err := uuid.Parse(userID)
|
|
if err != nil {
|
|
return map[string]interface{}{"error": "invalid user ID"}
|
|
}
|
|
|
|
pgUserID := pgtype.UUID{Bytes: userUUID, Valid: true}
|
|
|
|
mediaItems, err := h.db.GetUserMediaItemsForSync(context.Background(), pgUserID)
|
|
if err != nil {
|
|
return map[string]interface{}{"error": "failed to fetch initial state"}
|
|
}
|
|
|
|
progressMap := make(map[string]map[string]interface{})
|
|
for _, item := range mediaItems {
|
|
progress, err := h.db.GetUniversalProgress(context.Background(), database.GetUniversalProgressParams{
|
|
MediaItemID: item.ID,
|
|
UserID: pgUserID,
|
|
})
|
|
if err == nil {
|
|
bookID := uuid.UUID(item.ID.Bytes).String()
|
|
progressMap[bookID] = map[string]interface{}{
|
|
"percentage": progress.Percentage.Float64,
|
|
"current_page": progress.CurrentPage.Int32,
|
|
"total_pages": progress.TotalPages.Int32,
|
|
"last_read": progress.LastReadAt.Time,
|
|
}
|
|
}
|
|
}
|
|
|
|
return map[string]interface{}{
|
|
"progress": progressMap,
|
|
"devices": h.connManager.GetConnectionStats(),
|
|
}
|
|
}
|