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
276 changes: 210 additions & 66 deletions components/SettingsModal.tsx

Large diffs are not rendered by default.

10 changes: 10 additions & 0 deletions constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,16 @@ export const OPEN_ROUTER_MODELS = [
'openai/gpt-3.5-turbo',
];

// Default Local AI / LiteLLM settings
export const DEFAULT_LOCAL_AI_URL = 'http://localhost:4000/v1/chat/completions';
export const DEFAULT_LOCAL_AI_MODEL = 'gpt-3.5-turbo';

// API Provider labels for UI
export const API_PROVIDER_LABELS = {
openrouter: 'OpenRouter',
localai: 'Local AI / LiteLLM',
} as const;


export const SEVERITY_STYLES: Record<Severity, { headerBg: string; border: string; text: string }> = {
[Severity.CRITICAL]: {
Expand Down
43 changes: 35 additions & 8 deletions contexts/SettingsProvider.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,19 @@
// contexts/SettingsProvider.tsx
import React, { createContext, useContext, useState, useEffect, useMemo } from 'react';
import { ApiKeys } from '../types.ts';
import { OPEN_ROUTER_MODELS } from '../constants.ts';
import { ApiKeys, ApiProvider, LocalAiConfig } from '../types.ts';
import { OPEN_ROUTER_MODELS, DEFAULT_LOCAL_AI_URL, DEFAULT_LOCAL_AI_MODEL } from '../constants.ts';

interface SettingsContextType {
theme: 'light' | 'dark';
setTheme: (theme: 'light' | 'dark') => void;
apiProvider: ApiProvider;
setApiProvider: (provider: ApiProvider) => void;
apiKeys: ApiKeys;
setApiKeys: (keys: ApiKeys) => void;
openRouterModel: string;
setOpenRouterModel: (model: string) => void;
localAiConfig: LocalAiConfig;
setLocalAiConfig: (config: LocalAiConfig) => void;
saveApiKeys: boolean;
setSaveApiKeys: (save: boolean) => void;
}
Expand All @@ -18,8 +22,13 @@ const SettingsContext = createContext<SettingsContextType | undefined>(undefined

export const SettingsProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
const [theme, setTheme] = useState<'light' | 'dark'>('dark');
const [apiKeys, setApiKeys] = useState<ApiKeys>({ openrouter: '' });
const [apiProvider, setApiProvider] = useState<ApiProvider>('openrouter');
const [apiKeys, setApiKeys] = useState<ApiKeys>({ openrouter: '', localai: '' });
const [openRouterModel, setOpenRouterModel] = useState<string>(OPEN_ROUTER_MODELS[0]);
const [localAiConfig, setLocalAiConfig] = useState<LocalAiConfig>({
baseUrl: DEFAULT_LOCAL_AI_URL,
model: DEFAULT_LOCAL_AI_MODEL,
});
const [saveApiKeys, setSaveApiKeys] = useState<boolean>(false);

useEffect(() => {
Expand All @@ -31,6 +40,9 @@ export const SettingsProvider: React.FC<{ children: React.ReactNode }> = ({ chil
setTheme('dark');
}

const savedProvider = localStorage.getItem('apiProvider') as ApiProvider | null;
if (savedProvider) setApiProvider(savedProvider);

const savedSavePref = localStorage.getItem('saveApiKeys') === 'true';
setSaveApiKeys(savedSavePref);

Expand All @@ -39,8 +51,11 @@ export const SettingsProvider: React.FC<{ children: React.ReactNode }> = ({ chil
if (savedKeys) setApiKeys(JSON.parse(savedKeys));
}

const savedModel = localStorage.getItem('openRouterModel');
if (savedModel) setOpenRouterModel(savedModel);
const savedOpenRouterModel = localStorage.getItem('openRouterModel');
if (savedOpenRouterModel) setOpenRouterModel(savedOpenRouterModel);

const savedLocalAiConfig = localStorage.getItem('localAiConfig');
if (savedLocalAiConfig) setLocalAiConfig(JSON.parse(savedLocalAiConfig));

} catch (e) { console.error("Could not load settings:", e); }
}, []);
Expand All @@ -53,6 +68,11 @@ export const SettingsProvider: React.FC<{ children: React.ReactNode }> = ({ chil
catch (e) { console.error("Could not save theme:", e); }
}, [theme]);

useEffect(() => {
try { localStorage.setItem('apiProvider', apiProvider); }
catch (e) { console.error("Could not save API provider:", e); }
}, [apiProvider]);

useEffect(() => {
try {
localStorage.setItem('saveApiKeys', String(saveApiKeys));
Expand All @@ -66,15 +86,22 @@ export const SettingsProvider: React.FC<{ children: React.ReactNode }> = ({ chil

useEffect(() => {
try { localStorage.setItem('openRouterModel', openRouterModel); }
catch (e) { console.error("Could not save model:", e); }
catch (e) { console.error("Could not save OpenRouter model:", e); }
}, [openRouterModel]);

useEffect(() => {
try { localStorage.setItem('localAiConfig', JSON.stringify(localAiConfig)); }
catch (e) { console.error("Could not save Local AI config:", e); }
}, [localAiConfig]);

const value = useMemo(() => ({
theme, setTheme,
apiProvider, setApiProvider,
apiKeys, setApiKeys,
openRouterModel, setOpenRouterModel,
localAiConfig, setLocalAiConfig,
saveApiKeys, setSaveApiKeys,
}), [theme, apiKeys, openRouterModel, saveApiKeys]);
}), [theme, apiProvider, apiKeys, openRouterModel, localAiConfig, saveApiKeys]);

return <SettingsContext.Provider value={value}>{children}</SettingsContext.Provider>;
};
Expand All @@ -85,4 +112,4 @@ export const useSettings = (): SettingsContextType => {
throw new Error('useSettings must be used within a SettingsProvider');
}
return context;
};
};
20 changes: 16 additions & 4 deletions dockerizer.sh
100644 → 100755
Original file line number Diff line number Diff line change
@@ -1,19 +1,31 @@
#!/bin/bash

# Determine which docker compose command is available
if docker compose version &> /dev/null; then
DOCKER_COMPOSE_CMD="docker compose"
elif docker-compose version &> /dev/null; then
DOCKER_COMPOSE_CMD="docker-compose"
else
echo "❌ Neither 'docker compose' nor 'docker-compose' is available."
echo "💡 Please install Docker Compose."
exit 1
fi

echo "--- Using: $DOCKER_COMPOSE_CMD ---"
echo "--- Stopping any previous containers... ---"
docker-compose -f docker-compose.yml down -v
$DOCKER_COMPOSE_CMD -f docker-compose.yml down -v

if [ $? -ne 0 ]; then
echo "Warning: 'docker-compose down' failed. This might be the first run, which is okay. Continuing..."
echo "Warning: 'docker compose down' failed. This might be the first run, which is okay. Continuing..."
fi

echo "--- Building and starting the application... ---"
docker-compose -f docker-compose.yml up --build -d
$DOCKER_COMPOSE_CMD -f docker-compose.yml up --build -d

if [ $? -eq 0 ]; then
echo "--- Application is now running! ---"
echo "Access it at: http://localhost:6869"
echo "To stop the application, run: docker-compose -f docker-compose.yml down"
echo "To stop the application, run: $DOCKER_COMPOSE_CMD -f docker-compose.yml down"

# === Try to launch Firefox with checks ===
sleep 3 # Wait a bit for the server to start
Expand Down
41 changes: 33 additions & 8 deletions hooks/useApiOptions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,21 +7,46 @@ export const useApiOptions = (): {
apiOptions: ApiOptions | null;
isApiKeySet: boolean;
} => {
const { apiKeys, openRouterModel } = useSettings();
const isApiKeySet = !!apiKeys.openrouter?.trim();
const { apiProvider, apiKeys, openRouterModel, localAiConfig } = useSettings();

const isApiKeySet = useMemo(() => {
switch (apiProvider) {
case 'openrouter':
return !!apiKeys.openrouter?.trim();
case 'localai':
// For local AI, we just need a base URL (API key is optional)
return !!localAiConfig.baseUrl?.trim();
default:
return false;
}
}, [apiProvider, apiKeys, localAiConfig.baseUrl]);

const apiOptions = useMemo(() => {
if (!isApiKeySet) {
return null;
}
return {
apiKey: apiKeys.openrouter,
model: openRouterModel,
};
}, [isApiKeySet, apiKeys.openrouter, openRouterModel]);

switch (apiProvider) {
case 'openrouter':
return {
provider: 'openrouter' as const,
apiKey: apiKeys.openrouter,
model: openRouterModel,
};
case 'localai':
return {
provider: 'localai' as const,
apiKey: apiKeys.localai || '',
model: localAiConfig.model,
baseUrl: localAiConfig.baseUrl,
};
default:
return null;
}
}, [isApiKeySet, apiProvider, apiKeys, openRouterModel, localAiConfig]);

return {
apiOptions,
isApiKeySet
};
};
};
105 changes: 79 additions & 26 deletions services/Service.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
// @author: Albert C | @yz9yt | github.com/yz9yt
// services/Service.ts
// version 0.1 Beta
// version 0.2 Beta - Multi-provider support
import {
ApiOptions, Vulnerability, VulnerabilityReport, XssPayloadResult, ForgedPayloadResult,
ChatMessage, ExploitContext, HeadersReport, DomXssAnalysisResult,
FileUploadAnalysisResult, DastScanType, SqlmapCommandResult,
Severity
Severity, ApiProvider
} from '../types.ts';
import {
createSastAnalysisPrompt,
Expand Down Expand Up @@ -41,27 +41,55 @@ import {
resetContinuousFailureCount,
} from '../utils/apiManager.ts';

const OPENROUTER_API_URL = "https://openrouter.ai/api/v1/chat/completions";
// API endpoint URLs
const API_URLS = {
openrouter: "https://openrouter.ai/api/v1/chat/completions",
localai: "", // Will be provided by user
};

const getApiUrl = (options: ApiOptions): string => {
if (options.provider === 'localai' && options.baseUrl) {
return options.baseUrl;
}
return API_URLS[options.provider];
};

const getAuthHeader = (options: ApiOptions): Record<string, string> => {
const headers: Record<string, string> = {
'Content-Type': 'application/json'
};

if (options.apiKey) {
headers['Authorization'] = `Bearer ${options.apiKey}`;
}

return headers;
};

const callApi = async (prompt: string, options: ApiOptions, isJson: boolean = true) => {
await enforceRateLimit();
const { apiKey, model } = options;
if (!apiKey) {
const { apiKey, model, provider } = options;

// For non-local providers, API key is required
if (provider !== 'localai' && !apiKey) {
throw new Error("API Key is not configured.");
}

const apiUrl = getApiUrl(options);
if (!apiUrl) {
throw new Error("API URL is not configured.");
}

const signal = getNewAbortSignal();

try {
setRequestStatus('active');
updateRateLimitTimestamp();
incrementApiCallCount();

const response = await fetch(OPENROUTER_API_URL, {
const response = await fetch(apiUrl, {
method: 'POST',
headers: {
'Authorization': `Bearer ${apiKey}`,
'Content-Type': 'application/json'
},
headers: getAuthHeader(options),
body: JSON.stringify({
model: model,
messages: [{ role: 'user', content: prompt }],
Expand Down Expand Up @@ -91,7 +119,7 @@ const callApi = async (prompt: string, options: ApiOptions, isJson: boolean = tr
console.log("API request was cancelled.");
throw new Error("Request cancelled.");
}
console.error("Error calling OpenRouter:", error);
console.error("Error calling API:", error);
throw new Error(error.message || "An unknown error occurred while contacting the AI service.");
} finally {
setRequestStatus('idle');
Expand Down Expand Up @@ -336,23 +364,27 @@ export const generateSstiPayloads = async (engine: string, goal: string, options
// --- Chat Functions ---
const callOpenRouterChat = async (history: ChatMessage[], options: ApiOptions) => {
await enforceRateLimit();
const { apiKey, model } = options;
if (!apiKey) {
const { apiKey, model, provider } = options;

if (provider !== 'localai' && !apiKey) {
throw new Error("API Key is not configured.");
}

const apiUrl = getApiUrl(options);
if (!apiUrl) {
throw new Error("API URL is not configured.");
}

const signal = getNewAbortSignal();

try {
setRequestStatus('active');
updateRateLimitTimestamp();
incrementApiCallCount();

const response = await fetch(OPENROUTER_API_URL, {
const response = await fetch(apiUrl, {
method: 'POST',
headers: {
'Authorization': `Bearer ${apiKey}`,
'Content-Type': 'application/json'
},
headers: getAuthHeader(options),
body: JSON.stringify({
model: model,
messages: history.map(({ role, content }) => ({ role, content })),
Expand All @@ -375,7 +407,7 @@ const callOpenRouterChat = async (history: ChatMessage[], options: ApiOptions) =
console.log("Chat API request was cancelled.");
throw new Error("Request cancelled.");
}
console.error("Error calling OpenRouter Chat:", error);
console.error("Error calling Chat API:", error);
throw new Error(error.message || "An unknown error occurred while contacting the AI service.");
} finally {
setRequestStatus('idle');
Expand Down Expand Up @@ -417,18 +449,39 @@ export const continueGeneralChat = async (systemPrompt: string, history: ChatMes
return callOpenRouterChat(fullHistory, options);
};

export const testApi = async (apiKey: string, model: string): Promise<{ success: boolean; error?: string }> => {
if (!apiKey.startsWith('sk-or-')) {
export const testApi = async (apiKey: string, model: string, provider: ApiProvider = 'openrouter', baseUrl?: string): Promise<{ success: boolean; error?: string }> => {
// Validation based on provider
if (provider === 'openrouter' && apiKey && !apiKey.startsWith('sk-or-')) {
return { success: false, error: 'Invalid OpenRouter API key format. It should start with "sk-or-".' };
}
Comment on lines +454 to 456
Copy link

Copilot AI Dec 28, 2025

Choose a reason for hiding this comment

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

The validation logic checks if the API key starts with 'sk-or-' only when an apiKey is provided, but this validation is now conditional on the provider being 'openrouter'. However, if a user switches from LocalAI to OpenRouter with an empty key, then enters an invalid key format, the validation won't trigger until they click "Test API Connection". Consider moving this validation to run whenever the openrouter key changes, not just during API testing.

Copilot uses AI. Check for mistakes.

if (provider === 'localai' && !baseUrl) {
return { success: false, error: 'Base URL is required for Local AI.' };
}

if (provider !== 'localai' && !apiKey) {
return { success: false, error: 'API Key is required.' };
Copy link

Copilot AI Dec 28, 2025

Choose a reason for hiding this comment

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

The error message 'API Key is required.' is generic and doesn't specify which provider requires it. When this error is shown, users might not understand the context. Consider making it more specific, such as 'API Key is required for OpenRouter provider.'

Suggested change
return { success: false, error: 'API Key is required.' };
return { success: false, error: `API Key is required for ${provider} provider.` };

Copilot uses AI. Check for mistakes.
}

let apiUrl: string;
if (provider === 'localai') {
apiUrl = baseUrl!;
} else {
apiUrl = API_URLS[provider];
}

const headers: Record<string, string> = {
'Content-Type': 'application/json'
};

if (apiKey) {
headers['Authorization'] = `Bearer ${apiKey}`;
}

try {
const response = await fetch(OPENROUTER_API_URL, {
const response = await fetch(apiUrl, {
method: 'POST',
headers: {
'Authorization': `Bearer ${apiKey}`,
'Content-Type': 'application/json'
},
headers,
body: JSON.stringify({
model: model,
messages: [{ role: 'user', content: 'Test prompt' }],
Expand Down
Loading