Features Added: 🔥 Modern Chat System: - Real-time messaging with modern Tailwind CSS design - Post-linked discussions for adventure sharing - Chat categories (general, technical-support, adventure-planning) - Mobile-responsive interface with gradient backgrounds - JavaScript polling for live message updates 🎯 Comprehensive Admin Panel: - Chat room management with merge capabilities - Password reset system with email templates - User management with admin controls - Chat statistics and analytics dashboard - Room binding to posts and categorization �� Mobile API Integration: - RESTful API endpoints at /api/v1/chat - Session-based authentication for mobile apps - Comprehensive endpoints for rooms, messages, users - Mobile app compatibility (React Native, Flutter) 🛠️ Technical Improvements: - Enhanced database models with ChatRoom categories - Password reset token system with email verification - Template synchronization fixes for Docker deployment - Migration scripts for database schema updates - Improved error handling and validation 🎨 UI/UX Enhancements: - Modern card-based layouts matching app design - Consistent styling across chat and admin interfaces - Mobile-optimized touch interactions - Professional gradient designs and glass morphism effects 📚 Documentation: - Updated README with comprehensive API documentation - Added deployment instructions for Docker (port 8100) - Configuration guide for production environments - Mobile integration examples and endpoints This update transforms the platform into a comprehensive motorcycle adventure community with modern chat capabilities and professional admin management tools.
270 lines
9.9 KiB
HTML
270 lines
9.9 KiB
HTML
{% extends "admin/base.html" %}
|
|
|
|
{% block title %}Password Reset Email Template - Admin{% endblock %}
|
|
|
|
{% block admin_content %}
|
|
<div class="d-flex justify-content-between flex-wrap flex-md-nowrap align-items-center pb-2 mb-3 border-bottom">
|
|
<h1 class="h2">Password Reset Email Template</h1>
|
|
<div class="btn-toolbar mb-2 mb-md-0">
|
|
<a href="{{ url_for('admin.password_reset_request_detail', request_id=token.request_id) }}"
|
|
class="btn btn-outline-secondary btn-sm me-2">
|
|
<i class="fas fa-arrow-left"></i> Back to Request
|
|
</a>
|
|
<a href="{{ url_for('admin.password_reset_tokens') }}" class="btn btn-outline-secondary btn-sm">
|
|
<i class="fas fa-list"></i> All Tokens
|
|
</a>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Token Status Alert -->
|
|
<div class="alert alert-info">
|
|
<h5 class="alert-heading">
|
|
<i class="fas fa-info-circle"></i> Token Information
|
|
</h5>
|
|
<p class="mb-2">
|
|
<strong>Token Status:</strong>
|
|
{% if token.is_used %}
|
|
<span class="badge bg-success">Used</span> - This token has already been used
|
|
{% elif token.is_expired %}
|
|
<span class="badge bg-secondary">Expired</span> - This token has expired
|
|
{% else %}
|
|
<span class="badge bg-warning">Active</span> - This token is ready to use
|
|
{% endif %}
|
|
</p>
|
|
<p class="mb-2">
|
|
<strong>Expires:</strong> {{ token.expires_at.strftime('%B %d, %Y at %I:%M %p') }}
|
|
</p>
|
|
<p class="mb-0">
|
|
<strong>For User:</strong> {{ token.user.nickname }} ({{ token.user.email }})
|
|
</p>
|
|
</div>
|
|
|
|
<!-- Email Template Card -->
|
|
<div class="card">
|
|
<div class="card-header d-flex justify-content-between align-items-center">
|
|
<h6 class="m-0 fw-bold text-primary">Email Template - Copy and Send to User</h6>
|
|
<button type="button" class="btn btn-success btn-sm" onclick="copyEmailTemplate()">
|
|
<i class="fas fa-copy"></i> Copy All
|
|
</button>
|
|
</div>
|
|
<div class="card-body">
|
|
<!-- Email Subject -->
|
|
<div class="mb-4">
|
|
<label class="form-label fw-bold">Subject:</label>
|
|
<div class="input-group">
|
|
<input type="text" class="form-control" id="email-subject" readonly
|
|
value="Password Reset Request - Moto Adventure Website">
|
|
<button class="btn btn-outline-secondary" type="button" onclick="copyToClipboard('email-subject')">
|
|
<i class="fas fa-copy"></i>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Email Body -->
|
|
<div class="mb-4">
|
|
<label class="form-label fw-bold">Email Body:</label>
|
|
<div class="position-relative">
|
|
<textarea class="form-control" id="email-body" rows="12" readonly>Hello {{ token.user.nickname }},
|
|
|
|
We received your request for a password reset for your Moto Adventure website account.
|
|
|
|
To reset your password, please click the link below:
|
|
|
|
{{ reset_url }}
|
|
|
|
This link is valid for 24 hours and can only be used once. If you did not request this password reset, please ignore this email.
|
|
|
|
Important Security Information:
|
|
- This link expires on {{ token.expires_at.strftime('%B %d, %Y at %I:%M %p') }}
|
|
- Do not share this link with anyone
|
|
- If the link doesn't work, you may need to request a new password reset
|
|
|
|
If you have any questions or need assistance, please contact our support team.
|
|
|
|
Best regards,
|
|
Moto Adventure Team
|
|
|
|
---
|
|
This is an automated message. Please do not reply to this email.</textarea>
|
|
<button class="btn btn-outline-secondary position-absolute top-0 end-0 m-2"
|
|
type="button" onclick="copyToClipboard('email-body')">
|
|
<i class="fas fa-copy"></i> Copy
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Reset Link Only -->
|
|
<div class="mb-4">
|
|
<label class="form-label fw-bold">Reset Link Only:</label>
|
|
<div class="input-group">
|
|
<input type="text" class="form-control" id="reset-link" readonly value="{{ reset_url }}">
|
|
<button class="btn btn-outline-secondary" type="button" onclick="copyToClipboard('reset-link')">
|
|
<i class="fas fa-copy"></i>
|
|
</button>
|
|
</div>
|
|
<small class="text-muted">Use this if you prefer to compose your own email message.</small>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Instructions Card -->
|
|
<div class="card mt-4">
|
|
<div class="card-header">
|
|
<h6 class="m-0 fw-bold text-primary">Instructions for Admin</h6>
|
|
</div>
|
|
<div class="card-body">
|
|
<div class="row">
|
|
<div class="col-md-6">
|
|
<h6 class="fw-bold">How to Send:</h6>
|
|
<ol class="small">
|
|
<li>Copy the subject and email body above</li>
|
|
<li>Open your email client (Gmail, Outlook, etc.)</li>
|
|
<li>Create a new email to: <strong>{{ token.user.email }}</strong></li>
|
|
<li>Paste the subject and body</li>
|
|
<li>Send the email</li>
|
|
<li>Return here to monitor if the link was used</li>
|
|
</ol>
|
|
</div>
|
|
<div class="col-md-6">
|
|
<h6 class="fw-bold">Security Notes:</h6>
|
|
<ul class="small">
|
|
<li>Token expires in 24 hours automatically</li>
|
|
<li>Token can only be used once</li>
|
|
<li>Monitor token usage below</li>
|
|
<li>Do not share the reset link publicly</li>
|
|
<li>User must enter a new password to complete reset</li>
|
|
</ul>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Token Usage Tracking -->
|
|
<div class="card mt-4">
|
|
<div class="card-header">
|
|
<h6 class="m-0 fw-bold text-primary">Token Usage Tracking</h6>
|
|
</div>
|
|
<div class="card-body">
|
|
<div class="row">
|
|
<div class="col-md-3">
|
|
<div class="text-center">
|
|
<div class="h4 mb-0 {{ 'text-success' if token.is_used else 'text-muted' }}">
|
|
{{ 'Yes' if token.is_used else 'No' }}
|
|
</div>
|
|
<small class="text-muted">Used</small>
|
|
</div>
|
|
</div>
|
|
<div class="col-md-3">
|
|
<div class="text-center">
|
|
<div class="h4 mb-0 {{ 'text-danger' if token.is_expired else 'text-success' }}">
|
|
{{ 'Yes' if token.is_expired else 'No' }}
|
|
</div>
|
|
<small class="text-muted">Expired</small>
|
|
</div>
|
|
</div>
|
|
<div class="col-md-3">
|
|
<div class="text-center">
|
|
<div class="h4 mb-0">
|
|
{% if token.used_at %}
|
|
{{ token.used_at.strftime('%m/%d %H:%M') }}
|
|
{% else %}
|
|
-
|
|
{% endif %}
|
|
</div>
|
|
<small class="text-muted">Used At</small>
|
|
</div>
|
|
</div>
|
|
<div class="col-md-3">
|
|
<div class="text-center">
|
|
<div class="h4 mb-0">
|
|
{% if token.user_ip %}
|
|
{{ token.user_ip }}
|
|
{% else %}
|
|
-
|
|
{% endif %}
|
|
</div>
|
|
<small class="text-muted">User IP</small>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="mt-3">
|
|
<button type="button" class="btn btn-outline-secondary btn-sm" onclick="location.reload()">
|
|
<i class="fas fa-sync-alt"></i> Refresh Status
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
function copyToClipboard(elementId) {
|
|
const element = document.getElementById(elementId);
|
|
element.select();
|
|
element.setSelectionRange(0, 99999); // For mobile devices
|
|
|
|
try {
|
|
document.execCommand('copy');
|
|
showCopySuccess();
|
|
} catch (err) {
|
|
console.error('Failed to copy: ', err);
|
|
showCopyError();
|
|
}
|
|
}
|
|
|
|
function copyEmailTemplate() {
|
|
const subject = document.getElementById('email-subject').value;
|
|
const body = document.getElementById('email-body').value;
|
|
const combined = `Subject: ${subject}\n\n${body}`;
|
|
|
|
navigator.clipboard.writeText(combined).then(function() {
|
|
showCopySuccess();
|
|
}, function(err) {
|
|
console.error('Failed to copy: ', err);
|
|
showCopyError();
|
|
});
|
|
}
|
|
|
|
function showCopySuccess() {
|
|
// Create temporary success alert
|
|
const alert = document.createElement('div');
|
|
alert.className = 'alert alert-success alert-dismissible fade show position-fixed';
|
|
alert.style.top = '20px';
|
|
alert.style.right = '20px';
|
|
alert.style.zIndex = '9999';
|
|
alert.innerHTML = `
|
|
<i class="fas fa-check"></i> Copied to clipboard!
|
|
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
|
|
`;
|
|
document.body.appendChild(alert);
|
|
|
|
// Auto-remove after 3 seconds
|
|
setTimeout(() => {
|
|
if (alert.parentNode) {
|
|
alert.parentNode.removeChild(alert);
|
|
}
|
|
}, 3000);
|
|
}
|
|
|
|
function showCopyError() {
|
|
// Create temporary error alert
|
|
const alert = document.createElement('div');
|
|
alert.className = 'alert alert-danger alert-dismissible fade show position-fixed';
|
|
alert.style.top = '20px';
|
|
alert.style.right = '20px';
|
|
alert.style.zIndex = '9999';
|
|
alert.innerHTML = `
|
|
<i class="fas fa-times"></i> Failed to copy. Please select and copy manually.
|
|
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
|
|
`;
|
|
document.body.appendChild(alert);
|
|
|
|
// Auto-remove after 5 seconds
|
|
setTimeout(() => {
|
|
if (alert.parentNode) {
|
|
alert.parentNode.removeChild(alert);
|
|
}
|
|
}, 5000);
|
|
}
|
|
</script>
|
|
{% endblock %}
|