diff --git a/.github/ISSUE_TEMPLATE/bug.md b/.github/ISSUE_TEMPLATE/bug.md new file mode 100644 index 0000000..c0ac7b3 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug.md @@ -0,0 +1,27 @@ +--- +name: "🐞 Bug Fix" +about: "Report a bug or something that is not working as expected" +title: "[FIX] " +labels: bug +assignees: "" +--- + +## 🐛 Bug Description +A clear and concise description of what the bug is. + +## 🔄 Steps to Reproduce +Steps to reproduce the behavior: +1. Go to ... +2. Click on ... +3. See error ... + +## ✅ Expected Behavior +What should have happened instead? + +## 📸 Screenshots / Logs +If applicable, add screenshots or error messages. + +## 🖥️ Environment +- OS: +- Version: +- Other relevant info: diff --git a/.github/ISSUE_TEMPLATE/feature.md b/.github/ISSUE_TEMPLATE/feature.md new file mode 100644 index 0000000..647c8c4 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature.md @@ -0,0 +1,19 @@ +--- +name: "✨ Feature Request" +about: "Suggest a new feature or improvement" +title: "[FEATURE] " +labels: enhancement +assignees: "" +--- + +## ✨ Description +What would you like to add or improve? + +## 🎯 Use Case +Why is this useful? Who benefits from it? + +## 💡 Proposed Solution +How could this be implemented? (optional) + +## 📎 Additional Info +Anything else you’d like to share? (related issues, links, references, etc.) diff --git a/.github/ISSUE_TEMPLATE/other.md b/.github/ISSUE_TEMPLATE/other.md new file mode 100644 index 0000000..8846635 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/other.md @@ -0,0 +1,16 @@ +--- +name: "📌 Other" +about: "Questions, discussions, or anything else" +title: "[OTHER] " +labels: question +assignees: "" +--- + +## 📌 Description +What is this about? + +## 🌍 Context +Why is this relevant? + +## 📎 Additional Info +Anything else that might be useful. diff --git a/.github/workflows/gradle.yml b/.github/workflows/gradle.yml new file mode 100644 index 0000000..3aecabc --- /dev/null +++ b/.github/workflows/gradle.yml @@ -0,0 +1,43 @@ +# This workflow uses actions that are not certified by GitHub. +# They are provided by a third-party and are governed by +# separate terms of service, privacy policy, and support +# documentation. +# This workflow will build a Java project with Gradle and cache/restore any dependencies to improve the workflow execution time +# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-java-with-gradle + +name: Java CI with Gradle + +on: + push: + branches: [ "main" ] + pull_request: + branches: [ "main" ] + +jobs: + build: + + runs-on: ubuntu-latest + permissions: + contents: read + + steps: + - uses: actions/checkout@v4 + - name: Set up JDK 17 + uses: actions/setup-java@v4 + with: + java-version: '17' + distribution: 'temurin' + + # Configure Gradle for optimal use in GitHub Actions, including caching of downloaded dependencies. + # See: https://github.com/gradle/actions/blob/main/setup-gradle/README.md + - name: Setup Gradle + uses: gradle/actions/setup-gradle@af1da67850ed9a4cedd57bfd976089dd991e2582 # v4.0.0 + + - name: Build with Gradle Wrapper + run: ./gradlew build --exclude-task test + + - name: Upload build artifacts + uses: actions/upload-artifact@v4 + with: + name: Package + path: build/libs diff --git a/.gitignore b/.gitignore index b3db3e2..72a5930 100644 --- a/.gitignore +++ b/.gitignore @@ -36,4 +36,7 @@ out/ ### VS Code ### .vscode/ +### Project specific application.yml +build +.DS_Store \ No newline at end of file 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/image.png b/image.png new file mode 100644 index 0000000..b9c411b Binary files /dev/null and b/image.png differ diff --git a/src/main/java/com/booleanuk/Main.java b/src/main/java/com/booleanuk/Main.java index d1fae35..092e9fe 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; @@ -32,6 +33,13 @@ public static void main(String[] args) { @Override public void run(String... args) { + // Create a cohort. + Cohort cohort; + if (!this.cohortRepository.existsById(1)) { + cohort = this.cohortRepository.save(new Cohort()); + } else { + cohort = this.cohortRepository.findById(1).orElse(null); + } Role teacherRole; if (!this.roleRepository.existsByName(ERole.ROLE_TEACHER)) { teacherRole = this.roleRepository.save(new Role(ERole.ROLE_TEACHER)); @@ -51,49 +59,5 @@ public void run(String... args) { if (!this.roleRepository.existsByName(ERole.ROLE_ADMIN)) { this.roleRepository.save(new Role(ERole.ROLE_ADMIN)); } - // Create a cohort. - Cohort cohort; - if (!this.cohortRepository.existsById(1)) { - cohort = this.cohortRepository.save(new Cohort()); - } else { - cohort = this.cohortRepository.findById(1).orElse(null); - } - // Create some users - User studentUser; - if (!this.userRepository.existsById(1)) { - studentUser = new User("student@test.com", this.encoder.encode("Testpassword1!"), cohort); - studentUser.setRoles(studentRoles); - studentUser = this.userRepository.save(studentUser); - } else { - studentUser = this.userRepository.findById(1).orElse(null); - } - Profile studentProfile; - if (!this.profileRepository.existsById(1)) { - studentProfile = this.profileRepository.save(new Profile(studentUser, "Joe", "Bloggs", "Hello world!", "student1")); - } else { - studentProfile = this.profileRepository.findById(1).orElse(null); - } - - User teacherUser; - if (!this.userRepository.existsById(2)) { - teacherUser = new User("dave@email.com", this.encoder.encode("password")); - teacherUser.setRoles(teacherRoles); - teacherUser = this.userRepository.save(teacherUser); - } else { - teacherUser = this.userRepository.findById(2).orElse(null); - } - Profile teacherProfile; - if (!this.profileRepository.existsById(2)) { - teacherProfile = this.profileRepository.save(new Profile(teacherUser, "Rick", "Sanchez", "Hello there!", "teacher1")); - } else { - teacherProfile = this.profileRepository.findById(2).orElse(null); - } - - if (!this.postRepository.existsById(1)) { - this.postRepository.save(new Post(studentUser, "My first post!")); - } - if (!this.postRepository.existsById(2)) { - this.postRepository.save(new Post(teacherUser, "Hello, students!")); - } } } diff --git a/src/main/java/com/booleanuk/cohorts/controllers/AuthController.java b/src/main/java/com/booleanuk/cohorts/controllers/AuthController.java index 0c9ba64..089b7be 100644 --- a/src/main/java/com/booleanuk/cohorts/controllers/AuthController.java +++ b/src/main/java/com/booleanuk/cohorts/controllers/AuthController.java @@ -1,5 +1,23 @@ package com.booleanuk.cohorts.controllers; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.ResponseEntity; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.web.bind.annotation.CrossOrigin; +import org.springframework.web.bind.annotation.PostMapping; +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.ERole; import com.booleanuk.cohorts.models.Role; import com.booleanuk.cohorts.models.User; @@ -12,21 +30,10 @@ import com.booleanuk.cohorts.repository.UserRepository; import com.booleanuk.cohorts.security.jwt.JwtUtils; import com.booleanuk.cohorts.security.services.UserDetailsImpl; -import jakarta.validation.Valid; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.http.ResponseEntity; -import org.springframework.security.authentication.AuthenticationManager; -import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; -import org.springframework.security.core.Authentication; -import org.springframework.security.core.context.SecurityContextHolder; -import org.springframework.security.crypto.password.PasswordEncoder; -import org.springframework.web.bind.annotation.*; -import java.util.HashSet; -import java.util.List; -import java.util.Set; -import java.util.stream.Collectors; +import jakarta.validation.Valid; +//fixed issue with login. @CrossOrigin(origins = "*", maxAge = 3600) @RestController @RequestMapping @@ -69,6 +76,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..65946a3 100644 --- a/src/main/java/com/booleanuk/cohorts/controllers/CohortController.java +++ b/src/main/java/com/booleanuk/cohorts/controllers/CohortController.java @@ -1,16 +1,18 @@ 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.request.ProfileRequest; +import com.booleanuk.cohorts.payload.response.*; import com.booleanuk.cohorts.repository.CohortRepository; +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.*; + @CrossOrigin(origins = "*", maxAge = 3600) @RestController @RequestMapping("cohorts") @@ -18,6 +20,12 @@ public class CohortController { @Autowired private CohortRepository cohortRepository; + @Autowired + private UserRepository userRepository; + + @Autowired + private ProfileRepository profileRepository; + @GetMapping public ResponseEntity getAllCohorts() { CohortListResponse cohortListResponse = new CohortListResponse(); @@ -28,7 +36,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.getProfiles().isEmpty()) { ErrorResponse error = new ErrorResponse(); error.set("not found"); return new ResponseEntity<>(error, HttpStatus.NOT_FOUND); @@ -37,4 +45,34 @@ public ResponseEntity getCohortById(@PathVariable int id) { cohortResponse.set(cohort); return ResponseEntity.ok(cohortResponse); } + + @GetMapping("/teacher/{id}") + public ResponseEntity getCohorstByUserId(@PathVariable int id) { + User user = userRepository.findById(id).orElse(null); + if (user == null) return new ResponseEntity<>("User for id " + Integer.valueOf(id) + " not found", HttpStatus.NOT_FOUND); + Profile teacherProfile = profileRepository.findById(user.getProfile().getId()).orElse(null); + if (teacherProfile == null) return new ResponseEntity<>("Profile for user " + user.getEmail() +" not found", HttpStatus.NOT_FOUND); + + CohortResponse cohortResponse = new CohortResponse(); + Cohort cohort = teacherProfile.getCohort(); + cohortResponse.set(cohort); + + return new ResponseEntity(cohortResponse, HttpStatus.OK); + } + + @PatchMapping("/teacher/{id}") + public ResponseEntity addStudentToCohort(@PathVariable int id, @RequestBody ProfileRequest profileRequest){ + Cohort cohort = cohortRepository.findById(id).orElse(null); + if (cohort == null) return new ResponseEntity<>("Cohort for id " + Integer.valueOf(id) + " not found", HttpStatus.NOT_FOUND); + + Profile profile = profileRepository.findById(profileRequest.getUserId()).orElse(null); + if (profile == null) return new ResponseEntity<>("Profile not found", HttpStatus.NOT_FOUND); + + Cohort updatedCohort = cohortRepository.findById(profileRequest.getCohort()).orElse(null); + if (updatedCohort == null) return new ResponseEntity<>("Cohort not found", HttpStatus.NOT_FOUND); + + profile.setCohort(updatedCohort); + + return new ResponseEntity<>(profileRepository.save(profile), HttpStatus.OK); + } } 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..158b046 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,253 @@ 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 = user.getProfile(); + + 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); + } + + @DeleteMapping("/{id}") + public ResponseEntity deletePostById(@PathVariable int id) { + Post post = this.postRepository.findById(id).orElse(null); + if (post == null) return notFoundResponse("Post not found"); + + PostResponse postResponse = new PostResponse(); + postResponse.set(post); + postRepository.delete(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); + } + + @PutMapping("/{postId}") + public ResponseEntity updatePost(@PathVariable int postId, @RequestBody PostRequest postRequest) { + User currentUser = getCurrentAuthenticatedUser(); + if (currentUser == null) return unauthorizedResponse(); + + Post post = this.postRepository.findById(postId).orElse(null); + if (post == null) return notFoundResponse("Post not found"); + + // Only the owner can update their post + if (post.getUser() == null || post.getUser().getId() != currentUser.getId()) { + return forbiddenResponse("You can only edit your own posts"); + } + + // Update content only; likes unchanged + if (postRequest.getContent() == null || postRequest.getContent().trim().isEmpty()) { + return badRequestResponse("Content cannot be empty"); + } + post.setContent(postRequest.getContent().trim()); + // Explicitly set timeUpdated only on PUT update of post content + post.setTimeUpdated(java.time.OffsetDateTime.now()); + + 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..7ce1b16 --- /dev/null +++ b/src/main/java/com/booleanuk/cohorts/controllers/ProfileController.java @@ -0,0 +1,142 @@ +package com.booleanuk.cohorts.controllers; + +import java.time.LocalDate; +import java.time.format.DateTimeParseException; +import java.util.Optional; + +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.CrossOrigin; +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.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import com.booleanuk.cohorts.models.Cohort; +import com.booleanuk.cohorts.models.ERole; +import com.booleanuk.cohorts.models.Profile; +import com.booleanuk.cohorts.models.Role; +import com.booleanuk.cohorts.models.User; +import com.booleanuk.cohorts.payload.response.ProfileListResponse; +import com.booleanuk.cohorts.payload.response.ProfileResponse; +import com.booleanuk.cohorts.repository.CohortRepository; +import com.booleanuk.cohorts.repository.ProfileRepository; +import com.booleanuk.cohorts.repository.RoleRepository; +import com.booleanuk.cohorts.repository.UserRepository; + +@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 ResponseEntity getAllProfiles() { + ProfileListResponse profileListResponse = new ProfileListResponse(); + profileListResponse.set(this.profileRepository.findAll()); + return ResponseEntity.ok(profileListResponse); + } + + @GetMapping("{id}") + public ResponseEntity getById(@PathVariable int id){ + ProfileResponse profileResponse = new ProfileResponse(); + + Profile profile = this.profileRepository.findById(id).orElse(null); + if (profile == null) { + return new ResponseEntity<>("Not found", HttpStatus.NOT_FOUND); + } + profileResponse.set(profile); + return ResponseEntity.ok(profileResponse); + } + + @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.github_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); + } + + newProfile.setUser(user); + user.setProfile(newProfile); + 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); + } + } +} diff --git a/src/main/java/com/booleanuk/cohorts/controllers/SearchController.java b/src/main/java/com/booleanuk/cohorts/controllers/SearchController.java new file mode 100644 index 0000000..3d3f4ad --- /dev/null +++ b/src/main/java/com/booleanuk/cohorts/controllers/SearchController.java @@ -0,0 +1,53 @@ +package com.booleanuk.cohorts.controllers; + +import com.booleanuk.cohorts.models.Profile; +import com.booleanuk.cohorts.payload.response.ProfileListResponse; +import com.booleanuk.cohorts.payload.response.UserListResponse; +import com.booleanuk.cohorts.repository.ProfileRepository; +import com.booleanuk.cohorts.repository.UserRepository; +import org.springframework.beans.factory.annotation.Autowired; + +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Sort; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import java.util.LinkedList; +import java.util.List; + +@CrossOrigin(origins = "*", maxAge = 3600) +@RestController +@RequestMapping("search") +public class SearchController { + @Autowired + private UserRepository userRepository; + + @Autowired + private ProfileRepository profileRepository; + + @GetMapping("/profiles/{query}") + public ResponseEntity searchProfiles(@PathVariable String query) { + List result = new LinkedList(); + profileRepository.getProfilesByFirstNameContainingIgnoreCase(query).forEach(result::add); + profileRepository.getProfilesByLastNameContainingIgnoreCase(query).forEach(result::add); + + + ProfileListResponse profileListResponse = new ProfileListResponse(); + profileListResponse.set(result); + + return new ResponseEntity<>(profileListResponse, HttpStatus.OK); + + + } + + @GetMapping("/profiles") + public ResponseEntity searchProfilesDefault() { + List result = new LinkedList(); + profileRepository.findTop10ByOrderByIdDesc().forEach(result::add); + + ProfileListResponse profileListResponse = new ProfileListResponse(); + profileListResponse.set(result); + return new ResponseEntity<>(profileListResponse,HttpStatus.OK); + } +} diff --git a/src/main/java/com/booleanuk/cohorts/controllers/StudentController.java b/src/main/java/com/booleanuk/cohorts/controllers/StudentController.java new file mode 100644 index 0000000..3af2b2a --- /dev/null +++ b/src/main/java/com/booleanuk/cohorts/controllers/StudentController.java @@ -0,0 +1,95 @@ + +package com.booleanuk.cohorts.controllers; + + +import com.booleanuk.cohorts.models.Profile; +import com.booleanuk.cohorts.models.User; +import com.booleanuk.cohorts.payload.request.StudentRequest; +import com.booleanuk.cohorts.payload.response.ProfileListResponse; +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.security.crypto.password.PasswordEncoder; +import org.springframework.web.bind.annotation.*; + +import java.util.ArrayList; +import java.util.List; + +@CrossOrigin(origins = "*", maxAge = 3600) +@RestController +@RequestMapping("students") +public class StudentController { + @Autowired + private UserRepository userRepository; + + @Autowired + private ProfileRepository profileRepository; + + @Autowired + PasswordEncoder encoder; + + @GetMapping + public ResponseEntity getAllStudents() { + List allProfiles = this.profileRepository.findAll(); + + List students = new ArrayList<>(); + + for (Profile profile : allProfiles) { + if (profile.getRole() != null && + profile.getRole().getName() != null && + "ROLE_STUDENT".equals(profile.getRole().getName().name())) { + students.add(profile); + } + } + + ProfileListResponse studentListResponse = new ProfileListResponse(); + studentListResponse.set(students); + + return ResponseEntity.ok(studentListResponse); + } + + + @PatchMapping("{id}") + public ResponseEntity updateStudent(@PathVariable int id, @RequestBody StudentRequest studentRequest) { + + User user = userRepository.findById(id).orElse(null); + if (user == null) { + return new ResponseEntity<>("User not found", HttpStatus.NOT_FOUND); + } + + Profile profile = profileRepository.findById(id).orElse(null); + if (profile == null) { + return new ResponseEntity<>("Profile not found", HttpStatus.NOT_FOUND); + } + + if (profile.getRole().equals("ROLE_TEACHER")) { + return new ResponseEntity<>("Only users with the STUDENT role can be viewed.", HttpStatus.BAD_REQUEST); + } + + profile.setPhoto(studentRequest.getPhoto()); + profile.setFirstName(studentRequest.getFirst_name()); + profile.setLastName(studentRequest.getLast_name()); + profile.setUsername(studentRequest.getUsername()); + profile.setGithubUrl(studentRequest.getGithub_username()); + + user.setEmail(studentRequest.getEmail()); + profile.setMobile(studentRequest.getMobile()); + user.setPassword(encoder.encode(studentRequest.getPassword())); + profile.setBio(studentRequest.getBio()); + + profileRepository.save(profile); + + user.setProfile(profile); + user.setCohort(profile.getCohort()); + + try { + return new ResponseEntity<>(userRepository.save(user), 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/TeacherController.java b/src/main/java/com/booleanuk/cohorts/controllers/TeacherController.java new file mode 100644 index 0000000..9d0b358 --- /dev/null +++ b/src/main/java/com/booleanuk/cohorts/controllers/TeacherController.java @@ -0,0 +1,91 @@ +package com.booleanuk.cohorts.controllers; + +import com.booleanuk.cohorts.models.Cohort; +import com.booleanuk.cohorts.models.Profile; +import com.booleanuk.cohorts.models.Role; +import com.booleanuk.cohorts.models.User; +import com.booleanuk.cohorts.payload.request.StudentRequest; +import com.booleanuk.cohorts.payload.request.TeacherEditStudentRequest; +import com.booleanuk.cohorts.payload.response.ProfileListResponse; +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.dao.DataIntegrityViolationException; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.web.bind.annotation.*; + +import java.util.ArrayList; +import java.util.List; + + +@CrossOrigin(origins = "*", maxAge = 3600) +@RestController +@RequestMapping("teachers") +public class TeacherController { + + @Autowired + private UserRepository userRepository; + + @Autowired + private ProfileRepository profileRepository; + @Autowired private CohortRepository cohortRepository; + @Autowired private RoleRepository roleRepository; + + @Autowired + PasswordEncoder encoder; + + @PatchMapping("{id}") + public ResponseEntity updateStudent(@PathVariable int id, @RequestBody TeacherEditStudentRequest teacherEditStudentRequest) { + + User user = userRepository.findById(id).orElse(null); + if (user == null) { + return new ResponseEntity<>("User not found", HttpStatus.NOT_FOUND); + } + + Profile profile = profileRepository.findById(user.getProfile().getId()).orElse(null); + if (profile == null) { + return new ResponseEntity<>("Profile not found", HttpStatus.NOT_FOUND); + } + + if (profile.getRole().getName().name().equals("ROLE_TEACHER")) { + return new ResponseEntity<>("Teachers can only edit other Students!", HttpStatus.BAD_REQUEST); + } + + Cohort cohort = cohortRepository.findById(teacherEditStudentRequest.getCohort_id()).orElseThrow(); + Role role = roleRepository.findById(teacherEditStudentRequest.getRole_id()).orElseThrow(); + + profile.setCohort(cohort); + profile.setRole(role); + profile.setStartDate(teacherEditStudentRequest.getStart_date()); + profile.setEndDate(teacherEditStudentRequest.getEnd_date()); + + + return new ResponseEntity<>(profileRepository.save(profile),HttpStatus.OK); + } + + @GetMapping + public ResponseEntity getAllTeachers(){ + List allProfiles = this.profileRepository.findAll(); + + List teachers = new ArrayList<>(); + + for (Profile profile : allProfiles) { + if (profile.getRole() != null && + profile.getRole().getName() != null && + "ROLE_TEACHER".equals(profile.getRole().getName().name())) { + teachers.add(profile); + } + } + + ProfileListResponse teacherListResponse = new ProfileListResponse(); + teacherListResponse.set(teachers); + + return ResponseEntity.ok(teacherListResponse); + } + +} + diff --git a/src/main/java/com/booleanuk/cohorts/controllers/UserController.java b/src/main/java/com/booleanuk/cohorts/controllers/UserController.java index 1261544..67f5e4c 100644 --- a/src/main/java/com/booleanuk/cohorts/controllers/UserController.java +++ b/src/main/java/com/booleanuk/cohorts/controllers/UserController.java @@ -1,15 +1,30 @@ package com.booleanuk.cohorts.controllers; +import java.util.List; +import java.util.Set; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +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.PatchMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +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.Post; 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.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.*; @CrossOrigin(origins = "*", maxAge = 3600) @RestController @@ -18,6 +33,12 @@ public class UserController { @Autowired private UserRepository userRepository; + @Autowired + private ProfileRepository profileRepository; + + @Autowired + private PostRepository postRepository; + @GetMapping public ResponseEntity getAllUsers() { UserListResponse userListResponse = new UserListResponse(); @@ -25,6 +46,10 @@ public ResponseEntity getAllUsers() { return ResponseEntity.ok(userListResponse); } + record PostId( + int post_id + ){} + @GetMapping("{id}") public ResponseEntity getUserById(@PathVariable int id) { User user = this.userRepository.findById(id).orElse(null); @@ -38,6 +63,55 @@ public ResponseEntity getUserById(@PathVariable int id) { return ResponseEntity.ok(userResponse); } + @DeleteMapping("{id}") + public ResponseEntity deleteUser(@PathVariable int id) { + User user = this.userRepository.findById(id).orElse(null); + if (user == null){ + ErrorResponse error = new ErrorResponse(); + error.set("not found"); + return new ResponseEntity<>(error, HttpStatus.NOT_FOUND); + } + user.getRoles().clear(); + UserResponse userResponse = new UserResponse(); + userResponse.set(user); + try { + userRepository.delete(user); + return ResponseEntity.ok(userResponse); + } catch (Exception e){ + return new ResponseEntity<>("Could not delete user", HttpStatus.BAD_REQUEST); + } + } + + @PatchMapping("{user_id}/like") + public ResponseEntity updateLikedPosts(@PathVariable int user_id, @RequestBody PostId postId){ + int post_id = postId.post_id; + User user = userRepository.findById(user_id).orElse(null); + Post post = postRepository.findById(post_id).orElse(null); + ErrorResponse errorResponse = new ErrorResponse(); + + if (user == null) { + errorResponse.set("User not found"); + return new ResponseEntity<>(errorResponse, HttpStatus.NOT_FOUND); + } + if (post == null) { + errorResponse.set("Post not found"); + return new ResponseEntity<>(errorResponse, HttpStatus.NOT_FOUND); + } + Set likedPosts = user.getLikedPosts(); + + if (likedPosts.contains(post)) { + likedPosts.remove(post); + } else { + likedPosts.add(post); + } + user.setLikedPosts(likedPosts); + userRepository.save(user); + + UserResponse userResponse = new UserResponse(); + userResponse.set(user); + return ResponseEntity.ok(userResponse); + } + @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..3337b2d 100644 --- a/src/main/java/com/booleanuk/cohorts/models/Cohort.java +++ b/src/main/java/com/booleanuk/cohorts/models/Cohort.java @@ -1,9 +1,21 @@ package com.booleanuk.cohorts.models; +import com.fasterxml.jackson.annotation.JsonIdentityInfo; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.ObjectIdGenerators; import jakarta.persistence.*; import lombok.Data; import lombok.NoArgsConstructor; +import java.util.List; + +/* +@JsonIdentityInfo( + generator = ObjectIdGenerators.PropertyGenerator.class, + property = "id" +) +*/ + @NoArgsConstructor @Data @Entity @@ -13,7 +25,29 @@ 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("cohort") + private List profiles; + + + 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..11bde19 --- /dev/null +++ b/src/main/java/com/booleanuk/cohorts/models/Comment.java @@ -0,0 +1,54 @@ +package com.booleanuk.cohorts.models; + +import com.fasterxml.jackson.annotation.JsonIdentityInfo; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; + +import com.fasterxml.jackson.annotation.ObjectIdGenerators; +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.EqualsAndHashCode; +import lombok.NoArgsConstructor; + +/*@JsonIdentityInfo( + generator = ObjectIdGenerators.PropertyGenerator.class, + property = "id" +)*/ + +@NoArgsConstructor +@AllArgsConstructor +@Data +@EqualsAndHashCode(exclude = {"user", "post"}) +@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", "posts", "likedPosts", "cohort", "roles"}) + 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..8e08404 --- /dev/null +++ b/src/main/java/com/booleanuk/cohorts/models/Course.java @@ -0,0 +1,40 @@ +package com.booleanuk.cohorts.models; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonIncludeProperties; +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; + + @ManyToMany(mappedBy = "cohort_courses") + @JsonIncludeProperties("id") + 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..eb38ee2 100644 --- a/src/main/java/com/booleanuk/cohorts/models/Post.java +++ b/src/main/java/com/booleanuk/cohorts/models/Post.java @@ -1,14 +1,36 @@ package com.booleanuk.cohorts.models; +import java.time.OffsetDateTime; +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.PrePersist; +import jakarta.persistence.Table; +import jakarta.persistence.Transient; import lombok.AllArgsConstructor; import lombok.Data; +import lombok.EqualsAndHashCode; import lombok.NoArgsConstructor; +/*@JsonIdentityInfo( + generator = ObjectIdGenerators.PropertyGenerator.class, + property = "id" +)*/ + @NoArgsConstructor @AllArgsConstructor @Data +@EqualsAndHashCode(exclude = {"user", "comments"}) @Entity @Table(name = "posts") public class Post { @@ -19,27 +41,64 @@ public class Post { @Column(nullable = false) private String content; + @Column(nullable = false, columnDefinition = "int default 0") + private int likes = 0; + + @Column(name = "time_created", nullable = false, columnDefinition = "TIMESTAMP WITH TIME ZONE") + private OffsetDateTime timeCreated; + + @Column(name = "time_updated", nullable = false, columnDefinition = "TIMESTAMP WITH TIME ZONE") + private OffsetDateTime timeUpdated; + @ManyToOne @JoinColumn(name = "user_id", nullable = false) - @JsonIgnoreProperties("users") + @JsonIgnoreProperties(value = {"posts", "comments", "cohort", "roles", "likedPosts"}) 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; + } + + @PrePersist + protected void onCreate() { + OffsetDateTime now = OffsetDateTime.now(); + this.timeCreated = now; + this.timeUpdated = now; + } + + } diff --git a/src/main/java/com/booleanuk/cohorts/models/Profile.java b/src/main/java/com/booleanuk/cohorts/models/Profile.java index 4305d80..e6bcf2a 100644 --- a/src/main/java/com/booleanuk/cohorts/models/Profile.java +++ b/src/main/java/com/booleanuk/cohorts/models/Profile.java @@ -1,12 +1,37 @@ package com.booleanuk.cohorts.models; +import java.time.LocalDate; + +import org.hibernate.annotations.OnDelete; +import org.hibernate.annotations.OnDeleteAction; + import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import jakarta.persistence.*; + +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.OneToOne; +import jakarta.persistence.Table; +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Pattern; import lombok.Data; +import lombok.EqualsAndHashCode; import lombok.NoArgsConstructor; + +/*@JsonIdentityInfo( + generator = ObjectIdGenerators.PropertyGenerator.class, + property = "id" +)*/ + @NoArgsConstructor @Data +@EqualsAndHashCode(exclude = {"user", "cohort", "role"}) @Entity @Table(name = "profiles") public class Profile { @@ -14,32 +39,77 @@ public class Profile { @GeneratedValue(strategy = GenerationType.IDENTITY) private int id; + @OneToOne - @JoinColumn(name = "user_id", nullable = false) - @JsonIgnoreProperties("users") + @JoinColumn(name = "user_id", nullable = false, unique = true) + @OnDelete(action = OnDeleteAction.CASCADE) + @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(length = 300) 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") + @JsonIgnoreProperties({"profiles", "users", "deliveryLogs", "cohortCourses"}) + 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..0345fac 100644 --- a/src/main/java/com/booleanuk/cohorts/models/User.java +++ b/src/main/java/com/booleanuk/cohorts/models/User.java @@ -1,18 +1,28 @@ package com.booleanuk.cohorts.models; -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.*; import jakarta.persistence.*; import jakarta.validation.constraints.Email; import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.Size; import lombok.Data; +import lombok.EqualsAndHashCode; import lombok.NoArgsConstructor; import java.util.HashSet; +import java.util.List; import java.util.Set; +/* +@JsonIdentityInfo( + generator = ObjectIdGenerators.PropertyGenerator.class, + property = "id" +) +*/ + @NoArgsConstructor @Data +@EqualsAndHashCode(exclude = {"profile", "posts", "comments", "likedPosts", "cohort"}) @Entity @Table(name = "users", uniqueConstraints = { @@ -30,6 +40,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 +53,28 @@ public class User { @ManyToOne @JoinColumn(name = "cohort_id", nullable = true) - @JsonIgnoreProperties("users") + @JsonIncludeProperties({"id", "cohort_courses"}) private Cohort cohort; + @OneToMany(mappedBy = "user", cascade = CascadeType.ALL, orphanRemoval = true) + @JsonIncludeProperties({"id", "content", "likes", "timeCreated", "timeUpdated" }) + private List posts; + + @ManyToMany + @JoinTable(name = "user_liked_posts", + joinColumns = @JoinColumn(name = "user_id"), + inverseJoinColumns = @JoinColumn(name = "post_id")) + @JsonIncludeProperties(value="id") + private Set likedPosts = new HashSet<>(); + + @OneToMany(mappedBy = "user", cascade = CascadeType.ALL, orphanRemoval = true) + @JsonIncludeProperties({"id","body" }) + private List comments; + + @OneToOne(mappedBy = "user", cascade = CascadeType.ALL, orphanRemoval = true) + @JsonIgnoreProperties({"user", "role", "cohort"}) + private Profile profile; + public User(String email, String password) { this.email = email; this.password = password; @@ -55,4 +85,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/CohortRequest.java b/src/main/java/com/booleanuk/cohorts/payload/request/CohortRequest.java new file mode 100644 index 0000000..81c77aa --- /dev/null +++ b/src/main/java/com/booleanuk/cohorts/payload/request/CohortRequest.java @@ -0,0 +1,4 @@ +package com.booleanuk.cohorts.payload.request; + +public class CohortRequest { +} 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/request/ProfileRequest.java b/src/main/java/com/booleanuk/cohorts/payload/request/ProfileRequest.java new file mode 100644 index 0000000..87d6ecf --- /dev/null +++ b/src/main/java/com/booleanuk/cohorts/payload/request/ProfileRequest.java @@ -0,0 +1,19 @@ +package com.booleanuk.cohorts.payload.request; + + + +import com.booleanuk.cohorts.models.Cohort; +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +public class ProfileRequest { + private int cohort; + private int userId; + + public ProfileRequest(){} + + public int getCohort() { return cohort; } + public int getUserId() { return userId; } +} diff --git a/src/main/java/com/booleanuk/cohorts/payload/request/StudentRequest.java b/src/main/java/com/booleanuk/cohorts/payload/request/StudentRequest.java new file mode 100644 index 0000000..ba1380b --- /dev/null +++ b/src/main/java/com/booleanuk/cohorts/payload/request/StudentRequest.java @@ -0,0 +1,33 @@ +package com.booleanuk.cohorts.payload.request; + +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +public class StudentRequest { + + private String photo; + private String first_name; + private String last_name; + private String username; + private String github_username; + private String email; + private String mobile; + private String password; + private String bio; + + public StudentRequest(){} + + public String getPhoto() { return photo; } + public String getFirst_name() { return first_name; } + public String getLast_name() { return last_name; } + public String getUsername() { return username; } + public String getGithub_username() { return github_username; } + + public String getEmail() { return email; } + public String getMobile() { return mobile; } + public String getPassword() { return password; } + public String getBio() { return bio; } + +} diff --git a/src/main/java/com/booleanuk/cohorts/payload/request/TeacherEditStudentRequest.java b/src/main/java/com/booleanuk/cohorts/payload/request/TeacherEditStudentRequest.java new file mode 100644 index 0000000..efd9d1c --- /dev/null +++ b/src/main/java/com/booleanuk/cohorts/payload/request/TeacherEditStudentRequest.java @@ -0,0 +1,22 @@ +package com.booleanuk.cohorts.payload.request; + +import com.booleanuk.cohorts.models.Cohort; +import com.booleanuk.cohorts.models.Course; +import com.booleanuk.cohorts.models.Role; +import lombok.Data; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +import java.time.LocalDate; + +@Data +@NoArgsConstructor +public class TeacherEditStudentRequest { + + + private int role_id; + private int cohort_id; + private LocalDate start_date; + private LocalDate end_date; +} 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/payload/response/ProfileData.java b/src/main/java/com/booleanuk/cohorts/payload/response/ProfileData.java new file mode 100644 index 0000000..2fbfa78 --- /dev/null +++ b/src/main/java/com/booleanuk/cohorts/payload/response/ProfileData.java @@ -0,0 +1,14 @@ +package com.booleanuk.cohorts.payload.response; + +import com.booleanuk.cohorts.models.Profile; +import lombok.Getter; + +@Getter +public class ProfileData extends Data { + protected Profile profile; + + @Override + public void set(Profile profile) { + this.profile = profile; + } +} diff --git a/src/main/java/com/booleanuk/cohorts/payload/response/ProfileListData.java b/src/main/java/com/booleanuk/cohorts/payload/response/ProfileListData.java new file mode 100644 index 0000000..eebfd29 --- /dev/null +++ b/src/main/java/com/booleanuk/cohorts/payload/response/ProfileListData.java @@ -0,0 +1,16 @@ +package com.booleanuk.cohorts.payload.response; + +import com.booleanuk.cohorts.models.Profile; +import lombok.Getter; + +import java.util.List; + +@Getter +public class ProfileListData extends Data> { + protected List profiles; + + @Override + public void set(List profiles) { + this.profiles = profiles; + } +} diff --git a/src/main/java/com/booleanuk/cohorts/payload/response/ProfileListResponse.java b/src/main/java/com/booleanuk/cohorts/payload/response/ProfileListResponse.java new file mode 100644 index 0000000..b71770e --- /dev/null +++ b/src/main/java/com/booleanuk/cohorts/payload/response/ProfileListResponse.java @@ -0,0 +1,15 @@ +package com.booleanuk.cohorts.payload.response; + +import com.booleanuk.cohorts.models.Profile; +import lombok.Getter; + +import java.util.List; + +@Getter +public class ProfileListResponse extends Response { + public void set(List profiles) { + Data> data = new ProfileListData(); + data.set(profiles); + super.set(data); + } +} diff --git a/src/main/java/com/booleanuk/cohorts/payload/response/ProfileResponse.java b/src/main/java/com/booleanuk/cohorts/payload/response/ProfileResponse.java new file mode 100644 index 0000000..6f9cce0 --- /dev/null +++ b/src/main/java/com/booleanuk/cohorts/payload/response/ProfileResponse.java @@ -0,0 +1,13 @@ +package com.booleanuk.cohorts.payload.response; + +import com.booleanuk.cohorts.models.Profile; +import lombok.Getter; + +@Getter +public class ProfileResponse extends Response { + public void set(Profile profile) { + Data data = new ProfileData(); + data.set(profile); + super.set(data); + } +} 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..05c23c3 100644 --- a/src/main/java/com/booleanuk/cohorts/repository/ProfileRepository.java +++ b/src/main/java/com/booleanuk/cohorts/repository/ProfileRepository.java @@ -1,7 +1,20 @@ package com.booleanuk.cohorts.repository; import com.booleanuk.cohorts.models.Profile; +import com.booleanuk.cohorts.models.User; import org.springframework.data.jpa.repository.JpaRepository; +import java.util.List; + public interface ProfileRepository extends JpaRepository { + List getProfilesByFirstNameContains(String firstName); + + List getProfilesByLastNameContains(String lastName); + + List findTop10ByOrderByIdDesc(); + + List getProfilesByFirstNameContainingIgnoreCase(String firstName); + + List getProfilesByLastNameContainingIgnoreCase(String lastName); + } diff --git a/src/main/java/com/booleanuk/cohorts/repository/UserRepository.java b/src/main/java/com/booleanuk/cohorts/repository/UserRepository.java index ba5998d..e48c3e9 100644 --- a/src/main/java/com/booleanuk/cohorts/repository/UserRepository.java +++ b/src/main/java/com/booleanuk/cohorts/repository/UserRepository.java @@ -1,13 +1,20 @@ package com.booleanuk.cohorts.repository; -import com.booleanuk.cohorts.models.User; +import java.util.Optional; + import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; import org.springframework.stereotype.Repository; -import java.util.Optional; +import com.booleanuk.cohorts.models.User; @Repository public interface UserRepository extends JpaRepository { Optional findByEmail(String email); + + @Query("SELECT u FROM User u LEFT JOIN FETCH u.profile LEFT JOIN FETCH u.roles WHERE u.email = :email") + Optional findByEmailWithProfile(@Param("email") String email); + Boolean existsByEmail(String email); } diff --git a/src/main/java/com/booleanuk/cohorts/security/WebSecurityConfig.java b/src/main/java/com/booleanuk/cohorts/security/WebSecurityConfig.java index ad90f26..c5c7674 100644 --- a/src/main/java/com/booleanuk/cohorts/security/WebSecurityConfig.java +++ b/src/main/java/com/booleanuk/cohorts/security/WebSecurityConfig.java @@ -37,7 +37,7 @@ public AuthTokenFilter authenticationJwtTokenFilter() { public DaoAuthenticationProvider authenticationProvider() { DaoAuthenticationProvider authProvider = new DaoAuthenticationProvider(userDetailsService); -// authProvider.setUserDetailsService(userDetailsService); +// authProvider.setUserDetailsService(userDetailsService); authProvider.setPasswordEncoder(passwordEncoder()); return authProvider; @@ -58,10 +58,15 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { .authorizeHttpRequests((requests) -> requests .requestMatchers("/login", "/login/**").permitAll() .requestMatchers("/signup", "/signup/**").permitAll() + .requestMatchers("/profiles", "/profiles/**").authenticated() + .requestMatchers("/students", "/students/**").authenticated() + .requestMatchers("/teachers", "/teachers/**").authenticated() .requestMatchers("/users", "/users/**").authenticated() .requestMatchers("/posts", "/posts/**").authenticated() .requestMatchers("/cohorts", "/cohorts/**").authenticated() + .requestMatchers("/courses", "/courses/**").authenticated() .requestMatchers("/logs", "/logs/**").authenticated() + .requestMatchers("/search", "/search/**").authenticated() .requestMatchers("/").authenticated() ); http.authenticationProvider(authenticationProvider()); 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..20ed442 100644 --- a/src/main/java/com/booleanuk/cohorts/security/jwt/JwtUtils.java +++ b/src/main/java/com/booleanuk/cohorts/security/jwt/JwtUtils.java @@ -1,20 +1,23 @@ package com.booleanuk.cohorts.security.jwt; +import java.util.Date; + +import javax.crypto.SecretKey; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.security.core.Authentication; +import org.springframework.stereotype.Component; + import com.booleanuk.cohorts.security.services.UserDetailsImpl; + import io.jsonwebtoken.ExpiredJwtException; import io.jsonwebtoken.Jwts; import io.jsonwebtoken.MalformedJwtException; import io.jsonwebtoken.UnsupportedJwtException; import io.jsonwebtoken.io.Decoders; import io.jsonwebtoken.security.Keys; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.security.core.Authentication; -import org.springframework.stereotype.Component; - -import javax.crypto.SecretKey; -import java.util.Date; @Component public class JwtUtils { @@ -31,6 +34,9 @@ public String generateJwtToken(Authentication authentication) { return Jwts.builder() .subject((userPrincipal.getUsername())) + .claim("userId", userPrincipal.getId()) + .claim("firstName", userPrincipal.getFirstName()) + .claim("lastName", userPrincipal.getLastName()) .issuedAt(new Date()) .expiration(new Date((new Date()).getTime() + this.jwtExpirationMs)) .signWith(this.key()) @@ -45,6 +51,18 @@ public String getUserNameFromJwtToken(String token) { return Jwts.parser().verifyWith(this.key()).build().parseSignedClaims(token).getPayload().getSubject(); } + public String getFirstNameFromJwtToken(String token) { + return Jwts.parser().verifyWith(this.key()).build().parseSignedClaims(token).getPayload().get("firstName", String.class); + } + + public String getLastNameFromJwtToken(String token) { + return Jwts.parser().verifyWith(this.key()).build().parseSignedClaims(token).getPayload().get("lastName", String.class); + } + + public Integer getUserIdFromJwtToken(String token) { + return Jwts.parser().verifyWith(this.key()).build().parseSignedClaims(token).getPayload().get("userId", Integer.class); + } + public boolean validateJwtToken(String authToken) { try { Jwts.parser().verifyWith(this.key()).build().parse(authToken); diff --git a/src/main/java/com/booleanuk/cohorts/security/services/UserDetailsImpl.java b/src/main/java/com/booleanuk/cohorts/security/services/UserDetailsImpl.java index 08e45e5..c071105 100644 --- a/src/main/java/com/booleanuk/cohorts/security/services/UserDetailsImpl.java +++ b/src/main/java/com/booleanuk/cohorts/security/services/UserDetailsImpl.java @@ -1,35 +1,41 @@ package com.booleanuk.cohorts.security.services; -import com.booleanuk.cohorts.models.User; -import com.fasterxml.jackson.annotation.JsonIgnore; -import lombok.Getter; -import org.springframework.security.core.GrantedAuthority; -import org.springframework.security.core.authority.SimpleGrantedAuthority; -import org.springframework.security.core.userdetails.UserDetails; - import java.util.Collection; import java.util.List; import java.util.Objects; import java.util.stream.Collectors; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.core.userdetails.UserDetails; + +import com.booleanuk.cohorts.models.User; +import com.fasterxml.jackson.annotation.JsonIgnore; + +import lombok.Getter; + @Getter public class UserDetailsImpl implements UserDetails { private static final long serialVersionUID = 1L; - private int id; - private String username; - private String email; + private final int id; + private final String username; + private final String email; + private final String firstName; + private final String lastName; @JsonIgnore - private String password; + private final String password; - private Collection authorities; + private final Collection authorities; - public UserDetailsImpl(int id, String username, String email, String password, Collection authorities) { + public UserDetailsImpl(int id, String username, String email, String password, String firstName, String lastName, Collection authorities) { this.id = id; this.username = email; this.email = email; this.password = password; + this.firstName = firstName; + this.lastName = lastName; this.authorities = authorities; } @@ -37,11 +43,26 @@ public static UserDetailsImpl build(User user) { List authorities = user.getRoles().stream() .map(role -> new SimpleGrantedAuthority(role.getName().name())) .collect(Collectors.toList()); + + // Get firstName and lastName from profile, with fallbacks if profile is null + String firstName = ""; + String lastName = ""; + + if (user.getProfile() != null) { + firstName = user.getProfile().getFirstName() != null ? user.getProfile().getFirstName() : ""; + lastName = user.getProfile().getLastName() != null ? user.getProfile().getLastName() : ""; + System.out.println("Profile found for user " + user.getEmail() + ": " + firstName + " " + lastName); + } else { + System.out.println("No profile found for user " + user.getEmail()); + } + return new UserDetailsImpl( user.getId(), user.getUsername(), user.getEmail(), user.getPassword(), + firstName, + lastName, authorities); } diff --git a/src/main/java/com/booleanuk/cohorts/security/services/UserDetailsServiceImpl.java b/src/main/java/com/booleanuk/cohorts/security/services/UserDetailsServiceImpl.java index 6095644..ac67c71 100644 --- a/src/main/java/com/booleanuk/cohorts/security/services/UserDetailsServiceImpl.java +++ b/src/main/java/com/booleanuk/cohorts/security/services/UserDetailsServiceImpl.java @@ -1,14 +1,16 @@ package com.booleanuk.cohorts.security.services; -import com.booleanuk.cohorts.models.User; -import com.booleanuk.cohorts.repository.UserRepository; -import jakarta.transaction.Transactional; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.core.userdetails.UsernameNotFoundException; import org.springframework.stereotype.Service; +import com.booleanuk.cohorts.models.User; +import com.booleanuk.cohorts.repository.UserRepository; + +import jakarta.transaction.Transactional; + @Service public class UserDetailsServiceImpl implements UserDetailsService { @Autowired @@ -17,10 +19,9 @@ public class UserDetailsServiceImpl implements UserDetailsService { @Override @Transactional public UserDetails loadUserByUsername(String email) throws UsernameNotFoundException { - User user = userRepository.findByEmail(email).orElseThrow( + User user = userRepository.findByEmailWithProfile(email).orElseThrow( () -> new UsernameNotFoundException("User not found with email: " + email) ); return UserDetailsImpl.build(user); - } } \ No newline at end of file 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