February 10th, 2012

Create an invite only website

Tutorial for: Django

This tutorial will go through the steps of creating an "invites" app for Django, which can be then used on any Django website for an invite-only situation.

This tutorial will not go through the set-up of a Django project, just the configuration of a Django app which is used for sending out website invitations. I am sure there is a way to tie the system into the Admin interface, however this will focus on not using the Admin interface and just a simple view.

$ ./manage start app invites

You will need to have the authentication framework enabled for this to work, every new Django project has this enabled by default.

Lets being with the models:

from django.db import models
from django.contrib.auth.models import User

class Invite(models.Model):
  user = models.OneToOneField(User)
  cookie = models.SlugField()
  token = models.SlugField()
  def __unicode__(self):
    return u"%s %s's invite" % (self.user.first_name, self.user.last_name)
  @models.permalink
  def get_absolute_url(self):
    return ('invites.views.confirm_invite', [self.token])

Here is what each field will do in the app:

  • user stores the associated user with the invite.
  • cookie stores a randomly generated string which is stored in the users browser.
  • token stores a randomly generated string which is used in the invite URL.

Once we have the model created, we will need a middleware to make these fields work:

from django.shortcuts import redirect
from invites.models import Invite

class InviteMiddleware(object):
  def process_request(self, req):
    if req.path == '/i.auth':
      return None
    if not req.user.is_authenticated():
      if 'token' in req.COOKIES:
        return redirect('invites.views.login_user')
    return None
  def process_response(self, req, resp):
    if req.user.is_authenticated():
      if req.user.is_staff:
        return resp
      if 'token' in req.COOKIES:
        token = req.COOKIES['token']
      else:
        invite = Invite.objects.get(user=req.user)
    	token = invite.cookie
      resp.set_cookie('token', token, max_age=1209600)
    return resp

This middleware controls the invited user's authentication to the website, this invitation system does not allow the usage of a login form. This is to refrain users from passing around their username and password to people which should otherwise not have it.

The process_request function checks the path(this is an old version of the code used during development and initial debugging, so the path is hardcoded in. In production, use django's reverse function). It checks the path as this will cause an infinite loop if not checked, we'll get to what this path does later. The next part checks to see if the user is already logged in. If the user is not logged in, we look for a token in the user's cookies, if it's there, then redirect the user to the special login view.

The process_response function checks if the user is logged in and if they have admin site access. If they have admin site access, do nothing. Invited users are not site staff members. Then we check to see if a token is in the user's cookies. If it's there, we put that into a variable. If there is no token in the user's cookies, we locate the user's invite in the database and set the variable to that. Finally we set the cookie for the user's invitation token. If the user is not authenticated, this entire process is not done.

from django import forms
from invites.models import Invite

class InviteForm(forms.Form):
  username = forms.CharField(max_length=20)
  first_name = forms.CharField(max_length=40)
  last_name = forms.CharField(max_length=40)
  email = forms.EmailField()

This is the form needed for when we need to invite a new user to the site, it's pretty easy to understand what is happening here.

The next section is the views, which uses this simple render function I built.

def render(req, tmpl="", data={}):
  return render_to_response(tmpl, data, context_instance=RequestContext(req))

There are a total of 3 views, which will need to be mapped to the urls.py:

def invite_user(req):
  if req.method == 'POST':
    form = InviteForm(req.POST)
    if form.is_valid():
      user = User.objects.create_user(form.cleaned_data['username'], form.cleaned_data['email'], '**')
      user.first_name = form.cleaned_data['first_name']
      user.last_name = form.cleaned_data['last_name']
      user.is_active = False
      user.save()
      invite = Invite.objects.create(user=user, cookie='ck-test', token='tk-test')
      send_mail('Subject', 'Link: http://10.160.61.78:8001%s' % invite.get_absolute_url(), 'From', [user.email])
      return redirect('/')
  else:
    form = InviteForm()
  return render(req, 'invite_form.html', {'form':form})

I stripped out my site specific content. This is an older version which I used for development and debugging. The current version of this code actually generates the cookie and token and fetches some data from the settings.py. I would recommend writing the required functions, if you plan on deploying a similar system.

This view displays the InviteForm, and creates a user and an invite from the data. Once the data has been created, an email is sent to the user who is being invited. I would recommend using the template system for email body generation. This is what I did in the production version.

def confirm_invite(req, token):
  invite = get_object_or_404(Invite, token=token)
  user = invite.user
  if user.is_active == True:
    return redirect('/')
  user.is_active = True
  user.save()
  auth_user = authenticate(username=user.username, password='**')
  if auth_user is None:
    return redirect('/')
  login(req, auth_user)
  return redirect('/')

The confirm_invite function is used when a user clicks on the invitation link in their email. This development version lacks some essential exception checks, which will need to be implemented in order to use usable in production. It checks to see if the user is already active, so an invitation link cannot be used twice, then it logs the user in. The middleware takes care of the cookie creation for future logins.

def login_user(req):
  if req.user.is_authenticated():
    return redirect('/')
  if 'token' in req.COOKIES:
    try:
      invite = Invite.objects.get(cookie=req.COOKIES['token'])
    except Invite.DoesNotExist:
	  resp = redirect('/')
	  resp.delete_cookie('token')
	  return resp
    user = authenticate(username=invite.user.username, password='**')
    if user is None:
      return redirect('/')
    login(req, user)
  return redirect('/')

The login_user function logs a user in using the cookie in their browser, normally this will only be used if their session expires. Some users clear out their session cookies upon a browser close. The middleware redirects to this view once it verifies that a cookie exists. The view also checks to see if the cookie exists as well. If an invite has been revoked, well the user shouldn't be logged back in.

There you have it, if there are enough requests, I will update this tutorial with the newer versions which I am using in production. It includes many bug fixes and optimizations which my development versions do not include.

About Me

My Photo
Names Kevin, hugely into UNIX technologies, not just Linux. I've dabbled with the demons, played with the Sun, and now with the Penguins.




Kevin Veroneau Consulting Services
Do you require the services of a Django contractor? Do you need both a website and hosting services? Perhaps I can help.

If you like what you read, please consider donating to help with hosting costs, and to fund future books to review.

Python Powered | © 2012-2014 Kevin Veroneau