Authenticate endpoints (#193)

* Add auth to endpoints.
This commit is contained in:
William O'Beirne 2018-11-13 09:17:06 -05:00 committed by Daniel Ternyak
parent 22487b331b
commit a418f3d5b6
12 changed files with 302 additions and 276 deletions

2
backend/.gitignore vendored
View File

@ -68,3 +68,5 @@ dump.rdb
# jetbrains # jetbrains
.idea/ .idea/
# vscode
.vscode/

View File

@ -1,12 +1,13 @@
from datetime import datetime from datetime import datetime
from flask import Blueprint from flask import Blueprint, g
from flask_yoloapi import endpoint, parameter from flask_yoloapi import endpoint, parameter
from sqlalchemy.exc import IntegrityError from sqlalchemy.exc import IntegrityError
from grant.comment.models import Comment, comment_schema from grant.comment.models import Comment, comment_schema
from grant.milestone.models import Milestone from grant.milestone.models import Milestone
from grant.user.models import User, SocialMedia, Avatar 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 from .models import Proposal, proposals_schema, proposal_schema, ProposalUpdate, proposal_update_schema, db
blueprint = Blueprint("proposal", __name__, url_prefix="/api/v1/proposals") blueprint = Blueprint("proposal", __name__, url_prefix="/api/v1/proposals")
@ -39,28 +40,22 @@ def get_proposal_comments(proposal_id):
@blueprint.route("/<proposal_id>/comments", methods=["POST"]) @blueprint.route("/<proposal_id>/comments", methods=["POST"])
@requires_sm
@endpoint.api( @endpoint.api(
parameter('userId', type=int, required=True),
parameter('content', type=str, required=True) parameter('content', type=str, required=True)
) )
def post_proposal_comments(proposal_id, user_id, content): def post_proposal_comments(proposal_id, user_id, content):
proposal = Proposal.query.filter_by(id=proposal_id).first() proposal = Proposal.query.filter_by(id=proposal_id).first()
if proposal: if proposal:
user = User.query.filter_by(id=user_id).first() comment = Comment(
proposal_id=proposal_id,
if user: user_id=g.current_user.id,
comment = Comment( content=content
proposal_id=proposal_id, )
user_id=user_id, db.session.add(comment)
content=content db.session.commit()
) dumped_comment = comment_schema.dump(comment)
db.session.add(comment) return dumped_comment, 201
db.session.commit()
dumped_comment = comment_schema.dump(comment)
return dumped_comment, 201
else:
return {"message": "No user matching id"}, 404
else: else:
return {"message": "No proposal matching id"}, 404 return {"message": "No proposal matching id"}, 404
@ -73,8 +68,8 @@ def get_proposals(stage):
if stage: if stage:
proposals = ( proposals = (
Proposal.query.filter_by(stage=stage) Proposal.query.filter_by(stage=stage)
.order_by(Proposal.date_created.desc()) .order_by(Proposal.date_created.desc())
.all() .all()
) )
else: else:
proposals = Proposal.query.order_by(Proposal.date_created.desc()).all() proposals = Proposal.query.order_by(Proposal.date_created.desc()).all()
@ -83,6 +78,7 @@ def get_proposals(stage):
@blueprint.route("/", methods=["POST"]) @blueprint.route("/", methods=["POST"])
@requires_sm
@endpoint.api( @endpoint.api(
parameter('crowdFundContractAddress', type=str, required=True), parameter('crowdFundContractAddress', type=str, required=True),
parameter('content', type=str, required=True), parameter('content', type=str, required=True),
@ -92,7 +88,6 @@ def get_proposals(stage):
parameter('team', type=list, required=True) parameter('team', type=list, required=True)
) )
def make_proposal(crowd_fund_contract_address, content, title, milestones, category, team): 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() existing_proposal = Proposal.query.filter_by(proposal_address=crowd_fund_contract_address).first()
if existing_proposal: if existing_proposal:
return {"message": "Oops! Something went wrong."}, 409 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 return {"message": "No proposal matching id"}, 404
# TODO: Add authentication to endpoint
@blueprint.route("/<proposal_id>/updates", methods=["POST"]) @blueprint.route("/<proposal_id>/updates", methods=["POST"])
@requires_team_member_auth
@requires_sm
@endpoint.api( @endpoint.api(
parameter('title', type=str, required=True), parameter('title', type=str, required=True),
parameter('content', type=str, required=True) parameter('content', type=str, required=True)
) )
def post_proposal_update(proposal_id, title, content): def post_proposal_update(proposal_id, title, content):
proposal = Proposal.query.filter_by(id=proposal_id).first() update = ProposalUpdate(
if proposal: proposal_id=g.current_proposal.id,
update = ProposalUpdate( title=title,
proposal_id=proposal.id, content=content
title=title, )
content=content db.session.add(update)
) db.session.commit()
db.session.add(update)
db.session.commit()
dumped_update = proposal_update_schema.dump(update) dumped_update = proposal_update_schema.dump(update)
return dumped_update, 201 return dumped_update, 201
else:
return {"message": "No proposal matching id"}, 404

View File

@ -1,3 +1,4 @@
from sqlalchemy import func
from grant.comment.models import Comment from grant.comment.models import Comment
from grant.email.models import EmailVerification from grant.email.models import EmailVerification
from grant.extensions import ma, db from grant.extensions import ma, db
@ -57,7 +58,7 @@ class User(db.Model):
self.title = title self.title = title
@staticmethod @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( user = User(
account_address=account_address, account_address=account_address,
email_address=email_address, email_address=email_address,
@ -72,21 +73,22 @@ class User(db.Model):
db.session.add(ev) db.session.add(ev)
db.session.commit() db.session.commit()
send_email(user.email_address, 'signup', { if send_email:
'display_name': user.display_name, send_email(user.email_address, 'signup', {
'confirm_url': make_url(f'/email/verify?code={ev.code}') 'display_name': user.display_name,
}) 'confirm_url': make_url(f'/email/verify?code={ev.code}')
})
return user return user
@staticmethod @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: if not email_address and not account_address:
raise ValueError("Either email_address or account_address is required to get a user") raise ValueError("Either email_address or account_address is required to get a user")
return User.query.filter( return User.query.filter(
(User.account_address == account_address) | (func.lower(User.account_address) == func.lower(account_address)) |
(User.email_address == email_address) (func.lower(User.email_address) == func.lower(email_address))
).first() ).first()
class UserSchema(ma.Schema): class UserSchema(ma.Schema):

View File

@ -1,10 +1,9 @@
from flask import Blueprint, g from flask import Blueprint, g
from flask_yoloapi import endpoint, parameter 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.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') blueprint = Blueprint('user', __name__, url_prefix='/api/v1/users')
@ -35,7 +34,7 @@ def get_me():
@blueprint.route("/<user_identity>", methods=["GET"]) @blueprint.route("/<user_identity>", methods=["GET"])
@endpoint.api() @endpoint.api()
def get_user(user_identity): 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: if user:
result = user_schema.dump(user) result = user_schema.dump(user)
return result return result
@ -54,14 +53,14 @@ def get_user(user_identity):
parameter('rawTypedData', type=str, required=True) parameter('rawTypedData', type=str, required=True)
) )
def create_user( def create_user(
account_address, account_address,
email_address, email_address,
display_name, display_name,
title, title,
signed_message, signed_message,
raw_typed_data 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: if existing_user:
return {"message": "User with that address or email already exists"}, 409 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) sig_address = verify_signed_auth(signed_message, raw_typed_data)
if sig_address.lower() != account_address.lower(): if sig_address.lower() != account_address.lower():
return { return {
"message": "Message signature address ({sig_address}) doesn't match account_address ({account_address})".format( "message": "Message signature address ({sig_address}) doesn't match account_address ({account_address})".format(
sig_address=sig_address, sig_address=sig_address,
account_address=account_address account_address=account_address
) )
}, 400 }, 400
except BadSignatureException: except BadSignatureException:
return {"message": "Invalid message signature"}, 400 return {"message": "Invalid message signature"}, 400
@ -88,6 +87,7 @@ def create_user(
result = user_schema.dump(user) result = user_schema.dump(user)
return result return result
@blueprint.route("/auth", methods=["POST"]) @blueprint.route("/auth", methods=["POST"])
@endpoint.api( @endpoint.api(
parameter('accountAddress', type=str, required=True), parameter('accountAddress', type=str, required=True),
@ -95,7 +95,7 @@ def create_user(
parameter('rawTypedData', type=str, required=True) parameter('rawTypedData', type=str, required=True)
) )
def auth_user(account_address, signed_message, raw_typed_data): 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: if not existing_user:
return {"message": "No user exists with that address"}, 400 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) sig_address = verify_signed_auth(signed_message, raw_typed_data)
if sig_address.lower() != account_address.lower(): if sig_address.lower() != account_address.lower():
return { return {
"message": "Message signature address ({sig_address}) doesn't match account_address ({account_address})".format( "message": "Message signature address ({sig_address}) doesn't match account_address ({account_address})".format(
sig_address=sig_address, sig_address=sig_address,
account_address=account_address account_address=account_address
) )
}, 400 }, 400
except BadSignatureException: except BadSignatureException:
return {"message": "Invalid message signature"}, 400 return {"message": "Invalid message signature"}, 400
return user_schema.dump(existing_user) return user_schema.dump(existing_user)
@blueprint.route("/<user_identity>", methods=["PUT"]) @blueprint.route("/<user_identity>", methods=["PUT"])
@requires_sm
@requires_same_user_auth
@endpoint.api( @endpoint.api(
parameter('displayName', type=str, required=False), parameter('displayName', type=str, required=True),
parameter('title', type=str, required=False), parameter('title', type=str, required=True),
parameter('socialMedias', type=list, required=False), parameter('socialMedias', type=list, required=True),
parameter('avatar', type=dict, required=False), parameter('avatar', type=dict, required=True)
) )
def update_user(user_identity, display_name, title, social_medias, avatar): 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) user = g.current_user
if not user:
return {"message": "User with that address or email not found"}, 404
if display_name is not None: if display_name is not None:
user.display_name = display_name user.display_name = display_name
@ -132,11 +133,12 @@ def update_user(user_identity, display_name, title, social_medias, avatar):
user.title = title user.title = title
if social_medias is not None: if social_medias is not None:
sm_query = SocialMedia.query.filter_by(user_id=user.id) SocialMedia.query.filter_by(user_id=user.id).delete()
sm_query.delete()
for social_media in social_medias: for social_media in social_medias:
sm = SocialMedia(social_media_link=social_media.get("link"), user_id=user.id) sm = SocialMedia(social_media_link=social_media.get("link"), user_id=user.id)
db.session.add(sm) db.session.add(sm)
else:
SocialMedia.query.filter_by(user_id=user.id).delete()
if avatar is not None: if avatar is not None:
Avatar.query.filter_by(user_id=user.id).delete() 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: if avatar_link:
avatar_obj = Avatar(image_url=avatar_link, user_id=user.id) avatar_obj = Avatar(image_url=avatar_link, user_id=user.id)
db.session.add(avatar_obj) db.session.add(avatar_obj)
else:
Avatar.query.filter_by(user_id=user.id).delete()
db.session.commit() db.session.commit()
result = user_schema.dump(user) result = user_schema.dump(user)
return result return result

View File

@ -8,6 +8,7 @@ from itsdangerous import SignatureExpired, BadSignature
from itsdangerous import TimedJSONWebSignatureSerializer as Serializer from itsdangerous import TimedJSONWebSignatureSerializer as Serializer
from grant.settings import SECRET_KEY, AUTH_URL from grant.settings import SECRET_KEY, AUTH_URL
from ..proposal.models import Proposal
from ..user.models import User from ..user.models import User
TWO_WEEKS = 1209600 TWO_WEEKS = 1209600
@ -35,6 +36,7 @@ def verify_token(token):
class BadSignatureException(Exception): class BadSignatureException(Exception):
pass pass
def verify_signed_auth(signature, typed_data): def verify_signed_auth(signature, typed_data):
loaded_typed_data = ast.literal_eval(typed_data) loaded_typed_data = ast.literal_eval(typed_data)
url = AUTH_URL + "/message/recover" 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) response = requests.request("POST", url, data=payload, headers=headers)
json_response = response.json() json_response = response.json()
recovered_address = json_response.get('recoveredAddress') recovered_address = json_response.get('recoveredAddress')
if not recovered_address: if not recovered_address:
raise BadSignatureException("Authorization signature is invalid") raise BadSignatureException("Authorization signature is invalid")
return recovered_address return recovered_address
def requires_auth(f): # Decorator that requires you to have EIP-712 message signature headers for auth
@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
def requires_sm(f): def requires_sm(f):
@wraps(f) @wraps(f)
def decorated(*args, **kwargs): def decorated(*args, **kwargs):
@ -73,13 +59,12 @@ def requires_sm(f):
typed_data = request.headers.get('RawTypedData', None) typed_data = request.headers.get('RawTypedData', None)
if typed_data and signature: if typed_data and signature:
auth_address = None
try: try:
auth_address = verify_signed_auth(signature, typed_data) auth_address = verify_signed_auth(signature, typed_data)
except BadSignatureException: except BadSignatureException:
return jsonify(message="Invalid auth message signature"), 401 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: if not user:
return jsonify(message="No user exists with address: {}".format(auth_address)), 401 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 jsonify(message="Authentication is required to access this resource"), 401
return decorated 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 <user_identity>"), 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 <proposal_id>"), 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)

View File

@ -55,4 +55,4 @@ flask-sendgrid==0.6
sendgrid==5.3.0 sendgrid==5.3.0
# input validation # input validation
flask-yolo2API flask-yolo2API==0.2.4

View File

@ -1,6 +1,8 @@
from flask_testing import TestCase 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): class BaseTestConfig(TestCase):
@ -11,9 +13,36 @@ class BaseTestConfig(TestCase):
return app return app
def setUp(self): def setUp(self):
db.drop_all()
self.app = self.create_app().test_client() self.app = self.create_app().test_client()
db.create_all() db.create_all()
def tearDown(self): def tearDown(self):
db.session.remove() db.session.remove()
db.drop_all() 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()

View File

@ -1,81 +1,43 @@
import json import json
import random
from grant.proposal.models import Proposal, CATEGORIES from grant.proposal.models import Proposal
from grant.user.models import User, SocialMedia from grant.user.models import SocialMedia, Avatar
from ..config import BaseTestConfig from ..config import BaseUserConfig
from ..test_data import test_proposal
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)
}
class TestAPI(BaseTestConfig): class TestAPI(BaseUserConfig):
def test_create_new_proposal(self): def test_create_new_proposal(self):
self.assertIsNone(Proposal.query.filter_by( self.assertIsNone(Proposal.query.filter_by(
proposal_address=proposal["crowdFundContractAddress"] proposal_address=test_proposal["crowdFundContractAddress"]
).first()) ).first())
resp = self.app.post( resp = self.app.post(
"/api/v1/proposals/", "/api/v1/proposals/",
data=json.dumps(proposal), data=json.dumps(test_proposal),
headers=self.headers,
content_type='application/json' content_type='application/json'
) )
self.assertEqual(resp.status_code, 201)
proposal_db = Proposal.query.filter_by( proposal_db = Proposal.query.filter_by(
proposal_address=proposal["crowdFundContractAddress"] proposal_address=test_proposal["crowdFundContractAddress"]
).first() ).first()
self.assertEqual(proposal_db.title, proposal["title"]) self.assertEqual(proposal_db.title, test_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"])
# SocialMedia # 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) self.assertTrue(social_media_db)
# Avatar # 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): def test_create_new_proposal_comment(self):
proposal_res = self.app.post( proposal_res = self.app.post(
"/api/v1/proposals/", "/api/v1/proposals/",
data=json.dumps(proposal), data=json.dumps(test_proposal),
headers=self.headers,
content_type='application/json' content_type='application/json'
) )
proposal_json = proposal_res.json proposal_json = proposal_res.json
@ -94,15 +56,17 @@ class TestAPI(BaseTestConfig):
self.assertTrue(comment_res.json) self.assertTrue(comment_res.json)
def test_create_new_proposal_duplicate(self): def test_create_new_proposal_duplicate(self):
proposal_res = self.app.post( self.app.post(
"/api/v1/proposals/", "/api/v1/proposals/",
data=json.dumps(proposal), data=json.dumps(test_proposal),
headers=self.headers,
content_type='application/json' content_type='application/json'
) )
proposal_res2 = self.app.post( proposal_res2 = self.app.post(
"/api/v1/proposals/", "/api/v1/proposals/",
data=json.dumps(proposal), data=json.dumps(test_proposal),
headers=self.headers,
content_type='application/json' content_type='application/json'
) )

View File

@ -1,5 +1,6 @@
import json import json
import random import random
from grant.proposal.models import CATEGORIES from grant.proposal.models import CATEGORIES
message = { message = {
@ -43,7 +44,7 @@ message = {
} }
} }
user = { test_user = {
"accountAddress": '0xcd2a3d9f938e13cd947ec05abc7fe734df8dd826', "accountAddress": '0xcd2a3d9f938e13cd947ec05abc7fe734df8dd826',
"displayName": 'Groot', "displayName": 'Groot',
"emailAddress": 'iam@groot.com', "emailAddress": 'iam@groot.com',
@ -60,7 +61,7 @@ user = {
"rawTypedData": json.dumps(message["data"]) "rawTypedData": json.dumps(message["data"])
} }
team = [user] test_team = [test_user]
milestones = [ milestones = [
{ {
@ -72,11 +73,21 @@ milestones = [
} }
] ]
proposal = { test_proposal = {
"team": team, "team": test_team,
"crowdFundContractAddress": "0x20000", "crowdFundContractAddress": "0x20000",
"content": "## My Proposal", "content": "## My Proposal",
"title": "Give Me Money", "title": "Give Me Money",
"milestones": milestones, "milestones": milestones,
"category": random.choice(CATEGORIES) "category": random.choice(CATEGORIES)
} }
milestones = [
{
"title": "All the money straightaway",
"description": "cool stuff with it",
"date": "June 2019",
"payoutPercent": "100",
"immediatePayout": False
}
]

View File

@ -1,14 +1,14 @@
import json import json
from ..config import BaseTestConfig from ..config import BaseTestConfig
from ..test_data import user, message from ..test_data import test_user, message
class TestRequiredSignedMessageDecorator(BaseTestConfig): class TestRequiredSignedMessageDecorator(BaseTestConfig):
def test_required_sm_aborts_without_data_and_sig_headers(self): def test_required_sm_aborts_without_data_and_sig_headers(self):
self.app.post( self.app.post(
"/api/v1/users/", "/api/v1/users/",
data=json.dumps(user), data=json.dumps(test_user),
content_type='application/json' content_type='application/json'
) )
@ -53,7 +53,7 @@ class TestRequiredSignedMessageDecorator(BaseTestConfig):
def test_required_sm_decorator_authorizes_when_recovered_address_matches_existing_user(self): def test_required_sm_decorator_authorizes_when_recovered_address_matches_existing_user(self):
self.app.post( self.app.post(
"/api/v1/users/", "/api/v1/users/",
data=json.dumps(user), data=json.dumps(test_user),
content_type='application/json' content_type='application/json'
) )
@ -68,4 +68,4 @@ class TestRequiredSignedMessageDecorator(BaseTestConfig):
response_json = response.json response_json = response.json
self.assert200(response) self.assert200(response)
self.assertEqual(response_json["displayName"], user["displayName"]) self.assertEqual(response_json["displayName"], test_user["displayName"])

View File

@ -1,84 +1,101 @@
import copy import copy
import json import json
from grant.proposal.models import Proposal from animal_case import animalify
from grant.user.models import User
from ..config import BaseTestConfig
from ..test_data import team, proposal
from mock import patch 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( class TestAPI(BaseUserConfig):
"/api/v1/proposals/", # TODO create second signed message default user
data=json.dumps(proposal_by_account), # @patch('grant.email.send.send_email')
content_type='application/json' # 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 # TODO create second signed message default user
user_db = User.query.filter_by(account_address=proposal_by_account["team"][0]["accountAddress"]).first() # def test_create_new_user_via_proposal_by_email(self):
self.assertEqual(user_db.display_name, proposal_by_account["team"][0]["displayName"]) # self.remove_default_user()
self.assertEqual(user_db.title, proposal_by_account["team"][0]["title"]) # proposal_by_email = copy.deepcopy(test_proposal)
self.assertEqual(user_db.account_address, proposal_by_account["team"][0]["accountAddress"]) # del proposal_by_email["team"][0]["accountAddress"]
#
def test_create_new_user_via_proposal_by_email(self): # resp = self.app.post(
proposal_by_email = copy.deepcopy(proposal) # "/api/v1/proposals/",
del proposal_by_email["team"][0]["accountAddress"] # data=json.dumps(proposal_by_email),
# headers=self.headers,
self.app.post( # content_type='application/json'
"/api/v1/proposals/", # )
data=json.dumps(proposal_by_email), #
content_type='application/json' # self.assertEqual(resp, 201)
) #
# # User
# User # user_db = User.query.filter_by(email_address=proposal_by_email["team"][0]["emailAddress"]).first()
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.display_name, proposal_by_email["team"][0]["displayName"]) # self.assertEqual(user_db.title, proposal_by_email["team"][0]["title"])
self.assertEqual(user_db.title, proposal_by_email["team"][0]["title"])
def test_associate_user_via_proposal_by_email(self): 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"] del proposal_by_email["team"][0]["accountAddress"]
self.app.post( resp = self.app.post(
"/api/v1/proposals/", "/api/v1/proposals/",
data=json.dumps(proposal_by_email), data=json.dumps(proposal_by_email),
headers=self.headers,
content_type='application/json' content_type='application/json'
) )
self.assertEqual(resp.status_code, 201)
# User # User
user_db = User.query.filter_by(email_address=proposal_by_email["team"][0]["emailAddress"]).first() 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.display_name, proposal_by_email["team"][0]["displayName"])
self.assertEqual(user_db.title, proposal_by_email["team"][0]["title"]) self.assertEqual(user_db.title, proposal_by_email["team"][0]["title"])
proposal_db = Proposal.query.filter_by( proposal_db = Proposal.query.filter_by(
proposal_address=proposal["crowdFundContractAddress"] proposal_address=test_proposal["crowdFundContractAddress"]
).first() ).first()
self.assertEqual(proposal_db.team[0].id, user_db.id) self.assertEqual(proposal_db.team[0].id, user_db.id)
def test_associate_user_via_proposal_by_email_when_user_already_exists(self): def test_associate_user_via_proposal_by_email_when_user_already_exists(self):
proposal_by_email = copy.deepcopy(proposal) proposal_by_user_email = copy.deepcopy(test_proposal)
del proposal_by_email["team"][0]["accountAddress"] del proposal_by_user_email["team"][0]["accountAddress"]
self.app.post( resp = self.app.post(
"/api/v1/proposals/", "/api/v1/proposals/",
data=json.dumps(proposal_by_email), data=json.dumps(proposal_by_user_email),
headers=self.headers,
content_type='application/json' content_type='application/json'
) )
self.assertEqual(resp.status_code, 201)
# User # User
user_db = User.query.filter_by(email_address=proposal_by_email["team"][0]["emailAddress"]).first() self.assertEqual(self.user.display_name, proposal_by_user_email["team"][0]["displayName"])
self.assertEqual(user_db.display_name, proposal_by_email["team"][0]["displayName"]) self.assertEqual(self.user.title, proposal_by_user_email["team"][0]["title"])
self.assertEqual(user_db.title, proposal_by_email["team"][0]["title"])
proposal_db = Proposal.query.filter_by( proposal_db = Proposal.query.filter_by(
proposal_address=proposal["crowdFundContractAddress"] proposal_address=test_proposal["crowdFundContractAddress"]
).first() ).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" new_proposal_by_email["crowdFundContractAddress"] = "0x2222"
del new_proposal_by_email["team"][0]["accountAddress"] 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.display_name, new_proposal_by_email["team"][0]["displayName"])
self.assertEqual(user_db.title, new_proposal_by_email["team"][0]["title"]) self.assertEqual(user_db.title, new_proposal_by_email["team"][0]["title"])
proposal_db = Proposal.query.filter_by( proposal_db = Proposal.query.filter_by(
proposal_address=proposal["crowdFundContractAddress"] proposal_address=test_proposal["crowdFundContractAddress"]
).first() ).first()
self.assertEqual(proposal_db.team[0].id, user_db.id) self.assertEqual(proposal_db.team[0].id, user_db.id)
def test_get_all_users(self): def test_get_all_users(self):
self.app.post( self.app.post(
"/api/v1/proposals/", "/api/v1/proposals/",
data=json.dumps(proposal), data=json.dumps(test_proposal),
content_type='application/json' content_type='application/json'
) )
users_get_resp = self.app.get( users_get_resp = self.app.get(
@ -107,18 +124,17 @@ class TestAPI(BaseTestConfig):
) )
users_json = users_get_resp.json users_json = users_get_resp.json
print(users_json) self.assertEqual(users_json[0]["displayName"], test_team[0]["displayName"])
self.assertEqual(users_json[0]["displayName"], team[0]["displayName"])
def test_get_user_associated_with_proposal(self): def test_get_user_associated_with_proposal(self):
self.app.post( self.app.post(
"/api/v1/proposals/", "/api/v1/proposals/",
data=json.dumps(proposal), data=json.dumps(test_proposal),
content_type='application/json' content_type='application/json'
) )
data = { data = {
'proposalId': proposal["crowdFundContractAddress"] 'proposalId': test_proposal["crowdFundContractAddress"]
} }
users_get_resp = self.app.get( users_get_resp = self.app.get(
@ -127,25 +143,25 @@ class TestAPI(BaseTestConfig):
) )
users_json = users_get_resp.json users_json = users_get_resp.json
self.assertEqual(users_json[0]["avatar"]["imageUrl"], team[0]["avatar"]["link"]) self.assertEqual(users_json[0]["avatar"]["imageUrl"], test_team[0]["avatar"]["link"])
self.assertEqual(users_json[0]["socialMedias"][0]["socialMediaLink"], team[0]["socialMedias"][0]["link"]) self.assertEqual(users_json[0]["socialMedias"][0]["socialMediaLink"], test_team[0]["socialMedias"][0]["link"])
self.assertEqual(users_json[0]["displayName"], team[0]["displayName"]) self.assertEqual(users_json[0]["displayName"], test_user["displayName"])
def test_get_single_user(self): def test_get_single_user(self):
self.app.post( self.app.post(
"/api/v1/proposals/", "/api/v1/proposals/",
data=json.dumps(proposal), data=json.dumps(test_proposal),
content_type='application/json' content_type='application/json'
) )
users_get_resp = self.app.get( 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 users_json = users_get_resp.json
self.assertEqual(users_json["avatar"]["imageUrl"], team[0]["avatar"]["link"]) self.assertEqual(users_json["avatar"]["imageUrl"], test_team[0]["avatar"]["link"])
self.assertEqual(users_json["socialMedias"][0]["socialMediaLink"], team[0]["socialMedias"][0]["link"]) self.assertEqual(users_json["socialMedias"][0]["socialMediaLink"], test_team[0]["socialMedias"][0]["link"])
self.assertEqual(users_json["displayName"], team[0]["displayName"]) self.assertEqual(users_json["displayName"], test_team[0]["displayName"])
@patch('grant.email.send.send_email') @patch('grant.email.send.send_email')
def test_create_user(self, mock_send_email): def test_create_user(self, mock_send_email):
@ -153,15 +169,15 @@ class TestAPI(BaseTestConfig):
self.app.post( self.app.post(
"/api/v1/users/", "/api/v1/users/",
data=json.dumps(team[0]), data=json.dumps(test_team[0]),
content_type='application/json' content_type='application/json'
) )
# User # User
user_db = User.get_by_email_or_account_address(account_address=team[0]["accountAddress"]) user_db = User.get_by_identifier(account_address=test_team[0]["accountAddress"])
self.assertEqual(user_db.display_name, team[0]["displayName"]) self.assertEqual(user_db.display_name, test_team[0]["displayName"])
self.assertEqual(user_db.title, team[0]["title"]) self.assertEqual(user_db.title, test_team[0]["title"])
self.assertEqual(user_db.account_address, team[0]["accountAddress"]) self.assertEqual(user_db.account_address, test_team[0]["accountAddress"])
@patch('grant.email.send.send_email') @patch('grant.email.send.send_email')
def test_create_user_duplicate_400(self, mock_send_email): def test_create_user_duplicate_400(self, mock_send_email):
@ -170,64 +186,28 @@ class TestAPI(BaseTestConfig):
response = self.app.post( response = self.app.post(
"/api/v1/users/", "/api/v1/users/",
data=json.dumps(team[0]), data=json.dumps(test_team[0]),
content_type='application/json' content_type='application/json'
) )
self.assertEqual(response.status_code, 409) self.assertEqual(response.status_code, 409)
def test_update_user_remove_social_and_avatar(self): def test_update_user_remove_social_and_avatar(self):
self.app.post( updated_user = animalify(copy.deepcopy(user_schema.dump(self.user)))
"/api/v1/proposals/", updated_user["displayName"] = 'new display name'
data=json.dumps(proposal), updated_user["avatar"] = None
content_type='application/json' updated_user["socialMedias"] = None
)
updated_user = copy.deepcopy(team[0])
updated_user['displayName'] = 'Billy'
updated_user['title'] = 'Commander'
updated_user['socialMedias'] = []
updated_user['avatar'] = {}
user_update_resp = self.app.put( 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), data=json.dumps(updated_user),
headers=self.headers,
content_type='application/json' content_type='application/json'
) )
self.assert200(user_update_resp)
users_json = user_update_resp.json user_json = user_update_resp.json
self.assertFalse(users_json["avatar"]) self.assertFalse(user_json["avatar"])
self.assertFalse(len(users_json["socialMedias"])) self.assertFalse(len(user_json["socialMedias"]))
self.assertEqual(users_json["displayName"], updated_user["displayName"]) self.assertEqual(user_json["displayName"], updated_user["displayName"])
self.assertEqual(users_json["title"], updated_user["title"]) self.assertEqual(user_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"])

View File

@ -6,6 +6,7 @@ import { composeWithDevTools } from 'redux-devtools-extension';
import { persistStore, Persistor } from 'redux-persist'; import { persistStore, Persistor } from 'redux-persist';
import rootReducer, { AppState, combineInitialState } from './reducers'; import rootReducer, { AppState, combineInitialState } from './reducers';
import rootSaga from './sagas'; import rootSaga from './sagas';
import axios from 'api/axios';
const sagaMiddleware = createSagaMiddleware(); const sagaMiddleware = createSagaMiddleware();
@ -43,5 +44,24 @@ export function configureStore(initialState: Partial<AppState> = 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 }; return { store, persistor };
} }