Init cs2 rcon panel

This commit is contained in:
Shobhit Pathak 2023-09-28 10:53:33 +05:30
commit 78d9792a03
22 changed files with 4384 additions and 0 deletions

2
.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
node_modules
*.db

44
app.js Normal file
View File

@ -0,0 +1,44 @@
const express = require('express');
const session = require('express-session');
const ejs = require('ejs');
const app = express();
const bodyParser = require('body-parser');
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: true }));
app.use(session({
secret: 'keyboard cat',
resave: false,
saveUninitialized: true,
}));
const gameModule = require('./routes/game');
const gameRoutes = gameModule.router;
const serverRoutes = require('./routes/server');
const authRoutes = require('./routes/auth');
const port = 3000;
// Serve static files from the 'public' directory
app.use(express.static('public'));
// Set the view engine to EJS
app.set('view engine', 'ejs');
app.use('/', gameRoutes);
app.use('/', serverRoutes);
app.use('/', authRoutes)
app.get('/', (req, res) => {
if (req.session.user) {
res.redirect('/servers');
} else {
res.render('login');
}
});
app.listen(port, () => {
console.log(`Server is running on port ${port}`);
});

17
cfg/knife.cfg Normal file
View File

@ -0,0 +1,17 @@
mp_ct_default_secondary ""
mp_free_armor 1
mp_freezetime 10
mp_give_player_c4 0
mp_maxmoney 0
mp_respawn_immunitytime 0
mp_respawn_on_death_ct 0
mp_respawn_on_death_t 0
mp_roundtime 1.92
mp_roundtime_defuse 1.92
mp_roundtime_hostage 1.92
mp_t_default_secondary ""
mp_round_restart_delay 3
say Knife!
say Knife!
say Knife!

114
cfg/live.cfg Normal file
View File

@ -0,0 +1,114 @@
ammo_grenade_limit_default 1
ammo_grenade_limit_flashbang 2
ammo_grenade_limit_total 4
bot_quota 0
cash_player_bomb_defused 300
cash_player_bomb_planted 300
cash_player_damage_hostage -30
cash_player_interact_with_hostage 300
cash_player_killed_enemy_default 300
cash_player_killed_enemy_factor 1
cash_player_killed_hostage -1000
cash_player_killed_teammate -300
cash_player_rescued_hostage 1000
cash_team_bonus_shorthanded 1000
cash_team_elimination_bomb_map 3250
cash_team_elimination_hostage_map_ct 3000
cash_team_elimination_hostage_map_t 3000
cash_team_hostage_alive 0
cash_team_hostage_interaction 600
cash_team_loser_bonus 1400
cash_team_loser_bonus_consecutive_rounds 500
cash_team_planted_bomb_but_defused 800
cash_team_rescued_hostage 600
cash_team_terrorist_win_bomb 3500
cash_team_win_by_defusing_bomb 3500
cash_team_win_by_hostage_rescue 2900
cash_team_win_by_time_running_out_bomb 3250
cash_team_win_by_time_running_out_hostage 3250
ff_damage_reduction_bullets 0.33
ff_damage_reduction_grenade 0.85
ff_damage_reduction_grenade_self 1
ff_damage_reduction_other 0.4
mp_afterroundmoney 0
mp_autokick 0
mp_autoteambalance 0
mp_backup_restore_load_autopause 1
mp_backup_round_auto 1
mp_buy_anywhere 0
mp_buy_during_immunity 0
mp_buytime 20
mp_c4timer 40
mp_ct_default_melee weapon_knife
mp_ct_default_primary ""
mp_ct_default_secondary weapon_hkp2000
mp_death_drop_defuser 1
mp_death_drop_grenade 2
mp_death_drop_gun 1
mp_defuser_allocation 0
mp_display_kill_assists 1
mp_endmatch_votenextmap 0
mp_forcecamera 1
mp_free_armor 0
mp_freezetime 15
mp_friendlyfire 1
mp_give_player_c4 1
mp_halftime 1
mp_halftime_duration 15
mp_halftime_pausetimer 0
mp_ignore_round_win_conditions 0
mp_limitteams 0
mp_match_can_clinch 1
mp_match_end_restart 1
mp_maxmoney 16000
mp_maxrounds 24
mp_molotovusedelay 0
mp_overtime_enable 1
mp_overtime_halftime_pausetimer 0
mp_overtime_maxrounds 6
mp_overtime_startmoney 10000
mp_playercashawards 1
mp_randomspawn 0
mp_respawn_immunitytime 0
mp_respawn_on_death_ct 0
mp_respawn_on_death_t 0
mp_round_restart_delay 7
mp_roundtime 1.92
mp_roundtime_defuse 1.92
mp_roundtime_hostage 1.92
mp_solid_teammates 1
mp_starting_losses 1
mp_startmoney 800
mp_t_default_melee weapon_knife
mp_t_default_primary ""
mp_t_default_secondary weapon_glock
mp_teamcashawards 1
mp_timelimit 0
mp_weapons_allow_map_placed 1
mp_weapons_allow_zeus 1
mp_weapons_glow_on_ground 0
mp_win_panel_display_time 3
occlusion_test_async 0
spec_freeze_deathanim_time 0
spec_freeze_panel_extended_time 0
spec_freeze_time 2
spec_freeze_time_lock 2
spec_replay_enable 0
sv_allow_votes 0
sv_auto_full_alltalk_during_warmup_half_end 0
sv_coaching_enabled 1
sv_competitive_official_5v5 1
sv_damage_print_enable 0
sv_deadtalk 1
sv_hibernate_postgame_delay 300
sv_holiday_mode 0
sv_ignoregrenaderadio 0
sv_infinite_ammo 0
sv_occlude_players 1
sv_talk_enemy_dead 0
sv_talk_enemy_living 0
sv_voiceenable 1
tv_relayvoice 0
say Match is live!
say Match is live!
say Match is live!

70
cfg/live_kancha.cfg Normal file
View File

@ -0,0 +1,70 @@
ammo_grenade_limit_default 1
ammo_grenade_limit_flashbang 2
ammo_grenade_limit_total 4
bot_kick
bot_quota 0
mp_afterroundmoney 0
mp_autokick 0
mp_autoteambalance 0
mp_backup_restore_load_autopause 0
mp_backup_round_auto 1
mp_buytime 20
mp_c4timer 40
mp_ct_default_secondary "weapon_hkp2000"
mp_death_drop_gun 1
mp_endmatch_votenextmap 0
mp_free_armor 0
mp_freezetime 17
mp_give_player_c4 1
mp_halftime_duration 15
mp_halftime_pausetimer 0
mp_ignore_round_win_conditions 0
mp_limitteams 0
mp_match_end_restart 1
mp_maxmoney 16000
mp_maxrounds 24
mp_overtime_enable 1
mp_overtime_halftime_pausetimer 0
mp_overtime_maxrounds 6
mp_overtime_startmoney 10000
mp_respawn_immunitytime 0
mp_respawn_on_death_ct 0
mp_respawn_on_death_t 0
mp_round_restart_delay 5
mp_roundtime 1.92
mp_roundtime_defuse 1.92
mp_roundtime_hostage 1.92
mp_solid_teammates 1
mp_starting_losses 1
mp_startmoney 800
mp_t_default_secondary "weapon_glock"
mp_timelimit 0
spec_freeze_deathanim_time 0
spec_freeze_panel_extended_time 0
spec_freeze_time 2
spec_freeze_time_lock 2
mp_weapons_allow_typecount 5
cash_team_bonus_shorthanded 0
cash_team_loser_bonus_shorthanded 0
sv_allow_votes 0
sv_auto_full_alltalk_during_warmup_half_end 0
sv_coaching_enabled 1
sv_competitive_official_5v5 1
sv_damage_print_enable 0
sv_deadtalk 1
sv_hibernate_postgame_delay 300
sv_holiday_mode 0
sv_talk_enemy_dead 0
sv_talk_enemy_living 0
sv_voiceenable 1
tv_relayvoice 1
mp_friendlyfire 1
sv_coach_comm_unrestricted 1
say Match is live!!
say Match is live!!
say Match is live!!
say Match is live!!
say Match is live!!
say Match is live!!
say Match is live!!

114
cfg/live_wingman.cfg Normal file
View File

@ -0,0 +1,114 @@
ammo_grenade_limit_default 1
ammo_grenade_limit_flashbang 2
ammo_grenade_limit_total 4
bot_quota 0
cash_player_bomb_defused 300
cash_player_bomb_planted 300
cash_player_damage_hostage -30
cash_player_interact_with_hostage 300
cash_player_killed_enemy_default 300
cash_player_killed_enemy_factor 1
cash_player_killed_hostage -1000
cash_player_killed_teammate -300
cash_player_rescued_hostage 1000
cash_team_bonus_shorthanded 1000
cash_team_elimination_bomb_map 2750
cash_team_elimination_hostage_map_ct 2500
cash_team_elimination_hostage_map_t 2500
cash_team_hostage_alive 0
cash_team_hostage_interaction 600
cash_team_loser_bonus 2000
cash_team_loser_bonus_consecutive_rounds 300
cash_team_planted_bomb_but_defused 800
cash_team_rescued_hostage 600
cash_team_terrorist_win_bomb 3000
cash_team_win_by_defusing_bomb 3000
cash_team_win_by_hostage_rescue 2900
cash_team_win_by_time_running_out_bomb 2750
cash_team_win_by_time_running_out_hostage 2750
ff_damage_reduction_bullets 0.33
ff_damage_reduction_grenade 0.85
ff_damage_reduction_grenade_self 1
ff_damage_reduction_other 0.4
mp_afterroundmoney 0
mp_autokick 0
mp_autoteambalance 0
mp_backup_restore_load_autopause 1
mp_backup_round_auto 1
mp_buy_anywhere 0
mp_buy_during_immunity 0
mp_buytime 20
mp_c4timer 40
mp_ct_default_melee weapon_knife
mp_ct_default_primary ""
mp_ct_default_secondary weapon_hkp2000
mp_death_drop_defuser 1
mp_death_drop_grenade 2
mp_death_drop_gun 1
mp_defuser_allocation 0
mp_display_kill_assists 1
mp_endmatch_votenextmap 0
mp_forcecamera 1
mp_free_armor 0
mp_freezetime 10
mp_friendlyfire 1
mp_give_player_c4 1
mp_halftime 1
mp_halftime_duration 15
mp_halftime_pausetimer 0
mp_ignore_round_win_conditions 0
mp_limitteams 0
mp_match_can_clinch 1
mp_match_end_restart 1
mp_maxmoney 8000
mp_maxrounds 16
mp_molotovusedelay 0
mp_overtime_enable 1
mp_overtime_halftime_pausetimer 0
mp_overtime_maxrounds 4
mp_overtime_startmoney 8000
mp_playercashawards 1
mp_randomspawn 0
mp_respawn_immunitytime 0
mp_respawn_on_death_ct 0
mp_respawn_on_death_t 0
mp_round_restart_delay 7
mp_roundtime 1.5
mp_roundtime_defuse 1.5
mp_roundtime_hostage 1.5
mp_solid_teammates 1
mp_starting_losses 1
mp_startmoney 800
mp_t_default_melee weapon_knife
mp_t_default_primary ""
mp_t_default_secondary weapon_glock
mp_teamcashawards 1
mp_timelimit 0
mp_weapons_allow_map_placed 1
mp_weapons_allow_zeus 1
mp_weapons_glow_on_ground 0
mp_win_panel_display_time 3
occlusion_test_async 0
spec_freeze_deathanim_time 0
spec_freeze_panel_extended_time 0
spec_freeze_time 2
spec_freeze_time_lock 2
spec_replay_enable 0
sv_allow_votes 0
sv_auto_full_alltalk_during_warmup_half_end 0
sv_coaching_enabled 1
sv_competitive_official_5v5 1
sv_damage_print_enable 0
sv_deadtalk 1
sv_hibernate_postgame_delay 300
sv_holiday_mode 0
sv_ignoregrenaderadio 0
sv_infinite_ammo 0
sv_occlude_players 1
sv_talk_enemy_dead 0
sv_talk_enemy_living 0
sv_voiceenable 1
tv_relayvoice 0
say Match is live!
say Match is live!
say Match is live!

46
cfg/warmup.cfg Normal file
View File

@ -0,0 +1,46 @@
bot_kick
bot_quota 0
mp_autokick 0
mp_autoteambalance 0
mp_buy_anywhere 0
mp_buytime 15
mp_death_drop_gun 0
mp_free_armor 0
mp_ignore_round_win_conditions 0
mp_limitteams 0
mp_radar_showall 0
mp_respawn_on_death_ct 0
mp_respawn_on_death_t 0
mp_solid_teammates 0
mp_spectators_max 20
mp_maxmoney 16000
mp_startmoney 16000
mp_timelimit 0
sv_alltalk 1
sv_auto_full_alltalk_during_warmup_half_end 0
sv_coaching_enabled 1
sv_competitive_official_5v5 1
sv_deadtalk 1
sv_full_alltalk 1
sv_grenade_trajectory 0
sv_hibernate_when_empty 0
mp_weapons_allow_typecount -1
sv_infinite_ammo 0
sv_showimpacts 0
sv_voiceenable 1
sm_cvar sv_mute_players_with_social_penalties 0
sv_mute_players_with_social_penalties 0
tv_relayvoice 1
sv_cheats 0
mp_ct_default_melee weapon_knife
mp_ct_default_secondary weapon_hkp2000
mp_ct_default_primary ""
mp_t_default_melee weapon_knife
mp_t_default_secondary weapon_glock
mp_t_default_primary
mp_warmup_start
mp_warmup_pausetimer 1
mp_warmuptime 9999
say Warmup!
say Warmup!
say Warmup!

47
db.js Normal file
View File

@ -0,0 +1,47 @@
const Database = require('better-sqlite3');
const bcrypt = require('bcrypt');
const better_sqlite_client = new Database('cspanel.db');
better_sqlite_client.exec(`
CREATE TABLE IF NOT EXISTS servers (
id INTEGER PRIMARY KEY,
serverIP TEXT NOT NULL,
serverPort INTEGER NOT NULL,
rconPassword TEXT NOT NULL
)
`);
better_sqlite_client.exec(`
CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY,
username TEXT NOT NULL UNIQUE,
password TEXT NOT NULL
)
`);
const default_username = 'cspanel';
const default_password = 'v67ic55x4ghvjfj';
// Hash the default password
const hashed_password = bcrypt.hashSync(default_password, 10);
// Check if the default user already exists
const existing_user = better_sqlite_client.prepare(`
SELECT * FROM users WHERE username = ?
`).get(default_username);
if (existing_user) {
console.log('Default user already exists');
} else {
// Insert the default user into the users table
const insert_query = better_sqlite_client.prepare(`
INSERT INTO users (username, password)
VALUES (?, ?)
`);
insert_query.run(default_username, hashed_password);
console.log('Default user created successfully.');
}
module.exports = {
better_sqlite_client
};

14
modules/middleware.js Normal file
View File

@ -0,0 +1,14 @@
function is_authenticated(req, res, next) {
if (req.session.user) {
next();
} else {
const accept_header = req.headers['accept'];
if (accept_header && accept_header.includes('text/html')) {
res.redirect('/');
} else {
res.status(401).json({ status: 401, message: 'Unauthorized' });
}
}
}
module.exports = is_authenticated;

90
modules/rcon.js Normal file
View File

@ -0,0 +1,90 @@
const Rcon = require('rcon-srcds').default;
const { better_sqlite_client } = require('../db');
class RconManager {
constructor() {
this.rcons = {};
this.details = {};
this.init();
}
async init() {
try {
const servers_query = better_sqlite_client.prepare(`
SELECT * FROM servers
`);
const servers = servers_query.all();
console.log('All servers in DB:', servers);
for (const server of servers) {
const server_id = server.id.toString();
if (server_id in this.rcons) continue;
await this.connect(server_id, server)
}
} catch (error) {
console.error('Error connecting to MongoDB:', error);
}
}
async connect(server_id, server) {
let rcon_connection = null;
rcon_connection = new Rcon({ host: server.serverIP, port: server.serverPort, timeout: 5000 });
console.log("CONNECTING RCON", server_id, server.serverIP, server.serverPort);
// Set a timeout for the authentication process
const authenticationTimeout = setTimeout(async () => {
console.error('RCON Authentication timed out', server_id);
try {
await this.disconnect_rcon(server_id); // Disconnect the RCON connection
console.log('Timed out, disconnected RCON', server_id);
} catch (error) {
console.error('Error disconnecting RCON', server_id, error);
}
}, 10000);
try {
await rcon_connection.authenticate(server.rconPassword);
clearTimeout(authenticationTimeout);
console.log('RCON Authenticated', server_id, server.serverIP, server.serverPort);
} catch (error) {
clearTimeout(authenticationTimeout);
console.error('RCON Authentication failed', server_id, error);
// Handle the authentication error here as needed.
}
this.rcons[server_id] = rcon_connection;
this.details[server_id] = {
host: server.serverIP,
port: server.serverPort,
rcon_password: server.rconPassword,
connected: rcon_connection.isConnected(),
authenticated: rcon_connection.isAuthenticated()
};
return;
}
async disconnect_rcon(server_id) {
console.log('starting disconnect', server_id)
if (!this.rcons[server_id].connected) {
return Promise.resolve();
}
this.rcons[server_id].authenticated = false;
this.rcons[server_id].connected = false;
return new Promise((resolve, reject) => {
this.rcons[server_id].connection.once('close', () => {
resolve();
});
this.rcons[server_id].connection.once('error', (e) => {
console.error('Socket error during disconnect:', e);
resolve();
});
this.rcons[server_id].connection.end(); // Close the socket gracefully
});
}
}
module.exports = new RconManager();

2409
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

24
package.json Normal file
View File

@ -0,0 +1,24 @@
{
"name": "cs2panel",
"version": "1.0.0",
"description": "RCON Web Panel to control CS2 Servers.",
"main": "index.js",
"scripts": {
"start": "nodemon app.js",
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC",
"dependencies": {
"bcrypt": "^5.1.1",
"better-sqlite3": "^8.6.0",
"ejs": "^3.1.9",
"express": "^4.18.2",
"express-session": "^1.17.3",
"mongodb": "^6.1.0",
"rcon-srcds": "^2.0.2",
"sqlite3": "^5.1.6",
"srcds-rcon": "^2.2.1"
}
}

26
public/css/navbar.css Normal file
View File

@ -0,0 +1,26 @@
.custom-navbar {
background: linear-gradient(to bottom right, #3494E6, #EC6EAD, #3494E6);
box-shadow: 0px 2px 4px rgba(0, 0, 0, 0.2);
}
.custom-navbar .navbar-brand {
color: #fff;
font-weight: bold;
}
.custom-navbar .navbar-toggler-icon {
background-color: #fff;
}
.custom-navbar .navbar-nav .nav-link {
color: #fff;
transition: color 0.3s;
}
.custom-navbar .navbar-nav .nav-link:hover {
color: #f39c12;
}
.logout-btn {
margin: 0px;
}

188
public/js/console.js Normal file
View File

@ -0,0 +1,188 @@
$(document).ready(function () {
const current_path = window.location.pathname;
function fetch_servers() {
$.ajax({
url: '/api/servers',
type: 'GET',
success: function (data) {
$('#serverList').empty();
data.servers.forEach(function (server) {
const card = `
<div class="card server-card">
<div class="card-header">
<h3 class="card-title">${server.hostname} (${server.serverIP}:${server.serverPort})</h3>
</div>
<div class="card-body">
RCON Password: <input type="password" id="rconPassword" class="rcon-password-${server.id}" value="${server.rconPassword}" class="password-mask" disabled>
<button class="btn btn-sm btn-secondary toggle-password" server-id="${server.id}" class="hide-unhide-rcon">
<i class="fa fa-eye" server-id="${server.id}" id="toggleEyeIcon-${server.id}"></i>
</button>
<p class="status connected-status ${server.connected ? 'connected' : 'disconnected'}">
RCON Connected: ${server.connected ? 'Yes' : 'No'}
</p>
<p class="status authenticated-status ${server.authenticated ? 'authenticated' : 'not-authenticated'}">
RCON Authenticated: ${server.authenticated ? 'Yes' : 'No'}
</p>
${(!server.connected || !server.authenticated) ? '<button class="btn btn-success" server-id="' + server.id + '" id="reconnect_server">Reconnect</button>' : ''}
<a href="/manage/${server.id}" class="btn btn-primary">Manage</a>
<button class="btn btn-danger" server-id='${server.id}' id="delete_server">Delete</button>
</div>
</div>
`;
$('#serverList').append(card);
});
$(".toggle-password").click((event) => {
let server_id = $(event.target).attr("server-id");
toggle_password_visibility(server_id)
});
$("#reconnect_server").click(async (element) => {
try {
const server_id = $(element.target).attr("server-id");
const response = await $.ajax({
url: '/api/reconnect-server',
type: 'POST',
data: JSON.stringify({ server_id: server_id }),
headers: {
'Content-Type': 'application/json',
},
});
if (response.status === 200) {
fetch_servers();
} else {
console.error('Server responded with a non-200 status code:', response.status);
alert('An error occurred while reconnecting to the server.');
}
} catch (error) {
console.error(error);
alert('An error occurred while reconnecting to the server.');
}
});
$("#delete_server").click(async (element) => {
const confirmed = confirm("Are you sure you want to delete this server?");
if (confirmed) {
window.server_id = $(element.target).attr("server-id");
await send_post_request("/api/delete-server");
fetch_servers();
}
});
},
error: function (error) {
console.error(error);
alert('An error occurred while fetching servers.');
},
});
}
if (current_path == "/servers") {
fetch_servers();
}
function toggle_password_visibility(server_id) {
const password_field = document.getElementsByClassName("rcon-password-" + server_id)[0]
const eye_icon = document.getElementById(`toggleEyeIcon-${server_id}`);
if (password_field.type === 'password') {
password_field.type = 'text';
eye_icon.classList.remove('fa-eye');
eye_icon.classList.add('fa-eye-slash');
} else {
password_field.type = 'password';
eye_icon.classList.remove('fa-eye-slash');
eye_icon.classList.add('fa-eye');
}
}
async function send_post_request(apiEndpoint, data = {}) {
try {
data.server_id = window.server_id
const response = await fetch(apiEndpoint, {
method: 'POST',
body: JSON.stringify(data),
headers: {
'Content-Type': 'application/json',
},
});
if (response.ok) {
const data = await response.json();
console.log(data.message)
alert(data.message);
} else {
alert('Failed to perform the action');
}
} catch (error) {
console.error('Error:', error);
alert('An error occurred');
}
}
$('#pause_game').on('click', function () {
send_post_request('/api/pause');
});
$('#unpause_game').on('click', function () {
send_post_request('/api/unpause');
});
$('#restart_game').on('click', function () {
send_post_request('/api/restart');
});
$('#start_warmup').on('click', function () {
send_post_request('/api/start-warmup');
});
$('#knife_start').on('click', function () {
send_post_request('/api/start-knife');
});
$('#swap_team').on('click', function () {
send_post_request('/api/swap-team');
});
$('#go_live').on('click', function () {
send_post_request('/api/go-live');
});
$('#rconInputBtn').on('click', function () {
let data = {
command: $('#rconInput').val()
};
send_post_request('/api/rcon', data);
$('#rconInput').val('');
});
$('#list_backups').on('click', function () {
send_post_request('/api/list-backups');
});
$('#restore_latest_backup').on('click', function () {
send_post_request('/api/restore-latest-backup');
});
$('#restore_backup').on('click', function () {
const round_number = prompt('Enter round number to restore:');
if (round_number !== null && round_number.trim() !== '') {
const round_number_value = parseInt(round_number);
if (!isNaN(round_number_value)) {
send_post_request('/api/restore-round', { round_number: round_number_value });
} else {
alert('Invalid round number. Please enter a valid number.');
}
} else {
alert('Round number cannot be empty. Please enter a valid number.');
}
});
$('#server_setup_form').on('submit', async function (event) {
event.preventDefault();
const data = {
team1: $('#team1').val(),
team2: $('#team2').val(),
selectedMap: $('#selectedMap').val(),
game_mode: $('#game_mode').val(),
server_id: window.server_id
};
send_post_request('/api/setup-game', data);
});
});

38
routes/auth.js Normal file
View File

@ -0,0 +1,38 @@
const express = require('express');
const router = express.Router();
const bcrypt = require('bcrypt');
const { better_sqlite_client } = require('../db');
router.post('/auth/login', (req, res) => {
const { username, password } = req.body;
const query = better_sqlite_client.prepare('SELECT * FROM users WHERE username = ?');
const user = query.get(username);
if (user) {
bcrypt.compare(password, user.password, (err, result) => {
if (result) {
req.session.user = user;
res.status(200).json({ status: 200, message: 'Login successful' });
} else {
res.status(200).json({ status: 401, message: 'Invalid credentials' });
}
});
} else {
res.status(401).json({ status: 401, message: 'Invalid credentials' });
}
});
router.post('/auth/logout', (req, res) => {
req.session.destroy((err) => {
if (err) {
console.error(err);
res.status(500).json({ status: 500, message: 'Logout failed' });
} else {
res.redirect('/');
}
});
});
module.exports = router;

259
routes/game.js Normal file
View File

@ -0,0 +1,259 @@
const express = require('express');
const router = express.Router();
const readline = require('readline');
const fs = require('fs');
const rcon = require("../modules/rcon");
const is_authenticated = require("../modules/middleware");
const ALLOWED_STEAM_IDS = ['76561198154367261']
router.post('/api/setup-game', is_authenticated, async (req, res) => {
try {
const server_id = req.body.server_id;
const team1 = req.body.team1;
const team2 = req.body.team2;
const selected_map = req.body.selectedMap;
const game_mode = req.body.game_mode.toString();
rcon.rcons[server_id].execute(`mp_teamname_1 "${team1}"`);
rcon.rcons[server_id].execute(`mp_teamname_2 "${team2}"`);
rcon.rcons[server_id].execute(`game_mode ${game_mode}`);
if (game_mode == "1") {
execute_cfg_on_server(server_id, './cfg/live.cfg');
} else if (game_mode == "2") {
execute_cfg_on_server(server_id, './cfg/live_wingman.cfg');
}
rcon.rcons[server_id].execute(`mp_warmup_pausetimer 1`);
rcon.rcons[server_id].execute(`map ${selected_map}`);
// Adding 1 second delay in executing warmup.cfg to make it effective after map has been changed.
setTimeout(() => {
execute_cfg_on_server(server_id, './cfg/warmup.cfg');
}, 1000)
return res.status(200).json({ message: 'Game Created!' });
} catch (error) {
console.log(error);
res.status(500).json({ error: 'Internal server error' });
}
});
router.post('/api/restart', is_authenticated, async (req, res) => {
try {
const server_id = req.body.server_id;
rcon.rcons[server_id].execute('mp_restartgame 1');
return res.status(200).json({ message: 'Game restarted' });
} catch (error) {
console.log(error);
res.status(500).json({ error: 'Internal server error' });
}
});
router.post('/api/start-warmup', is_authenticated, (req, res) => {
try {
const server_id = req.body.server_id;
rcon.rcons[server_id].execute('mp_restartgame 1');
execute_cfg_on_server(server_id, './cfg/warmup.cfg');
return res.status(200).json({ message: 'Warmup started!' });
} catch (error) {
console.log(error);
res.status(500).json({ error: 'Internal server error' });
}
});
router.post('/api/start-knife', is_authenticated, async (req, res) => {
try {
const server_id = req.body.server_id;
rcon.rcons[server_id].execute('mp_warmup_end');
rcon.rcons[server_id].execute('mp_restartgame 1');
execute_cfg_on_server(server_id, './cfg/knife.cfg');
return res.status(200).json({ message: 'Knife started!' });
} catch (error) {
console.log(error);
res.status(500).json({ error: 'Internal server error' });
}
});
router.post('/api/swap-team', is_authenticated, async (req, res) => {
try {
const server_id = req.body.server_id;
rcon.rcons[server_id].execute('mp_swapteams');
return res.status(200).json({ message: 'Teams Swapped!' });
} catch (error) {
res.status(500).json({ error: 'Internal server error' });
}
});
router.post('/api/go-live', is_authenticated, async (req, res) => {
try {
const server_id = req.body.server_id;
rcon.rcons[server_id].execute('mp_warmup_end');
const response = await rcon.rcons[server_id].execute('game_mode');
const game_mode = response.split("=")[1].trim().toString();
if (game_mode == "1") {
console.log("Executing live.cfg")
execute_cfg_on_server(server_id, './cfg/live.cfg');
} else if (game_mode == "2") {
console.log("Executing live_wingman.cfg")
execute_cfg_on_server(server_id, './cfg/live_wingman.cfg');
}
rcon.rcons[server_id].execute('mp_restartgame 1');
return res.status(200).json({ message: 'Match is live!!' });
} catch (error) {
console.log(error);
res.status(500).json({ error: 'Internal server error' });
}
});
// List Round Backups API
router.post('/api/list-backups', is_authenticated, async (req, res) => {
try {
const server_id = req.body.server_id;
const response = await rcon.rcons[server_id].execute('mp_backup_restore_list_files');
console.log('Server response:', response);
return res.status(200).json({ message: response });
} catch (error) {
console.log(error)
res.status(500).json({ error: 'Internal server error' });
}
});
// Restore Round API
router.post('/api/restore-round', is_authenticated, async (req, res) => {
try {
const server_id = req.body.server_id;
let round_number = req.body.round_number.toString()
if (round_number.length == 1) {
round_number = "0" + round_number;
}
console.log(`SENDING mp_backup_restore_load_file backup_round${round_number}.txt`)
rcon.rcons[server_id].execute(`mp_backup_restore_load_file backup_round${round_number}.txt`);
rcon.rcons[server_id].execute('mp_pause_match');
return res.status(200).json({ message: 'Round Restored!' });
} catch (error) {
res.status(500).json({ error: 'Internal server error' });
}
});
router.post('/api/restore-latest-backup', is_authenticated, async (req, res) => {
try {
const server_id = req.body.server_id;
const response = await rcon.rcons[server_id].execute('mp_backup_round_file_last');
const last_round_file = response.split("=")[1].trim().toString();
if (last_round_file.includes('.txt')) {
rcon.rcons[server_id].execute(`mp_backup_restore_load_file ${last_round_file}`);
rcon.rcons[server_id].execute('mp_pause_match');
return res.status(200).json({ message: `Latest Round Restored! (${last_round_file})` });
} else {
return res.status(200).json({ message: 'No latest backup found!' });
}
} catch (error) {
console.log(error);
res.status(500).json({ error: 'Internal server error' });
}
});
// Pause Game API
router.post('/api/pause', is_authenticated, async (req, res) => {
try {
const server_id = req.body.server_id;
rcon.rcons[server_id].execute('mp_pause_match');
return res.status(200).json({ message: 'Game paused' });
} catch (error) {
res.status(500).json({ error: 'Internal server error' });
}
});
// Unpause Game API
router.post('/api/unpause', is_authenticated, async (req, res) => {
try {
const server_id = req.body.server_id;
rcon.rcons[server_id].execute('mp_unpause_match');
return res.status(200).json({ message: 'Game unpaused' });
} catch (error) {
res.status(500).json({ error: 'Internal server error' });
}
});
router.post('/api/rcon', is_authenticated, async (req, res) => {
try {
const server_id = req.body.server_id;
const command = req.body.command;
// Wrap the await call in a Promise and add a timeout
const executePromise = new Promise(async (resolve, reject) => {
try {
const response = await Promise.race([
rcon.rcons[server_id].execute(command),
new Promise((resolve, reject) => {
setTimeout(() => {
resolve({ error: 'Command execution timed out' });
}, 1000); // 1 seconds timeout
}),
]);
resolve(response);
} catch (error) {
reject(error);
}
});
// Wait for the wrapped Promise to resolve
const response = await executePromise;
console.log(response)
// Check if the result is an error or the actual response
if (response.error) {
return res.status(200).json({ message: 'Command sent!' });
}
return res.status(200).json({ message: 'Command sent! Response received:\n' + response.toString() });
} catch (error) {
res.status(500).json({ error: 'Internal server error' });
}
});
function check_whitelisted_players() {
rcon.rcons[server_id].execute('status_json')
.then((response) => {
console.log(response)
const server_status = JSON.parse(response)
const players = server_status['server']['clients']
for (var i = 0; i < players.length; i++) {
let player = players[i]
if (!player.bot && player.steamid64.includes('7656') && !ALLOWED_STEAM_IDS.includes(player.steamid64)) {
console.log(`kick ${player.name}`)
rcon.rcons[server_id].execute(`kick ${player.name}`);
}
}
return;
})
.catch(console.error);
}
function execute_cfg_on_server(server_id, cfg_path) {
const fileStream = fs.createReadStream(cfg_path, 'utf-8');
const rl = readline.createInterface({
input: fileStream,
terminal: false
});
rl.on('line', (line) => {
try {
console.log(`Line from file: ${line}`);
rcon.rcons[server_id].execute(line);
} catch (error) {
console.log(`Error in executing line ${line}, Error: ${error}`);
}
});
rl.on('close', () => {
console.log('File reading completed.');
});
}
module.exports = {
router
};

137
routes/server.js Normal file
View File

@ -0,0 +1,137 @@
// In your routes/server.js (create this file)
const express = require('express');
const router = express.Router();
const { better_sqlite_client } = require('../db');
const rcon = require("../modules/rcon");
const is_authenticated = require("../modules/middleware");
router.get('/add-server', is_authenticated, (req, res) => {
res.render('add-server');
});
router.get('/servers', is_authenticated, (req, res) => {
res.render('servers');
});
router.get('/manage/:server_id', is_authenticated, async (req, res) => {
try {
const server_id = req.params.server_id;
const servers_query = better_sqlite_client.prepare(`
SELECT * FROM servers WHERE id = ?
`);
const server = servers_query.get(server_id);
if (!server) {
return res.status(404).send('Server not found');
}
const response = await rcon.rcons[server_id].execute('hostname');
const hostname = response.toString().split("=")[1].trim();
const host = rcon.details[server_id].host;
const port = rcon.details[server_id].port;
res.render('manage', { server_id, hostname, host, port });
} catch (error) {
console.error(error);
return res.status(404).send('Internal Server Error!');
}
});
router.post('/api/add-server', is_authenticated, async (req, res) => {
const { server_ip, server_port, rcon_password } = req.body;
try {
const insert_query = better_sqlite_client.prepare(`
INSERT INTO servers (serverIP, serverPort, rconPassword) VALUES (?, ?, ?)
`);
const result = insert_query.run(server_ip, server_port, rcon_password);
if (result.changes > 0) {
res.status(201).json({ message: 'Server added successfully' });
rcon.init();
} else {
res.status(500).json({ error: 'Failed to add the server' });
}
} catch (error) {
console.error(error);
res.status(500).json({ error: 'Internal server error' });
}
});
router.get('/api/servers', is_authenticated, async (req, res) => {
try {
const servers_query = better_sqlite_client.prepare(`
SELECT * FROM servers
`);
const servers = servers_query.all();
for (var i = 0; i < servers.length; i++) {
const server = servers[i];
const server_id = server.id.toString();
let hostname = "-";
if (server_id in rcon.rcons) {
servers[i].connected = rcon.rcons[server_id].isConnected();
servers[i].authenticated = rcon.rcons[server_id].isAuthenticated();
if (servers[i].connected && servers[i].authenticated) {
const response = await rcon.rcons[server_id].execute('hostname');
hostname = response.toString().split("=")[1].trim();
}
} else {
servers[i].connected = false;
servers[i].authenticated = false;
}
servers[i].hostname = hostname;
}
res.json({ servers });
} catch (error) {
console.error(error);
res.status(500).json({ error: 'An error occurred while fetching servers.' });
}
});
router.post('/api/reconnect-server', is_authenticated, async (req, res) => {
try {
const server_id = req.body.server_id;
const server_query = better_sqlite_client.prepare(`
SELECT * FROM servers WHERE id = ?
`);
const server = server_query.get(server_id);
if (!server) {
return res.status(404).json({ error: 'Server not found' });
}
// Reconnect using RCON
await rcon.connect(server_id, server);
res.status(200).json({ status: 200 });
} catch (error) {
console.error(error);
res.status(500).json({ error: 'An error occurred while reconnecting to the server.' });
}
});
router.post('/api/delete-server', is_authenticated, async (req, res) => {
try {
const server_id = req.body.server_id;
const delete_server_query = better_sqlite_client.prepare('DELETE FROM servers WHERE id = ?');
const result = delete_server_query.run(server_id);
if (result.changes > 0) {
res.status(200).json({ status: 200, message: 'Server deleted successfully' });
} else {
res.status(404).json({ status: 404, message: 'Server not found' });
}
} catch (error) {
console.error(error);
res.status(500).json({ error: 'An error occurred while deleting the server.' });
}
});
module.exports = router;

172
views/add-server.ejs Normal file
View File

@ -0,0 +1,172 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Add a Server</title>
<!-- Include Bootstrap CSS -->
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
<!-- Custom CSS -->
<style>
body {
background: linear-gradient(to bottom right, #3494E6, #EC6EAD);
color: #fff;
height: 100vh;
}
.card {
border: none;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
background-color: #fff;
}
.card-header {
background-color: #007BFF;
color: #fff;
}
.card-title {
font-size: 24px;
}
.form-control {
border: 1px solid #ccc;
border-radius: 5px;
padding: 10px;
}
.btn-primary {
background-color: #007BFF;
border: none;
}
.btn-primary:hover {
background-color: #0056b3;
}
.btn-secondary {
background-color: #6c757d;
border: none;
}
.btn-secondary:hover {
background-color: #5a6268;
}
.toast {
background-color: #28a745;
color: #fff;
}
/* Custom styling for form elements */
.form-container {
margin-top: 20px;
}
.form-label {
font-weight: bold;
}
.btn-container {
margin-top: 20px;
}
/* Responsive styling for small screens */
@media (max-width: 576px) {
.form-container {
margin-top: 10px;
}
.btn-container {
margin-top: 10px;
}
}
</style>
<link rel="stylesheet" href="/css/navbar.css">
</head>
<body>
<%- include('partials/navbar') %>
<div class="container">
<div class="row">
<div class="col-md-6 offset-md-3 form-container">
<div class="card">
<div class="card-header">
<h3 class="card-title">Add a Server</h3>
</div>
<div class="card-body">
<form>
<div class="mb-3">
<label for="serverIP" class="form-label">Server IP:</label>
<input type="text" id="serverIP" class="form-control" required>
</div>
<div class="mb-3">
<label for="serverPort" class="form-label">Server Port:</label>
<input type="text" id="serverPort" class="form-control" required>
</div>
<div class="mb-3">
<label for="rconPassword" class="form-label">RCON Password:</label>
<input type="password" id="rconPassword" class="form-control" required>
</div>
<div class="btn-container">
<button id="submitButton" class="btn btn-primary">Submit</button>
<button type="reset" class="btn btn-secondary">Reset</button>
</div>
</form>
</div>
</div>
</div>
</div>
</div>
<!-- Include Bootstrap JS and jQuery -->
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.min.js"></script>
<script>
$(document).ready(function () {
// Event listener for the submit button
$('#submitButton').on('click', function (e) {
e.preventDefault(); // Prevent the default form submission
// Get input values
const serverIP = $('#serverIP').val();
const serverPort = $('#serverPort').val();
const rconPassword = $('#rconPassword').val();
if (!serverIP || !serverPort || !rconPassword) {
alert('All fields are required.');
return; // Do not proceed with the request
}
// Create server data object
const serverData = {
serverIP,
serverPort,
rconPassword,
};
// Make an API POST request to add the server
$.ajax({
url: '/api/add-server', // Adjust the API endpoint
type: 'POST',
data: JSON.stringify(serverData),
contentType: 'application/json',
success: function (data) {
// Display a success toast message
alert('Server added successfully!')
$('#successToast').toast('show');
$('#serverIP, #serverPort, #rconPassword').val('');
},
error: function (error) {
console.error(error);
alert('An error occurred while adding the server.');
},
});
});
});
</script>
</body>
</html>

165
views/login.ejs Normal file
View File

@ -0,0 +1,165 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Login</title>
<!-- Include Bootstrap CSS -->
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
<!-- Custom CSS -->
<style>
body {
background: linear-gradient(to bottom right, #3494E6, #EC6EAD);
color: #fff;
height: 100vh;
}
.card {
border: none;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
background-color: #fff;
}
.card-header {
background-color: #007BFF;
color: #fff;
}
.card-title {
font-size: 24px;
}
.form-control {
border: 1px solid #ccc;
border-radius: 5px;
padding: 10px;
}
.btn-primary {
background-color: #007BFF;
border: none;
}
.btn-primary:hover {
background-color: #0056b3;
}
.btn-secondary {
background-color: #6c757d;
border: none;
}
.btn-secondary:hover {
background-color: #5a6268;
}
.toast {
background-color: #28a745;
color: #fff;
}
/* Custom styling for form elements */
.form-container {
margin-top: 20px;
}
.form-label {
font-weight: bold;
}
.btn-container {
margin-top: 20px;
}
/* Responsive styling for small screens */
@media (max-width: 576px) {
.form-container {
margin-top: 10px;
}
.btn-container {
margin-top: 10px;
}
}
</style>
</head>
<body>
<div class="container">
<div class="row">
<div class="col-md-6 offset-md-3 form-container">
<div class="card">
<div class="card-header">
<h3 class="card-title">Login</h3>
</div>
<div class="card-body">
<form>
<div class="mb-3">
<label for="username" class="form-label">Username:</label>
<input type="text" id="username" class="form-control" required>
</div>
<div class="mb-3">
<label for="password" class="form-label">Password:</label>
<input type="password" id="password" class="form-control" required>
</div>
<div class="btn-container">
<button id="login_btn" class="btn btn-primary">Submit</button>
<button type="reset" class="btn btn-secondary">Reset</button>
</div>
</form>
</div>
</div>
</div>
</div>
</div>
<!-- Include Bootstrap JS and jQuery -->
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.min.js"></script>
<script>
$(document).ready(function () {
// Event listener for the submit button
$('#login_btn').on('click', function (e) {
e.preventDefault(); // Prevent the default form submission
// Get input values
const username = $('#username').val();
const password = $('#password').val();
if (!username || !password) {
alert('All fields are required.');
return;
}
// Create server data object
const user_data = {
username,
password,
};
// Make an API POST request to add the server
$.ajax({
url: '/auth/login', // Adjust the API endpoint
type: 'POST',
data: JSON.stringify(user_data),
contentType: 'application/json',
success: function (data) {
if (data.status == 200) {
window.location = "/servers";
} else if (data.status == 401) {
alert('Invalid Credentials')
}
},
error: function (error) {
console.error(error);
alert('Login Failed');
},
});
});
});
</script>
</body>
</html>

264
views/manage.ejs Normal file
View File

@ -0,0 +1,264 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>CSGO/CS2 Server Control Panel</title>
<!-- Add Bootstrap CSS CDN link -->
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
<!-- Custom CSS -->
<style>
body {
background: linear-gradient(to bottom right, #3494E6, #EC6EAD);
font-family: Arial, sans-serif;
text-align: center;
color: #333;
height: 100vh; /* Set body height to 100% of viewport height */
margin: 0; /* Remove margin to fill the entire viewport */
flex-direction: column;
justify-content: center;
align-items: center;
}
h1 {
color: #fff;
}
form.server-setup-form {
max-width: 400px;
margin: 0 auto;
padding: 20px;
border-radius: 10px;
background-color: rgba(255, 255, 255, 0.9); /* Semi-transparent white background */
}
label {
font-size: 16px;
}
input[type="text"], select {
padding: 10px;
font-size: 16px;
width: 100%;
margin-bottom: 10px;
border: 1px solid #ccc;
border-radius: 5px;
}
button {
margin: 10px;
padding: 10px 20px;
font-size: 16px;
cursor: pointer;
}
/* Custom CSS for input and button side by side */
.input-group {
display: flex;
align-items: center;
justify-content: center;
margin-top: 10px;
}
.input-group input[type="text"] {
width: 70%;
margin-bottom: 0px;
margin-right: 5px;
}
.input-group button {
width: 30%;
}
/* Custom CSS for action buttons */
.action-buttons button {
margin: 5px;
}
</style>
<link rel="stylesheet" href="/css/navbar.css">
</head>
<body>
<%- include('partials/navbar') %>
<div class="container">
<div class="row">
<div class="col-md-12">
<h1 class="mb-4"><%= hostname %> (<%= host %>:<%= port %>)</h1>
<form id="server_setup_form" class="server-setup-form">
<div class="mb-3">
<label for="team1" class="form-label">Team 1:</label>
<input type="text" id="team1" name="team1" class="form-control" required>
</div>
<div class="mb-3">
<label for="team2" class="form-label">Team 2:</label>
<input type="text" id="team2" name="team2" class="form-control" required>
</div>
<div class="mb-3">
<label for="map" class="form-label">Map:</label>
<select id="selectedMap" name="map" class="form-select">
<option value="de_inferno">de_inferno</option>
<option value="de_mirage">de_mirage</option>
<option value="de_dust2">de_dust2</option>
<option value="de_overpass">de_overpass</option>
<option value="de_ancient">de_ancient</option>
<option value="de_anubis">de_anubis</option>
<option value="de_nuke">de_nuke</option>
<option value="de_vertigo">de_vertigo</option>
</select>
</div>
<div class="mb-3">
<label for="gamemode" class="form-label">Gamemode:</label>
<select id="game_mode" name="gamemode" class="form-select">
<option value="1">Competitive</option>
<option value="2">Wingman</option>
</select>
</div>
<div class="mb-3">
<button type="submit" name="action" value="pause" class="btn btn-primary">
<svg viewBox="0 0 1024 1024" width="20" height="20" class="icon" version="1.1" xmlns="http://www.w3.org/2000/svg" fill="#ffffff"
stroke="#ffffff">
<g id="SVGRepo_bgCarrier" stroke-width="0"></g>
<g id="SVGRepo_tracerCarrier" stroke-linecap="round" stroke-linejoin="round"></g>
<g id="SVGRepo_iconCarrier">
<path
d="M511.9 135.3L182.7 323.7v376.8l329.1 188.4L841 700.5V323.7L511.9 135.3z m256 522.8l-256 146.5-256-146.5v-292l256-146.5 256 146.5v292z"
fill="#ffffff"></path>
<path
d="M365.6 512.1c0 80.8 65.5 146.3 146.3 146.3 80.8 0 146.3-65.5 146.3-146.3 0-80.8-65.5-146.3-146.3-146.3-80.8 0-146.3 65.5-146.3 146.3z m219.4 0c0 40.3-32.8 73.1-73.1 73.1s-73.1-32.8-73.1-73.1 32.8-73.1 73.1-73.1 73.1 32.8 73.1 73.1z"
fill="#ffffff"></path>
</g>
</svg>
Setup Game</button>
</div>
</form>
<div class="mb-3 action-buttons">
<button type="submit" id="pause_game" name="action" value="pause" class="btn btn-primary">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-pause" viewBox="0 0 16 16">
<path d="M5 0a1 1 0 0 1 1 1v14a1 1 0 0 1-2 0V1a1 1 0 0 1 1-1z"/>
<path d="M10 0a1 1 0 0 1 1 1v14a1 1 0 0 1-2 0V1a1 1 0 0 1 1-1z"/>
</svg> Pause Game
</button>
<button type="submit" id="unpause_game" name="action" value="unpause" class="btn btn-success">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-play" viewBox="0 0 16 16">
<path d="M11.742 8.47 5.975 12.764a.5.5 0 0 1-.31.108H4a1 1 0 0 1-1-1V3a1 1 0 0 1 1-1h1.665a.5.5 0 0 1 .31.108l5.767 4.294a.5.5 0 0 1 0 .816z"/>
</svg> Unpause Game
</button>
<button type="submit" id="restart_game" name="action" value="restart" class="btn btn-danger">
<svg viewBox="0 0 24 24" width="16" height="16" fill="none" xmlns="http://www.w3.org/2000/svg" stroke="#ffffff">
<g id="SVGRepo_bgCarrier" stroke-width="0"></g>
<g id="SVGRepo_tracerCarrier" stroke-linecap="round" stroke-linejoin="round"></g>
<g id="SVGRepo_iconCarrier">
<path
d="M12 2.99988C16.9706 2.99988 21 7.02931 21 11.9999C21 16.9704 16.9706 20.9999 12 20.9999C7.02944 20.9999 3 16.9704 3 11.9999C3 9.17261 4.30367 6.64983 6.34267 4.99988"
stroke="#ffffff" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"></path>
<path d="M3 4.49988H7V8.49988" stroke="#ffffff" stroke-linecap="round" stroke-linejoin="round" stroke-width="2">
</path>
</g>
</svg>
Restart Game
</button>
<button type="submit" id="list_backups" name="action" class="btn btn-primary">
<svg viewBox="0 0 24 24" width="16" height="16" fill="none" xmlns="http://www.w3.org/2000/svg" stroke="#ffffff">
<g id="SVGRepo_bgCarrier" stroke-width="0"></g>
<g id="SVGRepo_tracerCarrier" stroke-linecap="round" stroke-linejoin="round"></g>
<g id="SVGRepo_iconCarrier">
<path
d="M8 6.00067L21 6.00139M8 12.0007L21 12.0015M8 18.0007L21 18.0015M3.5 6H3.51M3.5 12H3.51M3.5 18H3.51M4 6C4 6.27614 3.77614 6.5 3.5 6.5C3.22386 6.5 3 6.27614 3 6C3 5.72386 3.22386 5.5 3.5 5.5C3.77614 5.5 4 5.72386 4 6ZM4 12C4 12.2761 3.77614 12.5 3.5 12.5C3.22386 12.5 3 12.2761 3 12C3 11.7239 3.22386 11.5 3.5 11.5C3.77614 11.5 4 11.7239 4 12ZM4 18C4 18.2761 3.77614 18.5 3.5 18.5C3.22386 18.5 3 18.2761 3 18C3 17.7239 3.22386 17.5 3.5 17.5C3.77614 17.5 4 17.7239 4 18Z"
stroke="#ffffff" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path>
</g>
</svg> List Backups
</button>
<button id="restore_backup" class="btn btn-primary">
<svg fill="#ffffff" height="16" width="16" version="1.1" id="XMLID_107_" xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 24 24" xml:space="preserve" stroke="#ffffff">
<g id="SVGRepo_bgCarrier" stroke-width="0"></g>
<g id="SVGRepo_tracerCarrier" stroke-linecap="round" stroke-linejoin="round"></g>
<g id="SVGRepo_iconCarrier">
<g id="folder-cycle">
<path d="M24,24h-5v-2h3V10H2v12h2v2H0V0h9.5l3,4H24V24z M2,8h20V6H11.5l-3-4H2V8z"></path>
<path
d="M18,12v5h-5l1.8-1.8C14.1,14.5,13.1,14,12,14c-2.2,0-4,1.8-4,4H6c0-3.3,2.7-6,6-6c1.6,0,3.1,0.7,4.2,1.8L18,12z M16,18 c0,2.2-1.8,4-4,4c-1.1,0-2.1-0.5-2.8-1.2L11,19H6v5l1.8-1.8C8.9,23.3,10.4,24,12,24c3.3,0,6-2.7,6-6H16z">
</path>
</g>
</g>
</svg> Restore Backup
</button>
<button id="restore_latest_backup" class="btn btn-primary">
<svg fill="#ffffff" height="16" width="16" version="1.1" id="XMLID_107_" xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 24 24" xml:space="preserve" stroke="#ffffff">
<g id="SVGRepo_bgCarrier" stroke-width="0"></g>
<g id="SVGRepo_tracerCarrier" stroke-linecap="round" stroke-linejoin="round"></g>
<g id="SVGRepo_iconCarrier">
<g id="folder-cycle">
<path d="M24,24h-5v-2h3V10H2v12h2v2H0V0h9.5l3,4H24V24z M2,8h20V6H11.5l-3-4H2V8z"></path>
<path
d="M18,12v5h-5l1.8-1.8C14.1,14.5,13.1,14,12,14c-2.2,0-4,1.8-4,4H6c0-3.3,2.7-6,6-6c1.6,0,3.1,0.7,4.2,1.8L18,12z M16,18 c0,2.2-1.8,4-4,4c-1.1,0-2.1-0.5-2.8-1.2L11,19H6v5l1.8-1.8C8.9,23.3,10.4,24,12,24c3.3,0,6-2.7,6-6H16z">
</path>
</g>
</g>
</svg> Restore Latest Round
</button>
<button type="submit" id="start_warmup" name="action" class="btn btn-success">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-play-fill" viewBox="0 0 16 16">
<path d="M11.742 8.47 5.975 12.764a.5.5 0 0 1-.31.108H4a1 1 0 0 1-1-1V3a1 1 0 0 1 1-1h1.665a.5.5 0 0 1 .31.108l5.767 4.294a.5.5 0 0 1 0 .816z"/>
</svg> Start Warmup
</button>
<button type="submit" id="knife_start" name="action" class="btn btn-success">
<svg height="16" width="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" stroke="#ffffff">
<g id="SVGRepo_bgCarrier" stroke-width="0"></g>
<g id="SVGRepo_tracerCarrier" stroke-linecap="round" stroke-linejoin="round"></g>
<g id="SVGRepo_iconCarrier">
<path
d="M14.7245 11.2754L16 12.4999L10.0129 17.8218C8.05054 19.5661 5.60528 20.6743 3 20.9999L3.79443 19.5435C4.6198 18.0303 5.03249 17.2737 5.50651 16.5582C5.92771 15.9224 6.38492 15.3113 6.87592 14.7278C7.42848 14.071 8.0378 13.4615 9.25644 12.2426L12 9.49822M11.5 8.99787L17.4497 3.04989C18.0698 2.42996 19.0281 2.3017 19.7894 2.73674C20.9027 3.37291 21.1064 4.89355 20.1997 5.80024L19.8415 6.15847C19.6228 6.3771 19.3263 6.49992 19.0171 6.49992H18L16 8.49992V8.67444C16 9.16362 16 9.40821 15.9447 9.63839C15.8957 9.84246 15.8149 10.0375 15.7053 10.2165C15.5816 10.4183 15.4086 10.5913 15.0627 10.9372L14.2501 11.7498L11.5 8.99787Z"
stroke="#ffffff" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path>
</g>
</svg> Start Knife Round
</button>
<button type="submit" id="swap_team" name="action" class="btn btn-success">
<svg viewBox="0 0 24 24" fill="none" width="16" height="16" xmlns="http://www.w3.org/2000/svg" stroke="#ffffff">
<g id="SVGRepo_bgCarrier" stroke-width="0"></g>
<g id="SVGRepo_tracerCarrier" stroke-linecap="round" stroke-linejoin="round"></g>
<g id="SVGRepo_iconCarrier">
<path fill-rule="evenodd" clip-rule="evenodd"
d="M16 3.93a.75.75 0 0 1 1.177-.617l4.432 3.069a.75.75 0 0 1 0 1.233l-4.432 3.069A.75.75 0 0 1 16 10.067V8H4a1 1 0 0 1 0-2h12V3.93zm-9.177 9.383A.75.75 0 0 1 8 13.93V16h12a1 1 0 1 1 0 2H8v2.067a.75.75 0 0 1-1.177.617l-4.432-3.069a.75.75 0 0 1 0-1.233l4.432-3.069z"
fill="#ffffff"></path>
</g>
</svg> Swap Teams
</button>
<button type="submit" id="go_live" name="action" class="btn btn-success">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-play-fill" viewBox="0 0 16 16">
<path d="M11.742 8.47 5.975 12.764a.5.5 0 0 1-.31.108H4a1 1 0 0 1-1-1V3a1 1 0 0 1 1-1h1.665a.5.5 0 0 1 .31.108l5.767 4.294a.5.5 0 0 1 0 .816z"/>
</svg> Go Live
</button>
</div>
<div class="input-group mb-3">
<input type="text" id="rconInput" name="sayInput" class="form-control" placeholder="Send RCON command to server" required style="flex: 2;">
<button type="submit" id="rconInputBtn" class="btn btn-primary" style="flex: 1;">
Send
<svg viewBox="0 0 24 24" fill="none" width="16" height="16" xmlns="http://www.w3.org/2000/svg" stroke="#ffffff">
<g id="SVGRepo_bgCarrier" stroke-width="0"></g>
<g id="SVGRepo_tracerCarrier" stroke-linecap="round" stroke-linejoin="round"></g>
<g id="SVGRepo_iconCarrier">
<path
d="M11.5003 12H5.41872M5.24634 12.7972L4.24158 15.7986C3.69128 17.4424 3.41613 18.2643 3.61359 18.7704C3.78506 19.21 4.15335 19.5432 4.6078 19.6701C5.13111 19.8161 5.92151 19.4604 7.50231 18.7491L17.6367 14.1886C19.1797 13.4942 19.9512 13.1471 20.1896 12.6648C20.3968 12.2458 20.3968 11.7541 20.1896 11.3351C19.9512 10.8529 19.1797 10.5057 17.6367 9.81135L7.48483 5.24303C5.90879 4.53382 5.12078 4.17921 4.59799 4.32468C4.14397 4.45101 3.77572 4.78336 3.60365 5.22209C3.40551 5.72728 3.67772 6.54741 4.22215 8.18767L5.24829 11.2793C5.34179 11.561 5.38855 11.7019 5.407 11.8459C5.42338 11.9738 5.42321 12.1032 5.40651 12.231C5.38768 12.375 5.34057 12.5157 5.24634 12.7972Z"
stroke="#ffffff" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path>
</g>
</svg>
</button>
</div>
</div>
</div>
</div>
<!-- Include Bootstrap JS and jQuery CDN links -->
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.min.js"></script>
<script src="/js/console.js"></script>
<script>
window.server_id = '<%= server_id %>';
</script>
</body>
</html>

23
views/partials/navbar.ejs Normal file
View File

@ -0,0 +1,23 @@
<nav class="navbar navbar-expand-lg custom-navbar">
<div class="container">
<a class="navbar-brand" href="/servers">CS2Panel</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav" aria-controls="navbarNav" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarNav">
<ul class="navbar-nav">
<li class="nav-item">
<a class="nav-link" href="/servers">All Servers</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/add-server">Add Server</a>
</li>
</ul>
</div>
<div class="ml-auto">
<form action="/auth/logout" method="post">
<button type="submit" class="btn logout-btn text-white">Logout</button>
</form>
</div>
</div>
</nav>

121
views/servers.ejs Normal file
View File

@ -0,0 +1,121 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Server Management</title>
<!-- Include Bootstrap CSS -->
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
<!-- Custom CSS -->
<style>
body {
background: linear-gradient(to bottom right, #3494E6, #EC6EAD);
color: #fff;
height: 100vh;
}
.server-list-container {
background-color: rgba(255, 255, 255, 0.1); /* Semi-transparent white background for better readability */
padding: 20px;
border-radius: 10px; /* Add rounded corners */
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2); /* Add a subtle shadow */
}
/* Responsive styling for small screens */
@media (max-width: 576px) {
/* Adjust padding for small screens */
.server-list-container {
padding: 10px;
}
}
.card {
border: none;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
background-color: #fff;
margin-bottom: 20px;
}
.card-header {
background-color: #007BFF;
color: #fff;
}
.card-title {
font-size: 24px;
}
.btn-primary {
background-color: #007BFF;
border: none;
}
.btn-primary:hover {
background-color: #0056b3;
}
.btn-danger {
background-color: #dc3545;
border: none;
}
.btn-danger:hover {
background-color: #c82333;
}
/* Custom styling for the server cards */
.server-card {
padding: 15px;
}
.server-card .btn {
margin-right: 10px;
}
.status {
font-weight: bold;
margin: 5px 0;
}
.connected {
color: green;
}
.disconnected {
color: red;
}
.authenticated {
color: green;
}
.not-authenticated {
color: red;
}
/* Responsive styling for small screens */
@media (max-width: 576px) {
.server-card {
padding: 10px;
}
.server-card .btn {
margin-bottom: 10px;
}
}
</style>
<link rel="stylesheet" href="/css/navbar.css">
</head>
<body>
<%- include('partials/navbar') %>
<div class="container">
<h2 class="mb-4">Server Management</h2>
<div class="server-list-container" id="serverList"></div>
</div>
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.min.js"></script>
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.2.0/css/all.min.css" rel="stylesheet">
<script src="/js/console.js"></script>
</body>
</html>