Merged account and avatar settings pages.

This commit is contained in:
flash 2018-08-11 20:56:54 +02:00
parent bd83bc15a0
commit 294380d7bc
9 changed files with 524 additions and 569 deletions

View file

@ -33,7 +33,7 @@ switch ($_GET['v'] ?? null) {
$getManageUsers = Database::prepare('
SELECT
u.`user_id`, u.`username`,
COALESCE(u.`user_colour`, r.`role_colour`) as `colour`
COALESCE(u.`user_colour`, r.`role_colour`) as `user_colour`
FROM `msz_users` as u
LEFT JOIN `msz_roles` as r
ON u.`display_role` = r.`role_id`

View file

@ -10,54 +10,23 @@ $queryOffset = (int)($_GET['o'] ?? 0);
$queryTake = 15;
$userPerms = perms_get_user(MSZ_PERMS_USER, $app->getUserId());
$settingsModes = [
'account' => [
'title' => 'Account',
'allow' => perms_check($userPerms, MSZ_USER_PERM_EDIT_PROFILE),
],
'images' => [
'title' => 'Avatar',
'allow' => perms_check($userPerms, MSZ_USER_PERM_CHANGE_AVATAR),
],
'sessions' => [
'title' => 'Sessions',
'allow' => true,
],
'login-history' => [
'title' => 'Login History',
'allow' => true,
],
'log' => [
'title' => 'Account Log',
'allow' => true,
],
$perms = [
'edit_profile' => perms_check($userPerms, MSZ_USER_PERM_EDIT_PROFILE),
'edit_avatar' => perms_check($userPerms, MSZ_USER_PERM_CHANGE_AVATAR),
];
$settingsMode = $_GET['m'] ?? null;
if ($settingsMode === 'avatar') {
header('Location: ?m=images');
return;
}
$settingsNavigation = [];
foreach ($settingsModes as $key => $value) {
if ($value['allow']) {
$settingsNavigation[$value['title']] = $key;
if ($settingsMode === null) {
$settingsMode = $key;
}
}
}
if (!$app->hasActiveSession() || !$settingsModes[$settingsMode]['allow']) {
if (!$app->hasActiveSession()) {
echo render_error(403);
return;
}
$tpl->var('settings_navigation', $settingsNavigation);
$settingsModes = [
'account' => 'Account',
'sessions' => 'Sessions',
'login-history' => 'Login History',
'log' => 'Account Log',
];
$settingsMode = $_GET['m'] ?? key($settingsModes);
$csrfErrorString = "Couldn't verify you, please refresh the page and retry.";
@ -86,6 +55,7 @@ $avatarErrorStrings = [
];
$tpl->vars([
'settings_perms' => $perms,
'settings_mode' => $settingsMode,
'settings_modes' => $settingsModes,
]);
@ -106,14 +76,10 @@ $avatarHeightMax = $app->getConfig()->get('Avatar', 'max_height', 'int', 4000);
$avatarFileSizeMax = $app->getConfig()->get('Avatar', 'max_filesize', 'int', 1000000);
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
switch ($settingsMode) {
case 'account':
if (!tmp_csrf_verify($_POST['csrf'] ?? '')) {
$settingsErrors[] = $csrfErrorString;
break;
}
if (isset($_POST['profile']) && is_array($_POST['profile'])) {
} else {
if (!empty($_POST['profile']) && is_array($_POST['profile'])) {
$setUserFieldErrors = user_profile_fields_set($app->getUserId(), $_POST['profile']);
if (count($setUserFieldErrors) > 0) {
@ -142,109 +108,8 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
}
}
if (!$disableAccountOptions) {
if (!empty($_POST['current_password'])
|| (
(isset($_POST['password']) || isset($_POST['email']))
&& (!empty($_POST['password']['new']) || !empty($_POST['email']['new']))
)
) {
$updateAccountFields = [];
$fetchPassword = Database::prepare('
SELECT `password`
FROM `msz_users`
WHERE `user_id` = :user_id
');
$fetchPassword->bindValue('user_id', $app->getUserId());
$currentPassword = $fetchPassword->execute() ? $fetchPassword->fetchColumn() : null;
if (empty($currentPassword)) {
$settingsErrors[] = 'Something went horribly wrong.';
break;
}
if (!password_verify($_POST['current_password'], $currentPassword)) {
$settingsErrors[] = 'Your current password was incorrect.';
break;
}
if (!empty($_POST['email']['new'])) {
if (empty($_POST['email']['confirm'])
|| $_POST['email']['new'] !== $_POST['email']['confirm']) {
$settingsErrors[] = 'The given e-mail addresses did not match.';
break;
}
$email_validate = user_validate_email($_POST['email']['new'], true);
if ($email_validate !== '') {
switch ($email_validate) {
case 'dns':
$settingsErrors[] = 'No valid MX record exists for this domain.';
break;
case 'format':
$settingsErrors[] = 'The given e-mail address was incorrectly formatted.';
break;
case 'in-use':
$settingsErrors[] = 'This e-mail address is already in use.';
break;
default:
$settingsErrors[] = 'Unknown e-mail validation error.';
}
break;
}
$updateAccountFields['email'] = strtolower($_POST['email']['new']);
audit_log('PERSONAL_EMAIL_CHANGE', $app->getUserId(), [
$updateAccountFields['email'],
]);
}
if (!empty($_POST['password']['new'])) {
if (empty($_POST['password']['confirm'])
|| $_POST['password']['new'] !== $_POST['password']['confirm']) {
$settingsErrors[] = "The given passwords did not match.";
break;
}
$password_validate = user_validate_password($_POST['password']['new']);
if ($password_validate !== '') {
$settingsErrors[] = "The given passwords was too weak.";
break;
}
$updateAccountFields['password'] = user_password_hash($_POST['password']['new']);
audit_log('PERSONAL_PASSWORD_CHANGE', $app->getUserId());
}
if (count($updateAccountFields) > 0) {
$updateUser = Database::prepare('
UPDATE `msz_users`
SET ' . pdo_prepare_array_update($updateAccountFields, true) . '
WHERE `user_id` = :user_id
');
$updateAccountFields['user_id'] = $app->getUserId();
$updateUser->execute($updateAccountFields);
}
}
}
break;
case 'images':
if (!tmp_csrf_verify($_POST['csrf'] ?? '')) {
$settingsErrors[] = $csrfErrorString;
break;
}
if (!empty($_POST['avatar']) && is_array($_POST['avatar']) && !empty($_POST['avatar']['mode'])) {
switch ($_POST['avatar']['mode']) {
if (!empty($_POST['avatar']) && is_array($_POST['avatar'])) {
switch ($_POST['avatar']['mode'] ?? '') {
case 'delete':
user_avatar_delete($app->getUserId());
break;
@ -286,21 +151,13 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
break;
}
}
break;
case 'sessions':
if (!tmp_csrf_verify($_POST['csrf'] ?? '')) {
$settingsErrors[] = $csrfErrorString;
break;
}
if (!empty($_POST['session']) && is_numeric($_POST['session'])) {
$session_id = (int)($_POST['session'] ?? 0);
if ($session_id < 1) {
$settingsErrors[] = 'Invalid session.';
break;
}
} else {
$findSession = Database::prepare('
SELECT `session_id`, `user_id`
FROM `msz_sessions`
@ -311,9 +168,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
if (!$session || (int)$session['user_id'] !== $app->getUserId()) {
$settingsErrors[] = 'You may only end your own sessions.';
break;
}
} else {
if ((int)$session['session_id'] === $app->getSessionId()) {
header('Location: /auth.php?m=logout&s=' . tmp_csrf_token());
return;
@ -323,11 +178,99 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
audit_log('PERSONAL_SESSION_DESTROY', $app->getUserId(), [
$session['session_id'],
]);
}
}
}
if (!$disableAccountOptions) {
if (!empty($_POST['current_password'])
|| (
(isset($_POST['password']) || isset($_POST['email']))
&& (!empty($_POST['password']['new']) || !empty($_POST['email']['new']))
)
) {
$updateAccountFields = [];
$fetchPassword = Database::prepare('
SELECT `password`
FROM `msz_users`
WHERE `user_id` = :user_id
');
$fetchPassword->bindValue('user_id', $app->getUserId());
$currentPassword = $fetchPassword->execute() ? $fetchPassword->fetchColumn() : null;
if (empty($currentPassword)) {
$settingsErrors[] = 'Something went horribly wrong.';
} else {
if (!password_verify($_POST['current_password'], $currentPassword)) {
$settingsErrors[] = 'Your current password was incorrect.';
} else {
if (!empty($_POST['email']['new'])) {
if (empty($_POST['email']['confirm'])
|| $_POST['email']['new'] !== $_POST['email']['confirm']) {
$settingsErrors[] = 'The given e-mail addresses did not match.';
} else {
$email_validate = user_validate_email($_POST['email']['new'], true);
if ($email_validate !== '') {
switch ($email_validate) {
case 'dns':
$settingsErrors[] = 'No valid MX record exists for this domain.';
break;
case 'format':
$settingsErrors[] = 'The given e-mail address was incorrectly formatted.';
break;
case 'in-use':
$settingsErrors[] = 'This e-mail address is already in use.';
break;
default:
$settingsErrors[] = 'Unknown e-mail validation error.';
}
} else {
$updateAccountFields['email'] = strtolower($_POST['email']['new']);
audit_log('PERSONAL_EMAIL_CHANGE', $app->getUserId(), [
$updateAccountFields['email'],
]);
}
}
}
if (!empty($_POST['password']['new'])) {
if (empty($_POST['password']['confirm'])
|| $_POST['password']['new'] !== $_POST['password']['confirm']) {
$settingsErrors[] = "The given passwords did not match.";
} else {
$password_validate = user_validate_password($_POST['password']['new']);
if ($password_validate !== '') {
$settingsErrors[] = "The given passwords was too weak.";
} else {
$updateAccountFields['password'] = user_password_hash($_POST['password']['new']);
audit_log('PERSONAL_PASSWORD_CHANGE', $app->getUserId());
}
}
}
if (count($updateAccountFields) > 0) {
$updateUser = Database::prepare('
UPDATE `msz_users`
SET ' . pdo_prepare_array_update($updateAccountFields, true) . '
WHERE `user_id` = :user_id
');
$updateAccountFields['user_id'] = $app->getUserId();
$updateUser->execute($updateAccountFields);
}
}
}
}
}
}
}
$tpl->var('settings_title', $settingsModes[$settingsMode]['title']);
$tpl->var('settings_title', $settingsModes[$settingsMode]);
$tpl->var('settings_errors', $settingsErrors);
switch ($settingsMode) {
@ -348,23 +291,18 @@ switch ($settingsMode) {
');
$getMail->bindValue('user_id', $app->getUserId());
$currentEmail = $getMail->execute() ? $getMail->fetchColumn() : 'Failed to fetch e-mail address.';
$tpl->vars([
'settings_profile_fields' => $profileFields,
'settings_profile_values' => $userFields,
'settings_disable_account_options' => $disableAccountOptions,
'settings_email' => $currentEmail,
]);
break;
case 'images':
$userHasAvatar = File::exists($app->getStore('avatars/original')->filename($avatarFileName));
$tpl->vars([
'avatar_user_id' => $app->getUserId(),
'avatar_max_width' => $avatarWidthMax,
'avatar_max_height' => $avatarHeightMax,
'avatar_max_filesize' => $avatarFileSizeMax,
'user_has_avatar' => $userHasAvatar,
'settings_profile_fields' => $profileFields,
'settings_profile_values' => $userFields,
'settings_disable_account_options' => $disableAccountOptions,
'settings_email' => $currentEmail,
]);
break;

View file

@ -286,6 +286,7 @@ class Application extends ApplicationBase
$this->templatingInstance->addFilter('html_colour');
$this->templatingInstance->addFilter('url_construct');
$this->templatingInstance->addFilter('country_name', 'get_country_name');
$this->templatingInstance->addFilter('flip', 'array_flip');
$this->templatingInstance->addFilter('first_paragraph');
$this->templatingInstance->addFilter('colour_get_css');
$this->templatingInstance->addFilter('colour_get_css_contrast');

View file

@ -1,8 +1,13 @@
{% extends '@mio/settings/master.twig' %}
{% block settings_content %}
<form method="post" action="?m=account" class="settings__account">
<div class="container">
<div class="container__title">Account</div>
<form action="" method="post" class="settings__account">
<input type="hidden" name="csrf" value="{{ csrf_token() }}">
<div class="settings__account__row">
{% if settings_perms.edit_profile %}
<div class="settings__account__column">
<div class="settings__account__title">Profile</div>
@ -17,6 +22,7 @@
</label>
{% endfor %}
</div>
{% endif %}
{% if settings_disable_account_options %}
<div class="settings__account__column settings__account__column--no-margin settings__account__column--disabled">
@ -106,9 +112,84 @@
{% endif %}
</div>
{% if settings_perms.edit_profile or not settings_disable_account_options %}
<div class="settings__account__row settings__account__row--buttons">
<button class="input__button" name="csrf" value="{{ csrf_token() }}">Update</button>
<button class="input__button">Update</button>
<button class="input__button" type="reset">Reset</button>
</div>
{% endif %}
</form>
</div>
{% if settings_perms.edit_avatar %}
<div class="container">
<div class="container__title">Avatar</div>
<form action="" method="post" class="settings__images" enctype="multipart/form-data">
<input type="hidden" name="MAX_FILE_SIZE" value="{{ avatar_max_filesize }}">
<input type="hidden" name="csrf" value="{{ csrf_token() }}">
<div class="settings__images__sections">
<div class="settings__images__requirements">
<ul class="settings__images__requirements__list">
<li class="settings__images__requirement settings__images__requirement--header">Guidelines</li>
<li class="settings__images__requirement">Keep things sane and suitable for all ages.</li>
<li class="settings__images__requirement">Image may not exceed the <strong>{{ avatar_max_filesize|byte_symbol(true) }}</strong> filesize limit.</li>
<li class="settings__images__requirement settings__images__requirement--header">Avatar</li>
<li class="settings__images__requirement">May not be larger than <strong>{{ avatar_max_width }}x{{ avatar_max_height }}</strong>.</li>
<li class="settings__images__requirement">Will be centre cropped to be <strong>200x200</strong>.</li>
<li class="settings__images__requirement">Animated gif images are allowed.</li>
</ul>
</div>
</div>
<div class="settings__avatar">
<label class="settings__avatar__label">
<div
class="avatar settings__avatar__preview"
id="avatar-preview"
style="background-image:url('/profile.php?u={{ avatar_user_id }}&amp;m=avatar')"></div>
<input
class="settings__avatar__input"
accept="image/png,image/jpeg,image/gif"
type="file"
name="avatar[file]"
id="avatar-selection">
<div class="settings__avatar__name" id="avatar-name">
Click to select a file!
</div>
</label>
<div class="settings__avatar__buttons">
<button
class="settings__avatar__button"
name="avatar[mode]"
value="upload">
Upload
</button>
<button
class="settings__avatar__button settings__avatar__button--delete{{ user_has_avatar ? '' : ' settings__avatar__button--disabled' }}"
{{ user_has_avatar ? '' : 'disabled' }}
name="avatar[mode]"
value="delete">
Delete
</button>
</div>
</div>
</form>
</div>
<script>
function updateAvatarPreview(name, url, previewEl, nameEl) {
url = url || "/profile.php?u={{ avatar_user_id }}&m=avatar";
previewEl = previewEl || document.getElementById('avatar-preview');
nameEl = nameEl || document.getElementById('avatar-name');
previewEl.style.backgroundImage = 'url(\'{0}\')'.replace('{0}', url);
nameEl.textContent = name;
}
document.getElementById('avatar-selection').addEventListener('change', function (ev) {
updateAvatarPreview(ev.target.files[0].name, URL.createObjectURL(ev.target.files[0]));
});
</script>
{% endif %}
{% endblock %}

View file

@ -1,77 +0,0 @@
{% extends '@mio/settings/master.twig' %}
{% block settings_content %}
<form
class="settings__images"
method="post"
action="?m=images"
enctype="multipart/form-data">
<input type="hidden"
name="MAX_FILE_SIZE"
value="{{ avatar_max_filesize }}">
<input type="hidden"
name="csrf"
value="{{ csrf_token() }}">
<div class="settings__images__sections">
<div class="settings__images__requirements">
<ul class="settings__images__requirements__list">
<li class="settings__images__requirement settings__images__requirement--header">Guidelines</li>
<li class="settings__images__requirement">Keep things sane and suitable for all ages.</li>
<li class="settings__images__requirement">Image may not exceed the <strong>{{ avatar_max_filesize|byte_symbol(true) }}</strong> filesize limit.</li>
<li class="settings__images__requirement settings__images__requirement--header">Avatar</li>
<li class="settings__images__requirement">May not be larger than <strong>{{ avatar_max_width }}x{{ avatar_max_height }}</strong>.</li>
<li class="settings__images__requirement">Will be centre cropped to be <strong>200x200</strong>.</li>
<li class="settings__images__requirement">Animated gif images are allowed.</li>
</ul>
</div>
</div>
<div class="settings__avatar">
<label class="settings__avatar__label">
<div
class="avatar settings__avatar__preview"
id="avatar-preview"
style="background-image:url('/profile.php?u={{ avatar_user_id }}&amp;m=avatar')"></div>
<input
class="settings__avatar__input"
accept="image/png,image/jpeg,image/gif"
type="file"
name="avatar[file]"
id="avatar-selection">
<div class="settings__avatar__name" id="avatar-name">
Click to select a file!
</div>
</label>
<div class="settings__avatar__buttons">
<button
class="settings__avatar__button"
name="avatar[mode]"
value="upload">
Upload
</button>
<button
class="settings__avatar__button settings__avatar__button--delete{{ user_has_avatar ? '' : ' settings__avatar__button--disabled' }}"
{{ user_has_avatar ? '' : 'disabled' }}
name="avatar[mode]"
value="delete">
Delete
</button>
</div>
</div>
</form>
<script>
function updateAvatarPreview(name, url, previewEl, nameEl) {
url = url || "/profile.php?u={{ avatar_user_id }}&m=avatar";
previewEl = previewEl || document.getElementById('avatar-preview');
nameEl = nameEl || document.getElementById('avatar-name');
previewEl.style.backgroundImage = 'url(\'' + url + '\')';
nameEl.textContent = name;
}
document.getElementById('avatar-selection').addEventListener('change', function (ev) {
updateAvatarPreview(ev.target.files[0].name, URL.createObjectURL(ev.target.files[0]));
});
</script>
{% endblock %}

View file

@ -4,11 +4,14 @@
{% set alpagination = pagination(audit_log_count, audit_log_take, audit_log_offset, '?m=log', 'settings__') %}
{% block settings_content %}
<div class="container">
<div class="container__title">Account Log</div>
<div class="settings__log">
<div class="settings__description">
<p>This is a log of all "important" actions that have been done using your account for your review. If you notice anything strange, please alert the staff.</p>
</div>
<div class="settings__log">
{{ alpagination }}
{% for log in audit_logs %}
@ -51,4 +54,5 @@
{{ alpagination }}
</div>
</div>
{% endblock %}

View file

@ -4,11 +4,14 @@
{% set lhpagination = pagination(login_attempts_count, login_attempts_take, login_attempts_offset, '?m=login-history', 'settings__') %}
{% block settings_content %}
<div class="container">
<div class="container__title">Login History</div>
<div class="settings__login-history">
<div class="settings__description">
<p>These are all the login attempts to your account. If any attempt that you don't recognise is marked as successful your account may be compromised, ask a staff member for advice in this case.</p>
</div>
<div class="settings__login-history">
{{ lhpagination }}
{% for attempt in user_login_attempts %}
@ -58,4 +61,5 @@
{{ lhpagination }}
</div>
</div>
{% endblock %}

View file

@ -4,7 +4,7 @@
{% set title = 'Settings » ' ~ settings_title %}
{% block content %}
{{ navigation(settings_navigation, settings_mode, true, '?m=%s') }}
{{ navigation(settings_modes|flip, settings_mode, true, '?m=%s') }}
{% block settings_container %}
{% if settings_errors is defined and settings_errors|length > 0 %}
@ -20,15 +20,15 @@
</div>
{% endif %}
<div class="container settings settings--{{ settings_mode }}">
<div class="container__title settings__title settings__title--{{ settings_mode }}">{{ title }}</div>
{% block settings_content %}
<div class="container">
<div class="container__title">{{ title }}</div>
<div class="container__content">
This is a blank settings page.
</div>
{% endblock %}
</div>
{% endblock %}
{% endblock %}
{{ navigation(mio_navigation) }}
{% endblock %}

View file

@ -4,11 +4,14 @@
{% set spagination = pagination(sessions_count, sessions_take, sessions_offset, '?m=sessions', 'settings__') %}
{% block settings_content %}
<div class="container">
<div class="container__title">Login History</div>
<div class="settings__sessions">
<div class="settings__description">
<p>These are the active logins to your account, clicking the Kill button will force a logout on that session. Your current login is highlighted with a darker purple so you don't accidentally force yourself to logout.</p>
</div>
<div class="settings__sessions">
{{ spagination }}
{% for session in user_sessions %}
@ -66,4 +69,5 @@
{{ spagination }}
</div>
</div>
{% endblock %}