diff --git a/src/stores/vespaStore.js b/src/stores/vespaStore.js index f036759..761d862 100644 --- a/src/stores/vespaStore.js +++ b/src/stores/vespaStore.js @@ -419,7 +419,7 @@ export const useVespaStore = defineStore('vespaStore', { async authCheck() { this.loadingAuth = true; try { - const response = await ApiService.get("/auth-check"); + const response = await ApiService.get("/auth-check/"); const data = response.data; if (data.isAuthenticated && data.user) { this.user = data.user; diff --git a/vespadb/observations/migrations/0035_alter_observation_options_and_more.py b/vespadb/observations/migrations/0035_alter_observation_options_and_more.py new file mode 100644 index 0000000..4298ab8 --- /dev/null +++ b/vespadb/observations/migrations/0035_alter_observation_options_and_more.py @@ -0,0 +1,22 @@ +# Generated by Django 5.1.4 on 2025-02-10 19:37 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('observations', '0034_remove_observation_species'), + ] + + operations = [ + migrations.AlterModelOptions( + name='observation', + options={'ordering': ['id']}, + ), + migrations.AlterField( + model_name='observation', + name='observation_datetime', + field=models.DateTimeField(blank=True, help_text='Datetime when the observation was made', null=True), + ), + ] diff --git a/vespadb/observations/models.py b/vespadb/observations/models.py index 898a28e..ac7082e 100644 --- a/vespadb/observations/models.py +++ b/vespadb/observations/models.py @@ -252,7 +252,7 @@ class Observation(models.Model): observer_email = models.EmailField(blank=True, null=True, help_text="Email of the observer") observer_received_email = models.BooleanField(default=False, help_text="Flag indicating if observer received email") observer_name = models.CharField(max_length=255, blank=True, null=True, help_text="Name of the observer") - observation_datetime = models.DateTimeField(help_text="Datetime when the observation was made") + observation_datetime = models.DateTimeField(null=True, blank=True, help_text="Datetime when the observation was made") wn_cluster_id = models.IntegerField(blank=True, null=True, help_text="Cluster ID of the observation") admin_notes = models.TextField(blank=True, null=True, help_text="Admin notes for the observation") diff --git a/vespadb/observations/views.py b/vespadb/observations/views.py index 27f8ebc..f79e720 100644 --- a/vespadb/observations/views.py +++ b/vespadb/observations/views.py @@ -652,10 +652,10 @@ def process_data(self, data: list[dict[str, Any]]) -> tuple[list[dict[str, Any]] errors.append({"record": idx, "error": f"Observation with id {observation_id} not found"}) continue else: # New record - data_item['created_by'] = self.request.user + data_item['created_by'] = self.request.user.pk if self.request.user else None if 'created_datetime' not in data_item: data_item['created_datetime'] = current_time - data_item['modified_by'] = self.request.user + data_item['modified_by'] = self.request.user.pk if self.request.user else None data_item['modified_datetime'] = current_time if 'longitude' in data_item and 'latitude' in data_item: @@ -719,7 +719,7 @@ def clean_data(self, data_dict: dict[str, Any]) -> dict[str, Any]: except (ValueError, TypeError): logger.exception(f"Invalid datetime format for {field}: {data_dict[field]}") data_dict.pop(field, None) - elif isinstance(data_dict[field], datetime): + elif isinstance(data_dict[field], datetime.datetime): data_dict[field] = data_dict[field].isoformat() else: data_dict.pop(field, None) diff --git a/vespadb/users/urls.py b/vespadb/users/urls.py index a814c0f..0470a08 100644 --- a/vespadb/users/urls.py +++ b/vespadb/users/urls.py @@ -12,7 +12,7 @@ urlpatterns = [ path("", include(router.urls)), - path("auth-check", AuthCheck.as_view(), name="auth_check"), + path("auth-check/", AuthCheck.as_view(), name="auth_check"), path("login/", LoginView.as_view(), name="login"), path("logout/", LogoutView.as_view(), name="logout"), path("change-password/", ChangePasswordView.as_view(), name="change_password"), diff --git a/vespadb/users/views.py b/vespadb/users/views.py index 4748667..da33463 100644 --- a/vespadb/users/views.py +++ b/vespadb/users/views.py @@ -11,6 +11,7 @@ from rest_framework.response import Response from rest_framework.views import APIView from rest_framework.authtoken.models import Token +from django.middleware.csrf import get_token from drf_yasg.utils import swagger_auto_schema from drf_yasg import openapi @@ -87,40 +88,68 @@ class LoginView(APIView): @swagger_auto_schema( operation_summary="User Login", - operation_description="Authenticate a user by username and password, log them in and return a bearer token.", + operation_description="Authenticate a user with a username and password, log them in, and return a CSRF token.", request_body=LoginSerializer, responses={ 200: openapi.Response( description="Login successful", - examples={ - "application/json": { - "detail": "Login successful.", - "token": "0123456789abcdef0123456789abcdef01234567" - } - }, + schema=openapi.Schema( + type=openapi.TYPE_OBJECT, + properties={ + "detail": openapi.Schema( + type=openapi.TYPE_STRING, + example="Login successful." + ), + "csrftoken": openapi.Schema( + type=openapi.TYPE_STRING, + example="abc123csrf" + ) + }, + ), + ), + 400: openapi.Response( + description="Bad Request", + schema=openapi.Schema( + type=openapi.TYPE_OBJECT, + properties={ + "error": openapi.Schema( + type=openapi.TYPE_STRING, + example="Invalid username or password." + ) + }, + ), ), - 400: "Bad Request", }, ) def post(self, request: Request) -> Response: - """ - Authenticate a user based on username and password, log them in and return a bearer token. - """ - serializer = LoginSerializer(data=request.data) - if serializer.is_valid(): - user = serializer.validated_data - login(request, user) - # Create or retrieve the token for the user - token, _ = Token.objects.get_or_create(user=user) - return Response( - { - "detail": "Login successful.", - "token": token.key, - }, - status=status.HTTP_200_OK, - ) + """ + Authenticate a user based on username and password, log them in, and return a CSRF token. + """ + serializer = LoginSerializer(data=request.data) + + if not serializer.is_valid(): return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) - + + user = serializer.validated_data + login(request, user) + + # Generate CSRF token + csrf_token = get_token(request) + + # Prepare response + response_data = { + "detail": "Login successful.", + "csrftoken": csrf_token + } + + # Create the response and set CSRF token in both header and cookie + response = Response(response_data, status=status.HTTP_200_OK) + response.set_cookie( + "csrftoken", csrf_token, httponly=False, secure=True, samesite="Lax" + ) + response["X-CSRFToken"] = csrf_token # Set CSRF token in response headers + + return response class LogoutView(APIView): """API view for user logout."""