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

parent 5593a42e
...@@ -45,27 +45,40 @@ ...@@ -45,27 +45,40 @@
{% endif %} {% endif %}
{% endwith %} {% endwith %}
<form method="post" action="/admin/cluster_tokens/generate" style="margin-bottom: 2rem;"> <div class="admin-card" style="margin-bottom: 2rem;">
<button type="submit" class="btn"><i class="fas fa-plus"></i> Generate New Token</button> <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> </form>
</div>
<table class="table"> <table class="table">
<thead> <thead>
<tr> <tr>
<th>ID</th> <th>Name</th>
<th>Token</th> <th>Token</th>
<th>Status</th> <th>Status</th>
<th>Created</th> <th>Created</th>
<th>Last Used</th>
<th>Actions</th> <th>Actions</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{% for token in worker_tokens %} {% for token in worker_tokens %}
<tr> <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="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><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('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"> <td class="actions-cell">
{% if token.get('active') %} {% if token.get('active') %}
<form method="post" action="/admin/cluster_tokens/{{ token.get('id') }}/deactivate" style="display: inline;"> <form method="post" action="/admin/cluster_tokens/{{ token.get('id') }}/deactivate" style="display: inline;">
...@@ -76,6 +89,7 @@ ...@@ -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> <button type="submit" class="btn btn-success" title="Activate" style="padding: 0.25rem 0.5rem; font-size: 0.75rem;">Activate</button>
</form> </form>
{% endif %} {% 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> </td>
</tr> </tr>
{% endfor %} {% endfor %}
...@@ -86,5 +100,25 @@ ...@@ -86,5 +100,25 @@
<p style="text-align: center; color: #64748b; margin-top: 2rem;">No cluster tokens found. Generate your first token above.</p> <p style="text-align: center; color: #64748b; margin-top: 2rem;">No cluster tokens found. Generate your first token above.</p>
{% endif %} {% endif %}
</div> </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> </div>
{% endblock %} {% endblock %}
\ No newline at end of file
...@@ -173,10 +173,10 @@ def generate_api_token(user_id: int) -> str: ...@@ -173,10 +173,10 @@ def generate_api_token(user_id: int) -> str:
return create_api_token(user_id) return create_api_token(user_id)
def generate_worker_token() -> str: def generate_worker_token(name: str) -> str:
"""Generate worker authentication token (admin only).""" """Generate worker authentication token (admin only)."""
from .database import create_worker_token from .database import create_worker_token
return create_worker_token() return create_worker_token(name)
def generate_user_api_token(user_id: int) -> str: def generate_user_api_token(user_id: int) -> str:
......
...@@ -330,20 +330,33 @@ def init_db(conn) -> None: ...@@ -330,20 +330,33 @@ def init_db(conn) -> None:
cursor.execute(''' cursor.execute('''
CREATE TABLE IF NOT EXISTS worker_tokens ( CREATE TABLE IF NOT EXISTS worker_tokens (
id INT AUTO_INCREMENT PRIMARY KEY, id INT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(255) NOT NULL,
token VARCHAR(255) UNIQUE NOT NULL, token VARCHAR(255) UNIQUE NOT NULL,
active BOOLEAN DEFAULT 1, 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 ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
''') ''')
else: else:
cursor.execute(''' cursor.execute('''
CREATE TABLE IF NOT EXISTS worker_tokens ( CREATE TABLE IF NOT EXISTS worker_tokens (
id INTEGER PRIMARY KEY, id INTEGER PRIMARY KEY,
name TEXT NOT NULL,
token TEXT UNIQUE NOT NULL, token TEXT UNIQUE NOT NULL,
active BOOLEAN DEFAULT 1, 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 # Sessions table for persistent sessions
if config['type'] == 'mysql': if config['type'] == 'mysql':
...@@ -1120,7 +1133,7 @@ def delete_user(user_id: int) -> bool: ...@@ -1120,7 +1133,7 @@ def delete_user(user_id: int) -> bool:
return success return success
def create_worker_token() -> str: def create_worker_token(name: str) -> str:
"""Create a worker authentication token.""" """Create a worker authentication token."""
import secrets import secrets
token = secrets.token_hex(32) token = secrets.token_hex(32)
...@@ -1129,9 +1142,9 @@ def create_worker_token() -> str: ...@@ -1129,9 +1142,9 @@ def create_worker_token() -> str:
cursor = conn.cursor() cursor = conn.cursor()
cursor.execute(''' cursor.execute('''
INSERT INTO worker_tokens (token, created_at, active) INSERT INTO worker_tokens (name, token, created_at, active)
VALUES (?, datetime('now'), 1) VALUES (?, ?, datetime('now'), 1)
''', (token,)) ''', (name, token))
conn.commit() conn.commit()
conn.close() conn.close()
...@@ -1142,7 +1155,7 @@ def get_worker_tokens() -> List[Dict[str, Any]]: ...@@ -1142,7 +1155,7 @@ def get_worker_tokens() -> List[Dict[str, Any]]:
"""Get all worker tokens.""" """Get all worker tokens."""
conn = get_db_connection() conn = get_db_connection()
cursor = conn.cursor() 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() rows = cursor.fetchall()
conn.close() conn.close()
return [dict(row) for row in rows] return [dict(row) for row in rows]
...@@ -1170,6 +1183,17 @@ def activate_worker_token(token_id: int) -> bool: ...@@ -1170,6 +1183,17 @@ def activate_worker_token(token_id: int) -> bool:
return success 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: def create_user_api_token(user_id: int, name: str) -> str:
"""Create a user API token for programmatic access.""" """Create a user API token for programmatic access."""
import jwt import jwt
......
...@@ -409,15 +409,6 @@ def update_settings(): ...@@ -409,15 +409,6 @@ def update_settings():
flash('Settings updated successfully!', 'success') flash('Settings updated successfully!', 'success')
return redirect(url_for('settings')) 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') @app.route('/admin/cluster_tokens')
@admin_required @admin_required
def cluster_tokens(): def cluster_tokens():
...@@ -431,9 +422,21 @@ def cluster_tokens(): ...@@ -431,9 +422,21 @@ def cluster_tokens():
@admin_required @admin_required
def generate_cluster_token(): def generate_cluster_token():
"""Generate a new 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 from .auth import generate_worker_token
token = generate_worker_token() token = generate_worker_token(token_name)
flash(f'New cluster token generated: {token[:20]}...', 'success') flash(f'New cluster token "{token_name}" generated: {token[:20]}...', 'success')
return redirect(url_for('cluster_tokens')) return redirect(url_for('cluster_tokens'))
@app.route('/admin/cluster_tokens/<int:token_id>/deactivate', methods=['POST']) @app.route('/admin/cluster_tokens/<int:token_id>/deactivate', methods=['POST'])
...@@ -458,6 +461,17 @@ def activate_cluster_token(token_id): ...@@ -458,6 +461,17 @@ def activate_cluster_token(token_id):
flash('Failed to activate token.', 'error') flash('Failed to activate token.', 'error')
return redirect(url_for('cluster_tokens')) 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') @app.route('/api_tokens')
@login_required @login_required
def api_tokens(): 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