Skip to content
This repository has been archived by the owner on Apr 28, 2020. It is now read-only.

Updated profile page #233

Merged
merged 32 commits into from
Sep 25, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
af8325f
Update UI of profile page.
vidya-ram Mar 7, 2018
fa5d26b
Add config.rb for lastuser_ui folder.
vidya-ram Mar 12, 2018
5d38d9d
added method to make email address primary for a user
Apr 19, 2018
2412ee8
added helper method to mask email address
Apr 19, 2018
df80c6a
added views to make email address primary and delete external IDs
Apr 19, 2018
c2f3f8d
Added some comments
Apr 19, 2018
342f1fe
moved mask_email to utils.py
Apr 19, 2018
31c2a54
using fixed number of masking character
Apr 20, 2018
3f25ad2
Minor UI fix.
vidya-ram Apr 20, 2018
38ca4db
Update btn text.
vidya-ram Apr 20, 2018
e3bc165
Merge branch 'profile_page' of https://github.com/hasgeek/lastuser in…
vidya-ram Apr 20, 2018
ff98b7e
Add EmailPrimaryForm and radio buttons to select email.
vidya-ram Apr 24, 2018
4b5fa36
Update the profile page UI.
vidya-ram Apr 25, 2018
76ebe6d
added option to add external ID
May 23, 2018
61d64f9
simplified mask_email()
Jun 20, 2018
9243fb2
moved non-existent extid handling to postcallback
Jun 20, 2018
1c021bc
removed the nopasswd flag, flashing directly
Jun 20, 2018
316586f
removed model method to make email primary
Jun 20, 2018
9a91373
replaced flask.ext.assets with flask_assets
Jun 20, 2018
79674ce
fixed test for making email primary
Jun 20, 2018
94087e5
added endpoint to resend verification email
Aug 13, 2018
88680ef
Merge branch 'master' into profile_page
jace Sep 18, 2018
177f082
Fix active session display and remove PK from extid removal route
jace Sep 18, 2018
5086a53
Hide orgs and apps in profile page
jace Sep 20, 2018
4472cdc
Rename 'profile' to 'account'
jace Sep 20, 2018
33fb9b3
Cleanup account page and phone handling
jace Sep 20, 2018
9df117e
Remove OpenID in tests
jace Sep 20, 2018
ac9524b
Send phone update notifications; support old verification links
jace Sep 25, 2018
61e083b
Merge stylesheets
jace Sep 25, 2018
10420a2
Remove phone type field
jace Sep 25, 2018
dcd04f2
Review account merge handling
jace Sep 25, 2018
f4fbfce
Remove unnecessary Compass config file
jace Sep 25, 2018
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -25,3 +25,6 @@ baseframe-packed.js
secrets.dev
secrets.test
*/.well-known/acme-challenge
ghostdriver.log
.pytest_cache
.vscode
16 changes: 7 additions & 9 deletions lastuser_core/models/session.py
Original file line number Diff line number Diff line change
Expand Up @@ -97,13 +97,11 @@ def authenticate(cls, buid):
cls.revoked_at == None).one_or_none() # NOQA


# Patch a retriever into the User class. This could be placed in the
# UserSession.user relationship's backref with a custom primaryjoin
# clause and explicit foreign_keys, but we're not sure if we can
# put the db.func.utcnow() in there too.
def active_sessions(self):
return self.sessions.filter(
User.active_sessions = db.relationship(UserSession,
lazy='dynamic',
primaryjoin=db.and_(
UserSession.user_id == User.id,
UserSession.accessed_at > db.func.utcnow() - timedelta(days=14),
UserSession.revoked_at == None).all() # NOQA

User.active_sessions = active_sessions
UserSession.revoked_at == None # NOQA
)
)
24 changes: 24 additions & 0 deletions lastuser_core/models/user.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
from hashlib import md5
from werkzeug import check_password_hash, cached_property
import bcrypt
import phonenumbers
from sqlalchemy import or_, event, DDL
from sqlalchemy.orm import defer, deferred
from sqlalchemy.ext.hybrid import hybrid_property
Expand Down Expand Up @@ -841,6 +842,12 @@ def __unicode__(self):
def __str__(self):
return str(self.__unicode__())

def parsed(self):
return phonenumbers.parse(self._phone)

def formatted(self):
return phonenumbers.format_number(self.parsed(), phonenumbers.PhoneNumberFormat.INTERNATIONAL)

@property
def primary(self):
return self.user.primary_phone == self
Expand Down Expand Up @@ -880,6 +887,7 @@ class UserPhoneClaim(OwnerMixin, BaseMixin, db.Model):
_phone = db.Column('phone', db.Unicode(16), nullable=False, index=True)
gets_text = db.Column(db.Boolean, nullable=False, default=True)
verification_code = db.Column(db.Unicode(4), nullable=False, default=newpin)
verification_attempts = db.Column(db.Integer, nullable=False, default=0)

private = db.Column(db.Boolean, nullable=False, default=False)
type = db.Column(db.Unicode(30), nullable=True)
Expand Down Expand Up @@ -917,6 +925,16 @@ def __unicode__(self):
def __str__(self):
return str(self.__unicode__())

def parsed(self):
return phonenumbers.parse(self._phone)

def formatted(self):
return phonenumbers.format_number(self.parsed(), phonenumbers.PhoneNumberFormat.INTERNATIONAL)

@hybrid_property
def verification_expired(self):
return self.verification_attempts >= 3

def permissions(self, user, inherited=None):
perms = super(UserPhoneClaim, self).permissions(user, inherited)
if user and user == self.user:
Expand Down Expand Up @@ -990,6 +1008,12 @@ def get(cls, service, userid=None, username=None):
param, value = require_one_of(True, userid=userid, username=username)
return cls.query.filter_by(**{param: value, 'service': service}).one_or_none()

def permissions(self, user, inherited=None):
perms = super(UserExternalId, self).permissions(user, inherited)
if user and user == self.user:
perms.add('delete_extid')
return perms


add_primary_relationship(User, 'primary_email', UserEmail, 'user', 'user_id')
add_primary_relationship(User, 'primary_phone', UserPhone, 'user', 'user_id')
Expand Down
14 changes: 14 additions & 0 deletions lastuser_core/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,3 +58,17 @@ def get_gravatar_md5sum(url):
if len(md5sum) != 32:
return None
return md5sum


def mask_email(email):
"""
Masks an email address

>>> mask_email(u'foobar@example.com')
u'f****@e****'

"""
if '@' not in email:
return email
username, domain = email.split('@')
return u'{u}****@{d}****'.format(u=username[0], d=domain[0])
83 changes: 80 additions & 3 deletions lastuser_oauth/static/css/app.css
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ a.loginbutton:hover {
}

input[name="field-openid"] {
background: url('../img/openid.png?1425290087') 4px no-repeat;
background: url('../img/openid.png?1366451008') 4px no-repeat;
width: 230px;
padding-left: 25px;
}
Expand All @@ -70,7 +70,7 @@ input[name="field-openid"] {
overflow: hidden;
text-align: left;
text-transform: capitalize;
background-image: url('../img/logo.png?1425290087');
background-image: url('../img/logo.png?1372854153');
background-repeat: no-repeat;
background-position: 50% 50%;
width: 113px;
Expand All @@ -79,10 +79,87 @@ input[name="field-openid"] {

@media only screen and (-webkit-min-device-pixel-ratio: 2) {
#logo {
background-image: url('../img/logo@2x.png?1425290087');
background-image: url('../img/logo@2x.png?1372854195');
-moz-background-size: 113px 75px;
-o-background-size: 113px 75px;
-webkit-background-size: 113px 75px;
background-size: 113px 75px;
}
}
.detail-box {
margin: 10px;
border-bottom: 1px solid #ddd;
margin: 0 -15px;
padding: 15px;
}
.detail-box .heading, .detail-box .heading-special {
margin: 0;
}
.detail-box .heading-special {
cursor: pointer;
}
.detail-box .para {
margin: 0;
}
.detail-box .button-box {
margin: 12px auto 0;
}
.detail-box .button-box-button {
margin-bottom: 12px;
}
.detail-box .detail-box-form {
padding: 12px 0 0;
}
.detail-box .detail-box-form .well {
margin-bottom: 0;
}
.detail-box .detail-box-panel {
margin-bottom: 0;
margin-top: 5px;
}
.detail-box .detail-box-table {
margin-bottom: 0;
}
.detail-box .detail-box-table tr:first-child > td {
border-top: 0;
}
.detail-box .detail-box-table td:first-child {
padding-left: 0;
}
.detail-box .detail-box-table .listwidget ul input {
top: 3px;
}

@media screen and (min-width: 768px) {
.detail > div {
display: inline-block;
float: none;
vertical-align: top;
margin: 0 -1px 30px;
}

.detail-box {
-webkit-box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1), 0 0 10px rgba(0, 0, 0, 0.1) inset;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1), 0 0 10px rgba(0, 0, 0, 0.1) inset;
padding: 15px;
margin: 0;
border: 1px solid #ddd;
border-radius: 4px;
}
.detail-box .button-box {
float: right;
}
.detail-box .button-box-button {
margin-bottom: 0;
}
.detail-box .detail-box-form {
clear: both;
}
}
@media (min-width: 992px) and (max-width: 1199px) {
.detail-box .form-horizontal .help-required {
position: absolute;
right: -10px;
bottom: 0;
}
}
78 changes: 78 additions & 0 deletions lastuser_oauth/static/sass/app.sass
Original file line number Diff line number Diff line change
Expand Up @@ -66,3 +66,81 @@ input[name="field-openid"]
#logo
background-image: image-url('logo@2x.png')
+background-size(image-width('logo.png') image-height('logo.png'))


// UI element: detail box
.detail-box
margin: 10px
border-bottom: 1px solid #ddd
margin: 0 -15px
padding: 15px

.heading
margin : 0

.heading-special
@extend .heading
cursor: pointer

.para
margin: 0

.button-box
margin: 12px auto 0

.button-box-button
margin-bottom: 12px

.detail-box-form
padding: 12px 0 0

.well
margin-bottom: 0


.detail-box-panel
margin-bottom: 0
margin-top: 5px

.detail-box-table
margin-bottom: 0

tr:first-child>td
border-top: 0

td:first-child
padding-left: 0

.listwidget ul input
top: 3px


@media screen and (min-width:768px)
.detail > div
display: inline-block
float: none
vertical-align: top
margin: 0 -1px 30px

.detail-box
-webkit-box-shadow: 0 1px 2px rgba(0,0,0,0.1), 0 0 10px rgba(0,0,0,0.1) inset
box-shadow: 0 1px 2px rgba(0,0,0,0.1), 0 0 10px rgba(0,0,0,0.1) inset
padding: 15px
margin: 0
border: 1px solid #ddd
border-radius: 4px

.button-box
float: right

.button-box-button
margin-bottom: 0

.detail-box-form
clear: both

@media (min-width:992px) and (max-width:1199px)
.detail-box .form-horizontal .help-required
position: absolute
right: -10px
bottom: 0
2 changes: 1 addition & 1 deletion lastuser_oauth/templates/merge.html.jinja2
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@
{% endif %}
<div class="form-actions">
<input class="btn btn-primary" type="submit" name="merge" value="Merge accounts"/>
<input class="btn" type="submit" name="skip" value="Skip"/>
<input class="btn btn-default" type="submit" name="skip" value="Skip"/>
</div>
</form>
{% endblock %}
24 changes: 16 additions & 8 deletions lastuser_oauth/views/account.py
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,14 @@ def get_user_extid(service, userdata):


def login_service_postcallback(service, userdata):
"""
Called from :func:login_service_callback after receiving data from the upstream login service
"""
# 1. Check whether we have an existing UserExternalId
user, extid, useremail = get_user_extid(service, userdata)
# If extid is not None, user.extid == user, guaranteed.
# If extid is None but useremail is not None, user == useremail.user
# However, if both extid and useremail are present, they may be different users

if extid is not None:
extid.oauth_token = userdata.get('oauth_token')
Expand All @@ -115,7 +122,7 @@ def login_service_postcallback(service, userdata):
)

if user is None:
if current_auth.is_authenticated:
if current_auth:
# Attach this id to currently logged-in user
user = current_auth.user
extid.user = user
Expand All @@ -127,12 +134,13 @@ def login_service_postcallback(service, userdata):
if valid_username(userdata['username']) and user.is_valid_username(userdata['username']):
# Set a username for this user if it's available
user.username = userdata['username']
else: # This id is attached to a user
if current_auth.is_authenticated and current_auth.user != user:
else: # We have an existing user account from extid or useremail
if current_auth and current_auth.user != user:
# Woah! Account merger handler required
# Always confirm with user before doing an account merger
session['merge_buid'] = user.buid
elif useremail and useremail.user != user:
# Once again, account merger required since the extid and useremail are linked to different users
session['merge_buid'] = useremail.user.buid

# Check for new email addresses
Expand Down Expand Up @@ -161,7 +169,7 @@ def login_service_postcallback(service, userdata):
if not user.fullname and userdata.get('fullname'):
user.fullname = userdata['fullname']

if not current_auth.is_authenticated: # If a user isn't already logged in, login now.
if not current_auth: # If a user isn't already logged in, login now.
login_internal(user)
flash(_(u"You have logged in via {service}").format(service=login_registry[service].title), 'success')
next_url = get_next_url(session=True)
Expand All @@ -171,19 +179,19 @@ def login_service_postcallback(service, userdata):

# Finally: set a login method cookie and send user on their way
if not current_auth.user.is_profile_complete():
login_next = url_for('.profile_new', next=next_url)
login_next = url_for('.account_new', next=next_url)
else:
login_next = next_url

if 'merge_buid' in session:
return set_loginmethod_cookie(redirect(url_for('.profile_merge', next=login_next), code=303), service)
return set_loginmethod_cookie(redirect(url_for('.account_merge', next=login_next), code=303), service)
else:
return set_loginmethod_cookie(redirect(login_next, code=303), service)


@lastuser_oauth.route('/profile/merge', methods=['GET', 'POST'])
@lastuser_oauth.route('/account/merge', methods=['GET', 'POST'])
@requires_login
def profile_merge():
def account_merge():
if 'merge_buid' not in session:
return redirect(get_next_url(), code=302)
other_user = User.get(buid=session['merge_buid'])
Expand Down
Loading