From 48f7fd34a7f8c3da82a7b46f5f1174f3e52a6409 Mon Sep 17 00:00:00 2001 From: Adam Kelly Date: Mon, 1 Jun 2020 22:34:45 +0100 Subject: [PATCH] Allow account registration with invites. --- fedireads/forms.py | 4 +-- fedireads/migrations/0043_siteinvite.py | 24 ++++++++++++++++++ fedireads/models/__init__.py | 2 +- fedireads/models/site.py | 19 ++++++++++++++ fedireads/templates/invite.html | 33 +++++++++++++++++++++++++ fedireads/urls.py | 1 + fedireads/view_actions.py | 16 +++++++++++- fedireads/views.py | 16 ++++++++++++ 8 files changed, 111 insertions(+), 4 deletions(-) create mode 100644 fedireads/migrations/0043_siteinvite.py create mode 100644 fedireads/templates/invite.html diff --git a/fedireads/forms.py b/fedireads/forms.py index 060658c78..c7805c4cf 100644 --- a/fedireads/forms.py +++ b/fedireads/forms.py @@ -1,5 +1,5 @@ ''' usin django model forms ''' -from django.forms import ModelForm, PasswordInput +from django.forms import ModelForm, PasswordInput, HiddenInput from django import forms from fedireads import models @@ -21,7 +21,7 @@ class RegisterForm(ModelForm): fields = ['username', 'email', 'password'] help_texts = {f: None for f in fields} widgets = { - 'password': PasswordInput(), + 'password': PasswordInput() } diff --git a/fedireads/migrations/0043_siteinvite.py b/fedireads/migrations/0043_siteinvite.py new file mode 100644 index 000000000..30e071595 --- /dev/null +++ b/fedireads/migrations/0043_siteinvite.py @@ -0,0 +1,24 @@ +# Generated by Django 3.0.3 on 2020-06-01 21:31 + +from django.db import migrations, models +import fedireads.models.site + + +class Migration(migrations.Migration): + + dependencies = [ + ('fedireads', '0042_sitesettings'), + ] + + operations = [ + migrations.CreateModel( + name='SiteInvite', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('code', models.CharField(default=fedireads.models.site.new_invite_code, max_length=32)), + ('expiry', models.DateTimeField(blank=True, null=True)), + ('use_limit', models.IntegerField(blank=True, null=True)), + ('times_used', models.IntegerField(default=0)), + ], + ), + ] diff --git a/fedireads/models/__init__.py b/fedireads/models/__init__.py index deb54867e..2077d4a96 100644 --- a/fedireads/models/__init__.py +++ b/fedireads/models/__init__.py @@ -6,4 +6,4 @@ from .status import Favorite, Boost, Tag, Notification, ReadThrough from .user import User, UserFollows, UserFollowRequest, UserBlocks from .user import FederatedServer from .import_job import ImportJob, ImportItem -from .site import SiteSettings +from .site import SiteSettings, SiteInvite diff --git a/fedireads/models/site.py b/fedireads/models/site.py index 8068e4a13..116398452 100644 --- a/fedireads/models/site.py +++ b/fedireads/models/site.py @@ -1,4 +1,9 @@ +import base64 +import datetime + +from Crypto import Random from django.db import models + from fedireads.settings import DOMAIN class SiteSettings(models.Model): @@ -17,3 +22,17 @@ class SiteSettings(models.Model): default_settings = SiteSettings(id=1) default_settings.save() return default_settings + +def new_invite_code(): + return base64.b32encode(Random.get_random_bytes(5)).decode('ascii') + +class SiteInvite(models.Model): + code = models.CharField(max_length=32, default=new_invite_code) + expiry = models.DateTimeField(blank=True, null=True) + use_limit = models.IntegerField(blank=True, null=True) + times_used = models.IntegerField(default=0) + + def valid(self): + return ( + (self.expiry is None or self.expiry > datetime.datetime.now()) and + (self.use_limit is None or self.times_used < self.use_limit)) diff --git a/fedireads/templates/invite.html b/fedireads/templates/invite.html new file mode 100644 index 000000000..1147673da --- /dev/null +++ b/fedireads/templates/invite.html @@ -0,0 +1,33 @@ +{% extends 'layout.html' %} +{% block content %} + +
+

About {{ site_settings.name }}

+

+ {{ site_settings.instance_description }} +

+ +

+ + More about this site + +

+
+ +
+

Create an Account

+

+ With a BookWyrm account, you can track and share your reading activity with + friends here and on any other federated server, like Mastodon and PixelFed. +

+ +
+
+ {% csrf_token %} + {{ register_form.as_p }} + + +
+
+
+{% endblock %} diff --git a/fedireads/urls.py b/fedireads/urls.py index 14ae6f4b7..1723ae92f 100644 --- a/fedireads/urls.py +++ b/fedireads/urls.py @@ -35,6 +35,7 @@ urlpatterns = [ # ui views re_path(r'^login/?$', views.login_page), re_path(r'^about/?$', views.about_page), + re_path(r'^invite/(?P[A-Za-z0-9]+)/?$', views.invite_page), path('', views.home), re_path(r'^(?Phome|local|federated)/?$', views.home_tab), diff --git a/fedireads/view_actions.py b/fedireads/view_actions.py index fe9168712..6b7c2ac29 100644 --- a/fedireads/view_actions.py +++ b/fedireads/view_actions.py @@ -51,7 +51,17 @@ def register(request): return redirect('/login') if not models.SiteSettings.get().allow_registration: - raise PermissionDenied + invite_code = request.POST.get('invite_code') + + if not invite_code: + raise PermissionDenied + + try: + invite = models.SiteInvite.objects.get(code=invite_code) + except models.SiteInvite.DoesNotExist: + raise PermissionDenied + else: + invite = None form = forms.RegisterForm(request.POST) if not form.is_valid(): @@ -62,6 +72,10 @@ def register(request): password = form.data['password'] user = models.User.objects.create_user(username, email, password) + if invite: + invite.times_used += 1 + invite.save() + login(request, user) return redirect('/') diff --git a/fedireads/views.py b/fedireads/views.py index 46824ce78..af9452ef8 100644 --- a/fedireads/views.py +++ b/fedireads/views.py @@ -222,6 +222,22 @@ def about_page(request): } return TemplateResponse(request, 'about.html', data) +def invite_page(request, code): + ''' Handle invites. ''' + try: + invite = models.SiteInvite.objects.get(code=code) + if not invite.valid(): + raise PermissionDenied + except models.SiteInvite.DoesNotExist: + raise PermissionDenied + + data = { + 'site_settings': models.SiteSettings.get(), + 'register_form': forms.RegisterForm(), + 'invite': invite, + } + return TemplateResponse(request, 'invite.html', data) + @login_required def notifications_page(request):