Skip to content

Commit f380987

Browse files
committed
[CE-116] Enable user profile get/update
Add user profile get/update support Add test case for user profile Change-Id: I5fbcc4814554a8da9827fd6b4ceb33a381249f98 Signed-off-by: Haitao Yue <hightall@me.com>
1 parent aba14bc commit f380987

File tree

17 files changed

+500
-51
lines changed

17 files changed

+500
-51
lines changed

docker-compose.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -117,7 +117,7 @@ services:
117117
environment:
118118
- SV_BaseURL=http://dashboard:8080/api/
119119
- RESTful_Server=dashboard:8080
120-
- RESTful_BaseURL=/v2/
120+
- RESTful_BaseURL=/api/v2/
121121
- DEBUG=node:*
122122
volumes:
123123
- ./user-dashboard:/usr/app/src

src/common/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,3 +24,4 @@
2424

2525
from .fabric_network_config import \
2626
FabricPreNetworkConfig, FabricV1NetworkConfig
27+
from .stringvalidator import StringValidator

src/common/stringvalidator.py

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
2+
# Copyright IBM Corp, All Rights Reserved.
3+
#
4+
# SPDX-License-Identifier: Apache-2.0
5+
#
6+
import re
7+
8+
9+
class StringValidator(object):
10+
# email borrowed from chenglee's validator
11+
REGEX_EMAIL = re.compile(
12+
'^[-!#$%&\'*+\\.\/0-9=?A-Z^_`{|}~]+@([-0-9A-Z]+\.)+([0-9A-Z]){2,4}$',
13+
re.IGNORECASE)
14+
REGEX_ALPHA = re.compile('^[a-z]+$', re.IGNORECASE)
15+
REGEX_TLD = re.compile('([a-z\-0-9]+\.)?([a-z\-0-9]+)\.([a-z]+)$',
16+
re.IGNORECASE)
17+
REGEX_HANDLE = re.compile('[a-z0-9\_]+$', re.IGNORECASE)
18+
19+
def validate(self, input, checks=[], log=False):
20+
21+
results = {}
22+
fail = False
23+
24+
# pass the input to the given checks one by one
25+
for check in checks:
26+
try:
27+
if isinstance(check, tuple):
28+
check_name = check[0]
29+
args = check[slice(1, len(check))]
30+
else:
31+
check_name = check
32+
args = None
33+
34+
method = getattr(self, '_check_' + check_name)
35+
results[check] = method(input.strip(),
36+
args) if args else method(input)
37+
38+
if not results[check]:
39+
if log:
40+
fail = True
41+
else:
42+
return False
43+
44+
except Exception as e:
45+
raise
46+
47+
return True if not fail else results
48+
49+
def _check_not_empty(self, input):
50+
"""Check if a given string is empty"""
51+
return False if not input else True
52+
53+
def _check_is_numeric(self, input):
54+
"""Check if a given string is numeric"""
55+
try:
56+
float(input)
57+
return True
58+
except Exception as e:
59+
return False
60+
61+
def _check_is_alpha(self, input):
62+
"""Check if a given string is alpha only"""
63+
return True if self.REGEX_ALPHA.match(input) else False
64+
65+
def _check_is_alphanumeric(self, input):
66+
"""Check if a given string is alphanumeric"""
67+
return True if input.isalnum() else False
68+
69+
def _check_is_integer(self, input):
70+
"""Check if a given string is integer"""
71+
try:
72+
int(input)
73+
return True
74+
except Exception as e:
75+
return False
76+
77+
def _check_is_float(self, input):
78+
"""Check if a given string is float"""
79+
try:
80+
return True if str(float(input)) == input else False
81+
except Exception as e:
82+
return False
83+
84+
def _check_longer_than(self, input, args):
85+
"""Check if a given string is longer than n"""
86+
return True if len(input) > args[0] else False
87+
88+
def _check_shorter_than(self, input, args):
89+
"""Check if a given string is shorter than n"""
90+
return True if len(input) < args[0] else False
91+
92+
def _check_is_email(self, input):
93+
"""Check if a given string is a valid email"""
94+
return True if self.REGEX_EMAIL.match(input) else False
95+
96+
def _check_is_tld(self, input):
97+
"""Check if a given string is a top level domain
98+
(only matches formats aaa.bbb and ccc.aaa.bbb)"""
99+
return True if self.REGEX_TLD.match(input) else False
100+
101+
def _check_is_handle(self, input):
102+
"""Check if a given string is a username
103+
handle (alpha-numeric-underscore)"""
104+
return True if self.REGEX_HANDLE.match(input) else False

src/dashboard.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
bp_stat_view, bp_stat_api, \
1717
bp_cluster_view, bp_cluster_api, \
1818
bp_host_view, bp_host_api, bp_auth_api, \
19-
bp_login, bp_user_api, bp_user_view
19+
bp_login, bp_user_api, bp_user_view, front_rest_user_v2
2020
from modules.user import User
2121

2222
logger = logging.getLogger(__name__)
@@ -54,6 +54,7 @@
5454
app.register_blueprint(bp_login)
5555
app.register_blueprint(bp_user_api)
5656
app.register_blueprint(bp_user_view)
57+
app.register_blueprint(front_rest_user_v2)
5758

5859
admin = os.environ.get("ADMIN", "admin")
5960
admin_password = os.environ.get("ADMIN_PASSWORD", "pass")

src/modules/models/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,4 +4,4 @@
44
# SPDX-License-Identifier: Apache-2.0
55
#
66
from .user import ADMIN, OPERATOR, COMMON_USER, \
7-
User, LoginHistory
7+
User, LoginHistory, Profile

src/modules/models/user.py

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,16 +12,51 @@
1212
COMMON_USER = 2
1313

1414

15+
class Profile(Document):
16+
"""
17+
Profile model of User
18+
:member name: name of user
19+
:member email: email of user
20+
:member bio: bio of user
21+
:member organization: organization of user
22+
:member url: user's url
23+
:member location: user's location
24+
"""
25+
name = StringField(default="")
26+
email = StringField(default="")
27+
bio = StringField(default="")
28+
organization = StringField(default="")
29+
url = StringField(default="")
30+
location = StringField(default="")
31+
32+
1533
class User(Document):
34+
"""
35+
User model
36+
:member username: user's username
37+
:member password: user's password, save encrypted password
38+
:member active: whether user is active
39+
:member isAdmin: whether user is admin
40+
:member role: user's role
41+
:member timestamp: user's create time
42+
:member balance: user's balance
43+
:member profile: user's profile
44+
"""
1645
username = StringField(unique=True)
17-
password = StringField(default=True)
46+
password = StringField(default="")
1847
active = BooleanField(default=True)
1948
isAdmin = BooleanField(default=False)
2049
role = IntField(default=COMMON_USER)
2150
timestamp = DateTimeField(default=datetime.datetime.now)
2251
balance = IntField(default=0)
52+
profile = ReferenceField(Profile)
2353

2454

2555
class LoginHistory(Document):
56+
"""
57+
User login history
58+
:member time: login time
59+
:member user: which user object
60+
"""
2661
time = DateTimeField(default=datetime.datetime.now)
2762
user = ReferenceField(User)

src/modules/user/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,3 +7,4 @@
77
DeleteUser, UserInfo
88
from .auth import Register, Login
99
from .user import User
10+
from .profile import UserProfile

src/modules/user/auth/login.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
sys.path.append(os.path.join(os.path.dirname(__file__), '..', '..', '..'))
1515
from common import log_handler, LOG_LEVEL
1616
from modules.user.user import User
17+
from modules.models import LoginHistory
1718

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

4445
user_obj = User()
4546
try:
46-
user = user_obj.get_by_username_w_password(username)
47+
user = user_obj.get_by_username(username)
48+
# compare input password with password in db
4749
if bcrypt.checkpw(password.encode('utf8'),
4850
bytes(user.password.encode())):
4951
login_user(user)
52+
53+
# if login success save login history
54+
login_history = LoginHistory(user=user.dbUser)
55+
login_history.save()
5056
user_id = str(user.id)
5157
data = {
5258
"success": True,

src/modules/user/profile.py

Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
2+
# Copyright IBM Corp, All Rights Reserved.
3+
#
4+
# SPDX-License-Identifier: Apache-2.0
5+
#
6+
from flask_restful import Resource, fields, marshal_with, reqparse
7+
from flask_login import login_required
8+
import logging
9+
import sys
10+
import os
11+
12+
sys.path.append(os.path.join(os.path.dirname(__file__), '..', '..'))
13+
from common import log_handler, LOG_LEVEL, StringValidator
14+
from modules.user.user import User
15+
16+
logger = logging.getLogger(__name__)
17+
logger.setLevel(LOG_LEVEL)
18+
logger.addHandler(log_handler)
19+
20+
21+
class ValidationError(Exception):
22+
pass
23+
24+
25+
def email(email_str):
26+
"""
27+
Email Field to validate input email string
28+
:param email_str: email address
29+
:return: email address string or raise exception
30+
"""
31+
validator = StringValidator()
32+
if validator.validate(email_str, ["is_email"]):
33+
return email_str
34+
else:
35+
raise ValidationError("{} not a valid email".format(email_str))
36+
37+
38+
user_profile_fields = {
39+
"username": fields.String,
40+
"name": fields.String,
41+
"email": fields.String,
42+
"bio": fields.String,
43+
"url": fields.String,
44+
"location": fields.String
45+
}
46+
47+
profile_response_fields = {
48+
"result": fields.Nested(user_profile_fields),
49+
"success": fields.Boolean,
50+
"error": fields.String
51+
}
52+
53+
update_response_fields = {
54+
"success": fields.Boolean,
55+
"error": fields.String
56+
}
57+
58+
update_profile_parser = reqparse.RequestParser()
59+
update_profile_parser.add_argument('name',
60+
location='form',
61+
help='name for update')
62+
update_profile_parser.add_argument('email',
63+
type=email,
64+
location='form',
65+
help='email for update')
66+
update_profile_parser.add_argument('bio',
67+
location='form',
68+
help='bio for update')
69+
update_profile_parser.add_argument('url',
70+
location='form',
71+
help='url for update')
72+
update_profile_parser.add_argument('location',
73+
location='form',
74+
help='location for update')
75+
76+
77+
class UserProfile(Resource):
78+
"""
79+
User Profile class, supply get/put method
80+
"""
81+
@marshal_with(profile_response_fields)
82+
def get(self, user_id):
83+
"""
84+
Get user profile information
85+
:param user_id: user id of User to query
86+
:return: profile data, status code
87+
"""
88+
user_obj = User()
89+
user = user_obj.get_by_id(user_id)
90+
if not user:
91+
return {"error": "No such User", "success": False}, 400
92+
93+
data = {
94+
"result": {
95+
"username": user.username,
96+
"name": user.profile.name if user.profile else "",
97+
"email": user.profile.email if user.profile else "",
98+
"bio": user.profile.bio if user.profile else "",
99+
"url": user.profile.url if user.profile else "",
100+
"location": user.profile.location if user.profile else "",
101+
},
102+
"success": True
103+
}
104+
105+
return data, 200
106+
107+
@marshal_with(update_response_fields)
108+
def put(self, user_id):
109+
"""
110+
Update user profile
111+
:param user_id: user id of User to update profile
112+
:return: api response, status code
113+
"""
114+
args = update_profile_parser.parse_args()
115+
name, email_addr = args["name"], args["email"]
116+
bio, url = args["bio"], args["url"]
117+
location = args["location"]
118+
user_obj = User()
119+
user = user_obj.get_by_id(user_id)
120+
if not user:
121+
return {"error": "No such User", "success": False}, 400
122+
else:
123+
user.update_profile(name=name, email=email_addr,
124+
bio=bio, url=url, location=location)
125+
return {"success": True}, 200

0 commit comments

Comments
 (0)