feat: tela Minha conta para trocar a propria senha
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
c4079b3e47
commit
4a5c70861d
@@ -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),
|
||||||
|
},
|
||||||
|
)
|
||||||
@@ -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 %}
|
||||||
@@ -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
|
||||||
Reference in New Issue
Block a user