Archived
1
0
Fork 0

Merge branch 'oauth-registration' into dev

This commit is contained in:
Henrik Hautakoski 2018-08-17 16:22:59 +02:00
commit fe542461f7
No known key found for this signature in database
GPG key ID: 839F3A7EAFAEAFAA
17 changed files with 488 additions and 275 deletions

View file

@ -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";

View file

@ -0,0 +1,6 @@
.spacer {
display: block;
border-bottom: 1px solid @gray-light;
margin: @spacer-margin;
}

View file

@ -154,6 +154,13 @@
@blankslate-spacing: 25px 10px;
@blankslate-spacing-sm: 15px 5px;
// ----------------------------------
// Spacer
// ----------------------------------
@spacer-color: @gray-light;
@spacer-margin: 1em 0;
// ----------------------------------
// Pagination
// ----------------------------------

View file

@ -0,0 +1,6 @@
.register {
&:extend(.section all);
.center-block();
width: 60%;
}

View file

@ -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:

View file

@ -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 .= '<li>' . $message->getMessage() . '</li>';
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: <ul>{$msg}</ul>");
$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();

107
app/forms/Registration.php Normal file
View file

@ -0,0 +1,107 @@
<?php
namespace App\Form;
/**
* Models
*/
use App\Model\Data\User;
/**
* Phalcon Form
*/
use Httpcb\Form as FormBase,
Phalcon\Forms\Element as FormElement;
/**
* Element types
*/
use Phalcon\Forms\Element\Text,
Phalcon\Forms\Element\Password,
Phalcon\Forms\Element\Submit;
/**
* Validators
*/
use Phalcon\Validation,
Phalcon\Validation\Validator\Callback as CallbackValidator,
Phalcon\Validation\Validator\Alnum as AlnumValidator,
Phalcon\Validation\Validator\Email as EmailValidator,
Phalcon\Validation\Validator\StringLength as StringLengthValidator,
Phalcon\Validation\Validator\Identical as IdenticalValidator,
Httpcb\Validation\Validator\Alpha as AlphaValidator;
class Registration extends FormBase
{
public function initialize(User $user)
{
$this->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);
}
}

View file

@ -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

70
app/library/Form.php Normal file
View file

@ -0,0 +1,70 @@
<?php
namespace Httpcb;
use Phalcon\Forms\Form as FormBase,
Phalcon\Forms\Element as FormElement;
class Form extends FormBase
{
public function renderDecorated($name, $opt = [])
{
$options = [
'label-class' => '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(
'<label class="%s" for="%s">%s</label>',
$opt['label-class'], $ele->getName(), $ele->getLabel());
}
$xhtml .= '<div class="' . implode(' ', $opt['class']) . '">'
. $ele->render();
if (strlen($opt['message']) > 0) {
$xhtml .= '<span class="help-block">' . $opt['message'] . '</span>';
}
$xhtml .= '</div>';
return $xhtml;
}
}

View file

@ -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'];
}
}

View file

@ -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'];
}
}

View file

@ -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;
}
}

View file

@ -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'];
}
}

View file

@ -0,0 +1,104 @@
<?php
namespace Httpcb\OAuth\UserData;
abstract class UserData implements UserDataInterface
{
protected $_provider = null;
protected $_id = null;
protected $_username = null;
protected $_email = null;
protected $_name = null;
protected $_firstname = null;
protected $_lastname = null;
/**
* {@inheritDoc}
*/
public function getProvider()
{
return $this->_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;
}
}

View file

@ -54,4 +54,9 @@ interface UserDataInterface
* @return string
*/
public function getEmail();
/**
* @return array
*/
public function toArray();
}

View file

@ -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 ]
]);
}

View file

@ -0,0 +1,43 @@
<div class="register">
<h2>Account registration</h2>
<div class="alert alert-info alert-dismissible" role="alert">
<strong>Information!</strong>
<p>
The form is prepared with the information provided by <strong>{{ provider }}</strong>.
Please check the information and make changes if necessary before continue.
</p>
</div>
<span class="spacer"></span>
<form class="form form-horizontal" method="post">
<div class="form-group">
{{ form.renderDecorated('email') }}
</div>
<div class="form-group">
{{ form.renderDecorated('username') }}
</div>
<div class="form-group">
{{ form.renderDecorated('first-name', ['length': 4]) }}
{{ form.renderDecorated('last-name', ['length': 4]) }}
</div>
<span class="spacer"></span>
<div class="form-group">
<div class="col-xs-12 col-xs-offset-2">
{{ form.render('submit') }}
</div>
</div>
</form>
</div>