- Python
- Django
- React
- TypeScript
- Tailwind CSS
- Azure
- Selenium
- CI/CD
STELLA
Plataforma web de acompanhamento de tratamento de FIV para pacientes da Clínica AMARE, com agenda, medicações e diário.
O STELLA é uma aplicação web de acompanhamento de tratamento de Fertilização In Vitro (FIV), feita em equipe (Grupo 10 da CESAR School) para pacientes da Clínica AMARE. A plataforma centraliza consultas, exames, medicações, sintomas e materiais educativos da jornada, devolvendo à paciente controle sobre o próprio tratamento. Nesta página eu detalho a API Django, que é onde está a maior parte da lógica do sistema, e dou uma visão rápida do front-end.
Arquitetura
O monorepo tem backend/, frontend/ e e2e/:
- Backend: Python 3.13 + Django 6.0.5 + Django REST Framework. Banco SQLite em desenvolvimento (o caminho vem de
DB_PATH) e autenticação por token (rest_framework.authtoken). Assets estáticos servidos com Whitenoise e o app Django hospedado num Azure App Service. - Frontend: React 19 + TypeScript + Vite + Tailwind, com TanStack Router (rotas em arquivo com
routeTree.gen.tsgerado) e componentes shadcn/ui. As telas (calendario,medicacoes,diario,copiloto,exames,videoteca,timeline,perfil) são SPA e consomem a API por umapi.tscentral. Deploy na Azure Static Web Apps. - e2e: 23 testes com Selenium + pytest simulando o usuário no navegador.
Modelos
O app Django tratamento concentra os modelos:
UserProfile, ligado 1:1 aoUser, com foto (validada por extensão), telefone, cidade e dados do tratamento (medica_responsavel,protocolo,data_inicio_tratamento).FaseTratamento, as etapas do tratamento comordem_cronologicae status (pendente, atual, concluída).EventoAgenda, consultas e exames com data, horário e local.Medicacao, com dose, datas ehorariosemJSONField(a lista de horários de cada remédio).Diario, check-ins comhumor,sintomasecondicoes_fisiologicasemJSONFielde notas livres.ConviteCopilotoeVinculoCopiloto, o modo co-piloto: um link com token convida um parceiro/familiar com permissões por área (perm_ver_fase,perm_ver_agenda,perm_ver_medicacoes,perm_receber_lembretes).
Um signal garante que todo usuário novo já nasça com perfil:
@receiver(post_save, sender=User)
def create_user_profile(sender, instance, created, **kwargs):
if created:
UserProfile.objects.create(user=instance)
Autenticação
O login é um endpoint próprio (/api/auth/login/) em vez do view padrão do Django REST, porque o login é por email. Ele acha o usuário pelo email (case-insensitive), autentica com a senha e devolve um token do authtoken:
user = User.objects.filter(email__iexact=email).first()
if user is None:
return Response({'detail': 'E-mail ou senha inválidos.'},
status=status.HTTP_401_UNAUTHORIZED)
authenticated_user = authenticate(request, username=user.username, password=password)
if authenticated_user is None:
return Response({'detail': 'E-mail ou senha inválidos.'},
status=status.HTTP_401_UNAUTHORIZED)
token, _ = Token.objects.get_or_create(user=authenticated_user)
return Response({'token': token.key, 'user': { ... }})
As views protegidas usam TokenAuthentication + IsAuthenticated, e o front manda o token no header.
As views da API
O perfil (UserProfileRetrieveUpdateAPIView) devolve o usuário com o perfil aninhado e, no PATCH/PUT, atualiza os dois: primeiro o User, depois o UserProfile criado sob demanda com get_or_create. A foto tem um endpoint separado que exige o arquivo em request.FILES.
O diário é o único recurso claramente por usuário: a view filtra pelos check-ins do usuário logado e salva com paciente=self.request.user:
class DiarioListCreate(generics.ListCreateAPIView):
serializer_class = DiarioSerializer
authentication_classes = [TokenAuthentication]
permission_classes = [permissions.IsAuthenticated]
def get_queryset(self):
return Diario.objects.filter(paciente=self.request.user)
def perform_create(self, serializer):
serializer.save(paciente=self.request.user)
Já agenda, fases e medicações usam queryset = Model.objects.all() sem autenticação e sem dono, como um conjunto global compartilhado:
class EventoAgendaListCreate(generics.ListCreateAPIView):
queryset = EventoAgenda.objects.all()
serializer_class = EventoAgendaSerializer
O convite de co-piloto devolve o token na resposta para o React montar o link (/convite/<token>), e o ConviteCopilotoSerializer valida o nome_identificador.
Configuração
O settings.py segue o padrão de 12-factor: SECRET_KEY, DEBUG, ALLOWED_HOSTS e CORS vêm de variáveis de ambiente com fallbacks para o desenvolvimento. O banco, porém, tem o engine fixo em SQLite, só trocando o caminho via DB_PATH:
_db_path = os.environ.get('DB_PATH', str(BASE_DIR / 'db.sqlite3'))
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.sqlite3',
'NAME': _db_path,
}
}
Os CSRF_TRUSTED_ORIGINS apontam para os domínios do backend e do front na Azure.
Testes, CI/CD e deploy
São 32 testes de API (Django APITestCase) e 23 testes E2E com Selenium. O GitHub Actions roda a suíte a cada push na main e automatiza o deploy: o front vai para a Azure Static Web Apps e o backend para o Azure App Service.
Particularidades e pontos que revisaria
-
Escopo e autorização inconsistentes entre recursos. O diário é filtrado pelo usuário autenticado, mas
FaseTratamento,EventoAgendaeMedicacaoexpõemobjects.all()sem autenticação e sem dono. Para uma plataforma individual, esses dados deveriam ser por paciente; hoje qualquer requisição (até sem login) lista tudo. O contraste entre as duas abordagens fica claro no código e é o que eu trataria primeiro. -
Convite de co-piloto com usuário “mock”. O
ConviteCopilotoCreateignora quem está logado e grava o convite paraUser.objects.first():def perform_create(self, serializer): usuario_teste = User.objects.first() # Mock user serializer.save(paciente=usuario_teste)É um atalho de desenvolvimento que ficou no código: em produção o convite não seria do paciente certo. Além disso, as permissões do co-piloto (
perm_ver_*) ficam no modelo, mas a API não tem endpoints de leitura que as apliquem, o vínculo e as permissões existem, só o controle fino por permissão não. -
SQLite também na “produção”. O README fala em banco Azure, mas o engine é SQLite em qualquer ambiente, só muda o caminho do arquivo. Numa hospedagem que recria o disco em redeploy, o arquivo pode ser perdido; um Postgres gerenciado evitaria esse risco.
-
Segredos com fallback arriscado. O
SECRET_KEYtem um valor fixo de fallback e oDEBUGéTruepor padrão quando a env não vem. Em produção as variáveis são setadas, mas subir o app sem elas deixa o servidor com debug ligado e chave conhecida. -
Token fixo sem expiração. O login usa
Token.objects.get_or_create, então o mesmo token vale para sempre, sem expiração nem rotação, e não há endpoint de logout que o invalide (só apagando na mão). Para um MVP passa, mas um JWT com expiração seria mais adequado. -
Media servida pelo próprio Django. A rota
re_path(r'^media/...', serve)serve os uploads (fotos de perfil) direto pelo Django; em produção é comum delegar isso a um storage externo. A validação da foto também checa só a extensão, não o conteúdo nem o tamanho. -
Dependências órfãs no
requirements.txt.django-ninjaepydanticaparecem listados, mas o código usa o Django REST Framework; provavelmente sobraram de uma tentativa anterior.
Pontos de melhoria
- Definir o dono de cada recurso (agenda, fases e medicações por paciente ou, se forem da clínica, deixar claro e proteger com permissão de staff).
- Ligar o convite de co-piloto ao usuário autenticado e implementar a leitura com as
perm_ver_*de verdade. - Migrar a “produção” para Postgres (o
requirements.txtnem tem o driver). - Tornar
SECRET_KEYeDEBUGobrigatórios via env (falhar se faltarem fora do dev). - Usar JWT com expiração/refresh e endpoint de logout, no lugar do token fixo.
- Servir os uploads por storage externo (blob) e validar conteúdo e tamanho da imagem.
Gabriel Rocha