Skip to content
New issue

Have a question about this project? # for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “#”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? # to your account

Security review, other small improvements / fixes #2

Merged
merged 18 commits into from
Mar 21, 2025
Merged
Show file tree
Hide file tree
Changes from 14 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
4 changes: 4 additions & 0 deletions .eslintrc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Adding ESLint

"root": true,
"extends": "next/core-web-vitals"
}
1 change: 1 addition & 0 deletions .npmrc
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
strict-peer-dependencies=false
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This solves the peer dependency issue with react for anyone who uses this repo

40 changes: 13 additions & 27 deletions app/accounts/page.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
"use client"

import { useState, useEffect } from "react"
import { useState, useEffect, useCallback } from "react"
import { useAuth } from "@clerk/nextjs"
import { useRouter } from "next/navigation"
import { Button } from "@/components/ui/button"
Expand Down Expand Up @@ -33,17 +33,11 @@ export default function AccountsPage() {
const router = useRouter()
const debugMode = process.env.NEXT_PUBLIC_DEBUG_MODE === 'true'

const fetchAccounts = async () => {
const fetchAccounts = useCallback(async () => {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Next convention

setLoading(true)
try {
// First check if our environment variables are correctly configured
const envCheck = await fetch("/api/check-env")
const envData = await envCheck.json()

// Only log in debug mode
if (debugMode) {
console.log("Environment check:", envData)

// Log current user information from Clerk's client-side hooks
console.log("Current user from Clerk client:", {
userId,
Expand Down Expand Up @@ -99,12 +93,15 @@ export default function AccountsPage() {
} finally {
setLoading(false)
}
}
}, [userId, debugMode, isLoaded, setLoading, setAccounts])

const deleteAccount = async (accountId: string) => {
setDeletingId(accountId)
try {
const response = await fetch(`/api/accounts?id=${accountId}`, {
// Import the enhanced fetch with CSRF protection
const { fetchWithCSRF } = await import('@/lib/fetch-with-csrf')

const response = await fetchWithCSRF(`/api/accounts?id=${accountId}`, {
method: "DELETE",
})

Expand Down Expand Up @@ -138,7 +135,7 @@ export default function AccountsPage() {
// Only fetch accounts if user is authenticated
fetchAccounts()
}
}, [isLoaded, userId])
}, [isLoaded, userId, fetchAccounts])

// Show loading state while clerk auth is loading
if (!isLoaded) {
Expand Down Expand Up @@ -171,23 +168,12 @@ export default function AccountsPage() {
// Helper function to test Pipedream API directly - only shown in debug mode
const testPipedreamApi = async () => {
try {
// First test connection through the check-env endpoint
const response = await fetch(`/api/check-env?includePipedreamTest=true`)
const data = await response.json()

if (debugMode) {
console.log("Pipedream API test result:", data)

// Log current user info from client side
console.log("Current user info from client:", {
clerkUserId: userId,
externalUserId: data.pipedream?.externalUserId ? "Set in ENV" : "Using Clerk ID"
})

// Then try the accounts endpoint explicitly
// Then try the accounts endpoint directly
console.log("Testing accounts endpoint directly...")
}

// Use existing fetch without CSRF since this is a GET request
const accountsResponse = await fetch("/api/accounts")
const accountsData = await accountsResponse.json()

Expand All @@ -201,8 +187,8 @@ export default function AccountsPage() {

toast({
title: "Pipedream API test",
description: `Test ${data.pipedreamTest?.success ? 'succeeded' : 'failed'}. Found ${accountsCount} accounts. Check console for details.`,
variant: data.pipedreamTest?.success ? "default" : "destructive",
description: `Test completed. Found ${accountsCount} accounts. Check console for details.`,
variant: "default"
})
} catch (error) {
if (debugMode) {
Expand Down Expand Up @@ -278,7 +264,7 @@ export default function AccountsPage() {
<div className="text-center py-10">
<h3 className="text-lg font-medium mb-2">No connected accounts</h3>
<p className="text-muted-foreground mb-4">
You haven't connected any accounts yet.
You haven&apos;t connected any accounts yet.
</p>
<Button onClick={() => router.push("/")}>
Browse MCP servers
Expand Down
7 changes: 4 additions & 3 deletions app/api/accounts/route.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
import { NextRequest, NextResponse } from "next/server"
import { createBackendClient } from "@pipedream/sdk/server"
import { getAuth } from "@clerk/nextjs/server"
import { ProjectEnvironment } from "@/lib/utils"
import { getPipedreamExternalUserId } from "@/lib/clerk"

// Initialize Pipedream client
const getPipedreamClient = () => {
return createBackendClient({
apiHost: process.env.PIPEDREAM_API_HOST,
environment: process.env.PIPEDREAM_ENVIRONMENT || "development",
environment: (process.env.PIPEDREAM_ENVIRONMENT as ProjectEnvironment) || "development",
credentials: {
clientId: process.env.PIPEDREAM_OAUTH_CLIENT_ID || process.env.CLIENT_ID || "",
clientSecret: process.env.PIPEDREAM_OAUTH_CLIENT_SECRET || process.env.CLIENT_SECRET || "",
Expand Down Expand Up @@ -41,7 +42,7 @@ async function getExternalUserId(req: NextRequest) {

// Only log headers for debugging in debug mode
if (debugMode) {
const headers = {}
const headers: Record<string, string> = {}
req.headers.forEach((value, key) => {
if (!key.toLowerCase().includes('cookie') && !key.toLowerCase().includes('auth')) {
headers[key] = value
Expand Down Expand Up @@ -176,7 +177,7 @@ export async function GET(request: NextRequest) {
// Log credentials being used (exclude sensitive values)
if (debugMode) {
console.log("Using Pipedream client with:", {
environment: process.env.PIPEDREAM_ENVIRONMENT || "development",
environment: (process.env.PIPEDREAM_ENVIRONMENT as ProjectEnvironment) || "development",
hasClientId: !!process.env.PIPEDREAM_OAUTH_CLIENT_ID,
hasClientSecret: !!process.env.PIPEDREAM_OAUTH_CLIENT_SECRET,
projectId: process.env.PIPEDREAM_PROJECT_ID,
Expand Down
104 changes: 21 additions & 83 deletions app/api/actions/[slug]/route.ts
Original file line number Diff line number Diff line change
@@ -1,88 +1,26 @@
import { NextResponse } from "next/server"
import { supabase } from "@/lib/supabase"

export async function GET(request: Request, { params }: { params: { slug: string } }) {
const slug = params.slug

if (!slug) {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Moved lots of code into lib (see imports above)

return NextResponse.json({ error: "App slug is required" }, { status: 400 })
}

import { NextRequest } from 'next/server'
import { secureJsonResponse } from "@/lib/security"
import { getAppActionsBySlug } from "@/lib/services/apps"

// Define a more specific type for the context parameter
export async function GET(
request: NextRequest,
{ params }: { params: Promise<{ slug: string }> }
) {
const { slug } = await params
try {
console.log(`Fetching actions for app with slug: ${slug}`)

// First, get the app ID from the slug
const { data: appData, error: appError } = await supabase
.from("apps")
.select("APP_ID")
.eq("APP_NAME_SLUG", slug)
.single()

if (appError || !appData) {
console.error("Error fetching app ID:", appError)
return NextResponse.json({ error: "App not found", details: appError?.message }, { status: 404 })
}

const appId = appData.APP_ID
console.log(`Found app ID: ${appId} for slug: ${slug}`)

// Now fetch components using the app_id
// Try with APP_ID (uppercase, matching the apps table convention)
let { data: actions, error } = await supabase.from("published_components").select("*").eq("APP_ID", appId)

if (error) {
console.error("Error with APP_ID, trying lowercase app_id:", error)

// If that fails, try with app_id (lowercase)
const { data: actionsLowercase, error: errorLowercase } = await supabase
.from("published_components")
.select("*")
.eq("app_id", appId)

if (errorLowercase) {
console.error("Error with both APP_ID and app_id:", errorLowercase)

// As a last resort, let's try to get a sample row to see the structure
const { data: sampleData, error: sampleError } = await supabase
.from("published_components")
.select("*")
.limit(1)

if (!sampleError && sampleData && sampleData.length > 0) {
console.log("Sample published_components columns:", Object.keys(sampleData[0]))
console.log("Sample published_components data:", JSON.stringify(sampleData[0]))
}

return NextResponse.json({ error: "Failed to fetch actions", details: errorLowercase.message }, { status: 500 })
}

actions = actionsLowercase
}

console.log(`Found ${actions?.length || 0} actions for app ID ${appId}`)

// Log the first action to see its structure
if (actions && actions.length > 0) {
console.log("First action structure:", Object.keys(actions[0]))
console.log("First action data:", JSON.stringify(actions[0]))
}

// Ensure we're returning a valid array
const safeActions = Array.isArray(actions) ? actions : []

return NextResponse.json({
actions: safeActions,
count: safeActions.length,
})
const result = await getAppActionsBySlug(slug)
return secureJsonResponse(result)
} catch (error) {
console.error("Error in actions API:", error instanceof Error ? error.message : String(error))
return NextResponse.json(
{
error: "Failed to fetch actions",
details: error instanceof Error ? error.message : String(error),
console.error("Error fetching app actions:", error)
const statusCode = (error as any)?.message?.includes("App not found") ? 404 : 500

return secureJsonResponse(
{
error: "Failed to fetch actions",
details: String(error)
},
{ status: 500 },
{ status: statusCode }
)
}
}

}
Loading