Skip to content

Commit 29bcf03

Browse files
committed
Introduced basic-auth
1 parent ebeb3c7 commit 29bcf03

13 files changed

Lines changed: 764 additions & 96 deletions

File tree

README.md

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,11 +21,17 @@ docker secret create msa.properties msa.properties
2121
docker stack deploy -c compose.yml msa
2222
```
2323

24-
The `msa.secrets` file contains environment variables for the admin UI to connect to the MQTT broker:
24+
The `msa.secrets` file contains environment variables for the admin UI and the MQTT broker:
2525
```
26+
MSA_USER=admin:changeme
2627
MSA_MQTT_USER=admin:admin
2728
```
2829

30+
| Variable | Description |
31+
|----------|-------------|
32+
| `MSA_USER` | MSA admin credentials in `username:password` format (for `/db-admin/` HTTP Basic Auth) |
33+
| `MSA_MQTT_USER` | MQTT broker credentials in `username:password` format (for admin UI to connect to broker) |
34+
2935
The `msa.properties` file contains application configuration:
3036
```properties
3137
version=0.2.0
@@ -70,7 +76,7 @@ The plugin automatically creates the following indexes for optimal query perform
7076
### Example Configuration
7177

7278
```properties
73-
plugin /usr/lib/sql_plugin.so
79+
plugin /usr/lib/libsql_plugin.so
7480
# Exclude command topics from persistence
7581
plugin_opt_exclude_topics cmd/#,+/test/exclude/#
7682
# Batch insert settings
@@ -114,6 +120,19 @@ plugin_opt_batch_size 100
114120
plugin_opt_flush_interval 50
115121
```
116122

123+
## Admin UI Keyboard Shortcuts
124+
125+
The web admin interface supports the following keyboard shortcuts for improved productivity:
126+
127+
| Shortcut | Action |
128+
|----------|--------|
129+
| `Ctrl+Enter` | Execute custom query (Database tab) or refresh messages (Broker tab) |
130+
| `Ctrl+1` | Switch to Database tab |
131+
| `Ctrl+2` | Switch to Broker tab |
132+
| `Ctrl+3` | Switch to ACL tab |
133+
| `Ctrl+Shift+R` | Toggle auto-refresh on/off |
134+
| `Escape` | Close modal dialogs |
135+
117136
## Acknowledgements
118137

119138
This project uses the following open source libraries:

admin/app.js

Lines changed: 268 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -192,18 +192,192 @@ function getCookie(name) {
192192
// Database Tab Functions
193193
// =============================================================================
194194

195-
async function executeSQL(sql) {
195+
// MSA authentication credentials (stored in memory for session)
196+
let msaCredentials = null;
197+
let loginModalOpen = false;
198+
199+
function getDbAuthHeader() {
200+
if (msaCredentials) {
201+
return 'Basic ' + btoa(msaCredentials.username + ':' + msaCredentials.password);
202+
}
203+
return null;
204+
}
205+
206+
function showLoginModal() {
207+
// Don't reopen or refocus if already open
208+
if (loginModalOpen) {
209+
return;
210+
}
211+
212+
loginModalOpen = true;
213+
const modal = document.getElementById('loginModal');
214+
const errorDiv = document.getElementById('loginError');
215+
errorDiv.textContent = '';
216+
errorDiv.style.display = 'none';
217+
modal.classList.add('active');
218+
document.getElementById('loginUsername').focus();
219+
}
220+
221+
function closeLoginModal() {
222+
loginModalOpen = false;
223+
const modal = document.getElementById('loginModal');
224+
modal.classList.remove('active');
225+
document.getElementById('loginForm').reset();
226+
}
227+
228+
async function handleLogin(event) {
229+
event.preventDefault();
230+
231+
const username = document.getElementById('loginUsername').value;
232+
const password = document.getElementById('loginPassword').value;
233+
const errorDiv = document.getElementById('loginError');
234+
235+
// Test credentials with a simple query
196236
try {
237+
const authHeader = 'Basic ' + btoa(username + ':' + password);
197238
const response = await fetch(`${API_BASE}/v1/execute`, {
198239
method: 'POST',
199240
headers: {
200241
'Content-Type': 'application/json',
242+
'X-Requested-With': 'XMLHttpRequest',
243+
'Authorization': authHeader
201244
},
245+
body: JSON.stringify({
246+
stmt: ['SELECT 1']
247+
})
248+
});
249+
250+
if (response.status === 401) {
251+
errorDiv.textContent = 'Invalid username or password';
252+
errorDiv.style.display = 'block';
253+
return;
254+
}
255+
256+
if (!response.ok) {
257+
errorDiv.textContent = 'Connection error: ' + response.status;
258+
errorDiv.style.display = 'block';
259+
return;
260+
}
261+
262+
// Credentials are valid - store them
263+
msaCredentials = { username, password };
264+
closeLoginModal();
265+
updateAuthMenuItem();
266+
267+
// Refresh data with new credentials
268+
dbConnState();
269+
loadMessages();
270+
271+
// Connect MQTT if on Broker tab
272+
const activeTab = document.querySelector('.tab-content.active');
273+
if (activeTab && activeTab.id === 'broker-tab' && !window.mqttConnected) {
274+
initMqttConnection();
275+
window.mqttConnected = true;
276+
}
277+
// Refresh broker display if on that tab
278+
if (activeTab && activeTab.id === 'broker-tab') {
279+
displayMqttMessages();
280+
}
281+
282+
} catch (error) {
283+
errorDiv.textContent = 'Connection failed: ' + error.message;
284+
errorDiv.style.display = 'block';
285+
}
286+
}
287+
288+
// Update the Login/Logout menu item and button based on auth state
289+
function updateAuthMenuItem() {
290+
const menuItem = document.getElementById('authMenuItem');
291+
const authButton = document.getElementById('authButton');
292+
const label = msaCredentials ? 'Logout' : 'Login';
293+
294+
if (menuItem) {
295+
menuItem.textContent = label;
296+
}
297+
if (authButton) {
298+
authButton.textContent = label;
299+
authButton.title = label;
300+
}
301+
}
302+
303+
// Handle Login/Logout menu click
304+
function handleAuthMenuClick() {
305+
toggleSettingsMenu();
306+
307+
if (msaCredentials) {
308+
performLogout();
309+
} else {
310+
showLoginModal();
311+
}
312+
}
313+
314+
// Handle Login/Logout button click (same as menu but no menu toggle)
315+
function handleAuthButtonClick() {
316+
if (msaCredentials) {
317+
performLogout();
318+
} else {
319+
showLoginModal();
320+
}
321+
}
322+
323+
// Perform logout - clear credentials and data
324+
function performLogout() {
325+
msaCredentials = null;
326+
loginModalOpen = false;
327+
updateAuthMenuItem();
328+
329+
// Clear database tab data
330+
//document.getElementById('results').innerHTML = '<div class="no-results">Please log in to view data</div>';
331+
document.getElementById('results').innerHTML = '<div class="no-results"></div>';
332+
document.getElementById('dbStatusIcon').textContent = '⚫';
333+
334+
// Clear broker tab data and disconnect MQTT
335+
mqttMessagesMap.clear();
336+
if (mqttClient && mqttClient.connected) {
337+
mqttClient.end();
338+
}
339+
window.mqttConnected = false;
340+
updateMqttStatus('Disconnected', '⚫', 'var(--ctp-overlay0)');
341+
342+
const brokerTbody = document.querySelector('#mqtt-messages-table tbody');
343+
if (brokerTbody) {
344+
//brokerTbody.innerHTML = '<tr><td colspan="6" class="login-required">Please log in to view data</td></tr>';
345+
brokerTbody.innerHTML = '<tr><td colspan="6" class="login-required"></td></tr>';
346+
}
347+
348+
// Stop auto-refresh if running
349+
if (isAutoRefreshEnabled) {
350+
toggleAutoRefresh(true);
351+
}
352+
}
353+
354+
async function executeSQL(sql) {
355+
try {
356+
const headers = {
357+
'Content-Type': 'application/json',
358+
'X-Requested-With': 'XMLHttpRequest', // Identify as AJAX to prevent browser auth dialog
359+
};
360+
361+
// Add auth header if we have credentials
362+
const authHeader = getDbAuthHeader();
363+
if (authHeader) {
364+
headers['Authorization'] = authHeader;
365+
}
366+
367+
const response = await fetch(`${API_BASE}/v1/execute`, {
368+
method: 'POST',
369+
headers: headers,
202370
body: JSON.stringify({
203371
stmt: [sql]
204372
})
205373
});
206374

375+
// If unauthorized, show login modal
376+
if (response.status === 401) {
377+
showLoginModal();
378+
throw new Error('Authentication required');
379+
}
380+
207381
if (!response.ok) {
208382
throw new Error(`HTTP error! status: ${response.status}`);
209383
}
@@ -296,6 +470,12 @@ async function loadMessages() {
296470
}
297471

298472
async function executeCustomQuery() {
473+
// Check if user is logged in
474+
if (!msaCredentials) {
475+
showLoginModal();
476+
return;
477+
}
478+
299479
let query = document.getElementById('customQuery').value.trim();
300480
if (!query) {
301481
showMessage('Please enter a SQL query', 'error');
@@ -525,8 +705,8 @@ function switchTab(tabName) {
525705
loadBrokerConfig();
526706
}
527707

528-
// Auto-connect MQTT if switching to Broker tab
529-
if (tabName === 'broker' && !window.mqttConnected) {
708+
// Auto-connect MQTT if switching to Broker tab (only if logged in)
709+
if (tabName === 'broker' && !window.mqttConnected && msaCredentials) {
530710
setTimeout(() => {
531711
initMqttConnection();
532712
window.mqttConnected = true;
@@ -561,7 +741,21 @@ function restoreActiveTab() {
561741

562742
async function loadBrokerConfig() {
563743
try {
564-
const response = await fetch('/broker-config');
744+
const headers = {
745+
'X-Requested-With': 'XMLHttpRequest'
746+
};
747+
const authHeader = getDbAuthHeader();
748+
if (authHeader) {
749+
headers['Authorization'] = authHeader;
750+
}
751+
752+
const response = await fetch('/broker-config', { headers });
753+
754+
if (response.status === 401) {
755+
showLoginModal();
756+
return;
757+
}
758+
565759
if (!response.ok) {
566760
throw new Error(`HTTP error! status: ${response.status}`);
567761
}
@@ -661,7 +855,15 @@ async function initMqttConnection() {
661855
let password = 'admin';
662856

663857
try {
664-
const credResponse = await fetch('/mqtt-credentials');
858+
const credHeaders = {
859+
'X-Requested-With': 'XMLHttpRequest'
860+
};
861+
const authHeader = getDbAuthHeader();
862+
if (authHeader) {
863+
credHeaders['Authorization'] = authHeader;
864+
}
865+
866+
const credResponse = await fetch('/mqtt-credentials', { headers: credHeaders });
665867
if (credResponse.ok) {
666868
const credentials = await credResponse.json();
667869
username = credentials.username;
@@ -795,6 +997,12 @@ function updateMqttStatus(text, icon, color) {
795997

796998
// Publish a message to the MQTT broker
797999
function publishMessage() {
1000+
// Check if user is logged in
1001+
if (!msaCredentials) {
1002+
showLoginModal();
1003+
return;
1004+
}
1005+
7981006
if (!mqttClient || !mqttClient.connected) {
7991007
console.error('MQTT client not connected, cannot publish message');
8001008
alert('MQTT client not connected. Please wait for connection.');
@@ -884,6 +1092,13 @@ function displayMqttMessages() {
8841092
return;
8851093
}
8861094

1095+
// Show login required message if not authenticated
1096+
if (!msaCredentials) {
1097+
//tbody.innerHTML = '<tr><td colspan="6" class="login-required">Please log in to view data</td></tr>';
1098+
tbody.innerHTML = '<tr><td colspan="6" class="login-required"></td></tr>';
1099+
return;
1100+
}
1101+
8871102
console.log('displayMqttMessages called, total topics:', mqttMessagesMap.size);
8881103

8891104
// Get filter values
@@ -1323,4 +1538,52 @@ function setupEventListeners() {
13231538
displayMqttMessages();
13241539
});
13251540
}
1541+
1542+
// Global keyboard shortcuts
1543+
document.addEventListener('keydown', (e) => {
1544+
// Escape - close modals and clear filters
1545+
if (e.key === 'Escape') {
1546+
closeAboutModal();
1547+
closeConfirmModal();
1548+
return;
1549+
}
1550+
1551+
// Ctrl+Enter - Execute query (Database) or Refresh (Broker)
1552+
if (e.ctrlKey && e.key === 'Enter') {
1553+
e.preventDefault();
1554+
if (document.getElementById('database-tab').classList.contains('active')) {
1555+
const customQuery = document.getElementById('customQuery').value.trim();
1556+
if (customQuery) {
1557+
executeCustomQuery();
1558+
} else {
1559+
loadMessages();
1560+
}
1561+
} else if (document.getElementById('broker-tab').classList.contains('active')) {
1562+
displayMqttMessages();
1563+
}
1564+
return;
1565+
}
1566+
1567+
// Ctrl+1/2/3 - Switch tabs
1568+
if (e.ctrlKey && !e.shiftKey && ['1', '2', '3'].includes(e.key)) {
1569+
e.preventDefault();
1570+
const tabs = document.querySelectorAll('.tab');
1571+
const tabIndex = parseInt(e.key) - 1;
1572+
if (tabs[tabIndex]) {
1573+
tabs[tabIndex].click();
1574+
}
1575+
return;
1576+
}
1577+
1578+
// Ctrl+Shift+R - Toggle auto-refresh
1579+
if (e.ctrlKey && e.shiftKey && e.key === 'R') {
1580+
e.preventDefault();
1581+
const checkbox = document.getElementById('autoRefreshCheckbox');
1582+
if (checkbox) {
1583+
checkbox.checked = !checkbox.checked;
1584+
toggleAutoRefresh();
1585+
}
1586+
return;
1587+
}
1588+
});
13261589
}

0 commit comments

Comments
 (0)