Fix Bootstrap compatibility and UI issues in user_tokens.html template

- Replace Bootstrap 5 classes and components with custom CSS compatible with base template
- Convert inline onclick handlers to data attributes with event delegation
- Add comprehensive custom CSS for modals, forms, cards, badges, and buttons
- Fix JavaScript template variable issues using data attributes
- Implement modern clipboard API with fallback for older browsers
- Add proper event listeners for token management actions (revoke, extend, delete)
- Template now works with existing custom CSS framework instead of Bootstrap
parent 89693099
......@@ -2,241 +2,542 @@
{% block title %}API Tokens - Fixture Manager{% endblock %}
{% block extra_css %}
/* Modal styles */
.modal {
display: none;
position: fixed;
z-index: 1000;
left: 0;
top: 0;
width: 100%;
height: 100%;
background-color: rgba(0,0,0,0.5);
}
.modal.show {
display: flex;
align-items: center;
justify-content: center;
}
.modal-dialog {
background: white;
border-radius: 8px;
max-width: 500px;
width: 90%;
max-height: 90vh;
overflow-y: auto;
box-shadow: 0 4px 20px rgba(0,0,0,0.3);
}
.modal-header {
padding: 1rem;
border-bottom: 1px solid #ddd;
display: flex;
justify-content: space-between;
align-items: center;
background-color: #f8f9fa;
border-radius: 8px 8px 0 0;
}
.modal-header.bg-success {
background-color: #28a745;
color: white;
}
.modal-title {
margin: 0;
font-size: 1.2rem;
font-weight: bold;
}
.modal-body {
padding: 1rem;
}
.modal-footer {
padding: 1rem;
border-top: 1px solid #ddd;
display: flex;
justify-content: flex-end;
gap: 0.5rem;
background-color: #f8f9fa;
border-radius: 0 0 8px 8px;
}
.close-btn {
background: none;
border: none;
font-size: 1.5rem;
cursor: pointer;
color: #666;
padding: 0;
width: 30px;
height: 30px;
display: flex;
align-items: center;
justify-content: center;
}
.close-btn:hover {
color: #000;
}
.close-btn.white {
color: white;
}
.close-btn.white:hover {
color: #ccc;
}
/* Form styles */
.form-label {
display: block;
margin-bottom: 0.5rem;
font-weight: bold;
color: #333;
}
.form-control {
width: 100%;
padding: 8px 12px;
border: 1px solid #ddd;
border-radius: 4px;
box-sizing: border-box;
font-size: 1rem;
}
.form-control:focus {
outline: none;
border-color: #007bff;
}
.form-control[readonly] {
background-color: #f8f9fa;
font-family: monospace;
}
.form-text {
font-size: 0.875rem;
color: #666;
margin-top: 0.25rem;
}
.form-select {
width: 100%;
padding: 8px 12px;
border: 1px solid #ddd;
border-radius: 4px;
box-sizing: border-box;
font-size: 1rem;
background-color: white;
}
/* Input group */
.input-group {
display: flex;
}
.input-group .form-control {
border-radius: 4px 0 0 4px;
border-right: none;
}
.input-group .btn {
border-radius: 0 4px 4px 0;
border-left: 1px solid #007bff;
}
/* Card styles */
.card {
background: white;
border: 1px solid #ddd;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
margin-bottom: 1rem;
}
.card-header {
padding: 1rem;
background-color: #f8f9fa;
border-bottom: 1px solid #ddd;
border-radius: 8px 8px 0 0;
}
.card-header.bg-info {
background-color: #17a2b8;
color: white;
}
.card-body {
padding: 1rem;
}
/* Table styles */
.table-responsive {
overflow-x: auto;
}
.table-hover tbody tr:hover {
background-color: #f8f9fa;
}
/* Badge styles */
.badge {
display: inline-block;
padding: 0.25rem 0.5rem;
font-size: 0.75rem;
font-weight: bold;
border-radius: 4px;
color: white;
}
.badge.bg-success {
background-color: #28a745;
}
.badge.bg-danger {
background-color: #dc3545;
}
.badge.bg-secondary {
background-color: #6c757d;
}
/* Button group */
.btn-group {
display: inline-flex;
gap: 2px;
}
.btn-outline-warning {
background-color: transparent;
color: #ffc107;
border: 1px solid #ffc107;
}
.btn-outline-warning:hover {
background-color: #ffc107;
color: #212529;
}
.btn-outline-info {
background-color: transparent;
color: #17a2b8;
border: 1px solid #17a2b8;
}
.btn-outline-info:hover {
background-color: #17a2b8;
color: white;
}
.btn-outline-danger {
background-color: transparent;
color: #dc3545;
border: 1px solid #dc3545;
}
.btn-outline-danger:hover {
background-color: #dc3545;
color: white;
}
.btn-outline-secondary {
background-color: transparent;
color: #6c757d;
border: 1px solid #6c757d;
}
.btn-outline-secondary:hover {
background-color: #6c757d;
color: white;
}
/* Utility classes */
.mb-3 {
margin-bottom: 1rem;
}
.mb-4 {
margin-bottom: 1.5rem;
}
.text-muted {
color: #666;
}
.font-monospace {
font-family: monospace;
}
.py-5 {
padding-top: 3rem;
padding-bottom: 3rem;
}
.fa-3x {
font-size: 3rem;
}
/* Hide elements by default */
.d-none {
display: none;
}
/* Success message styling */
.success-message {
background-color: #d4edda;
color: #155724;
border: 1px solid #c3e6cb;
padding: 12px;
border-radius: 4px;
margin-bottom: 1rem;
}
/* Warning message styling */
.warning-message {
background-color: #fff3cd;
color: #856404;
border: 1px solid #ffeaa7;
padding: 12px;
border-radius: 4px;
margin-bottom: 1rem;
}
/* Danger message styling */
.danger-message {
background-color: #f8d7da;
color: #721c24;
border: 1px solid #f5c6cb;
padding: 12px;
border-radius: 4px;
margin-bottom: 1rem;
}
{% endblock %}
{% block content %}
<div class="container-fluid">
<div class="content">
<!-- Header -->
<div class="row mb-4">
<div class="col-12">
<div class="d-flex justify-content-between align-items-center">
<div>
<h1 class="h3 mb-0">🔑 API Tokens</h1>
<p class="text-muted">Manage your API tokens for external application access</p>
</div>
<button type="button" class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#createTokenModal">
<i class="fas fa-plus"></i> Create New Token
</button>
<div class="mb-4">
<div class="d-flex justify-content-between align-items-center">
<div>
<h1>🔑 API Tokens</h1>
<p class="text-muted">Manage your API tokens for external application access</p>
</div>
<button type="button" class="btn" onclick="showCreateModal()">
➕ Create New Token
</button>
</div>
</div>
<!-- API Documentation Card -->
<div class="row mb-4">
<div class="col-12">
<div class="card border-info">
<div class="card-header bg-info text-white">
<h5 class="mb-0"><i class="fas fa-info-circle"></i> API Usage</h5>
</div>
<div class="card-body">
<p class="mb-2"><strong>Base URL:</strong> <code>{{ request.url_root }}api/</code></p>
<p class="mb-2"><strong>Authentication:</strong> Include your token in the Authorization header:</p>
<pre class="bg-light p-2 rounded"><code>Authorization: Bearer YOUR_TOKEN_HERE</code></pre>
<p class="mb-2"><strong>Available Endpoints:</strong></p>
<ul class="mb-0">
<li><code>GET /api/fixtures</code> - List all fixtures</li>
<li><code>GET /api/matches</code> - List all matches</li>
<li><code>GET /api/match/&lt;id&gt;</code> - Get match details</li>
</ul>
</div>
</div>
<div class="card mb-4">
<div class="card-header bg-info">
<h5 style="margin: 0; color: white;">ℹ️ API Usage</h5>
</div>
<div class="card-body">
<p><strong>Base URL:</strong> <code>{{ request.url_root }}api/</code></p>
<p><strong>Authentication:</strong> Include your token in the Authorization header:</p>
<pre style="background: #f8f9fa; padding: 0.5rem; border-radius: 4px; margin: 0.5rem 0;"><code>Authorization: Bearer YOUR_TOKEN_HERE</code></pre>
<p><strong>Available Endpoints:</strong></p>
<ul>
<li><code>GET /api/fixtures</code> - List all fixtures</li>
<li><code>GET /api/matches</code> - List all matches</li>
<li><code>GET /api/match/&lt;id&gt;</code> - Get match details</li>
</ul>
</div>
</div>
<!-- Tokens List -->
<div class="row">
<div class="col-12">
<div class="card">
<div class="card-header">
<h5 class="mb-0">Your API Tokens</h5>
</div>
<div class="card-body">
{% if tokens %}
<div class="table-responsive">
<table class="table table-hover">
<thead>
<tr>
<th>Name</th>
<th>Status</th>
<th>Created</th>
<th>Expires</th>
<th>Last Used</th>
<th>Actions</th>
</tr>
</thead>
<tbody id="tokensTableBody">
{% for token in tokens %}
<tr data-token-id="{{ token.id }}">
<td>
<strong>{{ token.name }}</strong>
</td>
<td>
{% if token.is_valid() %}
<span class="badge bg-success">Active</span>
{% elif token.is_expired() %}
<span class="badge bg-danger">Expired</span>
{% else %}
<span class="badge bg-secondary">Revoked</span>
<div class="card">
<div class="card-header">
<h5 style="margin: 0;">Your API Tokens</h5>
</div>
<div class="card-body">
{% if tokens %}
<div class="table-responsive">
<table class="table table-hover">
<thead>
<tr>
<th>Name</th>
<th>Status</th>
<th>Created</th>
<th>Expires</th>
<th>Last Used</th>
<th>Actions</th>
</tr>
</thead>
<tbody id="tokensTableBody">
{% for token in tokens %}
<tr data-token-id="{{ token.id }}">
<td>
<strong>{{ token.name }}</strong>
</td>
<td>
{% if token.is_valid() %}
<span class="badge bg-success">Active</span>
{% elif token.is_expired() %}
<span class="badge bg-danger">Expired</span>
{% else %}
<span class="badge bg-secondary">Revoked</span>
{% endif %}
</td>
<td>
<small class="text-muted">
{{ token.created_at.strftime('%Y-%m-%d %H:%M') }}
</small>
</td>
<td>
<small class="text-muted">
{{ token.expires_at.strftime('%Y-%m-%d %H:%M') }}
</small>
</td>
<td>
<small class="text-muted">
{% if token.last_used_at %}
{{ token.last_used_at.strftime('%Y-%m-%d %H:%M') }}
{% if token.last_used_ip %}
<br>from {{ token.last_used_ip }}
{% endif %}
</td>
<td>
<small class="text-muted">
{{ token.created_at.strftime('%Y-%m-%d %H:%M') }}
</small>
</td>
<td>
<small class="text-muted">
{{ token.expires_at.strftime('%Y-%m-%d %H:%M') }}
</small>
</td>
<td>
<small class="text-muted">
{% if token.last_used_at %}
{{ token.last_used_at.strftime('%Y-%m-%d %H:%M') }}
{% if token.last_used_ip %}
<br>from {{ token.last_used_ip }}
{% endif %}
{% else %}
Never used
{% endif %}
</small>
</td>
<td>
<div class="btn-group btn-group-sm" role="group">
{% if token.is_active and not token.is_expired() %}
<button type="button" class="btn btn-outline-warning"
onclick="revokeToken({{ token.id }}, '{{ token.name }}')"
title="Revoke Token">
<i class="fas fa-ban"></i>
</button>
<button type="button" class="btn btn-outline-info"
onclick="extendToken({{ token.id }}, '{{ token.name }}')"
title="Extend Expiration">
<i class="fas fa-clock"></i>
</button>
{% endif %}
<button type="button" class="btn btn-outline-danger"
onclick="deleteToken({{ token.id }}, '{{ token.name }}')"
title="Delete Token">
<i class="fas fa-trash"></i>
</button>
</div>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% else %}
<div class="text-center py-5">
<i class="fas fa-key fa-3x text-muted mb-3"></i>
<h5 class="text-muted">No API tokens yet</h5>
<p class="text-muted">Create your first API token to start accessing the API</p>
<button type="button" class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#createTokenModal">
<i class="fas fa-plus"></i> Create Your First Token
</button>
</div>
{% endif %}
{% else %}
Never used
{% endif %}
</small>
</td>
<td>
<div class="btn-group">
{% if token.is_active and not token.is_expired() %}
<button type="button" class="btn btn-sm btn-outline-warning revoke-token-btn"
data-token-id="{{ token.id }}"
data-token-name="{{ token.name }}"
title="Revoke Token">
🚫
</button>
<button type="button" class="btn btn-sm btn-outline-info extend-token-btn"
data-token-id="{{ token.id }}"
data-token-name="{{ token.name }}"
title="Extend Expiration">
🕒
</button>
{% endif %}
<button type="button" class="btn btn-sm btn-outline-danger delete-token-btn"
data-token-id="{{ token.id }}"
data-token-name="{{ token.name }}"
title="Delete Token">
🗑️
</button>
</div>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
{% else %}
<div class="text-center py-5">
<div style="font-size: 3rem; margin-bottom: 1rem;">🔑</div>
<h5 class="text-muted">No API tokens yet</h5>
<p class="text-muted">Create your first API token to start accessing the API</p>
<button type="button" class="btn" onclick="showCreateModal()">
➕ Create Your First Token
</button>
</div>
{% endif %}
</div>
</div>
</div>
<!-- Create Token Modal -->
<div class="modal fade" id="createTokenModal" tabindex="-1" aria-labelledby="createTokenModalLabel" aria-hidden="true">
<div class="modal" id="createTokenModal">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="createTokenModalLabel">Create New API Token</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<form id="createTokenForm">
<div class="modal-body">
<div class="mb-3">
<label for="tokenName" class="form-label">Token Name</label>
<input type="text" class="form-control" id="tokenName" name="name" required
placeholder="e.g., Mobile App, Dashboard Integration">
<div class="form-text">Choose a descriptive name to identify this token</div>
</div>
<div class="alert alert-warning">
<i class="fas fa-exclamation-triangle"></i>
<strong>Important:</strong> The token will only be shown once after creation. Make sure to copy and store it securely.
</div>
<div class="modal-header">
<h5 class="modal-title">Create New API Token</h5>
<button type="button" class="close-btn" onclick="hideCreateModal()">×</button>
</div>
<form id="createTokenForm">
<div class="modal-body">
<div class="mb-3">
<label for="tokenName" class="form-label">Token Name</label>
<input type="text" class="form-control" id="tokenName" name="name" required
placeholder="e.g., Mobile App, Dashboard Integration">
<div class="form-text">Choose a descriptive name to identify this token</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="submit" class="btn btn-primary">
<i class="fas fa-plus"></i> Create Token
</button>
<div class="warning-message">
⚠️ <strong>Important:</strong> The token will only be shown once after creation. Make sure to copy and store it securely.
</div>
</form>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" onclick="hideCreateModal()">Cancel</button>
<button type="submit" class="btn">
➕ Create Token
</button>
</div>
</form>
</div>
</div>
<!-- Token Created Modal -->
<div class="modal fade" id="tokenCreatedModal" tabindex="-1" aria-labelledby="tokenCreatedModalLabel" aria-hidden="true">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header bg-success text-white">
<h5 class="modal-title" id="tokenCreatedModalLabel">
<i class="fas fa-check-circle"></i> Token Created Successfully
</h5>
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal" aria-label="Close"></button>
<div class="modal" id="tokenCreatedModal">
<div class="modal-dialog">
<div class="modal-header bg-success">
<h5 class="modal-title" style="color: white;">
✅ Token Created Successfully
</h5>
<button type="button" class="close-btn white" onclick="hideTokenCreatedModal()">×</button>
</div>
<div class="modal-body">
<div class="success-message">
<strong>Your new API token has been created!</strong>
</div>
<div class="modal-body">
<div class="alert alert-success">
<strong>Your new API token has been created!</strong>
</div>
<div class="mb-3">
<label for="newTokenValue" class="form-label"><strong>Token Value:</strong></label>
<div class="input-group">
<input type="text" class="form-control font-monospace" id="newTokenValue" readonly>
<button class="btn btn-outline-secondary" type="button" onclick="copyToken()">
<i class="fas fa-copy"></i> Copy
</button>
</div>
</div>
<div class="alert alert-danger">
<i class="fas fa-exclamation-triangle"></i>
<strong>Warning:</strong> This token will not be shown again. Make sure to copy and store it in a secure location.
<div class="mb-3">
<label for="newTokenValue" class="form-label"><strong>Token Value:</strong></label>
<div class="input-group">
<input type="text" class="form-control font-monospace" id="newTokenValue" readonly>
<button class="btn btn-outline-secondary" type="button" onclick="copyToken()">
📋 Copy
</button>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-primary" data-bs-dismiss="modal">I've Saved the Token</button>
<div class="danger-message">
⚠️ <strong>Warning:</strong> This token will not be shown again. Make sure to copy and store it in a secure location.
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn" onclick="hideTokenCreatedModal()">I've Saved the Token</button>
</div>
</div>
</div>
<!-- Extend Token Modal -->
<div class="modal fade" id="extendTokenModal" tabindex="-1" aria-labelledby="extendTokenModalLabel" aria-hidden="true">
<div class="modal" id="extendTokenModal">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="extendTokenModalLabel">Extend Token Expiration</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<form id="extendTokenForm">
<div class="modal-body">
<p>Extend the expiration date for token: <strong id="extendTokenName"></strong></p>
<div class="mb-3">
<label for="extensionDays" class="form-label">Extend by (days)</label>
<select class="form-select" id="extensionDays" name="days">
<option value="30">30 days</option>
<option value="90">90 days</option>
<option value="180">180 days</option>
<option value="365" selected>365 days (1 year)</option>
</select>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="submit" class="btn btn-info">
<i class="fas fa-clock"></i> Extend Token
</button>
</div>
</form>
<div class="modal-header">
<h5 class="modal-title">Extend Token Expiration</h5>
<button type="button" class="close-btn" onclick="hideExtendModal()">×</button>
</div>
<form id="extendTokenForm">
<div class="modal-body">
<p>Extend the expiration date for token: <strong id="extendTokenName"></strong></p>
<div class="mb-3">
<label for="extensionDays" class="form-label">Extend by (days)</label>
<select class="form-select" id="extensionDays" name="days">
<option value="30">30 days</option>
<option value="90">90 days</option>
<option value="180">180 days</option>
<option value="365" selected>365 days (1 year)</option>
</select>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" onclick="hideExtendModal()">Cancel</button>
<button type="submit" class="btn btn-outline-info">
🕒 Extend Token
</button>
</div>
</form>
</div>
</div>
......@@ -244,6 +545,33 @@
// Global variables for token management
let currentTokenId = null;
// Modal management functions
function showCreateModal() {
document.getElementById('createTokenModal').classList.add('show');
}
function hideCreateModal() {
document.getElementById('createTokenModal').classList.remove('show');
document.getElementById('createTokenForm').reset();
}
function showExtendModal(tokenId, tokenName) {
currentTokenId = tokenId;
document.getElementById('extendTokenName').textContent = tokenName;
document.getElementById('extendTokenModal').classList.add('show');
}
function hideExtendModal() {
document.getElementById('extendTokenModal').classList.remove('show');
currentTokenId = null;
}
function hideTokenCreatedModal() {
document.getElementById('tokenCreatedModal').classList.remove('show');
// Reload page to show the new token in the list
setTimeout(() => location.reload(), 500);
}
// Create token form submission
document.getElementById('createTokenForm').addEventListener('submit', async function(e) {
e.preventDefault();
......@@ -269,21 +597,11 @@ document.getElementById('createTokenForm').addEventListener('submit', async func
if (response.ok) {
// Hide create modal
const createModal = bootstrap.Modal.getInstance(document.getElementById('createTokenModal'));
createModal.hide();
hideCreateModal();
// Show token value in success modal
document.getElementById('newTokenValue').value = data.token.token;
const tokenCreatedModal = new bootstrap.Modal(document.getElementById('tokenCreatedModal'));
tokenCreatedModal.show();
// Reset form
this.reset();
// Reload page after modal is closed
document.getElementById('tokenCreatedModal').addEventListener('hidden.bs.modal', function() {
location.reload();
}, { once: true });
document.getElementById('tokenCreatedModal').classList.add('show');
} else {
showAlert(data.error || 'Failed to create token', 'danger');
......@@ -316,8 +634,7 @@ document.getElementById('extendTokenForm').addEventListener('submit', async func
if (response.ok) {
showAlert(data.message, 'success');
const modal = bootstrap.Modal.getInstance(document.getElementById('extendTokenModal'));
modal.hide();
hideExtendModal();
setTimeout(() => location.reload(), 1500);
} else {
showAlert(data.error || 'Failed to extend token', 'danger');
......@@ -381,13 +698,6 @@ async function deleteToken(tokenId, tokenName) {
}
}
function extendToken(tokenId, tokenName) {
currentTokenId = tokenId;
document.getElementById('extendTokenName').textContent = tokenName;
const modal = new bootstrap.Modal(document.getElementById('extendTokenModal'));
modal.show();
}
function copyToken() {
const tokenInput = document.getElementById('newTokenValue');
const tokenValue = tokenInput.value;
......@@ -427,15 +737,16 @@ function fallbackCopyToken(tokenInput) {
function showAlert(message, type) {
// Create alert element
const alertDiv = document.createElement('div');
alertDiv.className = `alert alert-${type} alert-dismissible fade show`;
alertDiv.innerHTML = `
${message}
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
`;
let alertClass = 'success-message';
if (type === 'danger') alertClass = 'danger-message';
if (type === 'warning') alertClass = 'warning-message';
// Insert at top of container
const container = document.querySelector('.container-fluid');
container.insertBefore(alertDiv, container.firstChild);
alertDiv.className = alertClass;
alertDiv.innerHTML = message;
// Insert at top of content
const content = document.querySelector('.content');
content.insertBefore(alertDiv, content.firstChild);
// Auto-dismiss after 5 seconds
setTimeout(() => {
......@@ -444,5 +755,33 @@ function showAlert(message, type) {
}
}, 5000);
}
// Event listeners for token action buttons
document.addEventListener('click', function(e) {
if (e.target.classList.contains('modal')) {
e.target.classList.remove('show');
}
// Handle revoke token button clicks
if (e.target.classList.contains('revoke-token-btn')) {
const tokenId = e.target.getAttribute('data-token-id');
const tokenName = e.target.getAttribute('data-token-name');
revokeToken(tokenId, tokenName);
}
// Handle extend token button clicks
if (e.target.classList.contains('extend-token-btn')) {
const tokenId = e.target.getAttribute('data-token-id');
const tokenName = e.target.getAttribute('data-token-name');
showExtendModal(tokenId, tokenName);
}
// Handle delete token button clicks
if (e.target.classList.contains('delete-token-btn')) {
const tokenId = e.target.getAttribute('data-token-id');
const tokenName = e.target.getAttribute('data-token-name');
deleteToken(tokenId, tokenName);
}
});
</script>
{% endblock %}
\ No newline at end of file
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment