diff --git a/TEKDB/TEKDB/settings.py b/TEKDB/TEKDB/settings.py index 1706339b..91f8ac9f 100644 --- a/TEKDB/TEKDB/settings.py +++ b/TEKDB/TEKDB/settings.py @@ -262,7 +262,7 @@ TINYMCE_FILEBROWSER = False # Add Version to the admin site header -VERSION = "2.10.0" +VERSION = "2.11.0" ADMIN_SITE_HEADER = os.environ.get( "ADMIN_SITE_HEADER", default="ITK DB Admin v{}".format(VERSION) ) diff --git a/TEKDB/TEKDB/urls.py b/TEKDB/TEKDB/urls.py index eb694a67..ecf23991 100644 --- a/TEKDB/TEKDB/urls.py +++ b/TEKDB/TEKDB/urls.py @@ -35,7 +35,11 @@ urlpatterns = [ - # url(r'^login/', include('login.urls')), + path( + "change_password/", + login_views.TEKDBPasswordChangeView.as_view(), + name="change_password", + ), path("admin/filebrowser/", tekdb_filebrowser.urls), path("login/", login_views.login, name="login"), path("login_async/", login_views.login_async, name="login_async"), diff --git a/TEKDB/explore/static/explore/css/forms.css b/TEKDB/explore/static/explore/css/forms.css index f0ec63bc..0e03731a 100644 --- a/TEKDB/explore/static/explore/css/forms.css +++ b/TEKDB/explore/static/explore/css/forms.css @@ -2,10 +2,6 @@ text-align: center; } -.registration-form .helptext { - visibility: hidden; -} - input[type="text"], .form-control, input[type="password"] { diff --git a/TEKDB/explore/static/explore/js/modals.js b/TEKDB/explore/static/explore/js/modals.js index 6e46d2b2..9b8bc581 100644 --- a/TEKDB/explore/static/explore/js/modals.js +++ b/TEKDB/explore/static/explore/js/modals.js @@ -1,21 +1,93 @@ -$("#loginModal").on("shown.bs.modal", function () { - $("#loginInput").focus(); - var loginForm = document.querySelector("#loginModal form"); - const LOGIN_ERROR_MESSAGE = "Invalid username or password"; - loginForm.addEventListener("submit", function (event) { - event.preventDefault(); - var signIn = account.signIn(event, this, function (success) { - if (success) { - $("#loginModal").modal("hide"); - const exploreLink = document.querySelector(".nav-explore"); - exploreLink.classList.remove("disabled"); - exploreLink.href = "/explore"; - exploreLink.click(); - } else { - if (!loginForm.innerHTML.includes(LOGIN_ERROR_MESSAGE)) { - loginForm.append(LOGIN_ERROR_MESSAGE); +const LOGIN_MODAL_ID = "loginModal"; +const LOGIN_INPUT_ID = "loginInput"; +const loginForm = document.querySelector(`#${LOGIN_MODAL_ID} form`); +const LOGIN_ERROR_MESSAGE = "Invalid username or password"; + +$(`#${LOGIN_MODAL_ID}`).on("shown.bs.modal", function () { + $(`#${LOGIN_INPUT_ID}`).focus(); +}); + +loginForm.addEventListener("submit", function (event) { + event.preventDefault(); + account.signIn(event, this, function (success) { + if (success) { + $(`#${LOGIN_MODAL_ID}`).modal("hide"); + const exploreLink = document.querySelector(".nav-explore"); + exploreLink.classList.remove("disabled"); + exploreLink.href = "/explore"; + exploreLink.click(); + } else { + if (!loginForm.innerHTML.includes(LOGIN_ERROR_MESSAGE)) { + loginForm.append(LOGIN_ERROR_MESSAGE); + } + } + }); +}); + +const CHANGE_PASSWORD_MODAL_ID = "changePasswordModal"; +const CHANGE_PASSWORD_SUCCESS_ID = "changePasswordSuccessMessage"; +const CHANGE_PASSWORD_SUCCESS_MESSAGE = "Password successfully changed!"; +const changePasswordForm = document.querySelector(`#${CHANGE_PASSWORD_MODAL_ID} form`); + +changePasswordForm.addEventListener("submit", function (event) { + event.preventDefault(); + account.changePassword(event, this, function (response) { + if (response.success) { + // clear any previous error messages + const errorLists = document.querySelectorAll(`#${CHANGE_PASSWORD_MODAL_ID} ul`); + if (errorLists.length) { + errorLists.forEach((list) => list.remove()); + } + // clear any previous success message + const previousSuccessMessage = document.querySelector(`#${CHANGE_PASSWORD_SUCCESS_ID}`); + if (!previousSuccessMessage) { + // display success message + const successMessage = document.createElement("p"); + successMessage.setAttribute("id", `${CHANGE_PASSWORD_SUCCESS_ID}`); + successMessage.textContent = CHANGE_PASSWORD_SUCCESS_MESSAGE; + successMessage.setAttribute("style", "color: green;"); + successMessage.setAttribute("class", "m-0"); + + // insert success message after submit button + const submitButton = changePasswordForm.querySelector('#changePasswordSubmit'); + submitButton.after(successMessage); + } + + // reset form + changePasswordForm.reset(); + } else { + // display error messages + for (const elementId in response.data.data) { + const errorList = document.createElement("ul"); + const erroredInput = document.querySelector(`#${elementId}`); + erroredInput.after(errorList); + erroredInput.setAttribute("style", "border-color: red;"); + if (response.data.data[elementId].length > 0) { + for (const error in response.data.data[elementId]) { + const listItem = document.createElement("li"); + listItem.textContent = response.data.data[elementId][error]; + errorList.appendChild(listItem); + } + } } - }); + } }); }); + +$(`#${CHANGE_PASSWORD_MODAL_ID}`).on("hidden.bs.modal", function () { + // clear error messages + const errorLists = document.querySelectorAll(`#${CHANGE_PASSWORD_MODAL_ID} ul`); + errorLists.forEach((list) => list.remove()); + // reset input border color + const passwordInputs = document.querySelectorAll(`#${CHANGE_PASSWORD_MODAL_ID} input[type='password']`); + passwordInputs.forEach((input) => input.setAttribute("style", "")); + // clear form + document.querySelector(`#${CHANGE_PASSWORD_MODAL_ID} form`).reset(); + // clear success message + const successMessage = document.querySelector(`#${CHANGE_PASSWORD_SUCCESS_ID}`); + + if (successMessage) { + successMessage.remove(); + } +}); \ No newline at end of file diff --git a/TEKDB/explore/templates/modals.html b/TEKDB/explore/templates/modals.html index a408544e..fc0ffd58 100644 --- a/TEKDB/explore/templates/modals.html +++ b/TEKDB/explore/templates/modals.html @@ -15,35 +15,32 @@ class="btn-close" data-bs-dismiss="modal" aria-label="Close" - > + > -{% comment %} - -{% endcomment %} diff --git a/TEKDB/explore/templates/navbar.html b/TEKDB/explore/templates/navbar.html index 4e58c8c5..292486b2 100644 --- a/TEKDB/explore/templates/navbar.html +++ b/TEKDB/explore/templates/navbar.html @@ -37,19 +37,20 @@ {% if user.is_authenticated %} {% else %} diff --git a/TEKDB/login/static/login/js/account.js b/TEKDB/login/static/login/js/account.js index c18253fb..c5d1802c 100644 --- a/TEKDB/login/static/login/js/account.js +++ b/TEKDB/login/static/login/js/account.js @@ -51,5 +51,28 @@ var account = { callback(false); } }); + }, + changePassword: function(event, form, callback) { + var formData = $(form).serialize(); + var url = '/change_password/'; + $.ajax({ + url: url, + type: 'POST', + data: formData, + dataType: 'json', + success: function(response) { + if (response.success) { + console.log('%csuccessfully changed password', 'color:green;'); + callback({success: true, data: response.data}); + } else { + console.log('%cerror changing password: %o', 'color: red;', response.data); + callback({success: false, data: response.data}); + } + }, + error: function(response) { + console.log('%cerror with change password request submission: %o', 'color: red', response.data); + callback({success: false, data: response.responseJSON}); + } + }); } }; \ No newline at end of file diff --git a/TEKDB/login/templates/change_password.html b/TEKDB/login/templates/change_password.html new file mode 100644 index 00000000..e2bab785 --- /dev/null +++ b/TEKDB/login/templates/change_password.html @@ -0,0 +1,14 @@ +
+ {% csrf_token %} +
+ + + + + + +
+ +
\ No newline at end of file diff --git a/TEKDB/login/templates/create.html b/TEKDB/login/templates/create.html deleted file mode 100644 index 78bc3a27..00000000 --- a/TEKDB/login/templates/create.html +++ /dev/null @@ -1,21 +0,0 @@ -
-
- Avatar -
- -
- - - - - - - - Remember me -
- -
- - Forgot password? -
-
diff --git a/TEKDB/login/templates/forgot.html b/TEKDB/login/templates/forgot.html deleted file mode 100644 index 9f19422e..00000000 --- a/TEKDB/login/templates/forgot.html +++ /dev/null @@ -1,36 +0,0 @@ -
-
-
-
-
-
-
-
-

-

Forgot Password?

-

You can reset your password here.

-
- -
-
-
-
- - - -
-
-
- -
-
-
- -
-
-
-
-
-
-
-
diff --git a/TEKDB/login/templates/registration/login.html b/TEKDB/login/templates/registration/login.html index 81f9d852..12a42281 100644 --- a/TEKDB/login/templates/registration/login.html +++ b/TEKDB/login/templates/registration/login.html @@ -12,10 +12,3 @@
- diff --git a/TEKDB/login/templates/registration/registration_form.html b/TEKDB/login/templates/registration/registration_form.html deleted file mode 100644 index 78dc5d6a..00000000 --- a/TEKDB/login/templates/registration/registration_form.html +++ /dev/null @@ -1,11 +0,0 @@ -{% extends "base.html" %} - -{% block content %} -
- {% csrf_token %} - {{ form.as_p }} -
- -
-
-{% endblock %} diff --git a/TEKDB/login/tests/test_views.py b/TEKDB/login/tests/test_views.py index e69de29b..2ec703ca 100644 --- a/TEKDB/login/tests/test_views.py +++ b/TEKDB/login/tests/test_views.py @@ -0,0 +1,102 @@ +from django.test import TestCase, Client, RequestFactory +from django.contrib.auth import get_user_model +from django.urls import reverse +from unittest.mock import MagicMock, patch +import json +from login.views import ( + TEKDBPasswordChangeView, + index, + login_logic, +) + +User = get_user_model() + + +class LoginViewTests(TestCase): + def setUp(self): + self.client = Client() + self.factory = RequestFactory() + self.user = User.objects.create_user( + username="testuser", password="testpass123" + ) + + def test_index_view(self): + request = self.factory.get("/") + response = index(request) + self.assertEqual(response.status_code, 200) + self.assertIn("Login", response.content.decode("utf-8")) + + def test_login_success(self): + response = self.client.post( + reverse("login"), {"username": "testuser", "password": "testpass123"} + ) + self.assertTrue(response.wsgi_request.user.is_authenticated) + self.assertIn("explore", response.content.decode("utf-8").lower()) + self.assertEqual(response.status_code, 200) + + def test_login_invalid_credentials(self): + response = self.client.post( + reverse("login"), {"username": "testuser", "password": "wrongpassword"} + ) + self.assertFalse(response.wsgi_request.user.is_authenticated) + self.assertEqual(response.context["errorcode"], 403) + self.assertIn("incorrect", response.context["error"].lower()) + + def test_login_logic_success(self): + request = MagicMock() + request.POST = {"username": "testuser", "password": "testpass123"} + result = login_logic(request) + self.assertTrue(result["success"]) + self.assertEqual(result["username"], "testuser") + + def test_login_logic_failure(self): + request = MagicMock() + request.POST = {"username": "testuser", "password": "wrongpass"} + result = login_logic(request) + self.assertFalse(result["success"]) + + def test_login_async(self): + response = self.client.post( + reverse("login_async"), {"username": "testuser", "password": "testpass123"} + ) + self.assertEqual(response.status_code, 200) + data = json.loads(response.content) + self.assertTrue(data["success"]) + + def test_login_async_failure(self): + response = self.client.post( + reverse("login_async"), {"username": "testuser", "password": "wrongpass"} + ) + data = json.loads(response.content) + self.assertFalse(data["success"]) + + def test_password_change_form_invalid_returns_json_errors(self): + view = TEKDBPasswordChangeView() + form = MagicMock() + form.errors = {"old_password": ["This field is required."]} + + response = view.form_invalid(form) + + self.assertEqual(response.status_code, 400) + payload = json.loads(response.content) + self.assertEqual(payload["data"], form.errors) + self.assertFalse(payload["success"]) + + @patch("login.views.update_session_auth_hash") + def test_password_change_form_valid_updates_session_and_returns_success( + self, mock_update_session_auth_hash + ): + view = TEKDBPasswordChangeView() + view.request = self.factory.post("/change_password/") + form = MagicMock() + form.save.return_value = self.user + form.is_valid.return_value = True + + response = view.form_valid(form) + + self.assertEqual(response.status_code, 200) + payload = json.loads(response.content) + self.assertTrue(payload["success"]) + self.assertEqual(view.object, self.user) + form.save.assert_called_once_with() + mock_update_session_auth_hash.assert_called_once_with(view.request, self.user) diff --git a/TEKDB/login/urls.py b/TEKDB/login/urls.py index 34538d68..5119061b 100644 --- a/TEKDB/login/urls.py +++ b/TEKDB/login/urls.py @@ -3,8 +3,5 @@ from . import views urlpatterns = [ - path("create", views.create, name="create"), - path("forgot", views.forgot, name="forgot"), path("", views.index, name="index"), ] -# url(r'^logout$', views.logout, name='logout'), diff --git a/TEKDB/login/views.py b/TEKDB/login/views.py index 3f92ad70..f2bae458 100644 --- a/TEKDB/login/views.py +++ b/TEKDB/login/views.py @@ -1,8 +1,11 @@ -# Create your views here. from django.http.response import JsonResponse from django.shortcuts import render -from django.contrib.auth import authenticate -from django.contrib.auth import login as auth_login +from django.contrib.auth import ( + authenticate, + login as auth_login, + update_session_auth_hash, +) +from django.contrib.auth.views import PasswordChangeView def index(request): @@ -10,15 +13,6 @@ def index(request): "pageTitle": "Login", } return render(request, "index.html", context) - # return HttpResponse("

Server error: Already Logged In") - - -def forgot(request): - context = { - "pageTitle": "Forgot Login", - } - return render(request, "forgot.html", context) - # return HttpResponse("

Forgot Password") def login(request): @@ -66,3 +60,15 @@ def login_async(request): "success": login_user["success"], } return JsonResponse(context) + + +class TEKDBPasswordChangeView(PasswordChangeView): + def form_invalid(self, form): + return JsonResponse({"data": form.errors, "success": False}, status=400) + + def form_valid(self, form): + self.object = form.save() + # prevent user’s auth session to be invalidated + # and user have to log in again after password change + update_session_auth_hash(self.request, self.object) + return JsonResponse({"data": None, "success": True}, status=200)