410 lines
10 KiB
Go
410 lines
10 KiB
Go
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[:])
|
|
}
|