Files
opsxsql/backend/internal/handler/workflow.go
T
logan e8fd5f6b63 fix: LDAP导入用户 auth_source 修复
- BatchImportLdapUsers: 设置 AuthSource='ldap' + HashedPassword='-' + IsActive=true
- 修正已有 zhangsan/lisi/wangwu 用户 auth_source 从 local 改为 ldap
2026-05-31 00:18:43 +08:00

381 lines
10 KiB
Go

package handler
import (
"net/http"
"strconv"
"golang.org/x/crypto/bcrypt"
"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 {
c.JSON(http.StatusNotFound, gin.H{"detail": "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 {
c.JSON(http.StatusBadRequest, gin.H{"detail": 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 {
c.JSON(http.StatusInternalServerError, gin.H{"detail": err.Error()})
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 {
c.JSON(http.StatusNotFound, gin.H{"detail": "Workflow not found"})
return
}
var input map[string]interface{}
if err := c.ShouldBindJSON(&input); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"detail": 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 {
c.JSON(http.StatusBadRequest, gin.H{"detail": 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 {
c.JSON(http.StatusBadRequest, gin.H{"detail": 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 {
c.JSON(http.StatusBadRequest, gin.H{"detail": 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 {
c.JSON(http.StatusBadRequest, gin.H{"detail": err.Error()})
return
}
id, err := strconv.Atoi(userId)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"detail": "Invalid user ID"})
return
}
hash, err := bcrypt.GenerateFromPassword([]byte(input.NewPassword), bcrypt.DefaultCost)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"detail": "Hash failed"})
return
}
if err := model.DB.Model(&model.User{}).Where("id = ?", id).Update("hashed_password", string(hash)).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"detail": "Update failed"})
return
}
c.JSON(http.StatusOK, gin.H{"message": "Password reset successfully"})
}