Skip to content

Commit

Permalink
Browse files Browse the repository at this point in the history
Create UI for receiving and sending messages and create API routes
  • Loading branch information
john committed Apr 24, 2017
1 parent 19cf87b commit 0017766
Show file tree
Hide file tree
Showing 10 changed files with 282 additions and 4 deletions.
1 change: 1 addition & 0 deletions app/database/init.php
Expand Up @@ -31,6 +31,7 @@ $stmt = 'CREATE TABLE IF NOT EXISTS messages (
enc_keys VARCHAR(64) NOT NULL, enc_keys VARCHAR(64) NOT NULL,
keys_iv VARCHAR(24) NOT NULL, keys_iv VARCHAR(24) NOT NULL,
message_iv VARCHAR(24) NOT NULL, message_iv VARCHAR(24) NOT NULL,
is_read BOOLEAN NOT NULL DEFAULT 0,
FOREIGN KEY (sender_id) REFERENCES users(id), FOREIGN KEY (sender_id) REFERENCES users(id),
FOREIGN KEY (receiver_id) REFERENCES users(id) FOREIGN KEY (receiver_id) REFERENCES users(id)
)'; )';
Expand Down
37 changes: 36 additions & 1 deletion app/model/SecureMessage.php
Expand Up @@ -11,11 +11,21 @@ class SecureMessage {
public $message; public $message;
public $sender_id; public $sender_id;
public $receiver_id; public $receiver_id;
public $is_read = false;


/** /**
* Stores this information pertaining to this message in the DB * Stores this information pertaining to this message in the DB
*/ */
public function save() { public function save() {
$dbh = Database::connect();
if (SecureMessage::exists($this->id)) {
$stmt = $dbh->prepare("UPDATE messages SET is_read=:is_read WHERE id=:message_id");
$is_read = (int) $this->is_read;
$stmt->bindParam(':is_read', $is_read);
$stmt->bindParam(':message_id', $this->id);
$stmt->execute();
return;
}
$integrity_key = openssl_random_pseudo_bytes(16); $integrity_key = openssl_random_pseudo_bytes(16);
$encryption_key = openssl_random_pseudo_bytes(16); $encryption_key = openssl_random_pseudo_bytes(16);
$message_iv = openssl_random_pseudo_bytes(openssl_cipher_iv_length(Config::encr_algo)); $message_iv = openssl_random_pseudo_bytes(openssl_cipher_iv_length(Config::encr_algo));
Expand All @@ -27,7 +37,6 @@ class SecureMessage {


$enc_keys = openssl_encrypt($integrity_key . $encryption_key, Config::encr_algo, Config::secret_key, 0, $keys_iv); $enc_keys = openssl_encrypt($integrity_key . $encryption_key, Config::encr_algo, Config::secret_key, 0, $keys_iv);


$dbh = Database::connect();
$stmt = $dbh->prepare('INSERT INTO messages (message, sender_id, receiver_id, enc_keys, keys_iv, message_iv) $stmt = $dbh->prepare('INSERT INTO messages (message, sender_id, receiver_id, enc_keys, keys_iv, message_iv)
VALUES (:message, :sender_id, :receiver_id, :enc_keys, :keys_iv, :message_iv) VALUES (:message, :sender_id, :receiver_id, :enc_keys, :keys_iv, :message_iv)
'); ');
Expand All @@ -52,6 +61,24 @@ class SecureMessage {
$stmt->execute(); $stmt->execute();
} }


public static function exists($message_id) {
$dbh = Database::connect();
$stmt = $dbh->prepare("SELECT count(*) FROM messages WHERE id = :message_id");
$stmt->bindParam(':message_id', $message_id);
$stmt->execute();
return $stmt->fetchColumn() > 0;
}

public static function getByID($message_id) {
$messages = SecureMessage::all();
foreach ($messages as $m) {
if ($m->id == $message_id) {
return $m;
}
}
return null;
}

public static function all() { public static function all() {
$dbh = Database::connect(); $dbh = Database::connect();
$stmt = $dbh->prepare("SELECT * from messages"); $stmt = $dbh->prepare("SELECT * from messages");
Expand Down Expand Up @@ -81,6 +108,7 @@ class SecureMessage {
$m->message = $message; $m->message = $message;
$m->sender_id = $row['sender_id']; $m->sender_id = $row['sender_id'];
$m->receiver_id = $row['receiver_id']; $m->receiver_id = $row['receiver_id'];
$m->is_read = (bool) $row['is_read'];
return $m; return $m;
}, $stmt->fetchAll()); }, $stmt->fetchAll());


Expand Down Expand Up @@ -111,6 +139,13 @@ class SecureMessage {
}); });
} }


public static function count_unread($user_id) {
$messages = SecureMessage::all();
return count(array_filter($messages, function ($m) use ($user_id) {
return $m->receiver_id == $user_id && ! $m->is_read;
}));
}

public function sender() { public function sender() {
return User::getByID($this->sender_id); return User::getByID($this->sender_id);
} }
Expand Down
39 changes: 39 additions & 0 deletions public/api.php
@@ -0,0 +1,39 @@
<?php
defined('APP_DIR') or define('APP_DIR', __DIR__ . '/../app/');
include_once(APP_DIR . 'include/http.php');
include_once(APP_DIR . 'model/SecureMessage.php');
include_once(APP_DIR . 'model/User.php');

$data = User::authenticated();
if (!$data) {
echo json_encode(['unauthorized']);
exit();
}

/* POST API Routes */
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$params = Http::post_params();
$action = $params['action'];

if ($action === 'mark_unread') {
$m = SecureMessage::getByID($params['message_id']);
$m->is_read = true;
$m->save();
echo json_encode(['success']);
} else if ($action === 'send_message') {
$user = User::getByID($params['user_id']);
$recipient = User::get($params['recipient']);
if (!$recipient) {
echo json_encode(['unknown_recipient']);
} else {
$m = new SecureMessage;
$m->message = $params['message'];
$m->sender_id = $user->id;
$m->receiver_id = $recipient->id;
$m->save();
echo json_encode(['success']);
}
}

}
?>
10 changes: 9 additions & 1 deletion public/css/app.css
@@ -1,7 +1,8 @@
@import url("login.css"); @import url("login.css");
@import url("admin.css"); @import url("admin.css");
@import url("user.css"); @import url("user.css");

@import url("menu-button.css");
@import url("messages.css");


body { body {
background-color: #fcfcfc !important; background-color: #fcfcfc !important;
Expand All @@ -15,3 +16,10 @@ main {
input:-webkit-autofill { input:-webkit-autofill {
-webkit-box-shadow: 0 0 0 1000px white inset !important; -webkit-box-shadow: 0 0 0 1000px white inset !important;
} }

.notification-count {
background-color: #f44336;
border-radius: 50%;
padding: 2px 6px;
margin-left: 5px;
}
3 changes: 3 additions & 0 deletions public/css/menu-button.css
@@ -0,0 +1,3 @@
.menu-messages-count {
min-width: 1rem !important;
}
15 changes: 15 additions & 0 deletions public/css/messages.css
@@ -0,0 +1,15 @@
.messages-container {
padding-top: 50px;
}

.send-message-container {
position: absolute;
width: 100%;
left: 0;
bottom: 10px;
}

.message-compose-box {
padding: 10px !important;
border-radius: 5px;
}
58 changes: 57 additions & 1 deletion public/js/app.js
Expand Up @@ -6,8 +6,8 @@ $(() => $('#login-form').addClass('scale-in'));
$(() => $('.user-tile').addClass('scale-in')); $(() => $('.user-tile').addClass('scale-in'));


$(document).ready(function(){ $(document).ready(function(){
// the "href" attribute of .modal-trigger must specify the modal ID that wants to be triggered
$('.modal').modal(); $('.modal').modal();
Materialize.showStaggeredList('#messages-list')
}); });


$('.dropdown-button').dropdown({ $('.dropdown-button').dropdown({
Expand All @@ -25,3 +25,59 @@ $('.dropdown-button').dropdown({
$(document).ready(function(){ $(document).ready(function(){
$('.collapsible').collapsible(); $('.collapsible').collapsible();
}); });

$('#messages-list li').click(function() {
var message_id = this.value;
var message_icon = $('#inbox-icon-' + message_id);
if (message_icon.html().trim() != 'check') {
var params = new URLSearchParams();
params.append('action', 'mark_unread');
params.append('message_id', message_id);
axios.post('/api.php', params).then(function(response) {
message_icon.html('check');
message_icon.removeClass('red-text');
});
}
})

function addToSent(recipient, message) {
sentList = $('#sent-list');
var listItem = '<li><div class="collapsible-header">';
listItem += '<i class="material-icons">arrow_forward</i>';
listItem += recipient;
listItem += '</div><div class="collapsible-body"><span>';
listItem += message;
listItem += '</span></div></li>';
sentList.append(listItem);
}

$('#send-button').click(function() {
var recipient_elem = $('#recipient');
var compose_elem = $('#message-compose');
var user_id = $('#user-id').val();

if (!recipient_elem.val().trim()) {
recipient_elem.addClass('invalid');
$('#recipient-label').attr('data-error', 'This field is required');
return;
}

if (!compose_elem.val().trim()) return;

var params = new URLSearchParams();
params.append('action', 'send_message');
params.append('user_id', user_id);
params.append('recipient', recipient_elem.val());
params.append('message', compose_elem.val());
axios.post('/api.php', params).then(function(response) {
if (response.data[0] == 'unknown_recipient') {
recipient_elem.addClass('invalid');
$('#recipient-label').attr('data-error', 'User not found');
} else if (response.data[0] == 'success') {
addToSent(recipient_elem.val(), compose_elem.val());
recipient_elem.val('');
compose_elem.val('');
Materialize.toast('Message sent!', 4000)
}
})
});
105 changes: 105 additions & 0 deletions public/messages.php
@@ -0,0 +1,105 @@
<?php
defined('APP_DIR') or define('APP_DIR', __DIR__ . '/../app/');
include_once(APP_DIR . 'model/User.php');
include_once(APP_DIR . 'model/SecureMessage.php');
include_once(APP_DIR . 'include/http.php');

$data = User::authenticated();
if (!$data) {
Http::redirect('index.php');
}

$user = User::get($data->username);
$inbox = SecureMessage::all_to($user->id);
$outbox = SecureMessage::all_from($user->id);

include 'template/header.html';
include 'template/user_menu_button.php';
?>

<div class="row messages-container">
<div class="col l8 m10 s12 offset-l2 offset-m1">
<div class="row">
<div class="col s12">
<h1> Messages </h1>
</div>
<div class="col s12 card-panel">
<ul class="tabs">
<li class="tab col s3"><a href="#inbox">Inbox</a></li>
<li class="tab col s3"><a href="#sent">Sent</a></li>
</ul>
</div>
<div id="inbox" class="col s12">
<ul id="messages-list" class="collapsible popout" data-collapsible="accordion">
<?php foreach($inbox as $message) { ?>
<li value="<?php echo $message->id ?>">
<div class="collapsible-header">
<?php if ($message->is_read) { ?>
<i id="inbox-icon-<?php echo $message->id ?>"
class="material-icons">check
</i>
<?php } else { ?>
<i id="inbox-icon-<?php echo $message->id ?>"
class="material-icons red-text">fiber_new
</i>
<?php } ?>
<?php echo $message->sender()->username ?>
</div>
<div class="collapsible-body">
<span>
<?php echo $message->message ?>
</span>
</div>
</li>
<?php } ?>
</ul>
</div>
<div id="sent" class="col s12" style="display:none">
<ul id="sent-list" class="collapsible popout" data-collapsible="accordion">
<?php foreach($outbox as $message) { ?>
<li>
<div class="collapsible-header">
<?php if ($message->is_read) { ?>
<i class="material-icons green-text">check_circle</i>
<?php } else { ?>
<i class="material-icons">arrow_forward</i>
<?php } ?>
<?php echo $message->receiver()->username ?>
</div>
<div class="collapsible-body">
<span>
<?php echo $message->message ?>
</span>
</div>
</li>
<?php } ?>
</ul>
</div>
</div>
</div>
</div>

<div class="row send-message-container">
<div class="col l8 m10 s12 offset-l2 offset-m1 z-depth-3 message-compose-box">
<input type="hidden" id="user-id" value="<?php echo $user->id ?>">
<div class="input-field col s6">
<input placeholder="Enter their username" id="recipient" type="text" class="validate">
<label id="recipient-label" for="recipient">Recipient</label>
</div>
<div clas="col s12"></div>
<div class="input-field col s12">
<textarea placeholder="Craft a message..." id="message-compose" class="materialize-textarea"></textarea>
</div>
</div>
</div>

<div class="fixed-action-btn">
<button data-position="top" data-delay="50" data-tooltip="Send message"
id="send-button" class="modal-trigger btn-floating btn-large blue tooltipped">
<i class="large material-icons waves-effect waves-light">send</i>
</button>
</div>

<?php
include 'template/footer.html';
?>
1 change: 1 addition & 0 deletions public/template/footer.html
Expand Up @@ -4,6 +4,7 @@
integrity="sha256-hVVnYaiADRTO2PzUGmuLJr8BLUSjGIZsDYGmIJLv2b8=" integrity="sha256-hVVnYaiADRTO2PzUGmuLJr8BLUSjGIZsDYGmIJLv2b8="
crossorigin="anonymous"></script> crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/materialize/0.98.0/js/materialize.min.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/materialize/0.98.0/js/materialize.min.js"></script>
<script src="https://unpkg.com/axios/dist/axios.min.js"></script>
<script src="js/app.js"></script> <script src="js/app.js"></script>
</body> </body>
</html> </html>
17 changes: 16 additions & 1 deletion public/template/user_menu_button.php
@@ -1,15 +1,24 @@
<?php <?php
defined('APP_DIR') or define('APP_DIR', __DIR__ . '/../app/'); defined('APP_DIR') or define('APP_DIR', __DIR__ . '/../app/');
include_once(APP_DIR . 'model/User.php'); include_once(APP_DIR . 'model/User.php');
include_once(APP_DIR . 'model/SecureMessage.php');
include_once(APP_DIR . 'include/http.php'); include_once(APP_DIR . 'include/http.php');


$data = User::authenticated(); $data = User::authenticated();
$user = User::get($data->username);
$num_unread = SecureMessage::count_unread($user->id);

?> ?>


<div class="user-button-container"> <div class="user-button-container">
<!-- Dropdown Trigger --> <!-- Dropdown Trigger -->
<a class='dropdown-button btn user-button' href='#' data-activates='dropdown1'> <a class='dropdown-button btn user-button'data-activates='dropdown1'>
<?php echo $data->username ?> <?php echo $data->username ?>
<?php if ($num_unread > 0) { ?>
<span class="notification-count">
<?php echo $num_unread ?>
</span>
<?php } ?>
</a> </a>


<!-- Dropdown Structure --> <!-- Dropdown Structure -->
Expand All @@ -20,6 +29,12 @@ $data = User::authenticated();
<li><a href ="admin.php">Admin</a></li> <li><a href ="admin.php">Admin</a></li>
<?php } ?> <?php } ?>
<li class="divider"></li> <li class="divider"></li>
<li><a href="messages.php">
Messages
<span class="new badge menu-messages-count" data-badge-caption="">
<?php echo $num_unread ?>
</span>
</a></li>
<li><a href="logout.php">Sign out</a></li> <li><a href="logout.php">Sign out</a></li>
</ul> </ul>
</div> </div>

0 comments on commit 0017766

Please sign in to comment.