From e8e7d01ec08af3042f806d7697e37da77534a499 Mon Sep 17 00:00:00 2001 From: maksim Date: Wed, 25 Feb 2026 12:39:35 +0300 Subject: [PATCH] [FEATURE] OAUTH Authentication with access and refresh tokens. Sessions service --- docker-compose.yml | 29 +++++ pom.xml | 24 ++++ .../nto/configuration/JwtAuthFilter.java | 53 ++++++++ .../nto/configuration/SecurityConfig.java | 59 +++++++++ .../controller/AuthorizationController.java | 31 +++++ .../nto/controller/BookingController.java | 3 +- .../nto/controller/EmployeeController.java | 5 +- .../nto/controller/dto/CreateAccountDto.java | 14 +++ .../example/nto/controller/dto/LoginDto.java | 9 ++ .../java/com/example/nto/entity/Employee.java | 35 +++++- .../java/com/example/nto/entity/Session.java | 29 +++++ .../exception/DataValidationException.java | 7 ++ .../nto/exception/WrongPasswordException.java | 7 ++ .../handler/GlobalExceptionHandler.java | 14 ++- .../nto/repository/EmployeeRepository.java | 11 +- .../nto/repository/SessionRepository.java | 18 +++ .../nto/service/AuthorizationService.java | 9 ++ .../impl/AuthorizationServiceImpl.java | 116 ++++++++++++++++++ .../nto/service/impl/BookingServiceImpl.java | 2 + .../service/impl/EmployeeDetailsService.java | 30 +++++ .../nto/service/impl/EmployeeServiceImpl.java | 3 +- .../nto/service/impl/TokenService.java | 84 +++++++++++++ ...wordWithKnownUsernameValidatorService.java | 107 ++++++++++++++++ src/main/resources/application.yml | 6 +- .../1/0/2025-11-05--0001-employee.xml | 32 ----- .../1/0/2025-11-05--0002-place.xml | 27 ---- .../1/0/2025-11-05--0003-booking.xml | 36 ------ .../data/2025-11-05--0001-employee-data.xml | 14 --- .../data/2025-11-05--0002-place-data.xml | 14 --- .../data/2025-11-05--0003-booking-data.xml | 14 --- .../csv/2025-11-05--0001-employee-data.csv | 5 - .../data/csv/2025-11-05--0002-place-data.csv | 4 - .../csv/2025-11-05--0003-booking-data.csv | 3 - .../db.changelog/db.changelog-master.xml | 14 --- stuff.md | 4 +- 35 files changed, 695 insertions(+), 177 deletions(-) create mode 100644 docker-compose.yml create mode 100644 src/main/java/com/example/nto/configuration/JwtAuthFilter.java create mode 100644 src/main/java/com/example/nto/configuration/SecurityConfig.java create mode 100644 src/main/java/com/example/nto/controller/AuthorizationController.java create mode 100644 src/main/java/com/example/nto/controller/dto/CreateAccountDto.java create mode 100644 src/main/java/com/example/nto/controller/dto/LoginDto.java create mode 100644 src/main/java/com/example/nto/entity/Session.java create mode 100644 src/main/java/com/example/nto/exception/DataValidationException.java create mode 100644 src/main/java/com/example/nto/exception/WrongPasswordException.java create mode 100644 src/main/java/com/example/nto/repository/SessionRepository.java create mode 100644 src/main/java/com/example/nto/service/AuthorizationService.java create mode 100644 src/main/java/com/example/nto/service/impl/AuthorizationServiceImpl.java create mode 100644 src/main/java/com/example/nto/service/impl/EmployeeDetailsService.java create mode 100644 src/main/java/com/example/nto/service/impl/TokenService.java create mode 100644 src/main/java/com/example/nto/validation/PasswordWithKnownUsernameValidatorService.java delete mode 100644 src/main/resources/db.changelog/1/0/2025-11-05--0001-employee.xml delete mode 100644 src/main/resources/db.changelog/1/0/2025-11-05--0002-place.xml delete mode 100644 src/main/resources/db.changelog/1/0/2025-11-05--0003-booking.xml delete mode 100644 src/main/resources/db.changelog/data/2025-11-05--0001-employee-data.xml delete mode 100644 src/main/resources/db.changelog/data/2025-11-05--0002-place-data.xml delete mode 100644 src/main/resources/db.changelog/data/2025-11-05--0003-booking-data.xml delete mode 100644 src/main/resources/db.changelog/data/csv/2025-11-05--0001-employee-data.csv delete mode 100644 src/main/resources/db.changelog/data/csv/2025-11-05--0002-place-data.csv delete mode 100644 src/main/resources/db.changelog/data/csv/2025-11-05--0003-booking-data.csv delete mode 100644 src/main/resources/db.changelog/db.changelog-master.xml diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..f6a8b53 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,29 @@ +version: '3.8' +services: + api: + build: + context: . + dockerfile: Dockerfile + ports: + - "8080:8080" # todo: ?change + restart: unless-stopped + environment: + - SERVER_PORT=8080 + - SPRING_DATASOURCE_URL=jdbc:postgresql://hotel_db + - SPRING_DATASOURCE_USERNAME=admin + - SPRING_DATASOURCE_PASSWORD=secure_pwd + + postgres: + image: postgres:14.7-alpine + environment: + POSTGRES_DB: hotel_db + POSTGRES_USER: admin + POSTGRES_PASSWORD: secure_pwd + ports: + - "15432:5432" + #volumes: + # - db-data + restart: unless-stopped + +#volumes: +# db-data: diff --git a/pom.xml b/pom.xml index 28d9035..3a5d2bf 100644 --- a/pom.xml +++ b/pom.xml @@ -61,6 +61,10 @@ org.springframework.boot spring-boot-starter-web + + org.springframework.boot + spring-boot-starter-security + com.h2database h2 @@ -88,6 +92,26 @@ springdoc-openapi-starter-webmvc-ui 2.8.8 + + io.jsonwebtoken + jjwt-api + 0.11.5 + + + io.jsonwebtoken + jjwt-impl + 0.11.5 + + + io.jsonwebtoken + jjwt-jackson + 0.11.5 + + + com.google.guava + guava + 32.1.3-jre + \ No newline at end of file diff --git a/src/main/java/com/example/nto/configuration/JwtAuthFilter.java b/src/main/java/com/example/nto/configuration/JwtAuthFilter.java new file mode 100644 index 0000000..f2232da --- /dev/null +++ b/src/main/java/com/example/nto/configuration/JwtAuthFilter.java @@ -0,0 +1,53 @@ +package com.example.nto.configuration; + +import com.example.nto.service.impl.EmployeeDetailsService; +import com.example.nto.service.impl.TokenService; +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.web.authentication.WebAuthenticationDetailsSource; +import org.springframework.stereotype.Component; +import org.springframework.web.filter.OncePerRequestFilter; + +import java.io.IOException; + +@Component +public class JwtAuthFilter extends OncePerRequestFilter { + + private final TokenService tokenService; + private final EmployeeDetailsService userDetailsService; + + public JwtAuthFilter(TokenService tokenService, EmployeeDetailsService userDetailsService) { + this.tokenService = tokenService; + this.userDetailsService = userDetailsService; + } + + @Override + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { + String authHeader = request.getHeader("Authorization"); + String token = null; + String username = null; + + if (authHeader != null && authHeader.startsWith("Bearer ")) { + token = authHeader.substring(7); + username = tokenService.extractUsername(token); + } + + if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) { + UserDetails userDetails = userDetailsService.loadUserByUsername(username); + if (tokenService.validateToken(token, userDetails)) { + UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken( + userDetails, + null, + userDetails.getAuthorities()); + authToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); + SecurityContextHolder.getContext().setAuthentication(authToken); + } + } + filterChain.doFilter(request, response); + } +} diff --git a/src/main/java/com/example/nto/configuration/SecurityConfig.java b/src/main/java/com/example/nto/configuration/SecurityConfig.java new file mode 100644 index 0000000..ea336c8 --- /dev/null +++ b/src/main/java/com/example/nto/configuration/SecurityConfig.java @@ -0,0 +1,59 @@ +package com.example.nto.configuration; + +import com.example.nto.service.impl.EmployeeDetailsService; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.authentication.AuthenticationProvider; +import org.springframework.security.authentication.dao.DaoAuthenticationProvider; +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 SecurityConfig { + private final JwtAuthFilter jwtAuthFilter; + private final EmployeeDetailsService userDetailsService; + + public SecurityConfig(JwtAuthFilter jwtAuthFilter, EmployeeDetailsService userDetailsService) { + this.jwtAuthFilter = jwtAuthFilter; + this.userDetailsService = userDetailsService; + } + + @Bean + public SecurityFilterChain securityFilterChain(HttpSecurity httpRequest) throws Exception { + httpRequest.csrf(csrf -> csrf.disable()) + .authorizeHttpRequests(auth -> auth + .requestMatchers("/api/auth/register", "api/auth/login").permitAll() + .anyRequest().authenticated()) + .sessionManagement(sess -> sess.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) + .authenticationProvider(authenticationProvider()) + .addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter.class); + + return httpRequest.build(); + } + + @Bean + public AuthenticationProvider authenticationProvider() { + DaoAuthenticationProvider provider = new DaoAuthenticationProvider(); + provider.setUserDetailsService(userDetailsService); + provider.setPasswordEncoder(passwordEncoder()); + return provider; + } + + @Bean + public PasswordEncoder passwordEncoder() { + return new BCryptPasswordEncoder(); + } + + @Bean + public AuthenticationManager authenticationManager(AuthenticationConfiguration config) throws Exception { + return config.getAuthenticationManager(); + } +} diff --git a/src/main/java/com/example/nto/controller/AuthorizationController.java b/src/main/java/com/example/nto/controller/AuthorizationController.java new file mode 100644 index 0000000..98aaf13 --- /dev/null +++ b/src/main/java/com/example/nto/controller/AuthorizationController.java @@ -0,0 +1,31 @@ +package com.example.nto.controller; + +import com.example.nto.controller.dto.CreateAccountDto; +import com.example.nto.controller.dto.LoginDto; +import com.example.nto.service.AuthorizationService; +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/auth") +public class AuthorizationController { + + private final AuthorizationService authorizationService; + + public AuthorizationController(AuthorizationService authorizationService) { + this.authorizationService = authorizationService; + } + + @PostMapping("register") + public ResponseEntity register(@RequestBody CreateAccountDto dto) { + return ResponseEntity.status(201).body(authorizationService.createUser(dto)); + } + + @PostMapping("login") + public ResponseEntity login(@RequestBody LoginDto dto) { + return ResponseEntity.status(201).body(authorizationService.loginUser(dto)); + } +} diff --git a/src/main/java/com/example/nto/controller/BookingController.java b/src/main/java/com/example/nto/controller/BookingController.java index 7ae64a7..b406ec7 100644 --- a/src/main/java/com/example/nto/controller/BookingController.java +++ b/src/main/java/com/example/nto/controller/BookingController.java @@ -10,7 +10,7 @@ import lombok.RequiredArgsConstructor; import org.springframework.http.HttpStatus; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.*; - +/* @Validated @RestController @RequestMapping("api") @@ -31,3 +31,4 @@ public class BookingController { bookingService.create(code, bookingCreateDto); } } +*/ \ No newline at end of file diff --git a/src/main/java/com/example/nto/controller/EmployeeController.java b/src/main/java/com/example/nto/controller/EmployeeController.java index e913fe8..4d4c340 100644 --- a/src/main/java/com/example/nto/controller/EmployeeController.java +++ b/src/main/java/com/example/nto/controller/EmployeeController.java @@ -11,8 +11,9 @@ import org.springframework.web.bind.annotation.*; @RequiredArgsConstructor public class EmployeeController { - private final EmployeeService employeeService; + // private final EmployeeService employeeService; + /* @GetMapping("/{code}/auth") @ResponseStatus(code = HttpStatus.OK) public void login(@PathVariable String code) { @@ -24,4 +25,6 @@ public class EmployeeController { public EmployeeDto getByCode(@PathVariable String code) { return employeeService.getByCode(code); } + */ + } diff --git a/src/main/java/com/example/nto/controller/dto/CreateAccountDto.java b/src/main/java/com/example/nto/controller/dto/CreateAccountDto.java new file mode 100644 index 0000000..80dedba --- /dev/null +++ b/src/main/java/com/example/nto/controller/dto/CreateAccountDto.java @@ -0,0 +1,14 @@ +package com.example.nto.controller.dto; + +import lombok.Builder; +import lombok.Data; +import lombok.RequiredArgsConstructor; + +@Data +@Builder +// @RequiredArgsConstructor +public class CreateAccountDto { + private String username; + private String password; + private String deviceName; +} diff --git a/src/main/java/com/example/nto/controller/dto/LoginDto.java b/src/main/java/com/example/nto/controller/dto/LoginDto.java new file mode 100644 index 0000000..d304331 --- /dev/null +++ b/src/main/java/com/example/nto/controller/dto/LoginDto.java @@ -0,0 +1,9 @@ +package com.example.nto.controller.dto; + +import lombok.Data; + +@Data +public class LoginDto { + private String username; + private String password; +} diff --git a/src/main/java/com/example/nto/entity/Employee.java b/src/main/java/com/example/nto/entity/Employee.java index f0c4411..a2050d5 100644 --- a/src/main/java/com/example/nto/entity/Employee.java +++ b/src/main/java/com/example/nto/entity/Employee.java @@ -1,11 +1,19 @@ package com.example.nto.entity; import jakarta.persistence.*; + +import java.util.ArrayList; +import java.util.Collection; import java.util.List; +import java.util.Set; + import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; import lombok.NoArgsConstructor; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.core.userdetails.UserDetails; @Data @Entity @@ -13,7 +21,7 @@ import lombok.NoArgsConstructor; @NoArgsConstructor @AllArgsConstructor @Table(name = "employee") -public class Employee { +public class Employee implements UserDetails { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) @@ -22,12 +30,33 @@ public class Employee { @Column(name = "name") private String name; - @Column(name = "code") - private String code; + @Column(name = "password") + private String password; @Column(name = "photo_url") private String photoUrl; + // @Enumerated + @Column(name = "roles") + private Set roles; + + @OneToMany(mappedBy="employee") + private Set sessions; + @OneToMany(mappedBy = "employee", cascade = CascadeType.ALL, fetch = FetchType.LAZY) private List bookingList; + + @Override + public Collection getAuthorities() { + List authorities = new ArrayList<>(); + for (String s : roles) { + authorities.add(new SimpleGrantedAuthority(s)); + } + return authorities; + } + + @Override + public String getUsername() { + return name; + } } diff --git a/src/main/java/com/example/nto/entity/Session.java b/src/main/java/com/example/nto/entity/Session.java new file mode 100644 index 0000000..bf752e6 --- /dev/null +++ b/src/main/java/com/example/nto/entity/Session.java @@ -0,0 +1,29 @@ +package com.example.nto.entity; + +import jakarta.persistence.*; +import lombok.Data; + +import java.util.Date; + +@Entity +@Table(name = "sessions") +@Data +public class Session { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private long id; + + private boolean isExpired = false; + + private String refreshToken; + + private String deviceName; + + @ManyToOne + @JoinColumn(name="employee_id", nullable=false) + private Employee employee; + + private Date createdAt; + + private Date updatedAt; +} diff --git a/src/main/java/com/example/nto/exception/DataValidationException.java b/src/main/java/com/example/nto/exception/DataValidationException.java new file mode 100644 index 0000000..e38de5a --- /dev/null +++ b/src/main/java/com/example/nto/exception/DataValidationException.java @@ -0,0 +1,7 @@ +package com.example.nto.exception; + +public class DataValidationException extends RuntimeException { + public DataValidationException(String message) { + super(message); + } +} diff --git a/src/main/java/com/example/nto/exception/WrongPasswordException.java b/src/main/java/com/example/nto/exception/WrongPasswordException.java new file mode 100644 index 0000000..1486301 --- /dev/null +++ b/src/main/java/com/example/nto/exception/WrongPasswordException.java @@ -0,0 +1,7 @@ +package com.example.nto.exception; + +public class WrongPasswordException extends RuntimeException { + public WrongPasswordException(String message) { + super(message); + } +} diff --git a/src/main/java/com/example/nto/exception/handler/GlobalExceptionHandler.java b/src/main/java/com/example/nto/exception/handler/GlobalExceptionHandler.java index 1e605cc..e43f48a 100644 --- a/src/main/java/com/example/nto/exception/handler/GlobalExceptionHandler.java +++ b/src/main/java/com/example/nto/exception/handler/GlobalExceptionHandler.java @@ -1,8 +1,6 @@ package com.example.nto.exception.handler; -import com.example.nto.exception.BookingAlreadyExistsException; -import com.example.nto.exception.EmployeeNotFoundException; -import com.example.nto.exception.PlaceNotFoundException; +import com.example.nto.exception.*; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.ControllerAdvice; @@ -30,4 +28,14 @@ public class GlobalExceptionHandler { public ResponseEntity handleGenericException(Exception e) { return new ResponseEntity<>(e.getMessage(), HttpStatus.BAD_REQUEST); } + + @ExceptionHandler(DataValidationException.class) + public ResponseEntity handleDataValidationException(DataValidationException exception) { + return new ResponseEntity<>(exception.getMessage(), HttpStatus.UNPROCESSABLE_ENTITY); + } + + @ExceptionHandler(WrongPasswordException.class) + public ResponseEntity handleWrongPassword(WrongPasswordException exception) { + return new ResponseEntity<>(exception.getMessage(), HttpStatus.UNAUTHORIZED); + } } diff --git a/src/main/java/com/example/nto/repository/EmployeeRepository.java b/src/main/java/com/example/nto/repository/EmployeeRepository.java index 2ba1c6a..931192e 100644 --- a/src/main/java/com/example/nto/repository/EmployeeRepository.java +++ b/src/main/java/com/example/nto/repository/EmployeeRepository.java @@ -4,8 +4,15 @@ import com.example.nto.entity.Employee; import java.util.Optional; import org.springframework.data.jpa.repository.EntityGraph; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; +@Repository public interface EmployeeRepository extends JpaRepository { - @EntityGraph(attributePaths = {"bookingList", "bookingList.place"}) - Optional findByCode(String code); + + //Optional findByCode(String code); + + // @EntityGraph(attributePaths = {"bookingList", "bookingList.place"}) + Optional findByName(String name); + + // Optional findBy(); } diff --git a/src/main/java/com/example/nto/repository/SessionRepository.java b/src/main/java/com/example/nto/repository/SessionRepository.java new file mode 100644 index 0000000..d030add --- /dev/null +++ b/src/main/java/com/example/nto/repository/SessionRepository.java @@ -0,0 +1,18 @@ +package com.example.nto.repository; + +import com.example.nto.entity.Employee; +import com.example.nto.entity.Session; +import org.springframework.data.jpa.repository.EntityGraph; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import java.util.Optional; + +@Repository +public interface SessionRepository extends JpaRepository { + + //Optional findByCode(String code); + + //@EntityGraph(attributePaths = {"bookingList", "bookingList.place"}) + //Optional findByName(String name); +} diff --git a/src/main/java/com/example/nto/service/AuthorizationService.java b/src/main/java/com/example/nto/service/AuthorizationService.java new file mode 100644 index 0000000..ddc13c2 --- /dev/null +++ b/src/main/java/com/example/nto/service/AuthorizationService.java @@ -0,0 +1,9 @@ +package com.example.nto.service; + +import com.example.nto.controller.dto.CreateAccountDto; +import com.example.nto.controller.dto.LoginDto; + +public interface AuthorizationService { + public Object createUser(CreateAccountDto request); + public Object loginUser(LoginDto request); +} diff --git a/src/main/java/com/example/nto/service/impl/AuthorizationServiceImpl.java b/src/main/java/com/example/nto/service/impl/AuthorizationServiceImpl.java new file mode 100644 index 0000000..8e82dc4 --- /dev/null +++ b/src/main/java/com/example/nto/service/impl/AuthorizationServiceImpl.java @@ -0,0 +1,116 @@ +package com.example.nto.service.impl; + +import com.example.nto.controller.dto.CreateAccountDto; +import com.example.nto.controller.dto.LoginDto; +import com.example.nto.entity.Employee; +import com.example.nto.entity.Session; +import com.example.nto.exception.DataValidationException; +import com.example.nto.exception.WrongPasswordException; +import com.example.nto.repository.EmployeeRepository; +import com.example.nto.repository.SessionRepository; +import com.example.nto.service.AuthorizationService; +import com.example.nto.validation.PasswordWithKnownUsernameValidatorService; +import org.antlr.v4.runtime.misc.Pair; +import org.springframework.security.core.userdetails.UsernameNotFoundException; +import org.springframework.stereotype.Service; + +import java.sql.Date; +import java.time.Instant; +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; + +@Service +public class AuthorizationServiceImpl implements AuthorizationService { + + private final PasswordWithKnownUsernameValidatorService validatorService; + private final EmployeeRepository employeeRepository; + private final SessionRepository sessionRepository; + private final TokenService tokenService; + + + public AuthorizationServiceImpl(PasswordWithKnownUsernameValidatorService validatorService, EmployeeRepository employeeRepository, SessionRepository sessionRepository, TokenService tokenService) { + this.validatorService = validatorService; + this.employeeRepository = employeeRepository; + this.sessionRepository = sessionRepository; + this.tokenService = tokenService; + } + + @Override + public Object createUser(CreateAccountDto request) { + Pair validationResult = validatorService.isCorrect(request.getUsername(), request.getPassword()); + if (validationResult.a == false) { + throw new DataValidationException(validationResult.b); + } + + Employee employee = new Employee(); + employee.setName(request.getUsername()); + employee.setPassword(request.getPassword()); + + Employee emp = employeeRepository.save(employee); + + Session session = new Session(); + String refreshToken = tokenService.createRefreshToken(); + session.setRefreshToken(refreshToken); + session.setCreatedAt(Date.from(Instant.now())); + session.setUpdatedAt(Date.from(Instant.now())); + session.setEmployee(emp); + session.setDeviceName(request.getDeviceName()); + + sessionRepository.save(session); + + String accessToken = tokenService.generateToken(request.getUsername()); + + Map json = new HashMap<>(); + json.put("access_token", accessToken); + json.put("access_token_ttl", 600); + json.put("refresh_token", refreshToken); + + Map e = new HashMap<>(); + e.put("id", emp.getId()); + e.put("name", emp.getName()); + e.put("photo_url", emp.getPhotoUrl()); + e.put("booking_list", emp.getBookingList()); + + json.put("employee", e); + + return json; + } + + @Override + public Object loginUser(LoginDto dto) { + Optional employee = employeeRepository.findByName(dto.getUsername()); + if (employee.isEmpty()) { + throw new UsernameNotFoundException("No such user"); + } + + if (!Objects.equals(employee.get().getPassword(), dto.getPassword())) { + throw new WrongPasswordException("Error: wrong password"); + } + + Session session = new Session(); + session.setExpired(false); + String refreshToken = tokenService.createRefreshToken(); + session.setRefreshToken(refreshToken); + session.setCreatedAt(Date.from(Instant.now())); + session.setUpdatedAt(Date.from(Instant.now())); + session.setEmployee(employee.get()); + String accessToken = tokenService.generateToken(dto.getUsername()); + + Map json = new HashMap<>(); + json.put("access_token", accessToken); + json.put("access_token_ttl", 600); + json.put("refresh_token", refreshToken); + + Map emp = new HashMap<>(); + emp.put("id", employee.get().getId()); + emp.put("name", employee.get().getName()); + emp.put("photo_url", employee.get().getPhotoUrl()); + emp.put("booking_list", employee.get().getBookingList()); + + json.put("employee", emp); + + return json; + } +} diff --git a/src/main/java/com/example/nto/service/impl/BookingServiceImpl.java b/src/main/java/com/example/nto/service/impl/BookingServiceImpl.java index 8c5bccd..8949a6c 100644 --- a/src/main/java/com/example/nto/service/impl/BookingServiceImpl.java +++ b/src/main/java/com/example/nto/service/impl/BookingServiceImpl.java @@ -22,6 +22,7 @@ import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +/* @Service @RequiredArgsConstructor public class BookingServiceImpl implements BookingService { @@ -112,3 +113,4 @@ public class BookingServiceImpl implements BookingService { return bookingRepository.save(booking); } } +*/ \ No newline at end of file diff --git a/src/main/java/com/example/nto/service/impl/EmployeeDetailsService.java b/src/main/java/com/example/nto/service/impl/EmployeeDetailsService.java new file mode 100644 index 0000000..dc96e33 --- /dev/null +++ b/src/main/java/com/example/nto/service/impl/EmployeeDetailsService.java @@ -0,0 +1,30 @@ +package com.example.nto.service.impl; + +import com.example.nto.entity.Employee; +import com.example.nto.repository.EmployeeRepository; +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 java.util.Optional; + +@Service +public class EmployeeDetailsService implements UserDetailsService { + + private final EmployeeRepository employeeRepository; + + public EmployeeDetailsService(EmployeeRepository employeeRepository) { + this.employeeRepository = employeeRepository; + } + + @Override + public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { + Optional employee = employeeRepository.findByName(username); + if (employee.isEmpty()) { + throw new UsernameNotFoundException("User not found with username: " + username); + } + + return employee.get(); + } +} diff --git a/src/main/java/com/example/nto/service/impl/EmployeeServiceImpl.java b/src/main/java/com/example/nto/service/impl/EmployeeServiceImpl.java index fcb6882..2468f54 100644 --- a/src/main/java/com/example/nto/service/impl/EmployeeServiceImpl.java +++ b/src/main/java/com/example/nto/service/impl/EmployeeServiceImpl.java @@ -7,7 +7,7 @@ import com.example.nto.service.EmployeeService; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; - +/* @Service @RequiredArgsConstructor public class EmployeeServiceImpl implements EmployeeService { @@ -34,3 +34,4 @@ public class EmployeeServiceImpl implements EmployeeService { } } } +*/ \ No newline at end of file diff --git a/src/main/java/com/example/nto/service/impl/TokenService.java b/src/main/java/com/example/nto/service/impl/TokenService.java new file mode 100644 index 0000000..b90f208 --- /dev/null +++ b/src/main/java/com/example/nto/service/impl/TokenService.java @@ -0,0 +1,84 @@ +package com.example.nto.service.impl; + +import io.jsonwebtoken.Claims; +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.SignatureAlgorithm; +import io.jsonwebtoken.io.Decoders; +import io.jsonwebtoken.security.Keys; +import org.apache.commons.lang3.RandomStringUtils; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.stereotype.Component; + +import java.security.Key; +import java.time.Instant; +import java.time.temporal.ChronoUnit; +import java.util.Date; +import java.util.HashMap; +import java.util.Map; +import java.util.function.Function; + +@Component +public class TokenService { + + private final String SECRET; + + public TokenService(@Value("${jwt-secret}") String secret) { + SECRET = secret; + } + + public String generateToken(String email) { + Map claims = new HashMap<>(); + return createToken(claims, email); + } + + private String createToken(Map claims, String username) { + return Jwts.builder() + .setClaims(claims) + .setSubject(username) + .setIssuedAt(Date.from(Instant.now())) + .setExpiration(Date.from(Instant.now().plus(10, ChronoUnit.MINUTES))) // todo: change + .signWith(getSignKey(), SignatureAlgorithm.HS256) + .compact(); + } + + public String createRefreshToken() { + return RandomStringUtils.randomAlphanumeric(64); + } + + + public T extractClaim(String token, Function claimsResolver) { + final Claims claims = extractAllClaims(token); + return claimsResolver.apply(claims); + } + + public Date extractExpiration(String token) { + return extractClaim(token, Claims::getExpiration); + } + + private Boolean isTokenExpired(String token) { + return extractExpiration(token).before(new Date()); + } + + public String extractUsername(String token) { + return extractClaim(token, Claims::getSubject); + } + + public Boolean validateToken(String token, UserDetails userDetails) { + final String username = extractUsername(token); + return (username.equals(userDetails.getUsername()) && !isTokenExpired(token)); + } + + public Claims extractAllClaims(String token) { + return Jwts.parserBuilder() + .setSigningKey(getSignKey()) + .build() + .parseClaimsJws(token) + .getBody(); + } + + private Key getSignKey() { + byte[] keyBytes = Decoders.BASE64.decode(SECRET); + return Keys.hmacShaKeyFor(keyBytes); + } +} diff --git a/src/main/java/com/example/nto/validation/PasswordWithKnownUsernameValidatorService.java b/src/main/java/com/example/nto/validation/PasswordWithKnownUsernameValidatorService.java new file mode 100644 index 0000000..8fcc777 --- /dev/null +++ b/src/main/java/com/example/nto/validation/PasswordWithKnownUsernameValidatorService.java @@ -0,0 +1,107 @@ +package com.example.nto.validation; + +import org.antlr.v4.runtime.misc.Pair; +import org.springframework.stereotype.Service; + +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +@Service +public class PasswordWithKnownUsernameValidatorService { + + /** + * Checks if username and password are correctly validated. + * @param username + * @param password + * @return Pair[Bool, String], if Bool is true String is empty + */ + public Pair isCorrect(String username, String password) { + if (username == null) { + return new Pair<>(false, "No username provided"); + } + if (password == null) { + return new Pair<>(false, "No password provided"); + } + if (username.isEmpty()) { + return new Pair<>(false, "Empty username provided"); + } + + //String rgx = "[^A-Za-z0-9.]"; + //if (!username.matches(rgx)) { + //return new Pair<>(false, "Username does not match regex."); + //} + + var check = new boolean[username.length()]; + for (int i = 0; i < username.length(); i++) { + if (username.charAt(i) == '.') { + check[i] = true; + } + if (username.charAt(i) >= 65 && username.charAt(i) <= 90) { + check[i] = true; + } + if (username.charAt(i) >= 97 && username.charAt(i) <= 122) { + check[i] = true; + } + if (username.charAt(i) >= 48 && username.charAt(i) <= 57) { + check[i] = true; + } + } + + for (int i = 0; i < username.length(); i++) { + if (!check[i]) { + return new Pair<>(false, "Username does not match regex."); + } + } + + if (password.length() < 8) { + return new Pair<>(false, "Password is too short."); + } + + Map freq = new HashMap<>(); + for (int i = 0; i < password.length(); i++) { + char c = password.charAt(i); + Integer fr = freq.get(c); + if (fr == null) { + freq.put(c, 1); + } else { + freq.put(c, fr + 1); + } + } + + for (Integer count : freq.values()) { + if (count >= 3) { + return new Pair<>(false, "The same character must not appear 3 or more times."); + } + } + + String specialSymbols = "!#$%&'()*+,-./:;<=>?@[]^_`{|}~"; + boolean containsSpecials = false; + for (int i = 0; i < password.length(); i++) { + for (int j = 0; j < specialSymbols.length(); j++) { + // их не так много чтобы заморачиваться с Set + if (password.charAt(i) == specialSymbols.charAt(j)) { + containsSpecials = true; + break; + } + } + } + + if (!containsSpecials) { + return new Pair<>(false, "Password should contain one of these special characters: !#$%&'()*+,-./:;<=>?@[]^_`{|}~"); + } + + String passwordLowerCase = password.toLowerCase(); + String usernameLowerCase = username.toLowerCase(); + for (int i = 0; i < passwordLowerCase.length() - 2; i++) { + for (int j = 0; j < usernameLowerCase.length() - 2; j++) { + if (usernameLowerCase.substring(i, i + 2).equals(passwordLowerCase.substring(j, j + 2))) { + return new Pair<>(false, "Password must not contain 3 or more continuous characters from provided username"); + } + } + } + + return new Pair<>(true, ""); + } +} diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 8b68191..b57ffec 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -10,7 +10,7 @@ spring: generate-ddl: false hibernate: - ddl-auto: none + ddl-auto: create-drop show-sql: true @@ -19,4 +19,6 @@ spring: change-log: classpath:db.changelog/db.changelog-master.xml booking: - days-ahead: 3 \ No newline at end of file + days-ahead: 3 + +jwt-secret: FR2TQQ2ZhVTiVR5GuSN4ULBKm8tPiCB9lTZtUxldbUG diff --git a/src/main/resources/db.changelog/1/0/2025-11-05--0001-employee.xml b/src/main/resources/db.changelog/1/0/2025-11-05--0001-employee.xml deleted file mode 100644 index d1f92f8..0000000 --- a/src/main/resources/db.changelog/1/0/2025-11-05--0001-employee.xml +++ /dev/null @@ -1,32 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/src/main/resources/db.changelog/1/0/2025-11-05--0002-place.xml b/src/main/resources/db.changelog/1/0/2025-11-05--0002-place.xml deleted file mode 100644 index db4a2b2..0000000 --- a/src/main/resources/db.changelog/1/0/2025-11-05--0002-place.xml +++ /dev/null @@ -1,27 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/src/main/resources/db.changelog/1/0/2025-11-05--0003-booking.xml b/src/main/resources/db.changelog/1/0/2025-11-05--0003-booking.xml deleted file mode 100644 index fa62dce..0000000 --- a/src/main/resources/db.changelog/1/0/2025-11-05--0003-booking.xml +++ /dev/null @@ -1,36 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/src/main/resources/db.changelog/data/2025-11-05--0001-employee-data.xml b/src/main/resources/db.changelog/data/2025-11-05--0001-employee-data.xml deleted file mode 100644 index 40f8ddb..0000000 --- a/src/main/resources/db.changelog/data/2025-11-05--0001-employee-data.xml +++ /dev/null @@ -1,14 +0,0 @@ - - - - - - - \ No newline at end of file diff --git a/src/main/resources/db.changelog/data/2025-11-05--0002-place-data.xml b/src/main/resources/db.changelog/data/2025-11-05--0002-place-data.xml deleted file mode 100644 index e5351dd..0000000 --- a/src/main/resources/db.changelog/data/2025-11-05--0002-place-data.xml +++ /dev/null @@ -1,14 +0,0 @@ - - - - - - - \ No newline at end of file diff --git a/src/main/resources/db.changelog/data/2025-11-05--0003-booking-data.xml b/src/main/resources/db.changelog/data/2025-11-05--0003-booking-data.xml deleted file mode 100644 index eea0c6b..0000000 --- a/src/main/resources/db.changelog/data/2025-11-05--0003-booking-data.xml +++ /dev/null @@ -1,14 +0,0 @@ - - - - - - - \ No newline at end of file diff --git a/src/main/resources/db.changelog/data/csv/2025-11-05--0001-employee-data.csv b/src/main/resources/db.changelog/data/csv/2025-11-05--0001-employee-data.csv deleted file mode 100644 index 87ddc6b..0000000 --- a/src/main/resources/db.changelog/data/csv/2025-11-05--0001-employee-data.csv +++ /dev/null @@ -1,5 +0,0 @@ -name;code;photo_url -Ivanov Ivan;1111;https://catalog-cdn.detmir.st/media/2fe02057f9915e72a378795d32c79ea9.jpeg -Petrov Petr;2222;https://catalog-cdn.detmir.st/media/2fe02057f9915e72a378795d32c79ea9.jpeg -Kozlov Oleg;3333;https://catalog-cdn.detmir.st/media/2fe02057f9915e72a378795d32c79ea9.jpeg -Smirnova Anna;4444;https://catalog-cdn.detmir.st/media/2fe02057f9915e72a378795d32c79ea9.jpeg \ No newline at end of file diff --git a/src/main/resources/db.changelog/data/csv/2025-11-05--0002-place-data.csv b/src/main/resources/db.changelog/data/csv/2025-11-05--0002-place-data.csv deleted file mode 100644 index 3354529..0000000 --- a/src/main/resources/db.changelog/data/csv/2025-11-05--0002-place-data.csv +++ /dev/null @@ -1,4 +0,0 @@ -place_name -K-19 -M-16 -T-1 \ No newline at end of file diff --git a/src/main/resources/db.changelog/data/csv/2025-11-05--0003-booking-data.csv b/src/main/resources/db.changelog/data/csv/2025-11-05--0003-booking-data.csv deleted file mode 100644 index 11f0364..0000000 --- a/src/main/resources/db.changelog/data/csv/2025-11-05--0003-booking-data.csv +++ /dev/null @@ -1,3 +0,0 @@ -date;place_id;employee_id -2025-11-08;1;1 -2025-11-10;2;2 \ No newline at end of file diff --git a/src/main/resources/db.changelog/db.changelog-master.xml b/src/main/resources/db.changelog/db.changelog-master.xml deleted file mode 100644 index 90031f0..0000000 --- a/src/main/resources/db.changelog/db.changelog-master.xml +++ /dev/null @@ -1,14 +0,0 @@ - - - - - - - - - - - diff --git a/stuff.md b/stuff.md index b0bbb0d..ab50b02 100644 --- a/stuff.md +++ b/stuff.md @@ -1,4 +1,6 @@ Command to build fat jar: ```bash mvn clean package spring-boot:repackage -``` \ No newline at end of file +``` + +todo: карсиво сделать через аннотацию валидацию всего кроме зависящих юзернейма и пароля (ласт требование)