diff --git a/Dockerfile b/Dockerfile index 5d6e1cb..a5d1d6c 100644 --- a/Dockerfile +++ b/Dockerfile @@ -3,6 +3,7 @@ ENV DB_URL=jdbcConnectionString ENV USER_NAME=postgres ENV PASSWORD=password ENV CHANGELOG_VERSION=master.xml +ENV SPRING_JPA_PROPERTIES_HIBERNATE_DEFAULT_SCHEMA=public RUN mkdir workspace WORKDIR /workspace/ COPY target/salesmanager-*-SNAPSHOT.jar . diff --git a/build_and_run_app_in_container.sh b/build_and_run_app_in_container.sh index 95eeacd..ea59432 100755 --- a/build_and_run_app_in_container.sh +++ b/build_and_run_app_in_container.sh @@ -1,23 +1,37 @@ -# !/bin/bash +#!/bin/bash + +# This script will build the app and run it in a docker container + +# Function to perform housekeeping tasks +cleanup() { + echo "Performing cleanup..." + # Stop and remove the app container if it exists + docker stop app-test + docker rm app-test + # Stop and remove the postgresql container if it exists + docker stop postgres_container + docker rm postgres_container + # Remove image if exists + docker rmi myapp-img:v1 + # Remove volume if exists + docker volume rm postgresql-data +} + +# Trap the EXIT signal to perform cleanup +trap cleanup EXIT + # Getting local ip address from ifconfig LOCAL_IP=$(ifconfig | grep 'inet ' | grep -Fv 127.0.0.1 | awk '{print $2}') -# To run a local containerized postgresql db, run the following command: -# docker run -d -p 5432:5432 --name postgres_container -e POSTGRES_PASSWORD=Password123 -v postgresql-data:/var/lib/postgresql/data postgres -# Stop and remove the app container if it exists -docker stop app-test -docker rm app-test -# Remove image if exists -docker rmi myapp-img:v1 +# Run a postgresql container with a volume to persist data +docker run -d -p 5432:5432 --name postgres_container -e POSTGRES_PASSWORD=Password123 -v postgresql-data:/var/lib/postgresql/data postgres # Run any previous migrations and tag the latest version with Liquibase -mvn liquibase:rollback -Dliquibase.rollbackCount=999 -Dliquibase.url=jdbc:postgresql://${LOCAL_IP}:5432/postgres -Dliquibase.username=postgres -Dliquibase.password=Password123 -Dliquibase.changeLogFile=db/changelog/changelog_version-3.3.xml -# jdbc:postgresql://hostname:5432/MY_TEST_DATABASE mvn liquibase:tag -Dliquibase.tag=v3.2 -Dliquibase.url=jdbc:postgresql://${LOCAL_IP}:5432/postgres -Dliquibase.username=postgres -Dliquibase.password=Password123 -Dliquibase.changeLogFile=db/changelog/changelog_version-3.3.xml # Build the app and create a docker image from local dockerfile mvn clean package docker build -t myapp-img:v1 . -docker run -e USER_NAME=postgres -e PASSWORD=Password123 -e CHANGELOG_VERSION=changelog_version-3.3.xml -e DB_URL=jdbc:postgresql://${LOCAL_IP}:5432/postgres -t -d -p 80:8086 --name app-test myapp-img:v1 +docker run -d -e USER_NAME=postgres -e PASSWORD=Password123 -e CHANGELOG_VERSION=changelog_version-3.3.xml -e DB_URL=jdbc:postgresql://${LOCAL_IP}:5432/postgres -t -p 80:8086 --name app-test myapp-img:v1 docker ps -a echo "Waiting for 10 seconds for the app to start" sleep 10 # Print the logs of the app from the container -docker logs app-test \ No newline at end of file +docker logs -f app-test diff --git a/pom.xml b/pom.xml index f459443..681a22f 100644 --- a/pom.xml +++ b/pom.xml @@ -78,14 +78,59 @@ runtime - + + org.hibernate.javax.persistence + hibernate-jpa-2.1-api + 1.0.2.Final + + + + org.springframework.boot + spring-boot-starter-data-jpa + - + + org.springframework.security + spring-security-config + 5.1.6.RELEASE + + + + org.springframework.security + spring-security-core + 5.1.6.RELEASE + + + + org.springframework.security + spring-security-config + 5.1.6.RELEASE + + + + org.springframework.boot + spring-boot-starter-security + + + + org.springframework.security + spring-security-core + 5.1.6.RELEASE + + + + org.thymeleaf.extras + thymeleaf-extras-springsecurity5 + + + + + org.scala-lang + scala-library + 2.13.0 + + + diff --git a/src/main/java/net/codejava/AppController.java b/src/main/java/net/codejava/AppController.java index b09cba3..1862d07 100755 --- a/src/main/java/net/codejava/AppController.java +++ b/src/main/java/net/codejava/AppController.java @@ -2,16 +2,25 @@ import java.util.List; +import javax.servlet.http.HttpServletRequest; + import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; +import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; -import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.ModelAttribute; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.servlet.ModelAndView; +import java.security.Principal; + +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.authentication.BadCredentialsException; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; + @Controller public class AppController { @@ -21,6 +30,9 @@ public class AppController { */ @Autowired private SalesDAO dao; + + @Autowired + private AuthenticationManager authenticationManager; @Value("${enableSearchFeature}") private boolean enableSearchFeature; @@ -30,11 +42,14 @@ public boolean getEnableSearchFeature() { } @RequestMapping("/") - public String viewHomePage(Model model) { - List listSale = dao.list(); - model.addAttribute("enableSearchFeature", enableSearchFeature); - model.addAttribute("listSale", listSale); - return "index"; + public String viewHomePage(Model model , Principal principal) { + // if (principal != null) { + // User is logged in, add the data to the model + List listSale = dao.list(); + model.addAttribute("enableSearchFeature", enableSearchFeature); + model.addAttribute("listSale", listSale); + // } + return "index"; } @RequestMapping("/new") @@ -52,6 +67,30 @@ public String save(@ModelAttribute("sale") Sale sale) { return "redirect:/"; } + @RequestMapping(value = "/login", method = RequestMethod.GET) + public String loginGet(Model model) { + return "login"; + } + + @RequestMapping(value = "/login", method = RequestMethod.POST) + public String loginPost(HttpServletRequest request, Model model) { + String username = request.getParameter("username"); + String password = request.getParameter("password"); + + // Authenticate the user + Authentication auth = new UsernamePasswordAuthenticationToken(username, password); + try { + auth = authenticationManager.authenticate(auth); + SecurityContextHolder.getContext().setAuthentication(auth); + } catch (BadCredentialsException e) { + model.addAttribute("error", "Invalid username or password."); + return "login"; + } + + // User is authenticated, redirect to landing page + return "redirect:/"; + } + @RequestMapping("/edit/{id}") public ModelAndView showEditForm(@PathVariable(name = "id") int id) { ModelAndView mav = new ModelAndView("edit_form"); diff --git a/src/main/java/net/codejava/EnableWebSecurity.java b/src/main/java/net/codejava/EnableWebSecurity.java new file mode 100644 index 0000000..b09b2b8 --- /dev/null +++ b/src/main/java/net/codejava/EnableWebSecurity.java @@ -0,0 +1,5 @@ +package net.codejava; + +public @interface EnableWebSecurity { + +} diff --git a/src/main/java/net/codejava/PasswordEncoderConfig.java b/src/main/java/net/codejava/PasswordEncoderConfig.java new file mode 100644 index 0000000..24b7bbd --- /dev/null +++ b/src/main/java/net/codejava/PasswordEncoderConfig.java @@ -0,0 +1,15 @@ +package net.codejava; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.security.crypto.password.PasswordEncoder; + + +@Configuration +public class PasswordEncoderConfig { + + @Bean + public PasswordEncoder passwordEncoder() { + return new BCryptPasswordEncoder(); + } +} \ No newline at end of file diff --git a/src/main/java/net/codejava/SecurityConfig.java b/src/main/java/net/codejava/SecurityConfig.java new file mode 100644 index 0000000..5f6d46a --- /dev/null +++ b/src/main/java/net/codejava/SecurityConfig.java @@ -0,0 +1,57 @@ +package net.codejava; +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.annotation.web.configuration.WebSecurityConfigurerAdapter; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.crypto.password.PasswordEncoder; + + +@Configuration +@EnableWebSecurity +public class SecurityConfig extends WebSecurityConfigurerAdapter { + + @Autowired + private UserDetailsService userDetailsService; + + @Autowired + private PasswordEncoder passwordEncoder; + + @Bean + public AuthenticationManager authenticationManagerBean() throws Exception { + return super.authenticationManagerBean(); + } + + @Override + protected void configure(HttpSecurity http) throws Exception { + http + .authorizeRequests() + .antMatchers("/login").permitAll() + // .anyRequest().permitAll() + .anyRequest().authenticated() + .and() + .formLogin() + // .loginPage("/login") + // .loginProcessingUrl("/login") // This should match the form action in your login.html file + .usernameParameter("username") + .passwordParameter("password") + .defaultSuccessUrl("/", true) // This is the URL to redirect to after a successful login + .successForwardUrl("/") + .failureUrl("/login?error=true") + .permitAll() + .and() + .logout() + .logoutUrl("/logout") // This is the URL to send the user to once they have logged out + .invalidateHttpSession(true) + .permitAll(); + } + + @Autowired + public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception { + auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder); + } +} \ No newline at end of file diff --git a/src/main/java/net/codejava/User.java b/src/main/java/net/codejava/User.java new file mode 100644 index 0000000..6b5e31b --- /dev/null +++ b/src/main/java/net/codejava/User.java @@ -0,0 +1,49 @@ +package net.codejava; + +import javax.persistence.Entity; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.Table; + +@Entity +@Table(name = "\"user\"") +public class User { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + private String username; + private String password; + + // getters and setters methods + // getter for id + public Long getId() { + return id; + } + + // setter for id + public void setId(Long id) { + this.id = id; + } + + // getter for username + public String getUsername() { + return username; + } + + // setter for username + public void setUsername(String username) { + this.username = username; + } + + // getter for password + public String getPassword() { + return password; + } + + // setter for password{ + public void setPassword(String password){ + this.password = password; + } +} \ No newline at end of file diff --git a/src/main/java/net/codejava/UserDetailsServiceImpl.java b/src/main/java/net/codejava/UserDetailsServiceImpl.java new file mode 100644 index 0000000..4766bea --- /dev/null +++ b/src/main/java/net/codejava/UserDetailsServiceImpl.java @@ -0,0 +1,43 @@ +package net.codejava; + +import org.hibernate.exception.SQLGrammarException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.core.userdetails.UsernameNotFoundException; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.stereotype.Service; +import org.springframework.beans.factory.annotation.Autowired; +import java.util.ArrayList; + +@Service +public class UserDetailsServiceImpl implements UserDetailsService { + + private static final Logger logger = LoggerFactory.getLogger(UserDetailsServiceImpl.class); + + @Autowired + private UserRepository userRepository; + + @Autowired + private PasswordEncoder passwordEncoder; + + public void createUser(User user) { + user.setPassword(passwordEncoder.encode(user.getPassword())); + userRepository.save(user); + } + + @Override + public UserDetails loadUserByUsername(String username) { + try { + User user = userRepository.findByUsername(username); + if (user == null) { + throw new UsernameNotFoundException("User not found with username: " + username); + } + return new org.springframework.security.core.userdetails.User(user.getUsername(), user.getPassword(), new ArrayList<>()); + } catch (SQLGrammarException e) { + logger.error("SQL error when trying to find user by username: " + username, e); + throw new RuntimeException("An error occurred while trying to find user by username: " + username); + } + } +} \ No newline at end of file diff --git a/src/main/java/net/codejava/UserRepository.java b/src/main/java/net/codejava/UserRepository.java new file mode 100644 index 0000000..351d863 --- /dev/null +++ b/src/main/java/net/codejava/UserRepository.java @@ -0,0 +1,6 @@ +package net.codejava; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface UserRepository extends JpaRepository { + User findByUsername(String username); +} \ No newline at end of file diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index a0f5a3f..e212cc8 100755 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -4,4 +4,12 @@ spring.datasource.username=postgres spring.datasource.password=postgres spring.liquibase.change-log=classpath:db/changelog/changelog_version-3.3.xml server.port=8086 +spring.jpa.show-sql=true +spring.jpa.properties.hibernate.format_sql=true +spring.main.allow-bean-definition-overriding=true +spring.jpa.hibernate.ddl-auto=none +spring.jpa.properties.hibernate.jdbc.lob.non_contextual_creation=true +spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.PostgreSQLDialect +spring.jpa.properties.hibernate.temp.use_jdbc_metadata_defaults=false +# spring.jpa.properties.hibernate.default_schema=public enableSearchFeature=true diff --git a/src/main/resources/db/changelog/changelog_version-3.3.xml b/src/main/resources/db/changelog/changelog_version-3.3.xml index 79af3aa..9ec81c9 100755 --- a/src/main/resources/db/changelog/changelog_version-3.3.xml +++ b/src/main/resources/db/changelog/changelog_version-3.3.xml @@ -12,11 +12,11 @@ - + - + TRUNCATE TABLE sales + + + + + + + + + + + + + + + + + + + + + + + username = 'tsvi123' + + + + \ No newline at end of file diff --git a/src/main/resources/templates/index.html b/src/main/resources/templates/index.html index 7ddf47a..e81c5c2 100755 --- a/src/main/resources/templates/index.html +++ b/src/main/resources/templates/index.html @@ -71,7 +71,7 @@ -
+

Sales Records

Enter New Sale

@@ -83,6 +83,10 @@

Sales Records


+
+ + +
diff --git a/src/main/resources/templates/login.html b/src/main/resources/templates/login.html new file mode 100644 index 0000000..d7ec7ad --- /dev/null +++ b/src/main/resources/templates/login.html @@ -0,0 +1,35 @@ + + + + Sales Manager - Login + + + +
+

Welcome, Sales Manager!

+

You are using the Sales Manager App, a dedicated platform for storing and managing sales records.

+

Please enter your credentials to log in and access your sales records.

+
+
+
+
+ + + + + + +
+ + \ No newline at end of file