| |
| package main |
|
|
| import ( |
| "encoding/json" |
| "log" |
| "net/http" |
| "path/filepath" |
| "strings" |
| "time" |
|
|
| "github.com/gin-gonic/gin" |
| "github.com/google/uuid" |
| "github.com/gorilla/websocket" |
| "github.com/golang-jwt/jwt/v5" |
| ) |
|
|
| var upgrader = websocket.Upgrader{CheckOrigin: func(r *http.Request) bool { return true }} |
|
|
| |
| func getClientIP(c *gin.Context) string { |
| ip := c.ClientIP() |
| |
| |
| return ip |
| } |
|
|
| |
| func HandleRegister(c *gin.Context) { |
| var req struct { |
| Username string `json:"username" binding:"required"` |
| Email string `json:"email" binding:"required,email"` |
| Password string `json:"password" binding:"required,min=6"` |
| } |
| if c.BindJSON(&req) != nil { return } |
| if _, err := store.GetUserByEmail(req.Email); err == nil { c.JSON(400, gin.H{"error": "Email déjà utilisé"}); return } |
| if _, err := store.GetUserByUsername(req.Username); err == nil { c.JSON(400, gin.H{"error": "Pseudo déjà pris"}); return } |
| |
| hash, _ := HashPassword(req.Password) |
| u := &User{ID: uuid.New().String(), Username: req.Username, Email: req.Email, Password: hash, CreatedAt: time.Now()} |
| store.SaveUser(u) |
| token, _ := GenerateToken(u.ID) |
| SetTokenCookie(c, token) |
| |
| ip := getClientIP(c) |
| LogAccountCreated(u.Username, u.Email, ip) |
| c.JSON(200, gin.H{"user": u.Sanitize()}) |
| } |
|
|
| func HandleLogin(c *gin.Context) { |
| var req struct { Email, Password string } |
| if c.BindJSON(&req) != nil { return } |
| u, err := store.GetUserByEmail(req.Email) |
| if err != nil || !CheckPassword(u.Password, req.Password) { c.JSON(401, gin.H{"error": "Identifiants invalides"}); return } |
| token, _ := GenerateToken(u.ID) |
| SetTokenCookie(c, token) |
| |
| ip := getClientIP(c) |
| LogUserConnected(u.Username, ip) |
| c.JSON(200, gin.H{"user": u.Sanitize()}) |
| } |
|
|
| func HandleLogout(c *gin.Context) { ClearTokenCookie(c); c.JSON(200, gin.H{"ok": true}) } |
| func HandleMe(c *gin.Context) { uid := c.GetString("userID"); u, _ := store.GetUserByID(uid); c.JSON(200, u.Sanitize()) } |
| func (u *User) Sanitize() *User { cp := *u; cp.Password = ""; return &cp } |
|
|
| |
| func HandleListQuizzes(c *gin.Context) { uid := c.GetString("userID"); list, _ := store.ListQuizzesByUser(uid); c.JSON(200, list) } |
| func HandleCreateQuiz(c *gin.Context) { uid := c.GetString("userID"); q := &Quiz{ID: uuid.New().String(), Title: "Nouveau Quiz", CreatorID: uid, CreatedAt: time.Now(), UpdatedAt: time.Now(), Questions: []Question{}}; store.SaveQuiz(q); c.JSON(200, q) } |
| func HandleGetQuiz(c *gin.Context) { id := c.Param("id"); q, err := store.GetQuiz(id); if err != nil { c.JSON(404, gin.H{"error": "Quiz introuvable"}); return }; uid := c.GetString("userID"); if q.CreatorID != uid && !q.IsPublic { c.JSON(403, gin.H{"error": "Accès refusé"}); return }; c.JSON(200, q) } |
| func HandleUpdateQuiz(c *gin.Context) { uid := c.GetString("userID"); id := c.Param("id"); q, err := store.GetQuiz(id); if err != nil || q.CreatorID != uid { c.JSON(403, gin.H{"error": "Accès refusé"}); return }; if c.BindJSON(q) != nil { return }; store.SaveQuiz(q); c.JSON(200, q) } |
| func HandleDeleteQuiz(c *gin.Context) { uid := c.GetString("userID"); id := c.Param("id"); q, err := store.GetQuiz(id); if err != nil || q.CreatorID != uid { c.JSON(403, gin.H{"error": "Accès refusé"}); return }; store.DeleteQuiz(id); Alert("Quiz DELETED: %s by %s", id, uid); c.JSON(200, gin.H{"ok": true}) } |
| func HandlePublishQuiz(c *gin.Context) { uid := c.GetString("userID"); id := c.Param("id"); q, err := store.GetQuiz(id); if err != nil || q.CreatorID != uid { c.JSON(403, gin.H{"error": "Accès refusé"}); return }; q.IsPublic = !q.IsPublic; store.SaveQuiz(q); Info("Quiz Publish toggled: %s -> Public: %v", id, q.IsPublic); c.JSON(200, q) } |
| func HandleListPublicQuizzes(c *gin.Context) { list, _ := store.ListPublicQuizzes(20); c.JSON(200, list) } |
|
|
| |
| func HandleUploadImage(c *gin.Context) { |
| file, err := c.FormFile("file") |
| if err != nil { c.JSON(400, gin.H{"error": "Fichier manquant"}); return } |
| if file.Size > 5<<20 { c.JSON(400, gin.H{"error": "Max 5MB"}); return } |
| ext := strings.ToLower(filepath.Ext(file.Filename)) |
| allowed := map[string]bool{".png": true, ".jpg": true, ".jpeg": true, ".gif": true, ".webp": true} |
| if !allowed[ext] { c.JSON(400, gin.H{"error": "Format non supporté (png, jpg, gif, webp)"}); return } |
| filename := uuid.New().String() + ext |
| savePath := "/data/uploads/" + filename |
| if err := c.SaveUploadedFile(file, savePath); err != nil { LogError("Upload save failed", err); c.JSON(500, gin.H{"error": "Échec sauvegarde"}); return } |
| Info("Image Uploaded: %s by %s", filename, c.GetString("userID")) |
| c.JSON(200, gin.H{"url": "/uploads/" + filename}) |
| } |
|
|
| |
| func HandleCreateGame(c *gin.Context) { |
| uid := c.GetString("userID") |
| var req struct { QuizID string `json:"quiz_id" binding:"required"`; MaxPlayers int `json:"max_players"` } |
| if c.BindJSON(&req) != nil { return } |
| if req.MaxPlayers == 0 { req.MaxPlayers = 50 } |
| if req.MaxPlayers > 200 { req.MaxPlayers = 200 } |
| quiz, err := store.GetQuiz(req.QuizID) |
| if err != nil || (quiz.CreatorID != uid && !quiz.IsPublic) { c.JSON(403, gin.H{"error": "Quiz introuvable ou privé"}); return } |
| pin := GeneratePin() |
| for i := 0; i < 5; i++ { if _, err := store.GetGame(pin); err != nil { break }; pin = GeneratePin() } |
| game := &GameSession{Pin: pin, QuizID: quiz.ID, HostID: uid, State: StateWaiting, Players: make(map[string]*Player), Settings: GameSettings{MaxPlayers: req.MaxPlayers}, CreatedAt: time.Now()} |
| hostUser, _ := store.GetUserByID(uid) |
| game.Players[uid] = &Player{ID: uid, Name: hostUser.Username, Avatar: hostUser.AvatarURL, IsHost: true, Connected: true} |
| store.SaveGame(game) |
| Info("Game CREATED: Pin=%s Quiz=%s Host=%s MaxPlayers=%d", pin, quiz.ID, uid, req.MaxPlayers) |
| c.JSON(200, gin.H{"pin": pin, "game": game}) |
| } |
|
|
| func HandleGetGameInfo(c *gin.Context) { pin := c.Param("pin"); game, err := store.GetGame(pin); if err != nil { c.JSON(404, gin.H{"error": "Partie introuvable"}); return }; c.JSON(200, game) } |
|
|
| func HandleHostStartGame(c *gin.Context) { |
| uid := c.GetString("userID") |
| pin := c.Param("pin") |
| game, err := store.GetGame(pin) |
| if err != nil || game.HostID != uid { c.JSON(403, gin.H{"error": "Non autorisé"}); return } |
| |
| granted, notifyCh := slotManager.RequestSlots(pin, uid, game.Settings.MaxPlayers) |
|
|
| if granted { |
| game.State = StateLobby |
| store.SaveGame(game) |
| hub.BroadcastToRoom(pin, WSMessage{Type: MsgLobbyUpdate, Data: game.GetLobbyPlayers()}, "") |
| LogGameStart(pin, uid) |
| c.JSON(200, gin.H{"status": "started", "state": "lobby"}) |
| } else { |
| game.State = StateWaiting |
| store.SaveGame(game) |
| Alert("Game QUEUED: %s (Host: %s) waiting for %d slots", pin, uid, game.Settings.MaxPlayers) |
| select { |
| case <-notifyCh: |
| game.State = StateLobby; store.SaveGame(game) |
| hub.BroadcastToRoom(pin, WSMessage{Type: MsgLobbyUpdate, Data: game.GetLobbyPlayers()}, "") |
| LogGameStart(pin, uid) |
| c.JSON(200, gin.H{"status": "started", "state": "lobby"}) |
| case <-time.After(5 * time.Minute): |
| Error("Game Queue TIMEOUT: %s", pin) |
| c.JSON(408, gin.H{"error": "Timeout file d'attente"}) |
| } |
| } |
| } |
|
|
| func (g *GameSession) GetLobbyPlayers() []*Player { |
| list := make([]*Player, 0, len(g.Players)) |
| for _, p := range g.Players { list = append(list, p) } |
| return list |
| } |
|
|
| |
| func HandleWebSocket(c *gin.Context) { |
| pin := c.Param("pin") |
| conn, err := upgrader.Upgrade(c.Writer, c.Request, nil) |
| if err != nil { return } |
|
|
| tokenStr, _ := c.Cookie("token") |
| var userID string |
| if tokenStr != "" { |
| claims := &Claims{} |
| token, err := jwt.ParseWithClaims(tokenStr, claims, func(t *jwt.Token) (interface{}, error) { return jwtSecret, nil }) |
| if err == nil && token.Valid { userID = claims.UserID } |
| } |
|
|
| playerID := userID |
| isHost := false |
| if playerID == "" { playerID = "guest_" + uuid.New().String()[:8] } |
| game, _ := store.GetGame(pin) |
| if game != nil && game.HostID == playerID { isHost = true } |
|
|
| client := &Client{Conn: conn, Send: make(chan []byte, 256), GamePin: pin, PlayerID: playerID, IsHost: isHost} |
| hub.Register <- client |
|
|
| hub.Mutex.RLock() |
| room := hub.Rooms[pin] |
| hub.Mutex.RUnlock() |
| if room != nil && room.Game == nil && game != nil { |
| room.Mutex.Lock(); room.Game = game; room.Mutex.Unlock() |
| } |
|
|
| |
| go func() { |
| ticker := time.NewTicker(30 * time.Second) |
| defer func() { ticker.Stop(); hub.Unregister <- client }() |
| for { |
| select { |
| case msg, ok := <-client.Send: |
| if !ok { conn.WriteMessage(websocket.CloseMessage, []byte{}); return } |
| conn.SetWriteDeadline(time.Now().Add(10 * time.Second)) |
| if err := conn.WriteMessage(websocket.TextMessage, msg); err != nil { return } |
| case <-ticker.C: |
| conn.SetWriteDeadline(time.Now().Add(10 * time.Second)) |
| if err := conn.WriteMessage(websocket.PingMessage, nil); err != nil { return } |
| } |
| } |
| }() |
|
|
| |
| for { |
| _, data, err := conn.ReadMessage() |
| if err != nil { break } |
| HandleWSMessage(client, data) |
| } |
| } |
|
|
| func HandleWSMessage(client *Client, data []byte) { |
| var msg WSMessage |
| if err := json.Unmarshal(data, &msg); err != nil { return } |
|
|
| hub.Mutex.RLock() |
| room := hub.Rooms[client.GamePin] |
| hub.Mutex.RUnlock() |
| if room == nil || room.Game == nil { return } |
| game := room.Game |
|
|
| switch msg.Type { |
| case MsgJoin: |
| var payload struct { Name, Avatar string } |
| if m, ok := msg.Data.(map[string]interface{}); ok { |
| if v, ok := m["name"].(string); ok { payload.Name = v } |
| if v, ok := m["avatar"].(string); ok { payload.Avatar = v } |
| } |
| if payload.Name == "" { payload.Name = "Player " + client.PlayerID[:4] } |
|
|
| room.Mutex.Lock() |
| if _, exists := room.Clients[client.PlayerID]; !exists || !room.Clients[client.PlayerID].Connected { |
| p := &Player{ID: client.PlayerID, Name: payload.Name, Avatar: payload.Avatar, Connected: true} |
| game.Players[client.PlayerID] = p |
| room.Clients[client.PlayerID] = client |
| } else { |
| game.Players[client.PlayerID].Connected = true |
| client.Connected = true |
| } |
| room.Mutex.Unlock() |
| store.SaveGame(game) |
| |
| |
| ip := client.Conn.RemoteAddr().String() |
| if strings.HasPrefix(client.PlayerID, "guest_") { |
| LogAnonymousJoin(client.GamePin, ip) |
| } else { |
| LogPlayerJoin(payload.Name, client.GamePin) |
| } |
|
|
| hub.BroadcastToRoom(client.GamePin, WSMessage{Type: MsgPlayerJoined, Data: game.Players[client.PlayerID]}, client.PlayerID) |
| if game.State == StateLobby { hub.BroadcastToRoom(client.GamePin, WSMessage{Type: MsgLobbyUpdate, Data: game.GetLobbyPlayers()}, "") } |
|
|
| case MsgAnswer: |
| if game.State != StateQuestion { return } |
| var payload struct { QIndex int; Answer interface{} } |
| if m, ok := msg.Data.(map[string]interface{}); ok { |
| if v, ok := m["q_index"].(float64); ok { payload.QIndex = int(v) } |
| payload.Answer = m["answer"] |
| } |
| if payload.QIndex != game.CurrentQIndex { return } |
| room.Mutex.RLock(); p := game.Players[client.PlayerID]; room.Mutex.RUnlock() |
| if p == nil { return } |
| elapsed := time.Since(game.QuestionStartTime).Milliseconds() |
| p.Answer = &PlayerAnswer{QuestionIndex: payload.QIndex, Answer: payload.Answer, TimeTaken: int(elapsed)} |
| store.SaveGame(game) |
|
|
| case MsgStartGame: |
| if !client.IsHost || game.State != StateLobby { return } |
| go StartGameLoop(hub, store, client.GamePin) |
|
|
| case MsgNextQuestion: |
| if !client.IsHost || game.State != StateAnswer { return } |
| game.CurrentQIndex++ |
| NextQuestion(hub, store, client.GamePin) |
| } |
| } |