diff --git a/README.md b/README.md index d51bcc2..61249dd 100644 --- a/README.md +++ b/README.md @@ -87,6 +87,8 @@ The meaning of each variable can be found below: - `DEBUG`: if `True` the app runs in develoment mode - For production value `False` should be used - `SECRET_KEY`: used in assets management +- `GITHUB_CLIENT_ID`: For GitHub social login +- `GITHUB_SECRET_KEY`: For GitHub social login
diff --git a/api/authentication/migrations/0001_initial.py b/api/authentication/migrations/0001_initial.py index 5e076cc..5b3ddae 100644 --- a/api/authentication/migrations/0001_initial.py +++ b/api/authentication/migrations/0001_initial.py @@ -1,4 +1,4 @@ -# Generated by Django 3.2.5 on 2021-07-15 11:29 +# Generated by Django 3.2.13 on 2022-12-06 11:33 from django.conf import settings from django.db import migrations, models @@ -15,26 +15,12 @@ class Migration(migrations.Migration): operations = [ migrations.CreateModel( - name="ActiveSession", + name='ActiveSession', fields=[ - ( - "id", - models.BigAutoField( - auto_created=True, - primary_key=True, - serialize=False, - verbose_name="ID", - ), - ), - ("token", models.CharField(max_length=255)), - ("date", models.DateTimeField(auto_now_add=True)), - ( - "user", - models.ForeignKey( - on_delete=django.db.models.deletion.CASCADE, - to=settings.AUTH_USER_MODEL, - ), - ), + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('token', models.CharField(max_length=255)), + ('date', models.DateTimeField(auto_now_add=True)), + ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), ], ), ] diff --git a/api/authentication/serializers/login.py b/api/authentication/serializers/login.py index 229d326..f56b1cf 100644 --- a/api/authentication/serializers/login.py +++ b/api/authentication/serializers/login.py @@ -8,6 +8,7 @@ from api.authentication.models import ActiveSession + def _generate_jwt_token(user): token = jwt.encode( {"id": user.pk, "exp": datetime.utcnow() + timedelta(days=7)}, @@ -61,3 +62,8 @@ def validate(self, data): "token": session.token, "user": {"_id": user.pk, "username": user.username, "email": user.email}, } + + +class GithubSerializer(serializers.Serializer): + code = serializers.CharField(max_length=255) + diff --git a/api/authentication/viewsets/social_login.py b/api/authentication/viewsets/social_login.py new file mode 100644 index 0000000..98f31a0 --- /dev/null +++ b/api/authentication/viewsets/social_login.py @@ -0,0 +1,62 @@ +import requests +import jwt + +from rest_framework import viewsets +from rest_framework.response import Response +from rest_framework.permissions import AllowAny +from django.conf import settings +from django.core.exceptions import ObjectDoesNotExist + +from api.authentication.serializers.login import GithubSerializer, _generate_jwt_token +from api.user.models import User +from api.authentication.models import ActiveSession + + +class GithubSocialLogin(viewsets.ModelViewSet): + http_method_names = ["post"] + permission_classes = (AllowAny,) + serializer_class = GithubSerializer + + def create(self, request, *args, **kwargs): + serializer = self.get_serializer(data=request.data) + serializer.is_valid(raise_exception=True) + + code = serializer.data['code'] + client_id = getattr(settings, 'GITHUB_CLIENT_ID') + client_secret = getattr(settings, 'GITHUB_SECRET_KEY') + root_url = 'https://github.com/login/oauth/access_token' + + params = { 'client_id': client_id, 'client_secret': client_secret, 'code': code } + + data = requests.post(root_url, params=params, headers={ + 'Content-Type': 'application/x-www-form-urlencoded', + }) + + response = data._content.decode('utf-8') + access_token = response.split('&')[0].split('=')[1] + + user_data = requests.get('https://api.github.com/user', headers={ + "Authorization": "Bearer " + access_token + }).json() + + if User.objects.filter(username=user_data['login'], email=user_data['email']).exists(): + user = User.objects.get(username=user_data['login'], email=user_data['email']) + else: + user = User.objects.create_user(username=user_data['login'], email=user_data['email']) + + try: + session = ActiveSession.objects.get(user=user) + if not session.token: + raise ValueError + + jwt.decode(session.token, settings.SECRET_KEY, algorithms=["HS256"]) + + except (ObjectDoesNotExist, ValueError, jwt.ExpiredSignatureError): + session = ActiveSession.objects.create( + user=user, token=_generate_jwt_token(user) + ) + + return Response({ + "success": True, + "user": {"_id": user.pk, "username": user.username, "email": user.email, "token": session.token}, + }) \ No newline at end of file diff --git a/api/routers.py b/api/routers.py index 4418742..acfdc43 100644 --- a/api/routers.py +++ b/api/routers.py @@ -4,6 +4,7 @@ ActiveSessionViewSet, LogoutViewSet, ) +from api.authentication.viewsets.social_login import GithubSocialLogin from rest_framework import routers from api.user.viewsets import UserViewSet @@ -19,6 +20,8 @@ router.register(r"logout", LogoutViewSet, basename="logout") +router.register(r"github-login", GithubSocialLogin, basename="github-login") + urlpatterns = [ *router.urls, ] diff --git a/api/user/migrations/0001_initial.py b/api/user/migrations/0001_initial.py index 03f26d4..64fa0da 100644 --- a/api/user/migrations/0001_initial.py +++ b/api/user/migrations/0001_initial.py @@ -1,4 +1,4 @@ -# Generated by Django 3.2.5 on 2021-07-15 11:20 +# Generated by Django 3.2.13 on 2022-12-06 11:33 from django.db import migrations, models @@ -8,69 +8,27 @@ class Migration(migrations.Migration): initial = True dependencies = [ - ("auth", "0012_alter_user_first_name_max_length"), + ('auth', '0012_alter_user_first_name_max_length'), ] operations = [ migrations.CreateModel( - name="User", + 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(db_index=True, max_length=255)), - ( - "email", - models.EmailField(db_index=True, max_length=254, unique=True), - ), - ("is_active", models.BooleanField(default=True)), - ("date", models.DateTimeField(auto_now_add=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", - ), - ), + ('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(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)), + ('date', models.DateTimeField(auto_now_add=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={ - "abstract": False, + 'abstract': False, }, ), ] diff --git a/api/user/migrations/0002_user_is_staff.py b/api/user/migrations/0002_user_is_staff.py deleted file mode 100644 index 14b2db1..0000000 --- a/api/user/migrations/0002_user_is_staff.py +++ /dev/null @@ -1,18 +0,0 @@ -# Generated by Django 4.0.1 on 2022-05-27 12:34 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('api_user', '0001_initial'), - ] - - operations = [ - migrations.AddField( - model_name='user', - name='is_staff', - field=models.BooleanField(default=False), - ), - ] diff --git a/api/user/migrations/0003_alter_user_username.py b/api/user/migrations/0003_alter_user_username.py deleted file mode 100644 index 7729a82..0000000 --- a/api/user/migrations/0003_alter_user_username.py +++ /dev/null @@ -1,18 +0,0 @@ -# Generated by Django 3.2.13 on 2022-06-07 22:44 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('api_user', '0002_user_is_staff'), - ] - - operations = [ - migrations.AlterField( - model_name='user', - name='username', - field=models.CharField(db_index=True, max_length=255, unique=True), - ), - ] diff --git a/core/settings.py b/core/settings.py index 9d53a34..031efcc 100644 --- a/core/settings.py +++ b/core/settings.py @@ -21,6 +21,8 @@ # Build paths inside the project like this: BASE_DIR / 'subdir'. BASE_DIR = Path(__file__).resolve().parent.parent +environ.Env.read_env(os.path.join(BASE_DIR, '.env')) + # Quick-start development settings - unsuitable for production # See https://docs.djangoproject.com/en/3.2/howto/deployment/checklist/ @@ -176,3 +178,7 @@ TESTING = False TEST_RUNNER = "core.test_runner.CoreTestRunner" + +# GitHub social authentication +GITHUB_CLIENT_ID = env('GITHUB_CLIENT_ID') +GITHUB_SECRET_KEY = env('GITHUB_SECRET_KEY') \ No newline at end of file diff --git a/core/urls.py b/core/urls.py index 7365403..0a0351e 100644 --- a/core/urls.py +++ b/core/urls.py @@ -1,5 +1,8 @@ from django.urls import path, include +from django.contrib import admin +from api.authentication.viewsets.social_login import GithubSocialLogin urlpatterns = [ + path('admin/', admin.site.urls), path("api/users/", include(("api.routers", "api"), namespace="api")), ] diff --git a/env.sample b/env.sample index 9bb2b77..d3e8c68 100644 --- a/env.sample +++ b/env.sample @@ -3,3 +3,6 @@ DEBUG=True DJANGO_ALLOWED_HOSTS=localhost 127.0.0.1 [::1] DB_ENGINE=django.db.backends.sqlite3 DATABASE=db.sqlite3 + +GITHUB_CLIENT_ID= +GITHUB_SECRET_KEY= \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index 55b7f56..28e4698 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,6 +1,23 @@ +asgiref==3.5.2 +certifi==2022.9.24 +cffi==1.15.1 +charset-normalizer==2.1.1 +cryptography==38.0.4 +defusedxml==0.7.1 Django==3.2.13 -djangorestframework==3.13.1 -PyJWT==2.4.0 django-cors-headers==3.13.0 -gunicorn==20.1.0 django-environ==0.8.1 +djangorestframework==3.13.1 +gunicorn==20.1.0 +idna==3.4 +oauthlib==3.2.2 +pycparser==2.21 +PyJWT==2.4.0 +python3-openid==3.2.0 +pytz==2022.6 +requests==2.28.1 +requests-oauthlib==1.3.1 +six==1.16.0 +social-auth-core==4.3.0 +sqlparse==0.4.3 +urllib3==1.26.13