diff --git a/.installation_state/django_migrations.done b/.installation_state/django_migrations.done new file mode 100644 index 00000000..3a907cb4 --- /dev/null +++ b/.installation_state/django_migrations.done @@ -0,0 +1 @@ +2026-06-07T07:47:07Z diff --git a/.installation_state/env_file.done b/.installation_state/env_file.done new file mode 100644 index 00000000..7812f049 --- /dev/null +++ b/.installation_state/env_file.done @@ -0,0 +1 @@ +2026-06-07T07:36:50Z diff --git a/.installation_state/superuser.done b/.installation_state/superuser.done new file mode 100644 index 00000000..e04b301c --- /dev/null +++ b/.installation_state/superuser.done @@ -0,0 +1 @@ +2026-06-07T07:49:33Z diff --git a/nrm_app/settings.py b/nrm_app/settings.py index f08fe9d6..dcb980f3 100755 --- a/nrm_app/settings.py +++ b/nrm_app/settings.py @@ -419,7 +419,7 @@ def resolve_env_path(name, default="", *, trailing_sep=False): FERNET_KEY = env("FERNET_KEY") API_KEY = env("API_KEY", default="") - +RECAPTCHA_SECRET_KEY = env("RECAPTCHA_SECRET_KEY", default="") lulc_years = [ "2017_2018", diff --git a/users/views.py b/users/views.py index 0949e77e..3f001e9c 100644 --- a/users/views.py +++ b/users/views.py @@ -1,4 +1,6 @@ import logging +import token +import requests from django.conf import settings from django.contrib.auth.models import Group @@ -33,6 +35,45 @@ logger = logging.getLogger(__name__) +def verify_recaptcha(token): + try: + response = requests.post( + "https://www.google.com/recaptcha/api/siteverify", + data={ + "secret": settings.RECAPTCHA_SECRET_KEY, + "response": token, + }, + timeout=10, + ) + + response.raise_for_status() + + result = response.json() + success = result.get("success", False) + + if not success: + logger.warning( + "reCAPTCHA verification failed. Response: %s", + result, + ) + + return success + except requests.exceptions.Timeout: + logger.error("reCAPTCHA request timed out") + return False + except requests.exceptions.RequestException as exc: + logger.exception( + "Error while verifying reCAPTCHA: %s", + str(exc), + ) + return False + except Exception as exc: + logger.exception( + "Unexpected error during reCAPTCHA verification: %s", + str(exc), + ) + return False + class RegisterView(viewsets.GenericViewSet, generics.CreateAPIView): """API endpoint for user registration.""" @@ -95,6 +136,8 @@ def post(self, request, *args, **kwargs): }, status=status.HTTP_201_CREATED, ) + + class LoginView(TokenObtainPairView): @@ -107,16 +150,51 @@ class LoginView(TokenObtainPairView): def post(self, request, *args, **kwargs): # Call parent class method to validate credentials and get tokens - response = super().post(request, *args, **kwargs) - - token = response.data.get("access") - jwt_auth = JWTAuthentication() - validated_token = jwt_auth.get_validated_token(token) - user = jwt_auth.get_user(validated_token) - - response.data["user"] = UserSerializer(user).data + try: + captcha_token = request.data.get("captcha") + if not captcha_token: + logger.warning( + "Login attempt without captcha. Username: %s", + request.data.get("username"), + ) + return Response( + {"message": "Captcha is required"}, + status=status.HTTP_400_BAD_REQUEST, + ) + if not verify_recaptcha(captcha_token): + logger.warning( + "Invalid captcha during login. Username: %s", + request.data.get("username"), + ) + return Response( + {"message": "Invalid captcha"}, + status=status.HTTP_400_BAD_REQUEST, + ) + + response = super().post(request, *args, **kwargs) + + token = response.data.get("access") + jwt_auth = JWTAuthentication() + validated_token = jwt_auth.get_validated_token(token) + user = jwt_auth.get_user(validated_token) + logger.info( + "User logged in successfully. User ID: %s Username: %s", + user.id, + user.username, + ) + response.data["user"] = UserSerializer(user).data + return response + except Exception as exc: + logger.exception( + "Unexpected error during login: %s", + str(exc), + ) - return response + return Response( + {"message": "An error occurred during login"}, + status=status.HTTP_500_INTERNAL_SERVER_ERROR, + ) + class LogoutView(generics.GenericAPIView):