zcash-grant-system/backend/grant/user/views.py

400 lines
14 KiB
Python
Raw Normal View History

2019-03-14 13:29:02 -07:00
import validators
from animal_case import keys_to_snake_case
2019-03-14 13:29:02 -07:00
from flask import Blueprint, g, current_app
2019-03-01 12:11:03 -08:00
from marshmallow import fields
2019-03-14 13:29:02 -07:00
from validate_email import validate_email
2019-03-18 12:03:01 -07:00
from webargs import validate
2019-03-01 12:11:03 -08:00
import grant.utils.auth as auth
from grant.comment.models import Comment, user_comments_schema
2019-01-22 21:35:22 -08:00
from grant.email.models import EmailRecovery
2019-03-14 13:29:02 -07:00
from grant.extensions import limiter
2019-03-01 12:11:03 -08:00
from grant.parser import query, body
from grant.proposal.models import (
Proposal,
ProposalTeamInvite,
invites_with_proposal_schema,
ProposalContribution,
user_proposal_contributions_schema,
user_proposals_schema,
user_proposal_arbiters_schema
)
2019-03-01 12:11:03 -08:00
from grant.utils.enums import ProposalStatus, ContributionStatus
2019-01-22 21:35:22 -08:00
from grant.utils.exceptions import ValidationException
2019-03-14 13:29:02 -07:00
from grant.utils.requests import validate_blockchain_get
from grant.utils.social import verify_social, get_social_login_url, VerifySocialException
2019-01-22 21:35:22 -08:00
from grant.utils.upload import remove_avatar, sign_avatar_upload, AvatarException
from .models import (
User,
SocialMedia,
Avatar,
self_user_schema,
user_schema,
user_settings_schema,
db
)
blueprint = Blueprint('user', __name__, url_prefix='/api/v1/users')
@blueprint.route("/me", methods=["GET"])
2019-02-21 14:23:46 -08:00
@auth.requires_auth
def get_me():
dumped_user = self_user_schema.dump(g.current_user)
return dumped_user
2018-12-14 11:36:22 -08:00
@blueprint.route("/<user_id>", methods=["GET"])
2019-03-01 12:11:03 -08:00
@query({
"withProposals": fields.Bool(required=False, missing=None),
"withComments": fields.Bool(required=False, missing=None),
"withFunded": fields.Bool(required=False, missing=None),
"withPending": fields.Bool(required=False, missing=None),
"withArbitrated": fields.Bool(required=False, missing=None)
})
2019-02-06 14:37:45 -08:00
def get_user(user_id, with_proposals, with_comments, with_funded, with_pending, with_arbitrated):
2018-12-14 11:36:22 -08:00
user = User.get_by_id(user_id)
if user:
result = user_schema.dump(user)
2019-02-21 14:23:46 -08:00
authed_user = auth.get_authed_user()
2019-02-06 14:37:45 -08:00
is_self = authed_user and authed_user.id == user.id
if with_proposals:
proposals = Proposal.get_by_user(user)
proposals_dump = user_proposals_schema.dump(proposals)
result["proposals"] = proposals_dump
if with_funded:
contributions = ProposalContribution.get_by_userid(user_id)
if not authed_user or user.id != authed_user.id:
contributions = [c for c in contributions if c.status == ContributionStatus.CONFIRMED]
contributions = [c for c in contributions if not c.private]
contributions = [c for c in contributions if c.proposal.status == ProposalStatus.LIVE]
contributions_dump = user_proposal_contributions_schema.dump(contributions)
result["contributions"] = contributions_dump
if with_comments:
comments = Comment.get_by_user(user)
comments_dump = user_comments_schema.dump(comments)
result["comments"] = comments_dump
2019-02-06 14:37:45 -08:00
if with_pending and is_self:
pending = Proposal.get_by_user(user, [
ProposalStatus.STAKING,
ProposalStatus.PENDING,
ProposalStatus.APPROVED,
ProposalStatus.REJECTED,
])
pending_dump = user_proposals_schema.dump(pending)
result["pendingProposals"] = pending_dump
2019-02-06 14:37:45 -08:00
if with_arbitrated and is_self:
result["arbitrated"] = user_proposal_arbiters_schema.dump(user.arbiter_proposals)
return result
else:
2018-12-14 11:36:22 -08:00
message = "User with id matching {} not found".format(user_id)
return {"message": message}, 404
@blueprint.route("/", methods=["POST"])
@limiter.limit("30/day;5/minute")
2019-03-01 12:11:03 -08:00
@body({
2019-03-14 13:29:02 -07:00
"emailAddress": fields.Str(required=True, validate=lambda e: validate_email(e)),
2019-03-01 12:11:03 -08:00
"password": fields.Str(required=True),
2019-03-18 12:03:01 -07:00
"displayName": fields.Str(required=True, validate=validate.Length(min=2, max=50)),
"title": fields.Str(required=True, validate=validate.Length(min=2, max=50)),
2019-03-01 12:11:03 -08:00
})
def create_user(
email_address,
2018-12-14 11:36:22 -08:00
password,
display_name,
2018-12-14 11:36:22 -08:00
title
):
2018-12-14 11:36:22 -08:00
existing_user = User.get_by_email(email_address)
if existing_user:
2018-12-14 11:36:22 -08:00
return {"message": "User with that email already exists"}, 409
user = User.create(
email_address=email_address,
2018-12-14 11:36:22 -08:00
password=password,
display_name=display_name,
title=title
)
2018-12-14 11:36:22 -08:00
user.login()
result = self_user_schema.dump(user)
2018-11-27 11:07:09 -08:00
return result, 201
@blueprint.route("/auth", methods=["POST"])
2019-06-25 08:02:44 -07:00
@limiter.limit("30/hour;5/minute")
2019-03-01 12:11:03 -08:00
@body({
"email": fields.Str(required=True),
"password": fields.Str(required=True)
})
2018-12-14 11:36:22 -08:00
def auth_user(email, password):
2019-02-21 14:23:46 -08:00
authed_user = auth.auth_user(email, password)
return self_user_schema.dump(authed_user)
@blueprint.route("/me/password", methods=["PUT"])
2019-02-21 14:23:46 -08:00
@auth.requires_auth
2019-03-01 12:11:03 -08:00
@body({
"currentPassword": fields.Str(required=True),
"password": fields.Str(required=True)
})
2018-12-14 11:36:22 -08:00
def update_user_password(current_password, password):
if not g.current_user.check_password(current_password):
return {"message": "Current password incorrect"}, 403
g.current_user.set_password(password)
2019-03-01 12:11:03 -08:00
return {"message": "ok"}, 200
@blueprint.route("/me/email", methods=["PUT"])
2019-02-21 14:23:46 -08:00
@auth.requires_auth
2019-03-01 12:11:03 -08:00
@body({
2019-03-14 13:29:02 -07:00
"email": fields.Str(required=True, validate=lambda e: validate_email(e)),
2019-03-01 12:11:03 -08:00
"password": fields.Str(required=True)
})
def update_user_email(email, password):
if not g.current_user.check_password(password):
return {"message": "Password is incorrect"}, 403
2019-03-14 13:29:02 -07:00
current_app.logger.info(
f"Updating userId: {g.current_user.id} with current email: {g.current_user.email_address} to new email: {email}"
)
g.current_user.set_email(email)
2019-03-01 12:11:03 -08:00
return {"message": "ok"}, 200
2019-02-08 11:57:54 -08:00
@blueprint.route("/me/resend-verification", methods=["PUT"])
2019-02-21 14:23:46 -08:00
@auth.requires_auth
2019-02-08 11:57:54 -08:00
def resend_email_verification():
g.current_user.send_verification_email()
2019-03-01 12:11:03 -08:00
return {"message": "ok"}, 200
2019-02-08 11:57:54 -08:00
2018-12-14 11:36:22 -08:00
@blueprint.route("/logout", methods=["POST"])
2019-02-21 14:23:46 -08:00
@auth.requires_auth
2018-12-14 11:36:22 -08:00
def logout_user():
2019-02-21 14:23:46 -08:00
auth.logout_current_user()
2019-03-01 12:11:03 -08:00
return {"message": "ok"}, 200
2018-12-14 11:36:22 -08:00
@blueprint.route("/social/<service>/authurl", methods=["GET"])
2019-02-21 14:23:46 -08:00
@auth.requires_auth
def get_user_social_auth_url(service):
try:
return {"url": get_social_login_url(service)}
except VerifySocialException as e:
return {"message": str(e)}, 400
@blueprint.route("/social/<service>/verify", methods=["POST"])
2019-02-21 14:23:46 -08:00
@auth.requires_auth
2019-03-01 12:11:03 -08:00
@body({
"code": fields.Str(required=True)
})
def verify_user_social(service, code):
try:
# 1. verify with 3rd party
username = verify_social(service, code)
# 2. remove existing username/service
sm_other_db = SocialMedia.query.filter_by(service=service, username=username).first()
if sm_other_db:
db.session.delete(sm_other_db)
# 3. remove existing for authed user/service
sm_self_db = SocialMedia.query.filter_by(service=service, user_id=g.current_user.id).first()
if sm_self_db:
db.session.delete(sm_self_db)
# 4. set this users verified social item
sm = SocialMedia(service=service, username=username, user_id=g.current_user.id)
db.session.add(sm)
db.session.commit()
return {"username": username}, 200
except VerifySocialException as e:
return {"message": str(e)}, 400
2018-12-17 10:33:33 -08:00
@blueprint.route("/recover", methods=["POST"])
@limiter.limit("10/day;2/minute")
2019-03-01 12:11:03 -08:00
@body({
"email": fields.Str(required=True)
})
2018-12-17 10:33:33 -08:00
def recover_user(email):
existing_user = User.get_by_email(email)
if not existing_user:
return {"message": "No user exists with that email"}, 400
2019-02-21 14:23:46 -08:00
auth.throw_on_banned(existing_user)
2018-12-17 10:33:33 -08:00
existing_user.send_recovery_email()
2019-03-01 12:11:03 -08:00
return {"message": "ok"}, 200
2018-12-17 10:33:33 -08:00
@blueprint.route("/recover/<code>", methods=["POST"])
2019-03-01 12:11:03 -08:00
@body({
"password": fields.Str(required=True)
})
2018-12-17 10:33:33 -08:00
def recover_email(code, password):
er = EmailRecovery.query.filter_by(code=code).first()
if er:
if er.is_expired():
return {"message": "Reset code expired"}, 401
2019-02-21 14:23:46 -08:00
auth.throw_on_banned(er.user)
2018-12-17 10:33:33 -08:00
er.user.set_password(password)
db.session.delete(er)
db.session.commit()
2019-03-01 12:11:03 -08:00
return {"message": "ok"}, 200
2018-12-17 10:33:33 -08:00
return {"message": "Invalid reset code"}, 400
2018-12-14 11:36:22 -08:00
@blueprint.route("/avatar", methods=["POST"])
@limiter.limit("20/day;3/minute")
2019-02-21 14:23:46 -08:00
@auth.requires_auth
2019-03-01 12:11:03 -08:00
@body({
"mimetype": fields.Str(required=True)
})
2018-12-14 11:36:22 -08:00
def upload_avatar(mimetype):
2018-11-16 19:33:25 -08:00
user = g.current_user
try:
2018-12-14 11:36:22 -08:00
signed_post = sign_avatar_upload(mimetype, user.id)
return signed_post
except AvatarException as e:
2018-11-16 19:33:25 -08:00
return {"message": str(e)}, 400
@blueprint.route("/avatar", methods=["DELETE"])
2019-02-21 14:23:46 -08:00
@auth.requires_auth
2019-03-01 12:11:03 -08:00
@body({
"url": fields.Str(required=True)
})
2018-11-16 19:33:25 -08:00
def delete_avatar(url):
user = g.current_user
remove_avatar(url, user.id)
2018-12-14 11:36:22 -08:00
@blueprint.route("/<user_id>", methods=["PUT"])
2019-02-21 14:23:46 -08:00
@auth.requires_auth
@auth.requires_same_user_auth
2019-03-01 12:11:03 -08:00
@body({
2019-03-14 13:29:02 -07:00
"displayName": fields.Str(required=True, validate=lambda d: 2 <= len(d) <= 60),
"title": fields.Str(required=True, validate=lambda t: 2 <= len(t) <= 60),
2019-03-01 12:11:03 -08:00
"socialMedias": fields.List(fields.Dict(), required=True),
"avatar": fields.Str(required=True, allow_none=True, validate=lambda d: validators.url(d))
2019-03-01 12:11:03 -08:00
})
2018-12-14 11:36:22 -08:00
def update_user(user_id, display_name, title, social_medias, avatar):
user = g.current_user
if display_name is not None:
user.display_name = display_name
if title is not None:
user.title = title
# only allow deletions here, check for absent items
db_socials = SocialMedia.query.filter_by(user_id=user.id).all()
new_socials = list(map(lambda s: s['service'], social_medias))
for social in db_socials:
if social.service not in new_socials:
db.session.delete(social)
db_avatar = Avatar.query.filter_by(user_id=user.id).first()
if db_avatar:
db.session.delete(db_avatar)
if avatar:
new_avatar = Avatar(image_url=avatar, user_id=user.id)
db.session.add(new_avatar)
2018-12-14 11:36:22 -08:00
old_avatar_url = db_avatar and db_avatar.image_url
if old_avatar_url and old_avatar_url != avatar:
remove_avatar(old_avatar_url, user.id)
2018-11-16 19:33:25 -08:00
db.session.commit()
result = self_user_schema.dump(user)
return result
2018-12-14 11:36:22 -08:00
@blueprint.route("/<user_id>/invites", methods=["GET"])
2019-02-21 14:23:46 -08:00
@auth.requires_same_user_auth
2018-12-14 11:36:22 -08:00
def get_user_invites(user_id):
invites = ProposalTeamInvite.get_pending_for_user(g.current_user)
return invites_with_proposal_schema.dump(invites)
2018-12-14 11:36:22 -08:00
@blueprint.route("/<user_id>/invites/<invite_id>/respond", methods=["PUT"])
2019-02-21 14:23:46 -08:00
@auth.requires_same_user_auth
2019-03-01 12:11:03 -08:00
@body({
"response": fields.Bool(required=True)
})
2018-12-14 11:36:22 -08:00
def respond_to_invite(user_id, invite_id, response):
invite = ProposalTeamInvite.query.filter_by(id=invite_id).first()
if not invite:
return {"message": "No invite found with id {}".format(invite_id)}, 404
invite.accepted = response
db.session.add(invite)
if invite.accepted:
invite.proposal.team.append(g.current_user)
db.session.add(invite)
db.session.commit()
2019-03-01 12:11:03 -08:00
return {"message": "ok"}, 200
@blueprint.route("/<user_id>/settings", methods=["GET"])
2019-02-21 14:23:46 -08:00
@auth.requires_same_user_auth
def get_user_settings(user_id):
return user_settings_schema.dump(g.current_user.settings)
@blueprint.route("/<user_id>/settings", methods=["PUT"])
2019-02-21 14:23:46 -08:00
@auth.requires_same_user_auth
2019-03-01 12:11:03 -08:00
@body({
"emailSubscriptions": fields.Dict(required=False, missing=None),
2019-03-14 13:29:02 -07:00
"refundAddress": fields.Str(required=False, missing=None,
validate=lambda r: validate_blockchain_get('/validate/address', {'address': r})),
"tipJarAddress": fields.Str(required=False, missing=None,
validate=lambda r: validate_blockchain_get('/validate/address', {'address': r})),
"tipJarViewKey": fields.Str(required=False, missing=None) # TODO: add viewkey validation here
2019-03-01 12:11:03 -08:00
})
def set_user_settings(user_id, email_subscriptions, refund_address, tip_jar_address, tip_jar_view_key):
if email_subscriptions:
try:
email_subscriptions = keys_to_snake_case(email_subscriptions)
g.current_user.settings.email_subscriptions = email_subscriptions
except ValidationException as e:
return {"message": str(e)}, 400
if refund_address == '' and g.current_user.settings.refund_address:
return {"message": "Refund address cannot be unset, only changed"}, 400
if refund_address:
g.current_user.settings.refund_address = refund_address
# TODO: is additional validation needed similar to refund_address?
if tip_jar_address is not None:
g.current_user.settings.tip_jar_address = tip_jar_address
if tip_jar_view_key is not None:
g.current_user.settings.tip_jar_view_key = tip_jar_view_key
db.session.commit()
return user_settings_schema.dump(g.current_user.settings)
@blueprint.route("/<user_id>/arbiter/<proposal_id>", methods=["PUT"])
2019-02-21 14:23:46 -08:00
@auth.requires_same_user_auth
2019-03-01 12:11:03 -08:00
@body({
"isAccept": fields.Bool(required=False, missing=None)
})
def set_user_arbiter(user_id, proposal_id, is_accept):
try:
proposal = Proposal.query.filter_by(id=int(proposal_id)).first()
if not proposal:
return {"message": "No such proposal"}, 404
if is_accept:
proposal.arbiter.accept_nomination(g.current_user.id)
return {"message": "Accepted nomination"}, 200
else:
proposal.arbiter.reject_nomination(g.current_user.id)
return {"message": "Rejected nomination"}, 200
except ValidationException as e:
return {"message": str(e)}, 400