diff --git a/app/assets/less/application.less b/app/assets/less/application.less index 850a879..d851765 100644 --- a/app/assets/less/application.less +++ b/app/assets/less/application.less @@ -33,6 +33,7 @@ @import "components/button"; @import "components/badge"; @import "components/section"; +@import "components/spacer"; @import "components/pagination"; @import "components/blankslate"; @import "components/request-item"; @@ -42,6 +43,7 @@ @import "views/landingpage"; @import "views/about"; @import "views/login"; +@import "views/register"; // Plugins @import "vendor/ionicons/ionicons"; diff --git a/app/assets/less/components/spacer.less b/app/assets/less/components/spacer.less new file mode 100644 index 0000000..b1084d2 --- /dev/null +++ b/app/assets/less/components/spacer.less @@ -0,0 +1,6 @@ + +.spacer { + display: block; + border-bottom: 1px solid @gray-light; + margin: @spacer-margin; +} diff --git a/app/assets/less/variables.less b/app/assets/less/variables.less index d4930f7..f3a7c1d 100644 --- a/app/assets/less/variables.less +++ b/app/assets/less/variables.less @@ -154,6 +154,13 @@ @blankslate-spacing: 25px 10px; @blankslate-spacing-sm: 15px 5px; +// ---------------------------------- +// Spacer +// ---------------------------------- + +@spacer-color: @gray-light; +@spacer-margin: 1em 0; + // ---------------------------------- // Pagination // ---------------------------------- diff --git a/app/assets/less/views/register.less b/app/assets/less/views/register.less new file mode 100644 index 0000000..1cbe1c3 --- /dev/null +++ b/app/assets/less/views/register.less @@ -0,0 +1,6 @@ + +.register { + &:extend(.section all); + .center-block(); + width: 60%; +} diff --git a/app/config/routes.yml b/app/config/routes.yml index 11703ff..4c36381 100644 --- a/app/config/routes.yml +++ b/app/config/routes.yml @@ -43,6 +43,9 @@ router: oauth-disconnect-confirm: pattern: '/oauth/{provider:([a-z]+)}/disconnect/{confirm}' path: 'User::oauthdisconnect' + user-register: + pattern: '/register' + path: Auth::register user-settings: pattern: '/settings' path: diff --git a/app/controllers/AuthController.php b/app/controllers/AuthController.php index ec97e5f..012185b 100644 --- a/app/controllers/AuthController.php +++ b/app/controllers/AuthController.php @@ -2,9 +2,12 @@ namespace App\Controller; -use App\Controller\ControllerBase; +use App\Controller\ControllerBase, + App\Model\Data\User, + App\Form\Login as LoginForm, + App\Form\Registration as RegistrationForm; -use App\Form\Login as LoginForm; +use Httpcb\OAuth\UserData\UserDataInterface; class AuthController extends ControllerBase { @@ -78,17 +81,21 @@ class AuthController extends ControllerBase else { $result = $this->auth->loginOauth($data); - // There was an error when creating the account - if (is_array($result)) { - $msg = ''; - foreach ($result as $message) { - $msg .= '
  • ' . $message->getMessage() . '
  • '; + if ($result === false) { + + if (User::findFirstByEmail($data->getEmail())) { + $this->flash->error('The email address is already in use.'); + $this->response->redirect('/login'); + return; } - $this->flash->message('error', "Failed to create account: "); - $this->response->redirect('/login'); - } else { - $this->response->redirect('/'); + + $this->session->set('auth:register:data', $data); + $this->response->redirect(['for' => 'user-register']); + return; } + + // User is logged in. + $this->response->redirect('/'); } } catch(\Exception $e) { $this->flash->message('error', 'Failed to authenticate.'); @@ -106,6 +113,42 @@ class AuthController extends ControllerBase } } + public function registerAction() + { + $data = $this->session->get('auth:register:data'); + if (!($data instanceof UserDataInterface)) { + $this->response->redirect('/'); + return; + } + + $user = new User(); + $user->assign($data->toArray(), null, + [ 'email', 'username', 'firstname', 'lastname' ]); + + $form = new RegistrationForm($user); + + if ($this->request->isPost()) { + + $formData = $this->request->getPost(); + if ($form->isValid($formData)) { + $user->setOAuthId($data->getProvider(), $data->getId()); + if ($user->save()) { + $this->auth->systemLogin($user); + $this->flash->success('User successfully created. Now add your first callback!'); + $this->response->redirect('/callback/new'); + } else { + $this->flash->error('Could not create user'); + } + } + $form->setEntity($formData); + } else { + $form->isValid($data->toArray()); + } + + $this->view->provider = $data->getProvider(); + $this->view->form = $form; + } + public function logoutAction() { $this->auth->clearIdentity(); diff --git a/app/forms/Registration.php b/app/forms/Registration.php new file mode 100644 index 0000000..f6d5160 --- /dev/null +++ b/app/forms/Registration.php @@ -0,0 +1,107 @@ +setValidation(new Validation()); + + // Username + $username = new Text('username', array( + 'class' => 'form-control', + 'placeholder' => 'Username', + )); + + $username->setLabel('Username'); + + $username->addValidators([ + new AlnumValidator([ + 'message' => 'Username must contain only letters and numbers.' + ]), + new StringLengthValidator([ + 'min' => 2, + 'messageMinimum' => 'Username must be at least :min characters long.', + ]), + new CallbackValidator([ + 'callback' => function($data) { return User::findFirstByUsername($data['username']) === false; }, + 'message' => 'The username already exists.', + 'attribute' => 'username', + ]) + ]); + + $this->add($username); + + // Names + foreach([ 'first-name' => 'Firstname', 'last-name' => 'Lastname' ] as $id => $label) { + + $name = new Text($id, array( + 'class' => 'form-control', + 'placeholder' => $label, + )); + + $name->setLabel($label); + $name->addValidator(new AlphaValidator([ + 'allowSpace' => false, + 'allowEmpty' => true, + ])); + + $this->add($name); + } + + // Email + $email = new Text('email', array( + 'class' => 'form-control', + 'placeholder' => 'Email', + 'readonly' => '', + )); + + $email->addValidators([ + new IdenticalValidator([ + 'accepted' => $user->getEmail(), + ]), + new CallbackValidator([ + 'callback' => function($data) { return User::findFirstByEmail($data['email']) === false; }, + 'message' => 'This email already exist.', + ]) + ]); + + $email->setLabel('Email'); + $this->add($email); + + // Submit + $submit = new Submit('submit', array('class' => 'button button-success', 'value' => 'Register')); + $this->add($submit); + } +} diff --git a/app/library/Auth.php b/app/library/Auth.php index 2ec49ae..5adfc8c 100644 --- a/app/library/Auth.php +++ b/app/library/Auth.php @@ -44,26 +44,15 @@ class Auth extends Component * Login using OAuth * * @param UserDataInterface $data - * @return bool|\Phalcon\Mvc\Model\MessageInterface[] + * @return bool */ public function loginOauth(UserDataInterface $data) { $user = User::findFirstByOAuthID($data); + // Did not find any user. if (!$user) { - // Did not find any user. create him. - $user = User::createFromOAuthData($data); - - if ($user->save() === false) { - return $user->getMessages(); - } - } - // Here we activate the user. - // As for OAuth we perform registration if the user does not exist. - // We should therefore activate deleted accounts. - else if ($user->Status == User::STATUS_DELETED) { - $user->Status = User::STATUS_ACTIVE; - $user->save(); + return false; } $this->setIdentity($user->getId()); @@ -74,6 +63,17 @@ class Auth extends Component return true; } + /** + * The system logs in a user (without credentials). + * + * @param User $user + */ + public function systemLogin(User $user) + { + $this->setIdentity($user->getId()); + $this->eventsManager->fire('auth:onLogin', $this, 'System'); + } + /** * @param $identity * @return Auth diff --git a/app/library/Form.php b/app/library/Form.php new file mode 100644 index 0000000..ebbb8bc --- /dev/null +++ b/app/library/Form.php @@ -0,0 +1,70 @@ + 'control-label', + 'class' => [ 'col-sm-10' ], + 'message' => '' + ]; + + $ele = $this->get($name); + + if (isset($opt['label-length'])) { + $length = (int) $opt['label-length']; + } else { + $length = 2; + } + $options['label-class'] .= ' col-sm-' . $length; + + if (isset($opt['length'])) { + + $len = $opt['length']; + + if ($len === 'full') { + $options['class'] = []; + } else { + $options['class'] = [ 'col-sm-' . $len ]; + } + + unset($opt['length']); + } + + if ($ele->hasMessages()) { + $options['class'][] = 'has-error'; + $options['message'] = $ele->getMessages()->current(); + } + + return $this->_render($ele, $options); + } + + protected function _render(FormElement $ele, $opt) + { + $xhtml = ''; + + if (strlen($ele->getLabel()) > 0) { + + $xhtml .= sprintf( + '', + $opt['label-class'], $ele->getName(), $ele->getLabel()); + } + + $xhtml .= '
    ' + . $ele->render(); + + if (strlen($opt['message']) > 0) { + $xhtml .= '' . $opt['message'] . ''; + } + + $xhtml .= '
    '; + + return $xhtml; + } +} diff --git a/app/library/OAuth/UserData/Github.php b/app/library/OAuth/UserData/Github.php index a465770..0de7fea 100644 --- a/app/library/OAuth/UserData/Github.php +++ b/app/library/OAuth/UserData/Github.php @@ -2,67 +2,18 @@ namespace Httpcb\OAuth\UserData; -class Github implements UserDataInterface +class Github extends UserData { - protected $data; + protected $_provider = 'Github'; /** * {@inheritDoc} */ public function __construct(array $data) { - $this->data = $data; - } - - /** - * {@inheritDoc} - */ - public function getProvider() - { - return 'Github'; - } - - /** - * {@inheritDoc} - */ - public function getId() - { - return (int) $this->data['id']; - } - - /** - * {@inheritDoc} - */ - public function getUsername() - { - return $this->data['login']; - } - - /** - * {@inheritDoc} - */ - public function getName() - { - return $this->data['name']; - } - - public function getFirstname() - { - $pos = strpos($this->getName(), ' '); - return $pos !== false ? substr($this->getName(), 0, $pos) : $this->getName(); - } - - public function getLastname() - { - $pos = strpos($this->getName(), ' '); - return $pos !== false ? substr($this->getName(), $pos+1) : null; - } - - /** - * {@inheritDoc} - */ - public function getEmail() - { - return $this->data['email']; + $this->_id = $data['id']; + $this->_username = $data['login']; + $this->_name = $data['name']; + $this->_email = $data['email']; } } diff --git a/app/library/OAuth/UserData/Gitlab.php b/app/library/OAuth/UserData/Gitlab.php index 85f3653..9ea6d41 100644 --- a/app/library/OAuth/UserData/Gitlab.php +++ b/app/library/OAuth/UserData/Gitlab.php @@ -2,72 +2,18 @@ namespace Httpcb\OAuth\UserData; -class Gitlab implements UserDataInterface +class Gitlab extends UserData { - protected $data; + protected $_provider = 'Gitlab'; /** * {@inheritDoc} */ public function __construct(array $data) { - $this->data = $data; - } - - /** - * {@inheritDoc} - */ - public function getProvider() - { - return 'Gitlab'; - } - - /** - * {@inheritDoc} - */ - public function getId() - { - return (int) $this->data['id']; - } - - /** - * {@inheritDoc} - */ - public function getUsername() - { - return $this->data['username']; - } - - /** - * {@inheritDoc} - */ - public function getName() - { - return $this->data['name']; - } - - /** - * {@inheritDoc} - */ - public function getFirstname() - { - $pos = strpos($this->getName(), ' '); - return $pos !== false ? substr($this->getName(), 0, $pos) : $this->getName(); - } - - /** - * {@inheritDoc} - */ - public function getLastname() - { - $pos = strpos($this->getName(), ' '); - return $pos !== false ? substr($this->getName(), $pos+1) : null; - } - /** - * {@inheritDoc} - */ - public function getEmail() - { - return $this->data['email']; + $this->_id = $data['id']; + $this->_username = $data['username']; + $this->_name = $data['name']; + $this->_email = $data['email']; } } diff --git a/app/library/OAuth/UserData/Google.php b/app/library/OAuth/UserData/Google.php index 28aa7a1..096215e 100644 --- a/app/library/OAuth/UserData/Google.php +++ b/app/library/OAuth/UserData/Google.php @@ -2,76 +2,20 @@ namespace Httpcb\OAuth\UserData; -class Google implements UserDataInterface +class Google extends UserData { - protected $data; + protected $_provider = 'Google'; /** * {@inheritDoc} */ public function __construct(array $data) { - $this->data = $data; - } + $this->_id = $data['id']; + $this->_name = $data['displayName']; - /** - * {@inheritDoc} - */ - public function getProvider() - { - return 'Google'; - } - - /** - * {@inheritDoc} - */ - public function getId() - { - return (int) $this->data['id']; - } - - /** - * {@inheritDoc} - */ - public function getUsername() - { - return null; - } - - /** - * {@inheritDoc} - */ - public function getName() - { - return $this->data['displayName']; - } - - /** - * {@inheritDoc} - */ - public function getFirstname() - { - $pos = strpos($this->getName(), ' '); - return $pos !== false ? substr($this->getName(), 0, $pos) : $this->getName(); - } - - /** - * {@inheritDoc} - */ - public function getLastname() - { - $pos = strpos($this->getName(), ' '); - return $pos !== false ? substr($this->getName(), $pos+1) : null; - } - - /** - * {@inheritDoc} - */ - public function getEmail() - { - if (isset($this->data['emails'][0]['value'])) { - return $this->data['emails'][0]['value']; + if (isset($data['emails'][0]['value'])) { + $this->_email = $data['emails'][0]['value']; } - return null; } } diff --git a/app/library/OAuth/UserData/LinkedIn.php b/app/library/OAuth/UserData/LinkedIn.php index 0907453..6f1d969 100644 --- a/app/library/OAuth/UserData/LinkedIn.php +++ b/app/library/OAuth/UserData/LinkedIn.php @@ -2,78 +2,18 @@ namespace Httpcb\OAuth\UserData; -class LinkedIn implements UserDataInterface +class LinkedIn extends UserData { - protected $data; + protected $_provider = 'LinkedIn'; /** * {@inheritDoc} */ public function __construct(array $data) { - $this->data = $data; - } - - /** - * {@inheritDoc} - */ - public function getProvider() - { - return 'LinkedIn'; - } - - /** - * {@inheritDoc} - */ - public function getId() - { - return $this->data['id']; - } - - /** - * {@inheritDoc} - */ - public function getUsername() - { - return null; - } - - /** - * {@inheritDoc} - */ - public function getName() - { - $name = ''; - if ($this->getFirstname() !== null) { - $name = $this->getFirstname(); - } - if ($this->getLastname() !== null) { - $name .= ' ' . $this->getLastname(); - } - return $name; - } - - /** - * {@inheritDoc} - */ - public function getFirstname() - { - return isset($this->data['firstName']) ? $this->data['firstName'] : null; - } - - /** - * {@inheritDoc} - */ - public function getLastname() - { - return isset($this->data['lastName']) ? $this->data['lastName'] : null; - } - - /** - * {@inheritDoc} - */ - public function getEmail() - { - return $this->data['emailAddress']; + $this->_id = $data['id']; + $this->_firstname = isset($data['firstName']) ? $data['firstName'] : null; + $this->_lastname = isset($data['lastName']) ? $data['lastName'] : null; + $this->_email = $data['emailAddress']; } } diff --git a/app/library/OAuth/UserData/UserData.php b/app/library/OAuth/UserData/UserData.php new file mode 100644 index 0000000..00ba85c --- /dev/null +++ b/app/library/OAuth/UserData/UserData.php @@ -0,0 +1,104 @@ +_provider; + } + + /** + * {@inheritDoc} + */ + public function getId() + { + return $this->_id; + } + + /** + * {@inheritDoc} + */ + public function getUsername() + { + return $this->_username; + } + + /** + * {@inheritDoc} + */ + public function getName() + { + if ($this->_name === null && strlen($this->_firstname) > 0) { + $name = $this->_firstname; + if (strlen($this->_lastname) > 0) { + $name .= ' ' . $this->_lastname; + } + return $name; + } + return $this->_name; + } + + /** + * {@inheritDoc} + */ + public function getFirstname() + { + if ($this->_firstname === null) { + $pos = strpos($this->getName(), ' '); + return $pos !== false ? substr($this->getName(), 0, $pos) : $this->getName(); + + } + return $this->_firstname; + } + + /** + * {@inheritDoc} + */ + public function getLastname() + { + if ($this->_lastname === null) { + $pos = strpos($this->getName(), ' '); + return $pos !== false ? substr($this->getName(), $pos+1) : null; + } + return $this->_lastname; + } + + /** + * {@inheritDoc} + */ + public function getEmail() + { + return $this->_email; + } + + public function toArray() + { + $data = []; + foreach(get_class_methods($this) as $method) { + if (substr($method, 0, 3) == 'get') { + $field = lcfirst(substr($method, 3)); + $data[$field] = $this->$method(); + } + } + return $data; + } +} diff --git a/app/library/OAuth/UserData/UserDataInterface.php b/app/library/OAuth/UserData/UserDataInterface.php index 456b72d..358cafb 100644 --- a/app/library/OAuth/UserData/UserDataInterface.php +++ b/app/library/OAuth/UserData/UserDataInterface.php @@ -54,4 +54,9 @@ interface UserDataInterface * @return string */ public function getEmail(); + + /** + * @return array + */ + public function toArray(); } diff --git a/app/models/Data/User.php b/app/models/Data/User.php index 22a7df7..d813e8b 100644 --- a/app/models/Data/User.php +++ b/app/models/Data/User.php @@ -4,7 +4,7 @@ namespace App\Model\Data; use Phalcon\Mvc\Model, Phalcon\Validation, - Phalcon\Validation\Validator\Uniqueness, + Phalcon\Validation\Validator\Callback as CallbackValidator, InvalidArgumentException, Httpcb\OAuth\UserData\UserDataInterface; @@ -52,8 +52,14 @@ class User extends Model // Validation $validator = new Validation(); - $validator->add('username', new Uniqueness(['message' => 'The username already exists.'])); - $validator->add('email', new Uniqueness(['message' => 'The email address already exists.'])); + $validator->add('username', new CallbackValidator([ + 'callback' => function() { return $this->findFirstByUsername($this->getUsername()) === false; }, + 'message' => 'The username already exists.' + ])); + $validator->add('email', new CallbackValidator([ + 'callback' => function() { return $this->findFirstByEmail($this->getEmail()) === false; }, + 'message' => 'The email address already exists.' + ])); return $this->validate($validator); } @@ -295,6 +301,20 @@ class User extends Model return $this->linkedin_id; } + /** + * @param string $provider + * @param string $id + * @return $this + */ + public function setOAuthId($provider, $id) + { + $method = 'set' . ucfirst($provider) . 'Id'; + if (method_exists($this, $method)) { + $this->$method($id); + } + return $this; + } + /** * @param string $id * @return User @@ -339,8 +359,24 @@ class User extends Model static public function findFirstByUsernameOrEmail($value) { return self::findFirst([ - "(email = :v: OR username = :v:) AND status = :s:", - "bind" => [ 'v' => $value, 's' => self::STATUS_ACTIVE ] + "(email = :v: OR username = :v:) AND status != :s:", + "bind" => [ 'v' => $value, 's' => self::STATUS_DELETED ] + ]); + } + + static public function findFirstByEmail($email) + { + return self::findFirst([ + "email = :email: AND status != :s:", + "bind" => [ 'email' => $email, 's' => self::STATUS_DELETED ] + ]); + } + + static public function findFirstByUsername($username) + { + return self::findFirst([ + "username = :username: AND status != :s:", + "bind" => [ 'username' => $username, 's' => self::STATUS_DELETED ] ]); } @@ -349,8 +385,8 @@ class User extends Model $column = strtolower($oauth->getProvider()); return self::findFirst([ - "{$column}_id = :id:", - "bind" => [ 'id' => $oauth->getId() ] + "{$column}_id = :id: AND status != :s:", + "bind" => [ 'id' => $oauth->getId(), 's' => self::STATUS_DELETED ] ]); } diff --git a/app/views/auth/register.volt b/app/views/auth/register.volt new file mode 100644 index 0000000..7ae2c3d --- /dev/null +++ b/app/views/auth/register.volt @@ -0,0 +1,43 @@ + + +
    + +

    Account registration

    + + + + + +
    + +
    + {{ form.renderDecorated('email') }} +
    + +
    + {{ form.renderDecorated('username') }} +
    + +
    + {{ form.renderDecorated('first-name', ['length': 4]) }} + {{ form.renderDecorated('last-name', ['length': 4]) }} +
    + + + +
    +
    + {{ form.render('submit') }} +
    +
    +
    +