From 279000ccca6960700f2f71b365bfe88b28c9d78a Mon Sep 17 00:00:00 2001 From: DevlTz Date: Mon, 30 Mar 2026 17:20:40 -0300 Subject: [PATCH 1/8] refactor(users): remove username and implement custom UserManager for email auth. --- .../migrations/0003_alter_user_managers.py | 18 +++++++++++++ apps/users/models.py | 26 ++++++++++++++++--- 2 files changed, 41 insertions(+), 3 deletions(-) create mode 100644 apps/users/migrations/0003_alter_user_managers.py diff --git a/apps/users/migrations/0003_alter_user_managers.py b/apps/users/migrations/0003_alter_user_managers.py new file mode 100644 index 0000000..9f0873c --- /dev/null +++ b/apps/users/migrations/0003_alter_user_managers.py @@ -0,0 +1,18 @@ +# Generated by Django 5.2 on 2026-03-30 20:19 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('users', '0002_alter_user_options_remove_user_username_user_name_and_more'), + ] + + operations = [ + migrations.AlterModelManagers( + name='user', + managers=[ + ], + ), + ] diff --git a/apps/users/models.py b/apps/users/models.py index d507f58..6712b4e 100644 --- a/apps/users/models.py +++ b/apps/users/models.py @@ -1,7 +1,27 @@ # apps/users/models.py -from django.contrib.auth.models import AbstractUser +from django.contrib.auth.models import AbstractUser, BaseUserManager from django.db import models +class UserManager(BaseUserManager): + def create_user(self, email, password=None, **extra_fields): + if not email: + raise ValueError("Email is required") + email = self.normalize_email(email) + user = self.model(email=email, **extra_fields) + user.set_password(password) + user.save(using=self._db) + return user + def create_superuser(self, email, password=None, **extra_fields): + extra_fields.setdefault("is_staff", True) + extra_fields.setdefault("is_superuser", True) + + if extra_fields.get("is_staff") is not True: + raise ValueError("Superuser need is_staff=True.") + if extra_fields.get("is_superuser") is not True: + raise ValueError("Superuser need is_superuser=True.") + + return self.create_user(email, password, **extra_fields) + class User(AbstractUser): class UserType(models.TextChoices): ADVERTISER = "A", "Advertiser" @@ -10,7 +30,7 @@ class UserType(models.TextChoices): username = None name = models.CharField(max_length=120) email = models.EmailField(unique=True) - + objects = UserManager() USERNAME_FIELD = 'email' REQUIRED_FIELDS = ['name'] @@ -25,7 +45,7 @@ class UserType(models.TextChoices): # favorites = models.ManyToManyField( - # 'properties.Property', + # 'properties.Properties', # related_name='favorited_by', # blank=True # ) From d0f6dabfb511973b499cefde9592d9b6de4ee0ae Mon Sep 17 00:00:00 2001 From: DevlTz Date: Mon, 30 Mar 2026 17:21:47 -0300 Subject: [PATCH 2/8] feat(users - serializers): add RegisterSerializer with email case-insensitivity to prevent double account with e.g (Testes.admin && testes.admin) --- apps/users/serializers.py | 23 ++++++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/apps/users/serializers.py b/apps/users/serializers.py index c1a02b8..69ef43a 100644 --- a/apps/users/serializers.py +++ b/apps/users/serializers.py @@ -28,4 +28,25 @@ def update(self, instance, validated_data): defaults=preferences_data ) - return instance \ No newline at end of file + return instance + +class RegisterSerializer(serializers.ModelSerializer): + password = serializers.CharField(write_only=True, min_length=8) + + class Meta: + model = User + fields = ['id', 'name', 'email', 'password', 'user_type'] + + def create(self, validated_data): + user = User.objects.create_user( + email=validated_data['email'], + name=validated_data['name'], + user_type=validated_data['user_type'], + password=validated_data['password'] + ) + return user + def validate_email(self, value): + email = value.lower() + if User.objects.filter(email=email).exists(): + raise serializers.ValidationError("This email is currently in use.") + return email \ No newline at end of file From d8af7ba66b46af297cdbe7096f6a5976df67b09c Mon Sep 17 00:00:00 2001 From: DevlTz Date: Mon, 30 Mar 2026 17:23:09 -0300 Subject: [PATCH 3/8] feat(users): implement/changes the old /profile/ to /me/ endpoint and JWT auth routes --- apps/users/urls.py | 12 ++++++++++-- apps/users/views.py | 18 +++++++++++------- 2 files changed, 21 insertions(+), 9 deletions(-) diff --git a/apps/users/urls.py b/apps/users/urls.py index f13fdf9..18760a7 100644 --- a/apps/users/urls.py +++ b/apps/users/urls.py @@ -1,6 +1,14 @@ from rest_framework.routers import DefaultRouter -from .views import UserViewSet +from .views import UserViewSet, RegisterUserView +from django.urls import path +from rest_framework_simplejwt.views import (TokenObtainPairView, TokenRefreshView, TokenBlacklistView) router = DefaultRouter() router.register(r"", UserViewSet, basename="user") -urlpatterns = router.urls \ No newline at end of file +urlpatterns = [ + path('register/', RegisterUserView.as_view(), name='register'), + path('login/', TokenObtainPairView.as_view(), name='token_obtain_pair'), + path('token/refresh/', TokenRefreshView.as_view(), name='token_refresh'), + path('logout/', TokenBlacklistView.as_view(), name='logout') +] + router.urls + diff --git a/apps/users/views.py b/apps/users/views.py index 0125765..90f9b31 100644 --- a/apps/users/views.py +++ b/apps/users/views.py @@ -1,18 +1,23 @@ -from rest_framework import viewsets, status +from rest_framework import viewsets, status, generics from rest_framework.decorators import action from rest_framework.response import Response -from rest_framework.permissions import IsAuthenticated +from rest_framework.permissions import IsAuthenticated, AllowAny from django.shortcuts import get_object_or_404 from .models import User -from .serializers import UserSerializer +from .serializers import UserSerializer, RegisterSerializer + +class RegisterUserView(generics.CreateAPIView): + queryset = User.objects.all() + permission_classes = [AllowAny] + serializer_class = RegisterSerializer class UserViewSet(viewsets.GenericViewSet): queryset = User.objects.all() serializer_class = UserSerializer permission_classes = [IsAuthenticated] - @action(detail=False, methods=['get', 'patch'], url_path='profile') - def profile(self, request): + @action(detail=False, methods=['get', 'patch'], url_path='me') + def me(self, request): user = request.user if request.method == 'PATCH': @@ -28,7 +33,7 @@ def profile(self, request): @action(detail=False, methods=['get', 'post', 'delete'], url_path='favorites') def favorites(self, request): from apps.properties.serializers import PropertiesSerializer # Sei que soa estranho, mas esse import tem que tá aqui praa poder não ter import repetido - from apps.properties.models import Properties + from apps.properties.models import Properties user = request.user @@ -48,7 +53,6 @@ def favorites(self, request): user.favorites.add(property_obj) return Response({"message": "Property added to favorites"}, status=status.HTTP_200_OK) - elif request.method == 'DELETE': user.favorites.remove(property_obj) return Response({"message": "Property removed from favorites"}, status=status.HTTP_200_OK) \ No newline at end of file From fe099690d7124f704b57d861a2febd669ec4dfee Mon Sep 17 00:00:00 2001 From: DevlTz Date: Mon, 30 Mar 2026 17:23:32 -0300 Subject: [PATCH 4/8] chore(settings): setup SimpleJWT and DRF authentication settings --- config/settings.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/config/settings.py b/config/settings.py index c933853..3cf3b9a 100644 --- a/config/settings.py +++ b/config/settings.py @@ -20,7 +20,9 @@ THIRD_PARTY_APPS = [ "rest_framework", "rest_framework_simplejwt", + "rest_framework_simplejwt.token_blacklist", "corsheaders", + ] LOCAL_APPS = [ @@ -76,7 +78,7 @@ AUTH_PASSWORD_VALIDATORS = [ { - "NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator" + "NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator" }, {"NAME": "django.contrib.auth.password_validation.MinimumLengthValidator"}, {"NAME": "django.contrib.auth.password_validation.CommonPasswordValidator"}, @@ -104,4 +106,12 @@ ), } +from datetime import timedelta +SIMPLE_JWT = { + 'ACCESS_TOKEN_LIFETIME': timedelta(minutes=60), + 'REFRESH_TOKEN_LIFETIME': timedelta(days=1), + 'ROTATE_REFRESH_TOKENS': True, + 'BLACKLIST_AFTER_ROTATION': True, +} + CORS_ALLOW_ALL_ORIGINS = True From 46a3afaed58dd87338a4b33a6c8a4988f0afb232 Mon Sep 17 00:00:00 2001 From: DevlTz Date: Mon, 30 Mar 2026 17:57:34 -0300 Subject: [PATCH 5/8] docs: add detailed JWT authentication and security rules --- docs/diagram.png | Bin 0 -> 43667 bytes docs/diagrama_database.mermaid | 93 +++++++++++++++++++++++++++++++++ docs/jwt.md | 82 +++++++++++++++++++++++++++++ 3 files changed, 175 insertions(+) create mode 100644 docs/diagram.png create mode 100644 docs/diagrama_database.mermaid create mode 100644 docs/jwt.md diff --git a/docs/diagram.png b/docs/diagram.png new file mode 100644 index 0000000000000000000000000000000000000000..a59e37c0d71a42bf073fc6094743c71aecea5b4d GIT binary patch literal 43667 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 U@8N;ZSdIVxzbuVE_OC literal 0 HcmV?d00001 diff --git a/docs/diagrama_database.mermaid b/docs/diagrama_database.mermaid new file mode 100644 index 0000000..3a1412e --- /dev/null +++ b/docs/diagrama_database.mermaid @@ -0,0 +1,93 @@ +erDiagram + USERS { + bigserial id PK + text name + int age + varchar gender + text email UK "Login principal" + varchar user_type "A, S" + timestamp created_at + } + + SEARCH_PREFERENCES { + bigserial id PK + bigint user_id FK + char property_type "H, A" + numeric min_price + numeric max_price + varchar city + varchar neighborhood + } + + USER_FAVORITES { + bigint user_id FK + bigint property_id FK + } + + PROPERTIES { + bigserial id PK + bigint owner_id FK + bigint rooms_id FK + bigint rooms_extras_id FK + bigint condo_id FK + char property_purpose "R, S, B" + char type "H, A" + float area + int floors + int floor_number + numeric price + text address + varchar neighborhood + varchar city + boolean status + boolean has_mobilia + text description + vector embedding "1536 dim" + timestamp created_at + } + + ROOMS { + bigserial id PK + int bedrooms + int bathrooms + int parking_spots + } + + ROOMS_EXTRAS { + bigserial id PK + boolean living_room + boolean garden + boolean kitchen + boolean laundry_room + boolean pool + boolean office + } + + CONDO { + bigserial id PK + text name + text address + boolean gym + boolean pool + boolean court + boolean parks + boolean party_spaces + boolean concierge + boolean laundry_room + } + + PROPERTIES_PHOTOS { + bigserial id PK + bigint property_id FK + text r2_key "Cloudflare R2" + int order + } + + USERS ||--o{ PROPERTIES : "owns" + USERS ||--o| SEARCH_PREFERENCES : "has" + USERS }o--o{ USER_FAVORITES : "saves" + PROPERTIES }o--o{ USER_FAVORITES : "saved_by" + PROPERTIES ||--|| ROOMS : "has" + PROPERTIES ||--|| ROOMS_EXTRAS : "features" + PROPERTIES }o--|| CONDO : "belongs_to" + PROPERTIES ||--o{ PROPERTIES_PHOTOS : "has_images" \ No newline at end of file diff --git a/docs/jwt.md b/docs/jwt.md new file mode 100644 index 0000000..a4e29db --- /dev/null +++ b/docs/jwt.md @@ -0,0 +1,82 @@ +# Authentication & Security Documentation + +Esta documentação detalha a implementação do sistema de segurança e autenticação do projeto **HomeMatch**. + +## Visão Geral +A plataforma utiliza **JSON Web Tokens (JWT)** via `djangorestframework-simplejwt` para gerenciar sessões e permissões. O diferencial do nosso modelo é a autenticação baseada exclusivamente em **E-mail**, tendo o campo `username` sido removido para simplificar o fluxo do usuário. + +--- + +## Modelo de Usuário Customizado +O modelo `User` herda de `AbstractUser`, mas utiliza um `UserManager` customizado para suportar o e-mail como identificador único. + +* **Identificador de Login**: `email`. +* **Campos Obrigatórios**: `email`, `name`. +* **Tipos de Usuário (user_type)**: + * `A` (Advertiser/Anunciante). + * `S` (Seeker/Buscador). + +--- + +## Endpoints de Autenticação (JWT) + +| Método | Endpoint | Acesso | Descrição | +| :--- | :--- | :--- | :--- | +| `POST` | `/api/users/register/` | Público | Registra um novo usuário na plataforma. | +| `POST` | `/api/users/login/` | Público | Recebe as credenciais e retorna os tokens `access` e `refresh`. | +| `POST` | `/api/users/token/refresh/` | Público | Gera um novo `access` token utilizando um `refresh` válido. | +| `GET` | `/api/users/me/` | Autenticado | Retorna os dados do perfil do usuário logado. | + +--- + +## Proteção de Rotas: Imóveis (Properties) + +As rotas do app `properties` seguem a política de permissão `IsAuthenticatedOrReadOnly`. + +### 1. Leitura de Dados (Público) +Qualquer usuário pode realizar as seguintes operações sem necessidade de token: +* `GET /api/properties/`: Lista todos os imóveis cadastrados. +* `GET /api/properties/{id}/`: Visualiza detalhes de um imóvel específico. + +### 2. Escrita de Dados (Autenticado) +Requer o envio do cabeçalho `Authorization: Bearer `. + +* **Criação (`POST /api/properties/`)**: Permitida apenas para usuários com `user_type = 'A'` (Advertiser). +* **Edição (`PATCH /api/properties/{id}/`)**: Restrita ao proprietário do imóvel (`owner_id`). +* **Remoção (`DELETE /api/properties/{id}/`)**: Restrita ao proprietário do imóvel (`owner_id`). + +--- + +## Tratamento de Erros + +O sistema utiliza códigos de erro padronizados do Django REST Framework: + +* **401 Unauthorized**: Token ausente, expirado ou inválido. +* **403 Forbidden**: Usuário autenticado tenta realizar uma ação sem permissão (ex: Seeker tentando postar imóvel ou editar imóvel de terceiros). +* **400 Bad Request**: Erros de validação (ex: e-mail já cadastrado ou senha fora do padrão). + +--- + +## Exemplo de Requisição (CURL) + +Para acessar rotas protegidas no terminal, utilize: + +```bash +docker compose exec web python manage.py createsuperuser # Pra criar seu usuario - Se ainda não criado +``` + +```bash +curl -X POST http://localhost:8000/api/users/login/ -H "Content-Type: application/json" -d '{ + "email": "seu email cadastrado", + "password": "sua senha cadastrada" + }' +``` + * Só ai, você testa com seu token. + +```bash +curl -X GET http://localhost:8000/api/users/me/ \ + -H "Authorization: Bearer " \ + -H "Content-Type: application/json" +``` + + From e175a1cba57572d7105470331b4e43097f4a6855 Mon Sep 17 00:00:00 2001 From: DevlTz Date: Tue, 7 Apr 2026 16:59:16 -0300 Subject: [PATCH 6/8] Update config/settings.py --- config/settings.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/config/settings.py b/config/settings.py index 3cf3b9a..dd079a6 100644 --- a/config/settings.py +++ b/config/settings.py @@ -77,9 +77,7 @@ } AUTH_PASSWORD_VALIDATORS = [ - { - "NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator" - }, + {"NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator"}, {"NAME": "django.contrib.auth.password_validation.MinimumLengthValidator"}, {"NAME": "django.contrib.auth.password_validation.CommonPasswordValidator"}, {"NAME": "django.contrib.auth.password_validation.NumericPasswordValidator"}, From dc2c90a1fc490675d17135535a8ae2e9b835bf92 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 7 Apr 2026 20:04:25 +0000 Subject: [PATCH 7/8] docs(jwt): fix markdown table formatting and add missing logout endpoint Agent-Logs-Url: https://github.com/DevlTz/HomeMatch/sessions/ffd2d2d2-2749-48f3-a23a-f55c32465b7f Co-authored-by: DevlTz <110422010+DevlTz@users.noreply.github.com> --- docs/jwt.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/jwt.md b/docs/jwt.md index a4e29db..97e2b6b 100644 --- a/docs/jwt.md +++ b/docs/jwt.md @@ -25,6 +25,7 @@ O modelo `User` herda de `AbstractUser`, mas utiliza um `UserManager` customizad | `POST` | `/api/users/register/` | Público | Registra um novo usuário na plataforma. | | `POST` | `/api/users/login/` | Público | Recebe as credenciais e retorna os tokens `access` e `refresh`. | | `POST` | `/api/users/token/refresh/` | Público | Gera um novo `access` token utilizando um `refresh` válido. | +| `POST` | `/api/users/logout/` | Autenticado | Invalida o `refresh` token (blacklist), encerrando a sessão. | | `GET` | `/api/users/me/` | Autenticado | Retorna os dados do perfil do usuário logado. | --- From 3560ed1039c2988c1c8b9f16d9b63151745d5192 Mon Sep 17 00:00:00 2001 From: DevlTz Date: Tue, 7 Apr 2026 17:25:12 -0300 Subject: [PATCH 8/8] feat(users): setup email auth and /me endpoint, favorites temporarily disabled --- apps/properties/apps.py | 1 + apps/users/views.py | 2 +- config/settings.py | 11 +++++------ 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/apps/properties/apps.py b/apps/properties/apps.py index 7b70531..5c09d5b 100644 --- a/apps/properties/apps.py +++ b/apps/properties/apps.py @@ -4,3 +4,4 @@ class PropertiesConfig(AppConfig): default_auto_field = "django.db.models.BigAutoField" name = "apps.properties" + label = "properties" diff --git a/apps/users/views.py b/apps/users/views.py index 90f9b31..c152f3f 100644 --- a/apps/users/views.py +++ b/apps/users/views.py @@ -32,7 +32,7 @@ def me(self, request): # GET, POST e DELETE em /api/users/favorites/ @action(detail=False, methods=['get', 'post', 'delete'], url_path='favorites') def favorites(self, request): - from apps.properties.serializers import PropertiesSerializer # Sei que soa estranho, mas esse import tem que tá aqui praa poder não ter import repetido + from apps.properties.serializers import PropertiesSerializer # Sei que soa estranho, mas esse import tem que tá aqui para poder não ter import repetido from apps.properties.models import Properties user = request.user diff --git a/config/settings.py b/config/settings.py index dd079a6..ac6c622 100644 --- a/config/settings.py +++ b/config/settings.py @@ -1,5 +1,6 @@ from pathlib import Path from decouple import config +from datetime import timedelta BASE_DIR = Path(__file__).resolve().parent.parent @@ -103,13 +104,11 @@ "rest_framework.permissions.IsAuthenticatedOrReadOnly", ), } - -from datetime import timedelta SIMPLE_JWT = { - 'ACCESS_TOKEN_LIFETIME': timedelta(minutes=60), - 'REFRESH_TOKEN_LIFETIME': timedelta(days=1), - 'ROTATE_REFRESH_TOKENS': True, - 'BLACKLIST_AFTER_ROTATION': True, + "ACCESS_TOKEN_LIFETIME": timedelta(minutes=60), + "REFRESH_TOKEN_LIFETIME": timedelta(days=1), + "ROTATE_REFRESH_TOKENS": True, + "BLACKLIST_AFTER_ROTATION": True, } CORS_ALLOW_ALL_ORIGINS = True