diff --git a/pandora/config.0xdb.jsonc b/pandora/config.0xdb.jsonc index beed13d7..14cf5e6e 100644 --- a/pandora/config.0xdb.jsonc +++ b/pandora/config.0xdb.jsonc @@ -696,6 +696,7 @@ }, "menuExtras": [ "user", + //"persona", //"locale", "reload" ], diff --git a/pandora/user/persona.py b/pandora/user/persona.py new file mode 100644 index 00000000..8f017cb6 --- /dev/null +++ b/pandora/user/persona.py @@ -0,0 +1,88 @@ +# -*- coding: utf-8 -*- +# vi:si:et:sw=4:sts=4:ts=4 + +from django.conf import settings +from django.utils.http import same_origin +from django.core.exceptions import ImproperlyConfigured +from django.contrib.auth.models import User +from django.contrib import auth +try: + from django.utils.encoding import smart_bytes +except ImportError: + from django.utils.encoding import smart_str as smart_bytes + +from ox.django.shortcuts import json_response +from ox.utils import json +import requests + +import models + + +def signin(request): + data = json.loads(request.POST['data']) + response = json_response({ + 'errors': { + 'email': 'Failed to verify email' + } + }) + verification_data = verify(request, data['assertion']) + if verification_data: + email = verification_data['email'] + username = data.get('username') + qs = User.objects.filter(email__iexact=email) + if qs.count() == 0: + if not username: + response = json_response({ + 'errors': { + 'username': 'New user, please provide username' + } + }) + return response + user = User() + user.email = email + user.username = username + user.save() + else: + user = qs[0] + if user.is_active: + request.session['ui'] = '{}' + #fixme. use custom backend instead? + user.backend = 'django.contrib.auth.backends.ModelBackend' + auth.login(request, user) + user_json = models.init_user(user, request) + response = json_response({ + 'user': user_json + }) + else: + response = json_response({ + 'errors': { + 'email': 'User Disabled' + } + }) + return response + +def get_audience(request): + try: + audiences = settings.BROWSERID_AUDIENCES + except AttributeError: + raise ImproperlyConfigured('Required setting BROWSERID_AUDIENCES not found!') + + protocol = 'https' if request.is_secure() else 'http' + host = '%s://%s' % (protocol, request.get_host()) + for audience in audiences: + if same_origin(host, audience): + return audience + + raise ImproperlyConfigured('No audience could be found in BROWSERID_AUDIENCES for host `{0}`.' + .format(host)) + +def verify(request, assertion): + audience = get_audience(request) + data = {'assertion': assertion, 'audience': audience} + resp = requests.post('https://verifier.login.persona.org/verify', data=data, verify=True) + if resp.ok: + verification_data = json.loads(resp.content) + if verification_data['status'] == 'okay': + return verification_data + return None + diff --git a/pandora/user/views.py b/pandora/user/views.py index 0fd30ac9..69018d8a 100644 --- a/pandora/user/views.py +++ b/pandora/user/views.py @@ -23,6 +23,7 @@ from item import utils import models from decorators import capability_required_json +import persona def signin(request): ''' @@ -42,7 +43,9 @@ def signin(request): } ''' data = json.loads(request.POST['data']) - if 'username' in data and 'password' in data: + if 'assertion' in data: + response = persona.signin(request) + elif 'username' in data and 'password' in data: data['username'] = data['username'].strip() if settings.AUTH_CHECK_USERNAME: qs = User.objects.filter(username__iexact=data['username']) diff --git a/requirements.txt b/requirements.txt index 9d41c15b..d3032a0f 100644 --- a/requirements.txt +++ b/requirements.txt @@ -13,3 +13,4 @@ django-celery>=2.4.2 gunicorn>=0.14.3 html5lib South +requests>=2.0.0 diff --git a/static/js/account.js b/static/js/account.js index d470ea03..b6e0f541 100644 --- a/static/js/account.js +++ b/static/js/account.js @@ -30,24 +30,28 @@ pandora.ui.accountDialogOptions = function(action, value) { var buttons = { signin: ['signup', 'reset'], signup: ['signin'], + username: [], reset: ['signin'], resetAndSignin: [] }, buttonTitle = { signin: 'Sign In', signup: 'Sign Up', + username: 'Sign Up', reset: 'Reset Password', resetAndSignin: 'Sign In' }, dialogText = { signin: Ox._('To sign in to your account, please enter your username and password.'), signup: Ox._('To sign up for an account, please choose a username and password, and enter your e-mail address.'), + username: Ox._('To sign up for an account, please choose a username.'), reset: Ox._('To reset your password, please enter either your username or your e-mail address.'), resetAndSignin: Ox._('To sign in to your account, please choose a new password, and enter the code that we have just e-mailed to you.') }, dialogTitle = { signin: Ox._('Sign In'), signup: Ox._('Sign Up'), + username: Ox._('Sign Up'), reset: Ox._('Reset Password'), resetAndSignin: Ox._('Reset Password') }; @@ -132,6 +136,7 @@ pandora.ui.accountForm = function(action, value) { var items = { 'signin': ['username', 'password'], 'signup': ['newUsername', 'password', 'email'], + 'username': ['newUsername'], 'reset': ['usernameOrEmail'], 'resetAndSignin': ['oldUsername', 'newPassword', 'code'] }, diff --git a/static/js/mainMenu.js b/static/js/mainMenu.js index d9e10b47..eb8e9755 100644 --- a/static/js/mainMenu.js +++ b/static/js/mainMenu.js @@ -16,6 +16,8 @@ pandora.ui.mainMenu = function() { return pandora.$ui.localeButton = pandora.ui.localeButton(); } else if (menuExtra == 'reload') { return pandora.$ui.loadingIcon = pandora.ui.loadingIcon(); + } else if (menuExtra == 'persona') { + return pandora.$ui.personaButton = pandora.ui.personaButton(); } }), id: 'mainMenu', diff --git a/static/js/persona.js b/static/js/persona.js new file mode 100644 index 00000000..0a4250a6 --- /dev/null +++ b/static/js/persona.js @@ -0,0 +1,115 @@ +'use strict'; + +pandora.persona = {}; + +pandora.persona.init = function(callback) { + + (!navigator.id ? Ox.getFile : Ox.noop)( + 'https://login.persona.org/include.js', + function() { + navigator.id.watch({ + loggedInUser: pandora.user.email ? pandora.user.email : null, + onlogin: onlogin, + onlogout: onlogout + }); + callback && callback(); + } + ); + + function getUsername(callback) { + pandora.$ui.accountDialog = pandora.ui.accountDialog('username').open(); + pandora.$ui.accountForm.bindEvent({ + submit: function(data) { + callback(data.values.username); + pandora.$ui.accountDialog.close(); + } + }); + return pandora.$ui.accountDialog; + } + + function onlogin(assertion, username) { + pandora.api.signin({ + assertion: assertion, + username: username + }, function(result) { + if (!result.data.errors && result.status.code == 200) { + pandora.signin(result.data); + } else if (!username + && result.data.errors && result.data.errors.username) { + getUsername(function(username) { + onlogin(assertion, username); + }); + } else { + onlogout(); + //fixme: show some error + } + }); + } + + function onlogout() { + pandora.UI.set({page: ''}); + pandora.api.signout({}, function(result) { + pandora.signout(result.data); + }); + } + +}; + +pandora.persona.signin = function() { + + navigator.id.request(); + +}; + +pandora.persona.signout = function() { + + navigator.id.logout(); + +}; + +pandora.ui.personaButton = function() { + + var isGuest = pandora.user.level == 'guest', + that = Ox.Element({ + tooltip: Ox._( + isGuest ? 'Click to sign with Persona' + : 'Click to open preferences or doubleclick to sign out' + ) + }) + .css({marginLeft: '3px'}) + .bindEvent({ + singleclick: function() { + isGuest + ? pandora.persona.signin() + : pandora.UI.set({page: 'preferences'}); + }, + doubleclick: function() { + pandora.UI.set({page: isGuest ? 'signin' : 'signout'}); + } + }); + + pandora.persona.init(); + $('