Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions apps/properties/apps.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
from django.apps import AppConfig


class PropertiesConfig(AppConfig):
default_auto_field = "django.db.models.BigAutoField"
name = "apps.properties"
label = "properties"

def ready(self):
import apps.properties.signals # importa o arquivo de signals
import apps.properties.signals
18 changes: 18 additions & 0 deletions apps/users/migrations/0003_alter_user_managers.py
Original file line number Diff line number Diff line change
@@ -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=[
],
),
]
Comment on lines +12 to +18
Copy link

Copilot AI Apr 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This migration alters model managers but provides an empty managers=[], which is unusual and can create confusing migration state (and it doesn't affect the DB schema). Consider removing it, or include the intended manager so the migration accurately represents the model state.

Suggested change
operations = [
migrations.AlterModelManagers(
name='user',
managers=[
],
),
]
operations = []

Copilot uses AI. Check for mistakes.
26 changes: 23 additions & 3 deletions apps/users/models.py
Original file line number Diff line number Diff line change
@@ -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.")
Comment on lines +19 to +21
Copy link

Copilot AI Apr 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The error messages in these ValueErrors have grammatical issues ("Superuser need ..."). Adjust wording to be clear and correct (e.g., "Superuser needs is_staff=True.") so these messages are useful when surfaced in CLI/admin flows.

Suggested change
raise ValueError("Superuser need is_staff=True.")
if extra_fields.get("is_superuser") is not True:
raise ValueError("Superuser need is_superuser=True.")
raise ValueError("Superuser needs is_staff=True.")
if extra_fields.get("is_superuser") is not True:
raise ValueError("Superuser needs is_superuser=True.")

Copilot uses AI. Check for mistakes.

return self.create_user(email, password, **extra_fields)

class User(AbstractUser):
class UserType(models.TextChoices):
ADVERTISER = "A", "Advertiser"
Expand All @@ -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']

Expand All @@ -25,7 +45,7 @@ class UserType(models.TextChoices):


# favorites = models.ManyToManyField(
# 'properties.Property',
# 'properties.Properties',
# related_name='favorited_by',
# blank=True
# )
Expand Down
23 changes: 22 additions & 1 deletion apps/users/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,4 +28,25 @@ def update(self, instance, validated_data):
defaults=preferences_data
)

return instance
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
Copy link

Copilot AI Apr 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add a blank line between create and validate_email. Having def validate_email immediately after return user violates PEP 8 and makes the serializer harder to read.

Suggested change
return user
return user

Copilot uses AI. Check for mistakes.
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
12 changes: 10 additions & 2 deletions apps/users/urls.py
Original file line number Diff line number Diff line change
@@ -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
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

20 changes: 12 additions & 8 deletions apps/users/views.py
Original file line number Diff line number Diff line change
@@ -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]
Copy link

Copilot AI Apr 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

permission_classes = [AllowAny] has trailing whitespace. Please remove it to satisfy linters/formatters and keep diffs clean.

Suggested change
permission_classes = [AllowAny]
permission_classes = [AllowAny]

Copilot uses AI. Check for mistakes.
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':
Expand All @@ -27,8 +32,8 @@ def profile(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.models import Properties
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
Comment on lines 33 to +36
Copy link

Copilot AI Apr 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This endpoint relies on user.favorites (used later in the method), but the User model in this PR does not define a favorites relation (the ManyToMany field is commented out and there is no alternative relation in apps/properties). This will raise an AttributeError at runtime. Either add/restore the favorites ManyToMany (with a migration) or update the endpoint to use the actual persisted favorites model/table.

Copilot uses AI. Check for mistakes.

user = request.user

Expand All @@ -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)
13 changes: 10 additions & 3 deletions config/settings.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from pathlib import Path
from decouple import config
from datetime import timedelta

BASE_DIR = Path(__file__).resolve().parent.parent

Expand All @@ -20,7 +21,9 @@
THIRD_PARTY_APPS = [
"rest_framework",
"rest_framework_simplejwt",
"rest_framework_simplejwt.token_blacklist",
"corsheaders",

]

LOCAL_APPS = [
Expand Down Expand Up @@ -75,9 +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"},
{"NAME": "django.contrib.auth.password_validation.NumericPasswordValidator"},
Expand Down Expand Up @@ -105,6 +106,12 @@
"DEFAULT_FILTER_BACKENDS": ["django_filters.rest_framework.DjangoFilterBackend"]

}
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

Expand Down
Binary file added docs/diagram.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
93 changes: 93 additions & 0 deletions docs/diagrama_database.mermaid
Original file line number Diff line number Diff line change
@@ -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"
83 changes: 83 additions & 0 deletions docs/jwt.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
# 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. |
Comment on lines +23 to +27
Copy link

Copilot AI Apr 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The markdown table for the JWT endpoints is malformed (rows start with ||), which prevents proper rendering in standard Markdown parsers. Use single | separators and a single header separator row.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot apply changes based on this feedback

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in dc2c90a. The table now uses proper single | separators and I also added the missing POST /api/users/logout/ row, which was already defined in the URL conf but omitted from the docs.

| `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. |

---

## 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 <access_token>`.

* **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
Copy link

Copilot AI Apr 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This line uses informal/incorrect Portuguese ("Pra criar seu usuario"). Please revise to standard wording (e.g., "Para criar seu usuário") to keep documentation clear and professional.

Suggested change
docker compose exec web python manage.py createsuperuser # Pra criar seu usuario - Se ainda não criado
docker compose exec web python manage.py createsuperuser # Para criar seu usuário (caso ainda não tenha sido criado)

Copilot uses AI. Check for mistakes.
```

```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.
Copy link

Copilot AI Apr 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This sentence has a spelling/grammar issue ("Só ai"). Please correct to "Só aí" for proper Portuguese.

Suggested change
*ai, você testa com seu token.
*, você testa com seu token.

Copilot uses AI. Check for mistakes.

```bash
curl -X GET http://localhost:8000/api/users/me/ \
-H "Authorization: Bearer <SEU_ACCESS_TOKEN>" \
-H "Content-Type: application/json"
```


Loading