Added project

This commit is contained in:
2023-06-11 00:23:46 +03:00
parent 7a8ca16c1f
commit 366ce44813
36 changed files with 2274 additions and 23 deletions

View File

@@ -0,0 +1,13 @@
package ru.resprojects.restsrv;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class RestsrvApplication {
public static void main(String[] args) {
SpringApplication.run(RestsrvApplication.class, args);
}
}

View File

@@ -0,0 +1,17 @@
package ru.resprojects.restsrv.config;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import ru.resprojects.restsrv.token.TokenResourceDetails;
@Configuration
public class AppConfig {
@Bean
@ConfigurationProperties("auth")
public TokenResourceDetails tokenResourceDetails() {
return new TokenResourceDetails();
}
}

View File

@@ -0,0 +1,23 @@
package ru.resprojects.restsrv.config;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.stereotype.Component;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.Serializable;
@Component
public class TokenAuthenticationEntryPoint implements AuthenticationEntryPoint, Serializable {
private static final long serialVersionUID = 7782026919358529193L;
@Override
public void commence(HttpServletRequest request, HttpServletResponse response,
AuthenticationException authException) throws IOException, ServletException {
response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Unauthorized");
}
}

View File

@@ -0,0 +1,48 @@
package ru.resprojects.restsrv.config;
import lombok.extern.slf4j.Slf4j;
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 ru.resprojects.restsrv.service.AuthDetailsService;
import ru.resprojects.restsrv.token.TokenResourceDetails;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
@Component
public class TokenRequestsFilter extends OncePerRequestFilter {
public static final String SECURITY_SCHEME = "Bearer";
private final AuthDetailsService authDetailsService;
private final TokenResourceDetails tokenResourceDetails;
public TokenRequestsFilter(AuthDetailsService authDetailsService, TokenResourceDetails tokenResourceDetails) {
this.authDetailsService = authDetailsService;
this.tokenResourceDetails = tokenResourceDetails;
}
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
final String requestHeader = request.getHeader("Authorization");
String token = null;
if (requestHeader != null && requestHeader.startsWith(SECURITY_SCHEME + " ")) {
token = requestHeader.substring(SECURITY_SCHEME.length() + 1);
}
if (token != null && SecurityContextHolder.getContext().getAuthentication() == null) {
UserDetails userDetails = authDetailsService.loadUserByUsername(tokenResourceDetails.getUserByToken(token));
UsernamePasswordAuthenticationToken usernameAuthToken = new UsernamePasswordAuthenticationToken(
userDetails, null, userDetails.getAuthorities());
usernameAuthToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(usernameAuthToken);
}
filterChain.doFilter(request, response);
}
}

View File

@@ -0,0 +1,72 @@
package ru.resprojects.restsrv.config;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
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.authentication.UsernamePasswordAuthenticationFilter;
import ru.resprojects.restsrv.service.AuthDetailsService;
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
private static final String[] AUTH_WHITELIST = {
// swagger ui
"/v3/api-docs/**",
"/swagger-ui.html",
"/swagger-ui/**"
};
private final TokenRequestsFilter tokenRequestsFilter;
private final AuthDetailsService authDetailsService;
private final TokenAuthenticationEntryPoint tokenAuthenticationEntryPoint;
public WebSecurityConfig(
TokenRequestsFilter tokenRequestsFilter,
AuthDetailsService authDetailsService, TokenAuthenticationEntryPoint tokenAuthenticationEntryPoint) {
this.tokenRequestsFilter = tokenRequestsFilter;
this.authDetailsService = authDetailsService;
this.tokenAuthenticationEntryPoint = tokenAuthenticationEntryPoint;
}
@Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(authDetailsService).passwordEncoder(passwordEncoder());
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.cors().and().csrf().disable()
.authorizeRequests()
.antMatchers(AUTH_WHITELIST)
.permitAll()
.antMatchers("/**")
.authenticated()
.and()
.exceptionHandling().authenticationEntryPoint(tokenAuthenticationEntryPoint)
.and()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
http.addFilterBefore(tokenRequestsFilter, UsernamePasswordAuthenticationFilter.class);
}
}

View File

@@ -0,0 +1,51 @@
package ru.resprojects.restsrv.controller;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.enums.SecuritySchemeIn;
import io.swagger.v3.oas.annotations.enums.SecuritySchemeType;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.responses.ApiResponses;
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
import io.swagger.v3.oas.annotations.security.SecurityScheme;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import ru.resprojects.restsrv.exception.LastException;
@RestController
@RequestMapping("/error")
@Schema(name = "/error")
@Tag(name ="Profiles", description = "the profiles API with documentation annotations")
@SecurityScheme(
name = "customTokenAuth",
type = SecuritySchemeType.HTTP,
in = SecuritySchemeIn.HEADER,
scheme = "bearer",
description = "Авторизация при помощи токена доступа. В HEADER запроса должна присутствовать строка вида Authorization: Bearer "
)
public class ErrorController {
@Operation(
summary = "Получить последнюю ошибку",
description = "Возвращает информацию о последней ошибке.",
security = @SecurityRequirement(name = "customTokenAuth"),
tags = { "profile" }
)
@ApiResponses(value = {
@ApiResponse(
responseCode = "200",
description = "успешная операция",
content = @Content(schema = @Schema(implementation = LastException.class), mediaType = MediaType.APPLICATION_JSON_VALUE)
)
})
@GetMapping(value = "/last", produces = MediaType.APPLICATION_JSON_VALUE)
public ResponseEntity<LastException> getLastException() {
return ResponseEntity.ok(RestExceptionHandler.getLastException());
}
}

View File

@@ -0,0 +1,160 @@
package ru.resprojects.restsrv.controller;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.enums.SecuritySchemeIn;
import io.swagger.v3.oas.annotations.enums.SecuritySchemeType;
import io.swagger.v3.oas.annotations.media.ArraySchema;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.responses.ApiResponses;
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
import io.swagger.v3.oas.annotations.security.SecurityScheme;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
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;
import ru.resprojects.restsrv.dto.ProfileDto;
import ru.resprojects.restsrv.dto.ProfileIdDto;
import ru.resprojects.restsrv.dto.EmailDto;
import ru.resprojects.restsrv.exception.ErrorMessage;
import ru.resprojects.restsrv.model.Profile;
import ru.resprojects.restsrv.service.ProfileService;
import java.util.List;
@RestController
@RequestMapping("/profiles")
@Schema(name = "/profiles")
@Tag(name ="Profiles", description = "the profiles API with documentation annotations")
@SecurityScheme(
name = "customTokenAuth",
type = SecuritySchemeType.HTTP,
in = SecuritySchemeIn.HEADER,
scheme = "bearer",
description = "Авторизация при помощи токена доступа. В HEADER запроса должна присутствовать строка вида Authorization: Bearer "
)
public class ProfileController {
private final ProfileService profileService;
public ProfileController(ProfileService profileService) {
this.profileService = profileService;
}
@Operation(
summary = "Получить список профилей",
description = "Возвращает все созданные профили",
security = @SecurityRequirement(name = "customTokenAuth"),
tags = { "profile" }
)
@ApiResponses(value = {
@ApiResponse(
responseCode = "200",
description = "успешная операция",
content = @Content(
array = @ArraySchema(schema = @Schema(implementation = Profile.class)),
mediaType = MediaType.APPLICATION_JSON_VALUE
)
)
})
@GetMapping(produces = MediaType.APPLICATION_JSON_VALUE)
public ResponseEntity<List<Profile>> finaAll() {
return ResponseEntity.ok(profileService.findAll());
}
@Operation(
summary = "Поиск профиля по ID",
description = "Возвращает профиль с заданным ID",
security = @SecurityRequirement(name = "customTokenAuth"),
tags = { "profile" }
)
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "Успешная операция",
content = @Content(schema = @Schema(implementation = Profile.class), mediaType = MediaType.APPLICATION_JSON_VALUE)),
@ApiResponse(responseCode = "404", description = "Профиль не найден",
content = @Content(schema = @Schema(implementation = ErrorMessage.class), mediaType = MediaType.APPLICATION_JSON_VALUE)
) })
@GetMapping(value = "/{id}", produces = MediaType.APPLICATION_JSON_VALUE)
public ResponseEntity<Profile> findById(
@Parameter(description="Id профиля.", required=true)
@PathVariable("id") Integer id) {
Profile profile = profileService.findById(id);
return ResponseEntity.ok(profile);
}
@Operation(
summary = "Поиск профиля по email",
description = "Возвращает профиль по указанному email",
security = @SecurityRequirement(name = "customTokenAuth"),
tags = { "profile" }
)
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "Успешная операция",
content = @Content(schema = @Schema(implementation = Profile.class), mediaType = MediaType.APPLICATION_JSON_VALUE)),
@ApiResponse(responseCode = "404", description = "Профиль не найден",
content = @Content(schema = @Schema(implementation = ErrorMessage.class), mediaType = MediaType.APPLICATION_JSON_VALUE))
})
@PostMapping(value = "/get", consumes = {MediaType.APPLICATION_JSON_VALUE})
public ResponseEntity<Profile> findByEmail(
@Parameter(description="Email по которому необходимо произвести поиск.",
required=true, schema=@Schema(implementation = EmailDto.class))
@RequestBody EmailDto emailDto) {
Profile profile = profileService.findByEmail(emailDto.getEmail());
return ResponseEntity.ok(profile);
}
@Operation(
summary = "Получить последний созданный профиль",
description = "Возвращает последний созданный профиль",
security = @SecurityRequirement(name = "customTokenAuth"),
tags = { "profile" }
)
@ApiResponses(value = {
@ApiResponse(
responseCode = "200",
description = "успешная операция",
content = @Content(schema = @Schema(implementation = Profile.class), mediaType = MediaType.APPLICATION_JSON_VALUE)
)
})
@GetMapping(value = "/last", produces = MediaType.APPLICATION_JSON_VALUE)
public ResponseEntity<Profile> getLastProfile() {
Profile profile = profileService.getLastCreatedProfile();
return ResponseEntity.ok(profile);
}
@Operation(
summary = "Создание нового профиля",
description = "Создает профиль и возвращается его id",
security = @SecurityRequirement(name = "customTokenAuth"),
tags = { "profile" }
)
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "Успешная операция",
content = @Content(schema = @Schema(implementation = ProfileIdDto.class), mediaType = MediaType.APPLICATION_JSON_VALUE)),
@ApiResponse(responseCode = "400", description = "Передан некорректный email добавляемого профиля",
content = @Content(schema = @Schema(implementation = ErrorMessage.class), mediaType = MediaType.APPLICATION_JSON_VALUE)),
@ApiResponse(responseCode = "403", description = "Email добавляемого профиля уже существует",
content = @Content(schema = @Schema(implementation = ErrorMessage.class), mediaType = MediaType.APPLICATION_JSON_VALUE))
})
@PostMapping(value = "/set", consumes = MediaType.APPLICATION_JSON_VALUE)
public ResponseEntity<ProfileIdDto> save(
@Parameter(description="Данные нового профиля",
required=true, schema=@Schema(implementation = ProfileDto.class))
@RequestBody ProfileDto profileDto) {
Profile profile = new Profile();
profile.setName(profileDto.getName());
profile.setAge(profileDto.getAge());
profile.setEmail(profileDto.getEmail());
Profile newProfile = profileService.save(profile);
ProfileIdDto profileIdDto = new ProfileIdDto(newProfile.getId());
return ResponseEntity.ok(profileIdDto);
}
}

View File

@@ -0,0 +1,48 @@
package ru.resprojects.restsrv.controller;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import ru.resprojects.restsrv.exception.BadResourceException;
import ru.resprojects.restsrv.exception.ErrorMessage;
import ru.resprojects.restsrv.exception.LastException;
import ru.resprojects.restsrv.exception.ResourceAlreadyExistsException;
import ru.resprojects.restsrv.exception.ResourceNotFoundException;
import javax.servlet.http.HttpServletRequest;
import java.sql.Timestamp;
@RestControllerAdvice
public class RestExceptionHandler {
private static LastException lastException;
@ExceptionHandler(value = {ResourceNotFoundException.class})
public ResponseEntity<ErrorMessage> resourceNotFound(HttpServletRequest request, ResourceNotFoundException exception) {
setLastException(exception.getErrorMessage());
return ResponseEntity.status(HttpStatus.NOT_FOUND).body(exception.getErrorMessage());
}
@ExceptionHandler(value = {BadResourceException.class})
public ResponseEntity<ErrorMessage> badResource(HttpServletRequest request, BadResourceException exception) {
setLastException(exception.getErrorMessage());
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(exception.getErrorMessage());
}
@ExceptionHandler(value = {ResourceAlreadyExistsException.class})
public ResponseEntity<ErrorMessage> alreadyExistResource(HttpServletRequest request, ResourceAlreadyExistsException exception) {
setLastException(exception.getErrorMessage());
return ResponseEntity.status(HttpStatus.FORBIDDEN).body(exception.getErrorMessage());
}
private static void setLastException(ErrorMessage errorMessage) {
lastException = new LastException(errorMessage.getMessage(), new Timestamp(System.currentTimeMillis()));
}
public static LastException getLastException() {
return lastException;
}
}

View File

@@ -0,0 +1,20 @@
package ru.resprojects.restsrv.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Getter;
import lombok.Setter;
import java.io.Serializable;
@Getter
@Setter
public class EmailDto implements Serializable {
private static final long serialVersionUID = -5708728999488347598L;
@Schema(description = "Email для поиска профиля")
private String email;
public EmailDto() {
}
}

View File

@@ -0,0 +1,33 @@
package ru.resprojects.restsrv.dto;
import com.fasterxml.jackson.annotation.JsonProperty;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Getter;
import lombok.Setter;
import java.io.Serializable;
@Getter
@Setter
public class ProfileDto implements Serializable {
private static final long serialVersionUID = 8315646868618789737L;
@Schema(description = "Имя пользователя",
example = "Alex", required = false)
@JsonProperty("name")
private String name;
@Schema(description = "E-mail пользователя",
example = "user@example.com", required = true)
@JsonProperty("email")
private String email;
@Schema(description = "Возраст пользователя",
example = "30", required = false)
@JsonProperty("age")
private Integer age;
public ProfileDto() {
}
}

View File

@@ -0,0 +1,24 @@
package ru.resprojects.restsrv.dto;
import com.fasterxml.jackson.annotation.JsonProperty;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.Setter;
import java.io.Serializable;
@Getter
@Setter
@AllArgsConstructor
public class ProfileIdDto implements Serializable {
private static final long serialVersionUID = -7924116702230840180L;
@Schema(description = "ID созданного профиля")
@JsonProperty("idUser")
private Integer userId;
public ProfileIdDto() {
}
}

View File

@@ -0,0 +1,18 @@
package ru.resprojects.restsrv.exception;
public class BadResourceException extends RuntimeException {
private static final long serialVersionUID = 5658497082104293714L;
public BadResourceException() {
}
public BadResourceException(String msg) {
super(msg);
}
public ErrorMessage getErrorMessage() {
return new ErrorMessage(this.getMessage());
}
}

View File

@@ -0,0 +1,24 @@
package ru.resprojects.restsrv.exception;
import com.fasterxml.jackson.annotation.JsonProperty;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.Setter;
import java.io.Serializable;
@Getter
@Setter
@AllArgsConstructor
public class ErrorMessage implements Serializable {
private static final long serialVersionUID = -814379271893846783L;
@Schema(description = "Сообщение о ошибке")
@JsonProperty("msg")
private String message;
public ErrorMessage() {
}
}

View File

@@ -0,0 +1,28 @@
package ru.resprojects.restsrv.exception;
import com.fasterxml.jackson.annotation.JsonProperty;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.Setter;
import java.io.Serializable;
import java.sql.Timestamp;
@Getter
@Setter
@AllArgsConstructor
public class LastException implements Serializable {
private static final long serialVersionUID = -1153271692647620929L;
@Schema(description = "Сообщение о ошибки")
@JsonProperty("msg")
private String message;
@Schema(description = "Дата и время возникновения ошибки")
@JsonProperty("created")
private Timestamp created;
public LastException() {
}
}

View File

@@ -0,0 +1,18 @@
package ru.resprojects.restsrv.exception;
public class ResourceAlreadyExistsException extends RuntimeException {
private static final long serialVersionUID = -2318810158739700082L;
public ResourceAlreadyExistsException() {
}
public ResourceAlreadyExistsException(String msg) {
super(msg);
}
public ErrorMessage getErrorMessage() {
return new ErrorMessage(this.getMessage());
}
}

View File

@@ -0,0 +1,18 @@
package ru.resprojects.restsrv.exception;
public class ResourceNotFoundException extends RuntimeException {
private static final long serialVersionUID = -4368442279968350909L;
public ResourceNotFoundException() {
}
public ResourceNotFoundException(String msg) {
super(msg);
}
public ErrorMessage getErrorMessage() {
return new ErrorMessage(this.getMessage());
}
}

View File

@@ -0,0 +1,79 @@
package ru.resprojects.restsrv.model;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
import org.hibernate.annotations.Cache;
import org.hibernate.annotations.CacheConcurrencyStrategy;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.PrePersist;
import javax.persistence.SequenceGenerator;
import javax.persistence.Table;
import java.io.Serializable;
import java.sql.Timestamp;
@Entity
@Table(name = "profile")
@Cache(usage = CacheConcurrencyStrategy.READ_WRITE)
@Getter
@Setter
@ToString
public class Profile implements Serializable {
private static final long serialVersionUID = 4048798961366546485L;
private static final int START_SEQ = 5000;
@Schema(description = "Unique identifier of the Profile.",
example = "1", required = true)
@Id
@SequenceGenerator(name = "global_seq", sequenceName = "global_seq",
allocationSize = 1, initialValue = START_SEQ)
@GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "global_seq")
private Integer id;
@Schema(description = "Имя пользователя",
example = "Alex", required = false)
private String name;
@Schema(description = "E-mail пользователя",
example = "user@example.com", required = true)
private String email;
@Schema(description = "Возраст пользователя",
example = "30", required = false)
private Integer age;
@Schema(description = "Дата и время создания профиля",
example = "2020-08-24T10:16:17.929+00:00", required = false)
private Timestamp created;
public Profile() {
}
@PrePersist
public void prePersist() {
if (created == null) {
created = new Timestamp(System.currentTimeMillis());
}
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Profile profile = (Profile) o;
return id.equals(profile.id);
}
@Override
public int hashCode() {
return id.hashCode();
}
}

View File

@@ -0,0 +1,15 @@
package ru.resprojects.restsrv.repository;
import org.springframework.data.repository.CrudRepository;
import org.springframework.stereotype.Repository;
import ru.resprojects.restsrv.model.Profile;
import java.util.List;
@Repository
public interface ProfileRepository extends CrudRepository<Profile, Integer> {
List<Profile> findByEmailContainingIgnoreCase(String email);
Boolean existsByEmailContainingIgnoreCase(String email);
}

View File

@@ -0,0 +1,25 @@
package ru.resprojects.restsrv.service;
import org.springframework.security.core.userdetails.User;
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.ArrayList;
@Service
public class AuthDetailsService implements UserDetailsService {
private static final String INMEMORY_USER = "user";
private static final String INMEMORY_PASSWORD = "$2y$12$jj9Q40qh3NOokcPjIg2cFuFK/7jBZlZ/RcrEbXkOALRv88hcuLF5a";
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
if (INMEMORY_USER.equals(username)) {
return new User(INMEMORY_USER, INMEMORY_PASSWORD, new ArrayList<>());
} else {
throw new UsernameNotFoundException("Authorization error! User " + username + " not found!");
}
}
}

View File

@@ -0,0 +1,64 @@
package ru.resprojects.restsrv.service;
import org.springframework.stereotype.Service;
import ru.resprojects.restsrv.exception.BadResourceException;
import ru.resprojects.restsrv.exception.ResourceAlreadyExistsException;
import ru.resprojects.restsrv.exception.ResourceNotFoundException;
import ru.resprojects.restsrv.model.Profile;
import ru.resprojects.restsrv.repository.ProfileRepository;
import java.util.ArrayList;
import java.util.List;
import static ru.resprojects.restsrv.util.ValidationUtil.isEmailValid;
@Service
public class ProfileService {
private final ProfileRepository repository;
private Profile lastCreatedProfile;
public ProfileService(ProfileRepository repository) {
this.repository = repository;
}
public Profile findById(Integer id) throws ResourceNotFoundException {
return repository.findById(id)
.orElseThrow(() -> new ResourceNotFoundException("Cannot find profile with id: " + id));
}
public List<Profile> findAll() {
List<Profile> profiles = new ArrayList<>();
repository.findAll().forEach(profiles::add);
return profiles;
}
public Profile findByEmail(String email) throws ResourceNotFoundException {
List<Profile> profiles = repository.findByEmailContainingIgnoreCase(email);
if (!profiles.isEmpty()) {
return profiles.get(0);
} else {
throw new ResourceNotFoundException("Cannot find profile with email: " + email);
}
}
public Profile save(Profile profile) throws BadResourceException, ResourceAlreadyExistsException {
if (profile == null) {
throw new BadResourceException("Failed to save profile. Profile is null.");
}
String email = profile.getEmail();
if (!isEmailValid(email)) {
throw new BadResourceException("Failed to save profile. Email " + email + " is not valid.");
}
if (repository.existsByEmailContainingIgnoreCase(email)) {
throw new ResourceAlreadyExistsException("Profile with email: " + profile.getEmail() + " already exists.");
}
lastCreatedProfile = repository.save(profile);
return lastCreatedProfile;
}
public Profile getLastCreatedProfile() {
return lastCreatedProfile;
}
}

View File

@@ -0,0 +1,20 @@
package ru.resprojects.restsrv.token;
import lombok.Getter;
import lombok.Setter;
@Getter
@Setter
public class TokenResourceDetails {
private String token;
private String user;
public String getUserByToken(String token) {
if (!this.token.equals(token)) {
return null;
}
return user;
}
}

View File

@@ -0,0 +1,19 @@
package ru.resprojects.restsrv.util;
import java.util.regex.Pattern;
public final class ValidationUtil {
private ValidationUtil() {
}
// https://stackoverflow.com/a/48725527
public static boolean isEmailValid(String email) {
final Pattern EMAIL_REGEX = Pattern.compile(
"[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*@(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?",
Pattern.CASE_INSENSITIVE
);
return EMAIL_REGEX.matcher(email).matches();
}
}

View File

@@ -0,0 +1,65 @@
server:
port: 8010
spring:
profiles:
active: pgsql
---
spring:
profiles: pgsql, prod
jpa:
database: postgresql
generate-ddl: true
properties:
hibernate:
jdbc:
lob:
non_contextual_creation: true
database-platform: org.hibernate.dialect.PostgreSQL9Dialect
open-in-view: false
datasource:
platform: postgresql
initialization-mode: never
---
spring:
profiles: pgsql
datasource:
url: jdbc:postgresql://localhost/test
username: test
password: test
---
spring:
profiles: prod
datasource:
url: ${RESTSRV_PGSQL_DB_HOST}:${RESTSRV_PGSQL_DB_PORT}/${RESTSRV_PGSQL_DB_NAME}
username: ${RESTSRV_PGSQL_DB_USER}
password: ${RESTSRV_PGSQL_DB_PASSWORD}
---
spring:
profiles: test, demo
jpa:
database: h2
open-in-view: false
hibernate:
ddl-auto: none
database-platform: org.hibernate.dialect.H2Dialect
datasource:
url: jdbc:h2:mem:restsrv;DB_CLOSE_ON_EXIT=FALSE
initialization-mode: always
platform: h2
---
logging:
level:
ru.resprojects: debug
org.springframework.transaction: debug
org.springframework: error
pattern:
file: "%d %p %c{1.} [%t] %m%n"
console: "%clr(%d{HH:mm:ss.SSS}){yellow} %clr(%-5p) %clr(---){faint} %clr([%t]){cyan} %clr(%logger{36}){blue} %clr(:){red} %clr(%msg){faint}%n"
file:
name: restsrv.log
max-size: 5MB
auth:
token: secret
user: user

View File

@@ -0,0 +1,6 @@
DELETE FROM profile;
ALTER SEQUENCE global_seq RESTART WITH 5000;
INSERT INTO profile (name, email, age, created) VALUES
('h2user1', 'h2user1@example.com', 10, now()),
('h2user2', 'h2user2@example.com', 20, now());

View File

@@ -0,0 +1,6 @@
DELETE FROM profile;
ALTER SEQUENCE global_seq RESTART WITH 5000;
INSERT INTO profile (name, email, age) VALUES
('user1', 'user1@example.com', 20),
('user2', 'user2@example.com', 30);

View File

@@ -0,0 +1,13 @@
DROP TABLE IF EXISTS profile;
DROP SEQUENCE IF EXISTS global_seq;
CREATE SEQUENCE global_seq MINVALUE 5000;
CREATE TABLE profile (
id INT DEFAULT global_seq.nextval PRIMARY KEY,
name VARCHAR NOT NULL,
email VARCHAR NOT NULL,
age INT DEFAULT 0 NOT NULL,
created TIMESTAMP
);
CREATE UNIQUE INDEX profile_unique_email_idx ON profile(email);

View File

@@ -0,0 +1,13 @@
DROP TABLE IF EXISTS profile;
DROP SEQUENCE IF EXISTS global_seq CASCADE;
CREATE SEQUENCE global_seq START 5000;
CREATE TABLE profile (
id INTEGER PRIMARY KEY DEFAULT nextval('global_seq'),
name VARCHAR NOT NULL,
email VARCHAR NOT NULL,
age INTEGER DEFAULT 0 NOT NULL,
created TIMESTAMP DEFAULT now()::timestamp
);
CREATE UNIQUE INDEX profile_unique_email_idx ON profile(email);

View File

@@ -0,0 +1,44 @@
package ru.resprojects.restsrv;
import org.junit.Test;
import ru.resprojects.restsrv.util.ValidationUtil;
import java.util.List;
import static org.assertj.core.api.Assertions.assertThat;
public class ValidationUtilTest {
@Test
public void whenPassCorrectEmailToValidatorThenReturnTrue() {
List<String> emails = List.of(
"john@somewhere.com",
"john.foo@somewhere.com",
"john.foo+label@somewhere.com",
"john@192.168.1.10",
"john+label@192.168.1.10",
"john.foo@someserver",
"JOHN.FOO@somewhere.com"
);
for (String email : emails) {
assertThat(ValidationUtil.isEmailValid(email)).isTrue();
}
}
@Test
public void whenPassIncorrectEmailToValidatorThenReturnFalse() {
List<String> emails = List.of(
"@someserver",
"@someserver.com",
"john@.",
".@somewhere.com",
".@.somewhere.com"
);
for (String email : emails) {
assertThat(ValidationUtil.isEmailValid(email)).isFalse();
}
}
}

View File

@@ -0,0 +1,196 @@
package ru.resprojects.restsrv.controller;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.web.client.TestRestTemplate;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.test.context.jdbc.Sql;
import org.springframework.test.context.jdbc.SqlConfig;
import org.springframework.test.context.junit4.SpringRunner;
import ru.resprojects.restsrv.config.TokenRequestsFilter;
import ru.resprojects.restsrv.dto.EmailDto;
import ru.resprojects.restsrv.dto.ProfileDto;
import ru.resprojects.restsrv.dto.ProfileIdDto;
import ru.resprojects.restsrv.exception.ErrorMessage;
import ru.resprojects.restsrv.exception.LastException;
import ru.resprojects.restsrv.model.Profile;
import ru.resprojects.restsrv.repository.ProfileRepository;
import ru.resprojects.restsrv.token.TokenResourceDetails;
import static org.assertj.core.api.Assertions.assertThat;
@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@ActiveProfiles(profiles = {"test"})
@Sql(executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD,
scripts = {"classpath:schema-h2.sql"},
config = @SqlConfig(encoding = "UTF-8"))
public class ProfileControllerTest {
@Autowired
private TestRestTemplate restTemplate;
@Autowired
private ProfileRepository repository;
@Autowired
private TokenResourceDetails tokenResourceDetails;
private Profile exampleProfile;
private final HttpHeaders headers = new HttpHeaders();
@Before
public void init() {
headers.add("Authorization", TokenRequestsFilter.SECURITY_SCHEME + " " + tokenResourceDetails.getToken());
Profile profile = new Profile();
profile.setName("Alex");
profile.setEmail("alex@example.com");
profile.setAge(10);
exampleProfile = repository.save(profile);
}
@Test
public void whenFindByExistentIdThenStatus200AndReturnProfile() {
HttpEntity<String> entity = new HttpEntity<>(null, headers);
ResponseEntity<Profile> profile = restTemplate.exchange("/profiles/5000", HttpMethod.GET, entity, Profile.class);
assertThat(profile.getStatusCode()).isEqualTo(HttpStatus.OK);
assertThat(profile.getBody()).isNotNull();
assertThat(profile.getBody().getName()).isEqualTo(exampleProfile.getName());
}
@Test
public void whenRequestAllProfilesThenStatus200AndReturnListOfProfiles() {
HttpEntity<String> entity = new HttpEntity<>(null, headers);
ResponseEntity<Profile[]> profiles = restTemplate.exchange("/profiles", HttpMethod.GET, entity, Profile[].class);
assertThat(profiles.getStatusCode()).isEqualTo(HttpStatus.OK);
assertThat(profiles.getBody()).isNotNull();
assertThat(profiles.getBody()).isNotEmpty();
assertThat(profiles.getBody()[0].getName()).isEqualTo(exampleProfile.getName());
}
@Test
public void whenFindProfileByEmailThenStatus200AndReturnProfile() {
EmailDto emailDto = new EmailDto();
emailDto.setEmail("alex@example.com");
HttpEntity<EmailDto> entity = new HttpEntity<>(emailDto, headers);
ResponseEntity<Profile> profile = restTemplate.exchange("/profiles/get", HttpMethod.POST, entity, Profile.class);
assertThat(profile.getStatusCode()).isEqualTo(HttpStatus.OK);
assertThat(profile.getBody()).isNotNull();
assertThat(profile.getBody().getName()).isEqualTo(exampleProfile.getName());
assertThat(profile.getBody().getCreated()).isNotNull();
}
@Test
public void whenCreateProfileThenStatus200AndReturnIdSavedProfile() {
ProfileDto profileDto = new ProfileDto();
profileDto.setName("John");
profileDto.setAge(10);
profileDto.setEmail("john@gmail.com");
HttpEntity<ProfileDto> entity = new HttpEntity<>(profileDto, headers);
ResponseEntity<ProfileIdDto> newProfileId = restTemplate.exchange("/profiles/set", HttpMethod.POST, entity, ProfileIdDto.class);
assertThat(newProfileId.getStatusCode()).isEqualTo(HttpStatus.OK);
assertThat(newProfileId.getBody()).isNotNull();
assertThat(newProfileId.getBody().getUserId()).isEqualTo(5001);
}
@Test
public void whenRequestLastCreatedProfileThenStatus200AndReturnLastSavedProfile() {
ProfileDto profileDto = new ProfileDto();
profileDto.setName("John");
profileDto.setAge(10);
profileDto.setEmail("john@gmail.com");
HttpEntity<ProfileDto> entity = new HttpEntity<>(profileDto, headers);
ResponseEntity<ProfileIdDto> newProfileId = restTemplate.exchange("/profiles/set", HttpMethod.POST, entity, ProfileIdDto.class);
assertThat(newProfileId.getBody()).isNotNull();
HttpEntity<Void> newEntity = new HttpEntity<>(null, headers);
ResponseEntity<Profile> profile = restTemplate.exchange("/profiles/last", HttpMethod.GET, newEntity, Profile.class);
assertThat(profile.getStatusCode()).isEqualTo(HttpStatus.OK);
assertThat(profile.getBody()).isNotNull();
assertThat(profile.getBody().getId()).isEqualTo(newProfileId.getBody().getUserId());
}
@Test
public void whenRequestProfileWithNonexistentIdThenStatus404AndReturnErrorMessage() {
HttpEntity<String> entity = new HttpEntity<>(null, headers);
ResponseEntity<ErrorMessage> errorMessage = restTemplate.exchange("/profiles/5010", HttpMethod.GET, entity, ErrorMessage.class);
assertThat(errorMessage.getStatusCode()).isEqualTo(HttpStatus.NOT_FOUND);
assertThat(errorMessage.getBody()).isNotNull();
}
@Test
public void whenRequestProfileWithNonexistentEmailThenStatus404AndReturnErrorMessage() {
EmailDto emailDto = new EmailDto();
emailDto.setEmail("test@example.com");
HttpEntity<EmailDto> entity = new HttpEntity<>(emailDto, headers);
ResponseEntity<ErrorMessage> errorMessage = restTemplate.exchange("/profiles/get", HttpMethod.POST, entity, ErrorMessage.class);
assertThat(errorMessage.getStatusCode()).isEqualTo(HttpStatus.NOT_FOUND);
assertThat(errorMessage.getBody()).isNotNull();
}
@Test
public void whenCreatProfileWithIncorrectEmailThenReturnStatus400AndErrorMessage() {
ProfileDto profileDto = new ProfileDto();
profileDto.setName("John");
profileDto.setAge(10);
profileDto.setEmail(".@gmail.com");
HttpEntity<ProfileDto> entity = new HttpEntity<>(profileDto, headers);
ResponseEntity<ErrorMessage> errorMessage = restTemplate.exchange("/profiles/set", HttpMethod.POST, entity, ErrorMessage.class);
assertThat(errorMessage.getStatusCode()).isEqualTo(HttpStatus.BAD_REQUEST);
assertThat(errorMessage.getBody()).isNotNull();
}
@Test
public void whenCreatProfileWithExistentEmailThenReturnStatus403AndErrorMessage() {
ProfileDto profileDto = new ProfileDto();
profileDto.setName("John");
profileDto.setAge(10);
profileDto.setEmail("alex@example.com");
HttpEntity<ProfileDto> entity = new HttpEntity<>(profileDto, headers);
ResponseEntity<ErrorMessage> errorMessage = restTemplate.exchange("/profiles/set", HttpMethod.POST, entity, ErrorMessage.class);
assertThat(errorMessage.getStatusCode()).isEqualTo(HttpStatus.FORBIDDEN);
assertThat(errorMessage.getBody()).isNotNull();
}
@Test
public void whenRequestLastErrorThenStatus200AndReturnMessageWithLastError() {
HttpEntity<String> entity = new HttpEntity<>(null, headers);
ResponseEntity<ErrorMessage> errorMessage = restTemplate.exchange("/profiles/5010", HttpMethod.GET, entity, ErrorMessage.class);
assertThat(errorMessage.getStatusCode()).isEqualTo(HttpStatus.NOT_FOUND);
assertThat(errorMessage.getBody()).isNotNull();
ResponseEntity<LastException> lastErrorMessage = restTemplate.exchange("/error/last", HttpMethod.GET, entity, LastException.class);
assertThat(lastErrorMessage.getStatusCode()).isEqualTo(HttpStatus.OK);
assertThat(lastErrorMessage.getBody()).isNotNull();
assertThat(lastErrorMessage.getBody().getMessage()).isEqualTo(errorMessage.getBody().getMessage());
assertThat(lastErrorMessage.getBody().getCreated()).isNotNull();
}
}

View File

@@ -0,0 +1,133 @@
package ru.resprojects.restsrv.service;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.test.context.jdbc.Sql;
import org.springframework.test.context.jdbc.SqlConfig;
import org.springframework.test.context.junit4.SpringRunner;
import ru.resprojects.restsrv.RestsrvApplication;
import ru.resprojects.restsrv.exception.BadResourceException;
import ru.resprojects.restsrv.exception.ResourceAlreadyExistsException;
import ru.resprojects.restsrv.exception.ResourceNotFoundException;
import ru.resprojects.restsrv.model.Profile;
import java.util.List;
import static org.assertj.core.api.Assertions.assertThat;
@RunWith(SpringRunner.class)
@SpringBootTest(classes = RestsrvApplication.class)
@ActiveProfiles(profiles = {"test"})
@Sql(executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD,
scripts = {"classpath:schema-h2.sql"},
config = @SqlConfig(encoding = "UTF-8"))
public class ProfileServiceTest {
@Autowired
private ProfileService profileService;
private Profile exampleProfile;
@Before
public void init() {
Profile profile = new Profile();
profile.setName("h2user1");
profile.setEmail("h2user1@example.com");
profile.setAge(20);
exampleProfile = profileService.save(profile);
}
@Test
public void whenSaveNewProfileThenReturnedProfileWithId() {
Profile exampleProfile = new Profile();
exampleProfile.setName("Alex");
exampleProfile.setEmail("alex@example.com");
exampleProfile.setAge(20);
Profile newProfile = profileService.save(exampleProfile);
assertThat(newProfile).isNotNull();
assertThat(newProfile.getId()).isNotNull();
}
@Test
public void whenFindByIdThenReturnedProfile() {
Profile profile = profileService.findById(5000);
assertThat(profile).isNotNull();
}
@Test
public void whenFindByEmailThenReturnedProfile() {
Profile profile = profileService.findByEmail("h2user1@example.com");
assertThat(profile).isNotNull();
}
@Test
public void whenFindByEmailCaseInsensitiveThenReturnedProfile() {
Profile profile = profileService.findByEmail("H2uSer1@eXAmple.com");
assertThat(profile).isNotNull();
}
@Test
public void whenGetLastCreatedProfileThenReturnedLastSavedProfile() {
Profile profile = profileService.getLastCreatedProfile();
assertThat(profile).isNotNull();
assertThat(profile).isEqualTo(exampleProfile);
}
@Test
public void whenFindAllThenReturnedAllProfiles() {
List<Profile> profiles = profileService.findAll();
assertThat(profiles).isNotEmpty();
}
@Test(expected = ResourceNotFoundException.class)
public void whenFindByNonexistentIdWhenException() {
profileService.findById(5010);
}
@Test(expected = ResourceNotFoundException.class)
public void whenFindByNonexistentEmailThenException() {
profileService.findByEmail("test@test.com");
}
@Test(expected = BadResourceException.class)
public void whenSaveProfileWithIncorrectEmailThenException() {
Profile exampleProfile = new Profile();
exampleProfile.setName("Alex");
exampleProfile.setEmail(".@example.com");
exampleProfile.setAge(20);
profileService.save(exampleProfile);
}
@Test(expected = ResourceAlreadyExistsException.class)
public void whenSaveProfileWithExistEmailThenException() {
Profile exampleProfile = new Profile();
exampleProfile.setName("Alex");
exampleProfile.setEmail("h2user1@example.com");
exampleProfile.setAge(20);
profileService.save(exampleProfile);
}
@Test(expected = ResourceAlreadyExistsException.class)
public void whenSaveProfileWithExistEmailCaseInsensitiveThenException() {
Profile exampleProfile = new Profile();
exampleProfile.setName("Alex");
exampleProfile.setEmail("H2user1@example.com");
exampleProfile.setAge(20);
profileService.save(exampleProfile);
}
}