mirror of
https://github.com/DarkflameUniverse/DarkflameServer.git
synced 2026-05-13 19:05:04 +00:00
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:
144
dDashboardServer/static/js/api.js
Normal file
144
dDashboardServer/static/js/api.js
Normal 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';
|
||||
}
|
||||
}
|
||||
188
dDashboardServer/static/js/dashboard.js
Normal file
188
dDashboardServer/static/js/dashboard.js
Normal 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);
|
||||
});
|
||||
});
|
||||
46
dDashboardServer/static/js/login.js
Normal file
46
dDashboardServer/static/js/login.js
Normal 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();
|
||||
}
|
||||
43
dDashboardServer/static/js/register.js
Normal file
43
dDashboardServer/static/js/register.js
Normal 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';
|
||||
}
|
||||
});
|
||||
});
|
||||
75
dDashboardServer/static/js/wait-for-jq-dt.js
Normal file
75
dDashboardServer/static/js/wait-for-jq-dt.js
Normal 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);
|
||||
Reference in New Issue
Block a user