Revize fc79a8cb
Přidáno uživatelem Václav Honzík před téměř 3 roky(ů)
backend/src/main/java/cz/zcu/kiv/backendapi/security/SecurityConfig.java | ||
---|---|---|
17 | 17 |
import org.springframework.security.config.http.SessionCreationPolicy; |
18 | 18 |
import org.springframework.security.core.userdetails.UserDetailsService; |
19 | 19 |
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; |
20 |
import org.springframework.web.cors.CorsConfiguration; |
|
21 |
import org.springframework.web.cors.CorsConfigurationSource; |
|
22 |
import org.springframework.web.cors.UrlBasedCorsConfigurationSource; |
|
20 | 23 |
|
21 | 24 |
/** |
22 | 25 |
* Security config class |
... | ... | |
54 | 57 |
*/ |
55 | 58 |
@Override |
56 | 59 |
protected void configure(HttpSecurity http) throws Exception { |
57 |
http. |
|
58 |
csrf().disable() |
|
60 |
http.csrf().disable() |
|
61 |
.cors() |
|
62 |
.and() |
|
59 | 63 |
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS) |
60 | 64 |
.and() |
61 | 65 |
.addFilter(new JwtUsernameAndPasswordAuthenticationFilter(authenticationManager(), jwtUtils)) |
... | ... | |
69 | 73 |
.antMatchers("/delete/**").hasAuthority(Permission.DELETE.name()) |
70 | 74 |
.anyRequest() |
71 | 75 |
.authenticated(); |
72 |
|
|
73 | 76 |
} |
74 | 77 |
|
75 | 78 |
/** |
... | ... | |
95 | 98 |
provider.setPasswordEncoder(bCryptPasswordEncoder); |
96 | 99 |
return provider; |
97 | 100 |
} |
101 |
|
|
102 |
@Bean |
|
103 |
CorsConfigurationSource corsConfigurationSource() { |
|
104 |
final UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); |
|
105 |
source.registerCorsConfiguration("/**", new CorsConfiguration().applyPermitDefaultValues()); |
|
106 |
return source; |
|
107 |
} |
|
98 | 108 |
} |
frontend/package.json | ||
---|---|---|
13 | 13 |
"axios": "^0.26.0", |
14 | 14 |
"dotenv": "^16.0.0", |
15 | 15 |
"formik": "^2.2.9", |
16 |
"jwt-decode": "^3.1.2", |
|
16 | 17 |
"react": "^17.0.2", |
17 | 18 |
"react-dom": "^17.0.2", |
18 | 19 |
"react-redux": "^7.2.6", |
frontend/src/features/Auth/Login.tsx | ||
---|---|---|
5 | 5 |
import { useNavigate } from 'react-router-dom' |
6 | 6 |
import * as yup from 'yup' |
7 | 7 |
import { SchemaOf } from 'yup' |
8 |
import { PasswordDto, UserDto } from '../../swagger/data-contracts' |
|
9 | 8 |
import { RootState } from '../redux/store' |
10 | 9 |
import { logIn } from './userThunks' |
11 | 10 |
|
... | ... | |
30 | 29 |
onSubmit: () => { |
31 | 30 |
dispatch( |
32 | 31 |
logIn({ |
33 |
name: formik.values.username, |
|
34 |
passwords: { |
|
35 |
password: formik.values.password, |
|
36 |
confirmationPassword: '', |
|
37 |
} as PasswordDto, |
|
38 |
} as UserDto) |
|
32 |
username: formik.values.username, |
|
33 |
password: formik.values.password, |
|
34 |
}) |
|
39 | 35 |
) |
40 | 36 |
}, |
41 | 37 |
}) |
frontend/src/features/Auth/userSlice.ts | ||
---|---|---|
41 | 41 |
...state, |
42 | 42 |
lastErr: action.payload, |
43 | 43 |
}), |
44 |
setUserState: (state, action) => { |
|
45 |
console.log('user state set') |
|
46 |
return ({ ...state, ...action.payload }) |
|
47 |
}, |
|
44 | 48 |
}, |
45 | 49 |
|
46 | 50 |
// Thunks |
47 | 51 |
extraReducers: (builder) => { |
48 |
builder.addCase(logIn.fulfilled, () => { |
|
49 |
console.log('Action performed') // TODO remove |
|
50 |
}) // TODO funny |
|
52 |
builder.addCase(logIn.fulfilled, (state, action) => { |
|
53 |
return ({ ...state, ...action.payload }) |
|
54 |
}) |
|
55 |
builder.addCase(logIn.rejected, (state, action) => { |
|
56 |
if (action.payload && typeof action.payload === 'string') { |
|
57 |
return ({ ...state, lastErr: action.payload }) |
|
58 |
} |
|
59 |
}) |
|
51 | 60 |
}, |
52 | 61 |
}) |
53 | 62 |
|
54 | 63 |
const userReducer = persistReducer(persistConfig, userSlice.reducer) |
55 | 64 |
|
65 |
export const { logout, refreshTokens, setErr, setUserState } = userSlice.actions |
|
66 |
|
|
56 | 67 |
export default userReducer |
frontend/src/features/Auth/userThunks.ts | ||
---|---|---|
1 | 1 |
import { createAsyncThunk } from '@reduxjs/toolkit' |
2 | 2 |
import axiosInstance from '../../api/api' |
3 | 3 |
import { UserDto } from '../../swagger/data-contracts' |
4 |
import { setErr, setUserState, UserState } from './userSlice' |
|
5 |
import jwt from 'jwt-decode' |
|
6 |
import { RootState } from '../redux/store' |
|
4 | 7 |
|
5 | 8 |
const loginError = |
6 | 9 |
'Server error occurred while logging in. Please contact help service to resolve this issue or try again later.' |
7 | 10 |
|
11 |
// This is not present in the swagger since spring generates |
|
12 |
export interface UserLogin { |
|
13 |
username: string, |
|
14 |
password: string |
|
15 |
} |
|
8 | 16 |
|
9 | 17 |
export const logIn = createAsyncThunk( |
10 | 18 |
'user/login', |
11 |
async (userDto: UserDto, { dispatch, getState }) => {
|
|
19 |
async (userDto: UserLogin, { dispatch, getState }) => {
|
|
12 | 20 |
try { |
13 | 21 |
// @ts-ignore |
14 |
// TODO fix |
|
15 | 22 |
if (getState().user.isLoggedIn) { |
16 |
return |
|
23 |
return Promise.reject(undefined)
|
|
17 | 24 |
} |
18 |
console.log('Dispatching login thunk') // TODO remove |
|
25 |
|
|
19 | 26 |
const { data, status } = await axiosInstance.post('/login', userDto) |
20 |
const { accessToken, refreshToken } = data |
|
21 |
console.log(data) // TODO remove |
|
27 |
const [ accessToken, refreshToken ] = [data.access_token, data.refresh_token] |
|
22 | 28 |
if (status !== 200) { |
23 | 29 |
// TODO read API err |
24 |
dispatch({ type: 'user/setErr', payload: loginError }) |
|
25 |
return |
|
30 |
return Promise.reject(loginError) |
|
31 |
} |
|
32 |
|
|
33 |
// Strip bearer from access token |
|
34 |
const userInfo = jwt(accessToken.replace('Bearer ', '')) as any |
|
35 |
const { sub, authorities } = userInfo |
|
36 |
if (!sub || !authorities) { |
|
37 |
return Promise.reject(loginError) |
|
26 | 38 |
} |
27 | 39 |
|
28 |
dispatch({ type: 'user/refreshTokens', payload: { accessToken, refreshToken } }) |
|
40 |
const userState: UserState = { |
|
41 |
accessToken, |
|
42 |
refreshToken, |
|
43 |
username: sub, |
|
44 |
roles: authorities, |
|
45 |
isLoggedIn: true |
|
46 |
} |
|
47 |
|
|
48 |
return userState |
|
29 | 49 |
} catch (err: any) { |
30 |
dispatch({ type: 'user/setErr', payload: loginError }) |
|
50 |
console.log(err) |
|
51 |
return Promise.reject(loginError) |
|
31 | 52 |
} |
32 | 53 |
} |
33 | 54 |
) |
frontend/src/features/Home/Home.tsx | ||
---|---|---|
1 |
import { Button } from '@mui/material' |
|
2 |
import { useDispatch, useSelector } from 'react-redux' |
|
3 |
import { logout } from '../Auth/userSlice' |
|
4 |
import { RootState } from '../redux/store' |
|
5 |
|
|
1 | 6 |
const Home = () => { |
7 |
const dispatch = useDispatch() |
|
8 |
|
|
9 |
const userLoggedIn = useSelector( |
|
10 |
(state: RootState) => state.user.isLoggedIn |
|
11 |
) |
|
2 | 12 |
|
3 |
return (<> |
|
4 |
<h1>Home</h1> |
|
5 |
</>) |
|
13 |
return ( |
|
14 |
<> |
|
15 |
<h1>Home</h1> |
|
16 |
{userLoggedIn ? ( |
|
17 |
<Button |
|
18 |
size="large" |
|
19 |
variant="contained" |
|
20 |
color="primary" |
|
21 |
onClick={() => dispatch(logout())} |
|
22 |
> |
|
23 |
Logout |
|
24 |
</Button> |
|
25 |
) : null} |
|
26 |
</> |
|
27 |
) |
|
6 | 28 |
} |
7 | 29 |
|
8 |
export default Home |
|
30 |
export default Home |
frontend/src/utils/ApiCallError.ts | ||
---|---|---|
1 |
export default class ApiCallError extends Error {} |
frontend/src/utils/thunkResponse.ts | ||
---|---|---|
1 |
|
|
2 |
export default interface ThunkResponse<T> { |
|
3 |
success: boolean |
|
4 |
data?: T |
|
5 |
} |
Také k dispozici: Unified diff
re #9368 jwt simple login