Skip to content
Open
Show file tree
Hide file tree
Changes from 44 commits
Commits
Show all changes
68 commits
Select commit Hold shift + click to select a range
dcf0518
build: add security and jwt dependencies
gabrielvf64 Feb 28, 2025
11057cc
feat: configure permited request and authenticaded needed ones
gabrielvf64 Feb 28, 2025
9f37267
feat: add bcrypt password encoder and authentication manager
gabrielvf64 Feb 28, 2025
96d7923
build: add lombok
gabrielvf64 Feb 28, 2025
bdfd229
feat: generate token secret key and expire date
gabrielvf64 Feb 28, 2025
49dca41
feat: generate token
gabrielvf64 Feb 28, 2025
6816447
feat: create format and validate token
gabrielvf64 Mar 1, 2025
0972a78
refactor: use non deprecated methods
gabrielvf64 Mar 1, 2025
942a877
refactor: use method reference in lambda place
gabrielvf64 Mar 1, 2025
ea3e65d
refactor: add new LibraryUser attributes
gabrielvf64 Mar 1, 2025
372d235
refactor: adjust user h2 list
gabrielvf64 Mar 1, 2025
31af7d0
feat: JwtUserDetails class
gabrielvf64 Mar 1, 2025
1146aa3
refactor: permit all access to h2 console
gabrielvf64 Mar 1, 2025
1db966c
refactor: tiny refactor
gabrielvf64 Mar 1, 2025
bd03995
feat: find user by username
gabrielvf64 Mar 1, 2025
be6ea68
feat: get token from username and role
gabrielvf64 Mar 1, 2025
06eaea9
feat: add authentication endpoint
gabrielvf64 Mar 1, 2025
62f68b8
feat: filter requests and add authenticated ones to spring security c…
gabrielvf64 Mar 1, 2025
a66a25b
refactor: add empty constructor to use on security config
gabrielvf64 Mar 1, 2025
213deee
feat: add bean and config filter order properly
gabrielvf64 Mar 1, 2025
43d29a9
feat: encrypts user's password
gabrielvf64 Mar 1, 2025
bba9d8c
feat: update password
gabrielvf64 Mar 1, 2025
8261b3f
feat: free auth path
gabrielvf64 Mar 1, 2025
ecce5df
refactor: ajust string
gabrielvf64 Mar 1, 2025
d190ba6
refactor: improve create user to encrypt password
gabrielvf64 Mar 1, 2025
df827eb
refactor: easy password change check
gabrielvf64 Mar 1, 2025
dff054d
refactor: use raw query to get the user role
gabrielvf64 Mar 1, 2025
e594bf6
refactor: increase token expire time
gabrielvf64 Mar 1, 2025
11476b0
refactor: use @Autowired to be injected with dependencies on the secu…
gabrielvf64 Mar 1, 2025
3fcb023
refactor: use encrypted passwords on data.sql
gabrielvf64 Mar 1, 2025
1e20eb7
refactor: place classes on right packages
gabrielvf64 Mar 1, 2025
618fce4
feat: only ADMIN can findUserById
gabrielvf64 Mar 1, 2025
d281a0f
feat: add customer basic estructure
gabrielvf64 Mar 2, 2025
d0a79a7
refactor: change method name
gabrielvf64 Mar 2, 2025
a61ea3d
feat: customer basic structure
gabrielvf64 Mar 2, 2025
acf6206
refactor: remove customer fields from library user
gabrielvf64 Mar 2, 2025
0c24e01
refactor: remove customer fields from customer request
gabrielvf64 Mar 2, 2025
0072afc
refactor: adjust database startup data
gabrielvf64 Mar 2, 2025
a8bc60c
feat: define permissions on user controller
gabrielvf64 Mar 2, 2025
a269235
fix: nullpointer ex
gabrielvf64 Mar 2, 2025
ceaf3cd
feat: define book controller permissions
gabrielvf64 Mar 2, 2025
a02b580
feat: define loan controller permissions
gabrielvf64 Mar 2, 2025
faf4c07
feat: define customer controller permissions
gabrielvf64 Mar 2, 2025
c641ba7
fix: default constructor
gabrielvf64 Mar 2, 2025
1b53795
Merge branch 'main' into auth
gabrielvf64 Mar 6, 2025
88ea767
Merge branch 'main' into access-control
gabrielvf64 Mar 6, 2025
926453d
refactor: post merge ajusts
gabrielvf64 Mar 6, 2025
f9ca13b
Merge branch 'main' into auth
gabrielvf64 Mar 6, 2025
69097c0
refactor: pos merge ajusts
gabrielvf64 Mar 6, 2025
067d966
Merge branch 'main' into access-control
gabrielvf64 Mar 6, 2025
e3b1ce7
refactor: pos merge ajusts
gabrielvf64 Mar 6, 2025
81b4fc9
Merge branch 'main' into auth
gabrielvf64 Mar 8, 2025
9b5af32
Merge branch 'main' into access-control
gabrielvf64 Mar 8, 2025
894191b
Merge branch 'auth' into access-control
gabrielvf64 Mar 13, 2025
28ef918
fix: merge adjustments
gabrielvf64 Mar 13, 2025
a71f9d5
Merge branch 'main' into auth
gabrielvf64 Mar 13, 2025
b850ed3
fix: merge adjustments
gabrielvf64 Mar 13, 2025
cc2df59
fix: merge adjustments
gabrielvf64 Mar 13, 2025
8055a9a
fix findLoanByCustomerId
gabrielvf64 Mar 14, 2025
1756328
feat: access control to author controller
gabrielvf64 Mar 14, 2025
cdd5d79
refactor data
gabrielvf64 Mar 14, 2025
0bb4234
Merge branch 'main' into access-control
gabrielvf64 Mar 14, 2025
2b9bbb0
fix: query
gabrielvf64 Mar 14, 2025
fee2bf4
Merge branch 'main' into access-control
gabrielvf64 Mar 14, 2025
d814468
post-merge adjustments
gabrielvf64 Mar 14, 2025
4383356
Merge branch 'main' into auth
gabrielvf64 Mar 14, 2025
a67aade
Merge branch 'auth' into access-control
gabrielvf64 Mar 16, 2025
6f0c43b
post-merge adjustments
gabrielvf64 Mar 16, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 30 additions & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,29 @@
<scope>test</scope>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>

<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>0.12.6</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>0.12.6</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>
<version>0.12.6</version>
<scope>runtime</scope>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
Expand All @@ -58,6 +81,13 @@
<version>2.8.4</version>
</dependency>

<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.36</version>
<scope>provided</scope>
</dependency>


</dependencies>

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
package com.box.library.authentication;

import com.box.library.jwt.JwtUserDetailsService;
import com.box.library.request.UserLoginRequest;
import jakarta.validation.Valid;
import lombok.extern.slf4j.Slf4j;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.ResponseEntity;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.AuthenticationException;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@Slf4j
@RestController
@RequestMapping("/api/v1/auth")
public class AuthenticationController {


private final JwtUserDetailsService jwtUserDetailsService;
private final AuthenticationManager authenticationManager;

private static final Logger log = LoggerFactory.getLogger(AuthenticationController.class);

public AuthenticationController(JwtUserDetailsService jwtUserDetailsService,
AuthenticationManager authenticationManager) {
this.jwtUserDetailsService = jwtUserDetailsService;
this.authenticationManager = authenticationManager;
}

@PostMapping
public ResponseEntity<?> authenticate(@RequestBody @Valid UserLoginRequest loginRequest) {
log.info("Autenticando com {}", loginRequest.username());

try {
var authenticationToken = new UsernamePasswordAuthenticationToken(
loginRequest.username(), loginRequest.password());

authenticationManager.authenticate(authenticationToken);

var jwtToken = jwtUserDetailsService.getJwtToken(loginRequest.username());

return ResponseEntity.ok(jwtToken);
} catch (AuthenticationException e) {
log.error("Erro ao autenticar", e);
}
return ResponseEntity.badRequest().body(new RuntimeException("Credenciais inválidas"));
}
}
13 changes: 7 additions & 6 deletions src/main/java/com/box/library/book/BookController.java
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import io.swagger.v3.oas.annotations.tags.Tag;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.*;

import java.util.List;
Expand All @@ -13,49 +14,49 @@
@Tag(name = "Livros", description = "Endpoints para gerenciamento de livros")
public class BookController {

// TODO[5]: CRUD de livros

private final BookService service;

public BookController(BookService service) {
this.service = service;
}

// TODO[1]: Criação de livro
@PostMapping
@PreAuthorize("hasAuthority('ADMIN')")
public ResponseEntity<Book> create(@RequestBody Book book) {

// TODO[9]: Usando var
var savedBook = service.create(book);
return ResponseEntity.ok(savedBook);
}

@GetMapping
@PreAuthorize("hasAnyAuthority('ADMIN', 'CLIENT')")
public ResponseEntity<List<Book>> findAll() {
var books = service.findAll();
return books.isEmpty() ? ResponseEntity.noContent().build() : ResponseEntity.ok(books);
}

@GetMapping("/{id}")
@PreAuthorize("hasAnyAuthority('ADMIN', 'CLIENT')")
public ResponseEntity<Book> findById(@PathVariable Long id) {
var book = service.findById(id);
return ResponseEntity.ok(book);
}

@DeleteMapping("/{id}")
@PreAuthorize("hasAuthority('ADMIN')")
public ResponseEntity<Void> deleteById(@PathVariable Long id) {
service.deleteById(id);
return ResponseEntity.noContent().build();
}

@PutMapping("/{id}")
@PreAuthorize("hasAuthority('ADMIN')")
public ResponseEntity<Book> update(@PathVariable Long id, @RequestBody UpdateBook request) {
var updatedEntity = service.update(id, request);
return new ResponseEntity<>(updatedEntity, HttpStatus.OK);
}

// TODO[4]: Busca de livros por filtros
@GetMapping("/filter")
@PreAuthorize("hasAnyAuthority('ADMIN', 'CLIENT')")
public ResponseEntity<List<Book>> findAllByFilter(@RequestParam(required = false) String author, @RequestParam(required = false) String title,
@RequestParam(required = false) String isbn, @RequestParam(required = false) String publisher) {
var books = service.findAllByFilter(author, title, isbn, publisher);
Expand Down
62 changes: 62 additions & 0 deletions src/main/java/com/box/library/config/SecurityConfig.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
package com.box.library.config;

import com.box.library.jwt.JwtAuthorizationFilter;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
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.configurers.AbstractHttpConfigurer;
import org.springframework.security.config.annotation.web.configurers.HeadersConfigurer;
import org.springframework.security.config.http.SessionCreationPolicy;
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.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;

@Configuration
@EnableWebMvc
@EnableMethodSecurity
public class SecurityConfig {

public static final String API_PATH = "/api/v1";
public static final String USERS_PATH = "/users";
public static final String AUTHENTICATION_PATH = "/auth";
public static final String H2_CONSOLE_PATH = "/h2-console/**";

@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
return http
.csrf(AbstractHttpConfigurer::disable)
.headers(headers -> headers.frameOptions(HeadersConfigurer.FrameOptionsConfig::disable))
.formLogin(AbstractHttpConfigurer::disable)
.httpBasic(AbstractHttpConfigurer::disable)
.authorizeHttpRequests(authorizeRequests -> authorizeRequests
.requestMatchers(HttpMethod.POST, API_PATH + USERS_PATH).permitAll()
.requestMatchers(HttpMethod.POST, API_PATH + AUTHENTICATION_PATH).permitAll()
.requestMatchers(H2_CONSOLE_PATH).permitAll()
.anyRequest().authenticated())
.sessionManagement(sessionManagement -> sessionManagement
.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.addFilterBefore(jwtAuthorizationFilter(), UsernamePasswordAuthenticationFilter.class)
.build();
}

@Bean
public JwtAuthorizationFilter jwtAuthorizationFilter() {
return new JwtAuthorizationFilter();
}

@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}

@Bean
public AuthenticationManager authenticationManager(AuthenticationConfiguration authenticationConfiguration) throws Exception {
return authenticationConfiguration.getAuthenticationManager();
}
}
65 changes: 65 additions & 0 deletions src/main/java/com/box/library/customer/Customer.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
package com.box.library.customer;

import com.box.library.request.CreateCustomerRequest;
import com.box.library.user.LibraryUser;
import jakarta.persistence.*;

@Entity
@Table(name = "customers")
public class Customer {

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;

private String name;

private String cpf;

@OneToOne
@JoinColumn(name = "user_id", nullable = false)
private LibraryUser user;

// TODO: Por o endereço do cliente


public Customer() {
}

public Customer(CreateCustomerRequest request) {
this.name = request.name();
this.cpf = request.cpf();
}

public Long getId() {
return id;
}

public void setId(Long id) {
this.id = id;
}

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}

public String getCpf() {
return cpf;
}

public void setCpf(String cpf) {
this.cpf = cpf;
}

public LibraryUser getUser() {
return user;
}

public void setUser(LibraryUser user) {
this.user = user;
}
}
42 changes: 42 additions & 0 deletions src/main/java/com/box/library/customer/CustomerController.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package com.box.library.customer;

import com.box.library.jwt.JwtUserDetails;
import com.box.library.request.CreateCustomerRequest;
import jakarta.validation.Valid;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.web.bind.annotation.*;

@RestController
@RequestMapping("api/v1/customers")
public class CustomerController {

private final CustomerService service;

public CustomerController(CustomerService service) {
this.service = service;
}

@PostMapping
@PreAuthorize("hasAuthority('ADMIN') OR (hasAuthority('CLIENT') AND #id == authentication.principal.id)")
public ResponseEntity<Customer> create(@Valid @RequestBody CreateCustomerRequest request,
@AuthenticationPrincipal JwtUserDetails jwtUserDetails) {
var entity = service.save(request, jwtUserDetails);
return ResponseEntity.status(HttpStatus.CREATED).body(entity);
}

@GetMapping("/{id}")
@PreAuthorize("hasAuthority('ADMIN') OR (hasAuthority('CLIENT') AND #id == authentication.principal.id)")
public ResponseEntity<Customer> findById(@PathVariable Long id) {
return ResponseEntity.ok(service.findById(id));
}

@GetMapping("/details")
@PreAuthorize("hasAuthority('ADMIN') OR (hasAuthority('CLIENT') AND #id == authentication.principal.id)")
public ResponseEntity<Customer> getDetails(@AuthenticationPrincipal JwtUserDetails jwtUserDetails) {
var entity = service.findUserById(jwtUserDetails.getId());
return ResponseEntity.ok(entity);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package com.box.library.customer;

import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

@Repository
public interface CustomerRepository extends JpaRepository<Customer, Long> {
Customer findByUserId(Long id);
}
39 changes: 39 additions & 0 deletions src/main/java/com/box/library/customer/CustomerService.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package com.box.library.customer;

import com.box.library.jwt.JwtUserDetails;
import com.box.library.request.CreateCustomerRequest;
import com.box.library.user.LibraryUserService;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
public class CustomerService {

private final CustomerRepository repository;
private final LibraryUserService libraryUserService;

public CustomerService(CustomerRepository repository, LibraryUserService libraryUserService) {
this.repository = repository;
this.libraryUserService = libraryUserService;
}

@Transactional
public Customer save(CreateCustomerRequest request, JwtUserDetails jwtUserDetails) {
Customer entity = new Customer(request);

entity.setUser(libraryUserService.findById(jwtUserDetails.getId()));

return repository.save(entity);
}

@Transactional(readOnly = true)
public Customer findById(Long id) {
return repository.findById(id).orElseThrow(
() -> new RuntimeException("Customer id [" + id + "] not found"));
}

@Transactional(readOnly = true)
public Customer findUserById(Long id) {
return repository.findByUserId(id);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,11 @@ public ResponseEntity<String> handleNoFilterProvidedException(NoFilterProvidedEx
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(ex.getMessage());
}

@ExceptionHandler
public ResponseEntity<String> handleInvalidPasswordException(InvalidPasswordException ex) {
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(ex.getMessage());
}

@ExceptionHandler
public ResponseEntity<String> handleLoanNotFoundException(LoanNotFoundException ex) {
return ResponseEntity.status(HttpStatus.NOT_FOUND).body(ex.getMessage());
Expand Down
Loading