package api import ( "fmt" "log" "net/http" "strings" "time" "github.com/gin-gonic/gin" "github.com/starfleetcptn/gomft/internal/auth" "github.com/starfleetcptn/gomft/internal/db" "github.com/starfleetcptn/gomft/internal/scheduler" "golang.org/x/crypto/bcrypt" ) type RegisterRequest struct { Email string `json:"email" binding:"required,min=3,max=50"` Password string `json:"password" binding:"required,min=8"` } type LoginRequest struct { Email string `json:"email" binding:"required"` Password string `json:"password" binding:"required"` } type LoginResponse struct { Token string `json:"token"` User UserResponse `json:"user"` } type UserResponse struct { ID uint `json:"id"` Email string `json:"email"` } func InitializeRoutes(router *gin.Engine, database *db.DB, scheduler *scheduler.Scheduler, jwtSecret string) { api := router.Group("/api") // Auth routes auth := api.Group("/auth") { auth.POST("/register", handleRegister(database)) auth.POST("/login", handleLogin(database, jwtSecret)) auth.POST("/logout", handleLogout()) } // Protected routes protected := api.Group("") protected.Use(authMiddleware(database, jwtSecret)) { // Transfer config routes protected.GET("/configs", handleListConfigs(database)) protected.POST("/configs", handleCreateConfig(database)) protected.GET("/configs/:id", handleGetConfig(database)) protected.PUT("/configs/:id", handleUpdateConfig(database)) protected.DELETE("/configs/:id", handleDeleteConfig(database)) // Job routes protected.GET("/jobs", handleListJobs(database)) protected.POST("/jobs", handleCreateJob(database, scheduler)) protected.GET("/jobs/:id", handleGetJob(database)) protected.PUT("/jobs/:id", handleUpdateJob(database, scheduler)) protected.DELETE("/jobs/:id", handleDeleteJob(database, scheduler)) protected.POST("/jobs/:id/run", handleRunJob(database, scheduler)) protected.POST("/jobs/:id/enable", handleEnableJob(database, scheduler)) protected.POST("/jobs/:id/disable", handleDisableJob(database, scheduler)) // History routes protected.GET("/jobs/:id/history", handleGetJobHistory(database)) protected.GET("/history", handleListHistory(database)) } } func handleRegister(database *db.DB) gin.HandlerFunc { return func(c *gin.Context) { var req RegisterRequest if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } // Check if email already exists if _, err := database.GetUserByEmail(req.Email); err == nil { c.JSON(http.StatusBadRequest, gin.H{"error": "Email already exists"}) return } // Hash password hashedPassword, err := bcrypt.GenerateFromPassword([]byte(req.Password), bcrypt.DefaultCost) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to hash password"}) return } // Create user user := &db.User{ Email: req.Email, PasswordHash: string(hashedPassword), } if err := database.CreateUser(user); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create user"}) return } c.JSON(http.StatusCreated, gin.H{"message": "User registered successfully"}) } } func handleLogin(database *db.DB, jwtSecret string) gin.HandlerFunc { return func(c *gin.Context) { var req LoginRequest if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } user, err := database.GetUserByEmail(req.Email) if err != nil { c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid credentials"}) return } if err := bcrypt.CompareHashAndPassword([]byte(user.PasswordHash), []byte(req.Password)); err != nil { c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid credentials"}) return } // Generate JWT token token, err := auth.GenerateToken(user.ID, user.Email, jwtSecret, 24*time.Hour) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to generate token"}) return } c.JSON(http.StatusOK, LoginResponse{ Token: token, User: UserResponse{ ID: user.ID, Email: user.Email, }, }) } } func handleLogout() gin.HandlerFunc { return func(c *gin.Context) { // JWT tokens are stateless, so we don't need to do anything server-side // The client should discard the token c.JSON(http.StatusOK, gin.H{"message": "Logout successful"}) } } func authMiddleware(database *db.DB, jwtSecret string) gin.HandlerFunc { return func(c *gin.Context) { // Get Authorization header authHeader := c.GetHeader("Authorization") if authHeader == "" { c.JSON(http.StatusUnauthorized, gin.H{"error": "Authorization header required"}) c.Abort() return } // Check if the header has the Bearer prefix parts := strings.Split(authHeader, " ") if len(parts) != 2 || parts[0] != "Bearer" { c.JSON(http.StatusUnauthorized, gin.H{"error": "Authorization header must be in the format 'Bearer {token}'"}) c.Abort() return } // Validate token tokenString := parts[1] claims, err := auth.ValidateToken(tokenString, jwtSecret) if err != nil { c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid or expired token"}) c.Abort() return } // Set user ID in context c.Set("userID", claims.UserID) c.Set("email", claims.Email) c.Next() } } func handleListConfigs(database *db.DB) gin.HandlerFunc { return func(c *gin.Context) { // Get user ID from context userID := c.GetUint("userID") configs, err := database.GetTransferConfigs(userID) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch configs"}) return } c.JSON(http.StatusOK, configs) } } func handleCreateConfig(database *db.DB) gin.HandlerFunc { return func(c *gin.Context) { var config db.TransferConfig if err := c.ShouldBindJSON(&config); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } // Set user ID config.CreatedBy = c.GetUint("userID") if err := database.CreateTransferConfig(&config); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create config"}) return } c.JSON(http.StatusCreated, config) } } func handleGetConfig(database *db.DB) gin.HandlerFunc { return func(c *gin.Context) { id := c.Param("id") if id == "" { c.JSON(http.StatusBadRequest, gin.H{"error": "Missing config ID"}) return } var configID uint if _, err := fmt.Sscanf(id, "%d", &configID); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid config ID"}) return } config, err := database.GetTransferConfig(configID) if err != nil { c.JSON(http.StatusNotFound, gin.H{"error": "Config not found"}) return } // Check if user has access to this config if config.CreatedBy != c.GetUint("userID") { c.JSON(http.StatusForbidden, gin.H{"error": "Unauthorized"}) return } c.JSON(http.StatusOK, config) } } func handleUpdateConfig(database *db.DB) gin.HandlerFunc { return func(c *gin.Context) { id := c.Param("id") if id == "" { c.JSON(http.StatusBadRequest, gin.H{"error": "Missing config ID"}) return } var configID uint if _, err := fmt.Sscanf(id, "%d", &configID); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid config ID"}) return } // Get existing config existingConfig, err := database.GetTransferConfig(configID) if err != nil { c.JSON(http.StatusNotFound, gin.H{"error": "Config not found"}) return } // Check if user has access to this config if existingConfig.CreatedBy != c.GetUint("userID") { c.JSON(http.StatusForbidden, gin.H{"error": "Unauthorized"}) return } // Bind updated fields var updatedConfig db.TransferConfig if err := c.ShouldBindJSON(&updatedConfig); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } // Update fields but preserve ID and CreatedBy updatedConfig.ID = existingConfig.ID updatedConfig.CreatedBy = existingConfig.CreatedBy updatedConfig.CreatedAt = existingConfig.CreatedAt if err := database.UpdateTransferConfig(&updatedConfig); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to update config"}) return } // Regenerate the rclone config file if err := database.GenerateRcloneConfig(&updatedConfig); err != nil { // Log the error but continue anyway as the config was updated in the database log.Printf("Warning: Failed to regenerate rclone config after API update: %v", err) } c.JSON(http.StatusOK, updatedConfig) } } func handleDeleteConfig(database *db.DB) gin.HandlerFunc { return func(c *gin.Context) { id := c.Param("id") if id == "" { c.JSON(http.StatusBadRequest, gin.H{"error": "Missing config ID"}) return } var configID uint if _, err := fmt.Sscanf(id, "%d", &configID); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid config ID"}) return } // Get existing config to check ownership config, err := database.GetTransferConfig(configID) if err != nil { c.JSON(http.StatusNotFound, gin.H{"error": "Config not found"}) return } // Check if user has access to this config if config.CreatedBy != c.GetUint("userID") { c.JSON(http.StatusForbidden, gin.H{"error": "Unauthorized"}) return } if err := database.DeleteTransferConfig(configID); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } c.JSON(http.StatusOK, gin.H{"message": "Config deleted successfully"}) } } func handleListJobs(database *db.DB) gin.HandlerFunc { return func(c *gin.Context) { // Get user ID from context userID := c.GetUint("userID") jobs, err := database.GetJobs(userID) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch jobs"}) return } c.JSON(http.StatusOK, jobs) } } func handleCreateJob(database *db.DB, scheduler *scheduler.Scheduler) gin.HandlerFunc { return func(c *gin.Context) { var job db.Job if err := c.ShouldBindJSON(&job); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } // Set user ID job.CreatedBy = c.GetUint("userID") // Validate config exists and user has access _, err := database.GetTransferConfig(job.ConfigID) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid config ID"}) return } // Check if user has access to this config config, err := database.GetTransferConfig(job.ConfigID) if err != nil { c.JSON(http.StatusNotFound, gin.H{"error": "Config not found"}) return } if config.CreatedBy != c.GetUint("userID") { c.JSON(http.StatusForbidden, gin.H{"error": "Unauthorized"}) return } if err := database.CreateJob(&job); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create job"}) return } // Schedule the job if enabled if job.GetEnabled() { if err := scheduler.ScheduleJob(&job); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to schedule job"}) return } } c.JSON(http.StatusCreated, job) } } func handleGetJob(database *db.DB) gin.HandlerFunc { return func(c *gin.Context) { id := c.Param("id") if id == "" { c.JSON(http.StatusBadRequest, gin.H{"error": "Missing job ID"}) return } var jobID uint if _, err := fmt.Sscanf(id, "%d", &jobID); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid job ID"}) return } job, err := database.GetJob(jobID) if err != nil { c.JSON(http.StatusNotFound, gin.H{"error": "Job not found"}) return } // Check if user has access to this job if job.CreatedBy != c.GetUint("userID") { c.JSON(http.StatusForbidden, gin.H{"error": "Unauthorized"}) return } c.JSON(http.StatusOK, job) } } func handleUpdateJob(database *db.DB, scheduler *scheduler.Scheduler) gin.HandlerFunc { return func(c *gin.Context) { id := c.Param("id") if id == "" { c.JSON(http.StatusBadRequest, gin.H{"error": "Missing job ID"}) return } var jobID uint if _, err := fmt.Sscanf(id, "%d", &jobID); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid job ID"}) return } // Get existing job existingJob, err := database.GetJob(jobID) if err != nil { c.JSON(http.StatusNotFound, gin.H{"error": "Job not found"}) return } // Check if user has access to this job if existingJob.CreatedBy != c.GetUint("userID") { c.JSON(http.StatusForbidden, gin.H{"error": "Unauthorized"}) return } // Bind updated fields var updatedJob db.Job if err := c.ShouldBindJSON(&updatedJob); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } // Update fields but preserve ID and CreatedBy updatedJob.ID = existingJob.ID updatedJob.CreatedBy = existingJob.CreatedBy updatedJob.CreatedAt = existingJob.CreatedAt // Validate config exists and user has access if updatedJob.ConfigID != existingJob.ConfigID { _, err := database.GetTransferConfig(updatedJob.ConfigID) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid config ID"}) return } // Check if user has access to this config config, err := database.GetTransferConfig(updatedJob.ConfigID) if err != nil { c.JSON(http.StatusNotFound, gin.H{"error": "Config not found"}) return } if config.CreatedBy != c.GetUint("userID") { c.JSON(http.StatusForbidden, gin.H{"error": "Unauthorized"}) return } } // Check if schedule or enabled status changed scheduleChanged := updatedJob.Schedule != existingJob.Schedule || updatedJob.GetEnabled() != existingJob.GetEnabled() if err := database.UpdateJob(&updatedJob); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to update job"}) return } // Update the scheduler if needed if scheduleChanged { if updatedJob.GetEnabled() { if err := scheduler.ScheduleJob(&updatedJob); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to update job schedule"}) return } } else { scheduler.UnscheduleJob(updatedJob.ID) } } c.JSON(http.StatusOK, updatedJob) } } func handleDeleteJob(database *db.DB, scheduler *scheduler.Scheduler) gin.HandlerFunc { return func(c *gin.Context) { id := c.Param("id") if id == "" { c.JSON(http.StatusBadRequest, gin.H{"error": "Missing job ID"}) return } var jobID uint if _, err := fmt.Sscanf(id, "%d", &jobID); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid job ID"}) return } // Get existing job to check ownership job, err := database.GetJob(jobID) if err != nil { c.JSON(http.StatusNotFound, gin.H{"error": "Job not found"}) return } // Check if user has access to this job if job.CreatedBy != c.GetUint("userID") { c.JSON(http.StatusForbidden, gin.H{"error": "Unauthorized"}) return } // Remove from scheduler first scheduler.UnscheduleJob(jobID) if err := database.DeleteJob(jobID); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to delete job"}) return } c.JSON(http.StatusOK, gin.H{"message": "Job deleted successfully"}) } } func handleRunJob(database *db.DB, scheduler *scheduler.Scheduler) gin.HandlerFunc { return func(c *gin.Context) { id := c.Param("id") if id == "" { c.JSON(http.StatusBadRequest, gin.H{"error": "Missing job ID"}) return } var jobID uint if _, err := fmt.Sscanf(id, "%d", &jobID); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid job ID"}) return } // Get existing job job, err := database.GetJob(jobID) if err != nil { c.JSON(http.StatusNotFound, gin.H{"error": "Job not found"}) return } // Check if user has access to this job if job.CreatedBy != c.GetUint("userID") { c.JSON(http.StatusForbidden, gin.H{"error": "Unauthorized"}) return } // Run the job immediately if err := scheduler.RunJobNow(jobID); 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"}) } } func handleEnableJob(database *db.DB, scheduler *scheduler.Scheduler) gin.HandlerFunc { return func(c *gin.Context) { id := c.Param("id") if id == "" { c.JSON(http.StatusBadRequest, gin.H{"error": "Missing job ID"}) return } var jobID uint if _, err := fmt.Sscanf(id, "%d", &jobID); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid job ID"}) return } // Get existing job job, err := database.GetJob(jobID) if err != nil { c.JSON(http.StatusNotFound, gin.H{"error": "Job not found"}) return } // Check if user has access to this job if job.CreatedBy != c.GetUint("userID") { c.JSON(http.StatusForbidden, gin.H{"error": "Unauthorized"}) return } // Enable the job job.SetEnabled(true) // Add to scheduler if err := scheduler.ScheduleJob(job); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to schedule job"}) return } c.JSON(http.StatusOK, gin.H{"message": "Job enabled successfully"}) } } func handleDisableJob(database *db.DB, scheduler *scheduler.Scheduler) gin.HandlerFunc { return func(c *gin.Context) { id := c.Param("id") if id == "" { c.JSON(http.StatusBadRequest, gin.H{"error": "Missing job ID"}) return } var jobID uint if _, err := fmt.Sscanf(id, "%d", &jobID); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid job ID"}) return } // Get existing job job, err := database.GetJob(jobID) if err != nil { c.JSON(http.StatusNotFound, gin.H{"error": "Job not found"}) return } // Check if user has access to this job if job.CreatedBy != c.GetUint("userID") { c.JSON(http.StatusForbidden, gin.H{"error": "Unauthorized"}) return } // Disable the job job.SetEnabled(false) // Remove from scheduler scheduler.UnscheduleJob(jobID) c.JSON(http.StatusOK, gin.H{"message": "Job disabled successfully"}) } } func handleGetJobHistory(database *db.DB) gin.HandlerFunc { return func(c *gin.Context) { id := c.Param("id") if id == "" { c.JSON(http.StatusBadRequest, gin.H{"error": "Missing job ID"}) return } var jobID uint if _, err := fmt.Sscanf(id, "%d", &jobID); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid job ID"}) return } // Get existing job to check ownership _, err := database.GetJob(jobID) if err != nil { c.JSON(http.StatusNotFound, gin.H{"error": "Job not found"}) return } // Check if user has access to this job job, err := database.GetJob(jobID) if err != nil { c.JSON(http.StatusNotFound, gin.H{"error": "Job not found"}) return } if job.CreatedBy != c.GetUint("userID") { c.JSON(http.StatusForbidden, gin.H{"error": "Unauthorized"}) return } history, err := database.GetJobHistory(jobID) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch job history"}) return } c.JSON(http.StatusOK, history) } } func handleListHistory(database *db.DB) gin.HandlerFunc { return func(c *gin.Context) { // Get user ID from context userID := c.GetUint("userID") // TODO: Implement pagination // For now, just return the most recent 100 history entries for the user's jobs var history []db.JobHistory err := database.DB. Joins("JOIN jobs ON job_history.job_id = jobs.id"). Where("jobs.created_by = ?", userID). Order("start_time DESC"). Limit(100). Find(&history).Error if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch history"}) return } c.JSON(http.StatusOK, history) } }