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