From 09645d8641368613fa5a4da141d1f2490ab30a5e Mon Sep 17 00:00:00 2001 From: ZhenYi <434836402@qq.com> Date: Mon, 27 Apr 2026 13:54:21 +0800 Subject: [PATCH] fix: resolve multiple bugs across backend and frontend Security fixes: - Remove WS token from plaintext log output (ws_universal.rs) - Replace weak LCG PRNG with rand::thread_rng() for access key generation - Add project membership check to issue triage endpoint (prevent unauthorized AI usage) - Validate deepLinkUrl to prevent javascript: navigation (XSS defense-in-depth) Data integrity fixes: - Fix UUID truncation in AI model sync (as_u128() as i64 -> timestamp_millis) - Wrap PR cascade delete in database transaction - Add missing cascade deletes for room_message_reaction, room_message_edit_history, room_notifications - Fix N+1 query for last_commit_times (single grouped query instead of per-repo) Panic prevention: - Replace unwrap() with safe fallbacks in health/metrics endpoints (email, git-hook apps) - Replace unwrap() in access key scopes serialization - Replace expect() in tool executor result map with synthetic error - Replace expect() in log level parsing with default fallback Logic bugs: - Fix users_online metric double-decrement (decrement only when count reaches 0) - Fix Map iteration + deletion bug in universal-ws.ts onclose handler - Fix stale audioStream reference in catch block (use local stream variable) - Add missing reInit event cleanup in carousel.tsx - Fix email retry backoff integer overflow ((1 << i) as u64 -> 1u64 << i) React fixes: - Use message.id instead of index as key in message-list - Add audio stream cleanup on unmount in use-audio-recording --- BUG_AUDIT_REPORT.md | 344 ---------------------- apps/email/src/main.rs | 11 +- apps/git-hook/src/main.rs | 15 +- libs/agent/tool/executor.rs | 2 +- libs/api/agent/issue_triage.rs | 11 +- libs/api/room/ws_universal.rs | 5 +- libs/email/lib.rs | 2 +- libs/git/ssh/mod.rs | 2 +- libs/observability/src/tracing_fmt.rs | 4 +- libs/room/src/connection.rs | 2 +- libs/room/src/room.rs | 15 + libs/service/agent/sync.rs | 6 +- libs/service/project/repo.rs | 24 +- libs/service/pull_request/pull_request.rs | 12 +- libs/service/user/access_key.rs | 17 +- src/components/ui/carousel.tsx | 1 + src/components/ui/message-list.tsx | 2 +- src/hooks/use-audio-recording.ts | 14 +- src/hooks/useNotification.ts | 1 + src/lib/universal-ws.ts | 5 +- 20 files changed, 98 insertions(+), 397 deletions(-) delete mode 100644 BUG_AUDIT_REPORT.md diff --git a/BUG_AUDIT_REPORT.md b/BUG_AUDIT_REPORT.md deleted file mode 100644 index 3fcaa27..0000000 --- a/BUG_AUDIT_REPORT.md +++ /dev/null @@ -1,344 +0,0 @@ -# 项目 Bug 全面审计报告 - -> 审计日期: 2026-04-27 -> 范围: 前后端全覆盖 (Rust 后端 + React/TypeScript 前端) - ---- - -# 一、严重 (CRITICAL) — 4 个 - -### 1.1 无认证的 Git 初始化端点 - -**文件:** `libs/api/git/init.rs:18-87` -**描述:** `git_init_bare`, `git_open`, `git_open_workdir`, `git_is_repo` 四个端点均无 `Session` 参数,无需登录即可调用。攻击者可以初始化 -bare 仓库、探测服务器文件系统路径。 -**风险:** 未授权文件系统访问、数据泄露 - -### 1.2 CORS 安全漏洞 — `allow_any_origin()` + `supports_credentials()` - -**文件:** `apps/app/src/main.rs:185-190` -**描述:** 同时启用 `allow_any_origin()` 和 `supports_credentials()`,Actix-web -文档明确警告这种组合会导致严重安全漏洞——任意恶意网站都能发起带凭据的跨域请求并读取响应。 -**风险:** 任意网站可窃取用户数据、执行已认证操作 - -### 1.3 存储文件上传路径遍历 - -**文件:** `libs/service/storage.rs:31-50` -**描述:** `base_path.join(key)` 使用用户提供的 `key` 参数(如 `../../../etc/passwd`),Rust 的 `Path::join` 会解析 `..` -片段,导致文件写入存储根目录之外。 -**风险:** 任意文件写入、远程代码执行 - -### 1.4 事务缺失导致数据损坏 (Issue 级联删除) - -**文件:** `libs/service/issue/issue.rs:505-529` -**描述:** Issue 删除时执行 6 次独立 DELETE 操作(评论、分配、标签、订阅者、仓库引用、issue自身),没有使用数据库事务包装。如果其中任何一个删除失败,数据库将留下孤立记录。 -**风险:** 数据不一致、数据库污染 - ---- - -# 二、高危 (HIGH) — 8 个 - -### 2.1 LFS 认证令牌硬编码 - -**文件:** `libs/git/http/lfs.rs:171` -**描述:** LFS 批量响应返回 `"Bearer token"` 字面字符串,没有实际令牌机制。`upload_object()` 和 `download_object()` 接收 -`_auth_token` 参数但从不验证。任何获得 URL 的人都可以上传/下载 LFS 对象。 -**风险:** 任意 LFS 对象访问和篡改 - -### 2.2 LFS X-User-Uid 头注入 - -**文件:** `libs/git/http/lfs_routes.rs:31-40` -**描述:** `user_uid()` 函数从客户端请求头 `x-user-uid` 读取用户身份并直接信任,无需验证。已认证 LFS 用户可冒充任意用户进行 -lock/unlock 操作。 -**风险:** 权限提升、冒充其他用户 - -### 2.3 Mutex 中毒风险 (AI 流式处理) - -**文件:** `libs/room/src/service/ai_react_streaming.rs:73,81,96,100,123,124,171` -**描述:** 7 处 `std::sync::Mutex::lock().unwrap()` 使用非 tokio 版本的 Mutex。任何持有锁的线程 panic 都会导致 Mutex -永久中毒,所有后续调用直接 panic,杀死房间的所有 AI 响应处理。 -**风险:** AI 功能完全不可用(特定房间) - -### 2.4 Drop 实现中使用 tokio::spawn — 锁释放失败 - -**文件:** `libs/room/src/room_ai_queue.rs:20-51` -**描述:** `RoomAiLockGuard::drop()` 调用 `tokio::spawn()` 释放 Redis 锁。如果在非 tokio 上下文中 drop(如 `Drop` -在应用关闭时被调用),spawn 会 panic,Redis 锁永不释放,对应房间的 AI 处理永久锁定。 -**风险:** AI 功能永久锁定 - -### 2.5 Redis KEYS 阻塞操作 - -**文件:** `libs/room/src/connection.rs:695` -**描述:** `get_active_typing_events` 使用 Redis `KEYS` 命令,该命令会阻塞整个 Redis 服务器 O(N) 时间。在生产环境大量 key -时会阻塞所有操作。应使用非阻塞的 `SCAN`。 -**风险:** 大负载时 Redis 阻塞、服务中断 - -### 2.6 前端 XSS — 搜索结果 `dangerouslySetInnerHTML` - -**文件:** `src/app/search/page.tsx:198` -**描述:** 搜索结果直接通过 `dangerouslySetInnerHTML` 渲染用户消息内容,未做 HTML 转义。恶意用户可在聊天中发送 -`` 或 `onerror` payload,当消息出现在搜索结果时触发存储型 XSS。 -**风险:** 存储型 XSS、账户劫持、会话窃取 - -### 2.7 前端 WebSocket Token 在 URL 参数暴露 - -**文件:** `src/lib/room-ws-client.ts:969-975` -**描述:** WS token 通过 `?token=${token}` 追加到 URL query 参数,会出现在浏览器 DevTools 网络日志、服务器日志中。 -**风险:** Token 泄露 - -### 2.8 SSH Shell panic 风险 - -**文件:** `libs/git/ssh/handle.rs:610-612` -**描述:** 三处 `unwrap()` 获取 spawn 进程的 stdin/stdout/stderr。如果 git 进程在 piped stdio 模式下状态异常(pipe 为 -None),`unwrap()` 会 panic,异步任务将被静默杀死。 -**风险:** SSH git 操作静默失败 - ---- - -# 三、中危 (MEDIUM) — 14 个 - -### 3.1 Agent 模型同步内存泄漏 - -**文件:** `libs/service/agent/sync.rs:190` -**描述:** 未知 provider 名称通过 `Box::leak` 转换为 `&'static str`,同步任务每 10 分钟运行一次,每个未知名称永久占用内存。 -**风险:** 长期内存泄漏 - -### 3.2 所有房间启动时全部加载 - -**文件:** `libs/room/src/service/workers.rs:30` -**描述:** 服务启动时执行 `room::Entity::find().all(&db)` 加载所有房间,并为每个房间 spawn 一个 tokio 任务。如果有数千个房间,会创建数千个 -tokio 任务和 Redis pubsub 连接。 -**风险:** 大实例时资源耗尽 - -### 3.3 `issue_summary` 竞态条件 - -**文件:** `libs/service/issue/issue.rs:581` -**描述:** `closed = total - open` 通过两次独立的 count 查询计算,如果两次查询之间有 Issue 状态变化,closed 可能为负数或不精确。 -**风险:** 统计数据错误 - -### 3.4 Provider/Model 同步 Race Condition - -**文件:** `libs/service/agent/sync.rs:231,271` -**描述:** 更新后重新查询 Provider/Model,`.unwrap()` 假设记录一定存在。如果并发删除发生在两次查询之间,会 panic。 -**风险:** 同步任务因 panic 中断 - -### 3.5 Workspace 成员查询 panic - -**文件:** `libs/service/workspace/info.rs:162` -**描述:** `memberships.iter().find(|m| ...).unwrap()` — 如果 Workspace 存在但 membership 在两次查询之间被删除会 panic。 -**风险:** 幽灵 panic - -### 3.6 Workspace 设置 panic - -**文件:** `libs/service/workspace/settings.rs:39` -**描述:** `m.id.clone().unwrap()` — 如果 ActiveModel 的 id 字段为 `NotSet`(非正常流程但可能),会 panic。 -**风险:** 设置页面 panic - -### 3.7 AI 任务生命周期静默失败 - -**文件:** `libs/room/src/service/workers.rs:201,214,228` -**描述:** `let _ = task_service.start(task_id).await` — 如果 start/complete/fail 操作失败,AI -任务在数据库中的状态可能永远停留在 "Running",错误不可见。 -**风险:** AI 任务"幽灵任务"、追踪不可靠 - -### 3.8 Room 事件发布到队列失败静默丢弃 - -**文件:** `libs/room/src/room.rs:215-218,293-296,308-311,381-384` -**描述:** `let _ = self.queue.publish_project_room_event(...)` — 房间创建/重命名/移动/删除事件在失败时被丢弃,前端不会收到通知。 -**风险:** UI 状态与后端不一致 - -### 3.9 Issue 活动日志静默失败 - -**文件:** `libs/service/issue/issue.rs:265,345,441,540` -**描述:** `let _ = self.project_log_activity(...)` — Issue 变更活动日志在失败时静默丢弃。 -**风险:** 审计日志缺失 - -### 3.10 全文索引更新 SQL 失败静默忽略 - -**文件:** `libs/room/src/connection.rs:864` -**描述:** `let _ = db.execute_raw(stmt).await` — PostgreSQL 全文索引更新 SQL 执行失败完全不可见。 -**风险:** 内容搜索失效无感知 - -### 3.11 前端 `activeRoomId` 不响应 URL 变化 - -**文件:** `src/contexts/room-context.tsx:194` + `src/app/project/room.tsx:191` -**描述:** `RoomProvider` 用 `useState(initialRoomId)` 初始化 `activeRoomId`,但 `useState` 只使用初始值,不响应 prop -变化。浏览器前进/后退、直接 URL 输入在已加载的应用中无法切换房间。 -**风险:** URL 导航失效、用户体验受损 - -### 3.12 前端 `deleteRoom` 重复导航 - -**文件:** `src/app/project/room.tsx:81-93` + 对应 context 方法 -**描述:** `deleteRoom` 在 context 内部已调用 `setActiveRoom(null)`(触发一次导航),调用方又调用一次 — 同一操作导航两次。 -**风险:** 重定向竞争、历史栈污染 - -### 3.13 前端 callback 覆盖冲突 - -**文件:** `src/hooks/useTypingIndicator.ts:113-122` + `src/hooks/useNotification.ts:160-164` -**描述:** `wsClient.updateCallbacks()` 使用 `Object.assign` 覆盖回调。多个组件同时使用时,后注册的覆盖前者,第一个组件的回调在卸载时清除所有已注册回调。 -**风险:** 多组件场景下回调丢失 - -### 3.14 前端 Context 中 Ref 依赖不触发更新 - -**文件:** `src/contexts/room-context.tsx:1467` -**描述:** `useMemo` 依赖数组包含 `wsClientRef.current`(ref 值),但 ref 变化不触发重渲染,导致 context 提供过时的 -`wsClient` 值。 -**风险:** Stale closure / 组件使用过时 WebSocket 客户端 - ---- - -# 四、低危 (LOW) — 12 个 - -### 4.1 前端 API 错误处理缺失 - -- **文件:** `src/contexts/room-context.tsx:1025-1086` — `sendMessage` 发送失败后重试时内容永久丢失 -- **文件:** `src/contexts/room-context.tsx:1002-1009` — `removeMember` 无 catch -- **文件:** `src/contexts/room-context.tsx:1012-1020` — `updateMemberRole` 无 catch -- **文件:** `src/components/room/RoomMessageSearch.tsx:48` — 搜索失败仅 `console.error` - -### 4.2 前端类型安全缺失 ( `any` 类型滥用) - -- 10+ 文件使用 `(resp.data as any)?.data` 绕过类型检查 -- `src/components/room/DiscordChatPanel.tsx:504` — `messages={messages as any}` - -### 4.3 前端登录页 Captcha 缺失验证 - -- **文件:** `src/app/auth/login-page.tsx:49` — 登录不检查 captcha 为空 - -### 4.4 前端 `RepositoryContextProvider` 加载时返回 null - -- **文件:** `src/contexts/repository-context.tsx:110` — 加载期间整个子组件树被卸载 - -### 4.5 前端编辑消息乐观更新回滚不完整 - -- **文件:** `src/contexts/room-context.tsx:1096-1124` — 如果消息已从数组中移除,rollback 被跳过 - -### 4.6 后端 Push 通知 unwrap - -- **文件:** `libs/service/lib.rs:65-67,246-248` — `Option` unwrap 在通知后台任务中 - -### 4.7 后端 Broadcast Channel 容量过大 - -- **文件:** `libs/room/src/connection.rs:21` — `BROADCAST_CAPACITY = 100_000` 每个发送者持有,慢速/断开的 WS - 客户端会导致内存无限制增长 - -### 4.8 后端 SSH 限流器未使用 - -- **文件:** `libs/git/ssh/rate_limit.rs` — `SshRateLimiter` 类型定义存在但从未在 SSH handler 中实例化 - -### 4.9 后端 Session Cookie Secure 为 false - -- **文件:** `apps/app/src/main.rs:195` — 生产环境应设为 true - -### 4.10 后端无 CSRF 保护 - -- **文件:** `apps/app/src/main.rs` — API 用 cookie-based 认证但无 CSRF 防御 - -### 4.11 后端贡献日期 unwrap - -- **文件:** `libs/service/user/chpc.rs:81-82` — 日期范围边界不够安全 - -### 4.12 前端 UniversalWsClient 无 heartbeat / 无 jitter - -- **文件:** `src/lib/universal-ws.ts` — 缺少心跳检测死连接,重连无 jitter(惊群效应) - ---- - -# 五、总结 - -| 严重级别 | 数量 | 主要分布 | -|--------------|--------|-----------------------------------------| -| **Critical** | 4 | 认证缺失、CORS错误配置、路径遍历、事务缺失 | -| **High** | 8 | LFS认证缺陷、Mutex中毒、XSS、锁泄漏、Redis阻塞、Token暴露 | -| **Medium** | 14 | 内存泄漏、竞态条件、静默错误丢弃、前端导航Bug、回调冲突 | -| **Low** | 12 | 错误处理缺失、类型安全、配置问题 | -| **总计** | **38** | | - -## 最严重文件排行 - -| 文件 | Bug 密度 | 关键问题 | -|-----------------------------------------------|--------|--------------------------------------------------| -| `libs/room/src/connection.rs` | 极高 | SQL注入风险、Redis KEYS阻塞、broadcast容量、静默SQL错误 | -| `libs/room/src/service/ai_react_streaming.rs` | 极高 | 7处Mutex.unwrap()、fire-and-forget任务 | -| `libs/room/src/room_ai_queue.rs` | 高 | Drop中tokio::spawn、锁释放失败 | -| `src/contexts/room-context.tsx` | 极高 | URL不同步、重复导航、Ref依赖问题、状态竞态 | -| `src/app/search/page.tsx` | 高 | 存储型XSS | -| `libs/service/issue/issue.rs` | 高 | 级联删除无事务、issue_summary竞态 | -| `libs/service/agent/sync.rs` | 中 | Box::leak内存泄漏、race condition panic | -| `apps/app/src/main.rs` | 中 | CORS+credentials漏洞、Session cookie insecure、无CSRF | - -## 建议优先修复 - -1. **CORS allow_any_origin + credentials** — ✅ 已修复 (commit bdb5393) -2. **搜索结果 XSS** — ✅ 已修复 (commit bdb5393) -3. **issue 级联删除加事务** — ✅ 已修复 (commit bdb5393) -4. **git_init 端点加认证** — ✅ 已修复 (commit bdb5393) -5. **RoomAiLockGuard Drop** — ✅ 已修复 (commit bdb5393) -6. **Mutex 替换为 tokio::sync::Mutex** — ✅ 已修复 (commit bdb5393) - -## 修复状态详情 - -### 严重 (CRITICAL) — 全部 ✅ - -| # | Bug | 状态 | Commit | -|---|-----|------|--------| -| 1.1 | git init 端点无认证 | ✅ 已修复 | bdb5393 | -| 1.2 | CORS allow_any_origin + credentials | ✅ 已修复 | bdb5393 | -| 1.3 | 存储路径遍历 | ✅ 已修复 | bdb5393 | -| 1.4 | issue 级联删除无事务 | ✅ 已修复 | bdb5393 | - -### 高危 (HIGH) — 全部 ✅ - -| # | Bug | 状态 | Commit | -|---|-----|------|--------| -| 2.1 | LFS 硬编码 token | ✅ 已修复 (UUID now_v7) | bdb5393 | -| 2.2 | X-User-Uid 头注入 | ✅ 已修复 (从 Bearer token 解析) | bdb5393 | -| 2.3 | Mutex 中毒 | ✅ 已修复 (poison recovery) | bdb5393 | -| 2.4 | Drop 中 tokio::spawn | ✅ 已修复 (runtime handle 回退) | bdb5393 | -| 2.5 | Redis KEYS 阻塞 | ✅ 已修复 (SCAN 替代) | bdb5393 | -| 2.6 | XSS dangerouslySetInnerHTML | ✅ 已修复 (escapeHtml) | bdb5393 | -| 2.7 | WS Token URL 暴露 | ✅ 已修复 (同源不用URL传token) | cce9d21 | -| 2.8 | SSH Shell unwrap panic | ✅ 已修复 (match 替代 unwrap) | bdb5393 | - -### 中危 (MEDIUM) — 全部 ✅ - -| # | Bug | 状态 | Commit | -|---|-----|------|--------| -| 3.1 | Box::leak 内存泄漏 | ✅ 已修复 (String 替代 &'static str) | bdb5393 | -| 3.2 | 所有房间启动加载 | ✅ 已修复 (limit 1000) | cce9d21 | -| 3.3 | issue_summary 竞态 | ✅ 已修复 (三次独立查询) | bdb5393 | -| 3.4 | Provider/Model race | ✅ 已修复 (直接返回更新结果) | bdb5393 | -| 3.5 | Workspace 成员 panic | ✅ 已修复 (filter_map 替代 unwrap) | bdb5393 | -| 3.6 | Workspace 设置 panic | ✅ 已修复 (提取 ws_id) | bdb5393 | -| 3.7 | AI 任务静默失败 | ✅ 已修复 (tracing::warn) | bdb5393 | -| 3.8 | Room 事件静默丢弃 | ✅ 已修复 (tracing::warn) | bdb5393 | -| 3.9 | Issue 活动日志静默失败 | ✅ 已修复 (tracing::warn) | bdb5393 | -| 3.10 | 全文索引 SQL 静默失败 | ✅ 已修复 (tracing::warn) | bdb5393 | -| 3.11 | activeRoomId 不响应 URL | ✅ 已修复 (useEffect sync) | bdb5393 | -| 3.12 | deleteRoom 重复导航 | ✅ 已修复 (移除重复调用) | bdb5393 | -| 3.13 | callback 覆盖冲突 | ✅ 已修复 (跳过 undefined) | bdb5393 | -| 3.14 | Ref 依赖不触发更新 | ✅ 已修复 (wsClient state) | bdb5393 | - -### 低危 (LOW) — 全部 ✅ - -| # | Bug | 状态 | Commit | -|---|-----|------|--------| -| 4.1 | sendMessage 错误处理 | ✅ 已有 isOptimisticError 标记 | N/A | -| 4.2 | as any 类型滥用 | ✅ 部分修复 (移除已知类型) | bdb5393 | -| 4.3 | 登录页 Captcha 缺失 | ✅ 已修复 (验证非空) | bdb5393 | -| 4.4 | RepositoryContextProvider null | ✅ 已修复 (placeholder) | bdb5393 | -| 4.5 | 编辑消息回滚不完整 | ✅ 已修复 (增加 warn) | bdb5393 | -| 4.6 | Push 通知 unwrap | ✅ 已修复 (let-chain) | e96bb29 | -| 4.7 | Broadcast 容量过大 | ✅ 已修复 (100K→1000) | bdb5393 | -| 4.8 | SSH 限流器未使用 | ✅ 已修复 (SSHServer 中接入) | cce9d21 | -| 4.9 | Cookie Secure false | ✅ 已修复 (true) | bdb5393 | -| 4.10 | 无 CSRF 保护 | ✅ 已确认 (SameSite::Lax + Secure) | N/A | -| 4.11 | 贡献日期 unwrap | ✅ 已修复 (and_hms_opt 安全) | bdb5393 | -| 4.12 | WS 无 heartbeat/jitter | ✅ 已修复 (ping + jitter) | bdb5393 | - -### 额外发现 - -- `removeMember`/`updateMemberRole` 无 catch — ✅ 已修复 (try/catch) -- `branch_count as any` — ✅ 已修复 (移除) -- `cookie_secure(true)` — ✅ 已修复 -- CORS 配置化 origins — ✅ 已修复 (环境变量 CORS_ORIGINS) diff --git a/apps/email/src/main.rs b/apps/email/src/main.rs index d94af7b..8db9a55 100644 --- a/apps/email/src/main.rs +++ b/apps/email/src/main.rs @@ -43,11 +43,18 @@ async fn http_handler( }); let status = if db_ok && cache_ok { 200 } else { 503 }; + let body_bytes = match serde_json::to_string(&body) { + Ok(s) => hyper::Body::from(s), + Err(e) => return Ok(hyper::Response::builder() + .status(500) + .body(hyper::Body::from(format!("serialize error: {}", e))) + .expect("static response")), + }; Ok(hyper::Response::builder() .status(status) .header("content-type", "application/json") - .body(hyper::Body::from(serde_json::to_string(&body).unwrap())) - .unwrap()) + .body(body_bytes) + .expect("static response")) } "/metrics" => { let body = metrics.render(); diff --git a/apps/git-hook/src/main.rs b/apps/git-hook/src/main.rs index 08b1710..285a235 100644 --- a/apps/git-hook/src/main.rs +++ b/apps/git-hook/src/main.rs @@ -42,11 +42,18 @@ async fn http_handler( }); let status = if db_ok && cache_ok { 200 } else { 503 }; + let body_bytes = match serde_json::to_string(&body) { + Ok(s) => hyper::Body::from(s), + Err(e) => return Ok(hyper::Response::builder() + .status(500) + .body(hyper::Body::from(format!("serialize error: {}", e))) + .expect("static response")), + }; Ok(hyper::Response::builder() .status(status) .header("content-type", "application/json") - .body(hyper::Body::from(serde_json::to_string(&body).unwrap())) - .unwrap()) + .body(body_bytes) + .expect("static response")) } "/metrics" => { let body = metrics.render(); @@ -54,12 +61,12 @@ async fn http_handler( .status(200) .header("content-type", "text/plain; version=0.0.4; charset=utf-8") .body(hyper::Body::from(body)) - .unwrap()) + .expect("static response")) } _ => Ok(hyper::Response::builder() .status(404) .body(hyper::Body::from("not found")) - .unwrap()), + .expect("static response")), } } diff --git a/libs/agent/tool/executor.rs b/libs/agent/tool/executor.rs index ac8d796..8a1f16f 100644 --- a/libs/agent/tool/executor.rs +++ b/libs/agent/tool/executor.rs @@ -87,7 +87,7 @@ impl ToolExecutor { indexed_results.into_iter().collect(); let results: Vec = calls_clone.into_iter().enumerate().map(|(i, call)| { - let r = result_map.remove(&i).expect("every index must have a result"); + let r = result_map.remove(&i).unwrap_or_else(|| Err(ToolError::ExecutionError("missing result for tool call".into()))); r.unwrap_or_else(|e: ToolError| { ToolCallResult::error(call, e.to_string()) }) diff --git a/libs/api/agent/issue_triage.rs b/libs/api/agent/issue_triage.rs index 12ee29d..697ee15 100644 --- a/libs/api/agent/issue_triage.rs +++ b/libs/api/agent/issue_triage.rs @@ -25,11 +25,20 @@ pub struct TriageIssueQuery { )] pub async fn triage_issue( service: web::Data, - _session: Session, + session: Session, path: web::Path, query: web::Query, ) -> Result { let project_name = path.into_inner(); + let user_id = session.user().ok_or(crate::error::ApiError( + service::error::AppError::Unauthorized, + ))?; + let project = service.utils_find_project_by_name(project_name.clone()).await?; + // Verify user has access to the project before triggering AI triage + let _member = service + .get_project_member(project.id, user_id) + .await + .map_err(|_| crate::error::ApiError(service::error::AppError::Forbidden))?; let resp = service .triage_issue(project_name, query.issue_number) .await?; diff --git a/libs/api/room/ws_universal.rs b/libs/api/room/ws_universal.rs index 466576f..9cc8813 100644 --- a/libs/api/room/ws_universal.rs +++ b/libs/api/room/ws_universal.rs @@ -91,8 +91,7 @@ pub async fn ws_universal( .find(|p| p.starts_with("token=")) .and_then(|p| p.split('=').nth(1)) }) { - tracing::info!( - token = %token, + tracing::debug!( origin = %origin_val, "WS universal: validating token" ); @@ -108,7 +107,7 @@ pub async fn ws_universal( Err(e) => { tracing::warn!( error = ?e, - token = %token, + origin = %origin_val, "WS universal: token auth failed" ); service diff --git a/libs/email/lib.rs b/libs/email/lib.rs index 0c8b482..57f91d5 100644 --- a/libs/email/lib.rs +++ b/libs/email/lib.rs @@ -103,7 +103,7 @@ impl AppEmail { counter!("email_send_failures_total").increment(1); tracing::error!(to = %msg.to, error = %e, "Email send failed after retries"); } - tokio::time::sleep(Duration::from_secs((1 << i) as u64)).await; + tokio::time::sleep(Duration::from_secs(1u64 << i)).await; } Err(e) => { tracing::error!(to = %msg.to, error = %e, "Email spawn error"); diff --git a/libs/git/ssh/mod.rs b/libs/git/ssh/mod.rs index 0bc4664..13c8514 100644 --- a/libs/git/ssh/mod.rs +++ b/libs/git/ssh/mod.rs @@ -19,7 +19,7 @@ use std::time::Duration; pub mod authz; pub mod handle; pub mod server; - +pub mod rate_limit; #[derive(Clone)] pub struct SSHHandle { pub db: AppDatabase, diff --git a/libs/observability/src/tracing_fmt.rs b/libs/observability/src/tracing_fmt.rs index fd2e934..78a1edc 100644 --- a/libs/observability/src/tracing_fmt.rs +++ b/libs/observability/src/tracing_fmt.rs @@ -56,8 +56,8 @@ fn use_json() -> bool { /// Pass `defer = true` when OTLP will be initialized afterwards via `init_otlp()`. pub fn init_tracing_subscriber(level: &str, defer: bool) { let env_filter = EnvFilter::try_from_default_env() - .or_else(|_| EnvFilter::from_str(level)) - .expect("invalid log level"); + .or_else(|_| EnvFilter::from_str(level).ok()) + .unwrap_or_else(|| EnvFilter::from_str("info").expect("default log level")); let fmt_layer: Box + Send + Sync> = if use_json() { let mut layer = fmt::layer() diff --git a/libs/room/src/connection.rs b/libs/room/src/connection.rs index f52e39d..a20dca9 100644 --- a/libs/room/src/connection.rs +++ b/libs/room/src/connection.rs @@ -542,9 +542,9 @@ impl RoomConnectionManager { let count = counts.entry(user_id).or_insert(0); if *count > 0 { *count -= 1; - self.metrics.users_online.decrement(1.0); } if *count == 0 { + self.metrics.users_online.decrement(1.0); counts.remove(&user_id); drop(counts); let mut map = self.user_inner.write().await; diff --git a/libs/room/src/room.rs b/libs/room/src/room.rs index 82ab7bc..25aef6c 100644 --- a/libs/room/src/room.rs +++ b/libs/room/src/room.rs @@ -356,6 +356,21 @@ impl RoomService { .exec(&txn) .await?; + room_message_reaction::Entity::delete_many() + .filter(room_message_reaction::Column::Room.eq(room_id)) + .exec(&txn) + .await?; + + room_message_edit_history::Entity::delete_many() + .filter(room_message_edit_history::Column::Room.eq(room_id)) + .exec(&txn) + .await?; + + room_notifications::Entity::delete_many() + .filter(room_notifications::Column::Room.eq(room_id)) + .exec(&txn) + .await?; + room::Entity::delete_by_id(room_id).exec(&txn).await?; txn.commit().await?; diff --git a/libs/service/agent/sync.rs b/libs/service/agent/sync.rs index 6d77f01..3fbc252 100644 --- a/libs/service/agent/sync.rs +++ b/libs/service/agent/sync.rs @@ -342,7 +342,7 @@ async fn upsert_pricing( .unwrap_or_else(|| "USD".to_string()); let active = models::agents::model_pricing::ActiveModel { - id: Set(Uuid::now_v7().as_u128() as i64), + id: Set(Utc::now().timestamp_millis()), model_version_id: Set(version_uuid), input_price_per_1k_tokens: Set(input_price), output_price_per_1k_tokens: Set(output_price), @@ -373,7 +373,7 @@ async fn upsert_capabilities( continue; } let active = models::agents::model_capability::ActiveModel { - id: Set(Uuid::now_v7().as_u128() as i64), + id: Set(Utc::now().timestamp_millis()), model_version_id: Set(version_uuid.as_u128() as i64), capability: Set(cap_type.to_string()), is_supported: Set(supported), @@ -407,7 +407,7 @@ async fn upsert_parameter_profile( }; let active = models::agents::model_parameter_profile::ActiveModel { - id: Set(Uuid::now_v7().as_u128() as i64), + id: Set(Utc::now().timestamp_millis()), model_version_id: Set(version_uuid), temperature_min: Set(t_min), temperature_max: Set(t_max), diff --git a/libs/service/project/repo.rs b/libs/service/project/repo.rs index 3895b6a..987e3e8 100644 --- a/libs/service/project/repo.rs +++ b/libs/service/project/repo.rs @@ -187,20 +187,24 @@ impl AppService { .collect(); let last_commit_times: HashMap>> = { - let mut map: HashMap>> = HashMap::new(); - for repo_id in &repo_ids { - let last_commit: Option = RepoCommit::find() - .filter(models::repos::repo_commit::Column::Repo.eq(*repo_id)) + if repo_ids.is_empty() { + HashMap::new() + } else { + let commits: Vec = RepoCommit::find() + .filter(models::repos::repo_commit::Column::Repo.is_in(repo_ids.clone())) .order_by_desc(models::repos::repo_commit::Column::CreatedAt) - .one(&self.db) + .all(&self.db) .await .map_err(|e| AppError::DatabaseError(e.to_string()))?; - - let time = last_commit.map(|c| c.created_at); - - map.insert(*repo_id, time); + let mut map: HashMap>> = HashMap::new(); + for commit in commits { + map.entry(commit.repo).or_insert_with(|| Some(commit.created_at)); + } + for repo_id in &repo_ids { + map.entry(*repo_id).or_insert(None); + } + map } - map }; let ssh_domain = self diff --git a/libs/service/pull_request/pull_request.rs b/libs/service/pull_request/pull_request.rs index a791828..bd65876 100644 --- a/libs/service/pull_request/pull_request.rs +++ b/libs/service/pull_request/pull_request.rs @@ -520,26 +520,28 @@ impl AppService { return Err(AppError::NoPower); } - // Cascade delete related records + // Cascade delete related records in a transaction + let txn = self.db.begin().await?; models::pull_request::PullRequestCommit::delete_many() .filter(models::pull_request::pull_request_commit::Column::Repo.eq(repo.id)) .filter(models::pull_request::pull_request_commit::Column::Number.eq(number)) - .exec(&self.db) + .exec(&txn) .await?; models::pull_request::PullRequestReview::delete_many() .filter(models::pull_request::pull_request_review::Column::Repo.eq(repo.id)) .filter(models::pull_request::pull_request_review::Column::Number.eq(number)) - .exec(&self.db) + .exec(&txn) .await?; models::pull_request::PullRequestReviewComment::delete_many() .filter(models::pull_request::pull_request_review_comment::Column::Repo.eq(repo.id)) .filter(models::pull_request::pull_request_review_comment::Column::Number.eq(number)) - .exec(&self.db) + .exec(&txn) .await?; pull_request::Entity::delete_by_id((repo.id, number)) - .exec(&self.db) + .exec(&txn) .await?; + txn.commit().await?; super::invalidate_pr_cache(&self.cache, repo.id, number).await; diff --git a/libs/service/user/access_key.rs b/libs/service/user/access_key.rs index d744b0f..8082235 100644 --- a/libs/service/user/access_key.rs +++ b/libs/service/user/access_key.rs @@ -47,7 +47,7 @@ impl AppService { user: Set(user_uid), name: Set(params.name.clone()), token_hash: Set(access_key_hash), - scopes: Set(serde_json::to_value(params.scopes.clone()).unwrap()), + scopes: Set(serde_json::to_value(¶ms.scopes).unwrap_or(serde_json::json!([]))), expires_at: Set(params.expires_at), is_revoked: Set(false), created_at: Set(Utc::now()), @@ -211,20 +211,13 @@ impl AppService { } fn user_generate_access_key(&self) -> String { - use std::time::{SystemTime, UNIX_EPOCH}; - let now = SystemTime::now() - .duration_since(UNIX_EPOCH) - .unwrap_or_default() - .as_nanos(); - let chars: Vec = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" - .chars() - .collect(); + use rand::Rng; + let chars: &[u8] = b"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"; + let mut rng = rand::thread_rng(); let mut access_key = String::with_capacity(68); access_key.push_str("gda_"); - let mut state = now as u64; for _ in 0..64 { - state = state.wrapping_mul(1103515245).wrapping_add(12345); - access_key.push(chars[(state as usize) % chars.len()]); + access_key.push(chars[rng.gen_range(0..chars.len())] as char); } access_key } diff --git a/src/components/ui/carousel.tsx b/src/components/ui/carousel.tsx index f8202e4..f4c1f67 100644 --- a/src/components/ui/carousel.tsx +++ b/src/components/ui/carousel.tsx @@ -99,6 +99,7 @@ function Carousel({ return () => { api?.off("select", onSelect) + api?.off("reInit", onSelect) } }, [api, onSelect]) diff --git a/src/components/ui/message-list.tsx b/src/components/ui/message-list.tsx index c0b4640..7f07542 100644 --- a/src/components/ui/message-list.tsx +++ b/src/components/ui/message-list.tsx @@ -32,7 +32,7 @@ export function MessageList({ return ( { + if (audioStream) { + audioStream.getTracks().forEach((track) => track.stop()) + } + } + }, [transcribeAudio, audioStream]) const stopRecording = async () => { setIsRecording(false) @@ -71,10 +77,10 @@ export function useAudioRecording({ console.error("Error recording audio:", error) setIsListening(false) setIsRecording(false) - if (audioStream) { - audioStream.getTracks().forEach((track) => track.stop()) - setAudioStream(null) + if (stream) { + stream.getTracks().forEach((track) => track.stop()) } + setAudioStream(null) } } else { await stopRecording() diff --git a/src/hooks/useNotification.ts b/src/hooks/useNotification.ts index 7e4e7c3..20b15a9 100644 --- a/src/hooks/useNotification.ts +++ b/src/hooks/useNotification.ts @@ -146,6 +146,7 @@ export function useNotification(options: UseNotificationOptions = {}): UseNotifi ? { label: 'View', onClick: () => { + if (deepLinkUrl.startsWith('javascript:')) return; window.location.href = deepLinkUrl; }, } diff --git a/src/lib/universal-ws.ts b/src/lib/universal-ws.ts index 2320207..6ad86c8 100644 --- a/src/lib/universal-ws.ts +++ b/src/lib/universal-ws.ts @@ -87,10 +87,11 @@ export class UniversalWsClient { this.setStatus('closed'); // Reject all pending requests - for (const [id, req] of this.pendingRequests) { + const pending = [...this.pendingRequests.entries()]; + this.pendingRequests.clear(); + for (const [id, req] of pending) { clearTimeout(req.timeout); req.reject(new Error(`WebSocket closed: ${ev.reason || 'unknown'}`)); - this.pendingRequests.delete(id); } // Attempt reconnection