From a0d7f494abe00d2d3687033447688f1d1ff86bb3 Mon Sep 17 00:00:00 2001 From: MeOwOverlord Date: Mon, 8 Sep 2025 08:22:25 +0200 Subject: [PATCH 1/6] first approach --- pom.xml | 49 +++++++++++ .../userservice/config/SecurityConfig.java | 47 ++++++++++ .../controllers/ExamController.java | 37 ++++++++ .../controllers/GlobalExceptionHandler.java | 39 +++++++++ .../controllers/NotFoundException.java | 5 ++ .../userservice/dto/CreateExamRequest.java | 30 +++++++ .../com/ase/userservice/dto/ExamResponse.java | 18 ++++ .../com/ase/userservice/entities/Exam.java | 87 +++++++++++++++++++ .../repositories/ExamRepository.java | 13 +++ .../ase/userservice/services/ExamService.java | 71 +++++++++++++++ src/main/resources/application-prod.yaml | 14 +++ src/main/resources/application.yaml | 23 ++--- .../com/ase/userservice/ApplicationTests.java | 11 ++- .../com/ase/userservice/ExamController.java | 42 +++++++++ 14 files changed, 469 insertions(+), 17 deletions(-) create mode 100644 src/main/java/com/ase/userservice/config/SecurityConfig.java create mode 100644 src/main/java/com/ase/userservice/controllers/ExamController.java create mode 100644 src/main/java/com/ase/userservice/controllers/GlobalExceptionHandler.java create mode 100644 src/main/java/com/ase/userservice/controllers/NotFoundException.java create mode 100644 src/main/java/com/ase/userservice/dto/CreateExamRequest.java create mode 100644 src/main/java/com/ase/userservice/dto/ExamResponse.java create mode 100644 src/main/java/com/ase/userservice/entities/Exam.java create mode 100644 src/main/java/com/ase/userservice/repositories/ExamRepository.java create mode 100644 src/main/java/com/ase/userservice/services/ExamService.java create mode 100644 src/main/resources/application-prod.yaml create mode 100644 src/test/java/com/ase/userservice/ExamController.java diff --git a/pom.xml b/pom.xml index e26b2f8a..f03c24f0 100644 --- a/pom.xml +++ b/pom.xml @@ -64,5 +64,54 @@ h2 runtime + + + 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-oauth2-resource-server + + + + + org.springframework.boot + spring-boot-starter-validation + + + + + com.h2database + h2 + runtime + + + org.postgresql + postgresql + runtime + + + + + org.projectlombok + lombok + true + + + org.springframework.boot + spring-boot-starter-test + test + diff --git a/src/main/java/com/ase/userservice/config/SecurityConfig.java b/src/main/java/com/ase/userservice/config/SecurityConfig.java new file mode 100644 index 00000000..0a045f70 --- /dev/null +++ b/src/main/java/com/ase/userservice/config/SecurityConfig.java @@ -0,0 +1,47 @@ +package com.ase.userservice.config; + +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.core.GrantedAuthority; +import org.springframework.security.oauth2.jwt.Jwt; +import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationConverter; +import org.springframework.security.web.SecurityFilterChain; + +import java.util.Collection; +import java.util.List; +import java.util.stream.Collectors; + +@Configuration +@EnableMethodSecurity +public class SecurityConfig { + + @Bean + SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { + http + .csrf(csrf -> csrf.disable()) + .authorizeHttpRequests(auth -> auth + .requestMatchers("/actuator/**", "/v3/api-docs/**", "/swagger-ui/**").permitAll() + .anyRequest().authenticated()) + .oauth2ResourceServer(oauth -> oauth.jwt(jwt -> jwt.jwtAuthenticationConverter(rolesConverter()))); + return http.build(); + } + + /** Mappt das JWT-Claim 'roles' -> Spring GrantedAuthorities. */ + @Bean + public JwtAuthenticationConverter rolesConverter() { + JwtAuthenticationConverter converter = new JwtAuthenticationConverter(); + converter.setJwtGrantedAuthoritiesConverter(this::convertRolesClaim); + return converter; + } + + /** Liefert exakt Collection (kein Wildcard), damit der Converter-Typ passt. */ + private Collection convertRolesClaim(Jwt jwt) { + List roles = jwt.getClaimAsStringList("roles"); + if (roles == null) roles = List.of(); + return roles.stream() + .map(r -> (GrantedAuthority) new org.springframework.security.core.authority.SimpleGrantedAuthority(r)) + .collect(Collectors.toList()); + } +} diff --git a/src/main/java/com/ase/userservice/controllers/ExamController.java b/src/main/java/com/ase/userservice/controllers/ExamController.java new file mode 100644 index 00000000..9afe9d36 --- /dev/null +++ b/src/main/java/com/ase/userservice/controllers/ExamController.java @@ -0,0 +1,37 @@ +package com.ase.userservice.controllers; + +import com.ase.userservice.dto.CreateExamRequest; +import com.ase.userservice.dto.ExamResponse; +import com.ase.userservice.services.ExamService; +import jakarta.validation.Valid; +import org.springframework.http.ResponseEntity; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.util.UriComponentsBuilder; + +@RestController +@RequestMapping("/api/exams") +public class ExamController { + private final ExamService service; + + public ExamController(ExamService service) { + this.service = service; + } + + @PostMapping + @PreAuthorize("hasAuthority('exam:write') or hasAuthority('ROLE_ADMIN')") + public ResponseEntity createExam(@Valid @RequestBody CreateExamRequest req, + UriComponentsBuilder uri) { + ExamResponse created = service.create(req); + return ResponseEntity + .created(uri.path("/api/exams/{id}").buildAndExpand(created.id()).toUri()) + .body(created); + } + + @PutMapping("/{id}") + @PreAuthorize("hasAuthority('exam:write') or hasAuthority('ROLE_ADMIN')") + public ResponseEntity updateExam(@PathVariable Long id, + @Valid @RequestBody CreateExamRequest req) { + return ResponseEntity.ok(service.update(id, req)); + } +} diff --git a/src/main/java/com/ase/userservice/controllers/GlobalExceptionHandler.java b/src/main/java/com/ase/userservice/controllers/GlobalExceptionHandler.java new file mode 100644 index 00000000..2f1bb6c6 --- /dev/null +++ b/src/main/java/com/ase/userservice/controllers/GlobalExceptionHandler.java @@ -0,0 +1,39 @@ +package com.ase.userservice.controllers; + +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.MethodArgumentNotValidException; +import org.springframework.web.bind.annotation.ControllerAdvice; +import org.springframework.web.bind.annotation.ExceptionHandler; + +import java.util.Map; +import java.util.stream.Collectors; + +@ControllerAdvice +public class GlobalExceptionHandler { + @ExceptionHandler(MethodArgumentNotValidException.class) + public ResponseEntity handleValidation(MethodArgumentNotValidException ex) { + var errors = ex.getBindingResult().getFieldErrors().stream() + .collect(Collectors.toMap( + fe -> fe.getField(), + fe -> fe.getDefaultMessage(), + (a, b) -> a + )); + return ResponseEntity.badRequest().body(Map.of("message", "Validation failed", "errors", errors)); + } + + @ExceptionHandler(IllegalArgumentException.class) + public ResponseEntity handleIllegalArg(IllegalArgumentException ex) { + return ResponseEntity.badRequest().body(Map.of("message", ex.getMessage())); + } + + @ExceptionHandler(IllegalStateException.class) + public ResponseEntity handleIllegalState(IllegalStateException ex) { + return ResponseEntity.status(HttpStatus.CONFLICT).body(Map.of("message", ex.getMessage())); + } + + @ExceptionHandler(NotFoundException.class) + public ResponseEntity handleNotFound(NotFoundException ex) { + return ResponseEntity.status(HttpStatus.NOT_FOUND).body(Map.of("message", ex.getMessage())); + } +} diff --git a/src/main/java/com/ase/userservice/controllers/NotFoundException.java b/src/main/java/com/ase/userservice/controllers/NotFoundException.java new file mode 100644 index 00000000..38e6b311 --- /dev/null +++ b/src/main/java/com/ase/userservice/controllers/NotFoundException.java @@ -0,0 +1,5 @@ +package com.ase.userservice.controllers; + +public class NotFoundException extends RuntimeException { + public NotFoundException(String message) { super(message); } +} diff --git a/src/main/java/com/ase/userservice/dto/CreateExamRequest.java b/src/main/java/com/ase/userservice/dto/CreateExamRequest.java new file mode 100644 index 00000000..dfbf1008 --- /dev/null +++ b/src/main/java/com/ase/userservice/dto/CreateExamRequest.java @@ -0,0 +1,30 @@ +package com.ase.userservice.dto; + +import jakarta.validation.constraints.*; +import java.time.*; + +public record CreateExamRequest( + @NotBlank(message = "title is required") String title, + @NotBlank(message = "moduleCode is required") String moduleCode, + @NotNull LocalDate date, + @NotNull LocalTime startTime, + @NotNull LocalTime endTime, + @NotBlank String examiner, + String room, + @NotNull @Positive Integer capacity, + @PositiveOrZero Integer ects, + @NotNull LocalDate registrationDeadline, + @NotNull LocalDate deregistrationDeadline +) { + public void validateBusinessRules() { + if (endTime != null && startTime != null && !endTime.isAfter(startTime)) { + throw new IllegalArgumentException("endTime must be after startTime"); + } + if (registrationDeadline != null && date != null && registrationDeadline.isAfter(date)) { + throw new IllegalArgumentException("registrationDeadline must be on/before date"); + } + if (deregistrationDeadline != null && registrationDeadline != null && deregistrationDeadline.isBefore(registrationDeadline)) { + throw new IllegalArgumentException("deregistrationDeadline must be on/after registrationDeadline"); + } + } +} diff --git a/src/main/java/com/ase/userservice/dto/ExamResponse.java b/src/main/java/com/ase/userservice/dto/ExamResponse.java new file mode 100644 index 00000000..cc7219d4 --- /dev/null +++ b/src/main/java/com/ase/userservice/dto/ExamResponse.java @@ -0,0 +1,18 @@ +package com.ase.userservice.dto; + +import java.time.*; + +public record ExamResponse( + Long id, + String title, + String moduleCode, + LocalDate date, + LocalTime startTime, + LocalTime endTime, + String examiner, + String room, + Integer capacity, + Integer ects, + LocalDate registrationDeadline, + LocalDate deregistrationDeadline +) {} diff --git a/src/main/java/com/ase/userservice/entities/Exam.java b/src/main/java/com/ase/userservice/entities/Exam.java new file mode 100644 index 00000000..146e523f --- /dev/null +++ b/src/main/java/com/ase/userservice/entities/Exam.java @@ -0,0 +1,87 @@ +package com.ase.userservice.entities; + +import jakarta.persistence.*; +import java.time.*; + +@Entity +@Table(name = "exams") +public class Exam { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(nullable = false, length = 120) + private String title; + + @Column(nullable = false, length = 40) + private String moduleCode; + + @Column(nullable = false) + private LocalDate date; + + @Column(nullable = false) + private LocalTime startTime; + + @Column(nullable = false) + private LocalTime endTime; + + @Column(nullable = false, length = 80) + private String examiner; + + @Column(length = 80) + private String room; + + @Column(nullable = false) + private Integer capacity; + + @Column + private Integer ects; + + @Column(nullable = false) + private LocalDate registrationDeadline; + + @Column(nullable = false) + private LocalDate deregistrationDeadline; + + protected Exam() {} + + public Exam(String title, String moduleCode, LocalDate date, LocalTime startTime, LocalTime endTime, + String examiner, String room, Integer capacity, Integer ects, + LocalDate registrationDeadline, LocalDate deregistrationDeadline) { + this.title = title; + this.moduleCode = moduleCode; + this.date = date; + this.startTime = startTime; + this.endTime = endTime; + this.examiner = examiner; + this.room = room; + this.capacity = capacity; + this.ects = ects; + this.registrationDeadline = registrationDeadline; + this.deregistrationDeadline = deregistrationDeadline; + } + + public Long getId() { return id; } + public String getTitle() { return title; } + public void setTitle(String title) { this.title = title; } + public String getModuleCode() { return moduleCode; } + public void setModuleCode(String moduleCode) { this.moduleCode = moduleCode; } + public LocalDate getDate() { return date; } + public void setDate(LocalDate date) { this.date = date; } + public LocalTime getStartTime() { return startTime; } + public void setStartTime(LocalTime startTime) { this.startTime = startTime; } + public LocalTime getEndTime() { return endTime; } + public void setEndTime(LocalTime endTime) { this.endTime = endTime; } + public String getExaminer() { return examiner; } + public void setExaminer(String examiner) { this.examiner = examiner; } + public String getRoom() { return room; } + public void setRoom(String room) { this.room = room; } + public Integer getCapacity() { return capacity; } + public void setCapacity(Integer capacity) { this.capacity = capacity; } + public Integer getEcts() { return ects; } + public void setEcts(Integer ects) { this.ects = ects; } + public LocalDate getRegistrationDeadline() { return registrationDeadline; } + public void setRegistrationDeadline(LocalDate registrationDeadline) { this.registrationDeadline = registrationDeadline; } + public LocalDate getDeregistrationDeadline() { return deregistrationDeadline; } + public void setDeregistrationDeadline(LocalDate deregistrationDeadline) { this.deregistrationDeadline = deregistrationDeadline; } +} diff --git a/src/main/java/com/ase/userservice/repositories/ExamRepository.java b/src/main/java/com/ase/userservice/repositories/ExamRepository.java new file mode 100644 index 00000000..f448bc11 --- /dev/null +++ b/src/main/java/com/ase/userservice/repositories/ExamRepository.java @@ -0,0 +1,13 @@ +package com.ase.userservice.repositories; + +import com.ase.userservice.entities.Exam; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import java.time.LocalDate; + +@Repository +public interface ExamRepository extends JpaRepository { + boolean existsByTitleAndDate(String title, LocalDate date); + boolean existsByTitleAndDateAndIdNot(String title, LocalDate date, Long id); +} diff --git a/src/main/java/com/ase/userservice/services/ExamService.java b/src/main/java/com/ase/userservice/services/ExamService.java new file mode 100644 index 00000000..871d17b2 --- /dev/null +++ b/src/main/java/com/ase/userservice/services/ExamService.java @@ -0,0 +1,71 @@ +package com.ase.userservice.services; + +import com.ase.userservice.dto.CreateExamRequest; +import com.ase.userservice.dto.ExamResponse; +import com.ase.userservice.entities.Exam; +import com.ase.userservice.repositories.ExamRepository; +import com.ase.userservice.controllers.NotFoundException; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +public class ExamService { + private final ExamRepository repo; + + public ExamService(ExamRepository repo) { + this.repo = repo; + } + + @Transactional + public ExamResponse create(CreateExamRequest req) { + req.validateBusinessRules(); + if (repo.existsByTitleAndDate(req.title(), req.date())) { + throw new IllegalStateException("Exam with same title and date already exists"); + } + Exam exam = new Exam( + req.title(), req.moduleCode(), req.date(), req.startTime(), req.endTime(), + req.examiner(), req.room(), req.capacity(), req.ects(), + req.registrationDeadline(), req.deregistrationDeadline() + ); + Exam saved = repo.save(exam); + return toResponse(saved); + } + + @Transactional + public ExamResponse update(Long id, CreateExamRequest req) { + req.validateBusinessRules(); + + Exam exam = repo.findById(id) + .orElseThrow(() -> new NotFoundException("Exam " + id + " not found")); + + // Duplikate verhindern (außer beim gleichen Datensatz) + if (repo.existsByTitleAndDateAndIdNot(req.title(), req.date(), id)) { + throw new IllegalStateException("Exam with same title and date already exists"); + } + + // Felder 1:1 wie bei Erstellung + exam.setTitle(req.title()); + exam.setModuleCode(req.moduleCode()); + exam.setDate(req.date()); + exam.setStartTime(req.startTime()); + exam.setEndTime(req.endTime()); + exam.setExaminer(req.examiner()); + exam.setRoom(req.room()); + exam.setCapacity(req.capacity()); + exam.setEcts(req.ects()); + exam.setRegistrationDeadline(req.registrationDeadline()); + exam.setDeregistrationDeadline(req.deregistrationDeadline()); + + // exam ist gemanagt; speichern nicht zwingend nötig, aber explizit ist okay: + Exam saved = repo.save(exam); + return toResponse(saved); + } + + public static ExamResponse toResponse(Exam e) { + return new ExamResponse( + e.getId(), e.getTitle(), e.getModuleCode(), e.getDate(), + e.getStartTime(), e.getEndTime(), e.getExaminer(), e.getRoom(), + e.getCapacity(), e.getEcts(), e.getRegistrationDeadline(), e.getDeregistrationDeadline() + ); + } +} diff --git a/src/main/resources/application-prod.yaml b/src/main/resources/application-prod.yaml new file mode 100644 index 00000000..bab4c7b8 --- /dev/null +++ b/src/main/resources/application-prod.yaml @@ -0,0 +1,14 @@ +spring: + datasource: + url: jdbc:postgresql://${DB_HOST:localhost}:${DB_PORT:5432}/${DB_NAME:exams} + username: ${DB_USER} + password: ${DB_PASSWORD} + jpa: + hibernate: + ddl-auto: validate + properties: + hibernate: + dialect: org.hibernate.dialect.PostgreSQLDialect + +server: + port: 8080 diff --git a/src/main/resources/application.yaml b/src/main/resources/application.yaml index 4b5711b9..680cfb01 100644 --- a/src/main/resources/application.yaml +++ b/src/main/resources/application.yaml @@ -1,19 +1,20 @@ spring: - application: - name: ASE-UserService + datasource: + url: jdbc:h2:mem:examdb;DB_CLOSE_DELAY=-1;MODE=PostgreSQL + username: sa + password: '' + driver-class-name: org.h2.Driver jpa: - database-platform: org.hibernate.dialect.H2Dialect - show-sql: false hibernate: ddl-auto: update properties: hibernate: format_sql: true - datasource: - url: jdbc:h2:file:./data/mydb - driverClassName: org.h2.Driver - username: sa - password: password + jackson: + serialization: + write-dates-as-timestamps: false + server: - error: - include-message: always + port: 8080 + +logging.level.org.hibernate.SQL: INFO diff --git a/src/test/java/com/ase/userservice/ApplicationTests.java b/src/test/java/com/ase/userservice/ApplicationTests.java index be5b4249..98bfac6c 100644 --- a/src/test/java/com/ase/userservice/ApplicationTests.java +++ b/src/test/java/com/ase/userservice/ApplicationTests.java @@ -3,11 +3,10 @@ import org.junit.jupiter.api.Test; import org.springframework.boot.test.context.SpringBootTest; -@SpringBootTest +@SpringBootTest(properties = { + "spring.security.oauth2.resourceserver.jwt.jwk-set-uri=http://localhost/fake-jwks" +}) class ApplicationTests { - - @Test - void contextLoads() { - } - + @Test + void contextLoads() {} } diff --git a/src/test/java/com/ase/userservice/ExamController.java b/src/test/java/com/ase/userservice/ExamController.java new file mode 100644 index 00000000..06ee0510 --- /dev/null +++ b/src/test/java/com/ase/userservice/ExamController.java @@ -0,0 +1,42 @@ +package com.ase.userservice.controllers; + + +import com.ase.userservice.dto.CreateExamRequest; +import com.fasterxml.jackson.databind.ObjectMapper; + +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.http.MediaType; +import org.springframework.test.web.servlet.MockMvc; + +import java.time.*; + +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; + +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; + +@SpringBootTest +@AutoConfigureMockMvc +class ExamControllerIT { + @Autowired MockMvc mvc; + @Autowired ObjectMapper om; + + @Test + void createExam_returns201() throws Exception { + + CreateExamRequest req = new CreateExamRequest( + "SE I – Klausur", "ASE-101", LocalDate.now().plusDays(30), + LocalTime.of(9,0), LocalTime.of(11,0), + "Prof. Ada Lovelace", "HS A", 50, 5, + LocalDate.now().plusDays(25), LocalDate.now().plusDays(27) + ); + + mvc.perform(post("/api/exams") + .contentType(MediaType.APPLICATION_JSON) + .content(om.writeValueAsString(req))) + .andExpect(status().isCreated()) + .andExpect(jsonPath("$.id").exists()); + } +} From 4c56b0b76a0f2831fc7f41da1003e6d609da0134 Mon Sep 17 00:00:00 2001 From: MeOwOverlord Date: Wed, 10 Sep 2025 10:47:37 +0200 Subject: [PATCH 2/6] added ASE 435 code to ASE 436 --- .../userservice/dto/CreateExamRequest.java | 38 +++---- .../com/ase/userservice/dto/ExamResponse.java | 18 +-- .../com/ase/userservice/entities/Exam.java | 106 ++++++++++-------- .../repositories/ExamRepository.java | 8 +- .../ase/userservice/services/ExamService.java | 70 +++++++----- .../com/ase/userservice/ExamController.java | 42 ------- 6 files changed, 141 insertions(+), 141 deletions(-) delete mode 100644 src/test/java/com/ase/userservice/ExamController.java diff --git a/src/main/java/com/ase/userservice/dto/CreateExamRequest.java b/src/main/java/com/ase/userservice/dto/CreateExamRequest.java index dfbf1008..8ee9dbc4 100644 --- a/src/main/java/com/ase/userservice/dto/CreateExamRequest.java +++ b/src/main/java/com/ase/userservice/dto/CreateExamRequest.java @@ -1,30 +1,28 @@ package com.ase.userservice.dto; import jakarta.validation.constraints.*; -import java.time.*; +import java.time.LocalDate; +import java.time.LocalTime; +import java.time.Duration; +import java.util.List; public record CreateExamRequest( - @NotBlank(message = "title is required") String title, - @NotBlank(message = "moduleCode is required") String moduleCode, - @NotNull LocalDate date, - @NotNull LocalTime startTime, - @NotNull LocalTime endTime, - @NotBlank String examiner, - String room, - @NotNull @Positive Integer capacity, - @PositiveOrZero Integer ects, - @NotNull LocalDate registrationDeadline, - @NotNull LocalDate deregistrationDeadline + @NotBlank String title, + @NotBlank String moduleCode, + @NotNull LocalDate examDate, + @NotBlank String room, + @NotBlank String examType, + @NotBlank String semester, + @NotNull @PositiveOrZero Integer ects, + @NotNull @Positive Integer maxPoints, + @NotNull @Positive Integer duration, // Minuten + @NotNull @Positive Integer attemptNumber, // 1..n + boolean fileUploadRequired, + @NotNull @Size(max = 20) List<@NotBlank String> tools ) { public void validateBusinessRules() { - if (endTime != null && startTime != null && !endTime.isAfter(startTime)) { - throw new IllegalArgumentException("endTime must be after startTime"); - } - if (registrationDeadline != null && date != null && registrationDeadline.isAfter(date)) { - throw new IllegalArgumentException("registrationDeadline must be on/before date"); - } - if (deregistrationDeadline != null && registrationDeadline != null && deregistrationDeadline.isBefore(registrationDeadline)) { - throw new IllegalArgumentException("deregistrationDeadline must be on/after registrationDeadline"); + if (tools != null && tools.stream().anyMatch(s -> s == null || s.isBlank())) { + throw new IllegalArgumentException("tools must not contain blank items"); } } } diff --git a/src/main/java/com/ase/userservice/dto/ExamResponse.java b/src/main/java/com/ase/userservice/dto/ExamResponse.java index cc7219d4..50567f9f 100644 --- a/src/main/java/com/ase/userservice/dto/ExamResponse.java +++ b/src/main/java/com/ase/userservice/dto/ExamResponse.java @@ -1,18 +1,20 @@ package com.ase.userservice.dto; -import java.time.*; +import java.time.LocalDate; +import java.util.List; public record ExamResponse( Long id, String title, String moduleCode, - LocalDate date, - LocalTime startTime, - LocalTime endTime, - String examiner, + LocalDate examDate, String room, - Integer capacity, + String examType, + String semester, Integer ects, - LocalDate registrationDeadline, - LocalDate deregistrationDeadline + Integer maxPoints, + Integer duration, + Integer attemptNumber, + boolean fileUploadRequired, + List tools ) {} diff --git a/src/main/java/com/ase/userservice/entities/Exam.java b/src/main/java/com/ase/userservice/entities/Exam.java index 146e523f..e65485d1 100644 --- a/src/main/java/com/ase/userservice/entities/Exam.java +++ b/src/main/java/com/ase/userservice/entities/Exam.java @@ -1,64 +1,78 @@ package com.ase.userservice.entities; import jakarta.persistence.*; -import java.time.*; +import java.time.LocalDate; +import java.util.ArrayList; +import java.util.List; @Entity -@Table(name = "exams") +@Table( + name = "exams", + uniqueConstraints = { + @UniqueConstraint(name = "uk_exam_modcode_date_attempt", columnNames = {"module_code", "exam_date", "attempt_number"}) + } +) public class Exam { + @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; - @Column(nullable = false, length = 120) + @Column(nullable = false, length = 160) private String title; - @Column(nullable = false, length = 40) + @Column(name = "module_code", nullable = false, length = 40) private String moduleCode; - @Column(nullable = false) - private LocalDate date; - - @Column(nullable = false) - private LocalTime startTime; - - @Column(nullable = false) - private LocalTime endTime; + @Column(name = "exam_date", nullable = false) + private LocalDate examDate; @Column(nullable = false, length = 80) - private String examiner; - - @Column(length = 80) private String room; - @Column(nullable = false) - private Integer capacity; + @Column(nullable = false, length = 40) + private String examType; + + @Column(nullable = false, length = 20) + private String semester; - @Column + @Column(nullable = false) private Integer ects; @Column(nullable = false) - private LocalDate registrationDeadline; + private Integer maxPoints; @Column(nullable = false) - private LocalDate deregistrationDeadline; + private Integer duration; // Minuten + + @Column(name = "attempt_number", nullable = false) + private Integer attemptNumber; + + @Column(nullable = false) + private boolean fileUploadRequired; + + @ElementCollection + @CollectionTable(name = "exam_tools", joinColumns = @JoinColumn(name = "exam_id")) + @Column(name = "tool", nullable = false, length = 60) + private List tools = new ArrayList<>(); protected Exam() {} - public Exam(String title, String moduleCode, LocalDate date, LocalTime startTime, LocalTime endTime, - String examiner, String room, Integer capacity, Integer ects, - LocalDate registrationDeadline, LocalDate deregistrationDeadline) { + public Exam(String title, String moduleCode, LocalDate examDate, String room, String examType, + String semester, Integer ects, Integer maxPoints, Integer duration, + Integer attemptNumber, boolean fileUploadRequired, List tools) { this.title = title; this.moduleCode = moduleCode; - this.date = date; - this.startTime = startTime; - this.endTime = endTime; - this.examiner = examiner; + this.examDate = examDate; this.room = room; - this.capacity = capacity; + this.examType = examType; + this.semester = semester; this.ects = ects; - this.registrationDeadline = registrationDeadline; - this.deregistrationDeadline = deregistrationDeadline; + this.maxPoints = maxPoints; + this.duration = duration; + this.attemptNumber = attemptNumber; + this.fileUploadRequired = fileUploadRequired; + if (tools != null) this.tools = new ArrayList<>(tools); } public Long getId() { return id; } @@ -66,22 +80,26 @@ public Exam(String title, String moduleCode, LocalDate date, LocalTime startTime public void setTitle(String title) { this.title = title; } public String getModuleCode() { return moduleCode; } public void setModuleCode(String moduleCode) { this.moduleCode = moduleCode; } - public LocalDate getDate() { return date; } - public void setDate(LocalDate date) { this.date = date; } - public LocalTime getStartTime() { return startTime; } - public void setStartTime(LocalTime startTime) { this.startTime = startTime; } - public LocalTime getEndTime() { return endTime; } - public void setEndTime(LocalTime endTime) { this.endTime = endTime; } - public String getExaminer() { return examiner; } - public void setExaminer(String examiner) { this.examiner = examiner; } + public LocalDate getExamDate() { return examDate; } + public void setExamDate(LocalDate examDate) { this.examDate = examDate; } public String getRoom() { return room; } public void setRoom(String room) { this.room = room; } - public Integer getCapacity() { return capacity; } - public void setCapacity(Integer capacity) { this.capacity = capacity; } + public String getExamType() { return examType; } + public void setExamType(String examType) { this.examType = examType; } + public String getSemester() { return semester; } + public void setSemester(String semester) { this.semester = semester; } public Integer getEcts() { return ects; } public void setEcts(Integer ects) { this.ects = ects; } - public LocalDate getRegistrationDeadline() { return registrationDeadline; } - public void setRegistrationDeadline(LocalDate registrationDeadline) { this.registrationDeadline = registrationDeadline; } - public LocalDate getDeregistrationDeadline() { return deregistrationDeadline; } - public void setDeregistrationDeadline(LocalDate deregistrationDeadline) { this.deregistrationDeadline = deregistrationDeadline; } + public Integer getMaxPoints() { return maxPoints; } + public void setMaxPoints(Integer maxPoints) { this.maxPoints = maxPoints; } + public Integer getDuration() { return duration; } + public void setDuration(Integer duration) { this.duration = duration; } + public Integer getAttemptNumber() { return attemptNumber; } + public void setAttemptNumber(Integer attemptNumber) { this.attemptNumber = attemptNumber; } + public boolean isFileUploadRequired() { return fileUploadRequired; } + public void setFileUploadRequired(boolean fileUploadRequired) { this.fileUploadRequired = fileUploadRequired; } + public List getTools() { return tools; } + public void setTools(List tools) { + this.tools = tools != null ? new ArrayList<>(tools) : new ArrayList<>(); + } } diff --git a/src/main/java/com/ase/userservice/repositories/ExamRepository.java b/src/main/java/com/ase/userservice/repositories/ExamRepository.java index f448bc11..29781197 100644 --- a/src/main/java/com/ase/userservice/repositories/ExamRepository.java +++ b/src/main/java/com/ase/userservice/repositories/ExamRepository.java @@ -8,6 +8,10 @@ @Repository public interface ExamRepository extends JpaRepository { - boolean existsByTitleAndDate(String title, LocalDate date); - boolean existsByTitleAndDateAndIdNot(String title, LocalDate date, Long id); + + boolean existsByModuleCodeAndExamDateAndAttemptNumber( + String moduleCode, LocalDate examDate, Integer attemptNumber); + + boolean existsByModuleCodeAndExamDateAndAttemptNumberAndIdNot( + String moduleCode, LocalDate examDate, Integer attemptNumber, Long id); } diff --git a/src/main/java/com/ase/userservice/services/ExamService.java b/src/main/java/com/ase/userservice/services/ExamService.java index 871d17b2..43e58e22 100644 --- a/src/main/java/com/ase/userservice/services/ExamService.java +++ b/src/main/java/com/ase/userservice/services/ExamService.java @@ -1,10 +1,10 @@ package com.ase.userservice.services; +import com.ase.userservice.controllers.NotFoundException; import com.ase.userservice.dto.CreateExamRequest; import com.ase.userservice.dto.ExamResponse; import com.ase.userservice.entities.Exam; import com.ase.userservice.repositories.ExamRepository; -import com.ase.userservice.controllers.NotFoundException; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -19,16 +19,28 @@ public ExamService(ExamRepository repo) { @Transactional public ExamResponse create(CreateExamRequest req) { req.validateBusinessRules(); - if (repo.existsByTitleAndDate(req.title(), req.date())) { - throw new IllegalStateException("Exam with same title and date already exists"); + + if (repo.existsByModuleCodeAndExamDateAndAttemptNumber( + req.moduleCode(), req.examDate(), req.attemptNumber())) { + throw new IllegalStateException("Exam with same moduleCode, examDate and attemptNumber already exists"); } + Exam exam = new Exam( - req.title(), req.moduleCode(), req.date(), req.startTime(), req.endTime(), - req.examiner(), req.room(), req.capacity(), req.ects(), - req.registrationDeadline(), req.deregistrationDeadline() + req.title(), + req.moduleCode(), + req.examDate(), + req.room(), + req.examType(), + req.semester(), + req.ects(), + req.maxPoints(), + req.duration(), + req.attemptNumber(), + req.fileUploadRequired(), + req.tools() ); - Exam saved = repo.save(exam); - return toResponse(saved); + + return toResponse(repo.save(exam)); } @Transactional @@ -38,34 +50,42 @@ public ExamResponse update(Long id, CreateExamRequest req) { Exam exam = repo.findById(id) .orElseThrow(() -> new NotFoundException("Exam " + id + " not found")); - // Duplikate verhindern (außer beim gleichen Datensatz) - if (repo.existsByTitleAndDateAndIdNot(req.title(), req.date(), id)) { - throw new IllegalStateException("Exam with same title and date already exists"); + if (repo.existsByModuleCodeAndExamDateAndAttemptNumberAndIdNot( + req.moduleCode(), req.examDate(), req.attemptNumber(), id)) { + throw new IllegalStateException("Exam with same moduleCode, examDate and attemptNumber already exists"); } - // Felder 1:1 wie bei Erstellung exam.setTitle(req.title()); exam.setModuleCode(req.moduleCode()); - exam.setDate(req.date()); - exam.setStartTime(req.startTime()); - exam.setEndTime(req.endTime()); - exam.setExaminer(req.examiner()); + exam.setExamDate(req.examDate()); exam.setRoom(req.room()); - exam.setCapacity(req.capacity()); + exam.setExamType(req.examType()); + exam.setSemester(req.semester()); exam.setEcts(req.ects()); - exam.setRegistrationDeadline(req.registrationDeadline()); - exam.setDeregistrationDeadline(req.deregistrationDeadline()); + exam.setMaxPoints(req.maxPoints()); + exam.setDuration(req.duration()); + exam.setAttemptNumber(req.attemptNumber()); + exam.setFileUploadRequired(req.fileUploadRequired()); + exam.setTools(req.tools()); - // exam ist gemanagt; speichern nicht zwingend nötig, aber explizit ist okay: - Exam saved = repo.save(exam); - return toResponse(saved); + return toResponse(repo.save(exam)); } public static ExamResponse toResponse(Exam e) { return new ExamResponse( - e.getId(), e.getTitle(), e.getModuleCode(), e.getDate(), - e.getStartTime(), e.getEndTime(), e.getExaminer(), e.getRoom(), - e.getCapacity(), e.getEcts(), e.getRegistrationDeadline(), e.getDeregistrationDeadline() + e.getId(), + e.getTitle(), + e.getModuleCode(), + e.getExamDate(), + e.getRoom(), + e.getExamType(), + e.getSemester(), + e.getEcts(), + e.getMaxPoints(), + e.getDuration(), + e.getAttemptNumber(), + e.isFileUploadRequired(), + e.getTools() ); } } diff --git a/src/test/java/com/ase/userservice/ExamController.java b/src/test/java/com/ase/userservice/ExamController.java deleted file mode 100644 index 06ee0510..00000000 --- a/src/test/java/com/ase/userservice/ExamController.java +++ /dev/null @@ -1,42 +0,0 @@ -package com.ase.userservice.controllers; - - -import com.ase.userservice.dto.CreateExamRequest; -import com.fasterxml.jackson.databind.ObjectMapper; - -import org.junit.jupiter.api.Test; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.http.MediaType; -import org.springframework.test.web.servlet.MockMvc; - -import java.time.*; - -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; - -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; - -@SpringBootTest -@AutoConfigureMockMvc -class ExamControllerIT { - @Autowired MockMvc mvc; - @Autowired ObjectMapper om; - - @Test - void createExam_returns201() throws Exception { - - CreateExamRequest req = new CreateExamRequest( - "SE I – Klausur", "ASE-101", LocalDate.now().plusDays(30), - LocalTime.of(9,0), LocalTime.of(11,0), - "Prof. Ada Lovelace", "HS A", 50, 5, - LocalDate.now().plusDays(25), LocalDate.now().plusDays(27) - ); - - mvc.perform(post("/api/exams") - .contentType(MediaType.APPLICATION_JSON) - .content(om.writeValueAsString(req))) - .andExpect(status().isCreated()) - .andExpect(jsonPath("$.id").exists()); - } -} From 0927cb426313bb21108912a636c437514e934239 Mon Sep 17 00:00:00 2001 From: MeOwOverlord Date: Wed, 10 Sep 2025 11:14:53 +0200 Subject: [PATCH 3/6] added get to controller --- .../ase/userservice/controllers/ExamController.java | 13 +++++++++++++ .../com/ase/userservice/services/ExamService.java | 13 +++++++++++++ 2 files changed, 26 insertions(+) diff --git a/src/main/java/com/ase/userservice/controllers/ExamController.java b/src/main/java/com/ase/userservice/controllers/ExamController.java index 9afe9d36..2dfebdd4 100644 --- a/src/main/java/com/ase/userservice/controllers/ExamController.java +++ b/src/main/java/com/ase/userservice/controllers/ExamController.java @@ -8,6 +8,7 @@ import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.web.bind.annotation.*; import org.springframework.web.util.UriComponentsBuilder; +import java.util.List; @RestController @RequestMapping("/api/exams") @@ -34,4 +35,16 @@ public ResponseEntity updateExam(@PathVariable Long id, @Valid @RequestBody CreateExamRequest req) { return ResponseEntity.ok(service.update(id, req)); } + + @GetMapping("/{id}") + @PreAuthorize("hasAuthority('exam:read') or hasAuthority('ROLE_ADMIN')") + public ResponseEntity getExam(@PathVariable Long id) { + return ResponseEntity.ok(service.get(id)); + } + + @GetMapping + @PreAuthorize("hasAuthority('exam:read') or hasAuthority('ROLE_ADMIN')") + public ResponseEntity> listExams() { + return ResponseEntity.ok(service.list()); + } } diff --git a/src/main/java/com/ase/userservice/services/ExamService.java b/src/main/java/com/ase/userservice/services/ExamService.java index 43e58e22..29e92383 100644 --- a/src/main/java/com/ase/userservice/services/ExamService.java +++ b/src/main/java/com/ase/userservice/services/ExamService.java @@ -7,6 +7,7 @@ import com.ase.userservice.repositories.ExamRepository; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import java.util.List; @Service public class ExamService { @@ -88,4 +89,16 @@ public static ExamResponse toResponse(Exam e) { e.getTools() ); } + + @Transactional(readOnly = true) + public ExamResponse get(Long id) { + var exam = repo.findById(id) + .orElseThrow(() -> new NotFoundException("Exam " + id + " not found")); + return toResponse(exam); + } + + @Transactional(readOnly = true) + public List list() { + return repo.findAll().stream().map(ExamService::toResponse).toList(); + } } From 93035f59f63e43ab41904f4993373a2046b5c9ad Mon Sep 17 00:00:00 2001 From: MeOwOverlord Date: Wed, 10 Sep 2025 16:25:28 +0200 Subject: [PATCH 4/6] remove auth --- pom.xml | 10 ---- .../userservice/config/SecurityConfig.java | 47 ------------------- .../controllers/ExamController.java | 17 ++++--- 3 files changed, 10 insertions(+), 64 deletions(-) delete mode 100644 src/main/java/com/ase/userservice/config/SecurityConfig.java diff --git a/pom.xml b/pom.xml index f03c24f0..7b4d5af8 100644 --- a/pom.xml +++ b/pom.xml @@ -74,16 +74,6 @@ spring-boot-starter-data-jpa - - - org.springframework.boot - spring-boot-starter-security - - - org.springframework.boot - spring-boot-starter-oauth2-resource-server - - org.springframework.boot diff --git a/src/main/java/com/ase/userservice/config/SecurityConfig.java b/src/main/java/com/ase/userservice/config/SecurityConfig.java deleted file mode 100644 index 0a045f70..00000000 --- a/src/main/java/com/ase/userservice/config/SecurityConfig.java +++ /dev/null @@ -1,47 +0,0 @@ -package com.ase.userservice.config; - -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.core.GrantedAuthority; -import org.springframework.security.oauth2.jwt.Jwt; -import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationConverter; -import org.springframework.security.web.SecurityFilterChain; - -import java.util.Collection; -import java.util.List; -import java.util.stream.Collectors; - -@Configuration -@EnableMethodSecurity -public class SecurityConfig { - - @Bean - SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { - http - .csrf(csrf -> csrf.disable()) - .authorizeHttpRequests(auth -> auth - .requestMatchers("/actuator/**", "/v3/api-docs/**", "/swagger-ui/**").permitAll() - .anyRequest().authenticated()) - .oauth2ResourceServer(oauth -> oauth.jwt(jwt -> jwt.jwtAuthenticationConverter(rolesConverter()))); - return http.build(); - } - - /** Mappt das JWT-Claim 'roles' -> Spring GrantedAuthorities. */ - @Bean - public JwtAuthenticationConverter rolesConverter() { - JwtAuthenticationConverter converter = new JwtAuthenticationConverter(); - converter.setJwtGrantedAuthoritiesConverter(this::convertRolesClaim); - return converter; - } - - /** Liefert exakt Collection (kein Wildcard), damit der Converter-Typ passt. */ - private Collection convertRolesClaim(Jwt jwt) { - List roles = jwt.getClaimAsStringList("roles"); - if (roles == null) roles = List.of(); - return roles.stream() - .map(r -> (GrantedAuthority) new org.springframework.security.core.authority.SimpleGrantedAuthority(r)) - .collect(Collectors.toList()); - } -} diff --git a/src/main/java/com/ase/userservice/controllers/ExamController.java b/src/main/java/com/ase/userservice/controllers/ExamController.java index 2dfebdd4..a105d61d 100644 --- a/src/main/java/com/ase/userservice/controllers/ExamController.java +++ b/src/main/java/com/ase/userservice/controllers/ExamController.java @@ -5,11 +5,18 @@ import com.ase.userservice.services.ExamService; import jakarta.validation.Valid; import org.springframework.http.ResponseEntity; -import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.web.bind.annotation.*; import org.springframework.web.util.UriComponentsBuilder; + import java.util.List; +@CrossOrigin( + origins = "http://localhost:5173", + allowedHeaders = "*", + allowCredentials = "true", + maxAge = 3600, + methods = { RequestMethod.GET, RequestMethod.POST, RequestMethod.PUT, RequestMethod.DELETE, RequestMethod.OPTIONS } +) @RestController @RequestMapping("/api/exams") public class ExamController { @@ -20,30 +27,26 @@ public ExamController(ExamService service) { } @PostMapping - @PreAuthorize("hasAuthority('exam:write') or hasAuthority('ROLE_ADMIN')") public ResponseEntity createExam(@Valid @RequestBody CreateExamRequest req, UriComponentsBuilder uri) { ExamResponse created = service.create(req); return ResponseEntity - .created(uri.path("/api/exams/{id}").buildAndExpand(created.id()).toUri()) - .body(created); + .created(uri.path("/api/exams/{id}").buildAndExpand(created.id()).toUri()) + .body(created); } @PutMapping("/{id}") - @PreAuthorize("hasAuthority('exam:write') or hasAuthority('ROLE_ADMIN')") public ResponseEntity updateExam(@PathVariable Long id, @Valid @RequestBody CreateExamRequest req) { return ResponseEntity.ok(service.update(id, req)); } @GetMapping("/{id}") - @PreAuthorize("hasAuthority('exam:read') or hasAuthority('ROLE_ADMIN')") public ResponseEntity getExam(@PathVariable Long id) { return ResponseEntity.ok(service.get(id)); } @GetMapping - @PreAuthorize("hasAuthority('exam:read') or hasAuthority('ROLE_ADMIN')") public ResponseEntity> listExams() { return ResponseEntity.ok(service.list()); } From 16a205d8a688dca8757851f97cd79636fa863508 Mon Sep 17 00:00:00 2001 From: Fiete Minge Date: Fri, 19 Sep 2025 14:07:14 +0200 Subject: [PATCH 5/6] change to datetime / add delete endpoint --- .../ase/userservice/controllers/ExamController.java | 6 ++++++ .../com/ase/userservice/dto/CreateExamRequest.java | 3 ++- .../java/com/ase/userservice/dto/ExamResponse.java | 4 ++-- src/main/java/com/ase/userservice/entities/Exam.java | 10 +++++----- .../ase/userservice/repositories/ExamRepository.java | 6 +++--- .../java/com/ase/userservice/services/ExamService.java | 5 +++++ 6 files changed, 23 insertions(+), 11 deletions(-) diff --git a/src/main/java/com/ase/userservice/controllers/ExamController.java b/src/main/java/com/ase/userservice/controllers/ExamController.java index a105d61d..11432ac5 100644 --- a/src/main/java/com/ase/userservice/controllers/ExamController.java +++ b/src/main/java/com/ase/userservice/controllers/ExamController.java @@ -50,4 +50,10 @@ public ResponseEntity getExam(@PathVariable Long id) { public ResponseEntity> listExams() { return ResponseEntity.ok(service.list()); } + + @DeleteMapping("/{id}") + public ResponseEntity deleteExam(@PathVariable Long id) { + service.delete(id); + return ResponseEntity.ok().build(); + } } diff --git a/src/main/java/com/ase/userservice/dto/CreateExamRequest.java b/src/main/java/com/ase/userservice/dto/CreateExamRequest.java index 8ee9dbc4..6054287f 100644 --- a/src/main/java/com/ase/userservice/dto/CreateExamRequest.java +++ b/src/main/java/com/ase/userservice/dto/CreateExamRequest.java @@ -2,6 +2,7 @@ import jakarta.validation.constraints.*; import java.time.LocalDate; +import java.time.LocalDateTime; import java.time.LocalTime; import java.time.Duration; import java.util.List; @@ -9,7 +10,7 @@ public record CreateExamRequest( @NotBlank String title, @NotBlank String moduleCode, - @NotNull LocalDate examDate, + @NotNull LocalDateTime examDate, @NotBlank String room, @NotBlank String examType, @NotBlank String semester, diff --git a/src/main/java/com/ase/userservice/dto/ExamResponse.java b/src/main/java/com/ase/userservice/dto/ExamResponse.java index 50567f9f..873c6fb3 100644 --- a/src/main/java/com/ase/userservice/dto/ExamResponse.java +++ b/src/main/java/com/ase/userservice/dto/ExamResponse.java @@ -1,13 +1,13 @@ package com.ase.userservice.dto; -import java.time.LocalDate; +import java.time.LocalDateTime; import java.util.List; public record ExamResponse( Long id, String title, String moduleCode, - LocalDate examDate, + LocalDateTime examDate, String room, String examType, String semester, diff --git a/src/main/java/com/ase/userservice/entities/Exam.java b/src/main/java/com/ase/userservice/entities/Exam.java index e65485d1..21f0414a 100644 --- a/src/main/java/com/ase/userservice/entities/Exam.java +++ b/src/main/java/com/ase/userservice/entities/Exam.java @@ -1,7 +1,7 @@ package com.ase.userservice.entities; import jakarta.persistence.*; -import java.time.LocalDate; +import java.time.LocalDateTime; import java.util.ArrayList; import java.util.List; @@ -25,7 +25,7 @@ public class Exam { private String moduleCode; @Column(name = "exam_date", nullable = false) - private LocalDate examDate; + private LocalDateTime examDate; @Column(nullable = false, length = 80) private String room; @@ -58,7 +58,7 @@ public class Exam { protected Exam() {} - public Exam(String title, String moduleCode, LocalDate examDate, String room, String examType, + public Exam(String title, String moduleCode, LocalDateTime examDate, String room, String examType, String semester, Integer ects, Integer maxPoints, Integer duration, Integer attemptNumber, boolean fileUploadRequired, List tools) { this.title = title; @@ -80,8 +80,8 @@ public Exam(String title, String moduleCode, LocalDate examDate, String room, St public void setTitle(String title) { this.title = title; } public String getModuleCode() { return moduleCode; } public void setModuleCode(String moduleCode) { this.moduleCode = moduleCode; } - public LocalDate getExamDate() { return examDate; } - public void setExamDate(LocalDate examDate) { this.examDate = examDate; } + public LocalDateTime getExamDate() { return examDate; } + public void setExamDate(LocalDateTime examDate) { this.examDate = examDate; } public String getRoom() { return room; } public void setRoom(String room) { this.room = room; } public String getExamType() { return examType; } diff --git a/src/main/java/com/ase/userservice/repositories/ExamRepository.java b/src/main/java/com/ase/userservice/repositories/ExamRepository.java index 29781197..4293ae59 100644 --- a/src/main/java/com/ase/userservice/repositories/ExamRepository.java +++ b/src/main/java/com/ase/userservice/repositories/ExamRepository.java @@ -4,14 +4,14 @@ import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Repository; -import java.time.LocalDate; +import java.time.LocalDateTime; @Repository public interface ExamRepository extends JpaRepository { boolean existsByModuleCodeAndExamDateAndAttemptNumber( - String moduleCode, LocalDate examDate, Integer attemptNumber); + String moduleCode, LocalDateTime examDate, Integer attemptNumber); boolean existsByModuleCodeAndExamDateAndAttemptNumberAndIdNot( - String moduleCode, LocalDate examDate, Integer attemptNumber, Long id); + String moduleCode, LocalDateTime examDate, Integer attemptNumber, Long id); } diff --git a/src/main/java/com/ase/userservice/services/ExamService.java b/src/main/java/com/ase/userservice/services/ExamService.java index 29e92383..5c4a9cb6 100644 --- a/src/main/java/com/ase/userservice/services/ExamService.java +++ b/src/main/java/com/ase/userservice/services/ExamService.java @@ -101,4 +101,9 @@ public ExamResponse get(Long id) { public List list() { return repo.findAll().stream().map(ExamService::toResponse).toList(); } + + @Transactional + public void delete(Long id) { + repo.deleteById(id); + } } From 4630417b96387d1fb5a9e7c33dd768c825134654 Mon Sep 17 00:00:00 2001 From: MeOwOverlord Date: Mon, 22 Sep 2025 10:20:39 +0200 Subject: [PATCH 6/6] create dockerfile and k8s --- .github/workflows/deploy-application.yaml | 31 +++++++++++++++++++ Dockerfile | 14 +++++++++ k8s/deployment.yaml | 36 +++++++++++++++++++++++ k8s/kustomization.yaml | 9 ++++++ k8s/service.yaml | 12 ++++++++ 5 files changed, 102 insertions(+) create mode 100644 .github/workflows/deploy-application.yaml create mode 100644 Dockerfile create mode 100644 k8s/deployment.yaml create mode 100644 k8s/kustomization.yaml create mode 100644 k8s/service.yaml diff --git a/.github/workflows/deploy-application.yaml b/.github/workflows/deploy-application.yaml new file mode 100644 index 00000000..35b3bdaa --- /dev/null +++ b/.github/workflows/deploy-application.yaml @@ -0,0 +1,31 @@ +name: deploy-to-k8s + +on: + push: + branches: + - main + - edit-exam-ASE-436 + - create-exam-ASE-435 + +permissions: + contents: read + packages: write + +jobs: + deploy: + runs-on: ubuntu-latest + steps: + - name: Checkout repo + uses: actions/checkout@v5 + + - name: Upload image + uses: Agile-Software-Engineering-25/build-and-publish-image@v1 + with: + push: true + extra_tags: ${{ github.sha }} + + - name: Deploy to Namespace + uses: Agile-Software-Engineering-25/deploy-to-k8s@v1 + with: + kubeconfig: ${{ secrets.KUBECONFIG }} + namespace: ${{ vars.K8S_NAMESPACE }} diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 00000000..5dd8fd5e --- /dev/null +++ b/Dockerfile @@ -0,0 +1,14 @@ +# ---- Build stage +FROM maven:3.9-eclipse-temurin-21 AS build +WORKDIR /app +COPY pom.xml . +COPY src ./src +RUN mvn -q -DskipTests clean package + +# ---- Run stage +FROM eclipse-temurin:21-jre +WORKDIR /app +ENV JAVA_OPTS="-Xms256m -Xmx512m" +EXPOSE 8080 +COPY --from=build /app/target/*.jar app.jar +ENTRYPOINT ["sh","-c","java $JAVA_OPTS -jar /app/app.jar"] diff --git a/k8s/deployment.yaml b/k8s/deployment.yaml new file mode 100644 index 00000000..231f44f4 --- /dev/null +++ b/k8s/deployment.yaml @@ -0,0 +1,36 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: exam-service +spec: + replicas: 1 + selector: + matchLabels: + app: exam-service + template: + metadata: + labels: + app: exam-service + spec: + containers: + - name: app + image: ghcr.io/agile-software-engineering-25/team-14-backend-examination-office:latest + ports: + - containerPort: 8080 + readinessProbe: + tcpSocket: + port: 8080 + initialDelaySeconds: 5 + periodSeconds: 10 + livenessProbe: + tcpSocket: + port: 8080 + initialDelaySeconds: 20 + periodSeconds: 20 + resources: + requests: + cpu: "100m" + memory: "256Mi" + limits: + cpu: "500m" + memory: "512Mi" diff --git a/k8s/kustomization.yaml b/k8s/kustomization.yaml new file mode 100644 index 00000000..b6db69ba --- /dev/null +++ b/k8s/kustomization.yaml @@ -0,0 +1,9 @@ +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +resources: + - deployment.yaml + - service.yaml + +images: + - name: ghcr.io/agile-software-engineering-25/team-14-backend-examination-office + newTag: latest diff --git a/k8s/service.yaml b/k8s/service.yaml new file mode 100644 index 00000000..b86c0d15 --- /dev/null +++ b/k8s/service.yaml @@ -0,0 +1,12 @@ +apiVersion: v1 +kind: Service +metadata: + name: exam-service +spec: + type: ClusterIP + selector: + app: exam-service + ports: + - name: http + port: 80 + targetPort: 8080