165 lines
3.7 KiB
Go
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)
|
|
}
|
|
}
|
|
}
|