Merge pull request #54 from smittix/feature/login-system

This commit is contained in:
Smittix
2026-01-18 17:02:25 +00:00
committed by GitHub
11 changed files with 1318 additions and 634 deletions
+1
View File
@@ -15,6 +15,7 @@ venv/
.eggs/
*.egg-info/
*.egg
.uv
# IDE
.idea/
+1
View File
@@ -34,3 +34,4 @@ build/
uv.lock
*.db
*.sqlite3
intercept.db
+50 -4
View File
@@ -9,6 +9,8 @@ from __future__ import annotations
import sys
import site
from utils.database import get_db
# Ensure user site-packages is available (may be disabled when running as root/sudo)
if not site.ENABLE_USER_SITE:
user_site = site.getusersitepackages()
@@ -23,8 +25,8 @@ import subprocess
from typing import Any
from flask import Flask, render_template, jsonify, send_file, Response, request
from flask import Flask, render_template, jsonify, send_file, Response, request,redirect, url_for, flash, session
from werkzeug.security import check_password_hash
from config import VERSION, CHANGELOG
from utils.dependencies import check_tool, check_all_dependencies, TOOL_DEPENDENCIES
from utils.process import cleanup_stale_processes
@@ -36,14 +38,15 @@ from utils.constants import (
MAX_BT_DEVICE_AGE_SECONDS,
QUEUE_MAX_SIZE,
)
import logging
# Track application start time for uptime calculation
import time as _time
_app_start_time = _time.time()
logger = logging.getLogger('intercept.database')
# Create Flask app
app = Flask(__name__)
app.secret_key = "signals_intelligence_secret" # Required for flash messages
# Disable Werkzeug debugger PIN (not needed for local development tool)
os.environ['WERKZEUG_DEBUG_PIN'] = 'off'
@@ -156,6 +159,49 @@ cleanup_manager.register(adsb_aircraft)
# MAIN ROUTES
# ============================================
@app.before_request
def require_login():
# Routes that don't require login (to avoid infinite redirect loop)
allowed_routes = ['login', 'static', 'favicon', 'health']
# If user is not logged in and the current route is not allowed...
if 'logged_in' not in session and request.endpoint not in allowed_routes:
return redirect(url_for('login'))
@app.route('/logout')
def logout():
session.pop('logged_in', None)
return redirect(url_for('login'))
@app.route('/login', methods=['GET', 'POST'])
def login():
if request.method == 'POST':
username = request.form.get('username')
password = request.form.get('password')
# Connect to DB and find user
with get_db() as conn:
cursor = conn.execute(
'SELECT password_hash, role FROM users WHERE username = ?',
(username,)
)
user = cursor.fetchone()
# Verify user exists and password is correct
if user and check_password_hash(user['password_hash'], password):
# Store data in session
session['logged_in'] = True
session['username'] = username
session['role'] = user['role']
logger.info(f"User '{username}' logged in successfully.")
return redirect(url_for('index'))
else:
logger.warning(f"Failed login attempt for username: {username}")
flash("ACCESS DENIED: INVALID CREDENTIALS", "error")
return render_template('login.html', version=VERSION)
@app.route('/')
def index() -> str:
tools = {
+3
View File
@@ -125,6 +125,9 @@ SATELLITE_UPDATE_INTERVAL = _get_env_int('SATELLITE_UPDATE_INTERVAL', 30)
SATELLITE_TRAJECTORY_POINTS = _get_env_int('SATELLITE_TRAJECTORY_POINTS', 30)
SATELLITE_ORBIT_MINUTES = _get_env_int('SATELLITE_ORBIT_MINUTES', 45)
# Admin credentials
ADMIN_USERNAME = _get_env('ADMIN_USERNAME', 'admin')
ADMIN_PASSWORD = _get_env('ADMIN_PASSWORD', 'admin')
def configure_logging() -> None:
"""Configure application logging."""
Binary file not shown.
+1
View File
@@ -29,6 +29,7 @@ dependencies = [
"flask>=2.0.0",
"skyfield>=1.45",
"pyserial>=3.5",
"Werkzeug>=3.1.5",
]
[project.urls]
+88 -1
View File
@@ -990,6 +990,93 @@ header h1 .tagline {
animation: pulse-glow 2s infinite;
}
.help-btn {
position: absolute;
top: 50%;
transform: translateY(-50%);
right: 20px;
width: 32px;
height: 32px;
border-radius: 50%;
background: var(--bg-primary);
border: 1px solid var(--border-color);
color: var(--text-secondary);
font-size: 16px;
font-weight: bold;
cursor: pointer;
transition: all 0.2s ease;
display: flex;
align-items: center;
justify-content: center;
z-index: 100;
}
.help-btn:hover {
border-color: var(--accent-cyan);
color: var(--accent-cyan);
background: var(--bg-secondary);
}
#depsBtn {
right: 5.5em;
}
#helpBtn {
right: 3.5em;
}
.theme-toggle {
position: absolute;
top: 50%;
transform: translateY(-50%);
right: 8em;
width: 32px;
height: 32px;
border-radius: 50%;
background: var(--bg-primary);
border: 1px solid var(--border-color);
color: var(--text-secondary);
font-size: 16px;
cursor: pointer;
transition: all 0.2s ease;
display: flex;
align-items: center;
justify-content: center;
z-index: 100;
}
.theme-toggle:hover {
border-color: var(--accent-cyan);
color: var(--accent-cyan);
background: var(--bg-secondary);
}
.theme-toggle .icon-sun,
.theme-toggle .icon-moon {
position: absolute;
transition: opacity 0.2s, transform 0.2s;
}
.theme-toggle .icon-sun {
opacity: 0;
transform: rotate(-90deg);
}
.theme-toggle .icon-moon {
opacity: 1;
transform: rotate(0deg);
}
[data-theme="light"] .theme-toggle .icon-sun {
opacity: 1;
transform: rotate(0deg);
}
[data-theme="light"] .theme-toggle .icon-moon {
opacity: 0;
transform: rotate(90deg);
}
.help-modal {
display: none;
position: fixed;
@@ -5037,4 +5124,4 @@ body::before {
.preset-freq-btn:active {
transform: scale(0.98);
}
}
+123
View File
@@ -0,0 +1,123 @@
/* Container Layout */
.landing-overlay {
position: fixed;
top: 0; left: 0; width: 100%; height: 100%;
background: var(--bg-primary);
display: flex;
flex-direction: column; /* Stack logo, title, box vertically */
align-items: center;
justify-content: center;
overflow: hidden;
}
.landing-content {
position: relative;
z-index: 10;
display: flex;
flex-direction: column;
align-items: center;
width: 100%;
}
/* Background Effects */
.landing-scanline {
position: absolute;
top: 0; left: 0; width: 100%; height: 2px;
background: linear-gradient(90deg, transparent, var(--accent-cyan), transparent);
animation: scanlineMove 5s linear infinite;
opacity: 0.4;
z-index: 1; /* Behind content */
pointer-events: none;
}
@keyframes scanlineMove {
0% { top: 0; }
100% { top: 100%; }
}
/* Typography */
.landing-title {
font-family: 'JetBrains Mono', monospace;
font-size: 2.2rem;
font-weight: 700;
letter-spacing: 0.4em;
color: var(--text-primary);
margin: 20px 0 5px 0;
text-indent: 0.4em;
text-align: center;
}
.landing-tagline {
font-family: 'JetBrains Mono', monospace;
color: var(--accent-cyan);
font-size: 0.9rem;
letter-spacing: 0.15em;
margin-bottom: 30px;
}
/* The Login Box */
.login-box {
background: var(--bg-secondary);
border: 1px solid var(--border-color);
padding: 30px;
border-radius: 4px;
width: 380px;
z-index: 20;
box-shadow: 0 0 40px rgba(0, 0, 0, 0.6), inset 0 0 20px var(--accent-cyan-dim);
box-sizing: border-box; /* Ensures padding doesn't hide inputs */
display: flex;
flex-direction: column;
}
/* Hacker Style Error */
.flash-error {
font-family: 'JetBrains Mono', monospace;
font-size: 10px;
color: var(--accent-red);
background: rgba(239, 68, 68, 0.1);
border-left: 3px solid var(--accent-red);
padding: 10px;
margin-bottom: 20px;
display: flex;
gap: 10px;
text-transform: uppercase;
box-sizing: border-box;
}
.error-prefix { font-weight: 700; opacity: 0.7; }
/* Inputs */
.form-input {
width: 100%;
background: var(--bg-primary);
border: 1px solid var(--border-color);
color: var(--accent-cyan);
padding: 12px;
margin-bottom: 15px;
font-family: 'JetBrains Mono', monospace;
font-size: 11px;
outline: none;
box-sizing: border-box; /* Crucial for visibility */
}
.landing-enter-btn {
width: 100%;
background: transparent;
border: 2px solid var(--accent-cyan);
color: var(--accent-cyan);
padding: 15px;
font-family: 'JetBrains Mono', monospace;
font-weight: 600;
letter-spacing: 3px;
cursor: pointer;
transition: all 0.3s ease;
box-sizing: border-box;
}
.landing-version {
margin-top: 25px;
font-family: 'JetBrains Mono', monospace;
font-size: 10px;
color: rgba(255, 255, 255, 0.3);
letter-spacing: 2px;
}
+960 -629
View File
File diff suppressed because it is too large Load Diff
+65
View File
@@ -0,0 +1,65 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>iNTERCEPT // Restricted Access</title>
<link
rel="stylesheet"
href="{{ url_for('static', filename='css/index.css') }}"
/>
<link
rel="stylesheet"
href="{{ url_for('static', filename='css/login.css') }}"
/>
</head>
<body>
<div class="landing-overlay">
<div class="landing-scanline"></div>
<div class="landing-content">
<div class="landing-logo">
<svg width="100" height="100" viewBox="0 0 100 100" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M15 30 Q5 50, 15 70" stroke="#00d4ff" stroke-width="3" fill="none" stroke-linecap="round" opacity="0.5" class="signal-wave signal-wave-1"/>
<path d="M22 35 Q14 50, 22 65" stroke="#00d4ff" stroke-width="2.5" fill="none" stroke-linecap="round" opacity="0.7" class="signal-wave signal-wave-2"/>
<path d="M29 40 Q23 50, 29 60" stroke="#00d4ff" stroke-width="2" fill="none" stroke-linecap="round" class="signal-wave signal-wave-3"/>
<path d="M85 30 Q95 50, 85 70" stroke="#00d4ff" stroke-width="3" fill="none" stroke-linecap="round" opacity="0.5" class="signal-wave signal-wave-1"/>
<path d="M78 35 Q86 50, 78 65" stroke="#00d4ff" stroke-width="2.5" fill="none" stroke-linecap="round" opacity="0.7" class="signal-wave signal-wave-2"/>
<path d="M71 40 Q77 50, 71 60" stroke="#00d4ff" stroke-width="2" fill="none" stroke-linecap="round" class="signal-wave signal-wave-3"/>
<circle cx="50" cy="22" r="6" fill="#00ff88" class="logo-dot" />
<rect x="44" y="35" width="12" height="45" rx="2" fill="#00d4ff" />
<rect x="38" y="35" width="24" height="4" rx="1" fill="#00d4ff" />
<rect x="38" y="76" width="24" height="4" rx="1" fill="#00d4ff" />
</svg>
</div>
<h1 class="landing-title">SECURE LOGIN</h1>
<p class="landing-tagline">// Restricted Terminal Access</p>
<div class="login-box">
{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}
{% for category, message in messages %}
<div class="flash-error">
<span class="error-prefix">ERROR_SIG:</span>
<span class="error-message">ACCESS_DENIED // {{ message|upper }}</span>
</div>
{% endfor %}
{% endif %}
{% endwith %}
<form method="POST">
<input type="text" name="username" placeholder="OPERATOR ID" class="form-input" required autofocus />
<input type="password" name="password" placeholder="ENCRYPTION KEY" class="form-input" required />
<button type="submit" class="landing-enter-btn">
<span class="btn-text">INITIALIZE SESSION</span>
</button>
</form>
</div>
<p class="landing-version">SYSTEM AUTH v{{ version }}</p>
</div>
</div>
</body>
</html>
+26
View File
@@ -12,6 +12,8 @@ from contextlib import contextmanager
from datetime import datetime
from pathlib import Path
from typing import Any
from werkzeug.security import generate_password_hash
from config import ADMIN_USERNAME, ADMIN_PASSWORD
logger = logging.getLogger('intercept.database')
@@ -100,6 +102,30 @@ def init_db() -> None:
)
''')
# Users table for authentication
conn.execute('''
CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
username TEXT UNIQUE NOT NULL,
password_hash TEXT NOT NULL,
role TEXT NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
''')
cursor = conn.execute('SELECT COUNT(*) FROM users')
if cursor.fetchone()[0] == 0:
from config import ADMIN_USERNAME, ADMIN_PASSWORD
logger.info(f"Creating default admin user: {ADMIN_USERNAME}")
# Password hashing
hashed_pw = generate_password_hash(ADMIN_PASSWORD)
conn.execute('''
INSERT INTO users (username, password_hash, role)
VALUES (?, ?, ?)
''', (ADMIN_USERNAME, hashed_pw, 'admin'))
# =====================================================================
# TSCM (Technical Surveillance Countermeasures) Tables
# =====================================================================