├── src ├── main │ ├── resources │ │ ├── src │ │ │ └── org │ │ │ │ └── privatechat │ │ │ │ ├── login │ │ │ │ ├── LoginStyle.scss │ │ │ │ ├── LoginView.html │ │ │ │ └── LoginController.js │ │ │ │ ├── shared │ │ │ │ ├── theme │ │ │ │ │ ├── Theme.scss │ │ │ │ │ ├── NavbarView.html │ │ │ │ │ └── Navbar.js │ │ │ │ └── websocket │ │ │ │ │ ├── WebSocket.js │ │ │ │ │ └── dependencies │ │ │ │ │ └── stompjs.js │ │ │ │ ├── index.scss │ │ │ │ ├── user │ │ │ │ ├── notification │ │ │ │ │ ├── NotificationView.html │ │ │ │ │ ├── NotificationStyle.scss │ │ │ │ │ ├── NotificationService.js │ │ │ │ │ ├── NotificationDirective.js │ │ │ │ │ └── NotificationController.js │ │ │ │ ├── UserService.js │ │ │ │ └── AuthService.js │ │ │ │ ├── friendslist │ │ │ │ ├── FriendsListView.html │ │ │ │ └── FriendsListController.js │ │ │ │ ├── registration │ │ │ │ ├── RegistrationStyle.scss │ │ │ │ ├── RegistrationController.js │ │ │ │ └── RegistrationView.html │ │ │ │ ├── chat │ │ │ │ ├── ChatStyle.scss │ │ │ │ ├── ChatView.html │ │ │ │ ├── ChatService.js │ │ │ │ └── ChatController.js │ │ │ │ ├── index.html │ │ │ │ └── index.js │ │ ├── application.properties │ │ ├── package.json │ │ └── gulpfile.js │ └── java │ │ └── org │ │ └── privatechat │ │ ├── user │ │ ├── strategies │ │ │ ├── IUserRetrievalStrategy.java │ │ │ ├── UserRetrievalByIdStrategy.java │ │ │ ├── UserRetrievalByEmailStrategy.java │ │ │ └── UserRetrievalBySecurityContextStrategy.java │ │ ├── interfaces │ │ │ ├── INotificationController.java │ │ │ ├── IUserPresenceService.java │ │ │ ├── IUserController.java │ │ │ └── IUserService.java │ │ ├── exceptions │ │ │ ├── IsSameUserException.java │ │ │ └── UserNotFoundException.java │ │ ├── controllers │ │ │ ├── NotificationController.java │ │ │ └── UserController.java │ │ ├── mappers │ │ │ └── UserMapper.java │ │ ├── DTOs │ │ │ ├── NotificationDTO.java │ │ │ ├── UserDTO.java │ │ │ └── RegistrationDTO.java │ │ ├── repositories │ │ │ └── UserRepository.java │ │ ├── services │ │ │ ├── UserPresenceService.java │ │ │ └── UserService.java │ │ └── models │ │ │ └── User.java │ │ ├── shared │ │ ├── interfaces │ │ │ └── IErrorHandlerController.java │ │ ├── exceptions │ │ │ └── ValidationException.java │ │ ├── http │ │ │ └── JSONResponseHelper.java │ │ ├── database │ │ │ ├── seedDatabase.sql │ │ │ └── createDatabase.sql │ │ ├── websocket │ │ │ └── WebSocketConfig.java │ │ ├── error │ │ │ └── ErrorHandlerController.java │ │ └── security │ │ │ └── ApplicationSecurity.java │ │ ├── chat │ │ ├── DTOs │ │ │ ├── ChatChannelInitializationDTO.java │ │ │ ├── ChatMessageDTO.java │ │ │ └── EstablishedChatChannelDTO.java │ │ ├── interfaces │ │ │ ├── IChatService.java │ │ │ └── IChatChannelController.java │ │ ├── repositories │ │ │ ├── ChatMessageRepository.java │ │ │ └── ChatChannelRepository.java │ │ ├── mappers │ │ │ └── ChatMessageMapper.java │ │ ├── models │ │ │ ├── ChatChannel.java │ │ │ └── ChatMessage.java │ │ ├── controllers │ │ │ └── ChatChannelController.java │ │ └── services │ │ │ └── ChatService.java │ │ └── PrivateChatApplication.java └── test │ └── java │ └── com │ └── chat │ └── ChatApplicationTests.java ├── README.md └── pom.xml /src/main/resources/src/org/privatechat/login/LoginStyle.scss: -------------------------------------------------------------------------------- 1 | .login { 2 | max-width: 430px; 3 | padding: 15px; 4 | margin: 0 auto; 5 | } -------------------------------------------------------------------------------- /src/main/resources/src/org/privatechat/shared/theme/Theme.scss: -------------------------------------------------------------------------------- 1 | body { 2 | padding-top: 40px; 3 | padding-bottom: 40px; 4 | background-color: #eee !important; 5 | } -------------------------------------------------------------------------------- /src/main/resources/src/org/privatechat/index.scss: -------------------------------------------------------------------------------- 1 | @import 'shared/theme/Theme'; 2 | @import 'login/LoginStyle'; 3 | @import 'registration/RegistrationStyle'; 4 | @import 'chat/ChatStyle'; 5 | @import 'user/notification/NotificationStyle'; -------------------------------------------------------------------------------- /src/main/java/org/privatechat/user/strategies/IUserRetrievalStrategy.java: -------------------------------------------------------------------------------- 1 | package org.privatechat.user.strategies; 2 | 3 | import org.privatechat.user.models.User; 4 | 5 | public interface IUserRetrievalStrategy { 6 | public User getUser(T userIdentifier); 7 | } -------------------------------------------------------------------------------- /src/main/resources/src/org/privatechat/user/notification/NotificationView.html: -------------------------------------------------------------------------------- 1 |
2 | 5 |
-------------------------------------------------------------------------------- /src/main/java/org/privatechat/shared/interfaces/IErrorHandlerController.java: -------------------------------------------------------------------------------- 1 | package org.privatechat.shared.interfaces; 2 | 3 | import org.springframework.http.ResponseEntity; 4 | 5 | public interface IErrorHandlerController { 6 | ResponseEntity error(); 7 | 8 | ResponseEntity exception(Exception exception); 9 | } -------------------------------------------------------------------------------- /src/main/java/org/privatechat/user/interfaces/INotificationController.java: -------------------------------------------------------------------------------- 1 | package org.privatechat.user.interfaces; 2 | 3 | import org.springframework.messaging.handler.annotation.DestinationVariable; 4 | 5 | public interface INotificationController { 6 | String notifications(@DestinationVariable long userId, String message); 7 | } -------------------------------------------------------------------------------- /src/main/java/org/privatechat/user/interfaces/IUserPresenceService.java: -------------------------------------------------------------------------------- 1 | package org.privatechat.user.interfaces; 2 | 3 | import org.springframework.messaging.Message; 4 | import org.springframework.messaging.MessageChannel; 5 | 6 | public interface IUserPresenceService { 7 | void postSend(Message message, MessageChannel channel, boolean sent); 8 | } -------------------------------------------------------------------------------- /src/main/resources/src/org/privatechat/user/notification/NotificationStyle.scss: -------------------------------------------------------------------------------- 1 | .notifications { 2 | height: 113px; 3 | width: 217px; 4 | background-color: white; 5 | position: fixed; 6 | overflow-y: auto; 7 | margin-left: -216px; 8 | border-color: grey; 9 | border-width: 2px; 10 | border-style: solid; 11 | padding: 7px; 12 | } -------------------------------------------------------------------------------- /src/main/resources/src/org/privatechat/user/notification/NotificationService.js: -------------------------------------------------------------------------------- 1 | angular.module('org.privatechat.user.notification.NotificationService', [ 2 | 3 | ]) 4 | .factory('NotificationService', [ 5 | function() { 6 | var notifications = []; 7 | 8 | return { 9 | notifications: notifications, 10 | }; 11 | } 12 | ]); -------------------------------------------------------------------------------- /src/main/resources/src/org/privatechat/user/notification/NotificationDirective.js: -------------------------------------------------------------------------------- 1 | angular.module('org.privatechat.user.notification.NotificationDirective', [ 2 | 'org.privatechat.user.notification.NotificationController' 3 | ]) 4 | .directive('notifications', function() { 5 | return { 6 | restrict: 'E', 7 | templateUrl: 'user/notification/NotificationView.html', 8 | controller: 'NotificationController' 9 | }; 10 | }); -------------------------------------------------------------------------------- /src/main/resources/application.properties: -------------------------------------------------------------------------------- 1 | server.contextPath=/ 2 | spring.datasource.url = jdbc:mysql://localhost:3306/chat 3 | spring.datasource.username = root 4 | spring.datasource.password = pass 5 | spring.datasource.testWhileIdle = true 6 | spring.datasource.validationQuery = SELECT 1 7 | spring.jpa.show-sql = false 8 | spring.jpa.hibernate.naming-strategy = org.hibernate.cfg.DefaultNamingStrategy 9 | spring.jpa.properties.hibernate.dialect = org.hibernate.dialect.MySQL5Dialect -------------------------------------------------------------------------------- /src/main/resources/src/org/privatechat/friendslist/FriendsListView.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 |
5 |

Friends List

6 |
7 |
8 | 11 |
12 |
-------------------------------------------------------------------------------- /src/main/java/org/privatechat/shared/exceptions/ValidationException.java: -------------------------------------------------------------------------------- 1 | package org.privatechat.shared.exceptions; 2 | 3 | public class ValidationException extends Exception { 4 | private static final long serialVersionUID = 1L; 5 | public ValidationException() { super(); } 6 | public ValidationException(String message) { super(message); } 7 | public ValidationException(String message, Throwable cause) { super(message, cause); } 8 | public ValidationException(Throwable cause) { super(cause); } 9 | } -------------------------------------------------------------------------------- /src/main/java/org/privatechat/user/exceptions/IsSameUserException.java: -------------------------------------------------------------------------------- 1 | package org.privatechat.user.exceptions; 2 | 3 | public class IsSameUserException extends Exception { 4 | private static final long serialVersionUID = 1L; 5 | public IsSameUserException() { super(); } 6 | public IsSameUserException(String message) { super(message); } 7 | public IsSameUserException(String message, Throwable cause) { super(message, cause); } 8 | public IsSameUserException(Throwable cause) { super(cause); } 9 | } 10 | -------------------------------------------------------------------------------- /src/main/java/org/privatechat/user/exceptions/UserNotFoundException.java: -------------------------------------------------------------------------------- 1 | package org.privatechat.user.exceptions; 2 | 3 | public class UserNotFoundException extends Exception { 4 | private static final long serialVersionUID = 1L; 5 | public UserNotFoundException() { super(); } 6 | public UserNotFoundException(String message) { super(message); } 7 | public UserNotFoundException(String message, Throwable cause) { super(message, cause); } 8 | public UserNotFoundException(Throwable cause) { super(cause); } 9 | } 10 | -------------------------------------------------------------------------------- /src/main/resources/src/org/privatechat/registration/RegistrationStyle.scss: -------------------------------------------------------------------------------- 1 | .register { 2 | max-width: 530px; 3 | padding: 15px; 4 | margin: 0 auto; 5 | } 6 | 7 | .register > .form-control { 8 | position: relative; 9 | height: auto; 10 | -webkit-box-sizing: border-box; 11 | -moz-box-sizing: border-box; 12 | box-sizing: border-box; 13 | padding: 10px; 14 | font-size: 16px; 15 | } 16 | 17 | .register > label { 18 | margin-top: 10px; 19 | color: #737373; 20 | font-style: italic; 21 | font-weight: 300; 22 | } -------------------------------------------------------------------------------- /src/test/java/com/chat/ChatApplicationTests.java: -------------------------------------------------------------------------------- 1 | package com.chat; 2 | 3 | import org.junit.Test; 4 | import org.junit.runner.RunWith; 5 | import org.privatechat.PrivateChatApplication; 6 | import org.springframework.boot.test.SpringApplicationConfiguration; 7 | import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; 8 | 9 | @RunWith(SpringJUnit4ClassRunner.class) 10 | @SpringApplicationConfiguration(classes = PrivateChatApplication.class) 11 | public class ChatApplicationTests { 12 | 13 | @Test 14 | public void contextLoads() {} 15 | 16 | } 17 | -------------------------------------------------------------------------------- /src/main/resources/src/org/privatechat/shared/theme/NavbarView.html: -------------------------------------------------------------------------------- 1 | 14 | 15 |
16 |
-------------------------------------------------------------------------------- /src/main/java/org/privatechat/shared/http/JSONResponseHelper.java: -------------------------------------------------------------------------------- 1 | package org.privatechat.shared.http; 2 | 3 | import com.google.gson.GsonBuilder; 4 | import org.springframework.http.HttpStatus; 5 | import org.springframework.http.ResponseEntity; 6 | 7 | public class JSONResponseHelper { 8 | public static ResponseEntity createResponse(T responseObj, HttpStatus stat) { 9 | return new ResponseEntity( 10 | new GsonBuilder() 11 | .disableHtmlEscaping() 12 | .create() 13 | .toJson(responseObj) 14 | .toString(), 15 | stat 16 | ); 17 | } 18 | } -------------------------------------------------------------------------------- /src/main/java/org/privatechat/chat/DTOs/ChatChannelInitializationDTO.java: -------------------------------------------------------------------------------- 1 | package org.privatechat.chat.DTOs; 2 | 3 | public class ChatChannelInitializationDTO { 4 | private long userIdOne; 5 | 6 | private long userIdTwo; 7 | 8 | public ChatChannelInitializationDTO() {} 9 | 10 | public void setUserIdOne(long userIdOne) { 11 | this.userIdOne = userIdOne; 12 | } 13 | 14 | public void setUserIdTwo(long userIdTwo) { 15 | this.userIdTwo = userIdTwo; 16 | } 17 | 18 | public long getUserIdOne() { 19 | return this.userIdOne; 20 | } 21 | 22 | public long getUserIdTwo() { 23 | return userIdTwo; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/main/resources/src/org/privatechat/chat/ChatStyle.scss: -------------------------------------------------------------------------------- 1 | .from-recipient { 2 | color: #5F9EA0 !important; 3 | } 4 | 5 | .chat-message { 6 | color: #00BFFF; 7 | } 8 | 9 | #chat-area { 10 | overflow-y: auto; 11 | height: 500px; 12 | max-height: 600px; 13 | } 14 | 15 | // TODO: fix this because it's kind of hacky... 16 | // deals with auto-height sizing 17 | @media (max-height: 399px) { 18 | #chat-area { 19 | max-height: 100px; 20 | } 21 | } 22 | @media (min-height: 400px) { 23 | #chat-area { 24 | max-height: 300px; 25 | } 26 | } 27 | @media (min-height: 700px) { 28 | #chat-area { 29 | max-height: 600px; 30 | } 31 | } -------------------------------------------------------------------------------- /src/main/java/org/privatechat/user/controllers/NotificationController.java: -------------------------------------------------------------------------------- 1 | package org.privatechat.user.controllers; 2 | 3 | import org.privatechat.user.interfaces.INotificationController; 4 | import org.springframework.messaging.handler.annotation.DestinationVariable; 5 | import org.springframework.messaging.handler.annotation.SendTo; 6 | import org.springframework.web.bind.annotation.*; 7 | 8 | @RestController 9 | public class NotificationController implements INotificationController { 10 | 11 | @SendTo("/topic/user.notification.{userId}") 12 | public String notifications(@DestinationVariable long userId, String message) { 13 | return message; 14 | } 15 | } -------------------------------------------------------------------------------- /src/main/java/org/privatechat/shared/database/seedDatabase.sql: -------------------------------------------------------------------------------- 1 | USE chat; 2 | 3 | INSERT INTO user ( 4 | email, 5 | password, 6 | role, 7 | fullName 8 | ) VALUES ( 9 | 'evan@test.com', '$2a$10$qNl6oSAKpNe1Bqc6CTvJNOWiYgwOPizqJrxGUsv4WcZwqphX5Og6G', 'STANDARD-ROLE', 'Evan Delgado' 10 | ), ( 11 | 'annie@test.com', '$2a$10$qNl6oSAKpNe1Bqc6CTvJNOWiYgwOPizqJrxGUsv4WcZwqphX5Og6G', 'STANDARD-ROLE', 'Annie Smith' 12 | ), ( 13 | 'joe@test.com', '$2a$10$qNl6oSAKpNe1Bqc6CTvJNOWiYgwOPizqJrxGUsv4WcZwqphX5Og6G', 'STANDARD-ROLE', 'Joe Kelly' 14 | ), ( 15 | 'roger@test.com', '$2a$10$qNl6oSAKpNe1Bqc6CTvJNOWiYgwOPizqJrxGUsv4WcZwqphX5Og6G', 'STANDARD-ROLE', 'Roger Ford' 16 | ); -------------------------------------------------------------------------------- /src/main/java/org/privatechat/user/mappers/UserMapper.java: -------------------------------------------------------------------------------- 1 | package org.privatechat.user.mappers; 2 | 3 | import java.util.ArrayList; 4 | import java.util.List; 5 | import org.privatechat.user.DTOs.UserDTO; 6 | import org.privatechat.user.models.User; 7 | 8 | public class UserMapper { 9 | public static List mapUsersToUserDTOs(List users) { 10 | List dtos = new ArrayList(); 11 | 12 | for(User user : users) { 13 | dtos.add( 14 | new UserDTO( 15 | user.getId(), 16 | user.getEmail(), 17 | user.getFullName() 18 | ) 19 | ); 20 | } 21 | 22 | return dtos; 23 | } 24 | } -------------------------------------------------------------------------------- /src/main/resources/src/org/privatechat/friendslist/FriendsListController.js: -------------------------------------------------------------------------------- 1 | angular.module('org.privatechat.friendslist.FriendsListController', [ 2 | 'org.privatechat.shared.theme.Navbar', 3 | 'org.privatechat.user.UserService' 4 | ]).controller('FriendsListController', ['$scope', 'UserService', 5 | function($scope, UserService) { 6 | var self = $scope; 7 | 8 | var getFriendsList = function() { 9 | return UserService 10 | .getFriendslist() 11 | .then(function(users) { 12 | self.users = users.data; 13 | }); 14 | }; 15 | 16 | var construct = function() { 17 | getFriendsList(); 18 | }; 19 | 20 | construct(); 21 | } 22 | ]); -------------------------------------------------------------------------------- /src/main/java/org/privatechat/user/DTOs/NotificationDTO.java: -------------------------------------------------------------------------------- 1 | package org.privatechat.user.DTOs; 2 | 3 | public class NotificationDTO { 4 | private String type; 5 | 6 | private String contents; 7 | 8 | private long fromUserId; 9 | 10 | public NotificationDTO() {} 11 | 12 | public NotificationDTO(String type, String contents, long fromUserId) { 13 | this.type = type; 14 | this.contents = contents; 15 | this.fromUserId = fromUserId; 16 | } 17 | 18 | public String getType() { 19 | return this.type; 20 | } 21 | 22 | public String getContent() { 23 | return this.contents; 24 | } 25 | 26 | public long getfromUserId() { 27 | return this.fromUserId; 28 | } 29 | } -------------------------------------------------------------------------------- /src/main/resources/src/org/privatechat/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Private Chat Project 8 | 9 | 10 | 11 | 12 | 13 |
14 | 15 |
16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /src/main/java/org/privatechat/user/strategies/UserRetrievalByIdStrategy.java: -------------------------------------------------------------------------------- 1 | package org.privatechat.user.strategies; 2 | 3 | import org.privatechat.user.models.User; 4 | import org.privatechat.user.repositories.UserRepository; 5 | import org.springframework.beans.factory.annotation.Autowired; 6 | import org.springframework.stereotype.Service; 7 | 8 | @Service 9 | public class UserRetrievalByIdStrategy implements IUserRetrievalStrategy { 10 | private UserRepository userRepository; 11 | 12 | @Autowired 13 | public UserRetrievalByIdStrategy(UserRepository userRepository) { 14 | this.userRepository = userRepository; 15 | } 16 | 17 | @Override 18 | public User getUser(Long userIdentifier) { 19 | return userRepository.findById(userIdentifier); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/main/resources/src/org/privatechat/chat/ChatView.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 |
Private Chat
5 | 6 |
7 |
8 |

9 | {{msg.author}}: {{msg.contents}} 10 |

11 |
12 |
13 | 14 |
15 | 16 | 17 |
18 |
-------------------------------------------------------------------------------- /src/main/java/org/privatechat/user/strategies/UserRetrievalByEmailStrategy.java: -------------------------------------------------------------------------------- 1 | package org.privatechat.user.strategies; 2 | 3 | import org.privatechat.user.models.User; 4 | import org.privatechat.user.repositories.UserRepository; 5 | import org.springframework.beans.factory.annotation.Autowired; 6 | import org.springframework.stereotype.Service; 7 | 8 | @Service 9 | public class UserRetrievalByEmailStrategy implements IUserRetrievalStrategy { 10 | private UserRepository userRepository; 11 | 12 | @Autowired 13 | public UserRetrievalByEmailStrategy(UserRepository userRepository) { 14 | this.userRepository = userRepository; 15 | } 16 | 17 | @Override 18 | public User getUser(String userIdentifier) { 19 | return userRepository.findByEmail(userIdentifier); 20 | } 21 | } -------------------------------------------------------------------------------- /src/main/resources/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "private-chat-project", 3 | "version": "0.0.1", 4 | "main": "index.js", 5 | "author": "Matthew Vita", 6 | "license": "MIT", 7 | "dependencies": { 8 | "angular": "1.3.14", 9 | "angular-filter": "0.5.4", 10 | "angular-route": "1.3.14", 11 | "bootstrap": "^3.3.6", 12 | "gulp": "3.8.11", 13 | "gulp-concat": "2.5.2", 14 | "gulp-cssmin": "0.1.6", 15 | "gulp-jsvalidate": "^2.1.0", 16 | "gulp-minify-css": "1.0.0", 17 | "gulp-ng-annotate": "0.5.2", 18 | "gulp-rename": "1.2.0", 19 | "gulp-sass": "2.1.0", 20 | "gulp-sourcemaps": "1.5.0", 21 | "gulp-uglify": "1.1.0", 22 | "jquery": "2.1.3", 23 | "jquery.cookie": "1.4.1", 24 | "run-sequence": "1.1.0", 25 | "underscore": "^1.8.3" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/main/java/org/privatechat/chat/interfaces/IChatService.java: -------------------------------------------------------------------------------- 1 | package org.privatechat.chat.interfaces; 2 | 3 | import java.util.List; 4 | import org.privatechat.chat.DTOs.ChatChannelInitializationDTO; 5 | import org.privatechat.chat.DTOs.ChatMessageDTO; 6 | import org.privatechat.user.exceptions.IsSameUserException; 7 | import org.privatechat.user.exceptions.UserNotFoundException; 8 | import org.springframework.beans.BeansException; 9 | 10 | public interface IChatService { 11 | String establishChatSession(ChatChannelInitializationDTO chatChannelInitializationDTO) 12 | throws IsSameUserException, BeansException, UserNotFoundException; 13 | 14 | void submitMessage(ChatMessageDTO chatMessageDTO) 15 | throws BeansException, UserNotFoundException; 16 | 17 | List getExistingChatMessages(String channelUuid); 18 | } -------------------------------------------------------------------------------- /src/main/java/org/privatechat/user/interfaces/IUserController.java: -------------------------------------------------------------------------------- 1 | package org.privatechat.user.interfaces; 2 | 3 | import java.security.Principal; 4 | import org.privatechat.shared.exceptions.ValidationException; 5 | import org.privatechat.user.DTOs.RegistrationDTO; 6 | import org.privatechat.user.exceptions.UserNotFoundException; 7 | import org.springframework.http.ResponseEntity; 8 | import org.springframework.web.bind.annotation.RequestBody; 9 | 10 | public interface IUserController { 11 | ResponseEntity register(@RequestBody RegistrationDTO registeringUser) 12 | throws ValidationException; 13 | 14 | ResponseEntity retrieveRequestingUserFriendsList(Principal principal) 15 | throws UserNotFoundException; 16 | 17 | ResponseEntity retrieveRequestUserInfo() 18 | throws UserNotFoundException; 19 | } 20 | -------------------------------------------------------------------------------- /src/main/resources/src/org/privatechat/login/LoginView.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/main/java/org/privatechat/user/repositories/UserRepository.java: -------------------------------------------------------------------------------- 1 | package org.privatechat.user.repositories; 2 | 3 | import org.privatechat.user.models.User; 4 | import org.springframework.data.jpa.repository.Query; 5 | import org.springframework.data.repository.CrudRepository; 6 | import org.springframework.data.repository.query.Param; 7 | import org.springframework.stereotype.Repository; 8 | import javax.transaction.Transactional; 9 | import java.util.List; 10 | 11 | @Transactional 12 | @Repository 13 | public interface UserRepository extends CrudRepository { 14 | 15 | public User findByEmail(String email); 16 | 17 | public User findById(long id); 18 | 19 | @Query(" FROM" 20 | + " User u" 21 | + " WHERE" 22 | + " u.email IS NOT :excludedUser") 23 | public List findFriendsListFor(@Param("excludedUser") String excludedUser); 24 | } -------------------------------------------------------------------------------- /src/main/java/org/privatechat/user/DTOs/UserDTO.java: -------------------------------------------------------------------------------- 1 | package org.privatechat.user.DTOs; 2 | 3 | public class UserDTO { 4 | private long id; 5 | 6 | private String email; 7 | 8 | private String fullName; 9 | 10 | public UserDTO() {} 11 | 12 | public UserDTO(long id, String email, String fullName) { 13 | this.id = id; 14 | this.email = email; 15 | this.fullName = fullName; 16 | } 17 | 18 | public void setId(long id) { 19 | this.id = id; 20 | } 21 | 22 | public long getId() { 23 | return this.id; 24 | } 25 | 26 | public void setEmail(String email) { 27 | this.email = email; 28 | } 29 | 30 | public String getEmail() { 31 | return this.email; 32 | } 33 | 34 | public String getFullName() { 35 | return this.fullName; 36 | } 37 | 38 | public void setFullName(String fullName) { 39 | this.fullName = fullName; 40 | } 41 | } -------------------------------------------------------------------------------- /src/main/resources/src/org/privatechat/shared/theme/Navbar.js: -------------------------------------------------------------------------------- 1 | angular.module('org.privatechat.shared.theme.Navbar', [ 2 | 'org.privatechat.user.AuthService', 3 | 'org.privatechat.user.notification.NotificationDirective' 4 | ]) 5 | .directive('navbar', function() { 6 | return { 7 | restrict: 'E', 8 | templateUrl: 'shared/theme/NavbarView.html', 9 | controller: ['$scope', '$rootScope', 'AuthService', 10 | function($scope, $rootScope, AuthService) { 11 | var self = $scope; 12 | 13 | self.hasActiveSession = false; 14 | 15 | self.construct = function() { 16 | self.hasActiveSession = AuthService.hasActiveSession(); 17 | }; 18 | 19 | self.logout = function() { 20 | $rootScope.$broadcast('logout-event'); 21 | }; 22 | 23 | self.construct(); 24 | } 25 | ] 26 | }; 27 | }); -------------------------------------------------------------------------------- /src/main/java/org/privatechat/user/DTOs/RegistrationDTO.java: -------------------------------------------------------------------------------- 1 | package org.privatechat.user.DTOs; 2 | 3 | public class RegistrationDTO { 4 | private String email; 5 | private String password; 6 | private String fullName; 7 | 8 | public RegistrationDTO() {} 9 | 10 | public RegistrationDTO(String email, String fullName, String password) { 11 | this.email = email; 12 | this.fullName = fullName; 13 | this.password = password; 14 | } 15 | 16 | public void setFullName(String fullName) { 17 | this.fullName = fullName; 18 | } 19 | 20 | public String getFullName() { 21 | return this.fullName; 22 | } 23 | 24 | public void setEmail(String email) { 25 | this.email = email; 26 | } 27 | 28 | public void setPassword(String password) { 29 | this.password = password; 30 | } 31 | 32 | public String getEmail() { 33 | return this.email; 34 | } 35 | 36 | public String getPassword() { 37 | return this.password; 38 | } 39 | } -------------------------------------------------------------------------------- /src/main/java/org/privatechat/PrivateChatApplication.java: -------------------------------------------------------------------------------- 1 | package org.privatechat; 2 | 3 | import org.privatechat.shared.security.ApplicationSecurity; 4 | import org.springframework.boot.SpringApplication; 5 | import org.springframework.boot.autoconfigure.SpringBootApplication; 6 | import org.springframework.context.annotation.Bean; 7 | import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; 8 | import org.springframework.session.data.redis.config.annotation.web.http.EnableRedisHttpSession; 9 | 10 | @SpringBootApplication 11 | @EnableRedisHttpSession 12 | public class PrivateChatApplication { 13 | @Bean 14 | public WebSecurityConfigurerAdapter webSecurityConfigurerAdapter() { 15 | return new ApplicationSecurity(); 16 | } 17 | 18 | public static void main(String[] args) { 19 | // mvn spring-boot:run -Drun.jvmArguments='-Dserver.port={PORT}' 20 | SpringApplication.run(PrivateChatApplication.class, args); 21 | } 22 | } -------------------------------------------------------------------------------- /src/main/java/org/privatechat/chat/DTOs/ChatMessageDTO.java: -------------------------------------------------------------------------------- 1 | package org.privatechat.chat.DTOs; 2 | 3 | public class ChatMessageDTO { 4 | private String contents; 5 | 6 | private long fromUserId; 7 | 8 | private long toUserId; 9 | 10 | public ChatMessageDTO(){} 11 | 12 | public ChatMessageDTO(String contents, long fromUserId, long toUserId) { 13 | this.contents = contents; 14 | this.fromUserId = fromUserId; 15 | this.toUserId = toUserId; 16 | } 17 | 18 | public String getContents() { 19 | return this.contents; 20 | } 21 | 22 | public void setToUserId(long toUserId) { 23 | this.toUserId = toUserId; 24 | } 25 | 26 | public long getToUserId() { 27 | return this.toUserId; 28 | } 29 | 30 | public void setContents(String contents) { 31 | this.contents = contents; 32 | } 33 | 34 | public void setFromUserId(long userId) { 35 | this.fromUserId = userId; 36 | } 37 | 38 | public long getFromUserId() { 39 | return this.fromUserId; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/main/resources/src/org/privatechat/registration/RegistrationController.js: -------------------------------------------------------------------------------- 1 | angular.module('org.privatechat.registration.RegistrationController', [ 2 | 'org.privatechat.user.AuthService', 3 | ]).controller('RegistrationController', ['$scope', '$location', 'AuthService', 4 | function($scope, $location, AuthService) { 5 | var self = $scope; 6 | 7 | self.email = null; 8 | self.password = null; 9 | self.fullName = null; 10 | 11 | var construct = function() {}; 12 | 13 | var routeToLogin = function() { 14 | $location.path('/login'); 15 | }; 16 | 17 | self.attemptRegistration = function() { 18 | AuthService.register({ 19 | email: self.email, 20 | password: self.password, 21 | fullName: self.fullName, 22 | }) 23 | .then(function() { 24 | alert('Successfully registered.'); 25 | routeToLogin(); 26 | }) 27 | .catch(function(err) { 28 | alert(err.data); 29 | }); 30 | }; 31 | 32 | construct(); 33 | } 34 | ]); -------------------------------------------------------------------------------- /src/main/java/org/privatechat/shared/database/createDatabase.sql: -------------------------------------------------------------------------------- 1 | CREATE DATABASE chat; 2 | 3 | USE chat; 4 | 5 | CREATE TABLE user ( 6 | id INT NOT NULL AUTO_INCREMENT, 7 | email VARCHAR(64) NOT NULL, 8 | password VARCHAR(256) NOT NULL, 9 | role VARCHAR(45) DEFAULT NULL, 10 | fullName VARCHAR(64) NOT NULL, 11 | isPresent BIT DEFAULT 0, 12 | PRIMARY KEY (id), 13 | UNIQUE KEY username_UNIQUE (email) 14 | ); 15 | 16 | CREATE TABLE chatChannel ( 17 | uuid VARCHAR(256) NOT NULL, 18 | userIdOne INT NOT NULL, 19 | userIdTwo INT NOT NULL, 20 | PRIMARY KEY (uuid), 21 | FOREIGN KEY (userIdOne) REFERENCES user(id), 22 | FOREIGN KEY (userIdTwo) REFERENCES user(id) 23 | ); 24 | 25 | CREATE TABLE chatMessage ( 26 | id INT NOT NULL AUTO_INCREMENT, 27 | authorUserId INT NOT NULL, 28 | recipientUserId INT NOT NULL, 29 | contents TEXT NOT NULL, 30 | timeSent TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, 31 | PRIMARY KEY (id), 32 | FOREIGN KEY (authorUserId) REFERENCES user(id), 33 | FOREIGN KEY (recipientUserId) REFERENCES user(id) 34 | ); 35 | -------------------------------------------------------------------------------- /src/main/resources/src/org/privatechat/login/LoginController.js: -------------------------------------------------------------------------------- 1 | angular.module('org.privatechat.login.LoginController', [ 2 | 'org.privatechat.user.AuthService', 3 | 'org.privatechat.user.UserService', 4 | ]).controller('LoginController', ['$scope', '$location', 'AuthService', 'UserService', 5 | function($scope, $location, AuthService, UserService, WebSocketConnection) { 6 | var self = $scope; 7 | 8 | self.email = null; 9 | self.password = null; 10 | 11 | var construct = function() {}; 12 | 13 | var routeToFriendsList = function() { 14 | $location.path('/friendslist'); 15 | }; 16 | 17 | self.attemptLogin = function() { 18 | AuthService.login({ 19 | username: self.email, 20 | password: self.password 21 | }) 22 | .then(function() { 23 | return UserService.establishUserInfo(); 24 | }) 25 | .then(function() { 26 | routeToFriendsList(); 27 | }) 28 | .catch(function() { 29 | alert('Invalid credentials.'); 30 | }); 31 | }; 32 | 33 | construct(); 34 | } 35 | ]); -------------------------------------------------------------------------------- /src/main/resources/src/org/privatechat/chat/ChatService.js: -------------------------------------------------------------------------------- 1 | angular.module('org.privatechat.chat.ChatService', [ 2 | 3 | ]) 4 | .factory('ChatService', ['$http', 5 | function($http) { 6 | 7 | var establishChatSession = function(userIdOne, userIdTwo) { 8 | return $http({ 9 | method: 'PUT', 10 | url: '/api/private-chat/channel', 11 | data: { 12 | userIdOne: userIdOne, 13 | userIdTwo: userIdTwo 14 | }, 15 | headers: { 16 | 'Content-Type': 'application/json' 17 | } 18 | }); 19 | }; 20 | 21 | var getExistingChatSessionMessages = function(channelUuid) { 22 | return $http({ 23 | method: 'GET', 24 | url: '/api/private-chat/channel/' + channelUuid, 25 | headers: { 26 | 'Content-Type': 'application/json' 27 | } 28 | }); 29 | }; 30 | 31 | return { 32 | establishChatSession: establishChatSession, 33 | getExistingChatSessionMessages: getExistingChatSessionMessages, 34 | }; 35 | } 36 | ]); -------------------------------------------------------------------------------- /src/main/java/org/privatechat/chat/repositories/ChatMessageRepository.java: -------------------------------------------------------------------------------- 1 | package org.privatechat.chat.repositories; 2 | 3 | import java.util.List; 4 | import javax.transaction.Transactional; 5 | import org.privatechat.chat.models.ChatMessage; 6 | import org.springframework.data.domain.Pageable; 7 | import org.springframework.data.jpa.repository.Query; 8 | import org.springframework.data.repository.CrudRepository; 9 | import org.springframework.data.repository.query.Param; 10 | import org.springframework.stereotype.Repository; 11 | 12 | @Transactional 13 | @Repository 14 | public interface ChatMessageRepository extends CrudRepository { 15 | @Query(" FROM" 16 | + " ChatMessage m" 17 | + " WHERE" 18 | + " m.authorUser.id IN (:userIdOne, :userIdTwo)" 19 | + " AND" 20 | + " m.recipientUser.id IN (:userIdOne, :userIdTwo)" 21 | + " ORDER BY" 22 | + " m.timeSent" 23 | + " DESC") 24 | public List getExistingChatMessages( 25 | @Param("userIdOne") long userIdOne, @Param("userIdTwo") long userIdTwo, Pageable pageable); 26 | } 27 | -------------------------------------------------------------------------------- /src/main/java/org/privatechat/chat/mappers/ChatMessageMapper.java: -------------------------------------------------------------------------------- 1 | package org.privatechat.chat.mappers; 2 | 3 | import java.util.ArrayList; 4 | import java.util.List; 5 | import org.privatechat.chat.DTOs.ChatMessageDTO; 6 | import org.privatechat.chat.models.ChatMessage; 7 | import org.privatechat.user.models.User; 8 | 9 | public class ChatMessageMapper { 10 | public static List mapMessagesToChatDTOs(List chatMessages) { 11 | List dtos = new ArrayList(); 12 | 13 | for(ChatMessage chatMessage : chatMessages) { 14 | dtos.add( 15 | new ChatMessageDTO( 16 | chatMessage.getContents(), 17 | chatMessage.getAuthorUser().getId(), 18 | chatMessage.getRecipientUser().getId() 19 | ) 20 | ); 21 | } 22 | 23 | return dtos; 24 | } 25 | 26 | public static ChatMessage mapChatDTOtoMessage(ChatMessageDTO dto) { 27 | return new ChatMessage( 28 | 29 | // only need the id for mapping 30 | new User(dto.getFromUserId()), 31 | new User(dto.getToUserId()), 32 | 33 | dto.getContents() 34 | ); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/main/java/org/privatechat/user/strategies/UserRetrievalBySecurityContextStrategy.java: -------------------------------------------------------------------------------- 1 | package org.privatechat.user.strategies; 2 | 3 | import org.privatechat.user.models.User; 4 | import org.privatechat.user.repositories.UserRepository; 5 | import org.springframework.beans.factory.annotation.Autowired; 6 | import org.springframework.security.core.context.SecurityContext; 7 | import org.springframework.stereotype.Service; 8 | 9 | @Service 10 | public class UserRetrievalBySecurityContextStrategy implements IUserRetrievalStrategy { 11 | private UserRepository userRepository; 12 | 13 | @Autowired 14 | public UserRetrievalBySecurityContextStrategy(UserRepository userRepository) { 15 | this.userRepository = userRepository; 16 | } 17 | 18 | @Override 19 | public User getUser(SecurityContext securityContext) { 20 | org.springframework.security.core.userdetails.User userFromSecurityContext; 21 | 22 | userFromSecurityContext = (org.springframework.security.core.userdetails.User) 23 | securityContext.getAuthentication().getPrincipal(); 24 | 25 | return userRepository.findByEmail(userFromSecurityContext.getUsername()); 26 | } 27 | } -------------------------------------------------------------------------------- /src/main/java/org/privatechat/chat/interfaces/IChatChannelController.java: -------------------------------------------------------------------------------- 1 | package org.privatechat.chat.interfaces; 2 | 3 | import org.privatechat.chat.DTOs.ChatChannelInitializationDTO; 4 | import org.privatechat.chat.DTOs.ChatMessageDTO; 5 | import org.privatechat.user.exceptions.IsSameUserException; 6 | import org.privatechat.user.exceptions.UserNotFoundException; 7 | import org.springframework.beans.BeansException; 8 | import org.springframework.http.ResponseEntity; 9 | import org.springframework.messaging.handler.annotation.DestinationVariable; 10 | import org.springframework.web.bind.annotation.PathVariable; 11 | import org.springframework.web.bind.annotation.RequestBody; 12 | 13 | public interface IChatChannelController { 14 | ChatMessageDTO chatMessage(@DestinationVariable String channelId, ChatMessageDTO message) 15 | throws BeansException, UserNotFoundException; 16 | 17 | ResponseEntity establishChatChannel(@RequestBody ChatChannelInitializationDTO chatChannelInitialization) 18 | throws IsSameUserException, UserNotFoundException; 19 | 20 | ResponseEntity getExistingChatMessages(@PathVariable("channelUuid") String channelUuid); 21 | } 22 | -------------------------------------------------------------------------------- /src/main/java/org/privatechat/chat/models/ChatChannel.java: -------------------------------------------------------------------------------- 1 | package org.privatechat.chat.models; 2 | 3 | import javax.persistence.*; 4 | import javax.validation.constraints.NotNull; 5 | 6 | import org.privatechat.user.models.User; 7 | 8 | import java.util.UUID; 9 | 10 | @Entity 11 | @Table(name="chatChannel") 12 | public class ChatChannel { 13 | 14 | @Id 15 | @NotNull 16 | private String uuid; 17 | 18 | @OneToOne 19 | @JoinColumn(name = "userIdOne") 20 | private User userOne; 21 | 22 | @OneToOne 23 | @JoinColumn(name = "userIdTwo") 24 | private User userTwo; 25 | 26 | public ChatChannel(User userOne, User userTwo) { 27 | this.uuid = UUID.randomUUID().toString(); 28 | this.userOne = userOne; 29 | this.userTwo = userTwo; 30 | } 31 | 32 | public ChatChannel() {} 33 | 34 | public void setUserTwo(User user) { 35 | this.userTwo = user; 36 | } 37 | 38 | public void setUserOne(User user) { 39 | this.userOne = user; 40 | } 41 | 42 | public User getUserOne() { 43 | return this.userOne; 44 | } 45 | 46 | public User getUserTwo() { 47 | return this.userTwo; 48 | } 49 | 50 | public String getUuid() { 51 | return this.uuid; 52 | } 53 | } -------------------------------------------------------------------------------- /src/main/java/org/privatechat/chat/DTOs/EstablishedChatChannelDTO.java: -------------------------------------------------------------------------------- 1 | package org.privatechat.chat.DTOs; 2 | 3 | public class EstablishedChatChannelDTO { 4 | private String channelUuid; 5 | 6 | private String userOneFullName; 7 | 8 | private String userTwoFullName; 9 | 10 | public EstablishedChatChannelDTO() {} 11 | 12 | public EstablishedChatChannelDTO(String channelUuid, String userOneFullName, String userTwoFullName) { 13 | this.channelUuid = channelUuid; 14 | this.userOneFullName = userOneFullName; 15 | this.userTwoFullName = userTwoFullName; 16 | } 17 | 18 | public void setChannelUuid(String channelUuid) { 19 | this.channelUuid = channelUuid; 20 | } 21 | 22 | public String getChannelUuid() { 23 | return this.channelUuid; 24 | } 25 | 26 | public void setUserOneFullName(String userOneFullName) { 27 | this.userOneFullName = userOneFullName; 28 | } 29 | 30 | public String getUserOneFullName() { 31 | return this.userOneFullName; 32 | } 33 | 34 | public void setUserTwoFullName(String userTwoFullName) { 35 | this.userTwoFullName = userTwoFullName; 36 | } 37 | 38 | public String getUserTwoFullName() { 39 | return this.userTwoFullName; 40 | } 41 | } -------------------------------------------------------------------------------- /src/main/java/org/privatechat/user/interfaces/IUserService.java: -------------------------------------------------------------------------------- 1 | package org.privatechat.user.interfaces; 2 | 3 | import java.util.List; 4 | import org.privatechat.shared.exceptions.ValidationException; 5 | import org.privatechat.user.DTOs.NotificationDTO; 6 | import org.privatechat.user.DTOs.RegistrationDTO; 7 | import org.privatechat.user.DTOs.UserDTO; 8 | import org.privatechat.user.exceptions.UserNotFoundException; 9 | import org.privatechat.user.models.User; 10 | import org.springframework.beans.BeansException; 11 | import org.springframework.security.core.context.SecurityContext; 12 | 13 | public interface IUserService { 14 | User getUser(String userEmail) 15 | throws BeansException, UserNotFoundException; 16 | 17 | User getUser(long userId) 18 | throws BeansException, UserNotFoundException; 19 | 20 | User getUser(SecurityContext securityContext) 21 | throws BeansException, UserNotFoundException; 22 | 23 | boolean doesUserExist(String email); 24 | 25 | void addUser(RegistrationDTO registrationDTO) 26 | throws ValidationException; 27 | 28 | List retrieveFriendsList(User user); 29 | 30 | UserDTO retrieveUserInfo(User user); 31 | 32 | void setIsPresent(User user, Boolean stat); 33 | 34 | Boolean isPresent(User user); 35 | 36 | void notifyUser(User user, NotificationDTO notification); 37 | } -------------------------------------------------------------------------------- /src/main/resources/src/org/privatechat/shared/websocket/WebSocket.js: -------------------------------------------------------------------------------- 1 | angular.module('org.privatechat.shared.websocket.WebSocket', [ 2 | ]) 3 | .factory('WebSocket', [function() { 4 | var socket; 5 | 6 | var WHEN_CONNECTED_CALLBACK_WAIT_INTERVAL = 1000; 7 | 8 | var connect = function() { 9 | socket = Stomp.over(new SockJS('/ws')); 10 | socket.debug = null; 11 | socket.connect({}, onOpen, onClose); 12 | }; 13 | 14 | var disconnect = function() { 15 | socket.disconnect(); 16 | socket = null; 17 | }; 18 | 19 | var onOpen = function() {}; 20 | 21 | var onClose = function() { 22 | alert('You have disconnected, hit "OK" to reload.'); 23 | window.location.reload(); 24 | }; 25 | 26 | var isConnected = function() { 27 | return (socket && socket.connected); 28 | }; 29 | 30 | var whenConnected = function(_do) { 31 | setTimeout( 32 | function() { 33 | if (isConnected()) { 34 | if (_do !== null) { _do(); } 35 | return; 36 | } else { 37 | whenConnected(_do); 38 | } 39 | 40 | }, WHEN_CONNECTED_CALLBACK_WAIT_INTERVAL); 41 | }; 42 | 43 | return { 44 | get: function() { return socket; }, 45 | connect: connect, 46 | disconnect: disconnect, 47 | isConnected: isConnected, 48 | whenConnected: whenConnected 49 | }; 50 | } 51 | ]); -------------------------------------------------------------------------------- /src/main/java/org/privatechat/chat/repositories/ChatChannelRepository.java: -------------------------------------------------------------------------------- 1 | package org.privatechat.chat.repositories; 2 | 3 | import org.privatechat.chat.models.ChatChannel; 4 | import org.privatechat.user.models.User; 5 | import org.springframework.data.jpa.repository.Query; 6 | import org.springframework.data.repository.CrudRepository; 7 | import org.springframework.data.repository.query.Param; 8 | import org.springframework.stereotype.Repository; 9 | import javax.transaction.Transactional; 10 | import java.util.List; 11 | 12 | @Transactional 13 | @Repository 14 | public interface ChatChannelRepository extends CrudRepository { 15 | @Query(" FROM" 16 | + " ChatChannel c" 17 | + " WHERE" 18 | + " c.userOne.id IN (:userOneId, :userTwoId) " 19 | + " AND" 20 | + " c.userTwo.id IN (:userOneId, :userTwoId)") 21 | public List findExistingChannel( 22 | @Param("userOneId") long userOneId, @Param("userTwoId") long userTwoId); 23 | 24 | @Query(" SELECT" 25 | + " uuid" 26 | + " FROM" 27 | + " ChatChannel c" 28 | + " WHERE" 29 | + " c.userOne.id IN (:userIdOne, :userIdTwo)" 30 | + " AND" 31 | + " c.userTwo.id IN (:userIdOne, :userIdTwo)") 32 | public String getChannelUuid( 33 | @Param("userIdOne") long userIdOne, @Param("userIdTwo") long userIdTwo); 34 | 35 | @Query(" FROM" 36 | + " ChatChannel c" 37 | + " WHERE" 38 | + " c.uuid IS :uuid") 39 | public ChatChannel getChannelDetails(@Param("uuid") String uuid); 40 | } -------------------------------------------------------------------------------- /src/main/resources/src/org/privatechat/user/UserService.js: -------------------------------------------------------------------------------- 1 | angular.module('org.privatechat.user.UserService', [ 2 | 3 | ]) 4 | .factory('UserService', ['$http', 5 | function($http) { 6 | var USER_INFO = 'USER_INFO'; 7 | 8 | var getFriendslist = function() { 9 | return $http({ 10 | method: 'GET', 11 | url: '/api/user/requesting/friendslist', 12 | headers: { 13 | 'Content-Type': 'application/json' 14 | } 15 | }); 16 | }; 17 | 18 | var establishUserInfo = function() { 19 | return $http({ 20 | method: 'GET', 21 | url: '/api/user/requesting/info', 22 | }) 23 | .then(function(res) { 24 | if (res && res.data) { 25 | $.cookie(USER_INFO, JSON.stringify({ 26 | id: parseInt(res.data.id, 10), 27 | fullName: res.data.fullName, 28 | email: res.data.email, 29 | })); 30 | } 31 | 32 | return; 33 | }); 34 | }; 35 | 36 | var clearUserInfo = function() { 37 | $.removeCookie(USER_INFO); 38 | }; 39 | 40 | var getUserInfo = function() { 41 | var rawInfo = $.cookie(USER_INFO); 42 | 43 | return (rawInfo) ? JSON.parse($.cookie(USER_INFO)) : null; 44 | }; 45 | 46 | return { 47 | getFriendslist: getFriendslist, 48 | getUserInfo: getUserInfo, 49 | establishUserInfo: establishUserInfo, 50 | clearUserInfo: clearUserInfo, 51 | }; 52 | } 53 | ]); -------------------------------------------------------------------------------- /src/main/java/org/privatechat/chat/models/ChatMessage.java: -------------------------------------------------------------------------------- 1 | package org.privatechat.chat.models; 2 | 3 | import javax.persistence.*; 4 | import javax.validation.constraints.NotNull; 5 | 6 | import org.privatechat.user.models.User; 7 | 8 | import java.util.Date; 9 | 10 | @Entity 11 | @Table(name="chatMessage") 12 | public class ChatMessage { 13 | @Id 14 | @GeneratedValue(strategy = GenerationType.AUTO) 15 | private long id; 16 | 17 | @OneToOne 18 | @JoinColumn(name = "authorUserId") 19 | private User authorUser; 20 | 21 | @OneToOne 22 | @JoinColumn(name = "recipientUserId") 23 | private User recipientUser; 24 | 25 | @NotNull 26 | private Date timeSent; 27 | 28 | @NotNull 29 | private String contents; 30 | 31 | public ChatMessage() {} 32 | 33 | public ChatMessage(User authorUser, User recipientUser, String contents) { 34 | this.authorUser = authorUser; 35 | this.recipientUser = recipientUser; 36 | this.contents = contents; 37 | this.timeSent = new Date(); 38 | } 39 | 40 | public long getId() { 41 | return this.id; 42 | } 43 | 44 | public User getAuthorUser() { 45 | return this.authorUser; 46 | } 47 | 48 | public User getRecipientUser() { 49 | return this.recipientUser; 50 | } 51 | 52 | public void setAuthorUser(User user) { 53 | this.recipientUser = user; 54 | } 55 | 56 | public void setRecipientUser(User user) { 57 | this.authorUser = user; 58 | } 59 | 60 | public Date getTimeSent() { 61 | return this.timeSent; 62 | } 63 | 64 | public String getContents() { 65 | return this.contents; 66 | } 67 | } -------------------------------------------------------------------------------- /src/main/resources/src/org/privatechat/user/AuthService.js: -------------------------------------------------------------------------------- 1 | angular.module('org.privatechat.user.AuthService', [ 2 | 'org.privatechat.user.UserService', 3 | 'org.privatechat.shared.websocket.WebSocket' 4 | ]) 5 | .factory('AuthService', ['$http', '$location', 'UserService', 'WebSocket', '$rootScope', 6 | function($http, $location, UserService, WebSocket, $rootScope) { 7 | var login = function(loginDTO) { 8 | return $http({ 9 | method: 'POST', 10 | url: '/login', 11 | data: $.param(loginDTO), 12 | headers: { 13 | 'Content-Type': 'application/x-www-form-urlencoded' 14 | } 15 | }); 16 | }; 17 | 18 | var logout = function() { 19 | UserService.clearUserInfo(); 20 | WebSocket.disconnect(); 21 | $location.path('/'); 22 | 23 | return $http({ 24 | method: 'POST', 25 | url: '/logout' 26 | }); 27 | }; 28 | 29 | $rootScope.$on('logout-event', logout); 30 | 31 | var register = function(registerDTO) { 32 | return $http({ 33 | method: 'POST', 34 | url: '/api/user/register', 35 | data: registerDTO, 36 | headers: { 37 | 'Content-Type': 'application/json' 38 | } 39 | }); 40 | }; 41 | 42 | // TODO: currently just checking to see if the user's 43 | // metadata cookie is in place... not very robust. Call 44 | // the service to confirm this. 45 | var hasActiveSession = function() { 46 | var info = UserService.getUserInfo(); 47 | 48 | return (info !== null) ? true : false; 49 | }; 50 | 51 | return { 52 | login: login, 53 | register: register, 54 | hasActiveSession: hasActiveSession 55 | }; 56 | } 57 | ]); -------------------------------------------------------------------------------- /src/main/java/org/privatechat/user/services/UserPresenceService.java: -------------------------------------------------------------------------------- 1 | package org.privatechat.user.services; 2 | 3 | import org.privatechat.user.exceptions.UserNotFoundException; 4 | import org.privatechat.user.interfaces.IUserPresenceService; 5 | import org.privatechat.user.models.User; 6 | import org.springframework.beans.BeansException; 7 | import org.springframework.beans.factory.annotation.Autowired; 8 | import org.springframework.messaging.Message; 9 | import org.springframework.messaging.MessageChannel; 10 | import org.springframework.messaging.simp.stomp.StompHeaderAccessor; 11 | import org.springframework.messaging.support.ChannelInterceptorAdapter; 12 | import org.springframework.stereotype.Component; 13 | 14 | @Component 15 | public class UserPresenceService extends ChannelInterceptorAdapter implements IUserPresenceService { 16 | @Autowired 17 | private UserService userService; 18 | 19 | @Override 20 | public void postSend(Message message, MessageChannel channel, boolean sent) { 21 | StompHeaderAccessor stompDetails = StompHeaderAccessor.wrap(message); 22 | 23 | if(stompDetails.getCommand() == null) { return; } 24 | 25 | switch(stompDetails.getCommand()) { 26 | case CONNECT: 27 | case CONNECTED: 28 | toggleUserPresence(stompDetails.getUser().getName().toString(), true); 29 | break; 30 | case DISCONNECT: 31 | toggleUserPresence(stompDetails.getUser().getName().toString(), false); 32 | break; 33 | default: 34 | break; 35 | } 36 | } 37 | 38 | private void toggleUserPresence(String userEmail, Boolean isPresent) { 39 | try { 40 | User user = userService.getUser(userEmail); 41 | userService.setIsPresent(user, isPresent); 42 | } catch (BeansException | UserNotFoundException e) { 43 | e.printStackTrace(); 44 | } 45 | } 46 | } -------------------------------------------------------------------------------- /src/main/resources/src/org/privatechat/registration/RegistrationView.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | 4 | 5 |

valid Email required.

6 |
7 |
8 | 9 | 10 |

valid name required.

11 |

name is too short.

12 |

name is too long.

13 |
14 |
15 | 16 | 17 |

valid password required.

18 |

password is too short.

19 |

password is too long.

20 |
21 | 22 | 23 |
-------------------------------------------------------------------------------- /src/main/java/org/privatechat/shared/websocket/WebSocketConfig.java: -------------------------------------------------------------------------------- 1 | package org.privatechat.shared.websocket; 2 | 3 | import org.privatechat.user.services.UserPresenceService; 4 | import org.springframework.context.annotation.Bean; 5 | import org.springframework.context.annotation.Configuration; 6 | import org.springframework.messaging.simp.config.ChannelRegistration; 7 | import org.springframework.messaging.simp.config.MessageBrokerRegistry; 8 | import org.springframework.security.config.annotation.web.messaging.MessageSecurityMetadataSourceRegistry; 9 | import org.springframework.web.socket.config.annotation.AbstractWebSocketMessageBrokerConfigurer; 10 | import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker; 11 | import org.springframework.web.socket.config.annotation.StompEndpointRegistry; 12 | 13 | @Configuration 14 | @EnableWebSocketMessageBroker 15 | public class WebSocketConfig extends AbstractWebSocketMessageBrokerConfigurer { 16 | private final int OUTBOUND_CHANNEL_CORE_POOL_SIZE = 8; 17 | 18 | @Override 19 | public void configureMessageBroker(MessageBrokerRegistry config) { 20 | config.enableStompBrokerRelay("/topic/", "/queue/"); 21 | config.setApplicationDestinationPrefixes("/app"); 22 | } 23 | 24 | @Override 25 | public void registerStompEndpoints(StompEndpointRegistry registry) { 26 | registry.addEndpoint("/ws").withSockJS(); 27 | } 28 | 29 | @Bean 30 | public UserPresenceService presenceChannelInterceptor() { 31 | return new UserPresenceService(); 32 | } 33 | 34 | @Override 35 | public void configureClientInboundChannel(ChannelRegistration registration) { 36 | registration.setInterceptors(presenceChannelInterceptor()); 37 | } 38 | 39 | @Override 40 | public void configureClientOutboundChannel(ChannelRegistration registration) { 41 | registration.taskExecutor().corePoolSize(OUTBOUND_CHANNEL_CORE_POOL_SIZE); 42 | registration.setInterceptors(presenceChannelInterceptor()); 43 | } 44 | 45 | protected void configureInbound(MessageSecurityMetadataSourceRegistry messages) { 46 | messages.simpDestMatchers("/*").authenticated(); 47 | } 48 | } -------------------------------------------------------------------------------- /src/main/java/org/privatechat/user/models/User.java: -------------------------------------------------------------------------------- 1 | package org.privatechat.user.models; 2 | 3 | import org.hibernate.validator.constraints.Email; 4 | import javax.persistence.*; 5 | import javax.validation.constraints.NotNull; 6 | import javax.validation.constraints.Size; 7 | 8 | @Entity 9 | @Table(name="user", uniqueConstraints=@UniqueConstraint(columnNames={"email"})) 10 | public class User { 11 | 12 | @Id 13 | @GeneratedValue(strategy = GenerationType.AUTO) 14 | private long id; 15 | 16 | @NotNull(message="valid email required") 17 | @Email(message = "valid email required") 18 | private String email; 19 | 20 | @NotNull(message="valid password required") 21 | private String password; 22 | 23 | @NotNull(message="valid name required") 24 | private String fullName; 25 | 26 | private String role; 27 | 28 | private Boolean isPresent; 29 | 30 | public User() {} 31 | 32 | public User(long id) { 33 | this.id = id; 34 | } 35 | 36 | public User(String email, String fullName, String password, String role) { 37 | this.email = email; 38 | this.fullName = fullName; 39 | this.password = password; 40 | this.role = role; 41 | } 42 | 43 | public String getFullName() { 44 | return this.fullName; 45 | } 46 | 47 | public void setFullName(String fullName) { 48 | this.fullName = fullName; 49 | } 50 | 51 | public Boolean getIsPresent() { 52 | return this.isPresent; 53 | } 54 | 55 | public void setIsPresent(Boolean stat) { 56 | this.isPresent = stat; 57 | } 58 | 59 | public String getRole() { 60 | return this.role; 61 | } 62 | 63 | public void setRole(String role) { 64 | this.role = role; 65 | } 66 | 67 | public long getId() { 68 | return id; 69 | } 70 | 71 | public void setId(long id) { 72 | this.id = id; 73 | } 74 | 75 | public String getEmail() { 76 | return email; 77 | } 78 | 79 | public void setEmail(String email) { 80 | this.email = email; 81 | } 82 | 83 | public String getPassword() { 84 | return password; 85 | } 86 | 87 | public void setPassword(String password) { 88 | this.password = password; 89 | } 90 | } -------------------------------------------------------------------------------- /src/main/java/org/privatechat/user/controllers/UserController.java: -------------------------------------------------------------------------------- 1 | package org.privatechat.user.controllers; 2 | 3 | import java.security.Principal; 4 | import java.util.List; 5 | import org.privatechat.shared.exceptions.ValidationException; 6 | import org.privatechat.shared.http.JSONResponseHelper; 7 | import org.privatechat.user.DTOs.RegistrationDTO; 8 | import org.privatechat.user.DTOs.UserDTO; 9 | import org.privatechat.user.exceptions.UserNotFoundException; 10 | import org.privatechat.user.interfaces.IUserController; 11 | import org.privatechat.user.models.User; 12 | import org.privatechat.user.services.UserService; 13 | import org.springframework.beans.factory.annotation.Autowired; 14 | import org.springframework.http.HttpStatus; 15 | import org.springframework.http.ResponseEntity; 16 | import org.springframework.security.core.context.SecurityContextHolder; 17 | import org.springframework.web.bind.annotation.*; 18 | 19 | @RestController 20 | public class UserController implements IUserController { 21 | @Autowired 22 | private UserService userService; 23 | 24 | @RequestMapping(value="/api/user/register", method= RequestMethod.POST, produces="application/json", consumes="application/json") 25 | public ResponseEntity register(@RequestBody RegistrationDTO registeringUser) 26 | throws ValidationException { 27 | userService.addUser(registeringUser); 28 | 29 | return JSONResponseHelper.createResponse("", HttpStatus.OK); 30 | } 31 | 32 | // TODO: actually implement concept of a "friendslist" 33 | @RequestMapping(value="/api/user/requesting/friendslist", method=RequestMethod.GET, produces="application/json") 34 | public ResponseEntity retrieveRequestingUserFriendsList(Principal principal) 35 | throws UserNotFoundException { 36 | User requestingUser = userService.getUser(SecurityContextHolder.getContext()); 37 | List friendslistUsers = userService.retrieveFriendsList(requestingUser); 38 | 39 | return JSONResponseHelper.createResponse(friendslistUsers, HttpStatus.OK); 40 | } 41 | 42 | @RequestMapping(value="/api/user/requesting/info", method=RequestMethod.GET, produces="application/json") 43 | public ResponseEntity retrieveRequestUserInfo() 44 | throws UserNotFoundException { 45 | User requestingUser = userService.getUser(SecurityContextHolder.getContext()); 46 | UserDTO userDetails = userService.retrieveUserInfo(requestingUser); 47 | 48 | return JSONResponseHelper.createResponse(userDetails, HttpStatus.OK); 49 | } 50 | } -------------------------------------------------------------------------------- /src/main/resources/src/org/privatechat/user/notification/NotificationController.js: -------------------------------------------------------------------------------- 1 | angular.module('org.privatechat.user.notification.NotificationController', [ 2 | 'org.privatechat.user.AuthService', 3 | 'org.privatechat.shared.websocket.WebSocket', 4 | 'org.privatechat.user.UserService', 5 | 'org.privatechat.user.notification.NotificationService' 6 | ]).controller('NotificationController', ['$scope', 'AuthService', 'WebSocket', '$location', 'UserService', 'NotificationService', 7 | function($scope, AuthService, WebSocket, $location, UserService, NotificationService) { 8 | var self = $scope; 9 | 10 | var notificationsWebSocket; 11 | 12 | self.construct = function() { 13 | establishChannel(); 14 | }; 15 | 16 | var establishChannel = function() { 17 | notificationsWebSocket = WebSocket.get(); 18 | notificationsWebSocket.subscribe( 19 | '/topic/user.notification.' + UserService.getUserInfo().id, 20 | onMessage 21 | ); 22 | self.notifications = NotificationService.notifications; 23 | }; 24 | 25 | var notificationStrategies = { 26 | 'ChatMessageNotification': { 27 | 28 | onMessage: function(notification) { 29 | var notificationPath = '/private-chat/' + notification.fromUserId; 30 | 31 | var isDuplicateNotification = _.any(NotificationService.notifications, { 32 | fromUserId: notification.fromUserId, 33 | type: 'ChatMessageNotification' 34 | }); 35 | 36 | if ($location.path() !== notificationPath && !isDuplicateNotification) { 37 | notification.path = notificationPath; 38 | NotificationService.notifications.push(notification); 39 | self.$apply(); 40 | } 41 | }, 42 | 43 | onAcknowledgement: function(notification) { 44 | var index = NotificationService.notifications.indexOf(notification); 45 | NotificationService.notifications.splice(index, 1); 46 | $location.path(notification.path); 47 | } 48 | } 49 | }; 50 | 51 | var onMessage = function(rawNotification) { 52 | var notification = JSON.parse(rawNotification.body); 53 | 54 | notificationStrategies[notification.type].onMessage(notification); 55 | }; 56 | 57 | self.acknowledgeNotification = function(notification) { 58 | notificationStrategies[notification.type].onAcknowledgement(notification); 59 | }; 60 | 61 | WebSocket.whenConnected(self.construct); 62 | } 63 | ]); -------------------------------------------------------------------------------- /src/main/java/org/privatechat/shared/error/ErrorHandlerController.java: -------------------------------------------------------------------------------- 1 | package org.privatechat.shared.error; 2 | 3 | import java.io.File; 4 | import java.io.FileNotFoundException; 5 | import java.util.Scanner; 6 | import org.privatechat.shared.exceptions.ValidationException; 7 | import org.privatechat.shared.http.JSONResponseHelper; 8 | import org.privatechat.shared.interfaces.IErrorHandlerController; 9 | import org.privatechat.user.exceptions.IsSameUserException; 10 | import org.springframework.http.HttpStatus; 11 | import org.springframework.http.ResponseEntity; 12 | import org.springframework.web.bind.annotation.ControllerAdvice; 13 | import org.springframework.web.bind.annotation.ExceptionHandler; 14 | import org.springframework.web.bind.annotation.RequestMapping; 15 | import org.springframework.web.bind.annotation.RequestMethod; 16 | import org.springframework.web.bind.annotation.ResponseBody; 17 | import org.springframework.web.bind.annotation.RestController; 18 | 19 | @ControllerAdvice 20 | @RestController 21 | public class ErrorHandlerController implements IErrorHandlerController { 22 | @RequestMapping(value="/error", method=RequestMethod.GET, produces="text/html") 23 | @ResponseBody 24 | public ResponseEntity error() { 25 | return new ResponseEntity(routeToIndexFallBack(), HttpStatus.NOT_FOUND); 26 | } 27 | 28 | private String routeToIndexFallBack() { 29 | Scanner scanner; 30 | StringBuilder result = new StringBuilder(""); 31 | ClassLoader classLoader = getClass().getClassLoader(); 32 | 33 | try { 34 | scanner = new Scanner(new File(classLoader.getResource("static/index.html").getFile())); 35 | 36 | while (scanner.hasNextLine()) { 37 | String curLine = scanner.nextLine(); 38 | result.append(curLine).append("\n"); 39 | } 40 | 41 | scanner.close(); 42 | return result.toString(); 43 | } catch (FileNotFoundException e) { 44 | return "Not found."; 45 | } 46 | } 47 | 48 | @ExceptionHandler(Exception.class) 49 | public ResponseEntity exception(Exception exception) { 50 | if (isExceptionInWhiteList(exception)) { 51 | return JSONResponseHelper.createResponse( 52 | exception.getMessage(), 53 | HttpStatus.INTERNAL_SERVER_ERROR 54 | ); 55 | } 56 | 57 | return JSONResponseHelper.createResponse( 58 | "Error. Contact your administrator", 59 | HttpStatus.INTERNAL_SERVER_ERROR 60 | ); 61 | } 62 | 63 | // There's no way to iterate over exceptions with the 'instanceof' operator 64 | private Boolean isExceptionInWhiteList(Exception exception) { 65 | if (exception instanceof IsSameUserException) return true; 66 | if (exception instanceof ValidationException) return true; 67 | // TODO: if (exception instanceof UserNotFoundException) return true; 68 | 69 | return false; 70 | } 71 | } -------------------------------------------------------------------------------- /src/main/resources/src/org/privatechat/index.js: -------------------------------------------------------------------------------- 1 | angular.module('org.privatechat', [ 2 | 'ngRoute', 3 | 'angular.filter', 4 | 'org.privatechat.login.LoginController', 5 | 'org.privatechat.registration.RegistrationController', 6 | 'org.privatechat.friendslist.FriendsListController', 7 | 'org.privatechat.chat.ChatController', 8 | 'org.privatechat.user.AuthService', 9 | 'org.privatechat.shared.websocket.WebSocket', 10 | ]) 11 | .run(['$rootScope', '$location', 'AuthService', 'WebSocket', 12 | function($rootScope, $location, AuthService, WebSocket) { 13 | $rootScope.$on('$routeChangeStart', function(event, next, current) { 14 | if (next.$$route && next.$$route.auth) { 15 | if ( !AuthService.hasActiveSession()) { 16 | $location.path('/login'); 17 | } else { 18 | if (!WebSocket.isConnected()) { 19 | WebSocket.connect(); 20 | } 21 | } 22 | } 23 | }); 24 | }]) 25 | .factory('authHttpResponseInterceptor',['$q', '$location', 26 | function($q, $location){ 27 | 28 | var routeToLogin = function() { $location.path('/login'); }; 29 | 30 | return { 31 | response: function(response){ 32 | if (response.status === 401) { routeToLogin(); } 33 | 34 | return response || $q.when(response); 35 | }, 36 | responseError: function(rejection) { 37 | if (rejection.status === 401) { routeToLogin(); } 38 | 39 | return $q.reject(rejection); 40 | } 41 | }; 42 | }]) 43 | .config(['$routeProvider', '$httpProvider', 44 | function($routeProvider, $httpProvider) { 45 | $httpProvider.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest'; 46 | $httpProvider.defaults.withCredentials = true; 47 | 48 | // http://stackoverflow.com/a/19771501/1525534 - disable IE ajax request caching 49 | if (!$httpProvider.defaults.headers.get) { $httpProvider.defaults.headers.get = {}; } 50 | $httpProvider.defaults.headers.get['If-Modified-Since'] = 'Mon, 26 Jul 1997 05:00:00 GMT'; 51 | $httpProvider.defaults.headers.get['Cache-Control'] = 'no-cache'; 52 | $httpProvider.defaults.headers.get['Pragma'] = 'no-cache'; 53 | 54 | $httpProvider.interceptors.push('authHttpResponseInterceptor'); 55 | 56 | $routeProvider 57 | .when('/login', { 58 | controller: 'LoginController', 59 | templateUrl: 'login/LoginView.html' 60 | }) 61 | .when('/register', { 62 | controller: 'RegistrationController', 63 | templateUrl: 'registration/RegistrationView.html' 64 | }) 65 | .when('/friendslist', { 66 | controller: 'FriendsListController', 67 | templateUrl: 'friendslist/FriendsListView.html', 68 | auth: true, 69 | }) 70 | .when('/private-chat/:id', { 71 | controller: 'ChatController', 72 | templateUrl: 'chat/ChatView.html', 73 | auth: true, 74 | }) 75 | .otherwise({ 76 | redirectTo: '/login' 77 | }); 78 | }]); -------------------------------------------------------------------------------- /src/main/resources/src/org/privatechat/chat/ChatController.js: -------------------------------------------------------------------------------- 1 | angular.module('org.privatechat.chat.ChatController', [ 2 | 'org.privatechat.shared.theme.Navbar', 3 | 'org.privatechat.chat.ChatService', 4 | 'org.privatechat.user.UserService', 5 | 'org.privatechat.shared.websocket.WebSocket' 6 | ]).controller('ChatController', ['$scope', '$routeParams', 'ChatService', 'UserService', '$timeout', 'WebSocket', 7 | function($scope, $routeParams, ChatService, UserService, $timeout, WebSocket) { 8 | var self = $scope; 9 | 10 | var chatWebSocket; 11 | var channelUuid; 12 | var MESSAGES_RENDERING_WAIT_TIME = 1000; 13 | 14 | // TODO: implement .catch if user isn't authorized 15 | // to chat with this user (i.e.: not in friendslist) 16 | var construct = function() { 17 | ChatService 18 | .establishChatSession(UserService.getUserInfo().id, $routeParams.id) 19 | .then(establishChannel) 20 | .then(getExistingChatMessages); 21 | }; 22 | 23 | var establishChannel = function(channelDetailsPayload) { 24 | chatWebSocket = WebSocket.get(); 25 | channelUuid = channelDetailsPayload.data.channelUuid; 26 | self.userOneFullName = channelDetailsPayload.data.userOneFullName; 27 | self.userTwoFullName = channelDetailsPayload.data.userTwoFullName; 28 | chatWebSocket.subscribe('/topic/private.chat.' + channelUuid, onMessage); 29 | }; 30 | 31 | var getExistingChatMessages = function() { 32 | ChatService 33 | .getExistingChatSessionMessages(channelUuid) 34 | .then(function(messages) { 35 | self.showChat = true; 36 | 37 | messages.data 38 | .forEach(function(message) { 39 | addChatMessageToUI(message); 40 | }); 41 | 42 | scrollToLatestChatMessage(); 43 | }); 44 | }; 45 | 46 | var scrollToLatestChatMessage = function() { 47 | var chatContainer = $('#chat-area'); 48 | 49 | $timeout(function() { 50 | if (chatContainer.length > 0) { chatContainer.scrollTop(chatContainer[0].scrollHeight); } 51 | }, MESSAGES_RENDERING_WAIT_TIME); 52 | }; 53 | 54 | var addChatMessageToUI = function(message, withForceApply) { 55 | self.messages 56 | .push({ 57 | contents: message.contents, 58 | isFromRecipient: message.fromUserId != UserService.getUserInfo().id, 59 | author: (message.fromUserId == UserService.getUserInfo().id) ? self.userOneFullName : self.userTwoFullName 60 | }); 61 | 62 | if (withForceApply) { self.$apply(); } 63 | }; 64 | 65 | var onMessage = function(response) { 66 | addChatMessageToUI(JSON.parse(response.body), true); 67 | scrollToLatestChatMessage(); 68 | }; 69 | 70 | self.sendChatMessage = function() { 71 | if (!self.currentMessage || self.currentMessage.trim() === '') return; 72 | 73 | chatWebSocket.send("/app/private.chat." + channelUuid, {}, JSON.stringify({ 74 | fromUserId: UserService.getUserInfo().id, 75 | toUserId: $routeParams.id, 76 | contents: self.currentMessage 77 | })); 78 | 79 | self.currentMessage = null; 80 | }; 81 | 82 | WebSocket.whenConnected(construct); 83 | } 84 | ]); -------------------------------------------------------------------------------- /src/main/java/org/privatechat/chat/controllers/ChatChannelController.java: -------------------------------------------------------------------------------- 1 | package org.privatechat.chat.controllers; 2 | 3 | import java.util.List; 4 | import org.privatechat.chat.DTOs.ChatChannelInitializationDTO; 5 | import org.privatechat.chat.DTOs.ChatMessageDTO; 6 | import org.privatechat.chat.DTOs.EstablishedChatChannelDTO; 7 | import org.privatechat.chat.interfaces.IChatChannelController; 8 | import org.privatechat.chat.services.ChatService; 9 | import org.privatechat.shared.http.JSONResponseHelper; 10 | import org.privatechat.user.exceptions.IsSameUserException; 11 | import org.privatechat.user.exceptions.UserNotFoundException; 12 | import org.privatechat.user.models.User; 13 | import org.privatechat.user.services.UserService; 14 | import org.springframework.beans.BeansException; 15 | import org.springframework.beans.factory.annotation.Autowired; 16 | import org.springframework.http.HttpStatus; 17 | import org.springframework.http.ResponseEntity; 18 | import org.springframework.messaging.handler.annotation.DestinationVariable; 19 | import org.springframework.messaging.handler.annotation.MessageMapping; 20 | import org.springframework.messaging.handler.annotation.SendTo; 21 | import org.springframework.stereotype.Controller; 22 | import org.springframework.web.bind.annotation.PathVariable; 23 | import org.springframework.web.bind.annotation.RequestBody; 24 | import org.springframework.web.bind.annotation.RequestMapping; 25 | import org.springframework.web.bind.annotation.RequestMethod; 26 | 27 | @Controller 28 | public class ChatChannelController implements IChatChannelController { 29 | @Autowired 30 | private ChatService chatService; 31 | 32 | @Autowired 33 | private UserService userService; 34 | 35 | @MessageMapping("/private.chat.{channelId}") 36 | @SendTo("/topic/private.chat.{channelId}") 37 | public ChatMessageDTO chatMessage(@DestinationVariable String channelId, ChatMessageDTO message) 38 | throws BeansException, UserNotFoundException { 39 | chatService.submitMessage(message); 40 | 41 | return message; 42 | } 43 | 44 | @RequestMapping(value="/api/private-chat/channel", method=RequestMethod.PUT, produces="application/json", consumes="application/json") 45 | public ResponseEntity establishChatChannel(@RequestBody ChatChannelInitializationDTO chatChannelInitialization) 46 | throws IsSameUserException, UserNotFoundException { 47 | String channelUuid = chatService.establishChatSession(chatChannelInitialization); 48 | User userOne = userService.getUser(chatChannelInitialization.getUserIdOne()); 49 | User userTwo = userService.getUser(chatChannelInitialization.getUserIdTwo()); 50 | 51 | EstablishedChatChannelDTO establishedChatChannel = new EstablishedChatChannelDTO( 52 | channelUuid, 53 | userOne.getFullName(), 54 | userTwo.getFullName() 55 | ); 56 | 57 | return JSONResponseHelper.createResponse(establishedChatChannel, HttpStatus.OK); 58 | } 59 | 60 | @RequestMapping(value="/api/private-chat/channel/{channelUuid}", method=RequestMethod.GET, produces="application/json") 61 | public ResponseEntity getExistingChatMessages(@PathVariable("channelUuid") String channelUuid) { 62 | List messages = chatService.getExistingChatMessages(channelUuid); 63 | 64 | return JSONResponseHelper.createResponse(messages, HttpStatus.OK); 65 | } 66 | } -------------------------------------------------------------------------------- /src/main/resources/gulpfile.js: -------------------------------------------------------------------------------- 1 | var gulp = require('gulp'); 2 | var sourcemaps = require('gulp-sourcemaps'); 3 | var uglify = require('gulp-uglify'); 4 | var ngAnnotate = require('gulp-ng-annotate'); 5 | var sass = require('gulp-sass'); 6 | var concat = require('gulp-concat'); 7 | var cssmin = require('gulp-cssmin'); 8 | var rename = require('gulp-rename'); 9 | var runSeq = require('run-sequence'); 10 | var jsValidate = require('gulp-jsvalidate'); 11 | 12 | var buildLocation = '../../../target/classes/static'; 13 | var appLocation = './src/org/privatechat'; 14 | var npmLocation = './node_modules'; 15 | 16 | var tasks = { 17 | JS_VENDORS: 'js-vendors', 18 | JS_APP: 'js-app', 19 | WATCH: 'watch', 20 | CSS_APP: 'css-app', 21 | CSS_VENDORS: 'css-vendors', 22 | TEMPLATES: 'templates', 23 | IMAGES: 'images', 24 | DEFAULT: 'default', 25 | PRODUCTION: 'production' 26 | }; 27 | 28 | gulp.task(tasks.JS_VENDORS, function () { 29 | return gulp.src([ 30 | npmLocation + '/angular/angular.min.js', 31 | npmLocation + '/angular-filter/dist/angular-filter.min.js', 32 | npmLocation + '/angular-route/angular-route.min.js', 33 | npmLocation + '/jquery/dist/jquery.min.js', 34 | npmLocation + '/jquery.cookie/jquery.cookie.js', 35 | npmLocation + '/bootstrap/dist/js/bootstrap.min.js', 36 | npmLocation + '/underscore/underscore-min.js' 37 | ]) 38 | .pipe(concat('vendors.min.js')) 39 | .pipe(uglify()) 40 | .pipe(gulp.dest(buildLocation)); 41 | }); 42 | 43 | gulp.task(tasks.JS_APP, function () { 44 | return gulp.src([ 45 | appLocation + '/**/**.js' 46 | ]) 47 | .pipe(jsValidate()) 48 | .pipe(sourcemaps.init()) 49 | .pipe(concat('app.min.js')) 50 | .pipe(ngAnnotate()) 51 | .pipe(uglify()) 52 | .pipe(sourcemaps.write()) 53 | .pipe(gulp.dest(buildLocation)); 54 | }); 55 | 56 | gulp.task(tasks.WATCH, function () { 57 | gulp.watch(appLocation + '/**/**.js', [tasks.JS_APP]); 58 | gulp.watch(appLocation + '/**/**.scss', [tasks.CSS_APP]); 59 | gulp.watch(appLocation + '/**/**.html', [tasks.TEMPLATES]); 60 | }); 61 | 62 | gulp.task(tasks.CSS_APP, function () { 63 | return gulp.src(appLocation + '/index.scss') 64 | .pipe(sass({ 65 | errLogToConsole: true 66 | })) 67 | .pipe(cssmin()) 68 | .pipe(rename('app.min.css')) 69 | .pipe(gulp.dest(buildLocation)); 70 | }); 71 | 72 | gulp.task(tasks.CSS_VENDORS, function () { 73 | return gulp.src([ 74 | npmLocation + '/bootstrap/dist/css/bootstrap.min.css', 75 | ]) 76 | .pipe(concat('vendors.min.css')) 77 | .pipe(cssmin()) 78 | .pipe(gulp.dest(buildLocation)); 79 | }); 80 | 81 | gulp.task(tasks.TEMPLATES, function () { 82 | return gulp.src(appLocation + '/**/**.html') 83 | .pipe(gulp.dest(buildLocation)); 84 | }); 85 | 86 | gulp.task(tasks.IMAGES, function () { 87 | return gulp.src(appLocation + '/**/**.{gif,png,jpeg,jpg,svg,ico}') 88 | .pipe(gulp.dest(buildLocation)); 89 | }); 90 | 91 | gulp.task(tasks.DEFAULT, function (next) { 92 | runSeq([ 93 | tasks.JS_VENDORS, 94 | tasks.JS_APP, 95 | tasks.TEMPLATES, 96 | tasks.IMAGES, 97 | tasks.CSS_APP, 98 | tasks.CSS_VENDORS 99 | ], 100 | tasks.WATCH, 101 | next); 102 | }); 103 | 104 | gulp.task(tasks.PRODUCTION, function (next) { 105 | runSeq([ 106 | tasks.JS_VENDORS, 107 | tasks.JS_APP, 108 | tasks.TEMPLATES, 109 | tasks.IMAGES, 110 | tasks.CSS_APP, 111 | tasks.CSS_VENDORS 112 | ], 113 | next); 114 | }); -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Spring Boot Private Chat Project 2 | 3 | 1-1 instant messaging project designed to demonstrate WebSockets in a load-balanced environment. Users can register, login/logout, see a friendslist, private message, and send/receive notifications. WebSocket usages include user presence monitoring, notifications, and chat messages. 4 | 5 | ## Notable Technologies/Design Decisions 6 | - Backend: Java 8 with Spring Boot 7 | - Frontend: Angular.js 8 | - Message Broker: RabbitMQ (PubSub pattern for multi-server messaging) 9 | - Database: MySQL 10 | - ORM: Hibernate 11 | - WebSocket messaging protocol: Stomp 12 | - WebSocket handler: Sock.js (with cross-browser fallbacks) 13 | - Session Management: Spring Session Redis (works cross-server) 14 | - Security: Spring Security 15 | - Spring Controllers couple REST as well as WebSocket traffic 16 | - Fractal Design 17 | 18 | ## Setup 19 | 1. Install system dependencies: latest versions (at the time of this writing) of Java, MySQL, Redis, RabbitMQ, Node, NPM 20 | 2. Install RabbitMQ Stomp plugin: `$ sudo rabbitmq-plugins enable rabbitmq_stomp` 21 | 3. Update `src/main/resources/application.properties` with your MySQL credentials and port 22 | 4. Execute `src/main/java/org/privatechat/shared/database/createDatabase.sql` to create the database 23 | 5. Execute `src/main/java/org/privatechat/shared/database/seedDatabase.sql` to seed the database with some users (passwords are bcrypted, but they are all "testtest") 24 | 6. `cd` to `src/main/resources/` and run `$ sudo npm install && gulp` 25 | 7. `cd` to root of the project and execute `$ mvn spring-boot:run` or (`$ mvn spring-boot:run -Drun.jvmArguments='-Dserver.port={{your port here}}'` if you wish to run a few servers) 26 | 8. Visit [http://localhost:8080/](http://localhost:8080) 27 | 28 | ## Notes 29 | - Chat messages are persisted to the database, notifications are not. Will add that functionality later. 30 | - Uncomment `devtools` dependency in `pom.xml` for live reloading in development 31 | - Notifications must be subscribed to in unique per-user channels. Despite enabling the `/user` message broker prefix, Spring's `convertAndSendToUser(...)` failed to update all nodes of a notification message transmission. Going to post a SO question soon! 32 | - Friendlist feature is just every user in the database other than the current user (simple feature for demo, not meant to be realistic) 33 | - Chat messages are `LIMIT`ed by 100 34 | 35 | ## Todos 36 | - Separate out `index.js` file into smaller services/configs 37 | - Persist notifications 38 | - Figure out how to have Spring Security return a custom UserDTO on `/login`. Currently, after login, a call to `/api/user/requesting/info` is made and the `Principal` context drives the rest. Not elegant. 39 | - Refactor `user` table `isPresent` (boolean) to be `numberOfConnections` (int). 40 | - Implement notification emailer when recieving user is offline (batch up notifications in a time window) 41 | - Implement auto-logout timer on frontend 42 | - Implement smart pagination of messages on the frontend 43 | - Implement a UUID for notifications topics subscription (currently using user id, which is unsecure) 44 | - Wire up CSRF tokens with Spring Security 45 | - ChatService.getExistingChatMessages(...) currently reverses the `List 2 | 3 | 4.0.0 4 | org.chat 5 | chat 6 | 0.0.1-SNAPSHOT 7 | jar 8 | chat 9 | 10 | Private Chat WebSocket Project 11 | 12 | 13 | org.springframework.boot 14 | spring-boot-starter-parent 15 | 1.3.2.RELEASE 16 | 17 | 18 | 19 | 20 | UTF-8 21 | 1.8 22 | 23 | 24 | 25 | 26 | org.springframework.boot 27 | spring-boot-starter 28 | 29 | 30 | 31 | org.springframework.boot 32 | spring-boot-starter-test 33 | test 34 | 35 | 36 | 42 | 43 | 44 | org.springframework.session 45 | spring-session-data-redis 46 | 47 | 48 | 49 | org.springframework.boot 50 | spring-boot-starter-data-jpa 51 | 52 | 53 | 54 | mysql 55 | mysql-connector-java 56 | 57 | 58 | 59 | org.springframework.boot 60 | spring-boot-starter-security 61 | 62 | 63 | 64 | org.springframework.boot 65 | spring-boot-starter-web 66 | 67 | 68 | 69 | org.springframework 70 | spring-aop 71 | 72 | 73 | 74 | com.google.code.gson 75 | gson 76 | 77 | 78 | 79 | org.springframework.boot 80 | spring-boot-starter-websocket 81 | 82 | 83 | 84 | org.springframework 85 | spring-messaging 86 | 87 | 88 | 89 | io.projectreactor 90 | reactor-core 91 | 2.0.7.RELEASE 92 | 93 | 94 | 95 | io.projectreactor 96 | reactor-net 97 | 2.0.7.RELEASE 98 | 99 | 100 | 101 | io.netty 102 | netty-all 103 | 4.0.34.Final 104 | 105 | 106 | 107 | org.springframework.integration 108 | spring-integration-amqp 109 | 110 | 111 | 112 | com.google.guava 113 | guava 114 | 19.0 115 | 116 | 117 | 118 | 119 | 120 | 121 | org.springframework.boot 122 | spring-boot-maven-plugin 123 | 124 | 125 | 126 | -------------------------------------------------------------------------------- /src/main/java/org/privatechat/chat/services/ChatService.java: -------------------------------------------------------------------------------- 1 | package org.privatechat.chat.services; 2 | 3 | import org.privatechat.chat.DTOs.ChatChannelInitializationDTO; 4 | import org.privatechat.chat.DTOs.ChatMessageDTO; 5 | import org.privatechat.chat.interfaces.IChatService; 6 | import org.privatechat.chat.mappers.ChatMessageMapper; 7 | import org.privatechat.chat.models.ChatChannel; 8 | import org.privatechat.chat.models.ChatMessage; 9 | import org.privatechat.chat.repositories.ChatChannelRepository; 10 | import org.privatechat.chat.repositories.ChatMessageRepository; 11 | import org.privatechat.user.DTOs.NotificationDTO; 12 | import org.privatechat.user.exceptions.IsSameUserException; 13 | import org.privatechat.user.exceptions.UserNotFoundException; 14 | import org.privatechat.user.models.User; 15 | import org.privatechat.user.services.UserService; 16 | import org.springframework.beans.BeansException; 17 | import org.springframework.beans.factory.annotation.Autowired; 18 | import org.springframework.data.domain.PageRequest; 19 | import org.springframework.stereotype.Service; 20 | import com.google.common.collect.Lists; 21 | import java.util.List; 22 | 23 | @Service 24 | public class ChatService implements IChatService { 25 | private ChatChannelRepository chatChannelRepository; 26 | 27 | private ChatMessageRepository chatMessageRepository; 28 | 29 | private UserService userService; 30 | 31 | private final int MAX_PAGABLE_CHAT_MESSAGES = 100; 32 | 33 | @Autowired 34 | public ChatService( 35 | ChatChannelRepository chatChannelRepository, 36 | ChatMessageRepository chatMessageRepository, 37 | UserService userService) { 38 | this.chatChannelRepository = chatChannelRepository; 39 | this.chatMessageRepository = chatMessageRepository; 40 | this.userService = userService; 41 | } 42 | 43 | private String getExistingChannel(ChatChannelInitializationDTO chatChannelInitializationDTO) { 44 | List channel = chatChannelRepository 45 | .findExistingChannel( 46 | chatChannelInitializationDTO.getUserIdOne(), 47 | chatChannelInitializationDTO.getUserIdTwo() 48 | ); 49 | 50 | return (channel != null && !channel.isEmpty()) ? channel.get(0).getUuid() : null; 51 | } 52 | 53 | private String newChatSession(ChatChannelInitializationDTO chatChannelInitializationDTO) 54 | throws BeansException, UserNotFoundException { 55 | ChatChannel channel = new ChatChannel( 56 | userService.getUser(chatChannelInitializationDTO.getUserIdOne()), 57 | userService.getUser(chatChannelInitializationDTO.getUserIdTwo()) 58 | ); 59 | 60 | chatChannelRepository.save(channel); 61 | 62 | return channel.getUuid(); 63 | } 64 | 65 | public String establishChatSession(ChatChannelInitializationDTO chatChannelInitializationDTO) 66 | throws IsSameUserException, BeansException, UserNotFoundException { 67 | if (chatChannelInitializationDTO.getUserIdOne() == chatChannelInitializationDTO.getUserIdTwo()) { 68 | throw new IsSameUserException(); 69 | } 70 | 71 | String uuid = getExistingChannel(chatChannelInitializationDTO); 72 | 73 | // If channel doesn't already exist, create a new one 74 | return (uuid != null) ? uuid : newChatSession(chatChannelInitializationDTO); 75 | } 76 | 77 | public void submitMessage(ChatMessageDTO chatMessageDTO) 78 | throws BeansException, UserNotFoundException { 79 | ChatMessage chatMessage = ChatMessageMapper.mapChatDTOtoMessage(chatMessageDTO); 80 | 81 | chatMessageRepository.save(chatMessage); 82 | 83 | User fromUser = userService.getUser(chatMessage.getAuthorUser().getId()); 84 | User recipientUser = userService.getUser(chatMessage.getRecipientUser().getId()); 85 | 86 | userService.notifyUser(recipientUser, 87 | new NotificationDTO( 88 | "ChatMessageNotification", 89 | fromUser.getFullName() + " has sent you a message", 90 | chatMessage.getAuthorUser().getId() 91 | ) 92 | ); 93 | } 94 | 95 | public List getExistingChatMessages(String channelUuid) { 96 | ChatChannel channel = chatChannelRepository.getChannelDetails(channelUuid); 97 | 98 | List chatMessages = 99 | chatMessageRepository.getExistingChatMessages( 100 | channel.getUserOne().getId(), 101 | channel.getUserTwo().getId(), 102 | new PageRequest(0, MAX_PAGABLE_CHAT_MESSAGES) 103 | ); 104 | 105 | // TODO: fix this 106 | List messagesByLatest = Lists.reverse(chatMessages); 107 | 108 | return ChatMessageMapper.mapMessagesToChatDTOs(messagesByLatest); 109 | } 110 | } -------------------------------------------------------------------------------- /src/main/java/org/privatechat/shared/security/ApplicationSecurity.java: -------------------------------------------------------------------------------- 1 | package org.privatechat.shared.security; 2 | 3 | import org.privatechat.user.services.UserService; 4 | import org.springframework.beans.factory.annotation.Autowired; 5 | import org.springframework.boot.autoconfigure.security.SecurityProperties; 6 | import org.springframework.context.annotation.Bean; 7 | import org.springframework.context.annotation.Configuration; 8 | import org.springframework.core.annotation.Order; 9 | import org.springframework.security.authentication.AuthenticationManager; 10 | import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; 11 | import org.springframework.security.config.annotation.web.builders.HttpSecurity; 12 | import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; 13 | import org.springframework.security.core.Authentication; 14 | import org.springframework.security.core.AuthenticationException; 15 | import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; 16 | import org.springframework.security.web.AuthenticationEntryPoint; 17 | import org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler; 18 | import org.springframework.security.web.authentication.SimpleUrlAuthenticationSuccessHandler; 19 | import org.springframework.stereotype.Component; 20 | import javax.servlet.ServletException; 21 | import javax.servlet.http.HttpServletRequest; 22 | import javax.servlet.http.HttpServletResponse; 23 | import java.io.IOException; 24 | 25 | @Order(SecurityProperties.ACCESS_OVERRIDE_ORDER) 26 | public class ApplicationSecurity extends WebSecurityConfigurerAdapter { 27 | @Autowired 28 | private UserService userService; 29 | 30 | @Autowired 31 | private AuthenticationEntryPointHandler authenticationEntryPointHandler; 32 | 33 | @Autowired 34 | private AuthenticationSuccessHandler authenticationSuccessHandler; 35 | 36 | @Autowired 37 | private AuthenticationFailureHandler authenticationFailureHandler; 38 | 39 | @Bean 40 | @Override 41 | public AuthenticationManager authenticationManagerBean() 42 | throws Exception { 43 | return super.authenticationManagerBean(); 44 | } 45 | 46 | @Override 47 | public void configure(AuthenticationManagerBuilder auth) 48 | throws Exception { 49 | auth 50 | .userDetailsService(this.userService) 51 | .passwordEncoder(new BCryptPasswordEncoder()); 52 | } 53 | 54 | @Override 55 | protected void configure(HttpSecurity http) 56 | throws Exception { 57 | http 58 | .csrf() 59 | .disable(); // TODO: enable CSRF 60 | 61 | http 62 | .exceptionHandling() 63 | .authenticationEntryPoint(this.authenticationEntryPointHandler); 64 | 65 | http 66 | .formLogin() 67 | .successHandler(this.authenticationSuccessHandler) 68 | .failureHandler(this.authenticationFailureHandler); 69 | 70 | http 71 | .logout() 72 | .logoutSuccessUrl("/"); 73 | 74 | http 75 | .authorizeRequests() 76 | .antMatchers( 77 | "/index.html", 78 | "/login", 79 | "/api/user/register", 80 | "/", 81 | "/app.min.js", 82 | "/app.min.css", 83 | "/vendors.min.js", 84 | "/vendors.min.css", 85 | "/login/LoginView.html", 86 | "/registration/RegistrationView.html" 87 | ) 88 | .permitAll() 89 | .anyRequest() 90 | .authenticated(); 91 | } 92 | } 93 | 94 | @Component 95 | class AuthenticationEntryPointHandler implements AuthenticationEntryPoint { 96 | @Override 97 | public void commence(HttpServletRequest request, HttpServletResponse response, 98 | AuthenticationException authException) throws IOException, ServletException { 99 | 100 | response.sendError(HttpServletResponse.SC_UNAUTHORIZED); 101 | } 102 | } 103 | 104 | @Component 105 | class AuthenticationFailureHandler extends SimpleUrlAuthenticationFailureHandler { 106 | 107 | @Override 108 | public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, 109 | AuthenticationException exception) throws IOException, ServletException { 110 | 111 | super.onAuthenticationFailure(request, response, exception); 112 | } 113 | } 114 | 115 | @Component 116 | class AuthenticationSuccessHandler extends SimpleUrlAuthenticationSuccessHandler { 117 | 118 | @Override 119 | public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, 120 | Authentication authentication) throws IOException, ServletException { 121 | 122 | clearAuthenticationAttributes(request); 123 | } 124 | } -------------------------------------------------------------------------------- /src/main/java/org/privatechat/user/services/UserService.java: -------------------------------------------------------------------------------- 1 | package org.privatechat.user.services; 2 | 3 | import org.privatechat.shared.exceptions.ValidationException; 4 | import org.privatechat.user.DTOs.NotificationDTO; 5 | import org.privatechat.user.DTOs.RegistrationDTO; 6 | import org.privatechat.user.DTOs.UserDTO; 7 | import org.privatechat.user.exceptions.UserNotFoundException; 8 | import org.privatechat.user.interfaces.IUserService; 9 | import org.privatechat.user.mappers.UserMapper; 10 | import org.privatechat.user.models.User; 11 | import org.privatechat.user.repositories.UserRepository; 12 | import org.privatechat.user.strategies.*; 13 | import org.springframework.beans.BeansException; 14 | import org.springframework.beans.factory.BeanFactory; 15 | import org.springframework.beans.factory.annotation.Autowired; 16 | import org.springframework.messaging.simp.SimpMessagingTemplate; 17 | import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; 18 | import org.springframework.security.core.Authentication; 19 | import org.springframework.security.core.authority.AuthorityUtils; 20 | import org.springframework.security.core.context.SecurityContext; 21 | import org.springframework.security.core.context.SecurityContextHolder; 22 | import org.springframework.security.core.userdetails.UserDetails; 23 | import org.springframework.stereotype.Component; 24 | import org.springframework.security.core.userdetails.UserDetailsService; 25 | import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; 26 | import javax.validation.ConstraintViolationException; 27 | import java.util.List; 28 | 29 | @Component 30 | public class UserService implements UserDetailsService, IUserService { 31 | private UserRepository userRepository; 32 | 33 | @Autowired 34 | private SimpMessagingTemplate simpMessagingTemplate; 35 | 36 | @Autowired 37 | private BeanFactory beanFactory; 38 | 39 | @Autowired 40 | public UserService(UserRepository userRepository) { 41 | this.userRepository = userRepository; 42 | } 43 | 44 | private User getUser(T userIdentifier, IUserRetrievalStrategy strategy) 45 | throws UserNotFoundException { 46 | User user = strategy.getUser(userIdentifier); 47 | 48 | if (user == null) { throw new UserNotFoundException("User not found."); } 49 | 50 | return user; 51 | } 52 | 53 | public User getUser(long userId) 54 | throws BeansException, UserNotFoundException { 55 | return this.getUser(userId, beanFactory.getBean(UserRetrievalByIdStrategy.class)); 56 | } 57 | 58 | public User getUser(String userEmail) 59 | throws BeansException, UserNotFoundException { 60 | return this.getUser(userEmail, beanFactory.getBean(UserRetrievalByEmailStrategy.class)); 61 | } 62 | 63 | public User getUser(SecurityContext userSecurityContext) 64 | throws BeansException, UserNotFoundException { 65 | return this.getUser(userSecurityContext, beanFactory.getBean(UserRetrievalBySecurityContextStrategy.class)); 66 | } 67 | 68 | @Override 69 | public UserDetails loadUserByUsername(String email) { 70 | User user = userRepository.findByEmail(email); 71 | 72 | if (user == null) { return null; } 73 | 74 | UserDetails userDetails = new org.springframework.security.core.userdetails.User( 75 | user.getEmail(), 76 | user.getPassword(), 77 | AuthorityUtils.createAuthorityList(user.getRole()) 78 | ); 79 | 80 | Authentication authentication = null; 81 | try { 82 | authentication = new UsernamePasswordAuthenticationToken( 83 | userDetails, 84 | null, 85 | userDetails.getAuthorities() 86 | ); 87 | } catch (Exception e) {} 88 | 89 | SecurityContextHolder 90 | .getContext() 91 | .setAuthentication(authentication); 92 | 93 | return userDetails; 94 | } 95 | 96 | public boolean doesUserExist(String email) { 97 | User user = userRepository.findByEmail(email); 98 | 99 | return user != null; 100 | } 101 | 102 | public void addUser(RegistrationDTO registrationDTO) 103 | throws ValidationException { 104 | if (this.doesUserExist(registrationDTO.getEmail())) { 105 | throw new ValidationException("User already exists."); 106 | } 107 | 108 | String encryptedPassword = new BCryptPasswordEncoder().encode(registrationDTO.getPassword()); 109 | 110 | try { 111 | User user = new User( 112 | registrationDTO.getEmail(), 113 | registrationDTO.getFullName(), 114 | encryptedPassword, 115 | "STANDARD-ROLE" 116 | ); 117 | 118 | userRepository.save(user); 119 | } catch (ConstraintViolationException e) { 120 | throw new ValidationException(e.getConstraintViolations().iterator().next().getMessage()); 121 | } 122 | } 123 | 124 | public List retrieveFriendsList(User user) { 125 | List users = userRepository.findFriendsListFor(user.getEmail()); 126 | 127 | return UserMapper.mapUsersToUserDTOs(users); 128 | } 129 | 130 | public UserDTO retrieveUserInfo(User user) { 131 | return new UserDTO( 132 | user.getId(), 133 | user.getEmail(), 134 | user.getFullName() 135 | ); 136 | } 137 | 138 | // TODO: switch to a TINYINT field called "numOfConnections" to add/subtract 139 | // the total amount of user connections 140 | public void setIsPresent(User user, Boolean stat) { 141 | user.setIsPresent(stat); 142 | 143 | userRepository.save(user); 144 | } 145 | 146 | public Boolean isPresent(User user) { 147 | return user.getIsPresent(); 148 | } 149 | 150 | public void notifyUser(User recipientUser, NotificationDTO notification) { 151 | if (this.isPresent(recipientUser)) { 152 | simpMessagingTemplate 153 | .convertAndSend("/topic/user.notification." + recipientUser.getId(), notification); 154 | } else { 155 | System.out.println("sending email notification to " + recipientUser.getFullName()); 156 | // TODO: send email 157 | } 158 | } 159 | } -------------------------------------------------------------------------------- /src/main/resources/src/org/privatechat/shared/websocket/dependencies/stompjs.js: -------------------------------------------------------------------------------- 1 | // Generated by CoffeeScript 1.7.1 2 | 3 | /* 4 | Stomp Over WebSocket http://www.jmesnil.net/stomp-websocket/doc/ | Apache License V2.0 5 | 6 | Copyright (C) 2010-2013 [Jeff Mesnil](http://jmesnil.net/) 7 | Copyright (C) 2012 [FuseSource, Inc.](http://fusesource.com) 8 | */ 9 | 10 | (function() { 11 | var Byte, Client, Frame, Stomp, 12 | __hasProp = {}.hasOwnProperty, 13 | __slice = [].slice; 14 | 15 | Byte = { 16 | LF: '\x0A', 17 | NULL: '\x00' 18 | }; 19 | 20 | Frame = (function() { 21 | var unmarshallSingle; 22 | 23 | function Frame(command, headers, body) { 24 | this.command = command; 25 | this.headers = headers != null ? headers : {}; 26 | this.body = body != null ? body : ''; 27 | } 28 | 29 | Frame.prototype.toString = function() { 30 | var lines, name, skipContentLength, value, _ref; 31 | lines = [this.command]; 32 | skipContentLength = this.headers['content-length'] === false ? true : false; 33 | if (skipContentLength) { 34 | delete this.headers['content-length']; 35 | } 36 | _ref = this.headers; 37 | for (name in _ref) { 38 | if (!__hasProp.call(_ref, name)) continue; 39 | value = _ref[name]; 40 | lines.push("" + name + ":" + value); 41 | } 42 | if (this.body && !skipContentLength) { 43 | lines.push("content-length:" + (Frame.sizeOfUTF8(this.body))); 44 | } 45 | lines.push(Byte.LF + this.body); 46 | return lines.join(Byte.LF); 47 | }; 48 | 49 | Frame.sizeOfUTF8 = function(s) { 50 | if (s) { 51 | return encodeURI(s).match(/%..|./g).length; 52 | } else { 53 | return 0; 54 | } 55 | }; 56 | 57 | unmarshallSingle = function(data) { 58 | var body, chr, command, divider, headerLines, headers, i, idx, len, line, start, trim, _i, _j, _len, _ref, _ref1; 59 | divider = data.search(RegExp("" + Byte.LF + Byte.LF)); 60 | headerLines = data.substring(0, divider).split(Byte.LF); 61 | command = headerLines.shift(); 62 | headers = {}; 63 | trim = function(str) { 64 | return str.replace(/^\s+|\s+$/g, ''); 65 | }; 66 | _ref = headerLines.reverse(); 67 | for (_i = 0, _len = _ref.length; _i < _len; _i++) { 68 | line = _ref[_i]; 69 | idx = line.indexOf(':'); 70 | headers[trim(line.substring(0, idx))] = trim(line.substring(idx + 1)); 71 | } 72 | body = ''; 73 | start = divider + 2; 74 | if (headers['content-length']) { 75 | len = parseInt(headers['content-length']); 76 | body = ('' + data).substring(start, start + len); 77 | } else { 78 | chr = null; 79 | for (i = _j = start, _ref1 = data.length; start <= _ref1 ? _j < _ref1 : _j > _ref1; i = start <= _ref1 ? ++_j : --_j) { 80 | chr = data.charAt(i); 81 | if (chr === Byte.NULL) { 82 | break; 83 | } 84 | body += chr; 85 | } 86 | } 87 | return new Frame(command, headers, body); 88 | }; 89 | 90 | Frame.unmarshall = function(datas) { 91 | var data; 92 | return (function() { 93 | var _i, _len, _ref, _results; 94 | _ref = datas.split(RegExp("" + Byte.NULL + Byte.LF + "*")); 95 | _results = []; 96 | for (_i = 0, _len = _ref.length; _i < _len; _i++) { 97 | data = _ref[_i]; 98 | if ((data != null ? data.length : void 0) > 0) { 99 | _results.push(unmarshallSingle(data)); 100 | } 101 | } 102 | return _results; 103 | })(); 104 | }; 105 | 106 | Frame.marshall = function(command, headers, body) { 107 | var frame; 108 | frame = new Frame(command, headers, body); 109 | return frame.toString() + Byte.NULL; 110 | }; 111 | 112 | return Frame; 113 | 114 | })(); 115 | 116 | Client = (function() { 117 | var now; 118 | 119 | function Client(ws) { 120 | this.ws = ws; 121 | this.ws.binaryType = "arraybuffer"; 122 | this.counter = 0; 123 | this.connected = false; 124 | this.heartbeat = { 125 | outgoing: 10000, 126 | incoming: 10000 127 | }; 128 | this.maxWebSocketFrameSize = 16 * 1024; 129 | this.subscriptions = {}; 130 | } 131 | 132 | Client.prototype.debug = function(message) { 133 | var _ref; 134 | return typeof window !== "undefined" && window !== null ? (_ref = window.console) != null ? _ref.log(message) : void 0 : void 0; 135 | }; 136 | 137 | now = function() { 138 | if (Date.now) { 139 | return Date.now(); 140 | } else { 141 | return new Date().valueOf; 142 | } 143 | }; 144 | 145 | Client.prototype._transmit = function(command, headers, body) { 146 | var out; 147 | out = Frame.marshall(command, headers, body); 148 | if (typeof this.debug === "function") { 149 | this.debug(">>> " + out); 150 | } 151 | while (true) { 152 | if (out.length > this.maxWebSocketFrameSize) { 153 | this.ws.send(out.substring(0, this.maxWebSocketFrameSize)); 154 | out = out.substring(this.maxWebSocketFrameSize); 155 | if (typeof this.debug === "function") { 156 | this.debug("remaining = " + out.length); 157 | } 158 | } else { 159 | return this.ws.send(out); 160 | } 161 | } 162 | }; 163 | 164 | Client.prototype._setupHeartbeat = function(headers) { 165 | var serverIncoming, serverOutgoing, ttl, v, _ref, _ref1; 166 | if ((_ref = headers.version) !== Stomp.VERSIONS.V1_1 && _ref !== Stomp.VERSIONS.V1_2) { 167 | return; 168 | } 169 | _ref1 = (function() { 170 | var _i, _len, _ref1, _results; 171 | _ref1 = headers['heart-beat'].split(","); 172 | _results = []; 173 | for (_i = 0, _len = _ref1.length; _i < _len; _i++) { 174 | v = _ref1[_i]; 175 | _results.push(parseInt(v)); 176 | } 177 | return _results; 178 | })(), serverOutgoing = _ref1[0], serverIncoming = _ref1[1]; 179 | if (!(this.heartbeat.outgoing === 0 || serverIncoming === 0)) { 180 | ttl = Math.max(this.heartbeat.outgoing, serverIncoming); 181 | if (typeof this.debug === "function") { 182 | this.debug("send PING every " + ttl + "ms"); 183 | } 184 | this.pinger = Stomp.setInterval(ttl, (function(_this) { 185 | return function() { 186 | _this.ws.send(Byte.LF); 187 | return typeof _this.debug === "function" ? _this.debug(">>> PING") : void 0; 188 | }; 189 | })(this)); 190 | } 191 | if (!(this.heartbeat.incoming === 0 || serverOutgoing === 0)) { 192 | ttl = Math.max(this.heartbeat.incoming, serverOutgoing); 193 | if (typeof this.debug === "function") { 194 | this.debug("check PONG every " + ttl + "ms"); 195 | } 196 | return this.ponger = Stomp.setInterval(ttl, (function(_this) { 197 | return function() { 198 | var delta; 199 | delta = now() - _this.serverActivity; 200 | if (delta > ttl * 2) { 201 | if (typeof _this.debug === "function") { 202 | _this.debug("did not receive server activity for the last " + delta + "ms"); 203 | } 204 | return _this.ws.close(); 205 | } 206 | }; 207 | })(this)); 208 | } 209 | }; 210 | 211 | Client.prototype._parseConnect = function() { 212 | var args, connectCallback, errorCallback, headers; 213 | args = 1 <= arguments.length ? __slice.call(arguments, 0) : []; 214 | headers = {}; 215 | switch (args.length) { 216 | case 2: 217 | headers = args[0], connectCallback = args[1]; 218 | break; 219 | case 3: 220 | if (args[1] instanceof Function) { 221 | headers = args[0], connectCallback = args[1], errorCallback = args[2]; 222 | } else { 223 | headers.login = args[0], headers.passcode = args[1], connectCallback = args[2]; 224 | } 225 | break; 226 | case 4: 227 | headers.login = args[0], headers.passcode = args[1], connectCallback = args[2], errorCallback = args[3]; 228 | break; 229 | default: 230 | headers.login = args[0], headers.passcode = args[1], connectCallback = args[2], errorCallback = args[3], headers.host = args[4]; 231 | } 232 | return [headers, connectCallback, errorCallback]; 233 | }; 234 | 235 | Client.prototype.connect = function() { 236 | var args, errorCallback, headers, out; 237 | args = 1 <= arguments.length ? __slice.call(arguments, 0) : []; 238 | out = this._parseConnect.apply(this, args); 239 | headers = out[0], this.connectCallback = out[1], errorCallback = out[2]; 240 | if (typeof this.debug === "function") { 241 | this.debug("Opening Web Socket..."); 242 | } 243 | this.ws.onmessage = (function(_this) { 244 | return function(evt) { 245 | var arr, c, client, data, frame, messageID, onreceive, subscription, _i, _len, _ref, _results; 246 | data = typeof ArrayBuffer !== 'undefined' && evt.data instanceof ArrayBuffer ? (arr = new Uint8Array(evt.data), typeof _this.debug === "function" ? _this.debug("--- got data length: " + arr.length) : void 0, ((function() { 247 | var _i, _len, _results; 248 | _results = []; 249 | for (_i = 0, _len = arr.length; _i < _len; _i++) { 250 | c = arr[_i]; 251 | _results.push(String.fromCharCode(c)); 252 | } 253 | return _results; 254 | })()).join('')) : evt.data; 255 | _this.serverActivity = now(); 256 | if (data === Byte.LF) { 257 | if (typeof _this.debug === "function") { 258 | _this.debug("<<< PONG"); 259 | } 260 | return; 261 | } 262 | if (typeof _this.debug === "function") { 263 | _this.debug("<<< " + data); 264 | } 265 | _ref = Frame.unmarshall(data); 266 | _results = []; 267 | for (_i = 0, _len = _ref.length; _i < _len; _i++) { 268 | frame = _ref[_i]; 269 | switch (frame.command) { 270 | case "CONNECTED": 271 | if (typeof _this.debug === "function") { 272 | _this.debug("connected to server " + frame.headers.server); 273 | } 274 | _this.connected = true; 275 | _this._setupHeartbeat(frame.headers); 276 | _results.push(typeof _this.connectCallback === "function" ? _this.connectCallback(frame) : void 0); 277 | break; 278 | case "MESSAGE": 279 | subscription = frame.headers.subscription; 280 | onreceive = _this.subscriptions[subscription] || _this.onreceive; 281 | if (onreceive) { 282 | client = _this; 283 | messageID = frame.headers["message-id"]; 284 | frame.ack = function(headers) { 285 | if (headers == null) { 286 | headers = {}; 287 | } 288 | return client.ack(messageID, subscription, headers); 289 | }; 290 | frame.nack = function(headers) { 291 | if (headers == null) { 292 | headers = {}; 293 | } 294 | return client.nack(messageID, subscription, headers); 295 | }; 296 | _results.push(onreceive(frame)); 297 | } else { 298 | _results.push(typeof _this.debug === "function" ? _this.debug("Unhandled received MESSAGE: " + frame) : void 0); 299 | } 300 | break; 301 | case "RECEIPT": 302 | _results.push(typeof _this.onreceipt === "function" ? _this.onreceipt(frame) : void 0); 303 | break; 304 | case "ERROR": 305 | _results.push(typeof errorCallback === "function" ? errorCallback(frame) : void 0); 306 | break; 307 | default: 308 | _results.push(typeof _this.debug === "function" ? _this.debug("Unhandled frame: " + frame) : void 0); 309 | } 310 | } 311 | return _results; 312 | }; 313 | })(this); 314 | this.ws.onclose = (function(_this) { 315 | return function() { 316 | var msg; 317 | msg = "Whoops! Lost connection to " + _this.ws.url; 318 | if (typeof _this.debug === "function") { 319 | _this.debug(msg); 320 | } 321 | _this._cleanUp(); 322 | return typeof errorCallback === "function" ? errorCallback(msg) : void 0; 323 | }; 324 | })(this); 325 | return this.ws.onopen = (function(_this) { 326 | return function() { 327 | if (typeof _this.debug === "function") { 328 | _this.debug('Web Socket Opened...'); 329 | } 330 | headers["accept-version"] = Stomp.VERSIONS.supportedVersions(); 331 | headers["heart-beat"] = [_this.heartbeat.outgoing, _this.heartbeat.incoming].join(','); 332 | return _this._transmit("CONNECT", headers); 333 | }; 334 | })(this); 335 | }; 336 | 337 | Client.prototype.disconnect = function(disconnectCallback, headers) { 338 | if (headers == null) { 339 | headers = {}; 340 | } 341 | this._transmit("DISCONNECT", headers); 342 | this.ws.onclose = null; 343 | this.ws.close(); 344 | this._cleanUp(); 345 | return typeof disconnectCallback === "function" ? disconnectCallback() : void 0; 346 | }; 347 | 348 | Client.prototype._cleanUp = function() { 349 | this.connected = false; 350 | if (this.pinger) { 351 | Stomp.clearInterval(this.pinger); 352 | } 353 | if (this.ponger) { 354 | return Stomp.clearInterval(this.ponger); 355 | } 356 | }; 357 | 358 | Client.prototype.send = function(destination, headers, body) { 359 | if (headers == null) { 360 | headers = {}; 361 | } 362 | if (body == null) { 363 | body = ''; 364 | } 365 | headers.destination = destination; 366 | return this._transmit("SEND", headers, body); 367 | }; 368 | 369 | Client.prototype.subscribe = function(destination, callback, headers) { 370 | var client; 371 | if (headers == null) { 372 | headers = {}; 373 | } 374 | if (!headers.id) { 375 | headers.id = "sub-" + this.counter++; 376 | } 377 | headers.destination = destination; 378 | this.subscriptions[headers.id] = callback; 379 | this._transmit("SUBSCRIBE", headers); 380 | client = this; 381 | return { 382 | id: headers.id, 383 | unsubscribe: function() { 384 | return client.unsubscribe(headers.id); 385 | } 386 | }; 387 | }; 388 | 389 | Client.prototype.unsubscribe = function(id) { 390 | delete this.subscriptions[id]; 391 | return this._transmit("UNSUBSCRIBE", { 392 | id: id 393 | }); 394 | }; 395 | 396 | Client.prototype.begin = function(transaction) { 397 | var client, txid; 398 | txid = transaction || "tx-" + this.counter++; 399 | this._transmit("BEGIN", { 400 | transaction: txid 401 | }); 402 | client = this; 403 | return { 404 | id: txid, 405 | commit: function() { 406 | return client.commit(txid); 407 | }, 408 | abort: function() { 409 | return client.abort(txid); 410 | } 411 | }; 412 | }; 413 | 414 | Client.prototype.commit = function(transaction) { 415 | return this._transmit("COMMIT", { 416 | transaction: transaction 417 | }); 418 | }; 419 | 420 | Client.prototype.abort = function(transaction) { 421 | return this._transmit("ABORT", { 422 | transaction: transaction 423 | }); 424 | }; 425 | 426 | Client.prototype.ack = function(messageID, subscription, headers) { 427 | if (headers == null) { 428 | headers = {}; 429 | } 430 | headers["message-id"] = messageID; 431 | headers.subscription = subscription; 432 | return this._transmit("ACK", headers); 433 | }; 434 | 435 | Client.prototype.nack = function(messageID, subscription, headers) { 436 | if (headers == null) { 437 | headers = {}; 438 | } 439 | headers["message-id"] = messageID; 440 | headers.subscription = subscription; 441 | return this._transmit("NACK", headers); 442 | }; 443 | 444 | return Client; 445 | 446 | })(); 447 | 448 | Stomp = { 449 | VERSIONS: { 450 | V1_0: '1.0', 451 | V1_1: '1.1', 452 | V1_2: '1.2', 453 | supportedVersions: function() { 454 | return '1.1,1.0'; 455 | } 456 | }, 457 | client: function(url, protocols) { 458 | var klass, ws; 459 | if (protocols == null) { 460 | protocols = ['v10.stomp', 'v11.stomp']; 461 | } 462 | klass = Stomp.WebSocketClass || WebSocket; 463 | ws = new klass(url, protocols); 464 | return new Client(ws); 465 | }, 466 | over: function(ws) { 467 | return new Client(ws); 468 | }, 469 | Frame: Frame 470 | }; 471 | 472 | if (typeof exports !== "undefined" && exports !== null) { 473 | exports.Stomp = Stomp; 474 | } 475 | 476 | if (typeof window !== "undefined" && window !== null) { 477 | Stomp.setInterval = function(interval, f) { 478 | return window.setInterval(f, interval); 479 | }; 480 | Stomp.clearInterval = function(id) { 481 | return window.clearInterval(id); 482 | }; 483 | window.Stomp = Stomp; 484 | } else if (!exports) { 485 | self.Stomp = Stomp; 486 | } 487 | 488 | }).call(this); --------------------------------------------------------------------------------