feat: tela Minha conta para trocar a propria senha

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Gustavo
2026-08-13 20:44:15 -03:00
co-authored by Claude Sonnet 5
parent c4079b3e47
commit 4a5c70861d
3 changed files with 248 additions and 0 deletions
+75
View File
@@ -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),
},
)
+31
View File
@@ -0,0 +1,31 @@
<!-- app/templates/minha_conta.html -->
{% extends "base.html" %}
{% block titulo %}Minha conta{% endblock %}
{% block conteudo %}
<h1>Minha conta</h1>
<p><strong>Nome:</strong> {{ usuario_atual.nome }}</p>
<p><strong>Login:</strong> {{ usuario_atual.login }}</p>
{% if erro %}<div class="mensagem danger">{{ erro }}</div>{% endif %}
{% if mensagem %}<div class="mensagem success">{{ mensagem }}</div>{% endif %}
{% if eh_conta_env %}
<div class="mensagem warning">
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.
</div>
{% else %}
<h2>Trocar senha</h2>
<form method="post" action="/minha-conta/senha">
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
<label for="senha_atual">Senha atual</label>
<input type="password" id="senha_atual" name="senha_atual" required>
<label for="nova_senha">Nova senha</label>
<input type="password" id="nova_senha" name="nova_senha" required>
<label for="confirmar_nova_senha">Confirmar nova senha</label>
<input type="password" id="confirmar_nova_senha" name="confirmar_nova_senha" required>
<button type="submit">Salvar nova senha</button>
</form>
{% endif %}
{% endblock %}
+142
View File
@@ -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