diff options
| author | lucashemi <lucasxberger@gmail.com> | 2023-02-08 14:57:43 -0300 |
|---|---|---|
| committer | lucashemi <lucasxberger@gmail.com> | 2023-02-08 14:57:43 -0300 |
| commit | 514f2e7194a875cfc53d7e1bccd922db2bbb3f3f (patch) | |
| tree | 6b62dada02b9d02e624c40b7f3d4704537cbcb80 /api/src/main/java | |
readme
Diffstat (limited to 'api/src/main/java')
28 files changed, 773 insertions, 0 deletions
diff --git a/api/src/main/java/med/voll/api/ApiApplication.java b/api/src/main/java/med/voll/api/ApiApplication.java new file mode 100644 index 0000000..e08ef48 --- /dev/null +++ b/api/src/main/java/med/voll/api/ApiApplication.java @@ -0,0 +1,13 @@ +package med.voll.api; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class ApiApplication { + + public static void main(String[] args) { + SpringApplication.run(ApiApplication.class, args); + } + +} diff --git a/api/src/main/java/med/voll/api/controller/AuthenticationController.java b/api/src/main/java/med/voll/api/controller/AuthenticationController.java new file mode 100644 index 0000000..b7dcf27 --- /dev/null +++ b/api/src/main/java/med/voll/api/controller/AuthenticationController.java @@ -0,0 +1,37 @@ +package med.voll.api.controller; + +import jakarta.validation.Valid; +import med.voll.api.domain.user.AuthenticationData; +import med.voll.api.domain.user.User; +import med.voll.api.infra.security.TokenJWTData; +import med.voll.api.infra.security.TokenService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.ResponseEntity; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; + +@Controller +@RequestMapping("/login") +public class AuthenticationController { + + @Autowired + private AuthenticationManager authenticationManager; + + @Autowired + private TokenService tokenService; + + @PostMapping + public ResponseEntity performLogin(@RequestBody @Valid AuthenticationData data) { + var authenticationToken = new UsernamePasswordAuthenticationToken(data.login(), data.password()); + Authentication authentication = authenticationManager.authenticate(authenticationToken); + + var tokenJWT = tokenService.generateToken((User) authentication.getPrincipal()); + + return ResponseEntity.ok(new TokenJWTData(tokenJWT)); + } +} diff --git a/api/src/main/java/med/voll/api/controller/DoctorController.java b/api/src/main/java/med/voll/api/controller/DoctorController.java new file mode 100644 index 0000000..a262d34 --- /dev/null +++ b/api/src/main/java/med/voll/api/controller/DoctorController.java @@ -0,0 +1,66 @@ +package med.voll.api.controller; + +import jakarta.validation.Valid; +import med.voll.api.domain.doctor.DoctorListingData; +import med.voll.api.domain.doctor.Doctor; +import med.voll.api.domain.doctor.DoctorRepository; +import med.voll.api.domain.doctor.*; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; +import org.springframework.data.web.PageableDefault; +import org.springframework.http.ResponseEntity; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.util.UriComponentsBuilder; + +@RestController +@RequestMapping("/doctors") +public class DoctorController { + + @Autowired + private DoctorRepository doctorRepository; + + @PostMapping + @Transactional + public ResponseEntity register(@RequestBody @Valid DoctorRegistrationData data, UriComponentsBuilder uriBuilder) { + var doctor = new Doctor(data); + doctorRepository.save(doctor); + + var uri = uriBuilder.path("/doctors/{id}").buildAndExpand(doctor.getId()).toUri(); + + return ResponseEntity.created(uri).body(new DoctorDetailingData(doctor)); + } + + @GetMapping + public ResponseEntity<Page<DoctorListingData>> list(@PageableDefault(sort = {"name"}, direction = Sort.Direction.ASC) Pageable pagination) { + var page = doctorRepository.findAllByActiveTrue(pagination).map(DoctorListingData::new); + return ResponseEntity.ok(page); + } + + @PutMapping + @Transactional + public ResponseEntity update(@RequestBody @Valid DoctorUpdateData data) { + Doctor doctor = doctorRepository.getReferenceById(data.id()); + doctor.updateInformation(data); + + return ResponseEntity.ok(new DoctorDetailingData(doctor)); + } + + @DeleteMapping("/{id}") + @Transactional + public ResponseEntity delete(@PathVariable Long id) { + Doctor doctor = doctorRepository.getReferenceById(id); + doctor.delete(); + + return ResponseEntity.noContent().build(); + } + + @GetMapping("/{id}") + public ResponseEntity detail(@PathVariable Long id) { + Doctor doctor = doctorRepository.getReferenceById(id); + + return ResponseEntity.ok(new DoctorDetailingData(doctor)); + } +} diff --git a/api/src/main/java/med/voll/api/controller/PatientController.java b/api/src/main/java/med/voll/api/controller/PatientController.java new file mode 100644 index 0000000..bd774a1 --- /dev/null +++ b/api/src/main/java/med/voll/api/controller/PatientController.java @@ -0,0 +1,65 @@ +package med.voll.api.controller; + +import jakarta.validation.Valid; +import med.voll.api.domain.patient.PatientListingData; +import med.voll.api.domain.patient.Patient; +import med.voll.api.domain.patient.PatientRepository; +import med.voll.api.domain.patient.*; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; +import org.springframework.data.web.PageableDefault; +import org.springframework.http.ResponseEntity; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.util.UriComponentsBuilder; + +@RestController +@RequestMapping("/patients") +public class PatientController { + @Autowired + PatientRepository patientRepository; + + @PostMapping + @Transactional + public ResponseEntity register(@RequestBody @Valid PatientRegistrationData data, UriComponentsBuilder uriBuilder) { + var patient = new Patient(data); + patientRepository.save(patient); + + var uri = uriBuilder.path("/patients/{id}").buildAndExpand(patient.getId()).toUri(); + + return ResponseEntity.created(uri).body(new PatientDetailingData(patient)); + } + + @GetMapping + public ResponseEntity<Page<PatientListingData>> list(@PageableDefault(sort = {"name"}, direction = Sort.Direction.ASC) Pageable pagination) { + var page = patientRepository.findAllByActiveTrue(pagination).map(PatientListingData::new); + return ResponseEntity.ok(page); + } + + @PutMapping + @Transactional + public ResponseEntity update(@RequestBody @Valid PatientUpdateData data) { + Patient patient = patientRepository.getReferenceById(data.id()); + patient.updateInformation(data); + + return ResponseEntity.ok(new PatientDetailingData(patient)); + } + + @DeleteMapping("/{id}") + @Transactional + public ResponseEntity delete(@PathVariable Long id) { + Patient patient = patientRepository.getReferenceById(id); + patient.delete(); + + return ResponseEntity.noContent().build(); + } + + @GetMapping("/{id}") + public ResponseEntity detail(@PathVariable Long id) { + Patient patient = patientRepository.getReferenceById(id); + + return ResponseEntity.ok(new PatientDetailingData(patient)); + } +} diff --git a/api/src/main/java/med/voll/api/domain/address/Address.java b/api/src/main/java/med/voll/api/domain/address/Address.java new file mode 100644 index 0000000..b348d1f --- /dev/null +++ b/api/src/main/java/med/voll/api/domain/address/Address.java @@ -0,0 +1,45 @@ +package med.voll.api.domain.address; + +import jakarta.persistence.Embeddable; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Embeddable +@Getter +@NoArgsConstructor +@AllArgsConstructor +public class Address { + + private String addressLine1; + private String addressLine2; + private String postalCode; + private String city; + private String state; + + public Address(AddressData data) { + this.addressLine1 = data.addressLine1(); + this.addressLine2 = data.addressLine2(); + this.postalCode = data.postalCode(); + this.city = data.city(); + this.state = data.state(); + } + + public void updateInformation(AddressData data) { + if (data.addressLine1() != null) { + this.addressLine1 = data.addressLine1(); + } + if (data.addressLine2() != null) { + this.addressLine2 = data.addressLine2(); + } + if (data.postalCode() != null) { + this.postalCode = data.postalCode(); + } + if (data.city() != null) { + this.city = data.city(); + } + if (data.state() != null) { + this.state = data.state(); + } + } +} diff --git a/api/src/main/java/med/voll/api/domain/address/AddressData.java b/api/src/main/java/med/voll/api/domain/address/AddressData.java new file mode 100644 index 0000000..386219b --- /dev/null +++ b/api/src/main/java/med/voll/api/domain/address/AddressData.java @@ -0,0 +1,18 @@ +package med.voll.api.domain.address; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Pattern; + +public record AddressData( + @NotBlank(message = "Address1 is required") + String addressLine1, + String addressLine2, + @NotBlank(message = "Zip code is required") + @Pattern(regexp = "\\d{5}", message = "Zip code must have 9 digits") + String postalCode, + @NotBlank(message = "City is required") + String city, + @NotBlank(message = "State is required") + String state) { + +} diff --git a/api/src/main/java/med/voll/api/domain/doctor/Doctor.java b/api/src/main/java/med/voll/api/domain/doctor/Doctor.java new file mode 100644 index 0000000..9c3f5f5 --- /dev/null +++ b/api/src/main/java/med/voll/api/domain/doctor/Doctor.java @@ -0,0 +1,58 @@ +package med.voll.api.domain.doctor; + +import jakarta.persistence.*; +import lombok.AllArgsConstructor; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.NoArgsConstructor; +import med.voll.api.domain.address.Address; + +@Table(name = "doctors") +@Entity(name = "Doctor") +@Getter +@NoArgsConstructor +@AllArgsConstructor +@EqualsAndHashCode(of = "id") +public class Doctor { + + @Id @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + private String name; + private String email; + private String phone; + private Boolean active; + + @Enumerated(EnumType.STRING) + private Specialty specialty; + + @Embedded + private Address address; + + public Doctor(DoctorRegistrationData data) { + this.active = true; + this.name = data.name(); + this.email = data.email(); + this.phone = data.phone(); + this.specialty = data.specialty(); + this.address = new Address(data.addressData()); + } + + public void updateInformation(DoctorUpdateData data) { + if (data.name() != null) { + this.name = data.name(); + } + if (data.phone() != null) { + this.phone = data.phone(); + } + if (data.specialty() != null) { + this.specialty = data.specialty(); + } + if (data.addressData() != null) { + this.address.updateInformation(data.addressData()); + } + } + + public void delete() { + this.active = false; + } +} diff --git a/api/src/main/java/med/voll/api/domain/doctor/DoctorDetailingData.java b/api/src/main/java/med/voll/api/domain/doctor/DoctorDetailingData.java new file mode 100644 index 0000000..fff6d9c --- /dev/null +++ b/api/src/main/java/med/voll/api/domain/doctor/DoctorDetailingData.java @@ -0,0 +1,9 @@ +package med.voll.api.domain.doctor; + +import med.voll.api.domain.address.Address; + +public record DoctorDetailingData(Long id, String name, String email, String phone, Specialty specialty, Address address) { + public DoctorDetailingData(Doctor doctor) { + this(doctor.getId(), doctor.getName(), doctor.getEmail(), doctor.getPhone(), doctor.getSpecialty(), doctor.getAddress()); + } +} diff --git a/api/src/main/java/med/voll/api/domain/doctor/DoctorListingData.java b/api/src/main/java/med/voll/api/domain/doctor/DoctorListingData.java new file mode 100644 index 0000000..ec0ea33 --- /dev/null +++ b/api/src/main/java/med/voll/api/domain/doctor/DoctorListingData.java @@ -0,0 +1,8 @@ +package med.voll.api.domain.doctor; + +public record DoctorListingData(Long id, String name, String email, Specialty specialty) { + + public DoctorListingData(Doctor doctor) { + this(doctor.getId(), doctor.getName(), doctor.getEmail(), doctor.getSpecialty()); + } +} diff --git a/api/src/main/java/med/voll/api/domain/doctor/DoctorRegistrationData.java b/api/src/main/java/med/voll/api/domain/doctor/DoctorRegistrationData.java new file mode 100644 index 0000000..4ceaa2d --- /dev/null +++ b/api/src/main/java/med/voll/api/domain/doctor/DoctorRegistrationData.java @@ -0,0 +1,23 @@ +package med.voll.api.domain.doctor; + +import jakarta.validation.Valid; +import jakarta.validation.constraints.Email; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Pattern; +import med.voll.api.domain.address.AddressData; + +public record DoctorRegistrationData( + @NotBlank(message = "Name is required") + String name, + @Email(message = "Invalid email format") + @NotBlank(message = "Email is required") + String email, + @Pattern(regexp = "\\d{10}", message = "Telephone number format is invalid") + @NotBlank(message = "Telephone number is required") + String phone, + @NotNull(message = "Specialty is required") + Specialty specialty, + @NotNull(message = "Address data is required") + @Valid AddressData addressData) { +} diff --git a/api/src/main/java/med/voll/api/domain/doctor/DoctorRepository.java b/api/src/main/java/med/voll/api/domain/doctor/DoctorRepository.java new file mode 100644 index 0000000..1efd0af --- /dev/null +++ b/api/src/main/java/med/voll/api/domain/doctor/DoctorRepository.java @@ -0,0 +1,11 @@ +package med.voll.api.domain.doctor; + +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +@Repository +public interface DoctorRepository extends JpaRepository<Doctor, Long> { + Page<Doctor> findAllByActiveTrue(Pageable pagination); +} diff --git a/api/src/main/java/med/voll/api/domain/doctor/DoctorUpdateData.java b/api/src/main/java/med/voll/api/domain/doctor/DoctorUpdateData.java new file mode 100644 index 0000000..d3b9747 --- /dev/null +++ b/api/src/main/java/med/voll/api/domain/doctor/DoctorUpdateData.java @@ -0,0 +1,13 @@ +package med.voll.api.domain.doctor; + +import jakarta.validation.constraints.NotNull; +import med.voll.api.domain.address.AddressData; + +public record DoctorUpdateData( + @NotNull + Long id, + String name, + String phone, + Specialty specialty, + AddressData addressData) { +} diff --git a/api/src/main/java/med/voll/api/domain/doctor/Specialty.java b/api/src/main/java/med/voll/api/domain/doctor/Specialty.java new file mode 100644 index 0000000..7580089 --- /dev/null +++ b/api/src/main/java/med/voll/api/domain/doctor/Specialty.java @@ -0,0 +1,19 @@ +package med.voll.api.domain.doctor; + +public enum Specialty { + + ANESTHESIOLOGY, + CARDIOLOGY, + DERMATOLOGY, + GYNECOLOGY, + IMMUNOLOGY, + NEUROLOGY, + OPHTHALMOLOGY, + ORTHOPEDICS, + PATHOLOGY, + PEDIATRICS, + PSYCHIATRY, + RADIOLOGY, + SURGERY, + UROLOGY +} diff --git a/api/src/main/java/med/voll/api/domain/patient/Patient.java b/api/src/main/java/med/voll/api/domain/patient/Patient.java new file mode 100644 index 0000000..1e7562f --- /dev/null +++ b/api/src/main/java/med/voll/api/domain/patient/Patient.java @@ -0,0 +1,53 @@ +package med.voll.api.domain.patient; + +import jakarta.persistence.*; +import lombok.AllArgsConstructor; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.NoArgsConstructor; +import med.voll.api.domain.address.Address; + +@Table(name = "patients") +@Entity(name = "Patient") +@Getter +@NoArgsConstructor +@AllArgsConstructor +@EqualsAndHashCode(of = "id") +public class Patient { + + @Id @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + private String name; + private String email; + private String phone; + private String ssn; + private Boolean active; + + @Embedded + private Address address; + + public Patient(PatientRegistrationData data) { + this.active = true; + this.name = data.name(); + this.email = data.email(); + this.ssn = data.ssn(); + this.phone = data.phone(); + this.address = new Address(data.addressData()); + } + + public void updateInformation(PatientUpdateData data) { + if (data.name() != null) { + this.name = data.name(); + } + if (data.phone() != null) { + this.phone = data.phone(); + } + if (data.addressData() != null) { + this.address.updateInformation(data.addressData()); + } + } + + public void delete() { + this.active = false; + } +} diff --git a/api/src/main/java/med/voll/api/domain/patient/PatientDetailingData.java b/api/src/main/java/med/voll/api/domain/patient/PatientDetailingData.java new file mode 100644 index 0000000..76156be --- /dev/null +++ b/api/src/main/java/med/voll/api/domain/patient/PatientDetailingData.java @@ -0,0 +1,9 @@ +package med.voll.api.domain.patient; + +import med.voll.api.domain.address.Address; + +public record PatientDetailingData(Long id, String name, String email, String phone, String ssn, Address address) { + public PatientDetailingData(Patient patient) { + this(patient.getId(), patient.getName(), patient.getEmail(), patient.getPhone(), patient.getSsn(), patient.getAddress()); + } +} diff --git a/api/src/main/java/med/voll/api/domain/patient/PatientListingData.java b/api/src/main/java/med/voll/api/domain/patient/PatientListingData.java new file mode 100644 index 0000000..1d4a15d --- /dev/null +++ b/api/src/main/java/med/voll/api/domain/patient/PatientListingData.java @@ -0,0 +1,9 @@ +package med.voll.api.domain.patient; + +public record PatientListingData(Long id, String name, String phone) { + + public PatientListingData(Patient patient) { + this(patient.getId(), patient.getName(), patient.getPhone()); + } + +} diff --git a/api/src/main/java/med/voll/api/domain/patient/PatientRegistrationData.java b/api/src/main/java/med/voll/api/domain/patient/PatientRegistrationData.java new file mode 100644 index 0000000..ae1687d --- /dev/null +++ b/api/src/main/java/med/voll/api/domain/patient/PatientRegistrationData.java @@ -0,0 +1,24 @@ +package med.voll.api.domain.patient; + +import jakarta.validation.Valid; +import jakarta.validation.constraints.Email; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Pattern; +import med.voll.api.domain.address.AddressData; + +public record PatientRegistrationData( + @NotBlank(message = "Name is required") + String name, + @NotBlank(message = "Email is required") + @Email(message = "Invalid email format") + String email, + @NotBlank(message = "Telephone number is required") + @Pattern(regexp = "\\d{10}", message = "Telephone number format is invalid") + String phone, + @NotBlank(message = "SSN is required") + String ssn, + @NotNull(message = "Address is required") + @Valid + AddressData addressData) { +} diff --git a/api/src/main/java/med/voll/api/domain/patient/PatientRepository.java b/api/src/main/java/med/voll/api/domain/patient/PatientRepository.java new file mode 100644 index 0000000..e550ac2 --- /dev/null +++ b/api/src/main/java/med/voll/api/domain/patient/PatientRepository.java @@ -0,0 +1,11 @@ +package med.voll.api.domain.patient; + +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +@Repository +public interface PatientRepository extends JpaRepository<Patient, Long> { + Page<Patient> findAllByActiveTrue(Pageable pagination); +} diff --git a/api/src/main/java/med/voll/api/domain/patient/PatientUpdateData.java b/api/src/main/java/med/voll/api/domain/patient/PatientUpdateData.java new file mode 100644 index 0000000..55bf145 --- /dev/null +++ b/api/src/main/java/med/voll/api/domain/patient/PatientUpdateData.java @@ -0,0 +1,12 @@ +package med.voll.api.domain.patient; + +import jakarta.validation.constraints.NotNull; +import med.voll.api.domain.address.AddressData; + +public record PatientUpdateData( + @NotNull + Long id, + String name, + String phone, + AddressData addressData) { +} diff --git a/api/src/main/java/med/voll/api/domain/user/AuthenticationData.java b/api/src/main/java/med/voll/api/domain/user/AuthenticationData.java new file mode 100644 index 0000000..ce753b9 --- /dev/null +++ b/api/src/main/java/med/voll/api/domain/user/AuthenticationData.java @@ -0,0 +1,6 @@ +package med.voll.api.domain.user; + +import jakarta.validation.constraints.NotBlank; + +public record AuthenticationData(@NotBlank(message = "Email is required") String login, @NotBlank(message = "Password is required") String password) { +} diff --git a/api/src/main/java/med/voll/api/domain/user/AuthenticationService.java b/api/src/main/java/med/voll/api/domain/user/AuthenticationService.java new file mode 100644 index 0000000..b089a9f --- /dev/null +++ b/api/src/main/java/med/voll/api/domain/user/AuthenticationService.java @@ -0,0 +1,18 @@ +package med.voll.api.domain.user; + +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 AuthenticationService implements UserDetailsService { + @Autowired + private UserRepository userRepository; + + @Override + public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { + return userRepository.findByLogin(username); + } +} diff --git a/api/src/main/java/med/voll/api/domain/user/User.java b/api/src/main/java/med/voll/api/domain/user/User.java new file mode 100644 index 0000000..23d74e0 --- /dev/null +++ b/api/src/main/java/med/voll/api/domain/user/User.java @@ -0,0 +1,63 @@ +package med.voll.api.domain.user; + +import jakarta.persistence.*; +import lombok.AllArgsConstructor; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.NoArgsConstructor; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.core.userdetails.UserDetails; + +import java.util.Collection; +import java.util.List; + +@Table(name = "users") +@Entity(name = "User") +@Getter +@NoArgsConstructor +@AllArgsConstructor +@EqualsAndHashCode(of = "id") +public class User implements UserDetails { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + private String login; + private String password; + + @Override + public Collection<? extends GrantedAuthority> getAuthorities() { + return List.of(new SimpleGrantedAuthority("ROLE_USER")); + } + + @Override + public String getPassword() { + return this.password; + } + + @Override + public String getUsername() { + return this.login; + } + + @Override + public boolean isAccountNonExpired() { + return true; + } + + @Override + public boolean isAccountNonLocked() { + return true; + } + + @Override + public boolean isCredentialsNonExpired() { + return true; + } + + @Override + public boolean isEnabled() { + return true; + } +} diff --git a/api/src/main/java/med/voll/api/domain/user/UserRepository.java b/api/src/main/java/med/voll/api/domain/user/UserRepository.java new file mode 100644 index 0000000..20ce55d --- /dev/null +++ b/api/src/main/java/med/voll/api/domain/user/UserRepository.java @@ -0,0 +1,8 @@ +package med.voll.api.domain.user; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.security.core.userdetails.UserDetails; + +public interface UserRepository extends JpaRepository<User, Long> { + UserDetails findByLogin(String login); +} diff --git a/api/src/main/java/med/voll/api/infra/exception/ErrorTreatment.java b/api/src/main/java/med/voll/api/infra/exception/ErrorTreatment.java new file mode 100644 index 0000000..9e49fd3 --- /dev/null +++ b/api/src/main/java/med/voll/api/infra/exception/ErrorTreatment.java @@ -0,0 +1,29 @@ +package med.voll.api.infra.exception; + +import jakarta.persistence.EntityNotFoundException; +import org.springframework.http.ResponseEntity; +import org.springframework.validation.FieldError; +import org.springframework.web.bind.MethodArgumentNotValidException; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.RestControllerAdvice; + +@RestControllerAdvice +public class ErrorTreatment { + + @ExceptionHandler(EntityNotFoundException.class) + public ResponseEntity treatError404() { + return ResponseEntity.notFound().build(); + } + + @ExceptionHandler(MethodArgumentNotValidException.class) + public ResponseEntity treatError400(MethodArgumentNotValidException ex) { + var errors = ex.getFieldErrors(); + return ResponseEntity.badRequest().body(errors.stream().map(ValidationErrorData::new).toList()); + } + + private record ValidationErrorData(String field, String message) { + public ValidationErrorData(FieldError error) { + this(error.getField(), error.getDefaultMessage()); + } + } +} diff --git a/api/src/main/java/med/voll/api/infra/security/SecurityConfigurations.java b/api/src/main/java/med/voll/api/infra/security/SecurityConfigurations.java new file mode 100644 index 0000000..5055162 --- /dev/null +++ b/api/src/main/java/med/voll/api/infra/security/SecurityConfigurations.java @@ -0,0 +1,44 @@ +package med.voll.api.infra.security; + +import org.springframework.beans.factory.annotation.Autowired; +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.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.crypto.password.PasswordEncoder; +import org.springframework.security.web.SecurityFilterChain; +import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; + +@Configuration +@EnableWebSecurity +public class SecurityConfigurations { + + @Autowired + private SecurityFilter securityFilter; + + @Bean + public SecurityFilterChain securityFilterChain(HttpSecurity httpSecurity) throws Exception { + return httpSecurity.csrf().disable() + .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS) + .and().authorizeHttpRequests() + .requestMatchers(HttpMethod.POST, "/login").permitAll() + .anyRequest().authenticated() + .and().addFilterBefore(securityFilter, UsernamePasswordAuthenticationFilter.class) + .build(); + } + + @Bean + public AuthenticationManager authenticationManager(AuthenticationConfiguration authenticationConfiguration) throws Exception { + return authenticationConfiguration.getAuthenticationManager(); + } + + @Bean + public PasswordEncoder passwordEncoder() { + return new BCryptPasswordEncoder(); + } +} diff --git a/api/src/main/java/med/voll/api/infra/security/SecurityFilter.java b/api/src/main/java/med/voll/api/infra/security/SecurityFilter.java new file mode 100644 index 0000000..42f3df5 --- /dev/null +++ b/api/src/main/java/med/voll/api/infra/security/SecurityFilter.java @@ -0,0 +1,48 @@ +package med.voll.api.infra.security; + +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import med.voll.api.domain.user.UserRepository; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.stereotype.Component; +import org.springframework.web.filter.OncePerRequestFilter; + +import java.io.IOException; + +@Component +public class SecurityFilter extends OncePerRequestFilter { + + @Autowired + private TokenService tokenService; + + @Autowired + private UserRepository userRepository; + + @Override + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { + var tokenJWT = retrieveToken(request); + + if (tokenJWT != null) { + var subject = tokenService.getSubject(tokenJWT); + var user = userRepository.findByLogin(subject); + + var authentication = new UsernamePasswordAuthenticationToken(user, null, user.getAuthorities()); + SecurityContextHolder.getContext().setAuthentication(authentication); + } + + filterChain.doFilter(request, response); + } + + private String retrieveToken(HttpServletRequest request) { + var authorizationHeader = request.getHeader("Authorization"); + if (authorizationHeader != null) { + return authorizationHeader.replace("Bearer ", ""); + } + + return null; + } +} diff --git a/api/src/main/java/med/voll/api/infra/security/TokenJWTData.java b/api/src/main/java/med/voll/api/infra/security/TokenJWTData.java new file mode 100644 index 0000000..2be8aa8 --- /dev/null +++ b/api/src/main/java/med/voll/api/infra/security/TokenJWTData.java @@ -0,0 +1,4 @@ +package med.voll.api.infra.security; + +public record TokenJWTData(String token) { +} diff --git a/api/src/main/java/med/voll/api/infra/security/TokenService.java b/api/src/main/java/med/voll/api/infra/security/TokenService.java new file mode 100644 index 0000000..63dda73 --- /dev/null +++ b/api/src/main/java/med/voll/api/infra/security/TokenService.java @@ -0,0 +1,50 @@ +package med.voll.api.infra.security; + +import com.auth0.jwt.JWT; +import com.auth0.jwt.algorithms.Algorithm; +import com.auth0.jwt.exceptions.JWTCreationException; +import com.auth0.jwt.exceptions.JWTVerificationException; +import med.voll.api.domain.user.User; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; + +import java.time.Instant; +import java.time.LocalDateTime; +import java.time.ZoneOffset; + +@Service +public class TokenService { + + @Value("${api.security.token.secret}") + private String secret; + + public String generateToken(User user) { + try { + var algorithm = Algorithm.HMAC256(secret); + return JWT.create() + .withIssuer("API Voll.med") + .withSubject(user.getLogin()) + .withExpiresAt(expirationDate()) + .sign(algorithm); + } catch (JWTCreationException exception){ + throw new RuntimeException("Error when generating token JWT", exception); + } + } + + public String getSubject(String tokenJWT) { + try { + var algorithm = Algorithm.HMAC256(secret); + return JWT.require(algorithm) + .withIssuer("API Voll.med") + .build() + .verify(tokenJWT) + .getSubject(); + } catch (JWTVerificationException exception){ + throw new RuntimeException("Invalid or expired TokenJWT!"); + } + } + + private Instant expirationDate() { + return LocalDateTime.now().plusHours(2).toInstant(ZoneOffset.of("-03:00")); + } +} |
