diff --git a/pom.xml b/pom.xml index 98d0a43..aa0ac50 100644 --- a/pom.xml +++ b/pom.xml @@ -60,6 +60,16 @@ spring-dotenv 3.0.0 + + org.projectlombok + lombok + true + + + org.springframework.boot + spring-boot-starter-security + 3.4.5 + diff --git a/src/main/java/gtp/filesmanager/config/SecurityConfig.java b/src/main/java/gtp/filesmanager/config/SecurityConfig.java new file mode 100644 index 0000000..eabc258 --- /dev/null +++ b/src/main/java/gtp/filesmanager/config/SecurityConfig.java @@ -0,0 +1,53 @@ +package gtp.filesmanager.config; + +import gtp.filesmanager.service.UsersDetailService; +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.AuthenticationProvider; +import org.springframework.security.authentication.dao.DaoAuthenticationProvider; +import org.springframework.security.config.Customizer; +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.web.SecurityFilterChain; + +@Configuration +@EnableWebSecurity +public class SecurityConfig { + + @Autowired + private UsersDetailService usersDetailService; + + @Bean + public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { + http + .csrf(config -> config.disable()) + .authorizeHttpRequests(request -> request + .requestMatchers("/register","/login") //allow login and register url + .permitAll() + .anyRequest() + .authenticated()// + ) + .httpBasic(Customizer.withDefaults()) + .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)); + return http.build(); + } + + @Bean + public AuthenticationProvider authenticationProvider(){ + DaoAuthenticationProvider authProvider = new DaoAuthenticationProvider(); + authProvider.setPasswordEncoder(new BCryptPasswordEncoder(12)); + authProvider.setUserDetailsService(usersDetailService); + + return authProvider; + } + + @Bean + public AuthenticationManager authenticationManager(AuthenticationConfiguration configuration) throws Exception { + return configuration.getAuthenticationManager(); + } +} diff --git a/src/main/java/gtp/filesmanager/controller/UserController.java b/src/main/java/gtp/filesmanager/controller/UserController.java index e69de29..84806e7 100644 --- a/src/main/java/gtp/filesmanager/controller/UserController.java +++ b/src/main/java/gtp/filesmanager/controller/UserController.java @@ -0,0 +1,47 @@ +package gtp.filesmanager.controller; + +import gtp.filesmanager.dto.request.LoginRequest; +import gtp.filesmanager.dto.request.RegisterRequest; +import gtp.filesmanager.model.Users; +import gtp.filesmanager.service.UserServiceImpl; +import org.springframework.web.bind.annotation.*; + +import java.util.logging.Logger; + +@RestController +//@RequestMapping("/api/auth") +public class UserController { + private final Logger logger = Logger.getLogger(UserController.class.getName()); + private final UserServiceImpl userServiceImpl; + + //inject UserServiceImpl via constructor + public UserController(UserServiceImpl userServiceImpl) { + this.userServiceImpl = userServiceImpl; + } + + @PostMapping(path = "/register") + protected Object registerUser(@RequestBody RegisterRequest registerRequest){ + logger.info("Registering user with name: " + registerRequest.getUserName()); + if(registerRequest.getUserName() == null || registerRequest.getUserEmail() == null || registerRequest.getPassword() == null){ + throw new RuntimeException("Invalid request"); + } + try{ + return userServiceImpl.registerUser(registerRequest); + }catch (RuntimeException e){ + return e.getMessage(); + } + } + + @PostMapping(path = "/login") + protected Object loginUser(@RequestBody LoginRequest loginRequest){ + logger.info("Logging in user with name: " + loginRequest.getUserName()); + if(loginRequest.getUserName() == null || loginRequest.getPassword() == null){ + throw new RuntimeException("Invalid request"); + } + try{ + return userServiceImpl.loginUser(loginRequest); + }catch (RuntimeException e){ + return e.getMessage(); + } + } +} \ No newline at end of file diff --git a/src/main/java/gtp/filesmanager/dto/request/LoginRequest.java b/src/main/java/gtp/filesmanager/dto/request/LoginRequest.java index e69de29..2c6defb 100644 --- a/src/main/java/gtp/filesmanager/dto/request/LoginRequest.java +++ b/src/main/java/gtp/filesmanager/dto/request/LoginRequest.java @@ -0,0 +1,11 @@ +package gtp.filesmanager.dto.request; + +import lombok.Getter; +import org.antlr.v4.runtime.misc.NotNull; + +//DTO to accept when logging in +@Getter +public class LoginRequest { + private String userName; + private String password; +} \ No newline at end of file diff --git a/src/main/java/gtp/filesmanager/dto/request/RegisterRequest.java b/src/main/java/gtp/filesmanager/dto/request/RegisterRequest.java index e69de29..4123582 100644 --- a/src/main/java/gtp/filesmanager/dto/request/RegisterRequest.java +++ b/src/main/java/gtp/filesmanager/dto/request/RegisterRequest.java @@ -0,0 +1,11 @@ +package gtp.filesmanager.dto.request; + +import lombok.Getter; + +@Getter +public class RegisterRequest { + + private String userName; + private String password; + private String userEmail; +} \ No newline at end of file diff --git a/src/main/java/gtp/filesmanager/dto/response/UserResponse.java b/src/main/java/gtp/filesmanager/dto/response/UserResponse.java index e69de29..09c11b8 100644 --- a/src/main/java/gtp/filesmanager/dto/response/UserResponse.java +++ b/src/main/java/gtp/filesmanager/dto/response/UserResponse.java @@ -0,0 +1,11 @@ +package gtp.filesmanager.dto.response; + +import lombok.Getter; + +@Getter +public class UserResponse { + private Integer Id; + private String userName; + private String userEmail; + private Boolean isActive; +} \ No newline at end of file diff --git a/src/main/java/gtp/filesmanager/model/User.java b/src/main/java/gtp/filesmanager/model/User.java deleted file mode 100644 index e69de29..0000000 diff --git a/src/main/java/gtp/filesmanager/model/UserPrincipal.java b/src/main/java/gtp/filesmanager/model/UserPrincipal.java new file mode 100644 index 0000000..9c041cb --- /dev/null +++ b/src/main/java/gtp/filesmanager/model/UserPrincipal.java @@ -0,0 +1,53 @@ +package gtp.filesmanager.model; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.userdetails.UserDetails; + +import java.util.Collection; +import java.util.List; + +public class UserPrincipal implements UserDetails { + + private final Users userEntity; + + public UserPrincipal(Users userEntity){ + this.userEntity = userEntity; + } + + + @Override + public Collection getAuthorities() { + return List.of(); + } + + @Override + public String getPassword() { + return userEntity.getPassword(); + } + + @Override + public String getUsername() { + return userEntity.getUserName(); + } + + @Override + public boolean isAccountNonExpired() { + return UserDetails.super.isAccountNonExpired(); + } + + @Override + public boolean isAccountNonLocked() { + return UserDetails.super.isAccountNonLocked(); + } + + @Override + public boolean isCredentialsNonExpired() { + return UserDetails.super.isCredentialsNonExpired(); + } + + @Override + public boolean isEnabled() { + return UserDetails.super.isEnabled(); + } +} diff --git a/src/main/java/gtp/filesmanager/model/Users.java b/src/main/java/gtp/filesmanager/model/Users.java new file mode 100644 index 0000000..73e19c0 --- /dev/null +++ b/src/main/java/gtp/filesmanager/model/Users.java @@ -0,0 +1,39 @@ +package gtp.filesmanager.model; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import jakarta.persistence.*; +import lombok.Getter; +import lombok.Setter; + +import java.sql.Date; + +//create a user entity +@Getter +@Entity +@Table(name = "users") +public class Users { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Integer id; + + @Setter + @Column(unique = true,nullable = false) + private String userName; //unique for all + + @Setter + @JsonIgnore //ignores this field when serializing the object to JSON + private String password; + + @Setter + @Column(unique = true,nullable = false) + private String userEmail; + + @Setter + private Boolean isActive; + + @Setter + private Date createdAt; + + @Setter + private Date updatedAt; +} \ No newline at end of file diff --git a/src/main/java/gtp/filesmanager/repository/UserRepository.java b/src/main/java/gtp/filesmanager/repository/UserRepository.java index e69de29..ce760ad 100644 --- a/src/main/java/gtp/filesmanager/repository/UserRepository.java +++ b/src/main/java/gtp/filesmanager/repository/UserRepository.java @@ -0,0 +1,9 @@ +package gtp.filesmanager.repository; + +import gtp.filesmanager.model.Users; +import org.springframework.data.repository.CrudRepository; + +public interface UserRepository extends CrudRepository { + public Users findByUserName(String userName); + public Users findByUserEmail(String userEmail); +} \ No newline at end of file diff --git a/src/main/java/gtp/filesmanager/service/UserService.java b/src/main/java/gtp/filesmanager/service/UserService.java index e69de29..4c99bd4 100644 --- a/src/main/java/gtp/filesmanager/service/UserService.java +++ b/src/main/java/gtp/filesmanager/service/UserService.java @@ -0,0 +1,10 @@ +package gtp.filesmanager.service; + +import gtp.filesmanager.dto.request.LoginRequest; +import gtp.filesmanager.dto.request.RegisterRequest; +import gtp.filesmanager.model.Users; + +public interface UserService { + abstract Users registerUser(RegisterRequest registerRequest); + abstract Users loginUser(LoginRequest loginRequest); +} \ No newline at end of file diff --git a/src/main/java/gtp/filesmanager/service/UserServiceImpl.java b/src/main/java/gtp/filesmanager/service/UserServiceImpl.java new file mode 100644 index 0000000..676d5bb --- /dev/null +++ b/src/main/java/gtp/filesmanager/service/UserServiceImpl.java @@ -0,0 +1,67 @@ +package gtp.filesmanager.service; + +import gtp.filesmanager.dto.request.LoginRequest; +import gtp.filesmanager.dto.request.RegisterRequest; +import gtp.filesmanager.model.Users; +import gtp.filesmanager.repository.UserRepository; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.stereotype.Service; + +@Service +public class UserServiceImpl implements UserService { // Removed 'abstract' as this should be a concrete implementation + + @Autowired + private UserRepository userRepository; + + private BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder(12); // Renamed from 'encoder' for clarity + + @Autowired + private AuthenticationManager authManager; + + + @Override + public Users registerUser(RegisterRequest registerRequest) { + // Check if a user already exists + if (userRepository.findByUserName(registerRequest.getUserName()) != null) { + throw new RuntimeException("Username already exists"); + } + + if (userRepository.findByUserEmail(registerRequest.getUserEmail()) != null) { + throw new RuntimeException("Email already registered"); + } + + // Create a new user entity + Users newUser = new Users(); + newUser.setUserName(registerRequest.getUserName()); + newUser.setUserEmail(registerRequest.getUserEmail()); + newUser.setPassword(passwordEncoder.encode(registerRequest.getPassword())); // Encode password + + return userRepository.save(newUser); + } + + @Override + public Users loginUser(LoginRequest loginRequest){ + Users user = userRepository.findByUserName(loginRequest.getUserName()); + if(user == null){ + throw new RuntimeException("Invalid username"); + } + + //Authenticate user + Authentication authentication = authManager + .authenticate(new UsernamePasswordAuthenticationToken( + loginRequest.getUserName(), loginRequest.getPassword() + )); + + //check user authentication is successful + if (authentication.isAuthenticated()){ + return user; + }else{ + throw new RuntimeException("Invalid password"); + } + } + +} \ No newline at end of file diff --git a/src/main/java/gtp/filesmanager/service/UsersDetailService.java b/src/main/java/gtp/filesmanager/service/UsersDetailService.java new file mode 100644 index 0000000..7e7c192 --- /dev/null +++ b/src/main/java/gtp/filesmanager/service/UsersDetailService.java @@ -0,0 +1,24 @@ +package gtp.filesmanager.service; + +import gtp.filesmanager.model.UserPrincipal; +import gtp.filesmanager.model.Users; +import gtp.filesmanager.repository.UserRepository; +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 UsersDetailService implements UserDetailsService { + + @Autowired + private UserRepository userRepository; + + @Override + public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { + Users userEntity = userRepository.findByUserName(username); + + return new UserPrincipal(userEntity); + } +} diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 6d83714..d8aa98a 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -4,6 +4,7 @@ spring.datasource.username=${POSTGRES_USER} spring.datasource.password=${POSTGRES_PASSWORD} spring.datasource.driver-class-name=org.postgresql.Driver +spring.jpa.hibernate.ddl-auto=update spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.PostgreSQLDialect spring.jpa.properties.hibernate.format_sql=true spring.jpa.show-sql=true \ No newline at end of file