From ac6588abc3b99fecc744eb14b741181ef853d7fc Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Thu, 7 Aug 2025 11:28:29 +0000 Subject: [PATCH] Refactor project structure and implement LMS backend with Spring Boot Co-authored-by: egobinath2001 --- README.md | 189 ++++++++++++++ pom.xml | 231 ++++++++---------- .../LearningManagementSystemApplication.java | 12 + .../com/lms/controller/AuthController.java | 137 +++++++++++ .../com/lms/controller/CourseController.java | 159 ++++++++++++ .../lms/controller/InstructorController.java | 157 ++++++++++++ .../com/lms/controller/StudentController.java | 149 +++++++++++ src/main/java/com/lms/entity/Admin.java | 55 +++++ src/main/java/com/lms/entity/Course.java | 163 ++++++++++++ src/main/java/com/lms/entity/Enrollment.java | 79 ++++++ src/main/java/com/lms/entity/Instructor.java | 70 ++++++ src/main/java/com/lms/entity/Lesson.java | 106 ++++++++ src/main/java/com/lms/entity/Role.java | 7 + src/main/java/com/lms/entity/Student.java | 76 ++++++ src/main/java/com/lms/entity/User.java | 113 +++++++++ .../com/lms/payload/request/LoginRequest.java | 27 ++ .../lms/payload/request/SignupRequest.java | 106 ++++++++ .../com/lms/payload/response/JwtResponse.java | 66 +++++ .../lms/payload/response/MessageResponse.java | 17 ++ .../com/lms/repository/AdminRepository.java | 30 +++ .../com/lms/repository/CourseRepository.java | 37 +++ .../lms/repository/EnrollmentRepository.java | 41 ++++ .../lms/repository/InstructorRepository.java | 31 +++ .../com/lms/repository/LessonRepository.java | 32 +++ .../com/lms/repository/StudentRepository.java | 29 +++ .../com/lms/repository/UserRepository.java | 24 ++ .../com/lms/security/WebSecurityConfig.java | 82 +++++++ .../lms/security/jwt/AuthEntryPointJwt.java | 40 +++ .../com/lms/security/jwt/AuthTokenFilter.java | 63 +++++ .../java/com/lms/security/jwt/JwtUtils.java | 63 +++++ .../security/services/UserDetailsImpl.java | 97 ++++++++ .../services/UserDetailsServiceImpl.java | 26 ++ .../java/com/lms/service/CourseService.java | 43 ++++ .../com/lms/service/EnrollmentService.java | 41 ++++ .../com/lms/service/InstructorService.java | 39 +++ .../java/com/lms/service/StudentService.java | 37 +++ .../java/com/lms/service/UserService.java | 32 +++ .../lms/service/impl/CourseServiceImpl.java | 132 ++++++++++ .../service/impl/EnrollmentServiceImpl.java | 133 ++++++++++ .../service/impl/InstructorServiceImpl.java | 142 +++++++++++ .../lms/service/impl/StudentServiceImpl.java | 172 +++++++++++++ .../com/lms/service/impl/UserServiceImpl.java | 102 ++++++++ src/main/resources/application.properties | 38 +-- 43 files changed, 3272 insertions(+), 153 deletions(-) create mode 100644 README.md create mode 100644 src/main/java/com/lms/LearningManagementSystemApplication.java create mode 100644 src/main/java/com/lms/controller/AuthController.java create mode 100644 src/main/java/com/lms/controller/CourseController.java create mode 100644 src/main/java/com/lms/controller/InstructorController.java create mode 100644 src/main/java/com/lms/controller/StudentController.java create mode 100644 src/main/java/com/lms/entity/Admin.java create mode 100644 src/main/java/com/lms/entity/Course.java create mode 100644 src/main/java/com/lms/entity/Enrollment.java create mode 100644 src/main/java/com/lms/entity/Instructor.java create mode 100644 src/main/java/com/lms/entity/Lesson.java create mode 100644 src/main/java/com/lms/entity/Role.java create mode 100644 src/main/java/com/lms/entity/Student.java create mode 100644 src/main/java/com/lms/entity/User.java create mode 100644 src/main/java/com/lms/payload/request/LoginRequest.java create mode 100644 src/main/java/com/lms/payload/request/SignupRequest.java create mode 100644 src/main/java/com/lms/payload/response/JwtResponse.java create mode 100644 src/main/java/com/lms/payload/response/MessageResponse.java create mode 100644 src/main/java/com/lms/repository/AdminRepository.java create mode 100644 src/main/java/com/lms/repository/CourseRepository.java create mode 100644 src/main/java/com/lms/repository/EnrollmentRepository.java create mode 100644 src/main/java/com/lms/repository/InstructorRepository.java create mode 100644 src/main/java/com/lms/repository/LessonRepository.java create mode 100644 src/main/java/com/lms/repository/StudentRepository.java create mode 100644 src/main/java/com/lms/repository/UserRepository.java create mode 100644 src/main/java/com/lms/security/WebSecurityConfig.java create mode 100644 src/main/java/com/lms/security/jwt/AuthEntryPointJwt.java create mode 100644 src/main/java/com/lms/security/jwt/AuthTokenFilter.java create mode 100644 src/main/java/com/lms/security/jwt/JwtUtils.java create mode 100644 src/main/java/com/lms/security/services/UserDetailsImpl.java create mode 100644 src/main/java/com/lms/security/services/UserDetailsServiceImpl.java create mode 100644 src/main/java/com/lms/service/CourseService.java create mode 100644 src/main/java/com/lms/service/EnrollmentService.java create mode 100644 src/main/java/com/lms/service/InstructorService.java create mode 100644 src/main/java/com/lms/service/StudentService.java create mode 100644 src/main/java/com/lms/service/UserService.java create mode 100644 src/main/java/com/lms/service/impl/CourseServiceImpl.java create mode 100644 src/main/java/com/lms/service/impl/EnrollmentServiceImpl.java create mode 100644 src/main/java/com/lms/service/impl/InstructorServiceImpl.java create mode 100644 src/main/java/com/lms/service/impl/StudentServiceImpl.java create mode 100644 src/main/java/com/lms/service/impl/UserServiceImpl.java diff --git a/README.md b/README.md new file mode 100644 index 0000000..bda5cb7 --- /dev/null +++ b/README.md @@ -0,0 +1,189 @@ +# Learning Management System (LMS) Backend + +A comprehensive Learning Management System backend built with Spring Boot, featuring role-based authentication and comprehensive course management capabilities. + +## Features + +### Core Entities +- **Students**: User accounts for learners with enrollment tracking +- **Instructors**: User accounts for educators with course management +- **Admins**: Administrative users with system-wide permissions +- **Courses**: Course management with enrollment limits and status tracking +- **Enrollments**: Student-course relationships with grades and progress +- **Lessons**: Course content with multimedia support + +### Key Functionalities +- JWT-based authentication and authorization +- Role-based access control (ADMIN, INSTRUCTOR, STUDENT) +- Course enrollment and management +- User profile management +- Grade tracking and course completion +- RESTful API endpoints +- H2 in-memory database for development + +## Technology Stack + +- **Framework**: Spring Boot 3.2.0 +- **Security**: Spring Security with JWT +- **Database**: H2 (development), MySQL (production ready) +- **ORM**: Spring Data JPA with Hibernate +- **Java Version**: 17 +- **Build Tool**: Maven + +## API Endpoints + +### Authentication +- `POST /api/auth/signin` - User login +- `POST /api/auth/signup/student` - Student registration +- `POST /api/auth/signup/instructor` - Instructor registration +- `POST /api/auth/signup/admin` - Admin registration + +### Students +- `GET /api/students` - Get all students (Admin/Instructor) +- `GET /api/students/{id}` - Get student by ID +- `POST /api/students/{studentId}/enroll/{courseId}` - Enroll in course +- `GET /api/students/{studentId}/courses` - Get student's courses + +### Courses +- `GET /api/courses` - Get all courses (Public) +- `GET /api/courses/{id}` - Get course by ID (Public) +- `POST /api/courses` - Create course (Admin/Instructor) +- `GET /api/courses/available` - Get available courses +- `GET /api/courses/search?searchTerm=` - Search courses + +### Instructors +- `GET /api/instructors` - Get all instructors (Admin) +- `GET /api/instructors/{id}` - Get instructor by ID +- `POST /api/instructors/{instructorId}/assign-course/{courseId}` - Assign course +- `GET /api/instructors/{instructorId}/courses` - Get instructor's courses + +## Getting Started + +### Prerequisites +- Java 17 or higher +- Maven 3.6 or higher + +### Installation + +1. Clone the repository: +```bash +git clone +cd learning-management-system +``` + +2. Build the project: +```bash +mvn clean install +``` + +3. Run the application: +```bash +mvn spring-boot:run +``` + +The application will start on `http://localhost:8080` + +### Database Access +- H2 Console: `http://localhost:8080/h2-console` +- JDBC URL: `jdbc:h2:mem:lmsdb` +- Username: `sa` +- Password: `password` + +## Authentication + +The system uses JWT (JSON Web Tokens) for authentication. To access protected endpoints: + +1. Register or login to get a JWT token +2. Include the token in the Authorization header: `Bearer ` + +### Sample Authentication Flow + +1. **Register a student:** +```json +POST /api/auth/signup/student +{ + "username": "john_doe", + "email": "john@example.com", + "password": "password123", + "firstName": "John", + "lastName": "Doe", + "studentId": "STU001" +} +``` + +2. **Login:** +```json +POST /api/auth/signin +{ + "username": "john_doe", + "password": "password123" +} +``` + +3. **Response:** +```json +{ + "accessToken": "eyJhbGciOiJIUzUxMiJ9...", + "tokenType": "Bearer", + "id": 1, + "username": "john_doe", + "email": "john@example.com", + "authorities": ["ROLE_STUDENT"] +} +``` + +## Database Schema + +The system uses a single table inheritance strategy for users with the following main entities: + +- **users** - Base table for all user types (students, instructors, admins) +- **courses** - Course information and metadata +- **enrollments** - Student-course relationships with grades +- **lessons** - Course content and materials +- **student_courses** - Many-to-many relationship table + +## Security + +- Passwords are encrypted using BCrypt +- JWT tokens expire after 24 hours +- Role-based access control with method-level security +- CORS enabled for cross-origin requests + +## Configuration + +Key configuration properties in `application.properties`: + +```properties +# Database +spring.datasource.url=jdbc:h2:mem:lmsdb +spring.h2.console.enabled=true + +# JWT +jwt.secret= +jwt.expiration=86400000 + +# Server +server.port=8080 +``` + +## Production Deployment + +For production deployment: + +1. Replace H2 with MySQL/PostgreSQL +2. Update `application.properties` with production database settings +3. Use environment variables for sensitive configuration +4. Enable HTTPS +5. Configure proper CORS origins + +## Contributing + +1. Fork the repository +2. Create a feature branch +3. Commit your changes +4. Push to the branch +5. Create a Pull Request + +## License + +This project is licensed under the MIT License. \ No newline at end of file diff --git a/pom.xml b/pom.xml index 2dbde16..861a8ae 100644 --- a/pom.xml +++ b/pom.xml @@ -1,137 +1,100 @@ - 4.0.0 - - org.springframework.boot - spring-boot-starter-parent - 3.4.4 - - - com.online - OnlineLearningSystem - 0.0.1-SNAPSHOT - OnlineLearningSystem - project for Spring Boot - - - - - - - - - - - - - - - 24 - - - - org.springframework.boot - spring-boot-starter-web - - - - com.h2database - h2 - runtime - - - - - org.springframework.boot - spring-boot-starter-data-mongodb - - - - - org.springframework.boot - spring-boot-devtools - runtime - true - - - - org.springframework.boot - spring-boot-starter-security - - - - - org.springframework.security - spring-security-config - 6.2.1 - - - - - - io.jsonwebtoken - jjwt-api - 0.12.6 - - - - io.jsonwebtoken - jjwt-impl - 0.12.6 - runtime - - - - io.jsonwebtoken - jjwt-jackson - 0.12.6 - runtime - - - - org.projectlombok - lombok - true - - - org.springframework.boot - spring-boot-starter-test - test - - - org.springframework.boot - spring-boot-starter-data-jpa - - - - - - - org.apache.maven.plugins - maven-compiler-plugin - - - - org.projectlombok - lombok - - - - - - org.springframework.boot - spring-boot-maven-plugin - - - - org.projectlombok - lombok - - - - - - - + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> + 4.0.0 + + org.springframework.boot + spring-boot-starter-parent + 3.2.0 + + + + com.lms + learning-management-system + 0.0.1-SNAPSHOT + learning-management-system + Learning Management System Backend with Spring Boot + + + 17 + + + + + + org.springframework.boot + spring-boot-starter-web + + + + org.springframework.boot + spring-boot-starter-data-jpa + + + + org.springframework.boot + spring-boot-starter-security + + + + org.springframework.boot + spring-boot-starter-validation + + + + + com.h2database + h2 + runtime + + + + com.mysql + mysql-connector-j + runtime + + + + + io.jsonwebtoken + jjwt-api + 0.11.5 + + + + io.jsonwebtoken + jjwt-impl + 0.11.5 + runtime + + + + io.jsonwebtoken + jjwt-jackson + 0.11.5 + runtime + + + + + org.springframework.boot + spring-boot-starter-test + test + + + + org.springframework.security + spring-security-test + test + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + diff --git a/src/main/java/com/lms/LearningManagementSystemApplication.java b/src/main/java/com/lms/LearningManagementSystemApplication.java new file mode 100644 index 0000000..4c56ed5 --- /dev/null +++ b/src/main/java/com/lms/LearningManagementSystemApplication.java @@ -0,0 +1,12 @@ +package com.lms; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class LearningManagementSystemApplication { + + public static void main(String[] args) { + SpringApplication.run(LearningManagementSystemApplication.class, args); + } +} \ No newline at end of file diff --git a/src/main/java/com/lms/controller/AuthController.java b/src/main/java/com/lms/controller/AuthController.java new file mode 100644 index 0000000..21f1b1a --- /dev/null +++ b/src/main/java/com/lms/controller/AuthController.java @@ -0,0 +1,137 @@ +package com.lms.controller; + +import com.lms.entity.Role; +import com.lms.entity.Student; +import com.lms.entity.Instructor; +import com.lms.entity.Admin; +import com.lms.payload.request.LoginRequest; +import com.lms.payload.request.SignupRequest; +import com.lms.payload.response.JwtResponse; +import com.lms.payload.response.MessageResponse; +import com.lms.security.jwt.JwtUtils; +import com.lms.security.services.UserDetailsImpl; +import com.lms.service.UserService; +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.web.bind.annotation.*; + +import jakarta.validation.Valid; + +@CrossOrigin(origins = "*", maxAge = 3600) +@RestController +@RequestMapping("/api/auth") +public class AuthController { + + @Autowired + AuthenticationManager authenticationManager; + + @Autowired + private UserService userService; + + @Autowired + JwtUtils jwtUtils; + + @PostMapping("/signin") + public ResponseEntity authenticateUser(@Valid @RequestBody LoginRequest loginRequest) { + + Authentication authentication = authenticationManager.authenticate( + new UsernamePasswordAuthenticationToken(loginRequest.getUsername(), loginRequest.getPassword())); + + SecurityContextHolder.getContext().setAuthentication(authentication); + String jwt = jwtUtils.generateJwtToken(authentication); + + UserDetailsImpl userDetails = (UserDetailsImpl) authentication.getPrincipal(); + + return ResponseEntity.ok(new JwtResponse(jwt, + userDetails.getId(), + userDetails.getUsername(), + userDetails.getEmail(), + userDetails.getAuthorities())); + } + + @PostMapping("/signup/student") + public ResponseEntity registerStudent(@Valid @RequestBody SignupRequest signUpRequest) { + if (userService.existsByUsername(signUpRequest.getUsername())) { + return ResponseEntity + .badRequest() + .body(new MessageResponse("Error: Username is already taken!")); + } + + if (userService.existsByEmail(signUpRequest.getEmail())) { + return ResponseEntity + .badRequest() + .body(new MessageResponse("Error: Email is already in use!")); + } + + // Create new student account + Student student = new Student(signUpRequest.getUsername(), + signUpRequest.getEmail(), + signUpRequest.getPassword(), + signUpRequest.getFirstName(), + signUpRequest.getLastName(), + signUpRequest.getStudentId()); + + userService.saveUser(student); + + return ResponseEntity.ok(new MessageResponse("Student registered successfully!")); + } + + @PostMapping("/signup/instructor") + public ResponseEntity registerInstructor(@Valid @RequestBody SignupRequest signUpRequest) { + if (userService.existsByUsername(signUpRequest.getUsername())) { + return ResponseEntity + .badRequest() + .body(new MessageResponse("Error: Username is already taken!")); + } + + if (userService.existsByEmail(signUpRequest.getEmail())) { + return ResponseEntity + .badRequest() + .body(new MessageResponse("Error: Email is already in use!")); + } + + // Create new instructor account + Instructor instructor = new Instructor(signUpRequest.getUsername(), + signUpRequest.getEmail(), + signUpRequest.getPassword(), + signUpRequest.getFirstName(), + signUpRequest.getLastName(), + signUpRequest.getEmployeeId(), + signUpRequest.getDepartment()); + + userService.saveUser(instructor); + + return ResponseEntity.ok(new MessageResponse("Instructor registered successfully!")); + } + + @PostMapping("/signup/admin") + public ResponseEntity registerAdmin(@Valid @RequestBody SignupRequest signUpRequest) { + if (userService.existsByUsername(signUpRequest.getUsername())) { + return ResponseEntity + .badRequest() + .body(new MessageResponse("Error: Username is already taken!")); + } + + if (userService.existsByEmail(signUpRequest.getEmail())) { + return ResponseEntity + .badRequest() + .body(new MessageResponse("Error: Email is already in use!")); + } + + // Create new admin account + Admin admin = new Admin(signUpRequest.getUsername(), + signUpRequest.getEmail(), + signUpRequest.getPassword(), + signUpRequest.getFirstName(), + signUpRequest.getLastName(), + signUpRequest.getAdminId()); + + userService.saveUser(admin); + + return ResponseEntity.ok(new MessageResponse("Admin registered successfully!")); + } +} \ No newline at end of file diff --git a/src/main/java/com/lms/controller/CourseController.java b/src/main/java/com/lms/controller/CourseController.java new file mode 100644 index 0000000..e174838 --- /dev/null +++ b/src/main/java/com/lms/controller/CourseController.java @@ -0,0 +1,159 @@ +package com.lms.controller; + +import com.lms.entity.Course; +import com.lms.entity.Student; +import com.lms.service.CourseService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.*; + +import jakarta.validation.Valid; +import java.util.List; +import java.util.Optional; + +@RestController +@RequestMapping("/api/courses") +@CrossOrigin(origins = "*", maxAge = 3600) +public class CourseController { + + @Autowired + private CourseService courseService; + + @GetMapping + public ResponseEntity> getAllCourses() { + List courses = courseService.findAllCourses(); + return ResponseEntity.ok(courses); + } + + @GetMapping("/{id}") + public ResponseEntity getCourseById(@PathVariable Long id) { + Optional course = courseService.findById(id); + return course.map(ResponseEntity::ok) + .orElse(ResponseEntity.notFound().build()); + } + + @GetMapping("/code/{courseCode}") + public ResponseEntity getCourseByCourseCode(@PathVariable String courseCode) { + Optional course = courseService.findByCourseCode(courseCode); + return course.map(ResponseEntity::ok) + .orElse(ResponseEntity.notFound().build()); + } + + @GetMapping("/instructor/{instructorId}") + @PreAuthorize("hasRole('ADMIN') or hasRole('INSTRUCTOR')") + public ResponseEntity> getCoursesByInstructor(@PathVariable Long instructorId) { + List courses = courseService.findByInstructorId(instructorId); + return ResponseEntity.ok(courses); + } + + @GetMapping("/status/{status}") + @PreAuthorize("hasRole('ADMIN') or hasRole('INSTRUCTOR')") + public ResponseEntity> getCoursesByStatus(@PathVariable Course.CourseStatus status) { + List courses = courseService.findByStatus(status); + return ResponseEntity.ok(courses); + } + + @GetMapping("/search") + public ResponseEntity> searchCourses(@RequestParam String searchTerm) { + List courses = courseService.findByCourseNameOrDescriptionContaining(searchTerm); + return ResponseEntity.ok(courses); + } + + @GetMapping("/student/{studentId}") + @PreAuthorize("hasRole('ADMIN') or hasRole('INSTRUCTOR') or hasRole('STUDENT')") + public ResponseEntity> getCoursesByStudent(@PathVariable Long studentId) { + List courses = courseService.findByStudentId(studentId); + return ResponseEntity.ok(courses); + } + + @GetMapping("/available") + public ResponseEntity> getAvailableCourses() { + List courses = courseService.findAvailableCourses(); + return ResponseEntity.ok(courses); + } + + @PostMapping + @PreAuthorize("hasRole('ADMIN') or hasRole('INSTRUCTOR')") + public ResponseEntity createCourse(@Valid @RequestBody Course course) { + try { + if (courseService.existsByCourseCode(course.getCourseCode())) { + return ResponseEntity.badRequest().build(); + } + Course savedCourse = courseService.saveCourse(course); + return ResponseEntity.status(HttpStatus.CREATED).body(savedCourse); + } catch (Exception e) { + return ResponseEntity.badRequest().build(); + } + } + + @PutMapping("/{id}") + @PreAuthorize("hasRole('ADMIN') or (hasRole('INSTRUCTOR') and @courseService.findById(#id).get().instructor.username == authentication.name)") + public ResponseEntity updateCourse(@PathVariable Long id, @Valid @RequestBody Course courseDetails) { + Optional courseOptional = courseService.findById(id); + if (courseOptional.isPresent()) { + Course course = courseOptional.get(); + course.setCourseName(courseDetails.getCourseName()); + course.setDescription(courseDetails.getDescription()); + course.setCredits(courseDetails.getCredits()); + course.setMaxStudents(courseDetails.getMaxStudents()); + course.setFee(courseDetails.getFee()); + course.setStartDate(courseDetails.getStartDate()); + course.setEndDate(courseDetails.getEndDate()); + + Course updatedCourse = courseService.updateCourse(course); + return ResponseEntity.ok(updatedCourse); + } else { + return ResponseEntity.notFound().build(); + } + } + + @PatchMapping("/{id}/status") + @PreAuthorize("hasRole('ADMIN') or (hasRole('INSTRUCTOR') and @courseService.findById(#id).get().instructor.username == authentication.name)") + public ResponseEntity updateCourseStatus(@PathVariable Long id, @RequestParam Course.CourseStatus status) { + try { + courseService.updateCourseStatus(id, status); + return ResponseEntity.ok("Course status updated successfully"); + } catch (RuntimeException e) { + return ResponseEntity.badRequest().body(e.getMessage()); + } + } + + @DeleteMapping("/{id}") + @PreAuthorize("hasRole('ADMIN')") + public ResponseEntity deleteCourse(@PathVariable Long id) { + if (courseService.findById(id).isPresent()) { + courseService.deleteCourse(id); + return ResponseEntity.noContent().build(); + } else { + return ResponseEntity.notFound().build(); + } + } + + @GetMapping("/{courseId}/students") + @PreAuthorize("hasRole('ADMIN') or hasRole('INSTRUCTOR')") + public ResponseEntity> getCourseStudents(@PathVariable Long courseId) { + List students = courseService.getCourseStudents(courseId); + return ResponseEntity.ok(students); + } + + @GetMapping("/{courseId}/enrollment/current") + public ResponseEntity getCurrentEnrollment(@PathVariable Long courseId) { + int enrollment = courseService.getCurrentEnrollment(courseId); + return ResponseEntity.ok(enrollment); + } + + @GetMapping("/{courseId}/enrollment/full") + public ResponseEntity isEnrollmentFull(@PathVariable Long courseId) { + boolean isFull = courseService.isCourseEnrollmentFull(courseId); + return ResponseEntity.ok(isFull); + } + + @GetMapping("/count/status/{status}") + @PreAuthorize("hasRole('ADMIN')") + public ResponseEntity getCourseCountByStatus(@PathVariable Course.CourseStatus status) { + Long count = courseService.countByStatus(status); + return ResponseEntity.ok(count); + } +} \ No newline at end of file diff --git a/src/main/java/com/lms/controller/InstructorController.java b/src/main/java/com/lms/controller/InstructorController.java new file mode 100644 index 0000000..03f106f --- /dev/null +++ b/src/main/java/com/lms/controller/InstructorController.java @@ -0,0 +1,157 @@ +package com.lms.controller; + +import com.lms.entity.Course; +import com.lms.entity.Instructor; +import com.lms.service.InstructorService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.*; + +import jakarta.validation.Valid; +import java.util.List; +import java.util.Optional; + +@RestController +@RequestMapping("/api/instructors") +@CrossOrigin(origins = "*", maxAge = 3600) +public class InstructorController { + + @Autowired + private InstructorService instructorService; + + @GetMapping + @PreAuthorize("hasRole('ADMIN')") + public ResponseEntity> getAllInstructors() { + List instructors = instructorService.findAllInstructors(); + return ResponseEntity.ok(instructors); + } + + @GetMapping("/{id}") + @PreAuthorize("hasRole('ADMIN') or (hasRole('INSTRUCTOR') and @instructorService.findById(#id).get().username == authentication.name)") + public ResponseEntity getInstructorById(@PathVariable Long id) { + Optional instructor = instructorService.findById(id); + return instructor.map(ResponseEntity::ok) + .orElse(ResponseEntity.notFound().build()); + } + + @GetMapping("/employee-id/{employeeId}") + @PreAuthorize("hasRole('ADMIN')") + public ResponseEntity getInstructorByEmployeeId(@PathVariable String employeeId) { + Optional instructor = instructorService.findByEmployeeId(employeeId); + return instructor.map(ResponseEntity::ok) + .orElse(ResponseEntity.notFound().build()); + } + + @GetMapping("/email/{email}") + @PreAuthorize("hasRole('ADMIN')") + public ResponseEntity getInstructorByEmail(@PathVariable String email) { + Optional instructor = instructorService.findByEmail(email); + return instructor.map(ResponseEntity::ok) + .orElse(ResponseEntity.notFound().build()); + } + + @GetMapping("/department/{department}") + @PreAuthorize("hasRole('ADMIN')") + public ResponseEntity> getInstructorsByDepartment(@PathVariable String department) { + List instructors = instructorService.findByDepartment(department); + return ResponseEntity.ok(instructors); + } + + @GetMapping("/search") + @PreAuthorize("hasRole('ADMIN')") + public ResponseEntity> searchInstructorsByName(@RequestParam String name) { + List instructors = instructorService.findByNameContaining(name); + return ResponseEntity.ok(instructors); + } + + @GetMapping("/specialization") + @PreAuthorize("hasRole('ADMIN')") + public ResponseEntity> searchInstructorsBySpecialization(@RequestParam String specialization) { + List instructors = instructorService.findBySpecializationContaining(specialization); + return ResponseEntity.ok(instructors); + } + + @PostMapping + @PreAuthorize("hasRole('ADMIN')") + public ResponseEntity createInstructor(@Valid @RequestBody Instructor instructor) { + try { + if (instructorService.existsByEmployeeId(instructor.getEmployeeId())) { + return ResponseEntity.badRequest().build(); + } + Instructor savedInstructor = instructorService.saveInstructor(instructor); + return ResponseEntity.status(HttpStatus.CREATED).body(savedInstructor); + } catch (Exception e) { + return ResponseEntity.badRequest().build(); + } + } + + @PutMapping("/{id}") + @PreAuthorize("hasRole('ADMIN') or (hasRole('INSTRUCTOR') and @instructorService.findById(#id).get().username == authentication.name)") + public ResponseEntity updateInstructor(@PathVariable Long id, @Valid @RequestBody Instructor instructorDetails) { + Optional instructorOptional = instructorService.findById(id); + if (instructorOptional.isPresent()) { + Instructor instructor = instructorOptional.get(); + instructor.setFirstName(instructorDetails.getFirstName()); + instructor.setLastName(instructorDetails.getLastName()); + instructor.setEmail(instructorDetails.getEmail()); + instructor.setPhoneNumber(instructorDetails.getPhoneNumber()); + instructor.setDepartment(instructorDetails.getDepartment()); + instructor.setSpecialization(instructorDetails.getSpecialization()); + instructor.setBio(instructorDetails.getBio()); + + Instructor updatedInstructor = instructorService.updateInstructor(instructor); + return ResponseEntity.ok(updatedInstructor); + } else { + return ResponseEntity.notFound().build(); + } + } + + @DeleteMapping("/{id}") + @PreAuthorize("hasRole('ADMIN')") + public ResponseEntity deleteInstructor(@PathVariable Long id) { + if (instructorService.findById(id).isPresent()) { + instructorService.deleteInstructor(id); + return ResponseEntity.noContent().build(); + } else { + return ResponseEntity.notFound().build(); + } + } + + @PostMapping("/{instructorId}/assign-course/{courseId}") + @PreAuthorize("hasRole('ADMIN')") + public ResponseEntity assignCourseToInstructor(@PathVariable Long instructorId, @PathVariable Long courseId) { + try { + instructorService.assignCourseToInstructor(instructorId, courseId); + return ResponseEntity.ok("Course assigned to instructor successfully"); + } catch (RuntimeException e) { + return ResponseEntity.badRequest().body(e.getMessage()); + } + } + + @PostMapping("/{instructorId}/remove-course/{courseId}") + @PreAuthorize("hasRole('ADMIN')") + public ResponseEntity removeCourseFromInstructor(@PathVariable Long instructorId, @PathVariable Long courseId) { + try { + instructorService.removeCourseFromInstructor(instructorId, courseId); + return ResponseEntity.ok("Course removed from instructor successfully"); + } catch (RuntimeException e) { + return ResponseEntity.badRequest().body(e.getMessage()); + } + } + + @GetMapping("/{instructorId}/courses") + @PreAuthorize("hasRole('ADMIN') or (hasRole('INSTRUCTOR') and @instructorService.findById(#instructorId).get().username == authentication.name)") + public ResponseEntity> getInstructorCourses(@PathVariable Long instructorId) { + List courses = instructorService.getInstructorCourses(instructorId); + return ResponseEntity.ok(courses); + } + + @GetMapping("/count") + @PreAuthorize("hasRole('ADMIN')") + public ResponseEntity getActiveInstructorCount() { + Long count = instructorService.countActiveInstructors(); + return ResponseEntity.ok(count); + } +} \ No newline at end of file diff --git a/src/main/java/com/lms/controller/StudentController.java b/src/main/java/com/lms/controller/StudentController.java new file mode 100644 index 0000000..3ab1e14 --- /dev/null +++ b/src/main/java/com/lms/controller/StudentController.java @@ -0,0 +1,149 @@ +package com.lms.controller; + +import com.lms.entity.Course; +import com.lms.entity.Student; +import com.lms.service.StudentService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.*; + +import jakarta.validation.Valid; +import java.util.List; +import java.util.Optional; + +@RestController +@RequestMapping("/api/students") +@CrossOrigin(origins = "*", maxAge = 3600) +public class StudentController { + + @Autowired + private StudentService studentService; + + @GetMapping + @PreAuthorize("hasRole('ADMIN') or hasRole('INSTRUCTOR')") + public ResponseEntity> getAllStudents() { + List students = studentService.findAllStudents(); + return ResponseEntity.ok(students); + } + + @GetMapping("/{id}") + @PreAuthorize("hasRole('ADMIN') or hasRole('INSTRUCTOR') or (hasRole('STUDENT') and @studentService.findById(#id).get().username == authentication.name)") + public ResponseEntity getStudentById(@PathVariable Long id) { + Optional student = studentService.findById(id); + return student.map(ResponseEntity::ok) + .orElse(ResponseEntity.notFound().build()); + } + + @GetMapping("/student-id/{studentId}") + @PreAuthorize("hasRole('ADMIN') or hasRole('INSTRUCTOR')") + public ResponseEntity getStudentByStudentId(@PathVariable String studentId) { + Optional student = studentService.findByStudentId(studentId); + return student.map(ResponseEntity::ok) + .orElse(ResponseEntity.notFound().build()); + } + + @GetMapping("/email/{email}") + @PreAuthorize("hasRole('ADMIN')") + public ResponseEntity getStudentByEmail(@PathVariable String email) { + Optional student = studentService.findByEmail(email); + return student.map(ResponseEntity::ok) + .orElse(ResponseEntity.notFound().build()); + } + + @GetMapping("/search") + @PreAuthorize("hasRole('ADMIN') or hasRole('INSTRUCTOR')") + public ResponseEntity> searchStudentsByName(@RequestParam String name) { + List students = studentService.findByNameContaining(name); + return ResponseEntity.ok(students); + } + + @GetMapping("/course/{courseId}") + @PreAuthorize("hasRole('ADMIN') or hasRole('INSTRUCTOR')") + public ResponseEntity> getStudentsByCourse(@PathVariable Long courseId) { + List students = studentService.findByCourseId(courseId); + return ResponseEntity.ok(students); + } + + @PostMapping + @PreAuthorize("hasRole('ADMIN')") + public ResponseEntity createStudent(@Valid @RequestBody Student student) { + try { + if (studentService.existsByStudentId(student.getStudentId())) { + return ResponseEntity.badRequest().build(); + } + Student savedStudent = studentService.saveStudent(student); + return ResponseEntity.status(HttpStatus.CREATED).body(savedStudent); + } catch (Exception e) { + return ResponseEntity.badRequest().build(); + } + } + + @PutMapping("/{id}") + @PreAuthorize("hasRole('ADMIN') or (hasRole('STUDENT') and @studentService.findById(#id).get().username == authentication.name)") + public ResponseEntity updateStudent(@PathVariable Long id, @Valid @RequestBody Student studentDetails) { + Optional studentOptional = studentService.findById(id); + if (studentOptional.isPresent()) { + Student student = studentOptional.get(); + student.setFirstName(studentDetails.getFirstName()); + student.setLastName(studentDetails.getLastName()); + student.setEmail(studentDetails.getEmail()); + student.setPhoneNumber(studentDetails.getPhoneNumber()); + student.setAddress(studentDetails.getAddress()); + student.setDateOfBirth(studentDetails.getDateOfBirth()); + + Student updatedStudent = studentService.updateStudent(student); + return ResponseEntity.ok(updatedStudent); + } else { + return ResponseEntity.notFound().build(); + } + } + + @DeleteMapping("/{id}") + @PreAuthorize("hasRole('ADMIN')") + public ResponseEntity deleteStudent(@PathVariable Long id) { + if (studentService.findById(id).isPresent()) { + studentService.deleteStudent(id); + return ResponseEntity.noContent().build(); + } else { + return ResponseEntity.notFound().build(); + } + } + + @PostMapping("/{studentId}/enroll/{courseId}") + @PreAuthorize("hasRole('ADMIN') or (hasRole('STUDENT') and @studentService.findById(#studentId).get().username == authentication.name)") + public ResponseEntity enrollInCourse(@PathVariable Long studentId, @PathVariable Long courseId) { + try { + studentService.enrollStudentInCourse(studentId, courseId); + return ResponseEntity.ok("Student enrolled successfully"); + } catch (RuntimeException e) { + return ResponseEntity.badRequest().body(e.getMessage()); + } + } + + @PostMapping("/{studentId}/unenroll/{courseId}") + @PreAuthorize("hasRole('ADMIN') or (hasRole('STUDENT') and @studentService.findById(#studentId).get().username == authentication.name)") + public ResponseEntity unenrollFromCourse(@PathVariable Long studentId, @PathVariable Long courseId) { + try { + studentService.unenrollStudentFromCourse(studentId, courseId); + return ResponseEntity.ok("Student unenrolled successfully"); + } catch (RuntimeException e) { + return ResponseEntity.badRequest().body(e.getMessage()); + } + } + + @GetMapping("/{studentId}/courses") + @PreAuthorize("hasRole('ADMIN') or hasRole('INSTRUCTOR') or (hasRole('STUDENT') and @studentService.findById(#studentId).get().username == authentication.name)") + public ResponseEntity> getStudentCourses(@PathVariable Long studentId) { + List courses = studentService.getStudentCourses(studentId); + return ResponseEntity.ok(courses); + } + + @GetMapping("/count") + @PreAuthorize("hasRole('ADMIN')") + public ResponseEntity getActiveStudentCount() { + Long count = studentService.countActiveStudents(); + return ResponseEntity.ok(count); + } +} \ No newline at end of file diff --git a/src/main/java/com/lms/entity/Admin.java b/src/main/java/com/lms/entity/Admin.java new file mode 100644 index 0000000..2e0da53 --- /dev/null +++ b/src/main/java/com/lms/entity/Admin.java @@ -0,0 +1,55 @@ +package com.lms.entity; + +import jakarta.persistence.*; +import java.time.LocalDate; + +@Entity +@DiscriminatorValue("ADMIN") +public class Admin extends User { + + @Column(name = "admin_id", unique = true) + private String adminId; + + @Column(name = "hire_date") + private LocalDate hireDate; + + private String department; + + @Enumerated(EnumType.STRING) + @Column(name = "admin_level") + private AdminLevel adminLevel; + + // Constructors + public Admin() { + super(); + setRole(Role.ADMIN); + this.hireDate = LocalDate.now(); + this.adminLevel = AdminLevel.STANDARD; + } + + public Admin(String username, String email, String password, String firstName, String lastName, String adminId) { + super(username, email, password, firstName, lastName, Role.ADMIN); + this.adminId = adminId; + this.hireDate = LocalDate.now(); + this.adminLevel = AdminLevel.STANDARD; + } + + // Getters and Setters + public String getAdminId() { return adminId; } + public void setAdminId(String adminId) { this.adminId = adminId; } + + public LocalDate getHireDate() { return hireDate; } + public void setHireDate(LocalDate hireDate) { this.hireDate = hireDate; } + + public String getDepartment() { return department; } + public void setDepartment(String department) { this.department = department; } + + public AdminLevel getAdminLevel() { return adminLevel; } + public void setAdminLevel(AdminLevel adminLevel) { this.adminLevel = adminLevel; } + + public enum AdminLevel { + STANDARD, + SUPER_ADMIN, + SYSTEM_ADMIN + } +} \ No newline at end of file diff --git a/src/main/java/com/lms/entity/Course.java b/src/main/java/com/lms/entity/Course.java new file mode 100644 index 0000000..8f08ebf --- /dev/null +++ b/src/main/java/com/lms/entity/Course.java @@ -0,0 +1,163 @@ +package com.lms.entity; + +import jakarta.persistence.*; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Positive; +import java.math.BigDecimal; +import java.time.LocalDateTime; +import java.util.HashSet; +import java.util.Set; + +@Entity +@Table(name = "courses") +public class Course { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @NotBlank + @Column(name = "course_code", unique = true) + private String courseCode; + + @NotBlank + @Column(name = "course_name") + private String courseName; + + @Column(columnDefinition = "TEXT") + private String description; + + @NotNull + @Positive + private Integer credits; + + private Integer maxStudents; + + private BigDecimal fee; + + @Enumerated(EnumType.STRING) + private CourseStatus status; + + @Column(name = "start_date") + private LocalDateTime startDate; + + @Column(name = "end_date") + private LocalDateTime endDate; + + @Column(name = "created_at") + private LocalDateTime createdAt; + + @Column(name = "updated_at") + private LocalDateTime updatedAt; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "instructor_id") + private Instructor instructor; + + @ManyToMany(mappedBy = "enrolledCourses", fetch = FetchType.LAZY) + private Set enrolledStudents = new HashSet<>(); + + @OneToMany(mappedBy = "course", cascade = CascadeType.ALL, fetch = FetchType.LAZY) + private Set enrollments = new HashSet<>(); + + @OneToMany(mappedBy = "course", cascade = CascadeType.ALL, fetch = FetchType.LAZY) + private Set lessons = new HashSet<>(); + + // Constructors + public Course() { + this.createdAt = LocalDateTime.now(); + this.updatedAt = LocalDateTime.now(); + this.status = CourseStatus.DRAFT; + } + + public Course(String courseCode, String courseName, String description, Integer credits, Instructor instructor) { + this(); + this.courseCode = courseCode; + this.courseName = courseName; + this.description = description; + this.credits = credits; + this.instructor = instructor; + } + + // Getters and Setters + public Long getId() { return id; } + public void setId(Long id) { this.id = id; } + + public String getCourseCode() { return courseCode; } + public void setCourseCode(String courseCode) { this.courseCode = courseCode; } + + public String getCourseName() { return courseName; } + public void setCourseName(String courseName) { this.courseName = courseName; } + + public String getDescription() { return description; } + public void setDescription(String description) { this.description = description; } + + public Integer getCredits() { return credits; } + public void setCredits(Integer credits) { this.credits = credits; } + + public Integer getMaxStudents() { return maxStudents; } + public void setMaxStudents(Integer maxStudents) { this.maxStudents = maxStudents; } + + public BigDecimal getFee() { return fee; } + public void setFee(BigDecimal fee) { this.fee = fee; } + + public CourseStatus getStatus() { return status; } + public void setStatus(CourseStatus status) { this.status = status; } + + public LocalDateTime getStartDate() { return startDate; } + public void setStartDate(LocalDateTime startDate) { this.startDate = startDate; } + + public LocalDateTime getEndDate() { return endDate; } + public void setEndDate(LocalDateTime endDate) { this.endDate = endDate; } + + public LocalDateTime getCreatedAt() { return createdAt; } + public void setCreatedAt(LocalDateTime createdAt) { this.createdAt = createdAt; } + + public LocalDateTime getUpdatedAt() { return updatedAt; } + public void setUpdatedAt(LocalDateTime updatedAt) { this.updatedAt = updatedAt; } + + public Instructor getInstructor() { return instructor; } + public void setInstructor(Instructor instructor) { this.instructor = instructor; } + + public Set getEnrolledStudents() { return enrolledStudents; } + public void setEnrolledStudents(Set enrolledStudents) { this.enrolledStudents = enrolledStudents; } + + public Set getEnrollments() { return enrollments; } + public void setEnrollments(Set enrollments) { this.enrollments = enrollments; } + + public Set getLessons() { return lessons; } + public void setLessons(Set lessons) { this.lessons = lessons; } + + @PreUpdate + public void preUpdate() { + this.updatedAt = LocalDateTime.now(); + } + + // Helper methods + public void addStudent(Student student) { + this.enrolledStudents.add(student); + student.getEnrolledCourses().add(this); + } + + public void removeStudent(Student student) { + this.enrolledStudents.remove(student); + student.getEnrolledCourses().remove(this); + } + + public int getCurrentEnrollment() { + return this.enrolledStudents.size(); + } + + public boolean isEnrollmentFull() { + return maxStudents != null && getCurrentEnrollment() >= maxStudents; + } + + public enum CourseStatus { + DRAFT, + PUBLISHED, + ACTIVE, + COMPLETED, + ARCHIVED + } +} \ No newline at end of file diff --git a/src/main/java/com/lms/entity/Enrollment.java b/src/main/java/com/lms/entity/Enrollment.java new file mode 100644 index 0000000..8a4cb41 --- /dev/null +++ b/src/main/java/com/lms/entity/Enrollment.java @@ -0,0 +1,79 @@ +package com.lms.entity; + +import jakarta.persistence.*; +import java.time.LocalDateTime; + +@Entity +@Table(name = "enrollments") +public class Enrollment { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "student_id") + private Student student; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "course_id") + private Course course; + + @Column(name = "enrollment_date") + private LocalDateTime enrollmentDate; + + @Enumerated(EnumType.STRING) + private EnrollmentStatus status; + + private Double grade; + + @Column(name = "completion_date") + private LocalDateTime completionDate; + + private String notes; + + // Constructors + public Enrollment() { + this.enrollmentDate = LocalDateTime.now(); + this.status = EnrollmentStatus.ENROLLED; + } + + public Enrollment(Student student, Course course) { + this(); + this.student = student; + this.course = course; + } + + // Getters and Setters + public Long getId() { return id; } + public void setId(Long id) { this.id = id; } + + public Student getStudent() { return student; } + public void setStudent(Student student) { this.student = student; } + + public Course getCourse() { return course; } + public void setCourse(Course course) { this.course = course; } + + public LocalDateTime getEnrollmentDate() { return enrollmentDate; } + public void setEnrollmentDate(LocalDateTime enrollmentDate) { this.enrollmentDate = enrollmentDate; } + + public EnrollmentStatus getStatus() { return status; } + public void setStatus(EnrollmentStatus status) { this.status = status; } + + public Double getGrade() { return grade; } + public void setGrade(Double grade) { this.grade = grade; } + + public LocalDateTime getCompletionDate() { return completionDate; } + public void setCompletionDate(LocalDateTime completionDate) { this.completionDate = completionDate; } + + public String getNotes() { return notes; } + public void setNotes(String notes) { this.notes = notes; } + + public enum EnrollmentStatus { + ENROLLED, + COMPLETED, + DROPPED, + SUSPENDED, + TRANSFERRED + } +} \ No newline at end of file diff --git a/src/main/java/com/lms/entity/Instructor.java b/src/main/java/com/lms/entity/Instructor.java new file mode 100644 index 0000000..e93653a --- /dev/null +++ b/src/main/java/com/lms/entity/Instructor.java @@ -0,0 +1,70 @@ +package com.lms.entity; + +import jakarta.persistence.*; +import java.time.LocalDate; +import java.util.HashSet; +import java.util.Set; + +@Entity +@DiscriminatorValue("INSTRUCTOR") +public class Instructor extends User { + + @Column(name = "employee_id", unique = true) + private String employeeId; + + private String department; + + @Column(name = "hire_date") + private LocalDate hireDate; + + private String specialization; + + private String bio; + + @OneToMany(mappedBy = "instructor", cascade = CascadeType.ALL, fetch = FetchType.LAZY) + private Set courses = new HashSet<>(); + + // Constructors + public Instructor() { + super(); + setRole(Role.INSTRUCTOR); + this.hireDate = LocalDate.now(); + } + + public Instructor(String username, String email, String password, String firstName, String lastName, String employeeId, String department) { + super(username, email, password, firstName, lastName, Role.INSTRUCTOR); + this.employeeId = employeeId; + this.department = department; + this.hireDate = LocalDate.now(); + } + + // Getters and Setters + public String getEmployeeId() { return employeeId; } + public void setEmployeeId(String employeeId) { this.employeeId = employeeId; } + + public String getDepartment() { return department; } + public void setDepartment(String department) { this.department = department; } + + public LocalDate getHireDate() { return hireDate; } + public void setHireDate(LocalDate hireDate) { this.hireDate = hireDate; } + + public String getSpecialization() { return specialization; } + public void setSpecialization(String specialization) { this.specialization = specialization; } + + public String getBio() { return bio; } + public void setBio(String bio) { this.bio = bio; } + + public Set getCourses() { return courses; } + public void setCourses(Set courses) { this.courses = courses; } + + // Helper methods + public void addCourse(Course course) { + this.courses.add(course); + course.setInstructor(this); + } + + public void removeCourse(Course course) { + this.courses.remove(course); + course.setInstructor(null); + } +} \ No newline at end of file diff --git a/src/main/java/com/lms/entity/Lesson.java b/src/main/java/com/lms/entity/Lesson.java new file mode 100644 index 0000000..28d0d15 --- /dev/null +++ b/src/main/java/com/lms/entity/Lesson.java @@ -0,0 +1,106 @@ +package com.lms.entity; + +import jakarta.persistence.*; +import jakarta.validation.constraints.NotBlank; +import java.time.LocalDateTime; + +@Entity +@Table(name = "lessons") +public class Lesson { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @NotBlank + private String title; + + @Column(columnDefinition = "TEXT") + private String content; + + @Column(name = "lesson_order") + private Integer lessonOrder; + + private Integer duration; // in minutes + + @Column(name = "video_url") + private String videoUrl; + + @Column(name = "material_url") + private String materialUrl; + + @Enumerated(EnumType.STRING) + private LessonType type; + + @Column(name = "created_at") + private LocalDateTime createdAt; + + @Column(name = "updated_at") + private LocalDateTime updatedAt; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "course_id") + private Course course; + + // Constructors + public Lesson() { + this.createdAt = LocalDateTime.now(); + this.updatedAt = LocalDateTime.now(); + this.type = LessonType.LECTURE; + } + + public Lesson(String title, String content, Course course) { + this(); + this.title = title; + this.content = content; + this.course = course; + } + + // Getters and Setters + public Long getId() { return id; } + public void setId(Long id) { this.id = id; } + + public String getTitle() { return title; } + public void setTitle(String title) { this.title = title; } + + public String getContent() { return content; } + public void setContent(String content) { this.content = content; } + + public Integer getLessonOrder() { return lessonOrder; } + public void setLessonOrder(Integer lessonOrder) { this.lessonOrder = lessonOrder; } + + public Integer getDuration() { return duration; } + public void setDuration(Integer duration) { this.duration = duration; } + + public String getVideoUrl() { return videoUrl; } + public void setVideoUrl(String videoUrl) { this.videoUrl = videoUrl; } + + public String getMaterialUrl() { return materialUrl; } + public void setMaterialUrl(String materialUrl) { this.materialUrl = materialUrl; } + + public LessonType getType() { return type; } + public void setType(LessonType type) { this.type = type; } + + public LocalDateTime getCreatedAt() { return createdAt; } + public void setCreatedAt(LocalDateTime createdAt) { this.createdAt = createdAt; } + + public LocalDateTime getUpdatedAt() { return updatedAt; } + public void setUpdatedAt(LocalDateTime updatedAt) { this.updatedAt = updatedAt; } + + public Course getCourse() { return course; } + public void setCourse(Course course) { this.course = course; } + + @PreUpdate + public void preUpdate() { + this.updatedAt = LocalDateTime.now(); + } + + public enum LessonType { + LECTURE, + VIDEO, + ASSIGNMENT, + QUIZ, + READING, + LAB + } +} \ No newline at end of file diff --git a/src/main/java/com/lms/entity/Role.java b/src/main/java/com/lms/entity/Role.java new file mode 100644 index 0000000..1eb24ce --- /dev/null +++ b/src/main/java/com/lms/entity/Role.java @@ -0,0 +1,7 @@ +package com.lms.entity; + +public enum Role { + ADMIN, + INSTRUCTOR, + STUDENT +} \ No newline at end of file diff --git a/src/main/java/com/lms/entity/Student.java b/src/main/java/com/lms/entity/Student.java new file mode 100644 index 0000000..0e370ad --- /dev/null +++ b/src/main/java/com/lms/entity/Student.java @@ -0,0 +1,76 @@ +package com.lms.entity; + +import jakarta.persistence.*; +import java.time.LocalDate; +import java.util.HashSet; +import java.util.Set; + +@Entity +@DiscriminatorValue("STUDENT") +public class Student extends User { + + @Column(name = "student_id", unique = true) + private String studentId; + + @Column(name = "date_of_birth") + private LocalDate dateOfBirth; + + private String address; + + @Column(name = "enrollment_date") + private LocalDate enrollmentDate; + + @ManyToMany(fetch = FetchType.LAZY) + @JoinTable( + name = "student_courses", + joinColumns = @JoinColumn(name = "student_id"), + inverseJoinColumns = @JoinColumn(name = "course_id") + ) + private Set enrolledCourses = new HashSet<>(); + + @OneToMany(mappedBy = "student", cascade = CascadeType.ALL, fetch = FetchType.LAZY) + private Set enrollments = new HashSet<>(); + + // Constructors + public Student() { + super(); + setRole(Role.STUDENT); + this.enrollmentDate = LocalDate.now(); + } + + public Student(String username, String email, String password, String firstName, String lastName, String studentId) { + super(username, email, password, firstName, lastName, Role.STUDENT); + this.studentId = studentId; + this.enrollmentDate = LocalDate.now(); + } + + // Getters and Setters + public String getStudentId() { return studentId; } + public void setStudentId(String studentId) { this.studentId = studentId; } + + public LocalDate getDateOfBirth() { return dateOfBirth; } + public void setDateOfBirth(LocalDate dateOfBirth) { this.dateOfBirth = dateOfBirth; } + + public String getAddress() { return address; } + public void setAddress(String address) { this.address = address; } + + public LocalDate getEnrollmentDate() { return enrollmentDate; } + public void setEnrollmentDate(LocalDate enrollmentDate) { this.enrollmentDate = enrollmentDate; } + + public Set getEnrolledCourses() { return enrolledCourses; } + public void setEnrolledCourses(Set enrolledCourses) { this.enrolledCourses = enrolledCourses; } + + public Set getEnrollments() { return enrollments; } + public void setEnrollments(Set enrollments) { this.enrollments = enrollments; } + + // Helper methods + public void enrollInCourse(Course course) { + this.enrolledCourses.add(course); + course.getEnrolledStudents().add(this); + } + + public void unenrollFromCourse(Course course) { + this.enrolledCourses.remove(course); + course.getEnrolledStudents().remove(this); + } +} \ No newline at end of file diff --git a/src/main/java/com/lms/entity/User.java b/src/main/java/com/lms/entity/User.java new file mode 100644 index 0000000..23831c2 --- /dev/null +++ b/src/main/java/com/lms/entity/User.java @@ -0,0 +1,113 @@ +package com.lms.entity; + +import jakarta.persistence.*; +import jakarta.validation.constraints.Email; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Size; +import java.time.LocalDateTime; +import java.util.HashSet; +import java.util.Set; + +@Entity +@Table(name = "users") +@Inheritance(strategy = InheritanceType.SINGLE_TABLE) +@DiscriminatorColumn(name = "user_type", discriminatorType = DiscriminatorType.STRING) +public abstract class User { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @NotBlank + @Size(max = 50) + @Column(unique = true) + private String username; + + @NotBlank + @Size(max = 100) + @Email + @Column(unique = true) + private String email; + + @NotBlank + @Size(max = 120) + private String password; + + @NotBlank + @Size(max = 50) + private String firstName; + + @NotBlank + @Size(max = 50) + private String lastName; + + private String phoneNumber; + + @Column(name = "created_at") + private LocalDateTime createdAt; + + @Column(name = "updated_at") + private LocalDateTime updatedAt; + + @Column(name = "is_active") + private Boolean isActive = true; + + @Enumerated(EnumType.STRING) + @Column(length = 20) + private Role role; + + // Constructors + public User() { + this.createdAt = LocalDateTime.now(); + this.updatedAt = LocalDateTime.now(); + } + + public User(String username, String email, String password, String firstName, String lastName, Role role) { + this(); + this.username = username; + this.email = email; + this.password = password; + this.firstName = firstName; + this.lastName = lastName; + this.role = role; + } + + // Getters and Setters + public Long getId() { return id; } + public void setId(Long id) { this.id = id; } + + public String getUsername() { return username; } + public void setUsername(String username) { this.username = username; } + + public String getEmail() { return email; } + public void setEmail(String email) { this.email = email; } + + public String getPassword() { return password; } + public void setPassword(String password) { this.password = password; } + + public String getFirstName() { return firstName; } + public void setFirstName(String firstName) { this.firstName = firstName; } + + public String getLastName() { return lastName; } + public void setLastName(String lastName) { this.lastName = lastName; } + + public String getPhoneNumber() { return phoneNumber; } + public void setPhoneNumber(String phoneNumber) { this.phoneNumber = phoneNumber; } + + public LocalDateTime getCreatedAt() { return createdAt; } + public void setCreatedAt(LocalDateTime createdAt) { this.createdAt = createdAt; } + + public LocalDateTime getUpdatedAt() { return updatedAt; } + public void setUpdatedAt(LocalDateTime updatedAt) { this.updatedAt = updatedAt; } + + public Boolean getIsActive() { return isActive; } + public void setIsActive(Boolean isActive) { this.isActive = isActive; } + + public Role getRole() { return role; } + public void setRole(Role role) { this.role = role; } + + @PreUpdate + public void preUpdate() { + this.updatedAt = LocalDateTime.now(); + } +} \ No newline at end of file diff --git a/src/main/java/com/lms/payload/request/LoginRequest.java b/src/main/java/com/lms/payload/request/LoginRequest.java new file mode 100644 index 0000000..7b9f524 --- /dev/null +++ b/src/main/java/com/lms/payload/request/LoginRequest.java @@ -0,0 +1,27 @@ +package com.lms.payload.request; + +import jakarta.validation.constraints.NotBlank; + +public class LoginRequest { + @NotBlank + private String username; + + @NotBlank + private String password; + + public String getUsername() { + return username; + } + + public void setUsername(String username) { + this.username = username; + } + + public String getPassword() { + return password; + } + + public void setPassword(String password) { + this.password = password; + } +} \ No newline at end of file diff --git a/src/main/java/com/lms/payload/request/SignupRequest.java b/src/main/java/com/lms/payload/request/SignupRequest.java new file mode 100644 index 0000000..04fb8b0 --- /dev/null +++ b/src/main/java/com/lms/payload/request/SignupRequest.java @@ -0,0 +1,106 @@ +package com.lms.payload.request; + +import jakarta.validation.constraints.Email; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Size; + +public class SignupRequest { + @NotBlank + @Size(min = 3, max = 20) + private String username; + + @NotBlank + @Size(max = 50) + @Email + private String email; + + @NotBlank + @Size(min = 6, max = 40) + private String password; + + @NotBlank + @Size(max = 50) + private String firstName; + + @NotBlank + @Size(max = 50) + private String lastName; + + // Role-specific fields + private String studentId; + private String employeeId; + private String adminId; + private String department; + + public String getUsername() { + return username; + } + + public void setUsername(String username) { + this.username = username; + } + + public String getEmail() { + return email; + } + + public void setEmail(String email) { + this.email = email; + } + + public String getPassword() { + return password; + } + + public void setPassword(String password) { + this.password = password; + } + + public String getFirstName() { + return firstName; + } + + public void setFirstName(String firstName) { + this.firstName = firstName; + } + + public String getLastName() { + return lastName; + } + + public void setLastName(String lastName) { + this.lastName = lastName; + } + + public String getStudentId() { + return studentId; + } + + public void setStudentId(String studentId) { + this.studentId = studentId; + } + + public String getEmployeeId() { + return employeeId; + } + + public void setEmployeeId(String employeeId) { + this.employeeId = employeeId; + } + + public String getAdminId() { + return adminId; + } + + public void setAdminId(String adminId) { + this.adminId = adminId; + } + + public String getDepartment() { + return department; + } + + public void setDepartment(String department) { + this.department = department; + } +} \ No newline at end of file diff --git a/src/main/java/com/lms/payload/response/JwtResponse.java b/src/main/java/com/lms/payload/response/JwtResponse.java new file mode 100644 index 0000000..59cbabf --- /dev/null +++ b/src/main/java/com/lms/payload/response/JwtResponse.java @@ -0,0 +1,66 @@ +package com.lms.payload.response; + +import org.springframework.security.core.GrantedAuthority; + +import java.util.Collection; + +public class JwtResponse { + private String token; + private String type = "Bearer"; + private Long id; + private String username; + private String email; + private Collection authorities; + + public JwtResponse(String accessToken, Long id, String username, String email, Collection authorities) { + this.token = accessToken; + this.id = id; + this.username = username; + this.email = email; + this.authorities = authorities; + } + + public String getAccessToken() { + return token; + } + + public void setAccessToken(String accessToken) { + this.token = accessToken; + } + + public String getTokenType() { + return type; + } + + public void setTokenType(String tokenType) { + this.type = tokenType; + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getEmail() { + return email; + } + + public void setEmail(String email) { + this.email = email; + } + + public String getUsername() { + return username; + } + + public void setUsername(String username) { + this.username = username; + } + + public Collection getAuthorities() { + return authorities; + } +} \ No newline at end of file diff --git a/src/main/java/com/lms/payload/response/MessageResponse.java b/src/main/java/com/lms/payload/response/MessageResponse.java new file mode 100644 index 0000000..f09f14e --- /dev/null +++ b/src/main/java/com/lms/payload/response/MessageResponse.java @@ -0,0 +1,17 @@ +package com.lms.payload.response; + +public class MessageResponse { + private String message; + + public MessageResponse(String message) { + this.message = message; + } + + public String getMessage() { + return message; + } + + public void setMessage(String message) { + this.message = message; + } +} \ No newline at end of file diff --git a/src/main/java/com/lms/repository/AdminRepository.java b/src/main/java/com/lms/repository/AdminRepository.java new file mode 100644 index 0000000..e5e63e5 --- /dev/null +++ b/src/main/java/com/lms/repository/AdminRepository.java @@ -0,0 +1,30 @@ +package com.lms.repository; + +import com.lms.entity.Admin; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; +import org.springframework.stereotype.Repository; + +import java.util.List; +import java.util.Optional; + +@Repository +public interface AdminRepository extends JpaRepository { + + Optional findByAdminId(String adminId); + + Optional findByEmail(String email); + + Boolean existsByAdminId(String adminId); + + List findByDepartment(String department); + + List findByAdminLevel(Admin.AdminLevel adminLevel); + + @Query("SELECT a FROM Admin a WHERE a.firstName LIKE %:name% OR a.lastName LIKE %:name%") + List findByNameContaining(@Param("name") String name); + + @Query("SELECT COUNT(a) FROM Admin a WHERE a.isActive = true") + Long countActiveAdmins(); +} \ No newline at end of file diff --git a/src/main/java/com/lms/repository/CourseRepository.java b/src/main/java/com/lms/repository/CourseRepository.java new file mode 100644 index 0000000..af68801 --- /dev/null +++ b/src/main/java/com/lms/repository/CourseRepository.java @@ -0,0 +1,37 @@ +package com.lms.repository; + +import com.lms.entity.Course; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; +import org.springframework.stereotype.Repository; + +import java.util.List; +import java.util.Optional; + +@Repository +public interface CourseRepository extends JpaRepository { + + Optional findByCourseCode(String courseCode); + + Boolean existsByCourseCode(String courseCode); + + List findByInstructorId(Long instructorId); + + List findByStatus(Course.CourseStatus status); + + @Query("SELECT c FROM Course c WHERE c.courseName LIKE %:name% OR c.description LIKE %:name%") + List findByCourseNameOrDescriptionContaining(@Param("name") String name); + + @Query("SELECT c FROM Course c JOIN c.enrolledStudents s WHERE s.id = :studentId") + List findByStudentId(@Param("studentId") Long studentId); + + @Query("SELECT c FROM Course c WHERE c.credits = :credits") + List findByCredits(@Param("credits") Integer credits); + + @Query("SELECT COUNT(c) FROM Course c WHERE c.status = :status") + Long countByStatus(@Param("status") Course.CourseStatus status); + + @Query("SELECT c FROM Course c WHERE SIZE(c.enrolledStudents) < c.maxStudents OR c.maxStudents IS NULL") + List findAvailableCourses(); +} \ No newline at end of file diff --git a/src/main/java/com/lms/repository/EnrollmentRepository.java b/src/main/java/com/lms/repository/EnrollmentRepository.java new file mode 100644 index 0000000..0c53496 --- /dev/null +++ b/src/main/java/com/lms/repository/EnrollmentRepository.java @@ -0,0 +1,41 @@ +package com.lms.repository; + +import com.lms.entity.Enrollment; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; +import org.springframework.stereotype.Repository; + +import java.time.LocalDateTime; +import java.util.List; +import java.util.Optional; + +@Repository +public interface EnrollmentRepository extends JpaRepository { + + List findByStudentId(Long studentId); + + List findByCourseId(Long courseId); + + List findByStatus(Enrollment.EnrollmentStatus status); + + Optional findByStudentIdAndCourseId(Long studentId, Long courseId); + + @Query("SELECT e FROM Enrollment e WHERE e.student.id = :studentId AND e.status = :status") + List findByStudentIdAndStatus(@Param("studentId") Long studentId, + @Param("status") Enrollment.EnrollmentStatus status); + + @Query("SELECT e FROM Enrollment e WHERE e.course.id = :courseId AND e.status = :status") + List findByCourseIdAndStatus(@Param("courseId") Long courseId, + @Param("status") Enrollment.EnrollmentStatus status); + + @Query("SELECT e FROM Enrollment e WHERE e.enrollmentDate BETWEEN :startDate AND :endDate") + List findByEnrollmentDateBetween(@Param("startDate") LocalDateTime startDate, + @Param("endDate") LocalDateTime endDate); + + @Query("SELECT COUNT(e) FROM Enrollment e WHERE e.status = :status") + Long countByStatus(@Param("status") Enrollment.EnrollmentStatus status); + + @Query("SELECT AVG(e.grade) FROM Enrollment e WHERE e.course.id = :courseId AND e.grade IS NOT NULL") + Double findAverageGradeByCourseId(@Param("courseId") Long courseId); +} \ No newline at end of file diff --git a/src/main/java/com/lms/repository/InstructorRepository.java b/src/main/java/com/lms/repository/InstructorRepository.java new file mode 100644 index 0000000..52fc985 --- /dev/null +++ b/src/main/java/com/lms/repository/InstructorRepository.java @@ -0,0 +1,31 @@ +package com.lms.repository; + +import com.lms.entity.Instructor; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; +import org.springframework.stereotype.Repository; + +import java.util.List; +import java.util.Optional; + +@Repository +public interface InstructorRepository extends JpaRepository { + + Optional findByEmployeeId(String employeeId); + + Optional findByEmail(String email); + + Boolean existsByEmployeeId(String employeeId); + + List findByDepartment(String department); + + @Query("SELECT i FROM Instructor i WHERE i.firstName LIKE %:name% OR i.lastName LIKE %:name%") + List findByNameContaining(@Param("name") String name); + + @Query("SELECT i FROM Instructor i WHERE i.specialization LIKE %:specialization%") + List findBySpecializationContaining(@Param("specialization") String specialization); + + @Query("SELECT COUNT(i) FROM Instructor i WHERE i.isActive = true") + Long countActiveInstructors(); +} \ No newline at end of file diff --git a/src/main/java/com/lms/repository/LessonRepository.java b/src/main/java/com/lms/repository/LessonRepository.java new file mode 100644 index 0000000..53010c4 --- /dev/null +++ b/src/main/java/com/lms/repository/LessonRepository.java @@ -0,0 +1,32 @@ +package com.lms.repository; + +import com.lms.entity.Lesson; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; +import org.springframework.stereotype.Repository; + +import java.util.List; + +@Repository +public interface LessonRepository extends JpaRepository { + + List findByCourseId(Long courseId); + + List findByCourseIdOrderByLessonOrder(Long courseId); + + List findByType(Lesson.LessonType type); + + @Query("SELECT l FROM Lesson l WHERE l.course.id = :courseId AND l.type = :type") + List findByCourseIdAndType(@Param("courseId") Long courseId, + @Param("type") Lesson.LessonType type); + + @Query("SELECT l FROM Lesson l WHERE l.title LIKE %:title%") + List findByTitleContaining(@Param("title") String title); + + @Query("SELECT COUNT(l) FROM Lesson l WHERE l.course.id = :courseId") + Long countByCourseId(@Param("courseId") Long courseId); + + @Query("SELECT SUM(l.duration) FROM Lesson l WHERE l.course.id = :courseId") + Integer sumDurationByCourseId(@Param("courseId") Long courseId); +} \ No newline at end of file diff --git a/src/main/java/com/lms/repository/StudentRepository.java b/src/main/java/com/lms/repository/StudentRepository.java new file mode 100644 index 0000000..e7e6365 --- /dev/null +++ b/src/main/java/com/lms/repository/StudentRepository.java @@ -0,0 +1,29 @@ +package com.lms.repository; + +import com.lms.entity.Student; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; +import org.springframework.stereotype.Repository; + +import java.util.List; +import java.util.Optional; + +@Repository +public interface StudentRepository extends JpaRepository { + + Optional findByStudentId(String studentId); + + Optional findByEmail(String email); + + Boolean existsByStudentId(String studentId); + + @Query("SELECT s FROM Student s WHERE s.firstName LIKE %:name% OR s.lastName LIKE %:name%") + List findByNameContaining(@Param("name") String name); + + @Query("SELECT s FROM Student s JOIN s.enrolledCourses c WHERE c.id = :courseId") + List findByCourseId(@Param("courseId") Long courseId); + + @Query("SELECT COUNT(s) FROM Student s WHERE s.isActive = true") + Long countActiveStudents(); +} \ No newline at end of file diff --git a/src/main/java/com/lms/repository/UserRepository.java b/src/main/java/com/lms/repository/UserRepository.java new file mode 100644 index 0000000..b916544 --- /dev/null +++ b/src/main/java/com/lms/repository/UserRepository.java @@ -0,0 +1,24 @@ +package com.lms.repository; + +import com.lms.entity.User; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; +import org.springframework.stereotype.Repository; + +import java.util.Optional; + +@Repository +public interface UserRepository extends JpaRepository { + + Optional findByUsername(String username); + + Optional findByEmail(String email); + + Boolean existsByUsername(String username); + + Boolean existsByEmail(String email); + + @Query("SELECT u FROM User u WHERE u.username = :username OR u.email = :username") + Optional findByUsernameOrEmail(@Param("username") String username); +} \ No newline at end of file diff --git a/src/main/java/com/lms/security/WebSecurityConfig.java b/src/main/java/com/lms/security/WebSecurityConfig.java new file mode 100644 index 0000000..8a5b126 --- /dev/null +++ b/src/main/java/com/lms/security/WebSecurityConfig.java @@ -0,0 +1,82 @@ +package com.lms.security; + +import com.lms.security.jwt.AuthEntryPointJwt; +import com.lms.security.jwt.AuthTokenFilter; +import com.lms.security.services.UserDetailsServiceImpl; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.authentication.dao.DaoAuthenticationProvider; +import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration; +import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.http.SessionCreationPolicy; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.security.web.SecurityFilterChain; +import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; + +@Configuration +@EnableMethodSecurity(prePostEnabled = true) +public class WebSecurityConfig { + + @Autowired + UserDetailsServiceImpl userDetailsService; + + @Autowired + private AuthEntryPointJwt unauthorizedHandler; + + @Bean + public AuthTokenFilter authenticationJwtTokenFilter() { + return new AuthTokenFilter(); + } + + @Bean + public DaoAuthenticationProvider authenticationProvider() { + DaoAuthenticationProvider authProvider = new DaoAuthenticationProvider(); + + authProvider.setUserDetailsService(userDetailsService); + authProvider.setPasswordEncoder(passwordEncoder()); + + return authProvider; + } + + @Bean + public AuthenticationManager authenticationManager(AuthenticationConfiguration authConfig) throws Exception { + return authConfig.getAuthenticationManager(); + } + + @Bean + public PasswordEncoder passwordEncoder() { + return new BCryptPasswordEncoder(); + } + + @Bean + public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { + http.cors().and().csrf().disable() + .exceptionHandling().authenticationEntryPoint(unauthorizedHandler).and() + .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and() + .authorizeHttpRequests(authz -> authz + .requestMatchers("/api/auth/**").permitAll() + .requestMatchers("/api/courses").permitAll() + .requestMatchers("/api/courses/{id}").permitAll() + .requestMatchers("/api/courses/code/**").permitAll() + .requestMatchers("/api/courses/search").permitAll() + .requestMatchers("/api/courses/available").permitAll() + .requestMatchers("/api/courses/{courseId}/enrollment/current").permitAll() + .requestMatchers("/api/courses/{courseId}/enrollment/full").permitAll() + .requestMatchers("/h2-console/**").permitAll() + .anyRequest().authenticated() + ); + + // Allow H2 console frames + http.headers().frameOptions().sameOrigin(); + + http.authenticationProvider(authenticationProvider()); + + http.addFilterBefore(authenticationJwtTokenFilter(), UsernamePasswordAuthenticationFilter.class); + + return http.build(); + } +} \ No newline at end of file diff --git a/src/main/java/com/lms/security/jwt/AuthEntryPointJwt.java b/src/main/java/com/lms/security/jwt/AuthEntryPointJwt.java new file mode 100644 index 0000000..0eb90f9 --- /dev/null +++ b/src/main/java/com/lms/security/jwt/AuthEntryPointJwt.java @@ -0,0 +1,40 @@ +package com.lms.security.jwt; + +import com.fasterxml.jackson.databind.ObjectMapper; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.http.MediaType; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.web.AuthenticationEntryPoint; +import org.springframework.stereotype.Component; + +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; + +@Component +public class AuthEntryPointJwt implements AuthenticationEntryPoint { + + private static final Logger logger = LoggerFactory.getLogger(AuthEntryPointJwt.class); + + @Override + public void commence(HttpServletRequest request, HttpServletResponse response, + AuthenticationException authException) throws IOException, ServletException { + logger.error("Unauthorized error: {}", authException.getMessage()); + + response.setContentType(MediaType.APPLICATION_JSON_VALUE); + response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); + + final Map body = new HashMap<>(); + body.put("status", HttpServletResponse.SC_UNAUTHORIZED); + body.put("error", "Unauthorized"); + body.put("message", authException.getMessage()); + body.put("path", request.getServletPath()); + + final ObjectMapper mapper = new ObjectMapper(); + mapper.writeValue(response.getOutputStream(), body); + } +} \ No newline at end of file diff --git a/src/main/java/com/lms/security/jwt/AuthTokenFilter.java b/src/main/java/com/lms/security/jwt/AuthTokenFilter.java new file mode 100644 index 0000000..9f215d7 --- /dev/null +++ b/src/main/java/com/lms/security/jwt/AuthTokenFilter.java @@ -0,0 +1,63 @@ +package com.lms.security.jwt; + +import com.lms.security.services.UserDetailsServiceImpl; +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.web.authentication.WebAuthenticationDetailsSource; +import org.springframework.util.StringUtils; +import org.springframework.web.filter.OncePerRequestFilter; + +import java.io.IOException; + +public class AuthTokenFilter extends OncePerRequestFilter { + @Autowired + private JwtUtils jwtUtils; + + @Autowired + private UserDetailsServiceImpl userDetailsService; + + private static final Logger logger = LoggerFactory.getLogger(AuthTokenFilter.class); + + @Override + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) + throws ServletException, IOException { + try { + String jwt = parseJwt(request); + if (jwt != null && jwtUtils.validateJwtToken(jwt)) { + String username = jwtUtils.getUserNameFromJwtToken(jwt); + + UserDetails userDetails = userDetailsService.loadUserByUsername(username); + UsernamePasswordAuthenticationToken authentication = + new UsernamePasswordAuthenticationToken( + userDetails, + null, + userDetails.getAuthorities()); + authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); + + SecurityContextHolder.getContext().setAuthentication(authentication); + } + } catch (Exception e) { + logger.error("Cannot set user authentication: {}", e); + } + + filterChain.doFilter(request, response); + } + + private String parseJwt(HttpServletRequest request) { + String headerAuth = request.getHeader("Authorization"); + + if (StringUtils.hasText(headerAuth) && headerAuth.startsWith("Bearer ")) { + return headerAuth.substring(7); + } + + return null; + } +} \ No newline at end of file diff --git a/src/main/java/com/lms/security/jwt/JwtUtils.java b/src/main/java/com/lms/security/jwt/JwtUtils.java new file mode 100644 index 0000000..0e52e75 --- /dev/null +++ b/src/main/java/com/lms/security/jwt/JwtUtils.java @@ -0,0 +1,63 @@ +package com.lms.security.jwt; + +import com.lms.security.services.UserDetailsImpl; +import io.jsonwebtoken.*; +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 java.security.Key; +import java.util.Date; + +@Component +public class JwtUtils { + private static final Logger logger = LoggerFactory.getLogger(JwtUtils.class); + + @Value("${jwt.secret}") + private String jwtSecret; + + @Value("${jwt.expiration}") + private int jwtExpirationMs; + + public String generateJwtToken(Authentication authentication) { + + UserDetailsImpl userPrincipal = (UserDetailsImpl) authentication.getPrincipal(); + + return Jwts.builder() + .setSubject((userPrincipal.getUsername())) + .setIssuedAt(new Date()) + .setExpiration(new Date((new Date()).getTime() + jwtExpirationMs)) + .signWith(key(), SignatureAlgorithm.HS512) + .compact(); + } + + private Key key() { + return Keys.hmacShaKeyFor(Decoders.BASE64.decode(jwtSecret)); + } + + public String getUserNameFromJwtToken(String token) { + return Jwts.parserBuilder().setSigningKey(key()).build() + .parseClaimsJws(token).getBody().getSubject(); + } + + public boolean validateJwtToken(String authToken) { + try { + Jwts.parserBuilder().setSigningKey(key()).build().parse(authToken); + return true; + } catch (MalformedJwtException e) { + logger.error("Invalid JWT token: {}", e.getMessage()); + } catch (ExpiredJwtException e) { + logger.error("JWT token is expired: {}", e.getMessage()); + } catch (UnsupportedJwtException e) { + logger.error("JWT token is unsupported: {}", e.getMessage()); + } catch (IllegalArgumentException e) { + logger.error("JWT claims string is empty: {}", e.getMessage()); + } + + return false; + } +} \ No newline at end of file diff --git a/src/main/java/com/lms/security/services/UserDetailsImpl.java b/src/main/java/com/lms/security/services/UserDetailsImpl.java new file mode 100644 index 0000000..63ce8d0 --- /dev/null +++ b/src/main/java/com/lms/security/services/UserDetailsImpl.java @@ -0,0 +1,97 @@ +package com.lms.security.services; + +import com.lms.entity.User; +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; + +public class UserDetailsImpl implements UserDetails { + private static final long serialVersionUID = 1L; + + private Long id; + + private String username; + + private String email; + + private String password; + + private Collection authorities; + + public UserDetailsImpl(Long id, String username, String email, String password, + Collection authorities) { + this.id = id; + this.username = username; + this.email = email; + this.password = password; + this.authorities = authorities; + } + + public static UserDetailsImpl build(User user) { + GrantedAuthority authority = new SimpleGrantedAuthority("ROLE_" + user.getRole().name()); + + return new UserDetailsImpl( + user.getId(), + user.getUsername(), + user.getEmail(), + user.getPassword(), + List.of(authority)); + } + + @Override + public Collection getAuthorities() { + return authorities; + } + + public Long getId() { + return id; + } + + public String getEmail() { + return email; + } + + @Override + public String getPassword() { + return password; + } + + @Override + public String getUsername() { + return username; + } + + @Override + public boolean isAccountNonExpired() { + return true; + } + + @Override + public boolean isAccountNonLocked() { + return true; + } + + @Override + public boolean isCredentialsNonExpired() { + return true; + } + + @Override + public boolean isEnabled() { + return true; + } + + @Override + public boolean equals(Object o) { + if (this == o) + return true; + if (o == null || getClass() != o.getClass()) + return false; + UserDetailsImpl user = (UserDetailsImpl) o; + return Objects.equals(id, user.id); + } +} \ No newline at end of file diff --git a/src/main/java/com/lms/security/services/UserDetailsServiceImpl.java b/src/main/java/com/lms/security/services/UserDetailsServiceImpl.java new file mode 100644 index 0000000..d94c3fb --- /dev/null +++ b/src/main/java/com/lms/security/services/UserDetailsServiceImpl.java @@ -0,0 +1,26 @@ +package com.lms.security.services; + +import com.lms.entity.User; +import com.lms.repository.UserRepository; +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 org.springframework.transaction.annotation.Transactional; + +@Service +public class UserDetailsServiceImpl implements UserDetailsService { + @Autowired + UserRepository userRepository; + + @Override + @Transactional + public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { + User user = userRepository.findByUsername(username) + .orElse(userRepository.findByEmail(username) + .orElseThrow(() -> new UsernameNotFoundException("User Not Found with username: " + username))); + + return UserDetailsImpl.build(user); + } +} \ No newline at end of file diff --git a/src/main/java/com/lms/service/CourseService.java b/src/main/java/com/lms/service/CourseService.java new file mode 100644 index 0000000..f11711b --- /dev/null +++ b/src/main/java/com/lms/service/CourseService.java @@ -0,0 +1,43 @@ +package com.lms.service; + +import com.lms.entity.Course; +import com.lms.entity.Student; +import java.util.List; +import java.util.Optional; + +public interface CourseService { + + Course saveCourse(Course course); + + Optional findById(Long id); + + Optional findByCourseCode(String courseCode); + + List findAllCourses(); + + List findByInstructorId(Long instructorId); + + List findByStatus(Course.CourseStatus status); + + List findByCourseNameOrDescriptionContaining(String searchTerm); + + List findByStudentId(Long studentId); + + List findAvailableCourses(); + + Course updateCourse(Course course); + + void deleteCourse(Long id); + + void updateCourseStatus(Long courseId, Course.CourseStatus status); + + List getCourseStudents(Long courseId); + + int getCurrentEnrollment(Long courseId); + + boolean isCourseEnrollmentFull(Long courseId); + + boolean existsByCourseCode(String courseCode); + + Long countByStatus(Course.CourseStatus status); +} \ No newline at end of file diff --git a/src/main/java/com/lms/service/EnrollmentService.java b/src/main/java/com/lms/service/EnrollmentService.java new file mode 100644 index 0000000..526e3b3 --- /dev/null +++ b/src/main/java/com/lms/service/EnrollmentService.java @@ -0,0 +1,41 @@ +package com.lms.service; + +import com.lms.entity.Enrollment; +import java.time.LocalDateTime; +import java.util.List; +import java.util.Optional; + +public interface EnrollmentService { + + Enrollment saveEnrollment(Enrollment enrollment); + + Optional findById(Long id); + + List findByStudentId(Long studentId); + + List findByCourseId(Long courseId); + + List findByStatus(Enrollment.EnrollmentStatus status); + + Optional findByStudentIdAndCourseId(Long studentId, Long courseId); + + List findByStudentIdAndStatus(Long studentId, Enrollment.EnrollmentStatus status); + + List findByCourseIdAndStatus(Long courseId, Enrollment.EnrollmentStatus status); + + List findByEnrollmentDateBetween(LocalDateTime startDate, LocalDateTime endDate); + + Enrollment updateEnrollment(Enrollment enrollment); + + void deleteEnrollment(Long id); + + void updateEnrollmentStatus(Long enrollmentId, Enrollment.EnrollmentStatus status); + + void updateEnrollmentGrade(Long enrollmentId, Double grade); + + void completeEnrollment(Long enrollmentId, Double grade); + + Double getAverageGradeByCourseId(Long courseId); + + Long countByStatus(Enrollment.EnrollmentStatus status); +} \ No newline at end of file diff --git a/src/main/java/com/lms/service/InstructorService.java b/src/main/java/com/lms/service/InstructorService.java new file mode 100644 index 0000000..b3aab82 --- /dev/null +++ b/src/main/java/com/lms/service/InstructorService.java @@ -0,0 +1,39 @@ +package com.lms.service; + +import com.lms.entity.Course; +import com.lms.entity.Instructor; +import java.util.List; +import java.util.Optional; + +public interface InstructorService { + + Instructor saveInstructor(Instructor instructor); + + Optional findById(Long id); + + Optional findByEmployeeId(String employeeId); + + Optional findByEmail(String email); + + List findAllInstructors(); + + List findByDepartment(String department); + + List findByNameContaining(String name); + + List findBySpecializationContaining(String specialization); + + Instructor updateInstructor(Instructor instructor); + + void deleteInstructor(Long id); + + void assignCourseToInstructor(Long instructorId, Long courseId); + + void removeCourseFromInstructor(Long instructorId, Long courseId); + + List getInstructorCourses(Long instructorId); + + boolean existsByEmployeeId(String employeeId); + + Long countActiveInstructors(); +} \ No newline at end of file diff --git a/src/main/java/com/lms/service/StudentService.java b/src/main/java/com/lms/service/StudentService.java new file mode 100644 index 0000000..c4699ee --- /dev/null +++ b/src/main/java/com/lms/service/StudentService.java @@ -0,0 +1,37 @@ +package com.lms.service; + +import com.lms.entity.Course; +import com.lms.entity.Student; +import java.util.List; +import java.util.Optional; + +public interface StudentService { + + Student saveStudent(Student student); + + Optional findById(Long id); + + Optional findByStudentId(String studentId); + + Optional findByEmail(String email); + + List findAllStudents(); + + List findByNameContaining(String name); + + List findByCourseId(Long courseId); + + Student updateStudent(Student student); + + void deleteStudent(Long id); + + void enrollStudentInCourse(Long studentId, Long courseId); + + void unenrollStudentFromCourse(Long studentId, Long courseId); + + List getStudentCourses(Long studentId); + + boolean existsByStudentId(String studentId); + + Long countActiveStudents(); +} \ No newline at end of file diff --git a/src/main/java/com/lms/service/UserService.java b/src/main/java/com/lms/service/UserService.java new file mode 100644 index 0000000..00ea322 --- /dev/null +++ b/src/main/java/com/lms/service/UserService.java @@ -0,0 +1,32 @@ +package com.lms.service; + +import com.lms.entity.User; +import java.util.List; +import java.util.Optional; + +public interface UserService { + + User saveUser(User user); + + Optional findById(Long id); + + Optional findByUsername(String username); + + Optional findByEmail(String email); + + Optional findByUsernameOrEmail(String usernameOrEmail); + + List findAllUsers(); + + User updateUser(User user); + + void deleteUser(Long id); + + void deactivateUser(Long id); + + void activateUser(Long id); + + boolean existsByUsername(String username); + + boolean existsByEmail(String email); +} \ No newline at end of file diff --git a/src/main/java/com/lms/service/impl/CourseServiceImpl.java b/src/main/java/com/lms/service/impl/CourseServiceImpl.java new file mode 100644 index 0000000..56118d5 --- /dev/null +++ b/src/main/java/com/lms/service/impl/CourseServiceImpl.java @@ -0,0 +1,132 @@ +package com.lms.service.impl; + +import com.lms.entity.Course; +import com.lms.entity.Student; +import com.lms.repository.CourseRepository; +import com.lms.service.CourseService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; +import java.util.Optional; +import java.util.stream.Collectors; + +@Service +@Transactional +public class CourseServiceImpl implements CourseService { + + @Autowired + private CourseRepository courseRepository; + + @Override + public Course saveCourse(Course course) { + return courseRepository.save(course); + } + + @Override + @Transactional(readOnly = true) + public Optional findById(Long id) { + return courseRepository.findById(id); + } + + @Override + @Transactional(readOnly = true) + public Optional findByCourseCode(String courseCode) { + return courseRepository.findByCourseCode(courseCode); + } + + @Override + @Transactional(readOnly = true) + public List findAllCourses() { + return courseRepository.findAll(); + } + + @Override + @Transactional(readOnly = true) + public List findByInstructorId(Long instructorId) { + return courseRepository.findByInstructorId(instructorId); + } + + @Override + @Transactional(readOnly = true) + public List findByStatus(Course.CourseStatus status) { + return courseRepository.findByStatus(status); + } + + @Override + @Transactional(readOnly = true) + public List findByCourseNameOrDescriptionContaining(String searchTerm) { + return courseRepository.findByCourseNameOrDescriptionContaining(searchTerm); + } + + @Override + @Transactional(readOnly = true) + public List findByStudentId(Long studentId) { + return courseRepository.findByStudentId(studentId); + } + + @Override + @Transactional(readOnly = true) + public List findAvailableCourses() { + return courseRepository.findAvailableCourses(); + } + + @Override + public Course updateCourse(Course course) { + return courseRepository.save(course); + } + + @Override + public void deleteCourse(Long id) { + courseRepository.deleteById(id); + } + + @Override + public void updateCourseStatus(Long courseId, Course.CourseStatus status) { + Optional courseOpt = courseRepository.findById(courseId); + if (courseOpt.isPresent()) { + Course course = courseOpt.get(); + course.setStatus(status); + courseRepository.save(course); + } else { + throw new RuntimeException("Course not found with id: " + courseId); + } + } + + @Override + @Transactional(readOnly = true) + public List getCourseStudents(Long courseId) { + Optional courseOpt = courseRepository.findById(courseId); + if (courseOpt.isPresent()) { + return courseOpt.get().getEnrolledStudents().stream().collect(Collectors.toList()); + } + return List.of(); + } + + @Override + @Transactional(readOnly = true) + public int getCurrentEnrollment(Long courseId) { + Optional courseOpt = courseRepository.findById(courseId); + return courseOpt.map(Course::getCurrentEnrollment).orElse(0); + } + + @Override + @Transactional(readOnly = true) + public boolean isCourseEnrollmentFull(Long courseId) { + Optional courseOpt = courseRepository.findById(courseId); + return courseOpt.map(Course::isEnrollmentFull).orElse(false); + } + + @Override + @Transactional(readOnly = true) + public boolean existsByCourseCode(String courseCode) { + return courseRepository.existsByCourseCode(courseCode); + } + + @Override + @Transactional(readOnly = true) + public Long countByStatus(Course.CourseStatus status) { + return courseRepository.countByStatus(status); + } +} \ No newline at end of file diff --git a/src/main/java/com/lms/service/impl/EnrollmentServiceImpl.java b/src/main/java/com/lms/service/impl/EnrollmentServiceImpl.java new file mode 100644 index 0000000..5997281 --- /dev/null +++ b/src/main/java/com/lms/service/impl/EnrollmentServiceImpl.java @@ -0,0 +1,133 @@ +package com.lms.service.impl; + +import com.lms.entity.Enrollment; +import com.lms.repository.EnrollmentRepository; +import com.lms.service.EnrollmentService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.time.LocalDateTime; +import java.util.List; +import java.util.Optional; + +@Service +@Transactional +public class EnrollmentServiceImpl implements EnrollmentService { + + @Autowired + private EnrollmentRepository enrollmentRepository; + + @Override + public Enrollment saveEnrollment(Enrollment enrollment) { + return enrollmentRepository.save(enrollment); + } + + @Override + @Transactional(readOnly = true) + public Optional findById(Long id) { + return enrollmentRepository.findById(id); + } + + @Override + @Transactional(readOnly = true) + public List findByStudentId(Long studentId) { + return enrollmentRepository.findByStudentId(studentId); + } + + @Override + @Transactional(readOnly = true) + public List findByCourseId(Long courseId) { + return enrollmentRepository.findByCourseId(courseId); + } + + @Override + @Transactional(readOnly = true) + public List findByStatus(Enrollment.EnrollmentStatus status) { + return enrollmentRepository.findByStatus(status); + } + + @Override + @Transactional(readOnly = true) + public Optional findByStudentIdAndCourseId(Long studentId, Long courseId) { + return enrollmentRepository.findByStudentIdAndCourseId(studentId, courseId); + } + + @Override + @Transactional(readOnly = true) + public List findByStudentIdAndStatus(Long studentId, Enrollment.EnrollmentStatus status) { + return enrollmentRepository.findByStudentIdAndStatus(studentId, status); + } + + @Override + @Transactional(readOnly = true) + public List findByCourseIdAndStatus(Long courseId, Enrollment.EnrollmentStatus status) { + return enrollmentRepository.findByCourseIdAndStatus(courseId, status); + } + + @Override + @Transactional(readOnly = true) + public List findByEnrollmentDateBetween(LocalDateTime startDate, LocalDateTime endDate) { + return enrollmentRepository.findByEnrollmentDateBetween(startDate, endDate); + } + + @Override + public Enrollment updateEnrollment(Enrollment enrollment) { + return enrollmentRepository.save(enrollment); + } + + @Override + public void deleteEnrollment(Long id) { + enrollmentRepository.deleteById(id); + } + + @Override + public void updateEnrollmentStatus(Long enrollmentId, Enrollment.EnrollmentStatus status) { + Optional enrollmentOpt = enrollmentRepository.findById(enrollmentId); + if (enrollmentOpt.isPresent()) { + Enrollment enrollment = enrollmentOpt.get(); + enrollment.setStatus(status); + enrollmentRepository.save(enrollment); + } else { + throw new RuntimeException("Enrollment not found with id: " + enrollmentId); + } + } + + @Override + public void updateEnrollmentGrade(Long enrollmentId, Double grade) { + Optional enrollmentOpt = enrollmentRepository.findById(enrollmentId); + if (enrollmentOpt.isPresent()) { + Enrollment enrollment = enrollmentOpt.get(); + enrollment.setGrade(grade); + enrollmentRepository.save(enrollment); + } else { + throw new RuntimeException("Enrollment not found with id: " + enrollmentId); + } + } + + @Override + public void completeEnrollment(Long enrollmentId, Double grade) { + Optional enrollmentOpt = enrollmentRepository.findById(enrollmentId); + if (enrollmentOpt.isPresent()) { + Enrollment enrollment = enrollmentOpt.get(); + enrollment.setStatus(Enrollment.EnrollmentStatus.COMPLETED); + enrollment.setGrade(grade); + enrollment.setCompletionDate(LocalDateTime.now()); + enrollmentRepository.save(enrollment); + } else { + throw new RuntimeException("Enrollment not found with id: " + enrollmentId); + } + } + + @Override + @Transactional(readOnly = true) + public Double getAverageGradeByCourseId(Long courseId) { + return enrollmentRepository.findAverageGradeByCourseId(courseId); + } + + @Override + @Transactional(readOnly = true) + public Long countByStatus(Enrollment.EnrollmentStatus status) { + return enrollmentRepository.countByStatus(status); + } +} \ No newline at end of file diff --git a/src/main/java/com/lms/service/impl/InstructorServiceImpl.java b/src/main/java/com/lms/service/impl/InstructorServiceImpl.java new file mode 100644 index 0000000..1ee2751 --- /dev/null +++ b/src/main/java/com/lms/service/impl/InstructorServiceImpl.java @@ -0,0 +1,142 @@ +package com.lms.service.impl; + +import com.lms.entity.Course; +import com.lms.entity.Instructor; +import com.lms.repository.CourseRepository; +import com.lms.repository.InstructorRepository; +import com.lms.service.InstructorService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; +import java.util.Optional; +import java.util.stream.Collectors; + +@Service +@Transactional +public class InstructorServiceImpl implements InstructorService { + + @Autowired + private InstructorRepository instructorRepository; + + @Autowired + private CourseRepository courseRepository; + + @Autowired + private PasswordEncoder passwordEncoder; + + @Override + public Instructor saveInstructor(Instructor instructor) { + // Encode password before saving + instructor.setPassword(passwordEncoder.encode(instructor.getPassword())); + return instructorRepository.save(instructor); + } + + @Override + @Transactional(readOnly = true) + public Optional findById(Long id) { + return instructorRepository.findById(id); + } + + @Override + @Transactional(readOnly = true) + public Optional findByEmployeeId(String employeeId) { + return instructorRepository.findByEmployeeId(employeeId); + } + + @Override + @Transactional(readOnly = true) + public Optional findByEmail(String email) { + return instructorRepository.findByEmail(email); + } + + @Override + @Transactional(readOnly = true) + public List findAllInstructors() { + return instructorRepository.findAll(); + } + + @Override + @Transactional(readOnly = true) + public List findByDepartment(String department) { + return instructorRepository.findByDepartment(department); + } + + @Override + @Transactional(readOnly = true) + public List findByNameContaining(String name) { + return instructorRepository.findByNameContaining(name); + } + + @Override + @Transactional(readOnly = true) + public List findBySpecializationContaining(String specialization) { + return instructorRepository.findBySpecializationContaining(specialization); + } + + @Override + public Instructor updateInstructor(Instructor instructor) { + return instructorRepository.save(instructor); + } + + @Override + public void deleteInstructor(Long id) { + instructorRepository.deleteById(id); + } + + @Override + public void assignCourseToInstructor(Long instructorId, Long courseId) { + Optional instructorOpt = instructorRepository.findById(instructorId); + Optional courseOpt = courseRepository.findById(courseId); + + if (instructorOpt.isPresent() && courseOpt.isPresent()) { + Instructor instructor = instructorOpt.get(); + Course course = courseOpt.get(); + + instructor.addCourse(course); + instructorRepository.save(instructor); + } else { + throw new RuntimeException("Instructor or Course not found"); + } + } + + @Override + public void removeCourseFromInstructor(Long instructorId, Long courseId) { + Optional instructorOpt = instructorRepository.findById(instructorId); + Optional courseOpt = courseRepository.findById(courseId); + + if (instructorOpt.isPresent() && courseOpt.isPresent()) { + Instructor instructor = instructorOpt.get(); + Course course = courseOpt.get(); + + instructor.removeCourse(course); + instructorRepository.save(instructor); + } else { + throw new RuntimeException("Instructor or Course not found"); + } + } + + @Override + @Transactional(readOnly = true) + public List getInstructorCourses(Long instructorId) { + Optional instructorOpt = instructorRepository.findById(instructorId); + if (instructorOpt.isPresent()) { + return instructorOpt.get().getCourses().stream().collect(Collectors.toList()); + } + return List.of(); + } + + @Override + @Transactional(readOnly = true) + public boolean existsByEmployeeId(String employeeId) { + return instructorRepository.existsByEmployeeId(employeeId); + } + + @Override + @Transactional(readOnly = true) + public Long countActiveInstructors() { + return instructorRepository.countActiveInstructors(); + } +} \ No newline at end of file diff --git a/src/main/java/com/lms/service/impl/StudentServiceImpl.java b/src/main/java/com/lms/service/impl/StudentServiceImpl.java new file mode 100644 index 0000000..6c26d98 --- /dev/null +++ b/src/main/java/com/lms/service/impl/StudentServiceImpl.java @@ -0,0 +1,172 @@ +package com.lms.service.impl; + +import com.lms.entity.Course; +import com.lms.entity.Enrollment; +import com.lms.entity.Student; +import com.lms.repository.CourseRepository; +import com.lms.repository.EnrollmentRepository; +import com.lms.repository.StudentRepository; +import com.lms.service.StudentService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; +import java.util.Optional; +import java.util.stream.Collectors; + +@Service +@Transactional +public class StudentServiceImpl implements StudentService { + + @Autowired + private StudentRepository studentRepository; + + @Autowired + private CourseRepository courseRepository; + + @Autowired + private EnrollmentRepository enrollmentRepository; + + @Autowired + private PasswordEncoder passwordEncoder; + + @Override + public Student saveStudent(Student student) { + // Encode password before saving + student.setPassword(passwordEncoder.encode(student.getPassword())); + return studentRepository.save(student); + } + + @Override + @Transactional(readOnly = true) + public Optional findById(Long id) { + return studentRepository.findById(id); + } + + @Override + @Transactional(readOnly = true) + public Optional findByStudentId(String studentId) { + return studentRepository.findByStudentId(studentId); + } + + @Override + @Transactional(readOnly = true) + public Optional findByEmail(String email) { + return studentRepository.findByEmail(email); + } + + @Override + @Transactional(readOnly = true) + public List findAllStudents() { + return studentRepository.findAll(); + } + + @Override + @Transactional(readOnly = true) + public List findByNameContaining(String name) { + return studentRepository.findByNameContaining(name); + } + + @Override + @Transactional(readOnly = true) + public List findByCourseId(Long courseId) { + return studentRepository.findByCourseId(courseId); + } + + @Override + public Student updateStudent(Student student) { + return studentRepository.save(student); + } + + @Override + public void deleteStudent(Long id) { + studentRepository.deleteById(id); + } + + @Override + public void enrollStudentInCourse(Long studentId, Long courseId) { + Optional studentOpt = studentRepository.findById(studentId); + Optional courseOpt = courseRepository.findById(courseId); + + if (studentOpt.isPresent() && courseOpt.isPresent()) { + Student student = studentOpt.get(); + Course course = courseOpt.get(); + + // Check if already enrolled + Optional existingEnrollment = enrollmentRepository + .findByStudentIdAndCourseId(studentId, courseId); + + if (existingEnrollment.isEmpty()) { + // Check course capacity + if (!course.isEnrollmentFull()) { + // Create enrollment record + Enrollment enrollment = new Enrollment(student, course); + enrollmentRepository.save(enrollment); + + // Update many-to-many relationship + student.enrollInCourse(course); + studentRepository.save(student); + } else { + throw new RuntimeException("Course enrollment is full"); + } + } else { + throw new RuntimeException("Student is already enrolled in this course"); + } + } else { + throw new RuntimeException("Student or Course not found"); + } + } + + @Override + public void unenrollStudentFromCourse(Long studentId, Long courseId) { + Optional studentOpt = studentRepository.findById(studentId); + Optional courseOpt = courseRepository.findById(courseId); + + if (studentOpt.isPresent() && courseOpt.isPresent()) { + Student student = studentOpt.get(); + Course course = courseOpt.get(); + + // Find and update enrollment record + Optional enrollmentOpt = enrollmentRepository + .findByStudentIdAndCourseId(studentId, courseId); + + if (enrollmentOpt.isPresent()) { + Enrollment enrollment = enrollmentOpt.get(); + enrollment.setStatus(Enrollment.EnrollmentStatus.DROPPED); + enrollmentRepository.save(enrollment); + + // Update many-to-many relationship + student.unenrollFromCourse(course); + studentRepository.save(student); + } else { + throw new RuntimeException("Student is not enrolled in this course"); + } + } else { + throw new RuntimeException("Student or Course not found"); + } + } + + @Override + @Transactional(readOnly = true) + public List getStudentCourses(Long studentId) { + Optional studentOpt = studentRepository.findById(studentId); + if (studentOpt.isPresent()) { + return studentOpt.get().getEnrolledCourses().stream().collect(Collectors.toList()); + } + return List.of(); + } + + @Override + @Transactional(readOnly = true) + public boolean existsByStudentId(String studentId) { + return studentRepository.existsByStudentId(studentId); + } + + @Override + @Transactional(readOnly = true) + public Long countActiveStudents() { + return studentRepository.countActiveStudents(); + } +} \ No newline at end of file diff --git a/src/main/java/com/lms/service/impl/UserServiceImpl.java b/src/main/java/com/lms/service/impl/UserServiceImpl.java new file mode 100644 index 0000000..cd65532 --- /dev/null +++ b/src/main/java/com/lms/service/impl/UserServiceImpl.java @@ -0,0 +1,102 @@ +package com.lms.service.impl; + +import com.lms.entity.User; +import com.lms.repository.UserRepository; +import com.lms.service.UserService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; +import java.util.Optional; + +@Service +@Transactional +public class UserServiceImpl implements UserService { + + @Autowired + private UserRepository userRepository; + + @Autowired + private PasswordEncoder passwordEncoder; + + @Override + public User saveUser(User user) { + // Encode password before saving + user.setPassword(passwordEncoder.encode(user.getPassword())); + return userRepository.save(user); + } + + @Override + @Transactional(readOnly = true) + public Optional findById(Long id) { + return userRepository.findById(id); + } + + @Override + @Transactional(readOnly = true) + public Optional findByUsername(String username) { + return userRepository.findByUsername(username); + } + + @Override + @Transactional(readOnly = true) + public Optional findByEmail(String email) { + return userRepository.findByEmail(email); + } + + @Override + @Transactional(readOnly = true) + public Optional findByUsernameOrEmail(String usernameOrEmail) { + return userRepository.findByUsernameOrEmail(usernameOrEmail); + } + + @Override + @Transactional(readOnly = true) + public List findAllUsers() { + return userRepository.findAll(); + } + + @Override + public User updateUser(User user) { + return userRepository.save(user); + } + + @Override + public void deleteUser(Long id) { + userRepository.deleteById(id); + } + + @Override + public void deactivateUser(Long id) { + Optional userOptional = userRepository.findById(id); + if (userOptional.isPresent()) { + User user = userOptional.get(); + user.setIsActive(false); + userRepository.save(user); + } + } + + @Override + public void activateUser(Long id) { + Optional userOptional = userRepository.findById(id); + if (userOptional.isPresent()) { + User user = userOptional.get(); + user.setIsActive(true); + userRepository.save(user); + } + } + + @Override + @Transactional(readOnly = true) + public boolean existsByUsername(String username) { + return userRepository.existsByUsername(username); + } + + @Override + @Transactional(readOnly = true) + public boolean existsByEmail(String email) { + return userRepository.existsByEmail(email); + } +} \ No newline at end of file diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 548edc5..405873b 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -1,24 +1,24 @@ -spring.application.name=OnlineLearningSystem +# Server Configuration +server.port=8080 -spring.data.mongodb.uri=mongodb://localhost:27017/online - -role.admin="ADMIN"; -role.student="STUDENT" -role.instructor="INSTUCTOR" - - -# Use H2 in-memory database -spring.datasource.url=jdbc:h2:mem:testdb -spring.datasource.driver-class-name=org.h2.Driver +# Database Configuration (H2 for development) +spring.datasource.url=jdbc:h2:mem:lmsdb +spring.datasource.driverClassName=org.h2.Driver spring.datasource.username=sa -spring.datasource.password= - -# Hibernate DDL (optional: create / update / validate / none) -spring.jpa.hibernate.ddl-auto=update +spring.datasource.password=password +spring.h2.console.enabled=true +spring.h2.console.path=/h2-console -# Show SQL queries +# JPA Configuration +spring.jpa.database-platform=org.hibernate.dialect.H2Dialect +spring.jpa.hibernate.ddl-auto=create-drop spring.jpa.show-sql=true +spring.jpa.properties.hibernate.format_sql=true -# Enable H2 Console in browser -spring.h2.console.enabled=true -spring.h2.console.path=/h2-console +# JWT Configuration +jwt.secret=bGVhcm5pbmdNYW5hZ2VtZW50U3lzdGVtU2VjcmV0S2V5Rm9ySlNPTldlYlRva2VuU2lnbmluZw== +jwt.expiration=86400000 + +# Logging +logging.level.com.lms=DEBUG +logging.level.org.springframework.security=DEBUG