From 1294d6445ef318d751e13328ae7ab87ada0c3047 Mon Sep 17 00:00:00 2001 From: Your Name Date: Tue, 26 Aug 2025 14:15:40 +0200 Subject: [PATCH 1/2] ex finished --- .gitignore | 7 + build.gradle | 61 +++-- docker/Dockerfile | 9 + gradlew | 240 ------------------ .../com/booleanuk/api/{ => cinema}/.gitkeep | 0 .../java/com/booleanuk/api/cinema/Main.java | 11 + .../api/cinema/library/SecurityConfig.java | 53 ++++ .../controllers/AuthenticationController.java | 55 ++++ .../controllers/CustomerController.java | 88 +++++++ .../library/controllers/MovieController.java | 80 ++++++ .../controllers/ScreeningController.java | 68 +++++ .../api/cinema/library/models/Customer.java | 44 ++++ .../api/cinema/library/models/Movie.java | 49 ++++ .../api/cinema/library/models/Role.java | 6 + .../api/cinema/library/models/Screening.java | 52 ++++ .../api/cinema/library/models/Ticket.java | 41 +++ .../api/cinema/library/models/User.java | 41 +++ .../payload/request/CustomerRequest.java | 10 + .../library/payload/request/LoginRequest.java | 10 + .../library/payload/request/MovieRequest.java | 11 + .../payload/request/ScreeningRequest.java | 14 + .../payload/request/SignupRequest.java | 13 + .../payload/response/AuthResponse.java | 10 + .../payload/response/CustomerResponse.java | 18 ++ .../payload/response/MovieResponse.java | 19 ++ .../payload/response/ScreeningResponse.java | 19 ++ .../repository/CustomerRepository.java | 7 + .../library/repository/MovieRepository.java | 7 + .../repository/ScreeningRepository.java | 10 + .../library/repository/TicketRepository.java | 11 + .../library/repository/UserRepository.java | 10 + .../security/jwt/JwtRequestFilter.java | 53 ++++ .../cinema/library/security/jwt/JwtUtil.java | 60 +++++ src/main/resources/application.yml | 7 - .../com/booleanuk/api/{ => cinema}/.gitkeep | 0 35 files changed, 930 insertions(+), 264 deletions(-) create mode 100644 docker/Dockerfile rename src/main/java/com/booleanuk/api/{ => cinema}/.gitkeep (100%) create mode 100644 src/main/java/com/booleanuk/api/cinema/Main.java create mode 100644 src/main/java/com/booleanuk/api/cinema/library/SecurityConfig.java create mode 100644 src/main/java/com/booleanuk/api/cinema/library/controllers/AuthenticationController.java create mode 100644 src/main/java/com/booleanuk/api/cinema/library/controllers/CustomerController.java create mode 100644 src/main/java/com/booleanuk/api/cinema/library/controllers/MovieController.java create mode 100644 src/main/java/com/booleanuk/api/cinema/library/controllers/ScreeningController.java create mode 100644 src/main/java/com/booleanuk/api/cinema/library/models/Customer.java create mode 100644 src/main/java/com/booleanuk/api/cinema/library/models/Movie.java create mode 100644 src/main/java/com/booleanuk/api/cinema/library/models/Role.java create mode 100644 src/main/java/com/booleanuk/api/cinema/library/models/Screening.java create mode 100644 src/main/java/com/booleanuk/api/cinema/library/models/Ticket.java create mode 100644 src/main/java/com/booleanuk/api/cinema/library/models/User.java create mode 100644 src/main/java/com/booleanuk/api/cinema/library/payload/request/CustomerRequest.java create mode 100644 src/main/java/com/booleanuk/api/cinema/library/payload/request/LoginRequest.java create mode 100644 src/main/java/com/booleanuk/api/cinema/library/payload/request/MovieRequest.java create mode 100644 src/main/java/com/booleanuk/api/cinema/library/payload/request/ScreeningRequest.java create mode 100644 src/main/java/com/booleanuk/api/cinema/library/payload/request/SignupRequest.java create mode 100644 src/main/java/com/booleanuk/api/cinema/library/payload/response/AuthResponse.java create mode 100644 src/main/java/com/booleanuk/api/cinema/library/payload/response/CustomerResponse.java create mode 100644 src/main/java/com/booleanuk/api/cinema/library/payload/response/MovieResponse.java create mode 100644 src/main/java/com/booleanuk/api/cinema/library/payload/response/ScreeningResponse.java create mode 100644 src/main/java/com/booleanuk/api/cinema/library/repository/CustomerRepository.java create mode 100644 src/main/java/com/booleanuk/api/cinema/library/repository/MovieRepository.java create mode 100644 src/main/java/com/booleanuk/api/cinema/library/repository/ScreeningRepository.java create mode 100644 src/main/java/com/booleanuk/api/cinema/library/repository/TicketRepository.java create mode 100644 src/main/java/com/booleanuk/api/cinema/library/repository/UserRepository.java create mode 100644 src/main/java/com/booleanuk/api/cinema/library/security/jwt/JwtRequestFilter.java create mode 100644 src/main/java/com/booleanuk/api/cinema/library/security/jwt/JwtUtil.java delete mode 100644 src/main/resources/application.yml rename src/test/java/com/booleanuk/api/{ => cinema}/.gitkeep (100%) diff --git a/.gitignore b/.gitignore index c2065bc..765afb4 100644 --- a/.gitignore +++ b/.gitignore @@ -35,3 +35,10 @@ out/ ### VS Code ### .vscode/ + +docker/docker-compose.yml + +src/main/resources/application.yml + +docker/*.jar +build/libs/*.jar diff --git a/build.gradle b/build.gradle index fb070c4..2bc2ac9 100644 --- a/build.gradle +++ b/build.gradle @@ -1,33 +1,60 @@ 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.2-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.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() + useJUnitPlatform() } + +jar { + duplicatesStrategy = DuplicatesStrategy.EXCLUDE + manifest { + attributes 'Main-Class': 'com.booleanuk.Main' + } + + from { + configurations.runtimeClasspath.collect { it.isDirectory() ? it : zipTree(it) } + } +} \ No newline at end of file diff --git a/docker/Dockerfile b/docker/Dockerfile new file mode 100644 index 0000000..8214327 --- /dev/null +++ b/docker/Dockerfile @@ -0,0 +1,9 @@ +FROM eclipse-temurin:21-jdk-jammy + +WORKDIR /app + +COPY java.docker.day.2-0.0.2-SNAPSHOT.jar /app/app.jar + +EXPOSE 4000 + +ENTRYPOINT ["java", "-jar", "app.jar"] diff --git a/gradlew b/gradlew index a69d9cb..e69de29 100644 --- a/gradlew +++ b/gradlew @@ -1,240 +0,0 @@ -#!/bin/sh - -# -# Copyright © 2015-2021 the original authors. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# https://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# - -############################################################################## -# -# Gradle start up script for POSIX generated by Gradle. -# -# Important for running: -# -# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is -# noncompliant, but you have some other compliant shell such as ksh or -# bash, then to run this script, type that shell name before the whole -# command line, like: -# -# ksh Gradle -# -# Busybox and similar reduced shells will NOT work, because this script -# requires all of these POSIX shell features: -# * functions; -# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», -# «${var#prefix}», «${var%suffix}», and «$( cmd )»; -# * compound commands having a testable exit status, especially «case»; -# * various built-in commands including «command», «set», and «ulimit». -# -# Important for patching: -# -# (2) This script targets any POSIX shell, so it avoids extensions provided -# by Bash, Ksh, etc; in particular arrays are avoided. -# -# The "traditional" practice of packing multiple parameters into a -# space-separated string is a well documented source of bugs and security -# problems, so this is (mostly) avoided, by progressively accumulating -# options in "$@", and eventually passing that to Java. -# -# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, -# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; -# see the in-line comments for details. -# -# There are tweaks for specific operating systems such as AIX, CygWin, -# Darwin, MinGW, and NonStop. -# -# (3) This script is generated from the Groovy template -# https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt -# within the Gradle project. -# -# You can find Gradle at https://github.com/gradle/gradle/. -# -############################################################################## - -# Attempt to set APP_HOME - -# Resolve links: $0 may be a link -app_path=$0 - -# Need this for daisy-chained symlinks. -while - APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path - [ -h "$app_path" ] -do - ls=$( ls -ld "$app_path" ) - link=${ls#*' -> '} - case $link in #( - /*) app_path=$link ;; #( - *) app_path=$APP_HOME$link ;; - esac -done - -APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit - -APP_NAME="Gradle" -APP_BASE_NAME=${0##*/} - -# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' - -# Use the maximum available, or set MAX_FD != -1 to use that value. -MAX_FD=maximum - -warn () { - echo "$*" -} >&2 - -die () { - echo - echo "$*" - echo - exit 1 -} >&2 - -# OS specific support (must be 'true' or 'false'). -cygwin=false -msys=false -darwin=false -nonstop=false -case "$( uname )" in #( - CYGWIN* ) cygwin=true ;; #( - Darwin* ) darwin=true ;; #( - MSYS* | MINGW* ) msys=true ;; #( - NONSTOP* ) nonstop=true ;; -esac - -CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar - - -# Determine the Java command to use to start the JVM. -if [ -n "$JAVA_HOME" ] ; then - if [ -x "$JAVA_HOME/jre/sh/java" ] ; then - # IBM's JDK on AIX uses strange locations for the executables - JAVACMD=$JAVA_HOME/jre/sh/java - else - JAVACMD=$JAVA_HOME/bin/java - fi - if [ ! -x "$JAVACMD" ] ; then - die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME - -Please set the JAVA_HOME variable in your environment to match the -location of your Java installation." - fi -else - JAVACMD=java - which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. - -Please set the JAVA_HOME variable in your environment to match the -location of your Java installation." -fi - -# Increase the maximum file descriptors if we can. -if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then - case $MAX_FD in #( - max*) - MAX_FD=$( ulimit -H -n ) || - warn "Could not query maximum file descriptor limit" - esac - case $MAX_FD in #( - '' | soft) :;; #( - *) - ulimit -n "$MAX_FD" || - warn "Could not set maximum file descriptor limit to $MAX_FD" - esac -fi - -# Collect all arguments for the java command, stacking in reverse order: -# * args from the command line -# * the main class name -# * -classpath -# * -D...appname settings -# * --module-path (only if needed) -# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. - -# For Cygwin or MSYS, switch paths to Windows format before running java -if "$cygwin" || "$msys" ; then - APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) - CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) - - JAVACMD=$( cygpath --unix "$JAVACMD" ) - - # Now convert the arguments - kludge to limit ourselves to /bin/sh - for arg do - if - case $arg in #( - -*) false ;; # don't mess with options #( - /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath - [ -e "$t" ] ;; #( - *) false ;; - esac - then - arg=$( cygpath --path --ignore --mixed "$arg" ) - fi - # Roll the args list around exactly as many times as the number of - # args, so each arg winds up back in the position where it started, but - # possibly modified. - # - # NB: a `for` loop captures its iteration list before it begins, so - # changing the positional parameters here affects neither the number of - # iterations, nor the values presented in `arg`. - shift # remove old arg - set -- "$@" "$arg" # push replacement arg - done -fi - -# Collect all arguments for the java command; -# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of -# shell script including quotes and variable substitutions, so put them in -# double quotes to make sure that they get re-expanded; and -# * put everything else in single quotes, so that it's not re-expanded. - -set -- \ - "-Dorg.gradle.appname=$APP_BASE_NAME" \ - -classpath "$CLASSPATH" \ - org.gradle.wrapper.GradleWrapperMain \ - "$@" - -# Stop when "xargs" is not available. -if ! command -v xargs >/dev/null 2>&1 -then - die "xargs is not available" -fi - -# Use "xargs" to parse quoted args. -# -# With -n1 it outputs one arg per line, with the quotes and backslashes removed. -# -# In Bash we could simply go: -# -# readarray ARGS < <( xargs -n1 <<<"$var" ) && -# set -- "${ARGS[@]}" "$@" -# -# but POSIX shell has neither arrays nor command substitution, so instead we -# post-process each arg (as a line of input to sed) to backslash-escape any -# character that might be a shell metacharacter, then use eval to reverse -# that process (while maintaining the separation between arguments), and wrap -# the whole thing up as a single "set" statement. -# -# This will of course break if any of these variables contains a newline or -# an unmatched quote. -# - -eval "set -- $( - printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | - xargs -n1 | - sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | - tr '\n' ' ' - )" '"$@"' - -exec "$JAVACMD" "$@" diff --git a/src/main/java/com/booleanuk/api/.gitkeep b/src/main/java/com/booleanuk/api/cinema/.gitkeep 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 100644 index 0000000..c4fb60e --- /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 0000000..8e3cd29 --- /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 0000000..cc43f6e --- /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 0000000..3166cd5 --- /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 0000000..6be0052 --- /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 0000000..ffcb253 --- /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 0000000..661e8a3 --- /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 0000000..ddb8096 --- /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 0000000..ea743c1 --- /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 0000000..b3f84ac --- /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 0000000..fa365ba --- /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 0000000..a47b156 --- /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 0000000..c515168 --- /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 0000000..14edd6d --- /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 0000000..e9bbe75 --- /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 0000000..09b519d --- /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 0000000..d8f7e98 --- /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 0000000..6037846 --- /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 0000000..1bab4fe --- /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 0000000..3fc3203 --- /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 0000000..7c77927 --- /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 0000000..f44439c --- /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 0000000..5718ed5 --- /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 0000000..165996c --- /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 0000000..add86fb --- /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 0000000..d806591 --- /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 0000000..fecd8f9 --- /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 0000000..b3f1535 --- /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 diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml deleted file mode 100644 index 02c2591..0000000 --- a/src/main/resources/application.yml +++ /dev/null @@ -1,7 +0,0 @@ -server: - port: 4000 - error: - include-message: always - include-binding-errors: always - include-stacktrace: never - include-exception: false diff --git a/src/test/java/com/booleanuk/api/.gitkeep b/src/test/java/com/booleanuk/api/cinema/.gitkeep similarity index 100% rename from src/test/java/com/booleanuk/api/.gitkeep rename to src/test/java/com/booleanuk/api/cinema/.gitkeep From 2731204737b47a8b58aa82bdd0255c1ff205a893 Mon Sep 17 00:00:00 2001 From: Your Name Date: Tue, 26 Aug 2025 15:12:34 +0200 Subject: [PATCH 2/2] extension --- .gitignore | 2 -- docker/docker-compose.yml | 30 ++++++++++++++++++++++++++++++ 2 files changed, 30 insertions(+), 2 deletions(-) create mode 100644 docker/docker-compose.yml diff --git a/.gitignore b/.gitignore index 765afb4..0d281d9 100644 --- a/.gitignore +++ b/.gitignore @@ -36,8 +36,6 @@ out/ ### VS Code ### .vscode/ -docker/docker-compose.yml - src/main/resources/application.yml docker/*.jar diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml new file mode 100644 index 0000000..894a37e --- /dev/null +++ b/docker/docker-compose.yml @@ -0,0 +1,30 @@ +services: + app: + image: java-docker-day-2:0.0.2 + container_name: app + depends_on: + - db + ports: + - '4000:4000' + environment: + - SPRING_DATASOURCE_URL=jdbc:postgresql://db:5432/mypostgresuser + - SPRING_DATASOURCE_USERNAME=mypostgresuser + - SPRING_DATASOURCE_PASSWORD=mypostgrespassword + - SPRING_JPA_HIBERNATE_DDL_AUTO=update + + db: + image: postgres:latest + container_name: db + ports: + - '5432:5432' + environment: + - POSTGRES_USER=mypostgresuser + - POSTGRES_DATABASE=mypostgresuser + - POSTGRES_PASSWORD=mypostgrespassword + volumes: + - postgres-data:/var/lib/postgresql/data + +volumes: + postgres-data: + +