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
This commit is contained in:
SamR1 2017-12-25 17:45:28 +01:00
parent 166cbd201a
commit 3b13ee67c0
15 changed files with 568 additions and 41 deletions

View File

@ -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

View File

@ -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)

View File

@ -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

View File

@ -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

View File

@ -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))
}
}

View File

@ -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 (
<div className="App">
<NavBar />
<p className="App-body">
App in progress
</p>
<div className="container">
<div className="row">
<div className="col-md-6">
<br />
<Switch>
<Route
exact path="/register"
render={() => (
<UserForm
formType={'Register'}
/>
)}
/>
<Route
exact path="/login"
render={() => (
<UserForm
formType={'Login'}
/>
)}
/>
<Route
exact path="/logout"
render={() => (
<Logout />
)}
/>
</Switch>
</div>
</div>
</div>
</div>
)
}

View File

@ -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 (
<div>
<p>
You are now logged out.
Click <Link to="/login">here</Link> to log back in.</p>
</div>
)
}
}
export default connect(
state => ({
user: state.user,
}),
dispatch => ({
UserLogout: () => {
dispatch(logout())
}
})
)(Logout)

View File

@ -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 (
<header>
<nav className="navbar navbar-expand-lg navbar-light bg-light">
<span className="navbar-brand">mpwo</span>
@ -36,10 +31,50 @@ export default class NavBar extends React.Component {
Home
</Link>
</li>
{!props.user.isAuthenticated && (
<li className="nav-item">
<Link
className="nav-link"
to={{
pathname: '/login',
}}
>
Login
</Link>
</li>
)}
{!props.user.isAuthenticated && (
<li className="nav-item">
<Link
className="nav-link"
to={{
pathname: '/register',
}}
>
Register
</Link>
</li>
)}
{props.user.isAuthenticated && (
<li className="nav-item">
<Link
className="nav-link"
to={{
pathname: '/logout',
}}
>
Logout
</Link>
</li>
)}
</ul>
</div>
</nav>
</header>
)
}
)
}
export default connect(
state => ({
user: state.user,
})
)(NavBar)

View File

@ -0,0 +1,52 @@
import React from 'react'
export default function Form (props) {
return (
<div>
<h1>{props.formType}</h1>
<hr /><br />
<form onSubmit={event =>
props.handleUserFormSubmit(event, props.formType)}
>
{props.formType === 'Register' &&
<div className="form-group">
<input
name="username"
className="form-control input-lg"
type="text"
placeholder="Enter a username"
required
onChange={props.onHandleFormChange}
/>
</div>
}
<div className="form-group">
<input
name="email"
className="form-control input-lg"
type="email"
placeholder="Enter an email address"
required
onChange={props.onHandleFormChange}
/>
</div>
<div className="form-group">
<input
name="password"
className="form-control input-lg"
type="password"
placeholder="Enter a password"
required
onChange={props.onHandleFormChange}
/>
</div>
<input
type="submit"
className="btn btn-primary btn-lg btn-block"
value="Submit"
/>
</form>
</div>
)
}

View File

@ -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 (
<div>
{props.message !== '' && (
<code>{props.message}</code>
)}
<Form
formType={props.formType}
userForm={props.formData}
onHandleFormChange={event => props.onHandleFormChange(event)}
handleUserFormSubmit={(event, formType) =>
props.onHandleUserFormSubmit(event, formType)}
/>
</div>
)
}
export default connect(
state => ({
formData: state.formData,
message: state.message,
}),
dispatch => ({
onHandleFormChange: event => {
dispatch(handleFormChange(event))
},
onHandleUserFormSubmit: (event, formType) => {
dispatch(handleUserFormSubmit(event, formType))
},
})
)(UserForm)

View File

@ -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(
<Root store={store} history={history}>
<App />

View File

@ -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)
}
}

View File

@ -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

View File

@ -0,0 +1,18 @@
export default {
message: '',
user: {
id: '',
username: '',
email: '',
createdAt: '',
isAdmin: false,
isAuthenticated: false,
},
formData: {
formData: {
username: '',
email: '',
password: ''
}
},
}

View File

@ -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