Add cluster tokens management page for worker processes

parent 1596a654
{% extends "base.html" %}
{% block title %}Cluster Tokens - VidAI{% endblock %}
{% block head %}
<style>
.container { max-width: 1400px; margin: 2rem auto; padding: 0 2rem; }
.admin-card { background: white; padding: 2rem; border-radius: 12px; box-shadow: 0 2px 10px rgba(0,0,0,0.05); margin-bottom: 2rem; }
.card-header { margin-bottom: 1.5rem; }
.card-header h3 { margin: 0; color: #1e293b; }
.btn { padding: 0.75rem 2rem; background: #667eea; color: white; border: none; border-radius: 8px; font-size: 1rem; font-weight: 600; cursor: pointer; text-decoration: none; display: inline-block; }
.btn:hover { background: #5a67d8; }
.btn-danger { background: #dc2626; }
.btn-danger:hover { background: #b91c1c; }
.btn-success { background: #059669; }
.btn-success:hover { background: #047857; }
.btn-icon { padding: 0.5rem; background: none; border: none; cursor: pointer; color: #64748b; }
.btn-icon:hover { color: #374151; }
.table { width: 100%; border-collapse: collapse; margin-top: 1rem; }
.table th, .table td { padding: 1rem; text-align: left; border-bottom: 1px solid #e5e7eb; }
.table th { background: #f8fafc; font-weight: 600; color: #374151; }
.actions-cell { display: flex; gap: 0.5rem; }
.status-active { color: #065f46; font-weight: 500; }
.status-inactive { color: #dc2626; font-weight: 500; }
.alert { padding: 0.75rem; border-radius: 8px; margin-bottom: 1rem; }
.alert-error { background: #fee2e2; color: #dc2626; border: 1px solid #fecaca; }
.alert-success { background: #d1fae5; color: #065f46; border: 1px solid #a7f3d0; }
.token-preview { font-family: monospace; background: #f1f5f9; padding: 0.25rem 0.5rem; border-radius: 4px; font-size: 0.875rem; }
</style>
{% endblock %}
{% block content %}
<div class="container">
<div class="admin-card">
<div class="card-header">
<h3><i class="fas fa-key"></i> Cluster Tokens</h3>
<p>Manage authentication tokens for worker processes in the cluster.</p>
</div>
{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}
{% for category, message in messages %}
<div class="alert alert-{{ 'error' if category == 'error' else 'success' }}">{{ message }}</div>
{% endfor %}
{% endif %}
{% endwith %}
<form method="post" action="/admin/cluster_tokens/generate" style="margin-bottom: 2rem;">
<button type="submit" class="btn"><i class="fas fa-plus"></i> Generate New Token</button>
</form>
<table class="table">
<thead>
<tr>
<th>ID</th>
<th>Token</th>
<th>Status</th>
<th>Created</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{% for token in worker_tokens %}
<tr>
<td>{{ token.get('id') }}</td>
<td><span class="token-preview">{{ token.get('token')[:20] }}...</span></td>
<td><span class="status-{{ 'active' if token.get('active') else 'inactive' }}">{{ 'Active' if token.get('active') else 'Inactive' }}</span></td>
<td>{{ token.get('created_at', 'N/A')[:19] if token.get('created_at') else 'N/A' }}</td>
<td class="actions-cell">
{% if token.get('active') %}
<form method="post" action="/admin/cluster_tokens/{{ token.get('id') }}/deactivate" style="display: inline;">
<button type="submit" class="btn-icon" title="Deactivate"><i class="fas fa-ban"></i></button>
</form>
{% else %}
<form method="post" action="/admin/cluster_tokens/{{ token.get('id') }}/activate" style="display: inline;">
<button type="submit" class="btn btn-success" title="Activate" style="padding: 0.25rem 0.5rem; font-size: 0.75rem;">Activate</button>
</form>
{% endif %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
{% if not worker_tokens %}
<p style="text-align: center; color: #64748b; margin-top: 2rem;">No cluster tokens found. Generate your first token above.</p>
{% endif %}
</div>
</div>
{% endblock %}
\ No newline at end of file
......@@ -78,7 +78,7 @@
{% endif %}
<a href="/history" {% if active_page == 'history' %}class="active"{% endif %}>History</a>
{% if user.get('role') == 'admin' %}
<a href="/api_tokens" {% if active_page == 'api_tokens' %}class="active"{% endif %}>API Tokens</a>
<a href="/admin/cluster_tokens" {% if active_page == 'cluster_tokens' %}class="active"{% endif %}>Cluster Tokens</a>
<a href="/settings" {% if active_page == 'settings' %}class="active"{% endif %}>Settings</a>
{% endif %}
</nav>
......
......@@ -1138,6 +1138,38 @@ def create_worker_token() -> str:
return token
def get_worker_tokens() -> List[Dict[str, Any]]:
"""Get all worker tokens."""
conn = get_db_connection()
cursor = conn.cursor()
cursor.execute('SELECT id, token, active, created_at FROM worker_tokens ORDER BY created_at DESC')
rows = cursor.fetchall()
conn.close()
return [dict(row) for row in rows]
def deactivate_worker_token(token_id: int) -> bool:
"""Deactivate a worker token."""
conn = get_db_connection()
cursor = conn.cursor()
cursor.execute('UPDATE worker_tokens SET active = 0 WHERE id = ?', (token_id,))
conn.commit()
success = cursor.rowcount > 0
conn.close()
return success
def activate_worker_token(token_id: int) -> bool:
"""Activate a worker token."""
conn = get_db_connection()
cursor = conn.cursor()
cursor.execute('UPDATE worker_tokens SET active = 1 WHERE id = ?', (token_id,))
conn.commit()
success = cursor.rowcount > 0
conn.close()
return success
def create_user_api_token(user_id: int, name: str) -> str:
"""Create a user API token for programmatic access."""
import jwt
......
......@@ -416,7 +416,47 @@ def generate_worker_token():
from .auth import generate_worker_token
token = generate_worker_token()
flash(f'New worker token generated: {token[:20]}...', 'success')
return redirect(url_for('settings'))
return redirect(url_for('cluster_tokens'))
@app.route('/admin/cluster_tokens')
@admin_required
def cluster_tokens():
"""Cluster tokens management page."""
from .database import get_worker_tokens
worker_tokens = get_worker_tokens()
user = get_current_user_session()
return render_template('admin/cluster_tokens.html', user=user, worker_tokens=worker_tokens, active_page='cluster_tokens')
@app.route('/admin/cluster_tokens/generate', methods=['POST'])
@admin_required
def generate_cluster_token():
"""Generate a new cluster token."""
from .auth import generate_worker_token
token = generate_worker_token()
flash(f'New cluster token generated: {token[:20]}...', 'success')
return redirect(url_for('cluster_tokens'))
@app.route('/admin/cluster_tokens/<int:token_id>/deactivate', methods=['POST'])
@admin_required
def deactivate_cluster_token(token_id):
"""Deactivate a cluster token."""
from .database import deactivate_worker_token
if deactivate_worker_token(token_id):
flash('Token deactivated successfully!', 'success')
else:
flash('Failed to deactivate token.', 'error')
return redirect(url_for('cluster_tokens'))
@app.route('/admin/cluster_tokens/<int:token_id>/activate', methods=['POST'])
@admin_required
def activate_cluster_token(token_id):
"""Activate a cluster token."""
from .database import activate_worker_token
if activate_worker_token(token_id):
flash('Token activated successfully!', 'success')
else:
flash('Failed to activate token.', 'error')
return redirect(url_for('cluster_tokens'))
@app.route('/api_tokens')
@login_required
......
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