package handlers import ( "context" "crypto/sha256" "doudizhu-server/internal/captcha" "doudizhu-server/internal/db" "doudizhu-server/internal/game" "doudizhu-server/internal/models" "doudizhu-server/internal/redis" "doudizhu-server/internal/ws" "encoding/hex" "encoding/json" "errors" "net/http" "strings" "time" ) type Handler struct { GameMgr *game.GameManager Hub *ws.Hub Rdb *redis.Client DB *db.DB Captcha *captcha.Manager UserTTL time.Duration } func NewHandler(gameMgr *game.GameManager, hub *ws.Hub, rdb *redis.Client, database *db.DB, captchaMgr *captcha.Manager) *Handler { return &Handler{ GameMgr: gameMgr, Hub: hub, Rdb: rdb, DB: database, Captcha: captchaMgr, UserTTL: 24 * time.Hour, } } func (h *Handler) GetCaptcha(w http.ResponseWriter, r *http.Request) { ctx := context.Background() captchaID, imageBase64, err := h.Captcha.Generate(ctx) if err != nil { h.jsonError(w, http.StatusInternalServerError, "failed to generate captcha") return } h.jsonSuccess(w, models.CaptchaResponse{ CaptchaID: captchaID, Image: imageBase64, }) } func (h *Handler) Register(w http.ResponseWriter, r *http.Request) { var req models.RegisterRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { h.jsonError(w, http.StatusBadRequest, "invalid request") return } if req.Username == "" || req.Password == "" || req.Nickname == "" { h.jsonError(w, http.StatusBadRequest, "username, password and nickname required") return } if len(req.Username) < 3 || len(req.Username) > 20 { h.jsonError(w, http.StatusBadRequest, "username must be 3-20 characters") return } if len(req.Password) < 6 { h.jsonError(w, http.StatusBadRequest, "password must be at least 6 characters") return } ctx := context.Background() if !h.Captcha.Verify(ctx, req.CaptchaID, strings.ToUpper(req.Captcha)) { h.jsonError(w, http.StatusBadRequest, "invalid captcha") return } exists, err := h.DB.UsernameExists(ctx, req.Username) if err != nil { h.jsonError(w, http.StatusInternalServerError, "failed to check username") return } if exists { h.jsonError(w, http.StatusBadRequest, "username already exists") return } user := &models.User{ ID: generateUserID(), Username: req.Username, Password: hashPassword(req.Password), Nickname: req.Nickname, } if err := h.DB.CreateUser(ctx, user); err != nil { h.jsonError(w, http.StatusInternalServerError, "failed to create user") return } token := generateToken() h.Rdb.Set(ctx, redis.SessionKey(token), user.ID, h.UserTTL) h.jsonSuccess(w, map[string]interface{}{ "token": token, "userId": user.ID, "username": user.Username, "nickname": user.Nickname, }) } func (h *Handler) Login(w http.ResponseWriter, r *http.Request) { var req models.LoginRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { h.jsonError(w, http.StatusBadRequest, "invalid request") return } if req.Username == "" || req.Password == "" { h.jsonError(w, http.StatusBadRequest, "username and password required") return } ctx := context.Background() if !h.Captcha.Verify(ctx, req.CaptchaID, strings.ToUpper(req.Captcha)) { h.jsonError(w, http.StatusBadRequest, "invalid captcha") return } user, err := h.DB.GetUserByUsername(ctx, req.Username) if err != nil { h.jsonError(w, http.StatusUnauthorized, "invalid username or password") return } if user.Password != hashPassword(req.Password) { h.jsonError(w, http.StatusUnauthorized, "invalid username or password") return } token := generateToken() h.Rdb.Set(ctx, redis.SessionKey(token), user.ID, h.UserTTL) h.jsonSuccess(w, map[string]interface{}{ "token": token, "userId": user.ID, "username": user.Username, "nickname": user.Nickname, }) } func (h *Handler) ValidateToken(w http.ResponseWriter, r *http.Request) { token := r.Header.Get("Authorization") if token == "" { h.jsonError(w, http.StatusUnauthorized, "token required") return } token = strings.TrimPrefix(token, "Bearer ") ctx := context.Background() var userID string if err := h.Rdb.Get(ctx, redis.SessionKey(token), &userID); err != nil { h.jsonError(w, http.StatusUnauthorized, "invalid token") return } user, err := h.DB.GetUserByID(ctx, userID) if err != nil { h.jsonError(w, http.StatusUnauthorized, "user not found") return } h.jsonSuccess(w, map[string]interface{}{ "userId": user.ID, "username": user.Username, "nickname": user.Nickname, }) } func (h *Handler) Logout(w http.ResponseWriter, r *http.Request) { token := r.Header.Get("Authorization") if token == "" { h.jsonSuccess(w, nil) return } token = strings.TrimPrefix(token, "Bearer ") h.Rdb.Delete(context.Background(), redis.SessionKey(token)) h.jsonSuccess(w, nil) } func (h *Handler) CreateRoom(w http.ResponseWriter, r *http.Request) { userID, err := h.authenticate(r) if err != nil { h.jsonError(w, http.StatusUnauthorized, err.Error()) return } var req models.CreateRoomRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { h.jsonError(w, http.StatusBadRequest, "invalid request") return } if req.MaxPlayers < 2 || req.MaxPlayers > 10 { req.MaxPlayers = 4 } ctx := context.Background() var currentRoomID string h.Rdb.Get(ctx, redis.UserRoomKey(userID), ¤tRoomID) if currentRoomID != "" { room, _ := h.GameMgr.GetRoom(currentRoomID) if room != nil { h.jsonError(w, http.StatusBadRequest, "you are already in a room, please leave first") return } h.Rdb.Delete(ctx, redis.UserRoomKey(userID)) } room, player := h.GameMgr.CreateRoomWithUser(userID, req.PlayerName, req.MaxPlayers) h.Rdb.Set(ctx, redis.UserRoomKey(userID), room.ID, redis.RoomTTL) h.jsonSuccess(w, map[string]string{"roomId": room.ID, "playerId": player.ID}) } func (h *Handler) JoinRoom(w http.ResponseWriter, r *http.Request) { userID, err := h.authenticate(r) if err != nil { h.jsonError(w, http.StatusUnauthorized, err.Error()) return } roomID := strings.ToLower(extractRoomID(r.URL.Path)) if roomID == "" { h.jsonError(w, http.StatusBadRequest, "room id required") return } var req models.JoinRoomRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { h.jsonError(w, http.StatusBadRequest, "invalid request") return } if req.PlayerName == "" { h.jsonError(w, http.StatusBadRequest, "player name required") return } ctx := context.Background() var currentRoomID string h.Rdb.Get(ctx, redis.UserRoomKey(userID), ¤tRoomID) if currentRoomID != "" && currentRoomID != roomID { room, _ := h.GameMgr.GetRoom(currentRoomID) if room != nil { h.jsonError(w, http.StatusBadRequest, "you are already in another room, please leave first") return } h.Rdb.Delete(ctx, redis.UserRoomKey(userID)) } room, player, err := h.GameMgr.JoinRoomWithUser(roomID, userID, req.PlayerName) if err != nil { h.jsonError(w, http.StatusBadRequest, err.Error()) return } h.Rdb.Set(ctx, redis.UserRoomKey(userID), room.ID, redis.RoomTTL) h.jsonSuccess(w, map[string]string{"roomId": room.ID, "playerId": player.ID}) } func (h *Handler) GetCurrentRoom(w http.ResponseWriter, r *http.Request) { userID, err := h.authenticate(r) if err != nil { h.jsonError(w, http.StatusUnauthorized, err.Error()) return } ctx := context.Background() var roomID string if err := h.Rdb.Get(ctx, redis.UserRoomKey(userID), &roomID); err != nil { h.jsonSuccess(w, nil) return } room, err := h.GameMgr.GetRoom(roomID) if err != nil { h.Rdb.Delete(ctx, redis.UserRoomKey(userID)) h.jsonSuccess(w, nil) return } playerID := h.GameMgr.GetPlayerIDByUserID(roomID, userID) if playerID == "" { h.Rdb.Delete(ctx, redis.UserRoomKey(userID)) h.jsonSuccess(w, nil) return } h.jsonSuccess(w, map[string]string{"roomId": room.ID, "playerId": playerID}) } func (h *Handler) LeaveRoom(w http.ResponseWriter, r *http.Request) { userID, err := h.authenticate(r) if err != nil { h.jsonError(w, http.StatusUnauthorized, err.Error()) return } ctx := context.Background() var roomID string if err := h.Rdb.Get(ctx, redis.UserRoomKey(userID), &roomID); err != nil { h.jsonSuccess(w, nil) return } playerID := h.GameMgr.GetPlayerIDByUserID(roomID, userID) if err := h.GameMgr.LeaveRoom(roomID, playerID); err != nil { h.jsonError(w, http.StatusBadRequest, err.Error()) return } h.Rdb.Delete(ctx, redis.UserRoomKey(userID)) h.Hub.BroadcastRoomLeft(roomID, playerID) h.jsonSuccess(w, nil) } func (h *Handler) WebSocket(w http.ResponseWriter, r *http.Request) { ws.ServeWs(h.Hub, w, r) } func (h *Handler) authenticate(r *http.Request) (string, error) { token := r.Header.Get("Authorization") if token == "" { return "", errors.New("token required") } token = strings.TrimPrefix(token, "Bearer ") ctx := context.Background() var userID string if err := h.Rdb.Get(ctx, redis.SessionKey(token), &userID); err != nil { return "", errors.New("invalid token") } return userID, nil } func (h *Handler) jsonSuccess(w http.ResponseWriter, data interface{}) { w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(models.ApiResponse{ Status: 200, Code: 0, Message: "success", Data: data, }) } func (h *Handler) jsonError(w http.ResponseWriter, status int, msg string) { w.Header().Set("Content-Type", "application/json") w.WriteHeader(status) json.NewEncoder(w).Encode(models.ApiResponse{ Status: status, Code: 1, Message: msg, }) } func extractRoomID(path string) string { parts := strings.Split(strings.Trim(path, "/"), "/") for i, p := range parts { if p == "rooms" && i+1 < len(parts) && parts[i+1] != "join" && parts[i+1] != "leave" { return parts[i+1] } } return "" } func generateUserID() string { const charset = "abcdefghijklmnopqrstuvwxyz0123456789" b := make([]byte, 16) for i := range b { b[i] = charset[time.Now().UnixNano()%int64(len(charset))] time.Sleep(time.Nanosecond) } return string(b) } func generateToken() string { data := time.Now().String() + generateUserID() hash := sha256.Sum256([]byte(data)) return hex.EncodeToString(hash[:]) } func hashPassword(password string) string { hash := sha256.Sum256([]byte(password + "doudizhu_salt")) return hex.EncodeToString(hash[:]) }