Added e-mail confirmation and password reset

This commit is contained in:
Marcin Kurczewski 2014-09-08 13:06:32 +02:00
parent 121c2f80dc
commit 85a026c37b
23 changed files with 619 additions and 107 deletions

View file

@ -1,3 +1,8 @@
[basic]
serviceName = szuru2
serviceBaseUrl = http://localhost/
emailAddress = noreply@localhost
[database] [database]
host = localhost host = localhost
port = 27017 port = 27017
@ -6,6 +11,7 @@ name = booru-dev
[security] [security]
secret = change secret = change
minPasswordLength = 5 minPasswordLength = 5
needEmailActivationToRegister = 1
[security.privileges] [security.privileges]
register = anonymous register = anonymous

View file

@ -53,7 +53,6 @@
<script type="text/javascript" src="/js/Presenters/TagListPresenter.js"></script> <script type="text/javascript" src="/js/Presenters/TagListPresenter.js"></script>
<script type="text/javascript" src="/js/Presenters/HelpPresenter.js"></script> <script type="text/javascript" src="/js/Presenters/HelpPresenter.js"></script>
<script type="text/javascript" src="/js/Presenters/HomePresenter.js"></script> <script type="text/javascript" src="/js/Presenters/HomePresenter.js"></script>
<script type="text/javascript" src="/js/Presenters/PasswordResetPresenter.js"></script>
<script type="text/javascript" src="/js/Presenters/UserActivationPresenter.js"></script> <script type="text/javascript" src="/js/Presenters/UserActivationPresenter.js"></script>
<script type="text/javascript" src="/js/Router.js"></script> <script type="text/javascript" src="/js/Router.js"></script>
<script type="text/javascript" src="/js/Bootstrap.js"></script> <script type="text/javascript" src="/js/Bootstrap.js"></script>

View file

@ -40,12 +40,19 @@ App.Presenters.LoginPresenter = function(
var password = $el.find('[name=password]').val(); var password = $el.find('[name=password]').val();
var remember = $el.find('[name=remember]').val(); var remember = $el.find('[name=remember]').val();
//todo: client side error reporting if (userName.length == 0) {
messagePresenter.showError($messages, 'User name cannot be empty.');
return false;
}
if (password.length == 0) {
messagePresenter.showError($messages, 'Password cannot be empty.');
return false;
}
auth.loginFromCredentials(userName, password, remember) auth.loginFromCredentials(userName, password, remember)
.then(function(response) { .then(function(response) {
router.navigateToMainPage(); router.navigateToMainPage();
//todo: "redirect" to main page
}).fail(function(response) { }).fail(function(response) {
messagePresenter.showError($messages, response.json && response.json.error || response); messagePresenter.showError($messages, response.json && response.json.error || response);
}); });

View file

@ -1,32 +0,0 @@
var App = App || {};
App.Presenters = App.Presenters || {};
App.Presenters.PasswordResetPresenter = function(
jQuery,
topNavigationPresenter,
messagePresenter) {
var $el = jQuery('#content');
var $messages = $el;
function init(args) {
topNavigationPresenter.select('login');
if (args.token) {
alert('Got token');
} else {
render();
}
}
function render() {
$el.html('Password reset placeholder');
};
return {
init: init,
render: render,
};
};
App.DI.register('passwordResetPresenter', App.Presenters.PasswordResetPresenter);

View file

@ -50,10 +50,13 @@ App.Presenters.RegistrationPresenter = function(
} }
function registrationSuccess(apiResponse) { function registrationSuccess(apiResponse) {
//todo: tell user if it turned out that he needs to confirm his e-mail
$el.find('form').slideUp(function() { $el.find('form').slideUp(function() {
var message = 'Registration complete! '; var message = 'Registration complete! ';
if (!apiResponse.json.confirmed) {
message += '<br/>Check your inbox for activation e-mail.<br/>If e-mail doesn\'t show up, check your spam folder.';
} else {
message += '<a href="#/login">Click here</a> to login.'; message += '<a href="#/login">Click here</a> to login.';
}
messagePresenter.showInfo($messages, message); messagePresenter.showInfo($messages, message);
}); });
} }

View file

@ -3,21 +3,111 @@ App.Presenters = App.Presenters || {};
App.Presenters.UserActivationPresenter = function( App.Presenters.UserActivationPresenter = function(
jQuery, jQuery,
topNavigationPresenter) { promise,
util,
auth,
api,
router,
topNavigationPresenter,
messagePresenter) {
var $el = jQuery('#content'); var $el = jQuery('#content');
var $messages = $el;
var template;
var formHidden = false;
var operation;
function init(args) { function init(args) {
if (auth.isLoggedIn()) {
router.navigateToMainPage();
return;
}
topNavigationPresenter.select('login'); topNavigationPresenter.select('login');
reinit(args);
}
function reinit(args) {
operation = args.operation;
console.log(operation);
promise.wait(util.promiseTemplate('user-query-form')).then(function(html) {
template = _.template(html);
if (args.token) {
hideForm();
confirmToken(args.token);
} else {
showForm();
}
render(); render();
});
} }
function render() { function render() {
$el.html('Account activation placeholder'); $el.html(template());
}; $messages = $el.find('.messages');
if (formHidden)
$el.find('form').hide();
$el.find('form').submit(userQueryFormSubmitted);
}
function hideForm() {
formHidden = true;
}
function showForm() {
formHidden = false;
}
function userQueryFormSubmitted(e) {
e.preventDefault();
messagePresenter.hideMessages($messages);
var userNameOrEmail = $el.find('form input[name=user]').val();
if (userNameOrEmail.length == 0) {
messagePresenter.showError($messages, 'Field cannot be blank.');
return;
}
var url = operation == 'passwordReset'
? '/password-reset/' + userNameOrEmail
: '/activation/' + userNameOrEmail;
api.post(url).then(function(response) {
var message = operation == 'passwordReset'
? 'Password reset request sent.'
: 'Activation e-mail resent.';
message += ' Check your inbox.<br/>If e-mail doesn\'t show up, check your spam folder.';
$el.find('#user-query-form').slideUp(function() {
messagePresenter.showInfo($messages, message);
});
}).fail(function(response) {
messagePresenter.showError($messages, response.json && response.json.error || response);
});
}
function confirmToken(token) {
messagePresenter.hideMessages($messages);
var url = operation == 'passwordReset'
? '/finish-password-reset/' + token
: '/finish-activation/' + token;
api.post(url).then(function(response) {
var message = operation == 'passwordReset'
? 'Your new password is <strong>' + response.json.newPassword + '</strong>.'
: 'E-mail activation successful.';
$el.find('#user-query-form').slideUp(function() {
messagePresenter.showInfo($messages, message);
});
}).fail(function(response) {
messagePresenter.showError($messages, response.json && response.json.error || response);
});
}
return { return {
init: init, init: init,
reinit: reinit,
render: render, render: render,
}; };

View file

@ -24,8 +24,8 @@ App.Router = function(jQuery, util, appState) {
inject('#/logout', 'logoutPresenter'); inject('#/logout', 'logoutPresenter');
inject('#/register', 'registrationPresenter'); inject('#/register', 'registrationPresenter');
inject('#/upload', 'postUploadPresenter'); inject('#/upload', 'postUploadPresenter');
inject('#/password-reset(/:token)', 'passwordResetPresenter'); inject('#/password-reset(/:token)', 'userActivationPresenter', {operation: 'passwordReset'});
inject('#/activate(/:token)', 'userActivationPresenter'); inject('#/activate(/:token)', 'userActivationPresenter', {operation: 'activation'});
inject('#/users(/:searchArgs)', 'userListPresenter'); inject('#/users(/:searchArgs)', 'userListPresenter');
inject('#/user/:userName(/:tab)', 'userPresenter'); inject('#/user/:userName(/:tab)', 'userPresenter');
inject('#/posts(/:searchArgs)', 'postListPresenter'); inject('#/posts(/:searchArgs)', 'postListPresenter');
@ -40,9 +40,9 @@ App.Router = function(jQuery, util, appState) {
Path.root(newRoot); Path.root(newRoot);
}; };
function inject(path, presenterName) { function inject(path, presenterName, additionalParams) {
Path.map(path).to(function() { Path.map(path).to(function() {
util.initContentPresenter(presenterName, this.params); util.initContentPresenter(presenterName, _.extend(this.params, additionalParams));
}); });
}; };

View file

@ -0,0 +1,19 @@
<div class="messages"></div>
<div id="user-query-form">
<form class="form-wrapper">
<div class="form-row">
<label class="form-label" for="user-query-user">User name or e-mail:</label>
<div class="form-input">
<input autocomplete="off" type="text" name="user" id="user-query-user"/>
</div>
</div>
<div class="form-row">
<label class="form-label"></label>
<div class="form-input">
<button type="submit">Continue</button>
</div>
</div>
</form>
</div>

View file

@ -24,14 +24,18 @@ final class UserController extends AbstractController
{ {
$router->post('/api/users', [$this, 'createUser']); $router->post('/api/users', [$this, 'createUser']);
$router->get('/api/users', [$this, 'getFiltered']); $router->get('/api/users', [$this, 'getFiltered']);
$router->get('/api/users/:userName', [$this, 'getByName']); $router->get('/api/users/:userNameOrEmail', [$this, 'getByNameOrEmail']);
$router->put('/api/users/:userName', [$this, 'updateUser']); $router->put('/api/users/:userNameOrEmail', [$this, 'updateUser']);
$router->delete('/api/users/:userName', [$this, 'deleteUser']); $router->delete('/api/users/:userNameOrEmail', [$this, 'deleteUser']);
$router->post('/api/password-reset/:userNameOrEmail', [$this, 'passwordReset']);
$router->post('/api/finish-password-reset/:tokenName', [$this, 'finishPasswordReset']);
$router->post('/api/activation/:userNameOrEmail', [$this, 'activation']);
$router->post('/api/finish-activation/:tokenName', [$this, 'finishActivation']);
} }
public function getByName($userName) public function getByNameOrEmail($userNameOrEmail)
{ {
$user = $this->userService->getByName($userName); $user = $this->userService->getByNameOrEmail($userNameOrEmail);
return $this->userViewProxy->fromEntity($user); return $this->userViewProxy->fromEntity($user);
} }
@ -53,17 +57,18 @@ final class UserController extends AbstractController
$this->privilegeService->assertPrivilege(\Szurubooru\Privilege::REGISTER); $this->privilegeService->assertPrivilege(\Szurubooru\Privilege::REGISTER);
$formData = new \Szurubooru\FormData\RegistrationFormData($this->inputReader); $formData = new \Szurubooru\FormData\RegistrationFormData($this->inputReader);
$user = $this->userService->createUser($formData); $user = $this->userService->createUser($formData);
return $this->userViewProxy->fromEntity($user); return array_merge((array) $this->userViewProxy->fromEntity($user), ['confirmed' => $user->emailUnconfirmed == null]);
} }
public function updateUser($userName) public function updateUser($userNameOrEmail)
{ {
$user = $this->userService->getByNameOrEmail($userNameOrEmail);
$formData = new \Szurubooru\FormData\UserEditFormData($this->inputReader); $formData = new \Szurubooru\FormData\UserEditFormData($this->inputReader);
if ($formData->avatarStyle !== null) if ($formData->avatarStyle !== null)
{ {
$this->privilegeService->assertPrivilege( $this->privilegeService->assertPrivilege(
$this->privilegeService->isLoggedIn($userName) $this->privilegeService->isLoggedIn($userNameOrEmail)
? \Szurubooru\Privilege::CHANGE_OWN_AVATAR_STYLE ? \Szurubooru\Privilege::CHANGE_OWN_AVATAR_STYLE
: \Szurubooru\Privilege::CHANGE_ALL_AVATAR_STYLES); : \Szurubooru\Privilege::CHANGE_ALL_AVATAR_STYLES);
} }
@ -71,7 +76,7 @@ final class UserController extends AbstractController
if ($formData->userName !== null) if ($formData->userName !== null)
{ {
$this->privilegeService->assertPrivilege( $this->privilegeService->assertPrivilege(
$this->privilegeService->isLoggedIn($userName) $this->privilegeService->isLoggedIn($userNameOrEmail)
? \Szurubooru\Privilege::CHANGE_OWN_NAME ? \Szurubooru\Privilege::CHANGE_OWN_NAME
: \Szurubooru\Privilege::CHANGE_ALL_NAMES); : \Szurubooru\Privilege::CHANGE_ALL_NAMES);
} }
@ -79,7 +84,7 @@ final class UserController extends AbstractController
if ($formData->password !== null) if ($formData->password !== null)
{ {
$this->privilegeService->assertPrivilege( $this->privilegeService->assertPrivilege(
$this->privilegeService->isLoggedIn($userName) $this->privilegeService->isLoggedIn($userNameOrEmail)
? \Szurubooru\Privilege::CHANGE_OWN_PASSWORD ? \Szurubooru\Privilege::CHANGE_OWN_PASSWORD
: \Szurubooru\Privilege::CHANGE_ALL_PASSWORDS); : \Szurubooru\Privilege::CHANGE_ALL_PASSWORDS);
} }
@ -87,7 +92,7 @@ final class UserController extends AbstractController
if ($formData->email !== null) if ($formData->email !== null)
{ {
$this->privilegeService->assertPrivilege( $this->privilegeService->assertPrivilege(
$this->privilegeService->isLoggedIn($userName) $this->privilegeService->isLoggedIn($userNameOrEmail)
? \Szurubooru\Privilege::CHANGE_OWN_EMAIL_ADDRESS ? \Szurubooru\Privilege::CHANGE_OWN_EMAIL_ADDRESS
: \Szurubooru\Privilege::CHANGE_ALL_EMAIL_ADDRESSES); : \Szurubooru\Privilege::CHANGE_ALL_EMAIL_ADDRESSES);
} }
@ -99,20 +104,43 @@ final class UserController extends AbstractController
if ($formData->browsingSettings) if ($formData->browsingSettings)
{ {
$this->privilegeService->assertLoggedIn($userName); $this->privilegeService->assertLoggedIn($userNameOrEmail);
} }
$user = $this->userService->updateUser($userName, $formData); $user = $this->userService->updateUser($user, $formData);
return $this->userViewProxy->fromEntity($user); return $this->userViewProxy->fromEntity($user);
} }
public function deleteUser($userName) public function deleteUser($userNameOrEmail)
{ {
$this->privilegeService->assertPrivilege( $this->privilegeService->assertPrivilege(
$this->privilegeService->isLoggedIn($userName) $this->privilegeService->isLoggedIn($userNameOrEmail)
? \Szurubooru\Privilege::DELETE_OWN_ACCOUNT ? \Szurubooru\Privilege::DELETE_OWN_ACCOUNT
: \Szurubooru\Privilege::DELETE_ACCOUNTS); : \Szurubooru\Privilege::DELETE_ACCOUNTS);
return $this->userService->deleteUserByName($userName); $user = $this->userService->getByNameOrEmail($userNameOrEmail);
return $this->userService->deleteUser($user);
}
public function passwordReset($userNameOrEmail)
{
$user = $this->userService->getByNameOrEmail($userNameOrEmail);
return $this->userService->sendPasswordResetEmail($user);
}
public function activation($userNameOrEmail)
{
$user = $this->userService->getByNameOrEmail($userNameOrEmail, true);
return $this->userService->sendActivationEmail($user);
}
public function finishPasswordReset($tokenName)
{
return ['newPassword' => $this->userService->finishPasswordReset($tokenName)];
}
public function finishActivation($tokenName)
{
$this->userService->finishActivation($tokenName);
} }
} }

View file

@ -31,6 +31,7 @@ class UserViewProxy extends AbstractViewProxy
$this->privilegeService->isLoggedIn($user)) $this->privilegeService->isLoggedIn($user))
{ {
$result->email = $user->email; $result->email = $user->email;
$result->emailUnconfirmed = $user->emailUnconfirmed;
} }
} }
return $result; return $result;

View file

@ -15,6 +15,14 @@ class UserDao extends AbstractDao implements ICrudDao
return $this->entityConverter->toEntity($arrayEntity); return $this->entityConverter->toEntity($arrayEntity);
} }
public function getByEmail($userEmail, $allowUnconfirmed = false)
{
$arrayEntity = $this->collection->findOne(['email' => $userEmail]);
if (!$arrayEntity and $allowUnconfirmed)
$arrayEntity = $this->collection->findOne(['emailUnconfirmed' => $userEmail]);
return $this->entityConverter->toEntity($arrayEntity);
}
public function hasAnyUsers() public function hasAnyUsers()
{ {
return (bool) $this->collection->findOne(); return (bool) $this->collection->findOne();

View file

@ -4,6 +4,8 @@ namespace Szurubooru\Entities;
final class Token extends Entity final class Token extends Entity
{ {
const PURPOSE_LOGIN = 'login'; const PURPOSE_LOGIN = 'login';
const PURPOSE_ACTIVATE = 'activate';
const PURPOSE_PASSWORD_RESET = 'passwordReset';
public $name; public $name;
public $purpose; public $purpose;

View file

@ -16,6 +16,7 @@ final class User extends Entity
public $name; public $name;
public $email; public $email;
public $emailUnconfirmed;
public $passwordHash; public $passwordHash;
public $accessRank; public $accessRank;
public $registrationTime; public $registrationTime;

View file

@ -6,20 +6,20 @@ class AuthService
private $loggedInUser = null; private $loggedInUser = null;
private $loginToken = null; private $loginToken = null;
private $validator; private $config;
private $passwordService; private $passwordService;
private $timeService; private $timeService;
private $userService; private $userService;
private $tokenService; private $tokenService;
public function __construct( public function __construct(
\Szurubooru\Validator $validator, \Szurubooru\Config $config,
\Szurubooru\Services\PasswordService $passwordService, \Szurubooru\Services\PasswordService $passwordService,
\Szurubooru\Services\TimeService $timeService, \Szurubooru\Services\TimeService $timeService,
\Szurubooru\Services\TokenService $tokenService, \Szurubooru\Services\TokenService $tokenService,
\Szurubooru\Services\UserService $userService) \Szurubooru\Services\UserService $userService)
{ {
$this->validator = $validator; $this->config = $config;
$this->passwordService = $passwordService; $this->passwordService = $passwordService;
$this->timeService = $timeService; $this->timeService = $timeService;
$this->tokenService = $tokenService; $this->tokenService = $tokenService;
@ -43,12 +43,10 @@ class AuthService
return $this->loginToken; return $this->loginToken;
} }
public function loginFromCredentials($userName, $password) public function loginFromCredentials($userNameOrEmail, $password)
{ {
$this->validator->validateUserName($userName); $user = $this->userService->getByNameOrEmail($userNameOrEmail);
$this->validator->validatePassword($password); $this->validateUser($user);
$user = $this->userService->getByName($userName);
$passwordHash = $this->passwordService->getHash($password); $passwordHash = $this->passwordService->getHash($password);
if ($user->passwordHash != $passwordHash) if ($user->passwordHash != $passwordHash)
@ -61,13 +59,12 @@ class AuthService
public function loginFromToken($loginTokenName) public function loginFromToken($loginTokenName)
{ {
$this->validator->validateToken($loginTokenName);
$loginToken = $this->tokenService->getByName($loginTokenName); $loginToken = $this->tokenService->getByName($loginTokenName);
if ($loginToken->purpose != \Szurubooru\Entities\Token::PURPOSE_LOGIN) if ($loginToken->purpose != \Szurubooru\Entities\Token::PURPOSE_LOGIN)
throw new \Exception('This token is not a login token.'); throw new \Exception('This token is not a login token.');
$user = $this->userService->getById($loginToken->additionalData); $user = $this->userService->getById($loginToken->additionalData);
$this->validateUser($user);
$this->loginToken = $loginToken; $this->loginToken = $loginToken;
$this->loggedInUser = $user; $this->loggedInUser = $user;
@ -93,12 +90,18 @@ class AuthService
if (!$this->isLoggedIn()) if (!$this->isLoggedIn())
throw new \Exception('Not logged in.'); throw new \Exception('Not logged in.');
$this->tokenService->invalidateByToken($this->loginToken); $this->tokenService->invalidateByName($this->loginToken);
$this->loginToken = null; $this->loginToken = null;
} }
private function createAndSaveLoginToken(\Szurubooru\Entities\User $user) private function createAndSaveLoginToken(\Szurubooru\Entities\User $user)
{ {
return $this->tokenService->createAndSaveToken($user, \Szurubooru\Entities\Token::PURPOSE_LOGIN); return $this->tokenService->createAndSaveToken($user->id, \Szurubooru\Entities\Token::PURPOSE_LOGIN);
}
private function validateUser($user)
{
if (!$user->email and $this->config->security->needEmailActivationToRegister)
throw new \DomainException('User didn\'t confirm mail yet.');
} }
} }

View file

@ -3,4 +3,104 @@ namespace Szurubooru\Services;
class EmailService class EmailService
{ {
private $config;
public function __construct(\Szurubooru\Config $config)
{
$this->config = $config;
}
public function sendPasswordResetEmail(\Szurubooru\Entities\User $user, \Szurubooru\Entities\Token $token)
{
if (!$user->email)
throw new \BadMethodCall('An activated e-mail addreses is needed to reset the password.');
$recipientEmail = $user->email;
$senderName = $this->config->basic->serviceName . ' bot';
$subject = $this->config->basic->serviceName . ' password reset';
$body =
'Hello,' .
PHP_EOL . PHP_EOL .
'Someone (probably you) requested to reset password for an account at ' . $this->config->basic->serviceName . '. ' .
'In order to proceed, please click this link or paste it in your browser address bar: ' .
PHP_EOL . PHP_EOL .
$this->config->basic->serviceBaseUrl . '#/password-reset/' . $token->name .
PHP_EOL . PHP_EOL .
'Otherwise please ignore this mail.' .
$this->getFooter();
$this->sendEmail($senderName, $recipientEmail, $subject, $body);
}
public function sendActivationEmail(\Szurubooru\Entities\User $user, \Szurubooru\Entities\Token $token)
{
if (!$user->emailUnconfirmed)
{
throw new \BadMethodCallException(
$user->email
? 'E-mail for this account is already confirmed.'
: 'An e-mail address is needed to activate the account.');
}
$recipientEmail = $user->emailUnconfirmed;
$senderName = $this->config->basic->serviceName . ' bot';
$subject = $this->config->basic->serviceName . ' account activation';
$body =
'Hello,' .
PHP_EOL . PHP_EOL .
'Someone (probably you) registered at ' . $this->config->basic->serviceName . ' an account with this e-mail address. ' .
'In order to finish activation, please click this link or paste it in your browser address bar: ' .
PHP_EOL . PHP_EOL .
$this->config->basic->serviceBaseUrl . '#/activate/' . $token->name .
PHP_EOL . PHP_EOL .
'Otherwise please ignore this mail.' .
$this->getFooter();
$this->sendEmail($senderName, $recipientEmail, $subject, $body);
}
private function sendEmail($senderName, $recipientEmail, $subject, $body)
{
$senderEmail = $this->config->basic->emailAddress;
$domain = substr($senderEmail, strpos($senderEmail, '@') + 1);
$clientIp = isset($_SERVER['SERVER_ADDR'])
? $_SERVER['SERVER_ADDR']
: '';
$body = wordwrap($body, 70);
if (empty($recipientEmail))
throw new \InvalidArgumentException('Destination e-mail address was not found');
$messageId = sha1(date('r') . uniqid()) . '@' . $domain;
$headers = [];
$headers []= sprintf('MIME-Version: 1.0');
$headers []= sprintf('Content-Transfer-Encoding: 7bit');
$headers []= sprintf('Date: %s', date('r'));
$headers []= sprintf('Message-ID: <%s>', $messageId);
$headers []= sprintf('From: %s <%s>', $senderName, $senderEmail);
$headers []= sprintf('Reply-To: %s', $senderEmail);
$headers []= sprintf('Return-Path: %s', $senderEmail);
$headers []= sprintf('Subject: %s', $subject);
$headers []= sprintf('Content-Type: text/plain; charset=utf-8');
$headers []= sprintf('X-Mailer: PHP/%s', phpversion());
$headers []= sprintf('X-Originating-IP: %s', $clientIp);
$senderEmail = $this->config->basic->emailAddress;
$encodedSubject = '=?UTF-8?B?' . base64_encode($subject) . '?=';
$arguments = [$recipientEmail, $encodedSubject, $body, implode("\r\n", $headers), '-f' . $senderEmail];
//throw new \RuntimeException(htmlentities(print_r($arguments, true)));
call_user_func_array('mail', $arguments);
}
private function getFooter()
{
return PHP_EOL . PHP_EOL .
'Thank you and have a nice day,' . PHP_EOL .
$this->config->basic->serviceName . ' registration bot';
}
} }

View file

@ -4,14 +4,35 @@ namespace Szurubooru\Services;
class PasswordService class PasswordService
{ {
private $config; private $config;
private $alphabet;
private $pattern;
public function __construct(\Szurubooru\Config $config) public function __construct(\Szurubooru\Config $config)
{ {
$this->config = $config; $this->config = $config;
$this->alphabet =
[
'c' => str_split('bcdfghjklmnpqrstvwxyz'),
'v' => str_split('aeiou'),
'n' => str_split('0123456789'),
];
$this->pattern = str_split('cvcvnncvcv');
} }
public function getHash($password) public function getHash($password)
{ {
return hash('sha256', $this->config->security->secret . '/' . $password); return hash('sha256', $this->config->security->secret . '/' . $password);
} }
public function getRandomPassword()
{
$password = '';
foreach ($this->pattern as $token)
{
$subAlphabet = $this->alphabet[$token];
$character = $subAlphabet[mt_rand(0, count($subAlphabet) - 1)];
$password .= $character;
}
return $password;
}
} }

View file

@ -57,10 +57,21 @@ class PrivilegeService
{ {
$loggedInUser = $this->authService->getLoggedInUser(); $loggedInUser = $this->authService->getLoggedInUser();
if ($userIdentifier instanceof \Szurubooru\Entities\User) if ($userIdentifier instanceof \Szurubooru\Entities\User)
{
return $loggedInUser->name == $userIdentifier->name; return $loggedInUser->name == $userIdentifier->name;
}
elseif (is_string($userIdentifier)) elseif (is_string($userIdentifier))
{
if ($loggedInUser->email)
{
if ($loggedInUser->email == $userIdentifier)
return true;
}
return $loggedInUser->name == $userIdentifier; return $loggedInUser->name == $userIdentifier;
}
else else
{
throw new \InvalidArgumentException('Invalid user identifier.'); throw new \InvalidArgumentException('Invalid user identifier.');
} }
}
} }

View file

@ -18,23 +18,23 @@ class TokenService
return $token; return $token;
} }
public function invalidateByToken($tokenName) public function invalidateByName($tokenName)
{ {
return $this->tokenDao->deleteByName($tokenName); return $this->tokenDao->deleteByName($tokenName);
} }
public function invalidateByUser(\Szurubooru\Entities\User $user) public function invalidateByAdditionalData($additionalData)
{ {
return $this->tokenDao->deleteByAdditionalData($user->id); return $this->tokenDao->deleteByAdditionalData($additionalData);
} }
public function createAndSaveToken(\Szurubooru\Entities\User $user, $tokenPurpose) public function createAndSaveToken($additionalData, $tokenPurpose)
{ {
$token = new \Szurubooru\Entities\Token(); $token = new \Szurubooru\Entities\Token();
$token->name = hash('sha256', $user->name . '/' . microtime(true)); $token->name = sha1(date('r') . uniqid() . microtime(true));
$token->additionalData = $user->id; $token->additionalData = $additionalData;
$token->purpose = $tokenPurpose; $token->purpose = $tokenPurpose;
$this->invalidateByUser($user); $this->invalidateByAdditionalData($additionalData);
$this->tokenDao->save($token); $this->tokenDao->save($token);
return $token; return $token;
} }

View file

@ -11,6 +11,7 @@ class UserService
private $emailService; private $emailService;
private $fileService; private $fileService;
private $timeService; private $timeService;
private $tokenService;
public function __construct( public function __construct(
\Szurubooru\Config $config, \Szurubooru\Config $config,
@ -20,7 +21,8 @@ class UserService
\Szurubooru\Services\PasswordService $passwordService, \Szurubooru\Services\PasswordService $passwordService,
\Szurubooru\Services\EmailService $emailService, \Szurubooru\Services\EmailService $emailService,
\Szurubooru\Services\FileService $fileService, \Szurubooru\Services\FileService $fileService,
\Szurubooru\Services\TimeService $timeService) \Szurubooru\Services\TimeService $timeService,
\Szurubooru\Services\TokenService $tokenService)
{ {
$this->config = $config; $this->config = $config;
$this->validator = $validator; $this->validator = $validator;
@ -30,6 +32,20 @@ class UserService
$this->emailService = $emailService; $this->emailService = $emailService;
$this->fileService = $fileService; $this->fileService = $fileService;
$this->timeService = $timeService; $this->timeService = $timeService;
$this->tokenService = $tokenService;
}
public function getByNameOrEmail($userNameOrEmail, $allowUnconfirmed = false)
{
$user = $this->userDao->getByName($userNameOrEmail);
if ($user)
return $user;
$user = $this->userDao->getByEmail($userNameOrEmail, $allowUnconfirmed);
if ($user)
return $user;
throw new \InvalidArgumentException('User "' . $userNameOrEmail . '" was not found.');
} }
public function getByName($userName) public function getByName($userName)
@ -62,12 +78,15 @@ class UserService
$this->validator->validatePassword($formData->password); $this->validator->validatePassword($formData->password);
$this->validator->validateEmail($formData->email); $this->validator->validateEmail($formData->email);
if ($formData->email and $this->userDao->getByEmail($formData->email))
throw new \DomainException('User with this e-mail already exists.');
if ($this->userDao->getByName($formData->userName)) if ($this->userDao->getByName($formData->userName))
throw new \DomainException('User with this name already exists.'); throw new \DomainException('User with this name already exists.');
$user = new \Szurubooru\Entities\User(); $user = new \Szurubooru\Entities\User();
$user->name = $formData->userName; $user->name = $formData->userName;
$user->email = $formData->email; $user->emailUnconfirmed = $formData->email;
$user->passwordHash = $this->passwordService->getHash($formData->password); $user->passwordHash = $this->passwordService->getHash($formData->password);
$user->accessRank = $this->userDao->hasAnyUsers() $user->accessRank = $this->userDao->hasAnyUsers()
? \Szurubooru\Entities\User::ACCESS_RANK_REGULAR_USER ? \Szurubooru\Entities\User::ACCESS_RANK_REGULAR_USER
@ -76,15 +95,13 @@ class UserService
$user->lastLoginTime = null; $user->lastLoginTime = null;
$user->avatarStyle = \Szurubooru\Entities\User::AVATAR_STYLE_GRAVATAR; $user->avatarStyle = \Szurubooru\Entities\User::AVATAR_STYLE_GRAVATAR;
$this->sendActivationMailIfNeeded($user); $this->sendActivationEmailIfNeeded($user);
return $this->userDao->save($user); return $this->userDao->save($user);
} }
public function updateUser($userName, \Szurubooru\FormData\UserEditFormData $formData) public function updateUser(\Szurubooru\Entities\User $user, \Szurubooru\FormData\UserEditFormData $formData)
{ {
$user = $this->getByName($userName);
if ($formData->avatarStyle !== null) if ($formData->avatarStyle !== null)
{ {
$user->avatarStyle = \Szurubooru\Helpers\EnumHelper::avatarStyleFromString($formData->avatarStyle); $user->avatarStyle = \Szurubooru\Helpers\EnumHelper::avatarStyleFromString($formData->avatarStyle);
@ -95,7 +112,8 @@ class UserService
if ($formData->userName !== null and $formData->userName != $user->name) if ($formData->userName !== null and $formData->userName != $user->name)
{ {
$this->validator->validateUserName($formData->userName); $this->validator->validateUserName($formData->userName);
if ($this->userDao->getByName($formData->userName)) $userWithThisEmail = $this->userDao->getByName($formData->userName);
if ($userWithThisEmail and $userWithThisEmail->id != $user->id)
throw new \DomainException('User with this name already exists.'); throw new \DomainException('User with this name already exists.');
$user->name = $formData->userName; $user->name = $formData->userName;
@ -107,10 +125,13 @@ class UserService
$user->passwordHash = $this->passwordService->getHash($formData->password); $user->passwordHash = $this->passwordService->getHash($formData->password);
} }
if ($formData->email !== null) if ($formData->email !== null and $formData->email != $user->email)
{ {
$this->validator->validateEmail($formData->email); $this->validator->validateEmail($formData->email);
$user->email = $formData->email; if ($this->userDao->getByEmail($formData->email))
throw new \DomainException('User with this e-mail already exists.');
$user->emailUnconfirmed = $formData->email;
} }
if ($formData->accessRank !== null) if ($formData->accessRank !== null)
@ -128,17 +149,15 @@ class UserService
} }
if ($formData->email !== null) if ($formData->email !== null)
$this->sendActivationMailIfNeeded($user); $this->sendActivationEmailIfNeeded($user);
return $this->userDao->save($user); return $this->userDao->save($user);
} }
public function deleteUserByName($userName) public function deleteUser(\Szurubooru\Entities\User $user)
{ {
$user = $this->getByName($userName); $this->userDao->deleteById($user->id);
$this->userDao->deleteByName($userName);
$this->fileService->delete($this->getCustomAvatarSourcePath($user)); $this->fileService->delete($this->getCustomAvatarSourcePath($user));
return true;
} }
public function getCustomAvatarSourcePath(\Szurubooru\Entities\User $user) public function getCustomAvatarSourcePath(\Szurubooru\Entities\User $user)
@ -157,8 +176,70 @@ class UserService
$this->userDao->save($user); $this->userDao->save($user);
} }
private function sendActivationMailIfNeeded(\Szurubooru\Entities\User &$user) public function sendPasswordResetEmail(\Szurubooru\Entities\User $user)
{ {
//todo $token = $this->tokenService->createAndSaveToken($user->name, \Szurubooru\Entities\Token::PURPOSE_PASSWORD_RESET);
$this->emailService->sendPasswordResetEmail($user, $token);
}
public function finishPasswordReset($tokenName)
{
$token = $this->tokenService->getByName($tokenName);
if ($token->purpose != \Szurubooru\Entities\Token::PURPOSE_PASSWORD_RESET)
throw new \Exception('This token is not a password reset token.');
$user = $this->getByName($token->additionalData);
$newPassword = $this->passwordService->getRandomPassword();
$user->passwordHash = $this->passwordService->getHash($newPassword);
$this->userDao->save($user);
$this->tokenService->invalidateByName($token->name);
return $newPassword;
}
public function sendActivationEmail(\Szurubooru\Entities\User $user)
{
$token = $this->tokenService->createAndSaveToken($user->name, \Szurubooru\Entities\Token::PURPOSE_ACTIVATE);
$this->emailService->sendActivationEmail($user, $token);
}
public function finishActivation($tokenName)
{
$token = $this->tokenService->getByName($tokenName);
if ($token->purpose != \Szurubooru\Entities\Token::PURPOSE_ACTIVATE)
throw new \Exception('This token is not an activation token.');
$user = $this->getByName($token->additionalData);
$this->confirmEmail($user);
$this->tokenService->invalidateByName($token->name);
}
private function sendActivationEmailIfNeeded(\Szurubooru\Entities\User &$user)
{
if ($user->accessRank == \Szurubooru\Entities\User::ACCESS_RANK_ADMINISTRATOR or !$this->config->security->needEmailActivationToRegister)
{
$this->confirmEmail($user);
}
else
{
$this->sendActivationEmail($user);
}
}
private function confirmEmail(\Szurubooru\Entities\User &$user)
{
//security issue:
//1. two users set their unconfirmed mail to godzilla@empire.gov
//2. activation mail is sent to both of them
//3. first user confirms, ok
//4. second user confirms, ok
//5. two users share the same mail --> problem.
//by checking here again for users with such mail, this problem is solved with first-come first-serve approach:
//whoever confirms e-mail first, wins.
if ($this->userDao->getByEmail($user->emailUnconfirmed))
throw new \DomainException('This e-mail was already confirmed by someone else in the meantime.');
$user->email = $user->emailUnconfirmed;
$user->emailUnconfirmed = null;
$this->userDao->save($user);
} }
} }

View file

@ -3,7 +3,7 @@ namespace Szurubooru\Tests\Services;
class AuthServiceTest extends \Szurubooru\Tests\AbstractTestCase class AuthServiceTest extends \Szurubooru\Tests\AbstractTestCase
{ {
private $validatorMock; private $configMock;
private $passwordServiceMock; private $passwordServiceMock;
private $timeServiceMock; private $timeServiceMock;
private $tokenServiceMock; private $tokenServiceMock;
@ -11,7 +11,7 @@ class AuthServiceTest extends \Szurubooru\Tests\AbstractTestCase
public function setUp() public function setUp()
{ {
$this->validatorMock = $this->mock(\Szurubooru\Validator::class); $this->configMock = $this->mockConfig();
$this->passwordServiceMock = $this->mock(\Szurubooru\Services\PasswordService::class); $this->passwordServiceMock = $this->mock(\Szurubooru\Services\PasswordService::class);
$this->timeServiceMock = $this->mock(\Szurubooru\Services\TimeService::class); $this->timeServiceMock = $this->mock(\Szurubooru\Services\TimeService::class);
$this->tokenServiceMock = $this->mock(\Szurubooru\Services\TokenService::class); $this->tokenServiceMock = $this->mock(\Szurubooru\Services\TokenService::class);
@ -20,12 +20,13 @@ class AuthServiceTest extends \Szurubooru\Tests\AbstractTestCase
public function testInvalidPassword() public function testInvalidPassword()
{ {
$this->configMock->set('security/needEmailActivationToRegister', false);
$this->passwordServiceMock->method('getHash')->willReturn('unmatchingHash'); $this->passwordServiceMock->method('getHash')->willReturn('unmatchingHash');
$testUser = new \Szurubooru\Entities\User(); $testUser = new \Szurubooru\Entities\User();
$testUser->name = 'dummy'; $testUser->name = 'dummy';
$testUser->passwordHash = 'hash'; $testUser->passwordHash = 'hash';
$this->userServiceMock->expects($this->once())->method('getByName')->willReturn($testUser); $this->userServiceMock->expects($this->once())->method('getByNameOrEmail')->willReturn($testUser);
$authService = $this->getAuthService(); $authService = $this->getAuthService();
$this->setExpectedException(\Exception::class, 'Specified password is invalid'); $this->setExpectedException(\Exception::class, 'Specified password is invalid');
@ -34,17 +35,19 @@ class AuthServiceTest extends \Szurubooru\Tests\AbstractTestCase
public function testValidCredentials() public function testValidCredentials()
{ {
$this->configMock->set('security/needEmailActivationToRegister', false);
$this->passwordServiceMock->method('getHash')->willReturn('hash'); $this->passwordServiceMock->method('getHash')->willReturn('hash');
$testUser = new \Szurubooru\Entities\User(); $testUser = new \Szurubooru\Entities\User();
$testUser->id = 'an unusual database identifier';
$testUser->name = 'dummy'; $testUser->name = 'dummy';
$testUser->passwordHash = 'hash'; $testUser->passwordHash = 'hash';
$this->userServiceMock->expects($this->once())->method('getByName')->willReturn($testUser); $this->userServiceMock->expects($this->once())->method('getByNameOrEmail')->willReturn($testUser);
$testToken = new \Szurubooru\Entities\Token(); $testToken = new \Szurubooru\Entities\Token();
$testToken->name = 'mummy'; $testToken->name = 'mummy';
$this->tokenServiceMock->expects($this->once())->method('createAndSaveToken')->with( $this->tokenServiceMock->expects($this->once())->method('createAndSaveToken')->with(
$testUser, $testUser->id,
\Szurubooru\Entities\Token::PURPOSE_LOGIN)->willReturn($testToken); \Szurubooru\Entities\Token::PURPOSE_LOGIN)->willReturn($testToken);
$authService = $this->getAuthService(); $authService = $this->getAuthService();
@ -56,8 +59,28 @@ class AuthServiceTest extends \Szurubooru\Tests\AbstractTestCase
$this->assertEquals('mummy', $authService->getLoginToken()->name); $this->assertEquals('mummy', $authService->getLoginToken()->name);
} }
public function testValidCredentialsUnconfirmedEmail()
{
$this->configMock->set('security/needEmailActivationToRegister', true);
$this->passwordServiceMock->method('getHash')->willReturn('hash');
$testUser = new \Szurubooru\Entities\User();
$testUser->name = 'dummy';
$testUser->passwordHash = 'hash';
$this->userServiceMock->expects($this->once())->method('getByNameOrEmail')->willReturn($testUser);
$this->setExpectedException(\Exception::class, 'User didn\'t confirm mail yet');
$authService = $this->getAuthService();
$authService->loginFromCredentials('dummy', 'godzilla');
$this->assertFalse($authService->isLoggedIn());
$this->assertNull($testUser, $authService->getLoggedInUser());
$this->assertNull($authService->getLoginToken());
}
public function testInvalidToken() public function testInvalidToken()
{ {
$this->configMock->set('security/needEmailActivationToRegister', false);
$this->tokenServiceMock->expects($this->once())->method('getByName')->willReturn(null); $this->tokenServiceMock->expects($this->once())->method('getByName')->willReturn(null);
$this->setExpectedException(\Exception::class); $this->setExpectedException(\Exception::class);
@ -67,6 +90,7 @@ class AuthServiceTest extends \Szurubooru\Tests\AbstractTestCase
public function testValidToken() public function testValidToken()
{ {
$this->configMock->set('security/needEmailActivationToRegister', false);
$testUser = new \Szurubooru\Entities\User(); $testUser = new \Szurubooru\Entities\User();
$testUser->id = 5; $testUser->id = 5;
$testUser->name = 'dummy'; $testUser->name = 'dummy';
@ -87,10 +111,51 @@ class AuthServiceTest extends \Szurubooru\Tests\AbstractTestCase
$this->assertEquals('dummy_token', $authService->getLoginToken()->name); $this->assertEquals('dummy_token', $authService->getLoginToken()->name);
} }
public function testValidTokenInvalidPurpose()
{
$this->configMock->set('security/needEmailActivationToRegister', false);
$testToken = new \Szurubooru\Entities\Token();
$testToken->name = 'dummy_token';
$testToken->additionalData = 'whatever';
$testToken->purpose = null;
$this->tokenServiceMock->expects($this->once())->method('getByName')->willReturn($testToken);
$this->setExpectedException(\Exception::class, 'This token is not a login token');
$authService = $this->getAuthService();
$authService->loginFromToken($testToken->name);
$this->assertFalse($authService->isLoggedIn());
$this->assertNull($authService->getLoggedInUser());
$this->assertNull($authService->getLoginToken());
}
public function testValidTokenUnconfirmedEmail()
{
$this->configMock->set('security/needEmailActivationToRegister', true);
$testUser = new \Szurubooru\Entities\User();
$testUser->id = 5;
$testUser->name = 'dummy';
$this->userServiceMock->expects($this->once())->method('getById')->willReturn($testUser);
$testToken = new \Szurubooru\Entities\Token();
$testToken->name = 'dummy_token';
$testToken->additionalData = $testUser->id;
$testToken->purpose = \Szurubooru\Entities\Token::PURPOSE_LOGIN;
$this->tokenServiceMock->expects($this->once())->method('getByName')->willReturn($testToken);
$this->setExpectedException(\Exception::class, 'User didn\'t confirm mail yet');
$authService = $this->getAuthService();
$authService->loginFromToken($testToken->name);
$this->assertFalse($authService->isLoggedIn());
$this->assertNull($testUser, $authService->getLoggedInUser());
$this->assertNull($authService->getLoginToken());
}
private function getAuthService() private function getAuthService()
{ {
return new \Szurubooru\Services\AuthService( return new \Szurubooru\Services\AuthService(
$this->validatorMock, $this->configMock,
$this->passwordServiceMock, $this->passwordServiceMock,
$this->timeServiceMock, $this->timeServiceMock,
$this->tokenServiceMock, $this->tokenServiceMock,

View file

@ -0,0 +1,47 @@
<?php
namespace Szurubooru\Tests\Service;
class PasswordServiceTest extends \Szurubooru\Tests\AbstractTestCase
{
public function testGeneratingPasswords()
{
$configMock = $this->mockConfig();
$passwordService = new \Szurubooru\Services\PasswordService($configMock);
$sampleCount = 10000;
$distribution = [];
for ($i = 0; $i < $sampleCount; $i ++)
{
$password = $passwordService->getRandomPassword();
for ($j = 0; $j < strlen($password); $j ++)
{
$c = $password{$j};
if (!isset($distribution[$j]))
$distribution[$j] = [$c => 1];
elseif (!isset($distribution[$j][$c]))
$distribution[$j][$c] = 1;
else
$distribution[$j][$c] ++;
}
}
foreach ($distribution as $index => $characterDistribution)
{
$this->assertLessThan(10, $this->getRelativeStandardDeviation($characterDistribution));
}
}
private function getStandardDeviation($sample)
{
$mean = array_sum($sample) / count($sample);
foreach ($sample as $key => $num)
$devs[$key] = pow($num - $mean, 2);
return sqrt(array_sum($devs) / (count($devs)));
}
private function getRelativeStandardDeviation($sample)
{
$mean = array_sum($sample) / count($sample);
return 100 * $this->getStandardDeviation($sample) / $mean;
}
}

View file

@ -27,7 +27,7 @@ class PrivilegeServiceTest extends \Szurubooru\Tests\AbstractTestCase
$this->assertTrue($privilegeService->hasPrivilege($privilege)); $this->assertTrue($privilegeService->hasPrivilege($privilege));
} }
public function testIsLoggedInByString() public function testIsLoggedInByNameString()
{ {
$testUser1 = new \Szurubooru\Entities\User(); $testUser1 = new \Szurubooru\Entities\User();
$testUser1->name = 'dummy'; $testUser1->name = 'dummy';
@ -40,6 +40,21 @@ class PrivilegeServiceTest extends \Szurubooru\Tests\AbstractTestCase
$this->assertFalse($privilegeService->isLoggedIn($testUser2->name)); $this->assertFalse($privilegeService->isLoggedIn($testUser2->name));
} }
public function testIsLoggedInByEmailString()
{
$testUser1 = new \Szurubooru\Entities\User();
$testUser1->name = 'user1';
$testUser1->email = 'dummy';
$testUser2 = new \Szurubooru\Entities\User();
$testUser2->name = 'user2';
$testUser2->email = 'godzilla';
$this->authServiceMock->method('getLoggedInUser')->willReturn($testUser1);
$privilegeService = $this->getPrivilegeService();
$this->assertTrue($privilegeService->isLoggedIn($testUser1->email));
$this->assertFalse($privilegeService->isLoggedIn($testUser2->email));
}
public function testIsLoggedInByUser() public function testIsLoggedInByUser()
{ {
$testUser1 = new \Szurubooru\Entities\User(); $testUser1 = new \Szurubooru\Entities\User();

View file

@ -11,6 +11,7 @@ final class UserServiceTest extends \Szurubooru\Tests\AbstractTestCase
private $emailServiceMock; private $emailServiceMock;
private $fileServiceMock; private $fileServiceMock;
private $timeServiceMock; private $timeServiceMock;
private $tokenServiceMock;
public function setUp() public function setUp()
{ {
@ -22,6 +23,7 @@ final class UserServiceTest extends \Szurubooru\Tests\AbstractTestCase
$this->emailServiceMock = $this->mock(\Szurubooru\Services\EmailService::class); $this->emailServiceMock = $this->mock(\Szurubooru\Services\EmailService::class);
$this->fileServiceMock = $this->mock(\Szurubooru\Services\FileService::class); $this->fileServiceMock = $this->mock(\Szurubooru\Services\FileService::class);
$this->timeServiceMock = $this->mock(\Szurubooru\Services\TimeService::class); $this->timeServiceMock = $this->mock(\Szurubooru\Services\TimeService::class);
$this->tokenServiceMock = $this->mock(\Szurubooru\Services\TokenService::class);
} }
public function testGettingByName() public function testGettingByName()
@ -78,23 +80,56 @@ final class UserServiceTest extends \Szurubooru\Tests\AbstractTestCase
$this->assertEquals($expected, $actual); $this->assertEquals($expected, $actual);
} }
public function testValidRegistration() public function testValidRegistrationWithoutMailActivation()
{ {
$formData = new \Szurubooru\FormData\RegistrationFormData; $formData = new \Szurubooru\FormData\RegistrationFormData;
$formData->userName = 'user'; $formData->userName = 'user';
$formData->password = 'password'; $formData->password = 'password';
$formData->email = 'email'; $formData->email = 'human@people.gov';
$this->configMock->set('security/needEmailActivationToRegister', false);
$this->passwordServiceMock->method('getHash')->willReturn('hash'); $this->passwordServiceMock->method('getHash')->willReturn('hash');
$this->timeServiceMock->method('getCurrentTime')->willReturn('now'); $this->timeServiceMock->method('getCurrentTime')->willReturn('now');
$this->userDaoMock->method('hasAnyUsers')->willReturn(true); $this->userDaoMock->method('hasAnyUsers')->willReturn(true);
$this->userDaoMock->method('save')->will($this->returnArgument(0)); $this->userDaoMock->method('save')->will($this->returnArgument(0));
$this->emailServiceMock->expects($this->never())->method('sendActivationEmail');
$userService = $this->getUserService(); $userService = $this->getUserService();
$savedUser = $userService->createUser($formData); $savedUser = $userService->createUser($formData);
$this->assertEquals('user', $savedUser->name); $this->assertEquals('user', $savedUser->name);
$this->assertEquals('email', $savedUser->email); $this->assertEquals('human@people.gov', $savedUser->email);
$this->assertNull($savedUser->emailUnconfirmed);
$this->assertEquals('hash', $savedUser->passwordHash);
$this->assertEquals(\Szurubooru\Entities\User::ACCESS_RANK_REGULAR_USER, $savedUser->accessRank);
$this->assertEquals('now', $savedUser->registrationTime);
}
public function testValidRegistrationWithMailActivation()
{
$formData = new \Szurubooru\FormData\RegistrationFormData;
$formData->userName = 'user';
$formData->password = 'password';
$formData->email = 'human@people.gov';
$this->configMock->set('security/needEmailActivationToRegister', true);
$this->passwordServiceMock->method('getHash')->willReturn('hash');
$this->timeServiceMock->method('getCurrentTime')->willReturn('now');
$this->userDaoMock->method('hasAnyUsers')->willReturn(true);
$this->userDaoMock->method('save')->will($this->returnArgument(0));
$testToken = new \Szurubooru\Entities\Token();
$this->tokenServiceMock->expects($this->once())->method('createAndSaveToken')->willReturn($testToken);
$this->emailServiceMock->expects($this->once())->method('sendActivationEmail')->with(
$this->anything(),
$testToken);
$userService = $this->getUserService();
$savedUser = $userService->createUser($formData);
$this->assertEquals('user', $savedUser->name);
$this->assertNull($savedUser->email);
$this->assertEquals('human@people.gov', $savedUser->emailUnconfirmed);
$this->assertEquals('hash', $savedUser->passwordHash); $this->assertEquals('hash', $savedUser->passwordHash);
$this->assertEquals(\Szurubooru\Entities\User::ACCESS_RANK_REGULAR_USER, $savedUser->accessRank); $this->assertEquals(\Szurubooru\Entities\User::ACCESS_RANK_REGULAR_USER, $savedUser->accessRank);
$this->assertEquals('now', $savedUser->registrationTime); $this->assertEquals('now', $savedUser->registrationTime);
@ -107,6 +142,7 @@ final class UserServiceTest extends \Szurubooru\Tests\AbstractTestCase
$formData->password = 'password'; $formData->password = 'password';
$formData->email = 'email'; $formData->email = 'email';
$this->configMock->set('security/needEmailActivationToRegister', false);
$this->userDaoMock->method('hasAnyUsers')->willReturn(false); $this->userDaoMock->method('hasAnyUsers')->willReturn(false);
$this->userDaoMock->method('save')->will($this->returnArgument(0)); $this->userDaoMock->method('save')->will($this->returnArgument(0));
@ -143,6 +179,7 @@ final class UserServiceTest extends \Szurubooru\Tests\AbstractTestCase
$this->passwordServiceMock, $this->passwordServiceMock,
$this->emailServiceMock, $this->emailServiceMock,
$this->fileServiceMock, $this->fileServiceMock,
$this->timeServiceMock); $this->timeServiceMock,
$this->tokenServiceMock);
} }
} }