From 4a5c70861d8a8ea6e8fecd9068b7f4b4daded3f1 Mon Sep 17 00:00:00 2001 From: Gustavo Date: Thu, 13 Aug 2026 20:44:15 -0300 Subject: [PATCH] feat: tela Minha conta para trocar a propria senha Co-Authored-By: Claude Sonnet 5 --- app/routers/minha_conta.py | 75 ++++++++++++++++ app/templates/minha_conta.html | 31 +++++++ tests/test_router_minha_conta.py | 142 +++++++++++++++++++++++++++++++ 3 files changed, 248 insertions(+) create mode 100644 app/routers/minha_conta.py create mode 100644 app/templates/minha_conta.html create mode 100644 tests/test_router_minha_conta.py diff --git a/app/routers/minha_conta.py b/app/routers/minha_conta.py new file mode 100644 index 0000000..d86cfc7 --- /dev/null +++ b/app/routers/minha_conta.py @@ -0,0 +1,75 @@ +""" +Tela "Minha conta" — qualquer usuário logado (admin ou padrão) troca a +própria senha aqui, sem precisar de um admin. Pede a senha atual antes de +trocar, mesmo já estando logado (proteção extra contra sessão esquecida +aberta em máquina compartilhada). + +A conta de bootstrap (ADMIN_USER/ADMIN_PASSWORD do .env) não tem linha em +FUNCIONARIO_PAINEL — não há senha para trocar pelo painel; a tela avisa +que a troca é editando o .env do servidor. +""" +from fastapi import APIRouter, Depends, Form, Request +from fastapi.responses import HTMLResponse +from fastapi.templating import Jinja2Templates + +from ..auth import UsuarioLogado, exigir_login +from ..csrf import exigir_csrf, token_do_contexto +from ..db import funcionarios as db_funcionarios +from ..db import log as db_log +from ..senha import verificar_senha + +router = APIRouter() +templates = Jinja2Templates(directory="app/templates") + + +@router.get("/minha-conta", response_class=HTMLResponse) +def form_minha_conta(request: Request, usuario: UsuarioLogado = Depends(exigir_login)): + funcionario = db_funcionarios.obter_por_login(usuario.login) + return templates.TemplateResponse( + request, + "minha_conta.html", + { + "usuario_atual": usuario, "eh_conta_env": funcionario is None, + "csrf_token": token_do_contexto(request), + }, + ) + + +@router.post("/minha-conta/senha", dependencies=[Depends(exigir_csrf)]) +def trocar_senha( + request: Request, + senha_atual: str = Form(...), + nova_senha: str = Form(...), + confirmar_nova_senha: str = Form(...), + usuario: UsuarioLogado = Depends(exigir_login), +): + funcionario = db_funcionarios.obter_por_login(usuario.login) + + erro = None + if funcionario is None: + erro = "Esta conta é configurada por variável de ambiente (.env) — troque a senha lá, não por aqui." + elif not verificar_senha(senha_atual, funcionario["SENHA_HASH"]): + erro = "A senha atual digitada não confere." + elif not nova_senha or nova_senha != confirmar_nova_senha: + erro = "A nova senha e a confirmação não coincidem." + + if erro: + return templates.TemplateResponse( + request, + "minha_conta.html", + { + "usuario_atual": usuario, "eh_conta_env": funcionario is None, "erro": erro, + "csrf_token": token_do_contexto(request), + }, + ) + + db_funcionarios.redefinir_senha(funcionario["ID"], nova_senha) + db_log.registrar(str(usuario), "trocar_senha", "funcionario", str(funcionario["ID"]), True) + return templates.TemplateResponse( + request, + "minha_conta.html", + { + "usuario_atual": usuario, "eh_conta_env": False, "mensagem": "Senha alterada com sucesso.", + "csrf_token": token_do_contexto(request), + }, + ) diff --git a/app/templates/minha_conta.html b/app/templates/minha_conta.html new file mode 100644 index 0000000..f594a28 --- /dev/null +++ b/app/templates/minha_conta.html @@ -0,0 +1,31 @@ + +{% extends "base.html" %} +{% block titulo %}Minha conta{% endblock %} +{% block conteudo %} +

Minha conta

+

Nome: {{ usuario_atual.nome }}

+

Login: {{ usuario_atual.login }}

+ + {% if erro %}
{{ erro }}
{% endif %} + {% if mensagem %}
{{ mensagem }}
{% endif %} + + {% if eh_conta_env %} +
+ Esta conta é a conta de administrador configurada por variável de ambiente + (ADMIN_USER/ADMIN_PASSWORD no .env do servidor) — a senha se troca lá, + não existe uma linha correspondente na tabela de funcionários. +
+ {% else %} +

Trocar senha

+
+ + + + + + + + +
+ {% endif %} +{% endblock %} diff --git a/tests/test_router_minha_conta.py b/tests/test_router_minha_conta.py new file mode 100644 index 0000000..19a872a --- /dev/null +++ b/tests/test_router_minha_conta.py @@ -0,0 +1,142 @@ +import pytest +from fastapi import FastAPI +from fastapi.testclient import TestClient + +from app.auth import UsuarioLogado, exigir_login +from app.routers import minha_conta as router_minha_conta + +app_teste = FastAPI() +app_teste.include_router(router_minha_conta.router) +client = TestClient(app_teste, follow_redirects=False) +CSRF_TESTE = "teste-csrf-token" +client.cookies.set("csrf_token", CSRF_TESTE) + + +@pytest.fixture(autouse=True) +def _sem_log_real(monkeypatch): + monkeypatch.setattr(router_minha_conta.db_log, "registrar", lambda *args, **kwargs: None) + + +def _logar_como(login: str, nome: str, perfil: str = "PADRAO"): + app_teste.dependency_overrides[exigir_login] = lambda: UsuarioLogado(login=login, nome=nome, perfil=perfil) + + +def test_get_minha_conta_mostra_nome_e_login(monkeypatch): + _logar_como("fulano", "Fulano da Silva") + monkeypatch.setattr( + router_minha_conta.db_funcionarios, + "obter_por_login", + lambda login: {"ID": 1, "NOME": "Fulano da Silva", "LOGIN": "fulano", "SENHA_HASH": "x", "PERFIL": "PADRAO", "ATIVO": 1}, + ) + resposta = client.get("/minha-conta") + assert resposta.status_code == 200 + assert "Fulano da Silva" in resposta.text + assert "fulano" in resposta.text + + +def test_get_minha_conta_admin_env_mostra_aviso(monkeypatch): + import os + + monkeypatch.setenv("ADMIN_USER", "admin") + _logar_como("admin", "admin") + monkeypatch.setattr(router_minha_conta.db_funcionarios, "obter_por_login", lambda login: None) + resposta = client.get("/minha-conta") + assert resposta.status_code == 200 + assert ".env" in resposta.text + + +def test_post_trocar_senha_com_sucesso(monkeypatch): + _logar_como("fulano", "Fulano da Silva") + monkeypatch.setattr( + router_minha_conta.db_funcionarios, + "obter_por_login", + lambda login: {"ID": 1, "NOME": "Fulano da Silva", "LOGIN": "fulano", "SENHA_HASH": "hash-atual", "PERFIL": "PADRAO", "ATIVO": 1}, + ) + monkeypatch.setattr(router_minha_conta, "verificar_senha", lambda senha, hash_: True) + capturado = {} + monkeypatch.setattr( + router_minha_conta.db_funcionarios, + "redefinir_senha", + lambda fid, nova: capturado.setdefault("dados", (fid, nova)), + ) + + resposta = client.post( + "/minha-conta/senha", + data={ + "senha_atual": "senhaAtual123", "nova_senha": "senhaNova456", "confirmar_nova_senha": "senhaNova456", + "csrf_token": CSRF_TESTE, + }, + follow_redirects=True, + ) + assert resposta.status_code == 200 + assert capturado["dados"] == (1, "senhaNova456") + + +def test_post_trocar_senha_atual_errada_bloqueia(monkeypatch): + _logar_como("fulano", "Fulano da Silva") + monkeypatch.setattr( + router_minha_conta.db_funcionarios, + "obter_por_login", + lambda login: {"ID": 1, "NOME": "Fulano da Silva", "LOGIN": "fulano", "SENHA_HASH": "hash-atual", "PERFIL": "PADRAO", "ATIVO": 1}, + ) + monkeypatch.setattr(router_minha_conta, "verificar_senha", lambda senha, hash_: False) + chamou = {} + monkeypatch.setattr( + router_minha_conta.db_funcionarios, "redefinir_senha", lambda fid, nova: chamou.setdefault("ok", True) + ) + + resposta = client.post( + "/minha-conta/senha", + data={ + "senha_atual": "senhaErrada", "nova_senha": "senhaNova456", "confirmar_nova_senha": "senhaNova456", + "csrf_token": CSRF_TESTE, + }, + ) + assert resposta.status_code == 200 + assert "senha atual" in resposta.text.lower() + assert "ok" not in chamou + + +def test_post_trocar_senha_confirmacao_nao_bate_bloqueia(monkeypatch): + _logar_como("fulano", "Fulano da Silva") + monkeypatch.setattr( + router_minha_conta.db_funcionarios, + "obter_por_login", + lambda login: {"ID": 1, "NOME": "Fulano da Silva", "LOGIN": "fulano", "SENHA_HASH": "hash-atual", "PERFIL": "PADRAO", "ATIVO": 1}, + ) + monkeypatch.setattr(router_minha_conta, "verificar_senha", lambda senha, hash_: True) + chamou = {} + monkeypatch.setattr( + router_minha_conta.db_funcionarios, "redefinir_senha", lambda fid, nova: chamou.setdefault("ok", True) + ) + + resposta = client.post( + "/minha-conta/senha", + data={ + "senha_atual": "senhaAtual123", "nova_senha": "senhaNova456", "confirmar_nova_senha": "outraCoisa", + "csrf_token": CSRF_TESTE, + }, + ) + assert resposta.status_code == 200 + assert "não coincidem" in resposta.text + assert "ok" not in chamou + + +def test_post_trocar_senha_admin_env_bloqueia(monkeypatch): + _logar_como("admin", "admin") + monkeypatch.setattr(router_minha_conta.db_funcionarios, "obter_por_login", lambda login: None) + chamou = {} + monkeypatch.setattr( + router_minha_conta.db_funcionarios, "redefinir_senha", lambda fid, nova: chamou.setdefault("ok", True) + ) + + resposta = client.post( + "/minha-conta/senha", + data={ + "senha_atual": "qualquer", "nova_senha": "senhaNova456", "confirmar_nova_senha": "senhaNova456", + "csrf_token": CSRF_TESTE, + }, + ) + assert resposta.status_code == 200 + assert ".env" in resposta.text + assert "ok" not in chamou