mirror of
https://github.com/StarFleetCPTN/GoMFT.git
synced 2026-09-11 09:00:49 +02:00
feat: Implement configuration and job duplication functionality
- Added duplication feature for configurations and jobs, allowing users to create copies of existing entries. - Introduced new buttons in the UI for duplicating configurations and jobs, enhancing user experience. - Implemented backend logic to handle duplication requests, ensuring proper ownership checks and data integrity. - Added notifications for successful duplication actions to inform users of the outcome. - Updated relevant templates and handlers to support the new duplication functionality.
This commit is contained in:
@@ -427,3 +427,145 @@ func (h *Handlers) HandleDeleteConfig(c *gin.Context) {
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"message": "Config deleted successfully"})
|
||||
}
|
||||
|
||||
// HandleDuplicateConfig handles the POST /configs/:id/duplicate route
|
||||
func (h *Handlers) HandleDuplicateConfig(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
userID := c.GetUint("userID")
|
||||
|
||||
var originalConfig db.TransferConfig
|
||||
if err := h.DB.First(&originalConfig, id).Error; err != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "Config not found"})
|
||||
return
|
||||
}
|
||||
|
||||
// Check if user owns this config
|
||||
if originalConfig.CreatedBy != userID {
|
||||
// Check if user is admin
|
||||
isAdmin, exists := c.Get("isAdmin")
|
||||
if !exists || isAdmin != true {
|
||||
c.JSON(http.StatusForbidden, gin.H{"error": "You do not have permission to duplicate this config"})
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Create a duplicate config
|
||||
duplicateConfig := originalConfig
|
||||
duplicateConfig.ID = 0 // Set ID to 0 to create a new record
|
||||
duplicateConfig.Name = originalConfig.Name + " - Copy"
|
||||
duplicateConfig.CreatedAt = time.Now()
|
||||
duplicateConfig.UpdatedAt = time.Now()
|
||||
duplicateConfig.CreatedBy = userID
|
||||
|
||||
// Deep copy all boolean pointers
|
||||
skipProcessedVal := *originalConfig.SkipProcessedFiles
|
||||
duplicateConfig.SkipProcessedFiles = &skipProcessedVal
|
||||
|
||||
archiveEnabledVal := *originalConfig.ArchiveEnabled
|
||||
duplicateConfig.ArchiveEnabled = &archiveEnabledVal
|
||||
|
||||
deleteAfterTransferVal := *originalConfig.DeleteAfterTransfer
|
||||
duplicateConfig.DeleteAfterTransfer = &deleteAfterTransferVal
|
||||
|
||||
sourcePassiveModeVal := *originalConfig.SourcePassiveMode
|
||||
duplicateConfig.SourcePassiveMode = &sourcePassiveModeVal
|
||||
|
||||
destPassiveModeVal := *originalConfig.DestPassiveMode
|
||||
duplicateConfig.DestPassiveMode = &destPassiveModeVal
|
||||
|
||||
// Google Photos specific fields
|
||||
if originalConfig.DestReadOnly != nil {
|
||||
destReadOnlyVal := *originalConfig.DestReadOnly
|
||||
duplicateConfig.DestReadOnly = &destReadOnlyVal
|
||||
}
|
||||
|
||||
if originalConfig.SourceReadOnly != nil {
|
||||
sourceReadOnlyVal := *originalConfig.SourceReadOnly
|
||||
duplicateConfig.SourceReadOnly = &sourceReadOnlyVal
|
||||
}
|
||||
|
||||
if originalConfig.DestIncludeArchived != nil {
|
||||
destIncludeArchivedVal := *originalConfig.DestIncludeArchived
|
||||
duplicateConfig.DestIncludeArchived = &destIncludeArchivedVal
|
||||
}
|
||||
|
||||
if originalConfig.SourceIncludeArchived != nil {
|
||||
sourceIncludeArchivedVal := *originalConfig.SourceIncludeArchived
|
||||
duplicateConfig.SourceIncludeArchived = &sourceIncludeArchivedVal
|
||||
}
|
||||
|
||||
if originalConfig.UseBuiltinAuthSource != nil {
|
||||
useBuiltinAuthSourceVal := *originalConfig.UseBuiltinAuthSource
|
||||
duplicateConfig.UseBuiltinAuthSource = &useBuiltinAuthSourceVal
|
||||
}
|
||||
|
||||
if originalConfig.UseBuiltinAuthDest != nil {
|
||||
useBuiltinAuthDestVal := *originalConfig.UseBuiltinAuthDest
|
||||
duplicateConfig.UseBuiltinAuthDest = &useBuiltinAuthDestVal
|
||||
}
|
||||
|
||||
// Start a transaction
|
||||
tx := h.DB.Begin()
|
||||
if tx.Error != nil {
|
||||
log.Printf("Error beginning transaction: %v", tx.Error)
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to begin transaction"})
|
||||
return
|
||||
}
|
||||
|
||||
if err := tx.Create(&duplicateConfig).Error; err != nil {
|
||||
tx.Rollback()
|
||||
log.Printf("Error creating duplicate config: %v", err)
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("Failed to create duplicate config: %v", err)})
|
||||
return
|
||||
}
|
||||
|
||||
// Create audit log entry
|
||||
auditDetails := map[string]interface{}{
|
||||
"name": duplicateConfig.Name,
|
||||
"source_type": duplicateConfig.SourceType,
|
||||
"dest_type": duplicateConfig.DestinationType,
|
||||
"source_path": duplicateConfig.SourcePath,
|
||||
"dest_path": duplicateConfig.DestinationPath,
|
||||
"skip_processed_files": *duplicateConfig.SkipProcessedFiles,
|
||||
"archive_enabled": *duplicateConfig.ArchiveEnabled,
|
||||
"delete_after_transfer": *duplicateConfig.DeleteAfterTransfer,
|
||||
"source_passive_mode": *duplicateConfig.SourcePassiveMode,
|
||||
"dest_passive_mode": *duplicateConfig.DestPassiveMode,
|
||||
"duplicated_from": originalConfig.ID,
|
||||
}
|
||||
|
||||
auditLog := db.AuditLog{
|
||||
Action: "duplicate",
|
||||
EntityType: "config",
|
||||
EntityID: duplicateConfig.ID,
|
||||
UserID: userID,
|
||||
Details: auditDetails,
|
||||
Timestamp: time.Now(),
|
||||
}
|
||||
|
||||
if err := tx.Create(&auditLog).Error; err != nil {
|
||||
tx.Rollback()
|
||||
log.Printf("Error creating audit log: %v", err)
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create audit log"})
|
||||
return
|
||||
}
|
||||
|
||||
// Commit the transaction
|
||||
if err := tx.Commit().Error; err != nil {
|
||||
log.Printf("Error committing transaction: %v", err)
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to commit transaction"})
|
||||
return
|
||||
}
|
||||
|
||||
// Generate rclone config file for the duplicate
|
||||
if err := h.DB.GenerateRcloneConfig(&duplicateConfig); err != nil {
|
||||
log.Printf("Warning: Failed to generate rclone config for duplicate: %v", err)
|
||||
// Continue anyway, as the config was created in the database
|
||||
} else {
|
||||
log.Printf("Generated rclone config for duplicate config ID %d", duplicateConfig.ID)
|
||||
}
|
||||
|
||||
// Return with full page reload to show the new config
|
||||
c.Header("HX-Refresh", "true")
|
||||
c.JSON(http.StatusOK, gin.H{"message": "Config duplicated successfully"})
|
||||
}
|
||||
|
||||
@@ -733,3 +733,87 @@ func (h *Handlers) HandleRunJob(c *gin.Context) {
|
||||
successScript := fmt.Sprintf("<script>window.notyfInstance.success('Job \"%s\" has been started successfully')</script>", jobName)
|
||||
c.String(http.StatusOK, successScript)
|
||||
}
|
||||
|
||||
// HandleDuplicateJob handles duplication of a job
|
||||
func (h *Handlers) HandleDuplicateJob(c *gin.Context) {
|
||||
// Get the job ID from the URL
|
||||
idParam := c.Param("id")
|
||||
id, err := strconv.ParseUint(idParam, 10, 64)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid job ID"})
|
||||
return
|
||||
}
|
||||
|
||||
userID := c.GetUint("userID")
|
||||
|
||||
// Get the original job
|
||||
var originalJob db.Job
|
||||
if err := h.DB.First(&originalJob, id).Error; err != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "Job not found"})
|
||||
return
|
||||
}
|
||||
|
||||
// Create a new job as a copy of the original
|
||||
newJob := originalJob
|
||||
newJob.ID = 0 // Reset ID to create a new record
|
||||
newJob.Name = originalJob.Name + " - Copy"
|
||||
newJob.CreatedAt = time.Now()
|
||||
newJob.UpdatedAt = time.Now()
|
||||
|
||||
// Reset execution specific fields
|
||||
newJob.LastRun = nil
|
||||
newJob.NextRun = nil
|
||||
|
||||
// Save the new job
|
||||
if err := h.DB.Create(&newJob).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create duplicate job: " + err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
// If the job has associated configs, duplicate those associations
|
||||
configIDs := originalJob.GetConfigIDsList()
|
||||
if len(configIDs) > 0 {
|
||||
// Set the new job's config IDs
|
||||
newJob.SetConfigIDsList(configIDs)
|
||||
if err := h.DB.Save(&newJob).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to update config associations: " + err.Error()})
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Create audit log entry
|
||||
auditDetails := map[string]interface{}{
|
||||
"name": newJob.Name,
|
||||
"original_job_id": originalJob.ID,
|
||||
"new_job_id": newJob.ID,
|
||||
"schedule": newJob.Schedule,
|
||||
"enabled": newJob.GetEnabled(),
|
||||
"config_ids": configIDs,
|
||||
"webhook_enabled": newJob.GetWebhookEnabled(),
|
||||
"notify_on_success": newJob.GetNotifyOnSuccess(),
|
||||
"notify_on_failure": newJob.GetNotifyOnFailure(),
|
||||
}
|
||||
|
||||
auditLog := db.AuditLog{
|
||||
Action: "duplicate",
|
||||
EntityType: "job",
|
||||
EntityID: newJob.ID,
|
||||
UserID: userID,
|
||||
Details: auditDetails,
|
||||
Timestamp: time.Now(),
|
||||
}
|
||||
|
||||
if err := h.DB.Create(&auditLog).Error; err != nil {
|
||||
log.Printf("Warning: Failed to create audit log for job duplication: %v", err)
|
||||
// Continue anyway, as this is not critical
|
||||
}
|
||||
|
||||
// Schedule the job with the scheduler
|
||||
if err := h.Scheduler.ScheduleJob(&newJob); err != nil {
|
||||
log.Printf("Warning: Failed to schedule duplicated job: %v", err)
|
||||
// Continue anyway, as user can manually schedule later
|
||||
}
|
||||
|
||||
// Redirect to jobs page
|
||||
c.Redirect(http.StatusFound, "/jobs")
|
||||
}
|
||||
|
||||
@@ -0,0 +1,122 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/starfleetcptn/gomft/components"
|
||||
)
|
||||
|
||||
// HandleNotifications displays all notifications for the current user
|
||||
func (h *Handlers) HandleNotifications(c *gin.Context) {
|
||||
userID := c.GetUint("userID")
|
||||
|
||||
// Get notifications for the user
|
||||
notifications, err := h.DB.GetUserNotifications(userID, 50) // Get more for the full page
|
||||
if err != nil {
|
||||
c.String(http.StatusInternalServerError, "Failed to load notifications")
|
||||
return
|
||||
}
|
||||
|
||||
// Get unread count
|
||||
unreadCount, err := h.DB.GetUnreadNotificationCount(userID)
|
||||
if err != nil {
|
||||
c.String(http.StatusInternalServerError, "Failed to load notification count")
|
||||
return
|
||||
}
|
||||
|
||||
data := components.NotificationsData{
|
||||
Notifications: notifications,
|
||||
UnreadCount: unreadCount,
|
||||
}
|
||||
|
||||
// Render the notifications page
|
||||
components.NotificationsPage(c.Request.Context(), data).Render(c, c.Writer)
|
||||
}
|
||||
|
||||
// HandleLoadNotifications loads the notifications dropdown content
|
||||
func (h *Handlers) HandleLoadNotifications(c *gin.Context) {
|
||||
userID := c.GetUint("userID")
|
||||
|
||||
// Get 10 most recent notifications
|
||||
notifications, err := h.DB.GetUserNotifications(userID, 10)
|
||||
if err != nil {
|
||||
c.String(http.StatusInternalServerError, "Failed to load notifications")
|
||||
return
|
||||
}
|
||||
|
||||
// Get unread count
|
||||
unreadCount, err := h.DB.GetUnreadNotificationCount(userID)
|
||||
if err != nil {
|
||||
c.String(http.StatusInternalServerError, "Failed to load notification count")
|
||||
return
|
||||
}
|
||||
|
||||
data := components.NotificationsData{
|
||||
Notifications: notifications,
|
||||
UnreadCount: unreadCount,
|
||||
}
|
||||
|
||||
// Render just the dropdown content
|
||||
components.NotificationDropdown(data).Render(c, c.Writer)
|
||||
}
|
||||
|
||||
// HandleNotificationCount returns the notification count badge
|
||||
func (h *Handlers) HandleNotificationCount(c *gin.Context) {
|
||||
userID := c.GetUint("userID")
|
||||
|
||||
// Get unread count
|
||||
unreadCount, err := h.DB.GetUnreadNotificationCount(userID)
|
||||
|
||||
if err != nil {
|
||||
c.String(http.StatusInternalServerError, "Failed to load notification count")
|
||||
return
|
||||
}
|
||||
|
||||
// Render just the count badge
|
||||
components.NotificationCount(unreadCount).Render(c, c.Writer)
|
||||
}
|
||||
|
||||
// HandleMarkNotificationAsRead marks a single notification as read
|
||||
func (h *Handlers) HandleMarkNotificationAsRead(c *gin.Context) {
|
||||
userID := c.GetUint("userID")
|
||||
|
||||
// Get notification ID from path
|
||||
idParam := c.Param("id")
|
||||
id, err := strconv.ParseUint(idParam, 10, 64)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid notification ID"})
|
||||
return
|
||||
}
|
||||
|
||||
// Mark as read
|
||||
if err := h.DB.MarkNotificationAsRead(uint(id)); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to mark notification as read"})
|
||||
return
|
||||
}
|
||||
|
||||
// Return updated count
|
||||
unreadCount, err := h.DB.GetUnreadNotificationCount(userID)
|
||||
if err != nil {
|
||||
c.String(http.StatusInternalServerError, "Failed to load notification count")
|
||||
return
|
||||
}
|
||||
|
||||
// Return just the updated count badge
|
||||
components.NotificationCount(unreadCount).Render(c, c.Writer)
|
||||
}
|
||||
|
||||
// HandleMarkAllNotificationsAsRead marks all notifications for a user as read
|
||||
func (h *Handlers) HandleMarkAllNotificationsAsRead(c *gin.Context) {
|
||||
userID := c.GetUint("userID")
|
||||
|
||||
// Mark all as read
|
||||
if err := h.DB.MarkAllNotificationsAsRead(userID); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to mark notifications as read"})
|
||||
return
|
||||
}
|
||||
|
||||
// Return empty count (no more unread notifications)
|
||||
components.NotificationCount(0).Render(c, c.Writer)
|
||||
}
|
||||
@@ -35,6 +35,13 @@ func (h *Handlers) RegisterRoutes(router *gin.Engine) {
|
||||
authorized.GET("/profile/2fa/backup-codes", h.Handle2FABackupCodes)
|
||||
authorized.POST("/profile/2fa/regenerate-codes", h.Handle2FARegenerateCodes)
|
||||
|
||||
// Add notifications routes
|
||||
authorized.GET("/notifications", h.HandleNotifications)
|
||||
authorized.GET("/notifications/dropdown", h.HandleLoadNotifications)
|
||||
authorized.GET("/notifications/count", h.HandleNotificationCount)
|
||||
authorized.POST("/notifications/:id/read", h.HandleMarkNotificationAsRead)
|
||||
authorized.POST("/notifications/mark-all-read", h.HandleMarkAllNotificationsAsRead)
|
||||
|
||||
{
|
||||
authorized.GET("/dashboard", h.HandleDashboard)
|
||||
authorized.GET("/configs", h.HandleConfigs)
|
||||
@@ -44,6 +51,7 @@ func (h *Handlers) RegisterRoutes(router *gin.Engine) {
|
||||
authorized.PUT("/configs/:id", h.HandleUpdateConfig)
|
||||
authorized.POST("/configs/:id", h.HandleUpdateConfig)
|
||||
authorized.DELETE("/configs/:id", h.HandleDeleteConfig)
|
||||
authorized.POST("/configs/:id/duplicate", h.HandleDuplicateConfig)
|
||||
|
||||
// Path validation endpoint
|
||||
authorized.GET("/check-path", h.HandleCheckPath)
|
||||
@@ -60,6 +68,7 @@ func (h *Handlers) RegisterRoutes(router *gin.Engine) {
|
||||
authorized.PUT("/jobs/:id", h.HandleUpdateJob)
|
||||
authorized.POST("/jobs/:id", h.HandleUpdateJob)
|
||||
authorized.DELETE("/jobs/:id", h.HandleDeleteJob)
|
||||
authorized.POST("/jobs/:id/duplicate", h.HandleDuplicateJob)
|
||||
authorized.POST("/jobs/:id/run", h.HandleRunJob)
|
||||
authorized.GET("/history", h.HandleHistory)
|
||||
authorized.GET("/job-runs/:id", h.HandleJobRunDetails)
|
||||
@@ -132,6 +141,7 @@ func (h *Handlers) RegisterRoutes(router *gin.Engine) {
|
||||
settingsGroup.GET("", h.HandleSettings)
|
||||
settingsGroup.POST("/notifications", h.HandleCreateNotificationService)
|
||||
settingsGroup.DELETE("/notifications/:id", h.HandleDeleteNotificationService)
|
||||
settingsGroup.POST("/notifications/test", h.HandleTestNotification)
|
||||
settingsGroup.POST("/general", h.HandleSettings) // Placeholder for future implementation
|
||||
settingsGroup.POST("/security", h.HandleSettings) // Placeholder for future implementation
|
||||
}
|
||||
|
||||
@@ -1,56 +1,24 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"crypto/hmac"
|
||||
"crypto/sha256"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/starfleetcptn/gomft/components"
|
||||
"github.com/starfleetcptn/gomft/internal/db"
|
||||
)
|
||||
|
||||
// NotificationService represents a notification service configuration
|
||||
type NotificationService struct {
|
||||
ID uint `json:"id" gorm:"primaryKey"`
|
||||
Name string `json:"name" gorm:"not null"`
|
||||
Type string `json:"type" gorm:"not null"` // email, slack, webhook
|
||||
IsEnabled bool `json:"is_enabled" gorm:"default:true"`
|
||||
Config map[string]string `json:"config" gorm:"-"`
|
||||
ConfigJSON string `json:"-" gorm:"column:config"`
|
||||
Description string `json:"description"`
|
||||
EventTriggers string `json:"event_triggers" gorm:"column:event_triggers;default:'[]'"`
|
||||
PayloadTemplate string `json:"payload_template" gorm:"column:payload_template"`
|
||||
SecretKey string `json:"secret_key" gorm:"column:secret_key"`
|
||||
RetryPolicy string `json:"retry_policy" gorm:"column:retry_policy;default:'simple'"`
|
||||
LastUsed time.Time `json:"last_used" gorm:"column:last_used"`
|
||||
SuccessCount int `json:"success_count" gorm:"column:success_count;default:0"`
|
||||
FailureCount int `json:"failure_count" gorm:"column:failure_count;default:0"`
|
||||
CreatedBy uint `json:"created_by"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
|
||||
// BeforeSave converts Config map to JSON string for storage
|
||||
func (n *NotificationService) BeforeSave() error {
|
||||
configJSON, err := json.Marshal(n.Config)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
n.ConfigJSON = string(configJSON)
|
||||
return nil
|
||||
}
|
||||
|
||||
// AfterFind converts JSON string back to Config map
|
||||
func (n *NotificationService) AfterFind() error {
|
||||
if n.ConfigJSON != "" {
|
||||
return json.Unmarshal([]byte(n.ConfigJSON), &n.Config)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// HandleSettings handles GET /settings
|
||||
func (h *Handlers) HandleSettings(c *gin.Context) {
|
||||
// Check if the user has permission to view settings
|
||||
@@ -59,7 +27,7 @@ func (h *Handlers) HandleSettings(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
var notificationServices []NotificationService
|
||||
var notificationServices []db.NotificationService
|
||||
if err := h.DB.Find(¬ificationServices).Error; err != nil {
|
||||
log.Printf("Error fetching notification services: %v", err)
|
||||
}
|
||||
@@ -67,14 +35,6 @@ func (h *Handlers) HandleSettings(c *gin.Context) {
|
||||
// Convert to components.NotificationService
|
||||
var componentServices []components.NotificationService
|
||||
for _, service := range notificationServices {
|
||||
// Parse event triggers from JSON string to string slice
|
||||
var eventTriggers []string
|
||||
if service.EventTriggers != "" {
|
||||
if err := json.Unmarshal([]byte(service.EventTriggers), &eventTriggers); err != nil {
|
||||
log.Printf("Error parsing event triggers: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
componentServices = append(componentServices, components.NotificationService{
|
||||
ID: service.ID,
|
||||
Name: service.Name,
|
||||
@@ -82,7 +42,7 @@ func (h *Handlers) HandleSettings(c *gin.Context) {
|
||||
IsEnabled: service.IsEnabled,
|
||||
Config: service.Config,
|
||||
Description: service.Description,
|
||||
EventTriggers: eventTriggers,
|
||||
EventTriggers: service.EventTriggers,
|
||||
PayloadTemplate: service.PayloadTemplate,
|
||||
SecretKey: service.SecretKey,
|
||||
RetryPolicy: service.RetryPolicy,
|
||||
@@ -130,16 +90,17 @@ func (h *Handlers) HandleCreateNotificationService(c *gin.Context) {
|
||||
config["smtp_username"] = c.PostForm("smtp_username")
|
||||
config["smtp_password"] = c.PostForm("smtp_password")
|
||||
config["from_email"] = c.PostForm("from_email")
|
||||
case "slack":
|
||||
config["webhook_url"] = c.PostForm("webhook_url")
|
||||
config["channel"] = c.PostForm("channel")
|
||||
case "webhook":
|
||||
config["webhook_url"] = c.PostForm("webhook_url")
|
||||
config["method"] = c.PostForm("method")
|
||||
config["headers"] = c.PostForm("headers")
|
||||
|
||||
// Add the new webhook fields
|
||||
// Create event triggers JSON array
|
||||
// Create event triggers array
|
||||
// print all event triggers
|
||||
log.Printf("Event triggers: %v", c.PostForm("trigger_job_start"))
|
||||
log.Printf("Event triggers: %v", c.PostForm("trigger_job_complete"))
|
||||
log.Printf("Event triggers: %v", c.PostForm("trigger_job_error"))
|
||||
eventTriggers := make([]string, 0)
|
||||
if c.PostForm("trigger_job_start") == "on" {
|
||||
eventTriggers = append(eventTriggers, "job_start")
|
||||
@@ -151,21 +112,14 @@ func (h *Handlers) HandleCreateNotificationService(c *gin.Context) {
|
||||
eventTriggers = append(eventTriggers, "job_error")
|
||||
}
|
||||
|
||||
// Marshal the event triggers to JSON
|
||||
eventTriggersJSON, err := json.Marshal(eventTriggers)
|
||||
if err != nil {
|
||||
h.handleSettingsWithError(c, "Failed to process event triggers: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// Create new notification service with additional fields
|
||||
service := NotificationService{
|
||||
service := db.NotificationService{
|
||||
Name: name,
|
||||
Type: serviceType,
|
||||
IsEnabled: isEnabled,
|
||||
Config: config,
|
||||
Description: description,
|
||||
EventTriggers: string(eventTriggersJSON),
|
||||
EventTriggers: eventTriggers,
|
||||
PayloadTemplate: c.PostForm("payload_template"),
|
||||
SecretKey: c.PostForm("secret_key"),
|
||||
RetryPolicy: c.PostForm("retry_policy"),
|
||||
@@ -211,7 +165,7 @@ func (h *Handlers) HandleCreateNotificationService(c *gin.Context) {
|
||||
}
|
||||
|
||||
// Create new notification service
|
||||
service := NotificationService{
|
||||
service := db.NotificationService{
|
||||
Name: name,
|
||||
Type: serviceType,
|
||||
IsEnabled: isEnabled,
|
||||
@@ -267,14 +221,14 @@ func (h *Handlers) HandleDeleteNotificationService(c *gin.Context) {
|
||||
}
|
||||
|
||||
// Find service to delete (for audit log)
|
||||
var service NotificationService
|
||||
var service db.NotificationService
|
||||
if err := h.DB.First(&service, serviceID).Error; err != nil {
|
||||
h.handleSettingsWithError(c, "Notification service not found.")
|
||||
return
|
||||
}
|
||||
|
||||
// Delete the service
|
||||
if err := h.DB.Delete(&NotificationService{}, serviceID).Error; err != nil {
|
||||
if err := h.DB.Delete(&db.NotificationService{}, serviceID).Error; err != nil {
|
||||
log.Printf("Error deleting notification service: %v", err)
|
||||
h.handleSettingsWithError(c, "Failed to delete notification service: "+err.Error())
|
||||
return
|
||||
@@ -304,6 +258,224 @@ func (h *Handlers) HandleDeleteNotificationService(c *gin.Context) {
|
||||
h.handleSettingsWithSuccess(c, "Notification service deleted successfully.")
|
||||
}
|
||||
|
||||
// HandleTestNotification handles POST /settings/notifications/test
|
||||
// This endpoint tests a notification configuration without saving it
|
||||
func (h *Handlers) HandleTestNotification(c *gin.Context) {
|
||||
// Check if the user has permission to manage settings
|
||||
if !h.checkPermission(c, "system.settings") {
|
||||
c.JSON(http.StatusForbidden, gin.H{
|
||||
"success": false,
|
||||
"message": "You don't have permission to test notifications",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// Parse form data to create a test notification service
|
||||
name := c.PostForm("name")
|
||||
serviceType := c.PostForm("type")
|
||||
|
||||
// Validate required fields
|
||||
if name == "" || serviceType == "" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{
|
||||
"success": false,
|
||||
"message": "Name and type are required fields",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// Create config map based on service type
|
||||
config := make(map[string]string)
|
||||
|
||||
switch serviceType {
|
||||
case "email":
|
||||
config["smtp_host"] = c.PostForm("smtp_host")
|
||||
config["smtp_port"] = c.PostForm("smtp_port")
|
||||
config["smtp_username"] = c.PostForm("smtp_username")
|
||||
config["smtp_password"] = c.PostForm("smtp_password")
|
||||
config["from_email"] = c.PostForm("from_email")
|
||||
|
||||
// Basic validation
|
||||
if config["smtp_host"] == "" || config["smtp_port"] == "" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{
|
||||
"success": false,
|
||||
"message": "SMTP host and port are required for email notifications",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
case "webhook":
|
||||
config["webhook_url"] = c.PostForm("webhook_url")
|
||||
config["method"] = c.PostForm("method")
|
||||
config["headers"] = c.PostForm("headers")
|
||||
|
||||
// Basic validation
|
||||
if config["webhook_url"] == "" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{
|
||||
"success": false,
|
||||
"message": "Webhook URL is required for webhook notifications",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// Get additional webhook fields
|
||||
payloadTemplate := c.PostForm("payload_template")
|
||||
secretKey := c.PostForm("secret_key")
|
||||
|
||||
// Create and format sample payload
|
||||
samplePayload := generateSamplePayload(payloadTemplate)
|
||||
|
||||
// Send test webhook
|
||||
err := sendTestWebhook(config, samplePayload, secretKey)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{
|
||||
"success": false,
|
||||
"message": "Failed to send test webhook: " + err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": true,
|
||||
"message": "Test webhook sent successfully",
|
||||
})
|
||||
return
|
||||
|
||||
default:
|
||||
c.JSON(http.StatusBadRequest, gin.H{
|
||||
"success": false,
|
||||
"message": "Invalid notification service type",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// For email, simulate a successful test for now
|
||||
// In a real implementation, you would send an actual test notification
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": true,
|
||||
"message": fmt.Sprintf("Simulated %s notification test successful", serviceType),
|
||||
})
|
||||
}
|
||||
|
||||
// generateSamplePayload creates a sample payload for testing
|
||||
func generateSamplePayload(template string) string {
|
||||
// If no template provided, use a default sample
|
||||
if template == "" {
|
||||
return `{
|
||||
"event": "job_complete",
|
||||
"job": {
|
||||
"id": "sample-job-123",
|
||||
"name": "Test Job",
|
||||
"status": "completed",
|
||||
"message": "This is a test notification",
|
||||
"started_at": "` + time.Now().Add(-5*time.Minute).Format(time.RFC3339) + `",
|
||||
"completed_at": "` + time.Now().Format(time.RFC3339) + `",
|
||||
"duration_seconds": 300,
|
||||
"config_id": "config-456",
|
||||
"config_name": "Test Config",
|
||||
"transfer_bytes": 1024,
|
||||
"file_count": 5
|
||||
},
|
||||
"instance": {
|
||||
"id": "gomft-instance-1",
|
||||
"name": "GoMFT Test Instance",
|
||||
"version": "1.0.0",
|
||||
"environment": "testing"
|
||||
},
|
||||
"timestamp": "` + time.Now().Format(time.RFC3339) + `",
|
||||
"notification_id": "test-notification"
|
||||
}`
|
||||
}
|
||||
|
||||
// Replace placeholders in the template with sample values
|
||||
samplePayload := template
|
||||
// Replace common placeholders
|
||||
replacements := map[string]string{
|
||||
"{{job.id}}": "sample-job-123",
|
||||
"{{job.name}}": "Test Job",
|
||||
"{{job.status}}": "completed",
|
||||
"{{job.message}}": "This is a test notification",
|
||||
"{{job.event}}": "job_complete",
|
||||
"{{job.started_at}}": time.Now().Add(-5 * time.Minute).Format(time.RFC3339),
|
||||
"{{job.completed_at}}": time.Now().Format(time.RFC3339),
|
||||
"{{job.duration_seconds}}": "300",
|
||||
"{{job.config_id}}": "config-456",
|
||||
"{{job.config_name}}": "Test Config",
|
||||
"{{job.transfer_bytes}}": "1024",
|
||||
"{{job.file_count}}": "5",
|
||||
"{{instance.id}}": "gomft-instance-1",
|
||||
"{{instance.name}}": "GoMFT Test Instance",
|
||||
"{{instance.version}}": "1.0.0",
|
||||
"{{instance.environment}}": "testing",
|
||||
"{{timestamp}}": time.Now().Format(time.RFC3339),
|
||||
"{{notification.id}}": "test-notification",
|
||||
}
|
||||
|
||||
for placeholder, value := range replacements {
|
||||
samplePayload = strings.Replace(samplePayload, placeholder, value, -1)
|
||||
}
|
||||
|
||||
return samplePayload
|
||||
}
|
||||
|
||||
// sendTestWebhook sends a test webhook to the specified URL
|
||||
func sendTestWebhook(config map[string]string, payload string, secretKey string) error {
|
||||
webhookURL := config["webhook_url"]
|
||||
method := config["method"]
|
||||
if method == "" {
|
||||
method = "POST"
|
||||
}
|
||||
|
||||
// Create the request
|
||||
req, err := http.NewRequest(method, webhookURL, bytes.NewBufferString(payload))
|
||||
if err != nil {
|
||||
return fmt.Errorf("error creating request: %v", err)
|
||||
}
|
||||
|
||||
// Set default Content-Type if not specified
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
// Parse and set custom headers
|
||||
if config["headers"] != "" {
|
||||
var headers map[string]string
|
||||
if err := json.Unmarshal([]byte(config["headers"]), &headers); err == nil {
|
||||
for key, value := range headers {
|
||||
req.Header.Set(key, value)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Add signature if secret key is provided
|
||||
if secretKey != "" {
|
||||
signature := calculateSignature(payload, secretKey)
|
||||
req.Header.Set("X-GoMFT-Signature", signature)
|
||||
}
|
||||
|
||||
// Send the request
|
||||
client := &http.Client{
|
||||
Timeout: 10 * time.Second,
|
||||
}
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error sending webhook: %v", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
// Check the response
|
||||
if resp.StatusCode >= 400 {
|
||||
body, _ := io.ReadAll(resp.Body)
|
||||
return fmt.Errorf("webhook returned error %d: %s", resp.StatusCode, string(body))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// calculateSignature generates an HMAC signature for webhook payloads
|
||||
func calculateSignature(payload string, secretKey string) string {
|
||||
h := hmac.New(sha256.New, []byte(secretKey))
|
||||
h.Write([]byte(payload))
|
||||
return fmt.Sprintf("sha256=%x", h.Sum(nil))
|
||||
}
|
||||
|
||||
// Helper function to check if user has a specific permission
|
||||
func (h *Handlers) checkPermission(c *gin.Context, permission string) bool {
|
||||
// If user is admin, they have all permissions
|
||||
@@ -332,7 +504,7 @@ func (h *Handlers) checkPermission(c *gin.Context, permission string) bool {
|
||||
|
||||
// handleSettingsWithError renders the settings page with an error message
|
||||
func (h *Handlers) handleSettingsWithError(c *gin.Context, errorMessage string) {
|
||||
var notificationServices []NotificationService
|
||||
var notificationServices []db.NotificationService
|
||||
if err := h.DB.Find(¬ificationServices).Error; err != nil {
|
||||
log.Printf("Error fetching notification services: %v", err)
|
||||
}
|
||||
@@ -340,14 +512,6 @@ func (h *Handlers) handleSettingsWithError(c *gin.Context, errorMessage string)
|
||||
// Convert to components.NotificationService
|
||||
var componentServices []components.NotificationService
|
||||
for _, service := range notificationServices {
|
||||
// Parse event triggers from JSON string to string slice
|
||||
var eventTriggers []string
|
||||
if service.EventTriggers != "" {
|
||||
if err := json.Unmarshal([]byte(service.EventTriggers), &eventTriggers); err != nil {
|
||||
log.Printf("Error parsing event triggers: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
componentServices = append(componentServices, components.NotificationService{
|
||||
ID: service.ID,
|
||||
Name: service.Name,
|
||||
@@ -355,7 +519,7 @@ func (h *Handlers) handleSettingsWithError(c *gin.Context, errorMessage string)
|
||||
IsEnabled: service.IsEnabled,
|
||||
Config: service.Config,
|
||||
Description: service.Description,
|
||||
EventTriggers: eventTriggers,
|
||||
EventTriggers: service.EventTriggers,
|
||||
PayloadTemplate: service.PayloadTemplate,
|
||||
SecretKey: service.SecretKey,
|
||||
RetryPolicy: service.RetryPolicy,
|
||||
@@ -375,7 +539,7 @@ func (h *Handlers) handleSettingsWithError(c *gin.Context, errorMessage string)
|
||||
|
||||
// handleSettingsWithSuccess renders the settings page with a success message
|
||||
func (h *Handlers) handleSettingsWithSuccess(c *gin.Context, successMessage string) {
|
||||
var notificationServices []NotificationService
|
||||
var notificationServices []db.NotificationService
|
||||
if err := h.DB.Find(¬ificationServices).Error; err != nil {
|
||||
log.Printf("Error fetching notification services: %v", err)
|
||||
}
|
||||
@@ -383,14 +547,6 @@ func (h *Handlers) handleSettingsWithSuccess(c *gin.Context, successMessage stri
|
||||
// Convert to components.NotificationService
|
||||
var componentServices []components.NotificationService
|
||||
for _, service := range notificationServices {
|
||||
// Parse event triggers from JSON string to string slice
|
||||
var eventTriggers []string
|
||||
if service.EventTriggers != "" {
|
||||
if err := json.Unmarshal([]byte(service.EventTriggers), &eventTriggers); err != nil {
|
||||
log.Printf("Error parsing event triggers: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
componentServices = append(componentServices, components.NotificationService{
|
||||
ID: service.ID,
|
||||
Name: service.Name,
|
||||
@@ -398,7 +554,7 @@ func (h *Handlers) handleSettingsWithSuccess(c *gin.Context, successMessage stri
|
||||
IsEnabled: service.IsEnabled,
|
||||
Config: service.Config,
|
||||
Description: service.Description,
|
||||
EventTriggers: eventTriggers,
|
||||
EventTriggers: service.EventTriggers,
|
||||
PayloadTemplate: service.PayloadTemplate,
|
||||
SecretKey: service.SecretKey,
|
||||
RetryPolicy: service.RetryPolicy,
|
||||
|
||||
Reference in New Issue
Block a user