Files
MMGame/backend/internal/handlers/auth.go
T
gmh01 7614e79d12 refactor: rewrite backend in Go, replacing Python FastAPI
Port the FastAPI backend to Go 1.26, preserving all API functionality:
- auth (register/login/guest/logout/profile/history) with JWT + bcrypt
- scripts (list/detail) backed by embedded sample_data.json
- games (create/join/start/chat/phase/clue/vote/state/replay) + WebSocket
- rule-based NPC agent plus DeepSeek/Ollama AI backends
- SQLite persistence via modernc.org/sqlite (pure Go, no cgo)

Frontend and documents are unchanged.
2026-08-03 11:39:53 +08:00

264 lines
7.2 KiB
Go

package handlers
import (
"database/sql"
"encoding/json"
"errors"
"net/http"
"strings"
"time"
"github.com/golang-jwt/jwt/v5"
"golang.org/x/crypto/bcrypt"
"mmgame/internal/config"
"mmgame/internal/database"
"mmgame/internal/models"
"mmgame/internal/util"
)
var errUnauthorized = errors.New("unauthorized")
func createToken(userID string) (string, error) {
exp := time.Now().Add(time.Duration(config.Cfg.AccessTokenExpireMinutes) * time.Minute)
claims := jwt.MapClaims{
"sub": userID,
"exp": exp.Unix(),
"v": 0,
}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
return token.SignedString([]byte(config.Cfg.SecretKey))
}
func currentUser(r *http.Request) (*models.User, error) {
auth := r.Header.Get("Authorization")
if !strings.HasPrefix(auth, "Bearer ") {
return nil, errUnauthorized
}
tokenStr := strings.TrimPrefix(auth, "Bearer ")
claims := jwt.MapClaims{}
token, err := jwt.ParseWithClaims(tokenStr, claims, func(t *jwt.Token) (interface{}, error) {
return []byte(config.Cfg.SecretKey), nil
}, jwt.WithValidMethods([]string{config.Cfg.Algorithm}))
if err != nil || !token.Valid {
return nil, errUnauthorized
}
userID, _ := claims["sub"].(string)
if userID == "" {
return nil, errUnauthorized
}
row := database.DB.QueryRow(
`SELECT id, email, nickname, avatar_url, password_hash, role, is_guest, game_count, token_version, created_at
FROM users WHERE id = ?`, userID)
var u models.User
var email, nickname, avatarURL, passwordHash, createdAt sql.NullString
if err := row.Scan(&u.ID, &email, &nickname, &avatarURL, &passwordHash, &u.Role,
&u.IsGuest, &u.GameCount, &u.TokenVersion, &createdAt); err != nil {
return nil, errUnauthorized
}
if email.Valid {
u.Email = &email.String
}
if nickname.Valid {
u.Nickname = &nickname.String
}
if avatarURL.Valid {
u.AvatarURL = &avatarURL.String
}
if passwordHash.Valid {
u.PasswordHash = &passwordHash.String
}
u.CreatedAt = util.ParseTime(createdAt.String)
return &u, nil
}
func Register(w http.ResponseWriter, r *http.Request) {
var req struct {
Email string `json:"email"`
Password string `json:"password"`
Nickname string `json:"nickname"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
writeError(w, 422, "请求格式错误")
return
}
var existingID string
err := database.DB.QueryRow(`SELECT id FROM users WHERE email = ?`, req.Email).Scan(&existingID)
if err == nil {
writeError(w, 400, "邮箱已注册")
return
}
if err != sql.ErrNoRows {
writeError(w, 500, "服务器错误")
return
}
hash, err := bcrypt.GenerateFromPassword([]byte(req.Password), bcrypt.DefaultCost)
if err != nil {
writeError(w, 500, "服务器错误")
return
}
userID := newID()
_, err = database.DB.Exec(
`INSERT INTO users (id, email, nickname, password_hash, role, is_guest, game_count, token_version, created_at)
VALUES (?, ?, ?, ?, ?, 0, 0, 0, ?)`,
userID, req.Email, req.Nickname, string(hash), models.RolePlayer, util.NowStr())
if err != nil {
writeError(w, 500, "服务器错误")
return
}
token, _ := createToken(userID)
writeJSON(w, 200, map[string]interface{}{
"token": token,
"user_id": userID,
"nickname": req.Nickname,
"is_guest": false,
})
}
func Login(w http.ResponseWriter, r *http.Request) {
var req struct {
Email string `json:"email"`
Password string `json:"password"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
writeError(w, 422, "请求格式错误")
return
}
var id, email, nickname, passwordHash string
err := database.DB.QueryRow(
`SELECT id, email, nickname, password_hash FROM users WHERE email = ?`, req.Email).
Scan(&id, &email, &nickname, &passwordHash)
if err == sql.ErrNoRows || passwordHash == "" {
writeError(w, 401, "邮箱或密码错误")
return
}
if err != nil {
writeError(w, 500, "服务器错误")
return
}
if bcrypt.CompareHashAndPassword([]byte(passwordHash), []byte(req.Password)) != nil {
writeError(w, 401, "邮箱或密码错误")
return
}
token, _ := createToken(id)
writeJSON(w, 200, map[string]interface{}{
"token": token,
"user_id": id,
"nickname": nickname,
"is_guest": false,
})
}
func GuestLogin(w http.ResponseWriter, r *http.Request) {
var req struct {
Nickname string `json:"nickname"`
}
_ = json.NewDecoder(r.Body).Decode(&req)
if req.Nickname == "" {
req.Nickname = "游客"
}
userID := newID()
_, err := database.DB.Exec(
`INSERT INTO users (id, nickname, role, is_guest, game_count, token_version, created_at)
VALUES (?, ?, ?, 1, 0, 0, ?)`,
userID, req.Nickname, models.RolePlayer, util.NowStr())
if err != nil {
writeError(w, 500, "服务器错误")
return
}
token, _ := createToken(userID)
writeJSON(w, 200, map[string]interface{}{
"token": token,
"user_id": userID,
"nickname": req.Nickname,
"is_guest": true,
})
}
func Logout(w http.ResponseWriter, r *http.Request) {
writeJSON(w, 200, map[string]string{"status": "ok"})
}
func GetProfile(w http.ResponseWriter, r *http.Request) {
user, err := currentUser(r)
if err != nil {
writeError(w, 401, "未登录")
return
}
writeJSON(w, 200, map[string]interface{}{
"user_id": user.ID,
"email": nullStr(user.Email),
"nickname": nullStr(user.Nickname),
"avatar_url": nullStr(user.AvatarURL),
"is_guest": user.IsGuest,
"role": user.Role,
"game_count": user.GameCount,
"created_at": util.TimeStr(user.CreatedAt),
})
}
func UpdateProfile(w http.ResponseWriter, r *http.Request) {
user, err := currentUser(r)
if err != nil {
writeError(w, 401, "未登录")
return
}
var data map[string]interface{}
_ = json.NewDecoder(r.Body).Decode(&data)
var nickname, avatarURL *string
if v, ok := data["nickname"].(string); ok {
nickname = &v
}
if v, ok := data["avatar_url"].(string); ok {
avatarURL = &v
}
if nickname != nil {
if _, err := database.DB.Exec(`UPDATE users SET nickname = ? WHERE id = ?`, *nickname, user.ID); err != nil {
writeError(w, 500, "服务器错误")
return
}
}
if avatarURL != nil {
if _, err := database.DB.Exec(`UPDATE users SET avatar_url = ? WHERE id = ?`, *avatarURL, user.ID); err != nil {
writeError(w, 500, "服务器错误")
return
}
}
writeJSON(w, 200, map[string]string{"status": "ok"})
}
func GetHistory(w http.ResponseWriter, r *http.Request) {
if _, err := currentUser(r); err != nil {
writeError(w, 401, "未登录")
return
}
rows, err := database.DB.Query(
`SELECT id, script_id, status, phase, phase_index, started_at, completed_at, created_at
FROM game_sessions ORDER BY created_at DESC LIMIT 50`)
if err != nil {
writeError(w, 500, "服务器错误")
return
}
defer rows.Close()
var items []map[string]interface{}
for rows.Next() {
var id, scriptID, status, phase, startedAt, completedAt, createdAt sql.NullString
var phaseIndex int
if err := rows.Scan(&id, &scriptID, &status, &phase, &phaseIndex, &startedAt, &completedAt, &createdAt); err != nil {
continue
}
items = append(items, map[string]interface{}{
"id": id.String,
"script_id": scriptID.String,
"status": status.String,
"phase": phase.String,
"phase_index": phaseIndex,
"started_at": startedAt.String,
"completed_at": completedAt.String,
"created_at": createdAt.String,
})
}
writeJSON(w, 200, items)
}