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..1c4cc0d3a 100644 --- a/.gitignore +++ b/.gitignore @@ -9,3 +9,4 @@ dependency-reduced-pom.xml .factorypath .project .settings/ +application.yml 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..97f61e1b2 --- /dev/null +++ b/src/main/java/com/makersacademy/acebook/config/WebConfig.java @@ -0,0 +1,15 @@ +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; + +@Configuration +public class WebConfig implements WebMvcConfigurer { + + @Override + public void addResourceHandlers(ResourceHandlerRegistry registry) { + registry.addResourceHandler("/images/user_profile/**") + .addResourceLocations("file:uploads/user_profile/"); + } +} 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..d6c8fffa2 --- /dev/null +++ b/src/main/java/com/makersacademy/acebook/controller/FriendsController.java @@ -0,0 +1,86 @@ +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.GetMapping; +import org.springframework.web.bind.annotation.ModelAttribute; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.servlet.ModelAndView; +import org.springframework.web.servlet.view.RedirectView; + +import java.sql.Timestamp; +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("/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..2b1e6cdf6 --- /dev/null +++ b/src/main/java/com/makersacademy/acebook/controller/NotificationsController.java @@ -0,0 +1,50 @@ +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.http.ResponseEntity; +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; + + @GetMapping("/notifications") + public String index(Model model) { + List notifications = new ArrayList<>(); + notificationRepository.findAll().forEach(notifications::add); + Collections.reverse(notifications); + Map senderNames = new HashMap<>(); + for (Notification notification : notifications) { + Optional sender = userRepository.findById(notification.getSending_user_id()); + sender.ifPresent(user -> senderNames.put(notification.getId(), user.getUsername())); //needs updated with proper User getFirstName + } + model.addAttribute("notifications", notifications); + model.addAttribute("senderNames", senderNames); + return "notifications/index"; + } +// +// @PostMapping("/notifications") +// public RedirectView markAsRead(@ModelAttribute Notification notification) { +// Optional readNotification = notificationRepository.findById(1L); +// if (readNotification.isPresent()) { +// Notification notif = readNotification.get(); +// notif.set_read(true); +// notificationRepository.save(notif); +// } +//// notification.set_read(true); +// return new RedirectView("/notifications"); +// } +} diff --git a/src/main/java/com/makersacademy/acebook/controller/PostsController.java b/src/main/java/com/makersacademy/acebook/controller/PostsController.java index 57a7e5f4d..a522d582a 100644 --- a/src/main/java/com/makersacademy/acebook/controller/PostsController.java +++ b/src/main/java/com/makersacademy/acebook/controller/PostsController.java @@ -1,32 +1,56 @@ 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 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.ui.Model; import org.springframework.web.bind.annotation.*; +import org.springframework.web.servlet.ModelAndView; import org.springframework.web.servlet.view.RedirectView; import java.util.List; +import java.util.Optional; @Controller public class PostsController { @Autowired - PostRepository repository; + PostRepository postRepository; + + @Autowired + UserRepository userRepository; @GetMapping("/posts") public String index(Model model) { - Iterable posts = repository.findAll(); + Iterable posts = postRepository.findAll(); model.addAttribute("posts", posts); model.addAttribute("post", new Post()); + + DefaultOidcUser principal = (DefaultOidcUser) SecurityContextHolder + .getContext() + .getAuthentication() + .getPrincipal(); + + String username = (String) principal.getAttributes().get("email"); + User user = userRepository.findUserByUsername(username).get(); + model.addAttribute("user", user); + return "posts/index"; } @PostMapping("/posts") - public RedirectView create(@ModelAttribute Post post) { - repository.save(post); + public RedirectView create(@ModelAttribute Post post, @AuthenticationPrincipal(expression = "attributes['email']") String email) { + Optional user = userRepository.findUserByUsername(email); + if (user.isPresent()) { + post.setUser_id(user.get().getId()); + postRepository.save(post); + } return new RedirectView("/posts"); } } diff --git a/src/main/java/com/makersacademy/acebook/controller/UsersController.java b/src/main/java/com/makersacademy/acebook/controller/UsersController.java index a7c9db1d8..aeda672fa 100644 --- a/src/main/java/com/makersacademy/acebook/controller/UsersController.java +++ b/src/main/java/com/makersacademy/acebook/controller/UsersController.java @@ -3,16 +3,31 @@ import com.makersacademy.acebook.model.User; import com.makersacademy.acebook.repository.UserRepository; 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; + @Value("${file.upload-dir}") + private String uploadDir; + @GetMapping("/users/after-login") public RedirectView afterLogin() { DefaultOidcUser principal = (DefaultOidcUser) SecurityContextHolder @@ -21,10 +36,90 @@ public RedirectView afterLogin() { .getPrincipal(); 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"; 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(){ + DefaultOidcUser principal = (DefaultOidcUser) SecurityContextHolder + .getContext() + .getAuthentication() + .getPrincipal(); + + String username = (String) principal.getAttributes().get("email"); + User userByEmail = userRepository.findUserByUsername(username).get(); +// Optional user = userRepository.findById(userByEmail.getId()); + ModelAndView settings = new ModelAndView("/users/settings"); + settings.addObject("user", userByEmail); + return settings; + } + + @PostMapping("/settings") + public RedirectView update(@ModelAttribute User userFromForm, @RequestParam("file") MultipartFile file) { + DefaultOidcUser principal = (DefaultOidcUser) SecurityContextHolder + .getContext() + .getAuthentication() + .getPrincipal(); + + String email = (String) principal.getAttributes().get("email"); + + User userInDb = userRepository.findUserByUsername(email) + .orElseThrow(() -> new IllegalArgumentException("User not found")); + + try { + if (!file.isEmpty()) { + String fileName = saveImage(file, userInDb.getId().toString()); // Save to disk + userInDb.setProfile_pic("/images/user_profile/" + fileName); // Or adjust path + } + } catch (IOException e) { + e.printStackTrace(); // optionally log it + return new RedirectView("/settings?error=file"); + } + + userInDb.setFirst_name(userFromForm.getFirst_name()); + userInDb.setLast_name(userFromForm.getLast_name()); + + userRepository.save(userInDb); + + return new RedirectView("/settings"); + } + + + private String saveImage(MultipartFile file, String userId) throws IOException { + Path uploadPath = Paths.get(uploadDir); + if (!Files.exists(uploadPath)) { + Files.createDirectories(uploadPath); + } + + String contentType = file.getContentType(); + if (!contentType.equals("image/jpeg") && !contentType.equals("image/png")) { + throw new IllegalArgumentException("Only JPEG or PNG images are allowed"); + } + + String fileName = file.getOriginalFilename(); + String extension = getFileExtension(fileName); + String newFileName = userId + extension; + Path filePath = uploadPath.resolve(newFileName); + Files.copy(file.getInputStream(), filePath, StandardCopyOption.REPLACE_EXISTING); + + return newFileName; + } + + public static String getFileExtension(String fileName) { + Path path = Paths.get(fileName); + return Optional.ofNullable(path.getFileName()) + .map(Path::toString) + .filter(f -> f.contains(".")) + .map(f -> f.substring(f.lastIndexOf("."))) + .orElse(""); + } + + } 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..ddc3d75b7 --- /dev/null +++ b/src/main/java/com/makersacademy/acebook/model/Friend.java @@ -0,0 +1,31 @@ +package com.makersacademy.acebook.model; + +import jakarta.persistence.*; +import lombok.*; +import java.io.Serializable; + +// 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; +} \ 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..513798496 --- /dev/null +++ b/src/main/java/com/makersacademy/acebook/model/Notification.java @@ -0,0 +1,28 @@ +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; + private Long receiving_user_id; + private Long sending_user_id; + private String type; + private Long post_id; + private boolean is_read; + private Timestamp created_at; + +} diff --git a/src/main/java/com/makersacademy/acebook/model/Post.java b/src/main/java/com/makersacademy/acebook/model/Post.java index 33492c6b1..2afb66edf 100644 --- a/src/main/java/com/makersacademy/acebook/model/Post.java +++ b/src/main/java/com/makersacademy/acebook/model/Post.java @@ -1,22 +1,24 @@ 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; + private Long user_id; // current user is populated in PostController + @CreationTimestamp + private Timestamp time_posted; - 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..eb34dbcd3 100644 --- a/src/main/java/com/makersacademy/acebook/model/User.java +++ b/src/main/java/com/makersacademy/acebook/model/User.java @@ -14,18 +14,30 @@ 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; } } 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..a7098737c --- /dev/null +++ b/src/main/java/com/makersacademy/acebook/repository/FriendRequestRepository.java @@ -0,0 +1,14 @@ +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); +} 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..7ff63611e --- /dev/null +++ b/src/main/java/com/makersacademy/acebook/repository/NotificationRepository.java @@ -0,0 +1,9 @@ +package com.makersacademy.acebook.repository; + +import com.makersacademy.acebook.model.Notification; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.repository.CrudRepository; + +public interface NotificationRepository extends CrudRepository { + +} 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..d66121795 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -6,3 +6,7 @@ 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=uploads/user_profile +spring.servlet.multipart.enabled=true +spring.servlet.multipart.max-file-size=5MB +spring.servlet.multipart.max-request-size=5MB 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/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/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/templates/friends/friends.html b/src/main/resources/templates/friends/friends.html new file mode 100644 index 000000000..aa7708886 --- /dev/null +++ b/src/main/resources/templates/friends/friends.html @@ -0,0 +1,22 @@ + + + + + Friends + + + + +

Friend Requests

+
    +
  • +
+ +

Your Friends

+
    +
  • +
+ + + \ No newline at end of file diff --git a/src/main/resources/templates/landing.html b/src/main/resources/templates/landing.html new file mode 100644 index 000000000..344a21dfa --- /dev/null +++ b/src/main/resources/templates/landing.html @@ -0,0 +1,16 @@ + + + + + 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..d9311cc56 --- /dev/null +++ b/src/main/resources/templates/notifications/index.html @@ -0,0 +1,29 @@ + + + + + + Acebook + + + + +

Notifications

+ +
+ Signed in as +
+ + + + + + + + +
+ + diff --git a/src/main/resources/templates/posts/index.html b/src/main/resources/templates/posts/index.html index b5ef169f1..832973cb3 100644 --- a/src/main/resources/templates/posts/index.html +++ b/src/main/resources/templates/posts/index.html @@ -1,7 +1,9 @@ - - + + + Acebook @@ -11,7 +13,8 @@

Posts

- Signed in as + Signed in as + 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..e552a94ff --- /dev/null +++ b/src/main/resources/templates/users/settings.html @@ -0,0 +1,26 @@ + + + + + + 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/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..5cf8d8282 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); @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/uploads/user_profile/7.png b/uploads/user_profile/7.png new file mode 100644 index 000000000..acac3e70e Binary files /dev/null and b/uploads/user_profile/7.png differ