From b69a8aee0fc01bb87d15529a960b73f88b61cb36 Mon Sep 17 00:00:00 2001 From: Minecon724 Date: Thu, 3 Apr 2025 17:52:55 +0200 Subject: [PATCH] Refactor a bit --- src/anonchat/__init__.py | 19 +-- src/anonchat/admin_routes.py | 112 +++++++++--------- src/anonchat/error_handlers.py | 27 +++++ src/anonchat/models/admin.py | 1 - src/anonchat/routes.py | 18 +-- src/anonchat/static/css/styles.css | 4 +- src/anonchat/templates/404.html | 14 --- .../dashboard.html} | 12 +- .../{admin_login.html => admin/login.html} | 12 +- src/anonchat/templates/admin/settings.html | 95 +++++++++++++++ src/anonchat/templates/admin_settings.html | 44 ------- src/anonchat/templates/admin_webhook.html | 60 ---------- src/anonchat/templates/base.html | 35 +++++- src/anonchat/templates/create_inquiry.html | 9 +- src/anonchat/templates/error.html | 9 +- src/anonchat/templates/headers.html | 24 ---- src/anonchat/templates/inquiry.html | 26 ++-- 17 files changed, 253 insertions(+), 268 deletions(-) create mode 100644 src/anonchat/error_handlers.py delete mode 100644 src/anonchat/templates/404.html rename src/anonchat/templates/{admin_dashboard.html => admin/dashboard.html} (94%) rename src/anonchat/templates/{admin_login.html => admin/login.html} (78%) create mode 100644 src/anonchat/templates/admin/settings.html delete mode 100644 src/anonchat/templates/admin_settings.html delete mode 100644 src/anonchat/templates/admin_webhook.html delete mode 100644 src/anonchat/templates/headers.html diff --git a/src/anonchat/__init__.py b/src/anonchat/__init__.py index 82eff47..c1f9775 100644 --- a/src/anonchat/__init__.py +++ b/src/anonchat/__init__.py @@ -80,6 +80,9 @@ db = SQLAlchemy(app) scheduler = APScheduler() scheduler.init_app(app) +from .admin_routes import is_admin +app.jinja_env.globals['is_admin'] = is_admin + # Import models from .models import Inquiry, Message, Settings, Admin @@ -93,22 +96,8 @@ with app.app_context(): def health_check(): return jsonify(status="healthy"), 200 -# Register error handlers -from flask import render_template +from . import error_handlers -@app.errorhandler(404) -def page_not_found(e): - return render_template('404.html'), 404 - -# Rate limit exceeded handler -@app.errorhandler(429) -def ratelimit_handler(e): - if request.path.startswith('/admin'): - return render_template('error.html', error="Rate limit exceeded. Please try again later."), 429 - elif request.is_json: - return jsonify(error="Rate limit exceeded"), 429 - else: - return render_template('error.html', error="Too many requests. Please slow down and try again later."), 429 # Import routes from . import routes diff --git a/src/anonchat/admin_routes.py b/src/anonchat/admin_routes.py index e228247..71d145d 100644 --- a/src/anonchat/admin_routes.py +++ b/src/anonchat/admin_routes.py @@ -1,8 +1,8 @@ -from flask import request, jsonify, render_template, redirect, url_for, flash, session +from flask import request, render_template, redirect, url_for, flash, session from functools import wraps from . import app, db, limiter, csrf from .models import Inquiry, Message, Settings, Admin -from .webhooks import WebhookError, inquiry_created, inquiry_closed, inquiry_reopened +from .webhooks import WebhookError, inquiry_created def is_admin(): return 'admin_authenticated' in session and session['admin_authenticated'] @@ -12,7 +12,9 @@ def admin_required(f): @wraps(f) def decorated_function(*args, **kwargs): if not is_admin(): + flash('You must be logged in to access this page', 'error') return redirect(url_for('admin_login', next=request.url)) + return f(*args, **kwargs) return decorated_function @@ -23,11 +25,11 @@ def admin(): return redirect(url_for('admin_login')) @app.route('/admin/login', methods=['GET']) -def admin_login_get(): +def admin_login(): next_page = request.args.get('next') or url_for('admin_dashboard') if is_admin(): return redirect(next_page) - return render_template('admin_login.html', next=next_page) + return render_template('admin/login.html', next=next_page) @app.route('/admin/login', methods=['POST']) @limiter.limit("1 per minute", deduct_when=lambda response: not is_admin()) @@ -43,7 +45,7 @@ def admin_login_post(): if admin and admin.verify_password(password): session['admin_authenticated'] = True session['admin_username'] = admin.username - + # Redirect to next page if provided, otherwise to dashboard next_page = request.args.get('next') if next_page: @@ -80,67 +82,71 @@ def admin_dashboard(): # Sort by latest message timestamp (newest first) inquiries_with_data.sort(key=lambda x: x['latest_message'].timestamp if x['latest_message'] else None, reverse=True) - return render_template('admin_dashboard.html', inquiries_with_data=inquiries_with_data) + return render_template('admin/dashboard.html', inquiries_with_data=inquiries_with_data) -@app.route('/admin/webhook', methods=['GET', 'POST']) +@app.route('/admin/settings/webhook', methods=['POST']) @admin_required -def admin_webhook(): +def admin_settings_webhook(): settings = Settings.query.first() if not settings: settings = Settings() db.session.add(settings) - db.session.commit() - if request.method == 'POST': - # Update webhook settings - settings.webhook_enabled = request.form.get('webhook_enabled') == 'on' - settings.webhook_url = request.form.get('webhook_url', '') - settings.webhook_secret = request.form.get('webhook_secret', '') - db.session.commit() + # Update webhook settings + settings.webhook_enabled = request.form.get('webhook_enabled') == 'on' + settings.webhook_url = request.form.get('webhook_url', '') + settings.webhook_secret = request.form.get('webhook_secret', '') - # Test webhook if enabled and URL is provided - if settings.webhook_enabled and settings.webhook_url: - try: - inquiry_created('1234abcd1234abcd', 'This is a test message') - flash('Webhook settings saved and test sent successfully') - except WebhookError as e: - flash(f'Webhook test failed: {str(e)}') - else: - flash('Webhook settings saved') - - return redirect(url_for('admin_webhook')) - - return render_template('admin_webhook.html', settings=settings) + # Test webhook if enabled and URL is provided + if settings.webhook_enabled and settings.webhook_url: + try: + inquiry_created('1234abcd1234abcd', 'This is a test message') + flash('Webhook settings saved and test sent successfully') + except WebhookError as e: + flash(f'Webhook test failed: {str(e)}') + else: + flash('Webhook settings saved') -@app.route('/admin/settings', methods=['GET', 'POST']) + db.session.commit() + + return redirect(url_for('admin_settings')) + +@app.route('/admin/settings', methods=['GET']) @admin_required def admin_settings(): + settings = Settings.query.first() + if not settings: + settings = Settings() + db.session.add(settings) + + return render_template('admin/settings.html', settings=settings) + +@app.route('/admin/settings/password', methods=['POST']) +@admin_required +def admin_settings_password(): admin = Admin.query.filter_by(username=session['admin_username']).first() - if request.method == 'POST': - current_password = request.form.get('current_password') - new_password = request.form.get('new_password') - confirm_password = request.form.get('confirm_password') + current_password = request.form.get('current_password') + new_password = request.form.get('new_password') + confirm_password = request.form.get('confirm_password') - # Verify current password - if not admin.verify_password(current_password): - flash('Current password is incorrect', 'error') - return redirect(url_for('admin_settings')) - - # Validate new password - if new_password != confirm_password: - flash('New passwords do not match', 'error') - return redirect(url_for('admin_settings')) - - if len(new_password) < 8: - flash('Password must be at least 8 characters', 'error') - return redirect(url_for('admin_settings')) - - # Update password - admin.password_hash = Admin.hash_password(new_password) - db.session.commit() - - flash('Password updated successfully', 'success') + # Verify current password + if not admin.verify_password(current_password): + flash('Current password is incorrect', 'error') return redirect(url_for('admin_settings')) + + # Validate new password + if new_password != confirm_password: + flash('New passwords do not match', 'error') + return redirect(url_for('admin_settings')) + + if len(new_password) < 8: + flash('Password must be at least 8 characters', 'error') + return redirect(url_for('admin_settings')) + + # Update password + admin.rehash_password(new_password) - return render_template('admin_settings.html') \ No newline at end of file + flash('Password updated successfully', 'success') + return redirect(url_for('admin_settings')) + diff --git a/src/anonchat/error_handlers.py b/src/anonchat/error_handlers.py new file mode 100644 index 0000000..752be3d --- /dev/null +++ b/src/anonchat/error_handlers.py @@ -0,0 +1,27 @@ +from flask import request, render_template, jsonify +import werkzeug.exceptions + +def register_error_handlers(app): + @app.errorhandler(werkzeug.exceptions.NotFound) + def page_not_found(e): + return render_error(title="Page Not Found", message="The page you are looking for does not exist. It may have been moved or deleted.", status_code=e.code) + + # Rate limit exceeded handler + @app.errorhandler(werkzeug.exceptions.TooManyRequests) + def ratelimit_handler(e): + return render_error(title="Rate limit exceeded", message="You have made too many requests. Please try again later.", status_code=e.code) + + # CSRF error handler + @app.errorhandler(werkzeug.exceptions.BadRequest) + def handle_bad_request(e): + return render_error(title="Invalid request", message="A security error occurred. Please try again.", status_code=e.code) + + @app.errorhandler(werkzeug.exceptions.InternalServerError) + def internal_server_error(e): + return render_error(title="Internal Server Error", message="An unexpected error occurred. Please try again later.", status_code=e.code) + + def render_error(title="Error", message="An unexpected error occurred.", status_code=500): + if request.is_json or request.headers.get('X-Requested-With') == 'XMLHttpRequest': + return jsonify(error=title, message=message), status_code + else: + return render_template('error.html', title=title, message=message), status_code \ No newline at end of file diff --git a/src/anonchat/models/admin.py b/src/anonchat/models/admin.py index 3de0e9a..d775532 100644 --- a/src/anonchat/models/admin.py +++ b/src/anonchat/models/admin.py @@ -15,7 +15,6 @@ class Admin(db.Model): def rehash_password(self, password): """Rehash a password using Argon2id""" self.password_hash = self.hash_password(password) - db.session.add(self) db.session.commit() def verify_password(self, password): diff --git a/src/anonchat/routes.py b/src/anonchat/routes.py index db4fd3f..4e91c09 100644 --- a/src/anonchat/routes.py +++ b/src/anonchat/routes.py @@ -84,7 +84,7 @@ def inquiry(inquiry_id): return redirect(url_for('inquiry', inquiry_id=inquiry_id)) messages = Message.query.filter_by(inquiry_id=inquiry_id).order_by(Message.message_number).all() - return render_template('inquiry.html', inquiry=inquiry, messages=messages, is_admin=is_admin_user) + return render_template('inquiry.html', inquiry=inquiry, messages=messages) @app.route('/inquiry//delete', methods=['POST']) @limiter.limit("10 per hour", deduct_when=lambda response: response.status_code != 200) @@ -103,22 +103,6 @@ def delete_inquiry(inquiry_id): flash('Inquiry deleted successfully') return redirect(url_for('index')) -# CSRF error handler -@app.errorhandler(400) -def handle_csrf_error(e): - app.logger.error(f"CSRF error: {str(e)}") - if request.path.startswith('/admin'): - # Check if it's not an AJAX request - if not request.headers.get('X-Requested-With') == 'XMLHttpRequest': - flash("Your session has expired or the form was tampered with. Please try again.", "error") - return redirect(url_for('admin_login')) - - # For non-admin routes or AJAX requests - if request.is_json or request.headers.get('X-Requested-With') == 'XMLHttpRequest': - return jsonify(error="Invalid request. CSRF token missing or incorrect."), 400 - else: - return render_template('error.html', error="Invalid request. Please try again."), 400 - @app.route('/api/inquiry//messages', methods=['GET']) @limiter.limit("10 per hour", deduct_when=lambda response: response.status_code != 200) @limiter.limit("30 per minute") diff --git a/src/anonchat/static/css/styles.css b/src/anonchat/static/css/styles.css index dc14d00..84d2771 100644 --- a/src/anonchat/static/css/styles.css +++ b/src/anonchat/static/css/styles.css @@ -19,7 +19,7 @@ a:hover { a:visited { color: #c792ea; } -.container { +#app { width: 80%; max-width: 800px; margin: 4rem auto 2rem auto; @@ -28,6 +28,8 @@ a:visited { border-radius: 5px; box-shadow: 0 2px 5px rgba(0,0,0,0.3); } +main { +} footer { font-size: small; color: #999; diff --git a/src/anonchat/templates/404.html b/src/anonchat/templates/404.html deleted file mode 100644 index 76561bb..0000000 --- a/src/anonchat/templates/404.html +++ /dev/null @@ -1,14 +0,0 @@ -{% extends "base.html" %} - -{% block title %}404 Not Found - {{ config.SITE_TITLE }}{% endblock %} - -{% block content %} -
-
404
-

Page Not Found

-

Sorry, the page you're looking for doesn't exist or may have been moved.

- -
-{% endblock %} \ No newline at end of file diff --git a/src/anonchat/templates/admin_dashboard.html b/src/anonchat/templates/admin/dashboard.html similarity index 94% rename from src/anonchat/templates/admin_dashboard.html rename to src/anonchat/templates/admin/dashboard.html index a99bc6c..2bf4a4d 100644 --- a/src/anonchat/templates/admin_dashboard.html +++ b/src/anonchat/templates/admin/dashboard.html @@ -1,14 +1,12 @@ {% extends 'base.html' %} -{% from 'headers.html' import admin_header, flash_messages %} -{% block title %}Admin Dashboard - {{ config.SITE_TITLE }}{% endblock %} +{% block title %}Admin Dashboard{% endblock %} + +{% block header %} +

Admin Dashboard

+{% endblock %} {% block content %} -

Admin Dashboard

- - {{ admin_header() }} - {{ flash_messages() }} -

All Inquiries

{% if inquiries_with_data %} diff --git a/src/anonchat/templates/admin_login.html b/src/anonchat/templates/admin/login.html similarity index 78% rename from src/anonchat/templates/admin_login.html rename to src/anonchat/templates/admin/login.html index dcdcdad..4d6fe3d 100644 --- a/src/anonchat/templates/admin_login.html +++ b/src/anonchat/templates/admin/login.html @@ -1,12 +1,12 @@ {% extends 'base.html' %} -{% from 'headers.html' import flash_messages %} -{% block title %}Admin Login - {{ config.SITE_TITLE }}{% endblock %} + +{% block title %}Admin Login{% endblock %} + +{% block header %} +

Admin Login

+{% endblock %} {% block content %} -

Admin Login

- - {{ flash_messages() }} -
diff --git a/src/anonchat/templates/admin/settings.html b/src/anonchat/templates/admin/settings.html new file mode 100644 index 0000000..c523f29 --- /dev/null +++ b/src/anonchat/templates/admin/settings.html @@ -0,0 +1,95 @@ +{% extends 'base.html' %} + +{% block title %}Admin Settings{% endblock %} + +{% block header %} +

Admin Settings

+{% endblock %} + +{% block content %} +
+

Change Admin Password

+ + +
+ + +
+
+ + + Password must be at least 8 characters +
+
+ + +
+ + +
+ +
+ +
+

Webhook Settings

+ +
+

Configure webhook settings to receive notifications for new inquiries and responses.

+

Events will be sent as POST requests with JSON payloads. Event types include:

+
    +
  • inquiry_created - Triggered when a new inquiry is created. Contains the inquiry ID and the initial message.
  • +
  • inquiry_reopened - Triggered when an inquiry is reopened. Contains the inquiry ID.
  • +
  • inquiry_closed - Triggered when an inquiry is closed. Contains the inquiry ID.
  • +
  • inquiry_message - Triggered when a user adds a message to an existing inquiry. Contains the inquiry ID and the message content.
  • +
+
+ +
+ +
+ +
+ +
+ + +

The URL that will receive webhook events

+
+ +
+ + +

If provided, webhooks will include a signature header (X-Webhook-Signature)

+
+ + +
+ +
+

Webhook Payload Example

+
+    {
+      "event_type": "inquiry_created",
+      "timestamp": "2023-04-01T12:34:56.789Z",
+      "data": {
+        "inquiry_id": "abcdef1234567890",
+        "message": "Hello, I have a question..."
+      }
+    }
+
+
+ +
+ +
+

System Configuration

+

The following settings are configured through environment variables:

+
    +
  • Auto Delete Delay: {{ config.AUTO_DELETE_HOURS }} hours (set with AUTO_DELETE_HOURS)
  • +
+

To change these values, update your environment variables or .env file and restart the application.

+
+{% endblock %} \ No newline at end of file diff --git a/src/anonchat/templates/admin_settings.html b/src/anonchat/templates/admin_settings.html deleted file mode 100644 index 985d61e..0000000 --- a/src/anonchat/templates/admin_settings.html +++ /dev/null @@ -1,44 +0,0 @@ -{% extends 'base.html' %} - -{% from 'headers.html' import admin_header, flash_messages %} - -{% block title %}Admin Settings - {{ config.SITE_TITLE }}{% endblock %} - -{% block content %} -

Admin Settings

- - {{ admin_header() }} - {{ flash_messages() }} - -
-

Change Admin Password

-
- -
- - -
-
- - - Password must be at least 8 characters -
-
- - -
- -
-
- -
- -
-

System Configuration

-

The following settings are configured through environment variables:

-
    -
  • Auto Delete Delay: {{ config.AUTO_DELETE_HOURS }} hours (set with AUTO_DELETE_HOURS)
  • -
-

To change these values, update your environment variables or .env file and restart the application.

-
-{% endblock %} \ No newline at end of file diff --git a/src/anonchat/templates/admin_webhook.html b/src/anonchat/templates/admin_webhook.html deleted file mode 100644 index 88d8e57..0000000 --- a/src/anonchat/templates/admin_webhook.html +++ /dev/null @@ -1,60 +0,0 @@ -{% extends 'base.html' %} -{% from 'headers.html' import admin_header, flash_messages %} - -{% block title %}Webhook Settings - {{ config.SITE_TITLE }}{% endblock %} - -{% block content %} -

Webhook Settings

- - {{ admin_header() }} - {{ flash_messages() }} - -
-

Configure webhook settings to receive notifications for new inquiries and responses.

-

Events will be sent as POST requests with JSON payloads. Event types include:

-
    -
  • inquiry_created - Triggered when a new inquiry is created. Contains the inquiry ID and the initial message.
  • -
  • inquiry_reopened - Triggered when an inquiry is reopened. Contains the inquiry ID.
  • -
  • inquiry_closed - Triggered when an inquiry is closed. Contains the inquiry ID.
  • -
  • inquiry_message - Triggered when a user adds a message to an existing inquiry. Contains the inquiry ID and the message content.
  • -
-
- -
- -
- -
- -
- - -

The URL that will receive webhook events

-
- -
- - -

If provided, webhooks will include a signature header (X-Webhook-Signature)

-
- - -
- -
-

Webhook Payload Example

-
-{
-  "event_type": "inquiry_created",
-  "timestamp": "2023-04-01T12:34:56.789Z",
-  "data": {
-    "inquiry_id": "abcdef1234567890",
-    "message": "Hello, I have a question..."
-  }
-}
-        
-
-{% endblock %} \ No newline at end of file diff --git a/src/anonchat/templates/base.html b/src/anonchat/templates/base.html index 59f6cfb..16962b5 100644 --- a/src/anonchat/templates/base.html +++ b/src/anonchat/templates/base.html @@ -2,13 +2,42 @@ - - {% block title %}{{ config.SITE_TITLE }}{% endblock %} + + {% block title %}{% endblock %} - {{ config.SITE_TITLE }} -
+
+
+ {% block header %}{% endblock %} + + +
+
+ {% with messages = get_flashed_messages(with_categories=true) %} + {% if messages %} + {% for category, message in messages %} +
+ {{ message }} +
+ {% endfor %} + {% endif %} + {% endwith %} + {% block content %}{% endblock %}
diff --git a/src/anonchat/templates/create_inquiry.html b/src/anonchat/templates/create_inquiry.html index 97a0af7..9b761c1 100644 --- a/src/anonchat/templates/create_inquiry.html +++ b/src/anonchat/templates/create_inquiry.html @@ -1,16 +1,15 @@ {% extends "base.html" %} -{% from 'headers.html' import flash_messages %} +{% block title %}Create New Inquiry{% endblock %} -{% block title %}Create New Inquiry - {{ config.SITE_TITLE }}{% endblock %} +{% block header %} +

Create New Inquiry

+{% endblock %} {% block content %}
-

Start a New Conversation

Create a new inquiry to start an anonymous conversation.

- {{ flash_messages() }} -
diff --git a/src/anonchat/templates/error.html b/src/anonchat/templates/error.html index 64687e7..2e54cc4 100644 --- a/src/anonchat/templates/error.html +++ b/src/anonchat/templates/error.html @@ -1,12 +1,15 @@ {% extends "base.html" %} -{% block title %}Error - {{ config.SITE_TITLE }}{% endblock %} +{% block title %}{{ title }}{% endblock %} + +{% block header %} +

{{ title }}

+{% endblock %} {% block content %}
Error
-

Something went wrong

-

{{ error | default("An unexpected error occurred.") }}

+

{{ message }}

Go to Home {% if request.referrer %} diff --git a/src/anonchat/templates/headers.html b/src/anonchat/templates/headers.html deleted file mode 100644 index 7ff3bcf..0000000 --- a/src/anonchat/templates/headers.html +++ /dev/null @@ -1,24 +0,0 @@ -{% macro admin_header() %} - -{% endmacro %} - -{% macro flash_messages() %} - {% with messages = get_flashed_messages(with_categories=true) %} - {% if messages %} - {% for category, message in messages %} -
- {{ message }} -
- {% endfor %} - {% endif %} - {% endwith %} -{% endmacro %} \ No newline at end of file diff --git a/src/anonchat/templates/inquiry.html b/src/anonchat/templates/inquiry.html index 3eb526c..c86e738 100644 --- a/src/anonchat/templates/inquiry.html +++ b/src/anonchat/templates/inquiry.html @@ -1,25 +1,21 @@ {% extends "base.html" %} -{% from 'headers.html' import admin_header, flash_messages %} +{% block title %}Inquiry #{{ inquiry.id[:6] }}{% endblock %} -{% block title %}{% if is_admin %}Admin View - {% endif %}Inquiry #{{ inquiry.id[:6] }} - {{ config.SITE_TITLE }}{% endblock %} +{% block header %} +

+ Inquiry #{{ inquiry.id[:6] }} + + {% if inquiry.is_closed %} + CLOSED + {% endif %} +

+{% endblock %} {% block content %}
-

Inquiry #{{ inquiry.id[:6] }} - {% if inquiry.is_closed %} - CLOSED - {% endif %} -

- - {% if is_admin %} - {{ admin_header() }} - {% endif %} - - {{ flash_messages() }} - {% if not is_admin %} -

This is your conversation link: {{ request.url }}

+

This is your inquiry link: {{ request.url }}

Important: Do not share this link with anyone else, as anyone you share it with could access this chat.

{% endif %}