diff --git a/Makefile b/Makefile index 79091ca8..337bf1f9 100644 --- a/Makefile +++ b/Makefile @@ -2,6 +2,9 @@ include Makefile.config -include Makefile.custom.config .SILENT: +init-db: + $(FLASK) init_db + make-p: # Launch all P targets in parallel and exit as soon as one exits. set -m; (for p in $(P); do ($(MAKE) $$p || kill 0)& done; wait) diff --git a/mpwo_api/mpwo_api/tests/test_auth.py b/mpwo_api/mpwo_api/tests/test_auth.py index ad1b844a..2fb02354 100644 --- a/mpwo_api/mpwo_api/tests/test_auth.py +++ b/mpwo_api/mpwo_api/tests/test_auth.py @@ -2,7 +2,7 @@ import json import time from mpwo_api.tests.base import BaseTestCase -from mpwo_api.tests.utils import add_user +from mpwo_api.tests.utils import add_user, add_user_full class TestAuthBlueprint(BaseTestCase): @@ -328,14 +328,14 @@ class TestAuthBlueprint(BaseTestCase): 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') + def test_user_profile_minimal(self): + 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' ) @@ -356,6 +356,39 @@ class TestAuthBlueprint(BaseTestCase): self.assertFalse(data['data']['admin']) self.assertEqual(response.status_code, 200) + def test_user_profile_full(self): + add_user_full('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='12345678' + )), + 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.assertTrue(data['data']['first_name'] == 'John') + self.assertTrue(data['data']['last_name'] == 'Doe') + self.assertTrue(data['data']['birth_date']) + self.assertTrue(data['data']['bio'] == 'just a random guy') + self.assertTrue(data['data']['location'] == 'somewhere') + self.assertEqual(response.status_code, 200) + def test_invalid_profile(self): with self.client: response = self.client.get( @@ -366,3 +399,89 @@ class TestAuthBlueprint(BaseTestCase): self.assertTrue( data['message'] == 'Invalid token. Please log in again.') self.assertEqual(response.status_code, 401) + + def test_user_profile_valid_update(self): + 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='12345678' + )), + content_type='application/json' + ) + response = self.client.post( + '/api/auth/profile/edit', + content_type='application/json', + data=json.dumps(dict( + first_name='John', + last_name='Doe', + location='Somewhere', + bio='just a random guy', + birth_date='01/01/1980' + )), + 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['message'] == 'User profile updated.') + self.assertEqual(response.status_code, 200) + + def test_user_profile_valid_update_with_one_field(self): + 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='12345678' + )), + content_type='application/json' + ) + response = self.client.post( + '/api/auth/profile/edit', + content_type='application/json', + data=json.dumps(dict( + first_name='John' + )), + 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['message'] == 'User profile updated.') + self.assertEqual(response.status_code, 200) + + def test_user_profile_update_invalid_json(self): + 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='12345678' + )), + content_type='application/json' + ) + response = self.client.post( + '/api/auth/profile/edit', + content_type='application/json', + data=json.dumps(dict()), + headers=dict( + Authorization='Bearer ' + json.loads( + resp_login.data.decode() + )['auth_token'] + ) + ) + data = json.loads(response.data.decode()) + self.assertEqual(response.status_code, 400) + self.assertIn('Invalid payload.', data['message']) + self.assertIn('error', data['status']) diff --git a/mpwo_api/mpwo_api/tests/utils.py b/mpwo_api/mpwo_api/tests/utils.py index 9acdd6fc..915c3399 100644 --- a/mpwo_api/mpwo_api/tests/utils.py +++ b/mpwo_api/mpwo_api/tests/utils.py @@ -1,3 +1,5 @@ +import datetime + from mpwo_api import db from mpwo_api.users.models import User @@ -7,3 +9,15 @@ def add_user(username, email, password): db.session.add(user) db.session.commit() return user + + +def add_user_full(username, email, password): + user = User(username=username, email=email, password=password) + user.first_name = 'John' + user.last_name = 'Doe' + user.bio = 'just a random guy' + user.location = 'somewhere' + user.birth_date = datetime.datetime.strptime('01/01/1980', '%d/%m/%Y') + db.session.add(user) + db.session.commit() + return user diff --git a/mpwo_api/mpwo_api/users/auth.py b/mpwo_api/mpwo_api/users/auth.py index 5f36e6ac..d7f1df1f 100644 --- a/mpwo_api/mpwo_api/users/auth.py +++ b/mpwo_api/mpwo_api/users/auth.py @@ -1,3 +1,4 @@ +import datetime from flask import Blueprint, jsonify, request from sqlalchemy import exc, or_ @@ -155,6 +156,57 @@ def get_user_status(user_id): 'email': user.email, 'created_at': user.created_at, 'admin': user.admin, + 'first_name': user.first_name, + 'last_name': user.last_name, + 'bio': user.bio, + 'location': user.location, + 'birth_date': user.birth_date, } } return jsonify(response_object), 200 + + +@auth_blueprint.route('/auth/profile/edit', methods=['POST']) +@authenticate +def edit_user(user_id): + # get post data + post_data = request.get_json() + if not post_data: + response_object = { + 'status': 'error', + 'message': 'Invalid payload.' + } + return jsonify(response_object), 400 + first_name = post_data.get('first_name') + last_name = post_data.get('last_name') + bio = post_data.get('bio') + birth_date = post_data.get('birth_date') + location = post_data.get('location') + try: + user = User.query.filter_by(id=user_id).first() + user.first_name = first_name + user.last_name = last_name + user.bio = bio + user.location = location + user.birth_date = ( + datetime.datetime.strptime(birth_date, '%d/%m/%Y') + if birth_date + else None + ) + db.session.commit() + + response_object = { + 'status': 'success', + 'message': 'User profile updated.' + } + return jsonify(response_object), 200 + + # handler errors + except (exc.IntegrityError, exc.OperationalError, ValueError) as e: + db.session.rollback() + appLog.error(e) + response_object = { + 'status': 'error', + 'message': 'Error. Please try again or contact the administrator.' + } + return jsonify(response_object), 500 diff --git a/mpwo_api/mpwo_api/users/models.py b/mpwo_api/mpwo_api/users/models.py index d47199cc..ea86b7ff 100644 --- a/mpwo_api/mpwo_api/users/models.py +++ b/mpwo_api/mpwo_api/users/models.py @@ -9,18 +9,23 @@ from mpwo_api import bcrypt, db class User(db.Model): __tablename__ = "users" id = db.Column(db.Integer, primary_key=True, autoincrement=True) - username = db.Column(db.String(80), unique=True, nullable=False) + username = db.Column(db.String(20), unique=True, nullable=False) email = db.Column(db.String(120), unique=True, nullable=False) password = db.Column(db.String(255), nullable=False) created_at = db.Column(db.DateTime, nullable=False) admin = db.Column(db.Boolean, default=False, nullable=False) + first_name = db.Column(db.String(80), nullable=True) + last_name = db.Column(db.String(80), nullable=True) + birth_date = db.Column(db.DateTime, nullable=True) + location = db.Column(db.String(80), nullable=True) + bio = db.Column(db.String(200), nullable=True) def __repr__(self): return '' % self.username def __init__( self, username, email, password, - created_at=datetime.datetime.utcnow()): + created_at=datetime.datetime.now()): self.username = username self.email = email self.password = bcrypt.generate_password_hash( @@ -30,7 +35,11 @@ class User(db.Model): @staticmethod def encode_auth_token(user_id): - """Generates the auth token""" + """ + Generates the auth token + :param user_id: - + :return: JWToken + """ try: payload = { 'exp': datetime.datetime.utcnow() + datetime.timedelta( diff --git a/mpwo_api/server.py b/mpwo_api/server.py index 030a9702..92c30964 100644 --- a/mpwo_api/server.py +++ b/mpwo_api/server.py @@ -5,21 +5,18 @@ from mpwo_api.users.models import User @app.cli.command() -def recreate_db(): - """Recreates a database.""" +def init_db(): + """Init the database.""" db.drop_all() db.create_all() - db.session.commit() - print('Database (re)creation done.') - - -@app.cli.command() -def seed_db(): - """Seeds the database.""" - admin = User(username='admin', email='admin@example.com', password='admin') + admin = User( + username='admin', + email='admin@example.com', + password='mpwoadmin') admin.admin = True db.session.add(admin) db.session.commit() + print('Database initialization done.') @app.cli.command() diff --git a/mpwo_client/src/actions/index.js b/mpwo_client/src/actions/index.js index b02f4864..d7c21acd 100644 --- a/mpwo_client/src/actions/index.js +++ b/mpwo_client/src/actions/index.js @@ -1,5 +1,7 @@ import keyIndex from 'react-key-index' + import mpwoApi from '../mpwoApi' +import { history } from '../index' function AuthError(message) { return { type: 'AUTH_ERROR', message } @@ -17,25 +19,21 @@ function ProfileError(message) { return { type: 'PROFILE_ERROR', message } } -const updateFormDataEmail = value => ({ - type: 'UPDATE_FORMDATA_EMAIL', - email: value, +export const handleFormChange = (target, value) => ({ + type: 'UPDATE_USER_FORMDATA', + target, + value, }) -const updateFormDataUsername = value => ({ - type: 'UPDATE_FORMDATA_USERNAME', - username: value, +export const updateProfileFormData = (target, value) => ({ + type: 'UPDATE_PROFILE_FORMDATA', + target, + value, }) -const updateFormDataPassword = value => ({ - type: 'UPDATE_FORMDATA_PASSWORD', - password: value, -}) - -const updateFormDataPasswordConf = value => ({ - type: 'UPDATE_FORMDATA_PASSWORD_CONF', - passwordConf: value, -}) +function initProfileFormData(user) { + return { type: 'INIT_PROFILE_FORM', user } +} export function getProfile(dispatch) { return mpwoApi @@ -43,7 +41,6 @@ export function getProfile(dispatch) { .then(ret => { if (ret.status === 'success') { dispatch(ProfileSuccess(ret)) - // window.location.href = '/' } else { dispatch(ProfileError(ret.message)) } @@ -139,15 +136,29 @@ export function handleUserFormSubmit(event, formType) { } } -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)) - case 'password': - return dispatch(updateFormDataPassword(event.target.value)) - default: // case 'password-conf': - return dispatch(updateFormDataPasswordConf(event.target.value)) +export function initProfileForm () { + return (dispatch, getState) => { + const state = getState() + dispatch(initProfileFormData(state.user)) + } +} + +export function handleProfileFormSubmit(event) { + event.preventDefault() + return (dispatch, getState) => { + const state = getState() + return mpwoApi + .updateProfile(state.formProfile.formProfile) + .then(ret => { + if (ret.status === 'success') { + getProfile(dispatch) + history.push('/profile') + } else { + dispatch(AuthError(ret.message)) + } + }) + .catch(error => { + throw error + }) } } diff --git a/mpwo_client/src/components/App.css b/mpwo_client/src/components/App.css index 3a3421ff..5ffd7aff 100644 --- a/mpwo_client/src/components/App.css +++ b/mpwo_client/src/components/App.css @@ -31,6 +31,14 @@ text-align: left; } +label { + width: 100%; +} + +input, textarea { + width: 100%; +} + .page-title { font-size: 2em; margin: 1em; diff --git a/mpwo_client/src/components/App.jsx b/mpwo_client/src/components/App.jsx index 006b219d..7ac929c9 100644 --- a/mpwo_client/src/components/App.jsx +++ b/mpwo_client/src/components/App.jsx @@ -7,6 +7,7 @@ import Logout from './User/Logout' import NavBar from './NavBar' import NotFound from './NotFound' import Profile from './User/Profile' +import ProfileEdit from './User/ProfileEdit' import UserForm from './User/UserForm' import { isLoggedIn } from '../utils' @@ -57,6 +58,18 @@ export default class App extends React.Component { )} /> + ( + isLoggedIn() ? ( + + ) : ( + + ) + )} + /> ( diff --git a/mpwo_client/src/components/User/Form.jsx b/mpwo_client/src/components/User/Form.jsx index ecc6c973..ba4e46a2 100644 --- a/mpwo_client/src/components/User/Form.jsx +++ b/mpwo_client/src/components/User/Form.jsx @@ -51,10 +51,10 @@ export default function Form (props) { {props.formType === 'Register' &&
diff --git a/mpwo_client/src/components/User/Profile.jsx b/mpwo_client/src/components/User/Profile.jsx index 4cec2c85..c3a65a21 100644 --- a/mpwo_client/src/components/User/Profile.jsx +++ b/mpwo_client/src/components/User/Profile.jsx @@ -28,7 +28,12 @@ function Profile ({ user }) {

Email : {user.email}

-

Registration date : {user.createdAt}

+

Registration Date : {user.createdAt}

+

First Name : {user.firstName}

+

Last Name : {user.lastName}

+

Birth Date : {user.birthDate}

+

Location : {user.location}

+

Bio : {user.bio}

diff --git a/mpwo_client/src/components/User/ProfileEdit.jsx b/mpwo_client/src/components/User/ProfileEdit.jsx new file mode 100644 index 00000000..18c48e7f --- /dev/null +++ b/mpwo_client/src/components/User/ProfileEdit.jsx @@ -0,0 +1,162 @@ +import React from 'react' +import { Helmet } from 'react-helmet' +import { connect } from 'react-redux' + +import { + initProfileForm, + updateProfileFormData, + handleProfileFormSubmit +} from '../../actions' + + +class ProfileEdit extends React.Component { + componentDidMount() { + this.props.initForm(this.props.user) + } + render () { + const { formProfile, + onHandleFormChange, + onHandleProfileFormSubmit, + user + } = this.props + return ( +
+ + mpwo - {user.username} - Edit Profile + +
+

Profile Edition

+
+
+
+
+
+ {user.username} +
+
+
+
+
+ onHandleProfileFormSubmit(event)} + > +
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+