diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 000000000..edd91a437 Binary files /dev/null and b/.DS_Store differ diff --git a/.gitignore b/.gitignore index c64b53754..ce53ff50a 100644 --- a/.gitignore +++ b/.gitignore @@ -9,3 +9,5 @@ dependency-reduced-pom.xml .factorypath .project .settings/ +application.yml +.DS_Store diff --git a/README.md b/README.md index 395d736fa..969266918 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,7 @@ # Acebook +https://trello.com/b/VidgaAkH/acebook-engineering-project-2 + The application uses: - `maven` to build the project - `thymeleaf` for templating diff --git a/pom.xml b/pom.xml index 32aeb8fc4..9ca00565e 100644 --- a/pom.xml +++ b/pom.xml @@ -64,6 +64,7 @@ org.projectlombok lombok + 1.18.32 provided @@ -102,6 +103,7 @@ org.springframework.boot spring-boot-maven-plugin + 3.3.2 org.apache.maven.plugins @@ -114,6 +116,7 @@ org.projectlombok lombok + 1.18.32 diff --git a/src/.DS_Store b/src/.DS_Store new file mode 100644 index 000000000..0b3ee9cb1 Binary files /dev/null and b/src/.DS_Store differ diff --git a/src/main/.DS_Store b/src/main/.DS_Store new file mode 100644 index 000000000..af482a9f4 Binary files /dev/null and b/src/main/.DS_Store differ diff --git a/src/main/java/.DS_Store b/src/main/java/.DS_Store new file mode 100644 index 000000000..de3a99e1e Binary files /dev/null and b/src/main/java/.DS_Store differ diff --git a/src/main/java/com/.DS_Store b/src/main/java/com/.DS_Store new file mode 100644 index 000000000..4ed73db6d Binary files /dev/null and b/src/main/java/com/.DS_Store differ diff --git a/src/main/java/com/makersacademy/.DS_Store b/src/main/java/com/makersacademy/.DS_Store new file mode 100644 index 000000000..6700e0985 Binary files /dev/null and b/src/main/java/com/makersacademy/.DS_Store differ diff --git a/src/main/java/com/makersacademy/acebook/.DS_Store b/src/main/java/com/makersacademy/acebook/.DS_Store new file mode 100644 index 000000000..78797ea11 Binary files /dev/null and b/src/main/java/com/makersacademy/acebook/.DS_Store differ diff --git a/src/main/java/com/makersacademy/acebook/config/WebConfig.java b/src/main/java/com/makersacademy/acebook/config/WebConfig.java new file mode 100644 index 000000000..0bc46ff19 --- /dev/null +++ b/src/main/java/com/makersacademy/acebook/config/WebConfig.java @@ -0,0 +1,24 @@ +package com.makersacademy.acebook.config; + +import org.springframework.context.annotation.Configuration; +import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + +// This class customises how the app serves static resources (e.g. images) from the file system +// Resource handlers map URL paths to filesystem directories so static files (e.g. images) can be served via HTTP +// This is required for image storage outside the classpath (src) on uploading + +@Configuration +public class WebConfig implements WebMvcConfigurer { + + @Override + public void addResourceHandlers(ResourceHandlerRegistry registry) { + // Handler for profile images + registry.addResourceHandler("/images/user_profile/**") + .addResourceLocations("file:uploads/user_profile/"); + + // Handler for post images + registry.addResourceHandler("/uploads/post_images/**") + .addResourceLocations("file:uploads/post_images/"); + } +} diff --git a/src/main/java/com/makersacademy/acebook/controller/CommentController.java b/src/main/java/com/makersacademy/acebook/controller/CommentController.java new file mode 100644 index 000000000..da873916c --- /dev/null +++ b/src/main/java/com/makersacademy/acebook/controller/CommentController.java @@ -0,0 +1,49 @@ +//package com.makersacademy.acebook.controller; +// +//import com.makersacademy.acebook.dto.CommentRequest; +//import com.makersacademy.acebook.model.Comment; +//import com.makersacademy.acebook.model.Post; +//import com.makersacademy.acebook.model.User; +//import com.makersacademy.acebook.repository.PostRepository; +//import com.makersacademy.acebook.repository.UserRepository; +//import com.makersacademy.acebook.service.CommentService; +//import org.springframework.web.bind.annotation.*; +// +//import java.util.List; +// +//@RestController +//@RequestMapping("/post/comments") +//public class CommentController { +// +// private final CommentService commentService; +// private final PostRepository postRepository; +// private final UserRepository userRepository; +// +// public CommentController(CommentService commentService, PostRepository postRepository, UserRepository userRepository) { +// this.commentService = commentService; +// this.postRepository = postRepository; +// this.userRepository = userRepository; +// } +// +// @GetMapping("/post/{postId}") +// public List getCommentsByPost(@PathVariable Long postId) { +// return commentService.getCommentsForPost(postId); +// } +// +// @PostMapping +// public Comment createComment(@RequestBody CommentRequest request) { +// Post post = postRepository.findById(request.getPostId()) +// .orElseThrow(() -> new RuntimeException("Post not found")); +// User user = userRepository.findById(request.getUserId()) +// .orElseThrow(() -> new RuntimeException("User not found")); +// +// Long commentId = 0L; +// Comment comment = new Comment(commentId); +// comment.setPost(post); +// comment.setUser(user); +// comment.setContent(request.getContent()); +// +// return commentService.addComment(comment); +// } +//} +// diff --git a/src/main/java/com/makersacademy/acebook/controller/FriendsController.java b/src/main/java/com/makersacademy/acebook/controller/FriendsController.java new file mode 100644 index 000000000..19a4cb86c --- /dev/null +++ b/src/main/java/com/makersacademy/acebook/controller/FriendsController.java @@ -0,0 +1,133 @@ +package com.makersacademy.acebook.controller; + +import com.makersacademy.acebook.model.Friend; +import com.makersacademy.acebook.model.FriendRequest; +import com.makersacademy.acebook.model.User; +import com.makersacademy.acebook.repository.FriendRepository; +import com.makersacademy.acebook.repository.FriendRequestRepository; +import com.makersacademy.acebook.repository.UserRepository; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.oauth2.core.oidc.user.DefaultOidcUser; +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.servlet.ModelAndView; +import org.springframework.web.servlet.view.RedirectView; + +import java.sql.Timestamp; +import java.time.Instant; +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; + +@Controller +public class FriendsController { + + @Autowired + FriendRequestRepository friendRequestRepository; + @Autowired + FriendRepository friendRepository; + @Autowired + UserRepository userRepository; + + @GetMapping("/friends") + public ModelAndView friendList(@AuthenticationPrincipal(expression = "attributes['email']") String email) { + ModelAndView modelAndView = new ModelAndView("friends/friends"); + + // User! + Optional userOptional = userRepository.findUserByUsername(email); + + User currentUser = userOptional.get(); + Long userId = currentUser.getId(); + + // Friends! + List friendsList = friendRepository.findAllByMainUserId(userId); + + // Objectify! + List friendUsers = new ArrayList<>(); + for (Friend friend : friendsList) { + Long friendId = friend.getFriendUserId(); + Optional friendUser = userRepository.findById(friendId); + if (friendUser.isPresent()) { + friendUsers.add(friendUser.get()); + } + } + + // Friend Requests! + List pendingRequests = friendRequestRepository.findAllByReceiverIdAndStatus(userId, "pending"); + + // Objectify! + List requesterUsers = new ArrayList<>(); + for (FriendRequest request : pendingRequests) { + Long requesterId = request.getRequesterId(); + Optional requesterUser = userRepository.findById(requesterId); + if (requesterUser.isPresent()) { + requesterUsers.add(requesterUser.get()); + } + } + + modelAndView.addObject("friendUsers", friendUsers); + modelAndView.addObject("requesterUsers", requesterUsers); + + return modelAndView; + } + + @PostMapping("/friend_request/{requesterId}") + public RedirectView respondToFriendRequest( + @PathVariable Long requesterId, + @RequestParam String decision, + @AuthenticationPrincipal(expression = "attributes['email']") String email) { + + // Get User! + Optional userOptional = userRepository.findUserByUsername(email); + + User currentUser = userOptional.get(); + Long currentUserId = currentUser.getId(); + + // Get Friend Request! + Optional friendRequestOptional = friendRequestRepository + .findByRequesterIdAndReceiverIdAndStatus(requesterId, currentUserId, "pending"); + + + FriendRequest friendRequest = friendRequestOptional.get(); + + if (decision.equals("accept")) { + friendRequest.setStatus("accepted"); + Instant instant = Instant.now(); + Timestamp now = Timestamp.from(instant); + friendRequest.setRespondedAt(now); + friendRequestRepository.save(friendRequest); + + + Friend friendship1 = new Friend(); + friendship1.setMainUserId(currentUserId); + friendship1.setFriendUserId(requesterId); + friendship1.setFriendsSince(now); + friendRepository.save(friendship1); + + Friend friendship2 = new Friend(); + friendship2.setMainUserId(requesterId); + friendship2.setFriendUserId(currentUserId); + friendship2.setFriendsSince(now); + friendRepository.save(friendship2); + + } else if (decision.equals("decline")) { + friendRequest.setStatus("rejected"); +// friendRequest.setRespondedAt(); + friendRequestRepository.save(friendRequest); + } + + return new RedirectView("/friends"); + } + +// @PostMapping("/friends") +// public RedirectView create(@ModelAttribute Friend friend, @AuthenticationPrincipal(expression = "attributes['email']") String email) { +// Optional user = userRepository.findUserByUsername(email); +// if (user.isPresent()) { +// Long id = user.get().getId(); +// } +// return new RedirectView("friends/friends"); +// } + } + diff --git a/src/main/java/com/makersacademy/acebook/controller/HomeController.java b/src/main/java/com/makersacademy/acebook/controller/HomeController.java index 2036ec7e0..1bad57528 100644 --- a/src/main/java/com/makersacademy/acebook/controller/HomeController.java +++ b/src/main/java/com/makersacademy/acebook/controller/HomeController.java @@ -2,12 +2,13 @@ import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.servlet.ModelAndView; import org.springframework.web.servlet.view.RedirectView; @Controller public class HomeController { @RequestMapping(value = "/") - public RedirectView index() { - return new RedirectView("/posts"); + public ModelAndView index() { + return new ModelAndView("/landing"); } } diff --git a/src/main/java/com/makersacademy/acebook/controller/NotificationsController.java b/src/main/java/com/makersacademy/acebook/controller/NotificationsController.java new file mode 100644 index 000000000..e46bff360 --- /dev/null +++ b/src/main/java/com/makersacademy/acebook/controller/NotificationsController.java @@ -0,0 +1,65 @@ +package com.makersacademy.acebook.controller; + +import com.makersacademy.acebook.model.Notification; +import com.makersacademy.acebook.model.User; +import com.makersacademy.acebook.repository.NotificationRepository; +import com.makersacademy.acebook.repository.UserRepository; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.oauth2.core.oidc.user.DefaultOidcUser; +import org.springframework.stereotype.Controller; +import org.springframework.ui.Model; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.servlet.view.RedirectView; + +import java.util.*; + +@Controller +public class NotificationsController { + + @Autowired + NotificationRepository notificationRepository; + @Autowired + UserRepository userRepository; + + + // Get notifications for current user + @GetMapping("/notifications") + public String index(Model model) { + DefaultOidcUser principal = (DefaultOidcUser) SecurityContextHolder + .getContext() + .getAuthentication() + .getPrincipal(); + String username = (String) principal.getAttributes().get("email"); + User currentUser = userRepository.findUserByUsername(username).get(); + + Collection notifications = new ArrayList<>(); + notifications.addAll(notificationRepository.findByReceivingUserIdOrderByCreatedAtDesc(2L)); + Map senderNames = new HashMap<>(); + for (Notification notification : notifications) { + Optional sender = userRepository.findById(notification.getSendingUserId()); + sender.ifPresent(user -> senderNames.put(notification.getId(), user.getFirst_name())); + } + model.addAttribute("notifications", notifications); + model.addAttribute("senderNames", senderNames); + return "notifications/index"; + } + + + // Mark notification as read, redirect to specific post page + @PostMapping("/notifications") + public RedirectView markAsRead(@ModelAttribute Notification notification, @RequestParam String id) { + Long notificationId = Long.parseLong(id); + Optional readNotification = notificationRepository.findById(notificationId); + if (readNotification.isPresent()) { + Notification activeNotification = readNotification.get(); + activeNotification.setRead(true); + notificationRepository.save(activeNotification); + String postId = Long.toString(activeNotification.getPostId()); + return new RedirectView("/posts/" + postId); + } + else { + return new RedirectView("genericErrorPage"); + } + } +} diff --git a/src/main/java/com/makersacademy/acebook/controller/PostsController.java b/src/main/java/com/makersacademy/acebook/controller/PostsController.java index 57a7e5f4d..360dc3cc6 100644 --- a/src/main/java/com/makersacademy/acebook/controller/PostsController.java +++ b/src/main/java/com/makersacademy/acebook/controller/PostsController.java @@ -1,32 +1,107 @@ package com.makersacademy.acebook.controller; import com.makersacademy.acebook.model.Post; +import com.makersacademy.acebook.model.User; import com.makersacademy.acebook.repository.PostRepository; +import com.makersacademy.acebook.repository.UserRepository; +import com.makersacademy.acebook.service.ImageStorageService; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.oauth2.core.oidc.user.DefaultOidcUser; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; import org.springframework.web.bind.annotation.*; +import org.springframework.web.multipart.MultipartFile; +import org.springframework.web.servlet.ModelAndView; import org.springframework.web.servlet.view.RedirectView; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.nio.file.StandardCopyOption; import java.util.List; +import java.util.Optional; @Controller public class PostsController { @Autowired - PostRepository repository; + PostRepository postRepository; + @Autowired + UserRepository userRepository; + @Autowired + ImageStorageService imageStorageService; + @Value("${file.upload-dir.post-images}") + private String uploadDir; + + // View all posts @GetMapping("/posts") public String index(Model model) { - Iterable posts = repository.findAll(); + Iterable posts = postRepository.findByOrderByTimePostedDesc(); model.addAttribute("posts", posts); model.addAttribute("post", new Post()); + + // Creates principal variable of type Default0idcUser - Spring Security class which represents a user authenticated by Auth0 + // The get commands work together to extract user attributes from Auth0 (email, given_name, family_name) + DefaultOidcUser principal = (DefaultOidcUser) SecurityContextHolder + .getContext() + .getAuthentication() + .getPrincipal(); + + // Uses the pricipal variable above to extract email and assign to username var + // Then utilises the 'username' variable to search the database and return matching User object + // Adds the user object to the model (page) + String username = (String) principal.getAttributes().get("email"); + User user = userRepository.findUserByUsername(username).get(); + model.addAttribute("user", user); + return "posts/index"; } + + // Create new post @PostMapping("/posts") - public RedirectView create(@ModelAttribute Post post) { - repository.save(post); + public RedirectView create( + @RequestParam("content") String content, + @AuthenticationPrincipal(expression = "attributes['email']") String email, + @RequestParam("image") MultipartFile file + ) throws IOException { + Optional user = userRepository.findUserByUsername(email); + if (user.isPresent()) { + Post post = new Post(); + post.setContent(content); + post.setUserId(user.get().getId()); + + postRepository.save(post); + + String fileName = imageStorageService.storePostImage(file, String.valueOf(post.getId())); + if (fileName != null) { + post.setImage(fileName); + postRepository.save(post); + } + + } return new RedirectView("/posts"); } + + + //View specific post + @GetMapping("/posts/{id}") + public ModelAndView viewPost(@PathVariable("id") Long id) { + ModelAndView modelAndView = new ModelAndView("posts/post"); + ModelAndView errorView = new ModelAndView("genericErrorPage"); + Optional currentPost = postRepository.findById(id); + if (currentPost.isPresent()) { + Post post = currentPost.get(); + modelAndView.addObject("post", post); + return modelAndView; + } + else { + return errorView; + } + } } diff --git a/src/main/java/com/makersacademy/acebook/controller/UsersController.java b/src/main/java/com/makersacademy/acebook/controller/UsersController.java index a7c9db1d8..bd7d4d63a 100644 --- a/src/main/java/com/makersacademy/acebook/controller/UsersController.java +++ b/src/main/java/com/makersacademy/acebook/controller/UsersController.java @@ -2,29 +2,112 @@ import com.makersacademy.acebook.model.User; import com.makersacademy.acebook.repository.UserRepository; +import com.makersacademy.acebook.service.ImageStorageService; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.oauth2.core.oidc.user.DefaultOidcUser; import org.springframework.web.bind.annotation.*; +import org.springframework.web.multipart.MultipartFile; +import org.springframework.web.servlet.ModelAndView; import org.springframework.web.servlet.view.RedirectView; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.nio.file.StandardCopyOption; +import java.util.Optional; + @RestController public class UsersController { @Autowired UserRepository userRepository; + @Autowired + private ImageStorageService imageStorageService; + + // Inserts "uploads/user_profile" filepath (taken from application.properties) as uploadDir variable value + @Value("${file.upload-dir.user-profile}") + private String uploadDir; + @GetMapping("/users/after-login") public RedirectView afterLogin() { + + // Creates principal variable of type Default0idcUser - Spring Security class which represents a user authenticated by Auth0 + // The get commands work together to extract user attributes from Auth0 (email, given_name, family_name) DefaultOidcUser principal = (DefaultOidcUser) SecurityContextHolder .getContext() .getAuthentication() .getPrincipal(); + // Uses the pricipal variable above to extract email, given_name and family_name from Auth0 + // Sets a default profile picture String username = (String) principal.getAttributes().get("email"); + String first_name = (String) principal.getAttributes().get("given_name"); + String last_name = (String) principal.getAttributes().get("family_name"); + String profile_pic = "/images/profile/default.jpg"; + + // Uses the email address captured above to find the relevant user + // OR creates a new user if doesn't exist, utilising the elements captured above userRepository .findUserByUsername(username) - .orElseGet(() -> userRepository.save(new User(username))); + .orElseGet(() -> userRepository.save(new User(username, first_name, last_name, profile_pic))); return new RedirectView("/posts"); } -} + + @GetMapping("/settings") + public ModelAndView settings() { + + // Creates principal variable of type Default0idcUser - Spring Security class which represents a user authenticated by Auth0 + // The get commands work together to extract user attributes from Auth0 (email, given_name, family_name) + DefaultOidcUser principal = (DefaultOidcUser) SecurityContextHolder + .getContext() + .getAuthentication() + .getPrincipal(); + + // Uses the pricipal variable above to extract email + // Then utilises the 'username' variable to search the database and return matching User object + // Creates a new ModelandView and adds the User object to be referenced in Thymeleaf + String username = (String) principal.getAttributes().get("email"); + User userByEmail = userRepository.findUserByUsername(username).get(); + ModelAndView settings = new ModelAndView("/users/settings"); + settings.addObject("user", userByEmail); + return settings; + } + + @PostMapping("/settings") + public RedirectView update(@ModelAttribute User userFromForm, @RequestParam("file") MultipartFile file) throws IOException { + + // Creates principal variable of type Default0idcUser - Spring Security class which represents a user authenticated by Auth0 + // The get commands work together to extract user attributes from Auth0 (email, given_name, family_name) + DefaultOidcUser principal = (DefaultOidcUser) SecurityContextHolder + .getContext() + .getAuthentication() + .getPrincipal(); + + // Uses the pricipal variable above to extract email + String email = (String) principal.getAttributes().get("email"); + + // Creates a new User object representing the User extracted from the database, with matching email(username) + // Error mapping TBC + User userInDb = userRepository.findUserByUsername(email) + .orElseThrow(() -> new IllegalArgumentException("User not found")); + + imageStorageService.storeProfileImage(file, String.valueOf(userInDb.getId())); + + // Sets database user properties as values returned by User object from form + // The userFromForm is taken as an input to the function as a whole, generated by the form within the HTML template + userInDb.setFirst_name(userFromForm.getFirst_name()); + userInDb.setLast_name(userFromForm.getLast_name()); + + // Save function will update, rather than create, if user already in existence - once userInDb attributes are + // updated, essentially overwrites existing name and image fields + userRepository.save(userInDb); + + return new RedirectView("/settings"); + } +} \ No newline at end of file diff --git a/src/main/java/com/makersacademy/acebook/dto/CommentRequest.java b/src/main/java/com/makersacademy/acebook/dto/CommentRequest.java new file mode 100644 index 000000000..b4b727dd4 --- /dev/null +++ b/src/main/java/com/makersacademy/acebook/dto/CommentRequest.java @@ -0,0 +1,17 @@ +//package com.makersacademy.acebook.dto; +// +//import lombok.Getter; +//import lombok.Setter; +// +//@Getter +//@Setter +// +// +//public class CommentRequest { +// private Long postId; +// private Long userId; +// private String content; +// +// +//} +// diff --git a/src/main/java/com/makersacademy/acebook/model/Comment.java b/src/main/java/com/makersacademy/acebook/model/Comment.java new file mode 100644 index 000000000..4ad06c5d0 --- /dev/null +++ b/src/main/java/com/makersacademy/acebook/model/Comment.java @@ -0,0 +1,38 @@ +//package com.makersacademy.acebook.model; +// +//import jakarta.persistence.*; +//import lombok.Getter; +//import lombok.Setter; +//import lombok.NoArgsConstructor; +// +//import java.time.LocalDateTime; +// +//@Entity +//@Table(name = "comments") +//@Getter +//@Setter +//@NoArgsConstructor +//public class Comment { +// +// @Id +// @GeneratedValue(strategy = GenerationType.IDENTITY) +// private Long id; +// +// @ManyToOne(fetch = FetchType.LAZY) +// @JoinColumn(name = "post_id", nullable = false) +// private Post post; +// +// @ManyToOne(fetch = FetchType.LAZY) +// @JoinColumn(name = "user_id", nullable = false) +// private User user; +// +// @Column(nullable = false, columnDefinition = "TEXT") +// private String content; +// +// @Column(name = "created_at") +// private LocalDateTime createdAt = LocalDateTime.now(); +// +// public Comment(Long id) { +// this.id = id; +// } +//} diff --git a/src/main/java/com/makersacademy/acebook/model/CommentLike.java b/src/main/java/com/makersacademy/acebook/model/CommentLike.java new file mode 100644 index 000000000..fea180194 --- /dev/null +++ b/src/main/java/com/makersacademy/acebook/model/CommentLike.java @@ -0,0 +1,27 @@ +//package com.makersacademy.acebook.model; +//import jakarta.persistence.*; +//import lombok.Getter; +//import lombok.NoArgsConstructor; +//import lombok.Setter; +// +//@Entity +//@Table(name = "comment_likes", uniqueConstraints = @UniqueConstraint(columnNames = {"user_id", "comment_id"})) +//@Getter +//@Setter +//@IdClass(CommentLikeId.class) +//@NoArgsConstructor +//public class CommentLike { +// +// +// +// @Id +// +// @JoinColumn(name = "user_id", nullable = false) +// private Long userId; +// +// @Id +// +// @JoinColumn(name = "comment_id", nullable = false) +// private Long commentId; +// +//} diff --git a/src/main/java/com/makersacademy/acebook/model/CommentLikeId.java b/src/main/java/com/makersacademy/acebook/model/CommentLikeId.java new file mode 100644 index 000000000..f02131200 --- /dev/null +++ b/src/main/java/com/makersacademy/acebook/model/CommentLikeId.java @@ -0,0 +1,14 @@ +//package com.makersacademy.acebook.model; +// +//import java.io.Serializable; +// +//public class CommentLikeId implements Serializable { +// private Long userId; +// private Long commentId; +// +// public CommentLikeId(Long userId, Long commentId) { +// this.userId = userId; +// this.commentId = commentId; +// } +// +//} diff --git a/src/main/java/com/makersacademy/acebook/model/Friend.java b/src/main/java/com/makersacademy/acebook/model/Friend.java new file mode 100644 index 000000000..a546e292d --- /dev/null +++ b/src/main/java/com/makersacademy/acebook/model/Friend.java @@ -0,0 +1,35 @@ +package com.makersacademy.acebook.model; + +import jakarta.persistence.*; +import lombok.*; +import java.io.Serializable; +import java.sql.Timestamp; + +// Composite key class +@Data +@NoArgsConstructor +@AllArgsConstructor +class FriendId implements Serializable { + private Long mainUserId; + private Long friendUserId; +} + +@Data +@Entity +@Table(name = "friends") +@NoArgsConstructor +@AllArgsConstructor +@IdClass(FriendId.class) +public class Friend { + + @Id + @Column(name = "main_user_id") + private Long mainUserId; + + @Id + @Column(name = "friend_user_id") + private Long friendUserId; + + @Column(name = "friends_since") + private Timestamp friendsSince; +} \ No newline at end of file diff --git a/src/main/java/com/makersacademy/acebook/model/FriendRequest.java b/src/main/java/com/makersacademy/acebook/model/FriendRequest.java new file mode 100644 index 000000000..1b0e665ba --- /dev/null +++ b/src/main/java/com/makersacademy/acebook/model/FriendRequest.java @@ -0,0 +1,33 @@ +package com.makersacademy.acebook.model; + +import jakarta.persistence.*; +import lombok.*; + +import java.sql.Timestamp; + +@Data +@Entity +@Table(name = "friend_requests") +@NoArgsConstructor +@AllArgsConstructor +public class FriendRequest { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(name = "requester_id") + private Long requesterId; + + @Column(name = "receiver_id") + private Long receiverId; + + @Column(name = "status") + private String status; + + @Column(name = "created_at") + private Timestamp createdAt; + + @Column(name = "responded_at") + private Timestamp respondedAt; +} \ No newline at end of file diff --git a/src/main/java/com/makersacademy/acebook/model/Notification.java b/src/main/java/com/makersacademy/acebook/model/Notification.java new file mode 100644 index 000000000..a7b5c7d99 --- /dev/null +++ b/src/main/java/com/makersacademy/acebook/model/Notification.java @@ -0,0 +1,34 @@ +package com.makersacademy.acebook.model; + + +import jakarta.persistence.*; +import lombok.*; +import org.hibernate.annotations.DynamicUpdate; + +import java.sql.Timestamp; + +@Data +@Entity(name = "Notification") +@Table(name = "NOTIFICATIONS") +@NoArgsConstructor +@AllArgsConstructor +@DynamicUpdate +public class Notification { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(name="receiving_user_id") + private Long receivingUserId; + @Column(name="sending_user_id") + private Long sendingUserId; + private String type; + @Column(name= "post_id") + private Long postId; + @Column(name="is_read") + private boolean isRead; + @Column(name="created_at") + private Timestamp createdAt; + +} diff --git a/src/main/java/com/makersacademy/acebook/model/Post.java b/src/main/java/com/makersacademy/acebook/model/Post.java index 33492c6b1..1e7e93653 100644 --- a/src/main/java/com/makersacademy/acebook/model/Post.java +++ b/src/main/java/com/makersacademy/acebook/model/Post.java @@ -1,22 +1,30 @@ package com.makersacademy.acebook.model; import jakarta.persistence.*; +import lombok.*; +import org.hibernate.annotations.CreationTimestamp; -import lombok.Data; +import java.sql.Timestamp; @Data @Entity @Table(name = "POSTS") +@AllArgsConstructor +@NoArgsConstructor public class Post { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; private String content; + @Column(name="user_id") + private Long userId; // current user is populated in PostController + @CreationTimestamp + @Column(name="time_posted") + private Timestamp timePosted; + private String image; + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "user_id", insertable = false, updatable = false) + private User user; - public Post() {} - - public Post(String content) { - this.content = content; - } } diff --git a/src/main/java/com/makersacademy/acebook/model/User.java b/src/main/java/com/makersacademy/acebook/model/User.java index 6013fbe23..fe7f1ba63 100644 --- a/src/main/java/com/makersacademy/acebook/model/User.java +++ b/src/main/java/com/makersacademy/acebook/model/User.java @@ -14,18 +14,33 @@ public class User { private Long id; private String username; private boolean enabled; + private String first_name; + private String last_name; + private String profile_pic; - public User() { - this.enabled = TRUE; + public User(){ } public User(String username) { + this.enabled = TRUE; + } + + public User(String username, String first_name, String last_name, String profile_pic) { this.username = username; this.enabled = TRUE; + this.first_name = first_name; + this.last_name = last_name; + this.profile_pic = profile_pic; } - public User(String username, boolean enabled) { + public User(String username, boolean enabled, String first_name, String last_name, String profile_pic) { this.username = username; this.enabled = enabled; + this.first_name = first_name; + this.last_name = last_name; + this.profile_pic = profile_pic; + } + + public User(Long userId) { } -} +} \ No newline at end of file diff --git a/src/main/java/com/makersacademy/acebook/repository/CommentLikeRepository.java b/src/main/java/com/makersacademy/acebook/repository/CommentLikeRepository.java new file mode 100644 index 000000000..69e650d33 --- /dev/null +++ b/src/main/java/com/makersacademy/acebook/repository/CommentLikeRepository.java @@ -0,0 +1,13 @@ +//package com.makersacademy.acebook.repository; +//import com.makersacademy.acebook.model.CommentLike; +//import org.springframework.data.jpa.repository.JpaRepository; +//import org.springframework.stereotype.Repository; +//import java.util.Optional; +// +//@Repository +// +//public interface CommentLikeRepository extends JpaRepository { +// Optional findByUser_IdAndComment_Id(Long userId, Long commentId); +// long countByComment_Id(Long commentId); +// void deleteByUser_IdAndComment_Id(Long userId, Long commentId); +//} diff --git a/src/main/java/com/makersacademy/acebook/repository/CommentRepository.java b/src/main/java/com/makersacademy/acebook/repository/CommentRepository.java new file mode 100644 index 000000000..21f0fc50d --- /dev/null +++ b/src/main/java/com/makersacademy/acebook/repository/CommentRepository.java @@ -0,0 +1,16 @@ +//package com.makersacademy.acebook.repository; +// +//import com.makersacademy.acebook.model.Comment; +//import org.springframework.data.jpa.repository.JpaRepository; +//import org.springframework.stereotype.Repository; +//import java.util.List; +// +//@Repository +//public interface CommentRepository extends JpaRepository { +// +// +// List findByPost_IdOrderByCreatedAtDesc(Long postId); +// +// +// List findByUser_Id(Long userId); +//} diff --git a/src/main/java/com/makersacademy/acebook/repository/FriendRepository.java b/src/main/java/com/makersacademy/acebook/repository/FriendRepository.java new file mode 100644 index 000000000..a4cf0c4d7 --- /dev/null +++ b/src/main/java/com/makersacademy/acebook/repository/FriendRepository.java @@ -0,0 +1,13 @@ +package com.makersacademy.acebook.repository; + +import com.makersacademy.acebook.model.Friend; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.repository.CrudRepository; + +import java.util.List; + + +public interface FriendRepository extends CrudRepository { + List findAllByMainUserId(Long mainUserId); + +} diff --git a/src/main/java/com/makersacademy/acebook/repository/FriendRequestRepository.java b/src/main/java/com/makersacademy/acebook/repository/FriendRequestRepository.java new file mode 100644 index 000000000..33a002301 --- /dev/null +++ b/src/main/java/com/makersacademy/acebook/repository/FriendRequestRepository.java @@ -0,0 +1,16 @@ +package com.makersacademy.acebook.repository; + +import com.makersacademy.acebook.model.FriendRequest; +import com.makersacademy.acebook.model.User; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.repository.CrudRepository; + +import java.util.List; +import java.util.Optional; + + +public interface FriendRequestRepository extends CrudRepository { + List findAllByReceiverIdAndStatus(Long receiverId, String status); + + Optional findByRequesterIdAndReceiverIdAndStatus(Long requesterId, Long currentUserId, String pending); +} diff --git a/src/main/java/com/makersacademy/acebook/repository/NotificationRepository.java b/src/main/java/com/makersacademy/acebook/repository/NotificationRepository.java new file mode 100644 index 000000000..543138be5 --- /dev/null +++ b/src/main/java/com/makersacademy/acebook/repository/NotificationRepository.java @@ -0,0 +1,11 @@ +package com.makersacademy.acebook.repository; + +import com.makersacademy.acebook.model.Notification; +import org.springframework.data.domain.Sort; +import org.springframework.data.repository.CrudRepository; +import java.util.List; + +public interface NotificationRepository extends CrudRepository { + List findAllByReceivingUserId(Long receivingUserId); + List findByReceivingUserIdOrderByCreatedAtDesc(Long receivingUserId); +} diff --git a/src/main/java/com/makersacademy/acebook/repository/PostRepository.java b/src/main/java/com/makersacademy/acebook/repository/PostRepository.java index d435e0ce1..64a5943c1 100644 --- a/src/main/java/com/makersacademy/acebook/repository/PostRepository.java +++ b/src/main/java/com/makersacademy/acebook/repository/PostRepository.java @@ -1,9 +1,13 @@ package com.makersacademy.acebook.repository; +import com.makersacademy.acebook.model.Notification; import com.makersacademy.acebook.model.Post; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.repository.CrudRepository; -public interface PostRepository extends CrudRepository { +import java.util.List; +public interface PostRepository extends CrudRepository { + List findByUserIdOrderByTimePostedDesc(Long receivingUserId); + List findByOrderByTimePostedDesc(); } diff --git a/src/main/java/com/makersacademy/acebook/service/CommentService.java b/src/main/java/com/makersacademy/acebook/service/CommentService.java new file mode 100644 index 000000000..b4f8ed0de --- /dev/null +++ b/src/main/java/com/makersacademy/acebook/service/CommentService.java @@ -0,0 +1,57 @@ +//package com.makersacademy.acebook.service; +// +//import com.makersacademy.acebook.model.Comment; +//import com.makersacademy.acebook.model.CommentLike; +//import com.makersacademy.acebook.model.User; +//import com.makersacademy.acebook.repository.CommentLikeRepository; +//import com.makersacademy.acebook.repository.CommentRepository; +//import org.springframework.stereotype.Service; +//import org.springframework.transaction.annotation.Transactional; +// +//import java.time.LocalDateTime; +//import java.util.List; +// +//@Service +//public class CommentService { +// +// private final CommentRepository commentRepository; +// private final CommentLikeRepository commentLikeRepository; +// +// public CommentService(CommentRepository commentRepository, +// CommentLikeRepository commentLikeRepository) { +// this.commentRepository = commentRepository; +// this.commentLikeRepository = commentLikeRepository; +// } +// +// public Comment addComment(Comment comment) { +// comment.setCreatedAt(LocalDateTime.now()); +// return commentRepository.save(comment); +// } +// +// public List getCommentsForPost(Long postId) { +// return commentRepository.findByPost_IdOrderByCreatedAtDesc(postId); +// } +// +// @Transactional +// public void likeComment(Long userId, Long commentId) { +// boolean alreadyLiked = commentLikeRepository +// .findByUser_IdAndComment_Id(userId, commentId) +// .isPresent(); +// +// if (!alreadyLiked) { +// CommentLike like = new CommentLike(); +// like.setUserId(userId); +// like.setCommentId(commentId); +// commentLikeRepository.save(like); +// } +// } +// +// @Transactional +// public void unlikeComment(Long userId, Long commentId) { +// commentLikeRepository.deleteByUser_IdAndComment_Id(userId, commentId); +// } +// +// public long getLikesCount(Long commentId) { +// return commentLikeRepository.countByComment_Id(commentId); +// } +//} diff --git a/src/main/java/com/makersacademy/acebook/service/ImageStorageService.java b/src/main/java/com/makersacademy/acebook/service/ImageStorageService.java new file mode 100644 index 000000000..ad83fdcfb --- /dev/null +++ b/src/main/java/com/makersacademy/acebook/service/ImageStorageService.java @@ -0,0 +1,70 @@ +package com.makersacademy.acebook.service; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; +import org.springframework.util.StringUtils; +import org.springframework.web.multipart.MultipartFile; + +import java.io.IOException; +import java.nio.file.*; +import java.util.Optional; +import java.util.UUID; + +@Service +public class ImageStorageService { + + // Locations for storing profile / post images within the app setup - configured within application.properties + @Value("${file.upload-dir.user-profile}") + private String uploadDirProfile; + + @Value("${file.upload-dir.post-images}") + private String uploadDirPost; + + // Segregated methods for storing the different types of images + public String storeProfileImage(MultipartFile file, String id) throws IOException { + return storeImage(file, uploadDirProfile, id); + } + + public String storePostImage(MultipartFile file, String id) throws IOException { + return storeImage(file, uploadDirPost, id); + } + + // Generic storeImage method which is called in both methods above, but fed different parameters for storage location + // Takes a file, a String representing target directory (for image storage) and a string id which helps configure filename + private String storeImage(MultipartFile file, String targetDir, String id) throws IOException { + // Checks whether file is empty + if (file.isEmpty()){ + return null; + } + + // Check file is of correct type + String contentType = file.getContentType(); + if (!contentType.equals("image/jpeg") && !contentType.equals("image/png")) { + throw new IllegalArgumentException("Only JPEG or PNG images are allowed"); + } + + // Extracts the file extension from the filename, and concatenates with + String extension = getFileExtension(file.getOriginalFilename()); + String newFileName = id + extension; + + // These two blocks handle setting the upload path and getting the file stored in right location + Path uploadPath = Paths.get(targetDir); + if (!Files.exists(uploadPath)) { + Files.createDirectories(uploadPath); + } + + Path filePath = uploadPath.resolve(newFileName); + Files.copy(file.getInputStream(), filePath, StandardCopyOption.REPLACE_EXISTING); + + return newFileName; + } + + public static String getFileExtension(String fileName) { + return Optional.ofNullable(fileName) + .filter(f -> f.contains(".")) + .map(f -> f.substring(f.lastIndexOf("."))) + .orElse(""); + } + + +} \ No newline at end of file diff --git a/src/main/resources/.DS_Store b/src/main/resources/.DS_Store new file mode 100644 index 000000000..8cfd43044 Binary files /dev/null and b/src/main/resources/.DS_Store differ diff --git a/src/main/resources/application-test.properties b/src/main/resources/application-test.properties index 865b41e1c..926b9edf4 100644 --- a/src/main/resources/application-test.properties +++ b/src/main/resources/application-test.properties @@ -3,4 +3,4 @@ spring.datasource.username= spring.datasource.password= flyway.baseline-on-migrate=true spring.jpa.properties.hibernate.temp.use_jdbc_metadata_defaults = false -spring.jpa.database-platform=org.hibernate.dialect.PostgreSQL9Dialect +spring.jpa.database-platform=org.hibernate.dialect.PostgreSQLDialect diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 7b2ed1a6b..eab4a7acc 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -6,3 +6,9 @@ logging.level.org.springframework.web=DEBUG logging.level.org.springframework.security=DEBUG logging.level.org.springframework.web.client.RestTemplate=DEBUG logging.level.org.apache.http=DEBUG +file.upload-dir.user-profile=uploads/user_profile +file.upload-dir.post-images=uploads/post_images +spring.web.resources.static-locations=classpath:/static/,file:uploads/ +spring.servlet.multipart.enabled=true +spring.servlet.multipart.max-file-size=5MB +spring.servlet.multipart.max-request-size=5MB \ No newline at end of file diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml deleted file mode 100644 index 699e8575a..000000000 --- a/src/main/resources/application.yml +++ /dev/null @@ -1,5 +0,0 @@ -okta: - oauth2: - issuer: https://dev-edward-andress.uk.auth0.com/ - client-id: ${OKTA_CLIENT_ID} - client-secret: ${OKTA_CLIENT_SECRET} diff --git a/src/main/resources/db/migration/V10__create_notifications_table.sql b/src/main/resources/db/migration/V10__create_notifications_table.sql new file mode 100644 index 000000000..ed3bc154e --- /dev/null +++ b/src/main/resources/db/migration/V10__create_notifications_table.sql @@ -0,0 +1,17 @@ +DROP TABLE IF EXISTS notifications; + +CREATE TABLE notifications ( + id BIGSERIAL PRIMARY KEY, + receiving_user_id BIGINT NOT NULL, + sending_user_id BIGINT, + type TEXT NOT NULL, + post_id BIGINT, -- means clicking on this could take you to related post + comment_id BIGINT, -- same but for comment. + is_read BOOLEAN DEFAULT FALSE, + created_at TIMESTAMP DEFAULT NOW(), + + FOREIGN KEY (receiving_user_id) REFERENCES users(id) ON DELETE CASCADE, + FOREIGN KEY (sending_user_id) REFERENCES users(id) ON DELETE SET NULL, + FOREIGN KEY (post_id) REFERENCES posts(id) ON DELETE CASCADE, + FOREIGN KEY (comment_id) REFERENCES comments(id) ON DELETE CASCADE +); diff --git a/src/main/resources/db/migration/V11__create_friend_requests_table.sql b/src/main/resources/db/migration/V11__create_friend_requests_table.sql new file mode 100644 index 000000000..c8745a6a0 --- /dev/null +++ b/src/main/resources/db/migration/V11__create_friend_requests_table.sql @@ -0,0 +1,18 @@ +DROP TABLE IF EXISTS friend_requests; + +CREATE TABLE friend_requests ( + id BIGSERIAL PRIMARY KEY, + requester_id BIGINT NOT NULL, + receiver_id BIGINT NOT NULL, + status TEXT NOT NULL DEFAULT 'pending',-- 'pending', 'accepted', 'rejected' + created_at TIMESTAMP DEFAULT NOW(), + responded_at TIMESTAMP, + + FOREIGN KEY (requester_id) REFERENCES users(id) ON DELETE CASCADE, + FOREIGN KEY (receiver_id) REFERENCES users(id) ON DELETE CASCADE, + + CHECK (requester_id <> receiver_id), + CHECK (status IN ('pending', 'accepted', 'rejected')), + + UNIQUE (requester_id, receiver_id) -- prevent duplicate requests +); diff --git a/src/main/resources/db/migration/V12__alter_friends_table_add_friends_since.sql b/src/main/resources/db/migration/V12__alter_friends_table_add_friends_since.sql new file mode 100644 index 000000000..a12b44b04 --- /dev/null +++ b/src/main/resources/db/migration/V12__alter_friends_table_add_friends_since.sql @@ -0,0 +1,15 @@ + + +DROP TABLE IF EXISTS friends; + +CREATE TABLE friends ( + main_user_id bigint NOT NULL, + friend_user_id bigint NOT NULL, + friends_since timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (main_user_id, friend_user_id), + + FOREIGN KEY (main_user_id) REFERENCES users(id) ON DELETE CASCADE, + FOREIGN KEY (friend_user_id) REFERENCES users(id) ON DELETE CASCADE, + + CHECK (main_user_id <> friend_user_id) +); diff --git a/src/main/resources/db/migration/V13__add_image_column_to_post_table.sql b/src/main/resources/db/migration/V13__add_image_column_to_post_table.sql new file mode 100644 index 000000000..00961bfe6 --- /dev/null +++ b/src/main/resources/db/migration/V13__add_image_column_to_post_table.sql @@ -0,0 +1,2 @@ +ALTER TABLE posts +ADD image text; \ No newline at end of file diff --git a/src/main/resources/db/migration/V3__alter_users_table_for_new_columns.sql b/src/main/resources/db/migration/V3__alter_users_table_for_new_columns.sql new file mode 100644 index 000000000..eee4c625d --- /dev/null +++ b/src/main/resources/db/migration/V3__alter_users_table_for_new_columns.sql @@ -0,0 +1,9 @@ +ALTER TABLE users +ADD first_name varchar(50), +ADD last_name varchar(50), +ADD profile_pic text; + + + + + diff --git a/src/main/resources/db/migration/V4__create_friends_table.sql b/src/main/resources/db/migration/V4__create_friends_table.sql new file mode 100644 index 000000000..7091351da --- /dev/null +++ b/src/main/resources/db/migration/V4__create_friends_table.sql @@ -0,0 +1,8 @@ +DROP TABLE IF EXISTS friends; + +CREATE TABLE friends ( + main_user_id bigserial, + friend_user_id bigserial, + PRIMARY KEY(main_user_id, friend_user_id) + +); \ No newline at end of file diff --git a/src/main/resources/db/migration/V5__alter_posts_table_for_new_columns.sql b/src/main/resources/db/migration/V5__alter_posts_table_for_new_columns.sql new file mode 100644 index 000000000..298c8b226 --- /dev/null +++ b/src/main/resources/db/migration/V5__alter_posts_table_for_new_columns.sql @@ -0,0 +1,5 @@ +ALTER TABLE posts +ADD user_id bigserial, +ADD CONSTRAINT post_user_id +FOREIGN KEY (user_id) REFERENCES users(id), +ADD time_posted timestamp; \ No newline at end of file diff --git a/src/main/resources/db/migration/V6__create_post_likes_table.sql b/src/main/resources/db/migration/V6__create_post_likes_table.sql new file mode 100644 index 000000000..0941de6c2 --- /dev/null +++ b/src/main/resources/db/migration/V6__create_post_likes_table.sql @@ -0,0 +1,12 @@ +DROP TABLE IF EXISTS postLikes; + +CREATE TABLE post_likes ( + user_id bigserial NOT NULL, + post_id bigserial NOT NULL, + created_at TIMESTAMP DEFAULT NOW(), + + PRIMARY KEY (user_id, post_id), + + FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE, + FOREIGN KEY (post_id) REFERENCES posts(id) ON DELETE CASCADE +); diff --git a/src/main/resources/db/migration/V7__alter_friends_add_foreign_keys.sql b/src/main/resources/db/migration/V7__alter_friends_add_foreign_keys.sql new file mode 100644 index 000000000..34bd81b9b --- /dev/null +++ b/src/main/resources/db/migration/V7__alter_friends_add_foreign_keys.sql @@ -0,0 +1,14 @@ + + +DROP TABLE IF EXISTS friends; + +CREATE TABLE friends ( + main_user_id bigserial NOT NULL, + friend_user_id bigserial NOT NULL, + PRIMARY KEY (main_user_id, friend_user_id), + + FOREIGN KEY (main_user_id) REFERENCES users(id) ON DELETE CASCADE, + FOREIGN KEY (friend_user_id) REFERENCES users(id) ON DELETE CASCADE, + + CHECK (main_user_id <> friend_user_id) +); diff --git a/src/main/resources/db/migration/V8__create_comments_table.sql b/src/main/resources/db/migration/V8__create_comments_table.sql new file mode 100644 index 000000000..cd633704a --- /dev/null +++ b/src/main/resources/db/migration/V8__create_comments_table.sql @@ -0,0 +1,11 @@ +DROP TABLE IF EXISTS comments; +CREATE TABLE comments ( + id bigserial PRIMARY KEY, + post_id BIGINT NOT NULL, + user_id BIGINT NOT NULL, + content TEXT NOT NULL, + created_at TIMESTAMP DEFAULT NOW(), + + FOREIGN KEY (post_id) REFERENCES posts(id) ON DELETE CASCADE, + FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE +); \ No newline at end of file diff --git a/src/main/resources/db/migration/V9__create_comment_likes_table.sql b/src/main/resources/db/migration/V9__create_comment_likes_table.sql new file mode 100644 index 000000000..d2a79910e --- /dev/null +++ b/src/main/resources/db/migration/V9__create_comment_likes_table.sql @@ -0,0 +1,12 @@ + + +CREATE TABLE comment_likes ( + user_id BIGINT NOT NULL, + comment_id BIGINT NOT NULL, + created_at TIMESTAMP DEFAULT NOW(), + + PRIMARY KEY (user_id, comment_id), + + FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE, + FOREIGN KEY (comment_id) REFERENCES comments(id) ON DELETE CASCADE +); diff --git a/src/main/resources/static/images/other/noodlesFavicon.png b/src/main/resources/static/images/other/noodlesFavicon.png new file mode 100644 index 000000000..5e19d882f Binary files /dev/null and b/src/main/resources/static/images/other/noodlesFavicon.png differ diff --git a/src/main/resources/static/images/profile/default.jpg b/src/main/resources/static/images/profile/default.jpg new file mode 100644 index 000000000..4d1ac6d08 Binary files /dev/null and b/src/main/resources/static/images/profile/default.jpg differ diff --git a/src/main/resources/static/main.css b/src/main/resources/static/main.css index d0260873c..5564d6355 100644 --- a/src/main/resources/static/main.css +++ b/src/main/resources/static/main.css @@ -8,3 +8,13 @@ text-align: left; margin-bottom: 0.5rem;; } + +.navbar-brand { +} + +bg-light { +} + +.navbar { + +} \ No newline at end of file diff --git a/src/main/resources/templates/friends/friends.html b/src/main/resources/templates/friends/friends.html new file mode 100644 index 000000000..eafb0368f --- /dev/null +++ b/src/main/resources/templates/friends/friends.html @@ -0,0 +1,76 @@ + + + + + Friends + + + + + + + + + + + + + + + + +

Friend Requests

+
    +
  • + +
    + + +
    +
  • +
+ +

Your Friends

+
    +
  • +
+ + + + + diff --git a/src/main/resources/templates/genericErrorPage.html b/src/main/resources/templates/genericErrorPage.html new file mode 100644 index 000000000..839f803a1 --- /dev/null +++ b/src/main/resources/templates/genericErrorPage.html @@ -0,0 +1,64 @@ + + + + + + Acebook + + + + + + + + + + + + + + +
+

Nope, that didn't work. +

+
+ + + + + diff --git a/src/main/resources/templates/landing.html b/src/main/resources/templates/landing.html new file mode 100644 index 000000000..a928a0b95 --- /dev/null +++ b/src/main/resources/templates/landing.html @@ -0,0 +1,65 @@ + + + + + Acebook + + + + + + + + + + + + + + + + +

Welcome to Acebook

+ +Login / Sign Up + + + + + + + diff --git a/src/main/resources/templates/notifications/index.html b/src/main/resources/templates/notifications/index.html new file mode 100644 index 000000000..38fa1abb8 --- /dev/null +++ b/src/main/resources/templates/notifications/index.html @@ -0,0 +1,79 @@ + + + + + + + + + Acebook + + + + + + + + + + + + + + + + +

Notifications

+ + + + + + + + + + + + +
+ + + + diff --git a/src/main/resources/templates/posts/index.html b/src/main/resources/templates/posts/index.html index b5ef169f1..e9b0ee7d1 100644 --- a/src/main/resources/templates/posts/index.html +++ b/src/main/resources/templates/posts/index.html @@ -1,27 +1,87 @@ - - + + + Acebook - + + + + + + + + + + + + + + + + -

Posts

+ + +

Posts

- Signed in as + Signed in as + User Profile Image
-
-

Content:

-

+ + + +
    + User Profile Image + :
  • + Post image
- - + + diff --git a/src/main/resources/templates/posts/post.html b/src/main/resources/templates/posts/post.html new file mode 100644 index 000000000..6502b5895 --- /dev/null +++ b/src/main/resources/templates/posts/post.html @@ -0,0 +1,69 @@ + + + + + + + Acebook + + + + + + + + + + + + + + + + +
+

Here is post ! +

+ User Profile Image + : + +
+ + + + diff --git a/src/main/resources/templates/users/settings.html b/src/main/resources/templates/users/settings.html new file mode 100644 index 000000000..04d60869d --- /dev/null +++ b/src/main/resources/templates/users/settings.html @@ -0,0 +1,71 @@ + + + + + + Settings + + + + + + + + + + + + +

+

+User Profile Image + +
+ +
+ +
+ +
+ + +
+ + + + + \ No newline at end of file diff --git a/src/test/.DS_Store b/src/test/.DS_Store new file mode 100644 index 000000000..2b4ee20c6 Binary files /dev/null and b/src/test/.DS_Store differ diff --git a/src/test/java/.DS_Store b/src/test/java/.DS_Store new file mode 100644 index 000000000..de3a99e1e Binary files /dev/null and b/src/test/java/.DS_Store differ diff --git a/src/test/java/com/.DS_Store b/src/test/java/com/.DS_Store new file mode 100644 index 000000000..4ed73db6d Binary files /dev/null and b/src/test/java/com/.DS_Store differ diff --git a/src/test/java/com/makersacademy/.DS_Store b/src/test/java/com/makersacademy/.DS_Store new file mode 100644 index 000000000..6700e0985 Binary files /dev/null and b/src/test/java/com/makersacademy/.DS_Store differ diff --git a/src/test/java/com/makersacademy/acebook/.DS_Store b/src/test/java/com/makersacademy/acebook/.DS_Store new file mode 100644 index 000000000..cb520e21b Binary files /dev/null and b/src/test/java/com/makersacademy/acebook/.DS_Store differ diff --git a/src/test/java/com/makersacademy/acebook/feature/FriendsListTest.java b/src/test/java/com/makersacademy/acebook/feature/FriendsListTest.java new file mode 100644 index 000000000..b5631f1c4 --- /dev/null +++ b/src/test/java/com/makersacademy/acebook/feature/FriendsListTest.java @@ -0,0 +1,46 @@ +package com.makersacademy.acebook.feature; + +import com.github.javafaker.Faker; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.openqa.selenium.By; +import org.openqa.selenium.WebDriver; +import org.openqa.selenium.chrome.ChromeDriver; + +import static org.hamcrest.CoreMatchers.containsString; +import static org.hamcrest.MatcherAssert.assertThat; + + +public class FriendsListTest { + WebDriver driver; + Faker faker; + + @Before + public void setup() { + System.setProperty("webdriver.chrome.driver", "/usr/local/bin/chromedriver"); + driver = new ChromeDriver(); + faker = new Faker(); + } + + @After + public void tearDown() { + driver.close(); + } + + @Test + public void FriendsControllerNavigatesToFriendsList() { + String email = faker.name().username() + "@email.com"; + + driver.get("http://localhost:8080/"); + driver.findElement(By.linkText("Sign up")).click(); + driver.findElement(By.name("email")).sendKeys(email); + driver.findElement(By.name("password")).sendKeys("P@55qw0rd"); + driver.findElement(By.name("action")).click(); + driver.findElement(By.name("action")).click(); + + driver.get("http://localhost:8080/friends"); + String actualTitle = driver.getTitle(); + assertThat(actualTitle, containsString("Friends")); + } +} diff --git a/src/test/java/com/makersacademy/acebook/feature/SettingsTest.java b/src/test/java/com/makersacademy/acebook/feature/SettingsTest.java new file mode 100644 index 000000000..6c45f45c9 --- /dev/null +++ b/src/test/java/com/makersacademy/acebook/feature/SettingsTest.java @@ -0,0 +1,151 @@ +package com.makersacademy.acebook.feature; + +import com.github.javafaker.Faker; +import org.junit.After; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.junit.jupiter.api.Assertions; +import org.openqa.selenium.By; +import org.openqa.selenium.WebDriver; +import org.openqa.selenium.WebElement; +import org.openqa.selenium.chrome.ChromeDriver; +import org.openqa.selenium.support.ui.ExpectedConditions; +import org.openqa.selenium.support.ui.WebDriverWait; + +import java.nio.file.Path; +import java.nio.file.Paths; +import java.time.Duration; +import java.util.List; + +import static org.hamcrest.CoreMatchers.containsString; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.junit.jupiter.api.Assertions.assertEquals; + +public class SettingsTest { + + WebDriver driver; + Faker faker; + + @Before + public void setup() { + System.setProperty("webdriver.chrome.driver", "/usr/local/bin/chromedriver"); + driver = new ChromeDriver(); + faker = new Faker(); + } + + @After + public void tearDown() { + driver.close(); + } + + @Test + public void successfulGetRequestOnSettingsPage() { + String email = faker.name().username() + "@email.com"; + String givenName = faker.name().firstName(); + String familyName = faker.name().lastName(); + + driver.manage().timeouts().implicitlyWait(Duration.ofSeconds(5)); + driver.get("http://localhost:8080/"); + driver.findElement(By.linkText("Login / Sign Up")).click(); + driver.findElement(By.xpath("(//a[text()='Sign Up'])[1]")).click(); + driver.findElement(By.name("email")).sendKeys(email); + driver.findElement(By.name("given_name")).sendKeys(givenName); + driver.findElement(By.name("family_name")).sendKeys(familyName); + WebDriverWait wait = new WebDriverWait(driver, Duration.ofSeconds(10)); + WebElement passwordField = wait.until( + ExpectedConditions.elementToBeClickable(By.id("1-password")) + ); + passwordField.sendKeys("Password123"); + driver.findElement(By.className("auth0-label-submit")).click(); + driver.findElement(By.name("action")).click(); + WebElement settingsLink = wait.until( + ExpectedConditions.elementToBeClickable(By.linkText("| Settings |")) + ); + settingsLink.click(); + String actualTitle = driver.getTitle(); + assertThat(actualTitle, containsString("Settings")); + + } + + @Test + public void successfulImageUploadToSettingsPage() { + String email = faker.name().username() + "@email.com"; + String givenName = faker.name().firstName(); + String familyName = faker.name().lastName(); + + driver.manage().timeouts().implicitlyWait(Duration.ofSeconds(5)); + driver.get("http://localhost:8080/"); + driver.findElement(By.linkText("Login / Sign Up")).click(); + driver.findElement(By.xpath("(//a[text()='Sign Up'])[1]")).click(); + driver.findElement(By.name("email")).sendKeys(email); + driver.findElement(By.name("given_name")).sendKeys(givenName); + driver.findElement(By.name("family_name")).sendKeys(familyName); + WebDriverWait wait = new WebDriverWait(driver, Duration.ofSeconds(10)); + WebElement passwordField = wait.until( + ExpectedConditions.elementToBeClickable(By.id("1-password")) + ); + passwordField.sendKeys("Password123"); + driver.findElement(By.className("auth0-label-submit")).click(); + driver.findElement(By.name("action")).click(); + + driver.findElement(By.linkText("| Settings |")).click(); + + WebDriverWait waitForImageField = new WebDriverWait(driver, Duration.ofSeconds(10)); + waitForImageField.until(ExpectedConditions.presenceOfElementLocated(By.xpath("//img[@alt='User Profile Image']"))); + + Path imagePath = Paths.get("src/test/resources/Test_Profile.png"); + WebElement fileInput = driver.findElement(By.name("file")); + fileInput.sendKeys(imagePath.toAbsolutePath().toString()); + + driver.findElement(By.id("submit")).click(); + + WebDriverWait waitForImage = new WebDriverWait(driver, Duration.ofSeconds(10)); + waitForImage.until(ExpectedConditions.presenceOfElementLocated(By.xpath("//img[@alt='User Profile Image']"))); + + WebElement fileName = driver.findElement(By.xpath("//img[@alt='User Profile Image']")); + String src = fileName.getAttribute("src"); + + Assertions.assertFalse(src.contains("default.jpg")); + } + + @Test + public void successfulNameChangeToSettingsPage() { + String email = faker.name().username() + "@email.com"; + String givenName = faker.name().firstName(); + String familyName = faker.name().lastName(); + + driver.manage().timeouts().implicitlyWait(Duration.ofSeconds(5)); + driver.get("http://localhost:8080/"); + driver.findElement(By.linkText("Login / Sign Up")).click(); + driver.findElement(By.xpath("(//a[text()='Sign Up'])[1]")).click(); + driver.findElement(By.name("email")).sendKeys(email); + driver.findElement(By.name("given_name")).sendKeys(givenName); + driver.findElement(By.name("family_name")).sendKeys(familyName); + WebDriverWait wait = new WebDriverWait(driver, Duration.ofSeconds(10)); + WebElement passwordField = wait.until( + ExpectedConditions.elementToBeClickable(By.id("1-password")) + ); + passwordField.sendKeys("Password123"); + driver.findElement(By.className("auth0-label-submit")).click(); + driver.findElement(By.name("action")).click(); + + driver.findElement(By.linkText("| Settings |")).click(); + + WebDriverWait waitForImageField = new WebDriverWait(driver, Duration.ofSeconds(10)); + waitForImageField.until(ExpectedConditions.presenceOfElementLocated(By.xpath("//img[@alt='User Profile Image']"))); + + driver.findElement(By.id("last_name")).clear(); + driver.findElement(By.id("first_name")).clear(); + driver.findElement(By.id("first_name")).sendKeys("Harry"); + driver.findElement(By.id("last_name")).sendKeys("Parkes"); + driver.findElement(By.xpath("//input[@type='submit']")).click(); + + waitForImageField.until(ExpectedConditions.presenceOfElementLocated(By.xpath("//img[@alt='User Profile Image']"))); + List headers = driver.findElements(By.tagName("h2")); + assertEquals(headers.get(0).getText(), "First name: Harry"); + assertEquals(headers.get(1).getText(), "Surname: Parkes"); + + } + + } diff --git a/src/test/java/com/makersacademy/acebook/feature/SignUpTest.java b/src/test/java/com/makersacademy/acebook/feature/SignUpTest.java index dcb1416bb..16ea1f492 100644 --- a/src/test/java/com/makersacademy/acebook/feature/SignUpTest.java +++ b/src/test/java/com/makersacademy/acebook/feature/SignUpTest.java @@ -7,7 +7,15 @@ import org.junit.Test; import org.openqa.selenium.By; import org.openqa.selenium.WebDriver; +import org.openqa.selenium.WebElement; import org.openqa.selenium.chrome.ChromeDriver; +import org.openqa.selenium.support.ui.ExpectedConditions; +import org.openqa.selenium.support.ui.WebDriverWait; + +import java.time.Duration; + +import static org.hamcrest.CoreMatchers.containsString; +import static org.hamcrest.MatcherAssert.assertThat; public class SignUpTest { @@ -27,16 +35,27 @@ public void tearDown() { } @Test - public void successfulSignUpAlsoLogsInUser() { + public void userCanSignUp() { String email = faker.name().username() + "@email.com"; + String givenName = faker.name().firstName(); + String familyName = faker.name().lastName(); + driver.manage().timeouts().implicitlyWait(Duration.ofSeconds(5)); driver.get("http://localhost:8080/"); - driver.findElement(By.linkText("Sign up")).click(); + driver.findElement(By.linkText("Login / Sign Up")).click(); + driver.findElement(By.xpath("(//a[text()='Sign Up'])[1]")).click(); driver.findElement(By.name("email")).sendKeys(email); - driver.findElement(By.name("password")).sendKeys("P@55qw0rd"); + driver.findElement(By.name("given_name")).sendKeys(givenName); + driver.findElement(By.name("family_name")).sendKeys(familyName); + WebDriverWait wait = new WebDriverWait(driver, Duration.ofSeconds(10)); + WebElement passwordField = wait.until( + ExpectedConditions.elementToBeClickable(By.id("1-password")) + ); + passwordField.sendKeys("Password123"); + driver.findElement(By.className("auth0-label-submit")).click(); driver.findElement(By.name("action")).click(); - driver.findElement(By.name("action")).click(); - String greetingText = driver.findElement(By.id("greeting")).getText(); - Assert.assertEquals("Signed in as " + email, greetingText); + String actualTitle = driver.getTitle(); + assertThat(actualTitle, containsString("Acebook")); + } } diff --git a/src/test/java/com/makersacademy/acebook/model/FriendRequestTest.java b/src/test/java/com/makersacademy/acebook/model/FriendRequestTest.java new file mode 100644 index 000000000..0534f28bd --- /dev/null +++ b/src/test/java/com/makersacademy/acebook/model/FriendRequestTest.java @@ -0,0 +1,26 @@ +package com.makersacademy.acebook.model; + +import static net.bytebuddy.matcher.ElementMatchers.is; +import static org.hamcrest.CoreMatchers.containsString; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.notNullValue; + +import com.makersacademy.acebook.repository.FriendRequestRepository; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; + +import java.sql.Timestamp; + + +public class FriendRequestTest { + + @Autowired + FriendRequestRepository repository; + + @Test + public void friendRequestHasContent() { + FriendRequest request = new FriendRequest(null, 1L, 2L, "unapproved", new Timestamp(System.currentTimeMillis()), null); + repository.save(request); + assertThat(request.getStatus(), containsString("unapproved")); + } +} diff --git a/src/test/java/com/makersacademy/acebook/model/PostTest.java b/src/test/java/com/makersacademy/acebook/model/PostTest.java index 926d9b1bf..9f0e94e79 100644 --- a/src/test/java/com/makersacademy/acebook/model/PostTest.java +++ b/src/test/java/com/makersacademy/acebook/model/PostTest.java @@ -5,9 +5,15 @@ import org.junit.jupiter.api.Test; +import java.sql.Timestamp; +import java.time.Instant; + public class PostTest { - private Post post = new Post("hello"); + Instant instant = Instant.now(); + Timestamp now = Timestamp.from(instant); + + private Post post = new Post(null, "hello", 1L, now, null, null); @Test public void postHasContent() { diff --git a/src/test/java/com/makersacademy/acebook/model/UserTest.java b/src/test/java/com/makersacademy/acebook/model/UserTest.java new file mode 100644 index 000000000..6fbb0da6b --- /dev/null +++ b/src/test/java/com/makersacademy/acebook/model/UserTest.java @@ -0,0 +1,27 @@ +package com.makersacademy.acebook.model; + +import org.junit.jupiter.api.Test; + +import static org.hamcrest.CoreMatchers.containsString; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.junit.jupiter.api.Assertions.assertEquals; + +public class UserTest { + + private User user = new User("username", "Sasha", "Parkes", "static/images/profile/default.jpeg"); + + @Test + public void userHasContent() {assertThat(user.getUsername(), containsString("username"));} + + @Test + public void userIsEnabled() {assertEquals(true, user.isEnabled());} + + @Test + public void userHasFirstName() {assertThat(user.getFirst_name(), containsString("Sasha"));} + + @Test + public void userHasLastName() {assertThat(user.getLast_name(), containsString("Parkes"));} + +// @Test +// public void userHasProfilePic() {assertThat(user.getProfile_pic(), containsString("image/profpic"));} +} diff --git a/src/test/resources/Test_Profile.png b/src/test/resources/Test_Profile.png new file mode 100644 index 000000000..a934b14ec Binary files /dev/null and b/src/test/resources/Test_Profile.png differ