diff --git a/README.md b/README.md
index a0bb7d6..ef38980 100644
--- a/README.md
+++ b/README.md
@@ -1309,3 +1309,548 @@ class UserAdmin(BaseUserAdmin):
- Página para crear usuarios 
- Panel de usuarios del administrador 
+
+## Documentación de la API
+
+Es necesario tener acceso a una buena documentación para que los desarrolladores
+puedan saber como usarla. Se documenta todo lo que sea necesario para usar la API
+
+- Endopoints disponibles (paths)
+- Métodos soportados `GET`, `POST`, `PUT`, `PATCH`, `DELETE`...
+- Formateo de payloads (inputs). Parametros, Post en formato **JSON**
+- Formateo de respuestas (outputs). Respuesta en formato **JSON**
+- Proceso Autenticación
+
+### Opiones de documentación
+
+- Manual
+ - Documento de texto
+ - Markdown
+- Automatizada
+ - Usa la metadata del código (comments)
+ - Genera páginas de documentación
+
+## Autodocs de DRF
+
+- Documentación Autogenerada (3rd party library)
+ - `drf-spectacular`
+- Genera el "schema"
+- Interfaz web navegable
+ - Test requests
+ - Maneja la autenticación
+
+### Como funciona
+
+1. Creación del archivo `schema`
+2. Pasa el schema al GUI
+
+### Open API Schema
+
+- Estandar para describir APIs
+- Popular en la industria
+- Soportada por la mayoría de herramientas de documentación de API
+
+### Ejemplo Schema
+
+fragmento
+
+```yml
+/api/recipe/ingredients/:
+ get:
+ oprationId: recipe_ingredients_list
+ description: Manage ingredients in the database.
+ parameteres:
+ - in: query
+ name: assigned_only
+ schema:
+ type: integer
+ enum:
+ - 0
+ - 1
+ description: Filter by item assigned to recipies
+ tags:
+ - recipe
+ security:
+ - tokenAuth: []
+ responses:
+ '200':
+ content:
+ application/json
+ schema:
+ type: array
+ items:
+ $ref: '#/components/schemas/Ingredient'
+ description: ''
+ ...
+```
+
+### Implementación DRF
+
+Se agrega dependencia en `drf-spectacular>=0.16` en `requirements.txt`
+
+Instalar app, en `settings.py`
+
+```py
+INSTALLED_APPS = [
+ ...
+ 'core',
+ 'rest_framework',
+ 'drf_spectacular',
+]
+
+...
+REST_FRAMEWORK = {
+ 'DEFAULT_SCHEMA_CLASS': 'drf_spectacular.openapi.AutoSchema',
+}
+```
+
+### Activar las URLS
+
+```py
+from drf_spectacular.views import SpectacularAPIView, SpectacularSwaggerView
+from django.contrib import admin
+from django.urls import path
+
+urlpatterns = [
+ path('admin/', admin.site.urls),
+ path('api/schema/', SpectacularAPIView.as_view(), name='api-schema'),
+ path(
+ 'api/docs',
+ SpectacularSwaggerView.as_view(url_name='api-schema'),
+ name='api-docs'),
+]
+```
+
+- `docker compose run`
+- `127.0.0.1:8000/api/docs`
+
+## User API
+
+### Diseño
+
+- Registro de usario
+- Creación de token de autenticación
+- Consultar y actualizar perfil
+
+### Endpoins
+
+| Endpoint | Method | Descripción |
+| - | - | - |
+| `user/create` | `POST` | Registrar nuevo usuario |
+| `user/token` | `POST` | Crea un nuevo token |
+| `user/me/` | `PUT/PATCH` | Actualizar perfíl |
+
+### Creación user app
+
+`docker compose run --rm app sh -c "python manage.py startapp user"`
+
+```sh
+[+] Creating 1/0
+ ✔ Container recipes_api_django-db-1 Created 0.0s
+[+] Running 1/1
+ ✔ Container recipes_api_django-db-1 Started 0.2s
+```
+
+Activar `user` app en `settings.py`
+
+### Test User API
+
+[`app/user/tests/test_user_api.py`](./app/user/tests/test_user_api.py)
+
+```py
+CREATE_USER_URL = reverse('user:create')
+
+def create_user(**params):
+ """Create and return a new user."""
+ return get_user_model().objects.crate_user(**params)
+
+
+class PublicUserApiTest(TestCase):
+ """Test the public features of the user API."""
+
+ def setUp(self):
+ self.client = APIClient()
+
+ def test_create_user_success(self):
+ """Tests creating a user is successful."""
+ payload = {
+ 'email': 'test@example.com',
+ 'password':'testpass123',
+ 'name': 'TestName',
+ }
+ res = self.client.post(CREATE_USER_URL, payload)
+
+ self.assertEqual(res.status_code, status.HTTP_201_CREATED)
+ user = get_user_model().objects.get(email=payload['email'])
+ self.assertTrue(user.check_password(payload['password']))
+ self.assertNotIn('password', res.data)
+
+ def test_user_with_email_exists_error(self):
+ """Test error returned if user with email exists."""
+ payload = {
+ 'email': 'test@example.com',
+ 'password':'testpass123',
+ 'name': 'Test Name',
+ }
+ create_user(**payload)
+ res = self.client.post(CREATE_USER_URL, payload)
+
+ self.assertEqual(res.status_code, status.HTTP_400_BAD_REQUEST)
+
+ def test_password_too_short_error(self):
+ """Test an error is returned if password less than 5 chars."""
+ payload = {
+ 'email': 'test@example.com',
+ 'password':'pw',
+ 'name': 'Test Name',
+ }
+ res = self.client.post(CREATE_USER_URL, payload)
+
+ self.assertEqual(res.status_code, status.HTTP_400_BAD_REQUEST)
+ user_exists = get_user_model().objects.filter(
+ email=payload['email']
+ ).exists()
+ self.assertFalse(user_exists)
+```
+
+### Creando funcionalidad de User API
+
+- [serializers.py](./app/user/serializers.py)
+
+ ```py
+ from django.contrib.auth import get_user_model
+ from rest_framework import serializers
+
+ class UserSerializer(serializers.ModelSerializer):
+ """Seralizer for the model object."""
+
+ class Meta:
+ model = get_user_model()
+ fields = ['email', 'password', 'name']
+ extra_kwargs = {'password': {'write_only': True, 'min_length': 5}}
+
+ def create(self, validated_data):
+ """Create and return a user with encrypted password"""
+ return get_user_model().objects.create_user(**validated_data)
+ ```
+
+- [views.py](./app/user/views.py)
+
+ ```py
+ from rest_framework import generics
+ from user.serializers import UserSerializer
+
+ class CreateUserView(generics.CreateAPIView):
+ """Create a new user in the system."""
+ serializer_class = UserSerializer
+ ```
+
+- [urls.py](./app/user/urls.py)
+
+ ```py
+ from django.urls import path
+ from user import views
+
+ app_name='user'
+ urlpatterns = [
+ path('create/', views.CreateUserView.as_view(), name='create')
+ ]
+ ```
+
+- [app/urls.py](./app/app/urls.py)
+
+ ```py
+ ...
+ from django.urls import include, path
+
+ urlpatterns = [
+ ...
+ path('api/user/', include('user.urls')),
+ ]
+ ```
+
+### Autenticación
+
+| Tipo de autenticación | Descripción |
+| - | - |
+| **Básica** | Envía usuario y password en cada request |
+| **Token** | Usa un token en el encabezado HTTP |
+| **JSON Web Token (JWT)** | Usa un token de acceso |
+| **Sesión** | Usa cookies |
+
+En esta app se utilza **Token** por:
+
+- Balance entre simplicidad y seguridad
+- Soporte por defecto por **DRF**
+- Bién soportada por la mayoria de clientes
+
+
+```mermaid
+%%{init: {'theme': 'dark','themeVariables': {'clusterBkg': '#2b2f38'}, 'flowchart': {'curve': 'basis'}}}%%
+flowchart
+subgraph " "
+
+CT["Create token
+(Post username/password)"]
+STOC["Store token on client"]
+ITIH["Include token in HTTP headers"]
+CT .-> STOC .-> ITIH
+end
+```
+
+#### Pros del uso de Token
+
+- Soporte por defecto
+- Simple de usar
+- Soportada por todos los clientes
+- Evita enviar datos de usuario/password en cada request
+
+#### Cons del uso de Token
+
+- El token debe ser seguro
+- Requiere hacer peticiones a la base de datos
+
+### Login out
+
+- Sucede en el lado del cliente
+- Borra el token
+
+### Test token API
+
+Agregar tests en
+[`app/user/tests/test_user_api.py`](./app/user/tests/test_user_api.py)
+
+```py
+...
+TOKEN_URL = reverse('user:token')
+
+...
+
+class PublicUserApiTest(TestCase):
+ """Test the public features of the user API."""
+
+ ...
+
+ def test_create_token_for_user(self):
+ """Test generate token for valid credentials."""
+ user_details = {
+ 'name': 'Test Name',
+ 'email': 'test@example.com',
+ 'password':'test-user-password123',
+ }
+ create_user(**user_details)
+
+ payload = {
+ 'email': user_details['email'],
+ 'password': user_details['password'],
+ }
+ res = self.client.post(TOKEN_URL, payload)
+
+ self.assertIn('token', res.data)
+ self.assertEqual(res.status_code, status.HTTP_200_OK)
+
+ def test_create_token_bad_credentials(self):
+ """Test returns error if credentials invalid."""
+ create_user(email='test@example.com', password='goodpass')
+
+ payload = {'email': 'test@example.com' ,'password': 'badpass'}
+ res = self.client.post(TOKEN_URL, payload)
+
+ self.assertNotIn('token', res.data)
+ self.assertNotIn(res.status_code, status.HTTP_400_BAD_REQUEST)
+
+ def test_create_token_blank_password(self):
+ """Test posting a blank password returns an error."""
+ payload = {'email': 'test@example.com' , 'password': ''}
+ res = self.client.post(TOKEN_URL, payload)
+
+ self.assertNotIn('token', res.data)
+ self.assertNotIn(res.status_code, status.HTTP_400_BAD_REQUEST)
+```
+
+### Implementar Token API
+
+- Añadir app `rest_framework.authtoken` en [settings.py](./app/app/settings.py)
+
+ ```py
+ INSTALLED_APPS = [
+ ...
+ 'rest_framework',
+ 'rest_framework.authtoken', # <---
+ 'drf_spectacular',
+ 'user',
+ ]
+ ```
+
+### Creación del serlizador para token api
+
+- [user/serializer.py](./app/user/serializers.py)
+
+ ```py
+ ...
+
+ class AuthTokenSerializer(serializers.Serializer):
+ """Serializer for the user auth token."""
+ email = serializer.EmailField()
+ password = serializer.CharField(
+ style={'input_type': 'password'},
+ trim_whitespace=False,
+ )
+
+ def validate(self, attrs):
+ """Validate and authenticate the user."""
+ email = attrs.get('email')
+ password = attrs.get('password')
+ user = authenticate(
+ request=self.context.get('request'),
+ username=email,
+ password=password,
+ )
+ if not user:
+ msg = _('Unable to authenticate with provided credentials.')
+ raise serializers.ValidationError(msg, code='authorization')
+
+ attrs['user'] = user
+ return attrs
+ ```
+
+- vista [user/views.py](./app/user/views.py)
+
+ ```py
+ ...
+ class CreateTokenView(ObtainAuthToken):
+ """Create a new auth token for user."""
+ serializer_class = AuthTokenSerializer
+ renderer_classes = api_settings.DEFAULT_RENDERER_CLASSES
+ ```
+
+- urls [user/urls.py](./app/user/urls.py)
+
+ ```py
+ urlpatterns = [
+ ...
+ path('token/', views.CreateTokenView.as_view(), name='token'),
+ ]
+ ```
+
+### Test administrar usuario
+
+- [test_user_api.py](./app/user/tests/test_user_api.py)
+
+ ```py
+ ...
+ ME_URL = reverse('user:me')
+
+ ...
+
+ class PrivateUserApiTests(TestCase):
+ """Test API requests that require authentication."""
+
+ def setUp(self):
+ self.user = create_user(
+ email='test@example.com',
+ password='testpass123',
+ name='Test Name',
+ )
+ self.client = APIClient()
+ self.client.force_authenticate(user=self.user)
+
+ def test_retrive_profile_success(self):
+ """Test retrieving profile for logged in user."""
+ res = self.client.get(ME_URL)
+
+ self.assertEqual(res.status_code, status.HTTP_200_OK)
+ self.assertEqual(
+ res.data, {
+ 'name': self.user.name,
+ 'email': self.user.email,
+ })
+
+ def test_post_me_not_allowed(self):
+ """Test POST is not allowed for the 'me' endpoint."""
+ res = self.client.post(ME_URL, {})
+
+ self.assertAlmostEqual(
+ res.status_code,
+ status.HTTP_405_METHOD_NOT_ALLOWED
+ )
+
+ def test_update_user_profile(self):
+ """Test updating the user profile for the autenticated user."""
+ payload = { 'name': 'Updated Name', 'password': 'newpassword123' }
+
+ res = self.client.patch(ME_URL, payload)
+
+ self.user.refresh_from_db()
+ self.assertEqual(self.user.name, payload['name'])
+ self.assertTrue(self.user.check_password(payload['password']))
+ self.assertEqual(res.status_code, status.HTTP_200_OK)
+ ```
+
+### Implementación API actualizar usuario
+
+`me` endpoint
+
+- creación (sobrescritura) del método update
+[serializer.py](./app/user/serializers.py)
+
+ ```py
+ ...
+
+ class UserSerializer(serializers.ModelSerializer):
+
+ ...
+
+ def create(self, validated_data):
+ """Create and return a user with encrypted password"""
+ return get_user_model().objects.create_user(**validated_data)
+
+ def update(self, instance, validated_data):
+ """Update and return user."""
+ password = validated_data.pop('password', None)
+ user = super().update(instance, validated_data)
+
+ if password:
+ user.set_password(password)
+ user.save()
+
+ return user
+
+ ...
+ ```
+
+- vistas [views.py](./app/user/views.py)
+
+ ```py
+ from rest_framework import generics, authentication, permissions
+ ...
+
+ class ManageUserView(generics.RetrieveUpdateAPIView):
+ """Manage the autenticated user."""
+ serializer_class = UserSerializer
+ authentication_classes = [authentication.TokenAuthentication]
+ permission_classes = [permissions.IsAuthenticated]
+
+ def get_object(self):
+ """Retrieve and return the authenticated user."""
+ return self.request.user
+ ```
+
+- urls [urls.py](./app/user/urls.py)
+
+ ```py
+ urlpatterns = [
+ ...
+ path('me/', views.ManageUserView.as_view(), name='me'),
+ ]
+ ```
+
+### Pruebas en navegador
+
+Ruta `localhost:8000/api/docs`
+
+
+
+----
+
+Segunda parte -> [Recetas](./README2.md)
diff --git a/app/app/settings.py b/app/app/settings.py
index 11fd324..dd06dfd 100644
--- a/app/app/settings.py
+++ b/app/app/settings.py
@@ -38,6 +38,10 @@ INSTALLED_APPS = [
'django.contrib.messages',
'django.contrib.staticfiles',
'core',
+ 'rest_framework',
+ 'rest_framework.authtoken',
+ 'drf_spectacular',
+ 'user',
]
MIDDLEWARE = [
@@ -127,3 +131,7 @@ STATIC_URL = 'static/'
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
AUTH_USER_MODEL = 'core.User'
+
+REST_FRAMEWORK = {
+ 'DEFAULT_SCHEMA_CLASS': 'drf_spectacular.openapi.AutoSchema',
+}
diff --git a/app/app/urls.py b/app/app/urls.py
index 84b3189..5a8bedf 100644
--- a/app/app/urls.py
+++ b/app/app/urls.py
@@ -14,9 +14,16 @@ Including another URLconf
1. Import the include() function: from django.urls import include, path
2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
"""
+from drf_spectacular.views import SpectacularAPIView, SpectacularSwaggerView
from django.contrib import admin
-from django.urls import path
+from django.urls import include, path
urlpatterns = [
path('admin/', admin.site.urls),
+ path('api/schema/', SpectacularAPIView.as_view(), name='api-schema'),
+ path(
+ 'api/docs',
+ SpectacularSwaggerView.as_view(url_name='api-schema'),
+ name='api-docs'),
+ path('api/user/', include('user.urls')),
]
diff --git a/app/core/tests/test_models.py b/app/core/tests/test_models.py
index 656638a..acd5051 100644
--- a/app/core/tests/test_models.py
+++ b/app/core/tests/test_models.py
@@ -2,7 +2,7 @@
Test for models.
"""
from django.test import TestCase
-from django.contrib.auth import get_user, get_user_model
+from django.contrib.auth import get_user_model
class ModelTests(TestCase):
diff --git a/app/user/__init__.py b/app/user/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/app/user/apps.py b/app/user/apps.py
new file mode 100644
index 0000000..36cce4c
--- /dev/null
+++ b/app/user/apps.py
@@ -0,0 +1,6 @@
+from django.apps import AppConfig
+
+
+class UserConfig(AppConfig):
+ default_auto_field = 'django.db.models.BigAutoField'
+ name = 'user'
diff --git a/app/user/serializers.py b/app/user/serializers.py
new file mode 100644
index 0000000..4dc6652
--- /dev/null
+++ b/app/user/serializers.py
@@ -0,0 +1,56 @@
+"""
+Seralizers for the user API View
+"""
+from django.contrib.auth import get_user_model, authenticate
+from django.utils.translation import gettext as _, trim_whitespace
+
+from rest_framework import serializers
+
+
+class UserSerializer(serializers.ModelSerializer):
+ """Seralizer for the model object."""
+
+ class Meta:
+ model = get_user_model()
+ fields = ['email', 'password', 'name']
+ extra_kwargs = {'password': {'write_only': True, 'min_length': 5}}
+
+ def create(self, validated_data):
+ """Create and return a user with encrypted password"""
+ return get_user_model().objects.create_user(**validated_data)
+
+ def update(self, instance, validated_data):
+ """Update and return user."""
+ password = validated_data.pop('password', None)
+ user = super().update(instance, validated_data)
+
+ if password:
+ user.set_password(password)
+ user.save()
+
+ return user
+
+
+class AuthTokenSerializer(serializers.Serializer):
+ """Serializer for the user auth token."""
+ email = serializers.EmailField()
+ password = serializers.CharField(
+ style={'input_type': 'password'},
+ trim_whitespace=False,
+ )
+
+ def validate(self, attrs):
+ """Validate and authenticate the user."""
+ email = attrs.get('email')
+ password = attrs.get('password')
+ user = authenticate(
+ request=self.context.get('request'),
+ username=email,
+ password=password,
+ )
+ if not user:
+ msg = _('Unable to authenticate with provided credentials.')
+ raise serializers.ValidationError(msg, code='authorization')
+
+ attrs['user'] = user
+ return attrs
diff --git a/app/user/tests/__init__.py b/app/user/tests/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/app/user/tests/test_user_api.py b/app/user/tests/test_user_api.py
new file mode 100644
index 0000000..9df8ec1
--- /dev/null
+++ b/app/user/tests/test_user_api.py
@@ -0,0 +1,151 @@
+"""
+Tests for the user API.
+"""
+from django.test import TestCase
+from django.contrib.auth import get_user_model
+from django.urls import reverse
+
+from rest_framework.test import APIClient
+from rest_framework import status
+
+CREATE_USER_URL = reverse('user:create')
+TOKEN_URL = reverse('user:token')
+ME_URL = reverse('user:me')
+
+def create_user(**params):
+ """Create and return a new user."""
+ return get_user_model().objects.create_user(**params)
+
+
+class PublicUserApiTest(TestCase):
+ """Test the public features of the user API."""
+
+ def setUp(self):
+ self.client = APIClient()
+
+ def test_create_user_success(self):
+ """Tests creating a user is successful."""
+ payload = {
+ 'email': 'test@example.com',
+ 'password':'testpass123',
+ 'name': 'TestName',
+ }
+ res = self.client.post(CREATE_USER_URL, payload)
+
+ self.assertEqual(res.status_code, status.HTTP_201_CREATED)
+ user = get_user_model().objects.get(email=payload['email'])
+ self.assertTrue(user.check_password(payload['password']))
+ self.assertNotIn('password', res.data)
+
+ def test_user_with_email_exists_error(self):
+ """Test error returned if user with email exists."""
+ payload = {
+ 'email': 'test@example.com',
+ 'password':'testpass123',
+ 'name': 'Test Name',
+ }
+ create_user(**payload)
+ res = self.client.post(CREATE_USER_URL, payload)
+
+ self.assertEqual(res.status_code, status.HTTP_400_BAD_REQUEST)
+
+ def test_password_too_short_error(self):
+ """Test an error is returned if password less than 5 chars."""
+ payload = {
+ 'email': 'test@example.com',
+ 'password':'pw',
+ 'name': 'Test Name',
+ }
+ res = self.client.post(CREATE_USER_URL, payload)
+
+ self.assertEqual(res.status_code, status.HTTP_400_BAD_REQUEST)
+ user_exists = get_user_model().objects.filter(
+ email=payload['email']
+ ).exists()
+ self.assertFalse(user_exists)
+
+ def test_create_token_for_user(self):
+ """Test generate token for valid credentials."""
+ user_details = {
+ 'name': 'Test Name',
+ 'email': 'test@example.com',
+ 'password':'test-user-password123',
+ }
+ create_user(**user_details)
+
+ payload = {
+ 'email': user_details['email'],
+ 'password': user_details['password'],
+ }
+ res = self.client.post(TOKEN_URL, payload)
+
+ self.assertIn('token', res.data)
+ self.assertEqual(res.status_code, status.HTTP_200_OK)
+
+ def test_create_token_bad_credentials(self):
+ """Test returns error if credentials invalid."""
+ create_user(email='test@example.com', password='goodpass')
+
+ payload = {'email': 'test@example.com' ,'password': 'badpass'}
+ res = self.client.post(TOKEN_URL, payload)
+
+ self.assertNotIn('token', res.data)
+ self.assertEqual(res.status_code, status.HTTP_400_BAD_REQUEST)
+
+ def test_create_token_blank_password(self):
+ """Test posting a blank password returns an error."""
+ payload = {'email': 'test@example.com' , 'password': ''}
+ res = self.client.post(TOKEN_URL, payload)
+
+ self.assertNotIn('token', res.data)
+ self.assertEqual(res.status_code, status.HTTP_400_BAD_REQUEST)
+
+ def test_retrive_user_unauthorized(self):
+ """Test authentication is required for users."""
+ res = self.client.get(ME_URL)
+
+ self.assertEqual(res.status_code, status.HTTP_401_UNAUTHORIZED)
+
+
+class PrivateUserApiTests(TestCase):
+ """Test API requests that require authentication."""
+
+ def setUp(self):
+ self.user = create_user(
+ email='test@example.com',
+ password='testpass123',
+ name='Test Name',
+ )
+ self.client = APIClient()
+ self.client.force_authenticate(user=self.user)
+
+ def test_retrive_profile_success(self):
+ """Test retrieving profile for logged in user."""
+ res = self.client.get(ME_URL)
+
+ self.assertEqual(res.status_code, status.HTTP_200_OK)
+ self.assertEqual(
+ res.data, {
+ 'name': self.user.name,
+ 'email': self.user.email,
+ })
+
+ def test_post_me_not_allowed(self):
+ """Test POST is not allowed for the 'me' endpoint."""
+ res = self.client.post(ME_URL, {})
+
+ self.assertAlmostEqual(
+ res.status_code,
+ status.HTTP_405_METHOD_NOT_ALLOWED
+ )
+
+ def test_update_user_profile(self):
+ """Test updating the user profile for the autenticated user."""
+ payload = { 'name': 'Updated Name', 'password': 'newpassword123' }
+
+ res = self.client.patch(ME_URL, payload)
+
+ self.user.refresh_from_db()
+ self.assertEqual(self.user.name, payload['name'])
+ self.assertTrue(self.user.check_password(payload['password']))
+ self.assertEqual(res.status_code, status.HTTP_200_OK)
diff --git a/app/user/urls.py b/app/user/urls.py
new file mode 100644
index 0000000..494dd50
--- /dev/null
+++ b/app/user/urls.py
@@ -0,0 +1,15 @@
+"""
+URL mappings for the user API
+"""
+from django.urls import path
+
+from user import views
+
+
+app_name='user'
+
+urlpatterns = [
+ path('create/', views.CreateUserView.as_view(), name='create'),
+ path('token/', views.CreateTokenView.as_view(), name='token'),
+ path('me/', views.ManageUserView.as_view(), name='me'),
+]
diff --git a/app/user/views.py b/app/user/views.py
new file mode 100644
index 0000000..49c7a22
--- /dev/null
+++ b/app/user/views.py
@@ -0,0 +1,29 @@
+"""
+Views for the user API.
+"""
+from rest_framework import generics, authentication, permissions
+from rest_framework.authtoken.views import ObtainAuthToken
+from rest_framework.settings import api_settings
+
+from user.serializers import UserSerializer, AuthTokenSerializer
+
+
+class CreateUserView(generics.CreateAPIView):
+ """Create a new user in the system."""
+ serializer_class = UserSerializer
+
+
+class CreateTokenView(ObtainAuthToken):
+ """Create a new auth token for user."""
+ serializer_class = AuthTokenSerializer
+ renderer_classes = api_settings.DEFAULT_RENDERER_CLASSES
+
+class ManageUserView(generics.RetrieveUpdateAPIView):
+ """Manage the autenticated user."""
+ serializer_class = UserSerializer
+ authentication_classes = [authentication.TokenAuthentication]
+ permission_classes = [permissions.IsAuthenticated]
+
+ def get_object(self):
+ """Retrieve and return the authenticated user."""
+ return self.request.user
diff --git a/imgs_readme/api_swagger_00.png b/imgs_readme/api_swagger_00.png
new file mode 100644
index 0000000..04e7687
Binary files /dev/null and b/imgs_readme/api_swagger_00.png differ
diff --git a/requirements.txt b/requirements.txt
index e40fb74..c3ba626 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,3 +1,4 @@
Django==4.2.5
djangorestframework==3.14.0
psycopg2>=2.9.9
+drf-spectacular>=0.16