diff --git a/backend/internal/ghost/ghost.go b/backend/internal/ghost/ghost.go index d99c74a..d4564b4 100644 --- a/backend/internal/ghost/ghost.go +++ b/backend/internal/ghost/ghost.go @@ -13,6 +13,7 @@ import ( // GhOstTask defines a gh-ost execution task type GhOstTask struct { Host string `json:"host"` + Port int `json:"port,omitempty"` User string `json:"user"` Password string `json:"password"` Database string `json:"database"` @@ -60,12 +61,18 @@ func RunGhOst(ctx context.Context, task GhOstTask, ghOstPath string) (*GhOstResu args := []string{ "--host=" + task.Host, + fmt.Sprintf("--port=%d", task.Port), "--user=" + task.User, "--password=" + task.Password, "--database=" + task.Database, "--table=" + task.Table, "--alter=" + task.Alter, "--verbose", + "--skip-port-validation", + "--allow-on-master", + "--initially-drop-ghost-table", + "--initially-drop-old-table", + "--initially-drop-socket-file", } if task.ChunkSize > 0 { @@ -91,11 +98,11 @@ func RunGhOst(ctx context.Context, task GhOstTask, ghOstPath string) (*GhOstResu } // Dry-Run vs Execute - if task.DryRun || !task.Execute { - args = append(args, "--dry-run") - } else { + // gh-ost defaults to noop (dry-run), only --execute actually applies changes + if task.Execute { args = append(args, "--execute") } + // always verbose for output cmd := exec.CommandContext(ctx, ghOstPath, args...) var stdout, stderr bytes.Buffer @@ -193,12 +200,18 @@ func (m *TaskManager) ListTasks() map[string]string { func buildArgs(task GhOstTask, ghOstPath string) []string { args := []string{ "--host=" + task.Host, + fmt.Sprintf("--port=%d", task.Port), "--user=" + task.User, "--password=" + task.Password, "--database=" + task.Database, "--table=" + task.Table, "--alter=" + task.Alter, "--verbose", + "--skip-port-validation", + "--allow-on-master", + "--initially-drop-ghost-table", + "--initially-drop-old-table", + "--initially-drop-socket-file", } if task.ChunkSize > 0 { @@ -223,9 +236,8 @@ func buildArgs(task GhOstTask, ghOstPath string) []string { args = append(args, "--postpone-cut-over-flag-file=/tmp/gh-ost-cutover.flag") } - if task.DryRun || !task.Execute { - args = append(args, "--dry-run") - } else { + // gh-ost defaults to noop, only --execute actually applies changes + if task.Execute { args = append(args, "--execute") } diff --git a/backend/internal/handler/ghost.go b/backend/internal/handler/ghost.go index 4f6f523..f06270e 100644 --- a/backend/internal/handler/ghost.go +++ b/backend/internal/handler/ghost.go @@ -1,6 +1,7 @@ package handler import ( + "fmt" "net/http" "time" @@ -13,15 +14,27 @@ import ( // POST /api/v1/ghost/dry-run func GhOstDryRun(c *gin.Context) { - var task ghost.GhOstTask - if err := c.ShouldBindJSON(&task); err != nil { + var req struct { + InstanceID int `json:"instance_id"` + Database string `json:"database"` + Table string `json:"table"` + Alter string `json:"alter"` + } + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"detail": err.Error()}) + return + } + + // Look up instance connection info + task, err := buildGhOstTask(req.InstanceID, req.Database, req.Table, req.Alter) + if err != nil { c.JSON(http.StatusBadRequest, gin.H{"detail": err.Error()}) return } task.DryRun = true task.Execute = false - result, err := ghost.RunGhOst(c.Request.Context(), task, config.C.GhOstPath) + result, err := ghost.RunGhOst(c.Request.Context(), *task, config.C.GhOstPath) if err != nil { c.JSON(http.StatusInternalServerError, result) return @@ -29,17 +42,51 @@ func GhOstDryRun(c *gin.Context) { c.JSON(http.StatusOK, result) } +// buildGhOstTask resolves instance_id to connection params +func buildGhOstTask(instanceID int, database, table, alter string) (*ghost.GhOstTask, error) { + var inst model.Instance + if err := model.DB.First(&inst, instanceID).Error; err != nil { + return nil, fmt.Errorf("instance %d not found", instanceID) + } + if inst.DBType != "mysql" { + return nil, fmt.Errorf("gh-ost only supports MySQL instances, got %s", inst.DBType) + } + // gh-ost needs separate host and port + host := inst.Host + port := inst.Port + return &ghost.GhOstTask{ + Host: host, + Port: port, + User: inst.Username, + Password: inst.Password, + Database: database, + Table: table, + Alter: alter, + }, nil +} + // POST /api/v1/ghost/execute func GhOstExecute(c *gin.Context) { - var task ghost.GhOstTask - if err := c.ShouldBindJSON(&task); err != nil { + var req struct { + InstanceID int `json:"instance_id"` + Database string `json:"database"` + Table string `json:"table"` + Alter string `json:"alter"` + } + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"detail": err.Error()}) + return + } + + task, err := buildGhOstTask(req.InstanceID, req.Database, req.Table, req.Alter) + if err != nil { c.JSON(http.StatusBadRequest, gin.H{"detail": err.Error()}) return } task.DryRun = false task.Execute = true - taskID := ghost.RunGhOstAsync(task, config.C.GhOstPath, nil) + taskID := ghost.RunGhOstAsync(*task, config.C.GhOstPath, nil) c.JSON(http.StatusAccepted, gin.H{ "task_id": taskID, "message": "gh-ost execution started", diff --git a/backend/internal/handler/preferences.go b/backend/internal/handler/preferences.go index 8884dbb..bf2cb66 100644 --- a/backend/internal/handler/preferences.go +++ b/backend/internal/handler/preferences.go @@ -43,26 +43,41 @@ func UpdateUserPreferences(c *gin.Context) { c.JSON(http.StatusInternalServerError, gin.H{"detail": result.Error.Error()}) return } + + // Sync login_style and theme to SystemSetting so public endpoint picks them up + syncToSystemSetting := func(key, value string) { + var s model.SystemSetting + if model.DB.Where("`key` = ?", key).First(&s).Error == nil { + model.DB.Model(&s).Update("value", value) + } else { + model.DB.Create(&model.SystemSetting{ + Key: key, Value: value, ValueType: "string", + Group: "appearance", Label: key, SortOrder: 0, + }) + } + } + if v, ok := prefs["login_style"]; ok { + syncToSystemSetting("login_style", v) + } + if v, ok := prefs["theme"]; ok { + syncToSystemSetting("default_theme", v) + } + c.JSON(http.StatusOK, gin.H{"message": "Preferences updated", "preferences": prefs}) } // GET /api/v1/public/login-style (no auth required) func GetPublicLoginStyle(c *gin.Context) { - // Check system settings first - var setting model.SystemSetting - style := "split" // default - if model.DB.Where("`key` = ?", "login_style").First(&setting).Error == nil { - if setting.Value != "" { - style = setting.Value - } - } - - // Also return theme default + style := "split" theme := "dark" - if model.DB.Where("`key` = ?", "default_theme").First(&setting).Error == nil { - if setting.Value != "" { - theme = setting.Value - } + + var s model.SystemSetting + if model.DB.Where("`key` = ?", "login_style").Take(&s).Error == nil && s.Value != "" { + style = s.Value + } + s = model.SystemSetting{} // reset to avoid GORM reusing primary key + if model.DB.Where("`key` = ?", "default_theme").Take(&s).Error == nil && s.Value != "" { + theme = s.Value } c.JSON(http.StatusOK, gin.H{"login_style": style, "theme": theme}) diff --git a/frontend/src/components/login/CenteredCard.tsx b/frontend/src/components/login/CenteredCard.tsx index 1343cd9..62940ac 100644 --- a/frontend/src/components/login/CenteredCard.tsx +++ b/frontend/src/components/login/CenteredCard.tsx @@ -23,9 +23,9 @@ export default function CenteredCard() {

- SQL Audit Platform + SQL审核平台

-

Sign in to your account

+

登录您的账号

@@ -33,14 +33,14 @@ export default function CenteredCard() { setUsername(e.target.value)} style={{ padding: '11px 14px', borderRadius: 8, background: 'var(--dg-bg-secondary)', border: '1px solid var(--dg-border)', color: 'var(--dg-text-primary)', fontSize: 14, outline: 'none' }} /> setPassword(e.target.value)} style={{ padding: '11px 14px', borderRadius: 8, background: 'var(--dg-bg-secondary)', border: '1px solid var(--dg-border)', color: 'var(--dg-text-primary)', fontSize: 14, outline: 'none' }} @@ -52,7 +52,7 @@ export default function CenteredCard() { className="dg-gradient-bg" style={{ padding: '11px 14px', borderRadius: 8, border: 'none', color: '#fff', fontWeight: 600, fontSize: 14, cursor: loading || !username || !password ? 'not-allowed' : 'pointer', opacity: loading || !username || !password ? 0.6 : 1, marginTop: 4 }} > - {loading ? 'Signing in…' : 'Sign in'} + {loading ? '登录中…' : '登录'}
diff --git a/frontend/src/components/login/FullImage.tsx b/frontend/src/components/login/FullImage.tsx index 1f560f1..b2ce634 100644 --- a/frontend/src/components/login/FullImage.tsx +++ b/frontend/src/components/login/FullImage.tsx @@ -40,9 +40,9 @@ export default function FullImage() { >

- SQL Audit Platform + SQL审核平台

-

Sign in to your workspace

+

登录您的工作区

@@ -50,14 +50,14 @@ export default function FullImage() { setUsername(e.target.value)} style={{ padding: '11px 14px', borderRadius: 8, background: 'rgba(255,255,255,0.08)', border: '1px solid rgba(255,255,255,0.12)', color: '#fff', fontSize: 14, outline: 'none' }} /> setPassword(e.target.value)} style={{ padding: '11px 14px', borderRadius: 8, background: 'rgba(255,255,255,0.08)', border: '1px solid rgba(255,255,255,0.12)', color: '#fff', fontSize: 14, outline: 'none' }} @@ -69,7 +69,7 @@ export default function FullImage() { className="dg-gradient-bg" style={{ padding: '11px 14px', borderRadius: 8, border: 'none', color: '#fff', fontWeight: 600, fontSize: 14, cursor: loading || !username || !password ? 'not-allowed' : 'pointer', opacity: loading || !username || !password ? 0.6 : 1, marginTop: 4 }} > - {loading ? 'Signing in…' : 'Sign in'} + {loading ? '登录中…' : '登录'}
diff --git a/frontend/src/components/login/MinimalDark.tsx b/frontend/src/components/login/MinimalDark.tsx index 41be0d3..32f1840 100644 --- a/frontend/src/components/login/MinimalDark.tsx +++ b/frontend/src/components/login/MinimalDark.tsx @@ -18,23 +18,23 @@ export default function MinimalDark() {

- SQL Audit Platform + SQL审核平台

-

Secure database auditing & compliance

+

安全的数据库审核与合规管理

{error &&
{error}
} setUsername(e.target.value)} style={{ padding: '10px 14px', borderRadius: 8, background: 'var(--dg-bg-secondary)', border: 'none', color: 'var(--dg-text-primary)', fontSize: 14, outline: 'none' }} /> setPassword(e.target.value)} style={{ padding: '10px 14px', borderRadius: 8, background: 'var(--dg-bg-secondary)', border: 'none', color: 'var(--dg-text-primary)', fontSize: 14, outline: 'none' }} @@ -46,7 +46,7 @@ export default function MinimalDark() { className="dg-gradient-bg" style={{ padding: '10px 14px', borderRadius: 8, border: 'none', color: '#fff', fontWeight: 600, fontSize: 14, cursor: loading || !username || !password ? 'not-allowed' : 'pointer', opacity: loading || !username || !password ? 0.6 : 1, marginTop: 8 }} > - {loading ? 'Signing in…' : 'Sign in'} + {loading ? '登录中…' : '登录'}
diff --git a/frontend/src/components/login/SidePanel.tsx b/frontend/src/components/login/SidePanel.tsx index 72cfbc7..427594a 100644 --- a/frontend/src/components/login/SidePanel.tsx +++ b/frontend/src/components/login/SidePanel.tsx @@ -29,9 +29,9 @@ export default function SidePanel() {
-

SQL Audit Platform

+

SQL审核平台

- Empowering teams with intelligent SQL auditing, real-time compliance monitoring, and actionable insights for database security. + 为团队提供智能 SQL 审核、实时合规监控与数据库安全保障。

@@ -39,21 +39,21 @@ export default function SidePanel() { {/* Right Panel — Login Form */}
-

Welcome back

-

Sign in to access your dashboard

+

欢迎回来

+

登录以访问控制台

{error &&
{error}
} setUsername(e.target.value)} style={{ padding: '10px 14px', borderRadius: 8, background: 'var(--dg-bg-tertiary)', border: '1px solid var(--dg-border)', color: 'var(--dg-text-primary)', fontSize: 14, outline: 'none' }} /> setPassword(e.target.value)} style={{ padding: '10px 14px', borderRadius: 8, background: 'var(--dg-bg-tertiary)', border: '1px solid var(--dg-border)', color: 'var(--dg-text-primary)', fontSize: 14, outline: 'none' }} @@ -65,7 +65,7 @@ export default function SidePanel() { className="dg-gradient-bg" style={{ padding: '10px 14px', borderRadius: 8, border: 'none', color: '#fff', fontWeight: 600, fontSize: 14, cursor: loading || !username || !password ? 'not-allowed' : 'pointer', opacity: loading || !username || !password ? 0.6 : 1 }} > - {loading ? 'Signing in…' : 'Sign in'} + {loading ? '登录中…' : '登录'}
diff --git a/frontend/src/components/login/SplitView.tsx b/frontend/src/components/login/SplitView.tsx index 490a001..276c4e9 100644 --- a/frontend/src/components/login/SplitView.tsx +++ b/frontend/src/components/login/SplitView.tsx @@ -3,12 +3,12 @@ import { useTheme } from '@/hooks/useTheme'; import { Sun, Moon, Database, Code, GitBranch, Bell, History, UserCog } from 'lucide-react'; const features = [ - { icon: Database, label: 'SQL Auditing', desc: 'Real-time query analysis' }, - { icon: Code, label: 'Smart Rules', desc: 'Custom compliance rules' }, - { icon: GitBranch, label: 'Version Control', desc: 'Track schema changes' }, - { icon: Bell, label: 'Alerts', desc: 'Instant notifications' }, - { icon: History, label: 'History', desc: 'Full audit trail' }, - { icon: UserCog, label: 'Access Control', desc: 'Role-based permissions' }, + { icon: Database, label: 'SQL审核', desc: '实时查询分析与合规检测' }, + { icon: Code, label: '智能规则', desc: '自定义审核规则引擎' }, + { icon: GitBranch, label: '变更管理', desc: 'DDL/DML 变更审批流' }, + { icon: Bell, label: '即时通知', desc: '多渠道消息推送' }, + { icon: History, label: '审计追踪', desc: '完整操作历史记录' }, + { icon: UserCog, label: '权限管理', desc: '基于角色的访问控制' }, ]; export default function SplitView() { @@ -28,10 +28,10 @@ export default function SplitView() {

- SQL Audit Platform + SQL审核平台

- Enterprise-grade SQL auditing and compliance management. + 企业级 SQL 审核与合规管理平台。

{features.map((f) => ( @@ -51,21 +51,21 @@ export default function SplitView() { onSubmit={handleLogin} style={{ width: '100%', maxWidth: 360, display: 'flex', flexDirection: 'column', gap: 20 }} > -

Sign in

-

Enter your credentials to continue

+

登录

+

请输入您的账号信息

{error &&
{error}
} setUsername(e.target.value)} style={{ padding: '10px 14px', borderRadius: 8, background: 'var(--dg-bg-tertiary)', border: '1px solid var(--dg-border)', color: 'var(--dg-text-primary)', fontSize: 14, outline: 'none' }} /> setPassword(e.target.value)} style={{ padding: '10px 14px', borderRadius: 8, background: 'var(--dg-bg-tertiary)', border: '1px solid var(--dg-border)', color: 'var(--dg-text-primary)', fontSize: 14, outline: 'none' }} @@ -77,7 +77,7 @@ export default function SplitView() { className="dg-gradient-bg" style={{ padding: '10px 14px', borderRadius: 8, border: 'none', color: '#fff', fontWeight: 600, fontSize: 14, cursor: loading || !username || !password ? 'not-allowed' : 'pointer', opacity: loading || !username || !password ? 0.6 : 1 }} > - {loading ? 'Signing in…' : 'Sign in'} + {loading ? '登录中…' : '登录'}