uploaded
This commit is contained in:
20
admin/create-admin.js
Normal file
20
admin/create-admin.js
Normal file
@@ -0,0 +1,20 @@
|
||||
// create-admin.js
|
||||
const path = require('path');
|
||||
require('dotenv').config({ path: path.join(__dirname, '..', '.env') });
|
||||
|
||||
const bcrypt = require('bcrypt');
|
||||
const { pool } = require('../db'); // db.js is one level up
|
||||
|
||||
async function createSuperAdmin() {
|
||||
const username = 'admin_eagler';
|
||||
const password = 'ILOOyCo8zDWWNl9bOyFbtiBULrNKd9';
|
||||
const hash = await bcrypt.hash(password, 10);
|
||||
await pool.query(
|
||||
'INSERT INTO admin_users (username, password_hash, role) VALUES (?, ?, ?)',
|
||||
[username, hash, 'superadmin']
|
||||
);
|
||||
console.log('Superadmin created');
|
||||
process.exit();
|
||||
}
|
||||
|
||||
createSuperAdmin().catch(console.error);
|
||||
5
admin/db.js
Normal file
5
admin/db.js
Normal file
@@ -0,0 +1,5 @@
|
||||
DB_HOST=127.0.0.1
|
||||
DB_PORT=3306
|
||||
DB_USER=eaglertiers
|
||||
DB_PASSWORD=eagler_local_dev_2026
|
||||
DB_NAME=eaglertiers
|
||||
89
admin/server/admin.js
Normal file
89
admin/server/admin.js
Normal file
@@ -0,0 +1,89 @@
|
||||
require('dotenv').config();
|
||||
const express = require('express');
|
||||
const { configureSecurityHeaders } = require('./middleware/securityHeaders');
|
||||
const { adminLimiter } = require('./middleware/rateLimiter');
|
||||
const adminRoutes = require('./routes/admin');
|
||||
|
||||
const adminApp = express();
|
||||
const ADMIN_PORT = Number(process.env.ADMIN_PORT || 3005);
|
||||
const isDevelopment = process.env.NODE_ENV !== 'production';
|
||||
|
||||
/**
|
||||
* Trust proxy for correct IP detection
|
||||
* Important: Only enable if behind a reverse proxy
|
||||
*/
|
||||
adminApp.set('trust proxy', process.env.TRUST_PROXY === 'true' ? true : 1);
|
||||
|
||||
configureSecurityHeaders(adminApp, isDevelopment);
|
||||
|
||||
adminApp.use((req, res, next) => {
|
||||
const allowedOrigins = [
|
||||
'http://localhost:3005',
|
||||
'http://localhost:3000',
|
||||
process.env.ADMIN_ORIGIN || '',
|
||||
].filter(Boolean);
|
||||
|
||||
const origin = req.get('origin');
|
||||
if (allowedOrigins.includes(origin)) {
|
||||
res.setHeader('Access-Control-Allow-Origin', origin);
|
||||
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
|
||||
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization');
|
||||
res.setHeader('Access-Control-Allow-Credentials', 'true');
|
||||
}
|
||||
|
||||
if (req.method === 'OPTIONS') {
|
||||
return res.sendStatus(204);
|
||||
}
|
||||
|
||||
next();
|
||||
});
|
||||
|
||||
// Body parser
|
||||
adminApp.use(express.json({ limit: '10mb' }));
|
||||
adminApp.use(express.urlencoded({ limit: '10mb', extended: true }));
|
||||
|
||||
// Global rate limiting
|
||||
adminApp.use(adminLimiter);
|
||||
|
||||
// Admin routes with /api/admin prefix
|
||||
adminApp.use('/api/admin', adminRoutes);
|
||||
|
||||
// Health check endpoint
|
||||
adminApp.get('/health', (req, res) => {
|
||||
res.json({ status: 'ok', timestamp: new Date().toISOString() });
|
||||
});
|
||||
|
||||
// 404 handler
|
||||
adminApp.use((req, res) => {
|
||||
res.status(404).json({
|
||||
error: 'Not found',
|
||||
path: req.path,
|
||||
});
|
||||
});
|
||||
|
||||
// Error handler
|
||||
adminApp.use((err, req, res, next) => {
|
||||
console.error('Unhandled error:', err);
|
||||
|
||||
if (isDevelopment) {
|
||||
return res.status(500).json({
|
||||
error: 'Internal server error',
|
||||
message: err.message,
|
||||
stack: err.stack,
|
||||
});
|
||||
}
|
||||
|
||||
return res.status(500).json({
|
||||
error: 'Internal server error',
|
||||
});
|
||||
});
|
||||
|
||||
// Start admin server
|
||||
function startAdminServer() {
|
||||
adminApp.listen(ADMIN_PORT, () => {
|
||||
console.log(`EaglerTiers Admin API running on http://localhost:${ADMIN_PORT}`);
|
||||
console.log(`Admin endpoints require API key via Authorization header or ?key parameter`);
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = { adminApp, startAdminServer };
|
||||
18
admin/server/create-admin.js
Normal file
18
admin/server/create-admin.js
Normal file
@@ -0,0 +1,18 @@
|
||||
// create-admin.js
|
||||
require('dotenv').config();
|
||||
const bcrypt = require('bcrypt');
|
||||
const { pool } = require('./db');
|
||||
|
||||
async function createSuperAdmin() {
|
||||
const username = 'admin'; // change as needed
|
||||
const password = 'your-strong-password';
|
||||
const hash = await bcrypt.hash(password, 10);
|
||||
await pool.query(
|
||||
'INSERT INTO admin_users (username, password_hash, role) VALUES (?, ?, ?)',
|
||||
[username, hash, 'superadmin']
|
||||
);
|
||||
console.log('Superadmin created');
|
||||
process.exit();
|
||||
}
|
||||
|
||||
createSuperAdmin().catch(console.error);
|
||||
96
admin/server/db.js
Normal file
96
admin/server/db.js
Normal file
@@ -0,0 +1,96 @@
|
||||
// db.js
|
||||
const mysql = require('mysql2/promise');
|
||||
const path = require('path');
|
||||
require('dotenv').config({ path: path.join(__dirname, '.env') });
|
||||
|
||||
const {
|
||||
DB_HOST = '127.0.0.1',
|
||||
DB_PORT = '3306',
|
||||
DB_USER = 'eaglertiers',
|
||||
DB_PASSWORD = 'eagler_local_dev_2026',
|
||||
DB_NAME = 'eaglertiers',
|
||||
} = process.env;
|
||||
|
||||
// Create a connection pool (used by the app)
|
||||
const pool = mysql.createPool({
|
||||
host: DB_HOST,
|
||||
port: Number(DB_PORT),
|
||||
user: DB_USER,
|
||||
password: DB_PASSWORD,
|
||||
database: DB_NAME,
|
||||
waitForConnections: true,
|
||||
connectionLimit: 10,
|
||||
queueLimit: 0,
|
||||
enableKeepAlive: true,
|
||||
keepAliveInitialDelay: 0,
|
||||
});
|
||||
|
||||
/**
|
||||
* Ensures the database and required tables exist.
|
||||
* Call this once during server startup.
|
||||
*/
|
||||
async function ensureDatabase() {
|
||||
// First, connect without database to create it if necessary
|
||||
const rootPool = mysql.createPool({
|
||||
host: DB_HOST,
|
||||
port: Number(DB_PORT),
|
||||
user: DB_USER,
|
||||
password: DB_PASSWORD,
|
||||
connectionLimit: 1,
|
||||
});
|
||||
|
||||
try {
|
||||
// Create database if it doesn't exist
|
||||
await rootPool.query(`CREATE DATABASE IF NOT EXISTS \`${DB_NAME}\``);
|
||||
console.log(`Database "${DB_NAME}" ensured.`);
|
||||
|
||||
// Now use the main pool (with database selected) to create tables
|
||||
const connection = await pool.getConnection();
|
||||
|
||||
// Create players table
|
||||
await connection.query(`
|
||||
CREATE TABLE IF NOT EXISTS players (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
username VARCHAR(16) UNIQUE NOT NULL,
|
||||
region VARCHAR(2) NOT NULL DEFAULT 'NA',
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
)
|
||||
`);
|
||||
|
||||
// Create player_ranks table
|
||||
await connection.query(`
|
||||
CREATE TABLE IF NOT EXISTS player_ranks (
|
||||
player_id INT NOT NULL,
|
||||
gamemode VARCHAR(20) NOT NULL,
|
||||
tier VARCHAR(3) NOT NULL,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (player_id, gamemode),
|
||||
FOREIGN KEY (player_id) REFERENCES players(id) ON DELETE CASCADE
|
||||
)
|
||||
`);
|
||||
|
||||
// Create admin_users table (if you haven't already)
|
||||
await connection.query(`
|
||||
CREATE TABLE IF NOT EXISTS admin_users (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
username VARCHAR(50) UNIQUE NOT NULL,
|
||||
password_hash VARCHAR(255) NOT NULL,
|
||||
role ENUM('admin','superadmin') DEFAULT 'admin',
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
)
|
||||
`);
|
||||
|
||||
connection.release();
|
||||
console.log('All tables ensured.');
|
||||
} catch (err) {
|
||||
console.error('Failed to ensure database:', err);
|
||||
throw err;
|
||||
} finally {
|
||||
await rootPool.end();
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
pool,
|
||||
ensureDatabase,
|
||||
};
|
||||
309
admin/static/index.html
Normal file
309
admin/static/index.html
Normal file
@@ -0,0 +1,309 @@
|
||||
<!-- admin/static/index.html -->
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>EaglerTiers Admin</title>
|
||||
<style>
|
||||
body { font-family: Arial; margin: 20px; background: #f5f5f5; }
|
||||
.container { max-width: 1200px; margin: auto; background: white; padding: 20px; border-radius: 8px; box-shadow: 0 0 10px rgba(0,0,0,0.1); }
|
||||
h1 { color: #333; }
|
||||
input, select, button { padding: 8px; margin: 5px; width: 100%; max-width: 300px; }
|
||||
button { background: #007bff; color: white; border: none; cursor: pointer; }
|
||||
button:hover { background: #0056b3; }
|
||||
.error { color: red; }
|
||||
.success { color: green; }
|
||||
table { width: 100%; border-collapse: collapse; margin-top: 20px; }
|
||||
th, td { border: 1px solid #ddd; padding: 8px; text-align: left; }
|
||||
th { background: #f2f2f2; }
|
||||
.tab { overflow: hidden; border-bottom: 1px solid #ccc; }
|
||||
.tab button { background-color: inherit; float: left; border: none; outline: none; cursor: pointer; padding: 10px 16px; transition: 0.3s; max-width: auto; }
|
||||
.tab button:hover { background-color: #ddd; }
|
||||
.tab button.active { background-color: #ccc; }
|
||||
.tabcontent { display: none; padding: 20px 0; }
|
||||
.logout { float: right; background: #dc3545; }
|
||||
.logout:hover { background: #c82333; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container" id="app">
|
||||
<div v-if="!token">
|
||||
<h1>Admin Login</h1>
|
||||
<div>
|
||||
<input type="text" v-model="loginUsername" placeholder="Username">
|
||||
<input type="password" v-model="loginPassword" placeholder="Password">
|
||||
<button @click="login">Login</button>
|
||||
<p class="error" v-if="loginError">{{ loginError }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else>
|
||||
<div style="display: flex; justify-content: space-between; align-items: center;">
|
||||
<h1>EaglerTiers Admin Dashboard</h1>
|
||||
<button @click="logout" class="logout">Logout</button>
|
||||
</div>
|
||||
<div class="tab">
|
||||
<button :class="{active: activeTab === 'players'}" @click="activeTab = 'players'">Players</button>
|
||||
<button :class="{active: activeTab === 'ranks'}" @click="activeTab = 'ranks'">Ranks</button>
|
||||
<button v-if="userRole === 'superadmin'" :class="{active: activeTab === 'users'}" @click="activeTab = 'users'">Admin Users</button>
|
||||
</div>
|
||||
|
||||
<!-- Players Tab -->
|
||||
<div v-show="activeTab === 'players'" class="tabcontent" style="display: block;">
|
||||
<h2>Players</h2>
|
||||
<div>
|
||||
<input type="text" v-model="newPlayer.username" placeholder="Username">
|
||||
<select v-model="newPlayer.region">
|
||||
<option v-for="r in regions" :value="r">{{ r }}</option>
|
||||
</select>
|
||||
<button @click="createPlayer">Create Player</button>
|
||||
</div>
|
||||
<table>
|
||||
<thead><tr><th>ID</th><th>Username</th><th>Region</th><th>Created</th><th>Actions</th></tr></thead>
|
||||
<tbody>
|
||||
<tr v-for="p in players" :key="p.id">
|
||||
<td>{{ p.id }}</td>
|
||||
<td>{{ p.username }}</td>
|
||||
<td>{{ p.region }}</td>
|
||||
<td>{{ new Date(p.created_at).toLocaleDateString() }}</td>
|
||||
<td><button @click="deletePlayer(p.id)">Delete</button></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- Ranks Tab -->
|
||||
<div v-show="activeTab === 'ranks'" class="tabcontent">
|
||||
<h2>Manage Ranks</h2>
|
||||
<div>
|
||||
<select v-model="rankPlayerId">
|
||||
<option v-for="p in players" :value="p.id">{{ p.username }}</option>
|
||||
</select>
|
||||
<select v-model="rankGamemode">
|
||||
<option v-for="g in gamemodes" :value="g">{{ g }}</option>
|
||||
</select>
|
||||
<select v-model="rankTier">
|
||||
<option v-for="t in tiers" :value="t">{{ t }}</option>
|
||||
</select>
|
||||
<button @click="setRank">Set Rank</button>
|
||||
</div>
|
||||
<table>
|
||||
<thead><tr><th>Player</th><th>Gamemode</th><th>Tier</th><th>Actions</th></tr></thead>
|
||||
<tbody>
|
||||
<tr v-for="r in allRanks" :key="r.player_id + r.gamemode">
|
||||
<td>{{ getPlayerName(r.player_id) }}</td>
|
||||
<td>{{ r.gamemode }}</td>
|
||||
<td>{{ r.tier }}</td>
|
||||
<td><button @click="deleteRank(r.player_id, r.gamemode)">Delete</button></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- Admin Users Tab (superadmin only) -->
|
||||
<div v-show="activeTab === 'users' && userRole === 'superadmin'" class="tabcontent">
|
||||
<h2>Admin Users</h2>
|
||||
<div>
|
||||
<input type="text" v-model="newUser.username" placeholder="Username">
|
||||
<input type="password" v-model="newUser.password" placeholder="Password">
|
||||
<select v-model="newUser.role">
|
||||
<option value="admin">Admin</option>
|
||||
<option value="superadmin">Superadmin</option>
|
||||
</select>
|
||||
<button @click="createAdminUser">Create User</button>
|
||||
</div>
|
||||
<table>
|
||||
<thead><tr><th>ID</th><th>Username</th><th>Role</th><th>Created</th><th>Actions</th></tr></thead>
|
||||
<tbody>
|
||||
<tr v-for="u in adminUsers" :key="u.id">
|
||||
<td>{{ u.id }}</td>
|
||||
<td>{{ u.username }}</td>
|
||||
<td>{{ u.role }}</td>
|
||||
<td>{{ new Date(u.created_at).toLocaleDateString() }}</td>
|
||||
<td><button @click="deleteAdminUser(u.id)">Delete</button></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
|
||||
<script>
|
||||
const { createApp, ref, onMounted } = Vue;
|
||||
|
||||
createApp({
|
||||
setup() {
|
||||
const token = ref(localStorage.getItem('adminToken') || '');
|
||||
const userRole = ref(localStorage.getItem('userRole') || '');
|
||||
const activeTab = ref('players');
|
||||
const loginUsername = ref('');
|
||||
const loginPassword = ref('');
|
||||
const loginError = ref('');
|
||||
|
||||
const players = ref([]);
|
||||
const allRanks = ref([]);
|
||||
const adminUsers = ref([]);
|
||||
|
||||
const newPlayer = ref({ username: '', region: 'NA' });
|
||||
const rankPlayerId = ref('');
|
||||
const rankGamemode = ref('');
|
||||
const rankTier = ref('');
|
||||
const newUser = ref({ username: '', password: '', role: 'admin' });
|
||||
|
||||
const regions = ['NA', 'EU', 'AS', 'AU'];
|
||||
const gamemodes = ['NETH_POT', 'CRYSTAL', 'CLASSIC', 'BUILDUHC', 'SUMO', 'GAP', 'AXE', 'SMP', 'VANILLA', 'BRIDGE', 'COMBO', 'FIREBALL'];
|
||||
const tiers = ['HT1','LT1','HT2','LT2','HT3','LT3','HT4','LT4','HT5','LT5'];
|
||||
|
||||
const apiBase = ''; // relative to same origin
|
||||
|
||||
function getHeaders() {
|
||||
const headers = { 'Content-Type': 'application/json' };
|
||||
if (token.value) {
|
||||
headers['Authorization'] = `Bearer ${token.value}`;
|
||||
}
|
||||
return headers;
|
||||
}
|
||||
|
||||
async function login() {
|
||||
loginError.value = '';
|
||||
try {
|
||||
const res = await fetch('/admin/login', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ username: loginUsername.value, password: loginPassword.value })
|
||||
});
|
||||
const data = await res.json();
|
||||
if (!res.ok) {
|
||||
loginError.value = data.error || 'Login failed';
|
||||
return;
|
||||
}
|
||||
token.value = data.token;
|
||||
userRole.value = data.user.role;
|
||||
localStorage.setItem('adminToken', data.token);
|
||||
localStorage.setItem('userRole', data.user.role);
|
||||
fetchData();
|
||||
} catch (err) {
|
||||
loginError.value = 'Network error';
|
||||
}
|
||||
}
|
||||
|
||||
function logout() {
|
||||
token.value = '';
|
||||
userRole.value = '';
|
||||
localStorage.removeItem('adminToken');
|
||||
localStorage.removeItem('userRole');
|
||||
players.value = [];
|
||||
allRanks.value = [];
|
||||
adminUsers.value = [];
|
||||
}
|
||||
|
||||
async function fetchData() {
|
||||
if (!token.value) return;
|
||||
try {
|
||||
const [playersRes, ranksRes] = await Promise.all([
|
||||
fetch('/admin/players', { headers: getHeaders() }),
|
||||
fetch('/admin/rankings', { headers: getHeaders() })
|
||||
]);
|
||||
if (playersRes.ok) players.value = await playersRes.json();
|
||||
if (ranksRes.ok) allRanks.value = await ranksRes.json();
|
||||
|
||||
if (userRole.value === 'superadmin') {
|
||||
const usersRes = await fetch('/admin/users', { headers: getHeaders() });
|
||||
if (usersRes.ok) adminUsers.value = await usersRes.json();
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Fetch error', err);
|
||||
}
|
||||
}
|
||||
|
||||
async function createPlayer() {
|
||||
const res = await fetch('/admin/player', {
|
||||
method: 'POST',
|
||||
headers: getHeaders(),
|
||||
body: JSON.stringify(newPlayer.value)
|
||||
});
|
||||
if (res.ok) {
|
||||
newPlayer.value = { username: '', region: 'NA' };
|
||||
fetchData();
|
||||
} else {
|
||||
const err = await res.json();
|
||||
alert('Error: ' + err.error);
|
||||
}
|
||||
}
|
||||
|
||||
async function deletePlayer(id) {
|
||||
if (!confirm('Delete player? This will also delete all ranks.')) return;
|
||||
const res = await fetch(`/admin/player/${id}`, { method: 'DELETE', headers: getHeaders() });
|
||||
if (res.ok) fetchData();
|
||||
else alert('Delete failed');
|
||||
}
|
||||
|
||||
async function setRank() {
|
||||
if (!rankPlayerId.value || !rankGamemode.value || !rankTier.value) return;
|
||||
const res = await fetch(`/admin/player/${rankPlayerId.value}/rank`, {
|
||||
method: 'PUT',
|
||||
headers: getHeaders(),
|
||||
body: JSON.stringify({ gamemode: rankGamemode.value, tier: rankTier.value })
|
||||
});
|
||||
if (res.ok) {
|
||||
rankPlayerId.value = rankGamemode.value = rankTier.value = '';
|
||||
fetchData();
|
||||
} else {
|
||||
const err = await res.json();
|
||||
alert('Error: ' + err.error);
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteRank(playerId, gamemode) {
|
||||
const res = await fetch(`/admin/player/${playerId}/rank/${gamemode}`, {
|
||||
method: 'DELETE',
|
||||
headers: getHeaders()
|
||||
});
|
||||
if (res.ok) fetchData();
|
||||
else alert('Delete failed');
|
||||
}
|
||||
|
||||
function getPlayerName(playerId) {
|
||||
const p = players.value.find(p => p.id === playerId);
|
||||
return p ? p.username : '?';
|
||||
}
|
||||
|
||||
async function createAdminUser() {
|
||||
const res = await fetch('/admin/users', {
|
||||
method: 'POST',
|
||||
headers: getHeaders(),
|
||||
body: JSON.stringify(newUser.value)
|
||||
});
|
||||
if (res.ok) {
|
||||
newUser.value = { username: '', password: '', role: 'admin' };
|
||||
fetchData();
|
||||
} else {
|
||||
const err = await res.json();
|
||||
alert('Error: ' + err.error);
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteAdminUser(id) {
|
||||
if (!confirm('Delete admin user?')) return;
|
||||
const res = await fetch(`/admin/users/${id}`, { method: 'DELETE', headers: getHeaders() });
|
||||
if (res.ok) fetchData();
|
||||
else alert('Delete failed');
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
if (token.value) fetchData();
|
||||
});
|
||||
|
||||
return {
|
||||
token, userRole, activeTab, loginUsername, loginPassword, loginError,
|
||||
players, allRanks, adminUsers, newPlayer, rankPlayerId, rankGamemode, rankTier,
|
||||
newUser, regions, gamemodes, tiers,
|
||||
login, logout, fetchData, createPlayer, deletePlayer, setRank, deleteRank,
|
||||
getPlayerName, createAdminUser, deleteAdminUser
|
||||
};
|
||||
}
|
||||
}).mount('#app');
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user