diff --git a/Makefile b/Makefile index dd8115e5..79091ca8 100644 --- a/Makefile +++ b/Makefile @@ -14,3 +14,6 @@ serve-react: serve: $(MAKE) P="serve-react serve-python" make-p + +test-python: + $(FLASK) test diff --git a/mpwo_api/mpwo_api/tests/test_auth.py b/mpwo_api/mpwo_api/tests/test_auth.py index 027a06b3..ad1b844a 100644 --- a/mpwo_api/mpwo_api/tests/test_auth.py +++ b/mpwo_api/mpwo_api/tests/test_auth.py @@ -14,7 +14,8 @@ class TestAuthBlueprint(BaseTestCase): data=json.dumps(dict( username='justatest', email='test@test.com', - password='123456' + password='12345678', + password_conf='12345678' )), content_type='application/json' ) @@ -26,14 +27,15 @@ class TestAuthBlueprint(BaseTestCase): self.assertEqual(response.status_code, 201) def test_user_registration_user_already_exists(self): - add_user('test', 'test@test.com', 'test') + add_user('test', 'test@test.com', '12345678') with self.client: response = self.client.post( '/api/auth/register', data=json.dumps(dict( username='test', email='test@test.com', - password='test' + password='12345678', + password_conf='12345678' )), content_type='application/json' ) @@ -44,6 +46,101 @@ class TestAuthBlueprint(BaseTestCase): self.assertTrue(response.content_type == 'application/json') self.assertEqual(response.status_code, 400) + def test_user_registration_invalid_short_username(self): + with self.client: + response = self.client.post( + '/api/auth/register', + data=json.dumps(dict( + username='t', + email='test@test.com', + password='12345678', + password_conf='12345678' + )), + content_type='application/json' + ) + data = json.loads(response.data.decode()) + self.assertTrue(data['status'] == 'error') + self.assertTrue( + data['message'] == "Errors: Username: 3 to 12 characters required.\n") + self.assertTrue(response.content_type == 'application/json') + self.assertEqual(response.status_code, 400) + + def test_user_registration_invalid_long_username(self): + with self.client: + response = self.client.post( + '/api/auth/register', + data=json.dumps(dict( + username='testestestestestest', + email='test@test.com', + password='12345678', + password_conf='12345678' + )), + content_type='application/json' + ) + data = json.loads(response.data.decode()) + self.assertTrue(data['status'] == 'error') + self.assertTrue( + data['message'] == "Errors: Username: 3 to 12 characters required.\n") + self.assertTrue(response.content_type == 'application/json') + self.assertEqual(response.status_code, 400) + + def test_user_registration_invalid_email(self): + with self.client: + response = self.client.post( + '/api/auth/register', + data=json.dumps(dict( + username='test', + email='test@test', + password='12345678', + password_conf='12345678' + )), + content_type='application/json' + ) + data = json.loads(response.data.decode()) + self.assertTrue(data['status'] == 'error') + self.assertTrue( + data['message'] == "Errors: Valid email must be provided.\n") + self.assertTrue(response.content_type == 'application/json') + self.assertEqual(response.status_code, 400) + + def test_user_registration_invalid_short_password(self): + with self.client: + response = self.client.post( + '/api/auth/register', + data=json.dumps(dict( + username='test', + email='test@test.com', + password='1234567', + password_conf='1234567' + )), + content_type='application/json' + ) + data = json.loads(response.data.decode()) + self.assertTrue(data['status'] == 'error') + self.assertTrue( + data['message'] == "Errors: Password: 8 characters required.\n") + self.assertTrue(response.content_type == 'application/json') + self.assertEqual(response.status_code, 400) + + def test_user_registration_mismatched_password(self): + with self.client: + response = self.client.post( + '/api/auth/register', + data=json.dumps(dict( + username='test', + email='test@test.com', + password='12345678', + password_conf='87654321' + )), + content_type='application/json' + ) + data = json.loads(response.data.decode()) + self.assertTrue(data['status'] == 'error') + self.assertTrue( + data['message'] == "Errors: Password and password confirmation don\'t match.\n") + self.assertTrue(response.content_type == 'application/json') + self.assertEqual(response.status_code, 400) + def test_user_registration_invalid_json(self): with self.client: response = self.client.post( @@ -60,7 +157,10 @@ class TestAuthBlueprint(BaseTestCase): with self.client: response = self.client.post( '/api/auth/register', - data=json.dumps(dict(email='test@test.com', password='test')), + data=json.dumps(dict( + email='test@test.com', + password='12345678', + password_conf='12345678')), content_type='application/json', ) data = json.loads(response.data.decode()) @@ -73,7 +173,9 @@ class TestAuthBlueprint(BaseTestCase): response = self.client.post( '/api/auth/register', data=json.dumps(dict( - username='test', password='test')), + username='test', + password='12345678', + password_conf='12345678')), content_type='application/json', ) data = json.loads(response.data.decode()) @@ -86,7 +188,24 @@ class TestAuthBlueprint(BaseTestCase): response = self.client.post( '/api/auth/register', data=json.dumps(dict( - username='test', email='test@test.com')), + username='test', + email='test@test.com', + password_conf='12345678')), + content_type='application/json', + ) + data = json.loads(response.data.decode()) + self.assertEqual(response.status_code, 400) + self.assertIn('Invalid payload.', data['message']) + self.assertIn('error', data['status']) + + def test_user_registration_invalid_json_keys_no_password_conf(self): + with self.client: + response = self.client.post( + '/api/auth/register', + data=json.dumps(dict( + username='test', + email='test@test.com', + password='12345678')), content_type='application/json', ) data = json.loads(response.data.decode()) @@ -96,12 +215,12 @@ class TestAuthBlueprint(BaseTestCase): def test_registered_user_login(self): with self.client: - add_user('test', 'test@test.com', 'test') + add_user('test', 'test@test.com', '12345678') response = self.client.post( '/api/auth/login', data=json.dumps(dict( email='test@test.com', - password='test' + password='12345678' )), content_type='application/json' ) @@ -118,25 +237,42 @@ class TestAuthBlueprint(BaseTestCase): '/api/auth/login', data=json.dumps(dict( email='test@test.com', - password='test' + password='12345678' )), content_type='application/json' ) data = json.loads(response.data.decode()) self.assertTrue(data['status'] == 'error') - self.assertTrue(data['message'] == 'User does not exist.') + self.assertTrue(data['message'] == 'Invalid credentials.') + self.assertTrue(response.content_type == 'application/json') + self.assertEqual(response.status_code, 404) + + def test_registered_user_login_invalid_password(self): + add_user('test', 'test@test.com', '12345678') + with self.client: + response = self.client.post( + '/api/auth/login', + data=json.dumps(dict( + email='test@test.com', + password='123456789' + )), + content_type='application/json' + ) + data = json.loads(response.data.decode()) + self.assertTrue(data['status'] == 'error') + self.assertTrue(data['message'] == 'Invalid credentials.') self.assertTrue(response.content_type == 'application/json') self.assertEqual(response.status_code, 404) def test_valid_logout(self): - add_user('test', 'test@test.com', 'test') + add_user('test', 'test@test.com', '12345678') with self.client: # user login resp_login = self.client.post( '/api/auth/login', data=json.dumps(dict( email='test@test.com', - password='test' + password='12345678' )), content_type='application/json' ) @@ -155,13 +291,13 @@ class TestAuthBlueprint(BaseTestCase): self.assertEqual(response.status_code, 200) def test_invalid_logout_expired_token(self): - add_user('test', 'test@test.com', 'test') + add_user('test', 'test@test.com', '12345678') with self.client: resp_login = self.client.post( '/api/auth/login', data=json.dumps(dict( email='test@test.com', - password='test' + password='12345678' )), content_type='application/json' ) diff --git a/mpwo_api/mpwo_api/users/auth.py b/mpwo_api/mpwo_api/users/auth.py index d05e14ed..5f36e6ac 100644 --- a/mpwo_api/mpwo_api/users/auth.py +++ b/mpwo_api/mpwo_api/users/auth.py @@ -4,7 +4,7 @@ from sqlalchemy import exc, or_ from mpwo_api import appLog, bcrypt, db from .models import User -from .utils import authenticate +from .utils import authenticate, register_controls auth_blueprint = Blueprint('auth', __name__) @@ -15,7 +15,8 @@ def register_user(): post_data = request.get_json() 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: + or post_data.get('password') is None \ + or post_data.get('password_conf') is None: response_object = { 'status': 'error', 'message': 'Invalid payload.' @@ -24,6 +25,16 @@ def register_user(): username = post_data.get('username') email = post_data.get('email') password = post_data.get('password') + password_conf = post_data.get('password_conf') + + ret = register_controls(username, email, password, password_conf) + if ret != '': + response_object = { + 'status': 'error', + 'message': 'Errors: ' + ret + } + return jsonify(response_object), 400 + try: # check for existing user user = User.query.filter( @@ -90,7 +101,7 @@ def login_user(): else: response_object = { 'status': 'error', - 'message': 'User does not exist.' + 'message': 'Invalid credentials.' } return jsonify(response_object), 404 # handler errors diff --git a/mpwo_api/mpwo_api/users/utils.py b/mpwo_api/mpwo_api/users/utils.py index 0e652ef5..8b9350ae 100644 --- a/mpwo_api/mpwo_api/users/utils.py +++ b/mpwo_api/mpwo_api/users/utils.py @@ -1,4 +1,5 @@ from functools import wraps +import re from flask import request, jsonify @@ -33,3 +34,21 @@ def authenticate(f): def is_admin(user_id): user = User.query.filter_by(id=user_id).first() return user.admin + + +def is_valid_email(email): + mail_pattern = r"(^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$)" + return re.match(mail_pattern, email) is not None + + +def register_controls(username, email, password, password_conf): + ret = '' + if not 2 < len(username) < 13: + ret += 'Username: 3 to 12 characters required.\n' + if not is_valid_email(email): + ret += 'Valid email must be provided.\n' + if password != password_conf: + ret += 'Password and password confirmation don\'t match.\n' + if len(password) < 8: + ret += 'Password: 8 characters required.\n' + return ret diff --git a/mpwo_api/server.py b/mpwo_api/server.py index 5cfbb796..030a9702 100644 --- a/mpwo_api/server.py +++ b/mpwo_api/server.py @@ -26,7 +26,7 @@ def seed_db(): def test(): """Runs the tests without code coverage.""" tests = unittest.TestLoader().discover( - 'mpwo_api/tests', pattern='test*.py') + 'mpwo_api/mpwo_api/tests', pattern='test*.py') result = unittest.TextTestRunner(verbosity=2).run(tests) if result.wasSuccessful(): return 0 diff --git a/mpwo_client/package.json b/mpwo_client/package.json index 1fae1c1f..f9d0b080 100644 --- a/mpwo_client/package.json +++ b/mpwo_client/package.json @@ -7,6 +7,7 @@ "react": "^16.2.0", "react-dom": "^16.2.0", "react-helmet": "^5.2.0", + "react-key-index": "^0.1.1", "react-redux": "^5.0.6", "react-router-dom": "^4.2.2", "react-router-redux": "^5.0.0-alpha.9", diff --git a/mpwo_client/src/actions/index.js b/mpwo_client/src/actions/index.js index ffbded78..b02f4864 100644 --- a/mpwo_client/src/actions/index.js +++ b/mpwo_client/src/actions/index.js @@ -1,9 +1,14 @@ +import keyIndex from 'react-key-index' import mpwoApi from '../mpwoApi' function AuthError(message) { return { type: 'AUTH_ERROR', message } } +function AuthErrors(messages) { + return { type: 'AUTH_ERRORS', messages } +} + function ProfileSuccess(message) { return { type: 'PROFILE_SUCCESS', message } } @@ -27,6 +32,11 @@ const updateFormDataPassword = value => ({ password: value, }) +const updateFormDataPasswordConf = value => ({ + type: 'UPDATE_FORMDATA_PASSWORD_CONF', + passwordConf: value, +}) + export function getProfile(dispatch) { return mpwoApi .getProfile() @@ -46,7 +56,11 @@ export function getProfile(dispatch) { export function register(formData) { return function(dispatch) { return mpwoApi - .register(formData.username, formData.email, formData.password) + .register( + formData.username, + formData.email, + formData.password, + formData.passwordConf) .then(ret => { if (ret.status === 'success') { window.localStorage.setItem('authToken', ret.auth_token) @@ -92,6 +106,20 @@ export function logout() { return { type: 'LOGOUT' } } +function RegisterFormControl (formData) { + const errMsg = [] + if (formData.username.length < 3 || formData.username.length > 12) { + errMsg.push('Username: 3 to 12 characters required.') + } + if (formData.password !== formData.passwordConf) { + errMsg.push('Password and password confirmation don\'t match.') + } + if (formData.password.length < 8) { + errMsg.push('Password: 8 characters required.') + } + return errMsg +} + export function handleUserFormSubmit(event, formType) { event.preventDefault() return (dispatch, getState) => { @@ -101,7 +129,12 @@ export function handleUserFormSubmit(event, formType) { if (formType === 'Login') { dispatch(login(formData.formData)) } else { // formType === 'Register' - dispatch(register(formData.formData)) + const ret = RegisterFormControl(formData.formData) + if (ret.length === 0) { + dispatch(register(formData.formData)) + } else { + dispatch(AuthErrors(keyIndex(ret, 1))) + } } } } @@ -112,7 +145,9 @@ export const handleFormChange = event => dispatch => { return dispatch(updateFormDataEmail(event.target.value)) case 'username': return dispatch(updateFormDataUsername(event.target.value)) - default: // case 'password': + case 'password': return dispatch(updateFormDataPassword(event.target.value)) + default: // case 'password-conf': + return dispatch(updateFormDataPasswordConf(event.target.value)) } } diff --git a/mpwo_client/src/components/App.css b/mpwo_client/src/components/App.css index ccfd12a8..3a3421ff 100644 --- a/mpwo_client/src/components/App.css +++ b/mpwo_client/src/components/App.css @@ -1,5 +1,5 @@ .App { - /*text-align: center;*/ + text-align: center; } .App-logo { @@ -27,6 +27,10 @@ to { transform: rotate(360deg); } } +.card { + text-align: left; +} + .page-title { font-size: 2em; margin: 1em; diff --git a/mpwo_client/src/components/User/Form.jsx b/mpwo_client/src/components/User/Form.jsx index 666c3a4d..ecc6c973 100644 --- a/mpwo_client/src/components/User/Form.jsx +++ b/mpwo_client/src/components/User/Form.jsx @@ -1,10 +1,13 @@ import React from 'react' - +import { Helmet } from 'react-helmet' export default function Form (props) { return (
-

{props.formType}

+ + mpwo - {props.formType} + +

{props.formType}

@@ -13,7 +16,7 @@ export default function Form (props) {
props.handleUserFormSubmit(event, props.formType)} > - {props.formType === 'Register' && + {props.formType === 'Register' &&
- } -
- + } +
+
+ {props.formType === 'Register' && +
+ +
+ } -

+

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

diff --git a/mpwo_client/src/components/User/UserForm.jsx b/mpwo_client/src/components/User/UserForm.jsx index f0099a65..8741fcdb 100644 --- a/mpwo_client/src/components/User/UserForm.jsx +++ b/mpwo_client/src/components/User/UserForm.jsx @@ -16,6 +16,17 @@ function UserForm(props) { { props.message !== '' && ( {props.message} )} + { props.messages.length > 0 && ( + +
    + {props.messages.map(msg => ( +
  • + {msg.value} +
  • + ))} +
+
+ )} ({ formData: state.formData, message: state.message, + messages: state.messages, }), dispatch => ({ onHandleFormChange: event => { diff --git a/mpwo_client/src/mpwoApi.js b/mpwo_client/src/mpwoApi.js index 8168473d..a07e3437 100644 --- a/mpwo_client/src/mpwoApi.js +++ b/mpwo_client/src/mpwoApi.js @@ -17,7 +17,7 @@ export default class MpwoApi { .then(response => response.json()) .catch(error => error) } - static register(username, email, password) { + static register(username, email, password, passwordConf) { const request = new Request(`${apiUrl}auth/register`, { method: 'POST', headers: new Headers({ @@ -27,6 +27,7 @@ export default class MpwoApi { username: username, email: email, password: password, + password_conf: passwordConf, }), }) return fetch(request) diff --git a/mpwo_client/src/reducers/index.js b/mpwo_client/src/reducers/index.js index 3f2aa2eb..8a082d1b 100644 --- a/mpwo_client/src/reducers/index.js +++ b/mpwo_client/src/reducers/index.js @@ -2,20 +2,70 @@ import { combineReducers } from 'redux' import initial from './initial' +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 'UPDATE_FORMDATA_PASSWORD_CONF': + return { + formData: { + ...state.formData, + passwordConf: action.passwordConf + }, + } + case 'PROFILE_SUCCESS': + return initial.formData + default: + return state + } +} + const message = (state = initial.message, action) => { switch (action.type) { case 'AUTH_ERROR': case 'PROFILE_ERROR': return action.message case 'LOGOUT': - return '' case 'PROFILE_SUCCESS': + case '@@router/LOCATION_CHANGE': return '' default: return state } } +const messages = (state = initial.messages, action) => { + switch (action.type) { + case 'AUTH_ERRORS': + return action.messages + case 'LOGOUT': + case 'PROFILE_SUCCESS': + case '@@router/LOCATION_CHANGE': + return [] + default: + return state + } +} + const user = (state = initial.user, action) => { switch (action.type) { case 'AUTH_ERROR': @@ -37,40 +87,11 @@ const user = (state = initial.user, action) => { } } -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, + message, + messages, + user, }) export default reducers diff --git a/mpwo_client/src/reducers/initial.js b/mpwo_client/src/reducers/initial.js index eb3ad5d0..b9f9fd4e 100644 --- a/mpwo_client/src/reducers/initial.js +++ b/mpwo_client/src/reducers/initial.js @@ -1,5 +1,6 @@ export default { message: '', + messages: [], user: { id: '', username: '', @@ -12,7 +13,8 @@ export default { formData: { username: '', email: '', - password: '' + password: '', + passwordConf: '', } }, } diff --git a/package-lock.json b/package-lock.json index 7e475c81..60d29ce1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4059,8 +4059,7 @@ }, "jsbn": { "version": "0.1.1", - "bundled": true, - "optional": true + "bundled": true }, "json-schema": { "version": "0.2.3", @@ -4726,6 +4725,11 @@ "minimalistic-assert": "1.0.0" } }, + "hashids": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/hashids/-/hashids-1.1.4.tgz", + "integrity": "sha512-U/fnTE3edW0AV92ZI/BfEluMZuVcu3MDOopsN7jS+HqDYcarQo8rXQiWlsBlm0uX48/taYSdxRsfzh2HRg5Z6w==" + }, "hawk": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/hawk/-/hawk-6.0.2.tgz", @@ -9311,6 +9315,14 @@ "react-side-effect": "1.1.3" } }, + "react-key-index": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/react-key-index/-/react-key-index-0.1.1.tgz", + "integrity": "sha1-gxnk8JYa5EqOsKT3bkwhDvbTnNo=", + "requires": { + "hashids": "1.1.4" + } + }, "react-redux": { "version": "5.0.6", "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-5.0.6.tgz", diff --git a/package.json b/package.json index 08a00aa8..601e4d1b 100644 --- a/package.json +++ b/package.json @@ -7,6 +7,7 @@ "react": "^16.2.0", "react-dom": "^16.2.0", "react-helmet": "^5.2.0", + "react-key-index": "^0.1.1", "react-redux": "^5.0.6", "react-router-dom": "^4.2.2", "react-router-redux": "^5.0.0-alpha.9",