Refactor a bit

This commit is contained in:
Minecon724 2025-04-03 17:52:55 +02:00
commit b69a8aee0f
Signed by: Minecon724
GPG key ID: A02E6E67AB961189
17 changed files with 253 additions and 268 deletions

View file

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

View file

@ -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'))

View 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

View file

@ -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):

View file

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

View file

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

View file

@ -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 %}

View file

@ -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 %}

View file

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

View 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 %}

View file

@ -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 %}

View file

@ -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 %}

View file

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

View file

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

View file

@ -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 %}

View file

@ -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 %}

View file

@ -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 %}