feat: Enhance real-time collaboration features with user awareness and document sharing
- Added user information (UserID, UserName, UserAvatar) to Client struct for presence tracking. - Implemented failure handling in the broadcastMessage function to manage send failures and disconnect clients if necessary. - Introduced document ownership and sharing capabilities: - Added OwnerID and Is_Public fields to Document model. - Created DocumentShare model for managing document sharing with permissions. - Implemented functions for creating, listing, and managing document shares in the Postgres store. - Added user management functionality: - Created User model and associated functions for user management in the Postgres store. - Implemented session management with token hashing for security. - Updated database schema with migrations for users, sessions, and document shares. - Enhanced frontend Yjs integration with awareness event logging for user connections and disconnections.
This commit is contained in:
@@ -3,57 +3,147 @@ package handlers
|
||||
import (
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
|
||||
"github.com/M1ngdaXie/realtime-collab/internal/auth"
|
||||
"github.com/M1ngdaXie/realtime-collab/internal/hub"
|
||||
"github.com/M1ngdaXie/realtime-collab/internal/store"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/google/uuid"
|
||||
"github.com/gorilla/websocket"
|
||||
)
|
||||
|
||||
|
||||
var upgrader = websocket.Upgrader{
|
||||
ReadBufferSize: 1024,
|
||||
WriteBufferSize: 1024,
|
||||
CheckOrigin: func(r *http.Request) bool {
|
||||
// Allow all origins for development
|
||||
// TODO: Restrict in production
|
||||
return true
|
||||
},
|
||||
ReadBufferSize: 1024,
|
||||
WriteBufferSize: 1024,
|
||||
CheckOrigin: func(r *http.Request) bool {
|
||||
// Check origin against allowed origins from environment
|
||||
allowedOrigins := os.Getenv("ALLOWED_ORIGINS")
|
||||
if allowedOrigins == "" {
|
||||
// Default for development
|
||||
origin := r.Header.Get("Origin")
|
||||
return origin == "http://localhost:5173" || origin == "http://localhost:3000"
|
||||
}
|
||||
// Production: validate against ALLOWED_ORIGINS
|
||||
// TODO: Parse and validate origin
|
||||
return true
|
||||
},
|
||||
}
|
||||
|
||||
type WebSocketHandler struct {
|
||||
hub *hub.Hub
|
||||
}
|
||||
type WebSocketHandler struct {
|
||||
hub *hub.Hub
|
||||
store store.Store
|
||||
}
|
||||
|
||||
func NewWebSocketHandler(h *hub.Hub) *WebSocketHandler {
|
||||
return &WebSocketHandler{hub: h}
|
||||
}
|
||||
func NewWebSocketHandler(h *hub.Hub, s store.Store) *WebSocketHandler {
|
||||
return &WebSocketHandler{
|
||||
hub: h,
|
||||
store: s,
|
||||
}
|
||||
}
|
||||
|
||||
func (wsh *WebSocketHandler) HandleWebSocket(c *gin.Context){
|
||||
func (wsh *WebSocketHandler) HandleWebSocket(c *gin.Context) {
|
||||
roomID := c.Param("roomId")
|
||||
|
||||
if(roomID == ""){
|
||||
if roomID == "" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "roomId is required"})
|
||||
return
|
||||
}
|
||||
conn, err := upgrader.Upgrade(c.Writer, c.Request, nil)
|
||||
|
||||
// Parse document ID
|
||||
documentID, err := uuid.Parse(roomID)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to upgrade to WebSocket"})
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid document ID"})
|
||||
return
|
||||
}
|
||||
|
||||
// Create a new client
|
||||
clientID := uuid.New().String()
|
||||
client := hub.NewClient(clientID, conn, wsh.hub, roomID)
|
||||
|
||||
// Register client with hub
|
||||
wsh.hub.Register <- client
|
||||
// Try to authenticate via JWT token or share token
|
||||
var userID *uuid.UUID
|
||||
var userName string
|
||||
var userAvatar *string
|
||||
authenticated := false
|
||||
|
||||
// Start read and write pumps in separate goroutines
|
||||
go client.WritePump()
|
||||
go client.ReadPump()
|
||||
// Check for JWT token in query parameter
|
||||
jwtToken := c.Query("token")
|
||||
if jwtToken != "" {
|
||||
// Validate JWT and get user data from token claims (no DB query!)
|
||||
jwtSecret := os.Getenv("JWT_SECRET")
|
||||
if jwtSecret == "" {
|
||||
log.Println("JWT_SECRET not configured")
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Server configuration error"})
|
||||
return
|
||||
}
|
||||
|
||||
log.Printf("WebSocket connection established for client %s in room %s", clientID, roomID)
|
||||
}
|
||||
authMiddleware := auth.NewAuthMiddleware(wsh.store, jwtSecret)
|
||||
uid, name, avatar, err := authMiddleware.ValidateToken(jwtToken)
|
||||
if err == nil && uid != nil {
|
||||
// User data comes directly from JWT claims - no DB query needed!
|
||||
userID = uid
|
||||
userName = name
|
||||
if avatar != "" {
|
||||
userAvatar = &avatar
|
||||
}
|
||||
authenticated = true
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// If not authenticated via JWT, check for share token
|
||||
if !authenticated {
|
||||
shareToken := c.Query("share")
|
||||
if shareToken != "" {
|
||||
// Validate share token
|
||||
valid, err := wsh.store.ValidateShareToken(c.Request.Context(), documentID, shareToken)
|
||||
if err != nil {
|
||||
log.Printf("Error validating share token: %v", err)
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to validate share token"})
|
||||
return
|
||||
}
|
||||
if !valid {
|
||||
c.JSON(http.StatusForbidden, gin.H{"error": "Invalid or expired share token"})
|
||||
return
|
||||
}
|
||||
// Share token is valid, allow connection with anonymous user
|
||||
userName = "Anonymous"
|
||||
authenticated = true
|
||||
}
|
||||
}
|
||||
|
||||
// If still not authenticated, reject connection
|
||||
if !authenticated {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": "Authentication required. Provide 'token' or 'share' query parameter"})
|
||||
return
|
||||
}
|
||||
|
||||
// If authenticated with JWT, check document permissions
|
||||
if userID != nil {
|
||||
canView, err := wsh.store.CanViewDocument(c.Request.Context(), documentID, *userID)
|
||||
if err != nil {
|
||||
log.Printf("Error checking permissions: %v", err)
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to check permissions"})
|
||||
return
|
||||
}
|
||||
if !canView {
|
||||
c.JSON(http.StatusForbidden, gin.H{"error": "You don't have permission to access this document"})
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Upgrade connection
|
||||
conn, err := upgrader.Upgrade(c.Writer, c.Request, nil)
|
||||
if err != nil {
|
||||
log.Printf("Failed to upgrade connection: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Create client with user information
|
||||
clientID := uuid.New().String()
|
||||
client := hub.NewClient(clientID, userID, userName, userAvatar, conn, wsh.hub, roomID)
|
||||
|
||||
// Register client
|
||||
wsh.hub.Register <- client
|
||||
|
||||
// Start goroutines
|
||||
go client.WritePump()
|
||||
go client.ReadPump()
|
||||
|
||||
log.Printf("Client connected: %s (user: %s) to room: %s", clientID, userName, roomID)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user