From bc7e2c5821fd46dd0e477f61f02f002023808510 Mon Sep 17 00:00:00 2001 From: KevIsDev Date: Thu, 20 Mar 2025 11:17:27 +0000 Subject: [PATCH] feat(supabase): add credentials handling for Supabase API keys and URL This commit introduces the ability to fetch and store Supabase API keys and URL credentials when a project is selected. This enables the application to dynamically configure the Supabase connection environment variables, improving the integration with Supabase services. The changes include updates to the Supabase connection logic, new API endpoints, and modifications to the chat and prompt components to utilize the new credentials. --- app/components/chat/Chat.client.tsx | 4 + app/components/chat/SupabaseConnection.tsx | 9 ++- app/lib/.server/llm/stream-text.ts | 5 ++ app/lib/common/prompt-library.ts | 4 + app/lib/common/prompts/prompts.ts | 18 ++++- app/lib/hooks/useSupabaseConnection.ts | 44 ++++++++++- app/lib/stores/supabase.ts | 88 +++++++++++++++++----- app/routes/api.chat.ts | 5 +- app/routes/api.supabase.ts | 5 -- app/routes/api.supabase.variables.ts | 33 ++++++++ app/types/supabase.ts | 10 +++ 11 files changed, 192 insertions(+), 33 deletions(-) create mode 100644 app/routes/api.supabase.variables.ts diff --git a/app/components/chat/Chat.client.tsx b/app/components/chat/Chat.client.tsx index af5f2f44..4cf8c8ca 100644 --- a/app/components/chat/Chat.client.tsx +++ b/app/components/chat/Chat.client.tsx @@ -169,6 +169,10 @@ export const ChatImpl = memo( supabase: { isConnected: supabaseConn.isConnected, hasSelectedProject: !!selectedProject, + credentials: { + supabaseUrl: supabaseConn?.credentials?.supabaseUrl, + anonKey: supabaseConn?.credentials?.anonKey, + }, }, }, sendExtraMessageFields: true, diff --git a/app/components/chat/SupabaseConnection.tsx b/app/components/chat/SupabaseConnection.tsx index 2b03f442..6b2e34a0 100644 --- a/app/components/chat/SupabaseConnection.tsx +++ b/app/components/chat/SupabaseConnection.tsx @@ -21,11 +21,11 @@ export function SupabaseConnection() { handleCreateProject, updateToken, isConnected, + fetchProjectApiKeys, } = useSupabaseConnection(); const currentChatId = useStore(chatId); - // Add event listener for opening the connection dialog useEffect(() => { const handleOpenConnectionDialog = () => { setIsDialogOpen(true); @@ -38,7 +38,6 @@ export function SupabaseConnection() { }; }, [setIsDialogOpen]); - // Load the selected project from localStorage when connected or chat changes useEffect(() => { if (isConnected && currentChatId) { const savedProjectId = localStorage.getItem(`supabase-project-${currentChatId}`); @@ -70,6 +69,12 @@ export function SupabaseConnection() { } }, [isConnected, supabaseConn.token]); + useEffect(() => { + if (isConnected && supabaseConn.selectedProjectId && supabaseConn.token) { + fetchProjectApiKeys(supabaseConn.selectedProjectId).catch(console.error); + } + }, [isConnected, supabaseConn.selectedProjectId, supabaseConn.token]); + return (
diff --git a/app/lib/.server/llm/stream-text.ts b/app/lib/.server/llm/stream-text.ts index 1958dfad..7f5b4857 100644 --- a/app/lib/.server/llm/stream-text.ts +++ b/app/lib/.server/llm/stream-text.ts @@ -16,6 +16,10 @@ export interface StreamingOptions extends Omit[0] supabaseConnection?: { isConnected: boolean; hasSelectedProject: boolean; + credentials?: { + anonKey?: string; + supabaseUrl?: string; + }; }; } @@ -105,6 +109,7 @@ export async function streamText(props: { supabase: { isConnected: options?.supabaseConnection?.isConnected || false, hasSelectedProject: options?.supabaseConnection?.hasSelectedProject || false, + credentials: options?.supabaseConnection?.credentials || undefined, }, }) ?? getSystemPrompt(); diff --git a/app/lib/common/prompt-library.ts b/app/lib/common/prompt-library.ts index 084925b7..f4747d71 100644 --- a/app/lib/common/prompt-library.ts +++ b/app/lib/common/prompt-library.ts @@ -8,6 +8,10 @@ export interface PromptOptions { supabase?: { isConnected: boolean; hasSelectedProject: boolean; + credentials?: { + anonKey?: string; + supabaseUrl?: string; + }; }; } diff --git a/app/lib/common/prompts/prompts.ts b/app/lib/common/prompts/prompts.ts index 149f1b5f..eb7ec1a1 100644 --- a/app/lib/common/prompts/prompts.ts +++ b/app/lib/common/prompts/prompts.ts @@ -4,7 +4,11 @@ import { stripIndents } from '~/utils/stripIndent'; export const getSystemPrompt = ( cwd: string = WORK_DIR, - supabase?: { isConnected: boolean; hasSelectedProject: boolean }, + supabase?: { + isConnected: boolean; + hasSelectedProject: boolean; + credentials?: { anonKey?: string; supabaseUrl?: string }; + }, ) => ` You are Bolt, an expert AI assistant and exceptional senior software developer with vast knowledge across multiple programming languages, frameworks, and best practices. @@ -76,8 +80,16 @@ You are Bolt, an expert AI assistant and exceptional senior software developer w : '' : '' } - The environment variables for Supabase connection will be available in the project's \`.env\` file. - IMPORTANT: Create a .env file if it doesnt exist. + IMPORTANT: Create a .env file if it doesnt exist and include the following variables: + ${ + supabase?.isConnected && + supabase?.hasSelectedProject && + supabase?.credentials?.supabaseUrl && + supabase?.credentials?.anonKey + ? `VITE_SUPABASE_URL=${supabase.credentials.supabaseUrl} + VITE_SUPABASE_ANON_KEY=${supabase.credentials.anonKey}` + : 'SUPABASE_URL=your_supabase_url\nSUPABASE_ANON_KEY=your_supabase_anon_key' + } NEVER modify any Supabase configuration or \`.env\` files. CRITICAL DATA PRESERVATION AND SAFETY REQUIREMENTS: diff --git a/app/lib/hooks/useSupabaseConnection.ts b/app/lib/hooks/useSupabaseConnection.ts index 5bbe12ea..8a2f8118 100644 --- a/app/lib/hooks/useSupabaseConnection.ts +++ b/app/lib/hooks/useSupabaseConnection.ts @@ -2,21 +2,39 @@ import { useEffect, useState } from 'react'; import { toast } from 'react-toastify'; import { useStore } from '@nanostores/react'; import { logStore } from '~/lib/stores/logs'; -import { supabaseConnection, isConnecting, isFetchingStats, updateSupabaseConnection } from '~/lib/stores/supabase'; +import { + supabaseConnection, + isConnecting, + isFetchingStats, + isFetchingApiKeys, + updateSupabaseConnection, + fetchProjectApiKeys, +} from '~/lib/stores/supabase'; export function useSupabaseConnection() { const connection = useStore(supabaseConnection); const connecting = useStore(isConnecting); const fetchingStats = useStore(isFetchingStats); + const fetchingApiKeys = useStore(isFetchingApiKeys); const [isProjectsExpanded, setIsProjectsExpanded] = useState(false); const [isDropdownOpen, setIsDropdownOpen] = useState(false); useEffect(() => { const savedConnection = localStorage.getItem('supabase_connection'); + const savedCredentials = localStorage.getItem('supabaseCredentials'); if (savedConnection) { const parsed = JSON.parse(savedConnection); + + if (savedCredentials && !parsed.credentials) { + parsed.credentials = JSON.parse(savedCredentials); + } + updateSupabaseConnection(parsed); + + if (parsed.token && parsed.selectedProjectId && !parsed.credentials) { + fetchProjectApiKeys(parsed.selectedProjectId, parsed.token).catch(console.error); + } } }, []); @@ -50,7 +68,6 @@ export function useSupabaseConnection() { toast.success('Successfully connected to Supabase'); - // Keep the dialog open and expand the projects section setIsProjectsExpanded(true); return true; @@ -72,7 +89,7 @@ export function useSupabaseConnection() { setIsDropdownOpen(false); }; - const selectProject = (projectId: string) => { + const selectProject = async (projectId: string) => { const currentState = supabaseConnection.get(); let projectData = undefined; @@ -85,7 +102,18 @@ export function useSupabaseConnection() { project: projectData, }); - toast.success('Project selected successfully'); + if (projectId && currentState.token) { + try { + await fetchProjectApiKeys(projectId, currentState.token); + toast.success('Project selected successfully'); + } catch (error) { + console.error('Failed to fetch API keys:', error); + toast.error('Selected project but failed to fetch API keys'); + } + } else { + toast.success('Project selected successfully'); + } + setIsDropdownOpen(false); }; @@ -97,6 +125,7 @@ export function useSupabaseConnection() { connection, connecting, fetchingStats, + fetchingApiKeys, isProjectsExpanded, setIsProjectsExpanded, isDropdownOpen, @@ -107,5 +136,12 @@ export function useSupabaseConnection() { handleCreateProject, updateToken: (token: string) => updateSupabaseConnection({ ...connection, token }), isConnected: !!(connection.user && connection.token), + fetchProjectApiKeys: (projectId: string) => { + if (connection.token) { + return fetchProjectApiKeys(projectId, connection.token); + } + + return Promise.reject(new Error('No token available')); + }, }; } diff --git a/app/lib/stores/supabase.ts b/app/lib/stores/supabase.ts index 9f624e5b..3b8a5813 100644 --- a/app/lib/stores/supabase.ts +++ b/app/lib/stores/supabase.ts @@ -1,5 +1,5 @@ import { atom } from 'nanostores'; -import type { SupabaseUser, SupabaseStats } from '~/types/supabase'; +import type { SupabaseUser, SupabaseStats, SupabaseApiKey, SupabaseCredentials } from '~/types/supabase'; export interface SupabaseProject { id: string; @@ -22,10 +22,10 @@ export interface SupabaseConnectionState { stats?: SupabaseStats; selectedProjectId?: string; isConnected?: boolean; - project?: SupabaseProject; // Add the selected project data + project?: SupabaseProject; + credentials?: SupabaseCredentials; } -// Init from localStorage if available const savedConnection = typeof localStorage !== 'undefined' ? localStorage.getItem('supabase_connection') : null; const initialState: SupabaseConnectionState = savedConnection @@ -36,30 +36,28 @@ const initialState: SupabaseConnectionState = savedConnection stats: undefined, selectedProjectId: undefined, isConnected: false, - project: undefined, // Initialize as undefined + project: undefined, }; export const supabaseConnection = atom(initialState); -// After init, fetch stats if we have a token if (initialState.token && !initialState.stats) { fetchSupabaseStats(initialState.token).catch(console.error); } export const isConnecting = atom(false); export const isFetchingStats = atom(false); +export const isFetchingApiKeys = atom(false); export function updateSupabaseConnection(connection: Partial) { const currentState = supabaseConnection.get(); - // Set isConnected based on user presence AND token if (connection.user !== undefined || connection.token !== undefined) { const newUser = connection.user !== undefined ? connection.user : currentState.user; const newToken = connection.token !== undefined ? connection.token : currentState.token; connection.isConnected = !!(newUser && newToken); } - // Update the project data when selectedProjectId changes if (connection.selectedProjectId !== undefined) { if (connection.selectedProjectId && currentState.stats?.projects) { const selectedProject = currentState.stats.projects.find( @@ -69,7 +67,6 @@ export function updateSupabaseConnection(connection: Partial key.name === 'anon' || key.name === 'public'); + + if (anonKey) { + const supabaseUrl = `https://${projectId}.supabase.co`; + + updateSupabaseConnection({ + credentials: { + anonKey: anonKey.api_key, + supabaseUrl, + }, + }); + + return { anonKey: anonKey.api_key, supabaseUrl }; + } + + return null; + } catch (error) { + console.error('Failed to fetch project API keys:', error); + throw error; + } finally { + isFetchingApiKeys.set(false); + } +} diff --git a/app/routes/api.chat.ts b/app/routes/api.chat.ts index 6861d15d..5917dfc4 100644 --- a/app/routes/api.chat.ts +++ b/app/routes/api.chat.ts @@ -45,6 +45,10 @@ async function chatAction({ context, request }: ActionFunctionArgs) { supabase?: { isConnected: boolean; hasSelectedProject: boolean; + credentials?: { + anonKey?: string; + supabaseUrl?: string; + }; }; }>(); @@ -183,7 +187,6 @@ async function chatAction({ context, request }: ActionFunctionArgs) { // logger.debug('Code Files Selected'); } - // Stream the text const options: StreamingOptions = { supabaseConnection: supabase, toolChoice: 'none', diff --git a/app/routes/api.supabase.ts b/app/routes/api.supabase.ts index 56d9f810..955ae44e 100644 --- a/app/routes/api.supabase.ts +++ b/app/routes/api.supabase.ts @@ -7,7 +7,6 @@ export const action: ActionFunction = async ({ request }) => { return json({ error: 'Method not allowed' }, { status: 405 }); } - // Inside the action function try { const { token } = (await request.json()) as any; @@ -27,17 +26,14 @@ export const action: ActionFunction = async ({ request }) => { const projects = (await projectsResponse.json()) as SupabaseProject[]; - // Create a Map to store unique projects by ID const uniqueProjectsMap = new Map(); - // Only keep the latest version of each project for (const project of projects) { if (!uniqueProjectsMap.has(project.id)) { uniqueProjectsMap.set(project.id, project); } } - // Debug log to see unique projects console.log( 'Unique projects:', Array.from(uniqueProjectsMap.values()).map((p) => ({ id: p.id, name: p.name })), @@ -45,7 +41,6 @@ export const action: ActionFunction = async ({ request }) => { const uniqueProjects = Array.from(uniqueProjectsMap.values()); - // Sort projects by creation date (newest first) uniqueProjects.sort((a, b) => new Date(b.created_at).getTime() - new Date(a.created_at).getTime()); return json({ diff --git a/app/routes/api.supabase.variables.ts b/app/routes/api.supabase.variables.ts new file mode 100644 index 00000000..fd2d028f --- /dev/null +++ b/app/routes/api.supabase.variables.ts @@ -0,0 +1,33 @@ +import { json } from '@remix-run/node'; +import type { ActionFunctionArgs } from '@remix-run/node'; + +export async function action({ request }: ActionFunctionArgs) { + try { + // Add proper type assertion for the request body + const body = (await request.json()) as { projectId?: string; token?: string }; + const { projectId, token } = body; + + if (!projectId || !token) { + return json({ error: 'Project ID and token are required' }, { status: 400 }); + } + + const response = await fetch(`https://api.supabase.com/v1/projects/${projectId}/api-keys`, { + method: 'GET', + headers: { + Authorization: `Bearer ${token}`, + 'Content-Type': 'application/json', + }, + }); + + if (!response.ok) { + return json({ error: `Failed to fetch API keys: ${response.statusText}` }, { status: response.status }); + } + + const apiKeys = await response.json(); + + return json({ apiKeys }); + } catch (error) { + console.error('Error fetching project API keys:', error); + return json({ error: error instanceof Error ? error.message : 'Unknown error occurred' }, { status: 500 }); + } +} diff --git a/app/types/supabase.ts b/app/types/supabase.ts index d16d50c2..f99bbaf2 100644 --- a/app/types/supabase.ts +++ b/app/types/supabase.ts @@ -19,3 +19,13 @@ export interface SupabaseStats { projects: SupabaseProject[]; totalProjects: number; } + +export interface SupabaseApiKey { + name: string; + api_key: string; +} + +export interface SupabaseCredentials { + anonKey?: string; + supabaseUrl?: string; +}