diff --git a/.gitignore b/.gitignore index 7d8fffa..b8726fd 100644 --- a/.gitignore +++ b/.gitignore @@ -81,4 +81,4 @@ backups/ # Ignore binaries -gomft +gomft \ No newline at end of file diff --git a/README.md b/README.md index 421ec81..3cd619d 100644 --- a/README.md +++ b/README.md @@ -52,6 +52,15 @@ GoMFT is a web-based managed file transfer application built with Go, leveraging - Custom HTTP headers - Selectable events (job success, job failure) - Detailed JSON payload with job information +- **Multiple Notification Services**: Get job status updates through various notification channels: + - Email notifications with configurable SMTP settings + - Webhooks with authentication for custom integrations + - Pushbullet notifications with optional device targeting + - Ntfy.sh (both public and self-hosted) for simple push notifications + - Gotify server integration for self-hosted notifications + - Pushover notifications with customizable sounds and priorities + - Configurable message templates for all notification types + - Event-based triggers (job start, completion, errors) - **Scheduled Transfers**: Configure transfers using cron expressions with flexible scheduling options - **Transfer Monitoring**: Real-time status updates and detailed transfer logs with bytes and files transferred statistics - **File Metadata Tracking**: Complete history and status of all transferred files with detailed information: @@ -77,6 +86,14 @@ GoMFT is a web-based managed file transfer application built with Go, leveraging - **Transfer Configurations**: Full control over source and destination connection parameters - **Job Management**: Create, edit, and monitor transfer jobs with scheduling - **Security**: Role-based access control with admin-managed user accounts and secure password management +- **Authentication Providers**: Flexible authentication options: + - Built-in email/password authentication + - Authentik integration for enterprise SSO + - OpenID Connect (OIDC) support for standard identity providers + - OAuth2 integration for popular providers (Google, GitHub, etc.) + - Multiple provider support with fallback options + - Automatic user provisioning from external providers + - Role mapping from external identity providers - **Password Recovery**: Self-service password reset via email with secure token-based authentication - **User Profile Management**: Personal settings including theme preferences - **Modern UI**: Built with Templ, HTMX and Tailwind CSS for a responsive experience @@ -355,7 +372,41 @@ Log files contain detailed information about file transfers, job execution, and - Choose notification triggers (job success, job failure, or both) - Test your webhook integration with manual job runs -8. Manage file metadata: +8. **Webhook Notifications**: + - **Webhook Integration**: Send notifications to external systems when jobs complete + - **Secure Authentication**: HMAC-SHA256 signature for webhook verification + - **Custom Headers**: Add custom HTTP headers to webhook requests + - **Flexible Configuration**: Configure different webhooks for different jobs + - **Event Selection**: Choose to send notifications on success, failure, or both + - **Detailed Payload**: Rich JSON payload with complete job execution details + +9. **Multiple Notification Services**: + - **Pushbullet Integration**: Send notifications to your devices through Pushbullet + - Device targeting support for specific device delivery + - Customizable title and message templates + - API key-based authentication + - **Ntfy Integration**: Use public ntfy.sh or self-hosted ntfy server + - Topic-based routing of notifications + - Priority levels for different job events + - Optional username/password authentication for private servers + - Customizable title and message templates + - **Gotify Integration**: Send notifications to self-hosted Gotify servers + - Application token-based authentication + - Priority levels (1-10) for different notification importance + - Customizable title and message templates + - **Pushover Integration**: Professional notification delivery service + - Application and user key authentication + - Device targeting for selective delivery + - Sound selection for different notification types + - Priority levels from lowest to emergency + - Customizable title and message templates + - **Common Features**: + - Variable substitution in notification templates + - Job data access in templates (status, files, bytes, times) + - Event-based filtering (job start, completion, errors) + - Success/failure tracking for diagnostic purposes + +10. Manage file metadata: - Navigate to the "Files" section to view all processed files - Use filters to quickly find files by status, job ID, or filename - Click on any file to view detailed metadata including timestamps, size, and hash @@ -363,7 +414,7 @@ Log files contain detailed information about file transfers, job execution, and - Delete file metadata records when no longer needed - View files associated with specific jobs by navigating from the job details -9. Utilize admin tools (administrators only): +11. Utilize admin tools (administrators only): - Access the "Admin Tools" section from the navigation menu - View system statistics and server information - Create and manage database backups @@ -373,19 +424,57 @@ Log files contain detailed information about file transfers, job execution, and ### User Management -GoMFT uses a role-based access control system: +GoMFT uses a role-based access control system with flexible authentication options: - **Administrators**: Can create and manage users, access all features - **Regular Users**: Can manage transfers and view history -User management features: -- Only administrators can create new user accounts -- User passwords are securely hashed with bcrypt -- Password history tracking prevents reuse of recent passwords -- Account lockout after multiple failed login attempts -- Self-service password reset via secure email links -- JWT-based authentication with tokens -- User theme preference settings (light/dark) +#### Authentication Options + +1. **Built-in Authentication**: + - Email/password login with secure password hashing + - JWT-based session management + - Password history tracking + - Account lockout protection + - Self-service password reset + +2. **External Authentication Providers**: + - **Authentik Integration**: + - Enterprise-grade SSO capabilities + - Automatic user provisioning + - Role synchronization + - Group mapping support + - Secure token exchange + + - **OpenID Connect (OIDC)**: + - Standard-compliant identity provider support + - Automatic user creation and updates + - Role mapping from OIDC claims + - Multiple provider support + - Secure token validation + + - **OAuth2 Providers**: + - Google authentication + - GitHub integration + - Other OAuth2-compliant providers + - Custom provider configuration + - Automatic profile synchronization + +3. **Security Features**: + - Secure password hashing with bcrypt + - JWT-based authentication with tokens + - Password history tracking prevents reuse + - Account lockout after failed attempts + - Two-factor authentication support + - Session management and timeout + - Secure token storage and handling + +4. **User Profile Management**: + - Theme preferences (light/dark mode) + - Profile information updates + - Password change functionality + - Two-factor authentication setup + - External account linking ### Two-Factor Authentication (2FA) Implementation @@ -479,13 +568,16 @@ The following fields have been added to the `users` table: - Manual execution - Enable/disable schedules -8. **Webhook Notifications**: - - **Webhook Integration**: Send notifications to external systems when jobs complete - - **Secure Authentication**: HMAC-SHA256 signature for webhook verification - - **Custom Headers**: Add custom HTTP headers to webhook requests - - **Flexible Configuration**: Configure different webhooks for different jobs - - **Event Selection**: Choose to send notifications on success, failure, or both - - **Detailed Payload**: Rich JSON payload with complete job execution details +8. **Notification Options**: + - **Email Notifications**: Receive job status updates via email + - **Webhook Notifications**: Integration with external systems + - **Pushbullet**: Push notifications to your devices + - **Ntfy**: Simple push notifications via ntfy.sh + - **Gotify**: Self-hosted notification server integration + - **Pushover**: Professional notification service + - Configure event triggers (start, complete, error) + - Customize notification message templates + - Selective notification based on job status ### Email Notifications diff --git a/components/admin.templ b/components/admin.templ new file mode 100644 index 0000000..a48177c --- /dev/null +++ b/components/admin.templ @@ -0,0 +1,125 @@ +package components + +import ( + "context" +) + +// AdminLayout renders the main admin dashboard +templ AdminLayout(ctx context.Context) { + @LayoutWithContext("Administration", ctx) { +
+ + + + +
+
+ +
+
+
+

Total Users

+

1,234

+
+
+ +
+
+
+ +
+
+
+

Active Sessions

+

42

+
+
+ +
+
+
+ +
+
+
+

System Health

+

98%

+
+
+ +
+
+
+
+ + +
+
+

Recent Activity

+
+
+ + + + + + + + + + + + + + + + +
UserActionTimestamp
admin@example.comUpdated user permissions2024-02-20 14:32:10
+
+
+
+
+ } +} \ No newline at end of file diff --git a/components/admin_audit.templ b/components/admin_audit.templ new file mode 100644 index 0000000..007f84b --- /dev/null +++ b/components/admin_audit.templ @@ -0,0 +1,384 @@ +package components + +import ( + "context" + "fmt" + "time" +) + +// AuditLogEntry represents an audit log entry for display +type AuditLogEntry struct { + ID uint + Action string + EntityType string + EntityID uint + Username string + UserID uint + Timestamp time.Time + Details map[string]interface{} + DetailsSummary string +} + +type AuditLogsData struct { + Logs []AuditLogEntry + TotalPages int + CurrentPage int + TotalRecords int + FilterAction string + FilterEntity string + FilterUser string + FilterDateFrom string + FilterDateTo string +} + +// AdminAuditLogs renders the audit logs page +templ AdminAuditLogs(ctx context.Context, data AuditLogsData) { + @LayoutWithContext("Audit Logs", ctx) { +
+
+ +
+

+ + Audit Logs +

+ + + Export Logs + +
+ + +
+ + + + + + +
+
+

Filter Logs

+
+
+
+ +
+ + +
+ + +
+ + +
+ + +
+ + +
+ + +
+ + +
+ +
+ + +
+ + +
+ + Clear Filters + + +
+
+
+
+ + +
+
+

Audit Log Entries

+ Total entries: {fmt.Sprint(data.TotalRecords)} +
+
+ + + + + + + + + + + + if len(data.Logs) == 0 { + + + + } else { + for _, log := range data.Logs { + + + + + + + + } + } + +
TimestampUserActionEntityDetails
+ No audit logs found matching your filters. +
+ {log.Timestamp.Format("2006-01-02 15:04:05")} + + {log.Username} + + + {log.Action} + + + {log.EntityType} + + (ID: {fmt.Sprint(log.EntityID)}) + + + +
+
+ + + if data.TotalPages > 1 { +
+
+
+ Showing page {fmt.Sprint(data.CurrentPage)} of {fmt.Sprint(data.TotalPages)} +
+
+ if data.CurrentPage > 1 { + + Previous + + } + + + + + if data.CurrentPage < data.TotalPages { + + Next + + } +
+
+
+ } +
+
+
+ + + + + + + } +} + +// Helper functions +func getActionClass(action string) string { + baseClass := "px-2 py-1 text-xs rounded-full " + + switch action { + case "create": + return baseClass + "bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-300" + case "update": + return baseClass + "bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-300" + case "delete": + return baseClass + "bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-300" + case "login": + return baseClass + "bg-purple-100 text-purple-800 dark:bg-purple-900 dark:text-purple-300" + case "logout": + return baseClass + "bg-yellow-100 text-yellow-800 dark:bg-yellow-900 dark:text-yellow-300" + default: + return baseClass + "bg-gray-100 text-gray-800 dark:bg-gray-700 dark:text-gray-300" + } +} + +func buildPaginationURL(data AuditLogsData, page int) templ.SafeURL { + url := fmt.Sprintf("/admin/audit?page=%d", page) + + if data.FilterAction != "" { + url += fmt.Sprintf("&action=%s", data.FilterAction) + } + + if data.FilterEntity != "" { + url += fmt.Sprintf("&entity=%s", data.FilterEntity) + } + + if data.FilterUser != "" { + url += fmt.Sprintf("&user=%s", data.FilterUser) + } + + if data.FilterDateFrom != "" { + url += fmt.Sprintf("&date_from=%s", data.FilterDateFrom) + } + + if data.FilterDateTo != "" { + url += fmt.Sprintf("&date_to=%s", data.FilterDateTo) + } + + return templ.SafeURL(url) +} + + diff --git a/components/admin_database.templ b/components/admin_database.templ new file mode 100644 index 0000000..20738bb --- /dev/null +++ b/components/admin_database.templ @@ -0,0 +1,442 @@ +package components + +import ( + "context" + "time" +) + +// BackupFile represents a database backup file +type BackupFile struct { + Name string + Size string + Created time.Time + FullPath string +} + +// AdminDatabaseTools renders the database tools page with backup and restore functionality +templ AdminDatabaseTools(ctx context.Context, backups []BackupFile) { + @LayoutWithContext("Database Tools", ctx) { +
+
+ +
+

+ + Database Tools +

+
+ +
+
+ + +
+ + +
+ +
+
+
+ +
+

Optimize Database

+
+

Vacuum the database to reclaim space and optimize performance.

+
+ +
+
+ + +
+
+
+ +
+

Clear Job History

+
+

Remove old job history records to free up database space.

+
+ +
+
+ + +
+
+
+ +
+

Export Configuration

+
+

Export system configurations and jobs for backup or migration.

+ +
+
+ + +
+
+

Database Backups

+ + + Refresh + +
+
+ + + + + + + + + + + if len(backups) == 0 { + + + + } else { + for _, backup := range backups { + + + + + + + } + } + +
Backup NameSizeCreatedActions
+ No backups found. Create your first backup to get started. +
{ backup.Name }{ backup.Size }{ backup.Created.Format("2006-01-02 15:04:05") } +
+ + + + + +
+
+
+
+ + +
+
+

Restore from Backup File

+
+
+
+
+ +
+ +
+
+
+ +
+
+
+
+ + +
+
+
+ +
+
+

Important Safety Information

+
    +
  • Always create a backup before making significant changes
  • +
  • Restoring a backup will overwrite all current data
  • +
  • Database optimization may take time for large databases
  • +
  • Clear job history operations cannot be undone
  • +
+
+
+
+
+
+ + + + + + + + + } +} \ No newline at end of file diff --git a/components/admin_role_form.templ b/components/admin_role_form.templ new file mode 100644 index 0000000..7edfeac --- /dev/null +++ b/components/admin_role_form.templ @@ -0,0 +1,175 @@ +package components + +import ( + "context" + "fmt" + "strings" +) + +type RoleFormData struct { + Role *Role + IsNew bool + ErrorMessage string + AllPermissions []string +} + +templ AdminRoleForm(ctx context.Context, data RoleFormData) { + @LayoutWithContext(getFormTitle(data.IsNew), ctx) { +
+ +
+ + if data.ErrorMessage != "" { + + } + +
+
+
+ if !data.IsNew { + + } + + +
+ + +
+ + +
+ + +
+ + +
+ +
+ for _, perm := range data.AllPermissions { +
+ + +
+ } +
+
+ + +
+ + Cancel + + +
+
+
+
+ } +} + +// Helper functions +func getFormTitle(isNew bool) string { + if isNew { + return "New Role" + } + return "Edit Role" +} + +func getFormAction(isNew bool, role *Role) templ.SafeURL { + if isNew { + return "/admin/roles" + } + return templ.SafeURL("/admin/roles/" + fmt.Sprint(role.ID)) +} + +func hasPermission(role *Role, permission string) bool { + if role == nil || role.Permissions == nil { + return false + } + for _, p := range role.Permissions { + if p == permission { + return true + } + } + return false +} + +func formatPermissionLabel(permission string) string { + // Convert permission strings like "users.create" to "Create Users" + parts := strings.Split(permission, ".") + if len(parts) != 2 { + return permission + } + + resource := strings.Title(parts[0]) + action := strings.Title(parts[1]) + + return action + " " + resource +} \ No newline at end of file diff --git a/components/admin_roles.templ b/components/admin_roles.templ new file mode 100644 index 0000000..a39213c --- /dev/null +++ b/components/admin_roles.templ @@ -0,0 +1,323 @@ +package components + +import ( + "context" + "fmt" +) + +type Role struct { + ID uint + Name string + Description string + Permissions []string +} + +type RolesData struct { + Roles []Role + Error string +} + +templ AdminRoles(ctx context.Context, data RolesData) { + @LayoutWithContext("Role Management", ctx) { + +
+ + +
+

+ + Role Management +

+ + + New Role + +
+ + +
+
+ + + + + + + + + + + if len(data.Roles) == 0 { + + + + } else { + for _, role := range data.Roles { + + @RoleDialog( + fmt.Sprintf("delete-role-dialog-%d", role.ID), + "Delete Role", + fmt.Sprintf("Are you sure you want to delete role \"%s\"? This action cannot be undone.", role.Name), + "bg-red-600 hover:bg-red-700 dark:bg-red-600 dark:hover:bg-red-700", + "Delete Role", + "delete", + role.ID, + role.Name, + ) + + + + + + + + } + } + +
Role NameDescriptionPermissionsActions
+ No roles found. Create your first role to get started. +
{ role.Name }{ role.Description } +
+ for _, perm := range role.Permissions { + + { perm } + + } +
+
+
+ + + + +
+
+
+
+ + + + + if data.Error != "" { + + } + } +} + +// RoleDialog for confirmation actions +templ RoleDialog(id string, title string, message string, confirmClass string, confirmText string, action string, roleID uint, roleName string) { + +} + +script hideRoleDialog(id string) { + document.getElementById(id).classList.add("hidden"); + document.getElementById(id).classList.remove("flex"); +} + +script showRoleDialog(id string) { + document.getElementById(id).classList.remove("hidden"); + document.getElementById(id).classList.add("flex"); +} + +script triggerRoleDelete(dialogId string, roleID uint, roleName string) { + // Hide the dialog + document.getElementById(dialogId).classList.add("hidden"); + document.getElementById(dialogId).classList.remove("flex"); + + // Store data in a way that's accessible to event handlers + window.lastDeletedRole = { + id: roleID, + name: roleName + }; + + // Add custom marker to track this deletion + window.isRoleDeletingRequest = true; +} \ No newline at end of file diff --git a/components/admin_tools.templ b/components/admin_tools.templ deleted file mode 100644 index 5722e82..0000000 --- a/components/admin_tools.templ +++ /dev/null @@ -1,1389 +0,0 @@ -package components - -import ( - "context" - "fmt" - "time" -) - -type BackupFile struct { - Name string - Size string - ModTime time.Time -} - -type LogFile struct { - Name string - Size string - ModTime time.Time - Path string -} - -type AdminToolsData struct { - JobHistoryCount int - DatabaseSize string - LastBackupTime *time.Time - BackupCount int - SystemUptime string - ActiveJobs int - TotalConfigs int - TotalJobs int - TotalUsers int - DatabasePath string - BackupPath string - MaintenanceMessage string - BackupFiles []BackupFile - LogFiles []LogFile - LogContent string - CurrentLogFile string - EmailTestSuccess *bool - EmailTestMessage string - SmtpServer string - WebhookTestSuccess *bool - WebhookTestMessage string - WebhookStatusCode int - WebhookResponse string -} - -// Dialog component for confirmation dialogs -templ Dialog(id string, title string, message string, confirmClass string, confirmText string, formId string, targetAction string) { - -} - -script hideDialog(id string) { - document.getElementById(id).classList.add("hidden"); -} - -script submitFormAndHideDialog(formId string, dialogId string) { - // Use HTMX's API to trigger the request instead of bypassing it - htmx.trigger(document.getElementById(formId), 'submit'); - document.getElementById(dialogId).classList.add("hidden"); -} - -script showDialog(id string) { - document.getElementById(id).classList.remove("hidden"); -} - -// Backup dialog component specifically for restore and delete actions -templ BackupActionDialog(id string, title string, message string, confirmClass string, confirmText string, action string, backupName string) { - -} - -templ AdminTools(ctx context.Context, data AdminToolsData) { - @LayoutWithContext("Admin Tools", ctx) { - -
-
-
-

- - Admin Tools -

-
- - -
-
- - if data.MaintenanceMessage != "" { -
-
-
- -
-
-

- { data.MaintenanceMessage } -

-
-
-
- } - - -
-

- - System Overview -

-
-
-
-
-
- -
-
-
-
- Database Size -
-
-
- { data.DatabaseSize } -
-
-
-
-
-
-
- -
-
-
-
- -
-
-
-
- Job History Records -
-
-
- { fmt.Sprint(data.JobHistoryCount) } -
-
-
-
-
-
-
- -
-
-
-
- -
-
-
-
- System Uptime -
-
-
- { data.SystemUptime } -
-
-
-
-
-
-
- -
-
-
-
- -
-
-
-
- Active Jobs -
-
-
- { fmt.Sprint(data.ActiveJobs) } -
-
-
-
-
-
-
-
-
- - -
- -
-
-

- - Backup & Restore -

-
- -
-
- if data.LastBackupTime != nil { - Last backup: { data.LastBackupTime.Format("Jan 02, 2006 15:04:05") } - } else { - Last backup: Never - } -
- - - - - - - -
- -
- -
- -
- -
-

Restore Database

-
-
-
- - -
- -
-

- - Warning: This will replace your current database. Make sure to backup first! -

-
-
-
-
- - -
-
-

- - Maintenance Tools -

-
-
- - @Dialog("clear-job-dialog", "Clear Job History", "Are you sure you want to clear all job history? This cannot be undone.", "btn-danger", "Clear History", "purge-form", "") -
- -
- -
-
- -

- Runs VACUUM to optimize the database and reclaim unused space. -

-
-
- -
-
-
- - -
-
-
-

- - Email Testing -

-
-
-

- Test your email configuration by sending a test email to verify the server can send emails properly. -

- -
-
-
- - -
- -
- - -
- -
- - -
- -
- -
-
-
- - - - - -
-

- - Email Configuration -

- if data.SmtpServer != "" { -

- Current SMTP server: { data.SmtpServer } -

- } else { -

- Email settings are configured in your application configuration file. Make sure SMTP settings are properly configured before testing. -

- } -
-
-
-
- - -
-
-
-

- - Webhook Notification Testing -

-
-
-

- Test webhook notifications by sending a sample job execution payload to your webhook endpoint. -

- -
-
-
- -
-
- -
- -
-
- -
- -
-
- -
- -
-

- - Used to sign webhook payloads (X-Hub-Signature-256 header) -

-
- -
- -
-
- -
- -
-

- - Additional HTTP headers as JSON -

-
- -
- - -

- - Leave empty to use default test payload -

-
- -
- -
-
-
- - - - - -
-

- - Default Webhook Test Payload -

-
-
{
-										"event_type": "job_execution",
-										"job_id": 123,
-										"job_name": "Test Job",
-										"config_id": 456,
-										"config_name": "Test Config",
-										"status": "completed",
-										"start_time": "2023-06-18T15:30:45Z",
-										"end_time": "2023-06-18T15:35:12Z",
-										"duration_seconds": 267,
-										"history_id": 789,
-										"bytes_transferred": 1048576,
-										"files_transferred": 5,
-										"source": {
-											"type": "local",
-											"path": "/path/to/source"
-										},
-										"destination": {
-											"type": "s3",
-											"path": "bucket/path"
-										}
-										}
-
-
-
-
-
- - -
- @BackupsList(data) -
- - -
-
-
-

- - System Information -

-
-
-
-
-
Database Path
-
- { data.DatabasePath } -
-
- -
-
Backup Directory
-
- { data.BackupPath } -
-
- -
-
Total Users
-
- { fmt.Sprint(data.TotalUsers) } -
-
- -
-
Total Configurations
-
- { fmt.Sprint(data.TotalConfigs) } -
-
- -
-
Total Jobs
-
- { fmt.Sprint(data.TotalJobs) } -
-
- -
-
Application Version
-
-
- { AppVersion } - if AppVersion == "dev" { - - - - } else if AppVersion == "1.0.0" { - - - - } else if AppVersion == "1.1.0" { - - - - } else { - - - - } -
-
-
-
-
-
-
- - -
-
-
-

- - Build Information -

-
-
-
-
-
-
-
Version
-
- { AppVersion } -
-
- -
-
Build Time
-
- { getBuildTime() } -
-
- -
-
Commit
-
- { getCommit() } -
-
-
- - -
-
-
-
-
- - -
- @AdminLogViewer(data) -
- - -
-
-
-

- - Webhook Notifications -

-
-
-

- GoMFT can send webhook notifications when jobs run. You can configure webhooks - for individual jobs in the job edit form. Below is the format of the webhook payload: -

- -
-
{
-							"event_type": "job_execution",
-							"job_id": 123,
-							"job_name": "Daily Backup",
-							"config_id": 456,
-							"config_name": "Backup Config",
-							"status": "completed",
-							"start_time": "2023-06-18T15:30:45Z",
-							"end_time": "2023-06-18T15:35:12Z",
-							"duration_seconds": 267,
-							"history_id": 789,
-							"bytes_transferred": 1048576,
-							"files_transferred": 5,
-							"source": {
-								"type": "local",
-								"path": "/path/to/source"
-							},
-							"destination": {
-								"type": "s3",
-								"path": "bucket/path"
-							}
-							}
-
- -

Authentication

-

- When configuring a webhook, you can optionally provide a secret key. This will be used to sign - the webhook payload with HMAC-SHA256. The signature is provided in the X-Hub-Signature-256 header. -

- -

HTTP Request Details

-
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
PropertyValue
MethodPOST
Content-Typeapplication/json
User-AgentGoMFT-Webhook/1.0
X-Hub-Signature-256HMAC SHA256 signature (if secret configured)
Custom HeadersAny additional headers specified in the job configuration
-
-
-
-
-
-
- } -} - -// BackupsList is a separate component for the backups list that can be refreshed via HTMX -templ BackupsList(data AdminToolsData) { - if len(data.BackupFiles) > 0 { -
-
-

- - Available Backups -

- -
-
-
-
- - - - - - - - - - - for _, backup := range data.BackupFiles { - - - - - - - } - -
NameSizeDateAction
{ backup.Name }{ backup.Size }{ backup.ModTime.Format("Jan 02, 2006 15:04:05") } - - - - - - @BackupActionDialog( - fmt.Sprintf("restore-dialog-%s", backup.Name), - "RESTORE BACKUP", - fmt.Sprintf("Are you sure you want to restore the backup '%s'? This will replace your current database.", backup.Name), - "btn-warning", - "Restore", - "restore", - backup.Name, - ) - - - - @BackupActionDialog( - fmt.Sprintf("delete-dialog-%s", backup.Name), - "DELETE BACKUP", - fmt.Sprintf("Are you sure you want to delete the backup '%s'? This cannot be undone.", backup.Name), - "btn-danger", - "Delete", - "delete", - backup.Name, - ) - -
-
-
-
-
- } else { -
-
-

- - Available Backups -

- -
-
-
-
- -
-

No Backups Available

-

- Create a backup using the "Backup Database" button. -

-
-
-
- } -} - -// Add this new template after other admin tool templates -templ AdminLogViewer(data AdminToolsData) { -
-

- Log Files -

- -
-
-

Available Logs

-
- if len(data.LogFiles) == 0 { -
- No log files found -
- } else { -
- for _, logFile := range data.LogFiles { - - } -
- } -
-
- -
-
- -
-
-

- if data.CurrentLogFile != "" { - Log: { data.CurrentLogFile } - } else { - Select a log file - } -

- if data.CurrentLogFile != "" { -
- -
- } -
- - @AdminLogContent(data) -
-
-
-} - -// AdminLogContent template for log view -templ AdminLogContent(data AdminToolsData) { -
-
- -
- if data.CurrentLogFile == "" { -
-
- -

Select a log file to view its contents

-
-
- } else { - -
- -
- { data.LogContent } -
- - -
-
-
-
- - - } -
-} - -// getBuildTime returns the build time of the application -// This can be set using ldflags during build -// Example: go build -ldflags "-X github.com/starfleetcptn/gomft/components.BuildTime=2023-01-01T00:00:00Z" -var BuildTime = "unknown" - -func getBuildTime() string { - return BuildTime -} - -// getCommit returns the commit hash of the application -// This can be set using ldflags during build -// Example: go build -ldflags "-X github.com/starfleetcptn/gomft/components.Commit=abcdef123456" -var Commit = "unknown" - -func getCommit() string { - return Commit -} - -// EmailTestToast is a component for showing email test results -templ EmailTestToast(success bool, message string) { -
-
-
- if success { - - } else { - - } -
-
-

- if success { - Email Sent Successfully - } else { - Email Sending Failed - } -

-
- { message } -
-
-
- -
-
-
-} - -// Add a style for the animate-fade-in animation -script fadeInAnimation() { - // Add CSS animation if it doesn't exist - if (!document.getElementById('fade-in-animation')) { - const style = document.createElement('style'); - style.id = 'fade-in-animation'; - style.textContent = ` - @keyframes fadeIn { - from { opacity: 0; transform: translateY(-10px); } - to { opacity: 1; transform: translateY(0); } - } - .animate-fade-in { - animation: fadeIn 0.3s ease-out forwards; - } - `; - document.head.appendChild(style); - } -} - -// WebhookTestToast is a component for showing webhook test results -templ WebhookTestToast(success bool, message string, statusCode int, responseBody string) { -
-
-
- if success { - - } else { - - } -
-
-

- if success { - Webhook Sent Successfully - } else { - Webhook Sending Failed - } -

-
- { message } -
- - -
-
Status Code: { fmt.Sprint(statusCode) }
- if responseBody != "" { -
-
Response:
-
-
{ responseBody }
-
-
- } -
-
-
- -
-
-
-} diff --git a/components/auth_provider_form.templ b/components/auth_provider_form.templ new file mode 100644 index 0000000..eca1ef7 --- /dev/null +++ b/components/auth_provider_form.templ @@ -0,0 +1,655 @@ +package components + +import ( + "fmt" + "encoding/json" + "context" + "github.com/starfleetcptn/gomft/internal/db" +) + +templ AuthProviderForm(ctx context.Context, provider *db.AuthProvider, isNew bool) { + @LayoutWithContext(getPageTitle(isNew), ctx) { +
+
+
+
+ if isNew { +

+ + New Authentication Provider +

+

Configure a new external authentication source

+ } else { +

+ + Edit Authentication Provider +

+

Update an existing external authentication source

+ } +
+
+ + Back to Providers + +
+
+ +
+ if isNew { +
+ @formContent(provider, isNew) +
+ } else { +
+ + @formContent(provider, isNew) +
+ } +
+ + +
+
+
+ +
+
+

Authentication providers allow users to sign in using external identity providers.

+
+
+ +
+
+ +
+
+

Make sure to enter the correct callback URL in your external provider's configuration.

+
+
+ +
+
+ +
+
+

Configure attribute mappings to match the fields in your identity provider's user data.

+
+
+
+
+
+ + + } +} + +func getPageTitle(isNew bool) string { + if isNew { + return "New Authentication Provider" + } + return "Edit Authentication Provider" +} + +templ formContent(provider *db.AuthProvider, isNew bool) { +
+ +
+ + +

A descriptive name for this authentication provider

+
+ + +
+ + +

The type of external authentication service

+
+ + +
+
+ if provider == nil || provider.Enabled { + + } else { + + } + +
+

Whether this authentication provider is active and available for login

+
+ + +
+ + +

Additional information about this authentication provider

+
+
+ + +
+

Provider Icon

+ +
+
+ + +

URL to the provider's icon image (SVG recommended)

+
+ +
+ +
+
+ if provider != nil && provider.IconURL != "" { + + } else { + + } +
+
+

Preview of the icon that will be displayed on the login button.

+

If no URL is provided, a default icon will be used based on the provider type.

+
+
+
+
+
+ +
+

Connection Settings

+ +
+ +
+ + +

The base URL of the authentication provider

+
+ + +
+ + +

The client identifier assigned by the authentication provider

+
+ + +
+ + if isNew { + + } else { + + } +

The client secret for authentication with the provider

+
+ + +
+ + +

The callback URL that will handle the authentication response

+
+ + +
+ + +

Space-separated list of scopes to request from the provider

+
+
+
+ +
+

Authentik Settings

+ +
+ +
+ + +

Authentik tenant ID (optional, defaults to 'default')

+
+
+
+ +
+

OpenID Connect Settings

+ +
+ +
+ + +

URL to the OIDC discovery document

+
+
+
+ +
+

SAML Settings

+ +
+ +
+ + +

URL to the SAML metadata XML

+
+
+
+ +
+

User Attribute Mapping

+ +
+ +
+ + +

The attribute to use as the username

+
+ + +
+ + +

The attribute to use as the email address

+
+ + +
+ + +

The attribute to use as the display name

+
+ + +
+ + +

The attribute that contains user groups

+
+
+
+ +
+ + Cancel + + if !isNew { + + } + +
+} + +func getValue(provider *db.AuthProvider, field string) string { + if provider == nil { + return "" + } + + switch field { + case "name": + return provider.Name + case "provider_url": + return provider.ProviderURL + case "client_id": + return provider.ClientID + case "redirect_url": + return provider.RedirectURL + case "scopes": + return provider.Scopes + case "icon_url": + return provider.IconURL + default: + return "" + } +} + +func getRedirectURL(provider *db.AuthProvider) string { + if provider == nil || provider.RedirectURL == "" { + return fmt.Sprintf("https://%s/auth/callback", "your-app-domain.com") + } + return provider.RedirectURL +} + +func getConfigValue(provider *db.AuthProvider, key string) string { + if provider == nil || provider.Config == "" { + return "" + } + + var config map[string]interface{} + if err := json.Unmarshal([]byte(provider.Config), &config); err != nil { + return "" + } + + if value, ok := config[key]; ok { + if strValue, ok := value.(string); ok { + return strValue + } + } + + return "" +} + +func getAttributeValue(provider *db.AuthProvider, key string) string { + if provider == nil || provider.AttributeMapping == "" { + return "" + } + + var mapping map[string]string + if err := json.Unmarshal([]byte(provider.AttributeMapping), &mapping); err != nil { + return "" + } + + if value, ok := mapping[key]; ok { + return value + } + + return "" +} \ No newline at end of file diff --git a/components/auth_providers.templ b/components/auth_providers.templ new file mode 100644 index 0000000..1fa052b --- /dev/null +++ b/components/auth_providers.templ @@ -0,0 +1,380 @@ +package components + +import ( + "context" + "fmt" + "time" + "github.com/starfleetcptn/gomft/internal/db" +) + +// formatTime formats a time.Time value as a human-readable string +func formatTime(t time.Time) string { + return t.Format("Jan 02, 2006 15:04") +} + +templ AuthProviders(ctx context.Context, providers []db.AuthProvider) { + @LayoutWithContext("Authentication Providers", ctx) { + +
+ +
+
+
+
+

+ + Authentication Providers +

+

Manage external authentication sources like Authentik, OIDC, SAML, etc.

+
+
+ + + New Provider + +
+
+ +
+ if len(providers) > 0 { +
+
+ + + + + + + + + + + + + + for _, provider := range providers { + + + + + + + + + + } + +
NameIconTypeStatusProvider URLLast UsedActions
+ { provider.Name } + +
+ if provider.IconURL != "" { + { + } else { + if provider.Type == db.ProviderTypeAuthentik { + Authentik + } else if provider.Type == db.ProviderTypeOIDC { + OIDC + } else if provider.Type == db.ProviderTypeSAML { + SAML + } else if provider.Type == db.ProviderTypeOAuth2 { + OAuth2 + } else { + + } + } +
+
+ { string(provider.Type) } + + if provider.Enabled { + + Active + + } else { + + Disabled + + } + + { provider.ProviderURL } + + if provider.LastUsed.Valid { + { formatTime(provider.LastUsed.Time) } + } else { + Never + } + +
+ + + Edit + + + +
+
+
+
+ } else { +
+
+ +
+

No Authentication Providers

+

+ You haven't set up any external authentication providers yet. +

+ + + Add First Provider + +
+ } +
+ + +
+
+
+ +
+
+

Authentication providers allow users to sign in using external identity providers.

+
+
+ +
+
+ +
+
+

Make sure to configure callback URLs in your provider's settings.

+
+
+ +
+
+ +
+
+

Test your connections to ensure proper communication with external authentication systems.

+
+
+
+
+
+ + + + + + } +} \ No newline at end of file diff --git a/components/auth_providers_buttons.templ b/components/auth_providers_buttons.templ new file mode 100644 index 0000000..33aa3ef --- /dev/null +++ b/components/auth_providers_buttons.templ @@ -0,0 +1,52 @@ +package components + +import ( + "fmt" + "github.com/starfleetcptn/gomft/internal/db" +) + +// getProviderIcon returns the appropriate icon for a provider +func getProviderIcon(provider db.AuthProvider) templ.Component { + // If provider has a custom icon URL, use it + if provider.IconURL != "" { + return templ.Raw(fmt.Sprintf(`%s icon`, provider.IconURL, provider.Name)) + } + + // Otherwise fall back to default icons based on type + switch provider.Type { + case db.ProviderTypeAuthentik: + return templ.Raw(`Authentik`) + case db.ProviderTypeOIDC: + return templ.Raw(`OIDC`) + case db.ProviderTypeSAML: + return templ.Raw(`SAML`) + case db.ProviderTypeOAuth2: + return templ.Raw(`OAuth2`) + default: + return templ.Raw(``) + } +} + +templ AuthProviderButtons(providers []db.AuthProvider) { + if len(providers) == 0 { +
+ No external authentication providers available +
+ } else { +
+ for _, provider := range providers { + if provider.Enabled { + + + @getProviderIcon(provider) + + { provider.Name } + + } + } +
+ } +} \ No newline at end of file diff --git a/components/config_form.templ b/components/config_form.templ index 710bc92..a3dad3d 100644 --- a/components/config_form.templ +++ b/components/config_form.templ @@ -7,7 +7,6 @@ import ( "github.com/starfleetcptn/gomft/components/providers/source" "github.com/starfleetcptn/gomft/components/providers/destination" "github.com/starfleetcptn/gomft/components/providers/common" - "strconv" ) type ConfigFormData struct { @@ -47,7 +46,7 @@ func getInitialData(config *db.TransferConfig) string { sourceTeamDrive := "" // Google Photos source fields sourceReadOnly := false - sourceStartYear, _ := strconv.Atoi(getCurrentYear()) + sourceStartYear := 2025 sourceIncludeArchived := false filePattern := "" @@ -75,7 +74,7 @@ func getInitialData(config *db.TransferConfig) string { destTeamDrive := "" // Google Photos destination fields destReadOnly := false - destStartYear, _ := strconv.Atoi(getCurrentYear()) // Default to current year int + destStartYear := 2025 destIncludeArchived := false archivePath := "" @@ -84,6 +83,8 @@ func getInitialData(config *db.TransferConfig) string { skipProcessedFiles := true maxConcurrentTransfers := 4 rcloneFlags := "" + commandId := uint(1) // Default to 'copy' command + commandFlags := "" useBuiltinAuthSource := true useBuiltinAuthDest := true @@ -117,7 +118,7 @@ func getInitialData(config *db.TransferConfig) string { if config.SourceReadOnly != nil { sourceReadOnly = *config.SourceReadOnly } - sourceStartYear = config.SourceStartYear + // sourceStartYear = config.SourceStartYear if config.SourceIncludeArchived != nil { sourceIncludeArchived = *config.SourceIncludeArchived } @@ -152,7 +153,7 @@ func getInitialData(config *db.TransferConfig) string { if config.DestReadOnly != nil { destReadOnly = *config.DestReadOnly } - destStartYear = config.DestStartYear + // destStartYear = config.DestStartYear if config.DestIncludeArchived != nil { destIncludeArchived = *config.DestIncludeArchived } @@ -163,6 +164,8 @@ func getInitialData(config *db.TransferConfig) string { skipProcessedFiles = config.GetSkipProcessedFiles() maxConcurrentTransfers = config.MaxConcurrentTransfers rcloneFlags = config.RcloneFlags + commandId = config.CommandID + commandFlags = config.CommandFlags if config.UseBuiltinAuthSource != nil { useBuiltinAuthSource = *config.UseBuiltinAuthSource } else if sourceClientId != "" || sourceClientSecret != "" { @@ -238,12 +241,17 @@ func getInitialData(config *db.TransferConfig) string { skipProcessedFiles: %v, maxConcurrentTransfers: %d, rcloneFlags: '%s', + commandId: %d, + commandFlags: '%s', // Path validation states sourcePathValid: null, sourcePathError: '', destPathValid: null, destPathError: '', + + // Command configuration + requiresDestination: true, // Methods for path validation checkPath(path, type) { @@ -263,6 +271,68 @@ func getInitialData(config *db.TransferConfig) string { this[type + 'PathValid'] = false; this[type + 'PathError'] = 'Error checking path: ' + error.message; }); + }, + + // Method to check if destination is required based on command type + updateCommandRequirements() { + // List of commands that don't require destination + const listingCommands = ['ls', 'lsd', 'lsl', 'lsf', 'lsjson', 'listremotes']; + const infoCommands = ['md5sum', 'sha1sum', 'size', 'version']; + const dirCommands = ['mkdir', 'rmdir', 'rmdirs']; + const destructiveCommands = ['delete', 'purge']; + const specialSinglePathCommands = ['obscure']; + + // Get the command name from the command ID + // 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 + 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 + } + + console.log('Command ID:', this.commandId, 'Command Name:', commandName); + + // Check if the command requires a destination + if ( + listingCommands.includes(commandName) || + infoCommands.includes(commandName) || + (dirCommands.includes(commandName) && !this.rcloneFlags.includes('--dst')) || + destructiveCommands.includes(commandName) || + specialSinglePathCommands.includes(commandName) || + commandName === 'version' || + commandName === 'listremotes' + ) { + this.requiresDestination = false; + console.log('Destination not required for command:', commandName); + } else { + this.requiresDestination = true; + console.log('Destination required for command:', commandName); + } } }`, name, sourceType, sourcePath, sourceHost, sourcePort, sourceUser, sourcePassword, sourceKeyFile, sourceAuthType, @@ -275,171 +345,254 @@ func getInitialData(config *db.TransferConfig) string { destClientId, destClientSecret, destDriveId, destTeamDrive, destReadOnly, destStartYear, destIncludeArchived, useBuiltinAuthSource, useBuiltinAuthDest, - archivePath, archiveEnabled, deleteAfterTransfer, skipProcessedFiles, maxConcurrentTransfers, rcloneFlags) + archivePath, archiveEnabled, deleteAfterTransfer, skipProcessedFiles, maxConcurrentTransfers, rcloneFlags, + commandId, commandFlags) } templ ConfigForm(ctx context.Context, data ConfigFormData) { @LayoutWithContext(getConfigFormTitle(data.IsNew), ctx) { -
-
-
-
- + +
+
+
+ +
+
+ + + +
+

+ { getConfigFormTitle(data.IsNew) } +

+

+ Configure your file transfer settings using the form below +

+
+ + +
+ + +
+
+ +

Configuration Details

+
+

+ Give your configuration a descriptive name and set up the source and destination locations. +

+
+ + +
+ + +

Choose a descriptive name to identify this configuration.

+
+ + +
+

+ Command Configuration +

+ + + @common.RcloneFlags() +
+ + +
+

+ Source Configuration +

+ + + @common.SourceSelection() + + + + + + + + + + + + + + + + + + + + + +
+ + +
+

+ File Patterns +

+ + + @common.FilePatternFields() +
+ + +
+

+ Destination Configuration +

+ + + @common.DestinationSelection() + + + + + + + + + + + + + + + + + + + + + +
+ + +
+

+ Advanced Options +

+ + +
+

Archive Settings

+ @common.ArchiveOptions() +
+ + +
+ + +
+ + Cancel + + +
+
+
+ + +
+
+
+ +
+
+
Configuration Tips
+

+ Configure your file transfer settings carefully for optimal performance. Use file patterns to filter which files are transferred, and consider using archive options to keep track of processed files. +

+
+
- -

- { getConfigFormTitle(data.IsNew) } -

-

- Set up your file transfer configuration -

- -
- - - @common.NameField() - - - @common.SourceSelection() - - - - - - - - - - - - - - - - - - - - - - - - @common.FilePatternFields() - - - @common.DestinationSelection() - - - - - - - - - - - - - - - - - - - - - - - - @common.ArchiveOptions() - - - @common.RcloneFlags() - - -
- - Cancel - - -
-
-
- -
-

Configure your file transfer settings carefully for optimal performance

-
-
+ } } \ No newline at end of file diff --git a/components/configs.templ b/components/configs.templ index 2a4a3b3..5703089 100644 --- a/components/configs.templ +++ b/components/configs.templ @@ -6,39 +6,34 @@ import ( "github.com/starfleetcptn/gomft/internal/db" ) -// Dialog component for confirmation dialogs +// Dialog component for confirmation dialogs using Flowbite modal templ ConfigDialog(id string, title string, message string, confirmClass string, confirmText string, action string, configID uint, configName string) { -