mirror of
https://github.com/DarkflameUniverse/DarkflameServer.git
synced 2026-01-31 16:09:55 +00:00
WIP: basic server, no features
This commit is contained in:
177
dDashboardServer/static/css/dashboard.css
Normal file
177
dDashboardServer/static/css/dashboard.css
Normal file
@@ -0,0 +1,177 @@
|
||||
/* Minimal custom styling - mostly Bootstrap5 utilities */
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
|
||||
background-color: #f8f9fa;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
/* Sidebar adjustments */
|
||||
.navbar.flex-column {
|
||||
box-shadow: 0.125rem 0 0.25rem rgba(0, 0, 0, 0.075);
|
||||
}
|
||||
|
||||
.navbar.flex-column .navbar-nav {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.navbar.flex-column .nav-link {
|
||||
padding: 0.75rem 1.25rem;
|
||||
border-left: 3px solid transparent;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.navbar.flex-column .nav-link:hover {
|
||||
background-color: rgba(255, 255, 255, 0.1);
|
||||
border-left-color: #667eea;
|
||||
padding-left: 1.5rem;
|
||||
}
|
||||
|
||||
.navbar.flex-column .nav-link.active {
|
||||
background-color: rgba(255, 255, 255, 0.1);
|
||||
border-left-color: #667eea;
|
||||
}
|
||||
|
||||
main {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 0;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
/* Responsive design */
|
||||
@media (max-width: 991.98px) {
|
||||
body {
|
||||
display: block !important;
|
||||
}
|
||||
|
||||
main {
|
||||
margin-left: 0 !important;
|
||||
}
|
||||
|
||||
.navbar.flex-column {
|
||||
width: 100% !important;
|
||||
height: auto !important;
|
||||
position: relative !important;
|
||||
top: auto !important;
|
||||
start: auto !important;
|
||||
}
|
||||
}
|
||||
|
||||
.navbar {
|
||||
box-shadow: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075);
|
||||
}
|
||||
|
||||
.username {
|
||||
font-weight: 600;
|
||||
color: #667eea;
|
||||
font-size: 1.1em;
|
||||
}
|
||||
|
||||
.logout-btn {
|
||||
padding: 10px 20px;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 5px;
|
||||
cursor: pointer;
|
||||
font-weight: 600;
|
||||
transition: opacity 0.3s;
|
||||
}
|
||||
|
||||
.logout-btn:hover {
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
||||
gap: 20px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.card {
|
||||
background: white;
|
||||
padding: 25px;
|
||||
border-radius: 10px;
|
||||
box-shadow: 0 10px 30px rgba(0,0,0,0.2);
|
||||
}
|
||||
|
||||
.card h2 {
|
||||
color: #333;
|
||||
margin-bottom: 15px;
|
||||
font-size: 1.5em;
|
||||
border-bottom: 2px solid #667eea;
|
||||
padding-bottom: 10px;
|
||||
}
|
||||
|
||||
.stat {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 10px 0;
|
||||
border-bottom: 1px solid #eee;
|
||||
}
|
||||
|
||||
.stat:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
color: #666;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
color: #333;
|
||||
font-weight: bold;
|
||||
font-size: 1.2em;
|
||||
}
|
||||
|
||||
.status {
|
||||
display: inline-block;
|
||||
padding: 5px 15px;
|
||||
border-radius: 20px;
|
||||
font-size: 0.9em;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.status.online {
|
||||
background: #4caf50;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.status.offline {
|
||||
background: #f44336;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.world-list {
|
||||
max-height: 300px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.world-item {
|
||||
padding: 15px;
|
||||
background: #f5f5f5;
|
||||
border-radius: 5px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.world-item h3 {
|
||||
color: #333;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.world-detail {
|
||||
color: #666;
|
||||
font-size: 0.9em;
|
||||
margin: 3px 0;
|
||||
}
|
||||
|
||||
.loading {
|
||||
text-align: center;
|
||||
padding: 20px;
|
||||
color: #666;
|
||||
}
|
||||
30
dDashboardServer/static/css/login.css
Normal file
30
dDashboardServer/static/css/login.css
Normal file
@@ -0,0 +1,30 @@
|
||||
/* Custom styling for login page on top of Bootstrap5 */
|
||||
|
||||
body {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
|
||||
}
|
||||
|
||||
.card {
|
||||
border-radius: 0.5rem;
|
||||
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.2) !important;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
border: none;
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
background: linear-gradient(135deg, #5568d3 0%, #6a3f93 100%);
|
||||
}
|
||||
|
||||
.form-control:focus {
|
||||
border-color: #667eea;
|
||||
box-shadow: 0 0 0 0.2rem rgba(102, 126, 234, 0.25);
|
||||
}
|
||||
|
||||
h1 {
|
||||
color: #333;
|
||||
font-weight: 600;
|
||||
}
|
||||
240
dDashboardServer/static/js/dashboard.js
Normal file
240
dDashboardServer/static/js/dashboard.js
Normal file
@@ -0,0 +1,240 @@
|
||||
let ws = null;
|
||||
let reconnectAttempts = 0;
|
||||
const maxReconnectAttempts = 5;
|
||||
const reconnectDelay = 3000;
|
||||
|
||||
// Helper function to get cookie value
|
||||
function getCookie(name) {
|
||||
const nameEQ = name + '=';
|
||||
const cookies = document.cookie.split(';');
|
||||
for (let cookie of cookies) {
|
||||
cookie = cookie.trim();
|
||||
if (cookie.indexOf(nameEQ) === 0) {
|
||||
return decodeURIComponent(cookie.substring(nameEQ.length));
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// Helper function to delete cookie
|
||||
function deleteCookie(name) {
|
||||
document.cookie = `${name}=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/; SameSite=Strict`;
|
||||
}
|
||||
|
||||
// Check authentication on page load
|
||||
function checkAuthentication() {
|
||||
// Check localStorage first (most secure)
|
||||
let token = localStorage.getItem('dashboardToken');
|
||||
|
||||
// Fallback to cookie if localStorage empty
|
||||
if (!token) {
|
||||
token = getCookie('dashboardToken');
|
||||
}
|
||||
|
||||
if (!token) {
|
||||
// Redirect to login if no token
|
||||
window.location.href = '/login';
|
||||
return false;
|
||||
}
|
||||
|
||||
// Verify token is valid (asynchronous)
|
||||
fetch('/api/auth/verify', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ token: token })
|
||||
})
|
||||
.then(res => {
|
||||
if (!res.ok) {
|
||||
console.error('Verify endpoint returned:', res.status);
|
||||
throw new Error(`HTTP ${res.status}`);
|
||||
}
|
||||
return res.json();
|
||||
})
|
||||
.then(data => {
|
||||
console.log('Token verification response:', data);
|
||||
if (!data.valid) {
|
||||
// Token is invalid/expired, delete cookies and redirect to login
|
||||
console.log('Token verification failed, redirecting to login');
|
||||
deleteCookie('dashboardToken');
|
||||
deleteCookie('gmLevel');
|
||||
localStorage.removeItem('dashboardToken');
|
||||
window.location.href = '/login';
|
||||
} else {
|
||||
// Update UI with username
|
||||
console.log('Token verified, user:', data.username);
|
||||
const usernameElement = document.querySelector('.username');
|
||||
if (usernameElement) {
|
||||
usernameElement.textContent = data.username || 'User';
|
||||
} else {
|
||||
console.warn('Username element not found in DOM');
|
||||
}
|
||||
// Now that verification is complete, connect to WebSocket
|
||||
setTimeout(() => {
|
||||
console.log('Starting WebSocket connection');
|
||||
connectWebSocket();
|
||||
}, 100);
|
||||
}
|
||||
})
|
||||
.catch(err => {
|
||||
console.error('Token verification error:', err);
|
||||
// Network error - log but don't redirect immediately
|
||||
// This prevents redirect loops on network issues
|
||||
});
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// Get token from localStorage or cookie
|
||||
function getAuthToken() {
|
||||
let token = localStorage.getItem('dashboardToken');
|
||||
if (!token) {
|
||||
token = getCookie('dashboardToken');
|
||||
}
|
||||
console.log('getAuthToken called, token available:', !!token);
|
||||
return token;
|
||||
}
|
||||
|
||||
// Logout function
|
||||
function logout() {
|
||||
deleteCookie('dashboardToken');
|
||||
deleteCookie('gmLevel');
|
||||
localStorage.removeItem('dashboardToken');
|
||||
window.location.href = '/login';
|
||||
}
|
||||
|
||||
function connectWebSocket() {
|
||||
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
|
||||
const token = getAuthToken();
|
||||
if (!token) {
|
||||
console.error('No token available for WebSocket connection');
|
||||
window.location.href = '/login';
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`WebSocket connection attempt ${reconnectAttempts + 1}/${maxReconnectAttempts}`);
|
||||
|
||||
// Connect to WebSocket without token in URL (token is in cookies)
|
||||
const wsUrl = `${protocol}//${window.location.host}/ws`;
|
||||
console.log(`Connecting to WebSocket: ${wsUrl}`);
|
||||
|
||||
try {
|
||||
ws = new WebSocket(wsUrl);
|
||||
|
||||
ws.onopen = () => {
|
||||
console.log('WebSocket connected');
|
||||
reconnectAttempts = 0;
|
||||
|
||||
// Subscribe to dashboard updates
|
||||
ws.send(JSON.stringify({
|
||||
event: 'subscribe',
|
||||
subscription: 'dashboard_update'
|
||||
}));
|
||||
|
||||
document.getElementById('connection-status')?.remove();
|
||||
};
|
||||
|
||||
ws.onmessage = (event) => {
|
||||
try {
|
||||
const data = JSON.parse(event.data);
|
||||
|
||||
// Handle subscription confirmation
|
||||
if (data.subscribed) {
|
||||
console.log('Subscribed to:', data.subscribed);
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle dashboard updates
|
||||
if (data.event === 'dashboard_update') {
|
||||
updateDashboard(data);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error parsing WebSocket message:', error);
|
||||
}
|
||||
};
|
||||
|
||||
ws.onerror = (error) => {
|
||||
console.error('WebSocket error:', error);
|
||||
};
|
||||
|
||||
ws.onclose = () => {
|
||||
console.log('WebSocket disconnected');
|
||||
ws = null;
|
||||
|
||||
// Show connection status
|
||||
showConnectionStatus('Disconnected - Attempting to reconnect...');
|
||||
|
||||
// Attempt to reconnect with exponential backoff
|
||||
if (reconnectAttempts < maxReconnectAttempts) {
|
||||
reconnectAttempts++;
|
||||
const backoffDelay = reconnectDelay * Math.pow(2, reconnectAttempts - 1);
|
||||
console.log(`Reconnecting in ${backoffDelay}ms (attempt ${reconnectAttempts}/${maxReconnectAttempts})`);
|
||||
setTimeout(connectWebSocket, backoffDelay);
|
||||
} else {
|
||||
console.error('Max reconnection attempts reached');
|
||||
showConnectionStatus('Connection lost - Reload page to reconnect');
|
||||
}
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Failed to create WebSocket:', error);
|
||||
showConnectionStatus('Failed to connect - Reload page to retry');
|
||||
}
|
||||
}
|
||||
|
||||
function showConnectionStatus(message) {
|
||||
let statusEl = document.getElementById('connection-status');
|
||||
if (!statusEl) {
|
||||
statusEl = document.createElement('div');
|
||||
statusEl.id = 'connection-status';
|
||||
statusEl.style.cssText = 'position: fixed; top: 10px; right: 10px; background: #f44336; color: white; padding: 10px 20px; border-radius: 4px; z-index: 1000;';
|
||||
document.body.appendChild(statusEl);
|
||||
}
|
||||
statusEl.textContent = message;
|
||||
}
|
||||
|
||||
function updateDashboard(data) {
|
||||
// Update server status
|
||||
if (data.auth) {
|
||||
document.getElementById('auth-status').textContent = data.auth.online ? 'Online' : 'Offline';
|
||||
document.getElementById('auth-status').className = 'status ' + (data.auth.online ? 'online' : 'offline');
|
||||
}
|
||||
|
||||
if (data.chat) {
|
||||
document.getElementById('chat-status').textContent = data.chat.online ? 'Online' : 'Offline';
|
||||
document.getElementById('chat-status').className = 'status ' + (data.chat.online ? 'online' : 'offline');
|
||||
}
|
||||
|
||||
// Update world instances
|
||||
if (data.worlds) {
|
||||
document.getElementById('world-count').textContent = data.worlds.length;
|
||||
|
||||
const worldList = document.getElementById('world-list');
|
||||
if (data.worlds.length === 0) {
|
||||
worldList.innerHTML = '<div class="loading">No active world instances</div>';
|
||||
} else {
|
||||
worldList.innerHTML = data.worlds.map(world => `
|
||||
<div class="world-item">
|
||||
<h3>Zone ${world.mapID} - Instance ${world.instanceID}</h3>
|
||||
<div class="world-detail">Clone ID: ${world.cloneID}</div>
|
||||
<div class="world-detail">Players: ${world.players}</div>
|
||||
<div class="world-detail">Type: ${world.isPrivate ? 'Private' : 'Public'}</div>
|
||||
</div>
|
||||
`).join('');
|
||||
}
|
||||
}
|
||||
|
||||
// Update statistics
|
||||
if (data.stats) {
|
||||
if (data.stats.onlinePlayers !== undefined) {
|
||||
document.getElementById('online-players').textContent = data.stats.onlinePlayers;
|
||||
}
|
||||
if (data.stats.totalAccounts !== undefined) {
|
||||
document.getElementById('total-accounts').textContent = data.stats.totalAccounts;
|
||||
}
|
||||
if (data.stats.totalCharacters !== undefined) {
|
||||
document.getElementById('total-characters').textContent = data.stats.totalCharacters;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Connect on page load
|
||||
connectWebSocket();
|
||||
99
dDashboardServer/static/js/login.js
Normal file
99
dDashboardServer/static/js/login.js
Normal file
@@ -0,0 +1,99 @@
|
||||
// Check if user is already logged in
|
||||
function checkExistingToken() {
|
||||
const token = localStorage.getItem('dashboardToken');
|
||||
if (token) {
|
||||
verifyTokenAndRedirect(token);
|
||||
}
|
||||
}
|
||||
|
||||
function verifyTokenAndRedirect(token) {
|
||||
fetch('/api/auth/verify', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ token: token })
|
||||
})
|
||||
.then(res => res.json())
|
||||
.then(data => {
|
||||
if (data.valid) {
|
||||
window.location.href = '/';
|
||||
}
|
||||
})
|
||||
.catch(err => console.error('Token verification failed:', err));
|
||||
}
|
||||
|
||||
function showAlert(message, type) {
|
||||
const alert = document.getElementById('alert');
|
||||
alert.textContent = message;
|
||||
alert.className = 'alert';
|
||||
if (type === 'error') {
|
||||
alert.classList.add('alert-danger');
|
||||
} else if (type === 'success') {
|
||||
alert.classList.add('alert-success');
|
||||
}
|
||||
alert.style.display = 'block';
|
||||
}
|
||||
|
||||
// Wait for DOM to be ready
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const loginForm = document.getElementById('loginForm');
|
||||
if (!loginForm) {
|
||||
console.error('Login form not found');
|
||||
return;
|
||||
}
|
||||
|
||||
loginForm.addEventListener('submit', async (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
const username = document.getElementById('username').value;
|
||||
const password = document.getElementById('password').value;
|
||||
const rememberMe = document.getElementById('rememberMe').checked;
|
||||
|
||||
// Validate input
|
||||
if (!username || !password) {
|
||||
showAlert('Username and password are required', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
if (password.length > 40) {
|
||||
showAlert('Password exceeds maximum length (40 characters)', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
// Show loading state
|
||||
document.getElementById('loading').style.display = 'inline-block';
|
||||
document.getElementById('loginBtn').disabled = true;
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/auth/login', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ username, password, rememberMe })
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (data.success) {
|
||||
// Store token in localStorage (also set as cookie for API calls)
|
||||
localStorage.setItem('dashboardToken', data.token);
|
||||
document.cookie = `dashboardToken=${data.token}; path=/; SameSite=Strict`;
|
||||
showAlert('Login successful! Redirecting...', 'success');
|
||||
|
||||
// Redirect after a short delay (no token in URL)
|
||||
setTimeout(() => {
|
||||
window.location.href = '/';
|
||||
}, 1000);
|
||||
} else {
|
||||
showAlert(data.message || 'Login failed', 'error');
|
||||
document.getElementById('loading').style.display = 'none';
|
||||
document.getElementById('loginBtn').disabled = false;
|
||||
}
|
||||
} catch (error) {
|
||||
showAlert('Network error: ' + error.message, 'error');
|
||||
document.getElementById('loading').style.display = 'none';
|
||||
document.getElementById('loginBtn').disabled = false;
|
||||
}
|
||||
});
|
||||
|
||||
// Check existing token on page load
|
||||
checkExistingToken();
|
||||
});
|
||||
Reference in New Issue
Block a user