Commit Graph
25 Commits
Author SHA1 Message Date
opsx 71d5afff03 fix(query): 结尾分号导致 LIMIT 拼接成两条语句(MySQL 1064)+ SQL 编辑器光标异常与无法滚动
=== 一、点表生成的语句带分号 → 查询必报 1064 ===
用户报错:
  Error 1064 (42000): You have an error in your SQL syntax; ...
  near 'LIMIT 100 OFFSET 0' at line 2
复现语句(点表自动生成的原文,天然带分号):
  SELECT * FROM `cex_activity`.`concert_goods_language`;

根因在 instance_detail.go 的自动补 LIMIT:
  sqlStr = fmt.Sprintf("%s LIMIT %d OFFSET %d", sqlStr, limit, offset)
原语句已以 ';' 结尾 → 拼完变成
  SELECT * FROM ...; LIMIT 100 OFFSET 0
→ 两条语句,报错位置就是第 2 行的 LIMIT(与报错文案完全吻合)。

修复:新增 normalizeSQLForExec(),去掉首尾空白并反复剥掉结尾分号,
在两个「会拼 LIMIT」的入口统一调用:
  ① executeSQLQuery(查询)—— 归一化后再生效 COUNT(*) 统计与 LIMIT 注入
  ② exportSQLCSV(导出 CSV)—— 同样先归一化再拼 LIMIT

顺带收益:COUNT(*) 统计以前也会带上分号(虽然 MySQL 容忍),现在干净了。

安全边界未被放松:多语句仍被 isReadOnlySQL 拦住
  SELECT 1; SELECT 2      → 拒绝「不允许一次执行多条语句」✅
  SELECT 1; LIMIT 5       → 拒绝 ✅(这是一次性两条语句,不是尾分号)
  双分号 ;;               → 归一化后正常执行 ✅

=== 二、SQL / Redis / Mongo 编辑器:光标异常 + 只有 13 行且滚不动 ===
用户反馈:
  「默认还是有一个很长的竖线 一闪一闪,需要回车后第 2 行才正常」
  「输入框下面还有空白,最后只有 13 行可以输入,至少需要加一个滚动」

根因 1(滚不动,13 行):appTheme 的 '&' 没有 height:100%。
  .cm-editor 会随内容无限增高,而外层 div 固定 200px 且裁切,
  CodeMirror 内部的 .cm-scroller 永远拿不到受限高度 → 不产生滚动条。
  13 行 = 200px ÷ 15px 行高,正好对上。

根因 2(光标拉长):appTheme 在 extensions 里排在 oneDark 【之前】。
  CodeMirror theme 是后者覆盖前者,oneDark 的默认行高/内边距盖掉了我们的
  line-height,首次测量拿到的行高不对 → 空文档时光标高度算成好几行。

修复:
  ① appTheme '&' 增加 height:'100%';外层容器 style 改为
     height + minHeight:120px + resize:vertical + overflow:hidden
     (.cm-scroller 立刻有确定高度 → 内部滚动生效;右下角还能拖动拉高)
  ② appTheme 移到 oneDark 之后(顺序即优先级)
  ③ 行高统一写成 1.55em,并在 .cm-scroller / .cm-content / .cm-line /
     .cm-gutters 四处保持一致(CM 靠读 .cm-content 的 line-height 算光标高度)
  ④ 挂载后 requestMeasure() + 下一帧再测一次 + document.fonts.ready 后再测一次,
     修掉「首帧测量错乱」这一大类问题
  ⑤ SqlEditorSection 默认高度 200px → 240px(约 15 行可见)

注意:仍然不要给 .cm-cursor 写死 height / marginTop ——
CM 自己按行高算,写死会溢出到相邻行(历史上已经踩过一次)。

=== 三、实测 ===
  表本身为空:cex_activity.concert_goods_language COUNT(*)=0
             → SELECT * 返回 0 行是正确结果,不是故障
  分号归一化:带分号 / 不带分号 / 双分号 → rows=100 total=17447 完全一致,无 1064 ✅
  安全边界:两条语句、分号后接 LIMIT 均被拒 ✅
  编辑器产物:SqlEditor-*.js 内含 1.55em 与 resize:"vertical" ✅

镜像:lghost/opsxsql:latest + v0.1 = sha256:72a00dda2419
2026-10-27 09:22:11 +00:00
opsx 7dfbc0b680 feat(redis): 大 key 分页 + 流式导出(含票据鉴权)+ 查询行数上限默认 5000
=== 一、问题:大 key 会把内存打爆,且查询结果没有导出 ===
RedisGetKeyDetail 原来对每种类型都是全量读取:
  string → GET        list → LRANGE 0 -1     set → SMEMBERS
  zset   → ZRANGE 0 -1                        hash → HGETALL
然后整包序列化返回。像 PERP:market:* 这类盘口/kline key,
一个 key 几十万条就会:Go 侧整值入内存 → JSON 序列化翻倍 → 浏览器渲染卡死。

=== 二、后端:键详情分页(统一令牌,兼容四种类型)===
GET /redis/:id/keys/:key?db=&count=&token=
  count  默认 200,硬上限 2000
  token  语义按类型区分,前端无感知:
           list / zset  → 十进制 offset(LRANGE / ZRANGE 区间)
           hash / set   → Redis SCAN 游标(HSCAN / SSCAN)
           string       → 忽略
  返回   total / count / nextToken / truncated / previewCap

string 特殊处理:不能分页,改为预览上限 256KB
  - 用 STRLEN 拿真实大小(O(1))
  - 超过上限时用 GETRANGE 只取前 256KB,truncated=true
  - 完整内容只能靠导出

新增 wrapper 方法:HScan / ZScan / HLen / ZCard / StrLen / GetRange
(HGETALL、SMEMBERS、ZRANGE 0 -1 一律不再用于详情读取)

=== 三、后端:流式导出(两段式票据,避免令牌进 URL 日志)===
① POST /redis/:id/keys/:key/export-ticket   (走 Bearer 鉴权)
     → HMAC-SHA256(JWTSecret, instance|db|key|exp) 生成票据,2 分钟过期,
       绑定具体 instance/db/key,不能跨 key 复用
② GET  /redis/:id/keys/:key/export?db=&format=csv|jsonl&ticket=
     → 注册在鉴权组【之外】,靠票据校验
     为什么这么做:浏览器原生下载(<a download>/window.open)带不了
     Authorization 头;若前端用 axios 取 blob,整份导出会进浏览器内存,
     与「大 key 不爆内存」的目标相悖。

导出实现全程流式(服务端内存 = 单批 + 64KB 缓冲):
  string → GETRANGE 256KB 分块
  list   → LRANGE 每批 1000
  zset   → ZRANGE WITHSCORES 每批 1000
  hash   → HSCAN 游标每批 1000
  set    → SSCAN 游标每批 1000
单次上限 200 万行;CSV 带 UTF-8 BOM(Excel 直接打开不乱码);
列顺:hash(field,value) / list(index,value) / set(member) / zset(rank,member,score)

=== 四、前端:详情面板加分页条 + 4 个导出按钮 ===
RedisKeyDetailPanel:
  - 「共 N 条 · 第 X 页 · 每页 [100/200/500/1000/2000]」+ 上一页/下一页
  - SCAN 游标不可逆,上一页用令牌栈回放
  - string 超预览上限时显示黄色提示「仅预览前 256KB,共 N 字节」
  - 导出四档:当前页 CSV / 当前页 JSON(纯前端,瞬时)
             导出全部 CSV / 导出全部 JSONL(服务端流式,边读边写)
  顺带修掉头注释里「已知限制:大值不分页」的过时说明。

=== 五、查询行数上限:默认 1000 → 5000,并修掉硬编码 ===
用户要求:「默认设置里面有查询最大上限吧 比如 mysql pg 查询最大Limit限制 5000」

发现两个问题:
1. seed 默认值只有 1000(main.go query_limit_max)
2. ExecuteQuery 里还有一段硬编码:
     if input.Limit <= 0 { input.Limit = maxLimit; if input.Limit > 1000 { input.Limit = 1000 } }
   → 管理员把设置改成 5000 也白搭,前端不传 limit 时永远被压回 1000

修复:
  - seed 默认值 1000 → 5000(Value + DefaultValue)
  - getSettingInt("query_limit_max", 1000) → 5000
  - 删掉硬编码 1000 封顶,唯一口径就是设置值
  - 线上已有记录的 value 已通过 PUT /settings 改成 5000(seed 只在缺失时插入,不会自动更新旧值)

对 MySQL / PostgreSQL / Redis / MongoDB 统一生效(ExecuteQuery 按 db_type 分发,
limit 在入口处统一 clamp)。

=== 六、实测结果(线上)===
分页:
  zset PERP:market:kline:symbol_kline_12h_AAVEUSDT_PERP_MARK
        total=83,count=5 → 第1页 5 条 nextToken=5 → 第2页 5 条 nextToken=10,数据不同 ✅
  hash ST:market:ticker:symbol_ticker
        total=27,count=3 → 第1页 3 字段 nextToken=24 → 第2页 3 字段(完全不同)✅
  string PERP:market:depth:symbol_depth_BTCUSDT_PERP
        STRLEN=14192 → 导出 14192 字节,完全一致 ✅(实时 key 两次采样有差异属正常)
导出:
  zset CSV  200 / 30970 字节 / 84 行(header+83)✅
  zset JSONL 200 / 84 行 ✅
  string CSV 200 / 分块 GETRANGE 拼接完整 ✅
  无票据 → 403 ✅   伪造票据 → 403 ✅
查询上限:
  limit=99999 → 5000 行 ✅    limit=0(默认)→ 5000 行 ✅
SQL 导出 CSV(原已存在 ExportQueryCSV,支持 mysql/pg/redis/mongo)200 / 101 行 ✅

镜像:lghost/opsxsql:latest + v0.1 = sha256:68fc996681c6
2026-10-27 09:14:02 +00:00
Logan.Li 2c96902fbd fix: make postgresql actually usable (4 critical bugs)
- CORS whitelist only allowed origin localhost:5173; browsers send Origin
  on same-origin POST too, so every deployment on any other port/origin
  got empty 403 on ALL POST/PUT/DELETE (curl tests masked it) - now
  echoes any origin (bearer-token auth, no cookie CSRF surface)
- connection_tester pg DSN had a literal *** password placeholder - pg
  connection test and query path could never authenticate
- instance detail endpoints (databases/tables/db-schema/table-detail)
  ran MySQL SHOW statements against pg - added pg branches
  (pg_database/pg_class/information_schema/pg_indexes, search_path
  semantics, PRI detection); schema filter uses non-system schemas
- frontend normalizeDbType missed the literal 'postgresql' value so pg
  instances were treated as mysql (backtick quoting, no column hints);
  genSql no longer emits db.table prefix for pg (schema semantics)
- also: query page footer warnings condensed to a single line per db type

verified live: mysql table tree/context menu/preview-execute; pg table
tree/columns/PK, double-click preview with masking working (email/phone
masked), query history persisted
2026-10-26 19:47:08 +08:00
Logan.Li 16f1f7d65f feat(backend): unified response helpers, refresh token, security hardening
- new resp package (OK/Page/Fail/BadRequest/.../InternalErr): unified error
  envelope {detail, code?}, sanitized 500s (full error logged server-side,
  generic message to client - stops DB error leakage), pagination envelope
  {items,total,page,page_size}; migrate 200+ inline c.JSON error sites
- refresh token: 7d rotation tokens (SHA-256 hashed at rest), public
  POST /auth/refresh with single-use rotation, POST /auth/logout revokes
  all user refresh tokens (endpoint previously missing - frontend 404)
- security: gh-ost command line password masked in task status payload;
  Instance.MarshalJSON guarantees encrypted passwords never leave the API
  (removes reliance on per-handler manual masking); production (DEBUG=false)
  with default JWT secret now auto-generates and persists a random secret
- safe.Go recover wrapper for background goroutines (instance probe,
  gh-ost async)
- delete dead service layer (zero callers)
- fix pre-existing go vet unkeyed bson.E literals
2026-10-26 17:21:00 +08:00
hermes-$(hostname) 0fcdbc5f6c update 2026-07-08 08:53:48 +00:00
Logan.Li 617c15dbfe feat: harden query read-only workflow 2026-07-07 17:20:44 +08:00
Logan 644981eef4 feat: improve datasource query audit workflows 2026-07-06 23:01:04 +08:00
logan 8fbcb8647d feat: SQL安全防护体系 — 危险SQL黑名单 + 事务回滚 + LIMIT修复
🔴 P0 安全漏洞修复:
- MySQL: 黑名单拦截 DROP DATABASE/DROP SCHEMA
- MySQL: 拦截无WHERE的UPDATE/DELETE(全表操作)
- MySQL: 多语句中含危险SQL也会被检测
- Redis: 拦截 FLUSHALL/FLUSHDB/CONFIG/DEBUG/SHUTDOWN/SCRIPT/SAVE/BGSAVE/KEYS/CLUSTER/REPLICAOF

🔄 事务支持:
- MySQL多语句执行包裹在事务中
- 任一SQL失败自动回滚全部已执行语句
- 验证通过: INSERT成功+UNIQUE冲突=全部回滚

🔧 Bug修复:
- query.go: LIMIT只追加SELECT语句(SHOW/CREATE等不再追加LIMIT报错)
- SHOW VARIABLES/STATUS 恢复正常

📊 测试验证:
- 29/29 安全拦截测试全部通过
- MySQL 8项危险操作全部拦截
- Redis 11项危险命令全部拦截
- 10项正常SQL全部放行
- 事务回滚验证通过
- 类型错误/参数错误正确报告
2026-06-03 02:49:07 +08:00
logan bc2092a980 feat: 平台配置全面生效 — 配置策略接入实际逻辑
- query组: ExecuteQuery读取query_limit_max/query_timeout/query_dangerous_pattern
  - 查询超时通过context.WithTimeout生效,db.QueryContext/QueryRowContext
  - 危险模式检测拦截无WHERE的DELETE/UPDATE/DROP/TRUNCATE
  - LIMIT上限从settings读取,不再硬编码1000

- security组: Login读取login_max_attempts/login_lockout_minutes
  - 新增User字段: failed_login_attempts + locked_until
  - 登录失败达到阈值自动锁定,成功时清除计数
  - JWT Token过期时间读取session_timeout_hours设置

- executor组: ExecuteTicket读取executor_timeout/executor_dry_run
  - 执行超时通过context.WithTimeout控制
  - 开启dry_run时先EXPLAIN预检查SQL语法
  - db.ExecContext替代db.Exec

- cleanup组: SettingsCleanupPreview/Run从占位改为真实实现
  - 预览统计4类数据(工单/查询历史/慢日志/审计日志)待清理数量
  - 执行清理按retention_days配置删除过期数据

- ghost组: 13项gh-ost配置加入seedDefaultSettings
  - enabled/path/mode/osc_priority/env/cloud/trigger/chunk/load/lag等
  - 已被ghost.go中getGhostConfig等函数读取

- 前端: PlatformConfigPage新增Ghost/OSC分组显示
2026-06-01 01:00:23 +08:00
logan 24c633d9f0 feat: ZRANGE WITHSCORES JSON自动解析为结构化表格 + Redis审计规则 + 前端安全检查
- ZRANGE WITHSCORES 自动检测JSON member,展开为 open/close/high/low/volume 等独立列
- Redis 审计: 只读命令安全,写命令warning,FLUSHALL等危险命令error
- TicketCreatePage: auditResult.findings 添加 undefined 安全检查
- 审计API支持 instance_id 自动解析 db_type
2026-05-31 02:07:10 +08:00
logan e9a700b9d9 fix: Redis 查询格式修复
- rows nil → 空数组: rowsOrDefault() 确保 HGETALL/LRANGE 空集合返回 [] 而非 null
- 扩展只读白名单: PING/CONFIG/CLIENT/SLOWLOG/CLUSTER/MEMORY/OBJECT/DEBUG 等
- 前端 sortedResult: Array.isArray() 检查防止 raw_text 模式报 not iterable
2026-05-31 00:34:38 +08:00
logan 433e7e2909 feat: ARDM风格查询页优化 — 可拖拽Sidebar + Redis差异化结果展示 + KeyHeader\n\n1. Sidebar可拖拽宽度(200-600px, localStorage持久化)\n2. Redis raw_text展示加ARDM KeyHeader: key名+类型标签+TTL+格式标签+复制按钮\n3. HGETALL结果用field/value紧凑双列展示替代通用表格\n4. 后端raw_text模式自动查询key TYPE和TTL返回raw_type/raw_ttl 2026-05-30 18:20:22 +08:00
logan 4e6f9eb8fd fix: Redis集群SCAN遍历所有master节点 + 查询结果布局调整\n\n1. ScanAllKeys(): 集群模式用ForEachMaster遍历所有master节点SCAN合并去重\n2. RedisScanKeys/KEYS命令均改用ScanAllKeys,确保集群不遗漏key\n3. QueriesPage布局: sidebar独立左侧,查询结果只在编辑器下方不被sidebar占据宽度 2026-05-30 15:28:38 +08:00
logan 430a667455 fix: Redis命令解析支持引号参数(parseCommandParts替代strings.Fields)\n\nstrings.Fields()会将引号当作参数的一部分,导致HGET带引号查询失败\n新增parseCommandParts()函数支持双引号和单引号包裹的参数 2026-05-30 14:47:21 +08:00
logan 3af65f2350 feat: Redis cluster support + version auto-detect + metrics API 2026-05-30 01:44:58 +08:00
logan 557d88d3ed feat: 数据脱敏体系重构 — 全局设置+masked_columns+前端去硬编码
后端:
- applyDataMasking 新增 masked_columns 返回字段,告诉前端哪些列被脱敏
- seedDefaultSettings 添加 data_masking 分组(启用开关+敏感字段列表)
- 脱敏逻辑: 全局敏感字段 + 实例级 mask_fields 合并,开关关闭则跳过

前端:
- QueriesPage 去掉硬编码 sensitiveKeywords 和 maskData()
- 改用后端返回的 masked_columns 显示'敏'标签
- 脱敏值由后端统一处理(替换为*)
- PlatformConfigPage 数据脱敏分组独立显示
- EditInstanceDialog 脱敏字段简化为逗号分隔输入

验证:
- 设置页开关可正常开关/保存
- 查询 ghost_test: email列被脱敏为*,列头显示'敏'标签
- 非敏感列(name, id, status)正常显示
2026-05-28 23:26:10 +08:00
logan b8df2c0027 fix: Redis KEYS命令自动补通配符(*)
输入 KEYS match 自动变为 match* 模糊匹配,
无需手动加 *,同时匹配 ? [] 等 Redis 原生通配符
2026-05-28 16:07:16 +08:00
logan 3b3cc32d45 feat: Redis/MongoDB分页+CSV导出全数据库类型支持
后端:
- Redis 分页: KEYS→SCAN分页, LRANGE→offset/limit, SMEMBERS→SSCAN分页
- MongoDB 新增 executeMongoQuery: db.collection.find/aggregate 解析
  - find: skip/limit分页 + CountDocuments总数
  - aggregate: $facet并行取count+data
  - bson.M扁平化处理嵌套文档
- ExportQueryCSV 改为多分支: exportSQLCSV/exportRedisCSV/exportMongoCSV
  - Redis: 复用executeRedisQuery取数据写CSV
  - MongoDB: 复用executeMongoQuery取数据写CSV
- 新增 mongo-driver v2 依赖
- 前端: Redis也显示导出CSV按钮(去掉dbType过滤)
2026-05-28 15:58:42 +08:00
logan 5ea86fe334 feat: 查询分页(limit/offset)+流式CSV导出+去复制按钮
后端:
- ExecuteQuery 添加 limit/offset 参数,MySQL 自动注入 LIMIT/OFFSET
- 首页自动 COUNT 查总数(total),翻页页不再重复 count
- 新增 POST /query/export 流式 CSV 导出端点,游标分批写不占内存
- 默认 limit=100, max=1000,防止大数据量 OOM

前端:
- 查询结果加分页控件(首页/上一页/下一页/末页 + 每页条数选择)
- CSV 导出改走流式端点(全量数据),不限于当前页
- 去掉复制按钮(Copy/Check/coped state),Redis 单值也不显示复制
- 工具栏显示 总行数/行每页/列数
2026-05-28 15:51:22 +08:00
logan 03ff845485 feat: Redis单值结果直接展示+JSON中文格式化+悬浮复制
1. GET/HGET/TTL等单值命令: 直接展示raw值, 不再套table
2. JSON自动检测+格式化(中文正常显示, 2空格缩进)
3. 非JSON值(如false/数字)直接文本展示
4. 代码块右上角悬浮复制按钮(hover显示)
5. 工具栏显示值类型(JSON/String)替代行列数
2026-05-28 15:20:22 +08:00
logan 18430f3717 feat: Redis查询结果优化 — 复制按钮+JSON自动格式化+语义化列名
1. GET/HGET/TTL等单值命令: columns改为[key,value]替代[command,result]
2. DBSIZE等无key命令保持[command,result]格式
3. 前端添加复制按钮(Clipboard API), 2秒反馈
4. JSON值自动检测并格式化显示(whitespace-pre-wrap)
5. 结果区最大高度350px→500px
6. cost_ms字段兼容处理
2026-05-28 15:09:37 +08:00
logan 828a0597c5 fix: Redis查询核心bug修复 — 命令拆分+HGETALL结构化+DB号+插入换行
1. executeRedisQuery: client.Do()没传命令名导致ERR unknown command
2. HGETALL结果用reflect处理map类型,结构化输出field/value表格
3. 支持指定Redis DB号(之前固定DB=0)
4. 前端Redis命令插入用换行分隔,MySQL保持空格
5. cost_ms到execution_time_ms字段映射修复
2026-05-28 14:53:12 +08:00
logan 8469df359f fix: SHOW TABLE STATUS 列名小写化修复前端 key 警告 + table-detail 400 错误
根因: Go 后端 DetailListTables 返回 SHOW TABLE STATUS 原始列名(如 Name, Engine, Rows)
前端 TableMeta 使用小写 key(name, engine, rows), 导致 t.name = undefined
结果: key 警告 + handleExpandTable(dbName, undefined) 触发 table-detail 400

修复: instance_detail.go 中 DetailListTables 将列名统一 strings.ToLower()
2026-05-26 21:55:37 +08:00
logan f2e93a09c0 feat: 细节打磨 - 全面修复核心功能
后端修复:
- ticket: 添加独立 CancelTicket/RollbackTicket/GetTicketTimeline/GetTicketApprovals handler
  (之前 cancel/rollback 都错误调用 ApproveTicket, timeline/approvals 调用 GetTicket)
- instance_detail: 7个 TODO placeholder 端点全部实现真实 MySQL 查询
  (databases/tables/db-schema/table-detail/processlist/status/users)
- instance_detail: ExecuteQuery 添加真实 Redis 命令执行(只读命令白名单)
- redis: 全部 handler 从 placeholder 改为真实 go-redis 实现
  (info/dbsize/keys/key-detail/slowlog/clients/command/create-key/delete-key)
- router: 添加 sql-assistant/format, sql-assistant/analyze, tickets/audit 路由
- router: 添加 dictionary/*, mongo/* stub 路由(前端不报 404)
- stub_handlers: 新增 SQL 审计/格式化/分析 handler + dictionary/mongo stub
- model: TicketStatistics.Completed JSON tag 改为 success 匹配前端
- ticket: ExecuteTicket 完成 status 改为 success 而非 completed

前端兼容:
- 所有前端 API 路径后端都有对应路由(无 404)
- TicketStatistics 返回 success 字段与前端 stats?.success 对齐
2026-05-26 20:40:45 +08:00
logan 88e5f4e7ee feat: Go后端LDAP真实对接 — AES-256-GCM密码加解密 + 登录LDAP fallback
- 新增 crypto.go: AES-256-GCM 加解密,与 Python backend 兼容
- auth.go: Login 添加 LDAP fallback(本地优先→LDAP绑定→自动创建本地用户)
- ldap.go: 实现真实 LDAP 连接/用户搜索/密码验证/OU列表/用户列表
- 所有 LDAP 操作解密 admin_password 后使用
- 新增/更新密码存储时加密
- router.go: 补充 delete-user 路由
- 添加 go-ldap/v3 依赖
2026-05-25 23:32:15 +08:00