QuizZip / api.go
NathMen12's picture
Update api.go
26c5b19 verified
Raw
History Blame Contribute Delete
11.8 kB
// api.go
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 }}
// Helper pour récupérer IP réelle derrière proxy HF
func getClientIP(c *gin.Context) string {
ip := c.ClientIP()
// HF Spaces utilise un proxy, X-Forwarded-For est géré par c.ClientIP() si TrustedProxies configuré
// Mais on a fait r.SetTrustedProxies(nil), donc c.ClientIP() = RemoteAddr direct.
return ip
}
// --- Auth Handlers ---
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) // LOG PERSISTANT
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) // LOG PERSISTANT
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 }
// --- Quiz Handlers (Logs ajoutés sur actions critiques) ---
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) }
// --- Image Upload ---
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})
}
// --- Game Handlers ---
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) // LOG
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) // LOG quand démarre après queue
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
}
// --- WebSocket Handler ---
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()
}
// Write Pump
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 }
}
}
}()
// Read Pump
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)
// LOG JOIN
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)
}
}