From 3a6ee723066fc9e1ebdd7f535c7fe7fec76e03f2 Mon Sep 17 00:00:00 2001
From: Vamsi_0
Date: Fri, 16 Jan 2026 19:17:45 +0530
Subject: [PATCH] feat: enhance Google OAuth integration and improve
environment variable handling
- Added dotenv configuration to load environment variables from a .env file, ensuring proper overrides.
- Updated Google OAuth routes to use '/oauth/google' instead of '/auth/google' for better clarity.
- Enhanced logging for OAuth configuration and callback processes, including detailed output of redirect URIs and credentials.
- Improved credential fetching logic in the frontend to handle workflow IDs and provide appropriate authentication URLs.
- Refactored ConfigModal to utilize the new credential fetching mechanism, streamlining the user experience for connecting Google accounts.
---
apps/http-backend/src/index.ts | 15 +-
.../src/routes/google_callback.ts | 138 ++++++++++++++++--
.../src/routes/userRoutes/userRoutes.ts | 33 +++--
apps/web/app/hooks/useCredential.ts | 47 ++++--
.../workflows/[id]/components/ConfigModal.tsx | 98 ++++++-------
apps/web/app/workflows/[id]/page.tsx | 1 +
.../nodes/src/common/google-oauth-service.ts | 4 +-
7 files changed, 245 insertions(+), 91 deletions(-)
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) ? (
<>
);
}
- // Generic dropdown
if (field.type === "dropdown") {
return (
-
{/* Dynamic Form Block */}
{(() => {
const nodeConfig = getNodeConfig(
@@ -261,7 +259,6 @@ export default function ConfigModal({
);
}
-
if ((nodeConfig.fields || []).length === 0) {
return (
@@ -331,7 +328,6 @@ export default function ConfigModal({
);
}
-
return (
{nodeConfig.fields.map(renderField)}
@@ -357,7 +353,7 @@ export default function ConfigModal({