- if provider == nil || provider.Enabled {
+ if provider == nil || provider.GetEnabled() {
- if provider.Enabled {
+ if provider.GetEnabled() {
Active
diff --git a/components/auth_providers_buttons.templ b/components/auth_providers_buttons.templ
index 33aa3ef..02094f2 100644
--- a/components/auth_providers_buttons.templ
+++ b/components/auth_providers_buttons.templ
@@ -35,9 +35,9 @@ templ AuthProviderButtons(providers []db.AuthProvider) {
} else {
for _, provider := range providers {
- if provider.Enabled {
-
diff --git a/components/config_form.templ b/components/config_form.templ
index e106855..ca6b74e 100644
--- a/components/config_form.templ
+++ b/components/config_form.templ
@@ -290,33 +290,34 @@ func getInitialData(config *db.TransferConfig) string {
// This will need coordination with your backend to ensure the IDs match the commands
let commandName = '';
switch(parseInt(this.commandId)) {
- // Map command IDs to command names - adjust these based on your actual command IDs
+ // Correct mapping based on internal/db/migrations/009_add_rclone_tables.go
case 1: commandName = 'copy'; break;
- case 2: commandName = 'move'; break;
- case 3: commandName = 'sync'; break;
- case 4: commandName = 'ls'; break;
- case 5: commandName = 'lsd'; break;
- case 6: commandName = 'lsl'; break;
- case 7: commandName = 'lsf'; break;
- case 8: commandName = 'lsjson'; break;
- case 9: commandName = 'md5sum'; break;
- case 10: commandName = 'sha1sum'; break;
- case 11: commandName = 'size'; break;
- case 12: commandName = 'delete'; break;
- case 13: commandName = 'purge'; break;
- case 14: commandName = 'mkdir'; break;
- case 15: commandName = 'rmdir'; break;
- case 16: commandName = 'rmdirs'; break;
- case 17: commandName = 'check'; break;
- case 18: commandName = 'cleanup'; break;
- case 19: commandName = 'dedupe'; break;
- case 20: commandName = 'version'; break;
- case 21: commandName = 'listremotes'; break;
- case 22: commandName = 'cryptcheck'; break;
- case 23: commandName = 'bisync'; break;
- case 24: commandName = 'copyto'; break;
- case 25: commandName = 'moveto'; break;
- default: commandName = 'copy'; // Default to copy
+ case 2: commandName = 'sync'; break;
+ case 3: commandName = 'bisync'; break;
+ case 4: commandName = 'move'; break;
+ case 5: commandName = 'delete'; break;
+ case 6: commandName = 'purge'; break;
+ case 7: commandName = 'mkdir'; break;
+ case 8: commandName = 'rmdir'; break;
+ case 9: commandName = 'rmdirs'; break;
+ case 10: commandName = 'check'; break;
+ case 11: commandName = 'ls'; break;
+ case 12: commandName = 'lsd'; break;
+ case 13: commandName = 'lsl'; break;
+ case 14: commandName = 'lsf'; break;
+ case 15: commandName = 'lsjson'; break;
+ case 16: commandName = 'md5sum'; break;
+ case 17: commandName = 'sha1sum'; break;
+ case 18: commandName = 'size'; break;
+ case 19: commandName = 'version'; break;
+ case 20: commandName = 'cleanup'; break;
+ case 21: commandName = 'dedupe'; break;
+ case 22: commandName = 'copyto'; break;
+ case 23: commandName = 'moveto'; break;
+ case 24: commandName = 'listremotes'; break;
+ case 25: commandName = 'obscure'; break;
+ case 26: commandName = 'cryptcheck'; break;
+ default: commandName = 'copy'; // Default to copy if ID is unknown
}
console.log('Command ID:', this.commandId, 'Command Name:', commandName);
diff --git a/components/layout.templ b/components/layout.templ
index 7fe266f..b7c9006 100644
--- a/components/layout.templ
+++ b/components/layout.templ
@@ -142,7 +142,7 @@ templ LayoutWithContext(title string, ctx context.Context) {
}
-
+
if isLoggedIn(ctx) {
diff --git a/components/notifications/form/fields/gotify.templ b/components/notifications/form/fields/gotify.templ
index f177e90..b2709fa 100644
--- a/components/notifications/form/fields/gotify.templ
+++ b/components/notifications/form/fields/gotify.templ
@@ -2,7 +2,7 @@ package fields
import (
"github.com/starfleetcptn/gomft/components/notifications/types"
- "github.com/starfleetcptn/gomft/components/notifications/form/utils"
+ // "github.com/starfleetcptn/gomft/components/notifications/form/utils" // Removed as unused
)
templ GotifyFields(data types.NotificationFormData) {
@@ -59,35 +59,7 @@ templ GotifyFields(data types.NotificationFormData) {
}
Use placeholders for dynamic values. Available variables: job.*, instance.*, timestamp, notification.*
-
+ // Removed duplicate Event Triggers section - now handled in form.templ
diff --git a/components/notifications/form/fields/gotify_templ.go b/components/notifications/form/fields/gotify_templ.go
index c2e03e2..f4ec3e0 100644
--- a/components/notifications/form/fields/gotify_templ.go
+++ b/components/notifications/form/fields/gotify_templ.go
@@ -9,8 +9,8 @@ import "github.com/a-h/templ"
import templruntime "github.com/a-h/templ/runtime"
import (
- "github.com/starfleetcptn/gomft/components/notifications/form/utils"
"github.com/starfleetcptn/gomft/components/notifications/types"
+ // "github.com/starfleetcptn/gomft/components/notifications/form/utils" // Removed as unused
)
func GotifyFields(data types.NotificationFormData) templ.Component {
@@ -143,91 +143,7 @@ func GotifyFields(data types.NotificationFormData) templ.Component {
return templ_7745c5c3_Err
}
}
- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, "
Use placeholders for dynamic values. Available variables: job.*, instance.*, timestamp, notification.*
Event Triggers ")
- if templ_7745c5c3_Err != nil {
- return templ_7745c5c3_Err
- }
- if data.NotificationService.EventTriggers != nil {
- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, " ")
- if templ_7745c5c3_Err != nil {
- return templ_7745c5c3_Err
- }
- } else {
- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 21, " ")
- if templ_7745c5c3_Err != nil {
- return templ_7745c5c3_Err
- }
- }
- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 22, "Job Start
")
- if templ_7745c5c3_Err != nil {
- return templ_7745c5c3_Err
- }
- if data.NotificationService.EventTriggers != nil {
- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 23, " ")
- if templ_7745c5c3_Err != nil {
- return templ_7745c5c3_Err
- }
- } else {
- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 25, " ")
- if templ_7745c5c3_Err != nil {
- return templ_7745c5c3_Err
- }
- }
- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 26, "Job Complete
")
- if templ_7745c5c3_Err != nil {
- return templ_7745c5c3_Err
- }
- if data.NotificationService.EventTriggers != nil {
- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 27, " ")
- if templ_7745c5c3_Err != nil {
- return templ_7745c5c3_Err
- }
- } else {
- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 29, " ")
- if templ_7745c5c3_Err != nil {
- return templ_7745c5c3_Err
- }
- }
- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 30, "Job Error
Test Configuration Send Test NotificationSend a test notification to verify your Gotify configuration works correctly before saving.
")
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, "Use placeholders for dynamic values. Available variables: job.*, instance.*, timestamp, notification.*
Test Configuration Send Test NotificationSend a test notification to verify your Gotify configuration works correctly before saving.
")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
diff --git a/components/notifications/form/fields/ntfy.templ b/components/notifications/form/fields/ntfy.templ
index 79121b5..8291f52 100644
--- a/components/notifications/form/fields/ntfy.templ
+++ b/components/notifications/form/fields/ntfy.templ
@@ -2,7 +2,7 @@ package fields
import (
"github.com/starfleetcptn/gomft/components/notifications/types"
- "github.com/starfleetcptn/gomft/components/notifications/form/utils"
+ // "github.com/starfleetcptn/gomft/components/notifications/form/utils" // Removed as unused
)
templ NtfyFields(data types.NotificationFormData) {
@@ -76,35 +76,7 @@ templ NtfyFields(data types.NotificationFormData) {
}
Use placeholders for dynamic values. Available variables: job.*, instance.*, timestamp, notification.*
-
+ // Removed duplicate Event Triggers section - now handled in form.templ
diff --git a/components/notifications/form/fields/ntfy_templ.go b/components/notifications/form/fields/ntfy_templ.go
index 00d4d8a..dbf808e 100644
--- a/components/notifications/form/fields/ntfy_templ.go
+++ b/components/notifications/form/fields/ntfy_templ.go
@@ -9,8 +9,8 @@ import "github.com/a-h/templ"
import templruntime "github.com/a-h/templ/runtime"
import (
- "github.com/starfleetcptn/gomft/components/notifications/form/utils"
"github.com/starfleetcptn/gomft/components/notifications/types"
+ // "github.com/starfleetcptn/gomft/components/notifications/form/utils" // Removed as unused
)
func NtfyFields(data types.NotificationFormData) templ.Component {
@@ -199,91 +199,7 @@ func NtfyFields(data types.NotificationFormData) templ.Component {
return templ_7745c5c3_Err
}
}
- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 26, "
Use placeholders for dynamic values. Available variables: job.*, instance.*, timestamp, notification.*
Event Triggers ")
- if templ_7745c5c3_Err != nil {
- return templ_7745c5c3_Err
- }
- if data.NotificationService.EventTriggers != nil {
- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 27, " ")
- if templ_7745c5c3_Err != nil {
- return templ_7745c5c3_Err
- }
- } else {
- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 29, " ")
- if templ_7745c5c3_Err != nil {
- return templ_7745c5c3_Err
- }
- }
- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 30, "Job Start
")
- if templ_7745c5c3_Err != nil {
- return templ_7745c5c3_Err
- }
- if data.NotificationService.EventTriggers != nil {
- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 31, " ")
- if templ_7745c5c3_Err != nil {
- return templ_7745c5c3_Err
- }
- } else {
- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 33, " ")
- if templ_7745c5c3_Err != nil {
- return templ_7745c5c3_Err
- }
- }
- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 34, "Job Complete
")
- if templ_7745c5c3_Err != nil {
- return templ_7745c5c3_Err
- }
- if data.NotificationService.EventTriggers != nil {
- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 35, " ")
- if templ_7745c5c3_Err != nil {
- return templ_7745c5c3_Err
- }
- } else {
- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 37, " ")
- if templ_7745c5c3_Err != nil {
- return templ_7745c5c3_Err
- }
- }
- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 38, "Job Error
Test Configuration Send Test NotificationSend a test notification to verify your Ntfy configuration works correctly before saving.
")
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 26, "Use placeholders for dynamic values. Available variables: job.*, instance.*, timestamp, notification.*
Test Configuration Send Test NotificationSend a test notification to verify your Ntfy configuration works correctly before saving.
")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
diff --git a/components/notifications/form/fields/pushbullet.templ b/components/notifications/form/fields/pushbullet.templ
index 7c2b4e6..f0a59cf 100644
--- a/components/notifications/form/fields/pushbullet.templ
+++ b/components/notifications/form/fields/pushbullet.templ
@@ -2,7 +2,7 @@ package fields
import (
"github.com/starfleetcptn/gomft/components/notifications/types"
- "github.com/starfleetcptn/gomft/components/notifications/form/utils"
+ // "github.com/starfleetcptn/gomft/components/notifications/form/utils" // Removed as unused
)
templ PushbulletFields(data types.NotificationFormData) {
@@ -46,35 +46,7 @@ templ PushbulletFields(data types.NotificationFormData) {
}
Use placeholders for dynamic values. Available variables: job.*, instance.*, timestamp, notification.*
-
+ // Removed duplicate Event Triggers section - now handled in form.templ
diff --git a/components/notifications/form/fields/pushbullet_templ.go b/components/notifications/form/fields/pushbullet_templ.go
index 9eaf586..399fcd6 100644
--- a/components/notifications/form/fields/pushbullet_templ.go
+++ b/components/notifications/form/fields/pushbullet_templ.go
@@ -9,8 +9,8 @@ import "github.com/a-h/templ"
import templruntime "github.com/a-h/templ/runtime"
import (
- "github.com/starfleetcptn/gomft/components/notifications/form/utils"
"github.com/starfleetcptn/gomft/components/notifications/types"
+ // "github.com/starfleetcptn/gomft/components/notifications/form/utils" // Removed as unused
)
func PushbulletFields(data types.NotificationFormData) templ.Component {
@@ -128,91 +128,7 @@ func PushbulletFields(data types.NotificationFormData) templ.Component {
return templ_7745c5c3_Err
}
}
- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "
Use placeholders for dynamic values. Available variables: job.*, instance.*, timestamp, notification.*
Event Triggers ")
- if templ_7745c5c3_Err != nil {
- return templ_7745c5c3_Err
- }
- if data.NotificationService.EventTriggers != nil {
- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, " ")
- if templ_7745c5c3_Err != nil {
- return templ_7745c5c3_Err
- }
- } else {
- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, " ")
- if templ_7745c5c3_Err != nil {
- return templ_7745c5c3_Err
- }
- }
- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, "Job Start
")
- if templ_7745c5c3_Err != nil {
- return templ_7745c5c3_Err
- }
- if data.NotificationService.EventTriggers != nil {
- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 20, " ")
- if templ_7745c5c3_Err != nil {
- return templ_7745c5c3_Err
- }
- } else {
- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 22, " ")
- if templ_7745c5c3_Err != nil {
- return templ_7745c5c3_Err
- }
- }
- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 23, "Job Complete
")
- if templ_7745c5c3_Err != nil {
- return templ_7745c5c3_Err
- }
- if data.NotificationService.EventTriggers != nil {
- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 24, " ")
- if templ_7745c5c3_Err != nil {
- return templ_7745c5c3_Err
- }
- } else {
- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 26, " ")
- if templ_7745c5c3_Err != nil {
- return templ_7745c5c3_Err
- }
- }
- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 27, "Job Error
Test Configuration Send Test NotificationSend a test notification to verify your Pushbullet configuration works correctly before saving.
")
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "Use placeholders for dynamic values. Available variables: job.*, instance.*, timestamp, notification.*
Test Configuration Send Test NotificationSend a test notification to verify your Pushbullet configuration works correctly before saving.
")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
diff --git a/components/notifications/form/fields/pushover.templ b/components/notifications/form/fields/pushover.templ
index 82839d0..2ba71c5 100644
--- a/components/notifications/form/fields/pushover.templ
+++ b/components/notifications/form/fields/pushover.templ
@@ -2,7 +2,7 @@ package fields
import (
"github.com/starfleetcptn/gomft/components/notifications/types"
- "github.com/starfleetcptn/gomft/components/notifications/form/utils"
+ // "github.com/starfleetcptn/gomft/components/notifications/form/utils" // Removed as unused
)
templ PushoverFields(data types.NotificationFormData) {
@@ -97,35 +97,7 @@ templ PushoverFields(data types.NotificationFormData) {
}
Use placeholders for dynamic values. Available variables: job.*, instance.*, timestamp, notification.*
-
+ // Removed duplicate Event Triggers section - now handled in form.templ
diff --git a/components/notifications/form/fields/pushover_templ.go b/components/notifications/form/fields/pushover_templ.go
index 149c5fd..2d8081e 100644
--- a/components/notifications/form/fields/pushover_templ.go
+++ b/components/notifications/form/fields/pushover_templ.go
@@ -9,8 +9,8 @@ import "github.com/a-h/templ"
import templruntime "github.com/a-h/templ/runtime"
import (
- "github.com/starfleetcptn/gomft/components/notifications/form/utils"
"github.com/starfleetcptn/gomft/components/notifications/types"
+ // "github.com/starfleetcptn/gomft/components/notifications/form/utils" // Removed as unused
)
func PushoverFields(data types.NotificationFormData) templ.Component {
@@ -171,91 +171,7 @@ func PushoverFields(data types.NotificationFormData) templ.Component {
return templ_7745c5c3_Err
}
}
- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 22, "
Use placeholders for dynamic values. Available variables: job.*, instance.*, timestamp, notification.*
Event Triggers ")
- if templ_7745c5c3_Err != nil {
- return templ_7745c5c3_Err
- }
- if data.NotificationService.EventTriggers != nil {
- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 23, " ")
- if templ_7745c5c3_Err != nil {
- return templ_7745c5c3_Err
- }
- } else {
- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 25, " ")
- if templ_7745c5c3_Err != nil {
- return templ_7745c5c3_Err
- }
- }
- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 26, "Job Start
")
- if templ_7745c5c3_Err != nil {
- return templ_7745c5c3_Err
- }
- if data.NotificationService.EventTriggers != nil {
- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 27, " ")
- if templ_7745c5c3_Err != nil {
- return templ_7745c5c3_Err
- }
- } else {
- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 29, " ")
- if templ_7745c5c3_Err != nil {
- return templ_7745c5c3_Err
- }
- }
- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 30, "Job Complete
")
- if templ_7745c5c3_Err != nil {
- return templ_7745c5c3_Err
- }
- if data.NotificationService.EventTriggers != nil {
- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 31, " ")
- if templ_7745c5c3_Err != nil {
- return templ_7745c5c3_Err
- }
- } else {
- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 33, " ")
- if templ_7745c5c3_Err != nil {
- return templ_7745c5c3_Err
- }
- }
- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 34, "Job Error
Test Configuration Send Test NotificationSend a test notification to verify your Pushover configuration works correctly before saving.
")
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 22, "Use placeholders for dynamic values. Available variables: job.*, instance.*, timestamp, notification.*
Test Configuration Send Test NotificationSend a test notification to verify your Pushover configuration works correctly before saving.
")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
diff --git a/components/notifications/form/fields/webhook.templ b/components/notifications/form/fields/webhook.templ
index b844165..f248690 100644
--- a/components/notifications/form/fields/webhook.templ
+++ b/components/notifications/form/fields/webhook.templ
@@ -2,7 +2,7 @@ package fields
import (
"github.com/starfleetcptn/gomft/components/notifications/types"
- "github.com/starfleetcptn/gomft/components/notifications/form/utils"
+ // "github.com/starfleetcptn/gomft/components/notifications/form/utils" // Removed as unused
)
templ WebhookFields(data types.NotificationFormData) {
@@ -80,29 +80,7 @@ templ WebhookFields(data types.NotificationFormData) {
}
Use placeholders for dynamic values. Available variables: job.*, instance.*, timestamp, notification.*
-
+ // Removed duplicate Event Triggers section - now handled in form.templ
Secret Key (for signature verification)
if data.NotificationService.SecretKey != "" {
diff --git a/components/notifications/form/fields/webhook_templ.go b/components/notifications/form/fields/webhook_templ.go
index 0976873..d25f61b 100644
--- a/components/notifications/form/fields/webhook_templ.go
+++ b/components/notifications/form/fields/webhook_templ.go
@@ -9,8 +9,8 @@ import "github.com/a-h/templ"
import templruntime "github.com/a-h/templ/runtime"
import (
- "github.com/starfleetcptn/gomft/components/notifications/form/utils"
"github.com/starfleetcptn/gomft/components/notifications/types"
+ // "github.com/starfleetcptn/gomft/components/notifications/form/utils" // Removed as unused
)
func WebhookFields(data types.NotificationFormData) templ.Component {
@@ -137,151 +137,82 @@ func WebhookFields(data types.NotificationFormData) templ.Component {
return templ_7745c5c3_Err
}
}
- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, "
Use placeholders for dynamic values. Available variables: job.*, instance.*, timestamp, notification.*
Event Triggers ")
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, "
Use placeholders for dynamic values. Available variables: job.*, instance.*, timestamp, notification.*
Secret Key (for signature verification) ")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
- if data.NotificationService.EventTriggers != nil {
- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, " ")
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 20, "\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 21, "Job Start
")
- if templ_7745c5c3_Err != nil {
- return templ_7745c5c3_Err
- }
- if data.NotificationService.EventTriggers != nil {
- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 22, " ")
- if templ_7745c5c3_Err != nil {
- return templ_7745c5c3_Err
- }
- }
- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 24, "Job Complete
")
- if templ_7745c5c3_Err != nil {
- return templ_7745c5c3_Err
- }
- if data.NotificationService.EventTriggers != nil {
- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 25, " ")
- if templ_7745c5c3_Err != nil {
- return templ_7745c5c3_Err
- }
- }
- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 27, "Job Error
Secret Key (for signature verification) ")
- if templ_7745c5c3_Err != nil {
- return templ_7745c5c3_Err
- }
- if data.NotificationService.SecretKey != "" {
- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 28, "
")
- if templ_7745c5c3_Err != nil {
- return templ_7745c5c3_Err
- }
- }
- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 30, "
If provided, all webhooks will include an X-GoMFT-Signature header
Retry Policy ")
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 21, "If provided, all webhooks will include an X-GoMFT-Signature header
Retry Policy ")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if data.NotificationService.RetryPolicy != "" {
if data.NotificationService.RetryPolicy == "none" {
- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 31, "No retries ")
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 22, "No retries ")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
} else {
- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 32, "No retries ")
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 23, "No retries ")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 33, " ")
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 24, " ")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if data.NotificationService.RetryPolicy == "simple" {
- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 34, "Simple (3 retries) ")
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 25, "Simple (3 retries) ")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
} else {
- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 35, "Simple (3 retries) ")
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 26, "Simple (3 retries) ")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 36, " ")
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 27, " ")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if data.NotificationService.RetryPolicy == "exponential" {
- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 37, "Exponential backoff ")
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 28, "Exponential backoff ")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
} else {
- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 38, "Exponential backoff ")
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 29, "Exponential backoff ")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
} else {
- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 39, "No retries Simple (3 retries) Exponential backoff ")
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 30, "No retries Simple (3 retries) Exponential backoff ")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 40, "
Test Configuration Send Test NotificationSend a test notification to verify your configuration works correctly before saving.
")
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 31, "
Test Configuration Send Test NotificationSend a test notification to verify your configuration works correctly before saving.
")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
diff --git a/components/notifications/form/form.templ b/components/notifications/form/form.templ
index c7fbc1b..3a1aedb 100644
--- a/components/notifications/form/form.templ
+++ b/components/notifications/form/form.templ
@@ -102,17 +102,45 @@ templ NotificationForm(ctx context.Context, data types.NotificationFormData) {
@fields.GotifyFields(data)
@fields.PushoverFields(data)
+
+
+
Event Triggers
+
Select the job events that should trigger this notification.
+
+ for _, event := range []string{"job_start", "job_complete", "job_error"} {
+
+
+ { utils.FormatEventTriggerName(event) }
+
+ }
+
+
+
diff --git a/components/notifications/form/form_templ.go b/components/notifications/form/form_templ.go
index 0c93c66..7336dad 100644
--- a/components/notifications/form/form_templ.go
+++ b/components/notifications/form/form_templ.go
@@ -141,7 +141,7 @@ func NotificationForm(ctx context.Context, data types.NotificationFormData) temp
return templ_7745c5c3_Err
}
}
- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, " hx-target=\"body\">
Notification Type Select a type ")
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, " hx-target=\"#notification-form-container\">Notification Type Select a type ")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
@@ -284,50 +284,104 @@ func NotificationForm(ctx context.Context, data types.NotificationFormData) temp
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 32, "")
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 32, "
Event Triggers Select the job events that should trigger this notification.
")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
- if data.NotificationService.IsEnabled != false {
- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 33, " ")
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 34, "\" name=\"event_triggers[]\" type=\"checkbox\" value=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
- } else {
- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 35, " ")
+ var templ_7745c5c3_Var10 string
+ templ_7745c5c3_Var10, templ_7745c5c3_Err = templ.JoinStringErrs(event)
+ if templ_7745c5c3_Err != nil {
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/notifications/form/form.templ`, Line: 116, Col: 24}
+ }
+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var10))
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 35, "\" class=\"w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 rounded focus:ring-blue-500 dark:focus:ring-blue-600 dark:ring-offset-gray-800 focus:ring-2 dark:bg-gray-700 dark:border-gray-600\"")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ if utils.IsEventTriggerSelected(data.NotificationService, event, data.IsNew) {
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 36, " checked")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ }
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 37, "> ")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ var templ_7745c5c3_Var12 string
+ templ_7745c5c3_Var12, templ_7745c5c3_Err = templ.JoinStringErrs(utils.FormatEventTriggerName(event))
+ if templ_7745c5c3_Err != nil {
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/notifications/form/form.templ`, Line: 122, Col: 147}
+ }
+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var12))
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 39, "
")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 36, "
Enable this notification service
")
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 40, "
")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if data.IsNew {
- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 37, "Add Service")
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 43, "Add Service")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
} else {
- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 38, "Save Changes")
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 44, "Save Changes")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 39, "
Configure your notification service to receive alerts for job events. Different notification types have different configuration options.
")
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 45, "
Configure your notification service to receive alerts for job events. Different notification types have different configuration options.
")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
diff --git a/components/notifications/form/utils/utils.go b/components/notifications/form/utils/utils.go
index 18aaaa7..4310a6a 100644
--- a/components/notifications/form/utils/utils.go
+++ b/components/notifications/form/utils/utils.go
@@ -1,28 +1,88 @@
package utils
-// Contains checks if a string is in a slice.
-func Contains(slice []string, str string) bool {
- for _, s := range slice {
- if s == str {
- return true
- }
- }
- return false
-}
+import (
+ "strings"
+)
-// BoolToString converts bool to string for HTML attributes.
-// Returns "true" for true, "" for false (useful for checked attribute).
-func BoolToString(b bool) string {
- if b {
- return "true"
- }
- return ""
-}
-
-// GetNotificationFormTitle returns the appropriate title for the form.
+// GetNotificationFormTitle returns the title for the notification form page.
func GetNotificationFormTitle(isNew bool) string {
if isNew {
return "Add Notification Service"
}
return "Edit Notification Service"
}
+
+// Contains checks if a string slice contains a specific string.
+func Contains(slice []string, item string) bool {
+ for _, a := range slice {
+ if a == item {
+ return true
+ }
+ }
+ return false
+}
+
+// BoolToString converts a boolean to its string representation "true" or "false".
+// Useful for setting HTML attributes that expect string values.
+func BoolToString(b bool) string {
+ if b {
+ return "true"
+ }
+ return "false"
+}
+
+// IsEventTriggerSelected checks if a specific event trigger should be pre-selected.
+// It now accepts the anonymous struct type defined in types.NotificationFormData.
+// Defaults to checking 'job_complete' and 'job_error' when creating a new service.
+func IsEventTriggerSelected(service *struct {
+ ID uint
+ Name string
+ Description string
+ Type string
+ IsEnabled bool
+ EventTriggers []string
+ RetryPolicy string
+ WebhookURL string
+ Method string
+ Headers string
+ PayloadTemplate string
+ SecretKey string
+ PushbulletAPIKey string
+ PushbulletDeviceID string
+ PushbulletTitleTemplate string
+ PushbulletBodyTemplate string
+ NtfyServer string
+ NtfyTopic string
+ NtfyPriority string
+ NtfyUsername string
+ NtfyPassword string
+ NtfyTitleTemplate string
+ NtfyMessageTemplate string
+ GotifyURL string
+ GotifyToken string
+ GotifyPriority string
+ GotifyTitleTemplate string
+ GotifyMessageTemplate string
+ PushoverAPIToken string
+ PushoverUserKey string
+ PushoverDevice string
+ PushoverPriority string
+ PushoverSound string
+ PushoverTitleTemplate string
+ PushoverMessageTemplate string
+}, event string, isNew bool) bool {
+ if !isNew && service != nil {
+ // Use the Contains helper function
+ return Contains(service.EventTriggers, event)
+ }
+ // Default for new services: check complete and error
+ return isNew && (event == "job_complete" || event == "job_error")
+}
+
+// FormatEventTriggerName converts event trigger keys to human-readable names.
+func FormatEventTriggerName(event string) string {
+ // Replace underscores with spaces and capitalize words
+ name := strings.ReplaceAll(event, "_", " ")
+ name = strings.Title(name) // Use strings.Title for capitalization
+ return name
+}
diff --git a/go.mod b/go.mod
index ca18da0..d65fd64 100644
--- a/go.mod
+++ b/go.mod
@@ -12,6 +12,7 @@ require (
github.com/joho/godotenv v1.5.1
github.com/pquerna/otp v1.4.0
github.com/robfig/cron/v3 v3.0.1
+ github.com/stretchr/testify v1.10.0
golang.org/x/crypto v0.36.0
gopkg.in/natefinch/lumberjack.v2 v2.2.1
gorm.io/gorm v1.25.12
@@ -22,6 +23,7 @@ require (
github.com/bytedance/sonic v1.12.9 // indirect
github.com/bytedance/sonic/loader v0.2.3 // indirect
github.com/cloudwego/base64x v0.1.5 // indirect
+ github.com/davecgh/go-spew v1.1.1 // indirect
github.com/dustin/go-humanize v1.0.1 // indirect
github.com/gabriel-vasile/mimetype v1.4.8 // indirect
github.com/gin-contrib/sse v1.0.0 // indirect
@@ -43,6 +45,7 @@ require (
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/pelletier/go-toml/v2 v2.2.3 // indirect
+ github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/ugorji/go/codec v1.2.12 // indirect
diff --git a/internal/config/config.go b/internal/config/config.go
index 138a076..3ae3881 100644
--- a/internal/config/config.go
+++ b/internal/config/config.go
@@ -16,6 +16,7 @@ type Config struct {
Email EmailConfig `json:"email"`
BaseURL string `json:"base_url"` // Base URL for generating links in emails
TOTPEncryptKey string `json:"totp_encrypt_key"` // Encryption key for TOTP secrets
+ SkipSSLVerify bool `json:"skip_ssl_verify"` // Skip SSL verification for outgoing webhooks/notifications
}
type EmailConfig struct {
@@ -40,6 +41,7 @@ func Load() (*Config, error) {
JWTSecret: "change_this_to_a_secure_random_string",
BaseURL: "http://localhost:8080",
TOTPEncryptKey: "this-is-a-dev-key-not-for-production!", // Default development key
+ SkipSSLVerify: false, // Default to verifying SSL
Email: EmailConfig{
Enabled: false,
Host: "smtp.example.com",
@@ -119,6 +121,12 @@ func Load() (*Config, error) {
if emailRequireAuth := os.Getenv("EMAIL_REQUIRE_AUTH"); emailRequireAuth != "" {
cfg.Email.RequireAuth = strings.ToLower(emailRequireAuth) == "true"
}
+
+ // SSL Verification configuration
+ if sslVerify := os.Getenv("SSL_VERIFY"); sslVerify != "" {
+ // Default is true (verify), only set SkipSSLVerify to true if env var is explicitly "false"
+ cfg.SkipSSLVerify = strings.ToLower(sslVerify) == "false"
+ }
} else if !os.IsNotExist(err) {
return nil, err
} else {
@@ -148,6 +156,10 @@ func Load() (*Config, error) {
"EMAIL_REQUIRE_AUTH=" + strconv.FormatBool(cfg.Email.RequireAuth),
"EMAIL_USERNAME=" + cfg.Email.Username,
"EMAIL_PASSWORD=" + cfg.Email.Password,
+ "",
+ "# SSL Verification for outgoing notifications (webhooks, etc.)",
+ "# Set to false to disable SSL certificate verification (USE WITH CAUTION)",
+ "SSL_VERIFY=" + strconv.FormatBool(!cfg.SkipSSLVerify), // Default is true (verify)
}
if err := os.WriteFile(envPath, []byte(strings.Join(envContent, "\n")), 0644); err != nil {
diff --git a/internal/db/auth_provider.go b/internal/db/auth_provider.go
index b08c681..b32365f 100644
--- a/internal/db/auth_provider.go
+++ b/internal/db/auth_provider.go
@@ -28,7 +28,7 @@ type AuthProvider struct {
ID uint `gorm:"primarykey" json:"id"`
Name string `gorm:"not null" json:"name"`
Type ProviderType `gorm:"not null" json:"type"`
- Enabled bool `gorm:"default:true" json:"enabled"`
+ Enabled *bool `gorm:"default:true" json:"enabled"`
Description string `json:"description"`
ProviderURL string `json:"provider_url"`
ClientID string `json:"client_id"`
@@ -75,6 +75,21 @@ func (p *AuthProvider) SetConfig(data map[string]interface{}) error {
return nil
}
+// --- AuthProvider Helper Methods ---
+
+// GetEnabled returns the value of Enabled with a default if nil
+func (p *AuthProvider) GetEnabled() bool {
+ if p.Enabled == nil {
+ return true // Default to true if not set
+ }
+ return *p.Enabled
+}
+
+// SetEnabled sets the Enabled field
+func (p *AuthProvider) SetEnabled(value bool) {
+ p.Enabled = &value
+}
+
// ExternalUserIdentity represents a user identity from an external authentication provider
type ExternalUserIdentity struct {
ID uint `gorm:"primarykey" json:"id"`
diff --git a/internal/db/migrations/012_alter_boolean_defaults.go b/internal/db/migrations/012_alter_boolean_defaults.go
new file mode 100644
index 0000000..cfcbd42
--- /dev/null
+++ b/internal/db/migrations/012_alter_boolean_defaults.go
@@ -0,0 +1,281 @@
+package migrations
+
+import (
+ "encoding/json"
+ "fmt"
+ "os"
+ "path/filepath"
+ "time"
+
+ "github.com/go-gormigrate/gormigrate/v2"
+ "gorm.io/gorm"
+)
+
+// AlterBooleanDefaults changes boolean columns with default:true to pointers
+// using explicit table recreation with raw SQL for SQLite compatibility.
+func AlterBooleanDefaults() *gormigrate.Migration {
+
+ // --- Raw SQL CREATE TABLE statements for the target schema ---
+
+ const createNotificationServicesSQL = `
+ CREATE TABLE notification_services (
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
+ name TEXT NOT NULL,
+ type TEXT NOT NULL,
+ is_enabled INTEGER DEFAULT 1, -- Target: *bool, SQLite uses 0/1, default true
+ config TEXT,
+ description TEXT,
+ event_triggers TEXT DEFAULT '[]',
+ payload_template TEXT,
+ secret_key TEXT,
+ retry_policy TEXT DEFAULT 'simple',
+ last_used timestamp,
+ success_count INTEGER DEFAULT 0,
+ failure_count INTEGER DEFAULT 0,
+ created_by INTEGER,
+ created_at timestamp,
+ updated_at timestamp
+ );`
+
+ const createAuthProvidersSQL = `
+ CREATE TABLE auth_providers (
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
+ name TEXT NOT NULL,
+ type TEXT NOT NULL,
+ enabled INTEGER DEFAULT 1, -- Target: *bool, SQLite uses 0/1, default true
+ description TEXT,
+ provider_url TEXT,
+ client_id TEXT,
+ client_secret TEXT,
+ redirect_url TEXT,
+ scopes TEXT,
+ attribute_mapping TEXT,
+ config TEXT,
+ icon_url TEXT,
+ successful_logins INTEGER DEFAULT 0,
+ last_used timestamp,
+ created_at timestamp,
+ updated_at timestamp
+ );`
+
+ const createTransferConfigsSQL = `
+ CREATE TABLE transfer_configs (
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
+ name TEXT NOT NULL,
+ source_type TEXT NOT NULL,
+ source_path TEXT NOT NULL,
+ source_host TEXT,
+ source_port INTEGER DEFAULT 22,
+ source_user TEXT,
+ source_key_file TEXT,
+ source_bucket TEXT,
+ source_region TEXT,
+ source_access_key TEXT,
+ source_endpoint TEXT,
+ source_share TEXT,
+ source_domain TEXT,
+ source_passive_mode INTEGER DEFAULT 1, -- Already *bool, keep default
+ source_client_id TEXT,
+ source_drive_id TEXT,
+ source_team_drive TEXT,
+ source_read_only INTEGER,
+ source_start_year INTEGER,
+ source_include_archived INTEGER,
+ file_pattern TEXT DEFAULT '*',
+ output_pattern TEXT,
+ destination_type TEXT NOT NULL,
+ destination_path TEXT NOT NULL,
+ dest_host TEXT,
+ dest_port INTEGER DEFAULT 22,
+ dest_user TEXT,
+ dest_key_file TEXT,
+ dest_bucket TEXT,
+ dest_region TEXT,
+ dest_access_key TEXT,
+ dest_endpoint TEXT,
+ dest_share TEXT,
+ dest_domain TEXT,
+ dest_passive_mode INTEGER DEFAULT 1, -- Already *bool, keep default
+ dest_client_id TEXT,
+ dest_drive_id TEXT,
+ dest_team_drive TEXT,
+ dest_read_only INTEGER,
+ dest_start_year INTEGER,
+ dest_include_archived INTEGER,
+ use_builtin_auth_source INTEGER,
+ use_builtin_auth_dest INTEGER,
+ google_drive_authenticated INTEGER,
+ archive_path TEXT,
+ archive_enabled INTEGER DEFAULT 0,
+ rclone_flags TEXT,
+ command_id INTEGER DEFAULT 1,
+ command_flags TEXT,
+ command_flag_values TEXT,
+ delete_after_transfer INTEGER DEFAULT 0,
+ skip_processed_files INTEGER DEFAULT 1, -- Target: *bool, SQLite uses 0/1, default true
+ max_concurrent_transfers INTEGER DEFAULT 4,
+ created_by INTEGER,
+ created_at timestamp,
+ updated_at timestamp
+ );`
+
+ // --- End Raw SQL ---
+
+ return &gormigrate.Migration{
+ ID: "012_alter_boolean_defaults",
+ Migrate: func(tx *gorm.DB) error {
+ // --- Backup Logic (copied) ---
+ var count int64
+ if err := tx.Raw("SELECT count(*) FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%'").Scan(&count).Error; err != nil {
+ return fmt.Errorf("failed to check for existing tables: %v", err)
+ }
+ if count > 0 {
+ sqlDB, err := tx.DB()
+ if err != nil {
+ return fmt.Errorf("failed to get underlying database: %v", err)
+ }
+ var seq int
+ var name, dbPath string
+ if err := sqlDB.QueryRow("PRAGMA database_list").Scan(&seq, &name, &dbPath); err != nil {
+ return fmt.Errorf("failed to get database path: %v", err)
+ }
+ backupDir := os.Getenv("BACKUP_DIR")
+ if backupDir == "" {
+ backupDir = "/app/backups"
+ if _, err := os.Stat(backupDir); os.IsNotExist(err) {
+ backupDir = "backups"
+ }
+ }
+ if err := os.MkdirAll(backupDir, 0755); err != nil {
+ return fmt.Errorf("failed to create backup directory: %v", err)
+ }
+ dbFileName := filepath.Base(dbPath)
+ backupFileName := fmt.Sprintf("%s.backup.%s", dbFileName, time.Now().Format("20060102_150405"))
+ backupFile := filepath.Join(backupDir, backupFileName)
+ data, err := os.ReadFile(dbPath)
+ if err != nil {
+ return fmt.Errorf("failed to read database for backup: %v", err)
+ }
+ if err := os.WriteFile(backupFile, data, 0600); err != nil {
+ return fmt.Errorf("failed to create database backup: %v", err)
+ }
+ fmt.Printf("Created database backup at: %s\n", backupFile)
+ }
+ // --- End Backup Logic ---
+
+ // --- Table Recreation Logic for SQLite ---
+ if err := tx.Exec("PRAGMA foreign_keys = OFF").Error; err != nil {
+ return fmt.Errorf("failed to disable foreign keys: %w", err)
+ }
+ defer func() {
+ if err := tx.Exec("PRAGMA foreign_keys = ON").Error; err != nil {
+ fmt.Printf("Warning: failed to re-enable foreign keys: %v\n", err)
+ }
+ }()
+
+ // Helper function for table recreation
+ recreateTable := func(tableName, createSQL string) error {
+ fmt.Printf("Recreating table %s...\n", tableName)
+ oldTableName := fmt.Sprintf("_%s_old", tableName)
+
+ // Rename old table
+ if err := tx.Exec(fmt.Sprintf("ALTER TABLE %s RENAME TO %s", tableName, oldTableName)).Error; err == nil {
+ fmt.Printf("Renamed %s to %s.\n", tableName, oldTableName)
+
+ // Create new table using raw SQL
+ fmt.Printf("Creating new %s table...\n", tableName)
+ if err := tx.Exec(createSQL).Error; err != nil {
+ return fmt.Errorf("failed to create new %s table: %w", tableName, err)
+ }
+ fmt.Printf("New %s table created.\n", tableName)
+
+ // Copy data
+ fmt.Printf("Copying data to new %s table...\n", tableName)
+ // IMPORTANT: Ensure column order/names match if schema changed beyond types/defaults
+ if err := tx.Exec(fmt.Sprintf("INSERT INTO %s SELECT * FROM %s", tableName, oldTableName)).Error; err != nil {
+ return fmt.Errorf("failed to copy data to new %s table: %w", tableName, err)
+ }
+ fmt.Printf("Data copied to %s.\n", tableName)
+
+ // Drop old table
+ if err := tx.Exec(fmt.Sprintf("DROP TABLE %s", oldTableName)).Error; err != nil {
+ return fmt.Errorf("failed to drop old %s table: %w", tableName, err)
+ }
+ fmt.Printf("Successfully recreated %s.\n", tableName)
+ } else {
+ // Check if rename failed because table doesn't exist (fresh install)
+ var tableExists int
+ tx.Raw(fmt.Sprintf("SELECT count(*) FROM sqlite_master WHERE type='table' AND name='%s'", tableName)).Scan(&tableExists)
+ if tableExists == 0 {
+ fmt.Printf("%s table does not exist, creating.\n", tableName)
+ if err := tx.Exec(createSQL).Error; err != nil { // Create table directly
+ return fmt.Errorf("failed to create new %s table: %w", tableName, err)
+ }
+ } else {
+ return fmt.Errorf("failed to rename %s: %w", tableName, err) // Real rename error
+ }
+ }
+ return nil
+ }
+
+ // Recreate tables
+ if err := recreateTable("notification_services", createNotificationServicesSQL); err != nil {
+ return err
+ }
+ if err := recreateTable("auth_providers", createAuthProvidersSQL); err != nil {
+ return err
+ }
+ if err := recreateTable("transfer_configs", createTransferConfigsSQL); err != nil {
+ return err
+ }
+
+ // --- End Table Recreation Logic ---
+
+ // Create audit log entry
+ now := time.Now()
+ details, auditErr := json.Marshal(map[string]interface{}{
+ "tables_affected": []string{"notification_services", "auth_providers", "transfer_configs"},
+ "columns_altered": []string{"is_enabled", "enabled", "skip_processed_files"},
+ "new_type": "*bool (pointer to boolean)",
+ "method": "Table recreation (SQLite - Raw SQL)",
+ "message": "Changed boolean columns with default:true to pointers to handle false values correctly with GORM.",
+ })
+ if auditErr != nil {
+ fmt.Printf("Warning: Failed to marshal audit log details: %v\n", auditErr)
+ }
+
+ if auditErr == nil {
+ if auditExecErr := tx.Exec(`
+ INSERT INTO audit_logs (action, entity_type, entity_id, user_id, details, created_at, updated_at, timestamp)
+ VALUES ('schema_update', 'multiple_tables', 0, 1, ?, ?, ?, ?)
+ `, string(details), now, now, now).Error; auditExecErr != nil {
+ fmt.Printf("Warning: Failed to insert audit log: %v\n", auditExecErr)
+ }
+ }
+
+ return nil
+ },
+ Rollback: func(tx *gorm.DB) error {
+ // Rollback is complex and risky with table recreation. Log skip.
+ now := time.Now()
+ details, err := json.Marshal(map[string]interface{}{
+ "migration_id": "012_alter_boolean_defaults",
+ "message": "Skipping rollback of boolean column type changes (via table recreation) due to complexity/potential data loss.",
+ })
+ if err != nil {
+ fmt.Printf("Warning: Failed to marshal rollback audit log details: %v\n", err)
+ }
+
+ if err == nil {
+ if auditExecErr := tx.Exec(`
+ INSERT INTO audit_logs (action, entity_type, entity_id, user_id, details, created_at, updated_at, timestamp)
+ VALUES ('migration_rollback', 'multiple_tables', 0, 1, ?, ?, ?, ?)
+ `, string(details), now, now, now).Error; auditExecErr != nil {
+ fmt.Printf("Warning: Failed to insert rollback audit log: %v\n", auditExecErr)
+ }
+ }
+ fmt.Println("Rollback for migration 012_alter_boolean_defaults skipped for safety.")
+ return nil
+ },
+ }
+}
diff --git a/internal/db/migrations/migrations.go b/internal/db/migrations/migrations.go
index f4389f9..4bfbaa2 100644
--- a/internal/db/migrations/migrations.go
+++ b/internal/db/migrations/migrations.go
@@ -22,6 +22,7 @@ func GetMigrations(db *gorm.DB) *gormigrate.Gormigrate {
AddRcloneTables(), // 009
AddRcloneCommandToConfig(), // 010
AddAuthProviders(), // 011
+ AlterBooleanDefaults(), // 012
)
return gormigrate.New(db, gormigrate.DefaultOptions, migrations)
diff --git a/internal/db/notification.go b/internal/db/notification.go
index dcc0d75..37b214e 100644
--- a/internal/db/notification.go
+++ b/internal/db/notification.go
@@ -12,7 +12,7 @@ type NotificationService struct {
ID uint `json:"id" gorm:"primaryKey"`
Name string `json:"name" gorm:"not null"`
Type string `json:"type" gorm:"not null"` // email, webhook
- IsEnabled bool `json:"is_enabled" gorm:"default:true"`
+ 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"`
@@ -69,6 +69,8 @@ func (db *DB) GetNotificationServices(onlyEnabled bool) ([]NotificationService,
query := db.DB
if onlyEnabled {
+ // When using a pointer, we need to explicitly check for true
+ // GORM handles the underlying SQL correctly for different dialects
query = query.Where("is_enabled = ?", true)
}
@@ -102,3 +104,21 @@ func (db *DB) UpdateNotificationService(service *NotificationService) error {
func (db *DB) DeleteNotificationService(id uint) error {
return db.Delete(&NotificationService{}, id).Error
}
+
+// --- NotificationService Helper Methods ---
+
+// GetIsEnabled returns the value of IsEnabled with a default if nil
+func (n *NotificationService) GetIsEnabled() bool {
+ if n.IsEnabled == nil {
+ // If the pointer is nil, GORM might not have set it,
+ // or it was explicitly set to nil. We assume the DB default (true)
+ // if it's nil, aligning with the original gorm tag default.
+ return true
+ }
+ return *n.IsEnabled
+}
+
+// SetIsEnabled sets the IsEnabled field
+func (n *NotificationService) SetIsEnabled(value bool) {
+ n.IsEnabled = &value
+}
diff --git a/internal/db/transfer_config.go b/internal/db/transfer_config.go
index c18373c..bc60519 100644
--- a/internal/db/transfer_config.go
+++ b/internal/db/transfer_config.go
@@ -25,7 +25,7 @@ type TransferConfig struct {
SourceShare string `form:"source_share"`
SourceDomain string `form:"source_domain"`
// FTP source fields
- SourcePassiveMode *bool `gorm:"default:true" form:"source_passive_mode"`
+ SourcePassiveMode *bool `gorm:"default:true" form:"source_passive_mode"` // Already a pointer, no change needed here
// OneDrive and Google Drive source fields
SourceClientID string `form:"source_client_id"`
SourceClientSecret string `form:"source_client_secret" gorm:"-"` // Not stored in DB, only used for form
diff --git a/internal/email/email_test.go b/internal/email/email_test.go
new file mode 100644
index 0000000..417eb29
--- /dev/null
+++ b/internal/email/email_test.go
@@ -0,0 +1,201 @@
+package email
+
+import (
+ "fmt"
+ "strings"
+ "testing"
+ "time"
+
+ "github.com/starfleetcptn/gomft/internal/config"
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+)
+
+func createTestConfig(enabled bool) *config.Config {
+ return &config.Config{
+ BaseURL: "http://localhost:8080",
+ Email: config.EmailConfig{
+ Enabled: enabled,
+ Host: "smtp.example.com",
+ Port: 587,
+ Username: "user",
+ Password: "password",
+ FromEmail: "noreply@example.com",
+ FromName: "GoMFT Test",
+ RequireAuth: true,
+ EnableTLS: true,
+ ReplyTo: "support@example.com",
+ },
+ }
+}
+
+func TestNewService(t *testing.T) {
+ cfg := createTestConfig(true)
+ service := NewService(cfg)
+ require.NotNil(t, service)
+ assert.Equal(t, cfg, service.Config)
+}
+
+func TestGeneratePasswordResetEmailHTML(t *testing.T) {
+ cfg := createTestConfig(true)
+ service := NewService(cfg)
+
+ testUsername := "testuser"
+ testResetLink := "http://localhost:8080/reset-password?token=testtoken123"
+ testAppName := "GoMFT"
+ testYear := time.Now().Year()
+
+ data := map[string]interface{}{
+ "Username": testUsername,
+ "ResetLink": testResetLink,
+ "AppName": testAppName,
+ "Year": testYear,
+ "ExpiresHours": 0.25,
+ }
+
+ htmlContent, err := service.generatePasswordResetEmailHTML(data)
+ require.NoError(t, err)
+ require.NotEmpty(t, htmlContent)
+
+ // Basic checks for content presence
+ assert.Contains(t, htmlContent, "Reset Your Password")
+ assert.Contains(t, htmlContent, fmt.Sprintf("Hello %s", testUsername))
+ assert.Contains(t, htmlContent, testResetLink) // Check link appears (both in button and text)
+ assert.Contains(t, htmlContent, fmt.Sprintf("href=\"%s\"", testResetLink))
+ assert.Contains(t, htmlContent, fmt.Sprintf("© %d %s", testYear, testAppName))
+ assert.Contains(t, htmlContent, "This link will expire in 15 minutes.")
+
+ // Test without username
+ dataNoUser := map[string]interface{}{
+ "ResetLink": testResetLink,
+ "AppName": testAppName,
+ "Year": testYear,
+ "ExpiresHours": 0.25,
+ }
+ htmlContentNoUser, err := service.generatePasswordResetEmailHTML(dataNoUser)
+ require.NoError(t, err)
+ assert.Contains(t, htmlContentNoUser, "Hello,") // Should just say Hello,
+ assert.NotContains(t, htmlContentNoUser, fmt.Sprintf("Hello %s", testUsername))
+}
+
+func TestGenerateTestEmailHTML(t *testing.T) {
+ cfg := createTestConfig(true)
+ service := NewService(cfg)
+
+ testSubject := "My Test Subject"
+ testMessage := "This is the test message body."
+ testAppName := "GoMFT"
+ testYear := time.Now().Year()
+ testCurrentTime := time.Now().Format(time.RFC1123Z) // Use the same format
+
+ data := map[string]interface{}{
+ "Subject": testSubject,
+ "Message": testMessage,
+ "AppName": testAppName,
+ "Year": testYear,
+ "SMTPServer": cfg.Email.Host,
+ "SMTPPort": cfg.Email.Port,
+ "FromEmail": cfg.Email.FromEmail,
+ "CurrentTime": testCurrentTime,
+ }
+
+ htmlContent, err := service.generateTestEmailHTML(data)
+ require.NoError(t, err)
+ require.NotEmpty(t, htmlContent)
+
+ // Basic checks for content presence
+ assert.Contains(t, htmlContent, fmt.Sprintf("%s ", testSubject))
+ assert.Contains(t, htmlContent, fmt.Sprintf("%s ", testSubject))
+ assert.Contains(t, htmlContent, testMessage)
+ assert.Contains(t, htmlContent, fmt.Sprintf("%s:%d", cfg.Email.Host, cfg.Email.Port))
+ assert.Contains(t, htmlContent, cfg.Email.FromEmail)
+ assert.Contains(t, htmlContent, testCurrentTime)
+ assert.Contains(t, htmlContent, fmt.Sprintf("© %d %s", testYear, testAppName))
+}
+
+func TestSendPasswordResetEmail_Disabled(t *testing.T) {
+ cfg := createTestConfig(false) // Email disabled
+ service := NewService(cfg)
+
+ toEmail := "test@example.com"
+ username := "testuser"
+ resetToken := "disabledtoken123"
+
+ err := service.SendPasswordResetEmail(toEmail, username, resetToken)
+ require.Error(t, err)
+
+ expectedErrorSubstr := fmt.Sprintf("email service is disabled, reset link would be: %s/reset-password?token=%s",
+ cfg.BaseURL, resetToken)
+ assert.Contains(t, err.Error(), expectedErrorSubstr)
+}
+
+func TestSendTestEmail_Disabled(t *testing.T) {
+ cfg := createTestConfig(false) // Email disabled
+ service := NewService(cfg)
+
+ toEmail := "test@example.com"
+
+ err := service.SendTestEmail(toEmail, "Test Subject", "Test Message")
+ require.Error(t, err)
+ assert.EqualError(t, err, "email service is disabled")
+}
+
+// --- Placeholder/TODO for more complex tests ---
+
+// TODO: TestSendPasswordResetEmail_Enabled - Requires mocking sendEmail or SMTP interactions
+// TODO: TestSendTestEmail_Enabled - Requires mocking sendEmail or SMTP interactions
+// TODO: TestSendEmail - Requires extensive mocking of net/smtp package
+
+// Example structure for testing enabled path (without actual sending/mocking)
+// This verifies the function prepares the correct data before calling sendEmail
+func TestSendPasswordResetEmail_Enabled_DataPreparation(t *testing.T) {
+ cfg := createTestConfig(true)
+ service := NewService(cfg)
+
+ // We need a way to intercept the call to sendEmail or verify its inputs
+ // For now, we just check that no error occurs up to that point
+ // and that the HTML generation works (implicitly tested by TestGeneratePasswordResetEmailHTML)
+
+ toEmail := "recipient@example.com"
+ username := "testuser-enabled"
+ resetToken := "enabledtoken456"
+
+ // If generatePasswordResetEmailHTML works, this call should proceed
+ // without error until the actual sendEmail call (which we aren't testing here)
+ // A full test would mock sendEmail and verify the arguments passed to it.
+ err := service.SendPasswordResetEmail(toEmail, username, resetToken)
+
+ // In a real scenario without mocking, this might fail if SMTP connection fails.
+ // For this basic check, we assume HTML generation is the main potential failure point *before* sendEmail.
+ // If TestGeneratePasswordResetEmailHTML passes, we expect no error *from generation*.
+ // We cannot assert assert.NoError(t, err) reliably without mocking sendEmail.
+ t.Logf("SendPasswordResetEmail (enabled) returned: %v (expected success or SMTP error)", err)
+ // Asserting that the error, if any, is NOT related to template generation could be a weak check.
+ if err != nil {
+ assert.False(t, strings.Contains(err.Error(), "template"), "Error should be SMTP related, not template related")
+ }
+}
+
+func TestSendTestEmail_Enabled_DataPreparation(t *testing.T) {
+ cfg := createTestConfig(true)
+ service := NewService(cfg)
+
+ toEmail := "recipient@example.com"
+ subject := "Specific Test Subject"
+ message := "Specific test message."
+
+ // Test with specific subject and message
+ err := service.SendTestEmail(toEmail, subject, message)
+ t.Logf("SendTestEmail (enabled, specific) returned: %v (expected success or SMTP error)", err)
+ if err != nil {
+ assert.False(t, strings.Contains(err.Error(), "template"), "Error should be SMTP related, not template related")
+ }
+
+ // Test with default subject and message
+ errDefault := service.SendTestEmail(toEmail, "", "")
+ t.Logf("SendTestEmail (enabled, default) returned: %v (expected success or SMTP error)", errDefault)
+ if errDefault != nil {
+ assert.False(t, strings.Contains(errDefault.Error(), "template"), "Error should be SMTP related, not template related")
+ }
+ // A full test would mock sendEmail and verify the subject/message passed (checking defaults).
+}
diff --git a/internal/rclone_service/rclone_service.go b/internal/rclone_service/rclone_service.go
index 0213b0d..e527a01 100644
--- a/internal/rclone_service/rclone_service.go
+++ b/internal/rclone_service/rclone_service.go
@@ -3,6 +3,7 @@ package rclone_service
import (
"bytes"
"context"
+ "errors"
"fmt"
"log"
"os"
@@ -14,27 +15,36 @@ import (
"github.com/starfleetcptn/gomft/internal/db"
)
+// --- Mockable os/exec ---
+
+// execCommandContext allows mocking exec.CommandContext during tests.
+var execCommandContext = exec.CommandContext
+
+// cmdCombinedOutput allows mocking the CombinedOutput method during tests.
+var cmdCombinedOutput = (*exec.Cmd).CombinedOutput
+
+// cmdRun allows mocking the Run method during tests.
+var cmdRun = (*exec.Cmd).Run
+
+// --- Function Implementation ---
+
// TestRcloneConnection attempts to connect to a provider using temporary config created via `rclone config create`.
// It returns success (bool), a message (string), and an error.
func TestRcloneConnection(config db.TransferConfig, providerType string, dbInstance *db.DB) (bool, string, error) {
var remoteName string
var remotePath string
var provider string
- // Removed bucket, share as they are not used in current rclone config create args
var host, user, pass, keyFile, region, accessKey, secretKey, endpoint, domain, clientID, clientSecret, driveID, teamDrive string
var port int
- // Add other necessary fields like passiveMode, readOnly etc. if needed by rclone config create for specific types
var err error
- // Create a temporary directory for the config file
tempDir, err := os.MkdirTemp("", "gomft-rclone-test-")
if err != nil {
return false, "Failed to create temp directory for rclone config", err
}
- defer os.RemoveAll(tempDir) // Clean up the temp directory
+ defer os.RemoveAll(tempDir)
tempConfigPath := filepath.Join(tempDir, "rclone_test.conf")
- // Extract parameters based on providerType
if providerType == "source" {
remoteName = "testSource"
remotePath = config.SourcePath
@@ -44,18 +54,15 @@ func TestRcloneConnection(config db.TransferConfig, providerType string, dbInsta
user = config.SourceUser
pass = config.SourcePassword
keyFile = config.SourceKeyFile
- // bucket = config.SourceBucket // Removed - Not used in create args yet
region = config.SourceRegion
accessKey = config.SourceAccessKey
secretKey = config.SourceSecretKey
endpoint = config.SourceEndpoint
- // share = config.SourceShare // Removed - Not used in create args yet
domain = config.SourceDomain
clientID = config.SourceClientID
clientSecret = config.SourceClientSecret
driveID = config.SourceDriveID
teamDrive = config.SourceTeamDrive
- // Extract other source fields as needed (e.g., passiveMode, readOnly)
} else if providerType == "destination" {
remoteName = "testDest"
remotePath = config.DestinationPath
@@ -65,46 +72,38 @@ func TestRcloneConnection(config db.TransferConfig, providerType string, dbInsta
user = config.DestUser
pass = config.DestPassword
keyFile = config.DestKeyFile
- // bucket = config.DestBucket // Removed - Not used in create args yet
region = config.DestRegion
accessKey = config.DestAccessKey
secretKey = config.DestSecretKey
endpoint = config.DestEndpoint
- // share = config.DestShare // Removed - Not used in create args yet
domain = config.DestDomain
clientID = config.DestClientID
clientSecret = config.DestClientSecret
driveID = config.DestDriveID
teamDrive = config.DestTeamDrive
- // Extract other dest fields as needed (e.g., passiveMode, readOnly)
} else {
return false, "Invalid provider type specified", fmt.Errorf("unknown provider type: %s", providerType)
}
- // Get the rclone path from the environment variable or use the default path
rclonePath := os.Getenv("RCLONE_PATH")
if rclonePath == "" {
rclonePath = "rclone"
}
- // --- Use `rclone config create` to generate the temporary config section ---
createArgs := []string{
"config", "create", remoteName, provider,
"--config", tempConfigPath,
"--non-interactive",
- "--log-level", "DEBUG", // Use DEBUG for create to see details if it fails
+ "--log-level", "DEBUG",
}
- // --- Declare variables for lsd command *before* the switch/goto ---
var ctx context.Context
var cancel context.CancelFunc
var lsdArgs []string
var stdout, stderr bytes.Buffer
var lsdCmd *exec.Cmd
- var createCmd *exec.Cmd // Declare createCmd here as well
+ var createCmd *exec.Cmd
- // Add provider-specific arguments
- // Mirroring logic from db.GenerateRcloneConfig but passing args to CLI
switch provider {
case "sftp":
createArgs = append(createArgs, "host", host, "user", user)
@@ -112,20 +111,18 @@ func TestRcloneConnection(config db.TransferConfig, providerType string, dbInsta
createArgs = append(createArgs, "port", fmt.Sprintf("%d", port))
}
if pass != "" {
- createArgs = append(createArgs, "pass", pass) // Pass directly
+ createArgs = append(createArgs, "pass", pass)
}
if keyFile != "" {
createArgs = append(createArgs, "key_file", keyFile)
- // Note: Passphrase for keyfile might need 'pass' too, rclone handles this context.
}
- // Add other SFTP args like ssh_agent, use_insecure_cipher etc. if needed
case "s3":
- createArgs = append(createArgs, "provider", "AWS", "env_auth", "false") // Assume AWS for generic S3
+ createArgs = append(createArgs, "provider", "AWS", "env_auth", "false")
if accessKey != "" {
createArgs = append(createArgs, "access_key_id", accessKey)
}
if secretKey != "" {
- createArgs = append(createArgs, "secret_access_key", secretKey) // Pass directly
+ createArgs = append(createArgs, "secret_access_key", secretKey)
}
if region != "" {
createArgs = append(createArgs, "region", region)
@@ -133,19 +130,18 @@ func TestRcloneConnection(config db.TransferConfig, providerType string, dbInsta
if endpoint != "" {
createArgs = append(createArgs, "endpoint", endpoint)
}
- // Add other S3 args like acl, storage_class if needed
case "minio":
createArgs = append(createArgs, "provider", "Minio", "env_auth", "false")
if accessKey != "" {
createArgs = append(createArgs, "access_key_id", accessKey)
}
if secretKey != "" {
- createArgs = append(createArgs, "secret_access_key", secretKey) // Pass directly
+ createArgs = append(createArgs, "secret_access_key", secretKey)
}
if endpoint != "" {
createArgs = append(createArgs, "endpoint", endpoint)
}
- if region != "" { // Minio might ignore region, but add if present
+ if region != "" {
createArgs = append(createArgs, "region", region)
}
case "ftp":
@@ -156,13 +152,12 @@ func TestRcloneConnection(config db.TransferConfig, providerType string, dbInsta
if pass != "" {
createArgs = append(createArgs, "pass", pass)
}
- // Add explicit_tls, passive_mode based on config boolean pointers
- if config.GetSourcePassiveMode() || config.GetDestPassiveMode() { // Check based on providerType
+ if config.GetSourcePassiveMode() || config.GetDestPassiveMode() {
createArgs = append(createArgs, "passive_mode", "true")
} else {
createArgs = append(createArgs, "passive_mode", "false")
}
- createArgs = append(createArgs, "explicit_tls", "true") // Defaulting to true
+ createArgs = append(createArgs, "explicit_tls", "true")
case "smb":
createArgs = append(createArgs, "host", host, "user", user)
if port != 0 {
@@ -175,7 +170,7 @@ func TestRcloneConnection(config db.TransferConfig, providerType string, dbInsta
createArgs = append(createArgs, "domain", domain)
}
case "webdav":
- createArgs = append(createArgs, "url", endpoint, "vendor", "other", "user", user) // Default vendor
+ createArgs = append(createArgs, "url", endpoint, "vendor", "other", "user", user)
if pass != "" {
createArgs = append(createArgs, "pass", pass)
}
@@ -184,84 +179,70 @@ func TestRcloneConnection(config db.TransferConfig, providerType string, dbInsta
if pass != "" {
createArgs = append(createArgs, "pass", pass)
}
- // Add cases for gdrive, gphotos - these are complex due to token handling
- // For testing, they might require pre-existing tokens or manual auth flow outside this scope.
- // Passing client_id/secret might work for initial setup but not subsequent tests without a token.
case "gdrive":
- createArgs = append(createArgs, "scope", "drive") // Default scope
+ createArgs = append(createArgs, "scope", "drive")
if clientID != "" {
createArgs = append(createArgs, "client_id", clientID)
}
if clientSecret != "" {
createArgs = append(createArgs, "client_secret", clientSecret)
}
- if driveID != "" { // Use root_folder_id for specific drive/folder
+ if driveID != "" {
createArgs = append(createArgs, "root_folder_id", driveID)
}
if teamDrive != "" {
createArgs = append(createArgs, "team_drive", teamDrive)
}
- // Cannot pass token directly via 'config create' easily for testing non-interactive flow.
log.Println("Warning: Google Drive test may require pre-existing token or manual auth.")
-
case "gphotos":
- // Similar complexity to gdrive regarding tokens
if clientID != "" {
createArgs = append(createArgs, "client_id", clientID)
}
if clientSecret != "" {
createArgs = append(createArgs, "client_secret", clientSecret)
}
- // Add read_only, start_year, include_archived based on config
- // Cannot pass token directly via 'config create' easily for testing non-interactive flow.
log.Println("Warning: Google Photos test may require pre-existing token or manual auth.")
-
case "local":
- // 'rclone config create' might not be needed or work well for 'local' type.
- // Write a minimal config manually for local.
localConfigContent := fmt.Sprintf("[%s]\ntype = local\nnounc = true\n", remoteName)
if err := os.WriteFile(tempConfigPath, []byte(localConfigContent), 0600); err != nil {
return false, fmt.Sprintf("Failed to write temporary local config: %v", err), err
}
- goto RunLsd // Skip rclone config create for local
+ goto RunLsd
default:
return false, fmt.Sprintf("Provider type '%s' not yet supported for testing via 'rclone config create'", provider), fmt.Errorf("unsupported provider")
}
log.Printf("Executing rclone config create command: %s %s", rclonePath, strings.Join(createArgs, " "))
- createCmd = exec.Command(rclonePath, createArgs...) // Assign value using =
- if output, err := createCmd.CombinedOutput(); err != nil {
- // Log the config file content on error for debugging
+ createCmd = execCommandContext(context.Background(), rclonePath, createArgs...)
+ // Use the mockable function variable
+ if output, err := cmdCombinedOutput(createCmd); err != nil {
configContentBytes, _ := os.ReadFile(tempConfigPath)
log.Printf("Temp config content on create error:\n---\n%s\n---", string(configContentBytes))
return false, fmt.Sprintf("Failed to create temp config section: %v\nOutput: %s", err, string(output)), err
}
log.Printf("Successfully created temp config section for %s", remoteName)
-RunLsd: // Label to jump to for local type
+RunLsd:
- // --- Execute `rclone lsd` using the temporary config ---
- ctx, cancel = context.WithTimeout(context.Background(), 30*time.Second) // Assign values
+ ctx, cancel = context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
- lsdArgs = []string{ // Assign value
+ lsdArgs = []string{
"--config", tempConfigPath,
"lsd",
fmt.Sprintf("%s:%s", remoteName, remotePath),
"--low-level-retries", "1",
"--retries", "1",
- // Add -vv for verbose logging during test if needed
- // "-vv",
}
log.Printf("Executing rclone lsd command: %s %s", rclonePath, strings.Join(lsdArgs, " "))
- lsdCmd = exec.CommandContext(ctx, rclonePath, lsdArgs...) // Assign value
+ lsdCmd = execCommandContext(ctx, rclonePath, lsdArgs...)
- // var stdout, stderr bytes.Buffer // Moved declaration up
- lsdCmd.Stdout = &stdout // Assign buffer
- lsdCmd.Stderr = &stderr // Assign buffer
+ lsdCmd.Stdout = &stdout
+ lsdCmd.Stderr = &stderr
- err = lsdCmd.Run() // Assign error
+ // Use the mockable function variable
+ err = cmdRun(lsdCmd)
stdoutStr := stdout.String()
stderrStr := stderr.String()
@@ -269,14 +250,16 @@ RunLsd: // Label to jump to for local type
log.Printf("Rclone lsd stdout:\n%s", stdoutStr)
log.Printf("Rclone lsd stderr:\n%s", stderrStr)
- if ctx.Err() == context.DeadlineExceeded {
- return false, "Connection test timed out after 30 seconds.", ctx.Err()
- }
-
if err != nil {
- // Try to provide a more specific error message based on lsd output
+ if errors.Is(err, context.DeadlineExceeded) {
+ return false, "Connection test timed out after 30 seconds.", context.DeadlineExceeded
+ }
+ // Check ctx.Err() as a fallback - This check might be redundant now
+ if ctx.Err() == context.DeadlineExceeded {
+ return false, "Connection test timed out after 30 seconds.", ctx.Err()
+ }
+
errMsg := fmt.Sprintf("Connection test failed: %v. Stderr: %s", err, stderrStr)
- // Add specific error checks based on stderrStr if needed
if strings.Contains(stderrStr, "connect: connection refused") {
errMsg = "Connection test failed: Connection refused by host."
} else if strings.Contains(stderrStr, "no such host") || strings.Contains(stderrStr, "name resolution error") {
@@ -285,12 +268,11 @@ RunLsd: // Label to jump to for local type
errMsg = "Connection test failed: Authentication failed (check credentials/permissions)."
} else if strings.Contains(stderrStr, "directory not found") {
errMsg = "Connection test failed: Directory/Path not found (check path)."
- } else if strings.Contains(stderrStr, "Couldn't find section") { // Error from rclone config create
+ } else if strings.Contains(stderrStr, "Couldn't find section") {
errMsg = "Connection test failed: Invalid parameters provided for provider type."
}
return false, errMsg, err
}
- // If lsd runs without error, the connection is likely okay
return true, "Connection test successful!", nil
}
diff --git a/internal/rclone_service/rclone_service_test.go b/internal/rclone_service/rclone_service_test.go
new file mode 100644
index 0000000..9321c32
--- /dev/null
+++ b/internal/rclone_service/rclone_service_test.go
@@ -0,0 +1,301 @@
+package rclone_service
+
+import (
+ "context"
+ "errors"
+ "os/exec"
+ "strings"
+ "testing"
+
+ "github.com/starfleetcptn/gomft/internal/db"
+)
+
+// --- Mock os/exec ---
+
+// Note: The package-level variable 'execCommandContext' is defined in rclone_service.go
+// This helper function replaces it for the duration of a test.
+
+// MockExecCommand replaces the package-level execCommandContext variable (defined in rclone_service.go)
+// with a function provided by the test and returns a function to restore the original.
+func MockExecCommand(mockFunc func(ctx context.Context, command string, args ...string) *exec.Cmd) (restore func()) {
+ original := execCommandContext
+ execCommandContext = mockFunc
+ return func() { execCommandContext = original }
+}
+
+// Helper function to find the actual rclone command within args, skipping flags.
+func findRcloneCommand(args []string) string {
+ for i := 0; i < len(args); i++ {
+ arg := args[i]
+ if strings.HasPrefix(arg, "-") {
+ if i+1 < len(args) && !strings.HasPrefix(args[i+1], "-") {
+ i++
+ }
+ continue
+ }
+ return arg
+ }
+ return ""
+}
+
+// --- Tests ---
+
+func TestTestRcloneConnection_Success_SFTP(t *testing.T) {
+ config := db.TransferConfig{
+ SourceType: "sftp",
+ SourceHost: "testhost",
+ SourceUser: "testuser",
+ SourcePassword: "testpassword",
+ }
+ providerType := "source"
+ var dbInstance *db.DB
+ configCreateCalled := false
+
+ // Mock execCommandContext (only needed to return a basic cmd struct)
+ restoreExec := MockExecCommand(func(ctx context.Context, command string, args ...string) *exec.Cmd {
+ // Return a simple, non-nil command object. The actual execution is mocked below.
+ return exec.Command("echo", "mocked")
+ })
+ defer restoreExec()
+
+ // Mock cmdCombinedOutput for config create
+ originalCombinedOutput := cmdCombinedOutput
+ cmdCombinedOutput = func(c *exec.Cmd) ([]byte, error) {
+ configCreateCalled = true
+ return []byte(""), nil // Simulate success
+ }
+ defer func() { cmdCombinedOutput = originalCombinedOutput }() // Restore original
+
+ // Mock cmdRun for lsd
+ originalRun := cmdRun
+ cmdRun = func(c *exec.Cmd) error {
+ if !configCreateCalled {
+ t.Fatalf("lsd (Run) called before config create")
+ }
+ // Simulate success by returning nil error
+ // We also need to simulate writing to stdout if the main func uses it
+ if stdoutWriter, ok := c.Stdout.(interface{ WriteString(string) (int, error) }); ok {
+ stdoutWriter.WriteString(" -1 2023-01-01 10:00:00 -1 some_dir\n")
+ }
+ return nil
+ }
+ defer func() { cmdRun = originalRun }() // Restore original
+
+ success, msg, err := TestRcloneConnection(config, providerType, dbInstance)
+
+ if err != nil {
+ t.Errorf("Expected no error, but got: %v", err)
+ }
+ if !success {
+ t.Errorf("Expected success=true, but got false. Message: %s", msg)
+ }
+ if msg != "Connection test successful!" {
+ t.Errorf("Expected success message, but got: %q", msg)
+ }
+}
+
+func TestTestRcloneConnection_ConfigCreateFail(t *testing.T) {
+ config := db.TransferConfig{
+ SourceType: "sftp",
+ SourceHost: "testhost",
+ SourceUser: "testuser",
+ }
+ providerType := "source"
+ var dbInstance *db.DB
+ expectedStderr := "invalid parameters"
+ expectedErr := errors.New("exit status 1")
+
+ // Mock execCommandContext
+ restoreExec := MockExecCommand(func(ctx context.Context, command string, args ...string) *exec.Cmd {
+ return exec.Command("echo", "mocked")
+ })
+ defer restoreExec()
+
+ // Mock cmdCombinedOutput for config create failure
+ originalCombinedOutput := cmdCombinedOutput
+ cmdCombinedOutput = func(c *exec.Cmd) ([]byte, error) {
+ return []byte(expectedStderr), expectedErr // Simulate failure
+ }
+ defer func() { cmdCombinedOutput = originalCombinedOutput }()
+
+ // Mock cmdRun (should not be called)
+ originalRun := cmdRun
+ cmdRun = func(c *exec.Cmd) error {
+ t.Fatalf("lsd (Run) called after config create failure")
+ return errors.New("should not be called")
+ }
+ defer func() { cmdRun = originalRun }()
+
+ success, msg, err := TestRcloneConnection(config, providerType, dbInstance)
+
+ if err == nil {
+ t.Error("Expected an error from config create failure, but got nil")
+ } else if !errors.Is(err, expectedErr) {
+ t.Errorf("Expected error %v, got %v", expectedErr, err)
+ }
+ if success {
+ t.Error("Expected success=false for config create failure, but got true")
+ }
+ if !strings.Contains(msg, "Failed to create temp config section") {
+ t.Errorf("Expected message containing 'Failed to create temp config section', got: %q", msg)
+ }
+ // Note: The CombinedOutput mock returns stderr in the output byte slice
+ if !strings.Contains(msg, expectedStderr) {
+ t.Errorf("Expected message containing stderr %q, got: %q", expectedStderr, msg)
+ }
+}
+
+func TestTestRcloneConnection_LsdTimeout(t *testing.T) {
+ config := db.TransferConfig{
+ SourceType: "sftp",
+ SourceHost: "testhost",
+ SourceUser: "testuser",
+ SourcePassword: "pw",
+ }
+ providerType := "source"
+ var dbInstance *db.DB
+
+ // Mock execCommandContext
+ restoreExec := MockExecCommand(func(ctx context.Context, command string, args ...string) *exec.Cmd {
+ return exec.Command("echo", "mocked")
+ })
+ defer restoreExec()
+
+ // Mock cmdCombinedOutput for config create success
+ originalCombinedOutput := cmdCombinedOutput
+ cmdCombinedOutput = func(c *exec.Cmd) ([]byte, error) {
+ return []byte(""), nil
+ }
+ defer func() { cmdCombinedOutput = originalCombinedOutput }()
+
+ // Mock cmdRun for lsd timeout
+ originalRun := cmdRun
+ cmdRun = func(c *exec.Cmd) error {
+ // Simulate timeout error
+ return context.DeadlineExceeded
+ }
+ defer func() { cmdRun = originalRun }()
+
+ success, msg, err := TestRcloneConnection(config, providerType, dbInstance)
+
+ if err == nil {
+ t.Error("Expected a timeout error, but got nil")
+ } else if !errors.Is(err, context.DeadlineExceeded) {
+ t.Errorf("Expected context.DeadlineExceeded error, got: %v (type: %T)", err, err)
+ }
+ if success {
+ t.Error("Expected success=false for timeout, but got true")
+ }
+ if !strings.Contains(msg, "Connection test timed out") {
+ t.Errorf("Expected message containing 'Connection test timed out', got: %q", msg)
+ }
+}
+
+func TestTestRcloneConnection_LsdAuthFail(t *testing.T) {
+ config := db.TransferConfig{
+ SourceType: "sftp",
+ SourceHost: "testhost",
+ SourceUser: "wronguser",
+ SourcePassword: "wrongpassword",
+ }
+ providerType := "source"
+ var dbInstance *db.DB
+ expectedStderr := "authentication failed"
+ expectedErr := errors.New("exit status 1")
+
+ // Mock execCommandContext
+ restoreExec := MockExecCommand(func(ctx context.Context, command string, args ...string) *exec.Cmd {
+ return exec.Command("echo", "mocked")
+ })
+ defer restoreExec()
+
+ // Mock cmdCombinedOutput for config create success
+ originalCombinedOutput := cmdCombinedOutput
+ cmdCombinedOutput = func(c *exec.Cmd) ([]byte, error) {
+ return []byte(""), nil
+ }
+ defer func() { cmdCombinedOutput = originalCombinedOutput }()
+
+ // Mock cmdRun for lsd failure
+ originalRun := cmdRun
+ cmdRun = func(c *exec.Cmd) error {
+ // Simulate failure by returning error and writing to stderr buffer
+ if stderrWriter, ok := c.Stderr.(interface{ WriteString(string) (int, error) }); ok {
+ stderrWriter.WriteString(expectedStderr)
+ }
+ return expectedErr
+ }
+ defer func() { cmdRun = originalRun }()
+
+ success, msg, err := TestRcloneConnection(config, providerType, dbInstance)
+
+ if err == nil {
+ t.Error("Expected an error from lsd auth failure, but got nil")
+ } else if !errors.Is(err, expectedErr) {
+ if !strings.Contains(err.Error(), "exit status 1") {
+ t.Errorf("Expected error containing 'exit status 1', got: %v", err)
+ }
+ }
+ if success {
+ t.Error("Expected success=false for lsd auth failure, but got true")
+ }
+ // Check the parsed error message based on stderr
+ if !strings.Contains(msg, "Authentication failed") {
+ t.Errorf("Expected message containing 'Authentication failed', got: %q", msg)
+ }
+}
+
+func TestTestRcloneConnection_LocalSuccess(t *testing.T) {
+ tempPath := t.TempDir()
+ config := db.TransferConfig{
+ SourceType: "local",
+ SourcePath: tempPath,
+ }
+ providerType := "source"
+ var dbInstance *db.DB
+
+ // Mock execCommandContext (only lsd should be called)
+ restoreExec := MockExecCommand(func(ctx context.Context, command string, args ...string) *exec.Cmd {
+ rcloneCmd := findRcloneCommand(args)
+ if rcloneCmd != "lsd" {
+ t.Fatalf("Unexpected command call for local provider: %q", rcloneCmd)
+ }
+ return exec.Command("echo", "mocked for lsd")
+ })
+ defer restoreExec()
+
+ // Mock cmdCombinedOutput (should not be called)
+ originalCombinedOutput := cmdCombinedOutput
+ cmdCombinedOutput = func(c *exec.Cmd) ([]byte, error) {
+ t.Fatalf("CombinedOutput called unexpectedly for local provider")
+ return nil, errors.New("should not be called")
+ }
+ defer func() { cmdCombinedOutput = originalCombinedOutput }()
+
+ // Mock cmdRun for lsd success
+ originalRun := cmdRun
+ cmdRun = func(c *exec.Cmd) error {
+ // Simulate success
+ if stdoutWriter, ok := c.Stdout.(interface{ WriteString(string) (int, error) }); ok {
+ stdoutWriter.WriteString(" -1 2023-01-01 10:00:00 -1 some_local_dir\n")
+ }
+ return nil
+ }
+ defer func() { cmdRun = originalRun }()
+
+ success, msg, err := TestRcloneConnection(config, providerType, dbInstance)
+
+ if err != nil {
+ t.Errorf("Expected no error for local success, but got: %v", err)
+ }
+ if !success {
+ t.Errorf("Expected success=true for local success, but got false. Message: %s", msg)
+ }
+ if msg != "Connection test successful!" {
+ t.Errorf("Expected success message, but got: %q", msg)
+ }
+}
+
+// TODO: Add more tests for other providers (S3, FTP, WebDAV, etc.)
+// TODO: Add tests for destination providerType
+// TODO: Add tests for specific error string parsing (connection refused, dir not found)
diff --git a/internal/scheduler/notification.go b/internal/scheduler/notification.go
index b5d886a..a1ceeed 100644
--- a/internal/scheduler/notification.go
+++ b/internal/scheduler/notification.go
@@ -4,6 +4,7 @@ import (
"bytes"
"crypto/hmac"
"crypto/sha256"
+ "crypto/tls"
"encoding/hex"
"encoding/json"
"fmt"
@@ -30,15 +31,17 @@ type NotificationDB interface {
// Notifier handles sending notifications via various services.
type Notifier struct {
- db NotificationDB // Use the interface type
- logger *Logger
+ db NotificationDB // Use the interface type
+ logger *Logger
+ skipSSLVerify bool
}
// NewNotifier creates a new Notifier.
-func NewNotifier(database NotificationDB, logger *Logger) *Notifier { // Accept the interface type
+func NewNotifier(database NotificationDB, logger *Logger, skipSSLVerify bool) *Notifier { // Accept the interface type and skip flag
return &Notifier{
- db: database,
- logger: logger,
+ db: database,
+ logger: logger,
+ skipSSLVerify: skipSSLVerify, // Store the flag
}
}
@@ -138,10 +141,8 @@ func (n *Notifier) sendJobWebhookNotification(job *db.Job, history *db.JobHistor
n.logger.LogDebug("Webhook headers: %+v", req.Header)
- // Send the request with a timeout
- client := &http.Client{
- Timeout: 10 * time.Second,
- }
+ // Send the request with a timeout and configured TLS settings
+ client := createHTTPClient(10*time.Second, n.skipSSLVerify)
resp, err := client.Do(req)
if err != nil {
n.logger.LogError("Error sending webhook for job %d: %v", job.ID, err)
@@ -328,6 +329,26 @@ func generateEmailBody(job *db.Job, history *db.JobHistory, config *db.TransferC
return b.String()
}
+// createHTTPClient creates an HTTP client with appropriate TLS settings and timeout.
+func createHTTPClient(timeout time.Duration, skipSSLVerify bool) *http.Client {
+ // Clone the default transport to avoid modifying global state
+ transport := http.DefaultTransport.(*http.Transport).Clone()
+
+ if skipSSLVerify {
+ // Ensure TLSClientConfig exists before modifying it
+ if transport.TLSClientConfig == nil {
+ transport.TLSClientConfig = &tls.Config{}
+ }
+ transport.TLSClientConfig.InsecureSkipVerify = true
+ // TODO: Consider adding a log warning here when skipping verification
+ }
+
+ return &http.Client{
+ Timeout: timeout,
+ Transport: transport,
+ }
+}
+
// sendServiceWebhookNotification sends a webhook notification using a configured notification service.
func (n *Notifier) sendServiceWebhookNotification(service *db.NotificationService, job *db.Job, history *db.JobHistory, config *db.TransferConfig, eventType string) error {
n.logger.LogDebug("Preparing webhook notification via service %s for job %d", service.Name, job.ID)
@@ -413,10 +434,8 @@ func (n *Notifier) sendServiceWebhookNotification(service *db.NotificationServic
timeout = 15 * time.Second
}
- // Prepare client with timeout
- client := &http.Client{
- Timeout: timeout,
- }
+ // Prepare client with timeout and configured TLS settings
+ client := createHTTPClient(timeout, n.skipSSLVerify)
// Attempt to send with retries
var resp *http.Response
@@ -834,7 +853,7 @@ func (n *Notifier) sendPushbulletNotification(service *db.NotificationService, j
req.Header.Set("Access-Token", apiKey)
// Send the request
- client := &http.Client{Timeout: 10 * time.Second}
+ client := createHTTPClient(10*time.Second, n.skipSSLVerify)
resp, err := client.Do(req)
if err != nil {
return fmt.Errorf("failed to send Pushbullet notification: %v", err)
@@ -930,7 +949,7 @@ func (n *Notifier) sendNtfyNotification(service *db.NotificationService, job *db
}
// Send the request
- client := &http.Client{Timeout: 10 * time.Second}
+ client := createHTTPClient(10*time.Second, n.skipSSLVerify)
resp, err := client.Do(req)
if err != nil {
return fmt.Errorf("failed to send ntfy notification: %v", err)
@@ -1016,7 +1035,7 @@ func (n *Notifier) sendGotifyNotification(service *db.NotificationService, job *
req.Header.Set("X-Gotify-Key", token)
// Send the request
- client := &http.Client{Timeout: 10 * time.Second}
+ client := createHTTPClient(10*time.Second, n.skipSSLVerify)
resp, err := client.Do(req)
if err != nil {
return fmt.Errorf("failed to send Gotify notification: %v", err)
@@ -1111,7 +1130,7 @@ func (n *Notifier) sendPushoverNotification(service *db.NotificationService, job
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
// Send the request
- client := &http.Client{Timeout: 10 * time.Second}
+ client := createHTTPClient(10*time.Second, n.skipSSLVerify)
resp, err := client.Do(req)
if err != nil {
return fmt.Errorf("failed to send Pushover notification: %v", err)
diff --git a/internal/web/handlers/auth_handlers.go b/internal/web/handlers/auth_handlers.go
index d9c9741..27624bb 100644
--- a/internal/web/handlers/auth_handlers.go
+++ b/internal/web/handlers/auth_handlers.go
@@ -698,7 +698,7 @@ func (h *Handlers) HandleAuthProviderInit(c *gin.Context) {
// Get the auth provider
provider, err := h.DB.GetAuthProviderByID(c.Request.Context(), uint(providerID))
- if err != nil || !provider.Enabled {
+ if err != nil || !provider.GetEnabled() { // Use getter
h.HandleBadRequest(c, "Provider Not Available", "The authentication provider is not available")
return
}
@@ -862,7 +862,7 @@ func (h *Handlers) HandleAuthProviderCallback(c *gin.Context) {
// Get the auth provider
provider, err := h.DB.GetAuthProviderByID(c.Request.Context(), uint(providerID))
- if err != nil || !provider.Enabled {
+ if err != nil || !provider.GetEnabled() { // Use getter
h.HandleBadRequest(c, "Provider Not Available", "The authentication provider is not available")
return
}
diff --git a/internal/web/handlers/auth_provider_handlers.go b/internal/web/handlers/auth_provider_handlers.go
index c484209..f93ff67 100644
--- a/internal/web/handlers/auth_provider_handlers.go
+++ b/internal/web/handlers/auth_provider_handlers.go
@@ -99,8 +99,9 @@ func (h *Handlers) HandleCreateAuthProvider(c *gin.Context) {
Scopes: c.PostForm("scopes"),
Description: c.PostForm("description"),
IconURL: c.PostForm("icon_url"),
- Enabled: c.PostForm("enabled") == "on",
+ // Enabled will be set using the helper method below
}
+ provider.SetEnabled(c.PostForm("enabled") == "on") // Use helper method
// Process config values based on provider type
config := make(map[string]interface{})
@@ -211,7 +212,7 @@ func (h *Handlers) HandleUpdateAuthProvider(c *gin.Context) {
existingProvider.RedirectURL = c.PostForm("redirect_url")
existingProvider.Scopes = c.PostForm("scopes")
existingProvider.Description = c.PostForm("description")
- existingProvider.Enabled = c.PostForm("enabled") == "on"
+ existingProvider.SetEnabled(c.PostForm("enabled") == "on") // Use setter
// Update the type if changed
if providerType := c.PostForm("type"); providerType != "" {
diff --git a/internal/web/handlers/settings_handlers.go b/internal/web/handlers/settings_handlers.go
index 06722ef..679a82b 100644
--- a/internal/web/handlers/settings_handlers.go
+++ b/internal/web/handlers/settings_handlers.go
@@ -43,7 +43,7 @@ func (h *Handlers) HandleSettings(c *gin.Context) {
ID: service.ID,
Name: service.Name,
Type: service.Type,
- IsEnabled: service.IsEnabled,
+ IsEnabled: service.GetIsEnabled(), // Use getter
Config: service.Config,
Description: service.Description,
EventTriggers: service.EventTriggers,
@@ -76,6 +76,8 @@ func (h *Handlers) HandleCreateNotificationService(c *gin.Context) {
serviceType := c.PostForm("type")
description := c.PostForm("description")
isEnabled := c.PostForm("is_enabled") == "on"
+ // Read event triggers directly from the form array
+ eventTriggers := c.PostFormArray("event_triggers[]")
// Validate required fields
if name == "" || serviceType == "" {
@@ -86,7 +88,7 @@ func (h *Handlers) HandleCreateNotificationService(c *gin.Context) {
// Create config map based on service type
config := make(map[string]string)
- eventTriggers := make([]string, 0)
+ // eventTriggers slice is now populated above
fmt.Println("serviceType", serviceType)
switch serviceType {
case "email":
@@ -102,16 +104,7 @@ func (h *Handlers) HandleCreateNotificationService(c *gin.Context) {
config["title_template"] = c.PostForm("pushbullet_title_template")
config["body_template"] = c.PostForm("pushbullet_body_template")
- // Build event triggers
- if c.PostForm("trigger_job_start") == "on" {
- eventTriggers = append(eventTriggers, "job_start")
- }
- if c.PostForm("trigger_job_complete") == "on" {
- eventTriggers = append(eventTriggers, "job_complete")
- }
- if c.PostForm("trigger_job_error") == "on" {
- eventTriggers = append(eventTriggers, "job_error")
- }
+ // Event triggers are handled above
// Validate required fields
if config["api_key"] == "" {
@@ -121,14 +114,15 @@ func (h *Handlers) HandleCreateNotificationService(c *gin.Context) {
// Create new notification service
service := db.NotificationService{
- Name: name,
- Type: serviceType,
- IsEnabled: isEnabled,
+ Name: name,
+ Type: serviceType,
+ // IsEnabled will be set using the helper method below
Config: config,
Description: description,
EventTriggers: eventTriggers,
CreatedBy: c.GetUint("userID"),
}
+ service.SetIsEnabled(isEnabled) // Use helper method
// Save to database
if err := h.DB.Create(&service).Error; err != nil {
@@ -141,7 +135,7 @@ func (h *Handlers) HandleCreateNotificationService(c *gin.Context) {
auditDetails := map[string]interface{}{
"name": service.Name,
"type": service.Type,
- "is_enabled": service.IsEnabled,
+ "is_enabled": service.GetIsEnabled(), // Use getter
"description": service.Description,
"event_triggers": eventTriggers,
}
@@ -171,16 +165,7 @@ func (h *Handlers) HandleCreateNotificationService(c *gin.Context) {
config["title_template"] = c.PostForm("ntfy_title_template")
config["message_template"] = c.PostForm("ntfy_message_template")
- // Build event triggers
- if c.PostForm("trigger_job_start") == "on" {
- eventTriggers = append(eventTriggers, "job_start")
- }
- if c.PostForm("trigger_job_complete") == "on" {
- eventTriggers = append(eventTriggers, "job_complete")
- }
- if c.PostForm("trigger_job_error") == "on" {
- eventTriggers = append(eventTriggers, "job_error")
- }
+ // Event triggers are handled above
// Validate required fields
if config["server"] == "" || config["topic"] == "" {
@@ -190,14 +175,15 @@ func (h *Handlers) HandleCreateNotificationService(c *gin.Context) {
// Create new notification service
service := db.NotificationService{
- Name: name,
- Type: serviceType,
- IsEnabled: isEnabled,
+ Name: name,
+ Type: serviceType,
+ // IsEnabled will be set using the helper method below
Config: config,
Description: description,
EventTriggers: eventTriggers,
CreatedBy: c.GetUint("userID"),
}
+ service.SetIsEnabled(isEnabled) // Use helper method
// Save to database
if err := h.DB.Create(&service).Error; err != nil {
@@ -210,7 +196,7 @@ func (h *Handlers) HandleCreateNotificationService(c *gin.Context) {
auditDetails := map[string]interface{}{
"name": service.Name,
"type": service.Type,
- "is_enabled": service.IsEnabled,
+ "is_enabled": service.GetIsEnabled(), // Use getter
"description": service.Description,
"event_triggers": eventTriggers,
}
@@ -238,16 +224,7 @@ func (h *Handlers) HandleCreateNotificationService(c *gin.Context) {
config["title_template"] = c.PostForm("gotify_title_template")
config["message_template"] = c.PostForm("gotify_message_template")
- // Build event triggers
- if c.PostForm("trigger_job_start") == "on" {
- eventTriggers = append(eventTriggers, "job_start")
- }
- if c.PostForm("trigger_job_complete") == "on" {
- eventTriggers = append(eventTriggers, "job_complete")
- }
- if c.PostForm("trigger_job_error") == "on" {
- eventTriggers = append(eventTriggers, "job_error")
- }
+ // Event triggers are handled above
// Validate required fields
if config["url"] == "" || config["token"] == "" {
@@ -257,14 +234,15 @@ func (h *Handlers) HandleCreateNotificationService(c *gin.Context) {
// Create new notification service
service := db.NotificationService{
- Name: name,
- Type: serviceType,
- IsEnabled: isEnabled,
+ Name: name,
+ Type: serviceType,
+ // IsEnabled will be set using the helper method below
Config: config,
Description: description,
EventTriggers: eventTriggers,
CreatedBy: c.GetUint("userID"),
}
+ service.SetIsEnabled(isEnabled) // Use helper method
// Save to database
if err := h.DB.Create(&service).Error; err != nil {
@@ -277,7 +255,7 @@ func (h *Handlers) HandleCreateNotificationService(c *gin.Context) {
auditDetails := map[string]interface{}{
"name": service.Name,
"type": service.Type,
- "is_enabled": service.IsEnabled,
+ "is_enabled": service.GetIsEnabled(), // Use getter
"description": service.Description,
"event_triggers": eventTriggers,
}
@@ -307,16 +285,7 @@ func (h *Handlers) HandleCreateNotificationService(c *gin.Context) {
config["title_template"] = c.PostForm("pushover_title_template")
config["message_template"] = c.PostForm("pushover_message_template")
- // Build event triggers
- if c.PostForm("trigger_job_start") == "on" {
- eventTriggers = append(eventTriggers, "job_start")
- }
- if c.PostForm("trigger_job_complete") == "on" {
- eventTriggers = append(eventTriggers, "job_complete")
- }
- if c.PostForm("trigger_job_error") == "on" {
- eventTriggers = append(eventTriggers, "job_error")
- }
+ // Event triggers are handled above
// Validate required fields
if config["app_token"] == "" || config["user_key"] == "" {
@@ -326,14 +295,15 @@ func (h *Handlers) HandleCreateNotificationService(c *gin.Context) {
// Create new notification service
service := db.NotificationService{
- Name: name,
- Type: serviceType,
- IsEnabled: isEnabled,
+ Name: name,
+ Type: serviceType,
+ // IsEnabled will be set using the helper method below
Config: config,
Description: description,
EventTriggers: eventTriggers,
CreatedBy: c.GetUint("userID"),
}
+ service.SetIsEnabled(isEnabled) // Use helper method
// Save to database
if err := h.DB.Create(&service).Error; err != nil {
@@ -346,7 +316,7 @@ func (h *Handlers) HandleCreateNotificationService(c *gin.Context) {
auditDetails := map[string]interface{}{
"name": service.Name,
"type": service.Type,
- "is_enabled": service.IsEnabled,
+ "is_enabled": service.GetIsEnabled(), // Use getter
"description": service.Description,
"event_triggers": eventTriggers,
}
@@ -373,26 +343,13 @@ func (h *Handlers) HandleCreateNotificationService(c *gin.Context) {
config["headers"] = c.PostForm("headers")
// Add the new webhook fields
- // 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"))
- if c.PostForm("trigger_job_start") == "on" {
- eventTriggers = append(eventTriggers, "job_start")
- }
- if c.PostForm("trigger_job_complete") == "on" {
- eventTriggers = append(eventTriggers, "job_complete")
- }
- if c.PostForm("trigger_job_error") == "on" {
- eventTriggers = append(eventTriggers, "job_error")
- }
+ // Event triggers are handled above
// Create new notification service with additional fields
service := db.NotificationService{
- Name: name,
- Type: serviceType,
- IsEnabled: isEnabled,
+ Name: name,
+ Type: serviceType,
+ // IsEnabled will be set using the helper method below
Config: config,
Description: description,
EventTriggers: eventTriggers,
@@ -401,6 +358,7 @@ func (h *Handlers) HandleCreateNotificationService(c *gin.Context) {
RetryPolicy: c.PostForm("retry_policy"),
CreatedBy: c.GetUint("userID"),
}
+ service.SetIsEnabled(isEnabled) // Use helper method
// Save to database
if err := h.DB.Create(&service).Error; err != nil {
@@ -413,7 +371,7 @@ func (h *Handlers) HandleCreateNotificationService(c *gin.Context) {
auditDetails := map[string]interface{}{
"name": service.Name,
"type": service.Type,
- "is_enabled": service.IsEnabled,
+ "is_enabled": service.GetIsEnabled(), // Use getter
"description": service.Description,
"event_triggers": eventTriggers,
"retry_policy": service.RetryPolicy,
@@ -442,13 +400,14 @@ func (h *Handlers) HandleCreateNotificationService(c *gin.Context) {
// Create new notification service
service := db.NotificationService{
- Name: name,
- Type: serviceType,
- IsEnabled: isEnabled,
+ Name: name,
+ Type: serviceType,
+ // IsEnabled will be set using the helper method below
Config: config,
Description: description,
CreatedBy: c.GetUint("userID"),
}
+ service.SetIsEnabled(isEnabled) // Use helper method
// Save to database
if err := h.DB.Create(&service).Error; err != nil {
@@ -461,7 +420,7 @@ func (h *Handlers) HandleCreateNotificationService(c *gin.Context) {
auditDetails := map[string]interface{}{
"name": service.Name,
"type": service.Type,
- "is_enabled": service.IsEnabled,
+ "is_enabled": service.GetIsEnabled(), // Use getter
"description": service.Description,
}
@@ -1142,7 +1101,7 @@ func (h *Handlers) HandleNotificationsPage(c *gin.Context) {
ID: service.ID,
Name: service.Name,
Type: service.Type,
- IsEnabled: service.IsEnabled,
+ IsEnabled: service.GetIsEnabled(), // Use getter
Config: service.Config,
Description: service.Description,
EventTriggers: service.EventTriggers,
@@ -1281,7 +1240,7 @@ func (h *Handlers) HandleEditNotificationPage(c *gin.Context) {
Name: service.Name,
Description: service.Description,
Type: service.Type,
- IsEnabled: service.IsEnabled,
+ IsEnabled: service.GetIsEnabled(), // Use getter
EventTriggers: service.EventTriggers,
RetryPolicy: service.RetryPolicy,
PayloadTemplate: service.PayloadTemplate,
@@ -1353,6 +1312,7 @@ func (h *Handlers) HandleUpdateNotificationService(c *gin.Context) {
serviceType := c.PostForm("type")
description := c.PostForm("description")
isEnabled := c.PostForm("is_enabled") == "on"
+ eventTriggers := c.PostFormArray("event_triggers[]")
// Validate required fields
if name == "" || serviceType == "" {
@@ -1364,7 +1324,8 @@ func (h *Handlers) HandleUpdateNotificationService(c *gin.Context) {
service.Name = name
service.Type = serviceType
service.Description = description
- service.IsEnabled = isEnabled
+ service.EventTriggers = eventTriggers
+ service.SetIsEnabled(isEnabled) // Use setter
// Update type-specific fields based on service type
switch serviceType {
@@ -1377,18 +1338,7 @@ func (h *Handlers) HandleUpdateNotificationService(c *gin.Context) {
service.SecretKey = c.PostForm("secret_key")
service.RetryPolicy = c.PostForm("retry_policy")
- // Update event triggers
- eventTriggers := make([]string, 0)
- if c.PostForm("trigger_job_start") == "on" {
- eventTriggers = append(eventTriggers, "job_start")
- }
- if c.PostForm("trigger_job_complete") == "on" {
- eventTriggers = append(eventTriggers, "job_complete")
- }
- if c.PostForm("trigger_job_error") == "on" {
- eventTriggers = append(eventTriggers, "job_error")
- }
- service.EventTriggers = eventTriggers
+ // Event triggers are handled above
case "pushbullet":
// Update Pushbullet-specific fields
@@ -1397,18 +1347,7 @@ func (h *Handlers) HandleUpdateNotificationService(c *gin.Context) {
service.Config["title_template"] = c.PostForm("pushbullet_title_template")
service.Config["body_template"] = c.PostForm("pushbullet_body_template")
- // Update event triggers
- eventTriggers := make([]string, 0)
- if c.PostForm("trigger_job_start") == "on" {
- eventTriggers = append(eventTriggers, "job_start")
- }
- if c.PostForm("trigger_job_complete") == "on" {
- eventTriggers = append(eventTriggers, "job_complete")
- }
- if c.PostForm("trigger_job_error") == "on" {
- eventTriggers = append(eventTriggers, "job_error")
- }
- service.EventTriggers = eventTriggers
+ // Event triggers are handled above
case "ntfy":
// Update Ntfy-specific fields
@@ -1420,18 +1359,7 @@ func (h *Handlers) HandleUpdateNotificationService(c *gin.Context) {
service.Config["title_template"] = c.PostForm("ntfy_title_template")
service.Config["message_template"] = c.PostForm("ntfy_message_template")
- // Update event triggers
- eventTriggers := make([]string, 0)
- if c.PostForm("trigger_job_start") == "on" {
- eventTriggers = append(eventTriggers, "job_start")
- }
- if c.PostForm("trigger_job_complete") == "on" {
- eventTriggers = append(eventTriggers, "job_complete")
- }
- if c.PostForm("trigger_job_error") == "on" {
- eventTriggers = append(eventTriggers, "job_error")
- }
- service.EventTriggers = eventTriggers
+ // Event triggers are handled above
case "gotify":
// Update Gotify-specific fields
@@ -1441,18 +1369,7 @@ func (h *Handlers) HandleUpdateNotificationService(c *gin.Context) {
service.Config["title_template"] = c.PostForm("gotify_title_template")
service.Config["message_template"] = c.PostForm("gotify_message_template")
- // Update event triggers
- eventTriggers := make([]string, 0)
- if c.PostForm("trigger_job_start") == "on" {
- eventTriggers = append(eventTriggers, "job_start")
- }
- if c.PostForm("trigger_job_complete") == "on" {
- eventTriggers = append(eventTriggers, "job_complete")
- }
- if c.PostForm("trigger_job_error") == "on" {
- eventTriggers = append(eventTriggers, "job_error")
- }
- service.EventTriggers = eventTriggers
+ // Event triggers are handled above
case "pushover":
// Update Pushover-specific fields
@@ -1464,18 +1381,7 @@ func (h *Handlers) HandleUpdateNotificationService(c *gin.Context) {
service.Config["title_template"] = c.PostForm("pushover_title_template")
service.Config["message_template"] = c.PostForm("pushover_message_template")
- // Update event triggers
- eventTriggers := make([]string, 0)
- if c.PostForm("trigger_job_start") == "on" {
- eventTriggers = append(eventTriggers, "job_start")
- }
- if c.PostForm("trigger_job_complete") == "on" {
- eventTriggers = append(eventTriggers, "job_complete")
- }
- if c.PostForm("trigger_job_error") == "on" {
- eventTriggers = append(eventTriggers, "job_error")
- }
- service.EventTriggers = eventTriggers
+ // Event triggers are handled above
}
// Save to database
@@ -1489,7 +1395,7 @@ func (h *Handlers) HandleUpdateNotificationService(c *gin.Context) {
auditDetails := map[string]interface{}{
"name": service.Name,
"type": service.Type,
- "is_enabled": service.IsEnabled,
+ "is_enabled": service.GetIsEnabled(), // Use getter
"description": service.Description,
"event_triggers": service.EventTriggers,
}
@@ -1524,7 +1430,7 @@ func (h *Handlers) handleNotificationsWithError(c *gin.Context, errorMessage str
ID: service.ID,
Name: service.Name,
Type: service.Type,
- IsEnabled: service.IsEnabled,
+ IsEnabled: service.GetIsEnabled(), // Use getter
Config: service.Config,
Description: service.Description,
EventTriggers: service.EventTriggers,
@@ -1559,7 +1465,7 @@ func (h *Handlers) handleNotificationsWithSuccess(c *gin.Context, successMessage
ID: service.ID,
Name: service.Name,
Type: service.Type,
- IsEnabled: service.IsEnabled,
+ IsEnabled: service.GetIsEnabled(), // Use getter
Config: service.Config,
Description: service.Description,
EventTriggers: service.EventTriggers,
diff --git a/main.go b/main.go
index ed242e1..078cf18 100644
--- a/main.go
+++ b/main.go
@@ -14,6 +14,7 @@ import (
"github.com/gin-gonic/gin"
"github.com/robfig/cron/v3"
+
// "github.com/starfleetcptn/gomft/internal/api"
"github.com/starfleetcptn/gomft/components"
"github.com/starfleetcptn/gomft/internal/config"
@@ -144,7 +145,7 @@ func main() {
var dbJobExecutor scheduler.JobExecutorDB = database
var dbScheduler scheduler.SchedulerDB = database
- notifier := scheduler.NewNotifier(dbNotifier, schedLogger)
+ notifier := scheduler.NewNotifier(dbNotifier, schedLogger, cfg.SkipSSLVerify)
metadataHandler := scheduler.NewMetadataHandler(dbMetadata, schedLogger)
transferExecutor := scheduler.NewTransferExecutor(dbTransfer, schedLogger, metadataHandler, notifier)
jobExecutor := scheduler.NewJobExecutor(dbJobExecutor, schedLogger, schedCron, jobsMap, &jobMutex, transferExecutor, notifier)