Refactor a bit
This commit is contained in:
parent
9cf2da1128
commit
b69a8aee0f
17 changed files with 253 additions and 268 deletions
|
@ -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
|
||||
|
|
|
@ -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')
|
||||
flash('Password updated successfully', 'success')
|
||||
return redirect(url_for('admin_settings'))
|
||||
|
||||
|
|
27
src/anonchat/error_handlers.py
Normal file
27
src/anonchat/error_handlers.py
Normal file
|
@ -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
|
|
@ -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):
|
||||
|
|
|
@ -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/<inquiry_id>/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/<inquiry_id>/messages', methods=['GET'])
|
||||
@limiter.limit("10 per hour", deduct_when=lambda response: response.status_code != 200)
|
||||
@limiter.limit("30 per minute")
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -1,14 +0,0 @@
|
|||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}404 Not Found - {{ config.SITE_TITLE }}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="error-container">
|
||||
<div class="error-code">404</div>
|
||||
<h2>Page Not Found</h2>
|
||||
<p>Sorry, the page you're looking for doesn't exist or may have been moved.</p>
|
||||
<div class="error-actions">
|
||||
<a href="{{ url_for('index') }}" class="btn-primary">Go to Home</a>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
|
@ -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 %}
|
||||
<h2>Admin Dashboard</h2>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<h2>Admin Dashboard</h2>
|
||||
|
||||
{{ admin_header() }}
|
||||
{{ flash_messages() }}
|
||||
|
||||
<h3>All Inquiries</h3>
|
||||
|
||||
{% if inquiries_with_data %}
|
|
@ -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 %}
|
||||
<h2>Admin Login</h2>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<h2>Admin Login</h2>
|
||||
|
||||
{{ flash_messages() }}
|
||||
|
||||
<form method="POST">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
|
||||
<div>
|
95
src/anonchat/templates/admin/settings.html
Normal file
95
src/anonchat/templates/admin/settings.html
Normal file
|
@ -0,0 +1,95 @@
|
|||
{% extends 'base.html' %}
|
||||
|
||||
{% block title %}Admin Settings{% endblock %}
|
||||
|
||||
{% block header %}
|
||||
<h2>Admin Settings</h2>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="settings-form">
|
||||
<h3>Change Admin Password</h3>
|
||||
<form method="POST" action="{{ url_for('admin_settings_password') }}">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
|
||||
<div>
|
||||
<label for="current_password">Current Password:</label>
|
||||
<input type="password" id="current_password" name="current_password" required>
|
||||
</div>
|
||||
<div>
|
||||
<label for="new_password">New Password:</label>
|
||||
<input type="password" id="new_password" name="new_password" required>
|
||||
<small style="display: block; margin-top: 0.25rem; color: #6c757d;">Password must be at least 8 characters</small>
|
||||
</div>
|
||||
<div>
|
||||
<label for="confirm_password">Confirm New Password:</label>
|
||||
<input type="password" id="confirm_password" name="confirm_password" required>
|
||||
</div>
|
||||
<button type="submit">Update Password</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<br>
|
||||
|
||||
<div class="webhook-info">
|
||||
<h3>Webhook Settings</h3>
|
||||
|
||||
<div style="margin-bottom: 2rem;">
|
||||
<p>Configure webhook settings to receive notifications for new inquiries and responses.</p>
|
||||
<p>Events will be sent as POST requests with JSON payloads. Event types include:</p>
|
||||
<ul>
|
||||
<li><strong>inquiry_created</strong> - Triggered when a new inquiry is created. Contains the inquiry ID and the initial message.</li>
|
||||
<li><strong>inquiry_reopened</strong> - Triggered when an inquiry is reopened. Contains the inquiry ID.</li>
|
||||
<li><strong>inquiry_closed</strong> - Triggered when an inquiry is closed. Contains the inquiry ID.</li>
|
||||
<li><strong>inquiry_message</strong> - Triggered when a user adds a message to an existing inquiry. Contains the inquiry ID and the message content.</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<form method="POST" action="{{ url_for('admin_settings_webhook') }}">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
|
||||
<div>
|
||||
<label>
|
||||
<input type="checkbox" name="webhook_enabled" {% if settings.webhook_enabled %}checked{% endif %}>
|
||||
Enable Webhooks
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div style="margin-top: 1rem;">
|
||||
<label for="webhook_url">Webhook URL:</label>
|
||||
<input type="text" id="webhook_url" name="webhook_url" value="{{ settings.webhook_url or '' }}" placeholder="https://example.com/webhook">
|
||||
<p style="font-size: 0.8rem; color: #666;">The URL that will receive webhook events</p>
|
||||
</div>
|
||||
|
||||
<div style="margin-top: 1rem;">
|
||||
<label for="webhook_secret">Webhook Secret (optional):</label>
|
||||
<input type="text" id="webhook_secret" name="webhook_secret" value="{{ settings.webhook_secret or '' }}" placeholder="Secret key for HMAC signature">
|
||||
<p style="font-size: 0.8rem; color: #666;">If provided, webhooks will include a signature header (X-Webhook-Signature)</p>
|
||||
</div>
|
||||
|
||||
<button type="submit" style="margin-top: 1rem;">Save Settings</button>
|
||||
</form>
|
||||
|
||||
<div style="padding: 1rem;">
|
||||
<h3>Webhook Payload Example</h3>
|
||||
<pre style="background-color: #1e1e1e; padding: 1rem; overflow-x: auto; border-radius: 0.25rem; color: #d4d4d4;">
|
||||
{
|
||||
"event_type": "inquiry_created",
|
||||
"timestamp": "2023-04-01T12:34:56.789Z",
|
||||
"data": {
|
||||
"inquiry_id": "abcdef1234567890",
|
||||
"message": "Hello, I have a question..."
|
||||
}
|
||||
}</pre>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<br>
|
||||
|
||||
<div class="settings-info">
|
||||
<h3>System Configuration</h3>
|
||||
<p>The following settings are configured through environment variables:</p>
|
||||
<ul style="margin-left: 1.5rem;">
|
||||
<li><strong>Auto Delete Delay:</strong> {{ config.AUTO_DELETE_HOURS }} hours (set with AUTO_DELETE_HOURS)</li>
|
||||
</ul>
|
||||
<p><small>To change these values, update your environment variables or .env file and restart the application.</small></p>
|
||||
</div>
|
||||
{% endblock %}
|
|
@ -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 %}
|
||||
<h2>Admin Settings</h2>
|
||||
|
||||
{{ admin_header() }}
|
||||
{{ flash_messages() }}
|
||||
|
||||
<div class="settings-form">
|
||||
<h3>Change Admin Password</h3>
|
||||
<form method="POST">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
|
||||
<div>
|
||||
<label for="current_password">Current Password:</label>
|
||||
<input type="password" id="current_password" name="current_password" required>
|
||||
</div>
|
||||
<div>
|
||||
<label for="new_password">New Password:</label>
|
||||
<input type="password" id="new_password" name="new_password" required>
|
||||
<small style="display: block; margin-top: 0.25rem; color: #6c757d;">Password must be at least 8 characters</small>
|
||||
</div>
|
||||
<div>
|
||||
<label for="confirm_password">Confirm New Password:</label>
|
||||
<input type="password" id="confirm_password" name="confirm_password" required>
|
||||
</div>
|
||||
<button type="submit">Update Password</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<br>
|
||||
|
||||
<div class="settings-info">
|
||||
<h3>System Configuration</h3>
|
||||
<p>The following settings are configured through environment variables:</p>
|
||||
<ul style="margin-left: 1.5rem;">
|
||||
<li><strong>Auto Delete Delay:</strong> {{ config.AUTO_DELETE_HOURS }} hours (set with AUTO_DELETE_HOURS)</li>
|
||||
</ul>
|
||||
<p><small>To change these values, update your environment variables or .env file and restart the application.</small></p>
|
||||
</div>
|
||||
{% endblock %}
|
|
@ -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 %}
|
||||
<h2>Webhook Settings</h2>
|
||||
|
||||
{{ admin_header() }}
|
||||
{{ flash_messages() }}
|
||||
|
||||
<div style="margin-bottom: 2rem;">
|
||||
<p>Configure webhook settings to receive notifications for new inquiries and responses.</p>
|
||||
<p>Events will be sent as POST requests with JSON payloads. Event types include:</p>
|
||||
<ul>
|
||||
<li><strong>inquiry_created</strong> - Triggered when a new inquiry is created. Contains the inquiry ID and the initial message.</li>
|
||||
<li><strong>inquiry_reopened</strong> - Triggered when an inquiry is reopened. Contains the inquiry ID.</li>
|
||||
<li><strong>inquiry_closed</strong> - Triggered when an inquiry is closed. Contains the inquiry ID.</li>
|
||||
<li><strong>inquiry_message</strong> - Triggered when a user adds a message to an existing inquiry. Contains the inquiry ID and the message content.</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<form method="POST" action="{{ url_for('admin_webhook') }}">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
|
||||
<div>
|
||||
<label>
|
||||
<input type="checkbox" name="webhook_enabled" {% if settings.webhook_enabled %}checked{% endif %}>
|
||||
Enable Webhooks
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div style="margin-top: 1rem;">
|
||||
<label for="webhook_url">Webhook URL:</label>
|
||||
<input type="text" id="webhook_url" name="webhook_url" value="{{ settings.webhook_url or '' }}" placeholder="https://example.com/webhook">
|
||||
<p style="font-size: 0.8rem; color: #666;">The URL that will receive webhook events</p>
|
||||
</div>
|
||||
|
||||
<div style="margin-top: 1rem;">
|
||||
<label for="webhook_secret">Webhook Secret (optional):</label>
|
||||
<input type="text" id="webhook_secret" name="webhook_secret" value="{{ settings.webhook_secret or '' }}" placeholder="Secret key for HMAC signature">
|
||||
<p style="font-size: 0.8rem; color: #666;">If provided, webhooks will include a signature header (X-Webhook-Signature)</p>
|
||||
</div>
|
||||
|
||||
<button type="submit" style="margin-top: 1rem;">Save Settings</button>
|
||||
</form>
|
||||
|
||||
<div style="padding: 1rem;">
|
||||
<h3>Webhook Payload Example</h3>
|
||||
<pre style="background-color: #1e1e1e; padding: 1rem; overflow-x: auto; border-radius: 0.25rem; color: #d4d4d4;">
|
||||
{
|
||||
"event_type": "inquiry_created",
|
||||
"timestamp": "2023-04-01T12:34:56.789Z",
|
||||
"data": {
|
||||
"inquiry_id": "abcdef1234567890",
|
||||
"message": "Hello, I have a question..."
|
||||
}
|
||||
}
|
||||
</pre>
|
||||
</div>
|
||||
{% endblock %}
|
|
@ -2,13 +2,42 @@
|
|||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>{% block title %}{{ config.SITE_TITLE }}{% endblock %}</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>{% block title %}{% endblock %} - {{ config.SITE_TITLE }}</title>
|
||||
<link rel="stylesheet" href="{{ url_for('static', filename='css/styles.css') }}">
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div id="app">
|
||||
<header>
|
||||
{% block header %}{% endblock %}
|
||||
|
||||
<nav>
|
||||
{% if is_admin() %}
|
||||
<div style="margin-bottom: 1rem;">
|
||||
<span style="margin-right: 1rem;">Admin</span>
|
||||
<a href="{{ url_for('admin_dashboard') }}" style="margin-right: 1rem;">Dashboard</a>
|
||||
<a href="{{ url_for('admin_settings') }}" style="margin-right: 1rem;">Settings</a>
|
||||
<a href="{{ url_for('admin_logout') }}" style="margin-right: 1rem;">Logout</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
</nav>
|
||||
</header>
|
||||
|
||||
<main>
|
||||
{% with messages = get_flashed_messages(with_categories=true) %}
|
||||
{% if messages %}
|
||||
{% for category, message in messages %}
|
||||
<div style="background-color: {% if category == 'error' %}#e8c7ca{% else %}#c4ddca{% endif %};
|
||||
color: {% if category == 'error' %}#5a0000{% else %}#003500{% endif %};
|
||||
padding: 0.75rem;
|
||||
margin-bottom: 1rem;
|
||||
border-radius: 0.25rem;">
|
||||
{{ message }}
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
|
||||
{% block content %}{% endblock %}
|
||||
</main>
|
||||
</div>
|
||||
|
|
|
@ -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 %}
|
||||
<h2>Create New Inquiry</h2>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="create-inquiry">
|
||||
<h2>Start a New Conversation</h2>
|
||||
<p>Create a new inquiry to start an anonymous conversation.</p>
|
||||
|
||||
{{ flash_messages() }}
|
||||
|
||||
<form method="POST" action="{{ url_for('index') }}">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
|
||||
<div class="form-group">
|
||||
|
|
|
@ -1,12 +1,15 @@
|
|||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Error - {{ config.SITE_TITLE }}{% endblock %}
|
||||
{% block title %}{{ title }}{% endblock %}
|
||||
|
||||
{% block header %}
|
||||
<h2>{{ title }}</h2>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="error-container">
|
||||
<div class="error-code">Error</div>
|
||||
<h2>Something went wrong</h2>
|
||||
<p>{{ error | default("An unexpected error occurred.") }}</p>
|
||||
<p>{{ message }}</p>
|
||||
<div class="error-actions">
|
||||
<a href="{{ url_for('index') }}" class="btn-primary">Go to Home</a>
|
||||
{% if request.referrer %}
|
||||
|
|
|
@ -1,24 +0,0 @@
|
|||
{% 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>
|
||||
{% endmacro %}
|
||||
|
||||
{% macro flash_messages() %}
|
||||
{% 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,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 %}
|
||||
<h2>
|
||||
Inquiry #{{ inquiry.id[:6] }}
|
||||
|
||||
{% if inquiry.is_closed %}
|
||||
<span style="color: #6c757d; font-size: 0.9rem; padding: 0.3rem 0.6rem; background-color: #e9ecef; border-radius: 0.25rem; margin-left: 0.5rem;">CLOSED</span>
|
||||
{% endif %}
|
||||
</h2>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="inquiry-details" id="inquiry-details" data-inquiry-id="{{ inquiry.id }}" data-last-message-number="{{ messages[-1].message_number if messages else 0 }}">
|
||||
<h2>Inquiry #{{ inquiry.id[:6] }}
|
||||
{% if inquiry.is_closed %}
|
||||
<span style="color: #6c757d; font-size: 0.9rem; padding: 0.3rem 0.6rem; background-color: #e9ecef; border-radius: 0.25rem; margin-left: 0.5rem;">CLOSED</span>
|
||||
{% endif %}
|
||||
</h2>
|
||||
|
||||
{% if is_admin %}
|
||||
{{ admin_header() }}
|
||||
{% endif %}
|
||||
|
||||
{{ flash_messages() }}
|
||||
|
||||
{% if not is_admin %}
|
||||
<p>This is your conversation link: <code style="user-select: all;">{{ request.url }}</code></p>
|
||||
<p>This is your inquiry 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 %}
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue