commit fdf6c32defcc029d80f6afbf1420507c2302d408
Author: Schwobik <michal.schwob@seznam.cz>
Date:   Sun May 15 17:06:02 2022 +0200

    Administation page finished
    re #9818

diff --git a/backend/docker-compose.yml b/backend/docker-compose.yml
index 1a5197d..c39cfa6 100644
--- a/backend/docker-compose.yml
+++ b/backend/docker-compose.yml
@@ -19,7 +19,7 @@ services:
   app: # Spring boot application
     build: .
     container_name: app-backend  # name of the container
-    image: schwobik/backend-app:1.7
+    image: schwobik/backend-app:1.8
     ports:
       - "8080:8080"                 # expose port 8080 out of the docker container do the local machine
     depends_on:
@@ -31,7 +31,7 @@ services:
 #     Since our Dockerfile for web-server is located in react-app folder, our build context is ./react-app
     build: ../frontend
     container_name: frontend
-    image: schwobik/frontend-app:1.7
+    image: schwobik/frontend-app:1.8
     ports:
       - "80:80"
 
diff --git a/backend/src/main/java/cz/zcu/kiv/backendapi/config/CorsConfig.java b/backend/src/main/java/cz/zcu/kiv/backendapi/config/CorsConfig.java
index 6e42f6a..5358bf0 100644
--- a/backend/src/main/java/cz/zcu/kiv/backendapi/config/CorsConfig.java
+++ b/backend/src/main/java/cz/zcu/kiv/backendapi/config/CorsConfig.java
@@ -17,7 +17,6 @@ public class CorsConfig implements WebMvcConfigurer {
     public void addCorsMappings(CorsRegistry registry) {
         registry.addMapping("/**")
                 .allowedOrigins("*")
-                .allowedMethods(HttpMethod.GET.name(), HttpMethod.POST.name(), HttpMethod.DELETE.name(),
-                        HttpMethod.PUT.name(), HttpMethod.PATCH.name());
+                .allowedMethods("GET", "POST", "DELETE", "PUT", "OPTIONS", "PATCH");
     }
 }
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 fde3ec4..1520500 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
@@ -21,6 +21,8 @@ import org.springframework.web.cors.CorsConfiguration;
 import org.springframework.web.cors.CorsConfigurationSource;
 import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
 
+import java.util.Arrays;
+import java.util.Collections;
 import java.util.HashMap;
 import java.util.Map;
 
@@ -63,7 +65,6 @@ public class SecurityConfig extends WebSecurityConfigurerAdapter {
         PERMITTED_ENDPOINTS.put("/catalog-items", HttpMethod.GET);
         PERMITTED_ENDPOINTS.put("/catalog-items/**", HttpMethod.GET);
         PERMITTED_ENDPOINTS.put("/title-page", HttpMethod.GET);
-        PERMITTED_ENDPOINTS.put("/sources", HttpMethod.GET);
     }
 
     /**
@@ -72,6 +73,7 @@ public class SecurityConfig extends WebSecurityConfigurerAdapter {
      * @param http http security
      * @throws Exception exception
      */
+    // TODO configure and check rights
     @Override
     protected void configure(HttpSecurity http) throws Exception {
         http.csrf().disable()
@@ -84,15 +86,15 @@ public class SecurityConfig extends WebSecurityConfigurerAdapter {
                 .authorizeRequests()
                 .antMatchers(HttpMethod.GET, PERMITTED_ENDPOINTS.keySet().stream().filter(k -> PERMITTED_ENDPOINTS.get(k).equals(HttpMethod.GET)).toArray(String[]::new)).permitAll()
                 .antMatchers(HttpMethod.POST, "/login").permitAll()
-                .antMatchers(HttpMethod.POST, "/users", "/external-catalog-items").hasRole(Role.ADMIN.name())
+                .antMatchers("/external-catalog-items").hasRole(Role.ADMIN.name())
                 .antMatchers(HttpMethod.PATCH, "/users/*/permissions", "/users/*/password").hasRole(Role.ADMIN.name())
                 .antMatchers(HttpMethod.DELETE, "/users/**").hasRole(Role.ADMIN.name())
                 .antMatchers(HttpMethod.GET, "/users").hasRole(Role.ADMIN.name())
-                .antMatchers(HttpMethod.GET, "/path", "/external-catalog-items").hasAuthority(Permission.READ.name())
+                .antMatchers(HttpMethod.GET, "/path").hasAuthority(Permission.READ.name())
                 .antMatchers(HttpMethod.POST, "/catalog-items").hasAuthority(Permission.WRITE.name())
                 .antMatchers(HttpMethod.PUT, "/catalog-items/*").hasAuthority(Permission.WRITE.name())
                 .antMatchers(HttpMethod.DELETE, "/catalog-items/*").hasAuthority(Permission.DELETE.name())
-                .antMatchers(HttpMethod.POST, "/title-page", "/sources").hasRole(Role.ADMIN.name())
+                .antMatchers(HttpMethod.POST, "/title-page").hasRole(Role.ADMIN.name())
                 .anyRequest()
                 .authenticated();
     }
@@ -115,7 +117,7 @@ public class SecurityConfig extends WebSecurityConfigurerAdapter {
      */
     @Bean
     public DaoAuthenticationProvider authenticationProvider() {
-        DaoAuthenticationProvider provider = new DaoAuthenticationProvider();
+        final DaoAuthenticationProvider provider = new DaoAuthenticationProvider();
         provider.setUserDetailsService(userDetailsService);
         provider.setPasswordEncoder(bCryptPasswordEncoder);
         return provider;
@@ -123,8 +125,10 @@ public class SecurityConfig extends WebSecurityConfigurerAdapter {
 
     @Bean
     CorsConfigurationSource corsConfigurationSource() {
-        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
-        source.registerCorsConfiguration("/**", new CorsConfiguration().applyPermitDefaultValues());
+        final UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
+        CorsConfiguration corsConfiguration = new CorsConfiguration().applyPermitDefaultValues();
+        corsConfiguration.setAllowedMethods(java.util.List.of(HttpMethod.GET.name(), HttpMethod.HEAD.name(), HttpMethod.POST.name(), HttpMethod.DELETE.name(), HttpMethod.PATCH.name(), HttpMethod.PUT.name()));
+        source.registerCorsConfiguration("/**", corsConfiguration);
         return source;
     }
 }
diff --git a/frontend/src/features/Administration/Administration.tsx b/frontend/src/features/Administration/Administration.tsx
index 8e66d61..58a8b58 100644
--- a/frontend/src/features/Administration/Administration.tsx
+++ b/frontend/src/features/Administration/Administration.tsx
@@ -19,6 +19,7 @@ import {fetchUsers} from "./userDetailThunks"
 import {
     clear,
     consumeError,
+    resetIsRequestCompleted,
     setLoading,
     setSelectedUser
 } from "./userDetailSlice"
@@ -34,6 +35,7 @@ const Administration = () => {
     const loading = useSelector((state: RootState) => state.usersDetail.loading)
     const apiError = useSelector((state: RootState) => state.usersDetail.error)
     const selectedUser = useSelector((state: RootState) => state.usersDetail.selectedUser)
+    const isRequestCompleted = useSelector((state: RootState) => state.usersDetail.isRequestCompleted)
     const [selectedUserId, setSelectedUserId] = useState<number | undefined>(undefined)
 
     // Local state to display any error relevant error
@@ -59,11 +61,38 @@ const Administration = () => {
     // Use effect to read the error and consume it
     useEffect(() => {
         if (users.length > 0) {
-            setSelectedUserId(0)
-            dispatch(setSelectedUser(users.at(0)))
-            console.log("selected id: " + selectedUser)
+            // if there is user selected, leave that state - handled by useEffect on isRequestCompleted
+            if (selectedUser !== undefined) {
+                const selUser = users.find(u => u.email === selectedUser.email)
+                if (selUser !== undefined) {
+                    console.log("Selected from before")
+                    console.log(selUser)
+                    setSelectedUserId(users.findIndex(u => u.email === selectedUser.email))
+                    dispatch(setSelectedUser(selUser))
+                    return
+                }
+            } else {
+                setSelectedUserId(0)
+                dispatch(setSelectedUser(users.at(0)))
+            }
         }
-    }, [users])
+    }, [users, dispatch])
+
+    useEffect(() => {
+        if (isRequestCompleted) {
+            dispatch(resetIsRequestCompleted())
+            // dispatch(clear())
+            dispatch(fetchUsers())
+            // setSelectedUserId(0)
+            // dispatch(setSelectedUser(users.at(0)))
+            //
+            // if (users.length > 0) {
+            //     setSelectedUserId(0)
+            //     dispatch(setSelectedUser(users.at(0)))
+            //     console.log("selected id: " + selectedUser)
+            // }
+        }
+    }, [isRequestCompleted, dispatch])
 
 
     // Use effect to read the error and consume it
@@ -87,13 +116,6 @@ const Administration = () => {
         }
     }, [dispatch])
 
-    const childClosed = () => {
-        dispatch(clear())
-        dispatch(fetchUsers())
-        setSelectedUserId(0)
-        dispatch(setSelectedUser(users.at(0)))
-    }
-
     return (
         <Fragment>
             <ShowErrorIfPresent err={displayError} />
@@ -129,7 +151,7 @@ const Administration = () => {
                     </Grid>
                     <Grid item md={9} xs={6}>
                         {selectedUserId !== undefined  ?
-                            <UserDetail user={selectedUser as UserDto} onClose={childClosed} />
+                            <UserDetail user={selectedUser as UserDto}/>
                         : null }
                     </Grid>
                 </Grid>
diff --git a/frontend/src/features/Administration/UserDetail.tsx b/frontend/src/features/Administration/UserDetail.tsx
index ab4fc49..369adaf 100644
--- a/frontend/src/features/Administration/UserDetail.tsx
+++ b/frontend/src/features/Administration/UserDetail.tsx
@@ -5,7 +5,7 @@ import {
     Paper,
     Typography,
     FormGroup,
-    FormControlLabel, Container
+    FormControlLabel
 } from '@mui/material'
 import React, {Fragment, useEffect, useState} from 'react'
 import axiosInstance from "../../api/api"
@@ -14,17 +14,16 @@ import {PermissionDto, UserDto} from "../../swagger/data-contracts"
 import ClearIcon from "@mui/icons-material/Clear"
 import CheckIcon from '@mui/icons-material/Check';
 import {useDispatch, useSelector} from "react-redux"
-import {fetchUsers, savePermissions} from "./userDetailThunks"
+import {deleteUser, savePermissions} from "./userDetailThunks"
 import {RootState} from "../redux/store"
 import {setSelectedUserPermissions} from "./userDetailSlice"
 
 export interface UserDetailProps {
-    user: UserDto,
-    onClose: () => void
+    user: UserDto
 }
 
 const UserDetail = (props: UserDetailProps) => {
-    const selectedUser = useSelector((state: RootState) => state.usersDetail.selectedUser)
+    const selectedUser: UserDto | undefined = useSelector((state: RootState) => state.usersDetail.selectedUser)
     const [canRead, setCanRead] = useState(selectedUser?.permissions?.canRead)
     const [canWrite, setCanWrite] = useState(selectedUser?.permissions?.canWrite)
     const [canDelete, setCanDelete] = useState(selectedUser?.permissions?.canDelete)
@@ -69,21 +68,13 @@ const UserDetail = (props: UserDetailProps) => {
     }
 
 
-    const deleteUser = async () => {
-        const { data, status } = await axiosInstance.delete(
-            `/users/${selectedUser?.email}`
-        )
-        if (status !== 200) {
-            // TODO dodělat zpracování erroru
-            return
-        }
-        props.onClose();
+    const prepareDeleteUser = async () => {
+        dispatch(deleteUser())
     }
 
     const prepareSavePermissions = () => {
         dispatch(setSelectedUserPermissions({canRead: canRead, canWrite: canWrite, canDelete: canDelete}))
         dispatch(savePermissions())
-        dispatch(fetchUsers())
     }
 
     return (
@@ -139,7 +130,7 @@ const UserDetail = (props: UserDetailProps) => {
                 <Button startIcon={<ClearIcon />}
                         variant="contained"
                         color="primary"
-                        onClick={deleteUser}
+                        onClick={prepareDeleteUser}
                         sx={{ m: 2 }} >
                     Delete
                 </Button>
diff --git a/frontend/src/features/Administration/userDetailSlice.tsx b/frontend/src/features/Administration/userDetailSlice.tsx
index 247df15..2e0bb6c 100644
--- a/frontend/src/features/Administration/userDetailSlice.tsx
+++ b/frontend/src/features/Administration/userDetailSlice.tsx
@@ -1,14 +1,15 @@
 import { createSlice } from '@reduxjs/toolkit'
 import {PermissionDto, UserDto} from '../../swagger/data-contracts'
-import {fetchUsers} from './userDetailThunks'
+import {deleteUser, fetchUsers, savePermissions} from './userDetailThunks'
 import {number} from "yup"
 
 export interface UsersDetailState {
     users: UserDto[] // list of all fetched items
     loading: boolean // whether the users are loading
-    error?: string,
-    selectedUser?: UserDto,
+    error?: string
+    selectedUser?: UserDto
     permissions?: PermissionDto
+    isRequestCompleted: boolean
 }
 
 const initialState: UsersDetailState = {
@@ -16,7 +17,8 @@ const initialState: UsersDetailState = {
     loading: true,
     error: undefined,
     selectedUser: undefined,
-    permissions: undefined
+    permissions: undefined,
+    isRequestCompleted: false
 }
 
 const usersDetailSlice = createSlice({
@@ -34,6 +36,10 @@ const usersDetailSlice = createSlice({
             ...state,
             permissions: action.payload
         }),
+        resetIsRequestCompleted: (state) => ({
+            ...state,
+            isRequestCompleted: false
+        }),
     },
     extraReducers: (builder) => {
         builder.addCase(fetchUsers.pending, (state) => ({
@@ -50,6 +56,14 @@ const usersDetailSlice = createSlice({
             loading: false,
             error: action.error.message as string,
         }))
+        builder.addCase(savePermissions.fulfilled, (state, action) => ({
+            ...state,
+            isRequestCompleted: true,
+        }))
+        builder.addCase(deleteUser.fulfilled, (state, action) => ({
+            ...state,
+            isRequestCompleted: true,
+        }))
     },
 })
 
@@ -59,6 +73,7 @@ export const {
     consumeError,
     setSelectedUser,
     setSelectedUserPermissions,
+    resetIsRequestCompleted
 } = usersDetailSlice.actions
 const usersDetailReducer = usersDetailSlice.reducer
 export default usersDetailReducer
diff --git a/frontend/src/features/Administration/userDetailThunks.tsx b/frontend/src/features/Administration/userDetailThunks.tsx
index 02d4c73..9058d39 100644
--- a/frontend/src/features/Administration/userDetailThunks.tsx
+++ b/frontend/src/features/Administration/userDetailThunks.tsx
@@ -59,3 +59,25 @@ export const savePermissions = createAsyncThunk(
         }
     }
 )
+
+export const deleteUser = createAsyncThunk(
+    'usersDetail/deleteUser',
+    async (dispatch, { getState }) => {
+        const { usersDetail } = getState() as { usersDetail: UsersDetailState }
+        console.log("savePermissions called")
+
+        const selectedUser = usersDetail.selectedUser
+
+        if (selectedUser === undefined) {
+            return Promise.reject("User is undefined")
+        }
+        const { data, status } = await axiosInstance.delete(
+            `/users/${selectedUser?.email}`
+        )
+        if (status !== 200) {
+            // TODO dodělat zpracování erroru
+            return Promise.reject("Deleting user failed")
+        }
+        return status
+    }
+)
