Email feature

This commit is contained in:
Danila Balabaev 2015-03-23 20:22:54 +03:00
parent 8d2e8af586
commit ec283cb841
20 changed files with 421 additions and 1 deletions

15
.idea/deployment.xml generated Normal file
View File

@ -0,0 +1,15 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="PublishConfigData" autoUpload="Always" serverName="mw-taskboard.cloudapp.net (1)">
<serverData>
<paths name="mw-taskboard.cloudapp.net (1)">
<serverdata>
<mappings>
<mapping deploy="/" local="$PROJECT_DIR$" web="/" />
</mappings>
</serverdata>
</paths>
</serverData>
<option name="myAutoUpload" value="ALWAYS" />
</component>
</project>

View File

@ -3,6 +3,7 @@ require_once('lib/Slim/Slim.php');
require_once('lib/rb.php');
require_once('lib/password.php');
require_once('lib/JWT.php');
require_once('lib/PHPMailer/PHPMailerAutoload.php');
require_once('jsonResponse.php');
@ -51,6 +52,8 @@ $app->get('/authenticate', function() use($app, $jsonResponse) {
$app->response->setBody($jsonResponse->asJson());
});
require_once('mailFactory.php');
require_once('userRoutes.php');
require_once('boardRoutes.php');
require_once('itemRoutes.php');

View File

@ -19,6 +19,15 @@ $app->post('/boards', function() use($app, $jsonResponse) {
logAction($actor->username . ' added board ' . $board->name, null, $board->export());
$jsonResponse->addBeans(getBoards());
$jsonResponse->addAlert('success', 'New board ' . $board->name . ' created.');
foreach($board->sharedUser as $user) {
$body = getNewBoardEmailBody($board->id, $user->username, $board->name);
$subject = 'New board created!';
$recipient = $user->username;
$email = $user->email;
sendEmail($email, $recipient, $subject, $body);
}
}
$app->response->setBody($jsonResponse->asJson());
});
@ -37,6 +46,15 @@ $app->post('/boards/update', function() use($app, $jsonResponse) {
logAction($actor->username . ' updated board ' . $board->name, $before, $board->export());
}
$jsonResponse->addBeans(getBoards());
foreach($board->sharedUser as $user) {
$body = geEditBoardEmailBody($board->id, $user->username, $board->name);
$subject = 'Board updated!';
$recipient = $user->username;
$email = $user->email;
sendEmail($email, $recipient, $subject, $body);
}
}
$app->response->setBody($jsonResponse->asJson());
});

View File

@ -66,6 +66,30 @@ function getUser() {
return null;
}
function getUserByID($id) {
try {
$user = R::load('user', $id);
if ($user->id) {
return $user;
}
} catch (Exception $e) {}
return null;
}
function getLaneByID($id) {
try {
$lane = R::load('lane', $id);
if ($lane->id) {
return $lane;
}
} catch (Exception $e) {}
return null;
}
// Get all users.
function getUsers($sanitize = true) {
try {
@ -240,6 +264,21 @@ function updateUsername($user, $data) {
return $user;
}
// Change email if available.
function updateEmail($user, $data) {
global $jsonResponse;
$emailTaken = R::findOne('user', ' username = ?', [$data->newEmail]);
if (null != $user && null == $emailTaken) {
$user->email = $data->newEmail;
$jsonResponse->addAlert('success', 'Email updated.');
} else {
$jsonResponse->addAlert('error', 'Email already in use.');
}
return $user;
}
// Validate a provided JWT.
function validateToken($requireAdmin = false) {
global $jsonResponse, $app;
@ -333,6 +372,7 @@ function createInitialUser() {
$admin->defaultBoard = null;
$admin->salt = password_hash($admin->username . time(), PASSWORD_BCRYPT);
$admin->password = password_hash('admin', PASSWORD_BCRYPT, array('salt' => $admin->salt));
$admin->email = '';
R::store($admin);
}

View File

@ -29,6 +29,27 @@ $app->post('/boards/:id/items', function($id) use($app, $jsonResponse) {
} else {
$jsonResponse->addAlert('error', 'Failed to create board item.');
}
foreach($board->sharedUser as $user) {
$actor = getUser();
$body = getNewItemEmailBody(
$board->id,
$actor->username,
$board->name,
$item->title,
$item->description,
getUserByID($item->assignee)->username,
$item->category,
$item->dueDate,
$item->points,
$item->position
);
$subject = 'New item created!';
$recipient = $user->username;
$email = $user->email;
sendEmail($email, $recipient, $subject, $body);
}
}
}
$app->response->setBody($jsonResponse->asJson());
@ -62,6 +83,30 @@ $app->post('/items/:itemId', function($itemId) use ($app, $jsonResponse) {
logAction($user->username . ' updated item ' . $item->title, $before, $item->export(), $itemId);
$jsonResponse->addAlert('success', 'Updated item ' . $item->title . '.');
$jsonResponse->addBeans(getBoards());
$lane = R::load('lane', $item->lane_id);
$board = R::load('board', $lane->boardId);
foreach($board->sharedUser as $user) {
$actor = getUser();
$body = getEditItemEmailBody(
$board->id,
$actor->username,
$board->name,
$item->title,
$item->description,
getUserByID($item->assignee)->username,
$item->category,
$item->dueDate,
$item->points,
$item->position
);
$subject = 'Item edited';
$recipient = $user->username;
$email = $user->email;
sendEmail($email, $recipient, $subject, $body);
}
}
}
$app->response->setBody($jsonResponse->asJson());
@ -123,6 +168,24 @@ $app->post('/items/:itemId/comment', function($itemId) use ($app, $jsonResponse)
logAction($user->username . ' added a comment to item ' . $item->title, null, $comment->export(), $itemId);
$jsonResponse->addAlert('success', 'Comment added to item ' . $item->title . '.');
$jsonResponse->addBeans(R::load('item', $itemId));
$lane = R::load('lane', $item->lane_id);
$board = R::load('board', $lane->boardId);
foreach($board->sharedUser as $user) {
$body = getNewCommentEmailBody(
$board->id,
$user->username,
$board->name,
$item->title,
$comment->text
);
$subject = 'New comment';
$recipient = $user->username;
$email = $user->email;
sendEmail($email, $recipient, $subject, $body);
}
}
}
$app->response->setBody($jsonResponse->asJson());
@ -146,6 +209,25 @@ $app->post('/comments/:commentId', function($commentId) use ($app, $jsonResponse
logAction($user->username . ' edited comment ' . $comment->id, $before, $comment->export(), $comment->id);
$jsonResponse->addAlert('success', 'Comment edited.');
$jsonResponse->addBeans(R::load('item', $comment->item_id));
$item = R::load('item', $comment->item_id);
$lane = R::load('lane', $item->lane_id);
$board = R::load('board', $lane->boardId);
foreach($board->sharedUser as $user) {
$body = getEditCommentEmailBody(
$board->id,
$user->username,
$board->name,
$item->title,
$comment->text
);
$subject = 'Edit comment';
$recipient = $user->username;
$email = $user->email;
sendEmail($email, $recipient, $subject, $body);
}
}
$app->response->setBody($jsonResponse->asJson());
})->conditions(['commentId' => '\d+']);

9
api/mailConfig.php Normal file
View File

@ -0,0 +1,9 @@
<?php
$MAIL_HOST = 'smtp.somehost.com'; // Specify main and backup SMTP servers
$MAIL_USERNAME = 'user@somehost.com'; // SMTP username
$MAIL_PASSWORD = 'secretpassword'; // SMTP password
$MAIL_SMTPSECURE = 'ssl'; // Enable TLS encryption, `ssl` also accepted
$MAIL_PORT = 465; // TCP port to connect to
$MAIL_FROM = 'user@somehost.com';
$MAIL_FROMNAME = 'Taskboard';

105
api/mailFactory.php Normal file
View File

@ -0,0 +1,105 @@
<?php
require_once('mailConfig.php');
function createMailObject() {
global $MAIL_HOST, $MAIL_USERNAME, $MAIL_PASSWORD, $MAIL_SMTPSECURE, $MAIL_PORT, $MAIL_FROM, $MAIL_FROMNAME;
$mail = new PHPMailer;
$mail->isSMTP();
$mail->Host = $MAIL_HOST;
$mail->SMTPAuth = true;
$mail->Username = $MAIL_USERNAME;
$mail->Password = $MAIL_PASSWORD;
$mail->SMTPSecure = $MAIL_SMTPSECURE;
$mail->Port = $MAIL_PORT;
$mail->From = $MAIL_FROM;
$mail->FromName = $MAIL_FROMNAME;
return $mail;
}
function sendEmail($email, $recipient, $subject, $body) {
$mail = createMailObject();
$mail->addAddress($email, $recipient); // Add a recipient
$mail->isHTML(true);
$mail->Subject = $subject;
$mail->Body = $body;
$mail->send();
}
function getNewBoardEmailBody($boardid, $username, $boardname) {
$message = file_get_contents('mail_templates/newBoard.html');
$message = str_replace('%boardid%', $boardid, $message);
$message = str_replace('%username%', $username, $message);
$message = str_replace('%boardname%', $boardname, $message);
return $message;
}
function getEditBoardEmailBody($boardid, $username, $boardname) {
$message = file_get_contents('mail_templates/editBoard.html');
$message = str_replace('%boardid%', $boardid, $message);
$message = str_replace('%username%', $username, $message);
$message = str_replace('%boardname%', $boardname, $message);
return $message;
}
function getNewItemEmailBody($boardid, $username, $boardname, $title, $description, $assignee, $category, $dueDate, $points, $position)
{
$message = file_get_contents('mail_templates/newItem.html');
$message = str_replace('%boardid%', $boardid, $message);
$message = str_replace('%username%', $username, $message);
$message = str_replace('%boardname%', $boardname, $message);
$message = str_replace('%title%', $title, $message);
$message = str_replace('%description%', $description, $message);
$message = str_replace('%assignee%', $assignee, $message);
$message = str_replace('%category%', $category, $message);
$message = str_replace('%duedate%', $dueDate, $message);
$message = str_replace('%points%', $points, $message);
$message = str_replace('%position%', $position, $message);
return $message;
}
function getEditItemEmailBody($boardid, $username, $boardname, $title, $description, $assignee, $category, $dueDate, $points, $position)
{
$message = file_get_contents('mail_templates/editItem.html');
$message = str_replace('%boardid%', $boardid, $message);
$message = str_replace('%username%', $username, $message);
$message = str_replace('%boardname%', $boardname, $message);
$message = str_replace('%title%', $title, $message);
$message = str_replace('%description%', $description, $message);
$message = str_replace('%assignee%', $assignee, $message);
$message = str_replace('%category%', $category, $message);
$message = str_replace('%duedate%', $dueDate, $message);
$message = str_replace('%points%', $points, $message);
$message = str_replace('%position%', $position, $message);
return $message;
}
function getNewCommentEmailBody($boardid, $username, $boardname, $title, $comment)
{
$message = file_get_contents('mail_templates/newComment.html');
$message = str_replace('%boardid%', $boardid, $message);
$message = str_replace('%username%', $username, $message);
$message = str_replace('%boardname%', $boardname, $message);
$message = str_replace('%title%', $title, $message);
$message = str_replace('%comment%', $comment, $message);
return $message;
}
function getEditCommentEmailBody($boardid, $username, $boardname, $title, $comment)
{
$message = file_get_contents('mail_templates/editComment.html');
$message = str_replace('%boardid%', $boardid, $message);
$message = str_replace('%username%', $username, $message);
$message = str_replace('%boardname%', $boardname, $message);
$message = str_replace('%title%', $title, $message);
$message = str_replace('%comment%', $comment, $message);
return $message;
}

View File

@ -0,0 +1,7 @@
<!DOCTYPE html>
<html>
<body>
%username% updated board %boardname%!
<a href="http://localhost/#/boards/%boardid%">Navigate to board!</a>
</body>
</html>

View File

@ -0,0 +1,10 @@
<!DOCTYPE html>
<html>
<body>
%username% edited comment at board %boardname%:<br>
%title%<br>
%comment%<br>
<br>
<a href="http://localhost/#/boards/%boardid%">Navigate to board!</a>
</body>
</html>

View File

@ -0,0 +1,15 @@
<!DOCTYPE html>
<html>
<body>
%username% edited item at board %boardname%:<br>
%title%<br>
%description%<br>
%duedate%<br>
%assignee%<br>
%category%<br>
%points%<br>
%position%<br>
<a href="http://localhost/#/boards/%boardid%">Navigate to board!</a>
</body>
</html>

View File

@ -0,0 +1,7 @@
<!DOCTYPE html>
<html>
<body>
%username% created new board %boardname%!<br>
<a href="http://localhost/#/boards/%boardid%">Navigate to board!</a>
</body>
</html>

View File

@ -0,0 +1,10 @@
<!DOCTYPE html>
<html>
<body>
%username% created new comment at board %boardname%:<br>
%title%<br>
%comment%<br>
<br>
<a href="http://localhost/#/boards/%boardid%">Navigate to board!</a>
</body>
</html>

View File

@ -0,0 +1,15 @@
<!DOCTYPE html>
<html>
<body>
%username% created new item at board %boardname%:<br>
%title%<br>
%description%<br>
%duedate%<br>
%assignee%<br>
%category%<br>
%points%<br>
%position%<br>
<a href="http://localhost/#/boards/%boardid%">Navigate to board!</a>
</body>
</html>

View File

@ -86,6 +86,24 @@ $app->post('/updateusername', function() use($app, $jsonResponse) {
$app->response->setBody($jsonResponse->asJson());
});
// Update current user's email if not taken.
$app->post('/updateemail', function() use($app, $jsonResponse) {
$data = json_decode($app->environment['slim.input']);
if (validateToken()) {
$user = getUser();
$before = $user->export();
$user = updateEmail($user, $data);
R::store($user);
if ($jsonResponse->alerts[0]['type'] == 'success') {
logAction($before['username'] . ' changed email to ' . $user->email, $before, $user->export());
}
$jsonResponse->addBeans(getUsers());
}
$app->response->setBody($jsonResponse->asJson());
});
// Update current user's default board.
$app->post('/updateboard', function() use($app, $jsonResponse) {
$data = json_decode($app->environment['slim.input']);
@ -123,6 +141,7 @@ $app->get('/users/current', function() use($app, $jsonResponse) {
'userId' => $user->id,
'username' => $user->username,
'isAdmin' => $user->isAdmin,
'email' => $user->email,
'defaultBoard' => $user->defaultBoard
];
}
@ -151,6 +170,7 @@ $app->post('/users', function() use($app, $jsonResponse) {
$user = R::dispense('user');
$user->username = $data->username;
$user->isAdmin = $data->isAdmin;
$user->email = $data->email;
$user->defaultBoard = $data->defaultBoard;
$user->salt = password_hash($data->username . time(), PASSWORD_BCRYPT);
$user->password = password_hash($data->password, PASSWORD_BCRYPT, array('salt' => $user->salt));
@ -187,6 +207,7 @@ $app->post('/users/update', function() use($app, $jsonResponse) {
$user->password = password_hash($data->password, PASSWORD_BCRYPT, array('salt' => $user->salt));
}
$user->isAdmin = $data->isAdmin;
$user->email = $data->email;
$user->defaultBoard = $data->defaultBoard;
R::store($user);

View File

@ -117,6 +117,40 @@ function ($scope, $interval, UserService) {
}
};
$scope.emailFormData = {
newEmail: '',
emailError: false,
isSaving: false,
setAlert: function(message) {
this.isSaving = false;
this.emailError = true;
$scope.alerts.showAlert({ 'type': 'error', 'text': message });
},
reset: function() {
this.newEmail = '';
this.emailError = false;
this.isSaving = false;
}
};
$scope.changeEmail = function(newEmailFormData) {
$scope.emailFormData.isSaving = true;
if (newEmailFormData.newEmail === '') {
newEmailFormData.setAlert('Email cannot be blank.');
newEmailFormData.isSaving = false;
} else {
UserService.changeEmail(newEmailFormData.newEmail)
.success(function(data) {
$scope.alerts.showAlerts(data.alerts);
$scope.updateUsers(data.data);
$scope.loadCurrentUser();
newEmailFormData.isSaving = false;
newEmailFormData.newUsername = '';
});
}
};
$scope.updatingDefaultBoard = false;
$scope.setDefaultBoard = function() {
$scope.updatingDefaultBoard = true;

View File

@ -7,11 +7,13 @@ function ($scope, UserService) {
isAdd: true,
username: '',
password: '',
email: '',
verifyPass: '',
defaultBoard: null,
isAdmin: false,
passError: false,
usernameError: false,
emailError: false,
isSaving: false,
setUser: function(user) {
this.reset();
@ -19,6 +21,7 @@ function ($scope, UserService) {
this.isAdd = false;
this.userId = user.id;
this.username = user.username;
this.email = user.email;
this.defaultBoard = user.default_board;
this.isAdmin = user.is_admin == '1';
},
@ -29,6 +32,7 @@ function ($scope, UserService) {
this.isAdd = true;
this.username = '';
this.password = '';
this.email = '';
this.verifyPass = '';
this.defaultBoard = null;
this.isAdmin = false;

View File

@ -35,6 +35,12 @@ function($http) {
});
},
changeEmail: function(newEmail) {
return $http.post('api/updateemail', {
newEmail: newEmail
});
},
changeDefaultBoard: function(newDefaultBoard) {
return $http.post('api/updateboard', {
defaultBoard: newDefaultBoard
@ -53,6 +59,7 @@ function($http) {
return $http.post('api/users', {
username: formData.username,
password: formData.password,
email: formData.email,
defaultBoard: formData.defaultBoard,
boardAccess: formData.boardAccess,
isAdmin: formData.isAdmin
@ -64,6 +71,7 @@ function($http) {
userId: formData.userId,
newUsername: formData.username,
password: formData.password,
email: formData.email,
defaultBoard: formData.defaultBoard,
boardAccess: formData.boardAccess,
isAdmin: formData.isAdmin

View File

@ -41,4 +41,16 @@
</fieldset>
</form>
</div>
<div id="change-email">
<h4>Change Email</h4>
<form role="form" novalidate data-ng-submit="changeEmail(emailFormData)">
<fieldset data-ng-disabled="emailFormData.isSaving || loadingCurrentUser">
<div class="form-group" data-ng-class="{ 'has-error': emailFormData.emailError }">
<input class="form-control" placeholder="New Email" data-ng-model="emailFormData.newEmail">
</div>
<button type="submit" class="btn btn-info">Update Email</button>
<a class="btn btn-default" data-ng-click="emailFormData.reset()">Reset</a>
</fieldset>
</form>
</div>
</div>

View File

@ -23,6 +23,10 @@
<div class="form-group" data-ng-class="{ 'has-error': userFormData.passError }">
<input class="form-control" type="password" placeholder="Verify Password" data-ng-model="userFormData.verifyPass">
</div>
<div class="form-group" data-ng-class="{ 'has-error': userFormData.emailError }">
<h5><span data-ng-if="!userFormData.isAdd">Change </span>Email</h5>
<input class="form-control" type="text" placeholder="Email" data-ng-model="userFormData.email">
</div>
<div class="form-group" data-ng-if="boardNames.length">
<h5><span data-ng-if="!userFormData.isAdd">Select </span>Default Board
<span style="cursor:pointer;" class="fa fa-question-circle popover-dismiss"

View File

@ -3,7 +3,7 @@
<table class="table table-striped">
<thead>
<tr>
<th>User</th><th>Admin</th><th>Default Board</th><th>Actions</th>
<th>User</th><th>Email</th><th>Admin</th><th>Default Board</th><th>Actions</th>
</tr>
</thead>
<tbody>
@ -15,6 +15,7 @@
</tr>
<tr data-ng-repeat="user in users">
<td>{{ user.username }}</td>
<td>{{ user.email }}</td>
<td data-ng-class="user.is_admin == '1' ? 'green' : 'red'">
<span data-ng-if="user.is_admin == '1'" class="fa fa-check" title="Yes"></span>
<span data-ng-if="user.is_admin == '0'" class="fa fa-times" title="No"></span>