feat: Implement temporary API key based on system UUID for UI access

This commit introduces a new authentication mechanism for the web UI.
Instead of requiring a pre-configured API key, a temporary API key is
generated based on the system's UUID. This key is passed to the UI
as a URL parameter and used for API requests.

Changes:
- Added a new utility to get the system UUID and generate a temporary API key.
- Modified the `ccr ui` command to generate and pass the temporary API key.
- Updated the authentication middleware to validate the temporary API key.
- Adjusted the frontend to use the temporary API key from the URL.
- Added a dedicated endpoint to test API access without modifying data.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
musistudio
2025-08-07 15:00:42 +08:00
parent 4334f40926
commit 9cd5587f52
10 changed files with 288 additions and 17 deletions

View File

@@ -128,6 +128,20 @@ export function ConfigProvider({ children }: ConfigProviderProps) {
fetchConfig();
}, [hasFetched, apiKey]);
// Check if user has full access
useEffect(() => {
const checkAccess = async () => {
if (config) {
const hasFullAccess = await api.checkFullAccess();
// Store access level in a global state or context if needed
// For now, we'll just log it
console.log('User has full access:', hasFullAccess);
}
};
checkAccess();
}, [config]);
return (
<ConfigContext.Provider value={{ config, setConfig, error }}>
{children}

View File

@@ -61,18 +61,23 @@ export function Login() {
url: window.location.href
}));
// Test the API key by fetching config (skip if apiKey is empty)
if (apiKey) {
await api.getConfig();
}
// Test the API key by fetching config
await api.getConfig();
// Navigate to dashboard
// The ConfigProvider will handle fetching the config
navigate('/dashboard');
} catch {
} catch (error: any) {
// Clear the API key on failure
api.setApiKey('');
setError(t('login.invalidApiKey'));
// Check if it's an unauthorized error
if (error.message && error.message.includes('401')) {
setError(t('login.invalidApiKey'));
} else {
// For other errors, still allow access (restricted mode)
navigate('/dashboard');
}
}
};

View File

@@ -4,11 +4,14 @@ import type { Config, Provider, Transformer } from '@/types';
class ApiClient {
private baseUrl: string;
private apiKey: string;
private tempApiKey: string | null;
constructor(baseUrl: string = '/api', apiKey: string = '') {
this.baseUrl = baseUrl;
// Load API key from localStorage if available
this.apiKey = apiKey || localStorage.getItem('apiKey') || '';
// Load temp API key from URL if available
this.tempApiKey = new URLSearchParams(window.location.search).get('tempApiKey');
}
// Update base URL
@@ -26,14 +29,25 @@ class ApiClient {
localStorage.removeItem('apiKey');
}
}
// Update temp API key
setTempApiKey(tempApiKey: string | null) {
this.tempApiKey = tempApiKey;
}
// Create headers with API key authentication
private createHeaders(contentType: string = 'application/json'): HeadersInit {
const headers: Record<string, string> = {
'X-API-Key': this.apiKey,
'Accept': 'application/json',
};
// Use temp API key if available, otherwise use regular API key
if (this.tempApiKey) {
headers['X-Temp-API-Key'] = this.tempApiKey;
} else if (this.apiKey) {
headers['X-API-Key'] = this.apiKey;
}
if (contentType) {
headers['Content-Type'] = contentType;
}
@@ -126,6 +140,22 @@ class ApiClient {
return this.post<Config>('/config', config);
}
// Check if user has full access
async checkFullAccess(): Promise<boolean> {
try {
// Try to access test endpoint (won't actually modify anything)
// This will return 403 if user doesn't have full access
await this.post<Config>('/config/test', { test: true });
return true;
} catch (error: any) {
if (error.message && error.message.includes('403')) {
return false;
}
// For other errors, assume user has access (to avoid blocking legitimate users)
return true;
}
}
// Get providers
async getProviders(): Promise<Provider[]> {
return this.get<Provider[]>('/api/providers');

View File

@@ -40,3 +40,5 @@ export interface Config {
API_TIMEOUT_MS: string;
PROXY_URL: string;
}
export type AccessLevel = 'restricted' | 'full';