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 (