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..1bc475e --- /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 22 + uses: actions/setup-java@v4 + with: + java-version: '22' + 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..8c1efc7 100644 --- a/.gitignore +++ b/.gitignore @@ -36,4 +36,9 @@ out/ ### VS Code ### .vscode/ +### Project specific application.yml +build +.DS_Store +application-azuread.yml +Maincopy.md \ 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/build.gradle b/build.gradle index 481ead5..3e06b89 100644 --- a/build.gradle +++ b/build.gradle @@ -43,6 +43,16 @@ dependencies { testImplementation 'org.springframework.security:spring-security-test' testRuntimeOnly 'org.junit.platform:junit-platform-launcher' + // https://mvnrepository.com/artifact/org.springframework/spring-test + testImplementation 'org.springframework:spring-test' + // https://mvnrepository.com/artifact/org.junit.jupiter/junit-jupiter-api + testImplementation 'org.junit.jupiter:junit-jupiter-api' + // https://mvnrepository.com/artifact/org.junit.jupiter/junit-jupiter-engine + testImplementation 'org.junit.jupiter:junit-jupiter-engine' + // https://mvnrepository.com/artifact/com.h2database/h2 + testImplementation 'com.h2database:h2' + + // https://mvnrepository.com/artifact/jakarta.validation/jakarta.validation-api implementation 'jakarta.validation:jakarta.validation-api:3.1.1' } 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..ff802fb 100644 --- a/src/main/java/com/booleanuk/Main.java +++ b/src/main/java/com/booleanuk/Main.java @@ -1,18 +1,40 @@ package com.booleanuk; -import com.booleanuk.cohorts.models.*; -import com.booleanuk.cohorts.repository.*; +import java.time.LocalDate; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.CommandLineRunner; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.security.crypto.password.PasswordEncoder; -import java.util.HashSet; -import java.util.Set; +import com.booleanuk.cohorts.models.Cohort; +import com.booleanuk.cohorts.models.Course; +import com.booleanuk.cohorts.models.ERole; +import com.booleanuk.cohorts.models.Post; +import com.booleanuk.cohorts.models.Profile; +import com.booleanuk.cohorts.models.Role; +import com.booleanuk.cohorts.models.User; +import com.booleanuk.cohorts.repository.CohortRepository; +import com.booleanuk.cohorts.repository.CourseRepository; +import com.booleanuk.cohorts.repository.PostRepository; +import com.booleanuk.cohorts.repository.ProfileRepository; +import com.booleanuk.cohorts.repository.RoleRepository; +import com.booleanuk.cohorts.repository.UserRepository; @SpringBootApplication public class Main implements CommandLineRunner { + + // DATABASE SIZE CONFIGURATION + // Set to true for LARGE database: 3 courses, 4 cohorts per course (12 total), 30 students per cohort (360 total) + // Set to false for SMALL database: 3 courses, 1 cohort per course (3 total), 10 students per cohort (30 total) + private static final boolean USE_LARGE_DATABASE = false; + @Autowired private RoleRepository roleRepository; @Autowired @@ -24,6 +46,8 @@ public class Main implements CommandLineRunner { @Autowired private PostRepository postRepository; @Autowired + private CourseRepository courseRepository; + @Autowired PasswordEncoder encoder; public static void main(String[] args) { @@ -32,68 +56,204 @@ public static void main(String[] args) { @Override public void run(String... args) { - Role teacherRole; - if (!this.roleRepository.existsByName(ERole.ROLE_TEACHER)) { - teacherRole = this.roleRepository.save(new Role(ERole.ROLE_TEACHER)); - } else { - teacherRole = this.roleRepository.findByName(ERole.ROLE_TEACHER).orElse(null); - } - Set teacherRoles = new HashSet<>(); - teacherRoles.add(teacherRole); - Role studentRole; - if (!this.roleRepository.existsByName(ERole.ROLE_STUDENT)) { - studentRole = this.roleRepository.save(new Role(ERole.ROLE_STUDENT)); - } else { - studentRole = this.roleRepository.findByName(ERole.ROLE_STUDENT).orElse(null); - } - Set studentRoles = new HashSet<>(); - studentRoles.add(studentRole); - 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!")); - } - } -} + System.out.println("Initializing sample data..."); +// +// // Create roles +// +// Role teacherRole = createOrGetRole(ERole.ROLE_TEACHER); +// Role studentRole = createOrGetRole(ERole.ROLE_STUDENT); +// +// +// // Create courses with start and end dates +// Course javaFundamentals = createOrGetCourse("Software Development", +// LocalDate.of(2024, 1, 15), LocalDate.of(2024, 7, 15)); +// Course springBoot = createOrGetCourse("Front-End Development", +// LocalDate.of(2024, 2, 1), LocalDate.of(2024, 8, 1)); +// Course reactFundamentals = createOrGetCourse("Data Analytics", +// LocalDate.of(2024, 3, 1), LocalDate.of(2024, 9, 1)); +// +// +// // Create cohorts based on configuration +// List allCohorts = new ArrayList<>(); +// +// if (USE_LARGE_DATABASE) { +// // LARGE DATABASE: 4 cohorts per course (12 total) +// +// // Software Development cohorts (4 cohorts) +// allCohorts.add(createOrGetCohort("Software Development 2024 Q1", javaFundamentals)); +// allCohorts.add(createOrGetCohort("Software Development 2024 Q2", javaFundamentals)); +// allCohorts.add(createOrGetCohort("Software Development 2025 Q1", javaFundamentals)); +// allCohorts.add(createOrGetCohort("Software Development 2025 Q2", javaFundamentals)); +// +// // Front-End Development cohorts (4 cohorts) +// allCohorts.add(createOrGetCohort("Front-End Development 2024 Q1", springBoot)); +// allCohorts.add(createOrGetCohort("Front-End Development 2024 Q2", springBoot)); +// allCohorts.add(createOrGetCohort("Front-End Development 2025 Q1", springBoot)); +// allCohorts.add(createOrGetCohort("Front-End Development 2025 Q2", springBoot)); +// +// // Data Analytics cohorts (4 cohorts) +// allCohorts.add(createOrGetCohort("Data Analytics 2024 Q1", reactFundamentals)); +// allCohorts.add(createOrGetCohort("Data Analytics 2024 Q2", reactFundamentals)); +// allCohorts.add(createOrGetCohort("Data Analytics 2025 Q1", reactFundamentals)); +// allCohorts.add(createOrGetCohort("Data Analytics 2025 Q2", reactFundamentals)); +// } else { +// // SMALL DATABASE: 1 cohort per course (3 total) +// allCohorts.add(createOrGetCohort("Software Development 2025", javaFundamentals)); +// allCohorts.add(createOrGetCohort("Front-End Development 2025", springBoot)); +// allCohorts.add(createOrGetCohort("Data Analytics 2025", reactFundamentals)); +// } +// +// +// // Create teacher users +// User teacherJohn = createUser("t@t.com", "p", teacherRole); +// if (teacherJohn.getProfile() == null) { +// Profile johnProfile = new Profile(teacherJohn, "John", "Smith", "johnsmith", +// "https://github.com/johnsmith", "+44123456790", +// "Experienced Java developer and educator with 10+ years in software development.", +// teacherRole, "Java Development", allCohorts.get(0), null); +// profileRepository.save(johnProfile); +// teacherJohn.setProfile(johnProfile); +// userRepository.save(teacherJohn); +// } +// +// User teacherSarah = createUser("tt@t.com", "p", teacherRole); +// if (teacherSarah.getProfile() == null) { +// Profile sarahProfile = new Profile(teacherSarah, "Sarah", "Jones", "sarahjones", +// "https://github.com/sarahjones", "+44123456791", +// "Frontend specialist with expertise in React and modern web technologies.", +// teacherRole, "Frontend Development", allCohorts.get(1), null); +// profileRepository.save(sarahProfile); +// teacherSarah.setProfile(sarahProfile); +// userRepository.save(teacherSarah); +// } +// +// // Create student users +// List students = new ArrayList<>(); +// String[] firstNames = { +// "Alice", "Bob", "Carol", "David", "Emma", "Frank", "Grace", "Henry", +// "Ivy", "Jack", "Kate", "Liam", "Maya", "Noah", "Olivia", "Paul", +// "Quinn", "Ruby", "Sam", "Tina", "Uma", "Victor", "Wendy", "Xavier", +// "Yara", "Zoe", "Alex", "Blake", "Chloe", "Dan", "Eva", "Felix", +// "Gina", "Hugo", "Iris", "Jake", "Luna", "Max", "Nina", "Oscar" +// }; +// String[] lastNames = { +// "Johnson", "Wilson", "Brown", "Taylor", "Davis", "Miller", "Anderson", "Thomas", +// "Jackson", "White", "Harris", "Martin", "Thompson", "Garcia", "Martinez", "Robinson", +// "Clark", "Rodriguez", "Lewis", "Lee", "Walker", "Hall", "Allen", "Young", +// "Hernandez", "King", "Wright", "Lopez", "Hill", "Scott", "Green", "Adams", +// "Baker", "Gonzalez", "Nelson", "Carter", "Mitchell", "Perez", "Roberts", "Turner" +// }; +// +// // Create students based on configuration +// int totalStudents = USE_LARGE_DATABASE ? 360 : 30; // 360 for large (30 per cohort), 30 for small (10 per cohort) +// int studentsPerCohort = USE_LARGE_DATABASE ? 30 : 10; +// +// for (int i = 0; i < totalStudents; i++) { +// String firstName = firstNames[i % firstNames.length]; +// String lastName = lastNames[i % lastNames.length]; +// String email = firstName.toLowerCase() + i + "@s.com"; +// +// User student = createUser(email, "p", studentRole); +// if (student.getProfile() == null) { +// // Distribute students evenly across all cohorts +// Cohort assignedCohort = allCohorts.get(i / studentsPerCohort); +// +// Profile studentProfile = new Profile(student, firstName, lastName, +// firstName.toLowerCase() + lastName.toLowerCase() + i, +// "https://github.com/" + firstName.toLowerCase() + lastName.toLowerCase() + i, +// "+4412345679" + String.format("%03d", i), +// "Passionate about learning software development and building amazing applications.", +// studentRole, "Software Development", assignedCohort, null); +// profileRepository.save(studentProfile); +// student.setProfile(studentProfile); +// student = userRepository.save(student); +// } +// students.add(student); +// } +// +// // Create sample posts +// List samplePosts = Arrays.asList( +// "Just finished my first Java application! Excited to learn more about Spring Boot.", +// "Working on a React project today. The component lifecycle is fascinating!", +// "Had a great debugging session today. Finally understood how to use breakpoints effectively.", +// "Group project is going well. Collaboration through Git is becoming second nature.", +// "Completed the database design exercise. Understanding relationships is key!", +// "Learned about REST APIs today. Can't wait to build my own!", +// "Struggling with CSS Grid but making progress. Practice makes perfect!", +// "Just deployed my first application to the cloud. Such a great feeling!", +// "Code review session was very helpful. Learning from others is invaluable.", +// "Working on the final project. Bringing everything together is challenging but rewarding." +// ); +// +// // Create posts from different users (only if no posts exist) +// if (postRepository.count() == 0) { +// for (int i = 0; i < samplePosts.size(); i++) { +// User author = (i < 2) ? (i == 0 ? teacherJohn : teacherSarah) : students.get(i % students.size()); +// Post post = new Post(author, samplePosts.get(i)); +// post.setLikes((int) (Math.random() * 10)); // Random likes 0-9 +// postRepository.save(post); +// } +// } +// +// System.out.println("Sample data initialization completed!"); +// System.out.println("Created:"); +// System.out.println("- 2 roles (Teacher, Student)"); +// +// System.out.println("- 3 courses with date ranges (Software Development: Jan-Jul 2024, Front-End Development: Feb-Aug 2024, Data Analytics: Mar-Sep 2024)"); +// +// +// if (USE_LARGE_DATABASE) { +// System.out.println("- 12 cohorts (4 per course)"); +// System.out.println("- " + (2 + students.size()) + " users with profiles (2 teachers + " + students.size() + " students)"); +// System.out.println("- 30 students per cohort across 12 cohorts (360 total students)"); +// System.out.println(" - Software Development: 4 cohorts (120 students)"); +// System.out.println(" - Front-End Development: 4 cohorts (120 students)"); +// System.out.println(" - Data Analytics: 4 cohorts (120 students)"); +// } else { +// System.out.println("- 3 cohorts (1 per course)"); +// System.out.println("- " + (2 + students.size()) + " users with profiles (2 teachers + " + students.size() + " students)"); +// System.out.println("- 10 students per cohort across 3 cohorts (30 total students)"); +// System.out.println(" - Software Development: 1 cohort (10 students)"); +// System.out.println(" - Front-End Development: 1 cohort (10 students)"); +// System.out.println(" - Data Analytics: 1 cohort (10 students)"); +// } +// +// System.out.println("- " + samplePosts.size() + " sample posts"); +// } +// +// private Role createOrGetRole(ERole roleName) { +// return roleRepository.findByName(roleName) +// .orElseGet(() -> roleRepository.save(new Role(roleName))); +// } +// +// private User createUser(String email, String password, Role role) { +// // Check if user already exists +// return userRepository.findByEmail(email) +// .orElseGet(() -> { +// User user = new User(email, encoder.encode(password)); +// Set roles = new HashSet<>(); +// roles.add(role); +// user.setRoles(roles); +// return userRepository.save(user); +// }); +// } +// +// +// private Course createOrGetCourse(String name, LocalDate startDate, LocalDate endDate) { +// +// return courseRepository.findAll().stream() +// .filter(course -> course.getName().equals(name)) +// .findFirst() +// .orElseGet(() -> courseRepository.save(new Course(name, startDate, endDate, null))); +// } +// +// private Cohort createOrGetCohort(String name, Course course) { +// return cohortRepository.findAll().stream() +// .filter(cohort -> cohort.getName().equals(name)) +// .findFirst() +// .orElseGet(() -> { +// Cohort cohort = new Cohort(name, course); +// return cohortRepository.save(cohort); +// }); +// } +}} \ No newline at end of file diff --git a/src/main/java/com/booleanuk/cohorts/controllers/AuthController.java b/src/main/java/com/booleanuk/cohorts/controllers/AuthController.java index 0c9ba64..527c2cd 100644 --- a/src/main/java/com/booleanuk/cohorts/controllers/AuthController.java +++ b/src/main/java/com/booleanuk/cohorts/controllers/AuthController.java @@ -1,5 +1,25 @@ 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 jakarta.servlet.http.HttpServletRequest; + import com.booleanuk.cohorts.models.ERole; import com.booleanuk.cohorts.models.Role; import com.booleanuk.cohorts.models.User; @@ -7,26 +27,16 @@ import com.booleanuk.cohorts.payload.request.SignupRequest; import com.booleanuk.cohorts.payload.response.JwtResponse; import com.booleanuk.cohorts.payload.response.MessageResponse; +import com.booleanuk.cohorts.payload.response.RefreshTokenResponse; import com.booleanuk.cohorts.payload.response.TokenResponse; import com.booleanuk.cohorts.repository.RoleRepository; 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,37 +79,68 @@ 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) { - user.setCohort(signupRequest.getCohort()); - } - Set strRoles = signupRequest.getRole(); - Set roles = new HashSet<>(); - - if (strRoles == null) { - Role studentRole = roleRepository.findByName(ERole.ROLE_STUDENT).orElseThrow(() -> new RuntimeException("Error: Role is not found")); - roles.add(studentRole); - } else { - strRoles.forEach((role) -> { - switch (role) { - case "admin": - Role adminRole = roleRepository.findByName(ERole.ROLE_ADMIN).orElseThrow(() -> new RuntimeException("Error: Role is not found")); - roles.add(adminRole); - break; - case "teacher": - Role teacherRole = roleRepository.findByName(ERole.ROLE_TEACHER).orElseThrow(() -> new RuntimeException("Error: Role is not found")); - roles.add(teacherRole); - break; - default: - Role studentRole = roleRepository.findByName(ERole.ROLE_STUDENT).orElseThrow(() -> new RuntimeException("Error: Role is not found")); - roles.add(studentRole); - break; - } - }); - } - user.setRoles(roles); + userRepository.save(user); return ResponseEntity.ok((new MessageResponse("User registered successfully"))); } + + @PostMapping("/auth/refresh") + public ResponseEntity refreshToken(HttpServletRequest request) { + try { + // 1. Extract token from Authorization header + String headerAuth = request.getHeader("Authorization"); + if (headerAuth == null || !headerAuth.startsWith("Bearer ")) { + return ResponseEntity.badRequest().body(new MessageResponse("Authorization header missing or invalid")); + } + + String token = headerAuth.substring(7); + + // 2. Validate the token (allowing expired tokens for refresh) + if (!jwtUtils.validateJwtTokenForRefresh(token)) { + return ResponseEntity.badRequest().body(new MessageResponse("Invalid token")); + } + + // 3. Get user ID from token instead of email (this doesn't change when email is updated) + Integer userId = jwtUtils.getUserIdFromExpiredJwtToken(token); + + if (userId == null) { + return ResponseEntity.badRequest().body(new MessageResponse("Invalid token - no user ID found")); + } + + // 4. Find user by ID (stable identifier that doesn't change with email updates) + User user = userRepository.findByIdWithProfile(userId).orElse(null); + + if (user == null) { + // User not found by ID - this means the user was deleted + return ResponseEntity.badRequest().body(new MessageResponse("User not found. Please log in again.")); + } + + // 5. Create new authentication object with fresh user data + UserDetailsImpl userDetails = UserDetailsImpl.build(user); + UsernamePasswordAuthenticationToken authentication = + new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities()); + + // 6. Generate new token with fresh user data (including any email changes) + String newJwt = jwtUtils.generateJwtToken(authentication); + + // 7. Return new token in response + return ResponseEntity.ok(new RefreshTokenResponse(newJwt, "Token refreshed successfully")); + + } catch (Exception e) { + return ResponseEntity.badRequest().body(new MessageResponse("Error refreshing token: " + e.getMessage())); + } + } } diff --git a/src/main/java/com/booleanuk/cohorts/controllers/CohortController.java b/src/main/java/com/booleanuk/cohorts/controllers/CohortController.java index e96619c..c2c2de5 100644 --- a/src/main/java/com/booleanuk/cohorts/controllers/CohortController.java +++ b/src/main/java/com/booleanuk/cohorts/controllers/CohortController.java @@ -1,16 +1,26 @@ 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.repository.CohortRepository; +import com.booleanuk.cohorts.models.*; +import com.booleanuk.cohorts.payload.request.CohortRequest; +import com.booleanuk.cohorts.payload.request.CohortRequestWithProfiles; +import com.booleanuk.cohorts.payload.request.ProfileRequest; +import com.booleanuk.cohorts.payload.response.*; import org.springframework.beans.factory.annotation.Autowired; + +import com.booleanuk.cohorts.repository.CohortRepository; +import com.booleanuk.cohorts.repository.CourseRepository; +import com.booleanuk.cohorts.repository.ProfileRepository; +import com.booleanuk.cohorts.repository.UserRepository; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; +import java.time.LocalDate; +import java.time.format.DateTimeFormatter; +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; + @CrossOrigin(origins = "*", maxAge = 3600) @RestController @RequestMapping("cohorts") @@ -18,6 +28,15 @@ public class CohortController { @Autowired private CohortRepository cohortRepository; + @Autowired + private CourseRepository courseRepository; + + @Autowired + private UserRepository userRepository; + + @Autowired + private ProfileRepository profileRepository; + @GetMapping public ResponseEntity getAllCohorts() { CohortListResponse cohortListResponse = new CohortListResponse(); @@ -37,4 +56,106 @@ public ResponseEntity getCohortById(@PathVariable int id) { cohortResponse.set(cohort); return ResponseEntity.ok(cohortResponse); } + + @GetMapping("/teacher/{id}") + public ResponseEntity getCohortByUserId(@PathVariable int id) { + User user = userRepository.findById(id).orElse(null); + if (user == null) return new ResponseEntity<>("User for id " + 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); + } + + @PostMapping + public ResponseEntity addCohort(@RequestBody CohortRequest cohortRequest){ + + Course course = courseRepository.findById(cohortRequest.getCourseId()).orElse(null); + if (course == null) return new ResponseEntity<>("Course not found", HttpStatus.NOT_FOUND); + + String name = cohortRequest.getName(); + if (name.isBlank()) return new ResponseEntity<>("Name cannot be blank", HttpStatus.BAD_REQUEST); + + + LocalDate startDate = LocalDate.parse(cohortRequest.getStartDate().trim()); + LocalDate endDate = LocalDate.parse(cohortRequest.getEndDate().trim()); + + Cohort cohort = new Cohort(cohortRequest.getName(),startDate,endDate,course); + return ResponseEntity.ok(cohortRepository.save(cohort)); + } + + @PatchMapping("{id}") + public ResponseEntity editCohortById(@PathVariable int id, @RequestBody CohortRequestWithProfiles cohortRequest) { + Cohort cohort = cohortRepository.findById(id).orElse(null); + if (cohort == null) { + return new ResponseEntity<>("Cohort not found", HttpStatus.NOT_FOUND); + } + Course course = courseRepository.findById(cohortRequest.getCourseId()).orElse(null); + if (course == null) return new ResponseEntity<>("Course not found", HttpStatus.NOT_FOUND); + + String name = cohortRequest.getName(); + if (name.isBlank()) return new ResponseEntity<>("Name cannot be blank", HttpStatus.BAD_REQUEST); + + LocalDate startDate = LocalDate.parse(cohortRequest.getStartDate()); + LocalDate endDate = LocalDate.parse(cohortRequest.getEndDate()); + + + + List profiles = profileRepository.findAll().stream().filter(it -> cohortRequest.getProfileIds().contains(it.getId())).collect(Collectors.toList()); + for (Profile oldProfile : new ArrayList<>(cohort.getProfiles())) { + if (!profiles.contains(oldProfile)) { + oldProfile.setCohort(null); + } + } + + for (Profile newProfile : profiles) { + newProfile.setCohort(cohort); + } + + cohort.setProfiles(profiles); + cohort.setCourse(course); + cohort.setName(cohortRequest.getName()); + cohort.setStartDate(startDate); + cohort.setEndDate(endDate); + + return ResponseEntity.ok(cohortRepository.save(cohort)); + } + + @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.getProfileId()).orElse(null); + if (profile == null) return new ResponseEntity<>("Profile not found", HttpStatus.NOT_FOUND); + + profile.setCohort(cohort); + + return new ResponseEntity<>(profileRepository.save(profile), HttpStatus.OK); + } + + @DeleteMapping("{id}") + public ResponseEntity deleteCohort(@PathVariable int id) { + Cohort cohort = cohortRepository.findById(id).orElse(null); + if (cohort == null) return new ResponseEntity<>("Cohort for id " + Integer.valueOf(id) + " not found.", HttpStatus.NOT_FOUND); + CohortResponse cohortResponse = new CohortResponse(); + cohortResponse.set(cohort); + + for (Profile profile : cohort.getProfiles()){ + profile.setCohort(null); + } + cohort.getProfiles().clear(); + cohort.getCourse().getCohorts().remove(cohort); + try { + cohortRepository.delete(cohort); + return ResponseEntity.ok(cohortResponse); + } catch (Exception e) { + return new ResponseEntity<>("Could not delete Cohort", HttpStatus.BAD_REQUEST); + } + } } + 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..f1f2893 --- /dev/null +++ b/src/main/java/com/booleanuk/cohorts/controllers/CourseController.java @@ -0,0 +1,84 @@ +package com.booleanuk.cohorts.controllers; + + +import com.booleanuk.cohorts.models.*; +import com.booleanuk.cohorts.payload.request.CourseRequest; +import com.booleanuk.cohorts.payload.response.*; +import com.booleanuk.cohorts.repository.CohortRepository; +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.*; + +import java.time.LocalDate; +import java.util.ArrayList; +import java.util.List; + +@CrossOrigin(origins = "*", maxAge = 3600) +@RestController +@RequestMapping("courses") +public class CourseController { + @Autowired + private CourseRepository courseRepository; + + @Autowired + private CohortRepository cohortRepository; + + @GetMapping + public ResponseEntity getAllCourse(){ + CourseListResponse courseListResponse = new CourseListResponse(); + courseListResponse.set(this.courseRepository.findAll()); + return ResponseEntity.ok(courseListResponse); + } + + @GetMapping("students/{id}") + public ResponseEntity getAllStudents(@PathVariable int id){ + Course course = courseRepository.findById(id).orElse(null); + if (course == null) return new ResponseEntity<>("Course not found", HttpStatus.NOT_FOUND); + + List cohorts = course.getCohorts(); + List profiles = new ArrayList<>(); + for (Cohort cohort : cohorts) { + profiles.addAll(cohort.getProfiles()); + } + List students = profiles.stream().filter(it -> it.getRole().getId() == 2).toList(); + ProfileListResponse profileListResponse = new ProfileListResponse(); + profileListResponse.set(students); + return ResponseEntity.ok(profileListResponse); + } + + @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(); + String startDate = courseRequest.getStartDate(); + String endDate = courseRequest.getEndDate(); + + + if (startDate.isBlank() || endDate.isBlank()) + return new ResponseEntity<>("Date cannot be blank", HttpStatus.BAD_REQUEST); + + course.setName(courseRequest.getName()); + course.setStartDate(LocalDate.parse(courseRequest.getStartDate())); + course.setEndDate(LocalDate.parse(courseRequest.getEndDate())); + 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/NoteController.java b/src/main/java/com/booleanuk/cohorts/controllers/NoteController.java new file mode 100644 index 0000000..70690ae --- /dev/null +++ b/src/main/java/com/booleanuk/cohorts/controllers/NoteController.java @@ -0,0 +1,88 @@ +package com.booleanuk.cohorts.controllers; + +import com.booleanuk.cohorts.models.ERole; +import com.booleanuk.cohorts.models.Note; +import com.booleanuk.cohorts.models.User; +import com.booleanuk.cohorts.payload.request.NoteReqeuest; +import com.booleanuk.cohorts.payload.response.ErrorResponse; +import com.booleanuk.cohorts.repository.NoteRepository; +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.time.LocalDate; +import java.util.List; + +@RestController +@RequestMapping("notes") +public class NoteController { + + @Autowired private NoteRepository noteRepository; + @Autowired private UserRepository userRepository; + + + @GetMapping + public ResponseEntity getALlNotes(){ + + List notes = noteRepository.findAll(); + + if(notes.isEmpty()){ + ErrorResponse errorResponse = new ErrorResponse(); + errorResponse.set("No notes created"); + return new ResponseEntity<>(errorResponse, HttpStatus.NOT_FOUND); + } + + return new ResponseEntity<>(notes,HttpStatus.OK); + } + + @GetMapping("id") + public ResponseEntity getNoteById(@PathVariable int id){ + + Note note = noteRepository.findById(id).orElseThrow(); + + if(note.getUser() == null || note.getTitle().isEmpty() || note.getDescription().isEmpty()){ + ErrorResponse errorResponse = new ErrorResponse(); + errorResponse.set("Invalid note data"); + return new ResponseEntity<>(errorResponse, HttpStatus.NOT_FOUND); + } + + return new ResponseEntity<>(note,HttpStatus.OK); + } + + + + @PostMapping + private ResponseEntity createNote(@RequestBody NoteReqeuest noteReqeuest){ + + User user = userRepository.findById(noteReqeuest.getUser_id()).orElse(null); + + if(user == null ){ + ErrorResponse errorResponse = new ErrorResponse(); + errorResponse.set("No user with that ID exists"); + return new ResponseEntity<>(errorResponse, HttpStatus.NOT_FOUND); + } + + if(user.getProfile().getRole().getName().equals(ERole.ROLE_TEACHER)){ + ErrorResponse errorResponse = new ErrorResponse(); + errorResponse.set("Cannot add note to a teacher"); + return new ResponseEntity<>(errorResponse, HttpStatus.BAD_REQUEST); + } + + if(noteReqeuest.getTitle().isEmpty() || noteReqeuest.getDescription().isEmpty()){ + ErrorResponse errorResponse = new ErrorResponse(); + errorResponse.set("Note is missing description or title"); + return new ResponseEntity<>(errorResponse, HttpStatus.BAD_REQUEST); + } + + + Note note = new Note(); + note.setTitle(noteReqeuest.getTitle()); + note.setDescription(noteReqeuest.getDescription()); + note.setUser(user); + note.setCreated(LocalDate.now()); + return new ResponseEntity<>(noteRepository.save(note),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..12e85e9 100644 --- a/src/main/java/com/booleanuk/cohorts/controllers/PostController.java +++ b/src/main/java/com/booleanuk/cohorts/controllers/PostController.java @@ -1,23 +1,42 @@ 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.transaction.annotation.Transactional; +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 com.booleanuk.cohorts.security.services.UserDetailsImpl; -import java.util.ArrayList; -import java.util.List; +import static java.lang.Integer.parseInt; @CrossOrigin(origins = "*", maxAge = 3600) @RestController @@ -29,51 +48,287 @@ 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) { + Profile profile = user.getProfile(); + + if (profile != null) { + Author author = new Author(user.getId(), profile.getCohort().getId(), + profile.getFirstName(), profile.getLastName(), user.getEmail(), + profile.getBio(), profile.getGithubUrl()); + post.setAuthor(author); + } + } + } @GetMapping public ResponseEntity getAllPosts() { + List posts = this.postRepository.findAllWithUsers(); + 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.findByIdWithUser(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}") + @Transactional + public ResponseEntity deletePostById(@PathVariable int id) { + // Use the optimized query to fetch the post with all necessary data loaded + Post post = this.postRepository.findByIdWithUser(id).orElse(null); + if (post == null) return notFoundResponse("Post not found"); + + // Create a simple response object with just the basic info to avoid lazy loading issues + // We'll create a minimal post object with just the essential data + Post responsePost = new Post(); + responsePost.setId(post.getId()); + responsePost.setContent(post.getContent()); + responsePost.setLikes(post.getLikes()); + responsePost.setTimeCreated(post.getTimeCreated()); + responsePost.setTimeUpdated(post.getTimeUpdated()); + + // Set author info from the eagerly loaded data + if (post.getUser() != null) { + setAuthorInfo(responsePost, post.getUser()); + } + + // First remove the post from the liked_posts relationship table + // This prevents foreign key constraint violations + postRepository.removeLikedPostFromAllUsers(id); + + // Now delete the post - comments will be deleted automatically due to cascade + postRepository.deleteById(id); + + PostResponse postResponse = new PostResponse(); + postResponse.set(responsePost); + return ResponseEntity.ok(postResponse); + } + + private void setAuthorInfo(Post post, User user) { + if (user != null) { + Profile profile = user.getProfile(); + if (profile != null) { + Author author = new Author(user.getId(), profile.getCohort().getId(), + profile.getFirstName(), profile.getLastName(), user.getEmail(), + profile.getBio(), profile.getGithubUrl()); + post.setAuthor(author); + } + } + } + + @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..501e3ca --- /dev/null +++ b/src/main/java/com/booleanuk/cohorts/controllers/ProfileController.java @@ -0,0 +1,138 @@ +package com.booleanuk.cohorts.controllers; + +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; + + public 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, + 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); + + 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..f219b45 --- /dev/null +++ b/src/main/java/com/booleanuk/cohorts/controllers/StudentController.java @@ -0,0 +1,230 @@ + +package com.booleanuk.cohorts.controllers; + + +import com.booleanuk.cohorts.models.*; +import com.booleanuk.cohorts.payload.request.StudentRequest; +import com.booleanuk.cohorts.payload.response.MessageResponse; +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.time.format.DateTimeParseException; +import java.util.*; + +@CrossOrigin(origins = "*", maxAge = 3600) +@RestController +@RequestMapping("students") +public class StudentController { + @Autowired + private UserRepository userRepository; + + @Autowired + private ProfileRepository profileRepository; + + @Autowired + private CohortRepository cohortRepository; + @Autowired + private RoleRepository roleRepository; + + @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); + } + + record PostUserProfile( + String first_name, + String last_name, + String username, + String github_username, + String email, + String mobile, + String password, + String bio, + String role, + String specialism, + int cohort, + String start_date, + String end_date, + String photo + ){} + + @PostMapping("/create") + public ResponseEntity createNewStudent(@RequestBody PostUserProfile newUserProfile){ + + System.err.println(newUserProfile); + System.out.println("test----------------------------"); + + // Lag ny bruker + if (userRepository.existsByEmail(newUserProfile.email)) { + 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(!newUserProfile.email.matches(emailRegex)) + return ResponseEntity.badRequest().body(new MessageResponse("Email is incorrectly formatted")); + + if(!newUserProfile.password.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(newUserProfile.email, encoder.encode(newUserProfile.password)); + + //Lag ny profil som er koblet opp til ny bruker + + if(newUserProfile.first_name == null || newUserProfile.first_name == "" || newUserProfile.last_name == null || newUserProfile.last_name == ""){ + return new ResponseEntity<>("First and last name can't be empty or NULL. First name: " + newUserProfile.first_name + " Last name: " + newUserProfile.last_name, HttpStatus.BAD_REQUEST); + } + + + Optional optionalRole = roleRepository.findByName(ERole.valueOf(newUserProfile.role)); + if (optionalRole.isEmpty()) { + return new ResponseEntity<>("Role for id "+ newUserProfile.role + " not found", HttpStatus.BAD_REQUEST); + } + + Role role = optionalRole.get(); + + Optional optionalCohort = cohortRepository.findById(newUserProfile.cohort); + if (optionalCohort.isEmpty()) { + return new ResponseEntity<>("Cohort for id "+ newUserProfile.cohort + " not found", HttpStatus.BAD_REQUEST); + } + + Cohort cohort = optionalCohort.get(); + + Profile newProfile = null; + try { + newProfile = new Profile( + user, + newUserProfile.first_name, + newUserProfile.last_name, + newUserProfile.username, + "https://github.com/" + newUserProfile.github_username, + newUserProfile.mobile, + newUserProfile.bio, + role, + newUserProfile.specialism, + cohort, + newUserProfile.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); + + + try { + return new ResponseEntity<>(userRepository.save(user), HttpStatus.OK); + } catch (DataIntegrityViolationException e) { + return new ResponseEntity<>("User has an existing profile", HttpStatus.BAD_REQUEST); + } + + } + + @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(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<>("Only users with the STUDENT role can be viewed.", HttpStatus.BAD_REQUEST); + } + + if (studentRequest.getPhoto() != null) { + profile.setPhoto(studentRequest.getPhoto()); + } + + if (studentRequest.getFirst_name() != null) { + profile.setFirstName(studentRequest.getFirst_name()); + } + + if (studentRequest.getLast_name() != null) { + profile.setLastName(studentRequest.getLast_name()); + } + + if (studentRequest.getUsername() != null) { + profile.setUsername(studentRequest.getUsername()); + } + + if (studentRequest.getGithub_username() != null) { + profile.setGithubUrl(studentRequest.getGithub_username()); + } + + Cohort cohort = cohortRepository.findById(studentRequest.getCohortId()).orElse(null); + if (cohort!=null) { + profile.setCohort(cohort); + } + + + if (studentRequest.getEmail() != null) { + boolean emailExists = userRepository.existsByEmail(studentRequest.getEmail()); + if (emailExists && !studentRequest.getEmail().equals(user.getEmail())){ + return new ResponseEntity<>("Email is already in use", HttpStatus.BAD_REQUEST); + } + user.setEmail(studentRequest.getEmail()); + } + + if (studentRequest.getMobile() != null) { + profile.setMobile(studentRequest.getMobile()); + } + + if (studentRequest.getPassword() != null && !studentRequest.getPassword().isBlank()) { + user.setPassword(encoder.encode(studentRequest.getPassword())); + } + + if (studentRequest.getBio() != null) { + profile.setBio(studentRequest.getBio()); + } + + profileRepository.save(profile); + + user.setProfile(profile); + + 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..abb9fe1 --- /dev/null +++ b/src/main/java/com/booleanuk/cohorts/controllers/TeacherController.java @@ -0,0 +1,156 @@ +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.request.TeacherRequest; +import com.booleanuk.cohorts.payload.response.ProfileListResponse; +import com.booleanuk.cohorts.payload.response.Response; +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; + + @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); + } + + @GetMapping("{id}") + public ResponseEntity getTeachersByCohortId(@PathVariable int id){ + Cohort cohort = cohortRepository.findById(id).orElse(null); + if (cohort == null){ + return new ResponseEntity<>("Cohort not found", HttpStatus.NOT_FOUND); + } + + List allProfiles = 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()) && + profile.getCohort().getId() == cohort.getId()) { + teachers.add(profile); + } + } + ProfileListResponse teacherListResponse = new ProfileListResponse(); + teacherListResponse.set(teachers); + + return ResponseEntity.ok(teacherListResponse); + } + + @PatchMapping("{id}") + public ResponseEntity updateTeacher(@PathVariable int id, @RequestBody TeacherRequest teacherRequest) { + + 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_STUDENT")) { + return new ResponseEntity<>("Only users with the TEACHER role can be viewed.", HttpStatus.BAD_REQUEST); + } + + if (teacherRequest.getPhoto() != null) { + profile.setPhoto(teacherRequest.getPhoto()); + } + + if (teacherRequest.getFirst_name() != null) { + profile.setFirstName(teacherRequest.getFirst_name()); + } + + if (teacherRequest.getLast_name() != null) { + profile.setLastName(teacherRequest.getLast_name()); + } + + if (teacherRequest.getUsername() != null) { + profile.setUsername(teacherRequest.getUsername()); + } + + if (teacherRequest.getGithub_username() != null) { + profile.setGithubUrl(teacherRequest.getGithub_username()); + } + + if (teacherRequest.getEmail() != null) { + boolean emailExists = userRepository.existsByEmail(teacherRequest.getEmail()); + if (emailExists && !teacherRequest.getEmail().equals(user.getEmail())){ + return new ResponseEntity<>("Email is already in use", HttpStatus.BAD_REQUEST); + } + user.setEmail(teacherRequest.getEmail()); + } + + if (teacherRequest.getMobile() != null) { + profile.setMobile(teacherRequest.getMobile()); + } + + if (teacherRequest.getPassword() != null && !teacherRequest.getPassword().isBlank()) { + user.setPassword(encoder.encode(teacherRequest.getPassword())); + } + + if (teacherRequest.getBio() != null) { + profile.setBio(teacherRequest.getBio()); + } + + profileRepository.save(profile); + + user.setProfile(profile); + + 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/UserController.java b/src/main/java/com/booleanuk/cohorts/controllers/UserController.java index 1261544..9a413b8 100644 --- a/src/main/java/com/booleanuk/cohorts/controllers/UserController.java +++ b/src/main/java/com/booleanuk/cohorts/controllers/UserController.java @@ -1,12 +1,34 @@ package com.booleanuk.cohorts.controllers; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import jakarta.transaction.Transactional; +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.dao.DataIntegrityViolationException; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; @@ -18,6 +40,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 +53,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 +70,72 @@ 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); + } + + // 1. Fjern likes brukeren selv har gitt + for (Post likedPost : new HashSet<>(user.getLikedPosts())) { + likedPost.getLikedByUsers().remove(user); + } + user.getLikedPosts().clear(); + + // 2. Fjern likes andre brukere har gitt på brukerens egne posts + for (Post post : new HashSet<>(user.getPosts())) { + for (User liker : new HashSet<>(post.getLikedByUsers())) { + liker.getLikedPosts().remove(post); + } + post.getLikedByUsers().clear(); + } + + UserResponse userResponse = new UserResponse(); + userResponse.set(user); + + try { + userRepository.delete(user); // cascade sletter posts, comments, profile, notes + return ResponseEntity.ok(userResponse); + } catch (Exception e) { + return new ResponseEntity<>("Could not delete user: " + e.getMessage(), 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..2201c10 100644 --- a/src/main/java/com/booleanuk/cohorts/models/Cohort.java +++ b/src/main/java/com/booleanuk/cohorts/models/Cohort.java @@ -1,10 +1,19 @@ package com.booleanuk.cohorts.models; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; + + import jakarta.persistence.*; +import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; +import java.time.LocalDate; +import java.util.List; + + @NoArgsConstructor +@AllArgsConstructor @Data @Entity @Table(name = "cohorts") @@ -13,7 +22,57 @@ public class Cohort { @GeneratedValue(strategy = GenerationType.IDENTITY) private int id; + @Column + private String name; + + @Column + private LocalDate startDate; + + @Column + private LocalDate endDate; + + @ManyToOne + @JoinColumn(name = "course_id") + @JsonIgnoreProperties("cohorts") + private Course course; + + @OneToMany(mappedBy = "cohort", fetch = FetchType.LAZY) + @JsonIgnoreProperties("cohort") + private List profiles; + + public Cohort(int id) { this.id = id; } + + + + public Cohort(String name, List profiles, LocalDate startDate, LocalDate endDate, Course course){ + this.name = name; + this.profiles = profiles; + this.course = course; + } + + public Cohort(String name){ + this.name = name; + } + + public Cohort(String name,LocalDate startDate, LocalDate endDate ,Course course){ + this.name = name; + this.startDate = startDate; + this.endDate = endDate; + this.course = course; + } + + public Cohort(int id, String name) { + this.id = id; + this.name = name; + } + + @Override + public String toString(){ + return "Cohort Id: " + this.id + "\n" + + "Start date: " + this.startDate + + "End date: " + this.startDate; + } } 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..be637d6 --- /dev/null +++ b/src/main/java/com/booleanuk/cohorts/models/Comment.java @@ -0,0 +1,50 @@ +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; + + +@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..05c7efe --- /dev/null +++ b/src/main/java/com/booleanuk/cohorts/models/Course.java @@ -0,0 +1,54 @@ +package com.booleanuk.cohorts.models; + +import com.fasterxml.jackson.annotation.JsonIncludeProperties; +import jakarta.persistence.*; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.time.LocalDate; +import java.util.List; + + +@NoArgsConstructor +@AllArgsConstructor +@Data +@Entity +@Table(name = "courses") +public class Course { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private int id; + + @Column + private String name; + + @OneToMany(mappedBy = "course", cascade = CascadeType.ALL, orphanRemoval = true) + @JsonIncludeProperties("id") + private List cohorts; + + @Column + private LocalDate startDate; + + @Column + private LocalDate endDate; + + public Course(String name) { + this.name = name; + } + + public Course(String name, LocalDate startDate, LocalDate endDate, List cohorts){ + this.name = name; + this.startDate = startDate; + this.endDate = endDate; + this.cohorts = cohorts; + } + + @Override + public String toString(){ + + return "Course Name: " + this.name; + } + +} diff --git a/src/main/java/com/booleanuk/cohorts/models/Note.java b/src/main/java/com/booleanuk/cohorts/models/Note.java new file mode 100644 index 0000000..3eda85b --- /dev/null +++ b/src/main/java/com/booleanuk/cohorts/models/Note.java @@ -0,0 +1,39 @@ +package com.booleanuk.cohorts.models; + +import jakarta.persistence.*; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.time.LocalDate; + +@Data +@Table(name = "notes") +@Entity +@AllArgsConstructor +@NoArgsConstructor +public class Note { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private int id; + + @Column + private String title; + + @Column + private String description; + + @Column + private LocalDate created; + + @ManyToOne + @JoinColumn(name = "user_id") + private User user; + + public Note(User user, String title, String description) { + this.user = user; + this.title = title; + this.description = description; + } +} diff --git a/src/main/java/com/booleanuk/cohorts/models/Post.java b/src/main/java/com/booleanuk/cohorts/models/Post.java index 3d879ba..97f94c5 100644 --- a/src/main/java/com/booleanuk/cohorts/models/Post.java +++ b/src/main/java/com/booleanuk/cohorts/models/Post.java @@ -1,14 +1,21 @@ package com.booleanuk.cohorts.models; +import java.time.OffsetDateTime; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import jakarta.persistence.*; import lombok.AllArgsConstructor; import lombok.Data; +import lombok.EqualsAndHashCode; import lombok.NoArgsConstructor; @NoArgsConstructor @AllArgsConstructor @Data +@EqualsAndHashCode(exclude = {"user", "comments"}) @Entity @Table(name = "posts") public class Post { @@ -19,27 +26,67 @@ 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; + + @ManyToMany(mappedBy = "likedPosts") + private Set likedByUsers = new HashSet<>(); + @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, orphanRemoval = true, 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..b9c6906 100644 --- a/src/main/java/com/booleanuk/cohorts/models/Profile.java +++ b/src/main/java/com/booleanuk/cohorts/models/Profile.java @@ -1,12 +1,30 @@ package com.booleanuk.cohorts.models; + +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; @NoArgsConstructor @Data +@EqualsAndHashCode(exclude = {"user", "cohort", "role"}) @Entity @Table(name = "profiles") public class Profile { @@ -14,32 +32,70 @@ 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; + + + @ManyToOne + @JoinColumn(name = "cohort_id") + @JsonIgnoreProperties({"profiles", "users", "deliveryLogs", "cohortCourses"}) + private Cohort cohort; + + @ManyToOne + @JoinColumn(name = "role_id") + @JsonIgnoreProperties("cohort") + private Role role; + + @Column(length = 2500000) + 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, 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.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..1652dbb 100644 --- a/src/main/java/com/booleanuk/cohorts/models/User.java +++ b/src/main/java/com/booleanuk/cohorts/models/User.java @@ -1,18 +1,21 @@ 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; @NoArgsConstructor @Data +@EqualsAndHashCode(exclude = {"profile", "posts", "comments", "likedPosts"}) @Entity @Table(name = "users", uniqueConstraints = { @@ -30,6 +33,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 @@ -38,21 +42,39 @@ public class User { @ManyToMany(fetch = FetchType.LAZY) @JoinTable(name = "user_roles", joinColumns = @JoinColumn(name = "user_id"), inverseJoinColumns = @JoinColumn(name = "role_id")) + @JsonIgnore private Set roles = new HashSet<>(); - @ManyToOne - @JoinColumn(name = "cohort_id", nullable = true) - @JsonIgnoreProperties("users") - private Cohort cohort; + @OneToMany(mappedBy = "user", cascade = CascadeType.ALL, orphanRemoval = true) + @JsonIncludeProperties({"id", "content", "likes", "timeCreated", "timeUpdated" }) + private List posts; + + @ManyToMany(cascade = CascadeType.ALL) + @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"}) + private Profile profile; + + @OneToMany(mappedBy = "user", cascade = CascadeType.ALL, orphanRemoval = true) + private List notes; public User(String email, String password) { this.email = email; this.password = password; } - public User(String email, String password, Cohort cohort) { + public User(String email, String password, 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..919f410 --- /dev/null +++ b/src/main/java/com/booleanuk/cohorts/payload/request/CohortRequest.java @@ -0,0 +1,16 @@ +package com.booleanuk.cohorts.payload.request; + +import lombok.Getter; +import lombok.Setter; + + +@Getter +@Setter +public class CohortRequest { + private String name; + private int courseId; + private String startDate; + private String endDate; + + + public CohortRequest(){}} diff --git a/src/main/java/com/booleanuk/cohorts/payload/request/CohortRequestWithProfiles.java b/src/main/java/com/booleanuk/cohorts/payload/request/CohortRequestWithProfiles.java new file mode 100644 index 0000000..0d5c954 --- /dev/null +++ b/src/main/java/com/booleanuk/cohorts/payload/request/CohortRequestWithProfiles.java @@ -0,0 +1,18 @@ +package com.booleanuk.cohorts.payload.request; + +import lombok.Getter; +import lombok.Setter; + +import java.util.List; + +@Getter +@Setter +public class CohortRequestWithProfiles { + private String name; + private int courseId; + private String startDate; + private String endDate; + private List profileIds; + + public CohortRequestWithProfiles(){}} + 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..97900ae --- /dev/null +++ b/src/main/java/com/booleanuk/cohorts/payload/request/CommentRequest.java @@ -0,0 +1,22 @@ +package com.booleanuk.cohorts.payload.request; + +import jakarta.validation.constraints.NotBlank; +import lombok.Getter; +import lombok.Setter; + +@Setter +@Getter +public class CommentRequest { + @NotBlank + private String body; + + private int userId; + + public CommentRequest() {} + + public CommentRequest(String body, int userId) { + this.body = body; + 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..ec4f3a8 --- /dev/null +++ b/src/main/java/com/booleanuk/cohorts/payload/request/CourseRequest.java @@ -0,0 +1,24 @@ +package com.booleanuk.cohorts.payload.request; + +import jakarta.validation.constraints.NotBlank; +import lombok.Getter; + +import java.time.LocalDate; + +@Getter +public class CourseRequest { + @NotBlank + private String name; + private String startDate; + private String endDate; + + public CourseRequest() {} + + public CourseRequest(String name, String startDate, String endDate){ + this.name = name; + this.startDate = startDate; + this.endDate = endDate; + } + + +} diff --git a/src/main/java/com/booleanuk/cohorts/payload/request/NoteReqeuest.java b/src/main/java/com/booleanuk/cohorts/payload/request/NoteReqeuest.java new file mode 100644 index 0000000..64eed93 --- /dev/null +++ b/src/main/java/com/booleanuk/cohorts/payload/request/NoteReqeuest.java @@ -0,0 +1,13 @@ +package com.booleanuk.cohorts.payload.request; + +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@NoArgsConstructor +public class NoteReqeuest { + + private String title; + private String description; + private int user_id; +} 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..3e976eb --- /dev/null +++ b/src/main/java/com/booleanuk/cohorts/payload/request/PostRequest.java @@ -0,0 +1,21 @@ +package com.booleanuk.cohorts.payload.request; + +import jakarta.validation.constraints.NotBlank; +import lombok.Getter; +import lombok.Setter; + +@Setter +@Getter +public class PostRequest { + @NotBlank + private String content; + + private int userId; + + public PostRequest() {} + + public PostRequest(String content, int userId) { + this.content = content; + 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..e1a5e13 --- /dev/null +++ b/src/main/java/com/booleanuk/cohorts/payload/request/ProfileRequest.java @@ -0,0 +1,12 @@ +package com.booleanuk.cohorts.payload.request; + +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +public class ProfileRequest { + private int profileId; + + public ProfileRequest(){} +} diff --git a/src/main/java/com/booleanuk/cohorts/payload/request/SignupRequest.java b/src/main/java/com/booleanuk/cohorts/payload/request/SignupRequest.java index 0fc94de..3329932 100644 --- a/src/main/java/com/booleanuk/cohorts/payload/request/SignupRequest.java +++ b/src/main/java/com/booleanuk/cohorts/payload/request/SignupRequest.java @@ -17,11 +17,14 @@ public class SignupRequest { @Email private String email; - private Set role; - @NotBlank @Size(min = 6, max = 40) private String password; private Cohort cohort; + + public SignupRequest(String email, String password) { + this.email = email; + this.password = password; + } } 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..7797ae4 --- /dev/null +++ b/src/main/java/com/booleanuk/cohorts/payload/request/StudentRequest.java @@ -0,0 +1,24 @@ +package com.booleanuk.cohorts.payload.request; + +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +public class StudentRequest { + + private int cohortId; + 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() { + } +} + 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/request/TeacherRequest.java b/src/main/java/com/booleanuk/cohorts/payload/request/TeacherRequest.java new file mode 100644 index 0000000..c5a3ac3 --- /dev/null +++ b/src/main/java/com/booleanuk/cohorts/payload/request/TeacherRequest.java @@ -0,0 +1,22 @@ +package com.booleanuk.cohorts.payload.request; + +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +public class TeacherRequest { + + 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 TeacherRequest() { + } +} 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/payload/response/RefreshTokenResponse.java b/src/main/java/com/booleanuk/cohorts/payload/response/RefreshTokenResponse.java new file mode 100644 index 0000000..182542c --- /dev/null +++ b/src/main/java/com/booleanuk/cohorts/payload/response/RefreshTokenResponse.java @@ -0,0 +1,16 @@ +package com.booleanuk.cohorts.payload.response; + +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +public class RefreshTokenResponse { + private String token; + private String message; + + public RefreshTokenResponse(String token, String message) { + this.token = token; + this.message = message; + } +} \ 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/NoteRepository.java b/src/main/java/com/booleanuk/cohorts/repository/NoteRepository.java new file mode 100644 index 0000000..3890e77 --- /dev/null +++ b/src/main/java/com/booleanuk/cohorts/repository/NoteRepository.java @@ -0,0 +1,7 @@ +package com.booleanuk.cohorts.repository; + +import com.booleanuk.cohorts.models.Note; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface NoteRepository extends JpaRepository { +} diff --git a/src/main/java/com/booleanuk/cohorts/repository/PostRepository.java b/src/main/java/com/booleanuk/cohorts/repository/PostRepository.java index 4583ecf..e7d4124 100644 --- a/src/main/java/com/booleanuk/cohorts/repository/PostRepository.java +++ b/src/main/java/com/booleanuk/cohorts/repository/PostRepository.java @@ -2,6 +2,24 @@ import com.booleanuk.cohorts.models.Post; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Modifying; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; +import java.util.Optional; public interface PostRepository extends JpaRepository { + + @Query("SELECT p FROM Post p JOIN FETCH p.user u LEFT JOIN FETCH u.profile pr LEFT JOIN FETCH pr.cohort WHERE p.id = :id") + Optional findByIdWithUser(@Param("id") int id); + + @Query("SELECT p FROM Post p JOIN FETCH p.user u LEFT JOIN FETCH u.profile pr LEFT JOIN FETCH pr.cohort") + List findAllWithUsers(); + + @Modifying + @Transactional + @Query(value = "DELETE FROM user_liked_posts WHERE post_id = :postId", nativeQuery = true) + void removeLikedPostFromAllUsers(@Param("postId") int postId); } 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..1458fa0 100644 --- a/src/main/java/com/booleanuk/cohorts/repository/UserRepository.java +++ b/src/main/java/com/booleanuk/cohorts/repository/UserRepository.java @@ -1,13 +1,27 @@ package com.booleanuk.cohorts.repository; -import com.booleanuk.cohorts.models.User; +import java.util.List; +import java.util.Optional; + import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Modifying; +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); + + @Query("SELECT u FROM User u LEFT JOIN FETCH u.profile LEFT JOIN FETCH u.roles WHERE u.id = :id") + Optional findByIdWithProfile(@Param("id") Integer id); + Boolean existsByEmail(String email); + + List getTopById(int id); } diff --git a/src/main/java/com/booleanuk/cohorts/security/WebSecurityConfig.java b/src/main/java/com/booleanuk/cohorts/security/WebSecurityConfig.java index ad90f26..4ceef15 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,17 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { .authorizeHttpRequests((requests) -> requests .requestMatchers("/login", "/login/**").permitAll() .requestMatchers("/signup", "/signup/**").permitAll() + .requestMatchers("/auth/refresh", "/auth/refresh/**").permitAll() + .requestMatchers("/profiles", "/profiles/**").authenticated() + .requestMatchers("/students", "/students/**").authenticated() + .requestMatchers("/notes", "/notes/**").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..b46c699 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,12 @@ public String generateJwtToken(Authentication authentication) { return Jwts.builder() .subject((userPrincipal.getUsername())) + .claim("userId", userPrincipal.getId()) + .claim("firstName", userPrincipal.getFirstName()) + .claim("lastName", userPrincipal.getLastName()) + .claim("roleId", userPrincipal.getRoleId()) + .claim("specialism", userPrincipal.getSpecialism()) + .claim("cohortId", userPrincipal.getCohortId()) .issuedAt(new Date()) .expiration(new Date((new Date()).getTime() + this.jwtExpirationMs)) .signWith(this.key()) @@ -45,6 +54,30 @@ 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 Integer getRoleIdFromJwtToken(String token) { + return Jwts.parser().verifyWith(this.key()).build().parseSignedClaims(token).getPayload().get("roleId", Integer.class); + } + + public String getSpecialismFromJwtToken(String token) { + return Jwts.parser().verifyWith(this.key()).build().parseSignedClaims(token).getPayload().get("specialism", String.class); + } + + public Integer getCohortIdFromJwtToken(String token) { + return Jwts.parser().verifyWith(this.key()).build().parseSignedClaims(token).getPayload().get("cohortId", Integer.class); + } + public boolean validateJwtToken(String authToken) { try { Jwts.parser().verifyWith(this.key()).build().parse(authToken); @@ -60,5 +93,43 @@ public boolean validateJwtToken(String authToken) { } return false; } + + public boolean validateJwtTokenForRefresh(String authToken) { + try { + Jwts.parser().verifyWith(this.key()).build().parse(authToken); + return true; + } catch (MalformedJwtException e) { + logger.error("Invalid JWT token: {}", e.getMessage()); + return false; + } catch (ExpiredJwtException e) { + // For refresh, we allow expired tokens to be processed + logger.info("JWT token has expired but allowing for refresh: {}", e.getMessage()); + return true; + } catch (UnsupportedJwtException e) { + logger.error("JWT token is unsupported: {}", e.getMessage()); + return false; + } catch (IllegalArgumentException e) { + logger.error("JWT claims string is empty: {}", e.getMessage()); + return false; + } + } + + public String getUserNameFromExpiredJwtToken(String token) { + try { + return Jwts.parser().verifyWith(this.key()).build().parseSignedClaims(token).getPayload().getSubject(); + } catch (ExpiredJwtException e) { + // Extract username from expired token + return e.getClaims().getSubject(); + } + } + + public Integer getUserIdFromExpiredJwtToken(String token) { + try { + return Jwts.parser().verifyWith(this.key()).build().parseSignedClaims(token).getPayload().get("userId", Integer.class); + } catch (ExpiredJwtException e) { + // Extract userId from expired token + return e.getClaims().get("userId", Integer.class); + } + } } 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..4a7e3a2 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,47 @@ 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; + private final Integer roleId; + private final String specialism; + private final Integer cohortId; @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, Integer roleId, String specialism, Integer cohortId, Collection authorities) { this.id = id; this.username = email; this.email = email; this.password = password; + this.firstName = firstName; + this.lastName = lastName; + this.roleId = roleId; + this.specialism = specialism; + this.cohortId = cohortId; this.authorities = authorities; } @@ -37,11 +49,41 @@ 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 = ""; + String specialism = ""; + Integer cohortId = null; + + if (user.getProfile() != null) { + firstName = user.getProfile().getFirstName() != null ? user.getProfile().getFirstName() : ""; + lastName = user.getProfile().getLastName() != null ? user.getProfile().getLastName() : ""; + specialism = user.getProfile().getSpecialism() != null ? user.getProfile().getSpecialism() : ""; + if (user.getProfile().getCohort() != null) { + cohortId = user.getProfile().getCohort().getId(); + } + System.out.println("Profile found for user " + user.getEmail() + ": " + firstName + " " + lastName); + } else { + System.out.println("No profile found for user " + user.getEmail()); + } + + // Get the first role's ID (assuming users have only one primary role) + Integer roleId = null; + if (!user.getRoles().isEmpty()) { + roleId = user.getRoles().iterator().next().getId(); + } + return new UserDetailsImpl( user.getId(), user.getUsername(), user.getEmail(), user.getPassword(), + firstName, + lastName, + roleId, + specialism, + cohortId, 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 diff --git a/src/test/java/com/booleanuk/TeamDevSim/TeamDevSimApplicationTests.java b/src/test/java/com/booleanuk/TeamDevSim/TeamDevSimApplicationTests.java deleted file mode 100644 index b174732..0000000 --- a/src/test/java/com/booleanuk/TeamDevSim/TeamDevSimApplicationTests.java +++ /dev/null @@ -1,13 +0,0 @@ -package com.booleanuk.TeamDevSim; - -import org.junit.jupiter.api.Test; -import org.springframework.boot.test.context.SpringBootTest; - -@SpringBootTest -class TeamDevSimApplicationTests { - - @Test - void contextLoads() { - } - -} diff --git a/src/test/java/com/booleanuk/controllerTests/CohortControllerTest.java b/src/test/java/com/booleanuk/controllerTests/CohortControllerTest.java new file mode 100644 index 0000000..3f4b6f4 --- /dev/null +++ b/src/test/java/com/booleanuk/controllerTests/CohortControllerTest.java @@ -0,0 +1,257 @@ +package com.booleanuk.controllerTests; + +import com.booleanuk.cohorts.controllers.AuthController; +import com.booleanuk.cohorts.controllers.ProfileController; +import com.booleanuk.cohorts.models.Cohort; +import com.booleanuk.cohorts.models.Profile; +import com.booleanuk.cohorts.models.User; +import com.booleanuk.cohorts.models.Role; +import com.booleanuk.cohorts.models.ERole; +import com.booleanuk.cohorts.payload.request.SignupRequest; +import com.booleanuk.cohorts.repository.ProfileRepository; +import com.booleanuk.cohorts.repository.UserRepository; +import com.booleanuk.cohorts.repository.RoleRepository; +import com.booleanuk.cohorts.repository.CohortRepository; +import com.booleanuk.cohorts.security.services.UserDetailsImpl; +import jakarta.persistence.EntityManager; +import jakarta.persistence.PersistenceContext; +import jakarta.servlet.ServletContext; +import org.json.JSONArray; +import org.json.JSONObject; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.mock.web.MockServletContext; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.context.web.WebAppConfiguration; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.MvcResult; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.context.WebApplicationContext; + +import static org.junit.jupiter.api.Assertions.*; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; +import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@WebAppConfiguration +@SpringBootTest +@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE) +@Transactional +@DirtiesContext(classMode = DirtiesContext.ClassMode.BEFORE_EACH_TEST_METHOD) +public class CohortControllerTest { + @Autowired + private WebApplicationContext webApplicationContext; + + @Autowired + private UserRepository userRepository; + + @Autowired + private RoleRepository roleRepository; + + @Autowired + private CohortRepository cohortRepository; + + @Autowired + AuthController authController; + + @Autowired + ProfileController profileController; + + @Autowired + ProfileRepository profileRepository; + + @PersistenceContext + private EntityManager entityManager; + + private MockMvc mockMvc; + + private int actualUserId; + private int testCohortId; + + private User testUser; + + @BeforeEach + public void setup() throws Exception { + this.mockMvc = MockMvcBuilders.webAppContextSetup(this.webApplicationContext).build(); + + profileRepository.deleteAll(); + userRepository.deleteAll(); + roleRepository.deleteAll(); + cohortRepository.deleteAll(); + entityManager.flush(); + entityManager.clear(); + + + Role teacherRole = new Role(ERole.ROLE_TEACHER); + Role studentRole = new Role(ERole.ROLE_STUDENT); + roleRepository.save(teacherRole); + roleRepository.save(studentRole); + entityManager.flush(); + + + Cohort testCohort = new Cohort(); + testCohort = cohortRepository.save(testCohort); + testCohortId = testCohort.getId(); + entityManager.flush(); + + SignupRequest signupRequest = new SignupRequest("thomas@ladder.com", "@Qwerty12345"); + this.authController.registerUser(signupRequest); + entityManager.flush(); + + testUser = userRepository.findAll().get(0); + actualUserId = testUser.getId(); + + ProfileController.PostProfile postProfile = new ProfileController.PostProfile( + actualUserId, + "Thomas", + "Ladder", + "gottaStepUp", + "244783772", + "tallerThanU", + "I need a ladder, but can't afford one. So, steps will have to be taken", + "ROLE_TEACHER", + "Big moves", + testCohortId, + "1999-01-01", + "2039-01-01", + "https://media.makeameme.org/created/ladder-i.jpg" + ); + ResponseEntity profileRegisterResponse = this.profileController.createProfile(postProfile); + entityManager.flush(); + entityManager.clear(); + + // Refresh the user entity to get the updated state with profile + testUser = userRepository.findById(actualUserId).orElse(null); + } + + private void authenticateUser(User user) { + UserDetailsImpl userDetails = UserDetailsImpl.build(user); + UsernamePasswordAuthenticationToken authentication = + new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities()); + SecurityContextHolder.getContext().setAuthentication(authentication); + } + + @Test + public void heuristics_testClassSetup() { + ServletContext servletContext = webApplicationContext.getServletContext(); + + assertNotNull(servletContext); + assertTrue(servletContext instanceof MockServletContext); + assertNotNull(webApplicationContext.getBean("cohortController")); + } + + @Test + public void tryGetAllCohorts_testFirstNameOnFirstProfile_withSingleProfileInDb() throws Exception { + MvcResult result = this.mockMvc.perform(get("/cohorts") + .accept(MediaType.APPLICATION_JSON)) + .andDo(print()) + .andExpect(status().isOk()) + .andReturn(); + + JSONObject response = new JSONObject(result.getResponse().getContentAsString()).getJSONObject("data"); + JSONArray cohorts = response.getJSONArray("cohorts"); + assertNotNull(cohorts); + + JSONObject firstProfile = cohorts.getJSONObject(0).getJSONArray("profiles").getJSONObject(0); + assertEquals(firstProfile.getString("firstName"), "Thomas"); + } + + @Test + public void tryGetCohortsById_testEmailOnFirstProfile_withSingleProfileInDb() throws Exception { + MvcResult result = this.mockMvc.perform(get("/cohorts/" + testCohortId) + .accept(MediaType.APPLICATION_JSON)) + .andDo(print()) + .andExpect(status().isOk()) + .andReturn(); + + JSONObject response = new JSONObject(result.getResponse().getContentAsString()).getJSONObject("data").getJSONObject("cohort"); + assertNotNull(response); + + JSONObject firstProfile = response.getJSONArray("profiles").getJSONObject(0); + assertEquals(firstProfile.getString("firstName"), "Thomas"); + + } + + @Test + public void tryGetCohortsByUserId_testEmailOnFirstProfile_withSingleProfileInDb() throws Exception { + MvcResult result = this.mockMvc.perform(get("/cohorts/teacher/" + actualUserId)) + .andDo(print()) + .andExpect(status().isOk()) + .andReturn(); + + JSONObject response = new JSONObject(result.getResponse().getContentAsString()).getJSONObject("data").getJSONObject("cohort"); + assertNotNull(response); + + JSONObject firstProfile = response.getJSONArray("profiles").getJSONObject(0); + assertEquals(firstProfile.getString("firstName"), "Thomas"); + assertEquals(firstProfile.getJSONObject("user").getString("email"), "thomas@ladder.com"); + } + + @Test + public void tryAddStudentToCohort_checkProfileObjectAndResponseBody_withSingleProfileInDb() throws Exception { + Cohort secondTestCohort = new Cohort(); + secondTestCohort = cohortRepository.save(secondTestCohort); + int secondTestCohortId = secondTestCohort.getId(); + entityManager.flush(); + + SignupRequest studentSignupRequest = new SignupRequest("student@test.com", "@Student123"); + this.authController.registerUser(studentSignupRequest); + entityManager.flush(); + + User studentUser = userRepository.findByEmail("student@test.com").orElse(null); + assertNotNull(studentUser); + + ProfileController.PostProfile studentPostProfile = new ProfileController.PostProfile( + studentUser.getId(), + "Fritjof", + "Ladderson", + "BigLadderMan", + "748337483784", + "bigLadderMan", + "I invented the upside down ladder", + "ROLE_STUDENT", + "Alternative ladders", + secondTestCohortId, + "1999-01-01", + "2040-01-01", + "https://example.com/ladder.jpg" + ); + this.profileController.createProfile(studentPostProfile); + entityManager.flush(); + + Profile studentProfile = profileRepository.findById(studentUser.getId()).orElse(null); + assertNotNull(studentProfile); + + authenticateUser(testUser); + + String requestBody = "{\"profileId\":" + studentProfile.getId() + "}"; + + MvcResult result = this.mockMvc.perform(patch("/cohorts/teacher/" + testCohortId) + .contentType(MediaType.APPLICATION_JSON) + .content(requestBody)) + .andDo(print()) + .andExpect(status().isOk()) + .andReturn(); + + JSONObject response = new JSONObject(result.getResponse().getContentAsString()); + assertNotNull(response); + + assertEquals(testCohortId, response.getJSONObject("cohort").getInt("id")); + assertEquals("Fritjof", response.getString("firstName")); + assertEquals("Ladderson", response.getString("lastName")); + + Profile updatedProfile = profileRepository.findById(studentProfile.getId()).orElse(null); + assertNotNull(updatedProfile); + assertNotNull(updatedProfile.getCohort()); + assertEquals(testCohortId, updatedProfile.getCohort().getId()); + } + +} diff --git a/src/test/java/com/booleanuk/controllerTests/CourseControllerTest.java b/src/test/java/com/booleanuk/controllerTests/CourseControllerTest.java new file mode 100644 index 0000000..e7ab7bf --- /dev/null +++ b/src/test/java/com/booleanuk/controllerTests/CourseControllerTest.java @@ -0,0 +1,303 @@ +package com.booleanuk.controllerTests; + +import com.booleanuk.cohorts.controllers.AuthController; +import com.booleanuk.cohorts.controllers.ProfileController; +import com.booleanuk.cohorts.models.*; +import com.booleanuk.cohorts.payload.request.SignupRequest; +import com.booleanuk.cohorts.repository.*; +import com.booleanuk.cohorts.security.services.UserDetailsImpl; +import jakarta.persistence.EntityManager; +import jakarta.persistence.PersistenceContext; +import jakarta.servlet.ServletContext; +import org.json.JSONArray; +import org.json.JSONObject; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.mock.web.MockServletContext; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.context.web.WebAppConfiguration; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.MvcResult; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.context.WebApplicationContext; + +import java.time.LocalDate; + +import static org.junit.jupiter.api.Assertions.*; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; +import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@WebAppConfiguration +@SpringBootTest +@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE) +@Transactional +@DirtiesContext(classMode = DirtiesContext.ClassMode.BEFORE_EACH_TEST_METHOD) +public class CourseControllerTest { + @Autowired + private WebApplicationContext webApplicationContext; + + @Autowired + private UserRepository userRepository; + + @Autowired + private RoleRepository roleRepository; + + @Autowired + private CohortRepository cohortRepository; + + @Autowired + private CourseRepository courseRepository; + + @Autowired + AuthController authController; + + @Autowired + ProfileController profileController; + + @Autowired + ProfileRepository profileRepository; + + @PersistenceContext + private EntityManager entityManager; + + private MockMvc mockMvc; + + private int testCourseId; + private int testCohortId; + private User testTeacherUser; + private User testStudentUser; + + @BeforeEach + public void setup() throws Exception { + this.mockMvc = MockMvcBuilders.webAppContextSetup(this.webApplicationContext).build(); + + profileRepository.deleteAll(); + cohortRepository.deleteAll(); + courseRepository.deleteAll(); + userRepository.deleteAll(); + roleRepository.deleteAll(); + entityManager.flush(); + entityManager.clear(); + + Role teacherRole = new Role(ERole.ROLE_TEACHER); + Role studentRole = new Role(ERole.ROLE_STUDENT); + roleRepository.save(teacherRole); + roleRepository.save(studentRole); + entityManager.flush(); + + Course testCourse = new Course(); + testCourse.setName("Java Development"); + testCourse.setStartDate(LocalDate.parse("2024-01-01")); + testCourse.setEndDate(LocalDate.parse("2024-06-01")); + testCourse = courseRepository.save(testCourse); + testCourseId = testCourse.getId(); + entityManager.flush(); + + Cohort testCohort = new Cohort(); + testCohort.setName("Java Cohort 1"); + testCohort.setCourse(testCourse); + testCohort = cohortRepository.save(testCohort); + testCohortId = testCohort.getId(); + entityManager.flush(); + + SignupRequest teacherSignupRequest = new SignupRequest("teacher@test.com", "@Teacher123"); + this.authController.registerUser(teacherSignupRequest); + entityManager.flush(); + + testTeacherUser = userRepository.findByEmail("teacher@test.com").orElse(null); + + ProfileController.PostProfile teacherPostProfile = new ProfileController.PostProfile( + testTeacherUser.getId(), + "John", + "Teacher", + "johnTeacher", + "123456789", + "teacherGitHub", + "I am a teacher", + "ROLE_TEACHER", + "Teaching Java", + testCohortId, + "1980-01-01", + "2030-01-01", + "https://example.com/teacher.jpg" + ); + this.profileController.createProfile(teacherPostProfile); + entityManager.flush(); + + SignupRequest studentSignupRequest = new SignupRequest("student@test.com", "@Student123"); + this.authController.registerUser(studentSignupRequest); + entityManager.flush(); + + testStudentUser = userRepository.findByEmail("student@test.com").orElse(null); + + ProfileController.PostProfile studentPostProfile = new ProfileController.PostProfile( + testStudentUser.getId(), + "Jane", + "Student", + "janeStudent", + "987654321", + "studentGitHub", + "I am a student", + "ROLE_STUDENT", + "Learning Java", + testCohortId, + "2000-01-01", + "2040-01-01", + "https://example.com/student.jpg" + ); + this.profileController.createProfile(studentPostProfile); + entityManager.flush(); + entityManager.clear(); + + testTeacherUser = userRepository.findById(testTeacherUser.getId()).orElse(null); + testStudentUser = userRepository.findById(testStudentUser.getId()).orElse(null); + } + + private void authenticateUser(User user) { + UserDetailsImpl userDetails = UserDetailsImpl.build(user); + UsernamePasswordAuthenticationToken authentication = + new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities()); + SecurityContextHolder.getContext().setAuthentication(authentication); + } + + @Test + public void heuristics_testClassSetup() { + ServletContext servletContext = webApplicationContext.getServletContext(); + + assertNotNull(servletContext); + assertTrue(servletContext instanceof MockServletContext); + assertNotNull(webApplicationContext.getBean("courseController")); + } + + @Test + public void tryGetAllCourses_testCourseNameAndDates_withSingleCourseInDb() throws Exception { + MvcResult result = this.mockMvc.perform(get("/courses") + .accept(MediaType.APPLICATION_JSON)) + .andDo(print()) + .andExpect(status().isOk()) + .andReturn(); + + JSONObject response = new JSONObject(result.getResponse().getContentAsString()).getJSONObject("data"); + JSONArray courses = response.getJSONArray("courses"); + assertNotNull(courses); + assertEquals(1, courses.length()); + + JSONObject firstCourse = courses.getJSONObject(0); + assertEquals("Java Development", firstCourse.getString("name")); + assertEquals("2024-01-01", firstCourse.getString("startDate")); + assertEquals("2024-06-01", firstCourse.getString("endDate")); + } + + @Test + public void tryGetCourseById_testCourseDetailsAndCohorts_withSingleCourseInDb() throws Exception { + MvcResult result = this.mockMvc.perform(get("/courses/" + testCourseId) + .accept(MediaType.APPLICATION_JSON)) + .andDo(print()) + .andExpect(status().isOk()) + .andReturn(); + + JSONObject response = new JSONObject(result.getResponse().getContentAsString()).getJSONObject("data").getJSONObject("course"); + assertNotNull(response); + + assertEquals("Java Development", response.getString("name")); + assertEquals("2024-01-01", response.getString("startDate")); + assertEquals("2024-06-01", response.getString("endDate")); + + JSONArray cohorts = response.getJSONArray("cohorts"); + assertEquals(1, cohorts.length()); + assertEquals("Java Development", response.getString("name")); + } + + @Test + public void tryGetCourseById_testNotFound_withInvalidId() throws Exception { + this.mockMvc.perform(get("/courses/999") + .accept(MediaType.APPLICATION_JSON)) + .andDo(print()) + .andExpect(status().isNotFound()) + .andReturn(); + } + + @Test + public void tryGetAllStudents_testStudentProfilesInCourse_withStudentInDb() throws Exception { + MvcResult result = this.mockMvc.perform(get("/courses/students/" + testCourseId) + .accept(MediaType.APPLICATION_JSON)) + .andDo(print()) + .andExpect(status().isOk()) + .andReturn(); + + JSONObject response = new JSONObject(result.getResponse().getContentAsString()).getJSONObject("data"); + JSONArray profiles = response.getJSONArray("profiles"); + assertNotNull(profiles); + assertEquals(1, profiles.length()); + + JSONObject studentProfile = profiles.getJSONObject(0); + assertEquals("Jane", studentProfile.getString("firstName")); + assertEquals("Student", studentProfile.getString("lastName")); + assertEquals("janeStudent", studentProfile.getString("username")); + } + + @Test + public void tryGetAllStudents_testNotFound_withInvalidCourseId() throws Exception { + this.mockMvc.perform(get("/courses/students/999") + .accept(MediaType.APPLICATION_JSON)) + .andDo(print()) + .andExpect(status().isNotFound()) + .andReturn(); + } + + @Test + public void tryCreateCourse_testCourseCreation_withValidData() throws Exception { + String requestBody = """ + { + "name": "Python Development", + "startDate": "2024-07-01", + "endDate": "2024-12-01" + } + """; + + MvcResult result = this.mockMvc.perform(post("/courses") + .contentType(MediaType.APPLICATION_JSON) + .content(requestBody)) + .andDo(print()) + .andExpect(status().isCreated()) + .andReturn(); + + JSONObject response = new JSONObject(result.getResponse().getContentAsString()).getJSONObject("data").getJSONObject("course"); + assertNotNull(response); + + assertEquals("Python Development", response.getString("name")); + assertEquals("2024-07-01", response.getString("startDate")); + assertEquals("2024-12-01", response.getString("endDate")); + + Course savedCourse = courseRepository.findById(response.getInt("id")).orElse(null); + assertNotNull(savedCourse); + assertEquals("Python Development", savedCourse.getName()); + } + + @Test + public void tryCreateCourse_testBadRequest_withBlankDates() throws Exception { + String requestBody = """ + { + "name": "Invalid Course", + "startDate": "", + "endDate": "" + } + """; + + this.mockMvc.perform(post("/courses") + .contentType(MediaType.APPLICATION_JSON) + .content(requestBody)) + .andDo(print()) + .andExpect(status().isBadRequest()) + .andReturn(); + } +} \ No newline at end of file diff --git a/src/test/java/com/booleanuk/controllerTests/DeliveryLogControllerTest.java b/src/test/java/com/booleanuk/controllerTests/DeliveryLogControllerTest.java new file mode 100644 index 0000000..3e8bcf1 --- /dev/null +++ b/src/test/java/com/booleanuk/controllerTests/DeliveryLogControllerTest.java @@ -0,0 +1,4 @@ +package com.booleanuk.controllerTests; + +public class DeliveryLogControllerTest { +} diff --git a/src/test/java/com/booleanuk/controllerTests/PostControllerTest.java b/src/test/java/com/booleanuk/controllerTests/PostControllerTest.java new file mode 100644 index 0000000..3766922 --- /dev/null +++ b/src/test/java/com/booleanuk/controllerTests/PostControllerTest.java @@ -0,0 +1,507 @@ +package com.booleanuk.controllerTests; + +import com.booleanuk.cohorts.controllers.AuthController; +import com.booleanuk.cohorts.controllers.ProfileController; +import com.booleanuk.cohorts.models.*; +import com.booleanuk.cohorts.payload.request.SignupRequest; +import com.booleanuk.cohorts.repository.*; +import com.booleanuk.cohorts.security.services.UserDetailsImpl; +import jakarta.persistence.EntityManager; +import jakarta.persistence.PersistenceContext; +import jakarta.servlet.ServletContext; +import org.json.JSONArray; +import org.json.JSONObject; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.mock.web.MockServletContext; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.context.web.WebAppConfiguration; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.MvcResult; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.context.WebApplicationContext; + +import java.time.LocalDate; + +import static org.junit.jupiter.api.Assertions.*; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; +import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@WebAppConfiguration +@SpringBootTest +@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE) +@Transactional +@DirtiesContext(classMode = DirtiesContext.ClassMode.BEFORE_EACH_TEST_METHOD) +public class PostControllerTest { + @Autowired + private WebApplicationContext webApplicationContext; + + @Autowired + private UserRepository userRepository; + + @Autowired + private RoleRepository roleRepository; + + @Autowired + private CohortRepository cohortRepository; + + @Autowired + private CourseRepository courseRepository; + + @Autowired + private PostRepository postRepository; + + @Autowired + private CommentRepository commentRepository; + + @Autowired + AuthController authController; + + @Autowired + ProfileController profileController; + + @Autowired + ProfileRepository profileRepository; + + @PersistenceContext + private EntityManager entityManager; + + private MockMvc mockMvc; + + private int testCourseId; + private int testCohortId; + private int testPostId; + private int testCommentId; + private User testUser; + private User testUser2; + + @BeforeEach + public void setup() throws Exception { + this.mockMvc = MockMvcBuilders.webAppContextSetup(this.webApplicationContext).build(); + + commentRepository.deleteAll(); + postRepository.deleteAll(); + profileRepository.deleteAll(); + cohortRepository.deleteAll(); + courseRepository.deleteAll(); + userRepository.deleteAll(); + roleRepository.deleteAll(); + entityManager.flush(); + entityManager.clear(); + + Role teacherRole = new Role(ERole.ROLE_TEACHER); + Role studentRole = new Role(ERole.ROLE_STUDENT); + roleRepository.save(teacherRole); + roleRepository.save(studentRole); + entityManager.flush(); + + Course testCourse = new Course(); + testCourse.setName("Java Development"); + testCourse.setStartDate(LocalDate.parse("2024-01-01")); + testCourse.setEndDate(LocalDate.parse("2024-06-01")); + testCourse = courseRepository.save(testCourse); + testCourseId = testCourse.getId(); + entityManager.flush(); + + Cohort testCohort = new Cohort(); + testCohort.setName("Java Cohort 1"); + testCohort.setCourse(testCourse); + testCohort = cohortRepository.save(testCohort); + testCohortId = testCohort.getId(); + entityManager.flush(); + + SignupRequest signupRequest = new SignupRequest("john@test.com", "@Password123"); + this.authController.registerUser(signupRequest); + entityManager.flush(); + + testUser = userRepository.findByEmail("john@test.com").orElse(null); + + ProfileController.PostProfile postProfile = new ProfileController.PostProfile( + testUser.getId(), + "John", + "Doe", + "johndoe", + "123456789", + "johnGitHub", + "I am a developer", + "ROLE_STUDENT", + "Learning Java", + testCohortId, + "1990-01-01", + "2030-01-01", + "https://example.com/john.jpg" + ); + this.profileController.createProfile(postProfile); + entityManager.flush(); + + SignupRequest signupRequest2 = new SignupRequest("jane@test.com", "@Password123"); + this.authController.registerUser(signupRequest2); + entityManager.flush(); + + testUser2 = userRepository.findByEmail("jane@test.com").orElse(null); + + ProfileController.PostProfile postProfile2 = new ProfileController.PostProfile( + testUser2.getId(), + "Jane", + "Smith", + "janesmith", + "987654321", + "janeGitHub", + "I am also a developer", + "ROLE_STUDENT", + "Learning Java too", + testCohortId, + "1992-01-01", + "2030-01-01", + "https://example.com/jane.jpg" + ); + this.profileController.createProfile(postProfile2); + entityManager.flush(); + + Post testPost = new Post("This is a test post", testUser, 0); + testPost = postRepository.save(testPost); + testPostId = testPost.getId(); + entityManager.flush(); + + Comment testComment = new Comment("This is a test comment", testUser2, testPost); + testComment = commentRepository.save(testComment); + testCommentId = testComment.getId(); + entityManager.flush(); + entityManager.clear(); + + testUser = userRepository.findById(testUser.getId()).orElse(null); + testUser2 = userRepository.findById(testUser2.getId()).orElse(null); + } + + private void authenticateUser(User user) { + UserDetailsImpl userDetails = UserDetailsImpl.build(user); + UsernamePasswordAuthenticationToken authentication = + new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities()); + SecurityContextHolder.getContext().setAuthentication(authentication); + } + + @Test + public void heuristics_testClassSetup() { + ServletContext servletContext = webApplicationContext.getServletContext(); + + assertNotNull(servletContext); + assertTrue(servletContext instanceof MockServletContext); + assertNotNull(webApplicationContext.getBean("postController")); + } + + @Test + public void tryGetAllPosts_testPostContentAndAuthor_withSinglePostInDb() throws Exception { + MvcResult result = this.mockMvc.perform(get("/posts") + .accept(MediaType.APPLICATION_JSON)) + .andDo(print()) + .andExpect(status().isOk()) + .andReturn(); + + JSONObject response = new JSONObject(result.getResponse().getContentAsString()).getJSONObject("data"); + JSONArray posts = response.getJSONArray("posts"); + assertNotNull(posts); + assertEquals(1, posts.length()); + + JSONObject firstPost = posts.getJSONObject(0); + assertEquals("This is a test post", firstPost.getString("content")); + assertEquals("John", firstPost.getJSONObject("user").getJSONObject("profile").getString("firstName")); + assertEquals("Doe", firstPost.getJSONObject("user").getJSONObject("profile").getString("lastName")); + } + + @Test + public void tryCreatePost_testPostCreation_withAuthenticatedUser() throws Exception { + authenticateUser(testUser); + + String requestBody = """ + { + "content": "This is a new test post" + } + """; + + MvcResult result = this.mockMvc.perform(post("/posts") + .contentType(MediaType.APPLICATION_JSON) + .content(requestBody)) + .andDo(print()) + .andExpect(status().isCreated()) + .andReturn(); + + JSONObject response = new JSONObject(result.getResponse().getContentAsString()).getJSONObject("data").getJSONObject("post"); + assertNotNull(response); + + assertEquals("This is a new test post", response.getString("content")); + assertEquals("John", response.getJSONObject("user").getJSONObject("profile").getString("firstName")); + assertEquals(0, response.getInt("likes")); + } + + @Test + public void tryCreatePost_testUnauthorized_withoutAuthentication() throws Exception { + String requestBody = """ + { + "content": "This should fail" + } + """; + + this.mockMvc.perform(post("/posts") + .contentType(MediaType.APPLICATION_JSON) + .content(requestBody)) + .andDo(print()) + .andExpect(status().isUnauthorized()) + .andReturn(); + } + + @Test + public void tryGetPostById_testPostDetails_withValidId() throws Exception { + MvcResult result = this.mockMvc.perform(get("/posts/" + testPostId) + .accept(MediaType.APPLICATION_JSON)) + .andDo(print()) + .andExpect(status().isOk()) + .andReturn(); + + JSONObject response = new JSONObject(result.getResponse().getContentAsString()).getJSONObject("data").getJSONObject("post"); + assertNotNull(response); + + assertEquals("This is a test post", response.getString("content")); + assertEquals("John", response.getJSONObject("user").getJSONObject("profile").getString("firstName")); + assertEquals(testPostId, response.getInt("id")); + } + + @Test + public void tryGetPostById_testNotFound_withInvalidId() throws Exception { + this.mockMvc.perform(get("/posts/999") + .accept(MediaType.APPLICATION_JSON)) + .andDo(print()) + .andExpect(status().isNotFound()) + .andReturn(); + } + + @Test + public void tryDeletePostById_testPostDeletion_withValidId() throws Exception { + MvcResult result = this.mockMvc.perform(delete("/posts/" + testPostId)) + .andDo(print()) + .andExpect(status().isOk()) + .andReturn(); + + JSONObject response = new JSONObject(result.getResponse().getContentAsString()).getJSONObject("data").getJSONObject("post"); + assertNotNull(response); + assertEquals("This is a test post", response.getString("content")); + + Post deletedPost = postRepository.findById(testPostId).orElse(null); + assertNull(deletedPost); + } + + @Test + public void tryAddCommentToPost_testCommentCreation_withValidData() throws Exception { + String requestBody = """ + { + "body": "This is a new comment", + "userId": %d + } + """.formatted(testUser2.getId()); + + MvcResult result = this.mockMvc.perform(post("/posts/" + testPostId + "/comments") + .contentType(MediaType.APPLICATION_JSON) + .content(requestBody)) + .andDo(print()) + .andExpect(status().isCreated()) + .andReturn(); + + JSONObject response = new JSONObject(result.getResponse().getContentAsString()).getJSONObject("data").getJSONObject("comment"); + assertNotNull(response); + + assertEquals("This is a new comment", response.getString("body")); + assertEquals(testUser2.getId(), response.getJSONObject("user").getInt("id")); + } + + @Test + public void tryGetCommentsForPost_testCommentsRetrieval_withValidPostId() throws Exception { + MvcResult result = this.mockMvc.perform(get("/posts/" + testPostId + "/comments") + .accept(MediaType.APPLICATION_JSON)) + .andDo(print()) + .andExpect(status().isOk()) + .andReturn(); + + JSONObject response = new JSONObject(result.getResponse().getContentAsString()).getJSONObject("data").getJSONObject("post"); + assertNotNull(response); + + JSONArray comments = response.getJSONArray("comments"); + assertEquals(1, comments.length()); + assertEquals("This is a test comment", comments.getJSONObject(0).getString("body")); + } + + @Test + public void tryGetCommentById_testCommentRetrieval_withValidIds() throws Exception { + MvcResult result = this.mockMvc.perform(get("/posts/" + testPostId + "/comments/" + testCommentId) + .accept(MediaType.APPLICATION_JSON)) + .andDo(print()) + .andExpect(status().isOk()) + .andReturn(); + + JSONObject response = new JSONObject(result.getResponse().getContentAsString()).getJSONObject("data").getJSONObject("comment"); + assertNotNull(response); + + assertEquals("This is a test comment", response.getString("body")); + assertEquals(testCommentId, response.getInt("id")); + } + + @Test + public void tryUpdateComment_testCommentUpdate_withOwnerAuthentication() throws Exception { + authenticateUser(testUser2); + + String requestBody = """ + { + "body": "This is an updated comment" + } + """; + + MvcResult result = this.mockMvc.perform(put("/posts/" + testPostId + "/comments/" + testCommentId) + .contentType(MediaType.APPLICATION_JSON) + .content(requestBody)) + .andDo(print()) + .andExpect(status().isOk()) + .andReturn(); + + JSONObject response = new JSONObject(result.getResponse().getContentAsString()).getJSONObject("data").getJSONObject("comment"); + assertNotNull(response); + + assertEquals("This is an updated comment", response.getString("body")); + assertEquals(testCommentId, response.getInt("id")); + } + + @Test + public void tryUpdateComment_testForbidden_withNonOwnerAuthentication() throws Exception { + authenticateUser(testUser); + + String requestBody = """ + { + "body": "This should fail" + } + """; + + this.mockMvc.perform(put("/posts/" + testPostId + "/comments/" + testCommentId) + .contentType(MediaType.APPLICATION_JSON) + .content(requestBody)) + .andDo(print()) + .andExpect(status().isForbidden()) + .andReturn(); + } + + @Test + public void tryDeleteComment_testCommentDeletion_withOwnerAuthentication() throws Exception { + authenticateUser(testUser2); + + this.mockMvc.perform(delete("/posts/" + testPostId + "/comments/" + testCommentId)) + .andDo(print()) + .andExpect(status().isOk()) + .andReturn(); + + Comment deletedComment = commentRepository.findById(testCommentId).orElse(null); + assertNull(deletedComment); + } + + @Test + public void tryLikePost_testPostLiking_withAuthenticatedUser() throws Exception { + authenticateUser(testUser); + + MvcResult result = this.mockMvc.perform(post("/posts/" + testPostId + "/like")) + .andDo(print()) + .andExpect(status().isOk()) + .andReturn(); + + JSONObject response = new JSONObject(result.getResponse().getContentAsString()).getJSONObject("data").getJSONObject("post"); + assertNotNull(response); + + assertEquals(1, response.getInt("likes")); + assertEquals(testPostId, response.getInt("id")); + } + + @Test + public void tryUnlikePost_testPostUnliking_withAuthenticatedUser() throws Exception { + Post post = postRepository.findById(testPostId).orElse(null); + post.setLikes(1); + postRepository.save(post); + entityManager.flush(); + + authenticateUser(testUser); + + MvcResult result = this.mockMvc.perform(delete("/posts/" + testPostId + "/like")) + .andDo(print()) + .andExpect(status().isOk()) + .andReturn(); + + JSONObject response = new JSONObject(result.getResponse().getContentAsString()).getJSONObject("data").getJSONObject("post"); + assertNotNull(response); + + assertEquals(0, response.getInt("likes")); + assertEquals(testPostId, response.getInt("id")); + } + + @Test + public void tryUpdatePost_testPostUpdate_withOwnerAuthentication() throws Exception { + authenticateUser(testUser); + + String requestBody = """ + { + "content": "This is an updated post content" + } + """; + + MvcResult result = this.mockMvc.perform(put("/posts/" + testPostId) + .contentType(MediaType.APPLICATION_JSON) + .content(requestBody)) + .andDo(print()) + .andExpect(status().isOk()) + .andReturn(); + + JSONObject response = new JSONObject(result.getResponse().getContentAsString()).getJSONObject("data").getJSONObject("post"); + assertNotNull(response); + + assertEquals("This is an updated post content", response.getString("content")); + assertEquals(testPostId, response.getInt("id")); + assertNotNull(response.getString("timeUpdated")); + } + + @Test + public void tryUpdatePost_testForbidden_withNonOwnerAuthentication() throws Exception { + authenticateUser(testUser2); + + String requestBody = """ + { + "content": "This should fail" + } + """; + + this.mockMvc.perform(put("/posts/" + testPostId) + .contentType(MediaType.APPLICATION_JSON) + .content(requestBody)) + .andDo(print()) + .andExpect(status().isForbidden()) + .andReturn(); + } + + @Test + public void tryUpdatePost_testBadRequest_withEmptyContent() throws Exception { + authenticateUser(testUser); + + String requestBody = """ + { + "content": "" + } + """; + + this.mockMvc.perform(put("/posts/" + testPostId) + .contentType(MediaType.APPLICATION_JSON) + .content(requestBody)) + .andDo(print()) + .andExpect(status().isBadRequest()) + .andReturn(); + } +} \ No newline at end of file diff --git a/src/test/java/com/booleanuk/controllerTests/ProfileControllerTest.java b/src/test/java/com/booleanuk/controllerTests/ProfileControllerTest.java new file mode 100644 index 0000000..f37b339 --- /dev/null +++ b/src/test/java/com/booleanuk/controllerTests/ProfileControllerTest.java @@ -0,0 +1,201 @@ +package com.booleanuk.controllerTests; + +import com.booleanuk.cohorts.controllers.AuthController; +import com.booleanuk.cohorts.controllers.ProfileController; +import com.booleanuk.cohorts.controllers.SearchController; +import com.booleanuk.cohorts.models.Cohort; +import com.booleanuk.cohorts.models.ERole; +import com.booleanuk.cohorts.models.Role; +import com.booleanuk.cohorts.models.User; +import com.booleanuk.cohorts.payload.request.SignupRequest; +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 jakarta.persistence.EntityManager; +import jakarta.persistence.PersistenceContext; +import jakarta.servlet.ServletContext; +import org.json.JSONArray; +import org.json.JSONObject; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.mock.web.MockServletContext; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.context.web.WebAppConfiguration; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.MvcResult; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.context.WebApplicationContext; + +import java.util.List; + +import static org.junit.jupiter.api.Assertions.*; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@WebAppConfiguration +@SpringBootTest +@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE) +@Transactional +@DirtiesContext(classMode = DirtiesContext.ClassMode.BEFORE_EACH_TEST_METHOD) +public class ProfileControllerTest { + @Autowired + private WebApplicationContext webApplicationContext; + + @Autowired + private UserRepository userRepository; + + @Autowired + AuthController authController; + + @Autowired + ProfileController profileController; + + @Autowired + ProfileRepository profileRepository; + + @Autowired + RoleRepository roleRepository; + + @Autowired + CohortRepository cohortRepository; + + @PersistenceContext + private EntityManager entityManager; + + private MockMvc mockMvc; + + private int actualUserId; + private int testCohortId; + + @BeforeEach + public void setup() throws Exception { + this.mockMvc = MockMvcBuilders.webAppContextSetup(this.webApplicationContext).build(); + userRepository.deleteAll(); + roleRepository.deleteAll(); + cohortRepository.deleteAll(); + entityManager.flush(); + entityManager.clear(); + + + Role teacherRole = new Role(ERole.ROLE_TEACHER); + Role studentRole = new Role(ERole.ROLE_STUDENT); + roleRepository.save(teacherRole); + roleRepository.save(studentRole); + entityManager.flush(); + + + Cohort testCohort = new Cohort(); + testCohort = cohortRepository.save(testCohort); + testCohortId = testCohort.getId(); + entityManager.flush(); + + SignupRequest signupRequest = new SignupRequest("thomas@ladder.com", "@Qwerty12345"); + ResponseEntity registerResponse = this.authController.registerUser(signupRequest); + entityManager.flush(); + entityManager.clear(); + + + actualUserId = userRepository.findAll().get(0).getId(); + + } + + @Test + public void heuristics_testClassSetup() { + ServletContext servletContext = webApplicationContext.getServletContext(); + + assertNotNull(servletContext); + assertTrue(servletContext instanceof MockServletContext); + assertNotNull(webApplicationContext.getBean("searchController")); + } + + @Test + public void tryCreateProfile_testFirstNameOnCreatedProfile() throws Exception { + String profileJson = """ + { + "userId": %d, + "first_name": "Thomas", + "last_name": "Ladder", + "username": "gottaStepUp", + "mobile": "244783772", + "github_username": "tallerThanU", + "bio": "GI need a ladder, but can't afford one. So, steps will have to be taken", + "role": "ROLE_STUDENT", + "specialism": "Big moves", + "cohort": 1, + "start_date": "1999-01-01", + "end_date": "2039-01-01", + "photo": "https://media.makeameme.org/created/ladder-i.jpg" + } + """.formatted(actualUserId); + + MvcResult result = this.mockMvc.perform(post("/profiles") + .contentType(MediaType.APPLICATION_JSON) + .content(profileJson)) + .andDo(print()) + .andExpect(status().isOk()) + .andReturn(); + + assertNotNull(result); + + String json = result.getResponse().getContentAsString(); + JSONObject jsonObject = new JSONObject(json); + + JSONObject profile = jsonObject.getJSONObject("profile"); + String firstName = profile.getString("firstName"); + String username = profile.getString("username"); + + System.out.println("First Name: " + firstName); + assertEquals("Thomas", firstName, "Profile first name should be Thomas"); + assertEquals("gottaStepUp", username, "Profile username should be gottaStepUp"); + } + + @Test + public void tryGetProfileForId_testFirstNameOnFoundProfile_withProfilesInDB() throws Exception { + ResponseEntity profileResponse = profileController.createProfile(new ProfileController.PostProfile( + actualUserId, // Use the actual user ID + "Thomas", + "Ladder", + "gottaStepUp", + "244783772", + "tallerThanU", + "I need a ladder, but can't afford one. So, steps will have to be taken", + "ROLE_STUDENT", + "Big moves", + 1, + "1999-01-01", + "2039-01-01", + "https://media.makeameme.org/created/ladder-i.jpg" + )); + + entityManager.flush(); + entityManager.clear(); + + MvcResult result = this.mockMvc.perform(get("/profiles/"+ actualUserId)) + .andDo(print()) + .andExpect(status().isOk()) + .andDo(print()) + .andReturn(); + + assertNotNull(result); + + String json = result.getResponse().getContentAsString(); + JSONObject jsonObject = new JSONObject(json); + + JSONObject profile = jsonObject.getJSONObject("data").getJSONObject("profile"); + String firstName = profile.getString("firstName"); + String username = profile.getString("username"); + System.out.println("First Name: " + firstName); + assertEquals("Thomas", firstName, "Profile first name should be Thomas"); + assertEquals("gottaStepUp", username, "Profile first name should be Thomas"); + } + +} diff --git a/src/test/java/com/booleanuk/controllerTests/SearchControllerTest.java b/src/test/java/com/booleanuk/controllerTests/SearchControllerTest.java new file mode 100644 index 0000000..067f667 --- /dev/null +++ b/src/test/java/com/booleanuk/controllerTests/SearchControllerTest.java @@ -0,0 +1,202 @@ +package com.booleanuk.controllerTests; + +import com.booleanuk.cohorts.controllers.AuthController; +import com.booleanuk.cohorts.controllers.ProfileController; +import com.booleanuk.cohorts.controllers.SearchController; +import com.booleanuk.cohorts.models.Cohort; +import com.booleanuk.cohorts.models.ERole; +import com.booleanuk.cohorts.models.Role; +import com.booleanuk.cohorts.models.User; +import com.booleanuk.cohorts.payload.request.SignupRequest; +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 jakarta.persistence.EntityManager; +import jakarta.persistence.PersistenceContext; +import jakarta.servlet.ServletContext; +import org.json.JSONArray; +import org.json.JSONObject; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.mock.web.MockServletContext; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.context.bean.override.mockito.MockitoBean; +import org.springframework.test.context.web.WebAppConfiguration; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.MvcResult; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.context.WebApplicationContext; + +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import static org.junit.jupiter.api.Assertions.*; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; + +@WebAppConfiguration +@SpringBootTest +@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE) +@Transactional +@DirtiesContext(classMode = DirtiesContext.ClassMode.BEFORE_EACH_TEST_METHOD) +class SearchControllerTest { + + @Autowired + private WebApplicationContext webApplicationContext; + + @Autowired + private UserRepository userRepository; + + @Autowired + private RoleRepository roleRepository; + + @Autowired + private CohortRepository cohortRepository; + + @Autowired + AuthController authController; + + @Autowired + ProfileController profileController; + + @Autowired + ProfileRepository profileRepository; + + @PersistenceContext + private EntityManager entityManager; + + private MockMvc mockMvc; + + private int actualUserId; + private int testCohortId; + + private User testUser; + + @BeforeEach + public void setup() throws Exception { + this.mockMvc = MockMvcBuilders.webAppContextSetup(this.webApplicationContext).build(); + + profileRepository.deleteAll(); + userRepository.deleteAll(); + roleRepository.deleteAll(); + cohortRepository.deleteAll(); + entityManager.flush(); + entityManager.clear(); + + + Role teacherRole = new Role(ERole.ROLE_TEACHER); + Role studentRole = new Role(ERole.ROLE_STUDENT); + roleRepository.save(teacherRole); + roleRepository.save(studentRole); + entityManager.flush(); + + + Cohort testCohort = new Cohort(); + testCohort = cohortRepository.save(testCohort); + testCohortId = testCohort.getId(); + entityManager.flush(); + + SignupRequest signupRequest = new SignupRequest("thomas@ladder.com", "@Qwerty12345"); + ResponseEntity registerResponse = this.authController.registerUser(signupRequest); + entityManager.flush(); + entityManager.clear(); + + List users = userRepository.findAll(); + User createdUser = users.get(0); + int actualUserId = createdUser.getId(); + + System.out.println("Using user ID for profile creation: " + actualUserId); + + + ResponseEntity profileResponse = profileController.createProfile(new ProfileController.PostProfile( + actualUserId, // Use the actual user ID + "Thomas", + "Ladder", + "gottaStepUp", + "244783772", + "tallerThanU", + "I need a ladder, but can't afford one. So, steps will have to be taken", + "ROLE_STUDENT", + "Big moves", + 1, + "1999-01-01", + "2039-01-01", + "https://media.makeameme.org/created/ladder-i.jpg" + )); + + entityManager.flush(); + entityManager.clear(); + + } + + @Test + public void heuristics_testClassSetup() { + ServletContext servletContext = webApplicationContext.getServletContext(); + + assertNotNull(servletContext); + assertTrue(servletContext instanceof MockServletContext); + assertNotNull(webApplicationContext.getBean("searchController")); + } + + @Test + public void trySearchProfilesDefault_withProfilesInDB() throws Exception { + MvcResult result = this.mockMvc.perform(get("/search/profiles")) + .andDo(print()) + .andExpect(status().isOk()) + .andReturn(); + + String json = result.getResponse().getContentAsString(); + + JSONObject jsonObject = new JSONObject(json); + JSONArray jsonArray = jsonObject.getJSONObject("data").getJSONArray("profiles"); + + assertTrue(jsonArray.length() == 1, "Should return at least one profile"); + } + + @Test + public void trySearchProfilesQuery_testFirstNameOnFirstFoundProfile_withProfilesInDB() throws Exception { + MvcResult result = this.mockMvc.perform(get("/search/profiles/thomas")) + .andDo(print()) + .andExpect(status().isOk()) + .andDo(print()) + .andReturn(); + + String json = result.getResponse().getContentAsString(); + + JSONObject jsonObject = new JSONObject(json); + JSONArray profileArray = jsonObject.getJSONObject("data").getJSONArray("profiles"); + assertTrue(profileArray.length() > 0, "Should return at least one profile"); + + JSONObject firstProfile = profileArray.getJSONObject(0); + String profileFirstName = firstProfile.getString("firstName"); + assertTrue(profileFirstName.toLowerCase().contains("thomas"), + "Profile first name should contain 'thomas'"); + } + + @Test + public void trySearchProfileQuery_testNoProfilesFound_withProfilesInDB() throws Exception { + MvcResult result = this.mockMvc.perform(get("/search/profiles/firstnamethatdoesnotexsist")) + .andDo(print()) + .andExpect(status().isOk()) + .andDo(print()) + .andReturn(); + + String json = result.getResponse().getContentAsString(); + JSONObject jsonObject = new JSONObject(json); + + JSONArray profileArray = jsonObject.getJSONObject("data").getJSONArray("profiles"); + assertTrue(profileArray.length() == 0, "Should return no profiles"); + } +} diff --git a/src/test/java/com/booleanuk/controllerTests/StudentControllerTest.java b/src/test/java/com/booleanuk/controllerTests/StudentControllerTest.java new file mode 100644 index 0000000..d746cd5 --- /dev/null +++ b/src/test/java/com/booleanuk/controllerTests/StudentControllerTest.java @@ -0,0 +1,4 @@ +package com.booleanuk.controllerTests; + +public class StudentControllerTest { +} diff --git a/src/test/java/com/booleanuk/controllerTests/TeacherControllerTest.java b/src/test/java/com/booleanuk/controllerTests/TeacherControllerTest.java new file mode 100644 index 0000000..9cd76f4 --- /dev/null +++ b/src/test/java/com/booleanuk/controllerTests/TeacherControllerTest.java @@ -0,0 +1,4 @@ +package com.booleanuk.controllerTests; + +public class TeacherControllerTest { +} diff --git a/src/test/java/com/booleanuk/controllerTests/UserControllerTest.java b/src/test/java/com/booleanuk/controllerTests/UserControllerTest.java new file mode 100644 index 0000000..c8eb3c5 --- /dev/null +++ b/src/test/java/com/booleanuk/controllerTests/UserControllerTest.java @@ -0,0 +1,297 @@ +package com.booleanuk.controllerTests; + +import com.booleanuk.cohorts.controllers.*; +import com.booleanuk.cohorts.models.Cohort; +import com.booleanuk.cohorts.models.ERole; +import com.booleanuk.cohorts.models.Role; +import com.booleanuk.cohorts.models.User; +import com.booleanuk.cohorts.payload.request.PostRequest; +import com.booleanuk.cohorts.payload.request.SignupRequest; +import com.booleanuk.cohorts.payload.response.PostResponse; +import com.booleanuk.cohorts.repository.*; +import com.booleanuk.cohorts.security.services.UserDetailsImpl; +import jakarta.persistence.EntityManager; +import jakarta.persistence.PersistenceContext; +import jakarta.servlet.ServletContext; +import org.json.JSONArray; +import org.json.JSONObject; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.mock.web.MockServletContext; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.context.web.WebAppConfiguration; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.MvcResult; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.context.WebApplicationContext; + +import java.util.ArrayList; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.*; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.user; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; +import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@WebAppConfiguration +@SpringBootTest +@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE) +@Transactional +@DirtiesContext(classMode = DirtiesContext.ClassMode.BEFORE_EACH_TEST_METHOD) +public class UserControllerTest { + + @Autowired + private WebApplicationContext webApplicationContext; + + @Autowired + private UserRepository userRepository; + + @Autowired + private RoleRepository roleRepository; + + @Autowired + private CohortRepository cohortRepository; + + @Autowired + AuthController authController; + + @Autowired + ProfileController profileController; + + @Autowired + ProfileRepository profileRepository; + + @Autowired + PostController postController; + + @Autowired + PostRepository postRepository; + + @PersistenceContext + private EntityManager entityManager; + + private MockMvc mockMvc; + + private int actualUserId; + private int testCohortId; + + private User testUser; + + @BeforeEach + public void setup() throws Exception { + this.mockMvc = MockMvcBuilders.webAppContextSetup(this.webApplicationContext).build(); + profileRepository.deleteAll(); + userRepository.deleteAll(); + roleRepository.deleteAll(); + cohortRepository.deleteAll(); + entityManager.flush(); + entityManager.clear(); + + + Role teacherRole = new Role(ERole.ROLE_TEACHER); + Role studentRole = new Role(ERole.ROLE_STUDENT); + roleRepository.save(teacherRole); + roleRepository.save(studentRole); + entityManager.flush(); + + + Cohort testCohort = new Cohort(); + testCohort = cohortRepository.save(testCohort); + testCohortId = testCohort.getId(); + entityManager.flush(); + + SignupRequest signupRequest = new SignupRequest("thomas@ladder.com", "@Qwerty12345"); + ResponseEntity registerResponse = this.authController.registerUser(signupRequest); + entityManager.flush(); + entityManager.clear(); + + testUser = userRepository.findAll().get(0); + actualUserId = testUser.getId(); + } + + private void authenticateUser(User user) { + UserDetailsImpl userDetails = UserDetailsImpl.build(user); + UsernamePasswordAuthenticationToken authentication = + new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities()); + SecurityContextHolder.getContext().setAuthentication(authentication); + } + + @Test + public void heuristics_testClassSetup() { + ServletContext servletContext = webApplicationContext.getServletContext(); + + assertNotNull(servletContext); + assertTrue(servletContext instanceof MockServletContext); + assertNotNull(webApplicationContext.getBean("userController")); + } + + @Test + public void tryGetAllUsers_testEmailOnFirstUser_withSingleUserInDb() throws Exception { + MvcResult result = this.mockMvc.perform(get("/users") + .accept(MediaType.APPLICATION_JSON)) + .andDo(print()) + .andExpect(status().isOk()) + .andReturn(); + + assertNotNull(result); + String json = result.getResponse().getContentAsString(); + JSONObject jsonObject = new JSONObject(json).getJSONObject("data"); + + JSONArray jsonArray = jsonObject.getJSONArray("users"); + String email = jsonArray.getJSONObject(0).getString("email"); + + assertTrue(jsonArray.length() == 1); + assertEquals("thomas@ladder.com", email, "Email should be thomas@ladder.com"); + } + + @Test + public void tryGetAllUsers_testEmailOnMultipleUsers_withMultipleUserInDb() throws Exception { + this.authController.registerUser(new SignupRequest("fredrik@ladder.com", "@Qwerty12345")); + this.authController.registerUser(new SignupRequest("sara@ladder.com", "@Qwerty12345")); + this.authController.registerUser(new SignupRequest("josefine@ladder.com", "@Qwerty12345")); + entityManager.flush(); + entityManager.clear(); + + MvcResult result = this.mockMvc.perform(get("/users") + .accept(MediaType.APPLICATION_JSON)) + .andDo(print()) + .andExpect(status().isOk()) + .andReturn(); + + assertNotNull(result); + + String json = result.getResponse().getContentAsString(); + JSONObject jsonObject = new JSONObject(json).getJSONObject("data"); + JSONArray jsonArray = jsonObject.getJSONArray("users"); + + assertTrue(jsonArray.length() == 4); + + String emailThomas = jsonArray.getJSONObject(0).getString("email"); + String emailFredrik = jsonArray.getJSONObject(1).getString("email"); + String emailSara = jsonArray.getJSONObject(2).getString("email"); + String emailJosefine = jsonArray.getJSONObject(3).getString("email"); + + assertEquals("thomas@ladder.com", emailThomas, "Email should be thomas@ladder.com"); + assertEquals("fredrik@ladder.com", emailFredrik, "Email should be fredrik@ladder.com"); + assertEquals("sara@ladder.com", emailSara, "Email should be sara@ladder.com"); + assertEquals("josefine@ladder.com", emailJosefine, "Email should be josefine@ladder.com"); + } + + @Test + public void tryGetUserById_testEmailOnFirstUser_withSingleUserInDb() throws Exception { + MvcResult result = this.mockMvc.perform(get("/users/" + actualUserId) + .accept(MediaType.APPLICATION_JSON)) + .andDo(print()) + .andExpect(status().isOk()) + .andReturn(); + + assertNotNull(result); + String json = result.getResponse().getContentAsString(); + JSONObject jsonObject = new JSONObject(json).getJSONObject("data"); + + JSONObject user = jsonObject.getJSONObject("user"); + String email = user.getString("email"); + assertEquals("thomas@ladder.com", email, "Email should be thomas@ladder.com"); + } + + @Test + public void tryDeleteUserById_testReturnCodeAndIfUserIsActuallyDeleted() throws Exception { + MvcResult result = this.mockMvc.perform(delete("/users/" + actualUserId) + .accept(MediaType.APPLICATION_JSON)) + .andDo(print()) + .andExpect(status().isOk()) + .andReturn(); + + assertTrue(userRepository.findById(actualUserId).isEmpty(), "User list should be empty"); + } + + @Test + public void tryUpdateLikedPosts_testAddingSingleLikedPost_checkUserObjectAndResponseBody_withSingleUserIndb() throws Exception { + authenticateUser(testUser); + + ResponseEntity postResponse = this.postController.createPost(new PostRequest("It's not DNS... There's no way it's DNS... It was DNS", actualUserId)); + + entityManager.flush(); + entityManager.clear(); + + int postId = this.postRepository.findAll().get(0).getId(); + + // Create the request body with the post_id + String requestBody = "{\"post_id\": " + postId + "}"; + + MvcResult result = this.mockMvc.perform(patch("/users/" + actualUserId + "/like") + .with(user(UserDetailsImpl.build(testUser))) + .contentType(MediaType.APPLICATION_JSON) + .content(requestBody) + .accept(MediaType.APPLICATION_JSON)) + .andDo(print()) + .andExpect(status().isOk()) + .andReturn(); + + String json = result.getResponse().getContentAsString(); + JSONObject jsonObject = new JSONObject(json).getJSONObject("data"); + JSONObject user = jsonObject.getJSONObject("user"); + JSONArray likedPosts = user.getJSONArray("likedPosts"); + + assertEquals(1, likedPosts.length(), "User should have 1 liked post in their likedPosts array"); + User userWithLike = userRepository.getReferenceById(actualUserId); + assertTrue(userWithLike.getLikedPosts().size() == 1, "User should have 2 liked posts in their likedPosts array"); + } + + @Test + public void tryUpdateLikedPosts_testAddingMultipleLikedPost_checkUserObjectAndResponseBody_withSingleUserIndb() throws Exception { + authenticateUser(testUser); + + ResponseEntity postResponse = this.postController.createPost(new PostRequest("It's not DNS... There's no way it's DNS... It was DNS", actualUserId)); + ResponseEntity postResponse2 = this.postController.createPost(new PostRequest("Sorry I forgot", actualUserId)); + entityManager.flush(); + entityManager.clear(); + + // Add first post + int postId = this.postRepository.findAll().get(0).getId(); + String requestBody = "{\"post_id\": " + postId + "}"; + + MvcResult result = this.mockMvc.perform(patch("/users/" + actualUserId + "/like") + .with(user(UserDetailsImpl.build(testUser))) + .contentType(MediaType.APPLICATION_JSON) + .content(requestBody) + .accept(MediaType.APPLICATION_JSON)) + .andDo(print()) + .andExpect(status().isOk()) + .andReturn(); + + // Add second post + postId = this.postRepository.findAll().get(1).getId(); + requestBody = "{\"post_id\": " + postId + "}"; + + result = this.mockMvc.perform(patch("/users/" + actualUserId + "/like") + .with(user(UserDetailsImpl.build(testUser))) + .contentType(MediaType.APPLICATION_JSON) + .content(requestBody) + .accept(MediaType.APPLICATION_JSON)) + .andDo(print()) + .andExpect(status().isOk()) + .andReturn(); + + // Actual testing logic + String json = result.getResponse().getContentAsString(); + JSONObject jsonObject = new JSONObject(json).getJSONObject("data"); + JSONObject user = jsonObject.getJSONObject("user"); + JSONArray likedPosts = user.getJSONArray("likedPosts"); + + assertEquals(2, likedPosts.length(), "User should have 2 liked posts in their likedPosts array"); + + User userWithLike = userRepository.getReferenceById(actualUserId); + assertTrue(userWithLike.getLikedPosts().size() == 2, "User should have 2 liked posts in their likedPosts array"); + } +}