Projekt

Obecné

Profil

« Předchozí | Další » 

Revize a69da1e3

Přidáno uživatelem Václav Honzík před více než 2 roky(ů)

re #9367 - redux-persist for data persistence

Zobrazit rozdíly:

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
    "@testing-library/jest-dom": "^5.14.1",
13
    "@testing-library/react": "^12.0.0",
14
    "@testing-library/user-event": "^13.2.1",
15
    "@types/jest": "^27.0.1",
16
    "@types/node": "^16.7.13",
17
    "@types/react": "^17.0.20",
18
    "@types/react-dom": "^17.0.9",
19 12
    "axios": "^0.26.0",
20 13
    "dotenv": "^16.0.0",
21 14
    "formik": "^2.2.9",
......
25 18
    "react-router-dom": "^6.2.2",
26 19
    "react-scripts": "5.0.0",
27 20
    "redux": "^4.1.2",
21
    "redux-persist": "^6.0.0",
28 22
    "swagger-typescript-api": "^9.3.1",
29 23
    "ts-node": "^10.7.0",
30 24
    "typescript": "^4.4.2",
......
54 48
      "last 1 firefox version",
55 49
      "last 1 safari version"
56 50
    ]
51
  },
52
  "devDependencies": {
53
    "@types/react-redux": "^7.1.23",
54
    "@types/redux-persist": "^4.3.1",
55
    "@testing-library/jest-dom": "^5.14.1",
56
    "@testing-library/react": "^12.0.0",
57
    "@testing-library/user-event": "^13.2.1",
58
    "@types/jest": "^27.0.1",
59
    "@types/node": "^16.7.13",
60
    "@types/react": "^17.0.20",
61
    "@types/react-dom": "^17.0.9"
57 62
  }
58 63
}
frontend/src/api/axiosInstance.ts
16 16
axiosInstance.interceptors.request.use(config => {
17 17
    config.headers = {
18 18
        ...config.headers,
19
        Authorization: store.getState().auth.token
19
        Authorization: store.getState().user.accessToken ?? ''
20 20
    }
21 21
})
22 22

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

  
25 25
    if (error?.response?.status === 401) {
26
        const refreshToken = store.getState().auth.refreshToken
26
        const refreshToken = store.getState().user.refreshToken
27 27
        // TODO send the refresh token correctly
28 28
        console.log('401 called they want their token refreshed');
29 29
    }
frontend/src/features/Auth/authReducer.ts
1
import { AnyAction } from 'redux'
2
import { persist, load } from '../../utils/statePersistence'
3

  
4
export interface UserInfo {
5
    accessToken: string | undefined // to send api requests
6
    refreshToken: string | undefined // to refresh the api key
7
    username: string // to display the username
8
    roles: string[]
9
}
10

  
11
export interface AuthState extends UserInfo {
12
    isAuthenticated: boolean // if this is false all other values should be ignored
13
}
14

  
15
const statePersistName = 'auth'
16

  
17
// Initial state when the user first starts the application
18
const initialState: AuthState = (load(statePersistName) as AuthState) || {
19
    isAuthenticated: false,
20
    username: '',
21
    roles: [],
22
}
23

  
24
// All possible actions
25
export enum AuthStateActions {
26
    LOG_IN = 'LOG_IN',
27
    LOG_OUT = 'LOG_OUT',
28
    UPDATE_ACCESS_TOKEN = 'REFRESH_ACCESS_TOKEN',
29
    UPDATE_REFRESH_TOKEN = 'UPDATE_REFRESH_TOKEN',
30
    UPDATE_TOKENS = 'UPDATE_TOKENS',
31
}
32

  
33
// Actions
34
const authReducer = (state: AuthState = initialState, action: AnyAction) => {
35
    switch (action.type) {
36
        case AuthStateActions.LOG_IN:
37
            return persist(statePersistName, {
38
                ...action.payload,
39
                isAuthenticated: true,
40
            })
41

  
42
        case AuthStateActions.LOG_OUT:
43
            return persist(statePersistName, initialState)
44

  
45
        case AuthStateActions.UPDATE_ACCESS_TOKEN:
46
            return persist(statePersistName, {
47
                ...state,
48
                accessToken: action.payload,
49
            })
50

  
51
        case AuthStateActions.UPDATE_REFRESH_TOKEN:
52
            return persist(statePersistName, {
53
                ...state,
54
                refreshToken: action.payload,
55
            })
56

  
57
        case AuthStateActions.UPDATE_TOKENS:
58
            return persist(statePersistName, { ...state, ...action.payload })
59

  
60
        default:
61
            return state
62
    }
63
}
64

  
65
export default authReducer
frontend/src/features/Auth/userReducer.ts
1
import { AnyAction } from 'redux'
2
import persistReducer from 'redux-persist/es/persistReducer'
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 initialState: UserState = {
14
    roles: [],
15
    isLoggedIn: false,
16
    username: '',
17
}
18

  
19
// All possible actions
20
export enum AuthStateActions {
21
    LOG_IN = 'LOG_IN',
22
    LOG_OUT = 'LOG_OUT',
23
    UPDATE_ACCESS_TOKEN = 'REFRESH_ACCESS_TOKEN',
24
    UPDATE_REFRESH_TOKEN = 'UPDATE_REFRESH_TOKEN',
25
    UPDATE_TOKENS = 'UPDATE_TOKENS',
26
}
27

  
28
// Only needed if the state is to be persisted
29
const persistConfig = {
30
    key: 'auth',
31
    storage
32
}
33

  
34
const _authReducer = (
35
    state: UserState = initialState,
36
    action: AnyAction
37
): UserState => {
38
    switch (action.type) {
39
        case AuthStateActions.LOG_IN:
40
            return {
41
                ...action.payload,
42
                isAuthenticated: true,
43
            }
44
        case AuthStateActions.LOG_OUT:
45
            return initialState
46

  
47
        case AuthStateActions.UPDATE_ACCESS_TOKEN:
48
            return {
49
                ...state,
50
                accessToken: action.payload,
51
            }
52

  
53
        case AuthStateActions.UPDATE_REFRESH_TOKEN:
54
            return {
55
                ...state,
56
                refreshToken: action.payload,
57
            }
58

  
59
        case AuthStateActions.UPDATE_TOKENS:
60
            return { ...state, ...action.payload }
61

  
62
        default:
63
            return state
64
    }
65
}
66

  
67
const authReducer = persistReducer(persistConfig, _authReducer)
68

  
69
export default authReducer
frontend/src/features/Catalog/Catalog.tsx
8 8
    Stack,
9 9
    TextField,
10 10
} from '@mui/material'
11
import { CatalogDto } from '../../swagger/data-contracts'
11
import { CatalogItemDto } from '../../swagger/data-contracts'
12 12
import CatalogTable from './CatalogTable'
13 13
import { faker } from '@faker-js/faker'
14 14
import { useState } from 'react'
......
20 20
    }
21 21

  
22 22
    // Creates a generic row for the table
23
    const createRow = (): CatalogDto => ({
23
    const createRow = (): CatalogItemDto => ({
24 24
        name: faker.commerce.product(),
25 25
        certainty: faker.random.number({ min: 1, max: 1000 }),
26 26
        longitude: faker.address.latitude() as unknown as number,
......
31 31
        countries: [faker.address.country()],
32 32
        alternativeNames: [faker.commerce.productName()],
33 33
    })
34
    const data: CatalogDto[] = Array(33)
34
    const data: CatalogItemDto[] = Array(33)
35 35
        .fill({})
36 36
        .map(() => createRow())
37 37

  
frontend/src/features/Catalog/CatalogTable.tsx
2 2
import {
3 3
    Box,
4 4
    Paper,
5
    Stack,
6 5
    Table,
7 6
    TableBody,
8 7
    TableCell,
9 8
    TableContainer,
10
    TableFooter,
11 9
    TableHead,
12 10
    TablePagination,
13 11
    TableRow,
14 12
    Typography,
15 13
} from '@mui/material'
16
import TablePaginationActions, {
14
import {
17 15
    TablePaginationActionsProps,
18 16
} from '@mui/material/TablePagination/TablePaginationActions'
19 17
import { FunctionComponent, useState } from 'react'
......
22 20
import KeyboardArrowLeft from '@mui/icons-material/KeyboardArrowLeft'
23 21
import KeyboardArrowRight from '@mui/icons-material/KeyboardArrowRight'
24 22
import LastPageIcon from '@mui/icons-material/LastPage'
25
import { CatalogDto } from '../../swagger/data-contracts'
23
import { CatalogItemDto } from '../../swagger/data-contracts'
26 24

  
27 25
// Currently adapted from https://mui.com/components/tables/
28 26

  
......
129 127
].sort((a, b) => (a.calories < b.calories ? -1 : 1))
130 128

  
131 129
export interface CatalogTableProps {
132
    data: CatalogDto[]
130
    data: CatalogItemDto[]
133 131
}
134 132

  
135 133
const CatalogTable: FunctionComponent<CatalogTableProps> = ({ data }) => {
frontend/src/features/redux/store.ts
1
// Store that holds the state of the application
2 1

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

  
7
// Store holds shared state in the application
7 8
const store = createStore(
8
    combineReducers({ auth: authReducer, theme: themeReducer }),
9
    combineReducers({ user: userReducer, theme: themeReducer }),
9 10
    {}
10 11
)
11 12

  
12 13
export default store
13

  
14
export const persistor = persistStore(store)
14 15
export type AppStore = typeof store
15

  
16 16
export type RootState = ReturnType<typeof store.getState>
17

  
18 17
export type AppDispatch = typeof store.dispatch
frontend/src/index.tsx
4 4
import App from './App'
5 5
import reportWebVitals from './reportWebVitals'
6 6
import { BrowserRouter } from 'react-router-dom'
7
import store from './features/redux/store'
7
import store, { persistor } from './features/redux/store'
8 8
import { Provider } from 'react-redux'
9 9
import { injectStore } from './api/axiosInstance'
10
import { PersistGate } from 'redux-persist/integration/react'
10 11

  
11 12
// Injects store to the axios instance in ./api/axiosInstance
12 13
injectStore(store)
13 14

  
14 15
ReactDOM.render(
15 16
    <Provider store={store}>
16
        <React.StrictMode>
17
            <BrowserRouter>
18
                <App />
19
            </BrowserRouter>
20
        </React.StrictMode>
17
        <PersistGate loading={null} persistor={persistor}>
18
            <React.StrictMode>
19
                <BrowserRouter>
20
                    <App />
21
                </BrowserRouter>
22
            </React.StrictMode>
23
        </PersistGate>
21 24
    </Provider>,
22 25
    document.getElementById('root')
23 26
)
frontend/src/swagger/Catalog.ts
9 9
 * ---------------------------------------------------------------
10 10
 */
11 11

  
12
import { CatalogDto } from "./data-contracts";
12
import { CatalogItemDto } from "./data-contracts";
13 13
import { ContentType, HttpClient, RequestParams } from "./http-client";
14 14

  
15 15
export class Catalog<SecurityDataType = unknown> extends HttpClient<SecurityDataType> {
......
20 20
   * @name UpdateCatalogEntry
21 21
   * @request PUT:/catalog/{id}
22 22
   */
23
  updateCatalogEntry = (id: string, data: CatalogDto, params: RequestParams = {}) =>
23
  updateCatalogEntry = (id: string, data: CatalogItemDto, params: RequestParams = {}) =>
24 24
    this.request<void, any>({
25 25
      path: `/catalog/${id}`,
26 26
      method: "PUT",
......
49 49
   * @request GET:/catalog
50 50
   */
51 51
  getAllUsers1 = (params: RequestParams = {}) =>
52
    this.request<CatalogDto[], any>({
52
    this.request<CatalogItemDto[], any>({
53 53
      path: `/catalog`,
54 54
      method: "GET",
55 55
      ...params,
......
61 61
   * @name AddCatalogEntry
62 62
   * @request POST:/catalog
63 63
   */
64
  addCatalogEntry = (data: CatalogDto, params: RequestParams = {}) =>
64
  addCatalogEntry = (data: CatalogItemDto, params: RequestParams = {}) =>
65 65
    this.request<void, any>({
66 66
      path: `/catalog`,
67 67
      method: "POST",
frontend/src/swagger/CatalogItems.ts
1
/* eslint-disable */
2
/* tslint:disable */
3
/*
4
 * ---------------------------------------------------------------
5
 * ## THIS FILE WAS GENERATED VIA SWAGGER-TYPESCRIPT-API        ##
6
 * ##                                                           ##
7
 * ## AUTHOR: acacode                                           ##
8
 * ## SOURCE: https://github.com/acacode/swagger-typescript-api ##
9
 * ---------------------------------------------------------------
10
 */
11

  
12
import { CatalogItemDto } from "./data-contracts";
13
import { ContentType, HttpClient, RequestParams } from "./http-client";
14

  
15
export class CatalogItems<SecurityDataType = unknown> extends HttpClient<SecurityDataType> {
16
  /**
17
   * No description
18
   *
19
   * @tags catalog-controller
20
   * @name UpdateCatalogItem
21
   * @summary updates catalog item with given ID
22
   * @request PUT:/catalog-items/{id}
23
   */
24
  updateCatalogItem = (id: string, data: CatalogItemDto, params: RequestParams = {}) =>
25
    this.request<void, any>({
26
      path: `/catalog-items/${id}`,
27
      method: "PUT",
28
      body: data,
29
      type: ContentType.Json,
30
      ...params,
31
    });
32
  /**
33
   * No description
34
   *
35
   * @tags catalog-controller
36
   * @name DeleteCatalogItem
37
   * @summary deletes catalog item with given ID
38
   * @request DELETE:/catalog-items/{id}
39
   */
40
  deleteCatalogItem = (id: string, params: RequestParams = {}) =>
41
    this.request<void, any>({
42
      path: `/catalog-items/${id}`,
43
      method: "DELETE",
44
      ...params,
45
    });
46
  /**
47
   * No description
48
   *
49
   * @tags catalog-controller
50
   * @name GetCatalog
51
   * @summary returns catalog items based on filter
52
   * @request GET:/catalog-items
53
   */
54
  getCatalog = (query?: { name?: string; country?: string; type?: string }, params: RequestParams = {}) =>
55
    this.request<CatalogItemDto[], any>({
56
      path: `/catalog-items`,
57
      method: "GET",
58
      query: query,
59
      ...params,
60
    });
61
  /**
62
   * No description
63
   *
64
   * @tags catalog-controller
65
   * @name AddCatalogItem
66
   * @summary creates new catalog item
67
   * @request POST:/catalog-items
68
   */
69
  addCatalogItem = (data: CatalogItemDto, params: RequestParams = {}) =>
70
    this.request<void, any>({
71
      path: `/catalog-items`,
72
      method: "POST",
73
      body: data,
74
      type: ContentType.Json,
75
      ...params,
76
    });
77
}
frontend/src/swagger/Users.ts
9 9
 * ---------------------------------------------------------------
10 10
 */
11 11

  
12
import { UserDto } from "./data-contracts";
13
import { HttpClient, RequestParams } from "./http-client";
12
import { PasswordDto, PermissionDto, UserDto } from "./data-contracts";
13
import { ContentType, HttpClient, RequestParams } from "./http-client";
14 14

  
15 15
export class Users<SecurityDataType = unknown> extends HttpClient<SecurityDataType> {
16 16
  /**
......
18 18
   *
19 19
   * @tags user-controller
20 20
   * @name GetAllUsers
21
   * @summary returns all users
21 22
   * @request GET:/users
22 23
   */
23 24
  getAllUsers = (params: RequestParams = {}) =>
......
26 27
      method: "GET",
27 28
      ...params,
28 29
    });
30
  /**
31
   * No description
32
   *
33
   * @tags user-controller
34
   * @name RegisterNewUser
35
   * @summary registers new user
36
   * @request POST:/users
37
   */
38
  registerNewUser = (data: UserDto, params: RequestParams = {}) =>
39
    this.request<void, any>({
40
      path: `/users`,
41
      method: "POST",
42
      body: data,
43
      type: ContentType.Json,
44
      ...params,
45
    });
46
  /**
47
   * No description
48
   *
49
   * @tags user-controller
50
   * @name UpdatePermissions
51
   * @summary changes permissions to given user
52
   * @request PATCH:/users/{username}/permissions
53
   */
54
  updatePermissions = (username: string, data: PermissionDto, params: RequestParams = {}) =>
55
    this.request<void, any>({
56
      path: `/users/${username}/permissions`,
57
      method: "PATCH",
58
      body: data,
59
      type: ContentType.Json,
60
      ...params,
61
    });
62
  /**
63
   * No description
64
   *
65
   * @tags user-controller
66
   * @name ResetPassword
67
   * @summary changes password to given user
68
   * @request PATCH:/users/{username}/password
69
   */
70
  resetPassword = (username: string, data: PasswordDto, params: RequestParams = {}) =>
71
    this.request<void, any>({
72
      path: `/users/${username}/password`,
73
      method: "PATCH",
74
      body: data,
75
      type: ContentType.Json,
76
      ...params,
77
    });
78
  /**
79
   * No description
80
   *
81
   * @tags user-controller
82
   * @name ChangePassword
83
   * @summary changes password to logged-in user
84
   * @request PATCH:/users/password
85
   */
86
  changePassword = (query: { oldPassword: string }, data: PasswordDto, params: RequestParams = {}) =>
87
    this.request<void, any>({
88
      path: `/users/password`,
89
      method: "PATCH",
90
      query: query,
91
      body: data,
92
      type: ContentType.Json,
93
      ...params,
94
    });
95
  /**
96
   * No description
97
   *
98
   * @tags user-controller
99
   * @name RefreshToken
100
   * @summary returns a new access token and a refresh token to user
101
   * @request GET:/users/token
102
   */
103
  refreshToken = (params: RequestParams = {}) =>
104
    this.request<void, any>({
105
      path: `/users/token`,
106
      method: "GET",
107
      ...params,
108
    });
109
  /**
110
   * No description
111
   *
112
   * @tags user-controller
113
   * @name DeleteUser
114
   * @summary deletes user with given username
115
   * @request DELETE:/users/{username}
116
   */
117
  deleteUser = (username: string, params: RequestParams = {}) =>
118
    this.request<void, any>({
119
      path: `/users/${username}`,
120
      method: "DELETE",
121
      ...params,
122
    });
29 123
}
frontend/src/swagger/data-contracts.ts
9 9
 * ---------------------------------------------------------------
10 10
 */
11 11

  
12
export interface UserDto {
13
  name?: string;
14
  email: string;
15
  canRead?: boolean;
16
  canWrite?: boolean;
17
  canDelete?: boolean;
18
}
19

  
20
export interface CatalogDto {
12
export interface CatalogItemDto {
21 13
  /** @format uuid */
22 14
  id?: string;
23 15
  name?: string;
......
36 28
  alternativeNames?: string[];
37 29
  types?: string[];
38 30
}
31

  
32
export interface PasswordDto {
33
  password: string;
34
  confirmationPassword: string;
35
}
36

  
37
export interface PermissionDto {
38
  canRead?: boolean;
39
  canWrite?: boolean;
40
  canDelete?: boolean;
41
}
42

  
43
export interface UserDto {
44
  name?: string;
45
  email: string;
46
  permissions?: PermissionDto;
47
  passwords: PasswordDto;
48
}
49

  
50
export interface TitlePage {
51
  title?: string;
52
  content?: string;
53
}

Také k dispozici: Unified diff