2026-08-09 11:41:34 +00:00
|
|
|
"""Auth and user management API routes"""
|
|
|
|
|
|
2026-02-24 16:19:15 +00:00
|
|
|
from flask import Blueprint, jsonify, request
|
2026-08-09 11:41:34 +00:00
|
|
|
from flask_jwt_extended import (
|
|
|
|
|
create_access_token,
|
|
|
|
|
create_refresh_token,
|
|
|
|
|
get_jwt_identity,
|
|
|
|
|
jwt_required,
|
|
|
|
|
)
|
2026-02-24 11:03:23 +00:00
|
|
|
|
2026-02-14 16:56:10 +00:00
|
|
|
from app import db
|
2026-03-22 11:52:33 +00:00
|
|
|
from app.models import User
|
2026-08-09 11:41:34 +00:00
|
|
|
from app.services.auth_service import (
|
|
|
|
|
authenticate_user,
|
|
|
|
|
change_password,
|
|
|
|
|
get_login_history,
|
|
|
|
|
register_user,
|
|
|
|
|
)
|
2026-02-14 16:56:10 +00:00
|
|
|
|
|
|
|
|
api_bp = Blueprint("api", __name__)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# User Routes
|
|
|
|
|
@api_bp.route("/auth/register", methods=["POST"])
|
|
|
|
|
def register():
|
|
|
|
|
"""Register a new user"""
|
2026-08-09 11:41:34 +00:00
|
|
|
try:
|
|
|
|
|
data = request.get_json()
|
|
|
|
|
|
|
|
|
|
if not data:
|
|
|
|
|
return jsonify({"error": "Request body is required"}), 400
|
2026-02-24 16:19:15 +00:00
|
|
|
|
2026-08-09 11:41:34 +00:00
|
|
|
# Server-side validation: require confirm_password for all registrations
|
|
|
|
|
confirm = data.get("confirm_password")
|
|
|
|
|
if not confirm:
|
|
|
|
|
return jsonify({"error": "confirm_password is required"}), 400
|
2026-02-24 16:19:15 +00:00
|
|
|
|
2026-08-09 11:41:34 +00:00
|
|
|
# Use the auth service for registration with validation
|
|
|
|
|
user = register_user(data)
|
2026-02-24 16:19:15 +00:00
|
|
|
|
2026-08-09 11:41:34 +00:00
|
|
|
# Generate tokens so user is logged in immediately
|
|
|
|
|
access_token = create_access_token(identity=str(user.id))
|
|
|
|
|
refresh_token = create_refresh_token(identity=str(user.id))
|
2026-02-24 16:19:15 +00:00
|
|
|
|
2026-08-09 11:41:34 +00:00
|
|
|
return (
|
|
|
|
|
jsonify(
|
|
|
|
|
{
|
|
|
|
|
"user": user.to_dict(),
|
|
|
|
|
"access_token": access_token,
|
|
|
|
|
"refresh_token": refresh_token,
|
|
|
|
|
}
|
|
|
|
|
),
|
|
|
|
|
201,
|
|
|
|
|
)
|
2026-02-24 16:19:15 +00:00
|
|
|
|
2026-08-09 11:41:34 +00:00
|
|
|
except ValueError as e:
|
|
|
|
|
return jsonify({"error": str(e)}), 400
|
2026-02-14 16:56:10 +00:00
|
|
|
|
|
|
|
|
|
|
|
|
|
@api_bp.route("/auth/login", methods=["POST"])
|
|
|
|
|
def login():
|
2026-08-09 11:41:34 +00:00
|
|
|
"""Login user with account lockout and login history tracking"""
|
|
|
|
|
try:
|
|
|
|
|
data = request.get_json()
|
|
|
|
|
|
|
|
|
|
if not data or not data.get("email") or not data.get("password"):
|
|
|
|
|
return jsonify({"error": "Email and password are required"}), 400
|
|
|
|
|
|
|
|
|
|
# Capture IP and user agent for login history
|
|
|
|
|
ip_address = request.remote_addr or "unknown"
|
|
|
|
|
user_agent = request.headers.get("User-Agent", "unknown")
|
|
|
|
|
|
|
|
|
|
user, access_token, refresh_token = authenticate_user(
|
|
|
|
|
email=data["email"],
|
|
|
|
|
password=data["password"],
|
|
|
|
|
ip_address=ip_address,
|
|
|
|
|
user_agent=user_agent,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
jsonify(
|
|
|
|
|
{
|
|
|
|
|
"user": user.to_dict(),
|
|
|
|
|
"access_token": access_token,
|
|
|
|
|
"refresh_token": refresh_token,
|
|
|
|
|
}
|
|
|
|
|
),
|
|
|
|
|
200,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
except ValueError as e:
|
|
|
|
|
return jsonify({"error": str(e)}), 401
|
2026-02-14 16:56:10 +00:00
|
|
|
|
|
|
|
|
|
|
|
|
|
@api_bp.route("/users/me", methods=["GET"])
|
|
|
|
|
@jwt_required()
|
|
|
|
|
def get_current_user():
|
2026-08-09 11:41:34 +00:00
|
|
|
"""Get current user profile"""
|
2026-02-24 14:36:31 +00:00
|
|
|
user_id = int(get_jwt_identity())
|
|
|
|
|
user = db.session.get(User, user_id)
|
2026-02-24 16:19:15 +00:00
|
|
|
|
2026-02-14 16:56:10 +00:00
|
|
|
if not user:
|
|
|
|
|
return jsonify({"error": "User not found"}), 404
|
2026-02-24 16:19:15 +00:00
|
|
|
|
2026-02-14 16:56:10 +00:00
|
|
|
return jsonify(user.to_dict()), 200
|
2026-08-09 11:41:34 +00:00
|
|
|
|
|
|
|
|
|
|
|
|
|
@api_bp.route("/users/me/password", methods=["PUT"])
|
|
|
|
|
@jwt_required()
|
|
|
|
|
def update_password():
|
|
|
|
|
"""Change current user's password"""
|
|
|
|
|
try:
|
|
|
|
|
data = request.get_json()
|
|
|
|
|
|
|
|
|
|
if not data:
|
|
|
|
|
return jsonify({"error": "Request body is required"}), 400
|
|
|
|
|
|
|
|
|
|
current_password = data.get("current_password")
|
|
|
|
|
new_password = data.get("new_password")
|
|
|
|
|
confirm_password = data.get("confirm_password")
|
|
|
|
|
|
|
|
|
|
if not all([current_password, new_password, confirm_password]):
|
|
|
|
|
return jsonify(
|
|
|
|
|
{
|
|
|
|
|
"error": "current_password, new_password, and confirm_password are required"
|
|
|
|
|
}
|
|
|
|
|
), 400
|
|
|
|
|
|
|
|
|
|
user_id = int(get_jwt_identity())
|
|
|
|
|
user = db.session.get(User, user_id)
|
|
|
|
|
|
|
|
|
|
if not user:
|
|
|
|
|
return jsonify({"error": "User not found"}), 404
|
|
|
|
|
|
|
|
|
|
change_password(user, current_password, new_password, confirm_password)
|
|
|
|
|
|
|
|
|
|
return jsonify({"message": "Password updated successfully"}), 200
|
|
|
|
|
|
|
|
|
|
except ValueError as e:
|
|
|
|
|
return jsonify({"error": str(e)}), 400
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@api_bp.route("/users/me/login-history", methods=["GET"])
|
|
|
|
|
@jwt_required()
|
|
|
|
|
def get_user_login_history():
|
|
|
|
|
"""Get login history for current user"""
|
|
|
|
|
user_id = int(get_jwt_identity())
|
|
|
|
|
|
|
|
|
|
limit = request.args.get("limit", 20, type=int)
|
|
|
|
|
# Sanity check on limit
|
|
|
|
|
limit = min(max(limit, 1), 100)
|
|
|
|
|
|
|
|
|
|
history = get_login_history(user_id, limit=limit)
|
|
|
|
|
|
|
|
|
|
return jsonify(history), 200
|