Skip to content

Commit

Permalink
Merge pull request #13 from ShulV/feature-xmap-4-logout
Browse files Browse the repository at this point in the history
Feature xmap 4 logout
  • Loading branch information
ShulV authored Oct 28, 2023
2 parents c549dda + 893bb96 commit 1d9f6ce
Show file tree
Hide file tree
Showing 13 changed files with 2,464 additions and 121 deletions.
2,147 changes: 2,147 additions & 0 deletions postman/Xmap_request_collection_v1.19_22-10-2023.postman_collection.json

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions sql/generate_DB.sql
Original file line number Diff line number Diff line change
Expand Up @@ -90,8 +90,8 @@ CREATE TABLE public.users(
drop table if exists public.tokens cascade;
CREATE TABLE public.tokens(
id BIGINT NOT NULL GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
value varchar(255),
token_type varchar(255),
value varchar(255) NOT NULL UNIQUE,
token_type varchar(255) NOT NULL,
user_id BIGINT NOT NULL REFERENCES public.users (id) ON UPDATE CASCADE ON DELETE CASCADE
);
------------------------------------------------------------------------------------------------------------------------
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,23 +4,25 @@
import com.shulpov.spots_app.auth.requests.AuthenticationRequest;
import com.shulpov.spots_app.auth.requests.RegisterRequest;
import com.shulpov.spots_app.auth.responses.AuthenticationResponse;
import com.shulpov.spots_app.auth.responses.LogoutMessageResponse;
import com.shulpov.spots_app.auth.responses.RegisterErrorResponse;
import com.shulpov.spots_app.auth.responses.RegisterResponse;
import com.shulpov.spots_app.auth.services.AuthenticationService;
import com.shulpov.spots_app.dto.FieldErrorDto;
import com.shulpov.spots_app.responses.ErrorMessageResponse;
import com.shulpov.spots_app.utils.DtoConverter;
import jakarta.servlet.http.HttpServletRequest;
import io.jsonwebtoken.JwtException;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.*;

import javax.naming.AuthenticationException;
import java.util.List;

/**
Expand All @@ -29,29 +31,75 @@

@RestController
@RequestMapping(value = "/api/v1/auth")
@Tag(name="Контроллер управления аутентификацией пользователей")
@RequiredArgsConstructor
public class AuthenticationController {

@Autowired
private final AuthenticationService service;

@Autowired
private final DtoConverter dtoConverter;

/**
* Регистрация пользователя
* @param request данные пользователя, необходимые для регистрации (JSON)
* @param errors ошибки валидации (передавать их не нужно)
* @return RegisterResponse (userId, accessToken, refreshToken)
*/
@Operation(
summary = "Регистрация",
description = "Зарегистрироваться (с проверкой данных на валидность)."
)
@PostMapping(value="/register")
public ResponseEntity<RegisterResponse> register(
@Valid @RequestBody RegisterRequest request, BindingResult errors
@Parameter(description = "Объект с данными пользователя, необходимыми для регистрации", required = true)
@Valid @RequestBody RegisterRequest request,
@Parameter(description = "Ошибки валидации для ответа (передавать их не нужно)", hidden = true)
BindingResult errors
) {
RegisterResponse response = service.register(request, errors);
return ResponseEntity.status(HttpStatus.CREATED).body(response);
}

@Operation(
summary = "Аутентификация",
description = "Аутентифицироваться по логину и паролю"
)
@PostMapping(value="/authenticate")
public ResponseEntity<AuthenticationResponse> authenticate(
@Parameter(description = "Объект с логином и паролем пользователя", required = true)
@RequestBody AuthenticationRequest request) {
return ResponseEntity.ok(service.authenticate(request));
}

@Operation(
summary = "Получение новых токенов",
description = "Получение новых access и refresh токенов пользователя"
)
@PostMapping(value="/refresh-token")
public ResponseEntity<AuthenticationResponse> refreshToken(
@Parameter(description = "Refresh-токен (JWT)", required = true)
@RequestHeader(value = "Authorization") String refreshToken)
throws JwtException {
return ResponseEntity.status(HttpStatus.OK).body(service.refreshToken(refreshToken));
}

@Operation(
summary = "Выход из учетной записи",
description = "Выход из текущей (одной) учетной записи пользователя"
)
@DeleteMapping(value = "/logout")
public ResponseEntity<LogoutMessageResponse> logout(
@Parameter(description = "Refresh-токен (JWT)", required = true)
@RequestHeader(value = "Authorization") String refreshToken)
throws JwtException {
return ResponseEntity.status(HttpStatus.OK).body(service.logout(refreshToken));
}

@Operation(
summary = "Выход из всех учетных записей",
description = "Выход из всех учетных записей пользователя"
)
@DeleteMapping(value = "/logout-all")
public ResponseEntity<LogoutMessageResponse> logoutAll(
@Parameter(description = "Refresh-токен (JWT)", required = true)
@RequestHeader(value = "Authorization") String refreshToken)
throws JwtException {
return ResponseEntity.status(HttpStatus.OK).body(service.logoutAll(refreshToken));
}

/**
* Обработчик ошибки регистрации
* @param e исключение, содержащее текст ошибки и подробное описание ошибок полей
Expand All @@ -65,20 +113,10 @@ private ResponseEntity<RegisterErrorResponse> handleRegisterErrorException(Regis
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(response);
}

/**
* Аутентификация по паролю и логину
* @param request пароль и логин пользователя (JSON)
* @return AuthenticationResponse (userId, accessToken, refreshToken)
*/
@PostMapping(value="/authenticate")
public ResponseEntity<AuthenticationResponse> authenticate(@RequestBody AuthenticationRequest request) {
return ResponseEntity.ok(service.authenticate(request));
}

/**
* Обработчик ошибки аутентификации (для неверных логина или пароля)
* @param e исключение, содержащее текст ошибки
* @return ErrorMessageResponse
* @return ответ с сообщением об ошибке
*/
@ExceptionHandler
private ResponseEntity<ErrorMessageResponse> handleBadCredentialsException(BadCredentialsException e) {
Expand All @@ -88,20 +126,14 @@ private ResponseEntity<ErrorMessageResponse> handleBadCredentialsException(BadCr
}

/**
* Обновление access и refresh токенов
* @param request объект запроса
* @return ResponseEntity<AuthenticationResponse>
* Обработчик проблем, связанных с JWT-токенами
* @param e исключение, содержащее текст ошибки
* @return ответ с сообщением об ошибке
*/
@PostMapping(value="/refresh-token")
public ResponseEntity<AuthenticationResponse> refreshToken(HttpServletRequest request) throws AuthenticationException {
return ResponseEntity.status(HttpStatus.OK).body(service.refreshToken(request));
}

@ExceptionHandler
private ResponseEntity<ErrorMessageResponse> handleAuthenticationException(AuthenticationException e) {
private ResponseEntity<ErrorMessageResponse> handleJwtExceptionException(JwtException e) {
ErrorMessageResponse response = new ErrorMessageResponse();
response.setErrorMessage(e.getMessage());
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(response);
}

}

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package com.shulpov.spots_app.auth.responses;

import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.*;

@Setter
@Getter
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class LogoutMessageResponse {
private String message;
@JsonProperty("id")
private Long userId;
@JsonProperty("closed_session_number")
private long closedSessionNumber;
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,8 @@
import com.shulpov.spots_app.responses.ValidationErrorResponse;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;

/**
* @author Shulpov Victor
*/
@EqualsAndHashCode(callSuper = true)
@Data
@AllArgsConstructor
@NoArgsConstructor
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import com.shulpov.spots_app.auth.requests.AuthenticationRequest;
import com.shulpov.spots_app.auth.requests.RegisterRequest;
import com.shulpov.spots_app.auth.responses.AuthenticationResponse;
import com.shulpov.spots_app.auth.responses.LogoutMessageResponse;
import com.shulpov.spots_app.auth.responses.RegisterResponse;
import com.shulpov.spots_app.auth.token.Token;
import com.shulpov.spots_app.auth.token.TokenRepository;
Expand All @@ -13,9 +14,8 @@
import com.shulpov.spots_app.user.Role;
import com.shulpov.spots_app.user.User;
import com.shulpov.spots_app.user.UserRepository;
import jakarta.servlet.http.HttpServletRequest;
import io.jsonwebtoken.JwtException;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpHeaders;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
Expand All @@ -24,7 +24,6 @@
import org.springframework.transaction.annotation.Transactional;
import org.springframework.validation.BindingResult;

import javax.naming.AuthenticationException;
import java.util.Date;
import java.util.Optional;

Expand All @@ -47,7 +46,6 @@ public class AuthenticationService {
* Регистрация пользователя (с валидацией данных пользователя, с генерацией токенов для пользователя)
* @param request данные пользователя, указанные при регистрации
* @param errors ошибки валидации
* @return RegisterResponse
*/
public RegisterResponse register(RegisterRequest request, BindingResult errors) {
User user = User.builder()
Expand Down Expand Up @@ -77,9 +75,7 @@ public RegisterResponse register(RegisterRequest request, BindingResult errors)
/**
* Аутентификация пользователя по логину (почте) и паролю
* @param request учетные данные (логин и пароль)
* @return AuthenticationResponse
*/

public AuthenticationResponse authenticate(AuthenticationRequest request) throws BadCredentialsException {
String email = request.getEmail();
//throws BadCredentialsException
Expand Down Expand Up @@ -116,31 +112,46 @@ private void saveUserToken(User user, String jwtToken, TokenType tokenType) {
tokenRepository.save(token);
}

public AuthenticationResponse refreshToken(
HttpServletRequest request
) throws AuthenticationException {
final String authHeader = request.getHeader(HttpHeaders.AUTHORIZATION);
/**
* Проверить валидность refresh-токена
* @param tokenHeader значение заголовка Authorization (Refresh tokenValue)
* @return refresh-токен из БД
*/
private Token validateRefreshToken(String tokenHeader) throws JwtException {
final String oldRefreshToken;
//заголовка с refresh токеном нет
if (authHeader == null || !authHeader.startsWith("Refresh ")) {
throw new AuthenticationException("Refresh token not found in headers");
//заголовка с refresh-токеном нет
if (tokenHeader == null || !tokenHeader.startsWith("Refresh ")) {
throw new JwtException("Refresh token not found in headers");
}
oldRefreshToken = authHeader.substring(8);
oldRefreshToken = tokenHeader.substring(8);

//проверка: не протух ли токен
if(jwtService.isTokenExpired(oldRefreshToken)) {
throw new AuthenticationException("Refresh is expired");
try {
//проверка: не протух ли токен
if(jwtService.isTokenExpired(oldRefreshToken)) {
throw new JwtException("Refresh is expired");
}
} catch (JwtException e) {
throw new JwtException("JWT token error");
}

//проверка: есть ли refresh токен в базе
Optional<Token> refreshTokenFromDB = tokenService.getTokenByValue(oldRefreshToken);
if(refreshTokenFromDB.isEmpty()) {
throw new AuthenticationException("Refresh not found in DB");
throw new JwtException("Refresh not found in DB");
}
Token refreshToken = refreshTokenFromDB.get();
if(refreshToken.getUser() == null) {
throw new AuthenticationException("Token without user");//такого случаться вообще не должно!
throw new JwtException("Token without user");//такого случаться вообще не должно!
}
return refreshToken;
}

/**
* Получить новые access и refresh токены по старому refresh-токену
* @param refreshTokenHeader значение заголовка Authorization (Refresh tokenValue)
*/
public AuthenticationResponse refreshToken(String refreshTokenHeader) throws JwtException {
Token refreshToken = validateRefreshToken(refreshTokenHeader);
User user = refreshToken.getUser();
String newAccessToken = jwtService.generateAccessToken(user);
String newRefreshToken = jwtService.generateRefreshToken(user);
Expand All @@ -163,5 +174,34 @@ private AuthenticationResponse createAuthResponse(Long userId, String accessToke
.refreshToken(refreshToken)
.build();
}

/**
* Выйти из учетной записи
* @param refreshTokenHeader значение заголовка Authorization (Refresh tokenValue)
*/
public LogoutMessageResponse logout(String refreshTokenHeader) throws JwtException {
Token token = validateRefreshToken(refreshTokenHeader);
tokenService.deleteToken(token);
return LogoutMessageResponse.builder()
.closedSessionNumber(1L)
.userId(token.getUser().getId())
.message("Successful logout")
.build();
}

/**
* Выйти изо всех учетных записей
* @param refreshTokenHeader значение заголовка Authorization (Refresh tokenValue)
*/
public LogoutMessageResponse logoutAll(String refreshTokenHeader) throws JwtException {
Token token = validateRefreshToken(refreshTokenHeader);
long count = tokenService.count();
tokenService.deleteAllTokens(token.getUser().getTokens());
return LogoutMessageResponse.builder()
.closedSessionNumber(count)
.userId(token.getUser().getId())
.message("Successful logout")
.build();
}
}

Loading

0 comments on commit 1d9f6ce

Please sign in to comment.