mirror of
https://github.com/nottinghamtec/PyRIGS.git
synced 2026-03-05 11:38:25 +00:00
Compare commits
6 Commits
feature/di
...
feature/di
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
adc94820bb | ||
|
|
f3947d89ca | ||
|
|
0ad3aa7d3f | ||
|
|
01f754ad53 | ||
|
|
793f1d4e05 | ||
|
|
f5bf40bd9b |
@@ -1,5 +0,0 @@
|
|||||||
from django.contrib import admin
|
|
||||||
from models import AuthAttempt, DiscourseUserLink
|
|
||||||
|
|
||||||
admin.site.register(AuthAttempt)
|
|
||||||
admin.site.register(DiscourseUserLink)
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
from __future__ import unicode_literals
|
|
||||||
|
|
||||||
from django.db import models, migrations
|
|
||||||
import DiscourseAuth.models
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.CreateModel(
|
|
||||||
name='AuthAttempt',
|
|
||||||
fields=[
|
|
||||||
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
|
|
||||||
('nonce', models.CharField(default=DiscourseAuth.models.gen_nonce, max_length=25)),
|
|
||||||
('created', models.DateTimeField(auto_now=True)),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
]
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
from __future__ import unicode_literals
|
|
||||||
|
|
||||||
from django.db import models, migrations
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
('DiscourseAuth', '0001_initial'),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.RenameField(
|
|
||||||
model_name='authattempt',
|
|
||||||
old_name='created',
|
|
||||||
new_name='created_at',
|
|
||||||
),
|
|
||||||
]
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
from __future__ import unicode_literals
|
|
||||||
|
|
||||||
from django.db import models, migrations
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
('DiscourseAuth', '0002_auto_20170126_1513'),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.RenameField(
|
|
||||||
model_name='authattempt',
|
|
||||||
old_name='created_at',
|
|
||||||
new_name='created',
|
|
||||||
),
|
|
||||||
]
|
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
from __future__ import unicode_literals
|
|
||||||
|
|
||||||
from django.db import models, migrations
|
|
||||||
from django.conf import settings
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
|
||||||
('DiscourseAuth', '0003_auto_20170126_1621'),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.CreateModel(
|
|
||||||
name='DiscourseUserLink',
|
|
||||||
fields=[
|
|
||||||
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
|
|
||||||
('discourse_user_id', models.IntegerField()),
|
|
||||||
('django_user', models.OneToOneField(to=settings.AUTH_USER_MODEL)),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
]
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
from __future__ import unicode_literals
|
|
||||||
|
|
||||||
from django.db import models, migrations
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
('DiscourseAuth', '0004_discourseuserlink'),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.AlterField(
|
|
||||||
model_name='discourseuserlink',
|
|
||||||
name='discourse_user_id',
|
|
||||||
field=models.IntegerField(unique=True),
|
|
||||||
),
|
|
||||||
]
|
|
||||||
@@ -1,47 +0,0 @@
|
|||||||
import uuid
|
|
||||||
from django.db import models
|
|
||||||
|
|
||||||
from datetime import datetime, timedelta
|
|
||||||
|
|
||||||
from django.conf import settings
|
|
||||||
|
|
||||||
|
|
||||||
class AuthAttemptManager(models.Manager):
|
|
||||||
expiryMinutes = 10
|
|
||||||
|
|
||||||
def get_acceptable(self):
|
|
||||||
oldestAcceptableNonce = datetime.now() - timedelta(minutes=self.expiryMinutes)
|
|
||||||
return super(AuthAttemptManager, self).get_queryset().filter(created__gte=oldestAcceptableNonce)
|
|
||||||
|
|
||||||
def purge_unacceptable(self):
|
|
||||||
oldestAcceptableNonce = datetime.now() - timedelta(minutes=self.expiryMinutes)
|
|
||||||
super(AuthAttemptManager, self).get_queryset().filter(created__lt=oldestAcceptableNonce).delete()
|
|
||||||
|
|
||||||
|
|
||||||
def gen_nonce():
|
|
||||||
# return "THISISANONCETHATWEWILLREUSE"
|
|
||||||
return uuid.uuid4()
|
|
||||||
|
|
||||||
|
|
||||||
class AuthAttempt(models.Model):
|
|
||||||
nonce = models.CharField(max_length=25, default=gen_nonce)
|
|
||||||
created = models.DateTimeField(auto_now=True)
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
objects = AuthAttemptManager()
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return "AuthAttempt at " + str(self.created)
|
|
||||||
|
|
||||||
|
|
||||||
class DiscourseUserLink(models.Model):
|
|
||||||
django_user = models.OneToOneField(
|
|
||||||
settings.AUTH_USER_MODEL,
|
|
||||||
on_delete=models.CASCADE,
|
|
||||||
)
|
|
||||||
|
|
||||||
discourse_user_id = models.IntegerField(unique=True)
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return "{} - {}".format(self.discourse_user_id, str(self.django_user))
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
{% extends "base.html" %}
|
|
||||||
{% load i18n %}
|
|
||||||
|
|
||||||
{% block title %}{% trans "Associate User" %}{% endblock %}
|
|
||||||
|
|
||||||
{% block content %}
|
|
||||||
<form method="post" action="">
|
|
||||||
{% csrf_token %}
|
|
||||||
<p>You are currently logged in to django as "{{ djangouser }}". If this isn't you please log out</p>
|
|
||||||
<p>If you would like to link Discourse account "{{ discourseuser }}" to your django account, click below. This will remove any existing links.</p>
|
|
||||||
<input type="submit" value="{% trans 'Link my accounts' %}" />
|
|
||||||
</form>
|
|
||||||
|
|
||||||
{% endblock %}
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
{% extends "base.html" %}
|
|
||||||
{% load i18n %}
|
|
||||||
|
|
||||||
{% block title %}{% trans "Associate User" %}{% endblock %}
|
|
||||||
|
|
||||||
{% block content %}
|
|
||||||
{% if haslink %}
|
|
||||||
<form method="post" action="">
|
|
||||||
{% csrf_token %}
|
|
||||||
<p>Your account is currently linked to a Discourse account. To remove this link, click below. You will no longer be able to login using Discourse.</p>
|
|
||||||
<input type="submit" value="{% trans 'Un-Link my accounts' %}" />
|
|
||||||
</form>
|
|
||||||
{% else %}
|
|
||||||
<p>Your account is not currently linked to Discourse.</p>
|
|
||||||
{% endif %}
|
|
||||||
{% endblock %}
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
from django.conf.urls import patterns, url
|
|
||||||
|
|
||||||
from views import StartDiscourseAuth, ContinueDiscourseAuth, NewDiscourseUser, AssociateDiscourseUser, DisassociateDiscourseUser
|
|
||||||
|
|
||||||
urlpatterns = patterns('DiscourseAuth',
|
|
||||||
url(r'^start/$', StartDiscourseAuth.as_view(), name='start-auth'),
|
|
||||||
url(r'^continue/$', ContinueDiscourseAuth.as_view(), name='continue-auth'),
|
|
||||||
url(r'^new/$', NewDiscourseUser.as_view(), name='new-user'),
|
|
||||||
url(r'^associate/$', AssociateDiscourseUser.as_view(), name='associate-user'),
|
|
||||||
url(r'^disassociate/$', DisassociateDiscourseUser.as_view(), name='disassociate-user')
|
|
||||||
)
|
|
||||||
@@ -1,253 +0,0 @@
|
|||||||
|
|
||||||
from django.views.generic import View, FormView, TemplateView
|
|
||||||
|
|
||||||
from django.http import HttpResponseRedirect
|
|
||||||
from django.contrib.auth import login
|
|
||||||
from django.contrib.auth.backends import ModelBackend
|
|
||||||
from django.core.urlresolvers import reverse
|
|
||||||
|
|
||||||
from django.core.exceptions import PermissionDenied
|
|
||||||
|
|
||||||
from django.contrib.auth import get_user_model
|
|
||||||
import urllib
|
|
||||||
from hashlib import sha256
|
|
||||||
import hmac
|
|
||||||
from base64 import b64decode, b64encode
|
|
||||||
from django.db.models import Q
|
|
||||||
|
|
||||||
from django.conf import settings
|
|
||||||
|
|
||||||
from django.core.exceptions import ValidationError
|
|
||||||
|
|
||||||
from django.contrib import messages
|
|
||||||
|
|
||||||
from models import AuthAttempt, DiscourseUserLink
|
|
||||||
|
|
||||||
|
|
||||||
import time
|
|
||||||
from django.contrib.auth.decorators import login_required
|
|
||||||
from django.utils.decorators import method_decorator
|
|
||||||
|
|
||||||
from registration.forms import RegistrationForm
|
|
||||||
|
|
||||||
|
|
||||||
class StartDiscourseAuth(View):
|
|
||||||
http_method_names = ['get']
|
|
||||||
|
|
||||||
def get(self, request, *args, **kwargs):
|
|
||||||
# Where do we want to go once authentication is complete?
|
|
||||||
request.session['discourse_next'] = request.GET.get('next', "/")
|
|
||||||
|
|
||||||
# Generate random 'nonce'
|
|
||||||
attempt = AuthAttempt.objects.create()
|
|
||||||
nonce = attempt.nonce
|
|
||||||
|
|
||||||
# Where do we want discourse to send authenticated users?
|
|
||||||
redirect_uri = reverse('continue-auth')
|
|
||||||
redirect_uri = request.build_absolute_uri(redirect_uri)
|
|
||||||
|
|
||||||
# Data to sent to Discourse (payload)
|
|
||||||
data = {
|
|
||||||
'nonce': nonce,
|
|
||||||
'return_sso_url': redirect_uri
|
|
||||||
}
|
|
||||||
|
|
||||||
payload = urllib.urlencode(data)
|
|
||||||
b64payload = b64encode(payload.encode())
|
|
||||||
|
|
||||||
sig = hmac.new(settings.DISCOURSE_SECRET_KEY.encode(), b64payload, sha256).hexdigest()
|
|
||||||
|
|
||||||
return HttpResponseRedirect(settings.DISCOURSE_BASE_URL + '/session/sso_provider?' + urllib.urlencode({'sso': b64payload, 'sig': sig}))
|
|
||||||
|
|
||||||
|
|
||||||
class ContinueDiscourseAuth(View):
|
|
||||||
http_method_names = ['get']
|
|
||||||
|
|
||||||
def get(self, request, *args, **kwargs):
|
|
||||||
# Where do we want to go once authentication is complete?
|
|
||||||
nextUrl = request.session.get('discourse_next', "/")
|
|
||||||
|
|
||||||
rawSig = request.GET['sig']
|
|
||||||
rawSSO = request.GET['sso']
|
|
||||||
|
|
||||||
payload = urllib.unquote(rawSSO)
|
|
||||||
|
|
||||||
computed_sig = hmac.new(
|
|
||||||
settings.DISCOURSE_SECRET_KEY.encode(),
|
|
||||||
payload.encode(),
|
|
||||||
sha256
|
|
||||||
).hexdigest()
|
|
||||||
|
|
||||||
successful = hmac.compare_digest(computed_sig, rawSig.encode())
|
|
||||||
|
|
||||||
if not successful: # The signature doesn't match, not legit
|
|
||||||
raise ValidationError("Signature does not match, data has been manipulated")
|
|
||||||
|
|
||||||
decodedPayload = urllib.unquote_plus(b64decode(urllib.unquote(payload)).decode())
|
|
||||||
data = dict(data.split("=") for data in decodedPayload.split('&'))
|
|
||||||
|
|
||||||
# Get the nonce that's been returned by discourse
|
|
||||||
returnedNonce = data['nonce']
|
|
||||||
|
|
||||||
try: # See if it's in the database
|
|
||||||
storedAttempt = AuthAttempt.objects.get_acceptable().get(nonce=returnedNonce)
|
|
||||||
except AuthAttempt.DoesNotExist: # If it's not, this attempt is not valid
|
|
||||||
raise ValidationError("Nonce does not exist in database")
|
|
||||||
|
|
||||||
# Delete the nonce from the database - don't allow it to be reused
|
|
||||||
storedAttempt.delete()
|
|
||||||
# While we're at it, delete all the other expired attempts
|
|
||||||
AuthAttempt.objects.purge_unacceptable()
|
|
||||||
|
|
||||||
# If we've got this far, the attempt is valid, so let's load user information
|
|
||||||
external_id = int(data['external_id'])
|
|
||||||
|
|
||||||
# See if the user is already linked to a django user
|
|
||||||
try:
|
|
||||||
userLink = DiscourseUserLink.objects.get(discourse_user_id=external_id)
|
|
||||||
except DiscourseUserLink.DoesNotExist:
|
|
||||||
return self.linkNewUser(request, data)
|
|
||||||
|
|
||||||
# Load the user
|
|
||||||
user = userLink.django_user
|
|
||||||
# Slightly hacky way to login user without calling authenticate()
|
|
||||||
user.backend = "%s.%s" % (ModelBackend.__module__, ModelBackend.__name__)
|
|
||||||
# Login the user
|
|
||||||
login(request, user)
|
|
||||||
|
|
||||||
return HttpResponseRedirect(nextUrl)
|
|
||||||
|
|
||||||
def linkNewUser(self, request, data):
|
|
||||||
# Great, let's save the new user info in the session
|
|
||||||
request.session['discourse_data'] = data
|
|
||||||
request.session['discourse_started_registration'] = time.time()
|
|
||||||
|
|
||||||
if request.user is not None:
|
|
||||||
return HttpResponseRedirect(reverse('associate-user'))
|
|
||||||
else:
|
|
||||||
return HttpResponseRedirect(reverse('new-user'))
|
|
||||||
|
|
||||||
|
|
||||||
class SocialRegisterForm(RegistrationForm):
|
|
||||||
def __init__(self, *args, **kwargs):
|
|
||||||
super(SocialRegisterForm, self).__init__(*args, **kwargs)
|
|
||||||
self.fields.pop('password1')
|
|
||||||
self.fields.pop('password2')
|
|
||||||
|
|
||||||
self.fields['email'].widget.attrs['readonly'] = True
|
|
||||||
|
|
||||||
def clean_email(self):
|
|
||||||
initial = getattr(self, 'initial', None)
|
|
||||||
if(initial['email'] != self.cleaned_data['email']):
|
|
||||||
raise ValidationError("You cannot change the email")
|
|
||||||
|
|
||||||
return initial['email']
|
|
||||||
|
|
||||||
|
|
||||||
class AssociateDiscourseUser(TemplateView):
|
|
||||||
template_name = "DiscourseAuth/associate_user.html"
|
|
||||||
|
|
||||||
@method_decorator(login_required) # Require user is logged in for associating their account
|
|
||||||
def dispatch(self, request, *args, **kwargs):
|
|
||||||
self.data = self.request.session.get('discourse_data', None)
|
|
||||||
timeStarted = self.request.session.get('discourse_started_registration', 0)
|
|
||||||
|
|
||||||
max_reg_time = 20 * 60 # Seconds
|
|
||||||
|
|
||||||
if timeStarted < (time.time() - max_reg_time):
|
|
||||||
raise PermissionDenied('The Discourse authentication has expired, please try again')
|
|
||||||
|
|
||||||
if self.data is None:
|
|
||||||
raise PermissionDenied('Discourse authentication data is not present in this session')
|
|
||||||
|
|
||||||
return super(AssociateDiscourseUser, self).dispatch(request, *args, **kwargs)
|
|
||||||
|
|
||||||
def get(self, request, **kwargs):
|
|
||||||
return super(AssociateDiscourseUser, self).get(self, request, **kwargs)
|
|
||||||
|
|
||||||
def get_context_data(self, *args, **kwargs):
|
|
||||||
c = super(AssociateDiscourseUser, self).get_context_data()
|
|
||||||
c['discourseuser'] = self.data['username']
|
|
||||||
c['djangouser'] = self.request.user.username
|
|
||||||
|
|
||||||
return c
|
|
||||||
|
|
||||||
def post(self, request, **kwargs):
|
|
||||||
DiscourseUserLink.objects.filter(Q(django_user=request.user) | Q(discourse_user_id=self.data['external_id'])).delete()
|
|
||||||
DiscourseUserLink.objects.create(django_user=request.user, discourse_user_id=self.data['external_id'])
|
|
||||||
|
|
||||||
messages.success(self.request, 'Accounts successfully linked, you are now logged in.')
|
|
||||||
|
|
||||||
# Redirect them to the discourse login URL
|
|
||||||
nextUrl = "{}?next={}".format(reverse('start-auth'), request.session.get('discourse_next', "/"))
|
|
||||||
return HttpResponseRedirect(nextUrl)
|
|
||||||
|
|
||||||
|
|
||||||
class NewDiscourseUser(FormView):
|
|
||||||
template_name = 'registration/registration_form.html'
|
|
||||||
|
|
||||||
def dispatch(self, request, *args, **kwargs):
|
|
||||||
self.data = self.request.session.get('discourse_data', None)
|
|
||||||
timeStarted = self.request.session.get('discourse_started_registration', 0)
|
|
||||||
|
|
||||||
max_reg_time = 20 * 60 # Seconds
|
|
||||||
|
|
||||||
if timeStarted < (time.time() - max_reg_time):
|
|
||||||
raise PermissionDenied('The Discourse authentication has expired, please try again')
|
|
||||||
|
|
||||||
if self.data is None:
|
|
||||||
raise PermissionDenied('Discourse authentication data is not present in this session')
|
|
||||||
|
|
||||||
return super(NewDiscourseUser, self).dispatch(request, *args, **kwargs)
|
|
||||||
|
|
||||||
def get_initial(self):
|
|
||||||
data = self.data
|
|
||||||
initialForm = {
|
|
||||||
'username': data['username'],
|
|
||||||
'email': data['email'],
|
|
||||||
'name': data['name']
|
|
||||||
}
|
|
||||||
|
|
||||||
return initialForm
|
|
||||||
|
|
||||||
def get_form_class(self):
|
|
||||||
if settings.DISCOURSE_REGISTRATION_FORM:
|
|
||||||
return settings.DISCOURSE_REGISTRATION_FORM
|
|
||||||
else:
|
|
||||||
return SocialRegisterForm
|
|
||||||
|
|
||||||
def form_valid(self, form):
|
|
||||||
# This method is called when valid form data has been POSTed.
|
|
||||||
user = get_user_model().objects.create_user(**form.cleaned_data)
|
|
||||||
|
|
||||||
# Link the user to Discourse account
|
|
||||||
DiscourseUserLink.objects.filter(discourse_user_id=self.data['external_id']).delete()
|
|
||||||
DiscourseUserLink.objects.create(django_user=user, discourse_user_id=self.data['external_id'])
|
|
||||||
|
|
||||||
messages.success(self.request, 'Account successfully created, you are now logged in.')
|
|
||||||
|
|
||||||
# Redirect them to the discourse login URL
|
|
||||||
nextUrl = "{}?next={}".format(reverse('start-auth'), self.request.session.get('discourse_next', "/"))
|
|
||||||
return HttpResponseRedirect(nextUrl)
|
|
||||||
|
|
||||||
|
|
||||||
class DisassociateDiscourseUser(TemplateView):
|
|
||||||
template_name = "DiscourseAuth/disassociate_user.html"
|
|
||||||
|
|
||||||
@method_decorator(login_required) # Require user is logged in for associating their account
|
|
||||||
def dispatch(self, request, *args, **kwargs):
|
|
||||||
return super(DisassociateDiscourseUser, self).dispatch(request, *args, **kwargs)
|
|
||||||
|
|
||||||
def get_context_data(self, *args, **kwargs):
|
|
||||||
c = super(DisassociateDiscourseUser, self).get_context_data()
|
|
||||||
|
|
||||||
links = DiscourseUserLink.objects.filter(django_user=self.request.user)
|
|
||||||
|
|
||||||
c['haslink'] = links.count() > 0
|
|
||||||
|
|
||||||
return c
|
|
||||||
|
|
||||||
def post(self, request, **kwargs):
|
|
||||||
DiscourseUserLink.objects.filter(django_user=request.user).delete()
|
|
||||||
|
|
||||||
return self.get(self, request, **kwargs)
|
|
||||||
@@ -51,7 +51,6 @@ INSTALLED_APPS = (
|
|||||||
'django.contrib.messages',
|
'django.contrib.messages',
|
||||||
'django.contrib.staticfiles',
|
'django.contrib.staticfiles',
|
||||||
'RIGS',
|
'RIGS',
|
||||||
'DiscourseAuth',
|
|
||||||
|
|
||||||
'debug_toolbar',
|
'debug_toolbar',
|
||||||
'registration',
|
'registration',
|
||||||
@@ -59,6 +58,7 @@ INSTALLED_APPS = (
|
|||||||
'captcha',
|
'captcha',
|
||||||
'widget_tweaks',
|
'widget_tweaks',
|
||||||
'raven.contrib.django.raven_compat',
|
'raven.contrib.django.raven_compat',
|
||||||
|
'social.apps.django_app.default',
|
||||||
)
|
)
|
||||||
|
|
||||||
MIDDLEWARE_CLASSES = (
|
MIDDLEWARE_CLASSES = (
|
||||||
@@ -71,9 +71,34 @@ MIDDLEWARE_CLASSES = (
|
|||||||
'django.contrib.auth.middleware.AuthenticationMiddleware',
|
'django.contrib.auth.middleware.AuthenticationMiddleware',
|
||||||
'django.contrib.auth.middleware.SessionAuthenticationMiddleware',
|
'django.contrib.auth.middleware.SessionAuthenticationMiddleware',
|
||||||
'django.contrib.messages.middleware.MessageMiddleware',
|
'django.contrib.messages.middleware.MessageMiddleware',
|
||||||
# 'django.middleware.clickjacking.XFrameOptionsMiddleware',
|
'django.middleware.clickjacking.XFrameOptionsMiddleware',
|
||||||
)
|
)
|
||||||
|
|
||||||
|
AUTHENTICATION_BACKENDS = (
|
||||||
|
'RIGS.discourse.discourse.DiscourseAuth',
|
||||||
|
'django.contrib.auth.backends.ModelBackend',
|
||||||
|
)
|
||||||
|
|
||||||
|
SOCIAL_AUTH_PIPELINE = (
|
||||||
|
'social.pipeline.social_auth.social_details', # Load remote details
|
||||||
|
'social.pipeline.social_auth.social_uid', # Load remote ID
|
||||||
|
'social.pipeline.social_auth.auth_allowed', # Check not blacklisted
|
||||||
|
'social.pipeline.social_auth.social_user', # If already associated, login
|
||||||
|
'RIGS.discourse.pipeline.new_connection', # Choose a user account, much UI
|
||||||
|
'social.pipeline.social_auth.associate_user', # Associate the social auth with the user
|
||||||
|
'social.pipeline.social_auth.load_extra_data', # Save all the social info we have on this user
|
||||||
|
'RIGS.discourse.pipeline.update_avatar', # Load the avatar URL from the API, and save to user model
|
||||||
|
'social.pipeline.user.user_details', # Save any details that changed
|
||||||
|
)
|
||||||
|
|
||||||
|
DISCOURSE_HOST = os.environ.get('DISCOURSE_HOST') if os.environ.get('DISCOURSE_HOST') else 'http://localhost:4000'
|
||||||
|
DISCOURSE_SSO_SECRET = os.environ.get('DISCOURSE_SSO_SECRET') if os.environ.get('DISCOURSE_SSO_SECRET') else 'ABCDEFGHIJKLMNOP'
|
||||||
|
|
||||||
|
DISCOURSE_API_KEY = os.environ.get('DISCOURSE_API_KEY') if os.environ.get('DISCOURSE_HOST') else None
|
||||||
|
DISCOURSE_API_USER = os.environ.get('DISCOURSE_API_USER') if os.environ.get('DISCOURSE_HOST') else 'system'
|
||||||
|
|
||||||
|
REGISTRATION_OPEN = False # Disable built-in django registration - must register using forum
|
||||||
|
|
||||||
ROOT_URLCONF = 'PyRIGS.urls'
|
ROOT_URLCONF = 'PyRIGS.urls'
|
||||||
|
|
||||||
WSGI_APPLICATION = 'PyRIGS.wsgi.application'
|
WSGI_APPLICATION = 'PyRIGS.wsgi.application'
|
||||||
@@ -203,6 +228,8 @@ TEMPLATE_CONTEXT_PROCESSORS = (
|
|||||||
"django.core.context_processors.tz",
|
"django.core.context_processors.tz",
|
||||||
"django.core.context_processors.request",
|
"django.core.context_processors.request",
|
||||||
"django.contrib.messages.context_processors.messages",
|
"django.contrib.messages.context_processors.messages",
|
||||||
|
'social.apps.django_app.context_processors.backends',
|
||||||
|
'social.apps.django_app.context_processors.login_redirect',
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -222,7 +249,3 @@ TEMPLATE_DIRS = (
|
|||||||
USE_GRAVATAR=True
|
USE_GRAVATAR=True
|
||||||
|
|
||||||
TERMS_OF_HIRE_URL = "http://www.nottinghamtec.co.uk/terms.pdf"
|
TERMS_OF_HIRE_URL = "http://www.nottinghamtec.co.uk/terms.pdf"
|
||||||
|
|
||||||
|
|
||||||
DISCOURSE_SECRET_KEY = 'pB1f94Mfky1Y0eOrk2UjB1VqnAZ52P7v'
|
|
||||||
DISCOURSE_BASE_URL = 'https://forum.nottinghamtec.co.uk'
|
|
||||||
@@ -5,7 +5,6 @@ from django.conf import settings
|
|||||||
from registration.backends.default.views import RegistrationView
|
from registration.backends.default.views import RegistrationView
|
||||||
import RIGS
|
import RIGS
|
||||||
from RIGS import regbackend
|
from RIGS import regbackend
|
||||||
import DiscourseAuth.urls
|
|
||||||
|
|
||||||
urlpatterns = patterns('',
|
urlpatterns = patterns('',
|
||||||
# Examples:
|
# Examples:
|
||||||
@@ -19,7 +18,7 @@ urlpatterns = patterns('',
|
|||||||
url('^user/', include('registration.backends.default.urls')),
|
url('^user/', include('registration.backends.default.urls')),
|
||||||
|
|
||||||
url(r'^admin/', include(admin.site.urls)),
|
url(r'^admin/', include(admin.site.urls)),
|
||||||
url(r'^discourse-auth/', include(DiscourseAuth.urls)),
|
url('', include('social.apps.django_app.urls', namespace='social'))
|
||||||
)
|
)
|
||||||
|
|
||||||
if settings.DEBUG:
|
if settings.DEBUG:
|
||||||
|
|||||||
91
RIGS/discourse/discourse.py
Normal file
91
RIGS/discourse/discourse.py
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
from __future__ import unicode_literals
|
||||||
|
from social.backends.base import BaseAuth
|
||||||
|
from django.conf import settings
|
||||||
|
|
||||||
|
from .sso import DiscourseSSO
|
||||||
|
|
||||||
|
|
||||||
|
class DiscourseAssociation(object):
|
||||||
|
""" Use Association model to save the nonce by force. """
|
||||||
|
|
||||||
|
def __init__(self, handle, secret='', issued=0, lifetime=0, assoc_type=''):
|
||||||
|
self.handle = handle # as nonce
|
||||||
|
self.secret = secret.encode() # not use
|
||||||
|
self.issued = issued # not use
|
||||||
|
self.lifetime = lifetime # not use
|
||||||
|
self.assoc_type = assoc_type # as state
|
||||||
|
|
||||||
|
|
||||||
|
class DiscourseAuth(BaseAuth):
|
||||||
|
"""Discourse authentication backend"""
|
||||||
|
name = 'discourse'
|
||||||
|
secret = settings.DISCOURSE_SSO_SECRET
|
||||||
|
host = settings.DISCOURSE_HOST
|
||||||
|
|
||||||
|
EXTRA_DATA = [
|
||||||
|
('username', 'username'),
|
||||||
|
('email', 'email'),
|
||||||
|
('external_id', 'external_id')
|
||||||
|
]
|
||||||
|
|
||||||
|
sso = DiscourseSSO(secret)
|
||||||
|
|
||||||
|
def get_and_store_nonce(self, url):
|
||||||
|
# Create a nonce
|
||||||
|
nonce = self.strategy.random_string(64)
|
||||||
|
# Store the nonce
|
||||||
|
association = DiscourseAssociation(nonce)
|
||||||
|
self.strategy.storage.association.store(url, association)
|
||||||
|
return nonce
|
||||||
|
|
||||||
|
def get_nonce(self, nonce):
|
||||||
|
try:
|
||||||
|
return self.strategy.storage.association.get(
|
||||||
|
server_url=self.host,
|
||||||
|
handle=nonce
|
||||||
|
)[0]
|
||||||
|
except IndexError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def remove_nonce(self, nonce_id):
|
||||||
|
self.strategy.storage.association.remove([nonce_id])
|
||||||
|
|
||||||
|
def get_user_id(self, details, response):
|
||||||
|
"""Return current user id."""
|
||||||
|
|
||||||
|
return int(response['external_id'])
|
||||||
|
|
||||||
|
def get_user_details(self, response):
|
||||||
|
"""Return user basic information (id and email only)."""
|
||||||
|
|
||||||
|
return {'username': response['username'],
|
||||||
|
'email': response['email'],
|
||||||
|
'fullname': response['name'].replace('+', ' ') if 'name' in response else '',
|
||||||
|
'first_name': '',
|
||||||
|
'last_name': ''}
|
||||||
|
|
||||||
|
def auth_url(self):
|
||||||
|
"""Build and return complete URL."""
|
||||||
|
nonce = self.get_and_store_nonce(self.host)
|
||||||
|
|
||||||
|
return self.host + self.sso.build_login_URL(nonce, self.redirect_uri)
|
||||||
|
|
||||||
|
def auth_complete(self, *args, **kwargs):
|
||||||
|
"""Completes login process, must return user instance."""
|
||||||
|
|
||||||
|
try:
|
||||||
|
if not self.sso.validate(self.data['sso'], self.data['sig']):
|
||||||
|
raise Exception("Someone wants to hack us!")
|
||||||
|
except KeyError:
|
||||||
|
raise Exception("SSO Error, please try again")
|
||||||
|
|
||||||
|
nonce = self.sso.get_nonce(self.data['sso'])
|
||||||
|
nonce_obj = self.get_nonce(nonce)
|
||||||
|
if nonce_obj:
|
||||||
|
self.remove_nonce(nonce_obj.id)
|
||||||
|
else:
|
||||||
|
raise Exception("Nonce does not match!")
|
||||||
|
|
||||||
|
kwargs.update({'response': self.sso.get_data(
|
||||||
|
self.data['sso']), 'backend': self})
|
||||||
|
return self.strategy.authenticate(*args, **kwargs)
|
||||||
88
RIGS/discourse/pipeline.py
Normal file
88
RIGS/discourse/pipeline.py
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
from django.core.urlresolvers import reverse
|
||||||
|
from django.contrib.auth import REDIRECT_FIELD_NAME
|
||||||
|
from django.shortcuts import render_to_response
|
||||||
|
from django.core.exceptions import ValidationError
|
||||||
|
from django.conf import settings
|
||||||
|
|
||||||
|
import json
|
||||||
|
import requests
|
||||||
|
|
||||||
|
from social.pipeline.partial import partial
|
||||||
|
|
||||||
|
from RIGS.models import Profile
|
||||||
|
from RIGS import forms
|
||||||
|
|
||||||
|
|
||||||
|
class SocialRegisterForm(forms.ProfileRegistrationFormUniqueEmail):
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
super(SocialRegisterForm, self).__init__(*args, **kwargs)
|
||||||
|
self.fields.pop('password1')
|
||||||
|
self.fields.pop('password2')
|
||||||
|
self.fields.pop('captcha')
|
||||||
|
|
||||||
|
self.fields['email'].widget.attrs['readonly'] = True
|
||||||
|
|
||||||
|
def clean_email(self):
|
||||||
|
initial = getattr(self, 'initial', None)
|
||||||
|
if(initial['email'] != self.cleaned_data['email']):
|
||||||
|
raise ValidationError("You cannot change the email")
|
||||||
|
|
||||||
|
return initial['email']
|
||||||
|
|
||||||
|
|
||||||
|
@partial
|
||||||
|
def new_connection(backend, details, response, user=None, is_new=False, social=None, request=None, *args, **kwargs):
|
||||||
|
if social is not None:
|
||||||
|
return
|
||||||
|
|
||||||
|
data = backend.strategy.request_data()
|
||||||
|
|
||||||
|
if data.get('UseCurrentAccount') is not None:
|
||||||
|
return
|
||||||
|
|
||||||
|
alreadyLoggedIn = user is not None
|
||||||
|
|
||||||
|
context = {
|
||||||
|
'details': details,
|
||||||
|
'alreadyLoggedIn': alreadyLoggedIn,
|
||||||
|
'loggedInUser': user,
|
||||||
|
}
|
||||||
|
|
||||||
|
if not alreadyLoggedIn:
|
||||||
|
completeUrl = reverse('social:complete', kwargs={'backend': backend.name})
|
||||||
|
context['login_url'] = "{0}?{1}={2}".format(reverse('login'), REDIRECT_FIELD_NAME, completeUrl)
|
||||||
|
|
||||||
|
if data.get('username') is None:
|
||||||
|
form = SocialRegisterForm(initial=details)
|
||||||
|
else:
|
||||||
|
form = SocialRegisterForm(data, initial=details)
|
||||||
|
|
||||||
|
if form.is_valid():
|
||||||
|
new_user = Profile.objects.create_user(**form.cleaned_data)
|
||||||
|
return {'user': new_user}
|
||||||
|
|
||||||
|
context['form'] = form
|
||||||
|
|
||||||
|
return render_to_response('RIGS/social-associate.html', context)
|
||||||
|
|
||||||
|
|
||||||
|
def update_avatar(backend, details, response, user=None, social=None, *args, **kwargs):
|
||||||
|
host = settings.DISCOURSE_HOST
|
||||||
|
api_key = settings.DISCOURSE_API_KEY
|
||||||
|
api_user = settings.DISCOURSE_API_USER
|
||||||
|
if social is not None:
|
||||||
|
url = "{}/users/{}.json".format(host, details['username'])
|
||||||
|
params = {
|
||||||
|
'api_key': api_key,
|
||||||
|
'api_username': api_user
|
||||||
|
}
|
||||||
|
resp = requests.get(url=url, params=params)
|
||||||
|
extraData = json.loads(resp.text)
|
||||||
|
|
||||||
|
avatar_template = extraData['user']['avatar_template']
|
||||||
|
|
||||||
|
if avatar_template and user.avatar_template != avatar_template:
|
||||||
|
user.avatar_template = avatar_template
|
||||||
|
user.save()
|
||||||
|
|
||||||
|
return
|
||||||
46
RIGS/discourse/sso.py
Normal file
46
RIGS/discourse/sso.py
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
import urllib
|
||||||
|
from hashlib import sha256
|
||||||
|
import hmac
|
||||||
|
from base64 import b64decode, b64encode
|
||||||
|
|
||||||
|
|
||||||
|
class DiscourseSSO:
|
||||||
|
def __init__(self, secret_key):
|
||||||
|
self.__secret_key = secret_key
|
||||||
|
|
||||||
|
def validate(self, payload, sig):
|
||||||
|
payload = urllib.unquote(payload)
|
||||||
|
computed_sig = hmac.new(
|
||||||
|
self.__secret_key.encode(),
|
||||||
|
payload.encode(),
|
||||||
|
sha256
|
||||||
|
).hexdigest()
|
||||||
|
|
||||||
|
return hmac.compare_digest(unicode(computed_sig), sig)
|
||||||
|
|
||||||
|
def get_nonce(self, payload):
|
||||||
|
payload = b64decode(urllib.unquote(payload)).decode()
|
||||||
|
d = dict(nonce.split("=") for nonce in payload.split('&'))
|
||||||
|
|
||||||
|
if 'nonce' in d and d['nonce'] != '':
|
||||||
|
return d['nonce']
|
||||||
|
else:
|
||||||
|
raise Exception("Nonce could not be found in payload")
|
||||||
|
|
||||||
|
def get_data(self, payload):
|
||||||
|
payload = urllib.unquote(b64decode(urllib.unquote(payload)).decode())
|
||||||
|
d = dict(data.split("=") for data in payload.split('&'))
|
||||||
|
|
||||||
|
return d
|
||||||
|
|
||||||
|
def build_login_URL(self, nonce, redirect_uri):
|
||||||
|
data = {
|
||||||
|
'nonce': nonce,
|
||||||
|
'return_sso_url': redirect_uri
|
||||||
|
}
|
||||||
|
|
||||||
|
payload = urllib.urlencode(data)
|
||||||
|
payload = b64encode(payload.encode())
|
||||||
|
sig = hmac.new(self.__secret_key.encode(), payload, sha256).hexdigest()
|
||||||
|
|
||||||
|
return '/session/sso_provider?' + urllib.urlencode({'sso': payload, 'sig': sig})
|
||||||
@@ -122,7 +122,7 @@ class InvoiceArchive(generic.ListView):
|
|||||||
|
|
||||||
class InvoiceWaiting(generic.ListView):
|
class InvoiceWaiting(generic.ListView):
|
||||||
model = models.Event
|
model = models.Event
|
||||||
paginate_by = 25
|
# paginate_by = 25
|
||||||
template_name = 'RIGS/event_invoice.html'
|
template_name = 'RIGS/event_invoice.html'
|
||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
def get_context_data(self, **kwargs):
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ from django import forms
|
|||||||
from django.utils import formats
|
from django.utils import formats
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.core import serializers
|
from django.core import serializers
|
||||||
|
from django.core.exceptions import ValidationError
|
||||||
from django.contrib.auth.forms import UserCreationForm, UserChangeForm, AuthenticationForm, PasswordResetForm
|
from django.contrib.auth.forms import UserCreationForm, UserChangeForm, AuthenticationForm, PasswordResetForm
|
||||||
from registration.forms import RegistrationFormUniqueEmail
|
from registration.forms import RegistrationFormUniqueEmail
|
||||||
from captcha.fields import ReCaptchaField
|
from captcha.fields import ReCaptchaField
|
||||||
@@ -27,21 +28,15 @@ class ProfileRegistrationFormUniqueEmail(RegistrationFormUniqueEmail):
|
|||||||
raise forms.ValidationError("These initials are already in use. Please supply different initials.")
|
raise forms.ValidationError("These initials are already in use. Please supply different initials.")
|
||||||
return self.cleaned_data['initials']
|
return self.cleaned_data['initials']
|
||||||
|
|
||||||
|
def clean_first_name(self):
|
||||||
|
if self.cleaned_data["first_name"].strip() == '':
|
||||||
|
raise ValidationError("First name is required.")
|
||||||
|
return self.cleaned_data["first_name"]
|
||||||
|
|
||||||
class SocialRegisterForm(ProfileRegistrationFormUniqueEmail):
|
def clean_last_name(self):
|
||||||
def __init__(self, *args, **kwargs):
|
if self.cleaned_data["last_name"].strip() == '':
|
||||||
super(SocialRegisterForm, self).__init__(*args, **kwargs)
|
raise ValidationError("Last name is required.")
|
||||||
self.fields.pop('password1')
|
return self.cleaned_data["last_name"]
|
||||||
self.fields.pop('password2')
|
|
||||||
|
|
||||||
self.fields['email'].widget.attrs['readonly'] = True
|
|
||||||
|
|
||||||
def clean_email(self):
|
|
||||||
initial = getattr(self, 'initial', None)
|
|
||||||
if(initial['email'] != self.cleaned_data['email']):
|
|
||||||
raise ValidationError("You cannot change the email")
|
|
||||||
|
|
||||||
return initial['email']
|
|
||||||
|
|
||||||
|
|
||||||
# Login form
|
# Login form
|
||||||
|
|||||||
19
RIGS/migrations/0025_profile_avatar_template.py
Normal file
19
RIGS/migrations/0025_profile_avatar_template.py
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from django.db import models, migrations
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('RIGS', '0024_auto_20160229_2042'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='profile',
|
||||||
|
name='avatar_template',
|
||||||
|
field=models.CharField(max_length=255, null=True, editable=False, blank=True),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -22,6 +22,7 @@ class Profile(AbstractUser):
|
|||||||
initials = models.CharField(max_length=5, unique=True, null=True, blank=False)
|
initials = models.CharField(max_length=5, unique=True, null=True, blank=False)
|
||||||
phone = models.CharField(max_length=13, null=True, blank=True)
|
phone = models.CharField(max_length=13, null=True, blank=True)
|
||||||
api_key = models.CharField(max_length=40, blank=True, editable=False, null=True)
|
api_key = models.CharField(max_length=40, blank=True, editable=False, null=True)
|
||||||
|
avatar_template = models.CharField(max_length=255, blank=True, editable=False, null=True)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def make_api_key(cls):
|
def make_api_key(cls):
|
||||||
@@ -33,8 +34,13 @@ class Profile(AbstractUser):
|
|||||||
@property
|
@property
|
||||||
def profile_picture(self):
|
def profile_picture(self):
|
||||||
url = ""
|
url = ""
|
||||||
|
if settings.DISCOURSE_API_KEY is not None:
|
||||||
|
if self.avatar_template:
|
||||||
|
return settings.DISCOURSE_HOST+self.avatar_template.format(size=500)
|
||||||
|
|
||||||
if settings.USE_GRAVATAR or settings.USE_GRAVATAR is None:
|
if settings.USE_GRAVATAR or settings.USE_GRAVATAR is None:
|
||||||
url = "https://www.gravatar.com/avatar/" + hashlib.md5(self.email).hexdigest() + "?d=wavatar&s=500"
|
url = "https://www.gravatar.com/avatar/" + hashlib.md5(self.email).hexdigest() + "?d=wavatar&s=500"
|
||||||
|
|
||||||
return url
|
return url
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
|||||||
BIN
RIGS/static/imgs/forum-logo.gif
Normal file
BIN
RIGS/static/imgs/forum-logo.gif
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 300 KiB |
@@ -133,6 +133,20 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
</dd>
|
</dd>
|
||||||
</dl>
|
</dl>
|
||||||
|
|
||||||
|
<h4>Linked to {{object.social_auth.count}} Forum Account(s)</h4>
|
||||||
|
|
||||||
|
{% if object.social_auth.count > 0 %}
|
||||||
|
|
||||||
|
<a href="{% url 'unlink_forum' %}" class="btn btn-danger">
|
||||||
|
Unlink Forum Account(s) <span class="glyphicon glyphicon-pencil"></span>
|
||||||
|
</a>
|
||||||
|
{% else %}
|
||||||
|
<a href="{% url "social:begin" "discourse" %}" class="btn btn-success">
|
||||||
|
Link Forum Account <span class="glyphicon glyphicon-pencil"></span>
|
||||||
|
</a>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
55
RIGS/templates/RIGS/social-associate.html
Normal file
55
RIGS/templates/RIGS/social-associate.html
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
{% extends 'base.html' %}
|
||||||
|
{% load widget_tweaks %}
|
||||||
|
{% block title %}Associate{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="col-sm-10 col-sm-offset-1">
|
||||||
|
|
||||||
|
<div class="text-center">
|
||||||
|
<h1>R<small>ig</small> I<small>nformation</small> G<small>athering</small> S<small>ystem</small></h1>
|
||||||
|
</div>
|
||||||
|
<h2 class="text-center">Welcome <strong>{{details.username}}</strong></h2>
|
||||||
|
<h4 class="text-center">This is the first time you've visited RIGS with your forum account, so we need a few details to get you set up</h4>
|
||||||
|
<hr/>
|
||||||
|
|
||||||
|
|
||||||
|
{% if alreadyLoggedIn %}
|
||||||
|
<h2 class="text-center">You are logged in to RIGS as <strong>{{loggedInUser.username}}</strong></h2>
|
||||||
|
<div class="col-sm-8 col-sm-offset-2">
|
||||||
|
<form action="", method="post">{% csrf_token %}
|
||||||
|
<input type="hidden" name="UseCurrentAccount" value="1"/>
|
||||||
|
<button type="submit" class="btn btn-lg btn-primary center btn-block">Link Forum account to RIGS Account</button>
|
||||||
|
</form>
|
||||||
|
<a class="btn btn-lg btn-warning center btn-block" href="{% url 'logout' %}" role="button">Logout</a>
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
|
<h4 class="text-center"><a class="btn btn-info" href="{{login_url}}" role="button">I already have a RIGS account</a></h4>
|
||||||
|
{% if form.errors or supplement_form.errors %}
|
||||||
|
<div class="alert alert-danger">
|
||||||
|
{{form.errors}}
|
||||||
|
{{supplement_form.errors}}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<div class="col-sm-8 col-sm-offset-2">
|
||||||
|
<form action="" method="post" class="form-horizontal" role="form">{% csrf_token %}
|
||||||
|
{% for field in form %}
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="{{ field.id_for_label }}" class="control-label col-sm-4">{{ field.label }}</label>
|
||||||
|
<div class="controls col-sm-8">
|
||||||
|
{% render_field field class+="form-control" placeholder=field.label %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
<p><input type="submit" value="Register" class="btn btn-primary pull-right"></p>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
@@ -16,136 +16,136 @@ from selenium.webdriver.support.ui import WebDriverWait
|
|||||||
from RIGS import models
|
from RIGS import models
|
||||||
|
|
||||||
|
|
||||||
class UserRegistrationTest(LiveServerTestCase):
|
# class UserRegistrationTest(LiveServerTestCase):
|
||||||
|
|
||||||
def setUp(self):
|
# def setUp(self):
|
||||||
self.browser = webdriver.Firefox()
|
# self.browser = webdriver.Firefox()
|
||||||
self.browser.implicitly_wait(3) # Set implicit wait session wide
|
# self.browser.implicitly_wait(3) # Set implicit wait session wide
|
||||||
os.environ['RECAPTCHA_TESTING'] = 'True'
|
# os.environ['RECAPTCHA_TESTING'] = 'True'
|
||||||
|
|
||||||
def tearDown(self):
|
# def tearDown(self):
|
||||||
self.browser.quit()
|
# self.browser.quit()
|
||||||
os.environ['RECAPTCHA_TESTING'] = 'False'
|
# os.environ['RECAPTCHA_TESTING'] = 'False'
|
||||||
|
|
||||||
def test_registration(self):
|
# def test_registration(self):
|
||||||
# Navigate to the registration page
|
# # Navigate to the registration page
|
||||||
self.browser.get(self.live_server_url + '/user/register/')
|
# self.browser.get(self.live_server_url + '/user/register/')
|
||||||
title_text = self.browser.find_element_by_tag_name('h3').text
|
# title_text = self.browser.find_element_by_tag_name('h3').text
|
||||||
self.assertIn("User Registration", title_text)
|
# self.assertIn("User Registration", title_text)
|
||||||
|
|
||||||
# Check the form invites correctly
|
# # Check the form invites correctly
|
||||||
username = self.browser.find_element_by_id('id_username')
|
# username = self.browser.find_element_by_id('id_username')
|
||||||
self.assertEqual(username.get_attribute('placeholder'), 'Username')
|
# self.assertEqual(username.get_attribute('placeholder'), 'Username')
|
||||||
email = self.browser.find_element_by_id('id_email')
|
# email = self.browser.find_element_by_id('id_email')
|
||||||
self.assertEqual(email.get_attribute('placeholder'), 'E-mail')
|
# self.assertEqual(email.get_attribute('placeholder'), 'E-mail')
|
||||||
# If this is correct we don't need to test it later
|
# # If this is correct we don't need to test it later
|
||||||
self.assertEqual(email.get_attribute('type'), 'email')
|
# self.assertEqual(email.get_attribute('type'), 'email')
|
||||||
password1 = self.browser.find_element_by_id('id_password1')
|
# password1 = self.browser.find_element_by_id('id_password1')
|
||||||
self.assertEqual(password1.get_attribute('placeholder'), 'Password')
|
# self.assertEqual(password1.get_attribute('placeholder'), 'Password')
|
||||||
self.assertEqual(password1.get_attribute('type'), 'password')
|
# self.assertEqual(password1.get_attribute('type'), 'password')
|
||||||
password2 = self.browser.find_element_by_id('id_password2')
|
# password2 = self.browser.find_element_by_id('id_password2')
|
||||||
self.assertEqual(
|
# self.assertEqual(
|
||||||
password2.get_attribute('placeholder'), 'Password confirmation')
|
# password2.get_attribute('placeholder'), 'Password confirmation')
|
||||||
self.assertEqual(password2.get_attribute('type'), 'password')
|
# self.assertEqual(password2.get_attribute('type'), 'password')
|
||||||
first_name = self.browser.find_element_by_id('id_first_name')
|
# first_name = self.browser.find_element_by_id('id_first_name')
|
||||||
self.assertEqual(first_name.get_attribute('placeholder'), 'First name')
|
# self.assertEqual(first_name.get_attribute('placeholder'), 'First name')
|
||||||
last_name = self.browser.find_element_by_id('id_last_name')
|
# last_name = self.browser.find_element_by_id('id_last_name')
|
||||||
self.assertEqual(last_name.get_attribute('placeholder'), 'Last name')
|
# self.assertEqual(last_name.get_attribute('placeholder'), 'Last name')
|
||||||
initials = self.browser.find_element_by_id('id_initials')
|
# initials = self.browser.find_element_by_id('id_initials')
|
||||||
self.assertEqual(initials.get_attribute('placeholder'), 'Initials')
|
# self.assertEqual(initials.get_attribute('placeholder'), 'Initials')
|
||||||
phone = self.browser.find_element_by_id('id_phone')
|
# phone = self.browser.find_element_by_id('id_phone')
|
||||||
self.assertEqual(phone.get_attribute('placeholder'), 'Phone')
|
# self.assertEqual(phone.get_attribute('placeholder'), 'Phone')
|
||||||
|
|
||||||
# Fill the form out incorrectly
|
# # Fill the form out incorrectly
|
||||||
username.send_keys('TestUsername')
|
# username.send_keys('TestUsername')
|
||||||
email.send_keys('test@example.com')
|
# email.send_keys('test@example.com')
|
||||||
password1.send_keys('correcthorsebatterystaple')
|
# password1.send_keys('correcthorsebatterystaple')
|
||||||
# deliberate mistake
|
# # deliberate mistake
|
||||||
password2.send_keys('correcthorsebatterystapleerror')
|
# password2.send_keys('correcthorsebatterystapleerror')
|
||||||
first_name.send_keys('John')
|
# first_name.send_keys('John')
|
||||||
last_name.send_keys('Smith')
|
# last_name.send_keys('Smith')
|
||||||
initials.send_keys('JS')
|
# initials.send_keys('JS')
|
||||||
phone.send_keys('0123456789')
|
# phone.send_keys('0123456789')
|
||||||
self.browser.execute_script(
|
# self.browser.execute_script(
|
||||||
"return jQuery('#g-recaptcha-response').val('PASSED')")
|
# "return jQuery('#g-recaptcha-response').val('PASSED')")
|
||||||
|
|
||||||
# Submit incorrect form
|
# # Submit incorrect form
|
||||||
submit = self.browser.find_element_by_xpath("//input[@type='submit']")
|
# submit = self.browser.find_element_by_xpath("//input[@type='submit']")
|
||||||
submit.click()
|
# submit.click()
|
||||||
|
|
||||||
# Restablish error fields
|
# # Restablish error fields
|
||||||
password1 = self.browser.find_element_by_id('id_password1')
|
# password1 = self.browser.find_element_by_id('id_password1')
|
||||||
password2 = self.browser.find_element_by_id('id_password2')
|
# password2 = self.browser.find_element_by_id('id_password2')
|
||||||
|
|
||||||
# Read what the error is
|
# # Read what the error is
|
||||||
alert = self.browser.find_element_by_css_selector(
|
# alert = self.browser.find_element_by_css_selector(
|
||||||
'div.alert-danger').text
|
# 'div.alert-danger').text
|
||||||
self.assertIn("password fields didn't match", alert)
|
# self.assertIn("password fields didn't match", alert)
|
||||||
|
|
||||||
# Passwords should be empty
|
# # Passwords should be empty
|
||||||
self.assertEqual(password1.get_attribute('value'), '')
|
# self.assertEqual(password1.get_attribute('value'), '')
|
||||||
self.assertEqual(password2.get_attribute('value'), '')
|
# self.assertEqual(password2.get_attribute('value'), '')
|
||||||
|
|
||||||
# Correct error
|
# # Correct error
|
||||||
password1.send_keys('correcthorsebatterystaple')
|
# password1.send_keys('correcthorsebatterystaple')
|
||||||
password2.send_keys('correcthorsebatterystaple')
|
# password2.send_keys('correcthorsebatterystaple')
|
||||||
self.browser.execute_script(
|
# self.browser.execute_script(
|
||||||
"return jQuery('#g-recaptcha-response').val('PASSED')")
|
# "return jQuery('#g-recaptcha-response').val('PASSED')")
|
||||||
|
|
||||||
# Submit again
|
# # Submit again
|
||||||
password2.send_keys(Keys.ENTER)
|
# password2.send_keys(Keys.ENTER)
|
||||||
|
|
||||||
# Check we have a success message
|
# # Check we have a success message
|
||||||
alert = self.browser.find_element_by_css_selector(
|
# alert = self.browser.find_element_by_css_selector(
|
||||||
'div.alert-success').text
|
# 'div.alert-success').text
|
||||||
self.assertIn('register', alert)
|
# self.assertIn('register', alert)
|
||||||
self.assertIn('email', alert)
|
# self.assertIn('email', alert)
|
||||||
|
|
||||||
# Check Email
|
# # Check Email
|
||||||
self.assertEqual(len(mail.outbox), 1)
|
# self.assertEqual(len(mail.outbox), 1)
|
||||||
email = mail.outbox[0]
|
# email = mail.outbox[0]
|
||||||
self.assertIn('John Smith "JS" activation required', email.subject)
|
# self.assertIn('John Smith "JS" activation required', email.subject)
|
||||||
urls = re.findall(
|
# urls = re.findall(
|
||||||
'http[s]?://(?:[a-zA-Z]|[0-9]|[$-_@.&+]|[!*\(\),]|(?:%[0-9a-fA-F][0-9a-fA-F]))+', email.body)
|
# 'http[s]?://(?:[a-zA-Z]|[0-9]|[$-_@.&+]|[!*\(\),]|(?:%[0-9a-fA-F][0-9a-fA-F]))+', email.body)
|
||||||
self.assertEqual(len(urls), 1)
|
# self.assertEqual(len(urls), 1)
|
||||||
|
|
||||||
mail.outbox = [] # empty this for later
|
# mail.outbox = [] # empty this for later
|
||||||
|
|
||||||
# Follow link
|
# # Follow link
|
||||||
self.browser.get(urls[0]) # go to the first link
|
# self.browser.get(urls[0]) # go to the first link
|
||||||
|
|
||||||
# Complete registration
|
# # Complete registration
|
||||||
title_text = self.browser.find_element_by_tag_name('h2').text
|
# title_text = self.browser.find_element_by_tag_name('h2').text
|
||||||
self.assertIn('Complete', title_text)
|
# self.assertIn('Complete', title_text)
|
||||||
|
|
||||||
# Test login
|
# # Test login
|
||||||
self.browser.get(self.live_server_url + '/user/login')
|
# self.browser.get(self.live_server_url + '/user/login')
|
||||||
username = self.browser.find_element_by_id('id_username')
|
# username = self.browser.find_element_by_id('id_username')
|
||||||
self.assertEqual(username.get_attribute('placeholder'), 'Username')
|
# self.assertEqual(username.get_attribute('placeholder'), 'Username')
|
||||||
password = self.browser.find_element_by_id('id_password')
|
# password = self.browser.find_element_by_id('id_password')
|
||||||
self.assertEqual(password.get_attribute('placeholder'), 'Password')
|
# self.assertEqual(password.get_attribute('placeholder'), 'Password')
|
||||||
self.assertEqual(password.get_attribute('type'), 'password')
|
# self.assertEqual(password.get_attribute('type'), 'password')
|
||||||
|
|
||||||
username.send_keys('TestUsername')
|
# username.send_keys('TestUsername')
|
||||||
password.send_keys('correcthorsebatterystaple')
|
# password.send_keys('correcthorsebatterystaple')
|
||||||
self.browser.execute_script(
|
# self.browser.execute_script(
|
||||||
"return jQuery('#g-recaptcha-response').val('PASSED')")
|
# "return jQuery('#g-recaptcha-response').val('PASSED')")
|
||||||
password.send_keys(Keys.ENTER)
|
# password.send_keys(Keys.ENTER)
|
||||||
|
|
||||||
# Check we are logged in
|
# # Check we are logged in
|
||||||
udd = self.browser.find_element_by_class_name('navbar').text
|
# udd = self.browser.find_element_by_class_name('navbar').text
|
||||||
self.assertIn('Hi John', udd)
|
# self.assertIn('Hi John', udd)
|
||||||
|
|
||||||
# Check all the data actually got saved
|
# # Check all the data actually got saved
|
||||||
profileObject = models.Profile.objects.all()[0]
|
# profileObject = models.Profile.objects.all()[0]
|
||||||
self.assertEqual(profileObject.username, 'TestUsername')
|
# self.assertEqual(profileObject.username, 'TestUsername')
|
||||||
self.assertEqual(profileObject.first_name, 'John')
|
# self.assertEqual(profileObject.first_name, 'John')
|
||||||
self.assertEqual(profileObject.last_name, 'Smith')
|
# self.assertEqual(profileObject.last_name, 'Smith')
|
||||||
self.assertEqual(profileObject.initials, 'JS')
|
# self.assertEqual(profileObject.initials, 'JS')
|
||||||
self.assertEqual(profileObject.phone, '0123456789')
|
# self.assertEqual(profileObject.phone, '0123456789')
|
||||||
self.assertEqual(profileObject.email, 'test@example.com')
|
# self.assertEqual(profileObject.email, 'test@example.com')
|
||||||
|
|
||||||
# All is well
|
# # All is well
|
||||||
|
|
||||||
|
|
||||||
class EventTest(LiveServerTestCase):
|
class EventTest(LiveServerTestCase):
|
||||||
|
|||||||
@@ -153,6 +153,7 @@ urlpatterns = patterns('',
|
|||||||
url(r'^user/edit/$', login_required(views.ProfileUpdateSelf.as_view()),
|
url(r'^user/edit/$', login_required(views.ProfileUpdateSelf.as_view()),
|
||||||
name='profile_update_self'),
|
name='profile_update_self'),
|
||||||
url(r'^user/reset_api_key$', login_required(views.ResetApiKey.as_view(permanent=False)), name='reset_api_key'),
|
url(r'^user/reset_api_key$', login_required(views.ResetApiKey.as_view(permanent=False)), name='reset_api_key'),
|
||||||
|
url(r'^user/unlink_forum$', login_required(views.UnlinkForum.as_view(permanent=False)), name='unlink_forum'),
|
||||||
|
|
||||||
# ICS Calendar - API key authentication
|
# ICS Calendar - API key authentication
|
||||||
url(r'^ical/(?P<api_pk>\d+)/(?P<api_key>\w+)/rigs.ics$', api_key_required(ical.CalendarICS()), name="ics_calendar"),
|
url(r'^ical/(?P<api_pk>\d+)/(?P<api_key>\w+)/rigs.ics$', api_key_required(ical.CalendarICS()), name="ics_calendar"),
|
||||||
|
|||||||
@@ -382,3 +382,12 @@ class ResetApiKey(generic.RedirectView):
|
|||||||
self.request.user.save()
|
self.request.user.save()
|
||||||
|
|
||||||
return reverse_lazy('profile_detail')
|
return reverse_lazy('profile_detail')
|
||||||
|
|
||||||
|
class UnlinkForum(generic.RedirectView):
|
||||||
|
def get_redirect_url(self, *args, **kwargs):
|
||||||
|
for link in self.request.user.social_auth.all():
|
||||||
|
link.delete()
|
||||||
|
|
||||||
|
self.request.user.save()
|
||||||
|
|
||||||
|
return reverse_lazy('profile_detail')
|
||||||
|
|||||||
@@ -12,17 +12,23 @@ django-widget-tweaks==1.3
|
|||||||
gunicorn==19.3.0
|
gunicorn==19.3.0
|
||||||
icalendar==3.9.0
|
icalendar==3.9.0
|
||||||
lxml==3.4.4
|
lxml==3.4.4
|
||||||
|
oauthlib==2.0.0
|
||||||
Pillow==2.8.1
|
Pillow==2.8.1
|
||||||
psycopg2==2.6
|
psycopg2==2.6
|
||||||
Pygments==2.0.2
|
Pygments==2.0.2
|
||||||
|
PyJWT==1.4.2
|
||||||
PyPDF2==1.24
|
PyPDF2==1.24
|
||||||
python-dateutil==2.4.2
|
python-dateutil==2.4.2
|
||||||
|
python-openid==2.2.5
|
||||||
|
python-social-auth==0.2.21
|
||||||
pytz==2015.4
|
pytz==2015.4
|
||||||
raven==5.8.1
|
raven==5.8.1
|
||||||
reportlab==3.1.44
|
reportlab==3.1.44
|
||||||
|
requests==2.11.1
|
||||||
|
requests-oauthlib==0.7.0
|
||||||
selenium==2.53.6
|
selenium==2.53.6
|
||||||
simplejson==3.7.2
|
simplejson==3.7.2
|
||||||
six==1.9.0
|
six==1.10.0
|
||||||
sqlparse==0.1.15
|
sqlparse==0.1.15
|
||||||
static3==0.6.1
|
static3==0.6.1
|
||||||
svg2rlg==0.3
|
svg2rlg==0.3
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
{% extends 'base.html' %}
|
{% extends 'base.html' %}
|
||||||
|
{% load static from staticfiles %}
|
||||||
|
|
||||||
{% block title %}Login{% endblock %}
|
{% block title %}Login{% endblock %}
|
||||||
|
|
||||||
@@ -6,5 +7,43 @@
|
|||||||
<div class="text-center">
|
<div class="text-center">
|
||||||
<h1>R<small>ig</small> I<small>nformation</small> G<small>athering</small> S<small>ystem</small></h1>
|
<h1>R<small>ig</small> I<small>nformation</small> G<small>athering</small> S<small>ystem</small></h1>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<div class="panel-group">
|
||||||
|
{% url "social:complete" "discourse" as completeUrl %}
|
||||||
|
{% if not request.GET.next == completeUrl %}
|
||||||
|
<div class="panel panel-default">
|
||||||
|
<div class="panel-heading">
|
||||||
|
<h4 class="panel-title">
|
||||||
|
|
||||||
|
Login with TEC Forum
|
||||||
|
|
||||||
|
</h4>
|
||||||
|
</div>
|
||||||
|
<div id="forumLogin">
|
||||||
|
<div class="panel-body" style="text-align:center;">
|
||||||
|
<a class="btn btn-default" href="{% url "social:begin" "discourse" %}?next={{request.GET.next}}">
|
||||||
|
|
||||||
|
<h4>Login using</h4>
|
||||||
|
<img src="{% static "imgs/forum-logo.gif" %}" width=200></img>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
<div class="panel panel-default">
|
||||||
|
<div class="panel-heading">
|
||||||
|
<h4 class="panel-title">
|
||||||
|
Login with RIGS Credentials
|
||||||
|
</h4>
|
||||||
|
</div>
|
||||||
|
<div class="panel-body">
|
||||||
|
<div class="panel-body">
|
||||||
{% include 'registration/loginform.html' %}
|
{% include 'registration/loginform.html' %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
@@ -13,7 +13,7 @@
|
|||||||
{% render_field form.password class+="form-control" placeholder=form.password.label %}
|
{% render_field form.password class+="form-control" placeholder=form.password.label %}
|
||||||
</div>
|
</div>
|
||||||
<div class="text-right">
|
<div class="text-right">
|
||||||
<a href="{% url 'registration_register' %}" class="btn">Register</a>
|
{# <a href="{% url 'registration_register' %}" class="btn">Register</a> #}
|
||||||
<a href="{% url 'password_reset' %}" class="btn">Forgotten Password</a>
|
<a href="{% url 'password_reset' %}" class="btn">Forgotten Password</a>
|
||||||
<input type="submit" value="Login" class="btn btn-primary"/>
|
<input type="submit" value="Login" class="btn btn-primary"/>
|
||||||
<input type="hidden" name="next" value="{{ next }}"/>
|
<input type="hidden" name="next" value="{{ next }}"/>
|
||||||
|
|||||||
Reference in New Issue
Block a user