diff --git a/frontend/src/assets/style.css b/frontend/src/assets/style.css
index 34718a3..10b2e57 100644
--- a/frontend/src/assets/style.css
+++ b/frontend/src/assets/style.css
@@ -10,6 +10,7 @@ html {
color: black;
}
+#ChangePasswordMap,
#loginApp {
display: flex;
flex-direction: column;
@@ -256,6 +257,27 @@ footer {
padding: 10px;
}
+.error {
+ color: #D8000C;
+ background-color: #FFD2D2;
+ padding: 10px;
+ margin-top: 10px;
+ margin-bottom: 10px;
+ border: 1px solid #D8000C;
+ border-radius: 5px;
+ text-align: center;
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
+}
+
+.success {
+ color: #155724;
+ background-color: #d4edda;
+ border-color: #c3e6cb;
+ padding: 0.75rem 1.25rem;
+ margin-top: 1rem;
+ border: 1px solid transparent;
+ border-radius: 0.25rem;
+}
@media (max-width: 768px) {
#filters {
diff --git a/frontend/src/components/ChangePasswordPage.vue b/frontend/src/components/ChangePasswordPage.vue
new file mode 100644
index 0000000..c61cdda
--- /dev/null
+++ b/frontend/src/components/ChangePasswordPage.vue
@@ -0,0 +1,56 @@
+
+
+
+
+
Change Password
+
+
+
+
+
{{ error }}
+
{{ successMessage }}
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/frontend/src/components/LoginPage.vue b/frontend/src/components/LoginPage.vue
index f7cba90..f0243e0 100644
--- a/frontend/src/components/LoginPage.vue
+++ b/frontend/src/components/LoginPage.vue
@@ -6,7 +6,7 @@
-
{{ loginError }}
+ {{ error }}
@@ -14,7 +14,7 @@
diff --git a/frontend/src/router/index.js b/frontend/src/router/index.js
index c40fd58..9b6764b 100644
--- a/frontend/src/router/index.js
+++ b/frontend/src/router/index.js
@@ -1,7 +1,8 @@
+import { useVespaStore } from '@/stores/vespaStore';
import { createRouter, createWebHistory } from 'vue-router';
+import ChangePasswordPage from '../components/ChangePasswordPage.vue';
import Login from '../components/LoginPage.vue';
import MapPage from '../components/MapPage.vue';
-
const routes = [
{
path: '/login',
@@ -17,6 +18,11 @@ const routes = [
path: '/',
name: 'Home',
component: MapPage
+ },
+ {
+ path: '/change-password',
+ name: 'ChangePassword',
+ component: ChangePasswordPage
}
];
@@ -26,6 +32,11 @@ const router = createRouter({
});
router.beforeEach(async (to, from, next) => {
- next();
+ const vespaStore = useVespaStore();
+ if (to.meta.requiresAuth && !vespaStore.isLoggedIn) {
+ next({ name: 'Login' });
+ } else {
+ next();
+ }
});
export default router;
diff --git a/frontend/src/stores/vespaStore.js b/frontend/src/stores/vespaStore.js
index 0c976a8..64b9ce8 100644
--- a/frontend/src/stores/vespaStore.js
+++ b/frontend/src/stores/vespaStore.js
@@ -7,7 +7,6 @@ import { defineStore } from 'pinia';
export const useVespaStore = defineStore('vespaStore', {
state: () => ({
isLoggedIn: false,
- username: '',
userId: null,
loading: false,
error: null,
@@ -142,6 +141,7 @@ export const useVespaStore = defineStore('vespaStore', {
password: password
})
.then(() => {
+ this.isLoggedIn = true;
this.authCheck();
})
.catch((error) => {
@@ -190,9 +190,13 @@ export const useVespaStore = defineStore('vespaStore', {
async logout() {
this.loading = true;
await ApiService
- .get("/api-auth/logout/")
+ .post("/app_auth/logout/")
.then(() => {
- this.authCheck();
+ this.isLoggedIn = false;
+ this.user = {};
+ this.loading = false;
+ console.log('Logged out successfully');
+ this.router.push({ name: 'map' });
})
.catch((error) => {
console.error(error.response.data);
@@ -200,5 +204,36 @@ export const useVespaStore = defineStore('vespaStore', {
this.loading = false;
});
},
+ async changePassword(oldPassword, newPassword, confirmPassword) {
+ this.loading = true;
+ if (!oldPassword || !newPassword) {
+ this.error = "Vul aub alle velden in.";
+ this.loading = false;
+ return false;
+ }
+ if (newPassword !== confirmPassword) {
+ this.error = "De wachtwoorden komen niet overeen.";
+ this.loading = false;
+ return false;
+ }
+ try {
+ await ApiService.post("/app_auth/change-password/", {
+ old_password: oldPassword,
+ new_password: newPassword,
+ });
+ this.loading = false;
+ this.error = null;
+ return true;
+ } catch (error) {
+ this.loading = false;
+ if (error.response && error.response.data) {
+ const backendMessages = error.response.data;
+ this.error = backendMessages.detail || "Een onverwachte fout is opgetreden.";
+ } else {
+ this.error = error.message || "Een onverwachte fout is opgetreden.";
+ }
+ return false;
+ }
+ },
},
});
diff --git a/vespadb/app_auth/serializers.py b/vespadb/app_auth/serializers.py
index 5b1622b..9508fc2 100644
--- a/vespadb/app_auth/serializers.py
+++ b/vespadb/app_auth/serializers.py
@@ -62,3 +62,10 @@ def validate(self, data: dict[Any, Any]) -> User:
if user and user.check_password(data["password"]):
return user
raise ValidationError({"error": "Invalid username or password."})
+
+
+class ChangePasswordSerializer(serializers.Serializer):
+ """Validate user input for changing password."""
+
+ old_password = serializers.CharField(required=True)
+ new_password = serializers.CharField(required=True)
diff --git a/vespadb/app_auth/urls.py b/vespadb/app_auth/urls.py
index 6bdf6bc..458d9ba 100644
--- a/vespadb/app_auth/urls.py
+++ b/vespadb/app_auth/urls.py
@@ -2,10 +2,12 @@
from django.urls import path
-from vespadb.app_auth.views import AuthCheck, LoginView, expire_session_view
+from vespadb.app_auth.views import AuthCheck, ChangePasswordView, LoginView, LogoutView, expire_session_view
urlpatterns = [
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"), # New change password endpoint
path("expire-session//", expire_session_view, name="expire_session"),
]
diff --git a/vespadb/app_auth/views.py b/vespadb/app_auth/views.py
index 4033946..998a1de 100644
--- a/vespadb/app_auth/views.py
+++ b/vespadb/app_auth/views.py
@@ -1,8 +1,9 @@
"""views for the app_auth app."""
from collections.abc import Sequence
+from typing import Any
-from django.contrib.auth import login
+from django.contrib.auth import login, logout, update_session_auth_hash
from django.contrib.sessions.models import Session
from django.http import HttpResponseRedirect
from rest_framework import permissions, status
@@ -12,7 +13,7 @@
from rest_framework.response import Response
from rest_framework.views import APIView
-from vespadb.app_auth.serializers import LoginSerializer, UserSerializer
+from vespadb.app_auth.serializers import ChangePasswordSerializer, LoginSerializer, UserSerializer
class AuthCheck(APIView):
@@ -64,6 +65,32 @@ def post(self, request: Request) -> Response:
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
+class LogoutView(APIView):
+ """API view for user logout."""
+
+ def post(self, request: Request) -> Response:
+ """Logout a user."""
+ logout(request)
+ return Response({"detail": "Successfully logged out."}, status=status.HTTP_200_OK)
+
+
+class ChangePasswordView(APIView):
+ """Change password view."""
+
+ def post(self, request: Request, *args: Any, **kwargs: Any) -> Response:
+ """."""
+ serializer = ChangePasswordSerializer(data=request.data)
+ if serializer.is_valid():
+ user = request.user
+ if not user.check_password(serializer.validated_data["old_password"]):
+ return Response({"old_password": ["Wrong password."]}, status=status.HTTP_400_BAD_REQUEST)
+ user.set_password(serializer.validated_data["new_password"])
+ user.save()
+ update_session_auth_hash(request, user) # Important to keep the session active
+ return Response({"detail": "Password changed successfully."}, status=status.HTTP_200_OK)
+ return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
+
+
def expire_session_view(request: Request, session_key: str) -> HttpResponseRedirect:
"""
View to expire a specific session.
diff --git a/vespadb/settings.py b/vespadb/settings.py
index 16d6df8..c5f0c7d 100644
--- a/vespadb/settings.py
+++ b/vespadb/settings.py
@@ -57,6 +57,7 @@
"django.middleware.common.CommonMiddleware",
"django.middleware.security.SecurityMiddleware",
"django.contrib.sessions.middleware.SessionMiddleware",
+ "django.middleware.csrf.CsrfViewMiddleware",
"django.contrib.auth.middleware.AuthenticationMiddleware",
"django.contrib.messages.middleware.MessageMiddleware",
"django.middleware.clickjacking.XFrameOptionsMiddleware",
@@ -75,7 +76,7 @@
],
"DEFAULT_PERMISSION_CLASSES": ("rest_framework.permissions.IsAuthenticated",),
}
-CSRF_COOKIE_NAME = "csrftoken"
+
SESSION_COOKIE_AGE = 60 * 60
SESSION_SAVE_EVERY_REQUEST = True
diff --git a/vespadb/urls.py b/vespadb/urls.py
index 30b8553..4f86c29 100644
--- a/vespadb/urls.py
+++ b/vespadb/urls.py
@@ -8,8 +8,6 @@
from drf_yasg.views import get_schema_view
from rest_framework import permissions
-from vespadb.users.views import UserStatusView
-
schema_view = get_schema_view(
openapi.Info(
title="Vespawatch API Documentation",
@@ -28,9 +26,7 @@
path("redoc/", schema_view.with_ui("redoc", cache_timeout=0), name="schema-redoc"),
path("admin/", admin.site.urls),
# User views
- path("user_status/", UserStatusView.as_view(), name="user_status"),
path("app_auth/", include("vespadb.app_auth.urls")),
- path("api-auth/", include("rest_framework.urls", namespace="rest_framework")),
path("admin/", admin.site.urls),
# Include the observations app URLs
path("", include("vespadb.observations.urls", namespace="observations")),
diff --git a/vespadb/users/views.py b/vespadb/users/views.py
index 3deab1c..23edfa2 100644
--- a/vespadb/users/views.py
+++ b/vespadb/users/views.py
@@ -1,14 +1,8 @@
"""Views for the users app."""
from django.contrib.auth.models import User
-from django.contrib.auth.password_validation import validate_password
-from django.core.exceptions import ValidationError as DjangoValidationError
-from rest_framework import serializers, status, viewsets
-from rest_framework.decorators import action
+from rest_framework import serializers, viewsets
from rest_framework.permissions import AllowAny, BasePermission, IsAdminUser
-from rest_framework.request import Request
-from rest_framework.response import Response
-from rest_framework.views import APIView
from vespadb.permissions import IsAdminOrSelf
from vespadb.users.serializers import UserSerializer
@@ -34,41 +28,3 @@ def get_permissions(self) -> list[BasePermission]:
else:
permission_classes = [AllowAny]
return [permission() for permission in permission_classes]
-
- @action(detail=True, methods=["post"])
- def change_password(self, request: Request, pk: int | None = None) -> Response:
- """Change the password of the user."""
- user: User = self.get_object()
- if user != request.user:
- return Response({"message": "Unauthorized"}, status=status.HTTP_403_FORBIDDEN)
-
- old_password = request.data.get("old_password")
- new_password = request.data.get("new_password")
- confirm_new_password = request.data.get("confirm_new_password")
-
- if not user.check_password(old_password):
- return Response({"message": "Old password is incorrect."}, status=status.HTTP_400_BAD_REQUEST)
-
- if new_password != confirm_new_password:
- return Response({"message": "New passwords do not match."}, status=status.HTTP_400_BAD_REQUEST)
-
- try:
- validate_password(new_password, user)
- user.set_password(new_password)
- user.save()
- return Response({"message": "Password changed successfully."})
- except DjangoValidationError as e:
- return Response({"error": list(e.messages)}, status=status.HTTP_400_BAD_REQUEST)
-
-
-class UserStatusView(APIView):
- """View to check if a user is logged in."""
-
- permission_classes = [AllowAny]
-
- def get(self, request: Request) -> Response:
- """Return user status."""
- user: User = request.user
- if user.is_authenticated:
- return Response({"is_logged_in": True, "username": user.username, "user_id": user.id})
- return Response({"is_logged_in": False}, status=status.HTTP_401_UNAUTHORIZED)