commit fc79a8cbe518d9b4b1d2e6fd22f47add7cef0453
Author: Vaclav Honzik <vaclavhonzik98@gmail.com>
Date:   Fri Apr 1 21:29:27 2022 +0200

    re #9368 jwt simple login

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 9d17e56..299a751 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
@@ -17,6 +17,9 @@ import org.springframework.security.config.annotation.web.configuration.WebSecur
 import org.springframework.security.config.http.SessionCreationPolicy;
 import org.springframework.security.core.userdetails.UserDetailsService;
 import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
+import org.springframework.web.cors.CorsConfiguration;
+import org.springframework.web.cors.CorsConfigurationSource;
+import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
 
 /**
  * Security config class
@@ -54,8 +57,9 @@ public class SecurityConfig extends WebSecurityConfigurerAdapter {
      */
     @Override
     protected void configure(HttpSecurity http) throws Exception {
-        http.
-                csrf().disable()
+        http.csrf().disable()
+                .cors()
+                .and()
                 .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                 .and()
                 .addFilter(new JwtUsernameAndPasswordAuthenticationFilter(authenticationManager(), jwtUtils))
@@ -69,7 +73,6 @@ public class SecurityConfig extends WebSecurityConfigurerAdapter {
                 .antMatchers("/delete/**").hasAuthority(Permission.DELETE.name())
                 .anyRequest()
                 .authenticated();
-
     }
 
     /**
@@ -95,4 +98,11 @@ public class SecurityConfig extends WebSecurityConfigurerAdapter {
         provider.setPasswordEncoder(bCryptPasswordEncoder);
         return provider;
     }
+
+    @Bean
+    CorsConfigurationSource corsConfigurationSource() {
+        final UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
+        source.registerCorsConfiguration("/**", new CorsConfiguration().applyPermitDefaultValues());
+        return source;
+    }
 }
diff --git a/frontend/package.json b/frontend/package.json
index 86b6706..057b181 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -13,6 +13,7 @@
     "axios": "^0.26.0",
     "dotenv": "^16.0.0",
     "formik": "^2.2.9",
+    "jwt-decode": "^3.1.2",
     "react": "^17.0.2",
     "react-dom": "^17.0.2",
     "react-redux": "^7.2.6",
diff --git a/frontend/src/features/Auth/Login.tsx b/frontend/src/features/Auth/Login.tsx
index 25c63b4..5effcc9 100644
--- a/frontend/src/features/Auth/Login.tsx
+++ b/frontend/src/features/Auth/Login.tsx
@@ -5,7 +5,6 @@ import { useDispatch, useSelector } from 'react-redux'
 import { useNavigate } from 'react-router-dom'
 import * as yup from 'yup'
 import { SchemaOf } from 'yup'
-import { PasswordDto, UserDto } from '../../swagger/data-contracts'
 import { RootState } from '../redux/store'
 import { logIn } from './userThunks'
 
@@ -30,12 +29,9 @@ const Login = () => {
         onSubmit: () => {
             dispatch(
                 logIn({
-                    name: formik.values.username,
-                    passwords: {
-                        password: formik.values.password,
-                        confirmationPassword: '',
-                    } as PasswordDto,
-                } as UserDto)
+                    username: formik.values.username,
+                    password: formik.values.password,
+                })
             )
         },
     })
diff --git a/frontend/src/features/Auth/userSlice.ts b/frontend/src/features/Auth/userSlice.ts
index 4b9bb5d..acb7a65 100644
--- a/frontend/src/features/Auth/userSlice.ts
+++ b/frontend/src/features/Auth/userSlice.ts
@@ -41,16 +41,27 @@ export const userSlice = createSlice({
             ...state,
             lastErr: action.payload,
         }),
+        setUserState: (state, action) => {
+            console.log('user state set')
+            return ({ ...state, ...action.payload })
+        },
     },
 
     // Thunks
     extraReducers: (builder) => {
-        builder.addCase(logIn.fulfilled, () => {
-            console.log('Action performed') // TODO remove
-        }) // TODO funny
+        builder.addCase(logIn.fulfilled, (state, action) => {
+            return ({ ...state, ...action.payload })
+        })
+        builder.addCase(logIn.rejected, (state, action) => {
+            if (action.payload && typeof action.payload === 'string') {
+                return ({ ...state, lastErr: action.payload })
+            }
+        })
     },
 })
 
 const userReducer = persistReducer(persistConfig, userSlice.reducer)
 
+export const { logout, refreshTokens, setErr, setUserState } = userSlice.actions
+
 export default userReducer
diff --git a/frontend/src/features/Auth/userThunks.ts b/frontend/src/features/Auth/userThunks.ts
index afe5191..813f254 100644
--- a/frontend/src/features/Auth/userThunks.ts
+++ b/frontend/src/features/Auth/userThunks.ts
@@ -1,33 +1,54 @@
 import { createAsyncThunk } from '@reduxjs/toolkit'
 import axiosInstance from '../../api/api'
 import { UserDto } from '../../swagger/data-contracts'
+import { setErr, setUserState, UserState } from './userSlice'
+import jwt from 'jwt-decode' 
+import { RootState } from '../redux/store'
 
 const loginError =
     'Server error occurred while logging in. Please contact help service to resolve this issue or try again later.'
 
+// This is not present in the swagger since spring generates
+export interface UserLogin {
+    username: string,
+    password: string
+}
 
 export const logIn = createAsyncThunk(
     'user/login',
-    async (userDto: UserDto, { dispatch, getState }) => {
+    async (userDto: UserLogin, { dispatch, getState }) => {
         try {
             // @ts-ignore
-            // TODO fix
             if (getState().user.isLoggedIn) {
-                return
+                return Promise.reject(undefined)
             }
-            console.log('Dispatching login thunk') // TODO remove
+
             const { data, status } = await axiosInstance.post('/login', userDto)
-            const { accessToken, refreshToken } = data
-            console.log(data) // TODO remove
+            const [ accessToken, refreshToken ] = [data.access_token, data.refresh_token]
             if (status !== 200) {
                 // TODO read API err
-                dispatch({ type: 'user/setErr', payload: loginError })
-                return
+                return Promise.reject(loginError)
+            }
+
+            // Strip bearer from access token
+            const userInfo = jwt(accessToken.replace('Bearer ', '')) as any
+            const { sub, authorities } = userInfo
+            if (!sub || !authorities) {
+                return Promise.reject(loginError)
             }
 
-            dispatch({ type: 'user/refreshTokens', payload: { accessToken, refreshToken } })
+            const userState: UserState = {
+                accessToken,
+                refreshToken,
+                username: sub,
+                roles: authorities,
+                isLoggedIn: true
+            }
+            
+            return userState
         } catch (err: any) {
-            dispatch({ type: 'user/setErr', payload: loginError })
+            console.log(err)
+            return Promise.reject(loginError)
         }
     }
 )
diff --git a/frontend/src/features/Home/Home.tsx b/frontend/src/features/Home/Home.tsx
index 8a4ee2d..a4cf3c5 100644
--- a/frontend/src/features/Home/Home.tsx
+++ b/frontend/src/features/Home/Home.tsx
@@ -1,8 +1,30 @@
+import { Button } from '@mui/material'
+import { useDispatch, useSelector } from 'react-redux'
+import { logout } from '../Auth/userSlice'
+import { RootState } from '../redux/store'
+
 const Home = () => {
+    const dispatch = useDispatch()
+
+    const userLoggedIn = useSelector(
+        (state: RootState) => state.user.isLoggedIn
+    )
 
-    return (<>
-    <h1>Home</h1>
-    </>)
+    return (
+        <>
+            <h1>Home</h1>
+            {userLoggedIn ? (
+                <Button
+                    size="large"
+                    variant="contained"
+                    color="primary"
+                    onClick={() => dispatch(logout())}
+                >
+                    Logout
+                </Button>
+            ) : null}
+        </>
+    )
 }
 
-export default Home
\ No newline at end of file
+export default Home
diff --git a/frontend/src/utils/ApiCallError.ts b/frontend/src/utils/ApiCallError.ts
deleted file mode 100644
index b3cf28b..0000000
--- a/frontend/src/utils/ApiCallError.ts
+++ /dev/null
@@ -1 +0,0 @@
-export default class ApiCallError extends Error {}
diff --git a/frontend/src/utils/thunkResponse.ts b/frontend/src/utils/thunkResponse.ts
new file mode 100644
index 0000000..400726e
--- /dev/null
+++ b/frontend/src/utils/thunkResponse.ts
@@ -0,0 +1,5 @@
+
+export default interface ThunkResponse<T> {
+    success: boolean
+    data?: T
+}
\ No newline at end of file
