Add dashboard audit log and configuration management

- Implemented dashboard audit logging with InsertAuditLog, GetRecentAuditLogs, GetAuditLogsByIP, and CleanupOldAuditLogs methods.
- Created dashboard configuration management with GetDashboardConfig and SetDashboardConfig methods.
- Added new tables for dashboard_audit_log and dashboard_config in both MySQL and SQLite migrations.
- Updated CMakeLists to include Crow and ASIO for dashboard server functionality.
- Enhanced existing database classes to support new dashboard features, including character, play key, and property management.
- Added new methods for retrieving and managing play keys, properties, and pet names.
- Updated TestSQLDatabase to include stubs for new dashboard-related methods.
- Modified shared and dashboard configuration files for new settings.
This commit is contained in:
Aaron Kimbrell
2026-04-22 11:01:41 -05:00
parent d532a9b063
commit e3467465b4
92 changed files with 9133 additions and 77 deletions

View File

@@ -0,0 +1,144 @@
/**
* API Client for DarkflameServer Dashboard
* Provides a simple interface for making API calls with error handling
*/
const API = {
/**
* Base URL for API endpoints
*/
baseURL: '',
/**
* Make a GET request
* @param {string} endpoint - The API endpoint
* @param {object} params - Query parameters
* @returns {Promise<any>} Response data
*/
async get(endpoint, params = {}) {
const url = new URL(this.baseURL + endpoint, window.location.origin);
Object.keys(params).forEach(key => url.searchParams.append(key, params[key]));
const response = await fetch(url, {
method: 'GET',
credentials: 'same-origin',
headers: {
'Accept': 'application/json'
}
});
return this.handleResponse(response);
},
/**
* Make a POST request
* @param {string} endpoint - The API endpoint
* @param {object} data - Request body data
* @returns {Promise<any>} Response data
*/
async post(endpoint, data = {}) {
const response = await fetch(this.baseURL + endpoint, {
method: 'POST',
credentials: 'same-origin',
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json'
},
body: JSON.stringify(data)
});
return this.handleResponse(response);
},
/**
* Make a PUT request
* @param {string} endpoint - The API endpoint
* @param {object} data - Request body data
* @returns {Promise<any>} Response data
*/
async put(endpoint, data = {}) {
const response = await fetch(this.baseURL + endpoint, {
method: 'PUT',
credentials: 'same-origin',
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json'
},
body: JSON.stringify(data)
});
return this.handleResponse(response);
},
/**
* Make a DELETE request
* @param {string} endpoint - The API endpoint
* @returns {Promise<any>} Response data
*/
async delete(endpoint) {
const response = await fetch(this.baseURL + endpoint, {
method: 'DELETE',
credentials: 'same-origin',
headers: {
'Accept': 'application/json'
}
});
return this.handleResponse(response);
},
/**
* Handle fetch response
* @param {Response} response - Fetch response object
* @returns {Promise<any>} Parsed response data
*/
async handleResponse(response) {
const contentType = response.headers.get('content-type');
// Try to parse as JSON first (even if content-type is missing)
try {
const text = await response.text();
// Try to parse as JSON
if (text) {
try {
const data = JSON.parse(text);
if (!response.ok) {
throw new Error(data.error || `HTTP error! status: ${response.status}`);
}
return data;
} catch (jsonError) {
// Not JSON, return as text
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return text;
}
}
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return text;
} catch (error) {
throw error;
}
}
};
/**
* Logout function
*/
async function logout() {
try {
await API.post('/api/logout');
window.location.href = '/login';
} catch (error) {
console.error('Logout error:', error);
// Force redirect even on error
window.location.href = '/login';
}
}

View File

@@ -0,0 +1,188 @@
/**
* Main Dashboard JavaScript
* Common utilities and functions for all pages
*/
/**
* Show an alert message
* @param {string} type - Alert type (success, danger, warning, info)
* @param {string} message - Alert message
* @param {number} duration - Auto-dismiss duration in ms (0 = no auto-dismiss)
*/
function showAlert(type, message, duration = 5000) {
const alertsContainer = document.getElementById('alerts-container') || createAlertsContainer();
const alertId = 'alert-' + Date.now();
const alertHTML = `
<div id="${alertId}" class="alert alert-${type} alert-dismissible fade show" role="alert">
${message}
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>
`;
alertsContainer.insertAdjacentHTML('beforeend', alertHTML);
if (duration > 0) {
setTimeout(() => {
const alert = document.getElementById(alertId);
if (alert) {
const bsAlert = new bootstrap.Alert(alert);
bsAlert.close();
}
}, duration);
}
}
/**
* Create alerts container if it doesn't exist
*/
function createAlertsContainer() {
const main = document.querySelector('main');
const container = document.createElement('div');
container.id = 'alerts-container';
container.className = 'alerts-container';
main.insertBefore(container, main.firstChild);
return container;
}
/**
* Format timestamp to localized date/time
* @param {number} timestamp - Unix timestamp
* @returns {string} Formatted date/time
*/
function formatTimestamp(timestamp) {
if (!timestamp || timestamp === 0) return '-';
const date = new Date(timestamp * 1000);
return date.toLocaleString();
}
/**
* Format GM level to human-readable name
* @param {number} level - GM level number
* @returns {string} GM level name
*/
function formatGMLevel(level) {
const levels = {
0: 'Civilian',
1: 'Forum Moderator',
2: 'Junior Moderator',
3: 'Moderator',
4: 'Senior Moderator',
5: 'Lead Moderator',
6: 'Junior Developer',
7: 'Inactive Developer',
8: 'Developer',
9: 'Operator'
};
return levels[level] || 'Unknown';
}
/**
* Confirm action with modal
* @param {string} title - Modal title
* @param {string} message - Modal message
* @param {function} callback - Callback function if confirmed
*/
function confirmAction(title, message, callback) {
if (confirm(message)) {
callback();
}
}
/**
* Copy text to clipboard
* @param {string} text - Text to copy
*/
async function copyToClipboard(text) {
try {
await navigator.clipboard.writeText(text);
showAlert('success', 'Copied to clipboard!', 2000);
} catch (err) {
showAlert('danger', 'Failed to copy to clipboard');
}
}
/**
* Debounce function calls
* @param {function} func - Function to debounce
* @param {number} wait - Wait time in ms
* @returns {function} Debounced function
*/
function debounce(func, wait) {
let timeout;
return function executedFunction(...args) {
const later = () => {
clearTimeout(timeout);
func(...args);
};
clearTimeout(timeout);
timeout = setTimeout(later, wait);
};
}
/**
* Initialize DataTables default settings
*/
$.extend(true, $.fn.dataTable.defaults, {
responsive: true,
lengthMenu: [[10, 25, 50, 100, -1], [10, 25, 50, 100, "All"]],
pageLength: 25,
language: {
search: "_INPUT_",
searchPlaceholder: "Search...",
lengthMenu: "Show _MENU_ entries",
info: "Showing _START_ to _END_ of _TOTAL_ entries",
infoEmpty: "No entries found",
infoFiltered: "(filtered from _MAX_ total entries)",
zeroRecords: "No matching records found",
emptyTable: "No data available in table"
}
});
/**
* Handle form submission with API
* @param {string} formId - Form element ID
* @param {string} endpoint - API endpoint
* @param {function} onSuccess - Success callback
*/
function handleFormSubmit(formId, endpoint, onSuccess) {
const form = document.getElementById(formId);
if (!form) return;
form.addEventListener('submit', async (e) => {
e.preventDefault();
const formData = new FormData(form);
const data = Object.fromEntries(formData);
try {
const result = await API.post(endpoint, data);
if (result.success) {
showAlert('success', result.message || 'Operation successful');
if (onSuccess) onSuccess(result);
} else {
showAlert('danger', result.error || 'Operation failed');
}
} catch (error) {
showAlert('danger', error.message);
}
});
}
/**
* Initialize tooltips
*/
document.addEventListener('DOMContentLoaded', function() {
// Initialize Bootstrap tooltips
const tooltipTriggerList = [].slice.call(document.querySelectorAll('[data-bs-toggle="tooltip"]'));
tooltipTriggerList.map(function(tooltipTriggerEl) {
return new bootstrap.Tooltip(tooltipTriggerEl);
});
// Initialize Bootstrap popovers
const popoverTriggerList = [].slice.call(document.querySelectorAll('[data-bs-toggle="popover"]'));
popoverTriggerList.map(function(popoverTriggerEl) {
return new bootstrap.Popover(popoverTriggerEl);
});
});

View File

@@ -0,0 +1,46 @@
/**
* Login page functionality
*/
// Function to initialize login form
function initLoginForm() {
const form = document.getElementById('login-form');
if (!form) return; // Not on login page
form.addEventListener('submit', async (e) => {
e.preventDefault();
const username = document.getElementById('username').value;
const password = document.getElementById('password').value;
const messageDiv = document.getElementById('login-message');
try {
const response = await API.post('/api/login', { username, password });
if (response && response.success) {
messageDiv.className = 'alert alert-success';
messageDiv.textContent = 'Login successful! Redirecting...';
messageDiv.style.display = 'block';
setTimeout(() => {
window.location.href = '/';
}, 1000);
} else {
messageDiv.className = 'alert alert-danger';
messageDiv.textContent = response.error || 'Login failed';
messageDiv.style.display = 'block';
}
} catch (error) {
messageDiv.className = 'alert alert-danger';
messageDiv.textContent = error.message || 'An error occurred during login';
messageDiv.style.display = 'block';
}
});
}
// Initialize when DOM is ready
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', initLoginForm);
} else {
initLoginForm();
}

View File

@@ -0,0 +1,43 @@
document.addEventListener('DOMContentLoaded', () => {
const form = document.getElementById('register-form');
const alertBox = document.getElementById('register-alert');
form.addEventListener('submit', async (e) => {
e.preventDefault();
alertBox.style.display = 'none';
const username = document.getElementById('username').value.trim();
const password = document.getElementById('password').value;
const play_key = document.getElementById('play_key').value.trim();
try {
const res = await fetch('/api/register', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ username, password, play_key })
});
const data = await res.json();
if (!res.ok) {
alertBox.className = 'alert alert-danger';
alertBox.textContent = data.error || 'Registration failed';
alertBox.style.display = 'block';
return;
}
if (data.success) {
alertBox.className = 'alert alert-success';
alertBox.textContent = 'Account created successfully. You can now log in.';
alertBox.style.display = 'block';
form.reset();
} else {
alertBox.className = 'alert alert-danger';
alertBox.textContent = data.error || 'Registration failed';
alertBox.style.display = 'block';
}
} catch (err) {
alertBox.className = 'alert alert-danger';
alertBox.textContent = err.message || 'Registration failed';
alertBox.style.display = 'block';
}
});
});

View File

@@ -0,0 +1,75 @@
// Helper to wait for jQuery and DataTables (and optionally API) to be available
// Usage:
// safeInit(callback, { timeout: 5000, interval: 100, requireApi: false })
// The callback receives `window.jQuery` as its first argument.
(function(window) {
'use strict';
function waitFor(conditionFn, timeoutMs, intervalMs) {
return new Promise((resolve, reject) => {
const start = Date.now();
const iv = setInterval(() => {
try {
if (conditionFn()) {
clearInterval(iv);
resolve();
return;
}
} catch (e) {
// ignore
}
if (Date.now() - start > timeoutMs) {
clearInterval(iv);
reject(new Error('waitFor: timed out'));
}
}, intervalMs);
});
}
async function safeInit(cb, opts) {
opts = opts || {};
const timeout = typeof opts.timeout === 'number' ? opts.timeout : 5000;
const interval = typeof opts.interval === 'number' ? opts.interval : 100;
const requireApi = !!opts.requireApi;
// Wait for DOM ready first so scripts included at end of body have run
if (document.readyState === 'loading') {
await new Promise(r => document.addEventListener('DOMContentLoaded', r, { once: true }));
}
try {
await waitFor(() => window.jQuery && window.jQuery.fn && window.jQuery.fn.DataTable, timeout, interval);
if (requireApi) {
await waitFor(() => window.API, timeout, interval);
}
// call callback with jQuery
try { cb(window.jQuery); } catch (e) { console.error('safeInit callback error', e); }
} catch (err) {
console.error('safeInit: required libraries failed to load', err);
// If callback provided an onError handler, call it
if (opts.onError && typeof opts.onError === 'function') {
try { opts.onError(err); } catch (e) { console.error(e); }
} else {
// default fallback: show a banner if possible
const tableEls = document.querySelectorAll('table');
if (tableEls && tableEls.length) {
tableEls.forEach(el => {
const wrapper = document.createElement('div');
wrapper.className = 'alert alert-danger';
wrapper.textContent = 'Required JavaScript libraries failed to load (jQuery/DataTables). Please check your network or CDN allowlist.';
el.replaceWith(wrapper);
});
} else {
console.warn('safeInit: libraries missing');
}
}
}
}
// Expose globally
window.safeInit = safeInit;
window.waitForLibraries = function(timeoutMs, intervalMs) {
return waitFor(() => window.jQuery && window.jQuery.fn && window.jQuery.fn.DataTable, timeoutMs || 5000, intervalMs || 100);
};
})(window);