diff --git a/apps/http-backend/src/index.ts b/apps/http-backend/src/index.ts index 816207a..5c829d7 100644 --- a/apps/http-backend/src/index.ts +++ b/apps/http-backend/src/index.ts @@ -1,3 +1,16 @@ +import dotenv from "dotenv"; +import { fileURLToPath } from "url"; +import { dirname, join } from "path"; + +// Load .env file FIRST, before any other imports +// This ensures our environment variables override any from other packages +const __filename = fileURLToPath(import.meta.url); +const __dirname = dirname(__filename); +const envPath = join(__dirname, "../.env"); +dotenv.config({ path: envPath, override: true }); +console.log("📁 Loaded .env from:", envPath); +console.log("🔐 GOOGLE_REDIRECT_URI:", process.env.GOOGLE_REDIRECT_URI || "NOT SET"); + import cookieParser from 'cookie-parser' import { NodeRegistry } from "@repo/nodes/nodeClient"; import express from "express"; @@ -20,7 +33,7 @@ app.use(cookieParser()); app.use("/user" , userRouter) app.use('/node', sheetRouter) -app.use('/auth/google', googleAuth) // ← CHANGED THIS LINE! +app.use('/oauth/google', googleAuth) // ← CHANGED THIS LINE! const PORT= 3002 diff --git a/apps/http-backend/src/routes/google_callback.ts b/apps/http-backend/src/routes/google_callback.ts index baa4ee4..82d06d6 100644 --- a/apps/http-backend/src/routes/google_callback.ts +++ b/apps/http-backend/src/routes/google_callback.ts @@ -3,6 +3,28 @@ import { GoogleOAuthService } from "@repo/nodes"; import type { OAuthTokens } from "@repo/nodes"; import { AuthRequest, userMiddleware } from "./userRoutes/userMiddleware.js"; import { Router, Request, Response } from "express"; +import dotenv from "dotenv"; +import { fileURLToPath } from "url"; +import { dirname, join } from "path"; + +// Get the directory of this file +const __filename = fileURLToPath(import.meta.url); +const __dirname = dirname(__filename); + +// Load environment variables from the http-backend directory +// When running from dist/, we need to go up to the src directory, then to the root +// Use override: true to ensure this .env file takes precedence over others +const envPath = join(__dirname, "../../.env"); +dotenv.config({ path: envPath, override: true }); + +// Log OAuth configuration at module load +const REDIRECT_URI = process.env.GOOGLE_REDIRECT_URI || "http://localhost:3002/oauth/google/callback"; +console.log("🔐 OAuth Configuration loaded:"); +console.log(" .env path:", envPath); +console.log(" GOOGLE_REDIRECT_URI from env:", process.env.GOOGLE_REDIRECT_URI || "NOT SET (using default)"); +console.log(" Using Redirect URI:", REDIRECT_URI); +console.log(" GOOGLE_CLIENT_ID:", process.env.GOOGLE_CLIENT_ID ? `${process.env.GOOGLE_CLIENT_ID.substring(0, 20)}...` : "NOT SET"); +console.log(" GOOGLE_CLIENT_SECRET:", process.env.GOOGLE_CLIENT_SECRET ? "SET" : "NOT SET"); export const googleAuth: Router = Router(); googleAuth.get( @@ -11,12 +33,22 @@ googleAuth.get( async (req: AuthRequest, res: Response) => { // const userId = req.user?.id || "test_user"; // Get from auth middleware const userId = req.user?.id; + const workflowId = req.query.workflowId as string | undefined; + + // Ensure redirect URI matches Google Cloud Console configuration + const redirectUri = REDIRECT_URI; + + console.log("🔐 OAuth Initiate - Redirect URI:", redirectUri); + console.log("🔐 OAuth Initiate - User ID:", userId); + console.log("🔐 OAuth Initiate - Workflow ID:", workflowId || "NOT PROVIDED"); + + // Encode userId and workflowId in state (format: userId|workflowId) + const state = workflowId ? `${userId}|${workflowId}` : userId; const oauth2 = new google.auth.OAuth2( process.env.GOOGLE_CLIENT_ID, process.env.GOOGLE_CLIENT_SECRET, - process.env.GOOGLE_REDIRECT_URI || - "http://localhost:3002/auth/google/callback" + redirectUri ); const authUrl = oauth2.generateAuthUrl({ @@ -27,10 +59,11 @@ googleAuth.get( "https://www.googleapis.com/auth/gmail.send", "https://www.googleapis.com/auth/gmail.readonly", ], - state: userId, + state: state, prompt: "consent", }); + console.log("🔐 OAuth Initiate - Generated Auth URL:", authUrl); return res.redirect(authUrl); } ); @@ -46,11 +79,29 @@ googleAuth.get( return res.json({ error: "Missing or invalid authorization code" }); } + // Ensure redirect URI matches Google Cloud Console configuration + const redirectUri = REDIRECT_URI; + + // Parse state: format is "userId" or "userId|workflowId" + let userId: string | undefined; + let workflowId: string | undefined; + + if (state && typeof state === "string") { + const parts = state.split("|"); + userId = parts[0]; + workflowId = parts[1]; // Will be undefined if not provided + } + + console.log("🔐 OAuth Callback - Redirect URI:", redirectUri); + console.log("🔐 OAuth Callback - Received code:", code ? "✅ Present" : "❌ Missing"); + console.log("🔐 OAuth Callback - State:", state); + console.log("🔐 OAuth Callback - Parsed User ID:", userId); + console.log("🔐 OAuth Callback - Parsed Workflow ID:", workflowId || "NOT PROVIDED"); + const oauth2 = new google.auth.OAuth2( process.env.GOOGLE_CLIENT_ID, process.env.GOOGLE_CLIENT_SECRET, - process.env.GOOGLE_REDIRECT_URI || - "http://localhost:3002/auth/google/callback" + redirectUri ); try { @@ -69,23 +120,82 @@ googleAuth.get( console.warn(' To force new refresh_token, user needs to revoke access at: https://myaccount.google.com/permissions'); } - // Save tokens to database if userId (state) is provided - if (state && typeof state === 'string') { - console.log(' Saving tokens for userId:', state); - await Oauth.saveCredentials(state, tokens as OAuthTokens) + // Save tokens to database if userId is provided + if (userId) { + console.log(' Saving tokens for userId:', userId); + await Oauth.saveCredentials(userId, tokens as OAuthTokens) console.log(' ✅ Tokens saved to database'); } - // Redirect to success page - return res.redirect("http://localhost:3000/workflow"); + // Redirect to workflow page if workflowId is provided, otherwise to general workflow page + const redirectUrl = workflowId + ? `http://localhost:3000/workflows/${workflowId}` + : "http://localhost:3000/workflow"; + console.log(' Redirecting to:', redirectUrl); + return res.redirect(redirectUrl); } catch (err: any) { console.error("Google token exchange error:", err); - return res.redirect( - `http://localhost:3000/workflow?google=error&msg=${encodeURIComponent(err?.message ?? "Token exchange failed")}` - ); + // Parse state to get workflowId for error redirect + let workflowId: string | undefined; + if (state && typeof state === "string") { + const parts = state.split("|"); + workflowId = parts[1]; + } + const errorUrl = workflowId + ? `http://localhost:3000/workflows/${workflowId}?google=error&msg=${encodeURIComponent(err?.message ?? "Token exchange failed")}` + : `http://localhost:3000/workflow?google=error&msg=${encodeURIComponent(err?.message ?? "Token exchange failed")}`; + return res.redirect(errorUrl); } }) +// Debug endpoint to check OAuth configuration +googleAuth.get('/debug/config', async(req: Request, res: Response)=>{ + try { + const redirectUri = REDIRECT_URI; + const clientId = process.env.GOOGLE_CLIENT_ID; + const hasClientSecret = !!process.env.GOOGLE_CLIENT_SECRET; + + // Create a test OAuth2 client to verify configuration + const oauth2 = new google.auth.OAuth2( + clientId, + process.env.GOOGLE_CLIENT_SECRET, + redirectUri + ); + + const testAuthUrl = oauth2.generateAuthUrl({ + access_type: "offline", + scope: ["https://www.googleapis.com/auth/spreadsheets"], + state: "test", + prompt: "consent", + }); + + const urlObj = new URL(testAuthUrl); + const redirectUriInUrl = urlObj.searchParams.get('redirect_uri'); + + return res.json({ + environment: { + GOOGLE_REDIRECT_URI: redirectUri, + GOOGLE_CLIENT_ID: clientId ? `${clientId.substring(0, 20)}...` : "NOT SET", + GOOGLE_CLIENT_SECRET: hasClientSecret ? "SET" : "NOT SET", + }, + oauth2Client: { + redirectUri: redirectUri, + redirectUriInGeneratedUrl: redirectUriInUrl, + matches: redirectUri === redirectUriInUrl, + }, + testAuthUrl: testAuthUrl, + message: redirectUri === redirectUriInUrl + ? "✅ Configuration looks correct!" + : "❌ Redirect URI mismatch detected!" + }); + } catch (err) { + return res.status(500).json({ + error: err instanceof Error ? err.message : 'Unknown error', + stack: err instanceof Error ? err.stack : undefined + }); + } +}); + // Debug endpoint to check stored credentials googleAuth.get('/debug/credentials', async(req: Request, res: Response)=>{ try { diff --git a/apps/http-backend/src/routes/userRoutes/userRoutes.ts b/apps/http-backend/src/routes/userRoutes/userRoutes.ts index 51e115e..84c073c 100644 --- a/apps/http-backend/src/routes/userRoutes/userRoutes.ts +++ b/apps/http-backend/src/routes/userRoutes/userRoutes.ts @@ -213,20 +213,33 @@ router.get( }, }); + // if (credentials.length === 0) { + // // No credentials found - return the correct auth URL + // const authUrl = `${process.env.BACKEND_URL || "http://localhost:3002"}/oauth/google/initiate`; + // return res.status(statusCodes.OK).json({ + // message: + // "Credentials not found create credentials using this auth url", + // Data: authUrl, + // }); + // } + + // // Credentials found - return them + // return res.status(statusCodes.OK).json({ + // message: "Credentials Fetched successfully", + // Data: credentials, + // }); if (credentials.length === 0) { - // No credentials found - return the correct auth URL - const authUrl = `${process.env.BACKEND_URL || "http://localhost:3002"}/auth/google/initiate`; - return res.status(statusCodes.OK).json({ - message: - "Credentials not found create credentials using this auth url", - Data: authUrl, + return res.status(200).json({ + message: "No credentials found", + data: [], // always array + hasCredentials: false, }); } - // Credentials found - return them - return res.status(statusCodes.OK).json({ - message: "Credentials Fetched successfully", - Data: credentials, + return res.status(200).json({ + message: "Credentials fetched", + data: credentials, + hasCredentials: true, }); } catch (e) { console.log( diff --git a/apps/web/app/hooks/useCredential.ts b/apps/web/app/hooks/useCredential.ts index 24d6b11..2abea42 100644 --- a/apps/web/app/hooks/useCredential.ts +++ b/apps/web/app/hooks/useCredential.ts @@ -1,34 +1,53 @@ "use client"; import { useEffect, useState } from "react"; import { getCredentials } from "../workflow/lib/config"; +import { BACKEND_URL } from "@repo/common/zod"; + +export const useCredentials = (type: string, workflowId?: string): any => { + const [cred, setCred] = useState([]); + const [authUrl, setAuthUrl] = useState(null); -export const useCredentials = (type: string): any => { - const [cred, setCred] = useState(); - const [authUrl, setAuthUrl] = useState(); useEffect(() => { const fetchCred = async () => { try { - if (!type) return {}; + // Clear credentials and authUrl when type is empty to prevent leaking credentials + if (!type) { + setCred([]); + setAuthUrl(null); + return; + } + const response = await getCredentials(type); - if (response) { - console.log(typeof response); - if (typeof response === "string") setAuthUrl(response); - else setCred(response); - // console.log(response[0].nodeId) - console.log(response); - return cred; - } else return {}; + // Backend should ONLY return stored credentials + if (Array.isArray(response)) { + setCred(response); + } else { + setCred([]); + } + + // Frontend defines where to redirect for OAuth + if (type === "google") { + const baseUrl = `${BACKEND_URL}/oauth/google/initiate`; + const url = workflowId ? `${baseUrl}?workflowId=${workflowId}` : baseUrl; + setAuthUrl(url); + } else { + setAuthUrl(null); + } } catch (e) { console.log( e instanceof Error ? e.message - : "unknow error from useCredentials hook" + : "unknown error from useCredentials hook" ); + // Clear state on error to prevent stale data + setCred([]); + setAuthUrl(null); } }; fetchCred(); - }, [type]); + }, [type, workflowId]); + return { cred, authUrl }; }; diff --git a/apps/web/app/workflows/[id]/components/ConfigModal.tsx b/apps/web/app/workflows/[id]/components/ConfigModal.tsx index d8cb98a..9247e95 100644 --- a/apps/web/app/workflows/[id]/components/ConfigModal.tsx +++ b/apps/web/app/workflows/[id]/components/ConfigModal.tsx @@ -4,12 +4,14 @@ import { useEffect, useState } from "react"; import { HOOKS_URL } from "@repo/common/zod"; import { useAppSelector } from "@/app/hooks/redux"; import { toast } from "sonner"; -import { api } from "@/app/lib/api"; +import { useCredentials } from "@/app/hooks/useCredential"; + interface ConfigModalProps { isOpen: boolean; selectedNode: any | null; onClose: () => void; onSave: (selectedNode: string, config: any, userId: string) => Promise; + workflowId?: string; } export default function ConfigModal({ @@ -17,41 +19,33 @@ export default function ConfigModal({ selectedNode, onClose, onSave, + workflowId, }: ConfigModalProps) { const [config, setConfig] = useState>({}); - const [credentials, setCredentials] = useState([]); const [loading, setLoading] = useState(false); const userId = useAppSelector((state) => state.user.userId) as string; - // Fetch credentials only when required on node change + // Fetch credentials with hook based on node config (google, etc) if appropriate + let credType: string | null = null; + if (selectedNode) { + const nodeConfig = getNodeConfig(selectedNode.name || selectedNode.actionType); + if (nodeConfig && nodeConfig.credentials) credType = nodeConfig.credentials; + } + const { cred: credentials = [], authUrl } = useCredentials(credType ?? "", workflowId); + useEffect(() => { - setConfig({}); // Clear form when switching nodes - setCredentials([]); // Clear credentials on node switch - if (selectedNode) { - const nodeConfig = getNodeConfig(selectedNode.name || selectedNode.actionType); - if (nodeConfig && nodeConfig.credentials === "google") { - api.Credentials.getCredentials("google").then((res) => { - setCredentials(res || []); - }).catch((error) => { - console.error("Failed to fetch credentials:", error); - setCredentials([]); - }); - } - } - // Removed console log pollution + setConfig({}); + // We no longer set local credentials here; handled by useCredentials! }, [selectedNode]); - // No modal if not needed if (!isOpen || !selectedNode) return null; - // Save callback const handleSave = async () => { setLoading(true); try { await onSave(selectedNode.id, config, userId); toast.success("Configured Successfully"); - } catch (error) { - console.error("Save failed:", error); + } catch { toast.error("Failed to save config"); } finally { setLoading(false); @@ -59,35 +53,18 @@ export default function ConfigModal({ } }; - // Field rendering helper const renderField = (field: any) => { const fieldValue = config[field.name] || ""; - // Special handling for Google credential dropdown if (field.type === "dropdown" && field.name === "credentialId") { + // Use the values from useCredentials: credentials and authUrl return (
- {/* No credentials connected */} - {credentials.length === 0 ? ( - <> - -

- Connect your Google account to use Gmail & Sheets -

- - ) : ( + {(Array.isArray(credentials) && credentials.length > 0) ? ( <>