diff --git a/pom.xml b/pom.xml index 5bc6c53..6a8c65e 100644 --- a/pom.xml +++ b/pom.xml @@ -1,30 +1,34 @@ - 4.0.0 org.springframework.boot spring-boot-starter-parent 3.3.3 - + com libraryman-api 0.0.1-SNAPSHOT libraryman-api - Revolutionize book management with LibraryMan! Easily track stock, borrowers, and due dates, streamlining operations for schools, companies, and libraries worldwide, ensuring efficient and organized book lending. - + Revolutionize book management with LibraryMan! Easily track + stock, borrowers, and due dates, streamlining operations for schools, + companies, and libraries worldwide, ensuring efficient and organized + book lending. + - + - + - - - - + + + + 17 @@ -47,6 +51,26 @@ runtime true + + + io.jsonwebtoken + jjwt-api + 0.11.5 + + + + io.jsonwebtoken + jjwt-impl + 0.11.5 + runtime + + + + io.jsonwebtoken + jjwt-jackson + 0.11.5 + runtime + com.mysql @@ -67,9 +91,24 @@ org.springframework.boot - spring-boot-starter-cache + spring-boot-starter-security + + + + org.springframework.security + spring-security-test + test + + + + org.springframework.security + spring-security-oauth2-client + + org.springframework.boot + spring-boot-starter-cache + diff --git a/src/main/java/com/libraryman_api/LibrarymanApiApplication.java b/src/main/java/com/libraryman_api/LibrarymanApiApplication.java index 042e728..fcd4665 100644 --- a/src/main/java/com/libraryman_api/LibrarymanApiApplication.java +++ b/src/main/java/com/libraryman_api/LibrarymanApiApplication.java @@ -16,4 +16,4 @@ public static void main(String[] args) { SpringApplication.run(LibrarymanApiApplication.class, args); } -} +} \ No newline at end of file diff --git a/src/main/java/com/libraryman_api/book/BookController.java b/src/main/java/com/libraryman_api/book/BookController.java index 4401e67..8a59125 100644 --- a/src/main/java/com/libraryman_api/book/BookController.java +++ b/src/main/java/com/libraryman_api/book/BookController.java @@ -8,6 +8,7 @@ import org.springframework.data.domain.Sort; import org.springframework.data.web.PageableDefault; import org.springframework.http.ResponseEntity; +import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.web.bind.annotation.*; /** @@ -71,6 +72,7 @@ public ResponseEntity getBookById(@PathVariable int id) { * @return the added {@link Book} object. */ @PostMapping + @PreAuthorize("hasRole('LIBRARIAN') or hasRole('ADMIN')") public BookDto addBook(@RequestBody BookDto bookDto) { return bookService.addBook(bookDto); } @@ -83,6 +85,7 @@ public BookDto addBook(@RequestBody BookDto bookDto) { * @return the updated {@link Book} object. */ @PutMapping("/{id}") + @PreAuthorize("hasRole('LIBRARIAN') or hasRole('ADMIN')") public BookDto updateBook(@PathVariable int id, @RequestBody BookDto bookDtoDetails) { return bookService.updateBook(id, bookDtoDetails); } @@ -93,6 +96,7 @@ public BookDto updateBook(@PathVariable int id, @RequestBody BookDto bookDtoDeta * @param id the ID of the book to delete. */ @DeleteMapping("/{id}") + @PreAuthorize("hasRole('LIBRARIAN') or hasRole('ADMIN')") public void deleteBook(@PathVariable int id) { bookService.deleteBook(id); } diff --git a/src/main/java/com/libraryman_api/borrowing/BorrowingController.java b/src/main/java/com/libraryman_api/borrowing/BorrowingController.java index 0a6dc65..2c36af8 100644 --- a/src/main/java/com/libraryman_api/borrowing/BorrowingController.java +++ b/src/main/java/com/libraryman_api/borrowing/BorrowingController.java @@ -2,6 +2,8 @@ import com.libraryman_api.exception.ResourceNotFoundException; +import org.springframework.security.access.prepost.PreAuthorize; + import org.springframework.data.domain.Page; import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Pageable; @@ -39,6 +41,7 @@ public BorrowingController(BorrowingService borrowingService) { * The results are sorted by borrow date by default and limited to 5 members per page. */ @GetMapping + @PreAuthorize("hasRole('LIBRARIAN') or hasRole('ADMIN')") public Page getAllBorrowings(@PageableDefault(page=0, size=5, sort="borrowDate") Pageable pageable, @RequestParam(required = false) String sortBy, @RequestParam(required = false) String sortDir) { @@ -64,6 +67,7 @@ public Page getAllBorrowings(@PageableDefault(page=0, size=5, sor * @return the saved {@link Borrowings} object representing the borrowing record. */ @PostMapping + @PreAuthorize("hasRole('LIBRARIAN') or hasRole('ADMIN') or (hasRole('USER') and #borrowingsDto.member.memberId == authentication.principal.memberId)") public BorrowingsDto borrowBook(@RequestBody BorrowingsDto borrowingsDto) { return borrowingService.borrowBook(borrowingsDto); } @@ -101,6 +105,7 @@ public String payFine(@PathVariable int id) { * The results are sorted by borrow date by default and limited to 5 members per page. */ @GetMapping("member/{memberId}") + @PreAuthorize("hasRole('LIBRARIAN') or hasRole('ADMIN') or (hasRole('USER') and #memberId == authentication.principal.memberId)") public Page getAllBorrowingsOfAMember(@PathVariable int memberId, @PageableDefault(page=0, size=5, sort="borrowDate") Pageable pageable, @RequestParam(required = false) String sortBy, @@ -128,6 +133,7 @@ public Page getAllBorrowingsOfAMember(@PathVariable int memberId, * @throws ResourceNotFoundException if the borrowing record with the specified ID is not found. */ @GetMapping("{borrowingId}") + @PreAuthorize("hasRole('LIBRARIAN') or hasRole('ADMIN')") public BorrowingsDto getBorrowingById(@PathVariable int borrowingId) { return borrowingService.getBorrowingById(borrowingId) .orElseThrow(() -> new ResourceNotFoundException("Borrowing not found")); diff --git a/src/main/java/com/libraryman_api/member/MemberController.java b/src/main/java/com/libraryman_api/member/MemberController.java index da67d9e..2f921a8 100644 --- a/src/main/java/com/libraryman_api/member/MemberController.java +++ b/src/main/java/com/libraryman_api/member/MemberController.java @@ -8,6 +8,7 @@ import org.springframework.data.domain.Sort; import org.springframework.data.web.PageableDefault; import org.springframework.http.ResponseEntity; +import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.web.bind.annotation.*; /** @@ -39,6 +40,7 @@ public MemberController(MemberService memberService) { * The results are sorted by name by default and limited to 5 members per page. */ @GetMapping + @PreAuthorize("hasRole('LIBRARIAN') or hasRole('ADMIN')") public Page getAllMembers(@PageableDefault(page=0, size=5, sort="name") Pageable pageable, @RequestParam(required = false) String sortBy, @RequestParam(required = false) String sortDir) { @@ -65,23 +67,13 @@ public Page getAllMembers(@PageableDefault(page=0, size=5, sort="nam * @return a {@link ResponseEntity} containing the found {@link Members} object */ @GetMapping("/{id}") + @PreAuthorize("hasRole('LIBRARIAN') or hasRole('ADMIN')") public ResponseEntity getMemberById(@PathVariable int id) { return memberService.getMemberById(id) .map(ResponseEntity::ok) .orElseThrow(() -> new ResourceNotFoundException("Member not found")); } - /** - * Adds a new library member. - * - * @param membersDto the {@link Members} object representing the new member - * @return the added {@link Members} object - */ - @PostMapping - public MembersDto addMember(@RequestBody MembersDto membersDto) { - return memberService.addMember(membersDto); - } - /** * Updates an existing library member. * If the member is not found, a {@link ResourceNotFoundException} is thrown. @@ -102,6 +94,7 @@ public MembersDto updateMember(@PathVariable int id, @RequestBody MembersDto mem * @param id the ID of the member to delete */ @DeleteMapping("/{id}") + @PreAuthorize("hasRole('LIBRARIAN') or hasRole('ADMIN')") public void deleteMember(@PathVariable int id) { memberService.deleteMember(id); } diff --git a/src/main/java/com/libraryman_api/member/MemberRepository.java b/src/main/java/com/libraryman_api/member/MemberRepository.java index fffa8d8..2c02216 100644 --- a/src/main/java/com/libraryman_api/member/MemberRepository.java +++ b/src/main/java/com/libraryman_api/member/MemberRepository.java @@ -10,6 +10,7 @@ public interface MemberRepository extends JpaRepository { Optional findByMemberId(int memberId); + Optional findByUsername(String username); /** * SELECT SUM(amount) AS totalFines diff --git a/src/main/java/com/libraryman_api/member/MemberService.java b/src/main/java/com/libraryman_api/member/MemberService.java index 50262b8..2554db5 100644 --- a/src/main/java/com/libraryman_api/member/MemberService.java +++ b/src/main/java/com/libraryman_api/member/MemberService.java @@ -113,6 +113,7 @@ public MembersDto updateMember(int memberId, MembersDto membersDtoDetails) { Members member = memberRepository.findById(memberId) .orElseThrow(() -> new ResourceNotFoundException("Member not found")); member.setName(membersDtoDetails.getName()); + member.setUsername(membersDtoDetails.getUsername()); member.setEmail(membersDtoDetails.getEmail()); member.setPassword(membersDtoDetails.getPassword()); member.setRole(membersDtoDetails.getRole()); @@ -133,7 +134,6 @@ public MembersDto updateMember(int memberId, MembersDto membersDtoDetails) { * @param memberId the ID of the member to delete * @throws ResourceNotFoundException if the member is not found */ - @CacheEvict(value = "members", key = "#memberId") public void deleteMember(int memberId) { Members member = memberRepository.findById(memberId) @@ -145,45 +145,45 @@ public void deleteMember(int memberId) { notificationService.accountDeletionNotification(member); memberRepository.delete(member); } + /** * Converts a MembersDto object to a Members entity. * *

This method takes a MembersDto object and transforms it into a Members entity * to be used in database operations. It maps all relevant member details from - * the DTO, including member ID, role, name, email, password, and membership date.

+ * the DTO, including member ID, role, name, username, email, password, and membership date.

* * @param membersDto the DTO object containing member information * @return a Members entity with data populated from the DTO */ - - public Members DtoEntity(MembersDto membersDto){ Members members= new Members(); members.setMemberId(membersDto.getMemberId()); members.setRole(membersDto.getRole()); members.setName(membersDto.getName()); + members.setUsername(membersDto.getUsername()); members.setEmail(membersDto.getEmail()); members.setPassword(membersDto.getPassword()); members.setMembershipDate(membersDto.getMembershipDate()); return members; } + /** * Converts a Members entity to a MembersDto object. * *

This method takes a Members entity object and converts it into a MembersDto * object to be used for data transfer between layers. It maps all necessary - * member details, including member ID, name, role, email, password, and membership + * member details, including member ID, name, username, role, email, password, and membership * date, from the entity to the DTO.

* * @param members the entity object containing member information * @return a MembersDto object with data populated from the entity */ - - public MembersDto EntityToDto(Members members){ MembersDto membersDto= new MembersDto(); membersDto.setMemberId(members.getMemberId()); membersDto.setName(members.getName()); + membersDto.setUsername(members.getUsername()); membersDto.setRole(members.getRole()); membersDto.setEmail(members.getEmail()); membersDto.setPassword(members.getPassword()); diff --git a/src/main/java/com/libraryman_api/member/Members.java b/src/main/java/com/libraryman_api/member/Members.java index 352a3c7..aabdad3 100644 --- a/src/main/java/com/libraryman_api/member/Members.java +++ b/src/main/java/com/libraryman_api/member/Members.java @@ -2,11 +2,17 @@ import jakarta.persistence.*; +import java.util.Collection; +import java.util.Collections; import java.util.Date; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.core.userdetails.UserDetails; + @Entity -public class Members { +public class Members implements UserDetails{ @Id @GeneratedValue(strategy = GenerationType.SEQUENCE, @@ -20,6 +26,9 @@ public class Members { @Column(nullable = false) private String name; + @Column(name = "username") + private String username; + @Column(unique = true, nullable = false) private String email; @@ -33,6 +42,7 @@ public class Members { @Column(name = "membership_date") private Date membershipDate; + public Members() { @@ -91,4 +101,19 @@ public Date getMembershipDate() { public void setMembershipDate(Date membershipDate) { this.membershipDate = membershipDate; } + + public String getUsername() { + return username; + } + + public void setUsername(String username) { + this.username = username; + } + + @Override + public Collection getAuthorities() { + // TODO Auto-generated method stub + return Collections.singletonList(new SimpleGrantedAuthority("ROLE_"+role.name())); + } + } diff --git a/src/main/java/com/libraryman_api/member/MembersDto.java b/src/main/java/com/libraryman_api/member/MembersDto.java index d4f67fe..93a7c73 100644 --- a/src/main/java/com/libraryman_api/member/MembersDto.java +++ b/src/main/java/com/libraryman_api/member/MembersDto.java @@ -4,10 +4,11 @@ public class MembersDto { - private int memberId; private String name; + + private String username; private String email; @@ -19,9 +20,10 @@ public class MembersDto { private Date membershipDate; - public MembersDto(int memberId, String name, String email, String password, Role role, Date membershipDate) { + public MembersDto(int memberId, String name, String username, String email, String password, Role role, Date membershipDate) { this.memberId = memberId; this.name = name; + this.username = username; this.email = email; this.password = password; this.role = role; @@ -42,10 +44,18 @@ public void setMemberId(int memberId) { public String getName() { return name; } + + public String getUsername() { + return username; + } public void setName(String name) { this.name = name; } + + public void setUsername(String username) { + this.username = username; + } public String getEmail() { return email; @@ -84,6 +94,7 @@ public String toString() { return "MembersDto{" + "memberId=" + memberId + ", name='" + name + '\'' + + ", username='" + username + '\'' + ", email='" + email + '\'' + ", password='" + password + '\'' + ", role=" + role + diff --git a/src/main/java/com/libraryman_api/security/config/PasswordEncoder.java b/src/main/java/com/libraryman_api/security/config/PasswordEncoder.java new file mode 100644 index 0000000..4d2da03 --- /dev/null +++ b/src/main/java/com/libraryman_api/security/config/PasswordEncoder.java @@ -0,0 +1,14 @@ +package com.libraryman_api.security.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; + +@Configuration +public class PasswordEncoder { + + @Bean + public BCryptPasswordEncoder bCryptPasswordEncoder() { + return new BCryptPasswordEncoder(); + } +} \ No newline at end of file diff --git a/src/main/java/com/libraryman_api/security/config/WebConfiguration.java b/src/main/java/com/libraryman_api/security/config/WebConfiguration.java new file mode 100644 index 0000000..47d2d60 --- /dev/null +++ b/src/main/java/com/libraryman_api/security/config/WebConfiguration.java @@ -0,0 +1,87 @@ +package com.libraryman_api.security.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +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.configuration.EnableWebSecurity; +import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; +import org.springframework.security.web.SecurityFilterChain; +import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; +import org.springframework.web.cors.CorsConfiguration; +import org.springframework.web.cors.CorsConfigurationSource; +import org.springframework.web.cors.UrlBasedCorsConfigurationSource; +import org.springframework.web.filter.CorsFilter; + +import com.libraryman_api.security.jwt.JwtAuthenticationFilter; + +import static org.springframework.security.config.Customizer.withDefaults; +import static org.springframework.security.config.http.SessionCreationPolicy.STATELESS; + +@Configuration +@EnableWebSecurity(debug = true) // Do not use (debug=true) in a production system! as this contain sensitive information. +@EnableMethodSecurity(prePostEnabled = true) +public class WebConfiguration { + + private JwtAuthenticationFilter jwtFilter; + + public WebConfiguration(JwtAuthenticationFilter jwtFilter) { + this.jwtFilter=jwtFilter; + } + @Bean + public SecurityFilterChain web(HttpSecurity http) throws Exception { + http + .csrf(AbstractHttpConfigurer::disable) + .authorizeHttpRequests((request) -> request + // make sure it is in order to access the proper Url + + .requestMatchers("/api/signup").permitAll() + .requestMatchers("/api/login").permitAll() + .requestMatchers("/api/logout").permitAll() + .anyRequest().authenticated() + ) + .logout(logout->logout + .deleteCookies("LibraryManCookie")) + .sessionManagement(session -> session.sessionCreationPolicy(STATELESS)) + .formLogin(withDefaults()); + + http.addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter.class) + .httpBasic(httpBasic -> {}); + + http.oauth2Login(withDefaults()); + return http.build(); + } + + @Bean + public AuthenticationManager authenticationManager(AuthenticationConfiguration builder) throws Exception { + return builder.getAuthenticationManager(); + } + @Bean + public CorsConfigurationSource corsConfigurationSource() { + UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); + CorsConfiguration corsConfiguration = new CorsConfiguration(); + corsConfiguration.setAllowCredentials(true); + corsConfiguration.addAllowedOriginPattern("*"); + corsConfiguration.addAllowedHeader("Authorization"); + corsConfiguration.addAllowedHeader("Content-Type"); + corsConfiguration.addAllowedHeader("Accept"); + corsConfiguration.addAllowedMethod("POST"); + corsConfiguration.addAllowedMethod("PUT"); + corsConfiguration.addAllowedMethod("GET"); + corsConfiguration.addAllowedMethod("DELETE"); + corsConfiguration.addAllowedMethod("OPTIONS"); + corsConfiguration.setMaxAge(3600L); + + source.registerCorsConfiguration("/**", corsConfiguration); + return source; + } + + @Bean + public CorsFilter corsFilter() { + return new CorsFilter(corsConfigurationSource()); + } + +} diff --git a/src/main/java/com/libraryman_api/security/controllers/LoginController.java b/src/main/java/com/libraryman_api/security/controllers/LoginController.java new file mode 100644 index 0000000..aed0c1e --- /dev/null +++ b/src/main/java/com/libraryman_api/security/controllers/LoginController.java @@ -0,0 +1,43 @@ +package com.libraryman_api.security.controllers; + +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RestController; + +import com.libraryman_api.security.model.LoginRequest; +import com.libraryman_api.security.model.LoginResponse; +import com.libraryman_api.security.services.LoginService; + +import jakarta.servlet.http.Cookie; +import jakarta.servlet.http.HttpServletResponse; + +@RestController +public class LoginController { + + private final LoginService loginService; + + public LoginController(LoginService loginService) { + this.loginService = loginService; + } + + @PostMapping("/api/login") + public ResponseEntity login(@RequestBody LoginRequest loginRequest, HttpServletResponse response) { + LoginResponse loginResponse = loginService.login(loginRequest); + + if (loginResponse != null) { + setAuthCookie(response); + return new ResponseEntity<>(loginResponse, HttpStatus.OK); + } else { + return new ResponseEntity<>(HttpStatus.UNAUTHORIZED); + } + } + + private void setAuthCookie(HttpServletResponse response) { + Cookie cookie = new Cookie("LibraryManCookie", "libraryman_cookie"); + cookie.setMaxAge(3600); // (3600 seconds) + cookie.setPath("/"); + response.addCookie(cookie); + } +} diff --git a/src/main/java/com/libraryman_api/security/controllers/LogoutController.java b/src/main/java/com/libraryman_api/security/controllers/LogoutController.java new file mode 100644 index 0000000..20d4a92 --- /dev/null +++ b/src/main/java/com/libraryman_api/security/controllers/LogoutController.java @@ -0,0 +1,34 @@ +package com.libraryman_api.security.controllers; + +import jakarta.servlet.http.Cookie; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +import org.springframework.http.ResponseEntity; +import org.springframework.security.core.Authentication; +import org.springframework.security.web.authentication.logout.SecurityContextLogoutHandler; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +public class LogoutController { + + @PostMapping("/api/logout") + public ResponseEntity logout(HttpServletRequest request, HttpServletResponse response, Authentication authentication) { + if (authentication != null) { + new SecurityContextLogoutHandler().logout(request, response, authentication); + } + removeAuthCookie(response); + + return ResponseEntity.ok("Successfully logged out."); + } + + private void removeAuthCookie(HttpServletResponse response) { + Cookie cookie = new Cookie("LibraryManCookie", null); + cookie.setMaxAge(0); + cookie.setPath("/"); + cookie.setHttpOnly(true); + cookie.setSecure(true); + response.addCookie(cookie); + } +} diff --git a/src/main/java/com/libraryman_api/security/controllers/SignupController.java b/src/main/java/com/libraryman_api/security/controllers/SignupController.java new file mode 100644 index 0000000..a1f6891 --- /dev/null +++ b/src/main/java/com/libraryman_api/security/controllers/SignupController.java @@ -0,0 +1,36 @@ +package com.libraryman_api.security.controllers; + +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RestController; + +import com.libraryman_api.member.Members; +import com.libraryman_api.security.services.SignupService; + + +@RestController +public class SignupController { + + private SignupService signupService; + public SignupController(SignupService signupService) { + this.signupService=signupService; + } + + @PostMapping("/api/signup") + public void signup(@RequestBody Members members) { + this.signupService.signup(members); + + } + @PostMapping("/api/signup/admin") + @PreAuthorize("hasRole('ADMIN')") + public void signupAdmin(@RequestBody Members members) { + this.signupService.signupAdmin(members); + } + @PostMapping("/api/signup/librarian") + @PreAuthorize("hasRole('LIBRARIAN') or hasRole('ADMIN')") + public void signupLibrarian(@RequestBody Members members) { + this.signupService.signupLibrarian(members); + } +} diff --git a/src/main/java/com/libraryman_api/security/jwt/JwtAuthenticationFilter.java b/src/main/java/com/libraryman_api/security/jwt/JwtAuthenticationFilter.java new file mode 100644 index 0000000..875779b --- /dev/null +++ b/src/main/java/com/libraryman_api/security/jwt/JwtAuthenticationFilter.java @@ -0,0 +1,62 @@ +package com.libraryman_api.security.jwt; + + +import java.io.IOException; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.web.authentication.WebAuthenticationDetailsSource; +import org.springframework.stereotype.Component; +import org.springframework.web.filter.OncePerRequestFilter; + +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +@Component +public class JwtAuthenticationFilter extends OncePerRequestFilter { + + private JwtAuthenticationHelper jwtHelper; + + private UserDetailsService userDetailsService; + + public JwtAuthenticationFilter(JwtAuthenticationHelper jwtHelper,UserDetailsService userDetailsService) { + this.jwtHelper=jwtHelper; + this.userDetailsService=userDetailsService; + } + + @Override + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) + throws ServletException, IOException { + + String requestHeader=request.getHeader("Authorization"); + String username=null; + String token=null; + if(requestHeader!=null && requestHeader.startsWith("Bearer ")) { + token=requestHeader.substring(7); + username=jwtHelper.getUsernameFromToken(token); + if(username!=null && SecurityContextHolder.getContext().getAuthentication() == null) { + UserDetails userDetails=userDetailsService.loadUserByUsername(username); + if(!(jwtHelper.isTokenExpired(token))) { + UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken=new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities()); + usernamePasswordAuthenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); + SecurityContextHolder.getContext().setAuthentication(usernamePasswordAuthenticationToken); + + } + else { + System.out.println("Token is expired or user details not found."); + } + } + + } + filterChain.doFilter(request, response); + } + + + + +} diff --git a/src/main/java/com/libraryman_api/security/jwt/JwtAuthenticationHelper.java b/src/main/java/com/libraryman_api/security/jwt/JwtAuthenticationHelper.java new file mode 100644 index 0000000..b44e446 --- /dev/null +++ b/src/main/java/com/libraryman_api/security/jwt/JwtAuthenticationHelper.java @@ -0,0 +1,49 @@ +package com.libraryman_api.security.jwt; + +import java.util.Date; +import java.util.HashMap; +import java.util.Map; + +import javax.crypto.spec.SecretKeySpec; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.stereotype.Component; + +import io.jsonwebtoken.Claims; +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.SignatureAlgorithm; + +@Component +public class JwtAuthenticationHelper { + + private static final long JWT_TOKEN_VALIDITY=60*60; + @Value("${jwt.secretKey}") + private String secret; + public String getUsernameFromToken(String token) { + String username=getClaimsFromToken(token).getSubject(); + return username; + } + + public Claims getClaimsFromToken(String token) { + Claims claims=Jwts.parserBuilder() + .setSigningKey(secret.getBytes()) + .build().parseClaimsJws(token).getBody(); + return claims; + } + + public Boolean isTokenExpired(String token) { + Claims claims=getClaimsFromToken(token); + Date expDate=claims.getExpiration(); + return expDate.before(new Date()); + } + + public String generateToken(UserDetails userDetails) { + Map claims=new HashMap<>(); + return Jwts.builder().setClaims(claims).setSubject(userDetails.getUsername()) + .setIssuedAt(new Date(System.currentTimeMillis())) + .setExpiration(new Date(System.currentTimeMillis()+JWT_TOKEN_VALIDITY*1000)) + .signWith(new SecretKeySpec(secret.getBytes(),SignatureAlgorithm.HS512.getJcaName()),SignatureAlgorithm.HS512) + .compact(); + } +} diff --git a/src/main/java/com/libraryman_api/security/model/LoginRequest.java b/src/main/java/com/libraryman_api/security/model/LoginRequest.java new file mode 100644 index 0000000..e77d1cd --- /dev/null +++ b/src/main/java/com/libraryman_api/security/model/LoginRequest.java @@ -0,0 +1,28 @@ +package com.libraryman_api.security.model; + +public class LoginRequest { + + private String username; + + private String password; + + + + public String getUsername() { + return username; + } + + public void setUsername(String username) { + this.username = username; + } + + public String getPassword() { + return password; + } + + public void setPassword(String password) { + this.password = password; + } + + +} diff --git a/src/main/java/com/libraryman_api/security/model/LoginResponse.java b/src/main/java/com/libraryman_api/security/model/LoginResponse.java new file mode 100644 index 0000000..2b53d41 --- /dev/null +++ b/src/main/java/com/libraryman_api/security/model/LoginResponse.java @@ -0,0 +1,21 @@ +package com.libraryman_api.security.model; + + +public class LoginResponse { + + private String token; + + public LoginResponse(String token) { + this.token=token; + } + public String getToken() { + return token; + } + + public void setToken(String token) { + this.token = token; + } + + + +} diff --git a/src/main/java/com/libraryman_api/security/services/CustomUserDetailsService.java b/src/main/java/com/libraryman_api/security/services/CustomUserDetailsService.java new file mode 100644 index 0000000..2c66c82 --- /dev/null +++ b/src/main/java/com/libraryman_api/security/services/CustomUserDetailsService.java @@ -0,0 +1,22 @@ +package com.libraryman_api.security.services; + +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; + +import com.libraryman_api.member.MemberRepository; +@Service +public class CustomUserDetailsService implements UserDetailsService{ + + @Autowired + MemberRepository memberRepository; + + @Override + public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { + + return memberRepository.findByUsername(username).orElseThrow(()-> new UsernameNotFoundException("Username not Found")); + } + +} diff --git a/src/main/java/com/libraryman_api/security/services/LoginService.java b/src/main/java/com/libraryman_api/security/services/LoginService.java new file mode 100644 index 0000000..c255c4e --- /dev/null +++ b/src/main/java/com/libraryman_api/security/services/LoginService.java @@ -0,0 +1,51 @@ +package com.libraryman_api.security.services; + +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.authentication.BadCredentialsException; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.stereotype.Service; + +import com.libraryman_api.member.MemberRepository; +import com.libraryman_api.security.jwt.JwtAuthenticationHelper; +import com.libraryman_api.security.model.LoginRequest; +import com.libraryman_api.security.model.LoginResponse; + + +@Service +public class LoginService { + + private AuthenticationManager authenticationManager; + + private UserDetailsService userDetailsService; + + private JwtAuthenticationHelper jwtHelper; + + private MemberRepository memberRepository; + + public LoginService(AuthenticationManager authenticationManager,UserDetailsService userDetailsService,JwtAuthenticationHelper jwtHelper,MemberRepository memberRepository) { + this.authenticationManager=authenticationManager; + this.userDetailsService=userDetailsService; + this.jwtHelper=jwtHelper; + this.memberRepository=memberRepository; + } + + public LoginResponse login(LoginRequest loginRequest) { + Authenticate(loginRequest.getUsername(), loginRequest.getPassword()); + UserDetails userDetails=userDetailsService.loadUserByUsername(loginRequest.getUsername()); + String token=jwtHelper.generateToken(userDetails); + LoginResponse loginResponse=new LoginResponse(token); + return loginResponse; + } + + public void Authenticate(String username,String password) { + UsernamePasswordAuthenticationToken authenticateToken=new UsernamePasswordAuthenticationToken(username, password); + try { + authenticationManager.authenticate(authenticateToken); + } + catch(BadCredentialsException e){ + throw new BadCredentialsException("Invalid Username or Password"); + } + } +} diff --git a/src/main/java/com/libraryman_api/security/services/SignupService.java b/src/main/java/com/libraryman_api/security/services/SignupService.java new file mode 100644 index 0000000..33ff122 --- /dev/null +++ b/src/main/java/com/libraryman_api/security/services/SignupService.java @@ -0,0 +1,88 @@ +package com.libraryman_api.security.services; + +import java.util.Date; +import java.util.Optional; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; + + +import com.libraryman_api.exception.ResourceNotFoundException; +import com.libraryman_api.member.MemberRepository; +import com.libraryman_api.member.Members; +import com.libraryman_api.member.Role; +import com.libraryman_api.security.config.PasswordEncoder; + +@Service +public class SignupService { + + private MemberRepository memberRepository; + + private PasswordEncoder passwordEncoder; + + + public SignupService(MemberRepository memberRepository,PasswordEncoder passwordEncoder) { + this.memberRepository=memberRepository; + this.passwordEncoder=passwordEncoder; + } + public void signup(Members members) { + Optional memberOptId=memberRepository.findById(members.getMemberId()); + Optional memberOptUsername=memberRepository.findByUsername(members.getUsername()); + if(memberOptId.isPresent()) { + throw new ResourceNotFoundException("User already Exists"); + } + if(memberOptUsername.isPresent()) { + throw new ResourceNotFoundException("User already Exists"); + } + String encoded_password=passwordEncoder.bCryptPasswordEncoder().encode(members.getPassword()); + Members new_members=new Members(); + new_members.setEmail(members.getEmail()); + new_members.setName(members.getName()); + new_members.setPassword(encoded_password); + new_members.setRole(Role.USER); + new_members.setMembershipDate(new Date()); + new_members.setUsername(members.getUsername()); + memberRepository.save(new_members); + } + + public void signupAdmin(Members members) { + Optional memberOptId=memberRepository.findById(members.getMemberId()); + Optional memberOptUsername=memberRepository.findByUsername(members.getUsername()); + if(memberOptId.isPresent()) { + throw new ResourceNotFoundException("User already Exists"); + } + if(memberOptUsername.isPresent()) { + throw new ResourceNotFoundException("User already Exists"); + } + + String encoded_password=passwordEncoder.bCryptPasswordEncoder().encode(members.getPassword()); + Members new_members=new Members(); + new_members.setEmail(members.getEmail()); + new_members.setName(members.getName()); + new_members.setPassword(encoded_password); + new_members.setRole(Role.ADMIN); + new_members.setMembershipDate(new Date()); + new_members.setUsername(members.getUsername()); + memberRepository.save(new_members); + + } + public void signupLibrarian(Members members) { + Optional memberOptId=memberRepository.findById(members.getMemberId()); + Optional memberOptUsername=memberRepository.findByUsername(members.getUsername()); + if(memberOptId.isPresent()) { + throw new ResourceNotFoundException("User already Exists"); + } + if(memberOptUsername.isPresent()) { + throw new ResourceNotFoundException("User already Exists"); + } + String encoded_password=passwordEncoder.bCryptPasswordEncoder().encode(members.getPassword()); + Members new_members=new Members(); + new_members.setEmail(members.getEmail()); + new_members.setName(members.getName()); + new_members.setPassword(encoded_password); + new_members.setRole(Role.LIBRARIAN); + new_members.setMembershipDate(new Date()); + new_members.setUsername(members.getUsername()); + memberRepository.save(new_members); + } +} diff --git a/src/main/resources/application-development.properties b/src/main/resources/application-development.properties index aedaf67..30ef46b 100644 --- a/src/main/resources/application-development.properties +++ b/src/main/resources/application-development.properties @@ -30,8 +30,6 @@ server.error.include-exception=true # Logging for Spring Security logging.level.org.springframework.security=TRACE - - # --- Mail Service Setup --- # I use docker mail service https://hub.docker.com/r/maildev/maildev @@ -43,6 +41,3 @@ spring.mail.password=Add_Your_Mail_Service_Password spring.mail.properties.mail.smtp.auth=Add_Your_Mail_Service_SMTP spring.mail.properties.mail.starttls.enable=Add_Your_Mail_Service_Start_TLS spring.mail.properties.domain_name=Add_Your_Mail_Service_Domain_Name - - - diff --git a/src/main/resources/application-production.properties b/src/main/resources/application-production.properties index 6b8edc4..3e13ee0 100644 --- a/src/main/resources/application-production.properties +++ b/src/main/resources/application-production.properties @@ -6,7 +6,6 @@ spring.datasource.driver-class-name=${DATABASE_DRIVER_CLASS_NAME} spring.jpa.hibernate.ddl-auto=update spring.jpa.show-sql=false - # --- Mail Service Setup --- spring.mail.host=${MAIL_SERVICE_HOST} spring.mail.port=${MAIL_SERVICE_PORT} @@ -14,4 +13,10 @@ spring.mail.username=${MAIL_SERVICE_USERNAME} spring.mail.password=${MAIL_SERVICE_PASSWORD} spring.mail.properties.mail.smtp.auth=${MAIL_SERVICE_SMTP} spring.mail.properties.mail.starttls.enable=${MAIL_SERVICE_STARTTLS} -spring.mail.properties.domain_name=${MAIL_SERVICE_DOMAIN_NAME} \ No newline at end of file +spring.mail.properties.domain_name=${MAIL_SERVICE_DOMAIN_NAME} + +# --- Oauth 2.0 Configurations --- +spring.security.oauth2.client.registration.google.client-name=google +spring.security.oauth2.client.registration.google.client-id=${YOUR_CLIENT_ID} +spring.security.oauth2.client.registration.google.client-secret=${YOUR_SECRET_KEY} +spring.security.oauth2.client.registration.google.scope=email,profile \ No newline at end of file diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 53718a7..9cd73f6 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -1,2 +1,3 @@ spring.application.name=libraryman-api -spring.profiles.active=${ENV:development} \ No newline at end of file +spring.profiles.active=${ENV:development} +jwt.secretKey=${YOUR_JWT_SECRET_KEY} \ No newline at end of file