Added password resetting.
This commit is contained in:
parent
7c1dd2ed7d
commit
dc79fcb2f8
7 changed files with 251 additions and 19 deletions
|
@ -84,6 +84,7 @@
|
||||||
&--message {
|
&--message {
|
||||||
padding-left: 10px;
|
padding-left: 10px;
|
||||||
color: #aaa;
|
color: #aaa;
|
||||||
|
max-width: 220px;
|
||||||
}
|
}
|
||||||
|
|
||||||
&--error {
|
&--error {
|
||||||
|
|
|
@ -14,4 +14,9 @@
|
||||||
&__label {
|
&__label {
|
||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&--disabled {
|
||||||
|
// same colour as placeholder (in firefox)
|
||||||
|
color: #959595;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -82,6 +82,12 @@ if (PHP_SAPI === 'cli') {
|
||||||
WHERE `expires_on` < NOW()
|
WHERE `expires_on` < NOW()
|
||||||
');
|
');
|
||||||
|
|
||||||
|
// Remove old password reset records, left for a week for possible review
|
||||||
|
Database::exec('
|
||||||
|
DELETE FROM `msz_users_password_resets`
|
||||||
|
WHERE `reset_requested` < NOW() - INTERVAL 1 WEEK
|
||||||
|
');
|
||||||
|
|
||||||
// Cleans up the login history table
|
// Cleans up the login history table
|
||||||
Database::exec('
|
Database::exec('
|
||||||
DELETE FROM `msz_login_attempts`
|
DELETE FROM `msz_login_attempts`
|
||||||
|
|
194
public/auth.php
194
public/auth.php
|
@ -1,5 +1,6 @@
|
||||||
<?php
|
<?php
|
||||||
use Carbon\Carbon;
|
use Carbon\Carbon;
|
||||||
|
use Misuzu\Application;
|
||||||
use Misuzu\Database;
|
use Misuzu\Database;
|
||||||
use Misuzu\Net\IPAddress;
|
use Misuzu\Net\IPAddress;
|
||||||
use Misuzu\Users\Session;
|
use Misuzu\Users\Session;
|
||||||
|
@ -7,7 +8,7 @@ use Misuzu\Users\Session;
|
||||||
require_once __DIR__ . '/../misuzu.php';
|
require_once __DIR__ . '/../misuzu.php';
|
||||||
|
|
||||||
$config = $app->getConfig();
|
$config = $app->getConfig();
|
||||||
$templating = $app->getTemplating();
|
$tpl = $app->getTemplating();
|
||||||
|
|
||||||
$usernameValidationErrors = [
|
$usernameValidationErrors = [
|
||||||
'trim' => 'Your username may not start or end with spaces!',
|
'trim' => 'Your username may not start or end with spaces!',
|
||||||
|
@ -21,9 +22,9 @@ $usernameValidationErrors = [
|
||||||
|
|
||||||
$authMode = $_GET['m'] ?? 'login';
|
$authMode = $_GET['m'] ?? 'login';
|
||||||
$preventRegistration = $config->get('Auth', 'prevent_registration', 'bool', false);
|
$preventRegistration = $config->get('Auth', 'prevent_registration', 'bool', false);
|
||||||
$templating->addPath('auth', __DIR__ . '/../views/auth');
|
$tpl->addPath('auth', __DIR__ . '/../views/auth');
|
||||||
|
|
||||||
$templating->vars([
|
$tpl->vars([
|
||||||
'prevent_registration' => $preventRegistration,
|
'prevent_registration' => $preventRegistration,
|
||||||
'auth_mode' => $authMode,
|
'auth_mode' => $authMode,
|
||||||
'auth_username' => $_REQUEST['username'] ?? '',
|
'auth_username' => $_REQUEST['username'] ?? '',
|
||||||
|
@ -45,7 +46,182 @@ switch ($authMode) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
echo $templating->render('@auth.logout');
|
echo $tpl->render('@auth.logout');
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'reset':
|
||||||
|
if ($app->hasActiveSession()) {
|
||||||
|
header('Location: /settings.php');
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
$resetUser = (int)($_POST['user'] ?? $_GET['u'] ?? 0);
|
||||||
|
$getResetUser = Database::prepare('
|
||||||
|
SELECT `user_id`, `username`
|
||||||
|
FROM `msz_users`
|
||||||
|
WHERE `user_id` = :user_id
|
||||||
|
');
|
||||||
|
$getResetUser->bindValue('user_id', $resetUser);
|
||||||
|
$resetUser = $getResetUser->execute() ? $getResetUser->fetch(PDO::FETCH_ASSOC) : [];
|
||||||
|
|
||||||
|
if (empty($resetUser)) {
|
||||||
|
header('Location: ?m=forgot');
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
$tpl->var('auth_reset_message', 'A verification code should\'ve been sent to your e-mail address.');
|
||||||
|
|
||||||
|
while ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||||
|
$validateRequest = Database::prepare('
|
||||||
|
SELECT COUNT(`user_id`) > 0
|
||||||
|
FROM `msz_users_password_resets`
|
||||||
|
WHERE `user_id` = :user
|
||||||
|
AND `verification_code` = :code
|
||||||
|
AND `verification_code` IS NOT NULL
|
||||||
|
AND `reset_requested` > NOW() - INTERVAL 1 HOUR
|
||||||
|
');
|
||||||
|
$validateRequest->bindValue('user', $resetUser['user_id']);
|
||||||
|
$validateRequest->bindValue('code', $_POST['verification'] ?? '');
|
||||||
|
$validateRequest = $validateRequest->execute()
|
||||||
|
? (bool)$validateRequest->fetchColumn()
|
||||||
|
: false;
|
||||||
|
|
||||||
|
if (!$validateRequest) {
|
||||||
|
$tpl->var('auth_reset_error', 'Invalid verification code!');
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
$tpl->var('reset_verify', $_POST['verification']);
|
||||||
|
|
||||||
|
if (empty($_POST['password']['new'])
|
||||||
|
|| empty($_POST['password']['confirm'])
|
||||||
|
|| $_POST['password']['new'] !== $_POST['password']['confirm']) {
|
||||||
|
$tpl->var('auth_reset_error', 'Your passwords didn\'t match!');
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (user_validate_password($_POST['password']['new']) !== '') {
|
||||||
|
$tpl->var('auth_reset_error', 'Your password is too weak!');
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
$updatePassword = Database::prepare('
|
||||||
|
UPDATE `msz_users`
|
||||||
|
SET `password` = :password
|
||||||
|
WHERE `user_id` = :user
|
||||||
|
');
|
||||||
|
$updatePassword->bindValue('user', $resetUser['user_id']);
|
||||||
|
$updatePassword->bindValue('password', user_password_hash($_POST['password']['new']));
|
||||||
|
|
||||||
|
if ($updatePassword->execute()) {
|
||||||
|
audit_log('PASSWORD_RESET', $resetUser['user_id']);
|
||||||
|
} else {
|
||||||
|
throw new UnexpectedValueException('Password reset failed.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$invalidateCode = Database::prepare('
|
||||||
|
UPDATE `msz_users_password_resets`
|
||||||
|
SET `verification_code` = NULL
|
||||||
|
WHERE `verification_code` = :code
|
||||||
|
AND `user_id` = :user
|
||||||
|
');
|
||||||
|
$invalidateCode->bindValue('user', $resetUser['user_id']);
|
||||||
|
$invalidateCode->bindValue('code', $_POST['verification']);
|
||||||
|
|
||||||
|
if (!$invalidateCode->execute()) {
|
||||||
|
throw new UnexpectedValueException('Verification code invalidation failed.');
|
||||||
|
}
|
||||||
|
|
||||||
|
header('Location: /auth.php?m=login');
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
echo $tpl->render('@auth.password', [
|
||||||
|
'reset_user' => $resetUser,
|
||||||
|
]);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'forgot':
|
||||||
|
if ($app->hasActiveSession()) {
|
||||||
|
header('Location: /');
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
while ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||||
|
if (empty($_POST['email'])) {
|
||||||
|
$tpl->var('auth_forgot_error', 'Please enter an e-mail address.');
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
$forgotUser = Database::prepare('
|
||||||
|
SELECT `user_id`, `username`, `email`
|
||||||
|
FROM `msz_users`
|
||||||
|
WHERE LOWER(`email`) = LOWER(:email)
|
||||||
|
');
|
||||||
|
$forgotUser->bindValue('email', $_POST['email']);
|
||||||
|
$forgotUser = $forgotUser->execute() ? $forgotUser->fetch(PDO::FETCH_ASSOC) : [];
|
||||||
|
|
||||||
|
if (empty($forgotUser)) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
$ipAddress = IPAddress::remote()->getString();
|
||||||
|
$emailSent = Database::prepare('
|
||||||
|
SELECT COUNT(`verification_code`) > 0
|
||||||
|
FROM `msz_users_password_resets`
|
||||||
|
WHERE `user_id` = :user
|
||||||
|
AND `reset_ip` = INET6_ATON(:ip)
|
||||||
|
AND `reset_requested` > NOW() - INTERVAL 1 HOUR
|
||||||
|
AND `verification_code` IS NOT NULL
|
||||||
|
');
|
||||||
|
$emailSent->bindValue('user', $forgotUser['user_id']);
|
||||||
|
$emailSent->bindValue('ip', $ipAddress);
|
||||||
|
$emailSent = $emailSent->execute()
|
||||||
|
? (bool)$emailSent->fetchColumn()
|
||||||
|
: false;
|
||||||
|
|
||||||
|
if (!$emailSent) {
|
||||||
|
$verificationCode = bin2hex(random_bytes(6));
|
||||||
|
$insertResetKey = Database::prepare('
|
||||||
|
REPLACE INTO `msz_users_password_resets`
|
||||||
|
(`user_id`, `reset_ip`, `verification_code`)
|
||||||
|
VALUES
|
||||||
|
(:user, INET6_ATON(:ip), :code)
|
||||||
|
');
|
||||||
|
$insertResetKey->bindValue('user', $forgotUser['user_id']);
|
||||||
|
$insertResetKey->bindValue('ip', $ipAddress);
|
||||||
|
$insertResetKey->bindValue('code', $verificationCode);
|
||||||
|
|
||||||
|
if (!$insertResetKey->execute()) {
|
||||||
|
throw new UnexpectedValueException('A verification code failed to insert.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$messageBody = <<<MSG
|
||||||
|
Hey {$forgotUser['username']},
|
||||||
|
|
||||||
|
You, or someone pretending to be you, has requested a password reset for your account.
|
||||||
|
|
||||||
|
Your verification code is: {$verificationCode}
|
||||||
|
|
||||||
|
If you weren't the person who requested this reset, please send a reply to this e-mail.
|
||||||
|
MSG;
|
||||||
|
|
||||||
|
$message = (new Swift_Message('Flashii Password Reset'))
|
||||||
|
->setFrom([
|
||||||
|
$config->get('Mail', 'sender_email', 'string', 'sys@misuzu.lh') =>
|
||||||
|
$config->get('Mail', 'sender_name', 'string', 'Misuzu')
|
||||||
|
])
|
||||||
|
->setTo([$forgotUser['email'] => $forgotUser['username']])
|
||||||
|
->setBody($messageBody);
|
||||||
|
|
||||||
|
Application::mailer()->send($message);
|
||||||
|
}
|
||||||
|
|
||||||
|
header("Location: ?m=reset&u={$forgotUser['user_id']}");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
echo $tpl->render('@auth.auth');
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'login':
|
case 'login':
|
||||||
|
@ -122,10 +298,10 @@ switch ($authMode) {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!empty($authLoginError)) {
|
if (!empty($authLoginError)) {
|
||||||
$templating->var('auth_login_error', $authLoginError);
|
$tpl->var('auth_login_error', $authLoginError);
|
||||||
}
|
}
|
||||||
|
|
||||||
echo $templating->render('auth');
|
echo $tpl->render('@auth.auth');
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'register':
|
case 'register':
|
||||||
|
@ -183,14 +359,14 @@ switch ($authMode) {
|
||||||
|
|
||||||
user_role_add($createUser, MSZ_ROLE_MAIN);
|
user_role_add($createUser, MSZ_ROLE_MAIN);
|
||||||
|
|
||||||
$templating->var('auth_register_message', 'Welcome to Flashii! You may now log in.');
|
$tpl->var('auth_register_message', 'Welcome to Flashii! You may now log in.');
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!empty($authRegistrationError)) {
|
if (!empty($authRegistrationError)) {
|
||||||
$templating->var('auth_register_error', $authRegistrationError);
|
$tpl->var('auth_register_error', $authRegistrationError);
|
||||||
}
|
}
|
||||||
|
|
||||||
echo $templating->render('@auth.auth');
|
echo $tpl->render('@auth.auth');
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,13 +10,16 @@
|
||||||
<label class="form__row">
|
<label class="form__row">
|
||||||
<input class="text text--username" type="text" name="username" placeholder="username" value="{{ auth_username|default('') }}" required>
|
<input class="text text--username" type="text" name="username" placeholder="username" value="{{ auth_username|default('') }}" required>
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
<label class="form__row">
|
<label class="form__row">
|
||||||
<input class="text text--password" type="password" name="password" placeholder="password" required>
|
<input class="text text--password" type="password" name="password" placeholder="password" required>
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
<div class="form__row form__row--columns">
|
<div class="form__row form__row--columns">
|
||||||
<div class="form__column form__column--message{% if auth_login_error is defined %} form__column--error{% endif %}">
|
<div class="form__column form__column--message{% if auth_login_error is defined %} form__column--error{% endif %}">
|
||||||
{{ auth_login_message|default(auth_login_error|default('')) }}
|
{{ auth_login_message|default(auth_login_error|default('')) }}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form__column">
|
<div class="form__column">
|
||||||
<button class="button button--login">Login</button>
|
<button class="button button--login">Login</button>
|
||||||
</div>
|
</div>
|
||||||
|
@ -38,12 +41,15 @@
|
||||||
<label class="form__row">
|
<label class="form__row">
|
||||||
<input class="text text--username" type="text" name="username" placeholder="username" value="{{ auth_username|default('') }}" required>
|
<input class="text text--username" type="text" name="username" placeholder="username" value="{{ auth_username|default('') }}" required>
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
<label class="form__row">
|
<label class="form__row">
|
||||||
<input class="text text--password" type="password" name="password" placeholder="password" required>
|
<input class="text text--password" type="password" name="password" placeholder="password" required>
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
<label class="form__row">
|
<label class="form__row">
|
||||||
<input class="text text--email" type="text" name="email" placeholder="e-mail" value="{{ auth_email|default('') }}" required>
|
<input class="text text--email" type="text" name="email" placeholder="e-mail" value="{{ auth_email|default('') }}" required>
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
<div class="form__row form__row--columns">
|
<div class="form__row form__row--columns">
|
||||||
<div class="form__column form__column--message{% if auth_register_error is defined %} form__column--error{% endif %}">
|
<div class="form__column form__column--message{% if auth_register_error is defined %} form__column--error{% endif %}">
|
||||||
{{ auth_register_message|default(auth_register_error|default('')) }}
|
{{ auth_register_message|default(auth_register_error|default('')) }}
|
||||||
|
@ -57,24 +63,25 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{# <div class="form__wrapper">
|
<div class="form__wrapper">
|
||||||
<input class="form__toggle" id="_authmode_restore" type="radio" name="_authmode"{% if auth_mode == 'restore' %} checked{% endif %}>
|
<input class="form__toggle" id="_authmode_forgot" type="radio" name="_authmode"{% if auth_mode == 'forgot' %} checked{% endif %}>
|
||||||
<div class="form form--restore">
|
<div class="form form--forgot">
|
||||||
<label class="form__title" for="_authmode_restore">Forgot login details</label>
|
<label class="form__title" for="_authmode_forgot">Forgot your password</label>
|
||||||
<form class="form__content" method="post" action="?m=restore">
|
<form class="form__content" method="post" action="?m=forgot">
|
||||||
<label class="form__row">
|
<label class="form__row">
|
||||||
<input class="text text--email" type="text" name="email" placeholder="e-mail" value="{{ auth_email|default('') }}" required>
|
<input class="text text--email" type="text" name="email" placeholder="e-mail" value="{{ auth_email|default('') }}" required>
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
<div class="form__row form__row--columns">
|
<div class="form__row form__row--columns">
|
||||||
<div class="form__column form__column--message{% if auth_restore_error is defined %} form__column--error{% endif %}">
|
<div class="form__column form__column--message{% if auth_forgot_error is defined %} form__column--error{% endif %}">
|
||||||
{{ auth_restore_message|default(auth_restore_error|default('')) }}
|
{{ auth_forgot_message|default(auth_forgot_error|default('')) }}
|
||||||
</div>
|
</div>
|
||||||
<div class="form__column">
|
<div class="form__column">
|
||||||
<button class="button button--restore">Send reminder</button>
|
<button class="button button--forgot">Send reminder</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</div> #}
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
|
@ -4,7 +4,7 @@
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<title>Authentication</title>
|
<title>Authentication</title>
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
<link href="/css/auth.css" rel="stylesheet" type="text/css">
|
<link href="{{ '/css/auth.css'|asset_url }}" rel="stylesheet" type="text/css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div class="auth">
|
<div class="auth">
|
||||||
|
|
37
views/auth/password.twig
Normal file
37
views/auth/password.twig
Normal file
|
@ -0,0 +1,37 @@
|
||||||
|
{% extends '@auth/master.twig' %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="container">
|
||||||
|
<div class="form__wrapper">
|
||||||
|
<input class="form__toggle" id="_authmode_reset" type="radio" name="_authmode" checked>
|
||||||
|
<div class="form form--reset">
|
||||||
|
<label class="form__title" for="_authmode_reset">Resetting password for {{ reset_user.username }}</label>
|
||||||
|
<form class="form__content" method="post" action="?m=reset">
|
||||||
|
<input type="hidden" name="user" value="{{ reset_user.user_id }}">
|
||||||
|
|
||||||
|
<label class="form__row">
|
||||||
|
<input class="text text--password" type="text" name="verification" placeholder="verification code" maxlength="12" value="{{ reset_verify|default('') }}" required>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<label class="form__row">
|
||||||
|
<input class="text text--password" type="password" name="password[new]" placeholder="new password" required>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<label class="form__row">
|
||||||
|
<input class="text text--password" type="password" name="password[confirm]" placeholder="confirm password" required>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<div class="form__row form__row--columns">
|
||||||
|
<div class="form__column form__column--message{% if auth_reset_error is defined %} form__column--error{% endif %}">
|
||||||
|
{{ auth_reset_error|default(auth_reset_message|default('')) }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form__column">
|
||||||
|
<button class="button button--change">Change password</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
Loading…
Add table
Reference in a new issue