Skip to content
Snippets Groups Projects
Commit 6b67ed70 authored by Chris Li's avatar Chris Li
Browse files

Merge branch 'chris/feature/user-auth-api' into 'main'

User Authentication Feature Added

See merge request !1
parents 90e38a1b 2e517f24
No related branches found
No related tags found
1 merge request!1User Authentication Feature Added
Showing
with 470 additions and 25 deletions
......@@ -49,7 +49,7 @@ git merge <branch-name>
- [ ] Install [Miniconda](https://conda.io/projects/conda/en/stable/user-guide/install/download.html) or [Anaconda](https://www.anaconda.com/) to manage your python packages.
- [ ] Create a virtual env via conda: `conda create --name <env> --file requirements.txt`.
- [ ] Create a virtual env via conda: `conda env create -f environment.yml`.
- [ ] Start the Django server:
......
from django.shortcuts import render
from rest_framework import generics
from rest_framework.response import Response
from knox.models import AuthToken
from core.serializers.login import LoginSerializer
from core.serializers.register import RegisterSerializer
from core.serializers.user import UserSerializer
class RegisterAPI(generics.GenericAPIView):
serializer_class = RegisterSerializer
def post(self, request, *args, **kwargs):
serializer = self.get_serializer(data=request.data)
serializer.is_valid(raise_exception=True)
user = serializer.save()
user.is_active = False
user.save()
token = AuthToken.objects.create(user)
return Response({
"user": UserSerializer(user, context=self.get_serializer_context()).data,
"token": token[1]
})
class LoginAPI(generics.GenericAPIView):
serializer_class = LoginSerializer
def post(self, request):
serializer = self.get_serializer(data=request.data)
serializer.is_valid(raise_exception=True)
user = serializer.validated_data
token = AuthToken.objects.create(user)
return Response({
"user": UserSerializer(user, context=self.get_serializer_context()).data,
"token": token[1],
})
def verify_user_and_activate(request, token):
try:
auth = AuthToken.objects.filter(digest=token).first()
auth.user.is_active = True
auth.user.save()
return render(
request,
template_name='email/verification_success.html',
context={
'msg': 'Your Email is verified successfully and account has been activated.',
'status': 'Verification Successful!',
}
)
except:
return render(
request,
template_name='email/verification_fail.html',
context={
'msg': 'There is something wrong with this link, unable to verify the user...',
'minor_msg': 'There is something wrong with this link...',
'status': 'Verification Failed!',
}
)
from rest_framework import status
from rest_framework import generics
from rest_framework.response import Response
from django.contrib.auth.models import User
from core.serializers.password import ChangePasswordSerializer
from rest_framework.permissions import IsAuthenticated
class ChangePasswordView(generics.UpdateAPIView):
"""
An endpoint for changing password.
"""
serializer_class = ChangePasswordSerializer
model = User
permission_classes = (IsAuthenticated,)
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():
# Check old password
if not self.object.check_password(serializer.data.get("old_password")):
return Response({
"old_password": ["Wrong password."]
}, status=status.HTTP_400_BAD_REQUEST)
# set_password also hashes the password that the user will get
self.object.set_password(serializer.data.get("new_password"))
self.object.save()
response = {
'status': 'success',
'code': status.HTTP_200_OK,
'message': 'Password updated successfully',
'data': []
}
return Response(response)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
from rest_framework import status
from rest_framework import generics
from rest_framework.response import Response
from rest_framework.permissions import IsAuthenticated
from core.models.profile import Profile
from core.serializers.profile import ProfileSerializer
class ProfileViewSet(generics.RetrieveUpdateAPIView):
permission_classes = (IsAuthenticated,)
serializer_class = ProfileSerializer
def retrieve(self, request, *args, **kwargs):
instance = request.user.profile
serializer = self.get_serializer(instance)
return Response(serializer.data)
def update(self, request, *args, **kwargs):
partial = kwargs.pop('partial', False)
instance = request.user.profile
serializer = self.get_serializer(instance, data=request.data, partial=partial)
serializer.is_valid(raise_exception=True)
self.perform_update(serializer)
return Response(serializer.data)
......@@ -4,3 +4,6 @@ from django.apps import AppConfig
class CoreConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'core'
def ready(self) -> None:
import core.signals
# Generated by Django 4.1 on 2023-01-20 17:39
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
import django_countries.fields
import django_extensions.db.fields
class Migration(migrations.Migration):
initial = True
dependencies = [
('auth', '0012_alter_user_first_name_max_length'),
]
operations = [
migrations.CreateModel(
name='Profile',
fields=[
('created', django_extensions.db.fields.CreationDateTimeField(auto_now_add=True, verbose_name='created')),
('modified', django_extensions.db.fields.ModificationDateTimeField(auto_now=True, verbose_name='modified')),
('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, primary_key=True, serialize=False, to=settings.AUTH_USER_MODEL)),
('bio', models.TextField(blank=True)),
('birthday', models.DateField(null=True, verbose_name='Birthday')),
('country', django_countries.fields.CountryField(max_length=2, null=True, verbose_name='Country')),
('city', models.CharField(max_length=100, verbose_name='City in English')),
('affiliation', models.CharField(max_length=100, verbose_name='Name of your organization in English')),
('photo', models.ImageField(blank=True, default='https://static.productionready.io/images/smiley-cyrus.jpg', max_length=100000, null=True, upload_to='media/')),
],
options={
'get_latest_by': 'modified',
'abstract': False,
},
),
]
from datetime import datetime, timedelta
from django.conf import settings
from django.contrib.auth.models import User
from django.db import models
from django_countries.fields import CountryField
from django_extensions.db.models import TimeStampedModel
class Profile(TimeStampedModel):
user = models.OneToOneField(User, on_delete=models.CASCADE, primary_key=True)
bio = models.TextField(blank=True)
birthday = models.DateField(verbose_name="Birthday", null=True)
country = CountryField(null=True, verbose_name="Country")
city = models.CharField(max_length=100, verbose_name="City in English")
affiliation = models.CharField(
max_length=100, verbose_name="Name of your organization in English",
)
photo = models.ImageField(upload_to='media/', max_length=100000, null=True, blank=True, default="https://static.productionready.io/images/smiley-cyrus.jpg")
def __str__(self):
return self.user.username
import uuid
from django.db import models
class UUIDModel(models.Model):
id = models.UUIDField(primary_key=True, default=uuid.uuid4)
class Meta:
abstract = True
from django.contrib.auth import authenticate
from rest_framework import serializers
class LoginSerializer(serializers.Serializer):
username = serializers.CharField()
password = serializers.CharField()
def validate(self, data):
user = authenticate(**data)
if not user:
raise serializers.ValidationError('Invalid Credentials.')
if not user.is_active:
raise serializers.ValidationError('Account is not activated.')
return user
from rest_framework import serializers
from django.contrib.auth.models import User
class ChangePasswordSerializer(serializers.Serializer):
model = User
"""
Serializer for password change endpoint.
"""
old_password = serializers.CharField(required=True)
new_password = serializers.CharField(required=True)
from rest_framework import serializers
from core.models.profile import Profile
class ProfileSerializer(serializers.ModelSerializer):
country = serializers.CharField()
class Meta:
model = Profile
fields = ('user', 'bio', 'birthday', 'country', 'city', 'affiliation', 'photo')
from django.contrib.auth.models import User
from rest_framework import serializers
from core.models.profile import Profile
class RegisterSerializer(serializers.ModelSerializer):
class Meta:
model = User
fields = ('id', 'username', 'email', 'password')
extra_kwargs = {'password': {'write_only': True}}
def create(self, validated_data):
user = User.objects.create_user(
validated_data['username'],
validated_data['email'],
validated_data['password']
)
return user
from rest_framework import serializers
from django.contrib.auth.models import User
# User Serializer
class UserSerializer(serializers.ModelSerializer):
class Meta:
model = User
fields = ('id', 'username', 'email')
from django.contrib.auth.models import User
from django.core.mail import EmailMessage
from django.db.models.signals import post_save
from django.dispatch import receiver
from django_rest_passwordreset.signals import reset_password_token_created
from django.template.loader import render_to_string
from django.urls import reverse
from knox.models import AuthToken
from core.models.profile import Profile
@receiver(post_save, sender=User)
def create_related_profile(sender, instance, created, *args, **kwargs):
if instance and created:
# create profile for new user
instance.profile = Profile.objects.create(user=instance)
@receiver(post_save, sender=User)
def send_activation_email(sender, instance, created, *args, **kwargs):
# send an e-mail to the user
if instance and created:
context = {
'current_user': instance,
'username': instance.username,
'email': instance.email,
'domain': 'localhost:8000',
'token': AuthToken.objects.create(instance)[0].digest
}
# render email text
email_html_message = render_to_string('email/user_verification.html', context)
msg = EmailMessage(
# title:
"User Verification for {title}".format(title="Some website title"),
# message:
email_html_message,
# from:
"noreply@somehost.local",
# to:
[instance.email]
)
msg.send()
@receiver(reset_password_token_created)
def password_reset_token_created(sender, instance, reset_password_token, *args, **kwargs):
# send an e-mail to the user
context = {
'current_user': reset_password_token.user,
'username': reset_password_token.user.username,
'email': reset_password_token.user.email,
'reset_password_url': "{}?token={}".format(
instance.request.build_absolute_uri(reverse('password_reset:reset-password-confirm')),
reset_password_token.key)
}
# render email text
email_html_message = render_to_string('email/user_reset_password.html', context)
msg = EmailMessage(
# title:
"Password Reset for {title}".format(title="Some website title"),
# message:
email_html_message,
# from:
"noreply@somehost.local",
# to:
[reset_password_token.user.email]
)
msg.send()
from django.test import TestCase
from django.contrib.auth.models import User
from rest_framework.test import APIClient
from rest_framework.test import APITestCase
from rest_framework import status
# Create your tests here.
from .models.profile import Profile
class ProfileTestCase(APITestCase):
"""
Test suite for Contact
"""
def setUp(self):
self.client = APIClient()
self.data = {
"username": "xjhmlcy",
"email": "xjhmlcy@gmail.com",
"password": "abcdefg123"
}
self.url = "/api/auth/register"
def test_create_contact(self):
'''
test ContactViewSet create method
'''
data = self.data
response = self.client.post(self.url, data)
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(User.objects.count(), 1)
self.assertEqual(Profile.objects.count(), 1)
self.assertEqual(User.objects.get().username, "xjhmlcy")
self.assertEqual(Profile.objects.get().user.email, "xjhmlcy@gmail.com")
from django.urls import path
from django.urls import path, include
from knox import views as knox_views
from rest_framework import routers
from core.api.auth import RegisterAPI, LoginAPI, verify_user_and_activate
from core.api.password import ChangePasswordView
from core.api.profile import ProfileViewSet
urlpatterns = [
router = routers.DefaultRouter()
urlpatterns = router.urls
urlpatterns += [
# auth
path('api/auth/register', RegisterAPI.as_view(), name='register'),
path('api/auth/activate/<token>', verify_user_and_activate, name='activate'),
path('api/auth/login', LoginAPI.as_view(), name='login'),
path('api/auth/logout', knox_views.LogoutView.as_view(), name='logout'),
# passwd
path('api/change-password', ChangePasswordView.as_view(), name='change-password'),
path('api/password_reset/', include('django_rest_passwordreset.urls', namespace='password_reset')),
# profile
path('api/profile', ProfileViewSet.as_view(), name='profile')
]
# API
## Authentication
- url: api/auth/register
```json
// request
{
"username": "admin",
"email": "admin@bot.com",
"password": "Password@123"
}
// response
{
"user": {
"id": 2,
"username": "admin1",
"email": "admin1@bot.com"
},
"token": "790e890d571753148bbc9c4447f106e74ecf4d1404f080245f3e259703d58b09"
}
```
- url: api/auth/login
```json
// request
{
"username": "admin",
"password": "Password@123"
}
//response
{
"expiry": "2020-06-29T02:56:44.924698Z",
"token": "99a27b2ebe718a2f0db6224e55b622a59ccdae9cf66861c60979a25ffb4f133e"
}
```
......@@ -9,7 +9,8 @@ https://docs.djangoproject.com/en/4.1/topics/settings/
For the full list of settings and their values, see
https://docs.djangoproject.com/en/4.1/ref/settings/
"""
from datetime import timedelta
import os
from pathlib import Path
# Build paths inside the project like this: BASE_DIR / 'subdir'.
......@@ -31,15 +32,32 @@ ALLOWED_HOSTS = ['*']
# Application definition
INSTALLED_APPS = [
'core.apps.CoreConfig',
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'django_extensions',
'rest_framework',
'django_rest_passwordreset',
'knox',
'core',
]
REST_FRAMEWORK = {
# Use Django's standard `django.contrib.auth` permissions,
# or allow read-only access for unauthenticated users.
'DEFAULT_AUTHENTICATION_CLASSES': [
'knox.auth.TokenAuthentication',
],
}
REST_KNOX = {
'TOKEN_TTL': timedelta(days=3),
'AUTO_REFRESH': True,
}
MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
......@@ -55,7 +73,9 @@ ROOT_URLCONF = 'ece651_backend.urls'
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [],
'DIRS': [
os.path.join(BASE_DIR, 'templates'),
],
'APP_DIRS': True,
'OPTIONS': {
'context_processors': [
......@@ -122,3 +142,10 @@ STATIC_URL = 'static/'
# https://docs.djangoproject.com/en/4.1/ref/settings/#default-auto-field
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
# Email settings
EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend'
# Media Settings
MEDIA_URL = 'media/'
MEDIA_ROOT = os.path.join(BASE_DIR, 'media')
"""ece651_backend URL Configuration
The `urlpatterns` list routes URLs to views. For more information please see:
https://docs.djangoproject.com/en/4.1/topics/http/urls/
Examples:
Function views
1. Add an import: from my_app import views
2. Add a URL to urlpatterns: path('', views.home, name='home')
Class-based views
1. Add an import: from other_app.views import Home
2. Add a URL to urlpatterns: path('', Home.as_view(), name='home')
Including another URLconf
1. Import the include() function: from django.urls import include, path
2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
"""
from django.conf import settings
from django.conf.urls.static import static
from django.contrib import admin
from django.urls import path, include
from rest_framework import routers
router = routers.DefaultRouter()
urlpatterns = [
path('api/core/', include('core.urls')),
urlpatterns = router.urls
urlpatterns += [
path('', include('core.urls')),
path('admin/', admin.site.urls),
]
urlpatterns += static('media/', document_root=settings.MEDIA_ROOT)
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment