Skip to content

Commit

Permalink
[CE-116] Enable user profile get/update
Browse files Browse the repository at this point in the history
Add user profile get/update support
Add test case for user profile

Change-Id: I5fbcc4814554a8da9827fd6b4ceb33a381249f98
Signed-off-by: Haitao Yue <hightall@me.com>
  • Loading branch information
hightall committed Aug 30, 2017
1 parent aba14bc commit f380987
Show file tree
Hide file tree
Showing 17 changed files with 500 additions and 51 deletions.
2 changes: 1 addition & 1 deletion docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,7 @@ services:
environment:
- SV_BaseURL=http://dashboard:8080/api/
- RESTful_Server=dashboard:8080
- RESTful_BaseURL=/v2/
- RESTful_BaseURL=/api/v2/
- DEBUG=node:*
volumes:
- ./user-dashboard:/usr/app/src
Expand Down
1 change: 1 addition & 0 deletions src/common/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,3 +24,4 @@

from .fabric_network_config import \
FabricPreNetworkConfig, FabricV1NetworkConfig
from .stringvalidator import StringValidator
104 changes: 104 additions & 0 deletions src/common/stringvalidator.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@

# Copyright IBM Corp, All Rights Reserved.
#
# SPDX-License-Identifier: Apache-2.0
#
import re


class StringValidator(object):
# email borrowed from chenglee's validator
REGEX_EMAIL = re.compile(
'^[-!#$%&\'*+\\.\/0-9=?A-Z^_`{|}~]+@([-0-9A-Z]+\.)+([0-9A-Z]){2,4}$',
re.IGNORECASE)
REGEX_ALPHA = re.compile('^[a-z]+$', re.IGNORECASE)
REGEX_TLD = re.compile('([a-z\-0-9]+\.)?([a-z\-0-9]+)\.([a-z]+)$',
re.IGNORECASE)
REGEX_HANDLE = re.compile('[a-z0-9\_]+$', re.IGNORECASE)

def validate(self, input, checks=[], log=False):

results = {}
fail = False

# pass the input to the given checks one by one
for check in checks:
try:
if isinstance(check, tuple):
check_name = check[0]
args = check[slice(1, len(check))]
else:
check_name = check
args = None

method = getattr(self, '_check_' + check_name)
results[check] = method(input.strip(),
args) if args else method(input)

if not results[check]:
if log:
fail = True
else:
return False

except Exception as e:
raise

return True if not fail else results

def _check_not_empty(self, input):
"""Check if a given string is empty"""
return False if not input else True

def _check_is_numeric(self, input):
"""Check if a given string is numeric"""
try:
float(input)
return True
except Exception as e:
return False

def _check_is_alpha(self, input):
"""Check if a given string is alpha only"""
return True if self.REGEX_ALPHA.match(input) else False

def _check_is_alphanumeric(self, input):
"""Check if a given string is alphanumeric"""
return True if input.isalnum() else False

def _check_is_integer(self, input):
"""Check if a given string is integer"""
try:
int(input)
return True
except Exception as e:
return False

def _check_is_float(self, input):
"""Check if a given string is float"""
try:
return True if str(float(input)) == input else False
except Exception as e:
return False

def _check_longer_than(self, input, args):
"""Check if a given string is longer than n"""
return True if len(input) > args[0] else False

def _check_shorter_than(self, input, args):
"""Check if a given string is shorter than n"""
return True if len(input) < args[0] else False

def _check_is_email(self, input):
"""Check if a given string is a valid email"""
return True if self.REGEX_EMAIL.match(input) else False

def _check_is_tld(self, input):
"""Check if a given string is a top level domain
(only matches formats aaa.bbb and ccc.aaa.bbb)"""
return True if self.REGEX_TLD.match(input) else False

def _check_is_handle(self, input):
"""Check if a given string is a username
handle (alpha-numeric-underscore)"""
return True if self.REGEX_HANDLE.match(input) else False
3 changes: 2 additions & 1 deletion src/dashboard.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
bp_stat_view, bp_stat_api, \
bp_cluster_view, bp_cluster_api, \
bp_host_view, bp_host_api, bp_auth_api, \
bp_login, bp_user_api, bp_user_view
bp_login, bp_user_api, bp_user_view, front_rest_user_v2
from modules.user import User

logger = logging.getLogger(__name__)
Expand Down Expand Up @@ -54,6 +54,7 @@
app.register_blueprint(bp_login)
app.register_blueprint(bp_user_api)
app.register_blueprint(bp_user_view)
app.register_blueprint(front_rest_user_v2)

admin = os.environ.get("ADMIN", "admin")
admin_password = os.environ.get("ADMIN_PASSWORD", "pass")
Expand Down
2 changes: 1 addition & 1 deletion src/modules/models/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,4 @@
# SPDX-License-Identifier: Apache-2.0
#
from .user import ADMIN, OPERATOR, COMMON_USER, \
User, LoginHistory
User, LoginHistory, Profile
37 changes: 36 additions & 1 deletion src/modules/models/user.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,16 +12,51 @@
COMMON_USER = 2


class Profile(Document):
"""
Profile model of User
:member name: name of user
:member email: email of user
:member bio: bio of user
:member organization: organization of user
:member url: user's url
:member location: user's location
"""
name = StringField(default="")
email = StringField(default="")
bio = StringField(default="")
organization = StringField(default="")
url = StringField(default="")
location = StringField(default="")


class User(Document):
"""
User model
:member username: user's username
:member password: user's password, save encrypted password
:member active: whether user is active
:member isAdmin: whether user is admin
:member role: user's role
:member timestamp: user's create time
:member balance: user's balance
:member profile: user's profile
"""
username = StringField(unique=True)
password = StringField(default=True)
password = StringField(default="")
active = BooleanField(default=True)
isAdmin = BooleanField(default=False)
role = IntField(default=COMMON_USER)
timestamp = DateTimeField(default=datetime.datetime.now)
balance = IntField(default=0)
profile = ReferenceField(Profile)


class LoginHistory(Document):
"""
User login history
:member time: login time
:member user: which user object
"""
time = DateTimeField(default=datetime.datetime.now)
user = ReferenceField(User)
1 change: 1 addition & 0 deletions src/modules/user/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,4 @@
DeleteUser, UserInfo
from .auth import Register, Login
from .user import User
from .profile import UserProfile
8 changes: 7 additions & 1 deletion src/modules/user/auth/login.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
sys.path.append(os.path.join(os.path.dirname(__file__), '..', '..', '..'))
from common import log_handler, LOG_LEVEL
from modules.user.user import User
from modules.models import LoginHistory

logger = logging.getLogger(__name__)
logger.setLevel(LOG_LEVEL)
Expand Down Expand Up @@ -43,10 +44,15 @@ def post(self, **kwargs):

user_obj = User()
try:
user = user_obj.get_by_username_w_password(username)
user = user_obj.get_by_username(username)
# compare input password with password in db
if bcrypt.checkpw(password.encode('utf8'),
bytes(user.password.encode())):
login_user(user)

# if login success save login history
login_history = LoginHistory(user=user.dbUser)
login_history.save()
user_id = str(user.id)
data = {
"success": True,
Expand Down
125 changes: 125 additions & 0 deletions src/modules/user/profile.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@

# Copyright IBM Corp, All Rights Reserved.
#
# SPDX-License-Identifier: Apache-2.0
#
from flask_restful import Resource, fields, marshal_with, reqparse
from flask_login import login_required
import logging
import sys
import os

sys.path.append(os.path.join(os.path.dirname(__file__), '..', '..'))
from common import log_handler, LOG_LEVEL, StringValidator
from modules.user.user import User

logger = logging.getLogger(__name__)
logger.setLevel(LOG_LEVEL)
logger.addHandler(log_handler)


class ValidationError(Exception):
pass


def email(email_str):
"""
Email Field to validate input email string
:param email_str: email address
:return: email address string or raise exception
"""
validator = StringValidator()
if validator.validate(email_str, ["is_email"]):
return email_str
else:
raise ValidationError("{} not a valid email".format(email_str))


user_profile_fields = {
"username": fields.String,
"name": fields.String,
"email": fields.String,
"bio": fields.String,
"url": fields.String,
"location": fields.String
}

profile_response_fields = {
"result": fields.Nested(user_profile_fields),
"success": fields.Boolean,
"error": fields.String
}

update_response_fields = {
"success": fields.Boolean,
"error": fields.String
}

update_profile_parser = reqparse.RequestParser()
update_profile_parser.add_argument('name',
location='form',
help='name for update')
update_profile_parser.add_argument('email',
type=email,
location='form',
help='email for update')
update_profile_parser.add_argument('bio',
location='form',
help='bio for update')
update_profile_parser.add_argument('url',
location='form',
help='url for update')
update_profile_parser.add_argument('location',
location='form',
help='location for update')


class UserProfile(Resource):
"""
User Profile class, supply get/put method
"""
@marshal_with(profile_response_fields)
def get(self, user_id):
"""
Get user profile information
:param user_id: user id of User to query
:return: profile data, status code
"""
user_obj = User()
user = user_obj.get_by_id(user_id)
if not user:
return {"error": "No such User", "success": False}, 400

data = {
"result": {
"username": user.username,
"name": user.profile.name if user.profile else "",
"email": user.profile.email if user.profile else "",
"bio": user.profile.bio if user.profile else "",
"url": user.profile.url if user.profile else "",
"location": user.profile.location if user.profile else "",
},
"success": True
}

return data, 200

@marshal_with(update_response_fields)
def put(self, user_id):
"""
Update user profile
:param user_id: user id of User to update profile
:return: api response, status code
"""
args = update_profile_parser.parse_args()
name, email_addr = args["name"], args["email"]
bio, url = args["bio"], args["url"]
location = args["location"]
user_obj = User()
user = user_obj.get_by_id(user_id)
if not user:
return {"error": "No such User", "success": False}, 400
else:
user.update_profile(name=name, email=email_addr,
bio=bio, url=url, location=location)
return {"success": True}, 200
Loading

0 comments on commit f380987

Please sign in to comment.