diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 000000000..dd8fcfc10 Binary files /dev/null and b/.DS_Store differ diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 000000000..0c60fe0e8 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,11 @@ +# Exclude the target directory, which contains the build output +target/ + +# Exclude git configuration and repository data +.git +.gitignore + +# Exclude local configuration files (IDE settings, etc.) +.idea +*.iml +*.log diff --git a/.gitignore b/.gitignore index c64b53754..bf074f30b 100644 --- a/.gitignore +++ b/.gitignore @@ -9,3 +9,6 @@ dependency-reduced-pom.xml .factorypath .project .settings/ +.env + +.DS_STORE \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 000000000..93f28aa77 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,11 @@ +# Use Maven to build the JAR file +FROM maven:3.8.1-openjdk-17 AS build +WORKDIR /app +COPY . . +RUN mvn clean package + +# Use the JAR file from the build stage +FROM openjdk:17 +COPY --from=build /app/target/acebook-template-1.0-SNAPSHOT.jar app.jar +ENTRYPOINT ["java", "-jar", "/app.jar"] +EXPOSE 8080 \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 000000000..7de45b4d6 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,29 @@ +version: '3.8' + +services: + database: + image: postgres:latest + environment: + POSTGRES_DB: acebook_springboot_development + POSTGRES_USER: postgres + POSTGRES_HOST_AUTH_METHOD: trust + ports: + - "5432:5432" + volumes: + - postgres_data:/var/lib/postgresql/data + + app: + build: . + ports: + - "8080:8080" + depends_on: + - database + environment: + SPRING_DATASOURCE_URL: jdbc:postgresql://database:5432/acebook_springboot_development + SPRING_DATASOURCE_USERNAME: + SPRING_DATASOURCE_PASSWORD: + SPRING_JPA_HIBERNATE_DDL_AUTO: update + SPRING_JPA_DATABASE_PLATFORM: org.hibernate.dialect.PostgreSQL9Dialect + +volumes: + postgres_data: \ No newline at end of file diff --git a/pom.xml b/pom.xml index 261744b50..1842fe7e2 100644 --- a/pom.xml +++ b/pom.xml @@ -19,6 +19,26 @@ org.springframework.boot spring-boot-maven-plugin + + org.apache.maven.plugins + maven-surefire-plugin + 3.0.0-M5 + + + **/*Test.java + **/*Tests.java + **/*TestCase.java + + + + + org.apache.maven.plugins + maven-compiler-plugin + + 11 + 11 + + @@ -30,8 +50,8 @@ - jitpack.io - https://jitpack.io + central + https://repo.maven.apache.org/maven2 @@ -40,14 +60,21 @@ org.springframework.boot spring-boot-starter - - org.springframework.boot - spring-boot-starter-security - - + + org.springframework.boot + spring-boot-starter-security + + org.springframework.boot spring-boot-starter-thymeleaf + + + org.apache.httpcomponents + httpclient + 4.5.13 + + org.thymeleaf.extras thymeleaf-extras-springsecurity5 @@ -56,6 +83,17 @@ org.springframework.boot spring-boot-starter-data-jpa + + software.amazon.awssdk + s3 + 2.20.25 + + + software.amazon.awssdk + netty-nio-client + 2.20.25 + + org.springframework.boot spring-boot-starter-data-rest @@ -67,7 +105,7 @@ org.projectlombok lombok - 1.18.30 + 1.18.30 provided @@ -99,12 +137,17 @@ org.postgresql postgresql + 42.2.5 org.flywaydb flyway-core + + org.junit.jupiter + junit-jupiter + RELEASE + test + - - - + \ No newline at end of file diff --git a/render.yml b/render.yml new file mode 100644 index 000000000..27d80f210 --- /dev/null +++ b/render.yml @@ -0,0 +1,30 @@ +services: + - type: web + name: acebook-api + env: docker + plan: free + dockerfilePath: Dockerfile + envVars: + - key: DATABASE_URL + fromService: + name: acebook-db + type: database + property: connectionString + - key: DB_HOST + value: dpg-cpm5nc2ju9rs738kt4ng-a + - key: DB_NAME + value: acebook_springboot_development_fpol + - key: DB_USER + value: acebook_springboot_development_fpol_user + - key: DB_PASS + value: wiXc8BpegUV52sdrqykindpYNTIFiNDH + buildCommand: "./mvnw clean package" # Ensure this matches your build command + startCommand: "java -jar app.jar" + + - type: postgresql + name: acebook-db + plan: free + properties: + databaseName: acebook_springboot_development_fpol + user: acebook_springboot_development_fpol_user + password: wiXc8BpegUV52sdrqykindpYNTIFiNDH \ No newline at end of file diff --git a/src/main/java/com/makersacademy/acebook/SecurityConfiguration.java b/src/main/java/com/makersacademy/acebook/SecurityConfiguration.java index a6829646e..a9fe4e20e 100644 --- a/src/main/java/com/makersacademy/acebook/SecurityConfiguration.java +++ b/src/main/java/com/makersacademy/acebook/SecurityConfiguration.java @@ -26,13 +26,25 @@ protected void configure(AuthenticationManagerBuilder auth) throws Exception { @Override protected void configure(HttpSecurity http) throws Exception { http.authorizeRequests() - .antMatchers("/posts").hasRole("USER") - .antMatchers("/users").permitAll() - .and().formLogin(); + .antMatchers("/posts", "/profile", "/profile/**").hasRole("USER") + .antMatchers("/users", "/profile-pictures/**", "/login", "/users/new", "/css/**", "/js/**", "/images/**").permitAll() + .anyRequest().authenticated() + .and() + .formLogin() + .loginPage("/login") + .defaultSuccessUrl("/posts", true) + .permitAll() + .and() + .logout() + .logoutUrl("/logout") + .logoutSuccessUrl("/login?logout") + .permitAll() + .and() + .csrf(); } @Bean public PasswordEncoder getPasswordEncoder() { return NoOpPasswordEncoder.getInstance(); } -} +} \ No newline at end of file diff --git a/src/main/java/com/makersacademy/acebook/auth/AuthService.java b/src/main/java/com/makersacademy/acebook/auth/AuthService.java new file mode 100644 index 000000000..1625eced3 --- /dev/null +++ b/src/main/java/com/makersacademy/acebook/auth/AuthService.java @@ -0,0 +1,14 @@ +package com.makersacademy.acebook.auth; + +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.stereotype.Service; + +@Service +public class AuthService { + + public Boolean isLoggedIn() { + String username = SecurityContextHolder.getContext().getAuthentication().getName(); + return !username.equals("anonymousUser"); + } + +} diff --git a/src/main/java/com/makersacademy/acebook/config/S3Config.java b/src/main/java/com/makersacademy/acebook/config/S3Config.java new file mode 100644 index 000000000..ccb6158dd --- /dev/null +++ b/src/main/java/com/makersacademy/acebook/config/S3Config.java @@ -0,0 +1,19 @@ +package com.makersacademy.acebook.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import software.amazon.awssdk.auth.credentials.EnvironmentVariableCredentialsProvider; +import software.amazon.awssdk.regions.Region; +import software.amazon.awssdk.services.s3.S3Client; + +@Configuration +public class S3Config { + + @Bean + public S3Client s3Client() { + return S3Client.builder() + .region(Region.US_EAST_1) + .credentialsProvider(EnvironmentVariableCredentialsProvider.create()) + .build(); + } +} \ No newline at end of file diff --git a/src/main/java/com/makersacademy/acebook/controller/AuthController.java b/src/main/java/com/makersacademy/acebook/controller/AuthController.java new file mode 100644 index 000000000..08da915e0 --- /dev/null +++ b/src/main/java/com/makersacademy/acebook/controller/AuthController.java @@ -0,0 +1,25 @@ +package com.makersacademy.acebook.controller; + +import com.makersacademy.acebook.auth.AuthService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.util.*; + +@RestController +@RequestMapping("/auth") +public class AuthController { + + @Autowired + private AuthService authService; + + @GetMapping("/userLoggedIn") + public Map userLoggedIn() { + Map response = new HashMap<>(); + response.put("loggedIn", authService.isLoggedIn()); + return response; + } +} + diff --git a/src/main/java/com/makersacademy/acebook/controller/LoginController.java b/src/main/java/com/makersacademy/acebook/controller/LoginController.java new file mode 100644 index 000000000..f018a0575 --- /dev/null +++ b/src/main/java/com/makersacademy/acebook/controller/LoginController.java @@ -0,0 +1,14 @@ +package com.makersacademy.acebook.controller; + +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.GetMapping; + +@Controller +public class LoginController { + + @GetMapping("/login") + public String login(){ + return "login"; + } + +} diff --git a/src/main/java/com/makersacademy/acebook/controller/PostsController.java b/src/main/java/com/makersacademy/acebook/controller/PostsController.java index 57a7e5f4d..4fb89ef55 100644 --- a/src/main/java/com/makersacademy/acebook/controller/PostsController.java +++ b/src/main/java/com/makersacademy/acebook/controller/PostsController.java @@ -1,32 +1,70 @@ package com.makersacademy.acebook.controller; +import com.makersacademy.acebook.model.Comment; import com.makersacademy.acebook.model.Post; -import com.makersacademy.acebook.repository.PostRepository; +import com.makersacademy.acebook.model.User; +import com.makersacademy.acebook.repository.UserRepository; +import com.makersacademy.acebook.service.PostService; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.core.Authentication; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; -import org.springframework.web.bind.annotation.*; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.multipart.MultipartFile; import org.springframework.web.servlet.view.RedirectView; +import org.springframework.web.bind.annotation.*; -import java.util.List; +import java.io.IOException; @Controller public class PostsController { @Autowired - PostRepository repository; + private UserRepository userRepository; + + @Autowired + private PostService postService; @GetMapping("/posts") public String index(Model model) { - Iterable posts = repository.findAll(); + Iterable posts = postService.getAllPostsFromNewestToOldest(); model.addAttribute("posts", posts); model.addAttribute("post", new Post()); + model.addAttribute("newComment", new Comment()); return "posts/index"; } @PostMapping("/posts") - public RedirectView create(@ModelAttribute Post post) { - repository.save(post); - return new RedirectView("/posts"); + public RedirectView create(Post post, @RequestParam("image") MultipartFile image, @RequestParam(required = false) String redirectUrl, Authentication authentication) { + String username = authentication.getName(); + User user = userRepository.findByUsername(username); + post.setUser(user); + + try { + postService.savePost(post, image); + } catch (IOException e) { + e.printStackTrace(); + return new RedirectView(redirectUrl != null ? redirectUrl + "?error" : "/posts?error"); + } + + return new RedirectView(redirectUrl != null ? redirectUrl : "/posts"); + } + @PostMapping("/posts/{postId}/comments") + public String addComment(@PathVariable Long postId, @RequestParam String content, Authentication authentication) { + String username = authentication.getName(); + User user = userRepository.findByUsername(username); + postService.addComment(postId, content, user); + return "redirect:/posts"; } -} + + @PostMapping("/posts/{postId}/toggleLike") + public String toggleLikePost(@PathVariable Long postId, Authentication authentication) { + String username = authentication.getName(); + User user = userRepository.findByUsername(username); + postService.toggleLike(postId, user); + return "redirect:/posts"; + } + +} \ No newline at end of file diff --git a/src/main/java/com/makersacademy/acebook/controller/UserProfileController.java b/src/main/java/com/makersacademy/acebook/controller/UserProfileController.java new file mode 100644 index 000000000..63967ff6c --- /dev/null +++ b/src/main/java/com/makersacademy/acebook/controller/UserProfileController.java @@ -0,0 +1,66 @@ +package com.makersacademy.acebook.controller; +import com.makersacademy.acebook.model.Post; + +import com.makersacademy.acebook.model.User; + +import com.makersacademy.acebook.service.PostService; +import com.makersacademy.acebook.service.UserService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.stereotype.Controller; +import org.springframework.ui.Model; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.multipart.MultipartFile; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.servlet.mvc.support.RedirectAttributes; +import org.springframework.web.servlet.view.RedirectView; + +import java.io.IOException; +import java.util.List; + +@Controller +@RequestMapping("/profile") +public class UserProfileController { + + @Autowired + private UserService userService; + @Autowired + private PostService postService; + @GetMapping + public String getProfilePage(Authentication authentication, Model model) { + String username = authentication.getName(); + User user = userService.getUserByUsername(username); + List posts = postService.getPostsByUserId(user.getId()); + model.addAttribute("user", user); + model.addAttribute("posts", posts); + model.addAttribute("newPost", new Post()); + return "profile"; + } + + @PostMapping("/upload") + public RedirectView uploadProfilePicture(@RequestParam("profilePicture") MultipartFile profilePicture, Authentication authentication) { + String username = authentication.getName(); + User user = userService.getUserByUsername(username); + + try { + userService.updateUserProfile(user, profilePicture); + } catch (IOException e) { + e.printStackTrace(); + return new RedirectView("/profile?error"); + } + + return new RedirectView("/profile"); + } + + @PostMapping("/updateBio") + public String updateBio(@RequestParam("bio") String bio, Authentication authentication, RedirectAttributes redirectAttributes){ + String username = authentication.getName(); + User user = userService.getUserByUsername(username); + userService.updateBio(user, bio); + redirectAttributes.addFlashAttribute("message", "Bio updated successfully!"); + return "redirect:/profile"; + } +} \ No newline at end of file diff --git a/src/main/java/com/makersacademy/acebook/controller/UsersController.java b/src/main/java/com/makersacademy/acebook/controller/UsersController.java index 3c46bf0a1..636c71186 100644 --- a/src/main/java/com/makersacademy/acebook/controller/UsersController.java +++ b/src/main/java/com/makersacademy/acebook/controller/UsersController.java @@ -4,22 +4,35 @@ import com.makersacademy.acebook.model.User; import com.makersacademy.acebook.repository.AuthoritiesRepository; import com.makersacademy.acebook.repository.UserRepository; +import com.makersacademy.acebook.service.S3Service; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; 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.RequestParam; +import org.springframework.web.multipart.MultipartFile; import org.springframework.web.servlet.view.RedirectView; +import java.io.IOException; + @Controller public class UsersController { @Autowired UserRepository userRepository; + @Autowired AuthoritiesRepository authoritiesRepository; + @Autowired + PasswordEncoder passwordEncoder; + + @Autowired + S3Service s3Service; + @GetMapping("/users/new") public String signup(Model model) { model.addAttribute("user", new User()); @@ -27,10 +40,21 @@ public String signup(Model model) { } @PostMapping("/users") - public RedirectView signup(@ModelAttribute User user) { + public RedirectView signup(@ModelAttribute User user, @RequestParam("profilePicture") MultipartFile profilePicture) { + if (!profilePicture.isEmpty()) { + try { + String profilePictureUrl = s3Service.saveProfilePicture(profilePicture); + user.setProfilePictureUrl(profilePictureUrl); + } catch (IOException e) { + e.printStackTrace(); + return new RedirectView("/users/new?error"); + } + } + + user.setPassword(passwordEncoder.encode(user.getPassword())); userRepository.save(user); Authority authority = new Authority(user.getUsername(), "ROLE_USER"); authoritiesRepository.save(authority); return new RedirectView("/login"); } -} +} \ No newline at end of file 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..524ceda46 --- /dev/null +++ b/src/main/java/com/makersacademy/acebook/model/Comment.java @@ -0,0 +1,35 @@ +// Comment.java +package com.makersacademy.acebook.model; + +import lombok.Data; + +import javax.persistence.*; + +@Data +@Entity +@Table(name = "COMMENTS") +public class Comment { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(nullable = false) + private String content; + + @ManyToOne + @JoinColumn(name = "post_id", nullable = false) + private Post post; + + @ManyToOne + @JoinColumn(name = "user_id", nullable = false) + private User user; + + public Comment() {} + + public Comment(String content, Post post, User user) { + this.content = content; + this.post = post; + this.user = user; + } +} \ No newline at end of file diff --git a/src/main/java/com/makersacademy/acebook/model/Like.java b/src/main/java/com/makersacademy/acebook/model/Like.java new file mode 100644 index 000000000..d75e01ba7 --- /dev/null +++ b/src/main/java/com/makersacademy/acebook/model/Like.java @@ -0,0 +1,39 @@ +package com.makersacademy.acebook.model; + +import lombok.Data; + +import javax.persistence.*; +import java.time.LocalDateTime; + +@Data +@Entity +@Table(name = "likes") +public class Like { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @ManyToOne + @JoinColumn(name = "user_id") + private User user; + + @ManyToOne + @JoinColumn(name = "post_id") + private Post post; + + @Column(name = "created_at") + private LocalDateTime createdAt; + + // Constructors, getters, and setters (generated by Lombok @Data) + + // Constructors + public Like() { + } + + public Like(User user, Post post) { + this.user = user; + this.post = post; + this.createdAt = LocalDateTime.now(); + } +} \ No newline at end of file diff --git a/src/main/java/com/makersacademy/acebook/model/Post.java b/src/main/java/com/makersacademy/acebook/model/Post.java index 0098de1b3..02055ecb4 100644 --- a/src/main/java/com/makersacademy/acebook/model/Post.java +++ b/src/main/java/com/makersacademy/acebook/model/Post.java @@ -1,12 +1,13 @@ package com.makersacademy.acebook.model; -import javax.persistence.Entity; -import javax.persistence.GeneratedValue; -import javax.persistence.Id; -import javax.persistence.Table; -import javax.persistence.GenerationType; - import lombok.Data; +import lombok.Getter; +import lombok.Setter; + +import javax.persistence.*; +import java.util.ArrayList; +import java.util.List; +import java.time.LocalDateTime; @Data @Entity @@ -16,14 +17,73 @@ public class Post { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; + + @Setter + @Getter private String content; - public Post() {} + @Setter + @Getter + @ManyToOne + @JoinColumn(name = "user_id", nullable = false) + private User user; + + @Setter + @Getter + private String imageUrl; + + @Setter + @Getter + private LocalDateTime createdAt; + + @OneToMany(mappedBy = "post", cascade = CascadeType.ALL, fetch = FetchType.LAZY) + private List comments = new ArrayList<>(); + + @OneToMany(mappedBy = "post", cascade = CascadeType.ALL, fetch = FetchType.LAZY) + private List likes = new ArrayList<>(); + + public Post() { + this.createdAt = LocalDateTime.now(); + } + + public Post(String content, User user) { + this.content = content; + this.user = user; + this.createdAt = LocalDateTime.now(); + } - public Post(String content) { + public Post(String content, User user, String imageUrl) { this.content = content; + this.user = user; + this.imageUrl = imageUrl; + this.createdAt = LocalDateTime.now(); } - public String getContent() { return this.content; } - public void setContent(String content) { this.content = content; } -} + // Getters and setters for comments and likes (generated by Lombok @Data) + + public void addComment(Comment comment) { + comments.add(comment); + comment.setPost(this); + } + + public void removeComment(Comment comment) { + comments.remove(comment); + comment.setPost(null); + } + + public void addLike(Like like) { + likes.add(like); + like.setPost(this); + } + + public void removeLike(Like like) { + likes.remove(like); + like.setPost(null); + } + + public boolean containsUser(String username) { + return likes.stream().anyMatch(like -> like.getUser().getUsername().equals(username)); + } + + +} \ No newline at end of file diff --git a/src/main/java/com/makersacademy/acebook/model/User.java b/src/main/java/com/makersacademy/acebook/model/User.java index df2a0edf1..10d8f40e3 100644 --- a/src/main/java/com/makersacademy/acebook/model/User.java +++ b/src/main/java/com/makersacademy/acebook/model/User.java @@ -1,12 +1,11 @@ package com.makersacademy.acebook.model; -import javax.persistence.Entity; -import javax.persistence.GeneratedValue; -import javax.persistence.Id; -import javax.persistence.Table; -import javax.persistence.GenerationType; +import javax.persistence.*; import lombok.Data; +import lombok.Getter; +import lombok.Setter; +import org.springframework.web.multipart.MultipartFile; import static java.lang.Boolean.TRUE; @@ -17,28 +16,47 @@ public class User { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; + + @Setter + @Getter private String username; + + @Setter + @Getter private String password; private boolean enabled; + @Setter + @Getter + private String profilePictureUrl; + + @Setter + @Getter + private String bio; + + @Getter + @Transient + private MultipartFile profilePicture; + public User() { this.enabled = TRUE; } - public User(String username, String password) { + public User(String username, String password, String profilePictureUrl) { this.username = username; this.password = password; this.enabled = TRUE; + this.bio = bio; + this.profilePictureUrl = profilePictureUrl; } - public User(String username, String password, boolean enabled) { + public User(String username, String password, boolean enabled, String profilePictureUrl) { this.username = username; this.password = password; this.enabled = enabled; + this.bio = bio; + this.profilePictureUrl = profilePictureUrl; } - public String getUsername() { return this.username; } - public String getPassword() { return this.password; } - public void setUsername(String username) { this.username = username; } - public void setPassword(String password) { this.password = password; } + public void setProfilePicture(MultipartFile profilePicture) { this.profilePicture = profilePicture; } } 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..59d9897bc --- /dev/null +++ b/src/main/java/com/makersacademy/acebook/repository/CommentRepository.java @@ -0,0 +1,6 @@ +package com.makersacademy.acebook.repository; + +import com.makersacademy.acebook.model.Comment; +import org.springframework.data.repository.CrudRepository; + +public interface CommentRepository extends CrudRepository {} \ No newline at end of file diff --git a/src/main/java/com/makersacademy/acebook/repository/LikeRepository.java b/src/main/java/com/makersacademy/acebook/repository/LikeRepository.java new file mode 100644 index 000000000..67d46de55 --- /dev/null +++ b/src/main/java/com/makersacademy/acebook/repository/LikeRepository.java @@ -0,0 +1,15 @@ +package com.makersacademy.acebook.repository; + +import com.makersacademy.acebook.model.Like; +import com.makersacademy.acebook.model.Post; +import com.makersacademy.acebook.model.User; +import org.springframework.data.repository.CrudRepository; + +import java.util.Optional; + +public interface LikeRepository extends CrudRepository { + + Optional findByPostAndUser(Post post, User user); + long countByPostId(Long postId); + +} \ No newline at end of file diff --git a/src/main/java/com/makersacademy/acebook/repository/PostRepository.java b/src/main/java/com/makersacademy/acebook/repository/PostRepository.java index d435e0ce1..5d8cd1026 100644 --- a/src/main/java/com/makersacademy/acebook/repository/PostRepository.java +++ b/src/main/java/com/makersacademy/acebook/repository/PostRepository.java @@ -1,9 +1,12 @@ package com.makersacademy.acebook.repository; import com.makersacademy.acebook.model.Post; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.repository.CrudRepository; +import org.springframework.stereotype.Repository; -public interface PostRepository extends CrudRepository { +import java.util.List; -} +@Repository +public interface PostRepository extends CrudRepository { + List findByUserId(Long userId); +} \ No newline at end of file diff --git a/src/main/java/com/makersacademy/acebook/repository/UserRepository.java b/src/main/java/com/makersacademy/acebook/repository/UserRepository.java index 2cccc950f..983bf34b4 100644 --- a/src/main/java/com/makersacademy/acebook/repository/UserRepository.java +++ b/src/main/java/com/makersacademy/acebook/repository/UserRepository.java @@ -4,5 +4,5 @@ import org.springframework.data.repository.CrudRepository; public interface UserRepository extends CrudRepository { - + User findByUsername(String username); } diff --git a/src/main/java/com/makersacademy/acebook/service/PostService.java b/src/main/java/com/makersacademy/acebook/service/PostService.java new file mode 100644 index 000000000..f91dba25a --- /dev/null +++ b/src/main/java/com/makersacademy/acebook/service/PostService.java @@ -0,0 +1,140 @@ +package com.makersacademy.acebook.service; + +import com.makersacademy.acebook.model.Comment; +import com.makersacademy.acebook.model.Like; +import com.makersacademy.acebook.model.Post; +import com.makersacademy.acebook.model.User; +import com.makersacademy.acebook.repository.CommentRepository; +import com.makersacademy.acebook.repository.LikeRepository; +import com.makersacademy.acebook.repository.PostRepository; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; +import org.springframework.web.multipart.MultipartFile; +import software.amazon.awssdk.auth.credentials.AwsBasicCredentials; +import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider; +import software.amazon.awssdk.regions.Region; +import software.amazon.awssdk.services.s3.S3Client; +import software.amazon.awssdk.services.s3.model.PutObjectRequest; +import software.amazon.awssdk.services.s3.model.PutObjectResponse; +import software.amazon.awssdk.services.s3.presigner.S3Presigner; +import software.amazon.awssdk.services.s3.presigner.model.GetObjectPresignRequest; + +import javax.transaction.Transactional; +import java.io.IOException; + +import java.time.LocalDateTime; +import java.util.*; + +@Service +public class PostService { + + @Autowired + private PostRepository postRepository; + + @Autowired + private S3Service s3Service; + @Autowired + private CommentRepository commentRepository; + + @Autowired + private LikeRepository likeRepository; + private final S3Client s3Client; + private final S3Presigner s3Presigner; + private final String bucketName; + + @Autowired + public PostService( + @Value("${aws.region}") String region, + @Value("${aws.accessKeyId}") String accessKeyId, + @Value("${aws.secretAccessKey}") String secretAccessKey, + @Value("${aws.s3.bucket.name}") String bucketName) { + + this.bucketName = bucketName; + + AwsBasicCredentials awsCreds = AwsBasicCredentials.create(accessKeyId, secretAccessKey); + + this.s3Client = S3Client.builder() + .region(Region.of(region)) + .credentialsProvider(StaticCredentialsProvider.create(awsCreds)) + .build(); + + this.s3Presigner = S3Presigner.builder() + .region(Region.of(region)) + .credentialsProvider(StaticCredentialsProvider.create(awsCreds)) + .build(); + } + + + public String saveProfilePicture(MultipartFile image) throws IOException { + String filename = "profile_pictures/" + System.currentTimeMillis() + "_" + image.getOriginalFilename(); + PutObjectRequest putObjectRequest = PutObjectRequest.builder() + .bucket(bucketName) + .key(filename) + .build(); + Map metadata = new HashMap<>(); + metadata.put("Content-Type", image.getContentType()); + PutObjectResponse response = s3Client.putObject(putObjectRequest, + software.amazon.awssdk.core.sync.RequestBody.fromBytes(image.getBytes())); + GetObjectPresignRequest getObjectPresignRequest = GetObjectPresignRequest.builder() + .getObjectRequest(r -> r.bucket(bucketName).key(filename)) + .signatureDuration(java.time.Duration.ofDays(7)) + .build(); + return s3Presigner.presignGetObject(getObjectPresignRequest).url().toString(); + } + + @Transactional + public void savePost(Post post, MultipartFile image) throws IOException { + if (!image.isEmpty()) { + String imageUrl = s3Service.saveImage(image); + post.setImageUrl(imageUrl); + } + postRepository.save(post); + } + public List getPostsByUserId(Long userId) { + List posts = postRepository.findByUserId(userId); + posts.sort(Comparator.comparing(Post::getCreatedAt).reversed()); + return posts; + } + public Iterable getAllPosts() { + return postRepository.findAll(); + } + + public Iterable getAllPostsFromNewestToOldest(){ + List posts = (List) postRepository.findAll(); + posts.sort(Comparator.comparing(Post::getCreatedAt).reversed()); + return posts; + } + + + @Transactional + public Comment addComment(Long postId, String content, User user) { + Post post = postRepository.findById(postId) + .orElseThrow(() -> new RuntimeException("Post not found")); + + Comment comment = new Comment(content, post, user); + return commentRepository.save(comment); + } + + @Transactional + public void toggleLike(Long postId, User user) { + Post post = postRepository.findById(postId) + .orElseThrow(() -> new RuntimeException("Post not found")); + + Optional existingLike = likeRepository.findByPostAndUser(post, user); + + if (existingLike.isPresent()) { + Like like = existingLike.get(); + likeRepository.delete(like); + post.removeLike(like); // Remove the like from the post + } else { + Like like = new Like(user, post); + likeRepository.save(like); + post.addLike(like); // Link the like to the post + } + } + + public long countLikes(Long postId) { + return likeRepository.countByPostId(postId); + } +} \ No newline at end of file diff --git a/src/main/java/com/makersacademy/acebook/service/S3Service.java b/src/main/java/com/makersacademy/acebook/service/S3Service.java new file mode 100644 index 000000000..b3d538bf2 --- /dev/null +++ b/src/main/java/com/makersacademy/acebook/service/S3Service.java @@ -0,0 +1,71 @@ +package com.makersacademy.acebook.service; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; +import org.springframework.web.multipart.MultipartFile; +import software.amazon.awssdk.auth.credentials.AwsBasicCredentials; +import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider; +import software.amazon.awssdk.regions.Region; +import software.amazon.awssdk.services.s3.S3Client; +import software.amazon.awssdk.services.s3.model.PutObjectRequest; +import software.amazon.awssdk.services.s3.model.PutObjectResponse; +import software.amazon.awssdk.services.s3.presigner.S3Presigner; +import software.amazon.awssdk.services.s3.presigner.model.GetObjectPresignRequest; + +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; + +@Service +public class S3Service { + + private final S3Client s3Client; + private final S3Presigner s3Presigner; + private final String bucketName; + + public S3Service( + @Value("${aws.region}") String region, + @Value("${aws.accessKeyId}") String accessKeyId, + @Value("${aws.secretAccessKey}") String secretAccessKey, + @Value("${aws.s3.bucket.name}") String bucketName) { + + this.bucketName = bucketName; + + AwsBasicCredentials awsCreds = AwsBasicCredentials.create(accessKeyId, secretAccessKey); + + this.s3Client = S3Client.builder() + .region(Region.of(region)) + .credentialsProvider(StaticCredentialsProvider.create(awsCreds)) + .build(); + + this.s3Presigner = S3Presigner.builder() + .region(Region.of(region)) + .credentialsProvider(StaticCredentialsProvider.create(awsCreds)) + .build(); + } + + public String uploadFile(MultipartFile file, String folder) throws IOException { + String filename = folder + "/" + System.currentTimeMillis() + "_" + file.getOriginalFilename(); + PutObjectRequest putObjectRequest = PutObjectRequest.builder() + .bucket(bucketName) + .key(filename) + .build(); + Map metadata = new HashMap<>(); + metadata.put("Content-Type", file.getContentType()); + PutObjectResponse response = s3Client.putObject(putObjectRequest, + software.amazon.awssdk.core.sync.RequestBody.fromBytes(file.getBytes())); + GetObjectPresignRequest getObjectPresignRequest = GetObjectPresignRequest.builder() + .getObjectRequest(r -> r.bucket(bucketName).key(filename)) + .signatureDuration(java.time.Duration.ofDays(7)) + .build(); + return s3Presigner.presignGetObject(getObjectPresignRequest).url().toString(); + } + + public String saveProfilePicture(MultipartFile image) throws IOException { + return uploadFile(image, "profile_pictures"); + } + + public String saveImage(MultipartFile image) throws IOException { + return uploadFile(image, "post_images"); + } +} \ No newline at end of file diff --git a/src/main/java/com/makersacademy/acebook/service/UserService.java b/src/main/java/com/makersacademy/acebook/service/UserService.java new file mode 100644 index 000000000..47fb67658 --- /dev/null +++ b/src/main/java/com/makersacademy/acebook/service/UserService.java @@ -0,0 +1,44 @@ +package com.makersacademy.acebook.service; + +import com.makersacademy.acebook.model.User; +import com.makersacademy.acebook.repository.UserRepository; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.web.multipart.MultipartFile; + +import javax.transaction.Transactional; +import java.io.IOException; + +@Service +public class UserService { + + @Autowired + private UserRepository userRepository; + + @Autowired + private S3Service s3Service; + + public User getUserById(Long id) { + return userRepository.findById(id).orElse(null); + } + + public User getUserByUsername(String username) { + return userRepository.findByUsername(username); + } + + public User updateUserProfile(User user, MultipartFile profilePicture) throws IOException { + if (!profilePicture.isEmpty()) { + String profilePictureUrl = s3Service.uploadFile(profilePicture, "profile_pictures"); + user.setProfilePictureUrl(profilePictureUrl); + } + return userRepository.save(user); + } + + @Transactional + public void updateBio(User user, String bio){ + if(user != null){ + user.setBio(bio); + userRepository.save(user); + } + } +} \ No newline at end of file diff --git a/src/main/resources/application-dev.properties b/src/main/resources/application-dev.properties index 436755df9..45e4ba93c 100644 --- a/src/main/resources/application-dev.properties +++ b/src/main/resources/application-dev.properties @@ -1,6 +1,15 @@ -spring.datasource.url=jdbc:postgresql://localhost:5432/acebook_springboot_development -spring.datasource.username= -spring.datasource.password= -flyway.baseline-on-migrate=true +spring.datasource.url=jdbc:postgresql://${DB_HOST}:5432/${DB_NAME} +spring.datasource.username=${DB_USER} +spring.datasource.password=${DB_PASS} +spring.datasource.driver-class-name=org.postgresql.Driver +spring.jpa.database-platform=org.hibernate.dialect.PostgreSQLDialect + +spring.flyway.baseline-on-migrate=true spring.jpa.properties.hibernate.temp.use_jdbc_metadata_defaults = false -spring.jpa.database-platform=org.hibernate.dialect.PostgreSQL9Dialect +aws.accessKeyId=${AWS_ACCESS_KEY_ID} +aws.secretAccessKey=${AWS_SECRET_ACCESS_KEY} +aws.region=${AWS_REGION} +aws.s3.bucket.name=${AWS_S3_BUCKET_NAME} +# Flyway configurations +spring.flyway.enabled=true +spring.flyway.locations=classpath:db/migration \ No newline at end of file diff --git a/src/main/resources/application-test.properties b/src/main/resources/application-test.properties index 865b41e1c..f09bd0304 100644 --- a/src/main/resources/application-test.properties +++ b/src/main/resources/application-test.properties @@ -1,6 +1,6 @@ -spring.datasource.url=jdbc:postgresql://localhost:5432/acebook_springboot_test -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.datasource.url=jdbc:h2:mem:testdb +spring.datasource.driverClassName=org.h2.Driver +spring.datasource.username=sa +spring.datasource.password=password +spring.jpa.database-platform=org.hibernate.dialect.H2Dialect +spring.jpa.hibernate.ddl-auto=create-drop diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index c7ad09d6d..9beed4812 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -3,3 +3,26 @@ spring.data.rest.base-path=/api spring.datasource.platform=postgres spring.jpa.hibernate.ddl-auto=validate logging.level.org.springframework.web: DEBUG + + +spring.datasource.url=jdbc:postgresql://${DB_HOST}:5432/${DB_NAME} +spring.datasource.username=${DB_USER} +spring.datasource.password=${DB_PASS} +spring.datasource.driver-class-name=org.postgresql.Driver +spring.jpa.database-platform=org.hibernate.dialect.PostgreSQLDialect + + +spring.thymeleaf.prefix=classpath:/templates/ +spring.thymeleaf.suffix=.html +spring.thymeleaf.mode=HTML +spring.thymeleaf.encoding=UTF-8 +spring.thymeleaf.cache=false + +# Flyway configurations +spring.flyway.enabled=true +spring.flyway.locations=classpath:db/migration + +aws.accessKeyId=${AWS_ACCESS_KEY_ID} +aws.secretAccessKey=${AWS_SECRET_ACCESS_KEY} +aws.region=${AWS_REGION} +aws.s3.bucket.name=${AWS_S3_BUCKET_NAME} \ No newline at end of file diff --git a/src/main/resources/db/migration/V10__add_likes_table.sql b/src/main/resources/db/migration/V10__add_likes_table.sql new file mode 100644 index 000000000..be8634044 --- /dev/null +++ b/src/main/resources/db/migration/V10__add_likes_table.sql @@ -0,0 +1,11 @@ +CREATE TABLE likes ( + id bigserial PRIMARY KEY, + post_id BIGINT NOT NULL, + user_id BIGINT NOT NULL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + CONSTRAINT fk_like_post + FOREIGN KEY (post_id) REFERENCES posts(id), + CONSTRAINT fk_like_user + FOREIGN KEY (user_id) REFERENCES users(id), + CONSTRAINT unique_like_per_user_per_post UNIQUE (post_id, user_id) +); diff --git a/src/main/resources/db/migration/V11__add_created_at_colum_to_POSTS.sql b/src/main/resources/db/migration/V11__add_created_at_colum_to_POSTS.sql new file mode 100644 index 000000000..a9e7ef192 --- /dev/null +++ b/src/main/resources/db/migration/V11__add_created_at_colum_to_POSTS.sql @@ -0,0 +1,2 @@ +ALTER TABLE POSTS +ADD COLUMN created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP; \ No newline at end of file diff --git a/src/main/resources/db/migration/V3__add_user_id_foreign_key_to_posts.sql b/src/main/resources/db/migration/V3__add_user_id_foreign_key_to_posts.sql new file mode 100644 index 000000000..a36b8ba3d --- /dev/null +++ b/src/main/resources/db/migration/V3__add_user_id_foreign_key_to_posts.sql @@ -0,0 +1,4 @@ +ALTER TABLE posts ADD COLUMN user_id BIGINT; + +ALTER TABLE posts ADD CONSTRAINT fk_user +FOREIGN KEY (user_id) REFERENCES users(id); diff --git a/src/main/resources/db/migration/V4__delete_posts.sql b/src/main/resources/db/migration/V4__delete_posts.sql new file mode 100644 index 000000000..c1e72e85d --- /dev/null +++ b/src/main/resources/db/migration/V4__delete_posts.sql @@ -0,0 +1 @@ +DELETE FROM posts; \ No newline at end of file diff --git a/src/main/resources/db/migration/V5__add_imageurl_to_table.sql b/src/main/resources/db/migration/V5__add_imageurl_to_table.sql new file mode 100644 index 000000000..18adaf3fa --- /dev/null +++ b/src/main/resources/db/migration/V5__add_imageurl_to_table.sql @@ -0,0 +1 @@ +ALTER TABLE posts ADD COLUMN image_url VARCHAR(255); diff --git a/src/main/resources/db/migration/V6__alter_imageurl_collumn_length.sql b/src/main/resources/db/migration/V6__alter_imageurl_collumn_length.sql new file mode 100644 index 000000000..e482782f0 --- /dev/null +++ b/src/main/resources/db/migration/V6__alter_imageurl_collumn_length.sql @@ -0,0 +1 @@ +ALTER TABLE POSTS ALTER COLUMN image_url TYPE VARCHAR(2048); \ No newline at end of file diff --git a/src/main/resources/db/migration/V7__add_profile_picture_url_to_users.sql b/src/main/resources/db/migration/V7__add_profile_picture_url_to_users.sql new file mode 100644 index 000000000..85cac07b2 --- /dev/null +++ b/src/main/resources/db/migration/V7__add_profile_picture_url_to_users.sql @@ -0,0 +1 @@ +ALTER TABLE users ADD COLUMN profile_picture_url VARCHAR(2048); \ No newline at end of file diff --git a/src/main/resources/db/migration/V8__add_bio_collumn.sql b/src/main/resources/db/migration/V8__add_bio_collumn.sql new file mode 100644 index 000000000..62a10c56c --- /dev/null +++ b/src/main/resources/db/migration/V8__add_bio_collumn.sql @@ -0,0 +1 @@ +ALTER TABLE users ADD bio VARCHAR(255); \ No newline at end of file diff --git a/src/main/resources/db/migration/V9__create_comments_table.sql b/src/main/resources/db/migration/V9__create_comments_table.sql new file mode 100644 index 000000000..ae2e06b18 --- /dev/null +++ b/src/main/resources/db/migration/V9__create_comments_table.sql @@ -0,0 +1,11 @@ +CREATE TABLE comments ( + id bigserial PRIMARY KEY, + content varchar(250) NOT NULL, + post_id BIGINT NOT NULL, + user_id BIGINT NOT NULL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + CONSTRAINT fk_post + FOREIGN KEY (post_id) REFERENCES posts(id), + CONSTRAINT fk_comment_user + FOREIGN KEY (user_id) REFERENCES users(id) +); \ No newline at end of file diff --git a/src/main/resources/static/css/footer-style.css b/src/main/resources/static/css/footer-style.css new file mode 100644 index 000000000..f0466ca89 --- /dev/null +++ b/src/main/resources/static/css/footer-style.css @@ -0,0 +1,19 @@ +/* Fonts */ +@import url('https://fonts.googleapis.com/css2?family=Roboto:ital,wght@0,100;0,300;0,400;0,500;0,700;0,900;1,100;1,300;1,400;1,500;1,700;1,900&display=swap'); + +/* Styles */ +footer { + position: fixed; + bottom: 0px; + width: 100%; + padding: 2px; + background-color: #1877F2; + display: flex; + justify-content: space-around; +} + +footer p { + font-size: 0.8em; + font-family: "Roboto", sans-serif; + color: #fff; +} \ No newline at end of file diff --git a/src/main/resources/static/css/header-style.css b/src/main/resources/static/css/header-style.css new file mode 100644 index 000000000..770ea814d --- /dev/null +++ b/src/main/resources/static/css/header-style.css @@ -0,0 +1,63 @@ +/* Fonts */ +@import url('https://fonts.googleapis.com/css2?family=Ruda:wght@400..900&display=swap'); +@import url('https://fonts.googleapis.com/css2?family=Roboto:ital,wght@0,100;0,300;0,400;0,500;0,700;0,900;1,100;1,300;1,400;1,500;1,700;1,900&display=swap'); + +/* Styles */ +header { + position: fixed; + top: 0px; + margin: 0; + padding: 0; + width: 100%; + box-sizing: border-box; + font-family: "Roboto", sans-serif; + border-bottom: 2px solid #afafaf; + +} + +.header-container { + display: flex; + justify-content: space-between; + align-items: center; + background-color: #fff; + padding: 10px 20px; + max-height: 70px; + box-shadow: 0px 3px 5px lightgrey; +} + +.logo-container { + max-height: 100%; +} + +.logo { + max-height: 100%; + height: auto; + width: auto; + max-width: 45px; +} + +.header-title { + font-family: "Ruda", sans-serif; + font-size: 2.5vw; + font-weight: 800; + color: #1877F2; +} + +.nav-links { + display: flex; + gap: 20px; +} + +.nav-links a { + text-decoration: none; + color: black; + font-size: 1.1em; +} + +.nav-links a:hover { + text-decoration: none; +} + +#login-link { + font-weight: 700; +} \ No newline at end of file diff --git a/src/main/resources/static/css/main.css b/src/main/resources/static/css/main.css new file mode 100644 index 000000000..ef2e0d2a5 --- /dev/null +++ b/src/main/resources/static/css/main.css @@ -0,0 +1,190 @@ +body { + font-family: "Ruda", sans-serif; + background-color: #f0f2f5; + margin: 0; + padding: 0; +} + +main { + max-width: 70%; + margin: auto; + padding: 110px 2.5vw 40px 2.5vw; + background-color: #fff; + border-radius: 5px; + box-shadow: 0 0 10px rgba(0, 0, 0, 0.1); +} + +.italic-text { + font-style: italic; +} + +h1 { + color: #1877F2; +} + +h2 { + padding: 0; + margin: 0.5em 0; +} + +h3 { + font-size: 1em; +} + +#new-post-form { + max-width: 600px; + margin: 1.5vw 0 0 0; + padding: 5px 10px; + background-color: #f0f2f5; + border: 1px solid #ccc; + border-radius: 8px; +} + +.form-post-content, +.form-post-image { + flex: 1; + margin-right: 10px; +} + +.form-post-content textarea { + width: 100%; + height: 7vw; + padding: 8px; + margin-top: 5px; + border: 1px solid #ccc; + border-radius: 4px; +} + +.form-post-buttons { + text-align: center; +} + +.form-group { + margin-bottom: 15px; +} + +label { + display: block; + margin-bottom: 5px; + font-weight: bold; +} + +input[type="text"], input[type="password"], input[type="email"], textarea { + width: 100%; + padding: 10px; + margin-bottom: 10px; + border: 1px solid #ccc; + border-radius: 5px; + box-sizing: border-box; + font-family: "Ruda", sans-serif; +} + +input[type="submit"], +input[type="reset"] { + background-color: #1877F2; + color: #fff; + border: none; + padding: 10px 20px; + cursor: pointer; + border-radius: 5px; + font-family: "Ruda", sans-serif; + transition: background-color 0.3s ease; +} + +input[type="submit"]:hover, +input[type="reset"]:hover { + background-color: #135292; +} + +.posts-list{ +/* background-color: green;*/ + list-style-type: none; + padding: 0; +} + +.post-box { +/* background-color: pink;*/ + border: 1px solid #ccc; + margin-bottom: 2vw; + padding: 10px; + border-radius: 5px; + display: block; +} + +.user-info { +/* background-color: blue;*/ + display: flex; + align-items: flex-start; +} + +.profile-pic { + width: 50px; + height: 50px; + border-radius: 50%; + margin-right: 10px; +} + +.username { + margin: auto 0; + font-weight: bold; +} + +.post-row { +/* background-color: purple;*/ + display: flex; + justify-content: space-between; + margin: 2vw; +} + +<<<<<<< HEAD +footer { + background-color: #1877F2; + color: #fff; + text-align: center; + padding: 20px; +} +.post-text { +/* background-color: brown;*/ + flex: 1; + margin-right: 1vw; +} + +.post-image img { + max-width: 300px; + max-height: 300px; + border-radius: 5px; +} + +.post-likes, +.post-comments { + margin-bottom: 10px; +} + +.post-likes { + display: flex; + gap: 1vw; +} + +.comments-list { + margin-bottom: 1.5vw; +} + +.post-comments, +.add-comment { + border-top: 1px solid #ccc; +} + +.comment-block { + display: flex; + margin-bottom: 0.5vw; +} + +.comment-username { + margin-right: 0.5vw; +} + +.like-buttons { + display: flex; + margin: auto 0; +} + diff --git a/src/main/resources/static/css/profile.css b/src/main/resources/static/css/profile.css new file mode 100644 index 000000000..d921113a1 --- /dev/null +++ b/src/main/resources/static/css/profile.css @@ -0,0 +1,227 @@ +body { + font-family: Arial, sans-serif; + background-color: #f0f2f5; + margin: 0; + padding: 0; +} + +main { + padding: 100px 0; +} + +.profile-banner { + background-color: white; + padding: 20px; + display: flex; + align-items: center; + border-bottom: 1px solid #ddd; +} + +.spacer { + background-color: rgb(240, 242, 245); + height: 5px; + padding: 10px; + display: flex; + align-items: center; +} + +.profile-picture-container { + margin-right: 20px; + width: 158px; + height: 158px; + border-radius: 50%; + border: 4px solid #386ad1; + +} + +.profile-picture { + width: 150px; + height: 150px; + margin: auto; + border-radius: 50%; + border: 4px solid #ffffff; +} + + + + +.profile-info h1 { + margin: 0; + font-size: 36px; +} + +.profile-bio { + background-color: white; + padding: 20px; + margin: 20px auto; + width: 60%; + border: 1px solid #ddd; + border-radius: 8px; +} + +.profile-bio h2 { + margin-top: 0; + font-size: 20px; + border-bottom: 1px solid #ddd; + padding-bottom: 10px; +} + +.profile-bio p { + margin: 10px 0; +} + + + + +.new-post { + padding: 20px; + background-color: white; + width: 60%; + border: 1px solid #ddd; + border-radius: 8px; + margin: 20px auto; +} + +.form-row { + display: flex; + align-items: center; + justify-content: space-between; + margin-top: 10px; +} + +.form-picture { + width: 45px; + height: 45px; + margin-right: 10px; + border-radius: 50%; +} + +.post-text { + flex-grow: 1; + height: 22px; + width: 85%; + padding: 10px; + font-size: 18px; + margin: auto; + border: 1px solid #ccc; + border-radius: 10px; + resize: vertical; + background-color: rgb(240, 242, 245); +} + +.btn-div { + display: flex; + justify-content: flex-end; + flex-grow: 1; +} + +.button { + background-color: #1877F2; /* Facebook blue */ + border: none; + color: white; + padding: 10px 25px; + text-align: center; + text-decoration: none; + display: inline-block; + font-size: 16px; + border-radius: 5px; + margin-left: auto; +} + +input[type="file"] { + margin-top: 0; + margin-right: 10px; + width: auto; + flex-grow: 1; +} +.new-post .post-actions { + display: flex; + flex-direction: column; + gap: 10px; +} + +.new-post .post-actions input[type="file"] { + margin-bottom: 10px; +} + +.new-post .post-actions button { + align-self: flex-start; + padding: 10px 20px; + background-color: #4267b2; + color: white; + border: none; + border-radius: 5px; + cursor: pointer; +} + +.new-post .post-actions button:hover { + background-color: #365899; +} + + + +.post:last-child { + border-bottom: none; +} + +.post p { + margin: 15px 15px; +} + + + +.user-posts { + background-color: white; + padding: 20px; + margin: 20px auto; + width: 60%; + border: 1px solid #ddd; + border-radius: 8px; + +} + +.post { + padding: 10px; + border-radius: 5px; + border: 2px solid #ddd; + margin: 10px; +} + +.post-img-div { + display: flex; + justify-content: center; + align-items: center; + margin-top: 10px; + background-color: rgb(147, 153, 158); + +} + +.post-image { + max-width: 40%; + height: auto; + +} + + +.post-banner { + background-color: white; + padding: 20px; + display: flex; + align-items: center; + border-bottom: 1px solid #ddd; +} + + +.post-picture-container { + margin-right: 20px; + border-radius: 50%; + +} + + +.post-info h1 { + margin: 0; + font-size: 36px; +} + + diff --git a/src/main/resources/static/images/acebook_logo.png b/src/main/resources/static/images/acebook_logo.png new file mode 100644 index 000000000..134a30921 Binary files /dev/null and b/src/main/resources/static/images/acebook_logo.png differ diff --git a/src/main/resources/static/images/profile-user-icon.png b/src/main/resources/static/images/profile-user-icon.png new file mode 100644 index 000000000..174824794 Binary files /dev/null and b/src/main/resources/static/images/profile-user-icon.png differ diff --git a/src/main/resources/static/js/scripts.js b/src/main/resources/static/js/scripts.js new file mode 100644 index 000000000..947f1f86a --- /dev/null +++ b/src/main/resources/static/js/scripts.js @@ -0,0 +1,43 @@ +document.addEventListener("DOMContentLoaded", function() { + console.log("DOMContentLoaded event fired"); + + fetch("/auth/userLoggedIn") + .then(response => response.json()) + .then(data => { + const loginLink = document.getElementById("login-link"); + const signUpLink = document.getElementById("sign-up-link"); + + if (data.loggedIn) { + loginLink.textContent = "log out"; + loginLink.addEventListener("click", function(event) { + event.preventDefault(); + + fetch("/logout", { + method: "POST", + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + 'X-CSRF-Token': document.querySelector('meta[name="csrf-token"]').getAttribute('content') + } + }) + .then(response => { + if (response.ok) { + window.location.href = "/login?logout"; + } else { + console.error('Logout failed'); + } + }) + .catch(error => { + console.error('Error:', error); + }); + }); + signUpLink.style.display = "none"; + } else { + loginLink.textContent = "log in"; + loginLink.href = "/login"; + } + }) + .catch(error => { + console.error('Error:', error); + }); +}); + diff --git a/src/main/resources/static/main.css b/src/main/resources/static/main.css deleted file mode 100644 index d0260873c..000000000 --- a/src/main/resources/static/main.css +++ /dev/null @@ -1,10 +0,0 @@ -.posts-main { - border-collapse: collapse; -} - -.post-main { - border: 1px solid #999; - padding: 0.5rem; - text-align: left; - margin-bottom: 0.5rem;; -} diff --git a/src/main/resources/static/profile-pictures/Screenshot 2024-06-05 at 10.01.00.png b/src/main/resources/static/profile-pictures/Screenshot 2024-06-05 at 10.01.00.png new file mode 100644 index 000000000..0c28d6b4c Binary files /dev/null and b/src/main/resources/static/profile-pictures/Screenshot 2024-06-05 at 10.01.00.png differ diff --git a/src/main/resources/static/profile-pictures/pfp.jpeg b/src/main/resources/static/profile-pictures/pfp.jpeg new file mode 100644 index 000000000..bd9ea4a45 Binary files /dev/null and b/src/main/resources/static/profile-pictures/pfp.jpeg differ diff --git a/src/main/resources/templates/footer.html b/src/main/resources/templates/footer.html new file mode 100644 index 000000000..e40f4da1c --- /dev/null +++ b/src/main/resources/templates/footer.html @@ -0,0 +1,11 @@ + + + + + + +
+

© 2024 Acebook. All rights reserved.

+
+ + \ No newline at end of file diff --git a/src/main/resources/templates/header.html b/src/main/resources/templates/header.html new file mode 100644 index 000000000..0a59a89c8 --- /dev/null +++ b/src/main/resources/templates/header.html @@ -0,0 +1,31 @@ + + + + Acebook + + + +
+
+
+ +
+ +
+

Acebook

+
+ + +
+
+ + + + + diff --git a/src/main/resources/templates/login.html b/src/main/resources/templates/login.html new file mode 100644 index 000000000..a110e0047 --- /dev/null +++ b/src/main/resources/templates/login.html @@ -0,0 +1,31 @@ + + + + + Login + + + + + + +
+ +
+

Login

+
+

Username:

+

Password:

+

+
+
+

Invalid username of password

+
+
+

You have been logged out.

+
+
+ +
+ + \ No newline at end of file diff --git a/src/main/resources/templates/posts/index.html b/src/main/resources/templates/posts/index.html index 4eb260155..f7f7f32a1 100644 --- a/src/main/resources/templates/posts/index.html +++ b/src/main/resources/templates/posts/index.html @@ -1,27 +1,84 @@ - - - - Acebook - - - - -

Posts

- -
+ + + + Acebook + + + + + + +
+ +
+
Signed in as
-
-

Content:

-

-
+ +
+

New Post

+
+

Content:

+

Image:

+

+
+
+ + +
+

Posts

+
    +
  • + +
    +
    +
    + Post image +
    +
    + + +
    +

    Likes:

    -
      -
    • -
    + +
    + + +
    +

    Comments

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

    Add a Comment

    +
    + + +
    +
    +
  • +
+
+
- - +
+ + \ No newline at end of file diff --git a/src/main/resources/templates/profile.html b/src/main/resources/templates/profile.html new file mode 100644 index 000000000..e3092066d --- /dev/null +++ b/src/main/resources/templates/profile.html @@ -0,0 +1,71 @@ + + + + + Profile + + + + + + +
+
+
+
+
+ Profile Picture +
+
+

Username

+
+
+ +
+

Bio

+

This is the user's bio.

+
+ + + +
+
+ +
+
+ + Profile Picture + + +
+ +
+ +
+
+
+
+ +
+

Posts

+
+
+
+ Profile Picture +
+ +
+

Post content here

+
+ Post Image +
+
+
+
+ +
+ + + \ No newline at end of file diff --git a/src/main/resources/templates/users/new.html b/src/main/resources/templates/users/new.html index 2d763f396..36444f357 100644 --- a/src/main/resources/templates/users/new.html +++ b/src/main/resources/templates/users/new.html @@ -1,15 +1,26 @@ - - - - - Signup - - -
-

Username:

-

Password:

-

-
- - \ No newline at end of file + + + + Signup + + + + + + +
+ +
+

Signup

+
+

Username:

+

Password:

+

Profile Picture:

+

+
+
+ +
+ + diff --git a/src/test/java/SignUpTest.java b/src/test/java/SignUpTest.java index b0e16955b..cb85c8eae 100644 --- a/src/test/java/SignUpTest.java +++ b/src/test/java/SignUpTest.java @@ -8,7 +8,6 @@ import org.openqa.selenium.By; import org.openqa.selenium.WebDriver; import org.openqa.selenium.chrome.ChromeDriver; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; diff --git a/src/test/java/com/makersacademy/acebook/model/CommentTest.java b/src/test/java/com/makersacademy/acebook/model/CommentTest.java new file mode 100644 index 000000000..10d05a350 --- /dev/null +++ b/src/test/java/com/makersacademy/acebook/model/CommentTest.java @@ -0,0 +1,42 @@ +package com.makersacademy.acebook.model; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.Mockito.mock; + +public class CommentTest { + + private Comment comment; + private Post post; + private User user; + + @BeforeEach + public void setup() { + // Mock User + user = mock(User.class); + user.setUsername("testuser"); + + // Create Post + post = new Post("Test post content", user); + + // Create Comment + comment = new Comment("Test comment content", post, user); + } + + @Test + public void testCommentContent() { + assertEquals("Test comment content", comment.getContent()); + } + + @Test + public void testCommentPost() { + assertEquals(post, comment.getPost()); + } + + @Test + public void testCommentUser() { + assertEquals(user, comment.getUser()); + } +} diff --git a/src/test/java/com/makersacademy/acebook/model/LikeTest.java b/src/test/java/com/makersacademy/acebook/model/LikeTest.java new file mode 100644 index 000000000..3e70b682f --- /dev/null +++ b/src/test/java/com/makersacademy/acebook/model/LikeTest.java @@ -0,0 +1,44 @@ +package com.makersacademy.acebook.model; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.time.LocalDateTime; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.Mockito.mock; + +public class LikeTest { + + private Like like; + private User user; + private Post post; + + @BeforeEach + public void setup() { + // Mock User + user = mock(User.class); + + // Create Post + post = new Post("Test post content", user); + + // Create Like + like = new Like(user, post); + } + + @Test + public void testLikeUser() { + assertEquals(user, like.getUser()); + } + + @Test + public void testLikePost() { + assertEquals(post, like.getPost()); + } + + @Test + public void testLikeCreatedAt() { + // Check if the createdAt field is not null + assertEquals(LocalDateTime.now().getDayOfYear(), like.getCreatedAt().getDayOfYear()); + } +} diff --git a/src/test/java/com/makersacademy/acebook/model/PostTest.java b/src/test/java/com/makersacademy/acebook/model/PostTest.java index 732aafc6e..2e5d4b91c 100644 --- a/src/test/java/com/makersacademy/acebook/model/PostTest.java +++ b/src/test/java/com/makersacademy/acebook/model/PostTest.java @@ -1,17 +1,64 @@ package com.makersacademy.acebook.model; +import org.junit.Before; +import org.junit.Test; + +import java.time.LocalDateTime; +import java.time.temporal.ChronoUnit; + import static org.hamcrest.CoreMatchers.containsString; import static org.junit.Assert.*; - -import org.junit.Test; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; public class PostTest { - private Post post = new Post("hello"); + private User user; + private Post postWithImage; + private Post postWithoutImage; + + @Before + public void setUp(){ + user = mock(User.class); + when(user.getUsername()).thenReturn("testuser"); + postWithoutImage = new Post("hello", user); + postWithImage = new Post("hello with image", user, "http://image.url/test.jpg"); + } @Test public void postHasContent() { - assertThat(post.getContent(), containsString("hello")); + assertThat(postWithoutImage.getContent(), containsString("hello")); + } + + @Test + public void postHasUser() { + assertEquals("testuser", postWithoutImage.getUser().getUsername()); + } + + @Test + public void postWithImageHasContent() { + assertThat(postWithImage.getContent(), containsString("hello with image")); + } + + @Test + public void postWithImageHasUser() { + assertEquals("testuser", postWithImage.getUser().getUsername()); + } + + @Test + public void postWithImageHasImageUrl() { + assertEquals("http://image.url/test.jpg", postWithImage.getImageUrl()); + } + + @Test + public void postWithoutImageDoesNotHaveImageUrl() { + assertNull(postWithoutImage.getImageUrl()); } -} + @Test + public void postHasCreatedAtTimestampWhenCreated() { + Post newPost = new Post("timestamped post", user); + assertNotNull(newPost.getCreatedAt()); + assertTrue(ChronoUnit.SECONDS.between(newPost.getCreatedAt(), LocalDateTime.now()) < 1); + } +} \ No newline at end of file diff --git a/src/test/java/com/makersacademy/acebook/service/PostServiceIntegrationTest.java b/src/test/java/com/makersacademy/acebook/service/PostServiceIntegrationTest.java new file mode 100644 index 000000000..1ba95fc61 --- /dev/null +++ b/src/test/java/com/makersacademy/acebook/service/PostServiceIntegrationTest.java @@ -0,0 +1,71 @@ +package com.makersacademy.acebook.service; + +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.junit.Assert; +import org.junit.Test; +import org.junit.Before; +import org.junit.runner.RunWith; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; +import org.springframework.context.annotation.Import; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; + +import java.time.LocalDateTime; +import java.util.Iterator; +import java.util.List; + +@RunWith(SpringJUnit4ClassRunner.class) +@DataJpaTest +@Import(PostService.class) +@ActiveProfiles("test") +public class PostServiceIntegrationTest { + + @Autowired + private PostRepository postRepository; + + @Autowired + private UserRepository userRepository; + + @Autowired + private PostService postService; + + private User user; + + @Before + public void setUp() { + user = new User(); + user.setUsername("testuser"); + userRepository.save(user); + + Post post1 = new Post("First post", user); + post1.setCreatedAt(LocalDateTime.now().minusDays(1)); + postRepository.save(post1); + + Post post2 = new Post("Second post", user); + post2.setCreatedAt(LocalDateTime.now()); + postRepository.save(post2); + + Post post3 = new Post("Third post", user); + post3.setCreatedAt(LocalDateTime.now().minusHours(1)); + postRepository.save(post3); + } + + @Test + public void getAllPostsFromNewestToOldestShouldReturnPostsInDescendingOrderOfCreation() { + Iterable postsIterable = postService.getAllPostsFromNewestToOldest(); + List posts = (List) postsIterable; + + Assert.assertEquals(3, posts.size()); + + Iterator iterator = posts.iterator(); + Assert.assertEquals("Second post", iterator.next().getContent()); + Assert.assertEquals("Third post", iterator.next().getContent()); + Assert.assertEquals("First post", iterator.next().getContent()); + } +} \ No newline at end of file