Add user invitations
This commit is contained in:
parent
2f43d365a0
commit
10d96bfd66
12
ChangeLog
12
ChangeLog
|
|
@ -1,3 +1,15 @@
|
|||
Version 1.0.38 (unreleased)
|
||||
---------------------------
|
||||
|
||||
New features:
|
||||
|
||||
* User invites
|
||||
|
||||
Improvements:
|
||||
|
||||
Bug fixes:
|
||||
|
||||
|
||||
Version 1.0.37 (Jan 14, 2017)
|
||||
-----------------------------
|
||||
|
||||
|
|
|
|||
|
|
@ -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']);
|
||||
|
|
|
|||
|
|
@ -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'));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -42,6 +42,7 @@ class ClassProvider implements ServiceProviderInterface
|
|||
'CustomFilterModel',
|
||||
'GroupModel',
|
||||
'GroupMemberModel',
|
||||
'InviteModel',
|
||||
'LanguageModel',
|
||||
'LastLoginModel',
|
||||
'LinkModel',
|
||||
|
|
|
|||
|
|
@ -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') ?>
|
||||
|
|
|
|||
|
|
@ -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 ?>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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
|
|
@ -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')
|
||||
|
|
|
|||
|
|
@ -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'));
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue