diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2221589..20a3ced 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -15,11 +15,11 @@ jobs: steps: - uses: actions/checkout@v4 - - name: Set up JDK 8 + - name: Set up JDK 17 uses: actions/setup-java@v4 with: distribution: temurin - java-version: '8' + java-version: '17' cache: maven - name: Build and run tests diff --git a/UserFront/pom.xml b/UserFront/pom.xml index bdc2944..eb1245d 100644 --- a/UserFront/pom.xml +++ b/UserFront/pom.xml @@ -14,14 +14,14 @@ org.springframework.boot spring-boot-starter-parent - 1.5.4.RELEASE + 3.3.5 UTF-8 UTF-8 - 1.8 + 17 @@ -46,8 +46,8 @@ - mysql - mysql-connector-java + com.mysql + mysql-connector-j @@ -72,6 +72,13 @@ spring-security-test test + + + + org.junit.vintage + junit-vintage-engine + test + diff --git a/UserFront/src/main/java/com/userFront/config/PasswordEncoderConfig.java b/UserFront/src/main/java/com/userFront/config/PasswordEncoderConfig.java new file mode 100644 index 0000000..93683f4 --- /dev/null +++ b/UserFront/src/main/java/com/userFront/config/PasswordEncoderConfig.java @@ -0,0 +1,14 @@ +package com.userFront.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; + +@Configuration +public class PasswordEncoderConfig { + + @Bean + public BCryptPasswordEncoder passwordEncoder() { + return new BCryptPasswordEncoder(); + } +} diff --git a/UserFront/src/main/java/com/userFront/config/RequestFilter.java b/UserFront/src/main/java/com/userFront/config/RequestFilter.java deleted file mode 100644 index 65b14ce..0000000 --- a/UserFront/src/main/java/com/userFront/config/RequestFilter.java +++ /dev/null @@ -1,54 +0,0 @@ -package com.userFront.config; - -import javax.servlet.Filter; -import javax.servlet.FilterChain; -import javax.servlet.FilterConfig; -import javax.servlet.ServletRequest; -import javax.servlet.ServletResponse; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.core.Ordered; -import org.springframework.core.annotation.Order; -import org.springframework.stereotype.Component; - -@Component -@Order(Ordered.HIGHEST_PRECEDENCE) -public class RequestFilter implements Filter { - - private static final Logger LOG = LoggerFactory.getLogger(RequestFilter.class); - - public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) { - HttpServletResponse response = (HttpServletResponse) res; - HttpServletRequest request = (HttpServletRequest) req; - - response.setHeader("Access-Control-Allow-Origin", "http://localhost:4200"); //8081 - response.setHeader("Access-Control-Allow-Methods", "POST, PUT, GET, OPTIONS, DELETE"); - response.setHeader("Access-Control-Allow-Headers", "x-requested-with"); - response.setHeader("Access-Control-Max-Age", "3600"); - response.setHeader("Access-Control-Allow-Credentials", "true"); - - if (!(request.getMethod().equalsIgnoreCase("OPTIONS"))) { - try { - chain.doFilter(req, res); - } catch(Exception e) { - LOG.error("Error while processing request in RequestFilter", e); - } - } else { - LOG.debug("Handling CORS pre-flight request"); - response.setHeader("Access-Control-Allow-Methods", "POST,GET,DELETE"); - response.setHeader("Access-Control-Max-Age", "3600"); - response.setHeader("Access-Control-Allow-Headers", "authorization, content-type," + - "access-control-request-headers,access-control-request-method,accept,origin,authorization,x-requested-with"); - response.setStatus(HttpServletResponse.SC_OK); - } - - } - - public void init(FilterConfig filterConfig) {} - - public void destroy() {} - -} diff --git a/UserFront/src/main/java/com/userFront/config/SecurityConfig.java b/UserFront/src/main/java/com/userFront/config/SecurityConfig.java index c74bfa0..025dbf8 100644 --- a/UserFront/src/main/java/com/userFront/config/SecurityConfig.java +++ b/UserFront/src/main/java/com/userFront/config/SecurityConfig.java @@ -1,34 +1,33 @@ package com.userFront.config; +import java.util.Arrays; + import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; -import org.springframework.core.env.Environment; -import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; -import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity; +import org.springframework.security.authentication.dao.DaoAuthenticationProvider; +import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; -import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.security.web.SecurityFilterChain; import org.springframework.security.web.util.matcher.AntPathRequestMatcher; +import org.springframework.web.cors.CorsConfiguration; +import org.springframework.web.cors.CorsConfigurationSource; +import org.springframework.web.cors.UrlBasedCorsConfigurationSource; import com.userFront.service.UserServiceImpl.UserSecurityService; @Configuration @EnableWebSecurity -@EnableGlobalMethodSecurity(prePostEnabled=true) -public class SecurityConfig extends WebSecurityConfigurerAdapter { - - @Autowired - private Environment env; +@EnableMethodSecurity(prePostEnabled = true) +public class SecurityConfig { @Autowired private UserSecurityService userSecurityService; - @Bean - public BCryptPasswordEncoder passwordEncoder() { - return new BCryptPasswordEncoder(); - } + @Autowired + private BCryptPasswordEncoder passwordEncoder; private static final String[] PUBLIC_MATCHERS = { "/webjars/**", @@ -38,35 +37,59 @@ public BCryptPasswordEncoder passwordEncoder() { "/", "/about/**", "/contact/**", - "/error/**/*", + "/error/**", "/console/**", "/signup" }; - @Override - protected void configure(HttpSecurity http) throws Exception { - http - .authorizeRequests(). -// antMatchers("/**"). - antMatchers(PUBLIC_MATCHERS). - permitAll().anyRequest().authenticated(); - - http - .csrf().disable().cors().disable() - .formLogin().failureUrl("/index?error").defaultSuccessUrl("/userFront").loginPage("/index").permitAll() - .and() - .logout().logoutRequestMatcher(new AntPathRequestMatcher("/logout")).logoutSuccessUrl("/index?logout").deleteCookies("remember-me").permitAll() - .and() - .rememberMe(); + @Bean + public DaoAuthenticationProvider authenticationProvider() { + DaoAuthenticationProvider provider = new DaoAuthenticationProvider(); + provider.setUserDetailsService(userSecurityService); + provider.setPasswordEncoder(passwordEncoder); + return provider; } + @Bean + public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { + http + .cors(cors -> cors.configurationSource(corsConfigurationSource())) + // CSRF is enabled (Spring Security default). The server-rendered + // Thymeleaf forms receive the token automatically; the cross-origin + // JSON API consumed by the Angular SPA is exempted and relies on the + // CORS allowlist plus the authenticated session. Full SPA CSRF token + // handling is addressed with the frontend rewrite (Phase 7). + .csrf(csrf -> csrf.ignoringRequestMatchers("/api/**")) + .authorizeHttpRequests(auth -> auth + .requestMatchers(PUBLIC_MATCHERS).permitAll() + .anyRequest().authenticated()) + .formLogin(form -> form + .loginPage("/index") + .failureUrl("/index?error") + .defaultSuccessUrl("/userFront") + .permitAll()) + .logout(logout -> logout + .logoutRequestMatcher(new AntPathRequestMatcher("/logout")) + .logoutSuccessUrl("/index?logout") + .deleteCookies("remember-me") + .permitAll()) + .rememberMe(remember -> { + }); - - @Autowired - public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception { -// auth.inMemoryAuthentication().withUser("user").password("password").roles("USER"); //This is in-memory authentication - auth.userDetailsService(userSecurityService).passwordEncoder(passwordEncoder()); + return http.build(); } + @Bean + public CorsConfigurationSource corsConfigurationSource() { + CorsConfiguration configuration = new CorsConfiguration(); + configuration.setAllowedOrigins(Arrays.asList("http://localhost:4200")); + configuration.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "DELETE", "OPTIONS")); + configuration.setAllowedHeaders(Arrays.asList("*")); + configuration.setAllowCredentials(true); + configuration.setMaxAge(3600L); + UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); + source.registerCorsConfiguration("/**", configuration); + return source; + } } diff --git a/UserFront/src/main/java/com/userFront/domain/Appointment.java b/UserFront/src/main/java/com/userFront/domain/Appointment.java index 1d6623e..7fd9560 100644 --- a/UserFront/src/main/java/com/userFront/domain/Appointment.java +++ b/UserFront/src/main/java/com/userFront/domain/Appointment.java @@ -2,12 +2,12 @@ import java.util.Date; -import javax.persistence.Entity; -import javax.persistence.GeneratedValue; -import javax.persistence.GenerationType; -import javax.persistence.Id; -import javax.persistence.JoinColumn; -import javax.persistence.ManyToOne; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; @Entity public class Appointment { diff --git a/UserFront/src/main/java/com/userFront/domain/PrimaryAccount.java b/UserFront/src/main/java/com/userFront/domain/PrimaryAccount.java index 0354bfd..d93cd89 100644 --- a/UserFront/src/main/java/com/userFront/domain/PrimaryAccount.java +++ b/UserFront/src/main/java/com/userFront/domain/PrimaryAccount.java @@ -3,13 +3,13 @@ import java.math.BigDecimal; import java.util.List; -import javax.persistence.CascadeType; -import javax.persistence.Entity; -import javax.persistence.FetchType; -import javax.persistence.GeneratedValue; -import javax.persistence.GenerationType; -import javax.persistence.Id; -import javax.persistence.OneToMany; +import jakarta.persistence.CascadeType; +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.OneToMany; import com.fasterxml.jackson.annotation.JsonIgnore; diff --git a/UserFront/src/main/java/com/userFront/domain/PrimaryTransaction.java b/UserFront/src/main/java/com/userFront/domain/PrimaryTransaction.java index f0d2395..e8792b2 100644 --- a/UserFront/src/main/java/com/userFront/domain/PrimaryTransaction.java +++ b/UserFront/src/main/java/com/userFront/domain/PrimaryTransaction.java @@ -3,12 +3,12 @@ import java.math.BigDecimal; import java.util.Date; -import javax.persistence.Entity; -import javax.persistence.GeneratedValue; -import javax.persistence.GenerationType; -import javax.persistence.Id; -import javax.persistence.JoinColumn; -import javax.persistence.ManyToOne; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; @Entity public class PrimaryTransaction { diff --git a/UserFront/src/main/java/com/userFront/domain/Recipient.java b/UserFront/src/main/java/com/userFront/domain/Recipient.java index 9ec1f70..80a6a37 100644 --- a/UserFront/src/main/java/com/userFront/domain/Recipient.java +++ b/UserFront/src/main/java/com/userFront/domain/Recipient.java @@ -1,11 +1,11 @@ package com.userFront.domain; -import javax.persistence.Entity; -import javax.persistence.GeneratedValue; -import javax.persistence.GenerationType; -import javax.persistence.Id; -import javax.persistence.JoinColumn; -import javax.persistence.ManyToOne; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; import com.fasterxml.jackson.annotation.JsonIgnore; diff --git a/UserFront/src/main/java/com/userFront/domain/SavingsAccount.java b/UserFront/src/main/java/com/userFront/domain/SavingsAccount.java index 922a806..c064912 100644 --- a/UserFront/src/main/java/com/userFront/domain/SavingsAccount.java +++ b/UserFront/src/main/java/com/userFront/domain/SavingsAccount.java @@ -3,13 +3,13 @@ import java.math.BigDecimal; import java.util.List; -import javax.persistence.CascadeType; -import javax.persistence.Entity; -import javax.persistence.FetchType; -import javax.persistence.GeneratedValue; -import javax.persistence.GenerationType; -import javax.persistence.Id; -import javax.persistence.OneToMany; +import jakarta.persistence.CascadeType; +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.OneToMany; import com.fasterxml.jackson.annotation.JsonIgnore; diff --git a/UserFront/src/main/java/com/userFront/domain/SavingsTransaction.java b/UserFront/src/main/java/com/userFront/domain/SavingsTransaction.java index 8a5ac55..f19a3bc 100644 --- a/UserFront/src/main/java/com/userFront/domain/SavingsTransaction.java +++ b/UserFront/src/main/java/com/userFront/domain/SavingsTransaction.java @@ -3,12 +3,12 @@ import java.math.BigDecimal; import java.util.Date; -import javax.persistence.Entity; -import javax.persistence.GeneratedValue; -import javax.persistence.GenerationType; -import javax.persistence.Id; -import javax.persistence.JoinColumn; -import javax.persistence.ManyToOne; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; @Entity public class SavingsTransaction { diff --git a/UserFront/src/main/java/com/userFront/domain/User.java b/UserFront/src/main/java/com/userFront/domain/User.java index 3b14e71..89c0222 100644 --- a/UserFront/src/main/java/com/userFront/domain/User.java +++ b/UserFront/src/main/java/com/userFront/domain/User.java @@ -5,15 +5,15 @@ import java.util.List; import java.util.Set; -import javax.persistence.CascadeType; -import javax.persistence.Column; -import javax.persistence.Entity; -import javax.persistence.FetchType; -import javax.persistence.GeneratedValue; -import javax.persistence.GenerationType; -import javax.persistence.Id; -import javax.persistence.OneToMany; -import javax.persistence.OneToOne; +import jakarta.persistence.CascadeType; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.OneToMany; +import jakarta.persistence.OneToOne; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.userdetails.UserDetails; diff --git a/UserFront/src/main/java/com/userFront/domain/security/Role.java b/UserFront/src/main/java/com/userFront/domain/security/Role.java index 45aa369..65e1ec0 100644 --- a/UserFront/src/main/java/com/userFront/domain/security/Role.java +++ b/UserFront/src/main/java/com/userFront/domain/security/Role.java @@ -1,6 +1,6 @@ package com.userFront.domain.security; -import javax.persistence.*; +import jakarta.persistence.*; import java.util.HashSet; import java.util.Set; diff --git a/UserFront/src/main/java/com/userFront/domain/security/UserRole.java b/UserFront/src/main/java/com/userFront/domain/security/UserRole.java index a72a53d..48e5f07 100644 --- a/UserFront/src/main/java/com/userFront/domain/security/UserRole.java +++ b/UserFront/src/main/java/com/userFront/domain/security/UserRole.java @@ -2,7 +2,7 @@ import com.userFront.domain.User; -import javax.persistence.*; +import jakarta.persistence.*; @Entity diff --git a/UserFront/src/main/java/com/userFront/service/UserServiceImpl/AppointmentServiceImpl.java b/UserFront/src/main/java/com/userFront/service/UserServiceImpl/AppointmentServiceImpl.java index 838ad22..0d62b84 100644 --- a/UserFront/src/main/java/com/userFront/service/UserServiceImpl/AppointmentServiceImpl.java +++ b/UserFront/src/main/java/com/userFront/service/UserServiceImpl/AppointmentServiceImpl.java @@ -24,7 +24,7 @@ public List findAll() { } public Appointment findAppointment(Long id) { - return appointmentDao.findOne(id); + return appointmentDao.findById(id).orElse(null); } public void confirmAppointment(Long id) { diff --git a/UserFront/src/main/resources/application.properties b/UserFront/src/main/resources/application.properties index 94c53c4..ee307e9 100644 --- a/UserFront/src/main/resources/application.properties +++ b/UserFront/src/main/resources/application.properties @@ -1,3 +1,8 @@ +# The service layer contains mutual dependencies between UserService, +# AccountService and TransactionService. Spring Boot 2.6+ prohibits circular +# references by default; allow them to preserve the existing wiring. +spring.main.allow-circular-references = true + # =============================== # = DATA SOURCE # =============================== @@ -33,5 +38,5 @@ spring.jpa.show-sql = true spring.jpa.hibernate.ddl-auto = update # Allows Hibernate to generate SQL optimized for a particular DBMS -spring.jpa.properties.hibernate.dialect = org.hibernate.dialect.MySQL5Dialect +spring.jpa.properties.hibernate.dialect = org.hibernate.dialect.MySQLDialect diff --git a/UserFront/src/test/resources/application.properties b/UserFront/src/test/resources/application.properties index 1084b77..bf8ae0d 100644 --- a/UserFront/src/test/resources/application.properties +++ b/UserFront/src/test/resources/application.properties @@ -4,7 +4,11 @@ # Integration tests run against an in-memory H2 database so no external # MySQL instance is required in CI. -spring.datasource.url = jdbc:h2:mem:onlinebanking;DB_CLOSE_DELAY=-1;MODE=MySQL +# Mirror the production setting: allow the mutual service-layer dependencies +# that Spring Boot 2.6+ rejects by default. +spring.main.allow-circular-references = true + +spring.datasource.url = jdbc:h2:mem:onlinebanking;DB_CLOSE_DELAY=-1;MODE=MySQL;NON_KEYWORDS=USER spring.datasource.username = sa spring.datasource.password = spring.datasource.driver-class-name = org.h2.Driver