From c53a42b5c90890317c3af6d918a4332beea3cf93 Mon Sep 17 00:00:00 2001 From: Nathan Woodburn Date: Fri, 25 Jul 2025 11:45:19 +1000 Subject: [PATCH] feat: Add webserver routes and account logic --- alerts.py | 27 ++++++++ domains.py | 19 ++++++ server.py | 113 +++++++++++++++++++++++++++++++++ templates/account.html | 92 +++++++++++++++++++++++++++ templates/assets/css/index.css | 112 ++++++++++++++++++++++++++++++++ templates/index.html | 6 +- 6 files changed, 367 insertions(+), 2 deletions(-) create mode 100644 templates/account.html diff --git a/alerts.py b/alerts.py index 05e817f..271efc5 100644 --- a/alerts.py +++ b/alerts.py @@ -15,6 +15,33 @@ SMTP_PORT = int(os.getenv('SMTP_PORT', 465)) SMTP_USERNAME = os.getenv('SMTP_USERNAME', None) SMTP_PASSWORD = os.getenv('SMTP_PASSWORD', None) +NOTIFICATION_TYPES = [ + { + "type": "discord_webhook", + "fields": [ + { + "name": "url", + "label": "Discord Webhook URL", + "type": "text", + "required": True + } + ], + "description": "Send a notification to a Discord channel via webhook." + }, + { + "type": "email", + "fields": [ + { + "name": "email", + "label": "Email Address", + "type": "email", + "required": True + } + ], + "description": "Send an email notification." + } +] + def handle_alert(domain: str, notification: dict, alert_data: dict): """ diff --git a/domains.py b/domains.py index 04d304f..8c60fbe 100644 --- a/domains.py +++ b/domains.py @@ -115,6 +115,25 @@ def update_notification(domain: str, notification: dict): with open('data/domains.json', 'w') as f: json.dump(domains, f, indent=4) +def delete_notification(notification_id: str, user_name: str): + """ + Delete a notification for a domain. + """ + domains = get_domains() + domains_to_delete = [] + + for domain in domains: + domains[domain] = [n for n in domains[domain] if n['id'] != notification_id or n.get('user_name') != user_name] + if not domains[domain]: + domains_to_delete.append(domain) + + # Remove empty domains after iteration + for domain in domains_to_delete: + del domains[domain] + + with open('data/domains.json', 'w') as f: + json.dump(domains, f, indent=4) + def get_account_notifications(user_name: str) -> list: """ Get all notifications for a specific account. diff --git a/server.py b/server.py index 692d8df..2976b11 100644 --- a/server.py +++ b/server.py @@ -18,6 +18,7 @@ import dotenv import threading import time import domains +from alerts import NOTIFICATION_TYPES dotenv.load_dotenv() @@ -94,6 +95,118 @@ def index(): return render_template("index.html") +@app.route("/account") +def account(): + # Check if the user is logged in + # Check for token cookie + token = request.cookies.get("token") + if not token: + return redirect(f"https://login.hns.au/auth?return={request.host_url}login") + + user_data = requests.get(f"https://login.hns.au/auth/user?token={token}") + if user_data.status_code != 200: + return redirect(f"https://login.hns.au/auth?return={request.host_url}login") + user_data = user_data.json() + + notifications = domains.get_account_notifications(user_data["username"]) + if not notifications: + return render_template("account.html", user=user_data, domains=[], NOTIFICATION_TYPES=NOTIFICATION_TYPES) + + + return render_template("account.html", user=user_data, notifications=notifications, NOTIFICATION_TYPES=NOTIFICATION_TYPES) + +@app.route("/login") +def login(): + # Check if token parameter is present + token = request.args.get("token") + if not token: + return redirect(f"https://login.hns.au/auth?return={request.host_url}login") + # Set token cookie + response = make_response(redirect(f"{request.host_url}account")) + response.set_cookie("token", token, httponly=True, secure=True) + return response + +@app.route("/logout") +def logout(): + # Clear the token cookie + response = make_response(redirect(f"{request.host_url}")) + response.set_cookie("token", "", expires=0, httponly=True, secure=True) + return response + + +@app.route("/notification/", methods=["POST"]) +def addNotification(notificationtype: str): + """ + Add a notification for a domain. + """ + + token = request.cookies.get("token") + if not token: + return redirect(f"https://login.hns.au/auth?return={request.host_url}login") + + user_data = requests.get(f"https://login.hns.au/auth/user?token={token}") + if user_data.status_code != 200: + return redirect(f"https://login.hns.au/auth?return={request.host_url}login") + user_data = user_data.json() + username = user_data.get("username", None) + if not username: + return jsonify({"error": "Invalid user data"}), 400 + + notificationType = None + for notification in NOTIFICATION_TYPES: + if notification['type'] == notificationtype: + notificationType = notification + break + else: + return jsonify({"error": "Invalid notification type"}), 400 + + # Get form data + data = request.form.to_dict() + if not data or 'domain' not in data or 'blocks' not in data: + return jsonify({"error": "Invalid request data"}), 400 + + domain = data['domain'] + + for field in notificationType['fields']: + if field['name'] not in data and field.get('required', False): + return jsonify({"error": f"Missing field: {field['name']}"}), 400 + + + notification = data + + notification['type'] = notificationtype + notification['id'] = os.urandom(16).hex() # Generate a random ID for the notification + notification['user_name'] = username + # Delete domain duplicate from data + notification.pop('domain', None) + + domains.add_notification(domain, notification) + return redirect(f"{request.host_url}account") + +@app.route("/notification/delete/") +def delete_notification(notification_id: str): + """ + Delete a notification by its ID. + """ + token = request.cookies.get("token") + if not token: + return redirect(f"https://login.hns.au/auth?return={request.host_url}login") + + user_data = requests.get(f"https://login.hns.au/auth/user?token={token}") + if user_data.status_code != 200: + return redirect(f"https://login.hns.au/auth?return={request.host_url}login") + user_data = user_data.json() + + domains.delete_notification(notification_id, user_data['username']) + return redirect(f"{request.host_url}account") + + +@app.route("/account/") +def account_domain(domain: str): + # TODO - Implement account domain logic + return redirect(f"/account") + + @app.route("/") def catch_all(path: str): if os.path.isfile("templates/" + path): diff --git a/templates/account.html b/templates/account.html new file mode 100644 index 0000000..48afcc4 --- /dev/null +++ b/templates/account.html @@ -0,0 +1,92 @@ + + + + + + + FireAlerts + + + + + +
+ Logged in as {{user.username}} + Logout +
+ +

FireAlerts

+ +
+

Your Alerts

+
+ + + {% if notifications %} +
+ {% for notification in notifications %} +
+

Domain: {{notification.domain}}

+

Type: {{notification.notification.type.replace('_', ' ').title()}}

+

Blocks before expiry: {{notification.notification.blocks}}

+ {% for notificationType in NOTIFICATION_TYPES %} + {% if notificationType.type == notification.notification.type %} + {% for field in notificationType.fields %} +

{{field.label}}: {{notification.notification[field.name]}}

+ {% endfor %} + {% endif %} + {% endfor %} + + + Delete +
+ {% endfor %} +
+ {% else %} +
+

You have no notifications set up yet.

+

Use the forms below to add new notifications.

+
+ + {% endif %} + +
+

Setup your alerts below:

+
+ + {% for notificationType in NOTIFICATION_TYPES %} +
+

{{notificationType.description}}

+
+ +
+ + +
+ + + {% for field in notificationType.fields %} +
+ + +
+ {% endfor %} + + +
+ + +
+ + +
+
+ {% endfor %} + + + \ No newline at end of file diff --git a/templates/assets/css/index.css b/templates/assets/css/index.css index 9635f9c..d170ffe 100644 --- a/templates/assets/css/index.css +++ b/templates/assets/css/index.css @@ -17,4 +17,116 @@ a { } a:hover { text-decoration: underline; +} +.button { + display: inline-block; + padding: 10px 20px; + background-color: #ffffff; + color: #000000; + border-radius: 5px; + transition: background-color 0.3s ease; +} +.right { + float: right; +} +span.user { + font-weight: bold; + color: #ffffff; + margin-inline-end: 5px; +} + +.notification-form { + background-color: #1a1a1a; + border: 1px solid #333333; + border-radius: 8px; + padding: 20px; + margin: 20px auto; + max-width: 500px; +} + +.notification-form h3 { + margin-top: 0; + margin-bottom: 15px; + color: #ffffff; +} + +.form-group { + margin-bottom: 15px; +} + +.form-group label { + display: block; + margin-bottom: 5px; + font-weight: bold; + color: #ffffff; +} + +.form-group input { + width: 100%; + padding: 10px; + background-color: #333333; + border: 1px solid #555555; + border-radius: 4px; + color: #ffffff; + font-size: 14px; + box-sizing: border-box; +} + +.form-group input:focus { + outline: none; + border-color: #ffffff; + background-color: #444444; +} + +.notifications-list { + max-width: 800px; + margin: 30px auto; + padding: 0 20px; +} + +.notifications-list h2 { + color: #ffffff; + text-align: center; + margin-bottom: 20px; +} + +.notification-item { + background-color: #1a1a1a; + border: 1px solid #333333; + border-radius: 8px; + padding: 15px; + margin-bottom: 15px; +} + +.notification-item h4 { + margin-top: 0; + margin-bottom: 10px; + color: #ffffff; + font-size: 18px; +} + +.notification-item p { + margin: 5px 0; + color: #cccccc; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.notification-item p strong { + color: #ffffff; +} + +.no-notifications { + text-align: center; + max-width: 500px; + margin: 30px auto; + color: #cccccc; +} + +/* Specific handling for long URLs and IDs */ +.notification-item p:contains("URL"), +.notification-item p:contains("ID") { + font-family: monospace; + font-size: 12px; } \ No newline at end of file diff --git a/templates/index.html b/templates/index.html index bb349f8..d4419b4 100644 --- a/templates/index.html +++ b/templates/index.html @@ -4,7 +4,7 @@ - Nathan.Woodburn/ + FireAlerts @@ -12,7 +12,9 @@
-

Nathan.Woodburn/

+

FireAlerts

+

Get alerted before your Handshake domains expire.

+ Account