Files
doucan/app/internal/handlers/handlers.go
2026-02-20 23:39:49 +08:00

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), &currentRoomID)
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), &currentRoomID)
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[:])
}