Initial PULSE server commit with Authelia SSO integration
This commit is contained in:
32
.gitignore
vendored
Normal file
32
.gitignore
vendored
Normal file
@ -0,0 +1,32 @@
|
||||
# Node modules
|
||||
node_modules/
|
||||
|
||||
# Environment variables (NEVER commit these!)
|
||||
.env
|
||||
.env.local
|
||||
.env.*.local
|
||||
|
||||
# Database files
|
||||
*.db
|
||||
*.sqlite
|
||||
*.sqlite3
|
||||
|
||||
# Logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
logs/
|
||||
|
||||
# OS files
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
# Backups
|
||||
*.backup
|
||||
*.bak
|
||||
|
||||
# Editor files
|
||||
.vscode/
|
||||
.idea/
|
||||
*.swp
|
||||
*.swo
|
||||
*~
|
||||
1145
package-lock.json
generated
Normal file
1145
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
23
package.json
Normal file
23
package.json
Normal file
@ -0,0 +1,23 @@
|
||||
{
|
||||
"name": "pulse-server",
|
||||
"version": "1.0.0",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"description": "",
|
||||
"dependencies": {
|
||||
"bcryptjs": "^3.0.3",
|
||||
"body-parser": "^2.2.1",
|
||||
"cors": "^2.8.5",
|
||||
"dotenv": "^17.2.3",
|
||||
"express": "^5.1.0",
|
||||
"js-yaml": "^4.1.1",
|
||||
"jsonwebtoken": "^9.0.2",
|
||||
"mysql2": "^3.15.3",
|
||||
"ws": "^8.18.3"
|
||||
}
|
||||
}
|
||||
394
public/index.html
Normal file
394
public/index.html
Normal file
@ -0,0 +1,394 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>PULSE - Workflow Orchestration</title>
|
||||
<style>
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
min-height: 100vh;
|
||||
padding: 20px;
|
||||
}
|
||||
.container {
|
||||
max-width: 1400px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
.header {
|
||||
background: white;
|
||||
padding: 30px;
|
||||
border-radius: 10px;
|
||||
box-shadow: 0 10px 30px rgba(0,0,0,0.2);
|
||||
margin-bottom: 30px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
.header-left h1 {
|
||||
color: #667eea;
|
||||
font-size: 2.5em;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
.header-left p {
|
||||
color: #666;
|
||||
font-size: 1.1em;
|
||||
}
|
||||
.user-info {
|
||||
text-align: right;
|
||||
}
|
||||
.user-info .name {
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
font-size: 1.1em;
|
||||
}
|
||||
.user-info .email {
|
||||
color: #666;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
.user-info .badge {
|
||||
display: inline-block;
|
||||
background: #667eea;
|
||||
color: white;
|
||||
padding: 4px 12px;
|
||||
border-radius: 12px;
|
||||
font-size: 0.8em;
|
||||
margin-top: 5px;
|
||||
margin-left: 5px;
|
||||
}
|
||||
.grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(350px, 1fr));
|
||||
gap: 20px;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
.card {
|
||||
background: white;
|
||||
padding: 25px;
|
||||
border-radius: 10px;
|
||||
box-shadow: 0 5px 15px rgba(0,0,0,0.1);
|
||||
}
|
||||
.card h3 {
|
||||
color: #333;
|
||||
margin-bottom: 15px;
|
||||
font-size: 1.3em;
|
||||
}
|
||||
.status {
|
||||
display: inline-block;
|
||||
padding: 5px 15px;
|
||||
border-radius: 20px;
|
||||
font-size: 0.9em;
|
||||
font-weight: 600;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
.status.online { background: #10b981; color: white; }
|
||||
.status.offline { background: #ef4444; color: white; }
|
||||
.status.running { background: #3b82f6; color: white; }
|
||||
.status.completed { background: #10b981; color: white; }
|
||||
.status.failed { background: #ef4444; color: white; }
|
||||
button {
|
||||
background: #667eea;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 12px 24px;
|
||||
border-radius: 5px;
|
||||
cursor: pointer;
|
||||
font-size: 1em;
|
||||
font-weight: 600;
|
||||
transition: all 0.3s;
|
||||
margin-right: 10px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
button:hover {
|
||||
background: #5568d3;
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 5px 15px rgba(102, 126, 234, 0.4);
|
||||
}
|
||||
button.danger {
|
||||
background: #ef4444;
|
||||
}
|
||||
button.danger:hover {
|
||||
background: #dc2626;
|
||||
}
|
||||
.workflow-item, .execution-item, .worker-item {
|
||||
padding: 15px;
|
||||
border: 1px solid #e0e0e0;
|
||||
border-radius: 5px;
|
||||
margin-bottom: 10px;
|
||||
background: #f9f9f9;
|
||||
}
|
||||
.workflow-item:hover, .execution-item:hover, .worker-item:hover {
|
||||
background: #f0f0f0;
|
||||
}
|
||||
.workflow-name {
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
font-size: 1.1em;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
.workflow-desc {
|
||||
color: #666;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
.loading {
|
||||
text-align: center;
|
||||
padding: 20px;
|
||||
color: #666;
|
||||
}
|
||||
.empty {
|
||||
text-align: center;
|
||||
padding: 30px;
|
||||
color: #999;
|
||||
}
|
||||
.timestamp {
|
||||
font-size: 0.85em;
|
||||
color: #999;
|
||||
}
|
||||
.error {
|
||||
background: #fee2e2;
|
||||
border: 2px solid #ef4444;
|
||||
color: #991b1b;
|
||||
padding: 20px;
|
||||
border-radius: 10px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="header">
|
||||
<div class="header-left">
|
||||
<h1>⚡ PULSE</h1>
|
||||
<p>Pipelined Unified Logic & Server Engine</p>
|
||||
</div>
|
||||
<div class="user-info" id="userInfo">
|
||||
<div class="loading">Loading user...</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="authError" class="error" style="display: none;">
|
||||
<h3>⚠️ Authentication Required</h3>
|
||||
<p>Please access PULSE through <strong>https://pulse.lotusguild.org</strong> to authenticate via Authelia.</p>
|
||||
</div>
|
||||
|
||||
<div class="grid">
|
||||
<div class="card">
|
||||
<h3>👥 Workers</h3>
|
||||
<div id="workerStatus">
|
||||
<div class="loading">Loading...</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h3>📋 Workflows</h3>
|
||||
<div id="workflowList">
|
||||
<div class="loading">Loading...</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h3>🚀 Recent Executions</h3>
|
||||
<div id="executionList">
|
||||
<div class="loading">Loading...</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h3>⚡ Quick Actions</h3>
|
||||
<button onclick="refreshData()">🔄 Refresh Status</button>
|
||||
<button onclick="alert('Workflow creation UI coming soon!')">➕ Create Workflow</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
let currentUser = null;
|
||||
let ws = null;
|
||||
|
||||
async function loadUser() {
|
||||
try {
|
||||
const response = await fetch('/api/user');
|
||||
if (!response.ok) {
|
||||
document.getElementById('authError').style.display = 'block';
|
||||
document.getElementById('userInfo').innerHTML =
|
||||
'<div style="color: #ef4444;">⚠️ Not authenticated</div>';
|
||||
return false;
|
||||
}
|
||||
|
||||
currentUser = await response.json();
|
||||
document.getElementById('userInfo').innerHTML = `
|
||||
<div class="name">${currentUser.name}</div>
|
||||
<div class="email">${currentUser.email}</div>
|
||||
<div>${currentUser.groups.map(g =>
|
||||
`<span class="badge">${g}</span>`
|
||||
).join('')}</div>
|
||||
`;
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('Error loading user:', error);
|
||||
document.getElementById('authError').style.display = 'block';
|
||||
document.getElementById('userInfo').innerHTML =
|
||||
'<div style="color: #ef4444;">Error loading user</div>';
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async function loadWorkers() {
|
||||
try {
|
||||
const response = await fetch('/api/workers');
|
||||
const workers = await response.json();
|
||||
|
||||
if (workers.length === 0) {
|
||||
document.getElementById('workerStatus').innerHTML =
|
||||
'<div class="empty">No workers connected</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
document.getElementById('workerStatus').innerHTML = workers.map(w => `
|
||||
<div class="worker-item">
|
||||
<span class="status ${w.status}">${w.status}</span>
|
||||
<strong>${w.name}</strong>
|
||||
<div class="timestamp">Last seen: ${new Date(w.last_heartbeat).toLocaleString()}</div>
|
||||
</div>
|
||||
`).join('');
|
||||
} catch (error) {
|
||||
console.error('Error loading workers:', error);
|
||||
document.getElementById('workerStatus').innerHTML =
|
||||
'<div class="empty">Error loading workers</div>';
|
||||
}
|
||||
}
|
||||
|
||||
async function loadWorkflows() {
|
||||
try {
|
||||
const response = await fetch('/api/workflows');
|
||||
const workflows = await response.json();
|
||||
|
||||
if (workflows.length === 0) {
|
||||
document.getElementById('workflowList').innerHTML =
|
||||
'<div class="empty">No workflows defined yet</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
document.getElementById('workflowList').innerHTML = workflows.map(w => `
|
||||
<div class="workflow-item">
|
||||
<div class="workflow-name">${w.name}</div>
|
||||
<div class="workflow-desc">${w.description || 'No description'}</div>
|
||||
<div style="margin-top: 10px;">
|
||||
<button onclick="executeWorkflow('${w.id}')">▶️ Execute</button>
|
||||
${currentUser && currentUser.isAdmin ?
|
||||
`<button class="danger" onclick="deleteWorkflow('${w.id}', '${w.name}')">🗑️ Delete</button>`
|
||||
: ''}
|
||||
</div>
|
||||
</div>
|
||||
`).join('');
|
||||
} catch (error) {
|
||||
console.error('Error loading workflows:', error);
|
||||
document.getElementById('workflowList').innerHTML =
|
||||
'<div class="empty">Error loading workflows</div>';
|
||||
}
|
||||
}
|
||||
|
||||
async function loadExecutions() {
|
||||
try {
|
||||
const response = await fetch('/api/executions');
|
||||
const executions = await response.json();
|
||||
|
||||
if (executions.length === 0) {
|
||||
document.getElementById('executionList').innerHTML =
|
||||
'<div class="empty">No executions yet</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
document.getElementById('executionList').innerHTML = executions.slice(0, 10).map(e => `
|
||||
<div class="execution-item">
|
||||
<span class="status ${e.status}">${e.status}</span>
|
||||
<strong>${e.workflow_name || 'Unknown Workflow'}</strong>
|
||||
<div class="timestamp">
|
||||
Started by ${e.started_by} at ${new Date(e.started_at).toLocaleString()}
|
||||
</div>
|
||||
</div>
|
||||
`).join('');
|
||||
} catch (error) {
|
||||
console.error('Error loading executions:', error);
|
||||
document.getElementById('executionList').innerHTML =
|
||||
'<div class="empty">Error loading executions</div>';
|
||||
}
|
||||
}
|
||||
|
||||
async function executeWorkflow(workflowId) {
|
||||
if (!confirm('Execute this workflow?')) return;
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/executions', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ workflow_id: workflowId })
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
alert('Workflow execution started!');
|
||||
loadExecutions();
|
||||
} else {
|
||||
alert('Failed to start workflow');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error executing workflow:', error);
|
||||
alert('Error executing workflow');
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteWorkflow(workflowId, name) {
|
||||
if (!confirm(`Delete workflow "${name}"? This cannot be undone.`)) return;
|
||||
|
||||
try {
|
||||
const response = await fetch(`/api/workflows/${workflowId}`, {
|
||||
method: 'DELETE'
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
alert('Workflow deleted');
|
||||
loadWorkflows();
|
||||
} else {
|
||||
const data = await response.json();
|
||||
alert(data.error || 'Failed to delete workflow');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error deleting workflow:', error);
|
||||
alert('Error deleting workflow');
|
||||
}
|
||||
}
|
||||
|
||||
function refreshData() {
|
||||
loadWorkers();
|
||||
loadWorkflows();
|
||||
loadExecutions();
|
||||
}
|
||||
|
||||
function connectWebSocket() {
|
||||
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
|
||||
ws = new WebSocket(`${protocol}//${window.location.host}`);
|
||||
|
||||
ws.onmessage = (event) => {
|
||||
const data = JSON.parse(event.data);
|
||||
console.log('WebSocket message:', data);
|
||||
refreshData();
|
||||
};
|
||||
|
||||
ws.onclose = () => {
|
||||
console.log('WebSocket closed, reconnecting...');
|
||||
setTimeout(connectWebSocket, 5000);
|
||||
};
|
||||
}
|
||||
|
||||
// Initialize
|
||||
loadUser().then((success) => {
|
||||
if (success) {
|
||||
refreshData();
|
||||
connectWebSocket();
|
||||
setInterval(refreshData, 30000); // Refresh every 30 seconds
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
328
server.js
Normal file
328
server.js
Normal file
@ -0,0 +1,328 @@
|
||||
const express = require('express');
|
||||
const http = require('http');
|
||||
const WebSocket = require('ws');
|
||||
const mysql = require('mysql2/promise');
|
||||
const crypto = require('crypto');
|
||||
require('dotenv').config();
|
||||
|
||||
const app = express();
|
||||
const server = http.createServer(app);
|
||||
const wss = new WebSocket.Server({ server });
|
||||
|
||||
// Middleware
|
||||
app.use(express.json());
|
||||
app.use(express.static('public'));
|
||||
|
||||
// UUID generator
|
||||
function generateUUID() {
|
||||
return crypto.randomUUID();
|
||||
}
|
||||
|
||||
// Database pool
|
||||
const pool = mysql.createPool({
|
||||
host: process.env.DB_HOST,
|
||||
port: process.env.DB_PORT || 3306,
|
||||
user: process.env.DB_USER,
|
||||
password: process.env.DB_PASSWORD,
|
||||
database: process.env.DB_NAME,
|
||||
waitForConnections: true,
|
||||
connectionLimit: 10,
|
||||
queueLimit: 0
|
||||
});
|
||||
|
||||
// Initialize database tables
|
||||
async function initDatabase() {
|
||||
const connection = await pool.getConnection();
|
||||
try {
|
||||
await connection.query(`
|
||||
CREATE TABLE IF NOT EXISTS users (
|
||||
id VARCHAR(36) PRIMARY KEY,
|
||||
username VARCHAR(255) UNIQUE NOT NULL,
|
||||
display_name VARCHAR(255),
|
||||
email VARCHAR(255),
|
||||
groups TEXT,
|
||||
last_login TIMESTAMP,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
)
|
||||
`);
|
||||
|
||||
await connection.query(`
|
||||
CREATE TABLE IF NOT EXISTS workers (
|
||||
id VARCHAR(36) PRIMARY KEY,
|
||||
name VARCHAR(255) UNIQUE NOT NULL,
|
||||
status VARCHAR(50) NOT NULL,
|
||||
last_heartbeat TIMESTAMP NULL,
|
||||
api_key VARCHAR(255),
|
||||
metadata JSON,
|
||||
INDEX idx_status (status),
|
||||
INDEX idx_heartbeat (last_heartbeat)
|
||||
)
|
||||
`);
|
||||
|
||||
await connection.query(`
|
||||
CREATE TABLE IF NOT EXISTS workflows (
|
||||
id VARCHAR(36) PRIMARY KEY,
|
||||
name VARCHAR(255) NOT NULL,
|
||||
description TEXT,
|
||||
definition JSON NOT NULL,
|
||||
created_by VARCHAR(255),
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
INDEX idx_name (name)
|
||||
)
|
||||
`);
|
||||
|
||||
await connection.query(`
|
||||
CREATE TABLE IF NOT EXISTS executions (
|
||||
id VARCHAR(36) PRIMARY KEY,
|
||||
workflow_id VARCHAR(36) NOT NULL,
|
||||
status VARCHAR(50) NOT NULL,
|
||||
started_by VARCHAR(255),
|
||||
started_at TIMESTAMP NULL,
|
||||
completed_at TIMESTAMP NULL,
|
||||
logs JSON,
|
||||
FOREIGN KEY (workflow_id) REFERENCES workflows(id) ON DELETE CASCADE,
|
||||
INDEX idx_workflow (workflow_id),
|
||||
INDEX idx_status (status),
|
||||
INDEX idx_started (started_at)
|
||||
)
|
||||
`);
|
||||
|
||||
console.log('Database tables initialized successfully');
|
||||
} catch (error) {
|
||||
console.error('Database initialization error:', error);
|
||||
throw error;
|
||||
} finally {
|
||||
connection.release();
|
||||
}
|
||||
}
|
||||
|
||||
// WebSocket connections
|
||||
const clients = new Set();
|
||||
wss.on('connection', (ws) => {
|
||||
clients.add(ws);
|
||||
ws.on('close', () => clients.delete(ws));
|
||||
});
|
||||
|
||||
// Broadcast to all connected clients
|
||||
function broadcast(data) {
|
||||
clients.forEach(client => {
|
||||
if (client.readyState === WebSocket.OPEN) {
|
||||
client.send(JSON.stringify(data));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Authelia SSO Middleware
|
||||
async function authenticateSSO(req, res, next) {
|
||||
// Check for Authelia headers
|
||||
const remoteUser = req.headers['remote-user'];
|
||||
const remoteName = req.headers['remote-name'];
|
||||
const remoteEmail = req.headers['remote-email'];
|
||||
const remoteGroups = req.headers['remote-groups'];
|
||||
|
||||
if (!remoteUser) {
|
||||
return res.status(401).json({
|
||||
error: 'Not authenticated',
|
||||
message: 'Please access this service through auth.lotusguild.org'
|
||||
});
|
||||
}
|
||||
|
||||
// Check if user is in allowed groups (admin or employee)
|
||||
const groups = remoteGroups ? remoteGroups.split(',').map(g => g.trim()) : [];
|
||||
const allowedGroups = ['admin', 'employee'];
|
||||
const hasAccess = groups.some(g => allowedGroups.includes(g));
|
||||
|
||||
if (!hasAccess) {
|
||||
return res.status(403).json({
|
||||
error: 'Access denied',
|
||||
message: 'You must be in admin or employee group'
|
||||
});
|
||||
}
|
||||
|
||||
// Store/update user in database
|
||||
try {
|
||||
const userId = generateUUID();
|
||||
await pool.query(
|
||||
`INSERT INTO users (id, username, display_name, email, groups, last_login)
|
||||
VALUES (?, ?, ?, ?, ?, NOW())
|
||||
ON DUPLICATE KEY UPDATE
|
||||
display_name=VALUES(display_name),
|
||||
email=VALUES(email),
|
||||
groups=VALUES(groups),
|
||||
last_login=NOW()`,
|
||||
[userId, remoteUser, remoteName, remoteEmail, remoteGroups]
|
||||
);
|
||||
} catch (error) {
|
||||
console.error('Error updating user:', error);
|
||||
}
|
||||
|
||||
// Attach user info to request
|
||||
req.user = {
|
||||
username: remoteUser,
|
||||
name: remoteName || remoteUser,
|
||||
email: remoteEmail || '',
|
||||
groups: groups,
|
||||
isAdmin: groups.includes('admin')
|
||||
};
|
||||
|
||||
next();
|
||||
}
|
||||
|
||||
// Routes - All protected by SSO
|
||||
app.get('/api/user', authenticateSSO, (req, res) => {
|
||||
res.json(req.user);
|
||||
});
|
||||
|
||||
app.get('/api/workflows', authenticateSSO, async (req, res) => {
|
||||
try {
|
||||
const [rows] = await pool.query('SELECT * FROM workflows ORDER BY created_at DESC');
|
||||
res.json(rows);
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
app.post('/api/workflows', authenticateSSO, async (req, res) => {
|
||||
try {
|
||||
const { name, description, definition } = req.body;
|
||||
const id = generateUUID();
|
||||
|
||||
await pool.query(
|
||||
'INSERT INTO workflows (id, name, description, definition, created_by) VALUES (?, ?, ?, ?, ?)',
|
||||
[id, name, description, JSON.stringify(definition), req.user.username]
|
||||
);
|
||||
|
||||
res.json({ id, name, description, definition });
|
||||
broadcast({ type: 'workflow_created', workflow_id: id });
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
app.delete('/api/workflows/:id', authenticateSSO, async (req, res) => {
|
||||
try {
|
||||
// Only admins can delete workflows
|
||||
if (!req.user.isAdmin) {
|
||||
return res.status(403).json({ error: 'Admin access required' });
|
||||
}
|
||||
|
||||
await pool.query('DELETE FROM workflows WHERE id = ?', [req.params.id]);
|
||||
res.json({ success: true });
|
||||
broadcast({ type: 'workflow_deleted', workflow_id: req.params.id });
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
app.get('/api/workers', authenticateSSO, async (req, res) => {
|
||||
try {
|
||||
const [rows] = await pool.query('SELECT * FROM workers ORDER BY name');
|
||||
res.json(rows);
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
app.post('/api/workers/heartbeat', async (req, res) => {
|
||||
try {
|
||||
const { worker_id, name, metadata } = req.body;
|
||||
const apiKey = req.headers['x-api-key'];
|
||||
|
||||
// Verify API key
|
||||
if (apiKey !== process.env.WORKER_API_KEY) {
|
||||
return res.status(401).json({ error: 'Invalid API key' });
|
||||
}
|
||||
|
||||
await pool.query(
|
||||
`INSERT INTO workers (id, name, status, last_heartbeat, api_key, metadata)
|
||||
VALUES (?, ?, 'online', NOW(), ?, ?)
|
||||
ON DUPLICATE KEY UPDATE
|
||||
status='online',
|
||||
last_heartbeat=NOW(),
|
||||
metadata=VALUES(metadata)`,
|
||||
[worker_id, name, apiKey, JSON.stringify(metadata)]
|
||||
);
|
||||
|
||||
broadcast({ type: 'worker_update', worker_id, status: 'online' });
|
||||
res.json({ success: true });
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
app.post('/api/executions', authenticateSSO, async (req, res) => {
|
||||
try {
|
||||
const { workflow_id } = req.body;
|
||||
const id = generateUUID();
|
||||
|
||||
await pool.query(
|
||||
'INSERT INTO executions (id, workflow_id, status, started_by, started_at, logs) VALUES (?, ?, ?, ?, NOW(), ?)',
|
||||
[id, workflow_id, 'running', req.user.username, JSON.stringify([])]
|
||||
);
|
||||
|
||||
broadcast({ type: 'execution_started', execution_id: id, workflow_id });
|
||||
res.json({ id, workflow_id, status: 'running' });
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
app.get('/api/executions', authenticateSSO, async (req, res) => {
|
||||
try {
|
||||
const [rows] = await pool.query(
|
||||
'SELECT e.*, w.name as workflow_name FROM executions e LEFT JOIN workflows w ON e.workflow_id = w.id ORDER BY e.started_at DESC LIMIT 50'
|
||||
);
|
||||
res.json(rows);
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
app.get('/api/executions/:id', authenticateSSO, async (req, res) => {
|
||||
try {
|
||||
const [rows] = await pool.query('SELECT * FROM executions WHERE id = ?', [req.params.id]);
|
||||
if (rows.length === 0) {
|
||||
return res.status(404).json({ error: 'Not found' });
|
||||
}
|
||||
res.json(rows[0]);
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
// Health check (no auth required)
|
||||
app.get('/health', async (req, res) => {
|
||||
try {
|
||||
await pool.query('SELECT 1');
|
||||
res.json({
|
||||
status: 'ok',
|
||||
timestamp: new Date().toISOString(),
|
||||
database: 'connected',
|
||||
auth: 'authelia-sso'
|
||||
});
|
||||
} catch (error) {
|
||||
res.status(500).json({
|
||||
status: 'error',
|
||||
timestamp: new Date().toISOString(),
|
||||
database: 'disconnected',
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Start server
|
||||
const PORT = process.env.PORT || 8080;
|
||||
const HOST = process.env.HOST || '0.0.0.0';
|
||||
|
||||
initDatabase().then(() => {
|
||||
server.listen(PORT, HOST, () => {
|
||||
console.log(`PULSE Server running on http://${HOST}:${PORT}`);
|
||||
console.log(`Connected to MariaDB at ${process.env.DB_HOST}`);
|
||||
console.log(`Authentication: Authelia SSO`);
|
||||
console.log(`Worker API Key configured: ${process.env.WORKER_API_KEY ? 'Yes' : 'No'}`);
|
||||
});
|
||||
}).catch(err => {
|
||||
console.error('Failed to start server:', err);
|
||||
process.exit(1);
|
||||
});
|
||||
Reference in New Issue
Block a user