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
This commit is contained in:
ZhenYi 2026-04-27 13:54:21 +08:00
parent f36f08e3c4
commit 09645d8641
20 changed files with 98 additions and 397 deletions

View File

@ -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 会 panicRedis 锁永不释放,对应房间的 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 转义。恶意用户可在聊天中发送
`<script>alert(1)</script>``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)

View File

@ -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();

View File

@ -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")),
}
}

View File

@ -87,7 +87,7 @@ impl ToolExecutor {
indexed_results.into_iter().collect();
let results: Vec<ToolCallResult> = 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())
})

View File

@ -25,11 +25,20 @@ pub struct TriageIssueQuery {
)]
pub async fn triage_issue(
service: web::Data<AppService>,
_session: Session,
session: Session,
path: web::Path<String>,
query: web::Query<TriageIssueQuery>,
) -> Result<HttpResponse, crate::error::ApiError> {
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?;

View File

@ -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

View File

@ -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");

View File

@ -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,

View File

@ -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<dyn Layer<_> + Send + Sync> = if use_json() {
let mut layer = fmt::layer()

View File

@ -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;

View File

@ -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?;

View File

@ -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),

View File

@ -187,20 +187,24 @@ impl AppService {
.collect();
let last_commit_times: HashMap<Uuid, Option<DateTime<Utc>>> = {
let mut map: HashMap<Uuid, Option<DateTime<Utc>>> = HashMap::new();
for repo_id in &repo_ids {
let last_commit: Option<models::repos::repo_commit::Model> = RepoCommit::find()
.filter(models::repos::repo_commit::Column::Repo.eq(*repo_id))
if repo_ids.is_empty() {
HashMap::new()
} else {
let commits: Vec<models::repos::repo_commit::Model> = 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<Uuid, Option<DateTime<Utc>>> = 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

View File

@ -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;

View File

@ -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(&params.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<char> = "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
}

View File

@ -99,6 +99,7 @@ function Carousel({
return () => {
api?.off("select", onSelect)
api?.off("reInit", onSelect)
}
}, [api, onSelect])

View File

@ -32,7 +32,7 @@ export function MessageList({
return (
<ChatMessage
key={index}
key={message.id}
showTimeStamp={showTimeStamps}
{...message}
{...additionalOptions}

View File

@ -27,7 +27,13 @@ export function useAudioRecording({
}
checkSpeechSupport()
}, [transcribeAudio])
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()

View File

@ -146,6 +146,7 @@ export function useNotification(options: UseNotificationOptions = {}): UseNotifi
? {
label: 'View',
onClick: () => {
if (deepLinkUrl.startsWith('javascript:')) return;
window.location.href = deepLinkUrl;
},
}

View File

@ -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