First working version of invite users feature

This commit is contained in:
Maxi Redigonda 2019-10-28 15:27:25 -03:00
parent aa7cefc959
commit 5331c3363e
7 changed files with 547 additions and 1 deletions

View File

@ -4,6 +4,7 @@ $userControllers->setGroupPath('/user');
$userControllers->addController(new LoginController);
$userControllers->addController(new SignUpController);
$userControllers->addController(new InviteUserController);
$userControllers->addController(new LogoutController);
$userControllers->addController(new CheckSessionController);
$userControllers->addController(new SendRecoverPasswordController);

View File

@ -0,0 +1,148 @@
<?php
use Respect\Validation\Validator as DataValidator;
DataValidator::with('CustomValidations', true);
/**
* @api {post} /user/invite Invite
* @apiVersion 4.5.0
*
* @apiName Invite
*
* @apiGroup User
*
* @apiDescription This path invites an user on the system.
*
* @apiPermission staff1
*
* @apiParam {String} name The name of the invited user.
* @apiParam {String} email The email of the invited user.
* @apiParam {String} customfield_ Custom field values for this user.
*
* @apiUse INVALID_NAME
* @apiUse INVALID_EMAIL
* @apiUse INVALID_CAPTCHA
* @apiUse USER_EXISTS
* @apiUse ALREADY_BANNED
* @apiUse NO_PERMISSION
* @apiUse INVALID_CUSTOM_FIELD_OPTION
* @apiUse MAIL_SENDER_NOT_CONNECTED
*
* @apiSuccess {Object} data Information about invited user
* @apiSuccess {Number} data.userId Id of the invited user
* @apiSuccess {String} data.userEmail Email of the invited user
*
*/
class InviteUserController extends Controller {
const PATH = '/invite';
const METHOD = 'POST';
private $userEmail;
private $userName;
public function validations() {
$validations = [
'permission' => 'staff_1',
'requestData' => [
'name' => [
'validation' => DataValidator::length(2, 55),
'error' => ERRORS::INVALID_NAME
],
'email' => [
'validation' => DataValidator::email(),
'error' => ERRORS::INVALID_EMAIL
]
]
];
$validations['requestData']['captcha'] = [
'validation' => DataValidator::captcha(),
'error' => ERRORS::INVALID_CAPTCHA
];
return $validations;
}
public function handler() {
if (!Controller::isUserSystemEnabled()) {
throw new RequestException(ERRORS::USER_SYSTEM_DISABLED);
}
if (!Setting::getSetting('registration')->value) {
throw new RequestException(ERRORS::NO_PERMISSION);
}
$this->storeRequestData();
$existentUser = User::getUser($this->userEmail, 'email');
if (!$existentUser->isNull()) {
throw new RequestException(ERRORS::USER_EXISTS);
}
$banRow = Ban::getDataStore($this->userEmail, 'email');
if (!$banRow->isNull()) {
throw new RequestException(ERRORS::ALREADY_BANNED);
}
if(MailSender::getInstance()->isConnected()) {
$userId = $this->createNewUserAndRetrieveId();
$this->token = Hashing::generateRandomToken();
$recoverPassword = new RecoverPassword();
$recoverPassword->setProperties(array(
'email' => $this->userEmail,
'token' => $this->token,
'staff' => false
));
$recoverPassword->store();
$this->sendInvitationMail();
Response::respondSuccess([
'userId' => $userId,
'userEmail' => $this->userEmail
]);
// TODO: Log::createLog('SIGN_UP', null, User::getDataStore($userId));
} else {
throw new RequestException(ERRORS::MAIL_SENDER_NOT_CONNECTED);
}
}
public function storeRequestData() {
$this->userName = Controller::request('name');
$this->userEmail = Controller::request('email');
}
public function createNewUserAndRetrieveId() {
$userInstance = new User();
$userInstance->setProperties([
'name' => $this->userName,
'signupDate' => Date::getCurrentDate(),
'tickets' => 0,
'email' => $this->userEmail,
'password' => Hashing::hashPassword(Hashing::generateRandomToken()),
'verificationToken' => null,
'xownCustomfieldvalueList' => $this->getCustomFieldValues()
]);
return $userInstance->store();
}
public function sendInvitationMail() {
$mailSender = MailSender::getInstance();
$mailSender->setTemplate(MailTemplate::USER_INVITE, [
'to' => $this->userEmail,
'name' => $this->userName,
'url' => Setting::getSetting('url')->getValue(),
'token' => $this->token
]);
$mailSender->send();
}
}

View File

@ -18,7 +18,7 @@ DataValidator::with('CustomValidations', true);
* @apiParam {String} name The name of the new user.
* @apiParam {String} email The email of the new user.
* @apiParam {String} password The password of the new user.
* @apiParam {String} apiKey APIKey to sign up an user if the user system is disabled.
* @apiParam {String} apiKey APIKey to sign up an user if the registration system is disabled.
* @apiParam {String} customfield_ Custom field values for this user.
*
* @apiUse INVALID_NAME

View File

@ -251,6 +251,10 @@
* @apiDefine INVALID_COLOR
* @apiError {String} INVALID_COLOR The color should be in hexadecimal, preceded by a '#'
*/
/**
* @apiDefine MAIL_SENDER_NOT_CONNECTED
* @apiError {String} MAIL_SENDER_NOT_CONNECTED The mail sender is not connected.
*/
class ERRORS {
const INVALID_CREDENTIALS = 'INVALID_CREDENTIALS';
@ -317,4 +321,5 @@ class ERRORS {
const INVALID_CUSTOM_FIELD_OPTION = 'INVALID_CUSTOM_FIELD_OPTION';
const UNAVAILABLE_STATS = 'UNAVAILABLE_STATS';
const INVALID_COLOR = 'INVALID_COLOR';
const MAIL_SENDER_NOT_CONNECTED = 'MAIL_SENDER_NOT_CONNECTED';
}

View File

@ -25,6 +25,12 @@ class MailTexts {
'Hi, {{name}}. You have requested to recover your password.',
'Use this code in {{url}}/recover-password?email={{to}}&token={{token}} or click the button below.',
],
'USER_INVITE' => [
'User invited - OpenSupports',
'User invited',
'Hi, {{name}}. You have been invited to join our support center.',
'Use this code in {{url}}/recover-password?email={{to}}&token={{token}}&invited=true or click the button below to set up your password.'
],
'USER_SYSTEM_DISABLED' => [
'Access system changed - OpenSupports',
'Access system changed',

View File

@ -0,0 +1,384 @@
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Support Center</title>
<style type="text/css">
/* Take care of image borders and formatting, client hacks */
img { max-width: 600px; outline: none; text-decoration: none; -ms-interpolation-mode: bicubic;}
a img { border: none; }
table { border-collapse: collapse !important;}
#outlook a { padding:0; }
.ReadMsgBody { width: 100%; }
.ExternalClass { width: 100%; }
.backgroundTable { margin: 0 auto; padding: 0; width: 100% !important; }
table td { border-collapse: collapse; }
.ExternalClass * { line-height: 115%; }
.container-for-gmail-android { min-width: 600px; }
/* General styling */
* {
font-family: Helvetica, Arial, sans-serif;
}
body {
-webkit-font-smoothing: antialiased;
-webkit-text-size-adjust: none;
width: 100% !important;
margin: 0 !important;
height: 100%;
color: #676767;
}
td {
font-family: Helvetica, Arial, sans-serif;
font-size: 14px;
color: #777777;
text-align: center;
line-height: 21px;
}
a {
color: #676767;
text-decoration: none !important;
}
.pull-left {
text-align: left;
}
.pull-right {
text-align: right;
}
.header-lg,
.header-md,
.header-sm {
font-size: 32px;
font-weight: 700;
line-height: normal;
padding: 35px 0 0;
color: #4d4d4d;
}
.header-md {
font-size: 24px;
}
.header-sm {
padding: 5px 0;
font-size: 18px;
line-height: 1.3;
}
.content-padding {
padding: 20px 0 30px;
}
.mobile-header-padding-right {
width: 290px;
text-align: right;
padding-left: 10px;
}
.mobile-header-padding-left {
width: 290px;
text-align: left;
padding-left: 10px;
}
.free-text {
width: 100% !important;
padding: 10px 60px 0px;
}
.block-rounded {
border-radius: 5px;
border: 1px solid #e5e5e5;
vertical-align: top;
}
.button {
padding: 55px 0 0;
}
.info-block {
padding: 0 20px;
width: 260px;
}
.mini-block-container {
padding: 30px 50px;
width: 500px;
}
.mini-block {
background-color: #ffffff;
width: 498px;
border: 1px solid #cccccc;
border-radius: 5px;
padding: 60px 75px;
}
.block-rounded {
width: 260px;
}
.info-img {
width: 258px;
border-radius: 5px 5px 0 0;
}
.force-width-img {
width: 480px;
height: 1px !important;
}
.force-width-full {
width: 600px;
height: 1px !important;
}
.user-img img {
width: 82px;
border-radius: 5px;
border: 1px solid #cccccc;
}
.user-img {
width: 92px;
text-align: left;
}
.user-msg {
width: 236px;
font-size: 14px;
text-align: left;
font-style: italic;
}
.code-block {
padding: 10px 0;
border: 1px solid #cccccc;
color: #4d4d4d;
font-weight: bold;
font-size: 17px;
text-align: center;
}
.force-width-gmail {
min-width:600px;
height: 0px !important;
line-height: 1px !important;
font-size: 1px !important;
}
.button-width {
width: 228px;
}
</style>
<style type="text/css" media="screen">
@import url(http://fonts.googleapis.com/css?family=Oxygen:400,700);
</style>
<style type="text/css" media="screen">
@media screen {
/* Thanks Outlook 2013! */
* {
font-family: 'Oxygen', 'Helvetica Neue', 'Arial', 'sans-serif' !important;
}
}
</style>
<style type="text/css" media="only screen and (max-width: 480px)">
/* Mobile styles */
@media only screen and (max-width: 480px) {
table[class*="container-for-gmail-android"] {
min-width: 290px !important;
width: 100% !important;
}
table[class="w320"] {
width: 320px !important;
}
img[class="force-width-gmail"] {
display: none !important;
width: 0 !important;
height: 0 !important;
}
a[class="button-width"],
a[class="button-mobile"] {
width: 248px !important;
}
td[class*="mobile-header-padding-left"] {
width: 160px !important;
padding-left: 0 !important;
}
td[class*="mobile-header-padding-right"] {
width: 160px !important;
padding-right: 0 !important;
}
td[class="header-lg"] {
font-size: 24px !important;
padding-bottom: 5px !important;
}
td[class="header-md"] {
font-size: 18px !important;
padding-bottom: 5px !important;
}
td[class="content-padding"] {
padding: 5px 0 30px !important;
}
td[class="button"] {
padding: 15px 0 5px !important;
}
td[class*="free-text"] {
padding: 10px 18px 30px !important;
}
img[class="force-width-img"],
img[class="force-width-full"] {
display: none !important;
}
td[class="info-block"] {
display: block !important;
width: 280px !important;
padding-bottom: 40px !important;
}
td[class="info-img"],
img[class="info-img"] {
width: 278px !important;
}
td[class="mini-block-container"] {
padding: 8px 20px !important;
width: 280px !important;
}
td[class="mini-block"] {
padding: 20px !important;
}
td[class="user-img"] {
display: block !important;
text-align: center !important;
width: 100% !important;
padding-bottom: 10px;
}
td[class="user-msg"] {
display: block !important;
padding-bottom: 20px !important;
}
}
</style>
</head>
<body bgcolor="#f7f7f7">
<table align="center" cellpadding="0" cellspacing="0" class="container-for-gmail-android" width="100%">
<tr>
<td align="left" valign="top" width="100%" style="background-color: #ffffff;">
<center>
<table cellspacing="0" cellpadding="0" width="100%" bgcolor="#ffffff" style="border-bottom: 1px solid #cccccc">
<tr>
<td width="100%" height="80" valign="top" style="text-align: center; vertical-align:middle;">
<center>
<table cellpadding="0" cellspacing="0" width="600" class="w320">
<tr>
<td style="vertical-align: middle;padding: 15px 0;">
<img src="{{IMAGE_HEADER_URL}}" alt="logo">
</td>
</tr>
</table>
</center>
<!--[if gte mso 9]>
</v:textbox>
</v:rect>
<![endif]-->
</td>
</tr>
</table>
</center>
</td>
</tr>
<tr>
<td align="center" valign="top" width="100%" style="background-color: #f7f7f7;" class="content-padding">
<center>
<table cellspacing="0" cellpadding="0" width="600" class="w320">
<tr>
<td class="header-lg">
{{USER_INVITE_MATCH_1}}
</td>
</tr>
<tr>
<td class="free-text">
{{USER_INVITE_MATCH_2}}
</td>
</tr>
<tr>
<td class="mini-block-container">
<table cellspacing="0" cellpadding="0" width="100%" style="border-collapse:separate !important;">
<tr>
<td class="mini-block">
<table cellpadding="0" cellspacing="0" width="100%">
<tr>
<td style="padding-bottom: 30px;">
{{USER_INVITE_MATCH_3}}
</td>
</tr>
<tr>
<td class="code-block">
{{token}}
</td>
</tr>
<tr>
<td class="button">
<div><a class="button-mobile" target="_blank" href="{{url}}/recover-password?email={{to}}&token={{token}}&invited=true"
style="background-color:#ff6f6f;border-radius:5px;color:#ffffff;display:inline-block;font-family:'Cabin', Helvetica, Arial, sans-serif;font-size:14px;font-weight:regular;line-height:45px;text-align:center;text-decoration:none;width:155px;-webkit-text-size-adjust:none;mso-hide:all;">Set up your password</a></div>
</td>
</tr>
</table>
</td>
</tr>
</table>
</td>
</tr>
</table>
</center>
</td>
</tr>
<tr>
<td align="center" valign="top" width="100%" style="background-color: #ffffff; border-top: 1px solid #e5e5e5; border-bottom: 1px solid #e5e5e5; height: 100px;">
<center>
<table cellspacing="0" cellpadding="0" width="600" class="w320">
<tr>
<td style="padding: 25px 0 25px">
<strong>OpenSupports</strong><br />
Open source ticket system<br />
www.opensupports.com<br /><br />
</td>
</tr>
</table>
</center>
</td>
</tr>
</table>
</body>
</html>

View File

@ -19,6 +19,7 @@ class MailTemplate extends DataStore {
const USER_SIGNUP = 'USER_SIGNUP';
const USER_PASSWORD = 'USER_PASSWORD';
const PASSWORD_FORGOT = 'PASSWORD_FORGOT';
const USER_INVITE = 'USER_INVITE';
const USER_SYSTEM_DISABLED = 'USER_SYSTEM_DISABLED';
const USER_SYSTEM_ENABLED = 'USER_SYSTEM_ENABLED';
const TICKET_CREATED = 'TICKET_CREATED';
@ -32,6 +33,7 @@ class MailTemplate extends DataStore {
'USER_PASSWORD' => 'data/mail-templates/user-edit-password.html',
'USER_EMAIL' => 'data/mail-templates/user-edit-email.html',
'PASSWORD_FORGOT' => 'data/mail-templates/user-password-forgot.html',
'USER_INVITE' => 'data/mail-templates/user-invite.html',
'USER_SYSTEM_DISABLED' => 'data/mail-templates/user-system-disabled.html',
'USER_SYSTEM_ENABLED' => 'data/mail-templates/user-system-enabled.html',
'TICKET_CREATED' => 'data/mail-templates/ticket-created.html',