Compare commits

..

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

13 changed files with 147 additions and 853 deletions

View File

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

View File

@ -1 +1 @@
1.4.10
1.3.41

371
app.py
View File

@ -9,15 +9,21 @@ from typing import Dict, Any, Tuple, Union
import sys
import redis
import json
from pywebpush import webpush, WebPushException
import mysql.connector
import re
import threading
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"
@ -46,7 +52,10 @@ except redis.exceptions.ConnectionError as e:
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
@ -109,7 +118,7 @@ def index() -> Union[Response, str]:
@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"
backend_url = f"{app.config['BASE_URL']}/vapid-public-key"
try:
response = requests.get(backend_url)
return Response(response.content, status=response.status_code, mimetype=response.headers['Content-Type'])
@ -122,7 +131,7 @@ def proxy_save_subscription():
if not session.get("logged_in"):
return jsonify({'error': 'Unauthorized'}), 401
backend_url = f"{app.config['BACKEND_URL']}/save-subscription"
backend_url = f"{app.config['BASE_URL']}/save-subscription"
credentials = base64.b64decode(session["auth_credentials"]).decode()
username, password = credentials.split(":", 1)
@ -136,32 +145,67 @@ def proxy_save_subscription():
except requests.exceptions.RequestException as e:
return jsonify({"error": str(e)}), 502
def get_db_connection():
# This is a simplified version for demonstration.
# In a real application, you would use a connection pool.
return mysql.connector.connect(
host=app.config["DBHOST"],
user=app.config["DBUSER"],
password=app.config["DBPASS"],
database=app.config["DATABASE"],
port=app.config["DBPORT"],
)
def get_push_subscriptions():
conn = get_db_connection()
cursor = conn.cursor(dictionary=True)
cursor.execute("SELECT * FROM push_subscriptions")
subscriptions = cursor.fetchall()
cursor.close()
conn.close()
return subscriptions
def send_notification(subscription_info, message_body):
try:
webpush(
subscription_info=subscription_info,
data=message_body,
vapid_private_key=app.config["VAPID_PRIVATE_KEY"],
vapid_claims={"sub": app.config["VAPID_CLAIM_EMAIL"]},
)
except WebPushException as ex:
print(f"Web push error: {ex}")
# You might want to remove the subscription if it's invalid
if ex.response and ex.response.status_code == 410:
print("Subscription is no longer valid, removing from DB.")
# Add logic to remove the subscription from your database
@app.route('/send-test-notification', methods=['POST'])
def send_test_notification():
"""Proxies the request to send a test notification to the backend."""
"""Sends a test push notification to all users."""
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)
subscriptions = get_push_subscriptions()
if not subscriptions:
return jsonify({"message": "No push subscriptions found."}), 404
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
message_body = json.dumps({"title": "KTVManager", "body": "Ktv Test"})
for sub in subscriptions:
try:
send_notification(json.loads(sub['subscription_json']), message_body)
except Exception as e:
print(f"Error sending notification to subscription ID {sub.get('id', 'N/A')}: {e}")
return jsonify({"message": f"Test notification sent to {len(subscriptions)} subscription(s)."})
@app.route("/home")
@cache.cached(timeout=60, key_prefix=make_cache_key)
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 +213,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 +224,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,11 +232,9 @@ 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:
@ -209,7 +252,7 @@ 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"])
)
@ -220,7 +263,7 @@ 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 +285,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":
@ -253,13 +296,11 @@ def add_account() -> Union[Response, str]:
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()
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,7 +310,7 @@ 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)
return redirect(url_for("user_accounts"))
@ -277,7 +318,7 @@ def delete_account() -> Response:
@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)
@ -292,9 +333,6 @@ def validate_account() -> Tuple[Response, int]:
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()
return jsonify(response_data), response.status_code
except requests.exceptions.RequestException as e:
return jsonify({"error": str(e)}), 500
@ -304,247 +342,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"
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.")
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)
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.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
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:
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,34 @@
</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: <a href="#" id="version-link">{{ version }}</a></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';
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();
// Check for the 'loggedin' query parameter to trigger the prompt
const urlParams = new URLSearchParams(window.location.search);
if (urlParams.has('loggedin')) {
askPermission(registration);
}
}, 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);
}
});
});
});
}
document.getElementById('version-link').addEventListener('click', function(event) {
event.preventDefault();
fetch('{{ url_for("send_test_notification") }}', {
method: 'POST'
}).then(response => response.json())
.then(data => console.log(data.message))
.catch(err => console.error('Error sending test notification:', err));
});
}
function askPermission(registration) {
@ -118,10 +88,7 @@
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.');
});
throw new Error('Failed to fetch VAPID public key');
}
return response.json();
})
@ -143,33 +110,13 @@
}
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()