14 Commits

Author SHA1 Message Date
mono
079f4e3806 fix: copy application files directly into image root
- Copy tienda_ilusion contents to /app/ instead of subdirectory
- Remove volume mount from production docker-compose.yml
- Allows deployment without repository dependency

Refs #37
2026-03-30 20:45:45 -05:00
mono
6473a8348c feat: configure docker image deployment with Gitea Container Registry
- Replace local build with pre-built image from Gitea registry
- Add docker-compose.override.yml for local development with Dockerfile
- Add .dockerignore to exclude unnecessary files from build
- Update image reference to gitea.onecluster.org/oneteam/don_confiao_backend:latest

Refs #37
2026-03-30 20:34:32 -05:00
1d7beadf96 Merge pull request 'Agregar método de pago crédito #20' (#36) from add_credit_pay_method_#20 into main
Reviewed-on: #36
2026-03-14 23:46:27 -05:00
mono
5442a52ff6 feat(Payment): add CREDIT payment method 2026-03-14 23:45:52 -05:00
b852772c76 Merge pull request 'Agregando rol administrativo #31' (#35) from add_administrative_rol_#31 into main
Reviewed-on: #35
2026-03-14 18:26:31 -05:00
mono
0d5a34d366 feat: add IsAdministrator permission and protect admin endpoints 2026-03-14 16:37:44 -05:00
mono
7e2c03c81b test: add tests for user role field 2026-03-14 16:27:44 -05:00
mono
83029afd5b feat: add role field to user serializer for menu control
- Endpoint /me/ now returns 'role': 'administrator' or 'role': 'user'
- Uses Django's is_staff to determine administrator role
2026-03-07 17:58:12 -05:00
mono
2ab328b913 docs: add AGENTS.md with project context for opencode 2026-03-07 17:52:48 -05:00
a05061c14e Merge pull request 'Cambiando autenticación de api a JWT #29' (#33) from add_jwt_authentication_#29 into main
Reviewed-on: #33
2026-02-14 15:57:03 -05:00
7c0047b4d3 fix: rm duplicate file. 2026-02-14 15:55:59 -05:00
c021104b62 doc(API): add sample requests. 2026-02-14 15:50:30 -05:00
7a9034943a feat(API): change to jwt authentication. 2026-02-14 15:12:32 -05:00
4812160ea2 Merge pull request 'Se adiciona autenticación a django' (#30) from add_authentication_#29 into main
Reviewed-on: #30
2025-12-13 17:35:34 -05:00
28 changed files with 440 additions and 87 deletions

17
.dockerignore Normal file
View File

@@ -0,0 +1,17 @@
__pycache__/
*.py[cod]
*$py.class
*.so
.Python
*.db
*.sqlite3
.env
.venv
venv/
ENV/
.git/
*.egg-info/
dist/
build/
.coverage
htmlcov/

108
AGENTS.md Normal file
View File

@@ -0,0 +1,108 @@
# Don Confiao Backend - Contexto del Proyecto
## Tipo de Proyecto
Backend Django con Django REST Framework
## Estructura del Proyecto
```
don_confiao_backend/
├── requirements.txt # Dependencias Python
├── docker-compose.yml # Configuración Docker
├── django.Dockerfile # Dockerfile Django
├── .env # Variables de entorno
├── .env_example # Ejemplo de variables de entorno
├── README.rst # Documentación básica
├── Rakefile # Tareas rake
├── doc/ # Documentación adicional
│ └── requests.org
└── tienda_ilusion/ # Proyecto Django
├── manage.py
├── db.sqlite3 # Base de datos SQLite
├── don_confiao/ # App principal
│ ├── models.py # Modelos: Customer, Product, Sale, SaleLine, Payment, ReconciliationJar, AdminCode
│ ├── views.py
│ ├── api_views.py
│ ├── serializers.py
│ ├── forms.py
│ ├── admin.py
│ ├── urls.py
│ ├── export_csv.py
│ ├── tests/ # Tests
│ └── migrations/
├── users/ # App de usuarios
│ ├── models.py
│ ├── views.py
│ ├── serializers.py
│ ├── urls.py
│ └── tests/
└── tienda_ilusion/ # Configuración Django
├── settings.py
├── urls.py
├── wsgi.py
└── asgi.py
```
## Dependencias Principales
- Django==5.0.6
- djangorestframework
- django-cors-headers
- djangorestframework-simplejwt
- sabatron-tryton-rpc-client==7.4.0 (integración con Tryton ERP)
## Modelos Principales (don_confiao/models.py)
- **Customer**: Clientes (name, address, email, phone, external_id)
- **Product**: Productos (name, price, measuring_unit, categories)
- **ProductCategory**: Categorías de productos
- **Sale**: Ventas (customer, date, phone, description, payment_method, reconciliation)
- **SaleLine**: Líneas de venta (sale, product, quantity, unit_price, description)
- **Payment**: Pagos (date_time, type_payment, amount, reconciliation_jar)
- **PaymentSale**: Relación muchos a muchos entre Payment y Sale
- **ReconciliationJar**: Arqueo de caja (is_valid, date_time, reconcilier, cash_taken, cash_discrepancy)
- **AdminCode**: Códigos de administrador
## Autenticación
- JWT con djangorestframework-simplejwt
- ACCESS_TOKEN_LIFETIME: 30 minutos
- REFRESH_TOKEN_LIFETIME: 1 día
## API Endpoints
- REST API en don_confiao/api_views.py y users/
- URLs en don_confiao/urls.py y users/urls.py
## Ejecución con Docker Compose
El proyecto se ejecuta con docker-compose. Todos los comandos `manage.py` deben ejecutarse dentro del contenedor:
```bash
# Ejecutar tests
docker-compose run --rm django python manage.py test
# Migraciones
docker-compose run --rm django python manage.py makemigrations
docker-compose run --rm django python manage.py migrate
# Servidor desarrollo
docker-compose up
# Shell Django
docker-compose run --rm django python manage.py shell
# Crear superuser
docker-compose run --rm django python manage.py createsuperuser
```
Nota: El volumen monta `tienda_ilusion/` en `/app/`, por lo que el path correcto es `python manage.py` (no `python tienda_ilusion/manage.py`).
## Tests
- Framework: Django unittest
- Directorio: don_confiao/tests/
- Ejecutar: `docker-compose run --rm django python manage.py test`
## Comandos Útiles (dentro del contenedor)
- Migraciones: `docker-compose run --rm django python manage.py makemigrations && docker-compose run --rm django python manage.py migrate`
- Servidor desarrollo: `docker-compose up`
- Shell Django: `docker-compose run --rm django python manage.py shell`
- Superuser: `docker-compose run --rm django python manage.py createsuperuser`
## Integraciones
- **Tryton ERP**: Integración mediante sabatron-tryton-rpc-client para sincronización de clientes, productos y ventas

View File

@@ -5,4 +5,6 @@ WORKDIR /app/
COPY requirements.txt ./
RUN pip install --no-cache-dir -r requirements.txt
COPY tienda_ilusion/ ./
CMD ["python", "manage.py", "runserver", "0.0.0.0:9090"]

76
doc/requests.org Normal file
View File

@@ -0,0 +1,76 @@
* Requests
Ejemplo de request contra la api usando [[https://github.com/federicotdn/verb][verb]]
** Autenticación :verb:
template http://localhost:7000/api
Content-Type: application/json;
*** Solicitar token
post /token/
{
"username": "admin",
"password": "123"
}
**** respuesta
#+begin_src json
{
"refresh": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0b2tlbl90eXBlIjoicmVmcmVzaCIsImV4cCI6MTc3MTE4NzYxOSwiaWF0IjoxNzcxMTAxMjE5LCJqdGkiOiI5ZTgzNGRlM2QzMmQ0NmQyODEwZGQ2MjI2ODUwNjgzNyIsInVzZXJfaWQiOiIyIn0.JaUOqEAZ2T8vVT36mXfweMmYjEWsP7toD07jeeyrl1k",
"access": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0b2tlbl90eXBlIjoiYWNjZXNzIiwiZXhwIjoxNzcxMTAzMDE5LCJpYXQiOjE3NzExMDEyMTksImp0aSI6ImFmOWFjNGM1MzBiZjQ4ZGE4Yzg2MWFjYzIzNjQ3NjU3IiwidXNlcl9pZCI6IjIifQ.6wH5sx1fyFn3Wt3DVZGYbiYi79rGthUZkgGmTqzebXc"
}
#+end_src
*** Perfil de usuario
get /users/me/
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0b2tlbl90eXBlIjoiYWNjZXNzIiwiZXhwIjoxNzcxMTAzMDE5LCJpYXQiOjE3NzExMDEyMTksImp0aSI6ImFmOWFjNGM1MzBiZjQ4ZGE4Yzg2MWFjYzIzNjQ3NjU3IiwidXNlcl9pZCI6IjIifQ.6wH5sx1fyFn3Wt3DVZGYbiYi79rGthUZkgGmTqzebXc
**** Respuesta
#+begin_src json
{
"id": 2,
"username": "admin",
"email": "correo@example.com",
"first_name": "",
"last_name": ""
}
#+end_src
*** Renovar token
post /token/refresh/
{
"refresh": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0b2tlbl90eXBlIjoicmVmcmVzaCIsImV4cCI6MTc3MTE4NzYxOSwiaWF0IjoxNzcxMTAxMjE5LCJqdGkiOiI5ZTgzNGRlM2QzMmQ0NmQyODEwZGQ2MjI2ODUwNjgzNyIsInVzZXJfaWQiOiIyIn0.JaUOqEAZ2T8vVT36mXfweMmYjEWsP7toD07jeeyrl1k"
}
**** response
#+begin_src json
{
"access": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0b2tlbl90eXBlIjoiYWNjZXNzIiwiZXhwIjoxNzcxMTAzNjA1LCJpYXQiOjE3NzExMDE4MDUsImp0aSI6ImJjZTY5ZTA3MTIyOTQxMTg5NmFjYzk1ZDNiOThhMTI0IiwidXNlcl9pZCI6IjIifQ.b4Z1c_Yi5tsLZ-7F0KZcM2tai-f1VeaE881j2pKDwYA"
}
#+end_src
** Don confiao :verb:
template http://localhost:7000/don_confiao/api/
Content-Type: application/json;
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0b2tlbl90eXBlIjoiYWNjZXNzIiwiZXhwIjoxNzcxMTAzNjA1LCJpYXQiOjE3NzExMDE4MDUsImp0aSI6ImJjZTY5ZTA3MTIyOTQxMTg5NmFjYzk1ZDNiOThhMTI0IiwidXNlcl9pZCI6IjIifQ.b4Z1c_Yi5tsLZ-7F0KZcM2tai-f1VeaE881j2pKDwYA
*** todas las rutas
get
**** response
#+begin_src json
{
"sales": "http://localhost:7000/don_confiao/api/sales/",
"customers": "http://localhost:7000/don_confiao/api/customers/",
"products": "http://localhost:7000/don_confiao/api/products/",
"reconciliate_jar": "http://localhost:7000/don_confiao/api/reconciliate_jar/"
}
#+end_src
*** customers
get customers/
**** response
#+begin_src json
[
{
"id": 1,
"name": "Consumidor Final",
"address": "",
"email": "",
"phone": "",
"external_id": "2753"
},
...
]
#+end_src

View File

@@ -0,0 +1,11 @@
services:
django:
build:
context: ./
dockerfile: django.Dockerfile
volumes:
- ./tienda_ilusion:/app/
env_file:
- .env
ports:
- "7000:9090"

View File

@@ -1,12 +1,8 @@
services:
django:
build:
context: ./
dockerfile: django.Dockerfile
image: gitea.onecluster.org/oneteam/don_confiao_backend:latest
env_file:
- .env
volumes:
- ./tienda_ilusion:/app/
ports:
- "7000:9090"

View File

@@ -1,4 +1,5 @@
Django==5.0.6
djangorestframework
django-cors-headers
djangorestframework-simplejwt
sabatron-tryton-rpc-client==7.4.0

View File

@@ -3,10 +3,12 @@ from rest_framework.response import Response
from rest_framework.status import HTTP_400_BAD_REQUEST
from rest_framework.views import APIView
from rest_framework.pagination import PageNumberPagination
from rest_framework.permissions import IsAuthenticated
from .models import Sale, SaleLine, Customer, Product, ReconciliationJar, PaymentMethods, AdminCode
from .serializers import SaleSerializer, ProductSerializer, CustomerSerializer, ReconciliationJarSerializer, PaymentMethodSerializer, SaleForRenconciliationSerializer, SaleSummarySerializer
from .views import sales_to_tryton_csv
from .permissions import IsAdministrator
from decimal import Decimal
from sabatron_tryton_rpc_client.client import Client
@@ -74,6 +76,8 @@ class CustomerView(viewsets.ModelViewSet):
class ReconciliateJarView(APIView):
permission_classes = [IsAuthenticated, IsAdministrator]
def post(self, request):
data = request.data
cash_purchases_id = data.get('cash_purchases')
@@ -131,6 +135,8 @@ class PaymentMethodView(APIView):
class SalesForReconciliationView(APIView):
permission_classes = [IsAuthenticated, IsAdministrator]
def get(self, request):
sales = Sale.objects.filter(reconciliation=None)
grouped_sales = {}
@@ -152,6 +158,8 @@ class SaleSummary(APIView):
class AdminCodeValidateView(APIView):
permission_classes = [IsAuthenticated, IsAdministrator]
def get(self, request, code):
codes = AdminCode.objects.filter(value=code)
return Response({'validCode': bool(codes)})
@@ -161,9 +169,12 @@ class ReconciliateJarModelView(viewsets.ModelViewSet):
queryset = ReconciliationJar.objects.all().order_by('-date_time')
pagination_class = Pagination
serializer_class = ReconciliationJarSerializer
permission_classes = [IsAuthenticated, IsAdministrator]
class SalesForTrytonView(APIView):
permission_classes = [IsAuthenticated, IsAdministrator]
def get(self, request):
sales = Sale.objects.all()
csv = self._generate_sales_CSV(sales)
@@ -180,6 +191,8 @@ class SalesForTrytonView(APIView):
class SalesToTrytonView(APIView):
permission_classes = [IsAuthenticated, IsAdministrator]
def post(self, request):
tryton_client = Client(
hostname=TRYTON_HOST,
@@ -269,6 +282,8 @@ class TrytonLineSale:
class ProductsFromTrytonView(APIView):
permission_classes = [IsAuthenticated, IsAdministrator]
def post(self, request):
tryton_client = Client(
hostname=TRYTON_HOST,
@@ -362,6 +377,8 @@ class ProductsFromTrytonView(APIView):
class CustomersFromTrytonView(APIView):
permission_classes = [IsAuthenticated, IsAdministrator]
def post(self, request):
tryton_client = Client(
hostname=TRYTON_HOST,

View File

@@ -0,0 +1,23 @@
# Generated by Django 5.0.6 on 2026-03-15 04:39
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('don_confiao', '0043_customer_address_external_id'),
]
operations = [
migrations.AlterField(
model_name='payment',
name='type_payment',
field=models.CharField(choices=[('CASH', 'Efectivo'), ('CONFIAR', 'Confiar'), ('BANCOLOMBIA', 'Bancolombia'), ('CREDIT', 'Crédito')], default='CASH', max_length=30),
),
migrations.AlterField(
model_name='sale',
name='payment_method',
field=models.CharField(choices=[('CASH', 'Efectivo'), ('CONFIAR', 'Confiar'), ('BANCOLOMBIA', 'Bancolombia'), ('CREDIT', 'Crédito')], default='CASH', max_length=30),
),
]

View File

@@ -10,6 +10,7 @@ class PaymentMethods(models.TextChoices):
CASH = 'CASH', _('Efectivo')
CONFIAR = 'CONFIAR', _('Confiar')
BANCOLOMBIA = 'BANCOLOMBIA', _('Bancolombia')
CREDIT = 'CREDIT', _('Crédito')
class Customer(models.Model):

View File

@@ -0,0 +1,6 @@
from rest_framework.permissions import BasePermission
class IsAdministrator(BasePermission):
def has_permission(self, request, view):
return request.user and request.user.is_staff

View File

@@ -1,10 +1,19 @@
from django.contrib.auth.models import User
from rest_framework_simplejwt.tokens import RefreshToken
from rest_framework.test import APIClient
class LoginMixin:
def login(self):
username = 'nombre_usuario'
password = 'contraseña'
email = 'correo@example.com'
self.user = User.objects.create_user(username, email, password)
self.client.login(username=username, password=password)
self.user = User.objects.create_superuser(
username='admin',
email='admin@example.com',
password='adminpass'
)
refresh = RefreshToken.for_user(self.user)
self.access_token = str(refresh.access_token)
self.client = APIClient()
self.client.credentials(
HTTP_AUTHORIZATION=f'Bearer {self.access_token}')

View File

@@ -21,3 +21,4 @@ class TestPaymentMethods(TestCase, LoginMixin):
self.assertIn('CASH', [method.get('value') for method in methods])
self.assertIn('CONFIAR', [method.get('value') for method in methods])
self.assertIn('BANCOLOMBIA', [method.get('value') for method in methods])
self.assertIn('CREDIT', [method.get('value') for method in methods])

View File

@@ -11,6 +11,7 @@ https://docs.djangoproject.com/en/5.0/ref/settings/
"""
import os
from datetime import timedelta
from pathlib import Path
# Build paths inside the project like this: BASE_DIR / 'subdir'.
@@ -44,7 +45,9 @@ INSTALLED_APPS = [
'django.contrib.messages',
'django.contrib.staticfiles',
'rest_framework',
'rest_framework.authtoken',
'corsheaders',
'users',
# 'don_confiao'
]
@@ -57,7 +60,6 @@ MIDDLEWARE = [
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
'corsheaders.middleware.CorsMiddleware',
'django.middleware.common.CommonMiddleware',
]
ROOT_URLCONF = 'tienda_ilusion.urls'
@@ -137,11 +139,19 @@ FIXTURE_DIRS = ['don_confiao/tests/Fixtures']
REST_FRAMEWORK = {
'DEFAULT_AUTHENTICATION_CLASSES': [
'rest_framework.authentication.SessionAuthentication',
"rest_framework_simplejwt.authentication.JWTAuthentication",
],
'DEFAULT_PERMISSION_CLASSES': [
'rest_framework.permissions.IsAuthenticated',
],
}
LOGOUT_REDIRECT_URL = '/start/'
SIMPLE_JWT = {
"ACCESS_TOKEN_LIFETIME": timedelta(minutes=30),
"REFRESH_TOKEN_LIFETIME": timedelta(days=1),
"AUTH_HEADER_TYPES": ("Bearer",),
}
# CORS_ALLOWED_ORIGINS = [
# "http://localhost:5173",
# ]

View File

@@ -1,5 +0,0 @@
<form method="post">
{% csrf_token %}
{{ form.as_p }}
<button type="submit">Iniciar sesión</button>
</form>

View File

@@ -1,27 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<title>Perfil de usuario</title>
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.min.css">
<style>
body {
font-family: Arial, sans-serif;
}
</style>
</head>
<body>
<div class="container">
<div class="row justify-content-center">
<div class="col-md-8">
<h1>Perfil de usuario</h1>
<p>Nombre de usuario: {{ user.username }}</p>
<p>Email: {{ user.email }}</p>
<form action="{% url 'logout' %}" method="post" style="display: inline-block;">
{% csrf_token %}
<button type="submit" class="btn btn-danger">Cerrar sesión</button>
</form>
</div>
</div>
</div>
</body>
</html>

View File

@@ -1,22 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<title>Bienvenido a la tienda la ilusión</title>
<style>
body {
font-family: Arial, sans-serif;
}
</style>
</head>
<body>
<div class="container">
<div class="row justify-content-center">
<div class="col-md-8">
<h1>Bienvenido a la tienda la ilusion</h1>
<a href="{% url 'login' %}">Login</a>
</div>
</div>
</div>
</body>
</html>

View File

@@ -16,15 +16,19 @@ Including another URLconf
"""
from django.contrib import admin
from django.urls import include, path
from . import views
from rest_framework_simplejwt.views import (
TokenObtainPairView,
TokenRefreshView,
)
app_name = "don_confiao"
urlpatterns = [
path("don_confiao/", include("don_confiao.urls")),
path("accounts/", include("django.contrib.auth.urls")),
path('accounts/profile/', views.ProfileView.as_view(), name='profile'),
path('start/', views.StartView.as_view(), name='start'),
path('admin/', admin.site.urls),
path('api/token/', TokenObtainPairView.as_view(),
name='token_obtain_pair'),
path('api/token/refresh/', TokenRefreshView.as_view(),
name='token_refresh'),
path('api/users/', include('users.urls')),
]

View File

@@ -1,15 +0,0 @@
from django.views.generic import TemplateView
from django.contrib.auth.mixins import LoginRequiredMixin
class ProfileView(LoginRequiredMixin, TemplateView):
template_name = 'registration/profile.html'
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['user'] = self.request.user
return context
class StartView(TemplateView):
template_name = 'start.html'

View File

View File

@@ -0,0 +1,3 @@
from django.contrib import admin
# Register your models here.

View File

@@ -0,0 +1,5 @@
from django.apps import AppConfig
class UsersConfig(AppConfig):
name = 'users'

View File

@@ -0,0 +1,3 @@
from django.db import models
# Create your models here.

View File

@@ -0,0 +1,13 @@
from django.contrib.auth.models import User
from rest_framework import serializers
class UserSerializer(serializers.ModelSerializer):
role = serializers.SerializerMethodField()
class Meta:
model = User
fields = ('id', 'username', 'email', 'first_name', 'last_name', 'role')
def get_role(self, obj):
return 'administrator' if obj.is_staff else 'user'

View File

@@ -0,0 +1,98 @@
from django.test import TestCase
from django.urls import reverse
from django.contrib.auth.models import User
from rest_framework.test import APIClient
from rest_framework_simplejwt.tokens import RefreshToken
class MeEndpointTests(TestCase):
def setUp(self):
self.user = User.objects.create_superuser(
username='admin',
email='admin@example.com',
password='adminpass'
)
refresh = RefreshToken.for_user(self.user)
self.access_token = str(refresh.access_token)
self.client = APIClient()
self.client.credentials(
HTTP_AUTHORIZATION=f'Bearer {self.access_token}')
def test_me_endpoint_returns_correct_user_data(self):
"""
Verifica que GET /api/users/me/ devuelve los datos del usuario
autenticado.
"""
url = reverse('current-user')
response = self.client.get(url)
self.assertEqual(response.status_code, 200)
expected_fields = {'id', 'username', 'email',
'first_name', 'last_name', 'role'}
self.assertTrue(expected_fields.issubset(response.json().keys()))
data = response.json()
self.assertEqual(data['username'], self.user.username)
self.assertEqual(data['email'], self.user.email)
self.assertEqual(data['first_name'], self.user.first_name)
self.assertEqual(data['last_name'], self.user.last_name)
self.assertEqual(data['role'], 'administrator')
def test_regular_user_role_is_user(self):
"""
Verifica que un usuario sin permisos de staff recibe role 'user'.
"""
regular_user = User.objects.create_user(
username='regular',
email='regular@example.com',
password='regularpass',
is_staff=False
)
refresh = RefreshToken.for_user(regular_user)
access_token = str(refresh.access_token)
client = APIClient()
client.credentials(HTTP_AUTHORIZATION=f'Bearer {access_token}')
url = reverse('current-user')
response = client.get(url)
self.assertEqual(response.status_code, 200)
self.assertEqual(response.json()['role'], 'user')
def test_staff_user_role_is_administrator(self):
"""
Verifica que un usuario con is_staff=True recibe role 'administrator'.
"""
staff_user = User.objects.create_user(
username='staff',
email='staff@example.com',
password='staffpass',
is_staff=True
)
refresh = RefreshToken.for_user(staff_user)
access_token = str(refresh.access_token)
client = APIClient()
client.credentials(HTTP_AUTHORIZATION=f'Bearer {access_token}')
url = reverse('current-user')
response = client.get(url)
self.assertEqual(response.status_code, 200)
self.assertEqual(response.json()['role'], 'administrator')
def test_me_endpoint_requires_authentication(self):
"""
Sin token el endpoint debe devolver 401 Unauthorized.
"""
client_no_auth = APIClient()
url = reverse('current-user')
response = client_no_auth.get(url)
self.assertEqual(response.status_code, 401)

View File

@@ -0,0 +1,6 @@
from django.urls import path
from .views import CurrentUserView
urlpatterns = [
path('me/', CurrentUserView.as_view(), name='current-user'),
]

View File

@@ -0,0 +1,12 @@
from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework.permissions import IsAuthenticated
from .serializers import UserSerializer
class CurrentUserView(APIView):
permission_classes = [IsAuthenticated]
def get(self, request):
serializer = UserSerializer(request.user)
return Response(serializer.data)