Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -52,3 +52,4 @@ packages/common/tsconfig.tsbuildinfo
packages/db/tsconfig.tsbuildinfo
apps/http-backend/tsconfig.tsbuildinfo
packages/nodes/tsconfig.tsbuildinfo
.agents/rules/code-mentor.md
284 changes: 123 additions & 161 deletions apps/http-backend/src/routes/google_callback.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ 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";
const FRONTEND_URL = process.env.FRONTEND_URL || 'http://localhost:3000';
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)");
Expand All @@ -35,34 +36,23 @@ googleAuth.get(
// const userId = req.user?.id || "test_user"; // Get from auth middleware
const userId = req.user?.id;
const workflowId = req.query.workflowId as string | undefined;
const authType = req.query.authType as string;

// Ensure redirect URI matches Google Cloud Console configuration
const redirectUri = REDIRECT_URI;

const redirectUri = (req.query.redirect_url as string) || (workflowId ? `/workflows/${workflowId}` : '/workflows');
console.log(` redirect from google calback before auth: ${redirectUri}`)
if (!authType) return res.status(statusCodes.BAD_REQUEST).json({
error: "Auth Type is required",
success: false,
})

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,
redirectUri
);

const authUrl = oauth2.generateAuthUrl({
access_type: "offline",
scope: [
"https://www.googleapis.com/auth/spreadsheets",
"https://www.googleapis.com/auth/drive.readonly",
"https://www.googleapis.com/auth/gmail.send",
"https://www.googleapis.com/auth/gmail.readonly",
],
state: state,
prompt: "consent",
});
const authService = new GoogleOAuthService();
const authUrl = authService.getAuthUrl(userId!, authType, redirectUri)
Comment on lines 36 to +55

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🔴 Critical | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Confirm whether req.user carries `.id` or only `.sub`
rg -n --type=ts -C2 'req\.user(\?)?\.(id|sub)\b' apps/http-backend/src
rg -n --type=ts 'interface JwtPayload' -C5

Repository: Dev-Pross/BuildFlow

Length of output: 8964


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== candidate files =="
fd -i 'google_callback|auth|middleware|jwt|prisma|google_oauth|oauth' apps/http-backend/src -t f

echo
echo "== google_callback outline =="
ast-grep outline apps/http-backend/src/routes/google_callback.ts || true

echo
echo "== google_callback relevant lines =="
sed -n '1,180p' apps/http-backend/src/routes/google_callback.ts

echo
echo "== auth request/middlewares definitions =="
rg -n --type=ts -C3 'AuthRequest|userMiddleware|verify|jwt|jwtDecode|JWT' apps/http-backend/src | head -n 240

Repository: Dev-Pross/BuildFlow

Length of output: 25815


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== userMiddleware.ts full =="
cat -n apps/http-backend/src/routes/userRoutes/userMiddleware.ts

echo
echo "== package token/auth deps (focused) =="
rg 'next-auth|jsonwebtoken|jsonwebtoken|`@types/jsonwebtoken`' apps/http-backend -g 'package.json' -g 'pnpm-lock.yaml' -g 'yarn.lock' -g 'package-lock.json' || true

echo
echo "== GoogleOAuthService occurrences =="
fd -p 'google|oauth|auth' src -t f -i | sed 's#^`#apps/http-backend/src/`#' || true
rg -n --type=ts 'class GoogleOAuthService|saveCredentials|getAuthUrl|generateAuthUrl|secret' apps -C3 || true

Repository: Dev-Pross/BuildFlow

Length of output: 4555


🌐 Web query:

next-auth JWT token default sub field jwtPayload id

💡 Result:

In NextAuth.js (Auth.js), the sub (subject) field in the JWT payload is the standard, built-in field used to store the unique user identifier [1][2]. Key details regarding the sub field and JWT payload: 1. Default Behavior: The sub field is part of the DefaultJWT interface [1][3]. When a user signs in, NextAuth.js automatically maps the user's unique ID to the sub claim in the JWT [4][5]. 2. Persistence: Because sub is included by default, it is available in the jwt callback [6]. You can access it via token.sub [5]. 3. Customization: While sub is the standard location for the user ID, it is not uncommon for developers to explicitly add the user's ID to a different custom property (e.g., token.id or token.uid) within the jwt callback to simplify access or maintain specific naming conventions [6][5]. 4. Accessing the ID: - To use the default: Access token.sub [5]. - To add a custom ID field: javascript async jwt({ token, user }) { if (user) { token.id = user.id; // Adds 'id' to the JWT payload } return token; } 5. Client-side Exposure: By default, the sub field is often omitted from the session object returned to the client for security reasons [6]. To make the ID (whether stored in sub or a custom field) available to the client, you must explicitly forward it in the session callback [6][5]: javascript async session({ session, token }) { if (session.user) { session.user.id = token.sub; // Or token.id } return session; } In summary, sub is the intended field for the user ID in the JWT payload [2][5], but you have full flexibility to store it elsewhere or map it to other properties via the jwt and session callbacks [6].

Citations:


Use the authenticated user id from req.user.sub.

This middleware assigns the NextAuth JWT payload to req.user, whose subject claim is sub; id is not set by the existing code. Since userId is force-unwrapped and encoded into the OAuth state, a missing sub leaves the callback without the user id to persist credentials.

🐛 Proposed fix
-    const userId = req.user?.id;
+    const userId = req.user?.sub;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// const userId = req.user?.id || "test_user"; // Get from auth middleware
const userId = req.user?.id;
const workflowId = req.query.workflowId as string | undefined;
const authType = req.query.authType as string;
// Ensure redirect URI matches Google Cloud Console configuration
const redirectUri = REDIRECT_URI;
const redirectUri = (req.query.redirect_url as string) || (workflowId ? `/workflows/${workflowId}` : '/workflows');
console.log(` redirect from google calback before auth: ${redirectUri}`)
if (!authType) return res.status(statusCodes.BAD_REQUEST).json({
error: "Auth Type is required",
success: false,
})
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,
redirectUri
);
const authUrl = oauth2.generateAuthUrl({
access_type: "offline",
scope: [
"https://www.googleapis.com/auth/spreadsheets",
"https://www.googleapis.com/auth/drive.readonly",
"https://www.googleapis.com/auth/gmail.send",
"https://www.googleapis.com/auth/gmail.readonly",
],
state: state,
prompt: "consent",
});
const authService = new GoogleOAuthService();
const authUrl = authService.getAuthUrl(userId!, authType, redirectUri)
// const userId = req.user?.id || "test_user"; // Get from auth middleware
const userId = req.user?.sub;
const workflowId = req.query.workflowId as string | undefined;
const authType = req.query.authType as string;
// Ensure redirect URI matches Google Cloud Console configuration
const redirectUri = (req.query.redirect_url as string) || (workflowId ? `/workflows/${workflowId}` : '/workflows');
console.log(` redirect from google calback before auth: ${redirectUri}`)
if (!authType) return res.status(statusCodes.BAD_REQUEST).json({
error: "Auth Type is required",
success: false,
})
console.log("🔐 OAuth Initiate - Redirect URI:", redirectUri);
console.log("🔐 OAuth Initiate - User ID:", userId);
console.log("🔐 OAuth Initiate - Workflow ID:", workflowId || "NOT PROVIDED");
const authService = new GoogleOAuthService();
const authUrl = authService.getAuthUrl(userId!, authType, redirectUri)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/http-backend/src/routes/google_callback.ts` around lines 36 - 55, Update
the userId assignment in the Google OAuth initiation flow to read the
authenticated subject from req.user.sub instead of req.user.id. Keep the
existing force-unwrapped value passed to GoogleOAuthService.getAuthUrl, ensuring
the authenticated user identifier is encoded into the OAuth state.


console.log("🔐 OAuth Initiate - Generated Auth URL:", authUrl);
return res.redirect(authUrl);
Expand All @@ -76,163 +66,135 @@ googleAuth.get(
console.log("Request recieved to the callback from fronted ")
const code = req.query.code;
const state = req.query.state;
const Oauth = new GoogleOAuthService();
if (!code || typeof code !== "string") {
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);
const authService = new GoogleOAuthService()

const { userId, type, redirect_uri } = JSON.parse(Buffer.from(state as string, 'base64').toString())
console.log("🔐 OAuth Callback - Redirect URI:", FRONTEND_URL + redirect_uri);
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,
redirectUri
);

try {
Comment on lines 69 to 80

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

state is decoded outside the try block — malformed/missing state crashes unhandled.

code is validated before use, but state is not: if state is absent or not valid base64/JSON, Buffer.from/JSON.parse on Line 75 throws before the surrounding try (Line 80) can catch it, bypassing the intended graceful redirect-on-error flow.

🛡️ Proposed fix
+    if (!state || typeof state !== "string") {
+      return res.status(statusCodes.BAD_REQUEST).json({ error: "Missing or invalid state" });
+    }
+
     const authService = new GoogleOAuthService()
-
-    const { userId, type, redirect_uri } = JSON.parse(Buffer.from(state as string, 'base64').toString())
+    let userId: string, type: string, redirect_uri: string;
+    try {
+      ({ userId, type, redirect_uri } = JSON.parse(Buffer.from(state, 'base64').toString()));
+    } catch {
+      return res.status(statusCodes.BAD_REQUEST).json({ error: "Invalid state payload" });
+    }
     console.log("🔐 OAuth Callback - Redirect URI:", FRONTEND_URL + redirect_uri);

This also removes the need for the redundant re-decode in the catch block below (Line 110), since redirect_uri would already be in scope.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (!code || typeof code !== "string") {
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);
const authService = new GoogleOAuthService()
const { userId, type, redirect_uri } = JSON.parse(Buffer.from(state as string, 'base64').toString())
console.log("🔐 OAuth Callback - Redirect URI:", FRONTEND_URL + redirect_uri);
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,
redirectUri
);
try {
if (!code || typeof code !== "string") {
return res.json({ error: "Missing or invalid authorization code" });
}
if (!state || typeof state !== "string") {
return res.status(statusCodes.BAD_REQUEST).json({ error: "Missing or invalid state" });
}
const authService = new GoogleOAuthService()
let userId: string, type: string, redirect_uri: string;
try {
({ userId, type, redirect_uri } = JSON.parse(Buffer.from(state, 'base64').toString()));
} catch {
return res.status(statusCodes.BAD_REQUEST).json({ error: "Invalid state payload" });
}
console.log("🔐 OAuth Callback - Redirect URI:", FRONTEND_URL + redirect_uri);
console.log("🔐 OAuth Callback - Received code:", code ? "✅ Present" : "❌ Missing");
try {
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/http-backend/src/routes/google_callback.ts` around lines 69 - 80, Move
the state decoding and JSON parsing into the existing try block around the
Google OAuth callback flow so missing or malformed state follows the intended
error redirect path. Keep redirect_uri scoped for both success and catch
handling, and remove the catch block’s redundant state re-decode by reusing the
parsed value.

const { tokens } = await oauth2.getToken(code);

// DEBUG: Log tokens received from Google
console.log('\n🔐 Google OAuth Callback - Tokens received:');
console.log(' access_token:', tokens.access_token ? '✅ Present' : '❌ Missing');
console.log(' refresh_token:', tokens.refresh_token ? '✅ Present' : '❌ Missing');
console.log(' expiry_date:', tokens.expiry_date);
console.log(' token_type:', tokens.token_type);
console.log(' scope:', tokens.scope);
if (!tokens.refresh_token) {
console.warn('⚠️ WARNING: No refresh_token received! User may have already authorized this app.');
console.warn(' To force new refresh_token, user needs to revoke access at: https://myaccount.google.com/permissions');
}

// 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');
}
const tokens = await authService.getTokens(code);

// DEBUG: Log tokens received from Google
console.log('\n🔐 Google OAuth Callback - Tokens received:');
console.log(' access_token:', tokens.access_token ? '✅ Present' : '❌ Missing');
console.log(' refresh_token:', tokens.refresh_token ? '✅ Present' : '❌ Missing');
console.log(' expiry_date:', tokens.expiry_date);
console.log(' token_type:', tokens.token_type);
console.log(' scope:', tokens.scope);

if (!tokens.refresh_token) {
console.warn('⚠️ WARNING: No refresh_token received! User may have already authorized this app.');
console.warn(' To force new refresh_token, user needs to revoke access at: https://myaccount.google.com/permissions');
}

// Save tokens to database if userId is provided
if (userId) {
console.log(' Saving tokens for userId:', userId);
await authService.saveCredentials(userId, tokens as OAuthTokens, type)
console.log(' ✅ Tokens saved to database');
}
Comment on lines +74 to +101

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟠 Major | 🏗️ Heavy lift

state.userId is never verified against the authenticated session — unsigned state used for an authorization decision.

The base64 state payload is not signed/HMAC'd, and req.user (populated by userMiddleware) is never cross-checked against state.userId. An authenticated attacker can complete their own Google consent flow, tamper with state to substitute a victim's userId, and hit this callback to have saveCredentials(victimUserId, attackerTokens, type) create a new credential row owned by the victim but containing the attacker's tokens. Depending on downstream credential-selection logic (getCredentials orders by id desc), this can redirect a victim's future Sheets/Gmail node executions to attacker-controlled resources.

Bind the decoded userId to the authenticated session before persisting credentials.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/http-backend/src/routes/google_callback.ts` around lines 74 - 101,
Validate the decoded state.userId against the authenticated user from req.user
before calling authService.saveCredentials in the OAuth callback. Reject the
callback when the IDs are missing or do not match, and only persist tokens for
the authenticated session; do not use the unsigned state.userId alone for
authorization.


// Redirect to workflow page if workflowId is provided, otherwise to general workflow page
const redirectUrl = workflowId
? `http://localhost:3000/workflows/${workflowId}`
: "http://localhost:3000/workflows";
console.log(' Redirecting to:', redirectUrl);
return res.redirect(redirectUrl);

console.log(' Redirecting to:', redirect_uri);
return res.redirect(`${FRONTEND_URL}${redirect_uri}`);
} catch (err: any) {
console.error("Google token exchange error:", err);
// 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);
const { redirect_uri } = JSON.parse(Buffer.from(state as string, 'base64').toString());
return res.redirect(`${FRONTEND_URL}${redirect_uri}`);
}
})
})

// 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(statusCodes.INTERNAL_SERVER_ERROR).json({
error: err instanceof Error ? err.message : 'Unknown error',
stack: err instanceof Error ? err.stack : undefined
});
}
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(statusCodes.INTERNAL_SERVER_ERROR).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 {
const { prismaClient } = await import('@repo/db/client');
const credentials = await prismaClient.credential.findMany({
where: { type: 'google_oauth' },
select: {
id: true,
userId: true,
type: true,
config: true
}
});

const debugInfo = credentials.map(cred => {
const config = cred.config as any;
return {
id: cred.id,
userId: cred.userId,
hasAccessToken: !!config?.access_token,
hasRefreshToken: !!config?.refresh_token,
refreshTokenLength: config?.refresh_token?.length || 0,
expiryDate: config?.expiry_date,
expiresIn: config?.expiry_date ? Math.round((config.expiry_date - Date.now()) / 1000 / 60) + ' minutes' : 'N/A',
isInvalid: config?.invalid || false,
scope: config?.scope
};
});

console.log('\n📋 Stored Credentials Debug:');
console.table(debugInfo);

return res.json({ credentials: debugInfo });
} catch (err) {
return res.status(statusCodes.INTERNAL_SERVER_ERROR).json({ error: err instanceof Error ? err.message : 'Unknown error' });
}
googleAuth.get('/debug/credentials', async (req: Request, res: Response) => {
try {
const { prismaClient } = await import('@repo/db/client');

const credentials = await prismaClient.credential.findMany({
where: { type: { in: ['gmail_oauth', 'gsheet_oauth', 'google_oauth'] } },
select: {
id: true,
userId: true,
type: true,
config: true
}
});

const debugInfo = credentials.map(cred => {
const config = cred.config as any;
return {
id: cred.id,
userId: cred.userId,
hasAccessToken: !!config?.access_token,
hasRefreshToken: !!config?.refresh_token,
refreshTokenLength: config?.refresh_token?.length || 0,
expiryDate: config?.expiry_date,
expiresIn: config?.expiry_date ? Math.round((config.expiry_date - Date.now()) / 1000 / 60) + ' minutes' : 'N/A',
isInvalid: config?.invalid || false,
scope: config?.scope
};
});

console.log('\n📋 Stored Credentials Debug:');
console.table(debugInfo);

return res.json({ credentials: debugInfo });
} catch (err) {
return res.status(statusCodes.INTERNAL_SERVER_ERROR).json({ error: err instanceof Error ? err.message : 'Unknown error' });
}
});
Comment on lines +116 to 200

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🔴 Critical | ⚡ Quick win

Debug endpoints have no auth — /debug/credentials publicly leaks credential metadata (userId, token presence/expiry/scope) for every user, now across a widened type set.

Neither /debug/config nor /debug/credentials apply userMiddleware (contrast with every other route in this file and userRoutes.ts). /debug/credentials is the more serious exposure: it returns userId, hasAccessToken, hasRefreshToken, refreshTokenLength, expiryDate, and scope for all gmail_oauth/gsheet_oauth/google_oauth credentials in the database to any unauthenticated caller. This should require auth (and likely be gated to admins/non-production only).

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/http-backend/src/routes/google_callback.ts` around lines 116 - 200,
Protect both the /debug/config and /debug/credentials handlers in googleAuth
with the existing userMiddleware, matching the authenticated route pattern used
elsewhere in the file and userRoutes.ts. Additionally restrict
/debug/credentials to an approved admin or non-production-only access path,
ensuring unauthenticated callers cannot retrieve credential metadata for any
user.

46 changes: 45 additions & 1 deletion apps/http-backend/src/routes/sheet.routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ sheetRouter.get(
const sheets = await sheetExecutor.getSheets({
userId: userId,
credentialId: credentialId,
authType: 'gsheet_oauth'
});
if ((sheets as any)?.success === false) {
return res.status(statusCodes.NOT_FOUND).json({
Expand Down Expand Up @@ -76,7 +77,7 @@ sheetRouter.get(
});
}
const sheets = await sheetExecutor.getSheetTabs(
{ userId: userId, credentialId: credentialId },
{ userId: userId, credentialId: credentialId, authType: 'gsheet_oauth' },
sheetId
);

Expand All @@ -101,3 +102,46 @@ sheetRouter.get(
}
}
);

sheetRouter.get(
"/getHeaders/:cred/:sheetId/:sheetName",
userMiddleware,
async (req: AuthRequest, res: Response) => {
try {
const userId = req.user?.sub;
if (!userId)
return res.status(statusCodes.UNAUTHORIZED).json({
message: "User not authorized"
})

const { cred: credentialId, sheetId, sheetName } = req.params;

if (!credentialId || !sheetId || !sheetName) {
return res.status(statusCodes.BAD_REQUEST).json({
message: "Missing required parameters"
})
}

const result = await sheetExecutor.getHeaderRow({
userId, credentialId, authType: 'gsheet_oauth'
}, sheetId, sheetName
)

if (!result.success)
return res.status(statusCodes.NOT_FOUND).json({
message: "Failed to fetch headers", error: result
});

return res.status(statusCodes.OK).json({
message: "Headers fetched successfully",
headers: result.output
});
}
catch (e) {
return res.status(statusCodes.INTERNAL_SERVER_ERROR).json({
message: "Error fetching sheet headers",
error: e instanceof Error ? e.message : "Unknown error"
});
}
}
)
Loading