init accounts service
This commit is contained in:
commit
d0cdf6c38e
30 changed files with 993 additions and 0 deletions
8
.gitignore
vendored
Normal file
8
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
.python-version
|
||||||
|
.idea
|
||||||
|
media/*
|
||||||
|
delme*.py
|
||||||
|
Pipfile.lock
|
||||||
|
static
|
||||||
|
*.pyc
|
||||||
|
.env
|
||||||
23
Pipfile
Normal file
23
Pipfile
Normal file
|
|
@ -0,0 +1,23 @@
|
||||||
|
[[source]]
|
||||||
|
name = "pypi"
|
||||||
|
url = "https://pypi.org/simple"
|
||||||
|
verify_ssl = true
|
||||||
|
|
||||||
|
[dev-packages]
|
||||||
|
|
||||||
|
[packages]
|
||||||
|
django = "*"
|
||||||
|
django-oauth-toolkit = "*"
|
||||||
|
djangorestframework = "*"
|
||||||
|
markdown = "*"
|
||||||
|
django-filter = "*"
|
||||||
|
django-cors-middleware = "*"
|
||||||
|
pillow = "*"
|
||||||
|
django-model-utils = "*"
|
||||||
|
gunicorn = "*"
|
||||||
|
mysqlclient = "*"
|
||||||
|
django-mysql = "*"
|
||||||
|
python-decouple = "*"
|
||||||
|
|
||||||
|
[requires]
|
||||||
|
python_version = "3.7"
|
||||||
0
accounts/__init__.py
Normal file
0
accounts/__init__.py
Normal file
176
accounts/settings.py
Normal file
176
accounts/settings.py
Normal file
|
|
@ -0,0 +1,176 @@
|
||||||
|
"""
|
||||||
|
Django settings for accounts project.
|
||||||
|
|
||||||
|
Generated by 'django-admin startproject' using Django 2.2.
|
||||||
|
|
||||||
|
For more information on this file, see
|
||||||
|
https://docs.djangoproject.com/en/2.2/topics/settings/
|
||||||
|
|
||||||
|
For the full list of settings and their values, see
|
||||||
|
https://docs.djangoproject.com/en/2.2/ref/settings/
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
|
||||||
|
from decouple import config
|
||||||
|
|
||||||
|
# Build paths inside the project like this: os.path.join(BASE_DIR, ...)
|
||||||
|
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||||
|
|
||||||
|
|
||||||
|
# Quick-start development settings - unsuitable for production
|
||||||
|
# See https://docs.djangoproject.com/en/2.2/howto/deployment/checklist/
|
||||||
|
|
||||||
|
# SECURITY WARNING: keep the secret key used in production secret!
|
||||||
|
SECRET_KEY = 'tm9-y*5r=n028ii8iny#u1p3&mdjqf-#kj!z^fg%sbussj@r+o'
|
||||||
|
|
||||||
|
# SECURITY WARNING: don't run with debug turned on in production!
|
||||||
|
DEBUG = True
|
||||||
|
|
||||||
|
ALLOWED_HOSTS = ['*']
|
||||||
|
|
||||||
|
|
||||||
|
# Application definition
|
||||||
|
|
||||||
|
INSTALLED_APPS = [
|
||||||
|
'django.contrib.admin',
|
||||||
|
'django.contrib.auth',
|
||||||
|
'django.contrib.contenttypes',
|
||||||
|
'django.contrib.sessions',
|
||||||
|
'django.contrib.messages',
|
||||||
|
'django.contrib.staticfiles',
|
||||||
|
# 'dot_restrict_scopes',
|
||||||
|
'oauth2_provider',
|
||||||
|
'rest_framework',
|
||||||
|
'corsheaders',
|
||||||
|
'apps.users',
|
||||||
|
]
|
||||||
|
|
||||||
|
MIDDLEWARE = [
|
||||||
|
'django.middleware.security.SecurityMiddleware',
|
||||||
|
'django.contrib.sessions.middleware.SessionMiddleware',
|
||||||
|
'django.middleware.common.CommonMiddleware',
|
||||||
|
'django.middleware.csrf.CsrfViewMiddleware',
|
||||||
|
'django.contrib.auth.middleware.AuthenticationMiddleware',
|
||||||
|
'django.contrib.messages.middleware.MessageMiddleware',
|
||||||
|
'django.middleware.clickjacking.XFrameOptionsMiddleware',
|
||||||
|
'corsheaders.middleware.CorsMiddleware',
|
||||||
|
]
|
||||||
|
|
||||||
|
# Tell Django OAuth Toolkit to use the RestrictedApplication model
|
||||||
|
# OAUTH2_PROVIDER_APPLICATION_MODEL = 'dot_restrict_scopes.RestrictedApplication'
|
||||||
|
# OAUTH2_PROVIDER_ACCESS_TOKEN_MODEL = 'oauth2_provider.AccessToken'
|
||||||
|
# OAUTH2_PROVIDER_GRANT_MODEL = 'oauth2_provider.Grant'
|
||||||
|
# OAUTH2_PROVIDER_REFRESH_TOKEN_MODEL = 'oauth2_provider.RefreshToken'
|
||||||
|
|
||||||
|
|
||||||
|
OAUTH2_PROVIDER = {
|
||||||
|
# Tell Django OAuth Toolkit to use the scopes backend
|
||||||
|
# 'SCOPES_BACKEND_CLASS': 'dot_restrict_scopes.scopes.RestrictApplicationScopes',
|
||||||
|
# this is the list of available scopes
|
||||||
|
'SCOPES': {'read': 'Read scope',
|
||||||
|
'write': 'Write scope',
|
||||||
|
'groups': 'Access to your groups',
|
||||||
|
'external': 'Access resource from external'},
|
||||||
|
'OAUTH2_VALIDATOR_CLASS': 'utils.MultiGatewayOAuth2Validator'
|
||||||
|
}
|
||||||
|
|
||||||
|
# Tell dot-restrict-scopes which scopes backend it is wrapping
|
||||||
|
# NOTE: oauth2_provider.scopes.SettingsScopes is the default, so this is not
|
||||||
|
# strictly necessary if you want to use scopes from settings
|
||||||
|
DOT_RESTRICT_SCOPES = {
|
||||||
|
'WRAPPED_SCOPES_BACKEND_CLASS': 'oauth2_provider.scopes.SettingsScopes',
|
||||||
|
}
|
||||||
|
|
||||||
|
REST_FRAMEWORK = {
|
||||||
|
'DEFAULT_AUTHENTICATION_CLASSES': (
|
||||||
|
'oauth2_provider.contrib.rest_framework.OAuth2Authentication',
|
||||||
|
'rest_framework.authentication.SessionAuthentication',
|
||||||
|
),
|
||||||
|
'DEFAULT_PERMISSION_CLASSES': (
|
||||||
|
'rest_framework.permissions.IsAuthenticated',
|
||||||
|
),
|
||||||
|
'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.PageNumberPagination',
|
||||||
|
'PAGE_SIZE': 10
|
||||||
|
}
|
||||||
|
|
||||||
|
ROOT_URLCONF = 'accounts.urls'
|
||||||
|
|
||||||
|
TEMPLATES = [
|
||||||
|
{
|
||||||
|
'BACKEND': 'django.template.backends.django.DjangoTemplates',
|
||||||
|
'DIRS': [os.path.join(BASE_DIR, 'templates')],
|
||||||
|
'APP_DIRS': True,
|
||||||
|
'OPTIONS': {
|
||||||
|
'context_processors': [
|
||||||
|
'django.template.context_processors.debug',
|
||||||
|
'django.template.context_processors.request',
|
||||||
|
'django.contrib.auth.context_processors.auth',
|
||||||
|
'django.contrib.messages.context_processors.messages',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
WSGI_APPLICATION = 'accounts.wsgi.application'
|
||||||
|
|
||||||
|
|
||||||
|
# Database
|
||||||
|
# https://docs.djangoproject.com/en/2.2/ref/settings/#databases
|
||||||
|
|
||||||
|
DATABASES = {
|
||||||
|
'default': {
|
||||||
|
'ENGINE': 'django.db.backends.mysql',
|
||||||
|
'NAME': config('DB_NAME'),
|
||||||
|
'USER': config('DB_USER'),
|
||||||
|
'PASSWORD': config('DB_PASSWORD'),
|
||||||
|
'HOST': config('DB_HOST', '127.0.0.1'),
|
||||||
|
'PORT': config('DB_PORT', ''),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Password validation
|
||||||
|
# https://docs.djangoproject.com/en/2.2/ref/settings/#auth-password-validators
|
||||||
|
|
||||||
|
AUTH_PASSWORD_VALIDATORS = [
|
||||||
|
{
|
||||||
|
'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
# Internationalization
|
||||||
|
# https://docs.djangoproject.com/en/2.2/topics/i18n/
|
||||||
|
|
||||||
|
LANGUAGE_CODE = 'en-us'
|
||||||
|
|
||||||
|
TIME_ZONE = 'UTC'
|
||||||
|
|
||||||
|
USE_I18N = True
|
||||||
|
|
||||||
|
USE_L10N = True
|
||||||
|
|
||||||
|
USE_TZ = True
|
||||||
|
|
||||||
|
|
||||||
|
# Static files (CSS, JavaScript, Images)
|
||||||
|
# https://docs.djangoproject.com/en/2.2/howto/static-files/
|
||||||
|
|
||||||
|
|
||||||
|
AUTH_USER_MODEL = 'users.User'
|
||||||
|
CORS_ORIGIN_ALLOW_ALL = True
|
||||||
|
|
||||||
|
MEDIA_ROOT = os.path.join(BASE_DIR, 'media')
|
||||||
|
MEDIA_URL = "/media/"
|
||||||
|
|
||||||
|
STATIC_ROOT = os.path.join(BASE_DIR, 'static')
|
||||||
|
STATIC_URL = '/static/'
|
||||||
38
accounts/urls.py
Normal file
38
accounts/urls.py
Normal file
|
|
@ -0,0 +1,38 @@
|
||||||
|
"""accounts URL Configuration
|
||||||
|
|
||||||
|
The `urlpatterns` list routes URLs to views. For more information please see:
|
||||||
|
https://docs.djangoproject.com/en/2.2/topics/http/urls/
|
||||||
|
Examples:
|
||||||
|
Function views
|
||||||
|
1. Add an import: from my_app import views
|
||||||
|
2. Add a URL to urlpatterns: path('', views.home, name='home')
|
||||||
|
Class-based views
|
||||||
|
1. Add an import: from other_app.views import Home
|
||||||
|
2. Add a URL to urlpatterns: path('', Home.as_view(), name='home')
|
||||||
|
Including another URLconf
|
||||||
|
1. Import the include() function: from django.urls import include, path
|
||||||
|
2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
|
||||||
|
"""
|
||||||
|
from django.conf import settings
|
||||||
|
from django.conf.urls.static import static
|
||||||
|
from django.urls import path, include
|
||||||
|
from django.contrib import admin
|
||||||
|
|
||||||
|
from apps.users.views import UserListView, UserDetailView, AccountView, RequestOTPView, ChangePasswordView
|
||||||
|
from django.contrib.auth import urls as auth_urls
|
||||||
|
|
||||||
|
# Setup the URLs and include login URLs for the browsable API.
|
||||||
|
urlpatterns = [
|
||||||
|
path('admin/', admin.site.urls),
|
||||||
|
path('accounts/', include(auth_urls)),
|
||||||
|
path('oauth2/', include('oauth2_provider.urls', namespace='oauth2_provider')),
|
||||||
|
|
||||||
|
path('users/', UserListView.as_view()),
|
||||||
|
path('users/<code>/', UserDetailView.as_view(), name='user_detail'),
|
||||||
|
path('account/', AccountView.as_view(), name='account'),
|
||||||
|
path('request_otp/', RequestOTPView.as_view(), name='register'),
|
||||||
|
path('change_password/', ChangePasswordView.as_view(), name='change_password'),
|
||||||
|
]
|
||||||
|
|
||||||
|
urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
|
||||||
|
urlpatterns += static(settings.STATIC_URL, document_root=settings.STATIC_ROOT)
|
||||||
16
accounts/wsgi.py
Normal file
16
accounts/wsgi.py
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
"""
|
||||||
|
WSGI config for accounts project.
|
||||||
|
|
||||||
|
It exposes the WSGI callable as a module-level variable named ``application``.
|
||||||
|
|
||||||
|
For more information on this file, see
|
||||||
|
https://docs.djangoproject.com/en/2.2/howto/deployment/wsgi/
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
|
||||||
|
from django.core.wsgi import get_wsgi_application
|
||||||
|
|
||||||
|
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'accounts.settings')
|
||||||
|
|
||||||
|
application = get_wsgi_application()
|
||||||
0
apps/__init__.py
Normal file
0
apps/__init__.py
Normal file
0
apps/users/__init__.py
Normal file
0
apps/users/__init__.py
Normal file
32
apps/users/admin.py
Normal file
32
apps/users/admin.py
Normal file
|
|
@ -0,0 +1,32 @@
|
||||||
|
from django.contrib import admin
|
||||||
|
from .models import User
|
||||||
|
|
||||||
|
|
||||||
|
class UserAdmin(admin.ModelAdmin):
|
||||||
|
fields = [
|
||||||
|
'name',
|
||||||
|
'avatar',
|
||||||
|
'is_active',
|
||||||
|
'first_name',
|
||||||
|
'last_name',
|
||||||
|
'username',
|
||||||
|
'email',
|
||||||
|
'phone_number',
|
||||||
|
'code',
|
||||||
|
'password',
|
||||||
|
'otp',
|
||||||
|
'is_staff',
|
||||||
|
'is_superuser',
|
||||||
|
'groups',
|
||||||
|
'user_permissions',
|
||||||
|
'last_login',
|
||||||
|
'date_joined',
|
||||||
|
'last_update',
|
||||||
|
'otp_expire',
|
||||||
|
'otp_try',
|
||||||
|
'balance',
|
||||||
|
]
|
||||||
|
readonly_fields = ['last_update']
|
||||||
|
|
||||||
|
|
||||||
|
admin.site.register(User, UserAdmin)
|
||||||
5
apps/users/apps.py
Normal file
5
apps/users/apps.py
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
from django.apps import AppConfig
|
||||||
|
|
||||||
|
|
||||||
|
class UsersConfig(AppConfig):
|
||||||
|
name = 'users'
|
||||||
2
apps/users/constans.py
Normal file
2
apps/users/constans.py
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
MAX_OTP_TRY = 3
|
||||||
|
DEVELOPMENT_PHONE_NUMBERS = ['+989106853582']
|
||||||
56
apps/users/migrations/0001_initial.py
Normal file
56
apps/users/migrations/0001_initial.py
Normal file
|
|
@ -0,0 +1,56 @@
|
||||||
|
# Generated by Django 2.2.7 on 2019-11-14 08:47
|
||||||
|
|
||||||
|
import apps.users.models
|
||||||
|
import django.contrib.auth.validators
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.utils.timezone
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
initial = True
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('auth', '0011_update_proxy_permissions'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='User',
|
||||||
|
fields=[
|
||||||
|
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('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')),
|
||||||
|
('first_name', models.CharField(blank=True, max_length=30, verbose_name='first name')),
|
||||||
|
('last_name', models.CharField(blank=True, max_length=150, verbose_name='last name')),
|
||||||
|
('email', models.EmailField(blank=True, max_length=254, verbose_name='email address')),
|
||||||
|
('is_staff', models.BooleanField(default=False, help_text='Designates whether the user can log into this admin site.', verbose_name='staff status')),
|
||||||
|
('is_active', models.BooleanField(default=True, help_text='Designates whether this user should be treated as active. Unselect this instead of deleting accounts.', verbose_name='active')),
|
||||||
|
('date_joined', models.DateTimeField(default=django.utils.timezone.now, verbose_name='date joined')),
|
||||||
|
('name', models.CharField(blank=True, max_length=30, verbose_name='name')),
|
||||||
|
('username', models.CharField(blank=True, error_messages={'unique': 'A user with that username already exists.'}, help_text='Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.', max_length=150, null=True, unique=True, validators=[django.contrib.auth.validators.UnicodeUsernameValidator()], verbose_name='username')),
|
||||||
|
('phone_number', models.CharField(blank=True, max_length=30, null=True, unique=True, verbose_name='phone number')),
|
||||||
|
('otp', models.CharField(blank=True, max_length=6, null=True, verbose_name='otp')),
|
||||||
|
('otp_expire', models.DateTimeField(blank=True, null=True, verbose_name='otp expire')),
|
||||||
|
('otp_try', models.IntegerField(blank=True, default=0, null=True, verbose_name='otp try')),
|
||||||
|
('last_checkout_request', models.DateTimeField(blank=True, max_length=30, null=True, verbose_name='otp expire')),
|
||||||
|
('balance', models.IntegerField(default=0, verbose_name='balance')),
|
||||||
|
('iban', models.CharField(blank=True, max_length=30, null=True)),
|
||||||
|
('iban_verified', models.BooleanField(null=True)),
|
||||||
|
('avatar', models.ImageField(blank=True, null=True, upload_to='avatars')),
|
||||||
|
('code', models.IntegerField(db_index=True, null=True, unique=True, verbose_name='code')),
|
||||||
|
('last_update', models.DateTimeField(auto_now=True, max_length=30, null=True, verbose_name='last update')),
|
||||||
|
('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={
|
||||||
|
'verbose_name': 'user',
|
||||||
|
'verbose_name_plural': 'users',
|
||||||
|
'abstract': False,
|
||||||
|
},
|
||||||
|
managers=[
|
||||||
|
('objects', apps.users.models.UserManager()),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
]
|
||||||
0
apps/users/migrations/__init__.py
Normal file
0
apps/users/migrations/__init__.py
Normal file
165
apps/users/models.py
Normal file
165
apps/users/models.py
Normal file
|
|
@ -0,0 +1,165 @@
|
||||||
|
import random
|
||||||
|
from datetime import timedelta
|
||||||
|
|
||||||
|
from django.contrib.auth.base_user import BaseUserManager
|
||||||
|
from django.contrib.auth.models import AbstractUser
|
||||||
|
from django.contrib.auth.validators import UnicodeUsernameValidator
|
||||||
|
from django.db import models
|
||||||
|
from django.utils import timezone
|
||||||
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
from rest_framework.exceptions import APIException
|
||||||
|
|
||||||
|
from apps.users.constans import MAX_OTP_TRY
|
||||||
|
|
||||||
|
|
||||||
|
class UserManager(BaseUserManager):
|
||||||
|
use_in_migrations = True
|
||||||
|
|
||||||
|
def _create_user(self, phone_number=None, username=None, email=None, password=None, **extra_fields):
|
||||||
|
if not phone_number and not email:
|
||||||
|
raise ValueError('The given phone_number or email must be set')
|
||||||
|
|
||||||
|
email = self.normalize_email(email)
|
||||||
|
username = self.model.normalize_username(username)
|
||||||
|
# TODO: validate phone number
|
||||||
|
user = self.model(phone_number=phone_number, username=username, email=email, **extra_fields)
|
||||||
|
user.set_password(password)
|
||||||
|
user.set_otp(with_save=False)
|
||||||
|
user.set_code()
|
||||||
|
user.date_joined = timezone.now()
|
||||||
|
user.save(using=self._db)
|
||||||
|
|
||||||
|
return user
|
||||||
|
|
||||||
|
def create_user(self, phone_number=None, username=None, email=None, password=None, **extra_fields):
|
||||||
|
|
||||||
|
extra_fields.setdefault('is_staff', False)
|
||||||
|
extra_fields.setdefault('is_superuser', False)
|
||||||
|
|
||||||
|
# TODO create OTP
|
||||||
|
return self._create_user(phone_number=phone_number,
|
||||||
|
username=username,
|
||||||
|
email=email,
|
||||||
|
password=password,
|
||||||
|
**extra_fields)
|
||||||
|
|
||||||
|
def create_superuser(self, username, email, password, **extra_fields):
|
||||||
|
if not username:
|
||||||
|
raise ValueError('The given username must be set')
|
||||||
|
|
||||||
|
extra_fields.setdefault('is_staff', True)
|
||||||
|
extra_fields.setdefault('is_superuser', True)
|
||||||
|
|
||||||
|
if extra_fields.get('is_staff') is not True:
|
||||||
|
raise ValueError('Superuser must have is_staff=True.')
|
||||||
|
if extra_fields.get('is_superuser') is not True:
|
||||||
|
raise ValueError('Superuser must have is_superuser=True.')
|
||||||
|
|
||||||
|
return self._create_user(username=username, email=email, password=password, **extra_fields)
|
||||||
|
|
||||||
|
|
||||||
|
class User(AbstractUser):
|
||||||
|
# id
|
||||||
|
# first_name
|
||||||
|
# last_name
|
||||||
|
# email
|
||||||
|
# is_staff
|
||||||
|
# is_active
|
||||||
|
# password
|
||||||
|
|
||||||
|
name = models.CharField(_('name'), max_length=30, blank=True)
|
||||||
|
|
||||||
|
username_validator = UnicodeUsernameValidator()
|
||||||
|
username = models.CharField(
|
||||||
|
_('username'),
|
||||||
|
max_length=150,
|
||||||
|
unique=True,
|
||||||
|
help_text=_('Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.'),
|
||||||
|
validators=[username_validator],
|
||||||
|
error_messages={
|
||||||
|
'unique': _("A user with that username already exists."),
|
||||||
|
},
|
||||||
|
blank=True,
|
||||||
|
null=True
|
||||||
|
)
|
||||||
|
phone_number = models.CharField(_('phone number'), max_length=30, blank=True, null=True, unique=True)
|
||||||
|
otp = models.CharField(_('otp'), max_length=6, blank=True, null=True)
|
||||||
|
otp_expire = models.DateTimeField(_('otp expire'), blank=True, null=True)
|
||||||
|
otp_try = models.IntegerField(_('otp try'), blank=True, null=True, default=0)
|
||||||
|
|
||||||
|
last_checkout_request = models.DateTimeField(_('otp expire'), max_length=30, blank=True, null=True)
|
||||||
|
balance = models.IntegerField(_('balance'), default=0)
|
||||||
|
|
||||||
|
iban = models.CharField(null=True, max_length=30, blank=True)
|
||||||
|
iban_verified = models.BooleanField(null=True)
|
||||||
|
|
||||||
|
avatar = models.ImageField(upload_to='avatars', null=True, blank=True)
|
||||||
|
code = models.IntegerField(_('code'), unique=True, null=True, db_index=True) # unique random number
|
||||||
|
|
||||||
|
last_update = models.DateTimeField(_('last update'), max_length=30, blank=True, null=True, auto_now=True)
|
||||||
|
# last_login
|
||||||
|
# date_joined
|
||||||
|
|
||||||
|
objects = UserManager()
|
||||||
|
|
||||||
|
def set_otp(self, with_save=True):
|
||||||
|
self.otp = '12345'
|
||||||
|
# if self.phone_number in DEVELOPMENT_PHONE_NUMBERS:
|
||||||
|
# self.otp = '12345'
|
||||||
|
# else:
|
||||||
|
# self.otp = ''.join(random.choice('0123456789') for _ in range(5))
|
||||||
|
self.otp_expire = timezone.now() + timedelta(minutes=5)
|
||||||
|
self.otp_try = 0
|
||||||
|
if with_save:
|
||||||
|
self.save()
|
||||||
|
|
||||||
|
def set_code(self):
|
||||||
|
for code in random.sample(range(10000, 100000), 90000):
|
||||||
|
try:
|
||||||
|
code = str(code)
|
||||||
|
self.code = code
|
||||||
|
return self.save()
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
raise APIException('system error')
|
||||||
|
|
||||||
|
def otp_is_valid(self):
|
||||||
|
return bool(self.otp and timezone.now() <= self.otp_expire)
|
||||||
|
|
||||||
|
def check_otp(self, otp):
|
||||||
|
if self.otp_try <= MAX_OTP_TRY:
|
||||||
|
result = otp and self.otp == otp and self.otp_is_valid()
|
||||||
|
if result:
|
||||||
|
self.otp = None
|
||||||
|
self.date_joined = timezone.now()
|
||||||
|
else:
|
||||||
|
self.otp_try += 1
|
||||||
|
|
||||||
|
else:
|
||||||
|
self.otp = None
|
||||||
|
result = False
|
||||||
|
|
||||||
|
self.save()
|
||||||
|
return result
|
||||||
|
|
||||||
|
def check_auth(self, gateway, value):
|
||||||
|
result = False
|
||||||
|
if gateway == 'otp':
|
||||||
|
result = self.check_otp(value)
|
||||||
|
|
||||||
|
elif gateway == 'password':
|
||||||
|
result = self.check_password(value)
|
||||||
|
|
||||||
|
if result:
|
||||||
|
self.last_login = timezone.now()
|
||||||
|
self.save()
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
def send_otp(self):
|
||||||
|
# TODO: some action to sent OTP
|
||||||
|
return True
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return f"{self.code} - {self.username}"
|
||||||
82
apps/users/serializers.py
Normal file
82
apps/users/serializers.py
Normal file
|
|
@ -0,0 +1,82 @@
|
||||||
|
from django.contrib.auth.models import Group
|
||||||
|
from rest_framework import serializers
|
||||||
|
|
||||||
|
from apps.users.models import User
|
||||||
|
from django.utils import timezone
|
||||||
|
|
||||||
|
# public data
|
||||||
|
class UserSerializer(serializers.ModelSerializer):
|
||||||
|
class Meta:
|
||||||
|
model = User
|
||||||
|
fields = ('avatar', 'name', 'username', 'email', "first_name", "last_name", "code")
|
||||||
|
read_only_fields = ['avatar', 'name', 'username', 'email', 'first_name', 'last_name']
|
||||||
|
|
||||||
|
def to_internal_value(self, data):
|
||||||
|
code = data.get('code')
|
||||||
|
if code:
|
||||||
|
return User.objects.get(code=code)
|
||||||
|
|
||||||
|
|
||||||
|
class AccountSerializer(serializers.ModelSerializer):
|
||||||
|
class Meta:
|
||||||
|
model = User
|
||||||
|
fields = (
|
||||||
|
"code",
|
||||||
|
'username',
|
||||||
|
'email',
|
||||||
|
'phone_number',
|
||||||
|
"first_name",
|
||||||
|
"last_name",
|
||||||
|
'name',
|
||||||
|
'balance',
|
||||||
|
'avatar',
|
||||||
|
'iban',
|
||||||
|
'iban_verified',
|
||||||
|
)
|
||||||
|
read_only_fields = ['code', 'balance', 'email', 'phone_number', 'iban_verified']
|
||||||
|
|
||||||
|
|
||||||
|
class RequestOTPSerializer(serializers.ModelSerializer):
|
||||||
|
phone_number = serializers.CharField(required=True)
|
||||||
|
ttl = serializers.SerializerMethodField()
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = User
|
||||||
|
fields = (
|
||||||
|
'phone_number',
|
||||||
|
'otp_expire',
|
||||||
|
'ttl'
|
||||||
|
)
|
||||||
|
|
||||||
|
read_only_fields = ['otp_expire', 'ttl']
|
||||||
|
write_only_fields = []
|
||||||
|
|
||||||
|
def save(self, **kwargs):
|
||||||
|
validated_data = self.validated_data
|
||||||
|
user = User.objects.filter(phone_number=validated_data['phone_number']).first()
|
||||||
|
if not user:
|
||||||
|
user = User.objects.create_user(**validated_data)
|
||||||
|
|
||||||
|
if not user.otp_is_valid():
|
||||||
|
user.set_otp(with_save=True)
|
||||||
|
|
||||||
|
user.send_otp()
|
||||||
|
|
||||||
|
self.instance = user
|
||||||
|
return user
|
||||||
|
|
||||||
|
def get_ttl(self, obj: User):
|
||||||
|
if obj.otp_expire > timezone.now():
|
||||||
|
ttl = obj.otp_expire - timezone.now()
|
||||||
|
result = ttl.total_seconds()
|
||||||
|
|
||||||
|
else:
|
||||||
|
result = 0
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
class ChangePasswordSerializer(serializers.Serializer):
|
||||||
|
old_password = serializers.CharField(required=True)
|
||||||
|
old_password_gateway = serializers.CharField(default='password')
|
||||||
|
new_password = serializers.CharField(required=True)
|
||||||
3
apps/users/tests.py
Normal file
3
apps/users/tests.py
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
from django.test import TestCase
|
||||||
|
|
||||||
|
# Create your tests here.
|
||||||
69
apps/users/views.py
Normal file
69
apps/users/views.py
Normal file
|
|
@ -0,0 +1,69 @@
|
||||||
|
from django.utils import timezone
|
||||||
|
from oauth2_provider.contrib.rest_framework import TokenHasReadWriteScope, TokenHasScope, IsAuthenticatedOrTokenHasScope
|
||||||
|
from rest_framework import generics, permissions, status
|
||||||
|
from rest_framework.response import Response
|
||||||
|
|
||||||
|
from apps.users.models import User
|
||||||
|
from apps.users.serializers import UserSerializer, AccountSerializer, RequestOTPSerializer, ChangePasswordSerializer
|
||||||
|
|
||||||
|
|
||||||
|
class UserListView(generics.ListAPIView):
|
||||||
|
permission_classes = [permissions.IsAuthenticated, IsAuthenticatedOrTokenHasScope]
|
||||||
|
queryset = User.objects.all()
|
||||||
|
serializer_class = UserSerializer
|
||||||
|
required_scopes = []
|
||||||
|
|
||||||
|
|
||||||
|
class UserDetailView(generics.RetrieveAPIView):
|
||||||
|
permission_classes = [permissions.IsAuthenticated, IsAuthenticatedOrTokenHasScope]
|
||||||
|
queryset = User.objects.all()
|
||||||
|
serializer_class = UserSerializer
|
||||||
|
lookup_field = 'code'
|
||||||
|
required_scopes = []
|
||||||
|
|
||||||
|
|
||||||
|
class AccountView(generics.RetrieveUpdateAPIView):
|
||||||
|
permission_classes = [permissions.IsAuthenticated, IsAuthenticatedOrTokenHasScope]
|
||||||
|
serializer_class = AccountSerializer
|
||||||
|
required_scopes = []
|
||||||
|
|
||||||
|
def get_object(self):
|
||||||
|
return self.request.user
|
||||||
|
|
||||||
|
def perform_update(self, serializer):
|
||||||
|
serializer.save(last_update=timezone.now())
|
||||||
|
|
||||||
|
|
||||||
|
class RequestOTPView(generics.CreateAPIView):
|
||||||
|
permission_classes = []
|
||||||
|
serializer_class = RequestOTPSerializer
|
||||||
|
required_scopes = []
|
||||||
|
|
||||||
|
|
||||||
|
class ChangePasswordView(generics.UpdateAPIView):
|
||||||
|
permission_classes = [permissions.IsAuthenticated, IsAuthenticatedOrTokenHasScope]
|
||||||
|
serializer_class = ChangePasswordSerializer
|
||||||
|
required_scopes = []
|
||||||
|
model = User
|
||||||
|
|
||||||
|
def get_object(self, queryset=None):
|
||||||
|
obj = self.request.user
|
||||||
|
return obj
|
||||||
|
|
||||||
|
def update(self, request, *args, **kwargs):
|
||||||
|
self.object = self.get_object()
|
||||||
|
serializer = self.get_serializer(data=request.data)
|
||||||
|
|
||||||
|
if serializer.is_valid():
|
||||||
|
gateway = serializer.data.get("old_password_gateway")
|
||||||
|
old_password = serializer.data.get("old_password")
|
||||||
|
new_password = serializer.data.get("new_password")
|
||||||
|
if not self.object.check_auth(gateway, old_password):
|
||||||
|
return Response({"old_password": ["Wrong password/otp."]}, status=status.HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
|
self.object.set_password(new_password)
|
||||||
|
self.object.last_update = timezone.now()
|
||||||
|
self.object.save()
|
||||||
|
return Response({"state": 'success'}, status=status.HTTP_200_OK)
|
||||||
|
|
||||||
|
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||||
1
deploy.sh
Normal file
1
deploy.sh
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
# Deployment script
|
||||||
0
dot_restrict_scopes/__init__.py
Normal file
0
dot_restrict_scopes/__init__.py
Normal file
25
dot_restrict_scopes/admin.py
Normal file
25
dot_restrict_scopes/admin.py
Normal file
|
|
@ -0,0 +1,25 @@
|
||||||
|
"""
|
||||||
|
Django admin configuration for the dot-restrict-scopes package.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from django.contrib import admin
|
||||||
|
from django.contrib.admin.sites import NotRegistered
|
||||||
|
|
||||||
|
from oauth2_provider.admin import ApplicationAdmin
|
||||||
|
|
||||||
|
from .models import RestrictedApplication
|
||||||
|
from .forms import RestrictedApplicationForm
|
||||||
|
|
||||||
|
|
||||||
|
# The restricted application is registered by Django OAuth Toolkit, but we want
|
||||||
|
# to provide our own admin that uses our form
|
||||||
|
try:
|
||||||
|
admin.site.unregister(RestrictedApplication)
|
||||||
|
except NotRegistered:
|
||||||
|
pass
|
||||||
|
|
||||||
|
@admin.register(RestrictedApplication)
|
||||||
|
class RestrictedApplicationAdmin(ApplicationAdmin):
|
||||||
|
form = RestrictedApplicationForm
|
||||||
|
|
||||||
|
# admin.site.register(RestrictedApplication, RestrictedApplicationAdmin)
|
||||||
5
dot_restrict_scopes/apps.py
Normal file
5
dot_restrict_scopes/apps.py
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
from django.apps import AppConfig
|
||||||
|
|
||||||
|
|
||||||
|
class DotRestrictScopesConfig(AppConfig):
|
||||||
|
name = 'dot_restrict_scopes'
|
||||||
51
dot_restrict_scopes/forms.py
Normal file
51
dot_restrict_scopes/forms.py
Normal file
|
|
@ -0,0 +1,51 @@
|
||||||
|
"""
|
||||||
|
Django forms for use with the dot-restrict-scopes package.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from django import forms
|
||||||
|
|
||||||
|
from oauth2_provider.scopes import get_scopes_backend
|
||||||
|
|
||||||
|
|
||||||
|
class DelimitedListField(forms.MultipleChoiceField):
|
||||||
|
"""
|
||||||
|
Django form field that allows for the use of list widgets with a text field
|
||||||
|
containing a delimited list.
|
||||||
|
"""
|
||||||
|
delimiter = ','
|
||||||
|
|
||||||
|
def __init__(self, delimiter = None, *args, **kwargs):
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
self.delimiter = delimiter or self.delimiter
|
||||||
|
|
||||||
|
def prepare_value(self, value):
|
||||||
|
# If the value is already a list or tuple, just use it as-is
|
||||||
|
if isinstance(value, (list, tuple)): return value
|
||||||
|
# Otherwise, prepare the value by splitting on the delimiter, trimming
|
||||||
|
# leading and trailing whitespace and excluding empty values
|
||||||
|
return [p.strip() for p in value.split(self.delimiter) if p.strip()]
|
||||||
|
|
||||||
|
def clean(self, value):
|
||||||
|
# Let the parent clean the value first, then join the result using the
|
||||||
|
# specified delimiter
|
||||||
|
return self.delimiter.join(super().clean(value))
|
||||||
|
|
||||||
|
|
||||||
|
class RestrictedApplicationForm(forms.ModelForm):
|
||||||
|
"""
|
||||||
|
Form for creating or updating a restricted application.
|
||||||
|
"""
|
||||||
|
# allowed_scope is a space-delimited list, but we want to present
|
||||||
|
# a selection of valid scopes with checkboxes
|
||||||
|
allowed_scope = DelimitedListField(
|
||||||
|
label = 'Allowed scopes',
|
||||||
|
# The choices and initial values are callables, because the scopes might
|
||||||
|
# not be available at import type, e.g. if coming from the database
|
||||||
|
choices = lambda: get_scopes_backend().get_all_scopes().items(),
|
||||||
|
initial = lambda: get_scopes_backend().get_default_scopes(),
|
||||||
|
delimiter = ' ',
|
||||||
|
widget = forms.CheckboxSelectMultiple
|
||||||
|
)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
exclude = ()
|
||||||
41
dot_restrict_scopes/migrations/0001_initial.py
Normal file
41
dot_restrict_scopes/migrations/0001_initial.py
Normal file
|
|
@ -0,0 +1,41 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Generated by Django 1.11.4 on 2017-09-01 15:44
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
import oauth2_provider.generators
|
||||||
|
import oauth2_provider.validators
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
initial = True
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='RestrictedApplication',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(primary_key=True, serialize=False)),
|
||||||
|
('client_id', models.CharField(db_index=True, default=oauth2_provider.generators.generate_client_id, max_length=100, unique=True)),
|
||||||
|
('redirect_uris', models.TextField(blank=True, help_text='Allowed URIs list, space separated')),
|
||||||
|
('client_type', models.CharField(choices=[('confidential', 'Confidential'), ('public', 'Public')], max_length=32)),
|
||||||
|
('authorization_grant_type', models.CharField(choices=[('authorization-code', 'Authorization code'), ('implicit', 'Implicit'), ('password', 'Resource owner password-based'), ('client-credentials', 'Client credentials')], max_length=32)),
|
||||||
|
('client_secret', models.CharField(blank=True, db_index=True, default=oauth2_provider.generators.generate_client_secret, max_length=255)),
|
||||||
|
('name', models.CharField(blank=True, max_length=255)),
|
||||||
|
('skip_authorization', models.BooleanField(default=False)),
|
||||||
|
('created', models.DateTimeField(auto_now_add=True)),
|
||||||
|
('updated', models.DateTimeField(auto_now=True)),
|
||||||
|
('allowed_scope', models.TextField(blank=True)),
|
||||||
|
('user', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='dot_restrict_scopes_restrictedapplication', to=settings.AUTH_USER_MODEL)),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'abstract': False,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
||||||
0
dot_restrict_scopes/migrations/__init__.py
Normal file
0
dot_restrict_scopes/migrations/__init__.py
Normal file
25
dot_restrict_scopes/models.py
Normal file
25
dot_restrict_scopes/models.py
Normal file
|
|
@ -0,0 +1,25 @@
|
||||||
|
"""
|
||||||
|
Django models for the dot-restrict-scopes package.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from django.db import models
|
||||||
|
|
||||||
|
from oauth2_provider.models import AbstractApplication
|
||||||
|
from oauth2_provider.scopes import get_scopes_backend
|
||||||
|
|
||||||
|
|
||||||
|
class RestrictedApplication(AbstractApplication):
|
||||||
|
"""
|
||||||
|
Application model for use with Django OAuth Toolkit that allows the scopes
|
||||||
|
available to an application to be restricted on a per-application basis.
|
||||||
|
"""
|
||||||
|
allowed_scope = models.TextField(blank = True)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def allowed_scopes(self):
|
||||||
|
"""
|
||||||
|
Returns the set of allowed scope names for this application.
|
||||||
|
"""
|
||||||
|
all_scopes = set(get_scopes_backend().get_all_scopes().keys())
|
||||||
|
app_scopes = set(self.allowed_scope.split())
|
||||||
|
return app_scopes.intersection(all_scopes)
|
||||||
43
dot_restrict_scopes/scopes.py
Normal file
43
dot_restrict_scopes/scopes.py
Normal file
|
|
@ -0,0 +1,43 @@
|
||||||
|
"""
|
||||||
|
Django OAuth Toolkit scopes backend for the dot-restrict-scopes package.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
|
from django.utils import module_loading
|
||||||
|
|
||||||
|
from oauth2_provider.scopes import BaseScopes
|
||||||
|
|
||||||
|
|
||||||
|
class RestrictApplicationScopes(BaseScopes):
|
||||||
|
"""
|
||||||
|
Scopes backend that wraps another backend and restricts the scopes available
|
||||||
|
to an application based on the application's ``allowed_scopes``.
|
||||||
|
"""
|
||||||
|
def __init__(self):
|
||||||
|
# Initialise the wrapped backend from settings
|
||||||
|
self._wrapped = module_loading.import_string(
|
||||||
|
getattr(settings, 'DOT_RESTRICT_SCOPES', {}).get(
|
||||||
|
'WRAPPED_SCOPES_BACKEND_CLASS',
|
||||||
|
'oauth2_provider.scopes.SettingsScopes'
|
||||||
|
)
|
||||||
|
)()
|
||||||
|
|
||||||
|
def get_all_scopes(self):
|
||||||
|
# Just return all the available scopes from the wrapped backend
|
||||||
|
return self._wrapped.get_all_scopes()
|
||||||
|
|
||||||
|
def get_available_scopes(self, application = None, request = None, *args, **kwargs):
|
||||||
|
# Get the available scopes from the wrapped backend, then filter them
|
||||||
|
# based on the allowed_scopes of the application
|
||||||
|
scopes = self._wrapped.get_available_scopes(application, request, *args, **kwargs)
|
||||||
|
if application:
|
||||||
|
scopes = [s for s in scopes if s in application.allowed_scopes]
|
||||||
|
return scopes
|
||||||
|
|
||||||
|
def get_default_scopes(self, application = None, request = None, *args, **kwargs):
|
||||||
|
# Get the default scopes from the wrapped backend, then filter them
|
||||||
|
# based on the allowed_scopes of the application
|
||||||
|
scopes = self._wrapped.get_default_scopes(application, request, *args, **kwargs)
|
||||||
|
if application:
|
||||||
|
scopes = [s for s in scopes if s in application.allowed_scopes]
|
||||||
|
return scopes
|
||||||
21
manage.py
Normal file
21
manage.py
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
#!/usr/bin/env python
|
||||||
|
"""Django's command-line utility for administrative tasks."""
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'accounts.settings')
|
||||||
|
try:
|
||||||
|
from django.core.management import execute_from_command_line
|
||||||
|
except ImportError as exc:
|
||||||
|
raise ImportError(
|
||||||
|
"Couldn't import Django. Are you sure it's installed and "
|
||||||
|
"available on your PYTHONPATH environment variable? Did you "
|
||||||
|
"forget to activate a virtual environment?"
|
||||||
|
) from exc
|
||||||
|
execute_from_command_line(sys.argv)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
||||||
12
templates/base_generic.html
Normal file
12
templates/base_generic.html
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<title>Title</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
{% block content %}
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
40
templates/registration/login.html
Normal file
40
templates/registration/login.html
Normal file
|
|
@ -0,0 +1,40 @@
|
||||||
|
{% extends "base_generic.html" %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
|
||||||
|
{% if form.errors %}
|
||||||
|
<p>Your username and password didn't match. Please try again.</p>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if next %}
|
||||||
|
{% if user.is_authenticated %}
|
||||||
|
<p>Your account doesn't have access to this page. To proceed,
|
||||||
|
please login with an account that has access.</p>
|
||||||
|
{% else %}
|
||||||
|
<p>Please login to see this page.</p>
|
||||||
|
{% endif %}
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<form method="post" action="{% url 'login' %}">
|
||||||
|
{% csrf_token %}
|
||||||
|
<table>
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<td>{{ form.username.label_tag }}</td>
|
||||||
|
<td>{{ form.username }}</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<td>{{ form.password.label_tag }}</td>
|
||||||
|
<td>{{ form.password }}</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<input type="submit" value="login" />
|
||||||
|
<input type="hidden" name="next" value="{{ next }}" />
|
||||||
|
</form>
|
||||||
|
|
||||||
|
{# Assumes you setup the password_reset view in your URLconf #}
|
||||||
|
<p><a href="{% url 'password_reset' %}">Lost password?</a></p>
|
||||||
|
|
||||||
|
{% endblock %}
|
||||||
54
utils.py
Normal file
54
utils.py
Normal file
|
|
@ -0,0 +1,54 @@
|
||||||
|
import random
|
||||||
|
from time import time
|
||||||
|
from oauth2_provider.oauth2_validators import OAuth2Validator
|
||||||
|
|
||||||
|
from django.contrib.auth import get_user_model
|
||||||
|
|
||||||
|
USER_MODEL = get_user_model()
|
||||||
|
|
||||||
|
|
||||||
|
def random_code(pre_len=5, post_len=5):
|
||||||
|
code = str(time())[:pre_len] + ''.join(random.choice('0123456789') for _ in range(post_len))
|
||||||
|
return code
|
||||||
|
|
||||||
|
|
||||||
|
class MultiGatewayOAuth2Validator(OAuth2Validator): # pylint: disable=w0223
|
||||||
|
""" Primarily extend the functionality of token generation """
|
||||||
|
|
||||||
|
def validate_user(self, username, password, client, request, *args, **kwargs):
|
||||||
|
""" Here, you would be able to access the MOBILE/ OTP fields
|
||||||
|
which you will be sending in the request.post body. """
|
||||||
|
# otp = request.otp
|
||||||
|
# mobile = request.mobile
|
||||||
|
# user = AppropriateModel.objects.get(otp=otp, mobile=mobile)
|
||||||
|
auth_gateway = getattr(request, 'auth_gateway', 'username:password').split(':')
|
||||||
|
user_gateway = auth_gateway[0]
|
||||||
|
pass_gateway = auth_gateway[1]
|
||||||
|
|
||||||
|
user = None
|
||||||
|
if user_gateway == 'phone_number':
|
||||||
|
user = USER_MODEL.objects.get(
|
||||||
|
phone_number=username
|
||||||
|
)
|
||||||
|
|
||||||
|
elif auth_gateway == 'username':
|
||||||
|
user = USER_MODEL.objects.get(
|
||||||
|
username=username
|
||||||
|
)
|
||||||
|
|
||||||
|
elif user_gateway == 'email':
|
||||||
|
user = USER_MODEL.objects.get(
|
||||||
|
email=username
|
||||||
|
)
|
||||||
|
|
||||||
|
if user is None:
|
||||||
|
return False
|
||||||
|
|
||||||
|
if not user.check_auth(pass_gateway, password):
|
||||||
|
return False
|
||||||
|
|
||||||
|
if user.is_active:
|
||||||
|
request.user = user
|
||||||
|
return True
|
||||||
|
|
||||||
|
return False
|
||||||
Loading…
Add table
Reference in a new issue