“shubhamdhamal”
Fix: Include milestone progress in API responses for mobile app
a5cfef0
from flask import Blueprint, render_template, redirect, url_for, flash, request, jsonify, current_app
from flask_login import login_user, logout_user, login_required, current_user
# Assuming db and login_manager are initialized in __init__.py
from web_app import db, login_manager
from web_app.models import User
from web_app.auth_forms import LoginForm, RegistrationForm
import json
import random
import logging
import jwt
from datetime import datetime, timedelta
from functools import wraps
# Set up logging
logger = logging.getLogger(__name__)
# Define the blueprint
# If we later move this to an 'auth' subdirectory, the template_folder might change.
bp = Blueprint('auth', __name__, template_folder='templates/auth')
# ============================================================
# JWT TOKEN UTILITIES FOR MOBILE APP
# ============================================================
def generate_jwt_token(user_id, expires_in_days=30):
"""Generate a JWT token for mobile authentication"""
payload = {
'user_id': user_id,
'exp': datetime.utcnow() + timedelta(days=expires_in_days),
'iat': datetime.utcnow()
}
return jwt.encode(payload, current_app.config['SECRET_KEY'], algorithm='HS256')
def verify_jwt_token(token):
"""Verify a JWT token and return the user_id"""
try:
payload = jwt.decode(
token, current_app.config['SECRET_KEY'], algorithms=['HS256'])
return payload.get('user_id')
except jwt.ExpiredSignatureError:
return None
except jwt.InvalidTokenError:
return None
def token_required(f):
"""Decorator to require JWT token authentication for API endpoints"""
@wraps(f)
def decorated(*args, **kwargs):
token = None
# Check for token in Authorization header
auth_header = request.headers.get('Authorization')
if auth_header and auth_header.startswith('Bearer '):
token = auth_header.split(' ')[1]
if not token:
return jsonify({'error': 'Authentication token is missing'}), 401
user_id = verify_jwt_token(token)
if not user_id:
return jsonify({'error': 'Invalid or expired token'}), 401
# Get the user
user = User.query.get(user_id)
if not user:
return jsonify({'error': 'User not found'}), 401
# Pass the user to the route
return f(user, *args, **kwargs)
return decorated
@bp.route('/register', methods=['GET', 'POST'])
def register():
if current_user.is_authenticated:
return redirect(url_for('main.dashboard'))
form = RegistrationForm()
# Handle form submission
if form.validate_on_submit():
logger.info(
f"Registration form validated for email: {form.email.data}")
user = User(username=form.username.data, email=form.email.data)
user.set_password(form.password.data)
# Add registration metadata
user.registration_source = 'email_password'
user.last_seen = datetime.utcnow()
db.session.add(user)
db.session.commit()
logger.info(f"User created: {user.email}")
# Personalized welcome message
welcome_messages = [
f"Welcome to the community, {user.username}!",
f"Your learning journey begins now, {user.username}!",
f"Congratulations on joining, {user.username}!",
f"You're all set to start learning, {user.username}!"
]
flash(random.choice(welcome_messages), 'success')
# Auto-login after registration for seamless experience
login_user(user)
logger.info(f"User logged in after registration: {user.email}")
dashboard_url = url_for('main.dashboard')
logger.info(f"Redirecting to dashboard: {dashboard_url}")
# Use render template with meta refresh as fallback
return render_template('redirect.html', redirect_url=dashboard_url)
else:
if form.is_submitted():
logger.warning(
f"Registration form validation failed: {form.errors}")
return render_template('register.html', title='Join the Community', form=form)
@bp.route('/login', methods=['GET', 'POST'])
def login():
if current_user.is_authenticated:
return redirect(url_for('main.dashboard'))
form = LoginForm()
if form.validate_on_submit():
logger.info(f"Login form validated for email: {form.email.data}")
user = User.query.filter_by(email=form.email.data).first()
if user is None or not user.check_password(form.password.data):
# Helpful error message
if user is None:
logger.warning(
f"Login failed: No user found for email {form.email.data}")
flash(
'No account found with this email. Would you like to register?', 'warning')
return redirect(url_for('auth.register', email=form.email.data))
else:
logger.warning(
f"Login failed: Wrong password for {form.email.data}")
flash('Incorrect password. Please try again.', 'danger')
return redirect(url_for('auth.login'))
# Login successful
login_user(user, remember=form.remember_me.data)
logger.info(f"User logged in successfully: {user.email}")
# Update last seen
user.last_seen = datetime.utcnow()
db.session.commit()
# Personalized welcome back message
greeting = form.get_greeting()
motivation = form.get_motivation()
flash(f"{greeting} {motivation}", 'success')
# Redirect handling
next_page = request.args.get('next')
# Basic security check - default to dashboard if no next page or invalid
if not next_page or not next_page.startswith('/'):
next_page = url_for('main.dashboard')
logger.info(f"Redirecting to: {next_page}")
# Use render template with meta refresh as fallback
return render_template('redirect.html', redirect_url=next_page)
else:
if form.is_submitted():
logger.warning(f"Login form validation failed: {form.errors}")
# Pre-fill email if coming from registration suggestion
if request.args.get('email'):
form.email.data = request.args.get('email')
return render_template('login.html', title='Welcome Back', form=form)
@bp.route('/logout')
@login_required
def logout():
username = current_user.username
logout_user()
# Friendly goodbye messages
goodbye_messages = [
f"See you soon, {username}!",
f"Come back soon, {username}!",
f"Your learning path will be waiting, {username}!",
f"Taking a break? We'll be here when you return, {username}!"
]
flash(random.choice(goodbye_messages), 'info')
return redirect('/')
# This is needed by Flask-Login to load a user from the session
@login_manager.user_loader
def load_user(user_id):
return User.query.get(int(user_id))
# AJAX routes for enhanced user experience
@bp.route('/check-username', methods=['POST'])
def check_username():
"""Check if a username is available and suggest alternatives if not"""
data = request.get_json(silent=True) or {}
username = (data.get('username') or '').strip()
if len(username) < 3:
return jsonify({
'available': False,
'message': 'Username must be at least 3 characters long'
})
user = User.query.filter_by(username=username).first()
if user is not None:
base = username
suggestions = [
f"{base}{random.randint(1, 999)}",
f"awesome_{base}",
f"{base}_learner"
]
return jsonify({
'available': False,
'message': 'This username is already taken',
'suggestions': suggestions
})
return jsonify({
'available': True,
'message': 'Username is available!'
})
# ============================================================
# JSON API ENDPOINTS FOR MOBILE APP
# ============================================================
@bp.route('/api/register', methods=['POST'])
def register_json():
"""JSON API endpoint for mobile app registration"""
try:
data = request.get_json(silent=True) or {}
username = data.get('username', '').strip(
) or data.get('name', '').strip()
email = data.get('email', '').strip()
password = data.get('password', '').strip()
password2 = data.get('password2', '').strip(
) or password # If not provided, use password
# Validation
if not all([username, email, password]):
return jsonify({'error': 'All fields are required'}), 400
if len(username) < 3:
return jsonify({'error': 'Username must be at least 3 characters'}), 400
if '@' not in email or '.' not in email:
return jsonify({'error': 'Invalid email format'}), 400
if len(password) < 6:
return jsonify({'error': 'Password must be at least 6 characters'}), 400
if password != password2:
return jsonify({'error': 'Passwords do not match'}), 400
# Check if user exists
if User.query.filter_by(username=username).first():
return jsonify({'error': 'Username already exists'}), 409
if User.query.filter_by(email=email).first():
return jsonify({'error': 'Email already registered'}), 409
# Create user
user = User(username=username, email=email)
user.set_password(password)
user.registration_source = 'mobile'
user.last_seen = datetime.utcnow()
db.session.add(user)
db.session.commit()
# Generate JWT token for mobile authentication
token = generate_jwt_token(user.id)
return jsonify({
'success': True,
'message': 'Registration successful',
'user': {
'id': user.id,
'username': user.username,
'email': user.email,
'display_name': user.display_name or user.username
},
'token': token
}), 201
except Exception as e:
db.session.rollback()
return jsonify({'error': str(e)}), 500
@bp.route('/api/login', methods=['POST'])
def login_json():
"""JSON API endpoint for mobile app login"""
try:
data = request.get_json(silent=True) or {}
email = data.get('email', '').strip()
password = data.get('password', '').strip()
if not email or not password:
return jsonify({'error': 'Email and password are required'}), 400
user = User.query.filter_by(email=email).first()
if user is None or not user.check_password(password):
return jsonify({'error': 'Invalid email or password'}), 401
# Update last seen and login count
user.last_seen = datetime.utcnow()
user.login_count = (user.login_count or 0) + 1
db.session.commit()
# Generate JWT token for mobile authentication
token = generate_jwt_token(user.id)
return jsonify({
'success': True,
'message': 'Login successful',
'user': {
'id': user.id,
'username': user.username,
'email': user.email,
'display_name': user.display_name or user.username
},
'token': token
}), 200
except Exception as e:
return jsonify({'error': str(e)}), 500
@bp.route('/api/logout', methods=['POST'])
@login_required
def logout_json():
"""JSON API endpoint for mobile app logout"""
try:
logout_user()
return jsonify({
'success': True,
'message': 'Logout successful'
}), 200
except Exception as e:
return jsonify({'error': str(e)}), 500
@bp.route('/user', methods=['GET'])
@login_required
def get_user():
"""Get current user information - JSON endpoint for mobile"""
try:
user = current_user
return jsonify({
'success': True,
'user': {
'id': user.id,
'username': user.username,
'email': user.email,
'display_name': user.display_name or user.username,
'bio': user.bio or '',
'created_at': user.date_created.isoformat() if user.date_created else None,
'login_count': getattr(user, 'login_count', 0),
'registration_source': user.registration_source or 'web'
}
}), 200
except Exception as e:
return jsonify({'error': str(e)}), 500