[FEATURE] OAUTH Authentication with access and refresh tokens. Sessions service
This commit is contained in:
parent
e90f4768ec
commit
e8e7d01ec0
29
docker-compose.yml
Normal file
29
docker-compose.yml
Normal file
@ -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:
|
||||
24
pom.xml
24
pom.xml
@ -61,6 +61,10 @@
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-web</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-security</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.h2database</groupId>
|
||||
<artifactId>h2</artifactId>
|
||||
@ -88,6 +92,26 @@
|
||||
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
|
||||
<version>2.8.8</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.jsonwebtoken</groupId>
|
||||
<artifactId>jjwt-api</artifactId>
|
||||
<version>0.11.5</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.jsonwebtoken</groupId>
|
||||
<artifactId>jjwt-impl</artifactId>
|
||||
<version>0.11.5</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.jsonwebtoken</groupId>
|
||||
<artifactId>jjwt-jackson</artifactId>
|
||||
<version>0.11.5</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.google.guava</groupId>
|
||||
<artifactId>guava</artifactId>
|
||||
<version>32.1.3-jre</version>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
</project>
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
@ -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();
|
||||
}
|
||||
}
|
||||
@ -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));
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
*/
|
||||
@ -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);
|
||||
}
|
||||
*/
|
||||
|
||||
}
|
||||
|
||||
@ -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;
|
||||
}
|
||||
@ -0,0 +1,9 @@
|
||||
package com.example.nto.controller.dto;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
public class LoginDto {
|
||||
private String username;
|
||||
private String password;
|
||||
}
|
||||
@ -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<String> roles;
|
||||
|
||||
@OneToMany(mappedBy="employee")
|
||||
private Set<Session> sessions;
|
||||
|
||||
@OneToMany(mappedBy = "employee", cascade = CascadeType.ALL, fetch = FetchType.LAZY)
|
||||
private List<Booking> bookingList;
|
||||
|
||||
@Override
|
||||
public Collection<? extends GrantedAuthority> getAuthorities() {
|
||||
List<SimpleGrantedAuthority> authorities = new ArrayList<>();
|
||||
for (String s : roles) {
|
||||
authorities.add(new SimpleGrantedAuthority(s));
|
||||
}
|
||||
return authorities;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getUsername() {
|
||||
return name;
|
||||
}
|
||||
}
|
||||
|
||||
29
src/main/java/com/example/nto/entity/Session.java
Normal file
29
src/main/java/com/example/nto/entity/Session.java
Normal file
@ -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;
|
||||
}
|
||||
@ -0,0 +1,7 @@
|
||||
package com.example.nto.exception;
|
||||
|
||||
public class DataValidationException extends RuntimeException {
|
||||
public DataValidationException(String message) {
|
||||
super(message);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,7 @@
|
||||
package com.example.nto.exception;
|
||||
|
||||
public class WrongPasswordException extends RuntimeException {
|
||||
public WrongPasswordException(String message) {
|
||||
super(message);
|
||||
}
|
||||
}
|
||||
@ -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<String> handleGenericException(Exception e) {
|
||||
return new ResponseEntity<>(e.getMessage(), HttpStatus.BAD_REQUEST);
|
||||
}
|
||||
|
||||
@ExceptionHandler(DataValidationException.class)
|
||||
public ResponseEntity<String> handleDataValidationException(DataValidationException exception) {
|
||||
return new ResponseEntity<>(exception.getMessage(), HttpStatus.UNPROCESSABLE_ENTITY);
|
||||
}
|
||||
|
||||
@ExceptionHandler(WrongPasswordException.class)
|
||||
public ResponseEntity<String> handleWrongPassword(WrongPasswordException exception) {
|
||||
return new ResponseEntity<>(exception.getMessage(), HttpStatus.UNAUTHORIZED);
|
||||
}
|
||||
}
|
||||
|
||||
@ -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<Employee, Long> {
|
||||
@EntityGraph(attributePaths = {"bookingList", "bookingList.place"})
|
||||
Optional<Employee> findByCode(String code);
|
||||
|
||||
//Optional<Employee> findByCode(String code);
|
||||
|
||||
// @EntityGraph(attributePaths = {"bookingList", "bookingList.place"})
|
||||
Optional<Employee> findByName(String name);
|
||||
|
||||
// Optional<Employee> findBy();
|
||||
}
|
||||
|
||||
@ -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<Session, Long> {
|
||||
|
||||
//Optional<Employee> findByCode(String code);
|
||||
|
||||
//@EntityGraph(attributePaths = {"bookingList", "bookingList.place"})
|
||||
//Optional<Employee> findByName(String name);
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
@ -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<Boolean, String> 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<String, Object> json = new HashMap<>();
|
||||
json.put("access_token", accessToken);
|
||||
json.put("access_token_ttl", 600);
|
||||
json.put("refresh_token", refreshToken);
|
||||
|
||||
Map<String, Object> 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> 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<String, Object> json = new HashMap<>();
|
||||
json.put("access_token", accessToken);
|
||||
json.put("access_token_ttl", 600);
|
||||
json.put("refresh_token", refreshToken);
|
||||
|
||||
Map<String, Object> 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;
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
*/
|
||||
@ -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> employee = employeeRepository.findByName(username);
|
||||
if (employee.isEmpty()) {
|
||||
throw new UsernameNotFoundException("User not found with username: " + username);
|
||||
}
|
||||
|
||||
return employee.get();
|
||||
}
|
||||
}
|
||||
@ -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 {
|
||||
}
|
||||
}
|
||||
}
|
||||
*/
|
||||
84
src/main/java/com/example/nto/service/impl/TokenService.java
Normal file
84
src/main/java/com/example/nto/service/impl/TokenService.java
Normal file
@ -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<String, Object> claims = new HashMap<>();
|
||||
return createToken(claims, email);
|
||||
}
|
||||
|
||||
private String createToken(Map<String, Object> 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> T extractClaim(String token, Function<Claims, T> 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);
|
||||
}
|
||||
}
|
||||
@ -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<Boolean, String> 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<Character, Integer> 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, "");
|
||||
}
|
||||
}
|
||||
@ -10,7 +10,7 @@ spring:
|
||||
generate-ddl: false
|
||||
|
||||
hibernate:
|
||||
ddl-auto: none
|
||||
ddl-auto: create-drop
|
||||
|
||||
show-sql: true
|
||||
|
||||
@ -20,3 +20,5 @@ spring:
|
||||
|
||||
booking:
|
||||
days-ahead: 3
|
||||
|
||||
jwt-secret: FR2TQQ2ZhVTiVR5GuSN4ULBKm8tPiCB9lTZtUxldbUG
|
||||
|
||||
@ -1,32 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xmlns:ext="http://www.liquibase.org/xml/ns/dbchangelog-ext"
|
||||
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.0.xsd
|
||||
http://www.liquibase.org/xml/ns/dbchangelog-ext http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-ext.xsd">
|
||||
|
||||
<changeSet id="2025-11-05--0001-employee" author="anepretimov">
|
||||
<preConditions onFail="MARK_RAN">
|
||||
<not>
|
||||
<tableExists tableName="employee"/>
|
||||
</not>
|
||||
</preConditions>
|
||||
|
||||
<createTable tableName="employee">
|
||||
<column name="id" type="BIGINT" autoIncrement="true">
|
||||
<constraints primaryKey="true" nullable="false"/>
|
||||
</column>
|
||||
|
||||
<column name="name" type="VARCHAR(100)">
|
||||
<constraints nullable="false"/>
|
||||
</column>
|
||||
|
||||
<column name="code" type="VARCHAR(100)">
|
||||
<constraints nullable="false" unique="true"/>
|
||||
</column>
|
||||
|
||||
<column name="photo_url" type="VARCHAR(100)"/>
|
||||
</createTable>
|
||||
</changeSet>
|
||||
|
||||
</databaseChangeLog>
|
||||
@ -1,27 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xmlns:ext="http://www.liquibase.org/xml/ns/dbchangelog-ext"
|
||||
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.0.xsd
|
||||
http://www.liquibase.org/xml/ns/dbchangelog-ext http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-ext.xsd">
|
||||
|
||||
<changeSet id="2025-11-05--0002-place" author="anepretimov">
|
||||
<preConditions onFail="MARK_RAN">
|
||||
<not>
|
||||
<tableExists tableName="place"/>
|
||||
</not>
|
||||
</preConditions>
|
||||
|
||||
<createTable tableName="place">
|
||||
<column name="id" type="BIGINT" autoIncrement="true">
|
||||
<constraints primaryKey="true" nullable="false"/>
|
||||
</column>
|
||||
|
||||
<column name="place_name" type="VARCHAR(100)">
|
||||
<constraints nullable="false" unique="true"/>
|
||||
</column>
|
||||
</createTable>
|
||||
</changeSet>
|
||||
|
||||
|
||||
</databaseChangeLog>
|
||||
@ -1,36 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xmlns:ext="http://www.liquibase.org/xml/ns/dbchangelog-ext"
|
||||
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.0.xsd
|
||||
http://www.liquibase.org/xml/ns/dbchangelog-ext http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-ext.xsd">
|
||||
|
||||
<changeSet id="2025-11-05--0003-booking" author="anepretimov">
|
||||
<preConditions onFail="MARK_RAN">
|
||||
<not>
|
||||
<tableExists tableName="booking"/>
|
||||
</not>
|
||||
</preConditions>
|
||||
|
||||
<createTable tableName="booking">
|
||||
<column name="id" type="BIGINT" autoIncrement="true">
|
||||
<constraints primaryKey="true" nullable="false"/>
|
||||
</column>
|
||||
|
||||
<column name="date" type="DATE">
|
||||
<constraints nullable="false"/>
|
||||
</column>
|
||||
|
||||
<column name="employee_id" type="BIGINT">
|
||||
<constraints nullable="false" foreignKeyName="fk_booking_employee" referencedTableName="employee"
|
||||
referencedColumnNames="id"/>
|
||||
</column>
|
||||
|
||||
<column name="place_id" type="BIGINT">
|
||||
<constraints nullable="false" foreignKeyName="fk_booking_place" referencedTableName="place"
|
||||
referencedColumnNames="id"/>
|
||||
</column>
|
||||
</createTable>
|
||||
</changeSet>
|
||||
|
||||
</databaseChangeLog>
|
||||
@ -1,14 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xmlns:ext="http://www.liquibase.org/xml/ns/dbchangelog-ext"
|
||||
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.0.xsd
|
||||
http://www.liquibase.org/xml/ns/dbchangelog-ext http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-ext.xsd">
|
||||
|
||||
<changeSet id="2025-11-05--0001-employee-data" author="anepretimov">
|
||||
<loadData tableName="employee" file="db.changelog/data/csv/2025-11-05--0001-employee-data.csv"
|
||||
separator=";"
|
||||
quotchar='"'
|
||||
encoding="UTF-8"/>
|
||||
</changeSet>
|
||||
</databaseChangeLog>
|
||||
@ -1,14 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xmlns:ext="http://www.liquibase.org/xml/ns/dbchangelog-ext"
|
||||
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.0.xsd
|
||||
http://www.liquibase.org/xml/ns/dbchangelog-ext http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-ext.xsd">
|
||||
|
||||
<changeSet id="2025-11-05--0002-place-data" author="anepretimov">
|
||||
<loadData tableName="place" file="db.changelog/data/csv/2025-11-05--0002-place-data.csv"
|
||||
separator=";"
|
||||
quotchar='"'
|
||||
encoding="UTF-8"/>
|
||||
</changeSet>
|
||||
</databaseChangeLog>
|
||||
@ -1,14 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xmlns:ext="http://www.liquibase.org/xml/ns/dbchangelog-ext"
|
||||
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.0.xsd
|
||||
http://www.liquibase.org/xml/ns/dbchangelog-ext http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-ext.xsd">
|
||||
|
||||
<changeSet id="2025-11-05--0003-booking-data" author="anepretimov">
|
||||
<loadData tableName="booking" file="db.changelog/data/csv/2025-11-05--0003-booking-data.csv"
|
||||
separator=";"
|
||||
quotchar='"'
|
||||
encoding="UTF-8"/>
|
||||
</changeSet>
|
||||
</databaseChangeLog>
|
||||
@ -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
|
||||
|
@ -1,4 +0,0 @@
|
||||
place_name
|
||||
K-19
|
||||
M-16
|
||||
T-1
|
||||
|
@ -1,3 +0,0 @@
|
||||
date;place_id;employee_id
|
||||
2025-11-08;1;1
|
||||
2025-11-10;2;2
|
||||
|
@ -1,14 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog
|
||||
http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.8.xsd">
|
||||
|
||||
<include file="db.changelog/1/0/2025-11-05--0001-employee.xml"/>
|
||||
<include file="db.changelog/1/0/2025-11-05--0002-place.xml"/>
|
||||
<include file="db.changelog/1/0/2025-11-05--0003-booking.xml"/>
|
||||
|
||||
<include file="db.changelog/data/2025-11-05--0001-employee-data.xml"/>
|
||||
<include file="db.changelog/data/2025-11-05--0002-place-data.xml"/>
|
||||
<include file="db.changelog/data/2025-11-05--0003-booking-data.xml"/>
|
||||
</databaseChangeLog>
|
||||
Loading…
x
Reference in New Issue
Block a user