init accounts service

This commit is contained in:
mahdavi 2019-11-14 12:32:11 +03:30
commit d0cdf6c38e
30 changed files with 993 additions and 0 deletions

8
.gitignore vendored Normal file
View file

@ -0,0 +1,8 @@
.python-version
.idea
media/*
delme*.py
Pipfile.lock
static
*.pyc
.env

23
Pipfile Normal file
View 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
View file

176
accounts/settings.py Normal file
View 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
View 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
View 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
View file

0
apps/users/__init__.py Normal file
View file

32
apps/users/admin.py Normal file
View 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
View file

@ -0,0 +1,5 @@
from django.apps import AppConfig
class UsersConfig(AppConfig):
name = 'users'

2
apps/users/constans.py Normal file
View file

@ -0,0 +1,2 @@
MAX_OTP_TRY = 3
DEVELOPMENT_PHONE_NUMBERS = ['+989106853582']

View 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()),
],
),
]

View file

165
apps/users/models.py Normal file
View 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
View 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
View file

@ -0,0 +1,3 @@
from django.test import TestCase
# Create your tests here.

69
apps/users/views.py Normal file
View 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
View file

@ -0,0 +1 @@
# Deployment script

View file

View 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)

View file

@ -0,0 +1,5 @@
from django.apps import AppConfig
class DotRestrictScopesConfig(AppConfig):
name = 'dot_restrict_scopes'

View 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 = ()

View 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,
},
),
]

View 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)

View 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
View 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()

View 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>

View 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
View 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