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..3cd4f0e5 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.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' + 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() +} 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..8e3cd29c --- /dev/null +++ b/src/main/java/com/booleanuk/api/cinema/library/SecurityConfig.java @@ -0,0 +1,53 @@ +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.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 +@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(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)); + + http.addFilterBefore(jwtRequestFilter, UsernamePasswordAuthenticationFilter.class); + + return http.build(); + } +} 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..cc43f6ec --- /dev/null +++ b/src/main/java/com/booleanuk/api/cinema/library/controllers/AuthenticationController.java @@ -0,0 +1,55 @@ +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.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 lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.web.bind.annotation.*; + +@RestController +@RequiredArgsConstructor +public class AuthenticationController { + + 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; + } + + User user = User.builder() + .username(request.getUsername()) + .password(passwordEncoder.encode(request.getPassword())) + .role(role) + .build(); + + userRepository.save(user); + + return ResponseEntity.ok(new AuthResponse("User registered successfully")); + } + + @PostMapping("/login") + 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(new AuthResponse("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/controllers/CustomerController.java b/src/main/java/com/booleanuk/api/cinema/library/controllers/CustomerController.java new file mode 100644 index 00000000..3166cd55 --- /dev/null +++ b/src/main/java/com/booleanuk/api/cinema/library/controllers/CustomerController.java @@ -0,0 +1,88 @@ +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.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.stream.Collectors; + +@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().stream() + .map(this::toResponse) + .collect(Collectors.toList()); + } + + @GetMapping("/{id}") + public ResponseEntity getCustomerById(@PathVariable int id) { + return customerRepository.findById(id) + .map(customer -> ResponseEntity.ok(toResponse(customer))) + .orElse(ResponseEntity.notFound().build()); + } + + @PostMapping + 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 ResponseEntity updateCustomer(@PathVariable int id, @RequestBody CustomerRequest request) { + return customerRepository.findById(id).map(customer -> { + 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 ResponseEntity deleteCustomer(@PathVariable int id) { + if (!customerRepository.existsById(id)) { + return ResponseEntity.notFound().build(); + } + customerRepository.deleteById(id); + return ResponseEntity.noContent().build(); + } + + 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 new file mode 100644 index 00000000..6be00520 --- /dev/null +++ b/src/main/java/com/booleanuk/api/cinema/library/controllers/MovieController.java @@ -0,0 +1,80 @@ +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.stream.Collectors; + +@RestController +@RequestMapping("/movies") +public class MovieController { + + @Autowired + private MovieRepository movieRepository; + + @GetMapping + public List getAllMovies() { + return movieRepository.findAll().stream() + .map(this::toResponse) + .collect(Collectors.toList()); + } + + @GetMapping("/{id}") + public ResponseEntity getMovieById(@PathVariable int id) { + return movieRepository.findById(id) + .map(movie -> ResponseEntity.ok(toResponse(movie))) + .orElse(ResponseEntity.notFound().build()); + } + + @PostMapping + 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 ResponseEntity updateMovie(@PathVariable int id, @RequestBody MovieRequest request) { + return movieRepository.findById(id).map(movie -> { + 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 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 new file mode 100644 index 00000000..ffcb253b --- /dev/null +++ b/src/main/java/com/booleanuk/api/cinema/library/controllers/ScreeningController.java @@ -0,0 +1,68 @@ +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.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 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") +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")); + + List responses = movie.getScreenings().stream() + .map(this::toResponse) + .collect(Collectors.toList()); + + return ResponseEntity.ok(responses); + } + + @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 saved = screeningRepository.save(screening); + + 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/models/Customer.java b/src/main/java/com/booleanuk/api/cinema/library/models/Customer.java new file mode 100644 index 00000000..661e8a31 --- /dev/null +++ b/src/main/java/com/booleanuk/api/cinema/library/models/Customer.java @@ -0,0 +1,44 @@ +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 +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class Customer { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Integer id; + + private String name; + private String email; + private String phone; + + @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 = OffsetDateTime.now(); + this.updatedAt = OffsetDateTime.now(); + } + + @PreUpdate + protected void onUpdate() { + 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 new file mode 100644 index 00000000..ddb8096f --- /dev/null +++ b/src/main/java/com/booleanuk/api/cinema/library/models/Movie.java @@ -0,0 +1,49 @@ +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 +@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; + + @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") + private List screenings; + + + @PrePersist + protected void onCreate() { + this.createdAt = OffsetDateTime.now(); + this.updatedAt = OffsetDateTime.now(); + } + + @PreUpdate + protected void onUpdate() { + this.updatedAt = OffsetDateTime.now(); + } +} 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/Screening.java b/src/main/java/com/booleanuk/api/cinema/library/models/Screening.java new file mode 100644 index 00000000..b3f84ac0 --- /dev/null +++ b/src/main/java/com/booleanuk/api/cinema/library/models/Screening.java @@ -0,0 +1,52 @@ +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 +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class Screening { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Integer id; + + private Integer screenNumber; + @JsonFormat(pattern = "yyyy-MM-dd'T'HH:mm:ss.SSSXXX") + private OffsetDateTime startsAt; + private Integer capacity; + + @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") + @JsonIgnoreProperties("screenings") + private Movie movie; + + + @OneToMany(mappedBy = "screening", cascade = CascadeType.ALL) + private List tickets; + + @PrePersist + protected void onCreate() { + this.createdAt = OffsetDateTime.now(); + this.updatedAt = OffsetDateTime.now(); + } + + @PreUpdate + protected void onUpdate() { + this.updatedAt = OffsetDateTime.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 new file mode 100644 index 00000000..fa365bad --- /dev/null +++ b/src/main/java/com/booleanuk/api/cinema/library/models/Ticket.java @@ -0,0 +1,41 @@ +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; + + private Integer numSeats; + + private LocalDateTime createdAt; + private LocalDateTime updatedAt; + + @ManyToOne + @JoinColumn(name = "customer_id") + private Customer customer; + + @ManyToOne + @JoinColumn(name = "screening_id") + private Screening screening; + + @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/User.java b/src/main/java/com/booleanuk/api/cinema/library/models/User.java new file mode 100644 index 00000000..a47b1569 --- /dev/null +++ b/src/main/java/com/booleanuk/api/cinema/library/models/User.java @@ -0,0 +1,41 @@ +package com.booleanuk.api.cinema.library.models; + +import jakarta.persistence.*; +import lombok.*; +import java.time.LocalDateTime; + +@Entity +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +@Table(name = "app_user") +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; + + @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/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/LoginRequest.java b/src/main/java/com/booleanuk/api/cinema/library/payload/request/LoginRequest.java new file mode 100644 index 00000000..14edd6dc --- /dev/null +++ b/src/main/java/com/booleanuk/api/cinema/library/payload/request/LoginRequest.java @@ -0,0 +1,10 @@ +package com.booleanuk.api.cinema.library.payload.request; + +import lombok.Data; + +@Data +public class LoginRequest { + private String username; + private String password; +} + 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/request/ScreeningRequest.java b/src/main/java/com/booleanuk/api/cinema/library/payload/request/ScreeningRequest.java new file mode 100644 index 00000000..09b519d7 --- /dev/null +++ b/src/main/java/com/booleanuk/api/cinema/library/payload/request/ScreeningRequest.java @@ -0,0 +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; + @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/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; +} + 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/payload/response/CustomerResponse.java b/src/main/java/com/booleanuk/api/cinema/library/payload/response/CustomerResponse.java new file mode 100644 index 00000000..1bab4fed --- /dev/null +++ b/src/main/java/com/booleanuk/api/cinema/library/payload/response/CustomerResponse.java @@ -0,0 +1,18 @@ +package com.booleanuk.api.cinema.library.payload.response; + +import lombok.Builder; +import lombok.Data; + +import java.time.LocalDateTime; +import java.time.OffsetDateTime; + +@Data +@Builder +public class CustomerResponse { + private int id; + private String name; + private String email; + private String phone; + 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 new file mode 100644 index 00000000..3fc3203d --- /dev/null +++ b/src/main/java/com/booleanuk/api/cinema/library/payload/response/MovieResponse.java @@ -0,0 +1,19 @@ +package com.booleanuk.api.cinema.library.payload.response; + +import lombok.Builder; +import lombok.Data; + +import java.time.LocalDateTime; +import java.time.OffsetDateTime; + +@Data +@Builder +public class MovieResponse { + private int id; + private String title; + private String rating; + private String description; + private int runtimeMins; + 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 new file mode 100644 index 00000000..7c779272 --- /dev/null +++ b/src/main/java/com/booleanuk/api/cinema/library/payload/response/ScreeningResponse.java @@ -0,0 +1,19 @@ +package com.booleanuk.api.cinema.library.payload.response; + +import lombok.Builder; +import lombok.Data; + +import java.time.LocalDateTime; +import java.time.OffsetDateTime; + +@Data +@Builder +public class ScreeningResponse { + private int id; + private int screenNumber; + private int capacity; + private OffsetDateTime startsAt; + private OffsetDateTime createdAt; + private OffsetDateTime updatedAt; + +} 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); +} 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