from flask import Blueprint, request, jsonify, g, make_response from pydantic import ValidationError import jwt from app.services.user_service import UserService from app.services.jwt_service import JWTService from app.schemas.auth_dto import LoginDTO, RefreshTokenDTO from app.utils.jwt_decorators import jwt_required api_auth_bp = Blueprint('api_auth', __name__, url_prefix='/api/v1/auth') user_service = UserService() @api_auth_bp.route('/login', methods=['POST']) def login(): """ Endpoint de autenticación para obtener par de tokens (Access + Refresh). """ data = request.get_json(silent=True) or {} try: dto = LoginDTO(**data) except ValidationError as e: return jsonify({'error': 'ValidationError', 'details': e.errors()}), 400 user = user_service.authenticate(dto.email, dto.password) if not user: return jsonify({ 'error': 'Unauthorized', 'message': 'Credenciales de acceso incorrectas o cuenta inactiva.' }), 401 tokens = JWTService.generate_tokens(user) profile = user_service.get_profile_data(user) response_data = { 'access_token': tokens['access_token'], 'refresh_token': tokens['refresh_token'], 'token_type': tokens['token_type'], 'expires_in': tokens['expires_in'], 'user': profile } resp = make_response(jsonify(response_data), 200) # Almacenar refresh token en cookie segura HttpOnly resp.set_cookie( 'refresh_token', tokens['refresh_token'], httponly=True, samesite='Lax', max_age=7 * 24 * 3600, path='/api/v1/auth/refresh' ) return resp @api_auth_bp.route('/refresh', methods=['POST']) def refresh(): """ Renovación silenciosa del Access Token utilizando el Refresh Token. """ data = request.get_json(silent=True) or {} refresh_token = data.get('refresh_token') or request.cookies.get('refresh_token') if not refresh_token: return jsonify({ 'error': 'BadRequest', 'message': 'Refresh token no suministrado en el cuerpo ni en cookies.' }), 400 try: payload = JWTService.decode_token(refresh_token, expected_type='refresh') user_id = int(payload.get('sub')) user = user_service.get_by_id(user_id) if not user or not user.is_active: return jsonify({ 'error': 'Unauthorized', 'message': 'Usuario no encontrado o inactivo.' }), 401 new_access_token = JWTService.create_access_token(user) return jsonify({ 'access_token': new_access_token, 'token_type': 'Bearer', 'expires_in': int(JWTService.ACCESS_TOKEN_EXPIRES.total_seconds()) }), 200 except jwt.ExpiredSignatureError: return jsonify({'error': 'TokenExpired', 'message': 'El refresh token ha expirado. Reingrese credenciales.'}), 401 except jwt.InvalidTokenError as e: return jsonify({'error': 'InvalidToken', 'message': str(e)}), 401 @api_auth_bp.route('/logout', methods=['POST']) @jwt_required def logout(): """ Cierre de sesión: revoca el token de acceso activo y limpia cookies. """ JWTService.revoke_token(g.jwt_token) resp = make_response(jsonify({'message': 'Sesión finalizada y token revocado exitosamente.'}), 200) resp.delete_cookie('refresh_token', path='/api/v1/auth/refresh') return resp @api_auth_bp.route('/me', methods=['GET']) @jwt_required def get_current_user(): """ Retorna el perfil y los permisos del usuario autenticado vía JWT. """ profile = user_service.get_profile_data(g.jwt_user) return jsonify(profile), 200