Add user invitations

This commit is contained in:
Frederic Guillot 2017-01-22 22:38:00 -05:00
parent 2f43d365a0
commit 10d96bfd66
20 changed files with 349 additions and 19 deletions

View File

@ -1,3 +1,15 @@
Version 1.0.38 (unreleased)
---------------------------
New features:
* User invites
Improvements:
Bug fixes:
Version 1.0.37 (Jan 14, 2017)
-----------------------------

View File

@ -54,7 +54,7 @@ class UserCreationController extends BaseController
*
* @param array $values
*/
private function createUser(array $values)
protected function createUser(array $values)
{
$project_id = empty($values['project_id']) ? 0 : $values['project_id'];
unset($values['project_id']);

View File

@ -0,0 +1,107 @@
<?php
namespace Kanboard\Controller;
use Kanboard\Core\Controller\PageNotFoundException;
use Kanboard\Core\Security\Role;
use Kanboard\Notification\MailNotification;
/**
* Class UserInviteController
*
* @package Kanboard\Controller
* @author Frederic Guillot
*/
class UserInviteController extends BaseController
{
public function show(array $values = array(), array $errors = array())
{
$this->response->html($this->template->render('user_invite/show', array(
'projects' => $this->projectModel->getList(),
'errors' => $errors,
'values' => $values,
)));
}
public function save()
{
$values = $this->request->getValues();
if (! empty($values['emails']) && isset($values['project_id'])) {
$emails = explode("\r\n", trim($values['emails']));
$nb = $this->inviteModel->createInvites($emails, $values['project_id']);
$this->flash->success($nb > 1 ? t('%d invitations were sent.', $nb) : t('%d invitation was sent.', $nb));
}
$this->response->redirect($this->helper->url->to('UserListController', 'show'));
}
public function signup(array $values = array(), array $errors = array())
{
$invite = $this->getInvite();
$this->response->html($this->helper->layout->app('user_invite/signup', array(
'no_layout' => true,
'not_editable' => true,
'token' => $invite['token'],
'errors' => $errors,
'values' => $values + array('email' => $invite['email']),
'timezones' => $this->timezoneModel->getTimezones(true),
'languages' => $this->languageModel->getLanguages(true),
)));
}
public function register()
{
$invite = $this->getInvite();
$values = $this->request->getValues();
list($valid, $errors) = $this->userValidator->validateCreation($values);
if ($valid) {
$this->createUser($invite, $values);
} else {
$this->signup($values, $errors);
}
}
protected function getInvite()
{
$token = $this->request->getStringParam('token');
if (empty($token)) {
throw PageNotFoundException::getInstance()->withoutLayout();
}
$invite = $this->inviteModel->getByToken($token);
if (empty($invite)) {
throw PageNotFoundException::getInstance()->withoutLayout();
}
return $invite;
}
protected function createUser(array $invite, array $values)
{
$user_id = $this->userModel->create($values);
if ($user_id !== false) {
if ($invite['project_id'] != 0) {
$this->projectUserRoleModel->addUser($invite['project_id'], $user_id, Role::PROJECT_MEMBER);
}
if (! empty($values['notifications_enabled'])) {
$this->userNotificationTypeModel->saveSelectedTypes($user_id, array(MailNotification::TYPE));
}
$this->inviteModel->remove($invite['email']);
$this->flash->success(t('User created successfully.'));
$this->response->redirect($this->helper->url->to('AuthController', 'login'));
} else {
$this->flash->failure(t('Unable to create this user.'));
$this->response->redirect($this->helper->url->to('UserInviteController', 'signup'));
}
}
}

View File

@ -94,6 +94,7 @@ use Pimple\Container;
* @property \Kanboard\Model\ProjectFileModel $projectFileModel
* @property \Kanboard\Model\GroupModel $groupModel
* @property \Kanboard\Model\GroupMemberModel $groupMemberModel
* @property \Kanboard\Model\InviteModel $inviteModel
* @property \Kanboard\Model\LanguageModel $languageModel
* @property \Kanboard\Model\LastLoginModel $lastLoginModel
* @property \Kanboard\Model\LinkModel $linkModel

View File

@ -29,12 +29,12 @@ class AppHelper extends Base
*
* @access public
* @param string $param
* @param mixed $default_value
* @param mixed $default
* @return mixed
*/
public function config($param, $default_value = '')
public function config($param, $default = '')
{
return $this->configModel->get($param, $default_value);
return $this->configModel->get($param, $default);
}
/**

73
app/Model/InviteModel.php Normal file
View File

@ -0,0 +1,73 @@
<?php
namespace Kanboard\Model;
use Kanboard\Core\Base;
use Kanboard\Core\Security\Token;
/**
* Class InviteModel
*
* @package Kanboard\Model
* @author Frederic Guillot
*/
class InviteModel extends Base
{
const TABLE = 'invites';
public function createInvites(array $emails, $projectId)
{
$emails = array_unique($emails);
$nb = 0;
foreach ($emails as $email) {
$email = trim($email);
if (! empty($email) && $this->createInvite($email, $projectId)) {
$nb++;
}
}
return $nb;
}
protected function createInvite($email, $projectId)
{
$values = array(
'email' => $email,
'project_id' => $projectId,
'token' => Token::getToken(),
);
if ($this->db->table(self::TABLE)->insert($values)) {
$this->sendInvite($values);
return true;
}
return false;
}
protected function sendInvite(array $values)
{
$this->emailClient->send(
$values['email'],
$values['email'],
e('Kanboard Invitation'),
$this->template->render('user_invite/email', array('token' => $values['token']))
);
}
public function getByToken($token)
{
return $this->db->table(self::TABLE)
->eq('token', $token)
->findOne();
}
public function remove($email)
{
return $this->db->table(self::TABLE)
->eq('email', $email)
->remove();
}
}

View File

@ -6,14 +6,24 @@ use PDO;
use Kanboard\Core\Security\Token;
use Kanboard\Core\Security\Role;
const VERSION = 119;
const VERSION = 120;
function version_120(PDO $pdo)
{
$pdo->exec("
CREATE TABLE invites (
email VARCHAR(255) NOT NULL,
project_id INTEGER NOT NULL,
token VARCHAR(255) NOT NULL,
PRIMARY KEY(email, token)
) ENGINE=InnoDB CHARSET=utf8
");
}
function version_119(PDO $pdo)
{
$pdo->exec('ALTER TABLE `comments` ADD COLUMN `date_modification` BIGINT(20)');
$pdo->exec('UPDATE `comments`
SET `date_modification` = `date_creation`
WHERE `date_modification` IS NULL');
$pdo->exec('UPDATE `comments` SET `date_modification` = `date_creation` WHERE `date_modification` IS NULL');
}
function version_118(PDO $pdo)

View File

@ -6,14 +6,24 @@ use PDO;
use Kanboard\Core\Security\Token;
use Kanboard\Core\Security\Role;
const VERSION = 98;
const VERSION = 99;
function version_99(PDO $pdo)
{
$pdo->exec("
CREATE TABLE invites (
email VARCHAR(255) NOT NULL,
project_id INTEGER NOT NULL,
token VARCHAR(255) NOT NULL,
PRIMARY KEY(email, token)
)
");
}
function version_98(PDO $pdo)
{
$pdo->exec('ALTER TABLE "comments" ADD COLUMN date_modification BIGINT');
$pdo->exec('UPDATE "comments"
SET date_modification = date_creation
WHERE date_modification IS NULL');
$pdo->exec('UPDATE "comments" SET date_modification = date_creation WHERE date_modification IS NULL');
}
function version_97(PDO $pdo)

View File

@ -6,14 +6,24 @@ use Kanboard\Core\Security\Token;
use Kanboard\Core\Security\Role;
use PDO;
const VERSION = 109;
const VERSION = 110;
function version_110(PDO $pdo)
{
$pdo->exec("
CREATE TABLE invites (
email TEXT NOT NULL,
project_id INTEGER NOT NULL,
token TEXT NOT NULL,
PRIMARY KEY(email, token)
)
");
}
function version_109(PDO $pdo)
{
$pdo->exec('ALTER TABLE comments ADD COLUMN date_modification INTEGER');
$pdo->exec('UPDATE comments
SET date_modification = date_creation
WHERE date_modification IS NULL;');
$pdo->exec('UPDATE comments SET date_modification = date_creation WHERE date_modification IS NULL;');
}
function version_108(PDO $pdo)

View File

@ -136,6 +136,7 @@ class AuthenticationProvider implements ServiceProviderInterface
$acl->add('ICalendarController', '*', Role::APP_PUBLIC);
$acl->add('FeedController', '*', Role::APP_PUBLIC);
$acl->add('AvatarFileController', array('show', 'image'), Role::APP_PUBLIC);
$acl->add('UserInviteController', array('signup', 'register'), Role::APP_PUBLIC);
$acl->add('ConfigController', '*', Role::APP_ADMIN);
$acl->add('TagController', '*', Role::APP_ADMIN);

View File

@ -42,6 +42,7 @@ class ClassProvider implements ServiceProviderInterface
'CustomFilterModel',
'GroupModel',
'GroupMemberModel',
'InviteModel',
'LanguageModel',
'LastLoginModel',
'LinkModel',

View File

@ -53,6 +53,7 @@
>
<?php if (isset($no_layout) && $no_layout): ?>
<?= $this->app->flashMessage() ?>
<?= $content_for_layout ?>
<?php else: ?>
<?= $this->hook->render('template:layout:top') ?>

View File

@ -0,0 +1,12 @@
<p>
<?= t('You have been invited to register on Kanboard.') ?>
</p>
<p>
<?= $this->url->absoluteLink(t('Click here to join your team'), 'UserInviteController', 'signup', array('token' => $token)) ?>
</p>
<?php if ($this->app->config('application_url')): ?>
<hr>
<a href="<?= $this->app->config('application_url') ?>">Kanboard</a>
<?php endif ?>

View File

@ -0,0 +1,15 @@
<div class="page-header">
<h2><?= t('Invite people') ?></h2>
</div>
<form method="post" action="<?= $this->url->href('UserInviteController', 'save') ?>" autocomplete="off">
<?= $this->form->csrf() ?>
<?= $this->form->label(t('Emails'), 'emails') ?>
<?= $this->form->textarea('emails', $values, $errors, array('required', 'autofocus')) ?>
<p class="form-help"><?= t('Enter one email address by line.') ?></p>
<?= $this->form->label(t('Add these people to this project'), 'project_id') ?>
<?= $this->form->select('project_id', $projects, $values, $errors) ?>
<?= $this->modal->submitButtons() ?>
</form>

View File

@ -0,0 +1,46 @@
<div class="form-login">
<div class="page-header">
<h2><?= t('Sign-up') ?></h2>
</div>
<form method="post" action="<?= $this->url->href('UserInviteController', 'register', array('token' => $token)) ?>" autocomplete="off">
<?= $this->form->csrf() ?>
<fieldset>
<legend><?= t('Profile') ?></legend>
<?= $this->form->label(t('Username'), 'username') ?>
<?= $this->form->text('username', $values, $errors, array('autofocus', 'required', 'maxlength="50"')) ?>
<?= $this->form->label(t('Name'), 'name') ?>
<?= $this->form->text('name', $values, $errors) ?>
<?= $this->form->label(t('Email'), 'email') ?>
<?= $this->form->email('email', $values, $errors, array('required')) ?>
</fieldset>
<fieldset>
<legend><?= t('Credentials') ?></legend>
<?= $this->form->label(t('Password'), 'password') ?>
<?= $this->form->password('password', $values, $errors, array('required')) ?>
<?= $this->form->label(t('Confirmation'), 'confirmation') ?>
<?= $this->form->password('confirmation', $values, $errors, array('required')) ?>
</fieldset>
<fieldset>
<legend><?= t('Preferences') ?></legend>
<?= $this->form->label(t('Timezone'), 'timezone') ?>
<?= $this->form->select('timezone', $timezones, $values, $errors) ?>
<?= $this->form->label(t('Language'), 'language') ?>
<?= $this->form->select('language', $languages, $values, $errors) ?>
<?= $this->form->checkbox('notifications_enabled', t('Enable email notifications'), 1, isset($values['notifications_enabled']) && $values['notifications_enabled'] == 1 ? true : false) ?>
</fieldset>
<div class="form-actions">
<button class="btn btn-blue"><?= t('Sign-up') ?></button>
</div>
</form>
</div>

View File

@ -5,6 +5,9 @@
<li>
<?= $this->modal->medium('plus', t('New user'), 'UserCreationController', 'show') ?>
</li>
<li>
<?= $this->modal->medium('paper-plane', t('Invite people'), 'UserInviteController', 'show') ?>
</li>
<li>
<?= $this->modal->medium('upload', t('Import'), 'UserImportController', 'show') ?>
</li>

View File

@ -25,7 +25,7 @@ class UserValidator extends BaseValidator
return array(
new Validators\MaxLength('role', t('The maximum length is %d characters', 25), 25),
new Validators\MaxLength('username', t('The maximum length is %d characters', 50), 50),
new Validators\Unique('username', t('The username must be unique'), $this->db->getConnection(), UserModel::TABLE, 'id'),
new Validators\Unique('username', t('This username is already taken'), $this->db->getConnection(), UserModel::TABLE, 'id'),
new Validators\Email('email', t('Email address invalid')),
new Validators\Integer('is_ldap_user', t('This value must be an integer')),
);

File diff suppressed because one or more lines are too long

View File

@ -144,7 +144,7 @@ ul.form-errors li
.form-login
max-width: 350px
margin: 8% auto 0
margin: 5% auto 0
li
margin-left: 25px
line-height: 25px
@ -154,5 +154,6 @@ ul.form-errors li
.reset-password
margin-top: 20px
margin-bottom: 20px
a
color: color('light')

View File

@ -0,0 +1,27 @@
<?php
use Kanboard\Model\InviteModel;
require_once __DIR__.'/../Base.php';
class InviteModelTest extends Base
{
public function testCreation()
{
$inviteModel = new InviteModel($this->container);
$this->container['emailClient']
->expects($this->exactly(2))
->method('send');
$inviteModel->createInvites(array('user@domain1.tld', '', 'user@domain2.tld'), 1);
}
public function testRemove()
{
$inviteModel = new InviteModel($this->container);
$inviteModel->createInvites(array('user@domain1.tld', 'user@domain2.tld'), 0);
$this->assertTrue($inviteModel->remove('user@domain1.tld'));
$this->assertFalse($inviteModel->remove('foobar'));
}
}