Commit 7fdaca09 authored by Your Name's avatar Your Name

v0.99.32 - Fix OAuth authentication UI and restore full functionality

- Fixed duplicate authentication area in providers dashboard
- Restored full OAuth authentication with popup windows for all providers
- Added missing JavaScript authentication functions (authenticateClaude, authenticateQwen, authenticateCodex, authenticateKilo)
- Added missing backend endpoint /dashboard/providers/{provider_name}/auth/check
- Fixed import errors and attribute access for OAuth provider configs
- Fixed credential structure access for each OAuth provider type
- Fixed polling status responses (approved -> completed)
- Added human-readable expiration time formatting (days, hours, minutes, seconds)
- All OAuth flows now work with proper popup windows, polling, and status updates
parent e7663cb5
...@@ -54,7 +54,7 @@ from .auth.qwen import QwenOAuth2 ...@@ -54,7 +54,7 @@ from .auth.qwen import QwenOAuth2
from .handlers import RequestHandler, RotationHandler, AutoselectHandler from .handlers import RequestHandler, RotationHandler, AutoselectHandler
from .utils import count_messages_tokens, split_messages_into_chunks, get_max_request_tokens_for_model from .utils import count_messages_tokens, split_messages_into_chunks, get_max_request_tokens_for_model
__version__ = "0.99.31" __version__ = "0.99.32"
__all__ = [ __all__ = [
# Config # Config
"config", "config",
......
...@@ -32,6 +32,7 @@ from fastapi.templating import Jinja2Templates ...@@ -32,6 +32,7 @@ from fastapi.templating import Jinja2Templates
from jinja2 import Environment, FileSystemLoader from jinja2 import Environment, FileSystemLoader
from aisbf.models import ChatCompletionRequest, ChatCompletionResponse from aisbf.models import ChatCompletionRequest, ChatCompletionResponse
from aisbf.handlers import RequestHandler, RotationHandler, AutoselectHandler from aisbf.handlers import RequestHandler, RotationHandler, AutoselectHandler
from aisbf.config import Config
from aisbf.mcp import mcp_server, MCPAuthLevel, load_mcp_config from aisbf.mcp import mcp_server, MCPAuthLevel, load_mcp_config
from aisbf.database import DatabaseRegistry from aisbf.database import DatabaseRegistry
from aisbf.cache import initialize_cache from aisbf.cache import initialize_cache
...@@ -5759,6 +5760,88 @@ async def dashboard_provider_file_delete( ...@@ -5759,6 +5760,88 @@ async def dashboard_provider_file_delete(
return JSONResponse(status_code=500, content={"error": str(e)}) return JSONResponse(status_code=500, content={"error": str(e)})
# OAuth authentication check endpoints for providers
@app.get("/dashboard/providers/{provider_name}/auth/check")
async def dashboard_provider_auth_check(request: Request, provider_name: str):
"""Check OAuth authentication status for a provider"""
auth_check = require_admin(request)
if auth_check:
return auth_check
try:
# Load current provider configuration
config = Config()
provider_config = config.providers.get(provider_name)
if not provider_config:
return JSONResponse(
status_code=404,
content={"authenticated": False, "error": f"Provider '{provider_name}' not found"}
)
provider_type = provider_config.type
if provider_type == 'claude':
from aisbf.auth.claude import ClaudeAuth
claude_config = provider_config.claude_config or {}
auth = ClaudeAuth(credentials_file=claude_config.get('credentials_file', '~/.claude_credentials.json'))
is_auth = auth.is_authenticated()
result = {"authenticated": is_auth}
if is_auth and auth.tokens and 'expires_at' in auth.tokens:
result["expires_at"] = auth.tokens['expires_at']
return JSONResponse(result)
elif provider_type == 'kilocode':
from aisbf.auth.kilo import KiloOAuth2
kilo_config = provider_config.kilo_config or {}
auth = KiloOAuth2(credentials_file=kilo_config.get('credentials_file', '~/.kilo_credentials.json'))
is_auth = auth.is_authenticated()
result = {"authenticated": is_auth}
if is_auth and auth.credentials:
expires = auth.credentials.get('expires', 0)
if expires:
result["expires_at"] = expires
return JSONResponse(result)
elif provider_type == 'qwen':
from aisbf.auth.qwen import QwenOAuth2
qwen_config = provider_config.qwen_config or {}
auth = QwenOAuth2(credentials_file=qwen_config.get('credentials_file', '~/.aisbf/qwen_credentials.json'))
is_auth = auth.is_authenticated()
result = {"authenticated": is_auth}
if is_auth and auth.credentials:
expiry_date = auth.credentials.get('expiry_date', 0)
if expiry_date:
# Convert from milliseconds to seconds
result["expires_at"] = expiry_date / 1000
return JSONResponse(result)
elif provider_type == 'codex':
from aisbf.auth.codex import CodexOAuth2
codex_config = provider_config.codex_config or {}
auth = CodexOAuth2(credentials_file=codex_config.get('credentials_file', '~/.aisbf/codex_credentials.json'))
is_auth = auth.is_authenticated()
result = {"authenticated": is_auth}
if is_auth and auth.credentials:
expires = auth.credentials.get('expires', 0)
if expires:
result["expires_at"] = expires
return JSONResponse(result)
else:
return JSONResponse(
status_code=400,
content={"authenticated": False, "error": f"Provider type '{provider_type}' does not support OAuth authentication checks"}
)
except Exception as e:
logger.error(f"Error checking auth for provider {provider_name}: {e}")
return JSONResponse(
status_code=500,
content={"authenticated": False, "error": str(e)}
)
# User-specific rotation management routes # User-specific rotation management routes
@app.get("/dashboard/user/rotations", response_class=HTMLResponse) @app.get("/dashboard/user/rotations", response_class=HTMLResponse)
async def dashboard_user_rotations(request: Request): async def dashboard_user_rotations(request: Request):
...@@ -12065,7 +12148,7 @@ async def dashboard_kilo_auth_poll(request: Request): ...@@ -12065,7 +12148,7 @@ async def dashboard_kilo_auth_poll(request: Request):
return JSONResponse({ return JSONResponse({
"success": True, "success": True,
"status": "approved", "status": "completed",
"message": "Authentication completed successfully" "message": "Authentication completed successfully"
}) })
elif result['status'] == 'pending': elif result['status'] == 'pending':
...@@ -12750,7 +12833,7 @@ async def dashboard_qwen_auth_poll(request: Request): ...@@ -12750,7 +12833,7 @@ async def dashboard_qwen_auth_poll(request: Request):
return JSONResponse({ return JSONResponse({
"success": True, "success": True,
"status": "approved", "status": "completed",
"message": "Authentication completed successfully" "message": "Authentication completed successfully"
}) })
elif result is None: elif result is None:
......
...@@ -49,7 +49,7 @@ class InstallCommand(_install): ...@@ -49,7 +49,7 @@ class InstallCommand(_install):
setup( setup(
name="aisbf", name="aisbf",
version="0.99.31", version="0.99.32",
author="AISBF Contributors", author="AISBF Contributors",
author_email="stefy@nexlab.net", author_email="stefy@nexlab.net",
description="AISBF - AI Service Broker Framework || AI Should Be Free - A modular proxy server for managing multiple AI provider integrations", description="AISBF - AI Service Broker Framework || AI Should Be Free - A modular proxy server for managing multiple AI provider integrations",
......
...@@ -632,9 +632,7 @@ function renderProviderDetails(key) { ...@@ -632,9 +632,7 @@ function renderProviderDetails(key) {
</div> </div>
</div> </div>
</div> </div>
${authFieldsHtml}
<div class="form-group"> <div class="form-group">
<label>Rate Limit (seconds)</label> <label>Rate Limit (seconds)</label>
<input type="number" value="${provider.rate_limit || 0}" onchange="updateProvider('${key}', 'rate_limit', parseFloat(this.value))" step="0.1"> <input type="number" value="${provider.rate_limit || 0}" onchange="updateProvider('${key}', 'rate_limit', parseFloat(this.value))" step="0.1">
...@@ -878,6 +876,615 @@ function togglePricingFields(key) { ...@@ -878,6 +876,615 @@ function togglePricingFields(key) {
} }
} }
// Helper function to format expiration time
function formatExpirationTime(expiresAt) {
if (!expiresAt) return 'Unknown';
const now = Math.floor(Date.now() / 1000);
const secondsRemaining = expiresAt - now;
if (secondsRemaining <= 0) {
return 'Expired';
}
const days = Math.floor(secondsRemaining / 86400);
const hours = Math.floor((secondsRemaining % 86400) / 3600);
const minutes = Math.floor((secondsRemaining % 3600) / 60);
const seconds = secondsRemaining % 60;
if (days > 0) {
return `${days} day${days > 1 ? 's' : ''} ${hours} hour${hours !== 1 ? 's' : ''}`;
} else if (hours > 0) {
return `${hours} hour${hours > 1 ? 's' : ''} ${minutes} minute${minutes !== 1 ? 's' : ''}`;
} else if (minutes > 0) {
return `${minutes} minute${minutes > 1 ? 's' : ''} ${seconds} second${seconds !== 1 ? 's' : ''}`;
} else {
return `${seconds} second${seconds > 1 ? 's' : ''}`;
}
}
// OAuth authentication check functions
async function checkClaudeAuth(providerKey) {
const statusDiv = document.getElementById(`claude-auth-status-${providerKey}`);
if (!statusDiv) return;
statusDiv.style.display = 'block';
statusDiv.style.background = '#f0f0f0';
statusDiv.style.color = '#333';
statusDiv.innerHTML = 'Checking Claude authentication status...';
try {
const response = await fetch(`/dashboard/providers/${providerKey}/auth/check`, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
}
});
const result = await response.json();
if (response.ok && result.authenticated) {
const expirationText = result.expires_at ? formatExpirationTime(result.expires_at) : 'Unknown';
statusDiv.style.background = '#d4edda';
statusDiv.style.color = '#155724';
statusDiv.innerHTML = `✅ Claude authentication is valid. Expires in: ${expirationText}`;
} else {
statusDiv.style.background = '#f8d7da';
statusDiv.style.color = '#721c24';
statusDiv.innerHTML = `❌ Claude authentication failed: ${result.error || 'Not authenticated'}`;
}
} catch (error) {
statusDiv.style.background = '#f8d7da';
statusDiv.style.color = '#721c24';
statusDiv.innerHTML = `❌ Error checking Claude auth: ${error.message}`;
}
}
async function checkQwenAuth(providerKey) {
const statusDiv = document.getElementById(`qwen-auth-status-${providerKey}`);
if (!statusDiv) return;
statusDiv.style.display = 'block';
statusDiv.style.background = '#f0f0f0';
statusDiv.style.color = '#333';
statusDiv.innerHTML = 'Checking Qwen authentication status...';
try {
const response = await fetch(`/dashboard/providers/${providerKey}/auth/check`, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
}
});
const result = await response.json();
if (response.ok && result.authenticated) {
const expirationText = result.expires_at ? formatExpirationTime(result.expires_at) : 'Unknown';
statusDiv.style.background = '#d4edda';
statusDiv.style.color = '#155724';
statusDiv.innerHTML = `✅ Qwen authentication is valid. Expires in: ${expirationText}`;
} else {
statusDiv.style.background = '#f8d7da';
statusDiv.style.color = '#721c24';
statusDiv.innerHTML = `❌ Qwen authentication failed: ${result.error || 'Not authenticated'}`;
}
} catch (error) {
statusDiv.style.background = '#f8d7da';
statusDiv.style.color = '#721c24';
statusDiv.innerHTML = `❌ Error checking Qwen auth: ${error.message}`;
}
}
async function checkCodexAuth(providerKey) {
const statusDiv = document.getElementById(`codex-auth-status-${providerKey}`);
if (!statusDiv) return;
statusDiv.style.display = 'block';
statusDiv.style.background = '#f0f0f0';
statusDiv.style.color = '#333';
statusDiv.innerHTML = 'Checking Codex authentication status...';
try {
const response = await fetch(`/dashboard/providers/${providerKey}/auth/check`, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
}
});
const result = await response.json();
if (response.ok && result.authenticated) {
const expirationText = result.expires_at ? formatExpirationTime(result.expires_at) : 'Unknown';
statusDiv.style.background = '#d4edda';
statusDiv.style.color = '#155724';
statusDiv.innerHTML = `✅ Codex authentication is valid. Expires in: ${expirationText}`;
} else {
statusDiv.style.background = '#f8d7da';
statusDiv.style.color = '#721c24';
statusDiv.innerHTML = `❌ Codex authentication failed: ${result.error || 'Not authenticated'}`;
}
} catch (error) {
statusDiv.style.background = '#f8d7da';
statusDiv.style.color = '#721c24';
statusDiv.innerHTML = `❌ Error checking Codex auth: ${error.message}`;
}
}
async function checkKiloAuth(providerKey) {
const statusDiv = document.getElementById(`kilo-auth-status-${providerKey}`);
if (!statusDiv) return;
statusDiv.style.display = 'block';
statusDiv.style.background = '#f0f0f0';
statusDiv.style.color = '#333';
statusDiv.innerHTML = 'Checking Kilocode authentication status...';
try {
const response = await fetch(`/dashboard/providers/${providerKey}/auth/check`, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
}
});
const result = await response.json();
if (response.ok && result.authenticated) {
const expirationText = result.expires_at ? formatExpirationTime(result.expires_at) : 'Unknown';
statusDiv.style.background = '#d4edda';
statusDiv.style.color = '#155724';
statusDiv.innerHTML = `✅ Kilocode authentication is valid. Expires in: ${expirationText}`;
} else {
statusDiv.style.background = '#f8d7da';
statusDiv.style.color = '#721c24';
statusDiv.innerHTML = `❌ Kilocode authentication failed: ${result.error || 'Not authenticated'}`;
}
} catch (error) {
statusDiv.style.background = '#f8d7da';
statusDiv.style.color = '#721c24';
statusDiv.innerHTML = `❌ Error checking Kilocode auth: ${error.message}`;
}
}
// OAuth authentication functions
async function authenticateClaude(key) {
const statusEl = document.getElementById(`claude-auth-status-${key}`);
statusEl.style.display = 'block';
statusEl.style.background = '#0f2840';
statusEl.style.border = '1px solid #4a9eff';
statusEl.innerHTML = '<p style="margin: 0; color: #4a9eff;">🔄 Starting OAuth2 authentication flow...</p>';
try {
const response = await fetch('/dashboard/claude/auth/start', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
provider_key: key,
credentials_file: providersData[key].claude_config?.credentials_file || '~/.claude_credentials.json'
})
});
const data = await response.json();
if (!data.success || !data.auth_url) {
statusEl.style.background = '#402010';
statusEl.style.border = '1px solid #ff4a4a';
statusEl.innerHTML = `<p style="margin: 0; color: #ff4a4a;">✗ Failed to start authentication: ${data.error || 'Unknown error'}</p>`;
return;
}
statusEl.innerHTML = '<p style="margin: 0; color: #4a9eff;">🔄 Opening authentication window...</p>';
// Open OAuth2 URL in popup window
const authWindow = window.open(data.auth_url, 'claude-auth', 'width=600,height=700');
statusEl.innerHTML = '<p style="margin: 0; color: #4a9eff;">🔄 Please complete authentication in the popup window...</p>';
// Poll for completion
let pollCount = 0;
const maxPolls = 60;
let isCompleting = false;
await new Promise(resolve => setTimeout(resolve, 8000));
const pollInterval = setInterval(async () => {
pollCount++;
try {
const statusResponse = await fetch('/dashboard/claude/auth/callback-status');
const statusData = await statusResponse.json();
if (statusData.received) {
clearInterval(pollInterval);
if (authWindow && !authWindow.closed) {
authWindow.close();
}
if (statusData.error) {
statusEl.style.background = '#402010';
statusEl.style.border = '1px solid #ff4a4a';
statusEl.innerHTML = `<p style="margin: 0; color: #ff4a4a;">✗ OAuth2 error: ${statusData.error}</p>`;
return;
}
if (isCompleting) return;
isCompleting = true;
await new Promise(resolve => setTimeout(resolve, 1000));
try {
statusEl.innerHTML = '<p style="margin: 0; color: #4a9eff;">🔄 Completing authentication...</p>';
const completeResponse = await fetch('/dashboard/claude/auth/complete', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
}
});
const completeData = await completeResponse.json();
if (completeData.success) {
statusEl.style.background = '#0f4020';
statusEl.style.border = '1px solid #4eff9e';
statusEl.innerHTML = '<p style="margin: 0; color: #4eff9e;">✓ Authentication successful! Credentials saved.</p>';
} else {
statusEl.style.background = '#402010';
statusEl.style.border = '1px solid #ff4a4a';
statusEl.innerHTML = `<p style="margin: 0; color: #ff4a4a;">✗ Authentication incomplete. ${completeData.error || 'Please try again.'}</p>`;
}
} catch (error) {
statusEl.style.background = '#402010';
statusEl.style.border = '1px solid #ff4a4a';
statusEl.innerHTML = `<p style="margin: 0; color: #ff4a4a;">✗ Error completing authentication: ${error.message}</p>`;
}
}
} catch (error) {
console.error('Error checking callback status:', error);
}
if (pollCount >= maxPolls) {
clearInterval(pollInterval);
if (authWindow && !authWindow.closed) {
authWindow.close();
}
statusEl.style.background = '#402010';
statusEl.style.border = '1px solid #ff4a4a';
statusEl.innerHTML = '<p style="margin: 0; color: #ff4a4a;">✗ Authentication timeout. Please try again.</p>';
}
}, 2000);
} catch (error) {
statusEl.style.background = '#402010';
statusEl.style.border = '1px solid #ff4a4a';
statusEl.innerHTML = `<p style="margin: 0; color: #ff4a4a;">✗ Error: ${error.message}</p>`;
}
}
async function authenticateQwen(key) {
const statusEl = document.getElementById(`qwen-auth-status-${key}`);
statusEl.style.display = 'block';
statusEl.style.background = '#0f2840';
statusEl.style.border = '1px solid #4a9eff';
statusEl.innerHTML = '<p style="margin: 0; color: #4a9eff;">🔄 Starting Qwen OAuth2 Device Authorization flow...</p>';
try {
const response = await fetch('/dashboard/qwen/auth/start', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
provider_key: key,
credentials_file: providersData[key].qwen_config?.credentials_file || '~/.aisbf/qwen_credentials.json'
})
});
const data = await response.json();
if (!data.success) {
statusEl.style.background = '#402010';
statusEl.style.border = '1px solid #ff4a4a';
statusEl.innerHTML = `<p style="margin: 0; color: #ff4a4a;">✗ Failed to start authentication: ${data.error || 'Unknown error'}</p>`;
return;
}
statusEl.style.background = '#0f2840';
statusEl.style.border = '1px solid #4a9eff';
statusEl.innerHTML = `
<div style="margin: 0;">
<p style="margin: 0 0 10px 0; color: #4a9eff; font-weight: bold;">🔐 Qwen Device Authorization</p>
<p style="margin: 0 0 10px 0; color: #e0e0e0;">
Please visit: <a href="${data.verification_uri}" target="_blank" style="color: #4eff9e; text-decoration: underline;">${data.verification_uri}</a>
</p>
<p style="margin: 0 0 10px 0; color: #e0e0e0;">
Enter code: <strong style="color: #4eff9e; font-size: 18px; letter-spacing: 2px;">${data.user_code}</strong>
</p>
<p style="margin: 0; color: #a0a0a0; font-size: 13px;">
Waiting for authorization... (expires in ${Math.floor(data.expires_in / 60)} minutes)
</p>
</div>
`;
try {
window.open(data.verification_uri, 'qwen-auth', 'width=600,height=700');
} catch (e) {
console.error('Could not open auth window:', e);
}
let pollCount = 0;
const maxPolls = Math.floor(data.expires_in / data.interval);
const pollInterval = setInterval(async () => {
pollCount++;
try {
const pollResponse = await fetch('/dashboard/qwen/auth/poll', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
}
});
const pollData = await pollResponse.json();
if (pollData.status === 'completed') {
clearInterval(pollInterval);
statusEl.style.background = '#0f4020';
statusEl.style.border = '1px solid #4eff9e';
statusEl.innerHTML = '<p style="margin: 0; color: #4eff9e;">✓ Qwen authentication successful! Credentials saved.</p>';
} else if (pollData.status === 'denied') {
clearInterval(pollInterval);
statusEl.style.background = '#402010';
statusEl.style.border = '1px solid #ff4a4a';
statusEl.innerHTML = '<p style="margin: 0; color: #ff4a4a;">✗ Authorization denied by user.</p>';
} else if (pollData.status === 'expired') {
clearInterval(pollInterval);
statusEl.style.background = '#402010';
statusEl.style.border = '1px solid #ff4a4a';
statusEl.innerHTML = '<p style="margin: 0; color: #ff4a4a;">✗ Authorization code expired. Please try again.</p>';
}
} catch (error) {
console.error('Error polling Qwen auth:', error);
}
if (pollCount >= maxPolls) {
clearInterval(pollInterval);
statusEl.style.background = '#402010';
statusEl.style.border = '1px solid #ff4a4a';
statusEl.innerHTML = '<p style="margin: 0; color: #ff4a4a;">✗ Authentication timeout. Please try again.</p>';
}
}, data.interval * 1000);
} catch (error) {
statusEl.style.background = '#402010';
statusEl.style.border = '1px solid #ff4a4a';
statusEl.innerHTML = `<p style="margin: 0; color: #ff4a4a;">✗ Error: ${error.message}</p>`;
}
}
async function pollQwenAuth(providerKey, deviceCode) {
// This function is no longer needed as polling is handled in authenticateQwen
}
async function authenticateCodex(key) {
const statusEl = document.getElementById(`codex-auth-status-${key}`);
statusEl.style.display = 'block';
statusEl.style.background = '#0f2840';
statusEl.style.border = '1px solid #4a9eff';
statusEl.innerHTML = '<p style="margin: 0; color: #4a9eff;">🔄 Starting Codex OAuth2 Device Authorization flow...</p>';
try {
const response = await fetch('/dashboard/codex/auth/start', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
provider_key: key,
credentials_file: providersData[key].codex_config?.credentials_file || '~/.aisbf/codex_credentials.json',
issuer: providersData[key].codex_config?.issuer || 'https://auth.openai.com'
})
});
const data = await response.json();
if (!data.success) {
statusEl.style.background = '#402010';
statusEl.style.border = '1px solid #ff4a4a';
statusEl.innerHTML = `<p style="margin: 0; color: #ff4a4a;">✗ Failed to start authentication: ${data.error || 'Unknown error'}</p>`;
return;
}
statusEl.style.background = '#0f2840';
statusEl.style.border = '1px solid #4a9eff';
statusEl.innerHTML = `
<div style="margin: 0;">
<p style="margin: 0 0 10px 0; color: #4a9eff; font-weight: bold;">🔐 Codex Device Authorization</p>
<p style="margin: 0 0 10px 0; color: #e0e0e0;">
Please visit: <a href="${data.verification_uri}" target="_blank" style="color: #4eff9e; text-decoration: underline;">${data.verification_uri}</a>
</p>
<p style="margin: 0 0 10px 0; color: #e0e0e0;">
Enter code: <strong style="color: #4eff9e; font-size: 18px; letter-spacing: 2px;">${data.user_code}</strong>
</p>
<p style="margin: 0; color: #a0a0a0; font-size: 13px;">
Waiting for authorization... (expires in ${Math.floor(data.expires_in / 60)} minutes)
</p>
</div>
`;
try {
window.open(data.verification_uri, 'codex-auth', 'width=600,height=700');
} catch (e) {
console.error('Could not open auth window:', e);
}
let pollCount = 0;
const maxPolls = Math.floor(data.expires_in / data.interval);
const pollInterval = setInterval(async () => {
pollCount++;
try {
const pollResponse = await fetch('/dashboard/codex/auth/poll', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
}
});
const pollData = await pollResponse.json();
if (pollData.status === 'approved') {
clearInterval(pollInterval);
statusEl.style.background = '#0f4020';
statusEl.style.border = '1px solid #4eff9e';
statusEl.innerHTML = '<p style="margin: 0; color: #4eff9e;">✓ Codex authentication successful! Credentials saved.</p>';
} else if (pollData.status === 'denied') {
clearInterval(pollInterval);
statusEl.style.background = '#402010';
statusEl.style.border = '1px solid #ff4a4a';
statusEl.innerHTML = '<p style="margin: 0; color: #ff4a4a;">✗ Authorization denied by user.</p>';
} else if (pollData.status === 'expired') {
clearInterval(pollInterval);
statusEl.style.background = '#402010';
statusEl.style.border = '1px solid #ff4a4a';
statusEl.innerHTML = '<p style="margin: 0; color: #ff4a4a;">✗ Authorization code expired. Please try again.</p>';
}
} catch (error) {
console.error('Error polling Codex auth:', error);
}
if (pollCount >= maxPolls) {
clearInterval(pollInterval);
statusEl.style.background = '#402010';
statusEl.style.border = '1px solid #ff4a4a';
statusEl.innerHTML = '<p style="margin: 0; color: #ff4a4a;">✗ Authentication timeout. Please try again.</p>';
}
}, data.interval * 1000);
} catch (error) {
statusEl.style.background = '#402010';
statusEl.style.border = '1px solid #ff4a4a';
statusEl.innerHTML = `<p style="margin: 0; color: #ff4a4a;">✗ Error: ${error.message}</p>`;
}
}
async function pollCodexAuth(providerKey, deviceCode) {
// This function is no longer needed as polling is handled in authenticateCodex
}
async function authenticateKilo(key) {
const statusEl = document.getElementById(`kilo-auth-status-${key}`);
statusEl.style.display = 'block';
statusEl.style.background = '#0f2840';
statusEl.style.border = '1px solid #4a9eff';
statusEl.innerHTML = '<p style="margin: 0; color: #4a9eff;">🔄 Starting Kilocode OAuth2 Device Authorization flow...</p>';
try {
const response = await fetch('/dashboard/kilo/auth/start', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
provider_key: key,
credentials_file: providersData[key].kilo_config?.credentials_file || '~/.kilo_credentials.json'
})
});
const data = await response.json();
if (!data.success) {
statusEl.style.background = '#402010';
statusEl.style.border = '1px solid #ff4a4a';
statusEl.innerHTML = `<p style="margin: 0; color: #ff4a4a;">✗ Failed to start authentication: ${data.error || 'Unknown error'}</p>`;
return;
}
statusEl.style.background = '#0f2840';
statusEl.style.border = '1px solid #4a9eff';
statusEl.innerHTML = `
<div style="margin: 0;">
<p style="margin: 0 0 10px 0; color: #4a9eff; font-weight: bold;">🔐 Kilocode Device Authorization</p>
<p style="margin: 0 0 10px 0; color: #e0e0e0;">
Please visit: <a href="${data.verification_uri}" target="_blank" style="color: #4eff9e; text-decoration: underline;">${data.verification_uri}</a>
</p>
<p style="margin: 0 0 10px 0; color: #e0e0e0;">
Enter code: <strong style="color: #4eff9e; font-size: 18px; letter-spacing: 2px;">${data.user_code}</strong>
</p>
<p style="margin: 0; color: #a0a0a0; font-size: 13px;">
Waiting for authorization... (expires in ${Math.floor(data.expires_in / 60)} minutes)
</p>
</div>
`;
try {
window.open(data.verification_uri, 'kilo-auth', 'width=600,height=700');
} catch (e) {
console.error('Could not open auth window:', e);
}
let pollCount = 0;
const maxPolls = Math.floor(data.expires_in / data.interval);
const pollInterval = setInterval(async () => {
pollCount++;
try {
const pollResponse = await fetch('/dashboard/kilo/auth/poll', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
}
});
const pollData = await pollResponse.json();
if (pollData.status === 'completed') {
clearInterval(pollInterval);
statusEl.style.background = '#0f4020';
statusEl.style.border = '1px solid #4eff9e';
statusEl.innerHTML = '<p style="margin: 0; color: #4eff9e;">✓ Kilocode authentication successful! Credentials saved.</p>';
} else if (pollData.status === 'denied') {
clearInterval(pollInterval);
statusEl.style.background = '#402010';
statusEl.style.border = '1px solid #ff4a4a';
statusEl.innerHTML = '<p style="margin: 0; color: #ff4a4a;">✗ Authorization denied by user.</p>';
} else if (pollData.status === 'expired') {
clearInterval(pollInterval);
statusEl.style.background = '#402010';
statusEl.style.border = '1px solid #ff4a4a';
statusEl.innerHTML = '<p style="margin: 0; color: #ff4a4a;">✗ Authorization code expired. Please try again.</p>';
}
} catch (error) {
console.error('Error polling Kilo auth:', error);
}
if (pollCount >= maxPolls) {
clearInterval(pollInterval);
statusEl.style.background = '#402010';
statusEl.style.border = '1px solid #ff4a4a';
statusEl.innerHTML = '<p style="margin: 0; color: #ff4a4a;">✗ Authentication timeout. Please try again.</p>';
}
}, data.interval * 1000);
} catch (error) {
statusEl.style.background = '#402010';
statusEl.style.border = '1px solid #ff4a4a';
statusEl.innerHTML = `<p style="margin: 0; color: #ff4a4a;">✗ Error: ${error.message}</p>`;
}
}
async function pollKiloAuth(providerKey, deviceCode) {
// This function is no longer needed as polling is handled in authenticateKilo
}
// Initialize providers list immediately (DOM is already loaded since script is at end of body) // Initialize providers list immediately (DOM is already loaded since script is at end of body)
console.log('Initializing providers list...'); console.log('Initializing providers list...');
console.log('providersData:', Object.keys(providersData)); console.log('providersData:', Object.keys(providersData));
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment