[FEATURE] OAUTH Authentication with access and refresh tokens. Sessions service

This commit is contained in:
maksim 2026-02-25 12:39:35 +03:00
parent e90f4768ec
commit e8e7d01ec0
35 changed files with 695 additions and 177 deletions

29
docker-compose.yml Normal file
View 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
View File

@ -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>

View File

@ -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);
}
}

View File

@ -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();
}
}

View File

@ -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));
}
}

View File

@ -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);
}
}
*/

View File

@ -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);
}
*/
}

View File

@ -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;
}

View File

@ -0,0 +1,9 @@
package com.example.nto.controller.dto;
import lombok.Data;
@Data
public class LoginDto {
private String username;
private String password;
}

View File

@ -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;
}
}

View 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;
}

View File

@ -0,0 +1,7 @@
package com.example.nto.exception;
public class DataValidationException extends RuntimeException {
public DataValidationException(String message) {
super(message);
}
}

View File

@ -0,0 +1,7 @@
package com.example.nto.exception;
public class WrongPasswordException extends RuntimeException {
public WrongPasswordException(String message) {
super(message);
}
}

View File

@ -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);
}
}

View File

@ -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();
}

View File

@ -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);
}

View File

@ -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);
}

View File

@ -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;
}
}

View File

@ -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);
}
}
*/

View File

@ -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();
}
}

View File

@ -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 {
}
}
}
*/

View 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);
}
}

View File

@ -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, "");
}
}

View File

@ -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

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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 name code photo_url
2 Ivanov Ivan 1111 https://catalog-cdn.detmir.st/media/2fe02057f9915e72a378795d32c79ea9.jpeg
3 Petrov Petr 2222 https://catalog-cdn.detmir.st/media/2fe02057f9915e72a378795d32c79ea9.jpeg
4 Kozlov Oleg 3333 https://catalog-cdn.detmir.st/media/2fe02057f9915e72a378795d32c79ea9.jpeg
5 Smirnova Anna 4444 https://catalog-cdn.detmir.st/media/2fe02057f9915e72a378795d32c79ea9.jpeg

View File

@ -1,4 +0,0 @@
place_name
K-19
M-16
T-1
1 place_name
2 K-19
3 M-16
4 T-1

View File

@ -1,3 +0,0 @@
date;place_id;employee_id
2025-11-08;1;1
2025-11-10;2;2
1 date place_id employee_id
2 2025-11-08 1 1
3 2025-11-10 2 2

View File

@ -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>

View File

@ -2,3 +2,5 @@ Command to build fat jar:
```bash
mvn clean package spring-boot:repackage
```
todo: карсиво сделать через аннотацию валидацию всего кроме зависящих юзернейма и пароля (ласт требование)