Use sockets for server creation progress
This commit is contained in:
@@ -55,8 +55,8 @@ func InitializeControllers(c *dig.Container) {
|
||||
logging.Panic("unable to initialize membership controller")
|
||||
}
|
||||
|
||||
err = c.Invoke(NewSteam2FAController)
|
||||
err = c.Invoke(NewWebSocketController)
|
||||
if err != nil {
|
||||
logging.Panic("unable to initialize steam 2fa controller")
|
||||
logging.Panic("unable to initialize websocket controller")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -139,10 +139,16 @@ func (ac *ServerController) CreateServer(c *fiber.Ctx) error {
|
||||
if err := c.BodyParser(server); err != nil {
|
||||
return ac.errorHandler.HandleParsingError(c, err)
|
||||
}
|
||||
ac.service.GenerateServerPath(server)
|
||||
if err := ac.service.CreateServer(c, server); err != nil {
|
||||
|
||||
server.GenerateUUID()
|
||||
|
||||
// Use async server creation to avoid blocking other requests
|
||||
if err := ac.service.CreateServerAsync(c, server); err != nil {
|
||||
return ac.errorHandler.HandleServiceError(c, err)
|
||||
}
|
||||
|
||||
// Return immediately with server details
|
||||
// The actual creation will happen in the background with WebSocket updates
|
||||
return c.JSON(server)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,139 +0,0 @@
|
||||
package controller
|
||||
|
||||
import (
|
||||
"acc-server-manager/local/middleware"
|
||||
"acc-server-manager/local/model"
|
||||
"acc-server-manager/local/utl/common"
|
||||
"acc-server-manager/local/utl/error_handler"
|
||||
"acc-server-manager/local/utl/jwt"
|
||||
|
||||
"github.com/gofiber/fiber/v2"
|
||||
)
|
||||
|
||||
type Steam2FAController struct {
|
||||
tfaManager *model.Steam2FAManager
|
||||
errorHandler *error_handler.ControllerErrorHandler
|
||||
jwtHandler *jwt.OpenJWTHandler
|
||||
}
|
||||
|
||||
func NewSteam2FAController(tfaManager *model.Steam2FAManager, routeGroups *common.RouteGroups, auth *middleware.AuthMiddleware, jwtHandler *jwt.OpenJWTHandler) *Steam2FAController {
|
||||
controller := &Steam2FAController{
|
||||
tfaManager: tfaManager,
|
||||
errorHandler: error_handler.NewControllerErrorHandler(),
|
||||
jwtHandler: jwtHandler,
|
||||
}
|
||||
|
||||
steam2faRoutes := routeGroups.Steam2FA
|
||||
steam2faRoutes.Use(auth.AuthenticateOpen)
|
||||
|
||||
// Define routes
|
||||
steam2faRoutes.Get("/pending", auth.HasPermission(model.ServerView), controller.GetPendingRequests)
|
||||
steam2faRoutes.Get("/:id", auth.HasPermission(model.ServerView), controller.GetRequest)
|
||||
steam2faRoutes.Post("/:id/complete", auth.HasPermission(model.ServerUpdate), controller.CompleteRequest)
|
||||
steam2faRoutes.Post("/:id/cancel", auth.HasPermission(model.ServerUpdate), controller.CancelRequest)
|
||||
|
||||
return controller
|
||||
}
|
||||
|
||||
// GetPendingRequests gets all pending 2FA requests
|
||||
//
|
||||
// @Summary Get pending 2FA requests
|
||||
// @Description Get all pending Steam 2FA authentication requests
|
||||
// @Tags Steam 2FA
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Success 200 {array} model.Steam2FARequest
|
||||
// @Failure 500 {object} error_handler.ErrorResponse
|
||||
// @Router /steam2fa/pending [get]
|
||||
func (c *Steam2FAController) GetPendingRequests(ctx *fiber.Ctx) error {
|
||||
requests := c.tfaManager.GetPendingRequests()
|
||||
return ctx.JSON(requests)
|
||||
}
|
||||
|
||||
// GetRequest gets a specific 2FA request by ID
|
||||
//
|
||||
// @Summary Get 2FA request
|
||||
// @Description Get a specific Steam 2FA authentication request by ID
|
||||
// @Tags Steam 2FA
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param id path string true "2FA Request ID"
|
||||
// @Success 200 {object} model.Steam2FARequest
|
||||
// @Failure 404 {object} error_handler.ErrorResponse
|
||||
// @Failure 500 {object} error_handler.ErrorResponse
|
||||
// @Router /steam2fa/{id} [get]
|
||||
func (c *Steam2FAController) GetRequest(ctx *fiber.Ctx) error {
|
||||
id := ctx.Params("id")
|
||||
if id == "" {
|
||||
return c.errorHandler.HandleError(ctx, fiber.ErrBadRequest, fiber.StatusBadRequest)
|
||||
}
|
||||
|
||||
request, exists := c.tfaManager.GetRequest(id)
|
||||
if !exists {
|
||||
return c.errorHandler.HandleNotFoundError(ctx, "2FA request")
|
||||
}
|
||||
|
||||
return ctx.JSON(request)
|
||||
}
|
||||
|
||||
// CompleteRequest marks a 2FA request as completed
|
||||
//
|
||||
// @Summary Complete 2FA request
|
||||
// @Description Mark a Steam 2FA authentication request as completed
|
||||
// @Tags Steam 2FA
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param id path string true "2FA Request ID"
|
||||
// @Success 200 {object} model.Steam2FARequest
|
||||
// @Failure 400 {object} error_handler.ErrorResponse
|
||||
// @Failure 404 {object} error_handler.ErrorResponse
|
||||
// @Failure 500 {object} error_handler.ErrorResponse
|
||||
// @Router /steam2fa/{id}/complete [post]
|
||||
func (c *Steam2FAController) CompleteRequest(ctx *fiber.Ctx) error {
|
||||
id := ctx.Params("id")
|
||||
if id == "" {
|
||||
return c.errorHandler.HandleError(ctx, fiber.ErrBadRequest, fiber.StatusBadRequest)
|
||||
}
|
||||
|
||||
if err := c.tfaManager.CompleteRequest(id); err != nil {
|
||||
return c.errorHandler.HandleError(ctx, err, fiber.StatusBadRequest)
|
||||
}
|
||||
|
||||
request, exists := c.tfaManager.GetRequest(id)
|
||||
if !exists {
|
||||
return c.errorHandler.HandleNotFoundError(ctx, "2FA request")
|
||||
}
|
||||
|
||||
return ctx.JSON(request)
|
||||
}
|
||||
|
||||
// CancelRequest cancels a 2FA request
|
||||
//
|
||||
// @Summary Cancel 2FA request
|
||||
// @Description Cancel a Steam 2FA authentication request
|
||||
// @Tags Steam 2FA
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param id path string true "2FA Request ID"
|
||||
// @Success 200 {object} model.Steam2FARequest
|
||||
// @Failure 400 {object} error_handler.ErrorResponse
|
||||
// @Failure 404 {object} error_handler.ErrorResponse
|
||||
// @Failure 500 {object} error_handler.ErrorResponse
|
||||
// @Router /steam2fa/{id}/cancel [post]
|
||||
func (c *Steam2FAController) CancelRequest(ctx *fiber.Ctx) error {
|
||||
id := ctx.Params("id")
|
||||
if id == "" {
|
||||
return c.errorHandler.HandleError(ctx, fiber.ErrBadRequest, fiber.StatusBadRequest)
|
||||
}
|
||||
|
||||
if err := c.tfaManager.ErrorRequest(id, "cancelled by user"); err != nil {
|
||||
return c.errorHandler.HandleError(ctx, err, fiber.StatusBadRequest)
|
||||
}
|
||||
|
||||
request, exists := c.tfaManager.GetRequest(id)
|
||||
if !exists {
|
||||
return c.errorHandler.HandleNotFoundError(ctx, "2FA request")
|
||||
}
|
||||
|
||||
return ctx.JSON(request)
|
||||
}
|
||||
168
local/controller/websocket.go
Normal file
168
local/controller/websocket.go
Normal file
@@ -0,0 +1,168 @@
|
||||
package controller
|
||||
|
||||
import (
|
||||
"acc-server-manager/local/middleware"
|
||||
"acc-server-manager/local/service"
|
||||
"acc-server-manager/local/utl/common"
|
||||
"acc-server-manager/local/utl/jwt"
|
||||
"acc-server-manager/local/utl/logging"
|
||||
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"github.com/gofiber/websocket/v2"
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
type WebSocketController struct {
|
||||
webSocketService *service.WebSocketService
|
||||
jwtHandler *jwt.OpenJWTHandler
|
||||
}
|
||||
|
||||
// NewWebSocketController initializes WebSocketController
|
||||
func NewWebSocketController(
|
||||
wsService *service.WebSocketService,
|
||||
jwtHandler *jwt.OpenJWTHandler,
|
||||
routeGroups *common.RouteGroups,
|
||||
auth *middleware.AuthMiddleware,
|
||||
) *WebSocketController {
|
||||
wsc := &WebSocketController{
|
||||
webSocketService: wsService,
|
||||
jwtHandler: jwtHandler,
|
||||
}
|
||||
|
||||
// WebSocket routes
|
||||
wsRoutes := routeGroups.WebSocket
|
||||
wsRoutes.Use("/", wsc.upgradeWebSocket)
|
||||
wsRoutes.Get("/", websocket.New(wsc.handleWebSocket))
|
||||
|
||||
return wsc
|
||||
}
|
||||
|
||||
// upgradeWebSocket middleware to upgrade HTTP to WebSocket and validate authentication
|
||||
func (wsc *WebSocketController) upgradeWebSocket(c *fiber.Ctx) error {
|
||||
// Check if it's a WebSocket upgrade request
|
||||
if websocket.IsWebSocketUpgrade(c) {
|
||||
// Validate JWT token from query parameter or header
|
||||
token := c.Query("token")
|
||||
if token == "" {
|
||||
token = c.Get("Authorization")
|
||||
if token != "" && len(token) > 7 && token[:7] == "Bearer " {
|
||||
token = token[7:]
|
||||
}
|
||||
}
|
||||
|
||||
if token == "" {
|
||||
return fiber.NewError(fiber.StatusUnauthorized, "Missing authentication token")
|
||||
}
|
||||
|
||||
// Validate the token
|
||||
claims, err := wsc.jwtHandler.ValidateToken(token)
|
||||
if err != nil {
|
||||
return fiber.NewError(fiber.StatusUnauthorized, "Invalid authentication token")
|
||||
}
|
||||
|
||||
// Parse UserID string to UUID
|
||||
userID, err := uuid.Parse(claims.UserID)
|
||||
if err != nil {
|
||||
return fiber.NewError(fiber.StatusUnauthorized, "Invalid user ID in token")
|
||||
}
|
||||
|
||||
// Store user info in context for use in WebSocket handler
|
||||
c.Locals("userID", userID)
|
||||
c.Locals("username", claims.UserID) // Use UserID as username for now
|
||||
|
||||
return c.Next()
|
||||
}
|
||||
|
||||
return fiber.NewError(fiber.StatusUpgradeRequired, "WebSocket upgrade required")
|
||||
}
|
||||
|
||||
// handleWebSocket handles WebSocket connections
|
||||
func (wsc *WebSocketController) handleWebSocket(c *websocket.Conn) {
|
||||
// Generate a unique connection ID
|
||||
connID := uuid.New().String()
|
||||
|
||||
// Get user info from locals (set by middleware)
|
||||
userID, ok := c.Locals("userID").(uuid.UUID)
|
||||
if !ok {
|
||||
logging.Error("Failed to get user ID from WebSocket connection")
|
||||
c.Close()
|
||||
return
|
||||
}
|
||||
|
||||
username, _ := c.Locals("username").(string)
|
||||
logging.Info("WebSocket connection established for user: %s (ID: %s)", username, userID.String())
|
||||
|
||||
// Add the connection to the service
|
||||
wsc.webSocketService.AddConnection(connID, c, &userID)
|
||||
|
||||
// Handle connection cleanup
|
||||
defer func() {
|
||||
wsc.webSocketService.RemoveConnection(connID)
|
||||
logging.Info("WebSocket connection closed for user: %s", username)
|
||||
}()
|
||||
|
||||
// Handle incoming messages from the client
|
||||
for {
|
||||
messageType, message, err := c.ReadMessage()
|
||||
if err != nil {
|
||||
if websocket.IsUnexpectedCloseError(err, websocket.CloseGoingAway, websocket.CloseAbnormalClosure) {
|
||||
logging.Error("WebSocket error for user %s: %v", username, err)
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
// Handle different message types
|
||||
switch messageType {
|
||||
case websocket.TextMessage:
|
||||
wsc.handleTextMessage(connID, userID, message)
|
||||
case websocket.BinaryMessage:
|
||||
logging.Debug("Received binary message from user %s (not supported)", username)
|
||||
case websocket.PingMessage:
|
||||
// Respond with pong
|
||||
if err := c.WriteMessage(websocket.PongMessage, nil); err != nil {
|
||||
logging.Error("Failed to send pong to user %s: %v", username, err)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// handleTextMessage processes text messages from the client
|
||||
func (wsc *WebSocketController) handleTextMessage(connID string, userID uuid.UUID, message []byte) {
|
||||
logging.Debug("Received WebSocket message from user %s: %s", userID.String(), string(message))
|
||||
|
||||
// Parse the message to handle different types of client requests
|
||||
// For now, we'll just log it. In the future, you might want to handle:
|
||||
// - Subscription to specific server creation processes
|
||||
// - Client heartbeat/keepalive
|
||||
// - Request for status updates
|
||||
|
||||
// Example: If the message contains a server ID, associate this connection with that server
|
||||
// This is a simple implementation - you might want to use proper JSON parsing
|
||||
messageStr := string(message)
|
||||
if len(messageStr) > 10 && messageStr[:9] == "server_id" {
|
||||
// Extract server ID from message like "server_id:uuid"
|
||||
if serverIDStr := messageStr[10:]; len(serverIDStr) > 0 {
|
||||
if serverID, err := uuid.Parse(serverIDStr); err == nil {
|
||||
wsc.webSocketService.SetServerID(connID, serverID)
|
||||
logging.Info("Associated WebSocket connection %s with server %s", connID, serverID.String())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// GetWebSocketUpgrade returns the WebSocket upgrade handler for use in other controllers
|
||||
func (wsc *WebSocketController) GetWebSocketUpgrade() fiber.Handler {
|
||||
return wsc.upgradeWebSocket
|
||||
}
|
||||
|
||||
// GetWebSocketHandler returns the WebSocket connection handler for use in other controllers
|
||||
func (wsc *WebSocketController) GetWebSocketHandler() func(*websocket.Conn) {
|
||||
return wsc.handleWebSocket
|
||||
}
|
||||
|
||||
// BroadcastServerCreationProgress is a helper method for other services to broadcast progress
|
||||
func (wsc *WebSocketController) BroadcastServerCreationProgress(serverID uuid.UUID, step string, status string, message string) {
|
||||
// This can be used by the ServerService during server creation
|
||||
logging.Info("Broadcasting server creation progress: %s - %s: %s", serverID.String(), step, status)
|
||||
}
|
||||
Reference in New Issue
Block a user