111 lines
3.6 KiB
Go
111 lines
3.6 KiB
Go
package service
|
||
|
||
import (
|
||
"context"
|
||
"strings"
|
||
"time"
|
||
"unicode/utf8"
|
||
|
||
"golang.org/x/crypto/bcrypt"
|
||
roomeventsv1 "hyapp.local/api/proto/events/room/v1"
|
||
roomv1 "hyapp.local/api/proto/room/v1"
|
||
"hyapp/pkg/xerr"
|
||
"hyapp/services/room-service/internal/room/command"
|
||
"hyapp/services/room-service/internal/room/outbox"
|
||
"hyapp/services/room-service/internal/room/rank"
|
||
"hyapp/services/room-service/internal/room/state"
|
||
)
|
||
|
||
const maxRoomPasswordRunes = 64
|
||
|
||
// SetRoomPassword 设置或清空房间入房密码。
|
||
// 密码属于 Room Cell 核心状态:JoinRoom、快照恢复和列表投影都必须以同一份状态为准。
|
||
func (s *Service) SetRoomPassword(ctx context.Context, req *roomv1.SetRoomPasswordRequest) (*roomv1.SetRoomPasswordResponse, error) {
|
||
ctx = contextFromMeta(ctx, req.GetMeta())
|
||
password, passwordHash, err := normalizeRoomPassword(req.GetLocked(), req.GetPassword())
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
cmd := command.SetRoomPassword{
|
||
Base: baseFromMeta(req.GetMeta()),
|
||
Locked: req.GetLocked(),
|
||
Password: password,
|
||
PasswordHash: passwordHash,
|
||
}
|
||
|
||
result, err := s.mutateRoom(ctx, cmd, true, func(now time.Time, current *state.RoomState, _ *rank.LocalRank) (mutationResult, []outbox.Record, error) {
|
||
if err := requireActiveRoom(current); err != nil {
|
||
return mutationResult{}, nil, err
|
||
}
|
||
if err := requireOwnerPresent(current, cmd.ActorUserID()); err != nil {
|
||
// 锁房只允许 owner 操作,避免普通房管把房主挡在自己的房间外。
|
||
return mutationResult{}, nil, err
|
||
}
|
||
|
||
if !cmd.Locked {
|
||
if current.RoomPasswordHash == "" {
|
||
// 重复解锁是 no-op,只写 command log 保护幂等。
|
||
return mutationResult{snapshot: current.ToProto()}, nil, nil
|
||
}
|
||
current.RoomPasswordHash = ""
|
||
} else {
|
||
if roomPasswordMatches(current.RoomPasswordHash, password) {
|
||
// 同一个密码重复提交不产生版本和系统消息,避免客户端收到无意义状态变更。
|
||
return mutationResult{snapshot: current.ToProto()}, nil, nil
|
||
}
|
||
current.RoomPasswordHash = cmd.PasswordHash
|
||
}
|
||
|
||
current.Version++
|
||
currentHash := current.RoomPasswordHash
|
||
passwordEvent, err := outbox.Build(current.RoomID, "RoomPasswordChanged", current.Version, now, &roomeventsv1.RoomPasswordChanged{
|
||
ActorUserId: cmd.ActorUserID(),
|
||
Locked: cmd.Locked,
|
||
VisibleRegionId: current.VisibleRegionID,
|
||
})
|
||
if err != nil {
|
||
return mutationResult{}, nil, err
|
||
}
|
||
|
||
return mutationResult{
|
||
snapshot: current.ToProto(),
|
||
roomPasswordHash: ¤tHash,
|
||
}, []outbox.Record{passwordEvent}, nil
|
||
})
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
return &roomv1.SetRoomPasswordResponse{
|
||
Result: commandResult(result.applied, result.snapshot.GetVersion(), s.clock.Now()),
|
||
Room: result.snapshot,
|
||
}, nil
|
||
}
|
||
|
||
func normalizeRoomPassword(locked bool, raw string) (string, string, error) {
|
||
password := strings.TrimSpace(raw)
|
||
if !locked {
|
||
return "", "", nil
|
||
}
|
||
if password == "" {
|
||
return "", "", xerr.New(xerr.InvalidArgument, "room password is required")
|
||
}
|
||
if utf8.RuneCountInString(password) > maxRoomPasswordRunes {
|
||
// 密码只服务入房校验,必须给 snapshot 和 command log 一个有界载荷。
|
||
return "", "", xerr.New(xerr.InvalidArgument, "room password is too long")
|
||
}
|
||
hash, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
|
||
if err != nil {
|
||
return "", "", err
|
||
}
|
||
return password, string(hash), nil
|
||
}
|
||
|
||
func roomPasswordMatches(hash string, password string) bool {
|
||
if hash == "" || password == "" {
|
||
return false
|
||
}
|
||
return bcrypt.CompareHashAndPassword([]byte(hash), []byte(password)) == nil
|
||
}
|