diff --git a/pom.xml b/pom.xml index d6ef542..ee2a1cb 100644 --- a/pom.xml +++ b/pom.xml @@ -78,6 +78,32 @@ spring-boot-starter-webflux + + io.jsonwebtoken + jjwt-api + 0.11.5 + + + io.jsonwebtoken + jjwt-impl + 0.11.5 + runtime + + + io.jsonwebtoken + jjwt-jackson + 0.11.5 + runtime + + + org.springframework.boot + spring-boot-starter-security + + + org.springframework.security + spring-security-test + test + diff --git a/src/main/java/_11/asktpk/artisanconnectbackend/config/AppConfig.java b/src/main/java/_11/asktpk/artisanconnectbackend/config/AppConfig.java new file mode 100644 index 0000000..9636a39 --- /dev/null +++ b/src/main/java/_11/asktpk/artisanconnectbackend/config/AppConfig.java @@ -0,0 +1,15 @@ +package _11.asktpk.artisanconnectbackend.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.security.crypto.password.PasswordEncoder; + +@Configuration +public class AppConfig { + + @Bean + public PasswordEncoder passwordEncoder() { + return new BCryptPasswordEncoder(); + } +} \ No newline at end of file diff --git a/src/main/java/_11/asktpk/artisanconnectbackend/config/SecurityConfig.java b/src/main/java/_11/asktpk/artisanconnectbackend/config/SecurityConfig.java new file mode 100644 index 0000000..34e57d6 --- /dev/null +++ b/src/main/java/_11/asktpk/artisanconnectbackend/config/SecurityConfig.java @@ -0,0 +1,39 @@ +package _11.asktpk.artisanconnectbackend.config; + +import _11.asktpk.artisanconnectbackend.security.JwtRequestFilter; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +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.config.http.SessionCreationPolicy; +import org.springframework.security.web.SecurityFilterChain; +import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; + +@Configuration +@EnableWebSecurity +public class SecurityConfig { + + private final JwtRequestFilter jwtRequestFilter; + + public SecurityConfig(JwtRequestFilter jwtRequestFilter) { + this.jwtRequestFilter = jwtRequestFilter; + } + + @Bean + public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { + http + .cors(cors -> cors.configure(http)) + .csrf(AbstractHttpConfigurer::disable) + .authorizeHttpRequests(auth -> auth + .requestMatchers("/api/v1/auth/**").permitAll() + .requestMatchers("/api/v1/admin/**").hasRole("ADMIN") + .anyRequest().authenticated()) + .sessionManagement(session -> session + .sessionCreationPolicy(SessionCreationPolicy.STATELESS)); + + http.addFilterBefore(jwtRequestFilter, UsernamePasswordAuthenticationFilter.class); + + return http.build(); + } +} \ No newline at end of file diff --git a/src/main/java/_11/asktpk/artisanconnectbackend/controller/AuthController.java b/src/main/java/_11/asktpk/artisanconnectbackend/controller/AuthController.java new file mode 100644 index 0000000..5f6d98d --- /dev/null +++ b/src/main/java/_11/asktpk/artisanconnectbackend/controller/AuthController.java @@ -0,0 +1,77 @@ +package _11.asktpk.artisanconnectbackend.controller; + +import _11.asktpk.artisanconnectbackend.dto.*; +import _11.asktpk.artisanconnectbackend.entities.Client; +import _11.asktpk.artisanconnectbackend.security.JwtUtil; +import _11.asktpk.artisanconnectbackend.service.ClientService; +import jakarta.servlet.http.HttpServletRequest; +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.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping("/api/v1/auth") +public class AuthController { + + private final ClientService clientService; + private final JwtUtil jwtUtil; + + public AuthController(ClientService clientService, JwtUtil jwtUtil) { + this.clientService = clientService; + this.jwtUtil = jwtUtil; + } + + @PostMapping("/login") + public ResponseEntity login(@RequestBody AuthRequestDTO authRequestDTO) { + if (clientService.checkClientCredentials(authRequestDTO)) { + Client client = clientService.getClientByEmail(authRequestDTO.getEmail()); + Long userId = client.getId(); + String userRole = client.getRole().getRole(); + + String token = jwtUtil.generateToken(client.getEmail(), userRole, userId); + + return ResponseEntity.status(HttpStatus.OK) + .body(new AuthResponseDTO(userId, userRole, token)); + } else { + return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(null); + } + } + + @PostMapping("/register") + public ResponseEntity register(@RequestBody ClientRegistrationDTO clientDTO) { + if (clientService.getClientByEmail(clientDTO.getEmail()) != null) { + return ResponseEntity.status(HttpStatus.CONFLICT).build(); + } + + ClientDTO savedClient = clientService.registerClient(clientDTO); + + String token = jwtUtil.generateToken( + savedClient.getEmail(), + savedClient.getRole().getRole(), + savedClient.getId() + ); + + return ResponseEntity.status(HttpStatus.CREATED) + .body(new AuthResponseDTO( + savedClient.getId(), + savedClient.getRole().getRole(), + token + )); + } + + @PostMapping("/logout") + public ResponseEntity logout(HttpServletRequest request) { + String authHeader = request.getHeader("Authorization"); + + if (authHeader != null && authHeader.startsWith("Bearer ")) { + String token = authHeader.substring(7); + jwtUtil.blacklistToken(token); + return ResponseEntity.ok(new RequestResponseDTO("Successfully logged out")); + } + + return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(new RequestResponseDTO("Invalid token")); + } +} \ No newline at end of file diff --git a/src/main/java/_11/asktpk/artisanconnectbackend/controller/VariablesController.java b/src/main/java/_11/asktpk/artisanconnectbackend/controller/VariablesController.java index 009aeb4..e9767ba 100644 --- a/src/main/java/_11/asktpk/artisanconnectbackend/controller/VariablesController.java +++ b/src/main/java/_11/asktpk/artisanconnectbackend/controller/VariablesController.java @@ -13,7 +13,6 @@ import java.util.Map; @RestController @RequestMapping("/api/v1/vars") public class VariablesController { - @GetMapping("/categories") public List getAllVariables() { List categoriesDTOList = new ArrayList<>(); @@ -31,10 +30,4 @@ public class VariablesController { public List getAllStatuses() { return List.of(Enums.Status.values()); } - - @GetMapping("/roles") - public List getAllRoles() { - return List.of(Enums.Role.values()); - } - } diff --git a/src/main/java/_11/asktpk/artisanconnectbackend/dto/AuthRequestDTO.java b/src/main/java/_11/asktpk/artisanconnectbackend/dto/AuthRequestDTO.java new file mode 100644 index 0000000..3c37189 --- /dev/null +++ b/src/main/java/_11/asktpk/artisanconnectbackend/dto/AuthRequestDTO.java @@ -0,0 +1,10 @@ +package _11.asktpk.artisanconnectbackend.dto; + +import lombok.Getter; +import lombok.Setter; + +@Getter @Setter +public class AuthRequestDTO { + private String email; + private String password; +} diff --git a/src/main/java/_11/asktpk/artisanconnectbackend/dto/AuthResponseDTO.java b/src/main/java/_11/asktpk/artisanconnectbackend/dto/AuthResponseDTO.java new file mode 100644 index 0000000..5c76d39 --- /dev/null +++ b/src/main/java/_11/asktpk/artisanconnectbackend/dto/AuthResponseDTO.java @@ -0,0 +1,12 @@ +package _11.asktpk.artisanconnectbackend.dto; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.Setter; + +@Getter @Setter @AllArgsConstructor +public class AuthResponseDTO { + private Long user_id; + private String user_role; + private String token; +} diff --git a/src/main/java/_11/asktpk/artisanconnectbackend/dto/ClientDTO.java b/src/main/java/_11/asktpk/artisanconnectbackend/dto/ClientDTO.java index 09fde80..4be1595 100644 --- a/src/main/java/_11/asktpk/artisanconnectbackend/dto/ClientDTO.java +++ b/src/main/java/_11/asktpk/artisanconnectbackend/dto/ClientDTO.java @@ -6,7 +6,7 @@ import lombok.Setter; import jakarta.validation.constraints.Email; -import _11.asktpk.artisanconnectbackend.utils.Enums.Role; +import _11.asktpk.artisanconnectbackend.entities.Role; @Getter @Setter public class ClientDTO { diff --git a/src/main/java/_11/asktpk/artisanconnectbackend/dto/ClientRegistrationDTO.java b/src/main/java/_11/asktpk/artisanconnectbackend/dto/ClientRegistrationDTO.java new file mode 100644 index 0000000..4d4cd07 --- /dev/null +++ b/src/main/java/_11/asktpk/artisanconnectbackend/dto/ClientRegistrationDTO.java @@ -0,0 +1,16 @@ +package _11.asktpk.artisanconnectbackend.dto; + +import jakarta.validation.constraints.Email; +import jakarta.validation.constraints.NotBlank; +import lombok.Getter; +import lombok.Setter; + +@Getter @Setter +public class ClientRegistrationDTO { + @Email + @NotBlank + private String email; + private String firstName; + private String lastName; + private String password; +} diff --git a/src/main/java/_11/asktpk/artisanconnectbackend/entities/Client.java b/src/main/java/_11/asktpk/artisanconnectbackend/entities/Client.java index 0a25d3d..c6ca7c0 100644 --- a/src/main/java/_11/asktpk/artisanconnectbackend/entities/Client.java +++ b/src/main/java/_11/asktpk/artisanconnectbackend/entities/Client.java @@ -1,11 +1,11 @@ package _11.asktpk.artisanconnectbackend.entities; -import _11.asktpk.artisanconnectbackend.utils.Enums.Role; - import jakarta.persistence.*; import lombok.Getter; import lombok.Setter; +import org.hibernate.annotations.CreationTimestamp; +import java.util.Date; import java.util.List; @Entity @@ -24,14 +24,15 @@ public class Client { private String lastName; - private String image; // Optional field + private String image; - @Enumerated(EnumType.STRING) + @ManyToOne(cascade = CascadeType.ALL) + @JoinColumn(name = "role_id", referencedColumnName = "id") private Role role; -// @OneToMany(mappedBy = "client", cascade = CascadeType.ALL) -// private List notices; - @OneToMany(mappedBy = "client", cascade = CascadeType.ALL) private List orders; + + @CreationTimestamp + private Date createdAt; } diff --git a/src/main/java/_11/asktpk/artisanconnectbackend/entities/GlobalVariables.java b/src/main/java/_11/asktpk/artisanconnectbackend/entities/GlobalVariables.java deleted file mode 100644 index cb50f6f..0000000 --- a/src/main/java/_11/asktpk/artisanconnectbackend/entities/GlobalVariables.java +++ /dev/null @@ -1,16 +0,0 @@ -package _11.asktpk.artisanconnectbackend.entities; - -import jakarta.persistence.*; - -@Entity -@Table(name = "global_variables") -public class GlobalVariables { - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - private Long id; - - private String name; - private String value; - - // Getters, setters, and constructors -} diff --git a/src/main/java/_11/asktpk/artisanconnectbackend/entities/Role.java b/src/main/java/_11/asktpk/artisanconnectbackend/entities/Role.java new file mode 100644 index 0000000..ec2d8a8 --- /dev/null +++ b/src/main/java/_11/asktpk/artisanconnectbackend/entities/Role.java @@ -0,0 +1,19 @@ +package _11.asktpk.artisanconnectbackend.entities; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import lombok.Getter; +import lombok.Setter; + +@Entity +@Table(name = "roles") +@Getter +@Setter +public class Role { + @Id + private Long id; + @Column(name="rolename") + private String role; +} diff --git a/src/main/java/_11/asktpk/artisanconnectbackend/repository/ClientRepository.java b/src/main/java/_11/asktpk/artisanconnectbackend/repository/ClientRepository.java index d4d07b7..eccd51f 100644 --- a/src/main/java/_11/asktpk/artisanconnectbackend/repository/ClientRepository.java +++ b/src/main/java/_11/asktpk/artisanconnectbackend/repository/ClientRepository.java @@ -1,8 +1,8 @@ package _11.asktpk.artisanconnectbackend.repository; import _11.asktpk.artisanconnectbackend.entities.Client; - import org.springframework.data.jpa.repository.JpaRepository; public interface ClientRepository extends JpaRepository { + Client findByEmail(String email); } diff --git a/src/main/java/_11/asktpk/artisanconnectbackend/repository/RolesRepository.java b/src/main/java/_11/asktpk/artisanconnectbackend/repository/RolesRepository.java new file mode 100644 index 0000000..d766026 --- /dev/null +++ b/src/main/java/_11/asktpk/artisanconnectbackend/repository/RolesRepository.java @@ -0,0 +1,10 @@ +package _11.asktpk.artisanconnectbackend.repository; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; +import _11.asktpk.artisanconnectbackend.entities.Role; + +@Repository +public interface RolesRepository extends JpaRepository { + Role findRoleById(Long id); +} diff --git a/src/main/java/_11/asktpk/artisanconnectbackend/security/JwtRequestFilter.java b/src/main/java/_11/asktpk/artisanconnectbackend/security/JwtRequestFilter.java new file mode 100644 index 0000000..7700ee2 --- /dev/null +++ b/src/main/java/_11/asktpk/artisanconnectbackend/security/JwtRequestFilter.java @@ -0,0 +1,67 @@ +package _11.asktpk.artisanconnectbackend.security; + +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.jetbrains.annotations.NotNull; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.web.authentication.WebAuthenticationDetailsSource; +import org.springframework.stereotype.Component; +import org.springframework.web.filter.OncePerRequestFilter; + +import java.io.IOException; +import java.util.Collections; + +@Component +public class JwtRequestFilter extends OncePerRequestFilter { + + private final JwtUtil jwtUtil; + + public JwtRequestFilter(JwtUtil jwtUtil) { + this.jwtUtil = jwtUtil; + } + + @Override + protected void doFilterInternal(HttpServletRequest request, @NotNull HttpServletResponse response, @NotNull FilterChain chain) + throws ServletException, IOException { + + final String authorizationHeader = request.getHeader("Authorization"); + + String email = null; + String jwt = null; + + if (authorizationHeader != null && authorizationHeader.startsWith("Bearer ")) { + jwt = authorizationHeader.substring(7); + + if (jwtUtil.isBlacklisted(jwt)) { + response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); + return; + } + + try { + email = jwtUtil.extractEmail(jwt); + } catch (Exception e) { + logger.error(e.getMessage()); + response.setStatus(HttpServletResponse.SC_BAD_REQUEST); + return; + } + } + + if (email != null && SecurityContextHolder.getContext().getAuthentication() == null) { + String role = jwtUtil.extractRole(jwt); + + UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken( + email, null, Collections.singletonList(new SimpleGrantedAuthority("ROLE_" + role))); + + authToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); + SecurityContextHolder.getContext().setAuthentication(authToken); + } + +// logger.info("Token of user " + jwtUtil.extractEmail(jwt) + (jwtUtil.isTokenExpired(jwt) ? " is expired" : " is not expired")); + + chain.doFilter(request, response); + } +} \ No newline at end of file diff --git a/src/main/java/_11/asktpk/artisanconnectbackend/security/JwtUtil.java b/src/main/java/_11/asktpk/artisanconnectbackend/security/JwtUtil.java new file mode 100644 index 0000000..4ac4895 --- /dev/null +++ b/src/main/java/_11/asktpk/artisanconnectbackend/security/JwtUtil.java @@ -0,0 +1,80 @@ +package _11.asktpk.artisanconnectbackend.security; + +import io.jsonwebtoken.Claims; +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.SignatureAlgorithm; +import io.jsonwebtoken.security.Keys; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; + +import javax.crypto.SecretKey; +import java.util.Date; +import java.util.HashMap; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.function.Function; + +@Component +public class JwtUtil { + + @Value("${jwt.secret:defaultSecretKeyNeedsToBeAtLeast32BytesLong}") + private String secret; + + @Value("${jwt.expiration}") + private long expiration; + + // sterowanie tokenami wygasnietymi + private final Set blacklistedTokens = ConcurrentHashMap.newKeySet(); + + public void blacklistToken(String token) { + blacklistedTokens.add(token); + } + + public boolean isBlacklisted(String token) { + return blacklistedTokens.contains(token); + } + + + private SecretKey getSigningKey() { + return Keys.hmacShaKeyFor(secret.getBytes()); + } + + public String generateToken(String email, String role, Long userId) { + Map claims = new HashMap<>(); + claims.put("role", role); + claims.put("userId", userId); + return createToken(claims, email); + } + + private String createToken(Map claims, String subject) { + return Jwts.builder() + .setClaims(claims) + .setSubject(subject) + .setIssuedAt(new Date()) + .setExpiration(new Date(System.currentTimeMillis() + expiration)) + .signWith(getSigningKey(), SignatureAlgorithm.HS256) + .compact(); + } + + public String extractEmail(String token) { + return extractClaim(token, Claims::getSubject); + } + + public String extractRole(String token) { + return extractAllClaims(token).get("role", String.class); + } + + public T extractClaim(String token, Function claimsResolver) { + final Claims claims = extractAllClaims(token); + return claimsResolver.apply(claims); + } + + private Claims extractAllClaims(String token) { + return Jwts.parserBuilder() + .setSigningKey(getSigningKey()) + .build() + .parseClaimsJws(token) + .getBody(); + } +} \ No newline at end of file diff --git a/src/main/java/_11/asktpk/artisanconnectbackend/service/ClientService.java b/src/main/java/_11/asktpk/artisanconnectbackend/service/ClientService.java index 2394015..115480a 100644 --- a/src/main/java/_11/asktpk/artisanconnectbackend/service/ClientService.java +++ b/src/main/java/_11/asktpk/artisanconnectbackend/service/ClientService.java @@ -1,9 +1,13 @@ package _11.asktpk.artisanconnectbackend.service; +import _11.asktpk.artisanconnectbackend.dto.AuthRequestDTO; import _11.asktpk.artisanconnectbackend.dto.ClientDTO; +import _11.asktpk.artisanconnectbackend.dto.ClientRegistrationDTO; import _11.asktpk.artisanconnectbackend.entities.Client; import _11.asktpk.artisanconnectbackend.repository.ClientRepository; +import _11.asktpk.artisanconnectbackend.repository.RolesRepository; import jakarta.persistence.EntityNotFoundException; +import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.stereotype.Service; import java.util.List; @@ -11,9 +15,13 @@ import java.util.List; @Service public class ClientService { private final ClientRepository clientRepository; + private final PasswordEncoder passwordEncoder; + private final RolesRepository rolesRepository; - public ClientService(ClientRepository clientRepository) { + public ClientService(ClientRepository clientRepository, PasswordEncoder passwordEncoder, RolesRepository rolesRepository) { this.clientRepository = clientRepository; + this.passwordEncoder = passwordEncoder; + this.rolesRepository = rolesRepository; } private ClientDTO toDto(Client client) { @@ -42,6 +50,16 @@ public class ClientService { return client; } + private Client fromDto(ClientRegistrationDTO dto) { + Client client = new Client(); + + client.setFirstName(dto.getFirstName()); + client.setLastName(dto.getLastName()); + client.setEmail(dto.getEmail()); + client.setPassword(dto.getPassword()); + return client; + } + public List getAllClients() { List clients = clientRepository.findAll(); return clients.stream().map(this::toDto).toList(); @@ -75,4 +93,26 @@ public class ClientService { public void deleteClient(Long id) { clientRepository.deleteById(id); } + + // И замените метод checkClientCredentials на: + public boolean checkClientCredentials(AuthRequestDTO dto) { + Client cl = clientRepository.findByEmail(dto.getEmail()); + if (cl == null) { + return false; + } + + return passwordEncoder.matches(dto.getPassword(), cl.getPassword()); + } + + // При создании нового пользователя не забудьте шифровать пароль: + public ClientDTO registerClient(ClientRegistrationDTO clientDTO) { + Client client = fromDto(clientDTO); + client.setRole(rolesRepository.findRoleById(1L)); + client.setPassword(passwordEncoder.encode(client.getPassword())); + return toDto(clientRepository.save(client)); + } + + public Client getClientByEmail(String email) { + return clientRepository.findByEmail(email); + } } diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index ca1826a..4f7ea0c 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -23,3 +23,7 @@ tpay.clientSecret = 44898642be53381cdcc47f3e44bf5a15e592f5d270fc3a6cf6fb81a8b8eb tpay.authUrl = https://openapi.sandbox.tpay.com/oauth/auth tpay.transactionUrl = https://openapi.sandbox.tpay.com/transactions +#jwt settings +jwt.secret=DIXLsOs3FKmCAQwISd0SKsHMXJrPl3IKIRkVlkOvYW7kEcdUTbxh8zFe1B3eZWkY +jwt.expiration=300000 + diff --git a/src/main/resources/sql/data.sql b/src/main/resources/sql/data.sql index 3a9c65e..d9c2989 100644 --- a/src/main/resources/sql/data.sql +++ b/src/main/resources/sql/data.sql @@ -1,10 +1,15 @@ -INSERT INTO clients (email, first_name, image, last_name, password, role) +INSERT INTO roles (id, rolename) VALUES - ('dignissim.tempor.arcu@aol.ca', 'Diana', 'null', 'Harrison', 'password', 'USER'), - ('john.doe@example.com', 'John', 'null', 'Doe', 'password123', 'ADMIN'), - ('jane.smith@example.com', 'Jane', 'null', 'Smith', 'securepass', 'USER'), - ('michael.brown@example.com', 'Michael', 'null', 'Brown', 'mypassword', 'USER'), - ('emily.jones@example.com', 'Emily', 'null', 'Jones', 'passw0rd', 'USER'); + (1, 'USER'), + (2, 'ADMIN'); + +INSERT INTO clients (email, first_name, last_name, password, role_id) +VALUES + ('dignissim.tempor.arcu@aol.ca', 'Diana', 'Harrison', 'password', 1), + ('john.doe@example.com', 'John', 'Doe', 'password123', 2), + ('jane.smith@example.com', 'Jane', 'Smith', 'securepass', 1), + ('michael.brown@example.com', 'Michael', 'Brown', 'mypassword', 1), + ('emily.jones@example.com', 'Emily', 'Jones', 'passw0rd', 1); INSERT INTO notice (title, description, client_id, price, category, status, publish_date) VALUES