Merged in feature/MAPG-156-delete-account-functionality (pull request #153)

Feature/MAPG-156 delete account functionality
This commit is contained in:
Bence Pőcze 2020-06-25 18:30:42 +00:00
commit 45904a98c2
15 changed files with 218 additions and 90 deletions

View File

@ -0,0 +1,7 @@
(function () {
var form = document.getElementById('accountForm');
MapGuesser.toggleFormSubmitButtonDisableOnChange(form, ['password_new', 'password_new_confirm'])
MapGuesser.setOnsubmitForForm(form);
})();

View File

@ -0,0 +1,5 @@
(function () {
var form = document.getElementById('deleteAccountForm');
MapGuesser.setOnsubmitForForm(form, '/');
})();

View File

@ -15,7 +15,7 @@ var MapGuesser = {
document.head.appendChild(script); document.head.appendChild(script);
window.dataLayer = window.dataLayer || []; window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);} function gtag() { dataLayer.push(arguments); }
gtag('js', new Date()); gtag('js', new Date());
gtag('config', GOOGLE_ANALITICS_ID); gtag('config', GOOGLE_ANALITICS_ID);
}, },
@ -66,6 +66,46 @@ var MapGuesser = {
} }
}, },
setOnsubmitForForm: function (form, redirectOnSuccess) {
form.onsubmit = function (e) {
e.preventDefault();
document.getElementById('loading').style.visibility = 'visible';
var formData = new FormData(form);
var formError = form.getElementsByClassName('formError')[0];
var pageLeaveOnSuccess = typeof redirectOnSuccess === 'string';
MapGuesser.httpRequest('POST', form.action, function () {
if (!pageLeaveOnSuccess) {
document.getElementById('loading').style.visibility = 'hidden';
}
if (this.response.error) {
if (pageLeaveOnSuccess) {
document.getElementById('loading').style.visibility = 'hidden';
}
formError.style.display = 'block';
formError.innerHTML = this.response.error.errorText;
return;
}
if (!pageLeaveOnSuccess) {
formError.style.display = 'none';
form.reset();
} else {
if (redirectOnSuccess === '') {
window.location.reload();
} else {
window.location.replace(redirectOnSuccess);
}
}
}, formData);
}
},
showModal: function (id) { showModal: function (id) {
document.getElementById(id).style.visibility = 'visible'; document.getElementById(id).style.visibility = 'visible';
document.getElementById('cover').style.visibility = 'visible'; document.getElementById('cover').style.visibility = 'visible';
@ -142,6 +182,30 @@ var MapGuesser = {
} else { } else {
button.disabled = true; button.disabled = true;
} }
},
toggleFormSubmitButtonDisableOnChange: function (form, observedInputs) {
for (var i = 0; i < observedInputs.length; i++) {
var input = form.elements[observedInputs[i]];
switch (input.tagName) {
case 'INPUT':
case 'TEXTAREA':
input.oninput = function () {
MapGuesser.toggleDisableOnChange(this, form.elements.submit);
};
break;
case 'SELECT':
input.onchange = function () {
MapGuesser.toggleDisableOnChange(this, form.elements.submit);
};
break;
}
}
form.onreset = function () {
form.elements.submit.disabled = true;
}
} }
}; };

View File

@ -1,48 +0,0 @@
(function () {
var form = document.getElementById('profileForm');
form.elements.password_new.onkeyup = function () {
MapGuesser.toggleDisableOnChange(this, form.elements.save);
};
form.elements.password_new_confirm.onkeyup = function () {
MapGuesser.toggleDisableOnChange(this, form.elements.save);
};
form.onsubmit = function (e) {
document.getElementById('loading').style.visibility = 'visible';
e.preventDefault();
var formData = new FormData(form);
MapGuesser.httpRequest('POST', form.action, function () {
document.getElementById('loading').style.visibility = 'hidden';
if (this.response.error) {
var errorText;
switch (this.response.error) {
case 'password_not_match':
errorText = 'The given current password is wrong.'
break;
case 'password_too_short':
errorText = 'The given new password is too short. Please choose a password that is at least 6 characters long!'
break;
case 'passwords_not_match':
errorText = 'The given new passwords do not match.'
break;
}
var profileFormError = document.getElementById('profileFormError');
profileFormError.style.display = 'block';
profileFormError.innerHTML = errorText;
return;
}
document.getElementById('profileFormError').style.display = 'none';
form.reset();
form.elements.save.disabled = true;
}, formData);
};
})();

View File

@ -1,10 +1,14 @@
<?php namespace MapGuesser\Controller; <?php namespace MapGuesser\Controller;
use MapGuesser\Database\Query\Select;
use MapGuesser\Interfaces\Authorization\ISecured; use MapGuesser\Interfaces\Authorization\ISecured;
use MapGuesser\Interfaces\Database\IResultSet;
use MapGuesser\Interfaces\Request\IRequest; use MapGuesser\Interfaces\Request\IRequest;
use MapGuesser\Interfaces\Response\IContent; use MapGuesser\Interfaces\Response\IContent;
use MapGuesser\PersistentData\PersistentDataManager; use MapGuesser\PersistentData\PersistentDataManager;
use MapGuesser\PersistentData\Model\User; use MapGuesser\PersistentData\Model\User;
use MapGuesser\PersistentData\Model\UserConfirmation;
use MapGuesser\Repository\UserConfirmationRepository;
use MapGuesser\Response\HtmlContent; use MapGuesser\Response\HtmlContent;
use MapGuesser\Response\JsonContent; use MapGuesser\Response\JsonContent;
@ -14,10 +18,13 @@ class UserController implements ISecured
private PersistentDataManager $pdm; private PersistentDataManager $pdm;
private UserConfirmationRepository $userConfirmationRepository;
public function __construct(IRequest $request) public function __construct(IRequest $request)
{ {
$this->request = $request; $this->request = $request;
$this->pdm = new PersistentDataManager(); $this->pdm = new PersistentDataManager();
$this->userConfirmationRepository = new UserConfirmationRepository();
} }
public function authorize(): bool public function authorize(): bool
@ -27,7 +34,7 @@ class UserController implements ISecured
return $user !== null; return $user !== null;
} }
public function getProfile(): IContent public function getAccount(): IContent
{ {
/** /**
* @var User $user * @var User $user
@ -35,10 +42,21 @@ class UserController implements ISecured
$user = $this->request->user(); $user = $this->request->user();
$data = ['user' => $user->toArray()]; $data = ['user' => $user->toArray()];
return new HtmlContent('profile', $data); return new HtmlContent('account/account', $data);
} }
public function saveProfile(): IContent public function getDeleteAccount(): IContent
{
/**
* @var User $user
*/
$user = $this->request->user();
$data = ['user' => $user->toArray()];
return new HtmlContent('account/delete', $data);
}
public function saveAccount(): IContent
{ {
/** /**
* @var User $user * @var User $user
@ -46,18 +64,18 @@ class UserController implements ISecured
$user = $this->request->user(); $user = $this->request->user();
if (!$user->checkPassword($this->request->post('password'))) { if (!$user->checkPassword($this->request->post('password'))) {
$data = ['error' => 'password_not_match']; $data = ['error' => ['errorText' => 'The given current password is wrong.']];
return new JsonContent($data); return new JsonContent($data);
} }
if (strlen($this->request->post('password_new')) > 0) { if (strlen($this->request->post('password_new')) > 0) {
if (strlen($this->request->post('password_new')) < 6) { if (strlen($this->request->post('password_new')) < 6) {
$data = ['error' => 'password_too_short']; $data = ['error' => ['errorText' => 'The given new password is too short. Please choose a password that is at least 6 characters long!']];
return new JsonContent($data); return new JsonContent($data);
} }
if ($this->request->post('password_new') !== $this->request->post('password_new_confirm')) { if ($this->request->post('password_new') !== $this->request->post('password_new_confirm')) {
$data = ['error' => 'passwords_not_match']; $data = ['error' => ['errorText' => 'The given new passwords do not match.']];
return new JsonContent($data); return new JsonContent($data);
} }
@ -69,4 +87,30 @@ class UserController implements ISecured
$data = ['success' => true]; $data = ['success' => true];
return new JsonContent($data); return new JsonContent($data);
} }
public function deleteAccount(): IContent
{
/**
* @var User $user
*/
$user = $this->request->user();
if (!$user->checkPassword($this->request->post('password'))) {
$data = ['error' => ['errorText' => 'The given current password is wrong.']];
return new JsonContent($data);
}
\Container::$dbConnection->startTransaction();
foreach ($this->userConfirmationRepository->getByUser($user) as $userConfirmation) {
$this->pdm->deleteFromDb($userConfirmation);
}
$this->pdm->deleteFromDb($user);
\Container::$dbConnection->commit();
$data = ['success' => true];
return new JsonContent($data);
}
} }

View File

@ -1,5 +1,6 @@
<?php namespace MapGuesser\PersistentData; <?php namespace MapGuesser\PersistentData;
use Generator;
use MapGuesser\Database\Query\Modify; use MapGuesser\Database\Query\Modify;
use MapGuesser\Database\Query\Select; use MapGuesser\Database\Query\Select;
use MapGuesser\Interfaces\Database\IResultSet; use MapGuesser\Interfaces\Database\IResultSet;
@ -9,30 +10,8 @@ class PersistentDataManager
{ {
public function selectFromDb(Select $select, string $type, bool $withRelations = false): ?Model public function selectFromDb(Select $select, string $type, bool $withRelations = false): ?Model
{ {
$table = call_user_func([$type, 'getTable']); $select = $this->createSelect($select, $type, $withRelations);
$fields = call_user_func([$type, 'getFields']);
$select->from($table);
//TODO: only with some relations?
if ($withRelations) {
$relations = call_user_func([$type, 'getRelations']);
$columns = [];
foreach ($fields as $field) {
$columns[] = [$table, $field];
}
$columns = array_merge($columns, $this->getRelationColumns($relations));
$this->leftJoinRelations($select, $table, $relations);
$select->columns($columns);
} else {
$select->columns($fields);
}
//TODO: return with array?
$data = $select->execute()->fetch(IResultSet::FETCH_ASSOC); $data = $select->execute()->fetch(IResultSet::FETCH_ASSOC);
if ($data === null) { if ($data === null) {
@ -45,6 +24,18 @@ class PersistentDataManager
return $model; return $model;
} }
public function selectMultipleFromDb(Select $select, string $type, bool $withRelations = false): Generator
{
$select = $this->createSelect($select, $type, $withRelations);
while ($data = $select->execute()->fetch(IResultSet::FETCH_ASSOC)) {
$model = new $type();
$this->fillWithData($data, $model);
yield $model;
}
}
public function selectFromDbById($id, string $type, bool $withRelations = false): ?Model public function selectFromDbById($id, string $type, bool $withRelations = false): ?Model
{ {
$select = new Select(\Container::$dbConnection); $select = new Select(\Container::$dbConnection);
@ -136,6 +127,34 @@ class PersistentDataManager
$model->resetSnapshot(); $model->resetSnapshot();
} }
private function createSelect(Select $select, string $type, bool $withRelations = false): Select
{
$table = call_user_func([$type, 'getTable']);
$fields = call_user_func([$type, 'getFields']);
$select->from($table);
//TODO: only with some relations?
if ($withRelations) {
$relations = call_user_func([$type, 'getRelations']);
$columns = [];
foreach ($fields as $field) {
$columns[] = [$table, $field];
}
$columns = array_merge($columns, $this->getRelationColumns($relations));
$this->leftJoinRelations($select, $table, $relations);
$select->columns($columns);
} else {
$select->columns($fields);
}
return $select;
}
private function getRelationColumns(array $relations): array private function getRelationColumns(array $relations): array
{ {
$columns = []; $columns = [];

View File

@ -1,6 +1,9 @@
<?php namespace MapGuesser\Repository; <?php namespace MapGuesser\Repository;
use Generator;
use MapGuesser\Database\Query\Select; use MapGuesser\Database\Query\Select;
use MapGuesser\Interfaces\Database\IResultSet;
use MapGuesser\PersistentData\Model\User;
use MapGuesser\PersistentData\Model\UserConfirmation; use MapGuesser\PersistentData\Model\UserConfirmation;
use MapGuesser\PersistentData\PersistentDataManager; use MapGuesser\PersistentData\PersistentDataManager;
@ -25,4 +28,12 @@ class UserConfirmationRepository
return $this->pdm->selectFromDb($select, UserConfirmation::class); return $this->pdm->selectFromDb($select, UserConfirmation::class);
} }
public function getByUser(User $user): Generator
{
$select = new Select(\Container::$dbConnection);
$select->where('user_id', '=', $user->getId());
yield from $this->pdm->selectMultipleFromDb($select, UserConfirmation::class);
}
} }

View File

@ -1,22 +1,26 @@
<?php <?php
$jsFiles = [ $jsFiles = [
'js/profile.js', 'js/account/account.js',
]; ];
?> ?>
<?php require ROOT . '/views/templates/main_header.php'; ?> <?php require ROOT . '/views/templates/main_header.php'; ?>
<?php require ROOT . '/views/templates/header.php'; ?> <?php require ROOT . '/views/templates/header.php'; ?>
<h2>Profile</h2> <h2>Account</h2>
<div class="box"> <div class="box">
<form id="profileForm" action="/profile" method="post"> <form id="accountForm" action="/account" method="post">
<input class="big fullWidth" type="password" name="password" placeholder="Current password" autofocus> <input class="big fullWidth" type="password" name="password" placeholder="Current password" required minlength="6" autofocus>
<hr> <hr>
<?php /* TODO: disabled for the time being, email modification should be implemented */ ?> <?php /* TODO: disabled for the time being, email modification should be implemented */ ?>
<input class="big fullWidth" type="email" name="email" placeholder="Email address" value="<?= $user['email'] ?>" disabled> <input class="big fullWidth" type="email" name="email" placeholder="Email address" value="<?= $user['email'] ?>" disabled>
<input class="big fullWidth marginTop" type="password" name="password_new" placeholder="New password" minlength="6"> <input class="big fullWidth marginTop" type="password" name="password_new" placeholder="New password" minlength="6">
<input class="big fullWidth marginTop" type="password" name="password_new_confirm" placeholder="New password confirmation" minlength="6"> <input class="big fullWidth marginTop" type="password" name="password_new_confirm" placeholder="New password confirmation" minlength="6">
<p id="profileFormError" class="formError justify marginTop"></p> <p id="accountFormError" class="formError justify marginTop"></p>
<div class="right marginTop"> <div class="right marginTop">
<button type="submit" name="save" disabled>Save</button> <button type="submit" name="submit" disabled>Save</button>
</div>
<hr>
<div class="center">
<a class="button red" href="/account/delete" title="Delete account">Delete account</a>
</div> </div>
</form> </form>
</div> </div>

20
views/account/delete.php Normal file
View File

@ -0,0 +1,20 @@
<?php
$jsFiles = [
'js/account/delete.js',
];
?>
<?php require ROOT . '/views/templates/main_header.php'; ?>
<?php require ROOT . '/views/templates/header.php'; ?>
<h2>Delete account</h2>
<div class="box">
<form id="deleteAccountForm" action="/account/delete" method="post">
<p class="justify">Are you sure you want to delete your account? This cannot be undone!</p>
<input class="big fullWidth marginTop" type="password" name="password" placeholder="Current password" required minlength="6" autofocus>
<p id="deleteAccountFormError" class="formError justify marginTop"></p>
<div class="right marginTop">
<button class="red" type="submit" name="submit">Delete account</button>
</div>
</form>
</div>
<?php require ROOT . '/views/templates/footer.php'; ?>
<?php require ROOT . '/views/templates/main_footer.php'; ?>

View File

@ -1,6 +1,6 @@
<?php <?php
$jsFiles = [ $jsFiles = [
'js/login.js', 'js/login/login.js',
]; ];
?> ?>
<?php require ROOT . '/views/templates/main_header.php'; ?> <?php require ROOT . '/views/templates/main_header.php'; ?>

View File

@ -1,6 +1,6 @@
<?php <?php
$jsFiles = [ $jsFiles = [
'js/signup.js', 'js/login/signup.js',
]; ];
?> ?>
<?php require ROOT . '/views/templates/main_header.php'; ?> <?php require ROOT . '/views/templates/main_header.php'; ?>

View File

@ -7,7 +7,7 @@
</h1> </h1>
<p> <p>
<?php if (Container::$request->user()) : ?> <?php if (Container::$request->user()) : ?>
<span><a href="/profile" title="Profile"> <span><a href="/account" title="Account">
<?php /* Copyright (c) 2019 The Bootstrap Authors. License can be found in 'USED_SOFTWARE' in section 'Bootstrap Icons'. */ ?> <?php /* Copyright (c) 2019 The Bootstrap Authors. License can be found in 'USED_SOFTWARE' in section 'Bootstrap Icons'. */ ?>
<svg class="inline" width="1em" height="1em" viewBox="0 0 16 16" fill="currentColor" xmlns="http://www.w3.org/2000/svg"> <svg class="inline" width="1em" height="1em" viewBox="0 0 16 16" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" d="M3 14s-1 0-1-1 1-4 6-4 6 3 6 4-1 1-1 1H3zm5-6a3 3 0 1 0 0-6 3 3 0 0 0 0 6z"/> <path fill-rule="evenodd" d="M3 14s-1 0-1-1 1-4 6-4 6 3 6 4-1 1-1 1H3zm5-6a3 3 0 1 0 0-6 3 3 0 0 0 0 6z"/>

View File

@ -32,9 +32,11 @@ Container::$routeCollection->group('signup', function (MapGuesser\Routing\RouteC
$routeCollection->get('signup.cancel', 'cancel/{token}', [MapGuesser\Controller\LoginController::class, 'cancel']); $routeCollection->get('signup.cancel', 'cancel/{token}', [MapGuesser\Controller\LoginController::class, 'cancel']);
}); });
Container::$routeCollection->get('logout', 'logout', [MapGuesser\Controller\LoginController::class, 'logout']); Container::$routeCollection->get('logout', 'logout', [MapGuesser\Controller\LoginController::class, 'logout']);
Container::$routeCollection->group('profile', function (MapGuesser\Routing\RouteCollection $routeCollection) { Container::$routeCollection->group('account', function (MapGuesser\Routing\RouteCollection $routeCollection) {
$routeCollection->get('profile', '', [MapGuesser\Controller\UserController::class, 'getProfile']); $routeCollection->get('account', '', [MapGuesser\Controller\UserController::class, 'getAccount']);
$routeCollection->post('profile-action', '', [MapGuesser\Controller\UserController::class, 'saveProfile']); $routeCollection->post('account-action', '', [MapGuesser\Controller\UserController::class, 'saveAccount']);
$routeCollection->get('account.delete', 'delete', [MapGuesser\Controller\UserController::class, 'getDeleteAccount']);
$routeCollection->post('account.delete-action', 'delete', [MapGuesser\Controller\UserController::class, 'deleteAccount']);
}); });
//Container::$routeCollection->get('maps', 'maps', [MapGuesser\Controller\MapsController::class, 'getMaps']); //Container::$routeCollection->get('maps', 'maps', [MapGuesser\Controller\MapsController::class, 'getMaps']);
Container::$routeCollection->group('game', function (MapGuesser\Routing\RouteCollection $routeCollection) { Container::$routeCollection->group('game', function (MapGuesser\Routing\RouteCollection $routeCollection) {