655 lines
27 KiB
HTML
655 lines
27 KiB
HTML
{% extends 'base.html' %}
|
|
{% load static %}
|
|
|
|
{% block title %}API Management{% endblock %}
|
|
|
|
{% block css %}
|
|
<link href="{% static 'assets/plugins/datatables.net-bs5/css/dataTables.bootstrap5.min.css' %}" rel="stylesheet" />
|
|
<link href="{% static 'assets/plugins/prismjs/themes/prism.css' %}" rel="stylesheet" />
|
|
{% endblock %}
|
|
|
|
{% block content %}
|
|
<div id="content" class="app-content">
|
|
<div class="container">
|
|
<ul class="breadcrumb">
|
|
<li class="breadcrumb-item"><a href="{% url 'core:dashboard' %}">Dashboard</a></li>
|
|
<li class="breadcrumb-item active">API Management</li>
|
|
</ul>
|
|
|
|
<div class="row align-items-center mb-3">
|
|
<div class="col">
|
|
<h1 class="page-header">API Management</h1>
|
|
<p class="text-muted">Manage API keys, endpoints, and monitor API usage</p>
|
|
</div>
|
|
<div class="col-auto">
|
|
<button type="button" class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#createApiKeyModal">
|
|
<i class="fa fa-plus me-2"></i>Create API Key
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- API Statistics -->
|
|
<div class="row mb-4">
|
|
<div class="col-md-3">
|
|
<div class="card text-center">
|
|
<div class="card-body">
|
|
<div class="w-60px h-60px bg-primary bg-opacity-20 d-flex align-items-center justify-content-center rounded-circle mx-auto mb-3">
|
|
<i class="fa fa-key fa-2x text-primary"></i>
|
|
</div>
|
|
<h5>Active API Keys</h5>
|
|
<div class="fs-24px fw-600 text-primary">{{ active_api_keys }}</div>
|
|
<div class="text-muted small">{{ total_api_keys }} total</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="col-md-3">
|
|
<div class="card text-center">
|
|
<div class="card-body">
|
|
<div class="w-60px h-60px bg-info bg-opacity-20 d-flex align-items-center justify-content-center rounded-circle mx-auto mb-3">
|
|
<i class="fa fa-exchange fa-2x text-info"></i>
|
|
</div>
|
|
<h5>API Calls Today</h5>
|
|
<div class="fs-24px fw-600 text-info">{{ api_calls_today|floatformat:0 }}</div>
|
|
<div class="text-muted small">{{ api_calls_change }}% vs yesterday</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="col-md-3">
|
|
<div class="card text-center">
|
|
<div class="card-body">
|
|
<div class="w-60px h-60px bg-success bg-opacity-20 d-flex align-items-center justify-content-center rounded-circle mx-auto mb-3">
|
|
<i class="fa fa-check-circle fa-2x text-success"></i>
|
|
</div>
|
|
<h5>Success Rate</h5>
|
|
<div class="fs-24px fw-600 text-success">{{ success_rate }}%</div>
|
|
<div class="text-muted small">Last 24 hours</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="col-md-3">
|
|
<div class="card text-center">
|
|
<div class="card-body">
|
|
<div class="w-60px h-60px bg-warning bg-opacity-20 d-flex align-items-center justify-content-center rounded-circle mx-auto mb-3">
|
|
<i class="fa fa-clock fa-2x text-warning"></i>
|
|
</div>
|
|
<h5>Avg Response Time</h5>
|
|
<div class="fs-24px fw-600 text-warning">{{ avg_response_time }}ms</div>
|
|
<div class="text-muted small">Last hour</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- API Keys Management -->
|
|
<div class="card mb-4">
|
|
<div class="card-header">
|
|
<h4 class="card-title">API Keys</h4>
|
|
<div class="card-toolbar">
|
|
<div class="btn-group">
|
|
<button type="button" class="btn btn-outline-secondary btn-sm" onclick="refreshApiKeys()">
|
|
<i class="fa fa-refresh"></i>
|
|
</button>
|
|
<button type="button" class="btn btn-outline-primary btn-sm" onclick="exportApiKeys()">
|
|
<i class="fa fa-download"></i>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="card-body">
|
|
<div class="table-responsive">
|
|
<table class="table table-striped" id="apiKeysTable">
|
|
<thead>
|
|
<tr>
|
|
<th>Name</th>
|
|
<th>Key</th>
|
|
<th>Permissions</th>
|
|
<th>Usage</th>
|
|
<th>Rate Limit</th>
|
|
<th>Last Used</th>
|
|
<th>Status</th>
|
|
<th>Actions</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{% for api_key in api_keys %}
|
|
<tr>
|
|
<td>
|
|
<div class="fw-bold">{{ api_key.name }}</div>
|
|
<div class="text-muted small">{{ api_key.description|default:"No description" }}</div>
|
|
</td>
|
|
<td>
|
|
<div class="d-flex align-items-center">
|
|
<code class="me-2">{{ api_key.key_preview }}</code>
|
|
<button type="button" class="btn btn-outline-secondary btn-sm" onclick="copyApiKey('{{ api_key.key }}')">
|
|
<i class="fa fa-copy"></i>
|
|
</button>
|
|
</div>
|
|
</td>
|
|
<td>
|
|
{% for permission in api_key.permissions.all %}
|
|
<span class="badge bg-info me-1">{{ permission.name }}</span>
|
|
{% endfor %}
|
|
</td>
|
|
<td>
|
|
<div class="fw-bold">{{ api_key.usage_count }}</div>
|
|
<div class="text-muted small">{{ api_key.usage_today }} today</div>
|
|
</td>
|
|
<td>
|
|
{% if api_key.rate_limit %}
|
|
<div>{{ api_key.rate_limit }}/{{ api_key.rate_limit_period }}</div>
|
|
<div class="progress mt-1" style="height: 4px;">
|
|
<div class="progress-bar" style="width: {{ api_key.rate_limit_usage }}%"></div>
|
|
</div>
|
|
{% else %}
|
|
<span class="text-muted">Unlimited</span>
|
|
{% endif %}
|
|
</td>
|
|
<td>
|
|
{% if api_key.last_used %}
|
|
<div>{{ api_key.last_used|date:"M d, Y" }}</div>
|
|
<div class="text-muted small">{{ api_key.last_used|timesince }} ago</div>
|
|
{% else %}
|
|
<span class="text-muted">Never</span>
|
|
{% endif %}
|
|
</td>
|
|
<td>
|
|
{% if api_key.is_active %}
|
|
<span class="badge bg-success">Active</span>
|
|
{% else %}
|
|
<span class="badge bg-danger">Inactive</span>
|
|
{% endif %}
|
|
{% if api_key.expires_at %}
|
|
<div class="text-muted small">Expires {{ api_key.expires_at|date:"M d, Y" }}</div>
|
|
{% endif %}
|
|
</td>
|
|
<td>
|
|
<div class="btn-group">
|
|
<button type="button" class="btn btn-outline-primary btn-sm" onclick="editApiKey('{{ api_key.id }}')">
|
|
<i class="fa fa-edit"></i>
|
|
</button>
|
|
<button type="button" class="btn btn-outline-info btn-sm" onclick="viewApiKeyUsage('{{ api_key.id }}')">
|
|
<i class="fa fa-chart-line"></i>
|
|
</button>
|
|
{% if api_key.is_active %}
|
|
<button type="button" class="btn btn-outline-warning btn-sm" onclick="deactivateApiKey('{{ api_key.id }}')">
|
|
<i class="fa fa-pause"></i>
|
|
</button>
|
|
{% else %}
|
|
<button type="button" class="btn btn-outline-success btn-sm" onclick="activateApiKey('{{ api_key.id }}')">
|
|
<i class="fa fa-play"></i>
|
|
</button>
|
|
{% endif %}
|
|
<button type="button" class="btn btn-outline-danger btn-sm" onclick="deleteApiKey('{{ api_key.id }}')">
|
|
<i class="fa fa-trash"></i>
|
|
</button>
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
{% endfor %}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- API Endpoints -->
|
|
<div class="card mb-4">
|
|
<div class="card-header">
|
|
<h4 class="card-title">Available Endpoints</h4>
|
|
<div class="card-toolbar">
|
|
<button type="button" class="btn btn-outline-primary btn-sm" onclick="refreshEndpoints()">
|
|
<i class="fa fa-refresh"></i>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
<div class="card-body">
|
|
<div class="row">
|
|
{% for endpoint_group in api_endpoints %}
|
|
<div class="col-md-6 mb-4">
|
|
<div class="card">
|
|
<div class="card-header">
|
|
<h5 class="card-title">{{ endpoint_group.name }}</h5>
|
|
<span class="badge bg-primary">{{ endpoint_group.endpoints|length }} endpoints</span>
|
|
</div>
|
|
<div class="card-body">
|
|
{% for endpoint in endpoint_group.endpoints %}
|
|
<div class="d-flex align-items-center mb-2">
|
|
<span class="badge bg-{{ endpoint.method_color }} me-2">{{ endpoint.method }}</span>
|
|
<code class="flex-1">{{ endpoint.path }}</code>
|
|
<button type="button" class="btn btn-outline-secondary btn-sm ms-2" onclick="testEndpoint('{{ endpoint.path }}', '{{ endpoint.method }}')">
|
|
<i class="fa fa-play"></i>
|
|
</button>
|
|
</div>
|
|
{% endfor %}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{% endfor %}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- API Usage Analytics -->
|
|
<div class="card">
|
|
<div class="card-header">
|
|
<h4 class="card-title">Usage Analytics</h4>
|
|
<div class="card-toolbar">
|
|
<select class="form-select form-select-sm" onchange="updateUsageChart(this.value)">
|
|
<option value="24h">Last 24 Hours</option>
|
|
<option value="7d" selected>Last 7 Days</option>
|
|
<option value="30d">Last 30 Days</option>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
<div class="card-body">
|
|
<canvas id="usageChart" height="300"></canvas>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Create API Key Modal -->
|
|
<div class="modal fade" id="createApiKeyModal" tabindex="-1">
|
|
<div class="modal-dialog">
|
|
<div class="modal-content">
|
|
<div class="modal-header">
|
|
<h5 class="modal-title">Create API Key</h5>
|
|
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
|
</div>
|
|
<form id="createApiKeyForm">
|
|
<div class="modal-body">
|
|
{% csrf_token %}
|
|
|
|
<div class="mb-3">
|
|
<label class="form-label">Name</label>
|
|
<input type="text" name="name" class="form-control" required>
|
|
</div>
|
|
|
|
<div class="mb-3">
|
|
<label class="form-label">Description</label>
|
|
<textarea name="description" class="form-control" rows="3"></textarea>
|
|
</div>
|
|
|
|
<div class="mb-3">
|
|
<label class="form-label">Permissions</label>
|
|
<div class="row">
|
|
{% for permission in available_permissions %}
|
|
<div class="col-md-6">
|
|
<div class="form-check">
|
|
<input class="form-check-input" type="checkbox" name="permissions" value="{{ permission.id }}">
|
|
<label class="form-check-label">{{ permission.name }}</label>
|
|
</div>
|
|
</div>
|
|
{% endfor %}
|
|
</div>
|
|
</div>
|
|
|
|
<div class="mb-3">
|
|
<label class="form-label">Rate Limit</label>
|
|
<div class="row">
|
|
<div class="col-md-6">
|
|
<input type="number" name="rate_limit" class="form-control" placeholder="Requests">
|
|
</div>
|
|
<div class="col-md-6">
|
|
<select name="rate_limit_period" class="form-select">
|
|
<option value="minute">per minute</option>
|
|
<option value="hour">per hour</option>
|
|
<option value="day">per day</option>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="mb-3">
|
|
<label class="form-label">Expiration Date (Optional)</label>
|
|
<input type="date" name="expires_at" class="form-control">
|
|
</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">Create API Key</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- API Key Details Modal -->
|
|
<div class="modal fade" id="apiKeyDetailsModal" tabindex="-1">
|
|
<div class="modal-dialog modal-lg">
|
|
<div class="modal-content">
|
|
<div class="modal-header">
|
|
<h5 class="modal-title">API Key Details</h5>
|
|
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
|
</div>
|
|
<div class="modal-body" id="apiKeyDetailsContent">
|
|
<!-- Content loaded via AJAX -->
|
|
</div>
|
|
<div class="modal-footer">
|
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- API Test Modal -->
|
|
<div class="modal fade" id="apiTestModal" tabindex="-1">
|
|
<div class="modal-dialog modal-lg">
|
|
<div class="modal-content">
|
|
<div class="modal-header">
|
|
<h5 class="modal-title">Test API Endpoint</h5>
|
|
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
|
</div>
|
|
<div class="modal-body">
|
|
<div class="mb-3">
|
|
<label class="form-label">Endpoint</label>
|
|
<div class="input-group">
|
|
<span class="input-group-text" id="testMethod">GET</span>
|
|
<input type="text" class="form-control" id="testEndpoint" readonly>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="mb-3">
|
|
<label class="form-label">Headers</label>
|
|
<textarea class="form-control" id="testHeaders" rows="4">Content-Type: application/json
|
|
Authorization: Bearer YOUR_API_KEY</textarea>
|
|
</div>
|
|
|
|
<div class="mb-3" id="testBodySection" style="display: none;">
|
|
<label class="form-label">Request Body</label>
|
|
<textarea class="form-control" id="testBody" rows="6">{}</textarea>
|
|
</div>
|
|
|
|
<div class="mb-3">
|
|
<button type="button" class="btn btn-primary" onclick="executeApiTest()">
|
|
<i class="fa fa-play me-2"></i>Send Request
|
|
</button>
|
|
</div>
|
|
|
|
<div id="testResponse" style="display: none;">
|
|
<label class="form-label">Response</label>
|
|
<pre><code id="testResponseContent" class="language-json"></code></pre>
|
|
</div>
|
|
</div>
|
|
<div class="modal-footer">
|
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{% endblock %}
|
|
|
|
{% block js %}
|
|
<script src="{% static 'assets/plugins/datatables.net/js/jquery.dataTables.min.js' %}"></script>
|
|
<script src="{% static 'assets/plugins/datatables.net-bs5/js/dataTables.bootstrap5.min.js' %}"></script>
|
|
<script src="{% static 'assets/plugins/chart.js/dist/Chart.min.js' %}"></script>
|
|
<script src="{% static 'assets/plugins/prismjs/components/prism-core.min.js' %}"></script>
|
|
<script src="{% static 'assets/plugins/prismjs/components/prism-json.min.js' %}"></script>
|
|
|
|
<script>
|
|
var usageChart;
|
|
|
|
$(document).ready(function() {
|
|
// Initialize DataTable
|
|
$('#apiKeysTable').DataTable({
|
|
responsive: true,
|
|
pageLength: 10,
|
|
order: [[5, 'desc']],
|
|
columnDefs: [
|
|
{ orderable: false, targets: [7] }
|
|
]
|
|
});
|
|
|
|
// Initialize usage chart
|
|
initializeUsageChart();
|
|
|
|
// Create API Key form
|
|
$('#createApiKeyForm').submit(function(e) {
|
|
e.preventDefault();
|
|
|
|
var formData = new FormData(this);
|
|
|
|
$.post('{% url "core:create_api_key" %}', formData, function(response) {
|
|
if (response.success) {
|
|
$('#createApiKeyModal').modal('hide');
|
|
toastr.success('API key created successfully');
|
|
|
|
// Show the new API key
|
|
showNewApiKey(response.api_key);
|
|
|
|
// Refresh the table
|
|
location.reload();
|
|
} else {
|
|
toastr.error('Failed to create API key: ' + response.error);
|
|
}
|
|
}).fail(function() {
|
|
toastr.error('Failed to create API key');
|
|
});
|
|
});
|
|
});
|
|
|
|
function initializeUsageChart() {
|
|
var ctx = document.getElementById('usageChart').getContext('2d');
|
|
usageChart = new Chart(ctx, {
|
|
type: 'line',
|
|
data: {
|
|
labels: {{ usage_labels|safe }},
|
|
datasets: [{
|
|
label: 'API Calls',
|
|
data: {{ usage_data|safe }},
|
|
borderColor: 'rgb(54, 162, 235)',
|
|
backgroundColor: 'rgba(54, 162, 235, 0.1)',
|
|
tension: 0.4
|
|
}, {
|
|
label: 'Errors',
|
|
data: {{ error_data|safe }},
|
|
borderColor: 'rgb(255, 99, 132)',
|
|
backgroundColor: 'rgba(255, 99, 132, 0.1)',
|
|
tension: 0.4
|
|
}]
|
|
},
|
|
options: {
|
|
responsive: true,
|
|
maintainAspectRatio: false,
|
|
scales: {
|
|
y: {
|
|
beginAtZero: true
|
|
}
|
|
},
|
|
plugins: {
|
|
legend: {
|
|
position: 'top'
|
|
}
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
function copyApiKey(apiKey) {
|
|
navigator.clipboard.writeText(apiKey).then(function() {
|
|
toastr.success('API key copied to clipboard');
|
|
}).catch(function() {
|
|
toastr.error('Failed to copy API key');
|
|
});
|
|
}
|
|
|
|
function editApiKey(apiKeyId) {
|
|
// Load API key details and show edit modal
|
|
$.get('{% url "core:api_key_detail" 0 %}'.replace('0', apiKeyId), function(data) {
|
|
// Populate edit form with current values
|
|
showEditApiKeyModal(data);
|
|
});
|
|
}
|
|
|
|
function viewApiKeyUsage(apiKeyId) {
|
|
$.get('{% url "core:api_key_usage" 0 %}'.replace('0', apiKeyId), function(data) {
|
|
$('#apiKeyDetailsContent').html(data);
|
|
$('#apiKeyDetailsModal').modal('show');
|
|
});
|
|
}
|
|
|
|
function activateApiKey(apiKeyId) {
|
|
$.post('{% url "core:toggle_api_key" %}', {
|
|
'api_key_id': apiKeyId,
|
|
'action': 'activate',
|
|
'csrfmiddlewaretoken': '{{ csrf_token }}'
|
|
}, function(response) {
|
|
if (response.success) {
|
|
toastr.success('API key activated');
|
|
location.reload();
|
|
} else {
|
|
toastr.error('Failed to activate API key');
|
|
}
|
|
});
|
|
}
|
|
|
|
function deactivateApiKey(apiKeyId) {
|
|
if (confirm('Deactivate this API key? It will stop working immediately.')) {
|
|
$.post('{% url "core:toggle_api_key" %}', {
|
|
'api_key_id': apiKeyId,
|
|
'action': 'deactivate',
|
|
'csrfmiddlewaretoken': '{{ csrf_token }}'
|
|
}, function(response) {
|
|
if (response.success) {
|
|
toastr.success('API key deactivated');
|
|
location.reload();
|
|
} else {
|
|
toastr.error('Failed to deactivate API key');
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
function deleteApiKey(apiKeyId) {
|
|
if (confirm('Delete this API key? This action cannot be undone.')) {
|
|
$.post('{% url "core:delete_api_key" %}', {
|
|
'api_key_id': apiKeyId,
|
|
'csrfmiddlewaretoken': '{{ csrf_token }}'
|
|
}, function(response) {
|
|
if (response.success) {
|
|
toastr.success('API key deleted');
|
|
location.reload();
|
|
} else {
|
|
toastr.error('Failed to delete API key');
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
function testEndpoint(path, method) {
|
|
$('#testEndpoint').val(path);
|
|
$('#testMethod').text(method);
|
|
|
|
if (method === 'POST' || method === 'PUT' || method === 'PATCH') {
|
|
$('#testBodySection').show();
|
|
} else {
|
|
$('#testBodySection').hide();
|
|
}
|
|
|
|
$('#testResponse').hide();
|
|
$('#apiTestModal').modal('show');
|
|
}
|
|
|
|
function executeApiTest() {
|
|
var endpoint = $('#testEndpoint').val();
|
|
var method = $('#testMethod').text();
|
|
var headers = $('#testHeaders').val();
|
|
var body = $('#testBody').val();
|
|
|
|
// Parse headers
|
|
var headerObj = {};
|
|
headers.split('\n').forEach(function(line) {
|
|
var parts = line.split(':');
|
|
if (parts.length === 2) {
|
|
headerObj[parts[0].trim()] = parts[1].trim();
|
|
}
|
|
});
|
|
|
|
// Make the API request
|
|
$.ajax({
|
|
url: endpoint,
|
|
method: method,
|
|
headers: headerObj,
|
|
data: method !== 'GET' ? body : undefined,
|
|
success: function(response) {
|
|
$('#testResponseContent').text(JSON.stringify(response, null, 2));
|
|
Prism.highlightElement(document.getElementById('testResponseContent'));
|
|
$('#testResponse').show();
|
|
},
|
|
error: function(xhr) {
|
|
$('#testResponseContent').text(JSON.stringify({
|
|
status: xhr.status,
|
|
statusText: xhr.statusText,
|
|
response: xhr.responseText
|
|
}, null, 2));
|
|
Prism.highlightElement(document.getElementById('testResponseContent'));
|
|
$('#testResponse').show();
|
|
}
|
|
});
|
|
}
|
|
|
|
function updateUsageChart(period) {
|
|
$.get('{% url "core:api_usage_data" %}', {
|
|
'period': period
|
|
}, function(data) {
|
|
usageChart.data.labels = data.labels;
|
|
usageChart.data.datasets[0].data = data.usage_data;
|
|
usageChart.data.datasets[1].data = data.error_data;
|
|
usageChart.update();
|
|
});
|
|
}
|
|
|
|
function refreshApiKeys() {
|
|
location.reload();
|
|
}
|
|
|
|
function refreshEndpoints() {
|
|
location.reload();
|
|
}
|
|
|
|
function exportApiKeys() {
|
|
window.location.href = '{% url "core:export_api_keys" %}';
|
|
}
|
|
|
|
function showNewApiKey(apiKey) {
|
|
// Show modal with the new API key for copying
|
|
var modal = `
|
|
<div class="modal fade" id="newApiKeyModal" tabindex="-1">
|
|
<div class="modal-dialog">
|
|
<div class="modal-content">
|
|
<div class="modal-header">
|
|
<h5 class="modal-title">New API Key Created</h5>
|
|
</div>
|
|
<div class="modal-body">
|
|
<div class="alert alert-warning">
|
|
<strong>Important:</strong> Copy this API key now. You won't be able to see it again.
|
|
</div>
|
|
<div class="mb-3">
|
|
<label class="form-label">API Key</label>
|
|
<div class="input-group">
|
|
<input type="text" class="form-control" value="${apiKey}" readonly>
|
|
<button type="button" class="btn btn-outline-secondary" onclick="copyApiKey('${apiKey}')">
|
|
<i class="fa fa-copy"></i>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="modal-footer">
|
|
<button type="button" class="btn btn-primary" data-bs-dismiss="modal">I've copied the key</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
`;
|
|
|
|
$('body').append(modal);
|
|
$('#newApiKeyModal').modal('show');
|
|
|
|
$('#newApiKeyModal').on('hidden.bs.modal', function() {
|
|
$(this).remove();
|
|
});
|
|
}
|
|
</script>
|
|
{% endblock %}
|
|
|