diff --git a/backend/apps/authentication/migrations/0001_initial.py b/backend/apps/authentication/migrations/0001_initial.py new file mode 100644 index 0000000..cb3fe67 --- /dev/null +++ b/backend/apps/authentication/migrations/0001_initial.py @@ -0,0 +1,40 @@ +# Generated by Django 6.1 on 2026-08-23 16:39 + +import uuid + +import django.db.models.deletion +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + initial = True + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name="UserProfile", + fields=[ + ( + "id", + models.UUIDField( + default=uuid.uuid4, + editable=False, + primary_key=True, + serialize=False, + ), + ), + ( + "user", + models.OneToOneField( + on_delete=django.db.models.deletion.CASCADE, + related_name="profile", + to=settings.AUTH_USER_MODEL, + ), + ), + ], + ), + ] diff --git a/backend/apps/authentication/urls.py b/backend/apps/authentication/urls.py index fa61965..bdadc66 100644 --- a/backend/apps/authentication/urls.py +++ b/backend/apps/authentication/urls.py @@ -5,4 +5,7 @@ urlpatterns = [ path("register/", views.RegisterationView.as_view(), name="register"), path("login/", views.LoginView.as_view(), name="login"), + path("/", views.OAuthLoginView.as_view()), + path("/callback/", views.OAuthCallbackView.as_view()), + # path("google/",views.GoogleLogin.as_view(),name="google_login"), ] diff --git a/backend/apps/authentication/views.py b/backend/apps/authentication/views.py index 9375529..35cb74f 100644 --- a/backend/apps/authentication/views.py +++ b/backend/apps/authentication/views.py @@ -1,6 +1,13 @@ +from urllib.parse import urlencode + +import requests +from django.conf import settings from django.contrib.auth.models import User +from django.shortcuts import redirect from rest_framework import generics, status +from rest_framework.permissions import AllowAny from rest_framework.response import Response +from rest_framework.views import APIView from rest_framework_simplejwt.tokens import RefreshToken from .models import UserProfile @@ -62,3 +69,51 @@ def create(self, request, *args, **kwags): }, status=status.HTTP_200_OK, ) + + +class OAuthLoginView(APIView): + permission_classes = [AllowAny] + + def get(self, request, provider): + if provider != "google": + return Response({"error": "Unsupported provider"}, status=400) + params = { + "client_id": settings.GOOGLE_CLIENT_ID, + "redirect_uri": settings.GOOGLE_REDIRECT_URI, + "response_type": "code", + "scope": "openid email profile", + "access_type": "offline", + } + google_url = "https://accounts.google.com/o/oauth2/v2/auth?" + urlencode(params) + return redirect(google_url) + + +class OAuthCallbackView(APIView): + permission_classes = [AllowAny] + + def get(self, request, provider): + if provider != "google": + return Response({"error": "Unsupported provider"}, status=400) + code = request.GET.get("code") + if not code: + return Response({"error": "Authorization code missing"}, status=400) + token_response = requests.post( + "https://oauth2.googleapis.com/token", + data={ + "code": code, + "client_id": settings.GOOGLE_CLIENT_ID, + "client_secret": settings.GOOGLE_CLIENT_SECRET, + "redirect_uri": settings.GOOGLE_REDIRECT_URI, + "grant_type": "authorization_code", + }, + ) + token_data = token_response.json() + access_token = token_data.get("access_token") + if not access_token: + return Response({"error": "Failed to get access_token"}, status=400) + user_response = requests.get( + "https://www.googleapis.com/oauth2/v2/userinfo", + headers={"Authorization": f"Bearer {access_token}"}, + ) + user_data = user_response.json() + return Response(user_data) diff --git a/backend/config/settings/base.py b/backend/config/settings/base.py index c6d7070..0374db5 100644 --- a/backend/config/settings/base.py +++ b/backend/config/settings/base.py @@ -1,6 +1,12 @@ import os from pathlib import Path +from dotenv import load_dotenv + +load_dotenv() +GOOGLE_CLIENT_ID = os.getenv("GOOGLE_CLIENT_ID") +GOOGLE_CLIENT_SECRET = os.getenv("GOOGLE_CLIENT_SECRET") +GOOGLE_REDIRECT_URI = os.getenv("GOOGLE_REDIRECT_URI") # BASE_DIR points to the root of your backend project (/app) BASE_DIR = Path(__file__).resolve().parent.parent.parent @@ -12,11 +18,20 @@ "django.contrib.sessions", "django.contrib.messages", "django.contrib.staticfiles", + "rest_framework.authtoken", # Third-party apps "rest_framework", "rest_framework_simplejwt", # Local apps "apps.authentication", + # dj_auth_apps + "dj_rest_auth", + "dj_rest_auth.registration", + # allauth + "allauth", + "allauth.account", + "allauth.socialaccount", + "allauth.socialaccount.providers.google", ] MIDDLEWARE = [ @@ -27,6 +42,7 @@ "django.contrib.auth.middleware.AuthenticationMiddleware", "django.contrib.messages.middleware.MessageMiddleware", "django.middleware.clickjacking.XFrameOptionsMiddleware", + "allauth.account.middleware.AccountMiddleware", ] ROOT_URLCONF = "config.urls" @@ -90,3 +106,27 @@ # Default primary key field type DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField" +SITE_ID = 1 +AUTHENTICATION_BACKENDS = [ + "django.contrib.auth.backends.ModelBackend", + "allauth.account.auth_backends.AuthenticationBackend", +] +REST_FRAMEWORK = { + "DEFAULT_AUTHENTICATION_CLASSES": [ + "rest_framework.authentication.TokenAuthentication", + ], +} + +SOCIALACCOUNT_PROVIDERS = { + "google": { + "SCOPE": [ + "profile", + "email", + ], + "AUTH_PARAMS": { + "access_type": "online", + }, + }, +} + +LOGIN_REDIRECT_URL = "/" diff --git a/backend/config/urls.py b/backend/config/urls.py index cda5a53..3cf5ff5 100644 --- a/backend/config/urls.py +++ b/backend/config/urls.py @@ -21,6 +21,7 @@ urlpatterns = [ path("admin/", admin.site.urls), path("api/auth/", include("apps.authentication.urls")), + # path("accounts/", include("allauth.urls")), # path("catalog/", include("apps.catalog.urls")), # path("order/", include("apps.orders.urls")), # path("profile/", include("apps.profiles.urls")), diff --git a/backend/pyproject.toml b/backend/pyproject.toml index 01023eb..170caaf 100644 --- a/backend/pyproject.toml +++ b/backend/pyproject.toml @@ -13,16 +13,15 @@ dependencies = [ "djangorestframework>=3.15.0", "djangorestframework-simplejwt>=5.3.1", "django-cors-headers>=4.4.0", - # Database "psycopg[binary]>=3.2.0", - # Production WSGI Server "gunicorn>=23.0.0", - # Utilities & OAuth Integrations "python-dotenv>=1.0.1", "requests>=2.32.0", + "dj-rest-auth>=7.2.0", + "django-allauth>=65.19.1", ] [tool.uv] @@ -51,4 +50,4 @@ select = [ [tool.pytest.ini_options] DJANGO_SETTINGS_MODULE = "config.settings.local" python_files = ["test_*.py", "*_test.py", "tests.py"] -addopts = "--strict-markers --no-migrations" \ No newline at end of file +addopts = "--strict-markers --no-migrations" diff --git a/backend/uv.lock b/backend/uv.lock index 8e7c97a..12555d2 100644 --- a/backend/uv.lock +++ b/backend/uv.lock @@ -160,6 +160,16 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, ] +[[package]] +name = "dj-rest-auth" +version = "7.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "django" }, + { name = "djangorestframework" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/14/fd/c8a185a98f6860b802dc5e663c652efc01cd68f4e046c65f8cb9328c0077/dj_rest_auth-7.2.0.tar.gz", hash = "sha256:f77af37da5cf6ee28f03f283a378a2f5761860e72d1d6f2fa3d50e6286000482", size = 490126, upload-time = "2026-03-15T04:05:39.272Z" } + [[package]] name = "django" version = "5.2.17" @@ -174,6 +184,19 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/df/f8/ce120525ca78f12b07daf65786679c5d0b54a75285a8958d3ae55e39da35/django-5.2.17-py3-none-any.whl", hash = "sha256:f04fb3b36ee119e1af4fa1d397d5fd6cf12700f49321e84d4f4c642c5b1973db", size = 8315563, upload-time = "2026-08-04T15:03:59.1Z" }, ] +[[package]] +name = "django-allauth" +version = "65.19.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "asgiref" }, + { name = "django" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/cf/93/776a61df6ce71f82e98afa1938c6e540ec239a8f80b2747c3c33cb9581cc/django_allauth-65.19.1.tar.gz", hash = "sha256:999d01d5c422a82734cf5804df9f5a0d1d4ddd6acbb2f807db976ed7d49f9cb3", size = 2286662, upload-time = "2026-08-13T10:02:17.956Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/77/f6/3c5f838f2e66903b422ae45f8f44b9e5da6457a4d4da125d426b339d86c8/django_allauth-65.19.1-py3-none-any.whl", hash = "sha256:5c507b53c45d2b7cbfcac7643d0123d5f0a7df7d43a882edcdbedb5f6b396421", size = 2074420, upload-time = "2026-08-13T10:02:26.339Z" }, +] + [[package]] name = "django-cors-headers" version = "4.9.0" @@ -218,7 +241,9 @@ name = "fashio" version = "0.1.0" source = { virtual = "." } dependencies = [ + { name = "dj-rest-auth" }, { name = "django" }, + { name = "django-allauth" }, { name = "django-cors-headers" }, { name = "djangorestframework" }, { name = "djangorestframework-simplejwt" }, @@ -237,7 +262,9 @@ dev = [ [package.metadata] requires-dist = [ + { name = "dj-rest-auth", specifier = ">=7.2.0" }, { name = "django", specifier = ">=5.1,<6.0" }, + { name = "django-allauth", specifier = ">=65.19.1" }, { name = "django-cors-headers", specifier = ">=4.4.0" }, { name = "djangorestframework", specifier = ">=3.15.0" }, { name = "djangorestframework-simplejwt", specifier = ">=5.3.1" },