auth
This commit is contained in:
parent
937827d08b
commit
643203b8a5
9 changed files with 822 additions and 44 deletions
|
|
@ -14,7 +14,23 @@
|
|||
</head>
|
||||
|
||||
<body class="dark-theme">
|
||||
<div id="app">
|
||||
<div id="login-overlay" class="login-overlay glass hidden">
|
||||
<div class="login-box">
|
||||
<span class="logo-icon">▲</span>
|
||||
<div class="login-title">Agency</div>
|
||||
<p>Secure Agent Dashboard</p>
|
||||
<button id="login-btn" class="btn btn-primary">Login with Authentik</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="callback-overlay" class="login-overlay glass hidden">
|
||||
<div class="login-box">
|
||||
<div class="loader"></div>
|
||||
<p>Authenticating with Authentik...</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="app" class="hidden">
|
||||
<aside class="sidebar glass">
|
||||
<header class="sidebar-header">
|
||||
<a href="#" id="logo-link" class="logo">
|
||||
|
|
@ -31,6 +47,11 @@
|
|||
<!-- Tasks will be injected here -->
|
||||
</ul>
|
||||
</nav>
|
||||
<footer class="sidebar-footer">
|
||||
<button id="logout-btn" class="btn btn-ghost btn-sm logout-btn">
|
||||
Logout
|
||||
</button>
|
||||
</footer>
|
||||
</aside>
|
||||
|
||||
<main class="main-content">
|
||||
|
|
@ -127,7 +148,7 @@
|
|||
<div class="form-group">
|
||||
<label>Execution Schedule</label>
|
||||
<div id="schedule-presets" class="preset-group">
|
||||
<button type="button" class="btn btn-preset active" data-cron="">One-time</button>
|
||||
<button type="button" class="btn btn-preset active" data-cron="">Manual</button>
|
||||
<button type="button" class="btn btn-preset" data-cron="0 */5 * * * *">Every 5m</button>
|
||||
<button type="button" class="btn btn-preset" data-cron="0 0 * * * *">Hourly</button>
|
||||
<button type="button" class="btn btn-preset" data-cron="0 0 0 * * *">Daily</button>
|
||||
|
|
@ -147,8 +168,7 @@
|
|||
|
||||
<div class="modal-actions">
|
||||
<button type="button" class="btn btn-ghost" id="cancel-task">Cancel</button>
|
||||
<button type="submit" class="btn btn-primary" id="submit-task-btn">Execute
|
||||
Directive</button>
|
||||
<button type="submit" class="btn btn-primary" id="submit-task-btn">Save Task</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -2,6 +2,13 @@ import { marked } from 'marked';
|
|||
import DOMPurify from 'dompurify';
|
||||
|
||||
const API_URL = 'http://localhost:3000';
|
||||
// These should ideally be environment-specific
|
||||
const AUTH_CONFIG = {
|
||||
issuer: 'https://idm.flegr.me/application/o/bot/',
|
||||
clientId: 'CicDk8mpSBY1SW4ofCamO3B583ttmvKTSPDQwvpb',
|
||||
redirectUri: window.location.origin + '/callback',
|
||||
authorizeEndpoint: 'https://idm.flegr.me/application/o/authorize/',
|
||||
};
|
||||
|
||||
const state = {
|
||||
tasks: [],
|
||||
|
|
@ -9,9 +16,15 @@ const state = {
|
|||
selectedRunId: null,
|
||||
currentView: 'dashboard', // 'dashboard' or 'task'
|
||||
isEditing: false,
|
||||
token: localStorage.getItem('auth_token')
|
||||
};
|
||||
|
||||
// DOM elements
|
||||
const loginOverlay = document.getElementById('login-overlay');
|
||||
const callbackOverlay = document.getElementById('callback-overlay');
|
||||
const loginBtn = document.getElementById('login-btn');
|
||||
const appEl = document.getElementById('app');
|
||||
const logoutBtn = document.getElementById('logout-btn');
|
||||
const taskListEl = document.getElementById('task-list');
|
||||
const newTaskBtn = document.getElementById('new-task-btn');
|
||||
const modalContainer = document.getElementById('modal-container');
|
||||
|
|
@ -42,9 +55,31 @@ const toggleCustomCronBtn = document.getElementById('toggle-custom-cron');
|
|||
const customCronContainer = document.getElementById('custom-cron-container');
|
||||
const presetBtns = document.querySelectorAll('.btn-preset');
|
||||
|
||||
// Wrapper for fetch to include Authorization header
|
||||
async function fetchWithAuth(url, options = {}) {
|
||||
if (!state.token) {
|
||||
showLogin();
|
||||
throw new Error('Not authenticated');
|
||||
}
|
||||
|
||||
const headers = {
|
||||
...options.headers,
|
||||
'Authorization': `Bearer ${state.token}`
|
||||
};
|
||||
|
||||
const response = await fetch(url, { ...options, headers });
|
||||
|
||||
if (response.status === 401) {
|
||||
logout();
|
||||
throw new Error('Session expired');
|
||||
}
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
async function fetchTasks() {
|
||||
try {
|
||||
const response = await fetch(`${API_URL}/tasks`);
|
||||
const response = await fetchWithAuth(`${API_URL}/tasks`);
|
||||
const newTasks = await response.json();
|
||||
|
||||
// Check if we should follow the latest run (if we were already watching it)
|
||||
|
|
@ -88,7 +123,7 @@ async function fetchTasks() {
|
|||
|
||||
async function fetchRecentRuns() {
|
||||
try {
|
||||
const response = await fetch(`${API_URL}/runs/recent`);
|
||||
const response = await fetchWithAuth(`${API_URL}/runs/recent`);
|
||||
const recentRuns = await response.json();
|
||||
renderDashboard(recentRuns);
|
||||
} catch (error) {
|
||||
|
|
@ -230,8 +265,11 @@ function showTaskView(task) {
|
|||
|
||||
if (!run) {
|
||||
viewStatusEl.textContent = 'No runs';
|
||||
logsOutputEl.innerHTML = '';
|
||||
answerContainerEl.classList.add('hidden');
|
||||
viewStatusEl.className = 'status-badge pending';
|
||||
viewDateEl.textContent = '-';
|
||||
logsOutputEl.innerHTML = '<div class="waiting-placeholder">No logs available. Click "Run Task" to start the agent.</div>';
|
||||
answerContainerEl.classList.remove('hidden');
|
||||
answerOutputEl.innerHTML = '<div class="waiting-placeholder">Waiting for the first execution...</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -245,12 +283,12 @@ function showTaskView(task) {
|
|||
minute: '2-digit'
|
||||
});
|
||||
|
||||
answerContainerEl.classList.remove('hidden');
|
||||
if (run.answer) {
|
||||
answerContainerEl.classList.remove('hidden');
|
||||
const rawHtml = marked.parse(run.answer);
|
||||
answerOutputEl.innerHTML = DOMPurify.sanitize(rawHtml);
|
||||
} else {
|
||||
answerContainerEl.classList.add('hidden');
|
||||
answerOutputEl.innerHTML = '<div class="waiting-placeholder">Agent is working on the final answer...</div>';
|
||||
}
|
||||
|
||||
// Check if we should auto-scroll
|
||||
|
|
@ -279,7 +317,7 @@ rerunBtn.addEventListener('click', async () => {
|
|||
if (!state.selectedTaskId) return;
|
||||
|
||||
try {
|
||||
const response = await fetch(`${API_URL}/tasks/${state.selectedTaskId}/runs`, {
|
||||
const response = await fetchWithAuth(`${API_URL}/tasks/${state.selectedTaskId}/runs`, {
|
||||
method: 'POST',
|
||||
});
|
||||
const updatedTask = await response.json();
|
||||
|
|
@ -289,15 +327,15 @@ rerunBtn.addEventListener('click', async () => {
|
|||
}
|
||||
selectTask(updatedTask.id);
|
||||
} catch (error) {
|
||||
console.error('Error re-running task:', error);
|
||||
alert('Failed to re-run task.');
|
||||
console.error('Error running task:', error);
|
||||
alert('Failed to run task.');
|
||||
}
|
||||
});
|
||||
|
||||
newTaskBtn.addEventListener('click', () => {
|
||||
state.isEditing = false;
|
||||
modalTitle.textContent = 'New Agent Task';
|
||||
submitTaskBtn.textContent = 'Execute Directive';
|
||||
submitTaskBtn.textContent = 'Save Task';
|
||||
goalTextarea.value = '';
|
||||
cronInput.value = '';
|
||||
updateScheduleUI('');
|
||||
|
|
@ -316,8 +354,8 @@ editTaskBtn.addEventListener('click', () => {
|
|||
if (!task) return;
|
||||
|
||||
state.isEditing = true;
|
||||
modalTitle.textContent = 'Edit Directive';
|
||||
submitTaskBtn.textContent = 'Update Directive';
|
||||
modalTitle.textContent = 'Edit Agent Task';
|
||||
submitTaskBtn.textContent = 'Save Task';
|
||||
goalTextarea.value = task.goal;
|
||||
cronInput.value = task.cron || '';
|
||||
updateScheduleUI(task.cron || '');
|
||||
|
|
@ -383,13 +421,13 @@ newTaskForm.addEventListener('submit', async (e) => {
|
|||
try {
|
||||
let response;
|
||||
if (state.isEditing) {
|
||||
response = await fetch(`${API_URL}/tasks/${state.selectedTaskId}`, {
|
||||
response = await fetchWithAuth(`${API_URL}/tasks/${state.selectedTaskId}`, {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ goal, cron }),
|
||||
});
|
||||
} else {
|
||||
response = await fetch(`${API_URL}/tasks`, {
|
||||
response = await fetchWithAuth(`${API_URL}/tasks`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ goal, cron }),
|
||||
|
|
@ -415,11 +453,6 @@ newTaskForm.addEventListener('submit', async (e) => {
|
|||
}
|
||||
});
|
||||
|
||||
// Initial load
|
||||
// Initial fetch
|
||||
fetchTasks();
|
||||
|
||||
// Auto-refresh every 3 seconds
|
||||
let isPolling = false;
|
||||
async function startAutoRefresh() {
|
||||
setInterval(async () => {
|
||||
|
|
@ -433,4 +466,71 @@ async function startAutoRefresh() {
|
|||
}, 3000);
|
||||
}
|
||||
|
||||
startAutoRefresh();
|
||||
async function showLogin() {
|
||||
loginOverlay.classList.remove('hidden');
|
||||
appEl.classList.add('hidden');
|
||||
}
|
||||
|
||||
async function logout() {
|
||||
state.token = null;
|
||||
localStorage.removeItem('auth_token');
|
||||
showLogin();
|
||||
}
|
||||
|
||||
async function handleCallback() {
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
const code = params.get('code');
|
||||
if (!code) return;
|
||||
|
||||
window.history.replaceState({}, document.title, "/");
|
||||
callbackOverlay.classList.remove('hidden');
|
||||
loginOverlay.classList.add('hidden');
|
||||
|
||||
try {
|
||||
const response = await fetch(`${API_URL}/auth/callback?code=${code}&redirect_uri=${encodeURIComponent(AUTH_CONFIG.redirectUri)}`);
|
||||
const data = await response.json();
|
||||
|
||||
if (data.access_token) {
|
||||
state.token = data.access_token;
|
||||
localStorage.setItem('auth_token', data.access_token);
|
||||
callbackOverlay.classList.add('hidden');
|
||||
appEl.classList.remove('hidden');
|
||||
initializeApp();
|
||||
} else {
|
||||
throw new Error('No access token in response');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Auth callback failed:', error);
|
||||
alert('Authentication failed.');
|
||||
showLogin();
|
||||
}
|
||||
}
|
||||
|
||||
loginBtn.addEventListener('click', () => {
|
||||
const authUrl = `${AUTH_CONFIG.authorizeEndpoint}?client_id=${AUTH_CONFIG.clientId}&response_type=code&redirect_uri=${encodeURIComponent(AUTH_CONFIG.redirectUri)}&scope=openid profile email`;
|
||||
window.location.href = authUrl;
|
||||
});
|
||||
|
||||
logoutBtn.addEventListener('click', () => {
|
||||
logout();
|
||||
});
|
||||
|
||||
async function initializeApp() {
|
||||
if (!state.token) {
|
||||
showLogin();
|
||||
return;
|
||||
}
|
||||
|
||||
appEl.classList.remove('hidden');
|
||||
loginOverlay.classList.add('hidden');
|
||||
|
||||
await fetchTasks();
|
||||
startAutoRefresh();
|
||||
}
|
||||
|
||||
// Check for callback on load
|
||||
if (window.location.pathname === '/callback' || window.location.search.includes('code=')) {
|
||||
handleCallback();
|
||||
} else {
|
||||
initializeApp();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -434,6 +434,21 @@ body {
|
|||
padding-bottom: 8px;
|
||||
}
|
||||
|
||||
.answer-output {
|
||||
min-height: 100px;
|
||||
}
|
||||
|
||||
.waiting-placeholder {
|
||||
color: var(--text-dim);
|
||||
font-style: italic;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100px;
|
||||
background: rgba(255, 255, 255, 0.02);
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.answer-output h2 {
|
||||
font-size: 20px;
|
||||
}
|
||||
|
|
@ -711,4 +726,91 @@ textarea:focus {
|
|||
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background: var(--text-dim);
|
||||
}
|
||||
|
||||
.login-overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 1000;
|
||||
background: rgba(10, 10, 15, 0.8);
|
||||
backdrop-filter: blur(20px);
|
||||
}
|
||||
|
||||
.login-box {
|
||||
text-align: center;
|
||||
max-width: 400px;
|
||||
width: 90%;
|
||||
padding: 40px;
|
||||
border-radius: 24px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.login-box .logo-icon {
|
||||
font-size: 64px;
|
||||
margin-bottom: 16px;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.login-title {
|
||||
font-size: 32px;
|
||||
font-weight: 700;
|
||||
margin-bottom: 8px;
|
||||
letter-spacing: -0.02em;
|
||||
}
|
||||
|
||||
.login-box p {
|
||||
color: var(--text-dim);
|
||||
margin-bottom: 32px;
|
||||
}
|
||||
|
||||
.loader {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border: 4px solid rgba(255, 255, 255, 0.1);
|
||||
border-left-color: var(--primary);
|
||||
border-radius: 50%;
|
||||
animation: spin 1s linear infinite;
|
||||
margin: 0 auto 16px;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
.sidebar-footer {
|
||||
padding: 16px;
|
||||
border-top: 1px solid var(--glass-border);
|
||||
margin-top: auto;
|
||||
}
|
||||
|
||||
.btn-sm {
|
||||
padding: 6px 12px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.logout-btn {
|
||||
/* Styles for logout button, based on common patterns */
|
||||
background: transparent;
|
||||
color: var(--text-dim);
|
||||
border: none;
|
||||
padding: 8px 12px;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
transition: color 0.2s ease;
|
||||
}
|
||||
|
||||
.logout-btn:hover {
|
||||
color: var(--primary);
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue