Compare commits
69 Commits
Author | SHA1 | Date | |
---|---|---|---|
8b9c100e87 | |||
6462fc6009 | |||
dbf0161133 | |||
0d56235863 | |||
5c80751484 | |||
1d2b1db9df | |||
1dd83f4230 | |||
3888e6d536 | |||
d063d7fb7f | |||
70e7782918 | |||
785fdb6dbb | |||
5106686a12 | |||
21c48d0b6a | |||
7f42202383 | |||
824bdd080f | |||
ef38edfcd4 | |||
7622716b92 | |||
08ebb7a265 | |||
644ba005aa | |||
eea6708f42 | |||
8b9880ce44 | |||
21234fccc6 | |||
c62441a2d1 | |||
d519a268a0 | |||
e76a54854b | |||
6cf291f857 | |||
2cdb601706 | |||
e84441e7f1 | |||
7ad67a80f5 | |||
64f0da662b | |||
472098f9f1 | |||
ec928cf631 | |||
9e3348d9b6 | |||
75f210df5f | |||
b5fcc31cf4 | |||
898d737324 | |||
33c8af61ca | |||
b274bf12d3 | |||
868571f1a8 | |||
15a09789a0 | |||
44fa24f2b7 | |||
c494a90227 | |||
fc16f5f4f1 | |||
fc359a844f | |||
a45f2ba3cb | |||
7329313399 | |||
37f888fa2d | |||
22be992500 | |||
aec1549698 | |||
e30d85065d | |||
a6cddef761 | |||
70c900fc67 | |||
fcd35f166e | |||
a9ece95d07 | |||
85f1ddb877 | |||
f77f395de6 | |||
e16fb42e37 | |||
7468b1b391 | |||
3191fabf39 | |||
6736680131 | |||
16dfcab6ba | |||
a041ec2abb | |||
8c982666d6 | |||
9e63cbc2f0 | |||
e4f92a1db2 | |||
b81d0ae81c | |||
0f1e97bba2 | |||
a9276f63dc | |||
cf46844a4e |
@ -1,5 +1,5 @@
|
|||||||
[tool.bumpversion]
|
[tool.bumpversion]
|
||||||
current_version = "1.3.32"
|
current_version = "1.4.10"
|
||||||
commit = true
|
commit = true
|
||||||
tag = true
|
tag = true
|
||||||
tag_name = "{new_version}"
|
tag_name = "{new_version}"
|
||||||
|
367
app.py
367
app.py
@ -6,19 +6,18 @@ import requests.auth
|
|||||||
import os
|
import os
|
||||||
import base64
|
import base64
|
||||||
from typing import Dict, Any, Tuple, Union
|
from typing import Dict, Any, Tuple, Union
|
||||||
|
import sys
|
||||||
|
import redis
|
||||||
|
import json
|
||||||
|
import mysql.connector
|
||||||
|
import re
|
||||||
|
import threading
|
||||||
|
|
||||||
from lib.datetime import filter_accounts_next_30_days, filter_accounts_expired
|
from lib.datetime import filter_accounts_next_30_days, filter_accounts_expired
|
||||||
from lib.reqs import (get_urls, get_user_accounts, add_user_account,
|
from lib.reqs import (get_urls, get_user_accounts, add_user_account,
|
||||||
delete_user_account, get_stream_names)
|
delete_user_account, get_stream_names)
|
||||||
from config import DevelopmentConfig, ProductionConfig
|
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["OMP_NUM_THREADS"] = "1"
|
||||||
os.environ["MKL_NUM_THREADS"] = "1"
|
os.environ["MKL_NUM_THREADS"] = "1"
|
||||||
@ -30,18 +29,29 @@ if os.environ.get("FLASK_ENV") == "production":
|
|||||||
else:
|
else:
|
||||||
app.config.from_object(DevelopmentConfig)
|
app.config.from_object(DevelopmentConfig)
|
||||||
|
|
||||||
cache = Cache(app, config={"CACHE_TYPE": "SimpleCache"})
|
# 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"}
|
||||||
|
|
||||||
if app.config.get("OCR_ENABLED") and OCR_AVAILABLE:
|
cache = Cache(app, config=cache_config)
|
||||||
ocr = PaddleOCR(use_angle_cls=True, lang='en')
|
|
||||||
else:
|
app.config["OCR_ENABLED"] = False
|
||||||
app.config["OCR_ENABLED"] = False
|
|
||||||
|
|
||||||
app.config["SESSION_COOKIE_SECURE"] = not app.config["DEBUG"]
|
app.config["SESSION_COOKIE_SECURE"] = not app.config["DEBUG"]
|
||||||
app.config['SESSION_COOKIE_HTTPONLY'] = True
|
app.config['SESSION_COOKIE_HTTPONLY'] = True
|
||||||
app.config['SESSION_COOKIE_SAMESITE'] = 'Lax'
|
app.config['SESSION_COOKIE_SAMESITE'] = 'Lax'
|
||||||
app.config['PERMANENT_SESSION_LIFETIME'] = 60 * 60 * 24 * 365 # 1 year
|
app.config['PERMANENT_SESSION_LIFETIME'] = 60 * 60 * 24 * 365 # 1 year
|
||||||
cache.clear()
|
|
||||||
|
|
||||||
def get_version() -> str:
|
def get_version() -> str:
|
||||||
"""Retrieves the application version from the VERSION file.
|
"""Retrieves the application version from the VERSION file.
|
||||||
@ -60,6 +70,12 @@ def inject_version() -> Dict[str, str]:
|
|||||||
"""Injects the version into all templates."""
|
"""Injects the version into all templates."""
|
||||||
return dict(version=get_version(), config=app.config, session=session)
|
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
|
@app.before_request
|
||||||
def make_session_permanent() -> None:
|
def make_session_permanent() -> None:
|
||||||
"""Makes the user session permanent."""
|
"""Makes the user session permanent."""
|
||||||
@ -93,7 +109,7 @@ def index() -> Union[Response, str]:
|
|||||||
@app.route('/vapid-public-key', methods=['GET'])
|
@app.route('/vapid-public-key', methods=['GET'])
|
||||||
def proxy_vapid_public_key():
|
def proxy_vapid_public_key():
|
||||||
"""Proxies the request for the VAPID public key to the backend."""
|
"""Proxies the request for the VAPID public key to the backend."""
|
||||||
backend_url = f"{app.config['BASE_URL']}/vapid-public-key"
|
backend_url = f"{app.config['BACKEND_URL']}/vapid-public-key"
|
||||||
try:
|
try:
|
||||||
response = requests.get(backend_url)
|
response = requests.get(backend_url)
|
||||||
return Response(response.content, status=response.status_code, mimetype=response.headers['Content-Type'])
|
return Response(response.content, status=response.status_code, mimetype=response.headers['Content-Type'])
|
||||||
@ -106,7 +122,7 @@ def proxy_save_subscription():
|
|||||||
if not session.get("logged_in"):
|
if not session.get("logged_in"):
|
||||||
return jsonify({'error': 'Unauthorized'}), 401
|
return jsonify({'error': 'Unauthorized'}), 401
|
||||||
|
|
||||||
backend_url = f"{app.config['BASE_URL']}/save-subscription"
|
backend_url = f"{app.config['BACKEND_URL']}/save-subscription"
|
||||||
credentials = base64.b64decode(session["auth_credentials"]).decode()
|
credentials = base64.b64decode(session["auth_credentials"]).decode()
|
||||||
username, password = credentials.split(":", 1)
|
username, password = credentials.split(":", 1)
|
||||||
|
|
||||||
@ -120,12 +136,32 @@ def proxy_save_subscription():
|
|||||||
except requests.exceptions.RequestException as e:
|
except requests.exceptions.RequestException as e:
|
||||||
return jsonify({"error": str(e)}), 502
|
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
|
||||||
|
|
||||||
@app.route("/home")
|
@app.route("/home")
|
||||||
@cache.cached(timeout=60)
|
@cache.cached(timeout=60, key_prefix=make_cache_key)
|
||||||
def home() -> str:
|
def home() -> str:
|
||||||
"""Renders the home page with account statistics."""
|
"""Renders the home page with account statistics."""
|
||||||
if session.get("logged_in"):
|
if session.get("logged_in"):
|
||||||
base_url = app.config["BASE_URL"]
|
base_url = app.config["BACKEND_URL"]
|
||||||
all_accounts = get_user_accounts(base_url, session["auth_credentials"])
|
all_accounts = get_user_accounts(base_url, session["auth_credentials"])
|
||||||
return render_template(
|
return render_template(
|
||||||
"home.html",
|
"home.html",
|
||||||
@ -133,7 +169,6 @@ def home() -> str:
|
|||||||
accounts=len(all_accounts),
|
accounts=len(all_accounts),
|
||||||
current_month_accounts=filter_accounts_next_30_days(all_accounts),
|
current_month_accounts=filter_accounts_next_30_days(all_accounts),
|
||||||
expired_accounts=filter_accounts_expired(all_accounts),
|
expired_accounts=filter_accounts_expired(all_accounts),
|
||||||
ocr_enabled=app.config.get("OCR_ENABLED"),
|
|
||||||
)
|
)
|
||||||
return render_template("index.html")
|
return render_template("index.html")
|
||||||
|
|
||||||
@ -144,7 +179,7 @@ def login() -> Union[Response, str]:
|
|||||||
password = request.form["password"]
|
password = request.form["password"]
|
||||||
credentials = f"{username}:{password}"
|
credentials = f"{username}:{password}"
|
||||||
encoded_credentials = base64.b64encode(credentials.encode()).decode()
|
encoded_credentials = base64.b64encode(credentials.encode()).decode()
|
||||||
base_url = app.config["BASE_URL"]
|
base_url = app.config["BACKEND_URL"]
|
||||||
login_url = f"{base_url}/Login"
|
login_url = f"{base_url}/Login"
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@ -152,9 +187,11 @@ def login() -> Union[Response, str]:
|
|||||||
login_url, auth=requests.auth.HTTPBasicAuth(username, password)
|
login_url, auth=requests.auth.HTTPBasicAuth(username, password)
|
||||||
)
|
)
|
||||||
response.raise_for_status()
|
response.raise_for_status()
|
||||||
if response.json().get("auth") == "Success":
|
response_data = response.json()
|
||||||
|
if response_data.get("auth") == "Success":
|
||||||
session["logged_in"] = True
|
session["logged_in"] = True
|
||||||
session["username"] = username
|
session["username"] = response_data.get("username", username)
|
||||||
|
session["user_id"] = response_data.get("user_id")
|
||||||
session["auth_credentials"] = encoded_credentials
|
session["auth_credentials"] = encoded_credentials
|
||||||
next_url = request.args.get("next")
|
next_url = request.args.get("next")
|
||||||
if next_url:
|
if next_url:
|
||||||
@ -167,23 +204,23 @@ def login() -> Union[Response, str]:
|
|||||||
return render_template("index.html", error=error)
|
return render_template("index.html", error=error)
|
||||||
|
|
||||||
@app.route("/urls", methods=["GET"])
|
@app.route("/urls", methods=["GET"])
|
||||||
@cache.cached(timeout=300)
|
@cache.cached(timeout=300, key_prefix=make_cache_key)
|
||||||
def urls() -> Union[Response, str]:
|
def urls() -> Union[Response, str]:
|
||||||
"""Renders the URLs page."""
|
"""Renders the URLs page."""
|
||||||
if not session.get("logged_in"):
|
if not session.get("logged_in"):
|
||||||
return redirect(url_for("home"))
|
return redirect(url_for("home"))
|
||||||
base_url = app.config["BASE_URL"]
|
base_url = app.config["BACKEND_URL"]
|
||||||
return render_template(
|
return render_template(
|
||||||
"urls.html", urls=get_urls(base_url, session["auth_credentials"])
|
"urls.html", urls=get_urls(base_url, session["auth_credentials"])
|
||||||
)
|
)
|
||||||
|
|
||||||
@app.route("/accounts", methods=["GET"])
|
@app.route("/accounts", methods=["GET"])
|
||||||
@cache.cached(timeout=60)
|
@cache.cached(timeout=60, key_prefix=make_cache_key)
|
||||||
def user_accounts() -> Union[Response, str]:
|
def user_accounts() -> Union[Response, str]:
|
||||||
"""Renders the user accounts page."""
|
"""Renders the user accounts page."""
|
||||||
if not session.get("logged_in"):
|
if not session.get("logged_in"):
|
||||||
return redirect(url_for("home"))
|
return redirect(url_for("home"))
|
||||||
base_url = app.config["BASE_URL"]
|
base_url = app.config["BACKEND_URL"]
|
||||||
user_accounts_data = get_user_accounts(base_url, session["auth_credentials"])
|
user_accounts_data = get_user_accounts(base_url, session["auth_credentials"])
|
||||||
return render_template(
|
return render_template(
|
||||||
"user_accounts.html",
|
"user_accounts.html",
|
||||||
@ -205,7 +242,7 @@ def add_account() -> Union[Response, str]:
|
|||||||
"""Handles adding a new user account."""
|
"""Handles adding a new user account."""
|
||||||
if not session.get("logged_in"):
|
if not session.get("logged_in"):
|
||||||
return redirect(url_for("index", next=request.url))
|
return redirect(url_for("index", next=request.url))
|
||||||
base_url = app.config["BASE_URL"]
|
base_url = app.config["BACKEND_URL"]
|
||||||
shared_text = request.args.get('shared_text')
|
shared_text = request.args.get('shared_text')
|
||||||
|
|
||||||
if request.method == "POST":
|
if request.method == "POST":
|
||||||
@ -215,12 +252,14 @@ def add_account() -> Union[Response, str]:
|
|||||||
if add_user_account(
|
if add_user_account(
|
||||||
base_url, session["auth_credentials"], username, password, stream
|
base_url, session["auth_credentials"], username, password, stream
|
||||||
):
|
):
|
||||||
cache.delete_memoized(user_accounts)
|
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 redirect(url_for("user_accounts"))
|
||||||
|
|
||||||
return render_template(
|
return render_template(
|
||||||
"add_account.html",
|
"add_account.html",
|
||||||
ocr_enabled=app.config.get("OCR_ENABLED"),
|
|
||||||
text_input_enabled=app.config.get("TEXT_INPUT_ENABLED"),
|
text_input_enabled=app.config.get("TEXT_INPUT_ENABLED"),
|
||||||
shared_text=shared_text
|
shared_text=shared_text
|
||||||
)
|
)
|
||||||
@ -230,15 +269,15 @@ def delete_account() -> Response:
|
|||||||
"""Handles deleting a user account."""
|
"""Handles deleting a user account."""
|
||||||
stream = request.form.get("stream")
|
stream = request.form.get("stream")
|
||||||
username = request.form.get("username")
|
username = request.form.get("username")
|
||||||
base_url = app.config["BASE_URL"]
|
base_url = app.config["BACKEND_URL"]
|
||||||
delete_user_account(base_url, session["auth_credentials"], stream, username)
|
delete_user_account(base_url, session["auth_credentials"], stream, username)
|
||||||
cache.delete_memoized(user_accounts)
|
cache.delete_memoized(user_accounts, key_prefix=make_cache_key)
|
||||||
return redirect(url_for("user_accounts"))
|
return redirect(url_for("user_accounts"))
|
||||||
|
|
||||||
@app.route("/validateAccount", methods=["POST"])
|
@app.route("/validateAccount", methods=["POST"])
|
||||||
def validate_account() -> Tuple[Response, int]:
|
def validate_account() -> Tuple[Response, int]:
|
||||||
"""Forwards account validation requests to the backend."""
|
"""Forwards account validation requests to the backend."""
|
||||||
base_url = app.config["BASE_URL"]
|
base_url = app.config["BACKEND_URL"]
|
||||||
validate_url = f"{base_url}/validateAccount"
|
validate_url = f"{base_url}/validateAccount"
|
||||||
credentials = base64.b64decode(session["auth_credentials"]).decode()
|
credentials = base64.b64decode(session["auth_credentials"]).decode()
|
||||||
username, password = credentials.split(":", 1)
|
username, password = credentials.split(":", 1)
|
||||||
@ -252,7 +291,10 @@ def validate_account() -> Tuple[Response, int]:
|
|||||||
response.raise_for_status()
|
response.raise_for_status()
|
||||||
response_data = response.json()
|
response_data = response.json()
|
||||||
if response_data.get("message") == "Account is valid and updated":
|
if response_data.get("message") == "Account is valid and updated":
|
||||||
cache.delete_memoized(user_accounts)
|
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
|
return jsonify(response_data), response.status_code
|
||||||
except requests.exceptions.RequestException as e:
|
except requests.exceptions.RequestException as e:
|
||||||
return jsonify({"error": str(e)}), 500
|
return jsonify({"error": str(e)}), 500
|
||||||
@ -262,34 +304,247 @@ def stream_names() -> Union[Response, str]:
|
|||||||
"""Fetches and returns stream names as JSON."""
|
"""Fetches and returns stream names as JSON."""
|
||||||
if not session.get("logged_in"):
|
if not session.get("logged_in"):
|
||||||
return redirect(url_for("home"))
|
return redirect(url_for("home"))
|
||||||
base_url = app.config["BASE_URL"]
|
base_url = app.config["BACKEND_URL"]
|
||||||
return jsonify(get_stream_names(base_url, session["auth_credentials"]))
|
return jsonify(get_stream_names(base_url, session["auth_credentials"]))
|
||||||
|
|
||||||
if app.config.get("OCR_ENABLED"):
|
|
||||||
@app.route('/OCRupload', methods=['POST'])
|
@app.route('/config')
|
||||||
def ocr_upload() -> Union[Response, str, Tuple[Response, int]]:
|
def config():
|
||||||
"""Handles image uploads for OCR processing."""
|
"""Handles access to the configuration page."""
|
||||||
if 'image' not in request.files:
|
if session.get('user_id') and int(session.get('user_id')) == 1:
|
||||||
return jsonify({"error": "No image file found"}), 400
|
return redirect(url_for('config_dashboard'))
|
||||||
file = request.files['image']
|
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:
|
try:
|
||||||
image = Image.open(file.stream)
|
response = requests.post(backend_url)
|
||||||
image_np = np.array(image)
|
return Response(response.content, status=response.status_code, mimetype=response.headers['Content-Type'])
|
||||||
result = ocr.ocr(image_np)
|
except requests.exceptions.RequestException as e:
|
||||||
extracted_text = [line[1][0] for line in result[0]]
|
return jsonify({"error": str(e)}), 502
|
||||||
# Basic validation
|
|
||||||
if len(extracted_text) >= 4:
|
|
||||||
return render_template(
|
@app.route('/dns', methods=['GET', 'POST', 'DELETE'])
|
||||||
"add_account.html",
|
def proxy_dns():
|
||||||
username=extracted_text[2],
|
"""Proxies DNS management requests to the backend."""
|
||||||
password=extracted_text[3],
|
if not session.get('user_id') or int(session.get('user_id')) != 1:
|
||||||
ocr_enabled=True,
|
return jsonify({'error': 'Unauthorized'}), 401
|
||||||
text_input_enabled=app.config.get("TEXT_INPUT_ENABLED")
|
|
||||||
)
|
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:
|
else:
|
||||||
return jsonify({"error": "Could not extract required fields from image"}), 400
|
print(f"Failed to login: {response.text}")
|
||||||
except Exception as e:
|
exit(1)
|
||||||
return jsonify({"error": str(e)}), 500
|
|
||||||
|
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
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
app.run(
|
app.run(
|
||||||
|
@ -9,12 +9,6 @@ RUN pip install --no-cache-dir --prefix=/install -r requirements.txt
|
|||||||
# Final stage
|
# Final stage
|
||||||
FROM python:3.11-slim-bookworm AS final
|
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
|
WORKDIR /app
|
||||||
|
|
||||||
|
BIN
requirements.txt
BIN
requirements.txt
Binary file not shown.
@ -22,6 +22,9 @@ self.addEventListener('install', function(event) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
self.addEventListener('push', 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 data = event.data.json();
|
||||||
const options = {
|
const options = {
|
||||||
body: data.body,
|
body: data.body,
|
||||||
|
@ -34,3 +34,39 @@ div.awesomplete {
|
|||||||
display: block;
|
display: block;
|
||||||
width: 100%;
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -6,8 +6,7 @@
|
|||||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/awesomplete/1.1.5/awesomplete.min.css" />
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/awesomplete/1.1.5/awesomplete.min.css" />
|
||||||
<style>
|
<style>
|
||||||
/* Hide the spinner by default */
|
/* Hide the spinner by default */
|
||||||
#loadingSpinner,
|
#loadingSpinner {
|
||||||
#ocrLoadingSpinner {
|
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
@ -50,20 +49,6 @@
|
|||||||
<span id="buttonText">Add Account</span>
|
<span id="buttonText">Add Account</span>
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</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 %}
|
{% if text_input_enabled %}
|
||||||
<hr>
|
<hr>
|
||||||
<h2>Load Details Via Text</h2>
|
<h2>Load Details Via Text</h2>
|
||||||
@ -82,11 +67,6 @@
|
|||||||
document.getElementById("loadingSpinner").style.display = "inline-block";
|
document.getElementById("loadingSpinner").style.display = "inline-block";
|
||||||
document.getElementById("buttonText").textContent = "Working...";
|
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() {
|
document.addEventListener("DOMContentLoaded", function() {
|
||||||
var streamInput = document.getElementById("stream");
|
var streamInput = document.getElementById("stream");
|
||||||
|
@ -12,7 +12,7 @@
|
|||||||
<link rel="apple-touch-icon" sizes="180x180" href="{{ url_for('static', filename='apple-touch-icon.png') }}" />
|
<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" />
|
<meta name="apple-mobile-web-app-title" content="kTvManager" />
|
||||||
<link rel="manifest" href="{{ url_for('static', filename='site.webmanifest') }}?v={{ version }}" />
|
<link rel="manifest" href="{{ url_for('static', filename='site.webmanifest') }}?v={{ version }}" />
|
||||||
<link rel="stylesheet" href="{{ url_for('static', filename='styles.css') }}" />
|
<link rel="stylesheet" href="{{ url_for('static', filename='styles.css') }}?v={{ version }}" />
|
||||||
{% block head_content %}{% endblock %}
|
{% block head_content %}{% endblock %}
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
@ -46,25 +46,64 @@
|
|||||||
</main>
|
</main>
|
||||||
|
|
||||||
<footer class="bg-dark text-white text-center py-3 mt-5">
|
<footer class="bg-dark text-white text-center py-3 mt-5">
|
||||||
<p>Version: {{ version }}</p>
|
<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>
|
||||||
</footer>
|
</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://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://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>
|
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/js/bootstrap.min.js"></script>
|
||||||
{% block scripts %}{% endblock %}
|
{% block scripts %}{% endblock %}
|
||||||
<script>
|
<script>
|
||||||
if ('serviceWorker' in navigator && 'PushManager' in window) {
|
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) {
|
navigator.serviceWorker.register('{{ url_for("static", filename="service-worker.js") }}').then(function(registration) {
|
||||||
console.log('ServiceWorker registration successful with scope: ', registration.scope);
|
console.log('ServiceWorker registration successful with scope: ', registration.scope);
|
||||||
// Check for the 'loggedin' query parameter to trigger the prompt
|
const enableNotificationsBtn = document.getElementById('enable-notifications-btn');
|
||||||
const urlParams = new URLSearchParams(window.location.search);
|
|
||||||
if (urlParams.has('loggedin')) {
|
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);
|
askPermission(registration);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (enableNotificationsBtn) {
|
||||||
|
setupNotificationButton();
|
||||||
}
|
}
|
||||||
}, function(err) {
|
}, function(err) {
|
||||||
console.log('ServiceWorker registration failed: ', 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) {
|
function askPermission(registration) {
|
||||||
@ -79,7 +118,10 @@
|
|||||||
fetch('/vapid-public-key')
|
fetch('/vapid-public-key')
|
||||||
.then(response => {
|
.then(response => {
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
throw new Error('Failed to fetch VAPID public key');
|
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();
|
return response.json();
|
||||||
})
|
})
|
||||||
@ -101,13 +143,33 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function saveSubscription(subscription) {
|
function saveSubscription(subscription) {
|
||||||
|
console.log('Attempting to save subscription...');
|
||||||
fetch('/save-subscription', {
|
fetch('/save-subscription', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json'
|
||||||
'Authorization': 'Basic {{ session.auth_credentials }}'
|
|
||||||
},
|
},
|
||||||
body: JSON.stringify(subscription)
|
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);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
301
templates/config_dashboard.html
Normal file
301
templates/config_dashboard.html
Normal file
@ -0,0 +1,301 @@
|
|||||||
|
{% 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 %}
|
@ -4,7 +4,7 @@
|
|||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<h1>Welcome {{ username }}!</h1>
|
<h1>Welcome {{ username }}!</h1>
|
||||||
<br>
|
<button id="enable-notifications-btn" class="btn btn-primary my-3">Enable Notifications</button>
|
||||||
<h2>You have {{ accounts }} active accounts</h2>
|
<h2>You have {{ accounts }} active accounts</h2>
|
||||||
<br>
|
<br>
|
||||||
|
|
||||||
|
@ -25,8 +25,8 @@
|
|||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<h2>{{ username }}'s Accounts</h2>
|
<h2>{{ username }}'s Accounts</h2>
|
||||||
<div class="table-responsive">
|
<div>
|
||||||
<table class="table table-striped" id="accountsTable">
|
<table class="table table-striped dt-responsive nowrap" id="accountsTable" style="width:100%">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<!-- <th>#</th> -->
|
<!-- <th>#</th> -->
|
||||||
|
164
test_npm_update.py
Normal file
164
test_npm_update.py
Normal file
@ -0,0 +1,164 @@
|
|||||||
|
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()
|
Loading…
x
Reference in New Issue
Block a user