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
+
+
+
+
\ 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