diff --git a/.gitignore b/.gitignore index b3db3e2..d4bf309 100644 --- a/.gitignore +++ b/.gitignore @@ -36,4 +36,5 @@ out/ ### VS Code ### .vscode/ +### Project specific application.yml diff --git a/README.md b/README.md index dd76df0..fa76151 100644 --- a/README.md +++ b/README.md @@ -24,4 +24,6 @@ Once you are fully up to speed and working on the project it is perfectly accept Once you have your teams set up, enjoy working on the code. -We look forward to seeing what you manage to produce from it! \ No newline at end of file +We look forward to seeing what you manage to produce from it! + +----Test access Magnus----- diff --git a/src/main/java/com/booleanuk/Main.java b/src/main/java/com/booleanuk/Main.java index d1fae35..9b9d49d 100644 --- a/src/main/java/com/booleanuk/Main.java +++ b/src/main/java/com/booleanuk/Main.java @@ -8,6 +8,7 @@ import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.security.crypto.password.PasswordEncoder; +import java.time.LocalDate; import java.util.HashSet; import java.util.Set; @@ -69,7 +70,20 @@ public void run(String... args) { } Profile studentProfile; if (!this.profileRepository.existsById(1)) { - studentProfile = this.profileRepository.save(new Profile(studentUser, "Joe", "Bloggs", "Hello world!", "student1")); + studentProfile = this.profileRepository.save(new Profile(studentUser, + "Joe", + "Bloggs", + "usrname", + "mygit", + "95555", + "Hello world!", + studentRole, + "Backend Development", + cohort, + LocalDate.of(2025, 9, 8), + LocalDate.of(2026, 9, 8), + "fnwjkdnfj32.,." + )); } else { studentProfile = this.profileRepository.findById(1).orElse(null); } @@ -84,7 +98,20 @@ public void run(String... args) { } Profile teacherProfile; if (!this.profileRepository.existsById(2)) { - teacherProfile = this.profileRepository.save(new Profile(teacherUser, "Rick", "Sanchez", "Hello there!", "teacher1")); + teacherProfile = this.profileRepository.save(new Profile(teacherUser, + "Joe", + "Bloggs", + "usrname", + "mygit", + "95555", + "Hello world!", + teacherRole, + "Backend Development", + cohort, + LocalDate.of(2025, 9, 8), + LocalDate.of(2026, 9, 8), + "fnwjkdnfj32.,." + )); } else { teacherProfile = this.profileRepository.findById(2).orElse(null); } diff --git a/src/main/java/com/booleanuk/cohorts/controllers/AuthController.java b/src/main/java/com/booleanuk/cohorts/controllers/AuthController.java index 0c9ba64..3a20429 100644 --- a/src/main/java/com/booleanuk/cohorts/controllers/AuthController.java +++ b/src/main/java/com/booleanuk/cohorts/controllers/AuthController.java @@ -27,6 +27,7 @@ import java.util.Set; import java.util.stream.Collectors; +//fixed issue with login. @CrossOrigin(origins = "*", maxAge = 3600) @RestController @RequestMapping @@ -69,6 +70,17 @@ public ResponseEntity registerUser(@Valid @RequestBody SignupRequest signupRe if (userRepository.existsByEmail(signupRequest.getEmail())) { return ResponseEntity.badRequest().body(new MessageResponse("Error: Email is already in use!")); } + + + String emailRegex = "^\\w+([.-]?\\w+)*@\\w+([.-]?\\w+)*(\\.\\w{2,3})+$"; + String passwordRegex = "^(?=.*[A-Z])(?=.*[0-9])(?=.*[#?!@$%^&-]).{8,}$"; + + if(!signupRequest.getEmail().matches(emailRegex)) + return ResponseEntity.badRequest().body(new MessageResponse("Email is incorrectly formatted")); + + if(!signupRequest.getPassword().matches(passwordRegex)) + return ResponseEntity.badRequest().body(new MessageResponse("Password is incorrectly formatted")); + // Create a new user add salt here if using one User user = new User(signupRequest.getEmail(), encoder.encode(signupRequest.getPassword())); if (signupRequest.getCohort() != null) { diff --git a/src/main/java/com/booleanuk/cohorts/controllers/CohortController.java b/src/main/java/com/booleanuk/cohorts/controllers/CohortController.java index e96619c..e1445ad 100644 --- a/src/main/java/com/booleanuk/cohorts/controllers/CohortController.java +++ b/src/main/java/com/booleanuk/cohorts/controllers/CohortController.java @@ -1,10 +1,7 @@ package com.booleanuk.cohorts.controllers; -import com.booleanuk.cohorts.models.Cohort; -import com.booleanuk.cohorts.payload.response.CohortListResponse; -import com.booleanuk.cohorts.payload.response.CohortResponse; -import com.booleanuk.cohorts.payload.response.ErrorResponse; -import com.booleanuk.cohorts.payload.response.Response; +import com.booleanuk.cohorts.models.*; +import com.booleanuk.cohorts.payload.response.*; import com.booleanuk.cohorts.repository.CohortRepository; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpStatus; @@ -28,7 +25,7 @@ public ResponseEntity getAllCohorts() { @GetMapping("{id}") public ResponseEntity getCohortById(@PathVariable int id) { Cohort cohort = this.cohortRepository.findById(id).orElse(null); - if (cohort == null) { + if (cohort == null || cohort.getUsers().isEmpty()) { ErrorResponse error = new ErrorResponse(); error.set("not found"); return new ResponseEntity<>(error, HttpStatus.NOT_FOUND); diff --git a/src/main/java/com/booleanuk/cohorts/controllers/CourseController.java b/src/main/java/com/booleanuk/cohorts/controllers/CourseController.java new file mode 100644 index 0000000..13a5f12 --- /dev/null +++ b/src/main/java/com/booleanuk/cohorts/controllers/CourseController.java @@ -0,0 +1,53 @@ +package com.booleanuk.cohorts.controllers; + + +import com.booleanuk.cohorts.models.Course; +import com.booleanuk.cohorts.models.Post; +import com.booleanuk.cohorts.models.User; +import com.booleanuk.cohorts.payload.request.CourseRequest; +import com.booleanuk.cohorts.payload.request.PostRequest; +import com.booleanuk.cohorts.payload.response.*; +import com.booleanuk.cohorts.repository.CourseRepository; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +@CrossOrigin(origins = "*", maxAge = 3600) +@RestController +@RequestMapping("courses") +public class CourseController { + @Autowired + private CourseRepository courseRepository; + + @GetMapping + public ResponseEntity getAllCourse(){ + CourseListResponse courseListResponse = new CourseListResponse(); + courseListResponse.set(this.courseRepository.findAll()); + return ResponseEntity.ok(courseListResponse); + } + + @GetMapping("{id}") + public ResponseEntity getCourseById(@PathVariable int id){ + Course course = this.courseRepository.findById(id).orElse(null); + if (course == null) { + ErrorResponse error = new ErrorResponse(); + error.set("not found"); + return new ResponseEntity<>(error, HttpStatus.NOT_FOUND); + } + CourseResponse courseResponse = new CourseResponse(); + courseResponse.set(course); + return ResponseEntity.ok(courseResponse); + } + + @PostMapping + public ResponseEntity createCourse(@RequestBody CourseRequest courseRequest){ + Course course = new Course(courseRequest.getName()); + Course saveCourse = this.courseRepository.save(course); + CourseResponse courseResponse = new CourseResponse(); + courseResponse.set(saveCourse); + return new ResponseEntity<>(courseResponse, HttpStatus.CREATED); + + } + +} diff --git a/src/main/java/com/booleanuk/cohorts/controllers/PostController.java b/src/main/java/com/booleanuk/cohorts/controllers/PostController.java index bfb42ef..c47bbd0 100644 --- a/src/main/java/com/booleanuk/cohorts/controllers/PostController.java +++ b/src/main/java/com/booleanuk/cohorts/controllers/PostController.java @@ -1,23 +1,39 @@ package com.booleanuk.cohorts.controllers; +import java.util.List; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.web.bind.annotation.CrossOrigin; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + import com.booleanuk.cohorts.models.Author; +import com.booleanuk.cohorts.models.Comment; import com.booleanuk.cohorts.models.Post; import com.booleanuk.cohorts.models.Profile; import com.booleanuk.cohorts.models.User; +import com.booleanuk.cohorts.payload.request.CommentRequest; +import com.booleanuk.cohorts.payload.request.PostRequest; +import com.booleanuk.cohorts.payload.response.CommentResponse; import com.booleanuk.cohorts.payload.response.ErrorResponse; import com.booleanuk.cohorts.payload.response.PostListResponse; import com.booleanuk.cohorts.payload.response.PostResponse; import com.booleanuk.cohorts.payload.response.Response; +import com.booleanuk.cohorts.repository.CommentRepository; import com.booleanuk.cohorts.repository.PostRepository; import com.booleanuk.cohorts.repository.ProfileRepository; import com.booleanuk.cohorts.repository.UserRepository; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.http.HttpStatus; -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.*; - -import java.util.ArrayList; -import java.util.List; +import com.booleanuk.cohorts.security.services.UserDetailsImpl; @CrossOrigin(origins = "*", maxAge = 3600) @RestController @@ -29,51 +45,210 @@ public class PostController { private UserRepository userRepository; @Autowired private ProfileRepository profileRepository; + @Autowired + private CommentRepository commentRepository; + + private User getCurrentAuthenticatedUser() { + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + if (authentication != null && authentication.getPrincipal() instanceof UserDetailsImpl) { + UserDetailsImpl userDetails = (UserDetailsImpl) authentication.getPrincipal(); + return this.userRepository.findByEmail(userDetails.getEmail()).orElse(null); + } + return null; + } + + private ResponseEntity unauthorizedResponse() { + ErrorResponse error = new ErrorResponse(); + error.set("Authentication required"); + return new ResponseEntity<>(error, HttpStatus.UNAUTHORIZED); + } + + private ResponseEntity notFoundResponse(String message) { + ErrorResponse error = new ErrorResponse(); + error.set(message); + return new ResponseEntity<>(error, HttpStatus.NOT_FOUND); + } + + private ResponseEntity badRequestResponse(String message) { + ErrorResponse error = new ErrorResponse(); + error.set(message); + return new ResponseEntity<>(error, HttpStatus.BAD_REQUEST); + } + + private ResponseEntity forbiddenResponse(String message) { + ErrorResponse error = new ErrorResponse(); + error.set(message); + return new ResponseEntity<>(error, HttpStatus.FORBIDDEN); + } + + private void setAuthorInfo(Post post) { + User user = post.getUser(); + if (user != null && user.getCohort() != null) { + Profile profile = this.profileRepository.findById(user.getId()).orElse(null); + if (profile != null) { + Author author = new Author(user.getId(), user.getCohort().getId(), + profile.getFirstName(), profile.getLastName(), user.getEmail(), + profile.getBio(), profile.getGithubUrl()); + post.setAuthor(author); + } + } + } @GetMapping public ResponseEntity getAllPosts() { + List posts = this.postRepository.findAll(); + posts.forEach(this::setAuthorInfo); + PostListResponse postListResponse = new PostListResponse(); - User user = this.userRepository.findById(1).orElse(null); - if (user == null) { - ErrorResponse error = new ErrorResponse(); - error.set("not found"); - return new ResponseEntity<>(error, HttpStatus.NOT_FOUND); - } - Profile profile = this.profileRepository.findById(user.getId()).orElse(null); - if (profile == null) { - ErrorResponse error = new ErrorResponse(); - error.set("not found"); - return new ResponseEntity<>(error, HttpStatus.NOT_FOUND); - } - Author author = new Author(user.getId(), user.getCohort().getId(), profile.getFirstName(), - profile.getLastName(), user.getEmail(), profile.getBio(), profile.getGithubUrl()); - List posts = new ArrayList<>(); - Post post1 = this.postRepository.findById(1).orElse(null); - if (post1 == null){ - ErrorResponse error = new ErrorResponse(); - error.set("not found"); - return new ResponseEntity<>(error, HttpStatus.NOT_FOUND); - } - post1.setAuthor(author); - post1.setContent("Hello world!!"); - posts.add(post1); - Post post2 = this.postRepository.findById(2).orElse(null); - if (post2 == null){ - ErrorResponse error = new ErrorResponse(); - error.set("not found"); - return new ResponseEntity<>(error, HttpStatus.NOT_FOUND); - } - post2.setAuthor(author); - post2.setContent("Hello from the void!!"); - posts.add(post2); postListResponse.set(posts); return ResponseEntity.ok(postListResponse); } + @PostMapping + public ResponseEntity createPost(@RequestBody PostRequest postRequest) { + User currentUser = getCurrentAuthenticatedUser(); + if (currentUser == null) return unauthorizedResponse(); + + Post post = new Post(postRequest.getContent(), currentUser, 0); + Post savedPost = this.postRepository.save(post); + setAuthorInfo(savedPost); + + PostResponse postResponse = new PostResponse(); + postResponse.set(savedPost); + return new ResponseEntity<>(postResponse, HttpStatus.CREATED); + } + @GetMapping("/{id}") public ResponseEntity getPostById(@PathVariable int id) { - ErrorResponse error = new ErrorResponse(); - error.set("not found"); - return new ResponseEntity<>(error, HttpStatus.NOT_FOUND); + Post post = this.postRepository.findById(id).orElse(null); + if (post == null) return notFoundResponse("Post not found"); + + setAuthorInfo(post); + PostResponse postResponse = new PostResponse(); + postResponse.set(post); + return ResponseEntity.ok(postResponse); + } @PostMapping("/{postId}/comments") + public ResponseEntity addCommentToPost(@PathVariable int postId, @RequestBody CommentRequest commentRequest) { + Post post = this.postRepository.findById(postId).orElse(null); + if (post == null) return notFoundResponse("Post not found"); + + User user = this.userRepository.findById(commentRequest.getUserId()).orElse(null); + if (user == null) return notFoundResponse("User not found"); + + Comment comment = new Comment(commentRequest.getBody(), user, post); + Comment savedComment = this.commentRepository.save(comment); + + CommentResponse commentResponse = new CommentResponse(); + commentResponse.set(savedComment); + return new ResponseEntity<>(commentResponse, HttpStatus.CREATED); + } + + @GetMapping("/{postId}/comments") + public ResponseEntity getCommentsForPost(@PathVariable int postId) { + Post post = this.postRepository.findById(postId).orElse(null); + if (post == null) return notFoundResponse("Post not found"); + + PostResponse postResponse = new PostResponse(); + postResponse.set(post); + return ResponseEntity.ok(postResponse); + } + + @GetMapping("/{postId}/comments/{commentId}") + public ResponseEntity getCommentById(@PathVariable int postId, @PathVariable int commentId) { + Post post = this.postRepository.findById(postId).orElse(null); + if (post == null) return notFoundResponse("Post not found"); + + Comment comment = this.commentRepository.findById(commentId).orElse(null); + if (comment == null) return notFoundResponse("Comment not found"); + + if (comment.getPost().getId() != postId) + return badRequestResponse("Comment does not belong to the specified post"); + + CommentResponse commentResponse = new CommentResponse(); + commentResponse.set(comment); + return ResponseEntity.ok(commentResponse); + } + + @PutMapping("/{postId}/comments/{commentId}") + public ResponseEntity updateComment(@PathVariable int postId, @PathVariable int commentId, @RequestBody CommentRequest commentRequest) { + User currentUser = getCurrentAuthenticatedUser(); + if (currentUser == null) return unauthorizedResponse(); + + Post post = this.postRepository.findById(postId).orElse(null); + if (post == null) return notFoundResponse("Post not found"); + + Comment comment = this.commentRepository.findById(commentId).orElse(null); + if (comment == null) return notFoundResponse("Comment not found"); + + if (comment.getPost().getId() != postId) + return badRequestResponse("Comment does not belong to the specified post"); + + if (comment.getUser().getId() != currentUser.getId()) + return forbiddenResponse("You can only edit your own comments"); + + comment.setBody(commentRequest.getBody()); + Comment updatedComment = this.commentRepository.save(comment); + + CommentResponse commentResponse = new CommentResponse(); + commentResponse.set(updatedComment); + return ResponseEntity.ok(commentResponse); + } + + @DeleteMapping("/{postId}/comments/{commentId}") + public ResponseEntity deleteComment(@PathVariable int postId, @PathVariable int commentId) { + User currentUser = getCurrentAuthenticatedUser(); + if (currentUser == null) return unauthorizedResponse(); + + Post post = this.postRepository.findById(postId).orElse(null); + if (post == null) return notFoundResponse("Post not found"); + + Comment comment = this.commentRepository.findById(commentId).orElse(null); + if (comment == null) return notFoundResponse("Comment not found"); + + if (comment.getPost().getId() != postId) + return badRequestResponse("Comment does not belong to the specified post"); + + if (comment.getUser().getId() != currentUser.getId()) + return forbiddenResponse("You can only delete your own comments"); + + this.commentRepository.delete(comment); + + ErrorResponse success = new ErrorResponse(); + success.set("Comment deleted successfully"); + return ResponseEntity.ok(success); + } + + @PostMapping("/{postId}/like") + public ResponseEntity likePost(@PathVariable int postId) { + User currentUser = getCurrentAuthenticatedUser(); + if (currentUser == null) return unauthorizedResponse(); + + Post post = this.postRepository.findById(postId).orElse(null); + if (post == null) return notFoundResponse("Post not found"); + + post.setLikes(post.getLikes() + 1); + Post updatedPost = this.postRepository.save(post); + setAuthorInfo(updatedPost); + + PostResponse postResponse = new PostResponse(); + postResponse.set(updatedPost); + return ResponseEntity.ok(postResponse); + } + + @DeleteMapping("/{postId}/like") + public ResponseEntity unlikePost(@PathVariable int postId) { + User currentUser = getCurrentAuthenticatedUser(); + if (currentUser == null) return unauthorizedResponse(); + + Post post = this.postRepository.findById(postId).orElse(null); + if (post == null) return notFoundResponse("Post not found"); + + post.setLikes(Math.max(0, post.getLikes() - 1)); + Post updatedPost = this.postRepository.save(post); + setAuthorInfo(updatedPost); + + PostResponse postResponse = new PostResponse(); + postResponse.set(updatedPost); + return ResponseEntity.ok(postResponse); } } diff --git a/src/main/java/com/booleanuk/cohorts/controllers/ProfileController.java b/src/main/java/com/booleanuk/cohorts/controllers/ProfileController.java new file mode 100644 index 0000000..cc9510f --- /dev/null +++ b/src/main/java/com/booleanuk/cohorts/controllers/ProfileController.java @@ -0,0 +1,118 @@ +package com.booleanuk.cohorts.controllers; + +import com.booleanuk.cohorts.models.*; +import com.booleanuk.cohorts.repository.CohortRepository; +import com.booleanuk.cohorts.repository.ProfileRepository; +import com.booleanuk.cohorts.repository.RoleRepository; +import com.booleanuk.cohorts.repository.UserRepository; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.cglib.core.Local; +import org.springframework.dao.DataIntegrityViolationException; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.MethodArgumentNotValidException; +import org.springframework.web.bind.annotation.*; + +import java.lang.annotation.Target; +import java.time.LocalDate; +import java.time.format.DateTimeParseException; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; + +import static java.lang.Integer.parseInt; + +@CrossOrigin(origins = "*", maxAge = 3600) +@RestController +@RequestMapping("profiles") +public class ProfileController { + @Autowired + private ProfileRepository profileRepository; + + @Autowired + private CohortRepository cohortRepository; + + @Autowired + private RoleRepository roleRepository; + + @Autowired + private UserRepository userRepository; + + record PostProfile( + int userId, + String first_name, + String last_name, + String username, + String mobile, + //String github_username, + String bio, + String role, + String specialism, + int cohort, + String start_date, + String end_date, + String photo + ){} + + @GetMapping + public List createProfile() { + return profileRepository.findAll(); + } + @PostMapping + public ResponseEntity createProfile(@RequestBody PostProfile profile) { + + if(profile.first_name == null || profile.first_name == "" || profile.last_name == null || profile.last_name == ""){ + return new ResponseEntity<>("First and last name can't be empty or NULL. First name: " + profile.first_name + " Last name: " + profile.last_name, HttpStatus.BAD_REQUEST); + } + + Optional optionalUser = userRepository.findById(profile.userId); + if (optionalUser.isEmpty()) { + return new ResponseEntity<>("User for id "+ profile.userId + " not found", HttpStatus.BAD_REQUEST); + } + + User user = optionalUser.get(); + + Optional optionalRole = roleRepository.findByName(ERole.valueOf(profile.role)); + if (optionalRole.isEmpty()) { + return new ResponseEntity<>("Role for id "+ profile.role + " not found", HttpStatus.BAD_REQUEST); + } + + Role role = optionalRole.get(); + + Optional optionalCohort = cohortRepository.findById(profile.cohort); + if (optionalCohort.isEmpty()) { + return new ResponseEntity<>("Cohort for id "+ profile.cohort + " not found", HttpStatus.BAD_REQUEST); + } + + Cohort cohort = optionalCohort.get(); + + Profile newProfile = null; + try { + newProfile = new Profile( + user, + profile.first_name, + profile.last_name, + profile.username, + "https://github.com/" + profile.username, + profile.mobile, + profile.bio, + role, + profile.specialism, + cohort, + LocalDate.parse(profile.start_date), + LocalDate.parse(profile.end_date), + profile.photo + ); + } catch (DateTimeParseException e) { + return new ResponseEntity<>("Wrong formatting for start_date or end_date. Plese use the following format: 2025-09-14", + HttpStatus.BAD_REQUEST); + } + + try { + return new ResponseEntity<>(profileRepository.save(newProfile), HttpStatus.OK); + } catch (DataIntegrityViolationException e) { + return new ResponseEntity<>("User has an existing profile", HttpStatus.BAD_REQUEST); + } + } +} diff --git a/src/main/java/com/booleanuk/cohorts/controllers/UserController.java b/src/main/java/com/booleanuk/cohorts/controllers/UserController.java index 1261544..0c621a9 100644 --- a/src/main/java/com/booleanuk/cohorts/controllers/UserController.java +++ b/src/main/java/com/booleanuk/cohorts/controllers/UserController.java @@ -1,16 +1,22 @@ package com.booleanuk.cohorts.controllers; +import com.booleanuk.cohorts.models.Cohort; +import com.booleanuk.cohorts.models.Profile; import com.booleanuk.cohorts.models.User; import com.booleanuk.cohorts.payload.response.ErrorResponse; import com.booleanuk.cohorts.payload.response.Response; import com.booleanuk.cohorts.payload.response.UserListResponse; import com.booleanuk.cohorts.payload.response.UserResponse; +import com.booleanuk.cohorts.repository.ProfileRepository; import com.booleanuk.cohorts.repository.UserRepository; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.dao.DataIntegrityViolationException; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; +import static java.util.Arrays.stream; + @CrossOrigin(origins = "*", maxAge = 3600) @RestController @RequestMapping("users") @@ -18,6 +24,9 @@ public class UserController { @Autowired private UserRepository userRepository; + @Autowired + private ProfileRepository profileRepository; + @GetMapping public ResponseEntity getAllUsers() { UserListResponse userListResponse = new UserListResponse(); @@ -38,6 +47,35 @@ public ResponseEntity getUserById(@PathVariable int id) { return ResponseEntity.ok(userResponse); } + @PatchMapping("{id}") + public ResponseEntity updateUserWithProfile(@PathVariable int id) { + int profileId = profileRepository.findAll().stream().filter(it -> it.getUser().getId() == id).toList().getFirst().getId(); + Profile profile = profileRepository.findById(profileId).orElse(null); + if (profile == null){ + return new ResponseEntity<>("Could not add profile to user because the profile does not exist", HttpStatus.NOT_FOUND); + } + User user = userRepository.findById(id).orElse(null); + + if (user == null) { + return new ResponseEntity<>("User not found", HttpStatus.NOT_FOUND); + } + + if (user.getProfile() != null) { + return new ResponseEntity<>("A profile is already registered on this user", HttpStatus.BAD_REQUEST); + } + + Cohort cohort = profile.getCohort(); + + user.setProfile(profile); + user.setCohort(cohort); + try { + return new ResponseEntity<>(userRepository.save(user), HttpStatus.OK); + } catch (DataIntegrityViolationException e) { + return new ResponseEntity<>("User has an existing profile", HttpStatus.BAD_REQUEST); + } + + } + @PostMapping public void registerUser() { System.out.println("Register endpoint hit"); diff --git a/src/main/java/com/booleanuk/cohorts/models/Author.java b/src/main/java/com/booleanuk/cohorts/models/Author.java index 89aa218..dc66655 100644 --- a/src/main/java/com/booleanuk/cohorts/models/Author.java +++ b/src/main/java/com/booleanuk/cohorts/models/Author.java @@ -12,18 +12,18 @@ public class Author { private int id; private int cohortId; - private String firstName; - private String lastName; + private String first_name; + private String last_name; private String email; private String bio; private String githubUrl; private String role; - public Author(int id, int cohortId, String firstName, String lastName, String email, String bio, String githubUrl) { + public Author(int id, int cohortId, String first_name, String last_name, String email, String bio, String githubUrl) { this.id = id; this.cohortId = cohortId; - this.firstName = firstName; - this.lastName = lastName; + this.first_name = first_name; + this.last_name = last_name; this.email = email; this.bio = bio; this.githubUrl = githubUrl; diff --git a/src/main/java/com/booleanuk/cohorts/models/Cohort.java b/src/main/java/com/booleanuk/cohorts/models/Cohort.java index 2972906..6b2f1eb 100644 --- a/src/main/java/com/booleanuk/cohorts/models/Cohort.java +++ b/src/main/java/com/booleanuk/cohorts/models/Cohort.java @@ -1,9 +1,12 @@ package com.booleanuk.cohorts.models; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import jakarta.persistence.*; import lombok.Data; import lombok.NoArgsConstructor; +import java.util.List; + @NoArgsConstructor @Data @Entity @@ -13,7 +16,27 @@ public class Cohort { @GeneratedValue(strategy = GenerationType.IDENTITY) private int id; + + @ManyToMany + @JsonIgnoreProperties("cohorts") + @JoinTable(name = "cohort_course", + joinColumns = @JoinColumn(name = "cohort_id"), + inverseJoinColumns = @JoinColumn(name = "course_id") + ) + private List cohort_courses; + + @OneToMany(mappedBy = "cohort", fetch = FetchType.LAZY) + @JsonIgnoreProperties("users") + private List users; + + public Cohort(int id) { this.id = id; } + + @Override + public String toString(){ + + return "Cohort Id: " + this.id; + } } diff --git a/src/main/java/com/booleanuk/cohorts/models/Comment.java b/src/main/java/com/booleanuk/cohorts/models/Comment.java new file mode 100644 index 0000000..6e6fbe5 --- /dev/null +++ b/src/main/java/com/booleanuk/cohorts/models/Comment.java @@ -0,0 +1,45 @@ +package com.booleanuk.cohorts.models; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.Table; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@NoArgsConstructor +@AllArgsConstructor +@Data +@Entity +@Table(name = "comments") +public class Comment { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private int id; + + @Column(nullable = false) + private String body; + + @ManyToOne + @JoinColumn(name = "user_id", nullable = false) + @JsonIgnoreProperties("comments") + private User user; + + @ManyToOne + @JoinColumn(name = "post_id", nullable = false) + @JsonIgnoreProperties("comments") + private Post post; + + public Comment(String body, User user, Post post) { + this.body = body; + this.user = user; + this.post = post; + } +} diff --git a/src/main/java/com/booleanuk/cohorts/models/Course.java b/src/main/java/com/booleanuk/cohorts/models/Course.java new file mode 100644 index 0000000..ffc559a --- /dev/null +++ b/src/main/java/com/booleanuk/cohorts/models/Course.java @@ -0,0 +1,39 @@ +package com.booleanuk.cohorts.models; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import jakarta.persistence.*; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.List; + + +@NoArgsConstructor +@Data +@Entity +@Table(name = "courses") +public class Course { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private int id; + + @Column + private String name; + + @JsonIgnoreProperties("cohort_courses") + @ManyToMany(mappedBy = "cohort_courses") + private List cohorts; + + public Course(String name) { + this.id = id; + this.name = name; + } + + @Override + public String toString(){ + + return "Course Name: " + this.name; + } + +} diff --git a/src/main/java/com/booleanuk/cohorts/models/Post.java b/src/main/java/com/booleanuk/cohorts/models/Post.java index 3d879ba..d4be7b5 100644 --- a/src/main/java/com/booleanuk/cohorts/models/Post.java +++ b/src/main/java/com/booleanuk/cohorts/models/Post.java @@ -1,7 +1,21 @@ package com.booleanuk.cohorts.models; +import java.util.List; + import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import jakarta.persistence.*; + +import jakarta.persistence.CascadeType; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.OneToMany; +import jakarta.persistence.Table; +import jakarta.persistence.Transient; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; @@ -19,27 +33,48 @@ public class Post { @Column(nullable = false) private String content; + @Column(nullable = false, columnDefinition = "int default 0") + private int likes = 0; + @ManyToOne @JoinColumn(name = "user_id", nullable = false) @JsonIgnoreProperties("users") private User user; + @OneToMany(mappedBy = "post", cascade = CascadeType.ALL, fetch = FetchType.LAZY) + @JsonIgnoreProperties("post") + private List comments; + @Transient private Author author; public Post(int id) { this.id = id; + this.likes = 0; } public Post(User user, String content) { this.user = user; this.content = content; + this.likes = 0; } public Post(Author author, String content) { this.author = author; this.content = content; + this.likes = 0; } + public Post(String content, User user, List comments) { + this.content = content; + this.user = user; + this.comments = comments; + this.likes = 0; + } + public Post(String content, User user, int likes) { + this.content = content; + this.user = user; + this.likes = likes; + } } diff --git a/src/main/java/com/booleanuk/cohorts/models/Profile.java b/src/main/java/com/booleanuk/cohorts/models/Profile.java index 4305d80..6ea0d25 100644 --- a/src/main/java/com/booleanuk/cohorts/models/Profile.java +++ b/src/main/java/com/booleanuk/cohorts/models/Profile.java @@ -1,9 +1,16 @@ package com.booleanuk.cohorts.models; +import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import jakarta.persistence.*; +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Pattern; import lombok.Data; import lombok.NoArgsConstructor; +import org.hibernate.engine.internal.Cascade; + +import java.time.LocalDate; @NoArgsConstructor @Data @@ -14,32 +21,75 @@ public class Profile { @GeneratedValue(strategy = GenerationType.IDENTITY) private int id; - @OneToOne + @OneToOne(cascade = CascadeType.ALL) @JoinColumn(name = "user_id", nullable = false) - @JsonIgnoreProperties("users") + @JsonIgnoreProperties("profile") private User user; + @NotNull(message = "First name is mandatory") + @NotEmpty(message = "First name cannot be empty") + @Pattern(regexp = "^[a-zA-Z\\s]+$", message = "First name can only contain letters and spaces") @Column private String firstName; + @NotNull(message = "Last name is mandatory") + @NotEmpty(message = "Last name cannot be empty") + @Pattern(regexp = "^[a-zA-Z\\s]+$", message = "Last name can only contain letters and spaces") @Column private String lastName; + @Column + private String username; + @Column private String bio; @Column private String githubUrl; + @Column + private String mobile; + + @Column + private String specialism; + + @Column + private LocalDate startDate; + + @Column + private LocalDate endDate; + + @ManyToOne + @JoinColumn(name = "cohort_id") + @JsonIgnore + private Cohort cohort; + + @ManyToOne + @JoinColumn(name = "role_id") + @JsonIgnoreProperties("cohort") + private Role role; + + @Column + private String photo; + public Profile(int id) { this.id = id; } - public Profile(User user, String firstName, String lastName, String bio, String githubUrl) { + public Profile(User user, String firstName, String lastName, String username, String githubUrl, String mobile, + String bio, Role role, String specialism, Cohort cohort, LocalDate startDate, LocalDate endDate, String photo) { this.user = user; this.firstName = firstName; this.lastName = lastName; - this.bio = bio; + this.username = username; this.githubUrl = githubUrl; + this.mobile = mobile; + this.bio = bio; + this.role = role; + this.specialism = specialism; + this.cohort = cohort; + this.startDate = startDate; + this.endDate = endDate; + this.photo = photo; } } diff --git a/src/main/java/com/booleanuk/cohorts/models/User.java b/src/main/java/com/booleanuk/cohorts/models/User.java index 9b27170..107ae75 100644 --- a/src/main/java/com/booleanuk/cohorts/models/User.java +++ b/src/main/java/com/booleanuk/cohorts/models/User.java @@ -30,6 +30,7 @@ public class User { // The AuthController uses a built-in class for Users that expects a Username, we don't use it elsewhere in the code. @Transient + @Size(min = 7, max = 50, message = "Username must be between 7 and 50 characters") private String username = this.email; @NotBlank @@ -42,9 +43,15 @@ public class User { @ManyToOne @JoinColumn(name = "cohort_id", nullable = true) - @JsonIgnoreProperties("users") + @JsonIgnoreProperties({"users","cohort_courses"}) private Cohort cohort; + + @OneToOne(cascade = CascadeType.ALL) + @JoinColumn(name = "profile_id") + @JsonIgnoreProperties("user") + private Profile profile; + public User(String email, String password) { this.email = email; this.password = password; @@ -55,4 +62,10 @@ public User(String email, String password, Cohort cohort) { this.password = password; this.cohort = cohort; } + public User(String email, String password, Cohort cohort, Profile profile) { + this.email = email; + this.password = password; + this.cohort = cohort; + this.profile = profile; + } } diff --git a/src/main/java/com/booleanuk/cohorts/payload/request/CommentRequest.java b/src/main/java/com/booleanuk/cohorts/payload/request/CommentRequest.java new file mode 100644 index 0000000..d3bbab2 --- /dev/null +++ b/src/main/java/com/booleanuk/cohorts/payload/request/CommentRequest.java @@ -0,0 +1,33 @@ +package com.booleanuk.cohorts.payload.request; + +import jakarta.validation.constraints.NotBlank; + +public class CommentRequest { + @NotBlank + private String body; + + private int userId; + + public CommentRequest() {} + + public CommentRequest(String body, int userId) { + this.body = body; + this.userId = userId; + } + + public String getBody() { + return body; + } + + public void setBody(String body) { + this.body = body; + } + + public int getUserId() { + return userId; + } + + public void setUserId(int userId) { + this.userId = userId; + } +} diff --git a/src/main/java/com/booleanuk/cohorts/payload/request/CourseRequest.java b/src/main/java/com/booleanuk/cohorts/payload/request/CourseRequest.java new file mode 100644 index 0000000..d9f90c2 --- /dev/null +++ b/src/main/java/com/booleanuk/cohorts/payload/request/CourseRequest.java @@ -0,0 +1,18 @@ +package com.booleanuk.cohorts.payload.request; + +import jakarta.validation.constraints.NotBlank; + +public class CourseRequest { + @NotBlank + private String name; + + public CourseRequest() {} + + public CourseRequest(String name){ + this.name = name; + } + + public String getName() { return name; } + + +} diff --git a/src/main/java/com/booleanuk/cohorts/payload/request/PostRequest.java b/src/main/java/com/booleanuk/cohorts/payload/request/PostRequest.java new file mode 100644 index 0000000..052d54f --- /dev/null +++ b/src/main/java/com/booleanuk/cohorts/payload/request/PostRequest.java @@ -0,0 +1,33 @@ +package com.booleanuk.cohorts.payload.request; + +import jakarta.validation.constraints.NotBlank; + +public class PostRequest { + @NotBlank + private String content; + + private int userId; + + public PostRequest() {} + + public PostRequest(String content, int userId) { + this.content = content; + this.userId = userId; + } + + public String getContent() { + return content; + } + + public void setContent(String content) { + this.content = content; + } + + public int getUserId() { + return userId; + } + + public void setUserId(int userId) { + this.userId = userId; + } +} diff --git a/src/main/java/com/booleanuk/cohorts/payload/response/CommentData.java b/src/main/java/com/booleanuk/cohorts/payload/response/CommentData.java new file mode 100644 index 0000000..e76b4aa --- /dev/null +++ b/src/main/java/com/booleanuk/cohorts/payload/response/CommentData.java @@ -0,0 +1,15 @@ +package com.booleanuk.cohorts.payload.response; + +import com.booleanuk.cohorts.models.Comment; + +import lombok.Getter; + +@Getter +public class CommentData extends Data { + protected Comment comment; + + @Override + public void set(Comment comment) { + this.comment = comment; + } +} diff --git a/src/main/java/com/booleanuk/cohorts/payload/response/CommentResponse.java b/src/main/java/com/booleanuk/cohorts/payload/response/CommentResponse.java new file mode 100644 index 0000000..ca93871 --- /dev/null +++ b/src/main/java/com/booleanuk/cohorts/payload/response/CommentResponse.java @@ -0,0 +1,15 @@ +package com.booleanuk.cohorts.payload.response; + +import com.booleanuk.cohorts.models.Comment; + +import lombok.Getter; + +@Getter +public class CommentResponse extends Response { + + public void set(Comment comment) { + Data data = new CommentData(); + data.set(comment); + super.set(data); + } +} diff --git a/src/main/java/com/booleanuk/cohorts/payload/response/CourseData.java b/src/main/java/com/booleanuk/cohorts/payload/response/CourseData.java new file mode 100644 index 0000000..ff08d50 --- /dev/null +++ b/src/main/java/com/booleanuk/cohorts/payload/response/CourseData.java @@ -0,0 +1,15 @@ +package com.booleanuk.cohorts.payload.response; + +import com.booleanuk.cohorts.models.Course; +import lombok.Getter; + + +@Getter +public class CourseData extends Data { + protected Course course; + + @Override + public void set(Course course) { + this.course = course; + } +} diff --git a/src/main/java/com/booleanuk/cohorts/payload/response/CourseListData.java b/src/main/java/com/booleanuk/cohorts/payload/response/CourseListData.java new file mode 100644 index 0000000..2f46feb --- /dev/null +++ b/src/main/java/com/booleanuk/cohorts/payload/response/CourseListData.java @@ -0,0 +1,19 @@ +package com.booleanuk.cohorts.payload.response; + +import com.booleanuk.cohorts.models.Course; +import com.booleanuk.cohorts.models.Post; +import lombok.Getter; + +import java.util.List; + +@Getter +public class CourseListData extends Data> { + + protected List courses; + + @Override + public void set(List courseList) { + this.courses = courseList; + } + +} \ No newline at end of file diff --git a/src/main/java/com/booleanuk/cohorts/payload/response/CourseListResponse.java b/src/main/java/com/booleanuk/cohorts/payload/response/CourseListResponse.java new file mode 100644 index 0000000..4486843 --- /dev/null +++ b/src/main/java/com/booleanuk/cohorts/payload/response/CourseListResponse.java @@ -0,0 +1,16 @@ +package com.booleanuk.cohorts.payload.response; + +import com.booleanuk.cohorts.models.Course; +import com.booleanuk.cohorts.models.Post; +import lombok.Getter; + +import java.util.List; + +@Getter +public class CourseListResponse extends Response { + public void set(List courses) { + Data> data = new CourseListData(); + data.set(courses); + super.set(data); + } +} diff --git a/src/main/java/com/booleanuk/cohorts/payload/response/CourseResponse.java b/src/main/java/com/booleanuk/cohorts/payload/response/CourseResponse.java new file mode 100644 index 0000000..d18f643 --- /dev/null +++ b/src/main/java/com/booleanuk/cohorts/payload/response/CourseResponse.java @@ -0,0 +1,14 @@ +package com.booleanuk.cohorts.payload.response; + +import com.booleanuk.cohorts.models.Cohort; +import com.booleanuk.cohorts.models.Course; +import lombok.Getter; + +@Getter +public class CourseResponse extends Response{ + public void set(Course course){ + Data data = new CourseData(); + data.set(course); + super.set(data); + } +} \ No newline at end of file diff --git a/src/main/java/com/booleanuk/cohorts/repository/CommentRepository.java b/src/main/java/com/booleanuk/cohorts/repository/CommentRepository.java new file mode 100644 index 0000000..70d051b --- /dev/null +++ b/src/main/java/com/booleanuk/cohorts/repository/CommentRepository.java @@ -0,0 +1,8 @@ +package com.booleanuk.cohorts.repository; + +import org.springframework.data.jpa.repository.JpaRepository; + +import com.booleanuk.cohorts.models.Comment; + +public interface CommentRepository extends JpaRepository { +} diff --git a/src/main/java/com/booleanuk/cohorts/repository/CourseRepository.java b/src/main/java/com/booleanuk/cohorts/repository/CourseRepository.java new file mode 100644 index 0000000..6a0321e --- /dev/null +++ b/src/main/java/com/booleanuk/cohorts/repository/CourseRepository.java @@ -0,0 +1,7 @@ +package com.booleanuk.cohorts.repository; + +import com.booleanuk.cohorts.models.Course; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface CourseRepository extends JpaRepository { +} diff --git a/src/main/java/com/booleanuk/cohorts/repository/ProfileRepository.java b/src/main/java/com/booleanuk/cohorts/repository/ProfileRepository.java index 4ded968..4064ea8 100644 --- a/src/main/java/com/booleanuk/cohorts/repository/ProfileRepository.java +++ b/src/main/java/com/booleanuk/cohorts/repository/ProfileRepository.java @@ -1,6 +1,7 @@ package com.booleanuk.cohorts.repository; import com.booleanuk.cohorts.models.Profile; +import com.booleanuk.cohorts.models.User; import org.springframework.data.jpa.repository.JpaRepository; public interface ProfileRepository extends JpaRepository { diff --git a/src/main/java/com/booleanuk/cohorts/security/WebSecurityConfig.java b/src/main/java/com/booleanuk/cohorts/security/WebSecurityConfig.java index ad90f26..8043723 100644 --- a/src/main/java/com/booleanuk/cohorts/security/WebSecurityConfig.java +++ b/src/main/java/com/booleanuk/cohorts/security/WebSecurityConfig.java @@ -58,9 +58,11 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { .authorizeHttpRequests((requests) -> requests .requestMatchers("/login", "/login/**").permitAll() .requestMatchers("/signup", "/signup/**").permitAll() + .requestMatchers("/profiles", "/profiles/**").authenticated() .requestMatchers("/users", "/users/**").authenticated() .requestMatchers("/posts", "/posts/**").authenticated() .requestMatchers("/cohorts", "/cohorts/**").authenticated() + .requestMatchers("/courses", "/courses/**").authenticated() .requestMatchers("/logs", "/logs/**").authenticated() .requestMatchers("/").authenticated() ); diff --git a/src/main/java/com/booleanuk/cohorts/security/jwt/JwtUtils.java b/src/main/java/com/booleanuk/cohorts/security/jwt/JwtUtils.java index 33ef314..8f4ce40 100644 --- a/src/main/java/com/booleanuk/cohorts/security/jwt/JwtUtils.java +++ b/src/main/java/com/booleanuk/cohorts/security/jwt/JwtUtils.java @@ -31,6 +31,7 @@ public String generateJwtToken(Authentication authentication) { return Jwts.builder() .subject((userPrincipal.getUsername())) + .claim("userId", userPrincipal.getId()) .issuedAt(new Date()) .expiration(new Date((new Date()).getTime() + this.jwtExpirationMs)) .signWith(this.key()) diff --git a/src/main/resources/application.yml.example b/src/main/resources/application.yml.example deleted file mode 100644 index 23db90f..0000000 --- a/src/main/resources/application.yml.example +++ /dev/null @@ -1,27 +0,0 @@ -server: - port: 4000 - error: - include-message: always - include-binding-errors: always - include-stacktrace: never - include-exception: false - -spring: - datasource: - url: jdbc:postgresql://:5432/ - username: - password: - max-active: 3 - max-idle: 3 - jpa: - hibernate: - ddl-auto: update - properties: - hibernate: - format_sql: true - show-sql: true - -booleanuk: - app: - jwtSecret: - jwtExpirationMs: 86400000 \ No newline at end of file