From 90ccb8a00c87fa39f21353d66b7986d5980172c3 Mon Sep 17 00:00:00 2001 From: Samy Avrillon Date: Mon, 17 Feb 2025 03:38:48 +0100 Subject: [PATCH] Added Login --- build.gradle | 33 +++++++- compose.yaml | 8 +- .../misael/CustomUserDetailsService.java | 45 +++++++++++ .../com/bernard/misael/SpringSecurity.java | 54 +++++++++++++ .../java/com/bernard/misael/model/Role.java | 28 +++++++ .../java/com/bernard/misael/model/User.java | 37 +++++++++ .../misael/repository/RoleRepository.java | 10 +++ .../misael/repository/UserRepository.java | 11 +++ .../bernard/misael/service/UserService.java | 14 ++++ .../misael/service/UserServiceImpl.java | 69 ++++++++++++++++ .../bernard/misael/service/dto/UserDto.java | 21 +++++ .../bernard/misael/web/AuthController.java | 81 +++++++++++++++++++ .../misael/web/QuestionsController.java | 10 +-- src/main/resources/application.properties | 2 - src/main/resources/application.yml | 27 +++++++ .../db/migration/V1__init_user_and_roles.sql | 7 ++ src/main/resources/templates/adduser.html | 45 +++++++++++ src/main/resources/templates/header.html | 2 +- src/main/resources/templates/index.html | 13 +++ src/main/resources/templates/login.html | 22 +++-- 20 files changed, 519 insertions(+), 20 deletions(-) create mode 100644 src/main/java/com/bernard/misael/CustomUserDetailsService.java create mode 100644 src/main/java/com/bernard/misael/SpringSecurity.java create mode 100644 src/main/java/com/bernard/misael/model/Role.java create mode 100644 src/main/java/com/bernard/misael/model/User.java create mode 100644 src/main/java/com/bernard/misael/repository/RoleRepository.java create mode 100644 src/main/java/com/bernard/misael/repository/UserRepository.java create mode 100644 src/main/java/com/bernard/misael/service/UserService.java create mode 100644 src/main/java/com/bernard/misael/service/UserServiceImpl.java create mode 100644 src/main/java/com/bernard/misael/service/dto/UserDto.java create mode 100644 src/main/java/com/bernard/misael/web/AuthController.java delete mode 100644 src/main/resources/application.properties create mode 100644 src/main/resources/application.yml create mode 100644 src/main/resources/db/migration/V1__init_user_and_roles.sql create mode 100644 src/main/resources/templates/adduser.html create mode 100644 src/main/resources/templates/index.html diff --git a/build.gradle b/build.gradle index 581dcd7..9efb562 100644 --- a/build.gradle +++ b/build.gradle @@ -1,9 +1,20 @@ +buildscript { + repositories { + mavenCentral() + } + dependencies { + classpath 'org.flywaydb:flyway-database-postgresql:11.3.2' + classpath 'org.postgresql:postgresql:42.7.5' + } +} plugins { id 'java' id 'org.springframework.boot' version '3.4.2' + id 'org.flywaydb.flyway' version "11.3.2" id 'io.spring.dependency-management' version '1.1.7' } + group = 'com.bernard' version = '0.0.1-SNAPSHOT' @@ -21,15 +32,33 @@ dependencies { implementation 'org.springframework.boot:spring-boot-starter-data-jpa' implementation 'org.springframework.boot:spring-boot-starter-jdbc' implementation 'org.springframework.boot:spring-boot-starter-thymeleaf' + implementation 'org.springframework.boot:spring-boot-starter-security' implementation 'org.springframework.boot:spring-boot-starter-web' - implementation 'org.flywaydb:flyway-core' - implementation 'org.flywaydb:flyway-database-postgresql' + implementation 'org.springframework:spring-jdbc' + implementation 'org.flywaydb:flyway-core:11.3.2' + implementation 'org.flywaydb:flyway-database-postgresql:11.3.2' implementation 'org.springframework.session:spring-session-jdbc' + implementation 'jakarta.validation:jakarta.validation-api:3.1.1' developmentOnly 'org.springframework.boot:spring-boot-devtools' developmentOnly 'org.springframework.boot:spring-boot-docker-compose' runtimeOnly 'org.postgresql:postgresql' testImplementation 'org.springframework.boot:spring-boot-starter-test' testRuntimeOnly 'org.junit.platform:junit-platform-launcher' + + //Lombok + compileOnly 'org.projectlombok:lombok:1.18.36' + annotationProcessor 'org.projectlombok:lombok:1.18.36' + + testCompileOnly 'org.projectlombok:lombok:1.18.36' + testAnnotationProcessor 'org.projectlombok:lombok:1.18.36' +} + +flyway { + url = "jdbc:postgresql://127.0.0.1:10051/misael" + user = 'misael' + password = 'misael-dev' + driver = 'org.postgresql.Driver' + schemas = ['misael'] } tasks.named('test') { diff --git a/compose.yaml b/compose.yaml index 7c8044f..b0686b6 100644 --- a/compose.yaml +++ b/compose.yaml @@ -2,8 +2,8 @@ services: postgres: image: 'postgres:latest' environment: - - 'POSTGRES_DB=mydatabase' - - 'POSTGRES_PASSWORD=secret' - - 'POSTGRES_USER=myuser' + - 'POSTGRES_DB=misael' + - 'POSTGRES_PASSWORD=misael-dev' + - 'POSTGRES_USER=misael' ports: - - '5432' + - '10051:5432' diff --git a/src/main/java/com/bernard/misael/CustomUserDetailsService.java b/src/main/java/com/bernard/misael/CustomUserDetailsService.java new file mode 100644 index 0000000..810043c --- /dev/null +++ b/src/main/java/com/bernard/misael/CustomUserDetailsService.java @@ -0,0 +1,45 @@ +package com.bernard.misael; + +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +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; + +import com.bernard.misael.model.Role; +import com.bernard.misael.model.User; +import com.bernard.misael.repository.UserRepository; + +import java.util.Collection; +import java.util.stream.Collectors; + +@Service +public class CustomUserDetailsService implements UserDetailsService { + + private UserRepository userRepository; + + public CustomUserDetailsService(UserRepository userRepository) { + this.userRepository = userRepository; + } + + @Override + public UserDetails loadUserByUsername(String pseudo) throws UsernameNotFoundException { + User user = userRepository.findByName(pseudo); + + if (user != null) { + return new org.springframework.security.core.userdetails.User(user.getName(), + user.getPassword(), + mapRolesToAuthorities(user.getRoles())); + }else{ + throw new UsernameNotFoundException("Invalid username or password."); + } + } + + private Collection < ? extends GrantedAuthority> mapRolesToAuthorities(Collection roles) { + Collection < ? extends GrantedAuthority> mapRoles = roles.stream() + .map(role -> new SimpleGrantedAuthority(role.getName())) + .collect(Collectors.toList()); + return mapRoles; + } +} \ No newline at end of file diff --git a/src/main/java/com/bernard/misael/SpringSecurity.java b/src/main/java/com/bernard/misael/SpringSecurity.java new file mode 100644 index 0000000..2d736b0 --- /dev/null +++ b/src/main/java/com/bernard/misael/SpringSecurity.java @@ -0,0 +1,54 @@ +package com.bernard.misael; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +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.core.userdetails.UserDetailsService; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.security.web.SecurityFilterChain; +import org.springframework.security.web.util.matcher.AntPathRequestMatcher; + +@Configuration +@EnableWebSecurity +public class SpringSecurity { + + @Autowired + private UserDetailsService userDetailsService; + + @Bean + public static PasswordEncoder passwordEncoder(){ + return new BCryptPasswordEncoder(); + } + + @Bean + public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { + http.csrf().disable() + .authorizeHttpRequests((authorize) -> + authorize.requestMatchers("/**").permitAll() + // .requestMatchers("/index").permitAll() + // .requestMatchers("/users").hasRole("ADMIN") + ).formLogin( + form -> form + .loginPage("/login") + .loginProcessingUrl("/login") + .defaultSuccessUrl("/") + .permitAll() + ).logout( + logout -> logout + .logoutRequestMatcher(new AntPathRequestMatcher("/logout")) + .permitAll() + ); + return http.build(); + } + + @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/com/bernard/misael/model/Role.java b/src/main/java/com/bernard/misael/model/Role.java new file mode 100644 index 0000000..bbe7cb7 --- /dev/null +++ b/src/main/java/com/bernard/misael/model/Role.java @@ -0,0 +1,28 @@ +package com.bernard.misael.model; + +import jakarta.persistence.*; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +import java.util.List; + +@Getter +@Setter +@NoArgsConstructor +@AllArgsConstructor +@Entity +@Table(name="roles") +public class Role +{ + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(nullable=false, unique=true) + private String name; + + @ManyToMany(mappedBy="roles") + private List users; +} diff --git a/src/main/java/com/bernard/misael/model/User.java b/src/main/java/com/bernard/misael/model/User.java new file mode 100644 index 0000000..82eb664 --- /dev/null +++ b/src/main/java/com/bernard/misael/model/User.java @@ -0,0 +1,37 @@ +package com.bernard.misael.model; + +import java.util.ArrayList; +import java.util.List; + +import jakarta.persistence.*; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +@Getter +@Setter +@NoArgsConstructor +@AllArgsConstructor +@Entity +@Table(name="users") +public class User +{ + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(nullable=false) + private String name; + + @Column(nullable=false) + private String password; + + @ManyToMany(fetch = FetchType.EAGER, cascade=CascadeType.ALL) + @JoinTable( + name="users_roles", + joinColumns={@JoinColumn(name="USER_ID", referencedColumnName="ID")}, + inverseJoinColumns={@JoinColumn(name="ROLE_ID", referencedColumnName="ID")}) + private List roles = new ArrayList<>(); + +} \ No newline at end of file diff --git a/src/main/java/com/bernard/misael/repository/RoleRepository.java b/src/main/java/com/bernard/misael/repository/RoleRepository.java new file mode 100644 index 0000000..d028fce --- /dev/null +++ b/src/main/java/com/bernard/misael/repository/RoleRepository.java @@ -0,0 +1,10 @@ +package com.bernard.misael.repository; + +import org.springframework.data.jpa.repository.JpaRepository; + +import com.bernard.misael.model.Role; + +public interface RoleRepository extends JpaRepository { + + Role findByName(String name); +} \ No newline at end of file diff --git a/src/main/java/com/bernard/misael/repository/UserRepository.java b/src/main/java/com/bernard/misael/repository/UserRepository.java new file mode 100644 index 0000000..2ffcd24 --- /dev/null +++ b/src/main/java/com/bernard/misael/repository/UserRepository.java @@ -0,0 +1,11 @@ +package com.bernard.misael.repository; + +import org.springframework.data.jpa.repository.JpaRepository; + +import com.bernard.misael.model.User; + +public interface UserRepository extends JpaRepository { + + User findByName(String pseudo); + +} \ No newline at end of file diff --git a/src/main/java/com/bernard/misael/service/UserService.java b/src/main/java/com/bernard/misael/service/UserService.java new file mode 100644 index 0000000..127b2b3 --- /dev/null +++ b/src/main/java/com/bernard/misael/service/UserService.java @@ -0,0 +1,14 @@ +package com.bernard.misael.service; + +import java.util.List; + +import com.bernard.misael.model.User; +import com.bernard.misael.service.dto.UserDto; + +public interface UserService { + void saveUser(UserDto userDto); + + User findUserByName(String name); + + List findAllUsers(); +} \ No newline at end of file diff --git a/src/main/java/com/bernard/misael/service/UserServiceImpl.java b/src/main/java/com/bernard/misael/service/UserServiceImpl.java new file mode 100644 index 0000000..75e49d3 --- /dev/null +++ b/src/main/java/com/bernard/misael/service/UserServiceImpl.java @@ -0,0 +1,69 @@ +package com.bernard.misael.service; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.stereotype.Service; + +import com.bernard.misael.model.Role; +import com.bernard.misael.model.User; +import com.bernard.misael.repository.RoleRepository; +import com.bernard.misael.repository.UserRepository; +import com.bernard.misael.service.dto.UserDto; + +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; + +@Service +public class UserServiceImpl implements UserService { + + private UserRepository userRepository; + private RoleRepository roleRepository; + private PasswordEncoder passwordEncoder; + + public UserServiceImpl(UserRepository userRepository, + RoleRepository roleRepository, + PasswordEncoder passwordEncoder) { + this.userRepository = userRepository; + this.roleRepository = roleRepository; + this.passwordEncoder = passwordEncoder; + } + + @Override + public void saveUser(UserDto userDto) { + User user = new User(); + user.setName(userDto.getName()); + // encrypt the password using spring security + user.setPassword(passwordEncoder.encode(userDto.getPassword())); + + Role role = roleRepository.findByName("ROLE_ADMIN"); + if(role == null){ + role = checkRoleExist(); + } + user.setRoles(Arrays.asList(role)); + userRepository.save(user); + } + + @Override + public User findUserByName(String name) { + return userRepository.findByName(name); + } + + @Override + public List findAllUsers() { + List users = userRepository.findAll(); + return users.stream() + .map((user) -> mapToUserDto(user)) + .collect(Collectors.toList()); + } + + private UserDto mapToUserDto(User user){ + UserDto userDto = new UserDto(); + userDto.setName(user.getName()); + return userDto; + } + + private Role checkRoleExist(){ + Role role = new Role(); + role.setName("ROLE_ADMIN"); + return roleRepository.save(role); + } +} \ No newline at end of file diff --git a/src/main/java/com/bernard/misael/service/dto/UserDto.java b/src/main/java/com/bernard/misael/service/dto/UserDto.java new file mode 100644 index 0000000..899aca9 --- /dev/null +++ b/src/main/java/com/bernard/misael/service/dto/UserDto.java @@ -0,0 +1,21 @@ +package com.bernard.misael.service.dto; + +import jakarta.validation.constraints.Email; +import jakarta.validation.constraints.NotEmpty; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +@Getter +@Setter +@NoArgsConstructor +@AllArgsConstructor +public class UserDto +{ + private Long id; + @NotEmpty(message = "User name should not be empty") + private String name; + @NotEmpty(message = "Password should not be empty") + private String password; +} \ No newline at end of file diff --git a/src/main/java/com/bernard/misael/web/AuthController.java b/src/main/java/com/bernard/misael/web/AuthController.java new file mode 100644 index 0000000..6945037 --- /dev/null +++ b/src/main/java/com/bernard/misael/web/AuthController.java @@ -0,0 +1,81 @@ +package com.bernard.misael.web; + +import org.springframework.security.authentication.AnonymousAuthenticationToken; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.stereotype.Controller; +import org.springframework.ui.Model; +import org.springframework.validation.BindingResult; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.ModelAttribute; +import org.springframework.web.bind.annotation.PostMapping; + +import com.bernard.misael.model.User; +import com.bernard.misael.service.UserService; +import com.bernard.misael.service.dto.UserDto; + +import jakarta.validation.Valid; + +@Controller +public class AuthController { + + private UserService userService; + + public AuthController(UserService userService) { + this.userService = userService; + } + + @GetMapping("/login") + public String loginPage() { + return "login"; + } + + public User getLoggedInUser() { + if(SecurityContextHolder.getContext().getAuthentication().getPrincipal() + instanceof org.springframework.security.core.userdetails.User){ + org.springframework.security.core.userdetails.User user + = (org.springframework.security.core.userdetails.User) + SecurityContextHolder.getContext().getAuthentication().getPrincipal(); + return userService.findUserByName(user.getUsername()); + } else { + return null; + } + } + + @GetMapping("/") + public String index(Model model) { + if(SecurityContextHolder.getContext().getAuthentication().getPrincipal() instanceof org.springframework.security.core.userdetails.User){ + org.springframework.security.core.userdetails.User user + = (org.springframework.security.core.userdetails.User)SecurityContextHolder.getContext().getAuthentication().getPrincipal(); + model.addAttribute("username", user.getUsername()); + }else{ + model.addAttribute("username", "no-one"); + } + return "index"; + } + + @GetMapping("/adduser") + public String showRegistrationForm(Model model){ + // create model object to store form data + UserDto user = new UserDto(); + model.addAttribute("user", user); + return "adduser"; + } + + @PostMapping("/adduser/save") + public String registration(@Valid @ModelAttribute("user") UserDto userDto, + BindingResult result, + Model model){ + User existingUser = userService.findUserByName("mysaa"); + + if(existingUser != null){ + result.reject("User is already registered"); + } + + if(result.hasErrors()){ + model.addAttribute("user", userDto); + return "/adduser"; + } + userService.saveUser(userDto); + return "redirect:/adduser?success"; + } +} diff --git a/src/main/java/com/bernard/misael/web/QuestionsController.java b/src/main/java/com/bernard/misael/web/QuestionsController.java index 96eb641..54e3362 100644 --- a/src/main/java/com/bernard/misael/web/QuestionsController.java +++ b/src/main/java/com/bernard/misael/web/QuestionsController.java @@ -1,7 +1,12 @@ package com.bernard.misael.web; +import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.stereotype.Controller; +import org.springframework.ui.Model; import org.springframework.web.bind.annotation.RequestMapping; + +import com.bernard.misael.service.dto.UserDto; + import org.springframework.web.bind.annotation.GetMapping; @@ -23,11 +28,6 @@ public class QuestionsController { public String getForms() { return "forms.html"; } - @GetMapping("/login") - public String loginPage() { - return "login.html"; - } - } diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties deleted file mode 100644 index 086d092..0000000 --- a/src/main/resources/application.properties +++ /dev/null @@ -1,2 +0,0 @@ -spring.application.name=Misael -spring.session.jdbc.initialize-schema=always \ No newline at end of file diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml new file mode 100644 index 0000000..b364410 --- /dev/null +++ b/src/main/resources/application.yml @@ -0,0 +1,27 @@ +spring: + datasource: + url: jdbc:mysql://127.0.0.1:10051/misael + username: misael + password: misael-dev + hikari: + schema: misael + flyway: + enabled: true + locations: classpath:db/migration/structure, classpath:db/migration/data + validate-on-migrate: true + default-schema: misael + +# jpa: +# properties: +# javax: +# persistence: +# schema-generation: +# create-source: metadata +# scripts: +# action: update +# create-target: db-migration.sql +# database: +# action: none +# hibernate: +# ddl-auto: none +# generate-ddl: true diff --git a/src/main/resources/db/migration/V1__init_user_and_roles.sql b/src/main/resources/db/migration/V1__init_user_and_roles.sql new file mode 100644 index 0000000..02773fc --- /dev/null +++ b/src/main/resources/db/migration/V1__init_user_and_roles.sql @@ -0,0 +1,7 @@ +create table roles (id bigint generated by default as identity, name varchar(255) not null, primary key (id)); +create table users (id bigint generated by default as identity, name varchar(255) not null, password varchar(255) not null, primary key (id)); +create table users_roles (user_id bigint not null, role_id bigint not null); +alter table if exists roles drop constraint if exists UKofx66keruapi6vyqpv6f2or37; +alter table if exists roles add constraint UKofx66keruapi6vyqpv6f2or37 unique (name); +alter table if exists users_roles add constraint FKj6m8fwv7oqv74fcehir1a9ffy foreign key (role_id) references roles; +alter table if exists users_roles add constraint FK2o0jvgh89lemvvo17cbqvdxaa foreign key (user_id) references users; diff --git a/src/main/resources/templates/adduser.html b/src/main/resources/templates/adduser.html new file mode 100644 index 0000000..d44517c --- /dev/null +++ b/src/main/resources/templates/adduser.html @@ -0,0 +1,45 @@ + + + +
+ + + +
+
+ +
+ You have successfully registered our app! +
+
+ + +

+ + + +

+ + +
+
+ + \ No newline at end of file diff --git a/src/main/resources/templates/header.html b/src/main/resources/templates/header.html index 29ce0b6..85bcf13 100644 --- a/src/main/resources/templates/header.html +++ b/src/main/resources/templates/header.html @@ -4,7 +4,7 @@
\ No newline at end of file diff --git a/src/main/resources/templates/index.html b/src/main/resources/templates/index.html new file mode 100644 index 0000000..3ef592c --- /dev/null +++ b/src/main/resources/templates/index.html @@ -0,0 +1,13 @@ + + + +
+ + + +
+
+ Logged in as
+
+ + \ No newline at end of file diff --git a/src/main/resources/templates/login.html b/src/main/resources/templates/login.html index c29e637..2706e8a 100644 --- a/src/main/resources/templates/login.html +++ b/src/main/resources/templates/login.html @@ -8,14 +8,24 @@
-
- - +
+
Invalid Email or Password

+
+
+
You have been logged out.

+
+ + + +
- - + +
- Se connecter + Se connecter