Revize 37f6ff02
Přidáno uživatelem Václav Honzík před téměř 3 roky(ů)
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
re #9368 - redux slice for auth + axios interceptors start