mirror of
https://github.com/smittix/intercept.git
synced 2026-06-08 14:11:54 -07:00
Merge pull request #54 from smittix/feature/login-system
This commit is contained in:
@@ -15,6 +15,7 @@ venv/
|
||||
.eggs/
|
||||
*.egg-info/
|
||||
*.egg
|
||||
.uv
|
||||
|
||||
# IDE
|
||||
.idea/
|
||||
|
||||
@@ -34,3 +34,4 @@ build/
|
||||
uv.lock
|
||||
*.db
|
||||
*.sqlite3
|
||||
intercept.db
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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.
@@ -29,6 +29,7 @@ dependencies = [
|
||||
"flask>=2.0.0",
|
||||
"skyfield>=1.45",
|
||||
"pyserial>=3.5",
|
||||
"Werkzeug>=3.1.5",
|
||||
]
|
||||
|
||||
[project.urls]
|
||||
|
||||
+88
-1
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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
File diff suppressed because it is too large
Load Diff
@@ -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>
|
||||
@@ -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
|
||||
# =====================================================================
|
||||
|
||||
Reference in New Issue
Block a user