diff --git a/README.md b/README.md index 7ec5701..588d01e 100644 --- a/README.md +++ b/README.md @@ -27,6 +27,12 @@ python manage.py migrate python manage.py runserver ``` +### Tailwind + +```bash +python manage.py tailwind start +``` + ### Tareas de Celery ```bash diff --git a/config/api/v1/urls.py b/config/api/v1/urls.py index c7db314..95007fc 100644 --- a/config/api/v1/urls.py +++ b/config/api/v1/urls.py @@ -9,7 +9,7 @@ urlpatterns = [ "swagger/", SpectacularSwaggerView.as_view(url_name="schema"), name="swagger-ui" ), path("redoc/", SpectacularRedocView.as_view(url_name="schema"), name="redoc"), - path("shop/", include("shop.api.v1.routers", namespace="shop")), + path("shop/", include("shop.api.v1.routers", namespace="shop_api")), path("files/", include("files.api.v1.urls", namespace="files")), path("auth/", include("users.api.v1.urls", namespace="auth")), ] diff --git a/config/urls.py b/config/urls.py index 436870f..149f6b7 100644 --- a/config/urls.py +++ b/config/urls.py @@ -6,6 +6,7 @@ from django.urls import path, include urlpatterns = [ + path("", include("shop.urls")), path("admin/", admin.site.urls), path("api/v1/", include("config.api.v1.urls")), path("watchman/", include("watchman.urls")), diff --git a/shop/urls.py b/shop/urls.py new file mode 100644 index 0000000..3b2da6c --- /dev/null +++ b/shop/urls.py @@ -0,0 +1,9 @@ +from django.urls import path +from shop.views import index + +app_name = "shop" + + +urlpatterns = [ + path("", index, name="index"), +] diff --git a/shop/views.py b/shop/views.py index 91ea44a..ac0316b 100644 --- a/shop/views.py +++ b/shop/views.py @@ -1,3 +1,7 @@ +from django.http.response import HttpResponse from django.shortcuts import render + # Create your views here. +def index(request, *args, **kwargs): + return HttpResponse("Hello there") diff --git a/theme/static_src/tailwind.config.js b/theme/static_src/tailwind.config.js index 9485bc6..a76e4be 100644 --- a/theme/static_src/tailwind.config.js +++ b/theme/static_src/tailwind.config.js @@ -4,54 +4,75 @@ * If you need the full config, get it from here: * https://unpkg.com/browse/tailwindcss@latest/stubs/defaultConfig.stub.js */ +const colors = require('tailwindcss/colors') + module.exports = { - content: [ - /** - * HTML. Paths to Django template files that will contain Tailwind CSS classes. - */ + content: [ + /** + * HTML. Paths to Django template files that will contain Tailwind CSS classes. + */ - /* Templates within theme app (/templates), e.g. base.html. */ - '../templates/**/*.html', + /* Templates within theme app (/templates), e.g. base.html. */ + '../templates/**/*.html', - /* - * Main templates directory of the project (BASE_DIR/templates). - * Adjust the following line to match your project structure. - */ - '../../templates/**/*.html', + /* + * Main templates directory of the project (BASE_DIR/templates). + * Adjust the following line to match your project structure. + */ + '../../templates/**/*.html', - /* - * Templates in other django apps (BASE_DIR//templates). - * Adjust the following line to match your project structure. - */ - '../../**/templates/**/*.html', + /* + * Templates in other django apps (BASE_DIR//templates). + * Adjust the following line to match your project structure. + */ + '../../**/templates/**/*.html', - /** - * JS: If you use Tailwind CSS in JavaScript, uncomment the following lines and make sure - * patterns match your project structure. - */ - /* JS 1: Ignore any JavaScript in node_modules folder. */ - // '!../../**/node_modules', - /* JS 2: Process all JavaScript files in the project. */ - // '../../**/*.js', + /** + * JS: If you use Tailwind CSS in JavaScript, uncomment the following lines and make sure + * patterns match your project structure. + */ + /* JS 1: Ignore any JavaScript in node_modules folder. */ + // '!../../**/node_modules', + /* JS 2: Process all JavaScript files in the project. */ + // '../../**/*.js', - /** - * Python: If you use Tailwind CSS classes in Python, uncomment the following line - * and make sure the pattern below matches your project structure. - */ - // '../../**/*.py' - ], - theme: { - extend: {}, - }, - plugins: [ - /** - * '@tailwindcss/forms' is the forms plugin that provides a minimal styling - * for forms. If you don't like it or have own styling for forms, - * comment the line below to disable '@tailwindcss/forms'. - */ - require('@tailwindcss/forms'), - require('@tailwindcss/typography'), - require('@tailwindcss/aspect-ratio'), - ], + /** + * Python: If you use Tailwind CSS classes in Python, uncomment the following line + * and make sure the pattern below matches your project structure. + */ + // '../../**/*.py' + ], + theme: { + extend: { + colors: { + gray: colors.zinc, + accent: colors.teal, + primary: colors.rose, + secondary: { + 50: '#d1daf5', + 100: '#bac3e0', + 200: '#a4accc', + 300: '#8e96b8', + 400: '#7a81a4', + 500: '#656c91', + DEFAULT: '#52587e', + 600: '#3f446b', + 700: '#2c3259', + 800: '#192047', + 900: '#070e36' + } + } + } + }, + plugins: [ + /** + * '@tailwindcss/forms' is the forms plugin that provides a minimal styling + * for forms. If you don't like it or have own styling for forms, + * comment the line below to disable '@tailwindcss/forms'. + */ + require('@tailwindcss/forms'), + require('@tailwindcss/typography'), + require('@tailwindcss/aspect-ratio'), + ], } diff --git a/theme/templates/theme/base.html b/theme/templates/theme/base.html index 084966e..700bc3e 100644 --- a/theme/templates/theme/base.html +++ b/theme/templates/theme/base.html @@ -5,7 +5,12 @@ {{ title }} + + + {% tailwind_css %} + {% block extra_js %} + {% endblock %} {% block main %} diff --git a/users/forms/__init__.py b/users/forms/__init__.py index e69de29..aba5362 100644 --- a/users/forms/__init__.py +++ b/users/forms/__init__.py @@ -0,0 +1,3 @@ +from .login import * +from .register import * +from .reset_password import * diff --git a/users/forms/login.py b/users/forms/login.py index e69de29..06d8c2b 100644 --- a/users/forms/login.py +++ b/users/forms/login.py @@ -0,0 +1,28 @@ +from django.utils.text import gettext_lazy as _ +from django import forms +from django.contrib.auth.forms import AuthenticationForm, UsernameField + + +class LoginForm(AuthenticationForm): + username = UsernameField( + widget=forms.TextInput( + attrs={ + "autofocus": True, + "class": "bg-gray-50 border border-gray-300 text-gray-900 sm:text-sm rounded-lg focus:ring-primary-600" + "focus:border-primary-600 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600" + "dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500", + } + ) + ) + password = forms.CharField( + label=_("Password"), + strip=False, + widget=forms.PasswordInput( + attrs={ + "autocomplete": "current-password", + "class": "bg-gray-50 border border-gray-300 text-gray-900 sm:text-sm rounded-lg focus:ring-primary-600 " + "focus:border-primary-600 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 " + "dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500", + } + ), + ) diff --git a/users/templates/users/login.html b/users/templates/users/login.html index 6ebbb8d..0841c91 100644 --- a/users/templates/users/login.html +++ b/users/templates/users/login.html @@ -1,45 +1,56 @@ {% extends 'theme/base.html' %} {% block main %} -
-
+
+ - logo - Shoppy + logo + Shoppy -
-
-

- Sign in to your account -

-
-
- - -
-
- - -
-
-
-
- -
-
- -
-
- Forgot password? -
- -

- Don’t have an account yet? Sign up -

-
-
+ +
+
+

+ Sign in to your account +

+
+ {% csrf_token %} + {% for field in form %} + + {{ field }} + {% endfor %} + +
+
+
+ +
+
+ +
+
+ + Forgot password? + +
+ +

+ Don’t have an account yet? + Sign up +

+
+
-
-
+ + + {% endblock %} diff --git a/users/tests/test_change_password.py b/users/tests/test_api_change_password.py similarity index 90% rename from users/tests/test_change_password.py rename to users/tests/test_api_change_password.py index 968edf2..710e10a 100644 --- a/users/tests/test_change_password.py +++ b/users/tests/test_api_change_password.py @@ -1,6 +1,7 @@ from rest_framework import status from rest_framework.test import APITestCase from django.contrib.auth import get_user_model +from django.shortcuts import reverse User = get_user_model() @@ -18,7 +19,7 @@ class TestChangePassword(APITestCase): def test_login_then_change_password(self): response = self.client.post( - "/api/v1/auth/login/", + reverse("auth:login"), { "username": self.user.username, "password": self.password, @@ -29,7 +30,7 @@ class TestChangePassword(APITestCase): jwt_token = response.data.get("access") self.client.credentials(HTTP_AUTHORIZATION=f"Bearer {jwt_token}") response = self.client.put( - "/api/v1/auth/change-password/", + reverse("auth:change_password"), { "new_password": "barad-dur", "new_password2": "barad-dur", @@ -41,7 +42,7 @@ class TestChangePassword(APITestCase): def test_login_then_change_password_with_wrong_password(self): response = self.client.post( - "/api/v1/auth/login/", + reverse("auth:login"), { "username": self.user.username, "password": self.password, @@ -52,7 +53,7 @@ class TestChangePassword(APITestCase): jwt_token = response.data.get("access") self.client.credentials(HTTP_AUTHORIZATION=f"Bearer {jwt_token}") response = self.client.put( - "/api/v1/auth/change-password/", + reverse("auth:change_password"), { "new_password": "barad-dur", "new_password2": "barad-dur", @@ -64,7 +65,7 @@ class TestChangePassword(APITestCase): def test_login_then_change_password_mismatch_password(self): response = self.client.post( - "/api/v1/auth/login/", + reverse("auth:login"), { "username": self.user.username, "password": self.password, @@ -75,7 +76,7 @@ class TestChangePassword(APITestCase): jwt_token = response.data.get("access") self.client.credentials(HTTP_AUTHORIZATION=f"Bearer {jwt_token}") response = self.client.put( - "/api/v1/auth/change-password/", + reverse("auth:change_password"), { "new_password": "barad-dur", "new_password2": "mountdoom", @@ -87,7 +88,7 @@ class TestChangePassword(APITestCase): def test_login_then_change_password_but_its_the_same(self): response = self.client.post( - "/api/v1/auth/login/", + reverse("auth:login"), { "username": self.user.username, "password": self.password, @@ -98,7 +99,7 @@ class TestChangePassword(APITestCase): jwt_token = response.data.get("access") self.client.credentials(HTTP_AUTHORIZATION=f"Bearer {jwt_token}") response = self.client.put( - "/api/v1/auth/change-password/", + reverse("auth:change_password"), { "new_password": self.password, "new_password2": self.password, diff --git a/users/tests/test_api_login.py b/users/tests/test_api_login.py new file mode 100644 index 0000000..ab9f39e --- /dev/null +++ b/users/tests/test_api_login.py @@ -0,0 +1,101 @@ +from rest_framework import status +from rest_framework.test import APITestCase +from django.contrib.auth import get_user_model +from django.shortcuts import reverse + +User = get_user_model() + + +class TestLogin(APITestCase): + def setUp(self) -> None: + self.password = "theonering" + self.user = User.objects.create( + username="sauron", + email="sauron@mordor.middleearth", + ) + + self.user.set_password(self.password) + self.user.save() + + def test_login(self): + response = self.client.post( + reverse("auth:login"), + { + "username": self.user.username, + "password": self.password, + }, + ) + + assert response.status_code == status.HTTP_200_OK + assert response.data.get("access") is not None + assert response.data.get("refresh") is not None + + def test_login_then_verify(self): + response = self.client.post( + reverse("auth:login"), + { + "username": self.user.username, + "password": self.password, + }, + ) + + assert response.status_code == status.HTTP_200_OK + jwt_token = response.data.get("access") + response = self.client.post( + reverse("auth:verify_jwt"), + { + "token": jwt_token, + }, + ) + + assert response.status_code == status.HTTP_200_OK + + def test_login_then_refresh(self): + response = self.client.post( + reverse("auth:login"), + { + "username": self.user.username, + "password": self.password, + }, + ) + + assert response.status_code == status.HTTP_200_OK + jwt_token = response.data.get("refresh") + response = self.client.post( + reverse("auth:refresh_jwt"), + { + "refresh": jwt_token, + }, + ) + + assert response.status_code == status.HTTP_200_OK + assert response.data.get("access") is not None + assert response.data.get("access") != jwt_token + + def test_login_then_get_user_info(self): + response = self.client.post( + reverse("auth:login"), + { + "username": self.user.username, + "password": self.password, + }, + ) + + assert response.status_code == status.HTTP_200_OK + jwt_token = response.data.get("access") + self.client.credentials(HTTP_AUTHORIZATION=f"Bearer {jwt_token}") + response = self.client.get(reverse("auth:user_info")) + + assert response.status_code == status.HTTP_200_OK + assert response.data.get("email") == self.user.email + + def test_login_failed(self): + response = self.client.post( + reverse("auth:login"), + { + "username": self.user.username, + "password": "wrongpassword", + }, + ) + + assert response.status_code == status.HTTP_401_UNAUTHORIZED diff --git a/users/tests/test_login.py b/users/tests/test_login.py index 14a69bc..f035348 100644 --- a/users/tests/test_login.py +++ b/users/tests/test_login.py @@ -1,6 +1,7 @@ from rest_framework import status from rest_framework.test import APITestCase from django.contrib.auth import get_user_model +from django.shortcuts import reverse User = get_user_model() @@ -18,83 +19,64 @@ class TestLogin(APITestCase): def test_login(self): response = self.client.post( - "/api/v1/auth/login/", + reverse("users:login"), { "username": self.user.username, "password": self.password, }, ) - assert response.status_code == status.HTTP_200_OK - assert response.data.get("access") is not None - assert response.data.get("refresh") is not None + assert response.status_code == status.HTTP_302_FOUND + assert response.url == reverse("shop:index") - def test_login_then_verify(self): + def test_login_get(self): + response = self.client.get(reverse("users:login")) + assert response.status_code == status.HTTP_200_OK + + def test_login_already_logged_in(self): response = self.client.post( - "/api/v1/auth/login/", + reverse("users:login"), { "username": self.user.username, "password": self.password, }, ) - assert response.status_code == status.HTTP_200_OK - jwt_token = response.data.get("access") - response = self.client.post( - "/api/v1/auth/verify/", - { - "token": jwt_token, - }, - ) + assert response.status_code == status.HTTP_302_FOUND + assert response.url == reverse("shop:index") - assert response.status_code == status.HTTP_200_OK - - def test_login_then_refresh(self): - response = self.client.post( - "/api/v1/auth/login/", - { - "username": self.user.username, - "password": self.password, - }, - ) - - assert response.status_code == status.HTTP_200_OK - jwt_token = response.data.get("refresh") - response = self.client.post( - "/api/v1/auth/refresh/", - { - "refresh": jwt_token, - }, - ) - - assert response.status_code == status.HTTP_200_OK - assert response.data.get("access") is not None - assert response.data.get("access") != jwt_token - - def test_login_then_get_user_info(self): - response = self.client.post( - "/api/v1/auth/login/", - { - "username": self.user.username, - "password": self.password, - }, - ) - - assert response.status_code == status.HTTP_200_OK - jwt_token = response.data.get("access") - self.client.credentials(HTTP_AUTHORIZATION=f"Bearer {jwt_token}") - response = self.client.get("/api/v1/auth/me/") - - assert response.status_code == status.HTTP_200_OK - assert response.data.get("email") == self.user.email + response = self.client.get(reverse("users:login")) + assert response.status_code == status.HTTP_302_FOUND + assert response.url == reverse("shop:index") def test_login_failed(self): response = self.client.post( - "/api/v1/auth/login/", + reverse("users:login"), { "username": self.user.username, "password": "wrongpassword", }, ) - assert response.status_code == status.HTTP_401_UNAUTHORIZED + assert response.status_code == status.HTTP_400_BAD_REQUEST + + def test_login_then_logout(self): + response = self.client.post( + reverse("users:login"), + { + "username": self.user.username, + "password": self.password, + }, + ) + + assert response.status_code == status.HTTP_302_FOUND + assert response.url == reverse("shop:index") + + response = self.client.post(reverse("users:logout")) + assert response.status_code == status.HTTP_302_FOUND + assert response.url == reverse("shop:index") + + def test_logout_already_logged_out(self): + response = self.client.post(reverse("users:logout")) + assert response.status_code == status.HTTP_302_FOUND + assert response.url == reverse("shop:index") diff --git a/users/urls.py b/users/urls.py index 0170a02..f740d6e 100644 --- a/users/urls.py +++ b/users/urls.py @@ -1,11 +1,12 @@ from django.urls import path -from users.views import login, register, reset_password +from users.views import login, logout, register, reset_password app_name = "users" urlpatterns = [ path("login/", login, name="login"), + path("logout/", logout, name="logout"), path("register/", register, name="register"), path("reset-password/", reset_password, name="reset-password"), ] diff --git a/users/views.py b/users/views.py index 79bd669..251d498 100644 --- a/users/views.py +++ b/users/views.py @@ -1,18 +1,63 @@ -from django.views.generic import TemplateView +from django.views.generic import TemplateView, View +from django.shortcuts import render, reverse, redirect +from users.forms import LoginForm +from django.contrib.auth import login as login_user, logout as logout_user -class LoginView(TemplateView): - template_name = 'users/login.html' +class RedirectionMixin: + redirect_to = "" + + def get_redirection(self): + return reverse(self.redirect_to) + + +class LoginView(View, RedirectionMixin): + template_name = "users/login.html" + redirect_to = "shop:index" + login_form_class = LoginForm + + def get_login_form(self, request): + return self.login_form_class(data=request.POST) + + def get(self, request, *args, **kwargs): + if request.user.is_authenticated: + return redirect(self.get_redirection()) + + login_form = self.login_form_class() + return render(request, self.template_name, {"form": login_form}) + + def post(self, request, *args, **kwargs): + login_form = self.get_login_form(request) + + if login_form.is_valid(): + login_user(request, login_form.get_user()) + + return redirect(self.get_redirection()) + + return render(request, self.template_name, {"form": login_form}, status=400) + + +class LogoutView(View, RedirectionMixin): + redirect_to = "shop:index" + + def get(self, request, *args, **kwargs): + logout_user(request) + return redirect(self.get_redirection()) + + def post(self, request, *args, **kwargs): + logout_user(request) + return redirect(self.get_redirection()) class RegisterView(TemplateView): - template_name = 'users/register.html' + template_name = "users/register.html" class ResetPasswordView(TemplateView): - template_name = 'users/reset-password.html' + template_name = "users/reset-password.html" login = LoginView.as_view() +logout = LogoutView.as_view() register = RegisterView.as_view() reset_password = ResetPasswordView.as_view()