Projekt

Obecné

Profil

« Předchozí | Další » 

Revize 37f6ff02

Přidáno uživatelem Václav Honzík před téměř 3 roky(ů)

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

Zobrazit rozdíly:

backend/src/main/java/cz/zcu/kiv/backendapi/StubController.java
1
package cz.zcu.kiv.backendapi;
2

  
3
import org.springframework.web.bind.annotation.RestController;
4

  
5
import java.util.logging.Logger;
6

  
7
@RestController()
8
public class StubController {
9

  
10
    public static final Logger LOGGER = Logger.getLogger(StubController.class.getName());
11

  
12
    static {
13
        LOGGER.info("StubController initialized!");
14
    }
15

  
16
//    @GetMapping("/stub")
17
//    public StubDto getStubDto() {
18
//        return new StubDto("Hello API", true);
19
//    }
20
}
backend/src/main/java/cz/zcu/kiv/backendapi/StubDto.java
1
package cz.zcu.kiv.backendapi;
2

  
3
//
4
//import lombok.AllArgsConstructor;
5
//public record StubDto(String message, Boolean success) {}
frontend/package.json
9 9
    "@faker-js/faker": "^6.0.0",
10 10
    "@mui/icons-material": "^5.5.1",
11 11
    "@mui/material": "^5.5.2",
12
    "@reduxjs/toolkit": "^1.8.1",
12 13
    "axios": "^0.26.0",
13 14
    "dotenv": "^16.0.0",
14 15
    "formik": "^2.2.9",
......
19 20
    "react-scripts": "5.0.0",
20 21
    "redux": "^4.1.2",
21 22
    "redux-persist": "^6.0.0",
23
    "redux-thunk": "^2.4.1",
22 24
    "swagger-typescript-api": "^9.3.1",
23 25
    "ts-node": "^10.7.0",
24 26
    "typescript": "^4.4.2",
......
50 52
    ]
51 53
  },
52 54
  "devDependencies": {
53
    "@types/react-redux": "^7.1.23",
54
    "@types/redux-persist": "^4.3.1",
55 55
    "@testing-library/jest-dom": "^5.14.1",
56 56
    "@testing-library/react": "^12.0.0",
57 57
    "@testing-library/user-event": "^13.2.1",
58 58
    "@types/jest": "^27.0.1",
59 59
    "@types/node": "^16.7.13",
60 60
    "@types/react": "^17.0.20",
61
    "@types/react-dom": "^17.0.9"
61
    "@types/react-dom": "^17.0.9",
62
    "@types/react-redux": "^7.1.23",
63
    "@types/redux-persist": "^4.3.1"
62 64
  }
63 65
}
frontend/src/api/api.ts
1
import axios from 'axios'
2
import { Store } from 'redux'
3
import config from '../config/conf'
4
import { AppStore } from '../features/redux/store'
5

  
6
let store: AppStore // this will get injected later
7

  
8
export const injectStore = (_store: Store) => {
9
    store = _store
10
}
11

  
12
// Error
13
export interface ApiError {}
14

  
15
const createBaseInstance = () =>
16
    axios.create({
17
        baseURL: config.baseUrl,
18
    })
19

  
20
const axiosInstance = createBaseInstance()
21

  
22
axiosInstance.interceptors.request.use(
23
    (config) => {
24
        const accessToken = store.getState().user.accessToken // get the access token from the store
25
        if (accessToken) {
26
            // @ts-ignore
27
            // this will always be defined, axios developers are just lazy to provide better Typescript types
28
            config.headers['Authorization'] = accessToken as string
29
        }
30

  
31
        return config
32
    },
33
    (err) => {
34
        Promise.reject(err)
35
    }
36
)
37

  
38
axiosInstance.interceptors.response.use(
39
    (res) => res,
40
    async (err) => {
41

  
42
        const originalConfig = err.config
43

  
44
        // Original URL might be login in which case we don't want to refresh the access token
45
        // Since the user just failed to log in and no token expired
46
        if (originalConfig.url === '/login' || !err.response) {
47
            return Promise.reject(err)
48
        }
49

  
50
        // We need to set the refresh token in the auth header
51
        const oldRefreshToken = store.getState().user.refreshToken
52

  
53
        // If there is no refresh token we simply log the user out
54
        if (!oldRefreshToken) {
55
            store.dispatch({ type: 'user/logout' })
56
            return Promise.reject(err)
57
        }
58

  
59
        // Set this to retry the request that failed
60
        originalConfig.retry = true
61

  
62
        // Try refreshing the JWT
63
        try {
64
            const refreshConfig = createBaseInstance()
65
            // @ts-ignore
66
            refreshConfig.headers['Authorization'] = oldRefreshToken as string
67

  
68
            // Send the request
69
            const { data } = await axiosInstance.get('/refreshToken')
70

  
71
            const { accessToken, refreshToken } = data
72

  
73
            // Set the new tokens
74
            store.dispatch({
75
                type: 'user/refreshTokens',
76
                payload: { accessToken, refreshToken },
77
            })
78

  
79
            return axiosInstance(originalConfig)
80
        } catch (err: any) {
81
            // If the refresh token fails we log the user out
82
            store.dispatch({ type: 'user/logout' })
83
            return Promise.reject(err)
84
        }
85
    }
86
)
87

  
88
export default axiosInstance
frontend/src/api/axiosInstance.ts
1
import axios from 'axios'
2
import { Store } from 'redux'
3
import config from '../config/conf'
4
import { AppStore } from '../features/redux/store'
5

  
6
let store: AppStore // this will get injected later
7

  
8
export const injectStore = (_store: Store) => {
9
    store = _store
10
}
11

  
12
const axiosInstance = axios.create({
13
    baseURL: config.baseUrl,
14
})
15

  
16
axiosInstance.interceptors.request.use(config => {
17
    config.headers = {
18
        ...config.headers,
19
        Authorization: store.getState().user.accessToken ?? ''
20
    }
21
})
22

  
23
axiosInstance.interceptors.response.use(response => response, error => {
24

  
25
    if (error?.response?.status === 401) {
26
        const refreshToken = store.getState().user.refreshToken
27
        // TODO send the refresh token correctly
28
        console.log('401 called they want their token refreshed');
29
    }
30

  
31
})
32

  
33
export default axiosInstance
frontend/src/features/Auth/userReducer.ts
10 10
    isLoggedIn: boolean
11 11
}
12 12

  
13
const initialState: UserState = {
14
    roles: [],
15
    isLoggedIn: false,
16
    username: '',
17
}
18

  
19 13
// All possible actions
20 14
export enum AuthStateActions {
21 15
    LOG_IN = 'LOG_IN',
......
31 25
    storage
32 26
}
33 27

  
28
const initialState: UserState = {
29
    roles: [],
30
    isLoggedIn: false,
31
    username: '',
32
}
33

  
34

  
34 35
const _authReducer = (
35 36
    state: UserState = initialState,
36 37
    action: AnyAction
frontend/src/features/Auth/userSlice.ts
1
import { createSlice, PayloadAction } from '@reduxjs/toolkit'
2
import { persistReducer } from 'redux-persist'
3
import storage from 'redux-persist/lib/storage'
4

  
5
export interface UserState {
6
    accessToken?: string
7
    refreshToken?: string
8
    username: string
9
    roles: string[]
10
    isLoggedIn: boolean
11
}
12

  
13
const persistConfig = {
14
    key: 'auth',
15
    storage, // localStorage for browsers
16
}
17

  
18
// Default state when user first starts the application
19
const initialState: UserState = {
20
    roles: [],
21
    isLoggedIn: false,
22
    username: '',
23
}
24

  
25
export const userSlice = createSlice({
26
    name: 'user', // name to generate action types
27

  
28
    initialState, // default state
29

  
30
    // Reducers that update the state
31
    reducers: {
32
        logout: () => {
33
            return initialState // Reset to the inital state
34
        },
35
        refreshTokens: (state, action) => {
36
            return {
37
                ...state,
38
                accessToken: action.payload.accessToken,
39
                refreshToken: action.payload.refreshToken,
40
            }
41
        },
42
    },
43

  
44
    // For thunks (async operations)
45
    extraReducers: {},
46
})
47

  
48
const userReducer = persistReducer(persistConfig, userSlice.reducer)
49
export default userReducer
frontend/src/features/Auth/userThunks.ts
1
import { createAsyncThunk } from '@reduxjs/toolkit'
2
import axiosInstance from '../../api/api'
3
import { UserDto } from '../../swagger/data-contracts'
4

  
5
export interface RegisterUser {
6
    username: string
7
    email: string
8
    password: string
9
    passwordRepeat: string
10
}
11
export const registerUser = createAsyncThunk(
12
    'users/register',
13
    async (registerUser: UserDto) => {
14
        try {
15
            const { data } = await axiosInstance.post(
16
                '/users/register',
17
                registerUser
18
            )
19
        } catch (error: any) {
20

  
21
        }
22
    }
23
)
frontend/src/features/redux/store.ts
1 1

  
2
import { combineReducers, createStore } from 'redux'
2
import { applyMiddleware, combineReducers, createStore } from 'redux'
3 3
import { persistStore } from 'redux-persist'
4
import thunk from 'redux-thunk'
4 5
import userReducer from '../Auth/userReducer'
5 6
import themeReducer from '../Theme/themeReducer'
6 7

  
7 8
// Store holds shared state in the application
8 9
const store = createStore(
9 10
    combineReducers({ user: userReducer, theme: themeReducer }),
10
    {}
11
    applyMiddleware(thunk) // Thunk middleware so we can async fetch data from the api
11 12
)
12 13

  
13 14
export default store
frontend/src/index.tsx
6 6
import { BrowserRouter } from 'react-router-dom'
7 7
import store, { persistor } from './features/redux/store'
8 8
import { Provider } from 'react-redux'
9
import { injectStore } from './api/axiosInstance'
9
import { injectStore } from './api/api'
10 10
import { PersistGate } from 'redux-persist/integration/react'
11 11

  
12 12
// Injects store to the axios instance in ./api/axiosInstance

Také k dispozici: Unified diff