Update cluster tokens to be managed like API tokens with names, last_used, and delete functionality

parent 5593a42e
......@@ -45,27 +45,40 @@
{% 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>
<div class="admin-card" style="margin-bottom: 2rem;">
<div class="card-header">
<h3><i class="fas fa-plus"></i> Create New Token</h3>
</div>
<form method="post" action="/admin/cluster_tokens/generate">
<div style="display: flex; gap: 1rem; align-items: end;">
<div style="flex: 1;">
<label for="token_name" style="display: block; margin-bottom: 0.5rem; color: #374151; font-weight: 500;">Token Name</label>
<input type="text" id="token_name" name="token_name" required style="width: 100%; padding: 0.5rem; border: 2px solid #e5e7eb; border-radius: 8px; font-size: 1rem;">
</div>
<button type="submit" class="btn"><i class="fas fa-plus"></i> Generate Token</button>
</div>
</form>
</div>
<table class="table">
<thead>
<tr>
<th>ID</th>
<th>Name</th>
<th>Token</th>
<th>Status</th>
<th>Created</th>
<th>Last Used</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{% for token in worker_tokens %}
<tr>
<td>{{ token.get('id') }}</td>
<td>{{ token.get('name') }}</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>{{ token.get('last_used', 'Never')[:19] if token.get('last_used') else 'Never' }}</td>
<td class="actions-cell">
{% if token.get('active') %}
<form method="post" action="/admin/cluster_tokens/{{ token.get('id') }}/deactivate" style="display: inline;">
......@@ -76,6 +89,7 @@
<button type="submit" class="btn btn-success" title="Activate" style="padding: 0.25rem 0.5rem; font-size: 0.75rem;">Activate</button>
</form>
{% endif %}
<button onclick='deleteToken({{ token.get("id") }}, "{{ token.get("name") }}")' class="btn-icon" title="Delete" style="color: #dc2626;"><i class="fas fa-trash"></i></button>
</td>
</tr>
{% endfor %}
......@@ -86,5 +100,25 @@
<p style="text-align: center; color: #64748b; margin-top: 2rem;">No cluster tokens found. Generate your first token above.</p>
{% endif %}
</div>
<script>
function deleteToken(id, name) {
if (confirm(`Are you sure you want to delete the token "${name}"? This action cannot be undone.`)) {
// Create a form and submit it
const form = document.createElement('form');
form.method = 'POST';
form.action = `/admin/cluster_tokens/${id}/delete`;
const input = document.createElement('input');
input.type = 'hidden';
input.name = 'confirm_delete';
input.value = 'yes';
form.appendChild(input);
document.body.appendChild(form);
form.submit();
}
}
</script>
</div>
{% endblock %}
\ No newline at end of file
......@@ -173,10 +173,10 @@ def generate_api_token(user_id: int) -> str:
return create_api_token(user_id)
def generate_worker_token() -> str:
def generate_worker_token(name: str) -> str:
"""Generate worker authentication token (admin only)."""
from .database import create_worker_token
return create_worker_token()
return create_worker_token(name)
def generate_user_api_token(user_id: int) -> str:
......
......@@ -330,20 +330,33 @@ def init_db(conn) -> None:
cursor.execute('''
CREATE TABLE IF NOT EXISTS worker_tokens (
id INT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(255) NOT NULL,
token VARCHAR(255) UNIQUE NOT NULL,
active BOOLEAN DEFAULT 1,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
last_used TIMESTAMP NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
''')
else:
cursor.execute('''
CREATE TABLE IF NOT EXISTS worker_tokens (
id INTEGER PRIMARY KEY,
name TEXT NOT NULL,
token TEXT UNIQUE NOT NULL,
active BOOLEAN DEFAULT 1,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
last_used TIMESTAMP
)
''')
# Add missing columns if they don't exist
try:
cursor.execute('ALTER TABLE worker_tokens ADD COLUMN name TEXT')
except sqlite3.OperationalError:
pass
try:
cursor.execute('ALTER TABLE worker_tokens ADD COLUMN last_used TIMESTAMP')
except sqlite3.OperationalError:
pass
# Sessions table for persistent sessions
if config['type'] == 'mysql':
......@@ -1120,7 +1133,7 @@ def delete_user(user_id: int) -> bool:
return success
def create_worker_token() -> str:
def create_worker_token(name: str) -> str:
"""Create a worker authentication token."""
import secrets
token = secrets.token_hex(32)
......@@ -1129,9 +1142,9 @@ def create_worker_token() -> str:
cursor = conn.cursor()
cursor.execute('''
INSERT INTO worker_tokens (token, created_at, active)
VALUES (?, datetime('now'), 1)
''', (token,))
INSERT INTO worker_tokens (name, token, created_at, active)
VALUES (?, ?, datetime('now'), 1)
''', (name, token))
conn.commit()
conn.close()
......@@ -1142,7 +1155,7 @@ 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')
cursor.execute('SELECT id, name, token, active, created_at, last_used FROM worker_tokens ORDER BY created_at DESC')
rows = cursor.fetchall()
conn.close()
return [dict(row) for row in rows]
......@@ -1170,6 +1183,17 @@ def activate_worker_token(token_id: int) -> bool:
return success
def delete_worker_token(token_id: int) -> bool:
"""Delete a worker token."""
conn = get_db_connection()
cursor = conn.cursor()
cursor.execute('DELETE FROM worker_tokens 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
......
......@@ -409,15 +409,6 @@ def update_settings():
flash('Settings updated successfully!', 'success')
return redirect(url_for('settings'))
@app.route('/generate_worker_token')
@admin_required
def generate_worker_token():
"""Generate a new worker authentication token (admin only)."""
from .auth import generate_worker_token
token = generate_worker_token()
flash(f'New worker token generated: {token[:20]}...', 'success')
return redirect(url_for('cluster_tokens'))
@app.route('/admin/cluster_tokens')
@admin_required
def cluster_tokens():
......@@ -431,9 +422,21 @@ def cluster_tokens():
@admin_required
def generate_cluster_token():
"""Generate a new cluster token."""
token_name = request.form.get('token_name', '').strip()
if not token_name:
flash('Token name is required', 'error')
return redirect(url_for('cluster_tokens'))
# Check if name already exists
from .database import get_worker_tokens
existing = get_worker_tokens()
if any(t.get('name') == token_name for t in existing):
flash('A token with this name already exists. Please choose a different name.', 'error')
return redirect(url_for('cluster_tokens'))
from .auth import generate_worker_token
token = generate_worker_token()
flash(f'New cluster token generated: {token[:20]}...', 'success')
token = generate_worker_token(token_name)
flash(f'New cluster token "{token_name}" generated: {token[:20]}...', 'success')
return redirect(url_for('cluster_tokens'))
@app.route('/admin/cluster_tokens/<int:token_id>/deactivate', methods=['POST'])
......@@ -458,6 +461,17 @@ def activate_cluster_token(token_id):
flash('Failed to activate token.', 'error')
return redirect(url_for('cluster_tokens'))
@app.route('/admin/cluster_tokens/<int:token_id>/delete', methods=['POST'])
@admin_required
def delete_cluster_token(token_id):
"""Delete a cluster token."""
from .database import delete_worker_token
if delete_worker_token(token_id):
flash('Token deleted successfully!', 'success')
else:
flash('Failed to delete token.', 'error')
return redirect(url_for('cluster_tokens'))
@app.route('/api_tokens')
@login_required
def api_tokens():
......
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