Coverage for api\views\user_views.py: 93.8%
370 statements
« prev ^ index » next coverage.py v7.15.2, created at 2026-08-30 20:58 +0200
« prev ^ index » next coverage.py v7.15.2, created at 2026-08-30 20:58 +0200
1import logging
2from django.core.exceptions import ValidationError as DjangoValidationError
3from django.http import Http404
4from drf_spectacular.utils import extend_schema
5from rest_framework import status, viewsets
6from rest_framework.decorators import action
7from rest_framework.parsers import FormParser, MultiPartParser
8from rest_framework.exceptions import APIException, MethodNotAllowed
9from rest_framework.permissions import IsAuthenticated
10from rest_framework.response import Response
11from api.errors import ErrorCodes
12from api.serializers.travel_serializer import RequestTravelsSerializer, TravelSerializer
13from api.serializers.user_serializer import (NotificationsSerializer, PreferencesSerializer,
14 ProfilePicSerializer, UserSerializer, VehicleSerializer)
15from api.ownership import OwnedQuerysetMixin
16from api.soft_delete import SoftDeleteQuerysetMixin
17from users.models import Users
18from users.services.auth_service import EMAIL_DOMAIN_ERROR_MESSAGE, AuthService
19from users.services.exceptions import (EmailAlreadyRegisteredError,EmailDeliveryError,EmailDomainNotAllowedError,
20 IncorrectPasswordError,InvalidTokenError,NoUpcomingTravelsError,RequestStatusMissingError,
21 RequestTravelNotValidatedError,TokenExpiredError,UserNotFoundError)
22from users.services.user_service import UserService
24# Logger para almacenar los logs
25logger = logging.getLogger(__name__)
28# Endpoints de la cuenta del propio usuario.
29class UsersViewSet(OwnedQuerysetMixin, SoftDeleteQuerysetMixin, viewsets.ModelViewSet):
30 queryset = Users.objects.all()
31 serializer_class = UserSerializer
32 permission_classes = [IsAuthenticated]
33 # EL dueño del registro es el propio registro
34 owner_field = 'id'
36 lookup_field = 'id'
37 lookup_url_kwarg = 'id'
38 lookup_value_regex = '[0-9a-f-]{36}'
40 # No se utiliza put, por lo que no se pone
41 http_method_names = ['get', 'post', 'patch', 'delete', 'head', 'options']
43 # Sin ambito por defecto
44 throttle_scope = None
46 @extend_schema(exclude=True)
47 def create(self, request, *args, **kwargs):
48 """
49 POST /api/v1/users/ no existe. El alta es con POST /api/v1/register/.
50 Este endpoint lo genera automaticamente Django, por lo que se cierra
51 """
52 raise MethodNotAllowed('POST')
54 # Se sobreescriben los metodos list y retrieve para añadir logs y manejo de errores
55 def list(self, request, *args, **kwargs):
56 logger.info("Accessed UsersViewSet list endpoint")
57 try:
58 response = super().list(request, *args, **kwargs)
59 logger.info("Successfully retrieved user list")
60 return response
61 except (Http404, APIException, DjangoValidationError):
62 raise
63 except Exception as e:
64 logger.error(f"Error in UsersViewSet list endpoint: {str(e)}")
65 return Response({"error": "An error occurred while retrieving the user list", "error_code": ErrorCodes.INTERNAL_SERVER_ERROR}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
67 def retrieve(self, request, *args, **kwargs):
68 logger.info(f"Accessed UsersViewSet retrieve endpoint for user ID: {kwargs.get('id')}")
69 try:
70 response = super().retrieve(request, *args, **kwargs)
71 logger.info(f"Successfully retrieved user ID: {kwargs.get('id')}")
72 return response
73 except Http404:
74 raise
75 except Exception as e:
76 logger.error(f"Error in UsersViewSet retrieve endpoint for user ID {kwargs.get('id')}: {str(e)}")
77 return Response({"error": "An error occurred while retrieving the user", "error_code": ErrorCodes.INTERNAL_SERVER_ERROR}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
79 @action(
80 detail=True,
81 methods=['patch'],
82 url_path='upload-profile_picture',
83 parser_classes=[MultiPartParser, FormParser],
84 permission_classes=[IsAuthenticated]
85 )
86 def upload_profile_pic(self, request, *args, **kwargs):
87 """
88 Endpoint PATCH /users/{id}/upload-profile_picture/
89 Permite subir la imagen de perfil de un usuario
90 Recibe una imagen
91 Devuelve un mensaje de error o exito y el nombre del archivo subido
92 """
93 user = self.get_object()
94 logger.info(f"Accessed upload_profile_pic endpoint for user ID: {user.id}")
96 # Es necesario que se añada el campo de profile picture
97 if 'profile_picture' not in request.data:
98 logger.warning("profile_picture field is missing in the request")
99 return Response(
100 {"error": "profile_picture field is required", "error_code": ErrorCodes.MISSING_REQUIRED_FIELD},
101 status=status.HTTP_400_BAD_REQUEST
102 )
104 try:
105 # Se carga y guarda la imagen de perfil
106 profile_picture = request.data['profile_picture']
107 serializer = ProfilePicSerializer(data={"profile_picture": profile_picture})
108 serializer.is_valid(raise_exception=True)
110 name = UserService.update_profile_picture(user, profile_picture)
111 return Response({"message": "Profile picture updated successfully", "name": name}, status=status.HTTP_200_OK)
112 except (Http404, APIException, DjangoValidationError):
113 raise
114 except Exception as e:
115 logger.error(f"Error in upload_profile_pic endpoint for user ID {user.id}: {str(e)}")
116 return Response({"error": "An error occurred while uploading the profile picture", "error_code": ErrorCodes.INTERNAL_SERVER_ERROR}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
118 @action(
119 detail=True,
120 methods=['delete'],
121 url_path='delete-profile_picture',
122 parser_classes=[MultiPartParser, FormParser],
123 permission_classes=[IsAuthenticated]
124 )
125 def delete_profile_pic(self, request, *args, **kwargs):
126 """
127 Endpoint DELETE /users/{id}/delete-profile_picture/
128 Permite borrar la imagen de perfil de un usuario
129 Devuelve un mensaje de error o exito
130 """
131 user = self.get_object()
133 # Si no habia imagen de perfil, se debe devolver un mensaje de exito, ya que esta lo que queria, eliminar la imagen
134 if not UserService.delete_profile_picture(user):
135 return Response({"message": "No profile picture to delete"}, status=status.HTTP_200_OK)
137 return Response({
138 "message": "Profile picture deleted successfully",
139 }, status=status.HTTP_200_OK)
141 @action(
142 detail=True,
143 methods=['get'],
144 url_path='vehicles',
145 permission_classes=[IsAuthenticated]
146 )
147 def get_vehicles(self, request, *args, **kwargs):
148 """
149 Endpoint GET /users/{id}/vehicles/
150 Devuelve todos los vehículos activos del usuario paginados
151 """
152 user = self.get_object()
153 logger.info(f"Accessed get_vehicles endpoint for user ID: {user.id}")
155 try:
156 return self.paginated_response(
157 request, UserService.list_vehicles(user), VehicleSerializer,
158 f"Returned vehicles for user ID: {user.id}"
159 )
160 except (Http404, APIException, DjangoValidationError):
161 raise
162 except Exception as e:
163 logger.error(f"Error in get_vehicles: {str(e)}")
164 return Response({"error": "Error retrieving vehicles", "error_code": ErrorCodes.INTERNAL_SERVER_ERROR}, status=500)
166 @action(
167 detail=True,
168 methods=['get'],
169 url_path='notifications',
170 permission_classes=[IsAuthenticated]
171 )
172 def get_notifications(self, request, *args, **kwargs):
173 """
174 Endpoint GET /users/{id}/notifications/?ordering=desc|asc
175 Devuelve todas las notificaciones no borradas del usuario paginadas, ordenadas por fecha
176 """
177 user = self.get_object()
178 logger.info(f"Accessed get_notifications endpoint for user ID: {user.id}")
180 try:
181 ordering = request.query_params.get('ordering', 'desc')
182 return self.paginated_response(
183 request, UserService.list_notifications(user, ordering), NotificationsSerializer,
184 f"Returned notifications for user ID: {user.id}"
185 )
186 except (Http404, APIException, DjangoValidationError):
187 raise
188 except Exception as e:
189 logger.error(f"Error in get_notifications: {str(e)}")
190 return Response({"error": "Error retrieving notifications", "error_code": ErrorCodes.INTERNAL_SERVER_ERROR}, status=500)
192 @action(
193 detail=True,
194 methods=['get'],
195 url_path='notifications/unread-count',
196 permission_classes=[IsAuthenticated]
197 )
198 def get_unread_notifications_count(self, request, *args, **kwargs):
199 """
200 Endpoint GET /users/{id}/notifications/unread-count/
201 Devuelve el numero de notificaciones no leidas del usuario
202 """
203 user = self.get_object()
204 logger.info(f"Accessed get_unread_notifications_count endpoint for user ID: {user.id}")
206 try:
207 return Response({"count": UserService.count_unread_notifications(user)})
208 except (Http404, APIException, DjangoValidationError):
209 raise
210 except Exception as e:
211 logger.error(f"Error in get_unread_notifications_count: {str(e)}")
212 return Response({"error": "Error retrieving unread notifications count", "error_code": ErrorCodes.INTERNAL_SERVER_ERROR}, status=500)
214 @action(
215 detail=True,
216 methods=['get'],
217 url_path='driverratings',
218 permission_classes=[IsAuthenticated]
219 )
220 def get_driverratings(self, request, *args, **kwargs):
221 """
222 Endpoint GET /users/{id}/driverratings/
223 Devuelve la valoración media de cada criterio para un conductor, junto con el número total de valoraciones recibidas
224 """
225 user = self.get_object()
226 logger.info(f"Accessed get_driverratings endpoint for user ID: {user.id}")
228 try:
229 return Response(UserService.get_driver_ratings(user), status=200)
230 except (Http404, APIException, DjangoValidationError):
231 raise
232 except Exception as e:
233 logger.error(f"Error in get_driverratings: {str(e)}")
234 return Response({"error": "Error retrieving driverratings", "error_code": ErrorCodes.INTERNAL_SERVER_ERROR}, status=500)
236 @action(
237 detail=True,
238 methods=['get'],
239 url_path='preferences',
240 permission_classes=[IsAuthenticated]
241 )
242 def get_preferences(self, request, *args, **kwargs):
243 """
244 Endpoint GET /users/{id}/preferences/
245 Devuelve todas las preferencias no borradas del usuario paginadas
246 """
247 user = self.get_object()
248 logger.info(f"Accessed get_preferences endpoint for user ID: {user.id}")
250 try:
251 return self.paginated_response(
252 request, UserService.list_preferences(user), PreferencesSerializer,
253 f"Returned preferences for user ID: {user.id}"
254 )
255 except (Http404, APIException, DjangoValidationError):
256 raise
257 except Exception as e:
258 logger.error(f"Error in get_preferences: {str(e)}")
259 return Response({"error": "Error retrieving preferences", "error_code": ErrorCodes.INTERNAL_SERVER_ERROR}, status=500)
261 @action(
262 detail=True,
263 methods=['patch'],
264 url_path='update-preferences',
265 permission_classes=[IsAuthenticated]
266 )
267 def update_preferences(self, request, *args, **kwargs):
268 """
269 Endpoint PATCH /users/{id}/update-preferences/
270 Se encarga de actualizar las preferencias del usuario
271 Recibe una lista de nombres de preferencias: {"preferences": ["MUSIC", "SMOKING"...]}
272 Devuelve un mensaje de exito o error
273 """
274 user = self.get_object()
275 # Se sacan los codigos de las preferencias para buscarlas en bbdd
276 pref_codes = request.data.get('preferences', [])
278 logger.info(f"Accessed update_preferences endpoint for user ID: {user.id} with preferences: {pref_codes}")
279 try:
280 UserService.update_preferences(user, pref_codes)
281 return Response({"message": "Preferencias actualizadas"}, status=200)
283 except (Http404, APIException, DjangoValidationError):
284 raise
285 except Exception as e:
286 logger.error(f"Error: {str(e)}")
287 return Response({"error": "Error al actualizar preferencias", "error_code": ErrorCodes.INTERNAL_SERVER_ERROR}, status=500)
289 @action(
290 detail=True,
291 methods=['delete'],
292 url_path='clear-preferences',
293 permission_classes=[IsAuthenticated]
294 )
295 def clear_preferences(self, request, *args, **kwargs):
296 """
297 Endpoint DELETE /users/{id}/clear-preferences/
298 Elimina todas las preferencias asociadas al usuario
299 Devuelve un mensaje de exito o error
300 """
301 user = self.get_object()
302 logger.info(f"Accessed clear_preferences endpoint for user ID: {user.id}")
303 UserService.clear_preferences(user)
304 return Response({"message": "All preferences cleared"}, status=status.HTTP_200_OK)
306 @action(
307 detail=True,
308 methods=['get'],
309 url_path='travel',
310 permission_classes=[IsAuthenticated]
311 )
312 def get_travels(self, request, *args, **kwargs):
313 """
314 Endpoint GET /users/{id}/travel/?type=pending|past
315 Devuelve los viajes creados por el usuario filtrando pasados o futuros segun el parametro type
316 """
317 user = self.get_object()
319 # Si no se especifica, se devuelven todos
320 travel_type = request.query_params.get('type', 'all')
322 logger.info(f"Accessed get_travels endpoint for user ID: {user.id} with param: {travel_type}")
324 try:
325 qs = UserService.list_created_travels(user, travel_type)
326 logger.info(f"Successfully retrieved travels for user ID: {user.id} with type: {travel_type}")
327 return self.paginated_response(
328 request, qs, TravelSerializer,
329 f"Returned {travel_type} travels for user ID: {user.id}"
330 )
332 except (Http404, APIException, DjangoValidationError):
333 raise
334 except Exception as e:
335 logger.error(f"Error in get_travels: {str(e)}")
336 return Response({"error": "Error retrieving travels", "error_code": ErrorCodes.INTERNAL_SERVER_ERROR}, status=500)
338 @action(
339 detail=True,
340 methods=['get'],
341 url_path='my-requests',
342 permission_classes=[IsAuthenticated]
343 )
344 def my_requests(self, request, *args, **kwargs):
345 """
346 Endpoint GET /users/{id}/my-requests/?type=active|past|pending
347 Devuelve los RequestTravels solicitados por el usuario, filtrando por el estado de la solicitud, futuro, pasado o pendiente de aceptar
348 """
349 user = self.get_object()
350 request_type = request.query_params.get('type', 'all')
352 logger.info(f"Accessed my_requests for user ID: {user.id} with type: {request_type}")
354 try:
355 qs = UserService.list_own_requests(user, request_type)
356 logger.info(f"Successfully retrieved {request_type} requests for user ID: {user.id}")
358 return self.paginated_response(
359 request, qs, RequestTravelsSerializer,
360 f"Returned {request_type} requests for user ID: {user.id}"
361 )
363 except (Http404, APIException, DjangoValidationError):
364 raise
365 except Exception as e:
366 logger.error(f"Error in my_requests: {str(e)}")
367 return Response({"error": "Error retrieving your requests", "error_code": ErrorCodes.INTERNAL_SERVER_ERROR}, status=500)
369 @action(
370 detail=True,
371 methods=['get'],
372 url_path='received-requests',
373 permission_classes=[IsAuthenticated]
374 )
375 def received_requests(self, request, *args, **kwargs):
376 """
377 Endpoint GET /users/{id}/received-requests/
378 Devuelve los RequestTravels pendientes de aprobar que ha recibido el usuario
379 """
380 user = self.get_object()
382 logger.info(f"Accessed received_requests for user ID: {user.id}")
384 try:
385 return self.paginated_response(
386 request, UserService.list_received_requests(user), RequestTravelsSerializer,
387 f"Returned pending received requests for user ID: {user.id}"
388 )
390 except (Http404, APIException, DjangoValidationError):
391 raise
392 except Exception as e:
393 logger.error(f"Error in received_requests: {str(e)}")
394 return Response({"error": "Error retrieving received requests", "error_code": ErrorCodes.INTERNAL_SERVER_ERROR}, status=500)
396 # Funcion para paginar las respuestas de los endpoints
397 def paginated_response(self, request, queryset, serializer_class, log_msg):
398 context = self.get_serializer_context()
400 page = self.paginate_queryset(queryset)
401 if page is not None: 401 ↛ 406line 401 didn't jump to line 406 because the condition on line 401 was always true
402 serializer = serializer_class(page, many=True, context=context)
403 logger.info(log_msg + f" (page {request.query_params.get('page', 1)})")
404 return self.get_paginated_response(serializer.data)
406 serializer = serializer_class(queryset, many=True, context=context)
407 return Response(serializer.data)
409 @action(
410 detail=False,
411 methods=['post'],
412 url_path='verification_email',
413 permission_classes=[],
414 throttle_scope='verification_email'
415 )
416 def get_verification_email(self, request, *args, **kwargs):
417 """
418 Endpoint POST /users/verification_email/
419 Envia un mensaje de confirmacion al email del usuario especificado
420 Si se envia además el email, envia el correo de verificacion a ese email,
421 pero solo si quien llama esta autenticado y es el dueño de la cuenta
422 El nombre de usuario es obligatorio
423 """
425 username = request.data.get("username")
426 email = request.data.get("email")
428 logger.info(f"Accessed get_verification_email endpoint for username: {username}")
430 # El nombre de usuario es obligatorio
431 if not username: 431 ↛ 432line 431 didn't jump to line 432 because the condition on line 431 was never true
432 return Response({"error": "username query param is required", "error_code": ErrorCodes.MISSING_REQUIRED_FIELD}, status=status.HTTP_400_BAD_REQUEST)
434 try:
435 # El usuario debe existir
436 user = UserService.find_by_username_or_email(username)
437 except UserNotFoundError:
438 return Response({"error": "User not found", "error_code": ErrorCodes.USER_DONT_EXIST}, status=status.HTTP_404_NOT_FOUND)
440 # El destino del codigo no lo elige quien llama.
441 # Se admite una direccion distinta a la de la cuenta unicamente cuando el que la pide esta autenticado
442 # y es el dueño de esa cuenta, que es el flujo de cambio de correo desde el perfil
443 if email and not (request.user.is_authenticated and request.user == user):
444 logger.warning(f"Rejected verification email for username {username}: the caller is not the owner of the account")
445 return Response({"error": "Authentication as the owner is required to choose the destination address", "error_code": ErrorCodes.INSUFICIENT_CREDENTIALS}, status=status.HTTP_403_FORBIDDEN)
447 if email:
448 try:
449 AuthService.assert_email_domain_is_allowed(email)
450 except EmailDomainNotAllowedError:
451 return Response({"error": EMAIL_DOMAIN_ERROR_MESSAGE, "error_code": ErrorCodes.EMAIL_DOMAIN_NOT_ALLOWED}, status=status.HTTP_400_BAD_REQUEST)
453 try:
454 AuthService.send_verification_code(user, to_email=email)
455 return Response({"status": "OK"}, status=status.HTTP_200_OK)
457 except EmailDeliveryError:
458 return Response({"error": "Error sending verification email", "error_code": ErrorCodes.EMAIL_ERROR}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
459 except (Http404, APIException, DjangoValidationError):
460 raise
461 except Exception as e:
462 logger.error(f"Error in get_verification_email: {str(e)}")
463 return Response({"error": "Error sending verification email", "error_code": ErrorCodes.EMAIL_ERROR}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
465 @action(
466 detail=False,
467 methods=['post'],
468 url_path='verify_code',
469 permission_classes=[],
470 throttle_scope='verify_code'
471 )
472 def post_verify_code(self, request, *args, **kwargs):
473 """
474 Endpoint POST /users/verify_code/
475 Verifica el token del usuario
476 Si se pasa el email, se cambia el correo del usuario a ese (si el token es correcto)
477 Si se pasa la contraseña, se cambia la contraseña del usuario a esa (si el token es correcto)
478 Si se pasa el parametro validate con valor true, se marca el usuario como verificado (si el token es correcto)
479 El nombre de usuario y token son obligatorios
480 """
481 username = request.data.get("username")
482 token = request.data.get("token")
483 email = request.data.get("email")
484 password = request.data.get("password")
485 validate = request.data.get("validate", False)
487 logger.info(f"Accessed post_verify_code endpoint for username: {username}")
489 # El nombre de usuario y el token son obligatorios
490 if not username or not token:
491 return Response({"error": "username and token query param are required", "error_code": ErrorCodes.MISSING_REQUIRED_FIELD}, status=status.HTTP_400_BAD_REQUEST)
493 try: # Si el usuario no existe, se devuelve un error
494 user = UserService.find_by_username_or_email(username)
495 except UserNotFoundError:
496 return Response({"error": "User not found", "error_code": ErrorCodes.USER_DONT_EXIST}, status=status.HTTP_404_NOT_FOUND)
498 # Cambiar el correo de la cuenta solo puede hacerlo su dueño autenticado
499 if email and not (request.user.is_authenticated and request.user == user):
500 logger.warning(f"Rejected email change for username {username}: the caller is not the owner of the account")
501 return Response({"error": "Authentication as the owner is required to change the email", "error_code": ErrorCodes.INSUFICIENT_CREDENTIALS}, status=status.HTTP_403_FORBIDDEN)
502 was_unverified = not user.is_verify
504 try:
505 user = AuthService.consume_verification_code(
506 user, token, email=email, password=password, validate=validate
507 )
509 if validate and was_unverified and not email and not password:
510 logger.info(f"Registration completed, issuing session for user ID: {user.id}")
511 return Response(
512 {"status": "OK", **AuthService.build_auth_payload(user)},
513 status=status.HTTP_200_OK
514 )
516 return Response({"status": "OK"}, status=status.HTTP_200_OK)
518 except TokenExpiredError:
519 return Response({"error": "Token expired", "error_code": ErrorCodes.TOKEN_EXPIRED}, status=status.HTTP_400_BAD_REQUEST)
520 except InvalidTokenError:
521 return Response({"error": "Invalid token", "error_code": ErrorCodes.INCORRECT_TOKEN}, status=status.HTTP_400_BAD_REQUEST)
522 except EmailDomainNotAllowedError:
523 return Response({"error": EMAIL_DOMAIN_ERROR_MESSAGE, "error_code": ErrorCodes.EMAIL_DOMAIN_NOT_ALLOWED}, status=status.HTTP_400_BAD_REQUEST)
524 except EmailAlreadyRegisteredError:
525 # El correo nuevo ya lo tiene otra cuenta activa (ver §2.10)
526 return Response({"error": "Email already in use", "error_code": ErrorCodes.EMAIL_ALREADY_EXISTS}, status=status.HTTP_409_CONFLICT)
527 except (Http404, APIException, DjangoValidationError):
528 raise
529 except Exception as e:
530 logger.error(f"Error in post_verify_code: {str(e)}")
531 return Response({"error": "Error changing user data", "error_code": ErrorCodes.INTERNAL_SERVER_ERROR}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
533 @action(
534 detail=False,
535 methods=['get'],
536 url_path='exist_email',
537 permission_classes=[IsAuthenticated],
538 throttle_scope='user_lookup'
539 )
540 def get_exist_email(self, request, *args, **kwargs):
541 """
542 Endpoint GET /users/exist_email/
543 Verifica si un email ya esta en uso
544 Devuelve OK si el email no esta en uso o KO en caso contrario
545 """
547 email = request.query_params.get("email")
549 logger.info(f"Accessed get_exist_email endpoint for email: {email}")
550 if not email:
551 return Response({"error": "email query param is required", "error_code": ErrorCodes.MISSING_REQUIRED_FIELD}, status=status.HTTP_400_BAD_REQUEST)
553 if AuthService.email_is_taken(email):
554 logger.info(f"The email {email} is in use")
555 return Response({"status": "KO"}, status=status.HTTP_200_OK)
557 logger.info(f"The email {email} is not in use")
558 return Response({"status": "OK"}, status=status.HTTP_200_OK)
561 @action(
562 detail=False,
563 methods=['get'],
564 url_path='verify_user',
565 permission_classes=[],
566 throttle_scope='user_lookup'
567 )
568 def get_verify_user(self, request, *args, **kwargs):
569 """
570 Endpoint GET /users/verify_user/
571 Verifica si un usuario (nombre de usuario o email) ya esta en uso
572 Devuelve OK ambos estan libres o los campos en uso en caso contrario
573 """
575 email = request.query_params.get("email")
576 username = request.query_params.get("username")
578 logger.info(f"Accessed get_verify_user endpoint for email: {email}, username: {username}")
579 if not email or not username: 579 ↛ 580line 579 didn't jump to line 580 because the condition on line 579 was never true
580 return Response({"error": "Both email and username query params are required", "status": "KO", "error_code": ErrorCodes.MISSING_REQUIRED_FIELD}, status=status.HTTP_400_BAD_REQUEST)
582 email_taken = AuthService.email_is_taken(email)
583 user_taken = AuthService.username_is_taken(username)
585 if email_taken or user_taken:
586 logger.info(f"Email taken: {email_taken} or username taken: {user_taken}")
587 return Response({
588 "status": "KO",
589 "email_taken": email_taken,
590 "username_taken": user_taken
591 }, status=status.HTTP_200_OK)
593 logger.info(f"Both {email} and {username} are available")
594 return Response({"status": "OK"}, status=status.HTTP_200_OK)
597 @action(
598 detail=True,
599 methods=['post'],
600 url_path='rate_a_driver',
601 permission_classes=[IsAuthenticated]
602 )
603 def post_rate_a_driver(self, request, *args, **kwargs):
604 """
605 Endpoint POST /users/{id}/rate_a_driver/
606 Permite valorar a un conductor tras haber realizado un viaje con el
607 """
608 results = request.data.get('results')
609 request_travel = request.data.get('request_travel')
611 if not results or not request_travel:
612 logger.error("results and request_travel fields are required in the request body")
613 return Response({"error": "results and request_travel fields are required", "error_code": ErrorCodes.MISSING_REQUIRED_FIELD}, status=status.HTTP_400_BAD_REQUEST)
615 user = self.get_object()
617 try:
618 UserService.rate_driver(user, request_travel, results)
619 return Response({"status": "OK"}, status=status.HTTP_200_OK)
621 except RequestTravelNotValidatedError:
622 return Response({"error": "The specified request_travel is not found or not in the correct status", "error_code": ErrorCodes.TRAVEL_DONT_EXIST}, status=status.HTTP_400_BAD_REQUEST)
623 except RequestStatusMissingError:
624 return Response({"error": "Unvalidated status not found", "error_code": ErrorCodes.INTERNAL_SERVER_ERROR}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
625 except (Http404, APIException, DjangoValidationError):
626 raise
627 except Exception as e:
628 logger.error(f"Error saving driver ratings: {str(e)}")
629 return Response({"error": "Error saving driver ratings", "error_code": ErrorCodes.INTERNAL_SERVER_ERROR}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
632 @action(
633 detail=True,
634 methods=['post'],
635 url_path='logout',
636 permission_classes=[IsAuthenticated]
637 )
638 def post_logout(self, request, *args, **kwargs):
639 """
640 Endpoint POST /users/{id}/logout/
641 Cierra la sesion invalidando todos los tokens emitidos hasta ahora
642 Cierra todas las sesiones de la cuenta, no solo la del dispositivo que llama
643 """
644 user = self.get_object()
645 logger.info(f"Accessed logout endpoint for user ID: {user.id}")
647 AuthService.revoke_all_sessions(user)
649 return Response({"status": "OK"}, status=status.HTTP_200_OK)
651 @action(
652 detail=True,
653 methods=['post'],
654 url_path='change_password',
655 permission_classes=[IsAuthenticated]
656 )
657 def post_change_password(self, request, *args, **kwargs):
658 """
659 Endpoint POST /users/{id}/change_password/
660 Permite cambiar la contraseña de un usuario
661 Recibe la contraseña actual y la nueva contraseña
662 """
663 current_password = request.data.get('current_password')
664 new_password = request.data.get('new_password')
666 if not current_password or not new_password:
667 logger.error("current_password and new_password fields are required")
668 return Response({"error": "current_password and new_password fields are required", "error_code": ErrorCodes.MISSING_REQUIRED_FIELD}, status=status.HTTP_400_BAD_REQUEST)
670 user = self.get_object()
672 try:
673 UserService.change_password(user, current_password, new_password)
675 # Cambiar la contraseña revoca TODAS las sesiones de la cuenta
676 return Response(
677 {"status": "OK", **AuthService.build_auth_payload(user)},
678 status=status.HTTP_200_OK,
679 )
681 except IncorrectPasswordError:
682 return Response({"error": "Incorrect current password", "error_code": ErrorCodes.INVALID_CREDENTIALS}, status=status.HTTP_400_BAD_REQUEST)
684 @action(
685 detail=True,
686 methods=['post'],
687 url_path='two_factor/challenge',
688 permission_classes=[IsAuthenticated],
689 throttle_scope='two_factor'
690 )
691 def post_two_factor_challenge(self, request, *args, **kwargs):
692 """
693 Endpoint POST /users/{id}/two_factor/challenge/
694 Arranca el cambio del segundo factor y responde con lo que hay que
695 presentar para confirmarlo: la contraseña actual, o un codigo que se
696 acaba de enviar al correo de la cuenta si esta solo entra por Google
697 Devuelve {"method": "password"} o {"method": "email"}
698 """
699 user = self.get_object()
700 logger.info(f"Accessed two_factor challenge endpoint for user ID: {user.id}")
702 try:
703 return Response({"method": AuthService.start_second_factor_change(user)}, status=status.HTTP_200_OK)
705 except EmailDeliveryError:
706 return Response({"error": "Error sending verification email", "error_code": ErrorCodes.EMAIL_ERROR}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
708 @action(
709 detail=True,
710 methods=['post'],
711 url_path='two_factor',
712 permission_classes=[IsAuthenticated],
713 throttle_scope='two_factor'
714 )
715 def post_two_factor(self, request, *args, **kwargs):
716 """
717 Endpoint POST /users/{id}/two_factor/
718 Activa o desactiva la verificacion en dos pasos
719 Recibe enabled y la credencial que pidio el challenge: password o code
720 Devuelve el estado en el que ha quedado el ajuste
721 """
722 enabled = request.data.get('enabled')
724 if not isinstance(enabled, bool):
725 logger.error("enabled field is required and must be a boolean")
726 return Response({"error": "enabled field is required and must be a boolean", "error_code": ErrorCodes.MISSING_REQUIRED_FIELD}, status=status.HTTP_400_BAD_REQUEST)
728 user = self.get_object()
729 logger.info(f"Accessed two_factor endpoint for user ID: {user.id} with enabled: {enabled}")
731 try:
732 user = AuthService.set_second_factor(
733 user, enabled,
734 password=request.data.get('password'),
735 code=request.data.get('code'),
736 )
737 return Response(
738 {
739 "status": "OK",
740 "has_2FA": user.has_2FA,
741 **AuthService.build_auth_payload(user),
742 },
743 status=status.HTTP_200_OK,
744 )
746 except IncorrectPasswordError:
747 return Response({"error": "Incorrect current password", "error_code": ErrorCodes.INVALID_CREDENTIALS}, status=status.HTTP_400_BAD_REQUEST)
748 except TokenExpiredError:
749 return Response({"error": "Token expired", "error_code": ErrorCodes.TOKEN_EXPIRED}, status=status.HTTP_400_BAD_REQUEST)
750 except InvalidTokenError:
751 return Response({"error": "Invalid token", "error_code": ErrorCodes.INCORRECT_TOKEN}, status=status.HTTP_400_BAD_REQUEST)
753 @action(
754 detail=True,
755 methods=['get'],
756 url_path='next_travel',
757 permission_classes=[IsAuthenticated]
758 )
759 def get_next_travel(self, request, *args, **kwargs):
760 """
761 Endpoint GET /users/{id}/next_travel/
762 Devuelve los siguientes viajes activos del usuario, si no tiene devuelve un 404
763 """
764 user = self.get_object()
766 logger.info(f"Accessed get_next_travel endpoint for user ID: {user.id}")
768 try:
769 return Response({"results": UserService.get_next_travels(user)}, status=status.HTTP_200_OK)
771 except NoUpcomingTravelsError:
772 return Response({"error": "No upcoming travels found", "error_code": ErrorCodes.TRAVEL_DONT_EXIST}, status=status.HTTP_404_NOT_FOUND)
773 except (Http404, APIException, DjangoValidationError):
774 raise
775 except Exception as e:
776 logger.error(f"Error in get_next_travel: {str(e)}")
777 return Response({"error": "Error retrieving next travel", "error_code": ErrorCodes.INTERNAL_SERVER_ERROR}, status=500)