Files
GoMFT/internal/web/handlers/oauth_handlers.go
T
StarFleetCPTN 8607f2098a feat: Integrate Google Drive support and enhance configuration handling
- Add Google Drive as a source and destination option in the configuration forms.
- Implement Google Drive authentication flow and token management.
- Update job and configuration handlers to support Google Drive-specific settings.
- Enhance UI components to include Google Drive configuration templates.
- Introduce new tests for Google Drive integration and ensure proper handling of authentication and configuration.
- Update database migrations to accommodate new fields related to Google Drive configurations.
2025-03-15 17:36:36 -07:00

240 lines
6.3 KiB
Go

package handlers
import (
"context"
"fmt"
"io/ioutil"
"net/http"
"net/url"
"os"
"os/exec"
"path/filepath"
"strings"
"time"
"github.com/gin-contrib/sessions"
"github.com/gin-gonic/gin"
)
// GoogleDriveAuthHandler initiates the Google Drive OAuth flow
func (h *Handlers) HandleGoogleDriveAuth(c *gin.Context) {
configID := c.Query("config_id")
if configID == "" {
RenderErrorPage(c, "Missing config_id parameter", "")
return
}
// Prepare for the OAuth flow
dataDir := os.Getenv("DATA_DIR")
if dataDir == "" {
dataDir = "./data"
}
// Ensure oauth directory exists
oauthDir := filepath.Join(dataDir, "oauth")
if err := os.MkdirAll(oauthDir, 0755); err != nil {
RenderErrorPage(c, "Failed to create oauth directory", err.Error())
return
}
// Get rclone path
rclonePath := os.Getenv("RCLONE_PATH")
if rclonePath == "" {
rclonePath = "rclone"
}
// Set up a temporary rclone config
tempConfigPath := filepath.Join(oauthDir, fmt.Sprintf("temp_gdrive_%s.conf", configID))
// Build rclone command to get auth URL
cmd := exec.Command(
rclonePath,
"config",
"create",
"temp_gdrive",
"drive",
"--config",
tempConfigPath,
)
// Set a timeout context
ctx, cancel := context.WithTimeout(c.Request.Context(), 30*time.Second)
defer cancel()
// Run the command with proper context handling
// We can't use exec.CommandContext directly since we're creating the command differently
// So we'll use a goroutine with the context's Done() channel to handle cancellation
go func() {
<-ctx.Done() // Wait for context to be done (timeout or cancellation)
if cmd.Process != nil {
if err := cmd.Process.Kill(); err != nil {
RenderErrorPage(c, "Failed to kill rclone process", err.Error())
}
}
}()
// Run the command to get the browser URL (this will fail in a specific way)
output, err := cmd.CombinedOutput()
if err != nil {
outputStr := string(output)
// Look for the URL in the output
authURL := extractAuthURL(outputStr)
if authURL == "" {
RenderErrorPage(c, "Failed to get Google Drive authentication URL", outputStr)
return
}
// Store the config ID in the session
session := sessions.Default(c)
session.Set("gdrive_config_id", configID)
session.Set("gdrive_temp_config", tempConfigPath)
if err := session.Save(); err != nil {
RenderErrorPage(c, "Failed to save session", err.Error())
return
}
// Use component rendering instead of HTML template
// This would typically use a component like:
// components.GDriveAuth(c.Request.Context(), components.GDriveAuthData{
// AuthURL: authURL,
// ConfigID: configID,
// }).Render(c.Request.Context(), c.Writer)
// For now, we'll redirect to the configs page with the auth URL and config ID
c.Redirect(http.StatusFound, fmt.Sprintf("/configs/%s/gdrive-auth?auth_url=%s",
configID, url.QueryEscape(authURL)))
return
}
// If we get here, something unexpected happened
RenderErrorPage(c, "Unexpected result from rclone", string(output))
}
// HandleGoogleDriveCallback handles the manual entry of the OAuth code
func (h *Handlers) HandleGoogleDriveCallback(c *gin.Context) {
// Get the auth code from form submission
authCode := c.PostForm("auth_code")
if authCode == "" {
RenderErrorPage(c, "Missing authentication code", "")
return
}
// Get the config ID from the session
session := sessions.Default(c)
configID := session.Get("gdrive_config_id")
tempConfigPath := session.Get("gdrive_temp_config")
if configID == nil || tempConfigPath == nil {
RenderErrorPage(c, "Session expired or invalid. Please try again.", "")
return
}
// Get rclone path
rclonePath := os.Getenv("RCLONE_PATH")
if rclonePath == "" {
rclonePath = "rclone"
}
// Complete the OAuth flow with the provided code
cmd := exec.Command(
rclonePath,
"config",
"reconnect",
"temp_gdrive:",
"--config",
tempConfigPath.(string),
)
// Create a pipe for stdin
stdin, err := cmd.StdinPipe()
if err != nil {
RenderErrorPage(c, "Failed to create stdin pipe", err.Error())
return
}
// Start the command
if err := cmd.Start(); err != nil {
RenderErrorPage(c, "Failed to start rclone command", err.Error())
return
}
// Write the auth code to stdin
fmt.Fprintln(stdin, authCode)
stdin.Close()
// Wait for the command to complete
if err := cmd.Wait(); err != nil {
RenderErrorPage(c, "Failed to complete Google Drive authentication", err.Error())
return
}
// Read the token from the config file
configData, err := ioutil.ReadFile(tempConfigPath.(string))
if err != nil {
RenderErrorPage(c, "Failed to read token from config file", err.Error())
return
}
// Extract token from config
token := extractToken(string(configData))
if token == "" {
RenderErrorPage(c, "Failed to extract token from config", "")
return
}
// Store the token in the database
configIDStr := configID.(string)
if err := h.DB.StoreGoogleDriveToken(configIDStr, token); err != nil {
RenderErrorPage(c, "Failed to save token", err.Error())
return
}
// Clean up temporary config
os.Remove(tempConfigPath.(string))
// Clear session data
session.Delete("gdrive_config_id")
session.Delete("gdrive_temp_config")
if err := session.Save(); err != nil {
RenderErrorPage(c, "Failed to save session", err.Error())
return
}
// Redirect to the configs page
c.Redirect(http.StatusFound, "/configs?status=gdrive_auth_success")
}
// Helper function to extract the authentication URL from rclone output
func extractAuthURL(output string) string {
// This is a simplified version - you may need to improve the regex
// to handle different output formats from rclone
lines := strings.Split(output, "\n")
for _, line := range lines {
if strings.Contains(line, "http") && strings.Contains(line, "accounts.google.com") {
// Extract the URL - this is a simplified approach
words := strings.Fields(line)
for _, word := range words {
if strings.HasPrefix(word, "http") {
return word
}
}
}
}
return ""
}
// Helper function to extract token from rclone config
func extractToken(configData string) string {
// Look for the token JSON in the config
lines := strings.Split(configData, "\n")
for _, line := range lines {
if strings.Contains(line, "token") {
parts := strings.SplitN(line, "=", 2)
if len(parts) == 2 {
return strings.TrimSpace(parts[1])
}
}
}
return ""
}