From d064cf5dd8a993a0c7a07c504d68c0d233b9588a Mon Sep 17 00:00:00 2001 From: Your Name Date: Thu, 21 Aug 2025 14:04:31 +0200 Subject: [PATCH 1/7] start ex --- .gitignore | 2 ++ build.gradle | 49 ++++++++++++++++++++++++++++++++++--------------- 2 files changed, 36 insertions(+), 15 deletions(-) diff --git a/.gitignore b/.gitignore index dc01f204..b76b3899 100644 --- a/.gitignore +++ b/.gitignore @@ -36,3 +36,5 @@ out/ ### VS Code ### .vscode/ + +/src/main/resources/application.yml diff --git a/build.gradle b/build.gradle index 3d7f7607..4c7a5502 100644 --- a/build.gradle +++ b/build.gradle @@ -1,31 +1,50 @@ plugins { - id 'java' - id 'org.springframework.boot' version '3.3.1' - id 'io.spring.dependency-management' version '1.1.5' + id 'java' + id 'org.springframework.boot' version '3.4.2' + id 'io.spring.dependency-management' version '1.1.7' } group = 'com.booleanuk' version = '0.0.1-SNAPSHOT' java { - toolchain { - languageVersion = JavaLanguageVersion.of(21) - } + toolchain { + languageVersion = JavaLanguageVersion.of(21) + } +} + +configurations { + compileOnly { + extendsFrom annotationProcessor + } } repositories { - mavenCentral() + mavenCentral() } dependencies { - implementation 'org.springframework.boot:spring-boot-starter-data-jpa' - implementation 'org.springframework.boot:spring-boot-starter-web' - developmentOnly 'org.springframework.boot:spring-boot-devtools' - runtimeOnly 'org.postgresql:postgresql' - testImplementation 'org.springframework.boot:spring-boot-starter-test' - testRuntimeOnly 'org.junit.platform:junit-platform-launcher' + implementation 'org.springframework.boot:spring-boot-starter-data-jpa' + implementation 'org.springframework.boot:spring-boot-starter-security' + implementation 'org.springframework.boot:spring-boot-starter-web' + implementation 'io.jsonwebtoken:jjwt-api:0.12.6' + implementation 'io.jsonwebtoken:jjwt-impl:0.12.6' + runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.12.6' + + compileOnly 'org.projectlombok:lombok' + developmentOnly 'org.springframework.boot:spring-boot-devtools' + runtimeOnly 'org.postgresql:postgresql' + annotationProcessor 'org.projectlombok:lombok' + testImplementation 'org.springframework.boot:spring-boot-starter-test' + testImplementation 'org.springframework.security:spring-security-test' + + implementation 'jakarta.validation:jakarta.validation-api:3.1.1' +} + +tasks.named('bootBuildImage') { + builder = 'paketobuildpacks/builder-jammy-base:latest' } tasks.named('test') { - useJUnitPlatform() -} \ No newline at end of file + useJUnitPlatform() +} From 63a00432933a0aca57e48852563cf6ee3eb29376 Mon Sep 17 00:00:00 2001 From: Your Name Date: Thu, 21 Aug 2025 14:05:22 +0200 Subject: [PATCH 2/7] created models(tables) --- .../api/cinema/library/models/Customer.java | 28 +++++++++++++++++ .../api/cinema/library/models/Movie.java | 30 +++++++++++++++++++ .../api/cinema/library/models/Screening.java | 27 +++++++++++++++++ .../api/cinema/library/models/Ticket.java | 30 +++++++++++++++++++ 4 files changed, 115 insertions(+) create mode 100644 src/main/java/com/booleanuk/api/cinema/library/models/Customer.java create mode 100644 src/main/java/com/booleanuk/api/cinema/library/models/Movie.java create mode 100644 src/main/java/com/booleanuk/api/cinema/library/models/Screening.java create mode 100644 src/main/java/com/booleanuk/api/cinema/library/models/Ticket.java diff --git a/src/main/java/com/booleanuk/api/cinema/library/models/Customer.java b/src/main/java/com/booleanuk/api/cinema/library/models/Customer.java new file mode 100644 index 00000000..ac395c8c --- /dev/null +++ b/src/main/java/com/booleanuk/api/cinema/library/models/Customer.java @@ -0,0 +1,28 @@ +package com.booleanuk.api.cinema.library.models; + +import jakarta.persistence.*; +import lombok.*; +import java.time.LocalDateTime; +import java.util.List; + +@Entity +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class Customer { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Integer id; + + private String name; + private String email; + private String phone; + + private LocalDateTime createdAt; + private LocalDateTime updatedAt; + + @OneToMany(mappedBy = "customer", cascade = CascadeType.ALL) + private List tickets; +} diff --git a/src/main/java/com/booleanuk/api/cinema/library/models/Movie.java b/src/main/java/com/booleanuk/api/cinema/library/models/Movie.java new file mode 100644 index 00000000..883f15ad --- /dev/null +++ b/src/main/java/com/booleanuk/api/cinema/library/models/Movie.java @@ -0,0 +1,30 @@ +package com.booleanuk.api.cinema.library.models; + +import jakarta.persistence.*; +import lombok.*; +import java.time.LocalDateTime; +import java.util.List; + +@Entity +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class Movie { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Integer id; + + private String title; + private String rating; + @Column(length = 2000) + private String description; + private Integer runtimeMins; + + private LocalDateTime createdAt; + private LocalDateTime updatedAt; + + @OneToMany(mappedBy = "movie", cascade = CascadeType.ALL) + private List screenings; +} diff --git a/src/main/java/com/booleanuk/api/cinema/library/models/Screening.java b/src/main/java/com/booleanuk/api/cinema/library/models/Screening.java new file mode 100644 index 00000000..1a357c3b --- /dev/null +++ b/src/main/java/com/booleanuk/api/cinema/library/models/Screening.java @@ -0,0 +1,27 @@ +package com.booleanuk.api.cinema.library.models; + +import jakarta.persistence.*; +import lombok.*; +import java.time.LocalDateTime; +import java.util.List; + +@Entity +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class Screening { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + private LocalDateTime startTime; + + @ManyToOne + @JoinColumn(name = "movie_id") + private Movie movie; + + @OneToMany(mappedBy = "screening", cascade = CascadeType.ALL) + private List tickets; +} diff --git a/src/main/java/com/booleanuk/api/cinema/library/models/Ticket.java b/src/main/java/com/booleanuk/api/cinema/library/models/Ticket.java new file mode 100644 index 00000000..9ca6037d --- /dev/null +++ b/src/main/java/com/booleanuk/api/cinema/library/models/Ticket.java @@ -0,0 +1,30 @@ +package com.booleanuk.api.cinema.library.models; + +import jakarta.persistence.*; +import lombok.*; +import java.time.LocalDateTime; + +@Entity +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class Ticket { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Integer id; + + @ManyToOne + @JoinColumn(name = "customer_id") + private Customer customer; + + @ManyToOne + @JoinColumn(name = "screening_id") + private Screening screening; + + private Integer numSeats; + + private LocalDateTime createdAt; + private LocalDateTime updatedAt; +} From 4036646c0e3405fa19128c2f6b648b70e8c1047f Mon Sep 17 00:00:00 2001 From: Your Name Date: Thu, 21 Aug 2025 16:22:58 +0200 Subject: [PATCH 3/7] created repo controller securityConfig tests working --- .../java/com/booleanuk/api/cinema/Main.java | 11 +++ .../api/cinema/library/SecurityConfig.java | 17 ++++ .../controllers/CustomerController.java | 78 +++++++++++++++++++ .../library/controllers/MovieController.java | 51 ++++++++++++ .../controllers/ScreeningController.java | 50 ++++++++++++ .../api/cinema/library/models/Customer.java | 11 +++ .../api/cinema/library/models/Movie.java | 14 ++++ .../api/cinema/library/models/Screening.java | 23 +++++- .../api/cinema/library/models/Ticket.java | 17 +++- .../payload/request/ScreeningRequest.java | 11 +++ .../repository/CustomerRepository.java | 7 ++ .../library/repository/MovieRepository.java | 7 ++ .../repository/ScreeningRepository.java | 10 +++ .../library/repository/TicketRepository.java | 11 +++ 14 files changed, 313 insertions(+), 5 deletions(-) create mode 100644 src/main/java/com/booleanuk/api/cinema/Main.java create mode 100644 src/main/java/com/booleanuk/api/cinema/library/SecurityConfig.java create mode 100644 src/main/java/com/booleanuk/api/cinema/library/controllers/CustomerController.java create mode 100644 src/main/java/com/booleanuk/api/cinema/library/controllers/MovieController.java create mode 100644 src/main/java/com/booleanuk/api/cinema/library/controllers/ScreeningController.java create mode 100644 src/main/java/com/booleanuk/api/cinema/library/payload/request/ScreeningRequest.java create mode 100644 src/main/java/com/booleanuk/api/cinema/library/repository/CustomerRepository.java create mode 100644 src/main/java/com/booleanuk/api/cinema/library/repository/MovieRepository.java create mode 100644 src/main/java/com/booleanuk/api/cinema/library/repository/ScreeningRepository.java create mode 100644 src/main/java/com/booleanuk/api/cinema/library/repository/TicketRepository.java diff --git a/src/main/java/com/booleanuk/api/cinema/Main.java b/src/main/java/com/booleanuk/api/cinema/Main.java new file mode 100644 index 00000000..c4fb60ea --- /dev/null +++ b/src/main/java/com/booleanuk/api/cinema/Main.java @@ -0,0 +1,11 @@ +package com.booleanuk.api.cinema; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class Main { + public static void main(String[] args) { + SpringApplication.run(Main.class, args); + } +} diff --git a/src/main/java/com/booleanuk/api/cinema/library/SecurityConfig.java b/src/main/java/com/booleanuk/api/cinema/library/SecurityConfig.java new file mode 100644 index 00000000..b7e52441 --- /dev/null +++ b/src/main/java/com/booleanuk/api/cinema/library/SecurityConfig.java @@ -0,0 +1,17 @@ +package com.booleanuk.api.cinema.library; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.web.SecurityFilterChain; + +@Configuration +public class SecurityConfig { + @Bean + public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { + http + .csrf(csrf -> csrf.disable()) + .authorizeHttpRequests(auth -> auth.anyRequest().permitAll()); // alle requests toestaan + return http.build(); + } +} diff --git a/src/main/java/com/booleanuk/api/cinema/library/controllers/CustomerController.java b/src/main/java/com/booleanuk/api/cinema/library/controllers/CustomerController.java new file mode 100644 index 00000000..81bb5edf --- /dev/null +++ b/src/main/java/com/booleanuk/api/cinema/library/controllers/CustomerController.java @@ -0,0 +1,78 @@ +package com.booleanuk.api.cinema.library.controllers; + +import com.booleanuk.api.cinema.library.models.Customer; +import com.booleanuk.api.cinema.library.models.Screening; +import com.booleanuk.api.cinema.library.models.Ticket; +import com.booleanuk.api.cinema.library.repository.CustomerRepository; +import com.booleanuk.api.cinema.library.repository.ScreeningRepository; +import com.booleanuk.api.cinema.library.repository.TicketRepository; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.*; + +import java.util.List; +import java.util.Optional; + +@RestController +@RequestMapping("/customers") +public class CustomerController { + + @Autowired + private CustomerRepository customerRepository; + + @Autowired + private ScreeningRepository screeningRepository; + + @Autowired + private TicketRepository ticketRepository; + + @GetMapping + public List getAllCustomers() { + return customerRepository.findAll(); + } + + @GetMapping("/{id}") + public Optional getCustomerById(@PathVariable int id) { + return customerRepository.findById(id); + } + + @PostMapping + public Customer createCustomer(@RequestBody Customer customer) { + return customerRepository.save(customer); + } + + @PutMapping("/{id}") + public Customer updateCustomer(@PathVariable int id, @RequestBody Customer updatedCustomer) { + return customerRepository.findById(id).map(customer -> { + customer.setName(updatedCustomer.getName()); + customer.setEmail(updatedCustomer.getEmail()); + customer.setPhone(updatedCustomer.getPhone()); + return customerRepository.save(customer); + }).orElseGet(() -> { + updatedCustomer.setId(id); + return customerRepository.save(updatedCustomer); + }); + } + + @DeleteMapping("/{id}") + public void deleteCustomer(@PathVariable int id) { + customerRepository.deleteById(id); + } + + @PostMapping("/{customerId}/tickets") + public Ticket createTicket(@PathVariable int customerId, + @RequestParam int screeningId, + @RequestParam int numSeats) { + + Customer customer = customerRepository.findById(customerId) + .orElseThrow(() -> new RuntimeException("Customer not found")); + Screening screening = screeningRepository.findById(screeningId) + .orElseThrow(() -> new RuntimeException("Screening not found")); + + Ticket ticket = new Ticket(); + ticket.setCustomer(customer); + ticket.setScreening(screening); + ticket.setNumSeats(numSeats); + + return ticketRepository.save(ticket); + } +} diff --git a/src/main/java/com/booleanuk/api/cinema/library/controllers/MovieController.java b/src/main/java/com/booleanuk/api/cinema/library/controllers/MovieController.java new file mode 100644 index 00000000..33b8cafe --- /dev/null +++ b/src/main/java/com/booleanuk/api/cinema/library/controllers/MovieController.java @@ -0,0 +1,51 @@ +package com.booleanuk.api.cinema.library.controllers; + +import com.booleanuk.api.cinema.library.models.Movie; +import com.booleanuk.api.cinema.library.repository.MovieRepository; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.*; + +import java.util.List; +import java.util.Optional; + +@RestController +@RequestMapping("/movies") +public class MovieController { + + @Autowired + private MovieRepository movieRepository; + + @GetMapping + public List getAllMovies() { + return movieRepository.findAll(); + } + + @GetMapping("/{id}") + public Optional getMovieById(@PathVariable int id) { + return movieRepository.findById(id); + } + + @PostMapping + public Movie createMovie(@RequestBody Movie movie) { + return movieRepository.save(movie); + } + + @PutMapping("/{id}") + public Movie updateMovie(@PathVariable int id, @RequestBody Movie updatedMovie) { + return movieRepository.findById(id).map(movie -> { + movie.setTitle(updatedMovie.getTitle()); + movie.setRating(updatedMovie.getRating()); + movie.setDescription(updatedMovie.getDescription()); + movie.setRuntimeMins(updatedMovie.getRuntimeMins()); + return movieRepository.save(movie); + }).orElseGet(() -> { + updatedMovie.setId(id); + return movieRepository.save(updatedMovie); + }); + } + + @DeleteMapping("/{id}") + public void deleteMovie(@PathVariable int id) { + movieRepository.deleteById(id); + } +} diff --git a/src/main/java/com/booleanuk/api/cinema/library/controllers/ScreeningController.java b/src/main/java/com/booleanuk/api/cinema/library/controllers/ScreeningController.java new file mode 100644 index 00000000..7d4b665d --- /dev/null +++ b/src/main/java/com/booleanuk/api/cinema/library/controllers/ScreeningController.java @@ -0,0 +1,50 @@ +package com.booleanuk.api.cinema.library.controllers; + +import com.booleanuk.api.cinema.library.models.Movie; +import com.booleanuk.api.cinema.library.models.Screening; +import com.booleanuk.api.cinema.library.repository.MovieRepository; +import com.booleanuk.api.cinema.library.repository.ScreeningRepository; +import com.booleanuk.api.cinema.library.payload.request.ScreeningRequest; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +@RestController +@RequestMapping("/movies/{movieId}/screenings") +public class ScreeningController { + + @Autowired + private ScreeningRepository screeningRepository; + + @Autowired + private MovieRepository movieRepository; + + @GetMapping + public ResponseEntity> getScreeningsByMovie(@PathVariable Integer movieId) { + Movie movie = movieRepository.findById(movieId) + .orElseThrow(() -> new RuntimeException("Movie not found")); + return ResponseEntity.ok(movie.getScreenings()); + } + + @PostMapping + public ResponseEntity createScreening( + @PathVariable Integer movieId, + @RequestBody ScreeningRequest request) { + + Movie movie = movieRepository.findById(movieId) + .orElseThrow(() -> new RuntimeException("Movie not found")); + + Screening screening = Screening.builder() + .movie(movie) + .screenNumber(request.getScreenNumber()) + .capacity(request.getCapacity()) + .startsAt(request.getStartsAt()) + .build(); + + Screening savedScreening = screeningRepository.save(screening); + + return ResponseEntity.status(201).body(savedScreening); + } +} diff --git a/src/main/java/com/booleanuk/api/cinema/library/models/Customer.java b/src/main/java/com/booleanuk/api/cinema/library/models/Customer.java index ac395c8c..0e485d8f 100644 --- a/src/main/java/com/booleanuk/api/cinema/library/models/Customer.java +++ b/src/main/java/com/booleanuk/api/cinema/library/models/Customer.java @@ -25,4 +25,15 @@ public class Customer { @OneToMany(mappedBy = "customer", cascade = CascadeType.ALL) private List tickets; + + @PrePersist + protected void onCreate() { + this.createdAt = LocalDateTime.now(); + this.updatedAt = LocalDateTime.now(); + } + + @PreUpdate + protected void onUpdate() { + this.updatedAt = LocalDateTime.now(); + } } diff --git a/src/main/java/com/booleanuk/api/cinema/library/models/Movie.java b/src/main/java/com/booleanuk/api/cinema/library/models/Movie.java index 883f15ad..b916d1f1 100644 --- a/src/main/java/com/booleanuk/api/cinema/library/models/Movie.java +++ b/src/main/java/com/booleanuk/api/cinema/library/models/Movie.java @@ -1,5 +1,6 @@ package com.booleanuk.api.cinema.library.models; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import jakarta.persistence.*; import lombok.*; import java.time.LocalDateTime; @@ -26,5 +27,18 @@ public class Movie { private LocalDateTime updatedAt; @OneToMany(mappedBy = "movie", cascade = CascadeType.ALL) + @JsonIgnoreProperties("movie") private List screenings; + + + @PrePersist + protected void onCreate() { + this.createdAt = LocalDateTime.now(); + this.updatedAt = LocalDateTime.now(); + } + + @PreUpdate + protected void onUpdate() { + this.updatedAt = LocalDateTime.now(); + } } diff --git a/src/main/java/com/booleanuk/api/cinema/library/models/Screening.java b/src/main/java/com/booleanuk/api/cinema/library/models/Screening.java index 1a357c3b..8aa6e683 100644 --- a/src/main/java/com/booleanuk/api/cinema/library/models/Screening.java +++ b/src/main/java/com/booleanuk/api/cinema/library/models/Screening.java @@ -1,5 +1,6 @@ package com.booleanuk.api.cinema.library.models; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import jakarta.persistence.*; import lombok.*; import java.time.LocalDateTime; @@ -14,14 +15,32 @@ public class Screening { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) - private Long id; + private Integer id; - private LocalDateTime startTime; + private Integer screenNumber; + private LocalDateTime startsAt; + private Integer capacity; + + private LocalDateTime createdAt; + private LocalDateTime updatedAt; @ManyToOne @JoinColumn(name = "movie_id") + @JsonIgnoreProperties("screenings") private Movie movie; + @OneToMany(mappedBy = "screening", cascade = CascadeType.ALL) private List tickets; + + @PrePersist + protected void onCreate() { + this.createdAt = LocalDateTime.now(); + this.updatedAt = LocalDateTime.now(); + } + + @PreUpdate + protected void onUpdate() { + this.updatedAt = LocalDateTime.now(); + } } diff --git a/src/main/java/com/booleanuk/api/cinema/library/models/Ticket.java b/src/main/java/com/booleanuk/api/cinema/library/models/Ticket.java index 9ca6037d..fa365bad 100644 --- a/src/main/java/com/booleanuk/api/cinema/library/models/Ticket.java +++ b/src/main/java/com/booleanuk/api/cinema/library/models/Ticket.java @@ -15,6 +15,11 @@ public class Ticket { @GeneratedValue(strategy = GenerationType.IDENTITY) private Integer id; + private Integer numSeats; + + private LocalDateTime createdAt; + private LocalDateTime updatedAt; + @ManyToOne @JoinColumn(name = "customer_id") private Customer customer; @@ -23,8 +28,14 @@ public class Ticket { @JoinColumn(name = "screening_id") private Screening screening; - private Integer numSeats; + @PrePersist + protected void onCreate() { + this.createdAt = LocalDateTime.now(); + this.updatedAt = LocalDateTime.now(); + } - private LocalDateTime createdAt; - private LocalDateTime updatedAt; + @PreUpdate + protected void onUpdate() { + this.updatedAt = LocalDateTime.now(); + } } diff --git a/src/main/java/com/booleanuk/api/cinema/library/payload/request/ScreeningRequest.java b/src/main/java/com/booleanuk/api/cinema/library/payload/request/ScreeningRequest.java new file mode 100644 index 00000000..54a6eb34 --- /dev/null +++ b/src/main/java/com/booleanuk/api/cinema/library/payload/request/ScreeningRequest.java @@ -0,0 +1,11 @@ +package com.booleanuk.api.cinema.library.payload.request; + +import lombok.Data; +import java.time.LocalDateTime; + +@Data +public class ScreeningRequest { + private int screenNumber; + private int capacity; + private LocalDateTime startsAt; +} \ No newline at end of file diff --git a/src/main/java/com/booleanuk/api/cinema/library/repository/CustomerRepository.java b/src/main/java/com/booleanuk/api/cinema/library/repository/CustomerRepository.java new file mode 100644 index 00000000..f44439ca --- /dev/null +++ b/src/main/java/com/booleanuk/api/cinema/library/repository/CustomerRepository.java @@ -0,0 +1,7 @@ +package com.booleanuk.api.cinema.library.repository; + +import com.booleanuk.api.cinema.library.models.Customer; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface CustomerRepository extends JpaRepository { +} diff --git a/src/main/java/com/booleanuk/api/cinema/library/repository/MovieRepository.java b/src/main/java/com/booleanuk/api/cinema/library/repository/MovieRepository.java new file mode 100644 index 00000000..5718ed5f --- /dev/null +++ b/src/main/java/com/booleanuk/api/cinema/library/repository/MovieRepository.java @@ -0,0 +1,7 @@ +package com.booleanuk.api.cinema.library.repository; + +import com.booleanuk.api.cinema.library.models.Movie; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface MovieRepository extends JpaRepository { +} diff --git a/src/main/java/com/booleanuk/api/cinema/library/repository/ScreeningRepository.java b/src/main/java/com/booleanuk/api/cinema/library/repository/ScreeningRepository.java new file mode 100644 index 00000000..165996c5 --- /dev/null +++ b/src/main/java/com/booleanuk/api/cinema/library/repository/ScreeningRepository.java @@ -0,0 +1,10 @@ +package com.booleanuk.api.cinema.library.repository; + +import com.booleanuk.api.cinema.library.models.Screening; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.List; + +public interface ScreeningRepository extends JpaRepository { + List findByMovieId(int movieId); +} diff --git a/src/main/java/com/booleanuk/api/cinema/library/repository/TicketRepository.java b/src/main/java/com/booleanuk/api/cinema/library/repository/TicketRepository.java new file mode 100644 index 00000000..add86fba --- /dev/null +++ b/src/main/java/com/booleanuk/api/cinema/library/repository/TicketRepository.java @@ -0,0 +1,11 @@ +package com.booleanuk.api.cinema.library.repository; + +import com.booleanuk.api.cinema.library.models.Ticket; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.List; + +public interface TicketRepository extends JpaRepository { + List findByCustomerId(int customerId); + List findByScreeningId(int screeningId); +} From ddb0aae934ebbfa558647954a1cd8e28b4f1e218 Mon Sep 17 00:00:00 2001 From: Your Name Date: Fri, 22 Aug 2025 11:13:22 +0200 Subject: [PATCH 4/7] payload(request response) --- .../controllers/CustomerController.java | 76 +++++++++++-------- .../library/controllers/MovieController.java | 65 +++++++++++----- .../controllers/ScreeningController.java | 30 ++++++-- .../payload/request/CustomerRequest.java | 10 +++ .../library/payload/request/MovieRequest.java | 11 +++ .../payload/response/CustomerResponse.java | 17 +++++ .../payload/response/MovieResponse.java | 18 +++++ .../payload/response/ScreeningResponse.java | 17 +++++ 8 files changed, 187 insertions(+), 57 deletions(-) create mode 100644 src/main/java/com/booleanuk/api/cinema/library/payload/request/CustomerRequest.java create mode 100644 src/main/java/com/booleanuk/api/cinema/library/payload/request/MovieRequest.java create mode 100644 src/main/java/com/booleanuk/api/cinema/library/payload/response/CustomerResponse.java create mode 100644 src/main/java/com/booleanuk/api/cinema/library/payload/response/MovieResponse.java create mode 100644 src/main/java/com/booleanuk/api/cinema/library/payload/response/ScreeningResponse.java diff --git a/src/main/java/com/booleanuk/api/cinema/library/controllers/CustomerController.java b/src/main/java/com/booleanuk/api/cinema/library/controllers/CustomerController.java index 81bb5edf..3166cd55 100644 --- a/src/main/java/com/booleanuk/api/cinema/library/controllers/CustomerController.java +++ b/src/main/java/com/booleanuk/api/cinema/library/controllers/CustomerController.java @@ -3,14 +3,17 @@ import com.booleanuk.api.cinema.library.models.Customer; import com.booleanuk.api.cinema.library.models.Screening; import com.booleanuk.api.cinema.library.models.Ticket; +import com.booleanuk.api.cinema.library.payload.request.CustomerRequest; +import com.booleanuk.api.cinema.library.payload.response.CustomerResponse; import com.booleanuk.api.cinema.library.repository.CustomerRepository; import com.booleanuk.api.cinema.library.repository.ScreeningRepository; import com.booleanuk.api.cinema.library.repository.TicketRepository; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; import java.util.List; -import java.util.Optional; +import java.util.stream.Collectors; @RestController @RequestMapping("/customers") @@ -26,53 +29,60 @@ public class CustomerController { private TicketRepository ticketRepository; @GetMapping - public List getAllCustomers() { - return customerRepository.findAll(); + public List getAllCustomers() { + return customerRepository.findAll().stream() + .map(this::toResponse) + .collect(Collectors.toList()); } @GetMapping("/{id}") - public Optional getCustomerById(@PathVariable int id) { - return customerRepository.findById(id); + public ResponseEntity getCustomerById(@PathVariable int id) { + return customerRepository.findById(id) + .map(customer -> ResponseEntity.ok(toResponse(customer))) + .orElse(ResponseEntity.notFound().build()); } @PostMapping - public Customer createCustomer(@RequestBody Customer customer) { - return customerRepository.save(customer); + public ResponseEntity createCustomer(@RequestBody CustomerRequest request) { + Customer customer = new Customer(); + customer.setName(request.getName()); + customer.setEmail(request.getEmail()); + customer.setPhone(request.getPhone()); + + Customer saved = customerRepository.save(customer); + return ResponseEntity.status(201).body(toResponse(saved)); } @PutMapping("/{id}") - public Customer updateCustomer(@PathVariable int id, @RequestBody Customer updatedCustomer) { + public ResponseEntity updateCustomer(@PathVariable int id, @RequestBody CustomerRequest request) { return customerRepository.findById(id).map(customer -> { - customer.setName(updatedCustomer.getName()); - customer.setEmail(updatedCustomer.getEmail()); - customer.setPhone(updatedCustomer.getPhone()); - return customerRepository.save(customer); - }).orElseGet(() -> { - updatedCustomer.setId(id); - return customerRepository.save(updatedCustomer); - }); + customer.setName(request.getName()); + customer.setEmail(request.getEmail()); + customer.setPhone(request.getPhone()); + + Customer updated = customerRepository.save(customer); + return ResponseEntity.ok(toResponse(updated)); + }).orElse(ResponseEntity.notFound().build()); } @DeleteMapping("/{id}") - public void deleteCustomer(@PathVariable int id) { + public ResponseEntity deleteCustomer(@PathVariable int id) { + if (!customerRepository.existsById(id)) { + return ResponseEntity.notFound().build(); + } customerRepository.deleteById(id); + return ResponseEntity.noContent().build(); } - @PostMapping("/{customerId}/tickets") - public Ticket createTicket(@PathVariable int customerId, - @RequestParam int screeningId, - @RequestParam int numSeats) { - - Customer customer = customerRepository.findById(customerId) - .orElseThrow(() -> new RuntimeException("Customer not found")); - Screening screening = screeningRepository.findById(screeningId) - .orElseThrow(() -> new RuntimeException("Screening not found")); - - Ticket ticket = new Ticket(); - ticket.setCustomer(customer); - ticket.setScreening(screening); - ticket.setNumSeats(numSeats); - - return ticketRepository.save(ticket); + private CustomerResponse toResponse(Customer customer) { + return CustomerResponse.builder() + .id(customer.getId()) + .name(customer.getName()) + .email(customer.getEmail()) + .phone(customer.getPhone()) + .createdAt(customer.getCreatedAt()) + .updatedAt(customer.getUpdatedAt()) + .build(); } + } diff --git a/src/main/java/com/booleanuk/api/cinema/library/controllers/MovieController.java b/src/main/java/com/booleanuk/api/cinema/library/controllers/MovieController.java index 33b8cafe..6be00520 100644 --- a/src/main/java/com/booleanuk/api/cinema/library/controllers/MovieController.java +++ b/src/main/java/com/booleanuk/api/cinema/library/controllers/MovieController.java @@ -1,12 +1,15 @@ package com.booleanuk.api.cinema.library.controllers; import com.booleanuk.api.cinema.library.models.Movie; +import com.booleanuk.api.cinema.library.payload.request.MovieRequest; +import com.booleanuk.api.cinema.library.payload.response.MovieResponse; import com.booleanuk.api.cinema.library.repository.MovieRepository; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; import java.util.List; -import java.util.Optional; +import java.util.stream.Collectors; @RestController @RequestMapping("/movies") @@ -16,36 +19,62 @@ public class MovieController { private MovieRepository movieRepository; @GetMapping - public List getAllMovies() { - return movieRepository.findAll(); + public List getAllMovies() { + return movieRepository.findAll().stream() + .map(this::toResponse) + .collect(Collectors.toList()); } @GetMapping("/{id}") - public Optional getMovieById(@PathVariable int id) { - return movieRepository.findById(id); + public ResponseEntity getMovieById(@PathVariable int id) { + return movieRepository.findById(id) + .map(movie -> ResponseEntity.ok(toResponse(movie))) + .orElse(ResponseEntity.notFound().build()); } @PostMapping - public Movie createMovie(@RequestBody Movie movie) { - return movieRepository.save(movie); + public ResponseEntity createMovie(@RequestBody MovieRequest request) { + Movie movie = new Movie(); + movie.setTitle(request.getTitle()); + movie.setRating(request.getRating()); + movie.setDescription(request.getDescription()); + movie.setRuntimeMins(request.getRuntimeMins()); + + Movie saved = movieRepository.save(movie); + return ResponseEntity.status(201).body(toResponse(saved)); } @PutMapping("/{id}") - public Movie updateMovie(@PathVariable int id, @RequestBody Movie updatedMovie) { + public ResponseEntity updateMovie(@PathVariable int id, @RequestBody MovieRequest request) { return movieRepository.findById(id).map(movie -> { - movie.setTitle(updatedMovie.getTitle()); - movie.setRating(updatedMovie.getRating()); - movie.setDescription(updatedMovie.getDescription()); - movie.setRuntimeMins(updatedMovie.getRuntimeMins()); - return movieRepository.save(movie); - }).orElseGet(() -> { - updatedMovie.setId(id); - return movieRepository.save(updatedMovie); - }); + movie.setTitle(request.getTitle()); + movie.setRating(request.getRating()); + movie.setDescription(request.getDescription()); + movie.setRuntimeMins(request.getRuntimeMins()); + + Movie updated = movieRepository.save(movie); + return ResponseEntity.ok(toResponse(updated)); + }).orElse(ResponseEntity.notFound().build()); } @DeleteMapping("/{id}") - public void deleteMovie(@PathVariable int id) { + public ResponseEntity deleteMovie(@PathVariable int id) { + if (!movieRepository.existsById(id)) { + return ResponseEntity.notFound().build(); + } movieRepository.deleteById(id); + return ResponseEntity.noContent().build(); + } + + private MovieResponse toResponse(Movie movie) { + return MovieResponse.builder() + .id(movie.getId()) + .title(movie.getTitle()) + .rating(movie.getRating()) + .description(movie.getDescription()) + .runtimeMins(movie.getRuntimeMins()) + .createdAt(movie.getCreatedAt()) + .updatedAt(movie.getUpdatedAt()) + .build(); } } diff --git a/src/main/java/com/booleanuk/api/cinema/library/controllers/ScreeningController.java b/src/main/java/com/booleanuk/api/cinema/library/controllers/ScreeningController.java index 7d4b665d..ffcb253b 100644 --- a/src/main/java/com/booleanuk/api/cinema/library/controllers/ScreeningController.java +++ b/src/main/java/com/booleanuk/api/cinema/library/controllers/ScreeningController.java @@ -2,14 +2,16 @@ import com.booleanuk.api.cinema.library.models.Movie; import com.booleanuk.api.cinema.library.models.Screening; +import com.booleanuk.api.cinema.library.payload.request.ScreeningRequest; +import com.booleanuk.api.cinema.library.payload.response.ScreeningResponse; import com.booleanuk.api.cinema.library.repository.MovieRepository; import com.booleanuk.api.cinema.library.repository.ScreeningRepository; -import com.booleanuk.api.cinema.library.payload.request.ScreeningRequest; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; import java.util.List; +import java.util.stream.Collectors; @RestController @RequestMapping("/movies/{movieId}/screenings") @@ -22,14 +24,19 @@ public class ScreeningController { private MovieRepository movieRepository; @GetMapping - public ResponseEntity> getScreeningsByMovie(@PathVariable Integer movieId) { + public ResponseEntity> getScreeningsByMovie(@PathVariable Integer movieId) { Movie movie = movieRepository.findById(movieId) .orElseThrow(() -> new RuntimeException("Movie not found")); - return ResponseEntity.ok(movie.getScreenings()); + + List responses = movie.getScreenings().stream() + .map(this::toResponse) + .collect(Collectors.toList()); + + return ResponseEntity.ok(responses); } @PostMapping - public ResponseEntity createScreening( + public ResponseEntity createScreening( @PathVariable Integer movieId, @RequestBody ScreeningRequest request) { @@ -43,8 +50,19 @@ public ResponseEntity createScreening( .startsAt(request.getStartsAt()) .build(); - Screening savedScreening = screeningRepository.save(screening); + Screening saved = screeningRepository.save(screening); - return ResponseEntity.status(201).body(savedScreening); + return ResponseEntity.status(201).body(toResponse(saved)); + } + + private ScreeningResponse toResponse(Screening screening) { + return ScreeningResponse.builder() + .id(screening.getId()) + .screenNumber(screening.getScreenNumber()) + .capacity(screening.getCapacity()) + .startsAt(screening.getStartsAt()) + .createdAt(screening.getCreatedAt()) + .updatedAt(screening.getUpdatedAt()) + .build(); } } diff --git a/src/main/java/com/booleanuk/api/cinema/library/payload/request/CustomerRequest.java b/src/main/java/com/booleanuk/api/cinema/library/payload/request/CustomerRequest.java new file mode 100644 index 00000000..c5151684 --- /dev/null +++ b/src/main/java/com/booleanuk/api/cinema/library/payload/request/CustomerRequest.java @@ -0,0 +1,10 @@ +package com.booleanuk.api.cinema.library.payload.request; + +import lombok.Data; + +@Data +public class CustomerRequest { + private String name; + private String email; + private String phone; +} diff --git a/src/main/java/com/booleanuk/api/cinema/library/payload/request/MovieRequest.java b/src/main/java/com/booleanuk/api/cinema/library/payload/request/MovieRequest.java new file mode 100644 index 00000000..e9bbe75b --- /dev/null +++ b/src/main/java/com/booleanuk/api/cinema/library/payload/request/MovieRequest.java @@ -0,0 +1,11 @@ +package com.booleanuk.api.cinema.library.payload.request; + +import lombok.Data; + +@Data +public class MovieRequest { + private String title; + private String rating; + private String description; + private int runtimeMins; +} diff --git a/src/main/java/com/booleanuk/api/cinema/library/payload/response/CustomerResponse.java b/src/main/java/com/booleanuk/api/cinema/library/payload/response/CustomerResponse.java new file mode 100644 index 00000000..42c3ea32 --- /dev/null +++ b/src/main/java/com/booleanuk/api/cinema/library/payload/response/CustomerResponse.java @@ -0,0 +1,17 @@ +package com.booleanuk.api.cinema.library.payload.response; + +import lombok.Builder; +import lombok.Data; + +import java.time.LocalDateTime; + +@Data +@Builder +public class CustomerResponse { + private int id; + private String name; + private String email; + private String phone; + private LocalDateTime createdAt; + private LocalDateTime updatedAt; +} diff --git a/src/main/java/com/booleanuk/api/cinema/library/payload/response/MovieResponse.java b/src/main/java/com/booleanuk/api/cinema/library/payload/response/MovieResponse.java new file mode 100644 index 00000000..865577b7 --- /dev/null +++ b/src/main/java/com/booleanuk/api/cinema/library/payload/response/MovieResponse.java @@ -0,0 +1,18 @@ +package com.booleanuk.api.cinema.library.payload.response; + +import lombok.Builder; +import lombok.Data; + +import java.time.LocalDateTime; + +@Data +@Builder +public class MovieResponse { + private int id; + private String title; + private String rating; + private String description; + private int runtimeMins; + private LocalDateTime createdAt; + private LocalDateTime updatedAt; +} diff --git a/src/main/java/com/booleanuk/api/cinema/library/payload/response/ScreeningResponse.java b/src/main/java/com/booleanuk/api/cinema/library/payload/response/ScreeningResponse.java new file mode 100644 index 00000000..bcd42d73 --- /dev/null +++ b/src/main/java/com/booleanuk/api/cinema/library/payload/response/ScreeningResponse.java @@ -0,0 +1,17 @@ +package com.booleanuk.api.cinema.library.payload.response; + +import lombok.Builder; +import lombok.Data; + +import java.time.LocalDateTime; + +@Data +@Builder +public class ScreeningResponse { + private int id; + private int screenNumber; + private int capacity; + private LocalDateTime startsAt; + private LocalDateTime createdAt; + private LocalDateTime updatedAt; +} From 7c0aea98931eaab5b430d5ba7d99da46a8416aa2 Mon Sep 17 00:00:00 2001 From: Your Name Date: Fri, 22 Aug 2025 12:48:30 +0200 Subject: [PATCH 5/7] security --- build.gradle | 6 +- .../api/cinema/library/SecurityConfig.java | 27 ++++++++- .../controllers/AuthenticationController.java | 37 ++++++++++++ .../api/cinema/library/models/Role.java | 6 ++ .../api/cinema/library/models/User.java | 40 +++++++++++++ .../library/payload/request/AuthRequest.java | 9 +++ .../payload/response/AuthResponse.java | 10 ++++ .../library/repository/UserRepository.java | 10 ++++ .../security/jwt/JwtRequestFilter.java | 53 ++++++++++++++++ .../cinema/library/security/jwt/JwtUtil.java | 60 +++++++++++++++++++ 10 files changed, 254 insertions(+), 4 deletions(-) create mode 100644 src/main/java/com/booleanuk/api/cinema/library/controllers/AuthenticationController.java create mode 100644 src/main/java/com/booleanuk/api/cinema/library/models/Role.java create mode 100644 src/main/java/com/booleanuk/api/cinema/library/models/User.java create mode 100644 src/main/java/com/booleanuk/api/cinema/library/payload/request/AuthRequest.java create mode 100644 src/main/java/com/booleanuk/api/cinema/library/payload/response/AuthResponse.java create mode 100644 src/main/java/com/booleanuk/api/cinema/library/repository/UserRepository.java create mode 100644 src/main/java/com/booleanuk/api/cinema/library/security/jwt/JwtRequestFilter.java create mode 100644 src/main/java/com/booleanuk/api/cinema/library/security/jwt/JwtUtil.java diff --git a/build.gradle b/build.gradle index 4c7a5502..3cd4f0e5 100644 --- a/build.gradle +++ b/build.gradle @@ -27,9 +27,9 @@ dependencies { implementation 'org.springframework.boot:spring-boot-starter-data-jpa' implementation 'org.springframework.boot:spring-boot-starter-security' implementation 'org.springframework.boot:spring-boot-starter-web' - implementation 'io.jsonwebtoken:jjwt-api:0.12.6' - implementation 'io.jsonwebtoken:jjwt-impl:0.12.6' - runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.12.6' + implementation 'io.jsonwebtoken:jjwt-api:0.11.5' + implementation 'io.jsonwebtoken:jjwt-impl:0.11.5' + runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.11.5' compileOnly 'org.projectlombok:lombok' developmentOnly 'org.springframework.boot:spring-boot-devtools' diff --git a/src/main/java/com/booleanuk/api/cinema/library/SecurityConfig.java b/src/main/java/com/booleanuk/api/cinema/library/SecurityConfig.java index b7e52441..ce7af8c4 100644 --- a/src/main/java/com/booleanuk/api/cinema/library/SecurityConfig.java +++ b/src/main/java/com/booleanuk/api/cinema/library/SecurityConfig.java @@ -1,17 +1,42 @@ package com.booleanuk.api.cinema.library; +import com.booleanuk.api.cinema.library.security.jwt.JwtRequestFilter; +import lombok.RequiredArgsConstructor; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; 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.web.SecurityFilterChain; +import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; @Configuration +@RequiredArgsConstructor public class SecurityConfig { + + private final JwtRequestFilter jwtRequestFilter; + @Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http .csrf(csrf -> csrf.disable()) - .authorizeHttpRequests(auth -> auth.anyRequest().permitAll()); // alle requests toestaan + .authorizeHttpRequests(auth -> auth + .requestMatchers("/auth/**").permitAll() + .requestMatchers("/movies/**").hasAnyAuthority("ADMIN", "CUSTOMER") + .requestMatchers("/movies/**/screenings/**").hasAnyAuthority("ADMIN", "CUSTOMER") + .anyRequest().authenticated() + ) + .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)); + + http.addFilterBefore(jwtRequestFilter, UsernamePasswordAuthenticationFilter.class); + return http.build(); } + + @Bean + public BCryptPasswordEncoder passwordEncoder() { + return new BCryptPasswordEncoder(); + } } diff --git a/src/main/java/com/booleanuk/api/cinema/library/controllers/AuthenticationController.java b/src/main/java/com/booleanuk/api/cinema/library/controllers/AuthenticationController.java new file mode 100644 index 00000000..246f9eda --- /dev/null +++ b/src/main/java/com/booleanuk/api/cinema/library/controllers/AuthenticationController.java @@ -0,0 +1,37 @@ +package com.booleanuk.api.cinema.library.controllers; + +import com.booleanuk.api.cinema.library.models.User; +import com.booleanuk.api.cinema.library.payload.request.AuthRequest; +import com.booleanuk.api.cinema.library.payload.response.AuthResponse; +import com.booleanuk.api.cinema.library.repository.UserRepository; +import com.booleanuk.api.cinema.library.security.jwt.JwtUtil; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +@RestController +@RequestMapping("/auth") +public class AuthenticationController { + + @Autowired + private UserRepository userRepository; + + @Autowired + private JwtUtil jwtUtil; + + private BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder(); + + @PostMapping("/login") + public ResponseEntity login(@RequestBody AuthRequest request) { + User user = userRepository.findByUsername(request.getUsername()) + .orElseThrow(() -> new RuntimeException("User not found")); + + if (!passwordEncoder.matches(request.getPassword(), user.getPassword())) { + return ResponseEntity.status(401).body("Invalid credentials"); + } + + String token = jwtUtil.generateToken(user.getUsername(), user.getRole().name()); + return ResponseEntity.ok(new AuthResponse(token)); + } +} diff --git a/src/main/java/com/booleanuk/api/cinema/library/models/Role.java b/src/main/java/com/booleanuk/api/cinema/library/models/Role.java new file mode 100644 index 00000000..ea743c1d --- /dev/null +++ b/src/main/java/com/booleanuk/api/cinema/library/models/Role.java @@ -0,0 +1,6 @@ +package com.booleanuk.api.cinema.library.models; + +public enum Role { + ADMIN, + CUSTOMER +} diff --git a/src/main/java/com/booleanuk/api/cinema/library/models/User.java b/src/main/java/com/booleanuk/api/cinema/library/models/User.java new file mode 100644 index 00000000..b44375b7 --- /dev/null +++ b/src/main/java/com/booleanuk/api/cinema/library/models/User.java @@ -0,0 +1,40 @@ +package com.booleanuk.api.cinema.library.models; + +import jakarta.persistence.*; +import lombok.*; +import java.time.LocalDateTime; + +@Entity +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class User { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Integer id; + + @Column(unique = true, nullable = false) + private String username; + + @Column(nullable = false) + private String password; // hashed password + + @Enumerated(EnumType.STRING) + private Role role; + + private LocalDateTime createdAt; + private LocalDateTime updatedAt; + + @PrePersist + protected void onCreate() { + this.createdAt = LocalDateTime.now(); + this.updatedAt = LocalDateTime.now(); + } + + @PreUpdate + protected void onUpdate() { + this.updatedAt = LocalDateTime.now(); + } +} diff --git a/src/main/java/com/booleanuk/api/cinema/library/payload/request/AuthRequest.java b/src/main/java/com/booleanuk/api/cinema/library/payload/request/AuthRequest.java new file mode 100644 index 00000000..4275d874 --- /dev/null +++ b/src/main/java/com/booleanuk/api/cinema/library/payload/request/AuthRequest.java @@ -0,0 +1,9 @@ +package com.booleanuk.api.cinema.library.payload.request; + +import lombok.Data; + +@Data +public class AuthRequest { + private String username; + private String password; +} diff --git a/src/main/java/com/booleanuk/api/cinema/library/payload/response/AuthResponse.java b/src/main/java/com/booleanuk/api/cinema/library/payload/response/AuthResponse.java new file mode 100644 index 00000000..60378467 --- /dev/null +++ b/src/main/java/com/booleanuk/api/cinema/library/payload/response/AuthResponse.java @@ -0,0 +1,10 @@ +package com.booleanuk.api.cinema.library.payload.response; + +import lombok.AllArgsConstructor; +import lombok.Data; + +@Data +@AllArgsConstructor +public class AuthResponse { + private String token; +} diff --git a/src/main/java/com/booleanuk/api/cinema/library/repository/UserRepository.java b/src/main/java/com/booleanuk/api/cinema/library/repository/UserRepository.java new file mode 100644 index 00000000..d8065913 --- /dev/null +++ b/src/main/java/com/booleanuk/api/cinema/library/repository/UserRepository.java @@ -0,0 +1,10 @@ +package com.booleanuk.api.cinema.library.repository; + +import com.booleanuk.api.cinema.library.models.User; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.Optional; + +public interface UserRepository extends JpaRepository { + Optional findByUsername(String username); +} diff --git a/src/main/java/com/booleanuk/api/cinema/library/security/jwt/JwtRequestFilter.java b/src/main/java/com/booleanuk/api/cinema/library/security/jwt/JwtRequestFilter.java new file mode 100644 index 00000000..fecd8f92 --- /dev/null +++ b/src/main/java/com/booleanuk/api/cinema/library/security/jwt/JwtRequestFilter.java @@ -0,0 +1,53 @@ +package com.booleanuk.api.cinema.library.security.jwt; + +import com.booleanuk.api.cinema.library.models.User; +import com.booleanuk.api.cinema.library.repository.UserRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.stereotype.Component; +import org.springframework.web.filter.OncePerRequestFilter; + +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.util.Collections; + +@Component +@RequiredArgsConstructor +public class JwtRequestFilter extends OncePerRequestFilter { + + private final JwtUtil jwtUtil; + private final UserRepository userRepository; + + @Override + protected void doFilterInternal(HttpServletRequest request, + HttpServletResponse response, + FilterChain filterChain) + throws ServletException, IOException { + + final String authHeader = request.getHeader("Authorization"); + + if (authHeader != null && authHeader.startsWith("Bearer ")) { + String token = authHeader.substring(7); + if (jwtUtil.validateToken(token)) { + String username = jwtUtil.extractUsername(token); + User user = userRepository.findByUsername(username).orElse(null); + if (user != null) { + UsernamePasswordAuthenticationToken authToken = + new UsernamePasswordAuthenticationToken( + user.getUsername(), + null, + Collections.singleton(new SimpleGrantedAuthority(user.getRole().name())) + ); + SecurityContextHolder.getContext().setAuthentication(authToken); + } + } + } + + filterChain.doFilter(request, response); + } +} diff --git a/src/main/java/com/booleanuk/api/cinema/library/security/jwt/JwtUtil.java b/src/main/java/com/booleanuk/api/cinema/library/security/jwt/JwtUtil.java new file mode 100644 index 00000000..b3f15355 --- /dev/null +++ b/src/main/java/com/booleanuk/api/cinema/library/security/jwt/JwtUtil.java @@ -0,0 +1,60 @@ +package com.booleanuk.api.cinema.library.security.jwt; + +import io.jsonwebtoken.Claims; +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.SignatureAlgorithm; +import io.jsonwebtoken.JwtException; +import io.jsonwebtoken.security.Keys; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; + +import javax.crypto.SecretKey; +import java.util.Date; + +@Component +public class JwtUtil { + + private final SecretKey secretKey; + private final long expirationMs; + + public JwtUtil( + @Value("${booleanuk.app.jwtSecret}") String secret, + @Value("${booleanuk.app.jwtExpirationMs}") long expirationMs) { + this.secretKey = Keys.hmacShaKeyFor(secret.getBytes()); + this.expirationMs = expirationMs; + } + + public String generateToken(String username, String role) { + return Jwts.builder() + .setSubject(username) + .claim("role", role) + .setIssuedAt(new Date()) + .setExpiration(new Date(System.currentTimeMillis() + expirationMs)) + .signWith(secretKey, SignatureAlgorithm.HS256) + .compact(); + } + + public String extractUsername(String token) { + return extractAllClaims(token).getSubject(); + } + + public String extractRole(String token) { + return extractAllClaims(token).get("role", String.class); + } + + public boolean validateToken(String token) { + try { + extractAllClaims(token); + return true; + } catch (JwtException | IllegalArgumentException e) { + return false; + } + } + + private Claims extractAllClaims(String token) { + return Jwts.parser() + .setSigningKey(secretKey) + .parseClaimsJws(token) + .getBody(); + } +} \ No newline at end of file From eb9038b660858dceee648b3c4777be0e48386bcc Mon Sep 17 00:00:00 2001 From: Your Name Date: Fri, 22 Aug 2025 13:46:31 +0200 Subject: [PATCH 6/7] login/signup working --- .../api/cinema/library/SecurityConfig.java | 3 +- .../controllers/AuthenticationController.java | 40 ++++++++++++++----- .../api/cinema/library/models/User.java | 3 +- .../{AuthRequest.java => LoginRequest.java} | 3 +- .../payload/request/SignupRequest.java | 13 ++++++ 5 files changed, 47 insertions(+), 15 deletions(-) rename src/main/java/com/booleanuk/api/cinema/library/payload/request/{AuthRequest.java => LoginRequest.java} (83%) create mode 100644 src/main/java/com/booleanuk/api/cinema/library/payload/request/SignupRequest.java diff --git a/src/main/java/com/booleanuk/api/cinema/library/SecurityConfig.java b/src/main/java/com/booleanuk/api/cinema/library/SecurityConfig.java index ce7af8c4..dd64e37c 100644 --- a/src/main/java/com/booleanuk/api/cinema/library/SecurityConfig.java +++ b/src/main/java/com/booleanuk/api/cinema/library/SecurityConfig.java @@ -23,9 +23,8 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http .csrf(csrf -> csrf.disable()) .authorizeHttpRequests(auth -> auth - .requestMatchers("/auth/**").permitAll() + .requestMatchers("/login", "/signup").permitAll() .requestMatchers("/movies/**").hasAnyAuthority("ADMIN", "CUSTOMER") - .requestMatchers("/movies/**/screenings/**").hasAnyAuthority("ADMIN", "CUSTOMER") .anyRequest().authenticated() ) .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)); diff --git a/src/main/java/com/booleanuk/api/cinema/library/controllers/AuthenticationController.java b/src/main/java/com/booleanuk/api/cinema/library/controllers/AuthenticationController.java index 246f9eda..cc43f6ec 100644 --- a/src/main/java/com/booleanuk/api/cinema/library/controllers/AuthenticationController.java +++ b/src/main/java/com/booleanuk/api/cinema/library/controllers/AuthenticationController.java @@ -1,34 +1,52 @@ package com.booleanuk.api.cinema.library.controllers; +import com.booleanuk.api.cinema.library.models.Role; import com.booleanuk.api.cinema.library.models.User; -import com.booleanuk.api.cinema.library.payload.request.AuthRequest; +import com.booleanuk.api.cinema.library.payload.request.LoginRequest; +import com.booleanuk.api.cinema.library.payload.request.SignupRequest; import com.booleanuk.api.cinema.library.payload.response.AuthResponse; import com.booleanuk.api.cinema.library.repository.UserRepository; import com.booleanuk.api.cinema.library.security.jwt.JwtUtil; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.web.bind.annotation.*; @RestController -@RequestMapping("/auth") +@RequiredArgsConstructor public class AuthenticationController { - @Autowired - private UserRepository userRepository; + private final UserRepository userRepository; + private final JwtUtil jwtUtil; + private final BCryptPasswordEncoder passwordEncoder; + + @PostMapping("/signup") + public ResponseEntity signup(@RequestBody SignupRequest request) { + Role role; + try { + role = Role.valueOf(request.getRole().toUpperCase()); + } catch (Exception e) { + role = Role.CUSTOMER; + } - @Autowired - private JwtUtil jwtUtil; + User user = User.builder() + .username(request.getUsername()) + .password(passwordEncoder.encode(request.getPassword())) + .role(role) + .build(); - private BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder(); + userRepository.save(user); + + return ResponseEntity.ok(new AuthResponse("User registered successfully")); + } @PostMapping("/login") - public ResponseEntity login(@RequestBody AuthRequest request) { + public ResponseEntity login(@RequestBody LoginRequest request) { User user = userRepository.findByUsername(request.getUsername()) .orElseThrow(() -> new RuntimeException("User not found")); if (!passwordEncoder.matches(request.getPassword(), user.getPassword())) { - return ResponseEntity.status(401).body("Invalid credentials"); + return ResponseEntity.status(401).body(new AuthResponse("Invalid credentials")); } String token = jwtUtil.generateToken(user.getUsername(), user.getRole().name()); diff --git a/src/main/java/com/booleanuk/api/cinema/library/models/User.java b/src/main/java/com/booleanuk/api/cinema/library/models/User.java index b44375b7..a47b1569 100644 --- a/src/main/java/com/booleanuk/api/cinema/library/models/User.java +++ b/src/main/java/com/booleanuk/api/cinema/library/models/User.java @@ -9,6 +9,7 @@ @NoArgsConstructor @AllArgsConstructor @Builder +@Table(name = "app_user") public class User { @Id @@ -19,7 +20,7 @@ public class User { private String username; @Column(nullable = false) - private String password; // hashed password + private String password; @Enumerated(EnumType.STRING) private Role role; diff --git a/src/main/java/com/booleanuk/api/cinema/library/payload/request/AuthRequest.java b/src/main/java/com/booleanuk/api/cinema/library/payload/request/LoginRequest.java similarity index 83% rename from src/main/java/com/booleanuk/api/cinema/library/payload/request/AuthRequest.java rename to src/main/java/com/booleanuk/api/cinema/library/payload/request/LoginRequest.java index 4275d874..14edd6dc 100644 --- a/src/main/java/com/booleanuk/api/cinema/library/payload/request/AuthRequest.java +++ b/src/main/java/com/booleanuk/api/cinema/library/payload/request/LoginRequest.java @@ -3,7 +3,8 @@ import lombok.Data; @Data -public class AuthRequest { +public class LoginRequest { private String username; private String password; } + diff --git a/src/main/java/com/booleanuk/api/cinema/library/payload/request/SignupRequest.java b/src/main/java/com/booleanuk/api/cinema/library/payload/request/SignupRequest.java new file mode 100644 index 00000000..d8f7e987 --- /dev/null +++ b/src/main/java/com/booleanuk/api/cinema/library/payload/request/SignupRequest.java @@ -0,0 +1,13 @@ +package com.booleanuk.api.cinema.library.payload.request; + +import lombok.Data; +import com.booleanuk.api.cinema.library.models.Role; + + +@Data +public class SignupRequest { + private String username; + private String password; + private String role; +} + From 1f316e261319b87e38d086edf47eeb798503ffcc Mon Sep 17 00:00:00 2001 From: Your Name Date: Fri, 22 Aug 2025 15:28:50 +0200 Subject: [PATCH 7/7] update OffsetDateTime JsonFormat --- .../api/cinema/library/SecurityConfig.java | 26 ++++++++++++++----- .../api/cinema/library/models/Customer.java | 15 +++++++---- .../api/cinema/library/models/Movie.java | 15 +++++++---- .../api/cinema/library/models/Screening.java | 18 ++++++++----- .../payload/request/ScreeningRequest.java | 5 +++- .../payload/response/CustomerResponse.java | 5 ++-- .../payload/response/MovieResponse.java | 5 ++-- .../payload/response/ScreeningResponse.java | 8 +++--- 8 files changed, 66 insertions(+), 31 deletions(-) diff --git a/src/main/java/com/booleanuk/api/cinema/library/SecurityConfig.java b/src/main/java/com/booleanuk/api/cinema/library/SecurityConfig.java index dd64e37c..8e3cd29c 100644 --- a/src/main/java/com/booleanuk/api/cinema/library/SecurityConfig.java +++ b/src/main/java/com/booleanuk/api/cinema/library/SecurityConfig.java @@ -7,24 +7,41 @@ import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; 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.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.web.SecurityFilterChain; import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; +import org.springframework.http.HttpMethod; + @Configuration -@RequiredArgsConstructor +@EnableWebSecurity public class SecurityConfig { private final JwtRequestFilter jwtRequestFilter; + public SecurityConfig(JwtRequestFilter jwtRequestFilter) { + this.jwtRequestFilter = jwtRequestFilter; + } + + @Bean + public BCryptPasswordEncoder passwordEncoder() { + return new BCryptPasswordEncoder(); + } + @Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http .csrf(csrf -> csrf.disable()) .authorizeHttpRequests(auth -> auth .requestMatchers("/login", "/signup").permitAll() - .requestMatchers("/movies/**").hasAnyAuthority("ADMIN", "CUSTOMER") + .requestMatchers(HttpMethod.GET, "/movies/**").hasAnyAuthority("ADMIN", "CUSTOMER") + .requestMatchers(HttpMethod.POST, "/movies/**").hasAuthority("ADMIN") + .requestMatchers(HttpMethod.PUT, "/movies/**").hasAuthority("ADMIN") + .requestMatchers(HttpMethod.DELETE, "/movies/**").hasAuthority("ADMIN") + .requestMatchers(HttpMethod.GET, "/screenings/**").hasAnyAuthority("ADMIN", "CUSTOMER") + .requestMatchers(HttpMethod.POST, "/screenings/**").hasAuthority("ADMIN") .anyRequest().authenticated() ) .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)); @@ -33,9 +50,4 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { return http.build(); } - - @Bean - public BCryptPasswordEncoder passwordEncoder() { - return new BCryptPasswordEncoder(); - } } diff --git a/src/main/java/com/booleanuk/api/cinema/library/models/Customer.java b/src/main/java/com/booleanuk/api/cinema/library/models/Customer.java index 0e485d8f..661e8a31 100644 --- a/src/main/java/com/booleanuk/api/cinema/library/models/Customer.java +++ b/src/main/java/com/booleanuk/api/cinema/library/models/Customer.java @@ -1,8 +1,10 @@ package com.booleanuk.api.cinema.library.models; +import com.fasterxml.jackson.annotation.JsonFormat; import jakarta.persistence.*; import lombok.*; import java.time.LocalDateTime; +import java.time.OffsetDateTime; import java.util.List; @Entity @@ -20,20 +22,23 @@ public class Customer { private String email; private String phone; - private LocalDateTime createdAt; - private LocalDateTime updatedAt; + @JsonFormat(pattern = "yyyy-MM-dd'T'HH:mm:ss.SSSXXX") + private OffsetDateTime createdAt; + + @JsonFormat(pattern = "yyyy-MM-dd'T'HH:mm:ss.SSSXXX") + private OffsetDateTime updatedAt; @OneToMany(mappedBy = "customer", cascade = CascadeType.ALL) private List tickets; @PrePersist protected void onCreate() { - this.createdAt = LocalDateTime.now(); - this.updatedAt = LocalDateTime.now(); + this.createdAt = OffsetDateTime.now(); + this.updatedAt = OffsetDateTime.now(); } @PreUpdate protected void onUpdate() { - this.updatedAt = LocalDateTime.now(); + this.updatedAt = OffsetDateTime.now(); } } diff --git a/src/main/java/com/booleanuk/api/cinema/library/models/Movie.java b/src/main/java/com/booleanuk/api/cinema/library/models/Movie.java index b916d1f1..ddb8096f 100644 --- a/src/main/java/com/booleanuk/api/cinema/library/models/Movie.java +++ b/src/main/java/com/booleanuk/api/cinema/library/models/Movie.java @@ -1,9 +1,11 @@ package com.booleanuk.api.cinema.library.models; +import com.fasterxml.jackson.annotation.JsonFormat; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import jakarta.persistence.*; import lombok.*; import java.time.LocalDateTime; +import java.time.OffsetDateTime; import java.util.List; @Entity @@ -23,8 +25,11 @@ public class Movie { private String description; private Integer runtimeMins; - private LocalDateTime createdAt; - private LocalDateTime updatedAt; + @JsonFormat(pattern = "yyyy-MM-dd'T'HH:mm:ss.SSSXXX") + private OffsetDateTime createdAt; + + @JsonFormat(pattern = "yyyy-MM-dd'T'HH:mm:ss.SSSXXX") + private OffsetDateTime updatedAt; @OneToMany(mappedBy = "movie", cascade = CascadeType.ALL) @JsonIgnoreProperties("movie") @@ -33,12 +38,12 @@ public class Movie { @PrePersist protected void onCreate() { - this.createdAt = LocalDateTime.now(); - this.updatedAt = LocalDateTime.now(); + this.createdAt = OffsetDateTime.now(); + this.updatedAt = OffsetDateTime.now(); } @PreUpdate protected void onUpdate() { - this.updatedAt = LocalDateTime.now(); + this.updatedAt = OffsetDateTime.now(); } } diff --git a/src/main/java/com/booleanuk/api/cinema/library/models/Screening.java b/src/main/java/com/booleanuk/api/cinema/library/models/Screening.java index 8aa6e683..b3f84ac0 100644 --- a/src/main/java/com/booleanuk/api/cinema/library/models/Screening.java +++ b/src/main/java/com/booleanuk/api/cinema/library/models/Screening.java @@ -1,9 +1,11 @@ package com.booleanuk.api.cinema.library.models; +import com.fasterxml.jackson.annotation.JsonFormat; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import jakarta.persistence.*; import lombok.*; import java.time.LocalDateTime; +import java.time.OffsetDateTime; import java.util.List; @Entity @@ -18,11 +20,14 @@ public class Screening { private Integer id; private Integer screenNumber; - private LocalDateTime startsAt; + @JsonFormat(pattern = "yyyy-MM-dd'T'HH:mm:ss.SSSXXX") + private OffsetDateTime startsAt; private Integer capacity; - private LocalDateTime createdAt; - private LocalDateTime updatedAt; + @JsonFormat(pattern = "yyyy-MM-dd'T'HH:mm:ss.SSSXXX") + private OffsetDateTime createdAt; + @JsonFormat(pattern = "yyyy-MM-dd'T'HH:mm:ss.SSSXXX") + private OffsetDateTime updatedAt; @ManyToOne @JoinColumn(name = "movie_id") @@ -35,12 +40,13 @@ public class Screening { @PrePersist protected void onCreate() { - this.createdAt = LocalDateTime.now(); - this.updatedAt = LocalDateTime.now(); + this.createdAt = OffsetDateTime.now(); + this.updatedAt = OffsetDateTime.now(); } @PreUpdate protected void onUpdate() { - this.updatedAt = LocalDateTime.now(); + this.updatedAt = OffsetDateTime.now(); } + } diff --git a/src/main/java/com/booleanuk/api/cinema/library/payload/request/ScreeningRequest.java b/src/main/java/com/booleanuk/api/cinema/library/payload/request/ScreeningRequest.java index 54a6eb34..09b519d7 100644 --- a/src/main/java/com/booleanuk/api/cinema/library/payload/request/ScreeningRequest.java +++ b/src/main/java/com/booleanuk/api/cinema/library/payload/request/ScreeningRequest.java @@ -1,11 +1,14 @@ package com.booleanuk.api.cinema.library.payload.request; +import com.fasterxml.jackson.annotation.JsonFormat; import lombok.Data; import java.time.LocalDateTime; +import java.time.OffsetDateTime; @Data public class ScreeningRequest { private int screenNumber; private int capacity; - private LocalDateTime startsAt; + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ssXXX") // pattern voor input + private OffsetDateTime startsAt; } \ No newline at end of file diff --git a/src/main/java/com/booleanuk/api/cinema/library/payload/response/CustomerResponse.java b/src/main/java/com/booleanuk/api/cinema/library/payload/response/CustomerResponse.java index 42c3ea32..1bab4fed 100644 --- a/src/main/java/com/booleanuk/api/cinema/library/payload/response/CustomerResponse.java +++ b/src/main/java/com/booleanuk/api/cinema/library/payload/response/CustomerResponse.java @@ -4,6 +4,7 @@ import lombok.Data; import java.time.LocalDateTime; +import java.time.OffsetDateTime; @Data @Builder @@ -12,6 +13,6 @@ public class CustomerResponse { private String name; private String email; private String phone; - private LocalDateTime createdAt; - private LocalDateTime updatedAt; + private OffsetDateTime createdAt; + private OffsetDateTime updatedAt; } diff --git a/src/main/java/com/booleanuk/api/cinema/library/payload/response/MovieResponse.java b/src/main/java/com/booleanuk/api/cinema/library/payload/response/MovieResponse.java index 865577b7..3fc3203d 100644 --- a/src/main/java/com/booleanuk/api/cinema/library/payload/response/MovieResponse.java +++ b/src/main/java/com/booleanuk/api/cinema/library/payload/response/MovieResponse.java @@ -4,6 +4,7 @@ import lombok.Data; import java.time.LocalDateTime; +import java.time.OffsetDateTime; @Data @Builder @@ -13,6 +14,6 @@ public class MovieResponse { private String rating; private String description; private int runtimeMins; - private LocalDateTime createdAt; - private LocalDateTime updatedAt; + private OffsetDateTime createdAt; + private OffsetDateTime updatedAt; } diff --git a/src/main/java/com/booleanuk/api/cinema/library/payload/response/ScreeningResponse.java b/src/main/java/com/booleanuk/api/cinema/library/payload/response/ScreeningResponse.java index bcd42d73..7c779272 100644 --- a/src/main/java/com/booleanuk/api/cinema/library/payload/response/ScreeningResponse.java +++ b/src/main/java/com/booleanuk/api/cinema/library/payload/response/ScreeningResponse.java @@ -4,6 +4,7 @@ import lombok.Data; import java.time.LocalDateTime; +import java.time.OffsetDateTime; @Data @Builder @@ -11,7 +12,8 @@ public class ScreeningResponse { private int id; private int screenNumber; private int capacity; - private LocalDateTime startsAt; - private LocalDateTime createdAt; - private LocalDateTime updatedAt; + private OffsetDateTime startsAt; + private OffsetDateTime createdAt; + private OffsetDateTime updatedAt; + }