commit 37f6ff02a2860e20a60cf171f345c23852d91fb9
Author: Vaclav Honzik <vaclavhonzik98@gmail.com>
Date:   Thu Mar 31 19:40:01 2022 +0200

    re #9368 - redux slice for auth + axios interceptors start

diff --git a/backend/src/main/java/cz/zcu/kiv/backendapi/StubController.java b/backend/src/main/java/cz/zcu/kiv/backendapi/StubController.java
deleted file mode 100644
index b4d9b68..0000000
--- a/backend/src/main/java/cz/zcu/kiv/backendapi/StubController.java
+++ /dev/null
@@ -1,20 +0,0 @@
-package cz.zcu.kiv.backendapi;
-
-import org.springframework.web.bind.annotation.RestController;
-
-import java.util.logging.Logger;
-
-@RestController()
-public class StubController {
-
-    public static final Logger LOGGER = Logger.getLogger(StubController.class.getName());
-
-    static {
-        LOGGER.info("StubController initialized!");
-    }
-
-//    @GetMapping("/stub")
-//    public StubDto getStubDto() {
-//        return new StubDto("Hello API", true);
-//    }
-}
diff --git a/backend/src/main/java/cz/zcu/kiv/backendapi/StubDto.java b/backend/src/main/java/cz/zcu/kiv/backendapi/StubDto.java
deleted file mode 100644
index e37a47d..0000000
--- a/backend/src/main/java/cz/zcu/kiv/backendapi/StubDto.java
+++ /dev/null
@@ -1,5 +0,0 @@
-package cz.zcu.kiv.backendapi;
-
-//
-//import lombok.AllArgsConstructor;
-//public record StubDto(String message, Boolean success) {}
diff --git a/frontend/package.json b/frontend/package.json
index bdb87d0..4f4e989 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -9,6 +9,7 @@
     "@faker-js/faker": "^6.0.0",
     "@mui/icons-material": "^5.5.1",
     "@mui/material": "^5.5.2",
+    "@reduxjs/toolkit": "^1.8.1",
     "axios": "^0.26.0",
     "dotenv": "^16.0.0",
     "formik": "^2.2.9",
@@ -19,6 +20,7 @@
     "react-scripts": "5.0.0",
     "redux": "^4.1.2",
     "redux-persist": "^6.0.0",
+    "redux-thunk": "^2.4.1",
     "swagger-typescript-api": "^9.3.1",
     "ts-node": "^10.7.0",
     "typescript": "^4.4.2",
@@ -50,14 +52,14 @@
     ]
   },
   "devDependencies": {
-    "@types/react-redux": "^7.1.23",
-    "@types/redux-persist": "^4.3.1",
     "@testing-library/jest-dom": "^5.14.1",
     "@testing-library/react": "^12.0.0",
     "@testing-library/user-event": "^13.2.1",
     "@types/jest": "^27.0.1",
     "@types/node": "^16.7.13",
     "@types/react": "^17.0.20",
-    "@types/react-dom": "^17.0.9"
+    "@types/react-dom": "^17.0.9",
+    "@types/react-redux": "^7.1.23",
+    "@types/redux-persist": "^4.3.1"
   }
 }
diff --git a/frontend/src/api/api.ts b/frontend/src/api/api.ts
new file mode 100644
index 0000000..8aaaebc
--- /dev/null
+++ b/frontend/src/api/api.ts
@@ -0,0 +1,88 @@
+import axios from 'axios'
+import { Store } from 'redux'
+import config from '../config/conf'
+import { AppStore } from '../features/redux/store'
+
+let store: AppStore // this will get injected later
+
+export const injectStore = (_store: Store) => {
+    store = _store
+}
+
+// Error
+export interface ApiError {}
+
+const createBaseInstance = () =>
+    axios.create({
+        baseURL: config.baseUrl,
+    })
+
+const axiosInstance = createBaseInstance()
+
+axiosInstance.interceptors.request.use(
+    (config) => {
+        const accessToken = store.getState().user.accessToken // get the access token from the store
+        if (accessToken) {
+            // @ts-ignore
+            // this will always be defined, axios developers are just lazy to provide better Typescript types
+            config.headers['Authorization'] = accessToken as string
+        }
+
+        return config
+    },
+    (err) => {
+        Promise.reject(err)
+    }
+)
+
+axiosInstance.interceptors.response.use(
+    (res) => res,
+    async (err) => {
+
+        const originalConfig = err.config
+
+        // Original URL might be login in which case we don't want to refresh the access token
+        // Since the user just failed to log in and no token expired
+        if (originalConfig.url === '/login' || !err.response) {
+            return Promise.reject(err)
+        }
+
+        // We need to set the refresh token in the auth header
+        const oldRefreshToken = store.getState().user.refreshToken
+
+        // If there is no refresh token we simply log the user out
+        if (!oldRefreshToken) {
+            store.dispatch({ type: 'user/logout' })
+            return Promise.reject(err)
+        }
+
+        // Set this to retry the request that failed
+        originalConfig.retry = true
+
+        // Try refreshing the JWT
+        try {
+            const refreshConfig = createBaseInstance()
+            // @ts-ignore
+            refreshConfig.headers['Authorization'] = oldRefreshToken as string
+
+            // Send the request
+            const { data } = await axiosInstance.get('/refreshToken')
+
+            const { accessToken, refreshToken } = data
+
+            // Set the new tokens
+            store.dispatch({
+                type: 'user/refreshTokens',
+                payload: { accessToken, refreshToken },
+            })
+
+            return axiosInstance(originalConfig)
+        } catch (err: any) {
+            // If the refresh token fails we log the user out
+            store.dispatch({ type: 'user/logout' })
+            return Promise.reject(err)
+        }
+    }
+)
+
+export default axiosInstance
diff --git a/frontend/src/api/axiosInstance.ts b/frontend/src/api/axiosInstance.ts
deleted file mode 100644
index 4b562ba..0000000
--- a/frontend/src/api/axiosInstance.ts
+++ /dev/null
@@ -1,33 +0,0 @@
-import axios from 'axios'
-import { Store } from 'redux'
-import config from '../config/conf'
-import { AppStore } from '../features/redux/store'
-
-let store: AppStore // this will get injected later
-
-export const injectStore = (_store: Store) => {
-    store = _store
-}
-
-const axiosInstance = axios.create({
-    baseURL: config.baseUrl,
-})
-
-axiosInstance.interceptors.request.use(config => {
-    config.headers = {
-        ...config.headers,
-        Authorization: store.getState().user.accessToken ?? ''
-    }
-})
-
-axiosInstance.interceptors.response.use(response => response, error => {
-
-    if (error?.response?.status === 401) {
-        const refreshToken = store.getState().user.refreshToken
-        // TODO send the refresh token correctly
-        console.log('401 called they want their token refreshed');
-    }
-
-})
-
-export default axiosInstance
\ No newline at end of file
diff --git a/frontend/src/features/Auth/userReducer.ts b/frontend/src/features/Auth/userReducer.ts
index 3c86a0a..e82478e 100644
--- a/frontend/src/features/Auth/userReducer.ts
+++ b/frontend/src/features/Auth/userReducer.ts
@@ -10,12 +10,6 @@ export interface UserState {
     isLoggedIn: boolean
 }
 
-const initialState: UserState = {
-    roles: [],
-    isLoggedIn: false,
-    username: '',
-}
-
 // All possible actions
 export enum AuthStateActions {
     LOG_IN = 'LOG_IN',
@@ -31,6 +25,13 @@ const persistConfig = {
     storage
 }
 
+const initialState: UserState = {
+    roles: [],
+    isLoggedIn: false,
+    username: '',
+}
+
+
 const _authReducer = (
     state: UserState = initialState,
     action: AnyAction
diff --git a/frontend/src/features/Auth/userSlice.ts b/frontend/src/features/Auth/userSlice.ts
new file mode 100644
index 0000000..f10b798
--- /dev/null
+++ b/frontend/src/features/Auth/userSlice.ts
@@ -0,0 +1,49 @@
+import { createSlice, PayloadAction } from '@reduxjs/toolkit'
+import { persistReducer } from 'redux-persist'
+import storage from 'redux-persist/lib/storage'
+
+export interface UserState {
+    accessToken?: string
+    refreshToken?: string
+    username: string
+    roles: string[]
+    isLoggedIn: boolean
+}
+
+const persistConfig = {
+    key: 'auth',
+    storage, // localStorage for browsers
+}
+
+// Default state when user first starts the application
+const initialState: UserState = {
+    roles: [],
+    isLoggedIn: false,
+    username: '',
+}
+
+export const userSlice = createSlice({
+    name: 'user', // name to generate action types
+
+    initialState, // default state
+
+    // Reducers that update the state
+    reducers: {
+        logout: () => {
+            return initialState // Reset to the inital state
+        },
+        refreshTokens: (state, action) => {
+            return {
+                ...state,
+                accessToken: action.payload.accessToken,
+                refreshToken: action.payload.refreshToken,
+            }
+        },
+    },
+
+    // For thunks (async operations)
+    extraReducers: {},
+})
+
+const userReducer = persistReducer(persistConfig, userSlice.reducer)
+export default userReducer
diff --git a/frontend/src/features/Auth/userThunks.ts b/frontend/src/features/Auth/userThunks.ts
new file mode 100644
index 0000000..7fe5451
--- /dev/null
+++ b/frontend/src/features/Auth/userThunks.ts
@@ -0,0 +1,23 @@
+import { createAsyncThunk } from '@reduxjs/toolkit'
+import axiosInstance from '../../api/api'
+import { UserDto } from '../../swagger/data-contracts'
+
+export interface RegisterUser {
+    username: string
+    email: string
+    password: string
+    passwordRepeat: string
+}
+export const registerUser = createAsyncThunk(
+    'users/register',
+    async (registerUser: UserDto) => {
+        try {
+            const { data } = await axiosInstance.post(
+                '/users/register',
+                registerUser
+            )
+        } catch (error: any) {
+
+        }
+    }
+)
diff --git a/frontend/src/features/redux/store.ts b/frontend/src/features/redux/store.ts
index d589664..9065061 100644
--- a/frontend/src/features/redux/store.ts
+++ b/frontend/src/features/redux/store.ts
@@ -1,13 +1,14 @@
 
-import { combineReducers, createStore } from 'redux'
+import { applyMiddleware, combineReducers, createStore } from 'redux'
 import { persistStore } from 'redux-persist'
+import thunk from 'redux-thunk'
 import userReducer from '../Auth/userReducer'
 import themeReducer from '../Theme/themeReducer'
 
 // Store holds shared state in the application
 const store = createStore(
     combineReducers({ user: userReducer, theme: themeReducer }),
-    {}
+    applyMiddleware(thunk) // Thunk middleware so we can async fetch data from the api
 )
 
 export default store
diff --git a/frontend/src/index.tsx b/frontend/src/index.tsx
index 47c841f..6d4c989 100644
--- a/frontend/src/index.tsx
+++ b/frontend/src/index.tsx
@@ -6,7 +6,7 @@ import reportWebVitals from './reportWebVitals'
 import { BrowserRouter } from 'react-router-dom'
 import store, { persistor } from './features/redux/store'
 import { Provider } from 'react-redux'
-import { injectStore } from './api/axiosInstance'
+import { injectStore } from './api/api'
 import { PersistGate } from 'redux-persist/integration/react'
 
 // Injects store to the axios instance in ./api/axiosInstance
