Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 0 additions & 3 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,6 @@ name: CI

on:
push:
branches:
- main
- phase2

pull_request:
branches:
Expand Down
13 changes: 9 additions & 4 deletions backend/api-gateway/src/main/resources/application.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,15 +17,20 @@ spring:
predicates:
- Path=/api/v1/students/**

- id: enrollment-service-courses
- id: enrollment-service
uri: ${ENROLLMENT_SERVICE_URL:http://localhost:8082}
predicates:
- Path=/api/v1/enrollments/**

- id: course-service
uri: ${COURSE_SERVICE_URL:http://localhost:8083}
predicates:
- Path=/api/v1/courses/**

- id: enrollment-service-enrollments
uri: ${ENROLLMENT_SERVICE_URL:http://localhost:8082}
- id: grade-service
uri: ${GRADE_SERVICE_URL:http://localhost:8084}
predicates:
- Path=/api/v1/enrollments/**
- Path=/api/v1/grades/**

globalcors:
cors-configurations:
Expand Down
13 changes: 13 additions & 0 deletions backend/course-service/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
target/
*.class
*.jar
!.mvn/wrapper/maven-wrapper.jar
.idea/
*.iml
.vscode/
.DS_Store
*.log
HELP.md
.mvn/
mvnw
mvnw.cmd
18 changes: 18 additions & 0 deletions backend/course-service/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# ---- Build stage ----
FROM maven:3.9-eclipse-temurin-17 AS build
WORKDIR /app

COPY common-lib ./common-lib
RUN cd common-lib && mvn -B clean install -DskipTests

COPY course-service/pom.xml ./course-service/pom.xml
RUN cd course-service && mvn -B dependency:go-offline
COPY course-service/src ./course-service/src
RUN cd course-service && mvn -B clean package -DskipTests

# ---- Run stage ----
FROM eclipse-temurin:17-jre-alpine
WORKDIR /app
COPY --from=build /app/course-service/target/*.jar app.jar
EXPOSE 8083
ENTRYPOINT ["java", "-jar", "app.jar"]
97 changes: 97 additions & 0 deletions backend/course-service/pom.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>

<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.3.4</version>
<relativePath/>
</parent>

<groupId>com.example</groupId>
<artifactId>course-service</artifactId>
<version>1.0.0</version>
<name>course-service</name>
<description>Course catalog microservice - capacity, credits, semester, instructor, department, status</description>

<properties>
<java.version>17</java.version>
</properties>

<dependencies>
<dependency>
<groupId>com.example</groupId>
<artifactId>common-lib</artifactId>
<version>1.0.0</version>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>

<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
<version>2.6.0</version>
</dependency>

<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>test</scope>
</dependency>
</dependencies>

<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<excludes>
<exclude>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
</plugins>
</build>

</project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package com.example.courseservice;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

/**
* Entry point for the Course Service. Owns the course catalog: capacity,
* credits, semester, instructor, department, and status. Previously this
* lived inside enrollment-service; it's split out here so course data has
* its own bounded context and database (course_db), with
* enrollment-service calling it over REST via CourseClient instead of
* owning course rows directly.
*/
@SpringBootApplication
public class CourseServiceApplication {

public static void main(String[] args) {
SpringApplication.run(CourseServiceApplication.class, args);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package com.example.courseservice.config;

import com.example.common.security.JwtTokenValidator;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/** Kept separate from SecurityConfig to avoid a circular bean dependency - see student-service's JwtConfig for the full explanation. */
@Configuration
public class JwtConfig {

@Value("${jwt.secret}")
private String jwtSecret;

@Bean
public JwtTokenValidator jwtTokenValidator() {
return new JwtTokenValidator(jwtSecret);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
package com.example.courseservice.config;

import com.example.courseservice.security.JwtAuthenticationFilter;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;

import java.util.List;

@Configuration
@EnableWebSecurity
@EnableMethodSecurity
@RequiredArgsConstructor
public class SecurityConfig {

private final JwtAuthenticationFilter jwtAuthenticationFilter;

@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.csrf(csrf -> csrf.disable())
.cors(cors -> cors.configurationSource(corsConfigurationSource()))
.sessionManagement(sm -> sm.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.authorizeHttpRequests(auth -> auth
.requestMatchers("/actuator/health", "/v3/api-docs/**", "/swagger-ui/**", "/swagger-ui.html").permitAll()
.anyRequest().authenticated()
)
.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);

return http.build();
}

@Bean
public CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration configuration = new CorsConfiguration();
configuration.setAllowedOriginPatterns(List.of("*"));
configuration.setAllowedMethods(List.of("GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"));
configuration.setAllowedHeaders(List.of("*"));
configuration.setAllowCredentials(true);

UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", configuration);
return source;
}
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
package com.example.enrollmentservice.controller;
package com.example.courseservice.controller;

import com.example.enrollmentservice.dto.CourseDTO;
import com.example.enrollmentservice.service.CourseService;
import com.example.courseservice.dto.CourseDTO;
import com.example.courseservice.service.CourseService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;
Expand All @@ -12,43 +14,56 @@
import java.util.List;

/**
* Access rules: any authenticated user (ADMIN or STUDENT) can browse
* courses - students need this to see what's available to enroll in.
* Only ADMIN can create, update, or delete a course.
* Any authenticated user (ADMIN or STUDENT) can browse courses - students
* need this to see what's available to enroll in. Only ADMIN can create,
* update, or delete a course.
*/
@RestController
@RequestMapping("/api/v1/courses")
@RequiredArgsConstructor
@Tag(name = "Courses", description = "Course catalog: capacity, credits, semester, instructor, department, status")
public class CourseController {

private final CourseService courseService;

@PostMapping
@PreAuthorize("hasRole('ADMIN')")
@Operation(summary = "Create a course (ADMIN only)")
public ResponseEntity<CourseDTO> createCourse(@Valid @RequestBody CourseDTO courseDTO) {
return new ResponseEntity<>(courseService.createCourse(courseDTO), HttpStatus.CREATED);
}

@GetMapping("/{id}")
@Operation(summary = "Get a course by id")
public ResponseEntity<CourseDTO> getCourseById(@PathVariable Long id) {
return ResponseEntity.ok(courseService.getCourseById(id));
}

@GetMapping
@Operation(summary = "List all courses")
public ResponseEntity<List<CourseDTO>> getAllCourses() {
return ResponseEntity.ok(courseService.getAllCourses());
}

@PutMapping("/{id}")
@PreAuthorize("hasRole('ADMIN')")
@Operation(summary = "Update a course (ADMIN only)")
public ResponseEntity<CourseDTO> updateCourse(@PathVariable Long id, @RequestBody CourseDTO courseDTO) {
return ResponseEntity.ok(courseService.updateCourse(id, courseDTO));
}

@DeleteMapping("/{id}")
@PreAuthorize("hasRole('ADMIN')")
@Operation(summary = "Delete a course (ADMIN only)")
public ResponseEntity<Void> deleteCourse(@PathVariable Long id) {
courseService.deleteCourse(id);
return ResponseEntity.noContent().build();
}

/** Internal check used by enrollment-service before creating an enrollment. */
@GetMapping("/{id}/exists")
@Operation(summary = "Lightweight existence check (used by enrollment-service)")
public ResponseEntity<Boolean> existsById(@PathVariable Long id) {
return ResponseEntity.ok(courseService.existsById(id));
}
}
Loading
Loading