diff --git a/accounts/__init__.py b/accounts/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/accounts/admin.py b/accounts/admin.py new file mode 100644 index 0000000000000000000000000000000000000000..8c38f3f3dad51e4585f3984282c2a4bec5349c1e --- /dev/null +++ b/accounts/admin.py @@ -0,0 +1,3 @@ +from django.contrib import admin + +# Register your models here. diff --git a/accounts/apps.py b/accounts/apps.py new file mode 100644 index 0000000000000000000000000000000000000000..9b3fc5a44939430bfb326ca9a33f80e99b06b5be --- /dev/null +++ b/accounts/apps.py @@ -0,0 +1,5 @@ +from django.apps import AppConfig + + +class AccountsConfig(AppConfig): + name = 'accounts' diff --git a/accounts/authentication.py b/accounts/authentication.py new file mode 100644 index 0000000000000000000000000000000000000000..a274bb29ae42bdf4c8951bb5dd37b52abcbb07a5 --- /dev/null +++ b/accounts/authentication.py @@ -0,0 +1,23 @@ +import sys +from accounts.models import ListUser, Token + +class PasswordlessAuthenticationBackend(object): + + def authenticate(self, uid): + print('uid', uid, file=sys.stderr) + if not Token.objects.filter(uid=uid).exists(): + print('no token found', file=sys.stderr) + return None + token = Token.objects.get(uid=uid) + print('got token', file=sys.stderr) + try: + user = ListUser.objects.get(email=token.email) + print('got user', file=sys.stderr) + return user + except ListUser.DoesNotExist: + print('new user', file=sys.stderr) + return ListUser.objects.create(email=token.email) + + + def get_user(self, email): + return ListUser.objects.get(email=email) \ No newline at end of file diff --git a/accounts/migrations/0001_initial.py b/accounts/migrations/0001_initial.py new file mode 100644 index 0000000000000000000000000000000000000000..9bd3bc2a35051237df6b4a16de2482b6b1a0fd62 --- /dev/null +++ b/accounts/migrations/0001_initial.py @@ -0,0 +1,39 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.27 on 2019-12-23 10:46 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ('auth', '0008_alter_user_username_max_length'), + ] + + operations = [ + migrations.CreateModel( + name='ListUser', + fields=[ + ('password', models.CharField(max_length=128, verbose_name='password')), + ('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')), + ('is_superuser', models.BooleanField(default=False, help_text='Designates that this user has all permissions without explicitly assigning them.', verbose_name='superuser status')), + ('email', models.EmailField(max_length=254, primary_key=True, serialize=False)), + ('groups', models.ManyToManyField(blank=True, help_text='The groups this user belongs to. A user will get all permissions granted to each of their groups.', related_name='user_set', related_query_name='user', to='auth.Group', verbose_name='groups')), + ('user_permissions', models.ManyToManyField(blank=True, help_text='Specific permissions for this user.', related_name='user_set', related_query_name='user', to='auth.Permission', verbose_name='user permissions')), + ], + options={ + 'abstract': False, + }, + ), + migrations.CreateModel( + name='Token', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('email', models.EmailField(max_length=254)), + ('uid', models.CharField(max_length=255)), + ], + ), + ] diff --git a/accounts/migrations/__init__.py b/accounts/migrations/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/accounts/models.py b/accounts/models.py new file mode 100644 index 0000000000000000000000000000000000000000..c0d028aefb2b26257d692b756c6ecad9222226b4 --- /dev/null +++ b/accounts/models.py @@ -0,0 +1,31 @@ +from django.db import models +from django.contrib.auth.models import ( + AbstractBaseUser, BaseUserManager, PermissionsMixin +) + +class Token(models.Model): + email = models.EmailField() + uid = models.CharField(max_length=255) + +class ListUserManager(BaseUserManager): + + def create_user(self, email): + ListUser.objects.create(email=email) + + def create_superuser(self, email, password): + self.create_user(email) + +class ListUser(AbstractBaseUser, PermissionsMixin): + email = models.EmailField(primary_key=True) + USERNAME_FIELD = 'email' + #REQUIRED_FIELDS = ['email', 'height'] + + objects = ListUserManager() + + @property + def is_staff(self): + return self.email == 'harry.percival@example.com' + + @property + def is_active(self): + return True diff --git a/accounts/templates/login_email_sent.html b/accounts/templates/login_email_sent.html new file mode 100644 index 0000000000000000000000000000000000000000..2aa6e4ffd35016363271165018e8bbc5dfa387f9 --- /dev/null +++ b/accounts/templates/login_email_sent.html @@ -0,0 +1,7 @@ +<html> +<h1>Email sent</h1> + +<p>Check your email, you'll find a message with a link that will log you into +the site.</p> + +</html> \ No newline at end of file diff --git a/accounts/tests.py b/accounts/tests.py new file mode 100644 index 0000000000000000000000000000000000000000..7ce503c2dd97ba78597f6ff6e4393132753573f6 --- /dev/null +++ b/accounts/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/accounts/urls.py b/accounts/urls.py new file mode 100644 index 0000000000000000000000000000000000000000..10f0e241ced16c3e23ac731cc40a705c2d33ec0d --- /dev/null +++ b/accounts/urls.py @@ -0,0 +1,8 @@ +from django.conf.urls import url +from accounts import views + +urlpatterns = [ + url(r'^send_email$', views.send_login_email, name='send_login_email'), + url(r'^login$', views.login, name='login'), + url(r'^logout$', views.logout, name='logout'), +] \ No newline at end of file diff --git a/accounts/views.py b/accounts/views.py new file mode 100644 index 0000000000000000000000000000000000000000..b315e7cfcb53a64b2f811eca530c3cde3730c47e --- /dev/null +++ b/accounts/views.py @@ -0,0 +1,36 @@ +import uuid +import sys +from django.contrib.auth import authenticate + +from django.contrib.auth import login as auth_login, logout as auth_logout +from django.core.mail import send_mail +from django.shortcuts import redirect, render + +from accounts.models import Token + + +def send_login_email(request): + email = request.POST['email'] + uid = str(uuid.uuid4()) + Token.objects.create(email=email, uid=uid) + print('saving uid', uid, 'for email', email, file=sys.stderr) + url = request.build_absolute_uri(f'/accounts/login?uid={uid}') + send_mail( + 'Your login link for Superlists', + f'Use this link to log in:\n\n{url}', + 'noreply@superlists', + [email], + ) + return render(request, 'login_email_sent.html') + +def login(request): + print('login view', file=sys.stderr) + uid = request.GET.get('uid') + user = authenticate(uid=uid) + if user is not None: + auth_login(request, user) + return redirect('/') + +def logout(request): + auth_logout(request) + return redirect('/') \ No newline at end of file diff --git a/lists/templates/base.html b/lists/templates/base.html index ba8c158bd7b15596ba7b3881d929f5c2294c455b..907ba8fc8d9aae0cbf218b8abcb0a2d1912622f6 100644 --- a/lists/templates/base.html +++ b/lists/templates/base.html @@ -10,19 +10,20 @@ <body> <div class="container"> - <div class="row"> - <div class="col-md-6 col-md-offset-3 jumbotron"> - <div class="text-center"> + <div class="navbar"> <h1>{% block header_text %}{% endblock %}</h1> <h2>{% block header2_text %}{% endblock %}</h2> <h3>{% block header3_text %}{% endblock %}</h3> - <form method="POST" action="{% block form_action %}{% endblock %}"> - <input name="item_text" id="id_new_item" class="form-control input-lg" placeholder="Enter a to-do item" /> - {% csrf_token %} - </form> - </div> - </div> - </div> + {% if user.is_authenticated %} + <p>Logged in as {{ user.email }}</p> + <p><a id="id_logout" href="{% url 'logout' %}">Log out</a></p> + {% else %} + <form method="POST" action ="{% url 'send_login_email' %}"> + Enter email to log in: <input name="email" type="text" /> + {% csrf_token %} + </form> + {% endif %} + </div> <div class="row"> <div class="col-md-6 col-md-offset-3"> diff --git a/superlists/settings.py b/superlists/settings.py index 1d0a710fc535b391cc1c60cc140c15c3c02f7fa8..ed05199b9d3dbda93bd5fda7ed720b2a8964f07f 100644 --- a/superlists/settings.py +++ b/superlists/settings.py @@ -37,7 +37,13 @@ INSTALLED_APPS = [ 'django.contrib.sessions', 'django.contrib.messages', 'django.contrib.staticfiles', - 'lists' + 'lists', + 'accounts', +] + +AUTH_USER_MODEL = 'accounts.ListUser' +AUTHENTICATION_BACKENDS = [ + 'accounts.authentication.PasswordlessAuthenticationBackend', ] MIDDLEWARE = [ @@ -120,3 +126,27 @@ USE_TZ = True STATIC_URL = '/static/' STATIC_ROOT = os.path.join(BASE_DIR, 'static') + +EMAIL_HOST = 'smtp.gmail.com' +EMAIL_HOST_USER = 'ardianjatip@gmail.com' +EMAIL_HOST_PASSWORD = os.environ.get('EMAIL_PASSWORD') +EMAIL_PORT = 587 +EMAIL_USE_TLS = True + + +LOGGING = { + 'version': 1, + 'disable_existing_loggers': False, + 'handlers': { + 'console': { + 'level': 'DEBUG', + 'class': 'logging.StreamHandler', + }, + }, + 'loggers': { + 'django': { + 'handlers': ['console'], + }, + }, + 'root': {'level': 'INFO'}, +} \ No newline at end of file diff --git a/superlists/urls.py b/superlists/urls.py index cd49494da159a787734efb9636c417f5bd03c4ef..b04ad4405f803b8c336489dce79ba5bd35022456 100644 --- a/superlists/urls.py +++ b/superlists/urls.py @@ -16,8 +16,10 @@ Including another URLconf from django.conf.urls import url, include from lists import views as list_views from lists import urls as list_urls +from accounts import urls as accounts_urls urlpatterns = [ url(r'^$', list_views.home_page, name='home'), url(r'^lists/', include(list_urls)), -] + url(r'^accounts/', include(accounts_urls)), +] \ No newline at end of file