diff --git a/models/server.go b/models/server.go index 0c55b05..0409db5 100644 --- a/models/server.go +++ b/models/server.go @@ -8,7 +8,8 @@ import ( type Server struct { ID uuid.UUID `gorm:"primaryKey" json:"id"` - Password string `gorm:"not null" json:"-"` + Name string `gorm:"not null" json:"name"` + Password *string `gorm:"" json:"-"` CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"` UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at"` } diff --git a/network/http/web/api/category.go b/network/http/web/api/category.go index 939b534..8584557 100644 --- a/network/http/web/api/category.go +++ b/network/http/web/api/category.go @@ -1,10 +1,13 @@ package api import ( + "errors" "go_oxspeak_server/models" "go_oxspeak_server/network/http/handler" + "net/http" "github.com/gin-gonic/gin" + "gorm.io/gorm" ) type CategoryHandler struct { @@ -26,21 +29,96 @@ func (h *CategoryHandler) RegisterRoutes(rg *gin.RouterGroup) { func (h *CategoryHandler) getCategories(c *gin.Context) { var categories []models.Category - h.DB.Find(&categories) + result := h.DB.Find(&categories) + if result.Error != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": result.Error.Error()}) + return + } + c.JSON(http.StatusOK, categories) } func (h *CategoryHandler) getCategory(c *gin.Context) { + id := c.Param("id") + var category models.Category - h.DB.Find(&category) + result := h.DB.First(&category, "id = ?", id) + if result.Error != nil { + if errors.Is(result.Error, gorm.ErrRecordNotFound) { + c.JSON(http.StatusNotFound, gin.H{"error": "Category not found"}) + return + } + c.JSON(http.StatusInternalServerError, gin.H{"error": result.Error.Error()}) + return + } + c.JSON(http.StatusOK, category) } func (h *CategoryHandler) addCategory(c *gin.Context) { + var req CreateCategoryRequest + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + category := models.Category{ + ServerID: req.ServerID, + Name: req.Name, + } + + if err := h.DB.Create(&category).Error; err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + + c.JSON(http.StatusCreated, category) } func (h *CategoryHandler) updateCategory(c *gin.Context) { + id := c.Param("id") + var category models.Category + result := h.DB.First(&category, "id = ?", id) + if result.Error != nil { + if errors.Is(result.Error, gorm.ErrRecordNotFound) { + c.JSON(http.StatusNotFound, gin.H{"error": "Category not found"}) + return + } + c.JSON(http.StatusInternalServerError, gin.H{"error": result.Error.Error()}) + return + } + + var req UpdateCategoryRequest + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + if err := h.DB.Model(&category).Updates(req).Error; err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to update category"}) + return + } + + c.JSON(http.StatusOK, category) } func (h *CategoryHandler) deleteCategory(c *gin.Context) { + id := c.Param("id") + + var category models.Category + result := h.DB.First(&category, "id = ?", id) + if result.Error != nil { + if errors.Is(result.Error, gorm.ErrRecordNotFound) { + c.JSON(http.StatusNotFound, gin.H{"error": "Category not found"}) + return + } + c.JSON(http.StatusInternalServerError, gin.H{"error": result.Error.Error()}) + return + } + + if err := h.DB.Delete(&category).Error; err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to delete category"}) + return + } + + c.Status(http.StatusNoContent) } diff --git a/network/http/web/api/category_dto.go b/network/http/web/api/category_dto.go new file mode 100644 index 0000000..5aab897 --- /dev/null +++ b/network/http/web/api/category_dto.go @@ -0,0 +1,14 @@ +package api + +import "github.com/google/uuid" + +// DTOs pour Category + +type CreateCategoryRequest struct { + ServerID uuid.UUID `json:"server_id" binding:"required"` + Name string `json:"name" binding:"required"` +} + +type UpdateCategoryRequest struct { + Name string `json:"name" binding:"required"` +} diff --git a/network/http/web/api/channel.go b/network/http/web/api/channel.go index eaa5dd7..59a35fe 100644 --- a/network/http/web/api/channel.go +++ b/network/http/web/api/channel.go @@ -1,10 +1,13 @@ package api import ( + "errors" "go_oxspeak_server/models" "go_oxspeak_server/network/http/handler" + "net/http" "github.com/gin-gonic/gin" + "gorm.io/gorm" ) type ChannelHandler struct { @@ -26,26 +29,102 @@ func (h *ChannelHandler) RegisterRoutes(rg *gin.RouterGroup) { } func (h *ChannelHandler) getChannels(c *gin.Context) { - var users []models.User - h.DB.Find(&users) + var channels []models.Channel + result := h.DB.Find(&channels) + if result.Error != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": result.Error.Error()}) + return + } + c.JSON(http.StatusOK, channels) } func (h *ChannelHandler) getChannel(c *gin.Context) { - var user models.User - h.DB.Find(&user) + id := c.Param("id") + var channel models.Channel + result := h.DB.First(&channel, "id = ?", id) + if result.Error != nil { + if errors.Is(result.Error, gorm.ErrRecordNotFound) { + c.JSON(http.StatusNotFound, gin.H{"error": "Channel not found"}) + return + } + c.JSON(http.StatusInternalServerError, gin.H{"error": result.Error.Error()}) + return + } + c.JSON(http.StatusOK, channel) } func (h *ChannelHandler) addChannel(c *gin.Context) { + var req CreateChannelRequest + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + channel := models.Channel{ + ServerID: req.ServerID, + CategoryID: req.CategoryID, + Type: req.Type, + Name: req.Name, + } + if req.Position != nil { + channel.Position = *req.Position + } + + if err := h.DB.Create(&channel).Error; err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + + c.JSON(http.StatusCreated, channel) } func (h *ChannelHandler) updateChannel(c *gin.Context) { - var user models.User - h.DB.Find(&user) + id := c.Param("id") + + var channel models.Channel + result := h.DB.First(&channel, "id = ?", id) + if result.Error != nil { + if errors.Is(result.Error, gorm.ErrRecordNotFound) { + c.JSON(http.StatusNotFound, gin.H{"error": "Channel not found"}) + return + } + c.JSON(http.StatusInternalServerError, gin.H{"error": result.Error.Error()}) + return + } + + var req UpdateChannelRequest + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + if err := h.DB.Model(&channel).Updates(req).Error; err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to update channel"}) + return + } + + c.JSON(http.StatusOK, channel) } func (h *ChannelHandler) deleteChannel(c *gin.Context) { - var user models.User - h.DB.Find(&user) + id := c.Param("id") + + var channel models.Channel + result := h.DB.First(&channel, "id = ?", id) + if result.Error != nil { + if errors.Is(result.Error, gorm.ErrRecordNotFound) { + c.JSON(http.StatusNotFound, gin.H{"error": "Channel not found"}) + return + } + c.JSON(http.StatusInternalServerError, gin.H{"error": result.Error.Error()}) + return + } + + if err := h.DB.Delete(&channel).Error; err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to delete channel"}) + return + } + + c.Status(http.StatusNoContent) } diff --git a/network/http/web/api/channel_dto.go b/network/http/web/api/channel_dto.go new file mode 100644 index 0000000..386f4da --- /dev/null +++ b/network/http/web/api/channel_dto.go @@ -0,0 +1,24 @@ +package api + +import ( + "github.com/google/uuid" + "go_oxspeak_server/models" +) + +// DTOs pour Channel + +type CreateChannelRequest struct { + ServerID *uuid.UUID `json:"server_id,omitempty"` + CategoryID *uuid.UUID `json:"category_id,omitempty"` + Position *int32 `json:"position,omitempty"` + Type models.ChannelType `json:"type" binding:"required"` + Name *string `json:"name,omitempty"` +} + +type UpdateChannelRequest struct { + ServerID *uuid.UUID `json:"server_id,omitempty"` + CategoryID *uuid.UUID `json:"category_id,omitempty"` + Position *int32 `json:"position,omitempty"` + Type *models.ChannelType `json:"type,omitempty"` + Name *string `json:"name,omitempty"` +} diff --git a/network/http/web/api/message.go b/network/http/web/api/message.go index cb53658..d7b93fc 100644 --- a/network/http/web/api/message.go +++ b/network/http/web/api/message.go @@ -1,10 +1,13 @@ package api import ( + "errors" "go_oxspeak_server/models" "go_oxspeak_server/network/http/handler" + "net/http" "github.com/gin-gonic/gin" + "gorm.io/gorm" ) type MessageHandler struct { @@ -26,21 +29,96 @@ func (h *MessageHandler) RegisterRoutes(rg *gin.RouterGroup) { func (h *MessageHandler) getMessages(c *gin.Context) { var messages []models.Message - h.DB.Find(&messages) + result := h.DB.Find(&messages) + if result.Error != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": result.Error.Error()}) + return + } + c.JSON(http.StatusOK, messages) } func (h *MessageHandler) getMessage(c *gin.Context) { + id := c.Param("id") var message models.Message - h.DB.Find(&message) + result := h.DB.First(&message, "id = ?", id) + if result.Error != nil { + if errors.Is(result.Error, gorm.ErrRecordNotFound) { + c.JSON(http.StatusNotFound, gin.H{"error": "Message not found"}) + return + } + c.JSON(http.StatusInternalServerError, gin.H{"error": result.Error.Error()}) + return + } + c.JSON(http.StatusOK, message) } func (h *MessageHandler) addMessage(c *gin.Context) { + var req CreateMessageRequest + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + message := models.Message{ + ChannelID: req.ChannelID, + UserID: req.UserID, + Content: req.Content, + ReplyToID: req.ReplyToID, + } + + if err := h.DB.Create(&message).Error; err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + + c.JSON(http.StatusCreated, message) } func (h *MessageHandler) updateMessage(c *gin.Context) { + id := c.Param("id") + var message models.Message + result := h.DB.First(&message, "id = ?", id) + if result.Error != nil { + if errors.Is(result.Error, gorm.ErrRecordNotFound) { + c.JSON(http.StatusNotFound, gin.H{"error": "Message not found"}) + return + } + c.JSON(http.StatusInternalServerError, gin.H{"error": result.Error.Error()}) + return + } + + var req UpdateMessageRequest + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + if err := h.DB.Model(&message).Updates(req).Error; err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to update message"}) + return + } + + c.JSON(http.StatusOK, message) } func (h *MessageHandler) deleteMessage(c *gin.Context) { + id := c.Param("id") + var message models.Message + result := h.DB.First(&message, "id = ?", id) + if result.Error != nil { + if errors.Is(result.Error, gorm.ErrRecordNotFound) { + c.JSON(http.StatusNotFound, gin.H{"error": "Message not found"}) + return + } + c.JSON(http.StatusInternalServerError, gin.H{"error": result.Error.Error()}) + return + } + + if err := h.DB.Delete(&message).Error; err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to delete message"}) + return + } + + c.Status(http.StatusNoContent) } diff --git a/network/http/web/api/message_dto.go b/network/http/web/api/message_dto.go new file mode 100644 index 0000000..964899c --- /dev/null +++ b/network/http/web/api/message_dto.go @@ -0,0 +1,17 @@ +package api + +import "github.com/google/uuid" + +// DTOs pour Message + +type CreateMessageRequest struct { + ChannelID uuid.UUID `json:"channel_id" binding:"required"` + UserID uuid.UUID `json:"user_id" binding:"required"` + Content string `json:"content" binding:"required"` + ReplyToID *uuid.UUID `json:"reply_to_id,omitempty"` +} + +type UpdateMessageRequest struct { + Content string `json:"content" binding:"required"` + ReplyToID *uuid.UUID `json:"reply_to_id,omitempty"` +} diff --git a/network/http/web/api/server.go b/network/http/web/api/server.go index 2be02c5..5237256 100644 --- a/network/http/web/api/server.go +++ b/network/http/web/api/server.go @@ -1,10 +1,13 @@ package api import ( + "errors" "go_oxspeak_server/models" "go_oxspeak_server/network/http/handler" + "net/http" "github.com/gin-gonic/gin" + "gorm.io/gorm" ) type ServerHandler struct { @@ -17,30 +20,109 @@ func NewServerHandler(h *handler.Handler) *ServerHandler { func (h *ServerHandler) RegisterRoutes(rg *gin.RouterGroup) { server := rg.Group("/server") - server.GET("/", h.getServers) - server.GET("/:id/", h.getServer) - server.POST("/", h.addServer) - server.PUT("/:id/", h.updateServer) - server.DELETE("/:id/", h.deleteServer) + server.GET("/", h.serverList) + server.GET("/:id/", h.serverDetail) + server.POST("/", h.serverAdd) + server.PUT("/:id/", h.serverUpdate) + server.DELETE("/:id/", h.serverDelete) } -func (h *ServerHandler) getServers(c *gin.Context) { +func (h *ServerHandler) serverList(c *gin.Context) { var servers []models.Server - h.DB.Find(&servers) + result := h.DB.Find(&servers) + if result.Error != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": result.Error.Error()}) + return + } + + c.JSON(http.StatusOK, servers) } -func (h *ServerHandler) getServer(c *gin.Context) { +func (h *ServerHandler) serverDetail(c *gin.Context) { + id := c.Param("id") + var server models.Server - h.DB.Find(&server) + result := h.DB.First(&server, "id = ?", id) + if result.Error != nil { + if errors.Is(result.Error, gorm.ErrRecordNotFound) { + c.JSON(http.StatusNotFound, gin.H{"error": "Server not found"}) + return + } + c.JSON(http.StatusInternalServerError, gin.H{"error": result.Error.Error()}) + return + } + + c.JSON(http.StatusOK, server) } -func (h *ServerHandler) addServer(c *gin.Context) { +func (h *ServerHandler) serverAdd(c *gin.Context) { + var req CreateServerRequest + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + // construire le modèle + server := models.Server{ + Name: req.Name, + Password: req.Password, + } + + if err := h.DB.Create(&server).Error; err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + + c.JSON(http.StatusCreated, server) } -func (h *ServerHandler) updateServer(c *gin.Context) { +func (h *ServerHandler) serverUpdate(c *gin.Context) { + id := c.Param("id") + var server models.Server + result := h.DB.First(&server, "id = ?", id) + if result.Error != nil { + if errors.Is(result.Error, gorm.ErrRecordNotFound) { + c.JSON(http.StatusNotFound, gin.H{"error": "Server not found"}) + return + } + c.JSON(http.StatusInternalServerError, gin.H{"error": result.Error.Error()}) + return + } + + var req UpdateServerRequest + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + if err := h.DB.Model(&server).Updates(req).Error; err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to update server"}) + return + } + + c.JSON(http.StatusOK, server) } -func (h *ServerHandler) deleteServer(c *gin.Context) { +func (h *ServerHandler) serverDelete(c *gin.Context) { + id := c.Param("id") + + var server models.Server + result := h.DB.First(&server, "id = ?", id) + if result.Error != nil { + if errors.Is(result.Error, gorm.ErrRecordNotFound) { + c.JSON(http.StatusNotFound, gin.H{"error": "Server not found"}) + return + } + c.JSON(http.StatusInternalServerError, gin.H{"error": result.Error.Error()}) + return + } + + if err := h.DB.Delete(&server).Error; err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to delete server"}) + return + } + + c.Status(http.StatusNoContent) } diff --git a/network/http/web/api/server_dto.go b/network/http/web/api/server_dto.go new file mode 100644 index 0000000..6912668 --- /dev/null +++ b/network/http/web/api/server_dto.go @@ -0,0 +1,11 @@ +package api + +type CreateServerRequest struct { + Name string `json:"name" binding:"required"` + Password *string `json:"password,omitempty"` +} + +type UpdateServerRequest struct { + Name string `json:"name" binding:"required"` + Password *string `json:"password,omitempty"` +} diff --git a/network/udp/server.go b/network/udp/server.go index 9be38d0..c1f3a4d 100644 --- a/network/udp/server.go +++ b/network/udp/server.go @@ -54,8 +54,20 @@ func (s *Server) Run() error { _ = conn.SetWriteBuffer(8 * 1024 * 1024) } - fmt.Println("[udp] listening on", s.bindAddr, "with", len(s.conns), "worker(s)") + fmt.Println("[udp] listening on", s.bindAddr, "with", len(s.conns), "worker socket(s)") + // Cas Windows : listenUDP a créé une seule socket, + // on lance workerCount goroutines sur la même conn. + if len(s.conns) == 1 { + conn := s.conns[0] + for workerID := 0; workerID < workerCount; workerID++ { + s.wg.Add(1) + go s.workerLoop(workerID, conn) + } + return nil + } + + // Cas Unix : une conn par worker (SO_REUSEPORT). for workerID, conn := range s.conns { s.wg.Add(1) go s.workerLoop(workerID, conn) @@ -102,8 +114,7 @@ func (s *Server) workerLoop(workerID int, conn *net.UDPConn) { } } -// handlePacket reçoit maintenant directement la conn du worker. -// Aucun hop supplémentaire : le paquet reste dans la goroutine/conn du worker. +// handlePacket reçoit la conn du worker. func (s *Server) handlePacket(conn *net.UDPConn, data []byte, addr *net.UDPAddr) { if len(data) == 0 { return @@ -113,7 +124,6 @@ func (s *Server) handlePacket(conn *net.UDPConn, data []byte, addr *net.UDPAddr) switch pt { case PacketTypePing: - // exemple simple : echo du ping _, _ = conn.WriteToUDP([]byte{byte(PacketTypePing)}, addr) case PacketTypeConnect: