mirror of
https://github.com/stackblitz-labs/bolt.diy
synced 2025-06-26 18:26:38 +00:00
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.
This commit is contained in:
parent
02974089de
commit
bc7e2c5821
@ -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,
|
||||
|
@ -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 (
|
||||
<div className="relative">
|
||||
<div className="flex border border-bolt-elements-borderColor rounded-md overflow-hidden mr-2 text-sm">
|
||||
|
@ -16,6 +16,10 @@ export interface StreamingOptions extends Omit<Parameters<typeof _streamText>[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();
|
||||
|
||||
|
@ -8,6 +8,10 @@ export interface PromptOptions {
|
||||
supabase?: {
|
||||
isConnected: boolean;
|
||||
hasSelectedProject: boolean;
|
||||
credentials?: {
|
||||
anonKey?: string;
|
||||
supabaseUrl?: string;
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -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:
|
||||
|
@ -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'));
|
||||
},
|
||||
};
|
||||
}
|
||||
|
@ -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<SupabaseConnectionState>(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<SupabaseConnectionState>) {
|
||||
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<SupabaseConnectionS
|
||||
if (selectedProject) {
|
||||
connection.project = selectedProject;
|
||||
} else {
|
||||
// If project not found in stats but ID is provided, set a minimal project object
|
||||
connection.project = {
|
||||
id: connection.selectedProjectId,
|
||||
name: `Project ${connection.selectedProjectId.substring(0, 8)}...`,
|
||||
@ -80,8 +77,8 @@ export function updateSupabaseConnection(connection: Partial<SupabaseConnectionS
|
||||
};
|
||||
}
|
||||
} else if (connection.selectedProjectId === '') {
|
||||
// Clear the project when selectedProjectId is empty
|
||||
connection.project = undefined;
|
||||
connection.credentials = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
@ -89,13 +86,19 @@ export function updateSupabaseConnection(connection: Partial<SupabaseConnectionS
|
||||
supabaseConnection.set(newState);
|
||||
|
||||
/*
|
||||
* Always save the connection state to localStorage to persist across chats
|
||||
* Always save the connection state to localStorage to persist across chats
|
||||
*/
|
||||
if (connection.user || connection.token || connection.selectedProjectId !== undefined) {
|
||||
if (connection.user || connection.token || connection.selectedProjectId !== undefined || connection.credentials) {
|
||||
localStorage.setItem('supabase_connection', JSON.stringify(newState));
|
||||
|
||||
if (newState.credentials) {
|
||||
localStorage.setItem('supabaseCredentials', JSON.stringify(newState.credentials));
|
||||
} else {
|
||||
localStorage.removeItem('supabaseCredentials');
|
||||
}
|
||||
} else {
|
||||
localStorage.removeItem('supabase_connection');
|
||||
localStorage.removeItem('supabaseCredentials');
|
||||
}
|
||||
}
|
||||
|
||||
@ -103,23 +106,26 @@ export async function fetchSupabaseStats(token: string) {
|
||||
isFetchingStats.set(true);
|
||||
|
||||
try {
|
||||
const response = await fetch('https://api.supabase.com/v1/projects', {
|
||||
// Use the internal API route instead of direct Supabase API call
|
||||
const response = await fetch('/api/supabase', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
token,
|
||||
}),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to fetch projects');
|
||||
}
|
||||
|
||||
const projects = (await response.json()) as any;
|
||||
const data = (await response.json()) as any;
|
||||
|
||||
updateSupabaseConnection({
|
||||
stats: {
|
||||
projects,
|
||||
totalProjects: projects.length,
|
||||
},
|
||||
user: data.user,
|
||||
stats: data.stats,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch Supabase stats:', error);
|
||||
@ -128,3 +134,49 @@ export async function fetchSupabaseStats(token: string) {
|
||||
isFetchingStats.set(false);
|
||||
}
|
||||
}
|
||||
|
||||
export async function fetchProjectApiKeys(projectId: string, token: string) {
|
||||
isFetchingApiKeys.set(true);
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/supabase/variables', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
projectId,
|
||||
token,
|
||||
}),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to fetch API keys');
|
||||
}
|
||||
|
||||
const data = (await response.json()) as any;
|
||||
const apiKeys = data.apiKeys;
|
||||
|
||||
const anonKey = apiKeys.find((key: SupabaseApiKey) => 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);
|
||||
}
|
||||
}
|
||||
|
@ -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',
|
||||
|
@ -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<string, SupabaseProject>();
|
||||
|
||||
// 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({
|
||||
|
33
app/routes/api.supabase.variables.ts
Normal file
33
app/routes/api.supabase.variables.ts
Normal file
@ -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 });
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user