diff --git a/auth_remindme/settings.py b/auth_remindme/settings.py index b569ebce9d85cee48fb7ea7033c3abacbe70bf2e..3b79de802c31726234c39583b4bcca18f00a78cc 100644 --- a/auth_remindme/settings.py +++ b/auth_remindme/settings.py @@ -11,8 +11,8 @@ https://docs.djangoproject.com/en/4.0/ref/settings/ """ import os +from datetime import timedelta from pathlib import Path - import dj_database_url # Build paths inside the project like this: BASE_DIR / 'subdir'. @@ -56,6 +56,8 @@ INSTALLED_APPS = [ 'django.contrib.messages', 'django.contrib.staticfiles', 'main', + 'oauth', + 'rest_framework_simplejwt', ] MIDDLEWARE = [ @@ -164,3 +166,43 @@ for directory in [*STATICFILES_DIRS, STATIC_ROOT]: # Enable compression and caching features of whitenoise. # You can remove this if it causes problems on your setup. STATICFILES_STORAGE = 'whitenoise.storage.CompressedManifestStaticFilesStorage' + +REST_FRAMEWORK = { + 'DEFAULT_AUTHENTICATION_CLASSES': ( + 'rest_framework_simplejwt.authentication.JWTAuthentication', + ) +} + +AUTH_USER_MODEL = "oauth.UserAccount" + +SIMPLE_JWT = { + 'ACCESS_TOKEN_LIFETIME': timedelta(minutes=5), + 'REFRESH_TOKEN_LIFETIME': timedelta(days=1), + 'ROTATE_REFRESH_TOKENS': False, + 'BLACKLIST_AFTER_ROTATION': False, + 'UPDATE_LAST_LOGIN': False, + + 'ALGORITHM': 'HS256', + 'SIGNING_KEY': SECRET_KEY, + 'VERIFYING_KEY': None, + 'AUDIENCE': None, + 'ISSUER': None, + 'JWK_URL': None, + 'LEEWAY': 0, + + 'AUTH_HEADER_TYPES': ('Bearer', 'JWT',), + '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', + 'TOKEN_USER_CLASS': 'rest_framework_simplejwt.models.TokenUser', + + 'JTI_CLAIM': 'jti', + + 'SLIDING_TOKEN_REFRESH_EXP_CLAIM': 'refresh_exp', + 'SLIDING_TOKEN_LIFETIME': timedelta(minutes=5), + 'SLIDING_TOKEN_REFRESH_LIFETIME': timedelta(days=1), +} \ No newline at end of file diff --git a/auth_remindme/urls.py b/auth_remindme/urls.py index 3178999c40a6e11c697a51c8b3340273be791365..479ec1eee0cd064de7ab312099f4483f45a71392 100644 --- a/auth_remindme/urls.py +++ b/auth_remindme/urls.py @@ -19,4 +19,5 @@ from django.urls import include, path urlpatterns = [ path('admin/', admin.site.urls), path('', include('main.urls')), + path('', include('oauth.urls')) ] diff --git a/oauth/__init__.py b/oauth/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/oauth/admin.py b/oauth/admin.py new file mode 100644 index 0000000000000000000000000000000000000000..e7e6cde0f26f2d98ad15b2c7c292784decc9ac31 --- /dev/null +++ b/oauth/admin.py @@ -0,0 +1,4 @@ +from django.contrib import admin +from .models import UserAccount +# Register your models here. +admin.site.register(UserAccount) \ No newline at end of file diff --git a/oauth/apps.py b/oauth/apps.py new file mode 100644 index 0000000000000000000000000000000000000000..f5cfc41f3bf3633978a6170c319b016d47c0f549 --- /dev/null +++ b/oauth/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class OauthConfig(AppConfig): + default_auto_field = 'django.db.models.BigAutoField' + name = 'oauth' diff --git a/oauth/migrations/0001_initial.py b/oauth/migrations/0001_initial.py new file mode 100644 index 0000000000000000000000000000000000000000..1d42e8ca246f2f054c8e05107352abeb6220df1e --- /dev/null +++ b/oauth/migrations/0001_initial.py @@ -0,0 +1,34 @@ +# Generated by Django 3.2.13 on 2022-05-01 00:47 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ('auth', '0012_alter_user_first_name_max_length'), + ] + + operations = [ + migrations.CreateModel( + name='UserAccount', + 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(max_length=255, unique=True)), + ('first_name', models.CharField(max_length=255)), + ('last_name', models.CharField(max_length=255)), + ('is_active', models.BooleanField(default=True)), + ('is_staff', models.BooleanField(default=False)), + ('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={ + 'abstract': False, + }, + ), + ] diff --git a/oauth/migrations/__init__.py b/oauth/migrations/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/oauth/models.py b/oauth/models.py new file mode 100644 index 0000000000000000000000000000000000000000..b9ea87be8183abdaba02ad2f168c0efd170ac706 --- /dev/null +++ b/oauth/models.py @@ -0,0 +1,45 @@ +from django.db import models +from django.contrib.auth.models import AbstractBaseUser, PermissionsMixin, BaseUserManager + + +class UserAccountManager(BaseUserManager): + def create_user(self, username, password=None, **extra_fields): + user = self.model(username=username, **extra_fields) + + user.set_password(password) + user.save() + + return user + + def create_superuser(self, username, password, **extra_fields): + + extra_fields.setdefault('is_staff', True) + extra_fields.setdefault('is_superuser', True) + extra_fields.setdefault('is_active', True) + + if extra_fields.get('is_staff') is not True: + raise ValueError( + 'Superuser must be assigned to is_staff=True.') + if extra_fields.get('is_superuser') is not True: + raise ValueError( + 'Superuser must be assigned to is_superuser=True.') + + return self.create_user(username, password, **extra_fields) + +class UserAccount(AbstractBaseUser, PermissionsMixin): + username = models.CharField(max_length=255, unique=True) + first_name = models.CharField(max_length=255) + last_name = models.CharField(max_length=255) + is_active = models.BooleanField(default=True) + is_staff = models.BooleanField(default=False) + + objects = UserAccountManager() + + USERNAME_FIELD = 'username' + REQUIRED_FIELDS = ['first_name', 'last_name'] + + def get_full_name(self): + return "{fname} {lname}".format(fname=self.first_name, lname=self.last_name) + + def __str__(self): + return self.username \ No newline at end of file diff --git a/oauth/serializers.py b/oauth/serializers.py new file mode 100644 index 0000000000000000000000000000000000000000..221d266c11691dbe4055ec647dfc287028c212ab --- /dev/null +++ b/oauth/serializers.py @@ -0,0 +1,22 @@ +from rest_framework import serializers +from .models import UserAccount +from django.contrib.auth.password_validation import validate_password +from rest_framework.exceptions import ValidationError + +class UserSerializer(serializers.ModelSerializer): + class Meta: + model = UserAccount + fields = ('username', 'first_name', 'last_name', 'password') + extra_kwargs = {'password': {'write_only': True}} + + def create(self, validated_data): + password = validated_data.pop('password', None) + instance = self.Meta.model(**validated_data) + if password is not None: + try: + validate_password(password=password, user=instance) + instance.set_password(password) + instance.save() + return instance + except Exception as err: + raise err diff --git a/oauth/tests.py b/oauth/tests.py new file mode 100644 index 0000000000000000000000000000000000000000..7ce503c2dd97ba78597f6ff6e4393132753573f6 --- /dev/null +++ b/oauth/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/oauth/urls.py b/oauth/urls.py new file mode 100644 index 0000000000000000000000000000000000000000..acff786b3531d075a04b6cf7a50a2fbc7d40c8b4 --- /dev/null +++ b/oauth/urls.py @@ -0,0 +1,17 @@ +from django.urls import path +from rest_framework_simplejwt.views import ( + TokenObtainPairView, + TokenRefreshView, + TokenVerifyView +) +from . import views + +app_name = 'oauth' + +urlpatterns = [ + path('api/token/', TokenObtainPairView.as_view(), name='token_obtain_pair'), + path('api/token/refresh/', TokenRefreshView.as_view(), name='token_refresh'), + path('api/token/verify/', TokenVerifyView.as_view(), name='token_verify'), + path('api/create-user/', views.UserCreate.as_view()), + path('api/resource/', views.ResourceTest.as_view()), +] diff --git a/oauth/views.py b/oauth/views.py new file mode 100644 index 0000000000000000000000000000000000000000..8b5146c78a4921298e54cae89866eb92490b8c4f --- /dev/null +++ b/oauth/views.py @@ -0,0 +1,34 @@ +from rest_framework import generics +from rest_framework.exceptions import ValidationError +from rest_framework.response import Response +from rest_framework.views import APIView +from .models import UserAccount +from .serializers import UserSerializer +from rest_framework.permissions import AllowAny, IsAuthenticated +from djangorestframework_camel_case.parser import (CamelCaseJSONParser, + CamelCaseMultiPartParser, + CamelCaseFormParser) +from djangorestframework_camel_case.render import (CamelCaseJSONRenderer, + CamelCaseBrowsableAPIRenderer) + +class UserCreate(generics.CreateAPIView): + serializer_class = UserSerializer + permission_classes = [AllowAny] + parser_classes = (CamelCaseJSONParser, CamelCaseFormParser, CamelCaseMultiPartParser, ) + renderer_classes = (CamelCaseJSONRenderer, CamelCaseBrowsableAPIRenderer, ) + + def perform_create(self, serializer): + try: + serializer = serializer.save() + return serializer + except Exception as err: + error_message = {'error': err} + raise ValidationError(error_message) + +# Hanya untuk test token +class ResourceTest(generics.ListAPIView): + queryset = UserAccount.objects.all() + serializer_class = UserSerializer + permission_classes = [AllowAny] + parser_classes = (CamelCaseJSONParser, CamelCaseFormParser, CamelCaseMultiPartParser, ) + renderer_classes = (CamelCaseJSONRenderer, CamelCaseBrowsableAPIRenderer, ) \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index 6a4c29b05d0f367509ca0c10a0793e63e9cdb0af..424e42d3e015562619b17876521da882cf6331e2 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,9 @@ coverage>=5.3,<6.0 +django-cors-headers==3.7.0 dj-database-url>=0.5.0 +djangorestframework==3.11.0 +djangorestframework-camel-case==1.2.0 +djangorestframework-simplejwt>=4.4.0 Django>=3.1.0,<4.0.0 gunicorn>=20.0.0,<21.0.0 psycopg2-binary>=2.8.0,<3.0.0