2026-02-22 19:41:39 +00:00
|
|
|
package handlers
|
|
|
|
|
|
|
|
|
|
import (
|
2026-03-12 05:13:38 +00:00
|
|
|
"time"
|
|
|
|
|
|
2026-02-22 19:41:39 +00:00
|
|
|
"github.com/coder/websocket"
|
|
|
|
|
"github.com/gin-gonic/gin"
|
|
|
|
|
"go.uber.org/zap"
|
|
|
|
|
|
2026-03-06 18:13:16 +00:00
|
|
|
apperrors "veza-backend-api/internal/errors"
|
2026-02-22 19:41:39 +00:00
|
|
|
"veza-backend-api/internal/services"
|
|
|
|
|
chatws "veza-backend-api/internal/websocket/chat"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
type ChatWebSocketHandler struct {
|
|
|
|
|
chatService *services.ChatService
|
|
|
|
|
hub *chatws.Hub
|
|
|
|
|
handler *chatws.MessageHandler
|
|
|
|
|
logger *zap.Logger
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func NewChatWebSocketHandler(chatService *services.ChatService, hub *chatws.Hub, handler *chatws.MessageHandler, logger *zap.Logger) *ChatWebSocketHandler {
|
|
|
|
|
if logger == nil {
|
|
|
|
|
logger = zap.NewNop()
|
|
|
|
|
}
|
|
|
|
|
return &ChatWebSocketHandler{
|
|
|
|
|
chatService: chatService,
|
|
|
|
|
hub: hub,
|
|
|
|
|
handler: handler,
|
|
|
|
|
logger: logger,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (h *ChatWebSocketHandler) HandleWebSocket(c *gin.Context) {
|
|
|
|
|
tokenString := c.Query("token")
|
|
|
|
|
if tokenString == "" {
|
2026-03-06 18:13:16 +00:00
|
|
|
RespondWithAppError(c, apperrors.NewUnauthorizedError("missing token"))
|
2026-02-22 19:41:39 +00:00
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
claims, err := h.chatService.ValidateChatToken(tokenString)
|
|
|
|
|
if err != nil {
|
|
|
|
|
h.logger.Warn("Invalid chat token",
|
|
|
|
|
zap.Error(err))
|
2026-03-06 18:13:16 +00:00
|
|
|
RespondWithAppError(c, apperrors.NewUnauthorizedError("invalid or expired token"))
|
2026-02-22 19:41:39 +00:00
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
conn, err := websocket.Accept(c.Writer, c.Request, &websocket.AcceptOptions{
|
2026-03-13 23:44:46 +00:00
|
|
|
OriginPatterns: GetAllowedWebSocketOrigins(),
|
2026-02-22 19:41:39 +00:00
|
|
|
})
|
|
|
|
|
if err != nil {
|
|
|
|
|
h.logger.Error("Failed to accept WebSocket",
|
|
|
|
|
zap.Error(err),
|
|
|
|
|
zap.String("user_id", claims.UserID.String()))
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
client := chatws.NewClient(h.hub, conn, claims.UserID, claims.Username, h.handler, h.logger)
|
|
|
|
|
|
|
|
|
|
h.hub.Register(client)
|
|
|
|
|
|
|
|
|
|
client.SendJSON(chatws.NewActionConfirmedResponse("connected", true))
|
|
|
|
|
|
2026-03-06 18:13:16 +00:00
|
|
|
ctx := c.Request.Context()
|
2026-03-12 05:13:38 +00:00
|
|
|
|
|
|
|
|
// SECURITY(MEDIUM-010): Periodic token re-validation.
|
|
|
|
|
// Re-validate every 60s; close connection if token is expired or revoked.
|
|
|
|
|
go func() {
|
|
|
|
|
ticker := time.NewTicker(60 * time.Second)
|
|
|
|
|
defer ticker.Stop()
|
|
|
|
|
for {
|
|
|
|
|
select {
|
|
|
|
|
case <-ctx.Done():
|
|
|
|
|
return
|
|
|
|
|
case <-ticker.C:
|
|
|
|
|
if _, err := h.chatService.ValidateChatToken(tokenString); err != nil {
|
|
|
|
|
h.logger.Info("WebSocket token expired, closing connection",
|
|
|
|
|
zap.String("user_id", claims.UserID.String()))
|
|
|
|
|
conn.Close(websocket.StatusPolicyViolation, "token expired")
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}()
|
|
|
|
|
|
2026-02-22 19:41:39 +00:00
|
|
|
go client.WritePump(ctx)
|
|
|
|
|
go client.ReadPump(ctx)
|
|
|
|
|
}
|