Compare commits

..

No commits in common. "main" and "1.3.24" have entirely different histories.
main ... 1.3.24

13 changed files with 116 additions and 929 deletions

View File

@ -1,5 +1,5 @@
[tool.bumpversion]
current_version = "1.4.10"
current_version = "1.3.24"
commit = true
tag = true
tag_name = "{new_version}"

View File

@ -1 +1 @@
1.4.10
1.3.24

403
app.py
View File

@ -6,18 +6,20 @@ import requests.auth
import os
import base64
from typing import Dict, Any, Tuple, Union
import sys
import redis
import json
import mysql.connector
import re
import threading
from pywebpush import webpush, WebPushException
from lib.datetime import filter_accounts_next_30_days, filter_accounts_expired
from lib.reqs import (get_urls, get_user_accounts, add_user_account,
delete_user_account, get_stream_names)
from config import DevelopmentConfig, ProductionConfig
try:
from paddleocr import PaddleOCR
from PIL import Image
import numpy as np
OCR_AVAILABLE = True
except ImportError:
OCR_AVAILABLE = False
os.environ["OMP_NUM_THREADS"] = "1"
os.environ["MKL_NUM_THREADS"] = "1"
@ -29,29 +31,18 @@ if os.environ.get("FLASK_ENV") == "production":
else:
app.config.from_object(DevelopmentConfig)
# Check for Redis availability and configure cache
redis_url = app.config["REDIS_URL"]
cache_config = {"CACHE_TYPE": "redis", "CACHE_REDIS_URL": redis_url}
try:
# Use a short timeout to prevent hanging
r = redis.from_url(redis_url, socket_connect_timeout=1)
r.ping()
except redis.exceptions.ConnectionError as e:
print(
f"WARNING: Redis connection failed: {e}. Falling back to SimpleCache. "
"This is not recommended for production with multiple workers.",
file=sys.stderr,
)
cache_config = {"CACHE_TYPE": "SimpleCache"}
cache = Cache(app, config={"CACHE_TYPE": "SimpleCache"})
cache = Cache(app, config=cache_config)
app.config["OCR_ENABLED"] = False
if app.config.get("OCR_ENABLED") and OCR_AVAILABLE:
ocr = PaddleOCR(use_angle_cls=True, lang='en')
else:
app.config["OCR_ENABLED"] = False
app.config["SESSION_COOKIE_SECURE"] = not app.config["DEBUG"]
app.config['SESSION_COOKIE_HTTPONLY'] = True
app.config['SESSION_COOKIE_SAMESITE'] = 'Lax'
app.config['PERMANENT_SESSION_LIFETIME'] = 60 * 60 * 24 * 365 # 1 year
cache.clear()
def get_version() -> str:
"""Retrieves the application version from the VERSION file.
@ -70,12 +61,6 @@ def inject_version() -> Dict[str, str]:
"""Injects the version into all templates."""
return dict(version=get_version(), config=app.config, session=session)
def make_cache_key(*args, **kwargs):
"""Generate a cache key based on the user's session and request path."""
username = session.get('username', 'anonymous')
path = request.path
return f"view/{username}/{path}"
@app.before_request
def make_session_permanent() -> None:
"""Makes the user session permanent."""
@ -106,62 +91,33 @@ def index() -> Union[Response, str]:
return redirect(url_for("home"))
return render_template("index.html")
@app.route('/vapid-public-key', methods=['GET'])
def proxy_vapid_public_key():
"""Proxies the request for the VAPID public key to the backend."""
backend_url = f"{app.config['BACKEND_URL']}/vapid-public-key"
try:
response = requests.get(backend_url)
return Response(response.content, status=response.status_code, mimetype=response.headers['Content-Type'])
except requests.exceptions.RequestException as e:
return jsonify({"error": str(e)}), 502
@app.route('/vapid-public-key')
def vapid_public_key():
"""Provides the VAPID public key to the client."""
return jsonify({'public_key': app.config['VAPID_PUBLIC_KEY']})
@app.route('/save-subscription', methods=['POST'])
def proxy_save_subscription():
"""Proxies the request to save a push subscription to the backend."""
def save_subscription():
"""Saves a push notification subscription."""
if not session.get("logged_in"):
return jsonify({'error': 'Unauthorized'}), 401
backend_url = f"{app.config['BACKEND_URL']}/save-subscription"
credentials = base64.b64decode(session["auth_credentials"]).decode()
username, password = credentials.split(":", 1)
subscription_data = request.get_json()
if not subscription_data:
return jsonify({'error': 'No subscription data received'}), 400
try:
response = requests.post(
backend_url,
auth=requests.auth.HTTPBasicAuth(username, password),
json=request.get_json()
)
return Response(response.content, status=response.status_code, mimetype=response.headers['Content-Type'])
except requests.exceptions.RequestException as e:
return jsonify({"error": str(e)}), 502
@app.route('/send-test-notification', methods=['POST'])
def send_test_notification():
"""Proxies the request to send a test notification to the backend."""
if not session.get("logged_in"):
return jsonify({'error': 'Unauthorized'}), 401
backend_url = f"{app.config['BACKEND_URL']}/send-test-notification"
credentials = base64.b64decode(session["auth_credentials"]).decode()
username, password = credentials.split(":", 1)
try:
response = requests.post(
backend_url,
auth=requests.auth.HTTPBasicAuth(username, password),
json={}
)
return Response(response.content, status=response.status_code, mimetype=response.headers['Content-Type'])
except requests.exceptions.RequestException as e:
return jsonify({"error": str(e)}), 502
# Here you would typically save the subscription to a database
# For this example, we'll just store it in the session
session['push_subscription'] = subscription_data
print(f"Subscription saved for user: {session['username']}")
return jsonify({'success': True})
@app.route("/home")
@cache.cached(timeout=60, key_prefix=make_cache_key)
@cache.cached(timeout=60)
def home() -> str:
"""Renders the home page with account statistics."""
if session.get("logged_in"):
base_url = app.config["BACKEND_URL"]
base_url = app.config["BASE_URL"]
all_accounts = get_user_accounts(base_url, session["auth_credentials"])
return render_template(
"home.html",
@ -169,6 +125,7 @@ def home() -> str:
accounts=len(all_accounts),
current_month_accounts=filter_accounts_next_30_days(all_accounts),
expired_accounts=filter_accounts_expired(all_accounts),
ocr_enabled=app.config.get("OCR_ENABLED"),
)
return render_template("index.html")
@ -179,7 +136,7 @@ def login() -> Union[Response, str]:
password = request.form["password"]
credentials = f"{username}:{password}"
encoded_credentials = base64.b64encode(credentials.encode()).decode()
base_url = app.config["BACKEND_URL"]
base_url = app.config["BASE_URL"]
login_url = f"{base_url}/Login"
try:
@ -187,16 +144,14 @@ def login() -> Union[Response, str]:
login_url, auth=requests.auth.HTTPBasicAuth(username, password)
)
response.raise_for_status()
response_data = response.json()
if response_data.get("auth") == "Success":
if response.json().get("auth") == "Success":
session["logged_in"] = True
session["username"] = response_data.get("username", username)
session["user_id"] = response_data.get("user_id")
session["username"] = username
session["auth_credentials"] = encoded_credentials
next_url = request.args.get("next")
if next_url:
return redirect(next_url)
return redirect(url_for("home", loggedin=True))
return redirect(url_for("home"))
except requests.exceptions.RequestException:
pass # Fall through to error
@ -204,23 +159,23 @@ def login() -> Union[Response, str]:
return render_template("index.html", error=error)
@app.route("/urls", methods=["GET"])
@cache.cached(timeout=300, key_prefix=make_cache_key)
@cache.cached(timeout=300)
def urls() -> Union[Response, str]:
"""Renders the URLs page."""
if not session.get("logged_in"):
return redirect(url_for("home"))
base_url = app.config["BACKEND_URL"]
base_url = app.config["BASE_URL"]
return render_template(
"urls.html", urls=get_urls(base_url, session["auth_credentials"])
)
@app.route("/accounts", methods=["GET"])
@cache.cached(timeout=60, key_prefix=make_cache_key)
@cache.cached(timeout=60)
def user_accounts() -> Union[Response, str]:
"""Renders the user accounts page."""
if not session.get("logged_in"):
return redirect(url_for("home"))
base_url = app.config["BACKEND_URL"]
base_url = app.config["BASE_URL"]
user_accounts_data = get_user_accounts(base_url, session["auth_credentials"])
return render_template(
"user_accounts.html",
@ -242,7 +197,7 @@ def add_account() -> Union[Response, str]:
"""Handles adding a new user account."""
if not session.get("logged_in"):
return redirect(url_for("index", next=request.url))
base_url = app.config["BACKEND_URL"]
base_url = app.config["BASE_URL"]
shared_text = request.args.get('shared_text')
if request.method == "POST":
@ -252,14 +207,12 @@ def add_account() -> Union[Response, str]:
if add_user_account(
base_url, session["auth_credentials"], username, password, stream
):
cache.delete_memoized(user_accounts, key_prefix=make_cache_key)
# Run the NPM config update in a background thread
thread = threading.Thread(target=_update_npm_config_in_background)
thread.start()
cache.delete_memoized(user_accounts)
return redirect(url_for("user_accounts"))
return render_template(
"add_account.html",
ocr_enabled=app.config.get("OCR_ENABLED"),
text_input_enabled=app.config.get("TEXT_INPUT_ENABLED"),
shared_text=shared_text
)
@ -269,15 +222,15 @@ def delete_account() -> Response:
"""Handles deleting a user account."""
stream = request.form.get("stream")
username = request.form.get("username")
base_url = app.config["BACKEND_URL"]
base_url = app.config["BASE_URL"]
delete_user_account(base_url, session["auth_credentials"], stream, username)
cache.delete_memoized(user_accounts, key_prefix=make_cache_key)
cache.delete_memoized(user_accounts)
return redirect(url_for("user_accounts"))
@app.route("/validateAccount", methods=["POST"])
def validate_account() -> Tuple[Response, int]:
"""Forwards account validation requests to the backend."""
base_url = app.config["BACKEND_URL"]
base_url = app.config["BASE_URL"]
validate_url = f"{base_url}/validateAccount"
credentials = base64.b64decode(session["auth_credentials"]).decode()
username, password = credentials.split(":", 1)
@ -291,10 +244,7 @@ def validate_account() -> Tuple[Response, int]:
response.raise_for_status()
response_data = response.json()
if response_data.get("message") == "Account is valid and updated":
cache.delete_memoized(user_accounts, key_prefix=make_cache_key)
# Run the NPM config update in a background thread
thread = threading.Thread(target=_update_npm_config_in_background)
thread.start()
cache.delete_memoized(user_accounts)
return jsonify(response_data), response.status_code
except requests.exceptions.RequestException as e:
return jsonify({"error": str(e)}), 500
@ -304,247 +254,34 @@ def stream_names() -> Union[Response, str]:
"""Fetches and returns stream names as JSON."""
if not session.get("logged_in"):
return redirect(url_for("home"))
base_url = app.config["BACKEND_URL"]
base_url = app.config["BASE_URL"]
return jsonify(get_stream_names(base_url, session["auth_credentials"]))
@app.route('/config')
def config():
"""Handles access to the configuration page."""
if session.get('user_id') and int(session.get('user_id')) == 1:
return redirect(url_for('config_dashboard'))
return redirect(url_for('home'))
@app.route('/config/dashboard')
def config_dashboard():
"""Renders the configuration dashboard."""
if not session.get('user_id') or int(session.get('user_id')) != 1:
return redirect(url_for('home'))
return render_template('config_dashboard.html')
@app.route('/check-expiring-accounts', methods=['POST'])
def check_expiring_accounts():
"""Proxies the request to check for expiring accounts to the backend."""
if not session.get('user_id') or int(session.get('user_id')) != 1:
return jsonify({'error': 'Unauthorized'}), 401
backend_url = f"{app.config['BACKEND_URL']}/check-expiry"
if app.config.get("OCR_ENABLED"):
@app.route('/OCRupload', methods=['POST'])
def ocr_upload() -> Union[Response, str, Tuple[Response, int]]:
"""Handles image uploads for OCR processing."""
if 'image' not in request.files:
return jsonify({"error": "No image file found"}), 400
file = request.files['image']
try:
response = requests.post(backend_url)
return Response(response.content, status=response.status_code, mimetype=response.headers['Content-Type'])
except requests.exceptions.RequestException as e:
return jsonify({"error": str(e)}), 502
@app.route('/dns', methods=['GET', 'POST', 'DELETE'])
def proxy_dns():
"""Proxies DNS management requests to the backend."""
if not session.get('user_id') or int(session.get('user_id')) != 1:
return jsonify({'error': 'Unauthorized'}), 401
backend_url = f"{app.config['BACKEND_URL']}/dns"
credentials = base64.b64decode(session["auth_credentials"]).decode()
username, password = credentials.split(":", 1)
auth = requests.auth.HTTPBasicAuth(username, password)
try:
if request.method == 'GET':
response = requests.get(backend_url, auth=auth)
elif request.method == 'POST':
response = requests.post(backend_url, auth=auth, json=request.get_json())
if response.ok:
cache.clear()
elif request.method == 'DELETE':
response = requests.delete(backend_url, auth=auth, json=request.get_json())
if response.ok:
cache.clear()
return Response(response.content, status=response.status_code, mimetype=response.headers['Content-Type'])
except requests.exceptions.RequestException as e:
return jsonify({"error": str(e)}), 502
@app.route('/extra_urls', methods=['GET', 'POST', 'DELETE'])
def proxy_extra_urls():
"""Proxies extra URL management requests to the backend."""
if not session.get('user_id') or int(session.get('user_id')) != 1:
return jsonify({'error': 'Unauthorized'}), 401
backend_url = f"{app.config['BACKEND_URL']}/extra_urls"
credentials = base64.b64decode(session["auth_credentials"]).decode()
username, password = credentials.split(":", 1)
auth = requests.auth.HTTPBasicAuth(username, password)
try:
if request.method == 'GET':
response = requests.get(backend_url, auth=auth)
elif request.method == 'POST':
response = requests.post(backend_url, auth=auth, json=request.get_json())
if response.ok:
cache.clear()
elif request.method == 'DELETE':
response = requests.delete(backend_url, auth=auth, json=request.get_json())
if response.ok:
cache.clear()
return Response(response.content, status=response.status_code, mimetype=response.headers['Content-Type'])
except requests.exceptions.RequestException as e:
return jsonify({"error": str(e)}), 502
class NginxProxyManager:
def __init__(self, host, email, password):
self.host = host
self.email = email
self.password = password
self.token = None
def login(self):
url = f"{self.host}/api/tokens"
payload = {
"identity": self.email,
"secret": self.password
}
headers = {
"Content-Type": "application/json"
}
response = requests.post(url, headers=headers, data=json.dumps(payload))
if response.status_code == 200:
self.token = response.json()["token"]
print("Login successful.")
image = Image.open(file.stream)
image_np = np.array(image)
result = ocr.ocr(image_np)
extracted_text = [line[1][0] for line in result[0]]
# Basic validation
if len(extracted_text) >= 4:
return render_template(
"add_account.html",
username=extracted_text[2],
password=extracted_text[3],
ocr_enabled=True,
text_input_enabled=app.config.get("TEXT_INPUT_ENABLED")
)
else:
print(f"Failed to login: {response.text}")
exit(1)
def get_proxy_host(self, host_id):
if not self.token:
self.login()
url = f"{self.host}/api/nginx/proxy-hosts/{host_id}"
headers = {
"Authorization": f"Bearer {self.token}"
}
response = requests.get(url, headers=headers)
if response.status_code == 200:
return response.json()
else:
print(f"Failed to get proxy host {host_id}: {response.text}")
return None
def update_proxy_host_config(self, host_id, config):
if not self.token:
self.login()
url = f"{self.host}/api/nginx/proxy-hosts/{host_id}"
original_host_data = self.get_proxy_host(host_id)
if not original_host_data:
return
# Construct a new payload with only the allowed fields for an update
update_payload = {
"domain_names": original_host_data.get("domain_names", []),
"forward_scheme": original_host_data.get("forward_scheme", "http"),
"forward_host": original_host_data.get("forward_host"),
"forward_port": original_host_data.get("forward_port"),
"access_list_id": original_host_data.get("access_list_id", 0),
"certificate_id": original_host_data.get("certificate_id", 0),
"ssl_forced": original_host_data.get("ssl_forced", False),
"hsts_enabled": original_host_data.get("hsts_enabled", False),
"hsts_subdomains": original_host_data.get("hsts_subdomains", False),
"http2_support": original_host_data.get("http2_support", False),
"block_exploits": original_host_data.get("block_exploits", False),
"caching_enabled": original_host_data.get("caching_enabled", False),
"allow_websocket_upgrade": original_host_data.get("allow_websocket_upgrade", False),
"advanced_config": config, # The updated advanced config
"meta": original_host_data.get("meta", {}),
"locations": original_host_data.get("locations", []),
}
headers = {
"Authorization": f"Bearer {self.token}",
"Content-Type": "application/json"
}
response = requests.put(url, headers=headers, data=json.dumps(update_payload))
if response.status_code == 200:
print(f"Successfully updated proxy host {host_id}")
else:
print(f"Failed to update proxy host {host_id}: {response.text}")
def update_config_with_streams(config, streams):
# Get all stream names from the database
db_stream_names = {stream['streamName'] for stream in streams}
# Find all location blocks in the config
location_blocks = re.findall(r'location ~ \^/(\w+)\(\.\*\)\$ \{[^}]+\}', config)
# Remove location blocks that are not in the database
for stream_name in location_blocks:
if stream_name not in db_stream_names:
print(f"Removing location block for stream: {stream_name}")
pattern = re.compile(f'location ~ \\^/{re.escape(stream_name)}\\(\\.\\*\\)\\$ {{[^}}]+}}\\s*', re.DOTALL)
config = pattern.sub('', config)
# Update existing stream URLs
for stream in streams:
stream_name = stream['streamName']
stream_url = stream['streamURL']
if stream_url: # Ensure there is a URL to update to
# Use a more specific regex to avoid replacing parts of other URLs
pattern = re.compile(f'(location ~ \\^/{re.escape(stream_name)}\\(\\.\\*\\)\\$ {{\\s*return 302 )([^;]+)(;\\s*}})')
config = pattern.sub(f'\\1{stream_url}/$1$is_args$args\\3', config)
return config
def _update_npm_config():
"""Helper function to update the NPM config."""
if not session.get('user_id') or int(session.get('user_id')) != 1:
print("Unauthorized attempt to update NPM config.")
return
npm = NginxProxyManager(app.config['NPM_HOST'], app.config['NPM_EMAIL'], app.config['NPM_PASSWORD'])
npm.login()
host = npm.get_proxy_host(9)
if host:
current_config = host.get('advanced_config', '')
backend_url = f"{app.config['BACKEND_URL']}/get_all_stream_urls"
credentials = base64.b64decode(session["auth_credentials"]).decode()
username, password = credentials.split(":", 1)
auth = requests.auth.HTTPBasicAuth(username, password)
try:
response = requests.get(backend_url, auth=auth)
response.raise_for_status()
streams = response.json()
except requests.exceptions.RequestException as e:
print(f"Failed to fetch streams from backend: {e}")
return
if streams:
new_config = update_config_with_streams(current_config, streams)
npm.update_proxy_host_config(9, new_config)
print("NPM config updated successfully.")
else:
print("Failed to update NPM config.")
def _update_npm_config_in_background():
with app.app_context():
_update_npm_config()
@app.route('/update_host_9_config', methods=['POST'])
def update_host_9_config():
if not session.get('user_id') or int(session.get('user_id')) != 1:
return jsonify({'error': 'Unauthorized'}), 401
thread = threading.Thread(target=_update_npm_config_in_background)
thread.start()
return jsonify({'message': 'NPM config update started in the background.'}), 202
return jsonify({"error": "Could not extract required fields from image"}), 400
except Exception as e:
return jsonify({"error": str(e)}), 500
if __name__ == "__main__":
app.run(

View File

@ -9,6 +9,12 @@ RUN pip install --no-cache-dir --prefix=/install -r requirements.txt
# Final stage
FROM python:3.11-slim-bookworm AS final
RUN apt-get update && apt-get install -y --no-install-recommends \
libgomp1 \
libgl1 \
libglib2.0-0 \
&& apt-get clean \
&& rm -rf /var/lib/apt/lists/*
WORKDIR /app

Binary file not shown.

View File

@ -22,9 +22,6 @@ self.addEventListener('install', function(event) {
});
self.addEventListener('push', function(event) {
console.log('[Service Worker] Push Received.');
console.log(`[Service Worker] Push data: "${event.data.text()}"`);
const data = event.data.json();
const options = {
body: data.body,

View File

@ -34,39 +34,3 @@ div.awesomplete {
display: block;
width: 100%;
}
/* Responsive table styles */
table.dataTable.dtr-inline.collapsed>tbody>tr>td.dtr-control:before, table.dataTable.dtr-inline.collapsed>tbody>tr>th.dtr-control:before {
background-color: #337ab7;
border: 2px solid white;
border-radius: 50%;
box-shadow: 0 0 3px #444;
box-sizing: content-box;
content: '+';
color: white;
display: block;
height: 16px;
left: 4px;
position: absolute;
top: 50%;
transform: translateY(-50%);
width: 16px;
font-size: 14px;
line-height: 16px;
}
table.dataTable.dtr-inline.collapsed>tbody>tr.parent>td.dtr-control:before, table.dataTable.dtr-inline.collapsed>tbody>tr.parent>th.dtr-control:before {
content: '-';
background-color: #d33333;
}
/* Media query for mobile devices */
@media (max-width: 768px) {
.container {
margin-top: 1rem !important;
}
main {
padding: 0.5em;
}
}

View File

@ -6,7 +6,8 @@
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/awesomplete/1.1.5/awesomplete.min.css" />
<style>
/* Hide the spinner by default */
#loadingSpinner {
#loadingSpinner,
#ocrLoadingSpinner {
display: none;
}
</style>
@ -49,6 +50,20 @@
<span id="buttonText">Add Account</span>
</button>
</form>
{% if ocr_enabled %}
<hr>
<h2>Load Details Via OCR</h2>
<form action="/OCRupload" method="POST" enctype="multipart/form-data" onsubmit="showLoadingOCR()">
<div class="form-group">
<label for="image">Select Image</label>
<input type="file" class="form-control-file" id="image" name="image" accept="image/*" required>
</div>
<button type="submit" class="btn btn-success" id="ocrButton">
<span class="spinner-border spinner-border-sm" id="ocrLoadingSpinner" role="status" aria-hidden="true"></span>
<span id="ocrButtonText">Load Details</span>
</button>
</form>
{% endif %}
{% if text_input_enabled %}
<hr>
<h2>Load Details Via Text</h2>
@ -67,6 +82,11 @@
document.getElementById("loadingSpinner").style.display = "inline-block";
document.getElementById("buttonText").textContent = "Working...";
}
function showLoadingOCR() {
document.getElementById("ocrButton").disabled = true;
document.getElementById("ocrLoadingSpinner").style.display = "inline-block";
document.getElementById("ocrButtonText").textContent = "Processing...";
}
document.addEventListener("DOMContentLoaded", function() {
var streamInput = document.getElementById("stream");

View File

@ -12,7 +12,7 @@
<link rel="apple-touch-icon" sizes="180x180" href="{{ url_for('static', filename='apple-touch-icon.png') }}" />
<meta name="apple-mobile-web-app-title" content="kTvManager" />
<link rel="manifest" href="{{ url_for('static', filename='site.webmanifest') }}?v={{ version }}" />
<link rel="stylesheet" href="{{ url_for('static', filename='styles.css') }}?v={{ version }}" />
<link rel="stylesheet" href="{{ url_for('static', filename='styles.css') }}" />
{% block head_content %}{% endblock %}
</head>
<body>
@ -46,64 +46,23 @@
</main>
<footer class="bg-dark text-white text-center py-3 mt-5">
<p>Version: {% if session.user_id and session.user_id|int == 1 %}<a href="{{ url_for('config') }}" style="color: inherit; text-decoration: none;">{{ version }}</a>{% else %}{{ version }}{% endif %}</p>
<p>Version: {{ version }}</p>
</footer>
<input type="hidden" id="is-logged-in" value="{{ 'true' if session.get('logged_in') else 'false' }}">
<script src="https://code.jquery.com/jquery-3.5.1.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/popper.js@1.16.1/dist/umd/popper.min.js"></script>
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/js/bootstrap.min.js"></script>
{% block scripts %}{% endblock %}
<script>
if ('serviceWorker' in navigator && 'PushManager' in window) {
const isLoggedIn = document.getElementById('is-logged-in').value === 'true';
window.addEventListener('load', function() {
navigator.serviceWorker.register('{{ url_for("static", filename="service-worker.js") }}').then(function(registration) {
console.log('ServiceWorker registration successful with scope: ', registration.scope);
const enableNotificationsBtn = document.getElementById('enable-notifications-btn');
function setupNotificationButton() {
registration.pushManager.getSubscription().then(function(subscription) {
if (enableNotificationsBtn) {
if (subscription) {
enableNotificationsBtn.style.display = 'none';
} else {
enableNotificationsBtn.style.display = 'block';
enableNotificationsBtn.addEventListener('click', function() {
askPermission(registration);
});
}
}
});
}
if (enableNotificationsBtn) {
setupNotificationButton();
}
}, function(err) {
console.log('ServiceWorker registration failed: ', err);
});
const forceResubscribeBtn = document.getElementById('force-resubscribe-btn');
if (forceResubscribeBtn) {
forceResubscribeBtn.addEventListener('click', function() {
navigator.serviceWorker.ready.then(function(registration) {
registration.pushManager.getSubscription().then(function(subscription) {
if (subscription) {
subscription.unsubscribe().then(function(successful) {
if (successful) {
console.log('Unsubscribed successfully.');
askPermission(registration);
} else {
console.log('Unsubscribe failed.');
}
});
} else {
askPermission(registration);
}
});
});
});
}
}
function askPermission(registration) {
@ -115,18 +74,9 @@
}
function subscribeUser(registration) {
fetch('/vapid-public-key')
.then(response => {
if (!response.ok) {
return response.text().then(text => {
console.error('Failed to fetch VAPID public key. Server response:', text);
throw new Error('Failed to fetch VAPID public key. See server response in logs.');
});
}
return response.json();
})
fetch('http://localhost:3001/vapid-public-key')
.then(response => response.json())
.then(data => {
console.log('Received VAPID public key:', data.public_key);
const applicationServerKey = urlB64ToUint8Array(data.public_key);
registration.pushManager.subscribe({
userVisibleOnly: true,
@ -137,39 +87,17 @@
}).catch(function(err) {
console.log('Failed to subscribe the user: ', err);
});
}).catch(function(err) {
console.error('Error during subscription process:', err);
});
}
function saveSubscription(subscription) {
console.log('Attempting to save subscription...');
fetch('/save-subscription', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
'Content-Type': 'application/json',
'Authorization': 'Basic {{ session.auth_credentials }}'
},
body: JSON.stringify(subscription)
})
.then(response => {
if (response.ok) {
console.log('Subscription saved successfully.');
const enableNotificationsBtn = document.getElementById('enable-notifications-btn');
if (enableNotificationsBtn) {
enableNotificationsBtn.style.display = 'none';
}
return response.json();
} else {
console.error('Failed to save subscription. Status:', response.status);
response.text().then(text => console.error('Server response:', text));
throw new Error('Failed to save subscription.');
}
})
.then(data => {
console.log('Server response on save:', data);
})
.catch(err => {
console.error('Error during saveSubscription fetch:', err);
});
}

View File

@ -1,301 +0,0 @@
{% extends "base.html" %}
{% block title %}Config Dashboard{% endblock %}
{% block content %}
<div class="container mt-4">
<h2 class="mb-4">Configuration Dashboard</h2>
<div class="row">
<div class="col-md-12">
<div class="card">
<div class="card-header">
Actions
</div>
<div class="card-body">
<button id="send-test-notification-btn" class="btn btn-primary">Send Test Notification</button>
<button id="check-expiring-accounts-btn" class="btn btn-info">Check Expiring Accounts</button>
<button id="force-resubscribe-btn" class="btn btn-warning">Force Re-subscribe</button>
<button id="update-host-9-btn" class="btn btn-success">Update Redirect URLS</button>
</div>
</div>
</div>
</div>
<div class="row mt-4">
<div class="col-md-12">
<div class="card">
<div class="card-header">
DNS Manager
</div>
<div class="card-body">
<div class="input-group mb-3">
<input type="text" class="form-control" id="dns-entry-input" placeholder="Enter DNS entry">
<div class="input-group-append">
<button class="btn btn-primary" id="add-dns-btn">Add</button>
</div>
</div>
<table class="table table-striped">
<thead>
<tr>
<th>DNS Entry</th>
<th style="width: 10%;">Actions</th>
</tr>
</thead>
<tbody id="dns-list-table-body">
<!-- DNS entries will be loaded here -->
</tbody>
</table>
</div>
</div>
</div>
</div>
<div class="row mt-4">
<div class="col-md-12">
<div class="card">
<div class="card-header">
Extra URLs Manager
</div>
<div class="card-body">
<div class="input-group mb-3">
<input type="text" class="form-control" id="extra-url-input" placeholder="Enter Extra URL">
<div class="input-group-append">
<button class="btn btn-primary" id="add-extra-url-btn">Add</button>
</div>
</div>
<table class="table table-striped">
<thead>
<tr>
<th>Extra URL</th>
<th style="width: 10%;">Actions</th>
</tr>
</thead>
<tbody id="extra-urls-table-body">
<!-- Extra URLs will be loaded here -->
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
{% endblock %}
{% block scripts %}
{{ super() }}
<script>
document.addEventListener('DOMContentLoaded', function() {
// DNS Manager
const dnsListTableBody = document.getElementById('dns-list-table-body');
const addDnsBtn = document.getElementById('add-dns-btn');
const dnsEntryInput = document.getElementById('dns-entry-input');
function fetchDnsList() {
fetch("{{ url_for('proxy_dns') }}")
.then(response => {
if (!response.ok) {
// Log the error response text for debugging
response.text().then(text => console.error('Error response from proxy:', text));
throw new Error(`HTTP error! Status: ${response.status}`);
}
return response.json();
})
.then(data => {
dnsListTableBody.innerHTML = '';
if (!Array.isArray(data)) {
console.error("Received data is not an array:", data);
throw new Error("Invalid data format received from server.");
}
if (data.length === 0) {
const row = dnsListTableBody.insertRow();
const cell = row.insertCell();
cell.colSpan = 2;
cell.textContent = 'No DNS entries found.';
cell.classList.add('text-center');
} else {
data.forEach(entry => {
const row = dnsListTableBody.insertRow();
const entryCell = row.insertCell();
entryCell.textContent = entry;
const actionCell = row.insertCell();
const removeBtn = document.createElement('button');
removeBtn.className = 'btn btn-danger btn-sm';
removeBtn.textContent = 'Delete';
removeBtn.addEventListener('click', () => removeDnsEntry(entry));
actionCell.appendChild(removeBtn);
});
}
})
.catch(e => {
console.error('Error during fetchDnsList:', e);
dnsListTableBody.innerHTML = '';
const row = dnsListTableBody.insertRow();
const cell = row.insertCell();
cell.colSpan = 2;
cell.textContent = 'Error loading DNS entries. See browser console for details.';
cell.classList.add('text-center', 'text-danger');
});
}
function addDnsEntry() {
const dnsEntry = dnsEntryInput.value.trim();
if (dnsEntry) {
fetch("{{ url_for('proxy_dns') }}", {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ dns_entry: dnsEntry })
}).then(() => {
dnsEntryInput.value = '';
fetchDnsList();
});
}
}
function removeDnsEntry(dnsEntry) {
fetch("{{ url_for('proxy_dns') }}", {
method: 'DELETE',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ dns_entry: dnsEntry })
}).then(() => {
fetchDnsList();
});
}
addDnsBtn.addEventListener('click', addDnsEntry);
fetchDnsList();
// Extra URLs Manager
const extraUrlsTableBody = document.getElementById('extra-urls-table-body');
const addExtraUrlBtn = document.getElementById('add-extra-url-btn');
const extraUrlInput = document.getElementById('extra-url-input');
function fetchExtraUrlsList() {
fetch("{{ url_for('proxy_extra_urls') }}")
.then(response => {
if (!response.ok) {
// Log the error response text for debugging
response.text().then(text => console.error('Error response from proxy:', text));
throw new Error(`HTTP error! Status: ${response.status}`);
}
return response.json();
})
.then(data => {
extraUrlsTableBody.innerHTML = '';
if (!Array.isArray(data)) {
console.error("Received data is not an array:", data);
throw new Error("Invalid data format received from server.");
}
if (data.length === 0) {
const row = extraUrlsTableBody.insertRow();
const cell = row.insertCell();
cell.colSpan = 2;
cell.textContent = 'No extra URLs found.';
cell.classList.add('text-center');
} else {
data.forEach(entry => {
const row = extraUrlsTableBody.insertRow();
const entryCell = row.insertCell();
entryCell.textContent = entry;
const actionCell = row.insertCell();
const removeBtn = document.createElement('button');
removeBtn.className = 'btn btn-danger btn-sm';
removeBtn.textContent = 'Delete';
removeBtn.addEventListener('click', () => removeExtraUrl(entry));
actionCell.appendChild(removeBtn);
});
}
})
.catch(e => {
console.error('Error during fetchExtraUrlsList:', e);
extraUrlsTableBody.innerHTML = '';
const row = extraUrlsTableBody.insertRow();
const cell = row.insertCell();
cell.colSpan = 2;
cell.textContent = 'Error loading extra URLs. See browser console for details.';
cell.classList.add('text-center', 'text-danger');
});
}
function addExtraUrl() {
const extraUrl = extraUrlInput.value.trim();
if (extraUrl) {
fetch("{{ url_for('proxy_extra_urls') }}", {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ extra_url: extraUrl })
}).then(() => {
extraUrlInput.value = '';
fetchExtraUrlsList();
});
}
}
function removeExtraUrl(extraUrl) {
fetch("{{ url_for('proxy_extra_urls') }}", {
method: 'DELETE',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ extra_url: extraUrl })
}).then(() => {
fetchExtraUrlsList();
});
}
addExtraUrlBtn.addEventListener('click', addExtraUrl);
fetchExtraUrlsList();
// Other buttons
document.getElementById('send-test-notification-btn').addEventListener('click', function() {
fetch('{{ url_for("send_test_notification") }}', {
method: 'POST'
}).then(response => {
if (response.ok) {
alert('Test notification sent successfully!');
} else {
alert('Failed to send test notification.');
}
}).catch(err => {
console.error('Error sending test notification:', err);
alert('An error occurred while sending the test notification.');
});
});
document.getElementById('check-expiring-accounts-btn').addEventListener('click', function() {
fetch('{{ url_for("check_expiring_accounts") }}', {
method: 'POST'
}).then(response => {
if (response.ok) {
alert('Expiring accounts check triggered successfully!');
} else {
alert('Failed to trigger expiring accounts check.');
}
}).catch(err => {
console.error('Error triggering expiring accounts check:', err);
alert('An error occurred while triggering the expiring accounts check.');
});
});
document.getElementById('update-host-9-btn').addEventListener('click', function() {
fetch('{{ url_for("update_host_9_config") }}', {
method: 'POST'
}).then(response => {
if (response.ok) {
alert('Host 9 config updated successfully!');
} else {
alert('Failed to update Host 9 config.');
}
}).catch(err => {
console.error('Error updating Host 9 config:', err);
alert('An error occurred while updating the Host 9 config.');
});
});
});
</script>
{% endblock %}

View File

@ -4,7 +4,7 @@
{% block content %}
<h1>Welcome {{ username }}!</h1>
<button id="enable-notifications-btn" class="btn btn-primary my-3">Enable Notifications</button>
<br>
<h2>You have {{ accounts }} active accounts</h2>
<br>

View File

@ -25,8 +25,8 @@
{% block content %}
<h2>{{ username }}'s Accounts</h2>
<div>
<table class="table table-striped dt-responsive nowrap" id="accountsTable" style="width:100%">
<div class="table-responsive">
<table class="table table-striped" id="accountsTable">
<thead>
<tr>
<!-- <th>#</th> -->

View File

@ -1,164 +0,0 @@
import requests
import json
import mysql.connector
import re
import os
from dotenv import load_dotenv
# Load environment variables from .env file
load_dotenv()
# Assuming config.py is in the same directory or accessible via PYTHONPATH
from config import DevelopmentConfig as app_config
class NginxProxyManager:
def __init__(self, host, email, password):
self.host = host
self.email = email
self.password = password
self.token = None
def login(self):
url = f"{self.host}/api/tokens"
payload = {
"identity": self.email,
"secret": self.password
}
headers = {
"Content-Type": "application/json"
}
response = requests.post(url, headers=headers, data=json.dumps(payload))
if response.status_code == 200:
self.token = response.json()["token"]
print("Login successful.")
else:
print(f"Failed to login: {response.text}")
exit(1)
def get_proxy_host(self, host_id):
if not self.token:
self.login()
url = f"{self.host}/api/nginx/proxy-hosts/{host_id}"
headers = {
"Authorization": f"Bearer {self.token}"
}
response = requests.get(url, headers=headers)
if response.status_code == 200:
return response.json()
else:
print(f"Failed to get proxy host {host_id}: {response.text}")
return None
def update_proxy_host_config(self, host_id, config):
if not self.token:
self.login()
url = f"{self.host}/api/nginx/proxy-hosts/{host_id}"
original_host_data = self.get_proxy_host(host_id)
if not original_host_data:
return
# Construct a new payload with only the allowed fields for an update
update_payload = {
"domain_names": original_host_data.get("domain_names", []),
"forward_scheme": original_host_data.get("forward_scheme", "http"),
"forward_host": original_host_data.get("forward_host"),
"forward_port": original_host_data.get("forward_port"),
"access_list_id": original_host_data.get("access_list_id", 0),
"certificate_id": original_host_data.get("certificate_id", 0),
"ssl_forced": original_host_data.get("ssl_forced", False),
"hsts_enabled": original_host_data.get("hsts_enabled", False),
"hsts_subdomains": original_host_data.get("hsts_subdomains", False),
"http2_support": original_host_data.get("http2_support", False),
"block_exploits": original_host_data.get("block_exploits", False),
"caching_enabled": original_host_data.get("caching_enabled", False),
"allow_websocket_upgrade": original_host_data.get("allow_websocket_upgrade", False),
"advanced_config": config, # The updated advanced config
"meta": original_host_data.get("meta", {}),
"locations": original_host_data.get("locations", []),
}
headers = {
"Authorization": f"Bearer {self.token}",
"Content-Type": "application/json"
}
response = requests.put(url, headers=headers, data=json.dumps(update_payload))
if response.status_code == 200:
print(f"Successfully updated proxy host {host_id}")
else:
print(f"Failed to update proxy host {host_id}: {response.text}")
def get_streams_from_db(db_host, db_user, db_pass, db_name, db_port):
try:
conn = mysql.connector.connect(
host=db_host,
user=db_user,
password=db_pass,
database=db_name,
port=db_port
)
cursor = conn.cursor(dictionary=True)
cursor.execute("""
SELECT DISTINCT
SUBSTRING_INDEX(stream, ' ', 1) AS streamName,
streamURL
FROM userAccounts
""")
streams = cursor.fetchall()
cursor.close()
conn.close()
return streams
except mysql.connector.Error as err:
print(f"Error connecting to database: {err}")
return []
def update_config_with_streams(config, streams):
# Get all stream names from the database
db_stream_names = {stream['streamName'] for stream in streams}
# Find all location blocks in the config
location_blocks = re.findall(r'location ~ \^/(\w+)\(\.\*\)\$ \{[^}]+\}', config)
# Remove location blocks that are not in the database
for stream_name in location_blocks:
if stream_name not in db_stream_names:
print(f"Removing location block for stream: {stream_name}")
pattern = re.compile(f'location ~ \\^/{re.escape(stream_name)}\\(\\.\\*\\)\\$ {{[^}}]+}}\\s*', re.DOTALL)
config = pattern.sub('', config)
# Update existing stream URLs
for stream in streams:
stream_name = stream['streamName']
stream_url = stream['streamURL']
if stream_url: # Ensure there is a URL to update to
# Use a more specific regex to avoid replacing parts of other URLs
pattern = re.compile(f'(location ~ \\^/{re.escape(stream_name)}\\(\\.\\*\\)\\$ {{\\s*return 302 )([^;]+)(;\\s*}})')
config = pattern.sub(f'\\1{stream_url}/$1$is_args$args\\3', config)
return config
def main():
npm = NginxProxyManager(app_config.NPM_HOST, app_config.NPM_EMAIL, app_config.NPM_PASSWORD)
npm.login()
host = npm.get_proxy_host(9)
if host:
current_config = host.get('advanced_config', '')
print("Current Config:")
print(current_config)
streams = get_streams_from_db(app_config.DBHOST, app_config.DBUSER, app_config.DBPASS, app_config.DATABASE, app_config.DBPORT)
if streams:
new_config = update_config_with_streams(current_config, streams)
print("\nNew Config:")
print(new_config)
# Uncomment the following line to apply the changes
npm.update_proxy_host_config(9, new_config)
print("\nTo apply the changes, uncomment the last line in the main function.")
if __name__ == "__main__":
main()