From 68ceec22ebc84cc64e2a5c13a145e1308636455b Mon Sep 17 00:00:00 2001 From: AdrienLSH Date: Tue, 19 Mar 2024 13:59:21 +0100 Subject: [PATCH] add(settings): password changing --- accounts/locale/fr/LC_MESSAGES/django.po | 14 +- accounts/serializers/update_password.py | 37 ++++ accounts/urls.py | 5 +- accounts/views/update_password.py | 13 ++ frontend/static/js/api/Account.js | 24 +++ frontend/static/js/api/Client.js | 20 ++ frontend/static/js/lang/en.json | 3 +- frontend/static/js/lang/fr.json | 3 +- frontend/static/js/utils/formUtils.js | 6 +- frontend/static/js/views/MatchMakingView.js | 6 +- frontend/static/js/views/SettingsView.js | 194 ++++++++++-------- .../js/views/accounts/AuthenticationView.js | 4 +- .../views/tournament/TournamentCreateView.js | 4 +- 13 files changed, 234 insertions(+), 99 deletions(-) create mode 100644 accounts/serializers/update_password.py create mode 100644 accounts/views/update_password.py diff --git a/accounts/locale/fr/LC_MESSAGES/django.po b/accounts/locale/fr/LC_MESSAGES/django.po index 73661a4..c6ced36 100644 --- a/accounts/locale/fr/LC_MESSAGES/django.po +++ b/accounts/locale/fr/LC_MESSAGES/django.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2024-03-11 11:02+0100\n" +"POT-Creation-Date: 2024-03-19 13:37+0100\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -18,10 +18,18 @@ msgstr "" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n > 1);\n" -#: serializers/update_user.py:15 +#: serializers/update_password.py:19 +msgid "Current password is incorrect." +msgstr "Mot de passe actuel incorrect." + +#: serializers/update_password.py:24 +msgid "The password does not match." +msgstr "Le mot de passe ne correspond pas." + +#: serializers/update_password.py:31 serializers/update_user.py:15 msgid "You dont have permission for this user." msgstr "Vous n'avez pas de permissions pour cet utilisateur." -#: views/login.py:22 +#: views/login.py:23 msgid "Invalid username or password." msgstr "Nom d'utilisateur ou mot de passe incorect." diff --git a/accounts/serializers/update_password.py b/accounts/serializers/update_password.py new file mode 100644 index 0000000..23f708a --- /dev/null +++ b/accounts/serializers/update_password.py @@ -0,0 +1,37 @@ +from rest_framework.serializers import ModelSerializer, ValidationError +from rest_framework.fields import CharField +from django.contrib.auth.models import User +from django.contrib.auth import login +from django.utils.translation import gettext as _ + + +class UpdatePasswordSerializer(ModelSerializer): + current_password = CharField(write_only=True, required=True) + new_password = CharField(write_only=True, required=True) + new_password2 = CharField(write_only=True, required=True) + + class Meta: + model = User + fields = ['current_password', 'new_password', 'new_password2'] + + def validate_current_password(self, value): + if not self.instance.check_password(value): + raise ValidationError(_('Current password is incorrect.')) + return value + + def validate(self, data): + if data['new_password'] != data['new_password2']: + raise ValidationError({'new_password2': _('The password does not match.')}) + return data + + def update(self, instance, validated_data): + user = self.context['request'].user + + if user.pk != instance.pk: + raise ValidationError({'authorize': _('You dont have permission for this user.')}) + + instance.set_password(validated_data['new_password']) + + instance.save() + login(self.context['request'], instance) + return instance diff --git a/accounts/urls.py b/accounts/urls.py index dcac548..b6d6ed4 100644 --- a/accounts/urls.py +++ b/accounts/urls.py @@ -1,6 +1,6 @@ from django.urls import path -from .views import register, login, logout, delete, logged, update_profile +from .views import register, login, logout, delete, logged, update_profile, update_password urlpatterns = [ path("register", register.RegisterView.as_view(), name="register"), @@ -8,5 +8,6 @@ urlpatterns = [ path("logout", logout.LogoutView.as_view(), name="logout"), path("logged", logged.LoggedView.as_view(), name="logged"), path("delete", delete.DeleteView.as_view(), name="delete"), - path('update_profile', update_profile.UpdateProfileView.as_view(), name='update_profile') + path('update_profile', update_profile.UpdateProfileView.as_view(), name='update_profile'), + path('update_password', update_password.UpdatePasswordView.as_view(), name='update_password') ] diff --git a/accounts/views/update_password.py b/accounts/views/update_password.py new file mode 100644 index 0000000..c96e6b2 --- /dev/null +++ b/accounts/views/update_password.py @@ -0,0 +1,13 @@ +from ..serializers.update_password import UpdatePasswordSerializer +from rest_framework.generics import UpdateAPIView +from rest_framework.permissions import IsAuthenticated +from django.contrib.auth.models import User + + +class UpdatePasswordView(UpdateAPIView): + queryset = User.objects.all() + permission_classes = (IsAuthenticated,) + serializer_class = UpdatePasswordSerializer + + def get_object(self): + return self.queryset.get(pk=self.request.user.pk) diff --git a/frontend/static/js/api/Account.js b/frontend/static/js/api/Account.js index b3604a2..7991760 100644 --- a/frontend/static/js/api/Account.js +++ b/frontend/static/js/api/Account.js @@ -67,6 +67,30 @@ class Account } return respondeData['authorize'] || respondeData['detail'] || respondeData['username']?.join(' ') || 'Error.'; } + + async updatePassword(currentPassword, newPassword, newPassword2) + { + const data = { + current_password: currentPassword, + new_password: newPassword, + new_password2: newPassword2 + }; + const response = await this.client._put('/api/accounts/update_password', data); + if (response.ok) + return null; + + const responseData = await response.json(); + const formatedData = {}; + if (responseData['current_password']) + formatedData['currentPasswordDetail'] = responseData['current_password']; + if (responseData['new_password']) + formatedData['newPasswordDetail'] = responseData['new_password']; + if (responseData['new_password2']) + formatedData['newPassword2Detail'] = responseData['new_password2']; + if (formatedData == {}) + formatedData['passwordDetail'] = 'Error'; + return formatedData; + } } export { Account }; diff --git a/frontend/static/js/api/Client.js b/frontend/static/js/api/Client.js index 38c4767..5341b08 100644 --- a/frontend/static/js/api/Client.js +++ b/frontend/static/js/api/Client.js @@ -144,6 +144,26 @@ class Client return response; } + /** + * Send a PUT request with json + * @param {String} uri + * @param {*} data + * @returns {Promise} + */ + async _put(uri, data) + { + let response = await fetch(this._url + uri, { + method: "PUT", + headers: { + "X-CSRFToken": getCookie("csrftoken"), + "Content-Type": "application/json", + 'Accept-Language': this.lang.currentLang, + }, + body: JSON.stringify(data), + }); + return response; + } + /** * Send a PATCH request with json * @param {String} uri diff --git a/frontend/static/js/lang/en.json b/frontend/static/js/lang/en.json index 2573f4f..86c070a 100644 --- a/frontend/static/js/lang/en.json +++ b/frontend/static/js/lang/en.json @@ -52,5 +52,6 @@ "TournamentCreateButton": "Create tournament", "TournamentCreateTournamentName": "Tournament Name", "TournamentCreateNbPlayerByGame": "Number of player in a game", - "TournamentCreateNbPlayer": "Number of players in the tournament" + "TournamentCreateNbPlayer": "Number of players in the tournament", + "passwordSaved": "New password has been saved." } diff --git a/frontend/static/js/lang/fr.json b/frontend/static/js/lang/fr.json index aed5f98..5f39514 100644 --- a/frontend/static/js/lang/fr.json +++ b/frontend/static/js/lang/fr.json @@ -52,5 +52,6 @@ "TournamentCreateButton": "Créer le tournoi", "TournamentCreateTournamentName": "Nom du tournoi", "TournamentCreateNbPlayerByGame": "Nombre de joueurs en jeu", - "TournamentCreateNbPlayer": "Nombre de joueurs dans le tournoi" + "TournamentCreateNbPlayer": "Nombre de joueurs dans le tournoi", + "passwordSaved": "Nouveau mot de passe enregistré avec succès." } diff --git a/frontend/static/js/utils/formUtils.js b/frontend/static/js/utils/formUtils.js index 0222a4e..e9e2379 100644 --- a/frontend/static/js/utils/formUtils.js +++ b/frontend/static/js/utils/formUtils.js @@ -1,4 +1,4 @@ -export function clear(property_name, elements_id) +export function clearIds(property_name, elements_id) { elements_id.forEach(element_id => { let element = document.getElementById(element_id); @@ -6,6 +6,10 @@ export function clear(property_name, elements_id) }); } +export function clearElements(prop, elements) { + elements.forEach(element => element[prop] = ''); +} + export function fill_errors(errors, property_name) { Object.keys(errors).forEach(error_field => diff --git a/frontend/static/js/views/MatchMakingView.js b/frontend/static/js/views/MatchMakingView.js index c6c1404..35d6f0b 100644 --- a/frontend/static/js/views/MatchMakingView.js +++ b/frontend/static/js/views/MatchMakingView.js @@ -1,5 +1,5 @@ import { client, lang, navigateTo } from "../index.js"; -import { clear, fill_errors } from "../utils/formUtils.js"; +import { clearIds, fill_errors } from "../utils/formUtils.js"; import AbstractAuthenticatedView from "./abstracts/AbstractAuthenticatedView.js"; export default class extends AbstractAuthenticatedView { @@ -11,7 +11,7 @@ export default class extends AbstractAuthenticatedView { async toggle_search() { - clear("innerText", ["detail"]); + clearIds("innerText", ["detail"]); if (client.matchmaking.searching) { client.matchmaking.stop(); @@ -44,7 +44,7 @@ export default class extends AbstractAuthenticatedView { display_data(data) { - clear("innerText", ["detail"]); + clearIds("innerText", ["detail"]); fill_errors(data, "innerText"); } diff --git a/frontend/static/js/views/SettingsView.js b/frontend/static/js/views/SettingsView.js index 387cf80..21d389c 100644 --- a/frontend/static/js/views/SettingsView.js +++ b/frontend/static/js/views/SettingsView.js @@ -1,5 +1,5 @@ -import { client, navigateTo } from '../index.js'; -import { clear, fill_errors } from '../utils/formUtils.js'; +import {client, lang} from '../index.js'; +import {clearElements, fill_errors} from '../utils/formUtils.js' import AbstractAuthenticatedView from './abstracts/AbstractAuthenticatedView.js'; export default class extends AbstractAuthenticatedView @@ -14,10 +14,25 @@ export default class extends AbstractAuthenticatedView { this.avatarInit(); this.usernameInit(); - - // document.getElementById('delete-account-button').onclick = () => this.delete_account(); + this.passwordInit(); } + passwordInit() { + document.getElementById('currentPasswordInput').onkeydown = e => { + if (e.key === 'Enter') + this.savePassword(); + }; + document.getElementById('newPasswordInput').onkeydown = e => { + if (e.key === 'Enter') + this.savePassword(); + }; + document.getElementById('newPassword2Input').onkeydown = e => { + if (e.key === 'Enter') + this.savePassword(); + }; + document.getElementById('passwordSave').onclick = this.savePassword; + } + usernameInit() { const usernameInput = document.getElementById('usernameInput'); const usernameSave = document.getElementById('usernameSave'); @@ -54,50 +69,47 @@ export default class extends AbstractAuthenticatedView async displayAvatar() { let avatar = document.getElementById('avatar'); avatar.src = client.me.avatar_url + '?t=' + new Date().getTime(); - console.log(avatar.src); } - async delete_account() - { - let current_password = document.getElementById('current_password-input').value; - - let response_data = await client.account.delete(current_password); + async savePassword() { + const currentPasswordInput = document.getElementById('currentPasswordInput'); + const currentPassword = currentPasswordInput.value; + const currentPasswordDetail = document.getElementById('currentPasswordDetail'); + const newPasswordInput = document.getElementById('newPasswordInput'); + const newPassword = newPasswordInput.value; + const newPasswordDetail = document.getElementById('newPasswordDetail'); + const newPassword2Input = document.getElementById('newPassword2Input'); + const newPassword2 = newPassword2Input.value; + const newPassword2Detail = document.getElementById('newPassword2Detail'); + const passwordDetail = document.getElementById('passwordDetail'); - if (response_data === null || response_data === 'user deleted') - { - navigateTo('/login'); - return; - } - clear('innerHTML', ['current_password-input']); - fill_errors({'current_password-input': response_data.password}, 'innerHTML'); - } + clearElements('innerHTML', [currentPasswordDetail, + newPasswordDetail, + newPassword2Detail, + passwordDetail + ]); + if (!currentPassword.length) + currentPasswordDetail.innerHTML = lang.get('errorEmptyField'); + if (!newPassword.length) + newPasswordDetail.innerHTML = lang.get('errorEmptyField'); + if (!newPassword2.length) + newPassword2Detail.innerHTML = lang.get('errorEmptyField'); + if (!currentPassword.length || !newPassword.length || !newPassword2.length) + return; - async save_account() - { - let username = document.getElementById('username-input').value; - let new_password = document.getElementById('new_password-input').value; - let current_password = document.getElementById('current_password-input').value; - - let data = {}; - - data.username = username; - if (new_password.length != 0) - data.new_password = new_password; - - let response_data = await client.account.update(data, current_password); - - if (response_data === null) - { - navigateTo('/login'); - return; - } - - if (response_data === 'data has been alterate') - response_data = {'save-account': 'saved'}; - - clear('innerHTML', ['username', 'new_password', 'current_password', 'save-account', 'delete-account']); - fill_errors(response_data, 'innerHTML'); - } + const error = await client.account.updatePassword(currentPassword, newPassword, newPassword2); + if (!error) { + passwordDetail.classList.remove('text-danger'); + passwordDetail.classList.add('text-success'); + passwordDetail.innerHTML = lang.get('passwordSaved'); + setTimeout(_ => passwordDetail.innerHTML = '', 3000); + clearElements('value', [currentPasswordInput, newPasswordInput, newPassword2Input]); + } else { + passwordDetail.classList.add('text-danger'); + passwordDetail.classList.remove('text-success'); + fill_errors(error, 'innerHTML'); + } + } async saveUsername() { @@ -110,13 +122,13 @@ export default class extends AbstractAuthenticatedView const error = await client.account.updateUsername(username); if (!error) { - usernameDetail.classList.remove('text-danger'); + usernameDetail.classList.remove('text-danger', 'd-none'); usernameDetail.classList.add('text-success'); usernameDetail.innerHTML = 'Username Saved.'; - setTimeout(_ => usernameDetail.innerHTML = '', 2000); + setTimeout(_ => usernameDetail.add('d-none'), 2000); document.getElementById('usernameSave').classList.add('disabled'); } else { - usernameDetail.classList.remove('text-success'); + usernameDetail.classList.remove('text-success', 'd-none'); usernameDetail.classList.add('text-danger'); usernameDetail.innerHTML = error; document.getElementById('usernameSave').classList.add('disabled'); @@ -183,44 +195,58 @@ export default class extends AbstractAuthenticatedView const avatarUnchanged = client.me.avatar_url === '/static/avatars/default.avif'; return /* HTML */ ` -
-
-

Avatar

- - -
- - - -
-
-
-

Account

-
-
-
- - -
- -
- -
-
+
+
+

Avatar

+ + +
+ + + +
+
+
+

Account settings

+
+
+
+
+ + +
+ +
+ +
+
+
Change password
+
+
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+
+
+ +
+
`; - // - //

Settings

- // - // - // - // - // - // - // - // - // - // - // } } diff --git a/frontend/static/js/views/accounts/AuthenticationView.js b/frontend/static/js/views/accounts/AuthenticationView.js index 62b5c39..a3644f6 100644 --- a/frontend/static/js/views/accounts/AuthenticationView.js +++ b/frontend/static/js/views/accounts/AuthenticationView.js @@ -1,5 +1,5 @@ import { client, lang, navigateTo } from "../../index.js"; -import { clear, fill_errors } from "../../utils/formUtils.js"; +import { clearIds, fill_errors } from "../../utils/formUtils.js"; import AbstractNonAuthenticatedView from "../abstracts/AbstractNonAuthenticatedView.js"; export default class extends AbstractNonAuthenticatedView @@ -141,7 +141,7 @@ export default class extends AbstractNonAuthenticatedView let response_data = await response.json(); - clear("innerHTML", ["username", "password", 'login']); + clearIds("innerHTML", ["username", "password", 'login']); fill_errors(response_data, "innerHTML"); } diff --git a/frontend/static/js/views/tournament/TournamentCreateView.js b/frontend/static/js/views/tournament/TournamentCreateView.js index 58e2bb1..0f61b13 100644 --- a/frontend/static/js/views/tournament/TournamentCreateView.js +++ b/frontend/static/js/views/tournament/TournamentCreateView.js @@ -1,5 +1,5 @@ import {client, lang, navigateTo} from "../../index.js"; -import { clear, fill_errors } from "../../utils/formUtils.js"; +import { clearIds, fill_errors } from "../../utils/formUtils.js"; import AbstractAuthenticatedView from "../abstracts/AbstractAuthenticatedView.js"; export default class extends AbstractAuthenticatedView @@ -26,7 +26,7 @@ export default class extends AbstractAuthenticatedView return; } - clear("innerHTML", ["name", "nb_players", "nb_players_by_game"]); + clearIds("innerHTML", ["name", "nb_players", "nb_players_by_game"]); fill_errors(response_data, "innerHTML"); }