Skip to content
Open
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
40 changes: 0 additions & 40 deletions ENVEXAMPLE

This file was deleted.

4 changes: 2 additions & 2 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,11 @@ services:
POSTGRES_PASSWORD: ${DB_PASSWORD}
POSTGRES_DB: ${DB_NAME}
ports:
- "${DB_PORT:-5432}:${DB_PORT:-5432}"
- "${DB_HOST_PORT:-5433}:5432"
volumes:
- postgres_data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres"]
test: ["CMD-SHELL", "pg_isready -U ${DB_USER}"]
interval: 10s
timeout: 5s
retries: 5
Expand Down
76 changes: 36 additions & 40 deletions src/components/api/ApiKey.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { useState, useEffect } from 'react';
import React, { useState } from 'react';
import {
Box,
Button,
Expand All @@ -17,6 +17,7 @@ import {
import { ContentCopy, Visibility, VisibilityOff, Delete } from '@mui/icons-material';
import styled from 'styled-components';
import axios from 'axios';
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { useGlobalInfoStore } from '../../context/globalInfo';
import { apiUrl } from '../../apiConfig';
import { useTranslation } from 'react-i18next';
Expand All @@ -31,55 +32,50 @@ const Container = styled(Box)`

const ApiKeyManager = () => {
const { t } = useTranslation();
const [apiKey, setApiKey] = useState<string | null>(null);
const [apiKeyName, setApiKeyName] = useState<string>(t('apikey.default_name'));
const [loading, setLoading] = useState<boolean>(true);
const [showKey, setShowKey] = useState<boolean>(false);
const [copySuccess, setCopySuccess] = useState<boolean>(false);
const { notify } = useGlobalInfoStore();
const queryClient = useQueryClient();

useEffect(() => {
const fetchApiKey = async () => {
try {
const { data } = await axios.get(`${apiUrl}/auth/api-key`);
setApiKey(data.api_key);
} catch (error: any) {
notify('error', t('apikey.notifications.fetch_error', { error: error.message }));
} finally {
setLoading(false);
}
};
// Fetch API key with React Query
const { data: apiKey, isLoading } = useQuery({
queryKey: ['api-key'],
queryFn: async () => {
const { data } = await axios.get(`${apiUrl}/auth/api-key`);
return data.api_key as string | null;
},
staleTime: 10 * 60 * 1000, // 10 minutes
});
Comment on lines +41 to +49
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Scope cache by user to prevent API key leakage across logins.

Using a global ['api-key'] with 10m staleTime can show a previous user’s API key after sign‑out/in in the same tab. Scope by user.id (and gate with enabled) and update mutations accordingly.

Apply:

-import React, { useState } from 'react';
+import React, { useState, useContext } from 'react';
 ...
-import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
+import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
+import { AuthContext } from '../../context/auth';
 ...
 const ApiKeyManager = () => {
   const { t } = useTranslation();
   const [apiKeyName, setApiKeyName] = useState<string>(t('apikey.default_name'));
   const [showKey, setShowKey] = useState<boolean>(false);
   const [copySuccess, setCopySuccess] = useState<boolean>(false);
   const { notify } = useGlobalInfoStore();
   const queryClient = useQueryClient();
+  const { state } = useContext(AuthContext);
+  const { user } = state || {};
 
   // Fetch API key with React Query
-  const { data: apiKey, isLoading } = useQuery({
-    queryKey: ['api-key'],
+  const { data: apiKey, isLoading } = useQuery({
+    queryKey: ['api-key', user?.id],
     queryFn: async () => {
       const { data } = await axios.get(`${apiUrl}/auth/api-key`);
       return data.api_key as string | null;
     },
     staleTime: 10 * 60 * 1000, // 10 minutes
+    enabled: !!user?.id,
   });

Also update mutations:

-  const { mutate: generateApiKey, isPending: isGenerating } = useMutation({
+  const { mutate: generateApiKey, isPending: isGenerating } = useMutation({
     mutationFn: async () => {
       const { data } = await axios.post(`${apiUrl}/auth/generate-api-key`);
       return data.api_key as string;
     },
-    onSuccess: (newKey) => {
-      queryClient.setQueryData(['api-key'], newKey);
+    onSuccess: (newKey) => {
+      queryClient.setQueryData(['api-key', user?.id], newKey);
       notify('success', t('apikey.notifications.generate_success'));
     },
-  const { mutate: deleteApiKey, isPending: isDeleting } = useMutation({
+  const { mutate: deleteApiKey, isPending: isDeleting } = useMutation({
     mutationFn: async () => {
       await axios.delete(`${apiUrl}/auth/delete-api-key`);
     },
     onSuccess: () => {
-      queryClient.setQueryData(['api-key'], null);
+      queryClient.setQueryData(['api-key', user?.id], null);
       notify('success', t('apikey.notifications.delete_success'));
     },
📝 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
// Fetch API key with React Query
const { data: apiKey, isLoading } = useQuery({
queryKey: ['api-key'],
queryFn: async () => {
const { data } = await axios.get(`${apiUrl}/auth/api-key`);
return data.api_key as string | null;
},
staleTime: 10 * 60 * 1000, // 10 minutes
});
// Fetch API key with React Query
const { data: apiKey, isLoading } = useQuery({
queryKey: ['api-key', user?.id],
queryFn: async () => {
const { data } = await axios.get(`${apiUrl}/auth/api-key`);
return data.api_key as string | null;
},
staleTime: 10 * 60 * 1000, // 10 minutes
enabled: !!user?.id,
});
🤖 Prompt for AI Agents
In src/components/api/ApiKey.tsx around lines 41 to 49, the query uses a global
['api-key'] which can leak one user's API key to another after sign-out/in;
change the queryKey to include the current user's id (e.g. ['api-key', user.id])
and add enabled: !!user?.id so the query only runs when a user is present; also
update any mutations that create/delete/regenerate the API key to invalidate or
update the same scoped key (['api-key', user.id]) instead of the global key so
cache updates are per-user and no stale key is shown across logins.


fetchApiKey();

}, []);

const generateApiKey = async () => {
setLoading(true);
try {
// Generate mutation
const { mutate: generateApiKey, isPending: isGenerating } = useMutation({
mutationFn: async () => {
const { data } = await axios.post(`${apiUrl}/auth/generate-api-key`);
setApiKey(data.api_key);

return data.api_key as string;
},
onSuccess: (newKey) => {
queryClient.setQueryData(['api-key'], newKey);
notify('success', t('apikey.notifications.generate_success'));
} catch (error: any) {
},
onError: (error: any) => {
notify('error', t('apikey.notifications.generate_error', { error: error.message }));
} finally {
setLoading(false);
}
};
},
});

const deleteApiKey = async () => {
setLoading(true);
try {
// Delete mutation
const { mutate: deleteApiKey, isPending: isDeleting } = useMutation({
mutationFn: async () => {
await axios.delete(`${apiUrl}/auth/delete-api-key`);
setApiKey(null);
},
onSuccess: () => {
queryClient.setQueryData(['api-key'], null);
notify('success', t('apikey.notifications.delete_success'));
} catch (error: any) {
},
onError: (error: any) => {
notify('error', t('apikey.notifications.delete_error', { error: error.message }));
} finally {
setLoading(false);
}
};
},
});

const copyToClipboard = () => {
if (apiKey) {
Expand All @@ -90,7 +86,7 @@ const ApiKeyManager = () => {
}
};

if (loading) {
if (isLoading) {
return (
<Box
sx={{
Expand Down Expand Up @@ -155,7 +151,7 @@ const ApiKeyManager = () => {
</IconButton>
</Tooltip>
<Tooltip title={t('apikey.actions.delete')}>
<IconButton onClick={deleteApiKey} color="error">
<IconButton onClick={() => deleteApiKey()} color="error" disabled={isDeleting}>
<Delete />
</IconButton>
</Tooltip>
Expand All @@ -167,7 +163,7 @@ const ApiKeyManager = () => {
) : (
<>
<Typography>{t('apikey.no_key_message')}</Typography>
<Button onClick={generateApiKey} variant="contained" color="primary" sx={{ marginTop: '20px' }}>
<Button onClick={() => generateApiKey()} variant="contained" color="primary" sx={{ marginTop: '20px' }} disabled={isGenerating}>
{t('apikey.generate_button')}
</Button>
</>
Expand Down
60 changes: 34 additions & 26 deletions src/pages/MainPage.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { useCallback, useContext, useEffect } from 'react';
import React, { useCallback, useContext, useEffect, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { MainMenu } from "../components/dashboard/MainMenu";
import { Stack } from "@mui/material";
Expand Down Expand Up @@ -293,37 +293,45 @@ export const MainPage = ({ handleEditRecording, initialContent }: MainPageProps)
}
}, [user?.id, connectToQueueSocket, disconnectQueueSocket, t, setRerenderRuns, queuedRuns, setQueuedRuns]);

const DisplayContent = () => {
switch (content) {
case 'robots':
return <Recordings
handleEditRecording={handleEditRecording}
handleRunRecording={handleRunRecording}
setRecordingInfo={setRecordingInfo}
handleScheduleRecording={handleScheduleRecording}
/>;
case 'runs':
return <Runs
currentInterpretationLog={currentInterpretationLog}
abortRunHandler={abortRunHandler}
runId={ids.runId}
runningRecordingName={runningRecordingName}
/>;
case 'proxy':
return <ProxyForm />;
case 'apikey':
return <ApiKey />;
default:
return null;
}
}
// Keep subviews mounted to avoid refetch/remount lag on tab switches
const robotsView = useMemo(() => (
<Recordings
handleEditRecording={handleEditRecording}
handleRunRecording={handleRunRecording}
setRecordingInfo={setRecordingInfo}
handleScheduleRecording={handleScheduleRecording}
/>
), [handleEditRecording, handleRunRecording, setRecordingInfo, handleScheduleRecording]);

const runsView = useMemo(() => (
<Runs
currentInterpretationLog={currentInterpretationLog}
abortRunHandler={abortRunHandler}
runId={ids.runId}
runningRecordingName={runningRecordingName}
/>
), [currentInterpretationLog, abortRunHandler, ids.runId, runningRecordingName]);

const proxyView = useMemo(() => (<ProxyForm />), []);
const apiKeyView = useMemo(() => (<ApiKey />), []);

return (
<Stack direction='row' spacing={0} sx={{ minHeight: '900px' }}>
<Stack sx={{ width: 250, flexShrink: 0 }}>
<MainMenu value={content} handleChangeContent={setContent} />
</Stack>
{DisplayContent()}
<div style={{ display: content === 'robots' ? 'block' : 'none', width: '100%' }}>
{robotsView}
</div>
<div style={{ display: content === 'runs' ? 'block' : 'none', width: '100%' }}>
{runsView}
</div>
<div style={{ display: content === 'proxy' ? 'block' : 'none', width: '100%' }}>
{proxyView}
</div>
<div style={{ display: content === 'apikey' ? 'block' : 'none', width: '100%' }}>
{apiKeyView}
</div>
</Stack>
);
};