Skip to content

Commit

Permalink
reset password page (#29)
Browse files Browse the repository at this point in the history
  • Loading branch information
stevegerrits authored Mar 27, 2024
1 parent d584b8a commit 806e464
Show file tree
Hide file tree
Showing 12 changed files with 185 additions and 73 deletions.
22 changes: 22 additions & 0 deletions frontend/src/assets/style.css
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ html {
color: black;
}

#ChangePasswordMap,
#loginApp {
display: flex;
flex-direction: column;
Expand Down Expand Up @@ -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 {
Expand Down
56 changes: 56 additions & 0 deletions frontend/src/components/ChangePasswordPage.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
<template>
<div id="ChangePasswordMap">
<navbar-component></navbar-component>
<div class="modal-content" style="margin-top: 100px;">
<h2>Change Password</h2>
<input type="password" v-model="oldPassword" placeholder="Huidig wachtwoord" />
<input type="password" v-model="newPassword" placeholder="Nieuw wachtwoord" />
<input type="password" v-model="confirmNewPassword" placeholder="Bevestig nieuw wachtwoord" />
<button @click="changePassword">Wijzig Password</button>
<p v-if="error" class="error">{{ error }}</p>
<p v-if="successMessage" class="success">{{ successMessage }}</p>
</div>
<footer-component></footer-component>
</div>
</template>


<script>
import FooterComponent from '@/components/FooterComponent.vue';
import NavbarComponent from '@/components/NavbarComponent.vue';
import { useVespaStore } from '@/stores/vespaStore';
import { computed, ref } from 'vue';
export default {
components: {
NavbarComponent,
FooterComponent,
},
setup() {
const vespaStore = useVespaStore();
const oldPassword = ref('');
const newPassword = ref('');
const confirmNewPassword = ref('');
const successMessage = ref('');
const error = computed(() => {
return Array.isArray(vespaStore.error) ? vespaStore.error.join(', ') : vespaStore.error;
});
const changePassword = async () => {
const success = await vespaStore.changePassword(oldPassword.value, newPassword.value, confirmNewPassword.value);
if (success) {
successMessage.value = "Wachtwoord succesvol gewijzigd!";
}
};
return {
successMessage,
oldPassword,
newPassword,
error,
changePassword,
confirmNewPassword,
};
},
};
</script>
18 changes: 9 additions & 9 deletions frontend/src/components/LoginPage.vue
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,15 @@
<input type="text" v-model="username" placeholder="Gebruikersnaam" />
<input type="password" v-model="password" placeholder="Wachtwoord" />
<button @click="login">Login</button>
<p v-if="loginError" class="error">{{ loginError }}</p>
<p v-if="error" class="error">{{ error }}</p>
</div>
<footer-component></footer-component>
</div>
</template>

<script>
import { useVespaStore } from '@/stores/vespaStore';
import { ref } from 'vue';
import { computed, ref } from 'vue';
import { useRouter } from 'vue-router';
import FooterComponent from './FooterComponent.vue';
import NavbarComponent from './NavbarComponent.vue';
Expand All @@ -29,22 +29,22 @@ export default {
const vespaStore = useVespaStore();
const username = ref('');
const password = ref('');
const loginError = ref(null);
const error = computed(() => {
return Array.isArray(vespaStore.error) ? vespaStore.error.join(', ') : vespaStore.error;
});
const login = async () => {
try {
await vespaStore.login({ username: username.value, password: password.value });
await vespaStore.login({ username: username.value, password: password.value });
if (vespaStore.isLoggedIn) {
router.push('/map');
} catch (error) {
loginError.value = "Failed to login. Please check your username and password.";
}
};
return {
username,
password,
loginError,
login
login,
error
};
}
};
Expand Down
11 changes: 5 additions & 6 deletions frontend/src/components/NavbarComponent.vue
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
<span v-if="isLoggedIn">
Hallo, {{ username }}!
<a href="javascript:void(0);" @click="logout" class="button-style">Uitloggen</a>
<a href="javascript:void(0);" @click="navigateToChangePassword" class="button-style">Change Password</a>
</span>
<a v-else href="/login" class="button-style">Inloggen</a>
</div>
Expand All @@ -20,19 +21,17 @@ export default {
setup() {
const router = useRouter();
const vespaStore = useVespaStore();
// Correctly access the isLoggedIn state
const isLoggedIn = computed(() => vespaStore.isLoggedIn);
// Correctly access the username from the user object in the state
const username = computed(() => vespaStore.user.username);
const logout = async () => {
await vespaStore.logout();
router.push('/login');
};
return { isLoggedIn, username, logout };
const navigateToChangePassword = () => {
router.push({ name: 'ChangePassword' });
};
return { isLoggedIn, username, logout, navigateToChangePassword };
},
};
</script>
15 changes: 13 additions & 2 deletions frontend/src/router/index.js
Original file line number Diff line number Diff line change
@@ -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',
Expand All @@ -17,6 +18,11 @@ const routes = [
path: '/',
name: 'Home',
component: MapPage
},
{
path: '/change-password',
name: 'ChangePassword',
component: ChangePasswordPage
}
];

Expand All @@ -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;
41 changes: 38 additions & 3 deletions frontend/src/stores/vespaStore.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import { defineStore } from 'pinia';
export const useVespaStore = defineStore('vespaStore', {
state: () => ({
isLoggedIn: false,
username: '',
userId: null,
loading: false,
error: null,
Expand Down Expand Up @@ -142,6 +141,7 @@ export const useVespaStore = defineStore('vespaStore', {
password: password
})
.then(() => {
this.isLoggedIn = true;
this.authCheck();
})
.catch((error) => {
Expand Down Expand Up @@ -190,15 +190,50 @@ 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);
this.error = error.response.data.error;
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;
}
},
},
});
7 changes: 7 additions & 0 deletions vespadb/app_auth/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
4 changes: 3 additions & 1 deletion vespadb/app_auth/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -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/<str:session_key>/", expire_session_view, name="expire_session"),
]
31 changes: 29 additions & 2 deletions vespadb/app_auth/views.py
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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):
Expand Down Expand Up @@ -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.
Expand Down
3 changes: 2 additions & 1 deletion vespadb/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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

Expand Down
4 changes: 0 additions & 4 deletions vespadb/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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")),
Expand Down
Loading

0 comments on commit 806e464

Please sign in to comment.