Add comprehensive database integration, authentication, and admin dashboard
This commit introduces major enhancements to Talk2Me: ## Database Integration - PostgreSQL support with SQLAlchemy ORM - Redis integration for caching and real-time analytics - Automated database initialization scripts - Migration support infrastructure ## User Authentication System - JWT-based API authentication - Session-based web authentication - API key authentication for programmatic access - User roles and permissions (admin/user) - Login history and session tracking - Rate limiting per user with customizable limits ## Admin Dashboard - Real-time analytics and monitoring - User management interface (create, edit, delete users) - System health monitoring - Request/error tracking - Language pair usage statistics - Performance metrics visualization ## Key Features - Dual authentication support (token + user accounts) - Graceful fallback for missing services - Non-blocking analytics middleware - Comprehensive error handling - Session management with security features ## Bug Fixes - Fixed rate limiting bypass for admin routes - Added missing email validation method - Improved error handling for missing database tables - Fixed session-based authentication for API endpoints 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
284
app.py
284
app.py
@@ -5,7 +5,7 @@ import requests
|
||||
import json
|
||||
import logging
|
||||
from dotenv import load_dotenv
|
||||
from flask import Flask, render_template, request, jsonify, Response, send_file, send_from_directory, stream_with_context, g
|
||||
from flask import Flask, render_template, request, jsonify, Response, send_file, send_from_directory, stream_with_context, g, session, redirect, url_for
|
||||
from flask_cors import CORS, cross_origin
|
||||
import whisper
|
||||
import torch
|
||||
@@ -42,6 +42,13 @@ from session_manager import init_app as init_session_manager, track_resource
|
||||
from request_size_limiter import RequestSizeLimiter, limit_request_size
|
||||
from error_logger import ErrorLogger, log_errors, log_performance, log_exception, get_logger
|
||||
from memory_manager import MemoryManager, AudioProcessingContext, with_memory_management
|
||||
from analytics_middleware import analytics_tracker, track_translation, track_transcription, track_tts
|
||||
# Admin module will be loaded dynamically based on service availability
|
||||
from admin_loader import load_admin_module
|
||||
from auth import init_auth, require_auth, get_current_user, update_user_usage_stats
|
||||
from auth_routes import auth_bp
|
||||
from auth_models import User
|
||||
from user_rate_limiter import user_aware_rate_limit, get_user_rate_limit_status
|
||||
|
||||
# Error boundary decorator for Flask routes
|
||||
def with_error_boundary(func):
|
||||
@@ -166,6 +173,97 @@ error_logger = ErrorLogger(app, {
|
||||
# Update logger to use the new system
|
||||
logger = get_logger(__name__)
|
||||
|
||||
# Initialize analytics tracking
|
||||
analytics_tracker.init_app(app)
|
||||
|
||||
# Initialize database
|
||||
from database import db, init_db
|
||||
init_db(app)
|
||||
|
||||
# Initialize authentication system
|
||||
init_auth(app)
|
||||
|
||||
# Initialize admin dashboard dynamically
|
||||
admin_bp, init_admin = load_admin_module()
|
||||
init_admin(app)
|
||||
app.register_blueprint(admin_bp, url_prefix='/admin')
|
||||
|
||||
# Register authentication routes
|
||||
app.register_blueprint(auth_bp, url_prefix='/api/auth')
|
||||
|
||||
# Test route for session auth
|
||||
@app.route('/api/test-auth')
|
||||
def test_auth():
|
||||
"""Test authentication methods"""
|
||||
from flask import session as flask_session
|
||||
user = get_current_user()
|
||||
|
||||
# Also check admin count
|
||||
admin_count = User.query.filter_by(role='admin').count() if User else 0
|
||||
|
||||
return jsonify({
|
||||
'session_data': {
|
||||
'logged_in': flask_session.get('logged_in'),
|
||||
'user_id': flask_session.get('user_id'),
|
||||
'username': flask_session.get('username'),
|
||||
'user_role': flask_session.get('user_role'),
|
||||
'admin_token': bool(flask_session.get('admin_token'))
|
||||
},
|
||||
'current_user': {
|
||||
'found': user is not None,
|
||||
'username': user.username if user else None,
|
||||
'role': user.role if user else None,
|
||||
'is_admin': user.is_admin if user else None
|
||||
} if user else None,
|
||||
'admin_users_in_db': admin_count
|
||||
})
|
||||
|
||||
# Initialize admin user if none exists
|
||||
@app.route('/api/init-admin-user', methods=['POST'])
|
||||
def init_admin_user():
|
||||
"""Create initial admin user if none exists"""
|
||||
try:
|
||||
# Check if any admin users exist
|
||||
admin_exists = User.query.filter_by(role='admin').first()
|
||||
if admin_exists:
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'error': 'Admin user already exists'
|
||||
}), 400
|
||||
|
||||
# Create default admin user
|
||||
from auth import create_user
|
||||
user, error = create_user(
|
||||
email='admin@talk2me.local',
|
||||
username='admin',
|
||||
password='admin123', # Change this in production!
|
||||
full_name='Administrator',
|
||||
role='admin',
|
||||
is_verified=True
|
||||
)
|
||||
|
||||
if error:
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'error': error
|
||||
}), 400
|
||||
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'message': 'Admin user created successfully',
|
||||
'credentials': {
|
||||
'username': 'admin',
|
||||
'password': 'admin123',
|
||||
'note': 'Please change the password immediately!'
|
||||
}
|
||||
})
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to create admin user: {str(e)}")
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'error': 'Failed to create admin user'
|
||||
}), 500
|
||||
|
||||
# Initialize memory management
|
||||
memory_manager = MemoryManager(app, {
|
||||
'memory_threshold_mb': app.config.get('MEMORY_THRESHOLD_MB', 4096),
|
||||
@@ -673,14 +771,130 @@ LANGUAGE_TO_VOICE = {
|
||||
def index():
|
||||
return render_template('index.html', languages=sorted(SUPPORTED_LANGUAGES.values()))
|
||||
|
||||
@app.route('/login', methods=['GET', 'POST'])
|
||||
def login():
|
||||
"""User login page"""
|
||||
if request.method == 'POST':
|
||||
# Handle form-based login (for users without JavaScript)
|
||||
username = request.form.get('username')
|
||||
password = request.form.get('password')
|
||||
|
||||
# Special case: Check if it's the admin token being used as password
|
||||
admin_token = app.config.get('ADMIN_TOKEN', os.environ.get('ADMIN_TOKEN', 'default-admin-token'))
|
||||
if username == 'admin' and password == admin_token:
|
||||
# Direct admin login with token
|
||||
session['user_id'] = 'admin-token-user'
|
||||
session['username'] = 'admin'
|
||||
session['user_role'] = 'admin'
|
||||
session['logged_in'] = True
|
||||
session['admin_token'] = admin_token
|
||||
|
||||
next_url = request.args.get('next', url_for('admin.dashboard'))
|
||||
return redirect(next_url)
|
||||
|
||||
if username and password:
|
||||
# Try regular database authentication
|
||||
try:
|
||||
# Import here to avoid circular imports
|
||||
from auth import authenticate_user
|
||||
|
||||
user, error = authenticate_user(username, password)
|
||||
if not error and user:
|
||||
# Store user info in session
|
||||
session['user_id'] = str(user.id)
|
||||
session['username'] = user.username
|
||||
session['user_role'] = user.role
|
||||
session['logged_in'] = True
|
||||
|
||||
# Redirect based on role
|
||||
next_url = request.args.get('next')
|
||||
if next_url:
|
||||
return redirect(next_url)
|
||||
elif user.role == 'admin':
|
||||
return redirect(url_for('admin.dashboard'))
|
||||
else:
|
||||
return redirect(url_for('index'))
|
||||
else:
|
||||
return render_template('login.html', error=error or 'Login failed')
|
||||
except Exception as e:
|
||||
logger.error(f"Database login error: {e}")
|
||||
# If database login fails, still show error
|
||||
return render_template('login.html', error='Login failed - database error')
|
||||
|
||||
return render_template('login.html')
|
||||
|
||||
@app.route('/logout')
|
||||
def logout():
|
||||
"""Logout user"""
|
||||
session.clear()
|
||||
return redirect(url_for('index'))
|
||||
|
||||
@app.route('/admin-token-login', methods=['GET', 'POST'])
|
||||
def admin_token_login():
|
||||
"""Simple admin login with token only"""
|
||||
if request.method == 'POST':
|
||||
token = request.form.get('token', request.form.get('password', ''))
|
||||
admin_token = app.config.get('ADMIN_TOKEN', os.environ.get('ADMIN_TOKEN', 'default-admin-token'))
|
||||
|
||||
if token == admin_token:
|
||||
# Set admin session
|
||||
session['user_id'] = 'admin-token-user'
|
||||
session['username'] = 'admin'
|
||||
session['user_role'] = 'admin'
|
||||
session['logged_in'] = True
|
||||
session['admin_token'] = admin_token
|
||||
|
||||
next_url = request.args.get('next', url_for('admin.dashboard'))
|
||||
return redirect(next_url)
|
||||
else:
|
||||
error = 'Invalid admin token'
|
||||
else:
|
||||
error = None
|
||||
|
||||
# Simple form template
|
||||
return f'''
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Admin Token Login</title>
|
||||
<style>
|
||||
body {{ font-family: Arial; padding: 50px; background: #f0f0f0; }}
|
||||
.container {{ max-width: 400px; margin: auto; background: white; padding: 30px; border-radius: 10px; box-shadow: 0 2px 10px rgba(0,0,0,0.1); }}
|
||||
input {{ width: 100%; padding: 10px; margin: 10px 0; border: 1px solid #ddd; border-radius: 5px; }}
|
||||
button {{ width: 100%; padding: 10px; background: #667eea; color: white; border: none; border-radius: 5px; cursor: pointer; }}
|
||||
button:hover {{ background: #5a67d8; }}
|
||||
.error {{ color: red; margin: 10px 0; }}
|
||||
.info {{ color: #666; font-size: 14px; margin-top: 20px; }}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<h2>Admin Token Login</h2>
|
||||
{f'<div class="error">{error}</div>' if error else ''}
|
||||
<form method="POST">
|
||||
<input type="password" name="token" placeholder="Enter admin token" required autofocus>
|
||||
<button type="submit">Login as Admin</button>
|
||||
</form>
|
||||
<div class="info">
|
||||
<p>Use the ADMIN_TOKEN from your .env file</p>
|
||||
<p>Current token: {admin_token if app.debug else '[hidden in production]'}</p>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
'''
|
||||
|
||||
@app.route('/transcribe', methods=['POST'])
|
||||
@rate_limit(requests_per_minute=10, requests_per_hour=100, check_size=True)
|
||||
@user_aware_rate_limit(requests_per_minute=10, requests_per_hour=100, check_size=True)
|
||||
@limit_request_size(max_audio_size=25 * 1024 * 1024) # 25MB limit for audio
|
||||
@with_error_boundary
|
||||
@track_resource('audio_file')
|
||||
@log_performance('transcribe_audio')
|
||||
@with_memory_management
|
||||
def transcribe():
|
||||
# Get current user if authenticated
|
||||
user = get_current_user()
|
||||
|
||||
# Use memory management context
|
||||
with AudioProcessingContext(app.memory_manager, name='transcribe') as ctx:
|
||||
if 'audio' not in request.files:
|
||||
@@ -767,6 +981,24 @@ def transcribe():
|
||||
# Log detected language
|
||||
logger.info(f"Auto-detected language: {detected_language} ({detected_code})")
|
||||
|
||||
# Update user usage stats if authenticated
|
||||
if user:
|
||||
update_user_usage_stats(user, 'transcription')
|
||||
|
||||
# Track transcription analytics
|
||||
analytics_tracker._track_operation_complete(
|
||||
'transcriptions',
|
||||
int((time.time() - g.start_time) * 1000),
|
||||
True,
|
||||
None,
|
||||
{
|
||||
'detected_language': detected_language or source_lang,
|
||||
'audio_duration': len(transcribed_text.split()) / 3, # Rough estimate
|
||||
'file_size': os.path.getsize(temp_path)
|
||||
},
|
||||
{'detected_language': detected_language, 'text': transcribed_text}
|
||||
)
|
||||
|
||||
# Send notification if push is enabled
|
||||
if len(push_subscriptions) > 0:
|
||||
send_push_notification(
|
||||
@@ -804,12 +1036,15 @@ def transcribe():
|
||||
gc.collect()
|
||||
|
||||
@app.route('/translate', methods=['POST'])
|
||||
@rate_limit(requests_per_minute=20, requests_per_hour=300, check_size=True)
|
||||
@user_aware_rate_limit(requests_per_minute=20, requests_per_hour=300, check_size=True)
|
||||
@limit_request_size(max_size=1 * 1024 * 1024) # 1MB limit for JSON
|
||||
@with_error_boundary
|
||||
@log_performance('translate_text')
|
||||
def translate():
|
||||
try:
|
||||
# Get current user if authenticated
|
||||
user = get_current_user()
|
||||
|
||||
# Validate request size
|
||||
if not Validators.validate_json_size(request.json, max_size_kb=100):
|
||||
return jsonify({'error': 'Request too large'}), 413
|
||||
@@ -856,6 +1091,24 @@ def translate():
|
||||
|
||||
translated_text = response['message']['content'].strip()
|
||||
|
||||
# Update user usage stats if authenticated
|
||||
if user:
|
||||
update_user_usage_stats(user, 'translation')
|
||||
|
||||
# Track translation analytics
|
||||
analytics_tracker._track_operation_complete(
|
||||
'translations',
|
||||
int((time.time() - g.start_time) * 1000),
|
||||
True,
|
||||
None,
|
||||
{
|
||||
'source_lang': source_lang,
|
||||
'target_lang': target_lang,
|
||||
'text_length': len(text)
|
||||
},
|
||||
{'translation': translated_text}
|
||||
)
|
||||
|
||||
# Send notification if push is enabled
|
||||
if len(push_subscriptions) > 0:
|
||||
send_push_notification(
|
||||
@@ -874,7 +1127,7 @@ def translate():
|
||||
return jsonify({'error': f'Translation failed: {str(e)}'}), 500
|
||||
|
||||
@app.route('/translate/stream', methods=['POST'])
|
||||
@rate_limit(requests_per_minute=10, requests_per_hour=150, check_size=True)
|
||||
@user_aware_rate_limit(requests_per_minute=10, requests_per_hour=150, check_size=True)
|
||||
@limit_request_size(max_size=1 * 1024 * 1024) # 1MB limit for JSON
|
||||
@with_error_boundary
|
||||
def translate_stream():
|
||||
@@ -972,12 +1225,15 @@ def translate_stream():
|
||||
return jsonify({'error': f'Translation failed: {str(e)}'}), 500
|
||||
|
||||
@app.route('/speak', methods=['POST'])
|
||||
@rate_limit(requests_per_minute=15, requests_per_hour=200, check_size=True)
|
||||
@user_aware_rate_limit(requests_per_minute=15, requests_per_hour=200, check_size=True)
|
||||
@limit_request_size(max_size=1 * 1024 * 1024) # 1MB limit for JSON
|
||||
@with_error_boundary
|
||||
@track_resource('audio_file')
|
||||
def speak():
|
||||
try:
|
||||
# Get current user if authenticated
|
||||
user = get_current_user()
|
||||
|
||||
# Validate request size
|
||||
if not Validators.validate_json_size(request.json, max_size_kb=100):
|
||||
return jsonify({'error': 'Request too large'}), 413
|
||||
@@ -1066,6 +1322,24 @@ def speak():
|
||||
# Register for cleanup
|
||||
register_temp_file(temp_audio_path)
|
||||
|
||||
# Update user usage stats if authenticated
|
||||
if user:
|
||||
update_user_usage_stats(user, 'tts')
|
||||
|
||||
# Track TTS analytics
|
||||
analytics_tracker._track_operation_complete(
|
||||
'tts',
|
||||
int((time.time() - g.start_time) * 1000),
|
||||
True,
|
||||
None,
|
||||
{
|
||||
'language': language,
|
||||
'text_length': len(text),
|
||||
'voice': voice
|
||||
},
|
||||
{'audio_file': temp_audio_filename}
|
||||
)
|
||||
|
||||
# Add to session resources
|
||||
if hasattr(g, 'session_manager') and hasattr(g, 'user_session'):
|
||||
file_size = os.path.getsize(temp_audio_path)
|
||||
|
||||
Reference in New Issue
Block a user