package handlers import ( "encoding/json" "fmt" "io" "net/http" "net/url" "os" "path/filepath" "strconv" "time" "github.com/gin-gonic/gin" ) // HandleGDriveAuth initiates the Google Drive authentication process func (h *Handlers) HandleGDriveAuth(c *gin.Context) { // Get the config ID from the query parameter configIDStr := c.Param("id") if configIDStr == "" { RenderErrorPage(c, "Missing configuration ID", "") return } configID, err := strconv.ParseUint(configIDStr, 10, 64) if err != nil { RenderErrorPage(c, "Invalid configuration ID", err.Error()) return } // Get the configuration config, err := h.DB.GetTransferConfig(uint(configID)) if err != nil { RenderErrorPage(c, "Configuration not found", err.Error()) return } // Ensure it's a Google Drive or Google Photos configuration if config.SourceType != "gdrive" && config.DestinationType != "gdrive" && config.SourceType != "gphotos" && config.DestinationType != "gphotos" { RenderErrorPage(c, "Not a Google configuration", "The selected configuration is not set up for Google Drive or Google Photos") return } // Prepare for OAuth dataDir := os.Getenv("DATA_DIR") if dataDir == "" { dataDir = "./data" } // Get Rclone Config Path rcloneConfigPath := h.DB.GetConfigRclonePath(config) if rcloneConfigPath == "" { RenderErrorPage(c, "Rclone config not found", "The selected configuration does not have a valid rclone config") return } // Create a temporary config file for authentication tempConfigDir := filepath.Join(dataDir, "temp") if err := os.MkdirAll(tempConfigDir, 0755); err != nil { RenderErrorPage(c, "Failed to create temporary directory", err.Error()) return } tempConfigPath := filepath.Join(tempConfigDir, fmt.Sprintf("gdrive_auth_%d.conf", config.ID)) // Store the temporary config path in a cookie c.SetCookie("gdrive_temp_config", tempConfigPath, 3600, "/", "", false, true) // Get base URL for redirect URI baseURL := os.Getenv("BASE_URL") if baseURL == "" { // Try to detect the base URL from the request scheme := "http" if c.Request.TLS != nil { scheme = "https" } baseURL = fmt.Sprintf("%s://%s", scheme, c.Request.Host) } // Define the redirect URI for our callback redirectURI := fmt.Sprintf("%s/configs/gdrive-callback", baseURL) // Attempt to get GOOGLE_CLIENT_ID and GOOGLE_CLIENT_SECRET from ENV clientID := os.Getenv("GOOGLE_CLIENT_ID") clientSecret := os.Getenv("GOOGLE_CLIENT_SECRET") if clientID == "" || clientSecret == "" { // Check if we have client credentials in the existing config file existingClientID, existingClientSecret := h.DB.GetGDriveCredentialsFromConfig(config) if existingClientID != "" && existingClientSecret != "" { // Use credentials from existing config clientID = existingClientID clientSecret = existingClientSecret } else { // fallback to rclone client ID and secret clientID = "202264815644.apps.googleusercontent.com" clientSecret = "X4Z3ca8xfWDb1Voo-F9a7ZxJ" } } if config.DestClientID != "" && config.DestClientSecret == "" { // If user provided just client ID but no secret, use environment variable or default envSecret := os.Getenv("GOOGLE_CLIENT_SECRET") if envSecret != "" { // Use the secret from environment variable clientSecret = envSecret } else { // Use default rclone client secret clientSecret = "X4Z3ca8xfWDb1Voo-F9a7ZxJ" } } // Generate state parameter for security (to prevent CSRF) state := fmt.Sprintf("gomft_%d_%d", config.ID, time.Now().Unix()) // Store state in cookie for validation during callback c.SetCookie("gdrive_auth_state", state, 3600, "/", "", false, true) // Store config ID in cookie for use during callback c.SetCookie("gdrive_config_id", configIDStr, 3600, "/", "", false, true) // Determine the appropriate scope based on destination type var scope string if config.DestinationType == "gphotos" { // Read-only access is handled elsewhere in the config; here we need the full auth scope scope = url.QueryEscape("https://www.googleapis.com/auth/photoslibrary") } else { // Default to Google Drive scope scope = url.QueryEscape("https://www.googleapis.com/auth/drive") } // Create a config file with redirect URI-based auth configType := "drive" if config.DestinationType == "gphotos" { configType = "google photos" } configContent := fmt.Sprintf(`[temp_%s] type = %s client_id = %s client_secret = %s redirect_url = %s `, config.DestinationType, configType, clientID, clientSecret, redirectURI) // Write the config file if err := os.WriteFile(tempConfigPath, []byte(configContent), 0644); err != nil { RenderErrorPage(c, "Failed to create temporary config file", err.Error()) return } // Direct Google OAuth URL with our redirect authURL := fmt.Sprintf("https://accounts.google.com/o/oauth2/auth?client_id=%s&redirect_uri=%s&scope=%s&response_type=code&access_type=offline&state=%s", url.QueryEscape(clientID), url.QueryEscape(redirectURI), scope, url.QueryEscape(state)) // Redirect the user to Google's auth page directly c.Redirect(http.StatusFound, authURL) } // HandleGDriveAuthCallback handles the callback from Google OAuth func (h *Handlers) HandleGDriveAuthCallback(c *gin.Context) { // Get auth code from query parameters authCode := c.Query("code") if authCode == "" { RenderErrorPage(c, "Authentication failed", "No authorization code received from Google") return } // Verify state parameter to prevent CSRF state := c.Query("state") storedState, err := c.Cookie("gdrive_auth_state") if err != nil || state != storedState { RenderErrorPage(c, "Authentication failed", "Invalid state parameter") return } // Get config ID from cookie configIDStr, err := c.Cookie("gdrive_config_id") if err != nil { RenderErrorPage(c, "Authentication failed", "Unable to retrieve configuration ID") return } configID, err := strconv.ParseUint(configIDStr, 10, 64) if err != nil { RenderErrorPage(c, "Invalid configuration ID", err.Error()) return } // Get the temp config path from cookie tempConfigPath, err := c.Cookie("gdrive_temp_config") if err != nil || tempConfigPath == "" { RenderErrorPage(c, "Session expired", "The authentication session has expired") return } // Get base URL for redirect URI baseURL := os.Getenv("BASE_URL") if baseURL == "" { // Try to detect the base URL from the request scheme := "http" if c.Request.TLS != nil { scheme = "https" } baseURL = fmt.Sprintf("%s://%s", scheme, c.Request.Host) } redirectURI := fmt.Sprintf("%s/configs/gdrive-callback", baseURL) // Get the configuration to retrieve client ID and secret config, err := h.DB.GetTransferConfig(uint(configID)) if err != nil { RenderErrorPage(c, "Failed to get configuration", err.Error()) return } // Attempt to get GOOGLE_CLIENT_ID and GOOGLE_CLIENT_SECRET from ENV clientID := os.Getenv("GOOGLE_CLIENT_ID") clientSecret := os.Getenv("GOOGLE_CLIENT_SECRET") if clientID == "" || clientSecret == "" { // Check if we have client credentials in the existing config file existingClientID, existingClientSecret := h.DB.GetGDriveCredentialsFromConfig(config) if existingClientID != "" && existingClientSecret != "" { // Use credentials from existing config clientID = existingClientID clientSecret = existingClientSecret } else { // fallback to rclone client ID and secret clientID = "202264815644.apps.googleusercontent.com" clientSecret = "X4Z3ca8xfWDb1Voo-F9a7ZxJ" } } if config.DestClientID != "" && config.DestClientSecret == "" { // If user provided just client ID but no secret, use environment variable or default envSecret := os.Getenv("GOOGLE_CLIENT_SECRET") if envSecret != "" { // Use the secret from environment variable clientSecret = envSecret } else { // Use default rclone client secret clientSecret = "X4Z3ca8xfWDb1Voo-F9a7ZxJ" } } // Exchange auth code for token using HTTP request tokenURL := "https://oauth2.googleapis.com/token" formData := url.Values{ "code": {authCode}, "client_id": {clientID}, "client_secret": {clientSecret}, "redirect_uri": {redirectURI}, "grant_type": {"authorization_code"}, } resp, err := http.PostForm(tokenURL, formData) if err != nil { RenderErrorPage(c, "Failed to exchange authorization code for token", err.Error()) return } defer resp.Body.Close() body, err := io.ReadAll(resp.Body) if err != nil { RenderErrorPage(c, "Failed to read token response", err.Error()) return } if resp.StatusCode != http.StatusOK { RenderErrorPage(c, "Failed to exchange authorization code for token", string(body)) return } // Parse the token response var tokenResp struct { AccessToken string `json:"access_token"` TokenType string `json:"token_type"` RefreshToken string `json:"refresh_token"` ExpiresIn int `json:"expires_in"` } if err := json.Unmarshal(body, &tokenResp); err != nil { RenderErrorPage(c, "Failed to parse token response", err.Error()) return } // Create a token JSON in the format rclone expects tokenJSON := fmt.Sprintf(`{ "access_token": "%s", "token_type": "%s", "refresh_token": "%s", "expiry": "%s" }`, tokenResp.AccessToken, tokenResp.TokenType, tokenResp.RefreshToken, time.Now().Add(time.Duration(tokenResp.ExpiresIn)*time.Second).Format(time.RFC3339)) // Mark the configuration as authenticated in the database config.SetGoogleDriveAuthenticated(true) if err := h.DB.UpdateTransferConfig(config); err != nil { RenderErrorPage(c, "Failed to update configuration", err.Error()) return } // Generate the rclone config file with the token if err := h.DB.GenerateRcloneConfigWithToken(config, tokenJSON); err != nil { RenderErrorPage(c, "Failed to generate rclone configuration", err.Error()) return } // Clean up the temporary file os.Remove(tempConfigPath) // Clear cookies c.SetCookie("gdrive_temp_config", "", -1, "/", "", false, true) c.SetCookie("gdrive_auth_state", "", -1, "/", "", false, true) c.SetCookie("gdrive_config_id", "", -1, "/", "", false, true) // Redirect to the config list with a success message var successParam string if config.DestinationType == "gphotos" { successParam = "gphotos_auth_success" } else { successParam = "gdrive_auth_success" } c.Redirect(http.StatusFound, fmt.Sprintf("/configs?status=%s", successParam)) } // HandleGDriveTokenProcess processes a Google Drive token directly from a URL parameter func (h *Handlers) HandleGDriveTokenProcess(c *gin.Context) { // Get the parameters configID := c.Query("config_id") if configID == "" { RenderErrorPage(c, "Missing configuration ID", "") return } token := c.Query("token") if token == "" { RenderErrorPage(c, "Missing token", "") return } // Parse config ID configIDUint, err := strconv.ParseUint(configID, 10, 64) if err != nil { RenderErrorPage(c, "Invalid configuration ID", err.Error()) return } // Get the configuration config, err := h.DB.GetTransferConfig(uint(configIDUint)) if err != nil { RenderErrorPage(c, "Configuration not found", err.Error()) return } // Ensure it's a Google Drive or Google Photos configuration if config.SourceType != "gdrive" && config.DestinationType != "gdrive" && config.SourceType != "gphotos" && config.DestinationType != "gphotos" { RenderErrorPage(c, "Not a Google Drive configuration", "") return } // Mark the configuration as authenticated config.SetGoogleDriveAuthenticated(true) if err := h.DB.UpdateTransferConfig(config); err != nil { RenderErrorPage(c, "Failed to update configuration", err.Error()) return } // Generate the rclone config with the token if err := h.DB.GenerateRcloneConfigWithToken(config, token); err != nil { RenderErrorPage(c, "Failed to generate rclone configuration", err.Error()) return } // Redirect to the config list with success c.Redirect(http.StatusFound, "/configs?status=gdrive_auth_success") } // RenderErrorPage renders an error page with the given message func RenderErrorPage(c *gin.Context, title string, details string) { // Here we'd typically use a component for error display // For now, we'll just redirect to the configs page with an error in the query string errorURL := "/configs?error=" + url.QueryEscape(title) if details != "" { errorURL += "&details=" + url.QueryEscape(details) } c.Redirect(http.StatusFound, errorURL) }