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.

This commit is contained in:
Minecon724 2025-04-02 13:20:30 +02:00
commit 65fb1879b6
Signed by: Minecon724
GPG key ID: A02E6E67AB961189
11 changed files with 111 additions and 161 deletions

View file

@ -21,6 +21,6 @@ primary_region = 'ams'
[[vm]]
size = 'shared-cpu-1x'
memory = '1gb'
memory = '256mb'
cpu_kind = 'shared'
cpus = 1

View file

@ -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]

View file

@ -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.")

View file

@ -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)
try:
password_hasher.verify(self.password_hash, password)
return True
except argon2.exceptions.VerifyMismatchError:
return False

View file

@ -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/<inquiry_id>', 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():

View file

@ -1,26 +1,12 @@
{% extends 'base.html' %}
{% from 'admin_header.html' import admin_header %}
{% block title %}Admin Dashboard - {{ config.SITE_TITLE }}{% endblock %}
{% block content %}
<h2>Admin Dashboard</h2>
<div style="margin-bottom: 1rem;">
<a href="{{ url_for('index') }}" style="margin-right: 1rem;">Home</a>
<a href="{{ url_for('admin_webhook') }}" style="margin-right: 1rem;">Webhook Settings</a>
<a href="{{ url_for('admin_settings') }}" style="margin-right: 1rem;">Admin Settings</a>
<a href="{{ url_for('admin_logout') }}">Logout</a>
</div>
{% with messages = get_flashed_messages() %}
{% if messages %}
{% for message in messages %}
<div style="background-color: #d4edda; color: #155724; padding: 0.75rem; margin-bottom: 1rem; border-radius: 0.25rem;">
{{ message }}
</div>
{% endfor %}
{% endif %}
{% endwith %}
{{ admin_header() }}
<h3>All Inquiries</h3>
@ -55,7 +41,7 @@
{% endif %}
</td>
<td>
<a href="{{ url_for('admin_inquiry', inquiry_id=item.inquiry.id) }}">View/Respond</a>
<a href="{{ url_for('inquiry', inquiry_id=item.inquiry.id) }}">View/Respond</a>
<form method="POST" action="{{ url_for('delete_inquiry', inquiry_id=item.inquiry.id) }}" style="display: inline; margin-left: 0.5rem;">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
<button type="submit" onclick="return confirm('Are you sure you want to delete this inquiry? This action cannot be undone.')" style="background-color: #dc3545; color: white; border: none; padding: 0.25rem 0.5rem; border-radius: 0.25rem; cursor: pointer; font-size: 0.8rem;">Delete</button>

View file

@ -0,0 +1,22 @@
{% macro admin_header() %}
<div style="margin-bottom: 1rem;">
<a href="{{ url_for('admin_dashboard') }}" style="margin-right: 1rem;">Admin Dashboard</a>
<a href="{{ url_for('admin_settings') }}" style="margin-right: 1rem;">Admin Settings</a>
<a href="{{ url_for('admin_webhook') }}" style="margin-right: 1rem;">Webhook Settings</a>
<a href="{{ url_for('admin_logout') }}" style="margin-right: 1rem;">Logout</a>
</div>
{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}
{% for category, message in messages %}
<div style="background-color: {% if category == 'error' %}#f8d7da{% else %}#d4edda{% endif %};
color: {% if category == 'error' %}#721c24{% else %}#155724{% endif %};
padding: 0.75rem;
margin-bottom: 1rem;
border-radius: 0.25rem;">
{{ message }}
</div>
{% endfor %}
{% endif %}
{% endwith %}
{% endmacro %}

View file

@ -1,52 +0,0 @@
{% extends 'base.html' %}
{% block title %}Admin View - Inquiry #{{ inquiry.id[:6] }} - {{ config.SITE_TITLE }}{% endblock %}
{% block content %}
<div style="margin-bottom: 1rem;">
<a href="{{ url_for('admin_dashboard') }}" style="margin-right: 1rem;">Back to Dashboard</a>
<a href="{{ url_for('admin_settings') }}" style="margin-right: 1rem;">Admin Settings</a>
<a href="{{ url_for('admin_logout') }}" style="margin-right: 1rem;">Logout</a>
<form method="POST" action="{{ url_for('delete_inquiry', inquiry_id=inquiry.id) }}" style="display: inline;">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
<button type="submit" onclick="return confirm('Are you sure you want to delete this inquiry? This action cannot be undone.')" style="background-color: #dc3545; color: white; border: none; padding: 0.375rem 0.75rem; border-radius: 0.25rem; cursor: pointer;">Delete Inquiry</button>
</form>
</div>
<h2>Inquiry: {{ inquiry.id }}</h2>
{% with messages = get_flashed_messages() %}
{% if messages %}
{% for message in messages %}
<div style="background-color: #d4edda; color: #155724; padding: 0.75rem; margin-bottom: 1rem; border-radius: 0.25rem;">
{{ message }}
</div>
{% endfor %}
{% endif %}
{% endwith %}
<div class="conversation" style="margin-bottom: 2rem;">
{% if messages %}
{% for message in messages %}
<div class="message {% if message.is_admin %}admin-message{% else %}user-message{% endif %}" style="margin-bottom: 1rem; padding: 1rem; border-radius: 4px;">
<div>
{% if message.is_admin %}<span class="admin-badge">ADMIN:</span> {% endif %}
{{ message.content }}
</div>
<div class="timestamp">{{ message.timestamp.strftime('%Y-%m-%d %H:%M:%S') }}</div>
</div>
{% endfor %}
{% else %}
<p>No messages in this inquiry.</p>
{% endif %}
</div>
<div class="response-form">
<h3>Respond as Admin</h3>
<form method="POST">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
<textarea name="admin_response" rows="4" placeholder="Type your response here..." required></textarea>
<button type="submit">Send Response</button>
</form>
</div>
{% endblock %}

View file

@ -1,28 +1,13 @@
{% extends 'base.html' %}
{% from 'admin_header.html' import admin_header %}
{% block title %}Admin Settings - {{ config.SITE_TITLE }}{% endblock %}
{% block content %}
<h2>Admin Settings</h2>
<div style="margin-bottom: 1rem;">
<a href="{{ url_for('admin_dashboard') }}" style="margin-right: 1rem;">Back to Dashboard</a>
<a href="{{ url_for('admin_logout') }}" style="margin-right: 1rem;">Logout</a>
</div>
{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}
{% for category, message in messages %}
<div style="background-color: {% if category == 'error' %}#f8d7da{% else %}#d4edda{% endif %};
color: {% if category == 'error' %}#721c24{% else %}#155724{% endif %};
padding: 0.75rem;
margin-bottom: 1rem;
border-radius: 0.25rem;">
{{ message }}
</div>
{% endfor %}
{% endif %}
{% endwith %}
{{ admin_header() }}
<div class="settings-form">
<h3>Change Admin Password</h3>

View file

@ -1,25 +1,12 @@
{% extends 'base.html' %}
{% from 'admin_header.html' import admin_header %}
{% block title %}Webhook Settings - {{ config.SITE_TITLE }}{% endblock %}
{% block content %}
<h2>Webhook Settings</h2>
<div style="margin-bottom: 1rem;">
<a href="{{ url_for('admin_dashboard') }}" style="margin-right: 1rem;">Back to Dashboard</a>
<a href="{{ url_for('admin_settings') }}" style="margin-right: 1rem;">Admin Settings</a>
<a href="{{ url_for('admin_logout') }}">Logout</a>
</div>
{% with messages = get_flashed_messages() %}
{% if messages %}
{% for message in messages %}
<div style="background-color: #d4edda; color: #155724; padding: 0.75rem; margin-bottom: 1rem; border-radius: 0.25rem;">
{{ message }}
</div>
{% endfor %}
{% endif %}
{% endwith %}
{{ admin_header() }}
<div style="margin-bottom: 2rem;">
<p>Configure webhook settings to receive notifications for new inquiries and responses.</p>
@ -55,9 +42,9 @@
<button type="submit" style="margin-top: 1rem;">Save Settings</button>
</form>
<div style="margin-top: 2rem; padding: 1rem; background-color: #f8f9fa; border-radius: 0.25rem;">
<div style="padding: 1rem;">
<h3>Webhook Payload Example</h3>
<pre style="background-color: #f1f1f1; padding: 1rem; overflow-x: auto; border-radius: 0.25rem;">
<pre style="background-color: #1e1e1e; padding: 1rem; overflow-x: auto; border-radius: 0.25rem; color: #d4d4d4;">
{
"event_type": "new_inquiry",
"timestamp": "2023-04-01T12:34:56.789Z",

View file

@ -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 %}
<div class="inquiry-details">
{% if is_admin %}
{{ admin_header() }}
{% endif %}
<h2>Inquiry #{{ inquiry.id[:6] }}</h2>
<p>This is your conversation link: <code style="user-select: all;">{{ request.url }}</code></p>
<p class="warning"><strong>Important:</strong> Do not share this link with anyone else, as anyone you share it with could access this chat.</p>
{% if not is_admin %}
<p>This is your conversation link: <code style="user-select: all;">{{ request.url }}</code></p>
<p class="warning"><strong>Important:</strong> Do not share this link with anyone else, as anyone you share it with could access this chat.</p>
{% endif %}
<div style="margin-bottom: 1rem;">
<form method="POST" action="{{ url_for('delete_inquiry', inquiry_id=inquiry.id) }}" style="display: inline;">
@ -15,6 +24,16 @@
</form>
</div>
{% with messages = get_flashed_messages() %}
{% if messages %}
{% for message in messages %}
<div style="background-color: #d4edda; color: #155724; padding: 0.75rem; margin-bottom: 1rem; border-radius: 0.25rem;">
{{ message }}
</div>
{% endfor %}
{% endif %}
{% endwith %}
<div class="messages">
<h3>Messages</h3>
{% if messages %}
@ -32,16 +51,18 @@
{% endif %}
</div>
<div class="refresh-notice">
<p><em>Please refresh the page to see new messages.</em></p>
</div>
{% if not is_admin %}
<div class="refresh-notice">
<p><em>Please refresh the page to see new messages.</em></p>
</div>
{% endif %}
<div class="reply-form">
<h3>Reply</h3>
<h3>{% if is_admin %}Respond as Admin{% else %}Reply{% endif %}</h3>
<form method="POST" action="{{ url_for('inquiry', inquiry_id=inquiry.id) }}">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
<div class="form-group">
<textarea name="message" rows="4" placeholder="Type your message..." required></textarea>
<textarea name="{% if is_admin %}admin_response{% else %}message{% endif %}" rows="4" placeholder="Type your message..." required></textarea>
</div>
<button type="submit">Send Message</button>
</form>