From 84b000de5a2e95fe5c852a71e24d0a18203d0484 Mon Sep 17 00:00:00 2001 From: Zac Gaetano Date: Sun, 5 Apr 2026 00:13:37 -0400 Subject: [PATCH] feat: add token-based auth UI in dashboard --- frontend/src/App.jsx | 442 ++++++++++++------------------------------- 1 file changed, 121 insertions(+), 321 deletions(-) diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index d376925..eb00f29 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -20,9 +20,13 @@ const App = () => { const [showAddMcp, setShowAddMcp] = useState(false); const [mcpForm, setMcpForm] = useState({ name: '', server_type: 'sse', url: '', command: '' }); const [loading, setLoading] = useState(false); - const [activeView, setActiveView] = useState('tasks'); // 'tasks' | 'dashboard' + const [activeView, setActiveView] = useState('tasks'); + + // Auth token state + const [authToken, setAuthToken] = useState(''); + const [tokenType, setTokenType] = useState('oauth_token'); + const [tokenSubmitting, setTokenSubmitting] = useState(false); - // Fetch tasks on component mount useEffect(() => { fetchTasks(); fetchSystemInfo(); @@ -79,50 +83,32 @@ const App = () => { } }; - const [loginLoading, setLoginLoading] = useState(false); - const [authCode, setAuthCode] = useState(''); - const [codeSubmitting, setCodeSubmitting] = useState(false); - - const handleLogin = async () => { - setLoginLoading(true); - setAuthCode(''); - try { - const response = await fetch('/api/auth/login', { method: 'POST' }); - const data = await response.json(); - setAuthStatus(prev => ({ ...prev, ...data })); - if (data.status === 'error') { - alert(data.message || 'Login failed. Check container logs.'); - } - // Don't try window.open — popup blockers kill it. - // The auth_url will be shown as a clickable link in the pending state. - } catch (error) { - console.error('Error initiating login:', error); - alert('Failed to connect to auth endpoint'); - } - setLoginLoading(false); - }; - - const handleSubmitCode = async (e) => { + const handleSubmitToken = async (e) => { e.preventDefault(); - if (!authCode.trim()) return; - setCodeSubmitting(true); + if (!authToken.trim()) return; + setTokenSubmitting(true); try { - const response = await fetch('/api/auth/code', { + const response = await fetch('/api/auth/token', { method: 'POST', headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ code: authCode.trim() }) + body: JSON.stringify({ token: authToken.trim(), token_type: tokenType }) }); const data = await response.json(); if (data.status === 'logged_in') { - setAuthCode(''); + setAuthToken(''); fetchAuthStatus(); + } else if (data.status === 'token_saved') { + setAuthToken(''); + fetchAuthStatus(); + alert(data.message); } else { - alert(data.message || 'Code submission failed'); + alert(data.message || 'Token submission failed'); } } catch (error) { - console.error('Error submitting auth code:', error); + console.error('Error submitting token:', error); + alert('Failed to submit token'); } - setCodeSubmitting(false); + setTokenSubmitting(false); }; const handleLogout = async () => { @@ -178,40 +164,26 @@ const App = () => { const handleCreateTask = async (e) => { e.preventDefault(); setLoading(true); - try { const response = await fetch('/api/tasks', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(formData) }); - if (response.ok) { await fetchTasks(); setShowForm(false); - setFormData({ - name: '', - description: '', - prompt: '', - schedule_type: 'manual', - schedule_value: '', - enabled: true - }); + setFormData({ name: '', description: '', prompt: '', schedule_type: 'manual', schedule_value: '', enabled: true }); } } catch (error) { console.error('Error creating task:', error); } - setLoading(false); }; const handleRunTask = async (taskId) => { try { - const response = await fetch(`/api/tasks/${taskId}/run`, { - method: 'POST' - }); - const data = await response.json(); - console.log('Task started:', data); + await fetch(`/api/tasks/${taskId}/run`, { method: 'POST' }); await fetchTasks(); } catch (error) { console.error('Error running task:', error); @@ -253,24 +225,17 @@ const App = () => {
{authStatus?.status === 'logged_in' ? ( - ● {authStatus.account || 'Logged in'} - ) : authStatus?.status === 'pending' ? ( -
- {authStatus.auth_url && ( - 1. Open login → - )} -
- setAuthCode(e.target.value)} className="header-code-input" /> - -
-
+ + ● {authStatus.account || `Authenticated (${authStatus.auth_method || 'token'})`} + + ) : authStatus?.has_saved_token ? ( + + ● Token saved ({authStatus.token_type === 'api_key' ? 'API Key' : 'OAuth'}) + ) : ( - + setActiveView('dashboard')} style={{cursor:'pointer'}}> + ⚠ Not authenticated + )}
{systemInfo && ( @@ -289,40 +254,17 @@ const App = () => {
{systemInfo && ( <> -
-
📋
-
{systemInfo.task_count}
-
Total Tasks
-
-
-
-
{systemInfo.completed_runs || 0}
-
Completed Runs
-
-
-
-
{systemInfo.failed_runs || 0}
-
Failed Runs
-
-
-
-
{systemInfo.running_runs || 0}
-
Currently Running
-
-
-
🔄
-
{systemInfo.total_runs || 0}
-
Total Runs Ever
-
-
-
{systemInfo.scheduler_running ? '🟢' : '🔴'}
-
{systemInfo.scheduler_running ? 'Active' : 'Stopped'}
-
Scheduler
-
+
📋
{systemInfo.task_count}
Total Tasks
+
{systemInfo.completed_runs || 0}
Completed Runs
+
{systemInfo.failed_runs || 0}
Failed Runs
+
{systemInfo.running_runs || 0}
Currently Running
+
🔄
{systemInfo.total_runs || 0}
Total Runs
+
{systemInfo.scheduler_running ? '🟢' : '🔴'}
{systemInfo.scheduler_running ? 'Active' : 'Stopped'}
Scheduler
)}
+ {/* Auth Section */}

Claude Authentication

@@ -330,46 +272,59 @@ const App = () => {
-
Connected to Claude Max
-
{authStatus.account}
+
Connected to Claude
+
{authStatus.account || 'Authenticated'}{authStatus.auth_method && ` (${authStatus.auth_method})`}
- ) : authStatus?.status === 'pending' ? ( -
- -
-
Waiting for authorization code…
-
1. Click the link below to authorize in your browser
-
2. After authorizing, you'll see a code — paste it below
- {authStatus.auth_url && ( - - Open authorization page → - - )} -
- setAuthCode(e.target.value)} - className="auth-code-input" /> - -
-
-
) : ( -
- 🔐 -
-
Not logged in
-
Log in with your Claude Max account to run tasks
+
+
+ 🔐 +
+
{authStatus?.has_saved_token ? 'Token Saved — Update or Replace' : 'Authenticate with Token'}
+
Two options to authenticate:
+
- + +
+
+
Option 1: Setup Token (Claude Max/Pro)
+
Run this on TrueNAS to generate a long-lived token:
+ docker exec -it claude-persistent-agent claude setup-token +
Then paste the token (starts with sk-ant-oat) below.
+
+
+
Option 2: API Key (Console billing)
+
+ Get an API key from console.anthropic.com and paste it below. +
+
+
+ +
+
+ + +
+
+ setAuthToken(e.target.value)} className="auth-token-input" autoComplete="off" /> + +
+
+ + {authStatus?.has_saved_token && ( +
+ Saved: {authStatus.token_type === 'api_key' ? 'API Key' : 'OAuth Token'} + +
+ )}
)}
+ {/* MCP Section */}

MCP Servers

@@ -390,24 +345,19 @@ const App = () => { {mcpServers?.raw &&
{mcpServers.raw}
}
)} - {showAddMcp ? (
- setMcpForm({...mcpForm, name: e.target.value})} required /> - setMcpForm({...mcpForm, name: e.target.value})} required /> +
{mcpForm.server_type === 'sse' ? ( - setMcpForm({...mcpForm, url: e.target.value})} required /> + setMcpForm({...mcpForm, url: e.target.value})} required /> ) : ( - setMcpForm({...mcpForm, command: e.target.value})} required /> + setMcpForm({...mcpForm, command: e.target.value})} required /> )}
@@ -420,45 +370,26 @@ const App = () => {
+ {/* Usage Section */}

Claude API Usage

{usageStats ? ( <> -
- Claude Runs (all time) - {usageStats.claude_runs_total ?? '—'} -
-
- Active Sessions - {usageStats.session_count ?? '—'} -
-
- First Run - {usageStats.first_run ? new Date(usageStats.first_run).toLocaleString() : '—'} -
-
- Last Run - {usageStats.last_run ? new Date(usageStats.last_run).toLocaleString() : '—'} -
-
- 🔄 Next Monthly Reset - {usageStats.next_reset ? new Date(usageStats.next_reset).toLocaleDateString() : '—'} -
-
- ⏳ Days Until Reset - {usageStats.days_until_reset ?? '—'} -
- {usageStats.note && ( -
{usageStats.note}
- )} +
Claude Runs (all time){usageStats.claude_runs_total ?? '—'}
+
Active Sessions{usageStats.session_count ?? '—'}
+
First Run{usageStats.first_run ? new Date(usageStats.first_run).toLocaleString() : '—'}
+
Last Run{usageStats.last_run ? new Date(usageStats.last_run).toLocaleString() : '—'}
+
🔄 Next Monthly Reset{usageStats.next_reset ? new Date(usageStats.next_reset).toLocaleDateString() : '—'}
+
⏳ Days Until Reset{usageStats.days_until_reset ?? '—'}
) : ( -
Loading usage stats…
+
Loading usage stats...
)}
+ {/* Task Breakdown */}

Task Breakdown

@@ -481,41 +412,20 @@ const App = () => { {activeView === 'tasks' && (<>