mirror of
https://github.com/StarFleetCPTN/GoMFT.git
synced 2026-10-24 15:00:26 +02:00
initial commit
This commit is contained in:
commit
953ad364ab
68 files changed
+11440
No files matched your search
@@ -0,0 +1,34 @@
|
||||
package web
|
||||
|
||||
import (
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/starfleetcptn/gomft/internal/config"
|
||||
"github.com/starfleetcptn/gomft/internal/db"
|
||||
"github.com/starfleetcptn/gomft/internal/email"
|
||||
"github.com/starfleetcptn/gomft/internal/scheduler"
|
||||
"github.com/starfleetcptn/gomft/internal/web/handlers"
|
||||
)
|
||||
|
||||
// Handler is a wrapper around the handlers package
|
||||
type Handler struct {
|
||||
handlers *handlers.Handlers
|
||||
}
|
||||
|
||||
// NewHandler creates a new Handler instance that delegates to the handlers package
|
||||
func NewHandler(database *db.DB, scheduler *scheduler.Scheduler, jwtSecret string, dbPath string, backupDir string, cfg *config.Config) (*Handler, error) {
|
||||
// Create email service instance
|
||||
emailService := email.NewService(cfg)
|
||||
|
||||
// Create handlers instance
|
||||
handlersInstance := handlers.NewHandlers(database, scheduler, jwtSecret, dbPath, backupDir, emailService)
|
||||
|
||||
return &Handler{
|
||||
handlers: handlersInstance,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// InitializeRoutes delegates route registration to the handlers package
|
||||
func (h *Handler) InitializeRoutes(router *gin.Engine) {
|
||||
// Register all routes through the handlers package
|
||||
h.handlers.RegisterRoutes(router)
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// HandleBackupDB handles the POST /admin/backup route
|
||||
func (h *Handlers) HandleBackupDB(c *gin.Context) {
|
||||
// TODO: Implement database backup
|
||||
c.JSON(http.StatusOK, gin.H{"message": "Database backup initiated"})
|
||||
}
|
||||
@@ -0,0 +1,580 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/starfleetcptn/gomft/components"
|
||||
"github.com/starfleetcptn/gomft/internal/db"
|
||||
)
|
||||
|
||||
// HandleAdminTools displays the admin tools page
|
||||
func (h *Handlers) HandleAdminTools(c *gin.Context) {
|
||||
// Get system statistics
|
||||
data := components.AdminToolsData{
|
||||
SystemUptime: h.getSystemUptime(),
|
||||
DatabasePath: h.DBPath,
|
||||
BackupPath: h.BackupDir,
|
||||
}
|
||||
|
||||
// Get database size
|
||||
if dbSize, err := h.getDatabaseSize(); err == nil {
|
||||
data.DatabaseSize = dbSize
|
||||
} else {
|
||||
data.DatabaseSize = "Unknown"
|
||||
}
|
||||
|
||||
// Get job history count
|
||||
var jobHistoryCount int64
|
||||
if err := h.DB.Model(&db.JobHistory{}).Count(&jobHistoryCount).Error; err == nil {
|
||||
data.JobHistoryCount = int(jobHistoryCount)
|
||||
}
|
||||
|
||||
// Get active jobs count
|
||||
var activeJobs int64
|
||||
if err := h.DB.Model(&db.Job{}).Where("enabled = ?", true).Count(&activeJobs).Error; err == nil {
|
||||
data.ActiveJobs = int(activeJobs)
|
||||
}
|
||||
|
||||
// Get total configs count
|
||||
var totalConfigs int64
|
||||
if err := h.DB.Model(&db.TransferConfig{}).Count(&totalConfigs).Error; err == nil {
|
||||
data.TotalConfigs = int(totalConfigs)
|
||||
}
|
||||
|
||||
// Get total jobs count
|
||||
var totalJobs int64
|
||||
if err := h.DB.Model(&db.Job{}).Count(&totalJobs).Error; err == nil {
|
||||
data.TotalJobs = int(totalJobs)
|
||||
}
|
||||
|
||||
// Get total users count
|
||||
var totalUsers int64
|
||||
if err := h.DB.Model(&db.User{}).Count(&totalUsers).Error; err == nil {
|
||||
data.TotalUsers = int(totalUsers)
|
||||
}
|
||||
|
||||
// Get last backup time and backup count
|
||||
data.LastBackupTime, data.BackupCount = h.getBackupInfo()
|
||||
|
||||
// Get list of backup files
|
||||
data.BackupFiles = h.getBackupFiles()
|
||||
|
||||
// Check for maintenance issues
|
||||
data.MaintenanceMessage = h.checkMaintenanceIssues()
|
||||
|
||||
// Render the admin tools page
|
||||
components.AdminTools(components.CreateTemplateContext(c), data).Render(c, c.Writer)
|
||||
}
|
||||
|
||||
// HandleBackupDatabase handles the backup database request
|
||||
func (h *Handlers) HandleBackupDatabase(c *gin.Context) {
|
||||
fmt.Println("Backup database")
|
||||
// Create backup directory if it doesn't exist
|
||||
if err := os.MkdirAll(h.BackupDir, 0755); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("Failed to create backup directory: %v", err)})
|
||||
return
|
||||
}
|
||||
|
||||
// Create backup filename with timestamp
|
||||
timestamp := time.Now().Format("20060102_150405")
|
||||
backupFilename := filepath.Join(h.BackupDir, fmt.Sprintf("gomft_backup_%s.db", timestamp))
|
||||
|
||||
// Copy the database file to the backup location
|
||||
if err := h.copyDatabaseToBackup(backupFilename); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("Failed to create backup: %v", err)})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"message": "Database backup created successfully", "filename": backupFilename})
|
||||
}
|
||||
|
||||
// HandleRestoreDatabase handles the restore database request
|
||||
func (h *Handlers) HandleRestoreDatabase(c *gin.Context) {
|
||||
// Get the uploaded file
|
||||
file, err := c.FormFile("backup_file")
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "No backup file provided"})
|
||||
return
|
||||
}
|
||||
|
||||
// Create a temporary file to store the uploaded backup
|
||||
tempFile, err := os.CreateTemp("", "gomft_restore_*.db")
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("Failed to create temporary file: %v", err)})
|
||||
return
|
||||
}
|
||||
defer os.Remove(tempFile.Name())
|
||||
defer tempFile.Close()
|
||||
|
||||
// Save the uploaded file to the temporary location
|
||||
if err := c.SaveUploadedFile(file, tempFile.Name()); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("Failed to save uploaded file: %v", err)})
|
||||
return
|
||||
}
|
||||
|
||||
// Stop the scheduler to prevent jobs from running during restore
|
||||
h.Scheduler.Stop()
|
||||
|
||||
// Close the current database connection
|
||||
if err := h.DB.Close(); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("Failed to close database: %v", err)})
|
||||
return
|
||||
}
|
||||
|
||||
// Create a backup of the current database before restoring
|
||||
backupBeforeRestore := filepath.Join(h.BackupDir, fmt.Sprintf("pre_restore_backup_%s.db", time.Now().Format("20060102_150405")))
|
||||
if err := h.copyDatabaseToBackup(backupBeforeRestore); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("Failed to create pre-restore backup: %v", err)})
|
||||
return
|
||||
}
|
||||
|
||||
// Copy the temporary file to the database location
|
||||
if err := copyFile(tempFile.Name(), h.DBPath); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("Failed to restore database: %v", err)})
|
||||
return
|
||||
}
|
||||
|
||||
// Redirect to home page to reinitialize the application
|
||||
c.JSON(http.StatusOK, gin.H{"message": "Database restored successfully. The application will restart."})
|
||||
}
|
||||
|
||||
// HandleExportConfigs handles the export all configurations request
|
||||
func (h *Handlers) HandleExportConfigs(c *gin.Context) {
|
||||
// Get all configurations
|
||||
var configs []db.TransferConfig
|
||||
if err := h.DB.Find(&configs).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("Failed to retrieve configurations: %v", err)})
|
||||
return
|
||||
}
|
||||
|
||||
// Create a temporary file
|
||||
tmpFile, err := os.CreateTemp("", "gomft_configs_*.json")
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("Failed to create temporary file: %v", err)})
|
||||
return
|
||||
}
|
||||
defer os.Remove(tmpFile.Name()) // Clean up temp file when done
|
||||
defer tmpFile.Close()
|
||||
|
||||
// Write the configurations to the file
|
||||
encoder := json.NewEncoder(tmpFile)
|
||||
encoder.SetIndent("", " ") // Pretty print the JSON
|
||||
if err := encoder.Encode(configs); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("Failed to write configurations to file: %v", err)})
|
||||
return
|
||||
}
|
||||
|
||||
// Set headers for file download
|
||||
c.Header("Content-Description", "File Transfer")
|
||||
c.Header("Content-Transfer-Encoding", "binary")
|
||||
c.Header("Content-Disposition", fmt.Sprintf(`attachment; filename="gomft_configs_%s.json"`, time.Now().Format("20060102_150405")))
|
||||
c.Header("Content-Type", "application/json")
|
||||
|
||||
// Send the file
|
||||
c.File(tmpFile.Name())
|
||||
}
|
||||
|
||||
// HandleExportJobs handles the export all jobs request
|
||||
func (h *Handlers) HandleExportJobs(c *gin.Context) {
|
||||
// Get all jobs
|
||||
var jobs []db.Job
|
||||
if err := h.DB.Preload("Config").Find(&jobs).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("Failed to retrieve jobs: %v", err)})
|
||||
return
|
||||
}
|
||||
|
||||
// Create a temporary file
|
||||
tmpFile, err := os.CreateTemp("", "gomft_jobs_*.json")
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("Failed to create temporary file: %v", err)})
|
||||
return
|
||||
}
|
||||
defer os.Remove(tmpFile.Name()) // Clean up temp file when done
|
||||
defer tmpFile.Close()
|
||||
|
||||
// Write the jobs to the file
|
||||
encoder := json.NewEncoder(tmpFile)
|
||||
encoder.SetIndent("", " ") // Pretty print the JSON
|
||||
if err := encoder.Encode(jobs); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("Failed to write jobs to file: %v", err)})
|
||||
return
|
||||
}
|
||||
|
||||
// Set headers for file download
|
||||
c.Header("Content-Description", "File Transfer")
|
||||
c.Header("Content-Transfer-Encoding", "binary")
|
||||
c.Header("Content-Disposition", fmt.Sprintf(`attachment; filename="gomft_jobs_%s.json"`, time.Now().Format("20060102_150405")))
|
||||
c.Header("Content-Type", "application/json")
|
||||
|
||||
// Send the file
|
||||
c.File(tmpFile.Name())
|
||||
}
|
||||
|
||||
// HandleClearJobHistory handles the clear job history request
|
||||
func (h *Handlers) HandleClearJobHistory(c *gin.Context) {
|
||||
// Delete all job history records
|
||||
if err := h.DB.Exec("DELETE FROM job_histories").Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("Failed to clear job history: %v", err)})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"message": "Job history cleared successfully"})
|
||||
}
|
||||
|
||||
// HandleVacuumDatabase handles the vacuum database request
|
||||
func (h *Handlers) HandleVacuumDatabase(c *gin.Context) {
|
||||
// Execute VACUUM command to optimize the database
|
||||
if err := h.DB.Exec("VACUUM").Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("Failed to vacuum database: %v", err)})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"message": "Database vacuumed successfully"})
|
||||
}
|
||||
|
||||
// HandleRestoreDatabaseByFilename handles restoring a backup file from the backup directory
|
||||
func (h *Handlers) HandleRestoreDatabaseByFilename(c *gin.Context) {
|
||||
filename := c.Param("filename")
|
||||
|
||||
// Validate filename format
|
||||
if !strings.HasPrefix(filename, "gomft_backup_") || !strings.HasSuffix(filename, ".db") {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid backup filename"})
|
||||
return
|
||||
}
|
||||
|
||||
// Construct full file path
|
||||
backupPath := filepath.Join(h.BackupDir, filename)
|
||||
|
||||
// Check if file exists and is within backup directory
|
||||
if !strings.HasPrefix(backupPath, h.BackupDir) {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid backup path"})
|
||||
return
|
||||
}
|
||||
|
||||
// Check if file exists
|
||||
if _, err := os.Stat(backupPath); os.IsNotExist(err) {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "Backup file not found"})
|
||||
return
|
||||
}
|
||||
|
||||
// Stop the scheduler to prevent jobs from running during restore
|
||||
h.Scheduler.Stop()
|
||||
|
||||
// Close the current database connection
|
||||
if err := h.DB.Close(); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("Failed to close database: %v", err)})
|
||||
return
|
||||
}
|
||||
|
||||
// Create a backup of the current database before restoring
|
||||
backupBeforeRestore := filepath.Join(h.BackupDir, fmt.Sprintf("pre_restore_backup_%s.db", time.Now().Format("20060102_150405")))
|
||||
if err := h.copyDatabaseToBackup(backupBeforeRestore); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("Failed to create pre-restore backup: %v", err)})
|
||||
return
|
||||
}
|
||||
|
||||
// Copy the backup file to the database location
|
||||
if err := copyFile(backupPath, h.DBPath); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("Failed to restore database: %v", err)})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"message": "Database restored successfully. The application will restart."})
|
||||
}
|
||||
|
||||
// HandleRefreshBackups handles the HTMX request to refresh the backups list
|
||||
func (h *Handlers) HandleRefreshBackups(c *gin.Context) {
|
||||
// Get list of backup files
|
||||
backupFiles := h.getBackupFiles()
|
||||
|
||||
// Create data structure for the template
|
||||
data := components.AdminToolsData{
|
||||
BackupFiles: backupFiles,
|
||||
}
|
||||
|
||||
// Get last backup time and backup count
|
||||
data.LastBackupTime, data.BackupCount = h.getBackupInfo()
|
||||
|
||||
// Render just the BackupsList component
|
||||
components.BackupsList(data).Render(c, c.Writer)
|
||||
}
|
||||
|
||||
// Helper functions
|
||||
|
||||
// getSystemUptime returns the system uptime as a formatted string
|
||||
func (h *Handlers) getSystemUptime() string {
|
||||
uptime := time.Since(h.StartTime)
|
||||
days := int(uptime.Hours() / 24)
|
||||
hours := int(uptime.Hours()) % 24
|
||||
minutes := int(uptime.Minutes()) % 60
|
||||
|
||||
if days > 0 {
|
||||
return fmt.Sprintf("%d days, %d hours, %d minutes", days, hours, minutes)
|
||||
}
|
||||
if hours > 0 {
|
||||
return fmt.Sprintf("%d hours, %d minutes", hours, minutes)
|
||||
}
|
||||
return fmt.Sprintf("%d minutes", minutes)
|
||||
}
|
||||
|
||||
// getDatabaseSize returns the size of the database file as a formatted string
|
||||
func (h *Handlers) getDatabaseSize() (string, error) {
|
||||
fileInfo, err := os.Stat(h.DBPath)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
sizeBytes := fileInfo.Size()
|
||||
|
||||
// Format size
|
||||
if sizeBytes < 1024 {
|
||||
return fmt.Sprintf("%d B", sizeBytes), nil
|
||||
} else if sizeBytes < 1024*1024 {
|
||||
return fmt.Sprintf("%.2f KB", float64(sizeBytes)/1024), nil
|
||||
} else if sizeBytes < 1024*1024*1024 {
|
||||
return fmt.Sprintf("%.2f MB", float64(sizeBytes)/(1024*1024)), nil
|
||||
}
|
||||
return fmt.Sprintf("%.2f GB", float64(sizeBytes)/(1024*1024*1024)), nil
|
||||
}
|
||||
|
||||
// getBackupInfo returns the last backup time and total backup count
|
||||
func (h *Handlers) getBackupInfo() (*time.Time, int) {
|
||||
// Check if backup directory exists
|
||||
if _, err := os.Stat(h.BackupDir); os.IsNotExist(err) {
|
||||
return nil, 0
|
||||
}
|
||||
|
||||
// List all backup files
|
||||
backupFiles, err := filepath.Glob(filepath.Join(h.BackupDir, "gomft_backup_*.db"))
|
||||
if err != nil {
|
||||
return nil, 0
|
||||
}
|
||||
|
||||
if len(backupFiles) == 0 {
|
||||
return nil, 0
|
||||
}
|
||||
|
||||
// Find the most recent backup
|
||||
var lastBackupTime time.Time
|
||||
var lastBackupFile string
|
||||
|
||||
for _, file := range backupFiles {
|
||||
filename := filepath.Base(file)
|
||||
// Extract timestamp from filename (format: gomft_backup_20060102_150405.db)
|
||||
if len(filename) < 28 {
|
||||
continue
|
||||
}
|
||||
|
||||
timestampStr := filename[13:28]
|
||||
timestamp, err := time.Parse("20060102_150405", timestampStr)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
if timestamp.After(lastBackupTime) {
|
||||
lastBackupTime = timestamp
|
||||
lastBackupFile = file
|
||||
}
|
||||
}
|
||||
|
||||
if lastBackupFile == "" {
|
||||
return nil, len(backupFiles)
|
||||
}
|
||||
|
||||
return &lastBackupTime, len(backupFiles)
|
||||
}
|
||||
|
||||
// checkMaintenanceIssues checks for potential maintenance issues
|
||||
func (h *Handlers) checkMaintenanceIssues() string {
|
||||
var issues []string
|
||||
|
||||
// Check database size
|
||||
fileInfo, err := os.Stat(h.DBPath)
|
||||
if err == nil {
|
||||
sizeBytes := fileInfo.Size()
|
||||
// If database is larger than 100MB, suggest vacuum
|
||||
if sizeBytes > 100*1024*1024 {
|
||||
issues = append(issues, "Database size is large (>100MB). Consider running vacuum to optimize.")
|
||||
}
|
||||
}
|
||||
|
||||
// Check job history count
|
||||
var jobHistoryCount int64
|
||||
if err := h.DB.Model(&db.JobHistory{}).Count(&jobHistoryCount).Error; err == nil {
|
||||
// If more than 1000 job history records, suggest clearing old records
|
||||
if jobHistoryCount > 1000 {
|
||||
issues = append(issues, fmt.Sprintf("Job history contains %d records. Consider clearing old records.", jobHistoryCount))
|
||||
}
|
||||
}
|
||||
|
||||
// Check backup age
|
||||
lastBackupTime, _ := h.getBackupInfo()
|
||||
if lastBackupTime == nil {
|
||||
issues = append(issues, "No database backups found. Consider creating a backup.")
|
||||
} else {
|
||||
// If last backup is older than 7 days, suggest creating a new backup
|
||||
if time.Since(*lastBackupTime) > 7*24*time.Hour {
|
||||
issues = append(issues, fmt.Sprintf("Last backup is %d days old. Consider creating a new backup.", int(time.Since(*lastBackupTime).Hours()/24)))
|
||||
}
|
||||
}
|
||||
|
||||
// Join all issues with newlines
|
||||
if len(issues) > 0 {
|
||||
return fmt.Sprintf("Maintenance Recommendations:\n%s", strings.Join(issues, "\n"))
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
// copyDatabaseToBackup copies the database file to the specified backup location
|
||||
func (h *Handlers) copyDatabaseToBackup(backupPath string) error {
|
||||
return copyFile(h.DBPath, backupPath)
|
||||
}
|
||||
|
||||
// copyFile copies a file from src to dst
|
||||
func copyFile(src, dst string) error {
|
||||
sourceFile, err := os.Open(src)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer sourceFile.Close()
|
||||
|
||||
destFile, err := os.Create(dst)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer destFile.Close()
|
||||
|
||||
_, err = io.Copy(destFile, sourceFile)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return destFile.Sync()
|
||||
}
|
||||
|
||||
// getBackupFiles returns a list of backup files with their details
|
||||
func (h *Handlers) getBackupFiles() []components.BackupFile {
|
||||
var backupFiles []components.BackupFile
|
||||
|
||||
// Check if backup directory exists
|
||||
if _, err := os.Stat(h.BackupDir); os.IsNotExist(err) {
|
||||
return backupFiles
|
||||
}
|
||||
|
||||
// List all backup files
|
||||
files, err := filepath.Glob(filepath.Join(h.BackupDir, "gomft_backup_*.db"))
|
||||
if err != nil {
|
||||
return backupFiles
|
||||
}
|
||||
|
||||
for _, file := range files {
|
||||
fileInfo, err := os.Stat(file)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
// Format file size
|
||||
var sizeStr string
|
||||
size := fileInfo.Size()
|
||||
switch {
|
||||
case size < 1024:
|
||||
sizeStr = fmt.Sprintf("%d B", size)
|
||||
case size < 1024*1024:
|
||||
sizeStr = fmt.Sprintf("%.2f KB", float64(size)/1024)
|
||||
case size < 1024*1024*1024:
|
||||
sizeStr = fmt.Sprintf("%.2f MB", float64(size)/(1024*1024))
|
||||
default:
|
||||
sizeStr = fmt.Sprintf("%.2f GB", float64(size)/(1024*1024*1024))
|
||||
}
|
||||
|
||||
backupFiles = append(backupFiles, components.BackupFile{
|
||||
Name: filepath.Base(file),
|
||||
Size: sizeStr,
|
||||
ModTime: fileInfo.ModTime(),
|
||||
})
|
||||
}
|
||||
|
||||
// Sort backups by modification time, newest first
|
||||
sort.Slice(backupFiles, func(i, j int) bool {
|
||||
return backupFiles[i].ModTime.After(backupFiles[j].ModTime)
|
||||
})
|
||||
|
||||
return backupFiles
|
||||
}
|
||||
|
||||
// HandleDeleteBackup handles the DELETE /admin/delete-backup/:filename route
|
||||
func (h *Handlers) HandleDeleteBackup(c *gin.Context) {
|
||||
filename := c.Param("filename")
|
||||
|
||||
// Validate filename format
|
||||
if !strings.HasPrefix(filename, "gomft_backup_") || !strings.HasSuffix(filename, ".db") {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid backup filename"})
|
||||
return
|
||||
}
|
||||
|
||||
// Construct full file path
|
||||
filePath := filepath.Join(h.BackupDir, filename)
|
||||
|
||||
// Check if file exists and is within backup directory
|
||||
if !strings.HasPrefix(filePath, h.BackupDir) {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid backup path"})
|
||||
return
|
||||
}
|
||||
|
||||
// Delete the file
|
||||
if err := os.Remove(filePath); err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "Backup file not found"})
|
||||
} else {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("Failed to delete backup: %v", err)})
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"message": "Backup deleted successfully"})
|
||||
}
|
||||
|
||||
// HandleDownloadBackup handles the GET /admin/download-backup/:filename route
|
||||
func (h *Handlers) HandleDownloadBackup(c *gin.Context) {
|
||||
filename := c.Param("filename")
|
||||
|
||||
// Validate filename format
|
||||
if !strings.HasPrefix(filename, "gomft_backup_") || !strings.HasSuffix(filename, ".db") {
|
||||
c.String(http.StatusBadRequest, "Invalid backup filename")
|
||||
return
|
||||
}
|
||||
|
||||
// Construct full file path
|
||||
filePath := filepath.Join(h.BackupDir, filename)
|
||||
|
||||
// Check if file exists and is within backup directory
|
||||
if !strings.HasPrefix(filePath, h.BackupDir) {
|
||||
c.String(http.StatusBadRequest, "Invalid backup path")
|
||||
return
|
||||
}
|
||||
|
||||
// Check if file exists
|
||||
if _, err := os.Stat(filePath); os.IsNotExist(err) {
|
||||
c.String(http.StatusNotFound, "Backup file not found")
|
||||
return
|
||||
}
|
||||
|
||||
// Set headers for file download
|
||||
c.Header("Content-Description", "File Transfer")
|
||||
c.Header("Content-Transfer-Encoding", "binary")
|
||||
c.Header("Content-Disposition", fmt.Sprintf("attachment; filename=%s", filename))
|
||||
c.Header("Content-Type", "application/octet-stream")
|
||||
|
||||
// Serve the file
|
||||
c.File(filePath)
|
||||
}
|
||||
@@ -0,0 +1,295 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/starfleetcptn/gomft/internal/db"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
)
|
||||
|
||||
// HandleAPILogin handles the POST /api/login route
|
||||
func (h *Handlers) HandleAPILogin(c *gin.Context) {
|
||||
var loginData struct {
|
||||
Email string `json:"email"`
|
||||
Password string `json:"password"`
|
||||
}
|
||||
|
||||
if err := c.ShouldBindJSON(&loginData); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request data"})
|
||||
return
|
||||
}
|
||||
|
||||
// Get user by email
|
||||
var user db.User
|
||||
if err := h.DB.Where("email = ?", loginData.Email).First(&user).Error; err != nil {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid credentials"})
|
||||
return
|
||||
}
|
||||
|
||||
// Check password
|
||||
if err := bcrypt.CompareHashAndPassword([]byte(user.PasswordHash), []byte(loginData.Password)); err != nil {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid credentials"})
|
||||
return
|
||||
}
|
||||
|
||||
// Generate JWT token
|
||||
token, err := h.GenerateJWT(user.ID, user.Email, user.IsAdmin)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to generate token"})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"token": token,
|
||||
"user": gin.H{
|
||||
"id": user.ID,
|
||||
"email": user.Email,
|
||||
"is_admin": user.IsAdmin,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// HandleAPIConfigs handles the GET /api/configs route
|
||||
func (h *Handlers) HandleAPIConfigs(c *gin.Context) {
|
||||
userID := c.GetUint("userID")
|
||||
|
||||
var configs []db.TransferConfig
|
||||
h.DB.Where("created_by = ?", userID).Find(&configs)
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"configs": configs})
|
||||
}
|
||||
|
||||
// HandleAPIConfig handles the GET /api/configs/:id route
|
||||
func (h *Handlers) HandleAPIConfig(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
userID := c.GetUint("userID")
|
||||
|
||||
var config db.TransferConfig
|
||||
if err := h.DB.First(&config, id).Error; err != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "Config not found"})
|
||||
return
|
||||
}
|
||||
|
||||
// Check if user owns this config
|
||||
if config.CreatedBy != userID {
|
||||
// Check if user is admin
|
||||
isAdmin, exists := c.Get("isAdmin")
|
||||
if !exists || isAdmin != true {
|
||||
c.JSON(http.StatusForbidden, gin.H{"error": "You do not have permission to view this config"})
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"config": config})
|
||||
}
|
||||
|
||||
// HandleAPICreateConfig handles the POST /api/configs route
|
||||
func (h *Handlers) HandleAPICreateConfig(c *gin.Context) {
|
||||
var config db.TransferConfig
|
||||
if err := c.ShouldBindJSON(&config); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": fmt.Sprintf("Invalid request data: %v", err)})
|
||||
return
|
||||
}
|
||||
|
||||
userID := c.GetUint("userID")
|
||||
config.CreatedBy = userID
|
||||
|
||||
if err := h.DB.Create(&config).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("Failed to create config: %v", err)})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusCreated, gin.H{"config": config})
|
||||
}
|
||||
|
||||
// HandleAPIUpdateConfig handles the PUT /api/configs/:id route
|
||||
func (h *Handlers) HandleAPIUpdateConfig(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
userID := c.GetUint("userID")
|
||||
|
||||
var config db.TransferConfig
|
||||
if err := h.DB.First(&config, id).Error; err != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "Config not found"})
|
||||
return
|
||||
}
|
||||
|
||||
// Check if user owns this config
|
||||
if config.CreatedBy != userID {
|
||||
// Check if user is admin
|
||||
isAdmin, exists := c.Get("isAdmin")
|
||||
if !exists || isAdmin != true {
|
||||
c.JSON(http.StatusForbidden, gin.H{"error": "You do not have permission to update this config"})
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Get the old config values for comparison
|
||||
oldConfig := config
|
||||
|
||||
// Bind JSON data to config
|
||||
if err := c.ShouldBindJSON(&config); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": fmt.Sprintf("Invalid request data: %v", err)})
|
||||
return
|
||||
}
|
||||
|
||||
// Preserve fields that shouldn't be updated
|
||||
config.CreatedBy = oldConfig.CreatedBy
|
||||
|
||||
if err := h.DB.Save(&config).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("Failed to update config: %v", err)})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"config": config})
|
||||
}
|
||||
|
||||
// HandleAPIDeleteConfig handles the DELETE /api/configs/:id route
|
||||
func (h *Handlers) HandleAPIDeleteConfig(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
userID := c.GetUint("userID")
|
||||
|
||||
var config db.TransferConfig
|
||||
if err := h.DB.First(&config, id).Error; err != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "Config not found"})
|
||||
return
|
||||
}
|
||||
|
||||
// Check if user owns this config
|
||||
if config.CreatedBy != userID {
|
||||
// Check if user is admin
|
||||
isAdmin, exists := c.Get("isAdmin")
|
||||
if !exists || isAdmin != true {
|
||||
c.JSON(http.StatusForbidden, gin.H{"error": "You do not have permission to delete this config"})
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Check if config is in use by any jobs
|
||||
var jobCount int64
|
||||
h.DB.Model(&db.Job{}).Where("config_id = ?", config.ID).Count(&jobCount)
|
||||
if jobCount > 0 {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Config is in use by jobs and cannot be deleted"})
|
||||
return
|
||||
}
|
||||
|
||||
// Delete config
|
||||
if err := h.DB.Delete(&config).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("Failed to delete config: %v", err)})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"message": "Config deleted successfully"})
|
||||
}
|
||||
|
||||
// HandleAPITestConnection handles the POST /api/configs/test route
|
||||
func (h *Handlers) HandleAPITestConnection(c *gin.Context) {
|
||||
var config db.TransferConfig
|
||||
if err := c.ShouldBindJSON(&config); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": fmt.Sprintf("Invalid request data: %v", err)})
|
||||
return
|
||||
}
|
||||
|
||||
// TODO: Implement connection testing based on protocol
|
||||
// This is a placeholder for the actual connection testing logic
|
||||
success := true
|
||||
message := "Connection successful"
|
||||
|
||||
// Example of how connection testing might work
|
||||
switch config.SourceType {
|
||||
case "sftp":
|
||||
// Test SFTP connection
|
||||
// success, message = testSFTPConnection(config)
|
||||
case "ftp":
|
||||
// Test FTP connection
|
||||
// success, message = testFTPConnection(config)
|
||||
default:
|
||||
success = false
|
||||
message = "Unsupported source type"
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": success,
|
||||
"message": message,
|
||||
})
|
||||
}
|
||||
|
||||
// HandleAPIJobs handles the API jobs request
|
||||
func (h *Handlers) HandleAPIJobs(c *gin.Context) {
|
||||
// Implementation will be moved from the old handlers.go
|
||||
c.JSON(http.StatusOK, gin.H{"message": "API jobs handler stub"})
|
||||
}
|
||||
|
||||
// HandleAPIJob handles the API job request
|
||||
func (h *Handlers) HandleAPIJob(c *gin.Context) {
|
||||
// Implementation will be moved from the old handlers.go
|
||||
c.JSON(http.StatusOK, gin.H{"message": "API job handler stub"})
|
||||
}
|
||||
|
||||
// HandleAPICreateJob handles the API create job request
|
||||
func (h *Handlers) HandleAPICreateJob(c *gin.Context) {
|
||||
// Implementation will be moved from the old handlers.go
|
||||
c.JSON(http.StatusOK, gin.H{"message": "API create job handler stub"})
|
||||
}
|
||||
|
||||
// HandleAPIUpdateJob handles the API update job request
|
||||
func (h *Handlers) HandleAPIUpdateJob(c *gin.Context) {
|
||||
// Implementation will be moved from the old handlers.go
|
||||
c.JSON(http.StatusOK, gin.H{"message": "API update job handler stub"})
|
||||
}
|
||||
|
||||
// HandleAPIDeleteJob handles the API delete job request
|
||||
func (h *Handlers) HandleAPIDeleteJob(c *gin.Context) {
|
||||
// Implementation will be moved from the old handlers.go
|
||||
c.JSON(http.StatusOK, gin.H{"message": "API delete job handler stub"})
|
||||
}
|
||||
|
||||
// HandleAPIRunJob handles the API run job request
|
||||
func (h *Handlers) HandleAPIRunJob(c *gin.Context) {
|
||||
// Implementation will be moved from the old handlers.go
|
||||
c.JSON(http.StatusOK, gin.H{"message": "API run job handler stub"})
|
||||
}
|
||||
|
||||
// HandleAPIHistory handles the API history request
|
||||
func (h *Handlers) HandleAPIHistory(c *gin.Context) {
|
||||
// Implementation will be moved from the old handlers.go
|
||||
c.JSON(http.StatusOK, gin.H{"message": "API history handler stub"})
|
||||
}
|
||||
|
||||
// HandleAPIJobRun handles the API job run request
|
||||
func (h *Handlers) HandleAPIJobRun(c *gin.Context) {
|
||||
// Implementation will be moved from the old handlers.go
|
||||
c.JSON(http.StatusOK, gin.H{"message": "API job run handler stub"})
|
||||
}
|
||||
|
||||
// HandleAPIUsers handles the API users request
|
||||
func (h *Handlers) HandleAPIUsers(c *gin.Context) {
|
||||
// Implementation will be moved from the old handlers.go
|
||||
c.JSON(http.StatusOK, gin.H{"message": "API users handler stub"})
|
||||
}
|
||||
|
||||
// HandleAPIUser handles the API user request
|
||||
func (h *Handlers) HandleAPIUser(c *gin.Context) {
|
||||
// Implementation will be moved from the old handlers.go
|
||||
c.JSON(http.StatusOK, gin.H{"message": "API user handler stub"})
|
||||
}
|
||||
|
||||
// HandleAPICreateUser handles the API create user request
|
||||
func (h *Handlers) HandleAPICreateUser(c *gin.Context) {
|
||||
// Implementation will be moved from the old handlers.go
|
||||
c.JSON(http.StatusOK, gin.H{"message": "API create user handler stub"})
|
||||
}
|
||||
|
||||
// HandleAPIUpdateUser handles the API update user request
|
||||
func (h *Handlers) HandleAPIUpdateUser(c *gin.Context) {
|
||||
// Implementation will be moved from the old handlers.go
|
||||
c.JSON(http.StatusOK, gin.H{"message": "API update user handler stub"})
|
||||
}
|
||||
|
||||
// HandleAPIDeleteUser handles the API delete user request
|
||||
func (h *Handlers) HandleAPIDeleteUser(c *gin.Context) {
|
||||
// Implementation will be moved from the old handlers.go
|
||||
c.JSON(http.StatusOK, gin.H{"message": "API delete user handler stub"})
|
||||
}
|
||||
@@ -0,0 +1,554 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"encoding/base64"
|
||||
"log"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/golang-jwt/jwt/v5"
|
||||
"github.com/starfleetcptn/gomft/components"
|
||||
"github.com/starfleetcptn/gomft/internal/auth"
|
||||
"github.com/starfleetcptn/gomft/internal/db"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
)
|
||||
|
||||
// AuthMiddleware is a middleware function that checks if the user is authenticated
|
||||
func (h *Handlers) AuthMiddleware() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
// Get the JWT token from the cookie
|
||||
tokenString, err := c.Cookie("jwt_token")
|
||||
if err != nil {
|
||||
c.Redirect(http.StatusFound, "/login")
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
// Parse and validate the token
|
||||
token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
|
||||
return []byte(h.JWTSecret), nil
|
||||
})
|
||||
|
||||
if err != nil || !token.Valid {
|
||||
c.Redirect(http.StatusFound, "/login")
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
// Extract claims
|
||||
claims, ok := token.Claims.(jwt.MapClaims)
|
||||
if !ok {
|
||||
c.Redirect(http.StatusFound, "/login")
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
// Set user information in the context
|
||||
c.Set("userID", uint(claims["user_id"].(float64)))
|
||||
c.Set("email", claims["email"].(string))
|
||||
c.Set("username", claims["username"].(string))
|
||||
c.Set("isAdmin", claims["is_admin"].(bool))
|
||||
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
|
||||
// AdminMiddleware is a middleware function that checks if the user is an admin
|
||||
func (h *Handlers) AdminMiddleware() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
isAdmin, exists := c.Get("isAdmin")
|
||||
if !exists || !isAdmin.(bool) {
|
||||
c.Redirect(http.StatusFound, "/dashboard")
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
|
||||
// APIAuthMiddleware is a middleware function that checks if the API request is authenticated
|
||||
func (h *Handlers) APIAuthMiddleware() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
// Get the Authorization header
|
||||
authHeader := c.GetHeader("Authorization")
|
||||
if authHeader == "" {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": "Authorization header is required"})
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
// Check if the header is in the correct format
|
||||
parts := strings.Split(authHeader, " ")
|
||||
if len(parts) != 2 || parts[0] != "Bearer" {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": "Authorization header format must be Bearer {token}"})
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
// Parse and validate the token
|
||||
tokenString := parts[1]
|
||||
token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
|
||||
return []byte(h.JWTSecret), nil
|
||||
})
|
||||
|
||||
if err != nil || !token.Valid {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid or expired token"})
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
// Extract claims
|
||||
claims, ok := token.Claims.(jwt.MapClaims)
|
||||
if !ok {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid token claims"})
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
// Set user information in the context
|
||||
c.Set("userID", uint(claims["user_id"].(float64)))
|
||||
c.Set("email", claims["email"].(string))
|
||||
c.Set("username", claims["username"].(string))
|
||||
c.Set("isAdmin", claims["is_admin"].(bool))
|
||||
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
|
||||
// APIAdminMiddleware is a middleware function that checks if the API request is from an admin
|
||||
func (h *Handlers) APIAdminMiddleware() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
isAdmin, exists := c.Get("isAdmin")
|
||||
if !exists || !isAdmin.(bool) {
|
||||
c.JSON(http.StatusForbidden, gin.H{"error": "Admin privileges required"})
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
|
||||
// GenerateJWT generates a JWT token for the given user
|
||||
func (h *Handlers) GenerateJWT(userID uint, username string, isAdmin bool) (string, error) {
|
||||
token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
|
||||
"user_id": userID,
|
||||
"username": username,
|
||||
"is_admin": isAdmin,
|
||||
"exp": time.Now().Add(time.Hour * 24).Unix(),
|
||||
})
|
||||
|
||||
return token.SignedString([]byte(h.JWTSecret))
|
||||
}
|
||||
|
||||
// HandleLoginPage handles the GET /login route
|
||||
func (h *Handlers) HandleLoginPage(c *gin.Context) {
|
||||
// Check if user is already logged in
|
||||
if userID, exists := c.Get("userID"); exists && userID != nil {
|
||||
// User is logged in, redirect to dashboard
|
||||
c.Redirect(http.StatusFound, "/dashboard")
|
||||
return
|
||||
}
|
||||
|
||||
// Create template context and set email if available
|
||||
ctx := components.CreateTemplateContext(c)
|
||||
if email, exists := c.Get("email"); exists {
|
||||
ctx = context.WithValue(ctx, "email", email)
|
||||
}
|
||||
|
||||
// Check for message query param (used for password expired, etc.)
|
||||
message := c.Query("message")
|
||||
|
||||
// User is not logged in, show login page
|
||||
if message != "" {
|
||||
components.Login(ctx, message).Render(c.Request.Context(), c.Writer)
|
||||
} else {
|
||||
components.Login(ctx, "").Render(c.Request.Context(), c.Writer)
|
||||
}
|
||||
}
|
||||
|
||||
// HandleLogin handles the POST /login route
|
||||
func (h *Handlers) HandleLogin(c *gin.Context) {
|
||||
email := c.PostForm("email")
|
||||
password := c.PostForm("password")
|
||||
|
||||
// Get user by email
|
||||
var user db.User
|
||||
if err := h.DB.Where("email = ?", email).First(&user).Error; err != nil {
|
||||
components.Login(components.CreateTemplateContext(c), "Invalid credentials").Render(c, c.Writer)
|
||||
return
|
||||
}
|
||||
|
||||
// Check if account is locked
|
||||
if user.AccountLocked {
|
||||
if user.LockoutUntil != nil && time.Now().After(*user.LockoutUntil) {
|
||||
// Lockout period has expired, reset the lockout
|
||||
user.AccountLocked = false
|
||||
user.FailedLoginAttempts = 0
|
||||
user.LockoutUntil = nil
|
||||
h.DB.Save(&user)
|
||||
} else {
|
||||
// Account is still locked
|
||||
components.Login(components.CreateTemplateContext(c), "Account is locked due to too many failed login attempts. Please try again later.").Render(c, c.Writer)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Check password
|
||||
if err := bcrypt.CompareHashAndPassword([]byte(user.PasswordHash), []byte(password)); err != nil {
|
||||
// Increment failed login attempts
|
||||
user.FailedLoginAttempts++
|
||||
|
||||
// Check if we need to lock the account
|
||||
policy := auth.DefaultPasswordPolicy()
|
||||
if user.FailedLoginAttempts >= policy.MaxLoginAttempts {
|
||||
user.AccountLocked = true
|
||||
lockoutTime := time.Now().Add(policy.LockoutDuration)
|
||||
user.LockoutUntil = &lockoutTime
|
||||
h.DB.Save(&user)
|
||||
components.Login(components.CreateTemplateContext(c), "Account is locked due to too many failed login attempts. Please try again later.").Render(c, c.Writer)
|
||||
return
|
||||
}
|
||||
|
||||
h.DB.Save(&user)
|
||||
components.Login(components.CreateTemplateContext(c), "Invalid credentials").Render(c, c.Writer)
|
||||
return
|
||||
}
|
||||
|
||||
// Reset failed login attempts on successful login
|
||||
user.FailedLoginAttempts = 0
|
||||
user.AccountLocked = false
|
||||
user.LockoutUntil = nil
|
||||
h.DB.Save(&user)
|
||||
|
||||
// Check password expiration
|
||||
policy := auth.DefaultPasswordPolicy()
|
||||
if auth.IsPasswordExpired(user.LastPasswordChange, policy) {
|
||||
// Add flash message about password expiration
|
||||
// We're simplifying by just redirecting to login with a message
|
||||
c.SetCookie("jwt_token", "", -1, "/", "", false, true) // Logout the user
|
||||
c.Redirect(http.StatusFound, "/login?message=Your+password+has+expired.+Please+contact+an+administrator.")
|
||||
return
|
||||
}
|
||||
|
||||
// Generate JWT token with all necessary user information
|
||||
token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
|
||||
"user_id": user.ID,
|
||||
"email": user.Email,
|
||||
"username": strings.Split(user.Email, "@")[0], // Use email prefix as username
|
||||
"is_admin": user.IsAdmin,
|
||||
"exp": time.Now().Add(time.Hour * 24).Unix(),
|
||||
})
|
||||
|
||||
// Sign the token
|
||||
tokenString, err := token.SignedString([]byte(h.JWTSecret))
|
||||
if err != nil {
|
||||
components.Login(components.CreateTemplateContext(c), "Authentication error").Render(c, c.Writer)
|
||||
return
|
||||
}
|
||||
|
||||
// Set token in cookie
|
||||
c.SetCookie("jwt_token", tokenString, 86400, "/", "", false, true)
|
||||
c.Redirect(http.StatusFound, "/dashboard")
|
||||
}
|
||||
|
||||
// HandleLogout handles the POST /logout route
|
||||
func (h *Handlers) HandleLogout(c *gin.Context) {
|
||||
c.SetCookie("jwt_token", "", -1, "/", "", false, true)
|
||||
c.Redirect(http.StatusFound, "/login")
|
||||
}
|
||||
|
||||
// HandleChangePassword handles the POST /change-password route
|
||||
// This is now only for use from the profile page
|
||||
func (h *Handlers) HandleChangePassword(c *gin.Context) {
|
||||
// Get user ID from token
|
||||
tokenCookie, err := c.Cookie("jwt_token")
|
||||
if err != nil || tokenCookie == "" {
|
||||
if c.GetHeader("HX-Request") == "true" {
|
||||
c.Data(http.StatusUnauthorized, "text/html", []byte(`<div class="bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded mb-4" role="alert">
|
||||
<span class="block sm:inline">Authentication required</span>
|
||||
</div>`))
|
||||
return
|
||||
}
|
||||
c.Redirect(http.StatusFound, "/login")
|
||||
return
|
||||
}
|
||||
|
||||
claims, err := auth.ValidateToken(tokenCookie, h.JWTSecret)
|
||||
if err != nil {
|
||||
if c.GetHeader("HX-Request") == "true" {
|
||||
c.Data(http.StatusUnauthorized, "text/html", []byte(`<div class="bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded mb-4" role="alert">
|
||||
<span class="block sm:inline">Invalid authentication</span>
|
||||
</div>`))
|
||||
return
|
||||
}
|
||||
c.SetCookie("jwt_token", "", -1, "/", "", false, true)
|
||||
c.Redirect(http.StatusFound, "/login")
|
||||
return
|
||||
}
|
||||
userID := claims.UserID
|
||||
|
||||
// Get form values
|
||||
currentPassword := c.PostForm("current_password")
|
||||
newPassword := c.PostForm("new_password")
|
||||
confirmPassword := c.PostForm("confirm_password")
|
||||
|
||||
// Validate new password matches confirmation
|
||||
if newPassword != confirmPassword {
|
||||
c.Data(http.StatusOK, "text/html", []byte(`<div class="bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded mb-4" role="alert">
|
||||
<span class="block sm:inline">New password and confirmation do not match</span>
|
||||
</div>`))
|
||||
return
|
||||
}
|
||||
|
||||
// Get user
|
||||
var user db.User
|
||||
if err := h.DB.First(&user, userID).Error; err != nil {
|
||||
c.Data(http.StatusOK, "text/html", []byte(`<div class="bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded mb-4" role="alert">
|
||||
<span class="block sm:inline">User not found</span>
|
||||
</div>`))
|
||||
return
|
||||
}
|
||||
|
||||
// Verify current password
|
||||
if bcrypt.CompareHashAndPassword([]byte(user.PasswordHash), []byte(currentPassword)) != nil {
|
||||
c.Data(http.StatusOK, "text/html", []byte(`<div class="bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded mb-4" role="alert">
|
||||
<span class="block sm:inline">Current password is incorrect</span>
|
||||
</div>`))
|
||||
return
|
||||
}
|
||||
|
||||
// Validate password against policy
|
||||
policy := auth.DefaultPasswordPolicy()
|
||||
if err := auth.ValidatePassword(newPassword, policy); err != nil {
|
||||
errorMsg := `<div class="bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded mb-4" role="alert">
|
||||
<span class="block sm:inline">` + err.Error() + `</span>
|
||||
</div>`
|
||||
c.Data(http.StatusOK, "text/html", []byte(errorMsg))
|
||||
return
|
||||
}
|
||||
|
||||
// Check password history
|
||||
if err := auth.CheckPasswordHistory(user.ID, newPassword, user.PasswordHash, h.DB.DB, policy); err != nil {
|
||||
errorMsg := `<div class="bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded mb-4" role="alert">
|
||||
<span class="block sm:inline">` + err.Error() + `</span>
|
||||
</div>`
|
||||
c.Data(http.StatusOK, "text/html", []byte(errorMsg))
|
||||
return
|
||||
}
|
||||
|
||||
// Hash the new password
|
||||
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(newPassword), bcrypt.DefaultCost)
|
||||
if err != nil {
|
||||
c.Data(http.StatusOK, "text/html", []byte(`<div class="bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded mb-4" role="alert">
|
||||
<span class="block sm:inline">Error processing password</span>
|
||||
</div>`))
|
||||
return
|
||||
}
|
||||
|
||||
// Update password history
|
||||
if err := auth.UpdatePasswordHistory(user.ID, string(hashedPassword), h.DB.DB, policy); err != nil {
|
||||
c.Data(http.StatusOK, "text/html", []byte(`<div class="bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded mb-4" role="alert">
|
||||
<span class="block sm:inline">Error updating password history</span>
|
||||
</div>`))
|
||||
return
|
||||
}
|
||||
|
||||
// Update user's password
|
||||
user.PasswordHash = string(hashedPassword)
|
||||
user.LastPasswordChange = time.Now()
|
||||
if err := h.DB.Save(&user).Error; err != nil {
|
||||
c.Data(http.StatusOK, "text/html", []byte(`<div class="bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded mb-4" role="alert">
|
||||
<span class="block sm:inline">Error updating password</span>
|
||||
</div>`))
|
||||
return
|
||||
}
|
||||
|
||||
// Return success message
|
||||
c.Data(http.StatusOK, "text/html", []byte(`<div class="bg-green-100 border border-green-400 text-green-700 px-4 py-3 rounded mb-4" role="alert">
|
||||
<span class="block sm:inline">Password updated successfully!</span>
|
||||
</div>`))
|
||||
}
|
||||
|
||||
// HandleForgotPasswordPage displays the forgot password form
|
||||
func (h *Handlers) HandleForgotPasswordPage(c *gin.Context) {
|
||||
ctx := context.WithValue(c.Request.Context(), "theme", "light")
|
||||
components.ForgotPassword(ctx, "", "").Render(c.Request.Context(), c.Writer)
|
||||
}
|
||||
|
||||
// HandleForgotPassword processes the forgot password form submission
|
||||
func (h *Handlers) HandleForgotPassword(c *gin.Context) {
|
||||
email := c.PostForm("email")
|
||||
if email == "" {
|
||||
ctx := context.WithValue(c.Request.Context(), "theme", "light")
|
||||
components.ForgotPassword(ctx, "Email is required", "").Render(c.Request.Context(), c.Writer)
|
||||
return
|
||||
}
|
||||
|
||||
// Check if user exists
|
||||
user, err := h.DB.GetUserByEmail(email)
|
||||
if err != nil {
|
||||
// Don't reveal that the email doesn't exist for security reasons
|
||||
// But we'll log it for debugging
|
||||
log.Printf("Password reset requested for non-existent email: %s", email)
|
||||
ctx := context.WithValue(c.Request.Context(), "theme", "light")
|
||||
components.ForgotPassword(ctx, "", "If your email is registered, you will receive a password reset link.").Render(c.Request.Context(), c.Writer)
|
||||
return
|
||||
}
|
||||
|
||||
// Generate reset token
|
||||
token, err := generateResetToken(32)
|
||||
if err != nil {
|
||||
log.Printf("Error generating reset token: %v", err)
|
||||
ctx := context.WithValue(c.Request.Context(), "theme", "light")
|
||||
components.ForgotPassword(ctx, "An error occurred. Please try again later.", "").Render(c.Request.Context(), c.Writer)
|
||||
return
|
||||
}
|
||||
|
||||
// Save token in database with expiration time (15 minutes)
|
||||
expiration := time.Now().Add(15 * time.Minute)
|
||||
resetToken := &db.PasswordResetToken{
|
||||
UserID: user.ID,
|
||||
Token: token,
|
||||
ExpiresAt: expiration,
|
||||
}
|
||||
|
||||
if err := h.DB.CreatePasswordResetToken(resetToken); err != nil {
|
||||
log.Printf("Error saving reset token: %v", err)
|
||||
ctx := context.WithValue(c.Request.Context(), "theme", "light")
|
||||
components.ForgotPassword(ctx, "An error occurred. Please try again later.", "").Render(c.Request.Context(), c.Writer)
|
||||
return
|
||||
}
|
||||
|
||||
// Send password reset email
|
||||
err = h.Email.SendPasswordResetEmail(user.Email, user.Email, token)
|
||||
if err != nil {
|
||||
// If email sending fails, log the error but don't expose this to the user
|
||||
log.Printf("Error sending password reset email: %v", err)
|
||||
|
||||
// If email is disabled, log the reset link
|
||||
if strings.Contains(err.Error(), "email service is disabled") {
|
||||
log.Printf("Email service is disabled, reset link: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Show success message regardless of whether email was sent
|
||||
// This prevents user enumeration attacks
|
||||
ctx := context.WithValue(c.Request.Context(), "theme", "light")
|
||||
components.ForgotPassword(ctx, "", "If your email is registered, you will receive a password reset link.").Render(c.Request.Context(), c.Writer)
|
||||
}
|
||||
|
||||
// HandleResetPasswordPage displays the reset password form
|
||||
func (h *Handlers) HandleResetPasswordPage(c *gin.Context) {
|
||||
token := c.Query("token")
|
||||
if token == "" {
|
||||
c.Redirect(http.StatusFound, "/forgot-password")
|
||||
return
|
||||
}
|
||||
|
||||
// Validate token exists and hasn't expired
|
||||
_, err := h.DB.GetPasswordResetToken(token)
|
||||
if err != nil {
|
||||
log.Printf("Invalid reset token: %s, error: %v", token, err)
|
||||
c.Redirect(http.StatusFound, "/forgot-password")
|
||||
return
|
||||
}
|
||||
|
||||
ctx := context.WithValue(c.Request.Context(), "theme", "light")
|
||||
components.ResetPassword(ctx, token, "").Render(c.Request.Context(), c.Writer)
|
||||
}
|
||||
|
||||
// HandleResetPassword processes the reset password form submission
|
||||
func (h *Handlers) HandleResetPassword(c *gin.Context) {
|
||||
token := c.PostForm("token")
|
||||
password := c.PostForm("password")
|
||||
confirmPassword := c.PostForm("confirm-password")
|
||||
|
||||
if token == "" {
|
||||
c.Redirect(http.StatusFound, "/forgot-password")
|
||||
return
|
||||
}
|
||||
|
||||
if password == "" || confirmPassword == "" {
|
||||
ctx := context.WithValue(c.Request.Context(), "theme", "light")
|
||||
components.ResetPassword(ctx, token, "Both password fields are required.").Render(c.Request.Context(), c.Writer)
|
||||
return
|
||||
}
|
||||
|
||||
if password != confirmPassword {
|
||||
ctx := context.WithValue(c.Request.Context(), "theme", "light")
|
||||
components.ResetPassword(ctx, token, "Passwords do not match.").Render(c.Request.Context(), c.Writer)
|
||||
return
|
||||
}
|
||||
|
||||
if len(password) < 8 {
|
||||
ctx := context.WithValue(c.Request.Context(), "theme", "light")
|
||||
components.ResetPassword(ctx, token, "Password must be at least 8 characters long.").Render(c.Request.Context(), c.Writer)
|
||||
return
|
||||
}
|
||||
|
||||
// Validate token and get user
|
||||
resetToken, err := h.DB.GetPasswordResetToken(token)
|
||||
if err != nil {
|
||||
log.Printf("Invalid reset token: %s, error: %v", token, err)
|
||||
c.Redirect(http.StatusFound, "/forgot-password")
|
||||
return
|
||||
}
|
||||
|
||||
// Get the user
|
||||
user, err := h.DB.GetUserByID(resetToken.UserID)
|
||||
if err != nil {
|
||||
log.Printf("User not found for token: %s, user ID: %d, error: %v", token, resetToken.UserID, err)
|
||||
c.Redirect(http.StatusFound, "/forgot-password")
|
||||
return
|
||||
}
|
||||
|
||||
// Hash the new password
|
||||
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
|
||||
if err != nil {
|
||||
log.Printf("Error hashing password: %v", err)
|
||||
ctx := context.WithValue(c.Request.Context(), "theme", "light")
|
||||
components.ResetPassword(ctx, token, "An error occurred. Please try again later.").Render(c.Request.Context(), c.Writer)
|
||||
return
|
||||
}
|
||||
|
||||
// Update user's password
|
||||
user.PasswordHash = string(hashedPassword)
|
||||
user.LastPasswordChange = time.Now()
|
||||
if err := h.DB.UpdateUser(user); err != nil {
|
||||
log.Printf("Error updating user password: %v", err)
|
||||
ctx := context.WithValue(c.Request.Context(), "theme", "light")
|
||||
components.ResetPassword(ctx, token, "An error occurred. Please try again later.").Render(c.Request.Context(), c.Writer)
|
||||
return
|
||||
}
|
||||
|
||||
// Record password history
|
||||
passwordHistory := &auth.PasswordHistory{
|
||||
UserID: user.ID,
|
||||
PasswordHash: string(hashedPassword),
|
||||
}
|
||||
if err := h.DB.DB.Create(passwordHistory).Error; err != nil {
|
||||
log.Printf("Error recording password history: %v", err)
|
||||
}
|
||||
|
||||
// Mark token as used
|
||||
if err := h.DB.MarkPasswordResetTokenAsUsed(resetToken.ID); err != nil {
|
||||
log.Printf("Error marking token as used: %v", err)
|
||||
}
|
||||
|
||||
// Redirect to login with success message
|
||||
c.Redirect(http.StatusFound, "/login?message=Password+reset+successful.+Please+log+in+with+your+new+password.")
|
||||
}
|
||||
|
||||
// Helper function to generate a random token
|
||||
func generateResetToken(length int) (string, error) {
|
||||
b := make([]byte, length)
|
||||
_, err := rand.Read(b)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return base64.URLEncoding.EncodeToString(b), nil
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/starfleetcptn/gomft/components"
|
||||
"github.com/starfleetcptn/gomft/internal/auth"
|
||||
)
|
||||
|
||||
// HandleHome handles the GET / route
|
||||
func (h *Handlers) HandleHome(c *gin.Context) {
|
||||
// Check for JWT token in cookie
|
||||
tokenCookie, err := c.Cookie("jwt_token")
|
||||
if err == nil && tokenCookie != "" {
|
||||
// Token exists, validate it
|
||||
claims, err := auth.ValidateToken(tokenCookie, h.JWTSecret)
|
||||
if err == nil && claims != nil {
|
||||
// Valid token, redirect to dashboard
|
||||
c.Redirect(http.StatusFound, "/dashboard")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// User is not logged in, show home page
|
||||
components.Home(c.Request.Context()).Render(c, c.Writer)
|
||||
}
|
||||
@@ -0,0 +1,221 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/starfleetcptn/gomft/components"
|
||||
"github.com/starfleetcptn/gomft/internal/db"
|
||||
)
|
||||
|
||||
// HandleConfigs handles the GET /configs route
|
||||
func (h *Handlers) HandleConfigs(c *gin.Context) {
|
||||
userID := c.GetUint("userID")
|
||||
|
||||
var configs []db.TransferConfig
|
||||
h.DB.Where("created_by = ?", userID).Find(&configs)
|
||||
|
||||
data := components.ConfigsData{
|
||||
Configs: configs,
|
||||
}
|
||||
components.Configs(c.Request.Context(), data).Render(c, c.Writer)
|
||||
}
|
||||
|
||||
// HandleNewConfig handles the GET /configs/new route
|
||||
func (h *Handlers) HandleNewConfig(c *gin.Context) {
|
||||
data := components.ConfigFormData{
|
||||
Config: &db.TransferConfig{},
|
||||
IsNew: true,
|
||||
}
|
||||
components.ConfigForm(c.Request.Context(), data).Render(c, c.Writer)
|
||||
}
|
||||
|
||||
// HandleEditConfig handles the GET /configs/:id/edit route
|
||||
func (h *Handlers) HandleEditConfig(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
userID := c.GetUint("userID")
|
||||
|
||||
var config db.TransferConfig
|
||||
if err := h.DB.First(&config, id).Error; err != nil {
|
||||
c.Redirect(http.StatusFound, "/configs")
|
||||
return
|
||||
}
|
||||
|
||||
// Check if user owns this config
|
||||
if config.CreatedBy != userID {
|
||||
// Check if user is admin
|
||||
isAdmin, exists := c.Get("isAdmin")
|
||||
if !exists || isAdmin != true {
|
||||
c.Redirect(http.StatusFound, "/configs")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
data := components.ConfigFormData{
|
||||
Config: &config,
|
||||
IsNew: false,
|
||||
}
|
||||
components.ConfigForm(c.Request.Context(), data).Render(c, c.Writer)
|
||||
}
|
||||
|
||||
// HandleCreateConfig handles the POST /configs route
|
||||
func (h *Handlers) HandleCreateConfig(c *gin.Context) {
|
||||
var config db.TransferConfig
|
||||
if err := c.ShouldBind(&config); err != nil {
|
||||
log.Printf("Error binding config form: %v", err)
|
||||
c.String(http.StatusBadRequest, fmt.Sprintf("Invalid form data: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
userID := c.GetUint("userID")
|
||||
config.CreatedBy = userID
|
||||
|
||||
if err := h.DB.Create(&config).Error; err != nil {
|
||||
log.Printf("Error creating config: %v", err)
|
||||
c.String(http.StatusInternalServerError, fmt.Sprintf("Failed to create config: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
// Generate rclone config file
|
||||
if err := h.DB.GenerateRcloneConfig(&config); err != nil {
|
||||
log.Printf("Warning: Failed to generate rclone config: %v", err)
|
||||
// Continue anyway, as the config was created in the database
|
||||
} else {
|
||||
log.Printf("Generated rclone config for config ID %d", config.ID)
|
||||
}
|
||||
|
||||
c.Redirect(http.StatusFound, "/configs")
|
||||
}
|
||||
|
||||
// HandleUpdateConfig handles the PUT /configs/:id route
|
||||
func (h *Handlers) HandleUpdateConfig(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
userID := c.GetUint("userID")
|
||||
|
||||
var config db.TransferConfig
|
||||
if err := h.DB.First(&config, id).Error; err != nil {
|
||||
log.Printf("Error finding config: %v", err)
|
||||
c.String(http.StatusNotFound, "Config not found")
|
||||
return
|
||||
}
|
||||
|
||||
// Check if user owns this config
|
||||
if config.CreatedBy != userID {
|
||||
// Check if user is admin
|
||||
isAdmin, exists := c.Get("isAdmin")
|
||||
if !exists || isAdmin != true {
|
||||
c.String(http.StatusForbidden, "You do not have permission to update this config")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Get the old config values for comparison
|
||||
oldConfig := config
|
||||
|
||||
// Bind form data to config
|
||||
if err := c.ShouldBind(&config); err != nil {
|
||||
log.Printf("Error binding config form: %v", err)
|
||||
c.String(http.StatusBadRequest, fmt.Sprintf("Invalid form data: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
// Preserve fields that shouldn't be updated
|
||||
config.CreatedBy = oldConfig.CreatedBy
|
||||
|
||||
if err := h.DB.Save(&config).Error; err != nil {
|
||||
log.Printf("Error updating config: %v", err)
|
||||
c.String(http.StatusInternalServerError, fmt.Sprintf("Failed to update config: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
// Regenerate rclone config file
|
||||
if err := h.DB.GenerateRcloneConfig(&config); err != nil {
|
||||
log.Printf("Warning: Failed to regenerate rclone config: %v", err)
|
||||
// Continue anyway, as the config was updated in the database
|
||||
} else {
|
||||
log.Printf("Regenerated rclone config for config ID %d", config.ID)
|
||||
}
|
||||
|
||||
c.Redirect(http.StatusFound, "/configs")
|
||||
}
|
||||
|
||||
// HandleDeleteConfig handles the DELETE /configs/:id route
|
||||
func (h *Handlers) HandleDeleteConfig(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
userID := c.GetUint("userID")
|
||||
|
||||
var config db.TransferConfig
|
||||
if err := h.DB.First(&config, id).Error; err != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "Config not found"})
|
||||
return
|
||||
}
|
||||
|
||||
// Check if user owns this config
|
||||
if config.CreatedBy != userID {
|
||||
// Check if user is admin
|
||||
isAdmin, exists := c.Get("isAdmin")
|
||||
if !exists || isAdmin != true {
|
||||
c.JSON(http.StatusForbidden, gin.H{"error": "You do not have permission to delete this config"})
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Check if config is in use by any jobs
|
||||
var jobCount int64
|
||||
h.DB.Model(&db.Job{}).Where("config_id = ?", config.ID).Count(&jobCount)
|
||||
if jobCount > 0 {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Config is in use by jobs and cannot be deleted"})
|
||||
return
|
||||
}
|
||||
|
||||
// Delete config
|
||||
if err := h.DB.Delete(&config).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("Failed to delete config: %v", err)})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"message": "Config deleted successfully"})
|
||||
}
|
||||
|
||||
// HandleTestConnection handles the POST /configs/test route
|
||||
func (h *Handlers) HandleTestConnection(c *gin.Context) {
|
||||
var config db.TransferConfig
|
||||
if err := c.ShouldBind(&config); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": fmt.Sprintf("Invalid form data: %v", err)})
|
||||
return
|
||||
}
|
||||
|
||||
// TODO: Implement connection testing based on protocol
|
||||
// This is a placeholder for the actual connection testing logic
|
||||
success := true
|
||||
message := "Connection successful"
|
||||
|
||||
// Example of how connection testing might work
|
||||
switch config.SourceType {
|
||||
case "sftp":
|
||||
// Test SFTP connection
|
||||
// success, message = testSFTPConnection(config)
|
||||
default:
|
||||
success = false
|
||||
message = "Unsupported source type"
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": success,
|
||||
"message": message,
|
||||
})
|
||||
}
|
||||
|
||||
// HandleTestSFTPConnection handles the test SFTP connection request
|
||||
func (h *Handlers) HandleTestSFTPConnection(c *gin.Context) {
|
||||
// Implementation will be moved from the old handlers.go
|
||||
c.JSON(http.StatusOK, gin.H{"message": "Test SFTP connection handler stub"})
|
||||
}
|
||||
|
||||
// HandleBrowseDirectory handles the browse directory request
|
||||
func (h *Handlers) HandleBrowseDirectory(c *gin.Context) {
|
||||
// Implementation will be moved from the old handlers.go
|
||||
c.JSON(http.StatusOK, gin.H{"message": "Browse directory handler stub"})
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/starfleetcptn/gomft/components"
|
||||
"github.com/starfleetcptn/gomft/internal/db"
|
||||
)
|
||||
|
||||
// HandleDashboard handles the GET /dashboard route
|
||||
func (h *Handlers) HandleDashboard(c *gin.Context) {
|
||||
|
||||
// Get recent job history
|
||||
var recentHistory []db.JobHistory
|
||||
h.DB.Order("start_time DESC").Limit(5).Find(&recentHistory)
|
||||
|
||||
// Get job statistics
|
||||
var totalJobs int64
|
||||
h.DB.Model(&db.JobHistory{}).Where("job_histories.status = 'running' AND job_histories.end_time IS NULL").Count(&totalJobs)
|
||||
|
||||
var completedJobs int64
|
||||
h.DB.Model(&db.JobHistory{}).Where("status = ?", "completed").Count(&completedJobs)
|
||||
|
||||
var failedJobs int64
|
||||
h.DB.Model(&db.JobHistory{}).Where("status = ?", "failed").Count(&failedJobs)
|
||||
|
||||
data := components.DashboardData{
|
||||
RecentJobs: recentHistory,
|
||||
ActiveTransfers: int(totalJobs),
|
||||
CompletedToday: int(completedJobs),
|
||||
FailedTransfers: int(failedJobs),
|
||||
}
|
||||
|
||||
components.Dashboard(components.CreateTemplateContext(c), data).Render(c, c.Writer)
|
||||
}
|
||||
|
||||
// HandleDashboardStats handles the dashboard stats API request
|
||||
func (h *Handlers) HandleDashboardStats(c *gin.Context) {
|
||||
userID := c.GetUint("userID")
|
||||
|
||||
// Get job statistics
|
||||
var activeJobCount int64
|
||||
var completedJobCount int64
|
||||
var failedJobCount int64
|
||||
|
||||
h.DB.Model(&db.Job{}).Where("created_by = ? AND status = ?", userID, "running").Count(&activeJobCount)
|
||||
h.DB.Model(&db.Job{}).Where("created_by = ? AND status = ?", userID, "completed").Count(&completedJobCount)
|
||||
h.DB.Model(&db.Job{}).Where("created_by = ? AND status = ?", userID, "failed").Count(&failedJobCount)
|
||||
|
||||
// Get transfer statistics for the last 7 days
|
||||
var dailyStats []struct {
|
||||
Date string `json:"date"`
|
||||
Completed int64 `json:"completed"`
|
||||
Failed int64 `json:"failed"`
|
||||
}
|
||||
|
||||
for i := 6; i >= 0; i-- {
|
||||
date := time.Now().AddDate(0, 0, -i)
|
||||
startOfDay := time.Date(date.Year(), date.Month(), date.Day(), 0, 0, 0, 0, time.Local)
|
||||
endOfDay := time.Date(date.Year(), date.Month(), date.Day(), 23, 59, 59, 999999999, time.Local)
|
||||
|
||||
var completed int64
|
||||
var failed int64
|
||||
|
||||
h.DB.Model(&db.Job{}).
|
||||
Where("created_by = ? AND status = ? AND last_run BETWEEN ? AND ?", userID, "completed", startOfDay, endOfDay).
|
||||
Count(&completed)
|
||||
|
||||
h.DB.Model(&db.Job{}).
|
||||
Where("created_by = ? AND status = ? AND last_run BETWEEN ? AND ?", userID, "failed", startOfDay, endOfDay).
|
||||
Count(&failed)
|
||||
|
||||
dailyStats = append(dailyStats, struct {
|
||||
Date string `json:"date"`
|
||||
Completed int64 `json:"completed"`
|
||||
Failed int64 `json:"failed"`
|
||||
}{
|
||||
Date: startOfDay.Format("2006-01-02"),
|
||||
Completed: completed,
|
||||
Failed: failed,
|
||||
})
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"activeJobs": activeJobCount,
|
||||
"completedJobs": completedJobCount,
|
||||
"failedJobs": failedJobCount,
|
||||
"dailyStats": dailyStats,
|
||||
"uptime": time.Since(h.StartTime).String(),
|
||||
"uptimeSeconds": int64(time.Since(h.StartTime).Seconds()),
|
||||
})
|
||||
}
|
||||
|
||||
// HandleRecentJobs handles the recent jobs API request
|
||||
func (h *Handlers) HandleRecentJobs(c *gin.Context) {
|
||||
userID := c.GetUint("userID")
|
||||
|
||||
var recentJobs []db.Job
|
||||
h.DB.Where("created_by = ?", userID).Order("created_at DESC").Limit(5).Find(&recentJobs)
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"recentJobs": recentJobs,
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/starfleetcptn/gomft/internal/db"
|
||||
"github.com/starfleetcptn/gomft/internal/email"
|
||||
"github.com/starfleetcptn/gomft/internal/scheduler"
|
||||
)
|
||||
|
||||
// Handlers contains all the dependencies needed by the handlers
|
||||
type Handlers struct {
|
||||
DB *db.DB
|
||||
Scheduler *scheduler.Scheduler
|
||||
JWTSecret string
|
||||
StartTime time.Time
|
||||
DBPath string
|
||||
BackupDir string
|
||||
Email *email.Service
|
||||
}
|
||||
|
||||
// NewHandlers creates a new Handlers instance
|
||||
func NewHandlers(database *db.DB, scheduler *scheduler.Scheduler, jwtSecret string, dbPath string, backupDir string, emailService *email.Service) *Handlers {
|
||||
return &Handlers{
|
||||
DB: database,
|
||||
Scheduler: scheduler,
|
||||
JWTSecret: jwtSecret,
|
||||
StartTime: time.Now(),
|
||||
DBPath: dbPath,
|
||||
BackupDir: backupDir,
|
||||
Email: emailService,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,301 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/starfleetcptn/gomft/components"
|
||||
"github.com/starfleetcptn/gomft/internal/db"
|
||||
)
|
||||
|
||||
// HandleJobs handles the GET /jobs route
|
||||
func (h *Handlers) HandleJobs(c *gin.Context) {
|
||||
userID := c.GetUint("userID")
|
||||
|
||||
var jobs []db.Job
|
||||
h.DB.Where("created_by = ?", userID).Preload("Config").Find(&jobs)
|
||||
|
||||
data := components.JobsData{
|
||||
Jobs: jobs,
|
||||
}
|
||||
components.Jobs(c, data).Render(c, c.Writer)
|
||||
}
|
||||
|
||||
// HandleJobRunDetails handles the GET /job/:id route
|
||||
func (h *Handlers) HandleJobRunDetails(c *gin.Context) {
|
||||
userID := c.GetUint("userID")
|
||||
jobID := c.Param("id")
|
||||
|
||||
// Get job history
|
||||
var jobHistory db.JobHistory
|
||||
if err := h.DB.First(&jobHistory, jobID).Error; err != nil {
|
||||
c.String(http.StatusNotFound, "Job not found")
|
||||
return
|
||||
}
|
||||
|
||||
// Get job
|
||||
var job db.Job
|
||||
if err := h.DB.First(&job, jobHistory.JobID).Error; err != nil {
|
||||
c.String(http.StatusNotFound, "Job not found")
|
||||
return
|
||||
}
|
||||
|
||||
// Verify that the user owns this job
|
||||
if job.CreatedBy != userID {
|
||||
c.String(http.StatusForbidden, "You don't have permission to view this job run")
|
||||
return
|
||||
}
|
||||
|
||||
// Get the config
|
||||
var config db.TransferConfig
|
||||
if err := h.DB.First(&config, job.ConfigID).Error; err != nil {
|
||||
c.String(http.StatusNotFound, "Configuration not found")
|
||||
return
|
||||
}
|
||||
|
||||
data := components.JobRunDetailsData{
|
||||
JobHistory: jobHistory,
|
||||
Job: job,
|
||||
Config: config,
|
||||
}
|
||||
|
||||
components.JobRunDetails(c.Request.Context(), data).Render(c, c.Writer)
|
||||
}
|
||||
|
||||
// HandleNewJob handles the GET /jobs/new route
|
||||
func (h *Handlers) HandleNewJob(c *gin.Context) {
|
||||
// Get available configs for the user
|
||||
userID := c.GetUint("userID")
|
||||
var configs []db.TransferConfig
|
||||
h.DB.Where("created_by = ?", userID).Find(&configs)
|
||||
|
||||
data := components.JobFormData{
|
||||
Job: &db.Job{},
|
||||
Configs: configs,
|
||||
IsNew: true,
|
||||
}
|
||||
components.JobForm(c.Request.Context(), data).Render(c, c.Writer)
|
||||
}
|
||||
|
||||
// HandleEditJob handles the GET /jobs/:id/edit route
|
||||
func (h *Handlers) HandleEditJob(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
userID := c.GetUint("userID")
|
||||
|
||||
var job db.Job
|
||||
if err := h.DB.First(&job, id).Error; err != nil {
|
||||
c.Redirect(http.StatusFound, "/jobs")
|
||||
return
|
||||
}
|
||||
|
||||
// Check if user owns this job
|
||||
if job.CreatedBy != userID {
|
||||
// Check if user is admin
|
||||
isAdmin, exists := c.Get("isAdmin")
|
||||
if !exists || isAdmin != true {
|
||||
c.Redirect(http.StatusFound, "/jobs")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Get available configs for the user
|
||||
var configs []db.TransferConfig
|
||||
h.DB.Where("created_by = ?", userID).Find(&configs)
|
||||
|
||||
data := components.JobFormData{
|
||||
Job: &job,
|
||||
Configs: configs,
|
||||
IsNew: false,
|
||||
}
|
||||
components.JobForm(c.Request.Context(), data).Render(c, c.Writer)
|
||||
}
|
||||
|
||||
// HandleCreateJob handles the POST /jobs route
|
||||
func (h *Handlers) HandleCreateJob(c *gin.Context) {
|
||||
var job db.Job
|
||||
if err := c.ShouldBind(&job); err != nil {
|
||||
c.String(http.StatusBadRequest, "Invalid form data")
|
||||
return
|
||||
}
|
||||
|
||||
userID := c.GetUint("userID")
|
||||
job.CreatedBy = userID
|
||||
|
||||
// Verify that the config exists and belongs to the user
|
||||
var config db.TransferConfig
|
||||
if err := h.DB.First(&config, job.ConfigID).Error; err != nil {
|
||||
c.String(http.StatusBadRequest, "Invalid configuration selected")
|
||||
return
|
||||
}
|
||||
|
||||
// Check if the config belongs to the user
|
||||
if config.CreatedBy != userID {
|
||||
// Check if user is admin
|
||||
isAdmin, exists := c.Get("isAdmin")
|
||||
if !exists || isAdmin != true {
|
||||
c.String(http.StatusForbidden, "You do not have permission to use this configuration")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// If job name is empty, use the config name
|
||||
if job.Name == "" {
|
||||
job.Name = config.Name
|
||||
}
|
||||
|
||||
// Clear the Config field to prevent GORM from creating a new config
|
||||
job.Config = db.TransferConfig{}
|
||||
|
||||
// Create the job
|
||||
if err := h.DB.CreateJob(&job); err != nil {
|
||||
c.String(http.StatusInternalServerError, "Failed to create job")
|
||||
return
|
||||
}
|
||||
|
||||
// Schedule the job with the scheduler
|
||||
if err := h.Scheduler.ScheduleJob(&job); err != nil {
|
||||
c.String(http.StatusInternalServerError, "Job created but scheduling failed: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
c.Redirect(http.StatusFound, "/jobs")
|
||||
}
|
||||
|
||||
// HandleUpdateJob handles the PUT /jobs/:id route
|
||||
func (h *Handlers) HandleUpdateJob(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
userID := c.GetUint("userID")
|
||||
|
||||
var job db.Job
|
||||
if err := h.DB.First(&job, id).Error; err != nil {
|
||||
c.String(http.StatusNotFound, "Job not found")
|
||||
return
|
||||
}
|
||||
|
||||
// Check if user owns this job
|
||||
if job.CreatedBy != userID {
|
||||
// Check if user is admin
|
||||
isAdmin, exists := c.Get("isAdmin")
|
||||
if !exists || isAdmin != true {
|
||||
c.String(http.StatusForbidden, "You do not have permission to update this job")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Get the old job values for comparison
|
||||
oldJob := job
|
||||
|
||||
// Bind form data to job
|
||||
if err := c.ShouldBind(&job); err != nil {
|
||||
c.String(http.StatusBadRequest, "Invalid form data")
|
||||
return
|
||||
}
|
||||
|
||||
// Verify that the config exists and belongs to the user
|
||||
var config db.TransferConfig
|
||||
if err := h.DB.First(&config, job.ConfigID).Error; err != nil {
|
||||
c.String(http.StatusBadRequest, "Invalid configuration selected")
|
||||
return
|
||||
}
|
||||
|
||||
// Check if the config belongs to the user
|
||||
if config.CreatedBy != userID {
|
||||
// Check if user is admin
|
||||
isAdmin, exists := c.Get("isAdmin")
|
||||
if !exists || isAdmin != true {
|
||||
c.String(http.StatusForbidden, "You do not have permission to use this configuration")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// If job name is empty, use the config name
|
||||
if job.Name == "" {
|
||||
job.Name = config.Name
|
||||
}
|
||||
|
||||
// Preserve fields that shouldn't be updated
|
||||
job.CreatedBy = oldJob.CreatedBy
|
||||
job.ID = oldJob.ID
|
||||
|
||||
// Clear the Config field to prevent GORM from updating or creating a new config
|
||||
job.Config = db.TransferConfig{}
|
||||
|
||||
if err := h.DB.UpdateJob(&job); err != nil {
|
||||
c.String(http.StatusInternalServerError, "Failed to update job")
|
||||
return
|
||||
}
|
||||
|
||||
// Reschedule the job with the scheduler
|
||||
if err := h.Scheduler.ScheduleJob(&job); err != nil {
|
||||
c.String(http.StatusInternalServerError, "Job updated but scheduling failed: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
c.Redirect(http.StatusFound, "/jobs")
|
||||
}
|
||||
|
||||
// HandleDeleteJob handles the DELETE /jobs/:id route
|
||||
func (h *Handlers) HandleDeleteJob(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
userID := c.GetUint("userID")
|
||||
|
||||
var job db.Job
|
||||
if err := h.DB.First(&job, id).Error; err != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "Job not found"})
|
||||
return
|
||||
}
|
||||
|
||||
// Check if user owns this job
|
||||
if job.CreatedBy != userID {
|
||||
// Check if user is admin
|
||||
isAdmin, exists := c.Get("isAdmin")
|
||||
if !exists || isAdmin != true {
|
||||
c.JSON(http.StatusForbidden, gin.H{"error": "You do not have permission to delete this job"})
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Unschedule the job from the scheduler
|
||||
h.Scheduler.UnscheduleJob(job.ID)
|
||||
|
||||
// Delete job
|
||||
if err := h.DB.Delete(&job).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to delete job"})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"message": "Job deleted successfully"})
|
||||
}
|
||||
|
||||
// HandleRunJob handles the POST /jobs/:id/run route
|
||||
func (h *Handlers) HandleRunJob(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
userID := c.GetUint("userID")
|
||||
|
||||
var job db.Job
|
||||
if err := h.DB.First(&job, id).Error; err != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "Job not found"})
|
||||
return
|
||||
}
|
||||
|
||||
// Check if user owns this job
|
||||
if job.CreatedBy != userID {
|
||||
// Check if user is admin
|
||||
isAdmin, exists := c.Get("isAdmin")
|
||||
if !exists || isAdmin != true {
|
||||
c.JSON(http.StatusForbidden, gin.H{"error": "You do not have permission to run this job"})
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Run the job immediately using the scheduler
|
||||
if err := h.Scheduler.RunJobNow(job.ID); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to run job: " + err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"message": "Job started successfully",
|
||||
"jobId": job.ID,
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/starfleetcptn/gomft/components"
|
||||
"github.com/starfleetcptn/gomft/internal/db"
|
||||
)
|
||||
|
||||
// HandleProfile handles the GET /profile route
|
||||
func (h *Handlers) HandleProfile(c *gin.Context) {
|
||||
userID := c.GetUint("userID")
|
||||
var user db.User
|
||||
if err := h.DB.First(&user, userID).Error; err != nil {
|
||||
c.String(http.StatusInternalServerError, "Failed to retrieve user profile")
|
||||
return
|
||||
}
|
||||
components.Profile(c.Request.Context(), user).Render(c, c.Writer)
|
||||
}
|
||||
|
||||
// HandleUpdateTheme handles the POST /profile/theme route
|
||||
func (h *Handlers) HandleUpdateTheme(c *gin.Context) {
|
||||
userID := c.GetUint("userID")
|
||||
theme := c.PostForm("theme")
|
||||
|
||||
// Validate theme value
|
||||
validThemes := map[string]bool{
|
||||
"light": true,
|
||||
"dark": true,
|
||||
"system": true,
|
||||
}
|
||||
|
||||
if !validThemes[theme] {
|
||||
c.Status(http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
// Update user theme preference
|
||||
var user db.User
|
||||
if err := h.DB.First(&user, userID).Error; err != nil {
|
||||
c.Status(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
user.Theme = theme
|
||||
if err := h.DB.Save(&user).Error; err != nil {
|
||||
c.Status(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
// Set theme cookie for client-side theme switching
|
||||
c.SetCookie("theme", theme, 60*60*24*365, "/", "", false, false)
|
||||
|
||||
c.Status(http.StatusOK)
|
||||
}
|
||||
|
||||
// HandleUpdateProfile handles the POST /profile/update route
|
||||
func (h *Handlers) HandleUpdateProfile(c *gin.Context) {
|
||||
userID := c.GetUint("userID")
|
||||
|
||||
var user db.User
|
||||
if err := h.DB.First(&user, userID).Error; err != nil {
|
||||
c.String(http.StatusNotFound, "User not found")
|
||||
return
|
||||
}
|
||||
|
||||
// Update user fields
|
||||
user.Email = c.PostForm("email")
|
||||
|
||||
if err := h.DB.Save(&user).Error; err != nil {
|
||||
c.String(http.StatusInternalServerError, fmt.Sprintf("Failed to update profile: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
c.Redirect(http.StatusFound, "/profile")
|
||||
}
|
||||
@@ -0,0 +1,267 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"net/url"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/starfleetcptn/gomft/components"
|
||||
"github.com/starfleetcptn/gomft/internal/db"
|
||||
)
|
||||
|
||||
// HandleHistory handles the GET /history route
|
||||
func (h *Handlers) HandleHistory(c *gin.Context) {
|
||||
userID := c.GetUint("userID")
|
||||
|
||||
// Get pagination parameters
|
||||
page, err := strconv.Atoi(c.DefaultQuery("page", "1"))
|
||||
if err != nil || page < 1 {
|
||||
page = 1
|
||||
}
|
||||
|
||||
pageSize, err := strconv.Atoi(c.DefaultQuery("pageSize", "10"))
|
||||
if err != nil {
|
||||
pageSize = 10
|
||||
}
|
||||
// Limit page size options
|
||||
if pageSize != 10 && pageSize != 25 && pageSize != 50 && pageSize != 100 {
|
||||
pageSize = 10
|
||||
}
|
||||
|
||||
// Get search term
|
||||
searchTerm := c.Query("search")
|
||||
|
||||
// Build the query
|
||||
query := h.DB.Model(&db.JobHistory{}).
|
||||
Joins("JOIN jobs ON jobs.id = job_histories.job_id").
|
||||
Joins("JOIN transfer_configs ON transfer_configs.id = jobs.config_id").
|
||||
Where("jobs.created_by = ?", userID)
|
||||
|
||||
// Apply search if provided
|
||||
if searchTerm != "" {
|
||||
query = query.Where("transfer_configs.name LIKE ? OR job_histories.status LIKE ?",
|
||||
"%"+searchTerm+"%", "%"+searchTerm+"%")
|
||||
}
|
||||
|
||||
// Count total matching records for pagination
|
||||
var total int64
|
||||
query.Count(&total)
|
||||
|
||||
// Calculate total pages
|
||||
totalPages := int(math.Ceil(float64(total) / float64(pageSize)))
|
||||
if totalPages == 0 {
|
||||
totalPages = 1
|
||||
}
|
||||
|
||||
// Ensure page is within bounds
|
||||
if page > totalPages {
|
||||
page = totalPages
|
||||
}
|
||||
|
||||
// Get paginated results
|
||||
var history []db.JobHistory
|
||||
offset := (page - 1) * pageSize
|
||||
|
||||
query.Offset(offset).
|
||||
Limit(pageSize).
|
||||
Preload("Job.Config").
|
||||
Order("start_time desc").
|
||||
Find(&history)
|
||||
|
||||
// If we got no results and we're not on page 1, redirect to page 1
|
||||
// Only do this for non-HTMX requests to avoid navigation issues
|
||||
isHtmxRequest := c.GetHeader("HX-Request") == "true"
|
||||
if len(history) == 0 && page > 1 && total > 0 && !isHtmxRequest {
|
||||
redirectURL := fmt.Sprintf("/history?page=1&pageSize=%d", pageSize)
|
||||
if searchTerm != "" {
|
||||
redirectURL += fmt.Sprintf("&search=%s", url.QueryEscape(searchTerm))
|
||||
}
|
||||
c.Redirect(http.StatusFound, redirectURL)
|
||||
return
|
||||
}
|
||||
|
||||
data := components.HistoryData{
|
||||
History: history,
|
||||
CurrentPage: page,
|
||||
TotalPages: totalPages,
|
||||
SearchTerm: searchTerm,
|
||||
PageSize: pageSize,
|
||||
Total: int(total),
|
||||
}
|
||||
|
||||
// If this is an HTMX request, only render the history content component
|
||||
if isHtmxRequest {
|
||||
components.HistoryContent(c, data).Render(c, c.Writer)
|
||||
} else {
|
||||
components.History(c, data).Render(c, c.Writer)
|
||||
}
|
||||
}
|
||||
|
||||
// HandleDashboardData handles the GET /dashboard/data route
|
||||
func (h *Handlers) HandleDashboardData(c *gin.Context) {
|
||||
// Get recent job runs
|
||||
var recentRuns []db.JobHistory
|
||||
if err := h.DB.Preload("Job").Order("start_time desc").Limit(5).Find(&recentRuns).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to retrieve recent runs"})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"recent_runs": recentRuns,
|
||||
})
|
||||
}
|
||||
|
||||
// HandleDashboardJobsData handles the GET /dashboard/jobs route
|
||||
func (h *Handlers) HandleDashboardJobsData(c *gin.Context) {
|
||||
// Get active jobs
|
||||
var activeJobs []db.Job
|
||||
if err := h.DB.Where("enabled = ?", true).Find(&activeJobs).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to retrieve active jobs"})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"active_jobs": activeJobs,
|
||||
})
|
||||
}
|
||||
|
||||
// HandleDashboardHistoryData handles the GET /dashboard/history route
|
||||
func (h *Handlers) HandleDashboardHistoryData(c *gin.Context) {
|
||||
// Get job history stats
|
||||
var successCount int64
|
||||
var failureCount int64
|
||||
var pendingCount int64
|
||||
|
||||
h.DB.Model(&db.JobHistory{}).Where("status = ?", "success").Count(&successCount)
|
||||
h.DB.Model(&db.JobHistory{}).Where("status = ?", "failure").Count(&failureCount)
|
||||
h.DB.Model(&db.JobHistory{}).Where("status = ?", "pending").Count(&pendingCount)
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success_count": successCount,
|
||||
"failure_count": failureCount,
|
||||
"pending_count": pendingCount,
|
||||
})
|
||||
}
|
||||
|
||||
// RegisterRoutes registers all the routes for the web interface
|
||||
func (h *Handlers) RegisterRoutes(router *gin.Engine) {
|
||||
// Public routes
|
||||
router.GET("/", h.HandleHome)
|
||||
router.GET("/login", h.HandleLoginPage)
|
||||
router.POST("/login", h.HandleLogin)
|
||||
router.GET("/forgot-password", h.HandleForgotPasswordPage)
|
||||
router.POST("/forgot-password", h.HandleForgotPassword)
|
||||
router.GET("/reset-password", h.HandleResetPasswordPage)
|
||||
router.POST("/reset-password", h.HandleResetPassword)
|
||||
|
||||
// Protected routes
|
||||
authorized := router.Group("/")
|
||||
authorized.Use(h.AuthMiddleware())
|
||||
|
||||
// Password change route - only accessed from profile page
|
||||
authorized.POST("/change-password", h.HandleChangePassword)
|
||||
|
||||
{
|
||||
authorized.GET("/dashboard", h.HandleDashboard)
|
||||
authorized.GET("/configs", h.HandleConfigs)
|
||||
authorized.GET("/configs/new", h.HandleNewConfig)
|
||||
authorized.GET("/configs/:id", h.HandleEditConfig)
|
||||
authorized.POST("/configs", h.HandleCreateConfig)
|
||||
authorized.PUT("/configs/:id", h.HandleUpdateConfig)
|
||||
authorized.POST("/configs/:id", h.HandleUpdateConfig) // Add POST route for form submission
|
||||
authorized.DELETE("/configs/:id", h.HandleDeleteConfig)
|
||||
authorized.GET("/jobs", h.HandleJobs)
|
||||
authorized.GET("/jobs/new", h.HandleNewJob)
|
||||
authorized.GET("/jobs/:id", h.HandleEditJob)
|
||||
authorized.POST("/jobs", h.HandleCreateJob)
|
||||
authorized.PUT("/jobs/:id", h.HandleUpdateJob)
|
||||
authorized.POST("/jobs/:id", h.HandleUpdateJob) // Add POST route for form submission
|
||||
authorized.DELETE("/jobs/:id", h.HandleDeleteJob)
|
||||
authorized.POST("/jobs/:id/run", h.HandleRunJob)
|
||||
authorized.GET("/history", h.HandleHistory)
|
||||
authorized.GET("/job-runs/:id", h.HandleJobRunDetails)
|
||||
authorized.GET("/profile", h.HandleProfile)
|
||||
authorized.POST("/profile/theme", h.HandleUpdateTheme)
|
||||
authorized.POST("/logout", h.HandleLogout)
|
||||
|
||||
// AJAX routes for dashboard
|
||||
authorized.GET("/dashboard/data", h.HandleDashboardData)
|
||||
authorized.GET("/dashboard/jobs", h.HandleDashboardJobsData)
|
||||
authorized.GET("/dashboard/history", h.HandleDashboardHistoryData)
|
||||
|
||||
// Test connection routes
|
||||
authorized.POST("/test-connection", h.HandleTestConnection)
|
||||
authorized.POST("/test-sftp-connection", h.HandleTestSFTPConnection)
|
||||
authorized.POST("/browse-directory", h.HandleBrowseDirectory)
|
||||
}
|
||||
|
||||
// Admin-only routes
|
||||
admin := router.Group("/admin")
|
||||
admin.Use(h.AuthMiddleware(), h.AdminMiddleware())
|
||||
{
|
||||
admin.GET("/users", h.HandleUsers)
|
||||
admin.GET("/users/new", h.HandleNewUser)
|
||||
admin.POST("/users", h.HandleCreateUser)
|
||||
admin.DELETE("/users/:id", h.HandleDeleteUser)
|
||||
admin.GET("/register", h.HandleRegisterPage)
|
||||
admin.POST("/register", h.HandleRegister)
|
||||
|
||||
// Admin tools routes
|
||||
admin.GET("/tools", h.HandleAdminTools)
|
||||
admin.POST("/backup-database", h.HandleBackupDatabase)
|
||||
admin.POST("/restore-database", h.HandleRestoreDatabase)
|
||||
admin.POST("/restore-database/:filename", h.HandleRestoreDatabaseByFilename)
|
||||
admin.GET("/export-configs", h.HandleExportConfigs)
|
||||
admin.GET("/export-jobs", h.HandleExportJobs)
|
||||
admin.POST("/clear-job-history", h.HandleClearJobHistory)
|
||||
admin.POST("/vacuum-database", h.HandleVacuumDatabase)
|
||||
admin.GET("/download-backup/:filename", h.HandleDownloadBackup)
|
||||
admin.DELETE("/delete-backup/:filename", h.HandleDeleteBackup)
|
||||
admin.GET("/refresh-backups", h.HandleRefreshBackups)
|
||||
}
|
||||
|
||||
// API routes
|
||||
api := router.Group("/api")
|
||||
{
|
||||
api.POST("/login", h.HandleAPILogin)
|
||||
|
||||
// Protected API routes
|
||||
apiAuthorized := api.Group("/")
|
||||
apiAuthorized.Use(h.APIAuthMiddleware())
|
||||
{
|
||||
// Config endpoints
|
||||
apiAuthorized.GET("/configs", h.HandleAPIConfigs)
|
||||
apiAuthorized.GET("/configs/:id", h.HandleAPIConfig)
|
||||
apiAuthorized.POST("/configs", h.HandleAPICreateConfig)
|
||||
apiAuthorized.PUT("/configs/:id", h.HandleAPIUpdateConfig)
|
||||
apiAuthorized.DELETE("/configs/:id", h.HandleAPIDeleteConfig)
|
||||
|
||||
// Job endpoints
|
||||
apiAuthorized.GET("/jobs", h.HandleAPIJobs)
|
||||
apiAuthorized.GET("/jobs/:id", h.HandleAPIJob)
|
||||
apiAuthorized.POST("/jobs", h.HandleAPICreateJob)
|
||||
apiAuthorized.PUT("/jobs/:id", h.HandleAPIUpdateJob)
|
||||
apiAuthorized.DELETE("/jobs/:id", h.HandleAPIDeleteJob)
|
||||
apiAuthorized.POST("/jobs/:id/run", h.HandleAPIRunJob)
|
||||
|
||||
// History endpoints
|
||||
apiAuthorized.GET("/history", h.HandleAPIHistory)
|
||||
apiAuthorized.GET("/job-runs/:id", h.HandleAPIJobRun)
|
||||
|
||||
// Admin-only API routes
|
||||
apiAdmin := apiAuthorized.Group("/admin")
|
||||
apiAdmin.Use(h.APIAdminMiddleware())
|
||||
{
|
||||
// User management
|
||||
apiAdmin.GET("/users", h.HandleAPIUsers)
|
||||
apiAdmin.GET("/users/:id", h.HandleAPIUser)
|
||||
apiAdmin.POST("/users", h.HandleAPICreateUser)
|
||||
apiAdmin.PUT("/users/:id", h.HandleAPIUpdateUser)
|
||||
apiAdmin.DELETE("/users/:id", h.HandleAPIDeleteUser)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,316 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/starfleetcptn/gomft/components"
|
||||
"github.com/starfleetcptn/gomft/internal/db"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
)
|
||||
|
||||
// HandleUsers handles the GET /admin/users route
|
||||
func (h *Handlers) HandleUsers(c *gin.Context) {
|
||||
var users []db.User
|
||||
if err := h.DB.Find(&users).Error; err != nil {
|
||||
c.String(http.StatusInternalServerError, "Failed to retrieve users")
|
||||
return
|
||||
}
|
||||
data := components.UsersData{
|
||||
Users: users,
|
||||
}
|
||||
components.Users(components.CreateTemplateContext(c), data).Render(c, c.Writer)
|
||||
}
|
||||
|
||||
// HandleNewUser handles the GET /admin/users/new route
|
||||
func (h *Handlers) HandleNewUser(c *gin.Context) {
|
||||
data := components.UserFormData{
|
||||
IsNew: true,
|
||||
ErrorMessage: "",
|
||||
}
|
||||
components.UserForm(components.CreateTemplateContext(c), data).Render(c, c.Writer)
|
||||
}
|
||||
|
||||
// HandleCreateUser handles the POST /admin/users/new route
|
||||
func (h *Handlers) HandleCreateUser(c *gin.Context) {
|
||||
email := c.PostForm("email")
|
||||
password := c.PostForm("password")
|
||||
isAdmin := c.PostForm("is_admin") == "on"
|
||||
|
||||
// Check if email already exists
|
||||
var existingUser db.User
|
||||
if err := h.DB.Where("email = ?", email).First(&existingUser).Error; err == nil {
|
||||
c.String(http.StatusBadRequest, "Email already exists")
|
||||
return
|
||||
}
|
||||
|
||||
// Hash the password
|
||||
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
|
||||
if err != nil {
|
||||
c.String(http.StatusInternalServerError, "Failed to hash password")
|
||||
return
|
||||
}
|
||||
|
||||
// Create the user
|
||||
user := db.User{
|
||||
Email: email,
|
||||
PasswordHash: string(hashedPassword),
|
||||
IsAdmin: isAdmin,
|
||||
LastPasswordChange: time.Now(),
|
||||
}
|
||||
|
||||
if err := h.DB.Create(&user).Error; err != nil {
|
||||
c.String(http.StatusInternalServerError, "Failed to create user")
|
||||
return
|
||||
}
|
||||
|
||||
c.Redirect(http.StatusSeeOther, "/admin/users")
|
||||
}
|
||||
|
||||
// HandleDeleteUser handles the POST /admin/users/delete route
|
||||
func (h *Handlers) HandleDeleteUser(c *gin.Context) {
|
||||
userID, err := strconv.ParseUint(c.Param("id"), 10, 32)
|
||||
if err != nil {
|
||||
c.String(http.StatusBadRequest, "Invalid user ID")
|
||||
return
|
||||
}
|
||||
|
||||
// Don't allow deleting the current user
|
||||
currentUserID := c.GetUint("userID")
|
||||
if uint(userID) == currentUserID {
|
||||
c.String(http.StatusBadRequest, "Cannot delete your own account")
|
||||
return
|
||||
}
|
||||
|
||||
// Delete the user
|
||||
if err := h.DB.Delete(&db.User{}, userID).Error; err != nil {
|
||||
c.String(http.StatusInternalServerError, "Failed to delete user")
|
||||
return
|
||||
}
|
||||
|
||||
c.Redirect(http.StatusSeeOther, "/admin/users")
|
||||
}
|
||||
|
||||
// HandleRegisterPage handles the GET /register route
|
||||
func (h *Handlers) HandleRegisterPage(c *gin.Context) {
|
||||
// Check if any users exist
|
||||
var count int64
|
||||
h.DB.Model(&db.User{}).Count(&count)
|
||||
|
||||
// If users exist, don't allow registration
|
||||
if count > 0 {
|
||||
c.Redirect(http.StatusSeeOther, "/")
|
||||
return
|
||||
}
|
||||
|
||||
components.Register(c.Request.Context(), "").Render(c, c.Writer)
|
||||
}
|
||||
|
||||
// HandleRegister handles the POST /register route
|
||||
func (h *Handlers) HandleRegister(c *gin.Context) {
|
||||
// Check if any users exist
|
||||
var count int64
|
||||
h.DB.Model(&db.User{}).Count(&count)
|
||||
|
||||
// If users exist, don't allow registration
|
||||
if count > 0 {
|
||||
c.Redirect(http.StatusSeeOther, "/")
|
||||
return
|
||||
}
|
||||
|
||||
email := c.PostForm("email")
|
||||
password := c.PostForm("password")
|
||||
|
||||
// Hash the password
|
||||
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
|
||||
if err != nil {
|
||||
c.String(http.StatusInternalServerError, "Failed to hash password")
|
||||
return
|
||||
}
|
||||
|
||||
// Create the admin user
|
||||
user := db.User{
|
||||
Email: email,
|
||||
PasswordHash: string(hashedPassword),
|
||||
IsAdmin: true,
|
||||
LastPasswordChange: time.Now(),
|
||||
}
|
||||
|
||||
if err := h.DB.Create(&user).Error; err != nil {
|
||||
c.String(http.StatusInternalServerError, "Failed to create user")
|
||||
return
|
||||
}
|
||||
|
||||
// Generate JWT
|
||||
token, err := h.GenerateJWT(user.ID, user.Email, user.IsAdmin)
|
||||
if err != nil {
|
||||
c.String(http.StatusInternalServerError, "Failed to generate token")
|
||||
return
|
||||
}
|
||||
|
||||
// Set cookie
|
||||
c.SetCookie("jwt", token, 60*60*24, "/", "", false, true)
|
||||
|
||||
c.Redirect(http.StatusSeeOther, "/dashboard")
|
||||
}
|
||||
|
||||
// HandleEditUser handles the edit user page request
|
||||
func (h *Handlers) HandleEditUser(c *gin.Context) {
|
||||
// Only admin users can access this page
|
||||
isAdmin, exists := c.Get("isAdmin")
|
||||
if !exists || isAdmin != true {
|
||||
c.Redirect(http.StatusFound, "/dashboard")
|
||||
return
|
||||
}
|
||||
|
||||
id := c.Param("id")
|
||||
var user db.User
|
||||
if err := h.DB.First(&user, id).Error; err != nil {
|
||||
c.Redirect(http.StatusFound, "/users")
|
||||
return
|
||||
}
|
||||
|
||||
data := components.UserFormData{
|
||||
IsNew: false,
|
||||
ErrorMessage: "",
|
||||
}
|
||||
components.UserForm(c.Request.Context(), data).Render(c, c.Writer)
|
||||
}
|
||||
|
||||
// HandleUpdateUser handles the update user form submission
|
||||
func (h *Handlers) HandleUpdateUser(c *gin.Context) {
|
||||
// Only admin users can update users
|
||||
isAdmin, exists := c.Get("isAdmin")
|
||||
if !exists || isAdmin != true {
|
||||
c.String(http.StatusForbidden, "Only administrators can update users")
|
||||
return
|
||||
}
|
||||
|
||||
id := c.Param("id")
|
||||
var user db.User
|
||||
if err := h.DB.First(&user, id).Error; err != nil {
|
||||
log.Printf("Error finding user: %v", err)
|
||||
c.String(http.StatusNotFound, "User not found")
|
||||
return
|
||||
}
|
||||
|
||||
// Get the old user values for comparison
|
||||
oldUser := user
|
||||
|
||||
// Bind form data to user
|
||||
if err := c.ShouldBind(&user); err != nil {
|
||||
log.Printf("Error binding user form: %v", err)
|
||||
c.String(http.StatusBadRequest, fmt.Sprintf("Invalid form data: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
// Check if email already exists for a different user
|
||||
var existingUser db.User
|
||||
if user.Email != oldUser.Email {
|
||||
if err := h.DB.Where("email = ? AND id != ?", user.Email, user.ID).First(&existingUser).Error; err == nil {
|
||||
c.String(http.StatusBadRequest, "Email already in use")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Get password from form
|
||||
password := c.PostForm("password")
|
||||
|
||||
// Only update password if provided
|
||||
if password != "" {
|
||||
// Validate password complexity
|
||||
if !h.validatePasswordComplexity(password) {
|
||||
c.String(http.StatusBadRequest, "Password does not meet complexity requirements")
|
||||
return
|
||||
}
|
||||
|
||||
// Hash password
|
||||
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
|
||||
if err != nil {
|
||||
log.Printf("Error hashing password: %v", err)
|
||||
c.String(http.StatusInternalServerError, "Failed to hash password")
|
||||
return
|
||||
}
|
||||
user.PasswordHash = string(hashedPassword)
|
||||
user.LastPasswordChange = time.Now()
|
||||
} else {
|
||||
// Preserve the old password if not updating
|
||||
user.PasswordHash = oldUser.PasswordHash
|
||||
user.LastPasswordChange = oldUser.LastPasswordChange
|
||||
}
|
||||
|
||||
// Preserve fields that shouldn't be updated
|
||||
user.CreatedAt = oldUser.CreatedAt
|
||||
user.FailedLoginAttempts = oldUser.FailedLoginAttempts
|
||||
user.AccountLocked = oldUser.AccountLocked
|
||||
user.LockoutUntil = oldUser.LockoutUntil
|
||||
|
||||
// Update admin status
|
||||
user.IsAdmin = c.PostForm("is_admin") == "on"
|
||||
|
||||
if err := h.DB.Save(&user).Error; err != nil {
|
||||
log.Printf("Error updating user: %v", err)
|
||||
c.String(http.StatusInternalServerError, fmt.Sprintf("Failed to update user: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
c.Redirect(http.StatusFound, "/users")
|
||||
}
|
||||
|
||||
// HandleUnlockUser handles the unlock user request
|
||||
func (h *Handlers) HandleUnlockUser(c *gin.Context) {
|
||||
// Only admin users can unlock users
|
||||
isAdmin, exists := c.Get("isAdmin")
|
||||
if !exists || isAdmin != true {
|
||||
c.JSON(http.StatusForbidden, gin.H{"error": "Only administrators can unlock users"})
|
||||
return
|
||||
}
|
||||
|
||||
id := c.Param("id")
|
||||
var user db.User
|
||||
if err := h.DB.First(&user, id).Error; err != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "User not found"})
|
||||
return
|
||||
}
|
||||
|
||||
// Unlock user
|
||||
user.AccountLocked = false
|
||||
user.FailedLoginAttempts = 0
|
||||
user.LockoutUntil = nil
|
||||
|
||||
if err := h.DB.Save(&user).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("Failed to unlock user: %v", err)})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"message": "User unlocked successfully"})
|
||||
}
|
||||
|
||||
// validatePasswordComplexity validates that a password meets complexity requirements
|
||||
func (h *Handlers) validatePasswordComplexity(password string) bool {
|
||||
// Password must be at least 8 characters long
|
||||
if len(password) < 8 {
|
||||
return false
|
||||
}
|
||||
|
||||
// Check for at least one uppercase letter, one lowercase letter, and one number
|
||||
hasUpper := false
|
||||
hasLower := false
|
||||
hasNumber := false
|
||||
|
||||
for _, char := range password {
|
||||
if char >= 'A' && char <= 'Z' {
|
||||
hasUpper = true
|
||||
} else if char >= 'a' && char <= 'z' {
|
||||
hasLower = true
|
||||
} else if char >= '0' && char <= '9' {
|
||||
hasNumber = true
|
||||
}
|
||||
}
|
||||
|
||||
return hasUpper && hasLower && hasNumber
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
// AuthMiddleware is a middleware function that checks if the request has a valid JWT token
|
||||
func (m *Middleware) AuthMiddleware() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
// Get token from cookie
|
||||
tokenString, err := c.Cookie("jwt_token")
|
||||
if err != nil {
|
||||
c.Redirect(http.StatusFound, "/login")
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
// Parse and validate token
|
||||
token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
|
||||
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
|
||||
return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])
|
||||
}
|
||||
return []byte(m.JWTSecret), nil
|
||||
})
|
||||
|
||||
if err != nil || !token.Valid {
|
||||
c.SetCookie("jwt_token", "", -1, "/", "", false, true)
|
||||
c.Redirect(http.StatusFound, "/login")
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
// Extract claims
|
||||
claims, ok := token.Claims.(jwt.MapClaims)
|
||||
if !ok {
|
||||
c.SetCookie("jwt_token", "", -1, "/", "", false, true)
|
||||
c.Redirect(http.StatusFound, "/login")
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
// Set user information in context
|
||||
c.Set("userID", uint(claims["user_id"].(float64)))
|
||||
c.Set("email", claims["email"].(string))
|
||||
c.Set("username", claims["username"].(string))
|
||||
c.Set("isAdmin", claims["is_admin"].(bool))
|
||||
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user