commit 381df0b26bba6850324b6b5b937663d01866ef00
Author: Jakub Smid <jaksmid@seznam.cz>
Date:   Thu Mar 17 18:43:03 2022 +0100

    Added OpenAPI, user DTO and some basic user controller endpoints
    re #9204
    re #9215

diff --git a/backend/pom.xml b/backend/pom.xml
index 58f2a0e..f6f7c49 100644
--- a/backend/pom.xml
+++ b/backend/pom.xml
@@ -85,6 +85,12 @@
             <version>2.4.2</version>
             <optional>true</optional>
         </dependency>
+
+        <dependency>
+            <groupId>org.springdoc</groupId>
+            <artifactId>springdoc-openapi-ui</artifactId>
+            <version>1.6.6</version>
+        </dependency>
     </dependencies>
 
     <build>
diff --git a/backend/src/main/java/cz/zcu/kiv/backendapi/exception/ApiExceptionHandler.java b/backend/src/main/java/cz/zcu/kiv/backendapi/exception/ApiExceptionHandler.java
index 5cc9098..c749504 100644
--- a/backend/src/main/java/cz/zcu/kiv/backendapi/exception/ApiExceptionHandler.java
+++ b/backend/src/main/java/cz/zcu/kiv/backendapi/exception/ApiExceptionHandler.java
@@ -1,15 +1,46 @@
 package cz.zcu.kiv.backendapi.exception;
 
 
+import org.springframework.http.HttpStatus;
 import org.springframework.http.ResponseEntity;
+import org.springframework.security.core.userdetails.UsernameNotFoundException;
+import org.springframework.validation.BindException;
 import org.springframework.web.bind.annotation.ControllerAdvice;
 import org.springframework.web.bind.annotation.ExceptionHandler;
 
 @ControllerAdvice
 public class ApiExceptionHandler {
 
+    /**
+     * Handles bind exception
+     *
+     * @param exception bind exception
+     * @return response entity with message and status
+     */
+    @ExceptionHandler(value = {BindException.class})
+    public ResponseEntity<Object> handleBindException(BindException exception) {
+        return new ResponseEntity<>(exception.getBindingResult().getAllErrors(), HttpStatus.BAD_REQUEST);
+    }
+
+    /**
+     * Handles username not found exception
+     *
+     * @param exception username not found exception
+     * @return response entity with message and status
+     */
+    @ExceptionHandler(value = {UsernameNotFoundException.class})
+    public ResponseEntity<Object> handleUsernameNotFoundException(UsernameNotFoundException exception) {
+        return new ResponseEntity<>(exception.getMessage(), HttpStatus.NOT_FOUND);
+    }
+
+    /**
+     * Handles api request exception
+     *
+     * @param exception api request found exception
+     * @return response entity with message and status
+     */
     @ExceptionHandler(value = {ApiRequestException.class})
     public ResponseEntity<Object> handleApiRequestException(ApiRequestException exception) {
-        return new ResponseEntity<>(exception, exception.getHttpStatus());
+        return new ResponseEntity<>(exception.getMessage(), exception.getHttpStatus());
     }
 }
diff --git a/backend/src/main/java/cz/zcu/kiv/backendapi/security/SecurityConfig.java b/backend/src/main/java/cz/zcu/kiv/backendapi/security/SecurityConfig.java
index 5e2cbf1..9928ea9 100644
--- a/backend/src/main/java/cz/zcu/kiv/backendapi/security/SecurityConfig.java
+++ b/backend/src/main/java/cz/zcu/kiv/backendapi/security/SecurityConfig.java
@@ -11,11 +11,13 @@ import org.springframework.context.annotation.Configuration;
 import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
 import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
 import org.springframework.security.config.annotation.web.builders.HttpSecurity;
+import org.springframework.security.config.annotation.web.builders.WebSecurity;
 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.core.userdetails.UserDetailsService;
 import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
+import org.springframework.security.web.authentication.www.BasicAuthenticationEntryPoint;
 
 /**
  * Security config class
@@ -42,7 +44,8 @@ public class SecurityConfig extends WebSecurityConfigurerAdapter {
     /**
      * List of permitted pages without login
      */
-    private final String[] permittedUrls = new String[]{"/login", "/token/refresh"};
+    private final String[] permittedUrls = new String[]{"/login", "/token/refresh", "/register", "/swagger-ui/**",
+            "/swagger-ui.html", "/v3/api-docs", "/v3/api-docs/swagger-config"};
 
     /**
      * Security configuration
@@ -57,7 +60,6 @@ public class SecurityConfig extends WebSecurityConfigurerAdapter {
                 .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                 .and()
                 .authorizeRequests()
-                .antMatchers("/login").permitAll()
                 .antMatchers(permittedUrls).permitAll()
                 .antMatchers("/admin/**").hasRole(Role.ADMIN.name())
                 .antMatchers("/write/**").hasAuthority(Permission.WRITE.name())
@@ -66,9 +68,17 @@ public class SecurityConfig extends WebSecurityConfigurerAdapter {
                 .anyRequest()
                 .authenticated()
                 .and()
+                .logout()
+                .and()
                 .addFilter(new JwtUsernameAndPasswordAuthenticationFilter(authenticationManager(), jwtUtils))
                 .addFilterAfter(new JwtTokenVerifier(jwtUtils, permittedUrls), JwtUsernameAndPasswordAuthenticationFilter.class);
+
     }
+//    @Override
+//    public void configure(WebSecurity web) throws Exception {
+//        web.ignoring().antMatchers("/swagger-ui/**", "/v3/api-docs","/v3/api-docs/swagger-config", "/swagger-ui.html", "/swagger-ui-custom.html");
+//    }
+
 
     /**
      * Sets authentication provider to authentication manager
@@ -93,5 +103,4 @@ public class SecurityConfig extends WebSecurityConfigurerAdapter {
         provider.setPasswordEncoder(bCryptPasswordEncoder);
         return provider;
     }
-
 }
diff --git a/backend/src/main/java/cz/zcu/kiv/backendapi/user/IUserService.java b/backend/src/main/java/cz/zcu/kiv/backendapi/user/IUserService.java
index 2088aa1..100bbb5 100644
--- a/backend/src/main/java/cz/zcu/kiv/backendapi/user/IUserService.java
+++ b/backend/src/main/java/cz/zcu/kiv/backendapi/user/IUserService.java
@@ -1,5 +1,7 @@
 package cz.zcu.kiv.backendapi.user;
 
+import java.util.List;
+
 /**
  * User service interface
  */
@@ -12,4 +14,12 @@ public interface IUserService {
      * @return user by username
      */
     UserEntity getUserByName(String username);
+
+    void registerNewUser(UserDto userDto);
+
+    void updateUser(UserDto userDto);
+
+    void deleteUser(String username);
+
+    List<UserDto> getAllUsers();
 }
diff --git a/backend/src/main/java/cz/zcu/kiv/backendapi/user/UserController.java b/backend/src/main/java/cz/zcu/kiv/backendapi/user/UserController.java
index c236898..1b3f69c 100644
--- a/backend/src/main/java/cz/zcu/kiv/backendapi/user/UserController.java
+++ b/backend/src/main/java/cz/zcu/kiv/backendapi/user/UserController.java
@@ -11,16 +11,18 @@ import lombok.RequiredArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.http.HttpHeaders;
 import org.springframework.http.HttpStatus;
+import org.springframework.http.ResponseEntity;
 import org.springframework.security.core.GrantedAuthority;
-import org.springframework.web.bind.annotation.GetMapping;
-import org.springframework.web.bind.annotation.RestController;
+import org.springframework.web.bind.annotation.*;
 
 import javax.servlet.http.HttpServletRequest;
 import javax.servlet.http.HttpServletResponse;
+import javax.validation.Valid;
 import java.io.IOException;
 import java.time.LocalDateTime;
 import java.time.ZoneId;
 import java.util.Date;
+import java.util.List;
 import java.util.stream.Collectors;
 
 /**
@@ -42,6 +44,41 @@ public class UserController {
     private final JwtUtils jwtUtils;
 
 
+    /**
+     * Registers new user
+     *
+     * @param userDto user DTO
+     */
+    @PostMapping("/register")
+    public void registerNewUser(@RequestBody @Valid UserDto userDto) {
+        userService.registerNewUser(userDto);
+    }
+
+    /**
+     * Returns list of all users
+     *
+     * @return list of all users
+     */
+    @GetMapping("/users")
+    public ResponseEntity<List<UserDto>> getAllUsers() {
+        return new ResponseEntity<>(userService.getAllUsers(), HttpStatus.OK);
+    }
+
+    //TODO check if need, if needed probably change new dto without email, comment otherwise
+    @PutMapping("/user/{username}")
+    @ResponseStatus(value = HttpStatus.OK)
+    public void updateUser(@PathVariable String username, @RequestBody @Valid UserDto userDto) {
+        userService.updateUser(userDto);
+    }
+
+    //TODO check if needed, comment otherwise
+    @DeleteMapping("/user/{username}")
+    @ResponseStatus(value = HttpStatus.OK)
+    public void deleteUser(@PathVariable String username) {
+        userService.deleteUser(username);
+    }
+
+
     /**
      * Refreshes access token if refresh token is valid
      *
diff --git a/backend/src/main/java/cz/zcu/kiv/backendapi/user/UserDto.java b/backend/src/main/java/cz/zcu/kiv/backendapi/user/UserDto.java
new file mode 100644
index 0000000..7101847
--- /dev/null
+++ b/backend/src/main/java/cz/zcu/kiv/backendapi/user/UserDto.java
@@ -0,0 +1,43 @@
+package cz.zcu.kiv.backendapi.user;
+
+import com.sun.istack.NotNull;
+import cz.zcu.kiv.backendapi.validation.ValidEmail;
+import lombok.Data;
+
+import javax.validation.constraints.NotEmpty;
+
+/**
+ * User data object
+ */
+@Data
+public class UserDto {
+    /**
+     * Name
+     */
+    private String name;
+
+    /**
+     * Email - must be valid email
+     */
+    @ValidEmail
+    @NotNull
+    @NotEmpty(message = "Email must not be empty")
+    private String email;
+
+    /**
+     * Whether user has 'READ' authority
+     */
+    private boolean canRead;
+
+    /**
+     * Whether user has 'WRITE' authority
+     */
+    private boolean canWrite;
+
+    /**
+     * Whether user has 'DELETE' authority
+     */
+    private boolean canDelete;
+
+
+}
\ No newline at end of file
diff --git a/backend/src/main/java/cz/zcu/kiv/backendapi/user/UserRepository.java b/backend/src/main/java/cz/zcu/kiv/backendapi/user/UserRepository.java
index d84c7a7..85bc976 100644
--- a/backend/src/main/java/cz/zcu/kiv/backendapi/user/UserRepository.java
+++ b/backend/src/main/java/cz/zcu/kiv/backendapi/user/UserRepository.java
@@ -15,8 +15,10 @@ import java.util.Optional;
 public interface UserRepository extends JpaRepository<UserEntity, String> {
     /**
      * Returns user by email (username)
+     *
      * @param username username
      * @return optional user by email (username)
      */
     Optional<UserEntity> findByEmail(String username);
+
 }
diff --git a/backend/src/main/java/cz/zcu/kiv/backendapi/user/UserServiceImpl.java b/backend/src/main/java/cz/zcu/kiv/backendapi/user/UserServiceImpl.java
index a951e55..9c864e2 100644
--- a/backend/src/main/java/cz/zcu/kiv/backendapi/user/UserServiceImpl.java
+++ b/backend/src/main/java/cz/zcu/kiv/backendapi/user/UserServiceImpl.java
@@ -1,13 +1,19 @@
 package cz.zcu.kiv.backendapi.user;
 
+import cz.zcu.kiv.backendapi.exception.ApiRequestException;
 import lombok.RequiredArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
+import org.springframework.http.HttpStatus;
+import org.springframework.security.core.context.SecurityContextHolder;
 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 org.springframework.transaction.annotation.Transactional;
 
+import java.util.List;
+import java.util.stream.Collectors;
+
 /**
  * User service implementation
  */
@@ -16,25 +22,132 @@ import org.springframework.transaction.annotation.Transactional;
 @RequiredArgsConstructor
 @Slf4j
 public class UserServiceImpl implements IUserService, UserDetailsService {
+    /**
+     * Message for exception when user is not found by username
+     */
+    private static final String USER_NOT_FOUND = "User with username %s not found";
+
     /**
      * User repository
      */
     private final UserRepository userRepository;
 
+
     @Override
     public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
         return userRepository.findByEmail(username)
                 .orElseThrow(() -> {
-                    log.error(String.format("Username %s not found", username));
-                    throw new UsernameNotFoundException(String.format("Username %s not found", username));
+                    log.error(String.format(USER_NOT_FOUND, username));
+                    throw new UsernameNotFoundException(String.format(USER_NOT_FOUND, username));
                 });
     }
 
     @Override
     public UserEntity getUserByName(String username) {
         return userRepository.findByEmail(username).orElseThrow(() -> {
-            log.error(String.format("Username %s not found", username));
-            throw new UsernameNotFoundException(String.format("Username %s not found", username));
+            log.error(String.format(USER_NOT_FOUND, username));
+            throw new UsernameNotFoundException(String.format(USER_NOT_FOUND, username));
+        });
+    }
+
+    @Override
+    public void registerNewUser(UserDto userDto) {
+        if (userRepository.findByEmail(userDto.getEmail()).isPresent()) {
+            log.error("Trying to register new user with username that is already taken: " + userDto.getEmail());
+            throw new ApiRequestException(String.format("User with username %s already exists", userDto.getEmail()), HttpStatus.CONFLICT);
+        }
+        UserEntity userEntity = new UserEntity();
+        convertDtoToEntity(userDto, userEntity);
+        userRepository.save(userEntity);
+    }
+
+    @Override
+    public void updateUser(UserDto userDto) {
+        UserEntity userEntity = userRepository.findByEmail(userDto.getEmail()).orElseThrow(() ->{
+            log.error(String.format(USER_NOT_FOUND, userDto.getEmail()));
+            throw new UsernameNotFoundException(String.format(USER_NOT_FOUND, userDto.getEmail()));
         });
+        convertDtoToEntity(userDto, userEntity);
+        userRepository.save(userEntity);
+    }
+
+    //TODO maybe chceck if user is not deleting himself - or it might be ok
+    @Override
+    public void deleteUser(String username) {
+        UserEntity userEntity = userRepository.findByEmail(username).orElseThrow(() ->{
+            log.error(String.format(USER_NOT_FOUND, username));
+            throw new UsernameNotFoundException(String.format(USER_NOT_FOUND, username));
+        });
+        userRepository.delete(userEntity);
+    }
+
+    @Override
+    public List<UserDto> getAllUsers() {
+        return userRepository.findAll().stream().map(this::convertEntityToDto).collect(Collectors.toList());
+    }
+
+    /**
+     * Converts user DTO to user entity
+     *
+     * @param userDto    user DTO
+     * @param userEntity user entity
+     */
+    private void convertDtoToEntity(UserDto userDto, UserEntity userEntity) {
+        userEntity.setName(userDto.getName());
+        userEntity.setEmail(userDto.getEmail());
+        userEntity.setPermissions(getPermissionsFromDto(userDto));
+    }
+
+    /**
+     * Converts user entity to user DTO
+     *
+     * @param userEntity user entity
+     * @return user DTO from user entity
+     */
+    private UserDto convertEntityToDto(UserEntity userEntity) {
+        UserDto userDto = new UserDto();
+        userDto.setName(userEntity.getName());
+        userDto.setEmail(userEntity.getEmail());
+        setPermissionsToDto(userDto, userEntity);
+        return userDto;
+    }
+
+    /**
+     * Sets permission to user DTO based on user entity
+     *
+     * @param userDto    user DTO
+     * @param userEntity user entity
+     */
+    private void setPermissionsToDto(UserDto userDto, UserEntity userEntity) {
+        byte userPermissions = userEntity.getPermissions();
+        if ((userPermissions & Permission.READ.getBit()) == Permission.READ.getBit()) {
+            userDto.setCanRead(true);
+        }
+        if ((userPermissions & Permission.WRITE.getBit()) == Permission.WRITE.getBit()) {
+            userDto.setCanWrite(true);
+        }
+        if ((userPermissions & Permission.DELETE.getBit()) == Permission.DELETE.getBit()) {
+            userDto.setCanDelete(true);
+        }
+    }
+
+    /**
+     * Returns permissions as byte from user DTO
+     *
+     * @param userDto user DTO
+     * @return permissions as byte
+     */
+    private byte getPermissionsFromDto(UserDto userDto) {
+        byte permissions = (byte) 0;
+        if (userDto.isCanRead()) {
+            permissions |= Permission.READ.getBit();
+        }
+        if (userDto.isCanWrite()) {
+            permissions |= Permission.WRITE.getBit();
+        }
+        if (userDto.isCanDelete()) {
+            permissions |= Permission.DELETE.getBit();
+        }
+        return permissions;
     }
 }
diff --git a/backend/src/main/java/cz/zcu/kiv/backendapi/validation/EmailValidator.java b/backend/src/main/java/cz/zcu/kiv/backendapi/validation/EmailValidator.java
new file mode 100644
index 0000000..8d45d5b
--- /dev/null
+++ b/backend/src/main/java/cz/zcu/kiv/backendapi/validation/EmailValidator.java
@@ -0,0 +1,34 @@
+package cz.zcu.kiv.backendapi.validation;
+
+import javax.validation.ConstraintValidator;
+import javax.validation.ConstraintValidatorContext;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * Validator for email
+ */
+public class EmailValidator implements ConstraintValidator<ValidEmail, String> {
+    /**
+     * Email string pattern
+     */
+    private static final String EMAIL_PATTERN = "^[_A-Za-z0-9-+]+(.[_A-Za-z0-9-]+)*@[A-Za-z0-9-]+(.[A-Za-z0-9]+)*(.[A-Za-z]{2,})$";
+
+    /**
+     * Email patter
+     */
+    private static final Pattern PATTERN = Pattern.compile(EMAIL_PATTERN);
+
+    /**
+     * Check if email is valid (matches pattern)
+     *
+     * @param email                      email
+     * @param constraintValidatorContext constraint validator context
+     * @return true if email is valid, false otherwise
+     */
+    @Override
+    public boolean isValid(final String email, final ConstraintValidatorContext constraintValidatorContext) {
+        final Matcher matcher = PATTERN.matcher(email);
+        return matcher.matches();
+    }
+}
diff --git a/backend/src/main/java/cz/zcu/kiv/backendapi/validation/ValidEmail.java b/backend/src/main/java/cz/zcu/kiv/backendapi/validation/ValidEmail.java
new file mode 100644
index 0000000..5e51225
--- /dev/null
+++ b/backend/src/main/java/cz/zcu/kiv/backendapi/validation/ValidEmail.java
@@ -0,0 +1,25 @@
+package cz.zcu.kiv.backendapi.validation;
+
+import javax.validation.Constraint;
+import javax.validation.Payload;
+import java.lang.annotation.*;
+
+/**
+ * Constraint to email is valid
+ */
+@Target({ElementType.TYPE, ElementType.FIELD, ElementType.ANNOTATION_TYPE})
+@Retention(RetentionPolicy.RUNTIME)
+@Constraint(validatedBy = EmailValidator.class)
+@Documented
+public @interface ValidEmail {
+    /**
+     * Error message when validation fails
+     *
+     * @return error message
+     */
+    String message() default "Email is not valid - example \"x@x.xx\" (at least one character before and after \"@\" and two characters after \".\")";
+
+    Class<?>[] groups() default {};
+
+    Class<? extends Payload>[] payload() default {};
+}
