Add API key authentication to all endpoints
This commit is contained in:
parent
56d8fb4d56
commit
176439c1df
4 changed files with 207 additions and 28 deletions
|
|
@ -3,6 +3,36 @@ const { useState: useStateApp, useEffect: useEffectApp, useRef: useRefApp } = Re
|
|||
|
||||
const API_BASE = '';
|
||||
|
||||
// API key storage
|
||||
const API_KEY_STORAGE_KEY = 'clio_api_key';
|
||||
|
||||
function getStoredApiKey() {
|
||||
return localStorage.getItem(API_KEY_STORAGE_KEY);
|
||||
}
|
||||
|
||||
function setStoredApiKey(key) {
|
||||
localStorage.setItem(API_KEY_STORAGE_KEY, key);
|
||||
}
|
||||
|
||||
function clearStoredApiKey() {
|
||||
localStorage.removeItem(API_KEY_STORAGE_KEY);
|
||||
}
|
||||
|
||||
// Authenticated fetch wrapper
|
||||
async function authFetch(url, options = {}) {
|
||||
const apiKey = getStoredApiKey();
|
||||
const headers = {
|
||||
...options.headers,
|
||||
'X-API-Key': apiKey || '',
|
||||
};
|
||||
const response = await fetch(url, { ...options, headers });
|
||||
if (response.status === 401) {
|
||||
clearStoredApiKey();
|
||||
window.dispatchEvent(new Event('auth-required'));
|
||||
}
|
||||
return response;
|
||||
}
|
||||
|
||||
// Parse hash into route info
|
||||
function parseHash() {
|
||||
const hash = window.location.hash.slice(1) || '/dashboard';
|
||||
|
|
@ -70,14 +100,110 @@ function transformRecording(rec) {
|
|||
};
|
||||
}
|
||||
|
||||
function ApiKeyPrompt({ onSubmit }) {
|
||||
const [key, setKey] = useStateApp('');
|
||||
const [error, setError] = useStateApp(null);
|
||||
const [checking, setChecking] = useStateApp(false);
|
||||
|
||||
async function handleSubmit(e) {
|
||||
e.preventDefault();
|
||||
if (!key.trim()) return;
|
||||
|
||||
setChecking(true);
|
||||
setError(null);
|
||||
|
||||
try {
|
||||
// Test the key by making a simple request
|
||||
const res = await fetch(`${API_BASE}/api/v1/sources`, {
|
||||
headers: { 'X-API-Key': key.trim() }
|
||||
});
|
||||
|
||||
if (res.status === 401) {
|
||||
setError('Invalid API key');
|
||||
setChecking(false);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!res.ok) {
|
||||
setError('Server error');
|
||||
setChecking(false);
|
||||
return;
|
||||
}
|
||||
|
||||
setStoredApiKey(key.trim());
|
||||
onSubmit();
|
||||
} catch (err) {
|
||||
setError('Connection failed');
|
||||
setChecking(false);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div style={{
|
||||
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
||||
height: '100vh', background: 'var(--bg)',
|
||||
}}>
|
||||
<form onSubmit={handleSubmit} style={{
|
||||
background: 'var(--bg2)', padding: 32, borderRadius: 8,
|
||||
border: '1px solid var(--border)', width: 320,
|
||||
}}>
|
||||
<div style={{ fontFamily: 'var(--font-mono)', fontSize: 14, marginBottom: 20, color: 'var(--text)' }}>
|
||||
Enter API Key
|
||||
</div>
|
||||
<input
|
||||
type="password"
|
||||
value={key}
|
||||
onChange={(e) => setKey(e.target.value)}
|
||||
placeholder="API Key"
|
||||
autoFocus
|
||||
style={{
|
||||
width: '100%', padding: '10px 12px', marginBottom: 12,
|
||||
background: 'var(--bg3)', border: '1px solid var(--border)',
|
||||
borderRadius: 4, color: 'var(--text)', fontFamily: 'var(--font-mono)',
|
||||
fontSize: 13, outline: 'none',
|
||||
}}
|
||||
/>
|
||||
{error && (
|
||||
<div style={{ color: 'var(--red)', fontSize: 12, marginBottom: 12, fontFamily: 'var(--font-mono)' }}>
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
<button
|
||||
type="submit"
|
||||
disabled={checking}
|
||||
style={{
|
||||
width: '100%', padding: '10px 12px',
|
||||
background: 'var(--amber)', color: 'var(--bg)',
|
||||
border: 'none', borderRadius: 4, fontFamily: 'var(--font-mono)',
|
||||
fontSize: 12, fontWeight: 600, cursor: 'pointer',
|
||||
opacity: checking ? 0.6 : 1,
|
||||
}}
|
||||
>
|
||||
{checking ? 'Checking...' : 'Continue'}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function App() {
|
||||
const [data, setData] = useStateApp({ recordings: [], highlights: [], devices: {} });
|
||||
const [loading, setLoading] = useStateApp(true);
|
||||
const [error, setError] = useStateApp(null);
|
||||
const [needsAuth, setNeedsAuth] = useStateApp(!getStoredApiKey());
|
||||
|
||||
// Lift highlights state so adding/removing works across views
|
||||
const [highlights, setHighlights] = useStateApp([]);
|
||||
|
||||
// Listen for auth-required events (401 responses)
|
||||
useEffectApp(() => {
|
||||
function handleAuthRequired() {
|
||||
setNeedsAuth(true);
|
||||
}
|
||||
window.addEventListener('auth-required', handleAuthRequired);
|
||||
return () => window.removeEventListener('auth-required', handleAuthRequired);
|
||||
}, []);
|
||||
|
||||
// Initialize from URL hash
|
||||
const initialRoute = parseHash();
|
||||
const [view, setViewState] = useStateApp(initialRoute.view);
|
||||
|
|
@ -146,7 +272,7 @@ function App() {
|
|||
async function fetchRecordingDetail(recordingId) {
|
||||
setLoadingDetail(true);
|
||||
try {
|
||||
const res = await fetch(`${API_BASE}/api/v1/recordings/${recordingId}`);
|
||||
const res = await authFetch(`${API_BASE}/api/v1/recordings/${recordingId}`);
|
||||
if (!res.ok) throw new Error('Failed to fetch recording');
|
||||
const rec = await res.json();
|
||||
const transformed = transformRecording(rec);
|
||||
|
|
@ -189,9 +315,9 @@ function App() {
|
|||
async function fetchData() {
|
||||
try {
|
||||
const [recordingsRes, sourcesRes, highlightsRes] = await Promise.all([
|
||||
fetch(`${API_BASE}/api/v1/recordings?limit=1000`),
|
||||
fetch(`${API_BASE}/api/v1/sources`),
|
||||
fetch(`${API_BASE}/api/v1/highlights?limit=1000`),
|
||||
authFetch(`${API_BASE}/api/v1/recordings?limit=1000`),
|
||||
authFetch(`${API_BASE}/api/v1/sources`),
|
||||
authFetch(`${API_BASE}/api/v1/highlights?limit=1000`),
|
||||
]);
|
||||
|
||||
if (!recordingsRes.ok) throw new Error('Failed to fetch recordings');
|
||||
|
|
@ -256,6 +382,19 @@ function App() {
|
|||
fetchData();
|
||||
}, []);
|
||||
|
||||
// Show API key prompt if not authenticated
|
||||
if (needsAuth) {
|
||||
return (
|
||||
<ApiKeyPrompt onSubmit={() => {
|
||||
setNeedsAuth(false);
|
||||
setLoading(true);
|
||||
// Re-trigger data fetch by incrementing a counter or similar
|
||||
// For simplicity, just reload the page
|
||||
window.location.reload();
|
||||
}} />
|
||||
);
|
||||
}
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', height: '100vh', color: 'var(--text2)' }}>
|
||||
|
|
@ -363,5 +502,8 @@ function App() {
|
|||
);
|
||||
}
|
||||
|
||||
// Export utilities for other components
|
||||
Object.assign(window, { authFetch, getStoredApiKey });
|
||||
|
||||
const root = ReactDOM.createRoot(document.getElementById('root'));
|
||||
root.render(<App />);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue