From 3b13ee67c0953572b3e3bd0097169ffb3edc0efa Mon Sep 17 00:00:00 2001 From: SamR1 Date: Mon, 25 Dec 2017 17:45:28 +0100 Subject: [PATCH] Client: login/register Client: login/register Client: login/register Client: login/register Client: login/register Client: login/register Client: login/register Client: login/register Client: login/register --- mpwo_api/mpwo_api/__init__.py | 13 +++ mpwo_api/mpwo_api/tests/test_auth.py | 39 +++++++ mpwo_api/mpwo_api/users/auth.py | 32 ++++- mpwo_api/mpwo_api/users/utils.py | 35 ++++++ mpwo_client/src/actions/index.js | 117 +++++++++++++++++++ mpwo_client/src/components/App.jsx | 37 +++++- mpwo_client/src/components/Logout.jsx | 31 +++++ mpwo_client/src/components/NavBar.jsx | 55 +++++++-- mpwo_client/src/components/User/Form.jsx | 52 +++++++++ mpwo_client/src/components/User/UserForm.jsx | 36 ++++++ mpwo_client/src/index.js | 6 +- mpwo_client/src/mpwoApi.js | 48 ++++++++ mpwo_client/src/reducers/index.js | 69 ++++++++++- mpwo_client/src/reducers/initial.js | 18 +++ mpwo_client/src/reducers/user.js | 21 ---- 15 files changed, 568 insertions(+), 41 deletions(-) create mode 100644 mpwo_api/mpwo_api/users/utils.py create mode 100644 mpwo_client/src/components/Logout.jsx create mode 100644 mpwo_client/src/components/User/Form.jsx create mode 100644 mpwo_client/src/components/User/UserForm.jsx create mode 100644 mpwo_client/src/mpwoApi.js create mode 100644 mpwo_client/src/reducers/initial.js delete mode 100644 mpwo_client/src/reducers/user.js diff --git a/mpwo_api/mpwo_api/__init__.py b/mpwo_api/mpwo_api/__init__.py index 7cb2f79c..e4b85613 100644 --- a/mpwo_api/mpwo_api/__init__.py +++ b/mpwo_api/mpwo_api/__init__.py @@ -32,3 +32,16 @@ if app.debug: ).handlers = logging.getLogger('werkzeug').handlers logging.getLogger('sqlalchemy.orm').setLevel(logging.WARNING) appLog.setLevel(logging.DEBUG) + +if app.debug : + # Enable CORS + @app.after_request + def after_request(response): + response.headers.add('Access-Control-Allow-Origin', '*') + response.headers.add( + 'Access-Control-Allow-Headers', 'Content-Type,Authorization' + ) + response.headers.add( + 'Access-Control-Allow-Methods', 'GET,PUT,POST,DELETE,PATCH,OPTIONS' + ) + return response diff --git a/mpwo_api/mpwo_api/tests/test_auth.py b/mpwo_api/mpwo_api/tests/test_auth.py index d6c0b097..027a06b3 100644 --- a/mpwo_api/mpwo_api/tests/test_auth.py +++ b/mpwo_api/mpwo_api/tests/test_auth.py @@ -191,3 +191,42 @@ class TestAuthBlueprint(BaseTestCase): self.assertTrue( data['message'] == 'Invalid token. Please log in again.') self.assertEqual(response.status_code, 401) + + def test_user_profile(self): + add_user('test', 'test@test.com', 'test') + with self.client: + resp_login = self.client.post( + '/api/auth/login', + data=json.dumps(dict( + email='test@test.com', + password='test' + )), + content_type='application/json' + ) + response = self.client.get( + '/api/auth/profile', + headers=dict( + Authorization='Bearer ' + json.loads( + resp_login.data.decode() + )['auth_token'] + ) + ) + data = json.loads(response.data.decode()) + self.assertTrue(data['status'] == 'success') + self.assertTrue(data['data'] is not None) + self.assertTrue(data['data']['username'] == 'test') + self.assertTrue(data['data']['email'] == 'test@test.com') + self.assertTrue(data['data']['created_at']) + self.assertFalse(data['data']['admin']) + self.assertEqual(response.status_code, 200) + + def test_invalid_profile(self): + with self.client: + response = self.client.get( + '/api/auth/profile', + headers=dict(Authorization='Bearer invalid')) + data = json.loads(response.data.decode()) + self.assertTrue(data['status'] == 'error') + self.assertTrue( + data['message'] == 'Invalid token. Please log in again.') + self.assertEqual(response.status_code, 401) diff --git a/mpwo_api/mpwo_api/users/auth.py b/mpwo_api/mpwo_api/users/auth.py index bf69d588..d05e14ed 100644 --- a/mpwo_api/mpwo_api/users/auth.py +++ b/mpwo_api/mpwo_api/users/auth.py @@ -4,6 +4,7 @@ from sqlalchemy import exc, or_ from mpwo_api import appLog, bcrypt, db from .models import User +from .utils import authenticate auth_blueprint = Blueprint('auth', __name__) @@ -12,7 +13,9 @@ auth_blueprint = Blueprint('auth', __name__) def register_user(): # get post data post_data = request.get_json() - if not post_data: + if not post_data or post_data.get('username') is None \ + or post_data.get('email') is None \ + or post_data.get('password') is None: response_object = { 'status': 'error', 'message': 'Invalid payload.' @@ -52,9 +55,10 @@ def register_user(): except (exc.IntegrityError, exc.OperationalError, ValueError) as e: db.session.rollback() appLog.error(e) + response_object = { 'status': 'error', - 'message': 'Invalid payload.' + 'message': 'Error. Please try again or contact the administrator.' } return jsonify(response_object), 400 @@ -95,19 +99,20 @@ def login_user(): appLog.error(e) response_object = { 'status': 'error', - 'message': 'Try again' + 'message': 'Error. Please try again or contact the administrator.' } return jsonify(response_object), 500 @auth_blueprint.route('/auth/logout', methods=['GET']) -def logout_user(): +@authenticate +def logout_user(user_id): # get auth token auth_header = request.headers.get('Authorization') if auth_header: auth_token = auth_header.split(" ")[1] resp = User.decode_auth_token(auth_token) - if not isinstance(resp, str): + if not isinstance(user_id, str): response_object = { 'status': 'success', 'message': 'Successfully logged out.' @@ -125,3 +130,20 @@ def logout_user(): 'message': 'Provide a valid auth token.' } return jsonify(response_object), 403 + + +@auth_blueprint.route('/auth/profile', methods=['GET']) +@authenticate +def get_user_status(user_id): + user = User.query.filter_by(id=user_id).first() + response_object = { + 'status': 'success', + 'data': { + 'id': user.id, + 'username': user.username, + 'email': user.email, + 'created_at': user.created_at, + 'admin': user.admin, + } + } + return jsonify(response_object), 200 diff --git a/mpwo_api/mpwo_api/users/utils.py b/mpwo_api/mpwo_api/users/utils.py new file mode 100644 index 00000000..0e652ef5 --- /dev/null +++ b/mpwo_api/mpwo_api/users/utils.py @@ -0,0 +1,35 @@ +from functools import wraps + +from flask import request, jsonify + +from .models import User + + +def authenticate(f): + @wraps(f) + def decorated_function(*args, **kwargs): + response_object = { + 'status': 'error', + 'message': 'Something went wrong. Please contact us.' + } + code = 401 + auth_header = request.headers.get('Authorization') + if not auth_header: + response_object['message'] = 'Provide a valid auth token.' + code = 403 + return jsonify(response_object), code + auth_token = auth_header.split(" ")[1] + resp = User.decode_auth_token(auth_token) + if isinstance(resp, str): + response_object['message'] = resp + return jsonify(response_object), code + user = User.query.filter_by(id=resp).first() + if not user: + return jsonify(response_object), code + return f(resp, *args, **kwargs) + return decorated_function + + +def is_admin(user_id): + user = User.query.filter_by(id=user_id).first() + return user.admin diff --git a/mpwo_client/src/actions/index.js b/mpwo_client/src/actions/index.js index e69de29b..2a80528e 100644 --- a/mpwo_client/src/actions/index.js +++ b/mpwo_client/src/actions/index.js @@ -0,0 +1,117 @@ +import mpwoApi from '../mpwoApi' + +function AuthError(message) { + return { type: 'AUTH_ERROR', message } +} + +function ProfileSuccess(message) { + return { type: 'PROFILE_SUCCESS', message } +} + +function ProfileError(message) { + return { type: 'PROFILE_ERROR', message } +} + +const updateFormDataEmail = value => ({ + type: 'UPDATE_FORMDATA_EMAIL', + email: value, +}) + +const updateFormDataUsername = value => ({ + type: 'UPDATE_FORMDATA_USERNAME', + username: value, +}) + +const updateFormDataPassword = value => ({ + type: 'UPDATE_FORMDATA_PASSWORD', + password: value, +}) + +export function getProfile(dispatch) { + return mpwoApi + .getProfile() + .then(ret => { + if (ret.status === 'success') { + dispatch(ProfileSuccess(ret)) + } else { + dispatch(ProfileError(ret.message)) + } + }) + .catch(error => { + throw error + }) +} + +export function register(formData) { + return function(dispatch) { + return mpwoApi + .register(formData.username, formData.email, formData.password) + .then(ret => { + if (ret.status === 'success') { + window.localStorage.setItem('authToken', ret.auth_token) + getProfile(dispatch) + } else { + dispatch(AuthError(ret.message)) + } + }) + .catch(error => { + throw error + }) + } +} + +export function login(formData) { + return function(dispatch) { + return mpwoApi + .login(formData.email, formData.password) + .then(ret => { + if (ret.status === 'success') { + window.localStorage.setItem('authToken', ret.auth_token) + getProfile(dispatch) + } else { + dispatch(AuthError(ret.message)) + } + }) + .catch(error => { + throw error + }) + } +} + +export function loadProfile() { + if (window.localStorage.getItem('authToken')) { + return function(dispatch) { + getProfile(dispatch) + } + } + return { type: 'LOGOUT' } +} + +export function logout() { + return { type: 'LOGOUT' } +} + +export function handleUserFormSubmit(event, formType) { + event.preventDefault() + return (dispatch, getState) => { + const state = getState() + let { formData } = state.formData + formData.formData = state.formData.formData + if (formType === 'Login') { + dispatch(login(formData.formData)) + } else { // formType === 'Register' + dispatch(register(formData.formData)) + } + } +} + +export const handleFormChange = event => dispatch => { + switch (event.target.name) { + case 'email': + return dispatch(updateFormDataEmail(event.target.value)) + case 'username': + return dispatch(updateFormDataUsername(event.target.value)) + default: // case 'password': + return dispatch(updateFormDataPassword(event.target.value)) + } +} diff --git a/mpwo_client/src/components/App.jsx b/mpwo_client/src/components/App.jsx index d1ad0291..4b9ff909 100644 --- a/mpwo_client/src/components/App.jsx +++ b/mpwo_client/src/components/App.jsx @@ -1,7 +1,10 @@ import React from 'react' +import { Route, Switch } from 'react-router-dom' import './App.css' +import Logout from './Logout' import NavBar from './NavBar' +import UserForm from './User/UserForm' export default class App extends React.Component { constructor(props) { @@ -13,9 +16,37 @@ export default class App extends React.Component { return (
-

- App in progress -

+
+
+
+
+ + ( + + )} + /> + ( + + )} + /> + ( + + )} + /> + +
+
+
) } diff --git a/mpwo_client/src/components/Logout.jsx b/mpwo_client/src/components/Logout.jsx new file mode 100644 index 00000000..070af4ac --- /dev/null +++ b/mpwo_client/src/components/Logout.jsx @@ -0,0 +1,31 @@ +import React from 'react' +import { connect } from 'react-redux' +import { Link } from 'react-router-dom' + +import { logout } from '../actions' + +class Logout extends React.Component { + componentDidMount() { + this.props.UserLogout() + } + render() { + return ( +
+

+ You are now logged out. + Click here to log back in.

+
+ ) + } +} + +export default connect( + state => ({ + user: state.user, + }), + dispatch => ({ + UserLogout: () => { + dispatch(logout()) + } + }) +)(Logout) diff --git a/mpwo_client/src/components/NavBar.jsx b/mpwo_client/src/components/NavBar.jsx index edf6dd68..21a43904 100644 --- a/mpwo_client/src/components/NavBar.jsx +++ b/mpwo_client/src/components/NavBar.jsx @@ -1,14 +1,9 @@ import React from 'react' +import { connect } from 'react-redux' import { Link } from 'react-router-dom' -export default class NavBar extends React.Component { - constructor(props) { - super(props) - this.props = props - } - - render() { - return ( +function NavBar (props) { + return (
- ) - } + ) } +export default connect( + state => ({ + user: state.user, + }) +)(NavBar) diff --git a/mpwo_client/src/components/User/Form.jsx b/mpwo_client/src/components/User/Form.jsx new file mode 100644 index 00000000..6a31f75f --- /dev/null +++ b/mpwo_client/src/components/User/Form.jsx @@ -0,0 +1,52 @@ +import React from 'react' + + +export default function Form (props) { + return ( +
+

{props.formType}

+

+
+ props.handleUserFormSubmit(event, props.formType)} + > + {props.formType === 'Register' && +
+ +
+ } +
+ +
+
+ +
+ +
+
+ ) +} diff --git a/mpwo_client/src/components/User/UserForm.jsx b/mpwo_client/src/components/User/UserForm.jsx new file mode 100644 index 00000000..a9e90da0 --- /dev/null +++ b/mpwo_client/src/components/User/UserForm.jsx @@ -0,0 +1,36 @@ +import React from 'react' +import { connect } from 'react-redux' + +import Form from './Form' +import { handleFormChange, handleUserFormSubmit } from '../../actions' + +function UserForm(props) { + return ( +
+ {props.message !== '' && ( + {props.message} + )} +
props.onHandleFormChange(event)} + handleUserFormSubmit={(event, formType) => + props.onHandleUserFormSubmit(event, formType)} + /> +
+ ) +} +export default connect( + state => ({ + formData: state.formData, + message: state.message, + }), + dispatch => ({ + onHandleFormChange: event => { + dispatch(handleFormChange(event)) + }, + onHandleUserFormSubmit: (event, formType) => { + dispatch(handleUserFormSubmit(event, formType)) + }, + }) +)(UserForm) diff --git a/mpwo_client/src/index.js b/mpwo_client/src/index.js index a4516d9d..c09e306a 100644 --- a/mpwo_client/src/index.js +++ b/mpwo_client/src/index.js @@ -10,7 +10,7 @@ import App from './components/App' import Root from './components/Root' import registerServiceWorker from './registerServiceWorker' import reducers from './reducers' - +import { loadProfile } from './actions' export const history = createBrowserHistory() @@ -24,6 +24,10 @@ export const store = createStore( ) ) +if (window.localStorage.getItem('authToken') !== null) { + store.dispatch(loadProfile()) +} + ReactDOM.render( diff --git a/mpwo_client/src/mpwoApi.js b/mpwo_client/src/mpwoApi.js new file mode 100644 index 00000000..8168473d --- /dev/null +++ b/mpwo_client/src/mpwoApi.js @@ -0,0 +1,48 @@ +const apiUrl = `${process.env.REACT_APP_API_URL}` + + +export default class MpwoApi { + static login(email, password) { + const request = new Request(`${apiUrl}auth/login`, { + method: 'POST', + headers: new Headers({ + 'Content-Type': 'application/json', + }), + body: JSON.stringify({ + email: email, + password: password, + }), + }) + return fetch(request) + .then(response => response.json()) + .catch(error => error) + } + static register(username, email, password) { + const request = new Request(`${apiUrl}auth/register`, { + method: 'POST', + headers: new Headers({ + 'Content-Type': 'application/json', + }), + body: JSON.stringify({ + username: username, + email: email, + password: password, + }), + }) + return fetch(request) + .then(response => response.json()) + .catch(error => error) + } + static getProfile() { + const request = new Request(`${apiUrl}auth/profile`, { + method: 'GET', + headers: new Headers({ + 'Content-Type': 'application/json', + Authorization: `Bearer ${window.localStorage.getItem('authToken')}`, + }), + }) + return fetch(request) + .then(response => response.json()) + .catch(error => error) + } +} diff --git a/mpwo_client/src/reducers/index.js b/mpwo_client/src/reducers/index.js index b4efe78f..3f2aa2eb 100644 --- a/mpwo_client/src/reducers/index.js +++ b/mpwo_client/src/reducers/index.js @@ -1,9 +1,76 @@ import { combineReducers } from 'redux' -import user from './user' +import initial from './initial' + +const message = (state = initial.message, action) => { + switch (action.type) { + case 'AUTH_ERROR': + case 'PROFILE_ERROR': + return action.message + case 'LOGOUT': + return '' + case 'PROFILE_SUCCESS': + return '' + default: + return state + } +} + +const user = (state = initial.user, action) => { + switch (action.type) { + case 'AUTH_ERROR': + case 'PROFILE_ERROR': + case 'LOGOUT': + window.localStorage.removeItem('authToken') + return initial.user + case 'PROFILE_SUCCESS': + return { + id: action.message.data.id, + username: action.message.data.username, + email: action.message.data.email, + isAdmin: action.message.data.admin, + createdAt: action.message.data.created_at, + isAuthenticated: true + } + default: + return state + } +} + +const formData = (state = initial.formData, action) => { + switch (action.type) { + case 'UPDATE_FORMDATA_EMAIL': + return { + formData: { + ...state.formData, + email: action.email + }, + } + case 'UPDATE_FORMDATA_USERNAME': + return { + formData: { + ...state.formData, + username: action.username + }, + } + case 'UPDATE_FORMDATA_PASSWORD': + return { + formData: { + ...state.formData, + password: action.password + }, + } + case 'PROFILE_SUCCESS': + return initial.formData + default: + return state + } +} const reducers = combineReducers({ + message, user, + formData, }) export default reducers diff --git a/mpwo_client/src/reducers/initial.js b/mpwo_client/src/reducers/initial.js new file mode 100644 index 00000000..eb3ad5d0 --- /dev/null +++ b/mpwo_client/src/reducers/initial.js @@ -0,0 +1,18 @@ +export default { + message: '', + user: { + id: '', + username: '', + email: '', + createdAt: '', + isAdmin: false, + isAuthenticated: false, + }, + formData: { + formData: { + username: '', + email: '', + password: '' + } + }, +} diff --git a/mpwo_client/src/reducers/user.js b/mpwo_client/src/reducers/user.js deleted file mode 100644 index 7d34b3bc..00000000 --- a/mpwo_client/src/reducers/user.js +++ /dev/null @@ -1,21 +0,0 @@ -const user = (state = null, action) => { - switch (action.type) { - case 'AUTH_ERROR': - case 'PROFILE_ERROR': - case 'LOGOUT': - window.localStorage.removeItem('authToken') - return null - case 'PROFILE_SUCCESS': - return { - id: action.message.data.id, - username: action.message.data.username, - email: action.message.data.email, - isAdmin: action.message.data.is_admin, - createdAt: action.message.data.created_at, - } - default: - return state - } -} - -export default user