Add guest auth and enforce WS token identity#1
Conversation
新增游戏规则模块,引入房间阶段(active/voting/gameover)协议 扩展配置支持动态投票阈值、淘汰比例等游戏参数 在投票阶段禁止提交画作并添加倒计时广播 重构游戏结束条件检查以使用可配置规则
更新 n8n 工作流配置,引入分层重试策略(fast → quality → fallback)以提高可靠性和响应速度。 修改文档以说明新的配置选项和部署步骤。
|
@copilot 帮我review一下这次的改动是否合理 |
|
@delete-cloud I've opened a new pull request, #2, to work on those changes. Once the pull request is ready, I'll request review from you. |
There was a problem hiding this comment.
Pull request overview
Adds a guest-auth flow and enforces server-derived voter identity for Socket.IO votes, while also introducing server-driven room phase timing (active/voting/gameover) and configurable game/voting thresholds.
Changes:
- Add guest login endpoint (
/api/auth/guest/login) and persist guest identities/sessions in existing auth tables. - Require Socket.IO auth token at connect time; remove client-sent
voterIdand derive vote identity from the server-validated tokenuser_id. - Add phase timing protocol (
sync:statefields +phase:update) and new config-driven game rules/thresholds.
Reviewed changes
Copilot reviewed 19 out of 20 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
| n8n-workflow.json | Updates n8n workflow with fast→quality→fallback image generation and callbacks. |
| n8n-setup.md | Documents updated n8n workflow setup and env-driven model/size tuning. |
| frontend/src/types/index.ts | Removes voterId from generic vote payload typing. |
| frontend/src/types/battle.ts | Removes voterId from vote event payload typings (front→back), keeps server→client voter fields. |
| frontend/src/lib/api.ts | Adds guest login API + device/session identity helpers + localStorage token caching. |
| frontend/src/hooks/useWebSocket.ts | Adds authToken to WS handshake auth; stops emitting client voterId. |
| frontend/src/hooks/useBattleSystem.ts | Stops sending voterId in emitted vote events. |
| frontend/src/app/game/page.tsx | Initializes guest auth on game page, passes WS auth token, sets player id from auth user id. |
| backend/src/ws/socketio_handler.rs | Adds WS auth middleware, enforces authenticated vote identity, adds phase tick + phase update signaling, tweaks endgame logic. |
| backend/src/ws/mod.rs | Exposes new game_rules module. |
| backend/src/ws/game_rules.rs | Adds helper functions for ratio-based thresholds and AI overflow logic (with unit tests). |
| backend/src/services/auth.rs | Adds guest-device login provider and session issuance; includes openid normalization + tests. |
| backend/src/routes/themes.rs | Allows reusing rooms in active or voting status for a theme. |
| backend/src/routes/drawings.rs | Rejects drawing submission during voting; uses config-driven vote threshold. |
| backend/src/routes/auth.rs | Adds guest login request/response and route handler. |
| backend/src/models/room.rs | Makes vote threshold configurable via Config. |
| backend/src/main.rs | Registers Socket.IO namespace with auth middleware; adds guest login route. |
| backend/src/config.rs | Adds env-configurable game/voting parameters and durations. |
| backend/WS_PHASE_PROTOCOL.md | Documents WS phase protocol (sync:state, phase:update) and client countdown approach. |
| .gitignore | Adds ignore entry for backend/ARCHITECTURE.md. |
Comments suppressed due to low confidence (1)
backend/src/ws/socketio_handler.rs:388
check_game_end的函数注释仍描述了已删除/变更的规则(例如 total_items<=5 的前置检查、固定阈值 3/5/5 等),但实现已经改为基于game_rules和配置比例计算。建议同步更新注释(或删除过期的规则说明),避免文档与行为不一致。
/// 检查游戏结束条件
///
/// 游戏结束条件:
/// 1. 前置检查: total_items <= 5 时不检查(还没有 AI 鱼出现)
/// 2. 失败条件: 杀了 3 条非 AI 鱼
/// 3. 失败条件: AI 鱼数量 > 5
/// 4. 胜利条件: AI 全灭 + 人类 >= 5
async fn check_game_end(socket: &SocketRef, state: &AppState, room: &Room) {
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| impl Room { | ||
| /// 计算动态投票阈值 (在线人数的 30%) | ||
| pub fn vote_threshold(&self) -> i32 { | ||
| let dynamic = ((self.online_count as f64) * 0.3).ceil() as i32; | ||
| std::cmp::max(4, dynamic) | ||
| pub fn vote_threshold(&self, config: &Config) -> i32 { | ||
| let ratio = config.vote_threshold_ratio.clamp(0.0, 1.0); | ||
| let dynamic = ((self.online_count as f64) * ratio).ceil() as i32; | ||
| std::cmp::max(config.vote_min_threshold, dynamic) |
There was a problem hiding this comment.
Room::vote_threshold 的注释仍写“在线人数的 30%”,但实现已改为从 Config.vote_threshold_ratio 读取(默认 0.6)。建议更新注释以反映实际阈值来源/默认值,避免后续误用或调参时产生误解。
There was a problem hiding this comment.
Updated in 62f7080: comment now reflects config-driven ratio/min-threshold behavior for vote threshold calculation.
| check_game_end(socket, state, &room).await; | ||
| let refreshed: Room = sqlx::query_as("SELECT * FROM rooms WHERE id = $1") | ||
| .bind(room.id) | ||
| .fetch_one(&state.db) | ||
| .await | ||
| .ok()?; |
There was a problem hiding this comment.
check_game_end 里在将 room 置为 gameover 后会调用 emit_phase_update,但 phase_tick 在检测到 phase 变化时也会广播 phase:update。当投票超时触发 phase_tick 调用 check_game_end 时,这会导致重复的 phase:update(同一次状态切换 emit 两次)。建议统一由一个位置负责广播(例如移除 check_game_end 内的 emit,或让 check_game_end 返回是否已广播/更新后的 room 供 phase_tick 处理)。
There was a problem hiding this comment.
Updated in 62f7080: check_game_end now accepts a broadcast flag. phase_tick calls it with broadcast=false to avoid duplicate phase:update emissions on timeout transitions.
| /// 追击 (重复投同目标,增加票数) | ||
| async fn on_vote_chase( | ||
| socket: SocketRef, | ||
| Data(data): Data<BattleVoteCastData>, | ||
| state: SioState<Arc<AppState>>, | ||
| ) { | ||
| info!("[Socket.IO] Vote chase: {:?}", data); | ||
|
|
||
| // 检查游戏结束条件 | ||
| check_game_end(&socket, &state, &room).await; | ||
| } | ||
| let payload = serde_json::json!({ | ||
| "reason": "chase_disabled", | ||
| "fishId": data.fish_id | ||
| }); | ||
| let _ = socket.emit("vote:error", &payload); | ||
| let _ = state; | ||
| } |
There was a problem hiding this comment.
vote:chase 当前被禁用但仍注册 handler,并通过 let _ = state; 来消除未使用变量告警。这会让协议层面表现为“支持该事件但永远报错”,且增加维护噪音。建议:要么移除 socket.on("vote:chase", ...) 注册/前端事件类型;要么保留但将 handler 签名改为不接收 state(或加 feature flag)并在协议文档中明确弃用。
There was a problem hiding this comment.
Updated in 62f7080: removed the unused state extractor from vote:chase handler, kept event registration for backward compatibility, and documented that chase is currently disabled with chase_disabled response.
| fn normalize_guest_openid(device_id: Option<&str>) -> String { | ||
| if let Some(device_id) = device_id { | ||
| let trimmed = device_id.trim(); | ||
| if !trimmed.is_empty() { | ||
| return trimmed.chars().take(120).collect(); | ||
| } | ||
| } | ||
|
|
||
| format!("guest:{}", Uuid::new_v4()) | ||
| } |
There was a problem hiding this comment.
normalize_guest_openid 直接将客户端提供的 device_id 作为 openid(仅 trim/截断)。由于 device_id 并非秘密,攻击者可以伪造相同 device_id 来登录并获得同一 guest 用户身份,从而冒用投票身份。建议至少对 device_id 做服务端加盐哈希后再落库(保持稳定但不可被猜测/复用),或改为首次登录由服务端签发不可预测的 device token(客户端持久化该 token)。
There was a problem hiding this comment.
Updated in 62f7080: guest login now relies on server-issued random deviceToken identifiers (guest_device:UUID). Plain client-provided device_id values are no longer accepted as identity keys.
| { | ||
| "parameters": { | ||
| "jsCode": "const input = $json;\n\nconst theme = input.theme || {};\nconst keywords = Array.isArray(theme.keywords) ? theme.keywords : [];\nconst palette = Array.isArray(theme.palette) ? theme.palette : [];\nconst promptStyle = typeof theme.prompt_style === 'string' ? theme.prompt_style : 'stylized illustration';\n\nconst keyword = keywords.length ? keywords[Math.floor(Math.random() * keywords.length)] : 'fish';\nconst colors = palette.slice(0, 3).join(', ');\n\nconst modelFast = process.env.ZENMUX_MODEL_FAST || 'nano banana pro';\nconst modelQuality = process.env.ZENMUX_MODEL_QUALITY || modelFast;\nconst modelFallback = process.env.ZENMUX_MODEL_FALLBACK || 'google/gemini-3-pro-image-preview';\n\nconst sizeFast = process.env.ZENMUX_SIZE_FAST || '256x256';\nconst sizeQuality = process.env.ZENMUX_SIZE_QUALITY || '512x512';\nconst sizeFallback = process.env.ZENMUX_SIZE_FALLBACK || sizeQuality;\n\nconst promptFast = `A ${promptStyle} of a ${keyword}. Simple clean background. High contrast, crisp silhouette. Colors: ${colors}. No text, no watermark, no logo.`;\nconst promptQuality = `A ${promptStyle} of a ${keyword}. Highly detailed but clean silhouette, crisp edges, soft shading. Simple background. Colors: ${colors}. No text, no watermark, no logo.`;\n\nconst name = keyword;\nconst description = `${promptStyle}`.slice(0, 60);\n\nreturn [{\n json: {\n ...input,\n keyword,\n name,\n description,\n model_fast: modelFast,\n model_quality: modelQuality,\n model_fallback: modelFallback,\n size_fast: sizeFast,\n size_quality: sizeQuality,\n size_fallback: sizeFallback,\n prompt_fast: promptFast,\n prompt_quality: promptQuality,\n }\n}];\n" | ||
| }, | ||
| "id": "prepare", | ||
| "name": "Prepare", | ||
| "type": "n8n-nodes-base.code", |
There was a problem hiding this comment.
该 PR 标题/描述聚焦“guest auth + WS token identity”,但这里引入了较大幅度的 n8n workflow 变更(新的 fast/quality/fallback 分支、Zenmux endpoint 等),PR 描述未提及。建议在 PR 描述中补充该变更的动机/影响与验证方式,或将 workflow 调整拆分到独立 PR,便于回溯与风险评估。
There was a problem hiding this comment.
Updated PR description to call out the pre-existing n8n workflow/docs changes on this branch and their scope relative to the auth/ws fixes.
|
@copilot please re-review the latest commit 62f7080 (addressed all previous inline findings). |
|
@delete-cloud I've opened a new pull request, #3, to work on those changes. Once the pull request is ready, I'll request review from you. |
Summary
Verification