add(settings): password changing

This commit is contained in:
AdrienLSH 2024-03-19 13:59:21 +01:00
parent a3be4681cc
commit 68ceec22eb
13 changed files with 234 additions and 99 deletions

View File

@ -8,7 +8,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: PACKAGE VERSION\n" "Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \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" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n" "Language-Team: LANGUAGE <LL@li.org>\n"
@ -18,10 +18,18 @@ msgstr ""
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n > 1);\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." msgid "You dont have permission for this user."
msgstr "Vous n'avez pas de permissions pour cet utilisateur." msgstr "Vous n'avez pas de permissions pour cet utilisateur."
#: views/login.py:22 #: views/login.py:23
msgid "Invalid username or password." msgid "Invalid username or password."
msgstr "Nom d'utilisateur ou mot de passe incorect." msgstr "Nom d'utilisateur ou mot de passe incorect."

View File

@ -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

View File

@ -1,6 +1,6 @@
from django.urls import path 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 = [ urlpatterns = [
path("register", register.RegisterView.as_view(), name="register"), path("register", register.RegisterView.as_view(), name="register"),
@ -8,5 +8,6 @@ urlpatterns = [
path("logout", logout.LogoutView.as_view(), name="logout"), path("logout", logout.LogoutView.as_view(), name="logout"),
path("logged", logged.LoggedView.as_view(), name="logged"), path("logged", logged.LoggedView.as_view(), name="logged"),
path("delete", delete.DeleteView.as_view(), name="delete"), 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')
] ]

View File

@ -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)

View File

@ -67,6 +67,30 @@ class Account
} }
return respondeData['authorize'] || respondeData['detail'] || respondeData['username']?.join(' ') || 'Error.'; 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 }; export { Account };

View File

@ -144,6 +144,26 @@ class Client
return response; return response;
} }
/**
* Send a PUT request with json
* @param {String} uri
* @param {*} data
* @returns {Promise<Response>}
*/
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 * Send a PATCH request with json
* @param {String} uri * @param {String} uri

View File

@ -52,5 +52,6 @@
"TournamentCreateButton": "Create tournament", "TournamentCreateButton": "Create tournament",
"TournamentCreateTournamentName": "Tournament Name", "TournamentCreateTournamentName": "Tournament Name",
"TournamentCreateNbPlayerByGame": "Number of player in a game", "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."
} }

View File

@ -52,5 +52,6 @@
"TournamentCreateButton": "Créer le tournoi", "TournamentCreateButton": "Créer le tournoi",
"TournamentCreateTournamentName": "Nom du tournoi", "TournamentCreateTournamentName": "Nom du tournoi",
"TournamentCreateNbPlayerByGame": "Nombre de joueurs en jeu", "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."
} }

View File

@ -1,4 +1,4 @@
export function clear(property_name, elements_id) export function clearIds(property_name, elements_id)
{ {
elements_id.forEach(element_id => { elements_id.forEach(element_id => {
let element = document.getElementById(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) export function fill_errors(errors, property_name)
{ {
Object.keys(errors).forEach(error_field => Object.keys(errors).forEach(error_field =>

View File

@ -1,5 +1,5 @@
import { client, lang, navigateTo } from "../index.js"; 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"; import AbstractAuthenticatedView from "./abstracts/AbstractAuthenticatedView.js";
export default class extends AbstractAuthenticatedView { export default class extends AbstractAuthenticatedView {
@ -11,7 +11,7 @@ export default class extends AbstractAuthenticatedView {
async toggle_search() async toggle_search()
{ {
clear("innerText", ["detail"]); clearIds("innerText", ["detail"]);
if (client.matchmaking.searching) if (client.matchmaking.searching)
{ {
client.matchmaking.stop(); client.matchmaking.stop();
@ -44,7 +44,7 @@ export default class extends AbstractAuthenticatedView {
display_data(data) display_data(data)
{ {
clear("innerText", ["detail"]); clearIds("innerText", ["detail"]);
fill_errors(data, "innerText"); fill_errors(data, "innerText");
} }

View File

@ -1,5 +1,5 @@
import { client, navigateTo } from '../index.js'; import {client, lang} from '../index.js';
import { clear, fill_errors } from '../utils/formUtils.js'; import {clearElements, fill_errors} from '../utils/formUtils.js'
import AbstractAuthenticatedView from './abstracts/AbstractAuthenticatedView.js'; import AbstractAuthenticatedView from './abstracts/AbstractAuthenticatedView.js';
export default class extends AbstractAuthenticatedView export default class extends AbstractAuthenticatedView
@ -14,10 +14,25 @@ export default class extends AbstractAuthenticatedView
{ {
this.avatarInit(); this.avatarInit();
this.usernameInit(); this.usernameInit();
this.passwordInit();
// document.getElementById('delete-account-button').onclick = () => this.delete_account();
} }
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() { usernameInit() {
const usernameInput = document.getElementById('usernameInput'); const usernameInput = document.getElementById('usernameInput');
const usernameSave = document.getElementById('usernameSave'); const usernameSave = document.getElementById('usernameSave');
@ -54,50 +69,47 @@ export default class extends AbstractAuthenticatedView
async displayAvatar() { async displayAvatar() {
let avatar = document.getElementById('avatar'); let avatar = document.getElementById('avatar');
avatar.src = client.me.avatar_url + '?t=' + new Date().getTime(); avatar.src = client.me.avatar_url + '?t=' + new Date().getTime();
console.log(avatar.src);
} }
async delete_account() async savePassword() {
{ const currentPasswordInput = document.getElementById('currentPasswordInput');
let current_password = document.getElementById('current_password-input').value; const currentPassword = currentPasswordInput.value;
const currentPasswordDetail = document.getElementById('currentPasswordDetail');
let response_data = await client.account.delete(current_password); 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') clearElements('innerHTML', [currentPasswordDetail,
{ newPasswordDetail,
navigateTo('/login'); newPassword2Detail,
return; passwordDetail
} ]);
clear('innerHTML', ['current_password-input']); if (!currentPassword.length)
fill_errors({'current_password-input': response_data.password}, 'innerHTML'); 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() const error = await client.account.updatePassword(currentPassword, newPassword, newPassword2);
{ if (!error) {
let username = document.getElementById('username-input').value; passwordDetail.classList.remove('text-danger');
let new_password = document.getElementById('new_password-input').value; passwordDetail.classList.add('text-success');
let current_password = document.getElementById('current_password-input').value; passwordDetail.innerHTML = lang.get('passwordSaved');
setTimeout(_ => passwordDetail.innerHTML = '', 3000);
let data = {}; clearElements('value', [currentPasswordInput, newPasswordInput, newPassword2Input]);
} else {
data.username = username; passwordDetail.classList.add('text-danger');
if (new_password.length != 0) passwordDetail.classList.remove('text-success');
data.new_password = new_password; fill_errors(error, 'innerHTML');
}
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');
}
async saveUsername() async saveUsername()
{ {
@ -110,13 +122,13 @@ export default class extends AbstractAuthenticatedView
const error = await client.account.updateUsername(username); const error = await client.account.updateUsername(username);
if (!error) { if (!error) {
usernameDetail.classList.remove('text-danger'); usernameDetail.classList.remove('text-danger', 'd-none');
usernameDetail.classList.add('text-success'); usernameDetail.classList.add('text-success');
usernameDetail.innerHTML = 'Username Saved.'; usernameDetail.innerHTML = 'Username Saved.';
setTimeout(_ => usernameDetail.innerHTML = '', 2000); setTimeout(_ => usernameDetail.add('d-none'), 2000);
document.getElementById('usernameSave').classList.add('disabled'); document.getElementById('usernameSave').classList.add('disabled');
} else { } else {
usernameDetail.classList.remove('text-success'); usernameDetail.classList.remove('text-success', 'd-none');
usernameDetail.classList.add('text-danger'); usernameDetail.classList.add('text-danger');
usernameDetail.innerHTML = error; usernameDetail.innerHTML = error;
document.getElementById('usernameSave').classList.add('disabled'); 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'; const avatarUnchanged = client.me.avatar_url === '/static/avatars/default.avif';
return /* HTML */ ` return /* HTML */ `
<div class='container col-sm-10 col-8 d-flex rounded border border-2 bg-light-subtle py-3'> <div class='container-fluid col-lg-8 d-flex rounded border border-2 bg-light-subtle py-3'>
<div class='row col-4 bg-body-tertiary border rounded p-2 m-2 d-flex justify-content-center align-items-center'> <div class='row col-4 bg-body-tertiary border rounded p-2 m-2 d-flex justify-content-center align-items-center'>
<h2 class='border-bottom'>Avatar</h2> <h2 class='border-bottom'>Avatar</h2>
<img id='avatar' class='rounded p-0' src=${client.me.avatar_url} style='cursor: pointer;'> <img id='avatar' class='rounded p-0' src=${client.me.avatar_url} style='cursor: pointer;'>
<input id='avatarInput' class='d-none' type='file' accept='image/*'> <input id='avatarInput' class='d-none' type='file' accept='image/*'>
<div class='d-flex gap-2 mt-1 px-0'> <div class='d-flex gap-2 mt-1 px-0'>
<span class='my-auto ms-1 me-auto' id='avatarDetail'></span> <span class='my-auto ms-1 me-auto' id='avatarDetail'></span>
<button class='btn btn-primary d-none' id='avatarUpload'>Save</button> <button class='btn btn-primary d-none' id='avatarUpload'>Save</button>
<button class='btn btn-danger${avatarUnchanged ? ' d-none' : ''}' id='avatarDelete'>Delete</button> <button class='btn btn-danger${avatarUnchanged ? ' d-none' : ''}' id='avatarDelete'>Delete</button>
</div> </div>
</div> </div>
<div class='flex-grow-1'> <div class='flex-grow-1'>
<h1 class='border-bottom ps-1 mb-3'>Account</h1> <h1 class='border-bottom ps-1 mb-3'>Account settings</h1>
<div> <div class='d-flex flex-column gap-2'>
<div class='input-group'> <div>
<div class='form-floating'> <div class='input-group'>
<input type='text' class='form-control' id='usernameInput' placeholder='username' value=${client.me.username}> <div class='form-floating'>
<label for='usernameInput'>Username</label> <input type='text' class='form-control' id='usernameInput' placeholder='username' value=${client.me.username}>
</div> <label for='usernameInput'>Username</label>
<button class='input-group-text btn btn-success disabled' id='usernameSave'>Save</button> </div>
</div> <button class='input-group-text btn btn-success disabled' id='usernameSave'>Save</button>
<span class='form-text' id='usernameDetail'></span> </div>
</div> <span class='form-text' id='usernameDetail'></span>
</div> </div>
<div class='flex-grow-1 p-1 border rounded bg-body-tertiary'>
<h5 class='ps-1 border-bottom'>Change password</h5>
<div class='d-flex flex-column gap-1'>
<div class='form-floating'>
<input type='password' class='form-control' id='currentPasswordInput' placeholder='Current password'>
<label for='currentPasswordInput'>Current password</label>
</div>
<span class='text-danger ps-1' id='currentPasswordDetail'></span>
<div class='form-floating'>
<input type='password' class='form-control' id='newPasswordInput' placeholder='New password'>
<label for='newPassword2Input'>New password</label>
</div>
<span class='text-danger ps-1' id='newPasswordDetail'></span>
<div class='form-floating'>
<input type='password' class='form-control' id='newPassword2Input' placeholder='Repeat new password'>
<label for='newPassword2Input'>Repeat new password</label>
</div>
<span class='text-danger ps-1' id='newPassword2Detail'></span>
<div class='d-flex flex-row'>
<span class='ps-1 my-auto text-danger' id='passwordDetail'></span>
<button class='btn btn-success ms-auto' id='passwordSave'>Save</button>
</div>
</div>
</div>
<button class='btn btn-outline-danger'>Delete account</button>
</div>
</div>
</div> </div>
`; `;
// <input class='form-control' type='text' placeholder='Username' id='username-input' value=${client.me.username}>
// <h1>Settings</h1>
// <input class='form-control d-inline-block' type='text' placeholder='Username' id='username-input' value=${client.me.username}>
// <span id='username'></span>
// <input type=password placeholder='New Password' id='new_password-input'>
// <span id='new_password'></span>
// <input type=password placeholder='Current Password' id='current_password-input'>
// <span id='current_password'></span>
// <input type='button' value='Save Credentials' id='save-account-button'>
// <span id='save-account'></span>
//
// <input type='button' value='Delete Account' id='delete-account-button'>
// <span id='delete-account'></span>
} }
} }

View File

@ -1,5 +1,5 @@
import { client, lang, navigateTo } from "../../index.js"; 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"; import AbstractNonAuthenticatedView from "../abstracts/AbstractNonAuthenticatedView.js";
export default class extends AbstractNonAuthenticatedView export default class extends AbstractNonAuthenticatedView
@ -141,7 +141,7 @@ export default class extends AbstractNonAuthenticatedView
let response_data = await response.json(); let response_data = await response.json();
clear("innerHTML", ["username", "password", 'login']); clearIds("innerHTML", ["username", "password", 'login']);
fill_errors(response_data, "innerHTML"); fill_errors(response_data, "innerHTML");
} }

View File

@ -1,5 +1,5 @@
import {client, lang, navigateTo} from "../../index.js"; 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"; import AbstractAuthenticatedView from "../abstracts/AbstractAuthenticatedView.js";
export default class extends AbstractAuthenticatedView export default class extends AbstractAuthenticatedView
@ -26,7 +26,7 @@ export default class extends AbstractAuthenticatedView
return; return;
} }
clear("innerHTML", ["name", "nb_players", "nb_players_by_game"]); clearIds("innerHTML", ["name", "nb_players", "nb_players_by_game"]);
fill_errors(response_data, "innerHTML"); fill_errors(response_data, "innerHTML");
} }