From fca8ab9d2f43a0b856c6c1be81d4279af7176335 Mon Sep 17 00:00:00 2001 From: Andrii Podriez Date: Wed, 24 Apr 2024 16:05:56 +0200 Subject: [PATCH] Created userSlice to manage user authentication. Changed components related to login into function components. Organized all changed files that are related to authentication in src\pages\login\ . Signed-off-by: Andrii Podriez --- package.json | 3 +- src/index.js | 2 +- src/pages/login/login-form.js | 110 ++++++++++++++++++++++++ src/pages/login/login.css | 42 +++++++++ src/pages/login/login.js | 80 +++++++++++++++++ src/{user => pages/login}/logout.js | 29 ++++--- src/router.js | 14 +-- src/store/index.js | 26 ++++++ src/store/userSlice.js | 100 +++++++++++++++++++++ src/styles/app.css | 32 ------- src/user/login-form.js | 129 ---------------------------- src/user/login.js | 93 -------------------- 12 files changed, 384 insertions(+), 276 deletions(-) create mode 100644 src/pages/login/login-form.js create mode 100644 src/pages/login/login.css create mode 100644 src/pages/login/login.js rename src/{user => pages/login}/logout.js (69%) create mode 100644 src/store/index.js create mode 100644 src/store/userSlice.js delete mode 100644 src/user/login-form.js delete mode 100644 src/user/login.js diff --git a/package.json b/package.json index 91c907d..1a56e50 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,7 @@ "@fortawesome/free-brands-svg-icons": "^6.1.1", "@fortawesome/free-solid-svg-icons": "^6.1.1", "@fortawesome/react-fontawesome": "^0.1.18", - "@reduxjs/toolkit": "^1.9.7", + "@reduxjs/toolkit": "^2.2.3", "@rjsf/core": "^4.1.1", "babel-runtime": "^6.26.0", "bootstrap": "^5.1.3", @@ -48,6 +48,7 @@ "react-fullscreenable": "^2.5.1-0", "react-json-view": "^1.21.3", "react-notification-system": "^0.4.0", + "react-redux": "^7.2.8", "react-rnd": "^10.3.7", "react-router-dom": "^5.3.1", "react-svg-pan-zoom": "^3.11.0", diff --git a/src/index.js b/src/index.js index c7a7018..a899895 100644 --- a/src/index.js +++ b/src/index.js @@ -22,7 +22,7 @@ import Router from "./router"; // Redux import { Provider } from "react-redux"; -import store from "./redux/store"; +import {store} from "./store/index"; import "bootstrap/dist/css/bootstrap.css"; import "./styles/index.css"; diff --git a/src/pages/login/login-form.js b/src/pages/login/login-form.js new file mode 100644 index 0000000..77283ac --- /dev/null +++ b/src/pages/login/login-form.js @@ -0,0 +1,110 @@ +/** + * This file is part of VILLASweb. + * + * VILLASweb is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * VILLASweb is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with VILLASweb. If not, see . + ******************************************************************************/ + +import React, { useState, useEffect } from 'react'; +import { Form, Button, Col } from 'react-bootstrap'; +import RecoverPassword from '../../user/recover-password' +import { useDispatch } from 'react-redux' +import { login } from '../../store/userSlice'; +import _ from 'lodash'; + +import "./login.css"; + +const LoginForm = (props) => { + const [username, setUsername] = useState(''); + const [password, setPassword] = useState(''); + const [forgottenPassword, setForgottenPassword] = useState(false) + + //this variable is used to disable login button if either username or password is empty + const isInputValid = username !== '' && password !== ''; + + const dispatch = useDispatch(); + + const loginEvent = (event) => { + event.preventDefault(); + dispatch(login({username, password})) + } + + const villasLogin = ( +
+ + Username + + { + setUsername(e.target.value) + } + } /> + + + + + Password + + { + setPassword(e.target.value) + }} + /> + + + + {props.loginMessage && +
+ + Error: {props.loginMessage} + +
+ } + + + + + + + + + + + setForgottenPassword(false)} sessionToken={props.sessionToken} /> + + ); + + if (props.config) { + let externalLogin = _.get(props.config, ['authentication', 'external', 'enabled']) + let provider = _.get(props.config, ['authentication', 'external', 'provider_name']) + let url = _.get(props.config, ['authentication', 'external', 'authorize_url']) + "?rd=/login/complete" + + if (externalLogin && provider && url) { + return [ + villasLogin, +
, + + ]; + } + } + + return villasLogin; +} + +export default LoginForm; diff --git a/src/pages/login/login.css b/src/pages/login/login.css new file mode 100644 index 0000000..74b8e69 --- /dev/null +++ b/src/pages/login/login.css @@ -0,0 +1,42 @@ +/** + * This file is part of VILLASweb. + * + * VILLASweb is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * VILLASweb is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with VILLASweb. If not, see . + ******************************************************************************/ + +.login-parent { + display: flex; + max-width: 800px; + margin: 30px auto; +} + +.login-welcome { + float: right; + max-width: 400px; + padding: 15px 20px; + border-radius: var(--borderradius) 0px 0px var(--borderradius); + background-color: #fff; + box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.2), + 0 9px 18px 0 rgba(0, 0, 0, 0.1); +} + +.login-container { + float: left; + max-width: 400px; + border-radius: 0px var(--borderradius) var(--borderradius) 0px; + padding: 15px 20px; + background-color: #fff; + box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.2), + 0 9px 18px 0 rgba(0, 0, 0, 0.1); +} \ No newline at end of file diff --git a/src/pages/login/login.js b/src/pages/login/login.js new file mode 100644 index 0000000..59af39a --- /dev/null +++ b/src/pages/login/login.js @@ -0,0 +1,80 @@ +/** + * This file is part of VILLASweb. + * + * VILLASweb is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * VILLASweb is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with VILLASweb. If not, see . + ******************************************************************************/ + +import React, {useEffect, useRef} from 'react'; +import { NavbarBrand } from 'react-bootstrap'; +import NotificationSystem from 'react-notification-system'; +import { Redirect } from 'react-router-dom'; + +import { useSelector } from 'react-redux'; + +import LoginForm from './login-form'; +import Header from '../../common/header'; +import NotificationsDataManager from '../../common/data-managers/notifications-data-manager'; +import AppDispatcher from '../../common/app-dispatcher'; +import branding from '../../branding/branding'; + +import './login.css'; + +const Login = (props) => { + + const notificationSystem = useRef() + + + useEffect(() => { + NotificationsDataManager.setSystem(notificationSystem); + + console.log("redirected to login", currentUser) + + // load config in case the user goes directly to /login + // otherwise it will be loaded in app constructor + AppDispatcher.dispatch({ + type: 'config/load', + }); + }, []); + + const config = null + + const currentUser = useSelector(state => state.user.currentUser); + const loginMessage = useSelector(state => state.user.loginMessage); + + return currentUser == null ? + ( +
+ + +
+
+
+ {branding.getWelcome()} +
+ +
+ Login + + +
+
+ + {branding.getFooter()} +
+ ) + : + (); +} + +export default Login \ No newline at end of file diff --git a/src/user/logout.js b/src/pages/login/logout.js similarity index 69% rename from src/user/logout.js rename to src/pages/login/logout.js index 36a49b5..ac5dae6 100644 --- a/src/user/logout.js +++ b/src/pages/login/logout.js @@ -15,25 +15,26 @@ * along with VILLASweb. If not, see . ******************************************************************************/ -import React from 'react'; +import React, {useEffect} from 'react'; import { Redirect } from 'react-router-dom'; -import AppDispatcher from '../common/app-dispatcher'; -class Logout extends React.Component { +import { useDispatch } from 'react-redux' +import { logout } from '../../store/userSlice'; - componentDidMount() { - AppDispatcher.dispatch({ - type: 'users/logout' - }); +const Logout = () => { - // The Login Store and local storage are deleted automatically - } + const dispatch = useDispatch(); - render() { - return ( - - ); - } + useEffect(() => { + let isMounted = true; + if(isMounted) dispatch(logout()); + + return () => {isMounted = false}; + }, []); + + return ( + + ) } export default Logout; diff --git a/src/router.js b/src/router.js index a303e20..798093b 100644 --- a/src/router.js +++ b/src/router.js @@ -18,23 +18,25 @@ import React from "react"; import { BrowserRouter, Route, Switch } from "react-router-dom"; import App from "./app"; -import Login from "./user/login"; -import Logout from "./user/logout"; +import Login from "./pages/login/login"; +import Logout from "./pages/login/logout"; import LoginComplete from "./user/login-complete"; class Root extends React.Component { render() { return ( - - - + + + + + + - ); } } diff --git a/src/store/index.js b/src/store/index.js new file mode 100644 index 0000000..106544b --- /dev/null +++ b/src/store/index.js @@ -0,0 +1,26 @@ +/** + * This file is part of VILLASweb. + * + * VILLASweb is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * VILLASweb is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with VILLASweb. If not, see . + ******************************************************************************/ + +import { configureStore } from "@reduxjs/toolkit" +import userReducer from './userSlice' + +export const store = configureStore({ + reducer: { + user: userReducer + }, + devTools: true +}) \ No newline at end of file diff --git a/src/store/userSlice.js b/src/store/userSlice.js new file mode 100644 index 0000000..ce346d3 --- /dev/null +++ b/src/store/userSlice.js @@ -0,0 +1,100 @@ +/** + * This file is part of VILLASweb. + * + * VILLASweb is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * VILLASweb is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with VILLASweb. If not, see . + ******************************************************************************/ + +import {createSlice, createAsyncThunk} from '@reduxjs/toolkit' + +import RestAPI from '../common/api/rest-api'; +import ICDataDataManager from '../ic/ic-data-data-manager'; + +const userSlice = createSlice({ + name: 'login', + initialState: {currentUser: null, currentToken: null, isLoading: false, loginMessage: ''}, + extraReducers: (builder) => { + builder + .addCase(login.pending, (state, action) => { + state.isLoading = true + }) + .addCase(login.fulfilled, (state, action) => { + state.isLoading = false + state.currentUser = action.payload.user + state.currentToken = action.payload.token + + localStorage.setItem('currentUser', JSON.stringify(action.payload.user)); + localStorage.setItem('token', action.payload.token); + }) + .addCase(login.rejected, (state, action) => { + state.loginMessage = 'Wrong credentials! Please try again.' + }) + .addCase(logout.pending, (state) => { + state.currentUser = null + state.currentToken = null + }) + .addCase(loginExternal.pending, (state, action) => { + state.isLoading = true + }) + .addCase(loginExternal.fulfilled, (state, action) => { + state.isLoading = false + state.currentUser = action.payload.user + state.currentToken = action.payload.token + + localStorage.setItem('currentUser', JSON.stringify(action.payload.user)); + localStorage.setItem('token', action.payload.token); + }) + } +}) + +export const login = createAsyncThunk( + 'user/login', + async (userData, thunkAPI) => { + try { + const res = await RestAPI.post('/api/v2/authenticate/internal', userData) + + return {user: res.user, token: res.token} + } catch(error) { + console.log('Error while trying to log in: ', error) + return thunkAPI.rejectWithValue(error) + } + } +) + +export const loginExternal = createAsyncThunk( + 'user/loginExternal', + async (data, thunkAPI) => { + try { + const res = await RestAPI.post(this.makeURL('/authenticate/external'), null, null, 60000) + + return {user: res.user, token: res.token} + } catch(error) { + console.log('Error while trying to log in externally: ', error) + return thunkAPI.rejectWithValue(error) + } + } +) + +export const logout = createAsyncThunk( + 'user/logout', + async () => { + // disconnect from all infrastructure components + ICDataDataManager.closeAll(); + //remove token and current user from local storage + localStorage.clear(); + + console.log("logged out") + } +) + +export default userSlice.reducer \ No newline at end of file diff --git a/src/styles/app.css b/src/styles/app.css index ccf111e..e8dcefb 100644 --- a/src/styles/app.css +++ b/src/styles/app.css @@ -266,38 +266,6 @@ a:active { background-color: white; } -/** - * Login form - */ - .login-parent { - display: flex; - max-width: 800px; - - margin: 30px auto; - } - - .login-welcome { - float: right; - max-width: 400px; - padding: 15px 20px; - border-radius: var(--borderradius) 0px 0px var(--borderradius); - - background-color: #fff; - box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.2), - 0 9px 18px 0 rgba(0, 0, 0, 0.1); -} - -.login-container { - float: left; - max-width: 400px; - border-radius: 0px var(--borderradius) var(--borderradius) 0px; - padding: 15px 20px; - - background-color: #fff; - box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.2), - 0 9px 18px 0 rgba(0, 0, 0, 0.1); -} - hr { margin-top: 1rem; margin-bottom: 1rem; diff --git a/src/user/login-form.js b/src/user/login-form.js deleted file mode 100644 index c54bf6f..0000000 --- a/src/user/login-form.js +++ /dev/null @@ -1,129 +0,0 @@ -/** - * This file is part of VILLASweb. - * - * VILLASweb is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * VILLASweb is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with VILLASweb. If not, see . - ******************************************************************************/ - -import React, { Component } from 'react'; -import { Form, Button, Col } from 'react-bootstrap'; -import RecoverPassword from './recover-password' -import AppDispatcher from '../common/app-dispatcher'; -import _ from 'lodash'; - - -class LoginForm extends Component { - constructor(props) { - super(props); - - this.state = { - username: '', - password: '', - forgottenPassword: false, - disableLogin: true - } - } - - login(event) { - // prevent from submitting the form since we send an action - event.preventDefault(); - - // send login action - AppDispatcher.dispatch({ - type: 'users/login', - username: this.state.username, - password: this.state.password - }); - } - - handleChange(event) { - let disableLogin = this.state.disableLogin; - - if (event.target.id === 'username') { - disableLogin = this.state.password.length === 0 || event.target.value.length === 0; - } else if (event.target.id === 'password') { - disableLogin = this.state.username.length === 0 || event.target.value.length === 0; - } - - this.setState({ [event.target.id]: event.target.value, disableLogin }); - } - - openRecoverPassword() { - this.setState({ forgottenPassword: true }); - } - - closeRecoverPassword() { - this.setState({ forgottenPassword: false }); - } - - villaslogin() { - return ( -
- - Username - - this.handleChange(e)} /> - - - - - Password - - this.handleChange(e)} /> - - - - {this.props.loginMessage && -
- - Error: {this.props.loginMessage} - -
- } - - - - - - - - - - - this.closeRecoverPassword()} sessionToken={this.props.sessionToken} /> - - ); - } - - render() { - let villasLogin = this.villaslogin(); - - if (this.props.config) { - let externalLogin = _.get(this.props.config, ['authentication', 'external', 'enabled']) - let provider = _.get(this.props.config, ['authentication', 'external', 'provider_name']) - let url = _.get(this.props.config, ['authentication', 'external', 'authorize_url']) + "?rd=/login/complete" - - if (externalLogin && provider && url) { - return [ - villasLogin, -
, - - ]; - } - } - - return villasLogin; - } -} - -export default LoginForm; diff --git a/src/user/login.js b/src/user/login.js deleted file mode 100644 index a91fa89..0000000 --- a/src/user/login.js +++ /dev/null @@ -1,93 +0,0 @@ -/** - * This file is part of VILLASweb. - * - * VILLASweb is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * VILLASweb is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with VILLASweb. If not, see . - ******************************************************************************/ - -import React from 'react'; -import { Container } from 'flux/utils'; -import { NavbarBrand } from 'react-bootstrap'; -import NotificationSystem from 'react-notification-system'; -import { Redirect } from 'react-router-dom'; - -import LoginForm from './login-form'; -import Header from '../common/header'; -import NotificationsDataManager from '../common/data-managers/notifications-data-manager'; -import LoginStore from './login-store' -import AppDispatcher from '../common/app-dispatcher'; -import branding from '../branding/branding'; - - -class Login extends React.Component { - constructor(props) { - super(props); - - } - - static getStores() { - return [LoginStore] - } - - static calculateState(prevState, props) { - // We need to work with the login store here to trigger the re-render upon state change after login - // Upon successful login, the token and currentUser are stored in the local storage as strings - return { - loginMessage: LoginStore.getState().loginMessage, - token: LoginStore.getState().token, - currentUser: LoginStore.getState().currentUser, - config: LoginStore.getState().config, - } - } - - componentDidMount() { - NotificationsDataManager.setSystem(this.refs.notificationSystem); - - // load config in case the user goes directly to /login - // otherwise it will be loaded in app constructor - AppDispatcher.dispatch({ - type: 'config/load', - }); - } - - render() { - - if (this.state.currentUser !== null && this.state.currentUser !== "") { - return (); - } - - return ( -
- - -
-
-
- {branding.getWelcome()} -
- -
- Login - - -
-
- - {branding.getFooter()} -
- ); - } -} - -let fluxContainerConverter = require('../common/FluxContainerConverter'); -export default Container.create(fluxContainerConverter.convert(Login));