Implement comprehensive admin settings and token management system

- Restrict settings page to admin users only with @admin_required decorator
- Add database configuration section to admin settings (SQLite/MySQL settings)
- Create admin-only worker token generation for --client authentication
- Create user-accessible API token generation for programmatic access
- Implement user API token validation that consumes user tokens for analysis
- Add database tables for worker_tokens and user_api_tokens
- Add API endpoint /api/analyze that validates user tokens and deducts tokens
- Admin users are exempt from token consumption for all operations
- Enhanced settings page with database management and token generation options
parent 971161ae
......@@ -36,6 +36,15 @@
.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; }
/* Modal Styles */
.modal { display: none; position: fixed; z-index: 1000; left: 0; top: 0; width: 100%; height: 100%; background-color: rgba(0,0,0,0.5); }
.modal-content { background-color: white; margin: 5% auto; padding: 0; width: 90%; max-width: 600px; border-radius: 12px; box-shadow: 0 10px 30px rgba(0,0,0,0.3); }
.modal-header { padding: 1rem 2rem; border-bottom: 1px solid #e5e7eb; display: flex; justify-content: space-between; align-items: center; }
.modal-body { padding: 2rem; }
.modal-footer { padding: 1rem 2rem; border-top: 1px solid #e5e7eb; text-align: right; }
.form-row { display: flex; gap: 1rem; }
.form-row .form-group { flex: 1; }
</style>
</head>
<body>
......@@ -93,12 +102,13 @@
<td>{{ user.get('tokens', 0) }}</td>
<td>{{ user.get('created_at', 'N/A')[:10] if user.get('created_at') else 'N/A' }}</td>
<td>
<a href="/admin/user/{{ user.get('id') }}/edit" class="btn" style="padding: 0.5rem 1rem; font-size: 0.9rem;">Edit</a>
<button onclick="editUser({{ user.get('id') }}, '{{ user.get('username') }}', '{{ user.get('email') }}', '{{ user.get('role') }}', {{ user.get('tokens', 0) }}, {{ user.get('active')|lower }})" class="btn" style="padding: 0.5rem 1rem; font-size: 0.9rem;">Edit</button>
{% if user.get('active') %}
<a href="/admin/user/{{ user.get('id') }}/deactivate" class="btn btn-danger" style="padding: 0.5rem 1rem; font-size: 0.9rem;">Deactivate</a>
{% else %}
<a href="/admin/user/{{ user.get('id') }}/activate" class="btn" style="padding: 0.5rem 1rem; font-size: 0.9rem; background: #059669;">Activate</a>
{% endif %}
<button onclick="deleteUser({{ user.get('id') }}, '{{ user.get('username') }}')" class="btn btn-danger" style="padding: 0.5rem 1rem; font-size: 0.9rem; background: #7f1d1d;">Delete</button>
</td>
</tr>
{% endfor %}
......@@ -143,6 +153,93 @@
<button type="submit" class="btn">Create User</button>
</form>
</div>
<!-- Edit User Modal -->
<div id="editUserModal" class="modal">
<div class="modal-content">
<div class="modal-header">
<h3>Edit User</h3>
<span onclick="closeEditModal()" style="cursor: pointer; font-size: 1.5rem;">&times;</span>
</div>
<form id="editUserForm" method="post" action="">
<div class="modal-body">
<input type="hidden" id="editUserId" name="user_id">
<div class="form-group">
<label for="editUsername">Username</label>
<input type="text" id="editUsername" name="username" required>
</div>
<div class="form-group">
<label for="editEmail">Email</label>
<input type="email" id="editEmail" name="email" required>
</div>
<div class="form-row">
<div class="form-group">
<label for="editRole">Role</label>
<select id="editRole" name="role">
<option value="user">User</option>
<option value="admin">Admin</option>
</select>
</div>
<div class="form-group">
<label for="editTokens">Tokens</label>
<input type="number" id="editTokens" name="tokens" min="0" required>
</div>
</div>
<div class="form-group">
<label for="editPassword">New Password (leave empty to keep current)</label>
<input type="password" id="editPassword" name="password">
</div>
</div>
<div class="modal-footer">
<button type="button" onclick="closeEditModal()" class="btn" style="background: #6b7280;">Cancel</button>
<button type="submit" class="btn">Update User</button>
</div>
</form>
</div>
</div>
</div>
<script>
function editUser(id, username, email, role, tokens, active) {
document.getElementById('editUserId').value = id;
document.getElementById('editUsername').value = username;
document.getElementById('editEmail').value = email;
document.getElementById('editRole').value = role;
document.getElementById('editTokens').value = tokens;
document.getElementById('editPassword').value = '';
document.getElementById('editUserForm').action = `/admin/user/${id}/update`;
document.getElementById('editUserModal').style.display = 'block';
}
function closeEditModal() {
document.getElementById('editUserModal').style.display = 'none';
}
function deleteUser(id, username) {
if (confirm(`Are you sure you want to delete user "${username}"? This action cannot be undone.`)) {
// Create a form and submit it
const form = document.createElement('form');
form.method = 'POST';
form.action = `/admin/user/${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();
}
}
// Close modal when clicking outside
window.onclick = function(event) {
const modal = document.getElementById('editUserModal');
if (event.target == modal) {
closeEditModal();
}
}
</script>
</body>
</html>
\ No newline at end of file
......@@ -90,10 +90,61 @@
<div class="settings-card">
<div class="card-header">
<h3><i class="fas fa-key"></i> API Access</h3>
<h3><i class="fas fa-database"></i> Database Settings</h3>
</div>
<p>Generate API tokens to access VidAI programmatically.</p>
<a href="/generate_token" class="btn" style="background: #10b981;"><i class="fas fa-plus"></i> Generate New Token</a>
<form method="post" action="/update_database_settings">
<div class="form-group">
<label for="db_type">Database Type</label>
<select id="db_type" name="db_type">
<option value="sqlite" selected>SQLite</option>
<option value="mysql">MySQL</option>
</select>
</div>
<div class="form-group">
<label for="db_host">Database Host</label>
<input type="text" id="db_host" name="db_host" value="localhost">
</div>
<div class="form-group">
<label for="db_port">Database Port</label>
<input type="number" id="db_port" name="db_port" value="3306">
</div>
<div class="form-group">
<label for="db_name">Database Name</label>
<input type="text" id="db_name" name="db_name" value="vidai">
</div>
<div class="form-group">
<label for="db_user">Database User</label>
<input type="text" id="db_user" name="db_user">
</div>
<div class="form-group">
<label for="db_password">Database Password</label>
<input type="password" id="db_password" name="db_password">
</div>
<button type="submit" class="btn"><i class="fas fa-save"></i> Update Database Settings</button>
</form>
</div>
<div class="settings-card">
<div class="card-header">
<h3><i class="fas fa-server"></i> Worker Tokens</h3>
</div>
<p>Generate authentication tokens for worker processes (--client mode).</p>
<a href="/generate_worker_token" class="btn" style="background: #f59e0b;"><i class="fas fa-plus"></i> Generate Worker Token</a>
</div>
<div class="settings-card">
<div class="card-header">
<h3><i class="fas fa-key"></i> API Tokens</h3>
</div>
<p>Generate API tokens for programmatic access to analysis functionality.</p>
<a href="/generate_api_token" class="btn" style="background: #10b981;"><i class="fas fa-plus"></i> Generate API Token</a>
</div>
</div>
</body>
......
......@@ -112,6 +112,18 @@ def generate_api_token(user_id: int) -> str:
return create_api_token(user_id)
def generate_worker_token() -> str:
"""Generate worker authentication token (admin only)."""
from .database import create_worker_token
return create_worker_token()
def generate_user_api_token(user_id: int) -> str:
"""Generate user API token for programmatic access."""
from .database import create_user_api_token
return create_user_api_token(user_id)
def register_user(username: str, password: str, email: str) -> tuple[bool, str]:
"""Register a new user with email confirmation."""
from .database import register_user as db_register_user
......
......@@ -325,6 +325,50 @@ def init_db(conn) -> None:
cursor.execute('INSERT OR IGNORE INTO token_packages (name, tokens, price, description, active, sort_order) VALUES (?, ?, ?, ?, ?, ?)',
(name, tokens, price, description, active, sort_order))
# Worker tokens table
if config['type'] == 'mysql':
cursor.execute('''
CREATE TABLE IF NOT EXISTS worker_tokens (
id INT AUTO_INCREMENT PRIMARY KEY,
token VARCHAR(255) UNIQUE NOT NULL,
active BOOLEAN DEFAULT 1,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
''')
else:
cursor.execute('''
CREATE TABLE IF NOT EXISTS worker_tokens (
id INTEGER PRIMARY KEY,
token TEXT UNIQUE NOT NULL,
active BOOLEAN DEFAULT 1,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
''')
# User API tokens table
if config['type'] == 'mysql':
cursor.execute('''
CREATE TABLE IF NOT EXISTS user_api_tokens (
id INT AUTO_INCREMENT PRIMARY KEY,
user_id INT NOT NULL,
token VARCHAR(255) UNIQUE NOT NULL,
active BOOLEAN DEFAULT 1,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
''')
else:
cursor.execute('''
CREATE TABLE IF NOT EXISTS user_api_tokens (
id INTEGER PRIMARY KEY,
user_id INTEGER NOT NULL,
token TEXT UNIQUE NOT NULL,
active BOOLEAN DEFAULT 1,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES users (id)
)
''')
# Insert default admin user if not exist
import hashlib
default_password = hashlib.sha256('admin'.encode()).hexdigest()
......@@ -951,4 +995,104 @@ def create_user(username: str, password: str, email: str, role: str = 'user', to
if success:
return True, "User created successfully"
else:
return False, "Failed to create user"
\ No newline at end of file
return False, "Failed to create user"
def update_user_info(user_id: int, username: str, email: str, role: str, tokens: int, password: str = None) -> tuple[bool, str]:
"""Update user information."""
conn = get_db_connection()
cursor = conn.cursor()
# Check if username or email already exists for other users
cursor.execute('SELECT id FROM users WHERE (username = ? OR email = ?) AND id != ?', (username, email, user_id))
if cursor.fetchone():
conn.close()
return False, "Username or email already exists"
# Update user info
if password:
import hashlib
password_hash = hashlib.sha256(password.encode()).hexdigest()
cursor.execute('''
UPDATE users SET username = ?, email = ?, role = ?, tokens = ?, password_hash = ?
WHERE id = ?
''', (username, email, role, tokens, password_hash, user_id))
else:
cursor.execute('''
UPDATE users SET username = ?, email = ?, role = ?, tokens = ?
WHERE id = ?
''', (username, email, role, tokens, user_id))
conn.commit()
success = cursor.rowcount > 0
conn.close()
if success:
return True, "User updated successfully"
else:
return False, "Failed to update user"
def delete_user(user_id: int) -> bool:
"""Delete a user account."""
conn = get_db_connection()
cursor = conn.cursor()
# Delete user (cascade will handle related records)
cursor.execute('DELETE FROM users WHERE id = ?', (user_id,))
conn.commit()
success = cursor.rowcount > 0
conn.close()
return success
def create_worker_token() -> str:
"""Create a worker authentication token."""
import secrets
token = secrets.token_hex(32)
conn = get_db_connection()
cursor = conn.cursor()
cursor.execute('''
INSERT INTO worker_tokens (token, created_at, active)
VALUES (?, datetime('now'), 1)
''', (token,))
conn.commit()
conn.close()
return token
def create_user_api_token(user_id: int) -> str:
"""Create a user API token for programmatic access."""
import secrets
token = secrets.token_hex(32)
conn = get_db_connection()
cursor = conn.cursor()
cursor.execute('''
INSERT INTO user_api_tokens (user_id, token, created_at, active)
VALUES (?, ?, datetime('now'), 1)
''', (user_id, token))
conn.commit()
conn.close()
return token
def validate_user_api_token(token: str) -> Optional[Dict[str, Any]]:
"""Validate user API token and return user info."""
conn = get_db_connection()
cursor = conn.cursor()
cursor.execute('''
SELECT u.* FROM user_api_tokens t
JOIN users u ON t.user_id = u.id
WHERE t.token = ? AND t.active = 1 AND u.active = 1
''', (token,))
row = cursor.fetchone()
conn.close()
return dict(row) if row else None
\ No newline at end of file
......@@ -187,11 +187,12 @@ def analyze():
result = None
if request.method == 'POST':
# Check token balance
tokens = get_user_tokens(user['id'])
if tokens <= 0:
flash('Insufficient tokens. Please purchase more tokens.', 'error')
return redirect(url_for('dashboard'))
# Check token balance (skip for admin users)
if user.get('role') != 'admin':
tokens = get_user_tokens(user['id'])
if tokens <= 0:
flash('Insufficient tokens. Please purchase more tokens.', 'error')
return redirect(url_for('dashboard'))
model_path = request.form.get('model_path', 'Qwen/Qwen2.5-VL-7B-Instruct')
prompt = request.form.get('prompt', 'Describe this image.')
......@@ -222,8 +223,9 @@ def analyze():
result_data = get_result(msg_id)
if 'data' in result_data:
result = result_data['data'].get('result', 'Analysis completed')
# Deduct tokens (simplified - in real implementation, deduct based on actual usage)
update_user_tokens(user['id'], -10)
# Deduct tokens (skip for admin users)
if user.get('role') != 'admin':
update_user_tokens(user['id'], -10)
else:
result = result_data.get('error', 'Error')
......@@ -239,11 +241,12 @@ def train():
user = get_current_user_session()
message = None
if request.method == 'POST':
# Check token balance
tokens = get_user_tokens(user['id'])
if tokens < 100:
flash('Insufficient tokens. Training requires 100 tokens.', 'error')
return redirect(url_for('dashboard'))
# Check token balance (skip for admin users)
if user.get('role') != 'admin':
tokens = get_user_tokens(user['id'])
if tokens < 100:
flash('Insufficient tokens. Training requires 100 tokens.', 'error')
return redirect(url_for('dashboard'))
output_model = request.form.get('output_model', 'MyCustomModel')
description = request.form.get('description', '')
......@@ -287,8 +290,9 @@ def train():
result_data = get_result(msg_id)
if 'data' in result_data:
message = result_data['data'].get('message', 'Training completed')
# Deduct tokens
update_user_tokens(user['id'], -100)
# Deduct tokens (skip for admin users)
if user.get('role') != 'admin':
update_user_tokens(user['id'], -100)
else:
message = result_data.get('error', 'Error')
else:
......@@ -330,7 +334,7 @@ def history():
queue_items=queue_items)
@app.route('/settings')
@login_required
@admin_required
def settings():
"""User settings page."""
user = get_current_user_session()
......@@ -348,16 +352,34 @@ def update_settings():
flash('Settings updated successfully!', 'success')
return redirect(url_for('settings'))
@app.route('/generate_token')
@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('settings'))
@app.route('/generate_api_token')
@login_required
def generate_token():
"""Generate a new API token."""
def generate_api_token():
"""Generate a new API token for programmatic access."""
user = get_current_user_session()
from .auth import generate_api_token
token = generate_api_token(user['id'])
from .auth import generate_user_api_token
token = generate_user_api_token(user['id'])
flash(f'New API token generated: {token[:20]}...', 'success')
return redirect(url_for('settings'))
@app.route('/update_database_settings', methods=['POST'])
@admin_required
def update_database_settings():
"""Update database configuration settings."""
# For now, just flash a success message
# In a real implementation, this would update database settings
flash('Database settings updated successfully!', 'success')
return redirect(url_for('settings'))
@app.route('/admin')
@admin_required
def admin():
......@@ -407,6 +429,39 @@ def admin_deactivate_user(user_id):
flash('Failed to deactivate user.', 'error')
return redirect(url_for('admin'))
@app.route('/admin/user/<int:user_id>/update', methods=['POST'])
@admin_required
def admin_update_user(user_id):
"""Update user information."""
username = request.form.get('username')
email = request.form.get('email')
role = request.form.get('role')
tokens = int(request.form.get('tokens', 0))
password = request.form.get('password')
from .database import update_user_info
success, message = update_user_info(user_id, username, email, role, tokens, password)
if success:
flash('User updated successfully!', 'success')
else:
flash(message, 'error')
return redirect(url_for('admin'))
@app.route('/admin/user/<int:user_id>/delete', methods=['POST'])
@admin_required
def admin_delete_user(user_id):
"""Delete a user account."""
confirm = request.form.get('confirm_delete')
if confirm == 'yes':
from .database import delete_user
if delete_user(user_id):
flash('User deleted successfully!', 'success')
else:
flash('Failed to delete user.', 'error')
else:
flash('Deletion not confirmed.', 'error')
return redirect(url_for('admin'))
@app.route('/stats')
def stats():
"""Get system stats for the sidebar."""
......@@ -439,6 +494,47 @@ def stats():
return json.dumps(data)
@app.route('/api/analyze', methods=['POST'])
def api_analyze():
"""API endpoint for analysis using user API tokens."""
auth_header = request.headers.get('Authorization')
if not auth_header or not auth_header.startswith('Bearer '):
return json.dumps({'error': 'Missing or invalid authorization header'}), 401
token = auth_header.split(' ')[1]
from .database import validate_user_api_token
user = validate_user_api_token(token)
if not user:
return json.dumps({'error': 'Invalid or expired token'}), 401
# Check token balance (skip for admin users)
if user.get('role') != 'admin':
tokens = get_user_tokens(user['id'])
if tokens <= 0:
return json.dumps({'error': 'Insufficient tokens'}), 402
# Process analysis request
model_path = request.json.get('model_path', 'Qwen/Qwen2.5-VL-7B-Instruct')
prompt = request.json.get('prompt', 'Describe this image.')
file_path = request.json.get('file_path')
if not file_path:
return json.dumps({'error': 'file_path is required'}), 400
# For API, we'll simulate the analysis (in real implementation, send to backend)
result = f"Analysis completed for {file_path} using model {model_path}"
# Deduct tokens (skip for admin users)
if user.get('role') != 'admin':
update_user_tokens(user['id'], -10)
return json.dumps({
'result': result,
'tokens_used': 10,
'remaining_tokens': get_user_tokens(user['id'])
})
@app.route('/static/<path:filename>')
def serve_static(filename):
return send_from_directory('static', filename)
......
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