├── .gitignore ├── .vscode └── settings.json ├── LICENSE ├── Pipfile ├── Pipfile.lock ├── README.md ├── customuser ├── __init__.py ├── settings.py ├── urls.py └── wsgi.py ├── manage.py └── users ├── __init__.py ├── admin.py ├── apps.py ├── migrations ├── 0001_initial.py └── __init__.py ├── models.py ├── tests.py └── views.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | MANIFEST 27 | 28 | # PyInstaller 29 | # Usually these files are written by a python script from a template 30 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 31 | *.manifest 32 | *.spec 33 | 34 | # Installer logs 35 | pip-log.txt 36 | pip-delete-this-directory.txt 37 | 38 | # Unit test / coverage reports 39 | htmlcov/ 40 | .tox/ 41 | .coverage 42 | .coverage.* 43 | .cache 44 | nosetests.xml 45 | coverage.xml 46 | *.cover 47 | .hypothesis/ 48 | .pytest_cache/ 49 | 50 | # Translations 51 | *.mo 52 | *.pot 53 | 54 | # Django stuff: 55 | *.log 56 | local_settings.py 57 | db.sqlite3 58 | 59 | # Flask stuff: 60 | instance/ 61 | .webassets-cache 62 | 63 | # Scrapy stuff: 64 | .scrapy 65 | 66 | # Sphinx documentation 67 | docs/_build/ 68 | 69 | # PyBuilder 70 | target/ 71 | 72 | # Jupyter Notebook 73 | .ipynb_checkpoints 74 | 75 | # pyenv 76 | .python-version 77 | 78 | # celery beat schedule file 79 | celerybeat-schedule 80 | 81 | # SageMath parsed files 82 | *.sage.py 83 | 84 | # Environments 85 | .env 86 | .venv 87 | env/ 88 | venv/ 89 | ENV/ 90 | env.bak/ 91 | venv.bak/ 92 | 93 | # Spyder project settings 94 | .spyderproject 95 | .spyproject 96 | 97 | # Rope project settings 98 | .ropeproject 99 | 100 | # mkdocs documentation 101 | /site 102 | 103 | # mypy 104 | .mypy_cache/ 105 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "python.pythonPath": "/Users/snirp/.local/share/virtualenvs/braincards-sRN5H1Ai/bin/python" 3 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Roy Prins 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Pipfile: -------------------------------------------------------------------------------- 1 | [[source]] 2 | name = "pypi" 3 | url = "https://pypi.org/simple" 4 | verify_ssl = true 5 | 6 | [dev-packages] 7 | 8 | [packages] 9 | django = "*" 10 | django-allauth = "*" 11 | 12 | [requires] 13 | python_version = "3.7" 14 | -------------------------------------------------------------------------------- /Pipfile.lock: -------------------------------------------------------------------------------- 1 | { 2 | "_meta": { 3 | "hash": { 4 | "sha256": "32453cedc0bbdb343ed6c7bd78c2b7e0b2d52e64639c3ab8686b5be76b4d9d0d" 5 | }, 6 | "pipfile-spec": 6, 7 | "requires": { 8 | "python_version": "3.7" 9 | }, 10 | "sources": [ 11 | { 12 | "name": "pypi", 13 | "url": "https://pypi.org/simple", 14 | "verify_ssl": true 15 | } 16 | ] 17 | }, 18 | "default": { 19 | "certifi": { 20 | "hashes": [ 21 | "sha256:47f9c83ef4c0c621eaef743f133f09fa8a74a9b75f037e8624f83bd1b6626cb7", 22 | "sha256:993f830721089fef441cdfeb4b2c8c9df86f0c63239f06bd025a76a7daddb033" 23 | ], 24 | "version": "==2018.11.29" 25 | }, 26 | "chardet": { 27 | "hashes": [ 28 | "sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae", 29 | "sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691" 30 | ], 31 | "version": "==3.0.4" 32 | }, 33 | "defusedxml": { 34 | "hashes": [ 35 | "sha256:24d7f2f94f7f3cb6061acb215685e5125fbcdc40a857eff9de22518820b0a4f4", 36 | "sha256:702a91ade2968a82beb0db1e0766a6a273f33d4616a6ce8cde475d8e09853b20" 37 | ], 38 | "version": "==0.5.0" 39 | }, 40 | "django": { 41 | "hashes": [ 42 | "sha256:068d51054083d06ceb32ce02b7203f1854256047a0d58682677dd4f81bceabd7", 43 | "sha256:55409a056b27e6d1246f19ede41c6c610e4cab549c005b62cbeefabc6433356b" 44 | ], 45 | "index": "pypi", 46 | "version": "==2.1.4" 47 | }, 48 | "django-allauth": { 49 | "hashes": [ 50 | "sha256:45661b6fe308466154adf54461679fab6f0c6850b10c56dc322fbbb9d47e03d9" 51 | ], 52 | "index": "pypi", 53 | "version": "==0.38.0" 54 | }, 55 | "idna": { 56 | "hashes": [ 57 | "sha256:c357b3f628cf53ae2c4c05627ecc484553142ca23264e593d327bcde5e9c3407", 58 | "sha256:ea8b7f6188e6fa117537c3df7da9fc686d485087abf6ac197f9c46432f7e4a3c" 59 | ], 60 | "version": "==2.8" 61 | }, 62 | "oauthlib": { 63 | "hashes": [ 64 | "sha256:ac35665a61c1685c56336bda97d5eefa246f1202618a1d6f34fccb1bdd404162", 65 | "sha256:d883b36b21a6ad813953803edfa563b1b579d79ca758fe950d1bc9e8b326025b" 66 | ], 67 | "version": "==2.1.0" 68 | }, 69 | "python3-openid": { 70 | "hashes": [ 71 | "sha256:0086da6b6ef3161cfe50fb1ee5cceaf2cda1700019fda03c2c5c440ca6abe4fa", 72 | "sha256:628d365d687e12da12d02c6691170f4451db28d6d68d050007e4a40065868502" 73 | ], 74 | "version": "==3.1.0" 75 | }, 76 | "pytz": { 77 | "hashes": [ 78 | "sha256:31cb35c89bd7d333cd32c5f278fca91b523b0834369e757f4c5641ea252236ca", 79 | "sha256:8e0f8568c118d3077b46be7d654cc8167fa916092e28320cde048e54bfc9f1e6" 80 | ], 81 | "version": "==2018.7" 82 | }, 83 | "requests": { 84 | "hashes": [ 85 | "sha256:502a824f31acdacb3a35b6690b5fbf0bc41d63a24a45c4004352b0242707598e", 86 | "sha256:7bf2a778576d825600030a110f3c0e3e8edc51dfaafe1c146e39a2027784957b" 87 | ], 88 | "version": "==2.21.0" 89 | }, 90 | "requests-oauthlib": { 91 | "hashes": [ 92 | "sha256:8886bfec5ad7afb391ed5443b1f697c6f4ae98d0e5620839d8b4499c032ada3f", 93 | "sha256:e21232e2465808c0e892e0e4dbb8c2faafec16ac6dc067dd546e9b466f3deac8" 94 | ], 95 | "version": "==1.0.0" 96 | }, 97 | "urllib3": { 98 | "hashes": [ 99 | "sha256:61bf29cada3fc2fbefad4fdf059ea4bd1b4a86d2b6d15e1c7c0b582b9752fe39", 100 | "sha256:de9529817c93f27c8ccbfead6985011db27bd0ddfcdb2d86f3f663385c6a9c22" 101 | ], 102 | "version": "==1.24.1" 103 | } 104 | }, 105 | "develop": {} 106 | } 107 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # django-email-authentication 2 | Proof of concept of a custom Django user with email authentication 3 | -------------------------------------------------------------------------------- /customuser/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/snirp/django-email-authentication/843f81292379e34e2d509dc39cb34c0822b3a721/customuser/__init__.py -------------------------------------------------------------------------------- /customuser/settings.py: -------------------------------------------------------------------------------- 1 | """ 2 | Django settings for customuser project. 3 | 4 | Generated by 'django-admin startproject' using Django 2.1.4. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/2.1/topics/settings/ 8 | 9 | For the full list of settings and their values, see 10 | https://docs.djangoproject.com/en/2.1/ref/settings/ 11 | """ 12 | 13 | import os 14 | 15 | # Build paths inside the project like this: os.path.join(BASE_DIR, ...) 16 | BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) 17 | 18 | 19 | # Quick-start development settings - unsuitable for production 20 | # See https://docs.djangoproject.com/en/2.1/howto/deployment/checklist/ 21 | 22 | # SECURITY WARNING: keep the secret key used in production secret! 23 | SECRET_KEY = 'n%v9s8)gplq25r38o%6njr_(i$48+%3n7j@hpli%96bnvqla%2' 24 | 25 | # SECURITY WARNING: don't run with debug turned on in production! 26 | DEBUG = True 27 | 28 | ALLOWED_HOSTS = [] 29 | 30 | 31 | # Application definition 32 | 33 | INSTALLED_APPS = [ 34 | 'django.contrib.admin', 35 | 'django.contrib.auth', 36 | 'django.contrib.contenttypes', 37 | 'django.contrib.sessions', 38 | 'django.contrib.messages', 39 | 'django.contrib.staticfiles', 40 | 'django.contrib.sites', 41 | 'users', 42 | 'allauth', 43 | 'allauth.account', 44 | 'allauth.socialaccount', 45 | 'allauth.socialaccount.providers.google', 46 | ] 47 | 48 | MIDDLEWARE = [ 49 | 'django.middleware.security.SecurityMiddleware', 50 | 'django.contrib.sessions.middleware.SessionMiddleware', 51 | 'django.middleware.common.CommonMiddleware', 52 | 'django.middleware.csrf.CsrfViewMiddleware', 53 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 54 | 'django.contrib.messages.middleware.MessageMiddleware', 55 | 'django.middleware.clickjacking.XFrameOptionsMiddleware', 56 | ] 57 | 58 | AUTHENTICATION_BACKENDS = ( 59 | 'allauth.account.auth_backends.AuthenticationBackend', 60 | ) 61 | 62 | 63 | ROOT_URLCONF = 'customuser.urls' 64 | 65 | TEMPLATES = [ 66 | { 67 | 'BACKEND': 'django.template.backends.django.DjangoTemplates', 68 | 'DIRS': [], 69 | 'APP_DIRS': True, 70 | 'OPTIONS': { 71 | 'context_processors': [ 72 | 'django.template.context_processors.debug', 73 | 'django.template.context_processors.request', 74 | 'django.contrib.auth.context_processors.auth', 75 | 'django.contrib.messages.context_processors.messages', 76 | ], 77 | }, 78 | }, 79 | ] 80 | 81 | WSGI_APPLICATION = 'customuser.wsgi.application' 82 | 83 | 84 | # Database 85 | # https://docs.djangoproject.com/en/2.1/ref/settings/#databases 86 | 87 | DATABASES = { 88 | 'default': { 89 | 'ENGINE': 'django.db.backends.sqlite3', 90 | 'NAME': os.path.join(BASE_DIR, 'db.sqlite3'), 91 | } 92 | } 93 | 94 | 95 | # Password validation 96 | # https://docs.djangoproject.com/en/2.1/ref/settings/#auth-password-validators 97 | 98 | AUTH_PASSWORD_VALIDATORS = [ 99 | { 100 | 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', 101 | }, 102 | { 103 | 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', 104 | }, 105 | { 106 | 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', 107 | }, 108 | { 109 | 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', 110 | }, 111 | ] 112 | 113 | 114 | # Internationalization 115 | # https://docs.djangoproject.com/en/2.1/topics/i18n/ 116 | 117 | LANGUAGE_CODE = 'en-us' 118 | 119 | TIME_ZONE = 'UTC' 120 | 121 | USE_I18N = True 122 | 123 | USE_L10N = True 124 | 125 | USE_TZ = True 126 | 127 | 128 | # Static files (CSS, JavaScript, Images) 129 | # https://docs.djangoproject.com/en/2.1/howto/static-files/ 130 | 131 | STATIC_URL = '/static/' 132 | 133 | AUTH_USER_MODEL = 'users.User' 134 | 135 | SITE_ID = 1 136 | 137 | ACCOUNT_USER_MODEL_USERNAME_FIELD = None 138 | ACCOUNT_EMAIL_REQUIRED = True 139 | ACCOUNT_USERNAME_REQUIRED = False 140 | ACCOUNT_AUTHENTICATION_METHOD = 'email' -------------------------------------------------------------------------------- /customuser/urls.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | from django.urls import path, include 3 | 4 | urlpatterns = [ 5 | path('admin/', admin.site.urls), 6 | path('accounts/', include('allauth.urls')), 7 | ] 8 | -------------------------------------------------------------------------------- /customuser/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for customuser project. 3 | 4 | It exposes the WSGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/2.1/howto/deployment/wsgi/ 8 | """ 9 | 10 | import os 11 | 12 | from django.core.wsgi import get_wsgi_application 13 | 14 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'customuser.settings') 15 | 16 | application = get_wsgi_application() 17 | -------------------------------------------------------------------------------- /manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import os 3 | import sys 4 | 5 | if __name__ == '__main__': 6 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'customuser.settings') 7 | try: 8 | from django.core.management import execute_from_command_line 9 | except ImportError as exc: 10 | raise ImportError( 11 | "Couldn't import Django. Are you sure it's installed and " 12 | "available on your PYTHONPATH environment variable? Did you " 13 | "forget to activate a virtual environment?" 14 | ) from exc 15 | execute_from_command_line(sys.argv) 16 | -------------------------------------------------------------------------------- /users/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/snirp/django-email-authentication/843f81292379e34e2d509dc39cb34c0822b3a721/users/__init__.py -------------------------------------------------------------------------------- /users/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | from django.contrib.auth.admin import UserAdmin as BaseUserAdmin 3 | 4 | from .models import User 5 | 6 | 7 | class UserAdmin(BaseUserAdmin): 8 | fieldsets = ( 9 | (None, {'fields': ('email', 'password', 'name', 'last_login')}), 10 | ('Permissions', {'fields': ( 11 | 'is_active', 12 | 'is_staff', 13 | 'is_superuser', 14 | 'groups', 15 | 'user_permissions', 16 | )}), 17 | ) 18 | add_fieldsets = ( 19 | ( 20 | None, 21 | { 22 | 'classes': ('wide',), 23 | 'fields': ('email', 'password1', 'password2') 24 | } 25 | ), 26 | ) 27 | 28 | list_display = ('email', 'name', 'is_staff', 'last_login') 29 | list_filter = ('is_staff', 'is_superuser', 'is_active', 'groups') 30 | search_fields = ('email',) 31 | ordering = ('email',) 32 | filter_horizontal = ('groups', 'user_permissions',) 33 | 34 | 35 | admin.site.register(User, UserAdmin) -------------------------------------------------------------------------------- /users/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class UsersConfig(AppConfig): 5 | name = 'users' 6 | -------------------------------------------------------------------------------- /users/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.1.4 on 2018-12-15 10:20 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | initial = True 9 | 10 | dependencies = [ 11 | ('auth', '0009_alter_user_last_name_max_length'), 12 | ] 13 | 14 | operations = [ 15 | migrations.CreateModel( 16 | name='User', 17 | fields=[ 18 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 19 | ('password', models.CharField(max_length=128, verbose_name='password')), 20 | ('email', models.EmailField(max_length=254, unique=True)), 21 | ('name', models.CharField(blank=True, max_length=254, null=True)), 22 | ('is_staff', models.BooleanField(default=False)), 23 | ('is_superuser', models.BooleanField(default=False)), 24 | ('is_active', models.BooleanField(default=True)), 25 | ('last_login', models.DateTimeField(blank=True, null=True)), 26 | ('date_joined', models.DateTimeField(auto_now_add=True)), 27 | ('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')), 28 | ('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')), 29 | ], 30 | options={ 31 | 'abstract': False, 32 | }, 33 | ), 34 | ] 35 | -------------------------------------------------------------------------------- /users/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/snirp/django-email-authentication/843f81292379e34e2d509dc39cb34c0822b3a721/users/migrations/__init__.py -------------------------------------------------------------------------------- /users/models.py: -------------------------------------------------------------------------------- 1 | from django.contrib.auth.models import AbstractBaseUser, BaseUserManager, PermissionsMixin 2 | from django.db import models 3 | from django.utils import timezone 4 | 5 | 6 | class UserManager(BaseUserManager): 7 | 8 | def _create_user(self, email, password, is_staff, is_superuser, **extra_fields): 9 | if not email: 10 | raise ValueError('Users must have an email address') 11 | now = timezone.now() 12 | email = self.normalize_email(email) 13 | user = self.model( 14 | email=email, 15 | is_staff=is_staff, 16 | is_active=True, 17 | is_superuser=is_superuser, 18 | last_login=now, 19 | date_joined=now, 20 | **extra_fields 21 | ) 22 | user.set_password(password) 23 | user.save(using=self._db) 24 | return user 25 | 26 | def create_user(self, email, password, **extra_fields): 27 | return self._create_user(email, password, False, False, **extra_fields) 28 | 29 | def create_superuser(self, email, password, **extra_fields): 30 | user=self._create_user(email, password, True, True, **extra_fields) 31 | user.save(using=self._db) 32 | return user 33 | 34 | 35 | class User(AbstractBaseUser, PermissionsMixin): 36 | email = models.EmailField(max_length=254, unique=True) 37 | name = models.CharField(max_length=254, null=True, blank=True) 38 | is_staff = models.BooleanField(default=False) 39 | is_superuser = models.BooleanField(default=False) 40 | is_active = models.BooleanField(default=True) 41 | last_login = models.DateTimeField(null=True, blank=True) 42 | date_joined = models.DateTimeField(auto_now_add=True) 43 | 44 | 45 | USERNAME_FIELD = 'email' 46 | EMAIL_FIELD = 'email' 47 | REQUIRED_FIELDS = [] 48 | 49 | objects = UserManager() 50 | 51 | def get_absolute_url(self): 52 | return "/users/%i/" % (self.pk) 53 | -------------------------------------------------------------------------------- /users/tests.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | # Create your tests here. 4 | -------------------------------------------------------------------------------- /users/views.py: -------------------------------------------------------------------------------- 1 | from django.shortcuts import render, redirect 2 | --------------------------------------------------------------------------------