feat(frontend): T44 setup FastAPI static files and templates
- Mount static files on /static endpoint - Configure Jinja2Templates with directory structure - Create base template with Pico.css, HTMX, Chart.js - Create all template subdirectories (auth, dashboard, keys, tokens, profile, components) - Create initial CSS and JS files - Add tests for static files and templates configuration Tests: 12 passing Coverage: 100% on new configuration code
This commit is contained in:
42
templates/auth/login.html
Normal file
42
templates/auth/login.html
Normal file
@@ -0,0 +1,42 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Login - OpenRouter Monitor{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<article class="grid">
|
||||
<div>
|
||||
<h1>Login</h1>
|
||||
<p>Enter your credentials to access the dashboard.</p>
|
||||
</div>
|
||||
<div>
|
||||
<form action="/login" method="POST" hx-post="/login" hx-swap="outerHTML" hx-target="this">
|
||||
{% if error %}
|
||||
<div class="alert alert-danger" role="alert">
|
||||
{{ error }}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<label for="email">
|
||||
Email
|
||||
<input type="email" id="email" name="email" placeholder="your@email.com" required>
|
||||
</label>
|
||||
|
||||
<label for="password">
|
||||
Password
|
||||
<input type="password" id="password" name="password" placeholder="Password" required minlength="8">
|
||||
</label>
|
||||
|
||||
<fieldset>
|
||||
<label for="remember">
|
||||
<input type="checkbox" id="remember" name="remember" role="switch">
|
||||
Remember me
|
||||
</label>
|
||||
</fieldset>
|
||||
|
||||
<button type="submit">Login</button>
|
||||
</form>
|
||||
|
||||
<p>Don't have an account? <a href="/register">Register here</a>.</p>
|
||||
</div>
|
||||
</article>
|
||||
{% endblock %}
|
||||
64
templates/auth/register.html
Normal file
64
templates/auth/register.html
Normal file
@@ -0,0 +1,64 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Register - OpenRouter Monitor{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<article class="grid">
|
||||
<div>
|
||||
<h1>Create Account</h1>
|
||||
<p>Register to start monitoring your OpenRouter API keys.</p>
|
||||
</div>
|
||||
<div>
|
||||
<form action="/register" method="POST" hx-post="/register" hx-swap="outerHTML" hx-target="this">
|
||||
{% if error %}
|
||||
<div class="alert alert-danger" role="alert">
|
||||
{{ error }}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<label for="email">
|
||||
Email
|
||||
<input type="email" id="email" name="email" placeholder="your@email.com" required>
|
||||
</label>
|
||||
|
||||
<label for="password">
|
||||
Password
|
||||
<input
|
||||
type="password"
|
||||
id="password"
|
||||
name="password"
|
||||
placeholder="Password"
|
||||
required
|
||||
minlength="8"
|
||||
pattern="^(?=.*[a-z])(?=.*[A-Z])(?=.*\d).+$"
|
||||
title="Password must contain at least one lowercase letter, one uppercase letter, and one number"
|
||||
>
|
||||
<small>Minimum 8 characters with uppercase, lowercase, and number</small>
|
||||
</label>
|
||||
|
||||
<label for="password_confirm">
|
||||
Confirm Password
|
||||
<input type="password" id="password_confirm" name="password_confirm" placeholder="Confirm password" required>
|
||||
</label>
|
||||
|
||||
<button type="submit">Register</button>
|
||||
</form>
|
||||
|
||||
<p>Already have an account? <a href="/login">Login here</a>.</p>
|
||||
</div>
|
||||
</article>
|
||||
|
||||
<script>
|
||||
// Client-side password match validation
|
||||
document.getElementById('password_confirm').addEventListener('input', function() {
|
||||
const password = document.getElementById('password').value;
|
||||
const confirm = this.value;
|
||||
|
||||
if (password !== confirm) {
|
||||
this.setCustomValidity('Passwords do not match');
|
||||
} else {
|
||||
this.setCustomValidity('');
|
||||
}
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
40
templates/base.html
Normal file
40
templates/base.html
Normal file
@@ -0,0 +1,40 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta name="description" content="OpenRouter API Key Monitor - Monitor and manage your OpenRouter API keys">
|
||||
{% if csrf_token %}
|
||||
<meta name="csrf-token" content="{{ csrf_token }}">
|
||||
{% endif %}
|
||||
<title>{% block title %}OpenRouter Monitor{% endblock %}</title>
|
||||
|
||||
<!-- Pico.css for styling -->
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@picocss/pico@1/css/pico.min.css">
|
||||
|
||||
<!-- Custom styles -->
|
||||
<link rel="stylesheet" href="/static/css/style.css">
|
||||
|
||||
<!-- HTMX for dynamic content -->
|
||||
<script src="https://unpkg.com/htmx.org@1.9.10"></script>
|
||||
|
||||
<!-- Chart.js for charts -->
|
||||
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
||||
|
||||
{% block extra_head %}{% endblock %}
|
||||
</head>
|
||||
<body>
|
||||
{% include 'components/navbar.html' %}
|
||||
|
||||
<main class="container">
|
||||
{% block content %}{% endblock %}
|
||||
</main>
|
||||
|
||||
{% include 'components/footer.html' %}
|
||||
|
||||
<!-- Custom JavaScript -->
|
||||
<script src="/static/js/main.js"></script>
|
||||
|
||||
{% block extra_scripts %}{% endblock %}
|
||||
</body>
|
||||
</html>
|
||||
6
templates/components/footer.html
Normal file
6
templates/components/footer.html
Normal file
@@ -0,0 +1,6 @@
|
||||
<footer class="footer">
|
||||
<div class="container">
|
||||
<p>© 2024 OpenRouter API Key Monitor. All rights reserved.</p>
|
||||
<p><small>Version 1.0.0</small></p>
|
||||
</div>
|
||||
</footer>
|
||||
21
templates/components/navbar.html
Normal file
21
templates/components/navbar.html
Normal file
@@ -0,0 +1,21 @@
|
||||
<nav class="container-fluid">
|
||||
<ul>
|
||||
<li><strong><a href="/" class="navbar-brand">OpenRouter Monitor</a></strong></li>
|
||||
</ul>
|
||||
<ul>
|
||||
{% if user %}
|
||||
<li><a href="/dashboard">Dashboard</a></li>
|
||||
<li><a href="/keys">API Keys</a></li>
|
||||
<li><a href="/tokens">Tokens</a></li>
|
||||
<li><a href="/profile">Profile</a></li>
|
||||
<li>
|
||||
<form action="/logout" method="POST" style="display: inline;" hx-post="/logout" hx-redirect="/login">
|
||||
<button type="submit" class="outline">Logout</button>
|
||||
</form>
|
||||
</li>
|
||||
{% else %}
|
||||
<li><a href="/login">Login</a></li>
|
||||
<li><a href="/register" role="button">Register</a></li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
</nav>
|
||||
133
templates/dashboard/index.html
Normal file
133
templates/dashboard/index.html
Normal file
@@ -0,0 +1,133 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Dashboard - OpenRouter Monitor{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<h1>Dashboard</h1>
|
||||
|
||||
<!-- Summary Cards -->
|
||||
<div class="grid">
|
||||
<article>
|
||||
<header>
|
||||
<h3>Total Requests</h3>
|
||||
</header>
|
||||
<p style="font-size: 2rem; font-weight: bold;">{{ stats.total_requests | default(0) }}</p>
|
||||
</article>
|
||||
|
||||
<article>
|
||||
<header>
|
||||
<h3>Total Cost</h3>
|
||||
</header>
|
||||
<p style="font-size: 2rem; font-weight: bold;">${{ stats.total_cost | default(0) | round(2) }}</p>
|
||||
</article>
|
||||
|
||||
<article>
|
||||
<header>
|
||||
<h3>API Keys</h3>
|
||||
</header>
|
||||
<p style="font-size: 2rem; font-weight: bold;">{{ stats.api_keys_count | default(0) }}</p>
|
||||
</article>
|
||||
</div>
|
||||
|
||||
<!-- Charts -->
|
||||
<div class="grid">
|
||||
<article>
|
||||
<header>
|
||||
<h3>Usage Over Time</h3>
|
||||
</header>
|
||||
<canvas id="usageChart" height="200"></canvas>
|
||||
</article>
|
||||
|
||||
<article>
|
||||
<header>
|
||||
<h3>Top Models</h3>
|
||||
</header>
|
||||
<canvas id="modelsChart" height="200"></canvas>
|
||||
</article>
|
||||
</div>
|
||||
|
||||
<!-- Recent Activity -->
|
||||
<article>
|
||||
<header>
|
||||
<h3>Recent Usage</h3>
|
||||
</header>
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Date</th>
|
||||
<th>Model</th>
|
||||
<th>Requests</th>
|
||||
<th>Cost</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for usage in recent_usage %}
|
||||
<tr>
|
||||
<td>{{ usage.date }}</td>
|
||||
<td>{{ usage.model }}</td>
|
||||
<td>{{ usage.requests }}</td>
|
||||
<td>${{ usage.cost | round(4) }}</td>
|
||||
</tr>
|
||||
{% else %}
|
||||
<tr>
|
||||
<td colspan="4" style="text-align: center;">No usage data available</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</article>
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_scripts %}
|
||||
<script>
|
||||
// Usage Chart
|
||||
const usageCtx = document.getElementById('usageChart').getContext('2d');
|
||||
const usageData = {{ chart_data | default({"labels": [], "data": []}) | tojson }};
|
||||
|
||||
new Chart(usageCtx, {
|
||||
type: 'line',
|
||||
data: {
|
||||
labels: usageData.labels || [],
|
||||
datasets: [{
|
||||
label: 'Requests',
|
||||
data: usageData.data || [],
|
||||
borderColor: '#2563eb',
|
||||
backgroundColor: 'rgba(37, 99, 235, 0.1)',
|
||||
fill: true
|
||||
}]
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
scales: {
|
||||
y: {
|
||||
beginAtZero: true
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Models Chart
|
||||
const modelsCtx = document.getElementById('modelsChart').getContext('2d');
|
||||
const modelsData = {{ models_data | default({"labels": [], "data": []}) | tojson }};
|
||||
|
||||
new Chart(modelsCtx, {
|
||||
type: 'doughnut',
|
||||
data: {
|
||||
labels: modelsData.labels || [],
|
||||
datasets: [{
|
||||
data: modelsData.data || [],
|
||||
backgroundColor: [
|
||||
'#2563eb',
|
||||
'#10b981',
|
||||
'#f59e0b',
|
||||
'#ef4444',
|
||||
'#8b5cf6'
|
||||
]
|
||||
}]
|
||||
},
|
||||
options: {
|
||||
responsive: true
|
||||
}
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
92
templates/keys/index.html
Normal file
92
templates/keys/index.html
Normal file
@@ -0,0 +1,92 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}API Keys - OpenRouter Monitor{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<h1>API Keys Management</h1>
|
||||
|
||||
<!-- Add New Key Form -->
|
||||
<article>
|
||||
<header>
|
||||
<h3>Add New API Key</h3>
|
||||
</header>
|
||||
<form action="/keys" method="POST" hx-post="/keys" hx-swap="beforeend" hx-target="#keys-table tbody">
|
||||
<div class="grid">
|
||||
<label for="key_name">
|
||||
Key Name
|
||||
<input type="text" id="key_name" name="name" placeholder="Production Key" required>
|
||||
</label>
|
||||
|
||||
<label for="key_value">
|
||||
OpenRouter API Key
|
||||
<input
|
||||
type="password"
|
||||
id="key_value"
|
||||
name="key_value"
|
||||
placeholder="sk-or-..."
|
||||
required
|
||||
pattern="^sk-or-[a-zA-Z0-9]+$"
|
||||
title="Must be a valid OpenRouter API key starting with 'sk-or-'"
|
||||
>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<button type="submit">Add Key</button>
|
||||
</form>
|
||||
</article>
|
||||
|
||||
<!-- Keys List -->
|
||||
<article>
|
||||
<header>
|
||||
<h3>Your API Keys</h3>
|
||||
</header>
|
||||
<table class="table" id="keys-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Status</th>
|
||||
<th>Last Used</th>
|
||||
<th>Created</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for key in api_keys %}
|
||||
<tr id="key-{{ key.id }}">
|
||||
<td>{{ key.name }}</td>
|
||||
<td>
|
||||
{% if key.is_active %}
|
||||
<span style="color: var(--success-color);">Active</span>
|
||||
{% else %}
|
||||
<span style="color: var(--danger-color);">Inactive</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>{{ key.last_used_at or 'Never' }}</td>
|
||||
<td>{{ key.created_at }}</td>
|
||||
<td>
|
||||
<button
|
||||
class="outline secondary"
|
||||
hx-delete="/keys/{{ key.id }}"
|
||||
hx-confirm="Are you sure you want to delete this key?"
|
||||
hx-target="#key-{{ key.id }}"
|
||||
hx-swap="outerHTML"
|
||||
>
|
||||
Delete
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
{% else %}
|
||||
<tr>
|
||||
<td colspan="5" style="text-align: center;">No API keys found. Add your first key above.</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</article>
|
||||
|
||||
<!-- Security Notice -->
|
||||
<div class="alert" role="alert">
|
||||
<strong>Security Notice:</strong> Your API keys are encrypted and never displayed after creation.
|
||||
Only metadata (name, status, usage) is shown here.
|
||||
</div>
|
||||
{% endblock %}
|
||||
87
templates/profile/index.html
Normal file
87
templates/profile/index.html
Normal file
@@ -0,0 +1,87 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Profile - OpenRouter Monitor{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<h1>User Profile</h1>
|
||||
|
||||
<!-- Profile Information -->
|
||||
<article>
|
||||
<header>
|
||||
<h3>Account Information</h3>
|
||||
</header>
|
||||
<p><strong>Email:</strong> {{ user.email }}</p>
|
||||
<p><strong>Account Created:</strong> {{ user.created_at }}</p>
|
||||
</article>
|
||||
|
||||
<!-- Change Password -->
|
||||
<article>
|
||||
<header>
|
||||
<h3>Change Password</h3>
|
||||
</header>
|
||||
<form action="/profile/password" method="POST" hx-post="/profile/password" hx-swap="outerHTML">
|
||||
{% if password_message %}
|
||||
<div class="alert {% if password_success %}alert-success{% else %}alert-danger{% endif %}" role="alert">
|
||||
{{ password_message }}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<label for="current_password">
|
||||
Current Password
|
||||
<input type="password" id="current_password" name="current_password" required>
|
||||
</label>
|
||||
|
||||
<label for="new_password">
|
||||
New Password
|
||||
<input
|
||||
type="password"
|
||||
id="new_password"
|
||||
name="new_password"
|
||||
required
|
||||
minlength="8"
|
||||
pattern="^(?=.*[a-z])(?=.*[A-Z])(?=.*\d).+$"
|
||||
title="Password must contain at least one lowercase letter, one uppercase letter, and one number"
|
||||
>
|
||||
<small>Minimum 8 characters with uppercase, lowercase, and number</small>
|
||||
</label>
|
||||
|
||||
<label for="new_password_confirm">
|
||||
Confirm New Password
|
||||
<input type="password" id="new_password_confirm" name="new_password_confirm" required>
|
||||
</label>
|
||||
|
||||
<button type="submit">Update Password</button>
|
||||
</form>
|
||||
</article>
|
||||
|
||||
<!-- Danger Zone -->
|
||||
<article style="border-color: var(--danger-color);">
|
||||
<header>
|
||||
<h3 style="color: var(--danger-color);">Danger Zone</h3>
|
||||
</header>
|
||||
<p>Once you delete your account, there is no going back. Please be certain.</p>
|
||||
<button
|
||||
class="secondary"
|
||||
style="background-color: var(--danger-color); border-color: var(--danger-color);"
|
||||
hx-delete="/profile"
|
||||
hx-confirm="Are you absolutely sure you want to delete your account? All your data will be permanently removed."
|
||||
hx-redirect="/"
|
||||
>
|
||||
Delete Account
|
||||
</button>
|
||||
</article>
|
||||
|
||||
<script>
|
||||
// Client-side password match validation
|
||||
document.getElementById('new_password_confirm').addEventListener('input', function() {
|
||||
const password = document.getElementById('new_password').value;
|
||||
const confirm = this.value;
|
||||
|
||||
if (password !== confirm) {
|
||||
this.setCustomValidity('Passwords do not match');
|
||||
} else {
|
||||
this.setCustomValidity('');
|
||||
}
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
135
templates/stats/index.html
Normal file
135
templates/stats/index.html
Normal file
@@ -0,0 +1,135 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Statistics - OpenRouter Monitor{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<h1>Detailed Statistics</h1>
|
||||
|
||||
<!-- Filters -->
|
||||
<article>
|
||||
<header>
|
||||
<h3>Filters</h3>
|
||||
</header>
|
||||
<form action="/stats" method="GET" hx-get="/stats" hx-target="#stats-results" hx-push-url="true">
|
||||
<div class="grid">
|
||||
<label for="start_date">
|
||||
Start Date
|
||||
<input type="date" id="start_date" name="start_date" value="{{ filters.start_date }}">
|
||||
</label>
|
||||
|
||||
<label for="end_date">
|
||||
End Date
|
||||
<input type="date" id="end_date" name="end_date" value="{{ filters.end_date }}">
|
||||
</label>
|
||||
|
||||
<label for="api_key_id">
|
||||
API Key
|
||||
<select id="api_key_id" name="api_key_id">
|
||||
<option value="">All Keys</option>
|
||||
{% for key in api_keys %}
|
||||
<option value="{{ key.id }}" {% if filters.api_key_id == key.id %}selected{% endif %}>
|
||||
{{ key.name }}
|
||||
</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</label>
|
||||
|
||||
<label for="model">
|
||||
Model
|
||||
<input type="text" id="model" name="model" placeholder="gpt-4" value="{{ filters.model }}">
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<button type="submit">Apply Filters</button>
|
||||
<a href="/stats/export?{{ query_string }}" role="button" class="secondary">Export CSV</a>
|
||||
</form>
|
||||
</article>
|
||||
|
||||
<!-- Results -->
|
||||
<article id="stats-results">
|
||||
<header>
|
||||
<h3>Usage Details</h3>
|
||||
<p><small>Showing {{ stats|length }} results</small></p>
|
||||
</header>
|
||||
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Date</th>
|
||||
<th>API Key</th>
|
||||
<th>Model</th>
|
||||
<th>Requests</th>
|
||||
<th>Prompt Tokens</th>
|
||||
<th>Completion Tokens</th>
|
||||
<th>Total Tokens</th>
|
||||
<th>Cost</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for stat in stats %}
|
||||
<tr>
|
||||
<td>{{ stat.date }}</td>
|
||||
<td>{{ stat.api_key_name }}</td>
|
||||
<td>{{ stat.model }}</td>
|
||||
<td>{{ stat.requests }}</td>
|
||||
<td>{{ stat.prompt_tokens }}</td>
|
||||
<td>{{ stat.completion_tokens }}</td>
|
||||
<td>{{ stat.total_tokens }}</td>
|
||||
<td>${{ stat.cost | round(4) }}</td>
|
||||
</tr>
|
||||
{% else %}
|
||||
<tr>
|
||||
<td colspan="8" style="text-align: center;">No data found for the selected filters.</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<!-- Pagination -->
|
||||
{% if total_pages > 1 %}
|
||||
<nav>
|
||||
<ul>
|
||||
{% if page > 1 %}
|
||||
<li>
|
||||
<a href="?page={{ page - 1 }}&{{ query_string }}" class="secondary">« Previous</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
|
||||
{% for p in range(1, total_pages + 1) %}
|
||||
<li>
|
||||
{% if p == page %}
|
||||
<strong>{{ p }}</strong>
|
||||
{% else %}
|
||||
<a href="?page={{ p }}&{{ query_string }}">{{ p }}</a>
|
||||
{% endif %}
|
||||
</li>
|
||||
{% endfor %}
|
||||
|
||||
{% if page < total_pages %}
|
||||
<li>
|
||||
<a href="?page={{ page + 1 }}&{{ query_string }}" class="secondary">Next »</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
</nav>
|
||||
{% endif %}
|
||||
</article>
|
||||
|
||||
<!-- Summary -->
|
||||
<article>
|
||||
<header>
|
||||
<h3>Summary</h3>
|
||||
</header>
|
||||
<div class="grid">
|
||||
<div>
|
||||
<strong>Total Requests:</strong> {{ summary.total_requests }}
|
||||
</div>
|
||||
<div>
|
||||
<strong>Total Tokens:</strong> {{ summary.total_tokens }}
|
||||
</div>
|
||||
<div>
|
||||
<strong>Total Cost:</strong> ${{ summary.total_cost | round(2) }}
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
{% endblock %}
|
||||
114
templates/tokens/index.html
Normal file
114
templates/tokens/index.html
Normal file
@@ -0,0 +1,114 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}API Tokens - OpenRouter Monitor{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<h1>API Tokens Management</h1>
|
||||
|
||||
<!-- Add New Token Form -->
|
||||
<article>
|
||||
<header>
|
||||
<h3>Generate New API Token</h3>
|
||||
</header>
|
||||
<form action="/tokens" method="POST" hx-post="/tokens" hx-swap="afterend" hx-target="this">
|
||||
<label for="token_name">
|
||||
Token Name
|
||||
<input type="text" id="token_name" name="name" placeholder="Mobile App Token" required>
|
||||
</label>
|
||||
|
||||
<button type="submit">Generate Token</button>
|
||||
</form>
|
||||
</article>
|
||||
|
||||
<!-- New Token Display (shown after creation) -->
|
||||
{% if new_token %}
|
||||
<article style="border: 2px solid var(--warning-color);">
|
||||
<header>
|
||||
<h3 style="color: var(--warning-color);">Save Your Token!</h3>
|
||||
</header>
|
||||
<div class="alert alert-danger" role="alert">
|
||||
<strong>Warning:</strong> This token will only be displayed once. Copy it now!
|
||||
</div>
|
||||
<label for="new_token_value">
|
||||
Your New API Token
|
||||
<input
|
||||
type="text"
|
||||
id="new_token_value"
|
||||
value="{{ new_token }}"
|
||||
readonly
|
||||
onclick="this.select()"
|
||||
style="font-family: monospace;"
|
||||
>
|
||||
</label>
|
||||
<button onclick="navigator.clipboard.writeText(document.getElementById('new_token_value').value)">
|
||||
Copy to Clipboard
|
||||
</button>
|
||||
</article>
|
||||
{% endif %}
|
||||
|
||||
<!-- Tokens List -->
|
||||
<article>
|
||||
<header>
|
||||
<h3>Your API Tokens</h3>
|
||||
</header>
|
||||
<table class="table" id="tokens-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Status</th>
|
||||
<th>Last Used</th>
|
||||
<th>Created</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for token in api_tokens %}
|
||||
<tr id="token-{{ token.id }}">
|
||||
<td>{{ token.name }}</td>
|
||||
<td>
|
||||
{% if token.is_active %}
|
||||
<span style="color: var(--success-color);">Active</span>
|
||||
{% else %}
|
||||
<span style="color: var(--danger-color);">Revoked</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>{{ token.last_used_at or 'Never' }}</td>
|
||||
<td>{{ token.created_at }}</td>
|
||||
<td>
|
||||
{% if token.is_active %}
|
||||
<button
|
||||
class="outline secondary"
|
||||
hx-delete="/tokens/{{ token.id }}"
|
||||
hx-confirm="Are you sure you want to revoke this token? This action cannot be undone."
|
||||
hx-target="#token-{{ token.id }}"
|
||||
hx-swap="outerHTML"
|
||||
>
|
||||
Revoke
|
||||
</button>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% else %}
|
||||
<tr>
|
||||
<td colspan="5" style="text-align: center;">No API tokens found. Generate your first token above.</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</article>
|
||||
|
||||
<!-- Usage Instructions -->
|
||||
<article>
|
||||
<header>
|
||||
<h3>Using API Tokens</h3>
|
||||
</header>
|
||||
<p>Include your API token in the <code>Authorization</code> header:</p>
|
||||
<pre><code>Authorization: Bearer YOUR_API_TOKEN</code></pre>
|
||||
<p>Available endpoints:</p>
|
||||
<ul>
|
||||
<li><code>GET /api/v1/stats</code> - Get usage statistics</li>
|
||||
<li><code>GET /api/v1/usage</code> - Get detailed usage data</li>
|
||||
<li><code>GET /api/v1/keys</code> - List your API keys (metadata only)</li>
|
||||
</ul>
|
||||
</article>
|
||||
{% endblock %}
|
||||
Reference in New Issue
Block a user