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(' $getManageUsers = Database::prepare('
SELECT SELECT
u.`user_id`, u.`username`, 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 FROM `msz_users` as u
LEFT JOIN `msz_roles` as r LEFT JOIN `msz_roles` as r
ON u.`display_role` = r.`role_id` ON u.`display_role` = r.`role_id`

View file

@ -10,54 +10,23 @@ $queryOffset = (int)($_GET['o'] ?? 0);
$queryTake = 15; $queryTake = 15;
$userPerms = perms_get_user(MSZ_PERMS_USER, $app->getUserId()); $userPerms = perms_get_user(MSZ_PERMS_USER, $app->getUserId());
$perms = [
$settingsModes = [ 'edit_profile' => perms_check($userPerms, MSZ_USER_PERM_EDIT_PROFILE),
'account' => [ 'edit_avatar' => perms_check($userPerms, MSZ_USER_PERM_CHANGE_AVATAR),
'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,
],
]; ];
$settingsMode = $_GET['m'] ?? null;
if ($settingsMode === 'avatar') { if (!$app->hasActiveSession()) {
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']) {
echo render_error(403); echo render_error(403);
return; 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."; $csrfErrorString = "Couldn't verify you, please refresh the page and retry.";
@ -86,6 +55,7 @@ $avatarErrorStrings = [
]; ];
$tpl->vars([ $tpl->vars([
'settings_perms' => $perms,
'settings_mode' => $settingsMode, 'settings_mode' => $settingsMode,
'settings_modes' => $settingsModes, '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); $avatarFileSizeMax = $app->getConfig()->get('Avatar', 'max_filesize', 'int', 1000000);
if ($_SERVER['REQUEST_METHOD'] === 'POST') { if ($_SERVER['REQUEST_METHOD'] === 'POST') {
switch ($settingsMode) {
case 'account':
if (!tmp_csrf_verify($_POST['csrf'] ?? '')) { if (!tmp_csrf_verify($_POST['csrf'] ?? '')) {
$settingsErrors[] = $csrfErrorString; $settingsErrors[] = $csrfErrorString;
break; } else {
} if (!empty($_POST['profile']) && is_array($_POST['profile'])) {
if (isset($_POST['profile']) && is_array($_POST['profile'])) {
$setUserFieldErrors = user_profile_fields_set($app->getUserId(), $_POST['profile']); $setUserFieldErrors = user_profile_fields_set($app->getUserId(), $_POST['profile']);
if (count($setUserFieldErrors) > 0) { if (count($setUserFieldErrors) > 0) {
@ -142,109 +108,8 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
} }
} }
if (!$disableAccountOptions) { if (!empty($_POST['avatar']) && is_array($_POST['avatar'])) {
if (!empty($_POST['current_password']) switch ($_POST['avatar']['mode'] ?? '') {
|| (
(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']) {
case 'delete': case 'delete':
user_avatar_delete($app->getUserId()); user_avatar_delete($app->getUserId());
break; break;
@ -286,21 +151,13 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
break; 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); $session_id = (int)($_POST['session'] ?? 0);
if ($session_id < 1) { if ($session_id < 1) {
$settingsErrors[] = 'Invalid session.'; $settingsErrors[] = 'Invalid session.';
break; } else {
}
$findSession = Database::prepare(' $findSession = Database::prepare('
SELECT `session_id`, `user_id` SELECT `session_id`, `user_id`
FROM `msz_sessions` FROM `msz_sessions`
@ -311,9 +168,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
if (!$session || (int)$session['user_id'] !== $app->getUserId()) { if (!$session || (int)$session['user_id'] !== $app->getUserId()) {
$settingsErrors[] = 'You may only end your own sessions.'; $settingsErrors[] = 'You may only end your own sessions.';
break; } else {
}
if ((int)$session['session_id'] === $app->getSessionId()) { if ((int)$session['session_id'] === $app->getSessionId()) {
header('Location: /auth.php?m=logout&s=' . tmp_csrf_token()); header('Location: /auth.php?m=logout&s=' . tmp_csrf_token());
return; return;
@ -323,11 +178,99 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
audit_log('PERSONAL_SESSION_DESTROY', $app->getUserId(), [ audit_log('PERSONAL_SESSION_DESTROY', $app->getUserId(), [
$session['session_id'], $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; 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); $tpl->var('settings_errors', $settingsErrors);
switch ($settingsMode) { switch ($settingsMode) {
@ -348,23 +291,18 @@ switch ($settingsMode) {
'); ');
$getMail->bindValue('user_id', $app->getUserId()); $getMail->bindValue('user_id', $app->getUserId());
$currentEmail = $getMail->execute() ? $getMail->fetchColumn() : 'Failed to fetch e-mail address.'; $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)); $userHasAvatar = File::exists($app->getStore('avatars/original')->filename($avatarFileName));
$tpl->vars([ $tpl->vars([
'avatar_user_id' => $app->getUserId(), 'avatar_user_id' => $app->getUserId(),
'avatar_max_width' => $avatarWidthMax, 'avatar_max_width' => $avatarWidthMax,
'avatar_max_height' => $avatarHeightMax, 'avatar_max_height' => $avatarHeightMax,
'avatar_max_filesize' => $avatarFileSizeMax, 'avatar_max_filesize' => $avatarFileSizeMax,
'user_has_avatar' => $userHasAvatar, 'user_has_avatar' => $userHasAvatar,
'settings_profile_fields' => $profileFields,
'settings_profile_values' => $userFields,
'settings_disable_account_options' => $disableAccountOptions,
'settings_email' => $currentEmail,
]); ]);
break; break;

View file

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

View file

@ -1,8 +1,13 @@
{% extends '@mio/settings/master.twig' %} {% extends '@mio/settings/master.twig' %}
{% block settings_content %} {% 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"> <div class="settings__account__row">
{% if settings_perms.edit_profile %}
<div class="settings__account__column"> <div class="settings__account__column">
<div class="settings__account__title">Profile</div> <div class="settings__account__title">Profile</div>
@ -17,6 +22,7 @@
</label> </label>
{% endfor %} {% endfor %}
</div> </div>
{% endif %}
{% if settings_disable_account_options %} {% if settings_disable_account_options %}
<div class="settings__account__column settings__account__column--no-margin settings__account__column--disabled"> <div class="settings__account__column settings__account__column--no-margin settings__account__column--disabled">
@ -106,9 +112,84 @@
{% endif %} {% endif %}
</div> </div>
{% if settings_perms.edit_profile or not settings_disable_account_options %}
<div class="settings__account__row settings__account__row--buttons"> <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> <button class="input__button" type="reset">Reset</button>
</div> </div>
{% endif %}
</form> </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 %} {% 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__') %} {% set alpagination = pagination(audit_log_count, audit_log_take, audit_log_offset, '?m=log', 'settings__') %}
{% block settings_content %} {% block settings_content %}
<div class="container">
<div class="container__title">Account Log</div>
<div class="settings__log">
<div class="settings__description"> <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> <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>
<div class="settings__log">
{{ alpagination }} {{ alpagination }}
{% for log in audit_logs %} {% for log in audit_logs %}
@ -51,4 +54,5 @@
{{ alpagination }} {{ alpagination }}
</div> </div>
</div>
{% endblock %} {% endblock %}

View file

@ -4,11 +4,14 @@
{% set lhpagination = pagination(login_attempts_count, login_attempts_take, login_attempts_offset, '?m=login-history', 'settings__') %} {% set lhpagination = pagination(login_attempts_count, login_attempts_take, login_attempts_offset, '?m=login-history', 'settings__') %}
{% block settings_content %} {% block settings_content %}
<div class="container">
<div class="container__title">Login History</div>
<div class="settings__login-history">
<div class="settings__description"> <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> <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>
<div class="settings__login-history">
{{ lhpagination }} {{ lhpagination }}
{% for attempt in user_login_attempts %} {% for attempt in user_login_attempts %}
@ -58,4 +61,5 @@
{{ lhpagination }} {{ lhpagination }}
</div> </div>
</div>
{% endblock %} {% endblock %}

View file

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

View file

@ -4,11 +4,14 @@
{% set spagination = pagination(sessions_count, sessions_take, sessions_offset, '?m=sessions', 'settings__') %} {% set spagination = pagination(sessions_count, sessions_take, sessions_offset, '?m=sessions', 'settings__') %}
{% block settings_content %} {% block settings_content %}
<div class="container">
<div class="container__title">Login History</div>
<div class="settings__sessions">
<div class="settings__description"> <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> <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>
<div class="settings__sessions">
{{ spagination }} {{ spagination }}
{% for session in user_sessions %} {% for session in user_sessions %}
@ -66,4 +69,5 @@
{{ spagination }} {{ spagination }}
</div> </div>
</div>
{% endblock %} {% endblock %}