Files
huajishe-tts/docs/texas/12-auth-kick-single-room.md
e2hang 04d0af515c feat: 登录系统落地 + 踢人修正 + 单房间约束 + 权限收口
问题8 登录系统:
- node:sqlite 账号存储(scrypt 哈希 + HMAC 签名 token),preset admin 改为 e2hang/evan1115
- /api/auth/register|login|me,me 返回 room 供重联
- 网关拦截 create/join 需登录(401),列表保持开放

踢人逻辑修正(问题2 延伸):
- kickMove 真正释放座位(folded=false, name=null)而非锁死
- war/mill 去掉永久封禁语义

管理员越权:管理员 token 可删任意房间、踢任意房间内的人

单房间约束(用户新需求):
- 同一用户同时只能在一个房间,create/join 他房返回 409
- 踢人后若房间仅剩踢人者则自动解散并释放其记录(修复 2 人局踢光对手后被困 409)
- 陈旧房间记录兜底清理

UI 清理:移除"改名"按钮与"你的名字"输入框;删除房间按钮仅房主/管理员可见

docs: 新增 12-auth-kick-single-room.md 汇总本会话全部需求/根因/修复/边界测试

Co-Authored-By: Claude (CodeBuddy) <noreply@codebuddy.ai>
2026-08-25 14:13:59 +08:00

11 KiB
Raw Permalink Blame History

问题 8/2 延伸:登录系统落地 + 踢人修正 + 单房间约束 + 权限收口

本文档汇总本会话内围绕「登录身份(问题 8」「踢人逻辑问题 2 延伸)」「单房间约束(用户新需求)」 「管理员越权」「UI 清理」所做的工作:用户原始要求、发现的问题、根因分析、修复方案、验证结果。 配套的代码改动已落地于 packages/serverpackages/enginepackages/uiapps/web


一、用户提出的要求(按时间顺序)

  1. 预设管理员账号:指定 preset admin 账号 e2hang / 密码 evan1115(取代原默认 admin/admin123)。
  2. 清理测试账号:删除遗留的 alice 测试用户DB 只保留 admin。
  3. 踢人逻辑修正:踢人后要把空位置留出来,而不是写一个"伪弃牌"把座位锁死(之前 kickMovefolded=true 导致该座位谁也坐不进去)。
  4. 管理员越权:管理员(以 admin 身份登录)可以删除任意房间、在任意房间踢出任何人,不受"必须是房主"限制。
  5. 普通用户进房 + 按钮权限:普通用户必须能进入房间;只有房主和管理员有"踢人"按钮,普通用户没有。
  6. 单房间约束 + 自动重连:同一登录用户不能同时进入两个房间;每次重连自动进入他所在的那个房间。
  7. UI 清理:去掉"改名"按钮和"你的名字"输入框;普通用户(非房主非管理员)不显示"删除房间"按钮。
  8. 踢人后空房解救(后续 bug 修复2 人局踢掉唯一对手后,房主被困在空房、无法创建新房间(报 409需要让空房自动解散并释放房主。

二、现状与根因

2.1 登录系统(问题 8

  • 完成了 auth-db.ts(账号存储 + scrypt 哈希 + HMAC 签名 token+ auth.ts/api/auth/register|login|me)。
  • token 用 base64url(JSON{u,exp}).signature 格式HMAC-sha256 签名;TOKEN_TTL_MS = 30 天
  • 账号用 node:sqlite 落盘Node 22 内置无原生编译DB 文件 packages/server/data/tts-like.db;之前计划的内存 Map 改为 SQLite跨重启保留
  • 预设 adminauth-db.tsPRESET_USERS 改为 { username: 'e2hang', password: 'evan1115', role: 'admin' }(支持 AUTH_ADMIN_USER / AUTH_ADMIN_PASS 环境变量覆盖),首次启动 INSERT OR IGNORE 注入。

2.2 踢人逻辑错误(问题 2 延伸)

  • 根因holdem.tskickMove 在被踢者上设了 tp.folded = true。UI 中"踢人"按钮条件、以及座位是否可加入的判断,会把这个 folded=true 的座位当成"已占用/已弃牌",导致该位置既显示有人又无法被新玩家 join
  • 修复kickMove 改为真正释放座位——
    tp.seated = false;
    tp.inHand = false;
    tp.folded = false;   // 原来是 true → 锁死座位
    tp.allIn  = false;
    tp.name   = null;    // 座位显示为"空"
    tp.stack  = 0;
    
    war.ts / mill.tskick move 也去掉了"永久封禁"语义(G.kicked[target]=true 改为无副作用占位),并移除各 move 里 if (G.kicked?.[me]) return; 的守卫。
  • 效果:踢人后座位变空,新玩家可正常进入(带新 credentials。已单测验证holdem.test.ts 期望 folded===falsename===null)。

2.3 管理员越权

  • rooms.tsdelete / kick 处理器新增 isAdmin(app, token) 判定:携带有效 token 且 role==='admin' 时,跳过"必须是房主"校验。
  • token 取自 Authorization: Bearer <token>?token=(与 auth.ts 同款解析,避免直接依赖 auth.tsnode:sqlite 链,防止 vitest 解析失败)。

2.4 普通用户进房 + 按钮权限

  • 进房:服务端 lobby REST 本来就允许普通用户 list+join/api 与 vite 代理均 200 验证)。早前"普通用户进不去"是因为生产 dist 是旧版、缺登录 UI 导致无 token 被网关 401重建 dist 后解决。
  • 踢人按钮可见性LobbyRoomList
    const canKick = isAdmin || (isCurrent && currentPlayerId === m.createdBy);
    
    房主createdBy 且是本人座位)管理员可见"踢"按钮;普通用户看不到。
  • 管理员"删"按钮:每个房间 pill 对 isAdmin 渲染"删"按钮(确认后 roomsDelete 带 token不需要房主 credentials

2.5 单房间约束(用户新需求)

  • 根因/设计:需要阻止"同一登录用户同时处于两个房间",且重连时自动回原房间。
  • 服务端实现rooms.ts,在 mountApiPrefix 之前注册,以保留 /api/games/... 原始路径):
    • 内存 MapuserRoom用户名→matchIDroomPlayersmatchID→playerID→用户名
    • 中间件拦截 create / join / leave
      • create/join 前:若 userRoom.get(username) 指向别的房间409(中文提示"先离开才能创建/加入其他房间")。
      • 成功后(await next 之后):recordUserRoom 登记;create 的 matchID 从 ctx.body.matchID 读取。
      • leave 成功后:清 userRoom / roomPlayers 对应记录。
    • kick / delete 处理器内:清被踢用户 / 全房间的单房间记录。
  • 自动重连
    • 客户端 configmatchID/playerID/credentialslocalStorage 持久化,刷新即自动重连原房间。
    • 服务端 /api/auth/me 新增返回 room(来自 getUserRoom),供客户端重联时权威确认所在房间(已通过 setupAuth({ getUserRoom }) 注入)。
    • 客户端 LobbyRoomList.errMsg() 把 409 的 error 文案友好展示。

2.6 踢人后空房解救(关键 bug 修复)

  • 现象2 人局,房主踢掉唯一对手后,房间列表里房间还在(房主自己仍 seated 有 nameme.room 仍指向它,于是房主再 create409 "你已在房间 X 中"。用户感知为"房间消失但被卡住"。
  • 根因:单房间约束只认 userRoom 记录,而空房里房主仍被记录为"在房内",无退出途径(除非手动"退出房间",但用户没意识到自己还在房间里)。
  • 修复rooms.ts kick 处理器第 4 步):踢人后检查"除踢人者本人外是否还有其他人就座metadata 里有 name"
    • othersSeated === false(房间只剩踢人者自己/空了)→ 视为对局已散,server.db.wipe(matchID) + 清 presence/createdBy/roomPlayers释放踢人者的单房间记录
    • 于是 2 人局踢掉最后一人 → 房间消失、房主 me.room 清空、可立即创建新房200
    • 3 人及以上局踢掉一人但还有别人 → 房间保留,符合预期。
  • 陈旧记录兜底create/join 前先 roomExists(server, existing) 检查 userRoom 指向的房间是否还存在(如被管理员删),不存在则清掉陈旧记录,避免"房间没了却卡 409"。

2.7 UI 清理

  • 移除 LobbyRoomList 的"改名"按钮 + renameMe callback + onRename prop引擎层 renameMove 保留,仅去 UI 入口)。
  • 移除 App.tsx 联机模式下的"你的名字"输入框(playerName 状态保留登录时自动用登录名填充join/create 时作玩家名)。
  • 删除房间按钮可见性:新增 showDelete = currentMatchId && (isAdmin || isCurrentHost),仅房主/管理员可见;普通用户看不到。
  • web dist 重建确认:"你的名字"已从产物移除;"改名" UI 已移除(引擎日志里的"X 改名"字样属后端逻辑,非按钮)。

三、关键架构事实(本会话确认)

  • mountApiPrefix 会把 /api 前缀剥离;rooms.ts 的自定义中间件必须在 mountApiPrefix 之前注册,才能在 ctx.path 仍是 /api/games/... / /api/rooms/... 时拦截。
  • boardgame.io lobby 路由(已对 server.js 核实):
    • POST /games/:name/create → body 返回 { matchID }create 成功后才能拿到 matchID
    • POST /games/:name/:id/join → body { playerID, playerName }
    • POST /games/:name/:id/leave → body { playerID, credentials }
  • node:sqlite 在 vitest 下无法被 transform 解析;rooms.tsAuthDB 改用 import type(编译期擦除)+ 本地内联 extractToken / usernameFromCtx,避免运行时拉入 node:sqlite(否则 server 单测 12 例会崩)。
  • 踢人"释放 lobby 座位"server 直接改 metadata.players[target].name/credentials = undefined(镜像原生 leave),再 best-effort 经 Master dispatch 调游戏内 kick move回合制游戏守卫可能 no-op但 metadata 已权威释放座位)。

四、边界测试(端到端,对运行中的 :8000

Case 场景 结果
1 3 人局踢 1 人剩 2 人 座位清空、房间保留
2 2 人局踢唯一对手 房间解散(404)、me.room 清空、房主可开新房(200)
3 踢空座位(未就座) 200、无副作用、房间保留
4 非房主/非管理员踢人 403、座位不变
5 被踢者重进同一座位 重进 200、成功补位
6 踢自己target=playerID 400 invalid target、房间保留
7 3 人就座连续踢 2 人剩房主 房间解散(404)、me.room 清空
8 管理员 token 跨房踢人 200、目标移出、房间保留
9 无效 target座位 9 200、无副作用
10 房主带错 credentials 403、座位不变
11 单房间:在 A 房时 create/join B 房 409中文提示
12 在 A 房时 join A 房另一座位 200同房间允许
13 leave A 房后 me.room 清空、可再次 create(200)
14 房间被外部删除后 create 陈旧兜底释放、create 200

五、验证与质量门禁

  • pnpm -r ts:全 workspace 类型检查通过。
  • engine 测试 92 passed(含 holdem.test.ts 踢人/单测kick 期望 folded===falsename===null)。
  • server 测试 12 passed:原 2 个踢人测试因"2 人局踢后房间不再保留"改为断言房间解散(404);新增 3 人局变体断言房间保留 + 非房主 403。
  • web dist 重建(vite build),确认登录 UI、409 文案、单房间约束均已生效。
  • 所有临时测试账号清理,packages/server/data/tts-like.db 仅留 e2hang(admin)。
  • server 以 tsx watch 运行,源码改动热加载,无需手动重启。

六、已知限制 / 后续建议

  • userRoom / roomPlayers / presence内存态server 重启即清空(与 lobby metadata 同为 InMemory保持一致。重启后用户仍可凭 localStorage 里的 config 自动重连;此时 server 不记得该用户的"单房间"记录,但因客户端仍有 credentials不会误拦陈旧兜底进一步保护
  • 若需"server 重启后也记住谁在哪个房间",需把 userRoom 也落盘(如 SQLite 表),但这与 lobby metadata 的 InMemory 不一致,建议保持现状。
  • 踢人后"房间解散"的判断基于"除踢人者外无他人就座";若希望房主踢光对手后保留空房等新人,可改为仅释放房主记录而不 wipe——当前按用户反馈选了"解散 + 释放"。