v0.9.0 user and session management improvement, xterm.js addons
This commit is contained in:
@ -1,4 +1,3 @@
|
||||
// FILE: lixenwraith/chess/internal/server/http/auth.go
|
||||
package http
|
||||
|
||||
import (
|
||||
@ -9,6 +8,7 @@ import (
|
||||
"unicode"
|
||||
|
||||
"chess/internal/server/core"
|
||||
"chess/internal/server/service"
|
||||
|
||||
"github.com/gofiber/fiber/v2"
|
||||
)
|
||||
@ -90,8 +90,8 @@ func (h *HTTPHandler) RegisterHandler(c *fiber.Ctx) error {
|
||||
req.Email = strings.ToLower(req.Email)
|
||||
}
|
||||
|
||||
// Create user
|
||||
user, err := h.svc.CreateUser(req.Username, req.Email, req.Password)
|
||||
// Create user (temp by default via API)
|
||||
user, err := h.svc.CreateUser(req.Username, req.Email, req.Password, false)
|
||||
if err != nil {
|
||||
if strings.Contains(err.Error(), "already exists") {
|
||||
return c.Status(fiber.StatusConflict).JSON(core.ErrorResponse{
|
||||
@ -100,14 +100,30 @@ func (h *HTTPHandler) RegisterHandler(c *fiber.Ctx) error {
|
||||
Details: "username or email already taken",
|
||||
})
|
||||
}
|
||||
if strings.Contains(err.Error(), "limit") || strings.Contains(err.Error(), "capacity") {
|
||||
return c.Status(fiber.StatusServiceUnavailable).JSON(core.ErrorResponse{
|
||||
Error: "registration temporarily unavailable",
|
||||
Code: core.ErrResourceLimit,
|
||||
Details: err.Error(),
|
||||
})
|
||||
}
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(core.ErrorResponse{
|
||||
Error: "failed to create user",
|
||||
Code: core.ErrInternalError,
|
||||
})
|
||||
}
|
||||
|
||||
// Create session for new user
|
||||
sessionID, err := h.svc.CreateUserSession(user.UserID)
|
||||
if err != nil {
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(core.ErrorResponse{
|
||||
Error: "failed to create session",
|
||||
Code: core.ErrInternalError,
|
||||
})
|
||||
}
|
||||
|
||||
// Generate JWT token
|
||||
token, err := h.svc.GenerateUserToken(user.UserID)
|
||||
token, err := h.svc.GenerateUserToken(user.UserID, sessionID)
|
||||
if err != nil {
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(core.ErrorResponse{
|
||||
Error: "failed to generate token",
|
||||
@ -120,7 +136,7 @@ func (h *HTTPHandler) RegisterHandler(c *fiber.Ctx) error {
|
||||
UserID: user.UserID,
|
||||
Username: user.Username,
|
||||
Email: user.Email,
|
||||
ExpiresAt: time.Now().Add(7 * 24 * time.Hour),
|
||||
ExpiresAt: time.Now().Add(service.SessionTTL),
|
||||
})
|
||||
}
|
||||
|
||||
@ -173,18 +189,17 @@ func (h *HTTPHandler) LoginHandler(c *fiber.Ctx) error {
|
||||
// Normalize identifier for case-insensitive lookup
|
||||
req.Identifier = strings.ToLower(req.Identifier)
|
||||
|
||||
// Authenticate user
|
||||
user, err := h.svc.AuthenticateUser(req.Identifier, req.Password)
|
||||
// Authenticate user and create session (invalidates previous session)
|
||||
user, sessionID, err := h.svc.AuthenticateUser(req.Identifier, req.Password)
|
||||
if err != nil {
|
||||
// Always return same error to prevent user enumeration
|
||||
return c.Status(fiber.StatusUnauthorized).JSON(core.ErrorResponse{
|
||||
Error: "invalid credentials",
|
||||
Code: core.ErrInvalidRequest,
|
||||
})
|
||||
}
|
||||
|
||||
// Generate JWT token
|
||||
token, err := h.svc.GenerateUserToken(user.UserID)
|
||||
// Generate JWT token with session ID
|
||||
token, err := h.svc.GenerateUserToken(user.UserID, sessionID)
|
||||
if err != nil {
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(core.ErrorResponse{
|
||||
Error: "failed to generate token",
|
||||
@ -192,10 +207,6 @@ func (h *HTTPHandler) LoginHandler(c *fiber.Ctx) error {
|
||||
})
|
||||
}
|
||||
|
||||
// Update last login
|
||||
// TODO: for now, non-blocking if login time update fails, log/block in the future
|
||||
_ = h.svc.UpdateLastLogin(user.UserID)
|
||||
|
||||
return c.JSON(AuthResponse{
|
||||
Token: token,
|
||||
UserID: user.UserID,
|
||||
@ -229,4 +240,25 @@ func (h *HTTPHandler) GetCurrentUserHandler(c *fiber.Ctx) error {
|
||||
Email: user.Email,
|
||||
CreatedAt: user.CreatedAt,
|
||||
})
|
||||
}
|
||||
|
||||
// LogoutHandler invalidates the current session
|
||||
func (h *HTTPHandler) LogoutHandler(c *fiber.Ctx) error {
|
||||
// Extract session ID from token claims
|
||||
sessionID, ok := c.Locals("sessionID").(string)
|
||||
if !ok || sessionID == "" {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(core.ErrorResponse{
|
||||
Error: "no active session",
|
||||
Code: core.ErrInvalidRequest,
|
||||
})
|
||||
}
|
||||
|
||||
if err := h.svc.InvalidateSession(sessionID); err != nil {
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(core.ErrorResponse{
|
||||
Error: "failed to logout",
|
||||
Code: core.ErrInternalError,
|
||||
})
|
||||
}
|
||||
|
||||
return c.JSON(fiber.Map{"message": "logged out"})
|
||||
}
|
||||
@ -1,4 +1,3 @@
|
||||
// FILE: lixenwraith/chess/internal/server/http/handler.go
|
||||
package http
|
||||
|
||||
import (
|
||||
@ -100,6 +99,9 @@ func NewFiberApp(proc *processor.Processor, svc *service.Service, devMode bool)
|
||||
// Current user (requires auth)
|
||||
auth.Get("/me", AuthRequired(validateToken), h.GetCurrentUserHandler)
|
||||
|
||||
// Logout
|
||||
auth.Post("/logout", AuthRequired(validateToken), h.LogoutHandler)
|
||||
|
||||
// Game routes with standard rate limiting
|
||||
maxReq := rateLimitRate
|
||||
if devMode {
|
||||
@ -137,7 +139,7 @@ func NewFiberApp(proc *processor.Processor, svc *service.Service, devMode bool)
|
||||
api.Put("/games/:gameId/players", h.ConfigurePlayers)
|
||||
api.Get("/games/:gameId", h.GetGame)
|
||||
api.Delete("/games/:gameId", h.DeleteGame)
|
||||
api.Post("/games/:gameId/moves", h.MakeMove)
|
||||
api.Post("/games/:gameId/moves", OptionalAuth(validateToken), h.MakeMove)
|
||||
api.Post("/games/:gameId/undo", h.UndoMove)
|
||||
api.Get("/games/:gameId/board", h.GetBoard)
|
||||
|
||||
@ -370,7 +372,6 @@ func (h *HTTPHandler) GetGame(c *fiber.Ctx) error {
|
||||
func (h *HTTPHandler) MakeMove(c *fiber.Ctx) error {
|
||||
gameID := c.Params("gameId")
|
||||
|
||||
// Validate UUID format
|
||||
if !isValidUUID(gameID) {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(core.ErrorResponse{
|
||||
Error: "invalid game ID format",
|
||||
@ -379,7 +380,6 @@ func (h *HTTPHandler) MakeMove(c *fiber.Ctx) error {
|
||||
})
|
||||
}
|
||||
|
||||
// Ensure middleware validation ran
|
||||
validated, ok := c.Locals("validated").(bool)
|
||||
if !ok || !validated {
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(core.ErrorResponse{
|
||||
@ -388,7 +388,6 @@ func (h *HTTPHandler) MakeMove(c *fiber.Ctx) error {
|
||||
})
|
||||
}
|
||||
|
||||
// Retrieve validated parsed body
|
||||
validatedBody := c.Locals("validatedBody")
|
||||
if validatedBody == nil {
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(core.ErrorResponse{
|
||||
@ -399,15 +398,21 @@ func (h *HTTPHandler) MakeMove(c *fiber.Ctx) error {
|
||||
var req core.MoveRequest
|
||||
req = *(validatedBody.(*core.MoveRequest))
|
||||
|
||||
// Create command and execute
|
||||
// Get authenticated user ID if present
|
||||
userID, _ := c.Locals("userID").(string)
|
||||
|
||||
cmd := processor.NewMakeMoveCommand(gameID, req)
|
||||
cmd.UserID = userID // Pass user context for authorization
|
||||
|
||||
resp := h.proc.Execute(cmd)
|
||||
|
||||
// Return appropriate HTTP response with correct status code
|
||||
if !resp.Success {
|
||||
statusCode := fiber.StatusBadRequest
|
||||
if resp.Error.Code == core.ErrGameNotFound {
|
||||
switch resp.Error.Code {
|
||||
case core.ErrGameNotFound:
|
||||
statusCode = fiber.StatusNotFound
|
||||
case core.ErrUnauthorized:
|
||||
statusCode = fiber.StatusForbidden
|
||||
}
|
||||
return c.Status(statusCode).JSON(resp.Error)
|
||||
}
|
||||
|
||||
@ -1,4 +1,3 @@
|
||||
// FILE: lixenwraith/chess/internal/server/http/middleware.go
|
||||
package http
|
||||
|
||||
import (
|
||||
@ -23,7 +22,7 @@ func AuthRequired(validateToken TokenValidator) fiber.Handler {
|
||||
})
|
||||
}
|
||||
|
||||
userID, _, err := validateToken(token)
|
||||
userID, claims, err := validateToken(token)
|
||||
if err != nil {
|
||||
return c.Status(fiber.StatusUnauthorized).JSON(core.ErrorResponse{
|
||||
Error: "invalid or expired token",
|
||||
@ -32,6 +31,9 @@ func AuthRequired(validateToken TokenValidator) fiber.Handler {
|
||||
}
|
||||
|
||||
c.Locals("userID", userID)
|
||||
if sessionID, ok := claims["session_id"].(string); ok {
|
||||
c.Locals("sessionID", sessionID)
|
||||
}
|
||||
return c.Next()
|
||||
}
|
||||
}
|
||||
@ -44,11 +46,13 @@ func OptionalAuth(validateToken TokenValidator) fiber.Handler {
|
||||
return c.Next()
|
||||
}
|
||||
|
||||
userID, _, err := validateToken(token)
|
||||
userID, claims, err := validateToken(token)
|
||||
if err == nil {
|
||||
c.Locals("userID", userID)
|
||||
if sessionID, ok := claims["session_id"].(string); ok {
|
||||
c.Locals("sessionID", sessionID)
|
||||
}
|
||||
}
|
||||
// Continue regardless of token validity
|
||||
return c.Next()
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,4 +1,3 @@
|
||||
// FILE: lixenwraith/chess/internal/server/http/handler.go
|
||||
package http
|
||||
|
||||
import (
|
||||
|
||||
Reference in New Issue
Block a user