Files
2026-02-28 04:21:27 +00:00

165 lines
3.7 KiB
Go

package handlers
import (
"encoding/json"
"log"
"os"
"sync"
"github.com/gofiber/websocket/v2"
"github.com/golang-jwt/jwt/v5"
)
type wsMessage struct {
Type string `json:"type"`
Payload json.RawMessage `json:"payload,omitempty"`
UserID string `json:"user_id,omitempty"`
Name string `json:"name,omitempty"`
X float64 `json:"x,omitempty"`
Y float64 `json:"y,omitempty"`
}
type wsClient struct {
conn *websocket.Conn
userID string
name string
}
type whiteboardRoom struct {
clients map[*websocket.Conn]*wsClient
mu sync.RWMutex
}
var wsRooms = struct {
m map[string]*whiteboardRoom
mu sync.RWMutex
}{m: make(map[string]*whiteboardRoom)}
func getRoom(boardID string) *whiteboardRoom {
wsRooms.mu.Lock()
defer wsRooms.mu.Unlock()
if room, ok := wsRooms.m[boardID]; ok {
return room
}
room := &whiteboardRoom{clients: make(map[*websocket.Conn]*wsClient)}
wsRooms.m[boardID] = room
return room
}
func (r *whiteboardRoom) broadcast(sender *websocket.Conn, msg []byte) {
r.mu.RLock()
defer r.mu.RUnlock()
for conn := range r.clients {
if conn != sender {
_ = conn.WriteMessage(websocket.TextMessage, msg)
}
}
}
func (r *whiteboardRoom) userList() []map[string]string {
r.mu.RLock()
defer r.mu.RUnlock()
list := make([]map[string]string, 0, len(r.clients))
for _, c := range r.clients {
list = append(list, map[string]string{"user_id": c.userID, "name": c.name})
}
return list
}
func parseWSToken(tokenStr string) (userID string, email string, ok bool) {
secret := os.Getenv("JWT_SECRET")
if secret == "" {
secret = "changeme-jwt-secret"
}
type claims struct {
UserID string `json:"user_id"`
Email string `json:"email"`
jwt.RegisteredClaims
}
c := &claims{}
token, err := jwt.ParseWithClaims(tokenStr, c, func(t *jwt.Token) (interface{}, error) {
if _, ok := t.Method.(*jwt.SigningMethodHMAC); !ok {
return nil, jwt.ErrSignatureInvalid
}
return []byte(secret), nil
})
if err != nil || !token.Valid {
return "", "", false
}
return c.UserID, c.Email, true
}
func WhiteboardWS(c *websocket.Conn) {
boardID := c.Params("id")
tokenStr := c.Query("token", "")
userName := c.Query("name", "Anonymous")
userID, _, ok := parseWSToken(tokenStr)
if !ok {
_ = c.WriteJSON(map[string]string{"type": "error", "payload": "unauthorized"})
_ = c.Close()
return
}
room := getRoom(boardID)
client := &wsClient{conn: c, userID: userID, name: userName}
room.mu.Lock()
room.clients[c] = client
room.mu.Unlock()
joinMsg, _ := json.Marshal(map[string]interface{}{
"type": "join",
"user_id": userID,
"name": userName,
"users": room.userList(),
})
room.broadcast(nil, joinMsg)
selfMsg, _ := json.Marshal(map[string]interface{}{
"type": "users",
"users": room.userList(),
})
_ = c.WriteMessage(websocket.TextMessage, selfMsg)
defer func() {
room.mu.Lock()
delete(room.clients, c)
empty := len(room.clients) == 0
room.mu.Unlock()
leaveMsg, _ := json.Marshal(map[string]interface{}{
"type": "leave",
"user_id": userID,
"name": userName,
"users": room.userList(),
})
room.broadcast(nil, leaveMsg)
if empty {
wsRooms.mu.Lock()
delete(wsRooms.m, boardID)
wsRooms.mu.Unlock()
}
}()
for {
_, msg, err := c.ReadMessage()
if err != nil {
if !websocket.IsCloseError(err, websocket.CloseNormalClosure, websocket.CloseGoingAway, websocket.CloseNoStatusReceived) {
log.Printf("WS error board=%s user=%s: %v", boardID, userID, err)
}
break
}
var incoming map[string]interface{}
if json.Unmarshal(msg, &incoming) == nil {
incoming["user_id"] = userID
incoming["name"] = userName
outMsg, _ := json.Marshal(incoming)
room.broadcast(c, outMsg)
}
}
}