cleanup
This commit is contained in:
parent
250e2a3edd
commit
7ba4ed647f
44 changed files with 105 additions and 4320 deletions
|
|
@ -1,36 +0,0 @@
|
|||
"""
|
||||
Django admin configuration for the gooyal-restrict-scopes package.
|
||||
"""
|
||||
|
||||
from django.contrib import admin
|
||||
from django.contrib.admin.sites import NotRegistered
|
||||
|
||||
from oauth2_provider.admin import ApplicationAdmin
|
||||
|
||||
from .models import Application, Resource, Scope
|
||||
from .forms import ApplicationForm
|
||||
|
||||
|
||||
# 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(Application)
|
||||
except NotRegistered:
|
||||
pass
|
||||
|
||||
@admin.register(Application)
|
||||
class ApplicationAdmin(ApplicationAdmin):
|
||||
form = ApplicationForm
|
||||
|
||||
|
||||
@admin.register(Resource)
|
||||
class ResourceAdmin(admin.ModelAdmin):
|
||||
list_display = ("name", "user", "expires")
|
||||
|
||||
|
||||
@admin.register(Scope)
|
||||
class ScopeAdmin(admin.ModelAdmin):
|
||||
list_display = ('name', "resource", 'description', 'is_default')
|
||||
|
||||
|
||||
# admin.site.register(RestrictedApplication, RestrictedApplicationAdmin)
|
||||
|
|
@ -1,5 +0,0 @@
|
|||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class GooyalOauthConfig(AppConfig):
|
||||
name = 'apps.accounts_oauth2'
|
||||
|
|
@ -1,51 +0,0 @@
|
|||
"""
|
||||
Django forms for use with the gooyal-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 ApplicationForm(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 = ()
|
||||
|
|
@ -1,123 +0,0 @@
|
|||
# Generated by Django 4.1 on 2022-08-17 19:05
|
||||
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
import oauth2_provider.generators
|
||||
import oauth2_provider.models
|
||||
import uuid
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
run_before = [
|
||||
('oauth2_provider', '0001_initial'),
|
||||
]
|
||||
|
||||
dependencies = [
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='AccessToken',
|
||||
fields=[
|
||||
('id', models.BigAutoField(primary_key=True, serialize=False)),
|
||||
('token', models.CharField(max_length=255, unique=True)),
|
||||
('expires', models.DateTimeField()),
|
||||
('scope', models.TextField(blank=True)),
|
||||
('created', models.DateTimeField(auto_now_add=True)),
|
||||
('updated', models.DateTimeField(auto_now=True)),
|
||||
('detail', models.JSONField(blank=True, null=True)),
|
||||
],
|
||||
options={
|
||||
'abstract': False,
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Application',
|
||||
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'), ('openid-hybrid', 'OpenID connect hybrid')], max_length=32)),
|
||||
('client_secret', oauth2_provider.models.ClientSecretField(blank=True, db_index=True, default=oauth2_provider.generators.generate_client_secret, help_text='Hashed on Save. Copy it now if this is a new 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)),
|
||||
('algorithm', models.CharField(blank=True, choices=[('', 'No OIDC support'), ('RS256', 'RSA with SHA-2 256'), ('HS256', 'HMAC with SHA-2 256')], default='', max_length=5)),
|
||||
('allowed_scope', models.TextField(blank=True)),
|
||||
],
|
||||
options={
|
||||
'abstract': False,
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Grant',
|
||||
fields=[
|
||||
('id', models.BigAutoField(primary_key=True, serialize=False)),
|
||||
('code', models.CharField(max_length=255, unique=True)),
|
||||
('expires', models.DateTimeField()),
|
||||
('redirect_uri', models.TextField()),
|
||||
('scope', models.TextField(blank=True)),
|
||||
('created', models.DateTimeField(auto_now_add=True)),
|
||||
('updated', models.DateTimeField(auto_now=True)),
|
||||
('code_challenge', models.CharField(blank=True, default='', max_length=128)),
|
||||
('code_challenge_method', models.CharField(blank=True, choices=[('plain', 'plain'), ('S256', 'S256')], default='', max_length=10)),
|
||||
('nonce', models.CharField(blank=True, default='', max_length=255)),
|
||||
('claims', models.TextField(blank=True)),
|
||||
],
|
||||
options={
|
||||
'abstract': False,
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='IDToken',
|
||||
fields=[
|
||||
('id', models.BigAutoField(primary_key=True, serialize=False)),
|
||||
('jti', models.UUIDField(default=uuid.uuid4, editable=False, unique=True, verbose_name='JWT Token ID')),
|
||||
('expires', models.DateTimeField()),
|
||||
('scope', models.TextField(blank=True)),
|
||||
('created', models.DateTimeField(auto_now_add=True)),
|
||||
('updated', models.DateTimeField(auto_now=True)),
|
||||
],
|
||||
options={
|
||||
'abstract': False,
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='RefreshToken',
|
||||
fields=[
|
||||
('id', models.BigAutoField(primary_key=True, serialize=False)),
|
||||
('token', models.CharField(max_length=255)),
|
||||
('created', models.DateTimeField(auto_now_add=True)),
|
||||
('updated', models.DateTimeField(auto_now=True)),
|
||||
('revoked', models.DateTimeField(null=True)),
|
||||
],
|
||||
options={
|
||||
'abstract': False,
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Resource',
|
||||
fields=[
|
||||
('uuid', models.UUIDField(db_index=True, default=uuid.uuid4, editable=False, primary_key=True, serialize=False, unique=True)),
|
||||
('name', models.CharField(max_length=255)),
|
||||
('expires', models.DateTimeField()),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Scope',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('name', models.CharField(help_text='The name of the scope.', max_length=255, unique=True)),
|
||||
('description', models.TextField(help_text='A brief description of the scope. This text is displayed to users when authorising access for the scope.')),
|
||||
('is_default', models.BooleanField(default=False, help_text='Indicates if this scope should be included in the default scopes.')),
|
||||
('application', models.ForeignKey(blank=True, help_text='The application to which the scope belongs.', null=True, on_delete=django.db.models.deletion.CASCADE, related_name='scopes', to=settings.OAUTH2_PROVIDER_APPLICATION_MODEL)),
|
||||
('resource', models.ForeignKey(blank=True, help_text='The resource of scope.', null=True, on_delete=django.db.models.deletion.PROTECT, related_name='scopes', to='accounts_oauth2.resource')),
|
||||
],
|
||||
),
|
||||
]
|
||||
|
|
@ -1,92 +0,0 @@
|
|||
# Generated by Django 4.1 on 2022-08-17 19:05
|
||||
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
('accounts_oauth2', '0001_initial'),
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='resource',
|
||||
name='user',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='resources', to=settings.AUTH_USER_MODEL),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='refreshtoken',
|
||||
name='access_token',
|
||||
field=models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='refresh_token', to=settings.OAUTH2_PROVIDER_ACCESS_TOKEN_MODEL),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='refreshtoken',
|
||||
name='application',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='refresh_tokens', to=settings.OAUTH2_PROVIDER_APPLICATION_MODEL),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='refreshtoken',
|
||||
name='user',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='%(app_label)s_%(class)s', to=settings.AUTH_USER_MODEL),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='idtoken',
|
||||
name='application',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to=settings.OAUTH2_PROVIDER_APPLICATION_MODEL),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='idtoken',
|
||||
name='user',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='%(app_label)s_%(class)s', to=settings.AUTH_USER_MODEL),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='grant',
|
||||
name='application',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='grants', to=settings.OAUTH2_PROVIDER_APPLICATION_MODEL),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='grant',
|
||||
name='user',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='%(app_label)s_%(class)s', to=settings.AUTH_USER_MODEL),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='application',
|
||||
name='resource',
|
||||
field=models.OneToOneField(blank=True, help_text='The resource of application.', null=True, on_delete=django.db.models.deletion.PROTECT, related_name='application', to='accounts_oauth2.resource'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='application',
|
||||
name='user',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='%(app_label)s_%(class)s', to=settings.AUTH_USER_MODEL),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='accesstoken',
|
||||
name='application',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='access_tokens', to=settings.OAUTH2_PROVIDER_APPLICATION_MODEL),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='accesstoken',
|
||||
name='id_token',
|
||||
field=models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='access_token', to=settings.OAUTH2_PROVIDER_ID_TOKEN_MODEL),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='accesstoken',
|
||||
name='source_refresh_token',
|
||||
field=models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='refreshed_access_token', to=settings.OAUTH2_PROVIDER_REFRESH_TOKEN_MODEL),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='accesstoken',
|
||||
name='user',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='%(app_label)s_%(class)s', to=settings.AUTH_USER_MODEL),
|
||||
),
|
||||
migrations.AlterUniqueTogether(
|
||||
name='refreshtoken',
|
||||
unique_together={('token', 'revoked')},
|
||||
),
|
||||
]
|
||||
|
|
@ -1,38 +0,0 @@
|
|||
# Generated by Django 5.0.6 on 2024-06-26 12:34
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('accounts_oauth2', '0002_initial'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='application',
|
||||
name='allowed_origins',
|
||||
field=models.TextField(blank=True, default='', help_text='Allowed origins list to enable CORS, space separated'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='application',
|
||||
name='hash_client_secret',
|
||||
field=models.BooleanField(default=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='application',
|
||||
name='post_logout_redirect_uris',
|
||||
field=models.TextField(blank=True, default='', help_text='Allowed Post Logout URIs list, space separated'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='accesstoken',
|
||||
name='token',
|
||||
field=models.CharField(db_index=True, max_length=255, unique=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='scope',
|
||||
name='id',
|
||||
field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
|
||||
),
|
||||
]
|
||||
|
|
@ -1,168 +0,0 @@
|
|||
import requests
|
||||
from django.conf import settings
|
||||
from django.db import models
|
||||
from django.db.models import JSONField
|
||||
from oauth2_provider.models import AbstractApplication, AbstractAccessToken, AbstractGrant, AbstractRefreshToken, \
|
||||
AbstractIDToken
|
||||
from oauth2_provider.scopes import get_scopes_backend
|
||||
from oauth2_provider.settings import oauth2_settings
|
||||
import uuid
|
||||
|
||||
|
||||
class Resource(models.Model):
|
||||
uuid = models.UUIDField(primary_key=True, editable=False, default=uuid.uuid4, unique=True, db_index=True)
|
||||
name = models.CharField(max_length=255)
|
||||
|
||||
user = models.ForeignKey(
|
||||
settings.AUTH_USER_MODEL, on_delete=models.CASCADE, blank=True, null=True,
|
||||
related_name="resources"
|
||||
)
|
||||
expires = models.DateTimeField()
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
|
||||
class Application(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.
|
||||
"""
|
||||
user = models.ForeignKey(
|
||||
settings.AUTH_USER_MODEL,
|
||||
related_name="%(app_label)s_%(class)s",
|
||||
on_delete=models.PROTECT
|
||||
)
|
||||
allowed_scope = models.TextField(blank=True)
|
||||
resource = models.OneToOneField(
|
||||
Resource,
|
||||
models.PROTECT,
|
||||
blank=True, null=True,
|
||||
help_text='The resource of application.',
|
||||
related_name='application'
|
||||
)
|
||||
|
||||
@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)
|
||||
|
||||
|
||||
class Scope(models.Model):
|
||||
"""
|
||||
Django model for an OAuth scope.
|
||||
"""
|
||||
#: The application that created the scope
|
||||
# NOTE: This is not used to limit access to the scope in any way - we want the
|
||||
# scope to be available to other applications in order to request access
|
||||
# to the resource it protects!
|
||||
application = models.ForeignKey(
|
||||
oauth2_settings.APPLICATION_MODEL,
|
||||
models.CASCADE,
|
||||
# This field is nullable because it is only set for scopes created by
|
||||
# external resource servers, which have a corresponding OAuth application
|
||||
# record on the authorisation server
|
||||
blank=True, null=True,
|
||||
help_text='The application to which the scope belongs.',
|
||||
related_name='scopes'
|
||||
)
|
||||
resource = models.ForeignKey(
|
||||
Resource,
|
||||
models.PROTECT,
|
||||
blank=True, null=True,
|
||||
help_text='The resource of scope.',
|
||||
related_name='scopes'
|
||||
)
|
||||
#: The name of the scope
|
||||
name = models.CharField(
|
||||
max_length=255,
|
||||
unique=True,
|
||||
help_text='The name of the scope.'
|
||||
)
|
||||
#: A brief description of the scope
|
||||
description = models.TextField(
|
||||
help_text='A brief description of the scope. This text is displayed '
|
||||
'to users when authorising access for the scope.'
|
||||
)
|
||||
#: Indicates if the scope should be included in the default scopes
|
||||
is_default = models.BooleanField(
|
||||
default=False,
|
||||
help_text='Indicates if this scope should be included in the default scopes.'
|
||||
)
|
||||
|
||||
@property
|
||||
def final_name(self):
|
||||
args = []
|
||||
if self.resource:
|
||||
args.append(self.resource.name)
|
||||
|
||||
args.append(self.name)
|
||||
return '.'.join(args)
|
||||
|
||||
@property
|
||||
def final_description(self):
|
||||
resource_name = self.resource and self.resource.name
|
||||
|
||||
if resource_name:
|
||||
return f"{resource_name} -> {self.description}"
|
||||
return self.description
|
||||
|
||||
@classmethod
|
||||
def register(cls, name, description, is_default=False):
|
||||
"""
|
||||
Registers a scope with the given values. It always creates an instance in
|
||||
the local database, but if this resource server has an external authorisation
|
||||
server, it will also register the scope there.
|
||||
|
||||
Returns ``True`` on success. Should raise on failure.
|
||||
"""
|
||||
endpoint = settings.RESOURCE_SERVER_REGISTER_SCOPE_URL
|
||||
if endpoint:
|
||||
# If the endpoint is set, make the callout to the authz server
|
||||
token = "Bearer {}".format(oauth2_settings.RESOURCE_SERVER_AUTH_TOKEN)
|
||||
# Let any failures bubble up
|
||||
# The idea is to call this method during deployment as a post-migrate
|
||||
# hook, so we want failures to halt the deployment
|
||||
response = requests.post(
|
||||
endpoint,
|
||||
json={
|
||||
'name': name,
|
||||
'description': description,
|
||||
'is_default': is_default
|
||||
},
|
||||
headers={"Authorization": token}
|
||||
)
|
||||
# Raise the exception for anything other than 20x responses
|
||||
response.raise_for_status()
|
||||
# Always create/update the scope record locally
|
||||
_ = Scope.objects.update_or_create(
|
||||
name=name,
|
||||
defaults={'description': description, 'is_default': is_default}
|
||||
)
|
||||
return True
|
||||
|
||||
|
||||
class Grant(AbstractGrant):
|
||||
application = models.ForeignKey(
|
||||
oauth2_settings.APPLICATION_MODEL, on_delete=models.CASCADE, related_name='grants'
|
||||
)
|
||||
|
||||
|
||||
class RefreshToken(AbstractRefreshToken):
|
||||
application = models.ForeignKey(
|
||||
oauth2_settings.APPLICATION_MODEL, on_delete=models.CASCADE, related_name='refresh_tokens')
|
||||
|
||||
|
||||
class AccessToken(AbstractAccessToken):
|
||||
detail = JSONField(null=True, blank=True)
|
||||
application = models.ForeignKey(
|
||||
oauth2_settings.APPLICATION_MODEL, on_delete=models.CASCADE, blank=True, null=True, related_name='access_tokens'
|
||||
)
|
||||
|
||||
|
||||
class IDToken(AbstractIDToken):
|
||||
pass
|
||||
|
|
@ -1,33 +0,0 @@
|
|||
"""
|
||||
Django OAuth Toolkit scopes backend for the gooyal-dynamic-scopes package.
|
||||
"""
|
||||
|
||||
from django.conf import settings
|
||||
from django.utils import module_loading
|
||||
|
||||
from oauth2_provider.scopes import BaseScopes
|
||||
|
||||
from .models import Scope
|
||||
|
||||
|
||||
class Scopes(BaseScopes):
|
||||
"""
|
||||
Scopes backend that provides scopes from a Django model.
|
||||
"""
|
||||
|
||||
def get_queryset(self, application=None):
|
||||
queryset = Scope.objects.all()
|
||||
if application:
|
||||
queryset = queryset.filter(name__in=application.allowed_scopes).order_by('resource__uuid')
|
||||
return queryset
|
||||
|
||||
|
||||
def get_all_scopes(self):
|
||||
return {scope.final_name: scope.final_description for scope in self.get_queryset().all()}
|
||||
|
||||
def get_available_scopes(self, application=None, request=None, *args, **kwargs):
|
||||
scopes = [scope.final_name for scope in self.get_queryset(application).all()]
|
||||
return scopes
|
||||
|
||||
def get_default_scopes(self, application=None, request=None, *args, **kwargs):
|
||||
return [scope.final_name for scope in self.get_queryset(application).filter(is_default=True).all()]
|
||||
|
|
@ -1,20 +0,0 @@
|
|||
from oauth2_provider.settings import OAuth2ProviderSettings, USER_SETTINGS, DEFAULTS, IMPORT_STRINGS, MANDATORY
|
||||
|
||||
GOOYAL_DEFAULTS = {
|
||||
# Resource Server with Token Introspection additions
|
||||
"RESOURCE_SERVER_CLIENT_ID": None,
|
||||
"RESOURCE_SERVER_CLIENT_SECRET": None,
|
||||
|
||||
# set default
|
||||
"INTROSPECTION_USER_CREATE_METHOD": None,
|
||||
}
|
||||
|
||||
GOOYAL_IMPORT_STRINGS = ("INTROSPECTION_USER_CREATE_METHOD",)
|
||||
|
||||
GOOYAL_MANDATORY = ("INTROSPECTION_USER_CREATE_METHOD",)
|
||||
|
||||
DEFAULTS.update(GOOYAL_DEFAULTS)
|
||||
IMPORT_STRINGS = IMPORT_STRINGS + GOOYAL_IMPORT_STRINGS
|
||||
MANDATORY = MANDATORY + GOOYAL_MANDATORY
|
||||
|
||||
oauth2_settings = OAuth2ProviderSettings(USER_SETTINGS, DEFAULTS, IMPORT_STRINGS, MANDATORY)
|
||||
|
|
@ -1,26 +0,0 @@
|
|||
"""
|
||||
Django signal handlers for the gooyal-dynamic-scopes package.
|
||||
"""
|
||||
|
||||
from django.conf import settings
|
||||
|
||||
from oauth2_provider.settings import oauth2_settings
|
||||
|
||||
from .models import Scope
|
||||
|
||||
|
||||
def register_scopes(app_config, verbosity=2, interactive=True, **kwargs):
|
||||
"""
|
||||
``post_migrate`` signal handler that ensures the scopes required for the
|
||||
introspection and register-scope endpoints are registered when running as an
|
||||
authorisation server.
|
||||
|
||||
The signal is connected in ``apps.py``.
|
||||
"""
|
||||
# Register any scopes in the oauth2_provider settings
|
||||
for name, description in oauth2_settings.SCOPES.items():
|
||||
Scope.register(
|
||||
name,
|
||||
description,
|
||||
name in oauth2_settings.DEFAULT_SCOPES
|
||||
)
|
||||
|
|
@ -1,288 +0,0 @@
|
|||
import base64
|
||||
import binascii
|
||||
import logging
|
||||
from datetime import datetime, timedelta
|
||||
from urllib.parse import unquote_plus
|
||||
|
||||
import requests
|
||||
# import service_clients
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.utils.timezone import make_aware
|
||||
from oauth2_provider.models import get_access_token_model
|
||||
from oauth2_provider.oauth2_validators import OAuth2Validator as BaseOAuth2Validator
|
||||
from .settings import oauth2_settings
|
||||
from django.conf import settings
|
||||
|
||||
log = logging.getLogger("oauth2_provider")
|
||||
|
||||
AccessTokenModel = get_access_token_model()
|
||||
UserModel = get_user_model()
|
||||
|
||||
|
||||
class OAuth2Validator(BaseOAuth2Validator): # pylint: disable=w0223
|
||||
introspection_client = None
|
||||
|
||||
def get_introspection_client(self, introspection_client_id, introspection_client_secret):
|
||||
if not OAuth2Validator.introspection_client:
|
||||
OAuth2Validator.introspection_client = service_clients.Client(client_id=introspection_client_id,
|
||||
client_secret=introspection_client_secret,
|
||||
grant_type=service_clients.AccountsClient.GRANT_CLIENT_CREDENTIALS,
|
||||
scopes=['introspection'])
|
||||
|
||||
return OAuth2Validator.introspection_client
|
||||
|
||||
def validate_user(self, username, password, client, request, *args, **kwargs):
|
||||
auth_fields = getattr(request, 'auth_fields', 'username:password').split(':')
|
||||
|
||||
if len(auth_fields) != 2:
|
||||
return False
|
||||
|
||||
user_field, pass_field = auth_fields
|
||||
|
||||
if user_field not in ['phone_number', 'username', 'email']:
|
||||
return False
|
||||
|
||||
if pass_field not in ['password', 'otp', 'ott']:
|
||||
return False
|
||||
|
||||
if not username or not password:
|
||||
return False
|
||||
|
||||
user = UserModel.objects.filter(**{user_field: username}).first()
|
||||
|
||||
if not user:
|
||||
return False
|
||||
|
||||
if not user.check_auth(pass_field, password):
|
||||
return False
|
||||
|
||||
if user.is_active:
|
||||
request.user = user
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def _get_token_from_gooyal_authentication_server(
|
||||
self, token, introspection_url, introspection_token, introspection_credentials, introspection_client_id,
|
||||
introspection_client_secret
|
||||
):
|
||||
"""Use external introspection endpoint to "crack open" the token.
|
||||
:param introspection_url: introspection endpoint URL
|
||||
:param introspection_token: Bearer token
|
||||
:param introspection_credentials: Basic Auth credentials (id,secret)
|
||||
:return: :class:`models.AccessToken`
|
||||
|
||||
Some RFC 7662 implementations (including this one) use a Bearer token while others use Basic
|
||||
Auth. Depending on the external AS's implementation, provide either the introspection_token
|
||||
or the introspection_credentials.
|
||||
|
||||
If the resulting access_token identifies a username (e.g. Authorization Code grant), add
|
||||
that user to the UserModel. Also cache the access_token up until its expiry time or a
|
||||
configured maximum time.
|
||||
|
||||
"""
|
||||
|
||||
headers = None
|
||||
response = None
|
||||
if introspection_token:
|
||||
headers = {"Authorization": "Bearer {}".format(introspection_token)}
|
||||
try:
|
||||
response = requests.post(
|
||||
introspection_url,
|
||||
data={"token": token}, headers=headers
|
||||
)
|
||||
except requests.exceptions.RequestException:
|
||||
log.exception("Introspection: Failed POST to %r in token lookup", introspection_url)
|
||||
return None
|
||||
|
||||
elif introspection_credentials:
|
||||
client_id = introspection_credentials[0].encode("utf-8")
|
||||
client_secret = introspection_credentials[1].encode("utf-8")
|
||||
basic_auth = base64.b64encode(client_id + b":" + client_secret)
|
||||
headers = {"Authorization": "Basic {}".format(basic_auth.decode("utf-8"))}
|
||||
try:
|
||||
response = requests.post(
|
||||
introspection_url,
|
||||
data={"token": token}, headers=headers
|
||||
)
|
||||
except requests.exceptions.RequestException:
|
||||
log.exception("Introspection: Failed POST to %r in token lookup", introspection_url)
|
||||
return None
|
||||
|
||||
elif introspection_client_id and introspection_client_secret:
|
||||
data = {"token": token}
|
||||
introspection_client = self.get_introspection_client(introspection_client_id, introspection_client_secret)
|
||||
response = introspection_client.request(url=introspection_url, method='post', data=data,
|
||||
required_scopes=['introspection'], login_required=True)
|
||||
|
||||
try:
|
||||
content: dict = response.json()
|
||||
except ValueError:
|
||||
log.exception("Introspection: Failed to parse response as json")
|
||||
return None
|
||||
|
||||
user = None
|
||||
if "active" in content and content["active"] is True:
|
||||
if "username" in content:
|
||||
user, content = oauth2_settings.INTROSPECTION_USER_CREATE_METHOD(token, content)
|
||||
|
||||
max_caching_time = datetime.now() + timedelta(
|
||||
seconds=oauth2_settings.RESOURCE_SERVER_TOKEN_CACHING_SECONDS
|
||||
)
|
||||
|
||||
if "exp" in content:
|
||||
expires = datetime.utcfromtimestamp(content["exp"])
|
||||
if expires > max_caching_time:
|
||||
expires = max_caching_time
|
||||
else:
|
||||
expires = max_caching_time
|
||||
|
||||
scope = content.get("scope", "")
|
||||
expires = make_aware(expires)
|
||||
|
||||
access_token, _created = AccessTokenModel.objects.update_or_create(
|
||||
token=token,
|
||||
defaults={
|
||||
"user": user,
|
||||
"application": None,
|
||||
"scope": scope,
|
||||
"expires": expires,
|
||||
"detail": content,
|
||||
})
|
||||
|
||||
# try:
|
||||
# access_token = AccessTokenModel.objects.select_related("application", "user").get(token=token)
|
||||
# except AccessTokenModel.DoesNotExist:
|
||||
# access_token = AccessTokenModel.objects.create(
|
||||
# user=user,
|
||||
# token=token,
|
||||
# application=None,
|
||||
# scope=scope,
|
||||
# expires=expires,
|
||||
# detail=content
|
||||
# )
|
||||
# else:
|
||||
# access_token.expires = expires
|
||||
# access_token.scope = scope
|
||||
# access_token.detail = content
|
||||
# access_token.save()
|
||||
|
||||
return access_token
|
||||
|
||||
# def validate_bearer_token(self, token, scopes, request):
|
||||
# """
|
||||
# When users try to access resources, check that provided token is valid
|
||||
# """
|
||||
# if not token:
|
||||
# return False
|
||||
#
|
||||
# introspection_url = oauth2_settings.RESOURCE_SERVER_INTROSPECTION_URL
|
||||
# introspection_token = oauth2_settings.RESOURCE_SERVER_AUTH_TOKEN
|
||||
# introspection_credentials = oauth2_settings.RESOURCE_SERVER_INTROSPECTION_CREDENTIALS
|
||||
# introspection_client_id = oauth2_settings.RESOURCE_SERVER_CLIENT_ID
|
||||
# introspection_client_secret = oauth2_settings.RESOURCE_SERVER_CLIENT_SECRET
|
||||
#
|
||||
# try:
|
||||
# access_token = AccessTokenModel.objects.select_related("application", "user").get(token=token)
|
||||
# except AccessTokenModel.DoesNotExist:
|
||||
# access_token = None
|
||||
#
|
||||
# # if there is no token or it's invalid then introspect the token if there's an external OAuth server
|
||||
# if not access_token or not access_token.is_valid(scopes):
|
||||
# if introspection_url and (introspection_token or introspection_credentials or (introspection_client_id and
|
||||
# introspection_client_secret)):
|
||||
# access_token = self._get_token_from_gooyal_authentication_server(
|
||||
# token,
|
||||
# introspection_url,
|
||||
# introspection_token,
|
||||
# introspection_credentials,
|
||||
# introspection_client_id,
|
||||
# introspection_client_secret
|
||||
# )
|
||||
#
|
||||
# if access_token and access_token.is_valid(scopes):
|
||||
# request.client = access_token.application
|
||||
# request.user = access_token.user
|
||||
# request.scopes = scopes
|
||||
#
|
||||
# # this is needed by django rest framework
|
||||
# request.access_token = access_token
|
||||
# return True
|
||||
# else:
|
||||
# self._set_oauth2_error_on_request(request, access_token, scopes)
|
||||
# return False
|
||||
|
||||
def _authenticate_basic_auth(self, request):
|
||||
"""
|
||||
Authenticates with HTTP Basic Auth.
|
||||
|
||||
Note: as stated in rfc:`2.3.1`, client_id and client_secret must be encoded with
|
||||
"application/x-www-form-urlencoded" encoding algorithm.
|
||||
"""
|
||||
auth_string = self._extract_basic_auth(request)
|
||||
if not auth_string:
|
||||
return False
|
||||
|
||||
try:
|
||||
encoding = request.encoding or settings.DEFAULT_CHARSET or "utf-8"
|
||||
except AttributeError:
|
||||
encoding = "utf-8"
|
||||
|
||||
try:
|
||||
b64_decoded = base64.b64decode(auth_string)
|
||||
except (TypeError, binascii.Error):
|
||||
log.debug("Failed basic auth: %r can't be decoded as base64", auth_string)
|
||||
return False
|
||||
|
||||
try:
|
||||
auth_string_decoded = b64_decoded.decode(encoding)
|
||||
except UnicodeDecodeError:
|
||||
log.debug("Failed basic auth: %r can't be decoded as unicode by %r", auth_string, encoding)
|
||||
return False
|
||||
|
||||
try:
|
||||
client_id, client_secret = map(unquote_plus, auth_string_decoded.split(":", 1))
|
||||
except ValueError:
|
||||
log.debug("Failed basic auth, Invalid base64 encoding.")
|
||||
return False
|
||||
|
||||
if self._load_application(client_id, request) is None:
|
||||
log.debug("Failed basic auth: Application %s does not exist" % client_id)
|
||||
return False
|
||||
elif request.client.client_id != client_id:
|
||||
log.debug("Failed basic auth: wrong client id %s" % client_id)
|
||||
return False
|
||||
|
||||
# TODO: check why not work
|
||||
elif not client_secret == request.client.client_secret:
|
||||
log.debug("Failed basic auth: wrong client secret %s" % client_secret)
|
||||
return False
|
||||
else:
|
||||
return True
|
||||
|
||||
def _authenticate_request_body(self, request):
|
||||
"""
|
||||
Try to authenticate the client using client_id and client_secret
|
||||
parameters included in body.
|
||||
|
||||
Remember that this method is NOT RECOMMENDED and SHOULD be limited to
|
||||
clients unable to directly utilize the HTTP Basic authentication scheme.
|
||||
See rfc:`2.3.1` for more details.
|
||||
"""
|
||||
# TODO: check if oauthlib has already unquoted client_id and client_secret
|
||||
try:
|
||||
client_id = request.client_id
|
||||
client_secret = request.client_secret
|
||||
except AttributeError:
|
||||
return False
|
||||
|
||||
if self._load_application(client_id, request) is None:
|
||||
log.debug("Failed body auth: Application %s does not exists" % client_id)
|
||||
return False
|
||||
# TODO: check why not work
|
||||
elif not client_secret == request.client.client_secret:
|
||||
log.debug("Failed body auth: wrong client secret %s" % client_secret)
|
||||
return False
|
||||
else:
|
||||
return True
|
||||
|
||||
13
apps/gooyal_oauth2/admin.py
Executable file
13
apps/gooyal_oauth2/admin.py
Executable file
|
|
@ -0,0 +1,13 @@
|
|||
"""
|
||||
Django admin configuration for the gooyal-restrict-scopes package.
|
||||
"""
|
||||
|
||||
from django.contrib import admin
|
||||
from django.contrib.admin.sites import NotRegistered
|
||||
|
||||
from oauth2_provider.admin import ApplicationAdmin
|
||||
|
||||
|
||||
|
||||
# The restricted application is registered by Django OAuth Toolkit, but we want
|
||||
# to provide our own admin that uses our form
|
||||
6
apps/gooyal_oauth2/apps.py
Executable file
6
apps/gooyal_oauth2/apps.py
Executable file
|
|
@ -0,0 +1,6 @@
|
|||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class GooyalOauthConfig(AppConfig):
|
||||
default_auto_field = 'django.db.models.BigAutoField'
|
||||
name = 'apps.gooyal_oauth2'
|
||||
9
apps/gooyal_oauth2/forms.py
Normal file
9
apps/gooyal_oauth2/forms.py
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
"""
|
||||
Django forms for use with the gooyal-restrict-scopes package.
|
||||
"""
|
||||
|
||||
from django import forms
|
||||
|
||||
from oauth2_provider.scopes import get_scopes_backend
|
||||
|
||||
|
||||
1
apps/gooyal_oauth2/models.py
Normal file
1
apps/gooyal_oauth2/models.py
Normal file
|
|
@ -0,0 +1 @@
|
|||
# models
|
||||
30
apps/gooyal_oauth2/validators.py
Executable file
30
apps/gooyal_oauth2/validators.py
Executable file
|
|
@ -0,0 +1,30 @@
|
|||
import base64
|
||||
import binascii
|
||||
import logging
|
||||
from datetime import datetime, timedelta
|
||||
from urllib.parse import unquote_plus
|
||||
|
||||
import requests
|
||||
# import service_clients
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.utils.timezone import make_aware
|
||||
from oauth2_provider.models import get_access_token_model
|
||||
from oauth2_provider.oauth2_validators import OAuth2Validator as BaseOAuth2Validator
|
||||
|
||||
log = logging.getLogger("oauth2_provider")
|
||||
|
||||
AccessTokenModel = get_access_token_model()
|
||||
UserModel = get_user_model()
|
||||
|
||||
class OAuth2Validator(BaseOAuth2Validator): # pylint: disable=w0223
|
||||
def get_or_create_user_from_content(self, content):
|
||||
"""
|
||||
An optional layer to define where to store the profile in `UserModel` or a separate model.
|
||||
For example `UserOAuth`, where `user = models.OneToOneField(UserModel)` .
|
||||
|
||||
The function is called after checking that username is in the content.
|
||||
|
||||
Returns an UserModel instance;
|
||||
"""
|
||||
user, _ = UserModel.objects.get_or_create(pk=content["username"])
|
||||
return user
|
||||
|
|
@ -2,31 +2,4 @@ from django.contrib import admin
|
|||
from .models import User
|
||||
|
||||
|
||||
class UserAdmin(admin.ModelAdmin):
|
||||
fields = [
|
||||
'uuid',
|
||||
'name',
|
||||
'avatar',
|
||||
'is_active',
|
||||
'first_name',
|
||||
'last_name',
|
||||
'username',
|
||||
'email',
|
||||
'phone_number',
|
||||
'password',
|
||||
'otp',
|
||||
'is_staff',
|
||||
'is_superuser',
|
||||
'groups',
|
||||
'user_permissions',
|
||||
'last_login',
|
||||
'date_joined',
|
||||
'last_update',
|
||||
'otp_expire',
|
||||
'otp_try',
|
||||
'balance',
|
||||
]
|
||||
readonly_fields = ['last_update', 'uuid']
|
||||
|
||||
|
||||
admin.site.register(User, UserAdmin)
|
||||
admin.site.register(User)
|
||||
|
|
|
|||
|
|
@ -2,4 +2,5 @@ from django.apps import AppConfig
|
|||
|
||||
|
||||
class UsersConfig(AppConfig):
|
||||
default_auto_field = 'django.db.models.BigAutoField'
|
||||
name = 'apps.users'
|
||||
|
|
|
|||
|
|
@ -1,137 +0,0 @@
|
|||
from django.contrib.auth import get_user_model
|
||||
from django.contrib.auth.backends import ModelBackend
|
||||
|
||||
UserModel = get_user_model()
|
||||
|
||||
class OTPBackend(ModelBackend):
|
||||
|
||||
"""
|
||||
Authenticates against settings.AUTH_USER_MODEL.
|
||||
"""
|
||||
|
||||
def authenticate(self, request, phone_number=None, otp=None, **kwargs):
|
||||
if phone_number is None or otp is None:
|
||||
return
|
||||
|
||||
try:
|
||||
user = UserModel.objects.get(phone_number=phone_number)
|
||||
# user = UserModel._default_manager.get_by_natural_key(phone_number)
|
||||
except UserModel.DoesNotExist:
|
||||
return
|
||||
|
||||
else:
|
||||
if user.check_otp(otp) and self.user_can_authenticate(user):
|
||||
return user
|
||||
|
||||
def user_can_authenticate(self, user):
|
||||
"""
|
||||
Reject users with is_active=False. Custom user models that don't have
|
||||
that attribute are allowed.
|
||||
"""
|
||||
is_active = getattr(user, 'is_active', None)
|
||||
return is_active or is_active is None
|
||||
|
||||
# def _get_user_permissions(self, user_obj):
|
||||
# return user_obj.user_permissions.all()
|
||||
#
|
||||
# def _get_group_permissions(self, user_obj):
|
||||
# user_groups_field = get_user_model()._meta.get_field('groups')
|
||||
# user_groups_query = 'group__%s' % user_groups_field.related_query_name()
|
||||
# return Permission.objects.filter(**{user_groups_query: user_obj})
|
||||
#
|
||||
# def _get_permissions(self, user_obj, obj, from_name):
|
||||
# """
|
||||
# Return the permissions of `user_obj` from `from_name`. `from_name` can
|
||||
# be either "group" or "user" to return permissions from
|
||||
# `_get_group_permissions` or `_get_user_permissions` respectively.
|
||||
# """
|
||||
# if not user_obj.is_active or user_obj.is_anonymous or obj is not None:
|
||||
# return set()
|
||||
#
|
||||
# perm_cache_name = '_%s_perm_cache' % from_name
|
||||
# if not hasattr(user_obj, perm_cache_name):
|
||||
# if user_obj.is_superuser:
|
||||
# perms = Permission.objects.all()
|
||||
# else:
|
||||
# perms = getattr(self, '_get_%s_permissions' % from_name)(user_obj)
|
||||
# perms = perms.values_list('content_type__app_label', 'codename').order_by()
|
||||
# setattr(user_obj, perm_cache_name, {"%s.%s" % (ct, name) for ct, name in perms})
|
||||
# return getattr(user_obj, perm_cache_name)
|
||||
#
|
||||
# def get_user_permissions(self, user_obj, obj=None):
|
||||
# """
|
||||
# Return a set of permission strings the user `user_obj` has from their
|
||||
# `user_permissions`.
|
||||
# """
|
||||
# return self._get_permissions(user_obj, obj, 'user')
|
||||
#
|
||||
# def get_group_permissions(self, user_obj, obj=None):
|
||||
# """
|
||||
# Return a set of permission strings the user `user_obj` has from the
|
||||
# groups they belong.
|
||||
# """
|
||||
# return self._get_permissions(user_obj, obj, 'group')
|
||||
#
|
||||
# def get_all_permissions(self, user_obj, obj=None):
|
||||
# if not user_obj.is_active or user_obj.is_anonymous or obj is not None:
|
||||
# return set()
|
||||
# if not hasattr(user_obj, '_perm_cache'):
|
||||
# user_obj._perm_cache = super().get_all_permissions(user_obj)
|
||||
# return user_obj._perm_cache
|
||||
#
|
||||
# def has_perm(self, user_obj, perm, obj=None):
|
||||
# return user_obj.is_active and super().has_perm(user_obj, perm, obj=obj)
|
||||
#
|
||||
# def has_module_perms(self, user_obj, app_label):
|
||||
# """
|
||||
# Return True if user_obj has any permissions in the given app_label.
|
||||
# """
|
||||
# return user_obj.is_active and any(
|
||||
# perm[:perm.index('.')] == app_label
|
||||
# for perm in self.get_all_permissions(user_obj)
|
||||
# )
|
||||
#
|
||||
# def with_perm(self, perm, is_active=True, include_superusers=True, obj=None):
|
||||
# """
|
||||
# Return users that have permission "perm". By default, filter out
|
||||
# inactive users and include superusers.
|
||||
# """
|
||||
# if isinstance(perm, str):
|
||||
# try:
|
||||
# app_label, codename = perm.split('.')
|
||||
# except ValueError:
|
||||
# raise ValueError(
|
||||
# 'Permission name should be in the form '
|
||||
# 'app_label.permission_codename.'
|
||||
# )
|
||||
# elif not isinstance(perm, Permission):
|
||||
# raise TypeError(
|
||||
# 'The `perm` argument must be a string or a permission instance.'
|
||||
# )
|
||||
#
|
||||
# UserModel = get_user_model()
|
||||
# if obj is not None:
|
||||
# return UserModel._default_manager.none()
|
||||
#
|
||||
# permission_q = Q(group__user=OuterRef('pk')) | Q(user=OuterRef('pk'))
|
||||
# if isinstance(perm, Permission):
|
||||
# permission_q &= Q(pk=perm.pk)
|
||||
# else:
|
||||
# permission_q &= Q(codename=codename, content_type__app_label=app_label)
|
||||
#
|
||||
# user_q = Exists(Permission.objects.filter(permission_q))
|
||||
# if include_superusers:
|
||||
# user_q |= Q(is_superuser=True)
|
||||
# if is_active is not None:
|
||||
# user_q &= Q(is_active=is_active)
|
||||
#
|
||||
# return UserModel._default_manager.filter(user_q)
|
||||
#
|
||||
# def get_user(self, user_id):
|
||||
# try:
|
||||
# user = UserModel._default_manager.get(pk=user_id)
|
||||
# except UserModel.DoesNotExist:
|
||||
# return None
|
||||
# return user if self.user_can_authenticate(user) else None
|
||||
#
|
||||
#
|
||||
1250
apps/users/cities.py
1250
apps/users/cities.py
File diff suppressed because it is too large
Load diff
|
|
@ -1,2 +0,0 @@
|
|||
MAX_OTP_TRY = 3
|
||||
DEVELOPMENT_PHONE_NUMBERS = ['+989999999999', '+989999999998']
|
||||
|
|
@ -6,85 +6,3 @@ from django.utils.translation import gettext_lazy as _
|
|||
|
||||
UserModel = get_user_model()
|
||||
|
||||
class OTPAuthenticationForm(forms.Form):
|
||||
"""
|
||||
Base class for authenticating users. Extend this to get a form that accepts
|
||||
username/password logins.
|
||||
"""
|
||||
phone_number = UsernameField(widget=forms.TextInput(attrs={'autofocus': True}))
|
||||
otp = forms.CharField(
|
||||
label=_("otp"),
|
||||
strip=False,
|
||||
widget=forms.PasswordInput(attrs={'autocomplete': 'current-password'}),
|
||||
)
|
||||
|
||||
error_messages = {
|
||||
'invalid_login': _(
|
||||
"Please enter a correct %(phone_number)s and otp. Note that both "
|
||||
"fields may be case-sensitive."
|
||||
),
|
||||
'inactive': _("This account is inactive."),
|
||||
}
|
||||
|
||||
def __init__(self, request=None, *args, **kwargs):
|
||||
"""
|
||||
The 'request' parameter is set for custom auth use by subclasses.
|
||||
The form data comes in via the standard 'data' kwarg.
|
||||
"""
|
||||
self.request = request
|
||||
self.user_cache = None
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
# Set the max length and label for the "username" field.
|
||||
self.phone_number_field = UserModel._meta.get_field('phone_number')
|
||||
phone_number_max_length = self.phone_number_field.max_length or 24
|
||||
self.fields['phone_number'].max_length = phone_number_max_length
|
||||
self.fields['phone_number'].widget.attrs['maxlength'] = phone_number_max_length
|
||||
if self.fields['phone_number'].label is None:
|
||||
self.fields['phone_number'].label = capfirst(self.phone_number_field.verbose_name)
|
||||
|
||||
def clean(self):
|
||||
phone_number = self.cleaned_data.get('phone_number')
|
||||
otp = self.cleaned_data.get('otp')
|
||||
|
||||
if phone_number is not None and otp:
|
||||
self.user_cache = authenticate(self.request, phone_number=phone_number, otp=otp)
|
||||
if self.user_cache is None:
|
||||
raise self.get_invalid_login_error()
|
||||
else:
|
||||
self.confirm_login_allowed(self.user_cache)
|
||||
|
||||
return self.cleaned_data
|
||||
|
||||
def confirm_login_allowed(self, user):
|
||||
"""
|
||||
Controls whether the given User may log in. This is a policy setting,
|
||||
independent of end-user authentication. This default behavior is to
|
||||
allow login by active users, and reject login by inactive users.
|
||||
|
||||
If the given user cannot log in, this method should raise a
|
||||
``forms.ValidationError``.
|
||||
|
||||
If the given user may log in, this method should return None.
|
||||
"""
|
||||
if not user.is_active:
|
||||
raise forms.ValidationError(
|
||||
self.error_messages['inactive'],
|
||||
code='inactive',
|
||||
)
|
||||
|
||||
def get_user(self):
|
||||
return self.user_cache
|
||||
|
||||
def get_invalid_login_error(self):
|
||||
return forms.ValidationError(
|
||||
self.error_messages['invalid_login'],
|
||||
code='invalid_login',
|
||||
params={'phone_number': self.phone_number_field.verbose_name},
|
||||
)
|
||||
|
||||
|
||||
class ProfileUpdateForm(forms.ModelForm):
|
||||
class Meta:
|
||||
model = UserModel
|
||||
fields = ['name', 'first_name', 'last_name', 'email', 'iban', 'avatar']
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
# Generated by Django 4.1 on 2022-08-17 19:05
|
||||
# Generated by Django 5.0.6 on 2024-07-03 14:33
|
||||
|
||||
import apps.users.models
|
||||
import django.contrib.auth.validators
|
||||
from django.db import migrations, models
|
||||
import django.utils.timezone
|
||||
import uuid
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
|
@ -20,28 +20,18 @@ class Migration(migrations.Migration):
|
|||
name='User',
|
||||
fields=[
|
||||
('is_superuser', models.BooleanField(default=False, help_text='Designates that this user has all permissions without explicitly assigning them.', verbose_name='superuser status')),
|
||||
('uuid', models.UUIDField(db_index=True, default=uuid.uuid4, editable=False, primary_key=True, serialize=False, unique=True)),
|
||||
('name', models.CharField(blank=True, max_length=30, verbose_name='name')),
|
||||
('first_name', models.CharField(blank=True, max_length=30, verbose_name='first name')),
|
||||
('first_name', models.CharField(blank=True, max_length=150, 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')),
|
||||
('province', models.IntegerField(blank=True, null=True, verbose_name='Province')),
|
||||
('city', models.IntegerField(blank=True, null=True, verbose_name='City')),
|
||||
('postal_code', models.CharField(blank=True, max_length=20, null=True, verbose_name='postal_code')),
|
||||
('address', models.TextField(blank=True, null=True, verbose_name='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')),
|
||||
('uuid', models.UUIDField(db_index=True, default=uuid.uuid4, editable=False, primary_key=True, serialize=False, unique=True)),
|
||||
('password', models.CharField(max_length=128, verbose_name='password')),
|
||||
('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')),
|
||||
('balance', models.BigIntegerField(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')),
|
||||
('last_update', models.DateTimeField(auto_now=True, max_length=30, null=True, verbose_name='last update')),
|
||||
('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')),
|
||||
('date_joined', models.DateTimeField(default=django.utils.timezone.now, verbose_name='date joined')),
|
||||
|
|
|
|||
|
|
@ -1,18 +0,0 @@
|
|||
# Generated by Django 4.1 on 2022-09-08 13:01
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('users', '0001_initial'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='user',
|
||||
name='email_verified',
|
||||
field=models.BooleanField(null=True),
|
||||
),
|
||||
]
|
||||
|
|
@ -1,23 +0,0 @@
|
|||
# Generated by Django 4.1 on 2022-09-15 07:28
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('users', '0002_user_email_verified'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='user',
|
||||
name='ott',
|
||||
field=models.CharField(blank=True, max_length=36, null=True, verbose_name='one time token'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='user',
|
||||
name='ott_expire',
|
||||
field=models.DateTimeField(blank=True, null=True, verbose_name='otp expire'),
|
||||
),
|
||||
]
|
||||
|
|
@ -1,18 +0,0 @@
|
|||
# Generated by Django 4.1 on 2022-09-25 19:05
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('users', '0003_user_ott_user_ott_expire'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='user',
|
||||
name='balance',
|
||||
field=models.BigIntegerField(default=0, verbose_name='balance'),
|
||||
),
|
||||
]
|
||||
|
|
@ -12,40 +12,22 @@ from django.utils import timezone
|
|||
from django.utils.translation import gettext_lazy as _
|
||||
import uuid
|
||||
|
||||
from .provinces_and_cities import state
|
||||
# from .tasks import send_notification
|
||||
|
||||
from apps.users.constans import MAX_OTP_TRY, DEVELOPMENT_PHONE_NUMBERS
|
||||
|
||||
|
||||
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 and not username:
|
||||
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()
|
||||
def _create_user(self, pk, **extra_fields):
|
||||
user = self.model(pk=pk **extra_fields)
|
||||
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):
|
||||
|
||||
def create_user(self, pk, **extra_fields):
|
||||
extra_fields.setdefault('is_staff', False)
|
||||
extra_fields.setdefault('is_superuser', False)
|
||||
|
||||
return self._create_user(phone_number=phone_number,
|
||||
username=username,
|
||||
email=email,
|
||||
password=password,
|
||||
**extra_fields)
|
||||
return self._create_user(pk=pk, **extra_fields)
|
||||
|
||||
def create_superuser(self, username, email, password, **extra_fields):
|
||||
if not username:
|
||||
|
|
@ -64,36 +46,6 @@ class UserManager(BaseUserManager):
|
|||
|
||||
class User(AbstractUser):
|
||||
uuid = models.UUIDField(primary_key=True, editable=False, default=uuid.uuid4, unique=True, db_index=True)
|
||||
name = models.CharField(_('name'), max_length=30, blank=True)
|
||||
first_name = models.CharField(_('first name'), max_length=30, blank=True)
|
||||
last_name = models.CharField(_('last name'), max_length=150, blank=True)
|
||||
email = models.EmailField(_('email address'), blank=True)
|
||||
email_verified = models.BooleanField(null=True)
|
||||
|
||||
# country = models.CharField(_('country'), max_length=50, blank=True, null=True)
|
||||
# province = models.CharField(_('province'), max_length=50, blank=True, null=True)
|
||||
# city = models.CharField(_('city'), max_length=50, blank=True, null=True)
|
||||
# postal_code = models.CharField(_('postal_code'), max_length=20, blank=True, null=True)
|
||||
# address = models.TextField(_('address'), null=True, blank=True)
|
||||
|
||||
province = models.IntegerField(verbose_name=_("Province"), blank=True, null=True)
|
||||
city = models.IntegerField(verbose_name=_("City"), blank=True, null=True)
|
||||
postal_code = models.CharField(_('postal_code'), max_length=20, blank=True, null=True)
|
||||
address = models.TextField(_('address'), null=True, blank=True)
|
||||
|
||||
is_staff = models.BooleanField(
|
||||
_('staff status'),
|
||||
default=False,
|
||||
help_text=_('Designates whether the user can log into this admin site.'),
|
||||
)
|
||||
is_active = models.BooleanField(
|
||||
_('active'),
|
||||
default=True,
|
||||
help_text=_(
|
||||
'Designates whether this user should be treated as active. '
|
||||
'Unselect this instead of deleting accounts.'
|
||||
),
|
||||
)
|
||||
password = models.CharField(_('password'), max_length=128)
|
||||
username_validator = UnicodeUsernameValidator()
|
||||
username = models.CharField(
|
||||
|
|
@ -108,157 +60,17 @@ class User(AbstractUser):
|
|||
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)
|
||||
|
||||
ott = models.CharField(_('one time token'), max_length=36, blank=True, null=True)
|
||||
ott_expire = models.DateTimeField(_('otp expire'), blank=True, null=True)
|
||||
|
||||
last_checkout_request = models.DateTimeField(_('otp expire'), max_length=30, blank=True, null=True)
|
||||
balance = models.BigIntegerField(_('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)
|
||||
|
||||
last_update = models.DateTimeField(_('last update'), max_length=30, blank=True, null=True, auto_now=True)
|
||||
last_login = models.DateTimeField(_('last login'), blank=True, null=True)
|
||||
date_joined = models.DateTimeField(_('date joined'), default=timezone.now)
|
||||
|
||||
objects = UserManager()
|
||||
|
||||
@property
|
||||
def city_name(self):
|
||||
return state.get_city_by_id(self.city)
|
||||
|
||||
@property
|
||||
def province_name(self):
|
||||
return state.get_province_by_id(self.province)
|
||||
|
||||
def set_otp(self):
|
||||
if settings.DEBUG or 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
|
||||
|
||||
def set_ott(self):
|
||||
self.ott = ''.join(random.choice(string.ascii_letters+string.digits) for _ in range(32))
|
||||
# self.ott = ''.join(random.choice(string.ascii_letters+string.digits+string.punctuation) for _ in range(32))
|
||||
self.ott_expire = timezone.now() + timedelta(seconds=300)
|
||||
|
||||
def otp_is_valid(self):
|
||||
return bool(self.otp and timezone.now() <= self.otp_expire)
|
||||
|
||||
def ott_is_valid(self):
|
||||
return bool(self.ott and timezone.now() <= self.ott_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_ott(self, ott:str):
|
||||
ott = ott.strip()
|
||||
if ott and self.ott == ott and self.ott_is_valid():
|
||||
self.ott = None
|
||||
self.date_joined = timezone.now()
|
||||
result = True
|
||||
|
||||
else:
|
||||
self.ott = None
|
||||
result = False
|
||||
|
||||
self.save()
|
||||
return result
|
||||
|
||||
def check_auth(self, field, value):
|
||||
result = False
|
||||
if field == 'otp':
|
||||
result = self.check_otp(value)
|
||||
|
||||
elif field == 'password':
|
||||
result = self.check_password(value)
|
||||
|
||||
elif field == 'ott':
|
||||
result = self.check_ott(value)
|
||||
|
||||
if result:
|
||||
self.last_login = timezone.now()
|
||||
self.save()
|
||||
|
||||
return result
|
||||
|
||||
def send_otp(self):
|
||||
print(self.otp)
|
||||
self.notify(body=str(self.otp), title='OTP')
|
||||
|
||||
def notify(self, body, title=None, notification_type='sms'):
|
||||
# TODO: enable celery
|
||||
send_notification(phone_number=self.phone_number,
|
||||
title=title,
|
||||
body=body,
|
||||
user_uuid=self.uuid,
|
||||
notification_type=1,
|
||||
)
|
||||
|
||||
def __str__(self):
|
||||
return self.name or self.get_full_name() or self.username or self.phone_number or self.email or _('no name')
|
||||
|
||||
|
||||
def create_user_in_introspection(token, content):
|
||||
# content = {
|
||||
# "active": True,
|
||||
# "scope": "read write email",
|
||||
# "client_id": "J8NFmU4tJVgDxKaJFmXTWvaHO",
|
||||
# "username": "aaronpk",
|
||||
# "exp": 1437275311
|
||||
# }
|
||||
|
||||
user_client = service_clients.Client(access_token=token,
|
||||
scopes=content.get('scope', '').split(),
|
||||
expires_in=100)
|
||||
|
||||
user_account = user_client.accounts.account()
|
||||
if user_account.get('uuid'):
|
||||
content.update(user_account)
|
||||
|
||||
# content = {'active': True, 'scope': 'ipg.payment:submit accounts.account:retrieve', 'exp': 1602619137,
|
||||
# 'client_id': 'bd2hEGXytrqMjbFnplRyHJTeoW1vwKzCZJtH6ro0', 'username': '',
|
||||
# 'uuid': '94255117-117c-4a9f-af50-35a6c47503ac', 'email': '', 'phone_number': '+989106853582',
|
||||
# 'first_name': '', 'last_name': '', 'name': '', 'balance': 0,
|
||||
# 'avatar': 'http://accounts.gooyal.com/media/avatars/1691899.jpg', 'iban': '', 'iban_verified': None}
|
||||
|
||||
user = User.objects.filter(uuid=content['uuid']).first()
|
||||
if user:
|
||||
pass
|
||||
|
||||
else:
|
||||
user = User.objects.create_user(
|
||||
**{User.USERNAME_FIELD: content["username"]},
|
||||
uuid=content['uuid'],
|
||||
email=content['email'],
|
||||
phone_number=content['phone_number'],
|
||||
first_name=content['first_name'],
|
||||
last_name=content['last_name'],
|
||||
name=content['name'],
|
||||
avatar=content['avatar']
|
||||
)
|
||||
return user, content
|
||||
return str(self.username or self.pk)
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -5,117 +5,13 @@ from apps.users.models import User
|
|||
from django.utils import timezone
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
phone_number_validator = RegexValidator(regex=r'^\+98\d{10,10}$', message=_(
|
||||
"Phone number must be entered in the format: '+989999999999'."
|
||||
),)
|
||||
class PublicUserSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = User
|
||||
fields = ('avatar', 'name', 'username', "first_name", "last_name", "uuid")
|
||||
read_only_fields = ['avatar', 'name', 'username', 'first_name', 'last_name', "uuid"]
|
||||
|
||||
|
||||
class AccountSerializer(serializers.ModelSerializer):
|
||||
phone_number = serializers.CharField(validators=[phone_number_validator], required=False, read_only=True)
|
||||
class Meta:
|
||||
model = User
|
||||
fields = (
|
||||
"uuid",
|
||||
'username',
|
||||
'email',
|
||||
'phone_number',
|
||||
"first_name",
|
||||
"last_name",
|
||||
'name',
|
||||
'avatar',
|
||||
'iban',
|
||||
'iban_verified',
|
||||
# 'country',
|
||||
'province',
|
||||
'province_name',
|
||||
'city',
|
||||
'city_name',
|
||||
'postal_code',
|
||||
'address',
|
||||
)
|
||||
read_only_fields = ['uuid', 'email', 'phone_number', 'iban_verified', 'province_name', 'city_name',]
|
||||
|
||||
|
||||
class RequestOTPSerializer(serializers.ModelSerializer):
|
||||
phone_number = serializers.CharField(required=True, validators=[phone_number_validator])
|
||||
ttl = serializers.SerializerMethodField()
|
||||
|
||||
class Meta:
|
||||
model = User
|
||||
fields = (
|
||||
'phone_number',
|
||||
'otp_expire',
|
||||
'ttl'
|
||||
)
|
||||
|
||||
read_only_fields = ['otp_expire', 'ttl']
|
||||
|
||||
def save(self, **kwargs):
|
||||
# TODO: move it to query set
|
||||
validated_data = self.validated_data
|
||||
user = User.objects.filter(phone_number=validated_data['phone_number']).first()
|
||||
if not user:
|
||||
validated_data['username'] = validated_data['phone_number']
|
||||
user = User.objects.create_user(**validated_data)
|
||||
|
||||
if not user.otp_is_valid():
|
||||
user.set_otp()
|
||||
user.save()
|
||||
|
||||
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 RequestOTTSerializer(serializers.ModelSerializer):
|
||||
application = serializers.CharField(required=True, write_only=True)
|
||||
class Meta:
|
||||
model = User
|
||||
fields = (
|
||||
'application',
|
||||
'ott',
|
||||
'ott_expire',
|
||||
)
|
||||
|
||||
read_only_fields = ['ott', 'ott_expire']
|
||||
|
||||
|
||||
class UserInquirySerializer(serializers.ModelSerializer):
|
||||
phone_number = serializers.CharField(required=True, validators=[phone_number_validator])
|
||||
|
||||
class Meta:
|
||||
model = User
|
||||
fields = ('avatar', 'name', 'username', "first_name", "last_name", "uuid", 'phone_number')
|
||||
read_only_fields = ['avatar', 'name', 'username', 'first_name', 'last_name', "uuid"]
|
||||
|
||||
def save(self, **kwargs):
|
||||
# TODO: move it to query set
|
||||
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)
|
||||
|
||||
self.instance = user
|
||||
return user
|
||||
|
||||
|
||||
class ChangePasswordSerializer(serializers.Serializer):
|
||||
old_password = serializers.CharField(required=True)
|
||||
old_password_field = serializers.CharField(default='password')
|
||||
new_password = serializers.CharField(required=True)
|
||||
read_only_fields = ['uuid', 'iban_verified']
|
||||
|
|
|
|||
|
|
@ -1,28 +0,0 @@
|
|||
# from celery import shared_task
|
||||
# from django.conf import settings
|
||||
# # from service_clients import Client, AccountsClient
|
||||
#
|
||||
# # SCOPES = ['notifications.notification:submit']
|
||||
# # client = Client(client_id=settings.CLIENT_ID,
|
||||
# # client_secret=settings.CLIENT_SECRET,
|
||||
# # scopes=SCOPES,
|
||||
# # grant_type=AccountsClient.GRANT_CLIENT_CREDENTIALS)
|
||||
#
|
||||
# @shared_task
|
||||
# def send_notification(phone_number=None,
|
||||
# title=None,
|
||||
# body=None,
|
||||
# user_uuid=None,
|
||||
# email=None,
|
||||
# notification_type=1):
|
||||
#
|
||||
# data = {
|
||||
# 'phone_number': phone_number,
|
||||
# "body": f'{title}:\n{body}'
|
||||
# }
|
||||
# try:
|
||||
# print(client.request(required_scopes=SCOPES, url=f'{settings.BASE_NOTIFICATION_URL}/notifications/', data=data,
|
||||
# method='post', timeout=5).text)
|
||||
# except Exception as e:
|
||||
# print(e)
|
||||
#
|
||||
|
|
@ -1,19 +1,8 @@
|
|||
from django.urls import path
|
||||
from django.contrib.auth.views import LogoutView
|
||||
from .views import UserListView, UserDetailView, AccountView, RequestOTPView, ChangePasswordView, \
|
||||
OTPLoginView, ProfileDetailView, ProfileUpdateView, RequestOTTView
|
||||
from .views import AccountView
|
||||
|
||||
app_name = "users"
|
||||
|
||||
urlpatterns = [
|
||||
path('login/', OTPLoginView.as_view(), name='login'),
|
||||
path('logout/', LogoutView.as_view(), name='logout'),
|
||||
path('profile/', ProfileDetailView.as_view(), name='profile_detail'),
|
||||
path('profile/update/', ProfileUpdateView.as_view(), name='profile_update'),
|
||||
path('api/account/', AccountView.as_view(), name='account_api'),
|
||||
path('api/users/', UserListView.as_view(), name='user_list_api'),
|
||||
path('api/users/<uuid>/', UserDetailView.as_view(), name='user_detail_api'),
|
||||
path('api/request_otp/', RequestOTPView.as_view(), name='request_otp_api'),
|
||||
path('api/request_ott/', RequestOTTView.as_view(), name='request_ott_api'),
|
||||
path('api/change_password/', ChangePasswordView.as_view(), name='change_password_api'),
|
||||
]
|
||||
|
|
|
|||
|
|
@ -11,35 +11,13 @@ from rest_framework import generics, status, permissions
|
|||
from rest_framework.response import Response
|
||||
from rest_framework.views import APIView
|
||||
|
||||
from apps.accounts_oauth2.rest_framework import IsAuthenticatedOrTokenMatchesOASRequirements
|
||||
from apps.users.forms import OTPAuthenticationForm, ProfileUpdateForm
|
||||
from apps.gooyal_oauth2.rest_framework import IsAuthenticatedOrTokenMatchesOASRequirements
|
||||
from apps.users.models import User
|
||||
from apps.users.provinces_and_cities import State
|
||||
from apps.users.serializers import PublicUserSerializer, AccountSerializer, RequestOTPSerializer, RequestOTTSerializer, \
|
||||
ChangePasswordSerializer, UserInquirySerializer
|
||||
from apps.users.serializers import AccountSerializer
|
||||
|
||||
UserModel = get_user_model()
|
||||
|
||||
|
||||
class UserListView(generics.ListAPIView):
|
||||
permission_classes = [IsAuthenticatedOrTokenMatchesOASRequirements]
|
||||
queryset = User.objects.all()
|
||||
serializer_class = PublicUserSerializer
|
||||
required_alternate_scopes = {
|
||||
"GET": [["accounts.profile:list"]],
|
||||
}
|
||||
|
||||
|
||||
class UserDetailView(generics.RetrieveAPIView):
|
||||
permission_classes = [IsAuthenticatedOrTokenMatchesOASRequirements]
|
||||
queryset = User.objects.all()
|
||||
serializer_class = PublicUserSerializer
|
||||
lookup_field = 'uuid'
|
||||
required_alternate_scopes = {
|
||||
"GET": [["accounts.profile:retrieve"]],
|
||||
}
|
||||
|
||||
|
||||
class AccountView(generics.RetrieveUpdateAPIView):
|
||||
serializer_class = AccountSerializer
|
||||
permission_classes = [IsAuthenticatedOrTokenMatchesOASRequirements]
|
||||
|
|
@ -57,72 +35,6 @@ class AccountView(generics.RetrieveUpdateAPIView):
|
|||
serializer.save(last_update=timezone.now())
|
||||
|
||||
|
||||
class RequestOTPView(generics.CreateAPIView):
|
||||
permission_classes = []
|
||||
serializer_class = RequestOTPSerializer
|
||||
required_scopes = []
|
||||
|
||||
|
||||
class RequestOTTView(generics.CreateAPIView):
|
||||
permission_classes = [IsAuthenticatedOrTokenHasScope]
|
||||
required_scopes = ['accounts.account:request_ott']
|
||||
serializer_class = RequestOTTSerializer
|
||||
|
||||
def get_object(self):
|
||||
return self.request.user
|
||||
|
||||
def create(self, request, *args, **kwargs):
|
||||
instance: User = self.get_object()
|
||||
serializer = self.get_serializer(instance, data=request.data)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
instance.set_ott()
|
||||
instance.save()
|
||||
return Response(serializer.data)
|
||||
|
||||
|
||||
class UserInquiryView(generics.CreateAPIView):
|
||||
serializer_class = UserInquirySerializer
|
||||
permission_classes = [IsAuthenticatedOrTokenHasScope]
|
||||
required_scopes = ['accounts.profile:inquiry']
|
||||
|
||||
|
||||
class ChangePasswordView(generics.UpdateAPIView):
|
||||
permission_classes = [IsAuthenticatedOrTokenHasScope]
|
||||
serializer_class = ChangePasswordSerializer
|
||||
required_scopes = ["accounts.account:change_password"]
|
||||
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():
|
||||
pass_field = serializer.data.get("old_password_field")
|
||||
old_password = serializer.data.get("old_password")
|
||||
new_password = serializer.data.get("new_password")
|
||||
if not self.object.check_auth(pass_field, 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)
|
||||
|
||||
|
||||
class OTPLoginView(LoginView):
|
||||
template_name = 'users/login.html'
|
||||
"""
|
||||
Display the login form and handle the login action.
|
||||
"""
|
||||
form_class = OTPAuthenticationForm
|
||||
|
||||
|
||||
@method_decorator(login_required, name='dispatch')
|
||||
class ProfileDetailView(DetailView):
|
||||
model = User
|
||||
|
|
@ -131,20 +43,3 @@ class ProfileDetailView(DetailView):
|
|||
|
||||
def get_object(self, queryset=None):
|
||||
return self.request.user
|
||||
|
||||
|
||||
@method_decorator(login_required, name='dispatch')
|
||||
class ProfileUpdateView(UpdateView):
|
||||
model = User
|
||||
template_name = 'users/profile_update.html'
|
||||
pk_url_kwarg = 'uuid'
|
||||
form_class = ProfileUpdateForm
|
||||
|
||||
def get_success_url(self):
|
||||
return reverse('users:profile_update')
|
||||
|
||||
def get_object(self, queryset=None):
|
||||
return self.request.user
|
||||
|
||||
def form_valid(self, form):
|
||||
return super().form_valid(form)
|
||||
|
|
|
|||
|
|
@ -1,21 +1,4 @@
|
|||
from django.contrib import admin
|
||||
from .models import Transaction
|
||||
|
||||
|
||||
class TransactionAdmin(admin.ModelAdmin):
|
||||
fields = [
|
||||
'uuid',
|
||||
'amount',
|
||||
'payer',
|
||||
'payee',
|
||||
'create',
|
||||
'last_update',
|
||||
'state',
|
||||
'delay',
|
||||
'detail_id',
|
||||
'application'
|
||||
]
|
||||
readonly_fields = ['uuid', 'last_update', 'create', 'detail_id', 'application']
|
||||
|
||||
|
||||
admin.site.register(Transaction, TransactionAdmin)
|
||||
admin.site.register(Transaction)
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
# Generated by Django 4.1 on 2022-09-04 12:55
|
||||
# Generated by Django 5.0.6 on 2024-07-03 14:33
|
||||
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
import uuid
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
|
@ -12,6 +12,7 @@ class Migration(migrations.Migration):
|
|||
|
||||
dependencies = [
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
migrations.swappable_dependency(settings.OAUTH2_PROVIDER_APPLICATION_MODEL),
|
||||
]
|
||||
|
||||
operations = [
|
||||
|
|
@ -24,8 +25,8 @@ class Migration(migrations.Migration):
|
|||
('state', models.IntegerField(choices=[(1, 'created'), (2, 'delayed'), (3, 'pending'), (4, 'incomplete'), (5, 'success'), (6, 'failed'), (7, 'expected_failure')], default=1)),
|
||||
('create', models.DateTimeField(auto_now=True, null=True, verbose_name='create')),
|
||||
('last_update', models.DateTimeField(auto_now=True, null=True, verbose_name='last update')),
|
||||
('application_client_id', models.CharField(db_index=True, max_length=100, null=True)),
|
||||
('detail_id', models.IntegerField(blank=True, null=True)),
|
||||
('application', models.ForeignKey(null=True, on_delete=django.db.models.deletion.PROTECT, related_name='transactions', to=settings.OAUTH2_PROVIDER_APPLICATION_MODEL)),
|
||||
('payee', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='receipts', to=settings.AUTH_USER_MODEL)),
|
||||
('payer', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='payments', to=settings.AUTH_USER_MODEL)),
|
||||
],
|
||||
|
|
|
|||
|
|
@ -1,25 +0,0 @@
|
|||
# Generated by Django 4.1 on 2022-09-08 12:16
|
||||
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
migrations.swappable_dependency(settings.OAUTH2_PROVIDER_APPLICATION_MODEL),
|
||||
('wallet', '0001_initial'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RemoveField(
|
||||
model_name='transaction',
|
||||
name='application_client_id',
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='transaction',
|
||||
name='application',
|
||||
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.PROTECT, related_name='transactions', to=settings.OAUTH2_PROVIDER_APPLICATION_MODEL),
|
||||
),
|
||||
]
|
||||
|
|
@ -7,6 +7,7 @@ from django.db.models import F
|
|||
from django.utils.translation import gettext_lazy as _
|
||||
from rest_framework.exceptions import APIException, ValidationError
|
||||
from django.conf import settings
|
||||
from oauth2_provider import settings as oauth2_settings
|
||||
from .constans import StateChoices
|
||||
from apps.users.models import User
|
||||
|
||||
|
|
@ -58,7 +59,7 @@ class Transaction(models.Model):
|
|||
state = models.IntegerField(choices=StateChoices.choices, default=StateChoices.CREATED)
|
||||
create = models.DateTimeField(_('create'), blank=True, null=True, auto_now=True)
|
||||
last_update = models.DateTimeField(_('last update'), blank=True, null=True, auto_now=True)
|
||||
application = models.ForeignKey(settings.OAUTH2_PROVIDER_APPLICATION_MODEL, on_delete=models.PROTECT,
|
||||
application = models.ForeignKey(oauth2_settings.APPLICATION_MODEL, on_delete=models.PROTECT,
|
||||
related_name='transactions', null=True)
|
||||
detail_id = models.IntegerField(null=True, blank=True) # TODO: or better detail url
|
||||
|
||||
|
|
|
|||
|
|
@ -3,15 +3,11 @@ from django.db.transaction import atomic
|
|||
from rest_framework import serializers
|
||||
from rest_framework.exceptions import APIException
|
||||
|
||||
from apps.users.serializers import PublicUserSerializer
|
||||
from .models import Transaction
|
||||
from ..users.models import User
|
||||
|
||||
|
||||
class TransactionSerializer(serializers.ModelSerializer):
|
||||
payer = PublicUserSerializer(required=False)
|
||||
payee = PublicUserSerializer(required=False, read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = Transaction
|
||||
fields = ('uuid',
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ from oauth2_provider.contrib.rest_framework import TokenHasScope, IsAuthenticate
|
|||
from rest_framework import generics, permissions
|
||||
from rest_framework.response import Response
|
||||
|
||||
from apps.accounts_oauth2.rest_framework import IsAuthenticatedOrTokenMatchesOASRequirements
|
||||
from apps.gooyal_oauth2.rest_framework import IsAuthenticatedOrTokenMatchesOASRequirements
|
||||
from apps.wallet.models import Transaction
|
||||
from apps.wallet.serializers import TransactionSerializer, DepositSerializer, WithdrawSerializer, WalletSerializer
|
||||
from .constans import StateChoices
|
||||
|
|
|
|||
|
|
@ -35,18 +35,7 @@
|
|||
aria-controls="navbarNav" aria-expanded="false" aria-label="Toggle navigation">
|
||||
<span class="navbar-toggler-icon"></span>
|
||||
</button>
|
||||
<div class="collapse navbar-collapse" id="navbarNav">
|
||||
<ul class="navbar-nav">
|
||||
<li class="nav-item active">
|
||||
<a class="nav-link" href="{% url 'superapp:app_list' %}">home <span class="sr-only"></span></a>
|
||||
</li>
|
||||
{% if request.user.is_authenticated %}
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="{% url 'users:logout' %}">({{ request.user.phone_number }}) Logout</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
</nav>
|
||||
<div class="font-vazir container mb-3 mt-3">
|
||||
{% include "include/message_frame.html" %}
|
||||
|
|
|
|||
|
|
@ -52,8 +52,7 @@ INSTALLED_APPS = [
|
|||
# local apps
|
||||
'apps.users',
|
||||
'apps.wallet',
|
||||
'apps.accounts_oauth2',
|
||||
'apps.superapp',
|
||||
'apps.gooyal_oauth2',
|
||||
]
|
||||
|
||||
MIDDLEWARE = [
|
||||
|
|
@ -68,22 +67,25 @@ MIDDLEWARE = [
|
|||
'corsheaders.middleware.CorsMiddleware',
|
||||
]
|
||||
|
||||
# Tell Django OAuth Toolkit to use the Gooyal Application model
|
||||
OAUTH2_PROVIDER_ID_TOKEN_MODEL = 'accounts_oauth2.IDToken'
|
||||
OAUTH2_PROVIDER_APPLICATION_MODEL = 'accounts_oauth2.Application'
|
||||
OAUTH2_PROVIDER_ACCESS_TOKEN_MODEL = 'accounts_oauth2.AccessToken'
|
||||
OAUTH2_PROVIDER_GRANT_MODEL = 'accounts_oauth2.Grant'
|
||||
OAUTH2_PROVIDER_REFRESH_TOKEN_MODEL = 'accounts_oauth2.RefreshToken'
|
||||
|
||||
OAUTH2_PROVIDER_APPLICATION_MODEL = "oauth2_provider.Application"
|
||||
OAUTH2_PROVIDER_ACCESS_TOKEN_MODEL = "oauth2_provider.AccessToken"
|
||||
OAUTH2_PROVIDER_ID_TOKEN_MODEL = "oauth2_provider.IDToken"
|
||||
OAUTH2_PROVIDER_GRANT_MODEL = "oauth2_provider.Grant"
|
||||
OAUTH2_PROVIDER_REFRESH_TOKEN_MODEL = "oauth2_provider.RefreshToken"
|
||||
|
||||
|
||||
OAUTH2_PROVIDER = {
|
||||
# this is the list of available scopes
|
||||
'SCOPES_BACKEND_CLASS': 'apps.accounts_oauth2.scopes.Scopes',
|
||||
'SCOPES': {'read': 'Read scope',
|
||||
'write': 'Write scope',
|
||||
'groups': 'Access to your groups',
|
||||
'introspection': 'Introspect token scope'},
|
||||
'OAUTH2_VALIDATOR_CLASS': 'apps.accounts_oauth2.validators.OAuth2Validator',
|
||||
# "INTROSPECTION_USER_CREATE_METHOD": 'apps.users.models.create_user_in_introspection',
|
||||
# 'SCOPES_BACKEND_CLASS': 'apps.gooyal_oauth2.scopes.Scopes',
|
||||
# 'SCOPES': {'read': 'Read scope',
|
||||
# 'write': 'Write scope',
|
||||
# 'groups': 'Access to your groups',
|
||||
# 'introspection': 'Introspect token scope'},
|
||||
'OAUTH2_VALIDATOR_CLASS': 'apps.gooyal_oauth2.validators.OAuth2Validator',
|
||||
'RESOURCE_SERVER_INTROSPECTION_URL': 'https://accounts.gooyal.com/oauth2/introspect/',
|
||||
# 'RESOURCE_SERVER_AUTH_TOKEN': '3yUqsWtwKYKHnfivFcJu', # OR this but not both:
|
||||
'RESOURCE_SERVER_INTROSPECTION_CREDENTIALS': ('dCHm08iIoXvbRSgOI79SV6kIs0aoUzwJehtoczvJ','QBGwaye4Mvr2JHAtn5lQpZhd3RfSVw4XZNrgt6P4UBuV7u6IhsJXUV5QYv3v6rQYEuzjZdKYMjDQuXPmjHeF5UI5fFx080E7FPxFfYHIYBr3ZyvuPi1u7zDRF0EQbzbH'),
|
||||
}
|
||||
|
||||
# GOOYAL_DYNAMIC_SCOPES
|
||||
|
|
@ -99,7 +101,7 @@ REST_FRAMEWORK = {
|
|||
'rest_framework.permissions.IsAuthenticated',
|
||||
),
|
||||
'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.PageNumberPagination',
|
||||
'PAGE_SIZE': 10,
|
||||
'PAGE_SIZE': 20,
|
||||
"DEFAULT_SCHEMA_CLASS": "drf_spectacular.openapi.AutoSchema",
|
||||
}
|
||||
|
||||
|
|
@ -112,7 +114,7 @@ SPECTACULAR_SETTINGS = {
|
|||
}
|
||||
|
||||
|
||||
ROOT_URLCONF = 'accounts.urls'
|
||||
ROOT_URLCONF = 'wallet.urls'
|
||||
|
||||
TEMPLATES = [
|
||||
{
|
||||
|
|
@ -132,7 +134,6 @@ TEMPLATES = [
|
|||
]
|
||||
|
||||
AUTHENTICATION_BACKENDS = (
|
||||
'apps.users.backends.OTPBackend',
|
||||
'django.contrib.auth.backends.ModelBackend',
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ from rest_framework import permissions
|
|||
from drf_spectacular.views import SpectacularAPIView, SpectacularRedocView, SpectacularSwaggerView
|
||||
|
||||
|
||||
# from apps.accounts_oauth2.views.introspect import introspect_token
|
||||
# from apps.gooyal_oauth2.views.introspect import introspect_token
|
||||
from apps.wallet.views import TransactionList, TransactionDetail, TransactionPay, TransactionReceipt, \
|
||||
WithdrawVerify, WithdrawSubmit, DepositSubmit, DepositVerify
|
||||
from django.contrib.auth import urls as auth_urls
|
||||
|
|
@ -36,7 +36,6 @@ urlpatterns = [
|
|||
path('admin/', admin.site.urls),
|
||||
path('wallet/', include('apps.wallet.urls')),
|
||||
path('users/', include('apps.users.urls')),
|
||||
path('superapp/', include('apps.superapp.urls')),
|
||||
path('oauth2/', include('oauth2_provider.urls', namespace='oauth2_provider')),
|
||||
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue