diff --git a/server/pom.xml b/server/pom.xml index 5ab001a..c5d9654 100644 --- a/server/pom.xml +++ b/server/pom.xml @@ -84,6 +84,10 @@ postgresql test + + org.springframework.boot + spring-boot-starter-actuator + diff --git a/server/src/main/java/ru/tubryansk/tdms/config/SecurityConfiguration.java b/server/src/main/java/ru/tubryansk/tdms/config/SecurityConfiguration.java index ee746ac..ada70c3 100644 --- a/server/src/main/java/ru/tubryansk/tdms/config/SecurityConfiguration.java +++ b/server/src/main/java/ru/tubryansk/tdms/config/SecurityConfiguration.java @@ -1,19 +1,98 @@ package ru.tubryansk.tdms.config; +import jakarta.servlet.http.HttpSessionEvent; +import jakarta.servlet.http.HttpSessionListener; 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.ProviderManager; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.authentication.dao.DaoAuthenticationProvider; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; +import org.springframework.security.config.annotation.web.configurers.AuthorizeHttpRequestsConfigurer; +import org.springframework.security.config.annotation.web.configurers.SessionManagementConfigurer; +import org.springframework.security.config.http.SessionCreationPolicy; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContext; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.core.userdetails.User; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.crypto.factory.PasswordEncoderFactories; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.security.provisioning.InMemoryUserDetailsManager; import org.springframework.security.web.SecurityFilterChain; +import static org.springframework.security.web.context.HttpSessionSecurityContextRepository.SPRING_SECURITY_CONTEXT_KEY; + @Configuration public class SecurityConfiguration { @Bean - SecurityFilterChain securityFilterChain(HttpSecurity httpSecurity) throws Exception { + public SecurityFilterChain securityFilterChain(HttpSecurity httpSecurity) throws Exception { return httpSecurity - .authorizeHttpRequests(req -> req.requestMatchers("/**").permitAll()) - .csrf(AbstractHttpConfigurer::disable).build(); + .authorizeHttpRequests(this::configureHttpAuthorization) + .csrf(AbstractHttpConfigurer::disable) + .cors(AbstractHttpConfigurer::disable) + .authenticationManager(authenticationManager()) + .sessionManagement(this::configureSessionManagement) + .build(); + } + + private void configureHttpAuthorization(AuthorizeHttpRequestsConfigurer.AuthorizationManagerRequestMatcherRegistry httpAuthorization) { + /* API ROUTES */ + httpAuthorization.requestMatchers("/api/diploma-topic/**").permitAll(); + httpAuthorization.requestMatchers("/api/**").denyAll(); + /* STATIC ROUTES */ + httpAuthorization.requestMatchers("/**").permitAll(); + /* OTHER */ + httpAuthorization.anyRequest().denyAll(); + } + + @Bean + public AuthenticationManager authenticationManager() { + + return new ProviderManager(authenticationProvider()); + } + + private AuthenticationProvider authenticationProvider() { + DaoAuthenticationProvider provider = new DaoAuthenticationProvider(passwordEncoder()); + provider.setUserDetailsService(userDetailsService()); + return provider; + } + + private UserDetailsService userDetailsService() { + return new InMemoryUserDetailsManager(User.builder() + .username("admin") + .password("{noop}admin") + .authorities("ROLE_STUDENT", "ROLE_TEACHER", "ROLE_CURATOR") + .build()); + } + + private PasswordEncoder passwordEncoder() { + return PasswordEncoderFactories.createDelegatingPasswordEncoder(); + } + + @Bean + // todo: remove when login/logout is implemented + public HttpSessionListener autoAuthenticateUnderAdmin() { + return new HttpSessionListener() { + @Override + public void sessionCreated(HttpSessionEvent se) { + SecurityContext context = SecurityContextHolder.createEmptyContext(); + UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken("admin", "admin"); + Authentication authenticated = authenticationManager().authenticate(authentication); + context.setAuthentication(authenticated); + SecurityContextHolder.setContext(context); + se.getSession().setAttribute(SPRING_SECURITY_CONTEXT_KEY, context); + } + }; + } + + // todo: remove when login/logout is implemented, since we do not need automatically created session with no authentication + private void configureSessionManagement(SessionManagementConfigurer sessionManagement) { + sessionManagement.sessionCreationPolicy(SessionCreationPolicy.ALWAYS); } } diff --git a/server/src/main/java/ru/tubryansk/tdms/controller/DiplomaTopicRestController.java b/server/src/main/java/ru/tubryansk/tdms/controller/DiplomaTopicRestController.java new file mode 100644 index 0000000..996ad1f --- /dev/null +++ b/server/src/main/java/ru/tubryansk/tdms/controller/DiplomaTopicRestController.java @@ -0,0 +1,33 @@ +package ru.tubryansk.tdms.controller; + + +import jakarta.validation.constraints.PositiveOrZero; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import ru.tubryansk.tdms.dto.DiplomaTopicDTO; +import ru.tubryansk.tdms.service.DiplomaTopicService; + +import java.util.List; + + +@RestController +@RequestMapping("/api/diploma-topic/") +@Validated +public class DiplomaTopicRestController { + @Autowired + private DiplomaTopicService diplomaTopicService; + + @GetMapping("/get-all") + public List getAll() { + return diplomaTopicService.getAll(); + } + + @GetMapping("/get-by-id/{id:[\\-+]?\\d+}") + public DiplomaTopicDTO getById(@PathVariable @PositiveOrZero Integer id) { + return diplomaTopicService.getById(id); + } +} diff --git a/server/src/main/java/ru/tubryansk/tdms/controller/TestController.java b/server/src/main/java/ru/tubryansk/tdms/controller/TestController.java deleted file mode 100644 index d2abf1a..0000000 --- a/server/src/main/java/ru/tubryansk/tdms/controller/TestController.java +++ /dev/null @@ -1,16 +0,0 @@ -package ru.tubryansk.tdms.controller; - - -import org.springframework.stereotype.Controller; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.RequestMapping; - - -@Controller -@RequestMapping("/") -public class TestController { - @GetMapping - public String root() { - return "index.html"; - } -} diff --git a/server/src/main/java/ru/tubryansk/tdms/dto/DiplomaTopicDTO.java b/server/src/main/java/ru/tubryansk/tdms/dto/DiplomaTopicDTO.java index ede40ed..93e01bd 100644 --- a/server/src/main/java/ru/tubryansk/tdms/dto/DiplomaTopicDTO.java +++ b/server/src/main/java/ru/tubryansk/tdms/dto/DiplomaTopicDTO.java @@ -1,15 +1,16 @@ package ru.tubryansk.tdms.dto; -import lombok.AllArgsConstructor; -import lombok.Data; -import lombok.NoArgsConstructor; +import lombok.Builder; +import ru.tubryansk.tdms.entity.DiplomaTopic; -@Data -@AllArgsConstructor -@NoArgsConstructor -public class DiplomaTopicDTO { - private Integer id; - private String name; +@Builder +public record DiplomaTopicDTO(Integer id, String name) { + public static DiplomaTopicDTO fromEntity(DiplomaTopic diplomaTopic) { + return DiplomaTopicDTO.builder() + .id(diplomaTopic.getId()) + .name(diplomaTopic.getName()) + .build(); + } } diff --git a/server/src/main/java/ru/tubryansk/tdms/dto/ErrorResponse.java b/server/src/main/java/ru/tubryansk/tdms/dto/ErrorResponse.java new file mode 100644 index 0000000..c993f8e --- /dev/null +++ b/server/src/main/java/ru/tubryansk/tdms/dto/ErrorResponse.java @@ -0,0 +1,20 @@ +package ru.tubryansk.tdms.dto; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; + +public record ErrorResponse(String message, ErrorCode errorCode) { + @RequiredArgsConstructor + @Getter + public enum ErrorCode { + BAD_REQUEST(HttpStatus.BAD_REQUEST), + VALIDATION_ERROR(HttpStatus.BAD_REQUEST), + INTERNAL_ERROR(HttpStatus.INTERNAL_SERVER_ERROR), + NOT_FOUND(HttpStatus.NOT_FOUND), + ACCESS_DENIED(HttpStatus.FORBIDDEN) + ; + + private final HttpStatus httpStatus; + } +} \ No newline at end of file diff --git a/server/src/main/java/ru/tubryansk/tdms/exception/AccessDeniedException.java b/server/src/main/java/ru/tubryansk/tdms/exception/AccessDeniedException.java new file mode 100644 index 0000000..8c28bef --- /dev/null +++ b/server/src/main/java/ru/tubryansk/tdms/exception/AccessDeniedException.java @@ -0,0 +1,14 @@ +package ru.tubryansk.tdms.exception; + +import ru.tubryansk.tdms.dto.ErrorResponse; + +public class AccessDeniedException extends BusinessException { + public AccessDeniedException() { + super("Access denied"); + } + + @Override + public ErrorResponse.ErrorCode getErrorCode() { + return ErrorResponse.ErrorCode.ACCESS_DENIED; + } +} diff --git a/server/src/main/java/ru/tubryansk/tdms/exception/BusinessException.java b/server/src/main/java/ru/tubryansk/tdms/exception/BusinessException.java new file mode 100644 index 0000000..10dbe2b --- /dev/null +++ b/server/src/main/java/ru/tubryansk/tdms/exception/BusinessException.java @@ -0,0 +1,13 @@ +package ru.tubryansk.tdms.exception; + +import ru.tubryansk.tdms.dto.ErrorResponse; + +public class BusinessException extends RuntimeException { + public BusinessException(String message) { + super(message); + } + + public ErrorResponse.ErrorCode getErrorCode() { + return ErrorResponse.ErrorCode.INTERNAL_ERROR; + } +} diff --git a/server/src/main/java/ru/tubryansk/tdms/exception/GlobalExceptionHandler.java b/server/src/main/java/ru/tubryansk/tdms/exception/GlobalExceptionHandler.java new file mode 100644 index 0000000..288e4d5 --- /dev/null +++ b/server/src/main/java/ru/tubryansk/tdms/exception/GlobalExceptionHandler.java @@ -0,0 +1,44 @@ +package ru.tubryansk.tdms.exception; + +import jakarta.servlet.http.HttpServletResponse; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpStatus; +import org.springframework.stereotype.Component; +import org.springframework.web.bind.MethodArgumentNotValidException; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.ResponseStatus; +import org.springframework.web.bind.annotation.RestControllerAdvice; +import org.springframework.web.servlet.resource.NoResourceFoundException; +import ru.tubryansk.tdms.dto.ErrorResponse; + +@RestControllerAdvice +@Slf4j +public class GlobalExceptionHandler { + @ExceptionHandler(MethodArgumentNotValidException.class) + @ResponseStatus(HttpStatus.BAD_REQUEST) + public ErrorResponse handleMethodArgumentNotValidException(MethodArgumentNotValidException e) { + // todo: make a better error message + return new ErrorResponse(e.getMessage(), ErrorResponse.ErrorCode.VALIDATION_ERROR); + } + + @ExceptionHandler(BusinessException.class) + public ErrorResponse handleBusinessException(BusinessException e, HttpServletResponse response) { + response.setStatus(e.getErrorCode().getHttpStatus().value()); + return new ErrorResponse(e.getMessage(), e.getErrorCode()); + } + + @ExceptionHandler(NoResourceFoundException.class) + @ResponseStatus(HttpStatus.NOT_FOUND) + public ErrorResponse handleNoResourceFoundException(NoResourceFoundException e) { + // todo: make error page + return new ErrorResponse(e.getMessage(), ErrorResponse.ErrorCode.NOT_FOUND); + } + + @ExceptionHandler(Exception.class) + @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) + public ErrorResponse handleUnexpectedException(Exception e) { + // todo: make error page + log.error("Unexpected exception.", e); + return new ErrorResponse(e.getMessage(), ErrorResponse.ErrorCode.INTERNAL_ERROR); + } +} diff --git a/server/src/main/java/ru/tubryansk/tdms/exception/NotFoundException.java b/server/src/main/java/ru/tubryansk/tdms/exception/NotFoundException.java new file mode 100644 index 0000000..1fd0cfa --- /dev/null +++ b/server/src/main/java/ru/tubryansk/tdms/exception/NotFoundException.java @@ -0,0 +1,14 @@ +package ru.tubryansk.tdms.exception; + +import ru.tubryansk.tdms.dto.ErrorResponse; + +public class NotFoundException extends BusinessException { + public NotFoundException(Class entityClass, Integer id) { + super(entityClass.getSimpleName() + " with id " + id + " not found"); + } + + @Override + public ErrorResponse.ErrorCode getErrorCode() { + return ErrorResponse.ErrorCode.NOT_FOUND; + } +} diff --git a/server/src/main/java/ru/tubryansk/tdms/repository/DiplomaTopicRepository.java b/server/src/main/java/ru/tubryansk/tdms/repository/DiplomaTopicRepository.java new file mode 100644 index 0000000..49a1306 --- /dev/null +++ b/server/src/main/java/ru/tubryansk/tdms/repository/DiplomaTopicRepository.java @@ -0,0 +1,9 @@ +package ru.tubryansk.tdms.repository; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; +import ru.tubryansk.tdms.entity.DiplomaTopic; + +@Repository +public interface DiplomaTopicRepository extends JpaRepository { +} \ No newline at end of file diff --git a/server/src/main/java/ru/tubryansk/tdms/service/DiplomaTopicService.java b/server/src/main/java/ru/tubryansk/tdms/service/DiplomaTopicService.java new file mode 100644 index 0000000..d43cb9e --- /dev/null +++ b/server/src/main/java/ru/tubryansk/tdms/service/DiplomaTopicService.java @@ -0,0 +1,31 @@ +package ru.tubryansk.tdms.service; + +import jakarta.transaction.Transactional; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import ru.tubryansk.tdms.entity.DiplomaTopic; +import ru.tubryansk.tdms.exception.NotFoundException; +import ru.tubryansk.tdms.repository.DiplomaTopicRepository; +import ru.tubryansk.tdms.dto.DiplomaTopicDTO; + +import java.util.List; +import java.util.stream.Collectors; + +@Service +@Transactional +public class DiplomaTopicService { + @Autowired + private DiplomaTopicRepository diplomaTopicRepository; + + public List getAll() { + return diplomaTopicRepository.findAll() + .stream() + .map(DiplomaTopicDTO::fromEntity) + .collect(Collectors.toList()); + } + + public DiplomaTopicDTO getById(Integer id) { + return DiplomaTopicDTO.fromEntity(diplomaTopicRepository.findById(id) + .orElseThrow(() -> new NotFoundException(DiplomaTopic.class, id))); + } +}