Added Login

This commit is contained in:
Samy Avrillon 2025-02-17 03:38:48 +01:00
parent af5248faed
commit 90ccb8a00c
Signed by: Mysaa
GPG Key ID: 0220AC4A3D6A328B
20 changed files with 519 additions and 20 deletions

View File

@ -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') {

View File

@ -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'

View File

@ -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<Role> roles) {
Collection < ? extends GrantedAuthority> mapRoles = roles.stream()
.map(role -> new SimpleGrantedAuthority(role.getName()))
.collect(Collectors.toList());
return mapRoles;
}
}

View File

@ -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());
}
}

View File

@ -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<User> users;
}

View File

@ -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<Role> roles = new ArrayList<>();
}

View File

@ -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, Long> {
Role findByName(String name);
}

View File

@ -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, Long> {
User findByName(String pseudo);
}

View File

@ -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<UserDto> findAllUsers();
}

View File

@ -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<UserDto> findAllUsers() {
List<User> 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);
}
}

View File

@ -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;
}

View File

@ -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";
}
}

View File

@ -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";
}
}

View File

@ -1,2 +0,0 @@
spring.application.name=Misael
spring.session.jdbc.initialize-schema=always

View File

@ -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

View File

@ -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;

View File

@ -0,0 +1,45 @@
<!DOCTYPE html>
<html lang="fr" dir="ltr">
<head>
<div th:replace="~{html-head}"/>
</head>
<body>
<div th:replace="~{header}"/>
<main>
<div th:if="${param.success}">
You have successfully registered our app!
</div>
<form
method="post"
role="form"
th:action="@{/adduser/save}"
th:object="${user}"
>
<label for="name">Name</label>
<input
id="name"
name="name"
placeholder="Enter pseudo"
th:field="*{name}"
type="text"
/>
<p th:errors = "*{name}" th:if="${#fields.hasErrors('name')}"></p>
<label for="password">Password</label>
<input
id="password"
name="password"
placeholder="Enter password"
th:field="*{password}"
type="password"
/>
<p th:errors = "*{password}" class="text-danger"
th:if="${#fields.hasErrors('password')}"></p>
<button class="btn btn-primary" type="submit">Register</button>
</form>
</main>
</body>
</html>

View File

@ -4,7 +4,7 @@
<ul class="menu">
<li><a href="/questions/quizz">Quizz</a></li>
<li><a href="/questions/forms">Forms</a></li>
<li><a href="/questions/login">Login</a></li>
<li><a href="/login">Login</a></li>
</ul>
</div>
</nav>

View File

@ -0,0 +1,13 @@
<!DOCTYPE html>
<html lang="fr" dir="ltr">
<head>
<div th:replace="~{html-head}"/>
</head>
<body>
<div th:replace="~{header}"/>
<main>
Logged in as <div th:text="${username}"></div>
</main>
</body>
</html>

View File

@ -8,14 +8,24 @@
<div th:replace="~{header}"/>
<main>
<form>
<label for="login-pseudo">Pseudo :</label>
<input type="text" id="login-pseudo" name="pseudo"/>
<div th:if="${param.error}">
<div class="alert alert-danger">Invalid Email or Password</div><br/>
</div>
<div th:if="${param.logout}">
<div class="alert alert-success"> You have been logged out.</div><br/>
</div>
<form
method="post"
role="form"
th:action="@{/login}">
<label for="username">Pseudo :</label>
<input type="text" id="username" name="username"/>
<br/>
<label for="login-password">Mot de passe :</label>
<input type="text" id="login-password" name="password"/>
<label for="password">Mot de passe :</label>
<input type="password" id="password" name="password"/>
<br/>
<input id="connect" type="button">Se connecter</input>
<input id="connect" type="submit">Se connecter</input>
</form>
</main>
</body>