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

Commit

Permalink
New account page (formerly the profile page)
Browse files Browse the repository at this point in the history
  • Loading branch information
jace committed Sep 25, 2018
2 parents 5d9ed12 + f4fbfce commit 6090565
Show file tree
Hide file tree
Showing 28 changed files with 647 additions and 166 deletions.
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

0 comments on commit 6090565

Please sign in to comment.