diff --git a/.gitignore b/.gitignore index abe3ca065ee1df7cca14712e7ced1a639a14e451..bd53f23e55a814e447a4e995561405f262e56583 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,7 @@ __pycache__ *.pyc /static env/ + +.env +geckodriver.log +.vscode \ No newline at end of file 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 index 11c400f966ed0c7094c30b8949c95fc23a11a24b..daddd5f632460ced66e4b7796cc785d14c14b6ef 100644 --- a/accounts/migrations/0001_initial.py +++ b/accounts/migrations/0001_initial.py @@ -1,4 +1,4 @@ -# Generated by Django 2.1.7 on 2019-11-13 14:32 +# Generated by Django 2.1.7 on 2019-11-13 16:08 from django.db import migrations, models 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..a80d96b4df2cbedeb5807af34745d682de04d9ed --- /dev/null +++ b/accounts/models.py @@ -0,0 +1,33 @@ +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/tests/test_models.py b/accounts/tests/test_models.py deleted file mode 100644 index ba08223cf3366fd4e9f4672b1d578015cf7c89c8..0000000000000000000000000000000000000000 --- a/accounts/tests/test_models.py +++ /dev/null @@ -1,25 +0,0 @@ -from django.test import TestCase -from django.contrib.auth import get_user_model - -from accounts.models import Token - -User = get_user_model() - - -class UserModelTest(TestCase): - - def test_user_is_valid_with_email_only(self): - user = User(email='a@b.com') - user.full_clean() # should not raise - - def test_email_is_primary_key(self): - user = User(email='a@b.com') - self.assertEqual(user.pk, 'a@b.com') - - -class TokenModelTest(TestCase): - - def test_links_user_with_auto_generated_uid(self): - token1 = Token.objects.create(email='a@b.com') - token2 = Token.objects.create(email='a@b.com') - self.assertNotEqual(token1.uid, token2.uid) 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..842a06b518c94c675eb4ab9eba362c69e579b010 --- /dev/null +++ b/accounts/views.py @@ -0,0 +1,35 @@ +import uuid +import sys +from django.shortcuts import render, redirect +from django.core.mail import send_mail +from django.contrib.auth import authenticate +from django.contrib.auth import login as auth_login, logout as auth_logout + +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/functional_tests/test_login.py b/functional_tests/test_login.py deleted file mode 100644 index efbfe834b9a22530e429b341156a5e16df7cb919..0000000000000000000000000000000000000000 --- a/functional_tests/test_login.py +++ /dev/null @@ -1,48 +0,0 @@ -# from django.core import mail -# from selenium.webdriver.common.keys import Keys -# import re - -# from .base import FunctionalTest - -# TEST_EMAIL = 'edith@example.com' -# SUBJECT = 'Your login link for Superlists' - - -# class LoginTest(FunctionalTest): - -# def test_can_get_email_link_to_log_in(self): -# # Edith goes to the awesome superlists site -# # and notices a "Log in" section in the navbar for the first time -# # It's telling her to enter her email address, so she does -# self.browser.get(self.live_server_url) -# self.browser.find_element_by_name('email').send_keys(TEST_EMAIL) -# self.browser.find_element_by_name('email').send_keys(Keys.ENTER) - -# # A message appears telling her an email has been sent -# self.wait_for(lambda: self.assertIn( -# 'Check your email', -# self.browser.find_element_by_tag_name('body').text -# )) - -# # She checks her email and finds a message -# email = mail.outbox[0] -# self.assertIn(TEST_EMAIL, email.to) -# self.assertEqual(email.subject, SUBJECT) - -# # It has a url link in it -# self.assertIn('Use this link to log in', email.body) -# url_search = re.search(r'http://.+/.+$', email.body) -# if not url_search: -# self.fail(f'Could not find url in email body:\n{email.body}') -# url = url_search.group(0) -# self.assertIn(self.live_server_url, url) - -# # she clicks it -# self.browser.get(url) - -# # she is logged in! -# self.wait_for( -# lambda: self.browser.find_element_by_link_text('Log out') -# ) -# navbar = self.browser.find_element_by_css_selector('.navbar') -# self.assertIn(TEST_EMAIL, navbar.text) diff --git a/lists/templates/base.html b/lists/templates/base.html index 7445080ba28b988574fd8360f3e25e8545fd67f8..68208b536f0de780d567e92ecd7dcd2b73e216ee 100644 --- a/lists/templates/base.html +++ b/lists/templates/base.html @@ -16,6 +16,18 @@ <div class="container"> + <div class="navbar"> + {% 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 jumbotron"> <div class="text-center"> diff --git a/requirements.txt b/requirements.txt index de63f73237f011b74f8c8aa3b54e3115918a774c..9e64a2a8afc93559ee473f7cbacd2b91667f8b16 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,4 +3,5 @@ Django==2.1.7 selenium==3.141.0 gunicorn==19.9.0 psycopg2 -dj_database_url \ No newline at end of file +dj_database_url +django_mutpy \ No newline at end of file diff --git a/superlists/settings.py b/superlists/settings.py index f7b8385bee49ff5d48d06352fb47ce0243b05c2b..6058613fc9ce259ae66e242d694122575a7127aa 100644 --- a/superlists/settings.py +++ b/superlists/settings.py @@ -42,9 +42,16 @@ INSTALLED_APPS = [ 'django.contrib.messages', 'django.contrib.staticfiles', 'django_mutpy', - 'lists' + 'lists', + 'accounts', ] +AUTH_USER_MODEL = 'accounts.ListUser' +AUTHENTICATION_BACKENDS = [ + 'accounts.authentication.PasswordlessAuthenticationBackend', +] + + MIDDLEWARE = [ 'django.middleware.security.SecurityMiddleware', 'django.contrib.sessions.middleware.SessionMiddleware', @@ -138,3 +145,26 @@ STATIC_URL = '/static/' # STATICFILES_DIRS = ( # os.path.join(BASE_DIR, 'static'), # ) + +EMAIL_HOST = 'smtp.gmail.com' +EMAIL_HOST_USER = 'empecempc@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 fc78a74071537b13d312a82edc94c8aefdd5a06e..2b708a7eaf08ae43c72a8b535c50b6d4866291e5 100644 --- a/superlists/urls.py +++ b/superlists/urls.py @@ -15,9 +15,12 @@ Including another URLconf """ from django.conf.urls import url, include from lists import views, urls as list_urls +from accounts import urls as accounts_urls + urlpatterns = [ # url(r'^admin/', admin.site.urls), url(r'^$', views.home_page, name='home'), url(r'^lists/', include(list_urls)), + url(r'^accounts/', include(accounts_urls)), ]