From 65fb1879b61ce5a3a1b2a23df183f4d96f326230 Mon Sep 17 00:00:00 2001 From: Minecon724 Date: Wed, 2 Apr 2025 13:20:30 +0200 Subject: [PATCH] Refactor admin inquiry handling and password management; integrate Argon2 for password hashing, update session configuration, and enhance admin dashboard templates. Remove deprecated admin inquiry template. --- fly.toml | 2 +- pyproject.toml | 3 +- src/anonchat/__init__.py | 19 ++++++-- src/anonchat/models.py | 21 +++++--- src/anonchat/routes.py | 54 ++++++++------------- src/anonchat/templates/admin_dashboard.html | 20 ++------ src/anonchat/templates/admin_header.html | 22 +++++++++ src/anonchat/templates/admin_inquiry.html | 52 -------------------- src/anonchat/templates/admin_settings.html | 21 ++------ src/anonchat/templates/admin_webhook.html | 21 ++------ src/anonchat/templates/inquiry.html | 37 +++++++++++--- 11 files changed, 111 insertions(+), 161 deletions(-) create mode 100644 src/anonchat/templates/admin_header.html delete mode 100644 src/anonchat/templates/admin_inquiry.html diff --git a/fly.toml b/fly.toml index 1761eff..e463334 100644 --- a/fly.toml +++ b/fly.toml @@ -21,6 +21,6 @@ primary_region = 'ams' [[vm]] size = 'shared-cpu-1x' - memory = '1gb' + memory = '256mb' cpu_kind = 'shared' cpus = 1 diff --git a/pyproject.toml b/pyproject.toml index 1d300c5..1ad558c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -18,7 +18,8 @@ dependencies = [ "gunicorn (>=23.0.0,<24.0.0)", "psycopg2-binary (>=2.9.9,<3.0.0)", "redis (>=5.0.0,<6.0.0)", - "flask-session (>=0.5.0,<1.0.0)" + "flask-session (>=0.5.0,<1.0.0)", + "argon2-cffi (>=23.0.0,<24.0.0)" ] [tool.poetry] diff --git a/src/anonchat/__init__.py b/src/anonchat/__init__.py index 4b5b8ec..7e383ba 100644 --- a/src/anonchat/__init__.py +++ b/src/anonchat/__init__.py @@ -8,6 +8,7 @@ from flask import render_template, request, jsonify from flask_wtf.csrf import CSRFProtect from flask_session import Session import redis +from argon2 import PasswordHasher # Load environment variables from .env file load_dotenv() @@ -23,7 +24,7 @@ app.config['WEBHOOK_URL'] = os.environ.get('WEBHOOK_URL', '') app.config['WEBHOOK_SECRET'] = os.environ.get('WEBHOOK_SECRET', '') # Admin configurations app.config['ADMIN_USERNAME'] = os.environ.get('ADMIN_USERNAME', 'admin') -app.config['ADMIN_PASSWORD'] = os.environ.get('ADMIN_PASSWORD', 'change-this-password-in-production') +app.config['ADMIN_PASSWORD'] = os.environ.get('ADMIN_PASSWORD', None) app.config['ADMIN_FORCE_RESET'] = os.environ.get('ADMIN_FORCE_RESET', 'false').lower() == 'true' # Rate limit configurations app.config['RATELIMIT_STORAGE_URL'] = os.environ.get('RATELIMIT_STORAGE_URL', os.environ.get('REDIS_URL')) @@ -33,12 +34,20 @@ app.config['RATELIMIT_KEY_PREFIX'] = 'anonchat_rate_limit' app.config['BEHIND_PROXY'] = os.environ.get('BEHIND_PROXY', 'false').lower() == 'true' # Redis session configuration -app.config['SESSION_TYPE'] = 'redis' +app.config['SESSION_TYPE'] = os.environ.get('SESSION_TYPE', 'redis') app.config['SESSION_PERMANENT'] = False app.config['SESSION_USE_SIGNER'] = True -app.config['SESSION_REDIS'] = redis.from_url(os.environ.get('REDIS_URL', 'redis://localhost:6379/0')) +if app.config['SESSION_TYPE'] == 'redis': + app.config['SESSION_REDIS'] = redis.from_url(os.environ.get('REDIS_URL', 'redis://localhost:6379/0')) +elif app.config['SESSION_TYPE'] == 'filesystem': + app.config['SESSION_FILE_DIR'] = os.environ.get('SESSION_FILE_DIR', '/dev/shm/flask_session') + app.config['SESSION_FILE_THRESHOLD'] = os.environ.get('SESSION_FILE_THRESHOLD', 100) + app.config['SESSION_FILE_MODE'] = os.environ.get('SESSION_FILE_MODE', 384) app.config['SESSION_KEY_PREFIX'] = 'anonchat_session:' +# Initialize password hasher +password_hasher = PasswordHasher() + # Initialize session with Redis storage Session(app) @@ -127,7 +136,9 @@ def init_db_command(): # Initialize admin user if it doesn't exist # Note: Models already imported above admin_user = Admin.query.filter_by(username=app.config['ADMIN_USERNAME']).first() - if app.config['ADMIN_FORCE_RESET']: + if not admin_user and app.config['ADMIN_PASSWORD'] == None: + print("Admin user not found and no password provided. Skipping admin user initialization.") + elif app.config['ADMIN_FORCE_RESET'] or not admin_user: if admin_user: admin_user.password_hash = Admin.hash_password(app.config['ADMIN_PASSWORD']) print("Admin user password reset.") diff --git a/src/anonchat/models.py b/src/anonchat/models.py index 0163fd9..75f43af 100644 --- a/src/anonchat/models.py +++ b/src/anonchat/models.py @@ -1,7 +1,9 @@ from . import db import os import secrets -import hashlib +import argon2 +from argon2 import PasswordHasher +from . import password_hasher class Message(db.Model): id = db.Column(db.Integer, primary_key=True) @@ -23,14 +25,17 @@ class Settings(db.Model): class Admin(db.Model): id = db.Column(db.Integer, primary_key=True) username = db.Column(db.String(64), unique=True, nullable=False) - password_hash = db.Column(db.String(128), nullable=False) + password_hash = db.Column(db.String(255), nullable=False) - @staticmethod - def hash_password(password): - """Hash a password using salt and SHA-256""" - salt = os.environ.get('SECRET_KEY', 'default-salt') - return hashlib.sha256((password + salt).encode()).hexdigest() + @classmethod + def hash_password(cls, password): + """Hash a password using Argon2id""" + return password_hasher.hash(password) def verify_password(self, password): """Verify a password against the stored hash""" - return self.password_hash == self.hash_password(password) \ No newline at end of file + try: + password_hasher.verify(self.password_hash, password) + return True + except argon2.exceptions.VerifyMismatchError: + return False \ No newline at end of file diff --git a/src/anonchat/routes.py b/src/anonchat/routes.py index f0ba1f0..e27d4f6 100644 --- a/src/anonchat/routes.py +++ b/src/anonchat/routes.py @@ -99,25 +99,39 @@ def create_inquiry(): @limiter.limit("10 per hour", deduct_when=lambda response: response.status_code != 200) def inquiry(inquiry_id): inquiry = Inquiry.query.get_or_404(inquiry_id) + is_admin = 'admin_authenticated' in session and session['admin_authenticated'] if request.method == 'POST': - message_content = request.form.get('message') + if is_admin: + message_content = request.form.get('admin_response') + is_admin_message = True + else: + message_content = request.form.get('message') + is_admin_message = False + if message_content and message_content.strip(): - message = Message(content=message_content, inquiry_id=inquiry_id) + message = Message( + content=message_content, + inquiry_id=inquiry_id, + is_admin=is_admin_message + ) db.session.add(message) db.session.commit() # Send webhook for new message - send_webhook('new_message', { + webhook_event = 'admin_response' if is_admin_message else 'new_message' + send_webhook(webhook_event, { 'inquiry_id': inquiry_id, 'message': message_content, - 'is_admin': False + 'is_admin': is_admin_message }) + if is_admin: + flash('Response sent successfully') return redirect(url_for('inquiry', inquiry_id=inquiry_id)) messages = Message.query.filter_by(inquiry_id=inquiry_id).order_by(Message.timestamp).all() - return render_template('inquiry.html', inquiry=inquiry, messages=messages) + return render_template('inquiry.html', inquiry=inquiry, messages=messages, is_admin=is_admin) @app.route('/admin', methods=['GET']) def admin_login(): @@ -178,36 +192,6 @@ def admin_dashboard(): return render_template('admin_dashboard.html', inquiries_with_data=inquiries_with_data) -@app.route('/admin/inquiry/', methods=['GET', 'POST']) -@admin_required -def admin_inquiry(inquiry_id): - inquiry = Inquiry.query.get_or_404(inquiry_id) - - if request.method == 'POST': - admin_response = request.form.get('admin_response') - if admin_response and admin_response.strip(): - # Create message with is_admin flag instead of prefixing content - admin_message = Message( - content=admin_response, - inquiry_id=inquiry_id, - is_admin=True - ) - db.session.add(admin_message) - db.session.commit() - - # Send webhook for admin response - send_webhook('admin_response', { - 'inquiry_id': inquiry_id, - 'message': admin_response, - 'is_admin': True - }) - - flash('Response sent successfully') - return redirect(url_for('admin_inquiry', inquiry_id=inquiry_id)) - - messages = Message.query.filter_by(inquiry_id=inquiry_id).order_by(Message.timestamp).all() - return render_template('admin_inquiry.html', inquiry=inquiry, messages=messages) - @app.route('/admin/webhook', methods=['GET', 'POST']) @admin_required def admin_webhook(): diff --git a/src/anonchat/templates/admin_dashboard.html b/src/anonchat/templates/admin_dashboard.html index f2f319a..6301507 100644 --- a/src/anonchat/templates/admin_dashboard.html +++ b/src/anonchat/templates/admin_dashboard.html @@ -1,26 +1,12 @@ {% extends 'base.html' %} +{% from 'admin_header.html' import admin_header %} {% block title %}Admin Dashboard - {{ config.SITE_TITLE }}{% endblock %} {% block content %}

Admin Dashboard

-
- Home - Webhook Settings - Admin Settings - Logout -
- - {% with messages = get_flashed_messages() %} - {% if messages %} - {% for message in messages %} -
- {{ message }} -
- {% endfor %} - {% endif %} - {% endwith %} + {{ admin_header() }}

All Inquiries

@@ -55,7 +41,7 @@ {% endif %} - View/Respond + View/Respond
diff --git a/src/anonchat/templates/admin_header.html b/src/anonchat/templates/admin_header.html new file mode 100644 index 0000000..04c7a83 --- /dev/null +++ b/src/anonchat/templates/admin_header.html @@ -0,0 +1,22 @@ +{% macro admin_header() %} +
+ Admin Dashboard + Admin Settings + Webhook Settings + Logout +
+ +{% 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/admin_inquiry.html b/src/anonchat/templates/admin_inquiry.html deleted file mode 100644 index 63913c2..0000000 --- a/src/anonchat/templates/admin_inquiry.html +++ /dev/null @@ -1,52 +0,0 @@ -{% extends 'base.html' %} - -{% block title %}Admin View - Inquiry #{{ inquiry.id[:6] }} - {{ config.SITE_TITLE }}{% endblock %} - -{% block content %} -
- Back to Dashboard - Admin Settings - Logout - - - - -
- -

Inquiry: {{ inquiry.id }}

- - {% with messages = get_flashed_messages() %} - {% if messages %} - {% for message in messages %} -
- {{ message }} -
- {% endfor %} - {% endif %} - {% endwith %} - -
- {% if messages %} - {% for message in messages %} -
-
- {% if message.is_admin %}ADMIN: {% endif %} - {{ message.content }} -
-
{{ message.timestamp.strftime('%Y-%m-%d %H:%M:%S') }}
-
- {% endfor %} - {% else %} -

No messages in this inquiry.

- {% endif %} -
- -
-

Respond as Admin

-
- - - -
-
-{% endblock %} \ No newline at end of file diff --git a/src/anonchat/templates/admin_settings.html b/src/anonchat/templates/admin_settings.html index 7510ff8..ed5c498 100644 --- a/src/anonchat/templates/admin_settings.html +++ b/src/anonchat/templates/admin_settings.html @@ -1,28 +1,13 @@ {% extends 'base.html' %} +{% from 'admin_header.html' import admin_header %} + {% block title %}Admin Settings - {{ config.SITE_TITLE }}{% endblock %} {% block content %}

Admin Settings

-
- Back to Dashboard - Logout -
- - {% with messages = get_flashed_messages(with_categories=true) %} - {% if messages %} - {% for category, message in messages %} -
- {{ message }} -
- {% endfor %} - {% endif %} - {% endwith %} + {{ admin_header() }}

Change Admin Password

diff --git a/src/anonchat/templates/admin_webhook.html b/src/anonchat/templates/admin_webhook.html index e12dc9f..44f4332 100644 --- a/src/anonchat/templates/admin_webhook.html +++ b/src/anonchat/templates/admin_webhook.html @@ -1,25 +1,12 @@ {% extends 'base.html' %} +{% from 'admin_header.html' import admin_header %} {% block title %}Webhook Settings - {{ config.SITE_TITLE }}{% endblock %} {% block content %}

Webhook Settings

-
- Back to Dashboard - Admin Settings - Logout -
- - {% with messages = get_flashed_messages() %} - {% if messages %} - {% for message in messages %} -
- {{ message }} -
- {% endfor %} - {% endif %} - {% endwith %} + {{ admin_header() }}

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

@@ -55,9 +42,9 @@ -
+

Webhook Payload Example

-
+        
 {
   "event_type": "new_inquiry",
   "timestamp": "2023-04-01T12:34:56.789Z",
diff --git a/src/anonchat/templates/inquiry.html b/src/anonchat/templates/inquiry.html
index cd891dc..4ea2e50 100644
--- a/src/anonchat/templates/inquiry.html
+++ b/src/anonchat/templates/inquiry.html
@@ -1,12 +1,21 @@
 {% extends "base.html" %}
 
-{% block title %}Inquiry #{{ inquiry.id[:6] }} - {{ config.SITE_TITLE }}{% endblock %}
+{% from 'admin_header.html' import admin_header %}
+
+{% block title %}{% if is_admin %}Admin View - {% endif %}Inquiry #{{ inquiry.id[:6] }} - {{ config.SITE_TITLE }}{% endblock %}
 
 {% block content %}
     
+ {% if is_admin %} + {{ admin_header() }} + {% endif %} +

Inquiry #{{ inquiry.id[:6] }}

-

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

-

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

+ + {% if not is_admin %} +

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

+

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

+ {% endif %}
@@ -15,6 +24,16 @@
+ {% with messages = get_flashed_messages() %} + {% if messages %} + {% for message in messages %} +
+ {{ message }} +
+ {% endfor %} + {% endif %} + {% endwith %} +

Messages

{% if messages %} @@ -32,16 +51,18 @@ {% endif %}
-
-

Please refresh the page to see new messages.

-
+ {% if not is_admin %} +
+

Please refresh the page to see new messages.

+
+ {% endif %}
-

Reply

+

{% if is_admin %}Respond as Admin{% else %}Reply{% endif %}

- +