Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 0 additions & 40 deletions questions/migrations/0001_initial.py

This file was deleted.

Empty file removed questions/migrations/__init__.py
Empty file.
Binary file not shown.
Binary file not shown.
11 changes: 11 additions & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
asgiref==3.4.1
Django==3.2.6
django-ckeditor==6.1.0
django-js-asset==1.2.2
djangorestframework==3.12.4
djangorestframework-simplejwt==4.7.2
Pillow==8.3.1
PyJWT==2.1.0
pytz==2021.1
sqlparse==0.4.1
typing==3.7.4.3
Binary file not shown.
Binary file modified stackoverflow_api/__pycache__/settings.cpython-38.pyc
Binary file not shown.
Binary file modified stackoverflow_api/__pycache__/urls.cpython-38.pyc
Binary file not shown.
87 changes: 87 additions & 0 deletions stackoverflow_api/jwt_authorization.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
from django.conf import settings
from rest_framework import exceptions, authentication
from rest_framework.authentication import BaseAuthentication
from rest_framework.exceptions import AuthenticationFailed
import jwt

from users.models import User


class JWTAuthentication(BaseAuthentication):
authentication_header_prefix = 'Bearer'

def authenticate(self, request):
"""
Метод authenticate вызывается каждый раз, независимо от того, требует
ли того эндпоинт аутентификации. 'authenticate' имеет два возможных
возвращаемых значения:
1) None - мы возвращаем None если не хотим аутентифицироваться.
Обычно это означает, что мы значем, что аутентификация не удастся.
Примером этого является, например, случай, когда токен не включен в
заголовок.
2) (user, token) - мы возвращаем комбинацию пользователь/токен
тогда, когда аутентификация пройдена успешно. Если ни один из
случаев не соблюден, это означает, что произошла ошибка, и мы
ничего не возвращаем. В таком случае мы просто вызовем исключение
AuthenticationFailed и позволим DRF сделать все остальное.
"""
request.user = None

# 'auth_header' должен быть массивом с двумя элементами:
# 1) именем заголовка аутентификации (Token в нашем случае)
# 2) сам JWT, по которому мы должны пройти аутентифкацию
auth_header = authentication.get_authorization_header(request).split()
auth_header_prefix = self.authentication_header_prefix.lower()

if not auth_header:
return None

if len(auth_header) == 1:
# Некорректный заголовок токена, в заголовке передан один элемент
return None

elif len(auth_header) > 2:
# Некорректный заголовок токена, какие-то лишние пробельные символы
return None

# JWT библиотека которую мы используем, обычно некорректно обрабатывает
# тип bytes, который обычно используется стандартными библиотеками
# Python3 (HINT: использовать PyJWT). Чтобы точно решить это, нам нужно
# декодировать prefix и token. Это не самый чистый код, но это хорошее
# решение, потому что возможна ошибка, не сделай мы этого.
prefix = auth_header[0].decode('utf-8')
token = auth_header[1].decode('utf-8')

if prefix.lower() != auth_header_prefix:
# Префикс заголовка не тот, который мы ожидали - отказ.
return None

# К настоящему моменту есть "шанс", что аутентификация пройдет успешно.
# Мы делегируем фактическую аутентификацию учетных данных методу ниже.
return self._authenticate_credentials(request, token)

def _authenticate_credentials(self, request, token):
"""
Попытка аутентификации с предоставленными данными. Если успешно -
вернуть пользователя и токен, иначе - сгенерировать исключение.
"""
try:
payload = jwt.decode(token, settings.SECRET_KEY)
except Exception:
msg = 'Ошибка аутентификации. Невозможно декодировать токеню'
raise exceptions.AuthenticationFailed(msg)

try:
user = User.objects.get(pk=payload['id'])
except User.DoesNotExist:
msg = 'Пользователь соответствующий данному токену не найден.'
raise exceptions.AuthenticationFailed(msg)

if not user.is_active:
msg = 'Данный пользователь деактивирован.'
raise exceptions.AuthenticationFailed(msg)

return (user, token)



94 changes: 76 additions & 18 deletions stackoverflow_api/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
https://docs.djangoproject.com/en/3.2/ref/settings/
"""
import os
from datetime import timedelta
from pathlib import Path

# Build paths inside the project like this: BASE_DIR / 'subdir'.
Expand All @@ -25,7 +26,7 @@
# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = True

ALLOWED_HOSTS = []
ALLOWED_HOSTS = ['*']

AUTH_USER_MODEL = 'users.User'

Expand All @@ -35,6 +36,9 @@
'ckeditor',
'questions.apps.QuestionsConfig',
'users',
'rest_framework',
'rest_framework_simplejwt',
'rest_framework_simplejwt.token_blacklist',
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
Expand Down Expand Up @@ -86,23 +90,29 @@
}
}

REST_FRAMEWORK = {
'DEFAULT_AUTHENTICATION_CLASSESS': [
'custom_authorization.CustomAuthorization',
'rest_framework_simplejwt.authentication.JWTAuthentication',
],
}

# Password validation
# https://docs.djangoproject.com/en/3.2/ref/settings/#auth-password-validators

AUTH_PASSWORD_VALIDATORS = [
{
'NAME': 'django.contrib.users.password_validation.UserAttributeSimilarityValidator',
},
{
'NAME': 'django.contrib.users.password_validation.MinimumLengthValidator',
},
{
'NAME': 'django.contrib.users.password_validation.CommonPasswordValidator',
},
{
'NAME': 'django.contrib.users.password_validation.NumericPasswordValidator',
},
# {
# 'NAME': 'django.contrib.users.password_validation.UserAttributeSimilarityValidator',
# },
# {
# 'NAME': 'django.contrib.users.password_validation.MinimumLengthValidator',
# },
# {
# 'NAME': 'django.contrib.users.password_validation.CommonPasswordValidator',
# },
# {
# 'NAME': 'django.contrib.users.password_validation.NumericPasswordValidator',
# },
]


Expand All @@ -111,7 +121,7 @@

LANGUAGE_CODE = 'en-us'

TIME_ZONE = 'UTC'
TIME_ZONE = 'Europe/Moscow'

USE_I18N = True

Expand All @@ -123,13 +133,61 @@
# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/3.2/howto/static-files/

STATIC_URL = '/static/'

STATICFILES_DIRS = [
os.path.join(BASE_DIR, 'static'),
]
if DEBUG:
STATIC_URL = '/static/'
MEDIA_URL = '/media/'

STATICFILES_DIRS = [
os.path.join(BASE_DIR, 'static'),
]
MEDIA_ROOT = os.path.join(BASE_DIR, 'media')
else:
STATIC_ROOT = '/var/www/static/'

STATICFILES_DIRS = [
Path(BASE_DIR, '/var/www/static/')
]
MEDIA_URL = '/media/'
MEDIA_ROOT = Path(BASE_DIR, '/var/www/media/')


DEFAULT_USER_PROFILE_LOGO = '/media/users/default/logo.png'
CUSTOM_USER_PROFILE_LOGO = 'media/default/{}/logo/'
GITHUB_LINK_PATTERN = 'https://github.com/{}'
TWITTER_LINK_PATTERN = 'https://twitter.com/{}'
# Default primary key field type
# https://docs.djangoproject.com/en/3.2/ref/settings/#default-auto-field

DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'

# SIMPLE JWT SETTINGS
SIMPLE_JWT = {
'ACCESS_TOKEN_LIFETIME': timedelta(days=5), ## вставляется в exp при генерации токенов
'REFRESH_TOKEN_LIFETIME': timedelta(days=1),
'ROTATE_REFRESH_TOKENS': True, # при рефрешу токенов выдает новую пару
'BLACKLIST_AFTER_ROTATION': True, # для этого используется специальноое приложение blacklist app. ('rest_framework_simplejwt.token_blacklist' в installed_apps)
'UPDATE_LAST_LOGIN': True,

'ALGORITHM': 'HS256',
'SIGNING_KEY': SECRET_KEY,
'VERIFYING_KEY': None,
'AUDIENCE': None,
'ISSUER': None,
'JWK_URL': None,

'AUTH_HEADER_TYPES': ('Bearer',),
'AUTH_HEADER_NAME': 'HTTP_AUTHORIZATION',
'USER_ID_FIELD': 'id',
'USER_ID_CLAIM': 'user_id',
'USER_AUTHENTICATION_RULE': 'rest_framework_simplejwt.authentication.default_user_authentication_rule',

'AUTH_TOKEN_CLASSES': ('rest_framework_simplejwt.tokens.AccessToken',),
'TOKEN_TYPE_CLAIM': 'token_type',

'JTI_CLAIM': 'jti',

'SLIDING_TOKEN_REFRESH_EXP_CLAIM': 'refresh_exp',
'SLIDING_TOKEN_LIFETIME': timedelta(minutes=5),
'SLIDING_TOKEN_REFRESH_LIFETIME': timedelta(days=1),
}
2 changes: 1 addition & 1 deletion stackoverflow_api/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,5 +21,5 @@
urlpatterns = [
path('admin/', admin.site.urls),
path('admin-questions/', question_admin_site.urls),
path('', include('questions.urls'))
path('', include('users.urls'))
]
Binary file modified users/__pycache__/models.cpython-38.pyc
Binary file not shown.
Binary file added users/__pycache__/permissions.cpython-38.pyc
Binary file not shown.
Binary file added users/__pycache__/serializers.cpython-38.pyc
Binary file not shown.
Binary file added users/__pycache__/urls.cpython-38.pyc
Binary file not shown.
Binary file added users/__pycache__/views.cpython-38.pyc
Binary file not shown.
53 changes: 36 additions & 17 deletions users/migrations/0001_initial.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
# Generated by Django 3.2.5 on 2021-08-10 12:44
# Generated by Django 3.2.6 on 2021-08-17 15:56

import django.contrib.auth.models
import django.contrib.auth.validators
from django.db import migrations, models
import django.utils.timezone
import django.db.models.deletion
import django.db.models.manager
import uuid


class Migration(migrations.Migration):
Expand All @@ -18,28 +18,47 @@ class Migration(migrations.Migration):
migrations.CreateModel(
name='User',
fields=[
('id', models.BigAutoField(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')),
('username', models.CharField(error_messages={'unique': 'A user with that username already exists.'}, help_text='Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.', max_length=150, unique=True, validators=[django.contrib.auth.validators.UnicodeUsernameValidator()], verbose_name='username')),
('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')),
('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')),
('login', models.CharField(blank=True, max_length=150)),
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
('username', models.CharField(db_index=True, max_length=255, unique=True)),
('email', models.EmailField(db_index=True, max_length=254, unique=True)),
('is_active', models.BooleanField(default=True)),
('is_staff', models.BooleanField(default=False)),
('created_at', models.DateTimeField(auto_now_add=True)),
('updated_at', models.DateTimeField(auto_now=True)),
('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,
'unique_together': {('username', 'email')},
},
),
migrations.CreateModel(
name='UserProfileContactInfo',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('website_link', models.URLField(blank=True, null=True)),
('github_link', models.URLField(blank=True, null=True)),
('twitter_link', models.URLField(blank=True, null=True)),
('website_link_active', models.BooleanField(default=False)),
('github_link_active', models.BooleanField(default=False)),
('twitter_link_active', models.BooleanField(default=False)),
],
),
migrations.CreateModel(
name='UserProfile',
fields=[
('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, primary_key=True, serialize=False, to='users.user')),
('logo', models.ImageField(default='/media/users/default/logo.png', upload_to='media/default/<built-in function id>/logo/')),
('location', models.CharField(blank=True, max_length=200, null=True)),
('about', models.TextField(blank=True, max_length=255, null=True)),
('title', models.CharField(blank=True, max_length=100, null=True)),
('user_profile', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to='users.userprofilecontactinfo')),
],
managers=[
('objects', django.contrib.auth.models.UserManager()),
('test', django.db.models.manager.Manager()),
],
),
]
18 changes: 18 additions & 0 deletions users/migrations/0002_rename_user_profile_userprofile_contacts.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# Generated by Django 3.2.6 on 2021-08-17 16:00

from django.db import migrations


class Migration(migrations.Migration):

dependencies = [
('users', '0001_initial'),
]

operations = [
migrations.RenameField(
model_name='userprofile',
old_name='user_profile',
new_name='contacts',
),
]
Binary file modified users/migrations/__pycache__/0001_initial.cpython-38.pyc
Binary file not shown.
Binary file not shown.
Binary file modified users/migrations/__pycache__/__init__.cpython-38.pyc
Binary file not shown.
Loading