From 28a1e5331e7d15ad30f1ec2dc31c17020a5e7b5c Mon Sep 17 00:00:00 2001 From: lucasholter00 Date: Tue, 26 Aug 2025 10:34:44 +0200 Subject: [PATCH 1/3] Add volume mount for database data --- build.gradle | 53 +++++--- docker/.env | 2 + docker/docker-compose.yaml | 23 ++++ docker/dockerfile | 11 ++ gradlew | 0 .../com/booleanuk/api/{ => cinema}/.gitkeep | 0 .../java/com/booleanuk/api/cinema/Main.java | 11 ++ .../cinema/controllers/AuthController.java | 101 +++++++++++++++ .../cinema/controllers/MovieController.java | 118 ++++++++++++++++++ .../controllers/ScreaningController.java | 57 +++++++++ .../cinema/controllers/TicketController.java | 72 +++++++++++ .../cinema/controllers/UserController.java | 72 +++++++++++ .../ValidationExceptionHandler.java | 19 +++ .../booleanuk/api/cinema/models/ERole.java | 6 + .../booleanuk/api/cinema/models/Movie.java | 49 ++++++++ .../com/booleanuk/api/cinema/models/Role.java | 23 ++++ .../api/cinema/models/Screening.java | 53 ++++++++ .../booleanuk/api/cinema/models/Ticket.java | 41 ++++++ .../com/booleanuk/api/cinema/models/User.java | 70 +++++++++++ .../payload/request/CreateMovieRequest.java | 27 ++++ .../cinema/payload/request/LoginRequest.java | 15 +++ .../cinema/payload/request/SignupRequest.java | 32 +++++ .../payload/response/ErrorResponse.java | 19 +++ .../cinema/payload/response/JwtResponse.java | 25 ++++ .../payload/response/MessageResponse.java | 14 +++ .../payload/response/MovieListResponse.java | 8 ++ .../payload/response/MovieResponse.java | 6 + .../api/cinema/payload/response/Response.java | 14 +++ .../response/ScreeningListResponse.java | 8 ++ .../payload/response/ScreeningResponse.java | 6 + .../payload/response/TicketListResponse.java | 8 ++ .../payload/response/TicketResponse.java | 6 + .../payload/response/UserListResponse.java | 8 ++ .../cinema/payload/response/UserResponse.java | 6 + .../cinema/repository/MovieRepository.java | 8 ++ .../api/cinema/repository/RoleRepository.java | 12 ++ .../repository/ScreeningRepository.java | 8 ++ .../cinema/repository/TicketRepository.java | 15 +++ .../api/cinema/repository/UserRepository.java | 15 +++ .../cinema/security/WebSecurityConfig.java | 71 +++++++++++ .../security/jwt/AuthEntryPointJwt.java | 24 ++++ .../cinema/security/jwt/AuthTokenFilter.java | 54 ++++++++ .../api/cinema/security/jwt/JwtUtils.java | 63 ++++++++++ .../security/services/UserDetailsImpl.java | 59 +++++++++ .../services/UserDetailsServiceImpl.java | 26 ++++ src/main/resources/application.yml | 35 ++++-- .../com/booleanuk/api/{ => cinema}/.gitkeep | 0 47 files changed, 1349 insertions(+), 24 deletions(-) create mode 100644 docker/.env create mode 100644 docker/docker-compose.yaml create mode 100644 docker/dockerfile mode change 100644 => 100755 gradlew rename src/main/java/com/booleanuk/api/{ => cinema}/.gitkeep (100%) mode change 100644 => 100755 create mode 100755 src/main/java/com/booleanuk/api/cinema/Main.java create mode 100755 src/main/java/com/booleanuk/api/cinema/controllers/AuthController.java create mode 100755 src/main/java/com/booleanuk/api/cinema/controllers/MovieController.java create mode 100755 src/main/java/com/booleanuk/api/cinema/controllers/ScreaningController.java create mode 100755 src/main/java/com/booleanuk/api/cinema/controllers/TicketController.java create mode 100755 src/main/java/com/booleanuk/api/cinema/controllers/UserController.java create mode 100755 src/main/java/com/booleanuk/api/cinema/exceptionHandlers/ValidationExceptionHandler.java create mode 100755 src/main/java/com/booleanuk/api/cinema/models/ERole.java create mode 100755 src/main/java/com/booleanuk/api/cinema/models/Movie.java create mode 100755 src/main/java/com/booleanuk/api/cinema/models/Role.java create mode 100755 src/main/java/com/booleanuk/api/cinema/models/Screening.java create mode 100755 src/main/java/com/booleanuk/api/cinema/models/Ticket.java create mode 100755 src/main/java/com/booleanuk/api/cinema/models/User.java create mode 100755 src/main/java/com/booleanuk/api/cinema/payload/request/CreateMovieRequest.java create mode 100755 src/main/java/com/booleanuk/api/cinema/payload/request/LoginRequest.java create mode 100755 src/main/java/com/booleanuk/api/cinema/payload/request/SignupRequest.java create mode 100755 src/main/java/com/booleanuk/api/cinema/payload/response/ErrorResponse.java create mode 100755 src/main/java/com/booleanuk/api/cinema/payload/response/JwtResponse.java create mode 100755 src/main/java/com/booleanuk/api/cinema/payload/response/MessageResponse.java create mode 100755 src/main/java/com/booleanuk/api/cinema/payload/response/MovieListResponse.java create mode 100755 src/main/java/com/booleanuk/api/cinema/payload/response/MovieResponse.java create mode 100755 src/main/java/com/booleanuk/api/cinema/payload/response/Response.java create mode 100755 src/main/java/com/booleanuk/api/cinema/payload/response/ScreeningListResponse.java create mode 100755 src/main/java/com/booleanuk/api/cinema/payload/response/ScreeningResponse.java create mode 100755 src/main/java/com/booleanuk/api/cinema/payload/response/TicketListResponse.java create mode 100755 src/main/java/com/booleanuk/api/cinema/payload/response/TicketResponse.java create mode 100755 src/main/java/com/booleanuk/api/cinema/payload/response/UserListResponse.java create mode 100755 src/main/java/com/booleanuk/api/cinema/payload/response/UserResponse.java create mode 100755 src/main/java/com/booleanuk/api/cinema/repository/MovieRepository.java create mode 100755 src/main/java/com/booleanuk/api/cinema/repository/RoleRepository.java create mode 100755 src/main/java/com/booleanuk/api/cinema/repository/ScreeningRepository.java create mode 100755 src/main/java/com/booleanuk/api/cinema/repository/TicketRepository.java create mode 100755 src/main/java/com/booleanuk/api/cinema/repository/UserRepository.java create mode 100755 src/main/java/com/booleanuk/api/cinema/security/WebSecurityConfig.java create mode 100755 src/main/java/com/booleanuk/api/cinema/security/jwt/AuthEntryPointJwt.java create mode 100755 src/main/java/com/booleanuk/api/cinema/security/jwt/AuthTokenFilter.java create mode 100755 src/main/java/com/booleanuk/api/cinema/security/jwt/JwtUtils.java create mode 100755 src/main/java/com/booleanuk/api/cinema/security/services/UserDetailsImpl.java create mode 100755 src/main/java/com/booleanuk/api/cinema/security/services/UserDetailsServiceImpl.java mode change 100644 => 100755 src/main/resources/application.yml rename src/test/java/com/booleanuk/api/{ => cinema}/.gitkeep (100%) mode change 100644 => 100755 diff --git a/build.gradle b/build.gradle index fb070c4..6f0ef66 100644 --- a/build.gradle +++ b/build.gradle @@ -1,33 +1,52 @@ plugins { - id 'java' - id 'org.springframework.boot' version '3.1.4' - id 'io.spring.dependency-management' version '1.1.3' + 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' -sourceCompatibility = '17' +version = '0.0.1-SNAPSHOT' + +java { + toolchain { + languageVersion = JavaLanguageVersion.of(21) + } +} configurations { - compileOnly { - extendsFrom annotationProcessor - } + compileOnly { + extendsFrom annotationProcessor + } } repositories { - mavenCentral() + mavenCentral() } dependencies { - implementation 'org.springframework.boot:spring-boot-starter-data-jpa' - implementation 'org.springframework.boot:spring-boot-starter-web' - compileOnly 'org.projectlombok:lombok' - developmentOnly 'org.springframework.boot:spring-boot-devtools' - implementation 'org.postgresql:postgresql:42.6.0' - annotationProcessor 'org.projectlombok:lombok' - testImplementation 'org.springframework.boot:spring-boot-starter-test' + 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.0' + implementation 'org.springframework.boot:spring-boot-starter-validation' + +} + +tasks.named('bootBuildImage') { + builder = 'paketobuildpacks/builder-jammy-base:latest' } tasks.named('test') { - useJUnitPlatform() + useJUnitPlatform() } diff --git a/docker/.env b/docker/.env new file mode 100644 index 0000000..6f609ac --- /dev/null +++ b/docker/.env @@ -0,0 +1,2 @@ +DB_PW=admin +DB_USERNAME=postgres diff --git a/docker/docker-compose.yaml b/docker/docker-compose.yaml new file mode 100644 index 0000000..20ba970 --- /dev/null +++ b/docker/docker-compose.yaml @@ -0,0 +1,23 @@ +version: '3.8' +services: + db: + image: postgres:latest + environment: + POSTGRES_PASSWORD: ${DB_PW} + ports: + - "5432:5432" + volumes: + - db_vol:/var/lib/postgresql/data + + app: + image: cinema + depends_on: + - db + environment: + DB_URL : jdbc:postgresql://db:5432/postgres + DB_PW : ${DB_PW} + DB_USERNAME : ${DB_USERNAME} + ports: + - "4000:4000" +volumes: + db_vol: diff --git a/docker/dockerfile b/docker/dockerfile new file mode 100644 index 0000000..1796c18 --- /dev/null +++ b/docker/dockerfile @@ -0,0 +1,11 @@ +FROM openjdk:21-jdk-slim AS build + +WORKDIR /app +COPY . . +RUN ./gradlew bootJar + +FROM openjdk:21-jdk-slim +WORKDIR /app +COPY --from=build /app/build/libs/java.docker.day.2-0.0.1-SNAPSHOT.jar . +EXPOSE 4000 +CMD ["java", "-jar", "java.docker.day.2-0.0.1-SNAPSHOT.jar"] diff --git a/gradlew b/gradlew old mode 100644 new mode 100755 diff --git a/src/main/java/com/booleanuk/api/.gitkeep b/src/main/java/com/booleanuk/api/cinema/.gitkeep old mode 100644 new mode 100755 similarity index 100% rename from src/main/java/com/booleanuk/api/.gitkeep rename to src/main/java/com/booleanuk/api/cinema/.gitkeep 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 100755 index 0000000..746158a --- /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/controllers/AuthController.java b/src/main/java/com/booleanuk/api/cinema/controllers/AuthController.java new file mode 100755 index 0000000..f22c6cf --- /dev/null +++ b/src/main/java/com/booleanuk/api/cinema/controllers/AuthController.java @@ -0,0 +1,101 @@ +package com.booleanuk.api.cinema.controllers; + +import com.booleanuk.api.cinema.models.ERole; +import com.booleanuk.api.cinema.models.Role; +import com.booleanuk.api.cinema.models.User; +import com.booleanuk.api.cinema.payload.request.LoginRequest; +import com.booleanuk.api.cinema.payload.request.SignupRequest; +import com.booleanuk.api.cinema.payload.response.JwtResponse; +import com.booleanuk.api.cinema.payload.response.MessageResponse; +import com.booleanuk.api.cinema.repository.RoleRepository; +import com.booleanuk.api.cinema.repository.UserRepository; +import com.booleanuk.api.cinema.security.jwt.JwtUtils; +import com.booleanuk.api.cinema.security.services.UserDetailsImpl; +import jakarta.validation.Valid; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.ResponseEntity; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.web.bind.annotation.*; + +import java.time.OffsetDateTime; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; + +@CrossOrigin(origins = "*", maxAge = 3600) +@RestController +@RequestMapping("auth") +public class AuthController { + @Autowired + AuthenticationManager authenticationManager; + + @Autowired + UserRepository userRepository; + + @Autowired + RoleRepository roleRepository; + + @Autowired + PasswordEncoder encoder; + + @Autowired + JwtUtils jwtUtils; + + @PostMapping("/signin") + public ResponseEntity authenticateUser(@Valid @RequestBody LoginRequest loginRequest) { + // If using a salt for password use it here + Authentication authentication = authenticationManager + .authenticate(new UsernamePasswordAuthenticationToken(loginRequest.getUsername(), loginRequest.getPassword())); + SecurityContextHolder.getContext().setAuthentication(authentication); + String jwt = jwtUtils.generateJwtToken(authentication); + + UserDetailsImpl userDetails = (UserDetailsImpl) authentication.getPrincipal(); + List roles = userDetails.getAuthorities().stream().map((item) -> item.getAuthority()) + .collect(Collectors.toList()); + return ResponseEntity + .ok(new JwtResponse(jwt, userDetails.getId(), userDetails.getUsername(), userDetails.getEmail(), roles)); + } + + @PostMapping("/signup") + public ResponseEntity registerUser(@Valid @RequestBody SignupRequest signupRequest) { + if (userRepository.existsByUsername(signupRequest.getUsername())) { + return ResponseEntity.badRequest().body(new MessageResponse("Error: Username is already taken")); + } + if (userRepository.existsByEmail(signupRequest.getEmail())) { + return ResponseEntity.badRequest().body(new MessageResponse("Error: Email is already in use!")); + } + // Create a new user add salt here if using one + User user = new User(signupRequest.getUsername(), signupRequest.getEmail(), encoder.encode(signupRequest.getPassword()), signupRequest.getPhone()); + user.setCreated(OffsetDateTime.now()); + user.setUpdatedAt(OffsetDateTime.now()); + + Set strRoles = signupRequest.getRole(); + Set roles = new HashSet<>(); + + if (strRoles == null) { + Role userRole = roleRepository.findByName(ERole.ROLE_CUSTOMER).orElseThrow(() -> new RuntimeException("Error: Role is not found")); + roles.add(userRole); + } else { + strRoles.forEach((role) -> { + switch (role) { + case "admin": + Role adminRole = roleRepository.findByName(ERole.ROLE_ADMIN).orElseThrow(() -> new RuntimeException("Error: Role is not found")); + roles.add(adminRole); + break; + default: + Role userRole = roleRepository.findByName(ERole.ROLE_CUSTOMER).orElseThrow(() -> new RuntimeException("Error: Role is not found")); + roles.add(userRole); + break; + } + }); + } + user.setRoles(roles); + userRepository.save(user); + return ResponseEntity.ok((new MessageResponse("User registered successfully"))); + } +} \ No newline at end of file diff --git a/src/main/java/com/booleanuk/api/cinema/controllers/MovieController.java b/src/main/java/com/booleanuk/api/cinema/controllers/MovieController.java new file mode 100755 index 0000000..7eac58e --- /dev/null +++ b/src/main/java/com/booleanuk/api/cinema/controllers/MovieController.java @@ -0,0 +1,118 @@ +package com.booleanuk.api.cinema.controllers; + +import com.booleanuk.api.cinema.models.Movie; +import com.booleanuk.api.cinema.models.Screening; +import com.booleanuk.api.cinema.payload.request.CreateMovieRequest; +import com.booleanuk.api.cinema.payload.response.ErrorResponse; +import com.booleanuk.api.cinema.payload.response.MovieListResponse; +import com.booleanuk.api.cinema.payload.response.MovieResponse; +import com.booleanuk.api.cinema.payload.response.Response; +import com.booleanuk.api.cinema.repository.MovieRepository; +import com.booleanuk.api.cinema.repository.ScreeningRepository; +import com.booleanuk.api.cinema.repository.TicketRepository; +import jakarta.validation.Valid; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.server.ResponseStatusException; + +import java.time.OffsetDateTime; +import java.util.List; + +@RestController +@RequestMapping("movies") +public class MovieController { + @Autowired + MovieRepository repo; + + @Autowired + ScreeningRepository screeningRepo; + + @Autowired + TicketRepository ticketRepo; + + @GetMapping + public ResponseEntity getAll(){ + MovieListResponse movieList = new MovieListResponse(); + movieList.set(this.repo.findAll()); + return ResponseEntity.ok(movieList); + } + + @PostMapping + public ResponseEntity> addOne(@Valid @RequestBody CreateMovieRequest req){ + + Movie movie = new Movie(req.getTitle(), req.getRating(), req.getDescription(), req.getRuntimeMins()); + movie.setCreated(OffsetDateTime.now()); + movie.setUpdatedAt(OffsetDateTime.now()); + + Integer createdId = repo.save(movie).getId(); + + List screenings = req.getScreenings(); + if(screenings != null){ + for(Screening screening : screenings){ + screening.setMovie(movie); + screening.setCreated(OffsetDateTime.now()); + screening.setUpdatedAt(OffsetDateTime.now()); + + this.screeningRepo.save(screening); + } + } + + MovieResponse resp = new MovieResponse(); + Movie newMovie = repo.findById(createdId).orElse(null); + + if(newMovie == null){ + ErrorResponse error = new ErrorResponse(); + error.set("Not found"); + return new ResponseEntity<>(error, HttpStatus.NOT_FOUND); + } + + resp.set(newMovie); + return new ResponseEntity<>(resp, HttpStatus.CREATED); + } + + @PutMapping("{id}") + public ResponseEntity> editOne(@PathVariable Integer id, @Valid @RequestBody Movie movie){ + Movie toBeEdited = this.repo.findById(id).orElse(null); + + if(toBeEdited == null){ + ErrorResponse error = new ErrorResponse(); + error.set("Not found"); + return new ResponseEntity<>(error, HttpStatus.NOT_FOUND); + } + + toBeEdited.setTitle(movie.getTitle()); + toBeEdited.setRating(movie.getRating()); + toBeEdited.setDescription(movie.getDescription()); + toBeEdited.setRuntimeMins(movie.getRuntimeMins()); + + toBeEdited.setUpdatedAt(OffsetDateTime.now()); + + MovieResponse resp = new MovieResponse(); + resp.set(this.repo.save(toBeEdited)); + return new ResponseEntity<>(resp, HttpStatus.CREATED); + } + + @DeleteMapping("{id}") + public ResponseEntity> deleteOne(@PathVariable Integer id){ + Movie toBeDeleted = this.repo.findById(id).orElse(null); + + if(toBeDeleted == null){ + ErrorResponse error = new ErrorResponse(); + error.set("Not found"); + return new ResponseEntity<>(error, HttpStatus.NOT_FOUND); + } + + for(Screening curr : toBeDeleted.getScreenings()){ + this.ticketRepo.deleteAll(curr.getTickets()); + this.screeningRepo.delete(curr); + } + + this.repo.delete(toBeDeleted); + MovieResponse resp = new MovieResponse(); + resp.set(toBeDeleted); + return ResponseEntity.ok(resp); + } +} diff --git a/src/main/java/com/booleanuk/api/cinema/controllers/ScreaningController.java b/src/main/java/com/booleanuk/api/cinema/controllers/ScreaningController.java new file mode 100755 index 0000000..8a14d2a --- /dev/null +++ b/src/main/java/com/booleanuk/api/cinema/controllers/ScreaningController.java @@ -0,0 +1,57 @@ +package com.booleanuk.api.cinema.controllers; + +import com.booleanuk.api.cinema.models.Movie; +import com.booleanuk.api.cinema.models.Screening; +import com.booleanuk.api.cinema.payload.response.ErrorResponse; +import com.booleanuk.api.cinema.payload.response.Response; +import com.booleanuk.api.cinema.payload.response.ScreeningListResponse; +import com.booleanuk.api.cinema.payload.response.ScreeningResponse; +import com.booleanuk.api.cinema.repository.MovieRepository; +import com.booleanuk.api.cinema.repository.ScreeningRepository; +import jakarta.validation.Valid; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +@RestController +@RequestMapping("movies/{id}/screenings") +public class ScreaningController { + @Autowired + ScreeningRepository repo; + @Autowired + MovieRepository movieRepo; + + @PostMapping + public ResponseEntity> addOne(@PathVariable Integer id, @Valid @RequestBody Screening screening){ + Movie movie = this.movieRepo.findById(id).orElse(null); + + if(movie == null){ + ErrorResponse error = new ErrorResponse(); + error.set("Not found"); + return new ResponseEntity<>(error, HttpStatus.NOT_FOUND); + } + + screening.setMovie(movie); + ScreeningResponse resp = new ScreeningResponse(); + resp.set(this.repo.save(screening)); + + return new ResponseEntity<>(resp, HttpStatus.CREATED); + } + + @GetMapping + public ResponseEntity> getAll(@PathVariable Integer id){ + ScreeningListResponse resp = new ScreeningListResponse(); + Movie movie = this.movieRepo.findById(id).orElse(null); + + if(movie == null){ + ErrorResponse error = new ErrorResponse(); + error.set("Not found"); + return new ResponseEntity<>(error, HttpStatus.NOT_FOUND); + } + resp.set(movie.getScreenings()); + return ResponseEntity.ok(resp); + } +} diff --git a/src/main/java/com/booleanuk/api/cinema/controllers/TicketController.java b/src/main/java/com/booleanuk/api/cinema/controllers/TicketController.java new file mode 100755 index 0000000..a6c1854 --- /dev/null +++ b/src/main/java/com/booleanuk/api/cinema/controllers/TicketController.java @@ -0,0 +1,72 @@ +package com.booleanuk.api.cinema.controllers; + +import com.booleanuk.api.cinema.models.Screening; +import com.booleanuk.api.cinema.models.Ticket; +import com.booleanuk.api.cinema.models.User; +import com.booleanuk.api.cinema.payload.response.ErrorResponse; +import com.booleanuk.api.cinema.payload.response.Response; +import com.booleanuk.api.cinema.payload.response.TicketListResponse; +import com.booleanuk.api.cinema.payload.response.TicketResponse; +import com.booleanuk.api.cinema.repository.ScreeningRepository; +import com.booleanuk.api.cinema.repository.TicketRepository; +import com.booleanuk.api.cinema.repository.UserRepository; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +@RestController +@RequestMapping("customers/{customerId}/screenings/{screeningId}") +public class TicketController { + @Autowired + TicketRepository repo; + + @Autowired + UserRepository userRepo; + + @Autowired + ScreeningRepository screeningRepo; + + @PostMapping + public ResponseEntity> addOne(@PathVariable int customerId, @PathVariable int screeningId, @RequestBody Ticket ticket){ + User user = this.userRepo.findById(customerId).orElse(null); + Screening screening = this.screeningRepo.findById(screeningId).orElse(null); + + if(user == null || screening == null){ + ErrorResponse error = new ErrorResponse(); + error.set("Not found"); + return new ResponseEntity<>(error, HttpStatus.NOT_FOUND); + } + + ticket.setScreening(screening); + ticket.setCustomer(user); + + TicketResponse resp = new TicketResponse(); + resp.set(ticket); + + return new ResponseEntity<>(resp, HttpStatus.CREATED); + } + + @GetMapping + public ResponseEntity> getAll(@PathVariable int customerId, @PathVariable int screeningId){ + User user = this.userRepo.findById(customerId).orElse(null); + Screening screening = this.screeningRepo.findById(screeningId).orElse(null); + + if(user == null || screening == null){ + ErrorResponse error = new ErrorResponse(); + error.set("Ticket not found"); + return new ResponseEntity<>(error, HttpStatus.NOT_FOUND); + } + + List tickets = this.repo.findTicketsByCustomerAndScreening(user, screening); + + TicketListResponse resp = new TicketListResponse(); + resp.set(tickets); + + return ResponseEntity.ok(resp); + } + + +} diff --git a/src/main/java/com/booleanuk/api/cinema/controllers/UserController.java b/src/main/java/com/booleanuk/api/cinema/controllers/UserController.java new file mode 100755 index 0000000..44df9de --- /dev/null +++ b/src/main/java/com/booleanuk/api/cinema/controllers/UserController.java @@ -0,0 +1,72 @@ +package com.booleanuk.api.cinema.controllers; + +import com.booleanuk.api.cinema.models.Ticket; +import com.booleanuk.api.cinema.models.User; +import com.booleanuk.api.cinema.payload.response.ErrorResponse; +import com.booleanuk.api.cinema.payload.response.Response; +import com.booleanuk.api.cinema.payload.response.UserListResponse; +import com.booleanuk.api.cinema.payload.response.UserResponse; +import com.booleanuk.api.cinema.repository.TicketRepository; +import com.booleanuk.api.cinema.repository.UserRepository; +import jakarta.validation.Valid; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import java.time.OffsetDateTime; + +@RestController +@RequestMapping("customers") +public class UserController { + @Autowired + UserRepository repo; + + @Autowired + TicketRepository ticketRepo; + + @GetMapping + public ResponseEntity getAll(){ + UserListResponse resp = new UserListResponse(); + resp.set(this.repo.findAll()); + return ResponseEntity.ok(resp); + } + + @PutMapping("{id}") + public ResponseEntity> putOne(@PathVariable Integer id, @Valid @RequestBody User user){ + User toBeEdited = this.repo.findById(id).orElse(null); + if(toBeEdited == null){ + ErrorResponse error = new ErrorResponse(); + error.set("User not found"); + return new ResponseEntity<>(error, HttpStatus.NOT_FOUND); + } + + toBeEdited.setUsername(user.getUsername()); + toBeEdited.setEmail(user.getEmail()); + toBeEdited.setPhone(user.getPhone()); + toBeEdited.setUpdatedAt(OffsetDateTime.now()); + + this.repo.save(toBeEdited); + + UserResponse resp = new UserResponse(); + resp.set(toBeEdited); + return new ResponseEntity<>(resp, HttpStatus.CREATED); + } + + @DeleteMapping("{id}") + public ResponseEntity> deleteOne(@PathVariable Integer id){ + User toBeDeleted = this.repo.findById(id).orElse(null); + + if (toBeDeleted == null) { + ErrorResponse error = new ErrorResponse(); + error.set("User not found"); + return new ResponseEntity<>(error, HttpStatus.NOT_FOUND); + } + this.ticketRepo.deleteAll(toBeDeleted.getTickets()); + + this.repo.delete(toBeDeleted); + UserResponse resp = new UserResponse(); + resp.set(toBeDeleted); + return ResponseEntity.ok(resp); + } +} diff --git a/src/main/java/com/booleanuk/api/cinema/exceptionHandlers/ValidationExceptionHandler.java b/src/main/java/com/booleanuk/api/cinema/exceptionHandlers/ValidationExceptionHandler.java new file mode 100755 index 0000000..ef2e6f2 --- /dev/null +++ b/src/main/java/com/booleanuk/api/cinema/exceptionHandlers/ValidationExceptionHandler.java @@ -0,0 +1,19 @@ +package com.booleanuk.api.cinema.exceptionHandlers; + +import com.booleanuk.api.cinema.payload.response.ErrorResponse; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.MethodArgumentNotValidException; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.RestControllerAdvice; + +@RestControllerAdvice +public class ValidationExceptionHandler { + + @ExceptionHandler(MethodArgumentNotValidException.class) + public ResponseEntity handleValidationExceptions(MethodArgumentNotValidException e){ + ErrorResponse error = new ErrorResponse(); + error.set("bad request"); + return new ResponseEntity<>(error, HttpStatus.BAD_REQUEST); + } +} diff --git a/src/main/java/com/booleanuk/api/cinema/models/ERole.java b/src/main/java/com/booleanuk/api/cinema/models/ERole.java new file mode 100755 index 0000000..99c3093 --- /dev/null +++ b/src/main/java/com/booleanuk/api/cinema/models/ERole.java @@ -0,0 +1,6 @@ +package com.booleanuk.api.cinema.models; + +public enum ERole { + ROLE_CUSTOMER, + ROLE_ADMIN +} diff --git a/src/main/java/com/booleanuk/api/cinema/models/Movie.java b/src/main/java/com/booleanuk/api/cinema/models/Movie.java new file mode 100755 index 0000000..e50d355 --- /dev/null +++ b/src/main/java/com/booleanuk/api/cinema/models/Movie.java @@ -0,0 +1,49 @@ +package com.booleanuk.api.cinema.models; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import jakarta.persistence.*; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.time.OffsetDateTime; +import java.util.List; + +@NoArgsConstructor +@Entity +@Data +@Table(name="movies") +public class Movie { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private int id; + + @NotBlank + private String title; + + @NotBlank + private String rating; + + @NotBlank + private String description; + + @NotNull + private Integer runtimeMins; + + private OffsetDateTime created; + + private OffsetDateTime updatedAt; + + @OneToMany(mappedBy = "movie") + @JsonIgnoreProperties({"movie"}) + List screenings; + + public Movie(String title, String rating, String description, int runtimeMins) { + this.title = title; + this.description = description; + this.rating = rating; + this.runtimeMins = runtimeMins; + } +} diff --git a/src/main/java/com/booleanuk/api/cinema/models/Role.java b/src/main/java/com/booleanuk/api/cinema/models/Role.java new file mode 100755 index 0000000..7be97fe --- /dev/null +++ b/src/main/java/com/booleanuk/api/cinema/models/Role.java @@ -0,0 +1,23 @@ +package com.booleanuk.api.cinema.models; + +import jakarta.persistence.*; +import lombok.Data; +import lombok.NoArgsConstructor; + +@NoArgsConstructor +@Data +@Entity +@Table(name = "roles") +public class Role { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private int id; + + @Enumerated(EnumType.STRING) + @Column(length = 20) + private ERole name; + + public Role(ERole name) { + this.name = name; + } +} diff --git a/src/main/java/com/booleanuk/api/cinema/models/Screening.java b/src/main/java/com/booleanuk/api/cinema/models/Screening.java new file mode 100755 index 0000000..8bdb607 --- /dev/null +++ b/src/main/java/com/booleanuk/api/cinema/models/Screening.java @@ -0,0 +1,53 @@ +package com.booleanuk.api.cinema.models; + +import com.fasterxml.jackson.annotation.JsonFormat; +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; +import jakarta.persistence.*; +import jakarta.validation.constraints.NotNull; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.time.OffsetDateTime; +import java.util.List; + +@NoArgsConstructor +@Entity +@Data +@Table(name = "screenings") +public class Screening { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + int id; + + @ManyToOne + @JoinColumn(name="movie_id") + @JsonIgnore + Movie movie; + + @NotNull + Integer screenNumber; + + @NotNull + @JsonProperty("startsAt") + @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd HH:mm:ssXXX") // Accept "2023-03-19 11:30:00+00:00" OffsetDateTime startsAt; + OffsetDateTime startsAt; + + @NotNull + Integer capacity; + + OffsetDateTime created; + + OffsetDateTime updatedAt; + + @OneToMany(mappedBy = "screening") + @JsonIgnoreProperties({"screening"}) + List tickets; + + public Screening(int screenNumber, OffsetDateTime startsAt, int capacity) { + this.screenNumber = screenNumber; + this.startsAt = startsAt; + this.capacity = capacity; + } +} diff --git a/src/main/java/com/booleanuk/api/cinema/models/Ticket.java b/src/main/java/com/booleanuk/api/cinema/models/Ticket.java new file mode 100755 index 0000000..f594d56 --- /dev/null +++ b/src/main/java/com/booleanuk/api/cinema/models/Ticket.java @@ -0,0 +1,41 @@ +package com.booleanuk.api.cinema.models; + +import jakarta.persistence.*; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.time.OffsetDateTime; + +@NoArgsConstructor +@Entity +@Data +@Table(name="tickets") + +public class Ticket { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + int id; + + @ManyToOne + @JoinColumn(name = "customer_id") + User customer; + + @ManyToOne + @JoinColumn(name = "screening_id") + Screening screening; + + @NotNull + Integer numSeats; + + OffsetDateTime created; + + OffsetDateTime updatedAt; + + public Ticket(User customer, Screening screening, int numSeats) { + this.customer = customer; + this.screening = screening; + this.numSeats = numSeats; + } +} diff --git a/src/main/java/com/booleanuk/api/cinema/models/User.java b/src/main/java/com/booleanuk/api/cinema/models/User.java new file mode 100755 index 0000000..54441c1 --- /dev/null +++ b/src/main/java/com/booleanuk/api/cinema/models/User.java @@ -0,0 +1,70 @@ +package com.booleanuk.api.cinema.models; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import jakarta.persistence.*; +import jakarta.validation.constraints.Email; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.time.OffsetDateTime; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +@NoArgsConstructor +@Data +@Entity +@Table(name = "customers", + uniqueConstraints = { + @UniqueConstraint(columnNames = "username"), + @UniqueConstraint(columnNames = "email") + }) +public class User { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private int id; + + @NotBlank + @Size(max = 20) + private String username; + + @NotBlank + @Size(max = 50) + @Email + private String email; + + @NotBlank + @Size(max = 120) + private String password; + + @NotBlank + private String phone; + + private OffsetDateTime created; + + private OffsetDateTime updatedAt; + + @OneToMany(mappedBy = "customer") + @JsonIgnoreProperties({"customer"}) + List tickets; + + @ManyToMany(fetch = FetchType.LAZY) + @JoinTable(name = "user_roles", joinColumns = @JoinColumn(name = "user_id"), inverseJoinColumns = @JoinColumn(name = "role_id")) + private Set roles = new HashSet<>(); + + public User(String username, String email, String password, String phone) { + this.username = username; + this.email = email; + this.password = password; + this.phone = phone; + } + + public User(String username, String email, String password) { + this.username = username; + this.email = email; + this.password = password; + } +} diff --git a/src/main/java/com/booleanuk/api/cinema/payload/request/CreateMovieRequest.java b/src/main/java/com/booleanuk/api/cinema/payload/request/CreateMovieRequest.java new file mode 100755 index 0000000..4803df0 --- /dev/null +++ b/src/main/java/com/booleanuk/api/cinema/payload/request/CreateMovieRequest.java @@ -0,0 +1,27 @@ +package com.booleanuk.api.cinema.payload.request; + +import com.booleanuk.api.cinema.models.Screening; +import jakarta.validation.Valid; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import lombok.Getter; + +import java.util.List; + +@Getter +public class CreateMovieRequest { + @NotBlank + String title; + + @NotNull + String rating; + + @NotNull + String description; + + @NotNull + Integer runtimeMins; + + @Valid + List screenings; +} diff --git a/src/main/java/com/booleanuk/api/cinema/payload/request/LoginRequest.java b/src/main/java/com/booleanuk/api/cinema/payload/request/LoginRequest.java new file mode 100755 index 0000000..d795a0e --- /dev/null +++ b/src/main/java/com/booleanuk/api/cinema/payload/request/LoginRequest.java @@ -0,0 +1,15 @@ +package com.booleanuk.api.cinema.payload.request; + +import jakarta.validation.constraints.NotBlank; +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +public class LoginRequest { + @NotBlank + private String username; + + @NotBlank + private String password; +} diff --git a/src/main/java/com/booleanuk/api/cinema/payload/request/SignupRequest.java b/src/main/java/com/booleanuk/api/cinema/payload/request/SignupRequest.java new file mode 100755 index 0000000..0dee2bc --- /dev/null +++ b/src/main/java/com/booleanuk/api/cinema/payload/request/SignupRequest.java @@ -0,0 +1,32 @@ +package com.booleanuk.api.cinema.payload.request; + +import jakarta.validation.constraints.Email; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Size; +import lombok.Getter; +import lombok.Setter; + +import java.util.Set; + +@Getter +@Setter +public class SignupRequest { + @NotBlank + @Size(min = 3, max = 20) + private String username; + + @NotBlank + @Size(max = 50) + @Email + private String email; + + private Set role; + + @NotBlank + @Size(min = 6, max = 40) + private String password; + + @NotBlank + String phone; + +} diff --git a/src/main/java/com/booleanuk/api/cinema/payload/response/ErrorResponse.java b/src/main/java/com/booleanuk/api/cinema/payload/response/ErrorResponse.java new file mode 100755 index 0000000..13cc6a2 --- /dev/null +++ b/src/main/java/com/booleanuk/api/cinema/payload/response/ErrorResponse.java @@ -0,0 +1,19 @@ +package com.booleanuk.api.cinema.payload.response; + +import lombok.Getter; +import lombok.NoArgsConstructor; + +import java.util.HashMap; +import java.util.Map; + +@Getter +@NoArgsConstructor +public class ErrorResponse extends Response> { + public void set(String message) { + this.status = "error"; + + Map reply = new HashMap<>(); + reply.put("message", message); + this.data = reply; + } +} diff --git a/src/main/java/com/booleanuk/api/cinema/payload/response/JwtResponse.java b/src/main/java/com/booleanuk/api/cinema/payload/response/JwtResponse.java new file mode 100755 index 0000000..3e33618 --- /dev/null +++ b/src/main/java/com/booleanuk/api/cinema/payload/response/JwtResponse.java @@ -0,0 +1,25 @@ +package com.booleanuk.api.cinema.payload.response; + +import lombok.Getter; +import lombok.Setter; + +import java.util.List; + +@Getter +@Setter +public class JwtResponse { + private String token; + private String type = "Bearer"; + private int id; + private String username; + private String email; + private List roles; + + public JwtResponse(String token, int id, String username, String email, List roles) { + this.token = token; + this.id = id; + this.username = username; + this.email = email; + this.roles = roles; + } +} diff --git a/src/main/java/com/booleanuk/api/cinema/payload/response/MessageResponse.java b/src/main/java/com/booleanuk/api/cinema/payload/response/MessageResponse.java new file mode 100755 index 0000000..8160099 --- /dev/null +++ b/src/main/java/com/booleanuk/api/cinema/payload/response/MessageResponse.java @@ -0,0 +1,14 @@ +package com.booleanuk.api.cinema.payload.response; + +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +public class MessageResponse { + private String message; + + public MessageResponse(String message) { + this.message = message; + } +} diff --git a/src/main/java/com/booleanuk/api/cinema/payload/response/MovieListResponse.java b/src/main/java/com/booleanuk/api/cinema/payload/response/MovieListResponse.java new file mode 100755 index 0000000..aac2f7a --- /dev/null +++ b/src/main/java/com/booleanuk/api/cinema/payload/response/MovieListResponse.java @@ -0,0 +1,8 @@ +package com.booleanuk.api.cinema.payload.response; + +import com.booleanuk.api.cinema.models.Movie; + +import java.util.List; + +public class MovieListResponse extends Response> { +} diff --git a/src/main/java/com/booleanuk/api/cinema/payload/response/MovieResponse.java b/src/main/java/com/booleanuk/api/cinema/payload/response/MovieResponse.java new file mode 100755 index 0000000..4a4a9dd --- /dev/null +++ b/src/main/java/com/booleanuk/api/cinema/payload/response/MovieResponse.java @@ -0,0 +1,6 @@ +package com.booleanuk.api.cinema.payload.response; + +import com.booleanuk.api.cinema.models.Movie; + +public class MovieResponse extends Response{ +} diff --git a/src/main/java/com/booleanuk/api/cinema/payload/response/Response.java b/src/main/java/com/booleanuk/api/cinema/payload/response/Response.java new file mode 100755 index 0000000..d2b8c5d --- /dev/null +++ b/src/main/java/com/booleanuk/api/cinema/payload/response/Response.java @@ -0,0 +1,14 @@ +package com.booleanuk.api.cinema.payload.response; + +import lombok.Getter; + +@Getter +public class Response { + protected String status; + protected T data; + + public void set(T data) { + this.status = "success"; + this.data = data; + } +} diff --git a/src/main/java/com/booleanuk/api/cinema/payload/response/ScreeningListResponse.java b/src/main/java/com/booleanuk/api/cinema/payload/response/ScreeningListResponse.java new file mode 100755 index 0000000..e4f1f9d --- /dev/null +++ b/src/main/java/com/booleanuk/api/cinema/payload/response/ScreeningListResponse.java @@ -0,0 +1,8 @@ +package com.booleanuk.api.cinema.payload.response; + +import com.booleanuk.api.cinema.models.Screening; + +import java.util.List; + +public class ScreeningListResponse extends Response> { +} diff --git a/src/main/java/com/booleanuk/api/cinema/payload/response/ScreeningResponse.java b/src/main/java/com/booleanuk/api/cinema/payload/response/ScreeningResponse.java new file mode 100755 index 0000000..652dcd5 --- /dev/null +++ b/src/main/java/com/booleanuk/api/cinema/payload/response/ScreeningResponse.java @@ -0,0 +1,6 @@ +package com.booleanuk.api.cinema.payload.response; + +import com.booleanuk.api.cinema.models.Screening; + +public class ScreeningResponse extends Response{ +} diff --git a/src/main/java/com/booleanuk/api/cinema/payload/response/TicketListResponse.java b/src/main/java/com/booleanuk/api/cinema/payload/response/TicketListResponse.java new file mode 100755 index 0000000..42a31a2 --- /dev/null +++ b/src/main/java/com/booleanuk/api/cinema/payload/response/TicketListResponse.java @@ -0,0 +1,8 @@ +package com.booleanuk.api.cinema.payload.response; + +import com.booleanuk.api.cinema.models.Ticket; + +import java.util.List; + +public class TicketListResponse extends Response> { +} diff --git a/src/main/java/com/booleanuk/api/cinema/payload/response/TicketResponse.java b/src/main/java/com/booleanuk/api/cinema/payload/response/TicketResponse.java new file mode 100755 index 0000000..3f1b517 --- /dev/null +++ b/src/main/java/com/booleanuk/api/cinema/payload/response/TicketResponse.java @@ -0,0 +1,6 @@ +package com.booleanuk.api.cinema.payload.response; + +import com.booleanuk.api.cinema.models.Ticket; + +public class TicketResponse extends Response{ +} diff --git a/src/main/java/com/booleanuk/api/cinema/payload/response/UserListResponse.java b/src/main/java/com/booleanuk/api/cinema/payload/response/UserListResponse.java new file mode 100755 index 0000000..dbc1080 --- /dev/null +++ b/src/main/java/com/booleanuk/api/cinema/payload/response/UserListResponse.java @@ -0,0 +1,8 @@ +package com.booleanuk.api.cinema.payload.response; + +import com.booleanuk.api.cinema.models.User; + +import java.util.List; + +public class UserListResponse extends Response> { +} diff --git a/src/main/java/com/booleanuk/api/cinema/payload/response/UserResponse.java b/src/main/java/com/booleanuk/api/cinema/payload/response/UserResponse.java new file mode 100755 index 0000000..935898e --- /dev/null +++ b/src/main/java/com/booleanuk/api/cinema/payload/response/UserResponse.java @@ -0,0 +1,6 @@ +package com.booleanuk.api.cinema.payload.response; + +import com.booleanuk.api.cinema.models.User; + +public class UserResponse extends Response{ +} diff --git a/src/main/java/com/booleanuk/api/cinema/repository/MovieRepository.java b/src/main/java/com/booleanuk/api/cinema/repository/MovieRepository.java new file mode 100755 index 0000000..12e06d8 --- /dev/null +++ b/src/main/java/com/booleanuk/api/cinema/repository/MovieRepository.java @@ -0,0 +1,8 @@ +package com.booleanuk.api.cinema.repository; + +import com.booleanuk.api.cinema.models.Movie; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +@Repository +public interface MovieRepository extends JpaRepository {} diff --git a/src/main/java/com/booleanuk/api/cinema/repository/RoleRepository.java b/src/main/java/com/booleanuk/api/cinema/repository/RoleRepository.java new file mode 100755 index 0000000..e8609d7 --- /dev/null +++ b/src/main/java/com/booleanuk/api/cinema/repository/RoleRepository.java @@ -0,0 +1,12 @@ +package com.booleanuk.api.cinema.repository; + +import com.booleanuk.api.cinema.models.ERole; +import com.booleanuk.api.cinema.models.Role; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import java.util.Optional; +@Repository +public interface RoleRepository extends JpaRepository { + Optional findByName(ERole name); +} diff --git a/src/main/java/com/booleanuk/api/cinema/repository/ScreeningRepository.java b/src/main/java/com/booleanuk/api/cinema/repository/ScreeningRepository.java new file mode 100755 index 0000000..11d47fc --- /dev/null +++ b/src/main/java/com/booleanuk/api/cinema/repository/ScreeningRepository.java @@ -0,0 +1,8 @@ +package com.booleanuk.api.cinema.repository; + +import com.booleanuk.api.cinema.models.Screening; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +@Repository +public interface ScreeningRepository extends JpaRepository {} diff --git a/src/main/java/com/booleanuk/api/cinema/repository/TicketRepository.java b/src/main/java/com/booleanuk/api/cinema/repository/TicketRepository.java new file mode 100755 index 0000000..43e362e --- /dev/null +++ b/src/main/java/com/booleanuk/api/cinema/repository/TicketRepository.java @@ -0,0 +1,15 @@ +package com.booleanuk.api.cinema.repository; + +import com.booleanuk.api.cinema.models.Screening; +import com.booleanuk.api.cinema.models.Ticket; +import com.booleanuk.api.cinema.models.User; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import java.util.List; +import java.util.Optional; + +@Repository +public interface TicketRepository extends JpaRepository { + List findTicketsByCustomerAndScreening(User customer, Screening screening); +} diff --git a/src/main/java/com/booleanuk/api/cinema/repository/UserRepository.java b/src/main/java/com/booleanuk/api/cinema/repository/UserRepository.java new file mode 100755 index 0000000..398f10a --- /dev/null +++ b/src/main/java/com/booleanuk/api/cinema/repository/UserRepository.java @@ -0,0 +1,15 @@ +package com.booleanuk.api.cinema.repository; + +import com.booleanuk.api.cinema.models.User; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import java.util.Optional; + +@Repository +public interface UserRepository extends JpaRepository { + Optional findByUsername(String username); + + Boolean existsByUsername(String username); + Boolean existsByEmail(String email); +} diff --git a/src/main/java/com/booleanuk/api/cinema/security/WebSecurityConfig.java b/src/main/java/com/booleanuk/api/cinema/security/WebSecurityConfig.java new file mode 100755 index 0000000..16fe73e --- /dev/null +++ b/src/main/java/com/booleanuk/api/cinema/security/WebSecurityConfig.java @@ -0,0 +1,71 @@ +package com.booleanuk.api.cinema.security; + +import com.booleanuk.api.cinema.security.jwt.AuthEntryPointJwt; +import com.booleanuk.api.cinema.security.jwt.AuthTokenFilter; +import com.booleanuk.api.cinema.security.services.UserDetailsServiceImpl; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.authentication.dao.DaoAuthenticationProvider; +import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration; +import org.springframework.security.config.annotation.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.crypto.password.PasswordEncoder; +import org.springframework.security.web.SecurityFilterChain; +import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; + +@Configuration +@EnableWebSecurity +public class WebSecurityConfig { + @Autowired + private UserDetailsServiceImpl userDetailsService; + + @Autowired + private AuthEntryPointJwt unauthorizedHandler; + + @Bean + public AuthTokenFilter authenticationJwtTokenFilter() { + return new AuthTokenFilter(); + } + + @Bean + public DaoAuthenticationProvider authenticationProvider() { + DaoAuthenticationProvider authProvider = new DaoAuthenticationProvider(); + + authProvider.setUserDetailsService(userDetailsService); + authProvider.setPasswordEncoder(passwordEncoder()); + + return authProvider; + } + + @Bean + public PasswordEncoder passwordEncoder() { + return new BCryptPasswordEncoder(); + } + + @Bean + public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { + http + .csrf((csrf) -> csrf.disable()) + .exceptionHandling((exception) -> exception.authenticationEntryPoint(unauthorizedHandler)) + .sessionManagement((session) -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) + .authorizeHttpRequests((requests) -> requests + .requestMatchers("/auth/**").permitAll() + .requestMatchers("/books", "/books/**").authenticated() //Add permissions here + .requestMatchers("/movies", "/movies/**").permitAll() + .requestMatchers("/screenings", "/screenings/**").permitAll() + .requestMatchers("/customers", "/customers/**").permitAll() + ); + http.authenticationProvider(authenticationProvider()); + http.addFilterBefore(authenticationJwtTokenFilter(), UsernamePasswordAuthenticationFilter.class); + return http.build(); + } + + @Bean + public AuthenticationManager authenticationManager(AuthenticationConfiguration authenticationConfiguration) throws Exception { + return authenticationConfiguration.getAuthenticationManager(); + } +} diff --git a/src/main/java/com/booleanuk/api/cinema/security/jwt/AuthEntryPointJwt.java b/src/main/java/com/booleanuk/api/cinema/security/jwt/AuthEntryPointJwt.java new file mode 100755 index 0000000..d99b130 --- /dev/null +++ b/src/main/java/com/booleanuk/api/cinema/security/jwt/AuthEntryPointJwt.java @@ -0,0 +1,24 @@ +package com.booleanuk.api.cinema.security.jwt; + +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.web.AuthenticationEntryPoint; +import org.springframework.stereotype.Component; + +import java.io.IOException; + +@Component +public class AuthEntryPointJwt implements AuthenticationEntryPoint { + private static final Logger logger = LoggerFactory.getLogger(AuthEntryPointJwt.class); + + @Override + public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException { + logger.error("Unauthorized error: {}", authException.getMessage()); + response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Error: Unauthorized"); + } + +} diff --git a/src/main/java/com/booleanuk/api/cinema/security/jwt/AuthTokenFilter.java b/src/main/java/com/booleanuk/api/cinema/security/jwt/AuthTokenFilter.java new file mode 100755 index 0000000..acb1332 --- /dev/null +++ b/src/main/java/com/booleanuk/api/cinema/security/jwt/AuthTokenFilter.java @@ -0,0 +1,54 @@ +package com.booleanuk.api.cinema.security.jwt; + +import com.booleanuk.api.cinema.security.services.UserDetailsServiceImpl; +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.web.authentication.WebAuthenticationDetailsSource; +import org.springframework.util.StringUtils; +import org.springframework.web.filter.OncePerRequestFilter; + +import java.io.IOException; + +public class AuthTokenFilter extends OncePerRequestFilter { + @Autowired + private JwtUtils jwtUtils; + + @Autowired + private UserDetailsServiceImpl userDetailsService; + + private static final Logger logger = LoggerFactory.getLogger(AuthTokenFilter.class); + + @Override + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) + throws ServletException, IOException { + try { + String jwt = parseJwt(request); + if (jwt != null && jwtUtils.validateJwtToken(jwt)) { + String username = jwtUtils.getUserNameFromJwtToken(jwt); + UserDetails userDetails = userDetailsService.loadUserByUsername(username); + UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities()); + authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); + SecurityContextHolder.getContext().setAuthentication(authentication); + } + } catch (Exception e) { + logger.error("Cannot set user authentication: {}", e.getMessage()); + } + filterChain.doFilter(request, response); + } + + private String parseJwt(HttpServletRequest request) { + String headerAuth = request.getHeader("Authorization"); + if (StringUtils.hasText(headerAuth) && headerAuth.startsWith("Bearer ")) { + return headerAuth.substring(7); + } + return null; + } +} diff --git a/src/main/java/com/booleanuk/api/cinema/security/jwt/JwtUtils.java b/src/main/java/com/booleanuk/api/cinema/security/jwt/JwtUtils.java new file mode 100755 index 0000000..a759201 --- /dev/null +++ b/src/main/java/com/booleanuk/api/cinema/security/jwt/JwtUtils.java @@ -0,0 +1,63 @@ +package com.booleanuk.api.cinema.security.jwt; + +import com.booleanuk.api.cinema.security.services.UserDetailsImpl; +import io.jsonwebtoken.ExpiredJwtException; +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.MalformedJwtException; +import io.jsonwebtoken.UnsupportedJwtException; +import io.jsonwebtoken.io.Decoders; +import io.jsonwebtoken.security.Keys; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.security.core.Authentication; +import org.springframework.stereotype.Component; + +import javax.crypto.SecretKey; +import java.util.Date; + +@Component +public class JwtUtils { + private static final Logger logger = LoggerFactory.getLogger(JwtUtils.class); + + @Value("${booleanuk.app.jwtSecret}") + private String jwtSecret; + + @Value("${booleanuk.app.jwtExpirationMs}") + private int jwtExpirationMs; + + public String generateJwtToken(Authentication authentication) { + UserDetailsImpl userPrincipal = (UserDetailsImpl) authentication.getPrincipal(); + + return Jwts.builder() + .subject((userPrincipal.getUsername())) + .issuedAt(new Date()) + .expiration(new Date((new Date()).getTime() + this.jwtExpirationMs)) + .signWith(this.key()) + .compact(); + } + + private SecretKey key() { + return Keys.hmacShaKeyFor(Decoders.BASE64.decode(this.jwtSecret)); + } + + public String getUserNameFromJwtToken(String token) { + return Jwts.parser().verifyWith(this.key()).build().parseSignedClaims(token).getPayload().getSubject(); + } + + public boolean validateJwtToken(String authToken) { + try { + Jwts.parser().verifyWith(this.key()).build().parse(authToken); + return true; + } catch (MalformedJwtException e) { + logger.error("Invalid JWT token: {}", e.getMessage()); + } catch (ExpiredJwtException e) { + logger.error("JWT token has expired: {}", e.getMessage()); + } catch (UnsupportedJwtException e) { + logger.error("JWT token is unsupported: {}", e.getMessage()); + } catch (IllegalArgumentException e) { + logger.error("JWT claims string is empty: {}", e.getMessage()); + } + return false; + } +} diff --git a/src/main/java/com/booleanuk/api/cinema/security/services/UserDetailsImpl.java b/src/main/java/com/booleanuk/api/cinema/security/services/UserDetailsImpl.java new file mode 100755 index 0000000..9587e35 --- /dev/null +++ b/src/main/java/com/booleanuk/api/cinema/security/services/UserDetailsImpl.java @@ -0,0 +1,59 @@ +package com.booleanuk.api.cinema.security.services; + +import com.booleanuk.api.cinema.models.User; +import com.fasterxml.jackson.annotation.JsonIgnore; +import lombok.Getter; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.core.userdetails.UserDetails; + +import java.util.Collection; +import java.util.List; +import java.util.Objects; +import java.util.stream.Collectors; + +@Getter +public class UserDetailsImpl implements UserDetails { + private static final long serialVersionUID = 1L; + + private int id; + private String username; + private String email; + + @JsonIgnore + private String password; + + private Collection authorities; + + public UserDetailsImpl(int id, String username, String email, String password, Collection authorities) { + this.id = id; + this.username = username; + this.email = email; + this.password = password; + this.authorities = authorities; + } + + public static UserDetailsImpl build(User user) { + List authorities = user.getRoles().stream() + .map(role -> new SimpleGrantedAuthority(role.getName().name())) + .collect(Collectors.toList()); + return new UserDetailsImpl( + user.getId(), + user.getUsername(), + user.getEmail(), + user.getPassword(), + authorities); + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + UserDetailsImpl user = (UserDetailsImpl) o; + return Objects.equals(id, user.id); + } +} diff --git a/src/main/java/com/booleanuk/api/cinema/security/services/UserDetailsServiceImpl.java b/src/main/java/com/booleanuk/api/cinema/security/services/UserDetailsServiceImpl.java new file mode 100755 index 0000000..893a098 --- /dev/null +++ b/src/main/java/com/booleanuk/api/cinema/security/services/UserDetailsServiceImpl.java @@ -0,0 +1,26 @@ +package com.booleanuk.api.cinema.security.services; + +import com.booleanuk.api.cinema.models.User; +import com.booleanuk.api.cinema.repository.UserRepository; +import jakarta.transaction.Transactional; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.core.userdetails.UsernameNotFoundException; +import org.springframework.stereotype.Service; + +@Service +public class UserDetailsServiceImpl implements UserDetailsService { + @Autowired + UserRepository userRepository; + + @Override + @Transactional + public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { + User user = userRepository.findByUsername(username).orElseThrow( + () -> new UsernameNotFoundException("User not found with username: " + username) + ); + return UserDetailsImpl.build(user); + + } +} diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml old mode 100644 new mode 100755 index 02c2591..7a38bc0 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -1,7 +1,28 @@ -server: - port: 4000 - error: - include-message: always - include-binding-errors: always - include-stacktrace: never - include-exception: false +server: + port: 4000 + error: + include-message: always + include-binding-errors: always + include-stacktrace: never + include-exception: false + +spring: + datasource: + url: ${DB_URL} + username: ${DB_USERNAME} + password: ${DB_PW} + + max-active: 3 + max-idle: 3 + jpa: + hibernate: + ddl-auto: update + properties: + hibernate: + dialect: org.hibernate.dialect.PostgreSQLDialect + format_sql: true + show-sql: true +booleanuk: + app: + jwtSecret: ===============================BooleanUK=Spring============================================== + jwtExpirationMs: 86400000 diff --git a/src/test/java/com/booleanuk/api/.gitkeep b/src/test/java/com/booleanuk/api/cinema/.gitkeep old mode 100644 new mode 100755 similarity index 100% rename from src/test/java/com/booleanuk/api/.gitkeep rename to src/test/java/com/booleanuk/api/cinema/.gitkeep From 9f5416c98a80e91d73e1db5e7e4676a8caebf110 Mon Sep 17 00:00:00 2001 From: lucasholter00 Date: Tue, 26 Aug 2025 10:59:00 +0200 Subject: [PATCH 2/3] Add .env to gitignore, whoopsie --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index c2065bc..172dbf7 100644 --- a/.gitignore +++ b/.gitignore @@ -35,3 +35,4 @@ out/ ### VS Code ### .vscode/ +.env From 94ef893900d3f0acd71b90f6824af3e6214f9f28 Mon Sep 17 00:00:00 2001 From: lucasholter00 Date: Tue, 26 Aug 2025 11:00:01 +0200 Subject: [PATCH 3/3] Remove .env --- docker/.env | 2 -- 1 file changed, 2 deletions(-) delete mode 100644 docker/.env diff --git a/docker/.env b/docker/.env deleted file mode 100644 index 6f609ac..0000000 --- a/docker/.env +++ /dev/null @@ -1,2 +0,0 @@ -DB_PW=admin -DB_USERNAME=postgres