package handler import ( "net/http" "strconv" "golang.org/x/crypto/bcrypt" "sql-platform/internal/handler/resp" "sql-platform/internal/model" "github.com/gin-gonic/gin" ) // GET /api/v1/workflows/:id func GetWorkflow(c *gin.Context) { id := c.Param("id") var wf model.WorkflowDefinition if err := model.DB.First(&wf, id).Error; err != nil { resp.NotFound(c, "Workflow not found") return } var steps []model.WorkflowStep model.DB.Where("workflow_id = ?", wf.ID).Order("step_number").Find(&steps) c.JSON(http.StatusOK, gin.H{ "id": wf.ID, "name": wf.Name, "description": wf.Description, "category": wf.Category, "is_default": wf.IsDefault, "is_active": wf.IsActive, "created_at": wf.CreatedAt, "updated_at": wf.UpdatedAt, "created_by": wf.CreatedBy, "steps": steps, }) } // POST /api/v1/workflows func CreateWorkflow(c *gin.Context) { var input struct { Name string `json:"name" binding:"required"` Description string `json:"description"` Category string `json:"category"` IsActive *bool `json:"is_active"` IsDefault *bool `json:"is_default"` Steps []struct { StepNumber int `json:"step_number"` Name string `json:"name"` Description string `json:"description"` StepType string `json:"step_type"` ApproverRole string `json:"approver_role"` ApproverIDs string `json:"approver_ids"` IsRequired *bool `json:"is_required"` TimeoutHours *int `json:"timeout_hours"` AutoApproveOnTimeout *bool `json:"auto_approve_on_timeout"` } `json:"steps"` } if err := c.ShouldBindJSON(&input); err != nil { resp.BadRequest(c, err.Error()) return } active := true if input.IsActive != nil { active = *input.IsActive } wf := model.WorkflowDefinition{ Name: input.Name, Description: &input.Description, Category: &input.Category, IsActive: &active, IsDefault: input.IsDefault, } if err := model.DB.Create(&wf).Error; err != nil { resp.InternalErr(c, err) return } for _, s := range input.Steps { step := model.WorkflowStep{ WorkflowID: wf.ID, StepNumber: s.StepNumber, Name: s.Name, Description: &s.Description, StepType: s.StepType, ApproverRole: &s.ApproverRole, ApproverIDs: &s.ApproverIDs, IsRequired: s.IsRequired, TimeoutHours: s.TimeoutHours, AutoApproveOnTimeout: s.AutoApproveOnTimeout, } model.DB.Create(&step) } c.JSON(http.StatusCreated, gin.H{"id": wf.ID, "message": "Workflow created"}) } // PUT /api/v1/workflows/:id func UpdateWorkflow(c *gin.Context) { id := c.Param("id") var wf model.WorkflowDefinition if err := model.DB.First(&wf, id).Error; err != nil { resp.NotFound(c, "Workflow not found") return } var input map[string]interface{} if err := c.ShouldBindJSON(&input); err != nil { resp.BadRequest(c, err.Error()) return } model.DB.Model(&wf).Updates(input) // If steps provided, replace them if stepsRaw, ok := input["steps"]; ok { model.DB.Where("workflow_id = ?", wf.ID).Delete(&model.WorkflowStep{}) if stepsArr, ok := stepsRaw.([]interface{}); ok { for i, sRaw := range stepsArr { if sMap, ok := sRaw.(map[string]interface{}); ok { step := model.WorkflowStep{ WorkflowID: wf.ID, StepNumber: i + 1, } if v, ok := sMap["name"].(string); ok { step.Name = v } if v, ok := sMap["step_type"].(string); ok { step.StepType = v } if v, ok := sMap["approver_role"].(string); ok { step.ApproverRole = &v } if v, ok := sMap["approver_ids"].(string); ok { step.ApproverIDs = &v } if v, ok := sMap["description"].(string); ok { step.Description = &v } model.DB.Create(&step) } } } } c.JSON(http.StatusOK, gin.H{"message": "Workflow updated"}) } // DELETE /api/v1/workflows/:id func DeleteWorkflow(c *gin.Context) { id := c.Param("id") model.DB.Where("workflow_id = ?", id).Delete(&model.WorkflowStep{}) model.DB.Delete(&model.WorkflowDefinition{}, id) c.JSON(http.StatusOK, gin.H{"message": "Workflow deleted"}) } // POST /api/v1/workflows/:id/set-default func SetDefaultWorkflow(c *gin.Context) { id := c.Param("id") // unset all defaults model.DB.Model(&model.WorkflowDefinition{}).Where("is_default = ?", true).Update("is_default", false) // set this one as default defaultVal := true model.DB.Model(&model.WorkflowDefinition{}).Where("id = ?", id).Update("is_default", defaultVal) c.JSON(http.StatusOK, gin.H{"message": "Default workflow set"}) } // POST /api/v1/users/:id/reset-password func ResetPassword(c *gin.Context) { id := c.Param("id") var input struct { NewPassword string `json:"new_password" binding:"required"` } if err := c.ShouldBindJSON(&input); err != nil { resp.BadRequest(c, err.Error()) return } _, _ = id, input.NewPassword // TODO: hash password and update c.JSON(http.StatusOK, gin.H{"message": "Password reset (placeholder)"}) } // POST /api/v1/users/batch-import func BatchImportUsers(c *gin.Context) { var input struct { Users []struct { Username string `json:"username"` DisplayName string `json:"display_name"` Email string `json:"email"` Role string `json:"role"` Password string `json:"password"` } `json:"users"` ImportType string `json:"import_type"` SkipExisting bool `json:"skip_existing"` } if err := c.ShouldBindJSON(&input); err != nil { resp.BadRequest(c, err.Error()) return } successCount := 0 var errors []string for _, u := range input.Users { if u.Username == "" { errors = append(errors, "skip: empty username") continue } // Check if already exists var existing int64 model.DB.Model(&model.User{}).Where("username = ?", u.Username).Count(&existing) if existing > 0 { if input.SkipExisting { errors = append(errors, "skip: "+u.Username+" already exists") continue } errors = append(errors, "fail: "+u.Username+" already exists") continue } password := u.Password if password == "" { password = "admin123" } hash, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost) if err != nil { errors = append(errors, "fail: "+u.Username+" hash error") continue } role := u.Role if role == "" { role = "developer" } displayName := u.DisplayName if displayName == "" { displayName = u.Username } emailPtr := &u.Email if u.Email == "" { emailPtr = nil } user := model.User{ Username: u.Username, DisplayName: displayName, Email: emailPtr, HashedPassword: string(hash), Role: role, } if err := model.DB.Create(&user).Error; err != nil { errors = append(errors, "fail: "+u.Username+" - "+err.Error()) continue } successCount++ } c.JSON(http.StatusOK, gin.H{ "success_count": successCount, "fail_count": len(input.Users) - successCount, "errors": errors, }) } // POST /api/v1/users/batch-import-ldap func BatchImportLdapUsers(c *gin.Context) { var input struct { Users []struct { DN string `json:"dn"` CN string `json:"cn"` Nickname string `json:"nickname"` UID string `json:"uid"` Username string `json:"username"` Mail string `json:"mail"` } `json:"users"` } if err := c.ShouldBindJSON(&input); err != nil { resp.BadRequest(c, err.Error()) return } successCount := 0 var errors []string for _, u := range input.Users { // Derive username: uid > username > cn username := u.UID if username == "" { username = u.Username } if username == "" { username = u.CN } if username == "" { errors = append(errors, "skip: empty username for DN="+u.DN) continue } // Check if already exists var existing int64 model.DB.Model(&model.User{}).Where("username = ?", username).Count(&existing) if existing > 0 { errors = append(errors, "skip: "+username+" already exists") continue } // Derive display name: cn > nickname > username displayName := u.CN if displayName == "" { displayName = u.Nickname } if displayName == "" { displayName = username } email := u.Mail emailPtr := &email if email == "" { emailPtr = nil } isActive := true user := model.User{ Username: username, DisplayName: displayName, Email: emailPtr, HashedPassword: "-", // LDAP users don't need local password Role: "developer", AuthSource: "ldap", IsActive: &isActive, } if err := model.DB.Create(&user).Error; err != nil { errors = append(errors, "fail: "+username+" - "+err.Error()) continue } successCount++ } c.JSON(http.StatusOK, gin.H{ "success_count": successCount, "fail_count": len(input.Users) - successCount, "errors": errors, }) } // GET /api/v1/users/import-template func ImportTemplate(c *gin.Context) { csv := "username,display_name,email,role,password\nzhangsan,张三,zhangsan@test.com,developer,\nlisi,李四,lisi@test.com,dba,pass123\n" c.Header("Content-Type", "text/csv; charset=utf-8") c.Header("Content-Disposition", "attachment; filename=users_import_template.csv") c.Writer.Write([]byte("\xEF\xBB\xBF" + csv)) } // POST /api/v1/users/:userId/reset-password func ResetUserPassword(c *gin.Context) { userId := c.Param("userId") var input struct { NewPassword string `json:"new_password" binding:"required"` } if err := c.ShouldBindJSON(&input); err != nil { resp.BadRequest(c, err.Error()) return } id, err := strconv.Atoi(userId) if err != nil { resp.BadRequest(c, "Invalid user ID") return } hash, err := bcrypt.GenerateFromPassword([]byte(input.NewPassword), bcrypt.DefaultCost) if err != nil { resp.InternalMsg(c, "Hash failed") return } if err := model.DB.Model(&model.User{}).Where("id = ?", id).Update("hashed_password", string(hash)).Error; err != nil { resp.InternalMsg(c, "Update failed") return } c.JSON(http.StatusOK, gin.H{"message": "Password reset successfully"}) }