feat: login view

This commit is contained in:
2024-11-21 11:43:38 +01:00
parent 96eb517962
commit 2c7e51d602
15 changed files with 369 additions and 151 deletions
+6
View File
@@ -27,6 +27,12 @@ python manage.py migrate
python manage.py runserver
```
### Tailwind
```bash
python manage.py tailwind start
```
### Tareas de Celery
```bash
+1 -1
View File
@@ -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")),
]
+1
View File
@@ -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")),
+9
View File
@@ -0,0 +1,9 @@
from django.urls import path
from shop.views import index
app_name = "shop"
urlpatterns = [
path("", index, name="index"),
]
+4
View File
@@ -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")
+22 -1
View File
@@ -4,6 +4,8 @@
* 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: [
@@ -42,7 +44,26 @@ module.exports = {
// '../../**/*.py'
],
theme: {
extend: {},
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: [
/**
+5
View File
@@ -5,7 +5,12 @@
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>{{ title }}</title>
<script src="https://unpkg.com/htmx.org@2.0.3"></script>
{% tailwind_css %}
{% block extra_js %}
{% endblock %}
</head>
<body>
{% block main %}
+3
View File
@@ -0,0 +1,3 @@
from .login import *
from .register import *
from .reset_password import *
+28
View File
@@ -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",
}
),
)
+27 -16
View File
@@ -1,45 +1,56 @@
{% extends 'theme/base.html' %}
{% block main %}
<section class="bg-gray-50 dark:bg-gray-900">
<div class="flex flex-col items-center justify-center px-6 py-8 mx-auto md:h-screen lg:py-0">
<a href="#" class="flex items-center mb-6 text-2xl font-semibold text-gray-900 dark:text-white">
<img class="w-8 h-8 mr-2" src="https://flowbite.s3.amazonaws.com/blocks/marketing-ui/logo.svg" alt="logo">
Shoppy
</a>
<div class="w-full bg-white rounded-lg shadow dark:border md:mt-0 sm:max-w-md xl:p-0 dark:bg-gray-800 dark:border-gray-700">
<div
class="w-full bg-white rounded-lg shadow dark:border md:mt-0 sm:max-w-md xl:p-0 dark:bg-gray-800 dark:border-gray-700">
<div class="p-6 space-y-4 md:space-y-6 sm:p-8">
<h1 class="text-xl font-bold leading-tight tracking-tight text-gray-900 md:text-2xl dark:text-white">
Sign in to your account
</h1>
<form class="space-y-4 md:space-y-6" action="#">
<div>
<label for="email" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Your email</label>
<input type="email" name="email" id="email" 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" placeholder="name@company.com" required="">
</div>
<div>
<label for="password" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Password</label>
<input type="password" name="password" id="password" placeholder="••••••••" 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" required="">
</div>
<form class="space-y-4 md:space-y-6" action="{% url 'users:login' %}" method="post">
{% csrf_token %}
{% for field in form %}
<label for="{{ field.id_for_label }}"
class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">
{{ field.label }}
</label>
{{ field }}
{% endfor %}
<div class="flex items-center justify-between">
<div class="flex items-start">
<div class="flex items-center h-5">
<input id="remember" aria-describedby="remember" type="checkbox" class="w-4 h-4 border border-gray-300 rounded bg-gray-50 focus:ring-3 focus:ring-primary-300 dark:bg-gray-700 dark:border-gray-600 dark:focus:ring-primary-600 dark:ring-offset-gray-800" required="">
<input id="remember" aria-describedby="remember" type="checkbox"
class="w-4 h-4 border border-gray-300 rounded bg-gray-50 focus:ring-3 focus:ring-primary-300 dark:bg-gray-700 dark:border-gray-600 dark:focus:ring-primary-600 dark:ring-offset-gray-800">
</div>
<div class="ml-3 text-sm">
<label for="remember" class="text-gray-500 dark:text-gray-300">Remember me</label>
</div>
</div>
<a href="#" class="text-sm font-medium text-primary-600 hover:underline dark:text-primary-500">Forgot password?</a>
<a href="#" class="text-sm font-medium text-primary-600 hover:underline dark:text-primary-500">
Forgot password?
</a>
</div>
<button type="submit" class="w-full text-white bg-primary-600 hover:bg-primary-700 focus:ring-4 focus:outline-none focus:ring-primary-300 font-medium rounded-lg text-sm px-5 py-2.5 text-center dark:bg-primary-600 dark:hover:bg-primary-700 dark:focus:ring-primary-800">Sign in</button>
<button
class="w-full text-white bg-primary-600 hover:bg-primary-700 focus:ring-4 focus:outline-none focus:ring-primary-300 font-medium rounded-lg text-sm px-5 py-2.5 text-center dark:bg-primary-600 dark:hover:bg-primary-700 dark:focus:ring-primary-800">
Login
</button>
<p class="text-sm font-light text-gray-500 dark:text-gray-400">
Dont have an account yet? <a href="#" class="font-medium text-primary-600 hover:underline dark:text-primary-500">Sign up</a>
Dont have an account yet?
<a href="#" class="font-medium text-primary-600 hover:underline dark:text-primary-500">Sign up</a>
</p>
</form>
</div>
</div>
</div>
</section>
</section>
{% endblock %}
@@ -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,
+101
View File
@@ -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
+38 -56
View File
@@ -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")
+2 -1
View File
@@ -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"),
]
+50 -5
View File
@@ -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()