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:
parent
3fb583e102
commit
65fb1879b6
11 changed files with 111 additions and 161 deletions
2
fly.toml
2
fly.toml
|
@ -21,6 +21,6 @@ primary_region = 'ams'
|
|||
|
||||
[[vm]]
|
||||
size = 'shared-cpu-1x'
|
||||
memory = '1gb'
|
||||
memory = '256mb'
|
||||
cpu_kind = 'shared'
|
||||
cpus = 1
|
||||
|
|
|
@ -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]
|
||||
|
|
|
@ -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.")
|
||||
|
|
|
@ -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
|
|
@ -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():
|
||||
|
|
|
@ -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>
|
||||
|
|
22
src/anonchat/templates/admin_header.html
Normal file
22
src/anonchat/templates/admin_header.html
Normal 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 %}
|
|
@ -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 %}
|
|
@ -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>
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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>
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue