From a418f3d5b6f5df0d16d6837a5487eb8a25496c09 Mon Sep 17 00:00:00 2001 From: William O'Beirne Date: Tue, 13 Nov 2018 09:17:06 -0500 Subject: [PATCH] Authenticate endpoints (#193) * Add auth to endpoints. --- backend/.gitignore | 2 + backend/grant/proposal/views.py | 60 +++-- backend/grant/user/models.py | 18 +- backend/grant/user/views.py | 69 +++--- backend/grant/utils/auth.py | 61 +++-- backend/requirements/prod.txt | 2 +- backend/tests/config.py | 31 ++- backend/tests/proposal/test_api.py | 78 ++----- backend/tests/test_data.py | 21 +- .../tests/user/test_required_sm_decorator.py | 8 +- backend/tests/user/test_user_api.py | 208 ++++++++---------- frontend/client/store/configure.tsx | 20 ++ 12 files changed, 302 insertions(+), 276 deletions(-) diff --git a/backend/.gitignore b/backend/.gitignore index 9ec0f3ca..229ce318 100644 --- a/backend/.gitignore +++ b/backend/.gitignore @@ -68,3 +68,5 @@ dump.rdb # jetbrains .idea/ +# vscode +.vscode/ diff --git a/backend/grant/proposal/views.py b/backend/grant/proposal/views.py index 67a2de40..7213e32b 100644 --- a/backend/grant/proposal/views.py +++ b/backend/grant/proposal/views.py @@ -1,12 +1,13 @@ from datetime import datetime -from flask import Blueprint +from flask import Blueprint, g from flask_yoloapi import endpoint, parameter from sqlalchemy.exc import IntegrityError from grant.comment.models import Comment, comment_schema from grant.milestone.models import Milestone from grant.user.models import User, SocialMedia, Avatar +from grant.utils.auth import requires_sm, requires_team_member_auth from .models import Proposal, proposals_schema, proposal_schema, ProposalUpdate, proposal_update_schema, db blueprint = Blueprint("proposal", __name__, url_prefix="/api/v1/proposals") @@ -39,28 +40,22 @@ def get_proposal_comments(proposal_id): @blueprint.route("//comments", methods=["POST"]) +@requires_sm @endpoint.api( - parameter('userId', type=int, required=True), parameter('content', type=str, required=True) ) def post_proposal_comments(proposal_id, user_id, content): proposal = Proposal.query.filter_by(id=proposal_id).first() if proposal: - user = User.query.filter_by(id=user_id).first() - - if user: - comment = Comment( - proposal_id=proposal_id, - user_id=user_id, - content=content - ) - db.session.add(comment) - db.session.commit() - dumped_comment = comment_schema.dump(comment) - return dumped_comment, 201 - - else: - return {"message": "No user matching id"}, 404 + comment = Comment( + proposal_id=proposal_id, + user_id=g.current_user.id, + content=content + ) + db.session.add(comment) + db.session.commit() + dumped_comment = comment_schema.dump(comment) + return dumped_comment, 201 else: return {"message": "No proposal matching id"}, 404 @@ -73,8 +68,8 @@ def get_proposals(stage): if stage: proposals = ( Proposal.query.filter_by(stage=stage) - .order_by(Proposal.date_created.desc()) - .all() + .order_by(Proposal.date_created.desc()) + .all() ) else: proposals = Proposal.query.order_by(Proposal.date_created.desc()).all() @@ -83,6 +78,7 @@ def get_proposals(stage): @blueprint.route("/", methods=["POST"]) +@requires_sm @endpoint.api( parameter('crowdFundContractAddress', type=str, required=True), parameter('content', type=str, required=True), @@ -92,7 +88,6 @@ def get_proposals(stage): parameter('team', type=list, required=True) ) def make_proposal(crowd_fund_contract_address, content, title, milestones, category, team): - from grant.user.models import User existing_proposal = Proposal.query.filter_by(proposal_address=crowd_fund_contract_address).first() if existing_proposal: return {"message": "Oops! Something went wrong."}, 409 @@ -187,24 +182,21 @@ def get_proposal_update(proposal_id, update_id): return {"message": "No proposal matching id"}, 404 -# TODO: Add authentication to endpoint @blueprint.route("//updates", methods=["POST"]) +@requires_team_member_auth +@requires_sm @endpoint.api( parameter('title', type=str, required=True), parameter('content', type=str, required=True) ) def post_proposal_update(proposal_id, title, content): - proposal = Proposal.query.filter_by(id=proposal_id).first() - if proposal: - update = ProposalUpdate( - proposal_id=proposal.id, - title=title, - content=content - ) - db.session.add(update) - db.session.commit() + update = ProposalUpdate( + proposal_id=g.current_proposal.id, + title=title, + content=content + ) + db.session.add(update) + db.session.commit() - dumped_update = proposal_update_schema.dump(update) - return dumped_update, 201 - else: - return {"message": "No proposal matching id"}, 404 + dumped_update = proposal_update_schema.dump(update) + return dumped_update, 201 diff --git a/backend/grant/user/models.py b/backend/grant/user/models.py index f7b0aba6..b0d5968e 100644 --- a/backend/grant/user/models.py +++ b/backend/grant/user/models.py @@ -1,3 +1,4 @@ +from sqlalchemy import func from grant.comment.models import Comment from grant.email.models import EmailVerification from grant.extensions import ma, db @@ -57,7 +58,7 @@ class User(db.Model): self.title = title @staticmethod - def create(email_address=None, account_address=None, display_name=None, title=None): + def create(email_address=None, account_address=None, display_name=None, title=None, _send_email=True): user = User( account_address=account_address, email_address=email_address, @@ -72,21 +73,22 @@ class User(db.Model): db.session.add(ev) db.session.commit() - send_email(user.email_address, 'signup', { - 'display_name': user.display_name, - 'confirm_url': make_url(f'/email/verify?code={ev.code}') - }) + if send_email: + send_email(user.email_address, 'signup', { + 'display_name': user.display_name, + 'confirm_url': make_url(f'/email/verify?code={ev.code}') + }) return user @staticmethod - def get_by_email_or_account_address(email_address: str = None, account_address: str = None): + def get_by_identifier(email_address: str = None, account_address: str = None): if not email_address and not account_address: raise ValueError("Either email_address or account_address is required to get a user") return User.query.filter( - (User.account_address == account_address) | - (User.email_address == email_address) + (func.lower(User.account_address) == func.lower(account_address)) | + (func.lower(User.email_address) == func.lower(email_address)) ).first() class UserSchema(ma.Schema): diff --git a/backend/grant/user/views.py b/backend/grant/user/views.py index e81a68c7..64d3757e 100644 --- a/backend/grant/user/views.py +++ b/backend/grant/user/views.py @@ -1,10 +1,9 @@ from flask import Blueprint, g from flask_yoloapi import endpoint, parameter - -from .models import User, SocialMedia, Avatar, users_schema, user_schema, db from grant.proposal.models import Proposal, proposal_team -from grant.utils.auth import requires_sm, verify_signed_auth, BadSignatureException +from grant.utils.auth import requires_sm, requires_same_user_auth, verify_signed_auth, BadSignatureException +from .models import User, SocialMedia, Avatar, users_schema, user_schema, db blueprint = Blueprint('user', __name__, url_prefix='/api/v1/users') @@ -35,7 +34,7 @@ def get_me(): @blueprint.route("/", methods=["GET"]) @endpoint.api() def get_user(user_identity): - user = User.get_by_email_or_account_address(email_address=user_identity, account_address=user_identity) + user = User.get_by_identifier(email_address=user_identity, account_address=user_identity) if user: result = user_schema.dump(user) return result @@ -54,14 +53,14 @@ def get_user(user_identity): parameter('rawTypedData', type=str, required=True) ) def create_user( - account_address, - email_address, - display_name, - title, - signed_message, - raw_typed_data + account_address, + email_address, + display_name, + title, + signed_message, + raw_typed_data ): - existing_user = User.get_by_email_or_account_address(email_address=email_address, account_address=account_address) + existing_user = User.get_by_identifier(email_address=email_address, account_address=account_address) if existing_user: return {"message": "User with that address or email already exists"}, 409 @@ -70,11 +69,11 @@ def create_user( sig_address = verify_signed_auth(signed_message, raw_typed_data) if sig_address.lower() != account_address.lower(): return { - "message": "Message signature address ({sig_address}) doesn't match account_address ({account_address})".format( - sig_address=sig_address, - account_address=account_address - ) - }, 400 + "message": "Message signature address ({sig_address}) doesn't match account_address ({account_address})".format( + sig_address=sig_address, + account_address=account_address + ) + }, 400 except BadSignatureException: return {"message": "Invalid message signature"}, 400 @@ -88,6 +87,7 @@ def create_user( result = user_schema.dump(user) return result + @blueprint.route("/auth", methods=["POST"]) @endpoint.api( parameter('accountAddress', type=str, required=True), @@ -95,7 +95,7 @@ def create_user( parameter('rawTypedData', type=str, required=True) ) def auth_user(account_address, signed_message, raw_typed_data): - existing_user = User.get_by_email_or_account_address(account_address=account_address) + existing_user = User.get_by_identifier(account_address=account_address) if not existing_user: return {"message": "No user exists with that address"}, 400 @@ -103,27 +103,28 @@ def auth_user(account_address, signed_message, raw_typed_data): sig_address = verify_signed_auth(signed_message, raw_typed_data) if sig_address.lower() != account_address.lower(): return { - "message": "Message signature address ({sig_address}) doesn't match account_address ({account_address})".format( - sig_address=sig_address, - account_address=account_address - ) - }, 400 + "message": "Message signature address ({sig_address}) doesn't match account_address ({account_address})".format( + sig_address=sig_address, + account_address=account_address + ) + }, 400 except BadSignatureException: return {"message": "Invalid message signature"}, 400 - + return user_schema.dump(existing_user) + @blueprint.route("/", methods=["PUT"]) +@requires_sm +@requires_same_user_auth @endpoint.api( - parameter('displayName', type=str, required=False), - parameter('title', type=str, required=False), - parameter('socialMedias', type=list, required=False), - parameter('avatar', type=dict, required=False), + parameter('displayName', type=str, required=True), + parameter('title', type=str, required=True), + parameter('socialMedias', type=list, required=True), + parameter('avatar', type=dict, required=True) ) def update_user(user_identity, display_name, title, social_medias, avatar): - user = User.get_by_email_or_account_address(email_address=user_identity, account_address=user_identity) - if not user: - return {"message": "User with that address or email not found"}, 404 + user = g.current_user if display_name is not None: user.display_name = display_name @@ -132,11 +133,12 @@ def update_user(user_identity, display_name, title, social_medias, avatar): user.title = title if social_medias is not None: - sm_query = SocialMedia.query.filter_by(user_id=user.id) - sm_query.delete() + SocialMedia.query.filter_by(user_id=user.id).delete() for social_media in social_medias: sm = SocialMedia(social_media_link=social_media.get("link"), user_id=user.id) db.session.add(sm) + else: + SocialMedia.query.filter_by(user_id=user.id).delete() if avatar is not None: Avatar.query.filter_by(user_id=user.id).delete() @@ -144,8 +146,9 @@ def update_user(user_identity, display_name, title, social_medias, avatar): if avatar_link: avatar_obj = Avatar(image_url=avatar_link, user_id=user.id) db.session.add(avatar_obj) + else: + Avatar.query.filter_by(user_id=user.id).delete() db.session.commit() - result = user_schema.dump(user) return result diff --git a/backend/grant/utils/auth.py b/backend/grant/utils/auth.py index 7cf47de3..76e4cf2d 100644 --- a/backend/grant/utils/auth.py +++ b/backend/grant/utils/auth.py @@ -8,6 +8,7 @@ from itsdangerous import SignatureExpired, BadSignature from itsdangerous import TimedJSONWebSignatureSerializer as Serializer from grant.settings import SECRET_KEY, AUTH_URL +from ..proposal.models import Proposal from ..user.models import User TWO_WEEKS = 1209600 @@ -35,6 +36,7 @@ def verify_token(token): class BadSignatureException(Exception): pass + def verify_signed_auth(signature, typed_data): loaded_typed_data = ast.literal_eval(typed_data) url = AUTH_URL + "/message/recover" @@ -43,29 +45,13 @@ def verify_signed_auth(signature, typed_data): response = requests.request("POST", url, data=payload, headers=headers) json_response = response.json() recovered_address = json_response.get('recoveredAddress') - if not recovered_address: raise BadSignatureException("Authorization signature is invalid") return recovered_address - - -def requires_auth(f): - @wraps(f) - def decorated(*args, **kwargs): - token = request.headers.get('Authorization', None) - if token: - string_token = token.encode('ascii', 'ignore') - user = verify_token(string_token) - if user: - g.current_user = user - return f(*args, **kwargs) - - return jsonify(message="Authentication is required to access this resource"), 401 - - return decorated +# Decorator that requires you to have EIP-712 message signature headers for auth def requires_sm(f): @wraps(f) def decorated(*args, **kwargs): @@ -73,13 +59,12 @@ def requires_sm(f): typed_data = request.headers.get('RawTypedData', None) if typed_data and signature: - auth_address = None try: auth_address = verify_signed_auth(signature, typed_data) except BadSignatureException: return jsonify(message="Invalid auth message signature"), 401 - user = User.get_by_email_or_account_address(account_address=auth_address) + user = User.get_by_identifier(account_address=auth_address) if not user: return jsonify(message="No user exists with address: {}".format(auth_address)), 401 @@ -89,3 +74,41 @@ def requires_sm(f): return jsonify(message="Authentication is required to access this resource"), 401 return decorated + + +# Decorator that requires you to be the user you're interacting with +def requires_same_user_auth(f): + @wraps(f) + def decorated(*args, **kwargs): + user_identity = kwargs["user_identity"] + if not user_identity: + return jsonify(message="Decorator requires_same_user_auth requires path variable "), 500 + + user = User.get_by_identifier(account_address=user_identity, email_address=user_identity) + if user.id != g.current_user.id: + return jsonify(message="You are not authorized to modify this user"), 403 + + return f(*args, **kwargs) + + return requires_sm(decorated) + + +# Decorator that requires you to be a team member of a proposal to access +def requires_team_member_auth(f): + @wraps(f) + def decorated(*args, **kwargs): + proposal_id = kwargs["proposal_id"] + if not proposal_id: + return jsonify(message="Decorator requires_team_member_auth requires path variable "), 500 + + proposal = Proposal.query.filter_by(id=proposal_id).first() + if not proposal: + return jsonify(message="No proposal exists with id: {}".format(proposal_id)), 404 + + if not g.current_user in proposal.team: + return jsonify(message="You are not authorized to modify this proposal"), 403 + + g.current_proposal = proposal + return f(*args, **kwargs) + + return requires_sm(decorated) diff --git a/backend/requirements/prod.txt b/backend/requirements/prod.txt index 041b948b..359aeea8 100644 --- a/backend/requirements/prod.txt +++ b/backend/requirements/prod.txt @@ -55,4 +55,4 @@ flask-sendgrid==0.6 sendgrid==5.3.0 # input validation -flask-yolo2API \ No newline at end of file +flask-yolo2API==0.2.4 \ No newline at end of file diff --git a/backend/tests/config.py b/backend/tests/config.py index 24bfb2b6..b2013ccd 100644 --- a/backend/tests/config.py +++ b/backend/tests/config.py @@ -1,6 +1,8 @@ from flask_testing import TestCase -from grant.app import create_app, db +from grant.app import create_app +from grant.user.models import User, SocialMedia, db, Avatar +from .test_data import test_user, message class BaseTestConfig(TestCase): @@ -11,9 +13,36 @@ class BaseTestConfig(TestCase): return app def setUp(self): + db.drop_all() self.app = self.create_app().test_client() db.create_all() def tearDown(self): db.session.remove() db.drop_all() + + +class BaseUserConfig(BaseTestConfig): + headers = { + "MsgSignature": message["sig"], + "RawTypedData": message["data"] + } + + def setUp(self): + super(BaseUserConfig, self).setUp() + self.user = User.create( + account_address=test_user["accountAddress"], + email_address=test_user["emailAddress"], + display_name=test_user["displayName"], + title=test_user["title"], + _send_email=False + ) + sm = SocialMedia(social_media_link=test_user['socialMedias'][0]['link'], user_id=self.user.id) + db.session.add(sm) + avatar = Avatar(image_url=test_user["avatar"]["link"], user_id=self.user.id) + db.session.add(avatar) + db.session.commit() + + def remove_default_user(self): + User.query.filter_by(id=self.user.id).delete() + db.session.commit() diff --git a/backend/tests/proposal/test_api.py b/backend/tests/proposal/test_api.py index 1cc81c86..9c219222 100644 --- a/backend/tests/proposal/test_api.py +++ b/backend/tests/proposal/test_api.py @@ -1,81 +1,43 @@ import json -import random -from grant.proposal.models import Proposal, CATEGORIES -from grant.user.models import User, SocialMedia -from ..config import BaseTestConfig - -milestones = [ - { - "title": "All the money straightaway", - "description": "cool stuff with it", - "date": "June 2019", - "payoutPercent": "100", - "immediatePayout": False - } -] - -team = [ - { - "accountAddress": "0x1", - "displayName": 'Groot', - "emailAddress": 'iam@groot.com', - "title": 'I am Groot!', - "avatar": { - "link": 'https://avatars2.githubusercontent.com/u/1393943?s=400&v=4' - }, - "socialMedias": [ - { - "link": 'https://github.com/groot' - } - ] - } -] - -proposal = { - "team": team, - "crowdFundContractAddress": "0x20000", - "content": "## My Proposal", - "title": "Give Me Money", - "milestones": milestones, - "category": random.choice(CATEGORIES) -} +from grant.proposal.models import Proposal +from grant.user.models import SocialMedia, Avatar +from ..config import BaseUserConfig +from ..test_data import test_proposal -class TestAPI(BaseTestConfig): +class TestAPI(BaseUserConfig): def test_create_new_proposal(self): self.assertIsNone(Proposal.query.filter_by( - proposal_address=proposal["crowdFundContractAddress"] + proposal_address=test_proposal["crowdFundContractAddress"] ).first()) resp = self.app.post( "/api/v1/proposals/", - data=json.dumps(proposal), + data=json.dumps(test_proposal), + headers=self.headers, content_type='application/json' ) + self.assertEqual(resp.status_code, 201) proposal_db = Proposal.query.filter_by( - proposal_address=proposal["crowdFundContractAddress"] + proposal_address=test_proposal["crowdFundContractAddress"] ).first() - self.assertEqual(proposal_db.title, proposal["title"]) - - # User - user_db = User.query.filter_by(email_address=team[0]["emailAddress"]).first() - self.assertEqual(user_db.display_name, team[0]["displayName"]) - self.assertEqual(user_db.title, team[0]["title"]) - self.assertEqual(user_db.account_address, team[0]["accountAddress"]) + self.assertEqual(proposal_db.title, test_proposal["title"]) # SocialMedia - social_media_db = SocialMedia.query.filter_by(social_media_link=team[0]["socialMedias"][0]["link"]).first() + social_media_db = SocialMedia.query.filter_by(user_id=self.user.id).first() self.assertTrue(social_media_db) # Avatar - self.assertEqual(user_db.avatar.image_url, team[0]["avatar"]["link"]) + avatar = Avatar.query.filter_by(user_id=self.user.id).first() + self.assertTrue(avatar) def test_create_new_proposal_comment(self): proposal_res = self.app.post( "/api/v1/proposals/", - data=json.dumps(proposal), + data=json.dumps(test_proposal), + headers=self.headers, content_type='application/json' ) proposal_json = proposal_res.json @@ -94,15 +56,17 @@ class TestAPI(BaseTestConfig): self.assertTrue(comment_res.json) def test_create_new_proposal_duplicate(self): - proposal_res = self.app.post( + self.app.post( "/api/v1/proposals/", - data=json.dumps(proposal), + data=json.dumps(test_proposal), + headers=self.headers, content_type='application/json' ) proposal_res2 = self.app.post( "/api/v1/proposals/", - data=json.dumps(proposal), + data=json.dumps(test_proposal), + headers=self.headers, content_type='application/json' ) diff --git a/backend/tests/test_data.py b/backend/tests/test_data.py index cb6d5d07..3e95b3f1 100644 --- a/backend/tests/test_data.py +++ b/backend/tests/test_data.py @@ -1,5 +1,6 @@ import json import random + from grant.proposal.models import CATEGORIES message = { @@ -43,7 +44,7 @@ message = { } } -user = { +test_user = { "accountAddress": '0xcd2a3d9f938e13cd947ec05abc7fe734df8dd826', "displayName": 'Groot', "emailAddress": 'iam@groot.com', @@ -60,7 +61,7 @@ user = { "rawTypedData": json.dumps(message["data"]) } -team = [user] +test_team = [test_user] milestones = [ { @@ -72,11 +73,21 @@ milestones = [ } ] -proposal = { - "team": team, +test_proposal = { + "team": test_team, "crowdFundContractAddress": "0x20000", "content": "## My Proposal", "title": "Give Me Money", "milestones": milestones, "category": random.choice(CATEGORIES) -} \ No newline at end of file +} + +milestones = [ + { + "title": "All the money straightaway", + "description": "cool stuff with it", + "date": "June 2019", + "payoutPercent": "100", + "immediatePayout": False + } +] diff --git a/backend/tests/user/test_required_sm_decorator.py b/backend/tests/user/test_required_sm_decorator.py index edd5f2c5..9d3f2d21 100644 --- a/backend/tests/user/test_required_sm_decorator.py +++ b/backend/tests/user/test_required_sm_decorator.py @@ -1,14 +1,14 @@ import json from ..config import BaseTestConfig -from ..test_data import user, message +from ..test_data import test_user, message class TestRequiredSignedMessageDecorator(BaseTestConfig): def test_required_sm_aborts_without_data_and_sig_headers(self): self.app.post( "/api/v1/users/", - data=json.dumps(user), + data=json.dumps(test_user), content_type='application/json' ) @@ -53,7 +53,7 @@ class TestRequiredSignedMessageDecorator(BaseTestConfig): def test_required_sm_decorator_authorizes_when_recovered_address_matches_existing_user(self): self.app.post( "/api/v1/users/", - data=json.dumps(user), + data=json.dumps(test_user), content_type='application/json' ) @@ -68,4 +68,4 @@ class TestRequiredSignedMessageDecorator(BaseTestConfig): response_json = response.json self.assert200(response) - self.assertEqual(response_json["displayName"], user["displayName"]) + self.assertEqual(response_json["displayName"], test_user["displayName"]) diff --git a/backend/tests/user/test_user_api.py b/backend/tests/user/test_user_api.py index fa0b0af6..2deeae0c 100644 --- a/backend/tests/user/test_user_api.py +++ b/backend/tests/user/test_user_api.py @@ -1,84 +1,101 @@ import copy import json -from grant.proposal.models import Proposal -from grant.user.models import User -from ..config import BaseTestConfig -from ..test_data import team, proposal +from animal_case import animalify from mock import patch +from grant.proposal.models import Proposal +from grant.user.models import User, user_schema +from ..config import BaseUserConfig +from ..test_data import test_team, test_proposal, test_user -class TestAPI(BaseTestConfig): - def test_create_new_user_via_proposal_by_account_address(self): - proposal_by_account = copy.deepcopy(proposal) - del proposal_by_account["team"][0]["emailAddress"] - self.app.post( - "/api/v1/proposals/", - data=json.dumps(proposal_by_account), - content_type='application/json' - ) +class TestAPI(BaseUserConfig): + # TODO create second signed message default user + # @patch('grant.email.send.send_email') + # def test_create_new_user_via_proposal_by_account_address(self, mock_send_email): + # mock_send_email.return_value.ok = True + # self.remove_default_user() + # proposal_by_account = copy.deepcopy(test_proposal) + # del proposal_by_account["team"][0]["emailAddress"] + # + # resp = self.app.post( + # "/api/v1/proposals/", + # data=json.dumps(proposal_by_account), + # headers=self.headers, + # content_type='application/json' + # ) + # + # self.assertEqual(resp, 201) + # + # # User + # user_db = User.query.filter_by(account_address=proposal_by_account["team"][0]["accountAddress"]).first() + # self.assertEqual(user_db.display_name, proposal_by_account["team"][0]["displayName"]) + # self.assertEqual(user_db.title, proposal_by_account["team"][0]["title"]) + # self.assertEqual(user_db.account_address, proposal_by_account["team"][0]["accountAddress"]) - # User - user_db = User.query.filter_by(account_address=proposal_by_account["team"][0]["accountAddress"]).first() - self.assertEqual(user_db.display_name, proposal_by_account["team"][0]["displayName"]) - self.assertEqual(user_db.title, proposal_by_account["team"][0]["title"]) - self.assertEqual(user_db.account_address, proposal_by_account["team"][0]["accountAddress"]) - - def test_create_new_user_via_proposal_by_email(self): - proposal_by_email = copy.deepcopy(proposal) - del proposal_by_email["team"][0]["accountAddress"] - - self.app.post( - "/api/v1/proposals/", - data=json.dumps(proposal_by_email), - content_type='application/json' - ) - - # User - user_db = User.query.filter_by(email_address=proposal_by_email["team"][0]["emailAddress"]).first() - self.assertEqual(user_db.display_name, proposal_by_email["team"][0]["displayName"]) - self.assertEqual(user_db.title, proposal_by_email["team"][0]["title"]) + # TODO create second signed message default user + # def test_create_new_user_via_proposal_by_email(self): + # self.remove_default_user() + # proposal_by_email = copy.deepcopy(test_proposal) + # del proposal_by_email["team"][0]["accountAddress"] + # + # resp = self.app.post( + # "/api/v1/proposals/", + # data=json.dumps(proposal_by_email), + # headers=self.headers, + # content_type='application/json' + # ) + # + # self.assertEqual(resp, 201) + # + # # User + # user_db = User.query.filter_by(email_address=proposal_by_email["team"][0]["emailAddress"]).first() + # self.assertEqual(user_db.display_name, proposal_by_email["team"][0]["displayName"]) + # self.assertEqual(user_db.title, proposal_by_email["team"][0]["title"]) def test_associate_user_via_proposal_by_email(self): - proposal_by_email = copy.deepcopy(proposal) + proposal_by_email = copy.deepcopy(test_proposal) del proposal_by_email["team"][0]["accountAddress"] - self.app.post( + resp = self.app.post( "/api/v1/proposals/", data=json.dumps(proposal_by_email), + headers=self.headers, content_type='application/json' ) + self.assertEqual(resp.status_code, 201) # User user_db = User.query.filter_by(email_address=proposal_by_email["team"][0]["emailAddress"]).first() self.assertEqual(user_db.display_name, proposal_by_email["team"][0]["displayName"]) self.assertEqual(user_db.title, proposal_by_email["team"][0]["title"]) proposal_db = Proposal.query.filter_by( - proposal_address=proposal["crowdFundContractAddress"] + proposal_address=test_proposal["crowdFundContractAddress"] ).first() self.assertEqual(proposal_db.team[0].id, user_db.id) def test_associate_user_via_proposal_by_email_when_user_already_exists(self): - proposal_by_email = copy.deepcopy(proposal) - del proposal_by_email["team"][0]["accountAddress"] + proposal_by_user_email = copy.deepcopy(test_proposal) + del proposal_by_user_email["team"][0]["accountAddress"] - self.app.post( + resp = self.app.post( "/api/v1/proposals/", - data=json.dumps(proposal_by_email), + data=json.dumps(proposal_by_user_email), + headers=self.headers, content_type='application/json' ) + self.assertEqual(resp.status_code, 201) # User - user_db = User.query.filter_by(email_address=proposal_by_email["team"][0]["emailAddress"]).first() - self.assertEqual(user_db.display_name, proposal_by_email["team"][0]["displayName"]) - self.assertEqual(user_db.title, proposal_by_email["team"][0]["title"]) + self.assertEqual(self.user.display_name, proposal_by_user_email["team"][0]["displayName"]) + self.assertEqual(self.user.title, proposal_by_user_email["team"][0]["title"]) proposal_db = Proposal.query.filter_by( - proposal_address=proposal["crowdFundContractAddress"] + proposal_address=test_proposal["crowdFundContractAddress"] ).first() - self.assertEqual(proposal_db.team[0].id, user_db.id) + self.assertEqual(proposal_db.team[0].id, self.user.id) - new_proposal_by_email = copy.deepcopy(proposal) + new_proposal_by_email = copy.deepcopy(test_proposal) new_proposal_by_email["crowdFundContractAddress"] = "0x2222" del new_proposal_by_email["team"][0]["accountAddress"] @@ -92,14 +109,14 @@ class TestAPI(BaseTestConfig): self.assertEqual(user_db.display_name, new_proposal_by_email["team"][0]["displayName"]) self.assertEqual(user_db.title, new_proposal_by_email["team"][0]["title"]) proposal_db = Proposal.query.filter_by( - proposal_address=proposal["crowdFundContractAddress"] + proposal_address=test_proposal["crowdFundContractAddress"] ).first() self.assertEqual(proposal_db.team[0].id, user_db.id) def test_get_all_users(self): self.app.post( "/api/v1/proposals/", - data=json.dumps(proposal), + data=json.dumps(test_proposal), content_type='application/json' ) users_get_resp = self.app.get( @@ -107,18 +124,17 @@ class TestAPI(BaseTestConfig): ) users_json = users_get_resp.json - print(users_json) - self.assertEqual(users_json[0]["displayName"], team[0]["displayName"]) + self.assertEqual(users_json[0]["displayName"], test_team[0]["displayName"]) def test_get_user_associated_with_proposal(self): self.app.post( "/api/v1/proposals/", - data=json.dumps(proposal), + data=json.dumps(test_proposal), content_type='application/json' ) data = { - 'proposalId': proposal["crowdFundContractAddress"] + 'proposalId': test_proposal["crowdFundContractAddress"] } users_get_resp = self.app.get( @@ -127,25 +143,25 @@ class TestAPI(BaseTestConfig): ) users_json = users_get_resp.json - self.assertEqual(users_json[0]["avatar"]["imageUrl"], team[0]["avatar"]["link"]) - self.assertEqual(users_json[0]["socialMedias"][0]["socialMediaLink"], team[0]["socialMedias"][0]["link"]) - self.assertEqual(users_json[0]["displayName"], team[0]["displayName"]) + self.assertEqual(users_json[0]["avatar"]["imageUrl"], test_team[0]["avatar"]["link"]) + self.assertEqual(users_json[0]["socialMedias"][0]["socialMediaLink"], test_team[0]["socialMedias"][0]["link"]) + self.assertEqual(users_json[0]["displayName"], test_user["displayName"]) def test_get_single_user(self): self.app.post( "/api/v1/proposals/", - data=json.dumps(proposal), + data=json.dumps(test_proposal), content_type='application/json' ) users_get_resp = self.app.get( - "/api/v1/users/{}".format(proposal["team"][0]["emailAddress"]) + "/api/v1/users/{}".format(test_proposal["team"][0]["emailAddress"]) ) users_json = users_get_resp.json - self.assertEqual(users_json["avatar"]["imageUrl"], team[0]["avatar"]["link"]) - self.assertEqual(users_json["socialMedias"][0]["socialMediaLink"], team[0]["socialMedias"][0]["link"]) - self.assertEqual(users_json["displayName"], team[0]["displayName"]) + self.assertEqual(users_json["avatar"]["imageUrl"], test_team[0]["avatar"]["link"]) + self.assertEqual(users_json["socialMedias"][0]["socialMediaLink"], test_team[0]["socialMedias"][0]["link"]) + self.assertEqual(users_json["displayName"], test_team[0]["displayName"]) @patch('grant.email.send.send_email') def test_create_user(self, mock_send_email): @@ -153,15 +169,15 @@ class TestAPI(BaseTestConfig): self.app.post( "/api/v1/users/", - data=json.dumps(team[0]), + data=json.dumps(test_team[0]), content_type='application/json' ) # User - user_db = User.get_by_email_or_account_address(account_address=team[0]["accountAddress"]) - self.assertEqual(user_db.display_name, team[0]["displayName"]) - self.assertEqual(user_db.title, team[0]["title"]) - self.assertEqual(user_db.account_address, team[0]["accountAddress"]) + user_db = User.get_by_identifier(account_address=test_team[0]["accountAddress"]) + self.assertEqual(user_db.display_name, test_team[0]["displayName"]) + self.assertEqual(user_db.title, test_team[0]["title"]) + self.assertEqual(user_db.account_address, test_team[0]["accountAddress"]) @patch('grant.email.send.send_email') def test_create_user_duplicate_400(self, mock_send_email): @@ -170,64 +186,28 @@ class TestAPI(BaseTestConfig): response = self.app.post( "/api/v1/users/", - data=json.dumps(team[0]), + data=json.dumps(test_team[0]), content_type='application/json' ) self.assertEqual(response.status_code, 409) def test_update_user_remove_social_and_avatar(self): - self.app.post( - "/api/v1/proposals/", - data=json.dumps(proposal), - content_type='application/json' - ) - - updated_user = copy.deepcopy(team[0]) - updated_user['displayName'] = 'Billy' - updated_user['title'] = 'Commander' - updated_user['socialMedias'] = [] - updated_user['avatar'] = {} + updated_user = animalify(copy.deepcopy(user_schema.dump(self.user))) + updated_user["displayName"] = 'new display name' + updated_user["avatar"] = None + updated_user["socialMedias"] = None user_update_resp = self.app.put( - "/api/v1/users/{}".format(proposal["team"][0]["accountAddress"]), + "/api/v1/users/{}".format(self.user.account_address), data=json.dumps(updated_user), + headers=self.headers, content_type='application/json' ) + self.assert200(user_update_resp) - users_json = user_update_resp.json - self.assertFalse(users_json["avatar"]) - self.assertFalse(len(users_json["socialMedias"])) - self.assertEqual(users_json["displayName"], updated_user["displayName"]) - self.assertEqual(users_json["title"], updated_user["title"]) - - def test_update_user(self): - self.app.post( - "/api/v1/proposals/", - data=json.dumps(proposal), - content_type='application/json' - ) - - updated_user = copy.deepcopy(team[0]) - updated_user['displayName'] = 'Billy' - updated_user['title'] = 'Commander' - updated_user['socialMedias'] = [ - { - "link": "https://github.com/billyman" - } - ] - updated_user['avatar'] = { - "link": "https://x.io/avatar.png" - } - - user_update_resp = self.app.put( - "/api/v1/users/{}".format(proposal["team"][0]["accountAddress"]), - data=json.dumps(updated_user), - content_type='application/json' - ) - - users_json = user_update_resp.json - self.assertEqual(users_json["avatar"]["imageUrl"], updated_user["avatar"]["link"]) - self.assertEqual(users_json["socialMedias"][0]["socialMediaLink"], updated_user["socialMedias"][0]["link"]) - self.assertEqual(users_json["displayName"], updated_user["displayName"]) - self.assertEqual(users_json["title"], updated_user["title"]) + user_json = user_update_resp.json + self.assertFalse(user_json["avatar"]) + self.assertFalse(len(user_json["socialMedias"])) + self.assertEqual(user_json["displayName"], updated_user["displayName"]) + self.assertEqual(user_json["title"], updated_user["title"]) diff --git a/frontend/client/store/configure.tsx b/frontend/client/store/configure.tsx index 09466a26..13daf97e 100644 --- a/frontend/client/store/configure.tsx +++ b/frontend/client/store/configure.tsx @@ -6,6 +6,7 @@ import { composeWithDevTools } from 'redux-devtools-extension'; import { persistStore, Persistor } from 'redux-persist'; import rootReducer, { AppState, combineInitialState } from './reducers'; import rootSaga from './sagas'; +import axios from 'api/axios'; const sagaMiddleware = createSagaMiddleware(); @@ -43,5 +44,24 @@ export function configureStore(initialState: Partial = combineInitialS } } + // Any global listeners to the store go here + let prevState = store.getState(); + store.subscribe(() => { + const state = store.getState(); + + // Setup the API with auth credentials whenever they change + const { authSignature } = state.auth; + if (authSignature !== prevState.auth.authSignature) { + axios.defaults.headers.common.MsgSignature = authSignature + ? authSignature.signedMessage + : undefined; + axios.defaults.headers.common.RawTypedData = authSignature + ? JSON.stringify(authSignature.rawTypedData) + : undefined; + } + + prevState = state; + }); + return { store, persistor }; }